novelWriter 2.2b1__py3-none-any.whl → 2.2rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/METADATA +3 -3
  2. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/RECORD +60 -48
  3. novelwriter/__init__.py +3 -3
  4. novelwriter/assets/i18n/project_en_GB.json +1 -0
  5. novelwriter/assets/icons/novelwriter.ico +0 -0
  6. novelwriter/assets/icons/typicons_dark/icons.conf +8 -1
  7. novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
  8. novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
  9. novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
  10. novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
  11. novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
  12. novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
  13. novelwriter/assets/icons/typicons_light/icons.conf +8 -1
  14. novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
  15. novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
  16. novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
  17. novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
  18. novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
  19. novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
  20. novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
  21. novelwriter/assets/manual.pdf +0 -0
  22. novelwriter/assets/sample.zip +0 -0
  23. novelwriter/assets/text/release_notes.htm +4 -4
  24. novelwriter/common.py +22 -1
  25. novelwriter/config.py +12 -27
  26. novelwriter/constants.py +20 -3
  27. novelwriter/core/buildsettings.py +1 -1
  28. novelwriter/core/coretools.py +6 -1
  29. novelwriter/core/index.py +100 -34
  30. novelwriter/core/options.py +3 -0
  31. novelwriter/core/project.py +2 -2
  32. novelwriter/core/projectdata.py +1 -1
  33. novelwriter/core/tohtml.py +9 -3
  34. novelwriter/core/tokenizer.py +27 -20
  35. novelwriter/core/tomd.py +4 -0
  36. novelwriter/core/toodt.py +11 -4
  37. novelwriter/dialogs/preferences.py +80 -82
  38. novelwriter/dialogs/updates.py +25 -14
  39. novelwriter/enum.py +14 -4
  40. novelwriter/gui/doceditor.py +282 -177
  41. novelwriter/gui/dochighlight.py +7 -9
  42. novelwriter/gui/docviewer.py +142 -319
  43. novelwriter/gui/docviewerpanel.py +457 -0
  44. novelwriter/gui/editordocument.py +1 -1
  45. novelwriter/gui/mainmenu.py +16 -7
  46. novelwriter/gui/outline.py +10 -6
  47. novelwriter/gui/projtree.py +461 -376
  48. novelwriter/gui/sidebar.py +3 -3
  49. novelwriter/gui/statusbar.py +1 -1
  50. novelwriter/gui/theme.py +21 -2
  51. novelwriter/guimain.py +86 -32
  52. novelwriter/shared.py +23 -1
  53. novelwriter/tools/dictionaries.py +268 -0
  54. novelwriter/tools/manusbuild.py +17 -6
  55. novelwriter/tools/manuscript.py +1 -1
  56. novelwriter/tools/writingstats.py +1 -1
  57. novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
  58. novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
  59. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/LICENSE.md +0 -0
  60. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/WHEEL +0 -0
  61. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/entry_points.txt +0 -0
  62. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,268 @@
