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 @@
- accelgroupgtk-save
@@ -92,12 +90,14 @@
1.5
+ 0.7action_pensize_normal
+
@@ -105,6 +105,7 @@
3action_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
+ FalseFalse_FileTrue
@@ -235,6 +277,7 @@
+ FalseTrueFalse_Edit
@@ -276,6 +319,7 @@
+ FalseTrueFalse_Help
@@ -315,6 +359,7 @@
TrueFalseAnnotate PDF
+ action_open_pdfFalse
@@ -328,6 +373,7 @@
FalseTrueConnect to server
+ action_connectFalse
@@ -340,6 +386,7 @@
TrueFalseSave current document
+ action_saveTrue
@@ -362,6 +409,7 @@
action_zoom_inTrueFalse
+ action_zoom_inTrue
@@ -374,6 +422,7 @@
action_zoom_outTrueFalse
+ action_zoom_outTrue
@@ -386,6 +435,7 @@
action_zoom_fitTrueFalse
+ action_zoom_fitTrue
@@ -408,6 +458,7 @@
action_undoTrueFalse
+ action_undoTrue
@@ -420,7 +471,91 @@
action_redoTrueFalse
+ 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
+ CircleTrue
+ cournal-circle-tool
+ tool_penFalse
@@ -438,16 +573,20 @@
-
+ Falseaction_pen_colorTrueFalse
+ action_pen_color
+ False
+ FalseTrueTrueTrue
+ FalsenoneTruePen 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
+
+
+ Falseaction_pensize_smallTrueFalse
+ action_pensize_small
+ False0.7
+ cournal-pen-smalltool_pensize_normal
@@ -474,10 +673,14 @@
+ Falseaction_pensize_normalTrueFalse
+ action_pensize_normal
+ False1.5
+ cournal-pen-mediumFalse
@@ -486,10 +689,14 @@
+ Falseaction_pensize_bigTrueFalse
+ action_pensize_big
+ False8.0
+ cournal-pen-bigtool_pensize_normal
@@ -501,7 +708,7 @@
FalseTrue
- 1
+ 2
@@ -522,7 +729,64 @@
TrueTrue
- 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
+ FalseTrueFalseFalseTrue
+ Falseimage_btn_prevnoneFalse
@@ -596,10 +862,12 @@
+ FalseTrueFalseFalseTrue
+ Falseimage_btn_nextnone
@@ -637,61 +905,7 @@
FalseFalseend
- 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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+