# coding: utf8 # # This file is part of MyPaint. # Copyright (C) 2007 by Martin Renold # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY. See the COPYING file for more details. "the main drawing window" import gtk, os, zlib, random import infinitemydrawwidget import brush, document import command from time import time class Window(gtk.Window): def __init__(self, app): gtk.Window.__init__(self) self.app = app self.set_title('MyPaint') def delete_event_cb(window, event, app): return app.quit() self.connect('delete-event', delete_event_cb, self.app) self.connect('key-press-event', self.key_press_event_cb_before) self.connect_after('key-press-event', self.key_press_event_cb_after) self.set_size_request(600, 400) vbox = gtk.VBox() self.add(vbox) self.create_ui() vbox.pack_start(self.ui.get_widget('/Menubar'), expand=False) self.mdw = infinitemydrawwidget.InfiniteMyDrawWidget() self.mdw.allow_dragging() self.mdw.clear() self.mdw.set_brush(self.app.brush) vbox.pack_start(self.mdw) self.mdw.toolchange_observers.append(self.toolchange_cb) self.statusbar = sb = gtk.Statusbar() vbox.pack_end(sb, expand=False) #self.zoomlevel_values = [0.09, 0.12, 0.18, 0.25, 0.33, 0.50, 0.66, 1.0, 1.5, 2.0, 3.0, 4.0, 5.5, 8.0] 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, 16.0] self.zoomlevel = self.zoomlevel_values.index(1.0) self.modifying = False self.paint_below_stroke = None self.layer = document.Layer(self.mdw) self.command_stack = command.CommandStack() self.stroke = document.Stroke() self.stroke.start_recording(self.mdw, self.app.brush) self.pending_actions = [self.split_stroke] self.app.brush.observers.append(self.brush_modified_cb) self.app.brush.connect("split-stroke", self.split_stroke_cb) self.init_child_dialogs() def create_ui(self): ag = gtk.ActionGroup('WindowActions') # FIXME: this xml menu ony creates unneeded information duplication, I think. ui_string = """ """ actions = [ ('FileMenu', None, 'File'), ('Clear', None, 'Clear', 'period', None, self.clear_cb), ('Open', None, 'Open', 'O', None, self.open_cb), ('Save', None, 'Save', 'S', None, self.save_cb), ('Quit', None, 'Quit', None, None, self.quit_cb), ('EditMenu', None, 'Edit'), ('Undo', None, 'Undo', 'Z', None, self.undo_cb), ('Redo', None, 'Redo', 'Y', None, self.redo_cb), ('ModifyLastStroke', None, 'Modify Last Stroke', 'm', None, self.modify_last_stroke_cb), ('ModifyEnd', None, 'Stop Modifying', 'n', None, self.modify_end_cb), ('LowerLastStroke', None, 'Lower Last Stroke (Experimental, slow!)', 'Page_Down', None, self.lower_or_raise_last_stroke_cb), ('RaiseLastStroke', None, 'Raise Last Stroke', 'Page_Up', None, self.lower_or_raise_last_stroke_cb), ('BrushMenu', None, 'Brush'), ('InvertColor', None, 'Invert Color', 'x', None, self.invert_color_cb), ('Brighter', None, 'Brighter', None, None, self.brighter_cb), ('Darker', None, 'Darker', None, None, self.darker_cb), ('Bigger', None, 'Bigger', 'f', None, self.brush_bigger_cb), ('Smaller', None, 'Smaller', 'd', None, self.brush_smaller_cb), ('MoreOpaque', None, 'More Opaque', 's', None, self.more_opaque_cb), ('LessOpaque', None, 'Less Opaque', 'a', None, self.less_opaque_cb), ('PickColor', None, 'Pick Color', 'r', None, self.pick_color_cb), ('ChangeColor', None, 'Change Color', 'v', None, self.change_color_cb), ('ContextMenu', None, 'Brushkeys'), ('Context00', None, 'Restore Brush 0', '0', None, self.context_cb), ('Context00s', None, 'Save to Brush 0', '0', None, self.context_cb), ('Context01', None, 'Restore 1', '1', None, self.context_cb), ('Context01s', None, 'Save 1', '1', None, self.context_cb), ('Context02', None, 'Restore 2', '2', None, self.context_cb), ('Context02s', None, 'Save 2', '2', None, self.context_cb), ('Context03', None, 'Restore 3', '3', None, self.context_cb), ('Context03s', None, 'Save 3', '3', None, self.context_cb), ('Context04', None, 'Restore 4', '4', None, self.context_cb), ('Context04s', None, 'Save 4', '4', None, self.context_cb), ('Context05', None, 'Restore 5', '5', None, self.context_cb), ('Context05s', None, 'Save 5', '5', None, self.context_cb), ('Context06', None, 'Restore 6', '6', None, self.context_cb), ('Context06s', None, 'Save 6', '6', None, self.context_cb), ('Context07', None, 'Restore 7', '7', None, self.context_cb), ('Context07s', None, 'Save 7', '7', None, self.context_cb), ('Context08', None, 'Restore 8', '8', None, self.context_cb), ('Context08s', None, 'Save 8', '8', None, self.context_cb), ('Context09', None, 'Restore 9', '9', None, self.context_cb), ('Context09s', None, 'Save 9', '9', None, self.context_cb), ('ContextStore', None, 'Save to Most Recently Restored', 'q', None, self.context_cb), ('ContextHelp', None, 'Help!', None, None, self.context_help_cb), ('DialogMenu', None, 'Dialogs'), ('BrushSelectionWindow', None, 'Brush List', 'b', None, self.toggleBrushSelectionWindow_cb), ('BrushSettingsWindow', None, 'Brush Settings', 'b', None, self.toggleBrushSettingsWindow_cb), ('ColorSelectionWindow', None, 'GTK Color Dialog', 'g', None, self.toggleColorSelectionWindow_cb), ('HelpMenu', None, 'Help'), ('Docu', None, 'Where is the Documentation?', None, None, self.show_docu_cb), ('ShortcutHelp', None, 'Change the Keyboard Shortcuts?', None, None, self.shortcut_help_cb), ('About', None, 'About MyPaint', None, None, self.show_about_cb), ('DebugMenu', None, 'Debug'), ('PrintInputs', None, 'Print Brush Input Values to stdout', None, None, self.print_inputs_cb), ('DontPrintInputs', None, 'Stop Printing Them', None, None, self.dont_print_inputs_cb), ('ShortcutsMenu', None, 'Shortcuts'), ('ViewMenu', None, 'View'), ('Zoom1', None, 'Zoom 1:1', 'z', None, self.zoom_cb), ('ZoomIn', None, 'Zoom In', 'plus', None, self.zoom_cb), ('ZoomOut', None, 'Zoom Out', 'minus', None, self.zoom_cb), ('MoveLeft', None, 'Move Left', 'h', None, self.move_cb), ('MoveRight', None, 'Move Right', 'l', None, self.move_cb), ('MoveUp', None, 'Move Up', 'k', None, self.move_cb), ('MoveDown', None, 'Move Down', 'j', None, self.move_cb), ('ViewHelp', None, 'Help', None, None, self.view_help_cb), ] ag.add_actions(actions) self.ui = gtk.UIManager() self.ui.insert_action_group(ag, 0) self.ui.add_ui_from_string(ui_string) self.app.accel_group = self.ui.get_accel_group() self.add_accel_group(self.app.accel_group) def toggleWindow(self, w): if w.get_property('visible'): w.hide() else: #w.show() w.show_all() # might be for the first time def toggleBrushSelectionWindow_cb(self, action): self.toggleWindow(self.app.brushSelectionWindow) def toggleBrushSettingsWindow_cb(self, action): self.toggleWindow(self.app.brushSettingsWindow) def toggleColorSelectionWindow_cb(self, action): self.toggleWindow(self.app.colorSelectionWindow) def print_inputs_cb(self, action): self.app.brush.set_print_inputs(1) def dont_print_inputs_cb(self, action): self.app.brush.set_print_inputs(0) def finish_pending_actions(self, skip=None): # this function must be called before manipulation the command stack for f in self.pending_actions[:]: if f == skip: continue if f not in self.pending_actions: continue # list modified f() def split_stroke(self): # let the brush emit the signal (this calls self.split_stroke_cb) self.app.brush.split_stroke() def split_stroke_cb(self, widget): self.stroke.stop_recording() if not self.stroke.empty: self.finish_pending_actions(skip=self.split_stroke) pbs = self.paint_below_stroke if pbs and pbs in self.layer.strokes: z = self.layer.strokes.index(pbs) else: z = -1 self.command_stack.add(command.Stroke(self.layer, self.stroke, z)) self.layer.rerender() self.layer.populate_cache() self.stroke = document.Stroke() self.stroke.start_recording(self.mdw, self.app.brush) def undo_cb(self, action): self.finish_pending_actions() self.command_stack.undo() self.layer.rerender() def redo_cb(self, action): self.finish_pending_actions() self.command_stack.redo() self.layer.rerender() def get_recent_strokes(self, max_count): assert max_count > 0 result = self.layer.strokes[:] # copy def cmpfunc(a, b): return cmp(a.serial_number, b.serial_number) result.sort(cmpfunc, reverse=True) return result[:max_count] def modify_last_stroke_cb(self, action): self.start_or_continue_modifying() def start_or_continue_modifying(self, count=1): self.finish_pending_actions(skip=self.end_modifying) if self.modifying: assert self.end_modifying in self.pending_actions count = self.modifying + 1 if count > len(self.layer.strokes): print 'All strokes selected already!' return self.command_stack.undo() else: assert self.end_modifying not in self.pending_actions self.pending_actions.append(self.end_modifying) strokes = self.get_recent_strokes(count) cmd = command.ModifyStrokes(self.layer, strokes, self.app.brush) self.command_stack.add(cmd) self.layer.rerender() if not self.modifying: self.statusbar.push(3, 'modifying - change brush or color now') else: self.statusbar.pop(3) self.statusbar.push(3, 'modifying %d strokes' % count) self.modifying = count self.last_modifying_time = time() def end_modifying(self): assert self.modifying self.statusbar.pop(3) self.modifying = False self.pending_actions.remove(self.end_modifying) def modify_end_cb(self, action): if self.modifying: self.end_modifying() def lower_or_raise_last_stroke_cb(self, action): self.finish_pending_actions() action = action.get_name() cmd = self.command_stack.get_last_command() if not isinstance(cmd, command.Stroke): self.statusbar.push(4, 'last command was not a stroke') return # note: you can undo->lower->redo # this history manipluation is not in the undo/redo spirit # let's say it's a feature, not a bug self.command_stack.undo() if action == 'LowerLastStroke': # lower it such that the visible result changes intersections = [] rect = cmd.stroke.bbox for z, stroke in enumerate(self.layer.strokes): stroke.z = z if stroke is cmd.stroke: continue if rect.overlaps(stroke.bbox): intersections.append(stroke) below = [stroke for stroke in intersections if stroke.z < cmd.z] print len(below), 'strokes are below' if below: def cmpfunc(a, b): return cmp(a.z, b.z) below.sort(cmpfunc) cmd.z = below[-1].z else: cmd.z = 0 # clean up for stroke in self.layer.strokes: del stroke.z elif action == 'RaiseLastStroke': # raise to top, because it is cheapest to render there cmd.z = len(self.layer.strokes) else: assert False if cmd.z < 0: cmd.z = 0 if cmd.z >= len(self.layer.strokes): cmd.z = len(self.layer.strokes) self.paint_below_stroke = None else: self.paint_below_stroke = self.layer.strokes[cmd.z] self.command_stack.redo() self.layer.rerender() if self.paint_below_stroke: self.statusbar.push(4, 'painting below other strokes (slow)') else: self.statusbar.push(4, '') def brush_modified_cb(self): # called at every brush setting modification, should return fast self.split_stroke() if self.modifying: self.finish_pending_actions(skip=self.end_modifying) cmd = self.command_stack.get_last_command() if isinstance(cmd, command.ModifyStrokes): count = self.modifying if time() - self.last_modifying_time > 3: # split into a different undo action # (so the user can recover the previous modification if he selected a brush and forgot that he was still modifying) self.end_modifying() self.start_or_continue_modifying(count) cmd = self.command_stack.get_last_command() self.last_modifying_time = time() #print 'redo', count, 'modified strokes' self.command_stack.undo() cmd.set_new_brush(self.app.brush) self.command_stack.add(cmd) self.layer.rerender() if count == 1: self.statusbar.pop(3) self.statusbar.push(3, 'modifying one stroke (hit again to add more)') def toolchange_cb(self): # FIXME: add argument with tool id, and remember settings # also make sure proximity events outside the window are checked self.split_stroke() def key_press_event_cb_before(self, win, event): ANY_MODIFIER = gtk.gdk.SHIFT_MASK | gtk.gdk.MOD1_MASK | gtk.gdk.CONTROL_MASK if event.state & ANY_MODIFIER: # allow user shortcuts with modifiers return False if event.keyval == gtk.keysyms.Left: self.move('MoveLeft') elif event.keyval == gtk.keysyms.Right: self.move('MoveRight') elif event.keyval == gtk.keysyms.Up: self.move('MoveUp') elif event.keyval == gtk.keysyms.Down: self.move('MoveDown') else: return False return True def key_press_event_cb_after(self, win, event): # Not checking modifiers because this function gets only # called if no user keybinding accepted the event. if event.keyval == gtk.keysyms.KP_Add: self.zoom('ZoomIn') elif event.keyval == gtk.keysyms.KP_Subtract: self.zoom('ZoomOut') else: return False return True def clear_cb(self, action): self.finish_pending_actions() cmd = command.ClearLayer(self.layer) self.command_stack.add(cmd) self.statusbar.pop(1) # FIXME hm? undoable? self.layer.rerender() def invert_color_cb(self, action): self.app.brush.invert_color() def pick_color_cb(self, action): self.app.colorSelectionWindow.pick_color_at_pointer() def change_color_cb(self, action): self.app.colorSelectionWindow.show_change_color_window() def brush_bigger_cb(self, action): adj = self.app.brush_adjustment['radius_logarithmic'] adj.set_value(adj.get_value() + 0.3) def brush_smaller_cb(self, action): adj = self.app.brush_adjustment['radius_logarithmic'] adj.set_value(adj.get_value() - 0.3) def more_opaque_cb(self, action): # FIXME: hm, looks this slider should be logarithmic? adj = self.app.brush_adjustment['opaque'] adj.set_value(adj.get_value() * 1.8) def less_opaque_cb(self, action): adj = self.app.brush_adjustment['opaque'] adj.set_value(adj.get_value() / 1.8) def brighter_cb(self, action): cs = self.app.colorSelectionWindow cs.update() h, s, v = cs.get_color_hsv() v += 0.08 cs.set_color_hsv((h, s, v)) def darker_cb(self, action): cs = self.app.colorSelectionWindow cs.update() h, s, v = cs.get_color_hsv() v -= 0.08 cs.set_color_hsv((h, s, v)) def open_file(self, filename): self.finish_pending_actions() pixbuf = gtk.gdk.pixbuf_new_from_file(filename) cmd = command.LoadImage(self.layer, pixbuf) self.command_stack.add(cmd) self.layer.rerender() self.statusbar.pop(1) self.statusbar.push(1, 'Loaded from ' + filename) def save_file(self, filename): self.finish_pending_actions() self.mdw.save(filename) self.statusbar.pop(1) self.statusbar.push(1, 'Saved to ' + filename) def init_child_dialogs(self): dialog = gtk.FileChooserDialog("Open..", self, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) filter = gtk.FileFilter() filter.set_name("png") filter.add_pattern("*.png") dialog.add_filter(filter) self.opendialog = dialog dialog = gtk.FileChooserDialog("Save..", self, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) filter = gtk.FileFilter() filter.set_name("png") filter.add_pattern("*.png") dialog.add_filter(filter) self.savedialog = dialog def open_cb(self, action): dialog = self.opendialog if dialog.run() == gtk.RESPONSE_OK: self.open_file(dialog.get_filename()) dialog.hide() def save_cb(self, action): dialog = self.savedialog if dialog.run() == gtk.RESPONSE_OK: filename = dialog.get_filename() trash, ext = os.path.splitext(filename) if not ext: filename += '.png' if os.path.exists(filename): d2 = gtk.Dialog("Overwrite?", self, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_YES, gtk.RESPONSE_ACCEPT, gtk.STOCK_NO, gtk.RESPONSE_REJECT)) if d2.run() != gtk.RESPONSE_ACCEPT: filename = None d2.destroy() if filename: self.save_file(filename) dialog.hide() def quit_cb(self, action): self.finish_pending_actions() return self.app.quit() def move_cb(self, action): self.move(action.get_name()) def zoom_cb(self, action): self.zoom(action.get_name()) def move(self, command): self.split_stroke() step = min(self.mdw.window.get_size()) / 5 if command == 'MoveLeft': self.mdw.scroll(-step, 0) elif command == 'MoveRight': self.mdw.scroll(+step, 0) elif command == 'MoveUp': self.mdw.scroll(0, -step) elif command == 'MoveDown': self.mdw.scroll(0, +step) else: assert 0 self.split_stroke() # record new stroke with new coordinates def zoom(self, command): if command == 'ZoomIn': self.zoomlevel += 1 elif command == 'ZoomOut': self.zoomlevel -= 1 elif command == 'Zoom1': self.zoomlevel = self.zoomlevel_values.index(1.0) else: assert 0 if self.zoomlevel < 0: self.zoomlevel = 0 if self.zoomlevel >= len(self.zoomlevel_values): self.zoomlevel = len(self.zoomlevel_values) - 1 z = self.zoomlevel_values[self.zoomlevel] #self.statusbar.push(2, 'Zoom %.2f' % z) #print 'Zoom %.2f' % z self.split_stroke() self.mdw.zoom(z) self.split_stroke() # record new stroke with new coordinates def context_cb(self, action): # TODO: this context-thing is not very useful like that, is it? # You overwrite your settings too easy by accident. # - not storing settings under certain circumstances? # - think about other stuff... brush history, only those actually used, etc... name = action.get_name() store = False if name == 'ContextStore': context = self.app.selected_context if not context: print 'No context was selected, ignoring store command.' return store = True else: if name.endswith('s'): store = True name = name[:-1] i = int(name[-2:]) context = self.app.contexts[i] self.app.selected_context = context if store: context.copy_settings_from(self.app.brush) preview = self.app.brushSelectionWindow.get_preview_pixbuf() context.update_preview(preview) context.save() else: # restore self.app.select_brush(context) self.app.brushSelectionWindow.set_preview_pixbuf(context.preview) def show_about_cb(self, action): d = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK) d.set_markup( u"MyPaint - pressure sensitive painting application\n" u"Copyright (C) 2005-2007\n" u"Martin Renold <martinxyz@gmx.ch>\n\n" u"Contributors:\n" u"Artis Rozentāls <artis@aaa.apollo.lv> (brushes)\n" u"Yves Combe <yves@ycombe.net> (portability)\n" u"\n" u"This program is free software; you can redistribute it and/or modify " u"it under the terms of the GNU General Public License as published by " u"the Free Software Foundation; either version 2 of the License, or " u"(at your option) any later version.\n" u"\n" u"This program is distributed in the hope that it will be useful," u"but WITHOUT ANY WARRANTY. See the COPYING file for more details." ) d.run() d.destroy() def show_docu_cb(self, action): d = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK) d.set_markup("There is a tutorial in the html directory, also available " "on the MyPaint homepage. It explains the features which are " "hard to discover yourself.\n\n" "Comments about the brush settings (opaque, hardness, etc.) and " "inputs (pressure, speed, etc.) are available as tooltips. " "Put your mouse over a label to see them. " "\n" ) d.run() d.destroy() def context_help_cb(self, action): d = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK) d.set_markup("This is used to quickly save/restore brush settings " "using keyboard shortcuts. You can paint with one hand and " "change brushes with the other without interrupting." "\n\n" "There are 10 memory slots to hold brush settings.\n" "Those are annonymous " "brushes, they are not visible in the brush selector list. " "But they will stay even if you quit. " "They will also remember the selected color. In contrast, selecting a " "normal brush never changes the color. " ) d.run() d.destroy() def shortcut_help_cb(self, action): d = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK) d.set_markup("Move your mouse over a menu entry, then press the key to " "assign.") d.run() d.destroy() def view_help_cb(self, action): d = gtk.MessageDialog(self, buttons=gtk.BUTTONS_OK) d.set_markup( "You can also drag the canvas with the middle mouse button or with the arrow keys.\n\n" "Beware! You might have an infinite canvas, but not infinite memory. " "Whenever you scroll away or zoom away, more memory needs to be allocated." ) d.run() d.destroy()