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.
- {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/METADATA +1 -1
- {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/RECORD +81 -70
- novelwriter/__init__.py +5 -5
- 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_arrow-down.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_arrow-right.svg +4 -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_light/icons.conf +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
- novelwriter/assets/icons/typicons_light/typ_arrow-down.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_arrow-right.svg +4 -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/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -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/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/common.py +1 -1
- novelwriter/config.py +35 -12
- novelwriter/constants.py +5 -6
- novelwriter/core/buildsettings.py +60 -40
- novelwriter/core/coretools.py +98 -13
- novelwriter/core/docbuild.py +74 -7
- novelwriter/core/document.py +24 -3
- novelwriter/core/index.py +31 -112
- novelwriter/core/project.py +10 -15
- novelwriter/core/sessions.py +2 -2
- novelwriter/core/status.py +4 -4
- novelwriter/core/storage.py +8 -2
- novelwriter/core/tohtml.py +22 -25
- novelwriter/core/tokenizer.py +416 -232
- novelwriter/core/tomd.py +17 -8
- novelwriter/core/toodt.py +65 -7
- novelwriter/core/tree.py +8 -8
- novelwriter/dialogs/docsplit.py +7 -8
- novelwriter/dialogs/preferences.py +3 -6
- novelwriter/enum.py +17 -14
- novelwriter/extensions/modified.py +20 -2
- novelwriter/extensions/versioninfo.py +1 -1
- novelwriter/gui/doceditor.py +257 -279
- novelwriter/gui/dochighlight.py +29 -25
- novelwriter/gui/docviewer.py +139 -148
- novelwriter/gui/docviewerpanel.py +4 -24
- novelwriter/gui/editordocument.py +12 -1
- novelwriter/gui/itemdetails.py +6 -6
- novelwriter/gui/mainmenu.py +37 -16
- novelwriter/gui/noveltree.py +11 -19
- novelwriter/gui/outline.py +43 -20
- novelwriter/gui/projtree.py +35 -43
- novelwriter/gui/search.py +316 -0
- novelwriter/gui/sidebar.py +25 -30
- novelwriter/gui/theme.py +59 -6
- novelwriter/guimain.py +176 -173
- novelwriter/shared.py +26 -1
- novelwriter/text/__init__.py +3 -0
- novelwriter/text/counting.py +137 -0
- novelwriter/tools/manuscript.py +344 -55
- novelwriter/tools/manussettings.py +213 -71
- novelwriter/tools/welcome.py +1 -1
- {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/WHEEL +0 -0
- {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/entry_points.txt +0 -0
- {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
|
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, 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)
|
@@ -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 =
|
62
|
+
self.tbProject = NIconToolButton(self, iPx)
|
62
63
|
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))
|
64
|
+
self.tbProject.clicked.connect(lambda: self.requestViewChange.emit(nwView.PROJECT))
|
65
65
|
|
66
|
-
self.tbNovel =
|
66
|
+
self.tbNovel = NIconToolButton(self, iPx)
|
67
67
|
self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View")))
|
68
|
-
self.tbNovel.
|
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.
|
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.
|
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 =
|
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 =
|
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 =
|
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 =
|
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
|
-
|
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(
|
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", "
|
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
|
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
|
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
|
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 = [
|