novelWriter 2.4b1__py3-none-any.whl → 2.4.1__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 (89) hide show
  1. {novelWriter-2.4b1.dist-info → novelWriter-2.4.1.dist-info}/METADATA +5 -6
  2. {novelWriter-2.4b1.dist-info → novelWriter-2.4.1.dist-info}/RECORD +79 -83
  3. novelwriter/__init__.py +15 -8
  4. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  5. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  6. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  7. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  8. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  9. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  10. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  11. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  12. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  13. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  14. novelwriter/assets/icons/none.svg +4 -0
  15. novelwriter/assets/icons/typicons_dark/icons.conf +2 -2
  16. novelwriter/assets/icons/typicons_dark/typ_unfold-hidden.svg +4 -0
  17. novelwriter/assets/icons/typicons_dark/typ_unfold-visible.svg +4 -0
  18. novelwriter/assets/icons/typicons_light/icons.conf +2 -2
  19. novelwriter/assets/icons/typicons_light/typ_unfold-hidden.svg +4 -0
  20. novelwriter/assets/icons/typicons_light/typ_unfold-visible.svg +4 -0
  21. novelwriter/assets/manual.pdf +0 -0
  22. novelwriter/assets/sample.zip +0 -0
  23. novelwriter/common.py +11 -3
  24. novelwriter/config.py +12 -4
  25. novelwriter/core/buildsettings.py +7 -7
  26. novelwriter/core/coretools.py +21 -22
  27. novelwriter/core/docbuild.py +2 -2
  28. novelwriter/core/projectxml.py +1 -1
  29. novelwriter/core/spellcheck.py +3 -3
  30. novelwriter/core/status.py +3 -2
  31. novelwriter/core/tokenizer.py +3 -3
  32. novelwriter/core/toodt.py +333 -356
  33. novelwriter/dialogs/about.py +9 -11
  34. novelwriter/dialogs/docmerge.py +17 -14
  35. novelwriter/dialogs/docsplit.py +14 -12
  36. novelwriter/dialogs/editlabel.py +5 -4
  37. novelwriter/dialogs/preferences.py +29 -34
  38. novelwriter/dialogs/projectsettings.py +31 -28
  39. novelwriter/dialogs/quotes.py +10 -9
  40. novelwriter/dialogs/wordlist.py +17 -14
  41. novelwriter/error.py +14 -12
  42. novelwriter/extensions/circularprogress.py +12 -8
  43. novelwriter/extensions/configlayout.py +1 -3
  44. novelwriter/extensions/modified.py +33 -2
  45. novelwriter/extensions/pagedsidebar.py +16 -14
  46. novelwriter/extensions/simpleprogress.py +3 -1
  47. novelwriter/extensions/statusled.py +3 -1
  48. novelwriter/extensions/switch.py +10 -9
  49. novelwriter/extensions/switchbox.py +14 -13
  50. novelwriter/gui/doceditor.py +205 -246
  51. novelwriter/gui/dochighlight.py +26 -9
  52. novelwriter/gui/docviewer.py +55 -59
  53. novelwriter/gui/docviewerpanel.py +16 -13
  54. novelwriter/gui/editordocument.py +4 -4
  55. novelwriter/gui/itemdetails.py +45 -48
  56. novelwriter/gui/mainmenu.py +2 -2
  57. novelwriter/gui/noveltree.py +23 -21
  58. novelwriter/gui/outline.py +93 -94
  59. novelwriter/gui/projtree.py +32 -30
  60. novelwriter/gui/search.py +75 -29
  61. novelwriter/gui/sidebar.py +24 -28
  62. novelwriter/gui/statusbar.py +14 -14
  63. novelwriter/gui/theme.py +61 -39
  64. novelwriter/guimain.py +37 -33
  65. novelwriter/shared.py +21 -9
  66. novelwriter/text/counting.py +1 -0
  67. novelwriter/tools/dictionaries.py +15 -14
  68. novelwriter/tools/lipsum.py +20 -17
  69. novelwriter/tools/manusbuild.py +44 -35
  70. novelwriter/tools/manuscript.py +112 -112
  71. novelwriter/tools/manussettings.py +91 -98
  72. novelwriter/tools/noveldetails.py +20 -18
  73. novelwriter/tools/welcome.py +51 -48
  74. novelwriter/tools/writingstats.py +61 -55
  75. novelwriter/types.py +90 -0
  76. novelwriter/assets/icons/typicons_dark/typ_arrow-down.svg +0 -4
  77. novelwriter/assets/icons/typicons_dark/typ_arrow-right.svg +0 -4
  78. novelwriter/assets/icons/typicons_light/typ_arrow-down.svg +0 -4
  79. novelwriter/assets/icons/typicons_light/typ_arrow-right.svg +0 -4
  80. novelwriter/core/__init__.py +0 -3
  81. novelwriter/dialogs/__init__.py +0 -3
  82. novelwriter/extensions/__init__.py +0 -3
  83. novelwriter/gui/__init__.py +0 -3
  84. novelwriter/text/__init__.py +0 -3
  85. novelwriter/tools/__init__.py +0 -3
  86. {novelWriter-2.4b1.dist-info → novelWriter-2.4.1.dist-info}/LICENSE.md +0 -0
  87. {novelWriter-2.4b1.dist-info → novelWriter-2.4.1.dist-info}/WHEEL +0 -0
  88. {novelWriter-2.4b1.dist-info → novelWriter-2.4.1.dist-info}/entry_points.txt +0 -0
  89. {novelWriter-2.4b1.dist-info → novelWriter-2.4.1.dist-info}/top_level.txt +0 -0
