OSDN Git Service

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