# 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 osso, gtk, os, zlib, random import infinitemydrawwidget import brush, document import command from time import time # Hildon import hildon class Window(hildon.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) # Hildon harware keys self.connect("key-press-event", self.on_key_press) self.connect("window-state-event", self.on_window_state_change) self.window_in_fullscreen = False #The window isn't in full screen mode initially. 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) # ARNIM Removed statusbar 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] # ARNIM removed extreme zoomout levels self.zoomlevel_values = [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', 'L', 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', 'X', None, self.undo_cb), ('Redo', None, 'Redo', 'Y', None, self.redo_cb), ('ModifyLastStroke', None, 'Modify Last Stroke', '', None, self.modify_last_stroke_cb), ('ModifyEnd', None, 'Stop Modifying', '', None, self.modify_end_cb), ('LowerLastStroke', None, 'Lower Last Stroke (Experimental!)', '', None, self.lower_or_raise_last_stroke_cb), ('RaiseLastStroke', None, 'Raise Last Stroke', '', None, self.lower_or_raise_last_stroke_cb), ('BrushMenu', None, 'Brush'), ('InvertColor', None, 'Invert Color', 'h', None, self.invert_color_cb), ('Brighter', None, 'Brighter', 'a', None, self.brighter_cb), ('Darker', None, 'Darker', 'z', None, self.darker_cb), ('Bigger', None, 'Bigger', 'd', None, self.brush_bigger_cb), ('Smaller', None, 'Smaller', 'c', None, self.brush_smaller_cb), ('MoreOpaque', None, 'More Opaque', 's', None, self.more_opaque_cb), ('LessOpaque', None, 'Less Opaque', 'x', None, self.less_opaque_cb), ('PickColor', None, 'Pick Color', 'f', None, self.pick_color_cb), ('ChangeColor', None, 'Change Color', 'v', None, self.change_color_cb), ('ContextMenu', None, 'Brushkeys'), ('Context00', None, 'Restore Brush 0', 'q', None, self.context_cb), ('Context00s', None, 'Save to Brush 0', 'q', None, self.context_cb), ('Context01', None, 'Restore 1', 'w', None, self.context_cb), ('Context01s', None, 'Save 1', 'w', None, self.context_cb), ('Context02', None, 'Restore 2', 'e', None, self.context_cb), ('Context02s', None, 'Save 2', 'e', None, self.context_cb), ('Context03', None, 'Restore 3', 'r', None, self.context_cb), ('Context03s', None, 'Save 3', 'r', None, self.context_cb), ('Context04', None, 'Restore 4', 't', None, self.context_cb), ('Context04s', None, 'Save 4', 't', None, self.context_cb), ('Context05', None, 'Restore 5', 'y', None, self.context_cb), ('Context05s', None, 'Save 5', 'y', None, self.context_cb), ('Context06', None, 'Restore 6', 'u', None, self.context_cb), ('Context06s', None, 'Save 6', 'u', None, self.context_cb), ('Context07', None, 'Restore 7', 'i', None, self.context_cb), ('Context07s', None, 'Save 7', 'i', None, self.context_cb), ('Context08', None, 'Restore 8', 'o', None, self.context_cb), ('Context08s', None, 'Save 8', 'o', None, self.context_cb), ('Context09', None, 'Restore 9', 'p', None, self.context_cb), ('Context09s', None, 'Save 9', 'p', None, self.context_cb), ('ContextStore', None, 'Save to Most Recently Restored', 'period', 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', 'n', 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', 'comma', 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', '', None, self.move_cb), ('MoveRight', None, 'Move Right', '', None, self.move_cb), ('MoveUp', None, 'Move Up', '', None, self.move_cb), ('MoveDown', None, 'Move Down', '', 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) # Hildon ui_manager = self.ui menu_bar = ui_manager.get_widget("/Menubar") hildon_style_menu = gtk.Menu() for menu_component in menu_bar.get_children() : menu_component.reparent( hildon_style_menu ) self.set_menu( hildon_style_menu ) menu_bar.destroy() # Hildon hardware keys def on_window_state_change(self, widget, event, *args): if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN: self.window_in_fullscreen = True else: self.window_in_fullscreen = False def on_key_press(self, widget, event, *args): if event.keyval == gtk.keysyms.F6: # The "Full screen" hardware key has been pressed if self.window_in_fullscreen: self.window.unfullscreen () else: self.window.fullscreen () 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() # ARNIM if not self.modifying: # ARNIM self.statusbar.push(3, 'modifying - change brush or color now') # ARNIM else: # ARNIM self.statusbar.pop(3) # ARNIM self.statusbar.push(3, 'modifying %d strokes' % count) self.modifying = count self.last_modifying_time = time() def end_modifying(self): assert self.modifying # ARNIM 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): # ARNIM 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() # ARNIM if self.paint_below_stroke: # ARNIM self.statusbar.push(4, 'painting below other strokes (slow)') # ARNIM else: # ARNIM 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() # ARNIM if count == 1: # ARNIM self.statusbar.pop(3) # ARNIM 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') # hildon zoom elif event.keyval == gtk.keysyms.F7: self.zoom('ZoomIn') elif event.keyval == gtk.keysyms.F8: self.zoom('ZoomOut') # ARNIM undo elif event.keyval == gtk.keysyms.Escape: self.undo_cb # ARNIM redo elif event.keyval == gtk.keysyms.F5: self.redo_cb 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) # ARNIM self.statusbar.pop(1) # FIXME hm? undoable? self.layer.rerender() def invert_color_cb(self, action): self.app.brush.invert_color() hildon.hildon_banner_show_information(self, None, 'Invert Color') def pick_color_cb(self, action): self.app.colorSelectionWindow.pick_color_at_pointer() hildon.hildon_banner_show_information(self, None, 'Hold and Pick Color') 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'] v = adj.get_value() + 0.3 adj.set_value(v) hildon.hildon_banner_show_information(self, None, 'Radius %.2f' % v) def brush_smaller_cb(self, action): adj = self.app.brush_adjustment['radius_logarithmic'] v = adj.get_value() - 0.3 adj.set_value(v) hildon.hildon_banner_show_information(self, None, 'Radius %.2f' % v) def more_opaque_cb(self, action): # FIXME: hm, looks this slider should be logarithmic? adj = self.app.brush_adjustment['opaque'] v = (adj.get_value() * 1.8) adj.set_value(v) hildon.hildon_banner_show_information(self, None, 'Opacity %.2f' % v) def less_opaque_cb(self, action): adj = self.app.brush_adjustment['opaque'] v = (adj.get_value() / 1.8) adj.set_value(v) hildon.hildon_banner_show_information(self, None, 'Opacity %.2f' % v) 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)) hildon.hildon_banner_show_information(self, None, 'Brightness %.2f' % 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)) hildon.hildon_banner_show_information(self, None, 'Brightness %.2f' % 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() # ARNIM self.statusbar.pop(1) # ARNIM self.statusbar.push(1, 'Loaded from ' + filename) def save_file(self, filename): self.finish_pending_actions() self.mdw.save(filename) # ARNIM self.statusbar.pop(1) # ARNIM 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)) # Hildon dialog = hildon.FileChooserDialog(self, gtk.FILE_CHOOSER_ACTION_OPEN); 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)) # Hildon dialog = hildon.FileChooserDialog(self, gtk.FILE_CHOOSER_ACTION_SAVE); 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] # ARNIM hildon.hildon_banner_show_information(self, None, 'Zoom %.2f' % z) #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()