@@ -32,10 +32,10 @@ from enum import Enum
32
32
  from time import time
33
33
  from typing import TYPE_CHECKING
34
34
 
35
+ from PyQt5.QtCore import QPoint, QTimer, Qt, pyqtSignal, pyqtSlot
35
36
  from PyQt5.QtGui import (
36
37
  QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon, QMouseEvent, QPalette
37
38
  )
38
- from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot
39
39
  from PyQt5.QtWidgets import (
40
40
  QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView,
41
41
  QLabel, QMenu, QShortcut, QSizePolicy, QTreeWidget, QTreeWidgetItem,
@@ -54,6 +54,7 @@ from novelwriter.dialogs.projectsettings import GuiProjectSettings
54
54
  from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout
55
55
  from novelwriter.extensions.modified import NIconToolButton
56
56
  from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
57
+ from novelwriter.types import QtAlignLeft, QtAlignRight, QtMouseLeft, QtMouseMiddle, QtUserRole
57
58
 
58
59
  if TYPE_CHECKING: # pragma: no cover
59
60
  from novelwriter.guimain import GuiMain
@@ -263,14 +264,14 @@ class GuiProjectToolBar(QWidget):
263
264
  self.projTree = projView.projTree
264
265
  self.mainGui = projView.mainGui
265
266
 
266
- iPx = SHARED.theme.baseIconSize
267
+ iSz = SHARED.theme.baseIconSize
267
268
  mPx = CONFIG.pxInt(2)
268
269
 
269
270
  self.setContentsMargins(0, 0, 0, 0)
270
271
  self.setAutoFillBackground(True)
271
272
 
272
273
  # Widget Label
273
- self.viewLabel = QLabel(self.tr("Project Content"))
274
+ self.viewLabel = QLabel(self.tr("Project Content"), self)
274
275
  self.viewLabel.setFont(SHARED.theme.guiFontB)
275
276
  self.viewLabel.setContentsMargins(0, 0, 0, 0)
276
277
  self.viewLabel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
@@ -278,17 +279,17 @@ class GuiProjectToolBar(QWidget):
278
279
  # Quick Links
279
280
  self.mQuick = QMenu(self)
280
281
 
281
- self.tbQuick = NIconToolButton(self, iPx)
282
+ self.tbQuick = NIconToolButton(self, iSz)
282
283
  self.tbQuick.setToolTip("%s [Ctrl+L]" % self.tr("Quick Links"))
283
284
  self.tbQuick.setShortcut("Ctrl+L")
284
285
  self.tbQuick.setMenu(self.mQuick)
285
286
 
286
287
  # Move Buttons
287
- self.tbMoveU = NIconToolButton(self, iPx)
288
+ self.tbMoveU = NIconToolButton(self, iSz)
288
289
  self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up"))
289
290
  self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1))
290
291
 
291
- self.tbMoveD = NIconToolButton(self, iPx)
292
+ self.tbMoveD = NIconToolButton(self, iSz)
292
293
  self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down"))
293
294
  self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1))
294
295
 
