OSDN Git Service

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