novelWriter 2.1.1__py3-none-any.whl → 2.2rc1__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 (109) hide show
  1. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/METADATA +3 -3
  2. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/RECORD +105 -76
  3. novelwriter/__init__.py +6 -24
  4. novelwriter/assets/i18n/project_de_DE.json +10 -0
  5. novelwriter/assets/i18n/project_en_GB.json +11 -0
  6. novelwriter/assets/i18n/project_en_US.json +10 -0
  7. novelwriter/assets/i18n/project_ja_JP.json +11 -1
  8. novelwriter/assets/i18n/project_nb_NO.json +10 -0
  9. novelwriter/assets/i18n/project_nn_NO.json +10 -0
  10. novelwriter/assets/icons/novelwriter.ico +0 -0
  11. novelwriter/assets/icons/novelwriter.svg +8 -183
  12. novelwriter/assets/icons/typicons_dark/icons.conf +17 -2
  13. novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
  14. novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
  15. novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
  16. novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
  17. novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
  18. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +4 -0
  20. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +8 -0
  21. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +8 -0
  22. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +4 -0
  23. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +5 -0
  24. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +5 -0
  25. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +5 -0
  26. novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
  27. novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg +4 -0
  28. novelwriter/assets/icons/typicons_light/icons.conf +17 -2
  29. novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
  30. novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
  31. novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
  32. novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
  33. novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
  34. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +4 -0
  35. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +4 -0
  36. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +8 -0
  37. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +8 -0
  38. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +4 -0
  39. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +5 -0
  40. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +5 -0
  41. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +5 -0
  42. novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
  43. novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg +4 -0
  44. novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
  45. novelwriter/assets/icons/x-novelwriter-project.svg +7 -206
  46. novelwriter/assets/manual.pdf +0 -0
  47. novelwriter/assets/sample.zip +0 -0
  48. novelwriter/assets/syntax/default_dark.conf +1 -0
  49. novelwriter/assets/syntax/default_light.conf +1 -0
  50. novelwriter/assets/syntax/grey_dark.conf +1 -0
  51. novelwriter/assets/syntax/grey_light.conf +1 -0
  52. novelwriter/assets/syntax/light_owl.conf +1 -0
  53. novelwriter/assets/syntax/night_owl.conf +1 -0
  54. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  55. novelwriter/assets/syntax/solarized_light.conf +1 -0
  56. novelwriter/assets/syntax/tomorrow.conf +1 -0
  57. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  58. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  59. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  60. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  61. novelwriter/assets/text/credits_en.htm +7 -0
  62. novelwriter/assets/text/release_notes.htm +7 -37
  63. novelwriter/common.py +22 -1
  64. novelwriter/config.py +27 -42
  65. novelwriter/constants.py +45 -7
  66. novelwriter/core/buildsettings.py +40 -24
  67. novelwriter/core/coretools.py +8 -1
  68. novelwriter/core/docbuild.py +2 -6
  69. novelwriter/core/index.py +264 -175
  70. novelwriter/core/options.py +8 -3
  71. novelwriter/core/project.py +2 -2
  72. novelwriter/core/projectdata.py +3 -3
  73. novelwriter/core/tohtml.py +60 -59
  74. novelwriter/core/tokenizer.py +110 -70
  75. novelwriter/core/tomd.py +51 -38
  76. novelwriter/core/toodt.py +184 -147
  77. novelwriter/dialogs/preferences.py +75 -106
  78. novelwriter/dialogs/projsettings.py +101 -110
  79. novelwriter/dialogs/updates.py +25 -14
  80. novelwriter/enum.py +28 -3
  81. novelwriter/extensions/novelselector.py +1 -1
  82. novelwriter/gui/doceditor.py +1345 -1235
  83. novelwriter/gui/dochighlight.py +98 -62
  84. novelwriter/gui/docviewer.py +151 -340
  85. novelwriter/gui/docviewerpanel.py +457 -0
  86. novelwriter/gui/editordocument.py +126 -0
  87. novelwriter/gui/mainmenu.py +350 -300
  88. novelwriter/gui/noveltree.py +101 -125
  89. novelwriter/gui/outline.py +154 -171
  90. novelwriter/gui/projtree.py +480 -380
  91. novelwriter/gui/sidebar.py +106 -75
  92. novelwriter/gui/statusbar.py +1 -1
  93. novelwriter/gui/theme.py +114 -75
  94. novelwriter/guimain.py +353 -254
  95. novelwriter/shared.py +36 -3
  96. novelwriter/tools/dictionaries.py +268 -0
  97. novelwriter/tools/manusbuild.py +17 -6
  98. novelwriter/tools/manuscript.py +11 -3
  99. novelwriter/tools/manussettings.py +0 -14
  100. novelwriter/tools/projwizard.py +16 -2
  101. novelwriter/tools/writingstats.py +1 -1
  102. novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
  103. novelwriter/assets/icons/typicons_dark/typ_th-menu.svg +0 -4
  104. novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
  105. novelwriter/assets/icons/typicons_light/typ_th-menu.svg +0 -4
  106. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/LICENSE.md +0 -0
  107. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/WHEEL +0 -0
  108. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/entry_points.txt +0 -0
  109. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/top_level.txt +0 -0
