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.
Files changed (125) hide show
  1. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/METADATA +5 -6
  2. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/RECORD +119 -109
  3. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/WHEEL +1 -1
  4. novelWriter-2.4.dist-info/entry_points.txt +2 -0
  5. novelwriter/__init__.py +17 -10
  6. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  7. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  8. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  9. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  10. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  11. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  12. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  13. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  14. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  15. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  16. novelwriter/assets/i18n/project_nl_NL.json +11 -0
  17. novelwriter/assets/i18n/project_pt_BR.json +11 -0
  18. novelwriter/assets/icons/none.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
  20. novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
  21. novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
  22. novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
  23. novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
  24. novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
  25. novelwriter/assets/icons/typicons_dark/typ_unfold-hidden.svg +4 -0
  26. novelwriter/assets/icons/typicons_dark/typ_unfold-visible.svg +4 -0
  27. novelwriter/assets/icons/typicons_light/icons.conf +4 -0
  28. novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
  29. novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
  30. novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
  31. novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
  32. novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
  33. novelwriter/assets/icons/typicons_light/typ_unfold-hidden.svg +4 -0
  34. novelwriter/assets/icons/typicons_light/typ_unfold-visible.svg +4 -0
  35. novelwriter/assets/manual.pdf +0 -0
  36. novelwriter/assets/sample.zip +0 -0
  37. novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
  38. novelwriter/assets/syntax/default_dark.conf +1 -0
  39. novelwriter/assets/syntax/default_light.conf +1 -0
  40. novelwriter/assets/syntax/grey_dark.conf +1 -0
  41. novelwriter/assets/syntax/grey_light.conf +1 -0
  42. novelwriter/assets/syntax/light_owl.conf +1 -0
  43. novelwriter/assets/syntax/night_owl.conf +1 -0
  44. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  45. novelwriter/assets/syntax/solarized_light.conf +1 -0
  46. novelwriter/assets/syntax/tango.conf +23 -0
  47. novelwriter/assets/syntax/tomorrow.conf +1 -0
  48. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  49. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  50. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  51. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  52. novelwriter/assets/text/credits_en.htm +25 -23
  53. novelwriter/assets/themes/cyberpunk_night.conf +29 -0
  54. novelwriter/common.py +12 -4
  55. novelwriter/config.py +47 -16
  56. novelwriter/constants.py +5 -6
  57. novelwriter/core/buildsettings.py +64 -44
  58. novelwriter/core/coretools.py +97 -13
  59. novelwriter/core/docbuild.py +74 -7
  60. novelwriter/core/document.py +24 -3
  61. novelwriter/core/index.py +31 -112
  62. novelwriter/core/project.py +11 -15
  63. novelwriter/core/projectxml.py +3 -2
  64. novelwriter/core/sessions.py +2 -2
  65. novelwriter/core/spellcheck.py +3 -3
  66. novelwriter/core/status.py +6 -5
  67. novelwriter/core/storage.py +16 -6
  68. novelwriter/core/tohtml.py +22 -25
  69. novelwriter/core/tokenizer.py +417 -237
  70. novelwriter/core/tomd.py +17 -8
  71. novelwriter/core/toodt.py +386 -351
  72. novelwriter/core/tree.py +8 -8
  73. novelwriter/dialogs/about.py +10 -12
  74. novelwriter/dialogs/docmerge.py +17 -14
  75. novelwriter/dialogs/docsplit.py +20 -19
  76. novelwriter/dialogs/editlabel.py +5 -4
  77. novelwriter/dialogs/preferences.py +32 -40
  78. novelwriter/dialogs/projectsettings.py +31 -28
  79. novelwriter/dialogs/quotes.py +10 -9
  80. novelwriter/dialogs/wordlist.py +18 -15
  81. novelwriter/enum.py +17 -14
  82. novelwriter/error.py +14 -12
  83. novelwriter/extensions/circularprogress.py +12 -8
  84. novelwriter/extensions/configlayout.py +23 -3
  85. novelwriter/extensions/modified.py +51 -2
  86. novelwriter/extensions/pagedsidebar.py +16 -14
  87. novelwriter/extensions/simpleprogress.py +3 -1
  88. novelwriter/extensions/statusled.py +3 -1
  89. novelwriter/extensions/switch.py +10 -9
  90. novelwriter/extensions/switchbox.py +14 -13
  91. novelwriter/extensions/versioninfo.py +1 -1
  92. novelwriter/gui/doceditor.py +433 -496
  93. novelwriter/gui/dochighlight.py +54 -33
  94. novelwriter/gui/docviewer.py +162 -175
  95. novelwriter/gui/docviewerpanel.py +20 -37
  96. novelwriter/gui/editordocument.py +15 -4
  97. novelwriter/gui/itemdetails.py +51 -54
  98. novelwriter/gui/mainmenu.py +37 -17
  99. novelwriter/gui/noveltree.py +31 -37
  100. novelwriter/gui/outline.py +120 -98
  101. novelwriter/gui/projtree.py +114 -112
  102. novelwriter/gui/search.py +362 -0
  103. novelwriter/gui/sidebar.py +36 -45
  104. novelwriter/gui/statusbar.py +14 -14
  105. novelwriter/gui/theme.py +116 -34
  106. novelwriter/guimain.py +216 -207
  107. novelwriter/shared.py +31 -6
  108. novelwriter/text/counting.py +138 -0
  109. novelwriter/tools/dictionaries.py +15 -14
  110. novelwriter/tools/lipsum.py +20 -17
  111. novelwriter/tools/manusbuild.py +43 -35
  112. novelwriter/tools/manuscript.py +381 -104
  113. novelwriter/tools/manussettings.py +263 -125
  114. novelwriter/tools/noveldetails.py +21 -19
  115. novelwriter/tools/welcome.py +59 -57
  116. novelwriter/tools/writingstats.py +61 -55
  117. novelwriter/types.py +90 -0
  118. novelWriter-2.3rc1.dist-info/entry_points.txt +0 -5
  119. novelwriter/core/__init__.py +0 -3
  120. novelwriter/dialogs/__init__.py +0 -3
  121. novelwriter/extensions/__init__.py +0 -3
  122. novelwriter/gui/__init__.py +0 -3
  123. novelwriter/tools/__init__.py +0 -3
  124. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/LICENSE.md +0 -0
  125. {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
@@ -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, Qt, QSize, pyqtSignal
32
- from PyQt5.QtWidgets import QMenu, QToolButton, QVBoxLayout, QWidget
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
- viewChangeRequested = pyqtSignal(nwView)
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 = CONFIG.pxInt(24)
56
- iconSize = QSize(iPx, iPx)
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 = QToolButton(self)
64
+ self.tbProject = NIconToolButton(self, iSz)
62
65
  self.tbProject.setToolTip("{0} [Ctrl+T]".format(self.tr("Project Tree View")))
63
- self.tbProject.setIconSize(iconSize)
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 = QToolButton(self)
68
+ self.tbNovel = NIconToolButton(self, iSz)
67
69
  self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View")))
68
- self.tbNovel.setIconSize(iconSize)
69
- self.tbNovel.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL))
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 = QToolButton(self)
76
+ self.tbOutline = NIconToolButton(self, iSz)
72
77
  self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
73
- self.tbOutline.setIconSize(iconSize)
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 = QToolButton(self)
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 = QToolButton(self)
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 = QToolButton(self)
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 = QToolButton(self)
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
- fadeCol = qPalette.text().color()
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.tbSettings.setIcon(SHARED.theme.getIcon("settings"))
158
- self.tbSettings.setStyleSheet(buttonStyleMenu)
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))
@@ -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 qApp, QStatusBar, QLabel
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.baseIconSize
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.baseIconSize
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
- qApp.processEvents()
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 = qApp.allWidgets()
243
+ widgets = QApplication.allWidgets()
244
244
  if not self._debugInfo:
245
245
  if tracemalloc.is_tracing():
246
246
  self._traceMallocRef = "Total"