novelWriter 2.3.1__py3-none-any.whl → 2.4b1__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 (81) hide show
  1. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/RECORD +81 -70
  3. novelwriter/__init__.py +5 -5
  4. novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
  5. novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
  6. novelwriter/assets/icons/typicons_dark/typ_arrow-down.svg +4 -0
  7. novelwriter/assets/icons/typicons_dark/typ_arrow-right.svg +4 -0
  8. novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
  9. novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
  10. novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
  11. novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
  12. novelwriter/assets/icons/typicons_light/icons.conf +4 -0
  13. novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
  14. novelwriter/assets/icons/typicons_light/typ_arrow-down.svg +4 -0
  15. novelwriter/assets/icons/typicons_light/typ_arrow-right.svg +4 -0
  16. novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
  17. novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
  18. novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
  19. novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
  20. novelwriter/assets/manual.pdf +0 -0
  21. novelwriter/assets/sample.zip +0 -0
  22. novelwriter/assets/syntax/default_dark.conf +1 -0
  23. novelwriter/assets/syntax/default_light.conf +1 -0
  24. novelwriter/assets/syntax/grey_dark.conf +1 -0
  25. novelwriter/assets/syntax/grey_light.conf +1 -0
  26. novelwriter/assets/syntax/light_owl.conf +1 -0
  27. novelwriter/assets/syntax/night_owl.conf +1 -0
  28. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  29. novelwriter/assets/syntax/solarized_light.conf +1 -0
  30. novelwriter/assets/syntax/tomorrow.conf +1 -0
  31. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  32. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  33. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  34. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  35. novelwriter/assets/text/credits_en.htm +25 -23
  36. novelwriter/common.py +1 -1
  37. novelwriter/config.py +35 -12
  38. novelwriter/constants.py +5 -6
  39. novelwriter/core/buildsettings.py +60 -40
  40. novelwriter/core/coretools.py +98 -13
  41. novelwriter/core/docbuild.py +74 -7
  42. novelwriter/core/document.py +24 -3
  43. novelwriter/core/index.py +31 -112
  44. novelwriter/core/project.py +10 -15
  45. novelwriter/core/sessions.py +2 -2
  46. novelwriter/core/status.py +4 -4
  47. novelwriter/core/storage.py +8 -2
  48. novelwriter/core/tohtml.py +22 -25
  49. novelwriter/core/tokenizer.py +416 -232
  50. novelwriter/core/tomd.py +17 -8
  51. novelwriter/core/toodt.py +65 -7
  52. novelwriter/core/tree.py +8 -8
  53. novelwriter/dialogs/docsplit.py +7 -8
  54. novelwriter/dialogs/preferences.py +3 -6
  55. novelwriter/enum.py +17 -14
  56. novelwriter/extensions/modified.py +20 -2
  57. novelwriter/extensions/versioninfo.py +1 -1
  58. novelwriter/gui/doceditor.py +257 -279
  59. novelwriter/gui/dochighlight.py +29 -25
  60. novelwriter/gui/docviewer.py +139 -148
  61. novelwriter/gui/docviewerpanel.py +4 -24
  62. novelwriter/gui/editordocument.py +12 -1
  63. novelwriter/gui/itemdetails.py +6 -6
  64. novelwriter/gui/mainmenu.py +37 -16
  65. novelwriter/gui/noveltree.py +11 -19
  66. novelwriter/gui/outline.py +43 -20
  67. novelwriter/gui/projtree.py +35 -43
  68. novelwriter/gui/search.py +316 -0
  69. novelwriter/gui/sidebar.py +25 -30
  70. novelwriter/gui/theme.py +59 -6
  71. novelwriter/guimain.py +176 -173
  72. novelwriter/shared.py +26 -1
  73. novelwriter/text/__init__.py +3 -0
  74. novelwriter/text/counting.py +137 -0
  75. novelwriter/tools/manuscript.py +344 -55
  76. novelwriter/tools/manussettings.py +213 -71
  77. novelwriter/tools/welcome.py +1 -1
  78. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/LICENSE.md +0 -0
  79. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/WHEEL +0 -0
  80. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/entry_points.txt +0 -0
  81. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,316 @@
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 QSize, Qt, pyqtSignal, pyqtSlot
31
+ from PyQt5.QtGui import QCursor, QKeyEvent, QPalette
32
+ from PyQt5.QtWidgets import (
33
+ QHBoxLayout, QHeaderView, QLabel, QLineEdit, QToolBar, QTreeWidget,
34
+ QTreeWidgetItem, QVBoxLayout, QWidget, qApp
35
+ )
36
+
37
+ from novelwriter import CONFIG, SHARED
38
+ from novelwriter.common import checkInt
39
+ from novelwriter.core.coretools import DocSearch
40
+ from novelwriter.core.item import NWItem
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+ CACHE_TIMEOUT = 120.0 # 2 minutes
45
+
46
+
47
+ class GuiProjectSearch(QWidget):
48
+
49
+ C_NAME = 0
50
+ C_RESULT = 0
51
+ C_COUNT = 1
52
+
53
+ D_HANDLE = Qt.ItemDataRole.UserRole
54
+ D_RESULT = Qt.ItemDataRole.UserRole + 1
55
+
56
+ selectedItemChanged = pyqtSignal(str)
57
+ openDocumentSelectRequest = pyqtSignal(str, int, int, bool)
58
+
59
+ def __init__(self, parent: QWidget) -> None:
60
+ super().__init__(parent=parent)
61
+
62
+ logger.debug("Create: GuiProjectSearch")
63
+
64
+ iPx = SHARED.theme.baseIconSize
65
+ mPx = CONFIG.pxInt(2)
66
+
67
+ self._time = time()
68
+ self._search = DocSearch()
69
+ self._blocked = False
70
+
71
+ # Header
72
+ self.viewLabel = QLabel(self.tr("Project Search"))
73
+ self.viewLabel.setFont(SHARED.theme.guiFontB)
74
+ self.viewLabel.setContentsMargins(mPx, mPx, 0, mPx)
75
+
76
+ # Options
77
+ self.searchOpt = QToolBar(self)
78
+ self.searchOpt.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
79
+ self.searchOpt.setIconSize(QSize(iPx, iPx))
80
+ self.searchOpt.setContentsMargins(0, 0, 0, 0)
81
+
82
+ self.toggleCase = self.searchOpt.addAction(self.tr("Case Sensitive"))
83
+ self.toggleCase.setCheckable(True)
84
+ self.toggleCase.setChecked(CONFIG.searchProjCase)
85
+ self.toggleCase.toggled.connect(self._toggleCase)
86
+
87
+ self.toggleWord = self.searchOpt.addAction(self.tr("Whole Words Only"))
88
+ self.toggleWord.setCheckable(True)
89
+ self.toggleWord.setChecked(CONFIG.searchProjWord)
90
+ self.toggleWord.toggled.connect(self._toggleWord)
91
+
92
+ self.toggleRegEx = self.searchOpt.addAction(self.tr("RegEx Mode"))
93
+ self.toggleRegEx.setCheckable(True)
94
+ self.toggleRegEx.setChecked(CONFIG.searchProjRegEx)
95
+ self.toggleRegEx.toggled.connect(self._toggleRegEx)
96
+
97
+ # Search Box
98
+ self.searchText = QLineEdit(self)
99
+ self.searchText.setPlaceholderText(self.tr("Search text ..."))
100
+ self.searchText.setClearButtonEnabled(True)
101
+
102
+ self.searchAction = self.searchText.addAction(
103
+ SHARED.theme.getIcon("search"), QLineEdit.ActionPosition.TrailingPosition
104
+ )
105
+ self.searchAction.triggered.connect(self._processSearch)
106
+
107
+ # Search Result
108
+ self.searchResult = QTreeWidget(self)
109
+ self.searchResult.setHeaderHidden(True)
110
+ self.searchResult.setColumnCount(2)
111
+ self.searchResult.setIconSize(QSize(iPx, iPx))
112
+ self.searchResult.setIndentation(iPx)
113
+ self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked)
114
+ self.searchResult.itemSelectionChanged.connect(self._searchResultSelected)
115
+
116
+ treeHeader = self.searchResult.header()
117
+ treeHeader.setStretchLastSection(False)
118
+ treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.Stretch)
119
+ treeHeader.setSectionResizeMode(self.C_COUNT, QHeaderView.ResizeMode.ResizeToContents)
120
+
121
+ # Assemble
122
+ self.headerBox = QHBoxLayout()
123
+ self.headerBox.addWidget(self.viewLabel, 1)
124
+ self.headerBox.addWidget(self.searchOpt, 0)
125
+ self.headerBox.setContentsMargins(0, 0, 0, 0)
126
+
127
+ self.outerBox = QVBoxLayout()
128
+ self.outerBox.addLayout(self.headerBox, 0)
129
+ self.outerBox.addWidget(self.searchText, 0)
130
+ self.outerBox.addWidget(self.searchResult, 1)
131
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
132
+ self.outerBox.setSpacing(mPx)
133
+
134
+ self.setLayout(self.outerBox)
135
+ self.updateTheme()
136
+
137
+ logger.debug("Ready: GuiProjectSearch")
138
+
139
+ return
140
+
141
+ ##
142
+ # Methods
143
+ ##
144
+
145
+ def updateTheme(self) -> None:
146
+ """Update theme elements."""
147
+ qPalette = self.palette()
148
+ qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
149
+ self.setPalette(qPalette)
150
+
151
+ self.searchAction.setIcon(SHARED.theme.getIcon("search"))
152
+ self.toggleCase.setIcon(SHARED.theme.getIcon("search_case"))
153
+ self.toggleWord.setIcon(SHARED.theme.getIcon("search_word"))
154
+ self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex"))
155
+
156
+ return
157
+
158
+ def processReturn(self) -> None:
159
+ """Process a return keypress forwarded from the main GUI."""
160
+ if self.searchText.hasFocus():
161
+ self._processSearch()
162
+ elif (
163
+ self.searchResult.hasFocus()
164
+ and (items := self.searchResult.selectedItems())
165
+ and (data := items[0].data(0, self.D_RESULT))
166
+ and len(data) == 3
167
+ ):
168
+ self.openDocumentSelectRequest.emit(
169
+ str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False
170
+ )
171
+ return
172
+
173
+ def beginSearch(self) -> None:
174
+ """Focus the search box and select its text, if any."""
175
+ self.searchText.setFocus()
176
+ self.searchText.selectAll()
177
+ return
178
+
179
+ def closeProjectTasks(self) -> None:
180
+ """Run close project tasks."""
181
+ self.searchText.clear()
182
+ self.searchResult.clear()
183
+ return
184
+
185
+ ##
186
+ # Events
187
+ ##
188
+
189
+ def keyPressEvent(self, event: QKeyEvent) -> None:
190
+ """Process key press events. This handles up and down arrow key
191
+ presses to jump between search text box and result tree.
192
+ """
193
+ if (
194
+ event.key() == Qt.Key.Key_Down
195
+ and self.searchText.hasFocus()
196
+ and (first := self.searchResult.topLevelItem(0))
197
+ ):
198
+ first.setSelected(True)
199
+ self.searchResult.setFocus()
200
+ elif (
201
+ event.key() == Qt.Key.Key_Up
202
+ and self.searchResult.hasFocus()
203
+ and (first := self.searchResult.topLevelItem(0))
204
+ and first.isSelected()
205
+ ):
206
+ first.setSelected(False)
207
+ self.searchText.setFocus()
208
+ else:
209
+ super().keyPressEvent(event)
210
+ return
211
+
212
+ ##
213
+ # Private Slots
214
+ ##
215
+
216
+ @pyqtSlot()
217
+ def _processSearch(self) -> None:
218
+ """Perform a search."""
219
+ if not self._blocked:
220
+ qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
221
+ start = time()
222
+ self._blocked = True
223
+ self.searchResult.clear()
224
+ if text := self.searchText.text():
225
+ self._search.setUserRegEx(self.toggleRegEx.isChecked())
226
+ self._search.setCaseSensitive(self.toggleCase.isChecked())
227
+ self._search.setWholeWords(self.toggleWord.isChecked())
228
+ for item, results, capped in self._search.iterSearch(SHARED.project, text):
229
+ self._appendResultSet(item, results, capped)
230
+ logger.debug("Search took %.3f ms", 1000*(time() - start))
231
+ self._time = time()
232
+ qApp.restoreOverrideCursor()
233
+ self._blocked = False
234
+ return
235
+
236
+ @pyqtSlot()
237
+ def _searchResultSelected(self) -> None:
238
+ """Process search result selection."""
239
+ if items := self.searchResult.selectedItems():
240
+ if (data := items[0].data(0, self.D_RESULT)) and len(data) == 3:
241
+ self.selectedItemChanged.emit(str(data[0]))
242
+ elif data := items[0].data(0, self.D_HANDLE):
243
+ self.selectedItemChanged.emit(str(data))
244
+ return
245
+
246
+ @pyqtSlot("QTreeWidgetItem*", int)
247
+ def _searchResultDoubleClicked(self, item: QTreeWidgetItem, column: int) -> None:
248
+ """Process search result double click."""
249
+ if (data := item.data(0, self.D_RESULT)) and len(data) == 3:
250
+ self.openDocumentSelectRequest.emit(
251
+ str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), True
252
+ )
253
+ return
254
+
255
+ @pyqtSlot(bool)
256
+ def _toggleCase(self, state: bool) -> None:
257
+ """Enable/disable case sensitive mode."""
258
+ CONFIG.searchProjCase = state
259
+ return
260
+
261
+ @pyqtSlot(bool)
262
+ def _toggleWord(self, state: bool) -> None:
263
+ """Enable/disable whole word search mode."""
264
+ CONFIG.searchProjWord = state
265
+ return
266
+
267
+ @pyqtSlot(bool)
268
+ def _toggleRegEx(self, state: bool) -> None:
269
+ """Enable/disable regular expression search mode."""
270
+ CONFIG.searchProjRegEx = state
271
+ return
272
+
273
+ ##
274
+ # Internal Functions
275
+ ##
276
+
277
+ def _appendResultSet(
278
+ self, nwItem: NWItem, results: list[tuple[int, int, str]], capped: bool
279
+ ) -> None:
280
+ """Populate the result tree."""
281
+ if results:
282
+ tHandle = nwItem.itemHandle
283
+ docIcon = SHARED.theme.getItemIcon(
284
+ nwItem.itemType, nwItem.itemClass,
285
+ nwItem.itemLayout, nwItem.mainHeading
286
+ )
287
+ ext = "+" if capped else ""
288
+
289
+ tItem = QTreeWidgetItem()
290
+ tItem.setText(self.C_NAME, nwItem.itemName)
291
+ tItem.setIcon(self.C_NAME, docIcon)
292
+ tItem.setData(self.C_NAME, self.D_HANDLE, tHandle)
293
+ tItem.setText(self.C_COUNT, f"({len(results):n}{ext})")
294
+ tItem.setTextAlignment(self.C_COUNT, Qt.AlignmentFlag.AlignRight)
295
+ tItem.setForeground(self.C_COUNT, self.palette().highlight())
296
+ self.searchResult.addTopLevelItem(tItem)
297
+
298
+ rItems = []
299
+ for start, length, context in results:
300
+ rItem = QTreeWidgetItem()
301
+ rItem.setText(0, context)
302
+ rItem.setData(0, self.D_RESULT, (tHandle, start, length))
303
+ rItems.append(rItem)
304
+
305
+ tItem.addChildren(rItems)
306
+ tItem.setExpanded(True)
307
+
308
+ parent = self.searchResult.indexFromItem(tItem)
309
+ for i in range(tItem.childCount()):
310
+ self.searchResult.setFirstColumnSpanned(i, parent, True)
311
+
312
+ qApp.processEvents()
313
+
314
+ return
315
+
316
+ # 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, 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)
@@ -53,46 +55,41 @@ class GuiSideBar(QWidget):
53
55
  self.mainGui = mainGui
