novelWriter 2.5b1__py3-none-any.whl → 2.5.1__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 (78) hide show
  1. {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/RECORD +77 -75
  3. {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +3 -3
  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_pl_PL.json +116 -0
  17. novelwriter/assets/i18n/project_pt_BR.json +74 -74
  18. novelwriter/assets/manual.pdf +0 -0
  19. novelwriter/assets/sample.zip +0 -0
  20. novelwriter/assets/text/credits_en.htm +52 -44
  21. novelwriter/assets/themes/cyberpunk_night.conf +1 -0
  22. novelwriter/assets/themes/default_dark.conf +1 -0
  23. novelwriter/assets/themes/default_light.conf +1 -0
  24. novelwriter/assets/themes/dracula.conf +1 -0
  25. novelwriter/assets/themes/solarized_dark.conf +1 -0
  26. novelwriter/assets/themes/solarized_light.conf +1 -0
  27. novelwriter/common.py +12 -3
  28. novelwriter/config.py +67 -15
  29. novelwriter/constants.py +8 -10
  30. novelwriter/core/buildsettings.py +5 -3
  31. novelwriter/core/coretools.py +3 -1
  32. novelwriter/core/docbuild.py +1 -0
  33. novelwriter/core/project.py +15 -4
  34. novelwriter/core/status.py +4 -1
  35. novelwriter/core/storage.py +6 -1
  36. novelwriter/core/tohtml.py +69 -29
  37. novelwriter/core/tokenizer.py +83 -14
  38. novelwriter/core/toodt.py +48 -21
  39. novelwriter/core/toqdoc.py +37 -21
  40. novelwriter/dialogs/about.py +10 -15
  41. novelwriter/dialogs/docmerge.py +16 -16
  42. novelwriter/dialogs/docsplit.py +16 -16
  43. novelwriter/dialogs/editlabel.py +6 -8
  44. novelwriter/dialogs/preferences.py +106 -93
  45. novelwriter/dialogs/projectsettings.py +16 -20
  46. novelwriter/dialogs/quotes.py +9 -5
  47. novelwriter/dialogs/wordlist.py +6 -6
  48. novelwriter/enum.py +4 -5
  49. novelwriter/extensions/configlayout.py +38 -4
  50. novelwriter/extensions/modified.py +22 -3
  51. novelwriter/extensions/{circularprogress.py → progressbars.py} +26 -3
  52. novelwriter/extensions/statusled.py +39 -23
  53. novelwriter/gui/doceditor.py +22 -13
  54. novelwriter/gui/dochighlight.py +30 -39
  55. novelwriter/gui/docviewer.py +24 -15
  56. novelwriter/gui/docviewerpanel.py +7 -0
  57. novelwriter/gui/mainmenu.py +11 -11
  58. novelwriter/gui/outline.py +4 -3
  59. novelwriter/gui/projtree.py +85 -77
  60. novelwriter/gui/search.py +10 -1
  61. novelwriter/gui/statusbar.py +25 -29
  62. novelwriter/gui/theme.py +3 -0
  63. novelwriter/guimain.py +139 -124
  64. novelwriter/shared.py +19 -8
  65. novelwriter/text/patterns.py +113 -0
  66. novelwriter/tools/dictionaries.py +2 -8
  67. novelwriter/tools/lipsum.py +8 -12
  68. novelwriter/tools/manusbuild.py +9 -9
  69. novelwriter/tools/manuscript.py +10 -5
  70. novelwriter/tools/manussettings.py +7 -3
  71. novelwriter/tools/noveldetails.py +10 -10
  72. novelwriter/tools/welcome.py +19 -10
  73. novelwriter/tools/writingstats.py +3 -3
  74. novelwriter/types.py +5 -2
  75. novelwriter/extensions/simpleprogress.py +0 -53
  76. {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/LICENSE.md +0 -0
  77. {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/entry_points.txt +0 -0
  78. {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/top_level.txt +0 -0
@@ -1,84 +1,92 @@
1
- <!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
2
1
  <html>
3
2
  <body>
4
3
 
5
4
  <h3>Main Developer</h3>
6
- <p>Veronica Berglyd Olsen (<a href="https://github.com/vkbo">@vkbo</a>)</p>
5
+
6
+ <p>Veronica Berglyd Olsen</p>
7
7
 
8
8
  <h3>Contributors</h3>
9
9
 
10
- <p>
11
- &nbsp;&bull;&nbsp;<b>Concept:</b> Marian Lückhof (<a href="https://github.com/Number042">@Number042</a>)<br>
12
- &nbsp;&bull;&nbsp;<b>Internationalisation:</b> Bruno Meneguello (<a href="https://github.com/bkmeneguello">@bkmeneguello</a>)<br>
13
- &nbsp;&bull;&nbsp;<b>Setup and Packaging:</b> Rachel Powers (<a href="https://github.com/Ryex">@Ryex</a>)
14
- </p>
10
+ <ul>
11
+ <li><b>Early Concept:</b> Marian Lückhof</li>
12
+ <li><b>Internationalisation:</b> Bruno Meneguello</li>
13
+ <li><b>Setup and Packaging:</b> Rachel Powers</li>
14
+ </ul>
15
+
15
16
  <p>For other contributions, see the project's
16
17
  <a href="https://github.com/vkbo/novelWriter/graphs/contributors">Contributors</a> page.</p>
17
18
 
18
19
  <h3>Artwork</h3>
19
20
 
20
- <p>The artwork on the Welcome dialog was created by <a href="https://louisdurrant.art">Louis Durrant</a>.</p>
21
+ <p>The artwork on the Welcome dialog was created by Louis Durrant.</p>
21
22
 
22
23
  <h3>Translations</h3>
23
24
 
24
25
  <p>The default language is English (UK) with English (US) as an option. These are the original
25
26
  translators for the languages currently available:</p>
26
- <p>
27
- &nbsp;&bull;&nbsp;<b>Dutch:</b> Martijn van der Kleijn (<a href="https://github.com/mvdkleijn">@mvdkleijn</a>)<br>
28
- &nbsp;&bull;&nbsp;<b>French:</b> Jan Lüdke (<a href="https://github.com/jyhelle">@jyhelle</a>)<br>
29
- &nbsp;&bull;&nbsp;<b>German:</b> Myian (<a href="https://github.com/heymyian">@heymyian</a>)<br>
30
- &nbsp;&bull;&nbsp;<b>Italian:</b> Riccardo Mangili<br>
31
- &nbsp;&bull;&nbsp;<b>Japanese:</b> hebekeg (<a href="https://github.com/hebekeg">@hebekeg</a>)<br>
32
- &nbsp;&bull;&nbsp;<b>Latin American Spanish:</b> Tommy Marplatt (<a href="https://github.com/tmarplatt">@tmarplatt</a>)<br>
33
- &nbsp;&bull;&nbsp;<b>Norwegian:</b> Veronica Berglyd Olsen (<a href="https://github.com/vkbo">@vkbo</a>)<br>
34
- &nbsp;&bull;&nbsp;<b>Portuguese:</b> Bruno Meneguello (<a href="https://github.com/bkmeneguello">@bkmeneguello</a>)<br>
35
- &nbsp;&bull;&nbsp;<b>Simplified Chinese:</b> Qianzhi Long (<a href="https://github.com/longqzh">@longqzh</a>)
36
- </p>
27
+
28
+ <ul>
29
+ <li><b>Dutch:</b> Martijn van der Kleijn (mvdkleijn)</li>
30
+ <li><b>French:</b> Jan Lüdke (jyhelle)</li>
31
+ <li><b>German:</b> Myian (HeyMyian)</li>
32
+ <li><b>Italian:</b> Riccardo Mangili</li>
33
+ <li><b>Japanese:</b> hebekeg</li>
34
+ <li><b>Latin American Spanish:</b> Tommy Marplatt (tmarplatt)</li>
35
+ <li><b>Norwegian:</b> Veronica Berglyd Olsen (vkbo)</li>
36
+ <li><b>Polish:</b> Anna Maria Polak (Nauthiz)</li>
37
+ <li><b>Portuguese:</b> Bruno Meneguello (bkmeneguello)</li>
38
+ <li><b>Simplified Chinese:</b> Qianzhi Long (longqzh)</li>
39
+ </ul>
37
40
 
38
41
  <p>Additional larger translation contributions:</p>
39
- <p>
40
- &nbsp;&bull;&nbsp;<b>French:</b> Albert Aribaud (<a href="https://github.com/aaribaud">@aaribaud</a>)
41
- </p>
42
+
43
+ <ul>
44
+ <li><b>French:</b> Albert Aribaud (aaribaud)</li>
45
+ <li><b>Portuguese:</b> Oli Maia (olimaia)</li>
46
+ </ul>
42
47
 
43
48
  <p>Translations are managed on <a href="https://crowdin.com/project/novelwriter">Crowdin</a>, and
44
- more contributions are listed on the project's <a href="https://crowdin.com/project/novelwriter/members">Members</a> page.</p>
49
+ more contributions are listed on the project's Members page.</p>
45
50
 
46
51
  <h3>Libraries</h3>
47
52
 
48
53
  <p>The following libraries are dependencies of novelWriter:</p>
49
- <p>
50
- &nbsp;&bull;&nbsp;<a href="https://www.qt.io">Qt5</a> by Qt Company<br>
51
- &nbsp;&bull;&nbsp;<a href="https://www.riverbankcomputing.com/software/pyqt">PyQt5</a> by Riverbank Computing<br>
52
- &nbsp;&bull;&nbsp;<a href="https://abiword.github.io/enchant">Enchant</a> by Dom Lachowicz<br>
53
- &nbsp;&bull;&nbsp;<a href="https://pyenchant.github.io/pyenchant">PyEnchant</a> by Dimitri Merejkowsky
54
- </p>
54
+
55
+ <ul>
56
+ <li><b>Qt5</b> by Qt Company</li>
57
+ <li><b>PyQt5</b> by Riverbank Computing</li>
58
+ <li><b>Enchant</b> by Dom Lachowicz</li>
59
+ <li><b>PyEnchant</b> by Dimitri Merejkowsky</li>
60
+ </ul>
55
61
 
56
62
  <h3>Assets</h3>
57
63
 
58
64
  <p>Some of the assets bundled with novelWriter were adapted from the following sources:</p>
59
- <p>
60
- &nbsp;&bull;&nbsp;<a href="https://github.com/stephenhutchings/typicons.font">Typicons</a> icons by Stephen Hutchings (CC BY-SA 4.0)<br>
61
- &nbsp;&bull;&nbsp;<a href="https://github.com/chriskempson/base16">Tomorrow</a> syntax themes by Chris Kempson (MIT License)<br>
62
- &nbsp;&bull;&nbsp;<a href="https://github.com/sdras/night-owl-vscode-theme">Owl</a> syntax themes by Sarah Drasner (MIT License)<br>
63
- &nbsp;&bull;&nbsp;<a href="https://github.com/altercation/solarized">Solarized</a> themes by Ethan Schoonover (MIT License)<br>
64
- &nbsp;&bull;&nbsp;<a href="https://github.com/alemvigh">Cyberpunk Night</a> theme by Anders Lemvigh (CC BY-SA 4.0)<br>
65
- &nbsp;&bull;&nbsp;<a href="https://draculatheme.com">Dracula</a> theme by Zeno Rocha (MIT License)<br>
66
- &nbsp;&bull;&nbsp;<a href="https://github.com/loilo/vscode-snazzy-light">Snazzy Light</a> theme by Florian Reuschel (MIT License)
67
- </p>
65
+
66
+ <ul>
67
+ <li><b>Typicons</b> icons by Stephen Hutchings (CC BY-SA 4.0)</li>
68
+ <li><b>Tomorrow</b> syntax themes by Chris Kempson (MIT License)</li>
69
+ <li><b>Owl</b> syntax themes by Sarah Drasner (MIT License)</li>
70
+ <li><b>Solarized</b> themes by Ethan Schoonover (MIT License)</li>
71
+ <li><b>Cyberpunk Night</b> theme by Anders Lemvigh (CC BY-SA 4.0)</li>
72
+ <li><b>Dracula</b> theme by Zeno Rocha (MIT License)</li>
73
+ <li><b>Snazzy Light</b> theme by Florian Reuschel (MIT License)</li>
74
+ </ul>
68
75
 
69
76
  <h3>Fonts</h3>
70
77
 
71
78
  <p>The font used for the main novelWriter logo, mimetype and text banners is Pridi. Other fonts are
72
79
  used on buttons and icons.</p>
73
- <p>
74
- &nbsp;&bull;&nbsp;Pridi by Cadson Demak (Open Font License, Version 1.1)<br>
75
- &nbsp;&bull;&nbsp;Source Sans Pro by Paul D. Hunt (SIL Open Font License)
76
- </p>
80
+
81
+ <ul>
82
+ <li><b>Pridi</b> by Cadson Demak (Open Font License, Version 1.1)</li>
83
+ <li><b>Source Sans Pro</b> by Paul D. Hunt (SIL Open Font License)</li>
84
+ </ul>
77
85
 
78
86
  <h3>Special Mentions</h3>
79
87
 
80
- <p>Additional thanks to <a href="https://github.com/johnblommers">@johnblommers</a> who was an early user and who has provided a lot
81
- of very useful feedback over the years.</p>
88
+ <p>Additional thanks to John Blommers who was an early user and who has provided a lot of very
89
+ useful feedback over the years.</p>
82
90
 
83
91
  </body>
84
92
  </html>
@@ -25,6 +25,7 @@ linkvisited = 50, 0, 80
25
25
 
26
26
  [GUI]
27
27
  helptext = 97, 97, 97
28
+ fadedtext = 97, 97, 97
28
29
  errortext = 255, 77, 77
29
30
  statusnone = 50, 50, 50
30
31
  statussaved = 77, 255, 77
@@ -26,6 +26,7 @@ linkvisited = 102, 153, 204
26
26
 
27
27
  [GUI]
28
28
  helptext = 164, 164, 164
29
+ fadedtext = 148, 148, 148
29
30
  errortext = 255, 164, 164
30
31
  statusnone = 150, 152, 150
31
32
  statussaved = 39, 135, 78
@@ -26,6 +26,7 @@ linkvisited = 66, 113, 174
26
26
 
27
27
  [GUI]
28
28
  helptext = 92, 92, 92
29
+ fadedtext = 108, 108, 108
29
30
  errortext = 255, 92, 92
30
31
  statusnone = 120, 120, 120
31
32
  statussaved = 200, 15, 39
@@ -41,6 +41,7 @@ linkvisited = 139, 233, 253
41
41
 
42
42
  [GUI]
43
43
  helptext = 204, 172, 249
44
+ fadedtext = 98, 114, 164
44
45
  errortext = 255, 85, 85
45
46
  statusnone = 98, 114, 164
46
47
  statussaved = 80, 250, 123
@@ -25,6 +25,7 @@ linkvisited = 38, 139, 210
25
25
 
26
26
  [GUI]
27
27
  helptext = 166, 161, 149
28
+ fadedtext = 166, 161, 149
28
29
  errortext = 255, 161, 149
29
30
  statusnone = 88, 110, 117
30
31
  statussaved = 42, 161, 152
@@ -25,6 +25,7 @@ linkvisited = 38, 139, 210
25
25
 
26
26
  [GUI]
27
27
  helptext = 78, 91, 95
28
+ fadedtext = 78, 91, 95
28
29
  errortext = 255, 91, 95
29
30
  statusnone = 88, 110, 117
30
31
  statussaved = 42, 161, 152
novelwriter/common.py CHANGED
@@ -279,6 +279,16 @@ def simplified(text: str) -> str:
279
279
  return " ".join(str(text).strip().split())
280
280
 
281
281
 
282
+ def compact(text: str) -> str:
283
+ """Compact a string by removing spaces."""
284
+ return "".join(str(text).split())
285
+
286
+
287
+ def uniqueCompact(text: str) -> str:
288
+ """Return a unique, compact and sorted string."""
289
+ return "".join(sorted(set(compact(text))))
290
+
291
+
282
292
  def elide(text: str, length: int) -> str:
283
293
  """Elide a piece of text to a maximum length."""
284
294
  if len(text) > (cut := max(4, length)):
@@ -523,12 +533,11 @@ def readTextFile(path: str | Path) -> str:
523
533
 
524
534
 
525
535
  def makeFileNameSafe(text: str) -> str:
526
- """Return a filename safe string.
536
+ """Return a filename-safe string.
527
537
  See: https://unicode.org/reports/tr15/#Norm_Forms
528
538
  """
529
539
  text = unicodedata.normalize("NFKC", text).strip()
530
- allowed = (" ", ".", "-", "_")
531
- return "".join(c for c in text if c.isalnum() or c in allowed)
540
+ return "".join(c for c in text if c.isprintable() and c not in r'\/:*?"<>|')
532
541
 
533
542
 
534
543
  def getFileSize(path: Path) -> int:
novelwriter/config.py CHANGED
@@ -5,6 +5,7 @@ novelWriter – Config Class
5
5
  File History:
6
6
  Created: 2018-09-22 [0.0.1] Config
7
7
  Created: 2022-11-09 [2.0rc2] RecentProjects
8
+ Created: 2024-06-16 [2.5rc1] RecentPaths
8
9
 
9
10
  This file is a part of novelWriter
10
11
  Copyright 2018–2024, Veronica Berglyd Olsen
@@ -103,7 +104,8 @@ class Config:
103
104
  # User Settings
104
105
  # =============
105
106
 
106
- self._recentObj = RecentProjects(self)
107
+ self._recentProjects = RecentProjects(self)
108
+ self._recentPaths = RecentPaths(self)
107
109
 
108
110
  # General GUI Settings
109
111
  self.guiLocale = self._qLocale.name()
@@ -180,7 +182,6 @@ class Config:
180
182
  self.fmtPadThin = False
181
183
 
182
184
  # User Paths
183
- self._lastPath = self._homePath # The user's last used path
184
185
  self._backupPath = self._backPath # Backup path to use, can be none
185
186
 
186
187
  # Spell Checking Settings
@@ -253,7 +254,7 @@ class Config:
253
254
 
254
255
  @property
255
256
  def recentProjects(self) -> RecentProjects:
256
- return self._recentObj
257
+ return self._recentProjects
257
258
 
258
259
  @property
259
260
  def mainWinSize(self) -> list[int]:
@@ -343,7 +344,7 @@ class Config:
343
344
  self._outlnPanePos = [int(x/self.guiScale) for x in pos]
344
345
  return
345
346
 
346
- def setLastPath(self, path: str | Path) -> None:
347
+ def setLastPath(self, key: str, path: str | Path) -> None:
347
348
  """Set the last used path. Only the folder is saved, so if the
348
349
  path is not a folder, the parent of the path is used instead.
349
350
  """
@@ -352,8 +353,7 @@ class Config:
352
353
  if not path.is_dir():
353
354
  path = path.parent
354
355
  if path.is_dir():
355
- self._lastPath = path
356
- logger.debug("Last path updated: %s" % self._lastPath)
356
+ self._recentPaths.setPath(key, path)
357
357
  return
358
358
 
359
359
  def setBackupPath(self, path: Path | str) -> None:
@@ -438,11 +438,12 @@ class Config:
438
438
  return self._appPath / "assets" / target
439
439
  return self._appPath / "assets"
440
440
 
441
- def lastPath(self) -> Path:
441
+ def lastPath(self, key: str) -> Path:
442
442
  """Return the last path used by the user, if it exists."""
443
- if isinstance(self._lastPath, Path):
444
- if self._lastPath.is_dir():
445
- return self._lastPath
443
+ if path := self._recentPaths.getPath(key):
444
+ asPath = Path(path)
445
+ if asPath.is_dir():
446
+ return asPath
446
447
  return self._homePath
447
448
 
448
449
  def backupPath(self) -> Path:
@@ -516,7 +517,6 @@ class Config:
516
517
  logger.debug("Data Path: %s", self._dataPath)
517
518
  logger.debug("App Root: %s", self._appRoot)
518
519
  logger.debug("App Path: %s", self._appPath)
519
- logger.debug("Last Path: %s", self._lastPath)
520
520
  logger.debug("PDF Manual: %s", self.pdfDocs)
521
521
 
522
522
  # If the config and data folders don't exist, create them
@@ -531,7 +531,8 @@ class Config:
531
531
  (self._dataPath / "syntax").mkdir(exist_ok=True)
532
532
  (self._dataPath / "themes").mkdir(exist_ok=True)
533
533
 
534
- self._recentObj.loadCache()
534
+ self._recentPaths.loadCache()
535
+ self._recentProjects.loadCache()
535
536
  self._checkOptionalPackages()
536
537
 
537
538
  logger.debug("Config instance initialised")
@@ -600,7 +601,6 @@ class Config:
600
601
  self.hideHScroll = conf.rdBool(sec, "hidehscroll", self.hideHScroll)
601
602
  self.lastNotes = conf.rdStr(sec, "lastnotes", self.lastNotes)
602
603
  self.nativeFont = conf.rdBool(sec, "nativefont", self.nativeFont)
603
- self._lastPath = conf.rdPath(sec, "lastpath", self._lastPath)
604
604
 
605
605
  # Sizes
606
606
  sec = "Sizes"
@@ -710,7 +710,6 @@ class Config:
710
710
  "hidehscroll": str(self.hideHScroll),
711
711
  "lastnotes": str(self.lastNotes),
712
712
  "nativefont": str(self.nativeFont),
713
- "lastpath": str(self._lastPath),
714
713
  }
715
714
 
716
715
  conf["Sizes"] = {
@@ -811,7 +810,7 @@ class Config:
811
810
  """Pack a list of items into a comma-separated string for saving
812
811
  to the config file.
813
812
  """
814
- return ", ".join([str(inVal) for inVal in data])
813
+ return ", ".join(str(inVal) for inVal in data)
815
814
 
816
815
  def _checkOptionalPackages(self) -> None:
817
816
  """Check optional packages used by some features."""
@@ -893,3 +892,56 @@ class RecentProjects:
893
892
  logger.debug("Removed recent: %s", path)
894
893
  self.saveCache()
895
894
  return
895
+
896
+
897
+ class RecentPaths:
898
+
899
+ KEYS = ["default", "project", "import", "outline", "stats"]
900
+
901
+ def __init__(self, config: Config) -> None:
902
+ self._conf = config
903
+ self._data = {}
904
+ return
905
+
906
+ def setPath(self, key: str, path: Path | str) -> None:
907
+ """Set a path for a given key, and save the cache."""
908
+ if key in self.KEYS:
909
+ self._data[key] = str(path)
910
+ self.saveCache()
911
+ return
912
+
913
+ def getPath(self, key: str) -> str | None:
914
+ """Get a path for a given key, or return None."""
915
+ return self._data.get(key)
916
+
917
+ def loadCache(self) -> bool:
918
+ """Load the cache file for recent paths."""
919
+ self._data = {}
920
+ cacheFile = self._conf.dataPath(nwFiles.RECENT_PATH)
921
+ if cacheFile.is_file():
922
+ try:
923
+ with open(cacheFile, mode="r", encoding="utf-8") as inFile:
924
+ data = json.load(inFile)
925
+ if isinstance(data, dict):
926
+ for key, path in data.items():
927
+ if key in self.KEYS and isinstance(path, str):
928
+ self._data[key] = path
929
+ except Exception:
930
+ logger.error("Could not load recent paths cache")
931
+ logException()
932
+ return False
933
+ return True
934
+
935
+ def saveCache(self) -> bool:
936
+ """Save the cache dictionary of recent paths."""
937
+ cacheFile = self._conf.dataPath(nwFiles.RECENT_PATH)
938
+ cacheTemp = cacheFile.with_suffix(".tmp")
939
+ try:
940
+ with open(cacheTemp, mode="w+", encoding="utf-8") as outFile:
941
+ json.dump(self._data, outFile, indent=2)
942
+ cacheTemp.replace(cacheFile)
943
+ except Exception:
944
+ logger.error("Could not save recent paths cache")
945
+ logException()
946
+ return False
947
+ return True
novelwriter/constants.py CHANGED
@@ -57,17 +57,14 @@ class nwConst:
57
57
  STATUS_MSG_TIMEOUT = 15000 # milliseconds
58
58
  MAX_SEARCH_RESULT = 1000
59
59
 
60
- # Dialogs
61
- DLG_FINISHED = 2
62
-
63
60
 
64
61
  class nwRegEx:
65
62
 
66
63
  FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
67
- FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
68
- FMT_ST = r"(?<![\w\\])([~]{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
69
- FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:i|b|s|u|m|sup|sub)\])"
70
- FMT_SV = r"(?<!\\)(\[(?i)(?:footnote):)(.+?)(?<!\\)(\])"
64
+ FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
65
+ FMT_ST = r"(?<![\w\\])(~{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
66
+ FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:b|i|s|u|m|sup|sub)\])"
67
+ FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])"
71
68
 
72
69
 
73
70
  class nwShortcode:
@@ -107,6 +104,7 @@ class nwFiles:
107
104
  # Config Files
108
105
  CONF_FILE = "novelwriter.conf"
109
106
  RECENT_FILE = "recentProjects.json"
107
+ RECENT_PATH = "recentPaths.json"
110
108
 
111
109
  # Project Root Files
112
110
  PROJ_FILE = "nwProject.nwx"
@@ -414,7 +412,7 @@ class nwUnicode:
414
412
  U_EMDASH = "\u2014" # Long dash
415
413
  U_HBAR = "\u2015" # Horizontal bar
416
414
  U_HELLIP = "\u2026" # Ellipsis
417
- U_MAPOSS = "\u02bc" # Modifier letter single apostrophe
415
+ U_MAPOS = "\u02bc" # Modifier letter single apostrophe
418
416
  U_PRIME = "\u2032" # Prime
419
417
  U_DPRIME = "\u2033" # Double prime
420
418
 
@@ -481,7 +479,7 @@ class nwUnicode:
481
479
  H_EMDASH = "&mdash;"
482
480
  H_HBAR = "&#8213;"
483
481
  H_HELLIP = "&hellip;"
484
- H_MAPOSS = "&#700;"
482
+ H_MAPOS = "&#700;"
485
483
  H_PRIME = "&prime;"
486
484
  H_DPRIME = "&#8243;"
487
485
 
@@ -546,7 +544,7 @@ class nwHtmlUnicode():
546
544
  nwUnicode.U_EMDASH: nwUnicode.H_EMDASH,
547
545
  nwUnicode.U_HBAR: nwUnicode.H_HBAR,
548
546
  nwUnicode.U_HELLIP: nwUnicode.H_HELLIP,
549
- nwUnicode.U_MAPOSS: nwUnicode.H_MAPOSS,
547
+ nwUnicode.U_MAPOS: nwUnicode.H_MAPOS,
550
548
  nwUnicode.U_PRIME: nwUnicode.H_PRIME,
551
549
  nwUnicode.U_DPRIME: nwUnicode.H_DPRIME,
552
550
 
@@ -82,6 +82,7 @@ SETTINGS_TEMPLATE = {
82
82
  "format.stripUnicode": (bool, False),
83
83
  "format.replaceTabs": (bool, False),
84
84
  "format.keepBreaks": (bool, True),
85
+ "format.showDialogue": (bool, False),
85
86
  "format.firstLineIndent": (bool, False),
86
87
  "format.firstIndentWidth": (float, 1.4),
87
88
  "format.indentFirstPar": (bool, False),
@@ -131,6 +132,7 @@ SETTINGS_LABELS = {
131
132
  "format.stripUnicode": QT_TRANSLATE_NOOP("Builds", "Replace Unicode Characters"),
132
133
  "format.replaceTabs": QT_TRANSLATE_NOOP("Builds", "Replace Tabs with Spaces"),
133
134
  "format.keepBreaks": QT_TRANSLATE_NOOP("Builds", "Preserve Hard Line Breaks"),
135
+ "format.showDialogue": QT_TRANSLATE_NOOP("Builds", "Apply Dialogue Highlighting"),
134
136
 
135
137
  "format.grpParIndent": QT_TRANSLATE_NOOP("Builds", "First Line Indent"),
136
138
  "format.firstLineIndent": QT_TRANSLATE_NOOP("Builds", "Enable Indent"),
@@ -217,7 +219,7 @@ class BuildSettings:
217
219
  return self._order
218
220
 
219
221
  @property
220
- def lastPath(self) -> Path:
222
+ def lastBuildPath(self) -> Path:
221
223
  """The last used build path."""
222
224
  if self._path.is_dir():
223
225
  return self._path
@@ -291,7 +293,7 @@ class BuildSettings:
291
293
  self._order = value
292
294
  return
293
295
 
294
- def setLastPath(self, path: Path | str | None) -> None:
296
+ def setLastBuildPath(self, path: Path | str | None) -> None:
295
297
  """Set the last used build path."""
296
298
  if isinstance(path, str):
297
299
  path = Path(path)
@@ -459,7 +461,7 @@ class BuildSettings:
459
461
  self.setName(data.get("name", ""))
460
462
  self.setBuildID(data.get("uuid", ""))
461
463
  self.setOrder(data.get("order", 0))
462
- self.setLastPath(data.get("path", None))
464
+ self.setLastBuildPath(data.get("path", None))
463
465
  self.setLastBuildName(data.get("build", ""))
464
466
 
465
467
  buildFmt = str(data.get("format", ""))
@@ -348,7 +348,9 @@ class DocSearch:
348
348
  rxMatch = rxItt.next()
349
349
  pos = rxMatch.capturedStart()
350
350
  num = rxMatch.capturedLength()
351
- context = text[pos:pos+100].partition("\n")[0]
351
+ lim = text[:pos].rfind("\n") + 1
352
+ cut = text[lim:pos].rfind(" ") + lim + 1
353
+ context = text[cut:cut+100].partition("\n")[0]
352
354
  if context:
353
355
  results.append((pos, num, context))
354
356
  count += 1
@@ -338,6 +338,7 @@ class NWBuildDocument:
338
338
  bldObj.setJustify(self._build.getBool("format.justifyText"))
339
339
  bldObj.setLineHeight(self._build.getFloat("format.lineHeight"))
340
340
  bldObj.setKeepLineBreaks(self._build.getBool("format.keepBreaks"))
341
+ bldObj.setDialogueHighlight(self._build.getBool("format.showDialogue"))
341
342
  bldObj.setFirstLineIndent(
342
343
  self._build.getBool("format.firstLineIndent"),
343
344
  self._build.getFloat("format.firstIndentWidth"),
@@ -254,13 +254,23 @@ class NWProject:
254
254
  status = self._storage.initProjectStorage(projPath, clearLock)
255
255
  if status != NWStorageOpen.READY:
256
256
  if status == NWStorageOpen.UNKOWN:
257
- SHARED.error(self.tr("Not a known project file format."))
257
+ SHARED.error(
258
+ self.tr("Not a known project file format."),
259
+ info=self.tr("Path: {0}").format(str(projPath))
260
+ )
258
261
  elif status == NWStorageOpen.NOT_FOUND:
259
- SHARED.error(self.tr("Project file not found."))
262
+ SHARED.error(
263
+ self.tr("Project file not found."),
264
+ info=self.tr("Path: {0}").format(str(projPath))
265
+ )
266
+ elif status == NWStorageOpen.FAILED:
267
+ SHARED.error(
268
+ self.tr("Failed to open project."),
269
+ info=self.tr("Path: {0}").format(str(projPath)),
270
+ exc=self._storage.exc
271
+ )
260
272
  elif status == NWStorageOpen.LOCKED:
261
273
  self._state = NWProjectState.LOCKED
262
- elif status == NWStorageOpen.FAILED:
263
- SHARED.error(self.tr("Failed to open project."), exc=self._storage.exc)
264
274
  return False
265
275
 
266
276
  # Read Project XML
@@ -346,6 +356,7 @@ class NWProject:
346
356
  self.setProjectChanged(False)
347
357
  self._valid = True
348
358
  self._state = NWProjectState.READY
359
+ self._storage.lockSession() # Lock only after a successful open. See issue #1977.
349
360
 
350
361
  SHARED.newStatusMessage(self.tr("Opened Project: {0}").format(self._data.name))
351
362
 
@@ -121,7 +121,7 @@ class NWStatus:
121
121
  return key
122
122
 
123
123
  def update(self, update: list[tuple[str | None, StatusEntry]]) -> None:
124
- """Update the list of statuses, and from removed list."""
124
+ """Update the list of statuses."""
125
125
  self._store.clear()
126
126
  for key, entry in update:
127
127
  self._store[self._checkKey(key)] = entry
@@ -130,6 +130,9 @@ class NWStatus:
130
130
  if self._default not in self._store:
131
131
  self._default = next(iter(self._store)) if self._store else None
132
132
 
133
+ # Emit the change signal
134
+ SHARED.projectSingalProxy({"event": "statusLabels", "kind": self._prefix})
135
+
133
136
  return
134
137
 
135
138
  def check(self, value: str) -> str:
@@ -235,7 +235,6 @@ class NWStorage:
235
235
  if child.is_dir() and child.name.startswith("data_"):
236
236
  legacy.legacyDataFolder(basePath, child)
237
237
 
238
- self._writeLockFile()
239
238
  self._ready = True
240
239
 
241
240
  return NWStorageOpen.READY
@@ -249,6 +248,12 @@ class NWStorage:
249
248
  return True
250
249
  return True
251
250
 
251
+ def lockSession(self) -> None:
252
+ """Lock the session when the project is successfully opened."""
253
+ if self._ready:
254
+ self._writeLockFile()
255
+ return
256
+
252
257
  def closeSession(self) -> None:
253
258
  """Run tasks related to closing the session."""
254
259
  self._clearLockFile()