1 # -*- coding: utf-8 -*-
3 # This file is part of MyPaint.
4 # Copyright (C) 2007-2008 by Martin Renold <martinxyz@gmx.ch>
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.
12 This is the main drawing window, containing menu actions.
13 Painting is done in tileddrawwidget.py.
16 MYPAINT_VERSION="0.8.0"
19 from gettext import gettext as _
22 from gtk import gdk, keysyms
24 import tileddrawwidget, colorselectionwindow, historypopup, \
25 stategroup, colorpicker
26 from lib import document, helpers, backgroundsurface, command, layer
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)
41 func(self, *args, **kwargs)
43 self.app.drawWindow.window.set_cursor(None)
44 self.app.drawWindow.tdw.update_cursor()
48 class Window(gtk.Window):
49 def __init__(self, app):
50 gtk.Window.__init__(self)
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)
66 #TODO: move self.doc into application.py?
67 self.doc = document.Document()
68 self.doc.set_brush(self.app.brush)
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)
77 self.tdw = tileddrawwidget.TiledDrawWidget(self.doc)
78 vbox.pack_start(self.tdw)
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)
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
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
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
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)
103 # name, stock id, label, accelerator, tooltip, callback
104 ('FileMenu', None, _('File')),
105 ('Quit', gtk.STOCK_QUIT, _('Quit'), '<control>q', None, self.quit_cb),
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),
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),
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),
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),
132 ('LayerMenu', None, _('Layers')),
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),
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),
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),
160 ('DebugMenu', None, _('Debug')),
163 ('ShortcutsMenu', None, _('Shortcuts')),
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),
177 ag = self.action_group = gtk.ActionGroup('WindowActions')
178 ag.add_actions(actions)
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)
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),
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()
204 for action in ag.list_actions():
205 self.app.kbm.takeover_action(action)
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')
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'))
219 kbm.add_extra_key('<control>Left', 'RotateLeft')
220 kbm.add_extra_key('<control>Right', 'RotateRight')
222 sg = stategroup.StateGroup()
223 self.layerblink_state = sg.create_state(self.layerblink_state_enter, self.layerblink_state_leave)
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
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
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))
240 self.popup_states = {
241 'ColorChangerPopup': changer,
242 'ColorRingPopup': ring,
243 'ColorHistoryPopup': hist,
244 'ColorPickerPopup': pick,
246 changer.next_state = ring
247 ring.next_state = changer
248 changer.autoleave_timeout = None
249 ring.autoleave_timeout = None
251 pick.max_key_hit_duration = 0.0
252 pick.autoleave_timeout = None
254 hist.autoleave_timeout = 0.600
255 self.history_popup_state = hist
257 def drag_data_received(self, widget, context, x, y, selection, info, t):
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)
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():
272 w.show_all() # might be for the first time
275 def print_inputs_cb(self, action):
276 self.doc.brush.print_inputs = action.get_active()
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())
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()
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()
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"
302 pixbuf = self.doc.layer.surface.render_as_pixbuf(*bbox)
306 def paste_cb(self, action):
308 def callback(clipboard, pixbuf, trash):
310 print 'The clipboard doeas not contain any image to paste!'
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)
317 def brush_modified_cb(self):
318 # called at every brush setting modification, should return fast
319 self.doc.set_brush(self.app.brush)
321 def key_press_event_cb_before(self, win, event):
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
328 if key == keysyms.space:
330 self.tdw.start_drag(self.dragfunc_rotate)
332 self.tdw.start_drag(self.dragfunc_translate)
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)
342 def key_press_event_cb_after(self, win, event):
344 if self.fullscreen and key == keysyms.Escape: self.fullscreen_cb()
347 def key_release_event_cb_after(self, win, event):
350 def dragfunc_translate(self, dx, dy):
351 self.tdw.scroll(-dx, -dy)
353 def dragfunc_rotate(self, dx, dy):
354 self.tdw.scroll(-dx, -dy, False)
355 self.tdw.rotate(2*math.pi*dx/500.0)
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)
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
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)
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)
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)
390 def scroll_cb(self, win, event):
392 if d == gdk.SCROLL_UP:
393 if event.state & gdk.SHIFT_MASK:
394 self.rotate('RotateLeft')
397 elif d == gdk.SCROLL_DOWN:
398 if event.state & gdk.SHIFT_MASK:
399 self.rotate('RotateRight')
402 elif d == gdk.SCROLL_LEFT:
403 self.rotate('RotateRight')
404 elif d == gdk.SCROLL_LEFT:
405 self.rotate('RotateLeft')
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
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
419 def layer_bg_cb(self, action):
420 idx = self.doc.layer_idx - 1
423 self.doc.select_layer(idx)
424 self.layerblink_state.activate(action)
426 def layer_fg_cb(self, action):
427 idx = self.doc.layer_idx + 1
428 if idx >= len(self.doc.layers):
430 self.doc.select_layer(idx)
431 self.layerblink_state.activate(action)
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
438 self.doc.select_layer(idx)
439 self.layerblink_state.activate(action)
441 self.doc.select_layer(0)
442 self.layerblink_state.activate(action)
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
449 old_layer = self.doc.layer
450 self.doc.select_layer(idx)
451 if self.doc.layer != old_layer:
452 self.layerblink_state.activate()
454 # find the most recent (last) stroke that touches our picking point
455 si = self.doc.layer.get_stroke_info_at(x, y)
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)
464 def strokeblink_state_enter(self):
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
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?
480 self.tdw.current_layer_solo = False
481 self.tdw.queue_draw()
482 def layersolo_state_enter(self):
483 s = self.layerblink_state
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()
492 #def blink_layer_cb(self, action):
493 # self.layerblink_state.activate(action)
495 def solo_layer_cb(self, action):
496 self.layersolo_state.toggle(action)
498 def new_layer_cb(self, action):
499 insert_idx = self.doc.layer_idx
500 if action.get_name() == 'NewLayerFG':
502 self.doc.add_layer(insert_idx)
503 self.layerblink_state.activate(action)
506 def merge_layer_cb(self, action):
507 if self.doc.merge_layer_down():
508 self.layerblink_state.activate(action)
510 def toggle_layers_above_cb(self, action):
511 self.tdw.toggle_show_layers_above()
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()
519 state = self.popup_states[action.get_name()]
520 state.activate(action)
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()
529 adj2 = self.app.brush_adjustment['radius_logarithmic']
531 self.eraser_mode_original_radius = r
532 adj2.set_value(r + self.eraser_mode_radius_change)
534 def end_eraser_mode(self):
535 adj = self.app.brush_adjustment['eraser']
536 if not adj.get_value() > 0.9:
539 if self.eraser_mode_original_radius:
540 # save eraser radius, restore old radius
541 adj2 = self.app.brush_adjustment['radius_logarithmic']
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
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
557 print 'device change:', new_device.name, new_device.source
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
569 bm = self.app.brushmanager
570 bm.brush_by_device[old_device.name] = (bm.selected_brush, self.app.brush.save_to_string())
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)
579 # first time using that device
580 adj = self.app.brush_adjustment['eraser']
581 if is_eraser(new_device):
584 elif not is_eraser(new_device) and is_eraser(old_device):
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)
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)
603 def brighter_cb(self, action):
604 self.end_eraser_mode()
605 h, s, v = self.app.brush.get_color_hsv()
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()
614 self.app.brush.set_color_hsv((h, s, v))
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)
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)
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
628 if not self.app.filehandler.confirm_destructive_action(title=_('Quit'), question=_('Really Quit?')):
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())
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)
650 def zoom(self, command):
651 if command == 'ZoomIn' : self.zoomlevel += 1
652 elif command == 'ZoomOut': self.zoomlevel -= 1
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]
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)
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)
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
676 x, y = self.get_position()
677 w, h = self.get_size()
678 self.geometry_before_fullscreen = (x, y, w, h)
680 self.window.fullscreen()
681 #self.tdw.set_scroll_at_edges(True)
683 self.window.unfullscreen()
685 #self.tdw.set_scroll_at_edges(False)
686 del self.geometry_before_fullscreen
688 def context_cb(self, action):
689 name = action.get_name()
691 bm = self.app.brushmanager
692 if name == 'ContextStore':
693 context = bm.selected_context
695 print 'No context was selected, ignoring store command.'
699 if name.endswith('s'):
703 context = bm.contexts[i]
704 bm.selected_context = context
706 context.copy_settings_from(self.app.brush)
707 context.preview = bm.selected_brush.preview
710 # restore (but keep color)
711 color = self.app.brush.get_color_hsv()
712 context.set_color_hsv(color)
713 bm.select_brush(context)
715 def menu_show_cb(self, action):
718 self.menubar.select_first(False)
720 def menu_done_cb(self, *a, **kw):
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)
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"
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.")
741 d.set_wrap_license(True)
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'),
761 u'Sebastian Kraft (%s)' % _('desktop icon'),
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'
769 u'Tobias Jakobs (de)\n'
770 u'Martin Tabačan (cs)\n'
772 u'Manuel Quiñones (es)\n'
773 u'Gergely Aradszki (hu)\n'
774 u'Lamberto Tedaldi (it)\n'
780 def show_infodialog_cb(self, action):
783 _("Move your mouse over a menu entry, then press the key to assign."),
785 _("You can also drag the canvas with the mouse while holding the middle "
786 "mouse button or spacebar. Or with the arrow keys."
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."),
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."
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. "),
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. "
811 self.app.message_dialog(text[action.get_name()])