diff --git a/lector/__main__.py b/lector/__main__.py index 16c1c59..fc56771 100755 --- a/lector/__main__.py +++ b/lector/__main__.py @@ -462,7 +462,8 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): def add_bookmark(self): if self.tabWidget.currentIndex() != 0: - self.tabWidget.widget(self.tabWidget.currentIndex()).add_bookmark() + current_tab = self.tabWidget.currentWidget() + current_tab.sideDock.bookmarks.add_bookmark() def resizeEvent(self, event=None): if event: diff --git a/lector/contentwidgets.py b/lector/contentwidgets.py index 0d6bebd..65f6a18 100644 --- a/lector/contentwidgets.py +++ b/lector/contentwidgets.py @@ -482,15 +482,15 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser): self.parent.sideDock.setWindowOpacity(.95) self.current_annotation = None - self.parent.annotationListView.clearSelection() + self.parent.sideDock.annotations.annotationListView.clearSelection() else: self.annotation_mode = True self.viewport().setCursor(QtCore.Qt.IBeamCursor) self.parent.sideDock.hide() - selected_index = self.parent.annotationListView.currentIndex() - self.current_annotation = self.parent.annotationModel.data( + selected_index = self.parent.sideDock.annotations.annotationListView.currentIndex() + self.current_annotation = self.parent.sideDock.annotationModel.data( selected_index, QtCore.Qt.UserRole) logger.info('Selected annotation: ' + self.current_annotation['name']) @@ -619,14 +619,14 @@ class PliantQTextBrowser(QtWidgets.QTextBrowser): action = contextMenu.exec_(self.sender().mapToGlobal(position)) if action == addBookMarkAction: - self.parent.add_bookmark(cursor_at_mouse.position()) + self.parent.sideDock.bookmarks.add_bookmark(cursor_at_mouse.position()) if action == defineAction: self.main_window.definitionDialog.find_definition(selection) if action == searchAction: if selection and selection != '': - self.parent.searchLineEdit.setText(selection) + self.parent.sideDock.search.searchLineEdit.setText(selection) self.parent.toggle_side_dock(2, True) if action == searchGoogleAction: diff --git a/lector/dockwidgets.py b/lector/dockwidgets.py index 6f0c0e7..65063d9 100644 --- a/lector/dockwidgets.py +++ b/lector/dockwidgets.py @@ -14,9 +14,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import uuid + from PyQt5 import QtWidgets, QtGui, QtCore from lector.models import BookmarkProxyModel +from lector.threaded import BackGroundTextSearch class PliantDockWidget(QtWidgets.QDockWidget): @@ -26,6 +29,27 @@ class PliantDockWidget(QtWidgets.QDockWidget): self.notes_only = notes_only self.contentView = contentView self.current_annotation = None + self.parent = parent + + # Models + # The following models belong to the sideDock + # bookmarkModel, bookmarkProxyModel + # annotationModel + # searchResultsModel + self.bookmarkModel = None + self.bookmarkProxyModel = None + self.annotationModel = None + self.searchResultsModel = None + + # References + # All widgets belong to these + self.bookmarks = None + self.annotations = None + self.search = None + + # Widgets + # Except this one + self.sideDockTabWidget = None def showEvent(self, event=None): viewport_topRight = self.contentView.mapToGlobal( @@ -60,96 +84,434 @@ class PliantDockWidget(QtWidgets.QDockWidget): def set_annotation(self, annotation): self.current_annotation = annotation + def populate(self): + self.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable) + self.setTitleBarWidget(QtWidgets.QWidget(self)) # Removes titlebar + self.sideDockTabWidget = QtWidgets.QTabWidget(self) + self.setWidget(self.sideDockTabWidget) + + # This order is important + self.bookmarkModel = QtGui.QStandardItemModel(self) + self.bookmarkProxyModel = BookmarkProxyModel(self) + self.bookmarks = Bookmarks(self) + self.bookmarks.generate_bookmark_model() + + if not self.parent.are_we_doing_images_only: + self.annotationModel = QtGui.QStandardItemModel(self) + self.annotations = Annotations(self) + self.annotations.generate_annotation_model() + + self.searchResultsModel = QtGui.QStandardItemModel(self) + self.search = Search(self) + def closeEvent(self, event): self.hide() - - # Ignoring this event prevents application closure when everything is fullscreened + # Ignoring this event prevents application closure + # when everything is fullscreened event.ignore() -# TODO -# Maybe subclass PliantDockWidget for this -def populate_sideDock(tabWidget): - tabWidget.sideDock.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable) - tabWidget.sideDock.setTitleBarWidget(QtWidgets.QWidget()) - tabWidget.sideDockTabWidget = QtWidgets.QTabWidget() - tabWidget.sideDock.setWidget(tabWidget.sideDockTabWidget) +# For the following classes, the parent is the sideDock +# The parentTab is the parent... tab. So self.parent.parent +class Bookmarks: + def __init__(self, parent): + self.parent = parent + self.parentTab = self.parent.parent + self.bookmarkTreeView = QtWidgets.QTreeView(self.parent) + self._translate = QtCore.QCoreApplication.translate + self.bookmarks_string = self._translate('SideDock', 'Bookmarks') - # Bookmark tree view and model - tabWidget.bookmarkTreeView = QtWidgets.QTreeView(tabWidget) - tabWidget.bookmarkTreeView.setHeaderHidden(True) - tabWidget.bookmarkTreeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - tabWidget.bookmarkTreeView.customContextMenuRequested.connect( - tabWidget.generate_bookmark_context_menu) - tabWidget.bookmarkTreeView.clicked.connect(tabWidget.navigate_to_bookmark) - bookmarks_string = tabWidget._translate('Tab', 'Bookmarks') - tabWidget.sideDockTabWidget.addTab(tabWidget.bookmarkTreeView, bookmarks_string) + self.create_widgets() - tabWidget.bookmarkModel = QtGui.QStandardItemModel(tabWidget) - tabWidget.bookmarkProxyModel = BookmarkProxyModel(tabWidget) - tabWidget.generate_bookmark_model() + def create_widgets(self): + self.bookmarkTreeView.setHeaderHidden(True) + self.bookmarkTreeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.bookmarkTreeView.customContextMenuRequested.connect( + self.generate_bookmark_context_menu) + self.bookmarkTreeView.clicked.connect(self.navigate_to_bookmark) - # Annotation list view and model - # Leave this without a parent or it shows up in the image viewer - tabWidget.annotationListView = QtWidgets.QListView() - tabWidget.annotationListView.setEditTriggers(QtWidgets.QListView.NoEditTriggers) - tabWidget.annotationListView.doubleClicked.connect(tabWidget.contentView.toggle_annotation_mode) - annotations_string = tabWidget._translate('Tab', 'Annotations') + # Add widget to side dock + self.parent.sideDockTabWidget.addTab( + self.bookmarkTreeView, self.bookmarks_string) - tabWidget.annotationModel = QtGui.QStandardItemModel(tabWidget) - tabWidget.generate_annotation_model() + def add_bookmark(self, position=None): + identifier = uuid.uuid4().hex[:10] + description = self._translate('SideDock', 'New bookmark') - # Search view and model - tabWidget.searchLineEdit = QtWidgets.QLineEdit() - tabWidget.searchLineEdit.setFocusPolicy(QtCore.Qt.StrongFocus) - tabWidget.searchLineEdit.setClearButtonEnabled(True) - search_string = tabWidget._translate('Tab', 'Search') - tabWidget.searchLineEdit.setPlaceholderText(search_string) + if self.parentTab.are_we_doing_images_only: + chapter = self.parentTab.metadata['position']['current_chapter'] + cursor_position = 0 + else: + chapter, cursor_position = self.parent.contentView.record_position(True) + if position: # Should be the case when called from the context menu + cursor_position = position - search_book_string = tabWidget._translate('Tab', 'Search entire book') - tabWidget.searchBookButton = QtWidgets.QToolButton() - tabWidget.searchBookButton.setIcon( - tabWidget.main_window.QImageFactory.get_image('view-readermode')) - tabWidget.searchBookButton.setToolTip(search_book_string) - tabWidget.searchBookButton.setCheckable(True) - tabWidget.searchBookButton.setAutoRaise(True) + self.parentTab.metadata['bookmarks'][identifier] = { + 'chapter': chapter, + 'cursor_position': cursor_position, + 'description': description} - case_sensitive_string = tabWidget._translate('Tab', 'Match case') - tabWidget.caseSensitiveSearchButton = QtWidgets.QToolButton() - tabWidget.caseSensitiveSearchButton.setIcon( - tabWidget.main_window.QImageFactory.get_image('search-case')) - tabWidget.caseSensitiveSearchButton.setToolTip(case_sensitive_string) - tabWidget.caseSensitiveSearchButton.setCheckable(True) - tabWidget.caseSensitiveSearchButton.setAutoRaise(True) + self.parent.setVisible(True) + self.parent.sideDockTabWidget.setCurrentIndex(0) + self.add_bookmark_to_model( + description, chapter, cursor_position, identifier, True) - match_word_string = tabWidget._translate('Tab', 'Match word') - tabWidget.matchWholeWordButton = QtWidgets.QToolButton() - tabWidget.matchWholeWordButton.setIcon( - tabWidget.main_window.QImageFactory.get_image('search-word')) - tabWidget.matchWholeWordButton.setToolTip(match_word_string) - tabWidget.matchWholeWordButton.setCheckable(True) - tabWidget.matchWholeWordButton.setAutoRaise(True) + def add_bookmark_to_model( + self, description, chapter_number, cursor_position, + identifier, new_bookmark=False): - tabWidget.searchOptionsLayout = QtWidgets.QHBoxLayout() - tabWidget.searchOptionsLayout.setContentsMargins(0, 3, 0, 0) - tabWidget.searchOptionsLayout.addWidget(tabWidget.searchLineEdit) - tabWidget.searchOptionsLayout.addWidget(tabWidget.searchBookButton) - tabWidget.searchOptionsLayout.addWidget(tabWidget.caseSensitiveSearchButton) - tabWidget.searchOptionsLayout.addWidget(tabWidget.matchWholeWordButton) + def edit_new_bookmark(parent_item): + new_child = parent_item.child(parent_item.rowCount() - 1, 0) + source_index = self.parent.bookmarkModel.indexFromItem(new_child) + edit_index = self.bookmarkTreeView.model().mapFromSource(source_index) + self.parent.activateWindow() + self.bookmarkTreeView.setFocus() + self.bookmarkTreeView.setCurrentIndex(edit_index) + self.bookmarkTreeView.edit(edit_index) - # Leave this without a parent or it shows up in the image viewer - tabWidget.searchResultsTreeView = QtWidgets.QTreeView() - tabWidget.searchResultsTreeView.setHeaderHidden(True) - tabWidget.searchResultsTreeView.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) - tabWidget.searchResultsTreeView.clicked.connect(tabWidget.navigate_to_search_result) + def get_chapter_name(chapter_number): + for i in reversed(self.parentTab.metadata['toc']): + if i[2] <= chapter_number: + return i[1] + return 'Unknown' - tabWidget.searchTabLayout = QtWidgets.QVBoxLayout() - tabWidget.searchTabLayout.addLayout(tabWidget.searchOptionsLayout) - tabWidget.searchTabLayout.addWidget(tabWidget.searchResultsTreeView) - tabWidget.searchTabLayout.setContentsMargins(0, 0, 0, 0) - tabWidget.searchTabWidget = QtWidgets.QWidget() - tabWidget.searchTabWidget.setLayout(tabWidget.searchTabLayout) + bookmark = QtGui.QStandardItem() + bookmark.setData(False, QtCore.Qt.UserRole + 10) # Is Parent + bookmark.setData(chapter_number, QtCore.Qt.UserRole) # Chapter number + bookmark.setData(cursor_position, QtCore.Qt.UserRole + 1) # Cursor Position + bookmark.setData(identifier, QtCore.Qt.UserRole + 2) # Identifier + bookmark.setData(description, QtCore.Qt.DisplayRole) # Description + bookmark_chapter_name = get_chapter_name(chapter_number) - if not tabWidget.are_we_doing_images_only: - tabWidget.sideDockTabWidget.addTab(tabWidget.searchTabWidget, search_string) - tabWidget.sideDockTabWidget.addTab(tabWidget.annotationListView, annotations_string) + for i in range(self.parent.bookmarkModel.rowCount()): + parentIndex = self.parent.bookmarkModel.index(i, 0) + parent_chapter_number = parentIndex.data(QtCore.Qt.UserRole) + parent_chapter_name = parentIndex.data(QtCore.Qt.DisplayRole) + + # This prevents duplication of the bookmark in the new + # navigation model + if ((parent_chapter_number <= chapter_number) and + (parent_chapter_name == bookmark_chapter_name)): + bookmarkParent = self.parent.bookmarkModel.itemFromIndex(parentIndex) + bookmarkParent.appendRow(bookmark) + if new_bookmark: + edit_new_bookmark(bookmarkParent) + return + + # In case no parent item exists + bookmarkParent = QtGui.QStandardItem() + bookmarkParent.setData(True, QtCore.Qt.UserRole + 10) # Is Parent + bookmarkParent.setFlags(bookmarkParent.flags() & ~QtCore.Qt.ItemIsEditable) # Is Editable + bookmarkParent.setData(get_chapter_name(chapter_number), QtCore.Qt.DisplayRole) + bookmarkParent.setData(chapter_number, QtCore.Qt.UserRole) + + bookmarkParent.appendRow(bookmark) + self.parent.bookmarkModel.appendRow(bookmarkParent) + if new_bookmark: + edit_new_bookmark(bookmarkParent) + + def navigate_to_bookmark(self, index): + if not index.isValid(): + return + + is_parent = self.parent.bookmarkProxyModel.data( + index, QtCore.Qt.UserRole + 10) + if is_parent: + chapter_number = self.parent.bookmarkProxyModel.data( + index, QtCore.Qt.UserRole) + self.parentTab.set_content(chapter_number, True) + return + + chapter = self.parent.bookmarkProxyModel.data( + index, QtCore.Qt.UserRole) + cursor_position = self.parent.bookmarkProxyModel.data( + index, QtCore.Qt.UserRole + 1) + + self.parentTab.set_content(chapter, True) + if not self.parentTab.are_we_doing_images_only: + self.parentTab.set_cursor_position(cursor_position) + + def generate_bookmark_model(self): + for i in self.parentTab.metadata['bookmarks'].items(): + description = i[1]['description'] + chapter = i[1]['chapter'] + cursor_position = i[1]['cursor_position'] + identifier = i[0] + self.add_bookmark_to_model( + description, chapter, cursor_position, identifier) + + self.generate_bookmark_proxy_model() + + def generate_bookmark_proxy_model(self): + self.parent.bookmarkProxyModel.setSourceModel(self.parent.bookmarkModel) + self.parent.bookmarkProxyModel.setSortCaseSensitivity(False) + self.parent.bookmarkProxyModel.setSortRole(QtCore.Qt.UserRole) + self.parent.bookmarkProxyModel.sort(0) + self.bookmarkTreeView.setModel(self.parent.bookmarkProxyModel) + + def generate_bookmark_context_menu(self, position): + index = self.bookmarkTreeView.indexAt(position) + if not index.isValid(): + return + + is_parent = self.parent.bookmarkProxyModel.data( + index, QtCore.Qt.UserRole + 10) + if is_parent: + return + + bookmarkMenu = QtWidgets.QMenu() + editAction = bookmarkMenu.addAction( + self.parentTab.main_window.QImageFactory.get_image('edit-rename'), + self._translate('Tab', 'Edit')) + deleteAction = bookmarkMenu.addAction( + self.parentTab.main_window.QImageFactory.get_image('trash-empty'), + self._translate('Tab', 'Delete')) + + action = bookmarkMenu.exec_( + self.bookmarkTreeView.mapToGlobal(position)) + + if action == editAction: + self.bookmarkTreeView.edit(index) + + if action == deleteAction: + child_index = self.parent.bookmarkProxyModel.mapToSource(index) + parent_index = child_index.parent() + child_rows = self.parent.bookmarkModel.itemFromIndex(parent_index).rowCount() + delete_uuid = self.parent.bookmarkModel.data( + child_index, QtCore.Qt.UserRole + 2) + + self.parentTab.metadata['bookmarks'].pop(delete_uuid) + + self.parent.bookmarkModel.removeRow(child_index.row(), child_index.parent()) + if child_rows == 1: + self.parent.bookmarkModel.removeRow(parent_index.row()) + + +class Annotations: + def __init__(self, parent): + self.parent = parent + self.parentTab = self.parent.parent + self.annotationListView = QtWidgets.QListView(self.parent) + self._translate = QtCore.QCoreApplication.translate + self.annotations_string = self._translate('SideDock', 'Annotations') + + self.create_widgets() + + def create_widgets(self): + self.annotationListView.setEditTriggers(QtWidgets.QListView.NoEditTriggers) + self.annotationListView.doubleClicked.connect( + self.parent.contentView.toggle_annotation_mode) + + # Add widget to side dock + self.parent.sideDockTabWidget.addTab( + self.annotationListView, self.annotations_string) + + def generate_annotation_model(self): + # TODO + # Annotation previews will require creation of a + # QStyledItemDelegate + + saved_annotations = self.parent.main_window.settings['annotations'] + if not saved_annotations: + return + + # Create annotation model + for i in saved_annotations: + item = QtGui.QStandardItem() + item.setText(i['name']) + item.setData(i, QtCore.Qt.UserRole) + self.parent.annotationModel.appendRow(item) + self.annotationListView.setModel(self.parent.annotationModel) + + +class Search: + def __init__(self, parent): + self.parent = parent + self.parentTab = self.parent.parent + + self.searchThread = BackGroundTextSearch() + self.searchOptionsLayout = QtWidgets.QHBoxLayout() + self.searchTabLayout = QtWidgets.QVBoxLayout() + self.searchTimer = QtCore.QTimer(self.parent) + self.searchLineEdit = QtWidgets.QLineEdit(self.parent) + self.searchBookButton = QtWidgets.QToolButton(self.parent) + self.caseSensitiveSearchButton = QtWidgets.QToolButton(self.parent) + self.matchWholeWordButton = QtWidgets.QToolButton(self.parent) + self.searchResultsTreeView = QtWidgets.QTreeView(self.parent) + + self.create_widgets() + + def create_widgets(self): + self.searchThread.finished.connect(self.generate_search_result_model) + + self.searchTimer.setSingleShot(True) + self.searchTimer.timeout.connect(self.set_search_options) + + self.searchLineEdit.textChanged.connect( + lambda: self.searchLineEdit.setStyleSheet( + QtWidgets.QLineEdit.styleSheet(self.parent))) + self.searchLineEdit.textChanged.connect( + lambda: self.searchTimer.start(500)) + self.searchBookButton.clicked.connect( + lambda: self.searchTimer.start(100)) + self.caseSensitiveSearchButton.clicked.connect( + lambda: self.searchTimer.start(100)) + self.matchWholeWordButton.clicked.connect( + lambda: self.searchTimer.start(100)) + + self.searchLineEdit.setFocusPolicy(QtCore.Qt.StrongFocus) + self.searchLineEdit.setClearButtonEnabled(True) + self._translate = QtCore.QCoreApplication.translate + self.search_string = self._translate('SideDock', 'Search') + self.searchLineEdit.setPlaceholderText(self.search_string) + + search_book_string = self._translate('SideDock', 'Search entire book') + + self.searchBookButton.setIcon( + self.parent.main_window.QImageFactory.get_image('view-readermode')) + self.searchBookButton.setToolTip(search_book_string) + self.searchBookButton.setCheckable(True) + self.searchBookButton.setAutoRaise(True) + self.searchBookButton.setIconSize(QtCore.QSize(20, 20)) + + case_sensitive_string = self._translate('SideDock', 'Match case') + + self.caseSensitiveSearchButton.setIcon( + self.parent.main_window.QImageFactory.get_image('search-case')) + self.caseSensitiveSearchButton.setToolTip(case_sensitive_string) + self.caseSensitiveSearchButton.setCheckable(True) + self.caseSensitiveSearchButton.setAutoRaise(True) + self.caseSensitiveSearchButton.setIconSize(QtCore.QSize(20, 20)) + + match_word_string = self._translate('SideDock', 'Match word') + self.matchWholeWordButton.setIcon( + self.parent.main_window.QImageFactory.get_image('search-word')) + self.matchWholeWordButton.setToolTip(match_word_string) + self.matchWholeWordButton.setCheckable(True) + self.matchWholeWordButton.setAutoRaise(True) + self.matchWholeWordButton.setIconSize(QtCore.QSize(20, 20)) + + self.searchOptionsLayout.setContentsMargins(0, 3, 0, 0) + self.searchOptionsLayout.addWidget(self.searchLineEdit) + self.searchOptionsLayout.addWidget(self.searchBookButton) + self.searchOptionsLayout.addWidget(self.caseSensitiveSearchButton) + self.searchOptionsLayout.addWidget(self.matchWholeWordButton) + + self.searchResultsTreeView.setHeaderHidden(True) + self.searchResultsTreeView.setEditTriggers( + QtWidgets.QTreeView.NoEditTriggers) + self.searchResultsTreeView.clicked.connect( + self.navigate_to_search_result) + + self.searchTabLayout.addLayout(self.searchOptionsLayout) + self.searchTabLayout.addWidget(self.searchResultsTreeView) + self.searchTabLayout.setContentsMargins(0, 0, 0, 0) + self.searchTabWidget = QtWidgets.QWidget(self.parent) + self.searchTabWidget.setLayout(self.searchTabLayout) + + # Add widget to side dock + self.parent.sideDockTabWidget.addTab( + self.searchTabWidget, self.search_string) + + def set_search_options(self): + def generate_title_content_pair(required_chapters): + title_content_list = [] + for i in self.parentTab.metadata['toc']: + if i[2] in required_chapters: + title_content_list.append( + (i[1], self.parentTab.metadata['content'][i[2] - 1], i[2])) + return title_content_list + + # Select either the current chapter or all chapters + # Function name is descriptive + chapter_numbers = (self.parentTab.metadata['position']['current_chapter'],) + if self.searchBookButton.isChecked(): + chapter_numbers = [i + 1 for i in range(len(self.parentTab.metadata['content']))] + search_content = generate_title_content_pair(chapter_numbers) + + self.searchThread.set_search_options( + search_content, + self.searchLineEdit.text(), + self.caseSensitiveSearchButton.isChecked(), + self.matchWholeWordButton.isChecked()) + self.searchThread.start() + + def generate_search_result_model(self): + self.parent.searchResultsModel.clear() + search_results = self.searchThread.search_results + for i in search_results: + parentItem = QtGui.QStandardItem() + parentItem.setData(True, QtCore.Qt.UserRole) # Is parent? + parentItem.setData(i, QtCore.Qt.UserRole + 3) # Display text for label + + for j in search_results[i]: + childItem = QtGui.QStandardItem(parentItem) + childItem.setData(False, QtCore.Qt.UserRole) # Is parent? + childItem.setData(j[3], QtCore.Qt.UserRole + 1) # Chapter index + childItem.setData(j[0], QtCore.Qt.UserRole + 2) # Cursor Position + childItem.setData(j[1], QtCore.Qt.UserRole + 3) # Display text for label + childItem.setData(j[2], QtCore.Qt.UserRole + 4) # Search term + parentItem.appendRow(childItem) + self.parent.searchResultsModel.appendRow(parentItem) + + self.searchResultsTreeView.setModel(self.parent.searchResultsModel) + self.searchResultsTreeView.expandToDepth(1) + + # Reset stylesheet in case something is found + if search_results: + self.searchLineEdit.setStyleSheet( + QtWidgets.QLineEdit.styleSheet(self.parent)) + + # Or set to Red in case nothing is found + if not search_results and len(self.searchLineEdit.text()) > 2: + self.searchLineEdit.setStyleSheet("QLineEdit {color: red;}") + + # We'll be putting in labels instead of making a delegate + # QLabels can understand RTF, and they also have the somewhat + # distinct advantage of being a lot less work than a delegate + + def generate_label(index): + label_text = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole + 3) + labelWidget = PliantLabelWidget(index, self.navigate_to_search_result) + labelWidget.setText(label_text) + self.searchResultsTreeView.setIndexWidget(index, labelWidget) + + for parent_iter in range(self.parent.searchResultsModel.rowCount()): + parentItem = self.parent.searchResultsModel.item(parent_iter) + parentIndex = self.parent.searchResultsModel.index(parent_iter, 0) + generate_label(parentIndex) + + for child_iter in range(parentItem.rowCount()): + childIndex = self.parent.searchResultsModel.index(child_iter, 0, parentIndex) + generate_label(childIndex) + + def navigate_to_search_result(self, index): + if not index.isValid(): + return + + is_parent = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole) + if is_parent: + return + + chapter_number = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole + 1) + cursor_position = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole + 2) + search_term = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole + 4) + + self.parentTab.set_content(chapter_number, True) + if not self.parentTab.are_we_doing_images_only: + self.parentTab.set_cursor_position( + cursor_position, len(search_term)) + + +class PliantLabelWidget(QtWidgets.QLabel): + # This is a hack to get clickable / editable appearance + # search results in the tree view. + + def __init__(self, index, navigate_to_search_result): + super(PliantLabelWidget, self).__init__() + self.index = index + self.navigate_to_search_result = navigate_to_search_result + + def mousePressEvent(self, QMouseEvent): + self.navigate_to_search_result(self.index) + QtWidgets.QLabel.mousePressEvent(self, QMouseEvent) diff --git a/lector/models.py b/lector/models.py index 4e33fcf..c6baf26 100644 --- a/lector/models.py +++ b/lector/models.py @@ -27,23 +27,19 @@ class BookmarkProxyModel(QtCore.QSortFilterProxyModel): def __init__(self, parent=None): super(BookmarkProxyModel, self).__init__(parent) self.parent = parent + self.parentTab = self.parent.parent self.filter_text = None def setFilterParams(self, filter_text): self.filter_text = filter_text - def filterAcceptsRow(self, row, parent): - # TODO - # Connect this to the search bar - return True - def setData(self, index, value, role): if role == QtCore.Qt.EditRole: source_index = self.mapToSource(index) identifier = self.sourceModel().data(source_index, QtCore.Qt.UserRole + 2) self.sourceModel().setData(source_index, value, QtCore.Qt.DisplayRole) - self.parent.metadata['bookmarks'][identifier]['description'] = value + self.parentTab.metadata['bookmarks'][identifier]['description'] = value return True diff --git a/lector/widgets.py b/lector/widgets.py index 9a20685..13c63f0 100644 --- a/lector/widgets.py +++ b/lector/widgets.py @@ -19,14 +19,12 @@ # Double page, Continuous etc import os -import uuid import logging from PyQt5 import QtWidgets, QtGui, QtCore from lector.sorter import resize_image -from lector.threaded import BackGroundTextSearch -from lector.dockwidgets import PliantDockWidget, populate_sideDock +from lector.dockwidgets import PliantDockWidget from lector.contentwidgets import PliantQGraphicsView, PliantQTextBrowser logger = logging.getLogger(__name__) @@ -137,13 +135,14 @@ class Tab(QtWidgets.QWidget): self.contentView.setVerticalScrollBarPolicy( QtCore.Qt.ScrollBarAsNeeded) - # Create a common dock for annotations and bookmarks - # It is populated by the following method - self.sideDock = PliantDockWidget(self.main_window, False, self.contentView) - populate_sideDock(self) + # Create a common dock for bookmarks, annotations, and search + self.sideDock = PliantDockWidget( + self.main_window, False, self.contentView, self) + self.sideDock.populate() # Create the annotation notes dock - self.annotationNoteDock = PliantDockWidget(self.main_window, True, self.contentView) + self.annotationNoteDock = PliantDockWidget( + self.main_window, True, self.contentView, self) self.annotationNoteDock.setWindowTitle(self._translate('Tab', 'Note')) self.annotationNoteDock.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable) self.annotationNoteDock.hide() @@ -166,29 +165,6 @@ class Tab(QtWidgets.QWidget): self.annotationNoteDock.setWindowOpacity(.95) self.sideDock.hide() - # Create search references - if not self.are_we_doing_images_only: - self.searchResultsModel = None - - self.searchThread = BackGroundTextSearch() - self.searchThread.finished.connect(self.generate_search_result_model) - - self.searchTimer = QtCore.QTimer() - self.searchTimer.setSingleShot(True) - self.searchTimer.timeout.connect(self.set_search_options) - - self.searchLineEdit.textChanged.connect( - lambda: self.searchLineEdit.setStyleSheet( - QtWidgets.QLineEdit.styleSheet(self))) - self.searchLineEdit.textChanged.connect( - lambda: self.searchTimer.start(500)) - self.searchBookButton.clicked.connect( - lambda: self.searchTimer.start(100)) - self.caseSensitiveSearchButton.clicked.connect( - lambda: self.searchTimer.start(100)) - self.matchWholeWordButton.clicked.connect( - lambda: self.searchTimer.start(100)) - # Create tab in the central tab widget title = self.metadata['title'] if self.main_window.settings['attenuate_titles'] and len(title) > 30: @@ -214,17 +190,17 @@ class Tab(QtWidgets.QWidget): def toggle_side_dock(self, tab_required, override_hide=False): if (self.sideDock.isVisible() - and self.sideDockTabWidget.currentIndex() == tab_required + and self.sideDock.sideDockTabWidget.currentIndex() == tab_required and not override_hide): self.sideDock.hide() elif not self.sideDock.isVisible(): self.sideDock.show() if tab_required == 2: self.sideDock.activateWindow() - self.searchLineEdit.setFocus() - self.searchLineEdit.selectAll() + self.sideDock.search.searchLineEdit.setFocus() + self.sideDock.search.searchLineEdit.selectAll() - self.sideDockTabWidget.setCurrentIndex(tab_required) + self.sideDock.sideDockTabWidget.setCurrentIndex(tab_required) def update_last_accessed_time(self): self.metadata['last_accessed'] = QtCore.QDateTime().currentDateTime() @@ -238,7 +214,8 @@ class Tab(QtWidgets.QWidget): try: self.main_window.lib_ref.libraryModel.setData( - matching_item[0], self.metadata['last_accessed'], QtCore.Qt.UserRole + 12) + matching_item[0], + self.metadata['last_accessed'], QtCore.Qt.UserRole + 12) except IndexError: # The file has been deleted pass @@ -329,17 +306,20 @@ class Tab(QtWidgets.QWidget): ksToggleBookmarks = QtWidgets.QShortcut( QtGui.QKeySequence('Ctrl+B'), self.contentView) - ksToggleBookmarks.activated.connect(lambda: self.toggle_side_dock(0)) + ksToggleBookmarks.activated.connect( + lambda: self.toggle_side_dock(0)) # Shortcuts not required for comic view functionality if not self.are_we_doing_images_only: ksToggleAnnotations = QtWidgets.QShortcut( QtGui.QKeySequence('Ctrl+N'), self.contentView) - ksToggleAnnotations.activated.connect(lambda: self.toggle_side_dock(1)) + ksToggleAnnotations.activated.connect( + lambda: self.toggle_side_dock(1)) ksToggleSearch = QtWidgets.QShortcut( QtGui.QKeySequence('Ctrl+F'), self.contentView) - ksToggleSearch.activated.connect(lambda: self.toggle_side_dock(2)) + ksToggleSearch.activated.connect( + lambda: self.toggle_side_dock(2)) def generate_toc_model(self): # The toc list is: @@ -548,7 +528,8 @@ class Tab(QtWidgets.QWidget): current_index = self.main_window.bookToolBar.tocBox.currentIndex() if current_index == 0: - block_format.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter) + block_format.setAlignment( + QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter) else: block_format.setAlignment(alignment_dict[text_alignment]) @@ -570,259 +551,6 @@ class Tab(QtWidgets.QWidget): if old_position == new_position: break - def generate_annotation_model(self): - # TODO - # Annotation previews will require creation of a - # QStyledItemDelegate - - saved_annotations = self.main_window.settings['annotations'] - if not saved_annotations: - return - - # Create annotation model - for i in saved_annotations: - item = QtGui.QStandardItem() - item.setText(i['name']) - item.setData(i, QtCore.Qt.UserRole) - self.annotationModel.appendRow(item) - self.annotationListView.setModel(self.annotationModel) - - def add_bookmark(self, position=None): - identifier = uuid.uuid4().hex[:10] - description = self._translate('Tab', 'New bookmark') - - if self.are_we_doing_images_only: - chapter = self.metadata['position']['current_chapter'] - cursor_position = 0 - else: - chapter, cursor_position = self.contentView.record_position(True) - if position: # Should be the case when called from the context menu - cursor_position = position - - self.metadata['bookmarks'][identifier] = { - 'chapter': chapter, - 'cursor_position': cursor_position, - 'description': description} - - self.sideDock.setVisible(True) - self.sideDockTabWidget.setCurrentIndex(0) - self.add_bookmark_to_model( - description, chapter, cursor_position, identifier, True) - - def add_bookmark_to_model( - self, description, chapter_number, cursor_position, - identifier, new_bookmark=False): - - def edit_new_bookmark(parent_item): - new_child = parent_item.child(parent_item.rowCount() - 1, 0) - source_index = self.bookmarkModel.indexFromItem(new_child) - edit_index = self.bookmarkTreeView.model().mapFromSource(source_index) - self.sideDock.activateWindow() - self.bookmarkTreeView.setFocus() - self.bookmarkTreeView.setCurrentIndex(edit_index) - self.bookmarkTreeView.edit(edit_index) - - def get_chapter_name(chapter_number): - for i in reversed(self.metadata['toc']): - if i[2] <= chapter_number: - return i[1] - return 'Unknown' - - bookmark = QtGui.QStandardItem() - bookmark.setData(False, QtCore.Qt.UserRole + 10) # Is Parent - bookmark.setData(chapter_number, QtCore.Qt.UserRole) # Chapter number - bookmark.setData(cursor_position, QtCore.Qt.UserRole + 1) # Cursor Position - bookmark.setData(identifier, QtCore.Qt.UserRole + 2) # Identifier - bookmark.setData(description, QtCore.Qt.DisplayRole) # Description - bookmark_chapter_name = get_chapter_name(chapter_number) - - for i in range(self.bookmarkModel.rowCount()): - parentIndex = self.bookmarkModel.index(i, 0) - parent_chapter_number = parentIndex.data(QtCore.Qt.UserRole) - parent_chapter_name = parentIndex.data(QtCore.Qt.DisplayRole) - - # This prevents duplication of the bookmark in the new - # navigation model - if ((parent_chapter_number <= chapter_number) and - (parent_chapter_name == bookmark_chapter_name)): - bookmarkParent = self.bookmarkModel.itemFromIndex(parentIndex) - bookmarkParent.appendRow(bookmark) - if new_bookmark: - edit_new_bookmark(bookmarkParent) - return - - # In case no parent item exists - bookmarkParent = QtGui.QStandardItem() - bookmarkParent.setData(True, QtCore.Qt.UserRole + 10) # Is Parent - bookmarkParent.setFlags(bookmarkParent.flags() & ~QtCore.Qt.ItemIsEditable) # Is Editable - bookmarkParent.setData(get_chapter_name(chapter_number), QtCore.Qt.DisplayRole) - bookmarkParent.setData(chapter_number, QtCore.Qt.UserRole) - - bookmarkParent.appendRow(bookmark) - self.bookmarkModel.appendRow(bookmarkParent) - if new_bookmark: - edit_new_bookmark(bookmarkParent) - - def navigate_to_bookmark(self, index): - if not index.isValid(): - return - - is_parent = self.bookmarkProxyModel.data(index, QtCore.Qt.UserRole + 10) - if is_parent: - chapter_number = self.bookmarkProxyModel.data(index, QtCore.Qt.UserRole) - self.set_content(chapter_number, True) - return - - chapter = self.bookmarkProxyModel.data(index, QtCore.Qt.UserRole) - cursor_position = self.bookmarkProxyModel.data(index, QtCore.Qt.UserRole + 1) - - self.set_content(chapter, True) - if not self.are_we_doing_images_only: - self.set_cursor_position(cursor_position) - - def generate_bookmark_model(self): - for i in self.metadata['bookmarks'].items(): - description = i[1]['description'] - chapter = i[1]['chapter'] - cursor_position = i[1]['cursor_position'] - identifier = i[0] - self.add_bookmark_to_model( - description, chapter, cursor_position, identifier) - - self.generate_bookmark_proxy_model() - - def generate_bookmark_proxy_model(self): - self.bookmarkProxyModel.setSourceModel(self.bookmarkModel) - self.bookmarkProxyModel.setSortCaseSensitivity(False) - self.bookmarkProxyModel.setSortRole(QtCore.Qt.UserRole) - self.bookmarkProxyModel.sort(0) - self.bookmarkTreeView.setModel(self.bookmarkProxyModel) - - def generate_bookmark_context_menu(self, position): - index = self.bookmarkTreeView.indexAt(position) - if not index.isValid(): - return - - is_parent = self.bookmarkProxyModel.data(index, QtCore.Qt.UserRole + 10) - if is_parent: - return - - bookmarkMenu = QtWidgets.QMenu() - editAction = bookmarkMenu.addAction( - self.main_window.QImageFactory.get_image('edit-rename'), - self._translate('Tab', 'Edit')) - deleteAction = bookmarkMenu.addAction( - self.main_window.QImageFactory.get_image('trash-empty'), - self._translate('Tab', 'Delete')) - - action = bookmarkMenu.exec_( - self.bookmarkTreeView.mapToGlobal(position)) - - if action == editAction: - self.bookmarkTreeView.edit(index) - - if action == deleteAction: - child_index = self.bookmarkProxyModel.mapToSource(index) - parent_index = child_index.parent() - child_rows = self.bookmarkModel.itemFromIndex(parent_index).rowCount() - delete_uuid = self.bookmarkModel.data( - child_index, QtCore.Qt.UserRole + 2) - - self.metadata['bookmarks'].pop(delete_uuid) - - self.bookmarkModel.removeRow(child_index.row(), child_index.parent()) - if child_rows == 1: - self.bookmarkModel.removeRow(parent_index.row()) - - def set_search_options(self): - def generate_title_content_pair(required_chapters): - title_content_list = [] - for i in self.metadata['toc']: - if i[2] in required_chapters: - title_content_list.append( - (i[1], self.metadata['content'][i[2] - 1], i[2])) - return title_content_list - - # Select either the current chapter or all chapters - # Function name is descriptive - chapter_numbers = (self.metadata['position']['current_chapter'],) - if self.searchBookButton.isChecked(): - chapter_numbers = [i + 1 for i in range(len(self.metadata['content']))] - search_content = generate_title_content_pair(chapter_numbers) - - self.searchThread.set_search_options( - search_content, - self.searchLineEdit.text(), - self.caseSensitiveSearchButton.isChecked(), - self.matchWholeWordButton.isChecked()) - self.searchThread.start() - - def generate_search_result_model(self): - self.searchResultsModel = QtGui.QStandardItemModel() - search_results = self.searchThread.search_results - for i in search_results: - parentItem = QtGui.QStandardItem() - parentItem.setData(True, QtCore.Qt.UserRole) # Is parent? - parentItem.setData(i, QtCore.Qt.UserRole + 3) # Display text for label - - for j in search_results[i]: - childItem = QtGui.QStandardItem(parentItem) - childItem.setData(False, QtCore.Qt.UserRole) # Is parent? - childItem.setData(j[3], QtCore.Qt.UserRole + 1) # Chapter index - childItem.setData(j[0], QtCore.Qt.UserRole + 2) # Cursor Position - childItem.setData(j[1], QtCore.Qt.UserRole + 3) # Display text for label - childItem.setData(j[2], QtCore.Qt.UserRole + 4) # Search term - parentItem.appendRow(childItem) - self.searchResultsModel.appendRow(parentItem) - - self.searchResultsTreeView.setModel(self.searchResultsModel) - self.searchResultsTreeView.expandToDepth(1) - - # Reset stylesheet in case something is found - if search_results: - self.searchLineEdit.setStyleSheet( - QtWidgets.QLineEdit.styleSheet(self)) - - # Or set to Red in case nothing is found - if not search_results and len(self.searchLineEdit.text()) > 2: - self.searchLineEdit.setStyleSheet("QLineEdit {color: red;}") - - # We'll be putting in labels instead of making a delegate - # QLabels can understand RTF, and they also have the somewhat - # distinct advantage of being a lot less work than a delegate - - def generate_label(index): - label_text = self.searchResultsModel.data(index, QtCore.Qt.UserRole + 3) - labelWidget = PliantLabelWidget(index, self.navigate_to_search_result) - labelWidget.setText(label_text) - self.searchResultsTreeView.setIndexWidget(index, labelWidget) - - for parent_iter in range(self.searchResultsModel.rowCount()): - parentItem = self.searchResultsModel.item(parent_iter) - parentIndex = self.searchResultsModel.index(parent_iter, 0) - generate_label(parentIndex) - - for child_iter in range(parentItem.rowCount()): - childIndex = self.searchResultsModel.index(child_iter, 0, parentIndex) - generate_label(childIndex) - - def navigate_to_search_result(self, index): - if not index.isValid(): - return - - is_parent = self.searchResultsModel.data(index, QtCore.Qt.UserRole) - if is_parent: - return - - chapter_number = self.searchResultsModel.data(index, QtCore.Qt.UserRole + 1) - cursor_position = self.searchResultsModel.data(index, QtCore.Qt.UserRole + 2) - search_term = self.searchResultsModel.data(index, QtCore.Qt.UserRole + 4) - - self.set_content(chapter_number, True) - if not self.are_we_doing_images_only: - self.set_cursor_position( - cursor_position, len(search_term)) - def hide_mouse(self): self.contentView.viewport().setCursor(QtCore.Qt.BlankCursor) @@ -839,20 +567,6 @@ class Tab(QtWidgets.QWidget): self.main_window.closeEvent() -class PliantLabelWidget(QtWidgets.QLabel): - # This is a hack to get clickable / editable appearance - # search results in the tree view. - - def __init__(self, index, navigate_to_search_result): - super(PliantLabelWidget, self).__init__() - self.index = index - self.navigate_to_search_result = navigate_to_search_result - - def mousePressEvent(self, QMouseEvent): - self.navigate_to_search_result(self.index) - QtWidgets.QLabel.mousePressEvent(self, QMouseEvent) - - class PliantQGraphicsScene(QtWidgets.QGraphicsScene): def __init__(self, parent=None): super(PliantQGraphicsScene, self).__init__(parent) @@ -933,7 +647,8 @@ class DragDropTableView(QtWidgets.QTableView): self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) self.setFrameShape(QtWidgets.QFrame.Box) self.setFrameShadow(QtWidgets.QFrame.Plain) - self.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow) + self.setSizeAdjustPolicy( + QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow) self.setEditTriggers( QtWidgets.QAbstractItemView.DoubleClicked | QtWidgets.QAbstractItemView.EditKeyPressed |