novelWriter 2.4.4__py3-none-any.whl → 2.5__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 (122) hide show
  1. {novelWriter-2.4.4.dist-info → novelWriter-2.5.dist-info}/METADATA +4 -5
  2. {novelWriter-2.4.4.dist-info → novelWriter-2.5.dist-info}/RECORD +121 -111
  3. {novelWriter-2.4.4.dist-info → novelWriter-2.5.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +33 -39
  5. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  6. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  7. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  8. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  9. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  10. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  11. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  12. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  13. novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
  14. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  15. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  16. novelwriter/assets/i18n/project_en_GB.json +1 -0
  17. novelwriter/assets/i18n/project_pl_PL.json +116 -0
  18. novelwriter/assets/icons/typicons_dark/icons.conf +2 -0
  19. novelwriter/assets/icons/typicons_dark/nw_font.svg +4 -0
  20. novelwriter/assets/icons/typicons_dark/nw_quote.svg +4 -0
  21. novelwriter/assets/icons/typicons_light/icons.conf +2 -0
  22. novelwriter/assets/icons/typicons_light/nw_font.svg +4 -0
  23. novelwriter/assets/icons/typicons_light/nw_quote.svg +4 -0
  24. novelwriter/assets/manual.pdf +0 -0
  25. novelwriter/assets/sample.zip +0 -0
  26. novelwriter/assets/syntax/cyberpunk_night.conf +5 -3
  27. novelwriter/assets/syntax/default_dark.conf +32 -18
  28. novelwriter/assets/syntax/default_light.conf +24 -10
  29. novelwriter/assets/syntax/dracula.conf +44 -0
  30. novelwriter/assets/syntax/grey_dark.conf +5 -4
  31. novelwriter/assets/syntax/grey_light.conf +5 -4
  32. novelwriter/assets/syntax/light_owl.conf +7 -6
  33. novelwriter/assets/syntax/night_owl.conf +7 -6
  34. novelwriter/assets/syntax/snazzy.conf +42 -0
  35. novelwriter/assets/syntax/solarized_dark.conf +4 -3
  36. novelwriter/assets/syntax/solarized_light.conf +4 -3
  37. novelwriter/assets/syntax/tango.conf +27 -11
  38. novelwriter/assets/syntax/tomorrow.conf +6 -5
  39. novelwriter/assets/syntax/tomorrow_night.conf +7 -6
  40. novelwriter/assets/syntax/tomorrow_night_blue.conf +6 -5
  41. novelwriter/assets/syntax/tomorrow_night_bright.conf +6 -5
  42. novelwriter/assets/syntax/tomorrow_night_eighties.conf +6 -5
  43. novelwriter/assets/text/credits_en.htm +52 -41
  44. novelwriter/assets/themes/cyberpunk_night.conf +3 -0
  45. novelwriter/assets/themes/default_dark.conf +2 -0
  46. novelwriter/assets/themes/default_light.conf +2 -0
  47. novelwriter/assets/themes/dracula.conf +48 -0
  48. novelwriter/assets/themes/solarized_dark.conf +2 -0
  49. novelwriter/assets/themes/solarized_light.conf +2 -0
  50. novelwriter/common.py +33 -12
  51. novelwriter/config.py +184 -98
  52. novelwriter/constants.py +47 -35
  53. novelwriter/core/buildsettings.py +68 -69
  54. novelwriter/core/coretools.py +5 -23
  55. novelwriter/core/docbuild.py +52 -40
  56. novelwriter/core/document.py +3 -5
  57. novelwriter/core/index.py +115 -45
  58. novelwriter/core/item.py +8 -19
  59. novelwriter/core/options.py +2 -4
  60. novelwriter/core/project.py +37 -61
  61. novelwriter/core/projectdata.py +1 -3
  62. novelwriter/core/projectxml.py +12 -15
  63. novelwriter/core/sessions.py +3 -5
  64. novelwriter/core/spellcheck.py +4 -9
  65. novelwriter/core/status.py +211 -164
  66. novelwriter/core/storage.py +0 -8
  67. novelwriter/core/tohtml.py +139 -105
  68. novelwriter/core/tokenizer.py +278 -122
  69. novelwriter/core/{tomd.py → tomarkdown.py} +97 -78
  70. novelwriter/core/toodt.py +257 -166
  71. novelwriter/core/toqdoc.py +419 -0
  72. novelwriter/core/tree.py +5 -7
  73. novelwriter/dialogs/about.py +11 -18
  74. novelwriter/dialogs/docmerge.py +17 -19
  75. novelwriter/dialogs/docsplit.py +17 -19
  76. novelwriter/dialogs/editlabel.py +6 -10
  77. novelwriter/dialogs/preferences.py +200 -164
  78. novelwriter/dialogs/projectsettings.py +225 -189
  79. novelwriter/dialogs/quotes.py +12 -9
  80. novelwriter/dialogs/wordlist.py +9 -15
  81. novelwriter/enum.py +35 -30
  82. novelwriter/error.py +8 -15
  83. novelwriter/extensions/configlayout.py +55 -21
  84. novelwriter/extensions/eventfilters.py +1 -5
  85. novelwriter/extensions/modified.py +58 -14
  86. novelwriter/extensions/novelselector.py +1 -3
  87. novelwriter/extensions/pagedsidebar.py +9 -12
  88. novelwriter/extensions/{circularprogress.py → progressbars.py} +30 -8
  89. novelwriter/extensions/statusled.py +40 -26
  90. novelwriter/extensions/switch.py +4 -6
  91. novelwriter/extensions/switchbox.py +7 -6
  92. novelwriter/extensions/versioninfo.py +3 -9
  93. novelwriter/gui/doceditor.py +120 -139
  94. novelwriter/gui/dochighlight.py +231 -186
  95. novelwriter/gui/docviewer.py +69 -108
  96. novelwriter/gui/docviewerpanel.py +3 -10
  97. novelwriter/gui/editordocument.py +1 -3
  98. novelwriter/gui/itemdetails.py +7 -11
  99. novelwriter/gui/mainmenu.py +22 -18
  100. novelwriter/gui/noveltree.py +11 -24
  101. novelwriter/gui/outline.py +15 -26
  102. novelwriter/gui/projtree.py +35 -60
  103. novelwriter/gui/search.py +10 -3
  104. novelwriter/gui/sidebar.py +2 -6
  105. novelwriter/gui/statusbar.py +29 -37
  106. novelwriter/gui/theme.py +26 -48
  107. novelwriter/guimain.py +162 -160
  108. novelwriter/shared.py +36 -32
  109. novelwriter/text/patterns.py +113 -0
  110. novelwriter/tools/dictionaries.py +10 -20
  111. novelwriter/tools/lipsum.py +10 -16
  112. novelwriter/tools/manusbuild.py +9 -11
  113. novelwriter/tools/manuscript.py +71 -145
  114. novelwriter/tools/manussettings.py +71 -75
  115. novelwriter/tools/noveldetails.py +16 -21
  116. novelwriter/tools/welcome.py +21 -26
  117. novelwriter/tools/writingstats.py +9 -12
  118. novelwriter/types.py +49 -4
  119. novelwriter/extensions/simpleprogress.py +0 -55
  120. {novelWriter-2.4.4.dist-info → novelWriter-2.5.dist-info}/LICENSE.md +0 -0
  121. {novelWriter-2.4.4.dist-info → novelWriter-2.5.dist-info}/entry_points.txt +0 -0
  122. {novelWriter-2.4.4.dist-info → novelWriter-2.5.dist-info}/top_level.txt +0 -0
novelwriter/guimain.py CHANGED
@@ -23,18 +23,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
23
23
  """
24
24
  from __future__ import annotations
25
25
 
26
- import sys
27
26
  import logging
27
+ import sys
28
28
 
29
- from time import time
30
- from pathlib import Path
31
29
  from datetime import datetime
30
+ from pathlib import Path
31
+ from time import time
32
32
 
33
33
  from PyQt5.QtCore import Qt, QTimer, pyqtSlot
34
34
  from PyQt5.QtGui import QCloseEvent, QCursor, QIcon
35
35
  from PyQt5.QtWidgets import (
36
- QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, QShortcut, QSplitter,
37
- QStackedWidget, QVBoxLayout, QWidget
36
+ QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox,
37
+ QShortcut, QSplitter, QStackedWidget, QVBoxLayout, QWidget
38
38
  )
39
39
 
40
40
  from novelwriter import CONFIG, SHARED, __hexversion__, __version__
@@ -44,7 +44,7 @@ from novelwriter.dialogs.about import GuiAbout
44
44
  from novelwriter.dialogs.preferences import GuiPreferences
45
45
  from novelwriter.dialogs.projectsettings import GuiProjectSettings
46
46
  from novelwriter.dialogs.wordlist import GuiWordList
47
- from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemType, nwWidget, nwView
47
+ from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwFocus, nwItemType, nwView
48
48
  from novelwriter.gui.doceditor import GuiDocEditor
49
49
  from novelwriter.gui.docviewer import GuiDocViewer
50
50
  from novelwriter.gui.docviewerpanel import GuiDocViewerPanel
@@ -210,66 +210,68 @@ class GuiMain(QMainWindow):
210
210
  # Connect Signals
211
211
  # ===============
212
212
 
213
- SHARED.projectStatusChanged.connect(self.mainStatus.updateProjectStatus)
214
- SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage)
215
- SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage)
216
213
  SHARED.focusModeChanged.connect(self._focusModeChanged)
214
+ SHARED.indexAvailable.connect(self.docViewerPanel.indexHasAppeared)
215
+ SHARED.indexChangedTags.connect(self.docEditor.updateChangedTags)
217
216
  SHARED.indexChangedTags.connect(self.docViewerPanel.updateChangedTags)
217
+ SHARED.indexCleared.connect(self.docViewerPanel.indexWasCleared)
218
218
  SHARED.indexScannedText.connect(self.docViewerPanel.projectItemChanged)
219
- SHARED.indexScannedText.connect(self.projView.updateItemValues)
220
219
  SHARED.indexScannedText.connect(self.itemDetails.updateViewBox)
221
- SHARED.indexCleared.connect(self.docViewerPanel.indexWasCleared)
222
- SHARED.indexAvailable.connect(self.docViewerPanel.indexHasAppeared)
220
+ SHARED.indexScannedText.connect(self.projView.updateItemValues)
223
221
  SHARED.mainClockTick.connect(self._timeTick)
222
+ SHARED.projectStatusChanged.connect(self.mainStatus.updateProjectStatus)
223
+ SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage)
224
+ SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage)
224
225
 
225
226
  self.mainMenu.requestDocAction.connect(self._passDocumentAction)
226
227
  self.mainMenu.requestDocInsert.connect(self._passDocumentInsert)
227
228
  self.mainMenu.requestDocInsertText.connect(self._passDocumentInsert)
228
229
  self.mainMenu.requestDocKeyWordInsert.connect(self.docEditor.insertKeyWord)
229
- self.mainMenu.requestFocusChange.connect(self.switchFocus)
230
+ self.mainMenu.requestFocusChange.connect(self._switchFocus)
230
231
  self.mainMenu.requestViewChange.connect(self._changeView)
231
232
 
232
233
  self.sideBar.requestViewChange.connect(self._changeView)
233
234
 
234
- self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
235
235
  self.projView.openDocumentRequest.connect(self._openDocument)
236
- self.projView.wordCountsChanged.connect(self._updateStatusWordCount)
236
+ self.projView.projectSettingsRequest.connect(self.showProjectSettingsDialog)
237
+ self.projView.rootFolderChanged.connect(self.novelView.updateRootItem)
238
+ self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem)
239
+ self.projView.rootFolderChanged.connect(self.projView.updateRootItem)
240
+ self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
237
241
  self.projView.treeItemChanged.connect(self.docEditor.updateDocInfo)
238
242
  self.projView.treeItemChanged.connect(self.docViewer.updateDocInfo)
239
- self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox)
240
243
  self.projView.treeItemChanged.connect(self.docViewerPanel.projectItemChanged)
241
- self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem)
242
- self.projView.rootFolderChanged.connect(self.novelView.updateRootItem)
243
- self.projView.rootFolderChanged.connect(self.projView.updateRootItem)
244
- self.projView.projectSettingsRequest.connect(self.showProjectSettingsDialog)
244
+ self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox)
245
+ self.projView.wordCountsChanged.connect(self._updateStatusWordCount)
245
246
 
246
- self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
247
247
  self.novelView.openDocumentRequest.connect(self._openDocument)
248
+ self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
248
249
 
249
250
  self.projSearch.openDocumentSelectRequest.connect(self._openDocumentSelection)
250
251
  self.projSearch.selectedItemChanged.connect(self.itemDetails.updateViewBox)
251
252
 
252
- self.docEditor.editedStatusChanged.connect(self.mainStatus.updateDocumentStatus)
253
+ self.docEditor.closeDocumentRequest.connect(self.closeDocEditor)
253
254
  self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts)
254
255
  self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
256
+ self.docEditor.docTextChanged.connect(self.projSearch.textChanged)
257
+ self.docEditor.editedStatusChanged.connect(self.mainStatus.updateDocumentStatus)
255
258
  self.docEditor.loadDocumentTagRequest.connect(self._followTag)
256
- self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
257
259
  self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
258
- self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage)
260
+ self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
261
+ self.docEditor.requestNewNoteCreation.connect(self.projView.createNewNote)
262
+ self.docEditor.requestNextDocument.connect(self.openNextDocument)
263
+ self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)
264
+ self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
259
265
  self.docEditor.spellCheckStateChanged.connect(self.mainMenu.setSpellCheckState)
260
- self.docEditor.closeDocumentRequest.connect(self.closeDocEditor)
266
+ self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage)
261
267
  self.docEditor.toggleFocusModeRequest.connect(self.toggleFocusMode)
262
- self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
263
- self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)
264
- self.docEditor.requestNewNoteCreation.connect(self.projView.createNewNote)
265
- self.docEditor.docTextChanged.connect(self.projSearch.textChanged)
266
268
 
269
+ self.docViewer.closeDocumentRequest.connect(self.closeDocViewer)
267
270
  self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle)
268
271
  self.docViewer.loadDocumentTagRequest.connect(self._followTag)
269
- self.docViewer.closeDocumentRequest.connect(self.closeDocViewer)
270
272
  self.docViewer.reloadDocumentRequest.connect(self._reloadViewer)
271
- self.docViewer.togglePanelVisibility.connect(self._toggleViewerPanelVisibility)
272
273
  self.docViewer.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
274
+ self.docViewer.togglePanelVisibility.connect(self._toggleViewerPanelVisibility)
273
275
 
274
276
  self.docViewerPanel.loadDocumentTagRequest.connect(self._followTag)
275
277
  self.docViewerPanel.openDocumentRequest.connect(self._openDocument)
@@ -303,9 +305,6 @@ class GuiMain(QMainWindow):
303
305
  self.keyEscape.setKey(Qt.Key.Key_Escape)
304
306
  self.keyEscape.activated.connect(self._keyPressEscape)
305
307
 
306
- # Check that config loaded fine
307
- self.reportConfErr()
308
-
309
308
  # Initialise Main GUI
310
309
  self.initMain()
311
310
  self.asProjTimer.start()
@@ -327,6 +326,15 @@ class GuiMain(QMainWindow):
327
326
 
328
327
  def postLaunchTasks(self, cmdOpen: str | None) -> None:
329
328
  """Process tasks after the main window has been created."""
329
+ QApplication.processEvents()
330
+ app = QApplication.instance()
331
+ if isinstance(app, QApplication):
332
+ app.focusChanged.connect(self._appFocusChanged)
333
+
334
+ # Check that config loaded fine
335
+ if CONFIG.hasError:
336
+ SHARED.error(CONFIG.errorText())
337
+
330
338
  if cmdOpen:
331
339
  QApplication.processEvents()
332
340
  logger.info("Command line path: %s", cmdOpen)
@@ -369,9 +377,7 @@ class GuiMain(QMainWindow):
369
377
  if not msgYes:
370
378
  return False
371
379
 
372
- if self.docEditor.docChanged:
373
- self.saveDocument()
374
-
380
+ self.saveDocument()
375
381
  saveOK = self.saveProject()
376
382
  doBackup = False
377
383
  if SHARED.project.data.doBackup and CONFIG.backupOnClose:
@@ -514,9 +520,7 @@ class GuiMain(QMainWindow):
514
520
  # Disable focus mode if it is active
515
521
  if SHARED.focusMode:
516
522
  SHARED.setFocusMode(False)
517
- self.docEditor.saveCursorPosition()
518
- if self.docEditor.docChanged:
519
- self.saveDocument()
523
+ self.saveDocument()
520
524
  self.docEditor.clearEditor()
521
525
  if not beforeOpen:
522
526
  self.novelView.setActiveHandle(None)
@@ -553,42 +557,43 @@ class GuiMain(QMainWindow):
553
557
 
554
558
  return True
555
559
 
556
- def openNextDocument(self, tHandle: str, wrapAround: bool = False) -> bool:
560
+ @pyqtSlot(str, bool)
561
+ def openNextDocument(self, tHandle: str, wrapAround: bool) -> None:
557
562
  """Open the next document in the project tree, following the
558
563
  document with the given handle. Stop when reaching the end.
559
564
  """
560
- if not SHARED.hasProject:
561
- logger.error("No project open")
562
- return False
563
-
564
- nHandle = None # The next handle after tHandle
565
- fHandle = None # The first file handle we encounter
566
- foundIt = False # We've found tHandle, pick the next we see
567
- for tItem in SHARED.project.tree:
568
- if not tItem.isFileType():
569
- continue
570
- if fHandle is None:
571
- fHandle = tItem.itemHandle
572
- if tItem.itemHandle == tHandle:
573
- foundIt = True
574
- elif foundIt:
575
- nHandle = tItem.itemHandle
576
- break
577
-
578
- if nHandle is not None:
579
- self.openDocument(nHandle, tLine=1, doScroll=True)
580
- return True
581
- elif wrapAround:
582
- self.openDocument(fHandle, tLine=1, doScroll=True)
583
- return False
584
-
585
- return False
565
+ if SHARED.hasProject:
566
+ nHandle = None # The next handle after tHandle
567
+ fHandle = None # The first file handle we encounter
568
+ foundIt = False # We've found tHandle, pick the next we see
569
+ for tItem in SHARED.project.tree:
570
+ if not tItem.isFileType():
571
+ continue
572
+ if fHandle is None:
573
+ fHandle = tItem.itemHandle
574
+ if tItem.itemHandle == tHandle:
575
+ foundIt = True
576
+ elif foundIt:
577
+ nHandle = tItem.itemHandle
578
+ break
579
+ if nHandle is not None:
580
+ self.openDocument(nHandle, tLine=1, doScroll=True)
581
+ elif wrapAround:
582
+ self.openDocument(fHandle, tLine=1, doScroll=True)
583
+ return
586
584
 
587
- @pyqtSlot()
588
- def saveDocument(self) -> None:
585
+ def saveDocument(self, force: bool = False) -> None:
589
586
  """Save the current documents."""
590
587
  if SHARED.hasProject:
591
- self.docEditor.saveText()
588
+ self.docEditor.saveCursorPosition()
589
+ if force or self.docEditor.docChanged:
590
+ self.docEditor.saveText()
591
+ return
592
+
593
+ @pyqtSlot()
594
+ def forceSaveDocument(self) -> None:
595
+ """Save document even of it has not changed."""
596
+ self.saveDocument(force=True)
592
597
  return
593
598
 
594
599
  def viewDocument(self, tHandle: str | None = None, sTitle: str | None = None) -> bool:
@@ -618,6 +623,10 @@ class GuiMain(QMainWindow):
618
623
  # Make sure main tab is in Editor view
619
624
  self._changeView(nwView.EDITOR)
620
625
 
626
+ # If we're loading the document in the editor, it may need to be saved
627
+ if tHandle == self.docEditor.docHandle and self.docEditor.docChanged:
628
+ self.saveDocument()
629
+
621
630
  logger.debug("Viewing document with handle '%s'", tHandle)
622
631
  updateHistory = tHandle != self.docViewer.docHandle
623
632
  if self.docViewer.loadText(tHandle, updateHistory=updateHistory):
@@ -649,7 +658,7 @@ class GuiMain(QMainWindow):
649
658
  logger.error("No project open")
650
659
  return False
651
660
 
652
- lastPath = CONFIG.lastPath()
661
+ lastPath = CONFIG.lastPath("import")
653
662
  ffilter = formatFileFilter(["*.txt", "*.md", "*.nwd", "*"])
654
663
  loadFile, _ = QFileDialog.getOpenFileName(
655
664
  self, self.tr("Import File"), str(lastPath), filter=ffilter
@@ -664,7 +673,7 @@ class GuiMain(QMainWindow):
664
673
  try:
665
674
  with open(loadFile, mode="rt", encoding="utf-8") as inFile:
666
675
  text = inFile.read()
667
- CONFIG.setLastPath(loadFile)
676
+ CONFIG.setLastPath("import", loadFile)
668
677
  except Exception as exc:
669
678
  SHARED.error(self.tr(
670
679
  "Could not read file. The file must be an existing text file."
@@ -721,14 +730,6 @@ class GuiMain(QMainWindow):
721
730
 
722
731
  return
723
732
 
724
- def editItemLabel(self, tHandle: str | None = None) -> None:
725
- """Open the edit item dialog."""
726
- if SHARED.hasProject:
727
- if tHandle is None and (self.docEditor.anyFocus() or SHARED.focusMode):
728
- tHandle = self.docEditor.docHandle
729
- self.projView.renameTreeItem(tHandle)
730
- return
731
-
732
733
  def rebuildTrees(self) -> None:
733
734
  """Rebuild the project tree."""
734
735
  self.projView.populateTree()
@@ -750,7 +751,6 @@ class GuiMain(QMainWindow):
750
751
  self.mainStatus.setStatusMessage(
751
752
  self.tr("Indexing completed in {0} ms").format(f"{(tEnd - tStart)*1000.0:.1f}")
752
753
  )
753
- self.docEditor.updateTagHighLighting()
754
754
  self._updateStatusWordCount()
755
755
  QApplication.restoreOverrideCursor()
756
756
 
@@ -794,10 +794,7 @@ class GuiMain(QMainWindow):
794
794
  """Open the novel details dialog."""
795
795
  if SHARED.hasProject:
796
796
  dialog = GuiNovelDetails(self)
797
- dialog.setModal(True)
798
- dialog.show()
799
- dialog.raise_()
800
- QApplication.processEvents()
797
+ dialog.activateDialog()
801
798
  dialog.updateValues()
802
799
  return
803
800
 
@@ -805,12 +802,9 @@ class GuiMain(QMainWindow):
805
802
  def showBuildManuscriptDialog(self) -> None:
806
803
  """Open the build manuscript dialog."""
807
804
  if SHARED.hasProject:
808
- if (dialog := SHARED.findTopLevelWidget(GuiManuscript)) is None:
805
+ if not (dialog := SHARED.findTopLevelWidget(GuiManuscript)):
809
806
  dialog = GuiManuscript(self)
810
- dialog.setModal(False)
811
- dialog.show()
812
- dialog.raise_()
813
- QApplication.processEvents()
807
+ dialog.activateDialog()
814
808
  dialog.loadContent()
815
809
  return
816
810
 
@@ -827,12 +821,9 @@ class GuiMain(QMainWindow):
827
821
  def showWritingStatsDialog(self) -> None:
828
822
  """Open the session stats dialog."""
829
823
  if SHARED.hasProject:
830
- if (dialog := SHARED.findTopLevelWidget(GuiWritingStats)) is None:
824
+ if not (dialog := SHARED.findTopLevelWidget(GuiWritingStats)):
831
825
  dialog = GuiWritingStats(self)
832
- dialog.setModal(False)
833
- dialog.show()
834
- dialog.raise_()
835
- QApplication.processEvents()
826
+ dialog.activateDialog()
836
827
  dialog.populateGUI()
837
828
  return
838
829
 
@@ -840,11 +831,7 @@ class GuiMain(QMainWindow):
840
831
  def showAboutNWDialog(self) -> None:
841
832
  """Show the novelWriter about dialog."""
842
833
  dialog = GuiAbout(self)
843
- dialog.setModal(True)
844
- dialog.show()
845
- dialog.raise_()
846
- QApplication.processEvents()
847
- dialog.populateGUI()
834
+ dialog.exec()
848
835
  return
849
836
 
850
837
  @pyqtSlot()
@@ -858,24 +845,12 @@ class GuiMain(QMainWindow):
858
845
  def showDictionariesDialog(self) -> None:
859
846
  """Show the download dictionaries dialog."""
860
847
  dialog = GuiDictionaries(self)
861
- dialog.setModal(True)
862
- dialog.show()
863
- dialog.raise_()
864
- QApplication.processEvents()
848
+ dialog.activateDialog()
865
849
  if not dialog.initDialog():
866
850
  dialog.close()
867
851
  SHARED.error(self.tr("Could not initialise the dialog."))
868
852
  return
869
853
 
870
- def reportConfErr(self) -> None:
871
- """Checks if the Config module has any errors to report, and let
872
- the user know if this is the case. The Config module caches
873
- errors since it is initialised before the GUI itself.
874
- """
875
- if CONFIG.hasError:
876
- SHARED.error(CONFIG.errorText())
877
- return
878
-
879
854
  ##
880
855
  # Main Window Actions
881
856
  ##
@@ -906,9 +881,7 @@ class GuiMain(QMainWindow):
906
881
 
907
882
  if SHARED.hasProject:
908
883
  self.closeProject(True)
909
-
910
884
  CONFIG.saveConfig()
911
- self.reportConfErr()
912
885
 
913
886
  QApplication.quit()
914
887
 
@@ -976,14 +949,37 @@ class GuiMain(QMainWindow):
976
949
  SHARED.setFocusMode(not SHARED.focusMode)
977
950
  return
978
951
 
952
+ ##
953
+ # Private Slots
954
+ ##
955
+
956
+ @pyqtSlot("QWidget*", "QWidget*")
957
+ def _appFocusChanged(self, old: QWidget, new: QWidget) -> None:
958
+ """Alert main widgets that they have received or lost focus."""
959
+ if isinstance(new, QWidget):
960
+ docEditor = False
961
+ docViewer = False
962
+ if self.docEditor.isAncestorOf(new):
963
+ docEditor = True
964
+ elif self.docViewer.isAncestorOf(new):
965
+ docViewer = True
966
+
967
+ self.docEditor.changeFocusState(docEditor)
968
+ self.docViewer.changeFocusState(docViewer)
969
+
970
+ logger.debug("Main focus switched to: %s", type(new).__name__)
971
+
972
+ return
973
+
979
974
  @pyqtSlot(bool)
980
975
  def _focusModeChanged(self, focusMode: bool) -> None:
981
- """Handle change of focus mode. The Main GUI Focus Mode hides tree,
982
- view, statusbar and menu.
976
+ """Handle change of focus mode. The Main GUI Focus Mode hides
977
+ tree, view, statusbar and menu.
983
978
  """
984
979
  if focusMode:
985
980
  logger.debug("Activating Focus Mode")
986
- self.switchFocus(nwWidget.EDITOR)
981
+ self._changeView(nwView.EDITOR)
982
+ self.docEditor.setFocus()
987
983
  else:
988
984
  logger.debug("Deactivating Focus Mode")
989
985
 
@@ -1006,39 +1002,53 @@ class GuiMain(QMainWindow):
1006
1002
  self.docEditor.ensureCursorVisibleNoCentre()
1007
1003
  return
1008
1004
 
1009
- @pyqtSlot(nwWidget)
1010
- def switchFocus(self, paneNo: nwWidget) -> None:
1005
+ @pyqtSlot(nwFocus)
1006
+ def _switchFocus(self, paneNo: nwFocus) -> None:
1011
1007
  """Switch focus between main GUI views."""
1012
- if paneNo == nwWidget.TREE:
1013
- if self.projStack.currentWidget() is self.projView:
1014
- if self.projView.treeHasFocus():
1015
- self._changeView(nwView.NOVEL)
1016
- self.novelView.setTreeFocus()
1017
- else:
1018
- self.projView.setTreeFocus()
1019
- elif self.projStack.currentWidget() is self.novelView:
1020
- if self.novelView.treeHasFocus():
1021
- self._changeView(nwView.PROJECT)
1022
- self.projView.setTreeFocus()
1023
- else:
1024
- self.novelView.setTreeFocus()
1008
+ if paneNo == nwFocus.TREE:
1009
+ # Decision Matrix
1010
+ # vM | vP | fP | vN | fN | Focus
1011
+ # ----|----|----|----|----|---------
1012
+ # T | T | T | F | F | Novel
1013
+ # T | T | F | F | F | Project
1014
+ # T | F | F | T | T | Project
1015
+ # T | F | F | T | F | Novel
1016
+ # T | F | F | F | F | Project
1017
+ # F | T | T | F | F | Project
1018
+ # F | T | F | F | F | Project
1019
+ # F | F | F | T | T | Novel
1020
+ # F | F | F | T | F | Novel
1021
+ # F | F | F | F | F | Project
1022
+
1023
+ vM = self.mainStack.currentWidget() is self.splitMain
1024
+ vP = self.projStack.currentWidget() is self.projView
1025
+ vN = self.projStack.currentWidget() is self.novelView
1026
+ fP = self.projView.treeHasFocus()
1027
+ fN = self.novelView.treeHasFocus()
1028
+
1029
+ self._changeView(nwView.EDITOR)
1030
+ if (vM and (vP and fP or vN and not fN)) or (not vM and vN):
1031
+ self._changeView(nwView.NOVEL)
1032
+ self.novelView.setTreeFocus()
1025
1033
  else:
1026
1034
  self._changeView(nwView.PROJECT)
1027
1035
  self.projView.setTreeFocus()
1028
- elif paneNo == nwWidget.EDITOR:
1029
- self._changeView(nwView.EDITOR)
1030
- self.docEditor.setFocus()
1031
- elif paneNo == nwWidget.VIEWER:
1036
+
1037
+ elif paneNo == nwFocus.DOCUMENT:
1032
1038
  self._changeView(nwView.EDITOR)
1033
- self.docViewer.setFocus()
1034
- elif paneNo == nwWidget.OUTLINE:
1039
+ hasViewer = self.splitView.isVisible()
1040
+ if hasViewer and self.docEditor.anyFocus():
1041
+ self.docViewer.setFocus()
1042
+ elif hasViewer and self.docViewer.anyFocus():
1043
+ self.docEditor.setFocus()
1044
+ else:
1045
+ self.docEditor.setFocus()
1046
+
1047
+ elif paneNo == nwFocus.OUTLINE:
1035
1048
  self._changeView(nwView.OUTLINE)
1036
1049
  self.outlineView.setTreeFocus()
1037
- return
1038
1050
 
1039
- ##
1040
- # Private Slots
1041
- ##
1051
+ return
1042
1052
 
1043
1053
  @pyqtSlot(bool, bool, bool, bool)
1044
1054
  def _processConfigChanges(self, restart: bool, tree: bool, theme: bool, syntax: bool) -> None:
@@ -1148,7 +1158,7 @@ class GuiMain(QMainWindow):
1148
1158
  @pyqtSlot()
1149
1159
  def _reloadViewer(self) -> None:
1150
1160
  """Reload the document in the viewer."""
1151
- if self.docEditor.docChanged and self.docEditor.docHandle == self.docViewer.docHandle:
1161
+ if self.docEditor.docHandle == self.docViewer.docHandle:
1152
1162
  # If the two panels have the same document, save any changes in the editor
1153
1163
  self.saveDocument()
1154
1164
  self.docViewer.reloadText()
@@ -1178,16 +1188,13 @@ class GuiMain(QMainWindow):
1178
1188
 
1179
1189
  @pyqtSlot(nwDocAction)
1180
1190
  def _passDocumentAction(self, action: nwDocAction) -> None:
1181
- """Pass on a document action to the document viewer if it has
1182
- focus, or pass it to the document editor if it or any of its
1183
- child widgets have focus. If neither has focus, ignore it.
1191
+ """Pass on a document action to the editor or viewer based on
1192
+ which one has focus, or if neither has focus, ignore it.
1184
1193
  """
1185
- if self.docViewer.hasFocus():
1186
- self.docViewer.docAction(action)
1187
- elif self.docEditor.hasFocus():
1194
+ if self.docEditor.hasFocus():
1188
1195
  self.docEditor.docAction(action)
1189
- else:
1190
- logger.debug("Action cancelled as neither editor nor viewer has focus")
1196
+ elif self.docViewer.hasFocus():
1197
+ self.docViewer.docAction(action)
1191
1198
  return
1192
1199
 
1193
1200
  @pyqtSlot(str)
@@ -1228,7 +1235,7 @@ class GuiMain(QMainWindow):
1228
1235
  doSave &= SHARED.project.projChanged
1229
1236
  doSave &= SHARED.project.storage.isOpen()
1230
1237
  if doSave:
1231
- logger.debug("Autosaving project")
1238
+ logger.debug("Auto-saving project")
1232
1239
  self.saveProject(autoSave=True)
1233
1240
  return
1234
1241
 
@@ -1236,7 +1243,7 @@ class GuiMain(QMainWindow):
1236
1243
  def _autoSaveDocument(self) -> None:
1237
1244
  """Autosave of the document. This is a timer-activated slot."""
1238
1245
  if SHARED.hasProject and self.docEditor.docChanged:
1239
- logger.debug("Autosaving document")
1246
+ logger.debug("Auto-saving document")
1240
1247
  self.saveDocument()
1241
1248
  return
1242
1249
 
@@ -1395,10 +1402,7 @@ class GuiMain(QMainWindow):
1395
1402
 
1396
1403
  def _updateWindowTitle(self, projName: str | None = None) -> None:
1397
1404
  """Set the window title and add the project's name."""
1398
- winTitle = CONFIG.appName
1399
- if projName is not None:
1400
- winTitle += " - %s" % projName
1401
- self.setWindowTitle(winTitle)
1405
+ self.setWindowTitle(" - ".join(filter(None, [projName, CONFIG.appName])))
1402
1406
  return
1403
1407
 
1404
1408
  def _getTagSource(self, tag: str) -> tuple[str | None, str | None]:
@@ -1416,5 +1420,3 @@ class GuiMain(QMainWindow):
1416
1420
  ))
1417
1421
  return None, None
1418
1422
  return tHandle, sTitle
1419
-
1420
- # END Class GuiMain