54
56
 
55
57
  iPx = CONFIG.pxInt(24)
56
- iconSize = QSize(iPx, iPx)
57
58
  self.setContentsMargins(0, 0, 0, 0)
58
59
  self.installEventFilter(StatusTipFilter(mainGui))
59
60
 
60
61
  # Buttons
61
- self.tbProject = QToolButton(self)
62
+ self.tbProject = NIconToolButton(self, iPx)
62
63
  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))
64
+ self.tbProject.clicked.connect(lambda: self.requestViewChange.emit(nwView.PROJECT))
65
65
 
66
- self.tbNovel = QToolButton(self)
66
+ self.tbNovel = NIconToolButton(self, iPx)
67
67
  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))
68
+ self.tbNovel.clicked.connect(lambda: self.requestViewChange.emit(nwView.NOVEL))
70
69
 
71
- self.tbOutline = QToolButton(self)
70
+ self.tbSearch = NIconToolButton(self, iPx)
71
+ self.tbSearch.setToolTip("{0} [Ctrl+Shift+F]".format(self.tr("Search Project")))
72
+ self.tbSearch.clicked.connect(lambda: self.requestViewChange.emit(nwView.SEARCH))
73
+
74
+ self.tbOutline = NIconToolButton(self, iPx)
72
75
  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))
