novelWriter 2.2.1__py3-none-any.whl → 2.3b1__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.2.1.dist-info → novelWriter-2.3b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/RECORD +102 -92
  3. novelwriter/__init__.py +4 -4
  4. novelwriter/assets/icons/typicons_dark/icons.conf +6 -0
  5. novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
  6. novelwriter/assets/icons/typicons_dark/typ_document-add-col.svg +8 -0
  7. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
  8. novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
  9. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
  10. novelwriter/assets/icons/typicons_light/icons.conf +6 -0
  11. novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
  12. novelwriter/assets/icons/typicons_light/typ_document-add-col.svg +8 -0
  13. novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
  14. novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
  15. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
  16. novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
  17. novelwriter/assets/images/novelwriter-text-light.svg +4 -0
  18. novelwriter/assets/images/welcome-dark.jpg +0 -0
  19. novelwriter/assets/images/welcome-light.jpg +0 -0
  20. novelwriter/assets/manual.pdf +0 -0
  21. novelwriter/assets/sample.zip +0 -0
  22. novelwriter/assets/syntax/default_dark.conf +1 -0
  23. novelwriter/assets/syntax/default_light.conf +1 -0
  24. novelwriter/assets/syntax/grey_dark.conf +1 -0
  25. novelwriter/assets/syntax/grey_light.conf +1 -0
  26. novelwriter/assets/syntax/light_owl.conf +1 -0
  27. novelwriter/assets/syntax/night_owl.conf +1 -0
  28. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  29. novelwriter/assets/syntax/solarized_light.conf +1 -0
  30. novelwriter/assets/syntax/tomorrow.conf +1 -0
  31. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  32. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  33. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  34. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  35. novelwriter/assets/text/credits_en.htm +4 -2
  36. novelwriter/assets/themes/default_dark.conf +2 -2
  37. novelwriter/assets/themes/default_light.conf +2 -2
  38. novelwriter/common.py +48 -37
  39. novelwriter/config.py +36 -41
  40. novelwriter/constants.py +38 -16
  41. novelwriter/core/buildsettings.py +7 -7
  42. novelwriter/core/coretools.py +192 -154
  43. novelwriter/core/docbuild.py +6 -3
  44. novelwriter/core/document.py +6 -6
  45. novelwriter/core/index.py +89 -56
  46. novelwriter/core/item.py +21 -3
  47. novelwriter/core/options.py +8 -7
  48. novelwriter/core/project.py +69 -44
  49. novelwriter/core/projectdata.py +1 -14
  50. novelwriter/core/projectxml.py +13 -41
  51. novelwriter/core/sessions.py +2 -1
  52. novelwriter/core/spellcheck.py +2 -1
  53. novelwriter/core/status.py +2 -1
  54. novelwriter/core/storage.py +178 -140
  55. novelwriter/core/tohtml.py +4 -2
  56. novelwriter/core/tokenizer.py +73 -45
  57. novelwriter/core/toodt.py +40 -30
  58. novelwriter/core/tree.py +3 -2
  59. novelwriter/dialogs/about.py +70 -160
  60. novelwriter/dialogs/docmerge.py +6 -5
  61. novelwriter/dialogs/docsplit.py +6 -6
  62. novelwriter/dialogs/editlabel.py +1 -1
  63. novelwriter/dialogs/preferences.py +553 -703
  64. novelwriter/dialogs/{projsettings.py → projectsettings.py} +288 -262
  65. novelwriter/dialogs/quotes.py +27 -23
  66. novelwriter/dialogs/wordlist.py +96 -40
  67. novelwriter/enum.py +20 -18
  68. novelwriter/error.py +1 -1
  69. novelwriter/extensions/circularprogress.py +11 -11
  70. novelwriter/extensions/configlayout.py +185 -134
  71. novelwriter/extensions/modified.py +81 -0
  72. novelwriter/extensions/novelselector.py +26 -12
  73. novelwriter/extensions/pagedsidebar.py +14 -16
  74. novelwriter/extensions/simpleprogress.py +5 -5
  75. novelwriter/extensions/statusled.py +8 -8
  76. novelwriter/extensions/switch.py +31 -63
  77. novelwriter/extensions/switchbox.py +1 -1
  78. novelwriter/extensions/versioninfo.py +153 -0
  79. novelwriter/gui/doceditor.py +178 -150
  80. novelwriter/gui/dochighlight.py +63 -92
  81. novelwriter/gui/docviewer.py +49 -51
  82. novelwriter/gui/docviewerpanel.py +72 -24
  83. novelwriter/gui/itemdetails.py +7 -7
  84. novelwriter/gui/mainmenu.py +14 -18
  85. novelwriter/gui/noveltree.py +9 -8
  86. novelwriter/gui/outline.py +98 -75
  87. novelwriter/gui/projtree.py +188 -61
  88. novelwriter/gui/sidebar.py +3 -4
  89. novelwriter/gui/statusbar.py +3 -4
  90. novelwriter/gui/theme.py +60 -68
  91. novelwriter/guimain.py +49 -156
  92. novelwriter/shared.py +15 -1
  93. novelwriter/tools/dictionaries.py +5 -6
  94. novelwriter/tools/manuscript.py +6 -6
  95. novelwriter/tools/manussettings.py +192 -221
  96. novelwriter/tools/noveldetails.py +525 -0
  97. novelwriter/tools/welcome.py +802 -0
  98. novelwriter/tools/writingstats.py +9 -9
  99. novelwriter/assets/images/wizard-back.jpg +0 -0
  100. novelwriter/assets/text/gplv3_en.htm +0 -641
  101. novelwriter/assets/text/release_notes.htm +0 -60
  102. novelwriter/dialogs/projdetails.py +0 -518
  103. novelwriter/dialogs/projload.py +0 -294
  104. novelwriter/dialogs/updates.py +0 -172
  105. novelwriter/extensions/pageddialog.py +0 -130
  106. novelwriter/tools/projwizard.py +0 -478
  107. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/LICENSE.md +0 -0
  108. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/WHEEL +0 -0
  109. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/entry_points.txt +0 -0
  110. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/top_level.txt +0 -0
