Add global search feature (#1775)
This commit is contained in:
@@ -108,6 +108,7 @@ view_build = typ_export-grey.svg
|
|||||||
view_editor = mixed_edit.svg
|
view_editor = mixed_edit.svg
|
||||||
view_novel = typ_book-grey.svg
|
view_novel = typ_book-grey.svg
|
||||||
view_outline = typ_puzzle-outline.svg
|
view_outline = typ_puzzle-outline.svg
|
||||||
|
view_search = typ_search-grey.svg
|
||||||
|
|
||||||
deco_doc_h0 = nw_deco-h0.svg
|
deco_doc_h0 = nw_deco-h0.svg
|
||||||
deco_doc_h0_n = nw_deco-h0.svg
|
deco_doc_h0_n = nw_deco-h0.svg
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m20.547 14.707-0.7017-0.703-0.98054-0.98185c0.29717-0.90068 0.46343-1.859 0.46343-2.8578 0-5.0519-4.112-9.1639-9.1639-9.1639s-9.1639 4.112-9.1639 9.1639c0 5.0519 4.112 9.1639 9.1639 9.1639 0.99887 0 1.9585-0.16626 2.8591-0.46343l0.98185 0.98054 1.9794 1.9768 0.07986 0.07986 0.08378 0.072c0.78679 0.66242 1.7647 1.0264 2.7544 1.0264 2.2596 0 4.0976-1.838 4.0976-4.0989 0-1.0997-0.4294-2.1313-1.2096-2.9037zm-16.93-4.5427c0-3.6093 2.9364-6.5457 6.5457-6.5457s6.5457 2.9364 6.5457 6.5457-2.9364 6.5457-6.5457 6.5457-6.5457-2.9364-6.5457-6.5457z" fill="#aeaeae" stroke-width="1.3091"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 737 B |
@@ -108,6 +108,7 @@ view_build = typ_export-grey.svg
|
|||||||
view_editor = mixed_edit.svg
|
view_editor = mixed_edit.svg
|
||||||
view_novel = typ_book-grey.svg
|
view_novel = typ_book-grey.svg
|
||||||
view_outline = typ_puzzle-outline.svg
|
view_outline = typ_puzzle-outline.svg
|
||||||
|
view_search = typ_search-grey.svg
|
||||||
|
|
||||||
deco_doc_h0 = nw_deco-h0.svg
|
deco_doc_h0 = nw_deco-h0.svg
|
||||||
deco_doc_h0_n = nw_deco-h0.svg
|
deco_doc_h0_n = nw_deco-h0.svg
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m20.547 14.707-0.7017-0.703-0.98054-0.98185c0.29717-0.90068 0.46343-1.859 0.46343-2.8578 0-5.0519-4.112-9.1639-9.1639-9.1639s-9.1639 4.112-9.1639 9.1639c0 5.0519 4.112 9.1639 9.1639 9.1639 0.99887 0 1.9585-0.16626 2.8591-0.46343l0.98185 0.98054 1.9794 1.9768 0.07986 0.07986 0.08378 0.072c0.78679 0.66242 1.7647 1.0264 2.7544 1.0264 2.2596 0 4.0976-1.838 4.0976-4.0989 0-1.0997-0.4294-2.1313-1.2096-2.9037zm-16.93-4.5427c0-3.6093 2.9364-6.5457 6.5457-6.5457s6.5457 2.9364 6.5457 6.5457-2.9364 6.5457-6.5457 6.5457-6.5457-2.9364-6.5457-6.5457z" fill-opacity=".78039" stroke-width="1.3091"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 744 B |
+16
-10
@@ -183,17 +183,19 @@ class Config:
|
|||||||
# State
|
# State
|
||||||
self.showViewerPanel = True # The panel for the viewer is visible
|
self.showViewerPanel = True # The panel for the viewer is visible
|
||||||
self.showEditToolBar = False # The document editor toolbar visibility
|
self.showEditToolBar = False # The document editor toolbar visibility
|
||||||
self.useShortcodes = False # Use shortcodes for basic formatting
|
|
||||||
self.viewComments = True # Comments are shown in the viewer
|
self.viewComments = True # Comments are shown in the viewer
|
||||||
self.viewSynopsis = True # Synopsis is shown in the viewer
|
self.viewSynopsis = True # Synopsis is shown in the viewer
|
||||||
|
|
||||||
# Search Bar Switches
|
# Search Box States
|
||||||
self.searchCase = False
|
self.searchCase = False
|
||||||
self.searchWord = False
|
self.searchWord = False
|
||||||
self.searchRegEx = False
|
self.searchRegEx = False
|
||||||
self.searchLoop = False
|
self.searchLoop = False
|
||||||
self.searchNextFile = False
|
self.searchNextFile = False
|
||||||
self.searchMatchCap = False
|
self.searchMatchCap = False
|
||||||
|
self.searchProjCase = False
|
||||||
|
self.searchProjWord = False
|
||||||
|
self.searchProjRegEx = False
|
||||||
|
|
||||||
# System and App Information
|
# System and App Information
|
||||||
# ==========================
|
# ==========================
|
||||||
@@ -619,7 +621,6 @@ class Config:
|
|||||||
sec = "State"
|
sec = "State"
|
||||||
self.showViewerPanel = conf.rdBool(sec, "showviewerpanel", self.showViewerPanel)
|
self.showViewerPanel = conf.rdBool(sec, "showviewerpanel", self.showViewerPanel)
|
||||||
self.showEditToolBar = conf.rdBool(sec, "showedittoolbar", self.showEditToolBar)
|
self.showEditToolBar = conf.rdBool(sec, "showedittoolbar", self.showEditToolBar)
|
||||||
self.useShortcodes = conf.rdBool(sec, "useshortcodes", self.useShortcodes)
|
|
||||||
self.viewComments = conf.rdBool(sec, "viewcomments", self.viewComments)
|
self.viewComments = conf.rdBool(sec, "viewcomments", self.viewComments)
|
||||||
self.viewSynopsis = conf.rdBool(sec, "viewsynopsis", self.viewSynopsis)
|
self.viewSynopsis = conf.rdBool(sec, "viewsynopsis", self.viewSynopsis)
|
||||||
self.searchCase = conf.rdBool(sec, "searchcase", self.searchCase)
|
self.searchCase = conf.rdBool(sec, "searchcase", self.searchCase)
|
||||||
@@ -628,6 +629,9 @@ class Config:
|
|||||||
self.searchLoop = conf.rdBool(sec, "searchloop", self.searchLoop)
|
self.searchLoop = conf.rdBool(sec, "searchloop", self.searchLoop)
|
||||||
self.searchNextFile = conf.rdBool(sec, "searchnextfile", self.searchNextFile)
|
self.searchNextFile = conf.rdBool(sec, "searchnextfile", self.searchNextFile)
|
||||||
self.searchMatchCap = conf.rdBool(sec, "searchmatchcap", self.searchMatchCap)
|
self.searchMatchCap = conf.rdBool(sec, "searchmatchcap", self.searchMatchCap)
|
||||||
|
self.searchProjCase = conf.rdBool(sec, "searchprojcase", self.searchProjCase)
|
||||||
|
self.searchProjWord = conf.rdBool(sec, "searchprojword", self.searchProjWord)
|
||||||
|
self.searchProjRegEx = conf.rdBool(sec, "searchprojregex", self.searchProjRegEx)
|
||||||
|
|
||||||
# Check Values
|
# Check Values
|
||||||
# ============
|
# ============
|
||||||
@@ -725,7 +729,6 @@ class Config:
|
|||||||
conf["State"] = {
|
conf["State"] = {
|
||||||
"showviewerpanel": str(self.showViewerPanel),
|
"showviewerpanel": str(self.showViewerPanel),
|
||||||
"showedittoolbar": str(self.showEditToolBar),
|
"showedittoolbar": str(self.showEditToolBar),
|
||||||
"useshortcodes": str(self.useShortcodes),
|
|
||||||
"viewcomments": str(self.viewComments),
|
"viewcomments": str(self.viewComments),
|
||||||
"viewsynopsis": str(self.viewSynopsis),
|
"viewsynopsis": str(self.viewSynopsis),
|
||||||
"searchcase": str(self.searchCase),
|
"searchcase": str(self.searchCase),
|
||||||
@@ -734,6 +737,9 @@ class Config:
|
|||||||
"searchloop": str(self.searchLoop),
|
"searchloop": str(self.searchLoop),
|
||||||
"searchnextfile": str(self.searchNextFile),
|
"searchnextfile": str(self.searchNextFile),
|
||||||
"searchmatchcap": str(self.searchMatchCap),
|
"searchmatchcap": str(self.searchMatchCap),
|
||||||
|
"searchprojcase": str(self.searchProjCase),
|
||||||
|
"searchprojword": str(self.searchProjWord),
|
||||||
|
"searchprojregex": str(self.searchProjRegEx),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Write config file
|
# Write config file
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class nwConst:
|
|||||||
|
|
||||||
# Gui Settings
|
# Gui Settings
|
||||||
STATUS_MSG_TIMEOUT = 15000 # milliseconds
|
STATUS_MSG_TIMEOUT = 15000 # milliseconds
|
||||||
|
MAX_SEARCH_RESULT = 1000
|
||||||
|
|
||||||
# Dialogs
|
# Dialogs
|
||||||
DLG_FINISHED = 2
|
DLG_FINISHED = 2
|
||||||
|
|||||||
@@ -26,19 +26,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import shutil
|
|
||||||
import logging
|
import logging
|
||||||
|
import shutil
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from functools import partial
|
|
||||||
from zipfile import ZipFile, is_zipfile
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
|
from functools import partial
|
||||||
|
from pathlib import Path
|
||||||
|
from time import time
|
||||||
|
from zipfile import ZipFile, is_zipfile
|
||||||
|
|
||||||
from PyQt5.QtCore import QCoreApplication
|
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
|
||||||
@@ -304,6 +305,111 @@ class DocDuplicator:
|
|||||||
# END Class DocDuplicator
|
# END Class DocDuplicator
|
||||||
|
|
||||||
|
|
||||||
|
class DocSearch:
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# RegEx Object
|
||||||
|
self._regEx = QRegularExpression()
|
||||||
|
self.setCaseSensitive(False)
|
||||||
|
self._words = False
|
||||||
|
self._escape = True
|
||||||
|
|
||||||
|
# Project Cache
|
||||||
|
self._uuid = ""
|
||||||
|
self._time = 0.0
|
||||||
|
self._cache: dict[str, str] = {}
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Methods
|
||||||
|
##
|
||||||
|
|
||||||
|
def setCaseSensitive(self, state: bool) -> None:
|
||||||
|
"""Set the case sensitive search flag."""
|
||||||
|
opts = QRegularExpression.PatternOption.UseUnicodePropertiesOption
|
||||||
|
if not state:
|
||||||
|
opts |= QRegularExpression.PatternOption.CaseInsensitiveOption
|
||||||
|
self._regEx.setPatternOptions(opts)
|
||||||
|
return
|
||||||
|
|
||||||
|
def setWholeWords(self, state: bool) -> None:
|
||||||
|
"""Set the whole words search flag."""
|
||||||
|
self._words = state
|
||||||
|
return
|
||||||
|
|
||||||
|
def setUserRegEx(self, state: bool) -> None:
|
||||||
|
"""Set the escape flag to the opposite state."""
|
||||||
|
self._escape = not state
|
||||||
|
return
|
||||||
|
|
||||||
|
def iterSearch(
|
||||||
|
self, project: NWProject, search: str
|
||||||
|
) -> Iterable[tuple[NWItem, list[tuple[int, int, str]], bool]]:
|
||||||
|
"""Iteratively search through documents in a project."""
|
||||||
|
if project.data.uuid != self._uuid or time() - self._time > 20.0:
|
||||||
|
self._cache = {}
|
||||||
|
|
||||||
|
self._uuid = project.data.uuid
|
||||||
|
self._time = time()
|
||||||
|
self._regEx.setPattern(self._buildPattern(search))
|
||||||
|
logger.debug("Searching with pattern '%s'", self._regEx.pattern())
|
||||||
|
|
||||||
|
num = len(search)
|
||||||
|
storage = project.storage
|
||||||
|
for item in project.tree:
|
||||||
|
if item.isFileType():
|
||||||
|
tHandle = item.itemHandle
|
||||||
|
if (text := self._cache.get(tHandle)) is None:
|
||||||
|
text = storage.getDocument(tHandle).readDocument() or ""
|
||||||
|
self._cache[tHandle] = text
|
||||||
|
|
||||||
|
rxItt = self._regEx.globalMatch(text)
|
||||||
|
count = 0
|
||||||
|
capped = False
|
||||||
|
results = []
|
||||||
|
while rxItt.hasNext():
|
||||||
|
rxMatch = rxItt.next()
|
||||||
|
pos = rxMatch.capturedStart()
|
||||||
|
num = rxMatch.capturedLength()
|
||||||
|
context = text[pos:pos+100].partition("\n")[0]
|
||||||
|
if context:
|
||||||
|
results.append((pos, num, context))
|
||||||
|
count += 1
|
||||||
|
if count >= nwConst.MAX_SEARCH_RESULT:
|
||||||
|
capped = True
|
||||||
|
break
|
||||||
|
|
||||||
|
yield item, results, capped
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Internal Functions
|
||||||
|
##
|
||||||
|
|
||||||
|
def _buildPattern(self, search: str) -> str:
|
||||||
|
"""Build the search pattern string."""
|
||||||
|
if self._escape:
|
||||||
|
if CONFIG.verQtValue >= 0x050f00:
|
||||||
|
search = QRegularExpression.escape(search)
|
||||||
|
else:
|
||||||
|
# For older Qt versions, we escape manually
|
||||||
|
escaped = ""
|
||||||
|
for c in search:
|
||||||
|
if c.isalnum() or c == "_":
|
||||||
|
escaped += c
|
||||||
|
else:
|
||||||
|
escaped += f"\\{c}"
|
||||||
|
search = escaped
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
class ProjectBuilder:
|
class ProjectBuilder:
|
||||||
"""A class to build a new project from a set of user-defined
|
"""A class to build a new project from a set of user-defined
|
||||||
parameter provided by the New Project Wizard.
|
parameter provided by the New Project Wizard.
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ class nwView(Enum):
|
|||||||
PROJECT = 1
|
PROJECT = 1
|
||||||
NOVEL = 2
|
NOVEL = 2
|
||||||
OUTLINE = 3
|
OUTLINE = 3
|
||||||
|
SEARCH = 4
|
||||||
|
|
||||||
# END Enum nwView
|
# END Enum nwView
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ from PyQt5.QtWidgets import (
|
|||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
from novelwriter.common import minmax, transferCase
|
from novelwriter.common import minmax, transferCase
|
||||||
from novelwriter.constants import nwKeyWords, nwShortcode, nwUnicode
|
from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode
|
||||||
from novelwriter.core.document import NWDocument
|
from novelwriter.core.document import NWDocument
|
||||||
from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
|
from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
|
||||||
from novelwriter.extensions.eventfilters import WheelEventFilter
|
from novelwriter.extensions.eventfilters import WheelEventFilter
|
||||||
@@ -603,13 +603,15 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
# Setters
|
# Setters
|
||||||
##
|
##
|
||||||
|
|
||||||
def setDocumentChanged(self, state: bool) -> bool:
|
def setDocumentChanged(self, state: bool) -> None:
|
||||||
"""Keep track of the document changed variable, and emit the
|
"""Keep track of the document changed variable, and emit the
|
||||||
document change signal.
|
document change signal.
|
||||||
"""
|
"""
|
||||||
self._docChanged = state
|
if self._docChanged != state:
|
||||||
self.editedStatusChanged.emit(self._docChanged)
|
logger.debug("Document changed status is '%s'", state)
|
||||||
return self._docChanged
|
self._docChanged = state
|
||||||
|
self.editedStatusChanged.emit(self._docChanged)
|
||||||
|
return
|
||||||
|
|
||||||
def setCursorPosition(self, position: int) -> None:
|
def setCursorPosition(self, position: int) -> None:
|
||||||
"""Move the cursor to a given position in the document."""
|
"""Move the cursor to a given position in the document."""
|
||||||
@@ -637,6 +639,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 +801,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
|
||||||
@@ -1368,9 +1374,10 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
cursor.setPosition(0)
|
cursor.setPosition(0)
|
||||||
self.setTextCursor(cursor)
|
self.setTextCursor(cursor)
|
||||||
|
|
||||||
# Search up to a maximum of 1000, and make sure certain special
|
# Search up to a maximum of MAX_SEARCH_RESULT, and make sure
|
||||||
# searches like a regex search for .* don't loop infinitely
|
# certain special searches like a regex search for .* don't loop
|
||||||
while self.find(searchFor, findOpt) and len(resE) <= 1000:
|
# infinitely
|
||||||
|
while self.find(searchFor, findOpt) and len(resE) <= nwConst.MAX_SEARCH_RESULT:
|
||||||
cursor = self.textCursor()
|
cursor = self.textCursor()
|
||||||
if cursor.hasSelection():
|
if cursor.hasSelection():
|
||||||
resS.append(cursor.selectionStart())
|
resS.append(cursor.selectionStart())
|
||||||
@@ -2609,8 +2616,10 @@ class GuiDocEditSearch(QFrame):
|
|||||||
|
|
||||||
def setResultCount(self, currRes: int | None, resCount: int | None) -> None:
|
def setResultCount(self, currRes: int | None, resCount: int | None) -> None:
|
||||||
"""Set the count values for the current search."""
|
"""Set the count values for the current search."""
|
||||||
|
lim = nwConst.MAX_SEARCH_RESULT
|
||||||
|
numCount = f"{lim:n}+" if (resCount or 0) > lim else f"{resCount:n}"
|
||||||
sCurrRes = "?" if currRes is None else str(currRes)
|
sCurrRes = "?" if currRes is None else str(currRes)
|
||||||
sResCount = "?" if resCount is None else "1000+" if resCount > 1000 else str(resCount)
|
sResCount = "?" if resCount is None else numCount
|
||||||
minWidth = SHARED.theme.getTextWidth(f"{sResCount}//{sResCount}", self.boxFont)
|
minWidth = SHARED.theme.getTextWidth(f"{sResCount}//{sResCount}", self.boxFont)
|
||||||
self.resultLabel.setText(f"{sCurrRes}/{sResCount}")
|
self.resultLabel.setText(f"{sCurrRes}/{sResCount}")
|
||||||
self.resultLabel.setMinimumWidth(minWidth)
|
self.resultLabel.setMinimumWidth(minWidth)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class GuiItemDetails(QWidget):
|
|||||||
logger.debug("Create: GuiItemDetails")
|
logger.debug("Create: GuiItemDetails")
|
||||||
|
|
||||||
# Internal Variables
|
# Internal Variables
|
||||||
self._itemHandle = None
|
self._handle = None
|
||||||
|
|
||||||
# Sizes
|
# Sizes
|
||||||
hSp = CONFIG.pxInt(6)
|
hSp = CONFIG.pxInt(6)
|
||||||
@@ -194,7 +194,7 @@ class GuiItemDetails(QWidget):
|
|||||||
|
|
||||||
def clearDetails(self) -> None:
|
def clearDetails(self) -> None:
|
||||||
"""Clear all the data values."""
|
"""Clear all the data values."""
|
||||||
self._itemHandle = None
|
self._handle = None
|
||||||
self.labelIcon.clear()
|
self.labelIcon.clear()
|
||||||
self.labelData.clear()
|
self.labelData.clear()
|
||||||
self.statusIcon.clear()
|
self.statusIcon.clear()
|
||||||
@@ -210,11 +210,11 @@ class GuiItemDetails(QWidget):
|
|||||||
|
|
||||||
def refreshDetails(self) -> None:
|
def refreshDetails(self) -> None:
|
||||||
"""Reload the content of the details panel."""
|
"""Reload the content of the details panel."""
|
||||||
self.updateViewBox(self._itemHandle)
|
self.updateViewBox(self._handle)
|
||||||
|
|
||||||
def updateTheme(self) -> None:
|
def updateTheme(self) -> None:
|
||||||
"""Update theme elements."""
|
"""Update theme elements."""
|
||||||
self.updateViewBox(self._itemHandle)
|
self.updateViewBox(self._handle)
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -233,7 +233,7 @@ class GuiItemDetails(QWidget):
|
|||||||
self.clearDetails()
|
self.clearDetails()
|
||||||
return
|
return
|
||||||
|
|
||||||
self._itemHandle = tHandle
|
self._handle = tHandle
|
||||||
iPx = int(round(0.8*SHARED.theme.baseIconSize))
|
iPx = int(round(0.8*SHARED.theme.baseIconSize))
|
||||||
|
|
||||||
# Label
|
# Label
|
||||||
@@ -295,7 +295,7 @@ class GuiItemDetails(QWidget):
|
|||||||
"""Update the counts if the handle is the same as the one we're
|
"""Update the counts if the handle is the same as the one we're
|
||||||
already showing. Otherwise, do nothing.
|
already showing. Otherwise, do nothing.
|
||||||
"""
|
"""
|
||||||
if tHandle == self._itemHandle:
|
if tHandle == self._handle:
|
||||||
self.cCountData.setText(f"{cC:n}")
|
self.cCountData.setText(f"{cC:n}")
|
||||||
self.wCountData.setText(f"{wC:n}")
|
self.wCountData.setText(f"{wC:n}")
|
||||||
self.pCountData.setText(f"{pC:n}")
|
self.pCountData.setText(f"{pC:n}")
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ from PyQt5.QtCore import QUrl, pyqtSignal, pyqtSlot
|
|||||||
from PyQt5.QtWidgets import QMenuBar, QAction
|
from PyQt5.QtWidgets import QMenuBar, QAction
|
||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget
|
|
||||||
from novelwriter.common import openExternalPath
|
from novelwriter.common import openExternalPath
|
||||||
from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode
|
from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode
|
||||||
|
from novelwriter.enum import nwDocAction, nwDocInsert, nwView, nwWidget
|
||||||
from novelwriter.extensions.eventfilters import StatusTipFilter
|
from novelwriter.extensions.eventfilters import StatusTipFilter
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
@@ -55,6 +55,7 @@ class GuiMainMenu(QMenuBar):
|
|||||||
requestDocInsertText = pyqtSignal(str)
|
requestDocInsertText = pyqtSignal(str)
|
||||||
requestDocKeyWordInsert = pyqtSignal(str)
|
requestDocKeyWordInsert = pyqtSignal(str)
|
||||||
requestFocusChange = pyqtSignal(nwWidget)
|
requestFocusChange = pyqtSignal(nwWidget)
|
||||||
|
requestViewChange = pyqtSignal(nwView)
|
||||||
|
|
||||||
def __init__(self, mainGui: GuiMain) -> None:
|
def __init__(self, mainGui: GuiMain) -> None:
|
||||||
super().__init__(parent=mainGui)
|
super().__init__(parent=mainGui)
|
||||||
@@ -861,6 +862,14 @@ class GuiMainMenu(QMenuBar):
|
|||||||
self.aReplaceNext.setShortcut("Ctrl+Shift+1")
|
self.aReplaceNext.setShortcut("Ctrl+Shift+1")
|
||||||
self.aReplaceNext.triggered.connect(lambda: self.mainGui.docEditor.replaceNext())
|
self.aReplaceNext.triggered.connect(lambda: self.mainGui.docEditor.replaceNext())
|
||||||
|
|
||||||
|
# Search > Separator
|
||||||
|
self.srcMenu.addSeparator()
|
||||||
|
|
||||||
|
# Search > Find in Project
|
||||||
|
self.aFindProj = self.srcMenu.addAction(self.tr("Find in Project"))
|
||||||
|
self.aFindProj.setShortcut("Ctrl+Shift+F")
|
||||||
|
self.aFindProj.triggered.connect(lambda: self.requestViewChange.emit(nwView.SEARCH))
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def _buildToolsMenu(self) -> None:
|
def _buildToolsMenu(self) -> None:
|
||||||
|
|||||||
+42
-19
@@ -798,14 +798,20 @@ class GuiOutlineDetails(QScrollArea):
|
|||||||
hSpace = int(CONFIG.pxInt(10))
|
hSpace = int(CONFIG.pxInt(10))
|
||||||
vSpace = int(CONFIG.pxInt(4))
|
vSpace = int(CONFIG.pxInt(4))
|
||||||
|
|
||||||
|
bFont = SHARED.theme.guiFontB
|
||||||
|
|
||||||
# Details Area
|
# Details Area
|
||||||
self.titleLabel = QLabel("<b>%s</b>" % self.tr("Title"))
|
self.titleLabel = QLabel(self.tr("Title"))
|
||||||
self.fileLabel = QLabel("<b>%s</b>" % self.tr("Document"))
|
self.fileLabel = QLabel(self.tr("Document"))
|
||||||
self.itemLabel = QLabel("<b>%s</b>" % self.tr("Status"))
|
self.itemLabel = QLabel(self.tr("Status"))
|
||||||
self.titleValue = QLabel("")
|
self.titleValue = QLabel("")
|
||||||
self.fileValue = QLabel("")
|
self.fileValue = QLabel("")
|
||||||
self.itemValue = QLabel("")
|
self.itemValue = QLabel("")
|
||||||
|
|
||||||
|
self.titleLabel.setFont(bFont)
|
||||||
|
self.fileLabel.setFont(bFont)
|
||||||
|
self.itemLabel.setFont(bFont)
|
||||||
|
|
||||||
self.titleValue.setMinimumWidth(minTitle)
|
self.titleValue.setMinimumWidth(minTitle)
|
||||||
self.titleValue.setMaximumWidth(maxTitle)
|
self.titleValue.setMaximumWidth(maxTitle)
|
||||||
self.fileValue.setMinimumWidth(minTitle)
|
self.fileValue.setMinimumWidth(minTitle)
|
||||||
@@ -814,13 +820,17 @@ class GuiOutlineDetails(QScrollArea):
|
|||||||
self.itemValue.setMaximumWidth(maxTitle)
|
self.itemValue.setMaximumWidth(maxTitle)
|
||||||
|
|
||||||
# Stats Area
|
# Stats Area
|
||||||
self.cCLabel = QLabel("<b>%s</b>" % self.tr("Characters"))
|
self.cCLabel = QLabel(self.tr("Characters"))
|
||||||
self.wCLabel = QLabel("<b>%s</b>" % self.tr("Words"))
|
self.wCLabel = QLabel(self.tr("Words"))
|
||||||
self.pCLabel = QLabel("<b>%s</b>" % self.tr("Paragraphs"))
|
self.pCLabel = QLabel(self.tr("Paragraphs"))
|
||||||
self.cCValue = QLabel("")
|
self.cCValue = QLabel("")
|
||||||
self.wCValue = QLabel("")
|
self.wCValue = QLabel("")
|
||||||
self.pCValue = QLabel("")
|
self.pCValue = QLabel("")
|
||||||
|
|
||||||
|
self.cCLabel.setFont(bFont)
|
||||||
|
self.wCLabel.setFont(bFont)
|
||||||
|
self.pCLabel.setFont(bFont)
|
||||||
|
|
||||||
self.cCValue.setMinimumWidth(wCount)
|
self.cCValue.setMinimumWidth(wCount)
|
||||||
self.wCValue.setMinimumWidth(wCount)
|
self.wCValue.setMinimumWidth(wCount)
|
||||||
self.pCValue.setMinimumWidth(wCount)
|
self.pCValue.setMinimumWidth(wCount)
|
||||||
@@ -829,23 +839,36 @@ class GuiOutlineDetails(QScrollArea):
|
|||||||
self.pCValue.setAlignment(Qt.AlignRight)
|
self.pCValue.setAlignment(Qt.AlignRight)
|
||||||
|
|
||||||
# Synopsis
|
# Synopsis
|
||||||
self.synopLabel = QLabel("<b>%s</b>" % self.tr("Synopsis"))
|
self.synopLabel = QLabel(self.tr("Synopsis"))
|
||||||
|
self.synopLabel.setFont(bFont)
|
||||||
|
|
||||||
self.synopValue = QLabel("")
|
self.synopValue = QLabel("")
|
||||||
self.synopLWrap = QHBoxLayout()
|
|
||||||
self.synopValue.setWordWrap(True)
|
self.synopValue.setWordWrap(True)
|
||||||
self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
||||||
|
|
||||||
|
self.synopLWrap = QHBoxLayout()
|
||||||
self.synopLWrap.addWidget(self.synopValue, 1)
|
self.synopLWrap.addWidget(self.synopValue, 1)
|
||||||
|
|
||||||
# Tags
|
# Tags
|
||||||
self.povKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]))
|
self.povKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]))
|
||||||
self.focKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]))
|
self.focKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]))
|
||||||
self.chrKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY]))
|
self.chrKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY]))
|
||||||
self.pltKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]))
|
self.pltKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]))
|
||||||
self.timKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY]))
|
self.timKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY]))
|
||||||
self.wldKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY]))
|
self.wldKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY]))
|
||||||
self.objKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY]))
|
self.objKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY]))
|
||||||
self.entKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY]))
|
self.entKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY]))
|
||||||
self.cstKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY]))
|
self.cstKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY]))
|
||||||
|
|
||||||
|
self.povKeyLabel.setFont(bFont)
|
||||||
|
self.focKeyLabel.setFont(bFont)
|
||||||
|
self.chrKeyLabel.setFont(bFont)
|
||||||
|
self.pltKeyLabel.setFont(bFont)
|
||||||
|
self.timKeyLabel.setFont(bFont)
|
||||||
|
self.wldKeyLabel.setFont(bFont)
|
||||||
|
self.objKeyLabel.setFont(bFont)
|
||||||
|
self.entKeyLabel.setFont(bFont)
|
||||||
|
self.cstKeyLabel.setFont(bFont)
|
||||||
|
|
||||||
self.povKeyLWrap = QHBoxLayout()
|
self.povKeyLWrap = QHBoxLayout()
|
||||||
self.focKeyLWrap = QHBoxLayout()
|
self.focKeyLWrap = QHBoxLayout()
|
||||||
@@ -989,7 +1012,7 @@ class GuiOutlineDetails(QScrollArea):
|
|||||||
|
|
||||||
def clearDetails(self) -> None:
|
def clearDetails(self) -> None:
|
||||||
"""Clear all the data labels."""
|
"""Clear all the data labels."""
|
||||||
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
|
self.titleLabel.setText(self.tr("Title"))
|
||||||
self.titleValue.setText("")
|
self.titleValue.setText("")
|
||||||
self.fileValue.setText("")
|
self.fileValue.setText("")
|
||||||
self.itemValue.setText("")
|
self.itemValue.setText("")
|
||||||
@@ -1023,7 +1046,7 @@ class GuiOutlineDetails(QScrollArea):
|
|||||||
novIdx = pIndex.getItemHeading(tHandle, sTitle)
|
novIdx = pIndex.getItemHeading(tHandle, sTitle)
|
||||||
novRefs = pIndex.getReferences(tHandle, sTitle)
|
novRefs = pIndex.getReferences(tHandle, sTitle)
|
||||||
if nwItem and novIdx:
|
if nwItem and novIdx:
|
||||||
self.titleLabel.setText("<b>%s</b>" % self.tr(self.LVL_MAP.get(novIdx.level, "H1")))
|
self.titleLabel.setText(self.tr(self.LVL_MAP.get(novIdx.level, "H1")))
|
||||||
self.titleValue.setText(novIdx.title)
|
self.titleValue.setText(novIdx.title)
|
||||||
|
|
||||||
itemStatus, _ = nwItem.getImportStatus(incIcon=False)
|
itemStatus, _ = nwItem.getImportStatus(incIcon=False)
|
||||||
|
|||||||
@@ -270,7 +270,8 @@ class GuiProjectToolBar(QWidget):
|
|||||||
self.setAutoFillBackground(True)
|
self.setAutoFillBackground(True)
|
||||||
|
|
||||||
# Widget Label
|
# Widget Label
|
||||||
self.viewLabel = QLabel("<b>%s</b>" % self.tr("Project Content"))
|
self.viewLabel = QLabel(self.tr("Project Content"))
|
||||||
|
self.viewLabel.setFont(SHARED.theme.guiFontB)
|
||||||
self.viewLabel.setContentsMargins(0, 0, 0, 0)
|
self.viewLabel.setContentsMargins(0, 0, 0, 0)
|
||||||
self.viewLabel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
self.viewLabel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,316 @@
|
|||||||
|
"""
|
||||||
|
novelWriter – GUI Project Search
|
||||||
|
================================
|
||||||
|
|
||||||
|
File History:
|
||||||
|
Created: 2024-03-21 [2.4b1] GuiProjectSearch
|
||||||
|
|
||||||
|
This file is a part of novelWriter
|
||||||
|
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but
|
||||||
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot
|
||||||
|
from PyQt5.QtGui import QCursor, QKeyEvent, QPalette
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QToolBar, QTreeWidget,
|
||||||
|
QTreeWidgetItem, QVBoxLayout, QWidget, qApp
|
||||||
|
)
|
||||||
|
|
||||||
|
from novelwriter import CONFIG, SHARED
|
||||||
|
from novelwriter.common import checkInt
|
||||||
|
from novelwriter.core.coretools import DocSearch
|
||||||
|
from novelwriter.core.item import NWItem
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CACHE_TIMEOUT = 120.0 # 2 minutes
|
||||||
|
|
||||||
|
|
||||||
|
class GuiProjectSearch(QWidget):
|
||||||
|
|
||||||
|
C_NAME = 0
|
||||||
|
C_RESULT = 0
|
||||||
|
C_COUNT = 1
|
||||||
|
|
||||||
|
D_HANDLE = Qt.ItemDataRole.UserRole
|
||||||
|
D_RESULT = Qt.ItemDataRole.UserRole + 1
|
||||||
|
|
||||||
|
selectedItemChanged = pyqtSignal(str)
|
||||||
|
openDocumentSelectRequest = pyqtSignal(str, int, int, bool)
|
||||||
|
|
||||||
|
def __init__(self, parent: QWidget) -> None:
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
|
logger.debug("Create: GuiProjectSearch")
|
||||||
|
|
||||||
|
iPx = SHARED.theme.baseIconSize
|
||||||
|
mPx = CONFIG.pxInt(2)
|
||||||
|
|
||||||
|
self._time = time()
|
||||||
|
self._search = DocSearch()
|
||||||
|
self._blocked = False
|
||||||
|
|
||||||
|
# Header
|
||||||
|
self.viewLabel = QLabel(self.tr("Project Search"))
|
||||||
|
self.viewLabel.setFont(SHARED.theme.guiFontB)
|
||||||
|
self.viewLabel.setContentsMargins(mPx, mPx, 0, mPx)
|
||||||
|
|
||||||
|
# Options
|
||||||
|
self.searchOpt = QToolBar(self)
|
||||||
|
self.searchOpt.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||||
|
self.searchOpt.setIconSize(QSize(iPx, iPx))
|
||||||
|
self.searchOpt.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
self.toggleCase = self.searchOpt.addAction(self.tr("Case Sensitive"))
|
||||||
|
self.toggleCase.setCheckable(True)
|
||||||
|
self.toggleCase.setChecked(CONFIG.searchProjCase)
|
||||||
|
self.toggleCase.toggled.connect(self._toggleCase)
|
||||||
|
|
||||||
|
self.toggleWord = self.searchOpt.addAction(self.tr("Whole Words Only"))
|
||||||
|
self.toggleWord.setCheckable(True)
|
||||||
|
self.toggleWord.setChecked(CONFIG.searchProjWord)
|
||||||
|
self.toggleWord.toggled.connect(self._toggleWord)
|
||||||
|
|
||||||
|
self.toggleRegEx = self.searchOpt.addAction(self.tr("RegEx Mode"))
|
||||||
|
self.toggleRegEx.setCheckable(True)
|
||||||
|
self.toggleRegEx.setChecked(CONFIG.searchProjRegEx)
|
||||||
|
self.toggleRegEx.toggled.connect(self._toggleRegEx)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
)
|
||||||
|
self.searchAction.triggered.connect(self._processSearch)
|
||||||
|
|
||||||
|
# Search Result
|
||||||
|
self.searchResult = QTreeWidget(self)
|
||||||
|
self.searchResult.setHeaderHidden(True)
|
||||||
|
self.searchResult.setColumnCount(2)
|
||||||
|
self.searchResult.setIconSize(QSize(iPx, iPx))
|
||||||
|
self.searchResult.setIndentation(iPx)
|
||||||
|
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
|
||||||
|
self.headerBox = QHBoxLayout()
|
||||||
|
self.headerBox.addWidget(self.viewLabel, 1)
|
||||||
|
self.headerBox.addWidget(self.searchOpt, 0)
|
||||||
|
self.headerBox.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
self.outerBox = QVBoxLayout()
|
||||||
|
self.outerBox.addLayout(self.headerBox, 0)
|
||||||
|
self.outerBox.addWidget(self.searchText, 0)
|
||||||
|
self.outerBox.addWidget(self.searchResult, 1)
|
||||||
|
self.outerBox.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.outerBox.setSpacing(mPx)
|
||||||
|
|
||||||
|
self.setLayout(self.outerBox)
|
||||||
|
self.updateTheme()
|
||||||
|
|
||||||
|
logger.debug("Ready: GuiProjectSearch")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Methods
|
||||||
|
##
|
||||||
|
|
||||||
|
def updateTheme(self) -> None:
|
||||||
|
"""Update theme elements."""
|
||||||
|
qPalette = self.palette()
|
||||||
|
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
|
||||||
|
self.setPalette(qPalette)
|
||||||
|
|
||||||
|
self.searchAction.setIcon(SHARED.theme.getIcon("search"))
|
||||||
|
self.toggleCase.setIcon(SHARED.theme.getIcon("search_case"))
|
||||||
|
self.toggleWord.setIcon(SHARED.theme.getIcon("search_word"))
|
||||||
|
self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex"))
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def processReturn(self) -> None:
|
||||||
|
"""Process a return keypress forwarded from the main GUI."""
|
||||||
|
if self.searchText.hasFocus():
|
||||||
|
self._processSearch()
|
||||||
|
elif (
|
||||||
|
self.searchResult.hasFocus()
|
||||||
|
and (items := self.searchResult.selectedItems())
|
||||||
|
and (data := items[0].data(0, self.D_RESULT))
|
||||||
|
and len(data) == 3
|
||||||
|
):
|
||||||
|
self.openDocumentSelectRequest.emit(
|
||||||
|
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
def beginSearch(self) -> None:
|
||||||
|
"""Focus the search box and select its text, if any."""
|
||||||
|
self.searchText.setFocus()
|
||||||
|
self.searchText.selectAll()
|
||||||
|
return
|
||||||
|
|
||||||
|
def closeProjectTasks(self) -> None:
|
||||||
|
"""Run close project tasks."""
|
||||||
|
self.searchText.clear()
|
||||||
|
self.searchResult.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Events
|
||||||
|
##
|
||||||
|
|
||||||
|
def keyPressEvent(self, event: QKeyEvent) -> None:
|
||||||
|
"""Process key press events. This handles up and down arrow key
|
||||||
|
presses to jump between search text box and result tree.
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
event.key() == Qt.Key.Key_Down
|
||||||
|
and self.searchText.hasFocus()
|
||||||
|
and (first := self.searchResult.topLevelItem(0))
|
||||||
|
):
|
||||||
|
first.setSelected(True)
|
||||||
|
self.searchResult.setFocus()
|
||||||
|
elif (
|
||||||
|
event.key() == Qt.Key.Key_Up
|
||||||
|
and self.searchResult.hasFocus()
|
||||||
|
and (first := self.searchResult.topLevelItem(0))
|
||||||
|
and first.isSelected()
|
||||||
|
):
|
||||||
|
first.setSelected(False)
|
||||||
|
self.searchText.setFocus()
|
||||||
|
else:
|
||||||
|
super().keyPressEvent(event)
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Private Slots
|
||||||
|
##
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _processSearch(self) -> None:
|
||||||
|
"""Perform a search."""
|
||||||
|
if not self._blocked:
|
||||||
|
qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
|
||||||
|
start = time()
|
||||||
|
self._blocked = True
|
||||||
|
self.searchResult.clear()
|
||||||
|
if text := self.searchText.text():
|
||||||
|
self._search.setUserRegEx(self.toggleRegEx.isChecked())
|
||||||
|
self._search.setCaseSensitive(self.toggleCase.isChecked())
|
||||||
|
self._search.setWholeWords(self.toggleWord.isChecked())
|
||||||
|
for item, results, capped in self._search.iterSearch(SHARED.project, text):
|
||||||
|
self._appendResultSet(item, results, capped)
|
||||||
|
logger.debug("Search took %.3f ms", 1000*(time() - start))
|
||||||
|
self._time = time()
|
||||||
|
qApp.restoreOverrideCursor()
|
||||||
|
self._blocked = False
|
||||||
|
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
|
||||||
|
|
||||||
|
@pyqtSlot("QTreeWidgetItem*", int)
|
||||||
|
def _searchResultDoubleClicked(self, item: QTreeWidgetItem, column: int) -> None:
|
||||||
|
"""Process search result double click."""
|
||||||
|
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), True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
@pyqtSlot(bool)
|
||||||
|
def _toggleCase(self, state: bool) -> None:
|
||||||
|
"""Enable/disable case sensitive mode."""
|
||||||
|
CONFIG.searchProjCase = state
|
||||||
|
return
|
||||||
|
|
||||||
|
@pyqtSlot(bool)
|
||||||
|
def _toggleWord(self, state: bool) -> None:
|
||||||
|
"""Enable/disable whole word search mode."""
|
||||||
|
CONFIG.searchProjWord = state
|
||||||
|
return
|
||||||
|
|
||||||
|
@pyqtSlot(bool)
|
||||||
|
def _toggleRegEx(self, state: bool) -> None:
|
||||||
|
"""Enable/disable regular expression search mode."""
|
||||||
|
CONFIG.searchProjRegEx = state
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Internal Functions
|
||||||
|
##
|
||||||
|
|
||||||
|
def _appendResultSet(
|
||||||
|
self, nwItem: NWItem, results: list[tuple[int, int, str]], capped: bool
|
||||||
|
) -> None:
|
||||||
|
"""Populate the result tree."""
|
||||||
|
if results:
|
||||||
|
tHandle = nwItem.itemHandle
|
||||||
|
docIcon = SHARED.theme.getItemIcon(
|
||||||
|
nwItem.itemType, nwItem.itemClass,
|
||||||
|
nwItem.itemLayout, nwItem.mainHeading
|
||||||
|
)
|
||||||
|
ext = "+" if capped else ""
|
||||||
|
|
||||||
|
tItem = QTreeWidgetItem()
|
||||||
|
tItem.setText(self.C_NAME, nwItem.itemName)
|
||||||
|
tItem.setIcon(self.C_NAME, docIcon)
|
||||||
|
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)
|
||||||
|
|
||||||
|
rItems = []
|
||||||
|
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)
|
||||||
|
|
||||||
|
parent = self.searchResult.indexFromItem(tItem)
|
||||||
|
for i in range(tItem.childCount()):
|
||||||
|
self.searchResult.setFirstColumnSpanned(i, parent, True)
|
||||||
|
|
||||||
|
qApp.processEvents()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
# END Class GuiProjectSearch
|
||||||
@@ -45,7 +45,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class GuiSideBar(QWidget):
|
class GuiSideBar(QWidget):
|
||||||
|
|
||||||
viewChangeRequested = pyqtSignal(nwView)
|
requestViewChange = pyqtSignal(nwView)
|
||||||
|
|
||||||
def __init__(self, mainGui: GuiMain) -> None:
|
def __init__(self, mainGui: GuiMain) -> None:
|
||||||
super().__init__(parent=mainGui)
|
super().__init__(parent=mainGui)
|
||||||
@@ -61,15 +61,19 @@ class GuiSideBar(QWidget):
|
|||||||
# Buttons
|
# Buttons
|
||||||
self.tbProject = NIconToolButton(self, iPx)
|
self.tbProject = NIconToolButton(self, iPx)
|
||||||
self.tbProject.setToolTip("{0} [Ctrl+T]".format(self.tr("Project Tree View")))
|
self.tbProject.setToolTip("{0} [Ctrl+T]".format(self.tr("Project Tree View")))
|
||||||
self.tbProject.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT))
|
self.tbProject.clicked.connect(lambda: self.requestViewChange.emit(nwView.PROJECT))
|
||||||
|
|
||||||
self.tbNovel = NIconToolButton(self, iPx)
|
self.tbNovel = NIconToolButton(self, iPx)
|
||||||
self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View")))
|
self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View")))
|
||||||
self.tbNovel.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL))
|
self.tbNovel.clicked.connect(lambda: self.requestViewChange.emit(nwView.NOVEL))
|
||||||
|
|
||||||
|
self.tbSearch = NIconToolButton(self, iPx)
|
||||||
|
self.tbSearch.setToolTip("{0} [Ctrl+Shift+F]".format(self.tr("Search Project")))
|
||||||
|
self.tbSearch.clicked.connect(lambda: self.requestViewChange.emit(nwView.SEARCH))
|
||||||
|
|
||||||
self.tbOutline = NIconToolButton(self, iPx)
|
self.tbOutline = NIconToolButton(self, iPx)
|
||||||
self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
|
self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
|
||||||
self.tbOutline.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE))
|
self.tbOutline.clicked.connect(lambda: self.requestViewChange.emit(nwView.OUTLINE))
|
||||||
|
|
||||||
self.tbBuild = NIconToolButton(self, iPx)
|
self.tbBuild = NIconToolButton(self, iPx)
|
||||||
self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
|
self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
|
||||||
@@ -99,6 +103,7 @@ class GuiSideBar(QWidget):
|
|||||||
self.outerBox = QVBoxLayout()
|
self.outerBox = QVBoxLayout()
|
||||||
self.outerBox.addWidget(self.tbProject)
|
self.outerBox.addWidget(self.tbProject)
|
||||||
self.outerBox.addWidget(self.tbNovel)
|
self.outerBox.addWidget(self.tbNovel)
|
||||||
|
self.outerBox.addWidget(self.tbSearch)
|
||||||
self.outerBox.addWidget(self.tbOutline)
|
self.outerBox.addWidget(self.tbOutline)
|
||||||
self.outerBox.addWidget(self.tbBuild)
|
self.outerBox.addWidget(self.tbBuild)
|
||||||
self.outerBox.addStretch(1)
|
self.outerBox.addStretch(1)
|
||||||
@@ -129,6 +134,9 @@ class GuiSideBar(QWidget):
|
|||||||
self.tbNovel.setIcon(SHARED.theme.getIcon("view_novel"))
|
self.tbNovel.setIcon(SHARED.theme.getIcon("view_novel"))
|
||||||
self.tbNovel.setStyleSheet(buttonStyle)
|
self.tbNovel.setStyleSheet(buttonStyle)
|
||||||
|
|
||||||
|
self.tbSearch.setIcon(SHARED.theme.getIcon("view_search"))
|
||||||
|
self.tbSearch.setStyleSheet(buttonStyle)
|
||||||
|
|
||||||
self.tbOutline.setIcon(SHARED.theme.getIcon("view_outline"))
|
self.tbOutline.setIcon(SHARED.theme.getIcon("view_outline"))
|
||||||
self.tbOutline.setStyleSheet(buttonStyle)
|
self.tbOutline.setStyleSheet(buttonStyle)
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ class GuiTheme:
|
|||||||
|
|
||||||
# Fonts
|
# Fonts
|
||||||
self.guiFont = qApp.font()
|
self.guiFont = qApp.font()
|
||||||
|
self.guiFontB = qApp.font()
|
||||||
|
self.guiFontB.setBold(True)
|
||||||
|
|
||||||
qMetric = QFontMetrics(self.guiFont)
|
qMetric = QFontMetrics(self.guiFont)
|
||||||
self.fontPointSize = self.guiFont.pointSizeF()
|
self.fontPointSize = self.guiFont.pointSizeF()
|
||||||
@@ -486,7 +488,7 @@ class GuiIcons:
|
|||||||
"build_excluded", "build_filtered", "build_included", "proj_chapter", "proj_details",
|
"build_excluded", "build_filtered", "build_included", "proj_chapter", "proj_details",
|
||||||
"proj_document", "proj_folder", "proj_note", "proj_nwx", "proj_section", "proj_scene",
|
"proj_document", "proj_folder", "proj_note", "proj_nwx", "proj_section", "proj_scene",
|
||||||
"proj_stats", "proj_title", "status_idle", "status_lang", "status_lines", "status_stats",
|
"proj_stats", "proj_title", "status_idle", "status_lang", "status_lines", "status_stats",
|
||||||
"status_time", "view_build", "view_editor", "view_novel", "view_outline",
|
"status_time", "view_build", "view_editor", "view_novel", "view_outline", "view_search",
|
||||||
|
|
||||||
# Class Icons
|
# Class Icons
|
||||||
"cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none", "cls_novel",
|
"cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none", "cls_novel",
|
||||||
|
|||||||
+71
-70
@@ -38,33 +38,31 @@ from PyQt5.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter import CONFIG, SHARED, __hexversion__, __version__
|
from novelwriter import CONFIG, SHARED, __hexversion__, __version__
|
||||||
|
from novelwriter.common import formatFileFilter, formatVersion, hexToInt
|
||||||
from novelwriter.constants import nwConst
|
from novelwriter.constants import nwConst
|
||||||
from novelwriter.gui.theme import GuiTheme
|
|
||||||
from novelwriter.gui.sidebar import GuiSideBar
|
|
||||||
from novelwriter.gui.outline import GuiOutlineView
|
|
||||||
from novelwriter.gui.mainmenu import GuiMainMenu
|
|
||||||
from novelwriter.gui.projtree import GuiProjectView
|
|
||||||
from novelwriter.gui.doceditor import GuiDocEditor
|
|
||||||
from novelwriter.gui.docviewer import GuiDocViewer
|
|
||||||
from novelwriter.gui.noveltree import GuiNovelView
|
|
||||||
from novelwriter.gui.statusbar import GuiMainStatus
|
|
||||||
from novelwriter.gui.itemdetails import GuiItemDetails
|
|
||||||
from novelwriter.gui.docviewerpanel import GuiDocViewerPanel
|
|
||||||
from novelwriter.dialogs.about import GuiAbout
|
from novelwriter.dialogs.about import GuiAbout
|
||||||
from novelwriter.dialogs.wordlist import GuiWordList
|
|
||||||
from novelwriter.dialogs.preferences import GuiPreferences
|
from novelwriter.dialogs.preferences import GuiPreferences
|
||||||
from novelwriter.dialogs.projectsettings import GuiProjectSettings
|
from novelwriter.dialogs.projectsettings import GuiProjectSettings
|
||||||
from novelwriter.tools.welcome import GuiWelcome
|
from novelwriter.dialogs.wordlist import GuiWordList
|
||||||
from novelwriter.tools.manuscript import GuiManuscript
|
from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemType, nwWidget, nwView
|
||||||
|
from novelwriter.gui.doceditor import GuiDocEditor
|
||||||
|
from novelwriter.gui.docviewer import GuiDocViewer
|
||||||
|
from novelwriter.gui.docviewerpanel import GuiDocViewerPanel
|
||||||
|
from novelwriter.gui.itemdetails import GuiItemDetails
|
||||||
|
from novelwriter.gui.mainmenu import GuiMainMenu
|
||||||
|
from novelwriter.gui.noveltree import GuiNovelView
|
||||||
|
from novelwriter.gui.outline import GuiOutlineView
|
||||||
|
from novelwriter.gui.projtree import GuiProjectView
|
||||||
|
from novelwriter.gui.search import GuiProjectSearch
|
||||||
|
from novelwriter.gui.sidebar import GuiSideBar
|
||||||
|
from novelwriter.gui.statusbar import GuiMainStatus
|
||||||
|
from novelwriter.gui.theme import GuiTheme
|
||||||
from novelwriter.tools.dictionaries import GuiDictionaries
|
from novelwriter.tools.dictionaries import GuiDictionaries
|
||||||
|
from novelwriter.tools.manuscript import GuiManuscript
|
||||||
from novelwriter.tools.noveldetails import GuiNovelDetails
|
from novelwriter.tools.noveldetails import GuiNovelDetails
|
||||||
|
from novelwriter.tools.welcome import GuiWelcome
|
||||||
from novelwriter.tools.writingstats import GuiWritingStats
|
from novelwriter.tools.writingstats import GuiWritingStats
|
||||||
|
|
||||||
from novelwriter.enum import (
|
|
||||||
nwDocAction, nwDocInsert, nwDocMode, nwItemType, nwWidget, nwView
|
|
||||||
)
|
|
||||||
from novelwriter.common import formatFileFilter, formatVersion, hexToInt
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -123,6 +121,7 @@ class GuiMain(QMainWindow):
|
|||||||
# Main GUI Elements
|
# Main GUI Elements
|
||||||
self.mainStatus = GuiMainStatus(self)
|
self.mainStatus = GuiMainStatus(self)
|
||||||
self.projView = GuiProjectView(self)
|
self.projView = GuiProjectView(self)
|
||||||
|
self.projSearch = GuiProjectSearch(self)
|
||||||
self.novelView = GuiNovelView(self)
|
self.novelView = GuiNovelView(self)
|
||||||
self.docEditor = GuiDocEditor(self)
|
self.docEditor = GuiDocEditor(self)
|
||||||
self.docViewer = GuiDocViewer(self)
|
self.docViewer = GuiDocViewer(self)
|
||||||
@@ -136,6 +135,7 @@ class GuiMain(QMainWindow):
|
|||||||
self.projStack = QStackedWidget(self)
|
self.projStack = QStackedWidget(self)
|
||||||
self.projStack.addWidget(self.projView)
|
self.projStack.addWidget(self.projView)
|
||||||
self.projStack.addWidget(self.novelView)
|
self.projStack.addWidget(self.novelView)
|
||||||
|
self.projStack.addWidget(self.projSearch)
|
||||||
self.projStack.currentChanged.connect(self._projStackChanged)
|
self.projStack.currentChanged.connect(self._projStackChanged)
|
||||||
|
|
||||||
# Project Tree View
|
# Project Tree View
|
||||||
@@ -154,6 +154,8 @@ class GuiMain(QMainWindow):
|
|||||||
self.splitView.setHandleWidth(hWd)
|
self.splitView.setHandleWidth(hWd)
|
||||||
self.splitView.setOpaqueResize(False)
|
self.splitView.setOpaqueResize(False)
|
||||||
self.splitView.setSizes(CONFIG.viewPanePos)
|
self.splitView.setSizes(CONFIG.viewPanePos)
|
||||||
|
self.splitView.setCollapsible(0, False)
|
||||||
|
self.splitView.setCollapsible(1, False)
|
||||||
|
|
||||||
# Splitter : Document Editor / Document Viewer
|
# Splitter : Document Editor / Document Viewer
|
||||||
self.splitDocs = QSplitter(Qt.Horizontal, self)
|
self.splitDocs = QSplitter(Qt.Horizontal, self)
|
||||||
@@ -161,6 +163,8 @@ class GuiMain(QMainWindow):
|
|||||||
self.splitDocs.addWidget(self.splitView)
|
self.splitDocs.addWidget(self.splitView)
|
||||||
self.splitDocs.setOpaqueResize(False)
|
self.splitDocs.setOpaqueResize(False)
|
||||||
self.splitDocs.setHandleWidth(hWd)
|
self.splitDocs.setHandleWidth(hWd)
|
||||||
|
self.splitDocs.setCollapsible(0, False)
|
||||||
|
self.splitDocs.setCollapsible(1, False)
|
||||||
|
|
||||||
# Splitter : Project Tree / Document Area
|
# Splitter : Project Tree / Document Area
|
||||||
self.splitMain = QSplitter(Qt.Horizontal)
|
self.splitMain = QSplitter(Qt.Horizontal)
|
||||||
@@ -170,6 +174,10 @@ class GuiMain(QMainWindow):
|
|||||||
self.splitMain.setOpaqueResize(False)
|
self.splitMain.setOpaqueResize(False)
|
||||||
self.splitMain.setHandleWidth(hWd)
|
self.splitMain.setHandleWidth(hWd)
|
||||||
self.splitMain.setSizes(CONFIG.mainPanePos)
|
self.splitMain.setSizes(CONFIG.mainPanePos)
|
||||||
|
self.splitMain.setCollapsible(0, False)
|
||||||
|
self.splitMain.setCollapsible(0, False)
|
||||||
|
self.splitMain.setStretchFactor(1, 0)
|
||||||
|
self.splitMain.setStretchFactor(1, 1)
|
||||||
|
|
||||||
# Main Stack : Editor / Outline
|
# Main Stack : Editor / Outline
|
||||||
self.mainStack = QStackedWidget(self)
|
self.mainStack = QStackedWidget(self)
|
||||||
@@ -177,31 +185,6 @@ class GuiMain(QMainWindow):
|
|||||||
self.mainStack.addWidget(self.outlineView)
|
self.mainStack.addWidget(self.outlineView)
|
||||||
self.mainStack.currentChanged.connect(self._mainStackChanged)
|
self.mainStack.currentChanged.connect(self._mainStackChanged)
|
||||||
|
|
||||||
# Indices of Splitter Widgets
|
|
||||||
self.idxTree = self.splitMain.indexOf(self.treePane)
|
|
||||||
self.idxMain = self.splitMain.indexOf(self.splitDocs)
|
|
||||||
self.idxEditor = self.splitDocs.indexOf(self.docEditor)
|
|
||||||
self.idxViewer = self.splitDocs.indexOf(self.splitView)
|
|
||||||
self.idxViewDoc = self.splitView.indexOf(self.docViewer)
|
|
||||||
self.idxViewDocPanel = self.splitView.indexOf(self.docViewerPanel)
|
|
||||||
|
|
||||||
# Indices of Stack Widgets
|
|
||||||
self.idxEditorView = self.mainStack.indexOf(self.splitMain)
|
|
||||||
self.idxOutlineView = self.mainStack.indexOf(self.outlineView)
|
|
||||||
self.idxProjView = self.projStack.indexOf(self.projView)
|
|
||||||
self.idxNovelView = self.projStack.indexOf(self.novelView)
|
|
||||||
|
|
||||||
# Splitter Behaviour
|
|
||||||
self.splitMain.setCollapsible(self.idxTree, False)
|
|
||||||
self.splitMain.setCollapsible(self.idxMain, False)
|
|
||||||
self.splitDocs.setCollapsible(self.idxEditor, False)
|
|
||||||
self.splitDocs.setCollapsible(self.idxViewer, False)
|
|
||||||
self.splitView.setCollapsible(self.idxViewDoc, False)
|
|
||||||
self.splitView.setCollapsible(self.idxViewDocPanel, False)
|
|
||||||
|
|
||||||
self.splitMain.setStretchFactor(self.idxTree, 0)
|
|
||||||
self.splitMain.setStretchFactor(self.idxMain, 1)
|
|
||||||
|
|
||||||
# Editor / Viewer Default State
|
# Editor / Viewer Default State
|
||||||
self.splitView.setVisible(False)
|
self.splitView.setVisible(False)
|
||||||
self.docEditor.closeSearch()
|
self.docEditor.closeSearch()
|
||||||
@@ -237,14 +220,16 @@ class GuiMain(QMainWindow):
|
|||||||
SHARED.indexScannedText.connect(self.itemDetails.updateViewBox)
|
SHARED.indexScannedText.connect(self.itemDetails.updateViewBox)
|
||||||
SHARED.indexCleared.connect(self.docViewerPanel.indexWasCleared)
|
SHARED.indexCleared.connect(self.docViewerPanel.indexWasCleared)
|
||||||
SHARED.indexAvailable.connect(self.docViewerPanel.indexHasAppeared)
|
SHARED.indexAvailable.connect(self.docViewerPanel.indexHasAppeared)
|
||||||
|
SHARED.mainClockTick.connect(self._timeTick)
|
||||||
|
|
||||||
self.mainMenu.requestDocAction.connect(self._passDocumentAction)
|
self.mainMenu.requestDocAction.connect(self._passDocumentAction)
|
||||||
self.mainMenu.requestDocInsert.connect(self._passDocumentInsert)
|
self.mainMenu.requestDocInsert.connect(self._passDocumentInsert)
|
||||||
self.mainMenu.requestDocInsertText.connect(self._passDocumentInsert)
|
self.mainMenu.requestDocInsertText.connect(self._passDocumentInsert)
|
||||||
self.mainMenu.requestDocKeyWordInsert.connect(self.docEditor.insertKeyWord)
|
self.mainMenu.requestDocKeyWordInsert.connect(self.docEditor.insertKeyWord)
|
||||||
self.mainMenu.requestFocusChange.connect(self.switchFocus)
|
self.mainMenu.requestFocusChange.connect(self.switchFocus)
|
||||||
|
self.mainMenu.requestViewChange.connect(self._changeView)
|
||||||
|
|
||||||
self.sideBar.viewChangeRequested.connect(self._changeView)
|
self.sideBar.requestViewChange.connect(self._changeView)
|
||||||
|
|
||||||
self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
|
self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
|
||||||
self.projView.openDocumentRequest.connect(self._openDocument)
|
self.projView.openDocumentRequest.connect(self._openDocument)
|
||||||
@@ -261,6 +246,9 @@ 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.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)
|
||||||
self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
|
self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
|
||||||
@@ -299,12 +287,6 @@ class GuiMain(QMainWindow):
|
|||||||
self.asDocTimer = QTimer(self)
|
self.asDocTimer = QTimer(self)
|
||||||
self.asDocTimer.timeout.connect(self._autoSaveDocument)
|
self.asDocTimer.timeout.connect(self._autoSaveDocument)
|
||||||
|
|
||||||
# Main Clock
|
|
||||||
self.mainTimer = QTimer(self)
|
|
||||||
self.mainTimer.setInterval(1000)
|
|
||||||
self.mainTimer.timeout.connect(self._timeTick)
|
|
||||||
self.mainTimer.start()
|
|
||||||
|
|
||||||
# Shortcuts and Actions
|
# Shortcuts and Actions
|
||||||
self._connectMenuActions()
|
self._connectMenuActions()
|
||||||
|
|
||||||
@@ -408,6 +390,7 @@ class GuiMain(QMainWindow):
|
|||||||
self.outlineView.closeProjectTasks()
|
self.outlineView.closeProjectTasks()
|
||||||
self.novelView.closeProjectTasks()
|
self.novelView.closeProjectTasks()
|
||||||
self.projView.closeProjectTasks()
|
self.projView.closeProjectTasks()
|
||||||
|
self.projSearch.closeProjectTasks()
|
||||||
self.itemDetails.clearDetails()
|
self.itemDetails.clearDetails()
|
||||||
self.mainStatus.clearStatus()
|
self.mainStatus.clearStatus()
|
||||||
|
|
||||||
@@ -1031,12 +1014,15 @@ class GuiMain(QMainWindow):
|
|||||||
self.novelView.setTreeFocus()
|
self.novelView.setTreeFocus()
|
||||||
else:
|
else:
|
||||||
self.projView.setTreeFocus()
|
self.projView.setTreeFocus()
|
||||||
else:
|
elif self.projStack.currentWidget() is self.novelView:
|
||||||
if self.novelView.treeHasFocus():
|
if self.novelView.treeHasFocus():
|
||||||
self._changeView(nwView.PROJECT)
|
self._changeView(nwView.PROJECT)
|
||||||
self.projView.setTreeFocus()
|
self.projView.setTreeFocus()
|
||||||
else:
|
else:
|
||||||
self.novelView.setTreeFocus()
|
self.novelView.setTreeFocus()
|
||||||
|
else:
|
||||||
|
self._changeView(nwView.PROJECT)
|
||||||
|
self.projView.setTreeFocus()
|
||||||
elif paneNo == nwWidget.EDITOR:
|
elif paneNo == nwWidget.EDITOR:
|
||||||
self._changeView(nwView.EDITOR)
|
self._changeView(nwView.EDITOR)
|
||||||
self.docEditor.setFocus()
|
self.docEditor.setFocus()
|
||||||
@@ -1077,6 +1063,7 @@ class GuiMain(QMainWindow):
|
|||||||
self.sideBar.updateTheme()
|
self.sideBar.updateTheme()
|
||||||
self.projView.updateTheme()
|
self.projView.updateTheme()
|
||||||
self.novelView.updateTheme()
|
self.novelView.updateTheme()
|
||||||
|
self.projSearch.updateTheme()
|
||||||
self.outlineView.updateTheme()
|
self.outlineView.updateTheme()
|
||||||
self.itemDetails.updateTheme()
|
self.itemDetails.updateTheme()
|
||||||
self.mainStatus.updateTheme()
|
self.mainStatus.updateTheme()
|
||||||
@@ -1147,6 +1134,15 @@ class GuiMain(QMainWindow):
|
|||||||
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
|
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@pyqtSlot(str, int, int, bool)
|
||||||
|
def _openDocumentSelection(
|
||||||
|
self, tHandle: str, selStart: int, selLength: int, changeFocus: bool
|
||||||
|
) -> None:
|
||||||
|
"""Open a document and select a section of the text."""
|
||||||
|
if self.openDocument(tHandle, changeFocus=changeFocus):
|
||||||
|
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."""
|
||||||
@@ -1168,6 +1164,10 @@ class GuiMain(QMainWindow):
|
|||||||
elif view == nwView.NOVEL:
|
elif view == nwView.NOVEL:
|
||||||
self.mainStack.setCurrentWidget(self.splitMain)
|
self.mainStack.setCurrentWidget(self.splitMain)
|
||||||
self.projStack.setCurrentWidget(self.novelView)
|
self.projStack.setCurrentWidget(self.novelView)
|
||||||
|
elif view == nwView.SEARCH:
|
||||||
|
self.mainStack.setCurrentWidget(self.splitMain)
|
||||||
|
self.projStack.setCurrentWidget(self.projSearch)
|
||||||
|
self.projSearch.beginSearch()
|
||||||
elif view == nwView.OUTLINE:
|
elif view == nwView.OUTLINE:
|
||||||
self.mainStack.setCurrentWidget(self.outlineView)
|
self.mainStack.setCurrentWidget(self.outlineView)
|
||||||
return
|
return
|
||||||
@@ -1206,16 +1206,15 @@ class GuiMain(QMainWindow):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _timeTick(self) -> None:
|
def _timeTick(self) -> None:
|
||||||
"""Process time tick of the main timer."""
|
"""Process time tick of the main timer."""
|
||||||
if not SHARED.hasProject:
|
if SHARED.hasProject:
|
||||||
return
|
currTime = time()
|
||||||
currTime = time()
|
editIdle = currTime - self.docEditor.lastActive > CONFIG.userIdleTime
|
||||||
editIdle = currTime - self.docEditor.lastActive > CONFIG.userIdleTime
|
userIdle = qApp.applicationState() != Qt.ApplicationActive
|
||||||
userIdle = qApp.applicationState() != Qt.ApplicationActive
|
self.mainStatus.setUserIdle(editIdle or userIdle)
|
||||||
self.mainStatus.setUserIdle(editIdle or userIdle)
|
SHARED.updateIdleTime(currTime, editIdle or userIdle)
|
||||||
SHARED.updateIdleTime(currTime, editIdle or userIdle)
|
self.mainStatus.updateTime(idleTime=SHARED.projectIdleTime)
|
||||||
self.mainStatus.updateTime(idleTime=SHARED.projectIdleTime)
|
if CONFIG.memInfo and int(currTime) % 5 == 0: # pragma: no cover
|
||||||
if CONFIG.memInfo and int(currTime) % 5 == 0: # pragma: no cover
|
self.mainStatus.memInfo()
|
||||||
self.mainStatus.memInfo()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@@ -1257,15 +1256,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:
|
||||||
@@ -1275,7 +1275,7 @@ class GuiMain(QMainWindow):
|
|||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def _mainStackChanged(self, index: int) -> None:
|
def _mainStackChanged(self, index: int) -> None:
|
||||||
"""Process main window tab change."""
|
"""Process main window tab change."""
|
||||||
if index == self.idxOutlineView:
|
if self.mainStack.widget(index) == self.outlineView:
|
||||||
if SHARED.hasProject:
|
if SHARED.hasProject:
|
||||||
self.outlineView.refreshTree()
|
self.outlineView.refreshTree()
|
||||||
return
|
return
|
||||||
@@ -1284,9 +1284,10 @@ class GuiMain(QMainWindow):
|
|||||||
def _projStackChanged(self, index: int) -> None:
|
def _projStackChanged(self, index: int) -> None:
|
||||||
"""Process project view tab change."""
|
"""Process project view tab change."""
|
||||||
sHandle = None
|
sHandle = None
|
||||||
if index == self.idxProjView:
|
widget = self.projStack.widget(index)
|
||||||
|
if widget == self.projView:
|
||||||
sHandle = self.projView.getSelectedHandle()
|
sHandle = self.projView.getSelectedHandle()
|
||||||
elif index == self.idxNovelView:
|
elif widget == self.novelView:
|
||||||
sHandle, _ = self.novelView.getSelectedHandle()
|
sHandle, _ = self.novelView.getSelectedHandle()
|
||||||
self.itemDetails.updateViewBox(sHandle)
|
self.itemDetails.updateViewBox(sHandle)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from time import time
|
|||||||
from typing import TYPE_CHECKING, TypeVar
|
from typing import TYPE_CHECKING, TypeVar
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal
|
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, pyqtSignal
|
||||||
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QWidget
|
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QWidget
|
||||||
from novelwriter.common import formatFileFilter
|
from novelwriter.common import formatFileFilter
|
||||||
|
|
||||||
@@ -62,6 +62,7 @@ class SharedData(QObject):
|
|||||||
indexChangedTags = pyqtSignal(list, list)
|
indexChangedTags = pyqtSignal(list, list)
|
||||||
indexCleared = pyqtSignal()
|
indexCleared = pyqtSignal()
|
||||||
indexAvailable = pyqtSignal()
|
indexAvailable = pyqtSignal()
|
||||||
|
mainClockTick = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -79,6 +80,10 @@ class SharedData(QObject):
|
|||||||
self._idleRefTime = time()
|
self._idleRefTime = time()
|
||||||
self._focusMode = False
|
self._focusMode = False
|
||||||
|
|
||||||
|
self._clock = QTimer(self)
|
||||||
|
self._clock.setInterval(1000)
|
||||||
|
self._clock.timeout.connect(lambda: self.mainClockTick.emit())
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -158,6 +163,7 @@ class SharedData(QObject):
|
|||||||
soon as the Main GUI is created to ensure the SHARED singleton
|
soon as the Main GUI is created to ensure the SHARED singleton
|
||||||
has the properties needed for operation.
|
has the properties needed for operation.
|
||||||
"""
|
"""
|
||||||
|
self._clock.start()
|
||||||
self._gui = gui
|
self._gui = gui
|
||||||
self._theme = theme
|
self._theme = theme
|
||||||
self._resetProject()
|
self._resetProject()
|
||||||
|
|||||||
@@ -796,7 +796,7 @@ class _PreviewWidget(QTextBrowser):
|
|||||||
|
|
||||||
# Age Timer
|
# Age Timer
|
||||||
self.ageTimer = QTimer(self)
|
self.ageTimer = QTimer(self)
|
||||||
self.ageTimer.setInterval(10)
|
self.ageTimer.setInterval(10000)
|
||||||
self.ageTimer.timeout.connect(self._updateBuildAge)
|
self.ageTimer.timeout.connect(self._updateBuildAge)
|
||||||
self.ageTimer.start()
|
self.ageTimer.start()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[Meta]
|
[Meta]
|
||||||
timestamp = 2024-02-09 12:05:00
|
timestamp = 2024-03-25 11:57:35
|
||||||
|
|
||||||
[Main]
|
[Main]
|
||||||
theme = default
|
theme = default
|
||||||
@@ -69,7 +69,6 @@ useridletime = 300
|
|||||||
[State]
|
[State]
|
||||||
showviewerpanel = True
|
showviewerpanel = True
|
||||||
showedittoolbar = False
|
showedittoolbar = False
|
||||||
useshortcodes = False
|
|
||||||
viewcomments = True
|
viewcomments = True
|
||||||
viewsynopsis = True
|
viewsynopsis = True
|
||||||
searchcase = False
|
searchcase = False
|
||||||
@@ -78,4 +77,7 @@ searchregex = False
|
|||||||
searchloop = False
|
searchloop = False
|
||||||
searchnextfile = False
|
searchnextfile = False
|
||||||
searchmatchcap = False
|
searchmatchcap = False
|
||||||
|
searchprojcase = False
|
||||||
|
searchprojword = False
|
||||||
|
searchprojregex = False
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,11 @@ from tools import C, NWD_IGNORE, buildTestProject, cmpFiles, XML_IGNORE
|
|||||||
from mocked import causeOSError
|
from mocked import causeOSError
|
||||||
|
|
||||||
from novelwriter import CONFIG
|
from novelwriter import CONFIG
|
||||||
from novelwriter.constants import nwFiles, nwItemClass
|
from novelwriter.constants import nwConst, nwFiles, nwItemClass
|
||||||
|
from novelwriter.core.coretools import (
|
||||||
|
DocDuplicator, DocMerger, DocSearch, DocSplitter, ProjectBuilder
|
||||||
|
)
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter, ProjectBuilder
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
@@ -401,6 +403,97 @@ def testCoreTools_DocDuplicator(mockGUI, fncPath, tstPaths, mockRnd):
|
|||||||
# END Test testCoreTools_DocDuplicator
|
# END Test testCoreTools_DocDuplicator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreTools_DocSearch(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText):
|
||||||
|
"""Test the DocDuplicator utility."""
|
||||||
|
project = NWProject()
|
||||||
|
mockRnd.reset()
|
||||||
|
buildTestProject(project, fncPath)
|
||||||
|
project.storage.getDocument(C.hSceneDoc).writeDocument(
|
||||||
|
"### New Scene\n\n" + "\n\n".join(ipsumText)
|
||||||
|
)
|
||||||
|
|
||||||
|
search = DocSearch()
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
# ========
|
||||||
|
|
||||||
|
result = [(i.itemHandle, r, c) for i, r, c in search.iterSearch(project, "Scene")]
|
||||||
|
assert result[0] == (C.hTitlePage, [], False)
|
||||||
|
assert result[1] == (C.hChapterDoc, [], False)
|
||||||
|
assert result[2] == (C.hSceneDoc, [(8, 5, "Scene")], False)
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
assert list(search._cache.keys()) == [C.hTitlePage, C.hChapterDoc, C.hSceneDoc]
|
||||||
|
|
||||||
|
# Patterns
|
||||||
|
# ========
|
||||||
|
|
||||||
|
# Escape Using QRegularExpression
|
||||||
|
with monkeypatch.context() as mp:
|
||||||
|
mp.setattr(CONFIG, "verQtValue", 0x050f00)
|
||||||
|
assert search._buildPattern("[A-Za-z0-9_]+") == r"\[A\-Za\-z0\-9_\]\+"
|
||||||
|
|
||||||
|
# Escape Using Custom Implementation
|
||||||
|
with monkeypatch.context() as mp:
|
||||||
|
mp.setattr(CONFIG, "verQtValue", 0x050d00)
|
||||||
|
assert search._buildPattern("[A-Za-z0-9_]+") == r"\[A\-Za\-z0\-9_\]\+"
|
||||||
|
|
||||||
|
# Whole Words
|
||||||
|
search.setWholeWords(True)
|
||||||
|
search.setUserRegEx(True)
|
||||||
|
assert search._buildPattern("Hi") == r"\bHi\b"
|
||||||
|
assert search._buildPattern(r"\bHi") == r"\bHi\b"
|
||||||
|
assert search._buildPattern(r"Hi\b") == r"\bHi\b"
|
||||||
|
assert search._buildPattern(r"\bHi\b") == r"\bHi\b"
|
||||||
|
search.setWholeWords(False)
|
||||||
|
search.setUserRegEx(False)
|
||||||
|
|
||||||
|
# Test Settings
|
||||||
|
# =============
|
||||||
|
|
||||||
|
def pruneResult(result, index):
|
||||||
|
temp = [(i.itemHandle, r, c) for i, r, c in result][index][1]
|
||||||
|
return [(s, n, c.split()[0]) for s, n, c in temp]
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
assert pruneResult(search.iterSearch(project, "Lorem"), 2) == [
|
||||||
|
(15, 5, "Lorem"), (754, 5, "lorem"), (2056, 5, "lorem,"), (2209, 5, "lorem"),
|
||||||
|
(2425, 5, "lorem"), (2840, 5, "lorem."), (3399, 5, "lorem"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Whole Words
|
||||||
|
search.setWholeWords(True)
|
||||||
|
assert pruneResult(search.iterSearch(project, "Lor"), 2) == []
|
||||||
|
search.setWholeWords(False)
|
||||||
|
assert pruneResult(search.iterSearch(project, "Lor"), 2) == [
|
||||||
|
(15, 3, "Lorem"), (29, 3, "lor"), (754, 3, "lorem"), (2056, 3, "lorem,"),
|
||||||
|
(2209, 3, "lorem"), (2425, 3, "lorem"), (2840, 3, "lorem."), (3328, 3, "lor."),
|
||||||
|
(3399, 3, "lorem"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# As RegEx
|
||||||
|
search.setWholeWords(False)
|
||||||
|
search.setUserRegEx(True)
|
||||||
|
assert pruneResult(search.iterSearch(project, r"Lor\b"), 2) == [
|
||||||
|
(29, 3, "lor"), (3328, 3, "lor."),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Max Results
|
||||||
|
with monkeypatch.context() as mp:
|
||||||
|
mp.setattr(nwConst, "MAX_SEARCH_RESULT", 3)
|
||||||
|
assert pruneResult(search.iterSearch(project, "Lorem"), 2) == [
|
||||||
|
(15, 5, "Lorem"), (754, 5, "lorem"), (2056, 5, "lorem,"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Case Sensitive
|
||||||
|
search.setCaseSensitive(True)
|
||||||
|
assert pruneResult(search.iterSearch(project, "Lorem"), 2) == [(15, 5, "Lorem")]
|
||||||
|
search.setCaseSensitive(False)
|
||||||
|
|
||||||
|
# END Test testCoreTools_DocSearch
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreTools_ProjectBuilderWrapper(monkeypatch, caplog, fncPath, mockGUI):
|
def testCoreTools_ProjectBuilderWrapper(monkeypatch, caplog, fncPath, mockGUI):
|
||||||
"""Test the wrapper function of the project builder."""
|
"""Test the wrapper function of the project builder."""
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
|||||||
|
|
||||||
novelView.setTreeFocus()
|
novelView.setTreeFocus()
|
||||||
|
|
||||||
nwGUI.projStack.setCurrentIndex(nwGUI.idxNovelView)
|
nwGUI.projStack.setCurrentWidget(nwGUI.novelView)
|
||||||
nwGUI.rebuildIndex()
|
nwGUI.rebuildIndex()
|
||||||
novelTree._populateTree(rootHandle=None)
|
novelTree._populateTree(rootHandle=None)
|
||||||
assert novelTree.topLevelItemCount() == 3
|
assert novelTree.topLevelItemCount() == 3
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
novelWriter – Main GUI Project Search Tester
|
||||||
|
============================================
|
||||||
|
|
||||||
|
This file is a part of novelWriter
|
||||||
|
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but
|
||||||
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtWidgets import QAction
|
||||||
|
|
||||||
|
from novelwriter.enum import nwView
|
||||||
|
from novelwriter.gui.search import GuiProjectSearch
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.gui
|
||||||
|
def testGuiDocSearch_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||||
|
"""Test navigating the novel tree."""
|
||||||
|
nwGUI.openProject(prjLipsum)
|
||||||
|
nwGUI._changeView(nwView.SEARCH)
|
||||||
|
search = nwGUI.projSearch
|
||||||
|
|
||||||
|
def totalCount():
|
||||||
|
nonlocal search
|
||||||
|
res = search.searchResult
|
||||||
|
return sum(
|
||||||
|
int(res.topLevelItem(i).text(GuiProjectSearch.C_COUNT).strip("()"))
|
||||||
|
for i in range(res.topLevelItemCount())
|
||||||
|
)
|
||||||
|
|
||||||
|
# Plain search
|
||||||
|
search.searchText.setText("Lorem")
|
||||||
|
search.searchAction.activate(QAction.ActionEvent.Trigger)
|
||||||
|
assert search.searchResult.topLevelItemCount() == 14
|
||||||
|
assert totalCount() == 42
|
||||||
|
|
||||||
|
firstDoc = search.searchResult.topLevelItem(0)
|
||||||
|
firstResult = firstDoc.child(0)
|
||||||
|
assert firstDoc is not None
|
||||||
|
handle = firstDoc.data(GuiProjectSearch.C_RESULT, GuiProjectSearch.D_HANDLE)
|
||||||
|
result = firstResult.data(GuiProjectSearch.C_RESULT, GuiProjectSearch.D_RESULT)
|
||||||
|
assert result == (handle, 3, 5)
|
||||||
|
|
||||||
|
# Move down
|
||||||
|
qtbot.keyClick(search, Qt.Key.Key_Down)
|
||||||
|
assert firstDoc.isSelected() is True
|
||||||
|
|
||||||
|
# Move up
|
||||||
|
qtbot.keyClick(search, Qt.Key.Key_Up)
|
||||||
|
assert firstDoc.isSelected() is False
|
||||||
|
|
||||||
|
# Move right does nothing
|
||||||
|
qtbot.keyClick(search, Qt.Key.Key_Right)
|
||||||
|
assert firstDoc.isSelected() is False
|
||||||
|
|
||||||
|
# Selecting updates details
|
||||||
|
firstDoc.setSelected(True)
|
||||||
|
assert nwGUI.itemDetails._handle == handle
|
||||||
|
|
||||||
|
# Press return
|
||||||
|
search.searchResult.setFocus()
|
||||||
|
search.searchResult.clearSelection()
|
||||||
|
firstResult.setSelected(True)
|
||||||
|
with monkeypatch.context() as mp:
|
||||||
|
mp.setattr(search.searchResult, "hasFocus", lambda *a: True)
|
||||||
|
with qtbot.waitSignal(search.openDocumentSelectRequest, timeout=1000) as signal:
|
||||||
|
qtbot.keyClick(search, Qt.Key.Key_Return)
|
||||||
|
assert signal.args == [handle, 3, 5, False]
|
||||||
|
|
||||||
|
assert nwGUI.docEditor.docHandle == handle
|
||||||
|
assert nwGUI.docEditor.textCursor().selectedText() == "Lorem"
|
||||||
|
|
||||||
|
# Double-click
|
||||||
|
with qtbot.waitSignal(search.openDocumentSelectRequest, timeout=1000) as signal:
|
||||||
|
search._searchResultDoubleClicked(firstResult, 0)
|
||||||
|
assert signal.args == [handle, 3, 5, True]
|
||||||
|
|
||||||
|
# Case Sensitive
|
||||||
|
search.toggleCase.setChecked(True)
|
||||||
|
search.searchAction.activate(QAction.ActionEvent.Trigger)
|
||||||
|
assert search.searchResult.topLevelItemCount() == 7
|
||||||
|
assert totalCount() == 17
|
||||||
|
search.toggleCase.setChecked(False)
|
||||||
|
|
||||||
|
# Whole Words
|
||||||
|
search.searchText.setText("dolor")
|
||||||
|
with monkeypatch.context() as mp:
|
||||||
|
mp.setattr(search.searchText, "hasFocus", lambda *a: True)
|
||||||
|
qtbot.keyClick(search, Qt.Key.Key_Return)
|
||||||
|
|
||||||
|
assert search.searchResult.topLevelItemCount() == 10
|
||||||
|
assert totalCount() == 34
|
||||||
|
|
||||||
|
search.toggleWord.setChecked(True)
|
||||||
|
search.searchAction.activate(QAction.ActionEvent.Trigger)
|
||||||
|
assert search.searchResult.topLevelItemCount() == 10
|
||||||
|
assert totalCount() == 33
|
||||||
|
|
||||||
|
# RegEx
|
||||||
|
search.toggleRegEx.setChecked(True)
|
||||||
|
search.searchText.setText("(dolor|dolorem)")
|
||||||
|
search.searchAction.activate(QAction.ActionEvent.Trigger)
|
||||||
|
assert search.searchResult.topLevelItemCount() == 10
|
||||||
|
assert totalCount() == 34
|
||||||
|
|
||||||
|
# qtbot.stop()
|
||||||
|
nwGUI.closeProject()
|
||||||
|
|
||||||
|
# END Test testGuiDocSearch_Main
|
||||||
Reference in New Issue
Block a user