@@ -25,119 +25,150 @@ from __future__ import annotations
25
25
 
26
26
  import logging
27
27
 
28
- from PyQt5.QtCore import Qt, QSize, pyqtSignal
29
- from PyQt5.QtWidgets import (
30
- QToolBar, QWidget, QSizePolicy, QAction, QMenu, QToolButton
31
- )
28
+ from typing import TYPE_CHECKING
29
+
30
+ from PyQt5.QtCore import QEvent, QPoint, Qt, QSize, pyqtSignal
31
+ from PyQt5.QtGui import QPalette
32
+ from PyQt5.QtWidgets import QMenu, QToolButton, QVBoxLayout, QWidget
32
33
 
33
34
  from novelwriter import CONFIG, SHARED
34
35
  from novelwriter.enum import nwView
35
36
 
37
+ if TYPE_CHECKING: # pragma: no cover
38
+ from novelwriter.guimain import GuiMain
39
+
36
40
  logger = logging.getLogger(__name__)
37
41
 
38
42
 
39
- class GuiSideBar(QToolBar):
43
+ class GuiSideBar(QWidget):
40
44
 
41
45
  viewChangeRequested = pyqtSignal(nwView)
42
46
 
43
- def __init__(self, mainGui):
47
+ def __init__(self, mainGui: GuiMain) -> None:
44
48
  super().__init__(parent=mainGui)
45
49
 
46
50
  logger.debug("Create: GuiSideBar")
47
51
 
48
52
  self.mainGui = mainGui
49
53
 
50
- # Style
51
- iPx = CONFIG.pxInt(22)
52
- mPx = CONFIG.pxInt(60)
53
-
54
- lblFont = SHARED.theme.guiFont
55
- lblFont.setPointSizeF(0.65*SHARED.theme.fontPointSize)
56
-
57
- self.setMovable(False)
58
- self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
59
- self.setIconSize(QSize(iPx, iPx))
60
- self.setMaximumWidth(mPx)
54
+ iPx = CONFIG.pxInt(24)
55
+ iconSize = QSize(iPx, iPx)
61
56
  self.setContentsMargins(0, 0, 0, 0)
62
57
 
