novelWriter 2.4b1__py3-none-any.whl → 2.4rc1__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 (72) hide show
  1. {novelWriter-2.4b1.dist-info → novelWriter-2.4rc1.dist-info}/METADATA +5 -6
  2. {novelWriter-2.4b1.dist-info → novelWriter-2.4rc1.dist-info}/RECORD +62 -66
  3. novelwriter/__init__.py +5 -5
  4. novelwriter/assets/icons/none.svg +4 -0
  5. novelwriter/assets/icons/typicons_dark/icons.conf +2 -2
  6. novelwriter/assets/icons/typicons_dark/typ_unfold-hidden.svg +4 -0
  7. novelwriter/assets/icons/typicons_dark/typ_unfold-visible.svg +4 -0
  8. novelwriter/assets/icons/typicons_light/icons.conf +2 -2
  9. novelwriter/assets/icons/typicons_light/typ_unfold-hidden.svg +4 -0
  10. novelwriter/assets/icons/typicons_light/typ_unfold-visible.svg +4 -0
  11. novelwriter/assets/manual.pdf +0 -0
  12. novelwriter/assets/sample.zip +0 -0
  13. novelwriter/common.py +6 -1
  14. novelwriter/config.py +8 -4
  15. novelwriter/core/coretools.py +21 -22
  16. novelwriter/core/status.py +3 -2
  17. novelwriter/core/toodt.py +332 -355
  18. novelwriter/dialogs/about.py +9 -11
  19. novelwriter/dialogs/docmerge.py +17 -14
  20. novelwriter/dialogs/docsplit.py +14 -12
  21. novelwriter/dialogs/editlabel.py +5 -4
  22. novelwriter/dialogs/preferences.py +28 -33
  23. novelwriter/dialogs/projectsettings.py +29 -26
  24. novelwriter/dialogs/quotes.py +10 -9
  25. novelwriter/dialogs/wordlist.py +15 -12
  26. novelwriter/error.py +13 -11
  27. novelwriter/extensions/circularprogress.py +12 -8
  28. novelwriter/extensions/configlayout.py +1 -3
  29. novelwriter/extensions/modified.py +33 -2
  30. novelwriter/extensions/pagedsidebar.py +16 -14
  31. novelwriter/extensions/simpleprogress.py +3 -1
  32. novelwriter/extensions/statusled.py +3 -1
  33. novelwriter/extensions/switch.py +10 -9
  34. novelwriter/extensions/switchbox.py +14 -13
  35. novelwriter/gui/doceditor.py +182 -225
  36. novelwriter/gui/dochighlight.py +4 -4
  37. novelwriter/gui/docviewer.py +53 -57
  38. novelwriter/gui/docviewerpanel.py +16 -13
  39. novelwriter/gui/editordocument.py +4 -4
  40. novelwriter/gui/itemdetails.py +45 -48
  41. novelwriter/gui/noveltree.py +22 -20
  42. novelwriter/gui/outline.py +87 -88
  43. novelwriter/gui/projtree.py +31 -29
  44. novelwriter/gui/search.py +75 -29
  45. novelwriter/gui/sidebar.py +24 -28
  46. novelwriter/gui/statusbar.py +14 -14
  47. novelwriter/gui/theme.py +47 -35
  48. novelwriter/guimain.py +35 -31
  49. novelwriter/shared.py +5 -5
  50. novelwriter/tools/dictionaries.py +13 -12
  51. novelwriter/tools/lipsum.py +20 -17
  52. novelwriter/tools/manusbuild.py +35 -27
  53. novelwriter/tools/manuscript.py +68 -73
  54. novelwriter/tools/manussettings.py +68 -73
  55. novelwriter/tools/noveldetails.py +20 -18
  56. novelwriter/tools/welcome.py +47 -43
  57. novelwriter/tools/writingstats.py +61 -55
  58. novelwriter/types.py +90 -0
  59. novelwriter/assets/icons/typicons_dark/typ_arrow-down.svg +0 -4
  60. novelwriter/assets/icons/typicons_dark/typ_arrow-right.svg +0 -4
  61. novelwriter/assets/icons/typicons_light/typ_arrow-down.svg +0 -4
  62. novelwriter/assets/icons/typicons_light/typ_arrow-right.svg +0 -4
  63. novelwriter/core/__init__.py +0 -3
  64. novelwriter/dialogs/__init__.py +0 -3
  65. novelwriter/extensions/__init__.py +0 -3
  66. novelwriter/gui/__init__.py +0 -3
  67. novelwriter/text/__init__.py +0 -3
  68. novelwriter/tools/__init__.py +0 -3
  69. {novelWriter-2.4b1.dist-info → novelWriter-2.4rc1.dist-info}/LICENSE.md +0 -0
  70. {novelWriter-2.4b1.dist-info → novelWriter-2.4rc1.dist-info}/WHEEL +0 -0
  71. {novelWriter-2.4b1.dist-info → novelWriter-2.4rc1.dist-info}/entry_points.txt +0 -0
  72. {novelWriter-2.4b1.dist-info → novelWriter-2.4rc1.dist-info}/top_level.txt +0 -0
