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