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