novelwriter/gui/search.py CHANGED
@@ -27,22 +27,21 @@ import logging
27
27
 
28
28
  from time import time
29
29
 
30
- from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot
31
- from PyQt5.QtGui import QCursor, QKeyEvent, QPalette
30
+ from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
31
+ from PyQt5.QtGui import QCursor, QKeyEvent
32
32
  from PyQt5.QtWidgets import (
33
- QHBoxLayout, QHeaderView, QLabel, QLineEdit, QToolBar, QTreeWidget,
34
- QTreeWidgetItem, QVBoxLayout, QWidget, qApp
33
+ QApplication, QFrame, QHBoxLayout, QHeaderView, QLabel, QLineEdit,
34
+ QToolBar, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
35
35
  )
36
36
 
37
37
  from novelwriter import CONFIG, SHARED
38
- from novelwriter.common import checkInt
38
+ from novelwriter.common import checkInt, cssCol
39
39
  from novelwriter.core.coretools import DocSearch
40
40
  from novelwriter.core.item import NWItem
41
+ from novelwriter.types import QtAlignMiddle, QtAlignRight, QtUserRole
41
42
 
42
43
  logger = logging.getLogger(__name__)
43
44
 
44
- CACHE_TIMEOUT = 120.0 # 2 minutes
45
-
46
45
 
47
46
  class GuiProjectSearch(QWidget):
48
47
 
@@ -50,8 +49,8 @@ class GuiProjectSearch(QWidget):
50
49
  C_RESULT = 0
51
50
  C_COUNT = 1
52
51
 
53
- D_HANDLE = Qt.ItemDataRole.UserRole
54
- D_RESULT = Qt.ItemDataRole.UserRole + 1
52
+ D_HANDLE = QtUserRole
53
+ D_RESULT = QtUserRole + 1
55
54
 
56
55
  selectedItemChanged = pyqtSignal(str)
57
56
  openDocumentSelectRequest = pyqtSignal(str, int, int, bool)
@@ -61,22 +60,25 @@ class GuiProjectSearch(QWidget):
61
60
 
62
61
  logger.debug("Create: GuiProjectSearch")
63
62
 
64
- iPx = SHARED.theme.baseIconSize
63
+ iPx = SHARED.theme.baseIconHeight
64
+ iSz = SHARED.theme.baseIconSize
65
65
  mPx = CONFIG.pxInt(2)
66
+ tPx = CONFIG.pxInt(4)
66
67
 
67
68
  self._time = time()
68
69
  self._search = DocSearch()
69
70
  self._blocked = False
71
+ self._map: dict[str, tuple[int, float]] = {}
70
72
 
71
73
  # Header
72
- self.viewLabel = QLabel(self.tr("Project Search"))
74
+ self.viewLabel = QLabel(self.tr("Project Search"), self)
73
75
  self.viewLabel.setFont(SHARED.theme.guiFontB)
