OSDN Git Service

fb16aa038f4539cf8ff47ddb8fee6a59441e7944
[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.8.0"
17
18 import os, math
19 from gettext import gettext as _
20
21 import gtk
22 from gtk import gdk, keysyms
23
24 import tileddrawwidget, colorselectionwindow, historypopup, \
25        stategroup, colorpicker
26 from lib import document, helpers, backgroundsurface, command, layer
27
28 #TODO: make generic by taking the windows as arguments and put in a helper file?
29 def with_wait_cursor(func):
30     """python decorator that adds a wait cursor around a function"""
31     def wrapper(self, *args, **kwargs):
32         # process events which might include cursor changes
33         while gtk.events_pending():
34             gtk.main_iteration(False)
35         self.app.drawWindow.window.set_cursor(gdk.Cursor(gdk.WATCH))
36         self.app.drawWindow.tdw.window.set_cursor(None)
37         # make sure it is actually changed before we return
38         while gtk.events_pending():
39             gtk.main_iteration(False)
40         try:
41             func(self, *args, **kwargs)
42         finally:
43             self.app.drawWindow.window.set_cursor(None)
44             self.app.drawWindow.tdw.update_cursor()
45     return wrapper
46
47
48 class Window(gtk.Window):
49     def __init__(self, app):
50         gtk.Window.__init__(self)
51         self.app = app
52
53         self.connect('delete-event', self.quit_cb)
54         self.connect('key-press-event', self.key_press_event_cb_before)
55         self.connect('key-release-event', self.key_release_event_cb_before)
56         self.connect_after('key-press-event', self.key_press_event_cb_after)
57         self.connect_after('key-release-event', self.key_release_event_cb_after)
58         self.connect("drag-data-received", self.drag_data_received)
59         self.connect("button-press-event", self.button_press_cb)
60         self.connect("button-release-event", self.button_release_cb)
61         self.connect("scroll-event", self.scroll_cb)
62         self.set_default_size(600, 400)
63         vbox = gtk.VBox()
64         self.add(vbox)
65
66         #TODO: move self.doc into application.py?
67         self.doc = document.Document()
68         self.doc.set_brush(self.app.brush)
69
70         self.create_ui()
71         self.menubar = self.app.ui_manager.get_widget('/Menubar')
72         self.menubar.connect("selection-done", self.menu_done_cb)
73         self.menubar.connect("deactivate", self.menu_done_cb)
74         self.menubar.connect("cancel", self.menu_done_cb)
75         vbox.pack_start(self.menubar, expand=False)
76
77         self.tdw = tileddrawwidget.TiledDrawWidget(self.doc)
78         vbox.pack_start(self.tdw)
79
80         # FIXME: hack, to be removed
81         fname = os.path.join(self.app.datapath, 'backgrounds', '03_check1.png')
82         pixbuf = gdk.pixbuf_new_from_file(fname)
83         self.tdw.neutral_background_pixbuf = backgroundsurface.Background(pixbuf)
84
85         self.zoomlevel_values = [1.0/8, 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]
86         self.zoomlevel = self.zoomlevel_values.index(1.0)
87         self.tdw.zoom_min = min(self.zoomlevel_values)
88         self.tdw.zoom_max = max(self.zoomlevel_values)
89         self.fullscreen = False
90
91         self.app.brush.settings_observers.append(self.brush_modified_cb)
92         self.tdw.device_observers.append(self.device_changed_cb)
93         self.last_pen_device = None
94
95         self.eraser_mode_radius_change = 3*(0.3) # can go back to exact original with brush_smaller_cb()
96         self.eraser_mode_original_radius = None
97
98         # enable drag & drop
99         self.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_DROP, [("text/uri-list", 0, 1)], gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY)
100
101     def create_ui(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
107             ('EditMenu',           None, _('Edit')),
108             ('Undo',               gtk.STOCK_UNDO, _('Undo'), 'Z', None, self.undo_cb),
109             ('Redo',               gtk.STOCK_REDO, _('Redo'), 'Y', None, self.redo_cb),
110
111             ('BrushMenu',    None, _('Brush')),
112             ('Brighter',     None, _('Brighter'), None, None, self.brighter_cb),
113             ('Smaller',      None, _('Smaller'), 'd', None, self.brush_smaller_cb),
114             ('MoreOpaque',   None, _('More Opaque'), 's', None, self.more_opaque_cb),
115             ('LessOpaque',   None, _('Less Opaque'), 'a', None, self.less_opaque_cb),
116             ('Eraser',       None, _('Toggle Eraser Mode'), 'e', None, self.eraser_cb), # TODO: make toggle action
117             ('PickContext',  None, _('Pick Context (layer, brush and color)'), 'w', None, self.pick_context_cb),
118
119             ('ColorMenu',    None, _('Color')),
120             ('Darker',       None, _('Darker'), None, None, self.darker_cb),
121             ('Bigger',       None, _('Bigger'), 'f', None, self.brush_bigger_cb),
122             ('ColorPickerPopup',    gtk.STOCK_COLOR_PICKER, _('Pick Color'), 'r', None, self.popup_cb),
123             ('ColorHistoryPopup',  None, _('Color History'), 'x', None, self.popup_cb),
124             ('ColorChangerPopup', None, _('Color Changer'), 'v', None, self.popup_cb),
125             ('ColorRingPopup',  None, _('Color Ring'), None, None, self.popup_cb),
126
127             ('ContextMenu',  None, _('Brushkeys')),
128             #each of the context actions are generated and added below
129             ('ContextStore', None, _('Save to Most Recently Restored'), 'q', None, self.context_cb),
130             ('ContextHelp',  gtk.STOCK_HELP, _('Help!'), None, None, self.show_infodialog_cb),
131
132             ('LayerMenu',    None, _('Layers')),
133
134             ('LayersWindow', None, _('Layers...'), 'l', None, self.toggleWindow_cb),
135             ('BackgroundWindow', None, _('Background...'), None, None, self.toggleWindow_cb),
136             ('ClearLayer',   gtk.STOCK_CLEAR, _('Clear'), 'Delete', None, self.clear_layer_cb),
137             ('CopyLayer',          gtk.STOCK_COPY, _('Copy to Clipboard'), '<control>C', None, self.copy_cb),
138             ('PasteLayer',         gtk.STOCK_PASTE, _('Paste Clipboard (Replace Layer)'), '<control>V', None, self.paste_cb),
139             ('PickLayer',    None, _('Select Layer at Cursor'), 'h', None, self.pick_layer_cb),
140             ('LayerFG',      None, _('Next (above current)'),  'Page_Up', None, self.layer_fg_cb),
141             ('LayerBG',      None, _('Next (below current)'), 'Page_Down', None, self.layer_bg_cb),
142             ('NewLayerFG',   None, _('New (above current)'), '<control>Page_Up', None, self.new_layer_cb),
143             ('NewLayerBG',   None, _('New (below current)'), '<control>Page_Down', None, self.new_layer_cb),
144             ('MergeLayer',   None, _('Merge Down'), '<control>Delete', None, self.merge_layer_cb),
145             ('RemoveLayer',  gtk.STOCK_DELETE, _('Remove'), '<shift>Delete', None, self.remove_layer_cb),
146             ('IncreaseLayerOpacity', None, _('Increase Layer Opacity'),  'p', None, self.layer_increase_opacity),
147             ('DecreaseLayerOpacity', None, _('Decrease Layer Opacity'),  'o', None, self.layer_decrease_opacity),
148
149             ('BrushSelectionWindow',  None, _('Brush List...'), 'b', None, self.toggleWindow_cb),
150             ('BrushSettingsWindow',   None, _('Brush Settings...'), '<control>b', None, self.toggleWindow_cb),
151             ('ColorSelectionWindow',  None, _('Color Triangle...'), 'g', None, self.toggleWindow_cb),
152             ('ColorSamplerWindow',  gtk.STOCK_SELECT_COLOR, _('Color Sampler...'), 't', None, self.toggleWindow_cb),
153             ('SettingsWindow',        gtk.STOCK_PREFERENCES, _('Settings...'), None, None, self.toggleWindow_cb),
154
155             ('HelpMenu',     None, _('Help')),
156             ('Docu', None, _('Where is the Documentation?'), None, None, self.show_infodialog_cb),
157             ('ShortcutHelp',  None, _('Change the Keyboard Shortcuts?'), None, None, self.show_infodialog_cb),
158             ('About', gtk.STOCK_ABOUT, _('About MyPaint'), None, None, self.about_cb),
159
160             ('DebugMenu',    None, _('Debug')),
161
162
163             ('ShortcutsMenu', None, _('Shortcuts')),
164
165             ('ViewMenu', None, _('View')),
166             ('Fullscreen',   gtk.STOCK_FULLSCREEN, _('Fullscreen'), 'F11', None, self.fullscreen_cb),
167             ('ShowMenu',    None, _('Show Menu'), 'Menu', None, self.menu_show_cb),
168             ('ResetView',   gtk.STOCK_ZOOM_100, _('Reset (Zoom, Rotation, Mirror)'), 'F12', None, self.reset_view_cb),
169             ('ZoomIn',       gtk.STOCK_ZOOM_IN, _('Zoom In (at cursor)'), 'period', None, self.zoom_cb),
170             ('ZoomOut',      gtk.STOCK_ZOOM_OUT, _('Zoom Out'), 'comma', None, self.zoom_cb),
171             ('RotateLeft',   None, _('Rotate Counterclockwise'), None, None, self.rotate_cb),
172             ('RotateRight',  None, _('Rotate Clockwise'), None, None, self.rotate_cb),
173             ('SoloLayer',    None, _('Layer Solo'), 'Home', None, self.solo_layer_cb), # TODO: make toggle action
174             ('ToggleAbove',  None, _('Hide Layers Above Current'), 'End', None, self.toggle_layers_above_cb), # TODO: make toggle action
175             ('ViewHelp',  gtk.STOCK_HELP, _('Help'), None, None, self.show_infodialog_cb),
176             ]
177         ag = self.action_group = gtk.ActionGroup('WindowActions')
178         ag.add_actions(actions)
179         context_actions = []
180         for x in range(10):
181             r = ('Context0%d' % x,    None, _('Restore Brush %d') % x, 
182                     '%d' % x, None, self.context_cb)
183             s = ('Context0%ds' % x,   None, _('Save to Brush %d') % x, 
184                     '<control>%d' % x, None, self.context_cb)
185             context_actions.append(s)
186             context_actions.append(r)
187         ag.add_actions(context_actions)
188         toggle_actions = [
189             # name, stock id, label, accelerator, tooltip, callback, default toggle status
190             ('PrintInputs', None, _('Print Brush Input Values to stdout'), None, None, self.print_inputs_cb),
191             ('VisualizeRendering', None, _('Visualize Rendering'), None, None, self.visualize_rendering_cb),
192             ('NoDoubleBuffereing', None, _('Disable GTK Double Buffering'), None, None, self.no_double_buffering_cb),
193             ('Flip', None, _('Mirror Image'), 'i', None, self.flip_cb),
194             ]
195         ag.add_toggle_actions(toggle_actions)
196         self.app.ui_manager.insert_action_group(ag, -1)
197         menupath = os.path.join(self.app.datapath, 'gui/menu.xml')
198         self.app.ui_manager.add_ui_from_file(menupath)
199         #self.app.accel_group = self.app.ui_manager.get_accel_group()
200
201         kbm = self.app.kbm
202         kbm.add_window(self)
203
204         for action in ag.list_actions():
205             self.app.kbm.takeover_action(action)
206
207         kbm.add_extra_key('<control>z', 'Undo')
208         kbm.add_extra_key('<control>y', 'Redo')
209         kbm.add_extra_key('KP_Add', 'ZoomIn')
210         kbm.add_extra_key('KP_Subtract', 'ZoomOut')
211         kbm.add_extra_key('plus', 'ZoomIn')
212         kbm.add_extra_key('minus', 'ZoomOut')
213
214         kbm.add_extra_key('Left', lambda(action): self.move('MoveLeft'))
215         kbm.add_extra_key('Right', lambda(action): self.move('MoveRight'))
216         kbm.add_extra_key('Down', lambda(action): self.move('MoveDown'))
217         kbm.add_extra_key('Up', lambda(action): self.move('MoveUp'))
218
219         kbm.add_extra_key('<control>Left', 'RotateLeft')
220         kbm.add_extra_key('<control>Right', 'RotateRight')
221
222         sg = stategroup.StateGroup()
223         self.layerblink_state = sg.create_state(self.layerblink_state_enter, self.layerblink_state_leave)
224
225         sg = stategroup.StateGroup()
226         self.strokeblink_state = sg.create_state(self.strokeblink_state_enter, self.strokeblink_state_leave)
227         self.strokeblink_state.autoleave_timeout = 0.3
228
229         # separate stategroup...
230         sg2 = stategroup.StateGroup()
231         self.layersolo_state = sg2.create_state(self.layersolo_state_enter, self.layersolo_state_leave)
232         self.layersolo_state.autoleave_timeout = None
233
234         p2s = sg.create_popup_state
235         changer = p2s(colorselectionwindow.ColorChangerPopup(self.app))
236         ring = p2s(colorselectionwindow.ColorRingPopup(self.app))
237         hist = p2s(historypopup.HistoryPopup(self.app, self.doc))
238         pick = self.colorpick_state = p2s(colorpicker.ColorPicker(self.app, self.doc))
239
240         self.popup_states = {
241             'ColorChangerPopup': changer,
242             'ColorRingPopup': ring,
243             'ColorHistoryPopup': hist,
244             'ColorPickerPopup': pick,
245             }
246         changer.next_state = ring
247         ring.next_state = changer
248         changer.autoleave_timeout = None
249         ring.autoleave_timeout = None
250
251         pick.max_key_hit_duration = 0.0
252         pick.autoleave_timeout = None
253
254         hist.autoleave_timeout = 0.600
255         self.history_popup_state = hist
256
257     def drag_data_received(self, widget, context, x, y, selection, info, t):
258         if selection.data:
259             uri = selection.data.split("\r\n")[0]
260             fn = helpers.get_file_path_from_dnd_dropped_uri(uri)
261             if os.path.exists(fn):
262                 if self.app.filehandler.confirm_destructive_action():
263                     self.app.filehandler.open_file(fn)
264
265     def toggleWindow_cb(self, action):
266         s = action.get_name()
267         s = s[0].lower() + s[1:]
268         w = getattr(self.app, s)
269         if w.window and w.window.is_visible():
270             w.hide()
271         else:
272             w.show_all() # might be for the first time
273             w.present()
274
275     def print_inputs_cb(self, action):
276         self.doc.brush.print_inputs = action.get_active()
277
278     def visualize_rendering_cb(self, action):
279         self.tdw.visualize_rendering = action.get_active()
280     def no_double_buffering_cb(self, action):
281         self.tdw.set_double_buffered(not action.get_active())
282
283     def undo_cb(self, action):
284         cmd = self.doc.undo()
285         if isinstance(cmd, command.MergeLayer):
286             # show otherwise invisible change (hack...)
287             self.layerblink_state.activate()
288
289     def redo_cb(self, action):
290         cmd = self.doc.redo()
291         if isinstance(cmd, command.MergeLayer):
292             # show otherwise invisible change (hack...)
293             self.layerblink_state.activate()
294
295     def copy_cb(self, action):
296         # use the full document bbox, so we can past layers back to the correct position
297         bbox = self.doc.get_bbox()
298         if bbox.w == 0 or bbox.h == 0:
299             print "WARNING: empty document, nothing copied"
300             return
301         else:
302             pixbuf = self.doc.layer.surface.render_as_pixbuf(*bbox)
303         cb = gtk.Clipboard()
304         cb.set_image(pixbuf)
305
306     def paste_cb(self, action):
307         cb = gtk.Clipboard()
308         def callback(clipboard, pixbuf, trash):
309             if not pixbuf:
310                 print 'The clipboard doeas not contain any image to paste!'
311                 return
312             # paste to the upper left of our doc bbox (see above)
313             x, y, w, h = self.doc.get_bbox()
314             self.doc.load_layer_from_pixbuf(pixbuf, x, y)
315         cb.request_image(callback)
316
317     def brush_modified_cb(self):
318         # called at every brush setting modification, should return fast
319         self.doc.set_brush(self.app.brush)
320
321     def key_press_event_cb_before(self, win, event):
322         key = event.keyval 
323         ctrl = event.state & gdk.CONTROL_MASK
324         #ANY_MODIFIER = gdk.SHIFT_MASK | gdk.MOD1_MASK | gdk.CONTROL_MASK
325         #if event.state & ANY_MODIFIER:
326         #    # allow user shortcuts with modifiers
327         #    return False
328         if key == keysyms.space:
329             if ctrl:
330                 self.tdw.start_drag(self.dragfunc_rotate)
331             else:
332                 self.tdw.start_drag(self.dragfunc_translate)
333         else: return False
334         return True
335     def key_release_event_cb_before(self, win, event):
336         if event.keyval == keysyms.space:
337             self.tdw.stop_drag(self.dragfunc_translate)
338             self.tdw.stop_drag(self.dragfunc_rotate)
339             return True
340         return False
341
342     def key_press_event_cb_after(self, win, event):
343         key = event.keyval
344         if self.fullscreen and key == keysyms.Escape: self.fullscreen_cb()
345         else: return False
346         return True
347     def key_release_event_cb_after(self, win, event):
348         return False
349
350     def dragfunc_translate(self, dx, dy):
351         self.tdw.scroll(-dx, -dy)
352
353     def dragfunc_rotate(self, dx, dy):
354         self.tdw.scroll(-dx, -dy, False)
355         self.tdw.rotate(2*math.pi*dx/500.0)
356
357     #def dragfunc_rotozoom(self, dx, dy):
358     #    self.tdw.scroll(-dx, -dy, False)
359     #    self.tdw.zoom(math.exp(-dy/100.0))
360     #    self.tdw.rotate(2*math.pi*dx/500.0)
361
362     def button_press_cb(self, win, event):
363         #print event.device, event.button
364         if event.type != gdk.BUTTON_PRESS:
365             # ignore the extra double-click event
366             return
367         if event.button == 2:
368             # check whether we are painting (accidental)
369             pressure = event.get_axis(gdk.AXIS_PRESSURE)
370             if (event.state & gdk.BUTTON1_MASK) or pressure:
371                 # do not allow dragging while painting (often happens accidentally)
372                 pass
373             else:
374                 self.tdw.start_drag(self.dragfunc_translate)
375         elif event.button == 1:
376             if event.state & gdk.CONTROL_MASK:
377                 self.end_eraser_mode()
378                 self.colorpick_state.activate(event)
379         elif event.button == 3:
380             self.history_popup_state.activate(event)
381
382     def button_release_cb(self, win, event):
383         #print event.device, event.button
384         if event.button == 2:
385             self.tdw.stop_drag(self.dragfunc_translate)
386         # too slow to be useful:
387         #elif event.button == 3:
388         #    self.tdw.stop_drag(self.dragfunc_rotate)
389
390     def scroll_cb(self, win, event):
391         d = event.direction
392         if d == gdk.SCROLL_UP:
393             if event.state & gdk.SHIFT_MASK:
394                 self.rotate('RotateLeft')
395             else:
396                 self.zoom('ZoomIn')
397         elif d == gdk.SCROLL_DOWN:
398             if event.state & gdk.SHIFT_MASK:
399                 self.rotate('RotateRight')
400             else:
401                 self.zoom('ZoomOut')
402         elif d == gdk.SCROLL_LEFT:
403             self.rotate('RotateRight')
404         elif d == gdk.SCROLL_LEFT:
405             self.rotate('RotateLeft')
406
407     def clear_layer_cb(self, action):
408         self.doc.clear_layer()
409         if self.doc.is_empty():
410             # the user started a new painting
411             self.app.filehandler.filename = None
412
413     def remove_layer_cb(self, action):
414         self.doc.remove_layer()
415         if self.doc.is_empty():
416             # the user started a new painting
417             self.app.filehandler.filename = None
418
419     def layer_bg_cb(self, action):
420         idx = self.doc.layer_idx - 1
421         if idx < 0:
422             return
423         self.doc.select_layer(idx)
424         self.layerblink_state.activate(action)
425
426     def layer_fg_cb(self, action):
427         idx = self.doc.layer_idx + 1
428         if idx >= len(self.doc.layers):
429             return
430         self.doc.select_layer(idx)
431         self.layerblink_state.activate(action)
432
433     def pick_layer_cb(self, action):
434         x, y = self.tdw.get_cursor_in_model_coordinates()
435         for idx, layer in reversed(list(enumerate(self.doc.layers))):
436             alpha = layer.surface.get_alpha (x, y, 5) * layer.opacity
437             if alpha > 0.1:
438                 self.doc.select_layer(idx)
439                 self.layerblink_state.activate(action)
440                 return
441         self.doc.select_layer(0)
442         self.layerblink_state.activate(action)
443
444     def pick_context_cb(self, action):
445         x, y = self.tdw.get_cursor_in_model_coordinates()
446         for idx, layer in reversed(list(enumerate(self.doc.layers))):
447             alpha = layer.surface.get_alpha (x, y, 5) * layer.opacity
448             if alpha > 0.1:
449                 old_layer = self.doc.layer
450                 self.doc.select_layer(idx)
451                 if self.doc.layer != old_layer:
452                     self.layerblink_state.activate()
453
454                 # find the most recent (last) stroke that touches our picking point
455                 si = self.doc.layer.get_stroke_info_at(x, y)
456
457                 if si:
458                     self.app.brushmanager.select_brush(None) # FIXME: restore the selected brush
459                     self.app.brush.load_from_string(si.brush_string)
460                     self.si = si # FIXME: should be a method parameter?
461                     self.strokeblink_state.activate(action)
462                 return
463
464     def strokeblink_state_enter(self):
465         l = layer.Layer()
466         self.si.render_overlay(l.surface)
467         self.tdw.overlay_layer = l
468         self.tdw.queue_draw() # OPTIMIZE: excess
469     def strokeblink_state_leave(self, reason):
470         self.tdw.overlay_layer = None
471         self.tdw.queue_draw() # OPTIMIZE: excess
472
473     def layerblink_state_enter(self):
474         self.tdw.current_layer_solo = True
475         self.tdw.queue_draw()
476     def layerblink_state_leave(self, reason):
477         if self.layersolo_state.active:
478             # FIXME: use state machine concept, maybe?
479             return
480         self.tdw.current_layer_solo = False
481         self.tdw.queue_draw()
482     def layersolo_state_enter(self):
483         s = self.layerblink_state
484         if s.active:
485             s.leave()
486         self.tdw.current_layer_solo = True
487         self.tdw.queue_draw()
488     def layersolo_state_leave(self, reason):
489         self.tdw.current_layer_solo = False
490         self.tdw.queue_draw()
491
492     #def blink_layer_cb(self, action):
493     #    self.layerblink_state.activate(action)
494
495     def solo_layer_cb(self, action):
496         self.layersolo_state.toggle(action)
497
498     def new_layer_cb(self, action):
499         insert_idx = self.doc.layer_idx
500         if action.get_name() == 'NewLayerFG':
501             insert_idx += 1
502         self.doc.add_layer(insert_idx)
503         self.layerblink_state.activate(action)
504
505     @with_wait_cursor
506     def merge_layer_cb(self, action):
507         if self.doc.merge_layer_down():
508             self.layerblink_state.activate(action)
509
510     def toggle_layers_above_cb(self, action):
511         self.tdw.toggle_show_layers_above()
512
513     def popup_cb(self, action):
514         # This doesn't really belong here...
515         # just because all popups are color popups now...
516         # ...maybe should eraser_mode be a GUI state too?
517         self.end_eraser_mode()
518
519         state = self.popup_states[action.get_name()]
520         state.activate(action)
521
522     def eraser_cb(self, action):
523         adj = self.app.brush_adjustment['eraser']
524         if adj.get_value() > 0.9:
525             self.end_eraser_mode()
526         else:
527             # enter eraser mode
528             adj.set_value(1.0)
529             adj2 = self.app.brush_adjustment['radius_logarithmic']
530             r = adj2.get_value()
531             self.eraser_mode_original_radius = r
532             adj2.set_value(r + self.eraser_mode_radius_change)
533
534     def end_eraser_mode(self):
535         adj = self.app.brush_adjustment['eraser']
536         if not adj.get_value() > 0.9:
537             return
538         adj.set_value(0.0)
539         if self.eraser_mode_original_radius:
540             # save eraser radius, restore old radius
541             adj2 = self.app.brush_adjustment['radius_logarithmic']
542             r = adj2.get_value()
543             self.eraser_mode_radius_change = r - self.eraser_mode_original_radius
544             adj2.set_value(self.eraser_mode_original_radius)
545             self.eraser_mode_original_radius = None
546
547     def device_changed_cb(self, old_device, new_device):
548         # just enable eraser mode for now (TODO: remember full tool settings)
549         # small problem with this code: it doesn't work well with brushes that have (eraser not in [1.0, 0.0])
550         def is_eraser(device):
551             if device is None: return False
552             return device.source == gdk.SOURCE_ERASER or 'eraser' in device.name.lower()
553         if old_device is None and not is_eraser(new_device):
554             # keep whatever startup brush was choosen
555             return
556
557         print 'device change:', new_device.name, new_device.source
558
559         # When editing brush settings, it is often more convenient to use the mouse.
560         # Because of this, we don't restore brushsettings when switching to/from the mouse.
561         # We act as if the mouse was identical to the last active pen device.
562         if new_device.source == gdk.SOURCE_MOUSE and self.last_pen_device:
563             new_device = self.last_pen_device
564         if new_device.source == gdk.SOURCE_PEN:
565             self.last_pen_device = new_device
566         if old_device.source == gdk.SOURCE_MOUSE and self.last_pen_device:
567             old_device = self.last_pen_device
568
569         bm = self.app.brushmanager
570         bm.brush_by_device[old_device.name] = (bm.selected_brush, self.app.brush.save_to_string())
571
572         if new_device.name in bm.brush_by_device:
573             brush_to_select, brush_settings = bm.brush_by_device[new_device.name]
574             # mark as selected in brushlist
575             bm.select_brush(brush_to_select)
576             # restore modifications (radius / color change the user made)
577             self.app.brush.load_from_string(brush_settings)
578         else:
579             # first time using that device
580             adj = self.app.brush_adjustment['eraser']
581             if is_eraser(new_device):
582                 # enter eraser mode
583                 adj.set_value(1.0)
584             elif not is_eraser(new_device) and is_eraser(old_device):
585                 # leave eraser mode
586                 adj.set_value(0.0)
587
588     def brush_bigger_cb(self, action):
589         adj = self.app.brush_adjustment['radius_logarithmic']
590         adj.set_value(adj.get_value() + 0.3)
591     def brush_smaller_cb(self, action):
592         adj = self.app.brush_adjustment['radius_logarithmic']
593         adj.set_value(adj.get_value() - 0.3)
594
595     def more_opaque_cb(self, action):
596         # FIXME: hm, looks this slider should be logarithmic?
597         adj = self.app.brush_adjustment['opaque']
598         adj.set_value(adj.get_value() * 1.8)
599     def less_opaque_cb(self, action):
600         adj = self.app.brush_adjustment['opaque']
601         adj.set_value(adj.get_value() / 1.8)
602
603     def brighter_cb(self, action):
604         self.end_eraser_mode()
605         h, s, v = self.app.brush.get_color_hsv()
606         v += 0.08
607         if v > 1.0: v = 1.0
608         self.app.brush.set_color_hsv((h, s, v))
609     def darker_cb(self, action):
610         self.end_eraser_mode()
611         h, s, v = self.app.brush.get_color_hsv()
612         v -= 0.08
613         if v < 0.0: v = 0.0
614         self.app.brush.set_color_hsv((h, s, v))
615
616     def layer_increase_opacity(self, action):
617         opa = helpers.clamp(self.doc.layer.opacity + 0.08, 0.0, 1.0)
618         self.doc.set_layer_opacity(opa)
619
620     def layer_decrease_opacity(self, action):
621         opa = helpers.clamp(self.doc.layer.opacity - 0.08, 0.0, 1.0)
622         self.doc.set_layer_opacity(opa)
623
624     def quit_cb(self, *trash):
625         self.doc.split_stroke()
626         self.app.save_gui_config() # FIXME: should do this periodically, not only on quit
627
628         if not self.app.filehandler.confirm_destructive_action(title=_('Quit'), question=_('Really Quit?')):
629             return True
630
631         gtk.main_quit()
632         return False
633
634     def zoom_cb(self, action):
635         self.zoom(action.get_name())
636     def rotate_cb(self, action):
637         self.rotate(action.get_name())
638     def flip_cb(self, action):
639         self.tdw.set_flipped(action.get_active())
640
641     def move(self, command):
642         self.doc.split_stroke()
643         step = min(self.tdw.window.get_size()) / 5
644         if   command == 'MoveLeft' : self.tdw.scroll(-step, 0)
645         elif command == 'MoveRight': self.tdw.scroll(+step, 0)
646         elif command == 'MoveUp'   : self.tdw.scroll(0, -step)
647         elif command == 'MoveDown' : self.tdw.scroll(0, +step)
648         else: assert 0
649
650     def zoom(self, command):
651         if   command == 'ZoomIn' : self.zoomlevel += 1
652         elif command == 'ZoomOut': self.zoomlevel -= 1
653         else: assert 0
654         if self.zoomlevel < 0: self.zoomlevel = 0
655         if self.zoomlevel >= len(self.zoomlevel_values): self.zoomlevel = len(self.zoomlevel_values) - 1
656         z = self.zoomlevel_values[self.zoomlevel]
657         self.tdw.set_zoom(z)
658
659     def rotate(self, command):
660         if   command == 'RotateRight': self.tdw.rotate(+2*math.pi/14)
661         elif command == 'RotateLeft' : self.tdw.rotate(-2*math.pi/14)
662         else: assert 0
663
664     def reset_view_cb(self, command):
665         self.tdw.set_rotation(0.0)
666         self.zoomlevel = self.zoomlevel_values.index(1.0)
667         self.tdw.set_zoom(1.0)
668         self.tdw.set_flipped(False)
669         self.action_group.get_action('Flip').set_active(False)
670
671     def fullscreen_cb(self, *trash):
672         # note: there is some ugly flickering when toggling fullscreen
673         #       self.window.begin_paint/end_paint does not help against it
674         self.fullscreen = not self.fullscreen
675         if self.fullscreen:
676             x, y = self.get_position()
677             w, h = self.get_size()
678             self.geometry_before_fullscreen = (x, y, w, h)
679             self.menubar.hide()
680             self.window.fullscreen()
681             #self.tdw.set_scroll_at_edges(True)
682         else:
683             self.window.unfullscreen()
684             self.menubar.show()
685             #self.tdw.set_scroll_at_edges(False)
686             del self.geometry_before_fullscreen
687
688     def context_cb(self, action):
689         name = action.get_name()
690         store = False
691         bm = self.app.brushmanager
692         if name == 'ContextStore':
693             context = bm.selected_context
694             if not context:
695                 print 'No context was selected, ignoring store command.'
696                 return
697             store = True
698         else:
699             if name.endswith('s'):
700                 store = True
701                 name = name[:-1]
702             i = int(name[-2:])
703             context = bm.contexts[i]
704         bm.selected_context = context
705         if store:
706             context.copy_settings_from(self.app.brush)
707             context.preview = bm.selected_brush.preview
708             context.save()
709         else:
710             # restore (but keep color)
711             color = self.app.brush.get_color_hsv()
712             context.set_color_hsv(color)
713             bm.select_brush(context)
714
715     def menu_show_cb(self, action):
716         if self.fullscreen:
717             self.menubar.show()
718         self.menubar.select_first(False)
719
720     def menu_done_cb(self, *a, **kw):
721         if self.fullscreen:
722             self.menubar.hide()
723
724     def about_cb(self, action):
725         d = gtk.AboutDialog()
726         d.set_transient_for(self)
727         d.set_program_name("MyPaint")
728         d.set_version(MYPAINT_VERSION)
729         d.set_copyright(_("Copyright (C) 2005-2009\nMartin Renold and the MyPaint Development Team"))
730         d.set_website("http://mypaint.info/")
731         d.set_logo(self.app.pixmaps.mypaint_logo)
732         d.set_license(
733             _(u"This program is free software; you can redistribute it and/or modify "
734               u"it under the terms of the GNU General Public License as published by "
735               u"the Free Software Foundation; either version 2 of the License, or "
736               u"(at your option) any later version.\n"
737               u"\n"
738               u"This program is distributed in the hope that it will be useful, "
739               u"but WITHOUT ANY WARRANTY. See the COPYING file for more details.")
740             )
741         d.set_wrap_license(True)
742         d.set_authors([
743             u"Martin Renold (%s)" % _('programming'),
744             u"Artis Rozentāls (%s)" % _('brushes'),
745             u"Yves Combe (%s)" % _('portability'),
746             u"Popolon (%s)" % _('brushes, programming'),
747             u"Clement Skau (%s)" % _('programming'),
748             u"Marcelo 'Tanda' Cerviño (%s)" % _('patterns, brushes'),
749             u"Jon Nordby (%s)" % _('programming'),
750             u"Álinson Santos (%s)" % _('programming'),
751             u"Tumagonx (%s)" % _('portability'),
752             u"Ilya Portnov (%s)" % _('programming'),
753             u"David Revoy (%s)" % _('brushes'),
754             u"Ramón Miranda (%s)" % _('brushes'),
755             u"Enrico Guarnieri 'Ico_dY' (%s)" % _('brushes'),
756             u"Jonas Wagner (%s)" % _('programming'),
757             u"Luka Čehovin (%s)" % _('programming'),
758             u"Andrew Chadwick (%s)" % _('programming'),
759             ])
760         d.set_artists([
761             u'Sebastian Kraft (%s)' % _('desktop icon'),
762             ])
763         # list all translators, not only those of the current language
764         d.set_translator_credits(
765             u'Ilya Portnov (ru)\n'
766             u'Popolon (fr, zh_CN)\n'
767             u'Jon Nordby (nb)\n'
768             u'Griatch (sv)\n'
769             u'Tobias Jakobs (de)\n'
770             u'Martin Tabačan (cs)\n'
771             u'Tumagonx (id)\n'
772             u'Manuel Quiñones (es)\n'
773             u'Gergely Aradszki (hu)\n'
774             u'Lamberto Tedaldi (it)\n'
775             )
776         
777         d.run()
778         d.destroy()
779
780     def show_infodialog_cb(self, action):
781         text = {
782         'ShortcutHelp': 
783                 _("Move your mouse over a menu entry, then press the key to assign."),
784         'ViewHelp': 
785                 _("You can also drag the canvas with the mouse while holding the middle "
786                 "mouse button or spacebar. Or with the arrow keys."
787                 "\n\n"
788                 "In contrast to earlier versions, scrolling and zooming are harmless now and "
789                 "will not make you run out of memory. But you still require a lot of memory "
790                 "if you paint all over while fully zoomed out."),
791         'ContextHelp':
792                 _("This is used to quickly save/restore brush settings "
793                  "using keyboard shortcuts. You can paint with one hand and "
794                  "change brushes with the other without interrupting."
795                  "\n\n"
796                  "There are 10 memory slots to hold brush settings.\n"
797                  "Those are anonymous "
798                  "brushes, they are not visible in the brush selector list. "
799                  "But they will stay even if you quit. "
800                  "They will also remember the selected color. In contrast, selecting a "
801                  "normal brush never changes the color. "),
802         'Docu':
803                 _("There is a tutorial available "
804                  "on the MyPaint homepage. It explains some features which are "
805                  "hard to discover yourself.\n\n"
806                  "Comments about the brush settings (opaque, hardness, etc.) and "
807                  "inputs (pressure, speed, etc.) are available as tooltips. "
808                  "Put your mouse over a label to see them. "
809                  "\n"),
810         }
811         self.app.message_dialog(text[action.get_name()])