76
+ self.tbOutline.clicked.connect(lambda: self.requestViewChange.emit(nwView.OUTLINE))
75
77
 
76
- self.tbBuild = QToolButton(self)
78
+ self.tbBuild = NIconToolButton(self, iPx)
77
79
  self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
78
- self.tbBuild.setIconSize(iconSize)
79
80
  self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
80
81
 
81
- self.tbDetails = QToolButton(self)
82
+ self.tbDetails = NIconToolButton(self, iPx)
82
83
  self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details")))
83
- self.tbDetails.setIconSize(iconSize)
84
84
  self.tbDetails.clicked.connect(self.mainGui.showNovelDetailsDialog)
85
85
 
86
- self.tbStats = QToolButton(self)
86
+ self.tbStats = NIconToolButton(self, iPx)
87
87
  self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics")))
88
- self.tbStats.setIconSize(iconSize)
89
88
  self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog)
90
89
 
91
90
  # Settings Menu
92
- self.tbSettings = QToolButton(self)
91
+ self.tbSettings = NIconToolButton(self, iPx)
93
92
  self.tbSettings.setToolTip(self.tr("Settings"))
94
- self.tbSettings.setIconSize(iconSize)
95
- self.tbSettings.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
96
93
 
97
94
  self.mSettings = _PopRightMenu(self.tbSettings)
