OSDN Git Service

8df6ba2a5c11ed552559e151c6b4174b8db45493
[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"
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.app.filehandler.current_file_observers.append(self.update_title)
74
75         self.init_actions()
76
77         kbm = self.app.kbm
78         kbm.add_extra_key('Menu', 'ShowPopupMenu')
79         kbm.add_extra_key('Tab', 'ToggleSubwindows')
80
81         self.init_stategroups()
82
83         # Load Menubar, duplicate into self.popupmenu
84         menupath = os.path.join(self.app.datapath, 'gui/menu.xml')
85         menubar_xml = open(menupath).read()
86         self.app.ui_manager.add_ui_from_string(menubar_xml)
87         self._init_popupmenu(menubar_xml)
88
89         # Set up widgets
90         vbox = gtk.VBox()
91         self.add(vbox)
92         self.menubar = self.app.ui_manager.get_widget('/Menubar')
93         vbox.pack_start(self.menubar, expand=False)
94         vbox.pack_start(self.app.doc.tdw)
95
96         # Window handling
97         self.set_default_size(600, 400)
98         self.fullscreen = False
99
100     #XXX: Compatability
101     def get_doc(self):
102         print "DeprecationWarning: Use app.doc instead"
103         return self.app.doc
104     def get_tdw(self):
105         print "DeprecationWarning: Use app.doc.tdw instead"
106         return self.app.doc.tdw
107     tdw, doc = property(get_tdw), property(get_doc)
108
109     def init_actions(self):
110         actions = [
111             # name, stock id, label, accelerator, tooltip, callback
112             ('FileMenu',    None, _('File')),
113             ('Quit',         gtk.STOCK_QUIT, _('Quit'), '<control>q', None, self.quit_cb),
114
115             ('EditMenu',        None, _('Edit')),
116             ('PreferencesWindow', gtk.STOCK_PREFERENCES, _('Preferences...'), None, None, self.toggleWindow_cb),
117
118             ('ColorMenu',    None, _('Color')),
119             ('ColorPickerPopup',    gtk.STOCK_COLOR_PICKER, _('Pick Color'), 'r', None, self.popup_cb),
120             ('ColorHistoryPopup',  None, _('Color History'), 'x', None, self.popup_cb),
121             ('ColorChangerPopup', None, _('Color Changer'), 'v', None, self.popup_cb),
122             ('ColorRingPopup',  None, _('Color Ring'), None, None, self.popup_cb),
123             ('ColorSelectionWindow',  gtk.STOCK_SELECT_COLOR, _('Color Triangle...'), 'g', None, self.toggleWindow_cb),
124             ('ColorSamplerWindow',  gtk.STOCK_SELECT_COLOR, _('Color Sampler...'), 't', None, self.toggleWindow_cb),
125
126             ('ContextMenu',  None, _('Brushkeys')),
127             ('ContextHelp',  gtk.STOCK_HELP, _('Help!'), None, None, self.show_infodialog_cb),
128
129             ('LayerMenu',    None, _('Layers')),
130             ('LayersWindow', gtk.STOCK_INDEX, _('Layers...'), 'l', None, self.toggleWindow_cb),
131             ('BackgroundWindow', gtk.STOCK_PAGE_SETUP, _('Background...'), None, None, self.toggleWindow_cb),
132
133             ('BrushMenu',    None, _('Brush')),
134             ('BrushSelectionWindow',  None, _('Brush List...'), 'b', None, self.toggleWindow_cb),
135             ('BrushSettingsWindow',   gtk.STOCK_PROPERTIES, _('Brush Editor...'), '<control>b', None, self.toggleWindow_cb),
136             ('ImportBrushPack',       gtk.STOCK_OPEN, _('Import brush package...'), '', None, self.import_brush_pack_cb),
137
138             ('HelpMenu',   None, _('Help')),
139             ('Docu', gtk.STOCK_INFO, _('Where is the Documentation?'), None, None, self.show_infodialog_cb),
140             ('ShortcutHelp',  gtk.STOCK_INFO, _('Change the Keyboard Shortcuts?'), None, None, self.show_infodialog_cb),
141             ('About', gtk.STOCK_ABOUT, _('About MyPaint'), None, None, self.about_cb),
142
143             ('DebugMenu',    None, _('Debug')),
144             ('PrintMemoryLeak',  None, _('Print Memory Leak Info to stdout (Slow!)'), None, None, self.print_memory_leak_cb),
145             ('RunGarbageCollector',  None, _('Run Garbage Collector Now'), None, None, self.run_garbage_collector_cb),
146             ('StartProfiling',  gtk.STOCK_EXECUTE, _('Start/Stop Python Profiling (cProfile)'), None, None, self.start_profiling_cb),
147             ('InputTestWindow',  None, _('Test input devices...'), None, None, self.toggleWindow_cb),
148
149             ('ViewMenu', None, _('View')),
150             ('ShowPopupMenu',    None, _('Popup Menu'), 'Menu', None, self.popupmenu_show_cb),
151             ('Fullscreen',   gtk.STOCK_FULLSCREEN, _('Fullscreen'), 'F11', None, self.fullscreen_cb),
152             ('ToggleSubwindows',    None, _('Toggle Subwindows'), 'Tab', None, self.toggle_subwindows_cb),
153             ('ViewHelp',  gtk.STOCK_HELP, _('Help'), None, None, self.show_infodialog_cb),
154             ]
155         ag = self.action_group = gtk.ActionGroup('WindowActions')
156         ag.add_actions(actions)
157
158         for action in self.action_group.list_actions():
159             self.app.kbm.takeover_action(action)
160
161         self.app.ui_manager.insert_action_group(ag, -1)
162
163     def init_stategroups(self):
164         sg = stategroup.StateGroup()
165         p2s = sg.create_popup_state
166         changer = p2s(colorselectionwindow.ColorChangerPopup(self.app))
167         ring = p2s(colorselectionwindow.ColorRingPopup(self.app))
168         hist = p2s(historypopup.HistoryPopup(self.app, self.app.doc.model))
169         pick = self.colorpick_state = p2s(colorpicker.ColorPicker(self.app, self.app.doc.model))
170
171         self.popup_states = {
172             'ColorChangerPopup': changer,
173             'ColorRingPopup': ring,
174             'ColorHistoryPopup': hist,
175             'ColorPickerPopup': pick,
176             }
177         changer.next_state = ring
178         ring.next_state = changer
179         changer.autoleave_timeout = None
180         ring.autoleave_timeout = None
181
182         pick.max_key_hit_duration = 0.0
183         pick.autoleave_timeout = None
184
185         hist.autoleave_timeout = 0.600
186         self.history_popup_state = hist
187
188     def _init_popupmenu(self, xml):
189         """
190         Hopefully temporary hack for converting UIManager XML describing the
191         main menubar into a rebindable popup menu. UIManager by itself doesn't
192         let you do this, by design, but we need a bigger menu than the little
193         things it allows you to build.
194         """
195         ui_elt = ET.fromstring(xml)
196         rootmenu_elt = ui_elt.find("menubar")
197         rootmenu_elt.attrib["name"] = "PopupMenu"
198         ## XML-style menu jiggling. No need for this really though.
199         #for menu_elt in rootmenu_elt.findall("menu"):
200         #    for item_elt in menu_elt.findall("menuitem"):
201         #        if item_elt.attrib.get("action", "") == "ShowPopupMenu":
202         #            menu_elt.remove(item_elt)
203         ## Maybe shift a small number of frequently-used items to the top?
204         xml = ET.tostring(ui_elt)
205         self.app.ui_manager.add_ui_from_string(xml)
206         tmp_menubar = self.app.ui_manager.get_widget('/PopupMenu')
207         self.popupmenu = gtk.Menu()
208         for item in tmp_menubar.get_children():
209             tmp_menubar.remove(item)
210             self.popupmenu.append(item)
211         self.popupmenu.attach_to_widget(self.app.doc.tdw, None)
212         #self.popupmenu.set_title("MyPaint")
213         #self.popupmenu.set_take_focus(True)
214         self.popupmenu.connect("selection-done", self.popupmenu_done_cb)
215         self.popupmenu.connect("deactivate", self.popupmenu_done_cb)
216         self.popupmenu.connect("cancel", self.popupmenu_done_cb)
217         self.popupmenu_last_active = None
218
219
220     def update_title(self, filename):
221         if filename:
222             self.set_title("MyPaint - %s" % os.path.basename(filename))
223         else:
224             self.set_title("MyPaint")
225
226     # INPUT EVENT HANDLING
227     def drag_data_received(self, widget, context, x, y, selection, info, t):
228         if selection.data:
229             uri = selection.data.split("\r\n")[0]
230             fn = helpers.uri2filename(uri)
231             if os.path.exists(fn):
232                 if self.app.filehandler.confirm_destructive_action():
233                     self.app.filehandler.open_file(fn)
234
235     def print_memory_leak_cb(self, action):
236         helpers.record_memory_leak_status(print_diff = True)
237
238     def run_garbage_collector_cb(self, action):
239         helpers.run_garbage_collector()
240
241     def start_profiling_cb(self, action):
242         if getattr(self, 'profiler_active', False):
243             self.profiler_active = False
244             return
245
246         def doit():
247             import cProfile
248             profile = cProfile.Profile()
249
250             self.profiler_active = True
251             print '--- GUI Profiling starts ---'
252             while self.profiler_active:
253                 profile.runcall(gtk.main_iteration, False)
254                 if not gtk.events_pending():
255                     time.sleep(0.050) # ugly trick to remove "user does nothing" from profile
256             print '--- GUI Profiling ends ---'
257
258             profile.dump_stats('profile_fromgui.pstats')
259             #print 'profile written to mypaint_profile.pstats'
260             os.system('gprof2dot.py -f pstats profile_fromgui.pstats | dot -Tpng -o profile_fromgui.png && feh profile_fromgui.png &')
261
262         gobject.idle_add(doit)
263
264     def key_press_event_cb_before(self, win, event):
265         key = event.keyval 
266         ctrl = event.state & gdk.CONTROL_MASK
267         shift = event.state & gdk.SHIFT_MASK
268         #ANY_MODIFIER = gdk.SHIFT_MASK | gdk.MOD1_MASK | gdk.CONTROL_MASK
269         #if event.state & ANY_MODIFIER:
270         #    # allow user shortcuts with modifiers
271         #    return False
272         if key == keysyms.space:
273             if shift:
274                 self.app.doc.tdw.start_drag(self.app.doc.dragfunc_rotate)
275             elif ctrl:
276                 self.app.doc.tdw.start_drag(self.app.doc.dragfunc_zoom)
277             else:
278                 self.app.doc.tdw.start_drag(self.app.doc.dragfunc_translate)
279         else: return False
280         return True
281
282     def key_release_event_cb_before(self, win, event):
283         if event.keyval == keysyms.space:
284             self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_translate)
285             self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_rotate)
286             self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_zoom)
287             return True
288         return False
289
290     def key_press_event_cb_after(self, win, event):
291         key = event.keyval
292         if self.fullscreen and key == keysyms.Escape: self.fullscreen_cb()
293         else: return False
294         return True
295     def key_release_event_cb_after(self, win, event):
296         return False
297
298     def button_press_cb(self, win, event):
299         #print event.device, event.button
300         ctrl = event.state & gdk.CONTROL_MASK
301         alt  = event.state & gdk.MOD1_MASK
302         shift = event.state & gdk.SHIFT_MASK
303         if event.type != gdk.BUTTON_PRESS:
304             # ignore the extra double-click event
305             return
306         if event.button == 2:
307             # check whether we are painting (accidental)
308             if event.state & gdk.BUTTON1_MASK:
309                 # Do not allow dragging in the middle of
310                 # painting. This often happens by accident with wacom
311                 # tablet's stylus button.
312                 #
313                 # However we allow dragging if the user's pressure is
314                 # still below the click threshold.  This is because
315                 # some tablet PCs are not able to produce a
316                 # middle-mouse click without reporting pressure.
317                 # https://gna.org/bugs/index.php?15907
318                 pass
319             else:
320                 dragfunc = self.app.doc.dragfunc_translate
321                 if shift:
322                     dragfunc = self.app.doc.dragfunc_rotate
323                 elif ctrl:
324                     dragfunc = self.app.doc.dragfunc_zoom
325                 self.app.doc.tdw.start_drag(dragfunc)
326         elif event.button == 1:
327             if (ctrl or alt) and not (event.state & (gdk.BUTTON2_MASK | gdk.BUTTON3_MASK)):
328                 self.app.doc.end_eraser_mode()
329                 self.colorpick_state.activate(event)
330         elif event.button == 3:
331             if self.app.preferences['input.enable_history_popup']:
332                 self.history_popup_state.activate(event)
333
334     def button_release_cb(self, win, event):
335         #print event.device, event.button
336         if event.button == 2:
337             self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_translate)
338             self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_rotate)
339             self.app.doc.tdw.stop_drag(self.app.doc.dragfunc_zoom)
340
341     def scroll_cb(self, win, event):
342         d = event.direction
343         if d == gdk.SCROLL_UP:
344             if event.state & gdk.SHIFT_MASK:
345                 self.app.doc.rotate('RotateLeft')
346             else:
347                 self.app.doc.zoom('ZoomIn')
348         elif d == gdk.SCROLL_DOWN:
349             if event.state & gdk.SHIFT_MASK:
350                 self.app.doc.rotate('RotateRight')
351             else:
352                 self.app.doc.zoom('ZoomOut')
353         elif d == gdk.SCROLL_RIGHT:
354             self.app.doc.rotate('RotateRight')
355         elif d == gdk.SCROLL_LEFT:
356             self.app.doc.rotate('RotateLeft')
357
358     # WINDOW HANDLING
359     def toggleWindow_cb(self, action):
360         s = action.get_name()
361         window_name = s[0].lower() + s[1:] # WindowName -> windowName
362         w = self.app.windowmanager.get_window(window_name)
363         if w.window and w.window.is_visible():
364             w.hide()
365         else:
366             w.show_all() # might be for the first time
367             w.present()
368
369     def popup_cb(self, action):
370         # This doesn't really belong here...
371         # just because all popups are color popups now...
372         # ...maybe should eraser_mode be a GUI state too?
373         self.app.doc.end_eraser_mode()
374
375         state = self.popup_states[action.get_name()]
376         state.activate(action)
377
378     def fullscreen_cb(self, *trash):
379         self.fullscreen = not self.fullscreen
380         if self.fullscreen:
381             self.app.windowmanager.user_subwindows.hide()
382             x, y = self.get_position()
383             w, h = self.get_size()
384             self.geometry_before_fullscreen = (x, y, w, h)
385             self.menubar.hide()
386             # fix for fullscreen problem on Windows, https://gna.org/bugs/?15175
387             # on X11/Metacity it also helps a bit against flickering during the switch
388             while gtk.events_pending():
389                 gtk.main_iteration()
390             self.window.fullscreen()
391             #self.app.doc.tdw.set_scroll_at_edges(True)
392         else:
393             self.window.unfullscreen()
394             while gtk.events_pending():
395                 gtk.main_iteration()
396             self.menubar.show()
397             #self.app.doc.tdw.set_scroll_at_edges(False)
398             del self.geometry_before_fullscreen
399             self.app.windowmanager.user_subwindows.show()
400
401     def popupmenu_show_cb(self, action):
402         self.menubar.set_sensitive(False)   # excessive feedback?
403         self.popupmenu.popup(None, None, None, 1, 0)
404         if self.popupmenu_last_active is None:
405             self.popupmenu.select_first(True) # one less keypress
406         else:
407             self.popupmenu.select_item(self.popupmenu_last_active)
408
409     def popupmenu_done_cb(self, *a, **kw):
410         # Not sure if we need to bother with this level of feedback,
411         # but it actaully looks quite nice to see one menu taking over
412         # the other. Makes it clear that the popups are the same thing as
413         # the full menu, maybe.
414         self.menubar.set_sensitive(True)
415         self.popupmenu_last_active = self.popupmenu.get_active()
416
417     def toggle_subwindows_cb(self, action):
418         self.app.windowmanager.user_subwindows.toggle()
419
420     def quit_cb(self, *trash):
421         self.app.doc.model.split_stroke()
422         self.app.save_gui_config() # FIXME: should do this periodically, not only on quit
423
424         if not self.app.filehandler.confirm_destructive_action(title=_('Quit'), question=_('Really Quit?')):
425             return True
426
427         gtk.main_quit()
428         return False
429
430     def import_brush_pack_cb(self, *trash):
431         format_id, filename = dialogs.open_dialog(_("Import brush package..."), self,
432                                  [(_("MyPaint brush package (*.zip)"), "*.zip")])
433         if filename:
434             self.app.brushmanager.import_brushpack(filename,  self)
435
436     # INFORMATION
437     # TODO: Move into dialogs.py?
438     def about_cb(self, action):
439         d = gtk.AboutDialog()
440         d.set_transient_for(self)
441         d.set_program_name("MyPaint")
442         d.set_version(MYPAINT_VERSION)
443         d.set_copyright(_("Copyright (C) 2005-2010\nMartin Renold and the MyPaint Development Team"))
444         d.set_website("http://mypaint.info/")
445         d.set_logo(self.app.pixmaps.mypaint_logo)
446         d.set_license(
447             _(u"This program is free software; you can redistribute it and/or modify "
448               u"it under the terms of the GNU General Public License as published by "
449               u"the Free Software Foundation; either version 2 of the License, or "
450               u"(at your option) any later version.\n"
451               u"\n"
452               u"This program is distributed in the hope that it will be useful, "
453               u"but WITHOUT ANY WARRANTY. See the COPYING file for more details.")
454             )
455         d.set_wrap_license(True)
456         d.set_authors([
457             u"Martin Renold (%s)" % _('programming'),
458             u"Artis Rozentāls (%s)" % _('brushes'),
459             u"Yves Combe (%s)" % _('portability'),
460             u"Popolon (%s)" % _('brushes, programming'),
461             u"Clement Skau (%s)" % _('programming'),
462             u"Marcelo 'Tanda' Cerviño (%s)" % _('patterns, brushes'),
463             u"Jon Nordby (%s)" % _('programming'),
464             u"Álinson Santos (%s)" % _('programming'),
465             u"Tumagonx (%s)" % _('portability'),
466             u"Ilya Portnov (%s)" % _('programming'),
467             u"David Revoy (%s)" % _('brushes'),
468             u"Ramón Miranda (%s)" % _('brushes'),
469             u"Enrico Guarnieri 'Ico_dY' (%s)" % _('brushes'),
470             u"Jonas Wagner (%s)" % _('programming'),
471             u"Luka Čehovin (%s)" % _('programming'),
472             u"Andrew Chadwick (%s)" % _('programming'),
473             u"Till Hartmann (%s)" % _('programming'),
474             u"Nicola Lunghi (%s)" % _('patterns'),
475             u"Toni Kasurinen (%s)" % _('brushes'),
476             ])
477         d.set_artists([
478             u'Sebastian Kraft (%s)' % _('desktop icon'),
479             ])
480         # list all translators, not only those of the current language
481         d.set_translator_credits(
482             u'Ilya Portnov (ru)\n'
483             u'Popolon (fr, zh_CN, ja)\n'
484             u'Jon Nordby (nb)\n'
485             u'Griatch (sv)\n'
486             u'Tobias Jakobs (de)\n'
487             u'Martin Tabačan (cs)\n'
488             u'Tumagonx (id)\n'
489             u'Manuel Quiñones (es)\n'
490             u'Gergely Aradszki (hu)\n'
491             u'Lamberto Tedaldi (it)\n'
492             u'Dong-Jun Wu (zh_TW)\n'
493             u'Luka Čehovin (sl)\n'
494             u'Geuntak Jeong (ko)\n'
495             u'Łukasz Lubojański (pl)\n'
496             u'Daniel Korostil (uk)\n'
497             u'Julian Aloofi (de)\n'
498             u'Tor Egil Hoftun Kvæstad (nn_NO)\n'
499             )
500         
501         d.run()
502         d.destroy()
503
504     def show_infodialog_cb(self, action):
505         text = {
506         'ShortcutHelp': 
507                 _("Move your mouse over a menu entry, then press the key to assign."),
508         'ViewHelp': 
509                 _("You can also drag the canvas with the mouse while holding the middle "
510                 "mouse button or spacebar. Or with the arrow keys."
511                 "\n\n"
512                 "In contrast to earlier versions, scrolling and zooming are harmless now and "
513                 "will not make you run out of memory. But you still require a lot of memory "
514                 "if you paint all over while fully zoomed out."),
515         'ContextHelp':
516                 _("Brushkeys are used to quickly save/restore brush settings "
517                  "using keyboard shortcuts. You can paint with one hand and "
518                  "change brushes with the other without interrupting."
519                  "\n\n"
520                  "There are 10 memory slots to hold brush settings.\n"
521                  "Those are anonymous "
522                  "brushes, they are not visible in the brush selector list. "
523                  "But they will stay even if you quit. "),
524         'Docu':
525                 _("There is a tutorial available "
526                  "on the MyPaint homepage. It explains some features which are "
527                  "hard to discover yourself.\n\n"
528                  "Comments about the brush settings (opaque, hardness, etc.) and "
529                  "inputs (pressure, speed, etc.) are available as tooltips. "
530                  "Put your mouse over a label to see them. "
531                  "\n"),
532         }
533         self.app.message_dialog(text[action.get_name()])