From ec197f082909c04ca6f35fea7da961ab49be37c2 Mon Sep 17 00:00:00 2001 From: BasioMeusPuga Date: Thu, 19 Apr 2018 15:19:20 +0530 Subject: [PATCH] Annotation saving, loading, and deletion --- lector/annotations.py | 1 + lector/contentwidgets.py | 167 ++++++++++++++++++++++++++++++++++----- lector/database.py | 2 +- lector/sorter.py | 7 +- lector/threaded.py | 3 +- lector/widgets.py | 38 ++++++--- 6 files changed, 182 insertions(+), 36 deletions(-) diff --git a/lector/annotations.py b/lector/annotations.py index 172d54b..c5ac056 100644 --- a/lector/annotations.py +++ b/lector/annotations.py @@ -234,6 +234,7 @@ class AnnotationsUI(QtWidgets.QDialog, annotationswindow.Ui_Dialog): self.current_annotation = { 'name': annotation_name, + 'applicable_to': 'text', 'type': 'text_markup', 'components': annotation_components} diff --git a/lector/contentwidgets.py b/lector/contentwidgets.py index 8d712c1..ad76980 100644 --- a/lector/contentwidgets.py +++ b/lector/contentwidgets.py @@ -43,6 +43,8 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView): self.thread = None + self.annotation_dict = self.parent.metadata['annotations'] + self.filepath = filepath self.filetype = os.path.splitext(self.filepath)[1][1:] @@ -318,6 +320,9 @@ class PliantQGraphicsView(QtWidgets.QGraphicsView): # In case the program is closed when a contentView is fullscreened self.main_window.closeEvent() + def toggle_annotation_mode(self): + pass + class PliantQTextBrowser(QtWidgets.QTextBrowser): def __init__(self, main_window, parent=None): @@ -330,6 +335,7 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser): self.annotation_mode = False self.annotator = AnnotationPlacement() self.current_annotation = None + self.annotation_dict = self.parent.metadata['annotations'] self.common_functions = PliantWidgetsCommonFunctions( self, self.main_window) @@ -396,46 +402,82 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser): self.parent.metadata['position']['cursor_position'] = cursor_position def toggle_annotation_mode(self): - self.annotation_mode = True - self.viewport().setCursor(QtCore.Qt.IBeamCursor) - self.parent.annotationDock.setWindowOpacity(.40) + if self.annotation_mode: + self.annotation_mode = False + self.viewport().setCursor(QtCore.Qt.ArrowCursor) + self.parent.annotationDock.setWindowOpacity(.95) - selected_index = self.parent.annotationListView.currentIndex() - self.current_annotation = self.parent.annotationModel.data( - selected_index, QtCore.Qt.UserRole) - print('Current annotation: ' + self.current_annotation['name']) + self.current_annotation = None + self.parent.annotationListView.clearSelection() + + else: + self.annotation_mode = True + self.viewport().setCursor(QtCore.Qt.IBeamCursor) + self.parent.annotationDock.setWindowOpacity(.40) + + selected_index = self.parent.annotationListView.currentIndex() + self.current_annotation = self.parent.annotationModel.data( + selected_index, QtCore.Qt.UserRole) + print('Current annotation: ' + self.current_annotation['name']) def mouseReleaseEvent(self, event): # This takes care of annotation placement + # and addition to the list that holds all current annotations if not self.current_annotation: QtWidgets.QTextBrowser.mouseReleaseEvent(self, event) return - self.annotator.set_current_annotation( - 'text_markup', self.current_annotation['components']) - + current_chapter = self.parent.metadata['position']['current_chapter'] cursor = self.textCursor() + cursor_start = cursor.selectionStart() + cursor_end = cursor.selectionEnd() + annotation_type = 'text_markup' + applicable_to = 'text' + annotation_components = self.current_annotation['components'] + + self.annotator.set_current_annotation( + annotation_type, annotation_components) + new_cursor = self.annotator.format_text( - cursor, cursor.selectionStart(), cursor.selectionEnd()) + cursor, cursor_start, cursor_end) self.setTextCursor(new_cursor) - self.annotation_mode = False - self.viewport().setCursor(QtCore.Qt.ArrowCursor) - self.current_annotation = None - self.parent.annotationListView.clearSelection() - self.parent.annotationDock.setWindowOpacity(.95) + # TODO + # Maybe use annotation name for a consolidated annotation list + + this_annotation = { + 'name': self.current_annotation['name'], + 'applicable_to': applicable_to, + 'type': annotation_type, + 'cursor': (cursor_start, cursor_end), + 'components': annotation_components, + 'note': None} + + try: + self.annotation_dict[current_chapter].append(this_annotation) + except KeyError: + self.annotation_dict[current_chapter] = [] + self.annotation_dict[current_chapter].append(this_annotation) + + self.toggle_annotation_mode() def generate_textbrowser_context_menu(self, position): selection = self.textCursor().selection() selection = selection.toPlainText() + current_chapter = self.parent.metadata['position']['current_chapter'] + cursor_at_mouse = self.cursorForPosition(position) + annotation_is_present = self.common_functions.check_annotation_position( + 'text', current_chapter, cursor_at_mouse.position()) + contextMenu = QtWidgets.QMenu() # The following cannot be None because a click # outside the menu means that the action variable is None. defineAction = fsToggleAction = dfToggleAction = 'Caesar si viveret, ad remum dareris' - searchAction = searchGoogleAction = 'TODO Insert Latin Joke' searchWikipediaAction = searchYoutubeAction = 'Does anyone know something funny in Latin?' + searchAction = searchGoogleAction = bookmarksToggleAction = 'TODO Insert Latin Joke' + deleteAnnotationAction = editAnnotationNoteAction = 'Latin quote 2. Electric Boogaloo.' if selection and selection != '': first_selected_word = selection.split()[0] @@ -462,6 +504,17 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser): QtGui.QIcon(':/images/Youtube.png'), 'Youtube') + if annotation_is_present: + annotationsubMenu = contextMenu.addMenu('Annotation') + annotationsubMenu.setIcon(self.main_window.QImageFactory.get_image('annotate')) + + editAnnotationNoteAction = annotationsubMenu.addAction( + self.main_window.QImageFactory.get_image('edit-rename'), + self._translate('PliantQTextBrowser', 'Edit note')) + deleteAnnotationAction = annotationsubMenu.addAction( + self.main_window.QImageFactory.get_image('remove'), + self._translate('PliantQTextBrowser', 'Delete annotation')) + if self.parent.is_fullscreen: fsToggleAction = contextMenu.addAction( self.main_window.QImageFactory.get_image('view-fullscreen'), @@ -478,7 +531,6 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser): self.main_window.QImageFactory.get_image('visibility'), distraction_free_prompt) - bookmarksToggleAction = 'Latin quote 2. Electric Boogaloo.' if not self.main_window.settings['show_bars'] or self.parent.is_fullscreen: bookmarksToggleAction = contextMenu.addAction( self.main_window.QImageFactory.get_image('bookmarks'), @@ -490,6 +542,7 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser): if action == defineAction: self.main_window.definitionDialog.find_definition(selection) + if action == searchAction: self.main_window.bookToolBar.searchBar.setText(selection) self.main_window.bookToolBar.searchBar.setFocus() @@ -502,8 +555,14 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser): if action == searchYoutubeAction: webbrowser.open_new_tab( f'https://www.youtube.com/results?search_query={selection}') + + if action == deleteAnnotationAction: + self.common_functions.delete_annotation( + 'text', current_chapter, cursor_at_mouse.position()) + if action == bookmarksToggleAction: self.parent.toggle_bookmarks() + if action == fsToggleAction: self.parent.exit_fullscreen() if action == dfToggleAction: @@ -588,8 +647,76 @@ class PliantWidgetsCommonFunctions: if not was_button_pressed: self.pw.ignore_wheel_event = True - if not self.are_we_doing_images_only: - self.pw.record_position() + def load_annotations(self, chapter): + try: + chapter_annotations = self.pw.annotation_dict[chapter] + except KeyError: + return + + for i in chapter_annotations: + applicable_to = i['applicable_to'] + annotation_type = i['type'] + annotation_components = i['components'] + + if not self.are_we_doing_images_only and applicable_to == 'text': + cursor = self.pw.textCursor() + cursor_start = i['cursor'][0] + cursor_end = i['cursor'][1] + + self.pw.annotator.set_current_annotation( + annotation_type, annotation_components) + + new_cursor = self.pw.annotator.format_text( + cursor, cursor_start, cursor_end) + self.pw.setTextCursor(new_cursor) + + def check_annotation_position(self, annotation_type, chapter, cursor_position): + try: + chapter_annotations = self.pw.annotation_dict[chapter] + except KeyError: + return False + + for i in chapter_annotations: + if annotation_type == 'text': + cursor_start = i['cursor'][0] + cursor_end = i['cursor'][1] + + if cursor_start <= cursor_position <= cursor_end: + return True + + return False + + def delete_annotation(self, annotation_type, chapter, cursor_position): + try: + chapter_annotations = self.pw.annotation_dict[chapter] + except KeyError: + return + + for i in chapter_annotations: + if annotation_type == 'text': + cursor_start = i['cursor'][0] + cursor_end = i['cursor'][1] + + if cursor_start <= cursor_position <= cursor_end: + self.pw.annotation_dict[chapter].remove(i) + + current_scroll_position = self.pw.verticalScrollBar().value() + self.clear_annotations() + self.load_annotations(chapter) + self.pw.verticalScrollBar().setValue(current_scroll_position) + + def clear_annotations(self): + if not self.are_we_doing_images_only: + cursor = self.pw.textCursor() + cursor.setPosition(0) + cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor) + + previewCharFormat = QtGui.QTextCharFormat() + previewCharFormat.setFontStyleStrategy( + QtGui.QFont.PreferAntialias) + cursor.setCharFormat(previewCharFormat) + cursor.clearSelection() + self.pw.setTextCursor(cursor) def update_model(self): # We're updating the underlying model to have real-time diff --git a/lector/database.py b/lector/database.py index ea5f14d..2e7815b 100644 --- a/lector/database.py +++ b/lector/database.py @@ -219,7 +219,7 @@ class DatabaseFunctions: def modify_metadata(self, metadata_dict, book_hash): def generate_binary(column, data): - if column in ('Position', 'LastAccessed', 'Bookmarks'): + if column in ('Position', 'LastAccessed', 'Bookmarks', 'Annotations'): return sqlite3.Binary(pickle.dumps(data)) elif column == 'CoverImage': return sqlite3.Binary(data) diff --git a/lector/sorter.py b/lector/sorter.py index 6a4bc78..936891c 100644 --- a/lector/sorter.py +++ b/lector/sorter.py @@ -126,7 +126,8 @@ class BookSorter: def database_entry_for_book(self, file_hash): database_return = database.DatabaseFunctions( self.database_path).fetch_data( - ('Title', 'Author', 'Year', 'ISBN', 'Tags', 'Position', 'Bookmarks', 'CoverImage'), + ('Title', 'Author', 'Year', 'ISBN', 'Tags', + 'Position', 'Bookmarks', 'CoverImage', 'Annotations'), 'books', {'Hash': file_hash}, 'EQUALS')[0] @@ -134,7 +135,7 @@ class BookSorter: book_data = [] for count, i in enumerate(database_return): - if count in (5, 6): + if count in (5, 6, 8): # Position, Bookmarks, and Annotations are pickled if i: book_data.append(pickle.loads(i)) else: @@ -233,12 +234,14 @@ class BookSorter: position = book_data[5] bookmarks = book_data[6] cover = book_data[7] + annotations = book_data[8] this_book[file_md5]['position'] = position this_book[file_md5]['bookmarks'] = bookmarks this_book[file_md5]['content'] = content this_book[file_md5]['images_only'] = images_only this_book[file_md5]['cover'] = cover + this_book[file_md5]['annotations'] = annotations this_book[file_md5]['title'] = title this_book[file_md5]['author'] = author diff --git a/lector/threaded.py b/lector/threaded.py index f34ac93..242c31c 100644 --- a/lector/threaded.py +++ b/lector/threaded.py @@ -36,7 +36,8 @@ class BackGroundTabUpdate(QtCore.QThread): database_dict = { 'Position': i['position'], 'LastAccessed': i['last_accessed'], - 'Bookmarks': i['bookmarks']} + 'Bookmarks': i['bookmarks'], + 'Annotations': i['annotations']} database.DatabaseFunctions(self.database_path).modify_metadata( database_dict, book_hash) diff --git a/lector/widgets.py b/lector/widgets.py index 0b2cc63..fccc4c9 100644 --- a/lector/widgets.py +++ b/lector/widgets.py @@ -58,6 +58,14 @@ class Tab(QtWidgets.QWidget): chapter_content = self.metadata['content'][current_chapter - 1][1] + # Create relevant containers + if not self.metadata['annotations']: + self.metadata['annotations'] = {} + + # See bookmark availability + if not self.metadata['bookmarks']: + self.metadata['bookmarks'] = {} + # The content display widget is, by default a QTextBrowser. # In case the incoming data is only images # such as in the case of comic book files, @@ -97,6 +105,9 @@ class Tab(QtWidgets.QWidget): self.hiddenButton.clicked.connect(self.set_cursor_position) self.hiddenButton.animateClick(50) + # Load annotations for current content + self.contentView.common_functions.load_annotations(current_chapter) + # The following are common to both the text browser and # the graphics view self.contentView.setFrameShape(QtWidgets.QFrame.NoFrame) @@ -120,19 +131,13 @@ class Tab(QtWidgets.QWidget): self.annotationListView = QtWidgets.QListView(self.annotationDock) self.annotationListView.setResizeMode(QtWidgets.QListWidget.Adjust) self.annotationListView.setMaximumWidth(350) - if not self.are_we_doing_images_only: - self.annotationListView.doubleClicked.connect( - self.contentView.toggle_annotation_mode) + self.annotationListView.doubleClicked.connect(self.contentView.toggle_annotation_mode) self.annotationListView.setEditTriggers(QtWidgets.QListView.NoEditTriggers) self.annotationDock.setWidget(self.annotationListView) self.annotationModel = QtGui.QStandardItemModel(self) self.generate_annotation_model() - # See bookmark availability - if not self.metadata['bookmarks']: - self.metadata['bookmarks'] = {} - # Create the dock widget for context specific display self.bookmarkDock = PliantDockWidget(self.main_window, 'bookmarks', self.contentView) self.bookmarkDock.setWindowTitle(self._translate('Tab', 'Bookmarks')) @@ -335,6 +340,8 @@ class Tab(QtWidgets.QWidget): self.contentView.clear() self.contentView.setHtml(required_content) + self.contentView.common_functions.load_annotations(chapter_number + 1) + def format_view(self, font, font_size, foreground, background, padding, line_spacing, text_alignment): @@ -405,12 +412,19 @@ class Tab(QtWidgets.QWidget): if not saved_annotations: return - for i in saved_annotations: + def add_to_model(annotation): item = QtGui.QStandardItem() - item.setText(i['name']) - item.setData(i, QtCore.Qt.UserRole) + item.setText(annotation['name']) + item.setData(annotation, QtCore.Qt.UserRole) self.annotationModel.appendRow(item) + # Prevent annotation mixup + for i in saved_annotations: + if self.are_we_doing_images_only and i['applicable_to'] == 'images': + add_to_model(i) + elif not self.are_we_doing_images_only and i['applicable_to'] == 'text': + add_to_model(i) + self.annotationListView.setModel(self.annotationModel) def toggle_bookmarks(self): @@ -556,13 +570,13 @@ class PliantDockWidget(QtWidgets.QDockWidget): desktop_size = QtWidgets.QDesktopWidget().screenGeometry() if self.intended_for == 'bookmarks': - dock_x = viewport_topRight.x() - dock_width + 1 dock_width = desktop_size.width() // 5.5 + dock_x = viewport_topRight.x() - dock_width + 1 self.main_window.bookToolBar.bookmarkButton.setChecked(True) elif self.intended_for == 'annotations': - dock_x = viewport_topLeft.x() dock_width = desktop_size.width() // 10 + dock_x = viewport_topLeft.x() self.main_window.bookToolBar.annotationButton.setChecked(True) dock_y = viewport_topRight.y() + (viewport_height * .10)