OSDN Git Service

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