63
- stretch = QWidget(self)
64
- stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
65
-
66
- # Actions
67
- self.aProject = QAction(self.tr("Project"), self)
68
- self.aProject.setFont(lblFont)
69
- self.aProject.setToolTip(self.tr("Project Tree View"))
70
- self.aProject.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT))
71
-
72
- self.aNovel = QAction(self.tr("Novel"), self)
73
- self.aNovel.setFont(lblFont)
74
- self.aNovel.setToolTip(self.tr("Novel Tree View"))
75
- self.aNovel.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL))
76
-
77
- self.aOutline = QAction(self.tr("Outline"), self)
78
- self.aOutline.setFont(lblFont)
79
- self.aOutline.setToolTip(self.tr("Novel Outline View"))
80
- self.aOutline.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE))
81
-
82
- self.aBuild = QAction(self.tr("Build"), self)
83
- self.aBuild.setFont(lblFont)
84
- self.aBuild.setToolTip(self.tr("Build Manuscript"))
85
- self.aBuild.triggered.connect(lambda: self.mainGui.showBuildManuscriptDialog())
86
-
87
- self.aDetails = QAction(self.tr("Details"), self)
88
- self.aDetails.setFont(lblFont)
89
- self.aDetails.setToolTip(self.tr("Project Details"))
90
- self.aDetails.triggered.connect(lambda: self.mainGui.showProjectDetailsDialog())
91
-
92
- self.aStats = QAction(self.tr("Stats"), self)
93
- self.aStats.setFont(lblFont)
94
- self.aStats.setToolTip(self.tr("Writing Statistics"))
95
- self.aStats.triggered.connect(lambda: self.mainGui.showWritingStatsDialog())
58
+ # Buttons
59
+ self.tbProject = QToolButton(self)
60
+ self.tbProject.setToolTip("{0} [Ctrl+T]".format(self.tr("Project Tree View")))
61
+ self.tbProject.setIconSize(iconSize)
62
+ self.tbProject.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT))
63
+
64
+ self.tbNovel = QToolButton(self)
65
+ self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View")))
66
+ self.tbNovel.setIconSize(iconSize)
67
+ self.tbNovel.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL))
68
+
69
+ self.tbOutline = QToolButton(self)
70
+ self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
71
+ self.tbOutline.setIconSize(iconSize)
72
+ self.tbOutline.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE))
73
+
74
+ self.tbBuild = QToolButton(self)
75
+ self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
76
+ self.tbBuild.setIconSize(iconSize)
77
+ self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
78
+
79
+ self.tbDetails = QToolButton(self)
80
+ self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Project Details")))
81
+ self.tbDetails.setIconSize(iconSize)
82
+ self.tbDetails.clicked.connect(self.mainGui.showProjectDetailsDialog)
83
+
84
+ self.tbStats = QToolButton(self)
85
+ self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics")))
86
+ self.tbStats.setIconSize(iconSize)
87
+ self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog)
96
88
 
97
89
  # Settings Menu
98
- self.mSettings = QMenu(self)
90
+ self.tbSettings = QToolButton(self)
91
+ self.tbSettings.setToolTip(self.tr("Settings"))
92
+ self.tbSettings.setIconSize(iconSize)
93
+ self.tbSettings.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
99
94
 
95
+ self.mSettings = _PopRightMenu(self.tbSettings)
100
96
  self.mSettings.addAction(self.mainGui.mainMenu.aEditWordList)
101
97
  self.mSettings.addAction(self.mainGui.mainMenu.aProjectSettings)
102
98
  self.mSettings.addSeparator()
103
99
  self.mSettings.addAction(self.mainGui.mainMenu.aPreferences)
104
100
 
105
- self.tbSettings = QToolButton(self)
106
- self.tbSettings.setFont(lblFont)
107
- self.tbSettings.setText(self.tr("Settings"))
108
101
  self.tbSettings.setMenu(self.mSettings)
109
- self.tbSettings.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
110
102
  self.tbSettings.setPopupMode(QToolButton.InstantPopup)
111
103
 
112
104
  # Assemble
113
- self.addAction(self.aProject)
114
- self.addAction(self.aNovel)
115
- self.addAction(self.aOutline)
116
- self.addAction(self.aBuild)
117
- self.addWidget(stretch)
118
- self.addAction(self.aDetails)
119
- self.addAction(self.aStats)
120
- self.addWidget(self.tbSettings)
121
-
105
+ self.outerBox = QVBoxLayout()
106
+ self.outerBox.addWidget(self.tbProject)
107
+ self.outerBox.addWidget(self.tbNovel)
108
+ self.outerBox.addWidget(self.tbOutline)
109
+ self.outerBox.addWidget(self.tbBuild)
110
+ self.outerBox.addStretch(1)
111
+ self.outerBox.addWidget(self.tbDetails)
112
+ self.outerBox.addWidget(self.tbStats)
113
+ self.outerBox.addWidget(self.tbSettings)
114
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
115
+ self.outerBox.setSpacing(CONFIG.pxInt(4))
116
+
117
+ self.setLayout(self.outerBox)
122
118
  self.updateTheme()
123
119
 
124
120
  logger.debug("Ready: GuiSideBar")
125
121
 
126
122
  return
127
123
 