1
+ """
2
+ novelWriter – GUI Dictionary Downloader
3
+ =======================================
4
+
5
+ File History:
6
+ Created: 2023-11-19 [2.2rc1]
7
+
8
+ This file is a part of novelWriter
9
+ Copyright 2018–2023, Veronica Berglyd Olsen
10
+
11
+ This program is free software: you can redistribute it and/or modify
12
+ it under the terms of the GNU General Public License as published by
13
+ the Free Software Foundation, either version 3 of the License, or
14
+ (at your option) any later version.
15
+
16
+ This program is distributed in the hope that it will be useful, but
17
+ WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19
+ General Public License for more details.
20
+
21
+ You should have received a copy of the GNU General Public License
22
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+
28
+ from pathlib import Path
29
+ from zipfile import ZipFile
30
+
31
+ from PyQt5.QtGui import QCloseEvent, QTextCursor
32
+ from PyQt5.QtCore import pyqtSlot
33
+ from PyQt5.QtWidgets import (
34
+ QDialog, QDialogButtonBox, QFileDialog, QFrame, QHBoxLayout, QLabel,
35
+ QLineEdit, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget, qApp
36
+ )
37
+
38
+ from novelwriter import CONFIG, SHARED
39
+ from novelwriter.error import formatException
40
+ from novelwriter.common import openExternalPath, formatInt, getFileSize
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ class GuiDictionaries(QDialog):
46
+
47
+ def __init__(self, parent: QWidget) -> None:
48
+ super().__init__(parent=parent)
49
+
50
+ logger.debug("Create: GuiDictionaries")
51
+ self.setObjectName("GuiDictionaries")
52
+ self.setWindowTitle(self.tr("Add Dictionaries"))
53
+
54
+ self._installPath = None
55
+ self._currDicts = set()
56
+
57
+ iPx = CONFIG.pxInt(4)
58
+ mPx = CONFIG.pxInt(8)
59
+ sPx = CONFIG.pxInt(16)
60
+
61
+ self.setMinimumWidth(CONFIG.pxInt(500))
62
+ self.setMinimumHeight(CONFIG.pxInt(300))
63
+
64
+ # Hunspell Dictionaries
65
+ foUrl = "https://www.freeoffice.com/en/download/dictionaries"
66
+ loUrl = "https://extensions.libreoffice.org"
67
+ self.huInfo = QLabel("<br>".join([
68
+ self.tr("Download a dictionary from one of the links, and add it below."),
69
+ f"&nbsp;\u203a <a href='{foUrl}'>{foUrl}</a>",
70
+ f"&nbsp;\u203a <a href='{loUrl}'>{loUrl}</a>",
71
+ ]))
72
+ self.huInfo.setOpenExternalLinks(True)
73
+ self.huInfo.setWordWrap(True)
74
+ self.huInput = QLineEdit(self)
75
+ self.huBrowse = QPushButton(self)
76
+ self.huBrowse.setIcon(SHARED.theme.getIcon("browse"))
77
+ self.huBrowse.clicked.connect(self._doBrowseHunspell)
78
+ self.huImport = QPushButton(self.tr("Add Dictionary"), self)
79
+ self.huImport.setIcon(SHARED.theme.getIcon("add"))
80
+ self.huImport.clicked.connect(self._doImportHunspell)
81
+
82
+ self.huPathBox = QHBoxLayout()
83
+ self.huPathBox.addWidget(self.huInput)
84
+ self.huPathBox.addWidget(self.huBrowse)
85
+ self.huPathBox.setSpacing(iPx)
86
+ self.huAddBox = QHBoxLayout()
87
+ self.huAddBox.addStretch(1)
88
+ self.huAddBox.addWidget(self.huImport)
89
+
90
+ # Install Path
91
+ self.inInfo = QLabel(self.tr("Dictionary install location"))
92
+ self.inPath = QLineEdit(self)
93
+ self.inPath.setReadOnly(True)
94
+ self.inBrowse = QPushButton(self)
95
+ self.inBrowse.setIcon(SHARED.theme.getIcon("browse"))
96
+ self.inBrowse.clicked.connect(self._doOpenInstallLocation)
97
+
98
+ self.inBox = QHBoxLayout()
99
+ self.inBox.addWidget(self.inPath)
100
+ self.inBox.addWidget(self.inBrowse)
101
+ self.inBox.setSpacing(iPx)
102
+
103
+ # Info Box
104
+ self.infoBox = QPlainTextEdit(self)
105
+ self.infoBox.setReadOnly(True)
106
+ self.infoBox.setFixedHeight(4*SHARED.theme.fontPixelSize)
107
+ self.infoBox.setFrameStyle(QFrame.Shape.NoFrame)
108
+
109
+ # Buttons
110
+ self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
111
+ self.buttonBox.rejected.connect(self._doClose)
112
+
113
+ # Assemble
114
+ self.innerBox = QVBoxLayout()
115
+ self.innerBox.addWidget(self.huInfo)
116
+ self.innerBox.addLayout(self.huPathBox)
117
+ self.innerBox.addLayout(self.huAddBox)
118
+ self.innerBox.addSpacing(mPx)
119
+ self.innerBox.addWidget(self.inInfo)
120
+ self.innerBox.addLayout(self.inBox)
121
+ self.innerBox.addWidget(self.infoBox)
122
+ self.innerBox.setSpacing(iPx)
123
+
124
+ self.outerBox = QVBoxLayout()
125
+ self.outerBox.addLayout(self.innerBox, 0)
126
+ self.outerBox.addStretch(1)
127
+ self.outerBox.addWidget(self.buttonBox, 0)
128
+ self.outerBox.setSpacing(sPx)
129
+
130
+ self.setLayout(self.outerBox)
131
+
132
+ logger.debug("Ready: GuiDictionaries")
133
+
134
+ return
135
+
136
+ def __del__(self) -> None: # pragma: no cover
137
+ logger.debug("Delete: GuiDictionaries")
138
+ return
139
+
140
+ def initDialog(self) -> bool:
141
+ """Prepare and check that we can proceed."""
142
+ try:
143
+ import enchant
144
+ path = Path(enchant.get_user_config_dir())
145
+ except Exception:
146
+ logger.error("Could not get enchant path")
147
+ return False
148
+
149
+ self._installPath = Path(path).resolve()
150
+ if path.is_dir():
151
+ self.inPath.setText(str(path))
152
+ hunspell = path / "hunspell"
153
+ if hunspell.is_dir():
154
+ self._currDicts = set(
155
+ i.stem for i in hunspell.iterdir() if i.is_file() and i.suffix == ".aff"
156
+ )
157
+ self._appendLog(self.tr(
158
+ "Additional dictionaries found: {0}"
159
+ ).format(len(self._currDicts)))
160
+
161
+ qApp.processEvents()
162
+ self.adjustSize()
163
+
164
+ return True
165
+
166
+ ##
167
+ # Events
168
+ ##
169
+
170
+ def closeEvent(self, event: QCloseEvent) -> None:
171
+ """Capture the user closing the window."""
172
+ event.accept()
173
+ self.deleteLater()
174
+ return
175
+
176
+ ##
177
+ # Private Slots
178
+ ##
179
+
180
+ @pyqtSlot()
181
+ def _doBrowseHunspell(self):
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
+ ]
187
+ soxFile, _ = QFileDialog.getOpenFileName(
188
+ self, self.tr("Browse Files"), "", filter=";;".join(extFilter)
189
+ )
190
+ if soxFile:
191
+ path = Path(soxFile).absolute()
192
+ self.huInput.setText(str(path))
193
+ return
194
+
195
+ @pyqtSlot()
196
+ def _doImportHunspell(self):
197
+ """Import a hunspell dictionary from .sox or .oxt file."""
198
+ procErr = self.tr("Could not process dictionary file")
199
+ if self._installPath:
200
+ temp = self.huInput.text()
201
+ if temp and (path := Path(temp)).is_file():
202
+ hunspell = self._installPath / "hunspell"
203
+ hunspell.mkdir(exist_ok=True)
204
+ try:
205
+ nAff, nDic = self._extractDicts(path, hunspell)
206
+ if nAff == 0 or nDic == 0:
207
+ self._appendLog(procErr, err=True)
208
+ except Exception as exc:
209
+ self._appendLog(procErr, err=True)
210
+ self._appendLog(formatException(exc), err=True)
211
+ else:
212
+ self._appendLog(procErr, err=True)
213
+ return
214
+
215
+ @pyqtSlot()
216
+ def _doOpenInstallLocation(self) -> None:
217
+ """Open the dictionary folder."""
218
+ if not openExternalPath(Path(self.inPath.text())):
219
+ SHARED.error("Path not found.")
220
+ return
221
+
222
+ @pyqtSlot()
223
+ def _doClose(self) -> None:
224
+ """Close the dialog."""
225
+ self.close()
226
+ return
227
+
228
+ ##
229
+ # Internal Functions
230
+ ##
231
+
232
+ def _extractDicts(self, path: Path, output: Path) -> tuple[int, int]:
233
+ """Extract a zip archive and return the number of .aff and .dic
234
+ files found in it.
235
+ """
236
+ nAff = nDic = 0
237
+ with ZipFile(path, mode="r") as zipObj:
238
+ for item in zipObj.namelist():
239
+ zPath = Path(item)
240
+ if zPath.suffix not in (".aff", ".dic"):
241
+ continue
242
+ nAff += 1 if zPath.suffix == ".aff" else 0
243
+ nDic += 1 if zPath.suffix == ".dic" else 0
244
+ with zipObj.open(item) as zF:
245
+ oPath = output / zPath.name
246
+ oPath.write_bytes(zF.read())
247
+ size = getFileSize(oPath)
248
+ self._appendLog(self.tr(
249
+ "Added: {0} [{1}B]"
250
+ ).format(zPath.name, formatInt(size)))
251
+ return nAff, nDic
252
+
253
+ def _appendLog(self, text: str, err: bool = False) -> None:
254
+ """Append a line to the log output."""
255
+ cursor = self.infoBox.textCursor()
256
+ cursor.movePosition(QTextCursor.MoveOperation.End)
257
+ if cursor.position() > 0:
258
+ cursor.insertText("\n")
259
+ if err:
260
+ cursor.insertHtml(f"<font color='red'>{text}</font>")
261
+ else:
262
+ cursor.insertText(text)
263
+ cursor.movePosition(QTextCursor.MoveOperation.End)
264
+ cursor.deleteChar()
265
+ self.infoBox.setTextCursor(cursor)
266
+ return
267
+
268
+ # END Class GuiDictionaries
@@ -27,6 +27,7 @@ import logging
27
27
 
28
28
  from pathlib import Path
29
29
 
30
+ from PyQt5.QtGui import QCloseEvent
30
31
  from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
31
32
  from PyQt5.QtWidgets import (
32
33
  QAbstractButton, QAbstractItemView, QDialog, QDialogButtonBox, QFileDialog,
@@ -36,7 +37,7 @@ from PyQt5.QtWidgets import (
36
37
 
37
38
  from novelwriter import CONFIG, SHARED
38
39
  from novelwriter.enum import nwBuildFmt
39
- from novelwriter.common import makeFileNameSafe
40
+ from novelwriter.common import makeFileNameSafe, openExternalPath
40
41
  from novelwriter.constants import nwLabels
41
42
  from novelwriter.core.item import NWItem
42
43
  from novelwriter.core.docbuild import NWBuildDocument
@@ -175,9 +176,11 @@ class GuiManuscriptBuild(QDialog):
175
176
  self.buildBox.setVerticalSpacing(sp4)
176
177
 
177
178
  # Dialog Buttons
179
+ self.btnOpen = QPushButton(SHARED.theme.getIcon("browse"), self.tr("Open Folder"))
178
180
  self.btnBuild = QPushButton(SHARED.theme.getIcon("export"), self.tr("&Build"))
179
- self.dlgButtons = QDialogButtonBox(QDialogButtonBox.Close)
180
- self.dlgButtons.addButton(self.btnBuild, QDialogButtonBox.ActionRole)
181
+ self.dlgButtons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
182
+ self.dlgButtons.addButton(self.btnOpen, QDialogButtonBox.ButtonRole.ActionRole)
183
+ self.dlgButtons.addButton(self.btnBuild, QDialogButtonBox.ButtonRole.ActionRole)
181
184
 
182
185
  # Assemble GUI
183
186
  # ============
@@ -227,7 +230,7 @@ class GuiManuscriptBuild(QDialog):
227
230
 
228
231
  return
229
232
 
230
- def __del__(self): # pragma: no cover
233
+ def __del__(self) -> None: # pragma: no cover
231
234
  logger.debug("Delete: GuiManuscriptBuild")
232
235
  return
233
236
 
@@ -235,7 +238,7 @@ class GuiManuscriptBuild(QDialog):
235
238
  # Events
236
239
  ##
237
240
 
238
- def closeEvent(self, event):
241
+ def closeEvent(self, event: QCloseEvent) -> None:
239
242
  """Capture the user closing the window so we can save GUI
