OSDN Git Service

fullscreen: update action toggled
[mypaint-anime/master.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, toolbar
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     action = drawwindow.app.find_action(action_name)
133     if action is not None:
134         action.activate()
135         return True
136
137 def button_release_cb_abstraction(win, event, doc):
138     #print event.device, event.button
139     tdw = doc.tdw
140     if tdw.dragfunc is not None:
141         tdw.stop_drag(doc.dragfunc_translate)
142         tdw.stop_drag(doc.dragfunc_rotate)
143         tdw.stop_drag(doc.dragfunc_zoom)
144     return False
145
146 class Window (windowing.MainWindow, layout.MainWindow):
147
148     MENUISHBAR_RADIO_MENUBAR = 1
149     MENUISHBAR_RADIO_MAIN_TOOLBAR = 2
150     MENUISHBAR_RADIO_BOTH_BARS = 3
151
152     def __init__(self, app):
153         windowing.MainWindow.__init__(self, app)
154         self.app = app
155
156         # Window handling
157         self._updating_toggled_item = False
158         self._show_subwindows = True
159         self.is_fullscreen = False
160
161         # Enable drag & drop
162         self.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
163                             gtk.DEST_DEFAULT_HIGHLIGHT |
164                             gtk.DEST_DEFAULT_DROP,
165                             [("text/uri-list", 0, 1),
166                              ("application/x-color", 0, 2)],
167                             gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY)
168
169         # Connect events
170         self.connect('delete-event', self.quit_cb)
171         self.connect('key-press-event', self.key_press_event_cb_before)
172         self.connect('key-release-event', self.key_release_event_cb_before)
173         self.connect_after('key-press-event', self.key_press_event_cb_after)
174         self.connect_after('key-release-event', self.key_release_event_cb_after)
175         self.connect("drag-data-received", self.drag_data_received)
176         self.connect("window-state-event", self.window_state_event_cb)
177
178         self.app.filehandler.current_file_observers.append(self.update_title)
179
180         self.init_actions()
181
182         lm = app.layout_manager
183         layout.MainWindow.__init__(self, lm)
184         self.main_widget.connect("button-press-event", self.button_press_cb)
185         self.main_widget.connect("button-release-event",self.button_release_cb)
186         self.main_widget.connect("scroll-event", self.scroll_cb)
187
188         kbm = self.app.kbm
189         kbm.add_extra_key('Menu', 'ShowPopupMenu')
190         kbm.add_extra_key('Tab', 'ToggleSubwindows')
191
192         self.init_stategroups()
193
194     #XXX: Compatability
195     def get_doc(self):
196         print "DeprecationWarning: Use app.doc instead"
197         return self.app.doc
198     def get_tdw(self):
199         print "DeprecationWarning: Use app.doc.tdw instead"
200         return self.app.doc.tdw
201     tdw, doc = property(get_tdw), property(get_doc)
202
203     def init_actions(self):
204         actions = [
205             # name, stock id, label, accelerator, tooltip, callback
206             ('FileMenu',    None, _('File')),
207             ('Quit',         gtk.STOCK_QUIT, _('Quit'), '<control>q', None, self.quit_cb),
208             ('FrameToggle',  None, _('Toggle Document Frame'), None, None, self.toggle_frame_cb),
209
210             ('EditMenu',        None, _('Edit')),
211
212             ('ColorMenu',    None, _('Color')),
213             ('ColorPickerPopup',    gtk.STOCK_COLOR_PICKER, _('Pick Color'), 'r', None, self.popup_cb),
214             ('ColorHistoryPopup',  None, _('Color History'), 'x', None, self.popup_cb),
215             ('ColorChangerPopup', None, _('Color Changer'), 'v', None, self.popup_cb),
216             ('ColorRingPopup',  None, _('Color Ring'), None, None, self.popup_cb),
217             ('ColorDetailsDialog', None, _("Color Details"), None, None, self.color_details_dialog_cb),
218
219             ('ContextMenu',  None, _('Brushkeys')),
220             ('ContextHelp',  gtk.STOCK_HELP, _('Help!'), None, None, self.show_infodialog_cb),
221
222             ('LayerMenu',    None, _('Layers')),
223
224             # Scratchpad menu items
225             ('ScratchMenu',    None, _('Scratchpad')),
226             ('ScratchNew',  gtk.STOCK_NEW, _('New Scratchpad'), '', None, self.new_scratchpad_cb),
227             ('ScratchLoad',  gtk.STOCK_OPEN, _('Load Scratchpad...'), '', None, self.load_scratchpad_cb),
228             ('ScratchSaveNow',  gtk.STOCK_SAVE, _('Save Scratchpad Now'), '', None, self.save_current_scratchpad_cb),
229             ('ScratchSaveAs',  gtk.STOCK_SAVE_AS, _('Save Scratchpad As...'), '', None, self.save_as_scratchpad_cb),
230             ('ScratchRevert',  gtk.STOCK_UNDO, _('Revert Scratchpad'), '', None, self.revert_current_scratchpad_cb),
231             ('ScratchSaveAsDefault',  None, _('Save Scratchpad as Default'), None, None, self.save_scratchpad_as_default_cb),
232             ('ScratchClearDefault',  None, _('Clear the Default Scratchpad'), None, None, self.clear_default_scratchpad_cb),
233             ('ScratchPaletteOptions', None, _('Render a Palette')),
234             ('ScratchLoadPalette',  None, _('Load Palette File...'), None, None, self.draw_palette_cb),
235             ('ScratchDrawSatPalette',  None, _('Different Saturations of the current Color'), None, None, self.draw_sat_spectrum_cb),
236             ('ScratchCopyBackground',  None, _('Copy Background to Scratchpad'), None, None, self.scratchpad_copy_background_cb),
237
238             ('BrushMenu',    None, _('Brush')),
239             ('BrushChooserPopup', stock.TOOL_BRUSH, _("Change Brush..."), 'b', None, self.brush_chooser_popup_cb),
240             ('ImportBrushPack',       gtk.STOCK_OPEN, _('Import brush package...'), '', None, self.import_brush_pack_cb),
241
242             ('HelpMenu',   None, _('Help')),
243             ('Docu', gtk.STOCK_INFO, _('Where is the Documentation?'), None, None, self.show_infodialog_cb),
244             ('ShortcutHelp',  gtk.STOCK_INFO, _('Change the Keyboard Shortcuts?'), None, None, self.show_infodialog_cb),
245             ('About', gtk.STOCK_ABOUT, _('About MyPaint'), None, None, self.about_cb),
246
247             ('DebugMenu',    None, _('Debug')),
248             ('PrintMemoryLeak',  None, _('Print Memory Leak Info to Console (Slow!)'), None, None, self.print_memory_leak_cb),
249             ('RunGarbageCollector',  None, _('Run Garbage Collector Now'), None, None, self.run_garbage_collector_cb),
250             ('StartProfiling',  gtk.STOCK_EXECUTE, _('Start/Stop Python Profiling (cProfile)'), None, None, self.start_profiling_cb),
251             ('GtkInputDialog',  None, _('GTK input device dialog'), None, None, self.gtk_input_dialog_cb),
252
253
254             ('ViewMenu', None, _('View')),
255             ('MenuishBarMenu', None, _('Toolbars')),
256             ('ShowPopupMenu',    None, _('Popup Menu'), 'Menu', None, self.popupmenu_show_cb),
257             ('Fullscreen',   gtk.STOCK_FULLSCREEN, None, 'F11', None, self.fullscreen_cb),
258             ('ViewHelp',  gtk.STOCK_HELP, _('Help'), None, None, self.show_infodialog_cb),
259             ]
260         ag = self.action_group = gtk.ActionGroup('WindowActions')
261         self.app.add_action_group(ag)
262         ag.add_actions(actions)
263         self.update_fullscreen_action()
264
265         # Toggle actions
266         toggle_actions = [
267             ('PreferencesWindow', gtk.STOCK_PREFERENCES,
268                     _('Preferences'), None, None, self.toggle_window_cb),
269             ('InputTestWindow',  None,
270                     _('Test input devices'), None, None, self.toggle_window_cb),
271             ('FrameWindow',  None,
272                     _('Document Frame...'), None, None, self.toggle_window_cb),
273             ('LayersWindow', stock.TOOL_LAYERS,
274                     None, None, _("Toggle the Layers list"),
275                     self.toggle_window_cb),
276             ('BackgroundWindow', gtk.STOCK_PAGE_SETUP,
277                     _('Background'), None, None, self.toggle_window_cb),
278             ('BrushSelectionWindow', stock.TOOL_BRUSH,
279                     None, None,
280                     _("Edit and reorganise Brush Lists"),
281                     self.toggle_window_cb),
282             ('BrushSettingsWindow', gtk.STOCK_PROPERTIES,
283                     _('Brush Settings Editor'), '<control>b',
284                     _("Change Brush Settings in detail"),
285                     self.toggle_window_cb),
286             ('ColorSelectionWindow', stock.TOOL_COLOR_SELECTOR,
287                     None, None, _("Toggle the Colour Triangle"),
288                     self.toggle_window_cb),
289             ('ColorSamplerWindow', stock.TOOL_COLOR_SAMPLER,
290                     None, None, _("Toggle the advanced Colour Sampler"),
291                     self.toggle_window_cb),
292             ('ScratchWindow',  stock.TOOL_SCRATCHPAD, 
293                     None, None, _('Toggle the scratchpad'),
294                     self.toggle_window_cb),
295             ]
296         ag.add_toggle_actions(toggle_actions)
297
298         # Reflect changes from other places (like tools' close buttons) into
299         # the proxys' visible states.
300         lm = self.app.layout_manager
301         lm.tool_visibility_observers.append(self.update_toggled_item_visibility)
302         lm.subwindow_visibility_observers.append(self.update_subwindow_visibility)
303
304         # Initial toggle state
305         for spec in toggle_actions:
306             name = spec[0]
307             action = ag.get_action(name)
308             role = name[0].lower() + name[1:]
309             visible = not lm.get_window_hidden_by_role(role)
310             # The sidebar machinery won't be up yet, so reveal windows that
311             # should be initially visible only in an idle handler
312             gobject.idle_add(action.set_active, visible)
313
314         # More toggle actions - ones which don't control windows.
315         toggle_actions = [
316             ('ToggleSubwindows', None, _('Subwindows'), 'Tab',
317                     _("Show subwindows"), self.toggle_subwindows_cb,
318                     self.get_show_subwindows()),
319             ]
320         ag.add_toggle_actions(toggle_actions)
321
322         # Radio actions
323         menuishbar_radio_actions = [
324             ('MenuishBarRadioMenubar', None, _('Menubar only'), None,
325                 _("Show menu bar"),
326                 self.MENUISHBAR_RADIO_MENUBAR),
327             ('MenuishBarRadioMainToolbar', None, _('Toolbar only'), None,
328                 _("Show toolbar"),
329                 self.MENUISHBAR_RADIO_MAIN_TOOLBAR),
330             ('MenuishBarRadioMenubarAndMainToolbar', None, _('Both'), None,
331                 _("Show both the menu bar and the toolbar"),
332                 self.MENUISHBAR_RADIO_BOTH_BARS),
333             ]
334         menuishbar_state = 0
335         if self.get_ui_part_enabled("menubar"):
336             menuishbar_state += self.MENUISHBAR_RADIO_MENUBAR
337         if self.get_ui_part_enabled("main_toolbar"):
338             menuishbar_state += self.MENUISHBAR_RADIO_MAIN_TOOLBAR
339         if menuishbar_state == 0:
340             menuishbar_state = self.MENUISHBAR_RADIO_MAIN_TOOLBAR
341         ag.add_radio_actions(menuishbar_radio_actions,
342             menuishbar_state, self.on_menuishbar_radio_change)
343         gobject.idle_add(lambda: self.update_ui_parts())
344
345         # Keyboard handling
346         for action in self.action_group.list_actions():
347             self.app.kbm.takeover_action(action)
348
349     def init_stategroups(self):
350         sg = stategroup.StateGroup()
351         p2s = sg.create_popup_state
352         changer = p2s(colorselectionwindow.ColorChangerPopup(self.app))
353         ring = p2s(colorselectionwindow.ColorRingPopup(self.app))
354         hist = p2s(historypopup.HistoryPopup(self.app, self.app.doc.model))
355         pick = self.colorpick_state = p2s(colorpicker.ColorPicker(self.app, self.app.doc.model))
356
357         self.popup_states = {
358             'ColorChangerPopup': changer,
359             'ColorRingPopup': ring,
360             'ColorHistoryPopup': hist,
361             'ColorPickerPopup': pick,
362             }
363         changer.next_state = ring
364         ring.next_state = changer
365         changer.autoleave_timeout = None
366         ring.autoleave_timeout = None
367
368         pick.max_key_hit_duration = 0.0
369         pick.autoleave_timeout = None
370
371         hist.autoleave_timeout = 0.600
372         self.history_popup_state = hist
373
374     def init_main_widget(self):  # override
375         self.main_widget = self.app.doc.tdw
376
377     def init_menubar(self):   # override
378         # Load Menubar, duplicate into self.popupmenu
379         menupath = os.path.join(self.app.datapath, 'gui/menu.xml')
380         menubar_xml = open(menupath).read()
381         self.app.ui_manager.add_ui_from_string(menubar_xml)
382         self.popupmenu = self._clone_menu(menubar_xml, 'PopupMenu', self.app.doc.tdw)
383         self.menubar = self.app.ui_manager.get_widget('/Menubar')
384
385     def init_toolbar(self):
386         self.toolbar = toolbar.MainToolbar(self)
387
388     def _clone_menu(self, xml, name, owner=None):
389         """
390         Hopefully temporary hack for converting UIManager XML describing the
391         main menubar into a rebindable popup menu. UIManager by itself doesn't
392         let you do this, by design, but we need a bigger menu than the little
393         things it allows you to build.
394         """
395         ui_elt = ET.fromstring(xml)
396         rootmenu_elt = ui_elt.find("menubar")
397         rootmenu_elt.attrib["name"] = name
398         xml = ET.tostring(ui_elt)
399         self.app.ui_manager.add_ui_from_string(xml)
400         tmp_menubar = self.app.ui_manager.get_widget('/' + name)
401         popupmenu = gtk.Menu()
402         for item in tmp_menubar.get_children():
403             tmp_menubar.remove(item)
404             popupmenu.append(item)
405         if owner is not None:
406             popupmenu.attach_to_widget(owner, None)
407         popupmenu.set_title("MyPaint")
408         popupmenu.connect("selection-done", self.popupmenu_done_cb)
409         popupmenu.connect("deactivate", self.popupmenu_done_cb)
410         popupmenu.connect("cancel", self.popupmenu_done_cb)
411         self.popupmenu_last_active = None
412         return popupmenu
413
414
415     def update_title(self, filename):
416         if filename:
417             self.set_title("MyPaint - %s" % os.path.basename(filename))
418         else:
419             self.set_title("MyPaint")
420
421     # INPUT EVENT HANDLING
422     def drag_data_received(self, widget, context, x, y, selection, info, t):
423         if info == 1:
424             if selection.data:
425                 uri = selection.data.split("\r\n")[0]
426                 fn = helpers.uri2filename(uri)
427                 if os.path.exists(fn):
428                     if self.app.filehandler.confirm_destructive_action():
429                         self.app.filehandler.open_file(fn)
430         elif info == 2: # color
431             color = [((ord(selection.data[v]) | (ord(selection.data[v+1]) << 8)) / 65535.0)  for v in range(0,8,2)]
432             self.app.brush.set_color_rgb(color[:3])
433             self.app.ch.push_color(self.app.brush.get_color_hsv())
434             # Don't popup the color history for now, as I haven't managed to get it to cooperate.
435
436     def print_memory_leak_cb(self, action):
437         helpers.record_memory_leak_status(print_diff = True)
438
439     def run_garbage_collector_cb(self, action):
440         helpers.run_garbage_collector()
441
442     def start_profiling_cb(self, action):
443         if getattr(self, 'profiler_active', False):
444             self.profiler_active = False
445             return
446
447         def doit():
448             import cProfile
449             profile = cProfile.Profile()
450
451             self.profiler_active = True
452             print '--- GUI Profiling starts ---'
453             while self.profiler_active:
454                 profile.runcall(gtk.main_iteration, False)
455                 if not gtk.events_pending():
456                     time.sleep(0.050) # ugly trick to remove "user does nothing" from profile
457             print '--- GUI Profiling ends ---'
458
459             profile.dump_stats('profile_fromgui.pstats')
460             #print 'profile written to mypaint_profile.pstats'
461             os.system('gprof2dot.py -f pstats profile_fromgui.pstats | dot -Tpng -o profile_fromgui.png && feh profile_fromgui.png &')
462
463         gobject.idle_add(doit)
464
465     def gtk_input_dialog_cb(self, action):
466         d = gtk.InputDialog()
467         d.show()
468
469     def key_press_event_cb_before(self, win, event):
470         key = event.keyval
471         ctrl = event.state & gdk.CONTROL_MASK
472         shift = event.state & gdk.SHIFT_MASK
473         alt = event.state & gdk.MOD1_MASK
474         #ANY_MODIFIER = gdk.SHIFT_MASK | gdk.MOD1_MASK | gdk.CONTROL_MASK
475         #if event.state & ANY_MODIFIER:
476         #    # allow user shortcuts with modifiers
477         #    return False
478
479         # This may need a stateful flag
480         if self.app.scratchpad_doc.tdw.has_pointer:
481             thisdoc = self.app.scratchpad_doc
482             # Stop dragging on the main window
483             self.app.doc.tdw.dragfunc = None
484         else:
485             thisdoc = self.app.doc
486             # Stop dragging on the other window
487             self.app.scratchpad_doc.tdw.dragfunc = None
488         if key == keysyms.space:
489             if shift:
490                  thisdoc.tdw.start_drag(thisdoc.dragfunc_rotate)
491             elif ctrl:
492                 thisdoc.tdw.start_drag(thisdoc.dragfunc_zoom)
493             elif alt:
494                 thisdoc.tdw.start_drag(thisdoc.dragfunc_frame)
495             else:
496                 thisdoc.tdw.start_drag(thisdoc.dragfunc_translate)
497         else: return False
498         return True
499
500     def key_release_event_cb_before(self, win, event):
501         if self.app.scratchpad_doc.tdw.has_pointer:
502             thisdoc = self.app.scratchpad_doc
503         else:
504             thisdoc = self.app.doc
505         if event.keyval == keysyms.space:
506             thisdoc.tdw.stop_drag(thisdoc.dragfunc_translate)
507             thisdoc.tdw.stop_drag(thisdoc.dragfunc_rotate)
508             thisdoc.tdw.stop_drag(thisdoc.dragfunc_zoom)
509             thisdoc.tdw.stop_drag(thisdoc.dragfunc_frame)
510             return True
511         return False
512
513     def key_press_event_cb_after(self, win, event):
514         key = event.keyval
515         if self.is_fullscreen and key == keysyms.Escape:
516             self.fullscreen_cb()
517         else:
518             return False
519         return True
520
521     def key_release_event_cb_after(self, win, event):
522         return False
523
524     def button_press_cb(self, win, event):
525         return button_press_cb_abstraction(self, win, event, self.app.doc)
526
527     def button_release_cb(self, win, event):
528         return button_release_cb_abstraction(win, event, self.app.doc)
529
530     def scroll_cb(self, win, event):
531         d = event.direction
532         if d == gdk.SCROLL_UP:
533             if event.state & gdk.SHIFT_MASK:
534                 self.app.doc.rotate('RotateLeft')
535             else:
536                 self.app.doc.zoom('ZoomIn')
537         elif d == gdk.SCROLL_DOWN:
538             if event.state & gdk.SHIFT_MASK:
539                 self.app.doc.rotate('RotateRight')
540             else:
541                 self.app.doc.zoom('ZoomOut')
542         elif d == gdk.SCROLL_RIGHT:
543             self.app.doc.rotate('RotateRight')
544         elif d == gdk.SCROLL_LEFT:
545             self.app.doc.rotate('RotateLeft')
546
547     # WINDOW HANDLING
548     def toggle_window_cb(self, action):
549         if self._updating_toggled_item:
550             return
551         s = action.get_name()
552         active = action.get_active()
553         window_name = s[0].lower() + s[1:] # WindowName -> windowName
554         # If it's a tool, get it to hide/show itself
555         t = self.app.layout_manager.get_tool_by_role(window_name)
556         if t is not None:
557             t.set_hidden(not active)
558             return
559         # Otherwise, if it's a regular subwindow hide/show+present it.
560         w = self.app.layout_manager.get_subwindow_by_role(window_name)
561         if w is None:
562             return
563         onscreen = w.window is not None and w.window.is_visible()
564         if active:
565             if onscreen:
566                 return
567             w.show_all()
568             w.present()
569         else:
570             if not onscreen:
571                 return
572             w.hide()
573
574     def update_subwindow_visibility(self, window, active):
575         # Responds to non-tool subwindows being hidden and shown
576         role = window.get_role()
577         self.update_toggled_item_visibility(role, active)
578
579     def update_toggled_item_visibility(self, role, active, *a, **kw):
580         # Responds to any item with a role being hidden or shown by
581         # silently updating its ToggleAction to match.
582         action_name = role[0].upper() + role[1:]
583         action = self.action_group.get_action(action_name)
584         if action is None:
585             warn("Unable to find action %s" % action_name, RuntimeWarning, 1)
586             return
587         if action.get_active() != active:
588             self._updating_toggled_item = True
589             action.set_active(active)
590             self._updating_toggled_item = False
591
592     def popup_cb(self, action):
593         state = self.popup_states[action.get_name()]
594         state.activate(action)
595
596
597     def brush_chooser_popup_cb(self, action):
598         # It may be even nicer to do this as a real popup state with
599         # mouse-out to cancel. The Action is named accordingly. For now
600         # though a modal dialog will do as an implementation.
601         dialogs.change_current_brush_quick(self.app)
602
603     def color_details_dialog_cb(self, action):
604         dialogs.change_current_color_detailed(self.app)
605
606
607     # User-toggleable UI pieces: things like toolbars, status bars, menu bars.
608     # Saved between sessions.
609
610
611     def get_ui_part_enabled(self, part_name):
612         """Returns whether the named UI part is enabled in the prefs.
613         """
614         parts = self.app.preferences["ui.parts"]
615         return bool(parts.get(part_name, False))
616
617
618     def update_ui_parts(self, **updates):
619         """Updates the UI part prefs, then hide/show widgets to match.
620
621         Called without arguments, this updates the UI to match the
622         boolean-valued hash ``ui.parts`` in the app preferences. With keyword
623         arguments, the prefs are updated first, then changes are reflected in
624         the set of visible widgets. Current known parts:
625
626             :``main_toolbar``:
627                 The primary toolbar and its menu button.
628             :``menubar``:
629                 A conventional menu bar.
630
631         Currently the user cannot turn off both the main toolbar and the
632         menubar: the toolbar will be forced on if an attempt is made.
633         """
634         new_state = self.app.preferences["ui.parts"].copy()
635         new_state.update(updates)
636         # Menu bar
637         if new_state.get("menubar", False):
638             self.menubar.show_all()
639         else:
640             self.menubar.hide()
641             if not new_state.get("main_toolbar", False):
642                 new_state["main_toolbar"] = True
643         # Toolbar
644         if new_state.get("main_toolbar", False):
645             self.toolbar.show_all()
646         else:
647             self.toolbar.hide()
648         self.app.preferences["ui.parts"] = new_state
649         self.update_menu_button()
650
651
652     def update_menu_button(self):
653         """Updates the menu button to match toolbar and menubar visibility.
654
655         The menu button is visible when the menu bar is hidden. Since the user
656         must have either a toolbar or a menu or both, this ensures that a menu
657         is on-screen at all times in non-fullscreen mode.
658         """
659         toolbar_visible = self.toolbar.get_property("visible")
660         menubar_visible = self.menubar.get_property("visible")
661         if toolbar_visible and menubar_visible:
662             self.toolbar.menu_button.hide()
663         else:
664             self.toolbar.menu_button.show_all()
665
666
667     def on_menuishbar_radio_change(self, radioaction, current):
668         """Respond to a change of the 'menu bar/toolbar' radio menu items.
669         """
670         value = radioaction.get_current_value()
671         if value == self.MENUISHBAR_RADIO_MENUBAR:
672             self.update_ui_parts(main_toolbar=False, menubar=True)
673         elif value == self.MENUISHBAR_RADIO_MAIN_TOOLBAR:
674             self.update_ui_parts(main_toolbar=True, menubar=False)
675         else:
676             self.update_ui_parts(main_toolbar=True, menubar=True)
677
678
679     # Show Subwindows
680     # Not saved between sessions, defaults to on.
681     # Controlled via its ToggleAction, and entering or leaving fullscreen mode
682     # according to the setting of ui.hide_in_fullscreen in prefs.
683
684     def set_show_subwindows(self, show_subwindows):
685         """Programatically set the Show Subwindows option.
686         """
687         action = self.action_group.get_action("ToggleSubwindows")
688         currently_showing = action.get_active()
689         if show_subwindows != currently_showing:
690             action.set_active(show_subwindows)
691         self._show_subwindows = self._show_subwindows
692
693     def get_show_subwindows(self):
694         return self._show_subwindows
695
696     def toggle_subwindows_cb(self, action):
697         active = action.get_active()
698         lm = self.app.layout_manager
699         if active:
700             lm.toggle_user_tools(on=True)
701         else:
702             lm.toggle_user_tools(on=False)
703         self._show_subwindows = active
704
705
706     # Fullscreen mode
707     # This implementation requires an ICCCM and EWMH-compliant window manager
708     # which supports the _NET_WM_STATE_FULLSCREEN hint. There are several
709     # available.
710
711     def fullscreen_cb(self, *junk):
712         if not self.is_fullscreen:
713             self.fullscreen()
714         else:
715             self.unfullscreen()
716
717     def window_state_event_cb(self, widget, event):
718         # Respond to changes of the fullscreen state only
719         if not event.changed_mask & gdk.WINDOW_STATE_FULLSCREEN:
720             return
721         lm = self.app.layout_manager
722         self.is_fullscreen = event.new_window_state & gdk.WINDOW_STATE_FULLSCREEN
723         if self.is_fullscreen:
724             # Subwindow hiding 
725             if self.app.preferences.get("ui.hide_subwindows_in_fullscreen", True):
726                 self.set_show_subwindows(False)
727                 self._restore_subwindows_on_unfullscreen = True
728             if self.app.preferences.get("ui.hide_menubar_in_fullscreen", True):
729                 self.menubar.hide()
730                 self._restore_menubar_on_unfullscreen = True
731             if self.app.preferences.get("ui.hide_toolbar_in_fullscreen", True):
732                 self.toolbar.hide()
733                 self._restore_toolbar_on_unfullscreen = True
734             # fix for fullscreen problem on Windows, https://gna.org/bugs/?15175
735             # on X11/Metacity it also helps a bit against flickering during the switch
736             while gtk.events_pending():
737                 gtk.main_iteration()
738         else:
739             while gtk.events_pending():
740                 gtk.main_iteration()
741             if getattr(self, "_restore_menubar_on_unfullscreen", False):
742                 if self.get_ui_part_enabled("menubar"):
743                     self.menubar.show()
744                 del self._restore_menubar_on_unfullscreen
745             if getattr(self, "_restore_toolbar_on_unfullscreen", False):
746                 if self.get_ui_part_enabled("main_toolbar"):
747                     self.toolbar.show()
748                 del self._restore_toolbar_on_unfullscreen
749             if getattr(self, "_restore_subwindows_on_unfullscreen", False):
750                 self.set_show_subwindows(True)
751                 del self._restore_subwindows_on_unfullscreen
752         self.update_menu_button()
753         self.update_fullscreen_action()
754
755     def update_fullscreen_action(self):
756         action = self.action_group.get_action("Fullscreen")
757         if self.is_fullscreen:
758             action.set_stock_id(gtk.STOCK_LEAVE_FULLSCREEN)
759             action.set_tooltip(_("Leave Fullscreen Mode"))
760             action.set_label(_("UnFullscreen"))
761         else:
762             action.set_stock_id(gtk.STOCK_FULLSCREEN)
763             action.set_tooltip(_("Enter Fullscreen Mode"))
764             action.set_label(_("Fullscreen"))
765
766     def popupmenu_show_cb(self, action):
767         self.show_popupmenu()
768
769     def show_popupmenu(self, event=None):
770         self.menubar.set_sensitive(False)   # excessive feedback?
771         self.toolbar.menu_button.set_sensitive(False)
772         button = 1
773         time = 0
774         if event is not None:
775             if event.type == gdk.BUTTON_PRESS:
776                 button = event.button
777                 time = event.time
778         self.popupmenu.popup(None, None, None, button, time)
779         if event is None:
780             # We're responding to an Action, most probably the menu key.
781             # Open out the last highlighted menu to speed key navigation up.
782             if self.popupmenu_last_active is None:
783                 self.popupmenu.select_first(True) # one less keypress
784             else:
785                 self.popupmenu.select_item(self.popupmenu_last_active)
786
787     def popupmenu_done_cb(self, *a, **kw):
788         # Not sure if we need to bother with this level of feedback,
789         # but it actually looks quite nice to see one menu taking over
790         # the other. Makes it clear that the popups are the same thing as
791         # the full menu, maybe.
792         self.menubar.set_sensitive(True)
793         self.toolbar.menu_button.set_sensitive(True)
794         self.popupmenu_last_active = self.popupmenu.get_active()
795
796     # BEGIN -- Scratchpad menu options
797     def save_scratchpad_as_default_cb(self, action):
798         self.app.filehandler.save_scratchpad(self.app.filehandler.get_scratchpad_default(), export = True)
799
800     def clear_default_scratchpad_cb(self, action):
801         self.app.filehandler.delete_default_scratchpad()
802
803     # Unneeded since 'Save blank canvas' bug has been addressed.
804     #def clear_autosave_scratchpad_cb(self, action):
805     #    self.app.filehandler.delete_autosave_scratchpad()
806
807     def new_scratchpad_cb(self, action):
808         if os.path.isfile(self.app.filehandler.get_scratchpad_default()):
809             self.app.filehandler.open_scratchpad(self.app.filehandler.get_scratchpad_default())
810         else:
811             self.app.scratchpad_doc.model.clear()
812             # With no default - adopt the currently chosen background
813             bg = self.app.doc.model.background
814             if self.app.scratchpad_doc:
815                 self.app.scratchpad_doc.model.set_background(bg)
816
817         self.app.scratchpad_filename = self.app.preferences['scratchpad.last_opened'] = self.app.filehandler.get_scratchpad_autosave()
818
819     def load_scratchpad_cb(self, action):
820         if self.app.scratchpad_filename:
821             self.save_current_scratchpad_cb(action)
822             current_pad = self.app.scratchpad_filename
823         else:
824             current_pad = self.app.filehandler.get_scratchpad_autosave()
825         self.app.filehandler.open_scratchpad_dialog()
826         # Check to see if a file has been opened outside of the scratchpad directory
827         if not os.path.abspath(self.app.scratchpad_filename).startswith(os.path.abspath(self.app.filehandler.get_scratchpad_prefix())):
828             # file is NOT within the scratchpad directory - load copy as current scratchpad
829             self.app.scratchpad_filename = self.app.preferences['scratchpad.last_opened'] = current_pad
830
831     def save_as_scratchpad_cb(self, action):
832         self.app.filehandler.save_scratchpad_as_dialog()
833
834     def revert_current_scratchpad_cb(self, action):
835         if os.path.isfile(self.app.scratchpad_filename):
836             self.app.filehandler.open_scratchpad(self.app.scratchpad_filename)
837             print "Reverted to %s" % self.app.scratchpad_filename
838         else:
839             print "No file to revert to yet."
840
841     def save_current_scratchpad_cb(self, action):
842         self.app.filehandler.save_scratchpad(self.app.scratchpad_filename)
843
844     def scratchpad_copy_background_cb(self, action):
845         bg = self.app.doc.model.background
846         if self.app.scratchpad_doc:
847             self.app.scratchpad_doc.model.set_background(bg)
848
849     def draw_palette_cb(self, action):
850         # test functionality:
851         file_filters = [
852         (_("Gimp Palette Format"), ("*.gpl",)),
853         (_("All Files"), ("*.*",)),
854         ]
855         gimp_path = os.path.join(self.app.filehandler.get_gimp_prefix(), "palettes")
856         dialog = self.app.filehandler.get_open_dialog(start_in_folder=gimp_path,
857                                                   file_filters = file_filters)
858         try:
859             if dialog.run() == gtk.RESPONSE_OK:
860                 dialog.hide()
861                 filename = dialog.get_filename().decode('utf-8')
862                 if filename:
863                     #filename = "/home/ben/.gimp-2.6/palettes/Nature_Grass.gpl" # TEMP HACK TO TEST
864                     g = GimpPalette(filename)
865                     grid_size = 30.0
866                     column_limit = 7
867                     # IGNORE Gimp Palette 'columns'?
868                     if g.columns != 0:
869                         column_limit = g.columns   # use the value for columns in the palette
870                     draw_palette(self.app, g, self.app.scratchpad_doc, columns=column_limit, grid_size=grid_size, swatch_method=hatch_squiggle, scale = 25.0)
871         finally:
872             dialog.destroy()
873
874     def draw_sat_spectrum_cb(self, action):
875         g = GimpPalette()
876         hsv = self.app.brush.get_color_hsv()
877         g.append_sat_spectrum(hsv)
878         grid_size = 30.0
879         off_x = off_y = grid_size / 2.0
880         column_limit = 8
881         draw_palette(self.app, g, self.app.scratchpad_doc, columns=column_limit, grid_size=grid_size)
882
883     # END -- Scratchpad menu options
884
885     def quit_cb(self, *junk):
886         self.app.doc.model.split_stroke()
887         self.app.save_gui_config() # FIXME: should do this periodically, not only on quit
888
889         if not self.app.filehandler.confirm_destructive_action(title=_('Quit'), question=_('Really Quit?')):
890             return True
891
892         gtk.main_quit()
893         return False
894
895     def toggle_frame_cb(self, action):
896         enabled = self.app.doc.model.frame_enabled
897         self.app.doc.model.set_frame_enabled(not enabled)
898
899     def import_brush_pack_cb(self, *junk):
900         format_id, filename = dialogs.open_dialog(_("Import brush package..."), self,
901                                  [(_("MyPaint brush package (*.zip)"), "*.zip")])
902         if filename:
903             self.app.brushmanager.import_brushpack(filename,  self)
904
905     # INFORMATION
906     # TODO: Move into dialogs.py?
907     def about_cb(self, action):
908         d = gtk.AboutDialog()
909         d.set_transient_for(self)
910         d.set_program_name("MyPaint")
911         d.set_version(MYPAINT_VERSION)
912         d.set_copyright(_("Copyright (C) 2005-2010\nMartin Renold and the MyPaint Development Team"))
913         d.set_website("http://mypaint.info/")
914         d.set_logo(self.app.pixmaps.mypaint_logo)
915         d.set_license(
916             _(u"This program is free software; you can redistribute it and/or modify "
917               u"it under the terms of the GNU General Public License as published by "
918               u"the Free Software Foundation; either version 2 of the License, or "
919               u"(at your option) any later version.\n"
920               u"\n"
921               u"This program is distributed in the hope that it will be useful, "
922               u"but WITHOUT ANY WARRANTY. See the COPYING file for more details.")
923             )
924         d.set_wrap_license(True)
925         d.set_authors([
926             # (in order of appearance)
927             u"Martin Renold (%s)" % _('programming'),
928             u"Yves Combe (%s)" % _('portability'),
929             u"Popolon (%s)" % _('programming'),
930             u"Clement Skau (%s)" % _('programming'),
931             u"Jon Nordby (%s)" % _('programming'),
932             u"Álinson Santos (%s)" % _('programming'),
933             u"Tumagonx (%s)" % _('portability'),
934             u"Ilya Portnov (%s)" % _('programming'),
935             u"Jonas Wagner (%s)" % _('programming'),
936             u"Luka Čehovin (%s)" % _('programming'),
937             u"Andrew Chadwick (%s)" % _('programming'),
938             u"Till Hartmann (%s)" % _('programming'),
939             u'David Grundberg (%s)' % _('programming'),
940             u"Krzysztof Pasek (%s)" % _('programming'),
941             u"Ben O'Steen (%s)" % _('programming'),
942             u"Ferry Jérémie (%s)" % _('programming'),
943             ])
944         d.set_artists([
945             u"Artis Rozentāls (%s)" % _('brushes'),
946             u"Popolon (%s)" % _('brushes'),
947             u"Marcelo 'Tanda' Cerviño (%s)" % _('patterns, brushes'),
948             u"David Revoy (%s)" % _('brushes'),
949             u"Ramón Miranda (%s)" % _('brushes, patterns'),
950             u"Enrico Guarnieri 'Ico_dY' (%s)" % _('brushes'),
951             u'Sebastian Kraft (%s)' % _('desktop icon'),
952             u"Nicola Lunghi (%s)" % _('patterns'),
953             u"Toni Kasurinen (%s)" % _('brushes'),
954             u"Сан Саныч 'MrMamurk' (%s)" % _('patterns'),
955             u"Andrew Chadwick (%s)" % _('tool icons'),
956             u"Ben O'Steen (%s)" % _('tool icons'),
957             ])
958         # list all translators, not only those of the current language
959         d.set_translator_credits(
960             u'Ilya Portnov (ru)\n'
961             u'Popolon (fr, zh_CN, ja)\n'
962             u'Jon Nordby (nb)\n'
963             u'Griatch (sv)\n'
964             u'Tobias Jakobs (de)\n'
965             u'Martin Tabačan (cs)\n'
966             u'Tumagonx (id)\n'
967             u'Manuel Quiñones (es)\n'
968             u'Gergely Aradszki (hu)\n'
969             u'Lamberto Tedaldi (it)\n'
970             u'Dong-Jun Wu (zh_TW)\n'
971             u'Luka Čehovin (sl)\n'
972             u'Geuntak Jeong (ko)\n'
973             u'Łukasz Lubojański (pl)\n'
974             u'Daniel Korostil (uk)\n'
975             u'Julian Aloofi (de)\n'
976             u'Tor Egil Hoftun Kvæstad (nn_NO)\n'
977             u'João S. O. Bueno (pt_BR)\n'
978             u'David Grundberg (sv)\n'
979             u'Elliott Sales de Andrade (en_CA)\n'
980             )
981
982         d.run()
983         d.destroy()
984
985     def show_infodialog_cb(self, action):
986         text = {
987         'ShortcutHelp':
988                 _("Move your mouse over a menu entry, then press the key to assign."),
989         'ViewHelp':
990                 _("You can also drag the canvas with the mouse while holding the middle "
991                 "mouse button or spacebar. Or with the arrow keys."
992                 "\n\n"
993                 "In contrast to earlier versions, scrolling and zooming are harmless now and "
994                 "will not make you run out of memory. But you still require a lot of memory "
995                 "if you paint all over while fully zoomed out."),
996         'ContextHelp':
997                 _("Brushkeys are used to quickly save/restore brush settings "
998                  "using keyboard shortcuts. You can paint with one hand and "
999                  "change brushes with the other without interruption."
1000                  "\n\n"
1001                  "There are 10 memory slots to hold brush settings.\n"
1002                  "They are anonymous brushes, which are not visible in the "
1003                  "brush selector list. But they are remembered even if you "
1004                  "quit."),
1005         'Docu':
1006                 _("There is a tutorial available on the MyPaint homepage. It "
1007                  "explains some features which are hard to discover yourself."
1008                  "\n\n"
1009                  "Comments about the brush settings (opaque, hardness, etc.) and "
1010                  "inputs (pressure, speed, etc.) are available as tooltips. "
1011                  "Put your mouse over a label to see them. "
1012                  "\n"),
1013         }
1014         self.app.message_dialog(text[action.get_name()])
1015
1016