Make some GUI improvements

This commit is contained in:
Veronica Berglyd Olsen
2024-03-25 17:37:18 +01:00
parent 16954c3bba
commit 01a4ec8d76
3 changed files with 59 additions and 27 deletions
+11 -4
View File
@@ -38,7 +38,7 @@ from PyQt5.QtCore import QCoreApplication, QRegularExpression
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import isHandle, minmax, simplified from novelwriter.common import isHandle, minmax, simplified
from novelwriter.constants import nwFiles, nwItemClass from novelwriter.constants import nwConst, nwFiles, nwItemClass
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.core.storage import NWStorageCreate from novelwriter.core.storage import NWStorageCreate
@@ -353,7 +353,7 @@ class DocSearch:
def iterSearch( def iterSearch(
self, project: NWProject, search: str self, project: NWProject, search: str
) -> Iterable[tuple[NWItem, list[tuple[int, int, str]]]]: ) -> Iterable[tuple[NWItem, list[tuple[int, int, str]], bool]]:
"""Iteratively search through documents in a project.""" """Iteratively search through documents in a project."""
if project.data.uuid != self._uuid: if project.data.uuid != self._uuid:
self.clearTextCache(None) self.clearTextCache(None)
@@ -373,15 +373,22 @@ class DocSearch:
self._cache[tHandle] = text self._cache[tHandle] = text
rxItt = self._regEx.globalMatch(text) rxItt = self._regEx.globalMatch(text)
count = 0
capped = False
results = [] results = []
while rxItt.hasNext(): while rxItt.hasNext():
rxMatch = rxItt.next() rxMatch = rxItt.next()
pos = rxMatch.capturedStart() pos = rxMatch.capturedStart()
num = rxMatch.capturedLength() num = rxMatch.capturedLength()
context = text[pos:pos+100].partition("\n")[0] context = text[pos:pos+100].partition("\n")[0]
results.append((pos, num, context)) if context:
results.append((pos, num, context))
count += 1
if count >= nwConst.MAX_SEARCH_RESULT:
capped = True
break
yield item, results yield item, results, capped
return return
+47 -23
View File
@@ -23,18 +23,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
import time
import logging import logging
from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QKeyEvent, QPalette from PyQt5.QtGui import QKeyEvent, QPalette
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QHBoxLayout, QLabel, QLineEdit, QToolBar, QTreeWidget, QTreeWidgetItem, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QToolBar, QTreeWidget,
QVBoxLayout, QWidget, qApp QTreeWidgetItem, QVBoxLayout, QWidget, qApp
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import checkInt from novelwriter.common import checkInt
from novelwriter.constants import nwConst
from novelwriter.core.coretools import DocSearch from novelwriter.core.coretools import DocSearch
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
@@ -43,10 +43,15 @@ logger = logging.getLogger(__name__)
class GuiProjectSearch(QWidget): class GuiProjectSearch(QWidget):
C_NAME = 0
C_RESULT = 0
C_COUNT = 1
D_HANDLE = Qt.ItemDataRole.UserRole D_HANDLE = Qt.ItemDataRole.UserRole
D_RESULT = Qt.ItemDataRole.UserRole + 1 D_RESULT = Qt.ItemDataRole.UserRole + 1
openDocumentSelectRequest = pyqtSignal(str, int, int, bool) openDocumentSelectRequest = pyqtSignal(str, int, int, bool)
selectedItemChanged = pyqtSignal(str)
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -97,10 +102,17 @@ 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.setColumnCount(2)
self.searchResult.setIconSize(QSize(iPx, iPx)) self.searchResult.setIconSize(QSize(iPx, iPx))
self.searchResult.setIndentation(iPx) self.searchResult.setIndentation(iPx)
self.searchResult.itemPressed.connect(self._searchResultSelected) self.searchResult.itemPressed.connect(self._searchResultPressed)
self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked) self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked)
self.searchResult.itemSelectionChanged.connect(self._searchResultSelected)
treeHeader = self.searchResult.header()
treeHeader.setStretchLastSection(False)
treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.Stretch)
treeHeader.setSectionResizeMode(self.C_COUNT, QHeaderView.ResizeMode.ResizeToContents)
# Assemble # Assemble
self.headerBox = QHBoxLayout() self.headerBox = QHBoxLayout()
@@ -144,7 +156,7 @@ class GuiProjectSearch(QWidget):
if self.searchText.hasFocus(): if self.searchText.hasFocus():
self._processSearch() self._processSearch()
elif items := self.searchResult.selectedItems(): elif items := self.searchResult.selectedItems():
self._searchResultSelected(items[0], 0) self._searchResultPressed(items[0], 0)
return return
def beginSearch(self) -> None: def beginSearch(self) -> None:
@@ -203,18 +215,30 @@ class GuiProjectSearch(QWidget):
@pyqtSlot() @pyqtSlot()
def _processSearch(self) -> None: def _processSearch(self) -> None:
"""Perform a search.""" """Perform a search."""
start = time.time()
self.searchResult.clear() self.searchResult.clear()
if text := self.searchText.text(): if text := self.searchText.text():
self._search.setUserRegEx(self.toggleRegEx.isChecked()) self._search.setUserRegEx(self.toggleRegEx.isChecked())
self._search.setCaseSensitive(self.toggleCase.isChecked()) self._search.setCaseSensitive(self.toggleCase.isChecked())
self._search.setWholeWords(self.toggleWord.isChecked()) self._search.setWholeWords(self.toggleWord.isChecked())
for item, results in self._search.iterSearch(SHARED.project, text): for item, results, capped in self._search.iterSearch(SHARED.project, text):
self._appendResultSet(item, results) self._appendResultSet(item, results, capped)
logger.debug("Search took %.3f ms", 1000*(time.time() - start))
return
@pyqtSlot()
def _searchResultSelected(self) -> None:
"""Process search result selection."""
if items := self.searchResult.selectedItems():
if (data := items[0].data(0, self.D_RESULT)) and len(data) == 3:
self.selectedItemChanged.emit(str(data[0]))
elif data := items[0].data(0, self.D_HANDLE):
self.selectedItemChanged.emit(str(data))
return return
@pyqtSlot("QTreeWidgetItem*", int) @pyqtSlot("QTreeWidgetItem*", int)
def _searchResultSelected(self, item: QTreeWidgetItem, column: int) -> None: def _searchResultPressed(self, item: QTreeWidgetItem, column: int) -> None:
"""Process search result selection.""" """Process search result pressed."""
if (data := item.data(0, self.D_RESULT)) and len(data) == 3: if (data := item.data(0, self.D_RESULT)) and len(data) == 3:
self.openDocumentSelectRequest.emit( self.openDocumentSelectRequest.emit(
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False
@@ -252,11 +276,9 @@ class GuiProjectSearch(QWidget):
# Internal Functions # Internal Functions
## ##
def _initSearch(self) -> None: def _appendResultSet(
"""Initialise the search.""" self, nwItem: NWItem, results: list[tuple[int, int, str]], capped: bool
return ) -> 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 tHandle = nwItem.itemHandle
@@ -264,17 +286,15 @@ class GuiProjectSearch(QWidget):
nwItem.itemType, nwItem.itemClass, nwItem.itemType, nwItem.itemClass,
nwItem.itemLayout, nwItem.mainHeading nwItem.itemLayout, nwItem.mainHeading
) )
lim = nwConst.MAX_SEARCH_RESULT ext = "+" if capped else ""
count = len(results)
numResult = f"{count:n}"
if count > lim:
results = results[:lim]
numResult = f"{lim:n}+"
tItem = QTreeWidgetItem() tItem = QTreeWidgetItem()
tItem.setText(0, f"{nwItem.itemName} ({numResult})") tItem.setText(self.C_NAME, nwItem.itemName)
tItem.setIcon(0, docIcon) tItem.setIcon(self.C_NAME, docIcon)
tItem.setData(0, self.D_HANDLE, tHandle) tItem.setData(self.C_NAME, self.D_HANDLE, tHandle)
tItem.setText(self.C_COUNT, f"({len(results):n}{ext})")
tItem.setTextAlignment(self.C_COUNT, Qt.AlignmentFlag.AlignRight)
tItem.setForeground(self.C_COUNT, self.palette().highlight())
self.searchResult.addTopLevelItem(tItem) self.searchResult.addTopLevelItem(tItem)
rItems = [] rItems = []
@@ -287,6 +307,10 @@ class GuiProjectSearch(QWidget):
tItem.addChildren(rItems) tItem.addChildren(rItems)
tItem.setExpanded(True) tItem.setExpanded(True)
parent = self.searchResult.indexFromItem(tItem)
for i in range(tItem.childCount()):
self.searchResult.setFirstColumnSpanned(i, parent, True)
qApp.processEvents() qApp.processEvents()
return return
+1
View File
@@ -246,6 +246,7 @@ class GuiMain(QMainWindow):
self.novelView.openDocumentRequest.connect(self._openDocument) self.novelView.openDocumentRequest.connect(self._openDocument)
self.projSearch.openDocumentSelectRequest.connect(self._openDocumentSelection) self.projSearch.openDocumentSelectRequest.connect(self._openDocumentSelection)
self.projSearch.selectedItemChanged.connect(self.itemDetails.updateViewBox)
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)