@@ -328,7 +329,7 @@ class GuiProjectToolBar(QWidget):
328
329
  self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"]))
329
330
  self._buildRootMenu()
330
331
 
331
- self.tbAdd = NIconToolButton(self, iPx)
332
+ self.tbAdd = NIconToolButton(self, iSz)
332
333
  self.tbAdd.setToolTip("%s [Ctrl+N]" % self.tr("Add Item"))
333
334
  self.tbAdd.setShortcut("Ctrl+N")
334
335
  self.tbAdd.setMenu(self.mAdd)
@@ -345,7 +346,7 @@ class GuiProjectToolBar(QWidget):
345
346
  self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
346
347
  self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash())
347
348
 
348
- self.tbMore = NIconToolButton(self, iPx)
349
+ self.tbMore = NIconToolButton(self, iSz)
349
350
  self.tbMore.setToolTip(self.tr("More Options"))
350
351
  self.tbMore.setMenu(self.mMore)
351
352
 
@@ -384,16 +385,17 @@ class GuiProjectToolBar(QWidget):
384
385
  self.tbAdd.setStyleSheet(buttonStyle)
385
386
  self.tbMore.setStyleSheet(buttonStyle)
386
387
 
387
- self.tbQuick.setIcon(SHARED.theme.getIcon("bookmark"))
388
- self.tbMoveU.setIcon(SHARED.theme.getIcon("up"))
389
- self.tbMoveD.setIcon(SHARED.theme.getIcon("down"))
388
+ self.tbQuick.setThemeIcon("bookmark")
389
+ self.tbMoveU.setThemeIcon("up")
390
+ self.tbMoveD.setThemeIcon("down")
391
+ self.tbAdd.setThemeIcon("add")
392
+ self.tbMore.setThemeIcon("menu")
393
+
390
394
  self.aAddEmpty.setIcon(SHARED.theme.getIcon("proj_document"))
391
395
  self.aAddChap.setIcon(SHARED.theme.getIcon("proj_chapter"))
392
396
  self.aAddScene.setIcon(SHARED.theme.getIcon("proj_scene"))
393
397
  self.aAddNote.setIcon(SHARED.theme.getIcon("proj_note"))
394
398
  self.aAddFolder.setIcon(SHARED.theme.getIcon("proj_folder"))
395
- self.tbAdd.setIcon(SHARED.theme.getIcon("add"))
396
- self.tbMore.setIcon(SHARED.theme.getIcon("menu"))
397
399
 
398
400
  self.buildQuickLinksMenu()
399
401
  self._buildRootMenu()
@@ -451,7 +453,7 @@ class GuiProjectToolBar(QWidget):
451
453
 
452
454
  def _buildRootMenu(self) -> None:
453
455
  """Build the rood folder menu."""
454
- def addClass(itemClass):
456
+ def addClass(itemClass: nwItemClass) -> None:
455
457
  aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass]))
456
458
  aNew.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass]))
457
459
  aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass))
@@ -485,8 +487,8 @@ class GuiProjectTree(QTreeWidget):
485
487
  C_ACTIVE = 2
486
488
  C_STATUS = 3
487
489
 
488
- D_HANDLE = Qt.ItemDataRole.UserRole
489
- D_WORDS = Qt.ItemDataRole.UserRole + 1
490
+ D_HANDLE = QtUserRole
491
+ D_WORDS = QtUserRole + 1
490
492
 
491
493
  itemRefreshed = pyqtSignal(str, NWItem, QIcon)
492
494
 
@@ -516,10 +518,10 @@ class GuiProjectTree(QTreeWidget):
516
518
  self.customContextMenuRequested.connect(self._openContextMenu)
517
519
 
518
520
  # Tree Settings
519
- iPx = SHARED.theme.baseIconSize
521
+ iPx = SHARED.theme.baseIconHeight
520
522
  cMg = CONFIG.pxInt(6)
521
523
 
522
- self.setIconSize(QSize(iPx, iPx))
524
+ self.setIconSize(SHARED.theme.baseIconSize)
523
525
  self.setFrameStyle(QFrame.Shape.NoFrame)
524
526
  self.setUniformRowHeights(True)
525
527
  self.setAllColumnsShowFocus(True)
@@ -563,9 +565,9 @@ class GuiProjectTree(QTreeWidget):
563
565
  self.itemSelectionChanged.connect(self._treeSelectionChange)
