novelWriter 2.3rc1__py3-none-any.whl → 2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/METADATA +5 -6
- {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/RECORD +119 -109
- {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/WHEEL +1 -1
- novelWriter-2.4.dist-info/entry_points.txt +2 -0
- novelwriter/__init__.py +17 -10
- novelwriter/assets/i18n/nw_de_DE.qm +0 -0
- novelwriter/assets/i18n/nw_en_US.qm +0 -0
- novelwriter/assets/i18n/nw_es_419.qm +0 -0
- novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
- novelwriter/assets/i18n/nw_it_IT.qm +0 -0
- novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
- novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
- novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
- novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
- novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
- novelwriter/assets/i18n/project_nl_NL.json +11 -0
- novelwriter/assets/i18n/project_pt_BR.json +11 -0
- novelwriter/assets/icons/none.svg +4 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
- novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
- novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
- novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
- novelwriter/assets/icons/typicons_dark/typ_unfold-hidden.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_unfold-visible.svg +4 -0
- novelwriter/assets/icons/typicons_light/icons.conf +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
- novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
- novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
- novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
- novelwriter/assets/icons/typicons_light/typ_unfold-hidden.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_unfold-visible.svg +4 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
- novelwriter/assets/syntax/default_dark.conf +1 -0
- novelwriter/assets/syntax/default_light.conf +1 -0
- novelwriter/assets/syntax/grey_dark.conf +1 -0
- novelwriter/assets/syntax/grey_light.conf +1 -0
- novelwriter/assets/syntax/light_owl.conf +1 -0
- novelwriter/assets/syntax/night_owl.conf +1 -0
- novelwriter/assets/syntax/solarized_dark.conf +1 -0
- novelwriter/assets/syntax/solarized_light.conf +1 -0
- novelwriter/assets/syntax/tango.conf +23 -0
- novelwriter/assets/syntax/tomorrow.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
- novelwriter/assets/text/credits_en.htm +25 -23
- novelwriter/assets/themes/cyberpunk_night.conf +29 -0
- novelwriter/common.py +12 -4
- novelwriter/config.py +47 -16
- novelwriter/constants.py +5 -6
- novelwriter/core/buildsettings.py +64 -44
- novelwriter/core/coretools.py +97 -13
- novelwriter/core/docbuild.py +74 -7
- novelwriter/core/document.py +24 -3
- novelwriter/core/index.py +31 -112
- novelwriter/core/project.py +11 -15
- novelwriter/core/projectxml.py +3 -2
- novelwriter/core/sessions.py +2 -2
- novelwriter/core/spellcheck.py +3 -3
- novelwriter/core/status.py +6 -5
- novelwriter/core/storage.py +16 -6
- novelwriter/core/tohtml.py +22 -25
- novelwriter/core/tokenizer.py +417 -237
- novelwriter/core/tomd.py +17 -8
- novelwriter/core/toodt.py +386 -351
- novelwriter/core/tree.py +8 -8
- novelwriter/dialogs/about.py +10 -12
- novelwriter/dialogs/docmerge.py +17 -14
- novelwriter/dialogs/docsplit.py +20 -19
- novelwriter/dialogs/editlabel.py +5 -4
- novelwriter/dialogs/preferences.py +32 -40
- novelwriter/dialogs/projectsettings.py +31 -28
- novelwriter/dialogs/quotes.py +10 -9
- novelwriter/dialogs/wordlist.py +18 -15
- novelwriter/enum.py +17 -14
- novelwriter/error.py +14 -12
- novelwriter/extensions/circularprogress.py +12 -8
- novelwriter/extensions/configlayout.py +23 -3
- novelwriter/extensions/modified.py +51 -2
- novelwriter/extensions/pagedsidebar.py +16 -14
- novelwriter/extensions/simpleprogress.py +3 -1
- novelwriter/extensions/statusled.py +3 -1
- novelwriter/extensions/switch.py +10 -9
- novelwriter/extensions/switchbox.py +14 -13
- novelwriter/extensions/versioninfo.py +1 -1
- novelwriter/gui/doceditor.py +433 -496
- novelwriter/gui/dochighlight.py +54 -33
- novelwriter/gui/docviewer.py +162 -175
- novelwriter/gui/docviewerpanel.py +20 -37
- novelwriter/gui/editordocument.py +15 -4
- novelwriter/gui/itemdetails.py +51 -54
- novelwriter/gui/mainmenu.py +37 -17
- novelwriter/gui/noveltree.py +31 -37
- novelwriter/gui/outline.py +120 -98
- novelwriter/gui/projtree.py +114 -112
- novelwriter/gui/search.py +362 -0
- novelwriter/gui/sidebar.py +36 -45
- novelwriter/gui/statusbar.py +14 -14
- novelwriter/gui/theme.py +116 -34
- novelwriter/guimain.py +216 -207
- novelwriter/shared.py +31 -6
- novelwriter/text/counting.py +138 -0
- novelwriter/tools/dictionaries.py +15 -14
- novelwriter/tools/lipsum.py +20 -17
- novelwriter/tools/manusbuild.py +43 -35
- novelwriter/tools/manuscript.py +381 -104
- novelwriter/tools/manussettings.py +263 -125
- novelwriter/tools/noveldetails.py +21 -19
- novelwriter/tools/welcome.py +59 -57
- novelwriter/tools/writingstats.py +61 -55
- novelwriter/types.py +90 -0
- novelWriter-2.3rc1.dist-info/entry_points.txt +0 -5
- novelwriter/core/__init__.py +0 -3
- novelwriter/dialogs/__init__.py +0 -3
- novelwriter/extensions/__init__.py +0 -3
- novelwriter/gui/__init__.py +0 -3
- novelwriter/tools/__init__.py +0 -3
- {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,362 @@
|
|
1
|
+
"""
|
2
|
+
novelWriter – GUI Project Search
|
3
|
+
================================
|
4
|
+
|
5
|
+
File History:
|
6
|
+
Created: 2024-03-21 [2.4b1] GuiProjectSearch
|
7
|
+
|
8
|
+
This file is a part of novelWriter
|
9
|
+
Copyright 2018–2024, Veronica Berglyd Olsen
|
10
|
+
|
11
|
+
This program is free software: you can redistribute it and/or modify
|
12
|
+
it under the terms of the GNU General Public License as published by
|
13
|
+
the Free Software Foundation, either version 3 of the License, or
|
14
|
+
(at your option) any later version.
|
15
|
+
|
16
|
+
This program is distributed in the hope that it will be useful, but
|
17
|
+
WITHOUT ANY WARRANTY; without even the implied warranty of
|
18
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
19
|
+
General Public License for more details.
|
20
|
+
|
21
|
+
You should have received a copy of the GNU General Public License
|
22
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
23
|
+
"""
|
24
|
+
from __future__ import annotations
|
25
|
+
|
26
|
+
import logging
|
27
|
+
|
28
|
+
from time import time
|
29
|
+
|
30
|
+
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
|
31
|
+
from PyQt5.QtGui import QCursor, QKeyEvent
|
32
|
+
from PyQt5.QtWidgets import (
|
33
|
+
QApplication, QFrame, QHBoxLayout, QHeaderView, QLabel, QLineEdit,
|
34
|
+
QToolBar, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
|
35
|
+
)
|
36
|
+
|
37
|
+
from novelwriter import CONFIG, SHARED
|
38
|
+
from novelwriter.common import checkInt, cssCol
|
39
|
+
from novelwriter.core.coretools import DocSearch
|
40
|
+
from novelwriter.core.item import NWItem
|
41
|
+
from novelwriter.types import QtAlignMiddle, QtAlignRight, QtUserRole
|
42
|
+
|
43
|
+
logger = logging.getLogger(__name__)
|
44
|
+
|
45
|
+
|
46
|
+
class GuiProjectSearch(QWidget):
|
47
|
+
|
48
|
+
C_NAME = 0
|
49
|
+
C_RESULT = 0
|
50
|
+
C_COUNT = 1
|
51
|
+
|
52
|
+
D_HANDLE = QtUserRole
|
53
|
+
D_RESULT = QtUserRole + 1
|
54
|
+
|
55
|
+
selectedItemChanged = pyqtSignal(str)
|
56
|
+
openDocumentSelectRequest = pyqtSignal(str, int, int, bool)
|
57
|
+
|
58
|
+
def __init__(self, parent: QWidget) -> None:
|
59
|
+
super().__init__(parent=parent)
|
60
|
+
|
61
|
+
logger.debug("Create: GuiProjectSearch")
|
62
|
+
|
63
|
+
iPx = SHARED.theme.baseIconHeight
|
64
|
+
iSz = SHARED.theme.baseIconSize
|
65
|
+
mPx = CONFIG.pxInt(2)
|
66
|
+
tPx = CONFIG.pxInt(4)
|
67
|
+
|
68
|
+
self._time = time()
|
69
|
+
self._search = DocSearch()
|
70
|
+
self._blocked = False
|
71
|
+
self._map: dict[str, tuple[int, float]] = {}
|
72
|
+
|
73
|
+
# Header
|
74
|
+
self.viewLabel = QLabel(self.tr("Project Search"), self)
|
75
|
+
self.viewLabel.setFont(SHARED.theme.guiFontB)
|
76
|
+
self.viewLabel.setContentsMargins(mPx, tPx, 0, mPx)
|
77
|
+
|
78
|
+
# Options
|
79
|
+
self.searchOpt = QToolBar(self)
|
80
|
+
self.searchOpt.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
81
|
+
self.searchOpt.setIconSize(iSz)
|
82
|
+
self.searchOpt.setContentsMargins(0, 0, 0, 0)
|
83
|
+
|
84
|
+
self.toggleCase = self.searchOpt.addAction(self.tr("Case Sensitive"))
|
85
|
+
self.toggleCase.setCheckable(True)
|
86
|
+
self.toggleCase.setChecked(CONFIG.searchProjCase)
|
87
|
+
self.toggleCase.toggled.connect(self._toggleCase)
|
88
|
+
|
89
|
+
self.toggleWord = self.searchOpt.addAction(self.tr("Whole Words Only"))
|
90
|
+
self.toggleWord.setCheckable(True)
|
91
|
+
self.toggleWord.setChecked(CONFIG.searchProjWord)
|
92
|
+
self.toggleWord.toggled.connect(self._toggleWord)
|
93
|
+
|
94
|
+
self.toggleRegEx = self.searchOpt.addAction(self.tr("RegEx Mode"))
|
95
|
+
self.toggleRegEx.setCheckable(True)
|
96
|
+
self.toggleRegEx.setChecked(CONFIG.searchProjRegEx)
|
97
|
+
self.toggleRegEx.toggled.connect(self._toggleRegEx)
|
98
|
+
|
99
|
+
# Search Box
|
100
|
+
self.searchText = QLineEdit(self)
|
101
|
+
self.searchText.setPlaceholderText(self.tr("Search for"))
|
102
|
+
self.searchText.setClearButtonEnabled(True)
|
103
|
+
|
104
|
+
self.searchAction = self.searchText.addAction(
|
105
|
+
SHARED.theme.getIcon("search"), QLineEdit.ActionPosition.TrailingPosition
|
106
|
+
)
|
107
|
+
self.searchAction.triggered.connect(self._processSearch)
|
108
|
+
|
109
|
+
# Search Result
|
110
|
+
self.searchResult = QTreeWidget(self)
|
111
|
+
self.searchResult.setHeaderHidden(True)
|
112
|
+
self.searchResult.setColumnCount(2)
|
113
|
+
self.searchResult.setIconSize(iSz)
|
114
|
+
self.searchResult.setIndentation(iPx)
|
115
|
+
self.searchResult.setFrameStyle(QFrame.Shape.NoFrame)
|
116
|
+
self.searchResult.setUniformRowHeights(True)
|
117
|
+
self.searchResult.setAllColumnsShowFocus(True)
|
118
|
+
self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked)
|
119
|
+
self.searchResult.itemSelectionChanged.connect(self._searchResultSelected)
|
120
|
+
|
121
|
+
treeHeader = self.searchResult.header()
|
122
|
+
treeHeader.setStretchLastSection(False)
|
123
|
+
treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.Stretch)
|
124
|
+
treeHeader.setSectionResizeMode(self.C_COUNT, QHeaderView.ResizeMode.ResizeToContents)
|
125
|
+
|
126
|
+
# Assemble
|
127
|
+
self.headerBox = QHBoxLayout()
|
128
|
+
self.headerBox.addWidget(self.viewLabel, 1)
|
129
|
+
self.headerBox.addWidget(self.searchOpt, 0, QtAlignMiddle)
|
130
|
+
self.headerBox.setContentsMargins(0, 0, 0, 0)
|
131
|
+
self.headerBox.setSpacing(0)
|
132
|
+
|
133
|
+
self.headerWidget = QWidget(self)
|
134
|
+
self.headerWidget.setLayout(self.headerBox)
|
135
|
+
self.headerWidget.setContentsMargins(0, 0, 0, 0)
|
136
|
+
|
137
|
+
self.outerBox = QVBoxLayout()
|
138
|
+
self.outerBox.addWidget(self.headerWidget, 0)
|
139
|
+
self.outerBox.addWidget(self.searchText, 0)
|
140
|
+
self.outerBox.addWidget(self.searchResult, 1)
|
141
|
+
self.outerBox.setContentsMargins(0, 0, 0, 0)
|
142
|
+
self.outerBox.setSpacing(mPx)
|
143
|
+
|
144
|
+
self.setLayout(self.outerBox)
|
145
|
+
self.updateTheme()
|
146
|
+
|
147
|
+
logger.debug("Ready: GuiProjectSearch")
|
148
|
+
|
149
|
+
return
|
150
|
+
|
151
|
+
##
|
152
|
+
# Methods
|
153
|
+
##
|
154
|
+
|
155
|
+
def updateTheme(self) -> None:
|
156
|
+
"""Update theme elements."""
|
157
|
+
bPx = CONFIG.pxInt(1)
|
158
|
+
mPx = CONFIG.pxInt(2)
|
159
|
+
|
160
|
+
qPalette = self.palette()
|
161
|
+
colBase = cssCol(qPalette.base().color())
|
162
|
+
colFocus = cssCol(qPalette.highlight().color())
|
163
|
+
|
164
|
+
self.headerWidget.setStyleSheet(f"QWidget {{background: {colBase};}}")
|
165
|
+
self.headerWidget.setAutoFillBackground(True)
|
166
|
+
|
167
|
+
self.setStyleSheet(
|
168
|
+
"QToolBar {padding: 0; background: none;} "
|
169
|
+
f"QLineEdit {{border: {bPx}px solid {colBase}; padding: {mPx}px;}} "
|
170
|
+
f"QLineEdit:focus {{border: {bPx}px solid {colFocus};}} "
|
171
|
+
)
|
172
|
+
|
173
|
+
self.searchAction.setIcon(SHARED.theme.getIcon("search"))
|
174
|
+
self.toggleCase.setIcon(SHARED.theme.getIcon("search_case"))
|
175
|
+
self.toggleWord.setIcon(SHARED.theme.getIcon("search_word"))
|
176
|
+
self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex"))
|
177
|
+
|
178
|
+
return
|
179
|
+
|
180
|
+
def processReturn(self) -> None:
|
181
|
+
"""Process a return keypress forwarded from the main GUI."""
|
182
|
+
if self.searchText.hasFocus():
|
183
|
+
self._processSearch()
|
184
|
+
elif (
|
185
|
+
self.searchResult.hasFocus()
|
186
|
+
and (items := self.searchResult.selectedItems())
|
187
|
+
and (data := items[0].data(0, self.D_RESULT))
|
188
|
+
and len(data) == 3
|
189
|
+
):
|
190
|
+
self.openDocumentSelectRequest.emit(
|
191
|
+
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False
|
192
|
+
)
|
193
|
+
return
|
194
|
+
|
195
|
+
def beginSearch(self, text: str = "") -> None:
|
196
|
+
"""Focus the search box and select its text, if any."""
|
197
|
+
self.searchText.setFocus()
|
198
|
+
self.searchText.selectAll()
|
199
|
+
if text:
|
200
|
+
self.searchText.setText(text.partition("\n")[0])
|
201
|
+
self.searchText.selectAll()
|
202
|
+
return
|
203
|
+
|
204
|
+
def closeProjectTasks(self) -> None:
|
205
|
+
"""Run close project tasks."""
|
206
|
+
self._map = {}
|
207
|
+
self.searchText.clear()
|
208
|
+
self.searchResult.clear()
|
209
|
+
return
|
210
|
+
|
211
|
+
##
|
212
|
+
# Events
|
213
|
+
##
|
214
|
+
|
215
|
+
def keyPressEvent(self, event: QKeyEvent) -> None:
|
216
|
+
"""Process key press events. This handles up and down arrow key
|
217
|
+
presses to jump between search text box and result tree.
|
218
|
+
"""
|
219
|
+
if (
|
220
|
+
event.key() == Qt.Key.Key_Down
|
221
|
+
and self.searchText.hasFocus()
|
222
|
+
and (first := self.searchResult.topLevelItem(0))
|
223
|
+
):
|
224
|
+
first.setSelected(True)
|
225
|
+
self.searchResult.setFocus()
|
226
|
+
elif (
|
227
|
+
event.key() == Qt.Key.Key_Up
|
228
|
+
and self.searchResult.hasFocus()
|
229
|
+
and (first := self.searchResult.topLevelItem(0))
|
230
|
+
and first.isSelected()
|
231
|
+
):
|
232
|
+
first.setSelected(False)
|
233
|
+
self.searchText.setFocus()
|
234
|
+
else:
|
235
|
+
super().keyPressEvent(event)
|
236
|
+
return
|
237
|
+
|
238
|
+
##
|
239
|
+
# Public Slots
|
240
|
+
##
|
241
|
+
|
242
|
+
@pyqtSlot(str, float)
|
243
|
+
def textChanged(self, tHandle: str, timeStamp: float) -> None:
|
244
|
+
"""Update search result for a specific document."""
|
245
|
+
if (entry := self._map.get(tHandle)) and timeStamp > entry[1]:
|
246
|
+
start = time()
|
247
|
+
results, capped = self._search.searchText(SHARED.mainGui.docEditor.getText())
|
248
|
+
self._displayResultSet(SHARED.project.tree[tHandle], results, capped)
|
249
|
+
logger.debug("Updated search for '%s' in %.3f ms", tHandle, 1000*(time() - start))
|
250
|
+
return
|
251
|
+
|
252
|
+
##
|
253
|
+
# Private Slots
|
254
|
+
##
|
255
|
+
|
256
|
+
@pyqtSlot()
|
257
|
+
def _processSearch(self) -> None:
|
258
|
+
"""Perform a search."""
|
259
|
+
if not self._blocked:
|
260
|
+
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
|
261
|
+
start = time()
|
262
|
+
SHARED.mainGui.saveDocument()
|
263
|
+
self._blocked = True
|
264
|
+
self._map = {}
|
265
|
+
self.searchResult.clear()
|
266
|
+
if text := self.searchText.text():
|
267
|
+
self._search.setUserRegEx(self.toggleRegEx.isChecked())
|
268
|
+
self._search.setCaseSensitive(self.toggleCase.isChecked())
|
269
|
+
self._search.setWholeWords(self.toggleWord.isChecked())
|
270
|
+
for item, results, capped in self._search.iterSearch(SHARED.project, text):
|
271
|
+
self._displayResultSet(item, results, capped)
|
272
|
+
logger.debug("Search took %.3f ms", 1000*(time() - start))
|
273
|
+
self._time = time()
|
274
|
+
QApplication.restoreOverrideCursor()
|
275
|
+
self._blocked = False
|
276
|
+
return
|
277
|
+
|
278
|
+
@pyqtSlot()
|
279
|
+
def _searchResultSelected(self) -> None:
|
280
|
+
"""Process search result selection."""
|
281
|
+
if items := self.searchResult.selectedItems():
|
282
|
+
if (data := items[0].data(0, self.D_RESULT)) and len(data) == 3:
|
283
|
+
self.selectedItemChanged.emit(str(data[0]))
|
284
|
+
elif data := items[0].data(0, self.D_HANDLE):
|
285
|
+
self.selectedItemChanged.emit(str(data))
|
286
|
+
return
|
287
|
+
|
288
|
+
@pyqtSlot("QTreeWidgetItem*", int)
|
289
|
+
def _searchResultDoubleClicked(self, item: QTreeWidgetItem, column: int) -> None:
|
290
|
+
"""Process search result double click."""
|
291
|
+
if (data := item.data(0, self.D_RESULT)) and len(data) == 3:
|
292
|
+
self.openDocumentSelectRequest.emit(
|
293
|
+
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), True
|
294
|
+
)
|
295
|
+
return
|
296
|
+
|
297
|
+
@pyqtSlot(bool)
|
298
|
+
def _toggleCase(self, state: bool) -> None:
|
299
|
+
"""Enable/disable case sensitive mode."""
|
300
|
+
CONFIG.searchProjCase = state
|
301
|
+
return
|
302
|
+
|
303
|
+
@pyqtSlot(bool)
|
304
|
+
def _toggleWord(self, state: bool) -> None:
|
305
|
+
"""Enable/disable whole word search mode."""
|
306
|
+
CONFIG.searchProjWord = state
|
307
|
+
return
|
308
|
+
|
309
|
+
@pyqtSlot(bool)
|
310
|
+
def _toggleRegEx(self, state: bool) -> None:
|
311
|
+
"""Enable/disable regular expression search mode."""
|
312
|
+
CONFIG.searchProjRegEx = state
|
313
|
+
return
|
314
|
+
|
315
|
+
##
|
316
|
+
# Internal Functions
|
317
|
+
##
|
318
|
+
|
319
|
+
def _displayResultSet(
|
320
|
+
self, nwItem: NWItem | None, results: list[tuple[int, int, str]], capped: bool
|
321
|
+
) -> None:
|
322
|
+
"""Populate the result tree."""
|
323
|
+
if results and nwItem:
|
324
|
+
tHandle = nwItem.itemHandle
|
325
|
+
docIcon = SHARED.theme.getItemIcon(
|
326
|
+
nwItem.itemType, nwItem.itemClass,
|
327
|
+
nwItem.itemLayout, nwItem.mainHeading
|
328
|
+
)
|
329
|
+
ext = "+" if capped else ""
|
330
|
+
|
331
|
+
tItem = QTreeWidgetItem()
|
332
|
+
tItem.setText(self.C_NAME, nwItem.itemName)
|
333
|
+
tItem.setIcon(self.C_NAME, docIcon)
|
334
|
+
tItem.setData(self.C_NAME, self.D_HANDLE, tHandle)
|
335
|
+
tItem.setText(self.C_COUNT, f"({len(results):n}{ext})")
|
336
|
+
tItem.setTextAlignment(self.C_COUNT, QtAlignRight)
|
337
|
+
tItem.setForeground(self.C_COUNT, self.palette().highlight())
|
338
|
+
|
339
|
+
index = self._map.get(tHandle, (self.searchResult.topLevelItemCount(), 0.0))[0]
|
340
|
+
self.searchResult.takeTopLevelItem(index)
|
341
|
+
self.searchResult.insertTopLevelItem(index, tItem)
|
342
|
+
self._map[tHandle] = (index, time())
|
343
|
+
|
344
|
+
rItems = []
|
345
|
+
for start, length, context in results:
|
346
|
+
rItem = QTreeWidgetItem()
|
347
|
+
rItem.setText(0, context)
|
348
|
+
rItem.setData(0, self.D_RESULT, (tHandle, start, length))
|
349
|
+
rItems.append(rItem)
|
350
|
+
|
351
|
+
tItem.addChildren(rItems)
|
352
|
+
tItem.setExpanded(True)
|
353
|
+
|
354
|
+
parent = self.searchResult.indexFromItem(tItem)
|
355
|
+
for i in range(tItem.childCount()):
|
356
|
+
self.searchResult.setFirstColumnSpanned(i, parent, True)
|
357
|
+
|
358
|
+
QApplication.processEvents()
|
359
|
+
|
360
|
+
return
|
361
|
+
|
362
|
+
# END Class GuiProjectSearch
|
novelwriter/gui/sidebar.py
CHANGED
@@ -28,12 +28,14 @@ import logging
|
|
28
28
|
from typing import TYPE_CHECKING
|
29
29
|
|
30
30
|
from PyQt5.QtGui import QPalette
|
31
|
-
from PyQt5.QtCore import QEvent, QPoint,
|
32
|
-
from PyQt5.QtWidgets import QMenu,
|
31
|
+
from PyQt5.QtCore import QEvent, QPoint, QSize, pyqtSignal
|
32
|
+
from PyQt5.QtWidgets import QMenu, QVBoxLayout, QWidget
|
33
33
|
|
34
34
|
from novelwriter import CONFIG, SHARED
|
35
35
|
from novelwriter.enum import nwView
|
36
36
|
from novelwriter.extensions.eventfilters import StatusTipFilter
|
37
|
+
from novelwriter.extensions.modified import NIconToolButton
|
38
|
+
from novelwriter.gui.theme import STYLES_BIG_TOOLBUTTON
|
37
39
|
|
38
40
|
if TYPE_CHECKING: # pragma: no cover
|
39
41
|
from novelwriter.guimain import GuiMain
|
@@ -43,7 +45,7 @@ logger = logging.getLogger(__name__)
|
|
43
45
|
|
44
46
|
class GuiSideBar(QWidget):
|
45
47
|
|
46
|
-
|
48
|
+
requestViewChange = pyqtSignal(nwView)
|
47
49
|
|
48
50
|
def __init__(self, mainGui: GuiMain) -> None:
|
49
51
|
super().__init__(parent=mainGui)
|
@@ -52,47 +54,44 @@ class GuiSideBar(QWidget):
|
|
52
54
|
|
53
55
|
self.mainGui = mainGui
|
54
56
|
|
55
|
-
iPx =
|
56
|
-
|
57
|
+
iPx = int(1.2*SHARED.theme.baseButtonHeight)
|
58
|
+
iSz = QSize(iPx, iPx)
|
59
|
+
|
57
60
|
self.setContentsMargins(0, 0, 0, 0)
|
58
61
|
self.installEventFilter(StatusTipFilter(mainGui))
|
59
62
|
|
60
63
|
# Buttons
|
61
|
-
self.tbProject =
|
64
|
+
self.tbProject = NIconToolButton(self, iSz)
|
62
65
|
self.tbProject.setToolTip("{0} [Ctrl+T]".format(self.tr("Project Tree View")))
|
63
|
-
self.tbProject.
|
64
|
-
self.tbProject.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT))
|
66
|
+
self.tbProject.clicked.connect(lambda: self.requestViewChange.emit(nwView.PROJECT))
|
65
67
|
|
66
|
-
self.tbNovel =
|
68
|
+
self.tbNovel = NIconToolButton(self, iSz)
|
67
69
|
self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View")))
|
68
|
-
self.tbNovel.
|
69
|
-
|
70
|
+
self.tbNovel.clicked.connect(lambda: self.requestViewChange.emit(nwView.NOVEL))
|
71
|
+
|
72
|
+
self.tbSearch = NIconToolButton(self, iSz)
|
73
|
+
self.tbSearch.setToolTip("{0} [Ctrl+Shift+F]".format(self.tr("Project Search")))
|
74
|
+
self.tbSearch.clicked.connect(lambda: self.requestViewChange.emit(nwView.SEARCH))
|
70
75
|
|
71
|
-
self.tbOutline =
|
76
|
+
self.tbOutline = NIconToolButton(self, iSz)
|
72
77
|
self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
|
73
|
-
self.tbOutline.
|
74
|
-
self.tbOutline.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE))
|
78
|
+
self.tbOutline.clicked.connect(lambda: self.requestViewChange.emit(nwView.OUTLINE))
|
75
79
|
|
76
|
-
self.tbBuild =
|
80
|
+
self.tbBuild = NIconToolButton(self, iSz)
|
77
81
|
self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
|
78
|
-
self.tbBuild.setIconSize(iconSize)
|
79
82
|
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
|
80
83
|
|
81
|
-
self.tbDetails =
|
84
|
+
self.tbDetails = NIconToolButton(self, iSz)
|
82
85
|
self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details")))
|
83
|
-
self.tbDetails.setIconSize(iconSize)
|
84
86
|
self.tbDetails.clicked.connect(self.mainGui.showNovelDetailsDialog)
|
85
87
|
|
86
|
-
self.tbStats =
|
88
|
+
self.tbStats = NIconToolButton(self, iSz)
|
87
89
|
self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics")))
|
88
|
-
self.tbStats.setIconSize(iconSize)
|
89
90
|
self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog)
|
90
91
|
|
91
92
|
# Settings Menu
|
92
|
-
self.tbSettings =
|
93
|
+
self.tbSettings = NIconToolButton(self, iSz)
|
93
94
|
self.tbSettings.setToolTip(self.tr("Settings"))
|
94
|
-
self.tbSettings.setIconSize(iconSize)
|
95
|
-
self.tbSettings.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
96
95
|
|
97
96
|
self.mSettings = _PopRightMenu(self.tbSettings)
|
98
97
|
self.mSettings.addAction(self.mainGui.mainMenu.aEditWordList)
|
@@ -101,12 +100,12 @@ class GuiSideBar(QWidget):
|
|
101
100
|
self.mSettings.addAction(self.mainGui.mainMenu.aPreferences)
|
102
101
|
|
103
102
|
self.tbSettings.setMenu(self.mSettings)
|
104
|
-
self.tbSettings.setPopupMode(QToolButton.InstantPopup)
|
105
103
|
|
106
104
|
# Assemble
|
107
105
|
self.outerBox = QVBoxLayout()
|
108
106
|
self.outerBox.addWidget(self.tbProject)
|
109
107
|
self.outerBox.addWidget(self.tbNovel)
|
108
|
+
self.outerBox.addWidget(self.tbSearch)
|
110
109
|
self.outerBox.addWidget(self.tbOutline)
|
111
110
|
self.outerBox.addWidget(self.tbBuild)
|
112
111
|
self.outerBox.addStretch(1)
|
@@ -126,36 +125,28 @@ class GuiSideBar(QWidget):
|
|
126
125
|
def updateTheme(self) -> None:
|
127
126
|
"""Initialise GUI elements that depend on specific settings."""
|
128
127
|
qPalette = self.palette()
|
129
|
-
qPalette.setBrush(QPalette.Window, qPalette.base())
|
128
|
+
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
|
130
129
|
self.setPalette(qPalette)
|
131
130
|
|
132
|
-
|
133
|
-
buttonStyle = (
|
134
|
-
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
|
135
|
-
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
|
136
|
-
).format(CONFIG.pxInt(6), fadeCol.red(), fadeCol.green(), fadeCol.blue())
|
137
|
-
buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
|
131
|
+
buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON)
|
138
132
|
|
139
|
-
self.tbProject.setIcon(SHARED.theme.getIcon("view_editor"))
|
140
133
|
self.tbProject.setStyleSheet(buttonStyle)
|
141
|
-
|
142
|
-
self.tbNovel.setIcon(SHARED.theme.getIcon("view_novel"))
|
143
134
|
self.tbNovel.setStyleSheet(buttonStyle)
|
144
|
-
|
145
|
-
self.tbOutline.setIcon(SHARED.theme.getIcon("view_outline"))
|
135
|
+
self.tbSearch.setStyleSheet(buttonStyle)
|
146
136
|
self.tbOutline.setStyleSheet(buttonStyle)
|
147
|
-
|
148
|
-
self.tbBuild.setIcon(SHARED.theme.getIcon("view_build"))
|
149
137
|
self.tbBuild.setStyleSheet(buttonStyle)
|
150
|
-
|
151
|
-
self.tbDetails.setIcon(SHARED.theme.getIcon("proj_details"))
|
152
138
|
self.tbDetails.setStyleSheet(buttonStyle)
|
153
|
-
|
154
|
-
self.tbStats.setIcon(SHARED.theme.getIcon("proj_stats"))
|
155
139
|
self.tbStats.setStyleSheet(buttonStyle)
|
140
|
+
self.tbSettings.setStyleSheet(buttonStyle)
|
156
141
|
|
157
|
-
self.
|
158
|
-
self.
|
142
|
+
self.tbProject.setThemeIcon("view_editor")
|
143
|
+
self.tbNovel.setThemeIcon("view_novel")
|
144
|
+
self.tbSearch.setThemeIcon("view_search")
|
145
|
+
self.tbOutline.setThemeIcon("view_outline")
|
146
|
+
self.tbBuild.setThemeIcon("view_build")
|
147
|
+
self.tbDetails.setThemeIcon("proj_details")
|
148
|
+
self.tbStats.setThemeIcon("proj_stats")
|
149
|
+
self.tbSettings.setThemeIcon("settings")
|
159
150
|
|
160
151
|
return
|
161
152
|
|
@@ -166,7 +157,7 @@ class _PopRightMenu(QMenu):
|
|
166
157
|
|
167
158
|
def event(self, event: QEvent) -> bool:
|
168
159
|
"""Overload the show event and move the menu popup location."""
|
169
|
-
if event.type() == QEvent.Show:
|
160
|
+
if event.type() == QEvent.Type.Show:
|
170
161
|
if isinstance(parent := self.parent(), QWidget):
|
171
162
|
offset = QPoint(parent.width(), parent.height() - self.height())
|
172
163
|
self.move(parent.mapToGlobal(offset))
|
novelwriter/gui/statusbar.py
CHANGED
@@ -25,12 +25,12 @@ from __future__ import annotations
|
|
25
25
|
|
26
26
|
import logging
|
27
27
|
|
28
|
+
from datetime import datetime
|
28
29
|
from time import time
|
29
30
|
from typing import TYPE_CHECKING, Literal
|
30
|
-
from datetime import datetime
|
31
31
|
|
32
32
|
from PyQt5.QtCore import pyqtSlot, QLocale
|
33
|
-
from PyQt5.QtWidgets import
|
33
|
+
from PyQt5.QtWidgets import QApplication, QStatusBar, QLabel
|
34
34
|
|
35
35
|
from novelwriter import CONFIG, SHARED
|
36
36
|
from novelwriter.common import formatTime
|
@@ -58,7 +58,7 @@ class GuiMainStatus(QStatusBar):
|
|
58
58
|
colSaved = SHARED.theme.statSaved
|
59
59
|
colUnsaved = SHARED.theme.statUnsaved
|
60
60
|
|
61
|
-
iPx = SHARED.theme.
|
61
|
+
iPx = SHARED.theme.baseIconHeight
|
62
62
|
|
63
63
|
# Permanent Widgets
|
64
64
|
# =================
|
@@ -66,8 +66,8 @@ class GuiMainStatus(QStatusBar):
|
|
66
66
|
xM = CONFIG.pxInt(8)
|
67
67
|
|
68
68
|
# The Spell Checker Language
|
69
|
-
self.langIcon = QLabel("")
|
70
|
-
self.langText = QLabel(self.tr("None"))
|
69
|
+
self.langIcon = QLabel("", self)
|
70
|
+
self.langText = QLabel(self.tr("None"), self)
|
71
71
|
self.langIcon.setContentsMargins(0, 0, 0, 0)
|
72
72
|
self.langText.setContentsMargins(0, 0, xM, 0)
|
73
73
|
self.addPermanentWidget(self.langIcon)
|
@@ -75,7 +75,7 @@ class GuiMainStatus(QStatusBar):
|
|
75
75
|
|
76
76
|
# The Editor Status
|
77
77
|
self.docIcon = StatusLED(colNone, colSaved, colUnsaved, iPx, iPx, self)
|
78
|
-
self.docText = QLabel(self.tr("Editor"))
|
78
|
+
self.docText = QLabel(self.tr("Editor"), self)
|
79
79
|
self.docIcon.setContentsMargins(0, 0, 0, 0)
|
80
80
|
self.docText.setContentsMargins(0, 0, xM, 0)
|
81
81
|
self.addPermanentWidget(self.docIcon)
|
@@ -83,15 +83,15 @@ class GuiMainStatus(QStatusBar):
|
|
83
83
|
|
84
84
|
# The Project Status
|
85
85
|
self.projIcon = StatusLED(colNone, colSaved, colUnsaved, iPx, iPx, self)
|
86
|
-
self.projText = QLabel(self.tr("Project"))
|
86
|
+
self.projText = QLabel(self.tr("Project"), self)
|
87
87
|
self.projIcon.setContentsMargins(0, 0, 0, 0)
|
88
88
|
self.projText.setContentsMargins(0, 0, xM, 0)
|
89
89
|
self.addPermanentWidget(self.projIcon)
|
90
90
|
self.addPermanentWidget(self.projText)
|
91
91
|
|
92
92
|
# The Project and Session Stats
|
93
|
-
self.statsIcon = QLabel()
|
94
|
-
self.statsText = QLabel("")
|
93
|
+
self.statsIcon = QLabel(self)
|
94
|
+
self.statsText = QLabel("", self)
|
95
95
|
self.statsIcon.setContentsMargins(0, 0, 0, 0)
|
96
96
|
self.statsText.setContentsMargins(0, 0, xM, 0)
|
97
97
|
self.addPermanentWidget(self.statsIcon)
|
@@ -99,8 +99,8 @@ class GuiMainStatus(QStatusBar):
|
|
99
99
|
|
100
100
|
# The Session Clock
|
101
101
|
# Set the minimum width so the label doesn't rescale every second
|
102
|
-
self.timeIcon = QLabel()
|
103
|
-
self.timeText = QLabel("")
|
102
|
+
self.timeIcon = QLabel(self)
|
103
|
+
self.timeText = QLabel("", self)
|
104
104
|
self.timeText.setToolTip(self.tr("Session Time"))
|
105
105
|
self.timeText.setMinimumWidth(SHARED.theme.getTextWidth("00:00:00:"))
|
106
106
|
self.timeIcon.setContentsMargins(0, 0, 0, 0)
|
@@ -130,7 +130,7 @@ class GuiMainStatus(QStatusBar):
|
|
130
130
|
|
131
131
|
def updateTheme(self) -> None:
|
132
132
|
"""Update theme elements."""
|
133
|
-
iPx = SHARED.theme.
|
133
|
+
iPx = SHARED.theme.baseIconHeight
|
134
134
|
self.langIcon.setPixmap(SHARED.theme.getPixmap("status_lang", (iPx, iPx)))
|
135
135
|
self.statsIcon.setPixmap(SHARED.theme.getPixmap("status_stats", (iPx, iPx)))
|
136
136
|
self.timePixmap = SHARED.theme.getPixmap("status_time", (iPx, iPx))
|
@@ -198,7 +198,7 @@ class GuiMainStatus(QStatusBar):
|
|
198
198
|
def setStatusMessage(self, message: str) -> None:
|
199
199
|
"""Set the status bar message to display."""
|
200
200
|
self.showMessage(message, nwConst.STATUS_MSG_TIMEOUT)
|
201
|
-
|
201
|
+
QApplication.processEvents()
|
202
202
|
return
|
203
203
|
|
204
204
|
@pyqtSlot(str, str)
|
@@ -240,7 +240,7 @@ class GuiMainStatus(QStatusBar):
|
|
240
240
|
import tracemalloc
|
241
241
|
from collections import Counter
|
242
242
|
|
243
|
-
widgets =
|
243
|
+
widgets = QApplication.allWidgets()
|
244
244
|
if not self._debugInfo:
|
245
245
|
if tracemalloc.is_tracing():
|
246
246
|
self._traceMallocRef = "Total"
|