74
- self.viewLabel.setContentsMargins(mPx, mPx, 0, mPx)
76
+ self.viewLabel.setContentsMargins(mPx, tPx, 0, mPx)
75
77
 
76
78
  # Options
77
79
  self.searchOpt = QToolBar(self)
78
80
  self.searchOpt.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
79
- self.searchOpt.setIconSize(QSize(iPx, iPx))
81
+ self.searchOpt.setIconSize(iSz)
80
82
  self.searchOpt.setContentsMargins(0, 0, 0, 0)
81
83
 
82
84
  self.toggleCase = self.searchOpt.addAction(self.tr("Case Sensitive"))
@@ -96,7 +98,7 @@ class GuiProjectSearch(QWidget):
96
98
 
97
99
  # Search Box
98
100
  self.searchText = QLineEdit(self)
99
- self.searchText.setPlaceholderText(self.tr("Search text ..."))
101
+ self.searchText.setPlaceholderText(self.tr("Search"))
100
102
  self.searchText.setClearButtonEnabled(True)
101
103
 
102
104
  self.searchAction = self.searchText.addAction(
@@ -108,8 +110,11 @@ class GuiProjectSearch(QWidget):
108
110
  self.searchResult = QTreeWidget(self)
109
111
  self.searchResult.setHeaderHidden(True)
110
112
  self.searchResult.setColumnCount(2)
111
- self.searchResult.setIconSize(QSize(iPx, iPx))
113
+ self.searchResult.setIconSize(iSz)
112
114
  self.searchResult.setIndentation(iPx)
115
+ self.searchResult.setFrameStyle(QFrame.Shape.NoFrame)
116
+ self.searchResult.setUniformRowHeights(True)
117
+ self.searchResult.setAllColumnsShowFocus(True)
113
118
  self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked)
114
119
  self.searchResult.itemSelectionChanged.connect(self._searchResultSelected)
115
120
 
@@ -121,11 +126,16 @@ class GuiProjectSearch(QWidget):
121
126
  # Assemble
122
127
  self.headerBox = QHBoxLayout()
123
128
  self.headerBox.addWidget(self.viewLabel, 1)
124
- self.headerBox.addWidget(self.searchOpt, 0)
129
+ self.headerBox.addWidget(self.searchOpt, 0, QtAlignMiddle)
125
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)
126
136
 
127
137
  self.outerBox = QVBoxLayout()
128
- self.outerBox.addLayout(self.headerBox, 0)
138
+ self.outerBox.addWidget(self.headerWidget, 0)
129
139
  self.outerBox.addWidget(self.searchText, 0)
130
140
  self.outerBox.addWidget(self.searchResult, 1)
131
141
  self.outerBox.setContentsMargins(0, 0, 0, 0)
@@ -144,9 +154,21 @@ class GuiProjectSearch(QWidget):
144
154
 
145
155
  def updateTheme(self) -> None:
146
156
  """Update theme elements."""
157
+ bPx = CONFIG.pxInt(1)
158
+ mPx = CONFIG.pxInt(2)
159
+
147
160
  qPalette = self.palette()
148
- qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
149
- self.setPalette(qPalette)
161
+ colBase = cssCol(qPalette.base().color())
162
+ colFocus = cssCol(qPalette.highlight().color())
163
+
164
+ self.headerWidget.setStyleSheet(f"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
+ )
150
172
 
151
173
  self.searchAction.setIcon(SHARED.theme.getIcon("search"))
152
174
  self.toggleCase.setIcon(SHARED.theme.getIcon("search_case"))
@@ -170,14 +192,18 @@ class GuiProjectSearch(QWidget):
170
192
  )
171
193
  return
172
194
 
173
- def beginSearch(self) -> None:
195
+ def beginSearch(self, text: str = "") -> None:
174
196
  """Focus the search box and select its text, if any."""
175
197
  self.searchText.setFocus()
176
198
  self.searchText.selectAll()
