fix display of modifiers on OS X in the bindings editor
[ardour.git] / libs / gtkmm2ext / utils.cc
1 /*
2     Copyright (C) 1999 Paul Barton-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 <map>
22 #include <algorithm>
23
24 #include <gtk/gtkpaned.h>
25 #include <gtk/gtk.h>
26
27 #include <gtkmm/widget.h>
28 #include <gtkmm/button.h>
29 #include <gtkmm/window.h>
30 #include <gtkmm/paned.h>
31 #include <gtkmm/label.h>
32 #include <gtkmm/comboboxtext.h>
33 #include <gtkmm/tooltip.h>
34 #include <gtkmm/menuitem.h>
35
36 #include "gtkmm2ext/utils.h"
37 #include "gtkmm2ext/persistent_tooltip.h"
38
39 #include "pbd/i18n.h"
40
41 using namespace std;
42
43 void
44 Gtkmm2ext::init (const char* localedir)
45 {
46 #ifdef ENABLE_NLS
47         (void) bindtextdomain(PACKAGE, localedir);
48         (void) bind_textdomain_codeset (PACKAGE, "UTF-8");
49 #endif
50 }
51
52 void
53 Gtkmm2ext::get_ink_pixel_size (Glib::RefPtr<Pango::Layout> layout,
54                                int& width,
55                                int& height)
56 {
57         Pango::Rectangle ink_rect = layout->get_ink_extents ();
58
59         width = PANGO_PIXELS(ink_rect.get_width());
60         height = PANGO_PIXELS(ink_rect.get_height());
61 }
62
63 void
64 Gtkmm2ext::get_pixel_size (Glib::RefPtr<Pango::Layout> layout,
65                            int& width,
66                            int& height)
67 {
68         layout->get_pixel_size (width, height);
69 }
70
71 void
72 Gtkmm2ext::set_size_request_to_display_given_text (Gtk::Widget &w, const gchar *text,
73                                                    gint hpadding, gint vpadding)
74 {
75         int width, height;
76         w.ensure_style ();
77
78         get_pixel_size (w.create_pango_layout (text), width, height);
79         w.set_size_request(width + hpadding, height + vpadding);
80 }
81
82 /** Set width request to display given text, and height to display anything.
83     This is useful for setting many widgets to the same height for consistency. */
84 void
85 Gtkmm2ext::set_size_request_to_display_given_text_width (Gtk::Widget& w,
86                                                          const gchar* htext,
87                                                          gint         hpadding,
88                                                          gint         vpadding)
89 {
90         static const gchar* vtext = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
91
92         w.ensure_style ();
93
94         int hwidth, hheight;
95         get_pixel_size (w.create_pango_layout (htext), hwidth, hheight);
96
97         int vwidth, vheight;
98         get_pixel_size (w.create_pango_layout (vtext), vwidth, vheight);
99
100         w.set_size_request(hwidth + hpadding, vheight + vpadding);
101 }
102
103 void
104 Gtkmm2ext::set_height_request_to_display_any_text (Gtk::Widget& w, gint vpadding)
105 {
106         static const gchar* vtext = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
107
108         w.ensure_style ();
109
110         int width, height;
111         get_pixel_size (w.create_pango_layout (vtext), width, height);
112
113         w.set_size_request(-1, height + vpadding);
114 }
115
116 void
117 Gtkmm2ext::set_size_request_to_display_given_text (Gtk::Widget &w, std::string const & text,
118                                                    gint hpadding, gint vpadding)
119 {
120         int width, height;
121         w.ensure_style ();
122
123         get_pixel_size (w.create_pango_layout (text), width, height);
124         w.set_size_request(width + hpadding, height + vpadding);
125 }
126
127 void
128 Gtkmm2ext::set_size_request_to_display_given_text (Gtk::Widget &w,
129                                                    const std::vector<std::string>& strings,
130                                                    gint hpadding, gint vpadding)
131 {
132         int width, height;
133         int width_max = 0;
134         int height_max = 0;
135         w.ensure_style ();
136         vector<string> copy;
137         const vector<string>* to_use;
138         vector<string>::const_iterator i;
139
140         for (i = strings.begin(); i != strings.end(); ++i) {
141                 if ((*i).find_first_of ("gy") != string::npos) {
142                         /* contains a descender */
143                         break;
144                 }
145         }
146
147         if (i == strings.end()) {
148                 /* make a copy of the strings then add one that has a descender */
149                 copy = strings;
150                 copy.push_back ("g");
151                 to_use = &copy;
152         } else {
153                 to_use = &strings;
154         }
155
156         for (vector<string>::const_iterator i = to_use->begin(); i != to_use->end(); ++i) {
157                 get_pixel_size (w.create_pango_layout (*i), width, height);
158                 width_max = max(width_max,width);
159                 height_max = max(height_max, height);
160         }
161
162         w.set_size_request(width_max + hpadding, height_max + vpadding);
163 }
164
165 /** This version specifies horizontal padding in text to avoid assumptions
166     about font size.  Should be used anywhere padding is used to avoid text,
167     like combo boxes. */
168 void
169 Gtkmm2ext::set_size_request_to_display_given_text (Gtk::Widget&                    w,
170                                                    const std::vector<std::string>& strings,
171                                                    const std::string&              hpadding,
172                                                    gint                            vpadding)
173 {
174         int width_max = 0;
175         int height_max = 0;
176         w.ensure_style ();
177
178         for (vector<string>::const_iterator i = strings.begin(); i != strings.end(); ++i) {
179                 int width, height;
180                 get_pixel_size (w.create_pango_layout (*i), width, height);
181                 width_max = max(width_max,width);
182                 height_max = max(height_max, height);
183         }
184
185         int pad_width;
186         int pad_height;
187         get_pixel_size (w.create_pango_layout (hpadding), pad_width, pad_height);
188
189         w.set_size_request(width_max + pad_width, height_max + vpadding);
190 }
191
192 static inline guint8
193 demultiply_alpha (guint8 src,
194                   guint8 alpha)
195 {
196         /* cairo pixel buffer data contains RGB values with the alpha
197            values premultiplied.
198
199            GdkPixbuf pixel buffer data contains RGB values without the
200            alpha value applied.
201
202            this removes the alpha component from the cairo version and
203            returns the GdkPixbuf version.
204         */
205         return alpha ? ((guint (src) << 8) - src) / alpha : 0;
206 }
207
208 void
209 Gtkmm2ext::convert_bgra_to_rgba (guint8 const* src,
210                                  guint8*       dst,
211                                  int           width,
212                                  int           height)
213 {
214         guint8 const* src_pixel = src;
215         guint8*       dst_pixel = dst;
216
217         /* cairo pixel data is endian-dependent ARGB with A in the most significant 8 bits,
218            with premultipled alpha values (see preceding function)
219
220            GdkPixbuf pixel data is non-endian-dependent RGBA with R in the lowest addressable
221            8 bits, and non-premultiplied alpha values.
222
223            convert from the cairo values to the GdkPixbuf ones.
224         */
225
226         for (int y = 0; y < height; y++) {
227                 for (int x = 0; x < width; x++) {
228 #if G_BYTE_ORDER == G_LITTLE_ENDIAN
229                         /* Cairo [ B G R A ] is actually  [ B G R A ] in memory SOURCE
230                                                             0 1 2 3
231                            Pixbuf [ R G B A ] is actually [ R G B A ] in memory DEST
232                         */
233                         dst_pixel[0] = demultiply_alpha (src_pixel[2],
234                                                          src_pixel[3]); // R [0] <= [ 2 ]
235                         dst_pixel[1] = demultiply_alpha (src_pixel[1],
236                                                          src_pixel[3]); // G [1] <= [ 1 ]
237                         dst_pixel[2] = demultiply_alpha (src_pixel[0],
238                                                          src_pixel[3]); // B [2] <= [ 0 ]
239                         dst_pixel[3] = src_pixel[3]; // alpha
240
241 #elif G_BYTE_ORDER == G_BIG_ENDIAN
242                         /* Cairo [ B G R A ] is actually  [ A R G B ] in memory SOURCE
243                                                             0 1 2 3
244                            Pixbuf [ R G B A ] is actually [ R G B A ] in memory DEST
245                         */
246                         dst_pixel[0] = demultiply_alpha (src_pixel[1],
247                                                          src_pixel[0]); // R [0] <= [ 1 ]
248                         dst_pixel[1] = demultiply_alpha (src_pixel[2],
249                                                          src_pixel[0]); // G [1] <= [ 2 ]
250                         dst_pixel[2] = demultiply_alpha (src_pixel[3],
251                                                          src_pixel[0]); // B [2] <= [ 3 ]
252                         dst_pixel[3] = src_pixel[0]; // alpha
253
254 #else
255 #error ardour does not currently support PDP-endianess
256 #endif
257
258                         dst_pixel += 4;
259                         src_pixel += 4;
260                 }
261         }
262 }
263
264 Glib::RefPtr<Gdk::Pixbuf>
265 Gtkmm2ext::pixbuf_from_string(const string& name, const Pango::FontDescription& font, int clip_width, int clip_height, Gdk::Color fg)
266 {
267         static Glib::RefPtr<Gdk::Pixbuf>* empty_pixbuf = 0;
268
269         if (name.empty()) {
270                 if (empty_pixbuf == 0) {
271                         empty_pixbuf = new Glib::RefPtr<Gdk::Pixbuf>;
272                         *empty_pixbuf = Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB, true, 8, clip_width, clip_height);
273                 }
274                 return *empty_pixbuf;
275         }
276
277         if (clip_width <= 0 || clip_height <= 0) {
278                 /* negative values mean padding around natural size */
279                 int width, height;
280                 pixel_size (name, font, width, height);
281                 if (clip_width <= 0) {
282                         clip_width = width - clip_width;
283                 }
284                 if (clip_height <= 0) {
285                         clip_height = height - clip_height;
286                 }
287         }
288
289         Glib::RefPtr<Gdk::Pixbuf> buf = Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB, true, 8, clip_width, clip_height);
290
291         cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, clip_width, clip_height);
292         cairo_t* cr = cairo_create (surface);
293         cairo_text_extents_t te;
294
295         cairo_set_source_rgba (cr, fg.get_red_p(), fg.get_green_p(), fg.get_blue_p(), 1.0);
296         cairo_select_font_face (cr, font.get_family().c_str(),
297                                 CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
298         cairo_set_font_size (cr,  font.get_size() / Pango::SCALE);
299         cairo_text_extents (cr, name.c_str(), &te);
300
301         cairo_move_to (cr, 0.5, int (0.5 - te.height / 2 - te.y_bearing + clip_height / 2));
302         cairo_show_text (cr, name.c_str());
303
304         convert_bgra_to_rgba(cairo_image_surface_get_data (surface), buf->get_pixels(), clip_width, clip_height);
305
306         cairo_destroy(cr);
307         cairo_surface_destroy(surface);
308
309         return buf;
310 }
311
312 void
313 Gtkmm2ext::position_menu_anchored (const Gtk::Menu* const menu,
314                                    Gtk::Widget* const anchor,
315                                    const std::string& selected,
316                                    int& x, int& y, bool& push_in) {
317         using namespace Gdk;
318         using namespace Gtk;
319         using namespace Gtk::Menu_Helpers;
320
321          /* TODO: lacks support for rotated dropdown buttons */
322
323         if (!anchor->has_screen () || !anchor->get_has_window ()) {
324                 return;
325         }
326
327         Rectangle monitor;
328         {
329                 const int monitor_num = anchor->get_screen ()->get_monitor_at_window (
330                                 anchor->get_window ());
331                 anchor->get_screen ()->get_monitor_geometry (
332                                 (monitor_num < 0) ? 0 : monitor_num, monitor);
333         }
334
335         const Requisition menu_req = menu->size_request();
336         const Rectangle allocation = anchor->get_allocation();
337
338         /* The x and y position are handled separately.
339          *
340          * For the x position if the direction is LTR (or RTL), then we try in order:
341          *  a) align the left (right) of the menu with the left (right) of the button
342          *     if there's enough room until the right (left) border of the screen;
343          *  b) align the right (left) of the menu with the right (left) of the button
344          *     if there's enough room until the left (right) border of the screen;
345          *  c) align the right (left) border of the menu with the right (left) border
346          *     of the screen if there's enough space;
347          *  d) align the left (right) border of the menu with the left (right) border
348          *     of the screen, with the rightmost (leftmost) part of the menu that
349          *     overflows the screen.
350          *     XXX We always align left regardless of the direction because if x is
351          *     left of the current monitor, the menu popup code after us notices it
352          *     and enforces that the menu stays in the monitor that's at the left...*/
353
354         anchor->get_window ()->get_origin (x, y);
355
356         if (anchor->get_direction() == TEXT_DIR_RTL) {
357                 if (monitor.get_x() <= x + allocation.get_width() - menu_req.width) {
358                         /* a) align menu right and button right */
359                         x += allocation.get_width() - menu_req.width;
360                 } else if (x + menu_req.width <= monitor.get_x() + monitor.get_width()) {
361                         /* b) align menu left and button left: nothing to do*/
362                 } else if (menu_req.width > monitor.get_width()) {
363                         /* c) align menu left and screen left, guaranteed to fit */
364                         x = monitor.get_x();
365                 } else {
366                         /* d) XXX align left or the menu might change monitors */
367                         x = monitor.get_x();
368                 }
369         } else { /* LTR */
370                 if (x + menu_req.width <= monitor.get_x() + monitor.get_width()) {
371                         /* a) align menu left and button left: nothing to do*/
372                 } else if (monitor.get_x() <= x + allocation.get_width() - menu_req.width) {
373                         /* b) align menu right and button right */
374                         x += allocation.get_width() - menu_req.width;
375                 } else if (menu_req.width > monitor.get_width()) {
376                         /* c) align menu right and screen right, guaranteed to fit */
377                         x = monitor.get_x() + monitor.get_width() - menu_req.width;
378                 } else {
379                         /* d) align left */
380                         x = monitor.get_x();
381                 }
382         }
383
384         /* For the y position, try in order:
385          *  a) if there is a menu item with the same text as the button, align it
386          *     with the button, unless that makes the menu overflow the monitor.
387          *  b) align the top of the menu with the bottom of the button if there is
388          *     enough room below the button;
389          *  c) align the bottom of the menu with the top of the button if there is
390          *     enough room above the button;
391          *  d) align the bottom of the menu with the bottom of the monitor if there
392          *     is enough room, but avoid moving the menu to another monitor */
393
394         const MenuList& items = menu->items ();
395         int offset = 0;
396
397         MenuList::const_iterator i = items.begin();
398         for ( ; i != items.end(); ++i) {
399                 if (selected == ((std::string) i->get_label())) {
400                         break;
401                 }
402                 offset += i->size_request().height;
403         }
404         if (i != items.end() &&
405             y - offset >= monitor.get_y() &&
406             y - offset + menu_req.height <= monitor.get_y() + monitor.get_height()) {
407                 y -= offset;
408         } else if (y + allocation.get_height() + menu_req.height <= monitor.get_y() + monitor.get_height()) {
409                 y += allocation.get_height(); /* a) */
410         } else if ((y - menu_req.height) >= monitor.get_y()) {
411                 y -= menu_req.height; /* b) */
412         } else {
413                 y = monitor.get_y() + max(0, monitor.get_height() - menu_req.height);
414         }
415
416         push_in = false;
417 }
418
419 void
420 Gtkmm2ext::set_popdown_strings (Gtk::ComboBoxText& cr, const vector<string>& strings)
421 {
422         vector<string>::const_iterator i;
423
424         cr.clear ();
425
426         for (i = strings.begin(); i != strings.end(); ++i) {
427                 cr.append_text (*i);
428         }
429 }
430
431 void
432 Gtkmm2ext::get_popdown_strings (Gtk::ComboBoxText& cr, std::vector<std::string>& strings)
433 {
434         strings.clear ();
435         Glib::RefPtr<const Gtk::TreeModel> m = cr.get_model();
436         if (!m) {
437                 return;
438         }
439         for(Gtk::TreeModel::iterator i = m->children().begin(); i != m->children().end(); ++i) {
440                 Glib::ustring txt;
441                 (*i)->get_value(0, txt);
442                 strings.push_back (txt);
443         }
444 }
445
446 size_t
447 Gtkmm2ext::get_popdown_string_count (Gtk::ComboBoxText& cr)
448 {
449         Glib::RefPtr<const Gtk::TreeModel> m = cr.get_model();
450         if (!m) {
451                 return 0;
452         }
453         return m->children().size();
454 }
455
456 bool
457 Gtkmm2ext::contains_value (Gtk::ComboBoxText& cr, const std::string text)
458 {
459         std::vector<std::string> s;
460         get_popdown_strings (cr, s);
461         return (std::find (s.begin(), s.end(), text) != s.end());
462 }
463
464 bool
465 Gtkmm2ext::set_active_text_if_present (Gtk::ComboBoxText& cr, const std::string text)
466 {
467         if (contains_value(cr, text)) {
468                 cr.set_active_text (text);
469                 return true;
470         }
471         return false;
472 }
473
474 GdkWindow*
475 Gtkmm2ext::get_paned_handle (Gtk::Paned& paned)
476 {
477         return GTK_PANED(paned.gobj())->handle;
478 }
479
480 void
481 Gtkmm2ext::set_decoration (Gtk::Window* win, Gdk::WMDecoration decor)
482 {
483         win->get_window()->set_decorations (decor);
484 }
485
486 void Gtkmm2ext::set_treeview_header_as_default_label(Gtk::TreeViewColumn* c)
487 {
488         gtk_tree_view_column_set_widget( c->gobj(), GTK_WIDGET(0) );
489 }
490
491 void
492 Gtkmm2ext::detach_menu (Gtk::Menu& menu)
493 {
494         /* its possible for a Gtk::Menu to have no gobj() because it has
495            not yet been instantiated. Catch this and provide a safe
496            detach method.
497         */
498         if (menu.gobj()) {
499                 if (menu.get_attach_widget()) {
500                         menu.detach ();
501                 }
502         }
503 }
504
505 bool
506 Gtkmm2ext::possibly_translate_keyval_to_make_legal_accelerator (uint32_t& keyval)
507 {
508         int fakekey = GDK_VoidSymbol;
509
510         switch (keyval) {
511         case GDK_Tab:
512         case GDK_ISO_Left_Tab:
513                 fakekey = GDK_nabla;
514                 break;
515
516         case GDK_Up:
517                 fakekey = GDK_uparrow;
518                 break;
519
520         case GDK_Down:
521                 fakekey = GDK_downarrow;
522                 break;
523
524         case GDK_Right:
525                 fakekey = GDK_rightarrow;
526                 break;
527
528         case GDK_Left:
529                 fakekey = GDK_leftarrow;
530                 break;
531
532         case GDK_Return:
533                 fakekey = GDK_3270_Enter;
534                 break;
535
536         case GDK_KP_Enter:
537                 fakekey = GDK_F35;
538                 break;
539
540         default:
541                 break;
542         }
543
544         if (fakekey != GDK_VoidSymbol) {
545                 keyval = fakekey;
546                 return true;
547         }
548
549         return false;
550 }
551
552 uint32_t
553 Gtkmm2ext::possibly_translate_legal_accelerator_to_real_key (uint32_t keyval)
554 {
555         switch (keyval) {
556         case GDK_nabla:
557                 return GDK_Tab;
558                 break;
559
560         case GDK_uparrow:
561                 return GDK_Up;
562                 break;
563
564         case GDK_downarrow:
565                 return GDK_Down;
566                 break;
567
568         case GDK_rightarrow:
569                 return GDK_Right;
570                 break;
571
572         case GDK_leftarrow:
573                 return GDK_Left;
574                 break;
575
576         case GDK_3270_Enter:
577                 return GDK_Return;
578
579         case GDK_F35:
580                 return GDK_KP_Enter;
581                 break;
582         }
583
584         return keyval;
585 }
586
587 int
588 Gtkmm2ext::physical_screen_height (Glib::RefPtr<Gdk::Window> win)
589 {
590         GdkScreen* scr = gdk_screen_get_default();
591
592         if (win) {
593                 GdkRectangle r;
594                 gint monitor = gdk_screen_get_monitor_at_window (scr, win->gobj());
595                 gdk_screen_get_monitor_geometry (scr, monitor, &r);
596                 return r.height;
597         } else {
598                 return gdk_screen_get_height (scr);
599         }
600 }
601
602 int
603 Gtkmm2ext::physical_screen_width (Glib::RefPtr<Gdk::Window> win)
604 {
605         GdkScreen* scr = gdk_screen_get_default();
606
607         if (win) {
608                 GdkRectangle r;
609                 gint monitor = gdk_screen_get_monitor_at_window (scr, win->gobj());
610                 gdk_screen_get_monitor_geometry (scr, monitor, &r);
611                 return r.width;
612         } else {
613                 return gdk_screen_get_width (scr);
614         }
615 }
616
617 void
618 Gtkmm2ext::container_clear (Gtk::Container& c)
619 {
620         list<Gtk::Widget*> children = c.get_children();
621         for (list<Gtk::Widget*>::iterator child = children.begin(); child != children.end(); ++child) {
622                 c.remove (**child);
623         }
624 }
625
626 void
627 Gtkmm2ext::rounded_rectangle (Cairo::RefPtr<Cairo::Context> context, double x, double y, double w, double h, double r)
628 {
629         rounded_rectangle (context->cobj(), x, y, w, h, r);
630 }
631 void
632 Gtkmm2ext::rounded_top_rectangle (Cairo::RefPtr<Cairo::Context> context, double x, double y, double w, double h, double r)
633 {
634         rounded_top_rectangle (context->cobj(), x, y, w, h, r);
635 }
636 void
637 Gtkmm2ext::rounded_top_left_rectangle (Cairo::RefPtr<Cairo::Context> context, double x, double y, double w, double h, double r)
638 {
639         rounded_top_left_rectangle (context->cobj(), x, y, w, h, r);
640 }
641 void
642 Gtkmm2ext::rounded_top_right_rectangle (Cairo::RefPtr<Cairo::Context> context, double x, double y, double w, double h, double r)
643 {
644         rounded_top_right_rectangle (context->cobj(), x, y, w, h, r);
645 }
646 void
647 Gtkmm2ext::rounded_top_half_rectangle (Cairo::RefPtr<Cairo::Context> context, double x, double y, double w, double h, double r)
648 {
649         rounded_top_half_rectangle (context->cobj(), x, y, w, h, r);
650 }
651 void
652 Gtkmm2ext::rounded_bottom_half_rectangle (Cairo::RefPtr<Cairo::Context> context, double x, double y, double w, double h, double r)
653 {
654         rounded_bottom_half_rectangle (context->cobj(), x, y, w, h, r);
655 }
656
657 void
658 Gtkmm2ext::rounded_left_half_rectangle (Cairo::RefPtr<Cairo::Context> context, double x, double y, double w, double h, double r)
659 {
660         rounded_left_half_rectangle (context->cobj(), x, y, w, h, r);
661 }
662
663 void
664 Gtkmm2ext::rounded_right_half_rectangle (Cairo::RefPtr<Cairo::Context> context, double x, double y, double w, double h, double r)
665 {
666         rounded_right_half_rectangle (context->cobj(), x, y, w, h, r);
667 }
668
669 void
670 Gtkmm2ext::rounded_rectangle (cairo_t* cr, double x, double y, double w, double h, double r)
671 {
672         double degrees = M_PI / 180.0;
673
674         cairo_new_sub_path (cr);
675         cairo_arc (cr, x + w - r, y + r, r, -90 * degrees, 0 * degrees);  //tr
676         cairo_arc (cr, x + w - r, y + h - r, r, 0 * degrees, 90 * degrees);  //br
677         cairo_arc (cr, x + r, y + h - r, r, 90 * degrees, 180 * degrees);  //bl
678         cairo_arc (cr, x + r, y + r, r, 180 * degrees, 270 * degrees);  //tl
679         cairo_close_path (cr);
680 }
681
682 void
683 Gtkmm2ext::rounded_left_half_rectangle (cairo_t* cr, double x, double y, double w, double h, double r)
684 {
685         double degrees = M_PI / 180.0;
686
687         cairo_new_sub_path (cr);
688         cairo_line_to (cr, x+w, y); // tr
689         cairo_line_to (cr, x+w, y + h); // br
690         cairo_arc (cr, x + r, y + h - r, r, 90 * degrees, 180 * degrees);  //bl
691         cairo_arc (cr, x + r, y + r, r, 180 * degrees, 270 * degrees);  //tl
692         cairo_close_path (cr);
693 }
694
695 void
696 Gtkmm2ext::rounded_right_half_rectangle (cairo_t* cr, double x, double y, double w, double h, double r)
697 {
698         double degrees = M_PI / 180.0;
699
700         cairo_new_sub_path (cr);
701         cairo_arc (cr, x + w - r, y + r, r, -90 * degrees, 0 * degrees);  //tr
702         cairo_arc (cr, x + w - r, y + h - r, r, 0 * degrees, 90 * degrees);  //br
703         cairo_line_to (cr, x, y + h); // bl
704         cairo_line_to (cr, x, y); // tl
705         cairo_close_path (cr);
706 }
707
708 void
709 Gtkmm2ext::rounded_top_half_rectangle (cairo_t* cr, double x, double y, double w, double h, double r)
710 {
711         double degrees = M_PI / 180.0;
712
713         cairo_new_sub_path (cr);
714         cairo_move_to (cr, x+w, y+h);
715         cairo_line_to (cr, x, y+h);
716         cairo_arc (cr, x + r, y + r, r, 180 * degrees, 270 * degrees);  //tl
717         cairo_arc (cr, x + w - r, y + r, r, -90 * degrees, 0 * degrees);  //tr
718         cairo_close_path (cr);
719 }
720
721 void
722 Gtkmm2ext::rounded_bottom_half_rectangle (cairo_t* cr, double x, double y, double w, double h, double r)
723 {
724         double degrees = M_PI / 180.0;
725
726         cairo_new_sub_path (cr);
727         cairo_move_to (cr, x, y);
728         cairo_line_to (cr, x+w, y);
729         cairo_arc (cr, x + w - r, y + h - r, r, 0 * degrees, 90 * degrees);  //br
730         cairo_arc (cr, x + r, y + h - r, r, 90 * degrees, 180 * degrees);  //bl
731         cairo_close_path (cr);
732 }
733
734
735 void
736 Gtkmm2ext::rounded_top_rectangle (cairo_t* cr, double x, double y, double w, double h, double r)
737 {
738         double degrees = M_PI / 180.0;
739
740         cairo_new_sub_path (cr);
741         cairo_move_to (cr, x+w, y+h);
742         cairo_line_to (cr, x, y+h);
743         cairo_arc (cr, x + r, y + r, r, 180 * degrees, 270 * degrees);  //tl
744         cairo_arc (cr, x + w - r, y + r, r, -90 * degrees, 0 * degrees);  //tr
745         cairo_close_path (cr);
746 }
747
748 void
749 Gtkmm2ext::rounded_top_left_rectangle (cairo_t* cr, double x, double y, double w, double h, double r)
750 {
751 /*    A****B
752       H    *
753       *    *
754       *    *
755       F****E
756 */
757         cairo_move_to (cr, x+r,y); // Move to A
758         cairo_line_to (cr, x+w,y); // Straight line to B
759         cairo_line_to (cr, x+w,y+h); // Move to E
760         cairo_line_to (cr, x,y+h); // Line to F
761         cairo_line_to (cr, x,y+r); // Line to H
762         cairo_curve_to (cr, x,y,x,y,x+r,y); // Curve to A
763 }
764
765 void
766 Gtkmm2ext::rounded_top_right_rectangle (cairo_t* cr, double x, double y, double w, double h, double r)
767 {
768 /*    A****BQ
769       *    C
770       *    *
771       *    *
772       F****E
773 */
774         cairo_move_to (cr, x,y); // Move to A
775         cairo_line_to (cr, x+w-r,y); // Straight line to B
776         cairo_curve_to (cr, x+w,y,x+w,y,x+w,y+r); // Curve to C, Control points are both at Q
777         cairo_line_to (cr, x+w,y+h); // Move to E
778         cairo_line_to (cr, x,y+h); // Line to F
779         cairo_line_to (cr, x,y); // Line to A
780 }
781
782 Glib::RefPtr<Gdk::Window>
783 Gtkmm2ext::window_to_draw_on (Gtk::Widget& w, Gtk::Widget** parent)
784 {
785         if (w.get_has_window()) {
786                 return w.get_window();
787         }
788
789         (*parent) = w.get_parent();
790
791         while (*parent) {
792                 if ((*parent)->get_has_window()) {
793                         return (*parent)->get_window ();
794                 }
795                 (*parent) = (*parent)->get_parent ();
796         }
797
798         return Glib::RefPtr<Gdk::Window> ();
799 }
800
801 int
802 Gtkmm2ext::pixel_width (const string& str, const Pango::FontDescription& font)
803 {
804         Glib::RefPtr<Pango::Context> context = Glib::wrap (gdk_pango_context_get());
805         Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create (context);
806
807         layout->set_font_description (font);
808         layout->set_text (str);
809
810         int width, height;
811         Gtkmm2ext::get_ink_pixel_size (layout, width, height);
812
813 #ifdef __APPLE__
814         // Pango returns incorrect text width on some OS X
815         // So we have to make a correction
816         // To determine the correct indent take the largest symbol for which the width is correct
817         // and make the calculation
818         //
819         // see also libs/canvas/text.cc
820         int cor_width;
821         layout->set_text ("H");
822         layout->get_pixel_size (cor_width, height);
823         width += cor_width * 1.5;
824 #endif
825
826         return width;
827 }
828
829 void
830 Gtkmm2ext::pixel_size (const string& str, const Pango::FontDescription& font, int& width, int& height)
831 {
832         Gtk::Label foo;
833         Glib::RefPtr<Pango::Layout> layout = foo.create_pango_layout ("");
834
835         layout->set_font_description (font);
836         layout->set_text (str);
837
838         Gtkmm2ext::get_ink_pixel_size (layout, width, height);
839 }
840
841 #if 0
842 string
843 Gtkmm2ext::fit_to_pixels (const string& str, int pixel_width, Pango::FontDescription& font, int& actual_width, bool with_ellipses)
844 {
845         /* DECEMBER 2011: THIS PROTOTYPE OF fit_to_pixels() IS NOT USED
846            ANYWHERE AND HAS NOT BEEN TESTED.
847         */
848         Gtk::Label foo;
849         Glib::RefPtr<Pango::Layout> layout = foo.create_pango_layout (str);
850         Glib::RefPtr<const Pango::LayoutLine> line;
851
852         layout->set_font_description (font);
853         layout->set_width (pixel_width * PANGO_SCALE);
854
855         if (with_ellipses) {
856                 layout->set_ellipsize (Pango::ELLIPSIZE_END);
857         } else {
858                 layout->set_wrap (Pango::WRAP_CHAR);
859         }
860
861         line = layout->get_line (0);
862
863         /* XXX: might need special care to get the ellipsis character, not sure
864            how that works
865         */
866
867         string s = string (layout->get_text ().substr(line->get_start_index(), line->get_length()));
868
869         cerr << "fit to pixels of " << str << " returns " << s << endl;
870
871         return s;
872 }
873 #endif
874
875 /** Try to fit a string into a given horizontal space by ellipsizing it.
876  *  @param cr Cairo context in which the text will be plotted.
877  *  @param name Text.
878  *  @param avail Available horizontal space.
879  *  @return (Text, possibly ellipsized) and (horizontal size of text)
880  */
881
882 std::pair<std::string, double>
883 Gtkmm2ext::fit_to_pixels (cairo_t* cr, std::string name, double avail)
884 {
885         /* XXX hopefully there exists a more efficient way of doing this */
886
887         bool abbreviated = false;
888         uint32_t width = 0;
889
890         while (1) {
891                 cairo_text_extents_t ext;
892                 cairo_text_extents (cr, name.c_str(), &ext);
893
894                 if (ext.width < avail || name.length() <= 4) {
895                         width = ext.width;
896                         break;
897                 }
898
899                 if (abbreviated) {
900                         name = name.substr (0, name.length() - 4) + "...";
901                 } else {
902                         name = name.substr (0, name.length() - 3) + "...";
903                         abbreviated = true;
904                 }
905         }
906
907         return std::make_pair (name, width);
908 }
909
910 Gtk::Label *
911 Gtkmm2ext::left_aligned_label (string const & t)
912 {
913         Gtk::Label* l = new Gtk::Label (t);
914         l->set_alignment (0, 0.5);
915         return l;
916 }
917
918 Gtk::Label *
919 Gtkmm2ext::right_aligned_label (string const & t)
920 {
921         Gtk::Label* l = new Gtk::Label (t);
922         l->set_alignment (1, 0.5);
923         return l;
924 }
925
926 static bool
927 make_null_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>& t)
928 {
929         t->set_tip_area (Gdk::Rectangle (0, 0, 0, 0));
930         return true;
931 }
932
933 /** Hackily arrange for the provided widget to have no tooltip,
934  *  and also to stop any other widget from providing one while
935  * the mouse is over w.
936  */
937 void
938 Gtkmm2ext::set_no_tooltip_whatsoever (Gtk::Widget& w)
939 {
940         w.property_has_tooltip() = true;
941         w.signal_query_tooltip().connect (sigc::ptr_fun (make_null_tooltip));
942 }
943
944 void
945 Gtkmm2ext::enable_tooltips ()
946 {
947         gtk_rc_parse_string ("gtk-enable-tooltips = 1");
948         PersistentTooltip::set_tooltips_enabled (true);
949 }
950
951 void
952 Gtkmm2ext::disable_tooltips ()
953 {
954         gtk_rc_parse_string ("gtk-enable-tooltips = 0");
955         PersistentTooltip::set_tooltips_enabled (false);
956 }
957
958 bool
959 Gtkmm2ext::event_inside_widget_window (Gtk::Widget& widget, GdkEvent* ev)
960 {
961         gdouble evx, evy;
962
963         if (!gdk_event_get_root_coords (ev, &evx, &evy)) {
964                 return false;
965         }
966
967         gint wx;
968         gint wy;
969         gint width, height, depth;
970         gint x, y;
971
972         Glib::RefPtr<Gdk::Window> widget_window = widget.get_window();
973
974         widget_window->get_geometry (x, y, width, height, depth);
975         widget_window->get_root_origin (wx, wy);
976
977         if ((evx >= wx && evx < wx + width) &&
978             (evy >= wy && evy < wy + height)) {
979                 return true;
980         }
981
982         return false;
983 }
984
985 const char*
986 Gtkmm2ext::event_type_string (int event_type)
987 {
988         switch (event_type) {
989         case GDK_NOTHING:
990                 return "nothing";
991         case GDK_DELETE:
992                 return "delete";
993         case GDK_DESTROY:
994                 return "destroy";
995         case GDK_EXPOSE:
996                 return "expose";
997         case GDK_MOTION_NOTIFY:
998                 return "motion_notify";
999         case GDK_BUTTON_PRESS:
1000                 return "button_press";
1001         case GDK_2BUTTON_PRESS:
1002                 return "2button_press";
1003         case GDK_3BUTTON_PRESS:
1004                 return "3button_press";
1005         case GDK_BUTTON_RELEASE:
1006                 return "button_release";
1007         case GDK_KEY_PRESS:
1008                 return "key_press";
1009         case GDK_KEY_RELEASE:
1010                 return "key_release";
1011         case GDK_ENTER_NOTIFY:
1012                 return "enter_notify";
1013         case GDK_LEAVE_NOTIFY:
1014                 return "leave_notify";
1015         case GDK_FOCUS_CHANGE:
1016                 return "focus_change";
1017         case GDK_CONFIGURE:
1018                 return "configure";
1019         case GDK_MAP:
1020                 return "map";
1021         case GDK_UNMAP:
1022                 return "unmap";
1023         case GDK_PROPERTY_NOTIFY:
1024                 return "property_notify";
1025         case GDK_SELECTION_CLEAR:
1026                 return "selection_clear";
1027         case GDK_SELECTION_REQUEST:
1028                 return "selection_request";
1029         case GDK_SELECTION_NOTIFY:
1030                 return "selection_notify";
1031         case GDK_PROXIMITY_IN:
1032                 return "proximity_in";
1033         case GDK_PROXIMITY_OUT:
1034                 return "proximity_out";
1035         case GDK_DRAG_ENTER:
1036                 return "drag_enter";
1037         case GDK_DRAG_LEAVE:
1038                 return "drag_leave";
1039         case GDK_DRAG_MOTION:
1040                 return "drag_motion";
1041         case GDK_DRAG_STATUS:
1042                 return "drag_status";
1043         case GDK_DROP_START:
1044                 return "drop_start";
1045         case GDK_DROP_FINISHED:
1046                 return "drop_finished";
1047         case GDK_CLIENT_EVENT:
1048                 return "client_event";
1049         case GDK_VISIBILITY_NOTIFY:
1050                 return "visibility_notify";
1051         case GDK_NO_EXPOSE:
1052                 return "no_expose";
1053         case GDK_SCROLL:
1054                 return "scroll";
1055         case GDK_WINDOW_STATE:
1056                 return "window_state";
1057         case GDK_SETTING:
1058                 return "setting";
1059         case GDK_OWNER_CHANGE:
1060                 return "owner_change";
1061         case GDK_GRAB_BROKEN:
1062                 return "grab_broken";
1063         case GDK_DAMAGE:
1064                 return "damage";
1065         }
1066
1067         return "unknown";
1068 }
1069
1070 std::string
1071 Gtkmm2ext::markup_escape_text (std::string const& s)
1072 {
1073         return Glib::Markup::escape_text (s);
1074 }
1075
1076 void
1077 Gtkmm2ext::add_volume_shortcuts (Gtk::FileChooser& c)
1078 {
1079 #ifdef __APPLE__
1080         try {
1081                 /* This is a first order approach, listing all mounted volumes (incl network).
1082                  * One could use `diskutil` or `mount` to query local disks only, or
1083                  * something even fancier if deemed appropriate.
1084                  */
1085                 Glib::Dir dir("/Volumes");
1086                 for (Glib::DirIterator di = dir.begin(); di != dir.end(); di++) {
1087                         string fullpath = Glib::build_filename ("/Volumes", *di);
1088                         if (!Glib::file_test (fullpath, Glib::FILE_TEST_IS_DIR)) continue;
1089
1090                         try { /* add_shortcut_folder throws an exception if the folder being added already has a shortcut */
1091                                 c.add_shortcut_folder (fullpath);
1092                         }
1093                         catch (Glib::Error& e) {
1094                                 std::cerr << "add_shortcut_folder() threw Glib::Error: " << e.what() << std::endl;
1095                         }
1096                 }
1097         }
1098         catch (Glib::FileError& e) {
1099                 std::cerr << "listing /Volumnes failed: " << e.what() << std::endl;
1100         }
1101 #endif
1102 }
1103
1104 float
1105 Gtkmm2ext::paned_position_as_fraction (Gtk::Paned& paned, bool h)
1106 {
1107         const guint pos = gtk_paned_get_position (const_cast<GtkPaned*>(static_cast<const Gtk::Paned*>(&paned)->gobj()));
1108         return (double) pos / (h ? paned.get_allocation().get_height() : paned.get_allocation().get_width());
1109 }
1110
1111 void
1112 Gtkmm2ext::paned_set_position_as_fraction (Gtk::Paned& paned, float fraction, bool h)
1113 {
1114         gint v = (h ? paned.get_allocation().get_height() : paned.get_allocation().get_width());
1115
1116         if (v < 1) {
1117                 return;
1118         }
1119
1120         paned.set_position ((guint) floor (fraction * v));
1121 }
1122
1123 string
1124 Gtkmm2ext::show_gdk_event_state (int state)
1125 {
1126         string s;
1127         if (state & GDK_SHIFT_MASK) {
1128                 s += "+SHIFT";
1129         }
1130         if (state & GDK_LOCK_MASK) {
1131                 s += "+LOCK";
1132         }
1133         if (state & GDK_CONTROL_MASK) {
1134                 s += "+CONTROL";
1135         }
1136         if (state & GDK_MOD1_MASK) {
1137                 s += "+MOD1";
1138         }
1139         if (state & GDK_MOD2_MASK) {
1140                 s += "+MOD2";
1141         }
1142         if (state & GDK_MOD3_MASK) {
1143                 s += "+MOD3";
1144         }
1145         if (state & GDK_MOD4_MASK) {
1146                 s += "+MOD4";
1147         }
1148         if (state & GDK_MOD5_MASK) {
1149                 s += "+MOD5";
1150         }
1151         if (state & GDK_BUTTON1_MASK) {
1152                 s += "+BUTTON1";
1153         }
1154         if (state & GDK_BUTTON2_MASK) {
1155                 s += "+BUTTON2";
1156         }
1157         if (state & GDK_BUTTON3_MASK) {
1158                 s += "+BUTTON3";
1159         }
1160         if (state & GDK_BUTTON4_MASK) {
1161                 s += "+BUTTON4";
1162         }
1163         if (state & GDK_BUTTON5_MASK) {
1164                 s += "+BUTTON5";
1165         }
1166         if (state & GDK_SUPER_MASK) {
1167                 s += "+SUPER";
1168         }
1169         if (state & GDK_HYPER_MASK) {
1170                 s += "+HYPER";
1171         }
1172         if (state & GDK_META_MASK) {
1173                 s += "+META";
1174         }
1175         if (state & GDK_RELEASE_MASK) {
1176                 s += "+RELEASE";
1177         }
1178
1179         return s;
1180 }