98
95
  self.mSettings.addAction(self.mainGui.mainMenu.aEditWordList)
@@ -101,12 +98,12 @@ class GuiSideBar(QWidget):
101
98
  self.mSettings.addAction(self.mainGui.mainMenu.aPreferences)
102
99
 
103
100
  self.tbSettings.setMenu(self.mSettings)
104
- self.tbSettings.setPopupMode(QToolButton.InstantPopup)
105
101
 
106
102
  # Assemble
107
103
  self.outerBox = QVBoxLayout()
108
104
  self.outerBox.addWidget(self.tbProject)
109
105
  self.outerBox.addWidget(self.tbNovel)
106
+ self.outerBox.addWidget(self.tbSearch)
110
107
  self.outerBox.addWidget(self.tbOutline)
111
108
  self.outerBox.addWidget(self.tbBuild)
112
109
  self.outerBox.addStretch(1)
@@ -129,12 +126,7 @@ class GuiSideBar(QWidget):
129
126
  qPalette.setBrush(QPalette.Window, qPalette.base())
130
127
  self.setPalette(qPalette)
131
128
 
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;}}"
129
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON)
138
130
 
139
131
  self.tbProject.setIcon(SHARED.theme.getIcon("view_editor"))
140
132
  self.tbProject.setStyleSheet(buttonStyle)
