novelWriter 2.4.4__py3-none-any.whl → 2.5rc1__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 (110) hide show
  1. {novelWriter-2.4.4.dist-info → novelWriter-2.5rc1.dist-info}/METADATA +4 -5
  2. {novelWriter-2.4.4.dist-info → novelWriter-2.5rc1.dist-info}/RECORD +109 -101
  3. {novelWriter-2.4.4.dist-info → novelWriter-2.5rc1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +33 -39
  5. novelwriter/assets/i18n/project_en_GB.json +1 -0
  6. novelwriter/assets/icons/typicons_dark/icons.conf +2 -0
  7. novelwriter/assets/icons/typicons_dark/nw_font.svg +4 -0
  8. novelwriter/assets/icons/typicons_dark/nw_quote.svg +4 -0
  9. novelwriter/assets/icons/typicons_light/icons.conf +2 -0
  10. novelwriter/assets/icons/typicons_light/nw_font.svg +4 -0
  11. novelwriter/assets/icons/typicons_light/nw_quote.svg +4 -0
  12. novelwriter/assets/manual.pdf +0 -0
  13. novelwriter/assets/sample.zip +0 -0
  14. novelwriter/assets/syntax/cyberpunk_night.conf +5 -3
  15. novelwriter/assets/syntax/default_dark.conf +32 -18
  16. novelwriter/assets/syntax/default_light.conf +24 -10
  17. novelwriter/assets/syntax/dracula.conf +44 -0
  18. novelwriter/assets/syntax/grey_dark.conf +5 -4
  19. novelwriter/assets/syntax/grey_light.conf +5 -4
  20. novelwriter/assets/syntax/light_owl.conf +7 -6
  21. novelwriter/assets/syntax/night_owl.conf +7 -6
  22. novelwriter/assets/syntax/snazzy.conf +42 -0
  23. novelwriter/assets/syntax/solarized_dark.conf +4 -3
  24. novelwriter/assets/syntax/solarized_light.conf +4 -3
  25. novelwriter/assets/syntax/tango.conf +27 -11
  26. novelwriter/assets/syntax/tomorrow.conf +6 -5
  27. novelwriter/assets/syntax/tomorrow_night.conf +7 -6
  28. novelwriter/assets/syntax/tomorrow_night_blue.conf +6 -5
  29. novelwriter/assets/syntax/tomorrow_night_bright.conf +6 -5
  30. novelwriter/assets/syntax/tomorrow_night_eighties.conf +6 -5
  31. novelwriter/assets/text/credits_en.htm +4 -1
  32. novelwriter/assets/themes/cyberpunk_night.conf +3 -0
  33. novelwriter/assets/themes/default_dark.conf +2 -0
  34. novelwriter/assets/themes/default_light.conf +2 -0
  35. novelwriter/assets/themes/dracula.conf +48 -0
  36. novelwriter/assets/themes/solarized_dark.conf +2 -0
  37. novelwriter/assets/themes/solarized_light.conf +2 -0
  38. novelwriter/common.py +33 -12
  39. novelwriter/config.py +184 -98
  40. novelwriter/constants.py +47 -35
  41. novelwriter/core/buildsettings.py +68 -69
  42. novelwriter/core/coretools.py +5 -23
  43. novelwriter/core/docbuild.py +52 -40
  44. novelwriter/core/document.py +3 -5
  45. novelwriter/core/index.py +115 -45
  46. novelwriter/core/item.py +8 -19
  47. novelwriter/core/options.py +2 -4
  48. novelwriter/core/project.py +23 -57
  49. novelwriter/core/projectdata.py +1 -3
  50. novelwriter/core/projectxml.py +12 -15
  51. novelwriter/core/sessions.py +3 -5
  52. novelwriter/core/spellcheck.py +4 -9
  53. novelwriter/core/status.py +211 -164
  54. novelwriter/core/storage.py +0 -8
  55. novelwriter/core/tohtml.py +139 -105
  56. novelwriter/core/tokenizer.py +278 -122
  57. novelwriter/core/{tomd.py → tomarkdown.py} +97 -78
  58. novelwriter/core/toodt.py +257 -166
  59. novelwriter/core/toqdoc.py +419 -0
  60. novelwriter/core/tree.py +5 -7
  61. novelwriter/dialogs/about.py +11 -18
  62. novelwriter/dialogs/docmerge.py +17 -19
  63. novelwriter/dialogs/docsplit.py +17 -19
  64. novelwriter/dialogs/editlabel.py +6 -10
  65. novelwriter/dialogs/preferences.py +193 -144
  66. novelwriter/dialogs/projectsettings.py +225 -189
  67. novelwriter/dialogs/quotes.py +12 -9
  68. novelwriter/dialogs/wordlist.py +9 -15
  69. novelwriter/enum.py +35 -30
  70. novelwriter/error.py +8 -15
  71. novelwriter/extensions/configlayout.py +40 -21
  72. novelwriter/extensions/eventfilters.py +1 -5
  73. novelwriter/extensions/modified.py +58 -14
  74. novelwriter/extensions/novelselector.py +1 -3
  75. novelwriter/extensions/pagedsidebar.py +9 -12
  76. novelwriter/extensions/{circularprogress.py → progressbars.py} +30 -8
  77. novelwriter/extensions/statusled.py +29 -25
  78. novelwriter/extensions/switch.py +4 -6
  79. novelwriter/extensions/switchbox.py +7 -6
  80. novelwriter/extensions/versioninfo.py +3 -9
  81. novelwriter/gui/doceditor.py +118 -137
  82. novelwriter/gui/dochighlight.py +231 -186
  83. novelwriter/gui/docviewer.py +66 -107
  84. novelwriter/gui/docviewerpanel.py +3 -10
  85. novelwriter/gui/editordocument.py +1 -3
  86. novelwriter/gui/itemdetails.py +7 -11
  87. novelwriter/gui/mainmenu.py +22 -18
  88. novelwriter/gui/noveltree.py +11 -24
  89. novelwriter/gui/outline.py +14 -26
  90. novelwriter/gui/projtree.py +35 -60
  91. novelwriter/gui/search.py +10 -3
  92. novelwriter/gui/sidebar.py +2 -6
  93. novelwriter/gui/statusbar.py +29 -37
  94. novelwriter/gui/theme.py +26 -48
  95. novelwriter/guimain.py +134 -148
  96. novelwriter/shared.py +36 -32
  97. novelwriter/text/patterns.py +113 -0
  98. novelwriter/tools/dictionaries.py +10 -20
  99. novelwriter/tools/lipsum.py +10 -16
  100. novelwriter/tools/manusbuild.py +9 -11
  101. novelwriter/tools/manuscript.py +71 -145
  102. novelwriter/tools/manussettings.py +71 -75
  103. novelwriter/tools/noveldetails.py +16 -21
  104. novelwriter/tools/welcome.py +12 -26
  105. novelwriter/tools/writingstats.py +9 -12
  106. novelwriter/types.py +49 -4
  107. novelwriter/extensions/simpleprogress.py +0 -55
  108. {novelWriter-2.4.4.dist-info → novelWriter-2.5rc1.dist-info}/LICENSE.md +0 -0
  109. {novelWriter-2.4.4.dist-info → novelWriter-2.5rc1.dist-info}/entry_points.txt +0 -0
  110. {novelWriter-2.4.4.dist-info → novelWriter-2.5rc1.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,10 +1002,10 @@ 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:
1008
+ if paneNo == nwFocus.TREE:
1013
1009
  if self.projStack.currentWidget() is self.projView:
1014
1010
  if self.projView.treeHasFocus():
1015
1011
  self._changeView(nwView.NOVEL)
@@ -1025,21 +1021,19 @@ class GuiMain(QMainWindow):
1025
1021
  else:
1026
1022
  self._changeView(nwView.PROJECT)
1027
1023
  self.projView.setTreeFocus()
1028
- elif paneNo == nwWidget.EDITOR:
1029
- self._changeView(nwView.EDITOR)
1030
- self.docEditor.setFocus()
1031
- elif paneNo == nwWidget.VIEWER:
1024
+ elif paneNo == nwFocus.DOCUMENT:
1032
1025
  self._changeView(nwView.EDITOR)
1033
- self.docViewer.setFocus()
1034
- elif paneNo == nwWidget.OUTLINE:
1026
+ if self.docEditor.anyFocus():
1027
+ self.docViewer.setFocus()
1028
+ elif self.docViewer.anyFocus():
1029
+ self.docEditor.setFocus()
1030
+ else:
1031
+ self.docEditor.setFocus()
1032
+ elif paneNo == nwFocus.OUTLINE:
1035
1033
  self._changeView(nwView.OUTLINE)
1036
1034
  self.outlineView.setTreeFocus()
1037
1035
  return
1038
1036
 
1039
- ##
1040
- # Private Slots
1041
- ##
1042
-
1043
1037
  @pyqtSlot(bool, bool, bool, bool)
1044
1038
  def _processConfigChanges(self, restart: bool, tree: bool, theme: bool, syntax: bool) -> None:
1045
1039
  """Refresh GUI based on flags from the Preferences dialog."""
@@ -1148,7 +1142,7 @@ class GuiMain(QMainWindow):
1148
1142
  @pyqtSlot()
1149
1143
  def _reloadViewer(self) -> None:
1150
1144
  """Reload the document in the viewer."""
1151
- if self.docEditor.docChanged and self.docEditor.docHandle == self.docViewer.docHandle:
1145
+ if self.docEditor.docHandle == self.docViewer.docHandle:
1152
1146
  # If the two panels have the same document, save any changes in the editor
1153
1147
  self.saveDocument()
1154
1148
  self.docViewer.reloadText()
@@ -1178,16 +1172,13 @@ class GuiMain(QMainWindow):
1178
1172
 
1179
1173
  @pyqtSlot(nwDocAction)
1180
1174
  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.
1175
+ """Pass on a document action to the editor or viewer based on
1176
+ which one has focus, or if neither has focus, ignore it.
1184
1177
  """
1185
- if self.docViewer.hasFocus():
1186
- self.docViewer.docAction(action)
1187
- elif self.docEditor.hasFocus():
1178
+ if self.docEditor.hasFocus():
1188
1179
  self.docEditor.docAction(action)
1189
- else:
1190
- logger.debug("Action cancelled as neither editor nor viewer has focus")
1180
+ elif self.docViewer.hasFocus():
1181
+ self.docViewer.docAction(action)
1191
1182
  return
1192
1183
 
1193
1184
  @pyqtSlot(str)
@@ -1228,7 +1219,7 @@ class GuiMain(QMainWindow):
1228
1219
  doSave &= SHARED.project.projChanged
1229
1220
  doSave &= SHARED.project.storage.isOpen()
1230
1221
  if doSave:
1231
- logger.debug("Autosaving project")
1222
+ logger.debug("Auto-saving project")
1232
1223
  self.saveProject(autoSave=True)
1233
1224
  return
1234
1225
 
@@ -1236,7 +1227,7 @@ class GuiMain(QMainWindow):
1236
1227
  def _autoSaveDocument(self) -> None:
1237
1228
  """Autosave of the document. This is a timer-activated slot."""
1238
1229
  if SHARED.hasProject and self.docEditor.docChanged:
1239
- logger.debug("Autosaving document")
1230
+ logger.debug("Auto-saving document")
1240
1231
  self.saveDocument()
1241
1232
  return
1242
1233
 
@@ -1395,10 +1386,7 @@ class GuiMain(QMainWindow):
1395
1386
 
1396
1387
  def _updateWindowTitle(self, projName: str | None = None) -> None:
1397
1388
  """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)
1389
+ self.setWindowTitle(" - ".join(filter(None, [projName, CONFIG.appName])))
1402
1390
  return
1403
1391
 
1404
1392
  def _getTagSource(self, tag: str) -> tuple[str | None, str | None]:
@@ -1416,5 +1404,3 @@ class GuiMain(QMainWindow):
1416
1404
  ))
1417
1405
  return None, None
1418
1406
  return tHandle, sTitle
1419
-
1420
- # END Class GuiMain
novelwriter/shared.py CHANGED
@@ -31,7 +31,8 @@ from time import time
31
31
  from typing import TYPE_CHECKING, TypeVar
32
32
 
33
33
  from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, pyqtSignal
34
- from PyQt5.QtWidgets import QFileDialog, QMessageBox, QWidget
34
+ from PyQt5.QtGui import QFont
35
+ from PyQt5.QtWidgets import QFileDialog, QFontDialog, QMessageBox, QWidget
35
36
 
36
37
  from novelwriter.common import formatFileFilter
37
38
  from novelwriter.constants import nwFiles
@@ -171,6 +172,23 @@ class SharedData(QObject):
171
172
  logger.debug("Thread Pool Max Count: %d", QThreadPool.globalInstance().maxThreadCount())
172
173
  return
173
174
 
175
+ def closeEditor(self, tHandle: str | None = None) -> None:
176
+ """Close the document editor, optionally a specific document."""
177
+ if tHandle is None or tHandle == self.mainGui.docEditor.docHandle:
178
+ self.mainGui.closeDocument()
179
+ return
180
+
181
+ def saveEditor(self, tHandle: str | None = None) -> None:
182
+ """Save the editor content, optionally a specific document."""
183
+ docEditor = self.mainGui.docEditor
184
+ if (
185
+ self.hasProject and docEditor.docHandle
186
+ and (tHandle is None or tHandle == docEditor.docHandle)
187
+ ):
188
+ logger.debug("Saving editor document before action")
189
+ docEditor.saveText()
190
+ return
191
+
174
192
  def openProject(self, path: str | Path, clearLock: bool = False) -> bool:
175
193
  """Open a project."""
176
194
  if self.project.isValid:
@@ -198,25 +216,12 @@ class SharedData(QObject):
198
216
 
199
217
  def closeProject(self) -> None:
200
218
  """Close the current project."""
201
- self._closeDialogs()
219
+ self._closeToolDialogs()
202
220
  self.project.closeProject(self._idleTime)
203
221
  self._resetProject()
204
222
  self._resetIdleTimer()
205
223
  return
206
224
 
207
- def ensureEditorSaved(self, tHandle: str | None) -> None:
208
- """Ensure that the editor content is saved. Optionally, only if
209
- it is a specific handle.
210
- """
211
- docEditor = self.mainGui.docEditor
212
- if (
213
- self.hasProject and docEditor.docHandle
214
- and (tHandle is None or tHandle == docEditor.docHandle)
215
- ):
216
- logger.debug("Saving editor document before action")
217
- docEditor.saveText()
218
- return
219
-
220
225
  def updateSpellCheckLanguage(self, reload: bool = False) -> None:
221
226
  """Update the active spell check language from settings."""
222
227
  from novelwriter import CONFIG
@@ -257,8 +262,11 @@ class SharedData(QObject):
257
262
  QThreadPool.globalInstance().start(runnable, priority=priority)
258
263
  return
259
264
 
260
- def getProjectPath(self, parent: QWidget, path: str | Path | None = None,
261
- allowZip: bool = False) -> Path | None:
265
+ def getProjectPath(
266
+ self, parent: QWidget,
267
+ path: str | Path | None = None,
268
+ allowZip: bool = False
269
+ ) -> Path | None:
262
270
  """Open the file dialog and select a novelWriter project file."""
263
271
  label = (self.tr("novelWriter Project File or Zip File")
264
272
  if allowZip else self.tr("novelWriter Project File"))
@@ -269,6 +277,13 @@ class SharedData(QObject):
269
277
  )
270
278
  return Path(selected) if selected else None
271
279
 
280
+ def getFont(self, current: QFont, native: bool) -> tuple[QFont, bool]:
281
+ """Open the font dialog and select a font."""
282
+ kwargs = {}
283
+ if not native:
284
+ kwargs["options"] = QFontDialog.FontDialogOption.DontUseNativeDialog
285
+ return QFontDialog.getFont(current, self.mainGui, self.tr("Select Font"), **kwargs)
286
+
272
287
  def findTopLevelWidget(self, kind: type[NWWidget]) -> NWWidget | None:
273
288
  """Find a top level widget."""
274
289
  for widget in self.mainGui.children():
@@ -307,7 +322,6 @@ class SharedData(QObject):
307
322
  if log:
308
323
  logger.info(self._lastAlert, stacklevel=2)
309
324
  alert.exec()
310
- alert.deleteLater()
311
325
  return
312
326
 
313
327
  def warn(self, text: str, info: str = "", details: str = "", log: bool = True) -> None:
@@ -319,7 +333,6 @@ class SharedData(QObject):
319
333
  if log:
320
334
  logger.warning(self._lastAlert, stacklevel=2)
321
335
  alert.exec()
322
- alert.deleteLater()
323
336
  return
324
337
 
325
338
  def error(self, text: str, info: str = "", details: str = "", log: bool = True,
@@ -334,7 +347,6 @@ class SharedData(QObject):
334
347
  if log:
335
348
  logger.error(self._lastAlert, stacklevel=2)
336
349
  alert.exec()
337
- alert.deleteLater()
338
350
  return
339
351
 
340
352
  def question(self, text: str, info: str = "", details: str = "", warn: bool = False) -> bool:
@@ -345,7 +357,6 @@ class SharedData(QObject):
345
357
  self._lastAlert = alert.logMessage
346
358
  alert.exec()
347
359
  isYes = alert.result() == QMessageBox.StandardButton.Yes
348
- alert.deleteLater()
349
360
  return isYes
350
361
 
351
362
  ##
@@ -370,19 +381,14 @@ class SharedData(QObject):
370
381
  self._idleTime = 0.0
371
382
  return
372
383
 
373
- def _closeDialogs(self) -> None:
374
- """Close non-modal dialogs."""
375
- from novelwriter.tools.manuscript import GuiManuscript
376
- from novelwriter.tools.writingstats import GuiWritingStats
377
-
384
+ def _closeToolDialogs(self) -> None:
385
+ """Close all open tool dialogs."""
386
+ from novelwriter.extensions.modified import NToolDialog
378
387
  for widget in self.mainGui.children():
379
- if isinstance(widget, (GuiManuscript, GuiWritingStats)):
388
+ if isinstance(widget, NToolDialog):
380
389
  widget.close()
381
-
382
390
  return
383
391
 
384
- # END Class SharedData
385
-
386
392
 
387
393
  class _GuiAlert(QMessageBox):
388
394
 
@@ -443,5 +449,3 @@ class _GuiAlert(QMessageBox):
443
449
  self.setIconPixmap(self._theme.getPixmap("alert_question", (pSz, pSz)))
444
450
  self.setWindowTitle(self.tr("Question"))
445
451
  return
446
-
447
- # END Class _GuiAlert