OSDN Git Service

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