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