novelwriter/gui/theme.py CHANGED
@@ -67,12 +67,13 @@ class GuiTheme:
67
67
  self.themeLicense = ""
68
68
  self.themeLicenseUrl = ""
69
69
  self.themeIcons = ""
70
+ self.isLightTheme = True
70
71
 
71
72
  # GUI
72
- self.statNone = [120, 120, 120]
73
- self.statUnsaved = [200, 15, 39]
74
- self.statSaved = [2, 133, 37]
75
- self.helpText = [0, 0, 0]
73
+ self.statNone = QColor(120, 120, 120)
74
+ self.statUnsaved = QColor(200, 15, 39)
75
+ self.statSaved = QColor(2, 133, 37)
76
+ self.helpText = QColor(0, 0, 0)
76
77
 
77
78
  # Loaded Syntax Settings
78
79
  # ======================
@@ -87,23 +88,24 @@ class GuiTheme:
87
88
  self.syntaxLicenseUrl = ""
88
89
 
89
90
  # Colours
90
- self.colBack = [255, 255, 255]
91
- self.colText = [0, 0, 0]
92
- self.colLink = [0, 0, 0]
93
- self.colHead = [0, 0, 0]
94
- self.colHeadH = [0, 0, 0]
95
- self.colEmph = [0, 0, 0]
96
- self.colDialN = [0, 0, 0]
97
- self.colDialD = [0, 0, 0]
98
- self.colDialS = [0, 0, 0]
99
- self.colHidden = [0, 0, 0]
100
- self.colCode = [0, 0, 0]
101
- self.colKey = [0, 0, 0]
102
- self.colVal = [0, 0, 0]
103
- self.colSpell = [0, 0, 0]
104
- self.colError = [0, 0, 0]
105
- self.colRepTag = [0, 0, 0]
106
- self.colMod = [0, 0, 0]
91
+ self.colBack = QColor(255, 255, 255)
92
+ self.colText = QColor(0, 0, 0)
93
+ self.colLink = QColor(0, 0, 0)
94
+ self.colHead = QColor(0, 0, 0)
95
+ self.colHeadH = QColor(0, 0, 0)
96
+ self.colEmph = QColor(0, 0, 0)
97
+ self.colDialN = QColor(0, 0, 0)
98
+ self.colDialD = QColor(0, 0, 0)
99
+ self.colDialS = QColor(0, 0, 0)
100
+ self.colHidden = QColor(0, 0, 0)
101
+ self.colCode = QColor(0, 0, 0)
102
+ self.colKey = QColor(0, 0, 0)
103
+ self.colVal = QColor(0, 0, 0)
104
+ self.colOpt = QColor(0, 0, 0)
105
+ self.colSpell = QColor(0, 0, 0)
106
+ self.colError = QColor(0, 0, 0)
107
+ self.colRepTag = QColor(0, 0, 0)
108
+ self.colMod = QColor(0, 0, 0)
107
109
 
108
110
  # Class Setup
109
111
  # ===========
@@ -112,7 +114,7 @@ class GuiTheme:
112
114
  self._setGuiFont()
113
115
 
114
116
  # Load Themes
115
- self._guiPalette = QPalette()
117
+ self._guiPalette = QPalette()
116
118
  self._themeList: list[tuple[str, str]] = []
117
119
  self._syntaxList: list[tuple[str, str]] = []
118
120
  self._availThemes: dict[str, Path] = {}
@@ -254,13 +256,13 @@ class GuiTheme:
254
256
 
255
257
  backLNess = backCol.lightnessF()
256
258
  textLNess = textCol.lightnessF()
