Skip to content

Commit

Permalink
Editable annotations and saveable documents
Browse files Browse the repository at this point in the history
  • Loading branch information
Cimbali committed Oct 4, 2021
1 parent d0f468c commit 5b814fd
Show file tree
Hide file tree
Showing 4 changed files with 322 additions and 28 deletions.
96 changes: 91 additions & 5 deletions pympress/document.py
Expand Up @@ -291,11 +291,8 @@ def __init__(self, page, number, parent):

# Read annotations, in particular those that indicate media
for annotation in self.page.get_annot_mapping():
content = annotation.annot.get_contents()
if content:
self.annotations.append(content)

annot_type = annotation.annot.get_annot_type()

if annot_type == Poppler.AnnotType.LINK:
# just an Annot, not subclassed -- probably redundant with links
continue
Expand Down Expand Up @@ -343,7 +340,11 @@ def __init__(self, page, number, parent):
action = Link.build_closure(fileopen, filename)
elif annot_type in {Poppler.AnnotType.TEXT, Poppler.AnnotType.POPUP,
Poppler.AnnotType.FREE_TEXT}:
# text-only annotations, hide them from screen
# text-only annotations, hide them from screen and show them in annotations popup
content = annotation.annot.get_contents()
if content:
self.annotations.append(annotation.annot)

