FrameToggle: update in line with frame window
[mypaint:mypaint.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="1.0.0+git"
17
18 import os, time, webbrowser
19 from warnings import warn
20 from gettext import gettext as _
21
22 import gtk, gobject
23 from gtk import gdk, keysyms
24
25 import colorselectionwindow, historypopup, stategroup, colorpicker, windowing, layout, toolbar
26 import dialogs
27 from lib import helpers
28 import canvasevent
29 from colors import RGBColor
30
31 import pygtkcompat
32 import xml.etree.ElementTree as ET
33
34 # palette support
35 from lib.scratchpad_palette import GimpPalette, hatch_squiggle, squiggle, draw_palette
36
37 from overlays import LastPaintPosOverlay, ScaleOverlay
38 from symmetry import SymmetryOverlay
39
40
41
42 # TODO: put in a helper file?
43 def with_wait_cursor(func):
44     """python decorator that adds a wait cursor around a function"""
45     def wrapper(self, *args, **kwargs):
46         if pygtkcompat.USE_GTK3:
47             toplevels = gtk.Window.list_toplevels()
48         else:
49             toplevels = gtk.window_list_toplevels()
50         toplevels = [t for t in toplevels if t.get_window() is not None]
51         for toplevel in toplevels:
52             toplevel_win = toplevel.get_window()
53             if toplevel_win is not None:
54                 toplevel_win.set_cursor(gdk.Cursor(gdk.WATCH))
55             toplevel.set_sensitive(False)
56         self.app.doc.tdw.grab_add()
57         try:
58             func(self, *args, **kwargs)
59             # gtk main loop may be called in here...
60         finally:
61             for toplevel in toplevels:
62                 toplevel.set_sensitive(True)
63                 # ... which is why we need this check:
64                 toplevel_win = toplevel.get_window()
65                 if toplevel_win is not None:
66                     toplevel_win.set_cursor(None)
67             self.app.doc.tdw.grab_remove()
68     return wrapper
69
70
71 class Window (windowing.MainWindow, layout.MainWindow):
72     """Main drawing window.
73     """
74
75     MENUISHBAR_RADIO_MENUBAR = 1
76     MENUISHBAR_RADIO_MAIN_TOOLBAR = 2
77     MENUISHBAR_RADIO_BOTH_BARS = 3
78
79     def __init__(self, app):
80         windowing.MainWindow.__init__(self, app)
81         self.app = app
82
83         # Window handling
84         self._updating_toggled_item = False
85         self._show_subwindows = True
86         self.is_fullscreen = False
87
88         # Enable drag & drop
89         if not pygtkcompat.USE_GTK3:
90             self.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
91                             gtk.DEST_DEFAULT_HIGHLIGHT |
92                             gtk.DEST_DEFAULT_DROP,
93                             [("text/uri-list", 0, 1),
94                              ("application/x-color", 0, 2)],
95                             gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY)
96
97         # Connect events
98         self.connect('delete-event', self.quit_cb)
99         self.connect('key-press-event', self.key_press_event_cb_before)
100         self.connect('key-release-event', self.key_release_event_cb_before)
101         self.connect_after('key-press-event', self.key_press_event_cb_after)
102         self.connect_after('key-release-event', self.key_release_event_cb_after)
103         self.connect("drag-data-received", self.drag_data_received)
104         self.connect("window-state-event", self.window_state_event_cb)
105
106         self.update_overlays()
107
108         self.app.filehandler.current_file_observers.append(self.update_title)
109
110         self.init_actions()
111
112         lm = app.layout_manager
113         layout.MainWindow.__init__(self, lm)
114
115         # Park the focus on the main tdw rather than on the toolbar. Default
116         # activation doesn't really mean much for MyPaint's main window, so
117         # it's safe to do this and it looks better.
118         self.main_widget.set_can_default(True)
119         self.main_widget.set_can_focus(True)
120         self.main_widget.grab_focus()
121
122         kbm = self.app.kbm
123         kbm.add_extra_key('Menu', 'ShowPopupMenu')
124         kbm.add_extra_key('Tab', 'ToggleSubwindows')
125
126         self.init_stategroups()
127
128     def get_doc(self):
129         print "DeprecationWarning: Use app.doc instead"
130         return self.app.doc
131
132     def get_tdw(self):
133         print "DeprecationWarning: Use app.doc.tdw instead"
134         return self.app.doc.tdw
135
136     tdw, doc = property(get_tdw), property(get_doc)
137
138
139     def init_actions(self):
140         # Actions are defined in mypaint.xml: all we need to do here is connect
141         # some extra state management.
142
143         ag = self.action_group = self.app.builder.get_object("WindowActions")
144         self.update_fullscreen_action()
145
146         # Reflect changes from other places (like tools' close buttons) into
147         # the proxys' visible states.
148         lm = self.app.layout_manager
149         lm.tool_visibility_observers.append(self.update_toggled_item_visibility)
150         lm.subwindow_visibility_observers.append(self.update_subwindow_visibility)
151
152         # Initial toggle state
153         for action in ag.list_actions():
154             name = action.get_property("name")
155             if isinstance(action, gtk.ToggleAction):
156                 if name.endswith("Window"):
157                     role = name[0].lower() + name[1:]
158                     visible = not lm.get_window_hidden_by_role(role)
159                     # The sidebar machinery won't be up yet, so reveal windows
160                     # that should be initially visible only in an idle handler
161                     gobject.idle_add(action.set_active, visible)
162
163         # Initial state defined in the XML file
164         self._show_subwindows = bool(ag.get_action("ToggleSubwindows")
165                                                   .get_active())
166
167         # Set initial state from user prefs
168         ag.get_action("ToggleScaleFeedback").set_active(
169                 self.app.preferences.get("ui.feedback.scale", False))
170         ag.get_action("ToggleLastPosFeedback").set_active(
171                 self.app.preferences.get("ui.feedback.last_pos", False))
172         ag.get_action("ToggleSymmetryFeedback").set_active(
173                 self.app.preferences.get("ui.feedback.symmetry", False))
174
175         # Follow frame toggled state
176         self.app.doc.model.frame_observers.append(self.frame_changed_cb)
177
178         # Set initial states of radio actions
179         menuishbar_state = 0
180         if self.get_ui_part_enabled("menubar"):
181             menuishbar_state += self.MENUISHBAR_RADIO_MENUBAR
182         if self.get_ui_part_enabled("main_toolbar"):
183             menuishbar_state += self.MENUISHBAR_RADIO_MAIN_TOOLBAR
184         if menuishbar_state == 0:
185             menuishbar_state = self.MENUISHBAR_RADIO_MAIN_TOOLBAR
186
187         ag.get_action("MenuishBarRadioMenubar").set_current_value(menuishbar_state)
188         gobject.idle_add(lambda: self.update_ui_parts())
189
190         # Keyboard handling
191         for action in self.action_group.list_actions():
192             self.app.kbm.takeover_action(action)
193
194     def init_stategroups(self):
195         sg = stategroup.StateGroup()
196         p2s = sg.create_popup_state
197         changer_crossed_bowl = p2s(colorselectionwindow.ColorChangerCrossedBowlPopup(self.app))
198         changer_wash = p2s(colorselectionwindow.ColorChangerWashPopup(self.app))
199         ring = p2s(colorselectionwindow.ColorRingPopup(self.app))
200         hist = p2s(historypopup.HistoryPopup(self.app, self.app.doc.model))
201         pick = self.colorpick_state = p2s(colorpicker.ColorPicker(self.app, self.app.doc.model))
202
203         self.popup_states = {
204             'ColorChangerCrossedBowlPopup': changer_crossed_bowl,
205             'ColorChangerWashPopup': changer_wash,
206             'ColorRingPopup': ring,
207             'ColorHistoryPopup': hist,
208             'ColorPickerPopup': pick,
209             }
210
211         # not sure how useful this is; we can't cycle at the moment
212         changer_crossed_bowl.next_state = ring
213         ring.next_state = changer_wash
214         changer_wash.next_state = ring
215
216         changer_wash.autoleave_timeout = None
217         changer_crossed_bowl.autoleave_timeout = None
218         ring.autoleave_timeout = None
219
220         pick.max_key_hit_duration = 0.0
221         pick.autoleave_timeout = None
222
223         hist.autoleave_timeout = 0.600
224         self.history_popup_state = hist
225
226         for action_name, popup_state in self.popup_states.iteritems():
227             label = self.app.find_action(action_name).get_label()
228             popup_state.label = label
229
230     def init_main_widget(self):  # override
231         self.main_widget = self.app.doc.tdw
232
233     def init_menubar(self):   # override
234         # Load Menubar, duplicate into self.popupmenu
235         menupath = os.path.join(self.app.datapath, 'gui/menu.xml')
236         menubar_xml = open(menupath).read()
237         self.app.ui_manager.add_ui_from_string(menubar_xml)
238         self.popupmenu = self._clone_menu(menubar_xml, 'PopupMenu', self.app.doc.tdw)
239         self.menubar = self.app.ui_manager.get_widget('/Menubar')
240
241     def init_toolbar(self):
242         self.toolbar_manager = toolbar.ToolbarManager(self)
243         self.toolbar = self.toolbar_manager.toolbar1
244
245     def _clone_menu(self, xml, name, owner=None):
246         """
247         Hopefully temporary hack for converting UIManager XML describing the
248         main menubar into a rebindable popup menu. UIManager by itself doesn't
249         let you do this, by design, but we need a bigger menu than the little
250         things it allows you to build.
251         """
252         ui_elt = ET.fromstring(xml)
253         rootmenu_elt = ui_elt.find("menubar")
254         rootmenu_elt.attrib["name"] = name
255         xml = ET.tostring(ui_elt)
256         self.app.ui_manager.add_ui_from_string(xml)
257         tmp_menubar = self.app.ui_manager.get_widget('/' + name)
258         popupmenu = gtk.Menu()
259         for item in tmp_menubar.get_children():
260             tmp_menubar.remove(item)
261             popupmenu.append(item)
262         if owner is not None:
263             popupmenu.attach_to_widget(owner, None)
264         popupmenu.set_title("MyPaint")
265         popupmenu.connect("selection-done", self.popupmenu_done_cb)
266         popupmenu.connect("deactivate", self.popupmenu_done_cb)
267         popupmenu.connect("cancel", self.popupmenu_done_cb)
268         self.popupmenu_last_active = None
269         return popupmenu
270
271
272     def update_title(self, filename):
273         if filename:
274             self.set_title("MyPaint - %s" % os.path.basename(filename))
275         else:
276             self.set_title("MyPaint")
277
278     # INPUT EVENT HANDLING
279     def drag_data_received(self, widget, context, x, y, selection, info, t):
280         if info == 1:
281             if selection.data:
282                 uri = selection.data.split("\r\n")[0]
283                 fn = helpers.uri2filename(uri)
284                 if os.path.exists(fn):
285                     if self.app.filehandler.confirm_destructive_action():
286                         self.app.filehandler.open_file(fn)
287         elif info == 2: # color
288             color = RGBColor.new_from_drag_data(selection.data)
289             self.app.brush_color_manager.set_color(color)
290             self.app.brush_color_manager.push_history(color)
291             # Don't popup the color history for now, as I haven't managed
292             # to get it to cooperate.
293
294     def print_memory_leak_cb(self, action):
295         helpers.record_memory_leak_status(print_diff = True)
296
297     def run_garbage_collector_cb(self, action):
298         helpers.run_garbage_collector()
299
300     def start_profiling_cb(self, action):
301         if getattr(self, 'profiler_active', False):
302             self.profiler_active = False
303             return
304
305         def doit():
306             import cProfile
307             profile = cProfile.Profile()
308
309             self.profiler_active = True
310             print '--- GUI Profiling starts ---'
311             while self.profiler_active:
312                 profile.runcall(gtk.main_iteration, False)
313                 if not gtk.events_pending():
314                     time.sleep(0.050) # ugly trick to remove "user does nothing" from profile
315             print '--- GUI Profiling ends ---'
316
317             profile.dump_stats('profile_fromgui.pstats')
318             #print 'profile written to mypaint_profile.pstats'
319             os.system('gprof2dot.py -f pstats profile_fromgui.pstats | dot -Tpng -o profile_fromgui.png && feh profile_fromgui.png &')
320
321         gobject.idle_add(doit)
322
323     def gtk_input_dialog_cb(self, action):
324         d = gtk.InputDialog()
325         d.show()
326
327     def _get_active_doc(self):
328         # Determines which is the active doc for the purposes of keyboard
329         # event dispatch.
330         tdw_class = self.app.scratchpad_doc.tdw.__class__
331         tdw = tdw_class.get_active_tdw()
332         if tdw is not None:
333             if tdw is self.app.scratchpad_doc.tdw:
334                 return (self.app.scratchpad_doc, tdw)
335             elif tdw is self.app.doc.tdw:
336                 return (self.app.doc, tdw)
337         return (None, None)
338
339     def key_press_event_cb_before(self, win, event):
340         # Process keyboard events
341         target_doc, target_tdw = self._get_active_doc()
342         if target_doc is None:
343             return False
344         # Forward the keypress to the active doc's active InteractionMode.
345         return target_doc.modes.top.key_press_cb(win, target_tdw, event)
346
347
348     def key_release_event_cb_before(self, win, event):
349         # Process key-release events
350         target_doc, target_tdw = self._get_active_doc()
351         if target_doc is None:
352             return False
353         # Forward the event (see above)
354         return target_doc.modes.top.key_release_cb(win, target_tdw, event)
355
356
357     # XXX these can probably stay here for now.
358
359     def key_press_event_cb_after(self, win, event):
360         key = event.keyval
361         if self.is_fullscreen and key == keysyms.Escape:
362             self.fullscreen_cb()
363         else:
364             return False
365         return True
366
367     def key_release_event_cb_after(self, win, event):
368         return False
369
370
371     # WINDOW HANDLING
372     def toggle_window_cb(self, action):
373         if self._updating_toggled_item:
374             return
375         s = action.get_name()
376         active = action.get_active()
377         window_name = s[0].lower() + s[1:] # WindowName -> windowName
378         # If it's a tool, get it to hide/show itself
379         t = self.app.layout_manager.get_tool_by_role(window_name)
380         if t is not None:
381             t.set_hidden(not active)
382             return
383         # Otherwise, if it's a regular subwindow hide/show+present it.
384         w = self.app.layout_manager.get_subwindow_by_role(window_name)
385         if w is None:
386             return
387         gdk_window = w.get_window()
388         onscreen = gdk_window is not None and gdk_window.is_visible()
389         if active:
390             if onscreen:
391                 return
392             w.show_all()
393             w.present()
394         else:
395             if not onscreen:
396                 return
397             w.hide()
398
399     def update_subwindow_visibility(self, window, active):
400         # Responds to non-tool subwindows being hidden and shown
401         role = window.get_role()
402         self.update_toggled_item_visibility(role, active)
403
404     def update_toggled_item_visibility(self, role, active, *a, **kw):
405         # Responds to any item with a role being hidden or shown by
406         # silently updating its ToggleAction to match.
407         action_name = role[0].upper() + role[1:]
408         action = self.action_group.get_action(action_name)
409         if action is None:
410             warn("Unable to find action %s" % action_name, RuntimeWarning, 1)
411             return
412         if action.get_active() != active:
413             self._updating_toggled_item = True
414             action.set_active(active)
415             self._updating_toggled_item = False
416
417
418     # Feedback and overlays
419     # It's not intended that all categories of feedback will use overlays, but
420     # they currently all do. This may change if/when we add a conventional
421     # statusbar for textual types of feedback.
422
423     def toggle_scale_feedback_cb(self, action):
424         self.app.preferences['ui.feedback.scale'] = action.get_active()
425         self.update_overlays()
426
427     def toggle_last_pos_feedback_cb(self, action):
428         self.app.preferences['ui.feedback.last_pos'] = action.get_active()
429         self.update_overlays()
430
431     def toggle_symmetry_feedback_cb(self, action):
432         self.app.preferences['ui.feedback.symmetry'] = action.get_active()
433         self.update_overlays()
434
435     def update_overlays(self):
436         # Updates the list of overlays on the main doc's TDW to match the prefs
437         doc = self.app.doc
438         disp_overlays = [
439             ('ui.feedback.scale', ScaleOverlay),
440             ('ui.feedback.last_pos', LastPaintPosOverlay),
441             ('ui.feedback.symmetry', SymmetryOverlay),
442             ]
443         overlays_changed = False
444         for key, class_ in disp_overlays:
445             current_instance = None
446             for ov in doc.tdw.display_overlays:
447                 if isinstance(ov, class_):
448                     current_instance = ov
449             active = self.app.preferences.get(key, False)
450             if active and not current_instance:
451                 doc.tdw.display_overlays.append(class_(doc))
452                 overlays_changed = True
453             elif current_instance and not active:
454                 doc.tdw.display_overlays.remove(current_instance)
455                 overlays_changed = True
456         if overlays_changed:
457             doc.tdw.queue_draw()
458
459     def popup_cb(self, action):
460         state = self.popup_states[action.get_name()]
461         state.activate(action)
462
463
464     def brush_chooser_popup_cb(self, action):
465         # It may be even nicer to do this as a real popup state with
466         # mouse-out to cancel. The Action is named accordingly. For now
467         # though a modal dialog will do as an implementation.
468         dialogs.change_current_brush_quick(self.app)
469
470
471     def color_details_dialog_cb(self, action):
472         mgr = self.app.brush_color_manager
473         new_col = RGBColor.new_from_dialog(
474           title=_("Set current color"),
475           color=mgr.get_color(),
476           previous_color=mgr.get_previous_color(),
477           parent=self)
478         if new_col is not None:
479             mgr.set_color(new_col)
480
481
482     # User-toggleable UI pieces: things like toolbars, status bars, menu bars.
483     # Saved between sessions.
484
485
486     def get_ui_part_enabled(self, part_name):
487         """Returns whether the named UI part is enabled in the prefs.
488         """
489         parts = self.app.preferences["ui.parts"]
490         return bool(parts.get(part_name, False))
491
492
493     def update_ui_parts(self, **updates):
494         """Updates the UI part prefs, then hide/show widgets to match.
495
496         Called without arguments, this updates the UI to match the
497         boolean-valued hash ``ui.parts`` in the app preferences. With keyword
498         arguments, the prefs are updated first, then changes are reflected in
499         the set of visible widgets. Current known parts:
500
501             :``main_toolbar``:
502                 The primary toolbar and its menu button.
503             :``menubar``:
504                 A conventional menu bar.
505
506         Currently the user cannot turn off both the main toolbar and the
507         menubar: the toolbar will be forced on if an attempt is made.
508         """
509         new_state = self.app.preferences["ui.parts"].copy()
510         new_state.update(updates)
511         # Menu bar
512         if new_state.get("menubar", False):
513             self.menubar.show_all()
514         else:
515             self.menubar.hide()
516             if not new_state.get("main_toolbar", False):
517                 new_state["main_toolbar"] = True
518         # Toolbar
519         if new_state.get("main_toolbar", False):
520             self.toolbar.show_all()
521         else:
522             self.toolbar.hide()
523         self.app.preferences["ui.parts"] = new_state
524         self.update_menu_button()
525
526
527     def update_menu_button(self):
528         """Updates the menu button to match toolbar and menubar visibility.
529
530         The menu button is visible when the menu bar is hidden. Since the user
531         must have either a toolbar or a menu or both, this ensures that a menu
532         is on-screen at all times in non-fullscreen mode.
533         """
534         toolbar_visible = self.toolbar.get_property("visible")
535         menubar_visible = self.menubar.get_property("visible")
536         if toolbar_visible and menubar_visible:
537             self.toolbar_manager.menu_button.hide()
538         else:
539             self.toolbar_manager.menu_button.show_all()
540
541
542     def on_menuishbar_radio_change(self, radioaction, current):
543         """Respond to a change of the 'menu bar/toolbar' radio menu items.
544         """
545         value = radioaction.get_current_value()
546         if value == self.MENUISHBAR_RADIO_MENUBAR:
547             self.update_ui_parts(main_toolbar=False, menubar=True)
548         elif value == self.MENUISHBAR_RADIO_MAIN_TOOLBAR:
549             self.update_ui_parts(main_toolbar=True, menubar=False)
550         else:
551             self.update_ui_parts(main_toolbar=True, menubar=True)
552
553
554     # Show Subwindows
555     # Not saved between sessions, defaults to on.
556     # Controlled via its ToggleAction, and entering or leaving fullscreen mode
557     # according to the setting of ui.hide_in_fullscreen in prefs.
558
559     def set_show_subwindows(self, show_subwindows):
560         """Programatically set the Show Subwindows option.
561         """
562         action = self.action_group.get_action("ToggleSubwindows")
563         currently_showing = action.get_active()
564         if bool(show_subwindows) != bool(currently_showing):
565             action.set_active(show_subwindows)
566         self._show_subwindows = action.get_active()
567
568     def get_show_subwindows(self):
569         return self._show_subwindows
570
571     def toggle_subwindows_cb(self, action):
572         active = action.get_active()
573         lm = self.app.layout_manager
574         if active:
575             lm.toggle_user_tools(on=True)
576         else:
577             lm.toggle_user_tools(on=False)
578         self._show_subwindows = active
579
580
581     # Fullscreen mode
582     # This implementation requires an ICCCM and EWMH-compliant window manager
583     # which supports the _NET_WM_STATE_FULLSCREEN hint. There are several
584     # available.
585
586     def fullscreen_cb(self, *junk):
587         if not self.is_fullscreen:
588             self.fullscreen()
589         else:
590             self.unfullscreen()
591
592     def window_state_event_cb(self, widget, event):
593         # Respond to changes of the fullscreen state only
594         if not event.changed_mask & gdk.WINDOW_STATE_FULLSCREEN:
595             return
596         lm = self.app.layout_manager
597         self.is_fullscreen = event.new_window_state & gdk.WINDOW_STATE_FULLSCREEN
598         if self.is_fullscreen:
599             # Subwindow hiding 
600             if self.app.preferences.get("ui.hide_subwindows_in_fullscreen", True):
601                 self.set_show_subwindows(False)
602                 self._restore_subwindows_on_unfullscreen = True
603             if self.app.preferences.get("ui.hide_menubar_in_fullscreen", True):
604                 self.menubar.hide()
605                 self._restore_menubar_on_unfullscreen = True
606             if self.app.preferences.get("ui.hide_toolbar_in_fullscreen", True):
607                 self.toolbar.hide()
608                 self._restore_toolbar_on_unfullscreen = True
609             # fix for fullscreen problem on Windows, https://gna.org/bugs/?15175
610             # on X11/Metacity it also helps a bit against flickering during the switch
611             while gtk.events_pending():
612                 gtk.main_iteration()
613         else:
614             while gtk.events_pending():
615                 gtk.main_iteration()
616             if getattr(self, "_restore_menubar_on_unfullscreen", False):
617                 if self.get_ui_part_enabled("menubar"):
618                     self.menubar.show()
619                 del self._restore_menubar_on_unfullscreen
620             if getattr(self, "_restore_toolbar_on_unfullscreen", False):
621                 if self.get_ui_part_enabled("main_toolbar"):
622                     self.toolbar.show()
623                 del self._restore_toolbar_on_unfullscreen
624             if getattr(self, "_restore_subwindows_on_unfullscreen", False):
625                 self.set_show_subwindows(True)
626                 del self._restore_subwindows_on_unfullscreen
627         self.update_menu_button()
628         self.update_fullscreen_action()
629
630     def update_fullscreen_action(self):
631         action = self.action_group.get_action("Fullscreen")
632         if self.is_fullscreen:
633             action.set_stock_id(gtk.STOCK_LEAVE_FULLSCREEN)
634             action.set_tooltip(_("Leave Fullscreen Mode"))
635             action.set_label(_("UnFullscreen"))
636         else:
637             action.set_stock_id(gtk.STOCK_FULLSCREEN)
638             action.set_tooltip(_("Enter Fullscreen Mode"))
639             action.set_label(_("Fullscreen"))
640
641     def popupmenu_show_cb(self, action):
642         self.show_popupmenu()
643
644     def show_popupmenu(self, event=None):
645         self.menubar.set_sensitive(False)   # excessive feedback?
646         self.toolbar_manager.menu_button.set_sensitive(False)
647         button = 1
648         time = 0
649         if event is not None:
650             if event.type == gdk.BUTTON_PRESS:
651                 button = event.button
652                 time = event.time
653         self.popupmenu.popup(None, None, None, button, time)
654         if event is None:
655             # We're responding to an Action, most probably the menu key.
656             # Open out the last highlighted menu to speed key navigation up.
657             if self.popupmenu_last_active is None:
658                 self.popupmenu.select_first(True) # one less keypress
659             else:
660                 self.popupmenu.select_item(self.popupmenu_last_active)
661
662     def popupmenu_done_cb(self, *a, **kw):
663         # Not sure if we need to bother with this level of feedback,
664         # but it actually looks quite nice to see one menu taking over
665         # the other. Makes it clear that the popups are the same thing as
666         # the full menu, maybe.
667         self.menubar.set_sensitive(True)
668         self.toolbar_manager.menu_button.set_sensitive(True)
669         self.popupmenu_last_active = self.popupmenu.get_active()
670
671     # BEGIN -- Scratchpad menu options
672     def save_scratchpad_as_default_cb(self, action):
673         self.app.filehandler.save_scratchpad(self.app.filehandler.get_scratchpad_default(), export = True)
674
675     def clear_default_scratchpad_cb(self, action):
676         self.app.filehandler.delete_default_scratchpad()
677
678     # Unneeded since 'Save blank canvas' bug has been addressed.
679     #def clear_autosave_scratchpad_cb(self, action):
680     #    self.app.filehandler.delete_autosave_scratchpad()
681
682     def new_scratchpad_cb(self, action):
683         if os.path.isfile(self.app.filehandler.get_scratchpad_default()):
684             self.app.filehandler.open_scratchpad(self.app.filehandler.get_scratchpad_default())
685         else:
686             self.app.scratchpad_doc.model.clear()
687             # With no default - adopt the currently chosen background
688             bg = self.app.doc.model.background
689             if self.app.scratchpad_doc:
690                 self.app.scratchpad_doc.model.set_background(bg)
691
692         self.app.scratchpad_filename = self.app.preferences['scratchpad.last_opened'] = self.app.filehandler.get_scratchpad_autosave()
693
694     def load_scratchpad_cb(self, action):
695         if self.app.scratchpad_filename:
696             self.save_current_scratchpad_cb(action)
697             current_pad = self.app.scratchpad_filename
698         else:
699             current_pad = self.app.filehandler.get_scratchpad_autosave()
700         self.app.filehandler.open_scratchpad_dialog()
701         # Check to see if a file has been opened outside of the scratchpad directory
702         if not os.path.abspath(self.app.scratchpad_filename).startswith(os.path.abspath(self.app.filehandler.get_scratchpad_prefix())):
703             # file is NOT within the scratchpad directory - load copy as current scratchpad
704             self.app.scratchpad_filename = self.app.preferences['scratchpad.last_opened'] = current_pad
705
706     def save_as_scratchpad_cb(self, action):
707         self.app.filehandler.save_scratchpad_as_dialog()
708
709     def revert_current_scratchpad_cb(self, action):
710         if os.path.isfile(self.app.scratchpad_filename):
711             self.app.filehandler.open_scratchpad(self.app.scratchpad_filename)
712             print "Reverted to %s" % self.app.scratchpad_filename
713         else:
714             print "No file to revert to yet."
715
716     def save_current_scratchpad_cb(self, action):
717         self.app.filehandler.save_scratchpad(self.app.scratchpad_filename)
718
719     def scratchpad_copy_background_cb(self, action):
720         bg = self.app.doc.model.background
721         if self.app.scratchpad_doc:
722             self.app.scratchpad_doc.model.set_background(bg)
723
724     def draw_palette_cb(self, action):
725         # test functionality:
726         file_filters = [
727         (_("Gimp Palette Format"), ("*.gpl",)),
728         (_("All Files"), ("*.*",)),
729         ]
730         gimp_path = os.path.join(self.app.filehandler.get_gimp_prefix(), "palettes")
731         dialog = self.app.filehandler.get_open_dialog(start_in_folder=gimp_path,
732                                                   file_filters = file_filters)
733         try:
734             if dialog.run() == gtk.RESPONSE_OK:
735                 dialog.hide()
736                 filename = dialog.get_filename().decode('utf-8')
737                 if filename:
738                     #filename = "/home/ben/.gimp-2.6/palettes/Nature_Grass.gpl" # TEMP HACK TO TEST
739                     g = GimpPalette(filename)
740                     grid_size = 30.0
741                     column_limit = 7
742                     # IGNORE Gimp Palette 'columns'?
743                     if g.columns != 0:
744                         column_limit = g.columns   # use the value for columns in the palette
745                     draw_palette(self.app, g, self.app.scratchpad_doc, columns=column_limit, grid_size=grid_size, swatch_method=hatch_squiggle, scale = 25.0)
746         finally:
747             dialog.destroy()
748
749     def draw_sat_spectrum_cb(self, action):
750         g = GimpPalette()
751         hsv = self.app.brush.get_color_hsv()
752         g.append_sat_spectrum(hsv)
753         grid_size = 30.0
754         off_x = off_y = grid_size / 2.0
755         column_limit = 8
756         draw_palette(self.app, g, self.app.scratchpad_doc, columns=column_limit, grid_size=grid_size)
757
758     # END -- Scratchpad menu options
759
760
761     def palette_next_cb(self, action):
762         lm = self.app.layout_manager
763         combined = lm.get_tool_by_role('colorWindow').widget
764         pal_view = combined.get_palette_view()
765         pal_view.grid.select_next()
766         combined.show_palette_view()
767
768
769     def palette_prev_cb(self, action):
770         lm = self.app.layout_manager
771         combined = lm.get_tool_by_role('colorWindow').widget
772         pal_view = combined.get_palette_view()
773         pal_view.grid.select_previous()
774         combined.show_palette_view()
775
776
777     def quit_cb(self, *junk):
778         self.app.doc.model.split_stroke()
779         self.app.save_gui_config() # FIXME: should do this periodically, not only on quit
780
781         if not self.app.filehandler.confirm_destructive_action(title=_('Quit'), question=_('Really Quit?')):
782             return True
783
784         gtk.main_quit()
785         return False
786
787     def toggle_frame_cb(self, action):
788         enabled = bool(self.app.doc.model.frame_enabled)
789         desired = bool(action.get_active())
790         if enabled != desired:
791             self.app.doc.model.set_frame_enabled(desired)
792
793     def frame_changed_cb(self):
794         action = self.action_group.get_action("FrameToggle")
795         if getattr(action, "in_callback", False):
796             return
797         action.in_callback = True
798         enabled = bool(self.app.doc.model.frame_enabled)
799         action.set_active(enabled)
800         action.in_callback = False
801
802     def download_brush_pack_cb(self, *junk):
803         url = 'http://wiki.mypaint.info/index.php?title=Brush_Packages/redirect_mypaint_1.1_gui'
804         print 'URL:', url
805         webbrowser.open(url)
806
807     def import_brush_pack_cb(self, *junk):
808         format_id, filename = dialogs.open_dialog(_("Import brush package..."), self,
809                                  [(_("MyPaint brush package (*.zip)"), "*.zip")])
810         if filename:
811             self.app.brushmanager.import_brushpack(filename,  self)
812
813     # INFORMATION
814     # TODO: Move into dialogs.py?
815     def about_cb(self, action):
816         d = gtk.AboutDialog()
817         d.set_transient_for(self)
818         d.set_program_name("MyPaint")
819         d.set_version(MYPAINT_VERSION)
820         d.set_copyright(_("Copyright (C) 2005-2012\nMartin Renold and the MyPaint Development Team"))
821         d.set_website("http://mypaint.info/")
822         d.set_logo(self.app.pixmaps.mypaint_logo)
823         d.set_license(
824             _(u"This program is free software; you can redistribute it and/or modify "
825               u"it under the terms of the GNU General Public License as published by "
826               u"the Free Software Foundation; either version 2 of the License, or "
827               u"(at your option) any later version.\n"
828               u"\n"
829               u"This program is distributed in the hope that it will be useful, "
830               u"but WITHOUT ANY WARRANTY. See the COPYING file for more details.")
831             )
832         d.set_wrap_license(True)
833         d.set_authors([
834             # (in order of appearance)
835             u"Martin Renold (%s)" % _('programming'),
836             u"Yves Combe (%s)" % _('portability'),
837             u"Popolon (%s)" % _('programming'),
838             u"Clement Skau (%s)" % _('programming'),
839             u"Jon Nordby (%s)" % _('programming'),
840             u"Álinson Santos (%s)" % _('programming'),
841             u"Tumagonx (%s)" % _('portability'),
842             u"Ilya Portnov (%s)" % _('programming'),
843             u"Jonas Wagner (%s)" % _('programming'),
844             u"Luka Čehovin (%s)" % _('programming'),
845             u"Andrew Chadwick (%s)" % _('programming'),
846             u"Till Hartmann (%s)" % _('programming'),
847             u'David Grundberg (%s)' % _('programming'),
848             u"Krzysztof Pasek (%s)" % _('programming'),
849             u"Ben O'Steen (%s)" % _('programming'),
850             u"Ferry Jérémie (%s)" % _('programming'),
851             u"しげっち 'sigetch' (%s)" % _('programming'),
852             u"Richard Jones (%s)" % _('programming'),
853             u"David Gowers (%s)" % _('programming'),
854             ])
855         d.set_artists([
856             u"Artis Rozentāls (%s)" % _('brushes'),
857             u"Popolon (%s)" % _('brushes'),
858             u"Marcelo 'Tanda' Cerviño (%s)" % _('patterns, brushes'),
859             u"David Revoy (%s)" % _('brushes, tool icons'),
860             u"Ramón Miranda (%s)" % _('brushes, patterns'),
861             u"Enrico Guarnieri 'Ico_dY' (%s)" % _('brushes'),
862             u'Sebastian Kraft (%s)' % _('desktop icon'),
863             u"Nicola Lunghi (%s)" % _('patterns'),
864             u"Toni Kasurinen (%s)" % _('brushes'),
865             u"Сан Саныч 'MrMamurk' (%s)" % _('patterns'),
866             u"Andrew Chadwick (%s)" % _('tool icons'),
867             u"Ben O'Steen (%s)" % _('tool icons'),
868             ])
869         d.set_translator_credits(_("translator-credits"));
870
871         d.run()
872         d.destroy()
873
874     def show_infodialog_cb(self, action):
875         text = {
876         'ShortcutHelp':
877                 _("Move your mouse over a menu entry, then press the key to assign."),
878         'ContextHelp':
879                 _("Brush shortcut keys are used to quickly save/restore brush "
880                  "settings. You can paint with one hand and change brushes with "
881                  "the other, even in mid-stroke."
882                  "\n\n"
883                  "There are 10 persistent memory slots available."),
884         'Docu':
885                 _("There is a tutorial available on the MyPaint homepage. It "
886                  "explains some features which are hard to discover yourself."
887                  "\n\n"
888                  "Comments about the brush settings (opaque, hardness, etc.) and "
889                  "inputs (pressure, speed, etc.) are available as tooltips. "
890                  "Put your mouse over a label to see them. "
891                  "\n"),
892         }
893         self.app.message_dialog(text[action.get_name()])
894
895