257
-
258
- if self.helpText == [0, 0, 0]:
259
- if backLNess > textLNess:
259
+ self.isLightTheme = backLNess > textLNess
260
+ if self.helpText == QColor(0, 0, 0):
261
+ if self.isLightTheme:
260
262
  helpLCol = textLNess + 0.35*(backLNess - textLNess)
261
263
  else:
262
264
  helpLCol = backLNess + 0.65*(textLNess - backLNess)
263
- self.helpText = [int(255*helpLCol)]*3
265
+ self.helpText = QColor.fromHsl(0, 0, int(255*helpLCol))
264
266
 
265
267
  # Icons
266
268
  defaultIcons = "typicons_light" if backLNess >= 0.5 else "typicons_dark"
@@ -322,6 +324,7 @@ class GuiTheme:
322
324
  self.colCode = self._parseColour(confParser, cnfSec, "shortcode")
323
325
  self.colKey = self._parseColour(confParser, cnfSec, "keyword")
324
326
  self.colVal = self._parseColour(confParser, cnfSec, "value")
327
+ self.colOpt = self._parseColour(confParser, cnfSec, "optional")
325
328
  self.colSpell = self._parseColour(confParser, cnfSec, "spellcheckline")
326
329
  self.colError = self._parseColour(confParser, cnfSec, "errorline")
327
330
  self.colRepTag = self._parseColour(confParser, cnfSec, "replacetag")
@@ -367,22 +370,22 @@ class GuiTheme:
367
370
 
368
371
  def _setGuiFont(self) -> None:
369
372
  """Update the GUI's font style from settings."""
370
- theFont = QFont()
373
+ font = QFont()
371
374
  fontDB = QFontDatabase()
372
375
  if CONFIG.guiFont not in fontDB.families():
373
376
  if CONFIG.osWindows and "Arial" in fontDB.families():
374
377
  # On Windows we default to Arial if possible
375
- theFont.setFamily("Arial")
376
- theFont.setPointSize(10)
378
+ font.setFamily("Arial")
379
+ font.setPointSize(10)
377
380
  else:
378
- theFont = fontDB.systemFont(QFontDatabase.GeneralFont)
379
- CONFIG.guiFont = theFont.family()
380
- CONFIG.guiFontSize = theFont.pointSize()
381
+ font = fontDB.systemFont(QFontDatabase.GeneralFont)
382
+ CONFIG.guiFont = font.family()
383
+ CONFIG.guiFontSize = font.pointSize()
381
384
  else:
382
- theFont.setFamily(CONFIG.guiFont)
383
- theFont.setPointSize(CONFIG.guiFontSize)
385
+ font.setFamily(CONFIG.guiFont)
386
+ font.setPointSize(CONFIG.guiFontSize)
384
387
 
385
- qApp.setFont(theFont)
388
+ qApp.setFont(font)
386
389
 
387
390
  return
388
391
 
@@ -397,29 +400,14 @@ class GuiTheme:
397
400
 
398
401
  return True
399
402
 
400
- def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> list[int]:
403
+ def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> QColor:
401
404
  """Parse a colour value from a config string."""
402
- if parser.has_option(section, name):
403
- values = parser.get(section, name).split(",")
404
- result = []
405
- try:
406
- result.append(minmax(int(values[0]), 0, 255))
407
- result.append(minmax(int(values[1]), 0, 255))
408
- result.append(minmax(int(values[2]), 0, 255))
409
- except Exception:
410
- logger.error("Could not load theme colours for '%s' from config file", name)
411
- result = [0, 0, 0]
412
- else:
413
- logger.warning("Could not find theme colours for '%s' in config file", name)
414
- result = [0, 0, 0]
415
- return result
405
+ return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255]))
416
406
 
417
407
  def _setPalette(self, parser: NWConfigParser, section: str,
418
408
  name: str, value: QPalette.ColorRole) -> None:
419
409
  """Set a palette colour value from a config string."""
420
- self._guiPalette.setColor(
421
- value, QColor(*self._parseColour(parser, section, name))
422
- )
410
+ self._guiPalette.setColor(value, self._parseColour(parser, section, name))
423
411
  return
424
412
 
425
413
  # End Class GuiTheme
@@ -443,7 +431,7 @@ class GuiIcons:
443
431
  missing, a blank icon is returned and a warning issued.