self.page.remove_annot(annotation.annot)
continue
elif annot_type in {Poppler.AnnotType.STRIKE_OUT, Poppler.AnnotType.HIGHLIGHT,
Expand Down Expand Up @@ -576,6 +577,57 @@ def get_annotations(self):
return self.annotations


def new_annotation(self, pos, rect=None):
""" Add an annotation to this page
Args:
pos (`int`): The position in the list of annotations in which to insert this annotation
rect (:class:`~Poppler.Rectangle`): A rectangle for the position of this annotation
Returns:
:class:`~Poppler.Annot`: A new annotation on this page
"""
if pos < 0:
pos = 0
if pos > len(self.annotations):
pos = len(self.annotations)

if rect is None:
rect = Poppler.Rectangle()
rect.x1 = self.pw - 20
rect.x2 = rect.x1 + 20
rect.y2 = self.ph - len(self.annotations) * 20
rect.y1 = rect.y2 - 20

new_annot = Poppler.AnnotText.new(self.parent.doc, rect)
new_annot.set_icon(Poppler.ANNOT_TEXT_ICON_NOTE)
self.annotations.insert(pos, new_annot)
self.parent.made_changes()
return new_annot


def set_annotation(self, pos, value):
""" Add an annotation to this page
Args:
pos (`int`): The number of the annotation
value (`str`): The new contents of the annotation
"""
rect = self.annotations[pos].get_rectangle()
self.remove_annotation(pos)
self.new_annotation(pos, rect).set_contents(value)


def remove_annotation(self, pos):
""" Add an annotation to this page
Args:
pos (`int`): The number of the annotation
"""
self.parent.made_changes()
del self.annotations[pos]


def get_media(self):
""" Get the list of medias this page might want to play.
Expand Down Expand Up @@ -660,6 +712,8 @@ class Document(object):
page_labels = []
#: `bool` indicating whether the second half of pages are in fact notes pages
notes_after = False
#: `bool` indicating whether there were modifications to the document
changes = False

#: callback, to be connected to :func:`~pympress.extras.Media.play`
play_media = lambda *args: None
Expand All @@ -680,6 +734,7 @@ def __init__(self, builder, pop_doc, uri):
# Setup PDF file
self.uri = uri
self.doc = pop_doc
self.changes = False

# Pages number
if pop_doc is not None:
Expand Down Expand Up @@ -785,6 +840,37 @@ def create(builder, uri):
return doc


def made_changes(self):
""" Notify the document that some changes were made (e.g. annotations edited)
"""
self.changes = True


def has_changes(self):
""" Return whether that some changes were made (e.g. annotations edited)
"""
return self.changes


def save_changes(self, dest_uri=None):
""" Save the changes
Args:
dest_uri (`str` or `None`): The URI where to save the file, or None to save in-place
"""
for page in self.pages_cache.values():
for annot in page.get_annotations():
page.page.add_annot(annot)

self.doc.save(self.uri if dest_uri is None else dest_uri)

for page in self.pages_cache.values():
for annot in page.get_annotations():
page.page.remove_annot(annot)

self.changes = False


def guess_notes(self, horizontal, vertical, current_page=0):
""" Get our best guess for the document mode.
Expand Down
134 changes: 124 additions & 10 deletions pympress/extras.py
Expand Up @@ -32,7 +32,7 @@
import gi
import cairo
gi.require_version('Gtk', '3.0')
from gi.repository import Gdk, GLib, Gio
from gi.repository import Gtk, Gdk, GLib, Gio

import mimetypes
import functools
Expand All @@ -44,24 +44,39 @@
class Annotations(object):
""" Widget displaying a PDF’s text annotations.
"""
#: The containing :class:`~Gtk.TextView` widget for the annotations
annotations_textview = None
#: :class:`~Gtk.ScrolledWindow` making the annotations list scroll if it's too long
scrolled_window = None
#: The containing :class:`~Gtk.TreeView` widget for the annotations
annotations_treeview = None
#: The containing :class:`~Gtk.ListStore` storing the annotations to be displayed
annotations_liststore = None
#: The :class:`~Gtk.Entry` in which we are currently editing an annotation, or None
editing = None

new_doc_annotation = lambda *args: None
set_doc_annotation = lambda *args: None
remove_doc_annotation = lambda *args: None

def __init__(self, builder):
super(Annotations, self).__init__()
builder.load_widgets(self)
builder.setup_actions({
'add-annotation': dict(activate=self.add_annotation),
'remove-annotation': dict(activate=self.remove_annotation),
})


def add_annotations(self, annotations):
def load_annotations(self, annot_page):
""" Add annotations to be displayed (typically on going to a new slide).
Args:
annotations (`list`): A list of strings, that are the annotations to be displayed
annot_page (:class:`~pympress.document.Page`): The page object that contains the annotations
"""
buf = self.annotations_textview.get_buffer()
buf.set_text('\n'.join(annotations))
self.annotations_liststore.clear()
for annot in annot_page.get_annotations():
self.annotations_liststore.append([annot.get_contents()])

self.new_doc_annotation = annot_page.new_annotation
self.set_doc_annotation = annot_page.set_annotation
self.remove_doc_annotation = annot_page.remove_annotation


def on_scroll(self, widget, event):
Expand All @@ -74,7 +89,7 @@ def on_scroll(self, widget, event):
Returns:
`bool`: whether the event was consumed
"""
adj = self.scrolled_window.get_vadjustment()
adj = self.annotations_treeview.get_vadjustment()
if event.direction == Gdk.ScrollDirection.UP:
adj.set_value(adj.get_value() - adj.get_step_increment())
elif event.direction == Gdk.ScrollDirection.DOWN:
Expand All @@ -84,6 +99,105 @@ def on_scroll(self, widget, event):
return True


def try_cancel(self):
""" Try to cancel editing
Returns:
`bool`: whether editing was enabled and thus canceled
"""
if self.editing is None:
return False

rows = self.annotations_treeview.get_selection().get_selected_rows()[1]
if rows:
self.annotations_treeview.set_cursor(rows[0], None, False)
return True


def key_event(self, widget, event):
""" Handle a key (press/release) event.
Needed to forward events directly to the :class:`~Gtk.Entry`, bypassing the global action accelerators.
Args:
widget (:class:`~Gtk.Widget`): the widget which has received the event.
event (:class:`~Gdk.Event`): the GTK event.
Returns:
`bool`: whether the event was consumed
"""
if self.editing is None:
return False
elif event.get_event_type() == Gdk.EventType.KEY_PRESS:
return self.editing.do_key_press_event(self.editing, event)
elif event.get_event_type() == Gdk.EventType.KEY_RELEASE:
return self.editing.do_key_release_event(self.editing, event)
return False


def editing_started(self, cell_renderer, widget, entry_number):
""" Handle edit start
Args:
cell_renderer (:class:`~Gtk.CellRenderer`): The renderer which received the signal
widget (:class:`~Gtk.CellEditable`): the Gtk entry editing the annotation entry
entry_number (`str`): the string representation of the path identifying the edited cell
"""
self.editing = widget


def editing_validated(self, cell_renderer, entry_number, new_content):
""" Handle successful edit: store the new cell value in the model and the document
Args:
cell_renderer (:class:`~Gtk.CellRenderer`): The renderer which received the signal
entry_number (`str`): the string representation of the path identifying the edited cell
new_content (`str`): the new value of the edited cell
"""
row = self.annotations_liststore.get_iter(Gtk.TreePath.new_from_string(entry_number))
self.annotations_liststore.set_row(row, [new_content])
self.set_doc_annotation(int(entry_number), new_content)
self.editing_finished(cell_renderer)


def editing_finished(self, cell_renderer):
""" Handle the end of editing
Args:
cell_renderer (:class:`~Gtk.CellRenderer`): The renderer which received the signal
"""
self.editing = None


def add_annotation(self, gaction, param=None):
""" Add an annotation to the the page’s annotation list
Args:
gaction (:class:`~Gio.Action`): the action triggering the call, which identifies which backend
param (:class:`~GLib.Variant`): an optional parameter
"""
path = self.annotations_liststore.get_path(self.annotations_liststore.append())
self.annotations_treeview.set_cursor(path, self.annotations_treeview.get_columns()[0], True)
self.new_doc_annotation(path.get_indices()[0])
return True


def remove_annotation(self, gaction, param=None):
""" Remove an annotation to the from the page’s annotation list
Args:
gaction (:class:`~Gio.Action`): the action triggering the call, which identifies which backend
param (:class:`~GLib.Variant`): an optional parameter
"""
rows = self.annotations_treeview.get_selection().get_selected_rows()[1]
if not rows:
return False
self.annotations_liststore.remove(self.annotations_liststore.get_iter(rows[0]))
self.remove_doc_annotation(rows[0].get_indices()[0])
return True



class Media(object):
""" Class managing statically the medias and media player backends, to enable play/pause callbacks.
Expand Down

0 comments on commit 5b814fd

Please sign in to comment.