128
- def updateTheme(self):
129
- """Initialise GUI elements that depend on specific settings.
130
- """
131
- self.setStyleSheet("QToolBar {border: 0px;}")
132
-
133
- self.aProject.setIcon(SHARED.theme.getIcon("view_editor"))
134
- self.aNovel.setIcon(SHARED.theme.getIcon("view_novel"))
135
- self.aOutline.setIcon(SHARED.theme.getIcon("view_outline"))
136
- self.aBuild.setIcon(SHARED.theme.getIcon("view_build"))
137
- self.aDetails.setIcon(SHARED.theme.getIcon("proj_details"))
138
- self.aStats.setIcon(SHARED.theme.getIcon("proj_stats"))
124
+ def updateTheme(self) -> None:
125
+ """Initialise GUI elements that depend on specific settings."""
126
+ qPalette = self.palette()
127
+ qPalette.setBrush(QPalette.Window, qPalette.base())
128
+ self.setPalette(qPalette)
129
+
130
+ fadeCol = qPalette.text().color()
131
+ buttonStyle = (
132
+ "QToolButton {{padding: {0}px; border: none; background: transparent;}} "
133
+ "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
134
+ ).format(CONFIG.pxInt(6), fadeCol.red(), fadeCol.green(), fadeCol.blue())
135
+ buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
136
+
137
+ self.tbProject.setIcon(SHARED.theme.getIcon("view_editor"))
138
+ self.tbProject.setStyleSheet(buttonStyle)
139
+
140
+ self.tbNovel.setIcon(SHARED.theme.getIcon("view_novel"))
141
+ self.tbNovel.setStyleSheet(buttonStyle)
142
+
143
+ self.tbOutline.setIcon(SHARED.theme.getIcon("view_outline"))
144
+ self.tbOutline.setStyleSheet(buttonStyle)
145
+
146
+ self.tbBuild.setIcon(SHARED.theme.getIcon("view_build"))
147
+ self.tbBuild.setStyleSheet(buttonStyle)
148
+
149
+ self.tbDetails.setIcon(SHARED.theme.getIcon("proj_details"))
150
+ self.tbDetails.setStyleSheet(buttonStyle)
151
+
152
+ self.tbStats.setIcon(SHARED.theme.getIcon("proj_stats"))
153
+ self.tbStats.setStyleSheet(buttonStyle)
154
+
139
155
  self.tbSettings.setIcon(SHARED.theme.getIcon("settings"))
156
+ self.tbSettings.setStyleSheet(buttonStyleMenu)
140
157
 
141
158
  return
142
159
 
143
160
  # END Class GuiSideBar
161
+
162
+
163
+ class _PopRightMenu(QMenu):
164
+
165
+ def event(self, event: QEvent):
166
+ """Overload the show event and move the menu popup location."""
167
+ if event.type() == QEvent.Show:
168
+ parent = self.parent()
169
+ if isinstance(parent, QWidget):
170
+ offset = QPoint(parent.width(), parent.height() - self.height())
171
+ self.move(parent.mapToGlobal(offset))
172
+ return super(_PopRightMenu, self).event(event)
173
+
174
+ # END Class _PopRightMenu
@@ -97,7 +97,7 @@ class GuiMainStatus(QStatusBar):
97
97
  self.addPermanentWidget(self.statsText)
98
98
 
99
99
  # The Session Clock
100
- # Set the mimimum width so the label doesn't rescale every second
100
+ # Set the minimum width so the label doesn't rescale every second
101
101
  self.timeIcon = QLabel()
102
102
  self.timeText = QLabel("")
103
103
  self.timeText.setToolTip(self.tr("Session Time"))
novelwriter/gui/theme.py CHANGED
@@ -36,7 +36,7 @@ from PyQt5.QtGui import (
36
36
  )
37
37
 
38
38
  from novelwriter import CONFIG
39
- from novelwriter.enum import nwItemLayout, nwItemType
39
+ from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
40
40
  from novelwriter.error import logException
41
41
  from novelwriter.common import NWConfigParser, minmax
42
42
  from novelwriter.constants import nwLabels
@@ -51,7 +51,7 @@ logger = logging.getLogger(__name__)
51
51
 
52
52
  class GuiTheme:
53
53
 
54
- def __init__(self):
54
+ def __init__(self) -> None:
55
55
 
56
56
  self.iconCache = GuiIcons(self)
57
57
 
@@ -97,6 +97,7 @@ class GuiTheme:
97
97
  self.colDialD = [0, 0, 0]
98
98
  self.colDialS = [0, 0, 0]
99
99
  self.colHidden = [0, 0, 0]
100
+ self.colCode = [0, 0, 0]
100
101
  self.colKey = [0, 0, 0]
