OSDN Git Service

c0ce3ca6d08dcbd178a295d29df0894641363666
[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.7.0+git"
17
18 import os, re, math
19 from time import time
20 from glob import glob
21
22 import gtk
23 from gtk import gdk, keysyms
24
25 import tileddrawwidget, colorselectionwindow, historypopup, \
26        stategroup, keyboard, colorpicker
27 from lib import document, helpers, backgroundsurface
28
29 class Window(gtk.Window):
30     def __init__(self, app):
31         gtk.Window.__init__(self)
32         self.app = app
33
34         self.connect('delete-event', self.quit_cb)
35         self.connect('key-press-event', self.key_press_event_cb_before)
36         self.connect('key-release-event', self.key_release_event_cb_before)
37         self.connect_after('key-press-event', self.key_press_event_cb_after)
38         self.connect_after('key-release-event', self.key_release_event_cb_after)
39         self.connect("button-press-event", self.button_press_cb)
40         self.connect("button-release-event", self.button_release_cb)
41         self.connect("scroll-event", self.scroll_cb)
42         self.set_default_size(600, 400)
43         vbox = gtk.VBox()
44         self.add(vbox)
45
46         self.doc = document.Document()
47         self.doc.set_brush(self.app.brush)
48
49         self.create_ui()
50         self.menubar = self.ui.get_widget('/Menubar')
51         vbox.pack_start(self.menubar, expand=False)
52
53         self.tdw = tileddrawwidget.TiledDrawWidget(self.doc)
54         vbox.pack_start(self.tdw)
55
56         # FIXME: hack, to be removed
57         filename = os.path.join(self.app.datapath, 'backgrounds', '03_check1.png')
58         pixbuf = gdk.pixbuf_new_from_file(filename)
59         self.tdw.neutral_background_pixbuf = backgroundsurface.Background(helpers.gdkpixbuf2numpy(pixbuf))
60
61         self.zoomlevel_values = [2.0/11, 0.25, 1.0/3, 0.50, 2.0/3, 1.0, 1.5, 2.0, 3.0, 4.0, 5.5, 8.0]
62         self.zoomlevel = self.zoomlevel_values.index(1.0)
63         self.tdw.zoom_min = min(self.zoomlevel_values)
64         self.tdw.zoom_max = max(self.zoomlevel_values)
65         self.fullscreen = False
66
67         self.app.brush.settings_observers.append(self.brush_modified_cb)
68         self.tdw.device_observers.append(self.device_changed_cb)
69             
70         fn = os.path.join(self.app.confpath, 'save_history.conf')
71         if os.path.exists(fn):
72             self.save_history = [line.strip() for line in open(fn)]
73         else:
74             self.save_history = []
75
76         self.init_save_dialog()
77
78         #filename is a property so that all changes will update the title
79         self.filename = None
80
81         self.eraser_mode_radius_change = 3*(0.3) # can go back to exact original with brush_smaller_cb()
82         self.eraser_mode_original_radius = None
83         
84         
85     def get_filename(self):
86         return self._filename 
87     def set_filename(self,value):
88         self._filename = value
89         if self.filename: 
90             self.set_title("MyPaint - %s" % os.path.basename(self.filename))
91         else:
92             self.set_title("MyPaint")
93     filename = property(get_filename, set_filename)
94
95     def create_ui(self):
96         ag = self.action_group = gtk.ActionGroup('WindowActions')
97         # FIXME: this xml menu only creates unneeded information duplication, I think.
98                 # FIXME: better just use glade...
99         ui_string = """<ui>
100           <menubar name='Menubar'>
101             <menu action='FileMenu'>
102               <menuitem action='New'/>
103               <menuitem action='Open'/>
104               <menuitem action='OpenRecent'/>
105               <separator/>
106               <menuitem action='Save'/>
107               <menuitem action='SaveAs'/>
108               <menuitem action='SaveScrap'/>
109               <separator/>
110               <menuitem action='Quit'/>
111             </menu>
112             <menu action='EditMenu'>
113               <menuitem action='Undo'/>
114               <menuitem action='Redo'/>
115               <separator/>
116               <menuitem action='CopyLayer'/>
117               <menuitem action='PasteLayer'/>
118               <separator/>
119               <menuitem action='SettingsWindow'/>
120             </menu>
121             <menu action='ViewMenu'>
122               <menuitem action='Fullscreen'/>
123               <separator/>
124               <menuitem action='ResetView'/>
125               <menuitem action='ZoomIn'/>
126               <menuitem action='ZoomOut'/>
127               <menuitem action='RotateLeft'/>
128               <menuitem action='RotateRight'/>
129               <menuitem action='Flip'/>
130               <separator/>
131               <menuitem action='ViewHelp'/>
132             </menu>
133             <menu action='BrushMenu'>
134               <menuitem action='BrushSelectionWindow'/>
135               <menu action='ContextMenu'>
136                 <menuitem action='ContextStore'/>
137                 <separator/>
138                 <menuitem action='Context00'/>
139                 <menuitem action='Context00s'/>
140                 <menuitem action='Context01'/>
141                 <menuitem action='Context01s'/>
142                 <menuitem action='Context02'/>
143                 <menuitem action='Context02s'/>
144                 <menuitem action='Context03'/>
145                 <menuitem action='Context03s'/>
146                 <menuitem action='Context04'/>
147                 <menuitem action='Context04s'/>
148                 <menuitem action='Context05'/>
149                 <menuitem action='Context05s'/>
150                 <menuitem action='Context06'/>
151                 <menuitem action='Context06s'/>
152                 <menuitem action='Context07'/>
153                 <menuitem action='Context07s'/>
154                 <menuitem action='Context08'/>
155                 <menuitem action='Context08s'/>
156                 <menuitem action='Context09'/>
157                 <menuitem action='Context09s'/>
158                 <separator/>
159                 <menuitem action='ContextHelp'/>
160               </menu>
161               <separator/>
162               <menuitem action='BrushSettingsWindow'/>
163               <separator/>
164               <menuitem action='Bigger'/>
165               <menuitem action='Smaller'/>
166               <menuitem action='MoreOpaque'/>
167               <menuitem action='LessOpaque'/>
168               <separator/>
169               <menuitem action='Eraser'/>
170               <separator/>
171               <menuitem action='PickContext'/>
172             </menu>
173             <menu action='ColorMenu'>
174               <menuitem action='ColorSelectionWindow'/>
175               <menuitem action='ColorRingPopup'/>
176               <menuitem action='ColorChangerPopup'/>
177               <menuitem action='ColorPickerPopup'/>
178               <menuitem action='ColorHistoryPopup'/>
179               <separator/>
180               <menuitem action='Brighter'/>
181               <menuitem action='Darker'/>
182             </menu>
183             <menu action='LayerMenu'>
184               <menuitem action='BackgroundWindow'/>
185               <menuitem action='ClearLayer'/>
186               <separator/>
187               <menuitem action='NewLayerFG'/>
188               <menuitem action='NewLayerBG'/>
189               <menuitem action='RemoveLayer'/>
190               <menuitem action='MergeLayer'/>
191               <separator/>
192               <menuitem action='PickLayer'/>
193               <menuitem action='LayerFG'/>
194               <menuitem action='LayerBG'/>
195               <menuitem action='SoloLayer'/>
196               <menuitem action='ToggleAbove'/>
197               <separator/>
198               <menuitem action='IncreaseLayerOpacity'/>
199               <menuitem action='DecreaseLayerOpacity'/>
200             </menu>
201             <menu action='DebugMenu'>
202               <menuitem action='PrintInputs'/>
203               <menuitem action='VisualizeRendering'/>
204               <menuitem action='NoDoubleBuffereing'/>
205             </menu>
206             <menu action='HelpMenu'>
207               <menuitem action='Docu'/>
208               <menuitem action='ShortcutHelp'/>
209               <separator/>
210               <menuitem action='About'/>
211             </menu>
212           </menubar>
213         </ui>"""
214         actions = [
215                         # name, stock id, label, accelerator, tooltip, callback
216             ('FileMenu',     None, 'File'),
217             ('New',          None, 'New', '<control>N', None, self.new_cb),
218             ('Open',         None, 'Open...', '<control>O', None, self.open_cb),
219             ('OpenRecent',   None, 'Open Recent', 'F3', None, self.open_recent_cb),
220             ('Save',         None, 'Save', '<control>S', None, self.save_cb),
221             ('SaveAs',       None, 'Save As...', '<control><shift>S', None, self.save_as_cb),
222             ('SaveScrap',    None, 'Save Next Scrap', 'F2', None, self.save_scrap_cb),
223             ('Quit',         None, 'Quit', '<control>q', None, self.quit_cb),
224
225
226             ('EditMenu',           None, 'Edit'),
227             ('Undo',               None, 'Undo', 'Z', None, self.undo_cb),
228             ('Redo',               None, 'Redo', 'Y', None, self.redo_cb),
229             ('CopyLayer',          None, 'Copy Layer to Clipboard', '<control>C', None, self.copy_cb),
230             ('PasteLayer',         None, 'Paste Layer from Clipboard', '<control>V', None, self.paste_cb),
231
232             ('BrushMenu',    None, 'Brush'),
233             ('Brighter',     None, 'Brighter', None, None, self.brighter_cb),
234             ('Smaller',      None, 'Smaller', 'd', None, self.brush_smaller_cb),
235             ('MoreOpaque',   None, 'More Opaque', 's', None, self.more_opaque_cb),
236             ('LessOpaque',   None, 'Less Opaque', 'a', None, self.less_opaque_cb),
237             ('Eraser',       None, 'Toggle Eraser Mode', 'e', None, self.eraser_cb),
238             ('PickContext',  None, 'Pick Context (layer, brush and color)', 'w', None, self.pick_context_cb),
239
240             ('ColorMenu',    None, 'Color'),
241             ('Darker',       None, 'Darker', None, None, self.darker_cb),
242             ('Bigger',       None, 'Bigger', 'f', None, self.brush_bigger_cb),
243             ('ColorPickerPopup',    None, 'Pick Color', 'r', None, self.popup_cb),
244             ('ColorHistoryPopup',  None, 'Color History', 'x', None, self.popup_cb),
245             ('ColorChangerPopup', None, 'Color Changer', 'v', None, self.popup_cb),
246             ('ColorRingPopup',  None, 'Color Ring', None, None, self.popup_cb),
247
248             ('ContextMenu',  None, 'Brushkeys'),
249             ('Context00',    None, 'Restore Brush 0', '0', None, self.context_cb),
250             ('Context00s',   None, 'Save to Brush 0', '<control>0', None, self.context_cb),
251             ('Context01',    None, 'Restore 1', '1', None, self.context_cb),
252             ('Context01s',   None, 'Save 1', '<control>1', None, self.context_cb),
253             ('Context02',    None, 'Restore 2', '2', None, self.context_cb),
254             ('Context02s',   None, 'Save 2', '<control>2', None, self.context_cb),
255             ('Context03',    None, 'Restore 3', '3', None, self.context_cb),
256             ('Context03s',   None, 'Save 3', '<control>3', None, self.context_cb),
257             ('Context04',    None, 'Restore 4', '4', None, self.context_cb),
258             ('Context04s',   None, 'Save 4', '<control>4', None, self.context_cb),
259             ('Context05',    None, 'Restore 5', '5', None, self.context_cb),
260             ('Context05s',   None, 'Save 5', '<control>5', None, self.context_cb),
261             ('Context06',    None, 'Restore 6', '6', None, self.context_cb),
262             ('Context06s',   None, 'Save 6', '<control>6', None, self.context_cb),
263             ('Context07',    None, 'Restore 7', '7', None, self.context_cb),
264             ('Context07s',   None, 'Save 7', '<control>7', None, self.context_cb),
265             ('Context08',    None, 'Restore 8', '8', None, self.context_cb),
266             ('Context08s',   None, 'Save 8', '<control>8', None, self.context_cb),
267             ('Context09',    None, 'Restore 9', '9', None, self.context_cb),
268             ('Context09s',   None, 'Save 9', '<control>9', None, self.context_cb),
269             ('ContextStore', None, 'Save to Most Recently Restored', 'q', None, self.context_cb),
270             ('ContextHelp',  None, 'Help!', None, None, self.context_help_cb),
271
272             ('LayerMenu',    None, 'Layers'),
273
274             ('BackgroundWindow', None, 'Background...', None, None, self.toggleWindow_cb),
275             ('ClearLayer',   None, 'Clear', 'Delete', None, self.clear_layer_cb),
276             ('PickLayer',    None, 'Select Layer at Cursor', 'h', None, self.pick_layer_cb),
277             ('LayerFG',      None, 'Next (above current)',  'Page_Up', None, self.layer_fg_cb),
278             ('LayerBG',      None, 'Next (below current)', 'Page_Down', None, self.layer_bg_cb),
279             ('NewLayerFG',   None, 'New (above current)', '<control>Page_Up', None, self.new_layer_cb),
280             ('NewLayerBG',   None, 'New (below current)', '<control>Page_Down', None, self.new_layer_cb),
281             ('MergeLayer',   None, 'Merge Down', '<control>Delete', None, self.merge_layer_cb),
282             ('RemoveLayer',  None, 'Remove', '<shift>Delete', None, self.remove_layer_cb),
283             ('SoloLayer',    None, 'Toggle Solo Mode', 'Home', None, self.solo_layer_cb),
284             ('ToggleAbove',  None, 'Toggle Layers Above Current', 'End', None, self.toggle_layers_above_cb), # TODO: make toggle action
285             ('IncreaseLayerOpacity', None, 'Increase Layer Opacity',  'p', None, self.layer_increase_opacity),
286             ('DecreaseLayerOpacity', None, 'Decrease Layer Opacity',  'o', None, self.layer_decrease_opacity),
287
288             ('BrushSelectionWindow',  None, 'Brush List...', 'b', None, self.toggleWindow_cb),
289             ('BrushSettingsWindow',   None, 'Brush Settings...', '<control>b', None, self.toggleWindow_cb),
290             ('ColorSelectionWindow',  None, 'Color Triangle...', 'g', None, self.toggleWindow_cb),
291             ('SettingsWindow',        None, 'Settings...', None, None, self.toggleWindow_cb),
292
293             ('HelpMenu',     None, 'Help'),
294             ('Docu', None, 'Where is the Documentation?', None, None, self.show_docu_cb),
295             ('ShortcutHelp',  None, 'Change the Keyboard Shortcuts?', None, None, self.shortcut_help_cb),
296             ('About', None, 'About MyPaint', None, None, self.show_about_cb),
297
298             ('DebugMenu',    None, 'Debug'),
299
300
301             ('ShortcutsMenu', None, 'Shortcuts'),
302
303             ('ViewMenu', None, 'View'),
304             ('Fullscreen',   None, 'Fullscreen', 'F11', None, self.fullscreen_cb),
305             ('ResetView',   None, 'Reset (Zoom, Rotation, Mirror)', None, None, self.reset_view_cb),
306             ('ZoomOut',      None, 'Zoom Out (at cursor)', 'comma', None, self.zoom_cb),
307             ('ZoomIn',       None, 'Zoom In', 'period', None, self.zoom_cb),
308             ('RotateLeft',   None, 'Rotate Counterclockwise', None, None, self.rotate_cb),
309             ('RotateRight',  None, 'Rotate Clockwise', None, None, self.rotate_cb),
310             ('ViewHelp',     None, 'Help', None, None, self.view_help_cb),
311             ]
312         ag.add_actions(actions)
313         toggle_actions = [
314             # name, stock id, label, accelerator, tooltip, callback, default toggle status
315             ('PrintInputs', None, 'Print Brush Input Values to stdout', None, None, self.print_inputs_cb),
316             ('VisualizeRendering', None, 'Visualize Rendering', None, None, self.visualize_rendering_cb),
317             ('NoDoubleBuffereing', None, 'Disable GTK Double Buffering', None, None, self.no_double_buffering_cb),
318             ('Flip', None, 'Mirror Image', 'i', None, self.flip_cb),
319             ]
320         ag.add_toggle_actions(toggle_actions)
321         self.ui = gtk.UIManager()
322         self.ui.insert_action_group(ag, 0)
323         self.ui.add_ui_from_string(ui_string)
324         #self.app.accel_group = self.ui.get_accel_group()
325
326         self.app.kbm = kbm = keyboard.KeyboardManager()
327         kbm.add_window(self)
328
329         for action in ag.list_actions():
330             self.app.kbm.takeover_action(action)
331
332         kbm.add_extra_key('<control>z', 'Undo')
333         kbm.add_extra_key('<control>y', 'Redo')
334         kbm.add_extra_key('KP_Add', 'ZoomIn')
335         kbm.add_extra_key('KP_Subtract', 'ZoomOut')
336
337         kbm.add_extra_key('Left', lambda(action): self.move('MoveLeft'))
338         kbm.add_extra_key('Right', lambda(action): self.move('MoveRight'))
339         kbm.add_extra_key('Down', lambda(action): self.move('MoveDown'))
340         kbm.add_extra_key('Up', lambda(action): self.move('MoveUp'))
341
342         kbm.add_extra_key('<control>Left', 'RotateLeft')
343         kbm.add_extra_key('<control>Right', 'RotateRight')
344
345         sg = stategroup.StateGroup()
346         self.layerblink_state = sg.create_state(self.layerblink_state_enter, self.layerblink_state_leave)
347
348         # separate stategroup...
349         sg2 = stategroup.StateGroup()
350         self.layersolo_state = sg2.create_state(self.layersolo_state_enter, self.layersolo_state_leave)
351         self.layersolo_state.autoleave_timeout = None
352
353         p2s = sg.create_popup_state
354         changer = p2s(colorselectionwindow.ColorChangerPopup(self.app))
355         ring = p2s(colorselectionwindow.ColorRingPopup(self.app))
356         hist = p2s(historypopup.HistoryPopup(self.app, self.doc))
357         pick = self.colorpick_state = p2s(colorpicker.ColorPicker(self.app, self.doc))
358
359         self.popup_states = {
360             'ColorChangerPopup': changer,
361             'ColorRingPopup': ring,
362             'ColorHistoryPopup': hist,
363             'ColorPickerPopup': pick,
364             }
365         changer.next_state = ring
366         ring.next_state = changer
367         changer.autoleave_timeout = None
368         ring.autoleave_timeout = None
369
370         pick.max_key_hit_duration = 0.0
371         pick.autoleave_timeout = None
372
373         hist.autoleave_timeout = 0.600
374         self.history_popup_state = hist
375
376     def with_wait_cursor(func):
377         """python decorator that adds a wait cursor around a function"""
378         def wrapper(self, *args, **kwargs):
379             self.window.set_cursor(gdk.Cursor(gdk.WATCH))
380             self.tdw.window.set_cursor(None)
381             # make sure it is actually changed before we return
382             while gtk.events_pending():
383                 gtk.main_iteration(False)
384             try:
385                 func(self, *args, **kwargs)
386             finally:
387                 self.window.set_cursor(None)
388                 self.tdw.update_cursor()
389         return wrapper
390
391     def toggleWindow_cb(self, action):
392         s = action.get_name()
393         s = s[0].lower() + s[1:]
394         w = getattr(self.app, s)
395         if w.window and w.window.is_visible():
396             w.hide()
397         else:
398             w.show_all() # might be for the first time
399             w.present()
400
401     def print_inputs_cb(self, action):
402         self.doc.brush.print_inputs = action.get_active()
403
404     def visualize_rendering_cb(self, action):
405         self.tdw.visualize_rendering = action.get_active()
406     def no_double_buffering_cb(self, action):
407         self.tdw.set_double_buffered(not action.get_active())
408
409     def start_profiling(self):
410         def autopaint():
411             import pylab
412             events = pylab.load('painting30sec.dat.gz')
413             events[:,0] *= 0.3
414             events = list(events)
415             t0 = time()
416             t_old = 0.0
417             for t, x, y, pressure in events:
418                 sleeptime = t-(time()-t0)
419                 if sleeptime > 0.001:
420                     yield sleeptime
421                 dtime = t - t_old
422                 t_old = t
423                 self.doc.stroke_to(dtime, x, y, pressure)
424             print 'replay done.'
425             print self.repaints, 'repaints'
426             gtk.main_quit()
427             yield 10.0
428
429         import gobject
430         p = autopaint()
431         def timer_cb():
432             gobject.timeout_add(int(p.next()*1000.0), timer_cb)
433
434         self.repaints = 0
435         oldfunc=self.tdw.repaint
436         def count_repaints(*args, **kwargs):
437             self.repaints += 1
438             return oldfunc(*args, **kwargs)
439         self.tdw.repaint = count_repaints
440         timer_cb()
441
442         self.tdw.rotate(46.0/360*2*math.pi)
443         
444     def undo_cb(self, action):
445         self.doc.undo()
446
447     def redo_cb(self, action):
448         self.doc.redo()
449
450     def copy_cb(self, action):
451         # use the full document bbox, so we can past layers back to the correct position
452         bbox = self.doc.get_bbox()
453         pixbuf = self.doc.layer.surface.render_as_pixbuf(*bbox)
454         cb = gtk.Clipboard()
455         cb.set_image(pixbuf)
456
457     def paste_cb(self, action):
458         cb = gtk.Clipboard()
459         def callback(clipboard, pixbuf, trash):
460             if not pixbuf:
461                 print 'The clipboard doeas not contain any image to paste!'
462                 return
463             # paste to the upper left of our doc bbox (see above)
464             x, y, w, h = self.doc.get_bbox()
465             self.doc.load_layer_from_pixbuf(pixbuf, x, y)
466         cb.request_image(callback)
467
468     def brush_modified_cb(self):
469         # called at every brush setting modification, should return fast
470         self.doc.set_brush(self.app.brush)
471
472     def key_press_event_cb_before(self, win, event):
473         key = event.keyval 
474         ctrl = event.state & gdk.CONTROL_MASK
475         #ANY_MODIFIER = gdk.SHIFT_MASK | gdk.MOD1_MASK | gdk.CONTROL_MASK
476         #if event.state & ANY_MODIFIER:
477         #    # allow user shortcuts with modifiers
478         #    return False
479         if key == keysyms.space:
480             if ctrl:
481                 self.tdw.start_drag(self.dragfunc_rotate)
482             else:
483                 self.tdw.start_drag(self.dragfunc_translate)
484         else: return False
485         return True
486     def key_release_event_cb_before(self, win, event):
487         if event.keyval == keysyms.space:
488             self.tdw.stop_drag(self.dragfunc_translate)
489             self.tdw.stop_drag(self.dragfunc_rotate)
490             return True
491         return False
492
493     def key_press_event_cb_after(self, win, event):
494         key = event.keyval
495         if self.fullscreen and key == keysyms.Escape: self.fullscreen_cb()
496         else: return False
497         return True
498     def key_release_event_cb_after(self, win, event):
499         return False
500
501     def dragfunc_translate(self, dx, dy):
502         self.tdw.scroll(-dx, -dy)
503
504     def dragfunc_rotate(self, dx, dy):
505         self.tdw.scroll(-dx, -dy, False)
506         self.tdw.rotate(2*math.pi*dx/500.0)
507
508     #def dragfunc_rotozoom(self, dx, dy):
509     #    self.tdw.scroll(-dx, -dy, False)
510     #    self.tdw.zoom(math.exp(-dy/100.0))
511     #    self.tdw.rotate(2*math.pi*dx/500.0)
512
513     def button_press_cb(self, win, event):
514         #print event.device, event.button
515         if event.type != gdk.BUTTON_PRESS:
516             # ignore the extra double-click event
517             return
518         if event.button == 2:
519             # check whether we are painting (accidental)
520             pressure = event.get_axis(gdk.AXIS_PRESSURE)
521             if (event.state & gdk.BUTTON1_MASK) or pressure:
522                 # do not allow dragging while painting (often happens accidentally)
523                 pass
524             else:
525                 self.tdw.start_drag(self.dragfunc_translate)
526         elif event.button == 1:
527             if event.state & gdk.CONTROL_MASK:
528                 self.end_eraser_mode()
529                 self.colorpick_state.activate(event)
530         elif event.button == 3:
531             self.history_popup_state.activate(event)
532
533     def button_release_cb(self, win, event):
534         #print event.device, event.button
535         if event.button == 2:
536             self.tdw.stop_drag(self.dragfunc_translate)
537         # too slow to be useful:
538         #elif event.button == 3:
539         #    self.tdw.stop_drag(self.dragfunc_rotate)
540
541     def scroll_cb(self, win, event):
542         d = event.direction
543         if d == gdk.SCROLL_UP:
544             if event.state & gdk.SHIFT_MASK:
545                 self.rotate('RotateLeft')
546             else:
547                 self.zoom('ZoomIn')
548         elif d == gdk.SCROLL_DOWN:
549             if event.state & gdk.SHIFT_MASK:
550                 self.rotate('RotateRight')
551             else:
552                 self.zoom('ZoomOut')
553         elif d == gdk.SCROLL_LEFT:
554             self.rotate('RotateRight')
555         elif d == gdk.SCROLL_LEFT:
556             self.rotate('RotateLeft')
557
558     def clear_layer_cb(self, action):
559         self.doc.clear_layer()
560         if len(self.doc.layers) == 1:
561             # this is like creating a new document:
562             # make "save next" use a new file name
563             self.filename = None
564         
565     def remove_layer_cb(self, action):
566         if len(self.doc.layers) == 1:
567             self.doc.clear_layer()
568         else:
569             self.doc.remove_layer()
570             self.layerblink_state.activate(action)
571
572     def layer_bg_cb(self, action):
573         idx = self.doc.layer_idx - 1
574         if idx < 0:
575             return
576         self.doc.select_layer(idx)
577         self.layerblink_state.activate(action)
578
579     def layer_fg_cb(self, action):
580         idx = self.doc.layer_idx + 1
581         if idx >= len(self.doc.layers):
582             return
583         self.doc.select_layer(idx)
584         self.layerblink_state.activate(action)
585
586     def pick_layer_cb(self, action):
587         x, y = self.tdw.get_cursor_in_model_coordinates()
588         for idx, layer in reversed(list(enumerate(self.doc.layers))):
589             alpha = layer.surface.get_alpha (x, y, 5) * layer.opacity
590             if alpha > 0.1:
591                 self.doc.select_layer(idx)
592                 self.layerblink_state.activate(action)
593                 return
594         self.doc.select_layer(0)
595         self.layerblink_state.activate(action)
596
597     def pick_context_cb(self, action):
598         x, y = self.tdw.get_cursor_in_model_coordinates()
599         for idx, layer in reversed(list(enumerate(self.doc.layers))):
600             alpha = layer.surface.get_alpha (x, y, 5) * layer.opacity
601             if alpha > 0.1:
602                 old_layer = self.doc.layer
603                 self.doc.select_layer(idx)
604                 if self.doc.layer != old_layer:
605                     self.layerblink_state.activate(action)
606
607                 # find the most recent (last) stroke that touches our picking point
608                 brush = self.doc.layer.get_brush_at(x, y)
609
610                 if brush:
611                     # FIXME: clean brush concept?
612                     self.app.brush.load_from_string(brush)
613                     self.app.select_brush(None)
614                 else:
615                     print 'Nothing found!'
616
617                 #self.app.brush.copy_settings_from(stroke.brush_settings)
618                 #self.app.select_brush()
619                     
620                 return
621         self.doc.select_layer(0)
622         self.layerblink_state.activate(action)
623
624     def layerblink_state_enter(self):
625         self.tdw.current_layer_solo = True
626         self.tdw.queue_draw()
627     def layerblink_state_leave(self, reason):
628         if self.layersolo_state.active:
629             # FIXME: use state machine concept, maybe?
630             return
631         self.tdw.current_layer_solo = False
632         self.tdw.queue_draw()
633     def layersolo_state_enter(self):
634         s = self.layerblink_state
635         if s.active:
636             s.leave()
637         self.tdw.current_layer_solo = True
638         self.tdw.queue_draw()
639     def layersolo_state_leave(self, reason):
640         self.tdw.current_layer_solo = False
641         self.tdw.queue_draw()
642
643     #def blink_layer_cb(self, action):
644     #    self.layerblink_state.activate(action)
645
646     def solo_layer_cb(self, action):
647         self.layersolo_state.toggle(action)
648
649     def new_layer_cb(self, action):
650         insert_idx = self.doc.layer_idx
651         if action.get_name() == 'NewLayerFG':
652             insert_idx += 1
653         self.doc.add_layer(insert_idx)
654         self.layerblink_state.activate(action)
655
656     @with_wait_cursor
657     def merge_layer_cb(self, action):
658         dst_idx = self.doc.layer_idx - 1
659         if dst_idx < 0:
660             return
661         self.doc.merge_layer(dst_idx)
662         self.layerblink_state.activate(action)
663
664     def toggle_layers_above_cb(self, action):
665         self.tdw.toggle_show_layers_above()
666
667     def popup_cb(self, action):
668         # This doesn't really belong here...
669         # just because all popups are color popups now...
670         # ...maybe should eraser_mode be a GUI state too?
671         self.end_eraser_mode()
672
673         state = self.popup_states[action.get_name()]
674         state.activate(action)
675
676     def eraser_cb(self, action):
677         adj = self.app.brush_adjustment['eraser']
678         if adj.get_value() > 0.9:
679             self.end_eraser_mode()
680         else:
681             # enter eraser mode
682             adj.set_value(1.0)
683             adj2 = self.app.brush_adjustment['radius_logarithmic']
684             r = adj2.get_value()
685             self.eraser_mode_original_radius = r
686             adj2.set_value(r + self.eraser_mode_radius_change)
687
688     def end_eraser_mode(self):
689         adj = self.app.brush_adjustment['eraser']
690         if not adj.get_value() > 0.9:
691             return
692         adj.set_value(0.0)
693         if self.eraser_mode_original_radius:
694             # save eraser radius, restore old radius
695             adj2 = self.app.brush_adjustment['radius_logarithmic']
696             r = adj2.get_value()
697             self.eraser_mode_radius_change = r - self.eraser_mode_original_radius
698             adj2.set_value(self.eraser_mode_original_radius)
699             self.eraser_mode_original_radius = None
700
701     def device_changed_cb(self, old_device, new_device):
702         # just enable eraser mode for now (TODO: remember full tool settings)
703         # small problem with this code: it doesn't work well with brushes that have (eraser not in [1.0, 0.0])
704         adj = self.app.brush_adjustment['eraser']
705         if old_device is None and new_device.source != gdk.SOURCE_ERASER:
706             # keep whatever startup brush was choosen
707             return
708         if new_device.source == gdk.SOURCE_ERASER:
709             # enter eraser mode
710             adj.set_value(1.0)
711         elif new_device.source != gdk.SOURCE_ERASER and \
712                (old_device is None or old_device.source == gdk.SOURCE_ERASER):
713             # leave eraser mode
714             adj.set_value(0.0)
715         print 'device change:', new_device.name
716
717     def brush_bigger_cb(self, action):
718         adj = self.app.brush_adjustment['radius_logarithmic']
719         adj.set_value(adj.get_value() + 0.3)
720     def brush_smaller_cb(self, action):
721         adj = self.app.brush_adjustment['radius_logarithmic']
722         adj.set_value(adj.get_value() - 0.3)
723
724     def more_opaque_cb(self, action):
725         # FIXME: hm, looks this slider should be logarithmic?
726         adj = self.app.brush_adjustment['opaque']
727         adj.set_value(adj.get_value() * 1.8)
728     def less_opaque_cb(self, action):
729         adj = self.app.brush_adjustment['opaque']
730         adj.set_value(adj.get_value() / 1.8)
731
732     def brighter_cb(self, action):
733         self.end_eraser_mode()
734         h, s, v = self.app.brush.get_color_hsv()
735         v += 0.08
736         if v > 1.0: v = 1.0
737         self.app.brush.set_color_hsv((h, s, v))
738     def darker_cb(self, action):
739         self.end_eraser_mode()
740         h, s, v = self.app.brush.get_color_hsv()
741         v -= 0.08
742         if v < 0.0: v = 0.0
743         self.app.brush.set_color_hsv((h, s, v))
744
745     def layer_increase_opacity(self, action):
746         self.doc.layer.opacity = helpers.clamp(self.doc.layer.opacity + 0.08, 0.0, 1.0)
747         self.tdw.queue_draw()
748
749     def layer_decrease_opacity(self, action):
750         self.doc.layer.opacity = helpers.clamp(self.doc.layer.opacity - 0.08, 0.0, 1.0)
751         self.tdw.queue_draw()
752         
753     @with_wait_cursor
754     def open_file(self, filename):
755         try:
756             self.doc.load(filename)
757         except document.SaveLoadError, e:
758             d = gtk.MessageDialog(self, type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK)
759             d.set_markup(str(e))
760             d.run()
761             d.destroy()
762         else:
763             self.filename = os.path.abspath(filename)
764             print 'Loaded from', self.filename
765             self.reset_view_cb(None)
766             self.tdw.recenter_document()
767
768     @with_wait_cursor
769     def save_file(self, filename, **options):
770         try:
771             x, y, w, h =  self.doc.get_bbox()
772             if w == 0 and h == 0:
773                 raise document.SaveLoadError, 'Did not save, the canvas is empty.'
774             self.doc.save(filename, **options)
775         except document.SaveLoadError, e:
776             d = gtk.MessageDialog(self, type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK)
777             d.set_markup(str(e))
778             d.run()
779             d.destroy()
780         else:
781             self.filename = os.path.abspath(filename)
782             print 'Saved to', self.filename
783             self.save_history.append(os.path.abspath(filename))
784             self.save_history = self.save_history[-100:]
785             f = open(os.path.join(self.app.confpath, 'save_history.conf'), 'w')
786             f.write('\n'.join(self.save_history))
787             ## tell other gtk applications
788             ## (hm, is there any application that uses this at all? or is the code below wrong?)
789             #manager = gtk.recent_manager_get_default()
790             #manager.add_item(os.path.abspath(filename))
791
792     def confirm_destructive_action(self, title='Confirm', question='Really continue?'):
793         t = self.doc.unsaved_painting_time
794         if t < 30:
795             # no need to ask
796             return True
797
798         if t > 120:
799             t = '%d minutes' % (t/60)
800         else:
801             t = '%d seconds' % t
802         d = gtk.Dialog(title, 
803                        self,
804                        gtk.DIALOG_MODAL,
805                        (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
806                         gtk.STOCK_DISCARD, gtk.RESPONSE_OK))
807         d.set_has_separator(False)
808         d.set_default_response(gtk.RESPONSE_OK)
809         l = gtk.Label()
810         l.set_markup("<b>" + question + "</b>\n\nThis will discard %s of unsaved painting." % t)
811         l.set_padding(10, 10)
812         l.show()
813         d.vbox.pack_start(l)
814         response = d.run()
815         d.destroy()
816         return response == gtk.RESPONSE_OK
817
818     def new_cb(self, action):
819         if not self.confirm_destructive_action():
820             return
821         bg = self.doc.background
822         self.doc.clear()
823         self.doc.set_background(bg)
824         self.filename = None
825
826     def open_cb(self, action):
827         if not self.confirm_destructive_action():
828             return
829         dialog = gtk.FileChooserDialog("Open..", self,
830                                        gtk.FILE_CHOOSER_ACTION_OPEN,
831                                        (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
832                                         gtk.STOCK_OPEN, gtk.RESPONSE_OK))
833         dialog.set_default_response(gtk.RESPONSE_OK)
834
835         f = gtk.FileFilter()
836         f.set_name("All Recognized Formats")
837         f.add_pattern("*.ora")
838         f.add_pattern("*.png")
839         f.add_pattern("*.jpg")
840         f.add_pattern("*.jpeg")
841         dialog.add_filter(f)
842
843         f = gtk.FileFilter()
844         f.set_name("OpenRaster (*.ora)")
845         f.add_pattern("*.ora")
846         dialog.add_filter(f)
847
848         f = gtk.FileFilter()
849         f.set_name("PNG (*.png)")
850         f.add_pattern("*.png")
851         dialog.add_filter(f)
852
853         f = gtk.FileFilter()
854         f.set_name("JPEG (*.jpg; *.jpeg)")
855         f.add_pattern("*.jpg")
856         f.add_pattern("*.jpeg")
857         dialog.add_filter(f)
858
859         if self.filename:
860             dialog.set_filename(self.filename)
861         try:
862             if dialog.run() == gtk.RESPONSE_OK:
863                 self.open_file(dialog.get_filename())
864         finally:
865             dialog.destroy()
866         
867     def save_cb(self, action):
868         if not self.filename:
869             self.save_as_cb(action)
870         else:
871             self.save_file(self.filename)
872
873
874     def init_save_dialog(self):
875         dialog = gtk.FileChooserDialog("Save..", self,
876                                        gtk.FILE_CHOOSER_ACTION_SAVE,
877                                        (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
878                                         gtk.STOCK_SAVE, gtk.RESPONSE_OK))
879         self.save_dialog = dialog
880         dialog.set_default_response(gtk.RESPONSE_OK)
881         dialog.set_do_overwrite_confirmation(True)
882
883         filter2info = {}
884         self.filter2info = filter2info
885
886         f = gtk.FileFilter()
887         filter2info[f] = ('.ora', {})
888         f.set_name("Any format (prefer OpenRaster)")
889         self.save_filter_default = f
890
891         f.add_pattern("*.png")
892         f.add_pattern("*.ora")
893         f.add_pattern("*.jpg")
894         f.add_pattern("*.jpeg")
895         dialog.add_filter(f)
896
897         f = gtk.FileFilter()
898         filter2info[f] = ('.ora', {})
899         f.set_name("OpenRaster (*.ora)")
900         f.add_pattern("*.ora")
901         dialog.add_filter(f)
902
903         f = gtk.FileFilter()
904         filter2info[f] = ('.png', {'alpha': False})
905         f.set_name("PNG solid with background (*.png)")
906         f.add_pattern("*.png")
907         dialog.add_filter(f)
908
909         f = gtk.FileFilter()
910         filter2info[f] = ('.png', {'alpha': True})
911         f.set_name("PNG transparent (*.png)")
912         f.add_pattern("*.png")
913         dialog.add_filter(f)
914
915         f = gtk.FileFilter()
916         filter2info[f] = ('.jpg', {'quality': 90})
917         f.set_name("JPEG 90% quality (*.jpg; *.jpeg)")
918         f.add_pattern("*.jpg")
919         f.add_pattern("*.jpeg")
920         dialog.add_filter(f)
921
922     def save_as_cb(self, action):
923         dialog = self.save_dialog
924
925         def dialog_set_filename(s):
926             # According to pygtk docu we should use set_filename(),
927             # however doing so removes the selected filefilter.
928             path, name = os.path.split(s)
929             dialog.set_current_folder(path)
930             dialog.set_current_name(name)
931
932         if self.filename:
933             dialog_set_filename(self.filename)
934         else:
935             dialog_set_filename('')
936             dialog.set_filter(self.save_filter_default)
937
938         try:
939             while dialog.run() == gtk.RESPONSE_OK:
940
941                 filename = dialog.get_filename()
942                 name, ext = os.path.splitext(filename)
943                 ext_filter, options = self.filter2info.get(dialog.get_filter(), ('ora', {}))
944
945                 if ext:
946                     if ext_filter != ext:
947                         # Minor ugliness: if the user types '.png' but
948                         # leaves the default .ora filter selected, we
949                         # use the default options instead of those
950                         # above. However, they are the same at the moment.
951                         options = {}
952                     assert(filename)
953                     self.save_file(filename, **options)
954                     break
955                 
956                 # add proper extension
957                 filename = name + ext_filter
958
959                 # trigger overwrite confirmation for the modified filename
960                 dialog_set_filename(filename)
961                 dialog.response(gtk.RESPONSE_OK)
962
963         finally:
964             dialog.hide()
965
966     def save_scrap_cb(self, action):
967         filename = self.filename
968         prefix = self.app.settingsWindow.save_scrap_prefix
969
970         number = None
971         if filename:
972             l = re.findall(re.escape(prefix) + '([0-9]+)', filename)
973             if l:
974                 number = l[0]
975
976         if number:
977             # reuse the number, find the next character
978             char = 'a'
979             for filename in glob(prefix + number + '_*'):
980                 c = filename[len(prefix + number + '_')]
981                 if c >= 'a' and c <= 'z' and c >= char:
982                     char = chr(ord(c)+1)
983             if char > 'z':
984                 # out of characters, increase the number
985                 self.filename = None
986                 return self.save_scrap_cb(action)
987             filename = '%s%s_%c' % (prefix, number, char)
988         else:
989             # we don't have a scrap filename yet, find the next number
990             maximum = 0
991             for filename in glob(prefix + '[0-9][0-9][0-9]*'):
992                 filename = filename[len(prefix):]
993                 res = re.findall(r'[0-9]*', filename)
994                 if not res: continue
995                 number = int(res[0])
996                 if number > maximum:
997                     maximum = number
998             filename = '%s%03d_a' % (prefix, maximum+1)
999
1000         #if self.doc.is_layered():
1001         #    filename += '.ora'
1002         #else:
1003         #    filename += '.png'
1004         filename += '.ora'
1005
1006         assert not os.path.exists(filename)
1007         self.save_file(filename)
1008
1009     def open_recent_cb(self, action):
1010         # feed history with scrap directory (mainly for initial history)
1011         prefix = self.app.settingsWindow.save_scrap_prefix
1012         prefix = os.path.abspath(prefix)
1013         l = glob(prefix + '*.png') + glob(prefix + '*.ora') + glob(prefix + '*.jpg') + glob(prefix + '*.jpeg')
1014         l = [x for x in l if x not in self.save_history]
1015         l = l + self.save_history
1016         l = [x for x in l if os.path.exists(x)]
1017         l.sort(key=os.path.getmtime)
1018         self.save_history = l
1019
1020         # pick the next most recent file from the history
1021         idx = -1
1022         if self.filename in self.save_history:
1023             def basename(filename):
1024                 return os.path.splitext(filename)[0]
1025             idx = self.save_history.index(self.filename)
1026             while basename(self.save_history[idx]) == basename(self.filename):
1027                 idx -= 1
1028                 if idx == -1:
1029                     return
1030
1031         if not self.confirm_destructive_action():
1032             return
1033         self.open_file(self.save_history[idx])
1034
1035     def quit_cb(self, *trash):
1036         self.doc.split_stroke()
1037         self.app.save_gui_config() # FIXME: should do this periodically, not only on quit
1038
1039         if not self.confirm_destructive_action(title='Quit', question='Really Quit?'):
1040             return True
1041
1042         gtk.main_quit()
1043         return False
1044
1045     def zoom_cb(self, action):
1046         self.zoom(action.get_name())
1047     def rotate_cb(self, action):
1048         self.rotate(action.get_name())
1049     def flip_cb(self, action):
1050         self.tdw.set_flipped(action.get_active())
1051
1052     def move(self, command):
1053         self.doc.split_stroke()
1054         step = min(self.tdw.window.get_size()) / 5
1055         if   command == 'MoveLeft' : self.tdw.scroll(-step, 0)
1056         elif command == 'MoveRight': self.tdw.scroll(+step, 0)
1057         elif command == 'MoveUp'   : self.tdw.scroll(0, -step)
1058         elif command == 'MoveDown' : self.tdw.scroll(0, +step)
1059         else: assert 0
1060
1061     def zoom(self, command):
1062         if   command == 'ZoomIn' : self.zoomlevel += 1
1063         elif command == 'ZoomOut': self.zoomlevel -= 1
1064         else: assert 0
1065         if self.zoomlevel < 0: self.zoomlevel = 0
1066         if self.zoomlevel >= len(self.zoomlevel_values): self.zoomlevel = len(self.zoomlevel_values) - 1
1067         z = self.zoomlevel_values[self.zoomlevel]
1068         self.tdw.set_zoom(z)
1069
1070     def rotate(self, command):
1071         if   command == 'RotateRight': self.tdw.rotate(+2*math.pi/14)
1072         elif command == 'RotateLeft' : self.tdw.rotate(-2*math.pi/14)
1073         else: assert 0
1074
1075     def reset_view_cb(self, command):
1076         self.tdw.set_rotation(0.0)
1077         self.zoomlevel = self.zoomlevel_values.index(1.0)
1078         self.tdw.set_zoom(1.0)
1079         self.tdw.set_zoom(1.0)
1080         self.tdw.set_flipped(False)
1081         self.action_group.get_action('Flip').set_active(False)
1082
1083     def fullscreen_cb(self, *trash):
1084         # note: there is some ugly flickering when toggling fullscreen
1085         #       self.window.begin_paint/end_paint does not help against it
1086         self.fullscreen = not self.fullscreen
1087         if self.fullscreen:
1088             x, y = self.get_position()
1089             w, h = self.get_size()
1090             self.geometry_before_fullscreen = (x, y, w, h)
1091             self.menubar.hide()
1092             self.window.fullscreen()
1093             #self.tdw.set_scroll_at_edges(True)
1094         else:
1095             self.window.unfullscreen()
1096             self.menubar.show()
1097             #self.tdw.set_scroll_at_edges(False)
1098             del self.geometry_before_fullscreen
1099
1100     def context_cb(self, action):
1101         # TODO: this context-thing is not very useful like that, is it?
1102         #       You overwrite your settings too easy by accident.
1103         # - not storing settings under certain circumstances?
1104         # - think about other stuff... brush history, only those actually used, etc...
1105         name = action.get_name()
1106         store = False
1107         if name == 'ContextStore':
1108             context = self.app.selected_context
1109             if not context:
1110                 print 'No context was selected, ignoring store command.'
1111                 return
1112             store = True
1113         else:
1114             if name.endswith('s'):
1115                 store = True
1116                 name = name[:-1]
1117             i = int(name[-2:])
1118             context = self.app.contexts[i]
1119         self.app.selected_context = context
1120         if store:
1121             context.copy_settings_from(self.app.brush)
1122             preview = self.app.brushSelectionWindow.get_preview_pixbuf()
1123             context.update_preview(preview)
1124             context.save()
1125         else: # restore
1126             self.app.select_brush(context)
1127             self.app.brushSelectionWindow.set_preview_pixbuf(context.preview)
1128
1129     def show_about_cb(self, action):
1130         d = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK)
1131
1132         d.set_markup(
1133             u"MyPaint %s - pressure sensitive painting application\n"
1134             u"Copyright (C) 2005-2009\n"
1135             u"Martin Renold &lt;martinxyz@gmx.ch&gt;\n\n"
1136             u"Contributors:\n"
1137             u"Artis Rozentāls &lt;artis@aaa.apollo.lv&gt; (brushes)\n"
1138             u"Yves Combe &lt;yves@ycombe.net&gt; (portability)\n"
1139             u"Sebastian Kraft (desktop icon)\n"
1140             u"Popolon &lt;popolon@popolon.org&gt; (brushes)\n"
1141             u"Clement Skau &lt;clementskau@gmail.com&gt; (programming)\n"
1142             u'Marcelo "Tanda" Cerviño &lt;info@lodetanda.com.ar&gt; (patterns, brushes)\n'
1143             u'Jon Nordby &lt;jononor@gmail.com&gt; (programming)\n'
1144             u"\n"
1145             u"This program is free software; you can redistribute it and/or modify "
1146             u"it under the terms of the GNU General Public License as published by "
1147             u"the Free Software Foundation; either version 2 of the License, or "
1148             u"(at your option) any later version.\n"
1149             u"\n"
1150             u"This program is distributed in the hope that it will be useful, "
1151             u"but WITHOUT ANY WARRANTY. See the COPYING file for more details."
1152             % MYPAINT_VERSION
1153             )
1154
1155         d.run()
1156         d.destroy()
1157
1158     def show_docu_cb(self, action):
1159         d = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK)
1160         d.set_markup("There is a tutorial available "
1161                      "on the MyPaint homepage. It explains some features which are "
1162                      "hard to discover yourself.\n\n"
1163                      "Comments about the brush settings (opaque, hardness, etc.) and "
1164                      "inputs (pressure, speed, etc.) are available as tooltips. "
1165                      "Put your mouse over a label to see them. "
1166                      "\n"
1167                      )
1168         d.run()
1169         d.destroy()
1170
1171     def context_help_cb(self, action):
1172         d = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK)
1173         d.set_markup("This is used to quickly save/restore brush settings "
1174                      "using keyboard shortcuts. You can paint with one hand and "
1175                      "change brushes with the other without interrupting."
1176                      "\n\n"
1177                      "There are 10 memory slots to hold brush settings.\n"
1178                      "Those are annonymous "
1179                      "brushes, they are not visible in the brush selector list. "
1180                      "But they will stay even if you quit. "
1181                      "They will also remember the selected color. In contrast, selecting a "
1182                      "normal brush never changes the color. "
1183                      )
1184         d.run()
1185         d.destroy()
1186
1187     def shortcut_help_cb(self, action):
1188         d = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK)
1189         d.set_markup("Move your mouse over a menu entry, then press the key to "
1190                      "assign.")
1191         d.run()
1192         d.destroy()
1193
1194     def view_help_cb(self, action):
1195         d = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK)
1196         d.set_markup(
1197             "You can also drag the canvas with the mouse while holding the middle mouse button or spacebar. "
1198             "or with the arrow keys."
1199             "\n\n"
1200             "In contrast to earlier versions, scrolling and zooming are harmless now and "
1201             "will not make you run out of memory. But you still require a lot of memory "
1202             "if you paint all over while fully zoomed out."
1203             )
1204         d.run()
1205         d.destroy()
1206