OSDN Git Service

changelog, version bump
[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.0-beta1"
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
25 import dialogs
26 from lib import helpers
27 import xml.etree.ElementTree as ET
28
29
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)
42         try:
43             func(self, *args, **kwargs)
44         finally:
45             self.app.drawWindow.window.set_cursor(None)
46             self.app.doc.tdw.update_cursor()
47     return wrapper
48
49
50 class Window(windowing.MainWindow):
51     def __init__(self, app):
52         windowing.MainWindow.__init__(self, app)
53         self.app = app
54
55         # Enable drag & drop
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)
61
62         # Connect events
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)
72
73         self.init_actions()
74
75         kbm = self.app.kbm
76         kbm.add_extra_key('Menu', 'ShowPopupMenu')
77         kbm.add_extra_key('Tab', 'ToggleSubwindows')
78
79         self.init_stategroups()
80
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)
86
87         # Set up widgets
88         vbox = gtk.VBox()
89         self.add(vbox)
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)
93
94         # Window handling
95         self.set_default_size(600, 400)
96         self.fullscreen = False
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
113             ('EditMenu',        None, _('Edit')),
114             ('PreferencesWindow', gtk.STOCK_PREFERENCES, _('Preferences...'), None, None, self.toggleWindow_cb),
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             ('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),
123
124             ('ContextMenu',  None, _('Brushkeys')),
125             ('ContextHelp',  gtk.STOCK_HELP, _('Help!'), None, None, self.show_infodialog_cb),
126
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),
130
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),
135
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),
140
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),
146
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),
152             ]
153         ag = self.action_group = gtk.ActionGroup('WindowActions')
154         ag.add_actions(actions)
155
156         for action in self.action_group.list_actions():
157             self.app.kbm.takeover_action(action)
158
159         self.app.ui_manager.insert_action_group(ag, -1)
160
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))
168
169         self.popup_states = {
170             'ColorChangerPopup': changer,
171             'ColorRingPopup': ring,
172             'ColorHistoryPopup': hist,
173             'ColorPickerPopup': pick,
174             }
175         changer.next_state = ring
176         ring.next_state = changer
177         changer.autoleave_timeout = None
178         ring.autoleave_timeout = None
179
180         pick.max_key_hit_duration = 0.0
181         pick.autoleave_timeout = None
182
183         hist.autoleave_timeout = 0.600
184         self.history_popup_state = hist
185
186     def _init_popupmenu(self, xml):
187         """
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.
192         """
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
216
217     # INPUT EVENT HANDLING
218     def drag_data_received(self, widget, context, x, y, selection, info, t):
219         if selection.data:
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)
225
226     def print_memory_leak_cb(self, action):
227         helpers.record_memory_leak_status(print_diff = True)
228
229     def run_garbage_collector_cb(self, action):
230         helpers.run_garbage_collector()
231
232     def start_profiling_cb(self, action):
233         if getattr(self, 'profiler_active', False):
234             self.profiler_active = False
235             return
236
237         def doit():
238             import cProfile
239             profile = cProfile.Profile()
240
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 ---'
248
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 &')
252
253         gobject.idle_add(doit)
254
255     def key_press_event_cb_before(self, win, event):
256         key = event.keyval 
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
261         #    return False
262         if key == keysyms.space:
263             if ctrl:
264                 self.app.doc.tdw.start_drag(self.app.doc.dragfunc_rotate)
265             else:
266                 self.app.doc.tdw.start_drag(self.app.doc.dragfunc_translate)
267         else: return False
268         return True
269
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)
274             return True
275         return False
276
277     def key_press_event_cb_after(self, win, event):
278         key = event.keyval
279         if self.fullscreen and key == keysyms.Escape: self.fullscreen_cb()
280         else: return False
281         return True
282     def key_release_event_cb_after(self, win, event):
283         return False
284
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
289             return
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.
296                 #
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
302                 pass
303             else:
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)
315
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)
321
322     def scroll_cb(self, win, event):
323         d = event.direction
324         if d == gdk.SCROLL_UP:
325             if event.state & gdk.SHIFT_MASK:
326                 self.app.doc.rotate('RotateLeft')
327             else:
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')
332             else:
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')
338
339     # WINDOW HANDLING
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():
345             w.hide()
346         else:
347             w.show_all() # might be for the first time
348             w.present()
349
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()
355
356         state = self.popup_states[action.get_name()]
357         state.activate(action)
358
359     def fullscreen_cb(self, *trash):
360         self.fullscreen = not self.fullscreen
361         if 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)
366             self.menubar.hide()
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():
370                 gtk.main_iteration()
371             self.window.fullscreen()
372             #self.app.doc.tdw.set_scroll_at_edges(True)
373         else:
374             self.window.unfullscreen()
375             while gtk.events_pending():
376                 gtk.main_iteration()
377             self.menubar.show()
378             #self.app.doc.tdw.set_scroll_at_edges(False)
379             del self.geometry_before_fullscreen
380             self.app.user_subwindows.show()
381
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
387         else:
388             self.popupmenu.select_item(self.popupmenu_last_active)
389
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()
397
398     def toggle_subwindows_cb(self, action):
399         self.app.user_subwindows.toggle()
400
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
404
405         if not self.app.filehandler.confirm_destructive_action(title=_('Quit'), question=_('Really Quit?')):
406             return True
407
408         gtk.main_quit()
409         return False
410
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:
415             #try:
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
420             #    d.set_markup(text)
421             #    d.run()
422             #    d.destroy()
423
424     # INFORMATION
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)
434         d.set_license(
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"
439               u"\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.")
442             )
443         d.set_wrap_license(True)
444         d.set_authors([
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'),
464             ])
465         d.set_artists([
466             u'Sebastian Kraft (%s)' % _('desktop icon'),
467             ])
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'
472             u'Jon Nordby (nb)\n'
473             u'Griatch (sv)\n'
474             u'Tobias Jakobs (de)\n'
475             u'Martin Tabačan (cs)\n'
476             u'Tumagonx (id)\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'
487             )
488         
489         d.run()
490         d.destroy()
491
492     def show_infodialog_cb(self, action):
493         text = {
494         'ShortcutHelp': 
495                 _("Move your mouse over a menu entry, then press the key to assign."),
496         'ViewHelp': 
497                 _("You can also drag the canvas with the mouse while holding the middle "
498                 "mouse button or spacebar. Or with the arrow keys."
499                 "\n\n"
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."),
503         'ContextHelp':
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."
507                  "\n\n"
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. "),
514         'Docu':
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. "
521                  "\n"),
522         }
523         self.app.message_dialog(text[action.get_name()])