199
+ if text:
200
+ self.searchText.setText(text.partition("\n")[0])
201
+ self.searchText.selectAll()
177
202
  return
178
203
 
179
204
  def closeProjectTasks(self) -> None:
180
205
  """Run close project tasks."""
206
+ self._map = {}
181
207
  self.searchText.clear()
182
208
  self.searchResult.clear()
183
209
  return
@@ -209,6 +235,20 @@ class GuiProjectSearch(QWidget):
209
235
  super().keyPressEvent(event)
210
236
  return
211
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
+
212
252
  ##
213
253
  # Private Slots
214
254
  ##
@@ -217,19 +257,21 @@ class GuiProjectSearch(QWidget):
217
257
  def _processSearch(self) -> None:
218
258
  """Perform a search."""
219
259
  if not self._blocked:
220
- qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
260
+ QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
221
261
  start = time()
262
+ SHARED.mainGui.saveDocument()
222
263
  self._blocked = True
264
+ self._map = {}
223
265
  self.searchResult.clear()
224
266
  if text := self.searchText.text():
225
267
  self._search.setUserRegEx(self.toggleRegEx.isChecked())
226
268
  self._search.setCaseSensitive(self.toggleCase.isChecked())
227
269
  self._search.setWholeWords(self.toggleWord.isChecked())
228
270
  for item, results, capped in self._search.iterSearch(SHARED.project, text):
229
- self._appendResultSet(item, results, capped)
271
+ self._displayResultSet(item, results, capped)
230
272
  logger.debug("Search took %.3f ms", 1000*(time() - start))
231
273
  self._time = time()
232
- qApp.restoreOverrideCursor()
274
+ QApplication.restoreOverrideCursor()
233
275
  self._blocked = False
234
276
  return
235
277
 
@@ -274,11 +316,11 @@ class GuiProjectSearch(QWidget):
274
316
  # Internal Functions
275
317
  ##
276
318
 