101
102
  self.colVal = [0, 0, 0]
102
103
  self.colSpell = [0, 0, 0]
@@ -129,8 +130,10 @@ class GuiTheme:
129
130
  self.getIcon = self.iconCache.getIcon
130
131
  self.getPixmap = self.iconCache.getPixmap
131
132
  self.getItemIcon = self.iconCache.getItemIcon
133
+ self.getToggleIcon = self.iconCache.getToggleIcon
132
134
  self.loadDecoration = self.iconCache.loadDecoration
133
135
  self.getHeaderDecoration = self.iconCache.getHeaderDecoration
136
+ self.getHeaderDecorationNarrow = self.iconCache.getHeaderDecorationNarrow
134
137
 
135
138
  # Extract Other Info
136
139
  self.guiDPI = qApp.primaryScreen().logicalDotsPerInchX()
@@ -181,7 +184,7 @@ class GuiTheme:
181
184
  # Theme Methods
182
185
  ##
183
186
 
184
- def loadTheme(self):
187
+ def loadTheme(self) -> bool:
185
188
  """Load the currently specified GUI theme."""
186
189
  guiTheme = CONFIG.guiTheme
187
190
  if guiTheme not in self._availThemes:
@@ -268,7 +271,7 @@ class GuiTheme:
268
271
 
269
272
  return True
270
273
 
271
- def loadSyntax(self):
274
+ def loadSyntax(self) -> bool:
272
275
  """Load the currently specified syntax highlighter theme."""
273
276
  guiSyntax = CONFIG.guiSyntax
274
277
  if guiSyntax not in self._availSyntax:
@@ -316,6 +319,7 @@ class GuiTheme:
316
319
  self.colDialD = self._parseColour(confParser, cnfSec, "doublequotes")
317
320
  self.colDialS = self._parseColour(confParser, cnfSec, "singlequotes")
318
321
  self.colHidden = self._parseColour(confParser, cnfSec, "hidden")
322
+ self.colCode = self._parseColour(confParser, cnfSec, "shortcode")
319
323
  self.colKey = self._parseColour(confParser, cnfSec, "keyword")
320
324
  self.colVal = self._parseColour(confParser, cnfSec, "value")
321
325
  self.colSpell = self._parseColour(confParser, cnfSec, "spellcheckline")
@@ -361,7 +365,7 @@ class GuiTheme:
361
365
  # Internal Functions
362
366
  ##
363
367
 
364
- def _setGuiFont(self):
368
+ def _setGuiFont(self) -> None:
365
369
  """Update the GUI's font style from settings."""
366
370
  theFont = QFont()
367
371
  fontDB = QFontDatabase()
@@ -393,9 +397,7 @@ class GuiTheme:
393
397
 
394
398
  return True
395
399
 
396
- def _parseColour(
397
- self, parser: NWConfigParser, section: str, name: str
398
- ) -> list[int]:
400
+ def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> list[int]:
399
401
  """Parse a colour value from a config string."""
400
402
  if parser.has_option(section, name):
401
403
  values = parser.get(section, name).split(",")
@@ -412,9 +414,8 @@ class GuiTheme:
412
414
  result = [0, 0, 0]
413
415
  return result
414
416
 
415
- def _setPalette(
416
- self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole
417
- ):
417
+ def _setPalette(self, parser: NWConfigParser, section: str,
418
+ name: str, value: QPalette.ColorRole) -> None:
418
419
  """Set a palette colour value from a config string."""
