OSDN Git Service

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