Make meters with different colour parameters actually work.
[ardour.git] / libs / gtkmm2ext / fastmeter.cc
1 /*
2     Copyright (C) 2003-2006 Paul Davis
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18     $Id$
19 */
20
21 #include <iostream>
22 #include <cmath>
23 #include <algorithm>
24 #include <cstring>
25
26 #include <gdkmm/rectangle.h>
27 #include <gtkmm2ext/fastmeter.h>
28 #include <gtkmm2ext/utils.h>
29
30 #define UINT_TO_RGB(u,r,g,b) { (*(r)) = ((u)>>16)&0xff; (*(g)) = ((u)>>8)&0xff; (*(b)) = (u)&0xff; }
31 #define UINT_TO_RGBA(u,r,g,b,a) { UINT_TO_RGB(((u)>>8),r,g,b); (*(a)) = (u)&0xff; }
32 using namespace Gtk;
33 using namespace Gdk;
34 using namespace Glib;
35 using namespace Gtkmm2ext;
36 using namespace std;
37
38 int FastMeter::min_pattern_metric_size = 10;
39 int FastMeter::max_pattern_metric_size = 1024;
40
41 FastMeter::PatternMap FastMeter::v_pattern_cache;
42 FastMeter::PatternMap FastMeter::h_pattern_cache;
43
44 FastMeter::FastMeter (long hold, unsigned long dimen, Orientation o, int len, int clr0, int clr1, int clr2, int clr3)
45 {
46         orientation = o;
47         hold_cnt = hold;
48         hold_state = 0;
49         current_peak = 0;
50         current_level = 0;
51         last_peak_rect.width = 0;
52         last_peak_rect.height = 0;
53         _clr0 = clr0;
54         _clr1 = clr1;
55         _clr2 = clr2;
56         _clr3 = clr3;
57
58         set_events (BUTTON_PRESS_MASK|BUTTON_RELEASE_MASK);
59
60         pixrect.x = 0;
61         pixrect.y = 0;
62
63         if (orientation == Vertical) {
64                 if (!len) {
65                         len = 250;
66                 }
67                 pattern = request_vertical_meter(dimen, len, clr0, clr1, clr2, clr3);
68                 pixheight = len;
69                 pixwidth = dimen;
70         } else {
71                 if (!len) {
72                         len = 186; // interesting size, eh?
73                 }
74                 pattern = request_horizontal_meter(len, dimen, clr0, clr1, clr2, clr3);
75                 pixheight = dimen;
76                 pixwidth = len;
77         }
78
79         if (orientation == Vertical) {
80                 pixrect.width = min (pixwidth, (gint) dimen);
81                 pixrect.height = pixheight;
82         } else {
83                 pixrect.width = pixwidth;
84                 pixrect.height = min (pixheight, (gint) dimen);
85         }
86
87         request_width = pixrect.width;
88         request_height= pixrect.height;
89 }
90
91 Cairo::RefPtr<Cairo::Pattern>
92 FastMeter::generate_meter_pattern (
93                 int width, int height, int clr0, int clr1, int clr2, int clr3)
94 {
95         guint8 r0,g0,b0,r1,g1,b1,r2,g2,b2,r3,g3,b3,a;
96
97         /* clr0: color at top of the meter
98               1: color at the knee
99               2: color half-way between bottom and knee
100               3: color at the bottom of the meter
101         */
102
103         UINT_TO_RGBA (clr0, &r0, &g0, &b0, &a);
104         UINT_TO_RGBA (clr1, &r1, &g1, &b1, &a);
105         UINT_TO_RGBA (clr2, &r2, &g2, &b2, &a);
106         UINT_TO_RGBA (clr3, &r3, &g3, &b3, &a);
107
108         // fake log calculation copied from log_meter.h
109         // actual calculation:
110         // log_meter(0.0f) =
111         //  def = (0.0f + 20.0f) * 2.5f + 50f
112         //  return def / 115.0f
113
114         const int knee = (int)floor((float)height * 100.0f / 115.0f);
115         cairo_pattern_t* _p = cairo_pattern_create_linear (0.0, 0.0, width, height);
116
117         /* cairo coordinate space goes downwards as y value goes up, so invert
118          * knee-based positions by using (1.0 - y)
119          *
120          * also, double-stop the knee point, so that we get a hard transition
121          */
122
123         cairo_pattern_add_color_stop_rgb (_p, 0.0, r3/255.0, g3/255.0, b3/255.0); // bottom
124         cairo_pattern_add_color_stop_rgb (_p, 1.0 - (knee/(2.0 * height)), r2/255.0, g2/255.0, b2/255.0); // mid-point to knee
125         cairo_pattern_add_color_stop_rgb (_p, 1.0 - (knee/(double)height), r0/255.0, g0/255.0, b0/255.0); // knee
126         cairo_pattern_add_color_stop_rgb (_p, 1.0 - (knee/(double)height), r1/255.0, g1/255.0, b1/255.0); // double-stop @ knee
127         cairo_pattern_add_color_stop_rgb (_p, 1.0, r0/255.0, g0/255.0, b0/255.0); // top
128
129         Cairo::RefPtr<Cairo::Pattern> p (new Cairo::Pattern (_p, false));
130
131         return p;
132 }
133
134 Cairo::RefPtr<Cairo::Pattern>
135 FastMeter::request_vertical_meter(
136                 int width, int height, int clr0, int clr1, int clr2, int clr3)
137 {
138         if (height < min_pattern_metric_size)
139                 height = min_pattern_metric_size;
140         if (height > max_pattern_metric_size)
141                 height = max_pattern_metric_size;
142
143         const PatternMapKey key (width, height, clr0, clr1, clr2, clr3);
144         PatternMap::iterator i;
145         if ((i = v_pattern_cache.find (key)) != v_pattern_cache.end()) {
146                 return i->second;
147         }
148
149         Cairo::RefPtr<Cairo::Pattern> p = generate_meter_pattern (
150                 width, height, clr0, clr1, clr2, clr3);
151         v_pattern_cache[key] = p;
152
153         return p;
154 }
155
156 Cairo::RefPtr<Cairo::Pattern>
157 FastMeter::request_horizontal_meter(
158                 int width, int height, int clr0, int clr1, int clr2, int clr3)
159 {
160         if (width < min_pattern_metric_size)
161                 width = min_pattern_metric_size;
162         if (width > max_pattern_metric_size)
163                 width = max_pattern_metric_size;
164
165         const PatternMapKey key (width, height, clr0, clr1, clr2, clr3);
166         PatternMap::iterator i;
167         if ((i = h_pattern_cache.find (key)) != h_pattern_cache.end()) {
168                 return i->second;
169         }
170
171         /* flip height/width so that we get the right pattern */
172
173         Cairo::RefPtr<Cairo::Pattern> p = generate_meter_pattern (
174                 height, width, clr0, clr1, clr2, clr3);
175
176         /* rotate to make it horizontal */
177
178         cairo_matrix_t m;
179         cairo_matrix_init_rotate (&m, -M_PI/2.0);
180         cairo_pattern_set_matrix (p->cobj(), &m);
181
182         h_pattern_cache[key] = p;
183
184         return p;
185 }
186
187 FastMeter::~FastMeter ()
188 {
189 }
190
191 void
192 FastMeter::set_hold_count (long val)
193 {
194         if (val < 1) {
195                 val = 1;
196         }
197
198         hold_cnt = val;
199         hold_state = 0;
200         current_peak = 0;
201
202         queue_draw ();
203 }
204
205 void
206 FastMeter::on_size_request (GtkRequisition* req)
207 {
208         if (orientation == Vertical) {
209
210                 req->height = request_height;
211                 req->height = max(req->height, min_pattern_metric_size);
212                 req->height = min(req->height, max_pattern_metric_size);
213
214                 req->width  = request_width;
215
216         } else {
217
218                 req->width  = request_width;
219                 req->width  = max(req->width,  min_pattern_metric_size);
220                 req->width  = min(req->width,  max_pattern_metric_size);
221
222                 req->height = request_height;
223         }
224
225 }
226
227 void
228 FastMeter::on_size_allocate (Gtk::Allocation &alloc)
229 {
230         if (orientation == Vertical) {
231
232                 if (alloc.get_width() != request_width) {
233                         alloc.set_width (request_width);
234                 }
235
236                 int h = alloc.get_height();
237                 h = max (h, min_pattern_metric_size);
238                 h = min (h, max_pattern_metric_size);
239
240                 if (h != alloc.get_height()) {
241                         alloc.set_height (h);
242                 }
243
244                 if (pixheight != h) {
245                         pattern = request_vertical_meter (
246                                 request_width, h, _clr0, _clr1, _clr2, _clr3);
247                         pixheight = h;
248                         pixwidth  = request_width;
249                 }
250
251         } else {
252
253                 if (alloc.get_height() != request_height) {
254                         alloc.set_height(request_height);
255                 }
256
257                 int w = alloc.get_width();
258                 w = max (w, min_pattern_metric_size);
259                 w = min (w, max_pattern_metric_size);
260
261                 if (w != alloc.get_width()) {
262                         alloc.set_width (w);
263                 }
264
265                 if (pixwidth != w) {
266                         pattern = request_horizontal_meter (
267                                 w, request_height, _clr0, _clr1, _clr2, _clr3);
268                         pixheight = request_height;
269                         pixwidth  = w;
270                 }
271         }
272
273         DrawingArea::on_size_allocate (alloc);
274 }
275
276 bool
277 FastMeter::on_expose_event (GdkEventExpose* ev)
278 {
279         if (orientation == Vertical) {
280                 return vertical_expose (ev);
281         } else {
282                 return horizontal_expose (ev);
283         }
284 }
285
286 bool
287 FastMeter::vertical_expose (GdkEventExpose* ev)
288 {
289         Glib::RefPtr<Gdk::Window> win = get_window ();
290         gint top_of_meter;
291         GdkRectangle intersection;
292         GdkRectangle background;
293
294         cairo_t* cr = gdk_cairo_create (get_window ()->gobj());
295         cairo_rectangle (cr, ev->area.x, ev->area.y, ev->area.width, ev->area.height);
296         cairo_clip (cr);
297
298         top_of_meter = (gint) floor (pixheight * current_level);
299
300         /* reset the height & origin of the rect that needs to show the pixbuf
301          */
302
303         pixrect.height = top_of_meter;
304         pixrect.y = pixheight - top_of_meter;
305
306         background.x = 0;
307         background.y = 0;
308         background.width = pixrect.width;
309         background.height = pixheight - top_of_meter;
310
311         if (gdk_rectangle_intersect (&background, &ev->area, &intersection)) {
312                 cairo_set_source_rgb (cr, 0, 0, 0); // black
313                 cairo_rectangle (cr, intersection.x, intersection.y, intersection.width, intersection.height);
314                 cairo_fill (cr);
315         }
316
317         if (gdk_rectangle_intersect (&pixrect, &ev->area, &intersection)) {
318                 // draw the part of the meter image that we need. the area we draw is bounded "in reverse" (top->bottom)
319                 cairo_set_source (cr, pattern->cobj());
320                 cairo_rectangle (cr, intersection.x, intersection.y, intersection.width, intersection.height);
321                 cairo_fill (cr);
322         }
323
324         // draw peak bar
325
326         if (hold_state) {
327                 last_peak_rect.x = 0;
328                 last_peak_rect.width = pixwidth;
329                 last_peak_rect.y = pixheight - (gint) floor (pixheight * current_peak);
330                 last_peak_rect.height = min(3, pixheight - last_peak_rect.y);
331
332                 cairo_set_source (cr, pattern->cobj());
333                 cairo_rectangle (cr, 0, last_peak_rect.y, pixwidth, last_peak_rect.height);
334                 cairo_fill (cr);
335
336         } else {
337                 last_peak_rect.width = 0;
338                 last_peak_rect.height = 0;
339         }
340
341         cairo_destroy (cr);
342
343         return TRUE;
344 }
345
346 bool
347 FastMeter::horizontal_expose (GdkEventExpose* ev)
348 {
349         Glib::RefPtr<Gdk::Window> win = get_window ();
350         gint right_of_meter;
351         GdkRectangle intersection;
352         GdkRectangle background;
353
354         cairo_t* cr = gdk_cairo_create (get_window ()->gobj());
355         cairo_rectangle (cr, ev->area.x, ev->area.y, ev->area.width, ev->area.height);
356         cairo_clip (cr);
357
358         right_of_meter = (gint) floor (pixwidth * current_level);
359         pixrect.width = right_of_meter;
360
361         background.x = 0;
362         background.y = 0;
363         background.width  = pixwidth - right_of_meter;
364         background.height = pixrect.height;
365
366         if (gdk_rectangle_intersect (&background, &ev->area, &intersection)) {
367                 cairo_set_source_rgb (cr, 0, 0, 0); // black
368                 cairo_rectangle (cr, intersection.x + right_of_meter, intersection.y, intersection.width, intersection.height);
369                 cairo_fill (cr);
370         }
371
372         if (gdk_rectangle_intersect (&pixrect, &ev->area, &intersection)) {
373                 // draw the part of the meter image that we need. the area we draw is bounded "in reverse" (top->bottom)
374                 cairo_matrix_t m;
375                 cairo_matrix_init_translate (&m, -intersection.x, -intersection.y);
376                 cairo_pattern_set_matrix (pattern->cobj(), &m);
377                 cairo_set_source (cr, pattern->cobj());
378                 cairo_rectangle (cr, intersection.x, intersection.y, pixrect.width, intersection.height);
379                 cairo_fill (cr);
380         }
381
382         // draw peak bar
383         // XXX: peaks don't work properly
384         /*
385         if (hold_state && intersection.height > 0) {
386                 gint x = (gint) floor(pixwidth * current_peak);
387
388                 get_window()->draw_pixbuf (get_style()->get_fg_gc(get_state()), pixbuf,
389                                            x, intersection.y,
390                                            x, intersection.y,
391                                            3, intersection.height,
392                                            Gdk::RGB_DITHER_NONE, 0, 0);
393         }
394         */
395
396         cairo_destroy (cr);
397
398         return true;
399 }
400
401 void
402 FastMeter::set (float lvl)
403 {
404         float old_level = current_level;
405         float old_peak = current_peak;
406
407         current_level = lvl;
408
409         if (lvl > current_peak) {
410                 current_peak = lvl;
411                 hold_state = hold_cnt;
412         }
413
414         if (hold_state > 0) {
415                 if (--hold_state == 0) {
416                         current_peak = lvl;
417                 }
418         }
419
420         if (current_level == old_level && current_peak == old_peak && hold_state == 0) {
421                 return;
422         }
423
424
425         Glib::RefPtr<Gdk::Window> win;
426
427         if ((win = get_window()) == 0) {
428                 queue_draw ();
429                 return;
430         }
431
432         if (orientation == Vertical) {
433                 queue_vertical_redraw (win, old_level);
434         } else {
435                 queue_horizontal_redraw (win, old_level);
436         }
437 }
438
439 void
440 FastMeter::queue_vertical_redraw (const Glib::RefPtr<Gdk::Window>& win, float old_level)
441 {
442         GdkRectangle rect;
443
444         gint new_top = (gint) floor (pixheight * current_level);
445
446         rect.x = 0;
447         rect.width = pixwidth;
448         rect.height = new_top;
449         rect.y = pixheight - new_top;
450
451         if (current_level > old_level) {
452                 /* colored/pixbuf got larger, just draw the new section */
453                 /* rect.y stays where it is because of X coordinates */
454                 /* height of invalidated area is between new.y (smaller) and old.y
455                    (larger).
456                    X coordinates just make my brain hurt.
457                 */
458                 rect.height = pixrect.y - rect.y;
459         } else {
460                 /* it got smaller, compute the difference */
461                 /* rect.y becomes old.y (the smaller value) */
462                 rect.y = pixrect.y;
463                 /* rect.height is the old.y (smaller) minus the new.y (larger)
464                 */
465                 rect.height = pixrect.height - rect.height;
466         }
467
468         GdkRegion* region = 0;
469         bool queue = false;
470
471         if (rect.height != 0) {
472
473                 /* ok, first region to draw ... */
474
475                 region = gdk_region_rectangle (&rect);
476                 queue = true;
477         }
478
479         /* redraw the last place where the last peak hold bar was;
480            the next expose will draw the new one whether its part of
481            expose region or not.
482         */
483
484         if (last_peak_rect.width * last_peak_rect.height != 0) {
485                 if (!queue) {
486                         region = gdk_region_new ();
487                         queue = true;
488                 }
489                 gdk_region_union_with_rect (region, &last_peak_rect);
490         }
491
492         if (queue) {
493                 gdk_window_invalidate_region (win->gobj(), region, true);
494         }
495         if (region) {
496                 gdk_region_destroy(region);
497                 region = 0;
498         }
499 }
500
501 void
502 FastMeter::queue_horizontal_redraw (const Glib::RefPtr<Gdk::Window>& /*win*/, float /*old_level*/)
503 {
504         /* XXX OPTIMIZE (when we have some horizontal meters) */
505         queue_draw ();
506 }
507
508 void
509 FastMeter::clear ()
510 {
511         current_level = 0;
512         current_peak = 0;
513         hold_state = 0;
514         queue_draw ();
515 }