564
566
 
565
567
  # Auto Scroll
566
- self._scrollMargin = SHARED.theme.baseIconSize
568
+ self._scrollMargin = SHARED.theme.baseIconHeight
567
569
  self._scrollDirection = 0
568
- self._scrollTimer = QTimer()
570
+ self._scrollTimer = QTimer(self)
569
571
  self._scrollTimer.timeout.connect(self._doAutoScroll)
570
572
  self._scrollTimer.setInterval(250)
571
573
 
@@ -1228,7 +1230,7 @@ class GuiProjectTree(QTreeWidget):
1228
1230
  else:
1229
1231
  ctxMenu.buildSingleSelectMenu(hasChild)
1230
1232
 
1231
- ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
1233
+ ctxMenu.exec(self.viewport().mapToGlobal(clickPos))
1232
1234
  ctxMenu.deleteLater()
1233
1235
 
1234
1236
  return True
@@ -1254,11 +1256,11 @@ class GuiProjectTree(QTreeWidget):
1254
1256
  for viewing if the user middle-clicked.
1255
1257
  """
1256
1258
  super().mousePressEvent(event)
1257
- if event.button() == Qt.MouseButton.LeftButton:
1259
+ if event.button() == QtMouseLeft:
1258
1260
  selItem = self.indexAt(event.pos())
1259
1261
  if not selItem.isValid():
1260
1262
  self.clearSelection()
1261
- elif event.button() == Qt.MouseButton.MiddleButton:
1263
+ elif event.button() == QtMouseMiddle:
1262
1264
  selItem = self.itemAt(event.pos())
1263
1265
  if selItem:
1264
1266
  tHandle = selItem.data(self.C_DATA, self.D_HANDLE)
@@ -1266,7 +1268,7 @@ class GuiProjectTree(QTreeWidget):
1266
1268
  self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
1267
1269
  return
1268
1270
 
1269
- def startDrag(self, dropAction: Qt.DropActions) -> None:
1271
+ def startDrag(self, dropAction: Qt.DropAction) -> None:
1270
1272
  """Capture the drag and drop handling to pop alerts."""
1271
1273
  super().startDrag(dropAction)
1272
1274
  if self._popAlert:
@@ -1408,7 +1410,7 @@ class GuiProjectTree(QTreeWidget):
1408
1410
  itemList.remove(tHandle)
1409
1411
 
1410
1412
  dlgMerge = GuiDocMerge(self.mainGui, tHandle, itemList)
1411
- dlgMerge.exec_()
1413
+ dlgMerge.exec()
1412
1414
 
1413
1415
  if dlgMerge.result() == QDialog.DialogCode.Accepted:
1414
1416
 
@@ -1478,7 +1480,7 @@ class GuiProjectTree(QTreeWidget):
1478
1480
  return False
1479
1481
 
1480
1482
  dlgSplit = GuiDocSplit(self.mainGui, tHandle)
1481
- dlgSplit.exec_()
1483
+ dlgSplit.exec()
1482
1484
 
1483
1485
  if dlgSplit.result() == QDialog.DialogCode.Accepted:
1484
1486
 
@@ -1585,10 +1587,10 @@ class GuiProjectTree(QTreeWidget):
1585
1587
  newItem.setText(self.C_ACTIVE, "")
1586
1588
  newItem.setText(self.C_STATUS, "")
1587
1589
 
1588
- newItem.setTextAlignment(self.C_NAME, Qt.AlignmentFlag.AlignLeft)
1589
- newItem.setTextAlignment(self.C_COUNT, Qt.AlignmentFlag.AlignRight)
1590
- newItem.setTextAlignment(self.C_ACTIVE, Qt.AlignmentFlag.AlignLeft)
1591
- newItem.setTextAlignment(self.C_STATUS, Qt.AlignmentFlag.AlignLeft)
1590
+ newItem.setTextAlignment(self.C_NAME, QtAlignLeft)
1591
+ newItem.setTextAlignment(self.C_COUNT, QtAlignRight)
1592
+ newItem.setTextAlignment(self.C_ACTIVE, QtAlignLeft)
1593
+ newItem.setTextAlignment(self.C_STATUS, QtAlignLeft)
1592
1594
 
1593
1595
  newItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
1594
1596
  newItem.setData(self.C_DATA, self.D_WORDS, 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 for"))
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"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
+ )
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"