diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py
index 0a36bc81..7ecd04d7 100644
--- a/novelwriter/core/coretools.py
+++ b/novelwriter/core/coretools.py
@@ -306,10 +306,10 @@ class DocDuplicator:
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._escape = not regEx
- self._words = wordsOnly and not regEx
+ self._words = wholeWords
self._rxOpts = QRegularExpression.PatternOption.UseUnicodePropertiesOption
if not doCase:
self._rxOpts |= QRegularExpression.PatternOption.CaseInsensitiveOption
@@ -354,7 +354,10 @@ class DocSearch:
else:
escaped += f"\\{c}"
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
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 8cd72429..6f6edfb6 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -637,6 +637,14 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Cursor moved to line %d", line)
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
##
@@ -791,11 +799,7 @@ class GuiDocEditor(QPlainTextEdit):
def anyFocus(self) -> bool:
"""Check if any widget or child widget has focus."""
- if self.hasFocus():
- return True
- if self.isAncestorOf(qApp.focusWidget()):
- return True
- return False
+ return self.hasFocus() or self.isAncestorOf(qApp.focusWidget())
def revealLocation(self) -> None:
"""Tell the user where on the file system the file in the editor
diff --git a/novelwriter/gui/search.py b/novelwriter/gui/search.py
index 3568a3f1..674008c4 100644
--- a/novelwriter/gui/search.py
+++ b/novelwriter/gui/search.py
@@ -25,7 +25,7 @@ from __future__ import annotations
import logging
-from PyQt5.QtCore import QSize, Qt, pyqtSlot
+from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import (
QHBoxLayout, QLabel, QLineEdit, QToolBar, QTreeWidget, QTreeWidgetItem,
@@ -33,6 +33,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
+from novelwriter.common import checkInt
from novelwriter.core.coretools import DocSearch
from novelwriter.core.item import NWItem
@@ -41,6 +42,11 @@ logger = logging.getLogger(__name__)
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:
super().__init__(parent=parent)
@@ -69,10 +75,11 @@ class GuiProjectSearch(QWidget):
self.toggleRegEx = self.searchOpt.addAction(self.tr("RegEx Mode"))
self.toggleRegEx.setCheckable(True)
- # Controls
+ # Search Box
self.searchText = QLineEdit(self)
self.searchText.setPlaceholderText(self.tr("Search text ..."))
self.searchText.setClearButtonEnabled(True)
+
self.searchAction = self.searchText.addAction(
SHARED.theme.getIcon("search"), QLineEdit.ActionPosition.TrailingPosition
)
@@ -81,6 +88,9 @@ class GuiProjectSearch(QWidget):
# Search Result
self.searchResult = QTreeWidget(self)
self.searchResult.setHeaderHidden(True)
+ self.searchResult.setIconSize(QSize(iPx, iPx))
+ self.searchResult.setIndentation(iPx)
+ self.searchResult.itemPressed.connect(self._searchResultSelected)
# Assemble
self.headerBox = QHBoxLayout()
@@ -119,6 +129,12 @@ class GuiProjectSearch(QWidget):
return
+ def processReturn(self) -> None:
+ """Process a return key press forwarded from main GUI."""
+ if self.searchText.hasFocus():
+ self._processSearch()
+ return
+
##
# Private Slots
##
@@ -136,21 +152,40 @@ class GuiProjectSearch(QWidget):
self._appendResultSet(item, results)
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
##
- 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."""
if results:
+ tHandle = nwItem.itemHandle
+ docIcon = SHARED.theme.getItemIcon(
+ nwItem.itemType, nwItem.itemClass,
+ nwItem.itemLayout, nwItem.mainHeading
+ )
+
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 = []
- for start, end, context in results:
+ for start, length, context in results:
rItem = QTreeWidgetItem()
rItem.setText(0, context)
+ rItem.setData(0, self.D_RESULT, (tHandle, start, length))
rItems.append(rItem)
tItem.addChildren(rItems)
+ tItem.setExpanded(True)
self.searchResult.addTopLevelItem(tItem)
return
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 5983b2dc..c247c218 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -263,6 +263,8 @@ class GuiMain(QMainWindow):
self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
self.novelView.openDocumentRequest.connect(self._openDocument)
+ self.projSearch.openDocumentSelectRequest.connect(self._openDocumentSelection)
+
self.docEditor.editedStatusChanged.connect(self.mainStatus.updateDocumentStatus)
self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts)
self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
@@ -1150,6 +1152,13 @@ class GuiMain(QMainWindow):
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
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()
def _reloadViewer(self) -> None:
"""Reload the document in the viewer."""
@@ -1263,15 +1272,16 @@ class GuiMain(QMainWindow):
@pyqtSlot()
def _keyPressReturn(self) -> None:
- """Forward the return/enter keypress to the function that opens
- the currently selected item.
- """
- self.openSelectedItem()
+ """Process a return or enter keypress in the main window."""
+ if self.projStack.currentWidget() == self.projSearch:
+ self.projSearch.processReturn()
+ else:
+ self.openSelectedItem()
return
@pyqtSlot()
def _keyPressEscape(self) -> None:
- """Process escape keypress in the main window."""
+ """Process an escape keypress in the main window."""
if self.docEditor.docSearch.isVisible():
self.docEditor.closeSearch()
elif SHARED.focusMode:
diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py
index 48e4a06e..1aa0565f 100644
--- a/tests/test_gui/test_gui_outline.py
+++ b/tests/test_gui/test_gui_outline.py
@@ -237,7 +237,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, prjLipsum, fncPath, tstPat
selItem = outlineTree.topLevelItem(0)
outlineTree.setCurrentItem(selItem)
- assert outlineData.titleLabel.text() == "Title"
+ assert outlineData.titleLabel.text() == "Title"
assert outlineData.titleValue.text() == "Lorem Ipsum"
assert outlineData.fileValue.text() == "Lorem Ipsum"
assert outlineData.itemValue.text() == "Finished"
@@ -256,7 +256,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, prjLipsum, fncPath, tstPat
assert tHandle == "88243afbe5ed8"
assert sTitle == "T0001"
- assert outlineData.titleLabel.text() == "Scene"
+ assert outlineData.titleLabel.text() == "Scene"
assert outlineData.titleValue.text() == "Scene One"
assert outlineData.fileValue.text() == "Scene One"
assert outlineData.itemValue.text() == "Finished"
@@ -274,7 +274,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, prjLipsum, fncPath, tstPat
assert tHandle == "88243afbe5ed8"
assert sTitle == "T0002"
- assert outlineData.titleLabel.text() == "Section"
+ assert outlineData.titleLabel.text() == "Section"
assert outlineData.titleValue.text() == "Scene One, Section Two"
assert outlineData.fileValue.text() == "Scene One"
assert outlineData.itemValue.text() == "Finished"