1# This file is part of MyPaint. 2# Copyright (C) 2013-2018 by the MyPaint Development Team. 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 8"""Combined menubar and toolbars.""" 9 10 11## Imports 12 13from __future__ import division, print_function 14import logging 15 16from lib.gibindings import GObject 17from lib.gibindings import Gtk 18from lib.gibindings import Gdk 19from gettext import gettext as _ 20 21logger = logging.getLogger(__name__) 22 23 24## Class definitions 25 26class TopBar (Gtk.Grid): 27 """Combined menubar and toolbars which compacts when fullscreened. 28 29 This is a container widget for two horizontal toolbars and a menubar 30 with specialized behaviour when its parent window is fullscreened: 31 the menubar is repacked into the toolbar, and temporary CSS styles 32 are applied in order to attempt greater Fitts's Law compliance (and 33 a nicer look). 34 35 The toolbars and menubar are presented as properties for greater 36 flexibility in construction. All of these properties must be set up 37 at the time the widget is realized. 38 39 """ 40 41 ## Class constants 42 43 __gtype_name__ = 'MyPaintTopBar' 44 ICON_NAMES = { 45 "FileMenu": "mypaint-file-symbolic", 46 "EditMenu": "mypaint-edit-symbolic", 47 "ViewMenu": "mypaint-view-symbolic", 48 "BrushMenu": "mypaint-brush-symbolic", 49 "ColorMenu": "mypaint-colors-symbolic", 50 "LayerMenu": "mypaint-layers-symbolic", 51 "ScratchMenu": "mypaint-scratchpad-symbolic", 52 "HelpMenu": "mypaint-help-symbolic", 53 } 54 55 ## GObject properties, for Builder-style construction 56 57 #: The toolbar to present in position 1. 58 toolbar1 = GObject.Property( 59 type=Gtk.Toolbar, 60 flags=GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE, 61 nick='Toolbar-1 widget', 62 blurb="First GtkToolbar to show. This must be set at realize time." 63 ) 64 65 #: The toolbar to present in position 2. 66 toolbar1 = GObject.Property( 67 type=Gtk.Toolbar, 68 flags=GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE, 69 nick='Toolbar-2 widget', 70 blurb="Second GtkToolbar to show. This must be set at realize time." 71 ) 72 73 #: The menubar to present. 74 menubar = GObject.Property( 75 type=Gtk.MenuBar, 76 flags=GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE, 77 nick='Menu Bar widget', 78 blurb="The GtkMenuBar to show. This must be set at realize time." 79 ) 80 81 ## Construction & initialization 82 83 def __init__(self): 84 """Initialize""" 85 Gtk.Grid.__init__(self) 86 self.connect("realize", self._realize_cb) 87 # Widgets used in fullscreen mode 88 fs_menu = Gtk.Menu() 89 self._fs_menu = fs_menu 90 self._fs_menubutton = FakeMenuButton(_("<b>MyPaint</b>"), fs_menu) 91 self._fs_toolitem = Gtk.ToolItem() 92 93 def _realize_cb(self, widget): 94 """Assorted setup when the widget is realized""" 95 assert self.menubar is not None 96 assert self.toolbar1 is not None 97 assert self.toolbar2 is not None 98 # Packing details for Grid 99 self.menubar.set_hexpand(True) 100 self.toolbar1.set_hexpand(True) 101 self.toolbar2.set_hexpand(False) 102 self._fs_menubutton.set_hexpand(False) 103 # Specialized styles 104 prov = Gtk.CssProvider() 105 prov.load_from_data(b""" 106 .topbar { 107 padding: 0px; /* required by toolbars */ 108 margin: 0px; /* required by menubar */ 109 } 110 """) 111 bars = [self.toolbar1, self.toolbar2, self.menubar] 112 for b in bars: 113 style = b.get_style_context() 114 style.add_provider(prov, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 115 style.add_class("topbar") 116 # Initial packing; assume a non-fullscreened state 117 self.attach(self.menubar, 0, 0, 2, 1) 118 self.attach(self.toolbar1, 0, 1, 1, 1) 119 self.attach(self.toolbar2, 1, 1, 1, 1) 120 # Track state transitions of the window's toplevel 121 toplevel = self.get_toplevel() 122 assert toplevel is not None 123 toplevel.connect("window-state-event", self._toplevel_state_event_cb) 124 125 ## Event handling 126 127 def _toplevel_state_event_cb(self, toplevel, event): 128 """Repacks widgets when the toplevel changes fullsceen state""" 129 if not event.changed_mask & Gdk.WindowState.FULLSCREEN: 130 return 131 menubar = self.menubar 132 toolbar1 = self.toolbar1 133 toolbar2 = self.toolbar2 134 assert self is toolbar1.get_parent() 135 assert self is toolbar2.get_parent() 136 if event.new_window_state & Gdk.WindowState.FULLSCREEN: 137 # Remove menubar, use menu button on the 1st toolbar instead 138 assert menubar.get_parent() is self 139 assert self._fs_menubutton.get_parent() is None 140 menubar.hide() 141 self.remove(menubar) 142 for menuitem in list(menubar): 143 menubar.remove(menuitem) 144 self._fs_menu.append(menuitem) 145 item_name = menuitem.get_name() 146 icon_name = self.ICON_NAMES.get(item_name, None) 147 if hasattr(menuitem, "set_image"): 148 if icon_name: 149 icon_image = Gtk.Image() 150 icon_image.set_from_icon_name( 151 icon_name, 152 Gtk.IconSize.MENU, 153 ) 154 menuitem.set_image(icon_image) 155 else: 156 logger.warning( 157 "No icon for %r in the fullscreen state", 158 item_name, 159 ) 160 menuitem.show_all() 161 toolbar1.hide() 162 self.remove(toolbar1) 163 self.remove(toolbar2) 164 self.attach(toolbar1, 0, 0, 1, 1) 165 self.attach(toolbar2, 1, 0, 1, 1) 166 toolbar1.insert(self._fs_toolitem, 0) 167 self._fs_toolitem.add(self._fs_menubutton) 168 self._fs_toolitem.show_all() 169 else: 170 # Windowed mode: use a regular menu bar above the toolbar 171 assert menubar.get_parent() is None 172 assert self._fs_menubutton.get_parent() is self._fs_toolitem 173 toolbar1.remove(self._fs_toolitem) 174 self._fs_toolitem.remove(self._fs_menubutton) 175 for menuitem in list(self._fs_menu): 176 self._fs_menu.remove(menuitem) 177 menubar.append(menuitem) 178 if hasattr(menuitem, "set_image"): 179 menuitem.set_image(None) 180 toolbar1.hide() 181 toolbar2.hide() 182 self.remove(toolbar1) 183 self.remove(toolbar2) 184 self.attach(menubar, 0, 0, 2, 1) 185 self.attach(toolbar1, 0, 1, 1, 1) 186 self.attach(toolbar2, 1, 1, 1, 1) 187 menubar.show() 188 toolbar1.show_all() 189 toolbar2.show_all() 190 191 192class FakeMenuButton (Gtk.EventBox): 193 """Button-styled widget that launches a dropdown menu when clicked""" 194 195 def __init__(self, markup, menu): 196 """Initialize 197 198 :param markup: Markup to display in the button. 199 :param menu: The menu to present when clicked. 200 """ 201 Gtk.EventBox.__init__(self) 202 self.menu = menu 203 self.label = Gtk.Label() 204 self.label.set_markup(markup) 205 self.label.set_padding(8, 0) 206 # Intercept mouse clicks and use them for activating the togglebutton 207 # even if they're in its border, or (0, 0). Fitts would approve. 208 invis = Gtk.EventBox() 209 invis.set_visible_window(False) 210 invis.set_above_child(True) 211 invis.connect("button-press-event", self._button_press_cb) 212 invis.connect("enter-notify-event", self._enter_cb) 213 invis.connect("leave-notify-event", self._leave_cb) 214 self.invis_window = invis 215 # Toggle button, for the look of the thing only 216 self.togglebutton = Gtk.ToggleButton() 217 self.togglebutton.add(self.label) 218 self.togglebutton.set_relief(Gtk.ReliefStyle.NONE) 219 self.togglebutton.connect("toggled", self._togglebutton_toggled_cb) 220 # The underlying togglebutton can default and focus. Might as well make 221 # the Return key do something useful rather than invoking the 1st 222 # toolbar item. 223 self.togglebutton.set_can_default(True) 224 self.togglebutton.set_can_focus(True) 225 # Packing 226 invis.add(self.togglebutton) 227 self.add(invis) 228 # Menu signals 229 for sig in "selection-done", "deactivate", "cancel": 230 menu.connect(sig, self._menu_dismiss_cb) 231 232 def _enter_cb(self, widget, event): 233 """Prelight the button when hovered""" 234 self.togglebutton.set_state_flags(Gtk.StateFlags.PRELIGHT, False) 235 236 def _leave_cb(self, widget, event): 237 """Un-prelight the button when the pointer leaves""" 238 self.togglebutton.unset_state_flags(Gtk.StateFlags.PRELIGHT) 239 240 def _button_press_cb(self, widget, event): 241 """Post the menmu when clicked 242 243 Menu operation is much more convincing if we call popup() with event 244 details here rather than leaving it to the child button's "toggled" 245 event handler. 246 """ 247 pos_func = self._get_popup_menu_position 248 self.menu.popup(parent_menu_shell=None, parent_menu_item=None, 249 func=pos_func, data=None, button=event.button, 250 activate_time=event.time) 251 self.togglebutton.set_active(True) 252 253 def _togglebutton_toggled_cb(self, togglebutton): 254 """Post the menu from a keypress activating the toggle 255 256 The menu dismiss handler untoggles it.""" 257 if togglebutton.get_active(): 258 if not self.menu.get_property("visible"): 259 pos_func = self._get_popup_menu_position 260 self.menu.popup(None, None, pos_func, 1, 0) 261 262 def _menu_dismiss_cb(self, *a, **kw): 263 """Reset the button state when the user's finished 264 265 Also transfer focus back to the menu button.""" 266 self.unset_state_flags(Gtk.StateFlags.PRELIGHT) 267 self.togglebutton.set_active(False) 268 self.togglebutton.grab_focus() 269 270 def _get_popup_menu_position(self, menu, *junk): 271 """Position function for menu popup 272 273 This places the menu underneath the button, at the same x position. 274 """ 275 win = self.get_window() 276 x, y = win.get_origin()[1:] 277 y += self.get_allocated_height() 278 return x, y, True 279 280 281## Testing 282 283def _test(): 284 """Run an interactive test""" 285 toplevel = Gtk.Window() 286 toplevel.set_title("topbar test") 287 toplevel.connect("destroy", lambda *a: Gtk.main_quit()) 288 mainbox = Gtk.VBox() 289 topbar = TopBar() 290 canvas = Gtk.DrawingArea() 291 toplevel.set_size_request(500, 300) 292 293 # Fullscreen action 294 fs_act = Gtk.ToggleAction.new("Fullscreen", "Fullscreen", 295 "Enter fullscreen mode", 296 Gtk.STOCK_FULLSCREEN) 297 298 def _fullscreen_cb(action, toplevel): 299 if action.get_active(): 300 toplevel.fullscreen() 301 else: 302 toplevel.unfullscreen() 303 fs_act.connect("toggled", _fullscreen_cb, toplevel) 304 305 # One normally constructed menubar 306 menu1 = Gtk.Menu() 307 menuitem1 = Gtk.MenuItem.new_with_label("Demo") 308 menuitem2 = Gtk.CheckMenuItem() 309 menuitem3 = Gtk.MenuItem.new_with_label("Quit") 310 menuitem1.set_submenu(menu1) 311 menuitem2.set_related_action(fs_act) 312 menuitem3.connect("activate", lambda *a: Gtk.main_quit()) 313 menu1.append(menuitem2) 314 menu1.append(menuitem3) 315 menuitem1.show() 316 menuitem2.show() 317 menuitem3.show() 318 menubar = Gtk.MenuBar() 319 menubar.append(menuitem1) 320 321 # We need a pair of toolbars too. 322 toolbar1 = Gtk.Toolbar() 323 toolbar2 = Gtk.Toolbar() 324 toolitem1 = Gtk.ToggleToolButton() 325 toolitem1.set_related_action(fs_act) 326 toolbar2.insert(toolitem1, -1) 327 toolitem2 = Gtk.SeparatorToolItem() 328 toolitem2.set_draw(False) 329 toolitem2.set_expand(True) 330 toolbar1.insert(toolitem2, 0) 331 # Some junk items, to verify appearance in various GTK3 themes 332 toolitem3 = Gtk.ToolButton.new_from_stock(Gtk.STOCK_ZOOM_100) 333 toolitem3.set_is_important(True) 334 toolitem4 = Gtk.ToolButton.new_from_stock(Gtk.STOCK_ZOOM_100) 335 toolitem4.set_is_important(True) 336 toolitem4.set_sensitive(False) 337 toolitem5 = Gtk.ToolButton.new_from_stock(Gtk.STOCK_ZOOM_100) 338 toolitem6 = Gtk.ToolButton.new_from_stock(Gtk.STOCK_ZOOM_100) 339 toolitem6.set_sensitive(False) 340 toolbar1.insert(toolitem6, 0) 341 toolbar1.insert(toolitem5, 0) 342 toolbar1.insert(toolitem4, 0) 343 toolbar2.insert(toolitem3, 0) 344 345 # Assign topbar's properties 346 topbar.toolbar1 = toolbar1 347 topbar.toolbar2 = toolbar2 348 topbar.menubar = menubar 349 350 # Pack main UI, and start demo 351 mainbox.pack_start(topbar, False, False, 0) 352 mainbox.pack_start(canvas, True, True, 0) 353 toplevel.add(mainbox) 354 toplevel.show_all() 355 topbar.show_all() 356 Gtk.main() 357 358 359if __name__ == '__main__': 360 logging.basicConfig(level=logging.DEBUG) 361 _test() 362