@@ -142,6 +134,9 @@ class GuiSideBar(QWidget):
142
134
  self.tbNovel.setIcon(SHARED.theme.getIcon("view_novel"))
143
135
  self.tbNovel.setStyleSheet(buttonStyle)
144
136
 
137
+ self.tbSearch.setIcon(SHARED.theme.getIcon("view_search"))
138
+ self.tbSearch.setStyleSheet(buttonStyle)
139
+
145
140
  self.tbOutline.setIcon(SHARED.theme.getIcon("view_outline"))
146
141
  self.tbOutline.setStyleSheet(buttonStyle)
147
142
 
@@ -155,7 +150,7 @@ class GuiSideBar(QWidget):
155
150
  self.tbStats.setStyleSheet(buttonStyle)
156
151
 
157
152
  self.tbSettings.setIcon(SHARED.theme.getIcon("settings"))
158
- self.tbSettings.setStyleSheet(buttonStyleMenu)
153
+ self.tbSettings.setStyleSheet(buttonStyle)
159
154
 
160
155
  return
161
156
 
novelwriter/gui/theme.py CHANGED
@@ -43,6 +43,10 @@ from novelwriter.constants import nwLabels
43
43
 
44
44
  logger = logging.getLogger(__name__)
45
45
 
46
+ STYLES_FLAT_TABS = "flatTabWidget"
47
+ STYLES_MIN_TOOLBUTTON = "minimalToolButton"
48
+ STYLES_BIG_TOOLBUTTON = "bigToolButton"
49
+
46
50
 
