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.7.0+git"
23 from gtk import gdk, keysyms
25 import tileddrawwidget, colorselectionwindow, historypopup, \
26 stategroup, keyboard, colorpicker
27 from lib import document, helpers, backgroundsurface
29 class Window(gtk.Window):
30 def __init__(self, app):
31 gtk.Window.__init__(self)
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)
46 self.doc = document.Document()
47 self.doc.set_brush(self.app.brush)
50 self.menubar = self.ui.get_widget('/Menubar')
51 vbox.pack_start(self.menubar, expand=False)
53 self.tdw = tileddrawwidget.TiledDrawWidget(self.doc)
54 vbox.pack_start(self.tdw)
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))
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
67 self.app.brush.settings_observers.append(self.brush_modified_cb)
68 self.tdw.device_observers.append(self.device_changed_cb)
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)]
74 self.save_history = []
76 self.init_save_dialog()
78 #filename is a property so that all changes will update the title
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
85 def get_filename(self):
87 def set_filename(self,value):
88 self._filename = value
90 self.set_title("MyPaint - %s" % os.path.basename(self.filename))
92 self.set_title("MyPaint")
93 filename = property(get_filename, set_filename)
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...
100 <menubar name='Menubar'>
101 <menu action='FileMenu'>
102 <menuitem action='New'/>
103 <menuitem action='Open'/>
104 <menuitem action='OpenRecent'/>
106 <menuitem action='Save'/>
107 <menuitem action='SaveAs'/>
108 <menuitem action='SaveScrap'/>
110 <menuitem action='Quit'/>
112 <menu action='EditMenu'>
113 <menuitem action='Undo'/>
114 <menuitem action='Redo'/>
116 <menuitem action='CopyLayer'/>
117 <menuitem action='PasteLayer'/>
119 <menuitem action='SettingsWindow'/>
121 <menu action='ViewMenu'>
122 <menuitem action='Fullscreen'/>
124 <menuitem action='ResetView'/>
125 <menuitem action='ZoomIn'/>
126 <menuitem action='ZoomOut'/>
127 <menuitem action='RotateLeft'/>
128 <menuitem action='RotateRight'/>
129 <menuitem action='Flip'/>
131 <menuitem action='ViewHelp'/>
133 <menu action='BrushMenu'>
134 <menuitem action='BrushSelectionWindow'/>
135 <menu action='ContextMenu'>
136 <menuitem action='ContextStore'/>
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'/>
159 <menuitem action='ContextHelp'/>
162 <menuitem action='BrushSettingsWindow'/>
164 <menuitem action='Bigger'/>
165 <menuitem action='Smaller'/>
166 <menuitem action='MoreOpaque'/>
167 <menuitem action='LessOpaque'/>
169 <menuitem action='Eraser'/>
171 <menuitem action='PickContext'/>
173 <menu action='ColorMenu'>
174 <menuitem action='ColorSelectionWindow'/>
175 <menuitem action='ColorRingPopup'/>
176 <menuitem action='ColorChangerPopup'/>
177 <menuitem action='ColorPickerPopup'/>
178 <menuitem action='ColorHistoryPopup'/>
180 <menuitem action='Brighter'/>
181 <menuitem action='Darker'/>
183 <menu action='LayerMenu'>
184 <menuitem action='BackgroundWindow'/>
185 <menuitem action='ClearLayer'/>
187 <menuitem action='NewLayerFG'/>
188 <menuitem action='NewLayerBG'/>
189 <menuitem action='RemoveLayer'/>
190 <menuitem action='MergeLayer'/>
192 <menuitem action='PickLayer'/>
193 <menuitem action='LayerFG'/>
194 <menuitem action='LayerBG'/>
195 <menuitem action='SoloLayer'/>
196 <menuitem action='ToggleAbove'/>
198 <menuitem action='IncreaseLayerOpacity'/>
199 <menuitem action='DecreaseLayerOpacity'/>
201 <menu action='DebugMenu'>
202 <menuitem action='PrintInputs'/>
203 <menuitem action='VisualizeRendering'/>
204 <menuitem action='NoDoubleBuffereing'/>
206 <menu action='HelpMenu'>
207 <menuitem action='Docu'/>
208 <menuitem action='ShortcutHelp'/>
210 <menuitem action='About'/>
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),
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),
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),
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),
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),
272 ('LayerMenu', None, 'Layers'),
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),
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),
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),
298 ('DebugMenu', None, 'Debug'),
301 ('ShortcutsMenu', None, 'Shortcuts'),
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),
312 ag.add_actions(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),
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()
326 self.app.kbm = kbm = keyboard.KeyboardManager()
329 for action in ag.list_actions():
330 self.app.kbm.takeover_action(action)
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')
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'))
342 kbm.add_extra_key('<control>Left', 'RotateLeft')
343 kbm.add_extra_key('<control>Right', 'RotateRight')
345 sg = stategroup.StateGroup()
346 self.layerblink_state = sg.create_state(self.layerblink_state_enter, self.layerblink_state_leave)
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
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))
359 self.popup_states = {
360 'ColorChangerPopup': changer,
361 'ColorRingPopup': ring,
362 'ColorHistoryPopup': hist,
363 'ColorPickerPopup': pick,
365 changer.next_state = ring
366 ring.next_state = changer
367 changer.autoleave_timeout = None
368 ring.autoleave_timeout = None
370 pick.max_key_hit_duration = 0.0
371 pick.autoleave_timeout = None
373 hist.autoleave_timeout = 0.600
374 self.history_popup_state = hist
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)
385 func(self, *args, **kwargs)
387 self.window.set_cursor(None)
388 self.tdw.update_cursor()
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():
398 w.show_all() # might be for the first time
401 def print_inputs_cb(self, action):
402 self.doc.brush.print_inputs = action.get_active()
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())
409 def start_profiling(self):
412 events = pylab.load('painting30sec.dat.gz')
414 events = list(events)
417 for t, x, y, pressure in events:
418 sleeptime = t-(time()-t0)
419 if sleeptime > 0.001:
423 self.doc.stroke_to(dtime, x, y, pressure)
425 print self.repaints, 'repaints'
432 gobject.timeout_add(int(p.next()*1000.0), timer_cb)
435 oldfunc=self.tdw.repaint
436 def count_repaints(*args, **kwargs):
438 return oldfunc(*args, **kwargs)
439 self.tdw.repaint = count_repaints
442 self.tdw.rotate(46.0/360*2*math.pi)
444 def undo_cb(self, action):
447 def redo_cb(self, action):
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)
457 def paste_cb(self, action):
459 def callback(clipboard, pixbuf, trash):
461 print 'The clipboard doeas not contain any image to paste!'
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)
468 def brush_modified_cb(self):
469 # called at every brush setting modification, should return fast
470 self.doc.set_brush(self.app.brush)
472 def key_press_event_cb_before(self, win, event):
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
479 if key == keysyms.space:
481 self.tdw.start_drag(self.dragfunc_rotate)
483 self.tdw.start_drag(self.dragfunc_translate)
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)
493 def key_press_event_cb_after(self, win, event):
495 if self.fullscreen and key == keysyms.Escape: self.fullscreen_cb()
498 def key_release_event_cb_after(self, win, event):
501 def dragfunc_translate(self, dx, dy):
502 self.tdw.scroll(-dx, -dy)
504 def dragfunc_rotate(self, dx, dy):
505 self.tdw.scroll(-dx, -dy, False)
506 self.tdw.rotate(2*math.pi*dx/500.0)
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)
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
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)
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)
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)
541 def scroll_cb(self, win, event):
543 if d == gdk.SCROLL_UP:
544 if event.state & gdk.SHIFT_MASK:
545 self.rotate('RotateLeft')
548 elif d == gdk.SCROLL_DOWN:
549 if event.state & gdk.SHIFT_MASK:
550 self.rotate('RotateRight')
553 elif d == gdk.SCROLL_LEFT:
554 self.rotate('RotateRight')
555 elif d == gdk.SCROLL_LEFT:
556 self.rotate('RotateLeft')
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
565 def remove_layer_cb(self, action):
566 if len(self.doc.layers) == 1:
567 self.doc.clear_layer()
569 self.doc.remove_layer()
570 self.layerblink_state.activate(action)
572 def layer_bg_cb(self, action):
573 idx = self.doc.layer_idx - 1
576 self.doc.select_layer(idx)
577 self.layerblink_state.activate(action)
579 def layer_fg_cb(self, action):
580 idx = self.doc.layer_idx + 1
581 if idx >= len(self.doc.layers):
583 self.doc.select_layer(idx)
584 self.layerblink_state.activate(action)
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
591 self.doc.select_layer(idx)
592 self.layerblink_state.activate(action)
594 self.doc.select_layer(0)
595 self.layerblink_state.activate(action)
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
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)
607 # find the most recent (last) stroke that touches our picking point
608 brush = self.doc.layer.get_brush_at(x, y)
611 # FIXME: clean brush concept?
612 self.app.brush.load_from_string(brush)
613 self.app.select_brush(None)
615 print 'Nothing found!'
617 #self.app.brush.copy_settings_from(stroke.brush_settings)
618 #self.app.select_brush()
621 self.doc.select_layer(0)
622 self.layerblink_state.activate(action)
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?
631 self.tdw.current_layer_solo = False
632 self.tdw.queue_draw()
633 def layersolo_state_enter(self):
634 s = self.layerblink_state
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()
643 #def blink_layer_cb(self, action):
644 # self.layerblink_state.activate(action)
646 def solo_layer_cb(self, action):
647 self.layersolo_state.toggle(action)
649 def new_layer_cb(self, action):
650 insert_idx = self.doc.layer_idx
651 if action.get_name() == 'NewLayerFG':
653 self.doc.add_layer(insert_idx)
654 self.layerblink_state.activate(action)
657 def merge_layer_cb(self, action):
658 dst_idx = self.doc.layer_idx - 1
661 self.doc.merge_layer(dst_idx)
662 self.layerblink_state.activate(action)
664 def toggle_layers_above_cb(self, action):
665 self.tdw.toggle_show_layers_above()
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()
673 state = self.popup_states[action.get_name()]
674 state.activate(action)
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()
683 adj2 = self.app.brush_adjustment['radius_logarithmic']
685 self.eraser_mode_original_radius = r
686 adj2.set_value(r + self.eraser_mode_radius_change)
688 def end_eraser_mode(self):
689 adj = self.app.brush_adjustment['eraser']
690 if not adj.get_value() > 0.9:
693 if self.eraser_mode_original_radius:
694 # save eraser radius, restore old radius
695 adj2 = self.app.brush_adjustment['radius_logarithmic']
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
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
708 if new_device.source == gdk.SOURCE_ERASER:
711 elif new_device.source != gdk.SOURCE_ERASER and \
712 (old_device is None or old_device.source == gdk.SOURCE_ERASER):
715 print 'device change:', new_device.name
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)
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)
732 def brighter_cb(self, action):
733 self.end_eraser_mode()
734 h, s, v = self.app.brush.get_color_hsv()
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()
743 self.app.brush.set_color_hsv((h, s, v))
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()
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()
754 def open_file(self, filename):
756 self.doc.load(filename)
757 except document.SaveLoadError, e:
758 d = gtk.MessageDialog(self, type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK)
763 self.filename = os.path.abspath(filename)
764 print 'Loaded from', self.filename
765 self.reset_view_cb(None)
766 self.tdw.recenter_document()
769 def save_file(self, filename, **options):
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)
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))
792 def confirm_destructive_action(self, title='Confirm', question='Really continue?'):
793 t = self.doc.unsaved_painting_time
799 t = '%d minutes' % (t/60)
802 d = gtk.Dialog(title,
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)
810 l.set_markup("<b>" + question + "</b>\n\nThis will discard %s of unsaved painting." % t)
811 l.set_padding(10, 10)
816 return response == gtk.RESPONSE_OK
818 def new_cb(self, action):
819 if not self.confirm_destructive_action():
821 bg = self.doc.background
823 self.doc.set_background(bg)
826 def open_cb(self, action):
827 if not self.confirm_destructive_action():
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)
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")
844 f.set_name("OpenRaster (*.ora)")
845 f.add_pattern("*.ora")
849 f.set_name("PNG (*.png)")
850 f.add_pattern("*.png")
854 f.set_name("JPEG (*.jpg; *.jpeg)")
855 f.add_pattern("*.jpg")
856 f.add_pattern("*.jpeg")
860 dialog.set_filename(self.filename)
862 if dialog.run() == gtk.RESPONSE_OK:
863 self.open_file(dialog.get_filename())
867 def save_cb(self, action):
868 if not self.filename:
869 self.save_as_cb(action)
871 self.save_file(self.filename)
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)
884 self.filter2info = filter2info
887 filter2info[f] = ('.ora', {})
888 f.set_name("Any format (prefer OpenRaster)")
889 self.save_filter_default = f
891 f.add_pattern("*.png")
892 f.add_pattern("*.ora")
893 f.add_pattern("*.jpg")
894 f.add_pattern("*.jpeg")
898 filter2info[f] = ('.ora', {})
899 f.set_name("OpenRaster (*.ora)")
900 f.add_pattern("*.ora")
904 filter2info[f] = ('.png', {'alpha': False})
905 f.set_name("PNG solid with background (*.png)")
906 f.add_pattern("*.png")
910 filter2info[f] = ('.png', {'alpha': True})
911 f.set_name("PNG transparent (*.png)")
912 f.add_pattern("*.png")
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")
922 def save_as_cb(self, action):
923 dialog = self.save_dialog
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)
933 dialog_set_filename(self.filename)
935 dialog_set_filename('')
936 dialog.set_filter(self.save_filter_default)
939 while dialog.run() == gtk.RESPONSE_OK:
941 filename = dialog.get_filename()
942 name, ext = os.path.splitext(filename)
943 ext_filter, options = self.filter2info.get(dialog.get_filter(), ('ora', {}))
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.
953 self.save_file(filename, **options)
956 # add proper extension
957 filename = name + ext_filter
959 # trigger overwrite confirmation for the modified filename
960 dialog_set_filename(filename)
961 dialog.response(gtk.RESPONSE_OK)
966 def save_scrap_cb(self, action):
967 filename = self.filename
968 prefix = self.app.settingsWindow.save_scrap_prefix
972 l = re.findall(re.escape(prefix) + '([0-9]+)', filename)
977 # reuse the number, find the next character
979 for filename in glob(prefix + number + '_*'):
980 c = filename[len(prefix + number + '_')]
981 if c >= 'a' and c <= 'z' and c >= char:
984 # out of characters, increase the number
986 return self.save_scrap_cb(action)
987 filename = '%s%s_%c' % (prefix, number, char)
989 # we don't have a scrap filename yet, find the next number
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)
998 filename = '%s%03d_a' % (prefix, maximum+1)
1000 #if self.doc.is_layered():
1001 # filename += '.ora'
1003 # filename += '.png'
1006 assert not os.path.exists(filename)
1007 self.save_file(filename)
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
1020 # pick the next most recent file from the history
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):
1031 if not self.confirm_destructive_action():
1033 self.open_file(self.save_history[idx])
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
1039 if not self.confirm_destructive_action(title='Quit', question='Really Quit?'):
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())
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)
1061 def zoom(self, command):
1062 if command == 'ZoomIn' : self.zoomlevel += 1
1063 elif command == 'ZoomOut': self.zoomlevel -= 1
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)
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)
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)
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
1088 x, y = self.get_position()
1089 w, h = self.get_size()
1090 self.geometry_before_fullscreen = (x, y, w, h)
1092 self.window.fullscreen()
1093 #self.tdw.set_scroll_at_edges(True)
1095 self.window.unfullscreen()
1097 #self.tdw.set_scroll_at_edges(False)
1098 del self.geometry_before_fullscreen
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()
1107 if name == 'ContextStore':
1108 context = self.app.selected_context
1110 print 'No context was selected, ignoring store command.'
1114 if name.endswith('s'):
1118 context = self.app.contexts[i]
1119 self.app.selected_context = context
1121 context.copy_settings_from(self.app.brush)
1122 preview = self.app.brushSelectionWindow.get_preview_pixbuf()
1123 context.update_preview(preview)
1126 self.app.select_brush(context)
1127 self.app.brushSelectionWindow.set_preview_pixbuf(context.preview)
1129 def show_about_cb(self, action):
1130 d = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK)
1133 u"MyPaint %s - pressure sensitive painting application\n"
1134 u"Copyright (C) 2005-2009\n"
1135 u"Martin Renold <martinxyz@gmx.ch>\n\n"
1137 u"Artis Rozentāls <artis@aaa.apollo.lv> (brushes)\n"
1138 u"Yves Combe <yves@ycombe.net> (portability)\n"
1139 u"Sebastian Kraft (desktop icon)\n"
1140 u"Popolon <popolon@popolon.org> (brushes)\n"
1141 u"Clement Skau <clementskau@gmail.com> (programming)\n"
1142 u'Marcelo "Tanda" Cerviño <info@lodetanda.com.ar> (patterns, brushes)\n'
1143 u'Jon Nordby <jononor@gmail.com> (programming)\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"
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."
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. "
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."
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. "
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 "
1194 def view_help_cb(self, action):
1195 d = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK)
1197 "You can also drag the canvas with the mouse while holding the middle mouse button or spacebar. "
1198 "or with the arrow keys."
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."