444
432
  """
445
433
 
446
- ICON_KEYS = {
434
+ ICON_KEYS: set[str] = {
447
435
  # Project and GUI Icons
448
436
  "novelwriter", "alert_error", "alert_info", "alert_question", "alert_warn",
449
437
  "build_excluded", "build_filtered", "build_included", "proj_chapter", "proj_details",
@@ -453,7 +441,7 @@ class GuiIcons:
453
441
 
454
442
  # Class Icons
455
443
  "cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none", "cls_novel",
456
- "cls_object", "cls_plot", "cls_timeline", "cls_trash", "cls_world",
444
+ "cls_object", "cls_plot", "cls_template", "cls_timeline", "cls_trash", "cls_world",
457
445
 
458
446
  # Search Icons
459
447
  "search_cancel", "search_case", "search_loop", "search_preserve", "search_project",
@@ -464,9 +452,10 @@ class GuiIcons:
464
452
  "fmt_subscript", "fmt_superscript", "fmt_underline",
465
453
 
466
454
  # General Button Icons
467
- "add", "backward", "bookmark", "browse", "checked", "close", "cross", "down", "edit",
468
- "export", "forward", "maximise", "menu", "minimise", "noncheckable", "panel", "refresh",
469
- "remove", "revert", "search_replace", "search", "settings", "unchecked", "up", "view",
455
+ "add", "add_document", "backward", "bookmark", "browse", "checked", "close", "cross",
456
+ "document", "down", "edit", "export", "forward", "import", "maximise", "menu", "minimise",
457
+ "more", "noncheckable", "panel", "refresh", "remove", "revert", "search_replace", "search",
458
+ "settings", "star", "unchecked", "up", "view",
470
459
 
471
460
  # Switches
472
461
  "sticky-on", "sticky-off",
@@ -478,13 +467,14 @@ class GuiIcons:
478
467
  "deco_doc_nt_n",
479
468
  }
480
469
 
481
- TOGGLE_ICON_KEYS = {
470
+ TOGGLE_ICON_KEYS: dict[str, tuple[str, str]] = {
482
471
  "sticky": ("sticky-on", "sticky-off"),
483
472
  "bullet": ("bullet-on", "bullet-off"),
484
473
  }
485
474
 
486
- IMAGE_MAP = {
487
- "wiz-back": "wizard-back.jpg",
475
+ IMAGE_MAP: dict[str, tuple[str, str]] = {
476
+ "welcome": ("welcome-light.jpg", "welcome-dark.jpg"),
477
+ "nw-text": ("novelwriter-text-light.svg", "novelwriter-text-dark.svg"),
488
478
  }
489
479
 
490
480
  def __init__(self, mainTheme: GuiTheme) -> None:
@@ -598,7 +588,8 @@ class GuiIcons:
598
588
  if name in self._themeMap:
599
589
  imgPath = self._themeMap[name]
600
590
  elif name in self.IMAGE_MAP:
601
- imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name]
591
+ idx = 0 if self.mainTheme.isLightTheme else 1
592
+ imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx]
602
593
  else:
603
594
  logger.error("Decoration with name '%s' does not exist", name)
604
595
  return QPixmap()
@@ -608,12 +599,13 @@ class GuiIcons:
608
599
  return QPixmap()
609
600
 
610
601
  pixmap = QPixmap(str(imgPath))
602
+ tMode = Qt.TransformationMode.SmoothTransformation
611
603
  if w is not None and h is not None:
612
- return pixmap.scaled(w, h, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
604
+ return pixmap.scaled(w, h, Qt.AspectRatioMode.IgnoreAspectRatio, tMode)
613
605
  elif w is None and h is not None:
614
- return pixmap.scaledToHeight(h, Qt.SmoothTransformation)
606
+ return pixmap.scaledToHeight(h, tMode)
615
607
  elif w is not None and h is None:
616
- return pixmap.scaledToWidth(w, Qt.SmoothTransformation)
608
+ return pixmap.scaledToWidth(w, tMode)
617
609
 
618
610
  return pixmap
619
611
 
@@ -632,8 +624,8 @@ class GuiIcons:
632
624
  pOne = self.getPixmap(self.TOGGLE_ICON_KEYS[name][0], size)
633
625
  pTwo = self.getPixmap(self.TOGGLE_ICON_KEYS[name][1], size)
634
626
  icon = QIcon()
635
- icon.addPixmap(pOne, QIcon.Normal, QIcon.On)
636
- icon.addPixmap(pTwo, QIcon.Normal, QIcon.Off)
627
+ icon.addPixmap(pOne, QIcon.Mode.Normal, QIcon.State.On)
628
+ icon.addPixmap(pTwo, QIcon.Mode.Normal, QIcon.State.Off)
637
629
  return icon
638
630
  return QIcon()
639
631
 
@@ -641,7 +633,7 @@ class GuiIcons:
641
633
  """Return an icon from the icon buffer as a QPixmap. If it
642
634
  doesn't exist, return an empty QPixmap.
