X-Git-Url: https://git.carlh.net/gitweb/?a=blobdiff_plain;f=libs%2Fgtkmm2ext%2Futils.cc;h=06cd0b82a995cac76e73a38c963a10620e0c00e6;hb=0354dbdfe95d7c9ea1156cb23de038020ac73bf4;hp=0a808258804beaa05171ccd66207269b5a8f1434;hpb=57321bef35ac9ca98927e2fbb95845122d85303d;p=ardour.git diff --git a/libs/gtkmm2ext/utils.cc b/libs/gtkmm2ext/utils.cc index 0a80825880..06cd0b82a9 100644 --- a/libs/gtkmm2ext/utils.cc +++ b/libs/gtkmm2ext/utils.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 1999 Paul Barton-Davis + Copyright (C) 1999 Paul Barton-Davis This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -31,17 +32,19 @@ #include #include #include +#include #include "gtkmm2ext/utils.h" +#include "gtkmm2ext/persistent_tooltip.h" -#include "i18n.h" +#include "pbd/i18n.h" using namespace std; void Gtkmm2ext::init (const char* localedir) { -#ifdef ENABLE_NLS +#if ENABLE_NLS (void) bindtextdomain(PACKAGE, localedir); (void) bind_textdomain_codeset (PACKAGE, "UTF-8"); #endif @@ -53,7 +56,7 @@ Gtkmm2ext::get_ink_pixel_size (Glib::RefPtr layout, int& height) { Pango::Rectangle ink_rect = layout->get_ink_extents (); - + width = PANGO_PIXELS(ink_rect.get_width()); height = PANGO_PIXELS(ink_rect.get_height()); } @@ -72,13 +75,13 @@ Gtkmm2ext::set_size_request_to_display_given_text (Gtk::Widget &w, const gchar * { int width, height; w.ensure_style (); - + get_pixel_size (w.create_pango_layout (text), width, height); w.set_size_request(width + hpadding, height + vpadding); } /** Set width request to display given text, and height to display anything. - This is useful for setting many widgets to the same height for consistency. */ + * This is useful for setting many widgets to the same height for consistency. */ void Gtkmm2ext::set_size_request_to_display_given_text_width (Gtk::Widget& w, const gchar* htext, @@ -117,13 +120,13 @@ Gtkmm2ext::set_size_request_to_display_given_text (Gtk::Widget &w, std::string c { int width, height; w.ensure_style (); - + get_pixel_size (w.create_pango_layout (text), width, height); w.set_size_request(width + hpadding, height + vpadding); } void -Gtkmm2ext::set_size_request_to_display_given_text (Gtk::Widget &w, +Gtkmm2ext::set_size_request_to_display_given_text (Gtk::Widget &w, const std::vector& strings, gint hpadding, gint vpadding) { @@ -131,26 +134,26 @@ Gtkmm2ext::set_size_request_to_display_given_text (Gtk::Widget &w, int width_max = 0; int height_max = 0; w.ensure_style (); - vector copy; - const vector* to_use; - vector::const_iterator i; - - for (i = strings.begin(); i != strings.end(); ++i) { - if ((*i).find_first_of ("gy") != string::npos) { - /* contains a descender */ - break; - } - } - - if (i == strings.end()) { - /* make a copy of the strings then add one that has a descender */ - copy = strings; - copy.push_back ("g"); - to_use = © - } else { - to_use = &strings; - } - + vector copy; + const vector* to_use; + vector::const_iterator i; + + for (i = strings.begin(); i != strings.end(); ++i) { + if ((*i).find_first_of ("gy") != string::npos) { + /* contains a descender */ + break; + } + } + + if (i == strings.end()) { + /* make a copy of the strings then add one that has a descender */ + copy = strings; + copy.push_back ("g"); + to_use = © + } else { + to_use = &strings; + } + for (vector::const_iterator i = to_use->begin(); i != to_use->end(); ++i) { get_pixel_size (w.create_pango_layout (*i), width, height); width_max = max(width_max,width); @@ -161,8 +164,9 @@ Gtkmm2ext::set_size_request_to_display_given_text (Gtk::Widget &w, } /** This version specifies horizontal padding in text to avoid assumptions - about font size. Should be used anywhere padding is used to avoid text, - like combo boxes. */ + * about font size. Should be used anywhere padding is used to avoid text, + * like combo boxes. + */ void Gtkmm2ext::set_size_request_to_display_given_text (Gtk::Widget& w, const std::vector& strings, @@ -191,15 +195,15 @@ static inline guint8 demultiply_alpha (guint8 src, guint8 alpha) { - /* cairo pixel buffer data contains RGB values with the alpha - values premultiplied. - - GdkPixbuf pixel buffer data contains RGB values without the - alpha value applied. - - this removes the alpha component from the cairo version and - returns the GdkPixbuf version. - */ + /* cairo pixel buffer data contains RGB values with the alpha + * values premultiplied. + * + * GdkPixbuf pixel buffer data contains RGB values without the + * alpha value applied. + * + * this removes the alpha component from the cairo version and + * returns the GdkPixbuf version. + */ return alpha ? ((guint (src) << 8) - src) / alpha : 0; } @@ -211,51 +215,51 @@ Gtkmm2ext::convert_bgra_to_rgba (guint8 const* src, { guint8 const* src_pixel = src; guint8* dst_pixel = dst; - - /* cairo pixel data is endian-dependent ARGB with A in the most significant 8 bits, - with premultipled alpha values (see preceding function) - - GdkPixbuf pixel data is non-endian-dependent RGBA with R in the lowest addressable - 8 bits, and non-premultiplied alpha values. - convert from the cairo values to the GdkPixbuf ones. - */ + /* cairo pixel data is endian-dependent ARGB with A in the most significant 8 bits, + * with premultipled alpha values (see preceding function) + * + * GdkPixbuf pixel data is non-endian-dependent RGBA with R in the lowest addressable + * 8 bits, and non-premultiplied alpha values. + * + * convert from the cairo values to the GdkPixbuf ones. + */ for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { + for (int x = 0; x < width; x++) { #if G_BYTE_ORDER == G_LITTLE_ENDIAN - /* Cairo [ B G R A ] is actually [ B G R A ] in memory SOURCE - 0 1 2 3 - Pixbuf [ R G B A ] is actually [ R G B A ] in memory DEST - */ - dst_pixel[0] = demultiply_alpha (src_pixel[2], - src_pixel[3]); // R [0] <= [ 2 ] - dst_pixel[1] = demultiply_alpha (src_pixel[1], - src_pixel[3]); // G [1] <= [ 1 ] - dst_pixel[2] = demultiply_alpha (src_pixel[0], - src_pixel[3]); // B [2] <= [ 0 ] - dst_pixel[3] = src_pixel[3]; // alpha - + /* Cairo [ B G R A ] is actually [ B G R A ] in memory SOURCE + 0 1 2 3 + Pixbuf [ R G B A ] is actually [ R G B A ] in memory DEST + */ + dst_pixel[0] = demultiply_alpha (src_pixel[2], + src_pixel[3]); // R [0] <= [ 2 ] + dst_pixel[1] = demultiply_alpha (src_pixel[1], + src_pixel[3]); // G [1] <= [ 1 ] + dst_pixel[2] = demultiply_alpha (src_pixel[0], + src_pixel[3]); // B [2] <= [ 0 ] + dst_pixel[3] = src_pixel[3]; // alpha + #elif G_BYTE_ORDER == G_BIG_ENDIAN - /* Cairo [ B G R A ] is actually [ A R G B ] in memory SOURCE - 0 1 2 3 - Pixbuf [ R G B A ] is actually [ R G B A ] in memory DEST - */ - dst_pixel[0] = demultiply_alpha (src_pixel[1], - src_pixel[0]); // R [0] <= [ 1 ] - dst_pixel[1] = demultiply_alpha (src_pixel[2], - src_pixel[0]); // G [1] <= [ 2 ] - dst_pixel[2] = demultiply_alpha (src_pixel[3], - src_pixel[0]); // B [2] <= [ 3 ] - dst_pixel[3] = src_pixel[0]; // alpha - + /* Cairo [ B G R A ] is actually [ A R G B ] in memory SOURCE + 0 1 2 3 + Pixbuf [ R G B A ] is actually [ R G B A ] in memory DEST + */ + dst_pixel[0] = demultiply_alpha (src_pixel[1], + src_pixel[0]); // R [0] <= [ 1 ] + dst_pixel[1] = demultiply_alpha (src_pixel[2], + src_pixel[0]); // G [1] <= [ 2 ] + dst_pixel[2] = demultiply_alpha (src_pixel[3], + src_pixel[0]); // B [2] <= [ 3 ] + dst_pixel[3] = src_pixel[0]; // alpha + #else #error ardour does not currently support PDP-endianess -#endif - - dst_pixel += 4; - src_pixel += 4; - } +#endif + + dst_pixel += 4; + src_pixel += 4; + } } } @@ -272,21 +276,33 @@ Gtkmm2ext::pixbuf_from_string(const string& name, const Pango::FontDescription& return *empty_pixbuf; } + if (clip_width <= 0 || clip_height <= 0) { + /* negative values mean padding around natural size */ + int width, height; + pixel_size (name, font, width, height); + if (clip_width <= 0) { + clip_width = width - clip_width; + } + if (clip_height <= 0) { + clip_height = height - clip_height; + } + } + Glib::RefPtr buf = Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB, true, 8, clip_width, clip_height); cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, clip_width, clip_height); cairo_t* cr = cairo_create (surface); cairo_text_extents_t te; - + cairo_set_source_rgba (cr, fg.get_red_p(), fg.get_green_p(), fg.get_blue_p(), 1.0); cairo_select_font_face (cr, font.get_family().c_str(), CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); cairo_set_font_size (cr, font.get_size() / Pango::SCALE); cairo_text_extents (cr, name.c_str(), &te); - + cairo_move_to (cr, 0.5, int (0.5 - te.height / 2 - te.y_bearing + clip_height / 2)); cairo_show_text (cr, name.c_str()); - + convert_bgra_to_rgba(cairo_image_surface_get_data (surface), buf->get_pixels(), clip_width, clip_height); cairo_destroy(cr); @@ -295,6 +311,127 @@ Gtkmm2ext::pixbuf_from_string(const string& name, const Pango::FontDescription& return buf; } +void +_position_menu_anchored (int& x, int& y, bool& push_in, + const Gtk::Menu* const menu, + Gtk::Widget* const anchor, + const std::string& selected) +{ + using namespace Gtk; + using namespace Gtk::Menu_Helpers; + + /* TODO: lacks support for rotated dropdown buttons */ + + if (!anchor->has_screen () || !anchor->get_has_window ()) { + return; + } + + Gdk::Rectangle monitor; + { + const int monitor_num = anchor->get_screen ()->get_monitor_at_window ( + anchor->get_window ()); + anchor->get_screen ()->get_monitor_geometry ( + (monitor_num < 0) ? 0 : monitor_num, monitor); + } + + const Requisition menu_req = menu->size_request(); + const Gdk::Rectangle allocation = anchor->get_allocation(); + + /* The x and y position are handled separately. + * + * For the x position if the direction is LTR (or RTL), then we try in order: + * a) align the left (right) of the menu with the left (right) of the button + * if there's enough room until the right (left) border of the screen; + * b) align the right (left) of the menu with the right (left) of the button + * if there's enough room until the left (right) border of the screen; + * c) align the right (left) border of the menu with the right (left) border + * of the screen if there's enough space; + * d) align the left (right) border of the menu with the left (right) border + * of the screen, with the rightmost (leftmost) part of the menu that + * overflows the screen. + * XXX We always align left regardless of the direction because if x is + * left of the current monitor, the menu popup code after us notices it + * and enforces that the menu stays in the monitor that's at the left...*/ + + anchor->get_window ()->get_origin (x, y); + + if (anchor->get_direction() == TEXT_DIR_RTL) { + if (monitor.get_x() <= x + allocation.get_width() - menu_req.width) { + /* a) align menu right and button right */ + x += allocation.get_width() - menu_req.width; + } else if (x + menu_req.width <= monitor.get_x() + monitor.get_width()) { + /* b) align menu left and button left: nothing to do*/ + } else if (menu_req.width > monitor.get_width()) { + /* c) align menu left and screen left, guaranteed to fit */ + x = monitor.get_x(); + } else { + /* d) XXX align left or the menu might change monitors */ + x = monitor.get_x(); + } + } else { /* LTR */ + if (x + menu_req.width <= monitor.get_x() + monitor.get_width()) { + /* a) align menu left and button left: nothing to do*/ + } else if (monitor.get_x() <= x + allocation.get_width() - menu_req.width) { + /* b) align menu right and button right */ + x += allocation.get_width() - menu_req.width; + } else if (menu_req.width > monitor.get_width()) { + /* c) align menu right and screen right, guaranteed to fit */ + x = monitor.get_x() + monitor.get_width() - menu_req.width; + } else { + /* d) align left */ + x = monitor.get_x(); + } + } + + /* For the y position, try in order: + * a) if there is a menu item with the same text as the button, align it + * with the button, unless that makes the menu overflow the monitor. + * b) align the top of the menu with the bottom of the button if there is + * enough room below the button; + * c) align the bottom of the menu with the top of the button if there is + * enough room above the button; + * d) align the bottom of the menu with the bottom of the monitor if there + * is enough room, but avoid moving the menu to another monitor */ + + const MenuList& items = menu->items (); + int offset = 0; + + MenuList::const_iterator i = items.begin(); + for ( ; i != items.end(); ++i) { + const Label* label_widget = dynamic_cast(i->get_child()); + if (label_widget && selected == ((std::string) label_widget->get_label())) { + break; + } + offset += i->size_request().height; + } + if (i != items.end() && + y - offset >= monitor.get_y() && + y - offset + menu_req.height <= monitor.get_y() + monitor.get_height()) { + y -= offset; + } else if (y + allocation.get_height() + menu_req.height <= monitor.get_y() + monitor.get_height()) { + y += allocation.get_height(); /* a) */ + } else if ((y - menu_req.height) >= monitor.get_y()) { + y -= menu_req.height; /* b) */ + } else { + y = monitor.get_y() + max(0, monitor.get_height() - menu_req.height); + } + + push_in = false; +} + +void +Gtkmm2ext::anchored_menu_popup (Gtk::Menu* const menu, + Gtk::Widget* const anchor, + const std::string& selected, + guint button, guint32 time) +{ + menu->popup( + sigc::bind (sigc::ptr_fun(&_position_menu_anchored), + menu, anchor, selected), + button, + time); +} + void Gtkmm2ext::set_popdown_strings (Gtk::ComboBoxText& cr, const vector& strings) { @@ -381,30 +518,6 @@ Gtkmm2ext::detach_menu (Gtk::Menu& menu) } } -bool -Gtkmm2ext::possibly_translate_mod_to_make_legal_accelerator (GdkModifierType& mod) -{ -#ifdef GTKOSX - /* GTK on OS X is currently (February 2012) setting both - the Meta and Mod2 bits in the event modifier state if - the Command key is down. - - gtk_accel_groups_activate() does not invoke any of the logic - that gtk_window_activate_key() will that sorts out that stupid - state of affairs, and as a result it fails to find a match - for the key event and the current set of accelerators. - - to fix this, if the meta bit is set, remove the mod2 bit - from the modifier. this assumes that our bindings use Primary - which will have set the meta bit in the accelerator entry. - */ - if (mod & GDK_META_MASK) { - mod = GdkModifierType (mod & ~GDK_MOD2_MASK); - } -#endif - return true; -} - bool Gtkmm2ext::possibly_translate_keyval_to_make_legal_accelerator (uint32_t& keyval) { @@ -490,40 +603,41 @@ Gtkmm2ext::possibly_translate_legal_accelerator_to_real_key (uint32_t keyval) int Gtkmm2ext::physical_screen_height (Glib::RefPtr win) { - GdkScreen* scr = gdk_screen_get_default(); + GdkScreen* scr = gdk_screen_get_default(); - if (win) { - GdkRectangle r; - gint monitor = gdk_screen_get_monitor_at_window (scr, win->gobj()); - gdk_screen_get_monitor_geometry (scr, monitor, &r); - return r.height; - } else { - return gdk_screen_get_height (scr); - } + if (win) { + GdkRectangle r; + gint monitor = gdk_screen_get_monitor_at_window (scr, win->gobj()); + gdk_screen_get_monitor_geometry (scr, monitor, &r); + return r.height; + } else { + return gdk_screen_get_height (scr); + } } int Gtkmm2ext::physical_screen_width (Glib::RefPtr win) { - GdkScreen* scr = gdk_screen_get_default(); - - if (win) { - GdkRectangle r; - gint monitor = gdk_screen_get_monitor_at_window (scr, win->gobj()); - gdk_screen_get_monitor_geometry (scr, monitor, &r); - return r.width; - } else { - return gdk_screen_get_width (scr); - } + GdkScreen* scr = gdk_screen_get_default(); + + if (win) { + GdkRectangle r; + gint monitor = gdk_screen_get_monitor_at_window (scr, win->gobj()); + gdk_screen_get_monitor_geometry (scr, monitor, &r); + return r.width; + } else { + return gdk_screen_get_width (scr); + } } void Gtkmm2ext::container_clear (Gtk::Container& c) { - list children = c.get_children(); - for (list::iterator child = children.begin(); child != children.end(); ++child) { - c.remove (**child); - } + list children = c.get_children(); + for (list::iterator child = children.begin(); child != children.end(); ++child) { + (*child)->hide (); + c.remove (**child); + } } void @@ -702,7 +816,7 @@ Gtkmm2ext::window_to_draw_on (Gtk::Widget& w, Gtk::Widget** parent) } int -Gtkmm2ext::pixel_width (const string& str, Pango::FontDescription& font) +Gtkmm2ext::pixel_width (const string& str, const Pango::FontDescription& font) { Glib::RefPtr context = Glib::wrap (gdk_pango_context_get()); Glib::RefPtr layout = Pango::Layout::create (context); @@ -730,7 +844,7 @@ Gtkmm2ext::pixel_width (const string& str, Pango::FontDescription& font) } void -Gtkmm2ext::pixel_size (const string& str, Pango::FontDescription& font, int& width, int& height) +Gtkmm2ext::pixel_size (const string& str, const Pango::FontDescription& font, int& width, int& height) { Gtk::Label foo; Glib::RefPtr layout = foo.create_pango_layout (""); @@ -756,19 +870,18 @@ Gtkmm2ext::fit_to_pixels (const string& str, int pixel_width, Pango::FontDescrip layout->set_width (pixel_width * PANGO_SCALE); if (with_ellipses) { - layout->set_ellipsize (Pango::ELLIPSIZE_END); + layout->set_ellipsize (Pango::ELLIPSIZE_END); } else { - layout->set_wrap (Pango::WRAP_CHAR); + layout->set_wrap (Pango::WRAP_CHAR); } line = layout->get_line (0); /* XXX: might need special care to get the ellipsis character, not sure - how that works - */ - + * how that works + */ string s = string (layout->get_text ().substr(line->get_start_index(), line->get_length())); - + cerr << "fit to pixels of " << str << " returns " << s << endl; return s; @@ -781,7 +894,6 @@ Gtkmm2ext::fit_to_pixels (const string& str, int pixel_width, Pango::FontDescrip * @param avail Available horizontal space. * @return (Text, possibly ellipsized) and (horizontal size of text) */ - std::pair Gtkmm2ext::fit_to_pixels (cairo_t* cr, std::string name, double avail) { @@ -818,6 +930,14 @@ Gtkmm2ext::left_aligned_label (string const & t) return l; } +Gtk::Label * +Gtkmm2ext::right_aligned_label (string const & t) +{ + Gtk::Label* l = new Gtk::Label (t); + l->set_alignment (1, 0.5); + return l; +} + static bool make_null_tooltip (int, int, bool, const Glib::RefPtr& t) { @@ -840,39 +960,41 @@ void Gtkmm2ext::enable_tooltips () { gtk_rc_parse_string ("gtk-enable-tooltips = 1"); + PersistentTooltip::set_tooltips_enabled (true); } void Gtkmm2ext::disable_tooltips () { gtk_rc_parse_string ("gtk-enable-tooltips = 0"); + PersistentTooltip::set_tooltips_enabled (false); } bool Gtkmm2ext::event_inside_widget_window (Gtk::Widget& widget, GdkEvent* ev) { - gdouble evx, evy; + gdouble evx, evy; + + if (!gdk_event_get_root_coords (ev, &evx, &evy)) { + return false; + } - if (!gdk_event_get_root_coords (ev, &evx, &evy)) { - return false; - } - - gint wx; - gint wy; - gint width, height, depth; - gint x, y; + gint wx; + gint wy; + gint width, height, depth; + gint x, y; - Glib::RefPtr widget_window = widget.get_window(); + Glib::RefPtr widget_window = widget.get_window(); - widget_window->get_geometry (x, y, width, height, depth); - widget_window->get_root_origin (wx, wy); - - if ((evx >= wx && evx < wx + width) && - (evy >= wy && evy < wy + height)) { - return true; - } + widget_window->get_geometry (x, y, width, height, depth); + widget_window->get_root_origin (wx, wy); - return false; + if ((evx >= wx && evx < wx + width) && + (evy >= wy && evy < wy + height)) { + return true; + } + + return false; } const char* @@ -959,3 +1081,115 @@ Gtkmm2ext::event_type_string (int event_type) return "unknown"; } + +std::string +Gtkmm2ext::markup_escape_text (std::string const& s) +{ + return Glib::Markup::escape_text (s); +} + +void +Gtkmm2ext::add_volume_shortcuts (Gtk::FileChooser& c) +{ +#ifdef __APPLE__ + try { + /* This is a first order approach, listing all mounted volumes (incl network). + * One could use `diskutil` or `mount` to query local disks only, or + * something even fancier if deemed appropriate. + */ + Glib::Dir dir("/Volumes"); + for (Glib::DirIterator di = dir.begin(); di != dir.end(); di++) { + string fullpath = Glib::build_filename ("/Volumes", *di); + if (!Glib::file_test (fullpath, Glib::FILE_TEST_IS_DIR)) continue; + + try { /* add_shortcut_folder throws an exception if the folder being added already has a shortcut */ + c.add_shortcut_folder (fullpath); + } + catch (Glib::Error& e) { + std::cerr << "add_shortcut_folder() threw Glib::Error: " << e.what() << std::endl; + } + } + } + catch (Glib::FileError& e) { + std::cerr << "listing /Volumnes failed: " << e.what() << std::endl; + } +#endif +} + +float +Gtkmm2ext::paned_position_as_fraction (Gtk::Paned& paned, bool h) +{ + const guint pos = gtk_paned_get_position (const_cast(static_cast(&paned)->gobj())); + return (double) pos / (h ? paned.get_allocation().get_height() : paned.get_allocation().get_width()); +} + +void +Gtkmm2ext::paned_set_position_as_fraction (Gtk::Paned& paned, float fraction, bool h) +{ + gint v = (h ? paned.get_allocation().get_height() : paned.get_allocation().get_width()); + + if (v < 1) { + return; + } + + paned.set_position ((guint) floor (fraction * v)); +} + +string +Gtkmm2ext::show_gdk_event_state (int state) +{ + string s; + if (state & GDK_SHIFT_MASK) { + s += "+SHIFT"; + } + if (state & GDK_LOCK_MASK) { + s += "+LOCK"; + } + if (state & GDK_CONTROL_MASK) { + s += "+CONTROL"; + } + if (state & GDK_MOD1_MASK) { + s += "+MOD1"; + } + if (state & GDK_MOD2_MASK) { + s += "+MOD2"; + } + if (state & GDK_MOD3_MASK) { + s += "+MOD3"; + } + if (state & GDK_MOD4_MASK) { + s += "+MOD4"; + } + if (state & GDK_MOD5_MASK) { + s += "+MOD5"; + } + if (state & GDK_BUTTON1_MASK) { + s += "+BUTTON1"; + } + if (state & GDK_BUTTON2_MASK) { + s += "+BUTTON2"; + } + if (state & GDK_BUTTON3_MASK) { + s += "+BUTTON3"; + } + if (state & GDK_BUTTON4_MASK) { + s += "+BUTTON4"; + } + if (state & GDK_BUTTON5_MASK) { + s += "+BUTTON5"; + } + if (state & GDK_SUPER_MASK) { + s += "+SUPER"; + } + if (state & GDK_HYPER_MASK) { + s += "+HYPER"; + } + if (state & GDK_META_MASK) { + s += "+META"; + } + if (state & GDK_RELEASE_MASK) { + s += "+RELEASE"; + } + + return s; +}