419
420
  self._guiPalette.setColor(
420
421
  value, QColor(*self._parseColour(parser, section, name))
@@ -445,19 +446,27 @@ class GuiIcons:
445
446
  ICON_KEYS = {
446
447
  # Project and GUI Icons
447
448
  "novelwriter", "alert_error", "alert_info", "alert_question", "alert_warn",
448
- "build_excluded", "build_filtered", "build_included", "cls_archive", "cls_character",
449
- "cls_custom", "cls_entity", "cls_none", "cls_novel", "cls_object", "cls_plot",
450
- "cls_timeline", "cls_trash", "cls_world", "proj_chapter", "proj_details", "proj_document",
451
- "proj_folder", "proj_note", "proj_nwx", "proj_section", "proj_scene", "proj_stats",
452
- "proj_title", "search_cancel", "search_case", "search_loop", "search_preserve",
453
- "search_project", "search_regex", "search_word", "status_idle", "status_lang",
454
- "status_lines", "status_stats", "status_time", "view_build", "view_editor", "view_novel",
455
- "view_outline",
449
+ "build_excluded", "build_filtered", "build_included", "proj_chapter", "proj_details",
450
+ "proj_document", "proj_folder", "proj_note", "proj_nwx", "proj_section", "proj_scene",
451
+ "proj_stats", "proj_title", "status_idle", "status_lang", "status_lines", "status_stats",
452
+ "status_time", "view_build", "view_editor", "view_novel", "view_outline",
453
+
454
+ # Class Icons
455
+ "cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none", "cls_novel",
456
+ "cls_object", "cls_plot", "cls_timeline", "cls_trash", "cls_world",
457
+
458
+ # Search Icons
459
+ "search_cancel", "search_case", "search_loop", "search_preserve", "search_project",
460
+ "search_regex", "search_word",
461
+
462
+ # Format Icons
463
+ "fmt_bold", "fmt_italic", "fmt_mode-md", "fmt_mode-sc", "fmt_strike", "fmt_subscript",
464
+ "fmt_superscript", "fmt_underline",
456
465
 
457
466
  # General Button Icons
458
467
  "add", "backward", "bookmark", "browse", "checked", "close", "cross", "down", "edit",
459
- "export", "forward", "maximise", "menu", "minimise", "noncheckable", "reference",
460
- "refresh", "remove", "revert", "search_replace", "search", "settings", "unchecked", "up",
468
+ "export", "forward", "maximise", "menu", "minimise", "noncheckable", "panel", "refresh",
469
+ "remove", "revert", "search_replace", "search", "settings", "unchecked", "up", "view",
461
470
 
462
471
  # Switches
463
472
  "sticky-on", "sticky-off",
@@ -465,23 +474,32 @@ class GuiIcons:
465
474
 
466
475
  # Decorations
467
476
  "deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4", "deco_doc_more",
477
+ "deco_doc_h0_n", "deco_doc_h1_n", "deco_doc_h2_n", "deco_doc_h3_n", "deco_doc_h4_n",
478
+ "deco_doc_nt_n",
479
+ }
480
+
481
+ TOGGLE_ICON_KEYS = {
482
+ "sticky": ("sticky-on", "sticky-off"),
483
+ "bullet": ("bullet-on", "bullet-off"),
484
+ "fmt_mode": ("fmt_mode-sc", "fmt_mode-md"),
468
485
  }
469
486
 
470
487
  IMAGE_MAP = {
471
488
  "wiz-back": "wizard-back.jpg",
472
489
  }
473
490
 
474
- def __init__(self, mainTheme):
491
+ def __init__(self, mainTheme: GuiTheme) -> None:
475
492
 
476
493
  self.mainTheme = mainTheme
477
494
 
478
495
  # Storage
479
- self._qIcons = {}
480
- self._themeMap = {}
481
- self._headerDec = []
482
- self._confName = "icons.conf"
496
+ self._qIcons: dict[str, QIcon] = {}
497
+ self._themeMap: dict[str, Path] = {}
498
+ self._headerDec: list[QPixmap] = []
499
+ self._headerDecNarrow: list[QPixmap] = []
483
500
 
484
501
  # Icon Theme Path
502
+ self._confName = "icons.conf"
485
503
  self._iconPath = CONFIG.assetPath("icons")
486
504
 
487
505
  # Icon Theme Meta
@@ -499,7 +517,7 @@ class GuiIcons:
499
517
  # Actions
500
518
  ##
501
519
 
502
- def loadTheme(self, iconTheme):
520
+ def loadTheme(self, iconTheme: str) -> bool:
503
521
  """Update the theme map. This is more of an init, since many of
504
522
  the GUI icons cannot really be replaced without writing specific
505
523
  update functions for the classes where they're used.
@@ -566,6 +584,7 @@ class GuiIcons:
566
584
  self._qIcons[iconKey] = qIcon
567
585
 
568
586
  self._headerDec = []
587
+ self._headerDecNarrow = []
569
588
 
570
589
  return True
571
590
 
@@ -573,52 +592,60 @@ class GuiIcons:
573
592
  # Access Functions
574
593
  ##
575
594
 
576
- def loadDecoration(self, decoKey, pxW=None, pxH=None):
595
+ def loadDecoration(self, name: str, w: int | None = None, h: int | None = None) -> QPixmap:
577
596
  """Load graphical decoration element based on the decoration
578
597
  map or the icon map. This function always returns a QPixmap.
579
598
  """
580
- if decoKey in self._themeMap:
581
- imgPath = self._themeMap[decoKey]
582
- elif decoKey in self.IMAGE_MAP:
583
- imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[decoKey]
599
+ if name in self._themeMap:
600
+ imgPath = self._themeMap[name]
601
+ elif name in self.IMAGE_MAP:
602
+ imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name]
584
603
  else:
585
- logger.error("Decoration with name '%s' does not exist", decoKey)
604
+ logger.error("Decoration with name '%s' does not exist", name)
586
605
  return QPixmap()
587
606
 
588
607
  if not imgPath.is_file():
589
608
  logger.error("Asset not found: %s", imgPath)
590
609
  return QPixmap()
591
610
 
592
- theDeco = QPixmap(str(imgPath))
593
- if pxW is not None and pxH is not None:
594
- return theDeco.scaled(pxW, pxH, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
595
- elif pxW is None and pxH is not None:
596
- return theDeco.scaledToHeight(pxH, Qt.SmoothTransformation)
597
- elif pxW is not None and pxH is None:
598
- return theDeco.scaledToWidth(pxW, Qt.SmoothTransformation)
611
+ pixmap = QPixmap(str(imgPath))
612
+ if w is not None and h is not None:
613
+ return pixmap.scaled(w, h, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
614
+ elif w is None and h is not None:
615
+ return pixmap.scaledToHeight(h, Qt.SmoothTransformation)
616
+ elif w is not None and h is None:
617
+ return pixmap.scaledToWidth(w, Qt.SmoothTransformation)
599
618
 
600
- return theDeco
619
+ return pixmap
601
620
 
602
- def getIcon(self, iconKey):
603
- """Return an icon from the icon buffer. If it doesn't exist,
604
- return, load it, and if it still doesn't exist, return an empty
605
- icon.
606
- """
607
- if iconKey in self._qIcons:
608
- return self._qIcons[iconKey]
621
+ def getIcon(self, name: str) -> QIcon:
622
+ """Return an icon from the icon buffer, or load it."""
623
+ if name in self._qIcons:
624
+ return self._qIcons[name]
609
625
  else:
610
- qIcon = self._loadIcon(iconKey)
611
- self._qIcons[iconKey] = qIcon
612
- return qIcon
626
+ icon = self._loadIcon(name)
627
+ self._qIcons[name] = icon
628
+ return icon
629
+
630
+ def getToggleIcon(self, name: str, size: tuple[int, int]) -> QIcon:
631
+ """Return a toggle icon from the icon buffer. or load it."""
632
+ if name in self.TOGGLE_ICON_KEYS:
633
+ pOne = self.getPixmap(self.TOGGLE_ICON_KEYS[name][0], size)
634
+ pTwo = self.getPixmap(self.TOGGLE_ICON_KEYS[name][1], size)
635
+ icon = QIcon()
636
+ icon.addPixmap(pOne, QIcon.Normal, QIcon.On)
637
+ icon.addPixmap(pTwo, QIcon.Normal, QIcon.Off)
638
+ return icon
639
+ return QIcon()
613
640
 
614
- def getPixmap(self, iconKey, iconSize):
641
+ def getPixmap(self, name: str, size: tuple[int, int]) -> QPixmap:
615
642
  """Return an icon from the icon buffer as a QPixmap. If it
616
643
  doesn't exist, return an empty QPixmap.
617
644
  """
618
- qIcon = self.getIcon(iconKey)
619
- return qIcon.pixmap(iconSize[0], iconSize[1], QIcon.Normal)
645
+ return self.getIcon(name).pixmap(size[0], size[1], QIcon.Normal)
620
646
 
621
- def getItemIcon(self, tType, tClass, tLayout, hLevel="H0"):
647
+ def getItemIcon(self, tType: nwItemType, tClass: nwItemClass,
648
+ tLayout: nwItemLayout, hLevel: str = "H0") -> QIcon:
622
649
  """Get the correct icon for a project item based on type, class
623
650
  and header level
624
651
  """
@@ -645,45 +672,58 @@ class GuiIcons:
645
672
 
646
673
  return self.getIcon(iconName)
647
674
 
648
- def getHeaderDecoration(self, hLevel):
649
- """Get the decoration for a specific header level.
650
- """
675
+ def getHeaderDecoration(self, hLevel: int) -> QPixmap:
676
+ """Get the decoration for a specific header level."""
651
677
  if not self._headerDec:
652
678
  iPx = self.mainTheme.baseIconSize
653
679
  self._headerDec = [
654
- self.loadDecoration("deco_doc_h0", pxH=iPx),
655
- self.loadDecoration("deco_doc_h1", pxH=iPx),
656
- self.loadDecoration("deco_doc_h2", pxH=iPx),
657
- self.loadDecoration("deco_doc_h3", pxH=iPx),
658
- self.loadDecoration("deco_doc_h4", pxH=iPx),
680
+ self.loadDecoration("deco_doc_h0", h=iPx),
681
+ self.loadDecoration("deco_doc_h1", h=iPx),
682
+ self.loadDecoration("deco_doc_h2", h=iPx),
683
+ self.loadDecoration("deco_doc_h3", h=iPx),
684
+ self.loadDecoration("deco_doc_h4", h=iPx),
659
685
  ]
660
686
  return self._headerDec[minmax(hLevel, 0, 4)]
661
687
 
688
+ def getHeaderDecorationNarrow(self, hLevel: int) -> QPixmap:
689
+ """Get the narrow decoration for a specific header level."""
690
+ if not self._headerDecNarrow:
691
+ iPx = self.mainTheme.baseIconSize
692
+ self._headerDecNarrow = [
693
+ self.loadDecoration("deco_doc_h0_n", h=iPx),
694
+ self.loadDecoration("deco_doc_h1_n", h=iPx),
695
+ self.loadDecoration("deco_doc_h2_n", h=iPx),
696
+ self.loadDecoration("deco_doc_h3_n", h=iPx),
697
+ self.loadDecoration("deco_doc_h4_n", h=iPx),
698
+ self.loadDecoration("deco_doc_nt_n", h=iPx),
699
+ ]
700
+ return self._headerDecNarrow[minmax(hLevel, 0, 5)]
701
+
662
702
  ##
663
703
  # Internal Functions
664
704
  ##
665
705
 
666
- def _loadIcon(self, iconKey):
706
+ def _loadIcon(self, name: str) -> QIcon:
667
707
  """Load an icon from the assets themes folder. Is guaranteed to
668
708
  return a QIcon.
669
709
  """
670
- if iconKey not in self.ICON_KEYS:
671
- logger.error("Requested unknown icon name '%s'", iconKey)
710
+ if name not in self.ICON_KEYS:
711
+ logger.error("Requested unknown icon name '%s'", name)
672
712
  return QIcon()
673
713
 
674
714
  # If we just want the app icons, return right away
675
- if iconKey == "novelwriter":
715
+ if name == "novelwriter":
676
716
  return QIcon(str(self._iconPath / "novelwriter.svg"))
677
- elif iconKey == "proj_nwx":
717
+ elif name == "proj_nwx":
678
718
  return QIcon(str(self._iconPath / "x-novelwriter-project.svg"))
679
719
 
680
720
  # Otherwise, we load from the theme folder
681
- if iconKey in self._themeMap:
682
- logger.debug("Loading: %s", self._themeMap[iconKey].name)
683
- return QIcon(str(self._themeMap[iconKey]))
721
+ if name in self._themeMap:
722
+ logger.debug("Loading: %s", self._themeMap[name].name)
723
+ return QIcon(str(self._themeMap[name]))
684
724
 
685
725
  # If we didn't find one, give up and return an empty icon
686
- logger.warning("Did not load an icon for '%s'", iconKey)
726
+ logger.warning("Did not load an icon for '%s'", name)
687
727
 
688
728
  return QIcon()
689
729
 
@@ -694,9 +734,8 @@ class GuiIcons:
694
734
  # Module Functions
695
735
  # =============================================================================================== #
696
736
 
697
- def _loadInternalName(confParser, confFile):
698
- """Open a conf file and read the 'name' setting.
699
- """
737
+ def _loadInternalName(confParser: NWConfigParser, confFile: str | Path) -> str:
738
+ """Open a conf file and read the 'name' setting."""
700
739
  try:
701
740
  with open(confFile, mode="r", encoding="utf-8") as inFile:
702
741
  confParser.read_file(inFile)