643
635
  """
644
- return self.getIcon(name).pixmap(size[0], size[1], QIcon.Normal)
636
+ return self.getIcon(name).pixmap(size[0], size[1], QIcon.Mode.Normal)
645
637
 
646
638
  def getItemIcon(self, tType: nwItemType, tClass: nwItemClass,
647
639
  tLayout: nwItemLayout, hLevel: str = "H0") -> QIcon:
novelwriter/guimain.py CHANGED
@@ -30,14 +30,15 @@ from time import time
30
30
  from pathlib import Path
31
31
  from datetime import datetime
32
32
 
33
- from PyQt5.QtCore import Qt, QTimer, pyqtSlot
34
33
  from PyQt5.QtGui import QCloseEvent, QCursor, QIcon
34
+ from PyQt5.QtCore import Qt, QTimer, pyqtSlot
35
35
  from PyQt5.QtWidgets import (
36
- QDialog, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, QShortcut,
37
- QSplitter, QStackedWidget, QVBoxLayout, QWidget, qApp
36
+ QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, QShortcut, QSplitter,
37
+ QStackedWidget, QVBoxLayout, QWidget, qApp
38
38
  )
39
39
 
40
- from novelwriter import CONFIG, SHARED, __hexversion__
40
+ from novelwriter import CONFIG, SHARED, __hexversion__, __version__
41
+ from novelwriter.constants import nwConst
41
42
  from novelwriter.gui.theme import GuiTheme
42
43
  from novelwriter.gui.sidebar import GuiSideBar
43
44
  from novelwriter.gui.outline import GuiOutlineView
@@ -50,23 +51,19 @@ from novelwriter.gui.statusbar import GuiMainStatus
50
51
  from novelwriter.gui.itemdetails import GuiItemDetails
51
52
  from novelwriter.gui.docviewerpanel import GuiDocViewerPanel
52
53
  from novelwriter.dialogs.about import GuiAbout
53
- from novelwriter.dialogs.updates import GuiUpdates
54
- from novelwriter.dialogs.projload import GuiProjectLoad
55
54
  from novelwriter.dialogs.wordlist import GuiWordList
56
55
  from novelwriter.dialogs.preferences import GuiPreferences
57
- from novelwriter.dialogs.projdetails import GuiProjectDetails
58
- from novelwriter.dialogs.projsettings import GuiProjectSettings
56
+ from novelwriter.dialogs.projectsettings import GuiProjectSettings
57
+ from novelwriter.tools.welcome import GuiWelcome
59
58
  from novelwriter.tools.manuscript import GuiManuscript
60
- from novelwriter.tools.projwizard import GuiProjectWizard
61
59
  from novelwriter.tools.dictionaries import GuiDictionaries
60
+ from novelwriter.tools.noveldetails import GuiNovelDetails
62
61
  from novelwriter.tools.writingstats import GuiWritingStats
63
- from novelwriter.core.coretools import ProjectBuilder
64
62
 
65
63
  from novelwriter.enum import (
66
- nwDocAction, nwDocInsert, nwDocMode, nwItemType, nwItemClass, nwWidget, nwView
64
+ nwDocAction, nwDocInsert, nwDocMode, nwItemType, nwWidget, nwView
67
65
  )
68
- from novelwriter.common import hexToInt
69
- from novelwriter.constants import nwFiles
66
+ from novelwriter.common import formatFileFilter, formatVersion, hexToInt
70
67
 
71
68
  logger = logging.getLogger(__name__)
72
69
 
@@ -82,13 +79,6 @@ class GuiMain(QMainWindow):
82
79
  function. Also, the project instance and theme instance are created
83
80
  here. These should be passed around to all other objects who need
84
81
  them and new instances of them should generally not be created.
85
-
86
- * All other GUI classes that depend on any components from the
87
- main GUI should be passed a reference to the instance of this
88
- class.
89
- * All non-GUI classes can be passed a reference to the NWProject
90
- instance if the Main GUI is not needed (which it generally
91
- shouldn't need).
92
82
  """
93
83
 
94
84
  def __init__(self) -> None:
@@ -359,12 +349,18 @@ class GuiMain(QMainWindow):
359
349
  self.openProject(cmdOpen)
360
350
 
361
351
  if not SHARED.hasProject:
362
- self.showProjectLoadDialog()
352
+ self.showWelcomeDialog()
363
353
 
364
- # Determine whether release notes need to be shown or not
354
+ # If this is a new release, let the user know
365
355
  if hexToInt(CONFIG.lastNotes) < hexToInt(__hexversion__):
366
356
  CONFIG.lastNotes = __hexversion__
367
- self.showAboutNWDialog(showNotes=True)
357
+ trVersion = self.tr(
358
+ "You are now running novelWriter version {0}.".format(formatVersion(__version__))
359
+ )
360
+ trRelease = self.tr(
361
+ "Please check the {0}release notes{1} for further details."
362
+ ).format(f"<a href='{nwConst.URL_RELEASES}'>", "</a>")
363
+ SHARED.info(f"{trVersion}<br>{trRelease}")
368
364
 
369
365
  return
370
366
 
@@ -372,42 +368,6 @@ class GuiMain(QMainWindow):
372
368
  # Project Actions
373
369
  ##
374
370
 