47
51
  # =============================================================================================== #
48
52
  # Gui Theme Class
@@ -106,6 +110,7 @@ class GuiTheme:
106
110
  self.colError = QColor(0, 0, 0)
107
111
  self.colRepTag = QColor(0, 0, 0)
108
112
  self.colMod = QColor(0, 0, 0)
113
+ self.colMark = QColor(255, 255, 255, 128)
109
114
 
110
115
  # Class Setup
111
116
  # ===========
@@ -119,6 +124,7 @@ class GuiTheme:
119
124
  self._syntaxList: list[tuple[str, str]] = []
120
125
  self._availThemes: dict[str, Path] = {}
121
126
  self._availSyntax: dict[str, Path] = {}
127
+ self._styleSheets: dict[str, str] = {}
122
128
 
123
129
  self._listConf(self._availSyntax, CONFIG.assetPath("syntax"))
124
130
  self._listConf(self._availThemes, CONFIG.assetPath("themes"))
@@ -146,6 +152,8 @@ class GuiTheme:
146
152
 
147
153
  # Fonts
148
154
  self.guiFont = qApp.font()
155
+ self.guiFontB = qApp.font()
156
+ self.guiFontB.setBold(True)
149
157
 
150
158
  qMetric = QFontMetrics(self.guiFont)
151
159
  self.fontPointSize = self.guiFont.pointSizeF()
@@ -271,6 +279,9 @@ class GuiTheme:
271
279
  # Apply Styles
272
280
  qApp.setPalette(self._guiPalette)
273
281
 
282
+ # Reset stylesheets so that they are regenerated
283
+ self._buildStyleSheets(self._guiPalette)
284
+
274
285
  return True
275
286
 
276
287
  def loadSyntax(self) -> bool:
@@ -329,6 +340,7 @@ class GuiTheme:
329
340
  self.colError = self._parseColour(confParser, cnfSec, "errorline")
330
341
  self.colRepTag = self._parseColour(confParser, cnfSec, "replacetag")
331
342
  self.colMod = self._parseColour(confParser, cnfSec, "modifier")
343
+ self.colMark = self._parseColour(confParser, cnfSec, "texthighlight")
332
344
 
333
345
  return True
334
346
 
@@ -364,6 +376,10 @@ class GuiTheme:
364
376
 
365
377
  return self._syntaxList
366
378
 
379
+ def getStyleSheet(self, name: str) -> str:
380
+ """Load a standard style sheet."""
381
+ return self._styleSheets.get(name, "")
382
+
367
383
  ##
368
384
  # Internal Functions
369
385
  ##
@@ -410,6 +426,41 @@ class GuiTheme:
410
426
  self._guiPalette.setColor(value, self._parseColour(parser, section, name))
411
427
  return
412
428
 
429
+ def _buildStyleSheets(self, palette: QPalette) -> None:
430
+ """Build default style sheets."""
431
+ self._styleSheets = {}
432
+
433
+ aPx = CONFIG.pxInt(2)
434
+ bPx = CONFIG.pxInt(4)
435
+ cPx = CONFIG.pxInt(6)
436
+ dPx = CONFIG.pxInt(8)
437
+
438
+ tCol = palette.text().color()
439
+ hCol = palette.highlight().color()
440
+
441
+ # Flat Tab Widget and Tab Bar:
442
+ self._styleSheets[STYLES_FLAT_TABS] = (
443
+ "QTabWidget::pane {{border: 0;}} "
444
+ "QTabWidget QTabBar::tab {{border: 0; padding: {0}px {1}px;}} "
445
+ "QTabWidget QTabBar::tab:selected {{color: rgb({2}, {3}, {4});}} "
446
+ ).format(bPx, dPx, hCol.red(), hCol.green(), hCol.blue())
447
+
448
+ # Minimal Tool Button
449
+ self._styleSheets[STYLES_MIN_TOOLBUTTON] = (
450
+ "QToolButton {{padding: {0}px; margin: 0; border: none; background: transparent;}} "
451
+ "QToolButton:hover {{border: none; background: rgba({1}, {2}, {3}, 0.2);}} "
452
+ "QToolButton::menu-indicator {{image: none;}} "
453
+ ).format(aPx, tCol.red(), tCol.green(), tCol.blue())
454
+
455
+ # Big Tool Button
456
+ self._styleSheets[STYLES_BIG_TOOLBUTTON] = (
457
+ "QToolButton {{padding: {0}px; margin: 0; border: none; background: transparent;}} "
458
+ "QToolButton:hover {{border: none; background: rgba({1}, {2}, {3}, 0.2);}} "
459
+ "QToolButton::menu-indicator {{image: none;}} "
460
+ ).format(cPx, tCol.red(), tCol.green(), tCol.blue())
461
+
462
+ return
463
+
413
464
  # End Class GuiTheme
