Complete handling of search results
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user