375
- def newProject(self, projData: dict | None = None) -> bool:
376
- """Create a new project via the new project wizard."""
377
- if SHARED.hasProject:
378
- if not self.closeProject():
379
- SHARED.error(self.tr(
380
- "Cannot create a new project when another project is open."
381
- ))
382
- return False
383
-
384
- if projData is None:
385
- projData = self.showNewProjectDialog()
386
-
387
- if projData is None:
388
- return False
389
-
390
- projPath = projData.get("projPath", None)
391
- if projPath is None or projData is None:
392
- logger.error("No projData or projPath set")
393
- return False
394
-
395
- if (Path(projPath) / nwFiles.PROJ_FILE).is_file():
396
- SHARED.error(self.tr(
397
- "A project already exists in that location. "
398
- "Please choose another folder."
399
- ))
400
- return False
401
-
402
- logger.info("Creating new project")
403
- nwProject = ProjectBuilder()
404
- if nwProject.buildProject(projData):
405
- self.openProject(projPath)
406
- else:
407
- return False
408
-
409
- return True
410
-
411
371
  def closeProject(self, isYes: bool = False) -> bool:
412
372
  """Close the project if one is open. isYes is passed on from the
413
373
  close application event so the user doesn't get prompted twice
@@ -446,7 +406,7 @@ class GuiMain(QMainWindow):
446
406
  self.docViewerPanel.closeProjectTasks()
447
407
  self.outlineView.closeProjectTasks()
448
408
  self.novelView.closeProjectTasks()
449
- self.projView.clearProjectView()
409
+ self.projView.closeProjectTasks()
450
410
  self.itemDetails.clearDetails()
451
411
  self.mainStatus.clearStatus()
452
412
 
@@ -462,8 +422,7 @@ class GuiMain(QMainWindow):
462
422
  if projFile is None:
463
423
  return False
464
424
 
465
- # Make sure any open project is cleared out first before we load
466
- # another one
425
+ # Make sure any open project is cleared out first
467
426
  if not self.closeProject():
468
427
  return False
469
428
 
@@ -711,14 +670,9 @@ class GuiMain(QMainWindow):
711
670
  return False
712
671
 
713
672
  lastPath = CONFIG.lastPath()
714
- extFilter = [
715
- self.tr("Text files ({0})").format("*.txt"),
716
- self.tr("Markdown files ({0})").format("*.md"),
717
- self.tr("novelWriter files ({0})").format("*.nwd"),
718
- self.tr("All files ({0})").format("*"),
719
- ]
673
+ ffilter = formatFileFilter(["*.txt", "*.md", "*.nwd", "*"])
720
674
  loadFile, _ = QFileDialog.getOpenFileName(
721
- self, self.tr("Import File"), str(lastPath), filter=";;".join(extFilter)
675
+ self, self.tr("Import File"), str(lastPath), filter=ffilter
722
676
  )
723
677
  if not loadFile:
724
678
  return False
@@ -726,10 +680,10 @@ class GuiMain(QMainWindow):
726
680
  if loadFile.strip() == "":
727
681
  return False
728
682
 
729
- theText = None
683
+ text = None
730
684
  try:
731
685
  with open(loadFile, mode="rt", encoding="utf-8") as inFile:
732
- theText = inFile.read()
686
+ text = inFile.read()
733
687
  CONFIG.setLastPath(loadFile)
734
688
  except Exception as exc:
735
689
  SHARED.error(self.tr(
@@ -751,7 +705,7 @@ class GuiMain(QMainWindow):
751
705
  if not msgYes:
752
706
  return False
753
707
 
754
- self.docEditor.replaceText(theText)
708
+ self.docEditor.replaceText(text)
755
709
 
756
710
  return True
757
711
 
@@ -838,33 +792,14 @@ class GuiMain(QMainWindow):
838
792
  # Main Dialogs
839
793
  ##
840
794
 
841
- def showProjectLoadDialog(self) -> None:
842
- """Open the projects dialog for selecting either existing
843
- projects from a cache of recently opened projects, or provide a
844
- browse button for projects not yet cached. Selecting to create a
845
- new project is forwarded to the new project wizard.
846
- """
847
- dlgProj = GuiProjectLoad(self)
848
- dlgProj.exec_()
849
-
850
- if dlgProj.result() == QDialog.Accepted:
851
- if dlgProj.openState == GuiProjectLoad.OPEN_STATE:
852
- self.openProject(dlgProj.openPath)
853
- elif dlgProj.openState == GuiProjectLoad.NEW_STATE:
854
- self.newProject()
855
-
795
+ @pyqtSlot()
796
+ def showWelcomeDialog(self) -> None:
797
+ """Open the welcome dialog."""
798
+ dialog = GuiWelcome(self)
799
+ dialog.openProjectRequest.connect(self._openProject)
800
+ dialog.exec_()
856
801
  return
857
802
 
858
- def showNewProjectDialog(self) -> dict | None:
859
- """Open the wizard and assemble a project options dict."""
860
- newProj = GuiProjectWizard(self)
861
- newProj.exec_()
862
-
863
- if newProj.result() == QDialog.Accepted:
864
- return self._assembleProjectWizardData(newProj)
865
-
866
- return None
867
-
868
803
  @pyqtSlot()
869
804
  def showPreferencesDialog(self) -> None:
870
805
  """Open the preferences dialog."""
@@ -875,19 +810,19 @@ class GuiMain(QMainWindow):
875
810
 
876
811
  @pyqtSlot()
877
812
  @pyqtSlot(int)
878
- def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.TAB_MAIN) -> None:
813
+ def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.PAGE_SETTINGS) -> None:
879
814
  """Open the project settings dialog."""
880
815
  if SHARED.hasProject:
881
- dialog = GuiProjectSettings(self, focusTab=focusTab)
816
+ dialog = GuiProjectSettings(self, gotoPage=focusTab)
882
817
  dialog.newProjectSettingsReady.connect(self._processProjectSettingsChanges)
883
818
  dialog.exec_()
884
819
  return
885
820
 
886
821
  @pyqtSlot()
887
- def showProjectDetailsDialog(self) -> None:
888
- """Open the project details dialog."""
822
+ def showNovelDetailsDialog(self) -> None:
823
+ """Open the novel details dialog."""
889
824
  if SHARED.hasProject:
890
- dialog = GuiProjectDetails(self)
825
+ dialog = GuiNovelDetails(self)
891
826
  dialog.setModal(True)
892
827
  dialog.show()
893
828
  dialog.raise_()
@@ -931,7 +866,7 @@ class GuiMain(QMainWindow):
931
866
  return
932
867
 
933
868
  @pyqtSlot()
934
- def showAboutNWDialog(self, showNotes: bool = False) -> None:
869
+ def showAboutNWDialog(self) -> None:
935
870
  """Show the novelWriter about dialog."""
936
871
  dialog = GuiAbout(self)
937
872
  dialog.setModal(True)
@@ -939,8 +874,6 @@ class GuiMain(QMainWindow):
939
874
  dialog.raise_()
940
875
  qApp.processEvents()
941
876
  dialog.populateGUI()
942
- if showNotes:
943
- dialog.showReleaseNotes()
944
877
  return
945
878
 
946
879
  @pyqtSlot()
@@ -950,17 +883,6 @@ class GuiMain(QMainWindow):
950
883
  msgBox.aboutQt(self, "About Qt")
951
884
  return
952
885
 
953
- @pyqtSlot()
954
- def showUpdatesDialog(self) -> None:
955
- """Show the check for updates dialog."""
956
- dialog = GuiUpdates(self)
957
- dialog.setModal(True)
958
- dialog.show()
959
- dialog.raise_()
960
- qApp.processEvents()
961
- dialog.checkLatest()
962
- return
963
-
964
886
  @pyqtSlot()
965
887
  def showDictionariesDialog(self) -> None:
966
888
  """Show the download dictionaries dialog."""
@@ -1179,13 +1101,15 @@ class GuiMain(QMainWindow):
1179
1101
 
1180
1102
  return
1181
1103
 
1182
- @pyqtSlot()
1183
- def _processProjectSettingsChanges(self) -> None:
1104
+ @pyqtSlot(bool)
1105
+ def _processProjectSettingsChanges(self, rebuildTrees: bool) -> None:
1184
1106
  """Refresh data dependent on project settings."""
1185
1107
  logger.debug("Applying new project settings")
1186
1108
  SHARED.updateSpellCheckLanguage()
1187
1109
  self.itemDetails.refreshDetails()
1188
1110
  self._updateWindowTitle(SHARED.project.data.name)
1111
+ if rebuildTrees:
1112
+ self.rebuildTrees()
1189
1113
  return
1190
1114
 
1191
1115
  @pyqtSlot()
@@ -1207,6 +1131,13 @@ class GuiMain(QMainWindow):
1207
1131
  self.viewDocument(tHandle=tHandle, sTitle=sTitle)
1208
1132
  return
1209
1133
 
1134
+ @pyqtSlot(Path)
1135
+ def _openProject(self, path: Path) -> None:
1136
+ """Handle an open project request."""
1137
+ qApp.processEvents()
1138
+ self.openProject(path)
1139
+ return
1140
+
1210
1141
  @pyqtSlot(str, nwDocMode, str, bool)
1211
1142
  def _openDocument(self, tHandle: str, mode: nwDocMode, sTitle: str, setFocus: bool) -> None:
1212
1143
  """Handle an open document request."""
@@ -1461,44 +1392,6 @@ class GuiMain(QMainWindow):
1461
1392
  self.setWindowTitle(winTitle)
1462
1393
  return
1463
1394
 
1464
- def _assembleProjectWizardData(self, newProj: GuiProjectWizard) -> dict:
1465
- """Extract the user choices from the New Project Wizard and
1466
- store them in a dictionary.
1467
- """
1468
- projData = {
1469
- "projName": newProj.field("projName"),
1470
- "projTitle": newProj.field("projTitle"),
1471
- "projAuthor": newProj.field("projAuthor"),
1472
- "projPath": newProj.field("projPath"),
1473
- "popSample": newProj.field("popSample"),
1474
- "popMinimal": newProj.field("popMinimal"),
1475
- "popCustom": newProj.field("popCustom"),
1476
- "addRoots": [],
1477
- "addNotes": False,
1478
- "numChapters": 0,
1479
- "numScenes": 0,
1480
- }
1481
- if newProj.field("popCustom"):
1482
- addRoots = []
1483
- if newProj.field("addPlot"):
1484
- addRoots.append(nwItemClass.PLOT)
1485
- if newProj.field("addChar"):
1486
- addRoots.append(nwItemClass.CHARACTER)
1487
- if newProj.field("addWorld"):
1488
- addRoots.append(nwItemClass.WORLD)
1489
- projData["addRoots"] = addRoots
1490
- projData["addNotes"] = newProj.field("addNotes")
1491
- projData["numChapters"] = newProj.field("numChapters")
1492
- projData["numScenes"] = newProj.field("numScenes")
1493
-
1494
- try:
1495
- langIdx = newProj.field("projLang")
1496
- projData["projLang"] = CONFIG.listLanguages(CONFIG.LANG_PROJ)[langIdx][0]
1497
- except Exception:
1498
- projData["projLang"] = "en_GB"
1499
-
1500
- return projData
1501
-
1502
1395
  def _getTagSource(self, tag: str) -> tuple[str | None, str | None]:
1503
1396
  """Handle the index lookup of a tag and display an alert if the
