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.0-beta1"
19 from gettext import gettext as _
22 from gtk import gdk, keysyms
24 import colorselectionwindow, historypopup, stategroup, colorpicker, windowing
26 from lib import helpers
27 import xml.etree.ElementTree as ET
30 #TODO: make generic by taking the windows as arguments and put in a helper file?
31 def with_wait_cursor(func):
32 """python decorator that adds a wait cursor around a function"""
33 def wrapper(self, *args, **kwargs):
34 # process events which might include cursor changes
35 while gtk.events_pending():
36 gtk.main_iteration(False)
37 self.app.drawWindow.window.set_cursor(gdk.Cursor(gdk.WATCH))
38 self.app.doc.tdw.window.set_cursor(None)
39 # make sure it is actually changed before we return
40 while gtk.events_pending():
41 gtk.main_iteration(False)
43 func(self, *args, **kwargs)
45 self.app.drawWindow.window.set_cursor(None)
46 self.app.doc.tdw.update_cursor()
50 class Window(windowing.MainWindow):
51 def __init__(self, app):
52 windowing.MainWindow.__init__(self, app)
56 self.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
57 gtk.DEST_DEFAULT_HIGHLIGHT |
58 gtk.DEST_DEFAULT_DROP,
59 [("text/uri-list", 0, 1)],
60 gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY)
63 self.connect('delete-event', self.quit_cb)
64 self.connect('key-press-event', self.key_press_event_cb_before)
65 self.connect('key-release-event', self.key_release_event_cb_before)
66 self.connect_after('key-press-event', self.key_press_event_cb_after)
67 self.connect_after('key-release-event', self.key_release_event_cb_after)
68 self.connect("drag-data-received", self.drag_data_received)
69 self.connect("button-press-event", self.button_press_cb)
70 self.connect("button-release-event", self.button_release_cb)
71 self.connect("scroll-event", self.scroll_cb)
76 kbm.add_extra_key('Menu', 'ShowPopupMenu')
77 kbm.add_extra_key('Tab', 'ToggleSubwindows')
79 self.init_stategroups()
81 # Load Menubar, duplicate into self.popupmenu
82 menupath = os.path.join(self.app.datapath, 'gui/menu.xml')
83 menubar_xml = open(menupath).read()
84 self.app.ui_manager.add_ui_from_string(menubar_xml)
85 self._init_popupmenu(menubar_xml)
90 self.menubar = self.app.ui_manager.get_widget('/Menubar')
91 vbox.pack_start(self.menubar, expand=False)
92 vbox.pack_start(self.app.doc.tdw)
95 self.set_default_size(600, 400)
96 self.fullscreen = False
100 print "DeprecationWarning: Use app.doc instead"
103 print "DeprecationWarning: Use app.doc.tdw instead"
104 return self.app.doc.tdw
105 tdw, doc = property(get_tdw), property(get_doc)
107 def init_actions(self):
109 # name, stock id, label, accelerator, tooltip, callback
110 ('FileMenu', None, _('File')),
111 ('Quit', gtk.STOCK_QUIT, _('Quit'), '<control>q', None, self.quit_cb),
113 ('EditMenu', None, _('Edit')),
114 ('PreferencesWindow', gtk.STOCK_PREFERENCES, _('Preferences...'), None, None, self.toggleWindow_cb),
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 ('ColorSelectionWindow', gtk.STOCK_SELECT_COLOR, _('Color Triangle...'), 'g', None, self.toggleWindow_cb),
122 ('ColorSamplerWindow', gtk.STOCK_SELECT_COLOR, _('Color Sampler...'), 't', None, self.toggleWindow_cb),
124 ('ContextMenu', None, _('Brushkeys')),
125 ('ContextHelp', gtk.STOCK_HELP, _('Help!'), None, None, self.show_infodialog_cb),
127 ('LayerMenu', None, _('Layers')),
128 ('LayersWindow', gtk.STOCK_INDEX, _('Layers...'), 'l', None, self.toggleWindow_cb),
129 ('BackgroundWindow', gtk.STOCK_PAGE_SETUP, _('Background...'), None, None, self.toggleWindow_cb),
131 ('BrushMenu', None, _('Brush')),
132 ('BrushSelectionWindow', None, _('Brush List...'), 'b', None, self.toggleWindow_cb),
133 ('BrushSettingsWindow', gtk.STOCK_PROPERTIES, _('Brush Editor...'), '<control>b', None, self.toggleWindow_cb),
134 ('ImportBrushPack', gtk.STOCK_OPEN, _('Import brush package...'), '', None, self.import_brush_pack_cb),
136 ('HelpMenu', None, _('Help')),
137 ('Docu', gtk.STOCK_INFO, _('Where is the Documentation?'), None, None, self.show_infodialog_cb),
138 ('ShortcutHelp', gtk.STOCK_INFO, _('Change the Keyboard Shortcuts?'), None, None, self.show_infodialog_cb),
139 ('About', gtk.STOCK_ABOUT, _('About MyPaint'), None, None, self.about_cb),
141 ('DebugMenu', None, _('Debug')),
142 ('PrintMemoryLeak', None, _('Print Memory Leak Info to stdout (Slow!)'), None, None, self.print_memory_leak_cb),
143 ('RunGarbageCollector', None, _('Run Garbage Collector Now'), None, None, self.run_garbage_collector_cb),
144 ('StartProfiling', gtk.STOCK_EXECUTE, _('Start/Stop Python Profiling (cProfile)'), None, None, self.start_profiling_cb),
145 ('InputTestWindow', None, _('Test input devices...'), None, None, self.toggleWindow_cb),
147 ('ViewMenu', None, _('View')),
148 ('ShowPopupMenu', None, _('Popup Menu'), 'Menu', None, self.popupmenu_show_cb),
149 ('Fullscreen', gtk.STOCK_FULLSCREEN, _('Fullscreen'), 'F11', None, self.fullscreen_cb),
150 ('ToggleSubwindows', None, _('Toggle Subwindows'), 'Tab', None, self.toggle_subwindows_cb),
151 ('ViewHelp', gtk.STOCK_HELP, _('Help'), None, None, self.show_infodialog_cb),
153 ag = self.action_group = gtk.ActionGroup('WindowActions')
154 ag.add_actions(actions)
156 for action in self.action_group.list_actions():
157 self.app.kbm.takeover_action(action)
159 self.app.ui_manager.insert_action_group(ag, -1)
161 def init_stategroups(self):
162 sg = stategroup.StateGroup()
163 p2s = sg.create_popup_state
164 changer = p2s(colorselectionwindow.ColorChangerPopup(self.app))
165 ring = p2s(colorselectionwindow.ColorRingPopup(self.app))
166 hist = p2s(historypopup.HistoryPopup(self.app, self.app.doc.model))
167 pick = self.colorpick_state = p2s(colorpicker.ColorPicker(self.app, self.app.doc.model))
169 self.popup_states = {
170 'ColorChangerPopup': changer,
171 'ColorRingPopup': ring,
172 'ColorHistoryPopup': hist,
173 'ColorPickerPopup': pick,
175 changer.next_state = ring
176 ring.next_state = changer
177 changer.autoleave_timeout = None
178 ring.autoleave_timeout = None
180 pick.max_key_hit_duration = 0.0
181 pick.autoleave_timeout = None
183 hist.autoleave_timeout = 0.600
184 self.history_popup_state = hist
186 def _init_popupmenu(self, xml):
188 Hopefully temporary hack for converting UIManager XML describing the
189 main menubar into a rebindable popup menu. UIManager by itself doesn't
190 let you do this, by design, but we need a bigger menu than the little
191 things it allows you to build.
193 ui_elt = ET.fromstring(xml)
194 rootmenu_elt = ui_elt.find("menubar")
195 rootmenu_elt.attrib["name"] = "PopupMenu"
196 ## XML-style menu jiggling. No need for this really though.
197 #for menu_elt in rootmenu_elt.findall("menu"):
198 # for item_elt in menu_elt.findall("menuitem"):
199 # if item_elt.attrib.get("action", "") == "ShowPopupMenu":
200 # menu_elt.remove(item_elt)
201 ## Maybe shift a small number of frequently-used items to the top?
202 xml = ET.tostring(ui_elt)
203 self.app.ui_manager.add_ui_from_string(xml)
204 tmp_menubar = self.app.ui_manager.get_widget('/PopupMenu')
205 self.popupmenu = gtk.Menu()
206 for item in tmp_menubar.get_children():
207 tmp_menubar.remove(item)
208 self.popupmenu.append(item)
209 self.popupmenu.attach_to_widget(self.app.doc.tdw, None)
210 #self.popupmenu.set_title("MyPaint")
211 #self.popupmenu.set_take_focus(True)
212 self.popupmenu.connect("selection-done", self.popupmenu_done_cb)
213 self.popupmenu.connect("deactivate", self.popupmenu_done_cb)
214 self.popupmenu.connect("cancel", self.popupmenu_done_cb)
215 self.popupmenu_last_active = None
217 # INPUT EVENT HANDLING
218 def drag_data_received(self, widget, context, x, y, selection, info, t):
220 uri = selection.data.split("\r\n")[0]
221 fn = helpers.get_file_path_from_dnd_dropped_uri(uri)
222 if os.path.exists(fn):
223 if self.app.filehandler.confirm_destructive_action():
224 self.app.filehandler.open_file(fn)
226 def print_memory_leak_cb(self, action):
227 helpers.record_memory_leak_status(print_diff = True)
229 def run_garbage_collector_cb(self, action):
230 helpers.run_garbage_collector()
232 def start_profiling_cb(self, action):
233 if getattr(self, 'profiler_active', False):
234 self.profiler_active = False
239 profile = cProfile.Profile()
241 self.profiler_active = True
242 print '--- GUI Profiling starts ---'
243 while self.profiler_active:
244 profile.runcall(gtk.main_iteration, False)
245 if not gtk.events_pending():
246 time.sleep(0.050) # ugly trick to remove "user does nothing" from profile
247 print '--- GUI Profiling ends ---'
249 profile.dump_stats('profile_fromgui.pstats')
250 #print 'profile written to mypaint_profile.pstats'
251 os.system('gprof2dot.py -f pstats profile_fromgui.pstats | dot -Tpng -o profile_fromgui.png && feh profile_fromgui.png &')
253 gobject.idle_add(doit)
255 def key_press_event_cb_before(self, win, event):
257 ctrl = event.state & gdk.CONTROL_MASK
258 #ANY_MODIFIER = gdk.SHIFT_MASK | gdk.MOD1_MASK | gdk.CONTROL_MASK
259 #if event.state & ANY_MODIFIER:
260 # # allow user shortcuts with modifiers
262 if key == keysyms.space:
264 self.app.doc.tdw.start_drag(self.app.doc.dragfunc_rotate)
266 self.app.doc.tdw.start_drag(self.app.doc.dragfunc_translate)
270 def key_release_event_cb_before(self, win, event):
271 if event.keyval == keysyms.space:
272 self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_translate)
273 self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_rotate)
277 def key_press_event_cb_after(self, win, event):
279 if self.fullscreen and key == keysyms.Escape: self.fullscreen_cb()
282 def key_release_event_cb_after(self, win, event):
285 def button_press_cb(self, win, event):
286 #print event.device, event.button
287 if event.type != gdk.BUTTON_PRESS:
288 # ignore the extra double-click event
290 if event.button == 2:
291 # check whether we are painting (accidental)
292 if event.state & gdk.BUTTON1_MASK:
293 # Do not allow dragging in the middle of
294 # painting. This often happens by accident with wacom
295 # tablet's stylus button.
297 # However we allow dragging if the user's pressure is
298 # still below the click threshold. This is because
299 # some tablet PCs are not able to produce a
300 # middle-mouse click without reporting pressure.
301 # https://gna.org/bugs/index.php?15907
304 dragfunc = self.app.doc.dragfunc_translate
305 if event.state & gdk.CONTROL_MASK:
306 dragfunc = self.app.doc.dragfunc_rotate
307 self.app.doc.tdw.start_drag(dragfunc)
308 elif event.button == 1:
309 if (event.state & gdk.CONTROL_MASK) and not (event.state & (gdk.BUTTON2_MASK | gdk.BUTTON3_MASK)):
310 self.app.doc.end_eraser_mode()
311 self.colorpick_state.activate(event)
312 elif event.button == 3:
313 if self.app.preferences['input.enable_history_popup']:
314 self.history_popup_state.activate(event)
316 def button_release_cb(self, win, event):
317 #print event.device, event.button
318 if event.button == 2:
319 self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_translate)
320 self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_rotate)
322 def scroll_cb(self, win, event):
324 if d == gdk.SCROLL_UP:
325 if event.state & gdk.SHIFT_MASK:
326 self.app.doc.rotate('RotateLeft')
328 self.app.doc.zoom('ZoomIn')
329 elif d == gdk.SCROLL_DOWN:
330 if event.state & gdk.SHIFT_MASK:
331 self.app.doc.rotate('RotateRight')
333 self.app.doc.zoom('ZoomOut')
334 elif d == gdk.SCROLL_LEFT:
335 self.app.doc.rotate('RotateRight')
336 elif d == gdk.SCROLL_LEFT:
337 self.app.doc.rotate('RotateLeft')
340 def toggleWindow_cb(self, action):
341 s = action.get_name()
342 s = s[0].lower() + s[1:]
343 w = getattr(self.app, s)
344 if w.window and w.window.is_visible():
347 w.show_all() # might be for the first time
350 def popup_cb(self, action):
351 # This doesn't really belong here...
352 # just because all popups are color popups now...
353 # ...maybe should eraser_mode be a GUI state too?
354 self.app.doc.end_eraser_mode()
356 state = self.popup_states[action.get_name()]
357 state.activate(action)
359 def fullscreen_cb(self, *trash):
360 self.fullscreen = not self.fullscreen
362 self.app.user_subwindows.hide()
363 x, y = self.get_position()
364 w, h = self.get_size()
365 self.geometry_before_fullscreen = (x, y, w, h)
367 # fix for fullscreen problem on Windows, https://gna.org/bugs/?15175
368 # on X11/Metacity it also helps a bit against flickering during the switch
369 while gtk.events_pending():
371 self.window.fullscreen()
372 #self.app.doc.tdw.set_scroll_at_edges(True)
374 self.window.unfullscreen()
375 while gtk.events_pending():
378 #self.app.doc.tdw.set_scroll_at_edges(False)
379 del self.geometry_before_fullscreen
380 self.app.user_subwindows.show()
382 def popupmenu_show_cb(self, action):
383 self.menubar.set_sensitive(False) # excessive feedback?
384 self.popupmenu.popup(None, None, None, 1, 0)
385 if self.popupmenu_last_active is None:
386 self.popupmenu.select_first(True) # one less keypress
388 self.popupmenu.select_item(self.popupmenu_last_active)
390 def popupmenu_done_cb(self, *a, **kw):
391 # Not sure if we need to bother with this level of feedback,
392 # but it actaully looks quite nice to see one menu taking over
393 # the other. Makes it clear that the popups are the same thing as
394 # the full menu, maybe.
395 self.menubar.set_sensitive(True)
396 self.popupmenu_last_active = self.popupmenu.get_active()
398 def toggle_subwindows_cb(self, action):
399 self.app.user_subwindows.toggle()
401 def quit_cb(self, *trash):
402 self.app.doc.model.split_stroke()
403 self.app.save_gui_config() # FIXME: should do this periodically, not only on quit
405 if not self.app.filehandler.confirm_destructive_action(title=_('Quit'), question=_('Really Quit?')):
411 def import_brush_pack_cb(self, *trash):
412 format_id, filename = dialogs.open_dialog(_("Import brush package..."), self,
413 [(_("MyPaint brush package (*.zip)"), "*.zip")])
414 if filename is not None:
416 self.app.brushmanager.import_brushpack(filename, self)
417 #except Exception, e:
418 # d = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK_CANCEL, type=gtk.MESSAGE_ERROR)
419 # text = _("An error occured while importing brush package. Error was: %s") % e
425 # TODO: Move into dialogs.py?
426 def about_cb(self, action):
427 d = gtk.AboutDialog()
428 d.set_transient_for(self)
429 d.set_program_name("MyPaint")
430 d.set_version(MYPAINT_VERSION)
431 d.set_copyright(_("Copyright (C) 2005-2010\nMartin Renold and the MyPaint Development Team"))
432 d.set_website("http://mypaint.info/")
433 d.set_logo(self.app.pixmaps.mypaint_logo)
435 _(u"This program is free software; you can redistribute it and/or modify "
436 u"it under the terms of the GNU General Public License as published by "
437 u"the Free Software Foundation; either version 2 of the License, or "
438 u"(at your option) any later version.\n"
440 u"This program is distributed in the hope that it will be useful, "
441 u"but WITHOUT ANY WARRANTY. See the COPYING file for more details.")
443 d.set_wrap_license(True)
445 u"Martin Renold (%s)" % _('programming'),
446 u"Artis Rozentāls (%s)" % _('brushes'),
447 u"Yves Combe (%s)" % _('portability'),
448 u"Popolon (%s)" % _('brushes, programming'),
449 u"Clement Skau (%s)" % _('programming'),
450 u"Marcelo 'Tanda' Cerviño (%s)" % _('patterns, brushes'),
451 u"Jon Nordby (%s)" % _('programming'),
452 u"Álinson Santos (%s)" % _('programming'),
453 u"Tumagonx (%s)" % _('portability'),
454 u"Ilya Portnov (%s)" % _('programming'),
455 u"David Revoy (%s)" % _('brushes'),
456 u"Ramón Miranda (%s)" % _('brushes'),
457 u"Enrico Guarnieri 'Ico_dY' (%s)" % _('brushes'),
458 u"Jonas Wagner (%s)" % _('programming'),
459 u"Luka Čehovin (%s)" % _('programming'),
460 u"Andrew Chadwick (%s)" % _('programming'),
461 u"Till Hartmann (%s)" % _('programming'),
462 u"Nicola Lunghi (%s)" % _('patterns'),
463 u"Toni Kasurinen (%s)" % _('brushes'),
466 u'Sebastian Kraft (%s)' % _('desktop icon'),
468 # list all translators, not only those of the current language
469 d.set_translator_credits(
470 u'Ilya Portnov (ru)\n'
471 u'Popolon (fr, zh_CN, ja)\n'
474 u'Tobias Jakobs (de)\n'
475 u'Martin Tabačan (cs)\n'
477 u'Manuel Quiñones (es)\n'
478 u'Gergely Aradszki (hu)\n'
479 u'Lamberto Tedaldi (it)\n'
480 u'Dong-Jun Wu (zh_TW)\n'
481 u'Luka Čehovin (sl)\n'
482 u'Geuntak Jeong (ko)\n'
483 u'Łukasz Lubojański (pl)\n'
484 u'Daniel Korostil (uk)\n'
485 u'Julian Aloofi (de)\n'
486 u'Tor Egil Hoftun Kvæstad (nn_NO)\n'
492 def show_infodialog_cb(self, action):
495 _("Move your mouse over a menu entry, then press the key to assign."),
497 _("You can also drag the canvas with the mouse while holding the middle "
498 "mouse button or spacebar. Or with the arrow keys."
500 "In contrast to earlier versions, scrolling and zooming are harmless now and "
501 "will not make you run out of memory. But you still require a lot of memory "
502 "if you paint all over while fully zoomed out."),
504 _("This is used to quickly save/restore brush settings "
505 "using keyboard shortcuts. You can paint with one hand and "
506 "change brushes with the other without interrupting."
508 "There are 10 memory slots to hold brush settings.\n"
509 "Those are anonymous "
510 "brushes, they are not visible in the brush selector list. "
511 "But they will stay even if you quit. "
512 "They will also remember the selected color. In contrast, selecting a "
513 "normal brush never changes the color. "),
515 _("There is a tutorial available "
516 "on the MyPaint homepage. It explains some features which are "
517 "hard to discover yourself.\n\n"
518 "Comments about the brush settings (opaque, hardness, etc.) and "
519 "inputs (pressure, speed, etc.) are available as tooltips. "
520 "Put your mouse over a label to see them. "
523 self.app.message_dialog(text[action.get_name()])