novelWriter 2.2b1__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 (62) hide show
  1. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/METADATA +3 -3
  2. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/RECORD +60 -48
  3. novelwriter/__init__.py +3 -3
  4. novelwriter/assets/i18n/project_en_GB.json +1 -0
  5. novelwriter/assets/icons/novelwriter.ico +0 -0
  6. novelwriter/assets/icons/typicons_dark/icons.conf +8 -1
  7. novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
  8. novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
  9. novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
  10. novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
  11. novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
  12. novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
  13. novelwriter/assets/icons/typicons_light/icons.conf +8 -1
  14. novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
  15. novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
  16. novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
  17. novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
  18. novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
  19. novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
  20. novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
  21. novelwriter/assets/manual.pdf +0 -0
  22. novelwriter/assets/sample.zip +0 -0
  23. novelwriter/assets/text/release_notes.htm +4 -4
  24. novelwriter/common.py +22 -1
  25. novelwriter/config.py +12 -27
  26. novelwriter/constants.py +20 -3
  27. novelwriter/core/buildsettings.py +1 -1
  28. novelwriter/core/coretools.py +6 -1
  29. novelwriter/core/index.py +100 -34
  30. novelwriter/core/options.py +3 -0
  31. novelwriter/core/project.py +2 -2
  32. novelwriter/core/projectdata.py +1 -1
  33. novelwriter/core/tohtml.py +9 -3
  34. novelwriter/core/tokenizer.py +27 -20
  35. novelwriter/core/tomd.py +4 -0
  36. novelwriter/core/toodt.py +11 -4
  37. novelwriter/dialogs/preferences.py +80 -82
  38. novelwriter/dialogs/updates.py +25 -14
  39. novelwriter/enum.py +14 -4
  40. novelwriter/gui/doceditor.py +282 -177
  41. novelwriter/gui/dochighlight.py +7 -9
  42. novelwriter/gui/docviewer.py +142 -319
  43. novelwriter/gui/docviewerpanel.py +457 -0
  44. novelwriter/gui/editordocument.py +1 -1
  45. novelwriter/gui/mainmenu.py +16 -7
  46. novelwriter/gui/outline.py +10 -6
  47. novelwriter/gui/projtree.py +461 -376
  48. novelwriter/gui/sidebar.py +3 -3
  49. novelwriter/gui/statusbar.py +1 -1
  50. novelwriter/gui/theme.py +21 -2
  51. novelwriter/guimain.py +86 -32
  52. novelwriter/shared.py +23 -1
  53. novelwriter/tools/dictionaries.py +268 -0
  54. novelwriter/tools/manusbuild.py +17 -6
  55. novelwriter/tools/manuscript.py +1 -1
  56. novelwriter/tools/writingstats.py +1 -1
  57. novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
  58. novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
  59. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/LICENSE.md +0 -0
  60. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/WHEEL +0 -0
  61. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/entry_points.txt +0 -0
  62. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,457 @@
