Complete handling of search results

This commit is contained in:
Veronica Berglyd Olsen
2024-03-24 15:56:22 +01:00
parent 026aeb3d82
commit becb404203
5 changed files with 73 additions and 21 deletions
+6 -3
View File
@@ -306,10 +306,10 @@ class DocDuplicator:
class DocSearch: class DocSearch:
def __init__(self, project: NWProject, regEx: bool, doCase: bool, wordsOnly: bool) -> None: def __init__(self, project: NWProject, regEx: bool, doCase: bool, wholeWords: bool) -> None:
self._project = project self._project = project
self._escape = not regEx self._escape = not regEx
self._words = wordsOnly and not regEx self._words = wholeWords
self._rxOpts = QRegularExpression.PatternOption.UseUnicodePropertiesOption self._rxOpts = QRegularExpression.PatternOption.UseUnicodePropertiesOption
if not doCase: if not doCase:
self._rxOpts |= QRegularExpression.PatternOption.CaseInsensitiveOption self._rxOpts |= QRegularExpression.PatternOption.CaseInsensitiveOption
@@ -354,7 +354,10 @@ class DocSearch:
else: else:
escaped += f"\\{c}" escaped += f"\\{c}"
search = escaped search = escaped
return f"\\b{search}\\b" if self._words else search if self._words:
search = search if search.startswith("\\b") else f"\\b{search}"
search = search if search.endswith("\\b") else f"{search}\\b"
return search
# END Class DocSearch # END Class DocSearch
+9 -5
View File
@@ -637,6 +637,14 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Cursor moved to line %d", line) logger.debug("Cursor moved to line %d", line)
return return
def setCursorSelection(self, selStart: int, selLength: int) -> None:
"""Make a text selection."""
cursor = self.textCursor()
cursor.setPosition(selStart, QTextCursor.MoveMode.MoveAnchor)
cursor.setPosition(selStart + selLength, QTextCursor.MoveMode.KeepAnchor)
self.setTextCursor(cursor)
return
## ##
# Spell Checking # Spell Checking
## ##
@@ -791,11 +799,7 @@ class GuiDocEditor(QPlainTextEdit):
def anyFocus(self) -> bool: def anyFocus(self) -> bool:
"""Check if any widget or child widget has focus.""" """Check if any widget or child widget has focus."""
if self.hasFocus(): return self.hasFocus() or self.isAncestorOf(qApp.focusWidget())
return True
if self.isAncestorOf(qApp.focusWidget()):
return True
return False
def revealLocation(self) -> None: def revealLocation(self) -> None:
"""Tell the user where on the file system the file in the editor """Tell the user where on the file system the file in the editor
+40 -5
View File
@@ -25,7 +25,7 @@ from __future__ import annotations
import logging import logging
from PyQt5.QtCore import QSize, Qt, pyqtSlot from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QPalette from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QHBoxLayout, QLabel, QLineEdit, QToolBar, QTreeWidget, QTreeWidgetItem, QHBoxLayout, QLabel, QLineEdit, QToolBar, QTreeWidget, QTreeWidgetItem,
@@ -33,6 +33,7 @@ from PyQt5.QtWidgets import (
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import checkInt
from novelwriter.core.coretools import DocSearch from novelwriter.core.coretools import DocSearch
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
@@ -41,6 +42,11 @@ logger = logging.getLogger(__name__)
class GuiProjectSearch(QWidget): class GuiProjectSearch(QWidget):
D_HANDLE = Qt.ItemDataRole.UserRole
D_RESULT = Qt.ItemDataRole.UserRole + 1
openDocumentSelectRequest = pyqtSignal(str, int, int)
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -69,10 +75,11 @@ class GuiProjectSearch(QWidget):
self.toggleRegEx = self.searchOpt.addAction(self.tr("RegEx Mode")) self.toggleRegEx = self.searchOpt.addAction(self.tr("RegEx Mode"))
self.toggleRegEx.setCheckable(True) self.toggleRegEx.setCheckable(True)
# Controls # Search Box
self.searchText = QLineEdit(self) self.searchText = QLineEdit(self)
self.searchText.setPlaceholderText(self.tr("Search text ...")) self.searchText.setPlaceholderText(self.tr("Search text ..."))
self.searchText.setClearButtonEnabled(True) self.searchText.setClearButtonEnabled(True)
self.searchAction = self.searchText.addAction( self.searchAction = self.searchText.addAction(
SHARED.theme.getIcon("search"), QLineEdit.ActionPosition.TrailingPosition SHARED.theme.getIcon("search"), QLineEdit.ActionPosition.TrailingPosition
) )
@@ -81,6 +88,9 @@ class GuiProjectSearch(QWidget):
# Search Result # Search Result
self.searchResult = QTreeWidget(self) self.searchResult = QTreeWidget(self)
self.searchResult.setHeaderHidden(True) self.searchResult.setHeaderHidden(True)
self.searchResult.setIconSize(QSize(iPx, iPx))
self.searchResult.setIndentation(iPx)
self.searchResult.itemPressed.connect(self._searchResultSelected)
# Assemble # Assemble
self.headerBox = QHBoxLayout() self.headerBox = QHBoxLayout()
@@ -119,6 +129,12 @@ class GuiProjectSearch(QWidget):
return return
def processReturn(self) -> None:
"""Process a return key press forwarded from main GUI."""
if self.searchText.hasFocus():
self._processSearch()
return
## ##
# Private Slots # Private Slots
## ##
@@ -136,21 +152,40 @@ class GuiProjectSearch(QWidget):
self._appendResultSet(item, results) self._appendResultSet(item, results)
return return
@pyqtSlot("QTreeWidgetItem*", int)
def _searchResultSelected(self, item: QTreeWidgetItem, column: int) -> None:
"""Process search result selection."""
if (data := item.data(0, self.D_RESULT)) and len(data) == 3:
self.openDocumentSelectRequest.emit(
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1)
)
return
## ##
# Internal Functions # Internal Functions
## ##
def _appendResultSet(self, item: NWItem, results: list[tuple[int, int, str]]) -> None: def _appendResultSet(self, nwItem: NWItem, results: list[tuple[int, int, str]]) -> None:
"""Populate the result tree.""" """Populate the result tree."""
if results: if results:
tHandle = nwItem.itemHandle
docIcon = SHARED.theme.getItemIcon(
nwItem.itemType, nwItem.itemClass,
nwItem.itemLayout, nwItem.mainHeading
)
tItem = QTreeWidgetItem() tItem = QTreeWidgetItem()
tItem.setText(0, f"{item.itemName} ({len(results)})") tItem.setText(0, f"{nwItem.itemName} ({len(results)})")
tItem.setIcon(0, docIcon)
tItem.setData(0, self.D_HANDLE, tHandle)
rItems = [] rItems = []
for start, end, context in results: for start, length, context in results:
rItem = QTreeWidgetItem() rItem = QTreeWidgetItem()
rItem.setText(0, context) rItem.setText(0, context)
rItem.setData(0, self.D_RESULT, (tHandle, start, length))
rItems.append(rItem) rItems.append(rItem)
tItem.addChildren(rItems) tItem.addChildren(rItems)
tItem.setExpanded(True)
self.searchResult.addTopLevelItem(tItem) self.searchResult.addTopLevelItem(tItem)
return return
+15 -5
View File
@@ -263,6 +263,8 @@ class GuiMain(QMainWindow):
self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox) self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
self.novelView.openDocumentRequest.connect(self._openDocument) self.novelView.openDocumentRequest.connect(self._openDocument)
self.projSearch.openDocumentSelectRequest.connect(self._openDocumentSelection)
self.docEditor.editedStatusChanged.connect(self.mainStatus.updateDocumentStatus) self.docEditor.editedStatusChanged.connect(self.mainStatus.updateDocumentStatus)
self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts) self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts)
self.docEditor.docCountsChanged.connect(self.projView.updateCounts) self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
@@ -1150,6 +1152,13 @@ class GuiMain(QMainWindow):
self.viewDocument(tHandle=tHandle, sTitle=sTitle) self.viewDocument(tHandle=tHandle, sTitle=sTitle)
return return
@pyqtSlot(str, int, int)
def _openDocumentSelection(self, tHandle: str, selStart: int, selLength: int) -> None:
"""Open a document and select a section of the text."""
if self.openDocument(tHandle):
self.docEditor.setCursorSelection(selStart, selLength)
return
@pyqtSlot() @pyqtSlot()
def _reloadViewer(self) -> None: def _reloadViewer(self) -> None:
"""Reload the document in the viewer.""" """Reload the document in the viewer."""
@@ -1263,15 +1272,16 @@ class GuiMain(QMainWindow):
@pyqtSlot() @pyqtSlot()
def _keyPressReturn(self) -> None: def _keyPressReturn(self) -> None:
"""Forward the return/enter keypress to the function that opens """Process a return or enter keypress in the main window."""
the currently selected item. if self.projStack.currentWidget() == self.projSearch:
""" self.projSearch.processReturn()
self.openSelectedItem() else:
self.openSelectedItem()
return return
@pyqtSlot() @pyqtSlot()
def _keyPressEscape(self) -> None: def _keyPressEscape(self) -> None:
"""Process escape keypress in the main window.""" """Process an escape keypress in the main window."""
if self.docEditor.docSearch.isVisible(): if self.docEditor.docSearch.isVisible():
self.docEditor.closeSearch() self.docEditor.closeSearch()
elif SHARED.focusMode: elif SHARED.focusMode:
+3 -3
View File
@@ -237,7 +237,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, prjLipsum, fncPath, tstPat
selItem = outlineTree.topLevelItem(0) selItem = outlineTree.topLevelItem(0)
outlineTree.setCurrentItem(selItem) outlineTree.setCurrentItem(selItem)
assert outlineData.titleLabel.text() == "<b>Title</b>" assert outlineData.titleLabel.text() == "Title"
assert outlineData.titleValue.text() == "Lorem Ipsum" assert outlineData.titleValue.text() == "Lorem Ipsum"
assert outlineData.fileValue.text() == "Lorem Ipsum" assert outlineData.fileValue.text() == "Lorem Ipsum"
assert outlineData.itemValue.text() == "Finished" assert outlineData.itemValue.text() == "Finished"
@@ -256,7 +256,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, prjLipsum, fncPath, tstPat
assert tHandle == "88243afbe5ed8" assert tHandle == "88243afbe5ed8"
assert sTitle == "T0001" assert sTitle == "T0001"
assert outlineData.titleLabel.text() == "<b>Scene</b>" assert outlineData.titleLabel.text() == "Scene"
assert outlineData.titleValue.text() == "Scene One" assert outlineData.titleValue.text() == "Scene One"
assert outlineData.fileValue.text() == "Scene One" assert outlineData.fileValue.text() == "Scene One"
assert outlineData.itemValue.text() == "Finished" assert outlineData.itemValue.text() == "Finished"
@@ -274,7 +274,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, prjLipsum, fncPath, tstPat
assert tHandle == "88243afbe5ed8" assert tHandle == "88243afbe5ed8"
assert sTitle == "T0002" assert sTitle == "T0002"
assert outlineData.titleLabel.text() == "<b>Section</b>" assert outlineData.titleLabel.text() == "Section"
assert outlineData.titleValue.text() == "Scene One, Section Two" assert outlineData.titleValue.text() == "Scene One, Section Two"
assert outlineData.fileValue.text() == "Scene One" assert outlineData.fileValue.text() == "Scene One"
assert outlineData.itemValue.text() == "Finished" assert outlineData.itemValue.text() == "Finished"