1504
1397
  tag cannot be found.
novelwriter/shared.py CHANGED
@@ -31,8 +31,10 @@ from typing import TYPE_CHECKING, TypeVar
31
31
  from pathlib import Path
32
32
 
33
33
  from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal
34
- from PyQt5.QtWidgets import QMessageBox, QWidget
34
+ from PyQt5.QtWidgets import QFileDialog, QMessageBox, QWidget
35
+ from novelwriter.common import formatFileFilter
35
36
 
37
+ from novelwriter.constants import nwFiles
36
38
  from novelwriter.core.spellcheck import NWSpellEnchant
37
39
 
38
40
  if TYPE_CHECKING: # pragma: no cover
@@ -217,6 +219,18 @@ class SharedData(QObject):
217
219
  QThreadPool.globalInstance().start(runnable, priority=priority)
218
220
  return
219
221
 
222
+ def getProjectPath(self, parent: QWidget, path: str | Path | None = None,
223
+ allowZip: bool = False) -> Path | None:
224
+ """Open the file dialog and select a novelWriter project file."""
225
+ label = (self.tr("novelWriter Project File or Zip File")
226
+ if allowZip else self.tr("novelWriter Project File"))
227
+ ext = f"{nwFiles.PROJ_FILE} *.zip" if allowZip else nwFiles.PROJ_FILE
228
+ ffilter = formatFileFilter([(label, ext), "*"])
229
+ selected, _ = QFileDialog.getOpenFileName(
230
+ parent, self.tr("Open Project"), str(path or ""), filter=ffilter
231
+ )
232
+ return Path(selected) if selected else None
233
+
220
234
  def findTopLevelWidget(self, kind: type[NWWidget]) -> NWWidget | None:
221
235
  """Find a top level widget."""
222
236
  for widget in self.mainGui.children():
@@ -37,7 +37,7 @@ from PyQt5.QtWidgets import (
37
37
 
38
38
  from novelwriter import CONFIG, SHARED
39
39
  from novelwriter.error import formatException
40
- from novelwriter.common import openExternalPath, formatInt, getFileSize
40
+ from novelwriter.common import formatFileFilter, openExternalPath, formatInt, getFileSize
41
41
 
42
42
  logger = logging.getLogger(__name__)
43
43
 
@@ -180,12 +180,11 @@ class GuiDictionaries(QDialog):
180
180
  @pyqtSlot()
181
181
  def _doBrowseHunspell(self):
182
182
  """Browse for a Free/Libre Office dictionary."""
183
- extFilter = [
184
- self.tr("Free or Libre Office extension ({0})").format("*.sox *.oxt"),
185
- self.tr("All files ({0})").format("*"),
186
- ]
183
+ ffilter = formatFileFilter([
184
+ (self.tr("Free or Libre Office extension"), "*.sox *.oxt"), "*"
185
+ ])
187
186
  soxFile, _ = QFileDialog.getOpenFileName(
188
- self, self.tr("Browse Files"), "", filter=";;".join(extFilter)
187
+ self, self.tr("Browse Files"), "", filter=ffilter
189
188
  )
190
189
  if soxFile:
191
190
  path = Path(soxFile).absolute()