414
465
 
415
466
 
@@ -437,7 +488,7 @@ class GuiIcons:
437
488
  "build_excluded", "build_filtered", "build_included", "proj_chapter", "proj_details",
438
489
  "proj_document", "proj_folder", "proj_note", "proj_nwx", "proj_section", "proj_scene",
439
490
  "proj_stats", "proj_title", "status_idle", "status_lang", "status_lines", "status_stats",
440
- "status_time", "view_build", "view_editor", "view_novel", "view_outline",
491
+ "status_time", "view_build", "view_editor", "view_novel", "view_outline", "view_search",
441
492
 
442
493
  # Class Icons
443
494
  "cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none", "cls_novel",
@@ -448,8 +499,8 @@ class GuiIcons:
448
499
  "search_regex", "search_word",
449
500
 
450
501
  # Format Icons
451
- "fmt_bold", "fmt_bold-md", "fmt_italic", "fmt_italic-md", "fmt_strike", "fmt_strike-md",
452
- "fmt_subscript", "fmt_superscript", "fmt_underline",
502
+ "fmt_bold", "fmt_bold-md", "fmt_italic", "fmt_italic-md", "fmt_mark", "fmt_strike",
503
+ "fmt_strike-md", "fmt_subscript", "fmt_superscript", "fmt_underline",
453
504
 
454
505
  # General Button Icons
455
506
  "add", "add_document", "backward", "bookmark", "browse", "checked", "close", "cross",
@@ -460,6 +511,7 @@ class GuiIcons:
460
511
  # Switches
461
512
  "sticky-on", "sticky-off",
462
513
  "bullet-on", "bullet-off",
514
+ "unfold-show", "unfold-hide",
463
515
 
464
516
  # Decorations
465
517
  "deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4", "deco_doc_more",
@@ -470,6 +522,7 @@ class GuiIcons:
470
522
  TOGGLE_ICON_KEYS: dict[str, tuple[str, str]] = {
471
523
  "sticky": ("sticky-on", "sticky-off"),
472
524
  "bullet": ("bullet-on", "bullet-off"),
525
+ "unfold": ("unfold-show", "unfold-hide"),
473
526
  }
474
527
 
475
528
  IMAGE_MAP: dict[str, tuple[str, str]] = {
@@ -638,7 +691,7 @@ class GuiIcons:
638
691
  def getItemIcon(self, tType: nwItemType, tClass: nwItemClass,
639
692
  tLayout: nwItemLayout, hLevel: str = "H0") -> QIcon:
640
693
  """Get the correct icon for a project item based on type, class
641
- and header level
694
+ and heading level
642
695
  """
643
696
  iconName = None
644
697
  if tType == nwItemType.ROOT:
@@ -664,7 +717,7 @@ class GuiIcons:
664
717
  return self.getIcon(iconName)
665
718
 
666
719
  def getHeaderDecoration(self, hLevel: int) -> QPixmap:
667
- """Get the decoration for a specific header level."""
720
+ """Get the decoration for a specific heading level."""
668
721
  if not self._headerDec:
669
722
  iPx = self.mainTheme.baseIconSize
670
723
  self._headerDec = [
@@ -677,7 +730,7 @@ class GuiIcons:
677
730
  return self._headerDec[minmax(hLevel, 0, 4)]
678
731
 
679
732
  def getHeaderDecorationNarrow(self, hLevel: int) -> QPixmap:
680
- """Get the narrow decoration for a specific header level."""
733
+ """Get the narrow decoration for a specific heading level."""
681
734
  if not self._headerDecNarrow:
682
735
  iPx = self.mainTheme.baseIconSize
683
736
  self._headerDecNarrow = [