Menubar hiding, menu button on the toolbar
[mypaint:mypaint.git] / gui / drawwindow.py
1 # -*- coding: utf-8 -*-
2 #
3 # This file is part of MyPaint.
4 # Copyright (C) 2007-2008 by Martin Renold <martinxyz@gmx.ch>
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10
11 """
12 This is the main drawing window, containing menu actions.
13 Painting is done in tileddrawwidget.py.
14 """
15
16 MYPAINT_VERSION="0.9.1+git"
17
18 import os, math, time
19 from gettext import gettext as _
20
21 import gtk, gobject
22 from gtk import gdk, keysyms
23 import pango
24
25 import colorselectionwindow, historypopup, stategroup, colorpicker, windowing, layout
26 import dialogs
27 from lib import helpers
28 import stock
29
30 import xml.etree.ElementTree as ET
31
32 # palette support
33 from lib.scratchpad_palette import GimpPalette, hatch_squiggle, squiggle, draw_palette
34
35 # TODO: put in a helper file?
36 def with_wait_cursor(func):
37     """python decorator that adds a wait cursor around a function"""
38     def wrapper(self, *args, **kwargs):
39         toplevels = [t for t in gtk.window_list_toplevels()
40                      if t.window is not None]
41         for toplevel in toplevels:
42             toplevel.window.set_cursor(gdk.Cursor(gdk.WATCH))
43             toplevel.set_sensitive(False)
44         self.app.doc.tdw.grab_add()
45         try:
46             func(self, *args, **kwargs)
47             # gtk main loop may be called in here...
48         finally:
49             for toplevel in toplevels:
50                 toplevel.set_sensitive(True)
51                 # ... which is why we need this check:
52                 if toplevel.window is not None:
53                     toplevel.window.set_cursor(None)
54             self.app.doc.tdw.grab_remove()
55     return wrapper
56
57 def button_press_cb_abstraction(drawwindow, win, event, doc):
58     #print event.device, event.button
59
60     ## Ignore accidentals
61     # Single button-presses only, not 2ble/3ple
62     if event.type != gdk.BUTTON_PRESS:
63         # ignore the extra double-click event
64         return False
65
66     if event.button != 1:
67         # check whether we are painting (accidental)
68         if event.state & gdk.BUTTON1_MASK:
69             # Do not allow dragging in the middle of
70             # painting. This often happens by accident with wacom
71             # tablet's stylus button.
72             #
73             # However we allow dragging if the user's pressure is
74             # still below the click threshold.  This is because
75             # some tablet PCs are not able to produce a
76             # middle-mouse click without reporting pressure.
77             # https://gna.org/bugs/index.php?15907
78             return False
79
80     # Pick a suitable config option
81     ctrl = event.state & gdk.CONTROL_MASK
82     alt  = event.state & gdk.MOD1_MASK
83     shift = event.state & gdk.SHIFT_MASK
84     if shift:
85         modifier_str = "_shift"
86     elif alt or ctrl:
87         modifier_str = "_ctrl"
88     else:
89         modifier_str = ""
90     prefs_name = "input.button%d%s_action" % (event.button, modifier_str)
91     action_name = drawwindow.app.preferences.get(prefs_name, "no_action")
92
93     # No-ops
94     if action_name == 'no_action':
95         return True  # We handled it by doing nothing
96
97     # Straight line
98     # Really belongs in the tdw, but this is the only object with access
99     # to the application preferences.
100     if action_name == 'straight_line':
101         doc.tdw.straight_line_from_last_pos(is_sequence=False)
102         return True
103     if action_name == 'straight_line_sequence':
104         doc.tdw.straight_line_from_last_pos(is_sequence=True)
105         return True
106
107     # View control
108     if action_name.endswith("_canvas"):
109         dragfunc = None
110         if action_name == "pan_canvas":
111             dragfunc = doc.dragfunc_translate
112         elif action_name == "zoom_canvas":
113             dragfunc = doc.dragfunc_zoom
114         elif action_name == "rotate_canvas":
115             dragfunc = doc.dragfunc_rotate
116         if dragfunc is not None:
117             doc.tdw.start_drag(dragfunc)
118             return True
119         return False
120
121     # Application menu
122     if action_name == 'popup_menu':
123         drawwindow.show_popupmenu(event=event)
124         return True
125
126     if action_name in drawwindow.popup_states:
127         state = drawwindow.popup_states[action_name]
128         state.activate(event)
129         return True
130
131     # Dispatch regular GTK events.
132     for ag in drawwindow.action_group, doc.action_group:
133         action = ag.get_action(action_name)
134         if action is not None:
135             action.activate()
136             return True
137
138 def button_release_cb_abstraction(win, event, doc):
139     #print event.device, event.button
140     tdw = doc.tdw
141     if tdw.dragfunc is not None:
142         tdw.stop_drag(doc.dragfunc_translate)
143         tdw.stop_drag(doc.dragfunc_rotate)
144         tdw.stop_drag(doc.dragfunc_zoom)
145     return False
146
147 class Window (windowing.MainWindow, layout.MainWindow):
148
149     def __init__(self, app):
150         windowing.MainWindow.__init__(self, app)
151         self.app = app
152
153         # Window handling
154         self._updating_toggled_item = False
155         self._show_subwindows = True
156         self.is_fullscreen = False
157
158         # Enable drag & drop
159         self.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
160                             gtk.DEST_DEFAULT_HIGHLIGHT |
161                             gtk.DEST_DEFAULT_DROP,
162                             [("text/uri-list", 0, 1),
163                              ("application/x-color", 0, 2)],
164                             gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY)
165
166         # Connect events
167         self.connect('delete-event', self.quit_cb)
168         self.connect('key-press-event', self.key_press_event_cb_before)
169         self.connect('key-release-event', self.key_release_event_cb_before)
170         self.connect_after('key-press-event', self.key_press_event_cb_after)
171         self.connect_after('key-release-event', self.key_release_event_cb_after)
172         self.connect("drag-data-received", self.drag_data_received)
173         self.connect("window-state-event", self.window_state_event_cb)
174
175         self.app.filehandler.current_file_observers.append(self.update_title)
176
177         self.init_actions()
178
179         lm = app.layout_manager
180         layout.MainWindow.__init__(self, lm)
181         self.main_widget.connect("button-press-event", self.button_press_cb)
182         self.main_widget.connect("button-release-event",self.button_release_cb)
183         self.main_widget.connect("scroll-event", self.scroll_cb)
184
185         kbm = self.app.kbm
186         kbm.add_extra_key('Menu', 'ShowPopupMenu')
187         kbm.add_extra_key('Tab', 'ToggleSubwindows')
188
189         self.init_stategroups()
190
191     #XXX: Compatability
192     def get_doc(self):
193         print "DeprecationWarning: Use app.doc instead"
194         return self.app.doc
195     def get_tdw(self):
196         print "DeprecationWarning: Use app.doc.tdw instead"
197         return self.app.doc.tdw
198     tdw, doc = property(get_tdw), property(get_doc)
199
200     def init_actions(self):
201         actions = [
202             # name, stock id, label, accelerator, tooltip, callback
203             ('FileMenu',    None, _('File')),
204             ('Quit',         gtk.STOCK_QUIT, _('Quit'), '<control>q', None, self.quit_cb),
205             ('FrameToggle',  None, _('Toggle Document Frame'), None, None, self.toggle_frame_cb),
206
207             ('EditMenu',        None, _('Edit')),
208
209             ('ColorMenu',    None, _('Color')),
210             ('ColorPickerPopup',    gtk.STOCK_COLOR_PICKER, _('Pick Color'), 'r', None, self.popup_cb),
211             ('ColorHistoryPopup',  None, _('Color History'), 'x', None, self.popup_cb),
212             ('ColorChangerPopup', None, _('Color Changer'), 'v', None, self.popup_cb),
213             ('ColorRingPopup',  None, _('Color Ring'), None, None, self.popup_cb),
214
215             ('ContextMenu',  None, _('Brushkeys')),
216             ('ContextHelp',  gtk.STOCK_HELP, _('Help!'), None, None, self.show_infodialog_cb),
217
218             ('LayerMenu',    None, _('Layers')),
219
220             # Scratchpad menu items
221             ('ScratchMenu',    None, _('Scratchpad')),
222             ('ScratchNew',  gtk.STOCK_NEW, _('New Scratchpad'), '', None, self.new_scratchpad_cb),
223             ('ScratchLoad',  gtk.STOCK_OPEN, _('Load Scratchpad...'), '', None, self.load_scratchpad_cb),
224             ('ScratchSaveNow',  gtk.STOCK_SAVE, _('Save Scratchpad Now'), '', None, self.save_current_scratchpad_cb),
225             ('ScratchSaveAs',  gtk.STOCK_SAVE_AS, _('Save Scratchpad As...'), '', None, self.save_as_scratchpad_cb),
226             ('ScratchRevert',  gtk.STOCK_UNDO, _('Revert Scratchpad'), '', None, self.revert_current_scratchpad_cb),
227             ('ScratchSaveAsDefault',  None, _('Save Scratchpad as Default'), None, None, self.save_scratchpad_as_default_cb),
228             ('ScratchClearDefault',  None, _('Clear the Default Scratchpad'), None, None, self.clear_default_scratchpad_cb),
229             ('ScratchPaletteOptions', None, _('Render a Palette')),
230             ('ScratchLoadPalette',  None, _('Load Palette File...'), None, None, self.draw_palette_cb),
231             ('ScratchDrawSatPalette',  None, _('Different Saturations of the current Color'), None, None, self.draw_sat_spectrum_cb),
232             ('ScratchCopyBackground',  None, _('Copy Background to Scratchpad'), None, None, self.scratchpad_copy_background_cb),
233
234             ('BrushMenu',    None, _('Brush')),
235             ('ImportBrushPack',       gtk.STOCK_OPEN, _('Import brush package...'), '', None, self.import_brush_pack_cb),
236
237             ('HelpMenu',   None, _('Help')),
238             ('Docu', gtk.STOCK_INFO, _('Where is the Documentation?'), None, None, self.show_infodialog_cb),
239             ('ShortcutHelp',  gtk.STOCK_INFO, _('Change the Keyboard Shortcuts?'), None, None, self.show_infodialog_cb),
240             ('About', gtk.STOCK_ABOUT, _('About MyPaint'), None, None, self.about_cb),
241
242             ('DebugMenu',    None, _('Debug')),
243             ('PrintMemoryLeak',  None, _('Print Memory Leak Info to Console (Slow!)'), None, None, self.print_memory_leak_cb),
244             ('RunGarbageCollector',  None, _('Run Garbage Collector Now'), None, None, self.run_garbage_collector_cb),
245             ('StartProfiling',  gtk.STOCK_EXECUTE, _('Start/Stop Python Profiling (cProfile)'), None, None, self.start_profiling_cb),
246             ('GtkInputDialog',  None, _('GTK input device dialog'), None, None, self.gtk_input_dialog_cb),
247
248
249             ('ViewMenu', None, _('View')),
250             ('ShowPopupMenu',    None, _('Popup Menu'), 'Menu', None, self.popupmenu_show_cb),
251             ('Fullscreen',   gtk.STOCK_FULLSCREEN, _('Fullscreen'), 'F11', None, self.fullscreen_cb),
252             ('ViewHelp',  gtk.STOCK_HELP, _('Help'), None, None, self.show_infodialog_cb),
253             ]
254         ag = self.action_group = gtk.ActionGroup('WindowActions')
255         ag.add_actions(actions)
256
257         # Toggle actions
258         toggle_actions = [
259             ('PreferencesWindow', gtk.STOCK_PREFERENCES,
260                     _('Preferences'), None, None, self.toggle_window_cb),
261             ('InputTestWindow',  None,
262                     _('Test input devices'), None, None, self.toggle_window_cb),
263             ('FrameWindow',  None,
264                     _('Document Frame...'), None, None, self.toggle_window_cb),
265             ('LayersWindow', stock.TOOL_LAYERS,
266                     None, None, _("Toggle the Layers list"),
267                     self.toggle_window_cb),
268             ('BackgroundWindow', gtk.STOCK_PAGE_SETUP,
269                     _('Background'), None, None, self.toggle_window_cb),
270             ('BrushSelectionWindow', stock.TOOL_BRUSH,
271                     None, None, _("Toggle the Brush selector"),
272                     self.toggle_window_cb),
273             ('BrushSettingsWindow', gtk.STOCK_PROPERTIES,
274                     _('Brush Editor'), '<control>b', None,
275                     self.toggle_window_cb),
276             ('ColorSelectionWindow', stock.TOOL_COLOR_SELECTOR,
277                     None, None, _("Toggle the Colour Triangle"),
278                     self.toggle_window_cb),
279             ('ColorSamplerWindow', stock.TOOL_COLOR_SAMPLER,
280                     None, None, _("Toggle the advanced Colour Sampler"),
281                     self.toggle_window_cb),
282             ('ScratchWindow',  stock.TOOL_SCRATCHPAD, 
283                     None, None, _('Toggle the scratchpad'),
284                     self.toggle_window_cb),
285             ]
286         ag.add_toggle_actions(toggle_actions)
287
288         # Reflect changes from other places (like tools' close buttons) into
289         # the proxys' visible states.
290         lm = self.app.layout_manager
291         lm.tool_visibility_observers.append(self.update_toggled_item_visibility)
292         lm.subwindow_visibility_observers.append(self.update_subwindow_visibility)
293
294         # Initial toggle state
295         for spec in toggle_actions:
296             name = spec[0]
297             action = ag.get_action(name)
298             role = name[0].lower() + name[1:]
299             visible = not lm.get_window_hidden_by_role(role)
300             # The sidebar machinery won't be up yet, so reveal windows that
301             # should be initially visible only in an idle handler
302             gobject.idle_add(action.set_active, visible)
303
304         # More toggle actions - ones which don't control windows.
305         toggle_actions = [
306             ('ToggleMenubar', None, _('Menu Bar'), None,
307                     _("Show menu bar"), self.toggle_menubar_cb,
308                     self.get_show_menubar()),
309             ('ToggleToolbar', None, _('Toolbar'), None,
310                     _("Show toolbar"), self.toggle_toolbar_cb,
311                     self.get_show_toolbar()),
312             ('ToggleSubwindows', None, _('Subwindows'), 'Tab',
313                     _("Show subwindows"), self.toggle_subwindows_cb,
314                     self.get_show_subwindows()),
315             ]
316         ag.add_toggle_actions(toggle_actions)
317
318         # Keyboard handling
319         for action in self.action_group.list_actions():
320             self.app.kbm.takeover_action(action)
321         self.app.ui_manager.insert_action_group(ag, -1)
322
323     def init_stategroups(self):
324         sg = stategroup.StateGroup()
325         p2s = sg.create_popup_state
326         changer = p2s(colorselectionwindow.ColorChangerPopup(self.app))
327         ring = p2s(colorselectionwindow.ColorRingPopup(self.app))
328         hist = p2s(historypopup.HistoryPopup(self.app, self.app.doc.model))
329         pick = self.colorpick_state = p2s(colorpicker.ColorPicker(self.app, self.app.doc.model))
330
331         self.popup_states = {
332             'ColorChangerPopup': changer,
333             'ColorRingPopup': ring,
334             'ColorHistoryPopup': hist,
335             'ColorPickerPopup': pick,
336             }
337         changer.next_state = ring
338         ring.next_state = changer
339         changer.autoleave_timeout = None
340         ring.autoleave_timeout = None
341
342         pick.max_key_hit_duration = 0.0
343         pick.autoleave_timeout = None
344
345         hist.autoleave_timeout = 0.600
346         self.history_popup_state = hist
347
348     def init_main_widget(self):  # override
349         self.main_widget = self.app.doc.tdw
350
351     def init_menubar(self):   # override
352         # Load Menubar, duplicate into self.popupmenu
353         menupath = os.path.join(self.app.datapath, 'gui/menu.xml')
354         menubar_xml = open(menupath).read()
355         self.app.ui_manager.add_ui_from_string(menubar_xml)
356         self.popupmenu = self._clone_menu(menubar_xml, 'PopupMenu', self.app.doc.tdw)
357         self.menubar = self.app.ui_manager.get_widget('/Menubar')
358         if not self.get_show_menubar():
359             gobject.idle_add(self.menubar.hide)
360
361     def init_toolbar(self):
362         toolbarpath = os.path.join(self.app.datapath, 'gui/toolbar.xml')
363         toolbarbar_xml = open(toolbarpath).read()
364         self.app.ui_manager.add_ui_from_string(toolbarbar_xml)
365         self.toolbar1 = self.app.ui_manager.get_widget('/toolbar1')
366         self.toolbar1.set_style(gtk.TOOLBAR_ICONS)
367         self.toolbar1.set_border_width(0)
368         self.toolbar1.connect("style-set", self.on_toolbar1_style_set)
369         self.toolbar = gtk.HBox()
370         self.menu_button = FakeMenuButton(_("MyPaint"), self.popupmenu)
371         self.menu_button.set_border_width(0)
372         self.toolbar.pack_start(self.menu_button, False, False)
373         self.toolbar.pack_start(self.toolbar1, True, True)
374         if not self.get_show_toolbar():
375             gobject.idle_add(self.toolbar.hide)
376         gobject.idle_add(self.update_menu_and_toolbar_visibility)
377         self.set_default(self.menu_button)
378
379     def on_toolbar1_style_set(self, widget, oldstyle):
380         style = widget.style.copy()
381         self.menu_button.set_style(style)
382         style = widget.style.copy()
383         self.toolbar.set_style(style)
384
385     def _clone_menu(self, xml, name, owner=None):
386         """
387         Hopefully temporary hack for converting UIManager XML describing the
388         main menubar into a rebindable popup menu. UIManager by itself doesn't
389         let you do this, by design, but we need a bigger menu than the little
390         things it allows you to build.
391         """
392         ui_elt = ET.fromstring(xml)
393         rootmenu_elt = ui_elt.find("menubar")
394         rootmenu_elt.attrib["name"] = name
395         xml = ET.tostring(ui_elt)
396         self.app.ui_manager.add_ui_from_string(xml)
397         tmp_menubar = self.app.ui_manager.get_widget('/' + name)
398         popupmenu = gtk.Menu()
399         for item in tmp_menubar.get_children():
400             tmp_menubar.remove(item)
401             popupmenu.append(item)
402         if owner is not None:
403             popupmenu.attach_to_widget(owner, None)
404         popupmenu.set_title("MyPaint")
405         popupmenu.connect("selection-done", self.popupmenu_done_cb)
406         popupmenu.connect("deactivate", self.popupmenu_done_cb)
407         popupmenu.connect("cancel", self.popupmenu_done_cb)
408         self.popupmenu_last_active = None
409         return popupmenu
410
411
412     def update_title(self, filename):
413         if filename:
414             self.set_title("MyPaint - %s" % os.path.basename(filename))
415         else:
416             self.set_title("MyPaint")
417
418     # INPUT EVENT HANDLING
419     def drag_data_received(self, widget, context, x, y, selection, info, t):
420         if info == 1:
421             if selection.data:
422                 uri = selection.data.split("\r\n")[0]
423                 fn = helpers.uri2filename(uri)
424                 if os.path.exists(fn):
425                     if self.app.filehandler.confirm_destructive_action():
426                         self.app.filehandler.open_file(fn)
427         elif info == 2: # color
428             color = [((ord(selection.data[v]) | (ord(selection.data[v+1]) << 8)) / 65535.0)  for v in range(0,8,2)]
429             self.app.brush.set_color_rgb(color[:3])
430             self.app.ch.push_color(self.app.brush.get_color_hsv())
431             # Don't popup the color history for now, as I haven't managed to get it to cooperate.
432
433     def print_memory_leak_cb(self, action):
434         helpers.record_memory_leak_status(print_diff = True)
435
436     def run_garbage_collector_cb(self, action):
437         helpers.run_garbage_collector()
438
439     def start_profiling_cb(self, action):
440         if getattr(self, 'profiler_active', False):
441             self.profiler_active = False
442             return
443
444         def doit():
445             import cProfile
446             profile = cProfile.Profile()
447
448             self.profiler_active = True
449             print '--- GUI Profiling starts ---'
450             while self.profiler_active:
451                 profile.runcall(gtk.main_iteration, False)
452                 if not gtk.events_pending():
453                     time.sleep(0.050) # ugly trick to remove "user does nothing" from profile
454             print '--- GUI Profiling ends ---'
455
456             profile.dump_stats('profile_fromgui.pstats')
457             #print 'profile written to mypaint_profile.pstats'
458             os.system('gprof2dot.py -f pstats profile_fromgui.pstats | dot -Tpng -o profile_fromgui.png && feh profile_fromgui.png &')
459
460         gobject.idle_add(doit)
461
462     def gtk_input_dialog_cb(self, action):
463         d = gtk.InputDialog()
464         d.show()
465
466     def key_press_event_cb_before(self, win, event):
467         key = event.keyval
468         ctrl = event.state & gdk.CONTROL_MASK
469         shift = event.state & gdk.SHIFT_MASK
470         alt = event.state & gdk.MOD1_MASK
471         #ANY_MODIFIER = gdk.SHIFT_MASK | gdk.MOD1_MASK | gdk.CONTROL_MASK
472         #if event.state & ANY_MODIFIER:
473         #    # allow user shortcuts with modifiers
474         #    return False
475
476         # This may need a stateful flag
477         if self.app.scratchpad_doc.tdw.has_pointer:
478             thisdoc = self.app.scratchpad_doc
479             # Stop dragging on the main window
480             self.app.doc.tdw.dragfunc = None
481         else:
482             thisdoc = self.app.doc
483             # Stop dragging on the other window
484             self.app.scratchpad_doc.tdw.dragfunc = None
485         if key == keysyms.space:
486             if shift:
487                  thisdoc.tdw.start_drag(thisdoc.dragfunc_rotate)
488             elif ctrl:
489                 thisdoc.tdw.start_drag(thisdoc.dragfunc_zoom)
490             elif alt:
491                 thisdoc.tdw.start_drag(thisdoc.dragfunc_frame)
492             else:
493                 thisdoc.tdw.start_drag(thisdoc.dragfunc_translate)
494         else: return False
495         return True
496
497     def key_release_event_cb_before(self, win, event):
498         if self.app.scratchpad_doc.tdw.has_pointer:
499             thisdoc = self.app.scratchpad_doc
500         else:
501             thisdoc = self.app.doc
502         if event.keyval == keysyms.space:
503             thisdoc.tdw.stop_drag(thisdoc.dragfunc_translate)
504             thisdoc.tdw.stop_drag(thisdoc.dragfunc_rotate)
505             thisdoc.tdw.stop_drag(thisdoc.dragfunc_zoom)
506             thisdoc.tdw.stop_drag(thisdoc.dragfunc_frame)
507             return True
508         return False
509
510     def key_press_event_cb_after(self, win, event):
511         key = event.keyval
512         if self.is_fullscreen and key == keysyms.Escape:
513             self.fullscreen_cb()
514         else:
515             return False
516         return True
517
518     def key_release_event_cb_after(self, win, event):
519         return False
520
521     def button_press_cb(self, win, event):
522         return button_press_cb_abstraction(self, win, event, self.app.doc)
523
524     def button_release_cb(self, win, event):
525         return button_release_cb_abstraction(win, event, self.app.doc)
526
527     def scroll_cb(self, win, event):
528         d = event.direction
529         if d == gdk.SCROLL_UP:
530             if event.state & gdk.SHIFT_MASK:
531                 self.app.doc.rotate('RotateLeft')
532             else:
533                 self.app.doc.zoom('ZoomIn')
534         elif d == gdk.SCROLL_DOWN:
535             if event.state & gdk.SHIFT_MASK:
536                 self.app.doc.rotate('RotateRight')
537             else:
538                 self.app.doc.zoom('ZoomOut')
539         elif d == gdk.SCROLL_RIGHT:
540             self.app.doc.rotate('RotateRight')
541         elif d == gdk.SCROLL_LEFT:
542             self.app.doc.rotate('RotateLeft')
543
544     # WINDOW HANDLING
545     def toggle_window_cb(self, action):
546         if self._updating_toggled_item:
547             return
548         s = action.get_name()
549         active = action.get_active()
550         window_name = s[0].lower() + s[1:] # WindowName -> windowName
551         # If it's a tool, get it to hide/show itself
552         t = self.app.layout_manager.get_tool_by_role(window_name)
553         if t is not None:
554             t.set_hidden(not active)
555             return
556         # Otherwise, if it's a regular subwindow hide/show+present it.
557         w = self.app.layout_manager.get_subwindow_by_role(window_name)
558         if w is None:
559             return
560         onscreen = w.window is not None and w.window.is_visible()
561         if active:
562             if onscreen:
563                 return
564             w.show_all()
565             w.present()
566         else:
567             if not onscreen:
568                 return
569             w.hide()
570
571     def update_subwindow_visibility(self, window, active):
572         # Responds to non-tool subwindows being hidden and shown
573         role = window.get_role()
574         self.update_toggled_item_visibility(role, active)
575
576     def update_toggled_item_visibility(self, role, active, *a, **kw):
577         # Responds to any item with a role being hidden or shown by
578         # silently updating its ToggleAction to match.
579         action_name = role[0].upper() + role[1:]
580         action = self.action_group.get_action(action_name)
581         if action is None:
582             warn("Unable to find action %s" % action_name, RuntimeWarning, 1)
583             return
584         if action.get_active() != active:
585             self._updating_toggled_item = True
586             action.set_active(active)
587             self._updating_toggled_item = False
588
589     def popup_cb(self, action):
590         state = self.popup_states[action.get_name()]
591         state.activate(action)
592
593
594     # Show Toolbar
595     # Saved in the user prefs between sessions.
596     # Controlled via its ToggleAction only.
597
598     def set_show_toolbar(self, show_toolbar):
599         """Programatically set the Show Toolbar option.
600         """
601         action = self.action_group.get_action("ToggleToolbar")
602         if show_toolbar:
603             if not action.get_active():
604                 action.set_active(True)
605             self.app.preferences["ui.toolbar"] = True
606         else:
607             if action.get_active():
608                 action.set_active(False)
609             self.app.preferences["ui.toolbar"] = False
610         self.update_menu_and_toolbar_visibility()
611
612     def get_show_toolbar(self):
613         return self.app.preferences.get("ui.toolbar", True)
614
615     def toggle_toolbar_cb(self, action):
616         active = action.get_active()
617         if active:
618             self.toolbar.show_all()
619         else:
620             self.toolbar.hide()
621         self.app.preferences["ui.toolbar"] = active
622         self.update_menu_and_toolbar_visibility()
623
624
625     # Show Menubar
626     # Works like show toolbar.
627
628     def set_show_menubar(self, show_menubar):
629         """Programatically set the Show Menubar option.
630         """
631         action = self.action_group.get_action("ToggleMenubar")
632         if show_menubar:
633             if not action.get_active():
634                 action.set_active(True)
635             self.app.preferences["ui.menubar"] = True
636         else:
637             if action.get_active():
638                 action.set_active(False)
639             self.app.preferences["ui.menubar"] = False
640         self.update_menu_and_toolbar_visibility()
641
642     def get_show_menubar(self):
643         return self.app.preferences.get("ui.menubar", True)
644
645     def toggle_menubar_cb(self, action):
646         active = action.get_active()
647         if active:
648             self.menubar.show_all()
649         else:
650             self.menubar.hide()
651         self.app.preferences["ui.menubar"] = active
652         self.update_menu_and_toolbar_visibility()
653
654     # Menu and toolbar vislibility.
655     # In non-fullscreen mode, ensure that at least one of the menubar or the
656     # toolbar is visible. Make the UI reflect the one/the other/both/not
657     # neither interlocking of these settings.
658
659     def update_menu_and_toolbar_visibility(self):
660         if not (self.get_show_menubar() or self.get_show_toolbar()):
661             print "warning: forcing toolbar"
662             self.set_show_toolbar(True)
663         if self.get_show_menubar():
664             self.menu_button.hide()
665         else:
666             self.menu_button.show_all()
667         ag = self.action_group
668         tb_action = ag.get_action("ToggleToolbar")
669         mb_action = ag.get_action("ToggleMenubar")
670         if self.get_show_menubar() and not self.get_show_toolbar():
671             mb_action.set_sensitive(False)
672             tb_action.set_sensitive(True)
673         elif self.get_show_toolbar() and not self.get_show_menubar():
674             mb_action.set_sensitive(True)
675             tb_action.set_sensitive(False)
676         else:
677             mb_action.set_sensitive(True)
678             tb_action.set_sensitive(True)
679
680     # Show Subwindows
681     # Not saved between sessions, defaults to on.
682     # Controlled via its ToggleAction, and entering or leaving fullscreen mode
683     # according to the setting of ui.hide_in_fullscreen in prefs.
684
685     def set_show_subwindows(self, show_subwindows):
686         """Programatically set the Show Subwindows option.
687         """
688         action = self.action_group.get_action("ToggleSubwindows")
689         currently_showing = action.get_active()
690         if show_subwindows != currently_showing:
691             action.set_active(show_subwindows)
692         self._show_subwindows = self._show_subwindows
693
694     def get_show_subwindows(self):
695         return self._show_subwindows
696
697     def toggle_subwindows_cb(self, action):
698         active = action.get_active()
699         lm = self.app.layout_manager
700         if active:
701             lm.toggle_user_tools(on=True)
702         else:
703             lm.toggle_user_tools(on=False)
704         self._show_subwindows = active
705
706
707     # Fullscreen mode
708     # This implementation requires an ICCCM and EWMH-compliant window manager
709     # which supports the _NET_WM_STATE_FULLSCREEN hint. There are several
710     # available.
711
712     def fullscreen_cb(self, *junk):
713         if not self.is_fullscreen:
714             self.fullscreen()
715         else:
716             self.unfullscreen()
717
718     def window_state_event_cb(self, widget, event):
719         # Respond to changes of the fullscreen state only
720         if not event.changed_mask & gdk.WINDOW_STATE_FULLSCREEN:
721             return
722         lm = self.app.layout_manager
723         self.is_fullscreen = event.new_window_state & gdk.WINDOW_STATE_FULLSCREEN
724         if self.is_fullscreen:
725             # Subwindow hiding 
726             if self.app.preferences.get("ui.hide_subwindows_in_fullscreen", True):
727                 self.set_show_subwindows(False)
728                 self._restore_subwindows_on_unfullscreen = True
729             if self.app.preferences.get("ui.hide_menubar_in_fullscreen", True):
730                 self.menubar.hide()
731                 self._restore_menubar_on_unfullscreen = True
732             if self.app.preferences.get("ui.hide_toolbar_in_fullscreen", True):
733                 self.toolbar.hide()
734                 self._restore_toolbar_on_unfullscreen = True
735             # fix for fullscreen problem on Windows, https://gna.org/bugs/?15175
736             # on X11/Metacity it also helps a bit against flickering during the switch
737             while gtk.events_pending():
738                 gtk.main_iteration()
739         else:
740             while gtk.events_pending():
741                 gtk.main_iteration()
742             if getattr(self, "_restore_menubar_on_unfullscreen", False):
743                 if self.get_show_menubar():
744                     self.menubar.show()
745                 del self._restore_menubar_on_unfullscreen
746             if getattr(self, "_restore_toolbar_on_unfullscreen", False):
747                 if self.get_show_toolbar():
748                     self.toolbar.show()
749                 del self._restore_toolbar_on_unfullscreen
750             if getattr(self, "_restore_subwindows_on_unfullscreen", False):
751                 self.set_show_subwindows(True)
752                 del self._restore_subwindows_on_unfullscreen
753         self.update_menu_and_toolbar_visibility()
754
755     def popupmenu_show_cb(self, action):
756         self.show_popupmenu()
757
758     def show_popupmenu(self, event=None):
759         self.menubar.set_sensitive(False)   # excessive feedback?
760         self.menu_button.set_sensitive(False)
761         button = 1
762         time = 0
763         if event is not None:
764             if event.type == gdk.BUTTON_PRESS:
765                 button = event.button
766                 time = event.time
767         self.popupmenu.popup(None, None, None, button, time)
768         if event is None:
769             # We're responding to an Action, most probably the menu key.
770             # Open out the last highlighted menu to speed key navigation up.
771             if self.popupmenu_last_active is None:
772                 self.popupmenu.select_first(True) # one less keypress
773             else:
774                 self.popupmenu.select_item(self.popupmenu_last_active)
775
776     def popupmenu_done_cb(self, *a, **kw):
777         # Not sure if we need to bother with this level of feedback,
778         # but it actually looks quite nice to see one menu taking over
779         # the other. Makes it clear that the popups are the same thing as
780         # the full menu, maybe.
781         self.menubar.set_sensitive(True)
782         self.menu_button.set_sensitive(True)
783         self.popupmenu_last_active = self.popupmenu.get_active()
784
785     # BEGIN -- Scratchpad menu options
786     def save_scratchpad_as_default_cb(self, action):
787         self.app.filehandler.save_scratchpad(self.app.filehandler.get_scratchpad_default(), export = True)
788
789     def clear_default_scratchpad_cb(self, action):
790         self.app.filehandler.delete_default_scratchpad()
791
792     # Unneeded since 'Save blank canvas' bug has been addressed.
793     #def clear_autosave_scratchpad_cb(self, action):
794     #    self.app.filehandler.delete_autosave_scratchpad()
795
796     def new_scratchpad_cb(self, action):
797         if os.path.isfile(self.app.filehandler.get_scratchpad_default()):
798             self.app.filehandler.open_scratchpad(self.app.filehandler.get_scratchpad_default())
799         else:
800             self.app.scratchpad_doc.model.clear()
801             # With no default - adopt the currently chosen background
802             bg = self.app.doc.model.background
803             if self.app.scratchpad_doc:
804                 self.app.scratchpad_doc.model.set_background(bg)
805
806         self.app.scratchpad_filename = self.app.preferences['scratchpad.last_opened'] = self.app.filehandler.get_scratchpad_autosave()
807
808     def load_scratchpad_cb(self, action):
809         if self.app.scratchpad_filename:
810             self.save_current_scratchpad_cb(action)
811             current_pad = self.app.scratchpad_filename
812         else:
813             current_pad = self.app.filehandler.get_scratchpad_autosave()
814         self.app.filehandler.open_scratchpad_dialog()
815         # Check to see if a file has been opened outside of the scratchpad directory
816         if not os.path.abspath(self.app.scratchpad_filename).startswith(os.path.abspath(self.app.filehandler.get_scratchpad_prefix())):
817             # file is NOT within the scratchpad directory - load copy as current scratchpad
818             self.app.scratchpad_filename = self.app.preferences['scratchpad.last_opened'] = current_pad
819
820     def save_as_scratchpad_cb(self, action):
821         self.app.filehandler.save_scratchpad_as_dialog()
822
823     def revert_current_scratchpad_cb(self, action):
824         if os.path.isfile(self.app.scratchpad_filename):
825             self.app.filehandler.open_scratchpad(self.app.scratchpad_filename)
826             print "Reverted to %s" % self.app.scratchpad_filename
827         else:
828             print "No file to revert to yet."
829
830     def save_current_scratchpad_cb(self, action):
831         self.app.filehandler.save_scratchpad(self.app.scratchpad_filename)
832
833     def scratchpad_copy_background_cb(self, action):
834         bg = self.app.doc.model.background
835         if self.app.scratchpad_doc:
836             self.app.scratchpad_doc.model.set_background(bg)
837
838     def draw_palette_cb(self, action):
839         # test functionality:
840         file_filters = [
841         (_("Gimp Palette Format"), ("*.gpl",)),
842         (_("All Files"), ("*.*",)),
843         ]
844         gimp_path = os.path.join(self.app.filehandler.get_gimp_prefix(), "palettes")
845         dialog = self.app.filehandler.get_open_dialog(start_in_folder=gimp_path,
846                                                   file_filters = file_filters)
847         try:
848             if dialog.run() == gtk.RESPONSE_OK:
849                 dialog.hide()
850                 filename = dialog.get_filename().decode('utf-8')
851                 if filename:
852                     #filename = "/home/ben/.gimp-2.6/palettes/Nature_Grass.gpl" # TEMP HACK TO TEST
853                     g = GimpPalette(filename)
854                     grid_size = 30.0
855                     column_limit = 7
856                     # IGNORE Gimp Palette 'columns'?
857                     if g.columns != 0:
858                         column_limit = g.columns   # use the value for columns in the palette
859                     draw_palette(self.app, g, self.app.scratchpad_doc, columns=column_limit, grid_size=grid_size, swatch_method=hatch_squiggle, scale = 25.0)
860         finally:
861             dialog.destroy()
862
863     def draw_sat_spectrum_cb(self, action):
864         g = GimpPalette()
865         hsv = self.app.brush.get_color_hsv()
866         g.append_sat_spectrum(hsv)
867         grid_size = 30.0
868         off_x = off_y = grid_size / 2.0
869         column_limit = 8
870         draw_palette(self.app, g, self.app.scratchpad_doc, columns=column_limit, grid_size=grid_size)
871
872     # END -- Scratchpad menu options
873
874     def quit_cb(self, *junk):
875         self.app.doc.model.split_stroke()
876         self.app.save_gui_config() # FIXME: should do this periodically, not only on quit
877
878         if not self.app.filehandler.confirm_destructive_action(title=_('Quit'), question=_('Really Quit?')):
879             return True
880
881         gtk.main_quit()
882         return False
883
884     def toggle_frame_cb(self, action):
885         enabled = self.app.doc.model.frame_enabled
886         self.app.doc.model.set_frame_enabled(not enabled)
887
888     def import_brush_pack_cb(self, *junk):
889         format_id, filename = dialogs.open_dialog(_("Import brush package..."), self,
890                                  [(_("MyPaint brush package (*.zip)"), "*.zip")])
891         if filename:
892             self.app.brushmanager.import_brushpack(filename,  self)
893
894     # INFORMATION
895     # TODO: Move into dialogs.py?
896     def about_cb(self, action):
897         d = gtk.AboutDialog()
898         d.set_transient_for(self)
899         d.set_program_name("MyPaint")
900         d.set_version(MYPAINT_VERSION)
901         d.set_copyright(_("Copyright (C) 2005-2010\nMartin Renold and the MyPaint Development Team"))
902         d.set_website("http://mypaint.info/")
903         d.set_logo(self.app.pixmaps.mypaint_logo)
904         d.set_license(
905             _(u"This program is free software; you can redistribute it and/or modify "
906               u"it under the terms of the GNU General Public License as published by "
907               u"the Free Software Foundation; either version 2 of the License, or "
908               u"(at your option) any later version.\n"
909               u"\n"
910               u"This program is distributed in the hope that it will be useful, "
911               u"but WITHOUT ANY WARRANTY. See the COPYING file for more details.")
912             )
913         d.set_wrap_license(True)
914         d.set_authors([
915             # (in order of appearance)
916             u"Martin Renold (%s)" % _('programming'),
917             u"Yves Combe (%s)" % _('portability'),
918             u"Popolon (%s)" % _('programming'),
919             u"Clement Skau (%s)" % _('programming'),
920             u"Jon Nordby (%s)" % _('programming'),
921             u"Álinson Santos (%s)" % _('programming'),
922             u"Tumagonx (%s)" % _('portability'),
923             u"Ilya Portnov (%s)" % _('programming'),
924             u"Jonas Wagner (%s)" % _('programming'),
925             u"Luka Čehovin (%s)" % _('programming'),
926             u"Andrew Chadwick (%s)" % _('programming'),
927             u"Till Hartmann (%s)" % _('programming'),
928             u'David Grundberg (%s)' % _('programming'),
929             u"Krzysztof Pasek (%s)" % _('programming'),
930             u"Ben O'Steen (%s)" % _('programming'),
931             ])
932         d.set_artists([
933             u"Artis Rozentāls (%s)" % _('brushes'),
934             u"Popolon (%s)" % _('brushes'),
935             u"Marcelo 'Tanda' Cerviño (%s)" % _('patterns, brushes'),
936             u"David Revoy (%s)" % _('brushes'),
937             u"Ramón Miranda (%s)" % _('brushes, patterns'),
938             u"Enrico Guarnieri 'Ico_dY' (%s)" % _('brushes'),
939             u'Sebastian Kraft (%s)' % _('desktop icon'),
940             u"Nicola Lunghi (%s)" % _('patterns'),
941             u"Toni Kasurinen (%s)" % _('brushes'),
942             u"Сан Саныч 'MrMamurk' (%s)" % _('patterns'),
943             u"Andrew Chadwick (%s)" % _('tool icons'),
944             u"Ben O'Steen (%s)" % _('tool icons'),
945             ])
946         # list all translators, not only those of the current language
947         d.set_translator_credits(
948             u'Ilya Portnov (ru)\n'
949             u'Popolon (fr, zh_CN, ja)\n'
950             u'Jon Nordby (nb)\n'
951             u'Griatch (sv)\n'
952             u'Tobias Jakobs (de)\n'
953             u'Martin Tabačan (cs)\n'
954             u'Tumagonx (id)\n'
955             u'Manuel Quiñones (es)\n'
956             u'Gergely Aradszki (hu)\n'
957             u'Lamberto Tedaldi (it)\n'
958             u'Dong-Jun Wu (zh_TW)\n'
959             u'Luka Čehovin (sl)\n'
960             u'Geuntak Jeong (ko)\n'
961             u'Łukasz Lubojański (pl)\n'
962             u'Daniel Korostil (uk)\n'
963             u'Julian Aloofi (de)\n'
964             u'Tor Egil Hoftun Kvæstad (nn_NO)\n'
965             u'João S. O. Bueno (pt_BR)\n'
966             u'David Grundberg (sv)\n'
967             u'Elliott Sales de Andrade (en_CA)\n'
968             )
969
970         d.run()
971         d.destroy()
972
973     def show_infodialog_cb(self, action):
974         text = {
975         'ShortcutHelp':
976                 _("Move your mouse over a menu entry, then press the key to assign."),
977         'ViewHelp':
978                 _("You can also drag the canvas with the mouse while holding the middle "
979                 "mouse button or spacebar. Or with the arrow keys."
980                 "\n\n"
981                 "In contrast to earlier versions, scrolling and zooming are harmless now and "
982                 "will not make you run out of memory. But you still require a lot of memory "
983                 "if you paint all over while fully zoomed out."),
984         'ContextHelp':
985                 _("Brushkeys are used to quickly save/restore brush settings "
986                  "using keyboard shortcuts. You can paint with one hand and "
987                  "change brushes with the other without interruption."
988                  "\n\n"
989                  "There are 10 memory slots to hold brush settings.\n"
990                  "They are anonymous brushes, which are not visible in the "
991                  "brush selector list. But they are remembered even if you "
992                  "quit."),
993         'Docu':
994                 _("There is a tutorial available on the MyPaint homepage. It "
995                  "explains some features which are hard to discover yourself."
996                  "\n\n"
997                  "Comments about the brush settings (opaque, hardness, etc.) and "
998                  "inputs (pressure, speed, etc.) are available as tooltips. "
999                  "Put your mouse over a label to see them. "
1000                  "\n"),
1001         }
1002         self.app.message_dialog(text[action.get_name()])
1003
1004
1005 class FakeMenuButton(gtk.EventBox):
1006     """Launches the popup menu when clicked.
1007
1008     One of these sits to the left of the real toolbar when the main menu bar is
1009     hidden. In addition to providing access to a popup menu associated with the
1010     main view, this is a little more compliant with Fitts's Law than a normal
1011     `gtk.MenuBar`: when the window is fullscreened with only the "toolbar"
1012     present the ``(0, 0)`` screen pixel hits this button. Support note: Compiz
1013     edge bindings sometimes get in the way of this, so turn those off if you
1014     want Fitts's compliance.
1015     """
1016
1017     def __init__(self, text, menu):
1018         gtk.EventBox.__init__(self)
1019         self.menu = menu
1020         self.label = gtk.Label(text)
1021         self.label.set_padding(8, 0)
1022
1023         # Text settings
1024         #self.label.set_angle(5)
1025         attrs = pango.AttrList()
1026         attrs.change(pango.AttrWeight(pango.WEIGHT_HEAVY, 0, -1))
1027         self.label.set_attributes(attrs)
1028
1029         # Intercept mouse clicks and use them for activating the togglebutton
1030         # even if they're in its border, or (0, 0). Fitts would approve.
1031         invis = self.invis_window = gtk.EventBox()
1032         invis.set_visible_window(False)
1033         invis.set_above_child(True)
1034         invis.connect("button-press-event", self.on_button_press)
1035         invis.connect("enter-notify-event", self.on_enter)
1036         invis.connect("leave-notify-event", self.on_leave)
1037
1038         # The underlying togglebutton can default and focus. Might as well make
1039         # the Return key do something useful rather than invoking the 1st
1040         # toolbar item.
1041         self.togglebutton = gtk.ToggleButton()
1042         self.togglebutton.add(self.label)
1043         self.togglebutton.set_relief(gtk.RELIEF_HALF)
1044         self.togglebutton.set_flags(gtk.CAN_FOCUS)
1045         self.togglebutton.set_flags(gtk.CAN_DEFAULT)
1046         self.togglebutton.connect("toggled", self.on_togglebutton_toggled)
1047
1048         invis.add(self.togglebutton)
1049         self.add(invis)
1050         for sig in "selection-done", "deactivate", "cancel":
1051             menu.connect(sig, self.on_menu_dismiss)
1052
1053
1054     def on_enter(self, widget, event):
1055         # Not this set_state(). That one.
1056         #self.togglebutton.set_state(gtk.STATE_PRELIGHT)
1057         gtk.Widget.set_state(self.togglebutton, gtk.STATE_PRELIGHT)
1058
1059
1060     def on_leave(self, widget, event):
1061         #self.togglebutton.set_state(gtk.STATE_NORMAL)
1062         gtk.Widget.set_state(self.togglebutton, gtk.STATE_NORMAL)
1063
1064
1065     def on_button_press(self, widget, event):
1066         # Post the menu. Menu operation is much more convincing if we call
1067         # popup() with event details here rather than leaving it to the toggled
1068         # handler.
1069         pos_func = self._get_popup_menu_position
1070         self.menu.popup(None, None, pos_func, event.button, event.time)
1071         self.togglebutton.set_active(True)
1072
1073
1074     def on_togglebutton_toggled(self, togglebutton):
1075         # Post the menu from a keypress. Dismiss handler untoggles it.
1076         if togglebutton.get_active():
1077             if not self.menu.get_property("visible"):
1078                 pos_func = self._get_popup_menu_position
1079                 self.menu.popup(None, None, pos_func, 1, 0)
1080
1081
1082     def on_menu_dismiss(self, *a, **kw):
1083         # Reset the button state when the user's finished, and
1084         # park focus back on the menu button.
1085         self.set_state(gtk.STATE_NORMAL)
1086         self.togglebutton.set_active(False)
1087         self.togglebutton.grab_focus()
1088
1089
1090     def _get_popup_menu_position(self, menu, *junk):
1091         # Underneath the button, at the same x position.
1092         x, y = self.window.get_origin()
1093         y += self.allocation.height
1094         return x, y, True
1095
1096
1097     def set_style(self, style):
1098         # Propagate style changes to all children as well. Since this button is
1099         # stored on the toolbar, the main window makes it share a style with
1100         # it. Looks prettier.
1101         gtk.EventBox.set_style(self, style)
1102         style = style.copy()
1103         widget = self.togglebutton
1104         widget.set_style(style)
1105         style = style.copy()
1106         widget = widget.get_child()
1107         widget.set_style(style)