277
- def _appendResultSet(
278
- self, nwItem: NWItem, results: list[tuple[int, int, str]], capped: bool
319
+ def _displayResultSet(
320
+ self, nwItem: NWItem | None, results: list[tuple[int, int, str]], capped: bool
279
321
  ) -> None:
280
322
  """Populate the result tree."""
281
- if results:
323
+ if results and nwItem:
282
324
  tHandle = nwItem.itemHandle
283
325
  docIcon = SHARED.theme.getItemIcon(
284
326
  nwItem.itemType, nwItem.itemClass,
@@ -291,9 +333,13 @@ class GuiProjectSearch(QWidget):
291
333
  tItem.setIcon(self.C_NAME, docIcon)
292
334
  tItem.setData(self.C_NAME, self.D_HANDLE, tHandle)
293
335
  tItem.setText(self.C_COUNT, f"({len(results):n}{ext})")
294
- tItem.setTextAlignment(self.C_COUNT, Qt.AlignmentFlag.AlignRight)
336
+ tItem.setTextAlignment(self.C_COUNT, QtAlignRight)
295
337
  tItem.setForeground(self.C_COUNT, self.palette().highlight())
296
- self.searchResult.addTopLevelItem(tItem)
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())
297
343
 
298
344
  rItems = []
299
345
  for start, length, context in results:
@@ -309,7 +355,7 @@ class GuiProjectSearch(QWidget):
309
355
  for i in range(tItem.childCount()):
310
356
  self.searchResult.setFirstColumnSpanned(i, parent, True)
311
357
 
312
- qApp.processEvents()
358
+ QApplication.processEvents()
313
359
 
314
360
  return
315
361
 
@@ -28,7 +28,7 @@ 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, pyqtSignal
31
+ from PyQt5.QtCore import QEvent, QPoint, QSize, pyqtSignal
32
32
  from PyQt5.QtWidgets import QMenu, QVBoxLayout, QWidget
33
33
 
34
34
  from novelwriter import CONFIG, SHARED
@@ -54,41 +54,43 @@ class GuiSideBar(QWidget):
54
54
 
55
55
  self.mainGui = mainGui
56
56
 
57
- iPx = CONFIG.pxInt(24)
57
+ iPx = int(1.2*SHARED.theme.baseButtonHeight)
58
+ iSz = QSize(iPx, iPx)
59
+
58
60
  self.setContentsMargins(0, 0, 0, 0)
59
61
  self.installEventFilter(StatusTipFilter(mainGui))
60
62
 
61
63
  # Buttons
62
- self.tbProject = NIconToolButton(self, iPx)
64
+ self.tbProject = NIconToolButton(self, iSz)
63
65
  self.tbProject.setToolTip("{0} [Ctrl+T]".format(self.tr("Project Tree View")))
64
66
  self.tbProject.clicked.connect(lambda: self.requestViewChange.emit(nwView.PROJECT))
65
67
 
66
- self.tbNovel = NIconToolButton(self, iPx)
68
+ self.tbNovel = NIconToolButton(self, iSz)
67
69
  self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View")))
68
70
  self.tbNovel.clicked.connect(lambda: self.requestViewChange.emit(nwView.NOVEL))
69
71
 
70
- self.tbSearch = NIconToolButton(self, iPx)
71
- self.tbSearch.setToolTip("{0} [Ctrl+Shift+F]".format(self.tr("Search Project")))
72
+ self.tbSearch = NIconToolButton(self, iSz)
73
+ self.tbSearch.setToolTip("{0} [Ctrl+Shift+F]".format(self.tr("Project Search")))
72
74
  self.tbSearch.clicked.connect(lambda: self.requestViewChange.emit(nwView.SEARCH))
73
75
 
74
- self.tbOutline = NIconToolButton(self, iPx)
76
+ self.tbOutline = NIconToolButton(self, iSz)
75
77
  self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
76
78
  self.tbOutline.clicked.connect(lambda: self.requestViewChange.emit(nwView.OUTLINE))
77
79
 
78
- self.tbBuild = NIconToolButton(self, iPx)
80
+ self.tbBuild = NIconToolButton(self, iSz)
79
81
  self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
80
82
  self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
81
83
 
82
- self.tbDetails = NIconToolButton(self, iPx)
84
+ self.tbDetails = NIconToolButton(self, iSz)
83
85
  self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details")))
84
86
  self.tbDetails.clicked.connect(self.mainGui.showNovelDetailsDialog)
85
87
 
86
- self.tbStats = NIconToolButton(self, iPx)
88
+ self.tbStats = NIconToolButton(self, iSz)
87
89
  self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics")))
88
90
  self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog)
89
91
 
90
92
  # Settings Menu
91
- self.tbSettings = NIconToolButton(self, iPx)
93
+ self.tbSettings = NIconToolButton(self, iSz)
92
94
  self.tbSettings.setToolTip(self.tr("Settings"))
93
95
 
94
96
  self.mSettings = _PopRightMenu(self.tbSettings)
@@ -123,35 +125,29 @@ class GuiSideBar(QWidget):
123
125
  def updateTheme(self) -> None:
124
126
  """Initialise GUI elements that depend on specific settings."""
125
127
  qPalette = self.palette()
126
- qPalette.setBrush(QPalette.Window, qPalette.base())
128
+ qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
127
129
  self.setPalette(qPalette)
128
130
 
129
131
  buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON)
130
132
 
131
- self.tbProject.setIcon(SHARED.theme.getIcon("view_editor"))
132
133
  self.tbProject.setStyleSheet(buttonStyle)
133
-
134
- self.tbNovel.setIcon(SHARED.theme.getIcon("view_novel"))
135
134
  self.tbNovel.setStyleSheet(buttonStyle)
136
-
137
- self.tbSearch.setIcon(SHARED.theme.getIcon("view_search"))
138
135
  self.tbSearch.setStyleSheet(buttonStyle)
