-
-
Save flyser/2926964 to your computer and use it in GitHub Desktop.
stdin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/cournal-server.py b/cournal-server.py | |
index 75cf46a..44c99cd 100755 | |
--- a/cournal-server.py | |
+++ b/cournal-server.py | |
@@ -41,12 +41,14 @@ USERNAME = "test" | |
PASSWORD = "testpw" | |
class Page: | |
+ """ | |
+ A page in a document, having multiple strokes. | |
+ """ | |
def __init__(self): | |
self.strokes = [] | |
class CournalServer: | |
def __init__(self): | |
- # {documentname: Document()} | |
self.documents = dict() | |
def joinDocument(self, documentname, user): | |
@@ -92,32 +94,60 @@ class User(pb.Avatar): | |
self.remote.callRemote(method, pagenum, stroke) | |
class Document(pb.Viewable): | |
+ """ | |
+ A Cournal document, having multiple pages. | |
+ """ | |
def __init__(self, documentname): | |
self.name = documentname | |
- self.users = list() | |
+ self.users = [] | |
self.pages = [] | |
- #self.pages[0].strokes.append([1.01, 20.1, 2.0, 10.0]) | |
- #self.pages[0].strokes.append([2.01, 10.1, 3.0, 0.0]) | |
def addUser(self, user): | |
+ """ | |
+ Called, when a user starts editing this document. Send him all strokes | |
+ that are currently in the document. | |
+ | |
+ Positional arguments: | |
+ user -- The concerning User object. | |
+ """ | |
self.users.append(user) | |
for pagenum in range(len(self.pages)): | |
for stroke in self.pages[pagenum].strokes: | |
user.remote.callRemote("new_stroke", pagenum, stroke) | |
def removeUser(self, user): | |
+ """ | |
+ Called, when a user stops editing this document. Remove him from list | |
+ | |
+ Positional arguments: | |
+ user -- The concerning User object. | |
+ """ | |
self.users.remove(user) | |
- def broadcast(self, method, pagenum, stroke, except_user=None): | |
+ def broadcast(self, method, *args, except_user=None): | |
+ """ | |
+ Broadcast a method call to all clients | |
+ | |
+ Positional arguments: | |
+ method -- Name of the remote method | |
+ *args -- Arguments of the remote method | |
+ | |
+ Keyword arguments: | |
+ except_user -- Don't broadcast to this user. | |
+ """ | |
for user in self.users: | |
if user != except_user: | |
- user.send_stroke(method, pagenum, stroke) | |
+ user.send_stroke(method, *args) | |
def view_new_stroke(self, from_user, pagenum, stroke): | |
""" | |
- Broadcast the stroke received from one to all other clients | |
- | |
+ Broadcast the stroke received from one to all other clients. | |
Called by clients to add a new stroke. | |
+ | |
+ Positional arguments: | |
+ from_user -- The User object of the initiiating user. | |
+ pagenum -- Page number the new stroke. | |
+ stroke -- The new stroke | |
""" | |
while len(self.pages) <= pagenum: | |
self.pages.append(Page()) | |
@@ -127,6 +157,15 @@ class Document(pb.Viewable): | |
self.broadcast("new_stroke", pagenum, stroke, except_user=from_user) | |
def view_delete_stroke_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. | |
+ | |
+ 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 | |
+ """ | |
for stroke in self.pages[pagenum].strokes: | |
if stroke.coords == coords: | |
self.pages[pagenum].strokes.remove(stroke) | |
@@ -134,35 +173,12 @@ class Document(pb.Viewable): | |
debug(3, "Deleted stroke on page", pagenum+1) | |
self.broadcast("delete_stroke_with_coords", pagenum, coords, except_user=from_user) | |
-def debug(level, *args): | |
- if level <= DEBUGLEVEL: | |
- print(*args) | |
- | |
-def main(): | |
- realm = CournalRealm() | |
- realm.server = CournalServer() | |
- checker = checkers.InMemoryUsernamePasswordDatabaseDontUse() | |
- checker.addUser(USERNAME, PASSWORD) | |
- p = portal.Portal(realm, [checker]) | |
- args = CmdlineParser().parse() | |
- | |
- port = args.port | |
- | |
- try: | |
- reactor.listenTCP(port, pb.PBServerFactory(p)) | |
- except CannotListenError as err: | |
- debug(0, "ERROR: Failed to listen on port", err.port) | |
- return 1 | |
- | |
- debug(2, "Listening on port", port) | |
- reactor.run() | |
- | |
class CmdlineParser(): | |
""" | |
Parse commandline options. Results are available as attributes of this class | |
""" | |
def __init__(self): | |
- """Constructor. All variables initialized here are public""" | |
+ """Constructor. All variables initialized here are public.""" | |
self.port = DEFAULT_PORT | |
def parse(self): | |
@@ -180,5 +196,29 @@ class CmdlineParser(): | |
self.port = args.port[0] | |
return self | |
+def main(): | |
+ """Start a Cournal server""" | |
+ realm = CournalRealm() | |
+ realm.server = CournalServer() | |
+ checker = checkers.InMemoryUsernamePasswordDatabaseDontUse() | |
+ checker.addUser(USERNAME, PASSWORD) | |
+ p = portal.Portal(realm, [checker]) | |
+ args = CmdlineParser().parse() | |
+ | |
+ port = args.port | |
+ | |
+ try: | |
+ reactor.listenTCP(port, pb.PBServerFactory(p)) | |
+ except CannotListenError as err: | |
+ debug(0, "ERROR: Failed to listen on port", err.port) | |
+ return 1 | |
+ | |
+ debug(2, "Listening on port", port) | |
+ reactor.run() | |
+ | |
+def debug(level, *args): | |
+ if level <= DEBUGLEVEL: | |
+ print(*args) | |
+ | |
if __name__ == '__main__': | |
sys.exit(main()) | |
diff --git a/cournal.py b/cournal.py | |
index 3cf995c..428bd15 100755 | |
--- a/cournal.py | |
+++ b/cournal.py | |
@@ -27,6 +27,7 @@ from gi.repository import Gtk | |
from cournal import MainWindow | |
def main(): | |
+ """Start Cournal""" | |
Gtk.IconTheme.get_default().prepend_search_path("./icons") | |
window = MainWindow() | |
diff --git a/cournal/aboutdialog.py b/cournal/aboutdialog.py | |
index 803d44a..bc9b21e 100644 | |
--- a/cournal/aboutdialog.py | |
+++ b/cournal/aboutdialog.py | |
@@ -20,7 +20,17 @@ | |
from gi.repository import Gtk | |
class AboutDialog(Gtk.AboutDialog): | |
+ """ | |
+ The About Dialog of Cournal. | |
+ """ | |
def __init__(self, parent=None, **args): | |
+ """ | |
+ Constructor. | |
+ | |
+ Keyword arguments: | |
+ parent -- Parent window of this dialog (defaults to no parent) | |
+ **args -- Arguments passed to the Gtk.AboutDialog constructor | |
+ """ | |
Gtk.AboutDialog.__init__(self, **args) | |
self.set_modal(False) | |
@@ -35,14 +45,9 @@ class AboutDialog(Gtk.AboutDialog): | |
self.set_authors(["Fabian Henze", "Simon Vetter"]) | |
self.set_artists(["Simon Vetter"]) | |
- def response_cb(self, widget, response_id): | |
- self.destroy() | |
- | |
- if response_id == Gtk.ResponseType.ACCEPT: | |
- print("OK clicked") | |
- | |
def run_nonblocking(self): | |
- self.connect('response', self.response_cb) | |
+ """Run the dialog asynchronously, reusing the mainloop of the parent.""" | |
+ self.connect('response', lambda a,b: self.destroy()) | |
self.show() | |
# For testing purposes: | |
diff --git a/cournal/connectiondialog.py b/cournal/connectiondialog.py | |
index d1b12d0..e2d5359 100644 | |
--- a/cournal/connectiondialog.py | |
+++ b/cournal/connectiondialog.py | |
@@ -21,7 +21,19 @@ from gi.repository import Gtk, Gdk | |
from . import network | |
class ConnectionDialog(Gtk.Dialog): | |
+ """ | |
+ The "Connect to Server" dialog of Cournal. | |
+ """ | |
def __init__(self, parent, **args): | |
+ """ | |
+ Constructor. | |
+ | |
+ Positional arguments: | |
+ parent -- Parent window of this dialog | |
+ | |
+ Keyword arguments: | |
+ **args -- Arguments passed to the Gtk.Dialog constructor | |
+ """ | |
Gtk.Dialog.__init__(self, **args) | |
self.parent = parent | |
@@ -51,6 +63,14 @@ class ConnectionDialog(Gtk.Dialog): | |
self.show_all() | |
def response(self, widget, response_id): | |
+ """ | |
+ Called, when the user clicked on a button ('Connect' or 'Abort'). | |
+ Initiate a new connection or close the dialog. | |
+ | |
+ Positional arguments: | |
+ widget -- The widget, which triggered the response. | |
+ response_id -- A Gtk.ResponseType indicating, which button the user pressed. | |
+ """ | |
if response_id != Gtk.ResponseType.ACCEPT: | |
self.destroy() | |
return | |
@@ -70,8 +90,24 @@ class ConnectionDialog(Gtk.Dialog): | |
self.new_connection(server, port) | |
- | |
+ def confirm_clear_document(self): | |
+ message = Gtk.MessageDialog(self, (Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT), Gtk.MessageType.WARNING, Gtk.ButtonsType.YES_NO, "Close current document?" ) | |
+ message.format_secondary_text("You will loose all changes to your current document, if you connect to a server. Continue without saving?") | |
+ message.set_title("Warning") | |
+ if message.run() != Gtk.ResponseType.YES: | |
+ message.destroy() | |
+ return False | |
+ message.destroy() | |
+ return True | |
+ | |
def new_connection(self, server, port): | |
+ """ | |
+ Start to connect to a server and update UI accordingly. | |
+ | |
+ Positional arguments: | |
+ server -- The hostname of the server to connect to. | |
+ port -- The port on the server | |
+ """ | |
network.set_document(self.parent.document) | |
d = network.connect(server, port) | |
d.addCallbacks(self.on_connected, self.on_connection_failure) | |
@@ -82,21 +118,16 @@ class ConnectionDialog(Gtk.Dialog): | |
self.connecting_label.set_text("Connecting to {} ...".format(server)) | |
self.get_action_area().set_sensitive(False) | |
- | |
- def confirm_clear_document(self): | |
- message = Gtk.MessageDialog(self, (Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT), Gtk.MessageType.WARNING, Gtk.ButtonsType.YES_NO, "Close current document?" ) | |
- message.format_secondary_text("You will loose all changes to your current document, if you connect to a server. Continue without saving?") | |
- message.set_title("Warning") | |
- if message.run() != Gtk.ResponseType.YES: | |
- message.destroy() | |
- return False | |
- message.destroy() | |
- return True | |
- | |
def on_connected(self, perspective): | |
+ """ | |
+ Called, when the connection to the server succeeded. Just close the dialog. | |
+ """ | |
self.destroy() | |
def on_connection_failure(self, reason): | |
+ """ | |
+ Called, when the connection to the server failed. Display error message. | |
+ """ | |
error = reason.getErrorMessage() | |
self.multipage.set_current_page(0) | |
@@ -106,11 +137,21 @@ class ConnectionDialog(Gtk.Dialog): | |
self.get_action_area().set_sensitive(True) | |
def run_nonblocking(self): | |
+ """Run the dialog asynchronously, reusing the mainloop of the parent.""" | |
self.connect('response', self.response) | |
self.show() | |
class ServerPortEntry(Gtk.EventBox): | |
+ """ | |
+ A Gtk.Entry-like widget with one field for a hostname and one for a port. | |
+ """ | |
def __init__(self, **args): | |
+ """ | |
+ Constructor. | |
+ | |
+ Keyword arguments: | |
+ **args -- Arguments passed to the Gtk.EventBox constructor | |
+ """ | |
Gtk.EventBox.__init__(self, **args) | |
frame = Gtk.Frame() | |
@@ -138,20 +179,41 @@ class ServerPortEntry(Gtk.EventBox): | |
self.port_entry.set_text("6524") | |
self.port_entry.connect("insert_text", self.port_entry_updated) | |
- | |
+ | |
def port_entry_updated(self, widget, text, length, position): | |
+ """ | |
+ Prevent wrong input in the port entry. | |
+ Called each time the user changed the content of the port entry. | |
+ | |
+ Positional arguments: (see GtkEditable "insert-text" documentation) | |
+ widget -- The widget that was updated. | |
+ text -- The text to append. | |
+ length -- The length of the text in bytes, or -1. | |
+ position -- Location of the position text will be inserted at. | |
+ """ | |
if not text.isdigit(): | |
widget.emit_stop_by_name("insert_text") | |
return | |
- | |
+ | |
def set_activates_default(self, setting): | |
+ """ | |
+ Let pressing Enter in this widget activate the default widget for the | |
+ window containing this widget. | |
+ | |
+ In other words: If setting is True, pressing enter equals pressing "Connect" | |
+ | |
+ Positional Arguments: | |
+ setting -- True, to activate default widget when pressing enter. | |
+ """ | |
self.port_entry.set_activates_default(setting) | |
self.server_entry.set_activates_default(setting) | |
- | |
+ | |
def get_server(self): | |
+ """Return the hostname of the server given by the user""" | |
return self.server_entry.get_text() | |
def get_port(self): | |
+ """Return the portnumber given by the user""" | |
return int(self.port_entry.get_text()) | |
# For testing purposes: | |
diff --git a/cournal/document/document.py b/cournal/document/document.py | |
index 0f58238..e7f35c5 100644 | |
--- a/cournal/document/document.py | |
+++ b/cournal/document/document.py | |
@@ -26,7 +26,17 @@ import cairo | |
from . import Page | |
class Document: | |
+ """ | |
+ A Cournal document, having multiple pages. | |
+ """ | |
def __init__(self, pdfname): | |
+ """ | |
+ Constructor | |
+ | |
+ Positional arguments: | |
+ pdfname -- The filename of the PDF document, which will be annotated | |
+ """ | |
+ | |
self.pdfname = abspath(pdfname) | |
uri = GLib.filename_to_uri(self.pdfname, None) | |
self.pdf = Poppler.Document.new_from_file(uri, None) | |
@@ -44,17 +54,28 @@ class Document: | |
print("The document has {} pages".format(len(self.pages))) | |
def is_empty(self): | |
+ """ | |
+ Returns True, if no page of this document has a stroke on it. | |
+ Otherwise False | |
+ """ | |
for page in self.pages: | |
if len(page.layers[0].strokes) != 0: | |
return False | |
return True | |
def clear_pages(self): | |
+ """Deletes all strokes on all pages of this document""" | |
for page in self.pages: | |
for stroke in page.layers[0].strokes[:]: | |
page.delete_stroke(stroke) | |
def export_pdf(self, filename): | |
+ """ | |
+ Save the whole document (PDF+annotations) as a PDF file. | |
+ | |
+ Positional arguments: | |
+ filename -- filename of the new PDF file. | |
+ """ | |
try: | |
surface = cairo.PDFSurface(filename, 0, 0) | |
except IOError as ex: | |
@@ -74,6 +95,12 @@ class Document: | |
surface.show_page() # aka "next page" | |
def save_xoj_file(self, filename): | |
+ """ | |
+ Save the while document as a .xoj file. | |
+ | |
+ Positional arguments: | |
+ filename -- filename of the new .xoj file | |
+ """ | |
pagenum = 1 | |
try: | |
f = open_xoj(filename, "wb") | |
diff --git a/cournal/document/layer.py b/cournal/document/layer.py | |
index f55c24e..890067f 100644 | |
--- a/cournal/document/layer.py | |
+++ b/cournal/document/layer.py | |
@@ -19,11 +19,19 @@ | |
class Layer: | |
""" | |
- Stores information about a Xournal Layer. | |
- | |
- A layer contains one or more strokes. | |
+ A layer on a page, having a number and multiple strokes. | |
""" | |
def __init__(self, page, number, strokes=None): | |
+ """ | |
+ Constructor | |
+ | |
+ Positional arguments: | |
+ page -- The Page object, which is the parent of this layer. | |
+ number -- Layer number | |
+ | |
+ Keyword arguments: | |
+ strokes -- List of Stroke objects (defaults to []) | |
+ """ | |
self.number = number | |
self.page = page | |
self.strokes = strokes | |
diff --git a/cournal/document/page.py b/cournal/document/page.py | |
index 14677d9..da0bea5 100644 | |
--- a/cournal/document/page.py | |
+++ b/cournal/document/page.py | |
@@ -23,7 +23,21 @@ from . import Layer, Stroke | |
from .. import network | |
class Page: | |
+ """ | |
+ A page in a document, having a number and multiple layers. | |
+ """ | |
def __init__(self, document, pdf, number, layers=None): | |
+ """ | |
+ Constructor | |
+ | |
+ Positional arguments: | |
+ document -- The Document object, which is the parent of this page. | |
+ pdf -- A PopplerPage object | |
+ number -- Page number | |
+ | |
+ Keyword arguments: | |
+ layer -- List of Layer objects (defaults to a list of one Layer) | |
+ """ | |
self.document = document | |
self.pdf = pdf | |
self.number = number | |
@@ -35,6 +49,17 @@ class Page: | |
self.width, self.height = pdf.get_size() | |
def new_stroke(self, stroke, send_to_network=False): | |
+ """ | |
+ Add a new stroke 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 | |
+ | |
+ Keyword arguments: | |
+ send_to_network -- Set to True, to send the stroke the server | |
+ (defaults to False) | |
+ """ | |
self.layers[0].strokes.append(stroke) | |
stroke.layer = self.layers[0] | |
if self.widget: | |
@@ -43,18 +68,50 @@ class Page: | |
network.new_stroke(self.number, stroke) | |
def new_unfinished_stroke(self, color, linewidth): | |
+ """ | |
+ Add a new empty stroke, which is not sent to the server, till | |
+ finish_stroke() is called | |
+ | |
+ Positional arguments: | |
+ color -- tuple of four: (red, green, blue, opacity) | |
+ linewidth -- Line width in pt | |
+ """ | |
return Stroke(layer=self.layers[0], color=color, linewidth=linewidth, coords=[]) | |
def finish_stroke(self, stroke): | |
- #TODO: rerender portion of screen. | |
+ """ | |
+ Finish a stroke, that was created with new_unfinished_stroke() and | |
+ send it to the server, if connected. | |
+ | |
+ Positional arguments: | |
+ stroke -- The Stroke object, that was finished | |
+ """ | |
+ #TODO: rerender that part of the screen. | |
network.new_stroke(self.number, stroke) | |
def delete_stroke_with_coords(self, coords): | |
+ """ | |
+ Delete all strokes, 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) | |
def delete_stroke(self, stroke, send_to_network=False): | |
+ """ | |
+ Delete a stroke on this page and possibly send this request to the server, | |
+ if connected. | |
+ | |
+ Positional arguments: | |
+ stroke -- The Stroke object, that will be deleted. | |
+ | |
+ Keyword arguments: | |
+ send_to_network -- Set to True, to send the request for deletion the server | |
+ (defaults to False) | |
+ """ | |
self.layers[0].strokes.remove(stroke) | |
if self.widget: | |
self.widget.delete_remote_stroke(stroke) | |
@@ -62,6 +119,16 @@ class Page: | |
network.delete_stroke_with_coords(self.number, stroke.coords) | |
def get_strokes_near(self, x, y, radius): | |
+ """ | |
+ Finds strokes 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 | |
+ """ | |
for stroke in self.layers[0].strokes[:]: | |
for coord in stroke.coords: | |
s_x = coord[0] | |
diff --git a/cournal/document/stroke.py b/cournal/document/stroke.py | |
index 6193d2b..b4b0309 100644 | |
--- a/cournal/document/stroke.py | |
+++ b/cournal/document/stroke.py | |
@@ -22,7 +22,25 @@ import cairo | |
from twisted.spread import pb | |
class Stroke(pb.Copyable, pb.RemoteCopy): | |
+ """ | |
+ A pen stroke on a layer, having a color, a linewidth and a list of coordinates | |
+ | |
+ If a stroke has variable width, self.coords contains tuples of three, | |
+ else tuples of two floats. | |
+ FIXME: don't ignore the variable width | |
+ """ | |
def __init__(self, layer, 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.linewidth = linewidth | |
@@ -31,6 +49,8 @@ class Stroke(pb.Copyable, pb.RemoteCopy): | |
self.coords = [] | |
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 | |
@@ -39,6 +59,15 @@ class Stroke(pb.Copyable, pb.RemoteCopy): | |
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.color | |
@@ -60,5 +89,6 @@ class Stroke(pb.Copyable, pb.RemoteCopy): | |
context.restore() | |
return (x, y, x2, y2) | |
- | |
+ | |
+# Tell Twisted, that this class is allowed to be transmitted over the network. | |
pb.setUnjellyableForClass(Stroke, Stroke) | |
diff --git a/cournal/document/xojparser.py b/cournal/document/xojparser.py | |
index fba0a60..1c78725 100644 | |
--- a/cournal/document/xojparser.py | |
+++ b/cournal/document/xojparser.py | |
@@ -28,23 +28,36 @@ from . import Document, Stroke | |
"""A simplified parser for Xournal files using the ElementTree API.""" | |
def new_document(filename, window): | |
+ """ | |
+ Open a Xournal .xoj file | |
+ | |
+ Positional Arguments: | |
+ filename -- The filename of the Xournal document | |
+ window -- A Gtk.Window, which can be used as the parent of MessageDialogs or the like | |
+ | |
+ Return value: The new Document object | |
+ """ | |
with open_xoj(filename, "rb") as input: | |
tree = ET.parse(input) | |
pdfname = _get_background(tree) | |
document = Document(pdfname) | |
- | |
+ # We created an empty document with a PDF, now we will import the strokes: | |
return import_into_document(document, filename, window) | |
def import_into_document(document, filename, window): | |
""" | |
- Parse a Xournal .xoj file (wrapper function of ElementTree.parse()) | |
+ Parse a Xournal .xoj file and add all strokes to a given document. | |
- Note that while .xoj files are gzip-compressed xml files, this function | |
- expects decompressed input. | |
+ Note that this works on existing documents and will transfer the strokes | |
+ to the server, if connected. | |
Positional Arguments: | |
- input -- A file-like object or a string with Xournal XML content (NOT gziped) | |
+ document -- A Document object | |
+ filename -- The filename of the Xournal document | |
+ window -- A Gtk.Window, which can be used as the parent of MessageDialogs or the like | |
+ | |
+ Return value: The modified Document object, that was given as an argument. | |
""" | |
with open_xoj(filename, "rb") as input: | |
tree = ET.parse(input) | |
@@ -63,7 +76,15 @@ def import_into_document(document, filename, window): | |
return document | |
def _parse_stroke(stroke, layer): | |
- """Parse 'stroke' element""" | |
+ """ | |
+ Parse 'stroke' element | |
+ | |
+ Positional arguments: | |
+ stroke -- A ElementTree SubElement representing a stroke from a .xoj document | |
+ layer -- A Layer object. NOT from ElementTree | |
+ | |
+ Return value: A Stroke instance | |
+ """ | |
tool = stroke.attrib["tool"] | |
if tool not in ["pen", "eraser", "highlighter"]: | |
@@ -96,19 +117,28 @@ def _parse_stroke(stroke, layer): | |
return Stroke(layer, color=color, linewidth=nominalWidth, coords=coordinates) | |
def _get_background(tree): | |
- """Gets the background pdf file name of a xournal document""" | |
+ """ | |
+ Returns the background pdf file name of a Xournal document | |
+ | |
+ Positional arguments: | |
+ tree -- An ElementTree representation of a .xoj XML tree | |
+ """ | |
for bg in tree.findall("page/background"): | |
if "filename" in bg.attrib: | |
return bg.attrib["filename"] | |
def parse_color(code, default_opacity=255): | |
""" | |
- Parse a xournal color name and return a tuple of four: (r, g, b, opacity) | |
+ Parse a xournal color name. | |
- Keyword arguments: | |
+ Positional arguments: | |
code -- The color string to parse (mandatory) | |
+ | |
+ Keyword arguments: | |
default_opacity -- If 'code' does not contain opacity information, use this. | |
(default 255) | |
+ | |
+ Return value: tuple of four: (r, g, b, opacity) | |
""" | |
opacity = default_opacity | |
regex = re.compile(r"#([0-9a-fA-F]{2})([0-9a-fA-F]{2})" | |
diff --git a/cournal/mainwindow.py b/cournal/mainwindow.py | |
index f321c3e..0242dcd 100644 | |
--- a/cournal/mainwindow.py | |
+++ b/cournal/mainwindow.py | |
@@ -35,7 +35,16 @@ LINEWIDTH_NORMAL = 1.5 | |
LINEWIDTH_BIG = 8.0 | |
class MainWindow(Gtk.Window): | |
+ """ | |
+ Cournals main window | |
+ """ | |
def __init__(self, **args): | |
+ """ | |
+ Constructor. | |
+ | |
+ Keyword arguments: | |
+ **args -- Arguments passed to the Gtk.Window constructor | |
+ """ | |
Gtk.Window.__init__(self, title="Cournal", **args) | |
self.document = None | |
@@ -111,6 +120,12 @@ class MainWindow(Gtk.Window): | |
self.tool_pensize_big.connect("clicked", self.change_pen_size, LINEWIDTH_BIG) | |
def _set_document(self, document): | |
+ """ | |
+ Replace the current document (if any) with a new one. | |
+ | |
+ Positional arguments: | |
+ document -- The new Document object. | |
+ """ | |
self.document = document | |
for child in self.scrolledwindow.get_children(): | |
self.scrolledwindow.remove(child) | |
@@ -135,6 +150,9 @@ class MainWindow(Gtk.Window): | |
self.tool_pensize_big.set_sensitive(True) | |
def run_open_pdf_dialog(self, menuitem): | |
+ """ | |
+ Run an "Open PDF" dialog and create a new document with that PDF. | |
+ """ | |
dialog = Gtk.FileChooserDialog("Open File", self, Gtk.FileChooserAction.OPEN, | |
(Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT, | |
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)) | |
@@ -153,6 +171,9 @@ class MainWindow(Gtk.Window): | |
dialog.destroy() | |
def run_connection_dialog(self, menuitem): | |
+ """ | |
+ Run a "Connect to Server" dialog. | |
+ """ | |
def destroyed(widget): | |
self._connection_dialog = None | |
# Need to hold a reference, so the object does not get garbage collected | |
@@ -161,6 +182,9 @@ class MainWindow(Gtk.Window): | |
self._connection_dialog.run_nonblocking() | |
def run_import_xoj_dialog(self, menuitem): | |
+ """ | |
+ Run an "Import .xoj" dialog and import the strokes. | |
+ """ | |
dialog = Gtk.FileChooserDialog("Open File", self, Gtk.FileChooserAction.OPEN, | |
(Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT, | |
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)) | |
@@ -172,6 +196,9 @@ class MainWindow(Gtk.Window): | |
dialog.destroy() | |
def run_open_xoj_dialog(self, menuitem): | |
+ """ | |
+ Run an "Open .xoj" dialog and create a new document from a .xoj file. | |
+ """ | |
dialog = Gtk.FileChooserDialog("Open File", self, Gtk.FileChooserAction.OPEN, | |
(Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT, | |
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)) | |
@@ -190,12 +217,21 @@ class MainWindow(Gtk.Window): | |
dialog.destroy() | |
def save(self, menuitem): | |
+ """ | |
+ Save document to the last known filename or ask the user for a location. | |
+ | |
+ Positional arguments: | |
+ menuitem -- The menu item, that triggered this function | |
+ """ | |
if self.last_filename: | |
self.document.save_xoj_file(self.last_filename) | |
else: | |
self.run_save_as_dialog(menuitem) | |
def run_save_as_dialog(self, menuitem): | |
+ """ | |
+ Run a "Save as" dialog and save the document to a .xoj file | |
+ """ | |
dialog = Gtk.FileChooserDialog("Save File As", self, Gtk.FileChooserAction.SAVE, | |
(Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT, | |
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)) | |
@@ -209,6 +245,9 @@ class MainWindow(Gtk.Window): | |
dialog.destroy() | |
def run_export_pdf_dialog(self, menuitem): | |
+ """ | |
+ Run an "Export" dialog and save the document to a PDF file. | |
+ """ | |
dialog = Gtk.FileChooserDialog("Export PDF", self, Gtk.FileChooserAction.SAVE, | |
(Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT, | |
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)) | |
@@ -221,6 +260,9 @@ class MainWindow(Gtk.Window): | |
dialog.destroy() | |
def run_about_dialog(self, menuitem): | |
+ """ | |
+ Run an "About" dialog. | |
+ """ | |
def destroyed(widget): | |
self._about_dialog = None | |
# Need to hold a reference, so the object does not get garbage collected | |
@@ -229,6 +271,13 @@ class MainWindow(Gtk.Window): | |
self._about_dialog.run_nonblocking() | |
def run_error_dialog(self, first, second): | |
+ """ | |
+ Display an error dialog | |
+ | |
+ Positional arguments: | |
+ first -- Primary text of the message | |
+ second -- Secondary text of the message | |
+ """ | |
print("Unable to open PDF file:", second) | |
message = Gtk.MessageDialog(self, (Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT), Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, first) | |
message.format_secondary_text(second) | |
@@ -236,8 +285,14 @@ class MainWindow(Gtk.Window): | |
message.connect("response", lambda _,x: message.destroy()) | |
message.show() | |
- def change_pen_color(self, menuitem): | |
- color = menuitem.get_rgba() | |
+ def change_pen_color(self, colorbutton): | |
+ """ | |
+ Change the pen 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) | |
@@ -246,13 +301,23 @@ class MainWindow(Gtk.Window): | |
pen.color = red, green, blue, opacity | |
def change_pen_size(self, menuitem, linewidth): | |
+ """ | |
+ Change the pen 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 | |
def zoom_in(self, menuitem): | |
+ """Magnify document""" | |
self.layout.set_zoomlevel(change=0.2) | |
def zoom_out(self, menuitem): | |
+ """Zoom out""" | |
self.layout.set_zoomlevel(change=-0.2) | |
def zoom_100(self, menuitem): | |
+ """Reset Zoom""" | |
self.layout.set_zoomlevel(1) | |
diff --git a/cournal/network.py b/cournal/network.py | |
index 150f5a8..43c269f 100644 | |
--- a/cournal/network.py | |
+++ b/cournal/network.py | |
@@ -30,20 +30,41 @@ DEBUGLEVEL = 3 | |
USERNAME = "test" | |
PASSWORD = "testpw" | |
-class Network(pb.Referenceable): | |
+""" | |
+Network communication via one instance of the _Network() class. | |
+""" | |
+ | |
+class _Network(pb.Referenceable): | |
+ """ | |
+ Network communication with Twisted Perspective Broker (RPC-like) | |
+ """ | |
def __init__(self): | |
+ """Constructor""" | |
pb.Referenceable.__init__(self) | |
self.document = None | |
self.is_connected = False | |
def set_document(self, document): | |
+ """ | |
+ Associate this object with a document | |
+ | |
+ Positional arguments: | |
+ document -- The Document object | |
+ """ | |
self.document = document | |
- def connect(self, server, port): | |
+ def connect(self, hostname, port): | |
+ """ | |
+ Connect to a server | |
+ | |
+ Positional arguments: | |
+ hostname -- The hostname of the server | |
+ port -- The port to connect to | |
+ """ | |
if self.document is None: | |
return | |
self.factory = pb.PBClientFactory() | |
- reactor.connectTCP(server, port, self.factory) | |
+ reactor.connectTCP(hostname, port, self.factory) | |
d = self.factory.login(credentials.UsernamePassword(USERNAME, PASSWORD), | |
client=self) | |
@@ -51,6 +72,12 @@ class Network(pb.Referenceable): | |
return d | |
def connected(self, perspective): | |
+ """ | |
+ Called, when the connection succeeded. Join a document now | |
+ | |
+ Positional arguments: | |
+ perspective -- a reference to our user object | |
+ """ | |
debug(1, "Connected") | |
# This perspective is a reference to our User object. Save a reference | |
# to it here, otherwise it will get garbage collected after this call, | |
@@ -63,41 +90,77 @@ class Network(pb.Referenceable): | |
return d | |
def connection_failed(self, reason): | |
+ """ | |
+ Called, when the connection could not be established. | |
+ | |
+ Positional arguments: | |
+ reason -- A twisted Failure object with the reason the connection failed | |
+ """ | |
debug(0, "Connection failed due to:", reason.getErrorMessage()) | |
self.is_connected = False | |
return reason | |
def got_server_document(self, server_document, name): | |
+ """ | |
+ Called, when the server sent a reference to the remote document we requested | |
+ | |
+ Positional arguments: | |
+ server_document -- remote reference to the document we are editing | |
+ name -- Name of the document | |
+ """ | |
debug(2, "Started editing", name) | |
self.server_document = server_document | |
def remote_new_stroke(self, pagenum, stroke): | |
- """Called by the server""" | |
+ """ | |
+ Called by the server, to inform us about a new stroke | |
+ | |
+ Positional arguments: | |
+ pagenum -- On which page shall we add the stroke | |
+ stroke -- The received Stroke object | |
+ """ | |
if self.document and pagenum < len(self.document.pages): | |
self.document.pages[pagenum].new_stroke(stroke) | |
def new_stroke(self, pagenum, stroke): | |
- """Called by local code""" | |
+ """ | |
+ Called by local code to send a new stroke to the server | |
+ | |
+ Positional arguments: | |
+ pagenum -- On which page the stroke was added | |
+ stroke -- The Stroke object to send | |
+ """ | |
if self.is_connected: | |
self.server_document.callRemote("new_stroke", pagenum, stroke) | |
def remote_delete_stroke_with_coords(self, pagenum, coords): | |
- """Called by the server""" | |
+ """ | |
+ Called by the server, when a remote user deleted a stroke | |
+ | |
+ Positional arguments: | |
+ pagenum -- On which page the stroke was deleted | |
+ coords -- The list of coordinates identifying a stroke | |
+ """ | |
if self.document and pagenum < len(self.document.pages): | |
self.document.pages[pagenum].delete_stroke_with_coords(coords) | |
def delete_stroke_with_coords(self, pagenum, coords): | |
- """Called by local code""" | |
+ """ | |
+ 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 | |
+ | |
+ """ | |
if self.is_connected: | |
self.server_document.callRemote("delete_stroke_with_coords", pagenum, coords) | |
- def shutdown(self, result): | |
- reactor.stop() | |
- | |
-# This is, what will be exported and included in other files: | |
-network = Network() | |
+# This is, what will be exported and included by other modules: | |
+network = _Network() | |
def debug(level, *args): | |
+ """Helper function for debug output""" | |
if level <= DEBUGLEVEL: | |
print(*args) | |
diff --git a/cournal/viewer/layout.py b/cournal/viewer/layout.py | |
index ef6a8ec..fe927db 100644 | |
--- a/cournal/viewer/layout.py | |
+++ b/cournal/viewer/layout.py | |
@@ -24,23 +24,40 @@ from . import PageWidget | |
PAGE_SEPARATOR = 10 # px | |
class Layout(Gtk.Layout): | |
+ """ | |
+ The main pdf viewer/annotation widget containing one or more PageWidgets. | |
+ """ | |
+ | |
def __init__(self, document, **args): | |
+ """ | |
+ Constructor | |
+ | |
+ Positional arguments: | |
+ document -- A Document object, containing the pages we want to render. | |
+ | |
+ Keyword arguments: | |
+ **args -- Arguments passed to the Gtk.Layout constructor | |
+ """ | |
Gtk.Layout.__init__(self, **args) | |
self.document = document | |
self.children = [] | |
self.zoomlevel = 1 | |
- #color = | |
- #print(color) | |
- self.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(79/255,78/255,77/255,1)) | |
+ # The background color is visible between the PageWidgets | |
+ self.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(79/255, 78/255, 77/255, 1)) | |
+ | |
for page in self.document.pages: | |
self.children.append(PageWidget(page)) | |
self.put(self.children[-1], 0, 0) | |
- def get_height_for_width(self, width): | |
- return width * self.document.height / self.document.width | |
def do_size_allocate(self, allocation): | |
+ """ | |
+ Called, when the Layout is about to be resized. Resizes all children. | |
+ | |
+ Positional arguments: | |
+ allocation -- Gtk.Allocation object (containing the new height and width) | |
+ """ | |
self.set_allocation(allocation) | |
new_width = allocation.width*self.zoomlevel | |
@@ -48,7 +65,6 @@ class Layout(Gtk.Layout): | |
adjustment = self.get_vadjustment() | |
if old_width != new_width: | |
- #print("Ly: size_allocate") | |
new_height = 0 | |
for child in self.children: | |
new_height += self.allocate_child(child, 0, new_height, new_width) | |
@@ -60,6 +76,7 @@ class Layout(Gtk.Layout): | |
else: | |
new_height = old_height | |
self.set_size(new_width, new_height) | |
+ | |
# Shamelessly copied from the GtkLayout source code: | |
if self.get_realized(): | |
self.get_window().move_resize(allocation.x, allocation.y, | |
@@ -67,15 +84,33 @@ class Layout(Gtk.Layout): | |
self.get_bin_window().resize(new_width, max(allocation.height, new_height)) | |
def allocate_child(self, child, x, y, width): | |
+ """ | |
+ Allocate space for a child widget | |
+ | |
+ Positional arguments: | |
+ child -- The child widget (likely a PageWidget) | |
+ x -- The x coordinate of the child | |
+ y -- The y coordinate of the child | |
+ width -- The width of the child | |
+ | |
+ Return value: height of the child widget | |
+ """ | |
r = Gdk.Rectangle() | |
r.x = x | |
r.y = y | |
r.width = width | |
- r.height = child.do_get_preferred_height_for_width(width)[0] | |
+ r.height = child.get_preferred_height_for_width(width)[0] | |
child.size_allocate(r) | |
return r.height | |
def set_zoomlevel(self, absolute=None, change=None): | |
+ """ | |
+ Set zoomlevel of all child widgets. | |
+ | |
+ Keyword arguments: | |
+ absolute -- Set zoomlevel to absolute level (should be between 0.2 and 3.0) | |
+ change -- Alter zoomlevel by this value. It's ignored, if 'absolute' was given. | |
+ """ | |
if absolute: | |
self.zoomlevel = absolute | |
elif change: | |
diff --git a/cournal/viewer/pagewidget.py b/cournal/viewer/pagewidget.py | |
index 234973c..10d6aba 100644 | |
--- a/cournal/viewer/pagewidget.py | |
+++ b/cournal/viewer/pagewidget.py | |
@@ -23,7 +23,21 @@ import cairo | |
from .tools import pen, eraser | |
class PageWidget(Gtk.DrawingArea): | |
+ """ | |
+ A widget displaying a PDF page and its annotations | |
+ """ | |
+ | |
def __init__(self, page, **args): | |
+ """ | |
+ Constructor | |
+ | |
+ Positional arguments: | |
+ page -- The Page object to display | |
+ | |
+ Keyword arguments: | |
+ **args -- Arguments passed to the Gtk.DrawingArea constructor | |
+ """ | |
+ | |
Gtk.DrawingArea.__init__(self, **args) | |
self.page = page | |
@@ -47,6 +61,12 @@ class PageWidget(Gtk.DrawingArea): | |
self.connect("button-release-event", self.release) | |
def set_cursor(self, widget): | |
+ """ | |
+ Set the cursor to a black square indicating the pen tip | |
+ | |
+ Keyword arguments: | |
+ widget -- The widget to set the cursor for | |
+ """ | |
width, height = 4, 4 | |
s = cairo.ImageSurface(cairo.FORMAT_A1, width, height) | |
@@ -57,25 +77,42 @@ class PageWidget(Gtk.DrawingArea): | |
cursor_pixbuf = Gdk.pixbuf_get_from_surface(s, 0, 0, width, height) | |
cursor = Gdk.Cursor.new_from_pixbuf(Gdk.Display.get_default(), | |
cursor_pixbuf, width/2, height/2) | |
- self.get_window().set_cursor(cursor) | |
+ widget.get_window().set_cursor(cursor) | |
def do_get_request_mode(self): | |
+ """Tell Gtk that we like to calculate our height given a width.""" | |
return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH | |
def do_get_preferred_height_for_width(self, width): | |
- #print("get_preferred_height_for_width(", width, ")") | |
+ """ | |
+ Tell Gtk what height we would like to have occupy, if it gives us a width | |
+ | |
+ Positional arguments: | |
+ width -- width given by Gtk | |
+ """ | |
aspect_ratio = self.page.width / self.page.height | |
return (width / aspect_ratio, width / aspect_ratio) | |
def on_size_allocate(self, widget, alloc): | |
- #print("size_allocate", alloc.width, alloc.height) | |
+ """ | |
+ Called, when the widget was resized. | |
+ | |
+ Positional arguments: | |
+ widget -- The resized widget | |
+ alloc -- A Gtk.Allocation object | |
+ """ | |
self.set_allocation(alloc) | |
self.widget_width = alloc.width | |
self.widget_height = alloc.height | |
def draw(self, widget, context): | |
- #print("draw") | |
+ """ | |
+ Draw the widget (the PDF, all strokes and the background). Called by Gtk. | |
+ Positional arguments: | |
+ widget -- The widget to redraw | |
+ context -- A Cairo context to draw on | |
+ """ | |
scaling = self.widget_width / self.page.width | |
# Check if the page has already been rendered in the correct size | |
@@ -107,7 +144,13 @@ class PageWidget(Gtk.DrawingArea): | |
context.paint() | |
def press(self, widget, event): | |
- #print("Press " + str((event.x,event.y))) | |
+ """ | |
+ Mouse down event. Select a tool depending on the mouse button and call it. | |
+ | |
+ Positional arguments: | |
+ widget -- The widget, which triggered the event | |
+ event -- The Gdk.Event, which stores the location of the pointer | |
+ """ | |
if event.button == 1: | |
self.active_tool = pen | |
elif event.button == 3 or event.button == 2: | |
@@ -117,16 +160,32 @@ class PageWidget(Gtk.DrawingArea): | |
self.active_tool.press(self, event) | |
def motion(self, widget, event): | |
- #print("\rMotion "+str((event.x,event.y))+" ", end="") | |
+ """ | |
+ Mouse motion event. Call currently active tool, if any. | |
+ | |
+ Positional arguments: see press() | |
+ """ | |
if self.active_tool is not None: | |
self.active_tool.motion(self, event) | |
def release(self, widget, event): | |
+ """ | |
+ Mouse release event. Call currently active tool, if any. | |
+ | |
+ Positional arguments: see press() | |
+ """ | |
if self.active_tool is not None: | |
self.active_tool.release(self, event) | |
self.active_tool = None | |
def draw_remote_stroke(self, stroke): | |
+ """ | |
+ Draw a single stroke on the widget. | |
+ Meant do be called by networking code, when a remote user drew a stroke. | |
+ | |
+ Positional arguments: | |
+ stroke -- The Stroke object, which is to be drawn. | |
+ """ | |
if self.backbuffer: | |
scaling = self.widget_width / self.page.width | |
context = cairo.Context(self.backbuffer) | |
@@ -142,6 +201,13 @@ class PageWidget(Gtk.DrawingArea): | |
self.get_window().invalidate_rect(update_rect, False) | |
def delete_remote_stroke(self, stroke): | |
+ """ | |
+ 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. | |
+ | |
+ Positional arguments: | |
+ stroke -- The Stroke object, which was deleted. | |
+ """ | |
if self.backbuffer: | |
self.backbuffer_valid = False | |
self.get_window().invalidate_rect(None, False) | |
diff --git a/cournal/viewer/tools/eraser.py b/cournal/viewer/tools/eraser.py | |
index f790287..94c3186 100644 | |
--- a/cournal/viewer/tools/eraser.py | |
+++ b/cournal/viewer/tools/eraser.py | |
@@ -17,18 +17,48 @@ | |
# You should have received a copy of the GNU General Public License | |
# along with Cournal. If not, see <http://www.gnu.org/licenses/>. | |
+""" | |
+An eraser tool, that this deletes the complete stroke. | |
+""" | |
+ | |
THICKNESS = 6 # pt | |
def press(widget, event): | |
+ """ | |
+ Mouse down event. Delete all strokes 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) | |
def motion(widget, event): | |
+ """ | |
+ Mouse motion event. Delete all strokes near the pointer location. | |
+ | |
+ Positional arguments: see press() | |
+ """ | |
_delete_strokes_near(widget, event.x, event.y) | |
def release(widget, event): | |
+ """ | |
+ Mouse release event. Don't do anything, as the last motion event had the same | |
+ location. | |
+ | |
+ Positional arguments: see press() | |
+ """ | |
pass | |
def _delete_strokes_near(widget, x, y): | |
+ """ | |
+ Delete all strokes 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 (!)) | |
+ """ | |
scaling = widget.page.width / widget.get_allocation().width | |
x *= scaling | |
y *= scaling | |
diff --git a/cournal/viewer/tools/pen.py b/cournal/viewer/tools/pen.py | |
index bf66141..3075642 100644 | |
--- a/cournal/viewer/tools/pen.py | |
+++ b/cournal/viewer/tools/pen.py | |
@@ -20,6 +20,10 @@ | |
import cairo | |
from gi.repository import Gdk | |
+""" | |
+A pen tool. Draws a stroke with a certain color and size. | |
+""" | |
+ | |
_last_point = None | |
_current_coords = None | |
_current_stroke = None | |
@@ -27,6 +31,13 @@ linewidth = 1.5 | |
color = (0,0,128,255) | |
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 _last_point, _current_coords, _current_stroke, linewidth, color | |
actualWidth = widget.get_allocation().width | |
@@ -39,8 +50,12 @@ def press(widget, event): | |
widget.page.layers[0].strokes.append(_current_stroke) | |
def motion(widget, event): | |
+ """ | |
+ Mouse motion event. Draw a line from the last to the new pointer location. | |
+ | |
+ Positional arguments: see press() | |
+ """ | |
global _last_point, _current_coords, _current_stroke | |
- #print("\rMotion "+str((event.x,event.y))+" ", end="") | |
r, g, b, opacity = color | |
actualWidth = widget.get_allocation().width | |
@@ -67,6 +82,14 @@ def motion(widget, event): | |
_current_coords.append([event.x*widget.page.width/actualWidth, event.y*widget.page.width/actualWidth]) | |
def release(widget, event): | |
+ """ | |
+ Mouse release event. Inform the corresponding Page instance, that the strokes | |
+ is finished. | |
+ | |
+ This will cause the stroke to be sent to the server, if it is connected. | |
+ | |
+ Positional arguments: see press() | |
+ """ | |
global _last_point, _current_coords, _current_stroke | |
widget.page.finish_stroke(_current_stroke) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment