OSDN Git Service

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