240
243
  settings.
241
244
  """
@@ -253,7 +256,10 @@ class GuiManuscriptBuild(QDialog):
253
256
  """Handle button clicks from the dialog button box."""
254
257
  role = self.dlgButtons.buttonRole(button)
255
258
  if role == QDialogButtonBox.ActionRole:
256
- self._runBuild()
259
+ if button == self.btnBuild:
260
+ self._runBuild()
261
+ elif button == self.btnOpen:
262
+ self._openOutputFolder()
257
263
  elif role == QDialogButtonBox.RejectRole:
258
264
  self.close()
259
265
  return
@@ -385,4 +391,9 @@ class GuiManuscriptBuild(QDialog):
385
391
 
386
392
  return
387
393
 
394
+ def _openOutputFolder(self):
395
+ """Open the build folder in the system's file explorer."""
396
+ openExternalPath(Path(self.buildPath.text()))
397
+ return
398
+
388
399
  # END Class GuiManuscriptBuild
@@ -507,7 +507,7 @@ class _DetailsWidget(QWidget):
507
507
 
508
508
  self._initExpanded = True
509
509
 
510
- # Tree Vidget
510
+ # Tree Widget
511
511
  self.listView = QTreeWidget(self)
512
512
  self.listView.setHeaderLabels([self.tr("Setting"), self.tr("Value")])
