1 # -*- coding: utf-8 -*-
3 # This file is part of MyPaint.
4 # Copyright (C) 2007-2008 by Martin Renold <martinxyz@gmx.ch>
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.
12 This is the main drawing window, containing menu actions.
13 Painting is done in tileddrawwidget.py.
16 MYPAINT_VERSION="0.9.1+git"
19 from gettext import gettext as _
22 from gtk import gdk, keysyms
24 import colorselectionwindow, historypopup, stategroup, colorpicker, windowing, layout
26 from lib import helpers
27 import xml.etree.ElementTree as ET
29 # TODO: put in a helper file?
30 def with_wait_cursor(func):
31 """python decorator that adds a wait cursor around a function"""
32 def wrapper(self, *args, **kwargs):
33 toplevels = [t for t in gtk.window_list_toplevels()
34 if t.window is not None]
35 for toplevel in toplevels:
36 toplevel.window.set_cursor(gdk.Cursor(gdk.WATCH))
37 toplevel.set_sensitive(False)
38 self.app.doc.tdw.grab_add()
40 func(self, *args, **kwargs)
41 # gtk main loop may be called in here...
43 for toplevel in toplevels:
44 toplevel.set_sensitive(True)
45 # ... which is why we need this check:
46 if toplevel.window is not None:
47 toplevel.window.set_cursor(None)
48 self.app.doc.tdw.grab_remove()
52 class Window (windowing.MainWindow, layout.MainWindow):
54 def __init__(self, app):
55 windowing.MainWindow.__init__(self, app)
59 self.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
60 gtk.DEST_DEFAULT_HIGHLIGHT |
61 gtk.DEST_DEFAULT_DROP,
62 [("text/uri-list", 0, 1)],
63 gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY)
66 self.connect('delete-event', self.quit_cb)
67 self.connect('key-press-event', self.key_press_event_cb_before)
68 self.connect('key-release-event', self.key_release_event_cb_before)
69 self.connect_after('key-press-event', self.key_press_event_cb_after)
70 self.connect_after('key-release-event', self.key_release_event_cb_after)
71 self.connect("drag-data-received", self.drag_data_received)
72 self.connect("window-state-event", self.window_state_event_cb)
74 self.app.filehandler.current_file_observers.append(self.update_title)
78 layout.MainWindow.__init__(self, app.layout_manager)
79 self.main_widget.connect("button-press-event", self.button_press_cb)
80 self.main_widget.connect("button-release-event",self.button_release_cb)
81 self.main_widget.connect("scroll-event", self.scroll_cb)
84 kbm.add_extra_key('Menu', 'ShowPopupMenu')
85 kbm.add_extra_key('Tab', 'ToggleSubwindows')
87 self.init_stategroups()
90 self.is_fullscreen = False
94 print "DeprecationWarning: Use app.doc instead"
97 print "DeprecationWarning: Use app.doc.tdw instead"
98 return self.app.doc.tdw
99 tdw, doc = property(get_tdw), property(get_doc)
101 def init_actions(self):
103 # name, stock id, label, accelerator, tooltip, callback
104 ('FileMenu', None, _('File')),
105 ('Quit', gtk.STOCK_QUIT, _('Quit'), '<control>q', None, self.quit_cb),
106 ('FrameWindow', None, _('Document Frame...'), None, None, self.toggleWindow_cb),
107 ('FrameToggle', None, _('Toggle Document Frame'), None, None, self.toggle_frame_cb),
109 ('EditMenu', None, _('Edit')),
110 ('PreferencesWindow', gtk.STOCK_PREFERENCES, _('Preferences...'), None, None, self.toggleWindow_cb),
112 ('ColorMenu', None, _('Color')),
113 ('ColorPickerPopup', gtk.STOCK_COLOR_PICKER, _('Pick Color'), 'r', None, self.popup_cb),
114 ('ColorHistoryPopup', None, _('Color History'), 'x', None, self.popup_cb),
115 ('ColorChangerPopup', None, _('Color Changer'), 'v', None, self.popup_cb),
116 ('ColorRingPopup', None, _('Color Ring'), None, None, self.popup_cb),
117 ('ColorSelectionWindow', gtk.STOCK_SELECT_COLOR, _('Color Triangle...'), 'g', None, self.toggleWindow_cb),
118 ('ColorSamplerWindow', gtk.STOCK_SELECT_COLOR, _('Color Sampler...'), 't', None, self.toggleWindow_cb),
120 ('ContextMenu', None, _('Brushkeys')),
121 ('ContextHelp', gtk.STOCK_HELP, _('Help!'), None, None, self.show_infodialog_cb),
123 ('LayerMenu', None, _('Layers')),
124 ('LayersWindow', gtk.STOCK_INDEX, _('Layers...'), 'l', None, self.toggleWindow_cb),
125 ('BackgroundWindow', gtk.STOCK_PAGE_SETUP, _('Background...'), None, None, self.toggleWindow_cb),
127 ('BrushMenu', None, _('Brush')),
128 ('BrushSelectionWindow', None, _('Brush List...'), 'b', None, self.toggleWindow_cb),
129 ('BrushSettingsWindow', gtk.STOCK_PROPERTIES, _('Brush Editor...'), '<control>b', None, self.toggleWindow_cb),
130 ('ImportBrushPack', gtk.STOCK_OPEN, _('Import brush package...'), '', None, self.import_brush_pack_cb),
132 ('HelpMenu', None, _('Help')),
133 ('Docu', gtk.STOCK_INFO, _('Where is the Documentation?'), None, None, self.show_infodialog_cb),
134 ('ShortcutHelp', gtk.STOCK_INFO, _('Change the Keyboard Shortcuts?'), None, None, self.show_infodialog_cb),
135 ('About', gtk.STOCK_ABOUT, _('About MyPaint'), None, None, self.about_cb),
137 ('DebugMenu', None, _('Debug')),
138 ('PrintMemoryLeak', None, _('Print Memory Leak Info to stdout (Slow!)'), None, None, self.print_memory_leak_cb),
139 ('RunGarbageCollector', None, _('Run Garbage Collector Now'), None, None, self.run_garbage_collector_cb),
140 ('StartProfiling', gtk.STOCK_EXECUTE, _('Start/Stop Python Profiling (cProfile)'), None, None, self.start_profiling_cb),
141 ('InputTestWindow', None, _('Test input devices...'), None, None, self.toggleWindow_cb),
142 ('GtkInputDialog', None, _('GTK input devices dialog...'), None, None, self.gtk_input_dialog_cb),
145 ('ViewMenu', None, _('View')),
146 ('ShowPopupMenu', None, _('Popup Menu'), 'Menu', None, self.popupmenu_show_cb),
147 ('Fullscreen', gtk.STOCK_FULLSCREEN, _('Fullscreen'), 'F11', None, self.fullscreen_cb),
148 ('ToggleSubwindows', None, _('Toggle Subwindows'), 'Tab', None, self.toggle_subwindows_cb),
149 ('ViewHelp', gtk.STOCK_HELP, _('Help'), None, None, self.show_infodialog_cb),
151 ag = self.action_group = gtk.ActionGroup('WindowActions')
152 ag.add_actions(actions)
154 for action in self.action_group.list_actions():
155 self.app.kbm.takeover_action(action)
157 self.app.ui_manager.insert_action_group(ag, -1)
159 def init_stategroups(self):
160 sg = stategroup.StateGroup()
161 p2s = sg.create_popup_state
162 changer = p2s(colorselectionwindow.ColorChangerPopup(self.app))
163 ring = p2s(colorselectionwindow.ColorRingPopup(self.app))
164 hist = p2s(historypopup.HistoryPopup(self.app, self.app.doc.model))
165 pick = self.colorpick_state = p2s(colorpicker.ColorPicker(self.app, self.app.doc.model))
167 self.popup_states = {
168 'ColorChangerPopup': changer,
169 'ColorRingPopup': ring,
170 'ColorHistoryPopup': hist,
171 'ColorPickerPopup': pick,
173 changer.next_state = ring
174 ring.next_state = changer
175 changer.autoleave_timeout = None
176 ring.autoleave_timeout = None
178 pick.max_key_hit_duration = 0.0
179 pick.autoleave_timeout = None
181 hist.autoleave_timeout = 0.600
182 self.history_popup_state = hist
184 def init_main_widget(self): # override
185 self.main_widget = self.app.doc.tdw
187 def init_menubar(self): # override
188 # Load Menubar, duplicate into self.popupmenu
189 menupath = os.path.join(self.app.datapath, 'gui/menu.xml')
190 menubar_xml = open(menupath).read()
191 self.app.ui_manager.add_ui_from_string(menubar_xml)
192 self._init_popupmenu(menubar_xml)
193 self.menubar = self.app.ui_manager.get_widget('/Menubar')
195 def _init_popupmenu(self, xml):
197 Hopefully temporary hack for converting UIManager XML describing the
198 main menubar into a rebindable popup menu. UIManager by itself doesn't
199 let you do this, by design, but we need a bigger menu than the little
200 things it allows you to build.
202 ui_elt = ET.fromstring(xml)
203 rootmenu_elt = ui_elt.find("menubar")
204 rootmenu_elt.attrib["name"] = "PopupMenu"
205 ## XML-style menu jiggling. No need for this really though.
206 #for menu_elt in rootmenu_elt.findall("menu"):
207 # for item_elt in menu_elt.findall("menuitem"):
208 # if item_elt.attrib.get("action", "") == "ShowPopupMenu":
209 # menu_elt.remove(item_elt)
210 ## Maybe shift a small number of frequently-used items to the top?
211 xml = ET.tostring(ui_elt)
212 self.app.ui_manager.add_ui_from_string(xml)
213 tmp_menubar = self.app.ui_manager.get_widget('/PopupMenu')
214 self.popupmenu = gtk.Menu()
215 for item in tmp_menubar.get_children():
216 tmp_menubar.remove(item)
217 self.popupmenu.append(item)
218 self.popupmenu.attach_to_widget(self.app.doc.tdw, None)
219 #self.popupmenu.set_title("MyPaint")
220 #self.popupmenu.set_take_focus(True)
221 self.popupmenu.connect("selection-done", self.popupmenu_done_cb)
222 self.popupmenu.connect("deactivate", self.popupmenu_done_cb)
223 self.popupmenu.connect("cancel", self.popupmenu_done_cb)
224 self.popupmenu_last_active = None
227 def update_title(self, filename):
229 self.set_title("MyPaint - %s" % os.path.basename(filename))
231 self.set_title("MyPaint")
233 # INPUT EVENT HANDLING
234 def drag_data_received(self, widget, context, x, y, selection, info, t):
236 uri = selection.data.split("\r\n")[0]
237 fn = helpers.uri2filename(uri)
238 if os.path.exists(fn):
239 if self.app.filehandler.confirm_destructive_action():
240 self.app.filehandler.open_file(fn)
242 def print_memory_leak_cb(self, action):
243 helpers.record_memory_leak_status(print_diff = True)
245 def run_garbage_collector_cb(self, action):
246 helpers.run_garbage_collector()
248 def start_profiling_cb(self, action):
249 if getattr(self, 'profiler_active', False):
250 self.profiler_active = False
255 profile = cProfile.Profile()
257 self.profiler_active = True
258 print '--- GUI Profiling starts ---'
259 while self.profiler_active:
260 profile.runcall(gtk.main_iteration, False)
261 if not gtk.events_pending():
262 time.sleep(0.050) # ugly trick to remove "user does nothing" from profile
263 print '--- GUI Profiling ends ---'
265 profile.dump_stats('profile_fromgui.pstats')
266 #print 'profile written to mypaint_profile.pstats'
267 os.system('gprof2dot.py -f pstats profile_fromgui.pstats | dot -Tpng -o profile_fromgui.png && feh profile_fromgui.png &')
269 gobject.idle_add(doit)
271 def gtk_input_dialog_cb(self, action):
272 d = gtk.InputDialog()
275 def key_press_event_cb_before(self, win, event):
277 ctrl = event.state & gdk.CONTROL_MASK
278 shift = event.state & gdk.SHIFT_MASK
279 alt = event.state & gdk.MOD1_MASK
280 #ANY_MODIFIER = gdk.SHIFT_MASK | gdk.MOD1_MASK | gdk.CONTROL_MASK
281 #if event.state & ANY_MODIFIER:
282 # # allow user shortcuts with modifiers
284 if key == keysyms.space:
286 self.app.doc.tdw.start_drag(self.app.doc.dragfunc_rotate)
288 self.app.doc.tdw.start_drag(self.app.doc.dragfunc_zoom)
290 self.app.doc.tdw.start_drag(self.app.doc.dragfunc_frame)
292 self.app.doc.tdw.start_drag(self.app.doc.dragfunc_translate)
296 def key_release_event_cb_before(self, win, event):
297 if event.keyval == keysyms.space:
298 self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_translate)
299 self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_rotate)
300 self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_zoom)
301 self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_frame)
305 def key_press_event_cb_after(self, win, event):
307 if self.is_fullscreen and key == keysyms.Escape: self.fullscreen_cb()
310 def key_release_event_cb_after(self, win, event):
313 def button_press_cb(self, win, event):
314 #print event.device, event.button
316 ## Ignore accidentals
317 # Single button-presses only, not 2ble/3ple
318 if event.type != gdk.BUTTON_PRESS:
319 # ignore the extra double-click event
322 if event.button != 1:
323 # check whether we are painting (accidental)
324 if event.state & gdk.BUTTON1_MASK:
325 # Do not allow dragging in the middle of
326 # painting. This often happens by accident with wacom
327 # tablet's stylus button.
329 # However we allow dragging if the user's pressure is
330 # still below the click threshold. This is because
331 # some tablet PCs are not able to produce a
332 # middle-mouse click without reporting pressure.
333 # https://gna.org/bugs/index.php?15907
336 # Pick a suitable config option
337 ctrl = event.state & gdk.CONTROL_MASK
338 alt = event.state & gdk.MOD1_MASK
339 shift = event.state & gdk.SHIFT_MASK
341 modifier_str = "_shift"
343 modifier_str = "_ctrl"
346 prefs_name = "input.button%d%s_action" % (event.button, modifier_str)
347 action_name = self.app.preferences.get(prefs_name, "no_action")
350 if action_name == 'no_action':
351 return True # We handled it by doing nothing
354 # Really belongs in the tdw, but this is the only object with access
355 # to the application preferences.
356 if action_name == 'straight_line':
357 self.app.doc.tdw.straight_line_from_last_painting_pos()
361 if action_name.endswith("_canvas"):
363 if action_name == "pan_canvas":
364 dragfunc = self.app.doc.dragfunc_translate
365 elif action_name == "zoom_canvas":
366 dragfunc = self.app.doc.dragfunc_zoom
367 elif action_name == "rotate_canvas":
368 dragfunc = self.app.doc.dragfunc_rotate
369 if dragfunc is not None:
370 self.app.doc.tdw.start_drag(dragfunc)
375 if action_name == 'popup_menu':
376 self.show_popupmenu(event=event)
379 # Popup states, typically for changing colour. Kill eraser mode and
380 # then enter them the usual way.
381 if action_name in self.popup_states:
382 state = self.popup_states[action_name]
383 self.app.doc.end_eraser_mode()
384 state.activate(event)
387 # Dispatch regular GTK events.
388 for ag in self.action_group, self.app.doc.action_group:
389 action = ag.get_action(action_name)
390 if action is not None:
394 def button_release_cb(self, win, event):
395 #print event.device, event.button
396 tdw = self.app.doc.tdw
397 if tdw.dragfunc is not None:
398 tdw.stop_drag(self.app.doc.dragfunc_translate)
399 tdw.stop_drag(self.app.doc.dragfunc_rotate)
400 tdw.stop_drag(self.app.doc.dragfunc_zoom)
403 def scroll_cb(self, win, event):
405 if d == gdk.SCROLL_UP:
406 if event.state & gdk.SHIFT_MASK:
407 self.app.doc.rotate('RotateLeft')
409 self.app.doc.zoom('ZoomIn')
410 elif d == gdk.SCROLL_DOWN:
411 if event.state & gdk.SHIFT_MASK:
412 self.app.doc.rotate('RotateRight')
414 self.app.doc.zoom('ZoomOut')
415 elif d == gdk.SCROLL_RIGHT:
416 self.app.doc.rotate('RotateRight')
417 elif d == gdk.SCROLL_LEFT:
418 self.app.doc.rotate('RotateLeft')
421 def toggleWindow_cb(self, action):
422 s = action.get_name()
423 window_name = s[0].lower() + s[1:] # WindowName -> windowName
424 # If it's a tool, get it to hide/show itself
425 t = self.app.layout_manager.get_tool_by_role(window_name)
427 t.set_hidden(not t.hidden)
429 # Otherwise, if it's a regular subwindow hide/show+present it./
430 w = self.app.layout_manager.get_subwindow_by_role(window_name)
433 if w.window and w.window.is_visible():
436 w.show_all() # might be for the first time
439 def popup_cb(self, action):
440 # This doesn't really belong here...
441 # just because all popups are color popups now...
442 # ...maybe should eraser_mode be a GUI state too?
443 self.app.doc.end_eraser_mode()
445 state = self.popup_states[action.get_name()]
446 state.activate(action)
448 def fullscreen_cb(self, *trash):
449 if not self.is_fullscreen:
454 def window_state_event_cb(self, widget, event):
455 # Respond to changes of the fullscreen state only
456 if not event.changed_mask & gdk.WINDOW_STATE_FULLSCREEN:
458 lm = self.app.layout_manager
459 self.is_fullscreen = event.new_window_state & gdk.WINDOW_STATE_FULLSCREEN
460 if self.is_fullscreen:
461 lm.toggle_user_tools(on=False)
462 x, y = self.get_position()
463 w, h = self.get_size()
465 # fix for fullscreen problem on Windows, https://gna.org/bugs/?15175
466 # on X11/Metacity it also helps a bit against flickering during the switch
467 while gtk.events_pending():
469 #self.app.doc.tdw.set_scroll_at_edges(True)
471 while gtk.events_pending():
474 #self.app.doc.tdw.set_scroll_at_edges(False)
475 lm.toggle_user_tools(on=True)
477 def popupmenu_show_cb(self, action):
478 self.show_popupmenu()
480 def show_popupmenu(self, event=None):
481 self.menubar.set_sensitive(False) # excessive feedback?
484 if event is not None:
485 if event.type == gdk.BUTTON_PRESS:
486 button = event.button
488 self.popupmenu.popup(None, None, None, button, time)
490 # We're responding to an Action, most probably the menu key.
491 # Open out the last highlighted menu to speed key navigation up.
492 if self.popupmenu_last_active is None:
493 self.popupmenu.select_first(True) # one less keypress
495 self.popupmenu.select_item(self.popupmenu_last_active)
497 def popupmenu_done_cb(self, *a, **kw):
498 # Not sure if we need to bother with this level of feedback,
499 # but it actaully looks quite nice to see one menu taking over
500 # the other. Makes it clear that the popups are the same thing as
501 # the full menu, maybe.
502 self.menubar.set_sensitive(True)
503 self.popupmenu_last_active = self.popupmenu.get_active()
505 def toggle_subwindows_cb(self, action):
506 self.app.layout_manager.toggle_user_tools()
507 if self.app.layout_manager.saved_user_tools:
508 if self.is_fullscreen:
511 if not self.is_fullscreen:
514 def quit_cb(self, *trash):
515 self.app.doc.model.split_stroke()
516 self.app.save_gui_config() # FIXME: should do this periodically, not only on quit
518 if not self.app.filehandler.confirm_destructive_action(title=_('Quit'), question=_('Really Quit?')):
524 def toggle_frame_cb(self, action):
525 enabled = self.app.doc.model.frame_enabled
526 self.app.doc.model.set_frame_enabled(not enabled)
528 def import_brush_pack_cb(self, *trash):
529 format_id, filename = dialogs.open_dialog(_("Import brush package..."), self,
530 [(_("MyPaint brush package (*.zip)"), "*.zip")])
532 self.app.brushmanager.import_brushpack(filename, self)
535 # TODO: Move into dialogs.py?
536 def about_cb(self, action):
537 d = gtk.AboutDialog()
538 d.set_transient_for(self)
539 d.set_program_name("MyPaint")
540 d.set_version(MYPAINT_VERSION)
541 d.set_copyright(_("Copyright (C) 2005-2010\nMartin Renold and the MyPaint Development Team"))
542 d.set_website("http://mypaint.info/")
543 d.set_logo(self.app.pixmaps.mypaint_logo)
545 _(u"This program is free software; you can redistribute it and/or modify "
546 u"it under the terms of the GNU General Public License as published by "
547 u"the Free Software Foundation; either version 2 of the License, or "
548 u"(at your option) any later version.\n"
550 u"This program is distributed in the hope that it will be useful, "
551 u"but WITHOUT ANY WARRANTY. See the COPYING file for more details.")
553 d.set_wrap_license(True)
555 u"Martin Renold (%s)" % _('programming'),
556 u"Artis Rozentāls (%s)" % _('brushes'),
557 u"Yves Combe (%s)" % _('portability'),
558 u"Popolon (%s)" % _('brushes, programming'),
559 u"Clement Skau (%s)" % _('programming'),
560 u"Marcelo 'Tanda' Cerviño (%s)" % _('patterns, brushes'),
561 u"Jon Nordby (%s)" % _('programming'),
562 u"Álinson Santos (%s)" % _('programming'),
563 u"Tumagonx (%s)" % _('portability'),
564 u"Ilya Portnov (%s)" % _('programming'),
565 u"David Revoy (%s)" % _('brushes'),
566 u"Ramón Miranda (%s)" % _('brushes'),
567 u"Enrico Guarnieri 'Ico_dY' (%s)" % _('brushes'),
568 u"Jonas Wagner (%s)" % _('programming'),
569 u"Luka Čehovin (%s)" % _('programming'),
570 u"Andrew Chadwick (%s)" % _('programming'),
571 u"Till Hartmann (%s)" % _('programming'),
572 u"Nicola Lunghi (%s)" % _('patterns'),
573 u"Toni Kasurinen (%s)" % _('brushes'),
576 u'Sebastian Kraft (%s)' % _('desktop icon'),
578 # list all translators, not only those of the current language
579 d.set_translator_credits(
580 u'Ilya Portnov (ru)\n'
581 u'Popolon (fr, zh_CN, ja)\n'
584 u'Tobias Jakobs (de)\n'
585 u'Martin Tabačan (cs)\n'
587 u'Manuel Quiñones (es)\n'
588 u'Gergely Aradszki (hu)\n'
589 u'Lamberto Tedaldi (it)\n'
590 u'Dong-Jun Wu (zh_TW)\n'
591 u'Luka Čehovin (sl)\n'
592 u'Geuntak Jeong (ko)\n'
593 u'Łukasz Lubojański (pl)\n'
594 u'Daniel Korostil (uk)\n'
595 u'Julian Aloofi (de)\n'
596 u'Tor Egil Hoftun Kvæstad (nn_NO)\n'
597 u'João S. O. Bueno (pt_BR)\n'
603 def show_infodialog_cb(self, action):
606 _("Move your mouse over a menu entry, then press the key to assign."),
608 _("You can also drag the canvas with the mouse while holding the middle "
609 "mouse button or spacebar. Or with the arrow keys."
611 "In contrast to earlier versions, scrolling and zooming are harmless now and "
612 "will not make you run out of memory. But you still require a lot of memory "
613 "if you paint all over while fully zoomed out."),
615 _("Brushkeys are used to quickly save/restore brush settings "
616 "using keyboard shortcuts. You can paint with one hand and "
617 "change brushes with the other without interrupting."
619 "There are 10 memory slots to hold brush settings.\n"
620 "Those are anonymous "
621 "brushes, they are not visible in the brush selector list. "
622 "But they will stay even if you quit. "),
624 _("There is a tutorial available "
625 "on the MyPaint homepage. It explains some features which are "
626 "hard to discover yourself.\n\n"
627 "Comments about the brush settings (opaque, hardness, etc.) and "
628 "inputs (pressure, speed, etc.) are available as tooltips. "
629 "Put your mouse over a label to see them. "
632 self.app.message_dialog(text[action.get_name()])