Merge branch 'master' into cairocanvas
[ardour.git] / gtk2_ardour / utils.cc
1 /*
2     Copyright (C) 2003 Paul Davis
3
4     This program is free software; you an 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 */
19
20 #ifdef WAF_BUILD
21 #include "gtk2ardour-config.h"
22 #endif
23
24 #include <pango/pangoft2.h> // for fontmap resolution control for GnomeCanvas
25 #include <pango/pangocairo.h> // for fontmap resolution control for GnomeCanvas
26
27 #include <cstdlib>
28 #include <clocale>
29 #include <cstring>
30 #include <cctype>
31 #include <fstream>
32 #include <list>
33 #include <sys/stat.h>
34 #include <gtkmm/rc.h>
35 #include <gtkmm/window.h>
36 #include <gtkmm/combo.h>
37 #include <gtkmm/label.h>
38 #include <gtkmm/paned.h>
39 #include <gtk/gtkpaned.h>
40 #include <boost/algorithm/string.hpp>
41
42 #include "pbd/file_utils.h"
43
44 #include <gtkmm2ext/utils.h>
45 #include "ardour/rc_configuration.h"
46 #include "ardour/filesystem_paths.h"
47 #include "canvas/item.h"
48
49 #include "ardour_ui.h"
50 #include "debug.h"
51 #include "public_editor.h"
52 #include "keyboard.h"
53 #include "utils.h"
54 #include "i18n.h"
55 #include "rgb_macros.h"
56 #include "gui_thread.h"
57
58 using namespace std;
59 using namespace Gtk;
60 using namespace Glib;
61 using namespace PBD;
62 using Gtkmm2ext::Keyboard;
63
64 sigc::signal<void>  DPIReset;
65
66
67 /** Add an element to a menu, settings its sensitivity.
68  * @param m Menu to add to.
69  * @param e Element to add.
70  * @param s true to make sensitive, false to make insensitive
71  */
72 void
73 add_item_with_sensitivity (Menu_Helpers::MenuList& m, Menu_Helpers::MenuElem e, bool s)
74 {
75         m.push_back (e);
76         if (!s) {
77                 m.back().set_sensitive (false);
78         }
79 }
80
81
82 gint
83 just_hide_it (GdkEventAny */*ev*/, Gtk::Window *win)
84 {
85         win->hide ();
86         return 0;
87 }
88
89 /* xpm2rgb copied from nixieclock, which bore the legend:
90
91     nixieclock - a nixie desktop timepiece
92     Copyright (C) 2000 Greg Ercolano, erco@3dsite.com
93
94     and was released under the GPL.
95 */
96
97 unsigned char*
98 xpm2rgb (const char** xpm, uint32_t& w, uint32_t& h)
99 {
100         static long vals[256], val;
101         uint32_t t, x, y, colors, cpp;
102         unsigned char c;
103         unsigned char *savergb, *rgb;
104
105         // PARSE HEADER
106
107         if ( sscanf(xpm[0], "%u%u%u%u", &w, &h, &colors, &cpp) != 4 ) {
108                 error << string_compose (_("bad XPM header %1"), xpm[0])
109                       << endmsg;
110                 return 0;
111         }
112
113         savergb = rgb = (unsigned char*) malloc (h * w * 3);
114
115         // LOAD XPM COLORMAP LONG ENOUGH TO DO CONVERSION
116         for (t = 0; t < colors; ++t) {
117                 sscanf (xpm[t+1], "%c c #%lx", &c, &val);
118                 vals[c] = val;
119         }
120
121         // COLORMAP -> RGB CONVERSION
122         //    Get low 3 bytes from vals[]
123         //
124
125         const char *p;
126         for (y = h-1; y > 0; --y) {
127
128                 for (p = xpm[1+colors+(h-y-1)], x = 0; x < w; x++, rgb += 3) {
129                         val = vals[(int)*p++];
130                         *(rgb+2) = val & 0xff; val >>= 8;  // 2:B
131                         *(rgb+1) = val & 0xff; val >>= 8;  // 1:G
132                         *(rgb+0) = val & 0xff;             // 0:R
133                 }
134         }
135
136         return (savergb);
137 }
138
139 unsigned char*
140 xpm2rgba (const char** xpm, uint32_t& w, uint32_t& h)
141 {
142         static long vals[256], val;
143         uint32_t t, x, y, colors, cpp;
144         unsigned char c;
145         unsigned char *savergb, *rgb;
146         char transparent;
147
148         // PARSE HEADER
149
150         if ( sscanf(xpm[0], "%u%u%u%u", &w, &h, &colors, &cpp) != 4 ) {
151                 error << string_compose (_("bad XPM header %1"), xpm[0])
152                       << endmsg;
153                 return 0;
154         }
155
156         savergb = rgb = (unsigned char*) malloc (h * w * 4);
157
158         // LOAD XPM COLORMAP LONG ENOUGH TO DO CONVERSION
159
160         if (strstr (xpm[1], "None")) {
161                 sscanf (xpm[1], "%c", &transparent);
162                 t = 1;
163         } else {
164                 transparent = 0;
165                 t = 0;
166         }
167
168         for (; t < colors; ++t) {
169                 sscanf (xpm[t+1], "%c c #%lx", &c, &val);
170                 vals[c] = val;
171         }
172
173         // COLORMAP -> RGB CONVERSION
174         //    Get low 3 bytes from vals[]
175         //
176
177         const char *p;
178         for (y = h-1; y > 0; --y) {
179
180                 char alpha;
181
182                 for (p = xpm[1+colors+(h-y-1)], x = 0; x < w; x++, rgb += 4) {
183
184                         if (transparent && (*p++ == transparent)) {
185                                 alpha = 0;
186                                 val = 0;
187                         } else {
188                                 alpha = 255;
189                                 val = vals[(int)*p];
190                         }
191
192                         *(rgb+3) = alpha;                  // 3: alpha
193                         *(rgb+2) = val & 0xff; val >>= 8;  // 2:B
194                         *(rgb+1) = val & 0xff; val >>= 8;  // 1:G
195                         *(rgb+0) = val & 0xff;             // 0:R
196                 }
197         }
198
199         return (savergb);
200 }
201
202 Pango::FontDescription
203 get_font_for_style (string widgetname)
204 {
205         Gtk::Window window (WINDOW_TOPLEVEL);
206         Gtk::Label foobar;
207         Glib::RefPtr<Gtk::Style> style;
208
209         window.add (foobar);
210         foobar.set_name (widgetname);
211         foobar.ensure_style();
212
213         style = foobar.get_style ();
214
215         Glib::RefPtr<const Pango::Layout> layout = foobar.get_layout();
216
217         PangoFontDescription *pfd = const_cast<PangoFontDescription *> (pango_layout_get_font_description(const_cast<PangoLayout *>(layout->gobj())));
218
219         if (!pfd) {
220
221                 /* layout inherited its font description from a PangoContext */
222
223                 PangoContext* ctxt = (PangoContext*) pango_layout_get_context (const_cast<PangoLayout*>(layout->gobj()));
224                 pfd =  pango_context_get_font_description (ctxt);
225                 return Pango::FontDescription (pfd); /* make a copy */
226         }
227
228         return Pango::FontDescription (pfd); /* make a copy */
229 }
230
231 uint32_t
232 rgba_from_style (string style, uint32_t r, uint32_t g, uint32_t b, uint32_t a, string attr, int state, bool rgba)
233 {
234         /* In GTK+2, styles aren't set up correctly if the widget is not
235            attached to a toplevel window that has a screen pointer.
236         */
237
238         static Gtk::Window* window = 0;
239
240         if (window == 0) {
241                 window = new Window (WINDOW_TOPLEVEL);
242         }
243
244         Gtk::Label foo;
245
246         window->add (foo);
247
248         foo.set_name (style);
249         foo.ensure_style ();
250
251         GtkRcStyle* rc = foo.get_style()->gobj()->rc_style;
252
253         if (rc) {
254                 if (attr == "fg") {
255                         r = rc->fg[state].red / 257;
256                         g = rc->fg[state].green / 257;
257                         b = rc->fg[state].blue / 257;
258
259                         /* what a hack ... "a" is for "active" */
260                         if (state == Gtk::STATE_NORMAL && rgba) {
261                                 a = rc->fg[GTK_STATE_ACTIVE].red / 257;
262                         }
263                 } else if (attr == "bg") {
264                         r = g = b = 0;
265                         r = rc->bg[state].red / 257;
266                         g = rc->bg[state].green / 257;
267                         b = rc->bg[state].blue / 257;
268                 } else if (attr == "base") {
269                         r = rc->base[state].red / 257;
270                         g = rc->base[state].green / 257;
271                         b = rc->base[state].blue / 257;
272                 } else if (attr == "text") {
273                         r = rc->text[state].red / 257;
274                         g = rc->text[state].green / 257;
275                         b = rc->text[state].blue / 257;
276                 }
277         } else {
278                 warning << string_compose (_("missing RGBA style for \"%1\""), style) << endl;
279         }
280
281         window->remove ();
282
283         if (state == Gtk::STATE_NORMAL && rgba) {
284                 return (uint32_t) RGBA_TO_UINT(r,g,b,a);
285         } else {
286                 return (uint32_t) RGB_TO_UINT(r,g,b);
287         }
288 }
289
290 void
291 set_color (Gdk::Color& c, int rgb)
292 {
293         c.set_rgb((rgb >> 16)*256, ((rgb & 0xff00) >> 8)*256, (rgb & 0xff)*256);
294 }
295
296 bool
297 relay_key_press (GdkEventKey* ev, Gtk::Window* win)
298 {
299         if (!key_press_focus_accelerator_handler (*win, ev)) {
300                 return PublicEditor::instance().on_key_press_event(ev);
301         } else {
302                 return true;
303         }
304 }
305
306 bool
307 forward_key_press (GdkEventKey* ev)
308 {
309         return PublicEditor::instance().on_key_press_event(ev);
310 }
311
312 bool
313 emulate_key_event (Gtk::Widget* w, unsigned int keyval)
314 {
315         GdkDisplay  *display = gtk_widget_get_display (GTK_WIDGET(w->gobj()));
316         GdkKeymap   *keymap  = gdk_keymap_get_for_display (display);
317         GdkKeymapKey *keymapkey = NULL;
318         gint n_keys;
319
320         if (!gdk_keymap_get_entries_for_keyval(keymap, keyval, &keymapkey, &n_keys)) return false;
321         if (n_keys !=1) { g_free(keymapkey); return false;}
322
323         GdkEventKey ev;
324         ev.type = GDK_KEY_PRESS;
325         ev.window = gtk_widget_get_window(GTK_WIDGET(w->gobj()));
326         ev.send_event = FALSE;
327         ev.time = 0;
328         ev.state = 0;
329         ev.keyval = keyval;
330         ev.length = 0;
331         ev.string = (gchar*) "";
332         ev.hardware_keycode = keymapkey[0].keycode;
333         ev.group = keymapkey[0].group;
334         g_free(keymapkey);
335
336         forward_key_press(&ev);
337         ev.type = GDK_KEY_RELEASE;
338         return forward_key_press(&ev);
339 }
340
341 bool
342 key_press_focus_accelerator_handler (Gtk::Window& window, GdkEventKey* ev)
343 {
344         GtkWindow* win = window.gobj();
345         GtkWidget* focus = gtk_window_get_focus (win);
346         bool special_handling_of_unmodified_accelerators = false;
347         bool allow_activating = true;
348         /* consider all relevant modifiers but not LOCK or SHIFT */
349         const guint mask = (Keyboard::RelevantModifierKeyMask & ~(Gdk::SHIFT_MASK|Gdk::LOCK_MASK));
350
351         if (focus) {
352                 if (GTK_IS_ENTRY(focus) || Keyboard::some_magic_widget_has_focus()) {
353                         special_handling_of_unmodified_accelerators = true;
354                 }
355         }
356
357 #ifdef GTKOSX
358         /* at one time this appeared to be necessary. As of July 2012, it does not
359            appear to be. if it ever is necessar, figure out if it should apply
360            to all platforms.
361         */
362 #if 0 
363         if (Keyboard::some_magic_widget_has_focus ()) {
364                 allow_activating = false;
365         }
366 #endif
367 #endif
368
369
370         DEBUG_TRACE (DEBUG::Accelerators, string_compose ("Win = %1 focus = %7 Key event: code = %2  state = %3 special handling ? %4 magic widget focus ? %5 allow_activation ? %6\n",
371                                                           win,
372                                                           ev->keyval,
373                                                           ev->state,
374                                                           special_handling_of_unmodified_accelerators,
375                                                           Keyboard::some_magic_widget_has_focus(),
376                                                           allow_activating,
377                                                           focus));
378
379         /* This exists to allow us to override the way GTK handles
380            key events. The normal sequence is:
381
382            a) event is delivered to a GtkWindow
383            b) accelerators/mnemonics are activated
384            c) if (b) didn't handle the event, propagate to
385                the focus widget and/or focus chain
386
387            The problem with this is that if the accelerators include
388            keys without modifiers, such as the space bar or the
389            letter "e", then pressing the key while typing into
390            a text entry widget results in the accelerator being
391            activated, instead of the desired letter appearing
392            in the text entry.
393
394            There is no good way of fixing this, but this
395            represents a compromise. The idea is that
396            key events involving modifiers (not Shift)
397            get routed into the activation pathway first, then
398            get propagated to the focus widget if necessary.
399
400            If the key event doesn't involve modifiers,
401            we deliver to the focus widget first, thus allowing
402            it to get "normal text" without interference
403            from acceleration.
404
405            Of course, this can also be problematic: if there
406            is a widget with focus, then it will swallow
407            all "normal text" accelerators.
408         */
409
410         if (!special_handling_of_unmodified_accelerators) {
411
412                 /* XXX note that for a brief moment, the conditional above
413                  * included "|| (ev->state & mask)" so as to enforce the
414                  * implication of special_handling_of_UNMODIFIED_accelerators.
415                  * however, this forces any key that GTK doesn't allow and that
416                  * we have an alternative (see next comment) for to be
417                  * automatically sent through the accel groups activation
418                  * pathway, which prevents individual widgets & canvas items
419                  * from ever seeing it if is used by a key binding.
420                  * 
421                  * specifically, this hid Ctrl-down-arrow from MIDI region
422                  * views because it is also bound to an action.
423                  *
424                  * until we have a robust, clean binding system, this
425                  * quirk will have to remain in place.
426                  */
427
428                 /* pretend that certain key events that GTK does not allow
429                    to be used as accelerators are actually something that
430                    it does allow. but only where there are no modifiers.
431                 */
432
433                 uint32_t fakekey = ev->keyval;
434
435                 if (Gtkmm2ext::possibly_translate_keyval_to_make_legal_accelerator (fakekey)) {
436                         DEBUG_TRACE (DEBUG::Accelerators, string_compose ("\tactivate (was %1 now %2) without special hanlding of unmodified accels\n",
437                                                                           ev->keyval, fakekey));
438
439                         GdkModifierType mod = GdkModifierType (ev->state);
440
441                         mod = GdkModifierType (mod & gtk_accelerator_get_default_mod_mask());
442 #ifdef GTKOSX
443                         /* GTK on OS X is currently (February 2012) setting both
444                            the Meta and Mod2 bits in the event modifier state if 
445                            the Command key is down.
446
447                            gtk_accel_groups_activate() does not invoke any of the logic
448                            that gtk_window_activate_key() will that sorts out that stupid
449                            state of affairs, and as a result it fails to find a match
450                            for the key event and the current set of accelerators.
451
452                            to fix this, if the meta bit is set, remove the mod2 bit
453                            from the modifier. this assumes that our bindings use Primary
454                            which will have set the meta bit in the accelerator entry.
455                         */
456                         if (mod & GDK_META_MASK) {
457                                 mod = GdkModifierType (mod & ~GDK_MOD2_MASK);
458                         }
459 #endif
460
461                         if (allow_activating && gtk_accel_groups_activate(G_OBJECT(win), fakekey, mod)) {
462                                 DEBUG_TRACE (DEBUG::Accelerators, "\taccel group activated by fakekey\n");
463                                 return true;
464                         }
465                 }
466         }
467
468         if (!special_handling_of_unmodified_accelerators || (ev->state & mask)) {
469
470                 /* no special handling or there are modifiers in effect: accelerate first */
471
472                 DEBUG_TRACE (DEBUG::Accelerators, "\tactivate, then propagate\n");
473                 DEBUG_TRACE (DEBUG::Accelerators, string_compose ("\tevent send-event:%1 time:%2 length:%3 string:%4 hardware_keycode:%5 group:%6\n",
474                                         ev->send_event, ev->time, ev->length, ev->string, ev->hardware_keycode, ev->group));
475
476                 if (allow_activating) {
477                         DEBUG_TRACE (DEBUG::Accelerators, "\tsending to window\n");
478                         if (gtk_window_activate_key (win, ev)) {
479                                 DEBUG_TRACE (DEBUG::Accelerators, "\t\thandled\n");
480                                 return true;
481                         }
482                 } else {
483                         DEBUG_TRACE (DEBUG::Accelerators, "\tactivation skipped\n");
484                 }
485
486                 DEBUG_TRACE (DEBUG::Accelerators, "\tnot accelerated, now propagate\n");
487
488                 return gtk_window_propagate_key_event (win, ev);
489         }
490
491         /* no modifiers, propagate first */
492
493         DEBUG_TRACE (DEBUG::Accelerators, "\tpropagate, then activate\n");
494
495         if (!gtk_window_propagate_key_event (win, ev)) {
496                 DEBUG_TRACE (DEBUG::Accelerators, "\tpropagation didn't handle, so activate\n");
497                 if (allow_activating) {
498                         return gtk_window_activate_key (win, ev);
499                 } else {
500                         DEBUG_TRACE (DEBUG::Accelerators, "\tactivation skipped\n");
501                 }
502
503         } else {
504                 DEBUG_TRACE (DEBUG::Accelerators, "\thandled by propagate\n");
505                 return true;
506         }
507
508         DEBUG_TRACE (DEBUG::Accelerators, "\tnot handled\n");
509         return true;
510 }
511
512 Glib::RefPtr<Gdk::Pixbuf>
513 get_xpm (std::string name)
514 {
515         if (!xpm_map[name]) {
516
517                 SearchPath spath(ARDOUR::ardour_data_search_path());
518
519                 spath.add_subdirectory_to_paths("pixmaps");
520
521                 std::string data_file_path;
522
523                 if(!find_file_in_search_path (spath, name, data_file_path)) {
524                         fatal << string_compose (_("cannot find XPM file for %1"), name) << endmsg;
525                 }
526
527                 try {
528                         xpm_map[name] =  Gdk::Pixbuf::create_from_file (data_file_path);
529                 } catch(const Glib::Error& e)   {
530                         warning << "Caught Glib::Error: " << e.what() << endmsg;
531                 }
532         }
533
534         return xpm_map[name];
535 }
536
537 std::string
538 get_icon_path (const char* cname)
539 {
540         string name = cname;
541         name += X_(".png");
542
543         SearchPath spath(ARDOUR::ardour_data_search_path());
544
545         spath.add_subdirectory_to_paths("icons");
546
547         std::string data_file_path;
548
549         if (!find_file_in_search_path (spath, name, data_file_path)) {
550                 fatal << string_compose (_("cannot find icon image for %1 using %2"), name, spath.to_string()) << endmsg;
551         }
552
553         return data_file_path;
554 }
555
556 Glib::RefPtr<Gdk::Pixbuf>
557 get_icon (const char* cname)
558 {
559         Glib::RefPtr<Gdk::Pixbuf> img;
560         try {
561                 img = Gdk::Pixbuf::create_from_file (get_icon_path (cname));
562         } catch (const Gdk::PixbufError &e) {
563                 cerr << "Caught PixbufError: " << e.what() << endl;
564         } catch (...) {
565                 error << string_compose (_("Caught exception while loading icon named %1"), cname) << endmsg;
566         }
567
568         return img;
569 }
570
571 string
572 longest (vector<string>& strings)
573 {
574         if (strings.empty()) {
575                 return string ("");
576         }
577
578         vector<string>::iterator longest = strings.begin();
579         string::size_type longest_length = (*longest).length();
580
581         vector<string>::iterator i = longest;
582         ++i;
583
584         while (i != strings.end()) {
585
586                 string::size_type len = (*i).length();
587
588                 if (len > longest_length) {
589                         longest = i;
590                         longest_length = len;
591                 }
592
593                 ++i;
594         }
595
596         return *longest;
597 }
598
599 bool
600 key_is_legal_for_numeric_entry (guint keyval)
601 {
602         /* we assume that this does not change over the life of the process 
603          */
604
605         static int comma_decimal = -1;
606
607         switch (keyval) {
608         case GDK_period:
609         case GDK_comma:
610                 if (comma_decimal < 0) {
611                         std::lconv* lc = std::localeconv();
612                         if (strchr (lc->decimal_point, ',') != 0) {
613                                 comma_decimal = 1;
614                         } else {
615                                 comma_decimal = 0;
616                         }
617                 }
618                 break;
619         default:
620                 break;
621         }
622
623         switch (keyval) {
624         case GDK_decimalpoint:
625         case GDK_KP_Separator:
626                 return true;
627
628         case GDK_period:
629                 if (comma_decimal) {
630                         return false;
631                 } else {
632                         return true;
633                 }
634                 break;
635         case GDK_comma:
636                 if (comma_decimal) {
637                         return true;
638                 } else {
639                         return false;
640                 }
641                 break;
642         case GDK_minus:
643         case GDK_plus:
644         case GDK_0:
645         case GDK_1:
646         case GDK_2:
647         case GDK_3:
648         case GDK_4:
649         case GDK_5:
650         case GDK_6:
651         case GDK_7:
652         case GDK_8:
653         case GDK_9:
654         case GDK_KP_Add:
655         case GDK_KP_Subtract:
656         case GDK_KP_Decimal:
657         case GDK_KP_0:
658         case GDK_KP_1:
659         case GDK_KP_2:
660         case GDK_KP_3:
661         case GDK_KP_4:
662         case GDK_KP_5:
663         case GDK_KP_6:
664         case GDK_KP_7:
665         case GDK_KP_8:
666         case GDK_KP_9:
667         case GDK_Return:
668         case GDK_BackSpace:
669         case GDK_Delete:
670         case GDK_KP_Enter:
671         case GDK_Home:
672         case GDK_End:
673         case GDK_Left:
674         case GDK_Right:
675                 return true;
676
677         default:
678                 break;
679         }
680
681         return false;
682 }
683 void
684 set_pango_fontsize ()
685 {
686         long val = ARDOUR::Config->get_font_scale();
687
688         /* FT2 rendering - used by GnomeCanvas, sigh */
689
690         pango_ft2_font_map_set_resolution ((PangoFT2FontMap*) pango_ft2_font_map_new(), val/1024, val/1024);
691
692         /* Cairo rendering, in case there is any */
693
694         pango_cairo_font_map_set_resolution ((PangoCairoFontMap*) pango_cairo_font_map_get_default(), val/1024);
695 }
696
697 void
698 reset_dpi ()
699 {
700         long val = ARDOUR::Config->get_font_scale();
701         set_pango_fontsize ();
702         /* Xft rendering */
703
704         gtk_settings_set_long_property (gtk_settings_get_default(),
705                                         "gtk-xft-dpi", val, "ardour");
706         DPIReset();//Emit Signal
707 }
708
709 void
710 resize_window_to_proportion_of_monitor (Gtk::Window* window, int max_width, int max_height)
711 {
712         Glib::RefPtr<Gdk::Screen> screen = window->get_screen ();
713         Gdk::Rectangle monitor_rect;
714         screen->get_monitor_geometry (0, monitor_rect);
715
716         int const w = std::min (int (monitor_rect.get_width() * 0.8), max_width);
717         int const h = std::min (int (monitor_rect.get_height() * 0.8), max_height);
718
719         window->resize (w, h);
720 }
721
722
723 /** Replace _ with __ in a string; for use with menu item text to make underscores displayed correctly */
724 string
725 escape_underscores (string const & s)
726 {
727         string o;
728         string::size_type const N = s.length ();
729
730         for (string::size_type i = 0; i < N; ++i) {
731                 if (s[i] == '_') {
732                         o += "__";
733                 } else {
734                         o += s[i];
735                 }
736         }
737
738         return o;
739 }
740
741 /** Replace < and > with &lt; and &gt; respectively to make < > display correctly in markup strings */
742 string
743 escape_angled_brackets (string const & s)
744 {
745         string o = s;
746         boost::replace_all (o, "<", "&lt;");
747         boost::replace_all (o, ">", "&gt;");
748         return o;
749 }
750
751 Gdk::Color
752 unique_random_color (list<Gdk::Color>& used_colors)
753 {
754         Gdk::Color newcolor;
755
756         while (1) {
757
758                 /* avoid neon/glowing tones by limiting them to the
759                    "inner section" (paler) of a color wheel/circle.
760                 */
761
762                 const int32_t max_saturation = 48000; // 65535 would open up the whole color wheel
763
764                 newcolor.set_red (random() % max_saturation);
765                 newcolor.set_blue (random() % max_saturation);
766                 newcolor.set_green (random() % max_saturation);
767
768                 if (used_colors.size() == 0) {
769                         used_colors.push_back (newcolor);
770                         return newcolor;
771                 }
772
773                 for (list<Gdk::Color>::iterator i = used_colors.begin(); i != used_colors.end(); ++i) {
774                   Gdk::Color c = *i;
775                         float rdelta, bdelta, gdelta;
776
777                         rdelta = newcolor.get_red() - c.get_red();
778                         bdelta = newcolor.get_blue() - c.get_blue();
779                         gdelta = newcolor.get_green() - c.get_green();
780
781                         if (sqrt (rdelta*rdelta + bdelta*bdelta + gdelta*gdelta) > 25.0) {
782                                 used_colors.push_back (newcolor);
783                                 return newcolor;
784                         }
785                 }
786
787                 /* XXX need throttle here to make sure we don't spin for ever */
788         }
789 }