OSDN Git Service

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