139
-
140
- self.tbOutline.setIcon(SHARED.theme.getIcon("view_outline"))
141
136
  self.tbOutline.setStyleSheet(buttonStyle)
142
-
143
- self.tbBuild.setIcon(SHARED.theme.getIcon("view_build"))
144
137
  self.tbBuild.setStyleSheet(buttonStyle)
145
-
146
- self.tbDetails.setIcon(SHARED.theme.getIcon("proj_details"))
147
138
  self.tbDetails.setStyleSheet(buttonStyle)
148
-
149
- self.tbStats.setIcon(SHARED.theme.getIcon("proj_stats"))
150
139
  self.tbStats.setStyleSheet(buttonStyle)
151
-
152
- self.tbSettings.setIcon(SHARED.theme.getIcon("settings"))
153
140
  self.tbSettings.setStyleSheet(buttonStyle)
154
141
 
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")
150
+
155
151
  return
156
152
 
157
153
  # END Class GuiSideBar
@@ -161,7 +157,7 @@ class _PopRightMenu(QMenu):
161
157
 
162
158
  def event(self, event: QEvent) -> bool:
163
159
  """Overload the show event and move the menu popup location."""
164
- if event.type() == QEvent.Show:
160
+ if event.type() == QEvent.Type.Show:
165
161
  if isinstance(parent := self.parent(), QWidget):
166
162
  offset = QPoint(parent.width(), parent.height() - self.height())
167
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"
novelwriter/gui/theme.py CHANGED
@@ -29,17 +29,17 @@ import logging
29
29
  from math import ceil
30
30
  from pathlib import Path
31
31
 
32
- from PyQt5.QtCore import Qt
33
- from PyQt5.QtWidgets import qApp
32
+ from PyQt5.QtCore import QSize, Qt
34
33
  from PyQt5.QtGui import (
35
34
  QPalette, QColor, QIcon, QFont, QFontMetrics, QFontDatabase, QPixmap
36
35
  )
36
+ from PyQt5.QtWidgets import QApplication
37
37
 
38
38
  from novelwriter import CONFIG
39
+ from novelwriter.common import NWConfigParser, cssCol, minmax
40
+ from novelwriter.constants import nwLabels
39
41
  from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
40
42
  from novelwriter.error import logException
41
- from novelwriter.common import NWConfigParser, minmax
42
- from novelwriter.constants import nwLabels
43
43
 
44
44
  logger = logging.getLogger(__name__)
45
45
 
@@ -144,33 +144,42 @@ class GuiTheme:
144
144
  self.getHeaderDecorationNarrow = self.iconCache.getHeaderDecorationNarrow
145
145
 
146
146
  # Extract Other Info
147
- self.guiDPI = qApp.primaryScreen().logicalDotsPerInchX()
148
- self.guiScale = qApp.primaryScreen().logicalDotsPerInchX()/96.0
147
+ self.guiDPI = QApplication.primaryScreen().logicalDotsPerInchX()
148
+ self.guiScale = QApplication.primaryScreen().logicalDotsPerInchX()/96.0
149
149
  CONFIG.guiScale = self.guiScale
150
150
  logger.debug("GUI DPI: %.1f", self.guiDPI)
151
151
  logger.debug("GUI Scale: %.2f", self.guiScale)
152
152
 
153
153
  # Fonts
154
- self.guiFont = qApp.font()
155
- self.guiFontB = qApp.font()
154
+ self.guiFont = QApplication.font()
155
+ self.guiFontB = QApplication.font()
156
156
  self.guiFontB.setBold(True)
157
157
 
158
158
  qMetric = QFontMetrics(self.guiFont)
159
+ fHeight = qMetric.height()
160
+ fAscent = qMetric.ascent()
159
161
  self.fontPointSize = self.guiFont.pointSizeF()
160
- self.fontPixelSize = int(round(qMetric.height()))
161
- self.baseIconSize = int(round(qMetric.ascent()))
162
+ self.fontPixelSize = int(round(fHeight))
163
+ self.baseIconHeight = int(round(fAscent))
164
+ self.baseButtonHeight = int(round(1.35*fAscent))
162
165
  self.textNHeight = qMetric.boundingRect("N").height()
