diff --git a/pyPenNotes/trunk/src/SaveRestore.py b/pyPenNotes/trunk/src/SaveRestore.py new file mode 100644 index 0000000..1f8f51c --- /dev/null +++ b/pyPenNotes/trunk/src/SaveRestore.py @@ -0,0 +1,185 @@ +""" + * saveRestore.py - pyPenNotes - Save and restore the notes + * + * (C) 2008 by Kristian Mueller + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +import os +from pyPenNotes import COLOR_LIST + +class PenNote: + def __init__(self, bg_color="#000000000000", strokes=[]): + self.bg_color = bg_color + self.strokes = strokes[:] # Copy to make sure we don't run into strange bugs due to mutable objects + + +class BaseSaveRestore: + """Abstract class for saving and restoring notes.""" + + def __init__(self): + + # Changes that are not yet saved. + self.changes = {} + + # Quality loss in the compression (remove unneeded points). Measured in pixels. + self.quality_loss = 0 + + def revert_changes(self): + self.changes = {} + + def get_note(self, index): + """ + Abstract. Get a note by it's index. + Returns a SaveRestore.PenNote + """ + pass + + def in_line(self, point1, point2, point3): + """Check if the three points are in line, making the point in the middle unneeded.""" + + # Check if point2 is equal to or at most 'self.quality_loss' pixels different than point1 + if abs(point1[0] - point2[0]) < self.quality_loss \ + and abs(point1[1] - point2[1]) < self.quality_loss: + return True + + # Todo: Detect more unneeded points by looking for straight lines etc. + + def compress_note(self, note): + """ + Compress a note by removing unneeded points. + + 'note': The note to compress + """ + count = 0 + for stroke in note.strokes: + for index, point in enumerate(stroke[2]): + while len(stroke[2]) > (index + 2) \ + and self.in_line(stroke[2][index], stroke[2][index+1], stroke[2][index+2]): + del stroke[2][index+1] + count += 1 + print "Deleted %i points from a note." % count + return note + + + def set_note(self, index, penNote): + self.changes[index] = self.compress_note(penNote) + + def save(self): + """Abstract. Save all changes.""" + pass + +class BasicFile(BaseSaveRestore): + """Save to a basic file. Load everything into memory on startup.""" + + def __init__(self, file_name="~/.penNotes.strokes_data"): + """ + Initialize and load all the data into memory. + 'file_name': The name of the file where the data is stored + """ + BaseSaveRestore.__init__(self) + + self.file_name = os.path.expanduser(file_name) + self.pen_notes = [] # Not really necessary, as it will be defined in load() anyway, but keeping it here too so that __init__ sort of contains an overview of all data attributes + + self.load() + + def get_note(self, index): + if self.changes.has_key(index): + return self.changes[index] + elif len(self.pen_notes) > index: + return self.pen_notes[index] + else: + return PenNote() + + def load(self): + """Load data from the data file.""" + ## throw away the old stuff :-( + self.pen_notes = [] + + count = 0 + + if os.path.exists(self.file_name): + file_fd = open(self.file_name, 'r') + try: + for line in file_fd.read().split('\n'): + comma_values = line.split(",") + if len(comma_values) >= 2: ## at least 1 coord + if len(line.split(";")[1].rstrip()) <= 0: + continue + if not len(self.pen_notes): + raise Exception("Bad data file: %s" % self.file_name) + fg_color = COLOR_LIST[int(comma_values[0])] + bg_color = COLOR_LIST[int(comma_values[1])] + thickness = int(comma_values[2].split(";")[0]) + + self.pen_notes[-1].bg_color = bg_color + self.pen_notes[-1].strokes.append((fg_color, thickness, [])) + + for coord in line.split("; ")[1].split(" "): + coords = coord.split(",") + if len(coords) <= 1: + continue + + self.pen_notes[-1].strokes[-1][2].append((int(coords[0]), int(coords[1]))) + else: + if len(line) >= 1: + count += 1 + self.pen_notes.append(PenNote()) + finally: + file_fd.close() + else: + print "No notebook file found - using an empty one." + + print "Loaded %i notes from %s." % (count, self.file_name) + + def save(self): + """ + save data to file - format is: + Note 001 + fg, bg, thickness; x,y x,y x,y + fg, bg, thickness; x,y x,y x,y + Note 002 + fg, bg, thickness; x,y x,y x,y + """ + file_fd = open(self.file_name, 'w') + count = 0 + + # Move changes from self.changes to self.pen_notes + for index, note in self.changes.iteritems(): + while len(self.pen_notes) <= index: # The user has created a new note. Create new notes until we reach index + self.pen_notes.append(PenNote()) + self.pen_notes[index] = note + + + for note in self.pen_notes: + bg_color = note.bg_color + count += 1 + file_fd.write("Note %4.4d\n" %count) + + for piece in note.strokes: + fg_color = piece[0] + thickness = piece[1] + line = "%d, %d, %d; " %(COLOR_LIST.index(fg_color), COLOR_LIST.index(bg_color), thickness) + if len(piece[2]) >= 1: + for coord in piece[2]: + line += "%d,%d " % coord + file_fd.write("%s\n" %line) + + file_fd.close() + + print "Saved %i notes to %s." % (count, self.file_name) diff --git a/pyPenNotes/trunk/src/UserDrawingArea.py b/pyPenNotes/trunk/src/UserDrawingArea.py new file mode 100644 index 0000000..8bf60ac --- /dev/null +++ b/pyPenNotes/trunk/src/UserDrawingArea.py @@ -0,0 +1,171 @@ +#!/usr/bin/python +""" + * userDrawingArea.py - pyPenNotes - An area a user can draw on + * + * (C) 2008 by Kristian Mueller + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +import gtk +import gobject +import sys + +class UserDrawingArea(gtk.DrawingArea): + def __init__(self): + gtk.DrawingArea.__init__(self) + + self.fg_color = self.get_colormap().alloc_color("#0000FF") + self.bg_color = self.get_colormap().alloc_color("#000000") + self.line_width = 5 + + self.dragging = False + + # Each stroke is a tuple: (fg color, width, [(x,y)]) + self.strokes = [] + + self.add_events(gtk.gdk.BUTTON_MOTION_MASK | \ + gtk.gdk.BUTTON_PRESS_MASK | \ + gtk.gdk.BUTTON_RELEASE_MASK) + self.connect("realize", self.realize); + self.connect("button-press-event", self.mouse_press) + self.connect("button-release-event", self.mouse_up) + self.connect("motion-notify-event", self.mouse_move) + self.connect("expose-event", lambda widget,event: self.redraw()) + + def realize(self, event): + """Called when the widget is first displayed.""" + self.gc = self.get_style().fg_gc[gtk.STATE_NORMAL] + self.gc.set_line_attributes(self.line_width, \ + gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, \ + gtk.gdk.CAP_ROUND) + self.redraw() + + def set_bg_color(self, bg_color): + """ + Set the bg_color of the area. + + 'bg_color': Anything that can be the first argument to gtk.gdk.Colormap.alloc_color + """ + self.bg_color = self.get_colormap().alloc_color(bg_color) + self.redraw() + def get_bg_color(self): + """Get the bg_color of the area. The format is lowercase hex with four digits per color.""" + return self.bg_color.to_string() + + def set_fg_color(self, bg_color): + """ + Set the fg_color of the area. + + 'bg_color': Anything that can be the first argument to gtk.gdk.Colormap.alloc_color + """ + self.fg_color = self.get_colormap().alloc_color(bg_color) + self.redraw() + def get_fg_color(self): + """Get the fg_color of the area. The format is lowercase hex with four digits per color.""" + return self.fg_color.to_string() + + def clear(self): + """Clear all strokes from the area.""" + del self.strokes[:] + self.current_stroke_points = None + self.redraw() + + def get_strokes(self): + """Returns a list of the strokes. The stroke is a tuple of (fg_color, stroke_width, [(x,y)]), where fg_color is lowercase hex with 4 digits per color.""" + return [(fg_color.to_string(), stroke_width, points) \ + for (fg_color, stroke_width, points) in self.strokes] # Convert the fg_color to a hex string + + def set_strokes(self, strokes): + """ + Set strokes and draw them on the screen. + 'strokes': A tuple of (fg_color, stroke_width, [(x,y)]), where fg_color is lowercase hex with 4 digits per color. + """ + self.strokes = [(self.get_colormap().alloc_color(fg_color), stroke_width, points) \ + for (fg_color, stroke_width, points) in strokes] # Allocate fg_color + self.redraw() + + def new_stroke(self, x, y): + """Create a new stroke starting at (x, y)""" + self.strokes.append((self.fg_color, self.line_width, [(x,y)])) + self.current_stroke_points = self.strokes[-1][2] + self.update_gc() + self.add_point_to_stroke(x, y) + + def add_point_to_stroke(self, x, y): + """Add a point to the last stroke.""" + self.current_stroke_points.append((x,y)) + self.window.draw_lines(self.gc, self.current_stroke_points[-2:]) + + def clear_draw_screen(self, event): + """Overdraw the screen using background color.""" + fg_backup = self.gc.foreground + self.gc.foreground = self.gc.background + self.window.draw_rectangle(self.gc, True, 0, 0, -1, -1) + self.gc.foreground = fg_backup + + def update_gc(self): + """Private. Update self.gc from self.fg_color, self.bg_color and self.line_width.""" + self.gc.foreground = self.fg_color + self.gc.background = self.bg_color + self.gc.line_width= self.line_width + + def redraw(self): + """Redraw screen using the notes content.""" + ## step 1 - update the gc in case we have changed the background or something like that + self.update_gc() + + ## step 2 - clear screen <-- will cause a flicker! + self.clear_draw_screen(None) + + ## step 3 - redraw all the lines + fg_backup = self.gc.foreground + for stroke in self.strokes: + self.gc.foreground = stroke[0] + self.gc.line_width = stroke[1] + self.window.draw_lines(self.gc, stroke[2]) + self.gc.line_width = self.line_width + self.gc.foreground = fg_backup + def undo(self): + if len(self.strokes) >= 1: + self.strokes.pop() + if len(self.strokes): + self.current_stroke_points = self.strokes[-1][2] + self.redraw() + + def mouse_move(self, widget, event): + if(self.dragging): + self.add_point_to_stroke(int(event.x), int(event.y)) + + def mouse_press(self, widget, event): + self.dragging = True + self.new_stroke(int(event.x), int(event.y)) + + def mouse_up(self, widget, event): + self.dragging = False + +if __name__ == "__main__": # Show a sample UserDrawingArea + area = UserDrawingArea() + + window = gtk.Window(gtk.WINDOW_TOPLEVEL) + window.set_default_size(480, 640) + window.set_title("UserDrawingArea") + window.connect("destroy", lambda w: gtk.main_quit()) + window.add(area) + window.show_all() + gtk.main() + + print "Strokes:" + print area.get_strokes()