1
+ """
2
+ novelWriter – GUI Document Viewer Panel
3
+ =======================================
4
+
5
+ File History:
6
+ Created: 2023-11-14 [2.2rc1] GuiDocViewerPanel
7
+
8
+ This file is a part of novelWriter
9
+ Copyright 2018–2023, Veronica Berglyd Olsen
10
+
11
+ This program is free software: you can redistribute it and/or modify
12
+ it under the terms of the GNU General Public License as published by
13
+ the Free Software Foundation, either version 3 of the License, or
14
+ (at your option) any later version.
15
+
16
+ This program is distributed in the hope that it will be useful, but
17
+ WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19
+ General Public License for more details.
20
+
21
+ You should have received a copy of the GNU General Public License
22
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+
28
+ from enum import Enum
29
+
30
+ from PyQt5.QtCore import QModelIndex, QSize, Qt, pyqtSignal, pyqtSlot
31
+ from PyQt5.QtWidgets import (
32
+ QAbstractItemView, QFrame, QHeaderView, QTabWidget, QTreeWidget,
33
+ QTreeWidgetItem, QVBoxLayout, QWidget
34
+ )
35
+
36
+ from novelwriter import CONFIG, SHARED
37
+ from novelwriter.enum import nwDocMode, nwItemClass
38
+ from novelwriter.common import checkInt
39
+ from novelwriter.constants import nwHeaders, nwLabels, nwLists, trConst
40
+ from novelwriter.core.index import IndexHeading, IndexItem
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ class GuiDocViewerPanel(QWidget):
46
+
47
+ openDocumentRequest = pyqtSignal(str, Enum, str, bool)
48
+ loadDocumentTagRequest = pyqtSignal(str, Enum)
49
+
50
+ def __init__(self, parent: QWidget) -> None:
51
+ super().__init__(parent=parent)
52
+
53
+ logger.debug("Create: GuiDocViewerPanel")
54
+
55
+ self._lastHandle = None
56
+
57
+ self.tabBackRefs = _ViewPanelBackRefs(self)
58
+
59
+ self.mainTabs = QTabWidget(self)
60
+ self.mainTabs.addTab(self.tabBackRefs, self.tr("Backreferences"))
61
+
62
+ self.kwTabs: dict[str, _ViewPanelKeyWords] = {}
63
+ self.idTabs: dict[str, int] = {}
64
+ for itemClass in nwLists.USER_CLASSES:
65
+ cTab = _ViewPanelKeyWords(self, itemClass)
66
+ tabId = self.mainTabs.addTab(cTab, trConst(nwLabels.CLASS_NAME[itemClass]))
67
+ self.kwTabs[itemClass.name] = cTab
68
+ self.idTabs[itemClass.name] = tabId
69
+
70
+ # Assemble
71
+ self.outerBox = QVBoxLayout()
72
+ self.outerBox.addWidget(self.mainTabs)
73
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
74
+
75
+ self.setLayout(self.outerBox)
76
+ self.updateTheme()
77
+
78
+ logger.debug("Ready: GuiDocViewerPanel")
79
+
80
+ return
81
+
82
+ ##
83
+ # Methods
84
+ ##
85
+
86
+ def updateTheme(self) -> None:
87
+ """Update theme elements."""
88
+ vPx = CONFIG.pxInt(4)
89
+ lPx = CONFIG.pxInt(2)
90
+ rPx = CONFIG.pxInt(14)
91
+ hCol = self.palette().highlight().color()
92
+
93
+ styleSheet = (
94
+ "QTabWidget::pane {border: 0;} "
95
+ "QTabWidget QTabBar::tab {"
96
+ f"border: 0; padding: {vPx}px {rPx}px {vPx}px {lPx}px;"
97
+ "} "
98
+ "QTabWidget QTabBar::tab:selected {"
99
+ f"color: rgb({hCol.red()}, {hCol.green()}, {hCol.blue()});"
100
+ "} "
101
+ )
102
+ self.mainTabs.setStyleSheet(styleSheet)
103
+ self.updateHandle(self._lastHandle)
104
+
105
+ return
106
+
107
+ def openProjectTasks(self) -> None:
108
+ """Run open project tasks."""
109
+ widths = SHARED.project.options.getValue("GuiDocViewerPanel", "colWidths", {})
110
+ if isinstance(widths, dict):
111
+ for key, value in widths.items():
112
+ if key in self.kwTabs and isinstance(value, list):
113
+ self.kwTabs[key].setColumnWidths(value)
114
+ return
115
+
116
+ def closeProjectTasks(self) -> None:
117
+ """Run close project tasks."""
118
+ widths = {}
119
+ for key, tab in self.kwTabs.items():
120
+ widths[key] = tab.getColumnWidths()
121
+ SHARED.project.options.setValue("GuiDocViewerPanel", "colWidths", widths)
122
+ return
123
+
124
+ ##
125
+ # Public Slots
126
+ ##
127
+
128
+ @pyqtSlot()
129
+ def indexWasCleared(self) -> None:
130
+ """Handle event when the index has been cleared of content."""
131
+ self.tabBackRefs.clearContent()
132
+ for cTab in self.kwTabs.values():
133
+ cTab.clearContent()
134
+ return
135
+
136
+ @pyqtSlot()
137
+ def indexHasAppeared(self) -> None:
138
+ """Handle event when the index has appeared."""
139
+ for key, name, tClass, iItem, hItem in SHARED.project.index.getTagsData():
140
+ if tClass in self.kwTabs and iItem and hItem:
141
+ self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem)
142
+ self._updateTabVisibility()
143
+ self.updateHandle(self._lastHandle)
144
+ return
145
+
146
+ @pyqtSlot(str)
147
+ def projectItemChanged(self, tHandle: str) -> None:
148
+ """Update meta data for project item."""
149
+ self.tabBackRefs.refreshDocument(tHandle)
150
+ for key in SHARED.project.index.getDocumentTags(tHandle):
151
+ name, tClass, iItem, hItem = SHARED.project.index.getSingleTag(key)
152
+ if tClass in self.kwTabs and iItem and hItem:
153
+ self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem)
154
+ return
155
+
156
+ @pyqtSlot(str)
157
+ def updateHandle(self, tHandle: str | None) -> None:
158
+ """Update the document handle."""
159
+ self._lastHandle = tHandle
160
+ self.tabBackRefs.refreshContent(tHandle or None)
161
+ return
162
+
163
+ @pyqtSlot(list, list)
164
+ def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None:
165
+ """Forward tags changes to the lists."""
166
+ for key in updated:
167
+ name, tClass, iItem, hItem = SHARED.project.index.getSingleTag(key)
168
+ if tClass in self.kwTabs and iItem and hItem:
169
+ self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem)
170
+ for key in deleted:
171
+ for cTab in self.kwTabs.values():
172
+ if cTab.removeEntry(key):
173
+ break
174
+ else:
175
+ logger.warning("Could not remove tag '%s' from view panel", key)
176
+ self._updateTabVisibility()
177
+ return
178
+
179
+ ##
180
+ # Internal Functions
181
+ ##
182
+
183
+ def _updateTabVisibility(self) -> None:
184
+ """Hide class tabs with no content."""
185
+ if CONFIG.verQtValue >= 0x050f00:
186
+ for tClass, cTab in self.kwTabs.items():
187
+ self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.countEntries() > 0)
188
+ return
189
+
190
+ # END Class GuiDocViewerPanel
191
+
192
+
193
+ class _ViewPanelBackRefs(QTreeWidget):
194
+
195
+ C_DATA = 0
196
+ C_DOC = 0
197
+ C_EDIT = 1
198
+ C_VIEW = 2
199
+ C_TITLE = 3
200
+
201
+ D_HANDLE = Qt.ItemDataRole.UserRole
202
+
203
+ def __init__(self, parent: GuiDocViewerPanel) -> None:
204
+ super().__init__(parent=parent)
205
+
206
+ self._parent = parent
207
+ self._treeMap: dict[str, QTreeWidgetItem] = {}
208
+
209
+ iPx = SHARED.theme.baseIconSize
210
+ cMg = CONFIG.pxInt(6)
211
+
212
+ self.setHeaderLabels([self.tr("Document"), "", "", self.tr("First Heading")])
213
+ self.setIndentation(0)
214
+ self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
215
+ self.setIconSize(QSize(iPx, iPx))
216
+ self.setFrameStyle(QFrame.Shape.NoFrame)
217
+
218
+ # Set Header Sizes
219
+ treeHeader = self.header()
220
+ treeHeader.setStretchLastSection(True)
221
+ treeHeader.setSectionResizeMode(self.C_DOC, QHeaderView.ResizeMode.ResizeToContents)
222
+ treeHeader.setSectionResizeMode(self.C_EDIT, QHeaderView.ResizeMode.Fixed)
223
+ treeHeader.setSectionResizeMode(self.C_VIEW, QHeaderView.ResizeMode.Fixed)
224
+ treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.ResizeMode.ResizeToContents)
225
+ treeHeader.resizeSection(self.C_EDIT, iPx + cMg)
226
+ treeHeader.resizeSection(self.C_VIEW, iPx + cMg)
227
+
228
+ # Cache Icons Locally
229
+ self._editIcon = SHARED.theme.getIcon("edit")
230
+ self._viewIcon = SHARED.theme.getIcon("view")
231
+
232
+ # Signals
233
+ self.clicked.connect(self._treeItemClicked)
234
+ self.doubleClicked.connect(self._treeItemDoubleClicked)
235
+
236
+ return
237
+
238
+ def clearContent(self) -> None:
239
+ """Clear the widget."""
240
+ self.clear()
241
+ self._treeMap = {}
242
+ return
243
+
244
+ def refreshContent(self, dHandle: str | None) -> None:
245
+ """Update the content."""
246
+ self.clearContent()
247
+ if dHandle:
248
+ refs = SHARED.project.index.getBackReferenceList(dHandle)
249
+ for tHandle, (sTitle, hItem) in refs.items():
250
+ self._setTreeItemValues(tHandle, sTitle, hItem)
251
+ return
252
+
253
+ def refreshDocument(self, tHandle: str) -> None:
254
+ """Refresh document meta data."""
255
+ if iItem := SHARED.project.index.getItemData(tHandle):
256
+ for sTitle, hItem in iItem.items():
257
+ if f"{tHandle}:{sTitle}" in self._treeMap:
258
+ self._setTreeItemValues(tHandle, sTitle, hItem)
259
+ return
260
+
261
+ ##
262
+ # Private Slots
263
+ ##
264
+
265
+ @pyqtSlot("QModelIndex")
266
+ def _treeItemClicked(self, index: QModelIndex) -> None:
267
+ """Emit document open signal on user click."""
268
+ tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE)
269
+ if index.column() == self.C_EDIT:
270
+ self._parent.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
271
+ elif index.column() == self.C_VIEW:
272
+ self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
273
+ return
274
+
275
+ @pyqtSlot("QModelIndex")
276
+ def _treeItemDoubleClicked(self, index: QModelIndex) -> None:
277
+ """Emit follow tag signal on user double click."""
278
+ tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE)
279
+ if index.column() == self.C_DOC:
280
+ self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
281
+ return
282
+
283
+ ##
284
+ # Internal Functions
285
+ ##
286
+
287
+ def _setTreeItemValues(self, tHandle: str, sTitle: str, hItem: IndexHeading) -> None:
288
+ """Add or update a tree item."""
289
+ if nwItem := SHARED.project.tree[tHandle]:
290
+ docIcon = SHARED.theme.getItemIcon(
291
+ nwItem.itemType, nwItem.itemClass,
292
+ nwItem.itemLayout, nwItem.mainHeading
293
+ )
294
+ iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5
295
+ hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel)
296
+
297
+ tKey = f"{tHandle}:{sTitle}"
298
+ trItem = self._treeMap[tKey] if tKey in self._treeMap else QTreeWidgetItem()
299
+
300
+ trItem.setIcon(self.C_DOC, docIcon)
301
+ trItem.setText(self.C_DOC, nwItem.itemName)
302
+ trItem.setIcon(self.C_EDIT, self._editIcon)
303
+ trItem.setIcon(self.C_VIEW, self._viewIcon)
304
+ trItem.setText(self.C_TITLE, hItem.title)
305
+ trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
306
+ trItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
307
+
308
+ if tKey not in self._treeMap:
309
+ self.addTopLevelItem(trItem)
310
+ self._treeMap[tKey] = trItem
311
+
312
+ return
313
+
314
+ # END Class _ViewPanelBackRefs
315
+
316
+
317
+ class _ViewPanelKeyWords(QTreeWidget):
318
+
319
+ C_DATA = 0
320
+ C_NAME = 0
321
+ C_EDIT = 1
322
+ C_VIEW = 2
323
+ C_DOC = 3
324
+ C_TITLE = 4
325
+ C_SHORT = 5
326
+
327
+ D_TAG = Qt.ItemDataRole.UserRole
328
+
329
+ def __init__(self, parent: GuiDocViewerPanel, itemClass: nwItemClass) -> None:
330
+ super().__init__(parent=parent)
331
+
332
+ self._parent = parent
333
+ self._treeMap: dict[str, QTreeWidgetItem] = {}
334
+
335
+ iPx = SHARED.theme.baseIconSize
336
+ cMg = CONFIG.pxInt(6)
337
+
338
+ self.setHeaderLabels([
339
+ self.tr("Tag"), "", "", self.tr("Document"),
340
+ self.tr("Heading"), self.tr("Short Description")
341
+ ])
342
+ self.setIndentation(0)
343
+ self.setIconSize(QSize(iPx, iPx))
344
+ self.setFrameStyle(QFrame.Shape.NoFrame)
345
+ self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
346
+ self.setExpandsOnDoubleClick(False)
347
+ self.setDragEnabled(False)
348
+ self.setSortingEnabled(True)
349
+ self.sortByColumn(self.C_NAME, Qt.SortOrder.AscendingOrder)
350
+
351
+ # Set Header Sizes
352
+ treeHeader = self.header()
353
+ treeHeader.setStretchLastSection(True)
354
+ treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.ResizeToContents)
355
+ treeHeader.setSectionResizeMode(self.C_EDIT, QHeaderView.ResizeMode.Fixed)
356
+ treeHeader.setSectionResizeMode(self.C_VIEW, QHeaderView.ResizeMode.Fixed)
357
+ treeHeader.resizeSection(self.C_EDIT, iPx + cMg)
358
+ treeHeader.resizeSection(self.C_VIEW, iPx + cMg)
359
+ treeHeader.setSectionsMovable(False)
360
+
361
+ # Cache Icons Locally
362
+ self._classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass])
363
+ self._editIcon = SHARED.theme.getIcon("edit")
364
+ self._viewIcon = SHARED.theme.getIcon("view")
365
+
366
+ # Signals
367
+ self.clicked.connect(self._treeItemClicked)
368
+ self.doubleClicked.connect(self._treeItemDoubleClicked)
369
+
370
+ return
371
+
372
+ def countEntries(self) -> int:
373
+ """Return the number of items in the list."""
374
+ return self.topLevelItemCount()
375
+
376
+ def clearContent(self) -> None:
377
+ """Clear the list."""
378
+ self._treeMap = {}
379
+ self.clear()
380
+ return
381
+
382
+ def addUpdateEntry(self, tag: str, name: str, iItem: IndexItem, hItem: IndexHeading) -> None:
383
+ """Add a new entry, or update an existing one."""
384
+ nwItem = iItem.item
385
+ docIcon = SHARED.theme.getItemIcon(
386
+ nwItem.itemType, nwItem.itemClass,
387
+ nwItem.itemLayout, nwItem.mainHeading
388
+ )
389
+ iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5
390
+ hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel)
391
+
392
+ # This can not use a get call to the dictionary as that creates
393
+ # some weird issue with Qt, so we need to do this with an if
394
+ trItem = self._treeMap[tag] if tag in self._treeMap else QTreeWidgetItem()
395
+
396
+ trItem.setText(self.C_NAME, name)
397
+ trItem.setIcon(self.C_NAME, self._classIcon)
398
+ trItem.setIcon(self.C_EDIT, self._editIcon)
399
+ trItem.setIcon(self.C_VIEW, self._viewIcon)
400
+ trItem.setIcon(self.C_DOC, docIcon)
401
+ trItem.setText(self.C_DOC, nwItem.itemName)
402
+ trItem.setText(self.C_TITLE, hItem.title)
403
+ trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
404
+ trItem.setText(self.C_SHORT, hItem.synopsis)
405
+ trItem.setData(self.C_DATA, self.D_TAG, tag)
406
+
407
+ if tag not in self._treeMap:
408
+ self.addTopLevelItem(trItem)
409
+ self._treeMap[tag] = trItem
410
+
411
+ return
412
+
413
+ def removeEntry(self, tag: str) -> bool:
414
+ """Remove a tag from the list."""
415
+ if tag in self._treeMap:
416
+ self.takeTopLevelItem(self.indexOfTopLevelItem(self._treeMap[tag]))
417
+ self._treeMap.pop(tag, None)
418
+ return True
419
+ return False
420
+
421
+ def setColumnWidths(self, widths: list[int]) -> None:
422
+ """Set the column widths."""
423
+ if isinstance(widths, list) and len(widths) >= 2:
424
+ self.setColumnWidth(self.C_DOC, CONFIG.pxInt(checkInt(widths[0], 100)))
425
+ self.setColumnWidth(self.C_TITLE, CONFIG.pxInt(checkInt(widths[1], 100)))
426
+ return
427
+
428
+ def getColumnWidths(self) -> list[int]:
429
+ """Get the widths of the user-adjustable columns."""
430
+ return [
431
+ CONFIG.rpxInt(self.columnWidth(self.C_DOC)),
432
+ CONFIG.rpxInt(self.columnWidth(self.C_TITLE)),
433
+ ]
434
+
435
+ ##
436
+ # Private Slots
437
+ ##
438
+
439
+ @pyqtSlot("QModelIndex")
440
+ def _treeItemClicked(self, index: QModelIndex) -> None:
441
+ """Emit follow tag signal on user click."""
442
+ tag = index.siblingAtColumn(self.C_DATA).data(self.D_TAG)
443
+ if index.column() == self.C_EDIT:
444
+ self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.EDIT)
445
+ elif index.column() == self.C_VIEW:
446
+ self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
447
+ return
448
+
449
+ @pyqtSlot("QModelIndex")
450
+ def _treeItemDoubleClicked(self, index: QModelIndex) -> None:
451
+ """Emit follow tag signal on user double click."""
452
+ tag = index.siblingAtColumn(self.C_DATA).data(self.D_TAG)
453
+ if index.column() == self.C_NAME:
454
+ self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
455
+ return
456
+
457
+ # END Class _ViewPanelKeyWords
@@ -64,7 +64,7 @@ class GuiTextDocument(QTextDocument):
64
64
  return self._syntax
65
65
 
66
66
  ##
67
- # Metods
67
+ # Methods
68
68
  ##
69
69
 
70
70
  def setTextContent(self, text: str, tHandle: str) -> None:
@@ -27,15 +27,14 @@ import logging
27
27
 
28
28
  from typing import TYPE_CHECKING
29
29
  from pathlib import Path
30
- from urllib.parse import urljoin
31
- from urllib.request import pathname2url
32
30
 
33
- from PyQt5.QtCore import QUrl, pyqtSignal, pyqtSlot
34
31
  from PyQt5.QtGui import QDesktopServices
32
+ from PyQt5.QtCore import QUrl, pyqtSignal, pyqtSlot
35
33
  from PyQt5.QtWidgets import QMenuBar, QAction
36
34
 
37
35
  from novelwriter import CONFIG, SHARED
38
36
  from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget
37
+ from novelwriter.common import openExternalPath
39
38
  from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode
40
39
 
41
40
  if TYPE_CHECKING: # pragma: no cover
@@ -111,9 +110,7 @@ class GuiMainMenu(QMenuBar):
111
110
  def _openUserManualFile(self) -> None:
112
111
  """Open the documentation in PDF format."""
113
112
  if isinstance(CONFIG.pdfDocs, Path):
114
- QDesktopServices.openUrl(
115
- QUrl(urljoin("file:", pathname2url(str(CONFIG.pdfDocs))))
116
- )
113
+ openExternalPath(CONFIG.pdfDocs)
117
114
  return
118
115
 
119
116
  @pyqtSlot(str)
@@ -174,7 +171,7 @@ class GuiMainMenu(QMenuBar):
174
171
 
175
172
  # Project > Delete
176
173
  self.aDeleteItem = self.projMenu.addAction(self.tr("Delete Item"))
177
- self.aDeleteItem.setShortcuts(["Ctrl+Del", "Ctrl+Shift+Del"]) # Latter is deprecated
174
+ self.aDeleteItem.setShortcut("Ctrl+Shift+Del") # Cannot be Ctrl+Del, see #629
178
175
  self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.requestDeleteItem(None))
179
176
 
180
177
  # Project > Empty Trash
@@ -567,6 +564,13 @@ class GuiMainMenu(QMenuBar):
567
564
  lambda: self.requestDocInsert.emit(nwDocInsert.SYNOPSIS)
568
565
  )
569
566
 
567
+ # Insert > Short Description Comment
568
+ self.aInsShort = self.mInsComments.addAction(self.tr("Short Description Comment"))
569
+ self.aInsShort.setShortcut("Ctrl+K, U")
570
+ self.aInsShort.triggered.connect(
571
+ lambda: self.requestDocInsert.emit(nwDocInsert.SHORT)
572
+ )
573
+
570
574
  # Insert > Symbols
571
575
  self.mInsBreaks = self.insMenu.addMenu(self.tr("Page Break and Space"))
572
576
 
@@ -870,6 +874,11 @@ class GuiMainMenu(QMenuBar):
870
874
  self.aEditWordList = self.toolsMenu.addAction(self.tr("Project Word List"))
871
875
  self.aEditWordList.triggered.connect(lambda: self.mainGui.showProjectWordListDialog())
872
876
 
877
+ # Tools > Add Dictionaries
878
+ if CONFIG.osWindows or CONFIG.isDebug:
879
+ self.aAddDicts = self.toolsMenu.addAction(self.tr("Add Dictionaries"))
880
+ self.aAddDicts.triggered.connect(self.mainGui.showDictionariesDialog)
881
+
873
882
  # Tools > Separator
874
883
  self.toolsMenu.addSeparator()
875
884
 
@@ -377,13 +377,17 @@ class GuiOutlineTree(QTreeWidget):
377
377
  fH2 = self.font()
378
378
  fH2.setBold(True)
379
379
 
380
+ iType = nwItemType.FILE
381
+ iClass = nwItemClass.NO_CLASS
382
+ iLayout = nwItemLayout.DOCUMENT
383
+
380
384
  self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
381
385
  self._dIcon = {
382
- "H0": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H0"),
383
- "H1": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H1"),
384
- "H2": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H2"),
385
- "H3": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H3"),
386
- "H4": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H4"),
386
+ "H0": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H0"),
387
+ "H1": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H1"),
388
+ "H2": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H2"),
389
+ "H3": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H3"),
390
+ "H4": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H4"),
387
391
  }
388
392
 
389
393
  # Internals
@@ -549,7 +553,7 @@ class GuiOutlineTree(QTreeWidget):
549
553
  """Load the state of the main tree header, that is, column order
550
554
  and column width.
551
555
  """
552
- # Load whatever we saved last time, regardless of wether it
556
+ # Load whatever we saved last time, regardless of whether it
553
557
  # contains the correct names or number of columns.
554
558
  colState = SHARED.project.options.getValue("GuiOutline", "columnState", {})
555
559