163
166
  self.textNWidth = qMetric.boundingRect("N").width()
164
167
 
168
+ self.baseIconSize = QSize(self.baseIconHeight, self.baseIconHeight)
169
+ self.buttonIconSize = QSize(int(0.9*self.baseIconHeight), int(0.9*self.baseIconHeight))
170
+
165
171
  # Monospace Font
166
172
  self.guiFontFixed = QFont()
167
173
  self.guiFontFixed.setPointSizeF(0.95*self.fontPointSize)
168
- self.guiFontFixed.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
174
+ self.guiFontFixed.setFamily(
175
+ QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont).family()
176
+ )
169
177
 
170
178
  logger.debug("GUI Font Family: %s", self.guiFont.family())
171
179
  logger.debug("GUI Font Point Size: %.2f", self.fontPointSize)
172
180
  logger.debug("GUI Font Pixel Size: %d", self.fontPixelSize)
173
- logger.debug("GUI Base Icon Size: %d", self.baseIconSize)
181
+ logger.debug("GUI Base Icon Height: %d", self.baseIconHeight)
182
+ logger.debug("GUI Base Button Height: %d", self.baseButtonHeight)
174
183
  logger.debug("Text 'N' Height: %d", self.textNHeight)
175
184
  logger.debug("Text 'N' Width: %d", self.textNWidth)
176
185
 
@@ -248,7 +257,7 @@ class GuiTheme:
248
257
  self._setPalette(parser, sec, "link", QPalette.ColorRole.Link)
249
258
  self._setPalette(parser, sec, "linkvisited", QPalette.ColorRole.LinkVisited)
250
259
  else:
251
- self._guiPalette = qApp.style().standardPalette()
260
+ self._guiPalette = QApplication.style().standardPalette()
252
261
 
253
262
  # GUI
254
263
  sec = "GUI"
@@ -277,7 +286,7 @@ class GuiTheme:
277
286
  self.iconCache.loadTheme(self.themeIcons or defaultIcons)
278
287
 
279
288
  # Apply Styles
280
- qApp.setPalette(self._guiPalette)
289
+ QApplication.setPalette(self._guiPalette)
281
290
 
282
291
  # Reset stylesheets so that they are regenerated
283
292
  self._buildStyleSheets(self._guiPalette)
@@ -394,14 +403,14 @@ class GuiTheme:
394
403
  font.setFamily("Arial")
395
404
  font.setPointSize(10)
396
405
  else:
397
- font = fontDB.systemFont(QFontDatabase.GeneralFont)
406
+ font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont)
398
407
  CONFIG.guiFont = font.family()
399
408
  CONFIG.guiFontSize = font.pointSize()
400
409
  else:
401
410
  font.setFamily(CONFIG.guiFont)
402
411
  font.setPointSize(CONFIG.guiFontSize)
403
412
 
404
- qApp.setFont(font)
413
+ QApplication.setFont(font)
405
414
 
406
415
  return
407
416
 
@@ -440,24 +449,24 @@ class GuiTheme:
440
449
 
441
450
  # Flat Tab Widget and Tab Bar:
442
451
  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())
452
+ "QTabWidget::pane {border: 0;} "
453
+ f"QTabWidget QTabBar::tab {{border: 0; padding: {bPx}px {dPx}px;}} "
454
+ f"QTabWidget QTabBar::tab:selected {{color: {cssCol(hCol)};}} "
455
+ )
447
456
 
448
457
  # Minimal Tool Button
449
458
  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())
459
+ f"QToolButton {{padding: {aPx}px; margin: 0; border: none; background: transparent;}} "
460
+ f"QToolButton:hover {{border: none; background: {cssCol(tCol, 48)};}} "
461
+ "QToolButton::menu-indicator {image: none;} "
462
+ )
454
463
 
455
464
  # Big Tool Button
456
465
  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())
