diff --git a/TODO b/TODO index 7fe643d..da9eeb8 100644 --- a/TODO +++ b/TODO @@ -71,6 +71,7 @@ TODO Disable buttons for annotations, search in images Adjust key navigation according to viewport dimensions Search document using QTextCursor + Redo context menu order Filetypes: ✓ pdf support Parse TOC diff --git a/lector/__main__.py b/lector/__main__.py index 1714e6d..7f0f80d 100755 --- a/lector/__main__.py +++ b/lector/__main__.py @@ -398,10 +398,6 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): if not file_paths: return - def finishing_touches(): - self.profile_functions.format_contentView() - self.start_culling_timer() - print('Attempting to open: ' + ', '.join(file_paths)) contents = sorter.BookSorter( @@ -434,11 +430,9 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): if self.settings['last_open_tab'] == this_path: self.tabWidget.setCurrentIndex(i) self.settings['last_open_tab'] = None - finishing_touches() return self.tabWidget.setCurrentIndex(self.tabWidget.count() - 1) - finishing_touches() def start_culling_timer(self): if self.settings['perform_culling']: @@ -457,7 +451,7 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): # The hackiness of this hack is just... default_size = 170 # This is size of the QIcon (160 by default) + - # minimum margin is needed between thumbnails + # minimum margin needed between thumbnails # for n icons, the n + 1th icon will appear at > n +1.11875 # First, calculate the number of images per row @@ -759,32 +753,6 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): required_content = current_tab.metadata['content'][chapter_number][1] current_tab.contentView.loadImage(required_content) - def search_book(self, search_text): - if not (self.tabWidget.currentIndex() != 0 - and not self.tabWidget.currentWidget().are_we_doing_images_only): - return - - self.tabWidget.currentWidget().sideDock.setVisible(True) - self.tabWidget.currentWidget().sideDockTabWidget.setCurrentIndex(2) - - contentView = self.tabWidget.currentWidget().contentView - - text_cursor = contentView.textCursor() - something_found = True - if search_text: - text_cursor.setPosition(0, QtGui.QTextCursor.MoveAnchor) - contentView.setTextCursor(text_cursor) - contentView.verticalScrollBar().setValue(contentView.verticalScrollBar().maximum()) - something_found = contentView.find(search_text) - else: - text_cursor.clearSelection() - contentView.setTextCursor(text_cursor) - - # if not something_found: - # self.bookToolBar.searchBar.setStyleSheet("QLineEdit {color: red;}") - # else: - # self.bookToolBar.searchBar.setStyleSheet(self.lineEditStyleSheet) - def generate_library_context_menu(self, position): index = self.sender().indexAt(position) if not index.isValid(): diff --git a/lector/guifunctions.py b/lector/guifunctions.py index 6152017..1f89789 100644 --- a/lector/guifunctions.py +++ b/lector/guifunctions.py @@ -277,8 +277,7 @@ class ViewProfileModification: self.format_contentView() def format_contentView(self): - current_tab = self.tabWidget.widget( - self.tabWidget.currentIndex()) + current_tab = self.tabWidget.currentWidget() try: current_metadata = current_tab.metadata diff --git a/lector/threaded.py b/lector/threaded.py index dafb542..270f972 100644 --- a/lector/threaded.py +++ b/lector/threaded.py @@ -171,3 +171,65 @@ class BackGroundCacheRefill(QtCore.QThread): self.image_cache.append((next_page, refill_pixmap)) except (IndexError, TypeError): self.image_cache.append(None) + + +class BackGroundTextSearch(QtCore.QThread): + def __init__(self): + super(BackGroundTextSearch, self).__init__(None) + self.search_content = None + self.search_text = None + self.case_sensitive = False + self.match_words = False + self.search_results = [] + + def set_search_options( + self, search_content, search_text, + case_sensitive, match_words): + self.search_content = search_content + self.search_text = search_text + self.case_sensitive = case_sensitive + self.match_words = match_words + + def run(self): + if not self.search_text or len(self.search_text) < 3: + return + + self.search_results = {} + + # Create a new QTextDocument of each chapter and iterate + # through it looking for hits + + for i in self.search_content: + chapter = i[0] + chapterDocument = QtGui.QTextDocument() + chapterDocument.setHtml(i[1]) + + findFlags = QtGui.QTextDocument.FindFlags(0) + if self.case_sensitive: + findFlags = findFlags | QtGui.QTextDocument.FindCaseSensitively + if self.match_words: + findFlags = findFlags | QtGui.QTextDocument.FindWholeWords + + findResultCursor = chapterDocument.find(self.search_text, 0, findFlags) + while not findResultCursor.isNull(): + result_position = findResultCursor.position() + + surroundingTextCursor = QtGui.QTextCursor(chapterDocument) + surroundingTextCursor.setPosition( + result_position, QtGui.QTextCursor.MoveAnchor) + surroundingTextCursor.movePosition( + QtGui.QTextCursor.WordLeft, QtGui.QTextCursor.MoveAnchor, 2) + surroundingTextCursor.movePosition( + QtGui.QTextCursor.NextWord, QtGui.QTextCursor.KeepAnchor, 5) # 2n + 1 + surrounding_text = surroundingTextCursor.selection().toPlainText() + surrounding_text = surrounding_text.replace('\n', ' ') + + try: + self.search_results[chapter].append( + (result_position, surrounding_text)) + except KeyError: + self.search_results[chapter] = [(result_position, surrounding_text)] + + new_position = result_position + len(self.search_text) + findResultCursor = chapterDocument.find( + self.search_text, new_position, findFlags) diff --git a/lector/widgets.py b/lector/widgets.py index 8724292..1c3e9b4 100644 --- a/lector/widgets.py +++ b/lector/widgets.py @@ -25,6 +25,7 @@ from PyQt5 import QtWidgets, QtGui, QtCore from lector.models import BookmarkProxyModel from lector.sorter import resize_image +from lector.threaded import BackGroundTextSearch from lector.contentwidgets import PliantQGraphicsView, PliantQTextBrowser @@ -156,6 +157,7 @@ class Tab(QtWidgets.QWidget): # Search view and model self.searchLineEdit = QtWidgets.QLineEdit(self.sideDockTabWidget) self.searchLineEdit.setFocusPolicy(QtCore.Qt.StrongFocus) + self.searchLineEdit.setClearButtonEnabled(True) search_string = self._translate('Tab', 'Search') self.searchLineEdit.setPlaceholderText(search_string) @@ -190,13 +192,14 @@ class Tab(QtWidgets.QWidget): self.searchOptionsLayout.addWidget(self.caseSensitiveSearchButton) self.searchOptionsLayout.addWidget(self.matchWholeWordButton) - self.searchResultsListView = QtWidgets.QListView(self.sideDockTabWidget) - self.searchResultsListView.setEditTriggers(QtWidgets.QListView.NoEditTriggers) - self.searchResultsListView.doubleClicked.connect(self.go_to_search_result) + self.searchResultsTreeView = QtWidgets.QTreeView(self.sideDockTabWidget) + self.searchResultsTreeView.setHeaderHidden(True) + self.searchResultsTreeView.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) + self.searchResultsTreeView.clicked.connect(self.navigate_to_search_result) self.searchTabLayout = QtWidgets.QVBoxLayout(self.sideDockTabWidget) self.searchTabLayout.addLayout(self.searchOptionsLayout) - self.searchTabLayout.addWidget(self.searchResultsListView) + self.searchTabLayout.addWidget(self.searchResultsTreeView) self.searchTabLayout.setContentsMargins(0, 0, 0, 0) self.searchTabWidget = QtWidgets.QWidget(self.sideDockTabWidget) self.searchTabWidget.setLayout(self.searchTabLayout) @@ -227,6 +230,23 @@ 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.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: title = title[:30] + '...' @@ -259,6 +279,7 @@ class Tab(QtWidgets.QWidget): if tab_required == 2: self.sideDock.activateWindow() self.searchLineEdit.setFocus() + self.searchLineEdit.selectAll() self.sideDockTabWidget.setCurrentIndex(tab_required) @@ -278,7 +299,7 @@ class Tab(QtWidgets.QWidget): except IndexError: # The file has been deleted pass - def set_cursor_position(self, cursor_position=None): + def set_cursor_position(self, cursor_position=None, select_chars=0): try: required_position = self.metadata['position']['cursor_position'] except KeyError: @@ -296,7 +317,13 @@ class Tab(QtWidgets.QWidget): # textCursor() RETURNS a copy of the textcursor cursor = self.contentView.textCursor() cursor.setPosition( - required_position, QtGui.QTextCursor.MoveAnchor) + required_position - select_chars, + QtGui.QTextCursor.MoveAnchor) + if select_chars > 0: # Select search results + cursor.movePosition( + QtGui.QTextCursor.NextCharacter, + QtGui.QTextCursor.KeepAnchor, + select_chars) self.contentView.setTextCursor(cursor) self.contentView.ensureCursorVisible() @@ -624,19 +651,6 @@ class Tab(QtWidgets.QWidget): self.bookmarkProxyModel.sort(0) self.bookmarkTreeView.setModel(self.bookmarkProxyModel) - def update_bookmark_proxy_model(self): - pass - - # TODO - # This isn't being called currently - # See if there's any rationale for keeping it / removing it - - # self.bookmarkProxyModel.invalidateFilter() - # self.bookmarkProxyModel.setFilterParams( - # self.main_window.bookToolBar.searchBar.text()) - # self.bookmarkProxyModel.setFilterFixedString( - # self.main_window.bookToolBar.searchBar.text()) - def generate_bookmark_context_menu(self, position): index = self.bookmarkTreeView.indexAt(position) if not index.isValid(): @@ -673,8 +687,56 @@ class Tab(QtWidgets.QWidget): if child_rows == 1: self.bookmarkModel.removeRow(parent_index.row()) - def go_to_search_result(self, event): - print(event) + def set_search_options(self): + search_content = ( + self.metadata['content'][self.main_window.bookToolBar.tocBox.currentIndex()],) + if self.searchBookButton.isChecked(): + search_content = self.metadata['content'] + + 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.setText(i) + parentItem.setData(True, QtCore.Qt.UserRole) + chapter_index = self.main_window.bookToolBar.tocBox.findText( + i, QtCore.Qt.MatchExactly) + + for j in search_results[i]: + childItem = QtGui.QStandardItem() + childItem.setText(j[1]) + childItem.setData(False, QtCore.Qt.UserRole) # Is parent? + childItem.setData(chapter_index, QtCore.Qt.UserRole + 1) # Chapter index + childItem.setData(j[0], QtCore.Qt.UserRole + 2) # Cursor Position + parentItem.appendRow(childItem) + self.searchResultsModel.appendRow(parentItem) + + self.searchResultsTreeView.setModel(self.searchResultsModel) + self.searchResultsTreeView.expandToDepth(1) + + 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_index = self.searchResultsModel.data(index, QtCore.Qt.UserRole + 1) + cursor_position = self.searchResultsModel.data(index, QtCore.Qt.UserRole + 2) + + self.main_window.bookToolBar.tocBox.setCurrentIndex(chapter_index) + if not self.are_we_doing_images_only: + self.set_cursor_position( + cursor_position, len(self.searchLineEdit.text())) def hide_mouse(self): self.contentView.viewport().setCursor(QtCore.Qt.BlankCursor)