diff --git a/ChangeLog b/ChangeLog index 72a6a37..aaa6427 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,8 +1,17 @@ Version 0.3 -- not yet released * Undo/Redo * Accelerated stroke deletion + * Search + * Rect tool + * Line tool + * Circle tool + * Fill color + * Tool bar with icons + * Keyboard shortcuts + * Tool item preview * Changed servers file format from Pickle to JSON +>>>>>>> upstream/master Version 0.2.1 -- 2012-07-31 * Support for system-wide installation via setup.py * Fix a bug, which caused cournal to consume too much CPU if the connection diff --git a/TODO b/TODO index 27e78d0..af1e9d2 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,6 @@ This is a TODO List with (hopefully) future Cournal Features! (The order is no representation of a ranking of any kind) -* Keymapping (Ctrl+s for saving, Ctrl+o for opening a file, etc) * Remember the last Server connected to * Commandlineinterface (CLI) * Chat with both keyboard and handwriting compatibility @@ -15,4 +14,8 @@ This is a TODO List with (hopefully) future Cournal Features! * Support to open more than one PDF Document * Android Port * Show position of other users in scrollbar - +* Error handling +* Change Pickle to JSON +* Make fill color chooser more discreet +* Set sensitivity for not incompatible actions +* Print \ No newline at end of file diff --git a/cournal/document/circle.py b/cournal/document/circle.py new file mode 100644 index 0000000..f48d7b9 --- /dev/null +++ b/cournal/document/circle.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# This file is part of Cournal. +# Copyright (C) 2012 Simon Vetter +# +# Cournal 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 3 of the License, or +# (at your option) any later version. +# +# Cournal 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 Cournal. If not, see . + +import cairo +import math + +from twisted.spread import pb + +class Circle(pb.Copyable, pb.RemoteCopy): + def __init__(self, layer, color, fill, fill_color, linewidth, coords, scale): + """ + Constructor + + Positional arguments: + layer -- The Layer object, which is the parent of the stroke. + color -- tuple of four: (red, green, blue, opacity) + linewidth -- Line width in pt + + Keyword arguments: + coords -- A list of coordinates (defaults to []) + """ + self.layer = layer + self.color = color + self.fill_color = fill_color + self.fill = fill + self.linewidth = linewidth + self.coords = coords + self.scale = scale + + def in_bounds(self, x, y, radius): + """ + Test if point is in bounding box of the stroke. + + Positional arguments: + x, y -- point + + Returns: + true, if point is in bounding box + """ + distance = ((self.coords[0] - x)/self.scale[0])**2 + ((self.coords[1] - y)/self.scale[1])**2 + if distance < 1+radius/min(self.scale[0],self.scale[1]): + if not self.fill: + if distance < 1-radius/min(self.scale[0],self.scale[1]): + return False + return True + else: + return False + + def calculate_bounding_box(self, radius=5): + pass + + def getStateToCopy(self): + """Gather state to send when I am serialized for a peer.""" + + # d would be self.__dict__.copy() + d = dict() + d["color"] = self.color + d["fill_color"] = self.fill_color + d["fill"] = self.fill + d["coords"] = self.coords + d["scale"] = self.scale + d["linewidth"] = self.linewidth + return d + + def draw(self, context, scaling=1): + """ + Render this stroke + + Positional arguments: + context -- The cairo context to draw on + + Keyword arguments: + scaling -- scale the stroke by this factor (defaults to 1.0) + """ + if self.scale[0] * self.scale[1] * self.scale[2] == 0: + return + + context.save() + r, g, b, opacity = self.fill_color + + context.set_source_rgba(r/255, g/255, b/255, opacity/255) + context.set_antialias(cairo.ANTIALIAS_GRAY) + context.set_line_join(cairo.LINE_JOIN_ROUND) + context.set_line_cap(cairo.LINE_CAP_ROUND) + context.set_line_width(self.linewidth) + + if self.fill: + r, g, b, opacity = self.fill_color + context.set_source_rgba(r/255, g/255, b/255, opacity/255) + # Draw Circle and scale it to oval + context.translate(self.coords[0], self.coords[1]) + context.scale(self.scale[0]/self.scale[2], self.scale[1]/self.scale[2]) + context.arc(0., 0., self.scale[2], 0., 2 * math.pi) + context.scale(1 / (self.scale[0]/self.scale[2]), 1 / (self.scale[1]/self.scale[2])) + context.translate(-self.coords[0], -self.coords[1]) + context.fill() + + # Draw border + i = 0 + context.move_to(self.coords[0], self.coords[1] + self.scale[1]) + # We draw the border with strokes to keep konstant border width + while i < (math.pi * 2): + i += 0.1 + context.line_to( + self.coords[0] + math.sin(i)*self.scale[0], + self.coords[1] + math.cos(i)*self.scale[1]) + + r, g, b, opacity = self.color + context.set_source_rgba(r/255, g/255, b/255, opacity/255) + + x, y, x2, y2 = (a*scaling for a in context.stroke_extents()) + context.stroke() + context.restore() + + return (x, y, x2, y2) + +# Tell Twisted, that this class is allowed to be transmitted over the network. +pb.setUnjellyableForClass(Circle, Circle) diff --git a/cournal/document/document.py b/cournal/document/document.py index 7663a8c..51c3684 100644 --- a/cournal/document/document.py +++ b/cournal/document/document.py @@ -26,6 +26,7 @@ from cournal.document.page import Page from cournal.document import history from cournal.document import search +from cournal.document.stroke import Stroke class Document: """ @@ -60,19 +61,19 @@ def __init__(self, pdfname): def is_empty(self): """ - Returns True, if no page of this document has a stroke on it. + Returns True, if no page of this document has a item on it. Otherwise False """ for page in self.pages: - if len(page.layers[0].strokes) != 0: + if len(page.layers[0].items) != 0: return False return True def clear_pages(self): - """Deletes all strokes on all pages of this document""" + """Deletes all item on all pages of this document""" for page in self.pages: - for stroke in page.layers[0].strokes[:]: - page.delete_stroke(stroke, send_to_network=False) + for item in page.layers[0].items[:]: + page.delete_item(item, send_to_network=False) def export_pdf(self, filename): """ @@ -94,8 +95,8 @@ def export_pdf(self, filename): page.pdf.render_for_printing(context) - for stroke in page.layers[0].strokes: - stroke.draw(context) + for item in page.layers[0].items: + item.draw(context) surface.show_page() # aka "next page" @@ -131,15 +132,17 @@ def save_xoj_file(self, filename): for layer in page.layers: r += "\n" - for stroke in layer.strokes: - red, g, b, opacity = stroke.color - r += "\n".format(red, g, b, opacity, stroke.linewidth) - first = stroke.coords[0] - for coord in stroke.coords: - r += " {} {}".format(coord[0], coord[1]) - if len(stroke.coords) < 2: - r += " {} {}".format(first[0], first[1]) - r += "\n\n" + for item in layer.items: + #TODO: Parse other Items + if isinstance(item, Stroke): + red, g, b, opacity = item.color + r += "\n".format(red, g, b, opacity, item.linewidth) + first = item.coords[0] + for coord in item.coords: + r += " {} {}".format(coord[0], coord[1]) + if len(item.coords) < 2: + r += " {} {}".format(first[0], first[1]) + r += "\n\n" r += "\n" r += "\n" r += "" diff --git a/cournal/document/history.py b/cournal/document/history.py index 8a2f818..d4a8731 100644 --- a/cournal/document/history.py +++ b/cournal/document/history.py @@ -57,25 +57,25 @@ def redo(action): if len(_redo_list) == 0: _redo_action.set_sensitive(False) -def register_draw_stroke(stroke, page): +def register_draw_item(item, page): """ - Register draw stroke command in history. + Register draw item command in history. Positional arguments: - stroke -- drawn stroke - page -- page stroke was drawn on + item -- drawn item + page -- page item was drawn on """ - add_undo_command(CommandDrawStroke(stroke, page)) + add_undo_command(CommandDrawItem(item, page)) -def register_delete_stroke(stroke, page): +def register_delete_item(item, page): """ - Register delete stroke command in history. + Register delete item command in history. Positional arguments: - stroke -- deleted stroke - page -- page stroke was deleted from + item -- deleted item + page -- page item was deleted from """ - add_undo_command(CommandDeleteStroke(stroke, page)) + add_undo_command(CommandDeleteItem(item, page)) def add_undo_command(command, clear_redo=True): """ @@ -108,26 +108,26 @@ def add_redo_command(command): if len(_redo_list) == 1: _redo_action.set_sensitive(True) -class CommandDrawStroke: - """Draw stroke command.""" - def __init__(self, stroke, page): - self.stroke = stroke +class CommandDrawItem: + """Draw item command.""" + def __init__(self, item, page): + self.item = item self.page = page def undo(self): - self.page.delete_stroke(self.stroke, send_to_network=True, register_in_history=False) + self.page.delete_item(self.item, send_to_network=True, register_in_history=False) def redo(self): - self.page.new_stroke(self.stroke, send_to_network=True) + self.page.new_item(self.item, send_to_network=True) -class CommandDeleteStroke: - """Delete stroke command.""" - def __init__(self, stroke, page): - self.stroke = stroke +class CommandDeleteItem: + """Delete item command.""" + def __init__(self, item, page): + self.item = item self.page = page def undo(self): - self.page.new_stroke(self.stroke, send_to_network=True) + self.page.new_item(self.item, send_to_network=True) def redo(self): - self.page.delete_stroke(self.stroke, send_to_network=True, register_in_history=False) + self.page.delete_item(self.item, send_to_network=True, register_in_history=False) diff --git a/cournal/document/layer.py b/cournal/document/layer.py index 890067f..8f4d89f 100644 --- a/cournal/document/layer.py +++ b/cournal/document/layer.py @@ -19,9 +19,9 @@ class Layer: """ - A layer on a page, having a number and multiple strokes. + A layer on a page, having a number and multiple item. """ - def __init__(self, page, number, strokes=None): + def __init__(self, page, number, items=None): """ Constructor @@ -30,11 +30,11 @@ def __init__(self, page, number, strokes=None): number -- Layer number Keyword arguments: - strokes -- List of Stroke objects (defaults to []) + item -- List of items (defaults to []) """ self.number = number self.page = page - self.strokes = strokes + self.items = items - if self.strokes is None: - self.strokes = [] + if self.items is None: + self.items = [] diff --git a/cournal/document/page.py b/cournal/document/page.py index 7b26c62..92c24ca 100644 --- a/cournal/document/page.py +++ b/cournal/document/page.py @@ -20,6 +20,8 @@ from cournal.document.layer import Layer from cournal.document.stroke import Stroke +from cournal.document.rect import Rect +from cournal.document.circle import Circle from cournal.network import network from cournal.document import history @@ -50,26 +52,26 @@ def __init__(self, document, pdf, number, layers=None): self.width, self.height = pdf.get_size() self.search_marker = None - def new_stroke(self, stroke, send_to_network=False): + def new_item(self, item, send_to_network=False): """ - Add a new stroke to this page and possibly send it to the server, if + Add a new item to this page and possibly send it to the server, if connected. Positional arguments: - stroke -- The Stroke object, that will be added to this page + item -- The item, that will be added to this page Keyword arguments: - send_to_network -- Set True, to send the stroke to the server + send_to_network -- Set True, to send the item to the server (defaults to False) """ - self.layers[0].strokes.append(stroke) - stroke.calculate_bounding_box() - stroke.layer = self.layers[0] + self.layers[0].items.append(item) + item.calculate_bounding_box() + item.layer = self.layers[0] if self.widget: - self.widget.draw_remote_stroke(stroke) + self.widget.draw_remote_item(item) if send_to_network: - network.new_stroke(self.number, stroke) - + network.new_item(self.number, item) + def new_unfinished_stroke(self, color, linewidth): """ Add a new empty stroke, which is not sent to the server, till @@ -89,58 +91,53 @@ def finish_stroke(self, stroke): Positional arguments: stroke -- The Stroke object, that was finished """ - history.register_draw_stroke(stroke, self) + history.register_draw_item(stroke, self) stroke.calculate_bounding_box() - network.new_stroke(self.number, stroke) + network.new_item(self.number, stroke) - def delete_stroke_with_coords(self, coords): + def delete_item_with_coords(self, coords): """ - Delete all strokes, which have exactly the same coordinates as given. + Delete all items, which have exactly the same coordinates as given. Positional arguments coords -- The list of coordinates """ - for stroke in self.layers[0].strokes[:]: - if stroke.coords == coords: - self.delete_stroke(stroke, send_to_network=False) + for item in self.layers[0].items[:]: + if item.coords == coords: + self.delete_item(item, send_to_network=False) - def delete_stroke(self, stroke, send_to_network=False, register_in_history=True): + def delete_item(self, item, send_to_network=False, register_in_history=True): """ - Delete a stroke on this page and possibly send this request to the server, + Delete a item on this page and possibly send this request to the server, if connected. Positional arguments: - stroke -- The Stroke object, that will be deleted. + item -- The item, that will be deleted. Keyword arguments: send_to_network -- Set to True, to send the request for deletion the server (defaults to False) register_in_history -- Make this command undoable """ - self.layers[0].strokes.remove(stroke) + self.layers[0].items.remove(item) if self.widget: - self.widget.delete_remote_stroke(stroke) + self.widget.delete_remote_item(item) if send_to_network: - network.delete_stroke_with_coords(self.number, stroke.coords) + network.delete_item_with_coords(self.number, item.coords) if register_in_history: - history.register_delete_stroke(stroke, self) + history.register_delete_item(item, self) - def get_strokes_near(self, x, y, radius): + def get_items_near(self, x, y, radius): """ - Finds strokes near a given point + Finds items near a given point Positional arguments: x -- x coordinate of the given point y -- y coordinate of the given point radius -- Radius in pt, which influences the decision of what is considered "near" - Return value: Generator for a list of all strokes, which are near that point + Return value: Generator for a list of all objects, which are near that point """ - for stroke in self.layers[0].strokes[:]: - if stroke.in_bounds(x, y): - for coord in stroke.coords: - s_x = coord[0] - s_y = coord[1] - if ((s_x-x)**2 + (s_y-y)**2) < radius**2: - yield stroke - break + for item in self.layers[0].items[:]: + if item.in_bounds(x, y, radius): + yield item diff --git a/cournal/document/rect.py b/cournal/document/rect.py new file mode 100644 index 0000000..3d68078 --- /dev/null +++ b/cournal/document/rect.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# This file is part of Cournal. +# Copyright (C) 2012 Simon Vetter +# +# Cournal 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 3 of the License, or +# (at your option) any later version. +# +# Cournal 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 Cournal. If not, see . + +import cairo + +from twisted.spread import pb + +class Rect(pb.Copyable, pb.RemoteCopy): + def __init__(self, layer, color, fill, fill_color, linewidth, coords=None): + """ + Constructor + + Positional arguments: + layer -- The Layer object, which is the parent of the stroke. + color -- tuple of four: (red, green, blue, opacity) + linewidth -- Line width in pt + + Keyword arguments: + coords -- A list of coordinates (defaults to []) + """ + self.layer = layer + self.color = color + self.fill_color = fill_color + self.fill = fill + self.linewidth = linewidth + self.coords = coords + if self.coords is None: + self.coords = [] + + def in_bounds(self, x, y, radius): + """ + Test if point is in bounding box of the stroke. + + Positional arguments: + x, y -- point + + Returns: + true, if point is in bounding box + """ + if self.coords[0] < self.coords[2]: + s_x = self.coords[0] + e_x = self.coords[2] + else: + s_x = self.coords[2] + e_x = self.coords[0] + if self.coords[1] < self.coords[3]: + s_y = self.coords[1] + e_y = self.coords[3] + else: + s_y = self.coords[3] + e_y = self.coords[1] + + if x > s_x-radius and x < e_x+radius and y > s_y-radius and y < e_y+radius: + if not self.fill: + if x > s_x+radius and x < e_x-radius and y > s_y+radius and y < e_y-radius: + return False + return True + else: + return False + + def calculate_bounding_box(self, radius=5): + pass + + def getStateToCopy(self): + """Gather state to send when I am serialized for a peer.""" + + # d would be self.__dict__.copy() + d = dict() + d["color"] = self.color + d["fill_color"] = self.fill_color + d["fill"] = self.fill + d["coords"] = self.coords + d["linewidth"] = self.linewidth + return d + + def draw(self, context, scaling=1): + """ + Render this stroke + + Positional arguments: + context -- The cairo context to draw on + + Keyword arguments: + scaling -- scale the stroke by this factor (defaults to 1.0) + """ + context.save() + r, g, b, opacity = self.fill_color + + context.set_source_rgba(r/255, g/255, b/255, opacity/255) + context.set_antialias(cairo.ANTIALIAS_GRAY) + context.set_line_join(cairo.LINE_JOIN_ROUND) + context.set_line_cap(cairo.LINE_CAP_ROUND) + context.set_line_width(self.linewidth) + + if self.fill: + # Fill rect + context.move_to(self.coords[0], self.coords[1]) + context.line_to(self.coords[0], self.coords[3]) + context.line_to(self.coords[2], self.coords[3]) + context.line_to(self.coords[2], self.coords[1]) + context.close_path() + context.fill() + + # Draw border + context.move_to(self.coords[0], self.coords[1]) + context.line_to(self.coords[0], self.coords[3]) + context.line_to(self.coords[2], self.coords[3]) + context.line_to(self.coords[2], self.coords[1]) + context.close_path() + + r, g, b, opacity = self.color + context.set_source_rgba(r/255, g/255, b/255, opacity/255) + + x, y, x2, y2 = (a*scaling for a in context.stroke_extents()) + context.stroke() + context.restore() + + return (x, y, x2, y2) + +# Tell Twisted, that this class is allowed to be transmitted over the network. +pb.setUnjellyableForClass(Rect, Rect) diff --git a/cournal/document/stroke.py b/cournal/document/stroke.py index 2c9cba0..29d42f1 100644 --- a/cournal/document/stroke.py +++ b/cournal/document/stroke.py @@ -47,8 +47,9 @@ def __init__(self, color, linewidth, layer=None, coords=None): self.coords = coords if self.coords is None: self.coords = [] - - def in_bounds(self, x, y): + self.bound_min = None + + def in_bounds(self, x, y, radius): """ Test if point is in bounding box of the stroke. @@ -58,17 +59,24 @@ def in_bounds(self, x, y): Returns: true, if point is in bounding box """ - try: - if x > self.bound_min[0] and x < self.bound_max[0] and y > self.bound_min[1] and y < self.bound_max[1]: - return True - else: - return False - except: - self.calculate_bounding_box() - if x > self.bound_min[0] and x < self.bound_max[0] and y > self.bound_min[1] and y < self.bound_max[1]: - return True - else: - return False + if not self.bound_min: + self.calculate_bounding_box(radius) + + if x > self.bound_min[0] and x < self.bound_max[0] and y > self.bound_min[1] and y < self.bound_max[1]: + for coord in self.coords: + s_x = coord[0] + s_y = coord[1] + if ((s_x-x)**2 + (s_y-y)**2) < radius**2: + return True + if len(self.coords) == 2: # line + dx1 = (x - self.coords[0][0]) + dy1 = (y - self.coords[0][1]) + dx2 = (self.coords[1][0] - self.coords[0][0]) + dy2 = (self.coords[1][1] - self.coords[0][1]) + if (dy1 != 0 and dy2 != 0): + return (abs(abs(dx1/dy1) - abs(dx2/dy2)) < 0.02) + else: + return False def calculate_bounding_box(self, radius=5): """ diff --git a/cournal/document/xojparser.py b/cournal/document/xojparser.py index 956a578..a6de72a 100644 --- a/cournal/document/xojparser.py +++ b/cournal/document/xojparser.py @@ -73,7 +73,7 @@ def import_into_document(document, filename, window): strokes = pages[p].findall("layer/stroke") for s in range(len(strokes)): stroke = _parse_stroke(strokes[s], document.pages[p].layers[0]) - document.pages[p].new_stroke(stroke, send_to_network=True) + document.pages[p].new_item(stroke, send_to_network=True) return document def _parse_stroke(stroke, layer): diff --git a/cournal/mainwindow.glade b/cournal/mainwindow.glade index 9159102..e694efd 100644 --- a/cournal/mainwindow.glade +++ b/cournal/mainwindow.glade @@ -3,7 +3,6 @@ - accelgroup gtk-open @@ -28,7 +27,6 @@ - accelgroup gtk-save @@ -92,12 +90,14 @@ 1.5 + 0.7 action_pensize_normal + @@ -105,6 +105,7 @@ 3 action_pensize_normal + @@ -116,6 +117,46 @@ + + + Pen + True + + + + + + Rectangle + True + action_tool_pen + + + + + + Line + True + action_tool_pen + + + + + + Circle + True + action_tool_pen + + + + + + Fill + + + + + + True @@ -139,6 +180,7 @@ False + False False _File True @@ -235,6 +277,7 @@ + False True False _Edit @@ -276,6 +319,7 @@ + False True False _Help @@ -315,6 +359,7 @@ True False Annotate PDF + action_open_pdf False @@ -328,6 +373,7 @@ False True Connect to server + action_connect False @@ -340,6 +386,7 @@ True False Save current document + action_save True @@ -362,6 +409,7 @@ action_zoom_in True False + action_zoom_in True @@ -374,6 +422,7 @@ action_zoom_out True False + action_zoom_out True @@ -386,6 +435,7 @@ action_zoom_fit True False + action_zoom_fit True @@ -408,6 +458,7 @@ action_undo True False + action_undo True @@ -420,7 +471,91 @@ action_redo True False + action_redo + True + + + False + True + + + + + False + True + 1 + + + + + True + False + icons + + + False + action_tool_pen + True + False + action_tool_pen + False + Pen + True + cournal-pen-tool + + + False + True + + + + + False + action_tool_rect + True + False + action_tool_rect + False + Rect + True + cournal-rect-tool + tool_pen + + + False + True + + + + + False + action_tool_line + True + False + action_tool_line + False + Line + True + cournal-line-tool + tool_pen + + + False + True + + + + + False + action_tool_circle + True + False + action_tool_circle + False + Circle True + cournal-circle-tool + tool_pen False @@ -438,16 +573,20 @@ - + False action_pen_color True False + action_pen_color + False + False True True True + False none True Pen Color @@ -459,12 +598,72 @@ False + + + False + action_pen_fillcolor + True + False + action_pen_fillcolor + False + + + False + True + True + True + False + none + True + Fill Color + rgb(245,121,0) + + + + + False + + + + + False + action_fill_tool + True + False + action_fill_tool + False + Fill + True + cournal-fill-tool + True + + + False + True + + + + + False + True + False + False + + + False + True + + + False action_pensize_small True False + action_pensize_small + False 0.7 + cournal-pen-small tool_pensize_normal @@ -474,10 +673,14 @@ + False action_pensize_normal True False + action_pensize_normal + False 1.5 + cournal-pen-medium False @@ -486,10 +689,14 @@ + False action_pensize_big True False + action_pensize_big + False 8.0 + cournal-pen-big tool_pensize_normal @@ -501,7 +708,7 @@ False True - 1 + 2 @@ -522,7 +729,64 @@ True True - 2 + 3 + + + + + False + True + 5 + + + True + True + + gtk-find + + + True + True + 0 + + + + + gtk-find + False + True + True + True + False + True + + + False + True + 1 + + + + + gtk-close + False + True + True + True + False + True + + + False + True + 2 + + + + + False + True + 4 @@ -549,10 +813,12 @@ 1 + False True False False True + False image_btn_prev none False @@ -596,10 +862,12 @@ + False True False False True + False image_btn_next none @@ -637,61 +905,7 @@ False False end - 3 - - - - - False - True - 5 - - - True - True - - gtk-find - - - True - True - 0 - - - - - gtk-find - True - True - True - True - - - False - True - 1 - - - - - gtk-close - True - True - True - True - - - False - True - 2 - - - - - False - True - end - 4 + 5 diff --git a/cournal/mainwindow.py b/cournal/mainwindow.py index 3a6d282..c383bc3 100644 --- a/cournal/mainwindow.py +++ b/cournal/mainwindow.py @@ -24,7 +24,7 @@ import cournal from cournal.viewer.layout import Layout -from cournal.viewer.tools import pen +from cournal.viewer.tools import pen, rect, primary, line, circle from cournal.document.document import Document from cournal.document import xojparser from cournal.network import network @@ -97,9 +97,17 @@ def __init__(self, **args): action_pensize_normal = builder.get_object("action_pensize_normal") action_pensize_big = builder.get_object("action_pensize_big") tool_pen_color = builder.get_object("tool_pen_color") + tool_pen_fillcolor = builder.get_object("tool_pen_fillcolor") + action_tool_pen = builder.get_object("action_tool_pen") + action_tool_rect = builder.get_object("action_tool_rect") + action_tool_line = builder.get_object("action_tool_line") + action_tool_circle = builder.get_object("action_tool_circle") + action_fill_tool = builder.get_object("action_fill_tool") + self.actiongroup_document_specific = builder.get_object("actiongroup_document_specific") self.actiongroup_document_specific.set_sensitive(False) builder.get_object("tool_pensize_normal").set_active(True) + builder.get_object("tool_pen").set_active(True) # Workaround for bug https://bugzilla.gnome.org/show_bug.cgi?id=671786 if not Gtk.check_version(3,6,0) == None: @@ -123,6 +131,11 @@ def __init__(self, **args): a.connect_by_path(action_pensize_small.get_accel_path(), lambda a,b,c,d: action_pensize_small.activate()) a.connect_by_path(action_pensize_normal.get_accel_path(), lambda a,b,c,d: action_pensize_normal.activate()) a.connect_by_path(action_pensize_big.get_accel_path(), lambda a,b,c,d: action_pensize_big.activate()) + a.connect_by_path(action_tool_pen.get_accel_path(), lambda a,b,c,d: action_tool_pen.activate()) + a.connect_by_path(action_tool_rect.get_accel_path(), lambda a,b,c,d: action_tool_rect.activate()) + a.connect_by_path(action_tool_line.get_accel_path(), lambda a,b,c,d: action_tool_line.activate()) + a.connect_by_path(action_tool_circle.get_accel_path(), lambda a,b,c,d: action_tool_circle.activate()) + a.connect_by_path(action_fill_tool.get_accel_path(), lambda a,b,c,d: action_fill_tool.activate()) action_open_xoj.connect("activate", self.run_open_xoj_dialog) action_open_pdf.connect("activate", self.run_open_pdf_dialog) @@ -139,10 +152,16 @@ def __init__(self, **args): action_zoom_in.connect("activate", self.zoom_in) action_zoom_out.connect("activate", self.zoom_out) action_zoom_fit.connect("activate", self.zoom_fit) - tool_pen_color.connect("color-set", self.change_pen_color) - action_pensize_small.connect("activate", self.change_pen_size, LINEWIDTH_SMALL) - action_pensize_normal.connect("activate", self.change_pen_size, LINEWIDTH_NORMAL) - action_pensize_big.connect("activate", self.change_pen_size, LINEWIDTH_BIG) + tool_pen_color.connect("color-set", self.change_primary_color) + action_pensize_small.connect("activate", self.change_primary_size, LINEWIDTH_SMALL) + action_pensize_normal.connect("activate", self.change_primary_size, LINEWIDTH_NORMAL) + action_pensize_big.connect("activate", self.change_primary_size, LINEWIDTH_BIG) + tool_pen_fillcolor.connect("color-set", self.change_primary_fillcolor) + action_tool_pen.connect("activate", self.set_tool) + action_tool_rect.connect("activate", self.set_tool) + action_tool_line.connect("activate", self.set_tool) + action_tool_circle.connect("activate", self.set_tool) + action_fill_tool.connect("activate", self.set_fill) # Statusbar: self.statusbar_icon = builder.get_object("image_statusbar") @@ -157,6 +176,9 @@ def __init__(self, **args): self.button_prev_page.connect("clicked", self.jump_to_prev_page) self.button_next_page.connect("clicked", self.jump_to_next_page) + self.set_tool(pen) + #action_tool_pen.activate() + history.init(action_undo, action_redo) # Search bar: @@ -171,6 +193,47 @@ def __init__(self, **args): self.search_button.connect("clicked", self.search_document) self.search_field.connect("activate", self.search_document) + def set_tool(self, tool): + """ + Set the tool to be used + + Positional arguments: + tool -- clicked tool button + """ + #TODO: Check what happend to the action + #TODO: Maybe change sensitivity + + if tool not in [pen, rect, line, circle]: + if tool.get_name() == "action_tool_pen": + tool = pen + #action_fill_tool.set_sensitive(False) + #action_tool_pen_bg_color.set_sensitive(False) + elif tool.get_name() == "action_tool_rect": + tool = rect + #action_fill_tool.set_sensitive(True) + #action_tool_pen_bg_color.set_sensitive(True) + elif tool.get_name() == "action_tool_line": + tool = line + #action_tool_fill.set_sensitive(False) + #action_tool_pen_bg_color.set_sensitive(False) + elif tool.get_name() == "action_tool_circle": + tool = circle + #action_tool_fill.set_sensitive(True) + #action_tool_pen_bg_color.set_sensitive(True) + primary.current_tool = tool + + def set_fill(self, tool): + """ + Set the shape filling method + + Positional arguments: + tool -- fill button + """ + if tool.get_active(): + primary.fill = True + else: + primary.fill = False + def connect_event(self): """ Called by the networking layer when a connection is established. @@ -281,6 +344,7 @@ def _set_document(self, document): self.last_filename = None self.actiongroup_document_specific.set_sensitive(True) + if self.document.num_of_pages > 1: self.button_next_page.set_sensitive(True) @@ -527,9 +591,24 @@ def run_error_dialog(self, first, second): message.connect("response", lambda _,x: message.destroy()) message.show() - def change_pen_color(self, colorbutton): + def change_primary_color(self, colorbutton): + """ + Change the primary tool to a user defined color. + + Positional arguments: + colorbutton -- The Gtk.ColorButton, that triggered this function + """ + color = colorbutton.get_rgba() + red = int(color.red*255) + green = int(color.green*255) + blue = int(color.blue*255) + opacity = int(color.alpha*255) + + primary.color = red, green, blue, opacity + + def change_primary_fillcolor(self, colorbutton): """ - Change the pen to a user defined color. + Change the primary tool to a user defined color. Positional arguments: colorbutton -- The Gtk.ColorButton, that triggered this function @@ -540,17 +619,17 @@ def change_pen_color(self, colorbutton): blue = int(color.blue*255) opacity = int(color.alpha*255) - pen.color = red, green, blue, opacity + primary.fillcolor = red, green, blue, opacity - def change_pen_size(self, menuitem, linewidth): + def change_primary_size(self, menuitem, linewidth): """ - Change the pen to a user defined line width. + Change the primary tool to a user defined line width. Positional arguments: menuitem -- The menu item, that triggered this function linewidth -- New line width of the pen """ - pen.linewidth = linewidth + primary.linewidth = linewidth def zoom_in(self, menuitem): """Magnify document""" diff --git a/cournal/network.py b/cournal/network.py index fc30a0c..3f43bfd 100644 --- a/cournal/network.py +++ b/cournal/network.py @@ -48,7 +48,7 @@ def __init__(self): pb.Referenceable.__init__(self) self.document = None self.window = None - self.is_connected = False + self.is_connected = 0 self.is_stalled = True self.last_data_received = 0 self.watchdog = None @@ -102,7 +102,7 @@ def connected(self, perspective): # here, otherwise it will get garbage collected at the end of this # function and the server will think we logged out. self.is_stalled = False - self.is_connected = True + self.is_connected = 1 self.perspective = perspective self.perspective.notifyOnDisconnect(self.disconnect_event) self.data_received() @@ -117,7 +117,7 @@ def connection_failed(self, reason): reason -- A twisted Failure object with the reason the connection failed """ debug(0, _("Connection failed due to: {}").format(reason.getErrorMessage())) - self.is_connected = False + self.is_connected = 0 return reason @@ -130,7 +130,7 @@ def disconnect(self, _=None): def disconnect_event(self, event): """Called, when the client gets disconnected from the server.""" - self.is_connected = False + self.is_connected = 0 self.connection_problems() if self.window: self.window.disconnect_event() @@ -180,54 +180,55 @@ def got_server_document(self, server_document, name): """ self.data_received() debug(2, _("Started editing {}").format(name)) + self.is_connected = 2 self.server_document = server_document - def remote_new_stroke(self, pagenum, stroke): + def remote_new_item(self, pagenum, item): """ - Called by the server, to inform us about a new stroke + Called by the server, to inform us about a new item Positional arguments: - pagenum -- On which page shall we add the stroke - stroke -- The received Stroke object + pagenum -- On which page shall we add the item + item -- The received item """ self.data_received() if self.document and pagenum < len(self.document.pages): - self.document.pages[pagenum].new_stroke(stroke) + self.document.pages[pagenum].new_item(item) - def new_stroke(self, pagenum, stroke): + def new_item(self, pagenum, item): """ - Called by local code to send a new stroke to the server + Called by local code to send a new item to the server Positional arguments: - pagenum -- On which page the stroke was added - stroke -- The Stroke object to send + pagenum -- On which page the item was added + item -- The Stroke item to send """ - if self.is_connected: - d = self.server_document.callRemote("new_stroke", pagenum, stroke) + if self.is_connected > 1: + d = self.server_document.callRemote("new_item", pagenum, item) d.addCallbacks(lambda x: self.data_received(), self.disconnect) - def remote_delete_stroke_with_coords(self, pagenum, coords): + def remote_delete_item_with_coords(self, pagenum, coords): """ - Called by the server, when a remote user deleted a stroke + Called by the server, when a remote user deleted a item Positional arguments: - pagenum -- On which page the stroke was deleted - coords -- The list of coordinates identifying a stroke + pagenum -- On which page the item was deleted + coords -- The list of coordinates identifying a item """ self.data_received() if self.document and pagenum < len(self.document.pages): - self.document.pages[pagenum].delete_stroke_with_coords(coords) + self.document.pages[pagenum].delete_item_with_coords(coords) - def delete_stroke_with_coords(self, pagenum, coords): + def delete_item_with_coords(self, pagenum, coords): """ Called by local code to send a delete command to the server Positional arguments: - pagenum -- On which page the stroke was deleted - coords -- The list of coordinates identifying the stroke + pagenum -- On which page the item was deleted + coords -- The list of coordinates identifying the item """ - if self.is_connected: - d = self.server_document.callRemote("delete_stroke_with_coords", pagenum, coords) + if self.is_connected > 1: + d = self.server_document.callRemote("delete_item_with_coords", pagenum, coords) d.addCallback(lambda x,y: self.data_received(), self.disconnect) def ping(self): diff --git a/cournal/server/server.py b/cournal/server/server.py index 8d6670b..184f922 100755 --- a/cournal/server/server.py +++ b/cournal/server/server.py @@ -36,6 +36,9 @@ from cournal import __versionstring__ as cournal_version from cournal.document.stroke import Stroke +from cournal.document.rect import Rect +from cournal.document.circle import Circle + from cournal.server import pickle_legacy # 0 - none @@ -91,7 +94,7 @@ def dict_to_object(self, d): Constructs objects from a dict()-like object, which contain a __class__ key to specify the objects class. """ - classes = {"Document": Document, "Page": Page, "Stroke": Stroke} + classes = {"Document": Document, "Page": Page, "Stroke": Stroke, "Rect": Rect, "Circle": Circle} if "__class__" in d and d["__class__"] in classes: d.pop("__module__") # for future use class_name = d.pop("__class__") @@ -104,7 +107,7 @@ def dict_to_object(self, d): class Page: """ - A page in a document, having multiple strokes. + A page in a document, having multiple objects. """ def __init__(self, strokes=None): if strokes is None: @@ -408,7 +411,7 @@ def add_user(self, user): self.users.append(user) for pagenum in range(len(self.pages)): for stroke in self.pages[pagenum].strokes: - user.call_remote("new_stroke", pagenum, stroke) + user.call_remote("new_item", pagenum, stroke) def remove_user(self, user): """ @@ -434,43 +437,43 @@ def broadcast(self, method, *args, except_user=None): if user != except_user: user.call_remote(method, *args) - def view_new_stroke(self, from_user, pagenum, stroke): + def view_new_item(self, from_user, pagenum, item): """ - Broadcast the stroke received from one to all other clients. - Called by clients to add a new stroke. + Broadcast the item received from one to all other clients. + Called by clients to add a new item. Positional arguments: - from_user -- The User object of the initiiating user. - pagenum -- Page number the new stroke. - stroke -- The new stroke + from_user -- The User item of the initiiating user. + pagenum -- Page number the new item. + item -- The new item """ self.has_unsaved_changes = True while len(self.pages) <= pagenum: self.pages.append(Page()) - self.pages[pagenum].strokes.append(stroke) + self.pages[pagenum].strokes.append(item) - debug(3, _("New stroke on page {}").format(pagenum + 1)) - self.broadcast("new_stroke", pagenum, stroke, except_user=from_user) + debug(3, _("New item on page {}").format(pagenum + 1)) + self.broadcast("new_item", pagenum, item, except_user=from_user) - def view_delete_stroke_with_coords(self, from_user, pagenum, coords): + def view_delete_item_with_coords(self, from_user, pagenum, coords): """ - Broadcast the delete stroke command from one to all other clients. - Called by Clients to delete a stroke. + Broadcast the delete items command from one to all other clients. + Called by Clients to delete a item. Positional arguments: - from_user -- The User object of the initiiating user. - pagenum -- Page number the deleted stroke - coords -- The list coordinates of the deleted stroke + from_user -- The User item of the initiiating user. + pagenum -- Page number the deleted item + coords -- The list coordinates of the deleted item """ self.has_unsaved_changes = True - for stroke in self.pages[pagenum].strokes: - if stroke.coords == coords: - self.pages[pagenum].strokes.remove(stroke) + for item in self.pages[pagenum].strokes: + if item.coords == coords: + self.pages[pagenum].strokes.remove(item) - debug(3, _("Deleted stroke on page {}").format(pagenum + 1)) - self.broadcast("delete_stroke_with_coords", pagenum, coords, except_user=from_user) + debug(3, _("Deleted item on page {}").format(pagenum + 1)) + self.broadcast("delete_item_with_coords", pagenum, coords, except_user=from_user) class CmdlineParser(): """ diff --git a/cournal/viewer/pagewidget.py b/cournal/viewer/pagewidget.py index 3d5b956..22e7fa4 100644 --- a/cournal/viewer/pagewidget.py +++ b/cournal/viewer/pagewidget.py @@ -20,6 +20,7 @@ from gi.repository import Gtk, Gdk import cairo +from cournal.viewer.tools import pen, eraser, navigation, rect, primary from cournal.viewer.tools import pen, eraser, navigation from cournal.document import search @@ -110,7 +111,7 @@ def on_size_allocate(self, widget, alloc): def draw(self, widget, context): """ - Draw the widget (the PDF, all strokes and the background). Called by Gtk. + Draw the widget (the PDF, all objects and the background). Called by Gtk. Positional arguments: widget -- The widget to redraw @@ -132,8 +133,8 @@ def draw(self, widget, context): self.page.pdf.render(bb_ctx) bb_ctx.restore() - for stroke in self.page.layers[0].strokes: - stroke.draw(bb_ctx, scaling) + for item in self.page.layers[0].items: + item.draw(bb_ctx, scaling) # Highlight search result if self.page.search_marker: @@ -163,10 +164,14 @@ def press(self, widget, event): event -- The Gdk.Event, which stores the location of the pointer """ if event.button == 1: - self.active_tool = pen + self.active_tool = primary.current_tool elif event.button == 2: + if self.preview_item: + self.active_tool.release(self, event) self.active_tool = navigation elif event.button == 3: + if self.preview_item: + self.active_tool.release(self, event) self.active_tool = eraser else: return @@ -191,20 +196,20 @@ def release(self, widget, event): self.active_tool.release(self, event) self.active_tool = None - def draw_remote_stroke(self, stroke): + def draw_remote_item(self, item): """ - Draw a single stroke on the widget. - Meant to be called by networking code, when a remote user drew a stroke. + Draw a single item on the widget. + Meant to be called by networking code, when a remote user drew a item. Positional arguments: - stroke -- The Stroke object, which is to be drawn. + item -- The item, which is to be drawn. """ if self.backbuffer: scaling = self.widget_width / self.page.width context = cairo.Context(self.backbuffer) context.scale(scaling, scaling) - x, y, x2, y2 = stroke.draw(context, scaling) + x, y, x2, y2 = item.draw(context, scaling) update_rect = Gdk.Rectangle() update_rect.x = x-2 @@ -214,13 +219,13 @@ def draw_remote_stroke(self, stroke): if self.get_window(): self.get_window().invalidate_rect(update_rect, False) - def delete_remote_stroke(self, stroke): + def delete_remote_item(self, item): """ - Rerender the part of the widget, where a stroke was deleted - Meant do be called by networking code, when a remote user deleted a stroke. + Rerender the part of the widget, where a item was deleted + Meant do be called by networking code, when a remote user deleted a item. Positional arguments: - stroke -- The Stroke object, which was deleted. + item -- The item, which was deleted. """ if self.backbuffer: self.backbuffer_valid = False diff --git a/cournal/viewer/tools/circle.py b/cournal/viewer/tools/circle.py new file mode 100644 index 0000000..0c33bdb --- /dev/null +++ b/cournal/viewer/tools/circle.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# This file is part of Cournal. +# Copyright (C) 2012 Simon Vetter +# +# Cournal 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 3 of the License, or +# (at your option) any later version. +# +# Cournal 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 Cournal. If not, see . + +import cairo +from gi.repository import Gdk +from cournal.viewer.tools import primary +from cournal.document.circle import Circle +from cournal.document import history +import math + +""" +A circle tool. Draws a circle with certain colors and radius. +""" + +_start_point = None +_last_point = None +_start_point_copy = [0,0] + +def press(widget, event): + """ + Mouse down event. Keep the position + + Positional arguments: + widget -- The PageWidget, which triggered the event + event -- The Gdk.Event, which stores the location of the pointer + """ + global _start_point + _start_point = [event.x, event.y] + scaling = widget.backbuffer.get_width()/widget.page.width + widget.preview_item = Circle( + widget.page.layers[0], + primary.color, + primary.fill, + primary.fillcolor, + primary.linewidth, + [event.x/scaling ,event.y/scaling], + [0,0,1]) + +def motion(widget, event): + """ + Mouse motion event. Update item and set render borders + + Positional arguments: see press() + """ + global _last_point, _preview_copy + + scaling = widget.backbuffer.get_width()/widget.page.width + if _last_point: + update_rect = Gdk.Rectangle() + x = min(_last_point[0], _start_point[0]) - primary.linewidth*scaling/2 + y = min(_last_point[1], _start_point[1]) - primary.linewidth*scaling/2 + x2 = max(_last_point[0], _start_point[0]) + primary.linewidth*scaling/2 + y2 = max(_last_point[1], _start_point[1]) + primary.linewidth*scaling/2 + update_rect.x = x-2 + update_rect.y = y-2 + update_rect.width = x2-x+4 + update_rect.height = y2-y+4 + widget.get_window().invalidate_rect(update_rect, False) + _last_point = [event.x, event.y] + + update_rect = Gdk.Rectangle() + x = min(_start_point[0], event.x) - primary.linewidth*scaling/2 + y = min(_start_point[1], event.y) - primary.linewidth*scaling/2 + x2 = max(_start_point[0], event.x) + primary.linewidth*scaling/2 + y2 = max(_start_point[1], event.y) + primary.linewidth*scaling/2 + width = max(_start_point[0], event.x)-min(_start_point[0], event.x) + height = max(_start_point[1], event.y)-min(_start_point[1], event.y) + if width * height != 0: + update_rect.x = x-2 + update_rect.y = y-2 + update_rect.width = x2-x+4 + update_rect.height = y2-y+4 + widget.get_window().invalidate_rect(update_rect, False) + widget.preview_item.coords = [ + (x+x2)/scaling/2, + (y+y2)/scaling/2] + scale = (width + height) / 2 + widget.preview_item.scale = [ + width/scaling/2, + height/scaling/2, + scale/scaling + ] + else: + widget.preview_item.scale = [0, 0, 1] + + +def release(widget, event): + """ + Mouse release event. Inform the corresponding Page instance, that the stroke + is finished. + + This will cause the stroke to be sent to the server, if it is connected. + + Positional arguments: see press() + """ + global _start_point + widget.preview_item = None + actualWidth = widget.get_allocation().width + + # Sort Coords + cx = min(_start_point[0], event.x) + cy = min(_start_point[1], event.y) + cx2 = max(_start_point[0], event.x) + cy2 = max(_start_point[1], event.y) + + # Calculate center + center = [(cx+cx2) / 2, (cy+cy2) / 2] + + # Calculate width and height + width = cx2-cx + height = cy2-cy + + # Stop it here if there's nothing to draw + if height * width == 0: + return + + scale = (width + height) / 2 + scale_width = width / scale + scale_height = height / scale + + current_item = Circle( + widget.page.layers[0], + primary.color, + primary.fill, + primary.fillcolor, + primary.linewidth, + [center[0]*widget.page.width/actualWidth, center[1]*widget.page.width/actualWidth], + [width/2*widget.page.width/actualWidth, height/2*widget.page.width/actualWidth, scale*widget.page.width/actualWidth]) + widget.page.new_item(current_item, send_to_network=True) + history.register_draw_item(current_item, widget.page) + + _start_point = None + + \ No newline at end of file diff --git a/cournal/viewer/tools/eraser.py b/cournal/viewer/tools/eraser.py index 94c3186..93ca86e 100644 --- a/cournal/viewer/tools/eraser.py +++ b/cournal/viewer/tools/eraser.py @@ -18,28 +18,28 @@ # along with Cournal. If not, see . """ -An eraser tool, that this deletes the complete stroke. +An eraser tool, that this deletes the complete item. """ THICKNESS = 6 # pt def press(widget, event): """ - Mouse down event. Delete all strokes near the pointer location. + Mouse down event. Delete all item near the pointer location. Positional arguments: widget -- The PageWidget, which triggered the event event -- The Gdk.Event, which stores the location of the pointer """ - _delete_strokes_near(widget, event.x, event.y) + _delete_items_near(widget, event.x, event.y) def motion(widget, event): """ - Mouse motion event. Delete all strokes near the pointer location. + Mouse motion event. Delete all items near the pointer location. Positional arguments: see press() """ - _delete_strokes_near(widget, event.x, event.y) + _delete_items_near(widget, event.x, event.y) def release(widget, event): """ @@ -50,18 +50,18 @@ def release(widget, event): """ pass -def _delete_strokes_near(widget, x, y): +def _delete_items_near(widget, x, y): """ - Delete all strokes near a given point. + Delete all items near a given point. Positional arguments: - widget -- The PageWidget to delete strokes on. - x -- Delete stroke near this point (x coordinate on the widget (!)) - y -- Delete stroke near this point (y coordinate on the widget (!)) + widget -- The PageWidget to delete items on. + x -- Delete item near this point (x coordinate on the widget (!)) + y -- Delete item near this point (y coordinate on the widget (!)) """ scaling = widget.page.width / widget.get_allocation().width x *= scaling y *= scaling - for stroke in widget.page.get_strokes_near(x, y, THICKNESS): - widget.page.delete_stroke(stroke, send_to_network=True) + for item in widget.page.get_items_near(x, y, THICKNESS): + widget.page.delete_item(item, send_to_network=True) diff --git a/cournal/viewer/tools/line.py b/cournal/viewer/tools/line.py new file mode 100644 index 0000000..4042130 --- /dev/null +++ b/cournal/viewer/tools/line.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# This file is part of Cournal. +# Copyright (C) 2012 Simon Vetter +# +# Cournal 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 3 of the License, or +# (at your option) any later version. +# +# Cournal 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 Cournal. If not, see . + +import cairo +from gi.repository import Gdk +from cournal.viewer.tools import primary +from cournal.document.stroke import Stroke + +""" +A line tool. Draws a straight line with a certain color and size. +""" + +_start_point = None +_current_coords = None +_current_stroke = None +_last_point = None + +def press(widget, event): + """ + Mouse down event. Draw a point on the pointer location. + + Positional arguments: + widget -- The PageWidget, which triggered the event + event -- The Gdk.Event, which stores the location of the pointer + """ + global _start_point, _current_coords, _current_stroke + actualWidth = widget.get_allocation().width + + _current_stroke = widget.page.new_unfinished_stroke(color=primary.color, linewidth=primary.linewidth) + _current_coords = _current_stroke.coords + _current_coords.append([event.x*widget.page.width/actualWidth, event.y*widget.page.width/actualWidth]) + + _start_point = [event.x, event.y] + + widget.page.layers[0].items.append(_current_stroke) + widget.preview_item = Stroke( + primary.color, + primary.linewidth, + widget.page.layers[0], + [[_start_point[0]*widget.page.width/actualWidth, _start_point[1]*widget.page.width/actualWidth], + [_start_point[0]*widget.page.width/actualWidth, _start_point[1]*widget.page.width/actualWidth]] + ) + +def motion(widget, event): + """ + Mouse motion event. Update item and set render borders + + Positional arguments: see press() + """ + global _last_point + + scaling = widget.backbuffer.get_width()/widget.page.width + if _last_point: + update_rect = Gdk.Rectangle() + x = min(_last_point[0], _start_point[0]) - primary.linewidth*scaling/2 + y = min(_last_point[1], _start_point[1]) - primary.linewidth*scaling/2 + x2 = max(_last_point[0], _start_point[0]) + primary.linewidth*scaling/2 + y2 = max(_last_point[1], _start_point[1]) + primary.linewidth*scaling/2 + update_rect.x = x-2 + update_rect.y = y-2 + update_rect.width = x2-x+4 + update_rect.height = y2-y+4 + widget.get_window().invalidate_rect(update_rect, False) + _last_point = [event.x, event.y] + + update_rect = Gdk.Rectangle() + x = min(_start_point[0], event.x) - primary.linewidth*scaling/2 + y = min(_start_point[1], event.y) - primary.linewidth*scaling/2 + x2 = max(_start_point[0], event.x) + primary.linewidth*scaling/2 + y2 = max(_start_point[1], event.y) + primary.linewidth*scaling/2 + + update_rect.x = x-2 + update_rect.y = y-2 + update_rect.width = x2-x+4 + update_rect.height = y2-y+4 + widget.get_window().invalidate_rect(update_rect, False) + widget.preview_item.coords[1] = [event.x/scaling, event.y/scaling] + +def release(widget, event): + """ + Mouse release event. + + This will cause the item to be sent to the server, if it is connected. + + Positional arguments: see press() + """ + + global _start_point, _current_coords, _current_stroke + scaling = widget.backbuffer.get_width()/widget.page.width + + actualWidth = widget.get_allocation().width + _current_coords.append([event.x/scaling, event.y/scaling]) + widget.page.finish_stroke(_current_stroke) + + context = cairo.Context(widget.backbuffer) + context.scale(scaling, scaling) + _current_stroke.draw(context, scaling) + + + widget.preview_item = None + _start_point = None + _current_coords = None + _current_stroke = None diff --git a/cournal/viewer/tools/pen.py b/cournal/viewer/tools/pen.py index 0a72a81..f499068 100644 --- a/cournal/viewer/tools/pen.py +++ b/cournal/viewer/tools/pen.py @@ -19,6 +19,7 @@ import cairo from gi.repository import Gdk +from cournal.viewer.tools import primary """ A pen tool. Draws a stroke with a certain color and size. @@ -27,8 +28,6 @@ _last_point = None _current_coords = None _current_stroke = None -linewidth = 1.5 -color = (0,0,128,255) def press(widget, event): """ @@ -38,16 +37,16 @@ def press(widget, event): widget -- The PageWidget, which triggered the event event -- The Gdk.Event, which stores the location of the pointer """ - global _last_point, _current_coords, _current_stroke, linewidth, color + global _last_point, _current_coords, _current_stroke - _current_stroke = widget.page.new_unfinished_stroke(color=color, linewidth=linewidth) + _current_stroke = widget.page.new_unfinished_stroke(color=primary.color, linewidth=primary.linewidth) _current_coords = _current_stroke.coords widget.preview_item = _current_stroke _last_point = [event.x, event.y] motion(widget, event) - widget.page.layers[0].strokes.append(_current_stroke) + widget.page.layers[0].items.append(_current_stroke) def motion(widget, event): """ @@ -60,10 +59,10 @@ def motion(widget, event): update_rect = Gdk.Rectangle() scaling = widget.backbuffer.get_width()/widget.page.width - x = min(_last_point[0], event.x) - linewidth*scaling/2 - y = min(_last_point[1], event.y) - linewidth*scaling/2 - x2 = max(_last_point[0], event.x) + linewidth*scaling/2 - y2 = max(_last_point[1], event.y) + linewidth*scaling/2 + x = min(_last_point[0], event.x) - primary.linewidth*scaling/2 + y = min(_last_point[1], event.y) - primary.linewidth*scaling/2 + x2 = max(_last_point[0], event.x) + primary.linewidth*scaling/2 + y2 = max(_last_point[1], event.y) + primary.linewidth*scaling/2 update_rect.x = x-2 update_rect.y = y-2 diff --git a/cournal/viewer/tools/primary.py b/cournal/viewer/tools/primary.py new file mode 100644 index 0000000..bcbadce --- /dev/null +++ b/cournal/viewer/tools/primary.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# This file is part of Cournal. +# Copyright (C) 2012 Simon Vetter +# +# Cournal 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 3 of the License, or +# (at your option) any later version. +# +# Cournal 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 Cournal. If not, see . + +import cairo +from gi.repository import Gdk + +""" +The primary tool. Defines stroke color, fill color, width and the current tool +""" + +linewidth = 1.5 +color = (0,0,128,255) +fillcolor = (255,128,0,255) +current_tool = None +fill = True \ No newline at end of file diff --git a/cournal/viewer/tools/rect.py b/cournal/viewer/tools/rect.py new file mode 100644 index 0000000..d663038 --- /dev/null +++ b/cournal/viewer/tools/rect.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# This file is part of Cournal. +# Copyright (C) 2012 Simon Vetter +# +# Cournal 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 3 of the License, or +# (at your option) any later version. +# +# Cournal 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 Cournal. If not, see . + +import cairo +from gi.repository import Gdk +from cournal.viewer.tools import primary +from cournal.document.rect import Rect +from cournal.document import history + +_current_item = None +_current_coords = None +_start_point = None +_last_point = None + +def press(widget, event): + """ + Mouse down event. Set the start point for the item + + Positional arguments: + widget -- The PageWidget, which triggered the event + event -- The Gdk.Event, which stores the location of the pointer + """ + global _start_point, _current_coords, _current_item + + actualWidth = widget.get_allocation().width + + _current_item = Rect(widget.page.layers[0], primary.color, primary.fill, primary.fillcolor, primary.linewidth, []) + _current_coords = _current_item.coords + + _start_point = [event.x, event.y] + _current_coords.append(event.x*widget.page.width/actualWidth) + _current_coords.append(event.y*widget.page.width/actualWidth) + widget.preview_item = Rect( + widget.page.layers[0], + primary.color, + primary.fill, + primary.fillcolor, + primary.linewidth, + [_start_point[0]*widget.page.width/actualWidth, + _start_point[1]*widget.page.width/actualWidth, + _start_point[0]*widget.page.width/actualWidth, + _start_point[1]*widget.page.width/actualWidth] + ) + +def motion(widget, event): + """ + Mouse motion event. Update item and set render borders + + Positional arguments: see press() + """ + global _last_point + + scaling = widget.backbuffer.get_width()/widget.page.width + if _last_point: + update_rect = Gdk.Rectangle() + x = min(_last_point[0], _start_point[0]) - primary.linewidth*scaling/2 + y = min(_last_point[1], _start_point[1]) - primary.linewidth*scaling/2 + x2 = max(_last_point[0], _start_point[0]) + primary.linewidth*scaling/2 + y2 = max(_last_point[1], _start_point[1]) + primary.linewidth*scaling/2 + update_rect.x = x-2 + update_rect.y = y-2 + update_rect.width = x2-x+4 + update_rect.height = y2-y+4 + widget.get_window().invalidate_rect(update_rect, False) + _last_point = [event.x, event.y] + + update_rect = Gdk.Rectangle() + x = min(_start_point[0], event.x) - primary.linewidth*scaling/2 + y = min(_start_point[1], event.y) - primary.linewidth*scaling/2 + x2 = max(_start_point[0], event.x) + primary.linewidth*scaling/2 + y2 = max(_start_point[1], event.y) + primary.linewidth*scaling/2 + + update_rect.x = x-2 + update_rect.y = y-2 + update_rect.width = x2-x+4 + update_rect.height = y2-y+4 + widget.get_window().invalidate_rect(update_rect, False) + widget.preview_item.coords[2] = event.x/scaling + widget.preview_item.coords[3] = event.y/scaling + +def release(widget, event): + """ + Mouse release event. + + This will cause the item to be sent to the server, if it is connected. + + Positional arguments: see press() + """ + + + global _start_point, _current_coords, _current_item + actualWidth = widget.get_allocation().width + + _current_coords.append(event.x*widget.page.width/actualWidth) + _current_coords.append(event.y*widget.page.width/actualWidth) + widget.page.new_item(_current_item, send_to_network=True) + history.register_draw_item(_current_item, widget.page) + + widget.preview_item = None + _start_point = None + _current_coords = None + _current_item = None diff --git a/icons/hicolor/scalable/actions/cournal-circle-tool.svg b/icons/hicolor/scalable/actions/cournal-circle-tool.svg new file mode 100644 index 0000000..04faa08 --- /dev/null +++ b/icons/hicolor/scalable/actions/cournal-circle-tool.svg @@ -0,0 +1,179 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/hicolor/scalable/actions/cournal-fill-tool.svg b/icons/hicolor/scalable/actions/cournal-fill-tool.svg new file mode 100644 index 0000000..5c5d061 --- /dev/null +++ b/icons/hicolor/scalable/actions/cournal-fill-tool.svg @@ -0,0 +1,245 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/hicolor/scalable/actions/cournal-line-tool.svg b/icons/hicolor/scalable/actions/cournal-line-tool.svg new file mode 100644 index 0000000..b3ad40c --- /dev/null +++ b/icons/hicolor/scalable/actions/cournal-line-tool.svg @@ -0,0 +1,178 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/hicolor/scalable/actions/cournal-pen-big.svg b/icons/hicolor/scalable/actions/cournal-pen-big.svg new file mode 100644 index 0000000..b1369a9 --- /dev/null +++ b/icons/hicolor/scalable/actions/cournal-pen-big.svg @@ -0,0 +1,260 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/hicolor/scalable/actions/cournal-pen-medium.svg b/icons/hicolor/scalable/actions/cournal-pen-medium.svg new file mode 100644 index 0000000..de0f4f2 --- /dev/null +++ b/icons/hicolor/scalable/actions/cournal-pen-medium.svg @@ -0,0 +1,230 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/hicolor/scalable/actions/cournal-pen-small.svg b/icons/hicolor/scalable/actions/cournal-pen-small.svg new file mode 100644 index 0000000..9f6ff87 --- /dev/null +++ b/icons/hicolor/scalable/actions/cournal-pen-small.svg @@ -0,0 +1,230 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/hicolor/scalable/actions/cournal-pen-tool.svg b/icons/hicolor/scalable/actions/cournal-pen-tool.svg new file mode 100644 index 0000000..dad62e4 --- /dev/null +++ b/icons/hicolor/scalable/actions/cournal-pen-tool.svg @@ -0,0 +1,201 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/hicolor/scalable/actions/cournal-rect-tool.svg b/icons/hicolor/scalable/actions/cournal-rect-tool.svg new file mode 100644 index 0000000..4ad8364 --- /dev/null +++ b/icons/hicolor/scalable/actions/cournal-rect-tool.svg @@ -0,0 +1,178 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +