Merge branch 'master' into windows
[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 <stdlib.h>
27
28 #include <gdkmm/rectangle.h>
29 #include <gtkmm2ext/fastmeter.h>
30 #include <gtkmm2ext/utils.h>
31
32 #define UINT_TO_RGB(u,r,g,b) { (*(r)) = ((u)>>16)&0xff; (*(g)) = ((u)>>8)&0xff; (*(b)) = (u)&0xff; }
33 #define UINT_TO_RGBA(u,r,g,b,a) { UINT_TO_RGB(((u)>>8),r,g,b); (*(a)) = (u)&0xff; }
34 using namespace Gtk;
35 using namespace Gdk;
36 using namespace Glib;
37 using namespace Gtkmm2ext;
38 using namespace std;
39
40 int FastMeter::min_pattern_metric_size = 16;
41 int FastMeter::max_pattern_metric_size = 1024;
42 bool FastMeter::no_rgba_overlay = false;
43
44 FastMeter::Pattern10Map FastMeter::vm_pattern_cache;
45 FastMeter::PatternBgMap FastMeter::vb_pattern_cache;
46
47 FastMeter::FastMeter (long hold, unsigned long dimen, Orientation o, int len,
48                 int clr0, int clr1, int clr2, int clr3,
49                 int clr4, int clr5, int clr6, int clr7,
50                 int clr8, int clr9,
51                 int bgc0, int bgc1,
52                 int bgh0, int bgh1,
53                 float stp0, float stp1,
54                 float stp2, float stp3
55                 )
56 {
57         orientation = o;
58         hold_cnt = hold;
59         hold_state = 0;
60         bright_hold = false;
61         current_peak = 0;
62         current_level = 0;
63         last_peak_rect.width = 0;
64         last_peak_rect.height = 0;
65
66         highlight = false;
67         no_rgba_overlay = ! Glib::getenv("NO_METER_SHADE").empty();
68
69         _clr[0] = clr0;
70         _clr[1] = clr1;
71         _clr[2] = clr2;
72         _clr[3] = clr3;
73         _clr[4] = clr4;
74         _clr[5] = clr5;
75         _clr[6] = clr6;
76         _clr[7] = clr7;
77         _clr[8] = clr8;
78         _clr[9] = clr9;
79
80         _bgc[0] = bgc0;
81         _bgc[1] = bgc1;
82
83         _bgh[0] = bgh0;
84         _bgh[1] = bgh1;
85
86         _stp[0] = stp0;
87         _stp[1] = stp1;
88         _stp[2] = stp2;
89         _stp[3] = stp3;
90
91         set_events (BUTTON_PRESS_MASK|BUTTON_RELEASE_MASK);
92
93         pixrect.x = 1;
94         pixrect.y = 1;
95
96         if (!len) {
97                 len = 250;
98         }
99         fgpattern = request_vertical_meter(dimen, len, _clr, _stp, true);
100         bgpattern = request_vertical_background (dimen, len, _bgc, false);
101         pixheight = len;
102         pixwidth = dimen;
103
104         pixrect.width = pixwidth;
105         pixrect.height = pixheight;
106
107         request_width = pixrect.width + 2;
108         request_height= pixrect.height + 2;
109
110         queue_draw ();
111 }
112
113 FastMeter::~FastMeter ()
114 {
115 }
116
117 Cairo::RefPtr<Cairo::Pattern>
118 FastMeter::generate_meter_pattern (
119                 int width, int height, int *clr, float *stp, bool shade)
120 {
121         guint8 r,g,b,a;
122         double knee;
123         double soft = 1.5 / (double) height;
124
125         cairo_pattern_t* pat = cairo_pattern_create_linear (0.0, 0.0, 0.0, height);
126
127         /*
128           Cairo coordinate space goes downwards as y value goes up, so invert
129           knee-based positions by using (1.0 - y)
130         */
131
132         UINT_TO_RGBA (clr[9], &r, &g, &b, &a); // top/clip
133         cairo_pattern_add_color_stop_rgb (pat, 0.0,
134                                           r/255.0, g/255.0, b/255.0);
135
136         knee = ((float)height * stp[3] / 115.0f); // -0dB
137
138         UINT_TO_RGBA (clr[8], &r, &g, &b, &a);
139         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) - soft,
140                                           r/255.0, g/255.0, b/255.0);
141
142         UINT_TO_RGBA (clr[7], &r, &g, &b, &a);
143         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) + soft,
144                                           r/255.0, g/255.0, b/255.0);
145
146         knee = ((float)height * stp[2]/ 115.0f); // -3dB || -2dB
147
148         UINT_TO_RGBA (clr[6], &r, &g, &b, &a);
149         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) - soft,
150                                           r/255.0, g/255.0, b/255.0);
151
152         UINT_TO_RGBA (clr[5], &r, &g, &b, &a);
153         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) + soft,
154                                           r/255.0, g/255.0, b/255.0);
155
156         knee = ((float)height * stp[1] / 115.0f); // -9dB
157
158         UINT_TO_RGBA (clr[4], &r, &g, &b, &a);
159         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) - soft,
160                                           r/255.0, g/255.0, b/255.0);
161
162         UINT_TO_RGBA (clr[3], &r, &g, &b, &a);
163         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) + soft,
164                                           r/255.0, g/255.0, b/255.0);
165
166         knee = ((float)height * stp[0] / 115.0f); // -18dB
167
168         UINT_TO_RGBA (clr[2], &r, &g, &b, &a);
169         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) - soft,
170                                           r/255.0, g/255.0, b/255.0);
171
172         UINT_TO_RGBA (clr[1], &r, &g, &b, &a);
173         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) + soft,
174                                           r/255.0, g/255.0, b/255.0);
175
176         UINT_TO_RGBA (clr[0], &r, &g, &b, &a); // bottom
177         cairo_pattern_add_color_stop_rgb (pat, 1.0,
178                                           r/255.0, g/255.0, b/255.0);
179
180         if (shade && !no_rgba_overlay) {
181                 cairo_pattern_t* shade_pattern = cairo_pattern_create_linear (0.0, 0.0, width, 0.0);
182                 cairo_pattern_add_color_stop_rgba (shade_pattern, 0, 1.0, 1.0, 1.0, 0.2);
183                 cairo_pattern_add_color_stop_rgba (shade_pattern, 1, 0.0, 0.0, 0.0, 0.3);
184
185                 cairo_surface_t* surface;
186                 cairo_t* tc = 0;
187                 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
188                 tc = cairo_create (surface);
189                 cairo_set_source (tc, pat);
190                 cairo_rectangle (tc, 0, 0, width, height);
191                 cairo_fill (tc);
192                 cairo_set_source (tc, shade_pattern);
193                 cairo_rectangle (tc, 0, 0, width, height);
194                 cairo_fill (tc);
195
196                 cairo_pattern_destroy (pat);
197                 cairo_pattern_destroy (shade_pattern);
198
199                 pat = cairo_pattern_create_for_surface (surface);
200
201                 cairo_destroy (tc);
202                 cairo_surface_destroy (surface);
203         }
204
205         Cairo::RefPtr<Cairo::Pattern> p (new Cairo::Pattern (pat, false));
206
207         return p;
208 }
209
210
211 Cairo::RefPtr<Cairo::Pattern>
212 FastMeter::generate_meter_background (
213                 int width, int height, int *clr, bool shade)
214 {
215         guint8 r0,g0,b0,r1,g1,b1,a;
216
217         cairo_pattern_t* pat = cairo_pattern_create_linear (0.0, 0.0, 0.0, height);
218
219         UINT_TO_RGBA (clr[0], &r0, &g0, &b0, &a);
220         UINT_TO_RGBA (clr[1], &r1, &g1, &b1, &a);
221
222         cairo_pattern_add_color_stop_rgb (pat, 0.0,
223                                           r1/255.0, g1/255.0, b1/255.0);
224
225         cairo_pattern_add_color_stop_rgb (pat, 1.0,
226                                           r0/255.0, g0/255.0, b0/255.0);
227
228         if (shade && !no_rgba_overlay) {
229                 cairo_pattern_t* shade_pattern = cairo_pattern_create_linear (0.0, 0.0, width, 0.0);
230                 cairo_pattern_add_color_stop_rgba (shade_pattern, 0.0, 1.0, 1.0, 1.0, 0.15);
231                 cairo_pattern_add_color_stop_rgba (shade_pattern, 0.6, 0.0, 0.0, 0.0, 0.10);
232                 cairo_pattern_add_color_stop_rgba (shade_pattern, 1.0, 1.0, 1.0, 1.0, 0.20);
233
234                 cairo_surface_t* surface;
235                 cairo_t* tc = 0;
236                 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
237                 tc = cairo_create (surface);
238                 cairo_set_source (tc, pat);
239                 cairo_rectangle (tc, 0, 0, width, height);
240                 cairo_fill (tc);
241                 cairo_set_source (tc, shade_pattern);
242                 cairo_rectangle (tc, 0, 0, width, height);
243                 cairo_fill (tc);
244
245                 cairo_pattern_destroy (pat);
246                 cairo_pattern_destroy (shade_pattern);
247
248                 pat = cairo_pattern_create_for_surface (surface);
249
250                 cairo_destroy (tc);
251                 cairo_surface_destroy (surface);
252         }
253
254         Cairo::RefPtr<Cairo::Pattern> p (new Cairo::Pattern (pat, false));
255
256         return p;
257 }
258
259 Cairo::RefPtr<Cairo::Pattern>
260 FastMeter::request_vertical_meter(
261                 int width, int height, int *clr, float *stp, bool shade)
262 {
263         if (height < min_pattern_metric_size)
264                 height = min_pattern_metric_size;
265         if (height > max_pattern_metric_size)
266                 height = max_pattern_metric_size;
267
268         const Pattern10MapKey key (width, height,
269                         stp[0], stp[1], stp[2], stp[3],
270                         clr[0], clr[1], clr[2], clr[3],
271                         clr[4], clr[5], clr[6], clr[7],
272                         clr[8], clr[9]);
273
274         Pattern10Map::iterator i;
275         if ((i = vm_pattern_cache.find (key)) != vm_pattern_cache.end()) {
276                 return i->second;
277         }
278         // TODO flush pattern cache if it gets too large
279
280         Cairo::RefPtr<Cairo::Pattern> p = generate_meter_pattern (
281                 width, height, clr, stp, shade);
282         vm_pattern_cache[key] = p;
283
284         return p;
285 }
286
287 Cairo::RefPtr<Cairo::Pattern>
288 FastMeter::request_vertical_background(
289                 int width, int height, int *bgc, bool shade)
290 {
291         if (height < min_pattern_metric_size)
292                 height = min_pattern_metric_size;
293         if (height > max_pattern_metric_size)
294                 height = max_pattern_metric_size;
295
296         const PatternBgMapKey key (width, height, bgc[0], bgc[1]);
297         PatternBgMap::iterator i;
298         if ((i = vb_pattern_cache.find (key)) != vb_pattern_cache.end()) {
299                 return i->second;
300         }
301         // TODO flush pattern cache if it gets too large
302
303         Cairo::RefPtr<Cairo::Pattern> p = generate_meter_background (
304                 width, height, bgc, shade);
305         vb_pattern_cache[key] = p;
306
307         return p;
308 }
309
310
311 void
312 FastMeter::set_hold_count (long val)
313 {
314         if (val < 1) {
315                 val = 1;
316         }
317
318         hold_cnt = val;
319         hold_state = 0;
320         current_peak = 0;
321
322         queue_draw ();
323 }
324
325 void
326 FastMeter::on_size_request (GtkRequisition* req)
327 {
328         req->height = request_height;
329         req->height = max(req->height, min_pattern_metric_size);
330         req->height = min(req->height, max_pattern_metric_size);
331         req->height += 2;
332
333         req->width  = request_width;
334 }
335
336 void
337 FastMeter::on_size_allocate (Gtk::Allocation &alloc)
338 {
339         if (alloc.get_width() != request_width) {
340                 alloc.set_width (request_width);
341         }
342
343         int h = alloc.get_height();
344         h = max (h, min_pattern_metric_size + 2);
345         h = min (h, max_pattern_metric_size + 2);
346
347         if (h != alloc.get_height()) {
348                 alloc.set_height (h);
349         }
350
351         if (pixheight != h) {
352                 fgpattern = request_vertical_meter (request_width, h, _clr, _stp, true);
353                 bgpattern = request_vertical_background (request_width, h, highlight ? _bgh : _bgc, highlight);
354                 pixheight = h - 2;
355                 pixwidth  = request_width - 2;
356         }
357
358         DrawingArea::on_size_allocate (alloc);
359 }
360
361 bool
362 FastMeter::on_expose_event (GdkEventExpose* ev)
363 {
364         return vertical_expose (ev);
365 }
366
367 bool
368 FastMeter::vertical_expose (GdkEventExpose* ev)
369 {
370         Glib::RefPtr<Gdk::Window> win = get_window ();
371         gint top_of_meter;
372         GdkRectangle intersection;
373         GdkRectangle background;
374
375         cairo_t* cr = gdk_cairo_create (get_window ()->gobj());
376
377         cairo_rectangle (cr, ev->area.x, ev->area.y, ev->area.width, ev->area.height);
378         cairo_clip (cr);
379
380         cairo_set_source_rgb (cr, 0, 0, 0); // black
381         rounded_rectangle (cr, 0, 0, pixrect.width + 2, pixheight + 2, 2);
382         cairo_stroke (cr);
383
384         top_of_meter = (gint) floor (pixheight * current_level);
385
386         /* reset the height & origin of the rect that needs to show the pixbuf
387          */
388
389         pixrect.height = top_of_meter;
390         pixrect.y = 1 + pixheight - top_of_meter;
391
392         background.x = 1;
393         background.y = 1;
394         background.width = pixrect.width;
395         background.height = pixheight - top_of_meter;
396
397         if (gdk_rectangle_intersect (&background, &ev->area, &intersection)) {
398                 cairo_set_source (cr, bgpattern->cobj());
399                 cairo_rectangle (cr, intersection.x, intersection.y, intersection.width, intersection.height);
400                 cairo_fill (cr);
401         }
402
403         if (gdk_rectangle_intersect (&pixrect, &ev->area, &intersection)) {
404                 // draw the part of the meter image that we need. the area we draw is bounded "in reverse" (top->bottom)
405                 cairo_set_source (cr, fgpattern->cobj());
406                 cairo_rectangle (cr, intersection.x, intersection.y, intersection.width, intersection.height);
407                 cairo_fill (cr);
408         }
409
410         // draw peak bar
411
412         if (hold_state) {
413                 last_peak_rect.x = 1;
414                 last_peak_rect.width = pixwidth;
415                 last_peak_rect.y = max(1, 1 + pixheight - (gint) floor (pixheight * current_peak));
416                 if (bright_hold) {
417                         last_peak_rect.height = max(0, min(4, pixheight - last_peak_rect.y -1 ));
418                 } else {
419                         last_peak_rect.height = max(0, min(2, pixheight - last_peak_rect.y -1 ));
420                 }
421
422                 cairo_set_source (cr, fgpattern->cobj());
423                 cairo_rectangle (cr, 1, last_peak_rect.y, pixwidth, last_peak_rect.height);
424
425                 if (bright_hold && !no_rgba_overlay) {
426                         cairo_fill_preserve (cr);
427                         cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 0.3);
428                 }
429                 cairo_fill (cr);
430
431         } else {
432                 last_peak_rect.width = 0;
433                 last_peak_rect.height = 0;
434         }
435
436         cairo_destroy (cr);
437
438         return TRUE;
439 }
440
441 void
442 FastMeter::set (float lvl, float peak)
443 {
444         float old_level = current_level;
445         float old_peak = current_peak;
446
447         if (peak == -1) {
448                 if (lvl >= current_peak) {
449                         current_peak = lvl;
450                         hold_state = hold_cnt;
451                 }
452
453                 if (hold_state > 0) {
454                         if (--hold_state == 0) {
455                                 current_peak = lvl;
456                         }
457                 }
458                 bright_hold = false;
459         } else {
460                 current_peak = peak;
461                 hold_state = 1;
462                 bright_hold = true;
463         }
464
465         current_level = lvl;
466
467         if (current_level == old_level && current_peak == old_peak && (hold_state == 0 || peak != -1)) {
468                 return;
469         }
470
471         Glib::RefPtr<Gdk::Window> win;
472
473         if ((win = get_window()) == 0) {
474                 queue_draw ();
475                 return;
476         }
477
478         queue_vertical_redraw (win, old_level);
479 }
480
481 void
482 FastMeter::queue_vertical_redraw (const Glib::RefPtr<Gdk::Window>& win, float old_level)
483 {
484         GdkRectangle rect;
485
486         gint new_top = (gint) floor (pixheight * current_level);
487
488         rect.x = 1;
489         rect.width = pixwidth;
490         rect.height = new_top;
491         rect.y = 1 + pixheight - new_top;
492
493         if (current_level > old_level) {
494                 /* colored/pixbuf got larger, just draw the new section */
495                 /* rect.y stays where it is because of X coordinates */
496                 /* height of invalidated area is between new.y (smaller) and old.y
497                    (larger).
498                    X coordinates just make my brain hurt.
499                 */
500                 rect.height = pixrect.y - rect.y;
501         } else {
502                 /* it got smaller, compute the difference */
503                 /* rect.y becomes old.y (the smaller value) */
504                 rect.y = pixrect.y;
505                 /* rect.height is the old.y (smaller) minus the new.y (larger)
506                 */
507                 rect.height = pixrect.height - rect.height;
508         }
509
510         GdkRegion* region = 0;
511         bool queue = false;
512
513         if (rect.height != 0) {
514
515                 /* ok, first region to draw ... */
516
517                 region = gdk_region_rectangle (&rect);
518                 queue = true;
519         }
520
521         /* redraw the last place where the last peak hold bar was;
522            the next expose will draw the new one whether its part of
523            expose region or not.
524         */
525
526         if (last_peak_rect.width * last_peak_rect.height != 0) {
527                 if (!queue) {
528                         region = gdk_region_new ();
529                         queue = true;
530                 }
531                 gdk_region_union_with_rect (region, &last_peak_rect);
532         }
533
534         if (hold_state && current_peak > 0) {
535                 if (!queue) {
536                         region = gdk_region_new ();
537                         queue = true;
538                 }
539                 rect.x = 1;
540                 rect.y = max(1, 1 + pixheight - (gint) floor (pixheight * current_peak));
541                 if (bright_hold) {
542                         rect.height = max(0, min(4, pixheight - last_peak_rect.y -1 ));
543                 } else {
544                         rect.height = max(0, min(2, pixheight - last_peak_rect.y -1 ));
545                 }
546                 rect.width = pixwidth;
547                 gdk_region_union_with_rect (region, &rect);
548         }
549
550         if (queue) {
551                 gdk_window_invalidate_region (win->gobj(), region, true);
552         }
553         if (region) {
554                 gdk_region_destroy(region);
555                 region = 0;
556         }
557 }
558
559 void
560 FastMeter::set_highlight (bool onoff)
561 {
562         if (highlight == onoff) {
563                 return;
564         }
565         highlight = onoff;
566         bgpattern = request_vertical_background (request_width, pixheight, highlight ? _bgh : _bgc, highlight);
567         queue_draw ();
568 }
569
570 void
571 FastMeter::clear ()
572 {
573         current_level = 0;
574         current_peak = 0;
575         hold_state = 0;
576         queue_draw ();
577 }