466
+ f"QToolButton {{padding: {cPx}px; margin: 0; border: none; background: transparent;}} "
467
+ f"QToolButton:hover {{border: none; background: {cssCol(tCol, 48)};}} "
468
+ "QToolButton::menu-indicator {image: none;} "
469
+ )
461
470
 
462
471
  return
463
472
 
@@ -544,6 +553,9 @@ class GuiIcons:
544
553
  self._confName = "icons.conf"
545
554
  self._iconPath = CONFIG.assetPath("icons")
546
555
 
556
+ # None Icon
557
+ self._noIcon = QIcon(str(self._iconPath / "none.svg"))
558
+
547
559
  # Icon Theme Meta
548
560
  self.themeName = ""
549
561
  self.themeDescription = ""
@@ -674,13 +686,13 @@ class GuiIcons:
674
686
  def getToggleIcon(self, name: str, size: tuple[int, int]) -> QIcon:
675
687
  """Return a toggle icon from the icon buffer. or load it."""
676
688
  if name in self.TOGGLE_ICON_KEYS:
677
- pOne = self.getPixmap(self.TOGGLE_ICON_KEYS[name][0], size)
689
+ pOne = self.getPixmap(self.TOGGLE_ICON_KEYS[name][0], size)
678
690
  pTwo = self.getPixmap(self.TOGGLE_ICON_KEYS[name][1], size)
679
691
  icon = QIcon()
680
692
  icon.addPixmap(pOne, QIcon.Mode.Normal, QIcon.State.On)
681
693
  icon.addPixmap(pTwo, QIcon.Mode.Normal, QIcon.State.Off)
682
694
  return icon
683
- return QIcon()
695
+ return self._noIcon
684
696
 
685
697
  def getPixmap(self, name: str, size: tuple[int, int]) -> QPixmap:
686
698
  """Return an icon from the icon buffer as a QPixmap. If it
@@ -712,14 +724,14 @@ class GuiIcons:
712
724
  elif tLayout == nwItemLayout.NOTE:
713
725
  iconName = "proj_note"
714
726
  if iconName is None:
715
- return QIcon()
727
+ return self._noIcon
716
728
 
717
729
  return self.getIcon(iconName)
718
730
 
719
731
  def getHeaderDecoration(self, hLevel: int) -> QPixmap:
720
732
  """Get the decoration for a specific heading level."""
721
733
  if not self._headerDec:
722
- iPx = self.mainTheme.baseIconSize
734
+ iPx = self.mainTheme.baseIconHeight
723
735
  self._headerDec = [
724
736
  self.loadDecoration("deco_doc_h0", h=iPx),
725
737
  self.loadDecoration("deco_doc_h1", h=iPx),
@@ -732,7 +744,7 @@ class GuiIcons:
732
744
  def getHeaderDecorationNarrow(self, hLevel: int) -> QPixmap:
733
745
  """Get the narrow decoration for a specific heading level."""
734
746
  if not self._headerDecNarrow:
735
- iPx = self.mainTheme.baseIconSize
747
+ iPx = self.mainTheme.baseIconHeight
736
748
  self._headerDecNarrow = [
737
749
  self.loadDecoration("deco_doc_h0_n", h=iPx),
738
750
  self.loadDecoration("deco_doc_h1_n", h=iPx),
@@ -753,7 +765,7 @@ class GuiIcons:
753
765
  """
754
766
  if name not in self.ICON_KEYS:
755
767
  logger.error("Requested unknown icon name '%s'", name)
756
- return QIcon()
768
+ return self._noIcon
757
769
 
758
770
  # If we just want the app icons, return right away
759
771
  if name == "novelwriter":
@@ -769,7 +781,7 @@ class GuiIcons:
769
781
  # If we didn't find one, give up and return an empty icon
770
782
  logger.warning("Did not load an icon for '%s'", name)
771
783
 
772
- return QIcon()
784
+ return self._noIcon
773
785
 
774
786
  # END Class GuiIcons
775
787