OSDN Git Service

window toggles: fix annoying saved pos regression
[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
24 import colorselectionwindow, historypopup, stategroup, colorpicker, windowing, layout
25 import dialogs
26 from lib import helpers
27 import stock
28
29 import xml.etree.ElementTree as ET
30
31 # palette support
32 from lib.scratchpad_palette import GimpPalette, squiggle
33
34 # TODO: put in a helper file?
35 def with_wait_cursor(func):
36     """python decorator that adds a wait cursor around a function"""
37     def wrapper(self, *args, **kwargs):
38         toplevels = [t for t in gtk.window_list_toplevels()
39                      if t.window is not None]
40         for toplevel in toplevels:
41             toplevel.window.set_cursor(gdk.Cursor(gdk.WATCH))
42             toplevel.set_sensitive(False)
43         self.app.doc.tdw.grab_add()
44         try:
45             func(self, *args, **kwargs)
46             # gtk main loop may be called in here...
47         finally:
48             for toplevel in toplevels:
49                 toplevel.set_sensitive(True)
50                 # ... which is why we need this check:
51                 if toplevel.window is not None:
52                     toplevel.window.set_cursor(None)
53             self.app.doc.tdw.grab_remove()
54     return wrapper
55
56 def button_press_cb_abstraction(drawwindow, win, event, doc):
57     #print event.device, event.button
58
59     ## Ignore accidentals
60     # Single button-presses only, not 2ble/3ple
61     if event.type != gdk.BUTTON_PRESS:
62         # ignore the extra double-click event
63         return False
64
65     if event.button != 1:
66         # check whether we are painting (accidental)
67         if event.state & gdk.BUTTON1_MASK:
68             # Do not allow dragging in the middle of
69             # painting. This often happens by accident with wacom
70             # tablet's stylus button.
71             #
72             # However we allow dragging if the user's pressure is
73             # still below the click threshold.  This is because
74             # some tablet PCs are not able to produce a
75             # middle-mouse click without reporting pressure.
76             # https://gna.org/bugs/index.php?15907
77             return False
78
79     # Pick a suitable config option
80     ctrl = event.state & gdk.CONTROL_MASK
81     alt  = event.state & gdk.MOD1_MASK
82     shift = event.state & gdk.SHIFT_MASK
83     if shift:
84         modifier_str = "_shift"
85     elif alt or ctrl:
86         modifier_str = "_ctrl"
87     else:
88         modifier_str = ""
89     prefs_name = "input.button%d%s_action" % (event.button, modifier_str)
90     action_name = drawwindow.app.preferences.get(prefs_name, "no_action")
91
92     # No-ops
93     if action_name == 'no_action':
94         return True  # We handled it by doing nothing
95
96     # Straight line
97     # Really belongs in the tdw, but this is the only object with access
98     # to the application preferences.
99     if action_name == 'straight_line':
100         doc.tdw.straight_line_from_last_pos(is_sequence=False)
101         return True
102     if action_name == 'straight_line_sequence':
103         doc.tdw.straight_line_from_last_pos(is_sequence=True)
104         return True
105
106     # View control
107     if action_name.endswith("_canvas"):
108         dragfunc = None
109         if action_name == "pan_canvas":
110             dragfunc = doc.dragfunc_translate
111         elif action_name == "zoom_canvas":
112             dragfunc = doc.dragfunc_zoom
113         elif action_name == "rotate_canvas":
114             dragfunc = doc.dragfunc_rotate
115         if dragfunc is not None:
116             doc.tdw.start_drag(dragfunc)
117             return True
118         return False
119
120     # Application menu
121     if action_name == 'popup_menu':
122         drawwindow.show_popupmenu(event=event)
123         return True
124
125     if action_name in drawwindow.popup_states:
126         state = drawwindow.popup_states[action_name]
127         state.activate(event)
128         return True
129
130     # Dispatch regular GTK events.
131     for ag in drawwindow.action_group, doc.action_group:
132         action = ag.get_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     def __init__(self, app):
149         windowing.MainWindow.__init__(self, app)
150         self.app = app
151
152         # Window handling
153         self._updating_toggled_item = False
154         self._show_subwindows = True
155         self.is_fullscreen = False
156
157         # Enable drag & drop
158         self.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
159                             gtk.DEST_DEFAULT_HIGHLIGHT |
160                             gtk.DEST_DEFAULT_DROP,
161                             [("text/uri-list", 0, 1),
162                              ("application/x-color", 0, 2)],
163                             gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY)
164
165         # Connect events
166         self.connect('delete-event', self.quit_cb)
167         self.connect('key-press-event', self.key_press_event_cb_before)
168         self.connect('key-release-event', self.key_release_event_cb_before)
169         self.connect_after('key-press-event', self.key_press_event_cb_after)
170         self.connect_after('key-release-event', self.key_release_event_cb_after)
171         self.connect("drag-data-received", self.drag_data_received)
172         self.connect("window-state-event", self.window_state_event_cb)
173
174         self.app.filehandler.current_file_observers.append(self.update_title)
175
176         self.init_actions()
177
178         lm = app.layout_manager
179         layout.MainWindow.__init__(self, lm)
180         self.main_widget.connect("button-press-event", self.button_press_cb)
181         self.main_widget.connect("button-release-event",self.button_release_cb)
182         self.main_widget.connect("scroll-event", self.scroll_cb)
183
184         kbm = self.app.kbm
185         kbm.add_extra_key('Menu', 'ShowPopupMenu')
186         kbm.add_extra_key('Tab', 'ToggleSubwindows')
187
188         self.init_stategroups()
189
190     #XXX: Compatability
191     def get_doc(self):
192         print "DeprecationWarning: Use app.doc instead"
193         return self.app.doc
194     def get_tdw(self):
195         print "DeprecationWarning: Use app.doc.tdw instead"
196         return self.app.doc.tdw
197     tdw, doc = property(get_tdw), property(get_doc)
198
199     def init_actions(self):
200         actions = [
201             # name, stock id, label, accelerator, tooltip, callback
202             ('FileMenu',    None, _('File')),
203             ('Quit',         gtk.STOCK_QUIT, _('Quit'), '<control>q', None, self.quit_cb),
204             ('FrameToggle',  None, _('Toggle Document Frame'), None, None, self.toggle_frame_cb),
205
206             ('EditMenu',        None, _('Edit')),
207
208             ('ColorMenu',    None, _('Color')),
209             ('ColorPickerPopup',    gtk.STOCK_COLOR_PICKER, _('Pick Color'), 'r', None, self.popup_cb),
210             ('ColorHistoryPopup',  None, _('Color History'), 'x', None, self.popup_cb),
211             ('ColorChangerPopup', None, _('Color Changer'), 'v', None, self.popup_cb),
212             ('ColorRingPopup',  None, _('Color Ring'), None, None, self.popup_cb),
213
214             ('ContextMenu',  None, _('Brushkeys')),
215             ('ContextHelp',  gtk.STOCK_HELP, _('Help!'), None, None, self.show_infodialog_cb),
216
217             ('LayerMenu',    None, _('Layers')),
218
219             ('ScratchMenu',    None, _('Scratchpad')),
220             ('ScratchWindow',  None, _('Scratchpad...'), None, None, self.toggleWindow_cb),
221             ('ScratchSaveNow',  None, _('Save Scratchpad Now'), None, None, self.save_current_scratchpad_cb),
222             ('ScratchSaveAsDefault',  None, _('Save Scratchpad As Default'), None, None, self.save_scratchpad_as_default_cb),
223             ('ScratchClearDefault',  None, _('Clear the Default Scratchpad'), None, None, self.clear_default_scratchpad_cb),
224             ('ScratchClearAutosave',  None, _('Clear the Autosaved Scratchpad'), None, None, self.clear_autosave_scratchpad_cb),
225             ('ScratchLoadPalette',  None, _('Draw a palette in the current Scratchpad'), None, None, self.draw_palette_cb),
226
227
228             ('BrushMenu',    None, _('Brush')),
229             ('ImportBrushPack',       gtk.STOCK_OPEN, _('Import brush package...'), '', None, self.import_brush_pack_cb),
230
231             ('HelpMenu',   None, _('Help')),
232             ('Docu', gtk.STOCK_INFO, _('Where is the Documentation?'), None, None, self.show_infodialog_cb),
233             ('ShortcutHelp',  gtk.STOCK_INFO, _('Change the Keyboard Shortcuts?'), None, None, self.show_infodialog_cb),
234             ('About', gtk.STOCK_ABOUT, _('About MyPaint'), None, None, self.about_cb),
235
236             ('DebugMenu',    None, _('Debug')),
237             ('PrintMemoryLeak',  None, _('Print Memory Leak Info to Console (Slow!)'), None, None, self.print_memory_leak_cb),
238             ('RunGarbageCollector',  None, _('Run Garbage Collector Now'), None, None, self.run_garbage_collector_cb),
239             ('StartProfiling',  gtk.STOCK_EXECUTE, _('Start/Stop Python Profiling (cProfile)'), None, None, self.start_profiling_cb),
240             ('GtkInputDialog',  None, _('GTK input device dialog'), None, None, self.gtk_input_dialog_cb),
241
242
243             ('ViewMenu', None, _('View')),
244             ('ShowPopupMenu',    None, _('Popup Menu'), 'Menu', None, self.popupmenu_show_cb),
245             ('Fullscreen',   gtk.STOCK_FULLSCREEN, _('Fullscreen'), 'F11', None, self.fullscreen_cb),
246             ('ViewHelp',  gtk.STOCK_HELP, _('Help'), None, None, self.show_infodialog_cb),
247             ]
248         ag = self.action_group = gtk.ActionGroup('WindowActions')
249         ag.add_actions(actions)
250
251         # Toggle actions
252         toggle_actions = [
253             ('PreferencesWindow', gtk.STOCK_PREFERENCES,
254                     _('Preferences'), None, None, self.toggle_window_cb),
255             ('InputTestWindow',  None,
256                     _('Test input devices'), None, None, self.toggle_window_cb),
257             ('FrameWindow',  None,
258                     _('Document Frame...'), None, None, self.toggle_window_cb),
259             ('LayersWindow', stock.TOOL_LAYERS,
260                     None, None, _("Toggle the Layers list"),
261                     self.toggle_window_cb),
262             ('BackgroundWindow', gtk.STOCK_PAGE_SETUP,
263                     _('Background'), None, None, self.toggle_window_cb),
264             ('BrushSelectionWindow', stock.TOOL_BRUSH,
265                     None, None, _("Toggle the Brush selector"),
266                     self.toggle_window_cb),
267             ('BrushSettingsWindow', gtk.STOCK_PROPERTIES,
268                     _('Brush Editor'), '<control>b', None,
269                     self.toggle_window_cb),
270             ('ColorSelectionWindow', stock.TOOL_COLOR_SELECTOR,
271                     None, None, _("Toggle the Colour Triangle"),
272                     self.toggle_window_cb),
273             ('ColorSamplerWindow', stock.TOOL_COLOR_SAMPLER,
274                     None, None, _("Toggle the advanced Colour Sampler"),
275                     self.toggle_window_cb),
276             ]
277         ag.add_toggle_actions(toggle_actions)
278
279         # Reflect changes from other places (like tools' close buttons) into
280         # the proxys' visible states.
281         lm = self.app.layout_manager
282         lm.tool_visibility_observers.append(self.update_toggled_item_visibility)
283         lm.subwindow_visibility_observers.append(self.update_subwindow_visibility)
284
285         # Initial toggle state
286         for spec in toggle_actions:
287             name = spec[0]
288             action = ag.get_action(name)
289             role = name[0].lower() + name[1:]
290             visible = not lm.get_window_hidden_by_role(role)
291             # The sidebar machinery won't be up yet, so reveal windows that
292             # should be initially visible only in an idle handler
293             gobject.idle_add(action.set_active, visible)
294
295         # More toggle actions - ones which don't control windows.
296         toggle_actions = [
297             ('ToggleToolbar', None, _('Toolbar'), None,
298                     _("Show toolbar"), self.toggle_toolbar_cb,
299                     self.get_show_toolbar()),
300             ('ToggleSubwindows', None, _('Subwindows'), 'Tab',
301                     _("Show subwindows"), self.toggle_subwindows_cb,
302                     self.get_show_subwindows()),
303             ]
304         ag.add_toggle_actions(toggle_actions)
305
306         # Keyboard handling
307         for action in self.action_group.list_actions():
308             self.app.kbm.takeover_action(action)
309         self.app.ui_manager.insert_action_group(ag, -1)
310
311     def init_stategroups(self):
312         sg = stategroup.StateGroup()
313         p2s = sg.create_popup_state
314         changer = p2s(colorselectionwindow.ColorChangerPopup(self.app))
315         ring = p2s(colorselectionwindow.ColorRingPopup(self.app))
316         hist = p2s(historypopup.HistoryPopup(self.app, self.app.doc.model))
317         pick = self.colorpick_state = p2s(colorpicker.ColorPicker(self.app, self.app.doc.model))
318
319         self.popup_states = {
320             'ColorChangerPopup': changer,
321             'ColorRingPopup': ring,
322             'ColorHistoryPopup': hist,
323             'ColorPickerPopup': pick,
324             }
325         changer.next_state = ring
326         ring.next_state = changer
327         changer.autoleave_timeout = None
328         ring.autoleave_timeout = None
329
330         pick.max_key_hit_duration = 0.0
331         pick.autoleave_timeout = None
332
333         hist.autoleave_timeout = 0.600
334         self.history_popup_state = hist
335
336     def init_main_widget(self):  # override
337         self.main_widget = self.app.doc.tdw
338
339     def init_menubar(self):   # override
340         # Load Menubar, duplicate into self.popupmenu
341         menupath = os.path.join(self.app.datapath, 'gui/menu.xml')
342         menubar_xml = open(menupath).read()
343         self.app.ui_manager.add_ui_from_string(menubar_xml)
344         self._init_popupmenu(menubar_xml)
345         self.menubar = self.app.ui_manager.get_widget('/Menubar')
346
347     def init_toolbar(self):
348         action_groups = self.app.ui_manager.get_action_groups()
349         def findaction(name):
350             for group in action_groups:
351                 action = group.get_action(name)
352                 if action is not None:
353                     return action
354             return None
355         bar = gtk.Toolbar()
356         bar.insert(findaction("New").create_tool_item(), -1)
357         bar.insert(findaction("Open").create_tool_item(), -1)
358         bar.insert(findaction("Save").create_tool_item(), -1)
359         bar.insert(findaction("Undo").create_tool_item(), -1)
360         bar.insert(findaction("Redo").create_tool_item(), -1)
361
362         bar.insert(gtk.SeparatorToolItem(), -1)
363         bar.insert(findaction("ResetView").create_tool_item(), -1)
364         bar.insert(findaction("ZoomIn").create_tool_item(), -1)
365         bar.insert(findaction("ZoomOut").create_tool_item(), -1)
366         bar.insert(findaction("RotateLeft").create_tool_item(), -1)
367         bar.insert(findaction("RotateRight").create_tool_item(), -1)
368         bar.insert(findaction("MirrorVertical").create_tool_item(), -1)
369         bar.insert(findaction("MirrorHorizontal").create_tool_item(), -1)
370
371         bar.insert(gtk.SeparatorToolItem(), -1)
372         bar.insert(findaction("BlendModeNormal").create_tool_item(), -1)
373         bar.insert(findaction("BlendModeEraser").create_tool_item(), -1)
374         bar.insert(findaction("BlendModeLockAlpha").create_tool_item(), -1)
375
376         expander = gtk.SeparatorToolItem()
377         expander.set_expand(True)
378         expander.set_draw(False)
379         bar.insert(expander, -1)
380
381         for name in ["ColorSelectionWindow", "ColorSamplerWindow",
382                      "BrushSelectionWindow", "LayersWindow"]:
383             action = self.action_group.get_action(name)
384             tool_item = action.create_tool_item()
385             bar.insert(tool_item, -1)
386         self.toolbar = bar
387
388         if not self.get_show_toolbar():
389             gobject.idle_add(self.toolbar.hide)
390
391
392     def _init_popupmenu(self, xml):
393         """
394         Hopefully temporary hack for converting UIManager XML describing the
395         main menubar into a rebindable popup menu. UIManager by itself doesn't
396         let you do this, by design, but we need a bigger menu than the little
397         things it allows you to build.
398         """
399         ui_elt = ET.fromstring(xml)
400         rootmenu_elt = ui_elt.find("menubar")
401         rootmenu_elt.attrib["name"] = "PopupMenu"
402         ## XML-style menu jiggling. No need for this really though.
403         #for menu_elt in rootmenu_elt.findall("menu"):
404         #    for item_elt in menu_elt.findall("menuitem"):
405         #        if item_elt.attrib.get("action", "") == "ShowPopupMenu":
406         #            menu_elt.remove(item_elt)
407         ## Maybe shift a small number of frequently-used items to the top?
408         xml = ET.tostring(ui_elt)
409         self.app.ui_manager.add_ui_from_string(xml)
410         tmp_menubar = self.app.ui_manager.get_widget('/PopupMenu')
411         self.popupmenu = gtk.Menu()
412         for item in tmp_menubar.get_children():
413             tmp_menubar.remove(item)
414             self.popupmenu.append(item)
415         self.popupmenu.attach_to_widget(self.app.doc.tdw, None)
416         #self.popupmenu.set_title("MyPaint")
417         #self.popupmenu.set_take_focus(True)
418         self.popupmenu.connect("selection-done", self.popupmenu_done_cb)
419         self.popupmenu.connect("deactivate", self.popupmenu_done_cb)
420         self.popupmenu.connect("cancel", self.popupmenu_done_cb)
421         self.popupmenu_last_active = None
422
423
424     def update_title(self, filename):
425         if filename:
426             self.set_title("MyPaint - %s" % os.path.basename(filename))
427         else:
428             self.set_title("MyPaint")
429
430     # INPUT EVENT HANDLING
431     def drag_data_received(self, widget, context, x, y, selection, info, t):
432         if info == 1:
433             if selection.data:
434                 uri = selection.data.split("\r\n")[0]
435                 fn = helpers.uri2filename(uri)
436                 if os.path.exists(fn):
437                     if self.app.filehandler.confirm_destructive_action():
438                         self.app.filehandler.open_file(fn)
439         elif info == 2: # color
440             color = [((ord(selection.data[v]) | (ord(selection.data[v+1]) << 8)) / 65535.0)  for v in range(0,8,2)]
441             self.app.brush.set_color_rgb(color[:3])
442             self.app.ch.push_color(self.app.brush.get_color_hsv())
443             # Don't popup the color history for now, as I haven't managed to get it to cooperate.
444
445     def print_memory_leak_cb(self, action):
446         helpers.record_memory_leak_status(print_diff = True)
447
448     def run_garbage_collector_cb(self, action):
449         helpers.run_garbage_collector()
450
451     def start_profiling_cb(self, action):
452         if getattr(self, 'profiler_active', False):
453             self.profiler_active = False
454             return
455
456         def doit():
457             import cProfile
458             profile = cProfile.Profile()
459
460             self.profiler_active = True
461             print '--- GUI Profiling starts ---'
462             while self.profiler_active:
463                 profile.runcall(gtk.main_iteration, False)
464                 if not gtk.events_pending():
465                     time.sleep(0.050) # ugly trick to remove "user does nothing" from profile
466             print '--- GUI Profiling ends ---'
467
468             profile.dump_stats('profile_fromgui.pstats')
469             #print 'profile written to mypaint_profile.pstats'
470             os.system('gprof2dot.py -f pstats profile_fromgui.pstats | dot -Tpng -o profile_fromgui.png && feh profile_fromgui.png &')
471
472         gobject.idle_add(doit)
473
474     def gtk_input_dialog_cb(self, action):
475         d = gtk.InputDialog()
476         d.show()
477
478     def key_press_event_cb_before(self, win, event):
479         key = event.keyval
480         ctrl = event.state & gdk.CONTROL_MASK
481         shift = event.state & gdk.SHIFT_MASK
482         alt = event.state & gdk.MOD1_MASK
483         #ANY_MODIFIER = gdk.SHIFT_MASK | gdk.MOD1_MASK | gdk.CONTROL_MASK
484         #if event.state & ANY_MODIFIER:
485         #    # allow user shortcuts with modifiers
486         #    return False
487
488         # This may need a stateful flag
489         if self.app.filehandler.scratchpad_doc.tdw.has_pointer:
490             thisdoc = self.app.filehandler.scratchpad_doc
491             # Stop dragging on the main window
492             self.app.doc.tdw.dragfunc = None
493         else:
494             thisdoc = self.app.doc
495             # Stop dragging on the other window
496             self.app.filehandler.scratchpad_doc.tdw.dragfunc = None
497         if key == keysyms.space:
498             if shift:
499                  thisdoc.tdw.start_drag(thisdoc.dragfunc_rotate)
500             elif ctrl:
501                 thisdoc.tdw.start_drag(thisdoc.dragfunc_zoom)
502             elif alt:
503                 thisdoc.tdw.start_drag(thisdoc.dragfunc_frame)
504             else:
505                 thisdoc.tdw.start_drag(thisdoc.dragfunc_translate)
506         else: return False
507         return True
508
509     def key_release_event_cb_before(self, win, event):
510         if self.app.filehandler.scratchpad_doc.tdw.has_pointer:
511             thisdoc = self.app.filehandler.scratchpad_doc
512         else:
513             thisdoc = self.app.doc
514         if event.keyval == keysyms.space:
515             thisdoc.tdw.stop_drag(thisdoc.dragfunc_translate)
516             thisdoc.tdw.stop_drag(thisdoc.dragfunc_rotate)
517             thisdoc.tdw.stop_drag(thisdoc.dragfunc_zoom)
518             thisdoc.tdw.stop_drag(thisdoc.dragfunc_frame)
519             return True
520         return False
521
522     def key_press_event_cb_after(self, win, event):
523         key = event.keyval
524         if self.is_fullscreen and key == keysyms.Escape:
525             self.fullscreen_cb()
526         else:
527             return False
528         return True
529
530     def key_release_event_cb_after(self, win, event):
531         return False
532
533     def button_press_cb(self, win, event):
534         return button_press_cb_abstraction(self, win, event, self.app.doc)
535
536     def button_release_cb(self, win, event):
537         return button_release_cb_abstraction(win, event, self.app.doc)
538
539     def scroll_cb(self, win, event):
540         d = event.direction
541         if d == gdk.SCROLL_UP:
542             if event.state & gdk.SHIFT_MASK:
543                 self.app.doc.rotate('RotateLeft')
544             else:
545                 self.app.doc.zoom('ZoomIn')
546         elif d == gdk.SCROLL_DOWN:
547             if event.state & gdk.SHIFT_MASK:
548                 self.app.doc.rotate('RotateRight')
549             else:
550                 self.app.doc.zoom('ZoomOut')
551         elif d == gdk.SCROLL_RIGHT:
552             self.app.doc.rotate('RotateRight')
553         elif d == gdk.SCROLL_LEFT:
554             self.app.doc.rotate('RotateLeft')
555
556     # WINDOW HANDLING
557     def toggle_window_cb(self, action):
558         if self._updating_toggled_item:
559             return
560         s = action.get_name()
561         active = action.get_active()
562         window_name = s[0].lower() + s[1:] # WindowName -> windowName
563         # If it's a tool, get it to hide/show itself
564         t = self.app.layout_manager.get_tool_by_role(window_name)
565         if t is not None:
566             t.set_hidden(not active)
567             return
568         # Otherwise, if it's a regular subwindow hide/show+present it.
569         w = self.app.layout_manager.get_subwindow_by_role(window_name)
570         if w is None:
571             return
572         onscreen = w.window is not None and w.window.is_visible()
573         if active:
574             if onscreen:
575                 return
576             w.show_all()
577             w.present()
578         else:
579             if not onscreen:
580                 return
581             w.hide()
582
583     def update_subwindow_visibility(self, window, active):
584         # Responds to non-tool subwindows being hidden and shown
585         role = window.get_role()
586         self.update_toggled_item_visibility(role, active)
587
588     def update_toggled_item_visibility(self, role, active, *a, **kw):
589         # Responds to any item with a role being hidden or shown by
590         # silently updating its ToggleAction to match.
591         action_name = role[0].upper() + role[1:]
592         action = self.action_group.get_action(action_name)
593         if action is None:
594             warn("Unable to find action %s" % action_name, RuntimeWarning, 1)
595             return
596         if action.get_active() != active:
597             self._updating_toggled_item = True
598             action.set_active(active)
599             self._updating_toggled_item = False
600
601     def popup_cb(self, action):
602         state = self.popup_states[action.get_name()]
603         state.activate(action)
604
605
606     # Show Toolbar
607     # Saved in the user prefs between sessions.
608     # Controlled via its ToggleAction only.
609
610     def set_show_toolbar(self, show_toolbar):
611         """Programatically set the Show Toolbar option.
612         """
613         action = self.action_group.get_action("ToggleToolbar")
614         if show_toolbar:
615             if not action.get_active():
616                 action.set_active(True)
617             self.app.preferences["ui.toolbar"] = True
618         else:
619             if action.get_active():
620                 action.set_active(False)
621             self.app.preferences["ui.toolbar"] = False
622
623     def get_show_toolbar(self):
624         return self.app.preferences.get("ui.toolbar", True)
625
626     def toggle_toolbar_cb(self, action):
627         active = action.get_active()
628         if active:
629             self.toolbar.show_all()
630         else:
631             self.toolbar.hide()
632         self.app.preferences["ui.toolbar"] = active
633
634
635     # Show Subwindows
636     # Not saved between sessions, defaults to on.
637     # Controlled via its ToggleAction, and entering or leaving fullscreen mode
638     # according to the setting of ui.hide_in_fullscreen in prefs.
639
640     def set_show_subwindows(self, show_subwindows):
641         """Programatically set the Show Subwindows option.
642         """
643         action = self.action_group.get_action("ToggleSubwindows")
644         currently_showing = action.get_active()
645         if show_subwindows != currently_showing:
646             action.set_active(show_subwindows)
647         self._show_subwindows = self._show_subwindows
648
649     def get_show_subwindows(self):
650         return self._show_subwindows
651
652     def toggle_subwindows_cb(self, action):
653         active = action.get_active()
654         lm = self.app.layout_manager
655         if active:
656             lm.toggle_user_tools(on=True)
657         else:
658             lm.toggle_user_tools(on=False)
659         self._show_subwindows = active
660
661
662     # Fullscreen mode
663     # This implementation requires an ICCCM and EWMH-compliant window manager
664     # which supports the _NET_WM_STATE_FULLSCREEN hint. There are several
665     # available.
666
667     def fullscreen_cb(self, *junk):
668         if not self.is_fullscreen:
669             self.fullscreen()
670         else:
671             self.unfullscreen()
672
673     def window_state_event_cb(self, widget, event):
674         # Respond to changes of the fullscreen state only
675         if not event.changed_mask & gdk.WINDOW_STATE_FULLSCREEN:
676             return
677         lm = self.app.layout_manager
678         self.is_fullscreen = event.new_window_state & gdk.WINDOW_STATE_FULLSCREEN
679         if self.is_fullscreen:
680             # Subwindow hiding 
681             if self.app.preferences.get("ui.hide_subwindows_in_fullscreen", True):
682                 self.set_show_subwindows(False)
683                 self._restore_subwindows_on_unfullscreen = True
684             if self.app.preferences.get("ui.hide_menubar_in_fullscreen", True):
685                 self.menubar.hide()
686                 self._restore_menubar_on_unfullscreen = True
687             if self.app.preferences.get("ui.hide_toolbar_in_fullscreen", True):
688                 self.toolbar.hide()
689                 self._restore_toolbar_on_unfullscreen = True
690             # fix for fullscreen problem on Windows, https://gna.org/bugs/?15175
691             # on X11/Metacity it also helps a bit against flickering during the switch
692             while gtk.events_pending():
693                 gtk.main_iteration()
694         else:
695             while gtk.events_pending():
696                 gtk.main_iteration()
697             if getattr(self, "_restore_menubar_on_unfullscreen", False):
698                 self.menubar.show()
699                 del self._restore_menubar_on_unfullscreen
700             if getattr(self, "_restore_toolbar_on_unfullscreen", False):
701                 if self.get_show_toolbar():
702                     self.toolbar.show()
703                 del self._restore_toolbar_on_unfullscreen
704             if getattr(self, "_restore_subwindows_on_unfullscreen", False):
705                 self.set_show_subwindows(True)
706                 del self._restore_subwindows_on_unfullscreen
707
708     def popupmenu_show_cb(self, action):
709         self.show_popupmenu()
710
711     def show_popupmenu(self, event=None):
712         self.menubar.set_sensitive(False)   # excessive feedback?
713         button = 1
714         time = 0
715         if event is not None:
716             if event.type == gdk.BUTTON_PRESS:
717                 button = event.button
718                 time = event.time
719         self.popupmenu.popup(None, None, None, button, time)
720         if event is None:
721             # We're responding to an Action, most probably the menu key.
722             # Open out the last highlighted menu to speed key navigation up.
723             if self.popupmenu_last_active is None:
724                 self.popupmenu.select_first(True) # one less keypress
725             else:
726                 self.popupmenu.select_item(self.popupmenu_last_active)
727
728     def popupmenu_done_cb(self, *a, **kw):
729         # Not sure if we need to bother with this level of feedback,
730         # but it actaully looks quite nice to see one menu taking over
731         # the other. Makes it clear that the popups are the same thing as
732         # the full menu, maybe.
733         self.menubar.set_sensitive(True)
734         self.popupmenu_last_active = self.popupmenu.get_active()
735
736     def toggle_subwindows_cb(self, action):
737         self.app.layout_manager.toggle_user_tools()
738         if self.app.layout_manager.saved_user_tools:
739             if self.is_fullscreen:
740                 self.menubar.hide()
741         else:
742             if not self.is_fullscreen:
743                 self.menubar.show()
744
745     def save_scratchpad_as_default_cb(self, action):
746         self.app.filehandler.save_scratchpad(self.app.filehandler.get_scratchpad_default(), export = True)
747     
748     def clear_default_scratchpad_cb(self, action):
749         self.app.filehandler.delete_default_scratchpad()
750
751     def clear_autosave_scratchpad_cb(self, action):
752         self.app.filehandler.delete_autosave_scratchpad()
753
754     def save_current_scratchpad_cb(self, action):
755         self.app.filehandler.save_scratchpad(self.app.filehandler.scratchpad_filename)
756
757     def draw_palette_cb(self, action):
758         # test functionality:
759         file_filters = [
760         (_("Gimp Palette Format"), ("*.gpl",)),
761         (_("All Files"), ("*.*",)),
762         ]
763         gimp_path = os.path.join(self.app.filehandler.get_gimp_prefix(), "palettes")
764         dialog = self.app.filehandler.get_open_dialog(start_in_folder=gimp_path,
765                                                   file_filters = file_filters)
766         try:
767             if dialog.run() == gtk.RESPONSE_OK:
768                 dialog.hide()
769                 filename = dialog.get_filename().decode('utf-8')
770                 if filename:
771                     #filename = "/home/ben/.gimp-2.6/palettes/Nature_Grass.gpl" # TEMP HACK TO TEST
772                     g = GimpPalette(filename)
773                     grid_size = 30.0
774                     off_x = off_y = grid_size / 2.0
775                     column_limit = 7
776                     if g.columns != 0:
777                         column_limit = g.columns   # use the value for columns in the palette
778                     for colour_idx in xrange(len(g)):
779                         gen_events = squiggle(off_x, off_y, scale=13.0)
780                         # Set the color
781                         self.app.brush.set_color_rgb(g.rgb(colour_idx))
782                         # simulate strokes on scratchpad
783                         for t, x, y, pressure in gen_events:
784                             cr = self.app.filehandler.scratchpad_doc.tdw.get_model_coordinates_cairo_context()
785                             x, y = cr.device_to_user(x, y)
786                             self.app.filehandler.scratchpad_doc.model.stroke_to(0.008, x, y, pressure, 0.0, 0.0)
787                         self.app.filehandler.scratchpad_doc.model.split_stroke()
788                         off_x = ((colour_idx % column_limit) + 0.5) * grid_size
789                         if not (colour_idx % column_limit) and colour_idx:
790                             off_y += grid_size
791         finally:
792             dialog.destroy()
793
794     def quit_cb(self, *junk):
795         self.app.doc.model.split_stroke()
796         self.app.save_gui_config() # FIXME: should do this periodically, not only on quit
797
798         if not self.app.filehandler.confirm_destructive_action(title=_('Quit'), question=_('Really Quit?')):
799             return True
800
801         gtk.main_quit()
802         return False
803
804     def toggle_frame_cb(self, action):
805         enabled = self.app.doc.model.frame_enabled
806         self.app.doc.model.set_frame_enabled(not enabled)
807
808     def import_brush_pack_cb(self, *junk):
809         format_id, filename = dialogs.open_dialog(_("Import brush package..."), self,
810                                  [(_("MyPaint brush package (*.zip)"), "*.zip")])
811         if filename:
812             self.app.brushmanager.import_brushpack(filename,  self)
813
814     # INFORMATION
815     # TODO: Move into dialogs.py?
816     def about_cb(self, action):
817         d = gtk.AboutDialog()
818         d.set_transient_for(self)
819         d.set_program_name("MyPaint")
820         d.set_version(MYPAINT_VERSION)
821         d.set_copyright(_("Copyright (C) 2005-2010\nMartin Renold and the MyPaint Development Team"))
822         d.set_website("http://mypaint.info/")
823         d.set_logo(self.app.pixmaps.mypaint_logo)
824         d.set_license(
825             _(u"This program is free software; you can redistribute it and/or modify "
826               u"it under the terms of the GNU General Public License as published by "
827               u"the Free Software Foundation; either version 2 of the License, or "
828               u"(at your option) any later version.\n"
829               u"\n"
830               u"This program is distributed in the hope that it will be useful, "
831               u"but WITHOUT ANY WARRANTY. See the COPYING file for more details.")
832             )
833         d.set_wrap_license(True)
834         d.set_authors([
835             # (in order of appearance)
836             u"Martin Renold (%s)" % _('programming'),
837             u"Artis Rozentāls (%s)" % _('brushes'),
838             u"Yves Combe (%s)" % _('portability'),
839             u"Popolon (%s)" % _('brushes, programming'),
840             u"Clement Skau (%s)" % _('programming'),
841             u"Marcelo 'Tanda' Cerviño (%s)" % _('patterns, brushes'),
842             u"Jon Nordby (%s)" % _('programming'),
843             u"Álinson Santos (%s)" % _('programming'),
844             u"Tumagonx (%s)" % _('portability'),
845             u"Ilya Portnov (%s)" % _('programming'),
846             u"David Revoy (%s)" % _('brushes'),
847             u"Ramón Miranda (%s)" % _('brushes, patterns'),
848             u"Enrico Guarnieri 'Ico_dY' (%s)" % _('brushes'),
849             u"Jonas Wagner (%s)" % _('programming'),
850             u"Luka Čehovin (%s)" % _('programming'),
851             u"Andrew Chadwick (%s)" % _('programming'),
852             u"Till Hartmann (%s)" % _('programming'),
853             u"Nicola Lunghi (%s)" % _('patterns'),
854             u"Toni Kasurinen (%s)" % _('brushes'),
855             u"Сан Саныч (%s)" % _('patterns'),
856             u'David Grundberg (%s)' % _('programming'),
857             u"Krzysztof Pasek (%s)" % _('programming'),
858             ])
859         d.set_artists([
860             u'Sebastian Kraft (%s)' % _('desktop icon'),
861             ])
862         # list all translators, not only those of the current language
863         d.set_translator_credits(
864             u'Ilya Portnov (ru)\n'
865             u'Popolon (fr, zh_CN, ja)\n'
866             u'Jon Nordby (nb)\n'
867             u'Griatch (sv)\n'
868             u'Tobias Jakobs (de)\n'
869             u'Martin Tabačan (cs)\n'
870             u'Tumagonx (id)\n'
871             u'Manuel Quiñones (es)\n'
872             u'Gergely Aradszki (hu)\n'
873             u'Lamberto Tedaldi (it)\n'
874             u'Dong-Jun Wu (zh_TW)\n'
875             u'Luka Čehovin (sl)\n'
876             u'Geuntak Jeong (ko)\n'
877             u'Łukasz Lubojański (pl)\n'
878             u'Daniel Korostil (uk)\n'
879             u'Julian Aloofi (de)\n'
880             u'Tor Egil Hoftun Kvæstad (nn_NO)\n'
881             u'João S. O. Bueno (pt_BR)\n'
882             u'David Grundberg (sv)\n'
883             u'Elliott Sales de Andrade (en_CA)\n'
884             )
885
886         d.run()
887         d.destroy()
888
889     def show_infodialog_cb(self, action):
890         text = {
891         'ShortcutHelp':
892                 _("Move your mouse over a menu entry, then press the key to assign."),
893         'ViewHelp':
894                 _("You can also drag the canvas with the mouse while holding the middle "
895                 "mouse button or spacebar. Or with the arrow keys."
896                 "\n\n"
897                 "In contrast to earlier versions, scrolling and zooming are harmless now and "
898                 "will not make you run out of memory. But you still require a lot of memory "
899                 "if you paint all over while fully zoomed out."),
900         'ContextHelp':
901                 _("Brushkeys are used to quickly save/restore brush settings "
902                  "using keyboard shortcuts. You can paint with one hand and "
903                  "change brushes with the other without interruption."
904                  "\n\n"
905                  "There are 10 memory slots to hold brush settings.\n"
906                  "They are anonymous brushes, which are not visible in the "
907                  "brush selector list. But they are remembered even if you "
908                  "quit."),
909         'Docu':
910                 _("There is a tutorial available on the MyPaint homepage. It "
911                  "explains some features which are hard to discover yourself."
912                  "\n\n"
913                  "Comments about the brush settings (opaque, hardness, etc.) and "
914                  "inputs (pressure, speed, etc.) are available as tooltips. "
915                  "Put your mouse over a label to see them. "
916                  "\n"),
917         }
918         self.app.message_dialog(text[action.get_name()])