Skip to content

Instantly share code, notes, and snippets.

@flyser
Forked from anonymous/stdin.txt
Created June 13, 2012 22:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save flyser/2926964 to your computer and use it in GitHub Desktop.
Save flyser/2926964 to your computer and use it in GitHub Desktop.
stdin
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