513
513
  self.listView.setIndentation(SHARED.theme.baseIconSize)
@@ -125,7 +125,7 @@ class GuiWritingStats(QDialog):
125
125
  pOptions.getInt("GuiWritingStats", "sortOrder", Qt.DescendingOrder),
126
126
  (Qt.AscendingOrder, Qt.DescendingOrder), Qt.DescendingOrder
127
127
  )
128
- self.listBox.sortByColumn(sortCol, sortOrder)
128
+ self.listBox.sortByColumn(sortCol, sortOrder) # type: ignore
129
129
  self.listBox.setSortingEnabled(True)
130
130
 
131
131
  # Word Bar
@@ -1,4 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
3
- <path d="m12 2c-5.5138 0-10 4.4862-10 10s4.4862 10 10 10c2.02 0 3.965-0.59875 5.6238-1.73 0.57-0.39 0.7175-1.1675 0.32875-1.7375-0.38875-0.57125-1.165-0.715-1.7375-0.32875-1.2425 0.84875-2.7 1.2962-4.215 1.2962-4.1362 0-7.5-3.3638-7.5-7.5s3.3638-7.5 7.5-7.5 7.5 3.3638 7.5 7.5v0.625c0 0.69-0.56 1.25-1.25 1.25s-1.25-0.56-1.25-1.25v-3.75c0-0.69125-0.55875-1.25-1.25-1.25-0.55125 0-1.0062 0.3625-1.1725 0.86-0.725-0.53375-1.6112-0.86-2.5775-0.86-2.4125 0-4.375 1.9625-4.375 4.375s1.9625 4.375 4.375 4.375c1.3062 0 2.4688-0.5875 3.27-1.4988 0.685 0.90375 1.76 1.4988 2.98 1.4988 2.0675 0 3.75-1.6825 3.75-3.75v-0.625c0-5.5138-4.4862-10-10-10zm0 11.875c-1.0338 0-1.875-0.84125-1.875-1.875s0.84125-1.875 1.875-1.875 1.875 0.84125 1.875 1.875-0.84125 1.875-1.875 1.875z" fill="#69c" stroke-width="1.25"/>
4
- </svg>
@@ -1,4 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
3
- <path d="m12 2c-5.5138 0-10 4.4862-10 10s4.4862 10 10 10c2.02 0 3.965-0.59875 5.6238-1.73 0.57-0.39 0.7175-1.1675 0.32875-1.7375-0.38875-0.57125-1.165-0.715-1.7375-0.32875-1.2425 0.84875-2.7 1.2962-4.215 1.2962-4.1362 0-7.5-3.3638-7.5-7.5s3.3638-7.5 7.5-7.5 7.5 3.3638 7.5 7.5v0.625c0 0.69-0.56 1.25-1.25 1.25s-1.25-0.56-1.25-1.25v-3.75c0-0.69125-0.55875-1.25-1.25-1.25-0.55125 0-1.0062 0.3625-1.1725 0.86-0.725-0.53375-1.6112-0.86-2.5775-0.86-2.4125 0-4.375 1.9625-4.375 4.375s1.9625 4.375 4.375 4.375c1.3062 0 2.4688-0.5875 3.27-1.4988 0.685 0.90375 1.76 1.4988 2.98 1.4988 2.0675 0 3.75-1.6825 3.75-3.75v-0.625c0-5.5138-4.4862-10-10-10zm0 11.875c-1.0338 0-1.875-0.84125-1.875-1.875s0.84125-1.875 1.875-1.875 1.875 0.84125 1.875 1.875-0.84125 1.875-1.875 1.875z" fill="#4271ae" stroke-width="1.25"/>
4
- </svg>