From 5e57f02fb6886c3c58597eb7778bd06139d597c6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 1 Oct 2020 23:38:01 +0200 Subject: [PATCH 001/104] Bumped version to 1.0-beta4 --- docs/source/conf.py | 2 +- nw/__init__.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index afeadcd5..b72b8db3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -28,7 +28,7 @@ author = "Veronica Berglyd Olsen" # The short X.Y version version = "1.0" # The full version, including alpha/beta/rc tags -release = "1.0-beta3" +release = "1.0-beta4" # -- General configuration --------------------------------------------------- diff --git a/nw/__init__.py b/nw/__init__.py index aa755a2e..234ca2b7 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -39,9 +39,9 @@ __package__ = "nw" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen" __license__ = "GPLv3" -__version__ = "1.0b3" -__hexversion__ = "0x010000b3" -__date__ = "2020-09-20" +__version__ = "1.0b4" +__hexversion__ = "0x010000b4" +__date__ = "2020-10-11" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" __status__ = "Beta" From 381370079bdc4af7a9f22b7184b0c2c34aed96f5 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 2 Oct 2020 00:02:24 +0200 Subject: [PATCH 002/104] Updated changelog --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d73e598..50bc1aa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # novelWriter ChangeLog +## Version 1.0 Beta 4 [2020-10-11] + +**Bugfixes** + +* When the Trash folder doesn't exist because nothing has yet been deleted, the lookup function for the Trash folder's handle returns `None`. That meant that any item with a parent handle `None` would be treated as a Trash folder in many parts of the code before an actual Trash folder existed. This caused a few decision branches to make non-critical mistakes. This issue is now fixed with a new check function that takes this into account. PRs #452 and #453. +* If an older project was opened, one with a different project file layout than the more recent versions, a dialog asks whether the user wants the project updated or not. However, the function that moves files to the new location actually starts working before the dialog asks for permission. Instead, it just checks that it is allowed to change the project XML file only. The check is still run before the dialog, but the action of moving files around are now postponed to after the permission has been given and the project XML file parsed. PR #453. +* If there were multiple headings in a file, and the last paragraph did not end in a line break, the word counter for the individual sections would miss the last paragraph of the last section due to an index error. This has now been fixed. PR #453. + +**User Interface** + +* Minor changes to the text formatting on the Recent Projects dialog. PR #452. + +**Other Changes** + +* The command line switches `--quiet` and `--logfile=` have been removed. They were intended for testing, but have never been used. The default mode of only printing warnings and errors is quiet enough, and logging to file shouldn't be necessary for a GUI application. PR #453. +* A number of if statements and conditions in the code that were intended to alter behaviour when running tests, mostly to stop modal dialogs from blocking the main thread, have been removed. The changes to the program flow when running tests have now been reduced to a minimum, and modifications instead handled with pytest monkeypatches. PR #453. + +**Test Suite** + +* Major additions to the test suite taking the test coverage to 91%. PR #453. +* Test coverage for Linux (Ubuntu) for Python versions 3.6, 3.7, and 3.8 are now separate jobs. In addition, Windows with Python 3.8 and macOS with Python 3.8 is also tested. All OSes are piped into test coverage, and they all have status badges. PRs #453 and #454. + + ## Version 1.0 Beta 3 [2020-09-20] **Bugfixes** From e8383d1850bfb2bf38e924abdea70bbe7eb62237 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 4 Oct 2020 20:32:55 +0200 Subject: [PATCH 003/104] Fix spell checking for words with a dash --- nw/gui/dochighlight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 7b95ffeb..9a8fcd46 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -198,7 +198,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): # Build a QRegExp for spell checker # Include additional characters that the highlighter should # consider to be word separators - wordSep = r"_\+/" + wordSep = r"-_\+/" wordSep += nwUnicode.U_ENDASH wordSep += nwUnicode.U_EMDASH self.spellRx = QRegularExpression(r"\b[^\s"+wordSep+r"]+\b") @@ -314,7 +314,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): while rxSpell.hasNext(): rxMatch = rxSpell.next() if not self.theDict.checkWord(rxMatch.captured(0)): - if rxMatch.captured(0) == rxMatch.captured(0).upper(): + if rxMatch.captured(0).isupper(): continue xPos = rxMatch.capturedStart(0) xLen = rxMatch.capturedLength(0) From ac96636f8a2723b05fd82dde4bba0f5e9ee97d98 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 4 Oct 2020 20:46:38 +0200 Subject: [PATCH 004/104] Add dictionary information function to the spell check classes --- nw/core/spellcheck.py | 30 +++++++++++++++++++++++++++++- tests/test_project.py | 8 ++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index 02d0f1b3..d2c19784 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -87,6 +87,11 @@ class NWSpellCheck(): """ return [] + def describeDict(self): + """Dummy function. + """ + return "", "" + @staticmethod def expandLanguage(spTag): """Translate a language tag to something more user friendly. @@ -187,6 +192,21 @@ class NWSpellEnchant(NWSpellCheck): logger.error("Failed to list languages for enchant spell checking") return retList + def describeDict(self): + """Return the tag and provider of the currently loaded + dictionary. + """ + try: + spTag = self.theDict.tag + spName = self.theDict.provider.name + except Exception as e: + logger.error("Failed to extract information about the dictionary") + logger.error(str(e)) + spTag = "" + spName = "" + + return spTag, spName + # END Class NWSpellEnchant class NWSpellEnchantDummy: @@ -221,12 +241,14 @@ class NWSpellSimple(NWSpellCheck): def __init__(self): NWSpellCheck.__init__(self) + self.theLang = "" logger.debug("Simple spell checking activated") return def setLanguage(self, theLang, projectDict=None): """Load a dictionary as a list from the app assets folder. """ + self.theLang = theLang self.WORDS = [] dictFile = os.path.join(self.mainConf.dictPath, theLang+".dict") try: @@ -305,9 +327,15 @@ class NWSpellSimple(NWSpellCheck): if theBits[1] != ".dict": continue - spName = "%s [Internal]" % self.expandLanguage(theBits[0]) + spName = "%s [internal]" % self.expandLanguage(theBits[0]) retList.append((theBits[0], spName)) return retList + def describeDict(self): + """Return the tag and provider of the currently loaded + dictionary. + """ + return self.theLang, "internal" + # END Class NWSpellSimple diff --git a/tests/test_project.py b/tests/test_project.py index 21423ffe..282acbc7 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -377,6 +377,10 @@ def testSpellEnchant(nwTemp, nwConf): dList = spChk.listDictionaries() assert len(dList) > 0 + aTag, aName = spChk.describeDict() + assert aTag == "en" + assert aName != "" + @pytest.mark.project def testSpellSimple(nwTemp, nwConf): wList = os.path.join(nwTemp, "wordlist.txt") @@ -402,6 +406,10 @@ def testSpellSimple(nwTemp, nwConf): dList = spChk.listDictionaries() assert len(dList) > 0 + aTag, aName = spChk.describeDict() + assert aTag == "en" + assert aName == "internal" + @pytest.mark.project def testProjectOptions(nwDummy, nwLipsum): """Test the class that holds all the GUI state user options that are From 96a5b9e4aefaec875e0595ce8ba5bcf61fa3edfa Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 4 Oct 2020 21:21:30 +0200 Subject: [PATCH 005/104] More descriptive spell check tooltip --- nw/gui/doceditor.py | 8 ++++++-- nw/gui/dochighlight.py | 4 ++-- nw/gui/statusbar.py | 6 +++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 1410c7b4..e231a1bb 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -455,7 +455,11 @@ class GuiDocEditor(QTextEdit): theLang = self.theProject.projLang self.theDict.setLanguage(theLang, self.theProject.projDict) - self.theParent.statusBar.setLanguage(self.theDict.spellLanguage) + + aLang, aName = self.theDict.describeDict() + self.theParent.statusBar.setLanguage( + aLang, "%s/%s" % (self.mainConf.spellTool, aName) + ) if not self.bigDoc: self.spellCheckDocument() @@ -802,7 +806,7 @@ class GuiDocEditor(QTextEdit): mnuHead = QAction("Spelling Suggestion(s)", mnuContext) mnuContext.addAction(mnuHead) - theSuggest = self.theDict.suggestWords(theWord) + theSuggest = self.theDict.suggestWords(theWord)[:15] if len(theSuggest) > 0: for aWord in theSuggest: mnuWord = QAction("%s %s" % (nwUnicode.U_ENDASH, aWord), mnuContext) diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 9a8fcd46..05266b91 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -198,7 +198,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): # Build a QRegExp for spell checker # Include additional characters that the highlighter should # consider to be word separators - wordSep = r"-_\+/" + wordSep = r"\-_\+/" wordSep += nwUnicode.U_ENDASH wordSep += nwUnicode.U_EMDASH self.spellRx = QRegularExpression(r"\b[^\s"+wordSep+r"]+\b") @@ -314,7 +314,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): while rxSpell.hasNext(): rxMatch = rxSpell.next() if not self.theDict.checkWord(rxMatch.captured(0)): - if rxMatch.captured(0).isupper(): + if rxMatch.captured(0).isupper() or rxMatch.captured(0).isnumeric(): continue xPos = rxMatch.capturedStart(0) xLen = rxMatch.capturedLength(0) diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index 914fedae..c43c3f6f 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -152,13 +152,17 @@ class GuiMainStatus(QStatusBar): qApp.processEvents() return - def setLanguage(self, theLanguage): + def setLanguage(self, theLanguage, theProvider=""): """Set the language code for the spell checker. """ if theLanguage is None: self.langText.setText("None") + self.langText.setToolTip("") else: self.langText.setText(NWSpellCheck.expandLanguage(theLanguage)) + self.langText.setToolTip( + "Provider: %s" % (theProvider if theProvider else "unknown") + ) return def setProjectStatus(self, isChanged): From 0c1aaecd78a4a895b26e7926fca5bff9b9fc14e0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 7 Oct 2020 15:04:13 +0200 Subject: [PATCH 006/104] Qt threading done right (thread pool for word counter) --- nw/gui/doceditor.py | 81 ++++++++++++++++++++++++--------------------- nw/guimain.py | 3 +- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 72fd818e..6b21f11d 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -12,6 +12,7 @@ Created: 2020-04-25 [0.4.5] GuiDocEditHeader Rewritten: 2020-06-15 [0.9.0] GuiDocEditSearch Created: 2020-06-27 [0.10.0] GuiDocEditFooter + Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter This file is a part of novelWriter Copyright 2018–2020, Veronica Berglyd Olsen @@ -36,7 +37,8 @@ import logging from time import time from PyQt5.QtCore import ( - Qt, QSize, QThread, QTimer, pyqtSlot, QRegExp, QRegularExpression, QPointF + Qt, QSize, QTimer, pyqtSlot, pyqtSignal, QRegExp, QRegularExpression, + QPointF, QObject, QRunnable ) from PyQt5.QtGui import ( QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette, @@ -135,14 +137,15 @@ class GuiDocEditor(QTextEdit): activated=self._followTag ) - # Set Up Word Count Thread and Timer + # Set Up Word Counter self.wcInterval = self.mainConf.wordCountTimer self.wcTimer = QTimer() self.wcTimer.setInterval(int(self.wcInterval*1000)) self.wcTimer.timeout.connect(self._runCounter) self.wCounter = BackgroundWordCounter(self) - self.wCounter.finished.connect(self._updateCounts) + self.wCounter.setAutoDelete(False) + self.wCounter.signals.countsReady.connect(self._updateCounts) self.initEditor() @@ -912,21 +915,18 @@ class GuiDocEditor(QTextEdit): """Decide whether to run the word counter, or stop the timer due to inactivity. """ - sinceActive = time()-self.lastEdit - if sinceActive > 5*self.wcInterval: - logger.debug( - "Stopping word count timer: no activity last %.1f seconds" % sinceActive - ) - self.wcTimer.stop() - elif self.wCounter.isRunning(): - logger.verbose("Word counter thread is busy") - else: - logger.verbose("Starting word counter") - self.wCounter.start() + if self.wCounter.isRunning(): + logger.verbose("Word counter is busy") + return + + if time() - self.lastEdit < 5*self.wcInterval: + logger.verbose("Running word counter") + self.theParent.threadPool.start(self.wCounter) + return - @pyqtSlot() - def _updateCounts(self): + @pyqtSlot(int, int, int) + def _updateCounts(self, cCount, wCount, pCount): """Slot for the word counter's finished signal """ theItem = self.nwDocument.getCurrentItem() @@ -935,19 +935,17 @@ class GuiDocEditor(QTextEdit): logger.verbose("Updating word count") - self.charCount = self.wCounter.charCount - self.wordCount = self.wCounter.wordCount - self.paraCount = self.wCounter.paraCount - theItem.setCharCount(self.charCount) - theItem.setWordCount(self.wordCount) - theItem.setParaCount(self.paraCount) + self.charCount = cCount + self.wordCount = wCount + self.paraCount = pCount + theItem.setCharCount(cCount) + theItem.setWordCount(wCount) + theItem.setParaCount(pCount) - self.theParent.treeView.propagateCount(self.theHandle, self.wordCount) + self.theParent.treeView.propagateCount(self.theHandle, wCount) self.theParent.treeView.projectWordCount() - self.theParent.treeMeta.updateCounts( - self.theHandle, self.charCount, self.wordCount, self.paraCount - ) - self._checkDocSize(self.charCount) + self.theParent.treeMeta.updateCounts(self.theHandle, cCount, wCount, pCount) + self._checkDocSize(self.qDocument.characterCount()) self.docFooter.updateCounts() return @@ -1521,33 +1519,42 @@ class GuiDocEditor(QTextEdit): # END Class GuiDocEditor # =============================================================================================== # -# The Off GUI Thread Word Counter -# Runs the word counter in the background for the DocEditor +# The Off-GUI Thread Word Counter +# A runnable for the word counter to be run in the thread pool off the main GUI thread. # =============================================================================================== # -class BackgroundWordCounter(QThread): +class BackgroundWordCounter(QRunnable): def __init__(self, docEditor): - QThread.__init__(self, docEditor) + QRunnable.__init__(self) self.docEditor = docEditor - self.charCount = 0 - self.wordCount = 0 - self.paraCount = 0 + self.signals = BackgroundWordCounterSignals() + self._isRunning = False return + def isRunning(self): + return self._isRunning + + @pyqtSlot() def run(self): """Overloaded run function for the word counter, forwarding the call to the function that does the actual counting. """ + self._isRunning = True theText = self.docEditor.getText() cC, wC, pC = countWords(theText) - self.charCount = cC - self.wordCount = wC - self.paraCount = pC + self.signals.countsReady.emit(cC, wC, pC) + self._isRunning = False return ## END Class BackgroundWordCounter +class BackgroundWordCounterSignals(QObject): + + countsReady = pyqtSignal(int, int, int) + +# END Class BackgroundWordCounterSignals + # =============================================================================================== # # The Embedded Document Search/Replace Feature # Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport diff --git a/nw/guimain.py b/nw/guimain.py index 0cdef1ef..1aa89198 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -32,7 +32,7 @@ import os from datetime import datetime from time import time -from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtCore import Qt, QTimer, QThreadPool from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence, QCursor from PyQt5.QtWidgets import ( qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut, @@ -60,6 +60,7 @@ class GuiMain(QMainWindow): logger.debug("Initialising GUI ...") self.setObjectName("GuiMain") self.mainConf = nw.CONFIG + self.threadPool = QThreadPool() # Some runtime info useful for debugging logger.info("OS: %s" % self.mainConf.osType) From 357a407116b1bdf273716c88add500d794ee4c03 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 7 Oct 2020 15:51:12 +0200 Subject: [PATCH 007/104] Fix the main menu look on macOS --- nw/__init__.py | 10 ++++++++++ nw/gui/mainmenu.py | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/nw/__init__.py b/nw/__init__.py index 5aa259e9..f65d6d20 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -241,6 +241,16 @@ def main(sysArgs=None): # Finish initialising config CONFIG.initConfig(confPath, dataPath) + if CONFIG.osDarwin: + try: + from Foundation import NSBundle + bundle = NSBundle.mainBundle() + info = bundle.localizedInfoDictionary() or bundle.infoDictionary() + info["CFBundleName"] = "novelWriter" + except ImportError as e: + logger.error("Failed to set application name") + logger.error(str(e)) + # Import GUI (after dependency checks), and launch from nw.guimain import GuiMain if testMode: diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 6e8e8cf7..6bb7edf1 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -276,6 +276,7 @@ class GuiMainMenu(QMenuBar): self.aExitNW = QAction("Exit", self) self.aExitNW.setStatusTip("Exit %s" % self.mainConf.appName) self.aExitNW.setShortcut("Ctrl+Q") + self.aExitNW.setMenuRole(QAction.QuitRole) self.aExitNW.triggered.connect(lambda: self.theParent.closeMain()) self.projMenu.addAction(self.aExitNW) @@ -843,6 +844,7 @@ class GuiMainMenu(QMenuBar): self.aPreferences = QAction("Preferences", self) self.aPreferences.setStatusTip("Preferences") self.aPreferences.setShortcut("Ctrl+,") + self.aPreferences.setMenuRole(QAction.PreferencesRole) self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog()) self.toolsMenu.addAction(self.aPreferences) @@ -857,12 +859,14 @@ class GuiMainMenu(QMenuBar): # Help > About self.aAboutNW = QAction("About %s" % self.mainConf.appName, self) self.aAboutNW.setStatusTip("About %s" % self.mainConf.appName) + self.aAboutNW.setMenuRole(QAction.AboutRole) self.aAboutNW.triggered.connect(lambda: self.theParent.showAboutNWDialog()) self.helpMenu.addAction(self.aAboutNW) # Help > About Qt5 self.aAboutQt = QAction("About Qt5", self) self.aAboutQt.setStatusTip("About Qt5") + self.aAboutQt.setMenuRole(QAction.AboutQtRole) self.aAboutQt.triggered.connect(lambda: self.theParent.showAboutQtDialog()) self.helpMenu.addAction(self.aAboutQt) From 87baabeaa5989263eb7dd719c1862ea7e2dd6059 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 7 Oct 2020 18:38:43 +0200 Subject: [PATCH 008/104] Minor fixes to dialogs, partially related to macOS --- nw/gui/build.py | 3 +++ nw/gui/preferences.py | 2 +- nw/gui/writingstats.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index 6e571959..eb7a88e3 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -915,6 +915,9 @@ class GuiBuildNovel(QDialog): if theStatus: self.textFont.setText(theFont.family()) self.textSize.setValue(theFont.pointSize()) + + self.raise_() # Move the dialog to front (fixes a bug on macOS) + return def _loadCache(self): diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index ea475b70..a946c289 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -340,7 +340,7 @@ class GuiConfigEditGeneralTab(QWidget): dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.ShowDirsOnly dlgOpt |= QFileDialog.DontUseNativeDialog - newDir = QFileDialog.getExistingDirectory( + newDir = QFileDialog.getExistingDirectory( self, "Backup Directory", currDir, options=dlgOpt ) if newDir: diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index c467615c..f748f95e 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -337,7 +337,7 @@ class GuiWritingStats(QDialog): saveTo = QFileDialog.getSaveFileName( self, "Save Document As", savePath, options=dlgOpt ) - if saveTo: + if saveTo[0]: savePath = saveTo[0] else: return False From 48742ceb0e94fa9fbc4be001a038bcfe8576907d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 7 Oct 2020 19:01:03 +0200 Subject: [PATCH 009/104] Updated readme with more install information --- README.md | 384 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 202 insertions(+), 182 deletions(-) diff --git a/README.md b/README.md index a5e6aea7..9048ffe9 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ novelWriter is a Markdown-like text editor designed for writing novels and larger projects of many smaller plain text documents. It uses its own flavour of Markdown that supports a meta data syntax -for comments, synopsis and cross-referencing between files. It's designed to be a simple text editor +for comments, synopsis, and cross-referencing between files. It's designed to be a simple text editor that allows for easy organisation of text files and notes, built on plain text files for robustness. @@ -29,19 +29,6 @@ The full documentation is available at [novelwriter.readthedocs.io](https://nove The contributing guide is available in [CONTRIBUTING](CONTRIBUTING.md). -### Note on the Default Branch - -The default branch on this repository switched to `main` on 6. August 2020. If you are running -novelWriter from a git clone, you need to clone the repository again. - -Alternatively, you can run the following to get back on the new default branch: - -```bash -git remote update -git checkout -t origin/main -``` - - ### Development Status The application is still under initial development, but all core features have now been added. The @@ -49,15 +36,213 @@ core functionality has been in place for a while, and novelWriter is being used by the author and collaborators. No new major features will be added at this time, until the application is stable. Until then, -novelWriter is in a _beta_ state. Please report any issues you may encounter in the repository issue +novelWriter is in a _beta_ state. Please report any issues you encounter via the repository's issue tracker. You should be able to use novelWriter for real projects, but as with all software, please make regular backups of your data. There is a built in backup feature that can pack the entire project -into a zip file on close. Please check the documentation for further details. +into a zip file each time the main window or the project is closed. Please check the documentation +for further details. -## License +## Implementation + +The application is written in Python 3 using Qt5 via PyQt5. It is developed on Linux, but it should +in principle work fine on other operating systems as well as long as dependencies are met. The unit +tests are run on the latest versions of Ubuntu Linux, Windows Server and macOS. + + +## Installing and Running + +You can runt novelWriter either from a downloaded copy of the source code, or by running: +```bash +pip install novelwriter +``` +**Note:** On some systems you must use `pip3` instead for the Python 3 version. + +You can update novelWriter to the latest version by running: +```bash +pip install --upgrade novelwriter +``` + +The application can then be started with one of the commands, depending on your Python configuration: +```bash +./novelWriter.py +python novelWriter.py +python3 novelWriter.py +``` + +It also takes a few parameters for debugging and such, which can be listed with the switch `--help`. +The `--info`, `--debug` or `--verbose` flags are particularly useful for increasing logging output +for debugging. + +You can also provide a path to a folder containing a novelWriter project as the last parameter. + + +### Launcher and Icons + +In the root assets folder there are icons and scripts and a template for setting up a launcher on +Gnome desktops. You may need to modify those scripts slightly, but as they are, they work on Debian +and Ubuntu. For other operating systems, please consult your operating system documentation for how +to make those. Feel free to submit more if you are able to make them. + + +## Package Dependencies + +It is recommended that novelWriter runs with Qt 5.10 or later, and Python 3.6 or later. Running with +Qt as low as 5.2.1 and Python 3.4.3 has been tested, and worked in the past, but there are no +guarantees that this will keep working as these are not a part of the test builds. + + +### Linux + +Generally, dependencies can be installed via `pip` with: +```bash +pip3 install -r requirements.txt +``` + +You can also install the packages from the distro's own package manager. +For the apt package manager on Debian/Ubuntu systems, the following Python3 packages are needed: + +* `python3-pyqt5` for the GUI +* `python3-lxml` for writing project files +* `python3-enchant` for better spell checking (optional) + + +### macOS + +These instructions assume you're using brew, and have Python and pip set up. +If not, see the [brew docs](https://docs.brew.sh/Homebrew-and-Python) for help. + +Main requirements are installed via the requirements file. +You also need to install the `pyobjc` package on macOS, so you must run: +```bash +pip3 install --user -r requirements.txt +pip3 install --user pyobjc +``` + +For spell checking you may also need to install the enchant package. +It comes with a lot of default dictionaries. +```bash +brew install enchant +``` + + +### Windows + +On Windows, the `pip install` command is generally sufficient to install everything you need. +That should also install the Qt libraries and the spell check dictionary dependencies. + +**Note:** On Windows, make sure Python3 is in your PATH if you want to launch novelWriter from +command line. You can also right click the `novelWriter.py` file, create a shortcut, then right +click again, select "Properties" and change the target to your python executable and +`novelWriter.py`. + +It should look something like this: +``` +C:\...\AppData\Local\Programs\Python\Python38\python.exe novelWriter.py +``` + + +### Package Versions + +PyQt/Qt should be at least 5.3, but ideally 5.10 or higher for nearly all features to work. +Exporting to Markdown requires PyQt/Qt 5.14. There are no known minimum for `lxml`, but the code +was originally written with 4.2. The optional spell check library must be at least 3.0.0 to work +with Windows 64 bit systems. On Linux, 2.0.0 also works fine. + +If no external spell checking tool is installed, novelWriter will use a basic spell checker based on +standard Python package `difflib`. Currently, only English dictionaries are available for this spell +checker, but more can be added to the `nw/assets/dict` folder. See the [README](nw/assets/dict/README.md) +file in that folder for how to generate more dictionaries. Note that the difflib-based option is +both slow and limited. + + +## Key Features + +Some features of novelWriter are listed below. Consult the documentation for more information. + +### Markdown Flavour + +novelWriter is _not_ a full-feature Markdown editor. It allows for a minimal set of formatting +needed for writing text documents for novels. These are currently limited to: + +* Headings level 1 to 4 using the `#` syntax only. +* Emphasised and strong text. These are rendered as italicised and bold. +* Strikethrough text. +* Hard line breaks using two or more spaces at the end of a line. + +That is it. Features not supported in the editor are also not exported when using the export tool. + +In addition, novelWriter adds the following, which is otherwise not supported by Markdown: + +* A line starting with `%` is treated as a comment and not rendered on exports unless requested. + Comments do not count towards the word count. If the first word of the comment is `synopsis:`, the + comment is indexed and treated as the synopsis for the section of text under the same header. + These synopsis comments can be used to build an outline and exported to external documents. +* A set of meta data keyword/values starting with the character `@`. This is used for tagging + and inter-linking documents, and can be used to generate a project outline. +* Non-breaking spaces are supported as long as your system is using at least Qt 5.9. For earlier + version, non-breaking spaces are converted to normal spaces when saving the document. This is done + by the Qt library. +* Thin spaces are also supported, as well as non-breaking thin spaces, with the same library version + restriction as above. +* Tabs can be used in the text, and should be properly aligned. The width of a tab in pixels can be + changed in Preferences. Note that for the HTML format, most browsers will treat a tab as a space, + so it may not show up like expected. If you import the HTML file to Libre Office, for instance, + they should appear as expected. + +The core export format of novelWriter is HTML5. You can also export the entire project as a single +novelWriter Markdown-flavour document. In addition, other exports to Open Document, PDF, and plain +text is offered through the Qt library, although with limitations to formatting. + +The HTML format is well suited for file conversion tools and import into other text editors. + + +### Colour Themes + +The editor has syntax highlighting for the features it supports, and includes a set of different +syntax highlighting themes. The GUI also has an optional dark theme in addition to the default +system theme. + +New themes can easily be added to the `nw/assets/themes` folder. Have a look in the existing folders +for examples of how to define the colours. + + +### Easy Organising of Project Files + +The structure of the project is shown on the left hand side of the main GUI. Project files are +organised into root folders, indicating what class of file they are. The most important root folder +is the Novel folder, which contains all of the files that makes up the finished novel. Each root +folder can have subfolders. Folders have no impact on the final project structure, they are purely +tools for organising the files in whatever way the user needs. + +The editor supports four levels of headings, which determines what level the following text belongs +to. Headings of level one signify a book or partition title. Headings of level two signify the start +of a new chapter. Headings of level three signify the start of a new scene. Headings of level four +can be used internally in each scene to separate sections. + +Each novel file can be assigned a layout format, which shows up as a flag next to the item in the +project tree. These are mostly to help the user track what they contain, but they also have some +impact on the format of the exported document. See the documentation for further details. + + +#### Project Notes + +Supporting note files can be added for the story plot, characters, locations, story timeline, etc. +These have their separate root folders. These are optional files. + + +### Visualisation of Story Elements + +The different notes can be assigned tags, which other files can refer back to using the `@` meta +keywords. This information can be used to display an outline of the story, showing where each scene +connects to the plot, and which characters, etc. occur in them. In addition, the tags themselves are +clickable in the document view pane, and control-clickable in the editor. They make it possible to +quickly navigate between the documents while editing. + + +## Licenses This is Open Source software, and novelWriter is licensed under GPLv3. See the [GNU General Public License website](https://www.gnu.org/licenses/gpl-3.0.en.html) for more details, @@ -79,171 +264,6 @@ Bundled assets have the following licenses: main repo is available at [sdras/night-owl-vscode-theme](https://github.com/sdras/night-owl-vscode-theme). -## Markdown Flavour - -novelWriter is _not_ a full-feature Markdown editor. It allows for a minimal set of formatting -needed for writing text documents for novels. These are currently limited to: - -* Headings level 1 to 4 using the `#` syntax only. -* Emphasised and strong text. These are rendered as italicised and bold. -* Strikethrough text. -* Hard line breaks using two or more spaces at the end of a line. - -That is it. Features not supported in the editor are also not exported when using the export tool. - -In addition, novelWriter adds the following, which is otherwise not supported by Markdown: - -* A line starting with `%` is treated as a comment and not rendered on exports unless requested. - Comments do not count towards the word count. If the first word of the comment is `synopsis:`, the - comment is indexed and treated as the synopsis for the following section of text. These synopsis - comments can be used to build an outline and exported to external documents. -* A set of meta data keyword/value sets starting with the character `@`. This is used for tagging - and inter-linking documents. -* Non-breaking spaces are supported as long as your system is using at least Qt 5.9. For earlier - version, non-breaking spaces are converted to normal spaces when saving the document. This is done - by the Qt library. -* Thin spaces are also supported, as well as non-breaking thin spaces. -* Tabs can be used in the text, and should be properly aligned. The width of a tab in pixels can be - changed in Preferences. Note that tabs are exported as-is, also to HTML format. However, most - browsers will treat a tab as a space, so it may not show up like expected if you view the exported - HTML file. - -The core export format of novelWriter is HTML5. You can also export the entire project as a single -novelWriter Markdown-flavour document. In addition, other exports to Open Document, PDF, and plain -text is offered through the Qt library, although with limitations to formatting. - - -## Implementation - -The application is written in Python3 using Qt5 via PyQt5. It is developed on Linux, but it should -in principle work fine on other operating systems as well, as long as dependencies are met. It is -regularly tested on Windows 10. - -The application can be started from the source folder with one of the commands, depending on your -Python configuration: -```bash -./novelWriter.py -python novelWriter.py -python3 novelWriter.py -``` - -It also takes a few parameters for debugging and such, which can be listed with the switch `--help`. - -In the root assets folder there are icons and scripts and a template for setting up a launcher on -Gnome desktops. You may need to modify those scripts slightly, but as they are, they work on Debian -and Ubuntu. For other operating systems, please consult your operating system documentation for how -to make those. Feel free to submit more if you are able to make them. - - -## Package Dependencies - -It is recommended that novelWriter runs with Qt 5.10 or later, and Python 3.6 or later. Running with -Qt as low as 5.2.1 and Python 3.4.3 has been tested, and worked in the past, but there are no -guarantees that this will keep working as these are not a part of the test builds. - -For the apt package manager on Debian/Ubuntu systems, the following Python3 packages are needed: - -* `python3-pyqt5` for the GUI -* `python3-lxml` for writing project files - -These are optional, but recommended: - -* `python3-enchant` for better spell checking - -Alternatively, the packages can be installed with `pip` by running -```bash -pip install -r requirements.txt -``` - -in the application folder. - -You can also do them one at a time, skipping the ones you don't need: -```bash -pip install pyqt5 -pip install lxml -pip install pyenchant -``` - -PyQt/Qt should be at least 5.3, but ideally 5.10 or higher for nearly all features to work. -Exporting to Markdown requires PyQt/Qt 5.14. There are no known minimum for `lxml`, but the code -was originally written with 4.2. The optional spell check library must be at least 3.0.0 to work -with Windows 64 bit systems. On Linux, 2.0.0 also works fine. - -If no external spell checking tool is installed, novelWriter will use a basic spell checker based on -standard Python package `difflib`. Currently, only English dictionaries are available for this spell -checker, but more can be added to the `nw/assets/dict` folder. See the [README](nw/assets/dict/README.md) -file in that folder for how to generate more dictionaries. Note that the difflib-based option is -both slow and limited. - -Note: On Windows, make sure Python3 is in your PATH if you want to launch novelWriter from command -line. You can also right click the `novelWriter.py` file, create a shortcut, then right click again, -select "Properties" and change the target to your python executable and `novelWriter.py`. - -It should look something like this: -``` -C:\...\AppData\Local\Programs\Python\Python38\python.exe novelWriter.py -``` - -## Key Features - -Some features of novelWriter are listed below. Consult the documentation for more information. - - -### Colour Themes - -The editor has syntax highlighting for the features it supports, and includes a set of different -syntax highlighting themes. The GUI also has an optional dark theme in addition to the default -system theme. - -New themes can easily be added to the `nw/assets/themes` folder. Have a look in the existing folders -for examples of how to define the colours. - - -### Auto-Saving and Document Stats - -Open documents and the project file itself is saved regularly on a timer. The status of this is -indicated by two indicators on the right hand side of the status bar. Latest project word count is -shown next to these indicators in the status bar. The counts are updated regularly, but not -as-you-type. - -The word count for documents is presented in a footer in the document editor itself. Both project -and document word counters will also show how many words you've added in the current writing -session. - - -### Easy Organising of Project Files - -The structure of the project is shown on the left hand side of the main GUI. Project files are -organised into root folders, indicating what class of file they are. The most important root folder -is the Novel folder, which contains all of the files that makes up the finished novel. Each root -folder can have subfolders. Folders have no impact on the project structure, they are purely tools -for organising the files in whatever way the user needs. - -The editor supports four levels of headings, which determines what level the following text belongs -to. Headings of level one signify a book or partition title. Headings of level two signify the start -of a new chapter. Headings of level three signify the start of a new scene. Headings of level four -can be used internally in each scene to separate sections. - -Each novel file can be assigned a layout format, which shows up as a flag next to the item in the -project tree. These are mostly to help the user see what they contain, but they also have some -impact on the format of the exported document. See the documentation for further details. - - -#### Project Notes - -Supporting note files can be added for the story plot, characters, locations, story timeline, etc. -These have their separate root folders. These are optional files. - - -### Visualisation of Story Elements - -The different notes can be assigned tags, which other files can refer back to using special meta -keywords. This information can be used to display an outline of the story, showing where each scene -connects to the plot, and which characters, etc. occur in them. In addition, the tags themselves are -clickable in the document view pane, and control-clickable in the editor. They make it possible to -quickly navigate between the documents while editing. - - ## Screenshot **novelWriter with default system theme:** From e22f99cffbbb377632b5e09879d0119b4f411bc7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 7 Oct 2020 19:19:36 +0200 Subject: [PATCH 010/104] handle tuple returns from file dialog properly --- nw/gui/build.py | 6 ++---- nw/gui/writingstats.py | 7 +++---- nw/guimain.py | 6 ++---- tests/test_dialogs.py | 4 ++-- tests/test_gui.py | 6 +++--- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index eb7a88e3..aea1f237 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -762,12 +762,10 @@ class GuiBuildNovel(QDialog): if self.mainConf.showGUI: dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog - saveTo = QFileDialog.getSaveFileName( + savePath, _ = QFileDialog.getSaveFileName( self, "Save Document As", savePath, options=dlgOpt ) - if saveTo[0]: - savePath = saveTo[0] - else: + if not savePath: return False self.mainConf.setLastPath(savePath) diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index f748f95e..1d634ec6 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -334,12 +334,11 @@ class GuiWritingStats(QDialog): dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog - saveTo = QFileDialog.getSaveFileName( + savePath, _ = QFileDialog.getSaveFileName( self, "Save Document As", savePath, options=dlgOpt ) - if saveTo[0]: - savePath = saveTo[0] - else: + + if not savePath: return False self.mainConf.setLastPath(savePath) diff --git a/nw/guimain.py b/nw/guimain.py index 0cdef1ef..076eccf0 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -562,12 +562,10 @@ class GuiMain(QMainWindow): ] dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog - inPath = QFileDialog.getOpenFileName( + loadFile, _ = QFileDialog.getOpenFileName( self, "Import File", lastPath, options=dlgOpt, filter=";;".join(extFilter) ) - if inPath: - loadFile = inPath[0] - else: + if not loadFile: return False if loadFile.strip() == "": diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 6fe0f2e4..9d05f56f 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -283,10 +283,10 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert isinstance(sessLog, GuiWritingStats) qtbot.wait(stepDelay) - monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda *args, **kwargs: []) + monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda *args, **kwargs: ("", "")) assert not sessLog._saveData(sessLog.FMT_CSV) - monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda ss, tt, pp, options: [pp]) + monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda ss, tt, pp, options: (pp, "")) assert sessLog._saveData(sessLog.FMT_CSV) qtbot.wait(stepDelay) assert sessLog._saveData(sessLog.FMT_JSON) diff --git a/tests/test_gui.py b/tests/test_gui.py index 8cfb043d..0b32327e 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -1107,16 +1107,16 @@ def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp): nwGUI.closeDocument() # First, with no path - monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: []) + monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: ("", "")) assert not nwGUI.importDocument() # Then with a path, but an invalid one - monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: [" "]) + monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: (" ", "")) assert not nwGUI.importDocument() # Then a valid path, but bot a file that exists theFile = os.path.join(nwTemp, "import.txt") - monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: [theFile]) + monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: (theFile, "")) assert not nwGUI.importDocument() # Create the file and try again, but with no target document open From 8e5664c9ae866c65919def57f95dade2e86ca651 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 7 Oct 2020 19:26:38 +0200 Subject: [PATCH 011/104] Minor changes to imort document feature --- nw/gui/doceditor.py | 2 ++ nw/guimain.py | 1 + 2 files changed, 3 insertions(+) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 7e83461a..afb704c7 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -338,9 +338,11 @@ class GuiDocEditor(QTextEdit): ) % (docSize/1.0e6, nwConst.maxDocSize/1.0e6), nwAlert.ERROR) return False + qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) self.setPlainText(theText) self.setDocumentChanged(True) self.updateDocMargins() + qApp.restoreOverrideCursor() return True diff --git a/nw/guimain.py b/nw/guimain.py index 076eccf0..a741af88 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -558,6 +558,7 @@ class GuiMain(QMainWindow): extFilter = [ "Text files (*.txt)", "Markdown files (*.md)", + "novelWriter files (*.nwd)", "All files (*.*)", ] dlgOpt = QFileDialog.Options() From 8bbdbde7ca4bd1300fb8b47a3c0e36b63e96c922 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 7 Oct 2020 20:20:59 +0200 Subject: [PATCH 012/104] Minor changes to document re-highlighting --- nw/gui/doceditor.py | 5 ++--- nw/gui/dochighlight.py | 9 ++++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 6592c6f1..16683d1f 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -285,7 +285,7 @@ class GuiDocEditor(QTextEdit): self._allowAutoReplace(True) afTime = time() - logger.debug("Document highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))) + logger.debug("Document highlighted in %.3f ms" % (1000*(afTime-bfTime))) self.lastEdit = time() self._runCounter() @@ -557,9 +557,8 @@ class GuiDocEditor(QTextEdit): qApp.restoreOverrideCursor() afTime = time() logger.debug( - "Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime)) + "Document highlighted in %.3f ms" % (1000*(afTime-bfTime)) ) - self.theParent.statusBar.showMessage("Spell check complete") return True diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index e6e97533..564b1a9b 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -28,6 +28,8 @@ import nw import logging +from time import time + from PyQt5.QtCore import Qt, QRegularExpression from PyQt5.QtGui import ( QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush @@ -244,10 +246,15 @@ class GuiDocHighlighter(QSyntaxHighlighter): """ qDocument = self.document() nBlocks = qDocument.blockCount() + bfTime = time() for i in range(nBlocks): theBlock = qDocument.findBlockByNumber(i) - if theBlock.userState() & theType == theType: + if theBlock.userState() & theType > 0: self.rehighlightBlock(theBlock) + afTime = time() + logger.debug( + "Document highlighted in %.3f ms" % (1000*(afTime-bfTime)) + ) return ## From 014b970982ccf7e401bb524744a4c471ada645d0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 7 Oct 2020 20:33:34 +0200 Subject: [PATCH 013/104] Change a few checks to use built-in features --- nw/common.py | 8 ++++---- nw/core/spellcheck.py | 7 ++----- nw/gui/doceditor.py | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/nw/common.py b/nw/common.py index 2fef7874..17a8a337 100644 --- a/nw/common.py +++ b/nw/common.py @@ -203,12 +203,12 @@ def transferCase(theSource, theTarget): if len(theTarget) < 1 or len(theSource) < 1: return theResult - if theSource[0] == theSource[0].upper(): - theResult = theTarget[0].upper() + theTarget[1:] + if theSource.istitle(): + theResult = theTarget.title() - if theSource == theSource.upper(): + if theSource.isupper(): theResult = theTarget.upper() - elif theSource == theSource.lower(): + elif theSource.islower(): theResult = theTarget.lower() return theResult diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index d2c19784..ff338f62 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -291,15 +291,12 @@ class NWSpellSimple(NWSpellCheck): if len(theWord) == 0: return [] - firstUp = theWord[0] == theWord[0].upper() - theWord = theWord.lower() - - theMatches = get_close_matches(theWord, self.WORDS, n=10, cutoff=0.75) + theMatches = get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75) theOptions = [] for aWord in theMatches: if len(aWord) == 0: continue - if firstUp: + if theWord[0].isupper(): aWord = aWord[0].upper() + aWord[1:] aWord = aWord.replace("'", self.mainConf.fmtApostrophe) theOptions.append(aWord) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 16683d1f..f805e5ae 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -509,7 +509,7 @@ class GuiDocEditor(QTextEdit): aLang, aName = self.theDict.describeDict() self.theParent.statusBar.setLanguage( - aLang, "%s/%s" % (self.mainConf.spellTool, aName) + aLang, "%s [%s]" % (self.mainConf.spellTool.title(), aName.title()) ) if not self.bigDoc: From d9b1363788aa4904bcae91819b27ff2fc3efcbf6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 7 Oct 2020 23:35:09 +0200 Subject: [PATCH 014/104] Use markContentsDirty when possible also for document viewer --- nw/gui/docviewer.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 2d24efc1..49f9c9da 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -34,10 +34,10 @@ import logging from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot from PyQt5.QtGui import ( - QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon + QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor ) from PyQt5.QtWidgets import ( - QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton, + qApp, QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton, QAction, QMenu ) @@ -124,11 +124,15 @@ class GuiDocViewer(QTextBrowser): theOpt.setAlignment(Qt.AlignJustify) self.qDocument.setDefaultTextOption(theOpt) + # Refresh the tab stops + if self.mainConf.verQtValue >= 51000: + self.setTabStopDistance(self.mainConf.getTabWidth()) + else: + self.setTabStopWidth(self.mainConf.getTabWidth()) + # If we have a document open, we should reload it in case the font changed if self.theHandle is not None: - tHandle = self.theHandle - self.clearViewer() - self.loadText(tHandle) + self.redrawText() return True @@ -144,6 +148,8 @@ class GuiDocViewer(QTextBrowser): return False logger.debug("Generating preview for item %s" % tHandle) + qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) + sPos = self.verticalScrollBar().value() aDoc = ToHtml(self.theProject, self.theParent) aDoc.setPreview(True, self.mainConf.viewComments, self.mainConf.viewSynopsis) @@ -195,7 +201,8 @@ class GuiDocViewer(QTextBrowser): # Since we change the content while it may still be rendering, we mark # the document dirty again to make sure it's re-rendered properly. - self.qDocument.markContentsDirty(0, self.qDocument.characterCount()) + self.redrawText() + qApp.restoreOverrideCursor() return True @@ -205,6 +212,12 @@ class GuiDocViewer(QTextBrowser): self.loadText(self.theHandle, updateHistory=False) return + def redrawText(self): + """Redraw the text by marking the document content as "dirty". + """ + self.qDocument.markContentsDirty(0, self.qDocument.characterCount()) + return + def loadFromTag(self, theTag): """Load text in the document from a reference given by a meta tag rather than a known handle. This function depends on the From 214323fa6e553487628bd0ef95bceef27e7c1bd1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 7 Oct 2020 23:50:34 +0200 Subject: [PATCH 015/104] Add spin cursor to build tool print, and add formatting to project tree words column --- nw/gui/build.py | 2 ++ nw/gui/projtree.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index aea1f237..ad9af97b 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -899,8 +899,10 @@ class GuiBuildNovel(QDialog): def _doPrintPreview(self, thePrinter): """Connect the print preview painter to the document viewer. """ + qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) thePrinter.setOrientation(QPrinter.Portrait) self.docView.qDocument.print(thePrinter) + qApp.restoreOverrideCursor() return def _selectFont(self): diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index dfada052..858948dc 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -401,7 +401,7 @@ class GuiProjectTree(QTreeWidget): if nwItemS is None: return False - wCount = int(trItemS.text(self.C_COUNT)) + wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole)) if nwItemS.itemType == nwItemType.FILE: logger.debug("User requested file %s moved to trash" % tHandle) trItemP = trItemS.parent() @@ -550,12 +550,13 @@ class GuiProjectTree(QTreeWidget): """ tItem = self._getTreeItem(tHandle) if tItem is not None: - tItem.setText(self.C_COUNT, str(theCount)) + tItem.setText(self.C_COUNT, f"{theCount:n}") + tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount)) pItem = tItem.parent() if pItem is not None: pCount = 0 for i in range(pItem.childCount()): - pCount += int(pItem.child(i).text(self.C_COUNT)) + pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole)) pHandle = pItem.data(self.C_NAME, Qt.UserRole) if not nDepth > nwConst.maxDepth + 1 and pHandle != "": @@ -575,7 +576,7 @@ class GuiProjectTree(QTreeWidget): tItem = self.topLevelItem(n) if tItem == self.orphRoot: continue - nWords += int(tItem.text(self.C_COUNT)) + nWords += int(tItem.data(self.C_COUNT, Qt.UserRole)) self.theProject.setProjectWordCount(nWords) sWords = self.theProject.getSessionWordCount() @@ -715,7 +716,7 @@ class GuiProjectTree(QTreeWidget): self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) return - wCount = int(sItem.text(self.C_COUNT)) + wCount = int(sItem.data(self.C_COUNT, Qt.UserRole)) isSame = snItem.itemClass == dnItem.itemClass isNone = snItem.itemClass == nwItemClass.NO_CLASS isNote = snItem.itemLayout == nwItemLayout.NOTE @@ -809,6 +810,7 @@ class GuiProjectTree(QTreeWidget): newItem.setTextAlignment(self.C_FLAGS, Qt.AlignLeft | Qt.AlignVCenter) newItem.setData(self.C_NAME, Qt.UserRole, tHandle) + newItem.setData(self.C_COUNT, Qt.UserRole, 0) self.theMap[tHandle] = newItem if pHandle is None: @@ -881,6 +883,7 @@ class GuiProjectTree(QTreeWidget): self.orphRoot = newItem newItem.setExpanded(True) newItem.setData(self.C_NAME, Qt.UserRole, "") + newItem.setData(self.C_COUNT, Qt.UserRole, 0) newItem.setIcon(self.C_NAME, self.theTheme.getIcon("proj_orphan")) return From b40e4635d9cde3096c864b63eb6c3526f46cfecc Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 8 Oct 2020 00:06:17 +0200 Subject: [PATCH 016/104] Further number formatting improvements --- nw/gui/doceditor.py | 4 ++-- nw/gui/docviewer.py | 6 ------ nw/gui/itemdetails.py | 12 ++++++------ nw/gui/outline.py | 6 +++--- nw/gui/projsettings.py | 6 +++--- nw/gui/writingstats.py | 9 +++++---- 6 files changed, 19 insertions(+), 24 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index f805e5ae..ba0b1945 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -2273,10 +2273,10 @@ class GuiDocEditFooter(QWidget): wCount = self.theItem.wordCount wDiff = wCount - self.theItem.initCount - self.wordsText.setText("Words: {:n} ({:+n})".format(wCount, wDiff)) + self.wordsText.setText(f"Words: {wCount:n} ({wDiff:+n})") byteSize = self.docEditor.qDocument.characterCount() - self.wordsText.setToolTip("Document size is {:n} bytes".format(byteSize)) + self.wordsText.setToolTip(f"Document size is {byteSize:n} bytes") return diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 49f9c9da..b7404e9c 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -502,12 +502,6 @@ class GuiDocViewer(QTextBrowser): "mark {{" " color: rgb({eColR},{eColG},{eColB});" "}}\n" - "table {{" - " margin: 10px 0px;" - "}}\n" - "td {{" - " padding: 0px 4px;" - "}}\n" ".tags {{" " color: rgb({kColR},{kColG},{kColB});" " font-wright: bold;" diff --git a/nw/gui/itemdetails.py b/nw/gui/itemdetails.py index f64d570b..6d5cbcb5 100644 --- a/nw/gui/itemdetails.py +++ b/nw/gui/itemdetails.py @@ -195,9 +195,9 @@ class GuiItemDetails(QWidget): we're already showing. """ if tHandle == self.theHandle: - self.cCountData.setText("{:n}".format(cC)) - self.wCountData.setText("{:n}".format(wC)) - self.pCountData.setText("{:n}".format(pC)) + self.cCountData.setText(f"{cC:n}") + self.wCountData.setText(f"{wC:n}") + self.pCountData.setText(f"{pC:n}") return def updateViewBox(self, tHandle): @@ -252,9 +252,9 @@ class GuiItemDetails(QWidget): self.layoutData.setText(nwLabels.LAYOUT_NAME[nwItem.itemLayout]) if nwItem.itemType == nwItemType.FILE: - self.cCountData.setText("{:n}".format(nwItem.charCount)) - self.wCountData.setText("{:n}".format(nwItem.wordCount)) - self.pCountData.setText("{:n}".format(nwItem.paraCount)) + self.cCountData.setText(f"{nwItem.charCount:n}") + self.wCountData.setText(f"{nwItem.wordCount:n}") + self.pCountData.setText(f"{nwItem.paraCount:n}") else: self.cCountData.setText("–") self.wCountData.setText("–") diff --git a/nw/gui/outline.py b/nw/gui/outline.py index fcee1d8c..6d524992 100644 --- a/nw/gui/outline.py +++ b/nw/gui/outline.py @@ -431,9 +431,9 @@ class GuiOutline(QTreeWidget): newItem.setText(self.colIndex[nwOutline.LINE], sTitle[1:].lstrip("0")) newItem.setData(self.colIndex[nwOutline.LINE], Qt.UserRole, sTitle) newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"]) - newItem.setText(self.colIndex[nwOutline.CCOUNT], str(novIdx["cCount"])) - newItem.setText(self.colIndex[nwOutline.WCOUNT], str(novIdx["wCount"])) - newItem.setText(self.colIndex[nwOutline.PCOUNT], str(novIdx["pCount"])) + newItem.setText(self.colIndex[nwOutline.CCOUNT], "{:n}".format(novIdx["cCount"])) + newItem.setText(self.colIndex[nwOutline.WCOUNT], "{:n}".format(novIdx["wCount"])) + newItem.setText(self.colIndex[nwOutline.PCOUNT], "{:n}".format(novIdx["pCount"])) newItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight) newItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight) newItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight) diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index 998ec83c..30d9ad54 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -282,15 +282,15 @@ class GuiProjectEditMeta(QWidget): self.nRootLabel = QLabel("Root folders:") self.nRootLabel.setIndent(xInd) - self.nRootValue = QLabel("{:n}".format(nR)) + self.nRootValue = QLabel(f"{nR:n}") self.nDirLabel = QLabel("Folders:") self.nDirLabel.setIndent(xInd) - self.nDirValue = QLabel("{:n}".format(nD)) + self.nDirValue = QLabel(f"{nD:n}") self.nFileLabel = QLabel("Documents:") self.nFileLabel.setIndent(xInd) - self.nFileValue = QLabel("{:n}".format(nF)) + self.nFileValue = QLabel(f"{nF:n}") self.wordsLabel = QLabel("Word count:") self.wordsLabel.setIndent(xInd) diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index 1d634ec6..ccf1ce18 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -454,10 +454,11 @@ class GuiWritingStats(QDialog): ) return False + ttWords = ttNovel + ttNotes self.labelTotal.setText(self._formatTime(ttTime)) - self.novelWords.setText("{:n}".format(ttNovel)) - self.notesWords.setText("{:n}".format(ttNotes)) - self.totalWords.setText("{:n}".format(ttNovel + ttNotes)) + self.novelWords.setText(f"{ttNovel:n}") + self.notesWords.setText(f"{ttNotes:n}") + self.totalWords.setText(f"{ttWords:n}") return True @@ -544,7 +545,7 @@ class GuiWritingStats(QDialog): newItem = QTreeWidgetItem() newItem.setText(self.C_TIME, sStart) newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) - newItem.setText(self.C_COUNT, "{:n}".format(nWords)) + newItem.setText(self.C_COUNT, f"{nWords:n}") if nWords > 0 and listMax > 0: theBar = self.barImage.scaled( From 1f3d33c60a9bb49857af29d4d1428a3bba00f68a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 8 Oct 2020 00:56:06 +0200 Subject: [PATCH 017/104] Updated changelog --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50bc1aa0..74b453a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,41 @@ * When the Trash folder doesn't exist because nothing has yet been deleted, the lookup function for the Trash folder's handle returns `None`. That meant that any item with a parent handle `None` would be treated as a Trash folder in many parts of the code before an actual Trash folder existed. This caused a few decision branches to make non-critical mistakes. This issue is now fixed with a new check function that takes this into account. PRs #452 and #453. * If an older project was opened, one with a different project file layout than the more recent versions, a dialog asks whether the user wants the project updated or not. However, the function that moves files to the new location actually starts working before the dialog asks for permission. Instead, it just checks that it is allowed to change the project XML file only. The check is still run before the dialog, but the action of moving files around are now postponed to after the permission has been given and the project XML file parsed. PR #453. * If there were multiple headings in a file, and the last paragraph did not end in a line break, the word counter for the individual sections would miss the last paragraph of the last section due to an index error. This has now been fixed. PR #453. +* The cursor position of a document in the editor would only be saved if the document had been altered. It is now also saved in the cases where the user makes no changes. PR #460. +* When using an aspell dictionary for spell checking, words containing a hyphen would be highlighted as misspelled. This is not the case for hunspell dictionaries. The hyphen is now taken into account when splitting sentences into words for spell check highlighting. PR #462. +* Some of the file dialogs would fail with a non-critical error when the cancel button was clicked. The cancel is now captured consistently in all instances where such a dialog is used, and the calling function exited properly. PR #463. **User Interface** * Minor changes to the text formatting on the Recent Projects dialog. PR #452. +* The Build Novel Project tool has been improved. The settings side panel is now scrollable, and the document and settings panel now have a movable splitter between them. This gives more flexibility to the sizes of the various parts. PR #459. +* A new option to replace tabs with spaces has been added to the Build Novel Project tool. Previously, they were always replaces for HTML output, but converting them to the HTML code for tab is actually convenient for later import into for instance Libre Office, which converts them back to regular tabs. Issue #458, PR #459. +* Non-breaking spaces have been removed from the HTML conversion of keywords and tags. Issue #458, PR #459. +* An upper limit of how large a document the Build Novel Project tool can view has been set. It is 10 megabytes of generated HTML. The tool will still build larger documents, but they aren't displayed. This also limits which options are available in the "Save As" list for such large documents. Only native novelWriter exports are supported for such documents. The limit is an order of magnitude larger than a typical long novel. PR #460. +* The language indicator in the status bar now has a tooltip stating what tool and spell check dictionary provider is being used. PR #462. +* All representations of integers, mostly word counts, are now presented in the same way. They should all use a thousand separator representation defined by the localised settings. PR #464. +* Many parts of the GUI have had a spin/wait cursor added for processes that may take a while and will block the GUI in the meantime. PRs #460, #463 and #464. + +**Improvements for macOS** + +* The native macOS menu bar now pulls the correct menu entries into the first menu column. PR #463. +* The application name in the main menu would state Python instead of novelWriter. As long as the `pyobjc` package is installed, the label will now correctly state novelWriter. PR #463. +* Install and run instructions for macOS have been added to the main README. PR #463. + +**Editor Performance** + +* The syntax highlighter now remembers what type of line every line in the document is. This means that certain types of lines can be re-highlighted without having to process the entire document again. This is particularly useful for refreshing the highlighting of keywords and tags after the index has been rebuilt. PR #460. +* On a few occasions, the entire document in the editor would be reloaded in order to update the layout and formatting. This is not only slow for big documents, it also resets the undo stack. Instead, the entire document is "marked as dirty" to force the Qt library to update the layout, which is much faster. PR #460. +* For very large documents (in the megabyte range), the repositioning of the cursor when the document was opened would sometimes interfere with the rendering of the document. This could potentially cause the editor to hang for up to a couple of minutes. Instead, the repositioning of the cursor is now postponed until the document layout size has reached a point past the character where the cursor is to be moved. This mode is only used for documents larger than 50 kilobytes. PR #460. +* The document editor will no longer accept single documents larger than 5 megabytes. This restriction has also been applied to the Build Novel Project tool. For reference, a typical long novel is less than 1 megabyte in size. PR #460. **Other Changes** * The command line switches `--quiet` and `--logfile=` have been removed. They were intended for testing, but have never been used. The default mode of only printing warnings and errors is quiet enough, and logging to file shouldn't be necessary for a GUI application. PR #453. * A number of if statements and conditions in the code that were intended to alter behaviour when running tests, mostly to stop modal dialogs from blocking the main thread, have been removed. The changes to the program flow when running tests have now been reduced to a minimum, and modifications instead handled with pytest monkeypatches. PR #453. +* The `QtSvg` package is no longer in use by novelWriter. The internal dependency check has been dropped. PR #457. +* It is no longer possible to set the user's home folder as the root directory of a project. The home folder is the default lookup folder in many cases, so it's easy to do by mistake. PR #457. +* The background word counter has been rewritten to run on an application wide thread pool. This is a more appropriate way of running background tasks. PR #462. **Test Suite** From 788bec45676327f41a2a93e0ce1ca5f83c1c020f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 10 Oct 2020 17:52:18 +0200 Subject: [PATCH 018/104] Added line status icons --- .../icons/fallback/status_lines-dark.svg | 78 +++++++++++++++++++ nw/assets/icons/fallback/status_lines.svg | 78 +++++++++++++++++++ nw/gui/theme.py | 1 + 3 files changed, 157 insertions(+) create mode 100644 nw/assets/icons/fallback/status_lines-dark.svg create mode 100644 nw/assets/icons/fallback/status_lines.svg diff --git a/nw/assets/icons/fallback/status_lines-dark.svg b/nw/assets/icons/fallback/status_lines-dark.svg new file mode 100644 index 00000000..a977f507 --- /dev/null +++ b/nw/assets/icons/fallback/status_lines-dark.svg @@ -0,0 +1,78 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/status_lines.svg b/nw/assets/icons/fallback/status_lines.svg new file mode 100644 index 00000000..d249f307 --- /dev/null +++ b/nw/assets/icons/fallback/status_lines.svg @@ -0,0 +1,78 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 0af7df5e..88cdd72f 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -529,6 +529,7 @@ class GuiIcons: "status_lang" : (None, None), "status_time" : (None, None), "status_stats" : (None, None), + "status_lines" : (None, None), "doc_h1" : (QStyle.SP_FileIcon, "x-office-document"), "doc_h2" : (QStyle.SP_FileIcon, "x-office-document"), "doc_h3" : (QStyle.SP_FileIcon, "x-office-document"), From 7105cd637feee81be4343c2730b1391252133a28 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 10 Oct 2020 17:52:37 +0200 Subject: [PATCH 019/104] Added line counter to document footer --- nw/gui/doceditor.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index ba0b1945..d4498b5e 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -312,6 +312,8 @@ class GuiDocEditor(QTextEdit): else: self.setCursorLine(tLine) + self.docFooter.updateLineCount() + qApp.restoreOverrideCursor() return True @@ -462,6 +464,7 @@ class GuiDocEditor(QTextEdit): theCursor = self.textCursor() theCursor.setPosition(thePosition) self.setTextCursor(theCursor) + self.docFooter.updateLineCount() return True def getCursorPosition(self): @@ -488,6 +491,7 @@ class GuiDocEditor(QTextEdit): theBlock = self.qDocument.findBlockByLineNumber(theLine) if theBlock: self.setCursorPosition(theBlock.position()) + self.docFooter.updateLineCount() logger.verbose("Cursor moved to line %d" % theLine) return True @@ -736,6 +740,7 @@ class GuiDocEditor(QTextEdit): elif keyEvent == QKeySequence.Undo: self.docAction(nwDocAction.UNDO) else: + self.docFooter.updateLineCount() QTextEdit.keyPressEvent(self, keyEvent) return @@ -759,6 +764,7 @@ class GuiDocEditor(QTextEdit): if qApp.keyboardModifiers() == Qt.ControlModifier: theCursor = self.cursorForPosition(mEvent.pos()) self._followTag(theCursor) + self.docFooter.updateLineCount() QTextEdit.mouseReleaseEvent(self, mEvent) return @@ -2166,6 +2172,7 @@ class GuiDocEditFooter(QWidget): self.sPx = int(round(0.9*self.theTheme.baseIconSize)) fPx = int(0.9*self.theTheme.fontPixelSize) bSp = self.mainConf.pxInt(4) + hSp = self.mainConf.pxInt(6) lblFont = self.font() lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) @@ -2191,6 +2198,23 @@ class GuiDocEditFooter(QWidget): self.statusText.setPalette(self.thePalette) self.statusText.setFont(lblFont) + # Lines + self.linesIcon = QLabel("") + self.linesIcon.setPixmap(self.theTheme.getPixmap("status_lines", (self.sPx, self.sPx))) + self.linesIcon.setContentsMargins(0, 0, 0, 0) + self.linesIcon.setFixedHeight(self.sPx) + self.linesIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop) + + self.linesText = QLabel("Line: 0 of 0") + self.linesText.setIndent(0) + self.linesText.setMargin(0) + self.linesText.setContentsMargins(0, 0, 0, 0) + self.linesText.setAutoFillBackground(True) + self.linesText.setFixedHeight(fPx) + self.linesText.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.linesText.setPalette(self.thePalette) + self.linesText.setFont(lblFont) + # Words self.wordsIcon = QLabel("") self.wordsIcon.setPixmap(self.theTheme.getPixmap("status_stats", (self.sPx, self.sPx))) @@ -2214,6 +2238,9 @@ class GuiDocEditFooter(QWidget): self.outerBox.addWidget(self.statusIcon) self.outerBox.addWidget(self.statusText) self.outerBox.addStretch(1) + self.outerBox.addWidget(self.linesIcon) + self.outerBox.addWidget(self.linesText) + self.outerBox.addSpacing(hSp) self.outerBox.addWidget(self.wordsIcon) self.outerBox.addWidget(self.wordsText) self.setLayout(self.outerBox) @@ -2263,8 +2290,23 @@ class GuiDocEditFooter(QWidget): return + def updateLineCount(self): + """Update the word count. + """ + if self.theItem is None: + iLine = 0 + nLine = 0 + else: + theCursor = self.docEditor.textCursor() + iLine = theCursor.blockNumber() + 1 + nLine = self.docEditor.qDocument.blockCount() + + self.linesText.setText("Line: %d of %d" % (iLine, nLine)) + + return + def updateCounts(self): - """Update the word counts. + """Update the word count. """ if self.theItem is None: wCount = 0 From e9868744f4d7343b799301e0bfe328af6bac32b7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 10 Oct 2020 17:58:22 +0200 Subject: [PATCH 020/104] Remove the total line count, and trigger the update *after* the key/mouse events --- nw/gui/doceditor.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index d4498b5e..1d1d2a4e 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -740,8 +740,9 @@ class GuiDocEditor(QTextEdit): elif keyEvent == QKeySequence.Undo: self.docAction(nwDocAction.UNDO) else: - self.docFooter.updateLineCount() QTextEdit.keyPressEvent(self, keyEvent) + self.docFooter.updateLineCount() + return def focusNextPrevChild(self, toNext): @@ -764,8 +765,10 @@ class GuiDocEditor(QTextEdit): if qApp.keyboardModifiers() == Qt.ControlModifier: theCursor = self.cursorForPosition(mEvent.pos()) self._followTag(theCursor) - self.docFooter.updateLineCount() + QTextEdit.mouseReleaseEvent(self, mEvent) + self.docFooter.updateLineCount() + return def resizeEvent(self, theEvent): @@ -2205,7 +2208,7 @@ class GuiDocEditFooter(QWidget): self.linesIcon.setFixedHeight(self.sPx) self.linesIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop) - self.linesText = QLabel("Line: 0 of 0") + self.linesText = QLabel("Line: 0") self.linesText.setIndent(0) self.linesText.setMargin(0) self.linesText.setContentsMargins(0, 0, 0, 0) @@ -2295,13 +2298,11 @@ class GuiDocEditFooter(QWidget): """ if self.theItem is None: iLine = 0 - nLine = 0 else: theCursor = self.docEditor.textCursor() iLine = theCursor.blockNumber() + 1 - nLine = self.docEditor.qDocument.blockCount() - self.linesText.setText("Line: %d of %d" % (iLine, nLine)) + self.linesText.setText(f"Line: {iLine:n}") return From dee3972e3bb43e3641c554c1534e072432e130e6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 15:13:22 +0200 Subject: [PATCH 021/104] Updated changelog --- CHANGELOG.md | 1 + sample/nwProject.nwx | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74b453a3..571fe330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * The language indicator in the status bar now has a tooltip stating what tool and spell check dictionary provider is being used. PR #462. * All representations of integers, mostly word counts, are now presented in the same way. They should all use a thousand separator representation defined by the localised settings. PR #464. * Many parts of the GUI have had a spin/wait cursor added for processes that may take a while and will block the GUI in the meantime. PRs #460, #463 and #464. +* A line counter has been added to the footer of the document editor next to the word counter. It makes it easier to compare the position in the document when also accessing it in an external editor. PR #466. **Improvements for macOS** diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 2cbd1f6d..fa481af6 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 744 - 144 - 36162 + 765 + 148 + 37663 False @@ -120,7 +120,7 @@ 1811 318 8 - 1880 + 1332 Another Scene From 4a47be89defc847a27822da5f79ab044eb6a50d4 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 15:41:14 +0200 Subject: [PATCH 022/104] Setup script should fail when help docs build fails --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 515c2bfd..f0a9eb89 100755 --- a/setup.py +++ b/setup.py @@ -77,6 +77,7 @@ if buildDocs: print("") if buildFail: print("Documentation build: FAILED") + sys.exit(1) else: print("Documentation build: OK") print("") From 80e8788baaea7f558b6bb90803ae290936e4ab84 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 16:20:47 +0200 Subject: [PATCH 023/104] Added scroll past end feature by setting a large root frame margin of the document --- nw/config.py | 2 ++ nw/gui/doceditor.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/nw/config.py b/nw/config.py index 5f4a1300..721901ab 100644 --- a/nw/config.py +++ b/nw/config.py @@ -122,6 +122,8 @@ class Config: self.doReplaceDQuote = True self.doReplaceDash = True self.doReplaceDots = True + self.extendScroll = True + self.wordCountTimer = 5.0 self.showTabsNSpaces = False self.showLineEndings = False diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 1d1d2a4e..10f9de3f 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -377,6 +377,7 @@ class GuiDocEditor(QTextEdit): just ensure the margins are set correctly. """ wW = self.width() + wH = self.height() cM = self.mainConf.getTextMargin() vBar = self.verticalScrollBar() @@ -400,7 +401,7 @@ class GuiDocEditor(QTextEdit): tW = wW - 2*tB - sW tH = self.docHeader.height() fH = self.docFooter.height() - fY = self.height() - fH - tB - sH + fY = wH - fH - tB - sH self.docHeader.setGeometry(tB, tB, tW, tH) self.docFooter.setGeometry(tB, fY, tW, fH) @@ -412,7 +413,14 @@ class GuiDocEditor(QTextEdit): else: rH = 0 - self.setViewportMargins(tM, max(cM, tH, rH), tM, max(cM, fH)) + uM = max(cM, tH, rH) + lM = max(cM, fH) + self.setViewportMargins(tM, uM, tM, lM) + + if self.mainConf.extendScroll: + docFrame = self.qDocument.rootFrame().frameFormat() + docFrame.setBottomMargin(wH - uM - lM - self.theTheme.fontPixelSize) + self.qDocument.rootFrame().setFrameFormat(docFrame) return From fe71353198f156ba5a16de6a7cc6a598105fb06c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 16:40:54 +0200 Subject: [PATCH 024/104] Add option to hide scrollbars on all major GUI areas --- nw/config.py | 4 ++++ nw/gui/build.py | 12 ++++++++++-- nw/gui/doceditor.py | 6 ++++++ nw/gui/docviewer.py | 6 ++++++ nw/gui/outline.py | 12 ++++++++++++ nw/gui/outlinedetails.py | 13 +++++++++++++ nw/gui/projtree.py | 14 ++++++++++++++ nw/guimain.py | 3 +++ 8 files changed, 68 insertions(+), 2 deletions(-) diff --git a/nw/config.py b/nw/config.py index 721901ab..24089d93 100644 --- a/nw/config.py +++ b/nw/config.py @@ -102,6 +102,10 @@ class Config: self.outlnPanePos = [500, 150] self.isFullScreen = False + ## Features + self.hideVScroll = False # Hide vertical scroll bars on main widgets + self.hideHScroll = False # Hide horizontal scroll bars on main widgets + ## Project self.autoSaveProj = 60 self.autoSaveDoc = 30 diff --git a/nw/gui/build.py b/nw/gui/build.py index ad9af97b..dc9d13ad 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -450,10 +450,12 @@ class GuiBuildNovel(QDialog): # Tool Box Scroll Area self.toolsArea = QScrollArea() self.toolsArea.setMinimumWidth(self.mainConf.pxInt(250)) - self.toolsArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.toolsArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.toolsArea.setWidgetResizable(True) self.toolsArea.setWidget(self.toolsWidget) + if self.mainConf.hideVScroll: + self.toolsArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + if self.mainConf.hideHScroll: + self.toolsArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # Tools and Buttons Layout self.innerBox = QVBoxLayout() @@ -1116,6 +1118,12 @@ class GuiBuildNovelDocView(QTextBrowser): else: self.setTabStopWidth(self.mainConf.getTabWidth()) + # Scroll bars + if self.mainConf.hideVScroll: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + if self.mainConf.hideHScroll: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + docPalette = self.palette() docPalette.setColor(QPalette.Base, QColor(255, 255, 255)) docPalette.setColor(QPalette.Text, QColor(0, 0, 0)) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 10f9de3f..85cda2a6 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -224,6 +224,12 @@ class GuiDocEditor(QTextEdit): self.qDocument.setDefaultTextOption(theOpt) + # Scroll bars + if self.mainConf.hideVScroll: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + if self.mainConf.hideHScroll: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + # Refresh the tab stops if self.mainConf.verQtValue >= 51000: self.setTabStopDistance(self.mainConf.getTabWidth()) diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index b7404e9c..cfd289a4 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -124,6 +124,12 @@ class GuiDocViewer(QTextBrowser): theOpt.setAlignment(Qt.AlignJustify) self.qDocument.setDefaultTextOption(theOpt) + # Scroll bars + if self.mainConf.hideVScroll: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + if self.mainConf.hideHScroll: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + # Refresh the tab stops if self.mainConf.verQtValue >= 51000: self.setTabStopDistance(self.mainConf.getTabWidth()) diff --git a/nw/gui/outline.py b/nw/gui/outline.py index 6d524992..3470013f 100644 --- a/nw/gui/outline.py +++ b/nw/gui/outline.py @@ -118,6 +118,7 @@ class GuiOutline(QTreeWidget): self.colIndex = {} self.treeNCols = 0 + self.initOutline() self.clearOutline() self.headerMenu.setHiddenState(self.colHidden) @@ -125,6 +126,17 @@ class GuiOutline(QTreeWidget): return + def initOutline(self): + """Set or update outline settings. + """ + # Scroll bars + if self.mainConf.hideVScroll: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + if self.mainConf.hideHScroll: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + return + def clearOutline(self): """Clear the tree and header and set the default values for the columns arrays. diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py index c6372826..9f116b18 100644 --- a/nw/gui/outlinedetails.py +++ b/nw/gui/outlinedetails.py @@ -224,10 +224,23 @@ class GuiOutlineDetails(QScrollArea): self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setWidgetResizable(True) + self.initDetails() + logger.debug("GuiOutlineDetails initialisation complete") return + def initDetails(self): + """Set or update outline settings. + """ + # Scroll bars + if self.mainConf.hideVScroll: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + if self.mainConf.hideHScroll: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + return + def showItem(self, tHandle, sTitle): """Update the content of the tree with the given handle and line number pointing to a header. diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 858948dc..3c03b236 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -115,6 +115,9 @@ class GuiProjectTree(QTreeWidget): # The last column should just auto-scale self.resizeColumnToContents(self.C_FLAGS) + # Set custom settings + self.initTree() + logger.debug("GuiProjectTree initialisation complete") # Internal Mapping @@ -122,6 +125,17 @@ class GuiProjectTree(QTreeWidget): return + def initTree(self): + """Set or update tree widget settings. + """ + # Scroll bars + if self.mainConf.hideVScroll: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + if self.mainConf.hideHScroll: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + return + ## # Class Methods ## diff --git a/nw/guimain.py b/nw/guimain.py index 93c62210..18801727 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -790,6 +790,9 @@ class GuiMain(QMainWindow): self.saveDocument() self.docEditor.initEditor() self.docViewer.initViewer() + self.treeView.initTree() + self.projView.initOutline() + self.projMeta.initDetails() return From 1234c9feb69316ab5b44f0f122044ca0e528d176 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 18:21:49 +0200 Subject: [PATCH 025/104] Attemt to scroll upwards like a typewriter --- nw/config.py | 3 ++- nw/gui/doceditor.py | 41 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/nw/config.py b/nw/config.py index 24089d93..88576502 100644 --- a/nw/config.py +++ b/nw/config.py @@ -126,7 +126,8 @@ class Config: self.doReplaceDQuote = True self.doReplaceDash = True self.doReplaceDots = True - self.extendScroll = True + self.scrollPastEnd = True + self.scollWithCursor = False self.wordCountTimer = 5.0 self.showTabsNSpaces = False diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 85cda2a6..f4c87d9b 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -47,7 +47,7 @@ from PyQt5.QtGui import ( from PyQt5.QtWidgets import ( qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QWidget, QLabel, QToolBar, QToolButton, QHBoxLayout, QGridLayout, QLineEdit, QPushButton, - QFrame + QFrame, QAbstractSlider ) from nw.core import NWDoc, NWSpellCheck, NWSpellSimple, countWords @@ -88,6 +88,8 @@ class GuiDocEditor(QTextEdit): self.bigDoc = False # Flag for very large document size self.doReplace = False # Switch to temporarily disable auto-replace self.queuePos = None # Used for delayed change of cursor position + self.cursorLast = 0 # The last known vertical position of the cursor + self.lengthLast = 0 # Typography self.typDQOpen = self.mainConf.fmtDoubleQuotes[0] @@ -100,6 +102,8 @@ class GuiDocEditor(QTextEdit): self.qDocument.contentsChange.connect(self._docChange) self.qDocument.documentLayout().documentSizeChanged.connect(self._docSizeChanged) + self.verticalScrollBar().sliderMoved.connect(self._doVerticalScroll) + # Document Title self.docHeader = GuiDocEditHeader(self) self.docFooter = GuiDocEditFooter(self) @@ -319,6 +323,7 @@ class GuiDocEditor(QTextEdit): self.setCursorLine(tLine) self.docFooter.updateLineCount() + self.lengthLast = self.qDocument.characterCount() qApp.restoreOverrideCursor() @@ -423,7 +428,7 @@ class GuiDocEditor(QTextEdit): lM = max(cM, fH) self.setViewportMargins(tM, uM, tM, lM) - if self.mainConf.extendScroll: + if self.mainConf.scrollPastEnd: docFrame = self.qDocument.rootFrame().frameFormat() docFrame.setBottomMargin(wH - uM - lM - self.theTheme.fontPixelSize) self.qDocument.rootFrame().setFrameFormat(docFrame) @@ -757,6 +762,22 @@ class GuiDocEditor(QTextEdit): QTextEdit.keyPressEvent(self, keyEvent) self.docFooter.updateLineCount() + if self.mainConf.scollWithCursor: + docLen = self.qDocument.characterCount() + if docLen == self.lengthLast: + # No change, so just update last position + self.cursorLast = self.cursorRect().center().y() + else: + # The user typed something, so check if we need to + # scroll, and move the scroll bar the same distance + self.lengthLast = docLen + self.ensureCursorVisible() + cPos = self.cursorRect().center().y() + if cPos != self.cursorLast: + vBar = self.verticalScrollBar() + vBar.setValue(vBar.value() + cPos - self.cursorLast) + self.cursorLast = self.cursorRect().center().y() + return def focusNextPrevChild(self, toNext): @@ -782,9 +803,18 @@ class GuiDocEditor(QTextEdit): QTextEdit.mouseReleaseEvent(self, mEvent) self.docFooter.updateLineCount() + self.cursorLast = self.cursorRect().center().y() return + def wheelEvent(self, theEvent): + """Briefly capture the mouse wheel event to capture the cursor + position. + """ + QTextEdit.wheelEvent(self, theEvent) + self.cursorLast = self.cursorRect().center().y() + return + def resizeEvent(self, theEvent): """If the text editor is resize, we must make sure the document has its margins adjusted according to user preferences. @@ -819,6 +849,13 @@ class GuiDocEditor(QTextEdit): self._docAutoReplace(self.qDocument.findBlock(thePos)) return + @pyqtSlot(int) + def _doVerticalScroll(self, theChange): + """Update the cursor position on vertical scrolling. + """ + self.cursorLast = self.cursorRect().center().y() + return + @pyqtSlot("QPoint") def _openContextMenu(self, thePos): """Triggered by right click to open the context menu. Also From 0c0ef6916bfc928ad7434949b0a66b9089f38762 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 19:41:06 +0200 Subject: [PATCH 026/104] Save new config settings and update tests --- nw/config.py | 16 ++++++++++++++++ nw/gui/doceditor.py | 2 +- tests/reference/novelwriter.conf | 6 +++++- tests/reference/novelwriter_prefs.conf | 4 ++++ tests/test_dialogs.py | 2 +- 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/nw/config.py b/nw/config.py index 88576502..5ed12655 100644 --- a/nw/config.py +++ b/nw/config.py @@ -398,6 +398,12 @@ class Config: self.isFullScreen = self._parseLine( cnfParse, cnfSec, "fullscreen", self.CNF_BOOL, self.isFullScreen ) + self.hideVScroll = self._parseLine( + cnfParse, cnfSec, "hidevscroll", self.CNF_BOOL, self.hideVScroll + ) + self.hideHScroll = self._parseLine( + cnfParse, cnfSec, "hidehscroll", self.CNF_BOOL, self.hideHScroll + ) ## Project cnfSec = "Project" @@ -455,6 +461,12 @@ class Config: self.doReplaceDots = self._parseLine( cnfParse, cnfSec, "repdots", self.CNF_BOOL, self.doReplaceDots ) + self.scrollPastEnd = self._parseLine( + cnfParse, cnfSec, "scrollpastend", self.CNF_BOOL, self.scrollPastEnd + ) + self.scollWithCursor = self._parseLine( + cnfParse, cnfSec, "scollwithcursor", self.CNF_BOOL, self.scollWithCursor + ) self.fmtSingleQuotes = self._parseLine( cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes ) @@ -572,6 +584,8 @@ class Config: cnfParse.set(cnfSec, "viewpane", self._packList(self.viewPanePos)) cnfParse.set(cnfSec, "outlinepane", self._packList(self.outlnPanePos)) cnfParse.set(cnfSec, "fullscreen", str(self.isFullScreen)) + cnfParse.set(cnfSec, "hidevscroll", str(self.hideVScroll)) + cnfParse.set(cnfSec, "hidehscroll", str(self.hideHScroll)) ## Project cnfSec = "Project" @@ -597,6 +611,8 @@ class Config: cnfParse.set(cnfSec, "repdquotes", str(self.doReplaceDQuote)) cnfParse.set(cnfSec, "repdash", str(self.doReplaceDash)) cnfParse.set(cnfSec, "repdots", str(self.doReplaceDots)) + cnfParse.set(cnfSec, "scrollpastend", str(self.scrollPastEnd)) + cnfParse.set(cnfSec, "scollwithcursor", str(self.scollWithCursor)) cnfParse.set(cnfSec, "fmtsinglequote", self._packList(self.fmtSingleQuotes)) cnfParse.set(cnfSec, "fmtdoublequote", self._packList(self.fmtDoubleQuotes)) cnfParse.set(cnfSec, "spelltool", str(self.spellTool)) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index f4c87d9b..fe9581e9 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -430,7 +430,7 @@ class GuiDocEditor(QTextEdit): if self.mainConf.scrollPastEnd: docFrame = self.qDocument.rootFrame().frameFormat() - docFrame.setBottomMargin(wH - uM - lM - self.theTheme.fontPixelSize) + docFrame.setBottomMargin(wH - uM - lM - 4*tB - self.theTheme.fontPixelSize) self.qDocument.rootFrame().setFrameFormat(docFrame) return diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf index 49054ae7..f4bc8d61 100644 --- a/tests/reference/novelwriter.conf +++ b/tests/reference/novelwriter.conf @@ -1,5 +1,5 @@ [Main] -timestamp = 2020-06-29 17:34:15 +timestamp = 2020-10-11 18:29:34 theme = default syntax = default_light icons = typicons_colour_light @@ -16,6 +16,8 @@ docpane = 400, 400 viewpane = 500, 150 outlinepane = 500, 150 fullscreen = False +hidevscroll = False +hidehscroll = False [Project] autosaveproject = 60 @@ -37,6 +39,8 @@ repsquotes = True repdquotes = True repdash = True repdots = True +scrollpastend = True +scollwithcursor = False fmtsinglequote = ‘, ’ fmtdoublequote = “, ” spelltool = internal diff --git a/tests/reference/novelwriter_prefs.conf b/tests/reference/novelwriter_prefs.conf index 608116ea..77e216bd 100644 --- a/tests/reference/novelwriter_prefs.conf +++ b/tests/reference/novelwriter_prefs.conf @@ -16,6 +16,8 @@ docpane = 400, 400 viewpane = 500, 150 outlinepane = 500, 150 fullscreen = False +hidevscroll = False +hidehscroll = False [Project] autosaveproject = 40 @@ -37,6 +39,8 @@ repsquotes = True repdquotes = True repdash = True repdots = True +scrollpastend = True +scollwithcursor = False fmtsinglequote = ‘, ’ fmtdoublequote = “, ” spelltool = internal diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 9d05f56f..11d0cd05 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -1198,7 +1198,7 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC ignoreLines = [ 2, # Timestamp 11, 12, 13, 14, 15, 16, 17, # Window sizes - 7, 25, # Fonts (depends on system default) + 7, 27, # Fonts (depends on system default) ] assert cmpFiles(testConf, refConf, ignoreLines) From fa55983f3abdeae1d4e83fc900b8bc73a9fad7ea Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 19:57:00 +0200 Subject: [PATCH 027/104] Split General tab in Preferences into General and Projects --- nw/gui/preferences.py | 118 ++++++++++++++++++++++++++++-------------- tests/test_dialogs.py | 36 ++++++++----- 2 files changed, 101 insertions(+), 53 deletions(-) diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index a946c289..5cba9fda 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -55,15 +55,17 @@ class GuiPreferences(PagedDialog): self.setWindowTitle("Preferences") - self.tabGeneral = GuiConfigEditGeneralTab(self.theParent) - self.tabLayout = GuiConfigEditLayoutTab(self.theParent) - self.tabEditing = GuiConfigEditEditingTab(self.theParent) - self.tabAutoRep = GuiConfigEditAutoReplaceTab(self.theParent) + self.tabGeneral = GuiConfigEditGeneralTab(self.theParent) + self.tabProjects = GuiConfigEditProjectsTab(self.theParent) + self.tabLayout = GuiConfigEditLayoutTab(self.theParent) + self.tabEditing = GuiConfigEditEditingTab(self.theParent) + self.tabAutoRep = GuiConfigEditAutoReplaceTab(self.theParent) - self.addTab(self.tabGeneral, "General") - self.addTab(self.tabLayout, "Text Layout") - self.addTab(self.tabEditing, "Editor") - self.addTab(self.tabAutoRep, "Auto-Replace") + self.addTab(self.tabGeneral, "General") + self.addTab(self.tabProjects, "Projects") + self.addTab(self.tabLayout, "Text Layout") + self.addTab(self.tabEditing, "Editor") + self.addTab(self.tabAutoRep, "Auto-Replace") self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox.accepted.connect(self._doSave) @@ -91,6 +93,10 @@ class GuiPreferences(PagedDialog): validEntries &= retA needsRestart |= retB + retA, retB = self.tabProjects.saveValues() + validEntries &= retA + needsRestart |= retB + retA, retB = self.tabLayout.saveValues() validEntries &= retA needsRestart |= retB @@ -222,6 +228,70 @@ class GuiConfigEditGeneralTab(QWidget): self.showFullPath ) + return + + def saveValues(self): + """Save the values set for this tab. + """ + validEntries = True + needsRestart = False + + guiTheme = self.selectTheme.currentData() + guiIcons = self.selectIcons.currentData() + guiDark = self.preferDarkIcons.isChecked() + guiFont = self.guiFont.text() + guiFontSize = self.guiFontSize.value() + showFullPath = self.showFullPath.isChecked() + + # Check if restart is needed + needsRestart |= self.mainConf.guiTheme != guiTheme + needsRestart |= self.mainConf.guiIcons != guiIcons + needsRestart |= self.mainConf.guiFont != guiFont + needsRestart |= self.mainConf.guiFontSize != guiFontSize + + self.mainConf.guiTheme = guiTheme + self.mainConf.guiIcons = guiIcons + self.mainConf.guiDark = guiDark + self.mainConf.guiFont = guiFont + self.mainConf.guiFontSize = guiFontSize + self.mainConf.showFullPath = showFullPath + + self.mainConf.confChanged = True + + return validEntries, needsRestart + + ## + # Slots + ## + + def _selectFont(self): + """Open the QFontDialog and set a font for the font style. + """ + currFont = QFont() + currFont.setFamily(self.mainConf.guiFont) + currFont.setPointSize(self.mainConf.guiFontSize) + theFont, theStatus = QFontDialog.getFont(currFont, self) + if theStatus: + self.guiFont.setText(theFont.family()) + self.guiFontSize.setValue(theFont.pointSize()) + return + +# END Class GuiConfigEditGeneralTab + +class GuiConfigEditProjectsTab(QWidget): + + def __init__(self, theParent): + QWidget.__init__(self, theParent) + + self.mainConf = nw.CONFIG + self.theParent = theParent + self.theTheme = theParent.theTheme + + # The Form + self.mainForm = QConfigLayout() + self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.setLayout(self.mainForm) + # AutoSave Settings # ================= self.mainForm.addGroupLabel("Automatic Save") @@ -292,30 +362,12 @@ class GuiConfigEditGeneralTab(QWidget): validEntries = True needsRestart = False - guiTheme = self.selectTheme.currentData() - guiIcons = self.selectIcons.currentData() - guiDark = self.preferDarkIcons.isChecked() - guiFont = self.guiFont.text() - guiFontSize = self.guiFontSize.value() - showFullPath = self.showFullPath.isChecked() autoSaveDoc = self.autoSaveDoc.value() autoSaveProj = self.autoSaveProj.value() backupPath = self.backupPath backupOnClose = self.backupOnClose.isChecked() askBeforeBackup = self.askBeforeBackup.isChecked() - # Check if restart is needed - needsRestart |= self.mainConf.guiTheme != guiTheme - needsRestart |= self.mainConf.guiIcons != guiIcons - needsRestart |= self.mainConf.guiFont != guiFont - needsRestart |= self.mainConf.guiFontSize != guiFontSize - - self.mainConf.guiTheme = guiTheme - self.mainConf.guiIcons = guiIcons - self.mainConf.guiDark = guiDark - self.mainConf.guiFont = guiFont - self.mainConf.guiFontSize = guiFontSize - self.mainConf.showFullPath = showFullPath self.mainConf.autoSaveDoc = autoSaveDoc self.mainConf.autoSaveProj = autoSaveProj self.mainConf.backupPath = backupPath @@ -357,19 +409,7 @@ class GuiConfigEditGeneralTab(QWidget): self.askBeforeBackup.setEnabled(theState) return - def _selectFont(self): - """Open the QFontDialog and set a font for the font style. - """ - currFont = QFont() - currFont.setFamily(self.mainConf.guiFont) - currFont.setPointSize(self.mainConf.guiFontSize) - theFont, theStatus = QFontDialog.getFont(currFont, self) - if theStatus: - self.guiFont.setText(theFont.family()) - self.guiFontSize.setValue(theFont.pointSize()) - return - -# END Class GuiConfigEditGeneralTab +# END Class GuiConfigEditProjectsTab class GuiConfigEditLayoutTab(QWidget): diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 11d0cd05..cf04284b 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -1057,6 +1057,7 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC nwGUI.mainConf = tmpConf nwPrefs.mainConf = tmpConf nwPrefs.tabGeneral.mainConf = tmpConf + nwPrefs.tabProjects.mainConf = tmpConf nwPrefs.tabLayout.mainConf = tmpConf nwPrefs.tabEditing.mainConf = tmpConf nwPrefs.tabAutoRep.mainConf = tmpConf @@ -1065,7 +1066,6 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC qtbot.wait(keyDelay) tabGeneral = nwPrefs.tabGeneral nwPrefs._tabBox.setCurrentWidget(tabGeneral) - tabGeneral.backupPath = "no/where" qtbot.wait(keyDelay) assert not tabGeneral.preferDarkIcons.isChecked() @@ -1077,27 +1077,35 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC qtbot.mouseClick(tabGeneral.showFullPath, Qt.LeftButton) assert not tabGeneral.showFullPath.isChecked() - # Check Browse button - monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: "") - assert not tabGeneral._backupFolder() - monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: "some/dir") - qtbot.mouseClick(tabGeneral.backupGetPath, Qt.LeftButton) - # Check font button monkeypatch.setattr(QFontDialog, "getFont", lambda font, obj: (font, True)) qtbot.mouseClick(tabGeneral.fontButton, Qt.LeftButton) qtbot.wait(keyDelay) - assert not tabGeneral.backupOnClose.isChecked() - qtbot.mouseClick(tabGeneral.backupOnClose, Qt.LeftButton) - assert tabGeneral.backupOnClose.isChecked() + tabGeneral.guiFontSize.setValue(12) + + # Projects Settings + qtbot.wait(keyDelay) + tabProjects = nwPrefs.tabProjects + nwPrefs._tabBox.setCurrentWidget(tabProjects) + tabProjects.backupPath = "no/where" qtbot.wait(keyDelay) - tabGeneral.guiFontSize.setValue(12) - tabGeneral.autoSaveDoc.setValue(20) - tabGeneral.autoSaveProj.setValue(40) + assert not tabProjects.backupOnClose.isChecked() + qtbot.mouseClick(tabProjects.backupOnClose, Qt.LeftButton) + assert tabProjects.backupOnClose.isChecked() - # Text Layour Settings + # Check Browse button + monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: "") + assert not tabProjects._backupFolder() + monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: "some/dir") + qtbot.mouseClick(tabProjects.backupGetPath, Qt.LeftButton) + + qtbot.wait(keyDelay) + tabProjects.autoSaveDoc.setValue(20) + tabProjects.autoSaveProj.setValue(40) + + # Text Layout Settings qtbot.wait(keyDelay) tabLayout = nwPrefs.tabLayout nwPrefs._tabBox.setCurrentWidget(tabLayout) From ae8ec555fd1de3d3a65f4688193e6216c1df3a84 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 20:13:39 +0200 Subject: [PATCH 028/104] Connected the new settings to the Preferences dialog --- nw/gui/build.py | 11 ++++++++++ nw/gui/doceditor.py | 10 ++++++++- nw/gui/docviewer.py | 5 +++++ nw/gui/outline.py | 5 +++++ nw/gui/outlinedetails.py | 5 +++++ nw/gui/preferences.py | 45 +++++++++++++++++++++++++++++++++++++++- nw/gui/projtree.py | 5 +++++ 7 files changed, 84 insertions(+), 2 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index dc9d13ad..754553d8 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -452,10 +452,16 @@ class GuiBuildNovel(QDialog): self.toolsArea.setMinimumWidth(self.mainConf.pxInt(250)) self.toolsArea.setWidgetResizable(True) self.toolsArea.setWidget(self.toolsWidget) + if self.mainConf.hideVScroll: self.toolsArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.toolsArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + if self.mainConf.hideHScroll: self.toolsArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.toolsArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) # Tools and Buttons Layout self.innerBox = QVBoxLayout() @@ -1121,8 +1127,13 @@ class GuiBuildNovelDocView(QTextBrowser): # Scroll bars if self.mainConf.hideVScroll: self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + if self.mainConf.hideHScroll: self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) docPalette = self.palette() docPalette.setColor(QPalette.Base, QColor(255, 255, 255)) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index fe9581e9..e459b1b1 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -47,7 +47,7 @@ from PyQt5.QtGui import ( from PyQt5.QtWidgets import ( qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QWidget, QLabel, QToolBar, QToolButton, QHBoxLayout, QGridLayout, QLineEdit, QPushButton, - QFrame, QAbstractSlider + QFrame ) from nw.core import NWDoc, NWSpellCheck, NWSpellSimple, countWords @@ -231,8 +231,13 @@ class GuiDocEditor(QTextEdit): # Scroll bars if self.mainConf.hideVScroll: self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + if self.mainConf.hideHScroll: self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) # Refresh the tab stops if self.mainConf.verQtValue >= 51000: @@ -479,11 +484,14 @@ class GuiDocEditor(QTextEdit): """ if not isinstance(thePosition, int): return False + if thePosition >= 0: theCursor = self.textCursor() theCursor.setPosition(thePosition) self.setTextCursor(theCursor) self.docFooter.updateLineCount() + self.cursorLast = self.cursorRect().center().y() + return True def getCursorPosition(self): diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index cfd289a4..2d9ac637 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -127,8 +127,13 @@ class GuiDocViewer(QTextBrowser): # Scroll bars if self.mainConf.hideVScroll: self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + if self.mainConf.hideHScroll: self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) # Refresh the tab stops if self.mainConf.verQtValue >= 51000: diff --git a/nw/gui/outline.py b/nw/gui/outline.py index 3470013f..d3dc4815 100644 --- a/nw/gui/outline.py +++ b/nw/gui/outline.py @@ -132,8 +132,13 @@ class GuiOutline(QTreeWidget): # Scroll bars if self.mainConf.hideVScroll: self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + if self.mainConf.hideHScroll: self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) return diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py index 9f116b18..49998dd9 100644 --- a/nw/gui/outlinedetails.py +++ b/nw/gui/outlinedetails.py @@ -236,8 +236,13 @@ class GuiOutlineDetails(QScrollArea): # Scroll bars if self.mainConf.hideVScroll: self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + if self.mainConf.hideHScroll: self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) return diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index 5cba9fda..f1848bb3 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -225,7 +225,24 @@ class GuiConfigEditGeneralTab(QWidget): self.showFullPath.setChecked(self.mainConf.showFullPath) self.mainForm.addRow( "Show full path in document header", - self.showFullPath + self.showFullPath, + "Shows the document title and parent folder names." + ) + + self.hideVScroll = QSwitch() + self.hideVScroll.setChecked(self.mainConf.hideVScroll) + self.mainForm.addRow( + "Hide vertical scroll bars in main windows", + self.hideVScroll, + "Scrolling with mouse wheel and keys only." + ) + + self.hideHScroll = QSwitch() + self.hideHScroll.setChecked(self.mainConf.hideHScroll) + self.mainForm.addRow( + "Hide horizontal scroll bars in main windows", + self.hideHScroll, + "Scrolling with mouse wheel and keys only." ) return @@ -242,6 +259,8 @@ class GuiConfigEditGeneralTab(QWidget): guiFont = self.guiFont.text() guiFontSize = self.guiFontSize.value() showFullPath = self.showFullPath.isChecked() + hideVScroll = self.hideVScroll.isChecked() + hideHScroll = self.hideHScroll.isChecked() # Check if restart is needed needsRestart |= self.mainConf.guiTheme != guiTheme @@ -255,6 +274,8 @@ class GuiConfigEditGeneralTab(QWidget): self.mainConf.guiFont = guiFont self.mainConf.guiFontSize = guiFontSize self.mainConf.showFullPath = showFullPath + self.mainConf.hideVScroll = hideVScroll + self.mainConf.hideHScroll = hideHScroll self.mainConf.confChanged = True @@ -534,6 +555,24 @@ class GuiConfigEditLayoutTab(QWidget): theUnit="px" ) + ## Scroll Past End + self.scrollPastEnd = QSwitch() + self.scrollPastEnd.setChecked(self.mainConf.scrollPastEnd) + self.mainForm.addRow( + "Scroll past end of the document", + self.scrollPastEnd, + "Allows scrolling until last line is at the top." + ) + + ## Typewriter Scrolling + self.scollWithCursor = QSwitch() + self.scollWithCursor.setChecked(self.mainConf.scollWithCursor) + self.mainForm.addRow( + "Typewriter style scrolling", + self.scollWithCursor, + "Scrolls up when the cursor moves to a new line." + ) + return def saveValues(self): @@ -551,6 +590,8 @@ class GuiConfigEditLayoutTab(QWidget): doJustify = self.textJustify.isChecked() textMargin = self.textMargin.value() tabWidth = self.tabWidth.value() + scrollPastEnd = self.scrollPastEnd.isChecked() + scollWithCursor = self.scollWithCursor.isChecked() self.mainConf.textFont = textFont self.mainConf.textSize = textSize @@ -561,6 +602,8 @@ class GuiConfigEditLayoutTab(QWidget): self.mainConf.doJustify = doJustify self.mainConf.textMargin = textMargin self.mainConf.tabWidth = tabWidth + self.mainConf.scrollPastEnd = scrollPastEnd + self.mainConf.scollWithCursor = scollWithCursor self.mainConf.confChanged = True diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 3c03b236..03dc2ff8 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -131,8 +131,13 @@ class GuiProjectTree(QTreeWidget): # Scroll bars if self.mainConf.hideVScroll: self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + if self.mainConf.hideHScroll: self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) return From 2382118dad25870297fabb02f4e3b9973f6f1bd4 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 20:18:58 +0200 Subject: [PATCH 029/104] Add test coverage of the new options --- tests/reference/novelwriter_prefs.conf | 8 ++++---- tests/test_dialogs.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/reference/novelwriter_prefs.conf b/tests/reference/novelwriter_prefs.conf index 77e216bd..0e1d41f6 100644 --- a/tests/reference/novelwriter_prefs.conf +++ b/tests/reference/novelwriter_prefs.conf @@ -16,8 +16,8 @@ docpane = 400, 400 viewpane = 500, 150 outlinepane = 500, 150 fullscreen = False -hidevscroll = False -hidehscroll = False +hidevscroll = True +hidehscroll = True [Project] autosaveproject = 40 @@ -39,8 +39,8 @@ repsquotes = True repdquotes = True repdash = True repdots = True -scrollpastend = True -scollwithcursor = False +scrollpastend = False +scollwithcursor = True fmtsinglequote = ‘, ’ fmtdoublequote = “, ” spelltool = internal diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index cf04284b..1863d991 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -1077,6 +1077,16 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC qtbot.mouseClick(tabGeneral.showFullPath, Qt.LeftButton) assert not tabGeneral.showFullPath.isChecked() + qtbot.wait(keyDelay) + assert not tabGeneral.hideVScroll.isChecked() + qtbot.mouseClick(tabGeneral.hideVScroll, Qt.LeftButton) + assert tabGeneral.hideVScroll.isChecked() + + qtbot.wait(keyDelay) + assert not tabGeneral.hideHScroll.isChecked() + qtbot.mouseClick(tabGeneral.hideHScroll, Qt.LeftButton) + assert tabGeneral.hideHScroll.isChecked() + # Check font button monkeypatch.setattr(QFontDialog, "getFont", lambda font, obj: (font, True)) qtbot.mouseClick(tabGeneral.fontButton, Qt.LeftButton) @@ -1135,6 +1145,16 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC qtbot.mouseClick(tabLayout.textJustify, Qt.LeftButton) assert not tabLayout.textJustify.isChecked() + qtbot.wait(keyDelay) + assert tabLayout.scrollPastEnd.isChecked() + qtbot.mouseClick(tabLayout.scrollPastEnd, Qt.LeftButton) + assert not tabLayout.scrollPastEnd.isChecked() + + qtbot.wait(keyDelay) + assert not tabLayout.scollWithCursor.isChecked() + qtbot.mouseClick(tabLayout.scollWithCursor, Qt.LeftButton) + assert tabLayout.scollWithCursor.isChecked() + # Editor Settings qtbot.wait(keyDelay) tabEditing = nwPrefs.tabEditing From 710ad598d99cec0974f47dcfa5ebbfeb3c67dcbc Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 20:34:51 +0200 Subject: [PATCH 030/104] Count words when saving editor text --- nw/gui/doceditor.py | 5 +++++ nw/gui/projtree.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 1d1d2a4e..13c67650 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -360,9 +360,14 @@ class GuiDocEditor(QTextEdit): return False docText = self.getText() + + cC, wC, pC = countWords(docText) + self._updateCounts(cC, wC, pC) + theItem.setCharCount(self.charCount) theItem.setWordCount(self.wordCount) theItem.setParaCount(self.paraCount) + self.saveCursorPosition() self.nwDocument.saveDocument(docText) self.setDocumentChanged(False) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 858948dc..2d8900cf 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -241,7 +241,8 @@ class GuiProjectTree(QTreeWidget): # Add the new item to the tree if tHandle is not None: self.revealNewTreeItem(tHandle, nHandle) - self.theParent.editItem(tHandle) + if self.mainConf.showGUI: + self.theParent.editItem(tHandle) return True From 92c87872adbc7da14d7cb841e6e220866f4decdb Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 20:58:45 +0200 Subject: [PATCH 031/104] Try to fix writing stats test that keeps randomly failing --- tests/test_dialogs.py | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 1863d991..1b9cf880 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -228,6 +228,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): # Create new, save, close project nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": nwFuncTemp}) + qtbot.wait(200) assert nwGUI.saveProject() assert nwGUI.closeProject() qtbot.wait(stepDelay) @@ -236,11 +237,12 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger) assert getGuiItem("GuiWritingStats") is None + # Add some text to the scene file assert nwGUI.openProject(nwFuncTemp) qtbot.wait(stepDelay) - # Add some text to the scene file assert nwGUI.openDocument("0e17daca5f3e1") + nwGUI.docEditor.clear() assert nwGUI.docEditor.insertText( "# Scene One\n\n" "It was the best of times, it was the worst of times, it was the age of wisdom, it was " @@ -252,10 +254,18 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): "insisted on its being received, for good or for evil, in the superlative degree of " "comparison only.\n\n" ) + qtbot.wait(stepDelay) assert nwGUI.saveDocument() + qtbot.wait(200) # Ensures that the session length is > 0 + + assert nwGUI.saveProject() + assert nwGUI.closeProject() + qtbot.wait(stepDelay) # Add a note file with some text - nwGUI.setFocus(1) + assert nwGUI.openProject(nwFuncTemp) + qtbot.wait(stepDelay) + nwGUI.treeView.clearSelection() nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True) nwGUI.treeView.newTreeItem(nwItemType.FILE, None) @@ -264,8 +274,9 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): "# Jane Doe\n\n" "All about Jane.\n\n" ) + qtbot.wait(stepDelay) assert nwGUI.saveDocument() - qtbot.wait(500) # Ensures that the session length is > 0 + qtbot.wait(200) # Ensures that the session length is > 0 assert nwGUI.saveProject() assert nwGUI.closeProject() @@ -296,11 +307,12 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) - assert len(jsonData) == 2 + qtbot.wait(stepDelay) + assert len(jsonData) == 3 assert jsonData[1]["length"] >= 0 - assert jsonData[1]["newWords"] == 126 - assert jsonData[1]["novelWords"] == 127 - assert jsonData[1]["noteWords"] == 5 + assert jsonData[1]["newWords"] == 119 + assert jsonData[1]["novelWords"] == 125 + assert jsonData[1]["noteWords"] == 0 # No Novel Files qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton) @@ -315,7 +327,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert len(jsonData) == 1 assert jsonData[0]["length"] >= 0 assert jsonData[0]["newWords"] == 5 - assert jsonData[0]["novelWords"] == 127 + assert jsonData[0]["novelWords"] == 125 assert jsonData[0]["noteWords"] == 5 # No Note Files @@ -331,9 +343,9 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert len(jsonData) == 2 assert jsonData[1]["length"] >= 0 - assert jsonData[1]["newWords"] == 121 - assert jsonData[1]["novelWords"] == 127 - assert jsonData[1]["noteWords"] == 5 + assert jsonData[1]["newWords"] == 119 + assert jsonData[1]["novelWords"] == 125 + assert jsonData[1]["noteWords"] == 0 # No Negative Entries qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton) @@ -346,7 +358,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) - assert len(jsonData) == 2 + assert len(jsonData) == 3 # Un-hide Zero Entries qtbot.mouseClick(sessLog.hideNegative, Qt.LeftButton) @@ -359,7 +371,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) - assert len(jsonData) == 2 + assert len(jsonData) == 3 # Group by Day qtbot.mouseClick(sessLog.groupByDay, Qt.LeftButton) From 6ee326e7168eae7fbc48909703320b7581006e40 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 21:21:33 +0200 Subject: [PATCH 032/104] Hardcode the values of the session stats log --- tests/test_dialogs.py | 76 ++++++++++++------------------------------- 1 file changed, 20 insertions(+), 56 deletions(-) diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 1b9cf880..a78a684b 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -23,7 +23,7 @@ from nw.gui import ( GuiProjectLoad, GuiPreferences ) from nw.gui.custom import QuotesDialog -from nw.constants import nwItemType, nwItemLayout, nwItemClass +from nw.constants import nwItemType, nwItemLayout, nwItemClass, nwFiles keyDelay = 2 typeDelay = 1 @@ -233,54 +233,15 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert nwGUI.closeProject() qtbot.wait(stepDelay) - # Check that we cannot open when there is no project - nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger) - assert getGuiItem("GuiWritingStats") is None - - # Add some text to the scene file - assert nwGUI.openProject(nwFuncTemp) - qtbot.wait(stepDelay) - - assert nwGUI.openDocument("0e17daca5f3e1") - nwGUI.docEditor.clear() - assert nwGUI.docEditor.insertText( - "# Scene One\n\n" - "It was the best of times, it was the worst of times, it was the age of wisdom, it was " - "the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it " - "was the season of Light, it was the season of Darkness, it was the spring of hope, it " - "was the winter of despair, we had everything before us, we had nothing before us, we " - "were all going direct to Heaven, we were all going direct the other way – in short, the " - "period was so far like the present period, that some of its noisiest authorities " - "insisted on its being received, for good or for evil, in the superlative degree of " - "comparison only.\n\n" - ) - qtbot.wait(stepDelay) - assert nwGUI.saveDocument() - qtbot.wait(200) # Ensures that the session length is > 0 - - assert nwGUI.saveProject() - assert nwGUI.closeProject() - qtbot.wait(stepDelay) - - # Add a note file with some text - assert nwGUI.openProject(nwFuncTemp) - qtbot.wait(stepDelay) - - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) - assert nwGUI.openSelectedItem() - assert nwGUI.docEditor.insertText( - "# Jane Doe\n\n" - "All about Jane.\n\n" - ) - qtbot.wait(stepDelay) - assert nwGUI.saveDocument() - qtbot.wait(200) # Ensures that the session length is > 0 - - assert nwGUI.saveProject() - assert nwGUI.closeProject() - qtbot.wait(stepDelay) + sessFile = os.path.join(nwFuncTemp, "meta", nwFiles.SESS_STATS) + with open(sessFile, mode="w+", encoding="utf-8") as outFile: + outFile.write( + "# Start Time End Time Novel Notes\n" + "2020-01-01 21:00:00 2020-01-01 21:00:05 6 0\n" + "2020-01-03 21:00:00 2020-01-03 21:00:15 125 0\n" + "2020-01-03 21:30:00 2020-01-03 21:30:15 125 5\n" + "2020-01-06 21:00:00 2020-01-06 21:00:10 125 5\n" + ) # Open again, and check the stats assert nwGUI.openProject(nwFuncTemp) @@ -299,17 +260,18 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda ss, tt, pp, options: (pp, "")) assert sessLog._saveData(sessLog.FMT_CSV) - qtbot.wait(stepDelay) + qtbot.wait(100) assert sessLog._saveData(sessLog.FMT_JSON) - qtbot.wait(stepDelay) + qtbot.wait(100) jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) qtbot.wait(stepDelay) + assert len(jsonData) == 3 - assert jsonData[1]["length"] >= 0 + assert jsonData[1]["length"] >= 14.0 assert jsonData[1]["newWords"] == 119 assert jsonData[1]["novelWords"] == 125 assert jsonData[1]["noteWords"] == 0 @@ -325,7 +287,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): jsonData = json.loads(inFile.read()) assert len(jsonData) == 1 - assert jsonData[0]["length"] >= 0 + assert jsonData[0]["length"] >= 14.0 assert jsonData[0]["newWords"] == 5 assert jsonData[0]["novelWords"] == 125 assert jsonData[0]["noteWords"] == 5 @@ -342,7 +304,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): jsonData = json.loads(inFile.read()) assert len(jsonData) == 2 - assert jsonData[1]["length"] >= 0 + assert jsonData[1]["length"] >= 14.0 assert jsonData[1]["newWords"] == 119 assert jsonData[1]["novelWords"] == 125 assert jsonData[1]["noteWords"] == 0 @@ -371,7 +333,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) - assert len(jsonData) == 3 + assert len(jsonData) == 4 # Group by Day qtbot.mouseClick(sessLog.groupByDay, Qt.LeftButton) @@ -385,11 +347,13 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): # Check against both 1 and 2 as this can be 2 if test was started just before midnight. # A failed test should in any case produce a 4 - assert len(jsonData) in (1, 2) + assert len(jsonData) == 3 # qtbot.stopForInteraction() sessLog._doClose() + assert nwGUI.closeProject() + qtbot.wait(stepDelay) nwGUI.closeMain() @pytest.mark.gui From 870c5d83435f7cd56254e9e4d903d533eb526b66 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 21:23:39 +0200 Subject: [PATCH 033/104] Fix flake8 test --- tests/test_dialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index a78a684b..ddc77738 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -23,7 +23,7 @@ from nw.gui import ( GuiProjectLoad, GuiPreferences ) from nw.gui.custom import QuotesDialog -from nw.constants import nwItemType, nwItemLayout, nwItemClass, nwFiles +from nw.constants import nwItemLayout, nwItemClass, nwFiles keyDelay = 2 typeDelay = 1 From 9c75ed5b06c69239d9b52e34e1f9f8e61dd827ed Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 21:39:42 +0200 Subject: [PATCH 034/104] Leave scroll bar on build tool document, and don't scroll too far past end in doc editor --- nw/gui/build.py | 11 ----------- nw/gui/doceditor.py | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index 754553d8..37880cca 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -1124,17 +1124,6 @@ class GuiBuildNovelDocView(QTextBrowser): else: self.setTabStopWidth(self.mainConf.getTabWidth()) - # Scroll bars - if self.mainConf.hideVScroll: - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - else: - self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) - - if self.mainConf.hideHScroll: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - else: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - docPalette = self.palette() docPalette.setColor(QPalette.Base, QColor(255, 255, 255)) docPalette.setColor(QPalette.Text, QColor(0, 0, 0)) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index d65c89df..99e86764 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -440,7 +440,7 @@ class GuiDocEditor(QTextEdit): if self.mainConf.scrollPastEnd: docFrame = self.qDocument.rootFrame().frameFormat() - docFrame.setBottomMargin(wH - uM - lM - 4*tB - self.theTheme.fontPixelSize) + docFrame.setBottomMargin(wH - uM - lM - 5*self.theTheme.fontPixelSize) self.qDocument.rootFrame().setFrameFormat(docFrame) return From 77ec41a820518edd67967f4b9c126347b938d852 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 21:43:26 +0200 Subject: [PATCH 035/104] Make sure scroll past end margin cannot be negative --- nw/gui/doceditor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 99e86764..9cbe1abb 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -440,7 +440,8 @@ class GuiDocEditor(QTextEdit): if self.mainConf.scrollPastEnd: docFrame = self.qDocument.rootFrame().frameFormat() - docFrame.setBottomMargin(wH - uM - lM - 5*self.theTheme.fontPixelSize) + docMargin = wH - uM - lM - 5*self.theTheme.fontPixelSize + docFrame.setBottomMargin(max(0, docMargin)) self.qDocument.rootFrame().setFrameFormat(docFrame) return From c76c2df9255284096880b975d89ba0278de8cb97 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 22:52:06 +0200 Subject: [PATCH 036/104] Simplify the typewriter mode --- nw/config.py | 5 +++ nw/gui/doceditor.py | 42 +++++--------------------- nw/gui/preferences.py | 24 +++++++++++++-- tests/reference/novelwriter.conf | 3 +- tests/reference/novelwriter_prefs.conf | 1 + 5 files changed, 37 insertions(+), 38 deletions(-) diff --git a/nw/config.py b/nw/config.py index 5ed12655..66b03bcc 100644 --- a/nw/config.py +++ b/nw/config.py @@ -128,6 +128,7 @@ class Config: self.doReplaceDots = True self.scrollPastEnd = True self.scollWithCursor = False + self.scollFromPoint = 40 self.wordCountTimer = 5.0 self.showTabsNSpaces = False @@ -467,6 +468,9 @@ class Config: self.scollWithCursor = self._parseLine( cnfParse, cnfSec, "scollwithcursor", self.CNF_BOOL, self.scollWithCursor ) + self.scollFromPoint = self._parseLine( + cnfParse, cnfSec, "scollfrompoint", self.CNF_INT, self.scollFromPoint + ) self.fmtSingleQuotes = self._parseLine( cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes ) @@ -613,6 +617,7 @@ class Config: cnfParse.set(cnfSec, "repdots", str(self.doReplaceDots)) cnfParse.set(cnfSec, "scrollpastend", str(self.scrollPastEnd)) cnfParse.set(cnfSec, "scollwithcursor", str(self.scollWithCursor)) + cnfParse.set(cnfSec, "scollfrompoint", str(self.scollFromPoint)) cnfParse.set(cnfSec, "fmtsinglequote", self._packList(self.fmtSingleQuotes)) cnfParse.set(cnfSec, "fmtdoublequote", self._packList(self.fmtDoubleQuotes)) cnfParse.set(cnfSec, "spelltool", str(self.spellTool)) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 9cbe1abb..8ccdac08 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -88,8 +88,6 @@ class GuiDocEditor(QTextEdit): self.bigDoc = False # Flag for very large document size self.doReplace = False # Switch to temporarily disable auto-replace self.queuePos = None # Used for delayed change of cursor position - self.cursorLast = 0 # The last known vertical position of the cursor - self.lengthLast = 0 # Typography self.typDQOpen = self.mainConf.fmtDoubleQuotes[0] @@ -102,8 +100,6 @@ class GuiDocEditor(QTextEdit): self.qDocument.contentsChange.connect(self._docChange) self.qDocument.documentLayout().documentSizeChanged.connect(self._docSizeChanged) - self.verticalScrollBar().sliderMoved.connect(self._doVerticalScroll) - # Document Title self.docHeader = GuiDocEditHeader(self) self.docFooter = GuiDocEditFooter(self) @@ -440,7 +436,7 @@ class GuiDocEditor(QTextEdit): if self.mainConf.scrollPastEnd: docFrame = self.qDocument.rootFrame().frameFormat() - docMargin = wH - uM - lM - 5*self.theTheme.fontPixelSize + docMargin = wH - uM - lM - 4*tB - 5*self.theTheme.fontPixelSize docFrame.setBottomMargin(max(0, docMargin)) self.qDocument.rootFrame().setFrameFormat(docFrame) @@ -496,7 +492,6 @@ class GuiDocEditor(QTextEdit): theCursor.setPosition(thePosition) self.setTextCursor(theCursor) self.docFooter.updateLineCount() - self.cursorLast = self.cursorRect().center().y() return True @@ -777,20 +772,13 @@ class GuiDocEditor(QTextEdit): self.docFooter.updateLineCount() if self.mainConf.scollWithCursor: - docLen = self.qDocument.characterCount() - if docLen == self.lengthLast: - # No change, so just update last position - self.cursorLast = self.cursorRect().center().y() - else: - # The user typed something, so check if we need to - # scroll, and move the scroll bar the same distance - self.lengthLast = docLen - self.ensureCursorVisible() + kMod = keyEvent.modifiers() + if kMod == Qt.NoModifier or kMod == Qt.ShiftModifier: cPos = self.cursorRect().center().y() - if cPos != self.cursorLast: - vBar = self.verticalScrollBar() - vBar.setValue(vBar.value() + cPos - self.cursorLast) - self.cursorLast = self.cursorRect().center().y() + mPos = self.mainConf.scollFromPoint*self.height()*0.01 + vBar = self.verticalScrollBar() + vBar.setValue(vBar.value() + cPos - round(mPos)) + self.ensureCursorVisible() return @@ -817,18 +805,9 @@ class GuiDocEditor(QTextEdit): QTextEdit.mouseReleaseEvent(self, mEvent) self.docFooter.updateLineCount() - self.cursorLast = self.cursorRect().center().y() return - def wheelEvent(self, theEvent): - """Briefly capture the mouse wheel event to capture the cursor - position. - """ - QTextEdit.wheelEvent(self, theEvent) - self.cursorLast = self.cursorRect().center().y() - return - def resizeEvent(self, theEvent): """If the text editor is resize, we must make sure the document has its margins adjusted according to user preferences. @@ -863,13 +842,6 @@ class GuiDocEditor(QTextEdit): self._docAutoReplace(self.qDocument.findBlock(thePos)) return - @pyqtSlot(int) - def _doVerticalScroll(self, theChange): - """Update the cursor position on vertical scrolling. - """ - self.cursorLast = self.cursorRect().center().y() - return - @pyqtSlot("QPoint") def _openContextMenu(self, thePos): """Triggered by right click to open the context menu. Also diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index f1848bb3..7dffd340 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -555,6 +555,10 @@ class GuiConfigEditLayoutTab(QWidget): theUnit="px" ) + # Scroll Behaviour + # ================ + self.mainForm.addGroupLabel("Scroll Behaviour") + ## Scroll Past End self.scrollPastEnd = QSwitch() self.scrollPastEnd.setChecked(self.mainConf.scrollPastEnd) @@ -568,11 +572,25 @@ class GuiConfigEditLayoutTab(QWidget): self.scollWithCursor = QSwitch() self.scollWithCursor.setChecked(self.mainConf.scollWithCursor) self.mainForm.addRow( - "Typewriter style scrolling", + "Typewriter style scrolling when you type", self.scollWithCursor, - "Scrolls up when the cursor moves to a new line." + "Tries to keep the cursor at a fixed vertical position." ) + ## Font Size + self.scollFromPoint = QSpinBox(self) + self.scollFromPoint.setMinimum(10) + self.scollFromPoint.setMaximum(90) + self.scollFromPoint.setSingleStep(1) + self.scollFromPoint.setValue(self.mainConf.scollFromPoint) + self.mainForm.addRow( + "Position in the editor to keep the cursor", + self.scollFromPoint, + "In units of percentage of the editor height.", + theUnit = "%" + ) + + return def saveValues(self): @@ -592,6 +610,7 @@ class GuiConfigEditLayoutTab(QWidget): tabWidth = self.tabWidth.value() scrollPastEnd = self.scrollPastEnd.isChecked() scollWithCursor = self.scollWithCursor.isChecked() + scollFromPoint = self.scollFromPoint.value() self.mainConf.textFont = textFont self.mainConf.textSize = textSize @@ -604,6 +623,7 @@ class GuiConfigEditLayoutTab(QWidget): self.mainConf.tabWidth = tabWidth self.mainConf.scrollPastEnd = scrollPastEnd self.mainConf.scollWithCursor = scollWithCursor + self.mainConf.scollFromPoint = scollFromPoint self.mainConf.confChanged = True diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf index f4bc8d61..aa09b213 100644 --- a/tests/reference/novelwriter.conf +++ b/tests/reference/novelwriter.conf @@ -1,5 +1,5 @@ [Main] -timestamp = 2020-10-11 18:29:34 +timestamp = 2020-10-11 22:50:45 theme = default syntax = default_light icons = typicons_colour_light @@ -41,6 +41,7 @@ repdash = True repdots = True scrollpastend = True scollwithcursor = False +scollfrompoint = 40 fmtsinglequote = ‘, ’ fmtdoublequote = “, ” spelltool = internal diff --git a/tests/reference/novelwriter_prefs.conf b/tests/reference/novelwriter_prefs.conf index 0e1d41f6..b2cceaff 100644 --- a/tests/reference/novelwriter_prefs.conf +++ b/tests/reference/novelwriter_prefs.conf @@ -41,6 +41,7 @@ repdash = True repdots = True scrollpastend = False scollwithcursor = True +scollfrompoint = 40 fmtsinglequote = ‘, ’ fmtdoublequote = “, ” spelltool = internal From 322be33ac40b455c3a5e34a46ca3b858f561d262 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 22:53:38 +0200 Subject: [PATCH 037/104] Fix flake8 test --- nw/gui/preferences.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index 7dffd340..1af89953 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -590,7 +590,6 @@ class GuiConfigEditLayoutTab(QWidget): theUnit = "%" ) - return def saveValues(self): From d0bda15b0ad1e28e167a274fe3ec41b826b9aa0c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 11 Oct 2020 23:03:37 +0200 Subject: [PATCH 038/104] Change name of scroll position variable and fix calculation of it --- nw/config.py | 8 ++++---- nw/gui/doceditor.py | 4 ++-- nw/gui/preferences.py | 16 ++++++++-------- tests/reference/novelwriter.conf | 2 +- tests/reference/novelwriter_prefs.conf | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/nw/config.py b/nw/config.py index 66b03bcc..b4c14b27 100644 --- a/nw/config.py +++ b/nw/config.py @@ -128,7 +128,7 @@ class Config: self.doReplaceDots = True self.scrollPastEnd = True self.scollWithCursor = False - self.scollFromPoint = 40 + self.scollToPoint = 40 self.wordCountTimer = 5.0 self.showTabsNSpaces = False @@ -468,8 +468,8 @@ class Config: self.scollWithCursor = self._parseLine( cnfParse, cnfSec, "scollwithcursor", self.CNF_BOOL, self.scollWithCursor ) - self.scollFromPoint = self._parseLine( - cnfParse, cnfSec, "scollfrompoint", self.CNF_INT, self.scollFromPoint + self.scollToPoint = self._parseLine( + cnfParse, cnfSec, "scolltopoint", self.CNF_INT, self.scollToPoint ) self.fmtSingleQuotes = self._parseLine( cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes @@ -617,7 +617,7 @@ class Config: cnfParse.set(cnfSec, "repdots", str(self.doReplaceDots)) cnfParse.set(cnfSec, "scrollpastend", str(self.scrollPastEnd)) cnfParse.set(cnfSec, "scollwithcursor", str(self.scollWithCursor)) - cnfParse.set(cnfSec, "scollfrompoint", str(self.scollFromPoint)) + cnfParse.set(cnfSec, "scolltopoint", str(self.scollToPoint)) cnfParse.set(cnfSec, "fmtsinglequote", self._packList(self.fmtSingleQuotes)) cnfParse.set(cnfSec, "fmtdoublequote", self._packList(self.fmtDoubleQuotes)) cnfParse.set(cnfSec, "spelltool", str(self.spellTool)) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 8ccdac08..a7779845 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -775,9 +775,9 @@ class GuiDocEditor(QTextEdit): kMod = keyEvent.modifiers() if kMod == Qt.NoModifier or kMod == Qt.ShiftModifier: cPos = self.cursorRect().center().y() - mPos = self.mainConf.scollFromPoint*self.height()*0.01 + mPos = self.mainConf.scollToPoint * self.viewport().height() vBar = self.verticalScrollBar() - vBar.setValue(vBar.value() + cPos - round(mPos)) + vBar.setValue(vBar.value() + cPos - round(mPos * 0.01)) self.ensureCursorVisible() return diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index 1af89953..2a1cf653 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -578,14 +578,14 @@ class GuiConfigEditLayoutTab(QWidget): ) ## Font Size - self.scollFromPoint = QSpinBox(self) - self.scollFromPoint.setMinimum(10) - self.scollFromPoint.setMaximum(90) - self.scollFromPoint.setSingleStep(1) - self.scollFromPoint.setValue(self.mainConf.scollFromPoint) + self.scollToPoint = QSpinBox(self) + self.scollToPoint.setMinimum(10) + self.scollToPoint.setMaximum(90) + self.scollToPoint.setSingleStep(1) + self.scollToPoint.setValue(self.mainConf.scollToPoint) self.mainForm.addRow( "Position in the editor to keep the cursor", - self.scollFromPoint, + self.scollToPoint, "In units of percentage of the editor height.", theUnit = "%" ) @@ -609,7 +609,7 @@ class GuiConfigEditLayoutTab(QWidget): tabWidth = self.tabWidth.value() scrollPastEnd = self.scrollPastEnd.isChecked() scollWithCursor = self.scollWithCursor.isChecked() - scollFromPoint = self.scollFromPoint.value() + scollToPoint = self.scollToPoint.value() self.mainConf.textFont = textFont self.mainConf.textSize = textSize @@ -622,7 +622,7 @@ class GuiConfigEditLayoutTab(QWidget): self.mainConf.tabWidth = tabWidth self.mainConf.scrollPastEnd = scrollPastEnd self.mainConf.scollWithCursor = scollWithCursor - self.mainConf.scollFromPoint = scollFromPoint + self.mainConf.scollToPoint = scollToPoint self.mainConf.confChanged = True diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf index aa09b213..bb141482 100644 --- a/tests/reference/novelwriter.conf +++ b/tests/reference/novelwriter.conf @@ -41,7 +41,7 @@ repdash = True repdots = True scrollpastend = True scollwithcursor = False -scollfrompoint = 40 +scolltopoint = 40 fmtsinglequote = ‘, ’ fmtdoublequote = “, ” spelltool = internal diff --git a/tests/reference/novelwriter_prefs.conf b/tests/reference/novelwriter_prefs.conf index b2cceaff..ee591782 100644 --- a/tests/reference/novelwriter_prefs.conf +++ b/tests/reference/novelwriter_prefs.conf @@ -41,7 +41,7 @@ repdash = True repdots = True scrollpastend = False scollwithcursor = True -scollfrompoint = 40 +scolltopoint = 40 fmtsinglequote = ‘, ’ fmtdoublequote = “, ” spelltool = internal From d2325c57a34f4596321430995ba1714e43afbb8f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 13 Oct 2020 11:00:13 +0200 Subject: [PATCH 039/104] Change minimum Python version to 3.6 --- nw/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nw/__init__.py b/nw/__init__.py index 46c065b0..72327b2b 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -200,9 +200,9 @@ def main(sysArgs=None): # Check Packages and Versions errorData = [] errorCode = 0 - if sys.hexversion < 0x030403f0: + if sys.hexversion < 0x030600f0: errorData.append( - "At least Python 3.4.3 is required, but 3.6 is highly recommended." + "At least Python 3.6.0 is required, found %s." % CONFIG.verPyString ) errorCode |= 4 if CONFIG.verQtValue < 50200: From e5b24e39eec883e9048b9a0bee40423e05214a0e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 13 Oct 2020 19:27:50 +0200 Subject: [PATCH 040/104] Don't import nw in setup.py --- install.py | 4 ++-- setup.cfg | 1 + setup.py | 21 ++++++++++----------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/install.py b/install.py index ea7c976d..11754a26 100755 --- a/install.py +++ b/install.py @@ -35,7 +35,7 @@ except getopt.GetoptError: for inOpt, inArg in inOpts: if inOpt in ("-h", "--help"): print(helpMsg) - sys.exit() + sys.exit(0) elif inOpt in ("-d", "--debug"): buildWindowed = False @@ -72,7 +72,7 @@ if buildWindowed: instOpt.append("novelWriter.py") -import PyInstaller.__main__ # noqa: F401 +import PyInstaller.__main__ # noqa: E402 PyInstaller.__main__.run(instOpt) print("") diff --git a/setup.cfg b/setup.cfg index d9410a61..a791e31e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,6 @@ [metadata] license_files = LICENSE.md +version = attr: nw.__version__ [bdist_wheel] universal = 0 diff --git a/setup.py b/setup.py index f0a9eb89..6b10a84e 100755 --- a/setup.py +++ b/setup.py @@ -4,8 +4,6 @@ import sys import subprocess import setuptools -from nw import __version__, __url__, __docurl__, __issuesurl__, __sourceurl__ - ## # Build the Package ## @@ -119,19 +117,16 @@ if len(sys.argv) == 1: with open("README.md", "r") as inFile: longDescription = inFile.read() -with open("requirements.txt", "r") as inFile: - pkgRequirements = inFile.read().strip().splitlines() - setuptools.setup( name = "novelWriter", - version = __version__, + # version = __version__, # Set in setup.cfg author = "Veronica Berglyd Olsen", author_email = "code@vkbo.net", description = "A markdown-like document editor for writing novels", long_description = longDescription, long_description_content_type = "text/markdown", license = "GNU General Public License v3", - url = __url__, + url = "https://novelwriter.io", entry_points = { "console_scripts" : ["novelWriter-cli=nw:main"], "gui_scripts" : ["novelWriter=nw:main"], @@ -140,9 +135,9 @@ setuptools.setup( include_package_data = True, package_data = {"": ["*.conf"]}, project_urls = { - "Bug Tracker": __issuesurl__, - "Documentation": __docurl__, - "Source Code": __sourceurl__, + "Bug Tracker": "https://github.com/vkbo/novelWriter/issues", + "Documentation": "https://github.com/vkbo/novelWriter/issues", + "Source Code": "https://github.com/vkbo/novelWriter", }, classifiers = [ "Programming Language :: Python :: 3 :: Only", @@ -159,5 +154,9 @@ setuptools.setup( "Topic :: Text Editors", ], python_requires = ">=3.6", - install_requires = pkgRequirements, + install_requires = [ + "pyqt5>=5.2.1", + "lxml>=4.2.0", + "pyenchant>=3.0.0", + ], ) From 7c5a098cabe4188a496ac9c9cb574b78c7cdd76d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 13 Oct 2020 19:37:53 +0200 Subject: [PATCH 041/104] Update docs --- README.md | 6 ++---- docs/source/int_introduction.rst | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9048ffe9..46273c44 100644 --- a/README.md +++ b/README.md @@ -89,9 +89,8 @@ to make those. Feel free to submit more if you are able to make them. ## Package Dependencies -It is recommended that novelWriter runs with Qt 5.10 or later, and Python 3.6 or later. Running with -Qt as low as 5.2.1 and Python 3.4.3 has been tested, and worked in the past, but there are no -guarantees that this will keep working as these are not a part of the test builds. +It is recommended that novelWriter runs with Qt 5.10 or later, and requires Python 3.6 or later. +Minimum version of Qt is 5.2. ### Linux @@ -146,7 +145,6 @@ C:\...\AppData\Local\Programs\Python\Python38\python.exe novelWriter.py ### Package Versions -PyQt/Qt should be at least 5.3, but ideally 5.10 or higher for nearly all features to work. Exporting to Markdown requires PyQt/Qt 5.14. There are no known minimum for `lxml`, but the code was originally written with 4.2. The optional spell check library must be at least 3.0.0 to work with Windows 64 bit systems. On Linux, 2.0.0 also works fine. diff --git a/docs/source/int_introduction.rst b/docs/source/int_introduction.rst index 6ca43b5f..66ca0351 100644 --- a/docs/source/int_introduction.rst +++ b/docs/source/int_introduction.rst @@ -46,7 +46,7 @@ than the document editor itself are hidden away. The colour scheme of the user interface defaults to that of the host operating system. In addition, a dark theme is provided, and can be enabled in :guilabel:`Preferences` from the :guilabel:`Tools` menu. A number of syntax highlighting themes are also available in :guilabel:`Preferences`. A set of -icon themes in colour and greyscale are also offered. The icons are based on the Typicon_ icon set +icon themes in colour and greyscale are also offered. The icons are based on the Typicons_ icon set designed by Stephen Hutchings. The main window is split in two, or optionally three, panels. The left-most contains the project @@ -58,7 +58,7 @@ entire novel structure can be displayed, with all the tags and references listed you structure your novel project files, this outline can be quite different than your project tree. Your project tree lists files, your Outline tree lists the structure of the novel itself. -.. _Typicon: https://github.com/stephenhutchings/typicons.font +.. _Typicons: https://github.com/stephenhutchings/typicons.font .. _a_intro_project: From 2861f4be3627db37dc8ab4598ba80c86d83dc7ba Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 13 Oct 2020 20:16:06 +0200 Subject: [PATCH 042/104] Fix a bug in build tool for lower Qt versions --- nw/gui/build.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index 37880cca..1a69b62e 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -1013,8 +1013,9 @@ class GuiBuildNovel(QDialog): """ self.saveODT.setEnabled(theState) self.savePDF.setEnabled(theState) - self.saveMD.setEnabled(theState) self.saveTXT.setEnabled(theState) + if self.mainConf.verQtValue >= 51400: + self.saveMD.setEnabled(theState) return def _saveSettings(self): From 7d1e85c0d450cd499160d0e33e11a77fdebe94d1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 13 Oct 2020 21:51:49 +0200 Subject: [PATCH 043/104] Animate the scroll bar movement when following cursor --- nw/gui/custom.py | 2 +- nw/gui/doceditor.py | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/nw/gui/custom.py b/nw/gui/custom.py index 03fc6318..2955c447 100644 --- a/nw/gui/custom.py +++ b/nw/gui/custom.py @@ -326,7 +326,7 @@ class QSwitch(QAbstractButton): """ super().mouseReleaseEvent(event) if event.button() == Qt.LeftButton: - doAnim = QPropertyAnimation(self, b'offset', self) + doAnim = QPropertyAnimation(self, b"offset", self) doAnim.setDuration(120) doAnim.setStartValue(self.offset) if self.isChecked(): diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index a7779845..34ae52f2 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -38,7 +38,7 @@ from time import time from PyQt5.QtCore import ( Qt, QSize, QTimer, pyqtSlot, pyqtSignal, QRegExp, QRegularExpression, - QPointF, QObject, QRunnable + QPointF, QObject, QRunnable, QPropertyAnimation ) from PyQt5.QtGui import ( QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette, @@ -774,11 +774,22 @@ class GuiDocEditor(QTextEdit): if self.mainConf.scollWithCursor: kMod = keyEvent.modifiers() if kMod == Qt.NoModifier or kMod == Qt.ShiftModifier: + hWid = self.viewport().height() cPos = self.cursorRect().center().y() - mPos = self.mainConf.scollToPoint * self.viewport().height() + mPos = self.mainConf.scollToPoint * hWid vBar = self.verticalScrollBar() - vBar.setValue(vBar.value() + cPos - round(mPos * 0.01)) - self.ensureCursorVisible() + + # Compute the needed scroll and duration + pOld = vBar.value() + pNew = pOld + cPos - round(mPos*0.01) + aDur = 150 + round(abs(pNew - pOld)/hWid*500) + + if pNew >= 0: + doAnim = QPropertyAnimation(vBar, b"value", self) + doAnim.setDuration(aDur) + doAnim.setStartValue(pOld) + doAnim.setEndValue(pNew) + doAnim.start() return From 10e97a567b9729c62a75c38c95daa1fae054ee53 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 13 Oct 2020 21:59:02 +0200 Subject: [PATCH 044/104] Make sure the scrolling effect is covered by test --- tests/test_gui.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_gui.py b/tests/test_gui.py index 0b32327e..4afaaa2b 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -168,6 +168,13 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI.mainMenu.aSpellCheck.setChecked(True) assert nwGUI.mainMenu._toggleSpellCheck() + # Change some settings + nwGUI.mainConf.hideHScroll = True + nwGUI.mainConf.hideVScroll = True + nwGUI.mainConf.scrollPastEnd = True + nwGUI.mainConf.scollToPoint = 80 + nwGUI.mainConf.scollWithCursor = True + # Add a Character File nwGUI.setFocus(1) nwGUI.treeView.clearSelection() From 3fbe5e218c63c07763267668c13765a6195a2175 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 13 Oct 2020 22:14:17 +0200 Subject: [PATCH 045/104] Make sure the scroll bar anim cannot go past 650 ms, and add a percentage of total to the line counter --- nw/gui/doceditor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 34ae52f2..16c2c08c 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -782,7 +782,7 @@ class GuiDocEditor(QTextEdit): # Compute the needed scroll and duration pOld = vBar.value() pNew = pOld + cPos - round(mPos*0.01) - aDur = 150 + round(abs(pNew - pOld)/hWid*500) + aDur = 150 + round(min(abs(pNew - pOld)/hWid, 1.0)*500) if pNew >= 0: doAnim = QPropertyAnimation(vBar, b"value", self) @@ -2346,11 +2346,13 @@ class GuiDocEditFooter(QWidget): """ if self.theItem is None: iLine = 0 + iDist = 0 else: theCursor = self.docEditor.textCursor() iLine = theCursor.blockNumber() + 1 + iDist = 100*iLine/self.docEditor.qDocument.blockCount() - self.linesText.setText(f"Line: {iLine:n}") + self.linesText.setText(f"Line: {iLine:n} ({iDist:.0f}\u202f%)") return From 25b77c91db89d2c66b48d60823cb224bde8d9316 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 13 Oct 2020 22:46:39 +0200 Subject: [PATCH 046/104] Add 'Follow Tag' to the context meny when right clicking a tag, and disable spell check for it --- nw/gui/doceditor.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 16c2c08c..b41fd764 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -860,11 +860,18 @@ class GuiDocEditor(QTextEdit): """ userCursor = self.textCursor() userSelection = userCursor.hasSelection() + posCursor = self.cursorForPosition(thePos) mnuContext = QMenu() - # Cut, Copy and Paste - # =================== + # Follow, Cut, Copy and Paste + # =========================== + + if self._followTag(theCursor=posCursor, loadTag=False): + mnuTag = QAction("Follow Tag", mnuContext) + mnuTag.triggered.connect(lambda: self._followTag(theCursor=posCursor)) + mnuContext.addAction(mnuTag) + mnuContext.addSeparator() if userSelection: mnuCut = QAction("Cut", mnuContext) @@ -903,10 +910,13 @@ class GuiDocEditor(QTextEdit): # Spell Checking # ============== + posCursor = self.cursorForPosition(thePos) spellCheck = self.spellCheck + if posCursor.block().text().startswith("@"): + spellCheck = False + if spellCheck: - posCursor = self.cursorForPosition(thePos) posCursor.select(QTextCursor.WordUnderCursor) theWord = posCursor.selectedText().strip().strip(self.nonWord) spellCheck &= theWord != "" @@ -970,8 +980,8 @@ class GuiDocEditor(QTextEdit): @pyqtSlot() def _runCounter(self): - """Decide whether to run the word counter, or stop the timer due - to inactivity. + """Decide whether to run the word counter, or not due to + inactivity. """ if self.wCounter.isRunning(): logger.verbose("Word counter is busy") @@ -1035,7 +1045,7 @@ class GuiDocEditor(QTextEdit): # Internal Functions ## - def _followTag(self, theCursor=None): + def _followTag(self, theCursor=None, loadTag=True): """Activated by Ctrl+Enter. Checks that we're in a block starting with '@'. We then find the word under the cursor and check that it is after the ':'. If all this is fine, we have a @@ -1060,10 +1070,15 @@ class GuiDocEditor(QTextEdit): if wPos <= cPos: return False - logger.verbose("Attempting to follow tag '%s'" % theWord) - self.theParent.docViewer.loadFromTag(theWord) + if loadTag: + logger.verbose("Attempting to follow tag '%s'" % theWord) + self.theParent.docViewer.loadFromTag(theWord) + else: + logger.verbose("Potential tag '%s'" % theWord) - return True + return True + + return False def _openSpellContext(self): """Opens the spell check context menu at the current point of From 8d64a6672fffa936d02186658d97fa5c663d81c9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 14 Oct 2020 13:43:18 +0200 Subject: [PATCH 047/104] Only apply format to the first block when multiple blocks are selected --- nw/config.py | 2 +- nw/gui/doceditor.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/nw/config.py b/nw/config.py index b4c14b27..675fad3d 100644 --- a/nw/config.py +++ b/nw/config.py @@ -955,4 +955,4 @@ class Config: return -# End Class Config +# END Class Config diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index b41fd764..67d80d40 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -1299,13 +1299,25 @@ class GuiDocEditor(QTextEdit): return theCursor def _toggleFormat(self, fLen, fChar): - """Toggle strikethrough text. + """Toggle the formatting of a specific type for a piece of text. + If more than one block is selected, the formatting is applied to + the first block. """ theCursor = self._autoSelect() if theCursor.hasSelection(): posS = theCursor.selectionStart() posE = theCursor.selectionEnd() + blockS = self.qDocument.findBlock(posS) + blockE = self.qDocument.findBlock(posE) + + if blockS != blockE: + posE = blockS.position() + blockS.length() - 1 + theCursor.clearSelection() + theCursor.setPosition(posS, QTextCursor.MoveAnchor) + theCursor.setPosition(posE, QTextCursor.KeepAnchor) + self.setTextCursor(theCursor) + numB = 0 for n in range(fLen): if self.qDocument.characterAt(posS-n-1) == fChar: From 1c27a8e0da844c44c54c7c8350dded6c7a41ef00 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 14 Oct 2020 14:13:59 +0200 Subject: [PATCH 048/104] Minor fix to the select word and select all functions --- nw/gui/doceditor.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 67d80d40..9ce47cbd 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -756,8 +756,8 @@ class GuiDocEditor(QTextEdit): * The return and enter key redirects here even if the search box has focus. Since we need these keys to continue search, we block any further interaction here while it's in focus. - * The undo/redo sequences bypasses the doAction pathway from - the menu, so we redirect them back from here. + * The undo/redo/select all sequences bypasses the docAction + pathway from the menu, so we redirect them back from here. """ isReturn = keyEvent.key() == Qt.Key_Return isReturn |= keyEvent.key() == Qt.Key_Enter @@ -767,6 +767,8 @@ class GuiDocEditor(QTextEdit): self.docAction(nwDocAction.REDO) elif keyEvent == QKeySequence.Undo: self.docAction(nwDocAction.UNDO) + elif keyEvent == QKeySequence.SelectAll: + self.docAction(nwDocAction.SEL_ALL) else: QTextEdit.keyPressEvent(self, keyEvent) self.docFooter.updateLineCount() @@ -1292,10 +1294,11 @@ class GuiDocEditor(QTextEdit): reSelect = True if reSelect: theCursor.clearSelection() - theCursor.setPosition(posE-1) - theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, posE-posS-1) + theCursor.setPosition(posS, QTextCursor.MoveAnchor) + theCursor.setPosition(posE-1, QTextCursor.KeepAnchor) self.setTextCursor(theCursor) + return theCursor def _toggleFormat(self, fLen, fChar): @@ -1429,7 +1432,10 @@ class GuiDocEditor(QTextEdit): theCursor.clearSelection() theCursor.select(selMode) - if selMode == QTextCursor.BlockUnderCursor: + if selMode == QTextCursor.WordUnderCursor: + theCursor = self._autoSelect() + + elif selMode == QTextCursor.BlockUnderCursor: # This selection mode also selects the preceding oaragraph # separator, which we want to avoid. posS = theCursor.selectionStart() From 366d6019831faa5eaa774e24c3dab864c21d7a29 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 14 Oct 2020 16:06:51 +0200 Subject: [PATCH 049/104] Change the strikethrough colour to the color labelled as 'hidden' --- nw/gui/dochighlight.py | 8 ++++---- nw/gui/docviewer.py | 6 +++--- nw/gui/theme.py | 4 ++-- tests/test_gui.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 564b1a9b..ff61ee23 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -68,7 +68,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.colDialN = QColor(0, 0, 0) self.colDialD = QColor(0, 0, 0) self.colDialS = QColor(0, 0, 0) - self.colComm = QColor(0, 0, 0) + self.colHidden = QColor(0, 0, 0) self.colKey = QColor(0, 0, 0) self.colVal = QColor(0, 0, 0) self.colSpell = QColor(0, 0, 0) @@ -92,7 +92,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.colDialN = QColor(*self.theTheme.colDialN) self.colDialD = QColor(*self.theTheme.colDialD) self.colDialS = QColor(*self.theTheme.colDialS) - self.colComm = QColor(*self.theTheme.colComm) + self.colHidden = QColor(*self.theTheme.colHidden) self.colKey = QColor(*self.theTheme.colKey) self.colVal = QColor(*self.theTheme.colVal) self.colSpell = QColor(*self.theTheme.colSpell) @@ -118,14 +118,14 @@ class GuiDocHighlighter(QSyntaxHighlighter): "header4h" : self._makeFormat(self.colHeadH, "bold", 1.2), "bold" : self._makeFormat(self.colEmph, "bold"), "italic" : self._makeFormat(self.colEmph, "italic"), - "strike" : self._makeFormat(self.colEmph, "strike"), + "strike" : self._makeFormat(self.colHidden, "strike"), "trailing" : self._makeFormat(self.colTrail, "background"), "nobreak" : self._makeFormat(self.colTrail, "background"), "dialogue1" : self._makeFormat(self.colDialN), "dialogue2" : self._makeFormat(self.colDialD), "dialogue3" : self._makeFormat(self.colDialS), "replace" : self._makeFormat(self.colRepTag), - "hidden" : self._makeFormat(self.colComm), + "hidden" : self._makeFormat(self.colHidden), "keyword" : self._makeFormat(self.colKey), "modifier" : self._makeFormat(self.colMod), "value" : self._makeFormat(self.colVal, "underline"), diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 2d9ac637..39a769b3 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -532,9 +532,9 @@ class GuiDocViewer(QTextBrowser): hColR = self.theTheme.colHead[0], hColG = self.theTheme.colHead[1], hColB = self.theTheme.colHead[2], - cColR = self.theTheme.colComm[0], - cColG = self.theTheme.colComm[1], - cColB = self.theTheme.colComm[2], + cColR = self.theTheme.colHidden[0], + cColG = self.theTheme.colHidden[1], + cColB = self.theTheme.colHidden[2], eColR = self.theTheme.colEmph[0], eColG = self.theTheme.colEmph[1], eColB = self.theTheme.colEmph[2], diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 88cdd72f..5683d1a3 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -103,7 +103,7 @@ class GuiTheme: self.colDialN = [0, 0, 0] self.colDialD = [0, 0, 0] self.colDialS = [0, 0, 0] - self.colComm = [0, 0, 0] + self.colHidden = [0, 0, 0] self.colKey = [0, 0, 0] self.colVal = [0, 0, 0] self.colSpell = [0, 0, 0] @@ -356,7 +356,7 @@ class GuiTheme: self.colDialN = self._loadColour(confParser, cnfSec, "straightquotes") self.colDialD = self._loadColour(confParser, cnfSec, "doublequotes") self.colDialS = self._loadColour(confParser, cnfSec, "singlequotes") - self.colComm = self._loadColour(confParser, cnfSec, "hidden") + self.colHidden = self._loadColour(confParser, cnfSec, "hidden") self.colKey = self._loadColour(confParser, cnfSec, "keyword") self.colVal = self._loadColour(confParser, cnfSec, "value") self.colSpell = self._loadColour(confParser, cnfSec, "spellcheckline") diff --git a/tests/test_gui.py b/tests/test_gui.py index 4afaaa2b..ea7b54ec 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -1484,7 +1484,7 @@ def testThemes(qtbot, yesToAll, nwMinimal, nwTemp): assert nwGUI.theTheme.colDialN == [242, 119, 122] assert nwGUI.theTheme.colDialD == [153, 204, 153] assert nwGUI.theTheme.colDialS == [255, 204, 102] - assert nwGUI.theTheme.colComm == [153, 153, 153] + assert nwGUI.theTheme.colHidden == [153, 153, 153] assert nwGUI.theTheme.colKey == [242, 119, 122] assert nwGUI.theTheme.colVal == [204, 153, 204] assert nwGUI.theTheme.colSpell == [242, 119, 122] From 1a0f37fa452d8862f28754e663b60766368915fe Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 14 Oct 2020 18:13:42 +0200 Subject: [PATCH 050/104] Rename the regex constants --- nw/constants/constants.py | 4 ++-- nw/core/tokenizer.py | 4 ++-- nw/gui/dochighlight.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 9a539b73..e7549bb4 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -41,8 +41,8 @@ class nwConst(): class nwRegEx(): - FMT_I = r"(? Date: Wed, 14 Oct 2020 18:20:41 +0200 Subject: [PATCH 051/104] Also handle multi-paragraph formatting for quotes --- nw/gui/doceditor.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 9ce47cbd..d5e3b045 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -1237,6 +1237,11 @@ class GuiDocEditor(QTextEdit): posS = theCursor.selectionStart() posE = theCursor.selectionEnd() + blockS = self.qDocument.findBlock(posS) + blockE = self.qDocument.findBlock(posE) + if blockS != blockE: + posE = blockS.position() + blockS.length() - 1 + theCursor.clearSelection() theCursor.beginEditBlock() theCursor.setPosition(posE) @@ -1245,8 +1250,8 @@ class GuiDocEditor(QTextEdit): theCursor.insertText(tBefore) theCursor.endEditBlock() - theCursor.setPosition(posE + len(tBefore)) - theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, posE-posS) + theCursor.setPosition(posE + len(tBefore), QTextCursor.MoveAnchor) + theCursor.setPosition(posS + len(tBefore), QTextCursor.KeepAnchor) self.setTextCursor(theCursor) else: From 4fdc0ba9db5924a363bb804d04252d290d21a763 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 15 Oct 2020 10:07:53 +0200 Subject: [PATCH 052/104] Bumped version and update changelog from last release --- CHANGELOG.md | 27 +++++++++++++++------------ docs/source/conf.py | 2 +- nw/__init__.py | 6 +++--- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 571fe330..f46f8610 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,28 @@ # novelWriter ChangeLog +## Version 1.0 Release Candidate 4 [2020-10-25] + + ## Version 1.0 Beta 4 [2020-10-11] **Bugfixes** -* When the Trash folder doesn't exist because nothing has yet been deleted, the lookup function for the Trash folder's handle returns `None`. That meant that any item with a parent handle `None` would be treated as a Trash folder in many parts of the code before an actual Trash folder existed. This caused a few decision branches to make non-critical mistakes. This issue is now fixed with a new check function that takes this into account. PRs #452 and #453. -* If an older project was opened, one with a different project file layout than the more recent versions, a dialog asks whether the user wants the project updated or not. However, the function that moves files to the new location actually starts working before the dialog asks for permission. Instead, it just checks that it is allowed to change the project XML file only. The check is still run before the dialog, but the action of moving files around are now postponed to after the permission has been given and the project XML file parsed. PR #453. -* If there were multiple headings in a file, and the last paragraph did not end in a line break, the word counter for the individual sections would miss the last paragraph of the last section due to an index error. This has now been fixed. PR #453. -* The cursor position of a document in the editor would only be saved if the document had been altered. It is now also saved in the cases where the user makes no changes. PR #460. +* When the Trash folder didn't exist because nothing had been deleted yet, the lookup function for the Trash folder's handle returned `None`. That meant that any item with a parent handle `None` would be treated as a Trash folder in many parts of the code before the Trash folder was first used. This caused a few decision branches to make non-critical mistakes. In particular the project tree context menu. This issue has now been fixed with a new check function that takes this into account. PRs #452 and #453. +* If an older project was opened, one with a different project file layout than the more recent versions, a dialog asked whether the user wants the project updated or not. However, the function that moves files to their new location would actually start working before the dialog asked for permission. The permission would only be applied to the project XML file. Now, the check is still run before the dialog, but the action of moving files around are postponed to after the permission has been given and the project XML file parsed. PR #453. +* If there were multiple headings in a file, and the last paragraph did not end in a line break, the word counter for the individual sections would miss the last paragraph of the last section due to an indexing error. This has now been fixed. PR #453. +* The last cursor position of a document in the editor would only be saved if the document had been altered. It is now also saved in the cases where the user makes no changes. PR #460. * When using an aspell dictionary for spell checking, words containing a hyphen would be highlighted as misspelled. This is not the case for hunspell dictionaries. The hyphen is now taken into account when splitting sentences into words for spell check highlighting. PR #462. * Some of the file dialogs would fail with a non-critical error when the cancel button was clicked. The cancel is now captured consistently in all instances where such a dialog is used, and the calling function exited properly. PR #463. **User Interface** -* Minor changes to the text formatting on the Recent Projects dialog. PR #452. +* Some minor changes to the text formatting on the Recent Projects dialog. PR #452. * The Build Novel Project tool has been improved. The settings side panel is now scrollable, and the document and settings panel now have a movable splitter between them. This gives more flexibility to the sizes of the various parts. PR #459. -* A new option to replace tabs with spaces has been added to the Build Novel Project tool. Previously, they were always replaces for HTML output, but converting them to the HTML code for tab is actually convenient for later import into for instance Libre Office, which converts them back to regular tabs. Issue #458, PR #459. +* A new option to replace tabs with spaces has been added to the Build Novel Project tool. Previously, they were always replaces for HTML output. However, converting them to the HTML code for a tab is actually convenient for later import into for instance Libre Office, which then converts them back to regular tabs. Issue #458, PR #459. * Non-breaking spaces have been removed from the HTML conversion of keywords and tags. Issue #458, PR #459. -* An upper limit of how large a document the Build Novel Project tool can view has been set. It is 10 megabytes of generated HTML. The tool will still build larger documents, but they aren't displayed. This also limits which options are available in the "Save As" list for such large documents. Only native novelWriter exports are supported for such documents. The limit is an order of magnitude larger than a typical long novel. PR #460. +* An upper limit of how large a document the Build Novel Project tool can view has been set. It is 10 megabytes of generated HTML. The tool will still build larger documents, but they aren't displayed. This also limits which options are available in the "Save As" list for such large documents. Only native novelWriter exports are supported in such cases. The limit is an order of magnitude larger than a typical long novel. PR #460. * The language indicator in the status bar now has a tooltip stating what tool and spell check dictionary provider is being used. PR #462. -* All representations of integers, mostly word counts, are now presented in the same way. They should all use a thousand separator representation defined by the localised settings. PR #464. +* All representations of integers, mostly word counts, are now presented in the same way. They should all use a thousand separator representation defined by the local language settings. PR #464. * Many parts of the GUI have had a spin/wait cursor added for processes that may take a while and will block the GUI in the meantime. PRs #460, #463 and #464. * A line counter has been added to the footer of the document editor next to the word counter. It makes it easier to compare the position in the document when also accessing it in an external editor. PR #466. @@ -33,20 +36,20 @@ * The syntax highlighter now remembers what type of line every line in the document is. This means that certain types of lines can be re-highlighted without having to process the entire document again. This is particularly useful for refreshing the highlighting of keywords and tags after the index has been rebuilt. PR #460. * On a few occasions, the entire document in the editor would be reloaded in order to update the layout and formatting. This is not only slow for big documents, it also resets the undo stack. Instead, the entire document is "marked as dirty" to force the Qt library to update the layout, which is much faster. PR #460. -* For very large documents (in the megabyte range), the repositioning of the cursor when the document was opened would sometimes interfere with the rendering of the document. This could potentially cause the editor to hang for up to a couple of minutes. Instead, the repositioning of the cursor is now postponed until the document layout size has reached a point past the character where the cursor is to be moved. This mode is only used for documents larger than 50 kilobytes. PR #460. +* For very large documents (in the megabyte range), the repositioning of the cursor when the document was opened would sometimes interfere with the rendering of the document itself. This could potentially cause the editor to hang for up to a couple of minutes. Instead, the repositioning of the cursor is now postponed until the document layout size has reached past the character where the cursor is to be moved. This mode is only used for documents larger than 50 kilobytes. PR #460. * The document editor will no longer accept single documents larger than 5 megabytes. This restriction has also been applied to the Build Novel Project tool. For reference, a typical long novel is less than 1 megabyte in size. PR #460. **Other Changes** * The command line switches `--quiet` and `--logfile=` have been removed. They were intended for testing, but have never been used. The default mode of only printing warnings and errors is quiet enough, and logging to file shouldn't be necessary for a GUI application. PR #453. -* A number of if statements and conditions in the code that were intended to alter behaviour when running tests, mostly to stop modal dialogs from blocking the main thread, have been removed. The changes to the program flow when running tests have now been reduced to a minimum, and modifications instead handled with pytest monkeypatches. PR #453. -* The `QtSvg` package is no longer in use by novelWriter. The internal dependency check has been dropped. PR #457. +* A number of if-statements and conditions in the code that were intended to alter behaviour when running tests, mostly to stop modal dialogs from blocking the main thread, have been removed. These types of changes to the program flow when running tests have now been reduced to a minimum, and modifications instead handled with pytest monkeypatches. PR #453. +* The `QtSvg` package is no longer in use by novelWriter. The internal dependency check has been removed. PR #457. * It is no longer possible to set the user's home folder as the root directory of a project. The home folder is the default lookup folder in many cases, so it's easy to do by mistake. PR #457. * The background word counter has been rewritten to run on an application wide thread pool. This is a more appropriate way of running background tasks. PR #462. **Test Suite** -* Major additions to the test suite taking the test coverage to 91%. PR #453. +* Major additions to the test suite, taking the test coverage to 91%. PR #453. * Test coverage for Linux (Ubuntu) for Python versions 3.6, 3.7, and 3.8 are now separate jobs. In addition, Windows with Python 3.8 and macOS with Python 3.8 is also tested. All OSes are piped into test coverage, and they all have status badges. PRs #453 and #454. diff --git a/docs/source/conf.py b/docs/source/conf.py index b72b8db3..7aef4aa3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -28,7 +28,7 @@ author = "Veronica Berglyd Olsen" # The short X.Y version version = "1.0" # The full version, including alpha/beta/rc tags -release = "1.0-beta4" +release = "1.0-rc1" # -- General configuration --------------------------------------------------- diff --git a/nw/__init__.py b/nw/__init__.py index 72327b2b..38c9a263 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -39,9 +39,9 @@ __package__ = "nw" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen" __license__ = "GPLv3" -__version__ = "1.0b4" -__hexversion__ = "0x010000b4" -__date__ = "2020-10-11" +__version__ = "1.0rc1" +__hexversion__ = "0x010000c1" +__date__ = "2020-10-25" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" __status__ = "Beta" From 5f8e4b53160181b54cd5490bfcb93c67b9e83d72 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 15 Oct 2020 14:39:23 +0200 Subject: [PATCH 053/104] Updated changelog --- CHANGELOG.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f46f8610..b0293f81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,27 @@ # novelWriter ChangeLog -## Version 1.0 Release Candidate 4 [2020-10-25] +## Version 1.0 Release Candidate 1 [2020-10-25] + +**Important Notes** + +* The minimal supported Python version is now 3.6. While novelWriter has worked fine in the post with versions as low as 3.4, neither 3.4 nor 3.5 is tested. They have also both reached end of life. There are a couple of good reasons to drop support for older versions. PR #470. + 1. Python 3.6 introduces ordered dictionaries as the standard. + 2. The format string attribute was added in 3.6, and is much less clunky in many parts of the code than the full `"".format()` syntax. + 3. Especially 3.4 has limited support for `*var` expansion of iterables. These are used several places in the code. + +**Bugfixes** + +* Fixed a bug in the Build Novel Project tool where novelWriter would crash when trying to build the preview when running a version of the Qt library lower than 5.14. Issue #471, PR #472. + +**User Interface** + +* An option has been added in Preferences to hide horizontal or vertical scroll bars on the main GUI. These optons will hide scroll bars on the Project Tree, Document Editor, Document Viewer, Outline Tab and on the controls of the Build Novel Project tool. Scroll bars take up space, and as long as the project doesn't contain very long documents, scrolling with the mouse wheel is enough. The feature is of course entirely optional. PRs #468 and #469. +* It is no possible to enable scrolling past the end of the document with a new option in Preferences. Previously, the editor would just allow scrolling to the bottom of the document. The new option adds a margin to the bottom of the document itself that allows for scrolling past this point. This avoids having to type text at the bottom of the editor window. PRs #468 and #469. +* A new feature called "Typewriter Scrolling" has been added. It basically means that the editor window will try to keep the cursor at a given vertical position and instead scroll the document when the cursor moves to a new line, either by arrow keys or while typing. The position can also be defined in Preferences. The scroll bar uses an animation effect to perform the scrolling to avoid abrupt jumps in the editor window. PRs #468 and #474. +* The line counter in the Document Editor footer now shows the location in the document in terms of percentage. This is convenient for very large documents. PR #474. +* A "Follow Tag" option has been added to the Document Editor context menu. This option appears when right-clicking a tag value on a meta data line. PR #474. +* When applying a format from the format menu to a selection of multiple paragraphs (or lines), only the first paragraph (or line) receives the formatting. The editor doesn't allow markdown formatting to span multiple lines. Issue #451, PR #475. +* The syntax highlighter no longer uses the same colour to highlight strikethrough text as for emphasised text. The colour is intended to stand out, which makes little sense for such text. Instead, the highlighter uses the same colour as for comments. PR #476. ## Version 1.0 Beta 4 [2020-10-11] From bd9463fe5856b111cf01225b6bbd84427ba4bd52 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 15 Oct 2020 17:36:28 +0200 Subject: [PATCH 054/104] Added version scheme comment to main package init file --- nw/__init__.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/nw/__init__.py b/nw/__init__.py index 72327b2b..6924f94d 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -35,6 +35,28 @@ from PyQt5.QtWidgets import QApplication, QErrorMessage from nw.error import exceptionHandler from nw.config import Config +# +# Version Scheme +# ================ +# Generally follows PEP 440 +# Hex Version: +# - Digit 1,2 : Major Version (01, 02, 03) +# = Digit 3,4 : Minor Version (01, 09, 10, 99) +# - Digit 5,6 : Patch Version (01, 09, 10, 99) +# = Digit 7 : Release Type (a: aplha, b: beta, c: candidate, f: final) +# - Digit 8 : Release Number (0-9) +# +# Example : Full Short Description +# ------------------------------------------------------------------------- +# 0x010200a0 : 1.2-alpha0 1.2a0 Can be used for the dev branch +# 0x010200a1 : 1.2-alpha1 1.2a1 First alpha release +# 0x010200b1 : 1.2-beta1 1.2b1 First beta release +# 0x010200c1 : 1.2-rc1 1.2rc1 First release candidate +# 0x010200f0 : 1.2 1.2 Final release +# 0x010200f1 : 1.2-post1 1.2.post1 Post release, but not a code patch! +# +# 0x010201f0 : 1.2.1 1.2.1 Patch release + __package__ = "nw" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen" From 8e9fb586771819e9a1cd3795e409966e8d5fa357 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 15 Oct 2020 17:43:58 +0200 Subject: [PATCH 055/104] Swapped lines in source code --- nw/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nw/__init__.py b/nw/__init__.py index 6924f94d..3f78d540 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -54,8 +54,8 @@ from nw.config import Config # 0x010200c1 : 1.2-rc1 1.2rc1 First release candidate # 0x010200f0 : 1.2 1.2 Final release # 0x010200f1 : 1.2-post1 1.2.post1 Post release, but not a code patch! -# # 0x010201f0 : 1.2.1 1.2.1 Patch release +# __package__ = "nw" __author__ = "Veronica Berglyd Olsen" From d973f28755c2a69893127ab8d83d659cf25f12a3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 15 Oct 2020 18:39:15 +0200 Subject: [PATCH 056/104] Code and comment cleanup in main GUI --- nw/core/document.py | 4 +-- nw/core/project.py | 4 +-- nw/guimain.py | 67 ++++++++++++++++++++++++++++----------------- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/nw/core/document.py b/nw/core/document.py index 1261c12d..00fd7146 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -120,7 +120,7 @@ class NWDoc(): logger.verbose("DocMeta: '%s'" % self._docMeta) if showStatus and not isOrphan: - self.theParent.statusBar.setStatus("Opened Document: %s" % self._theItem.itemName) + self.theParent.setStatus("Opened Document: %s" % self._theItem.itemName) return theText @@ -166,7 +166,7 @@ class NWDoc(): os.unlink(docPath) os.rename(docTemp, docPath) - self.theParent.statusBar.setStatus("Saved Document: %s" % self._theItem.itemName) + self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName) return True diff --git a/nw/core/project.py b/nw/core/project.py index 46e37202..15fd7d27 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -782,7 +782,7 @@ class NWProject(): """Create a zip file of the entire project. """ logger.info("Backing up project") - self.theParent.statusBar.setStatus("Backing up project ...") + self.theParent.setStatus("Backing up project ...") if self.mainConf.backupPath is None or self.mainConf.backupPath == "": self.theParent.makeAlert(( @@ -847,7 +847,7 @@ class NWProject(): ) return False - self.theParent.statusBar.setStatus("Project backed up to '%s.zip'" % baseName) + self.theParent.setStatus("Project backed up to '%s.zip'" % baseName) return True diff --git a/nw/guimain.py b/nw/guimain.py index 18801727..6742a0d9 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -62,7 +62,9 @@ class GuiMain(QMainWindow): self.mainConf = nw.CONFIG self.threadPool = QThreadPool() - # Some runtime info useful for debugging + # System Info + # =========== + logger.info("OS: %s" % self.mainConf.osType) logger.info("Kernel: %s" % self.mainConf.kernelVer) logger.info("Host: %s" % self.mainConf.hostName) @@ -76,6 +78,9 @@ class GuiMain(QMainWindow): self.mainConf.verPyString, self.mainConf.verPyHexVal) ) + # Core Classes + # ============ + # Core Classes and settings self.theTheme = GuiTheme(self) self.theProject = NWProject(self) @@ -89,7 +94,7 @@ class GuiMain(QMainWindow): self.setWindowIcon(QIcon(self.mainConf.appIcon)) # Build the GUI - ################ + # ============= # Main GUI Elements self.statusBar = GuiMainStatus(self) @@ -106,7 +111,7 @@ class GuiMain(QMainWindow): self.statusIcons = [] self.importIcons = [] - # Assemble Main Window + # Project Tree View self.treePane = QWidget() self.treeBox = QVBoxLayout() self.treeBox.setContentsMargins(0, 0, 0, 0) @@ -114,20 +119,24 @@ class GuiMain(QMainWindow): self.treeBox.addWidget(self.treeMeta) self.treePane.setLayout(self.treeBox) + # Splitter : Document Viewer / Document Meta self.splitView = QSplitter(Qt.Vertical) self.splitView.addWidget(self.docViewer) self.splitView.addWidget(self.viewMeta) self.splitView.setSizes(self.mainConf.getViewPanePos()) + # Splitter : Document Editor / Document Viewer self.splitDocs = QSplitter(Qt.Horizontal) self.splitDocs.addWidget(self.docEditor) self.splitDocs.addWidget(self.splitView) + # Splitter : Project Outlie / Outline Details self.splitOutline = QSplitter(Qt.Vertical) self.splitOutline.addWidget(self.projView) self.splitOutline.addWidget(self.projMeta) self.splitOutline.setSizes(self.mainConf.getOutlinePanePos()) + # Main Tabs : Edirot / Outline self.tabWidget = QTabWidget() self.tabWidget.setTabPosition(QTabWidget.East) self.tabWidget.setStyleSheet("QTabWidget::pane {border: 0;}") @@ -135,6 +144,7 @@ class GuiMain(QMainWindow): self.tabWidget.addTab(self.splitOutline, "Outline") self.tabWidget.currentChanged.connect(self._mainTabChanged) + # Splitter : Project Tree / Main Tabs xCM = self.mainConf.pxInt(4) self.splitMain = QSplitter(Qt.Horizontal) self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM) @@ -142,6 +152,7 @@ class GuiMain(QMainWindow): self.splitMain.addWidget(self.tabWidget) self.splitMain.setSizes(self.mainConf.getMainPanePos()) + # Indices of All Splitter Widgets self.idxTree = self.splitMain.indexOf(self.treePane) self.idxMain = self.splitMain.indexOf(self.tabWidget) self.idxEditor = self.splitDocs.indexOf(self.docEditor) @@ -151,6 +162,7 @@ class GuiMain(QMainWindow): self.idxTabEdit = self.tabWidget.indexOf(self.splitDocs) self.idxTabProj = self.tabWidget.indexOf(self.splitOutline) + # Splitter Behaviour self.splitMain.setCollapsible(self.idxTree, False) self.splitMain.setCollapsible(self.idxMain, False) self.splitDocs.setCollapsible(self.idxEditor, False) @@ -158,10 +170,11 @@ class GuiMain(QMainWindow): self.splitView.setCollapsible(self.idxViewDoc, False) self.splitView.setCollapsible(self.idxViewMeta, False) + # Editor / Viewer Default State self.splitView.setVisible(False) self.docEditor.closeSearch() - # Build the Tree View + # Initialise the Project Tree self.treeView.itemSelectionChanged.connect(self._treeSingleClick) self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.rebuildTree() @@ -172,13 +185,13 @@ class GuiMain(QMainWindow): self.setStatusBar(self.statusBar) # Finalise Initialisation - ########################## + # ======================= - # Set Up Autosaving Project Timer + # Set Up Auto-Save Project Timer self.asProjTimer = QTimer() self.asProjTimer.timeout.connect(self._autoSaveProject) - # Set Up Autosaving Document Timer + # Set Up Auto-Save Document Timer self.asDocTimer = QTimer() self.asDocTimer.timeout.connect(self._autoSaveDocument) @@ -203,11 +216,13 @@ class GuiMain(QMainWindow): # Check that config loaded fine self.reportConfErr() + # Initialise Main GUI self.initMain() self.asProjTimer.start() self.asDocTimer.start() self.statusBar.clearStatus() + # Handle Windows Mode self.showNormal() if self.mainConf.isFullScreen: self.toggleFullScreenMode() @@ -224,7 +239,7 @@ class GuiMain(QMainWindow): self.showProjectLoadDialog() logger.debug("novelWriter is ready ...") - self.statusBar.setStatus("novelWriter is ready ...") + self.setStatus("novelWriter is ready ...") return @@ -249,8 +264,7 @@ class GuiMain(QMainWindow): ## def newProject(self, projData=None): - """Create new project with a few default files and folders. - The variable forceNew is used for testing. + """Create new project via the new project wizard. """ if self.hasProject: self.makeAlert( @@ -293,7 +307,7 @@ class GuiMain(QMainWindow): def closeProject(self, isYes=False): """Closes the project if one is open. isYes is passed on from the close application event so the user doesn't get prompted - twice. + twice to confirm. """ if not self.hasProject: # There is no project loaded, everything OK @@ -302,7 +316,7 @@ class GuiMain(QMainWindow): if not isYes: msgBox = QMessageBox() msgRes = msgBox.question( - self, "Close Project", "Save changes and close current project?" + self, "Close Project", "Save changes and close the current project?" ) if msgRes != QMessageBox.Yes: return False @@ -318,7 +332,7 @@ class GuiMain(QMainWindow): if self.mainConf.askBeforeBackup: msgBox = QMessageBox() msgRes = msgBox.question( - self, "Backup Project", "Backup current project?" + self, "Backup Project", "Backup the current project?" ) if msgRes != QMessageBox.Yes: doBackup = False @@ -433,6 +447,7 @@ class GuiMain(QMainWindow): if self.theProject.projPath is None: projPath = self.selectProjectPath() self.theProject.setProjectPath(projPath) + if self.theProject.projPath is None: return False @@ -454,6 +469,7 @@ class GuiMain(QMainWindow): if self.docEditor.docChanged: self.saveDocument() self.docEditor.clearEditor() + return True def openDocument(self, tHandle, tLine=None, changeFocus=True, doScroll=False): @@ -469,6 +485,7 @@ class GuiMain(QMainWindow): self.treeView.setSelectedHandle(tHandle, doScroll=doScroll) else: return False + return True def openNextDocument(self, tHandle, wrapAround=False): @@ -546,6 +563,7 @@ class GuiMain(QMainWindow): vPos[1] = bPos[1] - vPos[0] self.splitDocs.setSizes(vPos) self.viewMeta.setVisible(self.mainConf.showRefPanel) + self.docViewer.navigateTo(tAnchor) return True @@ -697,9 +715,9 @@ class GuiMain(QMainWindow): for nDone, tItem in enumerate(self.theProject.projTree): if tItem is not None: - self.statusBar.setStatus("Indexing: '%s'" % tItem.itemName) + self.setStatus("Indexing: '%s'" % tItem.itemName) else: - self.statusBar.setStatus("Indexing: Unknown item") + self.setStatus("Indexing: Unknown item") if tItem is not None and tItem.itemType == nwItemType.FILE: logger.verbose("Scanning: %s" % tItem.itemName) @@ -717,7 +735,7 @@ class GuiMain(QMainWindow): self.treeView.projectWordCount() tEnd = time() - self.statusBar.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0)) + self.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0)) self.docEditor.updateTagHighLighting() qApp.restoreOverrideCursor() @@ -754,7 +772,8 @@ class GuiMain(QMainWindow): def showProjectLoadDialog(self): """Opens the projects dialog for selecting either existing projects from a cache of recently opened projects, or provide a - browse button for projects not yet cached. + browse button for projects not yet cached. Selecting to create a + new project is forwarded to the new project wizard. """ dlgProj = GuiProjectLoad(self) dlgProj.exec_() @@ -767,7 +786,7 @@ class GuiMain(QMainWindow): return True def showNewProjectDialog(self): - """Open the wizard and assemble the project options dict. + """Open the wizard and assemble a project options dict. """ newProj = GuiProjectWizard(self) newProj.exec_() @@ -865,8 +884,7 @@ class GuiMain(QMainWindow): def makeAlert(self, theMessage, theLevel=nwAlert.INFO): """Alert both the user and the logger at the same time. Message - can be either a string or an array of strings. Severity level is - 0 = info, 1 = warning, and 2 = error. + can be either a string or an array of strings. """ if isinstance(theMessage, list): popMsg = "
".join(theMessage) @@ -955,7 +973,7 @@ class GuiMain(QMainWindow): return True def setFocus(self, paneNo): - """Switch focus to one of the three main gUi panes. + """Switch focus to one of the three main GUI panes. """ if paneNo == 1: self.treeView.setFocus() @@ -1236,9 +1254,9 @@ class GuiMain(QMainWindow): return def _treeKeyPressReturn(self): - """The user pressed return an item in the tree. If it is a file, - we open it. Otherwise, we do nothing. Pressing return does not - change focus to the editor as double click does. + """The user pressed return on an item in the tree. If it is a + file, we open it. Otherwise, we do nothing. Pressing return does + not change focus to the editor as double click does. """ tHandle = self.treeView.getSelectedHandle() logger.verbose("User pressed return on tree item with handle %s" % tHandle) @@ -1257,7 +1275,6 @@ class GuiMain(QMainWindow): """ if self.docEditor.docSearch.isVisible(): self.docEditor.closeSearch() - return elif self.isFocusMode: self.toggleFocusMode() return From 1923a55443cc5e861718912c036777e8fafd4099 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 15 Oct 2020 19:08:35 +0200 Subject: [PATCH 057/104] Code cleanup in core classes and rename of parHandle to itemParent in NWItem class --- nw/core/document.py | 11 +++++++---- nw/core/index.py | 6 +++--- nw/core/item.py | 19 +++++++++++-------- nw/core/project.py | 6 +++--- nw/core/tree.py | 10 +++++----- nw/gui/build.py | 4 ++-- nw/gui/docmerge.py | 2 +- nw/gui/docsplit.py | 4 ++-- nw/gui/projtree.py | 12 ++++++------ tests/test_item.py | 10 +++++----- tests/test_project.py | 4 ++-- 11 files changed, 47 insertions(+), 41 deletions(-) diff --git a/nw/core/document.py b/nw/core/document.py index 00fd7146..1452e9dd 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -67,7 +67,9 @@ class NWDoc(): def openDocument(self, tHandle, showStatus=True, isOrphan=False): """Open a document from handle, capturing potential file system - errors and parse meta data. + errors and parse meta data. If the document doesn't exist on + disk, return an empty string. If something went wrong, return + None. """ if not isHandle(tHandle): return None @@ -125,8 +127,8 @@ class NWDoc(): return theText def saveDocument(self, docText): - """Save the document via temp file in case of save failure, and - in any case keep a backup of the file. + """Save the document. The file is saved via a temp file in case + of save failure. Returns True if successful, False if not. """ if self._docHandle is None: return False @@ -139,6 +141,7 @@ class NWDoc(): docPath = os.path.join(self.theProject.projContent, docFile) docTemp = os.path.join(self.theProject.projContent, docFile+"~") + # DocMeta line if self._theItem is None: docMeta = "" else: @@ -171,7 +174,7 @@ class NWDoc(): return True def deleteDocument(self, tHandle): - """Permanently delete a document source file and its backups + """Permanently delete a document source file and related files from the project data folder. """ if not isHandle(tHandle): diff --git a/nw/core/index.py b/nw/core/index.py index f715d835..c08562e1 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -279,7 +279,7 @@ class NWIndex(): if theItem.itemLayout == nwItemLayout.NO_LAYOUT: logger.info("Not indexing no-layout item %s" % tHandle) return False - if theItem.parHandle is None: + if theItem.itemParent is None: logger.info("Not indexing orphaned item %s" % tHandle) return False @@ -288,7 +288,7 @@ class NWIndex(): self.textCounts[tHandle] = [cC, wC, pC] # If the file is archived or trashed, we don't index the file itself - if self.theProject.projTree.isTrashRoot(theItem.parHandle): + if self.theProject.projTree.isTrashRoot(theItem.itemParent): logger.info("Not indexing trash item %s" % tHandle) return False if theRoot.itemClass == nwItemClass.ARCHIVE: @@ -583,7 +583,7 @@ class NWIndex(): def getCounts(self, tHandle, sTitle=None): """Returns the counts for a file, or a section of a file - starting at title nTitle. + starting at title sTitle if it is provided. """ cC = 0 wC = 0 diff --git a/nw/core/item.py b/nw/core/item.py index 702287dd..944b5f0c 100644 --- a/nw/core/item.py +++ b/nw/core/item.py @@ -42,7 +42,7 @@ class NWItem(): self.itemName = "" self.itemHandle = None - self.parHandle = None + self.itemParent = None self.itemOrder = None self.itemType = nwItemType.NO_TYPE self.itemClass = nwItemClass.NO_CLASS @@ -70,7 +70,7 @@ class NWItem(): xPack = etree.SubElement(xParent, "item", attrib={ "handle" : str(self.itemHandle), "order" : str(self.itemOrder), - "parent" : str(self.parHandle), + "parent" : str(self.itemParent), }) self._subPack(xPack, "name", text=str(self.itemName)) self._subPack(xPack, "type", text=str(self.itemType.name)) @@ -85,6 +85,7 @@ class NWItem(): self._subPack(xPack, "cursorPos", text=str(self.cursorPos), none=False) else: self._subPack(xPack, "expanded", text=str(self.isExpanded)) + return def unpackXML(self, xItem): @@ -101,7 +102,7 @@ class NWItem(): return False if "parent" in xItem.attrib: - self.parHandle = xItem.attrib["parent"] + self.itemParent = xItem.attrib["parent"] setMap = { "name" : self.setName, @@ -131,9 +132,11 @@ class NWItem(): """ if not none and (text is None or text == "None"): return None - xSub = etree.SubElement(xParent, name, attrib=attrib) + xAttr = {} if attrib is None else attrib + xSub = etree.SubElement(xParent, name, attrib=xAttr) if text is not None: xSub.text = text + return ## @@ -162,14 +165,14 @@ class NWItem(): """Set the parent handle, and ensure that it is valid. """ if theParent is None: - self.parHandle = None + self.itemParent = None elif isinstance(theParent, str): if len(theParent) == 13: - self.parHandle = theParent + self.itemParent = theParent else: - self.parHandle = None + self.itemParent = None else: - self.parHandle = None + self.itemParent = None return def setOrder(self, theOrder): diff --git a/nw/core/project.py b/nw/core/project.py index 15fd7d27..622bcc38 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1139,16 +1139,16 @@ class NWProject(): # Technically a bug since treeOrder is built from the # same data as projTree continue - elif tItem.parHandle is None: + elif tItem.itemParent is None: # Item is a root, or already been identified as an # orphaned item sentItems.append(tHandle) yield tItem - elif tItem.parHandle in sentItems: + elif tItem.itemParent in sentItems: # Item's parent has been sent, so all is fine sentItems.append(tHandle) yield tItem - elif tItem.parHandle in iterItems: + elif tItem.itemParent in iterItems: # Item's parent exists, but hasn't been sent yet, so add # it again to the end logger.warning("Item %s found before its parent" % tHandle) diff --git a/nw/core/tree.py b/nw/core/tree.py index c51ae16c..b43230a9 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -134,7 +134,7 @@ class NWTree(): for xItem in xContent: nwItem = NWItem(self.theProject) if nwItem.unpackXML(xItem): - self.append(nwItem.itemHandle, nwItem.parHandle, nwItem) + self.append(nwItem.itemHandle, nwItem.itemParent, nwItem) nwItem.saveInitialCount() return True @@ -261,10 +261,10 @@ class NWTree(): tItem = self.__getitem__(tHandle) if tItem is not None: for i in range(nwConst.maxDepth + 1): - if tItem.parHandle is None: + if tItem.itemParent is None: return tItem else: - tHandle = tItem.parHandle + tHandle = tItem.itemParent tItem = self.__getitem__(tHandle) return None @@ -279,10 +279,10 @@ class NWTree(): if tItem is not None: tTree.append(tHandle) for i in range(nwConst.maxDepth + 1): - if tItem.parHandle is None: + if tItem.itemParent is None: return tTree else: - tHandle = tItem.parHandle + tHandle = tItem.itemParent tItem = self.__getitem__(tHandle) if tItem is None: return tTree diff --git a/nw/gui/build.py b/nw/gui/build.py index 1a69b62e..45a1a073 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -684,8 +684,8 @@ class GuiBuildNovel(QDialog): isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT isNone |= theItem.itemClass == nwItemClass.NO_CLASS isNone |= theItem.itemClass == nwItemClass.TRASH - isNone |= theItem.parHandle == self.theProject.projTree.trashRoot() - isNone |= theItem.parHandle is None + isNone |= theItem.itemParent == self.theProject.projTree.trashRoot() + isNone |= theItem.itemParent is None isNote = theItem.itemLayout == nwItemLayout.NOTE isNovel = not isNone and not isNote diff --git a/nw/gui/docmerge.py b/nw/gui/docmerge.py index 2418a161..d7d128fe 100644 --- a/nw/gui/docmerge.py +++ b/nw/gui/docmerge.py @@ -127,7 +127,7 @@ class GuiDocMerge(QDialog): ), nwAlert.ERROR) return - nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.parHandle) + nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent) newItem = self.theProject.projTree[nHandle] newItem.setStatus(srcItem.itemStatus) diff --git a/nw/gui/docsplit.py b/nw/gui/docsplit.py index 3f497c18..1a5e9491 100644 --- a/nw/gui/docsplit.py +++ b/nw/gui/docsplit.py @@ -154,7 +154,7 @@ class GuiDocSplit(QDialog): return # Check that another folder can be created - parTree = self.theProject.projTree.getItemPath(srcItem.parHandle) + parTree = self.theProject.projTree.getItemPath(srcItem.itemParent) if len(parTree) >= nwConst.maxDepth - 1: self.theParent.makeAlert(( "Cannot add new folder for the document split. " @@ -176,7 +176,7 @@ class GuiDocSplit(QDialog): # Create the folder fHandle = self.theProject.newFolder( - srcItem.itemName, srcItem.itemClass, srcItem.parHandle + srcItem.itemName, srcItem.itemClass, srcItem.itemParent ) self.theParent.treeView.revealNewTreeItem(fHandle) logger.verbose("Creating folder %s" % fHandle) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 9f4bb5e6..6b1ee7bd 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -219,7 +219,7 @@ class GuiProjectTree(QTreeWidget): pItem = self.theProject.projTree[pHandle] if pItem.itemType == nwItemType.FILE: nHandle = pHandle - pHandle = pItem.parHandle + pHandle = pItem.itemParent # If we again have no home, give up if pHandle is None: @@ -270,7 +270,7 @@ class GuiProjectTree(QTreeWidget): """ nwItem = self.theProject.projTree[tHandle] trItem = self._addTreeItem(nwItem, nHandle) - pHandle = nwItem.parHandle + pHandle = nwItem.itemParent if pHandle is not None and pHandle in self.theMap: self.theMap[pHandle].setExpanded(True) self.clearSelection() @@ -430,7 +430,7 @@ class GuiProjectTree(QTreeWidget): logger.error("Could not delete item") return False - pHandle = nwItemS.parHandle + pHandle = nwItemS.itemParent if self.theProject.projTree.isTrashRoot(pHandle): # If the file is in the trash folder already, as the # user if they want to permanently delete the file. @@ -815,7 +815,7 @@ class GuiProjectTree(QTreeWidget): project tree. """ tHandle = nwItem.itemHandle - pHandle = nwItem.parHandle + pHandle = nwItem.itemParent tClass = nwItem.itemClass newItem = QTreeWidgetItem([""]*4) @@ -1022,11 +1022,11 @@ class GuiProjectTreeMenu(QMenu): trashHandle = self.theTree.theProject.projTree.trashRoot() - inTrash = theItem.parHandle == trashHandle and trashHandle is not None + inTrash = theItem.itemParent == trashHandle and trashHandle is not None isTrash = theItem.itemHandle == trashHandle and trashHandle is not None isFile = theItem.itemType == nwItemType.FILE isArch = theRoot.itemClass == nwItemClass.ARCHIVE - isOrph = isFile and theItem.parHandle is None + isOrph = isFile and theItem.itemParent is None showOpen = isFile showView = isFile diff --git a/tests/test_item.py b/tests/test_item.py index d1097161..936ba185 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -31,13 +31,13 @@ def testItemSettersSimple(nwDummy): # Parent theItem.setParent(None) - assert theItem.parHandle is None + assert theItem.itemParent is None theItem.setParent(123) - assert theItem.parHandle is None + assert theItem.itemParent is None theItem.setParent("0123456789abcdef") - assert theItem.parHandle is None + assert theItem.itemParent is None theItem.setParent("0123456789abc") - assert theItem.parHandle == "0123456789abc" + assert theItem.itemParent == "0123456789abc" # Order theItem.setOrder(None) @@ -227,7 +227,7 @@ def testItemXMLPackUnpack(nwDummy): # Unpack assert theItem.unpackXML(xContent[0]) assert theItem.itemHandle == "0123456789abc" - assert theItem.parHandle == "0123456789abc" + assert theItem.itemParent == "0123456789abc" assert theItem.itemOrder == 1 assert theItem.isExpanded assert theItem.paraCount == 3 diff --git a/tests/test_project.py b/tests/test_project.py index 282acbc7..e8f1442b 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -521,7 +521,7 @@ def testProjectOrphanedFiles(nwDummy, nwLipsum): assert oItem is not None assert oItem.itemName == "Mars" assert oItem.itemHandle == "636b6aa9b697b" - assert oItem.parHandle is None + assert oItem.itemParent is None assert oItem.itemClass == nwItemClass.WORLD assert oItem.itemType == nwItemType.FILE assert oItem.itemLayout == nwItemLayout.NOTE @@ -531,7 +531,7 @@ def testProjectOrphanedFiles(nwDummy, nwLipsum): assert oItem is not None assert oItem.itemName == "Orphaned File 1" assert oItem.itemHandle == "736b6aa9b697b" - assert oItem.parHandle is None + assert oItem.itemParent is None assert oItem.itemClass == nwItemClass.NO_CLASS assert oItem.itemType == nwItemType.FILE assert oItem.itemLayout == nwItemLayout.NO_LAYOUT From bf7429f028419951ae33323f7aeb86dfe313ff7f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 15 Oct 2020 20:33:10 +0200 Subject: [PATCH 058/104] Update format statements and improve status bar clock --- .github/workflows/syntax.yml | 4 ++-- nw/gui/about.py | 1 - nw/gui/doceditor.py | 19 +++++++++---------- nw/gui/dochighlight.py | 10 +++++++--- nw/gui/outline.py | 10 +++++++--- nw/gui/outlinedetails.py | 10 +++++++--- nw/gui/projsettings.py | 7 ++++--- nw/gui/statusbar.py | 21 +++++++++------------ setup.cfg | 2 +- 9 files changed, 46 insertions(+), 38 deletions(-) diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml index 038480f7..bf8ffec3 100644 --- a/.github/workflows/syntax.yml +++ b/.github/workflows/syntax.yml @@ -25,5 +25,5 @@ jobs: flake8 tests --count --select=E9,F63,F7,F82 --show-source --statistics - name: Coding Style Violations run: | - flake8 nw --count --max-line-length=99 --ignore E203,E221,E226,E241,E251,E261,E266,E302,E305 --show-source --statistics - flake8 tests --count --max-line-length=99 --ignore E203,E221,E226,E241,E251,E261,E266,E302,E305 --show-source --statistics + flake8 nw --count --max-line-length=99 --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --show-source --statistics + flake8 tests --count --max-line-length=99 --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --show-source --statistics diff --git a/nw/gui/about.py b/nw/gui/about.py index c6957bae..248cf7d1 100644 --- a/nw/gui/about.py +++ b/nw/gui/about.py @@ -222,7 +222,6 @@ class GuiAbout(QDialog): hColB = self.theParent.theTheme.colHead[2], ) self.pageAbout.document().setDefaultStyleSheet(styleSheet) - # self.pageCredit.document().setDefaultStyleSheet(styleSheet) self.pageLicense.document().setDefaultStyleSheet(styleSheet) return diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index d5e3b045..6d431319 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -807,16 +807,16 @@ class GuiDocEditor(QTextEdit): return self.docSearch.cycleFocus(toNext) return True - def mouseReleaseEvent(self, mEvent): + def mouseReleaseEvent(self, theEvent): """If the mouse button is released and the control key is pressed, check if we're clicking on a tag, and trigger the follow tag function. """ if qApp.keyboardModifiers() == Qt.ControlModifier: - theCursor = self.cursorForPosition(mEvent.pos()) + theCursor = self.cursorForPosition(theEvent.pos()) self._followTag(theCursor) - QTextEdit.mouseReleaseEvent(self, mEvent) + QTextEdit.mouseReleaseEvent(self, theEvent) self.docFooter.updateLineCount() return @@ -1203,20 +1203,19 @@ class GuiDocEditor(QTextEdit): """Check if document size crosses the big document limit set in config. If so, we will set the big document flag to True. """ - newState = theSize > self.mainConf.bigDocLimit*1000 + bigLim = self.mainConf.bigDocLimit*1000 + newState = theSize > bigLim if newState != self.bigDoc: if newState: logger.info( - "The document size is {:n} > {:n}, big doc mode has been enabled".format( - theSize, self.mainConf.bigDocLimit*1000 - ) + f"The document size is {theSize:n} > {bigLim:n}, " + f"big doc mode has been enabled" ) else: logger.info( - "The document size is {:n} <= {:n}, big doc mode has been disabled".format( - theSize, self.mainConf.bigDocLimit*1000 - ) + f"The document size is {theSize:n} <= {bigLim:n}, " + f"big doc mode has been disabled" ) self.bigDoc = newState diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index ba39b6e6..cc11ef86 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -149,18 +149,22 @@ class GuiDocHighlighter(QSyntaxHighlighter): # Quoted Strings if self.mainConf.highlightQuotes: + fmtDO = self.mainConf.fmtDoubleQuotes[0] + fmtDC = self.mainConf.fmtDoubleQuotes[1] + fmtSO = self.mainConf.fmtSingleQuotes[0] + fmtSC = self.mainConf.fmtSingleQuotes[1] self.hRules.append(( - "\\B{:s}(.*?){:s}\\B".format('"', '"'), { + "\\B\"(.*?)\"\\B", { 0 : self.hStyles["dialogue1"], } )) self.hRules.append(( - "\\B{:s}(.*?){:s}\\B".format(*self.mainConf.fmtDoubleQuotes), { + f"\\B{fmtDO:s}(.*?){fmtDC:s}\\B", { 0 : self.hStyles["dialogue2"], } )) self.hRules.append(( - "\\B{:s}(.*?){:s}\\B".format(*self.mainConf.fmtSingleQuotes), { + f"\\B{fmtSO:s}(.*?){fmtSC:s}\\B", { 0 : self.hStyles["dialogue3"], } )) diff --git a/nw/gui/outline.py b/nw/gui/outline.py index d3dc4815..4238cc44 100644 --- a/nw/gui/outline.py +++ b/nw/gui/outline.py @@ -439,6 +439,10 @@ class GuiOutline(QTreeWidget): newItem = QTreeWidgetItem() hIcon = "doc_%s" % tLevel.lower() + cC = int(novIdx["cCount"]) + wC = int(novIdx["wCount"]) + pC = int(novIdx["pCount"]) + newItem.setText(self.colIndex[nwOutline.TITLE], novIdx["title"]) newItem.setData(self.colIndex[nwOutline.TITLE], Qt.UserRole, tHandle) newItem.setIcon(self.colIndex[nwOutline.TITLE], self.theTheme.getIcon(hIcon)) @@ -448,9 +452,9 @@ class GuiOutline(QTreeWidget): newItem.setText(self.colIndex[nwOutline.LINE], sTitle[1:].lstrip("0")) newItem.setData(self.colIndex[nwOutline.LINE], Qt.UserRole, sTitle) newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"]) - newItem.setText(self.colIndex[nwOutline.CCOUNT], "{:n}".format(novIdx["cCount"])) - newItem.setText(self.colIndex[nwOutline.WCOUNT], "{:n}".format(novIdx["wCount"])) - newItem.setText(self.colIndex[nwOutline.PCOUNT], "{:n}".format(novIdx["pCount"])) + newItem.setText(self.colIndex[nwOutline.CCOUNT], f"{cC:n}") + newItem.setText(self.colIndex[nwOutline.WCOUNT], f"{wC:n}") + newItem.setText(self.colIndex[nwOutline.PCOUNT], f"{pC:n}") newItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight) newItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight) newItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight) diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py index 49998dd9..4f6a56f5 100644 --- a/nw/gui/outlinedetails.py +++ b/nw/gui/outlinedetails.py @@ -266,9 +266,13 @@ class GuiOutlineDetails(QScrollArea): self.fileValue.setText(nwItem.itemName) self.itemValue.setText(nwItem.itemStatus) - self.cCValue.setText("{:n}".format(checkInt(novIdx["cCount"], 0))) - self.wCValue.setText("{:n}".format(checkInt(novIdx["wCount"], 0))) - self.pCValue.setText("{:n}".format(checkInt(novIdx["pCount"], 0))) + cC = checkInt(novIdx["cCount"], 0) + wC = checkInt(novIdx["wCount"], 0) + pC = checkInt(novIdx["pCount"], 0) + + self.cCValue.setText(f"{cC:n}") + self.wCValue.setText(f"{wC:n}") + self.pCValue.setText(f"{pC:n}") self.synopValue.setText(novIdx["synopsis"]) diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index 30d9ad54..d51d8006 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -270,11 +270,12 @@ class GuiProjectEditMeta(QWidget): self.revLabel = QLabel("Revision count:") self.revLabel.setIndent(xInd) - self.revValue = QLabel("{:n}".format(self.theProject.saveCount)) + self.revValue = QLabel(f"{self.theProject.saveCount:n}") + editHours = self.theProject.editTime/3600 self.editLabel = QLabel("Edit time:") self.editLabel.setIndent(xInd) - self.editValue = QLabel("{:.2f} hours".format(self.theProject.editTime/3600)) + self.editValue = QLabel(f"{editHours:.2f} hours") self.statsLabel = QLabel("Project Stats") @@ -294,7 +295,7 @@ class GuiProjectEditMeta(QWidget): self.wordsLabel = QLabel("Word count:") self.wordsLabel.setIndent(xInd) - self.wordsValue = QLabel("{:n}".format(self.theProject.currWCount)) + self.wordsValue = QLabel(f"{self.theProject.currWCount:n}") self.mainForm.addWidget(self.headLabel, 0, 0, 1, 2, Qt.AlignTop) self.mainForm.addWidget(self.nameLabel, 1, 0, 1, 1, Qt.AlignTop) diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index c43c3f6f..91a86519 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -196,10 +196,7 @@ class GuiMainStatus(QStatusBar): "Project word count (session change)" ) self.statsText.setText(( - "Words: {pWC:n} ({sWC:+n})" - ).format( - pWC = self.projWords, - sWC = self.sessWords, + f"Words: {self.projWords:n} ({self.sessWords:+n})" )) return @@ -207,16 +204,16 @@ class GuiMainStatus(QStatusBar): """Update the session clock. """ if self.refTime is None: - theTime = "00:00:00" + self.timeText.setText("00:00:00") else: # This is much faster than using datetime format tS = int(time() - self.refTime) - tM = int(tS/60) - tH = int(tM/60) - tM = tM - tH*60 - tS = tS - tM*60 - tH*3600 - theTime = "%02d:%02d:%02d" % (tH, tM, tS) - self.timeText.setText(theTime) + tM = tS//60 + tH = tM//60 + tM %= 60 + tS %= 60 + self.timeText.setText(f"{tH:02d}:{tM:02d}:{tS:02d}") + return # END Class GuiMainStatus @@ -237,7 +234,7 @@ class StatusLED(QAbstractButton): return ## - # Getters and Setters + # Setters ## def setState(self, theState): diff --git a/setup.cfg b/setup.cfg index a791e31e..ff0117c5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,6 +6,6 @@ version = attr: nw.__version__ universal = 0 [flake8] -ignore = E203,E221,E226,E241,E251,E261,E266,E302,E305 +ignore = E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 max-line-length = 99 exclude = docs/* From e0d15a882ea241832f143c6e4a9876690b5980fe Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 15 Oct 2020 20:56:09 +0200 Subject: [PATCH 059/104] Further improvements to the status bar clock --- nw/gui/statusbar.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index 91a86519..da8a0427 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -206,14 +206,8 @@ class GuiMainStatus(QStatusBar): if self.refTime is None: self.timeText.setText("00:00:00") else: - # This is much faster than using datetime format tS = int(time() - self.refTime) - tM = tS//60 - tH = tM//60 - tM %= 60 - tS %= 60 - self.timeText.setText(f"{tH:02d}:{tM:02d}:{tS:02d}") - + self.timeText.setText(f"{tS//3600:02d}:{(tS//60)%60:02d}:{tS%60:02d}") return # END Class GuiMainStatus From 7deee064d08a04d618abecc28ace49c629d19fa6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 15 Oct 2020 22:44:47 +0200 Subject: [PATCH 060/104] More improvements, mostly string formatting --- nw/core/tohtml.py | 1 + nw/core/tools.py | 17 ++++++++--------- nw/gui/statusbar.py | 4 +++- tests/test_tools.py | 1 + 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 770505db..df6c4a09 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -41,6 +41,7 @@ class ToHtml(Tokenizer): def __init__(self, theProject, theParent): Tokenizer.__init__(self, theProject, theParent) + self.genMode = self.M_EXPORT self.cssStyles = True diff --git a/nw/core/tools.py b/nw/core/tools.py index 59c141d9..8062d591 100644 --- a/nw/core/tools.py +++ b/nw/core/tools.py @@ -136,7 +136,6 @@ def numberToWord(numVal, theLanguage): def _numberToWordEN(numVal): """Convert numbers to English words. """ - numWord = "" oneWord = "" tenWord = "" hunWord = "" @@ -145,8 +144,8 @@ def _numberToWordEN(numVal): return "Zero" oneVal = numVal % 10 - tenVal = (numVal-oneVal) % 100 - hunVal = (numVal-tenVal-oneVal) % 1000 + tenVal = (numVal - oneVal) % 100 + hunVal = (numVal - tenVal - oneVal) % 1000 theHundreds = { 100: "One Hundred", 200: "Two Hundred", 300: "Three Hundred", @@ -167,18 +166,18 @@ def _numberToWordEN(numVal): } hunWord = theHundreds.get(hunVal, "") - tenWord = theTens.get(tenVal, "") if tenVal == 10: oneWord = theTeens.get(oneVal, "") - numWord = ("%s %s" % (hunWord, oneWord)).strip() + return f"{hunWord} {oneWord}".strip() else: oneWord = theOnes.get(oneVal, "") if tenVal == 0: - numWord = ("%s %s" % (hunWord, oneWord)).strip() + return f"{hunWord} {oneWord}".strip() else: + tenWord = theTens.get(tenVal, "") if oneVal == 0: - numWord = ("%s %s" % (hunWord, tenWord)).strip() + return f"{hunWord} {tenWord}".strip() else: - numWord = ("%s %s-%s" % (hunWord, tenWord, oneWord)).strip() + return f"{hunWord} {tenWord}-{oneWord}".strip() - return numWord + return "" diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index da8a0427..b0ab87ae 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -207,7 +207,9 @@ class GuiMainStatus(QStatusBar): self.timeText.setText("00:00:00") else: tS = int(time() - self.refTime) - self.timeText.setText(f"{tS//3600:02d}:{(tS//60)%60:02d}:{tS%60:02d}") + self.timeText.setText( + f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}" + ) return # END Class GuiMainStatus diff --git a/tests/test_tools.py b/tests/test_tools.py index bfff8f3f..4832aadb 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -61,6 +61,7 @@ def testNumberWords(): assert numberToWord(21, "en") == "Twenty-One" assert numberToWord(29, "en") == "Twenty-Nine" assert numberToWord(42, "en") == "Forty-Two" + assert numberToWord(114, "en") == "One Hundred Fourteen" assert numberToWord(142, "en") == "One Hundred Forty-Two" assert numberToWord(999, "en") == "Nine Hundred Ninety-Nine" From 487a1167959c9fc9414967173abd7f4dacec57f0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 15 Oct 2020 22:51:30 +0200 Subject: [PATCH 061/104] Removed some redundant parantheses --- nw/gui/statusbar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index b0ab87ae..0b95ef16 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -195,9 +195,9 @@ class GuiMainStatus(QStatusBar): self.statsText.setToolTip( "Project word count (session change)" ) - self.statsText.setText(( + self.statsText.setText( f"Words: {self.projWords:n} ({self.sessWords:+n})" - )) + ) return def _updateTime(self): From 062ec44890f6d9cda595bd6f0f2b003a89ef3c93 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 16 Oct 2020 14:11:02 +0200 Subject: [PATCH 062/104] Some minor fixes to the debian and ubuntu install scripts --- assets/installDebian.sh | 49 ------------------- ...nstallUbuntu.sh => installDebianUbuntu.sh} | 0 assets/mime/x-novelwriter-project.xml | 2 +- 3 files changed, 1 insertion(+), 50 deletions(-) delete mode 100755 assets/installDebian.sh rename assets/{installUbuntu.sh => installDebianUbuntu.sh} (100%) diff --git a/assets/installDebian.sh b/assets/installDebian.sh deleted file mode 100755 index 3f5ae392..00000000 --- a/assets/installDebian.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -cd .. - -EXEC=$(pwd)/novelWriter.py -EXEC=$(echo $EXEC | sed 's_/_\\/_g') - -sed "s/%%exec%%/$EXEC/g" assets/novelwriter.desktop > /usr/share/applications/novelwriter.desktop - -if [ ! -d /usr/share/icons/hicolor/24x24/apps ]; then - mkdir -pv /usr/share/icons/hicolor/24x24/apps -fi -if [ ! -d /usr/share/icons/hicolor/48x48/apps ]; then - mkdir -pv /usr/share/icons/hicolor/48x48/apps -fi -if [ ! -d /usr/share/icons/hicolor/96x96/apps ]; then - mkdir -pv /usr/share/icons/hicolor/96x96/apps -fi -if [ ! -d /usr/share/icons/hicolor/256x256/apps ]; then - mkdir -pv /usr/share/icons/hicolor/256x256/apps -fi -if [ ! -d /usr/share/icons/hicolor/512x512/apps ]; then - mkdir -pv /usr/share/icons/hicolor/512x512/apps -fi -if [ ! -d /usr/share/icons/hicolor/scalable/apps ]; then - mkdir -pv /usr/share/icons/hicolor/scalable/apps -fi -if [ ! -d /usr/share/icons/hicolor/scalable/mimetypes ]; then - mkdir -pv /usr/share/icons/hicolor/scalable/mimetypes -fi - -cp -v assets/icons/24x24/novelwriter.png /usr/share/icons/hicolor/24x24/apps/ -cp -v assets/icons/48x48/novelwriter.png /usr/share/icons/hicolor/48x48/apps/ -cp -v assets/icons/96x96/novelwriter.png /usr/share/icons/hicolor/96x96/apps/ -cp -v assets/icons/256x256/novelwriter.png /usr/share/icons/hicolor/256x256/apps/ -cp -v assets/icons/512x512/novelwriter.png /usr/share/icons/hicolor/512x512/apps/ -cp -v assets/icons/novelwriter.svg /usr/share/icons/hicolor/scalable/apps/ -cp -v assets/icons/x-novelwriter-project.svg /usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg -cp -v assets/mime/x-novelwriter-project.xml /usr/share/mime/packages/ - -update-mime-database /usr/share/mime/ -update-icon-caches /usr/share/icons/hicolor/24x24/apps/ -update-icon-caches /usr/share/icons/hicolor/48x48/apps/ -update-icon-caches /usr/share/icons/hicolor/96x96/apps/ -update-icon-caches /usr/share/icons/hicolor/256x256/apps/ -update-icon-caches /usr/share/icons/hicolor/512x512/apps/ -update-icon-caches /usr/share/icons/hicolor/1024x1024/apps/ -update-icon-caches /usr/share/icons/hicolor/scalable/apps/ -update-icon-caches /usr/share/icons/hicolor/scalable/mimetypes/ diff --git a/assets/installUbuntu.sh b/assets/installDebianUbuntu.sh similarity index 100% rename from assets/installUbuntu.sh rename to assets/installDebianUbuntu.sh diff --git a/assets/mime/x-novelwriter-project.xml b/assets/mime/x-novelwriter-project.xml index 21ecbe0a..789c08fd 100644 --- a/assets/mime/x-novelwriter-project.xml +++ b/assets/mime/x-novelwriter-project.xml @@ -1,7 +1,7 @@ - novelWriter Project + novelWriter Project From 0888bd70e1b73c224897bbb87a05f7e02297d4b9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 16 Oct 2020 14:22:10 +0200 Subject: [PATCH 063/104] Renamed the root assets folder to setup to avoid mixing it with nw/assets --- MANIFEST.in | 2 +- README.md | 4 ++-- docs/source/int_started.rst | 2 +- .../icons/1024x1024/novelwriter.png | Bin .../icons/1024x1024/x-novelwriter-project.png | Bin .../icons/128x128/novelwriter.png | Bin .../icons/128x128/x-novelwriter-project.png | Bin {assets => setup}/icons/16x16/novelwriter.png | Bin .../icons/16x16/x-novelwriter-project.png | Bin .../icons/16x16@2x/novelwriter.png | Bin .../icons/16x16@2x/x-novelwriter-project.png | Bin .../icons/18x18@2x/novelwriter.png | Bin .../icons/18x18@2x/x-novelwriter-project.png | Bin {assets => setup}/icons/24x24/novelwriter.png | Bin .../icons/24x24/x-novelwriter-project.png | Bin .../icons/256x256/novelwriter.png | Bin .../icons/256x256/x-novelwriter-project.png | Bin {assets => setup}/icons/32x32/novelwriter.png | Bin .../icons/32x32/x-novelwriter-project.png | Bin .../icons/32x32@2x/novelwriter.png | Bin .../icons/32x32@2x/x-novelwriter-project.png | Bin {assets => setup}/icons/48x48/novelwriter.png | Bin .../icons/48x48/x-novelwriter-project.png | Bin .../icons/512x512/novelwriter.png | Bin .../icons/512x512/x-novelwriter-project.png | Bin {assets => setup}/icons/64x64/novelwriter.png | Bin .../icons/64x64/x-novelwriter-project.png | Bin {assets => setup}/icons/96x96/novelwriter.png | Bin .../icons/96x96/x-novelwriter-project.png | Bin {assets => setup}/icons/novelwriter.ico | Bin {assets => setup}/icons/novelwriter.svg | 0 .../icons/x-novelwriter-project.svg | 0 {assets => setup}/installDebianUbuntu.sh | 18 +++++++++--------- .../mime/x-novelwriter-project.xml | 0 {assets => setup}/novelwriter.desktop | 0 35 files changed, 13 insertions(+), 13 deletions(-) rename {assets => setup}/icons/1024x1024/novelwriter.png (100%) rename {assets => setup}/icons/1024x1024/x-novelwriter-project.png (100%) rename {assets => setup}/icons/128x128/novelwriter.png (100%) rename {assets => setup}/icons/128x128/x-novelwriter-project.png (100%) rename {assets => setup}/icons/16x16/novelwriter.png (100%) rename {assets => setup}/icons/16x16/x-novelwriter-project.png (100%) rename {assets => setup}/icons/16x16@2x/novelwriter.png (100%) rename {assets => setup}/icons/16x16@2x/x-novelwriter-project.png (100%) rename {assets => setup}/icons/18x18@2x/novelwriter.png (100%) rename {assets => setup}/icons/18x18@2x/x-novelwriter-project.png (100%) rename {assets => setup}/icons/24x24/novelwriter.png (100%) rename {assets => setup}/icons/24x24/x-novelwriter-project.png (100%) rename {assets => setup}/icons/256x256/novelwriter.png (100%) rename {assets => setup}/icons/256x256/x-novelwriter-project.png (100%) rename {assets => setup}/icons/32x32/novelwriter.png (100%) rename {assets => setup}/icons/32x32/x-novelwriter-project.png (100%) rename {assets => setup}/icons/32x32@2x/novelwriter.png (100%) rename {assets => setup}/icons/32x32@2x/x-novelwriter-project.png (100%) rename {assets => setup}/icons/48x48/novelwriter.png (100%) rename {assets => setup}/icons/48x48/x-novelwriter-project.png (100%) rename {assets => setup}/icons/512x512/novelwriter.png (100%) rename {assets => setup}/icons/512x512/x-novelwriter-project.png (100%) rename {assets => setup}/icons/64x64/novelwriter.png (100%) rename {assets => setup}/icons/64x64/x-novelwriter-project.png (100%) rename {assets => setup}/icons/96x96/novelwriter.png (100%) rename {assets => setup}/icons/96x96/x-novelwriter-project.png (100%) rename {assets => setup}/icons/novelwriter.ico (100%) rename {assets => setup}/icons/novelwriter.svg (100%) rename {assets => setup}/icons/x-novelwriter-project.svg (100%) rename {assets => setup}/installDebianUbuntu.sh (54%) rename {assets => setup}/mime/x-novelwriter-project.xml (100%) rename {assets => setup}/novelwriter.desktop (100%) diff --git a/MANIFEST.in b/MANIFEST.in index 36a99646..b3004fc3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ include LICENSE.md -recursive-include assets * +recursive-include setup * recursive-include nw/assets * recursive-include sample *.nwx *.nwd diff --git a/README.md b/README.md index 46273c44..3b7835ed 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![pypi](https://img.shields.io/pypi/v/novelwriter)](https://pypi.org/project/novelWriter) [![python](https://img.shields.io/pypi/pyversions/novelwriter)](https://pypi.org/project/novelWriter) - + novelWriter is a Markdown-like text editor designed for writing novels and larger projects of many smaller plain text documents. It uses its own flavour of Markdown that supports a meta data syntax @@ -81,7 +81,7 @@ You can also provide a path to a folder containing a novelWriter project as the ### Launcher and Icons -In the root assets folder there are icons and scripts and a template for setting up a launcher on +In the root setup folder there are icons and scripts and a template for setting up a launcher on Gnome desktops. You may need to modify those scripts slightly, but as they are, they work on Debian and Ubuntu. For other operating systems, please consult your operating system documentation for how to make those. Feel free to submit more if you are able to make them. diff --git a/docs/source/int_started.rst b/docs/source/int_started.rst index e96a0285..0d0236c8 100644 --- a/docs/source/int_started.rst +++ b/docs/source/int_started.rst @@ -140,7 +140,7 @@ encountered. To list all options, run: python novelWriter.py --help -There are also a couple of install scripts in the assets folder which will assist in setting up a +There are also a couple of install scripts in the setup folder which will assist in setting up a launch icon and the novelWriter project file mimetype for Gnome desktops on Linux. Currently, there's one script for Debian and one for Ubuntu. diff --git a/assets/icons/1024x1024/novelwriter.png b/setup/icons/1024x1024/novelwriter.png similarity index 100% rename from assets/icons/1024x1024/novelwriter.png rename to setup/icons/1024x1024/novelwriter.png diff --git a/assets/icons/1024x1024/x-novelwriter-project.png b/setup/icons/1024x1024/x-novelwriter-project.png similarity index 100% rename from assets/icons/1024x1024/x-novelwriter-project.png rename to setup/icons/1024x1024/x-novelwriter-project.png diff --git a/assets/icons/128x128/novelwriter.png b/setup/icons/128x128/novelwriter.png similarity index 100% rename from assets/icons/128x128/novelwriter.png rename to setup/icons/128x128/novelwriter.png diff --git a/assets/icons/128x128/x-novelwriter-project.png b/setup/icons/128x128/x-novelwriter-project.png similarity index 100% rename from assets/icons/128x128/x-novelwriter-project.png rename to setup/icons/128x128/x-novelwriter-project.png diff --git a/assets/icons/16x16/novelwriter.png b/setup/icons/16x16/novelwriter.png similarity index 100% rename from assets/icons/16x16/novelwriter.png rename to setup/icons/16x16/novelwriter.png diff --git a/assets/icons/16x16/x-novelwriter-project.png b/setup/icons/16x16/x-novelwriter-project.png similarity index 100% rename from assets/icons/16x16/x-novelwriter-project.png rename to setup/icons/16x16/x-novelwriter-project.png diff --git a/assets/icons/16x16@2x/novelwriter.png b/setup/icons/16x16@2x/novelwriter.png similarity index 100% rename from assets/icons/16x16@2x/novelwriter.png rename to setup/icons/16x16@2x/novelwriter.png diff --git a/assets/icons/16x16@2x/x-novelwriter-project.png b/setup/icons/16x16@2x/x-novelwriter-project.png similarity index 100% rename from assets/icons/16x16@2x/x-novelwriter-project.png rename to setup/icons/16x16@2x/x-novelwriter-project.png diff --git a/assets/icons/18x18@2x/novelwriter.png b/setup/icons/18x18@2x/novelwriter.png similarity index 100% rename from assets/icons/18x18@2x/novelwriter.png rename to setup/icons/18x18@2x/novelwriter.png diff --git a/assets/icons/18x18@2x/x-novelwriter-project.png b/setup/icons/18x18@2x/x-novelwriter-project.png similarity index 100% rename from assets/icons/18x18@2x/x-novelwriter-project.png rename to setup/icons/18x18@2x/x-novelwriter-project.png diff --git a/assets/icons/24x24/novelwriter.png b/setup/icons/24x24/novelwriter.png similarity index 100% rename from assets/icons/24x24/novelwriter.png rename to setup/icons/24x24/novelwriter.png diff --git a/assets/icons/24x24/x-novelwriter-project.png b/setup/icons/24x24/x-novelwriter-project.png similarity index 100% rename from assets/icons/24x24/x-novelwriter-project.png rename to setup/icons/24x24/x-novelwriter-project.png diff --git a/assets/icons/256x256/novelwriter.png b/setup/icons/256x256/novelwriter.png similarity index 100% rename from assets/icons/256x256/novelwriter.png rename to setup/icons/256x256/novelwriter.png diff --git a/assets/icons/256x256/x-novelwriter-project.png b/setup/icons/256x256/x-novelwriter-project.png similarity index 100% rename from assets/icons/256x256/x-novelwriter-project.png rename to setup/icons/256x256/x-novelwriter-project.png diff --git a/assets/icons/32x32/novelwriter.png b/setup/icons/32x32/novelwriter.png similarity index 100% rename from assets/icons/32x32/novelwriter.png rename to setup/icons/32x32/novelwriter.png diff --git a/assets/icons/32x32/x-novelwriter-project.png b/setup/icons/32x32/x-novelwriter-project.png similarity index 100% rename from assets/icons/32x32/x-novelwriter-project.png rename to setup/icons/32x32/x-novelwriter-project.png diff --git a/assets/icons/32x32@2x/novelwriter.png b/setup/icons/32x32@2x/novelwriter.png similarity index 100% rename from assets/icons/32x32@2x/novelwriter.png rename to setup/icons/32x32@2x/novelwriter.png diff --git a/assets/icons/32x32@2x/x-novelwriter-project.png b/setup/icons/32x32@2x/x-novelwriter-project.png similarity index 100% rename from assets/icons/32x32@2x/x-novelwriter-project.png rename to setup/icons/32x32@2x/x-novelwriter-project.png diff --git a/assets/icons/48x48/novelwriter.png b/setup/icons/48x48/novelwriter.png similarity index 100% rename from assets/icons/48x48/novelwriter.png rename to setup/icons/48x48/novelwriter.png diff --git a/assets/icons/48x48/x-novelwriter-project.png b/setup/icons/48x48/x-novelwriter-project.png similarity index 100% rename from assets/icons/48x48/x-novelwriter-project.png rename to setup/icons/48x48/x-novelwriter-project.png diff --git a/assets/icons/512x512/novelwriter.png b/setup/icons/512x512/novelwriter.png similarity index 100% rename from assets/icons/512x512/novelwriter.png rename to setup/icons/512x512/novelwriter.png diff --git a/assets/icons/512x512/x-novelwriter-project.png b/setup/icons/512x512/x-novelwriter-project.png similarity index 100% rename from assets/icons/512x512/x-novelwriter-project.png rename to setup/icons/512x512/x-novelwriter-project.png diff --git a/assets/icons/64x64/novelwriter.png b/setup/icons/64x64/novelwriter.png similarity index 100% rename from assets/icons/64x64/novelwriter.png rename to setup/icons/64x64/novelwriter.png diff --git a/assets/icons/64x64/x-novelwriter-project.png b/setup/icons/64x64/x-novelwriter-project.png similarity index 100% rename from assets/icons/64x64/x-novelwriter-project.png rename to setup/icons/64x64/x-novelwriter-project.png diff --git a/assets/icons/96x96/novelwriter.png b/setup/icons/96x96/novelwriter.png similarity index 100% rename from assets/icons/96x96/novelwriter.png rename to setup/icons/96x96/novelwriter.png diff --git a/assets/icons/96x96/x-novelwriter-project.png b/setup/icons/96x96/x-novelwriter-project.png similarity index 100% rename from assets/icons/96x96/x-novelwriter-project.png rename to setup/icons/96x96/x-novelwriter-project.png diff --git a/assets/icons/novelwriter.ico b/setup/icons/novelwriter.ico similarity index 100% rename from assets/icons/novelwriter.ico rename to setup/icons/novelwriter.ico diff --git a/assets/icons/novelwriter.svg b/setup/icons/novelwriter.svg similarity index 100% rename from assets/icons/novelwriter.svg rename to setup/icons/novelwriter.svg diff --git a/assets/icons/x-novelwriter-project.svg b/setup/icons/x-novelwriter-project.svg similarity index 100% rename from assets/icons/x-novelwriter-project.svg rename to setup/icons/x-novelwriter-project.svg diff --git a/assets/installDebianUbuntu.sh b/setup/installDebianUbuntu.sh similarity index 54% rename from assets/installDebianUbuntu.sh rename to setup/installDebianUbuntu.sh index 91e78409..c47f4d26 100755 --- a/assets/installDebianUbuntu.sh +++ b/setup/installDebianUbuntu.sh @@ -5,7 +5,7 @@ cd .. EXEC=$(pwd)/novelWriter.py EXEC=$(echo $EXEC | sed 's_/_\\/_g') -sed "s/%%exec%%/$EXEC/g" assets/novelwriter.desktop > /usr/share/applications/novelwriter.desktop +sed "s/%%exec%%/$EXEC/g" setup/novelwriter.desktop > /usr/share/applications/novelwriter.desktop if [ ! -d /usr/share/icons/hicolor/24x24/apps ]; then mkdir -pv /usr/share/icons/hicolor/24x24/apps @@ -29,14 +29,14 @@ if [ ! -d /usr/share/icons/hicolor/scalable/mimetypes ]; then mkdir -pv /usr/share/icons/hicolor/scalable/mimetypes fi -cp -v assets/icons/24x24/novelwriter.png /usr/share/icons/hicolor/24x24/apps/ -cp -v assets/icons/48x48/novelwriter.png /usr/share/icons/hicolor/48x48/apps/ -cp -v assets/icons/96x96/novelwriter.png /usr/share/icons/hicolor/96x96/apps/ -cp -v assets/icons/256x256/novelwriter.png /usr/share/icons/hicolor/256x256/apps/ -cp -v assets/icons/512x512/novelwriter.png /usr/share/icons/hicolor/512x512/apps/ -cp -v assets/icons/novelwriter.svg /usr/share/icons/hicolor/scalable/apps/ -cp -v assets/icons/x-novelwriter-project.svg /usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg -cp -v assets/mime/x-novelwriter-project.xml /usr/share/mime/packages/ +cp -v setup/icons/24x24/novelwriter.png /usr/share/icons/hicolor/24x24/apps/ +cp -v setup/icons/48x48/novelwriter.png /usr/share/icons/hicolor/48x48/apps/ +cp -v setup/icons/96x96/novelwriter.png /usr/share/icons/hicolor/96x96/apps/ +cp -v setup/icons/256x256/novelwriter.png /usr/share/icons/hicolor/256x256/apps/ +cp -v setup/icons/512x512/novelwriter.png /usr/share/icons/hicolor/512x512/apps/ +cp -v setup/icons/novelwriter.svg /usr/share/icons/hicolor/scalable/apps/ +cp -v setup/icons/x-novelwriter-project.svg /usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg +cp -v setup/mime/x-novelwriter-project.xml /usr/share/mime/packages/ update-mime-database /usr/share/mime/ update-icon-caches /usr/share/icons/* diff --git a/assets/mime/x-novelwriter-project.xml b/setup/mime/x-novelwriter-project.xml similarity index 100% rename from assets/mime/x-novelwriter-project.xml rename to setup/mime/x-novelwriter-project.xml diff --git a/assets/novelwriter.desktop b/setup/novelwriter.desktop similarity index 100% rename from assets/novelwriter.desktop rename to setup/novelwriter.desktop From 403066a83f07d573823811adae3f925f3472260b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 16 Oct 2020 18:53:29 +0200 Subject: [PATCH 064/104] Moe settings from setup.py to setup.cfg, and add pyproject.toml --- pyproject.toml | 3 +++ setup.cfg | 45 ++++++++++++++++++++++++++++++++++++++++++++- setup.py | 48 +----------------------------------------------- 3 files changed, 48 insertions(+), 48 deletions(-) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..9787c3bd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index ff0117c5..f9b19c12 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,49 @@ [metadata] -license_files = LICENSE.md +name = novelWriter version = attr: nw.__version__ +author = Veronica Berglyd Olsen +author_email = code@vkbo.net +description = A markdown-like document editor for writing novels +url = https://novelwriter.io +long_description = file: README.md +long_description_content_type = text/markdown +license_files = LICENSE.md +license = GNU General Public License v3 +classifiers = + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: Implementation :: CPython + License :: OSI Approved :: GNU General Public License v3 (GPLv3) + Development Status :: 4 - Beta + Operating System :: OS Independent + Intended Audience :: End Users/Desktop + Natural Language :: English + Topic :: Text Editors +python_requires = >=3.6 +install_requires = + pyqt5>=5.2.1 + lxml>=4.2.0 + pyenchant>=3.0.0 +project_urls = + Bug Tracker = https://github.com/vkbo/novelWriter/issues + Documentation = https://github.com/vkbo/novelWriter/issues + Source Code = https://github.com/vkbo/novelWriter + +[options] +include_package_data = True +packages = find: + +[options.packages.find] +exclude = docs, tests, sample + +[options.entry_points] +console_script = + novelWriter-cli = nw:main +gui_scripts = + novelWriter = nw:main [bdist_wheel] universal = 0 diff --git a/setup.py b/setup.py index 6b10a84e..ed17b904 100755 --- a/setup.py +++ b/setup.py @@ -113,50 +113,4 @@ if len(sys.argv) == 1: # Build the Package ## -# Read content from files -with open("README.md", "r") as inFile: - longDescription = inFile.read() - -setuptools.setup( - name = "novelWriter", - # version = __version__, # Set in setup.cfg - author = "Veronica Berglyd Olsen", - author_email = "code@vkbo.net", - description = "A markdown-like document editor for writing novels", - long_description = longDescription, - long_description_content_type = "text/markdown", - license = "GNU General Public License v3", - url = "https://novelwriter.io", - entry_points = { - "console_scripts" : ["novelWriter-cli=nw:main"], - "gui_scripts" : ["novelWriter=nw:main"], - }, - packages = setuptools.find_packages(exclude=["docs", "tests", "sample"]), - include_package_data = True, - package_data = {"": ["*.conf"]}, - project_urls = { - "Bug Tracker": "https://github.com/vkbo/novelWriter/issues", - "Documentation": "https://github.com/vkbo/novelWriter/issues", - "Source Code": "https://github.com/vkbo/novelWriter", - }, - classifiers = [ - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: Implementation :: CPython", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Development Status :: 4 - Beta", - "Operating System :: OS Independent", - "Intended Audience :: End Users/Desktop", - "Natural Language :: English", - "Topic :: Text Editors", - ], - python_requires = ">=3.6", - install_requires = [ - "pyqt5>=5.2.1", - "lxml>=4.2.0", - "pyenchant>=3.0.0", - ], -) +setuptools.setup() From 70086f5ca65dde4bafe39ff1d8d4bfe8710ddd5d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 00:58:48 +0200 Subject: [PATCH 065/104] Added python and iss script for making setup.exe releases on Windows --- make_windows.py | 177 ++++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- setup/win_setup.iss | 51 +++++++++++++ 3 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 make_windows.py create mode 100644 setup/win_setup.iss diff --git a/make_windows.py b/make_windows.py new file mode 100644 index 00000000..c64b79d0 --- /dev/null +++ b/make_windows.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import nw +import os +import sys +import getopt +import subprocess + +# Defaults +buildWindowed = True +runPip = False +oneFile = False +makeSetup = False +innoSetup = None + +# Parse Options +shortOpt = "hd" +longOpt = [ + "help", + "debug", + "pip", + "onefile", + "setup", + "inno=", +] +helpMsg = ( + "\n" + "novelWriter Install Script\n" + "\n" + "Usage:\n" + " -h, --help Print this message.\n" + " --pip Install dependecies first.\n" + " --onefile Create a single executable file.\n" + " --setup Make Inno Setup file.\n" + " --inno= Path to the Inoo Setup exec.\n" + " -d, --debug Build novelWriter for debugging. To debug novelwriter after build,\n" + " run it from command line with the debug options. Please check the\n" + " novelWriter --help output for details.\n" +) + +try: + inOpts, inArgs = getopt.getopt(sys.argv[1:], shortOpt, longOpt) +except getopt.GetoptError: + print(helpMsg) + sys.exit(1) + +for inOpt, inArg in inOpts: + if inOpt in ("-h", "--help"): + print(helpMsg) + sys.exit(0) + elif inOpt in ("-d", "--debug"): + buildWindowed = False + elif inOpt == "--pip": + runPip = True + elif inOpt == "--onefile": + oneFile = True + elif inOpt == "--setup": + makeSetup = True + elif inOpt == "--inno": + innoSetup = inArg + +# Run pip +if runPip: + print("") + print("###########################") + print(" Installing Dependencies") + print("###########################") + print("") + try: + subprocess.call([ + sys.executable, "-m", + "pip", "install", "--user", "--upgrade", "pip" + ]) + subprocess.call([ + sys.executable, "-m", + "pip", "install", "--user", "--upgrade", "pyinstaller" + ]) + subprocess.call([ + sys.executable, "-m", + "pip", "install", "--user", "--upgrade", "-r", "requirements.txt" + ]) + except Exception as e: + print("Failed with error:") + print(str(e)) + sys.exit(1) + +# Run pyinstaller +print("") +print("#######################") +print(" Running PyInstaller") +print("#######################") +print("") +instOpt = [ + "--name=novelWriter", + "--clean", + "--add-data=%s;%s" % (os.path.join("nw", "assets"), "assets"), + "--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"), + "--exclude-module=PyQt5.QtQml", + "--exclude-module=PyQt5.QtBluetooth", + "--exclude-module=PyQt5.QtDBus", + "--exclude-module=PyQt5.QtMultimedia", + "--exclude-module=PyQt5.QtMultimediaWidgets", + "--exclude-module=PyQt5.QtNetwork", + "--exclude-module=PyQt5.QtNetworkAuth", + "--exclude-module=PyQt5.QtNfc", + "--exclude-module=PyQt5.QtQuick", + "--exclude-module=PyQt5.QtQuickWidgets", + "--exclude-module=PyQt5.QtRemoteObjects", + "--exclude-module=PyQt5.QtSensors", + "--exclude-module=PyQt5.QtSerialPort", + "--exclude-module=PyQt5.QtSql", +] +if buildWindowed: + instOpt.append("--windowed") +if oneFile and not makeSetup: + instOpt.append("--onefile") +else: + instOpt.append("--onedir") + +instOpt.append("novelWriter.py") + +import PyInstaller.__main__ # noqa: E402 +PyInstaller.__main__.run(instOpt) + +if not oneFile: + delIfExists = [ + "Qt5DBus.dll", "Qt5Network.dll", "Qt5Qml.dll", "Qt5QmlModels.dll", "Qt5Quick.dll", + "Qt5Quick3D.dll", "Qt5Quick3DAssetImport.dll", "Qt5Quick3DRender.dll", + "Qt5Quick3DRuntimeRender.dll", "Qt5Quick3DUtils.dll", "Qt5Sql.dll" + ] + distDir = os.path.join(os.getcwd(), "dist", "novelWriter") + for delFile in delIfExists: + delPath = os.path.join(distDir, delFile) + if os.path.isfile(delPath): + print("Deleting file: %s" % delPath) + os.unlink(delPath) + +print("") +print("Build Finished") +print("") +print("If everything went well, the novelWriter executable should be in the folder named 'dist'") +print("") + +if makeSetup: + print("") + print("######################") + print(" Running Inno Setup") + print("######################") + print("") + if innoSetup is None: + innoSetup = "C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe" + if not os.path.isfile(innoSetup): + print("ERROR: Cannot fine Inno Setup's ISCC.exe file.") + print(" Looked in: %s" % innoSetup) + print(" Please provide a path with the --inno= option.") + sys.exit(1) + + # Read the iss template + issData = "" + with open(os.path.join("setup", "win_setup.iss"), mode="r") as inFile: + issData = inFile.read() + + issData = issData.replace(r"%%version%%", nw.__version__) + issData = issData.replace(r"%%dir%%", os.getcwd()) + + with open("setup.iss", mode="w+") as outFile: + outFile.write(issData) + + try: + subprocess.call( + [innoSetup, "setup.iss"] + ) + except Exception as e: + print("Failed with error:") + print(str(e)) + sys.exit(1) diff --git a/setup.cfg b/setup.cfg index f9b19c12..ad1c6ddb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,7 @@ description = A markdown-like document editor for writing novels url = https://novelwriter.io long_description = file: README.md long_description_content_type = text/markdown -license_files = LICENSE.md +license_file = LICENSE.md license = GNU General Public License v3 classifiers = Programming Language :: Python :: 3 :: Only diff --git a/setup/win_setup.iss b/setup/win_setup.iss new file mode 100644 index 00000000..6491b317 --- /dev/null +++ b/setup/win_setup.iss @@ -0,0 +1,51 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define nwAppDir "%%dir%%\dist" +#define nwAppName "novelWriter" +#define nwAppVersion "%%version%%" +#define nwAppPublisher "novelWriter" +#define nwAppURL "http://novelWriter.io" +#define nwAppExeName "novelWriter.exe" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{459A75D0-951F-4932-9809-6002EC8E733E} +AppName={#nwAppName} +AppVersion={#nwAppVersion} +AppVerName={#nwAppName} {#nwAppVersion} +AppPublisher={#nwAppPublisher} +AppPublisherURL={#nwAppURL} +AppSupportURL={#nwAppURL} +AppUpdatesURL={#nwAppURL} +DefaultDirName={autopf}\{#nwAppName} +DisableProgramGroupPage=yes +; The [Icons] "quicklaunchicon" entry uses {userappdata} but its [Tasks] entry has a proper IsAdminInstallMode Check. +UsedUserAreasWarning=no +; Uncomment the following line to run in non administrative install mode (install for current user only.) +;PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +OutputDir={#nwAppDir} +OutputBaseFilename=setup-novelwriter-{#nwAppVersion} +Compression=lzma +SolidCompression=yes +WizardStyle=modern + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode + +[Files] +Source: "{#nwAppDir}\novelWriter\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{autoprograms}\{#nwAppName}"; Filename: "{app}\{#nwAppExeName}" +Name: "{autodesktop}\{#nwAppName}"; Filename: "{app}\{#nwAppExeName}"; Tasks: desktopicon +Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#nwAppName}"; Filename: "{app}\{#nwAppExeName}"; Tasks: quicklaunchicon + +[Run] +Filename: "{app}\{#nwAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(nwAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent From 115cef3ffb18c5fe601ddbdcdd1505207037bf20 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 01:05:49 +0200 Subject: [PATCH 066/104] Added more comments to windows script --- make_windows.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/make_windows.py b/make_windows.py index c64b79d0..27e8c112 100644 --- a/make_windows.py +++ b/make_windows.py @@ -1,5 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +""" +This script will either build: + * A single file executable named dist/novelWriter.exe. This is a quite + slow option, and the file is fairly big. Option --onefile + * A single directory named dist/novelWriter with a novelWriter.exe, and + all dependecies included. This is the default. + * The latter can be combined with a build stage of a setup.exe file + named setup-novelwriter-.exe. Option --setup. + +In addition, providing the --pip flag will cause the script to try to +install all dependencies needed for runing the build, and for running +novelWriter itself. +""" import nw import os @@ -26,14 +39,14 @@ longOpt = [ ] helpMsg = ( "\n" - "novelWriter Install Script\n" + "novelWriter Install Script for Windows\n" "\n" "Usage:\n" " -h, --help Print this message.\n" " --pip Install dependecies first.\n" " --onefile Create a single executable file.\n" " --setup Make Inno Setup file.\n" - " --inno= Path to the Inoo Setup exec.\n" + " --inno= Path to the Inno Setup exec.\n" " -d, --debug Build novelWriter for debugging. To debug novelwriter after build,\n" " run it from command line with the debug options. Please check the\n" " novelWriter --help output for details.\n" @@ -111,8 +124,10 @@ instOpt = [ "--exclude-module=PyQt5.QtSerialPort", "--exclude-module=PyQt5.QtSql", ] + if buildWindowed: instOpt.append("--windowed") + if oneFile and not makeSetup: instOpt.append("--onefile") else: @@ -124,6 +139,7 @@ import PyInstaller.__main__ # noqa: E402 PyInstaller.__main__.run(instOpt) if not oneFile: + # These dll files are not nee3ded, and take up a fair bit of space. delIfExists = [ "Qt5DBus.dll", "Qt5Network.dll", "Qt5Qml.dll", "Qt5QmlModels.dll", "Qt5Quick.dll", "Qt5Quick3D.dll", "Qt5Quick3DAssetImport.dll", "Qt5Quick3DRender.dll", @@ -168,9 +184,7 @@ if makeSetup: outFile.write(issData) try: - subprocess.call( - [innoSetup, "setup.iss"] - ) + subprocess.call([innoSetup, "setup.iss"]) except Exception as e: print("Failed with error:") print(str(e)) From 0665b9b0321910752e30eba3a781543178e31b38 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 01:30:13 +0200 Subject: [PATCH 067/104] Added temp iss file to gitignore, and fiex a bug in windows make --- .gitignore | 1 + make_windows.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 57dc50a9..830a409a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /deploy/ *.spec *.egg-info +setup.iss # Documentation /docs/build/ diff --git a/make_windows.py b/make_windows.py index 27e8c112..cc75f519 100644 --- a/make_windows.py +++ b/make_windows.py @@ -14,7 +14,6 @@ install all dependencies needed for runing the build, and for running novelWriter itself. """ -import nw import os import sys import getopt @@ -177,6 +176,7 @@ if makeSetup: with open(os.path.join("setup", "win_setup.iss"), mode="r") as inFile: issData = inFile.read() + import nw # noqa: E402 issData = issData.replace(r"%%version%%", nw.__version__) issData = issData.replace(r"%%dir%%", os.getcwd()) From ede9d034c797b393867ea86a29464469aeaf42c1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 01:43:47 +0200 Subject: [PATCH 068/104] Make sample.zip before pyinstaller runs, and make sure the make_windows script is windows only --- make_windows.py | 395 +++++++++++++++++++++++++----------------------- 1 file changed, 204 insertions(+), 191 deletions(-) mode change 100644 => 100755 make_windows.py diff --git a/make_windows.py b/make_windows.py old mode 100644 new mode 100755 index cc75f519..d5b938c0 --- a/make_windows.py +++ b/make_windows.py @@ -1,191 +1,204 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -This script will either build: - * A single file executable named dist/novelWriter.exe. This is a quite - slow option, and the file is fairly big. Option --onefile - * A single directory named dist/novelWriter with a novelWriter.exe, and - all dependecies included. This is the default. - * The latter can be combined with a build stage of a setup.exe file - named setup-novelwriter-.exe. Option --setup. - -In addition, providing the --pip flag will cause the script to try to -install all dependencies needed for runing the build, and for running -novelWriter itself. -""" - -import os -import sys -import getopt -import subprocess - -# Defaults -buildWindowed = True -runPip = False -oneFile = False -makeSetup = False -innoSetup = None - -# Parse Options -shortOpt = "hd" -longOpt = [ - "help", - "debug", - "pip", - "onefile", - "setup", - "inno=", -] -helpMsg = ( - "\n" - "novelWriter Install Script for Windows\n" - "\n" - "Usage:\n" - " -h, --help Print this message.\n" - " --pip Install dependecies first.\n" - " --onefile Create a single executable file.\n" - " --setup Make Inno Setup file.\n" - " --inno= Path to the Inno Setup exec.\n" - " -d, --debug Build novelWriter for debugging. To debug novelwriter after build,\n" - " run it from command line with the debug options. Please check the\n" - " novelWriter --help output for details.\n" -) - -try: - inOpts, inArgs = getopt.getopt(sys.argv[1:], shortOpt, longOpt) -except getopt.GetoptError: - print(helpMsg) - sys.exit(1) - -for inOpt, inArg in inOpts: - if inOpt in ("-h", "--help"): - print(helpMsg) - sys.exit(0) - elif inOpt in ("-d", "--debug"): - buildWindowed = False - elif inOpt == "--pip": - runPip = True - elif inOpt == "--onefile": - oneFile = True - elif inOpt == "--setup": - makeSetup = True - elif inOpt == "--inno": - innoSetup = inArg - -# Run pip -if runPip: - print("") - print("###########################") - print(" Installing Dependencies") - print("###########################") - print("") - try: - subprocess.call([ - sys.executable, "-m", - "pip", "install", "--user", "--upgrade", "pip" - ]) - subprocess.call([ - sys.executable, "-m", - "pip", "install", "--user", "--upgrade", "pyinstaller" - ]) - subprocess.call([ - sys.executable, "-m", - "pip", "install", "--user", "--upgrade", "-r", "requirements.txt" - ]) - except Exception as e: - print("Failed with error:") - print(str(e)) - sys.exit(1) - -# Run pyinstaller -print("") -print("#######################") -print(" Running PyInstaller") -print("#######################") -print("") -instOpt = [ - "--name=novelWriter", - "--clean", - "--add-data=%s;%s" % (os.path.join("nw", "assets"), "assets"), - "--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"), - "--exclude-module=PyQt5.QtQml", - "--exclude-module=PyQt5.QtBluetooth", - "--exclude-module=PyQt5.QtDBus", - "--exclude-module=PyQt5.QtMultimedia", - "--exclude-module=PyQt5.QtMultimediaWidgets", - "--exclude-module=PyQt5.QtNetwork", - "--exclude-module=PyQt5.QtNetworkAuth", - "--exclude-module=PyQt5.QtNfc", - "--exclude-module=PyQt5.QtQuick", - "--exclude-module=PyQt5.QtQuickWidgets", - "--exclude-module=PyQt5.QtRemoteObjects", - "--exclude-module=PyQt5.QtSensors", - "--exclude-module=PyQt5.QtSerialPort", - "--exclude-module=PyQt5.QtSql", -] - -if buildWindowed: - instOpt.append("--windowed") - -if oneFile and not makeSetup: - instOpt.append("--onefile") -else: - instOpt.append("--onedir") - -instOpt.append("novelWriter.py") - -import PyInstaller.__main__ # noqa: E402 -PyInstaller.__main__.run(instOpt) - -if not oneFile: - # These dll files are not nee3ded, and take up a fair bit of space. - delIfExists = [ - "Qt5DBus.dll", "Qt5Network.dll", "Qt5Qml.dll", "Qt5QmlModels.dll", "Qt5Quick.dll", - "Qt5Quick3D.dll", "Qt5Quick3DAssetImport.dll", "Qt5Quick3DRender.dll", - "Qt5Quick3DRuntimeRender.dll", "Qt5Quick3DUtils.dll", "Qt5Sql.dll" - ] - distDir = os.path.join(os.getcwd(), "dist", "novelWriter") - for delFile in delIfExists: - delPath = os.path.join(distDir, delFile) - if os.path.isfile(delPath): - print("Deleting file: %s" % delPath) - os.unlink(delPath) - -print("") -print("Build Finished") -print("") -print("If everything went well, the novelWriter executable should be in the folder named 'dist'") -print("") - -if makeSetup: - print("") - print("######################") - print(" Running Inno Setup") - print("######################") - print("") - if innoSetup is None: - innoSetup = "C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe" - if not os.path.isfile(innoSetup): - print("ERROR: Cannot fine Inno Setup's ISCC.exe file.") - print(" Looked in: %s" % innoSetup) - print(" Please provide a path with the --inno= option.") - sys.exit(1) - - # Read the iss template - issData = "" - with open(os.path.join("setup", "win_setup.iss"), mode="r") as inFile: - issData = inFile.read() - - import nw # noqa: E402 - issData = issData.replace(r"%%version%%", nw.__version__) - issData = issData.replace(r"%%dir%%", os.getcwd()) - - with open("setup.iss", mode="w+") as outFile: - outFile.write(issData) - - try: - subprocess.call([innoSetup, "setup.iss"]) - except Exception as e: - print("Failed with error:") - print(str(e)) - sys.exit(1) +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +This script will either build: + * A single file executable named dist/novelWriter.exe. This is a quite + slow option, and the file is fairly big. Option --onefile + * A single directory named dist/novelWriter with a novelWriter.exe, and + all dependecies included. This is the default. + * The latter can be combined with a build stage of a setup.exe file + named setup-novelwriter-.exe. Option --setup. + +In addition, providing the --pip flag will cause the script to try to +install all dependencies needed for runing the build, and for running +novelWriter itself. +""" + +import os +import sys +import getopt +import subprocess + +if not sys.platform.startswith("win32"): + print("ERROR: This script is intended for Windows only.") + sys.exit(1) + +# Defaults +buildWindowed = True +runPip = False +oneFile = False +makeSetup = False +innoSetup = None + +# Parse Options +shortOpt = "hd" +longOpt = [ + "help", + "debug", + "pip", + "onefile", + "setup", + "inno=", +] +helpMsg = ( + "\n" + "novelWriter Install Script for Windows\n" + "\n" + "Usage:\n" + " -h, --help Print this message.\n" + " --pip Install dependecies first.\n" + " --onefile Create a single executable file.\n" + " --setup Make Inno Setup file.\n" + " --inno= Path to the Inno Setup exec.\n" + " -d, --debug Build novelWriter for debugging. To debug novelwriter after build,\n" + " run it from command line with the debug options. Please check the\n" + " novelWriter --help output for details.\n" +) + +try: + inOpts, inArgs = getopt.getopt(sys.argv[1:], shortOpt, longOpt) +except getopt.GetoptError: + print(helpMsg) + sys.exit(1) + +for inOpt, inArg in inOpts: + if inOpt in ("-h", "--help"): + print(helpMsg) + sys.exit(0) + elif inOpt in ("-d", "--debug"): + buildWindowed = False + elif inOpt == "--pip": + runPip = True + elif inOpt == "--onefile": + oneFile = True + elif inOpt == "--setup": + makeSetup = True + elif inOpt == "--inno": + innoSetup = inArg + +# Run pip +if runPip: + print("") + print("###########################") + print(" Installing Dependencies") + print("###########################") + print("") + try: + subprocess.call([ + sys.executable, "-m", + "pip", "install", "--user", "--upgrade", "pip" + ]) + subprocess.call([ + sys.executable, "-m", + "pip", "install", "--user", "--upgrade", "pyinstaller" + ]) + subprocess.call([ + sys.executable, "-m", + "pip", "install", "--user", "--upgrade", "-r", "requirements.txt" + ]) + except Exception as e: + print("Failed with error:") + print(str(e)) + sys.exit(1) + +# Run pyinstaller +print("") +print("#######################") +print(" Running PyInstaller") +print("#######################") +print("") +instOpt = [ + "--name=novelWriter", + "--clean", + "--add-data=%s;%s" % (os.path.join("nw", "assets"), "assets"), + "--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"), + "--exclude-module=PyQt5.QtQml", + "--exclude-module=PyQt5.QtBluetooth", + "--exclude-module=PyQt5.QtDBus", + "--exclude-module=PyQt5.QtMultimedia", + "--exclude-module=PyQt5.QtMultimediaWidgets", + "--exclude-module=PyQt5.QtNetwork", + "--exclude-module=PyQt5.QtNetworkAuth", + "--exclude-module=PyQt5.QtNfc", + "--exclude-module=PyQt5.QtQuick", + "--exclude-module=PyQt5.QtQuickWidgets", + "--exclude-module=PyQt5.QtRemoteObjects", + "--exclude-module=PyQt5.QtSensors", + "--exclude-module=PyQt5.QtSerialPort", + "--exclude-module=PyQt5.QtSql", +] + +if buildWindowed: + instOpt.append("--windowed") + +if oneFile and not makeSetup: + instOpt.append("--onefile") +else: + instOpt.append("--onedir") + +instOpt.append("novelWriter.py") + +# Make sample.zip first +print("Building sample.zip") +try: + subprocess.call([sys.executable, "setup.py" "sample"]) +except Exception as e: + print("Failed with error:") + print(str(e)) + sys.exit(1) + +import PyInstaller.__main__ # noqa: E402 +PyInstaller.__main__.run(instOpt) + +if not oneFile: + # These dll files are not nee3ded, and take up a fair bit of space. + delIfExists = [ + "Qt5DBus.dll", "Qt5Network.dll", "Qt5Qml.dll", "Qt5QmlModels.dll", "Qt5Quick.dll", + "Qt5Quick3D.dll", "Qt5Quick3DAssetImport.dll", "Qt5Quick3DRender.dll", + "Qt5Quick3DRuntimeRender.dll", "Qt5Quick3DUtils.dll", "Qt5Sql.dll" + ] + distDir = os.path.join(os.getcwd(), "dist", "novelWriter") + for delFile in delIfExists: + delPath = os.path.join(distDir, delFile) + if os.path.isfile(delPath): + print("Deleting file: %s" % delPath) + os.unlink(delPath) + +print("") +print("Build Finished") +print("") +print("If everything went well, the novelWriter executable should be in the folder named 'dist'") +print("") + +if makeSetup: + print("") + print("######################") + print(" Running Inno Setup") + print("######################") + print("") + if innoSetup is None: + innoSetup = "C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe" + if not os.path.isfile(innoSetup): + print("ERROR: Cannot fine Inno Setup's ISCC.exe file.") + print(" Looked in: %s" % innoSetup) + print(" Please provide a path with the --inno= option.") + sys.exit(1) + + # Read the iss template + issData = "" + with open(os.path.join("setup", "win_setup.iss"), mode="r") as inFile: + issData = inFile.read() + + import nw # noqa: E402 + issData = issData.replace(r"%%version%%", nw.__version__) + issData = issData.replace(r"%%dir%%", os.getcwd()) + + with open("setup.iss", mode="w+") as outFile: + outFile.write(issData) + + try: + subprocess.call([innoSetup, "setup.iss"]) + except Exception as e: + print("Failed with error:") + print(str(e)) + sys.exit(1) From a5201d79cbbba375b824d3d11d00c97db63bb2c3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 01:46:50 +0200 Subject: [PATCH 069/104] Fix typo --- make_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make_windows.py b/make_windows.py index d5b938c0..674fb023 100755 --- a/make_windows.py +++ b/make_windows.py @@ -141,7 +141,7 @@ instOpt.append("novelWriter.py") # Make sample.zip first print("Building sample.zip") try: - subprocess.call([sys.executable, "setup.py" "sample"]) + subprocess.call([sys.executable, "setup.py", "sample"]) except Exception as e: print("Failed with error:") print(str(e)) From 3b63dd49dc4acf374c210f29b73e50b9cf09b272 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 13:03:32 +0200 Subject: [PATCH 070/104] Cleanup of the setup script --- docs/source/conf.py | 1 - setup.py | 93 +++++++++++++++++++++++++++++---------------- 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b72b8db3..a76fce4c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,7 +12,6 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os # import sys # sys.path.insert(0, os.path.abspath(".")) import os diff --git a/setup.py b/setup.py index ed17b904..403a71f2 100755 --- a/setup.py +++ b/setup.py @@ -4,27 +4,22 @@ import sys import subprocess import setuptools -## -# Build the Package -## +# =========================================================================== # +# Qt Assistant Documentation Builder +# =========================================================================== # -buildDocs = False -buildSample = False +def buildQtDocs(): + """This function will build the documentation as a Qt help file. The + file is then copied into the nw/assets/help directory and can be + included in builds. -if "qthelp" in sys.argv: - buildDocs = True - sys.argv.remove("qthelp") - -if "sample" in sys.argv: - buildSample = True - sys.argv.remove("sample") - -## -# Qt Assistant Documentation -## - -if buildDocs: + Depends on packages: + * pip install sphinx + * pip install sphinx-rtd-theme + * pip install sphinxcontrib-qthelp + It also requires the qhelpgenerator to be available on the system. + """ buildDir = os.path.join("docs", "build", "qthelp") helpDir = os.path.join("nw", "assets", "help") @@ -41,14 +36,14 @@ if buildDocs: try: subprocess.call(["make", "-C", "docs", "qthelp"]) except Exception as e: - print("Failed with error:") + print("QtHelp Build Error:") print(str(e)) buildFail = True try: subprocess.call(["qhelpgenerator", os.path.join(buildDir, inFile)]) except Exception as e: - print("Failed with error:") + print("QtHelp Build Error:") print(str(e)) buildFail = True @@ -56,7 +51,7 @@ if buildDocs: try: os.mkdir(helpDir) except Exception as e: - print("Failed with error:") + print("QtHelp Build Error:") print(str(e)) buildFail = True @@ -68,7 +63,7 @@ if buildDocs: os.rename(os.path.join(buildDir, outFile), os.path.join(helpDir, outFile)) os.rename(os.path.join(buildDir, datFile), os.path.join(helpDir, datFile)) except Exception as e: - print("Failed with error:") + print("QtHelp Build Error:") print(str(e)) buildFail = True @@ -80,11 +75,20 @@ if buildDocs: print("Documentation build: OK") print("") -## -# Sample Project ZIP file -## + return -if buildSample: +# =========================================================================== # +# Sample Project ZIP File Builder +# =========================================================================== # + +def buildSampleZip(): + """Bundle the sample project into a single zip file to be saved into + the nw/assets folder for further bundling into builds. + """ + print("") + print("Building Sample ZIP File") + print("========================") + print("") srcSample = "sample" dstSample = os.path.join("nw", "assets", "sample.zip") @@ -96,8 +100,10 @@ if buildSample: from zipfile import ZipFile with ZipFile(dstSample, "w") as zipObj: + print("Compressing: nwProject.nwx") zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx") for docFile in os.listdir(os.path.join(srcSample, "content")): + print("Compressing: content/%s" % docFile) srcDoc = os.path.join(srcSample, "content", docFile) zipObj.write(srcDoc, "content/"+docFile) @@ -105,12 +111,33 @@ if buildSample: print("Error: Could not find sample project source directory.") sys.exit(1) -if len(sys.argv) == 1: - # Nothing more to do - sys.exit(0) + print("") + print("Built file: %s" % dstSample) + print("") -## -# Build the Package -## + return -setuptools.setup() +# =========================================================================== # +# Process Jobs +# =========================================================================== # + +if __name__ == "__main__": + + # Process non-standard jobs + + if "qthelp" in sys.argv: + sys.argv.remove("qthelp") + buildQtDocs() + + if "sample" in sys.argv: + sys.argv.remove("sample") + buildSampleZip() + + if len(sys.argv) == 1: + # Nothing more to do + sys.exit(0) + + # Run the standard setup + setuptools.setup() + +# END Main From f0adc59d4cfcbe686f1c845ad2b2ad2463be5813 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 13:56:27 +0200 Subject: [PATCH 071/104] Rewritten make_windows.py script --- make_windows.py | 378 +++++++++++++++++++++++++++++++----------------- 1 file changed, 247 insertions(+), 131 deletions(-) diff --git a/make_windows.py b/make_windows.py index 674fb023..766846b3 100755 --- a/make_windows.py +++ b/make_windows.py @@ -16,72 +16,22 @@ novelWriter itself. import os import sys -import getopt +import shutil import subprocess -if not sys.platform.startswith("win32"): - print("ERROR: This script is intended for Windows only.") - sys.exit(1) +OS_NONE = 0 +OS_LINUX = 1 +OS_WIN = 2 +OS_DARWIN = 3 -# Defaults -buildWindowed = True -runPip = False -oneFile = False -makeSetup = False -innoSetup = None +# =============================================================================================== # +# Package Installer +# =============================================================================================== # -# Parse Options -shortOpt = "hd" -longOpt = [ - "help", - "debug", - "pip", - "onefile", - "setup", - "inno=", -] -helpMsg = ( - "\n" - "novelWriter Install Script for Windows\n" - "\n" - "Usage:\n" - " -h, --help Print this message.\n" - " --pip Install dependecies first.\n" - " --onefile Create a single executable file.\n" - " --setup Make Inno Setup file.\n" - " --inno= Path to the Inno Setup exec.\n" - " -d, --debug Build novelWriter for debugging. To debug novelwriter after build,\n" - " run it from command line with the debug options. Please check the\n" - " novelWriter --help output for details.\n" -) - -try: - inOpts, inArgs = getopt.getopt(sys.argv[1:], shortOpt, longOpt) -except getopt.GetoptError: - print(helpMsg) - sys.exit(1) - -for inOpt, inArg in inOpts: - if inOpt in ("-h", "--help"): - print(helpMsg) - sys.exit(0) - elif inOpt in ("-d", "--debug"): - buildWindowed = False - elif inOpt == "--pip": - runPip = True - elif inOpt == "--onefile": - oneFile = True - elif inOpt == "--setup": - makeSetup = True - elif inOpt == "--inno": - innoSetup = inArg - -# Run pip -if runPip: +def installPackages(): print("") - print("###########################") - print(" Installing Dependencies") - print("###########################") + print("Installing Dependencies") + print("#######################") print("") try: subprocess.call([ @@ -101,86 +51,131 @@ if runPip: print(str(e)) sys.exit(1) -# Run pyinstaller -print("") -print("#######################") -print(" Running PyInstaller") -print("#######################") -print("") -instOpt = [ - "--name=novelWriter", - "--clean", - "--add-data=%s;%s" % (os.path.join("nw", "assets"), "assets"), - "--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"), - "--exclude-module=PyQt5.QtQml", - "--exclude-module=PyQt5.QtBluetooth", - "--exclude-module=PyQt5.QtDBus", - "--exclude-module=PyQt5.QtMultimedia", - "--exclude-module=PyQt5.QtMultimediaWidgets", - "--exclude-module=PyQt5.QtNetwork", - "--exclude-module=PyQt5.QtNetworkAuth", - "--exclude-module=PyQt5.QtNfc", - "--exclude-module=PyQt5.QtQuick", - "--exclude-module=PyQt5.QtQuickWidgets", - "--exclude-module=PyQt5.QtRemoteObjects", - "--exclude-module=PyQt5.QtSensors", - "--exclude-module=PyQt5.QtSerialPort", - "--exclude-module=PyQt5.QtSql", -] + return -if buildWindowed: - instOpt.append("--windowed") +# =============================================================================================== # +# Run PyInstaller on Package +# =============================================================================================== # -if oneFile and not makeSetup: - instOpt.append("--onefile") -else: - instOpt.append("--onedir") +def freezePackage(buildWindowed, oneFile, makeSetup, hostOS): + """Run PyInstaller to freeze the packages. This assumes all + dependencies are already in place. + """ + import PyInstaller.__main__ # noqa: E402 -instOpt.append("novelWriter.py") + print("") + print("Running PyInstaller") + print("###################") + print("") -# Make sample.zip first -print("Building sample.zip") -try: - subprocess.call([sys.executable, "setup.py", "sample"]) -except Exception as e: - print("Failed with error:") - print(str(e)) - sys.exit(1) + if hostOS == OS_WIN: + dotDot = ";" + else: + dotDot = ":" -import PyInstaller.__main__ # noqa: E402 -PyInstaller.__main__.run(instOpt) - -if not oneFile: - # These dll files are not nee3ded, and take up a fair bit of space. - delIfExists = [ - "Qt5DBus.dll", "Qt5Network.dll", "Qt5Qml.dll", "Qt5QmlModels.dll", "Qt5Quick.dll", - "Qt5Quick3D.dll", "Qt5Quick3DAssetImport.dll", "Qt5Quick3DRender.dll", - "Qt5Quick3DRuntimeRender.dll", "Qt5Quick3DUtils.dll", "Qt5Sql.dll" + instOpt = [ + "--name=novelWriter", + "--clean", + "--add-data=%s%s%s" % (os.path.join("nw", "assets"), dotDot, "assets"), + "--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"), + "--exclude-module=PyQt5.QtQml", + "--exclude-module=PyQt5.QtBluetooth", + "--exclude-module=PyQt5.QtDBus", + "--exclude-module=PyQt5.QtMultimedia", + "--exclude-module=PyQt5.QtMultimediaWidgets", + "--exclude-module=PyQt5.QtNetwork", + "--exclude-module=PyQt5.QtNetworkAuth", + "--exclude-module=PyQt5.QtNfc", + "--exclude-module=PyQt5.QtQuick", + "--exclude-module=PyQt5.QtQuickWidgets", + "--exclude-module=PyQt5.QtRemoteObjects", + "--exclude-module=PyQt5.QtSensors", + "--exclude-module=PyQt5.QtSerialPort", + "--exclude-module=PyQt5.QtSql", ] - distDir = os.path.join(os.getcwd(), "dist", "novelWriter") - for delFile in delIfExists: - delPath = os.path.join(distDir, delFile) - if os.path.isfile(delPath): - print("Deleting file: %s" % delPath) - os.unlink(delPath) -print("") -print("Build Finished") -print("") -print("If everything went well, the novelWriter executable should be in the folder named 'dist'") -print("") + if buildWindowed: + instOpt.append("--windowed") + + if oneFile and not makeSetup: + instOpt.append("--onefile") + else: + instOpt.append("--onedir") + + instOpt.append("novelWriter.py") + + # Make sample.zip first + try: + subprocess.call([sys.executable, "setup.py", "sample"]) + except Exception as e: + print("Failed with error:") + print(str(e)) + sys.exit(1) + + PyInstaller.__main__.run(instOpt) + + if not oneFile: + # These files are not needed, and take up a fair bit of space. + delFiles = [] + if hostOS == OS_WIN: + delFiles = [ + "Qt5DBus.dll", + "Qt5Network.dll", + "Qt5Qml.dll", + "Qt5QmlModels.dll", + "Qt5Quick.dll", + "Qt5Quick3D.dll", + "Qt5Quick3DAssetImport.dll", + "Qt5Quick3DRender.dll", + "Qt5Quick3DRuntimeRender.dll", + "Qt5Quick3DUtils.dll", + "Qt5Sql.dll" + ] + elif hostOS == OS_LINUX: + delFiles = [ + "libQt5DBus.so.5", + "libQt5Network.so.5", + "libQt5Qml.so.5", + "libQt5QmlModels.so.5", + "libQt5Quick.so.5", + "libQt5Quick3D.so.5", + "libQt5Quick3DAssetImport.so.5", + "libQt5Quick3DRender.so.5", + "libQt5Quick3DRuntimeRender.so.5", + "libQt5Quick3DUtils.so.5", + "libQt5Sql.so.5" + ] + distDir = os.path.join(os.getcwd(), "dist", "novelWriter") + for delFile in delFiles: + delPath = os.path.join(distDir, delFile) + if os.path.isfile(delPath): + print("Deleting file: %s" % delPath) + os.unlink(delPath) -if makeSetup: print("") - print("######################") - print(" Running Inno Setup") - print("######################") + print("Build Finished") print("") - if innoSetup is None: - innoSetup = "C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe" - if not os.path.isfile(innoSetup): + print("The novelWriter executable should be in the folder named 'dist'") + print("") + + return + +# =============================================================================================== # +# Inno Setup Builder +# =============================================================================================== # + +def innoSetup(innoExec): + """Run the Inno Setup tool to build a setup.exe file for Windows. + """ + print("") + print("Running Inno Setup") + print("##################") + print("") + if innoExec is None: + innoExec = "C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe" + if not os.path.isfile(innoExec): print("ERROR: Cannot fine Inno Setup's ISCC.exe file.") - print(" Looked in: %s" % innoSetup) + print(" Looked in: %s" % innoExec) print(" Please provide a path with the --inno= option.") sys.exit(1) @@ -197,8 +192,129 @@ if makeSetup: outFile.write(issData) try: - subprocess.call([innoSetup, "setup.iss"]) + subprocess.call([innoExec, "setup.iss"]) except Exception as e: print("Failed with error:") print(str(e)) sys.exit(1) + + return + +# =============================================================================================== # +# Clean Build and Dist Folders +# =============================================================================================== # + +def cleanInstall(): + """Recursively delete the 'build' and 'dist' folders. + """ + print("") + print("Cleaning up build environment ...") + + buildDir = os.path.join(os.getcwd(), "build") + if os.path.isdir(buildDir): + try: + shutil.rmtree(buildDir) + print("Deleted folder 'build'") + except Exception as e: + print("Error: Cannot delete 'build' folder.") + print(str(e)) + sys.exit(1) + else: + print("Folder 'build' not found") + + distDir = os.path.join(os.getcwd(), "dist") + if os.path.isdir(distDir): + try: + shutil.rmtree(distDir) + print("Deleted folder 'dist'") + except Exception as e: + print("Error: Cannot delete 'dist' folder.") + print(str(e)) + sys.exit(1) + else: + print("Folder 'dist' not found") + + print("") + + return + +# =============================================================================================== # +# Process Build Steps +# =============================================================================================== # + +if __name__ == "__main__": + """Parse command line options and run the commands. + """ + # Detect OS + if sys.platform.startswith("linux"): + hostOS = OS_LINUX + elif sys.platform.startswith("darwin"): + hostOS = OS_DARWIN + elif sys.platform.startswith("win32"): + hostOS = OS_WIN + elif sys.platform.startswith("cygwin"): + hostOS = OS_WIN + else: + hostOS = OS_NONE + + # Flags and Variables + buildWindowed = True + oneFile = False + makeSetup = False + innoExec = None + + if "help" in sys.argv: + print( + "\n" + "novelWriter Make Tool\n" + "=====================\n" + "This tool provides build commands for distibuting novelWriter as\n" + "a package. The available options are as follows:\n" + "\n" + "pip Run pip to install all package dependencies for\n" + " novelWriter and this build tool.\n" + "onefile Build a standalone executable with all dependencies\n" + " bundled. This does not produce a setup.exe on Windows.\n" + "setup Build a setup.exe installer for Windows. This option\n" + " automaticall disables the 'onefile' option.\n" + "clean This will attempt to delete the 'build' and 'dist'\n" + " folders in the current folder.\n" + ) + sys.exit(0) + + if not os.path.isfile(os.path.join(os.getcwd(), "novelWriter.py")): + print("Error: This script must be run in the root folder of novelWriter.") + sys.exit(1) + + if not os.path.isdir(os.path.join(os.getcwd(), "nw")): + print("Error: This script must be run in the root folder of novelWriter.") + sys.exit(1) + + if "clean" in sys.argv: + sys.argv.remove("clean") + cleanInstall() + sys.exit(0) + + if "pip" in sys.argv: + sys.argv.remove("pip") + installPackages() + + if "onefile" in sys.argv: + sys.argv.remove("onefile") + oneFile = True + + if "setup" in sys.argv: + sys.argv.remove("setup") + if hostOS == OS_WIN: + oneFile = False + makeSetup = True + else: + print("Error: Argument 'setup' for Inno Setup is Windows only.") + sys.exit(1) + + freezePackage(buildWindowed, oneFile, makeSetup, hostOS) + + if makeSetup: + innoSetup(innoExec) + +# END Main From a20296cded774a572150f37ea9ab4bd013d7a5e7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 13:57:19 +0200 Subject: [PATCH 072/104] Rnamed make_windows.py to make.py, and deleted install.py --- install.py | 84 -------------------------------------- make_windows.py => make.py | 0 2 files changed, 84 deletions(-) delete mode 100755 install.py rename make_windows.py => make.py (100%) diff --git a/install.py b/install.py deleted file mode 100755 index 11754a26..00000000 --- a/install.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import os -import sys -import getopt -import subprocess - -# Defaults -buildWindowed = True - -# Parse Options -shortOpt = "hd" -longOpt = [ - "help", - "debug", -] -helpMsg = ( - "\n" - "novelWriter Install Script\n" - "\n" - "Usage:\n" - " -h, --help Print this message.\n" - " -d, --debug Build novelWriter for debugging. To debug novelwriter after build,\n" - " run it from command line with the debug options. Please check the\n" - " novelWriter --help output for details.\n" -) - -try: - inOpts, inArgs = getopt.getopt(sys.argv[1:], shortOpt, longOpt) -except getopt.GetoptError: - print(helpMsg) - sys.exit(2) - -for inOpt, inArg in inOpts: - if inOpt in ("-h", "--help"): - print(helpMsg) - sys.exit(0) - elif inOpt in ("-d", "--debug"): - buildWindowed = False - -# Run pip -packList = ["pyinstaller"] -with open("requirements.txt", mode="r") as reqFile: - for reqPack in reqFile: - if len(reqPack.strip()) > 0: - packList.append(reqPack) - -for packName in packList: - print("Installing package dependency: %s" % packName) - try: - subprocess.call([sys.executable, "-m", "pip", "install", packName]) - except Exception as e: - print("Failed with error:") - print(str(e)) - -# Run pyinstaller -if sys.platform.startswith("win32"): - dotDot = ";" -else: - dotDot = ":" - -instOpt = [ - "--name=novelWriter", - "--clean", - "--onefile", - "--add-data=%s%s%s" % (os.path.join("nw", "assets"), dotDot, "assets"), - "--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"), -] -if buildWindowed: - instOpt.append("--windowed") - -instOpt.append("novelWriter.py") - -import PyInstaller.__main__ # noqa: E402 -PyInstaller.__main__.run(instOpt) - -print("") -print("##################") -print(" Build Finished") -print("##################") -print("") -print("If everything went well, the novelWriter executable should be in the folder named 'dist'") -print("") diff --git a/make_windows.py b/make.py similarity index 100% rename from make_windows.py rename to make.py From db2eb98c274e36a545e962f35c199cd1aa69df1f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 14:09:32 +0200 Subject: [PATCH 073/104] Minor changes to the windows inno setup --- make.py | 20 ++++++-------------- setup/win_setup.iss | 2 +- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/make.py b/make.py index 766846b3..6162a313 100755 --- a/make.py +++ b/make.py @@ -164,20 +164,13 @@ def freezePackage(buildWindowed, oneFile, makeSetup, hostOS): # Inno Setup Builder # =============================================================================================== # -def innoSetup(innoExec): +def innoSetup(): """Run the Inno Setup tool to build a setup.exe file for Windows. """ print("") print("Running Inno Setup") print("##################") print("") - if innoExec is None: - innoExec = "C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe" - if not os.path.isfile(innoExec): - print("ERROR: Cannot fine Inno Setup's ISCC.exe file.") - print(" Looked in: %s" % innoExec) - print(" Please provide a path with the --inno= option.") - sys.exit(1) # Read the iss template issData = "" @@ -192,9 +185,9 @@ def innoSetup(innoExec): outFile.write(issData) try: - subprocess.call([innoExec, "setup.iss"]) + subprocess.call(["iscc", "setup.iss"]) except Exception as e: - print("Failed with error:") + print("Inno Setup failed with error:") print(str(e)) sys.exit(1) @@ -261,7 +254,6 @@ if __name__ == "__main__": buildWindowed = True oneFile = False makeSetup = False - innoExec = None if "help" in sys.argv: print( @@ -271,10 +263,10 @@ if __name__ == "__main__": "This tool provides build commands for distibuting novelWriter as\n" "a package. The available options are as follows:\n" "\n" - "pip Run pip to install all package dependencies for\n" - " novelWriter and this build tool.\n" "onefile Build a standalone executable with all dependencies\n" " bundled. This does not produce a setup.exe on Windows.\n" + "pip Run pip to install all package dependencies for\n" + " novelWriter and this build tool.\n" "setup Build a setup.exe installer for Windows. This option\n" " automaticall disables the 'onefile' option.\n" "clean This will attempt to delete the 'build' and 'dist'\n" @@ -315,6 +307,6 @@ if __name__ == "__main__": freezePackage(buildWindowed, oneFile, makeSetup, hostOS) if makeSetup: - innoSetup(innoExec) + innoSetup() # END Main diff --git a/setup/win_setup.iss b/setup/win_setup.iss index 6491b317..83f016b6 100644 --- a/setup/win_setup.iss +++ b/setup/win_setup.iss @@ -27,7 +27,7 @@ UsedUserAreasWarning=no ;PrivilegesRequired=lowest PrivilegesRequiredOverridesAllowed=dialog OutputDir={#nwAppDir} -OutputBaseFilename=setup-novelwriter-{#nwAppVersion} +OutputBaseFilename=novelwriter_{#nwAppVersion}_win10_full_setup Compression=lzma SolidCompression=yes WizardStyle=modern From 614572ca80e70a6602979c646465b91b64b7a2ea Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 15:05:58 +0200 Subject: [PATCH 074/104] Minor modifications to make.py, and made Inno Setyp 64 bit --- make.py | 47 ++++++++++++++++++++++++++++----------------- setup/win_setup.iss | 3 ++- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/make.py b/make.py index 6162a313..7a0923b9 100755 --- a/make.py +++ b/make.py @@ -254,24 +254,29 @@ if __name__ == "__main__": buildWindowed = True oneFile = False makeSetup = False + doFreeze = False - if "help" in sys.argv: - print( - "\n" - "novelWriter Make Tool\n" - "=====================\n" - "This tool provides build commands for distibuting novelWriter as\n" - "a package. The available options are as follows:\n" - "\n" - "onefile Build a standalone executable with all dependencies\n" - " bundled. This does not produce a setup.exe on Windows.\n" - "pip Run pip to install all package dependencies for\n" - " novelWriter and this build tool.\n" - "setup Build a setup.exe installer for Windows. This option\n" - " automaticall disables the 'onefile' option.\n" - "clean This will attempt to delete the 'build' and 'dist'\n" - " folders in the current folder.\n" - ) + helpMsg = ( + "\n" + "novelWriter Make Tool\n" + "=====================\n" + "This tool provides build commands for distibuting novelWriter as a\n" + "package. The available options are as follows:\n" + "\n" + "freeze Freeze the package and produces a folder of all\n" + " dependecies using pyinstaller.\n" + "onefile Build a standalone executable with all dependencies\n" + " bundled. Implies 'freeze', cannot be used with 'setup'.\n" + "pip Run pip to install all package dependencies for\n" + " novelWriter and this build tool.\n" + "setup Build a setup.exe installer for Windows. This option\n" + " automaticall disables the 'onefile' option.\n" + "clean This will attempt to delete the 'build' and 'dist'\n" + " folders in the current folder.\n" + ) + + if "help" in sys.argv or len(sys.argv) <= 1: + print(helpMsg) sys.exit(0) if not os.path.isfile(os.path.join(os.getcwd(), "novelWriter.py")): @@ -291,8 +296,13 @@ if __name__ == "__main__": sys.argv.remove("pip") installPackages() + if "freeze" in sys.argv: + sys.argv.remove("freeze") + doFreeze = True + if "onefile" in sys.argv: sys.argv.remove("onefile") + doFreeze = True oneFile = True if "setup" in sys.argv: @@ -304,7 +314,8 @@ if __name__ == "__main__": print("Error: Argument 'setup' for Inno Setup is Windows only.") sys.exit(1) - freezePackage(buildWindowed, oneFile, makeSetup, hostOS) + if doFreeze: + freezePackage(buildWindowed, oneFile, makeSetup, hostOS) if makeSetup: innoSetup() diff --git a/setup/win_setup.iss b/setup/win_setup.iss index 83f016b6..8b6be609 100644 --- a/setup/win_setup.iss +++ b/setup/win_setup.iss @@ -27,10 +27,11 @@ UsedUserAreasWarning=no ;PrivilegesRequired=lowest PrivilegesRequiredOverridesAllowed=dialog OutputDir={#nwAppDir} -OutputBaseFilename=novelwriter_{#nwAppVersion}_win10_full_setup +OutputBaseFilename=novelwriter_{#nwAppVersion}_win_amd64_setup Compression=lzma SolidCompression=yes WizardStyle=modern +ArchitecturesInstallIn64BitMode=x64 [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" From 3378b387cfdc5d7f2feec98ae881c86abdb2f004 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 15:52:40 +0200 Subject: [PATCH 075/104] Reduce size of freeze build and added more documentation --- README.md | 2 ++ make.py | 50 +++++++++++++++++++++++++++++--------------------- setup/BUILD.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 21 deletions(-) create mode 100644 setup/BUILD.md diff --git a/README.md b/README.md index 3b7835ed..be86e0f4 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,8 @@ for debugging. You can also provide a path to a folder containing a novelWriter project as the last parameter. +For more install options, see [Build and Install novelWriter](setup/BUILD.md). + ### Launcher and Icons diff --git a/make.py b/make.py index 7a0923b9..15ed18ae 100755 --- a/make.py +++ b/make.py @@ -28,28 +28,29 @@ OS_DARWIN = 3 # Package Installer # =============================================================================================== # -def installPackages(): +def installPackages(hostOS): + """Install package dependencies both for this script and for running + novelWriter itself. + """ print("") print("Installing Dependencies") print("#######################") print("") - try: - subprocess.call([ - sys.executable, "-m", - "pip", "install", "--user", "--upgrade", "pip" - ]) - subprocess.call([ - sys.executable, "-m", - "pip", "install", "--user", "--upgrade", "pyinstaller" - ]) - subprocess.call([ - sys.executable, "-m", - "pip", "install", "--user", "--upgrade", "-r", "requirements.txt" - ]) - except Exception as e: - print("Failed with error:") - print(str(e)) - sys.exit(1) + + installQueue = ["pip", "pyinstaller", "-r requirements.txt"] + if hostOS == OS_DARWIN: + installQueue.append("pyobjc") + + pyCmd = [sys.executable, "-m"] + pipCmd = ["pip", "install", "--user", "--upgrade"] + for stepCmd in installQueue: + pkgCmd = stepCmd.split(" ") + try: + subprocess.call(pyCmd + pipCmd + pkgCmd) + except Exception as e: + print("Failed with error:") + print(str(e)) + sys.exit(1) return @@ -73,6 +74,7 @@ def freezePackage(buildWindowed, oneFile, makeSetup, hostOS): else: dotDot = ":" + sys.modules["FixTk"] = None instOpt = [ "--name=novelWriter", "--clean", @@ -92,6 +94,12 @@ def freezePackage(buildWindowed, oneFile, makeSetup, hostOS): "--exclude-module=PyQt5.QtSensors", "--exclude-module=PyQt5.QtSerialPort", "--exclude-module=PyQt5.QtSql", + "--exclude-module=FixTk", + "--exclude-module=tcl", + "--exclude-module=tk", + "--exclude-module=_tkinter", + "--exclude-module=tkinter", + "--exclude-module=Tkinter", ] if buildWindowed: @@ -263,8 +271,9 @@ if __name__ == "__main__": "This tool provides build commands for distibuting novelWriter as a\n" "package. The available options are as follows:\n" "\n" + "help Print the help message.\n" "freeze Freeze the package and produces a folder of all\n" - " dependecies using pyinstaller.\n" + " dependencies using pyinstaller.\n" "onefile Build a standalone executable with all dependencies\n" " bundled. Implies 'freeze', cannot be used with 'setup'.\n" "pip Run pip to install all package dependencies for\n" @@ -290,11 +299,10 @@ if __name__ == "__main__": if "clean" in sys.argv: sys.argv.remove("clean") cleanInstall() - sys.exit(0) if "pip" in sys.argv: sys.argv.remove("pip") - installPackages() + installPackages(hostOS) if "freeze" in sys.argv: sys.argv.remove("freeze") diff --git a/setup/BUILD.md b/setup/BUILD.md new file mode 100644 index 00000000..d97e702a --- /dev/null +++ b/setup/BUILD.md @@ -0,0 +1,44 @@ +# Build and Install novelWriter + +The root folder of the repository contains two scripts for setup and install: + +## Script `setup.py` + +The `setup.py` is a standard Python setup script with a couple of additional options: + +* `qthelp`: Will attempt to build a single file QtAssistand documentation file. + This requires the Qt tools to be installed on the local system, as well as the sphinx build tools + for the documentation. +* `sample`: Will create a `sample.zip` file in the `nw/assets` folder. + This is the file the New Project Wizard uses to generate an example project. + If novelWriter is run from source, this file is not needed. + +To install novelWriter as a local Python package, run: +```bash +sudo python setup.py install +``` + +## Script `make.py` + +The `make.py` script provides a number of convenient options for building packages if novelWriter. + +Usage: +```bash +python make.py [command] +``` + +It currently accept the following commands: + +* `help`: Print the help message. +* `freeze`: Freeze the package and produces a folder of all dependencies using pyinstaller. +* `onefile`: Build a standalone executable with all dependencies bundled. + Implies `freeze`, cannot be used with `setup`. +* `pip`: Run pip to install all package dependencies for novelWriter and this build tool. +* `setup`: Build a setup.exe installer for Windows. + This option automaticall disables the `onefile` option. +* `clean`: This will attempt to delete the `build` and `dist` folders in the current folder. + +For instance, to create a Windows installer, run: +```bash +python make.py freeze setup +``` From 139293b21cfb340c9a70cca279e80021fc35765f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 16:58:47 +0200 Subject: [PATCH 076/104] Added a launcher option to setup.py, and updated documentation --- README.md | 47 +++++---- setup.py | 181 +++++++++++++++++++++++++++++++++-- setup/BUILD.md | 7 +- setup/installDebianUbuntu.sh | 42 -------- 4 files changed, 209 insertions(+), 68 deletions(-) delete mode 100755 setup/installDebianUbuntu.sh diff --git a/README.md b/README.md index be86e0f4..f8d19eb6 100644 --- a/README.md +++ b/README.md @@ -65,30 +65,42 @@ You can update novelWriter to the latest version by running: pip install --upgrade novelwriter ``` -The application can then be started with one of the commands, depending on your Python configuration: -```bash -./novelWriter.py -python novelWriter.py -python3 novelWriter.py -``` - It also takes a few parameters for debugging and such, which can be listed with the switch `--help`. The `--info`, `--debug` or `--verbose` flags are particularly useful for increasing logging output for debugging. You can also provide a path to a folder containing a novelWriter project as the last parameter. +## Installing from Source (Linux) + +You can then install novelWriter from the source directly with: +```bash +python3 setup.py sample +sudo python3 setup.py install +sudo python3 setup.py launcher +``` + +The last line will install the application icons and set up a launcher for novelWriter. +The method uses hardcoded paths, so it may or may not work for your Linux distro. + +It may prompt you to choose which executable to configure. +You can also use this to configure it to run from source. + +## Running from Source (Linux) + +If you want to run directly from the source, the application can be started with: +```bash +./novelWriter.py +``` + +You can also create a launcher for the source with: +```bash +sudo python3 setup.py launcher +``` + For more install options, see [Build and Install novelWriter](setup/BUILD.md). -### Launcher and Icons - -In the root setup folder there are icons and scripts and a template for setting up a launcher on -Gnome desktops. You may need to modify those scripts slightly, but as they are, they work on Debian -and Ubuntu. For other operating systems, please consult your operating system documentation for how -to make those. Feel free to submit more if you are able to make them. - - ## Package Dependencies It is recommended that novelWriter runs with Qt 5.10 or later, and requires Python 3.6 or later. @@ -99,7 +111,7 @@ Minimum version of Qt is 5.2. Generally, dependencies can be installed via `pip` with: ```bash -pip3 install -r requirements.txt +pip install -r requirements.txt ``` You can also install the packages from the distro's own package manager. @@ -144,6 +156,9 @@ It should look something like this: C:\...\AppData\Local\Programs\Python\Python38\python.exe novelWriter.py ``` +You can also run the `make.py` script to generate an installer. +See [Build and Install novelWriter](setup/BUILD.md) for more details. + ### Package Versions diff --git a/setup.py b/setup.py index 403a71f2..4683e3f2 100755 --- a/setup.py +++ b/setup.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 import os import sys +import shutil import subprocess import setuptools -# =========================================================================== # +# =============================================================================================== # # Qt Assistant Documentation Builder -# =========================================================================== # +# =============================================================================================== # def buildQtDocs(): """This function will build the documentation as a Qt help file. The @@ -77,9 +78,9 @@ def buildQtDocs(): return -# =========================================================================== # +# =============================================================================================== # # Sample Project ZIP File Builder -# =========================================================================== # +# =============================================================================================== # def buildSampleZip(): """Bundle the sample project into a single zip file to be saved into @@ -117,13 +118,173 @@ def buildSampleZip(): return -# =========================================================================== # +# =============================================================================================== # +# Create Launcher +# =============================================================================================== # + +def makeLauncherLinux(): + """Will attempt to install icons and make a launcher. + """ + print("") + print("Creating Launcher") + print("=================") + print("") + + exOpts = [] + + testExec = shutil.which("novelWriter") + if testExec is not None: + exOpts.append(testExec) + + testExec = shutil.which("novelwriter") + if testExec is not None: + exOpts.append(testExec) + + testExec = os.path.join(os.getcwd(), "novelWriter.py") + if os.path.isfile(testExec): + exOpts.append(testExec) + + useExec = "" + nOpts = len(exOpts) + if nOpts == 0: + print("Error: No executables for novelWriter found.") + sys.exit(1) + elif nOpts == 1: + useExec = exOpts[0] + else: + print("Found multiple novelWriter executables:") + print("") + for iExec, anExec in enumerate(exOpts): + print(" [%d] %s" % (iExec, anExec)) + print("") + intVal = int(input("Please select which novelWriter executable to use: ")) + print("") + + if intVal >= 0 and intVal < nOpts: + useExec = exOpts[intVal] + else: + print("Error: Invalid selection.") + sys.exit(1) + + print("Using executable: %s " % useExec) + + # Read the Template + desktopData = "" + with open(os.path.join("setup", "novelwriter.desktop"), mode="r") as inFile: + desktopData = inFile.read() + + desktopData = desktopData.replace(r"%%exec%%", useExec) + + desktopFile = "/usr/share/applications/novelwriter.desktop" + try: + with open(desktopFile, mode="w+") as outFile: + outFile.write(desktopData) + print("Wrote file: %s" % desktopFile) + except Exception as e: + print("Error: Could not write novelwriter.desktop file.") + print(str(e)) + sys.exit(1) + + print("") + + # Copy Icons + + iconDirs = [ + "/usr/share/icons/hicolor/24x24/apps", + "/usr/share/icons/hicolor/48x48/apps", + "/usr/share/icons/hicolor/96x96/apps", + "/usr/share/icons/hicolor/256x256/apps", + "/usr/share/icons/hicolor/512x512/apps", + "/usr/share/icons/hicolor/scalable/apps", + "/usr/share/icons/hicolor/scalable/mimetypes", + ] + for iconDir in iconDirs: + if not os.path.isdir: + try: + os.mkdir(iconDir) + print("Created folder: %s" % iconDir) + except Exception as e: + print("Error: Could not make folder: %s" % iconDir) + print(str(e)) + + copyList = [( + "setup/icons/24x24/novelwriter.png", + "/usr/share/icons/hicolor/24x24/apps/novelwriter.png" + ), ( + "setup/icons/48x48/novelwriter.png", + "/usr/share/icons/hicolor/48x48/apps/novelwriter.png" + ), ( + "setup/icons/96x96/novelwriter.png", + "/usr/share/icons/hicolor/96x96/apps/novelwriter.png" + ), ( + "setup/icons/256x256/novelwriter.png", + "/usr/share/icons/hicolor/256x256/apps/novelwriter.png" + ), ( + "setup/icons/512x512/novelwriter.png", + "/usr/share/icons/hicolor/512x512/apps/novelwriter.png" + ), ( + "setup/icons/novelwriter.svg", + "/usr/share/icons/hicolor/scalable/apps/novelwriter.svg" + ), ( + "setup/icons/x-novelwriter-project.svg", + "/usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg" + ), ( + "setup/mime/x-novelwriter-project.xml", + "/usr/share/mime/packages/x-novelwriter-project.xml" + )] + for srcFile, dstFile in copyList: + try: + shutil.copyfile(srcFile, dstFile) + print("Copied file to: %s" % dstFile) + except Exception as e: + print("Error: Could not copy file: %s" % srcFile) + print(str(e)) + + print("") + + # Update System + try: + subprocess.call(["update-mime-database", "/usr/share/mime/"]) + print("Updated mime database.") + except Exception as e: + print("Error: Filed to update mime database.") + print(str(e)) + + try: + subprocess.call(["update-icon-caches", "/usr/share/icons/*"]) + print("Updated icon cache.") + except Exception as e: + print("Error: Filed to update icon cache.") + print(str(e)) + + print("") + print("Done!") + print("") + + return + +# =============================================================================================== # # Process Jobs -# =========================================================================== # +# =============================================================================================== # if __name__ == "__main__": - # Process non-standard jobs + helpMsg = ( + "\n" + "novelWriter Setup Tool\n" + "======================\n" + "This tool provides some additional setup commands for novelWriter.\n" + "\n" + "help Print the help message.\n" + "gthelp Build the help documentation for use with the QtAssistant.\n" + "sample Build the sample project as a zip file.\n" + "launcher Install launcher icons for freedesktop systems.\n" + ) + + if "help" in sys.argv: + sys.argv.remove("help") + print(helpMsg) + sys.exit(0) if "qthelp" in sys.argv: sys.argv.remove("qthelp") @@ -133,7 +294,11 @@ if __name__ == "__main__": sys.argv.remove("sample") buildSampleZip() - if len(sys.argv) == 1: + if "launcher" in sys.argv: + sys.argv.remove("launcher") + makeLauncherLinux() + + if len(sys.argv) <= 1: # Nothing more to do sys.exit(0) diff --git a/setup/BUILD.md b/setup/BUILD.md index d97e702a..81479cf7 100644 --- a/setup/BUILD.md +++ b/setup/BUILD.md @@ -2,6 +2,7 @@ The root folder of the repository contains two scripts for setup and install: + ## Script `setup.py` The `setup.py` is a standard Python setup script with a couple of additional options: @@ -10,8 +11,10 @@ The `setup.py` is a standard Python setup script with a couple of additional opt This requires the Qt tools to be installed on the local system, as well as the sphinx build tools for the documentation. * `sample`: Will create a `sample.zip` file in the `nw/assets` folder. - This is the file the New Project Wizard uses to generate an example project. - If novelWriter is run from source, this file is not needed. + This is the file the New Project Wizard uses to generate an example project. + If novelWriter is run from source, this file is not needed. +* `launcher`: Will try to copy the novelWriter icons and create a novelWriter.desktop file to launch + the application. This should work on standard Linux desktops. To install novelWriter as a local Python package, run: ```bash diff --git a/setup/installDebianUbuntu.sh b/setup/installDebianUbuntu.sh deleted file mode 100755 index c47f4d26..00000000 --- a/setup/installDebianUbuntu.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -cd .. - -EXEC=$(pwd)/novelWriter.py -EXEC=$(echo $EXEC | sed 's_/_\\/_g') - -sed "s/%%exec%%/$EXEC/g" setup/novelwriter.desktop > /usr/share/applications/novelwriter.desktop - -if [ ! -d /usr/share/icons/hicolor/24x24/apps ]; then - mkdir -pv /usr/share/icons/hicolor/24x24/apps -fi -if [ ! -d /usr/share/icons/hicolor/48x48/apps ]; then - mkdir -pv /usr/share/icons/hicolor/48x48/apps -fi -if [ ! -d /usr/share/icons/hicolor/96x96/apps ]; then - mkdir -pv /usr/share/icons/hicolor/96x96/apps -fi -if [ ! -d /usr/share/icons/hicolor/256x256/apps ]; then - mkdir -pv /usr/share/icons/hicolor/256x256/apps -fi -if [ ! -d /usr/share/icons/hicolor/512x512/apps ]; then - mkdir -pv /usr/share/icons/hicolor/512x512/apps -fi -if [ ! -d /usr/share/icons/hicolor/scalable/apps ]; then - mkdir -pv /usr/share/icons/hicolor/scalable/apps -fi -if [ ! -d /usr/share/icons/hicolor/scalable/mimetypes ]; then - mkdir -pv /usr/share/icons/hicolor/scalable/mimetypes -fi - -cp -v setup/icons/24x24/novelwriter.png /usr/share/icons/hicolor/24x24/apps/ -cp -v setup/icons/48x48/novelwriter.png /usr/share/icons/hicolor/48x48/apps/ -cp -v setup/icons/96x96/novelwriter.png /usr/share/icons/hicolor/96x96/apps/ -cp -v setup/icons/256x256/novelwriter.png /usr/share/icons/hicolor/256x256/apps/ -cp -v setup/icons/512x512/novelwriter.png /usr/share/icons/hicolor/512x512/apps/ -cp -v setup/icons/novelwriter.svg /usr/share/icons/hicolor/scalable/apps/ -cp -v setup/icons/x-novelwriter-project.svg /usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg -cp -v setup/mime/x-novelwriter-project.xml /usr/share/mime/packages/ - -update-mime-database /usr/share/mime/ -update-icon-caches /usr/share/icons/* From 2ffca7db48c8f115dac9de07d96c691528d7c0b1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 17:26:04 +0200 Subject: [PATCH 077/104] Updated and cleaned up the readme --- README.md | 84 +++++++++++++++++++++++++++---------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index f8d19eb6..ce5cfdf0 100644 --- a/README.md +++ b/README.md @@ -52,28 +52,37 @@ in principle work fine on other operating systems as well as long as dependencie tests are run on the latest versions of Ubuntu Linux, Windows Server and macOS. -## Installing and Running +# Installing and Running -You can runt novelWriter either from a downloaded copy of the source code, or by running: +novelWriter is available on [pypi.org](https://pypi.org/project/novelWriter/), and can be installed with: ```bash pip install novelwriter ``` -**Note:** On some systems you must use `pip3` instead for the Python 3 version. -You can update novelWriter to the latest version by running: +To upgrade an existing installation, use: ```bash pip install --upgrade novelwriter ``` -It also takes a few parameters for debugging and such, which can be listed with the switch `--help`. -The `--info`, `--debug` or `--verbose` flags are particularly useful for increasing logging output -for debugging. +Dependencies are installed automatically, but can generally be installed with: +```bash +pip install -r requirements.txt +``` -You can also provide a path to a folder containing a novelWriter project as the last parameter. +Below are some brief instructions on how to get started on different operating systems. -## Installing from Source (Linux) -You can then install novelWriter from the source directly with: +## Linux + +Either download the source, or install with pip. + +If you run from source, install the dependencies via pip, or directly from the OS repo. +There are very few dependencies, and they should be available in the standard repo. +The Python packages needed are `pyqt5`, `lxml` and `pyenchant`. + +### Installing from Source + +You can also install novelWriter from source with: ```bash python3 setup.py sample sudo python3 setup.py install @@ -82,18 +91,18 @@ sudo python3 setup.py launcher The last line will install the application icons and set up a launcher for novelWriter. The method uses hardcoded paths, so it may or may not work for your Linux distro. +If you have any issues, please submit a ticket so the script can be tuned. -It may prompt you to choose which executable to configure. -You can also use this to configure it to run from source. +The script may prompt you to choose which executable to configure if it finds more than one. -## Running from Source (Linux) +### Running from Source If you want to run directly from the source, the application can be started with: ```bash ./novelWriter.py ``` -You can also create a launcher for the source with: +You can also create a launcher for running directly from source with: ```bash sudo python3 setup.py launcher ``` @@ -101,28 +110,7 @@ sudo python3 setup.py launcher For more install options, see [Build and Install novelWriter](setup/BUILD.md). -## Package Dependencies - -It is recommended that novelWriter runs with Qt 5.10 or later, and requires Python 3.6 or later. -Minimum version of Qt is 5.2. - - -### Linux - -Generally, dependencies can be installed via `pip` with: -```bash -pip install -r requirements.txt -``` - -You can also install the packages from the distro's own package manager. -For the apt package manager on Debian/Ubuntu systems, the following Python3 packages are needed: - -* `python3-pyqt5` for the GUI -* `python3-lxml` for writing project files -* `python3-enchant` for better spell checking (optional) - - -### macOS +## macOS These instructions assume you're using brew, and have Python and pip set up. If not, see the [brew docs](https://docs.brew.sh/Homebrew-and-Python) for help. @@ -140,11 +128,16 @@ It comes with a lot of default dictionaries. brew install enchant ``` - ### Windows -On Windows, the `pip install` command is generally sufficient to install everything you need. -That should also install the Qt libraries and the spell check dictionary dependencies. +On Windows, you may first need to install Python. +See the [python.org](https://www.python.org/) website for download packages. +It is recommended that you install the latest version of Python 3.8. + +To install dependencies, run: +```bash +pip install --user -r requirements.txt +``` **Note:** On Windows, make sure Python3 is in your PATH if you want to launch novelWriter from command line. You can also right click the `novelWriter.py` file, create a shortcut, then right @@ -156,11 +149,11 @@ It should look something like this: C:\...\AppData\Local\Programs\Python\Python38\python.exe novelWriter.py ``` -You can also run the `make.py` script to generate an installer. +You can also run the `make.py` script to generate a single executable, or an installer. See [Build and Install novelWriter](setup/BUILD.md) for more details. -### Package Versions +## Package Versions Exporting to Markdown requires PyQt/Qt 5.14. There are no known minimum for `lxml`, but the code was originally written with 4.2. The optional spell check library must be at least 3.0.0 to work @@ -172,8 +165,15 @@ checker, but more can be added to the `nw/assets/dict` folder. See the [README]( file in that folder for how to generate more dictionaries. Note that the difflib-based option is both slow and limited. +## Debugging -## Key Features +If you need to debug novelWriter, you must run it from command line. +It takes a few parameters, which can be listed with the switch `--help`. +The `--info`, `--debug` or `--verbose` flags are particularly useful for increasing logging output +for debugging. + + +# Key Features Some features of novelWriter are listed below. Consult the documentation for more information. From 95db7faf8a74001ef71e20069eacbde2e7f17119 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 17:32:07 +0200 Subject: [PATCH 078/104] Fixed wrong heading type --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce5cfdf0..41587d20 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ It comes with a lot of default dictionaries. brew install enchant ``` -### Windows +## Windows On Windows, you may first need to install Python. See the [python.org](https://www.python.org/) website for download packages. From 3045275ab52163bf0b58d87cd3b1b2e90586a9c0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 17:45:34 +0200 Subject: [PATCH 079/104] Updated the main docstrings in the setup and make scripts --- make.py | 18 ++++++++++-------- setup.py | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/make.py b/make.py index 15ed18ae..859ad8ca 100755 --- a/make.py +++ b/make.py @@ -1,15 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -This script will either build: - * A single file executable named dist/novelWriter.exe. This is a quite - slow option, and the file is fairly big. Option --onefile - * A single directory named dist/novelWriter with a novelWriter.exe, and - all dependecies included. This is the default. - * The latter can be combined with a build stage of a setup.exe file - named setup-novelwriter-.exe. Option --setup. +This make script is intended for building distributable packages of +novelWriter. These are either: -In addition, providing the --pip flag will cause the script to try to + * A single file executable named dist/novelWriter(.exe). This is a + quite slow option, and the file is fairly big. + * A single directory named dist/novelWriter with a novelWriter(.exe), + and all dependecies included. + * The latter can be combined with a build stage of a setup.exe file if + on Windows. This requires Inno Setup to be installed and in path. + +In addition, providing the pip otion will cause the script to try to install all dependencies needed for runing the build, and for running novelWriter itself. """ diff --git a/setup.py b/setup.py index 4683e3f2..c3c5710b 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,22 @@ #!/usr/bin/env python3 +""" +The main setup script for novelWeiter. + +It runs the standard setuptool.setup() with all options taken from the +setup.cfg file. + +In addtion, a few speicalised commands are available: + + * sample: Will build a sample.zip file, which is the way the sample project is + included into distributable packages. + * qthelp: Will build a QtAssistant readable version of the novelWriter + documentation. This should also be a part of distributed packages. It allows + for reading the help offline. Otherwise, the F1 button redirects to the + online documentation only. + * launcher: Will attempt to install novelWriter icons, mime type and create a + launcher for the application. + +""" import os import sys import shutil @@ -276,7 +294,7 @@ if __name__ == "__main__": "This tool provides some additional setup commands for novelWriter.\n" "\n" "help Print the help message.\n" - "gthelp Build the help documentation for use with the QtAssistant.\n" + "qthelp Build the help documentation for use with the QtAssistant.\n" "sample Build the sample project as a zip file.\n" "launcher Install launcher icons for freedesktop systems.\n" ) From 4584a4b8b44f443d6e0d24c19b66506512bc17f8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 23:35:33 +0200 Subject: [PATCH 080/104] Renamed setup/BUILD.md to setup/README.md --- README.md | 4 ++-- setup/{BUILD.md => README.md} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename setup/{BUILD.md => README.md} (100%) diff --git a/README.md b/README.md index 41587d20..9efa0b24 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ You can also create a launcher for running directly from source with: sudo python3 setup.py launcher ``` -For more install options, see [Build and Install novelWriter](setup/BUILD.md). +For more install options, see [Build and Install novelWriter](setup/README.md). ## macOS @@ -150,7 +150,7 @@ C:\...\AppData\Local\Programs\Python\Python38\python.exe novelWriter.py ``` You can also run the `make.py` script to generate a single executable, or an installer. -See [Build and Install novelWriter](setup/BUILD.md) for more details. +See [Build and Install novelWriter](setup/README.md) for more details. ## Package Versions diff --git a/setup/BUILD.md b/setup/README.md similarity index 100% rename from setup/BUILD.md rename to setup/README.md From 50cc135dde7e9a14e017235ca6318a7e478df7a8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 17 Oct 2020 23:46:54 +0200 Subject: [PATCH 081/104] Fixed lots of top-of-file docstrings that were out of date --- nw/constants/iso.py | 6 +++--- nw/core/spellcheck.py | 4 ++-- nw/core/tools.py | 15 ++++++++------- nw/error.py | 2 +- nw/gui/custom.py | 8 ++++---- nw/gui/outlinedetails.py | 8 ++++---- nw/gui/preferences.py | 8 ++++---- nw/gui/projload.py | 2 +- nw/gui/projtree.py | 8 ++++---- nw/gui/theme.py | 8 ++++---- 10 files changed, 35 insertions(+), 34 deletions(-) diff --git a/nw/constants/iso.py b/nw/constants/iso.py index 69bd6d23..ccdc0808 100644 --- a/nw/constants/iso.py +++ b/nw/constants/iso.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -"""novelWriter Language Codes +"""novelWriter ISO Codes - novelWriter – Language Codes -============================== + novelWriter – ISO Codes +========================= Handles translating language codes to language names File History: diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index ff338f62..e3093070 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -"""novelWriter Spell Check Wrapper +"""novelWriter Spell Check Classes - novelWriter – Spell Check Wrapper + novelWriter – Spell Check Classes =================================== Wrapper class for spell checking diff --git a/nw/core/tools.py b/nw/core/tools.py index 8062d591..ef9fbc6b 100644 --- a/nw/core/tools.py +++ b/nw/core/tools.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- -"""novelWriter Word Counter +"""novelWriter Various Tools - novelWriter – Word Counter -============================ - Simple word counter + novelWriter – Various Tools +============================= + Various core tool functions File History: - Created: 2019-04-22 [0.0.1] countWords - Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN - Merged: 2020-05-08 [0.4.5] All of the above into this file + Created: 2019-04-22 [0.0.1] countWords + Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN + Merged: 2020-05-08 [0.4.5] All of the above into this file + Created: 2020-07-05 [0.10.0] numberToRoman This file is a part of novelWriter Copyright 2018–2020, Veronica Berglyd Olsen diff --git a/nw/error.py b/nw/error.py index a8f3c149..988b49e6 100644 --- a/nw/error.py +++ b/nw/error.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""novelWriter Init +"""novelWriter Exception Handling novelWriter – Exception Handling ================================== diff --git a/nw/gui/custom.py b/nw/gui/custom.py index 2955c447..a6260f33 100644 --- a/nw/gui/custom.py +++ b/nw/gui/custom.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -"""novelWriter Addition QConfigLayout +"""novelWriter Custom Widgets and Layouts - novelWriter – Addition QConfigLayout -====================================== - A custom Qt grid layout for config forms similar to QFormLayout + novelWriter – Custom Widgets and Layouts +========================================== + Various custom widget and layout classes File History: Created: 2020-05-03 [0.4.5] QConfigLayout diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py index 4f6a56f5..18f2b90f 100644 --- a/nw/gui/outlinedetails.py +++ b/nw/gui/outlinedetails.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -"""novelWriter GUI Project Outline +"""novelWriter GUI Project Outline Details - novelWriter – GUI Project Outline -=================================== - Class holding the project outline view + novelWriter – GUI Project Outline Details +=========================================== + Class holding the project outline details view File History: Created: 2020-06-02 [0.7.0] diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index 2a1cf653..7b8e0905 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -"""novelWriter GUI Config Editor +"""novelWriter GUI Preferences - novelWriter – GUI Config Editor -================================= - Class holding the config dialog + novelWriter – GUI Preferences +=============================== + Class holding the preferences dialog File History: Created: 2019-06-10 [0.1.5] diff --git a/nw/gui/projload.py b/nw/gui/projload.py index 8f18f7bb..29c9c9ab 100644 --- a/nw/gui/projload.py +++ b/nw/gui/projload.py @@ -3,7 +3,7 @@ novelWriter – GUI Open Project ================================ - New and open project dialog + The open project dialog File History: Created: 2020-02-26 [0.4.5] diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 6b1ee7bd..77fdf610 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -"""novelWriter GUI Document Tree +"""novelWriter GUI Project Tree - novelWriter – GUI Document Tree -================================= - Class holding the left side document tree view + novelWriter – GUI project Tree +================================ + Class holding the left side project tree view File History: Created: 2018-09-29 [0.0.1] GuiProjectTree diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 5683d1a3..9d907a67 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -"""novelWriter Theme Class +"""novelWriter Theme and Icons Classes - novelWriter – Theme Class -=========================== - This class reads and store the main theme + novelWriter – Theme and Icons Classs +====================================== + This class reads and stores the themes and the icons File History: Created: 2019-05-18 [0.1.3] GuiTheme From b54527e9a56b0f0e03bc65dba17a139d16acdf70 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 18 Oct 2020 11:53:44 +0200 Subject: [PATCH 082/104] Updated changelog with latest changes --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0293f81..5f5e9615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,11 @@ * When applying a format from the format menu to a selection of multiple paragraphs (or lines), only the first paragraph (or line) receives the formatting. The editor doesn't allow markdown formatting to span multiple lines. Issue #451, PR #475. * The syntax highlighter no longer uses the same colour to highlight strikethrough text as for emphasised text. The colour is intended to stand out, which makes little sense for such text. Instead, the highlighter uses the same colour as for comments. PR #476. +**Other Changes** + +* Since support for Python < 3.6 has been dropped, it is now possible to use `f""` formatted strings in many more places in the source code where this is convenient. This has been implemented many places, but the code is still a mix of all three styles of formatting text. PR #478. +* Extensive changes have been made to the build and distribute tools. The `install.py` file has been dropped, and the features in it merged into a new file named `make.py`. The make file can now also build a setup installer for Windows. The `setup.py` file has been rewritten to a more standardised source layout, and all the setup configuration moved to the `setup.cfg` file. PRs #479 and #480. + ## Version 1.0 Beta 4 [2020-10-11] From 947f5b79debd09f217565e5746f07cef2339e5b1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 18 Oct 2020 11:58:44 +0200 Subject: [PATCH 083/104] This will be beta 5 release instead --- CHANGELOG.md | 4 ++-- docs/source/conf.py | 2 +- nw/__init__.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f5e9615..801417bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,12 @@ # novelWriter ChangeLog -## Version 1.0 Release Candidate 1 [2020-10-25] +## Version 1.0 Release Beta 5 [2020-10-18] **Important Notes** * The minimal supported Python version is now 3.6. While novelWriter has worked fine in the post with versions as low as 3.4, neither 3.4 nor 3.5 is tested. They have also both reached end of life. There are a couple of good reasons to drop support for older versions. PR #470. 1. Python 3.6 introduces ordered dictionaries as the standard. - 2. The format string attribute was added in 3.6, and is much less clunky in many parts of the code than the full `"".format()` syntax. + 2. The format string decorator (`f""`) was added in 3.6, and is much less clunky in many parts of the code than the full `"".format()` syntax. 3. Especially 3.4 has limited support for `*var` expansion of iterables. These are used several places in the code. **Bugfixes** diff --git a/docs/source/conf.py b/docs/source/conf.py index c0f62bfc..e8607fed 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ author = "Veronica Berglyd Olsen" # The short X.Y version version = "1.0" # The full version, including alpha/beta/rc tags -release = "1.0-rc1" +release = "1.0-beta5" # -- General configuration --------------------------------------------------- diff --git a/nw/__init__.py b/nw/__init__.py index 516b471d..d4597c17 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -61,9 +61,9 @@ __package__ = "nw" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen" __license__ = "GPLv3" -__version__ = "1.0rc1" -__hexversion__ = "0x010000c1" -__date__ = "2020-10-25" +__version__ = "1.0b5" +__hexversion__ = "0x010000b5" +__date__ = "2020-10-18" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" __status__ = "Beta" From 8e222c1e675cc4b263f1f4f569907788a82ee10f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 18 Oct 2020 19:25:12 +0200 Subject: [PATCH 084/104] Change typewriter mode to scroll at any point --- CHANGELOG.md | 2 +- nw/gui/doceditor.py | 47 ++++++++++++++++++++++++++++----------------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 801417bc..d93b0044 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # novelWriter ChangeLog -## Version 1.0 Release Beta 5 [2020-10-18] +## Version 1.0 Beta 5 [2020-10-18] **Important Notes** diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 6d431319..12b038ca 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -61,6 +61,11 @@ logger = logging.getLogger(__name__) class GuiDocEditor(QTextEdit): + MOVE_KEYS = ( + Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down, + Qt.Key_PageUp, Qt.Key_PageDown + ) + def __init__(self, theParent): QTextEdit.__init__(self, theParent) @@ -765,34 +770,40 @@ class GuiDocEditor(QTextEdit): return elif keyEvent == QKeySequence.Redo: self.docAction(nwDocAction.REDO) + return elif keyEvent == QKeySequence.Undo: self.docAction(nwDocAction.UNDO) + return elif keyEvent == QKeySequence.SelectAll: self.docAction(nwDocAction.SEL_ALL) - else: - QTextEdit.keyPressEvent(self, keyEvent) - self.docFooter.updateLineCount() + return if self.mainConf.scollWithCursor: + + cOld = self.cursorRect().center().y() + QTextEdit.keyPressEvent(self, keyEvent) + kMod = keyEvent.modifiers() - if kMod == Qt.NoModifier or kMod == Qt.ShiftModifier: - hWid = self.viewport().height() - cPos = self.cursorRect().center().y() - mPos = self.mainConf.scollToPoint * hWid - vBar = self.verticalScrollBar() - - # Compute the needed scroll and duration - pOld = vBar.value() - pNew = pOld + cPos - round(mPos*0.01) - aDur = 150 + round(min(abs(pNew - pOld)/hWid, 1.0)*500) - - if pNew >= 0: + okMod = kMod == Qt.NoModifier or kMod == Qt.ShiftModifier + okKey = keyEvent.key() not in self.MOVE_KEYS + if okMod and okKey: + cNew = self.cursorRect().center().y() + cMov = cNew - cOld + mPos = self.mainConf.scollToPoint * self.viewport().height() * 0.01 + if abs(cMov) > 0 and cOld > mPos: + # Move the scroll bar + vBar = self.verticalScrollBar() doAnim = QPropertyAnimation(vBar, b"value", self) - doAnim.setDuration(aDur) - doAnim.setStartValue(pOld) - doAnim.setEndValue(pNew) + doAnim.setDuration(150) + doAnim.setStartValue(vBar.value()) + doAnim.setEndValue(vBar.value() + cMov) doAnim.start() + else: + QTextEdit.keyPressEvent(self, keyEvent) + + self.docFooter.updateLineCount() + return def focusNextPrevChild(self, toNext): From dc4257bbf402b8828356f2a1c8b09ef82755a768 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 18 Oct 2020 19:37:32 +0200 Subject: [PATCH 085/104] Renamed the typewriter config settings --- nw/config.py | 41 +++++++++++++++----------- nw/gui/doceditor.py | 4 +-- nw/gui/preferences.py | 28 +++++++++--------- tests/reference/novelwriter.conf | 4 +-- tests/reference/novelwriter_prefs.conf | 4 +-- tests/test_dialogs.py | 6 ++-- tests/test_gui.py | 4 +-- 7 files changed, 48 insertions(+), 43 deletions(-) diff --git a/nw/config.py b/nw/config.py index 675fad3d..993d657c 100644 --- a/nw/config.py +++ b/nw/config.py @@ -127,8 +127,8 @@ class Config: self.doReplaceDash = True self.doReplaceDots = True self.scrollPastEnd = True - self.scollWithCursor = False - self.scollToPoint = 40 + self.autoScroll = False + self.autoScrollPos = 30 self.wordCountTimer = 5.0 self.showTabsNSpaces = False @@ -465,11 +465,11 @@ class Config: self.scrollPastEnd = self._parseLine( cnfParse, cnfSec, "scrollpastend", self.CNF_BOOL, self.scrollPastEnd ) - self.scollWithCursor = self._parseLine( - cnfParse, cnfSec, "scollwithcursor", self.CNF_BOOL, self.scollWithCursor + self.autoScroll = self._parseLine( + cnfParse, cnfSec, "autoscroll", self.CNF_BOOL, self.autoScroll ) - self.scollToPoint = self._parseLine( - cnfParse, cnfSec, "scolltopoint", self.CNF_INT, self.scollToPoint + self.autoScrollPos = self._parseLine( + cnfParse, cnfSec, "autoscrollpos", self.CNF_INT, self.autoScrollPos ) self.fmtSingleQuotes = self._parseLine( cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes @@ -616,8 +616,8 @@ class Config: cnfParse.set(cnfSec, "repdash", str(self.doReplaceDash)) cnfParse.set(cnfSec, "repdots", str(self.doReplaceDots)) cnfParse.set(cnfSec, "scrollpastend", str(self.scrollPastEnd)) - cnfParse.set(cnfSec, "scollwithcursor", str(self.scollWithCursor)) - cnfParse.set(cnfSec, "scolltopoint", str(self.scollToPoint)) + cnfParse.set(cnfSec, "autoscroll", str(self.autoScroll)) + cnfParse.set(cnfSec, "autoscrollpos", str(self.autoScrollPos)) cnfParse.set(cnfSec, "fmtsinglequote", self._packList(self.fmtSingleQuotes)) cnfParse.set(cnfSec, "fmtdoublequote", self._packList(self.fmtDoubleQuotes)) cnfParse.set(cnfSec, "spelltool", str(self.spellTool)) @@ -911,16 +911,21 @@ class Config: """ if cnfParse.has_section(cnfSec): if cnfParse.has_option(cnfSec, cnfName): - if cnfType == self.CNF_STR: - return cnfParse.get(cnfSec, cnfName) - elif cnfType == self.CNF_INT: - return cnfParse.getint(cnfSec, cnfName) - elif cnfType == self.CNF_BOOL: - return cnfParse.getboolean(cnfSec, cnfName) - elif cnfType == self.CNF_LIST: - return self._unpackList( - cnfParse.get(cnfSec, cnfName), len(cnfDefault), cnfDefault - ) + try: + if cnfType == self.CNF_STR: + return cnfParse.get(cnfSec, cnfName) + elif cnfType == self.CNF_INT: + return cnfParse.getint(cnfSec, cnfName) + elif cnfType == self.CNF_BOOL: + return cnfParse.getboolean(cnfSec, cnfName) + elif cnfType == self.CNF_LIST: + return self._unpackList( + cnfParse.get(cnfSec, cnfName), len(cnfDefault), cnfDefault + ) + except ValueError as e: + logger.error("Failed to load value from config file.") + logger.error(str(e)) + return cnfDefault def _checkNone(self, checkVal): diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 12b038ca..c6f0a0f4 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -778,7 +778,7 @@ class GuiDocEditor(QTextEdit): self.docAction(nwDocAction.SEL_ALL) return - if self.mainConf.scollWithCursor: + if self.mainConf.autoScroll: cOld = self.cursorRect().center().y() QTextEdit.keyPressEvent(self, keyEvent) @@ -789,7 +789,7 @@ class GuiDocEditor(QTextEdit): if okMod and okKey: cNew = self.cursorRect().center().y() cMov = cNew - cOld - mPos = self.mainConf.scollToPoint * self.viewport().height() * 0.01 + mPos = self.mainConf.autoScrollPos * self.viewport().height() * 0.01 if abs(cMov) > 0 and cOld > mPos: # Move the scroll bar vBar = self.verticalScrollBar() diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index 7b8e0905..945e35f6 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -569,23 +569,23 @@ class GuiConfigEditLayoutTab(QWidget): ) ## Typewriter Scrolling - self.scollWithCursor = QSwitch() - self.scollWithCursor.setChecked(self.mainConf.scollWithCursor) + self.autoScroll = QSwitch() + self.autoScroll.setChecked(self.mainConf.autoScroll) self.mainForm.addRow( "Typewriter style scrolling when you type", - self.scollWithCursor, + self.autoScroll, "Tries to keep the cursor at a fixed vertical position." ) ## Font Size - self.scollToPoint = QSpinBox(self) - self.scollToPoint.setMinimum(10) - self.scollToPoint.setMaximum(90) - self.scollToPoint.setSingleStep(1) - self.scollToPoint.setValue(self.mainConf.scollToPoint) + self.autoScrollPos = QSpinBox(self) + self.autoScrollPos.setMinimum(10) + self.autoScrollPos.setMaximum(90) + self.autoScrollPos.setSingleStep(1) + self.autoScrollPos.setValue(int(self.mainConf.autoScrollPos)) self.mainForm.addRow( - "Position in the editor to keep the cursor", - self.scollToPoint, + "Minimum position for Typewriter scrolling", + self.autoScrollPos, "In units of percentage of the editor height.", theUnit = "%" ) @@ -608,8 +608,8 @@ class GuiConfigEditLayoutTab(QWidget): textMargin = self.textMargin.value() tabWidth = self.tabWidth.value() scrollPastEnd = self.scrollPastEnd.isChecked() - scollWithCursor = self.scollWithCursor.isChecked() - scollToPoint = self.scollToPoint.value() + autoScroll = self.autoScroll.isChecked() + autoScrollPos = self.autoScrollPos.value() self.mainConf.textFont = textFont self.mainConf.textSize = textSize @@ -621,8 +621,8 @@ class GuiConfigEditLayoutTab(QWidget): self.mainConf.textMargin = textMargin self.mainConf.tabWidth = tabWidth self.mainConf.scrollPastEnd = scrollPastEnd - self.mainConf.scollWithCursor = scollWithCursor - self.mainConf.scollToPoint = scollToPoint + self.mainConf.autoScroll = autoScroll + self.mainConf.autoScrollPos = autoScrollPos self.mainConf.confChanged = True diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf index bb141482..aef7459f 100644 --- a/tests/reference/novelwriter.conf +++ b/tests/reference/novelwriter.conf @@ -40,8 +40,8 @@ repdquotes = True repdash = True repdots = True scrollpastend = True -scollwithcursor = False -scolltopoint = 40 +autoscroll = False +autoscrollpos = 30 fmtsinglequote = ‘, ’ fmtdoublequote = “, ” spelltool = internal diff --git a/tests/reference/novelwriter_prefs.conf b/tests/reference/novelwriter_prefs.conf index ee591782..831bbf08 100644 --- a/tests/reference/novelwriter_prefs.conf +++ b/tests/reference/novelwriter_prefs.conf @@ -40,8 +40,8 @@ repdquotes = True repdash = True repdots = True scrollpastend = False -scollwithcursor = True -scolltopoint = 40 +autoscroll = True +autoscrollpos = 30 fmtsinglequote = ‘, ’ fmtdoublequote = “, ” spelltool = internal diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index ddc77738..abe14b64 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -1127,9 +1127,9 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC assert not tabLayout.scrollPastEnd.isChecked() qtbot.wait(keyDelay) - assert not tabLayout.scollWithCursor.isChecked() - qtbot.mouseClick(tabLayout.scollWithCursor, Qt.LeftButton) - assert tabLayout.scollWithCursor.isChecked() + assert not tabLayout.autoScroll.isChecked() + qtbot.mouseClick(tabLayout.autoScroll, Qt.LeftButton) + assert tabLayout.autoScroll.isChecked() # Editor Settings qtbot.wait(keyDelay) diff --git a/tests/test_gui.py b/tests/test_gui.py index ea7b54ec..0d9849d6 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -172,8 +172,8 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI.mainConf.hideHScroll = True nwGUI.mainConf.hideVScroll = True nwGUI.mainConf.scrollPastEnd = True - nwGUI.mainConf.scollToPoint = 80 - nwGUI.mainConf.scollWithCursor = True + nwGUI.mainConf.autoScrollPos = 80 + nwGUI.mainConf.autoScroll = True # Add a Character File nwGUI.setFocus(1) From eed349ab85d6a151c3f1b1aa333f02efe21b3c1f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 18 Oct 2020 19:56:24 +0200 Subject: [PATCH 086/104] Added more comments and tweaked the scroll features --- nw/config.py | 62 +++++++++++++++++++++++++-------------------- nw/gui/doceditor.py | 5 ++-- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/nw/config.py b/nw/config.py index 993d657c..65fd4d8a 100644 --- a/nw/config.py +++ b/nw/config.py @@ -107,44 +107,52 @@ class Config: self.hideHScroll = False # Hide horizontal scroll bars on main widgets ## Project - self.autoSaveProj = 60 - self.autoSaveDoc = 30 + self.autoSaveProj = 60 # Interval for auto-saving project in seconds + self.autoSaveDoc = 30 # Interval for auto-saving document in seconds ## Text Editor - self.textFont = None - self.textSize = 12 - self.textFixedW = True - self.textWidth = 600 - self.textMargin = 40 - self.tabWidth = 40 - self.focusWidth = 800 - self.hideFocusFooter = False - self.doJustify = False - self.autoSelect = True - self.doReplace = True - self.doReplaceSQuote = True - self.doReplaceDQuote = True - self.doReplaceDash = True - self.doReplaceDots = True - self.scrollPastEnd = True - self.autoScroll = False - self.autoScrollPos = 30 + self.textFont = None # Editor font + self.textSize = 12 # Editor font size + self.textFixedW = True # Keep editor text fixed width + self.textWidth = 600 # Editor text width + self.textMargin = 40 # Editor/viewer text margin + self.tabWidth = 40 # Editor tabulator width - self.wordCountTimer = 5.0 - self.showTabsNSpaces = False - self.showLineEndings = False - self.bigDocLimit = 800 - self.showFullPath = True - self.highlightQuotes = True - self.highlightEmph = True + self.focusWidth = 800 # Focus Mode text width + self.hideFocusFooter = False # Hide document footer in Focus Mode + self.showFullPath = True # Show full document path in editor header + self.autoSelect = True # Auto-select word when applying format with no selection + self.doJustify = False # Justify text + self.showTabsNSpaces = False # Show tabs and spaces in edior + self.showLineEndings = False # Show line endings in editor + + self.doReplace = True # Enable auto-replace as you type + self.doReplaceSQuote = True # Smart single quotes + self.doReplaceDQuote = True # Smart double quotes + self.doReplaceDash = True # Replace multiple hyphens with dashes + self.doReplaceDots = True # Replace three dots with ellipsis + + self.scrollPastEnd = True # Allow scrolling past end of document + self.autoScroll = False # Typewriter-like scrolling + self.autoScrollPos = 30 # Start point for typewriter-like scrolling + + self.wordCountTimer = 5.0 # Interval for word count update in seconds + self.bigDocLimit = 800 # Size threshold for heavy editor features in kilobytes + + self.highlightQuotes = True # Highlight text in quotes + self.highlightEmph = True # Add colour to text emphasis + + ## User-Selected Symbols self.fmtApostrophe = nwUnicode.U_RSQUO self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO] self.fmtDoubleQuotes = [nwUnicode.U_LDQUO, nwUnicode.U_RDQUO] + ## Spell Checking self.spellTool = None self.spellLanguage = None + ## Search Bar Switches self.searchCase = False self.searchWord = False self.searchRegEx = False diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index c6f0a0f4..9a2cb683 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -441,8 +441,7 @@ class GuiDocEditor(QTextEdit): if self.mainConf.scrollPastEnd: docFrame = self.qDocument.rootFrame().frameFormat() - docMargin = wH - uM - lM - 4*tB - 5*self.theTheme.fontPixelSize - docFrame.setBottomMargin(max(0, docMargin)) + docFrame.setBottomMargin(max(0, 0.6*(wH - uM - lM - 4*tB))) self.qDocument.rootFrame().setFrameFormat(docFrame) return @@ -794,7 +793,7 @@ class GuiDocEditor(QTextEdit): # Move the scroll bar vBar = self.verticalScrollBar() doAnim = QPropertyAnimation(vBar, b"value", self) - doAnim.setDuration(150) + doAnim.setDuration(120) doAnim.setStartValue(vBar.value()) doAnim.setEndValue(vBar.value() + cMov) doAnim.start() From f69c37fe91ca5060060cdf684a40547c3c83fbf6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 19 Oct 2020 23:46:15 +0200 Subject: [PATCH 087/104] Cleanup of document viewer stylesheet --- nw/gui/docviewer.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 39a769b3..0c9a8086 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -501,49 +501,45 @@ class GuiDocViewer(QTextBrowser): """ styleSheet = ( "body {{" - " color: rgb({tColR},{tColG},{tColB});" - " font-size: {textSize:.1f}pt;" + " color: rgb({tColR}, {tColG}, {tColB});" "}}\n" "h1, h2, h3, h4 {{" - " color: rgb({hColR},{hColG},{hColB});" + " color: rgb({hColR}, {hColG}, {hColB});" "}}\n" "a {{" - " color: rgb({aColR},{aColG},{aColB});" + " color: rgb({aColR}, {aColG}, {aColB});" "}}\n" "mark {{" - " color: rgb({eColR},{eColG},{eColB});" + " color: rgb({eColR}, {eColG}, {eColB});" "}}\n" ".tags {{" - " color: rgb({kColR},{kColG},{kColB});" - " font-wright: bold;" + " color: rgb({kColR}, {kColG}, {kColB});" "}}\n" ".comment {{" - " color: rgb({cColR},{cColG},{cColB});" + " color: rgb({cColR}, {cColG}, {cColB});" "}}\n" ".synopsis {{" - " color: rgb({mColR},{mColG},{mColB});" - " font-wright: bold;" + " color: rgb({mColR}, {mColG}, {mColB});" "}}\n" ).format( - textSize = self.mainConf.textSize, tColR = self.theTheme.colText[0], tColG = self.theTheme.colText[1], tColB = self.theTheme.colText[2], hColR = self.theTheme.colHead[0], hColG = self.theTheme.colHead[1], hColB = self.theTheme.colHead[2], - cColR = self.theTheme.colHidden[0], - cColG = self.theTheme.colHidden[1], - cColB = self.theTheme.colHidden[2], - eColR = self.theTheme.colEmph[0], - eColG = self.theTheme.colEmph[1], - eColB = self.theTheme.colEmph[2], aColR = self.theTheme.colVal[0], aColG = self.theTheme.colVal[1], aColB = self.theTheme.colVal[2], + eColR = self.theTheme.colEmph[0], + eColG = self.theTheme.colEmph[1], + eColB = self.theTheme.colEmph[2], kColR = self.theTheme.colKey[0], kColG = self.theTheme.colKey[1], kColB = self.theTheme.colKey[2], + cColR = self.theTheme.colHidden[0], + cColG = self.theTheme.colHidden[1], + cColB = self.theTheme.colHidden[2], mColR = self.theTheme.colMod[0], mColG = self.theTheme.colMod[1], mColB = self.theTheme.colMod[2], From 37c5ecc75551bd76d56679780e4c7de8e59d5bd1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 21 Oct 2020 20:05:54 +0200 Subject: [PATCH 088/104] Skip intermediate steps when loading/saving json files, and always use 2 space indent --- nw/config.py | 5 ++--- nw/core/index.py | 15 +++++---------- nw/core/options.py | 5 ++--- nw/core/project.py | 2 +- nw/core/tree.py | 2 +- nw/gui/writingstats.py | 2 +- 6 files changed, 12 insertions(+), 19 deletions(-) diff --git a/nw/config.py b/nw/config.py index 65fd4d8a..bc777812 100644 --- a/nw/config.py +++ b/nw/config.py @@ -690,8 +690,7 @@ class Config: if os.path.isfile(cacheFile): try: with open(cacheFile, mode="r", encoding="utf8") as inFile: - theJson = inFile.read() - theData = json.loads(theJson) + theData = json.load(inFile) for projPath in theData.keys(): theEntry = theData[projPath] @@ -729,7 +728,7 @@ class Config: try: with open(cacheTemp, mode="w+", encoding="utf8") as outFile: - outFile.write(json.dumps(self.recentProj, indent=2)) + json.dump(self.recentProj, outFile, indent=2) except Exception as e: self.hasError = True self.errData.append("Could not save recent project cache") diff --git a/nw/core/index.py b/nw/core/index.py index c08562e1..97ad7ba4 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -159,8 +159,7 @@ class NWIndex(): logger.debug("Loading index file") try: with open(indexFile, mode="r", encoding="utf8") as inFile: - theJson = inFile.read() - theData = json.loads(theJson) + theData = json.load(inFile) except Exception as e: logger.error("Failed to load index file") logger.error(str(e)) @@ -190,23 +189,18 @@ class NWIndex(): """Save the current index as a json file in the project meta data folder. """ - indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) - logger.debug("Saving index file") - if self.mainConf.debugInfo: - nIndent = 2 - else: - nIndent = None + indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) try: with open(indexFile, mode="w+", encoding="utf8") as outFile: - outFile.write(json.dumps({ + json.dump({ "tagIndex" : self.tagIndex, "refIndex" : self.refIndex, "novelIndex" : self.novelIndex, "noteIndex" : self.noteIndex, "textCounts" : self.textCounts, - }, indent=nIndent)) + }, outFile, indent=2) except Exception as e: logger.error("Failed to save index file") logger.error(str(e)) @@ -218,6 +212,7 @@ class NWIndex(): """Check that the entries in the index are valid and contain the elements it should. """ + logger.debug("Checking index") self.indexBroken = False try: diff --git a/nw/core/options.py b/nw/core/options.py index e808f74c..35358fc0 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -109,8 +109,7 @@ class OptionState(): logger.debug("Loading GUI options file") try: with open(stateFile, mode="r", encoding="utf8") as inFile: - theJson = inFile.read() - theState = json.loads(theJson) + theState = json.load(inFile) except Exception as e: logger.error("Failed to load GUI options file") logger.error(str(e)) @@ -137,7 +136,7 @@ class OptionState(): try: with open(stateFile, mode="w+", encoding="utf8") as outFile: - outFile.write(json.dumps(self.theState, indent=2)) + json.dump(self.theState, outFile, indent=2) except Exception as e: logger.error("Failed to save GUI options file") logger.error(str(e)) diff --git a/nw/core/project.py b/nw/core/project.py index 622bcc38..1bf53151 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1024,7 +1024,7 @@ class NWProject(): return True def setTreeOrder(self, newOrder): - """A list representing the liner/flattened order of project + """A list representing the linear/flattened order of project items in the GUI project tree. The user can rearrange the order by drag-and-drop. Forwarded to the NWTree class. """ diff --git a/nw/core/tree.py b/nw/core/tree.py index b43230a9..f33b0d59 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -177,7 +177,7 @@ class NWTree(): # Dump the JSON with open(tocJson, mode="w+", encoding="utf8") as outFile: - outFile.write(json.dumps(jsonData, indent=2)) + json.dump(jsonData, outFile, indent=2) except Exception as e: logger.error(str(e)) diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index ccf1ce18..fa03a278 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -362,7 +362,7 @@ class GuiWritingStats(QDialog): "novelWords": wA, "noteWords": wB, }) - outFile.write(json.dumps(jsonData, indent=2)) + json.dump(jsonData, outFile, indent=2) wSuccess = True elif dataFmt == self.FMT_CSV: From 20e44ad7aeb9107f63cbed9ada955777de135840 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 21 Oct 2020 20:06:08 +0200 Subject: [PATCH 089/104] Update tests --- tests/test_dialogs.py | 10 +++++----- tests/test_index.py | 5 ++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index abe14b64..e223adae 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -266,7 +266,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: - jsonData = json.loads(inFile.read()) + jsonData = json.load(inFile) qtbot.wait(stepDelay) @@ -301,7 +301,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: - jsonData = json.loads(inFile.read()) + jsonData = json.load(inFile) assert len(jsonData) == 2 assert jsonData[1]["length"] >= 14.0 @@ -318,7 +318,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: - jsonData = json.loads(inFile.read()) + jsonData = json.load(inFile) assert len(jsonData) == 3 @@ -331,7 +331,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: - jsonData = json.loads(inFile.read()) + jsonData = json.load(inFile) assert len(jsonData) == 4 @@ -343,7 +343,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: - jsonData = json.loads(inFile.read()) + jsonData = json.load(inFile) # Check against both 1 and 2 as this can be 2 if test was started just before midnight. # A failed test should in any case produce a 4 diff --git a/tests/test_index.py b/tests/test_index.py index a86fb017..9fabae5b 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -27,7 +27,6 @@ def testIndexBuildCheck(monkeypatch, nwLipsum, nwDummy, nwTempProj, nwRef): theProject.projTree.setSeed(42) assert theProject.openProject(nwLipsum) - theProject.mainConf.debugInfo = True monkeypatch.setattr("nw.core.index.time", lambda: 123.4) theIndex = NWIndex(theProject, nwDummy) @@ -49,7 +48,7 @@ def testIndexBuildCheck(monkeypatch, nwLipsum, nwDummy, nwTempProj, nwRef): raise Exception # Make the save fail - monkeypatch.setattr(json, "dumps", doPanic) + monkeypatch.setattr(json, "dump", doPanic) assert not theIndex.saveIndex() # Make the save pass @@ -83,7 +82,7 @@ def testIndexBuildCheck(monkeypatch, nwLipsum, nwDummy, nwTempProj, nwRef): assert not theIndex.textCounts # Make the load fail - monkeypatch.setattr(json, "loads", doPanic) + monkeypatch.setattr(json, "load", doPanic) assert not theIndex.loadIndex() # Make the load pass From eca28698940be2ceb58f39ed20b00f54dab2dc51 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 22 Oct 2020 22:25:44 +0200 Subject: [PATCH 090/104] Install launcher and icons the right way --- .gitignore | 1 + setup.py | 174 +++++++++--------- .../icon-novelwriter-1024.png} | Bin .../icon-novelwriter-128.png} | Bin .../icon-novelwriter-16.png} | Bin .../icon-novelwriter-16@2.png} | Bin .../icon-novelwriter-18@2.png} | Bin setup/icons/scaled/icon-novelwriter-22.png | Bin 0 -> 985 bytes .../icon-novelwriter-24.png} | Bin .../icon-novelwriter-256.png} | Bin .../icon-novelwriter-32.png} | Bin .../icon-novelwriter-48.png} | Bin .../icon-novelwriter-512.png} | Bin .../icon-novelwriter-64.png} | Bin .../icon-novelwriter-96.png} | Bin .../icon-novelwriter32@2.png} | Bin .../mime-novelwriter-1024.png} | Bin .../mime-novelwriter-128.png} | Bin .../mime-novelwriter-16.png} | Bin .../mime-novelwriter-16@2.png} | Bin .../mime-novelwriter-18@2.png} | Bin setup/icons/scaled/mime-novelwriter-22.png | Bin 0 -> 986 bytes .../mime-novelwriter-24.png} | Bin .../mime-novelwriter-256.png} | Bin .../mime-novelwriter-32.png} | Bin .../mime-novelwriter-32@2.png} | Bin .../mime-novelwriter-48.png} | Bin .../mime-novelwriter-512.png} | Bin .../mime-novelwriter-64.png} | Bin .../mime-novelwriter-96.png} | Bin 30 files changed, 91 insertions(+), 84 deletions(-) rename setup/icons/{1024x1024/novelwriter.png => scaled/icon-novelwriter-1024.png} (100%) rename setup/icons/{128x128/novelwriter.png => scaled/icon-novelwriter-128.png} (100%) rename setup/icons/{16x16/novelwriter.png => scaled/icon-novelwriter-16.png} (100%) rename setup/icons/{16x16@2x/novelwriter.png => scaled/icon-novelwriter-16@2.png} (100%) rename setup/icons/{18x18@2x/novelwriter.png => scaled/icon-novelwriter-18@2.png} (100%) create mode 100644 setup/icons/scaled/icon-novelwriter-22.png rename setup/icons/{24x24/novelwriter.png => scaled/icon-novelwriter-24.png} (100%) rename setup/icons/{256x256/novelwriter.png => scaled/icon-novelwriter-256.png} (100%) rename setup/icons/{32x32/novelwriter.png => scaled/icon-novelwriter-32.png} (100%) rename setup/icons/{48x48/novelwriter.png => scaled/icon-novelwriter-48.png} (100%) rename setup/icons/{512x512/novelwriter.png => scaled/icon-novelwriter-512.png} (100%) rename setup/icons/{32x32@2x/novelwriter.png => scaled/icon-novelwriter-64.png} (100%) rename setup/icons/{96x96/novelwriter.png => scaled/icon-novelwriter-96.png} (100%) rename setup/icons/{64x64/novelwriter.png => scaled/icon-novelwriter32@2.png} (100%) rename setup/icons/{1024x1024/x-novelwriter-project.png => scaled/mime-novelwriter-1024.png} (100%) rename setup/icons/{128x128/x-novelwriter-project.png => scaled/mime-novelwriter-128.png} (100%) rename setup/icons/{16x16/x-novelwriter-project.png => scaled/mime-novelwriter-16.png} (100%) rename setup/icons/{16x16@2x/x-novelwriter-project.png => scaled/mime-novelwriter-16@2.png} (100%) rename setup/icons/{18x18@2x/x-novelwriter-project.png => scaled/mime-novelwriter-18@2.png} (100%) create mode 100644 setup/icons/scaled/mime-novelwriter-22.png rename setup/icons/{24x24/x-novelwriter-project.png => scaled/mime-novelwriter-24.png} (100%) rename setup/icons/{256x256/x-novelwriter-project.png => scaled/mime-novelwriter-256.png} (100%) rename setup/icons/{32x32/x-novelwriter-project.png => scaled/mime-novelwriter-32.png} (100%) rename setup/icons/{32x32@2x/x-novelwriter-project.png => scaled/mime-novelwriter-32@2.png} (100%) rename setup/icons/{48x48/x-novelwriter-project.png => scaled/mime-novelwriter-48.png} (100%) rename setup/icons/{512x512/x-novelwriter-project.png => scaled/mime-novelwriter-512.png} (100%) rename setup/icons/{64x64/x-novelwriter-project.png => scaled/mime-novelwriter-64.png} (100%) rename setup/icons/{96x96/x-novelwriter-project.png => scaled/mime-novelwriter-96.png} (100%) diff --git a/.gitignore b/.gitignore index 830a409a..66c21d61 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.spec *.egg-info setup.iss +novelwriter.desktop # Documentation /docs/build/ diff --git a/setup.py b/setup.py index c3c5710b..da2a6c34 100755 --- a/setup.py +++ b/setup.py @@ -140,14 +140,17 @@ def buildSampleZip(): # Create Launcher # =============================================================================================== # -def makeLauncherLinux(): +def xdgInstall(): """Will attempt to install icons and make a launcher. """ print("") - print("Creating Launcher") - print("=================") + print("XDG Install") + print("===========") print("") + # Find Executable(s) + # ================== + exOpts = [] testExec = shutil.which("novelWriter") @@ -185,95 +188,94 @@ def makeLauncherLinux(): sys.exit(1) print("Using executable: %s " % useExec) + print("") + + # Create and Install Launcher + # =========================== - # Read the Template desktopData = "" with open(os.path.join("setup", "novelwriter.desktop"), mode="r") as inFile: desktopData = inFile.read() desktopData = desktopData.replace(r"%%exec%%", useExec) - desktopFile = "/usr/share/applications/novelwriter.desktop" - try: - with open(desktopFile, mode="w+") as outFile: - outFile.write(desktopData) - print("Wrote file: %s" % desktopFile) - except Exception as e: - print("Error: Could not write novelwriter.desktop file.") - print(str(e)) - sys.exit(1) + desktopFile = os.path.join(os.getcwd(), "novelwriter.desktop") + with open(desktopFile, mode="w+") as outFile: + outFile.write(desktopData) + + exCode = subprocess.call( + ["xdg-desktop-menu", "install", "--novendor", "./novelwriter.desktop"] + ) + if exCode == 0: + print("Installed menu desktop file") + else: + print(f"Error {exCode}: Could not install menu desktop file") + + exCode = subprocess.call( + ["xdg-desktop-icon", "install", "--novendor", "./novelwriter.desktop"] + ) + if exCode == 0: + print("Installed icon desktop file") + else: + print(f"Error {exCode}: Could not install icon desktop file") print("") - # Copy Icons + # Install MimeType + # ================ - iconDirs = [ - "/usr/share/icons/hicolor/24x24/apps", - "/usr/share/icons/hicolor/48x48/apps", - "/usr/share/icons/hicolor/96x96/apps", - "/usr/share/icons/hicolor/256x256/apps", - "/usr/share/icons/hicolor/512x512/apps", - "/usr/share/icons/hicolor/scalable/apps", - "/usr/share/icons/hicolor/scalable/mimetypes", - ] - for iconDir in iconDirs: - if not os.path.isdir: - try: - os.mkdir(iconDir) - print("Created folder: %s" % iconDir) - except Exception as e: - print("Error: Could not make folder: %s" % iconDir) - print(str(e)) - - copyList = [( - "setup/icons/24x24/novelwriter.png", - "/usr/share/icons/hicolor/24x24/apps/novelwriter.png" - ), ( - "setup/icons/48x48/novelwriter.png", - "/usr/share/icons/hicolor/48x48/apps/novelwriter.png" - ), ( - "setup/icons/96x96/novelwriter.png", - "/usr/share/icons/hicolor/96x96/apps/novelwriter.png" - ), ( - "setup/icons/256x256/novelwriter.png", - "/usr/share/icons/hicolor/256x256/apps/novelwriter.png" - ), ( - "setup/icons/512x512/novelwriter.png", - "/usr/share/icons/hicolor/512x512/apps/novelwriter.png" - ), ( - "setup/icons/novelwriter.svg", - "/usr/share/icons/hicolor/scalable/apps/novelwriter.svg" - ), ( - "setup/icons/x-novelwriter-project.svg", - "/usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg" - ), ( - "setup/mime/x-novelwriter-project.xml", - "/usr/share/mime/packages/x-novelwriter-project.xml" - )] - for srcFile, dstFile in copyList: - try: - shutil.copyfile(srcFile, dstFile) - print("Copied file to: %s" % dstFile) - except Exception as e: - print("Error: Could not copy file: %s" % srcFile) - print(str(e)) + exCode = subprocess.call([ + "xdg-mime", "install", + "setup/mime/x-novelwriter-project.xml" + ]) + if exCode == 0: + print("Installed mimetype") + else: + print(f"Error {exCode}: Could not install mimetype") print("") - # Update System - try: - subprocess.call(["update-mime-database", "/usr/share/mime/"]) - print("Updated mime database.") - except Exception as e: - print("Error: Filed to update mime database.") - print(str(e)) + # Install Icons + # ============= - try: - subprocess.call(["update-icon-caches", "/usr/share/icons/*"]) - print("Updated icon cache.") - except Exception as e: - print("Error: Filed to update icon cache.") - print(str(e)) + sizeArr = ["16", "22", "24", "32", "48", "96", "128", "256", "512"] + + # App Icon + for aSize in sizeArr: + exCode = subprocess.call([ + "xdg-icon-resource", "install", + "--novendor", "--noupdate", + "--context", "apps", + "--size", aSize, + f"setup/icons/scaled/icon-novelwriter-{aSize}.png", + "novelwriter" + ]) + if exCode == 0: + print(f"Installed app icon size {aSize}") + else: + print(f"Error {exCode}: Could not install app icon size {aSize}") + + # Mimetype + for aSize in sizeArr: + exCode = subprocess.call([ + "xdg-icon-resource", "install", + "--noupdate", + "--context", "mimetypes", + "--size", aSize, + f"setup/icons/scaled/mime-novelwriter-{aSize}.png", + "application-x-novelwriter-project" + ]) + if exCode == 0: + print(f"Installed mime icon size {aSize}") + else: + print(f"Error {exCode}: Could not install mime icon size {aSize}") + + # Update Cache + exCode = subprocess.call(["xdg-icon-resource", "forceupdate"]) + if exCode == 0: + print("Updated icon cache") + else: + print("Error {exCode}: Could not update icon cache") print("") print("Done!") @@ -293,10 +295,10 @@ if __name__ == "__main__": "======================\n" "This tool provides some additional setup commands for novelWriter.\n" "\n" - "help Print the help message.\n" - "qthelp Build the help documentation for use with the QtAssistant.\n" - "sample Build the sample project as a zip file.\n" - "launcher Install launcher icons for freedesktop systems.\n" + "help Print the help message.\n" + "qthelp Build the help documentation for use with the QtAssistant.\n" + "sample Build the sample project as a zip file.\n" + "xdg-install Install launcher and icons for freedesktop systems.\n" ) if "help" in sys.argv: @@ -312,9 +314,13 @@ if __name__ == "__main__": sys.argv.remove("sample") buildSampleZip() - if "launcher" in sys.argv: - sys.argv.remove("launcher") - makeLauncherLinux() + if "xdg-install" in sys.argv: + sys.argv.remove("xdg-install") + if not sys.platform.startswith("win32"): + xdgInstall() + else: + print("ERROR: xdg-install cannot be used on Windows") + sys.exit(1) if len(sys.argv) <= 1: # Nothing more to do diff --git a/setup/icons/1024x1024/novelwriter.png b/setup/icons/scaled/icon-novelwriter-1024.png similarity index 100% rename from setup/icons/1024x1024/novelwriter.png rename to setup/icons/scaled/icon-novelwriter-1024.png diff --git a/setup/icons/128x128/novelwriter.png b/setup/icons/scaled/icon-novelwriter-128.png similarity index 100% rename from setup/icons/128x128/novelwriter.png rename to setup/icons/scaled/icon-novelwriter-128.png diff --git a/setup/icons/16x16/novelwriter.png b/setup/icons/scaled/icon-novelwriter-16.png similarity index 100% rename from setup/icons/16x16/novelwriter.png rename to setup/icons/scaled/icon-novelwriter-16.png diff --git a/setup/icons/16x16@2x/novelwriter.png b/setup/icons/scaled/icon-novelwriter-16@2.png similarity index 100% rename from setup/icons/16x16@2x/novelwriter.png rename to setup/icons/scaled/icon-novelwriter-16@2.png diff --git a/setup/icons/18x18@2x/novelwriter.png b/setup/icons/scaled/icon-novelwriter-18@2.png similarity index 100% rename from setup/icons/18x18@2x/novelwriter.png rename to setup/icons/scaled/icon-novelwriter-18@2.png diff --git a/setup/icons/scaled/icon-novelwriter-22.png b/setup/icons/scaled/icon-novelwriter-22.png new file mode 100644 index 0000000000000000000000000000000000000000..27ca5f32cfc4a75a4e925e5449b44df0a2b44ee5 GIT binary patch literal 985 zcmV;~119{5P)s5hpsa3O>MrMx>=pOByQ;Nu#MIB=;scYdP5ao|DrS+;H|jXRour^__36@5qVs z=i{N~%($18@ol05dQr1q|H4aBy(5!RrX_h4|b`YvDqi8k#xR9UlGI0e8zj)_^~<-!qCkL$7cO(%c8DP)wvwnTSYkZrQ>(MRIr%6%CqJMwGD>r(!}e<* zvas)986RI?h5lx)la$)285z_{1MD8X?l`g8%Hlx?k({yd0c?yO(BNVxtR8yjn^#8!p+)o1iWgtyPo z7~Kna*0|UOETG*poL!nfeco+QL()$0yWT#d_(g$ol~ zhdjU$ksyP^mA7QqOciJ}(5Yh-=Im_uQyInF#jY*t%}@RV_IVrt*u~%f00000NkvXX Hu0mjfXL-uX literal 0 HcmV?d00001 diff --git a/setup/icons/24x24/novelwriter.png b/setup/icons/scaled/icon-novelwriter-24.png similarity index 100% rename from setup/icons/24x24/novelwriter.png rename to setup/icons/scaled/icon-novelwriter-24.png diff --git a/setup/icons/256x256/novelwriter.png b/setup/icons/scaled/icon-novelwriter-256.png similarity index 100% rename from setup/icons/256x256/novelwriter.png rename to setup/icons/scaled/icon-novelwriter-256.png diff --git a/setup/icons/32x32/novelwriter.png b/setup/icons/scaled/icon-novelwriter-32.png similarity index 100% rename from setup/icons/32x32/novelwriter.png rename to setup/icons/scaled/icon-novelwriter-32.png diff --git a/setup/icons/48x48/novelwriter.png b/setup/icons/scaled/icon-novelwriter-48.png similarity index 100% rename from setup/icons/48x48/novelwriter.png rename to setup/icons/scaled/icon-novelwriter-48.png diff --git a/setup/icons/512x512/novelwriter.png b/setup/icons/scaled/icon-novelwriter-512.png similarity index 100% rename from setup/icons/512x512/novelwriter.png rename to setup/icons/scaled/icon-novelwriter-512.png diff --git a/setup/icons/32x32@2x/novelwriter.png b/setup/icons/scaled/icon-novelwriter-64.png similarity index 100% rename from setup/icons/32x32@2x/novelwriter.png rename to setup/icons/scaled/icon-novelwriter-64.png diff --git a/setup/icons/96x96/novelwriter.png b/setup/icons/scaled/icon-novelwriter-96.png similarity index 100% rename from setup/icons/96x96/novelwriter.png rename to setup/icons/scaled/icon-novelwriter-96.png diff --git a/setup/icons/64x64/novelwriter.png b/setup/icons/scaled/icon-novelwriter32@2.png similarity index 100% rename from setup/icons/64x64/novelwriter.png rename to setup/icons/scaled/icon-novelwriter32@2.png diff --git a/setup/icons/1024x1024/x-novelwriter-project.png b/setup/icons/scaled/mime-novelwriter-1024.png similarity index 100% rename from setup/icons/1024x1024/x-novelwriter-project.png rename to setup/icons/scaled/mime-novelwriter-1024.png diff --git a/setup/icons/128x128/x-novelwriter-project.png b/setup/icons/scaled/mime-novelwriter-128.png similarity index 100% rename from setup/icons/128x128/x-novelwriter-project.png rename to setup/icons/scaled/mime-novelwriter-128.png diff --git a/setup/icons/16x16/x-novelwriter-project.png b/setup/icons/scaled/mime-novelwriter-16.png similarity index 100% rename from setup/icons/16x16/x-novelwriter-project.png rename to setup/icons/scaled/mime-novelwriter-16.png diff --git a/setup/icons/16x16@2x/x-novelwriter-project.png b/setup/icons/scaled/mime-novelwriter-16@2.png similarity index 100% rename from setup/icons/16x16@2x/x-novelwriter-project.png rename to setup/icons/scaled/mime-novelwriter-16@2.png diff --git a/setup/icons/18x18@2x/x-novelwriter-project.png b/setup/icons/scaled/mime-novelwriter-18@2.png similarity index 100% rename from setup/icons/18x18@2x/x-novelwriter-project.png rename to setup/icons/scaled/mime-novelwriter-18@2.png diff --git a/setup/icons/scaled/mime-novelwriter-22.png b/setup/icons/scaled/mime-novelwriter-22.png new file mode 100644 index 0000000000000000000000000000000000000000..384e2ca4b9cff0e599d47515b4b0f74c9fc5118d GIT binary patch literal 986 zcmV<0110>4P)d+xoT_r2$y zbDzYuEi5cF_6X*e)4MY>GlvwIWOOz*Hhvu#7#No% z$%F_YN;X~B5ke4)#mHnbnUj-~;Pmu#$_lm_o$Bi9CPh)$e)ERE=I4=b+&~tBTp|Iv z9Fo_|w-XcmFg}hf%LpN`*=%%mb-AL^Xl!X|soknvHl<6FB+D`f@7~dvPNRPOh!PG{ zb$CeC@iF#KpGduY$>yt9mEiC& zcDtSS_I5M&mzS4+mjoBye8c1ghq#G*nfkw;P1u_{kI6@7-hg(W5-2MPNg5LrV)Iv$H6Qf}$wsx=!Ey`|P}Zi+prs zZcIBIbdHRagyx093b)y8=6fK7z+G3z=@LpU+njEDU%w8f9;952w=! zfZOe+rlyAa`g(?jhH$xDG&eW1v$Mn0)D+=x*brvY&9YfrTU+hz?R`+GbSssx5Q0b~ z^4r+h*hF58lgZ?>?d|QX!{KNwRcVEw!nuo!i^EhZ_1y6N3$d4|d1`T3+5i9m07*qo IM6N<$f(rfLQUCw| literal 0 HcmV?d00001 diff --git a/setup/icons/24x24/x-novelwriter-project.png b/setup/icons/scaled/mime-novelwriter-24.png similarity index 100% rename from setup/icons/24x24/x-novelwriter-project.png rename to setup/icons/scaled/mime-novelwriter-24.png diff --git a/setup/icons/256x256/x-novelwriter-project.png b/setup/icons/scaled/mime-novelwriter-256.png similarity index 100% rename from setup/icons/256x256/x-novelwriter-project.png rename to setup/icons/scaled/mime-novelwriter-256.png diff --git a/setup/icons/32x32/x-novelwriter-project.png b/setup/icons/scaled/mime-novelwriter-32.png similarity index 100% rename from setup/icons/32x32/x-novelwriter-project.png rename to setup/icons/scaled/mime-novelwriter-32.png diff --git a/setup/icons/32x32@2x/x-novelwriter-project.png b/setup/icons/scaled/mime-novelwriter-32@2.png similarity index 100% rename from setup/icons/32x32@2x/x-novelwriter-project.png rename to setup/icons/scaled/mime-novelwriter-32@2.png diff --git a/setup/icons/48x48/x-novelwriter-project.png b/setup/icons/scaled/mime-novelwriter-48.png similarity index 100% rename from setup/icons/48x48/x-novelwriter-project.png rename to setup/icons/scaled/mime-novelwriter-48.png diff --git a/setup/icons/512x512/x-novelwriter-project.png b/setup/icons/scaled/mime-novelwriter-512.png similarity index 100% rename from setup/icons/512x512/x-novelwriter-project.png rename to setup/icons/scaled/mime-novelwriter-512.png diff --git a/setup/icons/64x64/x-novelwriter-project.png b/setup/icons/scaled/mime-novelwriter-64.png similarity index 100% rename from setup/icons/64x64/x-novelwriter-project.png rename to setup/icons/scaled/mime-novelwriter-64.png diff --git a/setup/icons/96x96/x-novelwriter-project.png b/setup/icons/scaled/mime-novelwriter-96.png similarity index 100% rename from setup/icons/96x96/x-novelwriter-project.png rename to setup/icons/scaled/mime-novelwriter-96.png From 92610ca88a6c624d954c30f4084a55faa3702c54 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 22 Oct 2020 22:33:32 +0200 Subject: [PATCH 091/104] Move the import of setuptools to where it's used --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index da2a6c34..e515e9c0 100755 --- a/setup.py +++ b/setup.py @@ -21,7 +21,6 @@ import os import sys import shutil import subprocess -import setuptools # =============================================================================================== # # Qt Assistant Documentation Builder @@ -327,6 +326,7 @@ if __name__ == "__main__": sys.exit(0) # Run the standard setup + import setuptools setuptools.setup() # END Main From 56cbf86df42efcecef0bc0183fb05f10886f4617 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 22 Oct 2020 22:34:31 +0200 Subject: [PATCH 092/104] Flake8 tag --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e515e9c0..49cd049d 100755 --- a/setup.py +++ b/setup.py @@ -326,7 +326,7 @@ if __name__ == "__main__": sys.exit(0) # Run the standard setup - import setuptools + import setuptools # noqa: F401 setuptools.setup() # END Main From 299b8cde1ba82db36408e22524742adbc8c7577c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 22 Oct 2020 22:40:54 +0200 Subject: [PATCH 093/104] Update readmes with the XDG changes --- README.md | 4 +++- setup/README.md | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9efa0b24..54348b1f 100644 --- a/README.md +++ b/README.md @@ -104,8 +104,10 @@ If you want to run directly from the source, the application can be started with You can also create a launcher for running directly from source with: ```bash -sudo python3 setup.py launcher +python3 setup.py xdg-install ``` +This will install the launcher and icons for the current user. +To install them system-wide, run the above command with `sudo` or as root. For more install options, see [Build and Install novelWriter](setup/README.md). diff --git a/setup/README.md b/setup/README.md index 81479cf7..252f70cd 100644 --- a/setup/README.md +++ b/setup/README.md @@ -13,8 +13,9 @@ The `setup.py` is a standard Python setup script with a couple of additional opt * `sample`: Will create a `sample.zip` file in the `nw/assets` folder. This is the file the New Project Wizard uses to generate an example project. If novelWriter is run from source, this file is not needed. -* `launcher`: Will try to copy the novelWriter icons and create a novelWriter.desktop file to launch +* `xdg-install`: Will install novelWriter icons, mimetype, and desktop and menu launcher on Linux desktops. the application. This should work on standard Linux desktops. + By default, this is installed for the current user. Run with `sudo` to install system-wide. To install novelWriter as a local Python package, run: ```bash From bfcbf66044805b70a0f8c9d609ede51fe802f5a9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 22 Oct 2020 23:00:06 +0200 Subject: [PATCH 094/104] Some minor tweaks of the xdg install --- setup.py | 16 +++++++--------- setup/novelwriter.desktop | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 49cd049d..1eae8bf7 100755 --- a/setup.py +++ b/setup.py @@ -193,13 +193,11 @@ def xdgInstall(): # =========================== desktopData = "" - with open(os.path.join("setup", "novelwriter.desktop"), mode="r") as inFile: + with open("./setup/novelwriter.desktop", mode="r") as inFile: desktopData = inFile.read() desktopData = desktopData.replace(r"%%exec%%", useExec) - - desktopFile = os.path.join(os.getcwd(), "novelwriter.desktop") - with open(desktopFile, mode="w+") as outFile: + with open("./novelwriter.desktop", mode="w+") as outFile: outFile.write(desktopData) exCode = subprocess.call( @@ -225,7 +223,7 @@ def xdgInstall(): exCode = subprocess.call([ "xdg-mime", "install", - "setup/mime/x-novelwriter-project.xml" + "./setup/mime/x-novelwriter-project.xml" ]) if exCode == 0: print("Installed mimetype") @@ -237,7 +235,7 @@ def xdgInstall(): # Install Icons # ============= - sizeArr = ["16", "22", "24", "32", "48", "96", "128", "256", "512"] + sizeArr = ["16", "22", "24", "32", "48", "64", "96", "128", "256", "512"] # App Icon for aSize in sizeArr: @@ -246,7 +244,7 @@ def xdgInstall(): "--novendor", "--noupdate", "--context", "apps", "--size", aSize, - f"setup/icons/scaled/icon-novelwriter-{aSize}.png", + f"./setup/icons/scaled/icon-novelwriter-{aSize}.png", "novelwriter" ]) if exCode == 0: @@ -261,7 +259,7 @@ def xdgInstall(): "--noupdate", "--context", "mimetypes", "--size", aSize, - f"setup/icons/scaled/mime-novelwriter-{aSize}.png", + f"./setup/icons/scaled/mime-novelwriter-{aSize}.png", "application-x-novelwriter-project" ]) if exCode == 0: @@ -274,7 +272,7 @@ def xdgInstall(): if exCode == 0: print("Updated icon cache") else: - print("Error {exCode}: Could not update icon cache") + print(f"Error {exCode}: Could not update icon cache") print("") print("Done!") diff --git a/setup/novelwriter.desktop b/setup/novelwriter.desktop index 69550600..37236fab 100644 --- a/setup/novelwriter.desktop +++ b/setup/novelwriter.desktop @@ -2,7 +2,7 @@ Type=Application Encoding=UTF-8 Name=novelWriter -Comment=Multi-document markdown editor for novels +Comment=Multi-document markdown-like editor for novels Exec=%%exec%% %f Icon=novelwriter Categories=Qt;Office;WordProcessor; From 170c2dc19c63f6ce5b3a59432660a5625ff5bee3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 23 Oct 2020 20:26:29 +0200 Subject: [PATCH 095/104] Added a new formatTime function to common, and replaced similar functions in other classes --- nw/common.py | 10 ++++++++++ nw/gui/statusbar.py | 6 ++---- nw/gui/writingstats.py | 38 +++++++++++--------------------------- tests/test_common.py | 22 ++++++++++++++++++++-- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/nw/common.py b/nw/common.py index 17a8a337..021fa162 100644 --- a/nw/common.py +++ b/nw/common.py @@ -169,6 +169,16 @@ def formatTimeStamp(theTime, fileSafe=False): else: return datetime.fromtimestamp(theTime).strftime(nwConst.tStampFmt) +def formatTime(tS): + """Format the time spent in 00:00:00 format. + """ + if isinstance(tS, int): + if tS >= 86400: + return f"{tS//86400:d}-{tS//3600%24:02d}:{tS%3600//60:02d}:{tS%60:02d}" + else: + return f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}" + return "ERROR" + def splitVersionNumber(vString): """ Splits a version string on the form aa.bb.cc into major, minor and patch, and computes an integer value aabbcc. diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index 0b95ef16..ace755ae 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -35,6 +35,7 @@ from PyQt5.QtGui import QColor, QPainter from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton from nw.core import NWSpellCheck +from nw.common import formatTime logger = logging.getLogger(__name__) @@ -206,10 +207,7 @@ class GuiMainStatus(QStatusBar): if self.refTime is None: self.timeText.setText("00:00:00") else: - tS = int(time() - self.refTime) - self.timeText.setText( - f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}" - ) + self.timeText.setText(formatTime(round(time() - self.refTime))) return # END Class GuiMainStatus diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index fa03a278..170d8cf8 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -39,6 +39,7 @@ from PyQt5.QtWidgets import ( QLabel, QGroupBox, QMenu, QAction, QFileDialog, QSpinBox, QHBoxLayout ) +from nw.common import formatTime from nw.constants import nwConst, nwFiles, nwAlert from nw.gui.custom import QSwitch @@ -123,11 +124,11 @@ class GuiWritingStats(QDialog): self.infoForm = QGridLayout(self) self.infoBox.setLayout(self.infoForm) - self.labelTotal = QLabel(self._formatTime(0)) + self.labelTotal = QLabel(formatTime(0)) self.labelTotal.setFont(self.theTheme.guiFontFixed) self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight) - self.labelFilter = QLabel(self._formatTime(0)) + self.labelFilter = QLabel(formatTime(0)) self.labelFilter.setFont(self.theTheme.guiFontFixed) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) @@ -367,34 +368,26 @@ class GuiWritingStats(QDialog): elif dataFmt == self.FMT_CSV: outFile.write( - "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n" % ( - "Date", "Length (sec)", "Words Changed", "Novel Words", "Note Words" - ) + '"Date","Length (sec)","Words Changed","Novel Words","Note Words"\n' ) for _, sD, tT, wD, wA, wB in self.filterData: - outFile.write( - "\"%s\",%d,%d,%d,%d\n" % (sD, tT, wD, wA, wB) - ) + outFile.write(f'"{sD}",{tT:.0f},{wD},{wA},{wB}\n') wSuccess = True else: errMsg = "Unknown format" except Exception as e: - errMsg = str(e) + errMsg = str(e).replace("\n", "
") # Report to user if wSuccess: self.theParent.makeAlert( - "%s file successfully written to:
%s" % ( - textFmt, savePath - ), nwAlert.INFO + f"{textFmt} file successfully written to:
{savePath}", nwAlert.INFO ) else: self.theParent.makeAlert( - "Failed to write %s file. %s" % ( - textFmt, errMsg - ), nwAlert.ERROR + f"Failed to write {textFmt} file.
{errMsg}", nwAlert.ERROR ) return True @@ -455,7 +448,7 @@ class GuiWritingStats(QDialog): return False ttWords = ttNovel + ttNotes - self.labelTotal.setText(self._formatTime(ttTime)) + self.labelTotal.setText(formatTime(round(ttTime))) self.novelWords.setText(f"{ttNovel:n}") self.notesWords.setText(f"{ttNotes:n}") self.totalWords.setText(f"{ttWords:n}") @@ -544,7 +537,7 @@ class GuiWritingStats(QDialog): newItem = QTreeWidgetItem() newItem.setText(self.C_TIME, sStart) - newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) + newItem.setText(self.C_LENGTH, formatTime(round(sDiff))) newItem.setText(self.C_COUNT, f"{nWords:n}") if nWords > 0 and listMax > 0: @@ -567,17 +560,8 @@ class GuiWritingStats(QDialog): self.listBox.addTopLevelItem(newItem) self.timeFilter += sDiff - self.labelFilter.setText(self._formatTime(self.timeFilter)) + self.labelFilter.setText(formatTime(round(self.timeFilter))) return True - def _formatTime(self, tS): - """Format the time spent in 00:00:00 format. - """ - tM = int(tS/60) - tH = int(tM/60) - tM = tM - tH*60 - tS = tS - tM*60 - tH*3600 - return "%02d:%02d:%02d" % (tH, tM, tS) - # END Class GuiWritingStats diff --git a/tests/test_common.py b/tests/test_common.py index 4dc9cfcf..2a24f31f 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -7,7 +7,7 @@ import pytest from nw.common import ( checkString, checkBool, checkInt, colRange, formatInt, transferCase, - fuzzyTime, checkHandle, formatTimeStamp + fuzzyTime, checkHandle, formatTimeStamp, formatTime ) from nwtools import cmpList @@ -78,11 +78,29 @@ def testColRange(): ) @pytest.mark.core -def testFormatTime(): +def testFormatTimeStamp(): tTime = time.mktime(time.gmtime(0)) assert formatTimeStamp(tTime, False) == "1970-01-01 00:00:00" assert formatTimeStamp(tTime, True) == "1970-01-01 00.00.00" +@pytest.mark.core +def testFormatTime(): + assert formatTime("1") == "ERROR" + assert formatTime(1.0) == "ERROR" + assert formatTime(1) == "00:00:01" + assert formatTime(59) == "00:00:59" + assert formatTime(60) == "00:01:00" + assert formatTime(180) == "00:03:00" + assert formatTime(194) == "00:03:14" + assert formatTime(3540) == "00:59:00" + assert formatTime(3599) == "00:59:59" + assert formatTime(3600) == "01:00:00" + assert formatTime(11640) == "03:14:00" + assert formatTime(11655) == "03:14:15" + assert formatTime(86399) == "23:59:59" + assert formatTime(86400) == "1-00:00:00" + assert formatTime(360000) == "4-04:00:00" + @pytest.mark.core def testFormatInt(): assert formatInt(1000) == "1000" From 8db7c17798691e6b23380c60da02ade2407f351c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 23 Oct 2020 20:56:44 +0200 Subject: [PATCH 096/104] Drop using formatting a few places where it isn't necessary --- nw/gui/about.py | 13 ++++++------- nw/gui/build.py | 8 ++++---- nw/gui/mainmenu.py | 6 +++--- nw/gui/writingstats.py | 8 ++++++-- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/nw/gui/about.py b/nw/gui/about.py index 248cf7d1..7f00cf11 100644 --- a/nw/gui/about.py +++ b/nw/gui/about.py @@ -55,14 +55,14 @@ class GuiAbout(QDialog): self.innerBox = QHBoxLayout() self.innerBox.setSpacing(self.mainConf.pxInt(16)) - self.setWindowTitle("About %s" % self.mainConf.appName) + self.setWindowTitle("About novelWriter") self.setMinimumWidth(self.mainConf.pxInt(650)) self.setMinimumHeight(self.mainConf.pxInt(600)) nPx = self.mainConf.pxInt(96) self.nwIcon = QLabel() self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx))) - self.lblName = QLabel("%s" % self.mainConf.appName) + self.lblName = QLabel("novelWriter") self.lblVers = QLabel("v%s" % nw.__version__) self.lblDate = QLabel(datetime.strptime(nw.__date__, "%Y-%m-%d").strftime("%x")) @@ -115,17 +115,17 @@ class GuiAbout(QDialog): """ listPrefix = "  •  " aboutMsg = ( - "

About {name:s}

" + "

About novelWriter

" "

{copyright:s}.

" "

Website: {domain:s}

" - "

{name:s} is a markdown-like text editor designed for " + "

novelWriter is a markdown-like text editor designed for " "organising and writing novels. It is written in Python 3 with a " "Qt5 GUI, using PyQt5.

" - "

{name:s} is free software: you can redistribute it and/or " + "

novelWriter is free software: you can redistribute it and/or " "modify it under the terms of the GNU General Public License as " "published by the Free Software Foundation, either version 3 of " "the License, or (at your option) any later version.

" - "

{name:s} is distributed in the hope that it will be useful, " + "

novelWriter is distributed in the hope that it will be useful, " "but WITHOUT ANY WARRANTY; without even the implied warranty of " "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

" "

See the License tab for the full text, or visit the GNU website " @@ -134,7 +134,6 @@ class GuiAbout(QDialog): "

Credits

" "

{credits:s}

" ).format( - name = self.mainConf.appName, copyright = nw.__copyright__, website = nw.__url__, domain = nw.__domain__, diff --git a/nw/gui/build.py b/nw/gui/build.py index 45a1a073..9b8b42ac 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -391,11 +391,11 @@ class GuiBuildNovel(QDialog): self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF)) self.saveMenu.addAction(self.savePDF) - self.saveHTM = QAction("%s HTML (.htm)" % self.mainConf.appName, self) + self.saveHTM = QAction("novelWriter HTML (.htm)", self) self.saveHTM.triggered.connect(lambda: self._saveDocument(self.FMT_HTM)) self.saveMenu.addAction(self.saveHTM) - self.saveNWD = QAction("%s Markdown (.nwd)" % self.mainConf.appName, self) + self.saveNWD = QAction("novelWriter Markdown (.nwd)", self) self.saveNWD.triggered.connect(lambda: self._saveDocument(self.FMT_NWD)) self.saveMenu.addAction(self.saveNWD) @@ -408,11 +408,11 @@ class GuiBuildNovel(QDialog): self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT)) self.saveMenu.addAction(self.saveTXT) - self.saveJsonH = QAction("JSON + %s HTML (.json)" % self.mainConf.appName, self) + self.saveJsonH = QAction("JSON + novelWriter HTML (.json)", self) self.saveJsonH.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_H)) self.saveMenu.addAction(self.saveJsonH) - self.saveJsonM = QAction("JSON + %s Markdown (.json)" % self.mainConf.appName, self) + self.saveJsonM = QAction("JSON + novelWriters Markdown (.json)", self) self.saveJsonM.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_M)) self.saveMenu.addAction(self.saveJsonM) diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 6bb7edf1..7fcd2a89 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -274,7 +274,7 @@ class GuiMainMenu(QMenuBar): # Project > Exit self.aExitNW = QAction("Exit", self) - self.aExitNW.setStatusTip("Exit %s" % self.mainConf.appName) + self.aExitNW.setStatusTip("Exit novelWriter") self.aExitNW.setShortcut("Ctrl+Q") self.aExitNW.setMenuRole(QAction.QuitRole) self.aExitNW.triggered.connect(lambda: self.theParent.closeMain()) @@ -857,8 +857,8 @@ class GuiMainMenu(QMenuBar): self.helpMenu = self.addMenu("&Help") # Help > About - self.aAboutNW = QAction("About %s" % self.mainConf.appName, self) - self.aAboutNW.setStatusTip("About %s" % self.mainConf.appName) + self.aAboutNW = QAction("About novelWriter", self) + self.aAboutNW.setStatusTip("About novelWriter") self.aAboutNW.setMenuRole(QAction.AboutRole) self.aAboutNW.triggered.connect(lambda: self.theParent.showAboutNWDialog()) self.helpMenu.addAction(self.aAboutNW) diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index 170d8cf8..c394c43d 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -383,11 +383,15 @@ class GuiWritingStats(QDialog): # Report to user if wSuccess: self.theParent.makeAlert( - f"{textFmt} file successfully written to:
{savePath}", nwAlert.INFO + "%s file successfully written to:
%s" % ( + textFmt, savePath + ), nwAlert.INFO ) else: self.theParent.makeAlert( - f"Failed to write {textFmt} file.
{errMsg}", nwAlert.ERROR + "Failed to write %s file.
%s" % ( + textFmt, errMsg + ), nwAlert.ERROR ) return True From c0ad631039662c587ba57e030ec99b6a8108386c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 23 Oct 2020 21:05:41 +0200 Subject: [PATCH 097/104] Updated source file descriptions --- nw/config.py | 2 +- nw/core/document.py | 2 +- nw/core/index.py | 2 +- nw/core/spellcheck.py | 2 +- nw/core/status.py | 2 +- nw/core/tokenizer.py | 2 +- nw/core/tree.py | 2 +- nw/gui/build.py | 8 ++++---- nw/gui/doceditor.py | 2 +- nw/gui/dochighlight.py | 2 +- nw/gui/docmerge.py | 2 +- nw/gui/docviewer.py | 2 +- nw/gui/itemdetails.py | 2 +- nw/gui/itemeditor.py | 2 +- nw/gui/mainmenu.py | 4 ++-- nw/gui/outlinedetails.py | 2 +- nw/gui/projload.py | 2 +- nw/gui/projtree.py | 2 +- nw/gui/theme.py | 2 +- nw/gui/writingstats.py | 2 +- nw/guimain.py | 2 +- 21 files changed, 25 insertions(+), 25 deletions(-) diff --git a/nw/config.py b/nw/config.py index bc777812..0aa6cf50 100644 --- a/nw/config.py +++ b/nw/config.py @@ -3,7 +3,7 @@ novelWriter – Config Class ============================ - This class reads and store the main preferences of the application + Class reading and holding the preferences of the application File History: Created: 2018-09-22 [0.0.1] diff --git a/nw/core/document.py b/nw/core/document.py index 1452e9dd..b1056a6b 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -3,7 +3,7 @@ novelWriter – Project Document ================================ - Class holding a document + Class holding a single novelWriter document File History: Created: 2018-09-29 [0.0.1] diff --git a/nw/core/index.py b/nw/core/index.py index 97ad7ba4..29f80ea9 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -3,7 +3,7 @@ novelWriter – Project Index ============================= - Class holding the index of tags + Class holding the project index of tags, headers and references File History: Created: 2019-05-27 [0.1.4] diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index e3093070..7223c616 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -3,7 +3,7 @@ novelWriter – Spell Check Classes =================================== - Wrapper class for spell checking + Wrapper class for spell checking tools File History: Created: 2019-06-11 [0.1.5] diff --git a/nw/core/status.py b/nw/core/status.py index a2e5b92a..7a1c7127 100644 --- a/nw/core/status.py +++ b/nw/core/status.py @@ -3,7 +3,7 @@ novelWriter – Project Item Status Class ========================================= - Class holding the status elements of a project item + Class holding the status/importance elements of a project item File History: Created: 2019-05-19 [0.1.3] diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 5087c67c..c7b23208 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -3,7 +3,7 @@ novelWriter – Text Tokenizer ============================== - Splits a piece of nW markdown text into its elements + Splits a piece of novelWriter markdown text into its elements File History: Created: 2019-05-05 [0.0.1] diff --git a/nw/core/tree.py b/nw/core/tree.py index f33b0d59..fc908745 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -3,7 +3,7 @@ novelWriter – Project Tree Class ================================== - Class holding the data of the project tree + Class holding the project's tree of project items File History: Created: 2020-05-07 [0.4.5] diff --git a/nw/gui/build.py b/nw/gui/build.py index 9b8b42ac..4a8f8aad 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -"""novelWriter GUI Build Novel +"""novelWriter GUI Build Novel Project - novelWriter – GUI Build Novel -=============================== - Class holding the build novel window + novelWriter – GUI Build Novel Project +======================================= + Class holding the build novel project dialog File History: Created: 2020-05-09 [0.5] diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 9a2cb683..a24a1f4e 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -3,7 +3,7 @@ novelWriter – GUI Document Editor =================================== - Class holding the document editor + Class holding the main document editor File History: Created: 2018-09-29 [0.0.1] GuiDocEditor diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index cc11ef86..309b74a8 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -3,7 +3,7 @@ novelWriter – GUI Document Highlighter ======================================== - Syntax highlighting for MarkDown + Subclass for the main editor syntax highlighting File History: Created: 2019-04-06 [0.0.1] diff --git a/nw/gui/docmerge.py b/nw/gui/docmerge.py index d7d128fe..f2a49e19 100644 --- a/nw/gui/docmerge.py +++ b/nw/gui/docmerge.py @@ -3,7 +3,7 @@ novelWriter – GUI Doc Merge ============================= - Tool for merging multiple documents to one + Tool for merging multiple documents to one document File History: Created: 2020-01-23 [0.4.3] diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 0c9a8086..636c4637 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -3,7 +3,7 @@ novelWriter – GUI Document Viewer =================================== - Class holding the document html viewer + Class holding the main document viewer File History: Created: 2019-05-10 [0.0.1] GuiDocViewer diff --git a/nw/gui/itemdetails.py b/nw/gui/itemdetails.py index 6d5cbcb5..da1a8f4a 100644 --- a/nw/gui/itemdetails.py +++ b/nw/gui/itemdetails.py @@ -3,7 +3,7 @@ novelWriter – GUI Document Details ==================================== - Class holding the left side document details panel + Class holding the project tree item details panel File History: Created: 2019-04-24 [0.0.1] diff --git a/nw/gui/itemeditor.py b/nw/gui/itemeditor.py index 95a68c82..a56a93bf 100644 --- a/nw/gui/itemeditor.py +++ b/nw/gui/itemeditor.py @@ -3,7 +3,7 @@ novelWriter – GUI Item Editor =============================== - Class holding the item editor + Class holding the item editor dialog File History: Created: 2019-04-27 [0.0.1] diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 7fcd2a89..706b5bba 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -3,10 +3,10 @@ novelWriter – GUI Main Menu ============================= - Class holding the main window + Class holding the main window menu File History: - Created: 2019-04-27 [0.0.1] (Split from winmain) + Created: 2019-04-27 [0.0.1] This file is a part of novelWriter Copyright 2018–2020, Veronica Berglyd Olsen diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py index 18f2b90f..d19bf2e9 100644 --- a/nw/gui/outlinedetails.py +++ b/nw/gui/outlinedetails.py @@ -3,7 +3,7 @@ novelWriter – GUI Project Outline Details =========================================== - Class holding the project outline details view + Class holding the project outline details panel File History: Created: 2020-06-02 [0.7.0] diff --git a/nw/gui/projload.py b/nw/gui/projload.py index 29c9c9ab..35621032 100644 --- a/nw/gui/projload.py +++ b/nw/gui/projload.py @@ -3,7 +3,7 @@ novelWriter – GUI Open Project ================================ - The open project dialog + Class holding the load/browse/new project dialog File History: Created: 2020-02-26 [0.4.5] diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 77fdf610..89e41374 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -3,7 +3,7 @@ novelWriter – GUI project Tree ================================ - Class holding the left side project tree view + Class holding the project tree view File History: Created: 2018-09-29 [0.0.1] GuiProjectTree diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 9d907a67..2b5f31f5 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -3,7 +3,7 @@ novelWriter – Theme and Icons Classs ====================================== - This class reads and stores the themes and the icons + Class managing and caching themes and icons File History: Created: 2019-05-18 [0.1.3] GuiTheme diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index c394c43d..fb10f7a4 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -3,7 +3,7 @@ novelWriter – GUI Writing Statistics ====================================== - Class showing the word count and session statistics + Class holding the word count and session statistics dialog File History: Created: 2019-10-20 [0.3] diff --git a/nw/guimain.py b/nw/guimain.py index 6742a0d9..9b1a8a42 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -3,7 +3,7 @@ novelWriter – GUI Main Window =============================== - Class holding the main window + Class holding the main application window File History: Created: 2018-09-22 [0.0.1] From 3b14aacc0fee6b2d064f436a7b70ec5ea731177e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 24 Oct 2020 12:06:42 +0200 Subject: [PATCH 098/104] Fix icon link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54348b1f..72ae5824 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![pypi](https://img.shields.io/pypi/v/novelwriter)](https://pypi.org/project/novelWriter) [![python](https://img.shields.io/pypi/pyversions/novelwriter)](https://pypi.org/project/novelWriter) - + novelWriter is a Markdown-like text editor designed for writing novels and larger projects of many smaller plain text documents. It uses its own flavour of Markdown that supports a meta data syntax From 8d31428a9d14473cfdc0b5d0d789d25c70ca61be Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 24 Oct 2020 17:54:02 +0200 Subject: [PATCH 099/104] Improve a few functions in common and item --- nw/common.py | 13 ++++++------ nw/config.py | 2 +- nw/core/item.py | 50 +++++++++++++++++++++++++++------------------- tests/test_item.py | 2 +- 4 files changed, 39 insertions(+), 28 deletions(-) diff --git a/nw/common.py b/nw/common.py index 021fa162..d5563d16 100644 --- a/nw/common.py +++ b/nw/common.py @@ -152,13 +152,13 @@ def formatInt(theInt): theVal /= 1000.0 if theVal < 1000.0: if theVal < 10.0: - return "%4.2f%s%s" % (theVal, nwUnicode.U_THNSP, pF) + return f"{theVal:4.2f}{nwUnicode.U_THNSP}{pF}" elif theVal < 100.0: - return "%4.1f%s%s" % (theVal, nwUnicode.U_THNSP, pF) + return f"{theVal:4.1f}{nwUnicode.U_THNSP}{pF}" else: - return "%3.0f%s%s" % (theVal, nwUnicode.U_THNSP, pF) + return f"{theVal:3.0f}{nwUnicode.U_THNSP}{pF}" - return "%d" % theInt + return str(theInt) def formatTimeStamp(theTime, fileSafe=False): """Take a number (on the format returned by time.time()) and convert @@ -170,11 +170,12 @@ def formatTimeStamp(theTime, fileSafe=False): return datetime.fromtimestamp(theTime).strftime(nwConst.tStampFmt) def formatTime(tS): - """Format the time spent in 00:00:00 format. + """Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format + if a full day or longer. """ if isinstance(tS, int): if tS >= 86400: - return f"{tS//86400:d}-{tS//3600%24:02d}:{tS%3600//60:02d}:{tS%60:02d}" + return f"{tS//86400:d}-{tS%86400//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}" else: return f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}" return "ERROR" diff --git a/nw/config.py b/nw/config.py index 0aa6cf50..a82c133b 100644 --- a/nw/config.py +++ b/nw/config.py @@ -911,7 +911,7 @@ class Config: def _packList(self, inData): """Pack a list of items into a comma separated string. """ - return ", ".join(str(inVal) for inVal in inData) + return ", ".join([str(inVal) for inVal in inData]) def _parseLine(self, cnfParse, cnfSec, cnfName, cnfType, cnfDefault): """Parse a line and return the correct datatype. diff --git a/nw/core/item.py b/nw/core/item.py index 944b5f0c..6793af39 100644 --- a/nw/core/item.py +++ b/nw/core/item.py @@ -29,7 +29,7 @@ import logging from lxml import etree -from nw.common import checkInt +from nw.common import checkInt, isHandle from nw.constants import nwItemType, nwItemClass, nwItemLayout logger = logging.getLogger(__name__) @@ -104,27 +104,37 @@ class NWItem(): if "parent" in xItem.attrib: self.itemParent = xItem.attrib["parent"] - setMap = { - "name" : self.setName, - "order" : self.setOrder, - "type" : self.setType, - "class" : self.setClass, - "layout" : self.setLayout, - "status" : self.setStatus, - "expanded" : self.setExpanded, - "exported" : self.setExported, - "charCount" : self.setCharCount, - "wordCount" : self.setWordCount, - "paraCount" : self.setParaCount, - "cursorPos" : self.setCursorPos, - } + retStatus = True for xValue in xItem: - if xValue.tag in setMap: - setMap[xValue.tag](xValue.text) + if xValue.tag == "name": + self.setName(xValue.text) + elif xValue.tag == "order": + self.setOrder(xValue.text) + elif xValue.tag == "type": + self.setType(xValue.text) + elif xValue.tag == "class": + self.setClass(xValue.text) + elif xValue.tag == "layout": + self.setLayout(xValue.text) + elif xValue.tag == "status": + self.setStatus(xValue.text) + elif xValue.tag == "expanded": + self.setExpanded(xValue.text) + elif xValue.tag == "exported": + self.setExported(xValue.text) + elif xValue.tag == "charCount": + self.setCharCount(xValue.text) + elif xValue.tag == "wordCount": + self.setWordCount(xValue.text) + elif xValue.tag == "paraCount": + self.setParaCount(xValue.text) + elif xValue.tag == "cursorPos": + self.setCursorPos(xValue.text) else: logger.error("Unknown tag '%s'" % xValue.tag) + retStatus = False - return True + return retStatus @staticmethod def _subPack(xParent, name, attrib=None, text=None, none=True): @@ -153,7 +163,7 @@ class NWItem(): """Set the item handle, and ensure it is valid. """ if isinstance(theHandle, str): - if len(theHandle) == 13: + if isHandle(theHandle): self.itemHandle = theHandle else: self.itemHandle = None @@ -167,7 +177,7 @@ class NWItem(): if theParent is None: self.itemParent = None elif isinstance(theParent, str): - if len(theParent) == 13: + if isHandle(theParent): self.itemParent = theParent else: self.itemParent = None diff --git a/tests/test_item.py b/tests/test_item.py index 936ba185..95aa8470 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -252,7 +252,7 @@ def testItemXMLPackUnpack(nwDummy): xDummy = etree.SubElement(nwXML, "item", attrib={"handle": "0123456789abc"}) xParam = etree.SubElement(xDummy, "invalid") xParam.text = "stuff" - assert theItem.unpackXML(xDummy) # Passes, but not saved + assert not theItem.unpackXML(xDummy) # Pack Valid Item xDummy = etree.SubElement(nwXML, "group") From f13bd877022350d597a9e508b6d5c89b5e50cab6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 24 Oct 2020 18:31:18 +0200 Subject: [PATCH 100/104] Allow more than one meta data line in documents, and split the current line into three --- nw/core/document.py | 99 +++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/nw/core/document.py b/nw/core/document.py index 1452e9dd..6b9361d0 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -30,7 +30,7 @@ import os from nw.constants import nwAlert from nw.common import isHandle -from nw.constants import nwItemLayout, nwItemClass, nwConst +from nw.constants import nwItemLayout, nwItemClass logger = logging.getLogger(__name__) @@ -45,7 +45,7 @@ class NWDoc(): self._theItem = None # The currently open item self._docHandle = None # The handle of the currently open item self._fileLoc = None # The file location of the currently open item - self._docMeta = "" # The meta string of the currently open item + self._docMeta = {} # The meta data of the currently open item # Internal Mapping self.makeAlert = self.theParent.makeAlert @@ -62,7 +62,7 @@ class NWDoc(): self._theItem = None self._docHandle = None self._fileLoc = None - self._docMeta = "" + self._docMeta = {} return def openDocument(self, tHandle, showStatus=True, isOrphan=False): @@ -94,16 +94,21 @@ class NWDoc(): self._fileLoc = docPath theText = "" - self._docMeta = "" + self._docMeta = {} if os.path.isfile(docPath): try: with open(docPath, mode="r", encoding="utf8") as inFile: - fstLine = inFile.readline() - if fstLine.startswith("%%~ "): - # This is the meta line - self._docMeta = fstLine[4:].strip() - else: - theText = fstLine + + # Check the first <= 10 lines for metadata + for i in range(10): + inLine = inFile.readline() + if inLine.startswith(r"%%~"): + self._parseMeta(inLine) + else: + theText = inLine + break + + # Load the rest of the file theText += inFile.read() except Exception as e: @@ -119,8 +124,6 @@ class NWDoc(): logger.debug("The requested document does not exist.") return "" - logger.verbose("DocMeta: '%s'" % self._docMeta) - if showStatus and not isOrphan: self.theParent.setStatus("Opened Document: %s" % self._theItem.itemName) @@ -145,14 +148,10 @@ class NWDoc(): if self._theItem is None: docMeta = "" else: - itemPath = self.theProject.projTree.getItemPath(self._docHandle) docMeta = ( - "%%~ {handlepath:s}:{itemclass:s}:{itemlayout:s}:{itemname:s}\n" - ).format( - handlepath = ":".join(itemPath), - itemclass = self._theItem.itemClass.name, - itemlayout = self._theItem.itemLayout.name, - itemname = self._theItem.itemName, + f"%%~name: {self._theItem.itemName:s}\n" + f"%%~path: {self._theItem.itemParent:s}/{self._theItem.itemHandle:s}\n" + f"%%~kind: {self._theItem.itemClass.name:s}/{self._theItem.itemLayout.name:s}\n" ) try: @@ -215,39 +214,43 @@ class NWDoc(): """Parses the document meta tag and returns the path and name as a list and a string. """ - if len(self._docMeta) < 14: - # Not enough information - return "", [], None, None + theName = self._docMeta.get("name", "") + theParent = self._docMeta.get("parent", None) + theClass = self._docMeta.get("class", None) + theLayout = self._docMeta.get("layout", None) - theMeta = self._docMeta + return theName, theParent, theClass, theLayout - # Scan for handles - thePath = [] - for n in range(nwConst.maxDepth + 5): - if len(theMeta) < 14: - break - if theMeta[13] == ":": - theHandle = theMeta[:13] - if isHandle(theHandle): - thePath.append(theHandle) - theMeta = theMeta[14:] - else: - break - else: - break + ## + # Internal Functions + ## - theClass = nwItemClass.NO_CLASS - for aClass in nwItemClass: - if theMeta.startswith(aClass.name): - theClass = aClass - theMeta = theMeta[len(aClass.name)+1:] + def _parseMeta(self, metaLine): + """Parse a line from the document statting with the characters + %%~ that may contain meta data. + """ + if metaLine.startswith("%%~name:"): + self._docMeta["name"] = metaLine[9:].strip() - theLayout = nwItemLayout.NO_LAYOUT - for aLayout in nwItemLayout: - if theMeta.startswith(aLayout.name): - theLayout = aLayout - theMeta = theMeta[len(aLayout.name)+1:] + elif metaLine.startswith("%%~path:"): + metaVal = metaLine[9:].strip() + metaBits = metaVal.split("/") + if len(metaBits) == 2: + if isHandle(metaBits[0]): + self._docMeta["parent"] = metaBits[0] + if isHandle(metaBits[1]): + self._docMeta["handle"] = metaBits[1] - return theMeta, thePath, theClass, theLayout + elif metaLine.startswith("%%~kind:"): + metaVal = metaLine[9:].strip() + metaBits = metaVal.split("/") + if len(metaBits) == 2: + if metaBits[0] in nwItemClass.__members__: + self._docMeta["class"] = nwItemClass[metaBits[0]] + if metaBits[1] in nwItemLayout.__members__: + self._docMeta["layout"] = nwItemLayout[metaBits[1]] + + # print(self._docMeta) + return # END Class NWDoc From 538009eff65182077edae572e7e83a5af6161a8d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 24 Oct 2020 18:56:01 +0200 Subject: [PATCH 101/104] Updated tests --- nw/core/document.py | 6 +++--- sample/content/636b6aa9b697b.nwd | 5 ++++- tests/lipsum/content/04468803b92e1.nwd | 4 +++- tests/lipsum/content/2426c6f0ca922.nwd | 4 +++- tests/lipsum/content/441420a886d82.nwd | 4 +++- tests/lipsum/content/47666c91c7ccf.nwd | 4 +++- tests/lipsum/content/4c4f28287af27.nwd | 4 +++- tests/lipsum/content/7a992350f3eb6.nwd | 4 +++- tests/lipsum/content/846352075de7d.nwd | 4 +++- tests/lipsum/content/88243afbe5ed8.nwd | 4 +++- tests/lipsum/content/88d59a277361b.nwd | 4 +++- tests/lipsum/content/8c58a65414c23.nwd | 4 +++- tests/lipsum/content/db7e733775d4d.nwd | 4 +++- tests/lipsum/content/eb103bc70c90c.nwd | 4 +++- tests/lipsum/content/f8c0562e50f1b.nwd | 4 +++- tests/lipsum/content/f96ec11c6a3da.nwd | 4 +++- tests/lipsum/content/fb609cd8319dc.nwd | 4 +++- tests/lipsum/nwProject.nwx | 8 ++++---- tests/minimal/content/8c659a11cd429.nwd | 4 +++- tests/minimal/content/a35baf2e93843.nwd | 4 +++- tests/minimal/content/f5ab3e30151e1.nwd | 4 +++- tests/minimal/nwProject.nwx | 13 +++++++------ tests/reference/gui/1_031b4af5197ec.nwd | 4 +++- tests/reference/gui/1_0e17daca5f3e1.nwd | 4 +++- tests/reference/gui/1_1a6562590ef19.nwd | 4 +++- tests/reference/gui/1_41cfc0d1f2d12.nwd | 4 +++- tests/reference/gui/4_73475cb40a568.nwd | 4 +++- tests/reference/gui/5_031b4af5197ec.nwd | 4 +++- tests/reference/gui/5_25fc0e7096fc6.nwd | 4 +++- tests/reference/gui/5_2858dcd1057d3.nwd | 4 +++- tests/reference/gui/5_2fca346db6561.nwd | 4 +++- tests/reference/gui/5_31489056e0916.nwd | 4 +++- tests/reference/gui/5_41cfc0d1f2d12.nwd | 4 +++- tests/reference/gui/5_98010bd9270f9.nwd | 4 +++- tests/test_dialogs.py | 4 ++-- tests/test_gui.py | 10 ---------- tests/test_project.py | 21 ++++++++++----------- 37 files changed, 120 insertions(+), 67 deletions(-) diff --git a/nw/core/document.py b/nw/core/document.py index d1b4196a..2cbb16bd 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -230,10 +230,10 @@ class NWDoc(): %%~ that may contain meta data. """ if metaLine.startswith("%%~name:"): - self._docMeta["name"] = metaLine[9:].strip() + self._docMeta["name"] = metaLine[8:].strip() elif metaLine.startswith("%%~path:"): - metaVal = metaLine[9:].strip() + metaVal = metaLine[8:].strip() metaBits = metaVal.split("/") if len(metaBits) == 2: if isHandle(metaBits[0]): @@ -242,7 +242,7 @@ class NWDoc(): self._docMeta["handle"] = metaBits[1] elif metaLine.startswith("%%~kind:"): - metaVal = metaLine[9:].strip() + metaVal = metaLine[8:].strip() metaBits = metaVal.split("/") if len(metaBits) == 2: if metaBits[0] in nwItemClass.__members__: diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 5936fbd3..eb0545cf 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -1,4 +1,7 @@ -%%~ 636b6aa9b697b:e7ded148d6e4a:7031beac91f75:NOVEL:SCENE:Making a Scene +%%~name: Making a Scene +%%~path: e7ded148d6e4a/636b6aa9b697b +%%~kind: NOVEL/SCENE +%%~ ### Making a Scene @pov: Jane diff --git a/tests/lipsum/content/04468803b92e1.nwd b/tests/lipsum/content/04468803b92e1.nwd index af7504dc..6d706890 100644 --- a/tests/lipsum/content/04468803b92e1.nwd +++ b/tests/lipsum/content/04468803b92e1.nwd @@ -1,4 +1,6 @@ -%%~ 04468803b92e1:60bdf227455cc:WORLD:NOTE:Ancient Europe +%%~name: Ancient Europe +%%~path: 60bdf227455cc/04468803b92e1 +%%~kind: WORLD/NOTE # Ancient Europe @tag: Europe diff --git a/tests/lipsum/content/2426c6f0ca922.nwd b/tests/lipsum/content/2426c6f0ca922.nwd index 98ea99c3..ad926141 100644 --- a/tests/lipsum/content/2426c6f0ca922.nwd +++ b/tests/lipsum/content/2426c6f0ca922.nwd @@ -1,4 +1,6 @@ -%%~ 2426c6f0ca922:6c6afb1247750:PLOT:NOTE:Main +%%~name: Main +%%~path: 6c6afb1247750/2426c6f0ca922 +%%~kind: PLOT/NOTE # Main Plot @tag: Main diff --git a/tests/lipsum/content/441420a886d82.nwd b/tests/lipsum/content/441420a886d82.nwd index 820862ba..26237180 100644 --- a/tests/lipsum/content/441420a886d82.nwd +++ b/tests/lipsum/content/441420a886d82.nwd @@ -1,4 +1,6 @@ -%%~ 441420a886d82:6bd935d2490cd:b3643d0f92e32:NOVEL:CHAPTER:Chapter Two +%%~name: Chapter Two +%%~path: 6bd935d2490cd/441420a886d82 +%%~kind: NOVEL/CHAPTER ## Chapter Two @pov: Bod diff --git a/tests/lipsum/content/47666c91c7ccf.nwd b/tests/lipsum/content/47666c91c7ccf.nwd index 027b20e5..7ea17223 100644 --- a/tests/lipsum/content/47666c91c7ccf.nwd +++ b/tests/lipsum/content/47666c91c7ccf.nwd @@ -1,4 +1,6 @@ -%%~ 47666c91c7ccf:6bd935d2490cd:b3643d0f92e32:NOVEL:SCENE:Scene Five +%%~name: Scene Five +%%~path: 6bd935d2490cd/47666c91c7ccf +%%~kind: NOVEL/SCENE ### Scene Five @pov: Bod diff --git a/tests/lipsum/content/4c4f28287af27.nwd b/tests/lipsum/content/4c4f28287af27.nwd index 50f646b5..d845442f 100644 --- a/tests/lipsum/content/4c4f28287af27.nwd +++ b/tests/lipsum/content/4c4f28287af27.nwd @@ -1,4 +1,6 @@ -%%~ 4c4f28287af27:67a8707f2f249:CHARACTER:NOTE:Mr. Nobody +%%~name: Mr. Nobody +%%~path: 67a8707f2f249/4c4f28287af27 +%%~kind: CHARACTER/NOTE # Nobody Owens @tag: Bod diff --git a/tests/lipsum/content/7a992350f3eb6.nwd b/tests/lipsum/content/7a992350f3eb6.nwd index ab7eb619..6982e548 100644 --- a/tests/lipsum/content/7a992350f3eb6.nwd +++ b/tests/lipsum/content/7a992350f3eb6.nwd @@ -1,4 +1,6 @@ -%%~ 7a992350f3eb6:b3643d0f92e32:NOVEL:TITLE:Lorem Ipusm +%%~name: Lorem Ipsum +%%~path: b3643d0f92e32/7a992350f3eb6 +%%~kind: NOVEL/TITLE # Lorem Ipsum **By lipsum.com** diff --git a/tests/lipsum/content/846352075de7d.nwd b/tests/lipsum/content/846352075de7d.nwd index 6a608f16..d362ccc6 100644 --- a/tests/lipsum/content/846352075de7d.nwd +++ b/tests/lipsum/content/846352075de7d.nwd @@ -1,4 +1,6 @@ -%%~ 846352075de7d:b3643d0f92e32:NOVEL:BOOK:Interlude +%%~name: Interlude +%%~path: b3643d0f92e32/846352075de7d +%%~kind: NOVEL/BOOK ## Why do we use it? % Exctracted from the lipsum.com website. diff --git a/tests/lipsum/content/88243afbe5ed8.nwd b/tests/lipsum/content/88243afbe5ed8.nwd index eba37bae..426ffeba 100644 --- a/tests/lipsum/content/88243afbe5ed8.nwd +++ b/tests/lipsum/content/88243afbe5ed8.nwd @@ -1,4 +1,6 @@ -%%~ 88243afbe5ed8:45e6b01ca35c1:b3643d0f92e32:NOVEL:SCENE:Scene One +%%~name: Scene One +%%~path: 45e6b01ca35c1/88243afbe5ed8 +%%~kind: NOVEL/SCENE ### Scene One @pov: Bod diff --git a/tests/lipsum/content/88d59a277361b.nwd b/tests/lipsum/content/88d59a277361b.nwd index f6ac2810..4d55bfda 100644 --- a/tests/lipsum/content/88d59a277361b.nwd +++ b/tests/lipsum/content/88d59a277361b.nwd @@ -1,4 +1,6 @@ -%%~ 88d59a277361b:b3643d0f92e32:NOVEL:UNNUMBERED:Prologue +%%~name: Prologue +%%~path: b3643d0f92e32/88d59a277361b +%%~kind: NOVEL/UNNUMBERED ## Prologue % Synopsis:Explanation from the lipsum.com website. diff --git a/tests/lipsum/content/8c58a65414c23.nwd b/tests/lipsum/content/8c58a65414c23.nwd index 408a3bec..28e54bef 100644 --- a/tests/lipsum/content/8c58a65414c23.nwd +++ b/tests/lipsum/content/8c58a65414c23.nwd @@ -1,4 +1,6 @@ -%%~ 8c58a65414c23:b3643d0f92e32:NOVEL:PAGE:Front Matter +%%~name: Front Matter +%%~path: b3643d0f92e32/8c58a65414c23 +%%~kind: NOVEL/PAGE % Exctracted from the lipsum.com website. Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of “de Finibus Bonorum et Malorum” (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, “Lorem ipsum dolor sit amet..”, comes from a line in section 1.10.32. diff --git a/tests/lipsum/content/db7e733775d4d.nwd b/tests/lipsum/content/db7e733775d4d.nwd index 7f4de78f..d677152f 100644 --- a/tests/lipsum/content/db7e733775d4d.nwd +++ b/tests/lipsum/content/db7e733775d4d.nwd @@ -1,4 +1,6 @@ -%%~ db7e733775d4d:b3643d0f92e32:NOVEL:PARTITION:Act One +%%~name: Act One +%%~path: b3643d0f92e32/db7e733775d4d +%%~kind: NOVEL/PARTITION # Act One “Fusce maximus felis libero” \ No newline at end of file diff --git a/tests/lipsum/content/eb103bc70c90c.nwd b/tests/lipsum/content/eb103bc70c90c.nwd index db11bbf0..65ce7e49 100644 --- a/tests/lipsum/content/eb103bc70c90c.nwd +++ b/tests/lipsum/content/eb103bc70c90c.nwd @@ -1,4 +1,6 @@ -%%~ eb103bc70c90c:6bd935d2490cd:b3643d0f92e32:NOVEL:SCENE:Scene Three +%%~name: Scene Three +%%~path: 6bd935d2490cd/eb103bc70c90c +%%~kind: NOVEL/SCENE ### Scene Three @pov: Bod diff --git a/tests/lipsum/content/f8c0562e50f1b.nwd b/tests/lipsum/content/f8c0562e50f1b.nwd index 15a33bc8..f8218e1f 100644 --- a/tests/lipsum/content/f8c0562e50f1b.nwd +++ b/tests/lipsum/content/f8c0562e50f1b.nwd @@ -1,4 +1,6 @@ -%%~ f8c0562e50f1b:6bd935d2490cd:b3643d0f92e32:NOVEL:SCENE:Scene Four +%%~name: Scene Four +%%~path: 6bd935d2490cd/f8c0562e50f1b +%%~kind: NOVEL/SCENE ### Scene Four @pov: Bod diff --git a/tests/lipsum/content/f96ec11c6a3da.nwd b/tests/lipsum/content/f96ec11c6a3da.nwd index b95163d5..60853dc2 100644 --- a/tests/lipsum/content/f96ec11c6a3da.nwd +++ b/tests/lipsum/content/f96ec11c6a3da.nwd @@ -1,4 +1,6 @@ -%%~ f96ec11c6a3da:45e6b01ca35c1:b3643d0f92e32:NOVEL:SCENE:Scene Two +%%~name: Scene Two +%%~path: 45e6b01ca35c1/f96ec11c6a3da +%%~kind: NOVEL/SCENE ### Scene Two @pov: Bod diff --git a/tests/lipsum/content/fb609cd8319dc.nwd b/tests/lipsum/content/fb609cd8319dc.nwd index fa9626f2..ff48ad12 100644 --- a/tests/lipsum/content/fb609cd8319dc.nwd +++ b/tests/lipsum/content/fb609cd8319dc.nwd @@ -1,4 +1,6 @@ -%%~ fb609cd8319dc:45e6b01ca35c1:b3643d0f92e32:NOVEL:CHAPTER:Chapter One +%%~name: Chapter One +%%~path: 45e6b01ca35c1/fb609cd8319dc +%%~kind: NOVEL/CHAPTER ## Chapter One @pov: Bod diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index 680059d5..5dec55bb 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,19 +1,19 @@ - + Lorem Ipsum Lorem Ipsum lipsum.com - 9 + 10 22 - 1552 + 1571 False False None True - 846352075de7d + 04468803b92e1 None 3847 3109 diff --git a/tests/minimal/content/8c659a11cd429.nwd b/tests/minimal/content/8c659a11cd429.nwd index 5ecf5c59..bdb8079f 100644 --- a/tests/minimal/content/8c659a11cd429.nwd +++ b/tests/minimal/content/8c659a11cd429.nwd @@ -1,3 +1,5 @@ -%%~ 8c659a11cd429:a6d311a93600a:a508bb932959c:NOVEL:SCENE:New Scene +%%~name: New Scene +%%~path: a6d311a93600a/8c659a11cd429 +%%~kind: NOVEL/SCENE ### New Scene diff --git a/tests/minimal/content/a35baf2e93843.nwd b/tests/minimal/content/a35baf2e93843.nwd index a7745e3b..a1120152 100644 --- a/tests/minimal/content/a35baf2e93843.nwd +++ b/tests/minimal/content/a35baf2e93843.nwd @@ -1,4 +1,6 @@ -%%~ a35baf2e93843:a508bb932959c:NOVEL:TITLE:Title Page +%%~name: Title Page +%%~path: a508bb932959c/a35baf2e93843 +%%~kind: NOVEL/TITLE # Minimal By Jane Doe, John Doh diff --git a/tests/minimal/content/f5ab3e30151e1.nwd b/tests/minimal/content/f5ab3e30151e1.nwd index ba1c9faa..f08335a0 100644 --- a/tests/minimal/content/f5ab3e30151e1.nwd +++ b/tests/minimal/content/f5ab3e30151e1.nwd @@ -1,3 +1,5 @@ -%%~ f5ab3e30151e1:a6d311a93600a:a508bb932959c:NOVEL:CHAPTER:New Chapter +%%~name: New Chapter +%%~path: a6d311a93600a/f5ab3e30151e1 +%%~kind: NOVEL/CHAPTER ## New Chapter diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index af99a8ce..d45f9bfa 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -1,17 +1,18 @@ - + Test Minimal Minimal Jane Doe John Doh - 1 + 3 1 - 8 + 33 True False + None True None None @@ -57,7 +58,7 @@ 28 6 1 - 0 + 33
New Chapter @@ -76,7 +77,7 @@ 11 2 0 - 0 + 16 New Scene @@ -88,7 +89,7 @@ 9 2 0 - 0 + 15 Plot diff --git a/tests/reference/gui/1_031b4af5197ec.nwd b/tests/reference/gui/1_031b4af5197ec.nwd index 9a330c9c..acb36501 100644 --- a/tests/reference/gui/1_031b4af5197ec.nwd +++ b/tests/reference/gui/1_031b4af5197ec.nwd @@ -1,4 +1,6 @@ -%%~ 031b4af5197ec:44cb730c42048:PLOT:NOTE:New File +%%~name: New File +%%~path: 44cb730c42048/031b4af5197ec +%%~kind: PLOT/NOTE # Main Plot @tag: MainPlot diff --git a/tests/reference/gui/1_0e17daca5f3e1.nwd b/tests/reference/gui/1_0e17daca5f3e1.nwd index 3475abd2..bf24dbe6 100644 --- a/tests/reference/gui/1_0e17daca5f3e1.nwd +++ b/tests/reference/gui/1_0e17daca5f3e1.nwd @@ -1,4 +1,6 @@ -%%~ 0e17daca5f3e1:31489056e0916:73475cb40a568:NOVEL:SCENE:New Scene +%%~name: New Scene +%%~path: 31489056e0916/0e17daca5f3e1 +%%~kind: NOVEL/SCENE # Novel ## Chapter diff --git a/tests/reference/gui/1_1a6562590ef19.nwd b/tests/reference/gui/1_1a6562590ef19.nwd index de40e292..9a3ca0a9 100644 --- a/tests/reference/gui/1_1a6562590ef19.nwd +++ b/tests/reference/gui/1_1a6562590ef19.nwd @@ -1,4 +1,6 @@ -%%~ 1a6562590ef19:71ee45a3c0db9:CHARACTER:NOTE:New File +%%~name: New File +%%~path: 71ee45a3c0db9/1a6562590ef19 +%%~kind: CHARACTER/NOTE # Jane Doe @tag: Jane diff --git a/tests/reference/gui/1_41cfc0d1f2d12.nwd b/tests/reference/gui/1_41cfc0d1f2d12.nwd index e80cbd1b..8e8cb037 100644 --- a/tests/reference/gui/1_41cfc0d1f2d12.nwd +++ b/tests/reference/gui/1_41cfc0d1f2d12.nwd @@ -1,4 +1,6 @@ -%%~ 41cfc0d1f2d12:811786ad1ae74:WORLD:NOTE:New File +%%~name: New File +%%~path: 811786ad1ae74/41cfc0d1f2d12 +%%~kind: WORLD/NOTE # Main Location @tag: Home diff --git a/tests/reference/gui/4_73475cb40a568.nwd b/tests/reference/gui/4_73475cb40a568.nwd index 5d18b581..5a903143 100644 --- a/tests/reference/gui/4_73475cb40a568.nwd +++ b/tests/reference/gui/4_73475cb40a568.nwd @@ -1,4 +1,6 @@ -%%~ 73475cb40a568:b3643d0f92e32:NOVEL:SCENE:Chapter One +%%~name: Chapter One +%%~path: b3643d0f92e32/73475cb40a568 +%%~kind: NOVEL/SCENE ## Chapter One @pov: Bod diff --git a/tests/reference/gui/5_031b4af5197ec.nwd b/tests/reference/gui/5_031b4af5197ec.nwd index bc9d9f7d..cbaf3205 100644 --- a/tests/reference/gui/5_031b4af5197ec.nwd +++ b/tests/reference/gui/5_031b4af5197ec.nwd @@ -1,4 +1,6 @@ -%%~ 031b4af5197ec:0e17daca5f3e1:b3643d0f92e32:NOVEL:SCENE:Scene One +%%~name: Scene One +%%~path: 0e17daca5f3e1/031b4af5197ec +%%~kind: NOVEL/SCENE ### Scene One @pov: Bod diff --git a/tests/reference/gui/5_25fc0e7096fc6.nwd b/tests/reference/gui/5_25fc0e7096fc6.nwd index d84c563a..3247412b 100644 --- a/tests/reference/gui/5_25fc0e7096fc6.nwd +++ b/tests/reference/gui/5_25fc0e7096fc6.nwd @@ -1,4 +1,6 @@ -%%~ 25fc0e7096fc6:811786ad1ae74:b3643d0f92e32:NOVEL:CHAPTER:Chapter One +%%~name: Chapter One +%%~path: 811786ad1ae74/25fc0e7096fc6 +%%~kind: NOVEL/CHAPTER ## Chapter One @pov: Bod diff --git a/tests/reference/gui/5_2858dcd1057d3.nwd b/tests/reference/gui/5_2858dcd1057d3.nwd index 198fd30a..c1e79bdf 100644 --- a/tests/reference/gui/5_2858dcd1057d3.nwd +++ b/tests/reference/gui/5_2858dcd1057d3.nwd @@ -1,4 +1,6 @@ -%%~ 2858dcd1057d3:0e17daca5f3e1:b3643d0f92e32:NOVEL:SCENE:Scene Two +%%~name: Scene Two +%%~path: 0e17daca5f3e1/2858dcd1057d3 +%%~kind: NOVEL/SCENE ### Scene Two @pov: Bod diff --git a/tests/reference/gui/5_2fca346db6561.nwd b/tests/reference/gui/5_2fca346db6561.nwd index 93d611ff..c33e7901 100644 --- a/tests/reference/gui/5_2fca346db6561.nwd +++ b/tests/reference/gui/5_2fca346db6561.nwd @@ -1,4 +1,6 @@ -%%~ 2fca346db6561:0e17daca5f3e1:b3643d0f92e32:NOVEL:SCENE:Scene Two, Section Two +%%~name: Scene Two, Section Two +%%~path: 0e17daca5f3e1/2fca346db6561 +%%~kind: NOVEL/SCENE #### Scene Two, Section Two Suspendisse potenti. Fusce tempus lorem nec laoreet suscipit. Fusce vulputate nisl ac diam tincidunt, nec malesuada quam pellentesque. Maecenas congue, tellus quis commodo rutrum, magna leo egestas arcu, quis suscipit ex risus id ligula. Suspendisse potenti. Morbi blandit lacus vitae laoreet vulputate. Donec vitae tellus eleifend, lobortis eros eu, tincidunt enim. Nullam et ullamcorper nisi. Vivamus tellus ex, lobortis quis rutrum ut, dapibus sit amet turpis. Phasellus pellentesque metus diam, commodo tristique ante commodo ac. Ut mollis ipsum nec diam blandit sollicitudin. Duis bibendum lacus nec commodo dapibus. Sed condimentum luctus ante, id ultricies urna varius nec. Nam convallis magna nec bibendum ultrices. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed auctor pharetra quam, vitae porta ex bibendum eu. diff --git a/tests/reference/gui/5_31489056e0916.nwd b/tests/reference/gui/5_31489056e0916.nwd index 7c26cdb5..e494f6b9 100644 --- a/tests/reference/gui/5_31489056e0916.nwd +++ b/tests/reference/gui/5_31489056e0916.nwd @@ -1,4 +1,6 @@ -%%~ 31489056e0916:811786ad1ae74:b3643d0f92e32:NOVEL:SCENE:Scene One +%%~name: Scene One +%%~path: 811786ad1ae74/31489056e0916 +%%~kind: NOVEL/SCENE ### Scene One @pov: Bod diff --git a/tests/reference/gui/5_41cfc0d1f2d12.nwd b/tests/reference/gui/5_41cfc0d1f2d12.nwd index a39fc1c1..6f887f44 100644 --- a/tests/reference/gui/5_41cfc0d1f2d12.nwd +++ b/tests/reference/gui/5_41cfc0d1f2d12.nwd @@ -1,4 +1,6 @@ -%%~ 41cfc0d1f2d12:0e17daca5f3e1:b3643d0f92e32:NOVEL:SCENE:Scene One, Section Two +%%~name: Scene One, Section Two +%%~path: 0e17daca5f3e1/41cfc0d1f2d12 +%%~kind: NOVEL/SCENE #### Scene One, Section Two Integer vel libero ipsum. Donec varius aliquam libero, sit amet commodo urna hendrerit non. Nullam quis erat mollis nunc viverra volutpat tincidunt in odio. Nam vitae quam sem. Aliquam suscipit nulla non lorem pharetra semper. Ut suscipit erat eu ligula accumsan ultrices. Phasellus nisl tellus, placerat sed laoreet id, consectetur nec dolor. Sed fringilla ipsum id dapibus posuere. Aenean finibus pharetra tincidunt. Ut molestie malesuada nulla, id posuere lorem tincidunt eu. Aliquam tempor eros a est vulputate, scelerisque pulvinar ipsum fermentum. In hac habitasse platea dictumst. diff --git a/tests/reference/gui/5_98010bd9270f9.nwd b/tests/reference/gui/5_98010bd9270f9.nwd index 4afe14ea..0725f6a5 100644 --- a/tests/reference/gui/5_98010bd9270f9.nwd +++ b/tests/reference/gui/5_98010bd9270f9.nwd @@ -1,4 +1,6 @@ -%%~ 98010bd9270f9:811786ad1ae74:b3643d0f92e32:NOVEL:SCENE:Scene Two +%%~name: Scene Two +%%~path: 811786ad1ae74/98010bd9270f9 +%%~kind: NOVEL/SCENE ### Scene Two @pov: Bod diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index e223adae..81c1fc89 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -640,7 +640,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef testFile = os.path.join(nwTempGUI, "4_71ee45a3c0db9.nwd") refFile = os.path.join(nwRef, "gui", "4_73475cb40a568.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, refFile, [1]) + assert cmpFiles(testFile, refFile, [1, 2, 3]) # Split By Scene assert nwGUI.treeView.setSelectedHandle("73475cb40a568") @@ -702,7 +702,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef testFile = os.path.join(nwTempGUI, "5_25fc0e7096fc6.nwd") refFile = os.path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, refFile, [1]) + assert cmpFiles(testFile, refFile, [1, 2, 3]) projFile = os.path.join(nwLipsum, "content", "031b4af5197ec.nwd") testFile = os.path.join(nwTempGUI, "5_031b4af5197ec.nwd") diff --git a/tests/test_gui.py b/tests/test_gui.py index 0d9849d6..b1dac72f 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -684,16 +684,6 @@ def testProjectTree(qtbot, yesToAll, nwMinimal, nwTemp): orItem = nwTree._getTreeItem("1234567890abc") assert orItem.text(nwTree.C_NAME) == "Orphaned File 1" - # Move it to the Plot folder - # plItem = nwTree._getTreeItem("7695ce551d265") - # orRect = nwTree.visualItemRect(orItem) - # plRect = nwTree.visualItemRect(plItem) - - # qtbot.mouseMove(nwTree.viewport(), pos=orRect.center(), delay=1000) - # qtbot.mousePress(nwTree.viewport(), Qt.LeftButton, pos=orRect.center(), delay=1000) - # qtbot.mouseMove(nwTree.viewport(), pos=plRect.center(), delay=1000) - # qtbot.mouseRelease(nwTree.viewport(), Qt.LeftButton, pos=plRect.center(), delay=1000) - # qtbot.stopForInteraction() nwGUI.closeMain() nwGUI.close() diff --git a/tests/test_project.py b/tests/test_project.py index e8f1442b..dbe414bb 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -335,20 +335,17 @@ def testDocMeta(nwDummy, nwLipsum): aDoc = NWDoc(theProject, nwDummy) assert aDoc.openDocument("47666c91c7ccf") - theMeta, thePath, theClass, theLayout = aDoc.getMeta() + theName, theParent, theClass, theLayout = aDoc.getMeta() - assert theMeta == "Scene Five" - assert len(thePath) == 3 - assert thePath[0] == "47666c91c7ccf" - assert thePath[1] == "6bd935d2490cd" - assert thePath[2] == "b3643d0f92e32" + assert theName == "Scene Five" + assert theParent == "6bd935d2490cd" assert theClass == nwItemClass.NOVEL assert theLayout == nwItemLayout.SCENE - aDoc._docMeta = "too_short" - theMeta, thePath, theClass, theLayout = aDoc.getMeta() - assert theMeta == "" - assert thePath == [] + aDoc._docMeta = {"stuff": None} + theName, theParent, theClass, theLayout = aDoc.getMeta() + assert theName == "" + assert theParent is None assert theClass is None assert theLayout is None @@ -488,7 +485,9 @@ def testProjectOrphanedFiles(nwDummy, nwLipsum): # First Item with Meta Data orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd") with open(orphPath, mode="w", encoding="utf8") as outFile: - outFile.write(r"%%~ 5eaea4e8cdee8:15c4492bd5107:WORLD:NOTE:Mars") + outFile.write("%%~name:Mars\n") + outFile.write("%%~path:5eaea4e8cdee8/636b6aa9b697b\n") + outFile.write("%%~kind:WORLD/NOTE\n") outFile.write("\n") # Second Item without Meta Data From 79d345e0f8f3e426f30e7f8c3a01a4ab3c692ae8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 24 Oct 2020 18:57:22 +0200 Subject: [PATCH 102/104] Updated sample project --- sample/content/14298de4d9524.nwd | 4 +++- sample/content/53b69b83cdafc.nwd | 4 +++- sample/content/5eaea4e8cdee8.nwd | 4 +++- sample/content/636b6aa9b697b.nwd | 1 - sample/content/6a2d6d5f4f401.nwd | 4 +++- sample/content/88706ddc78b1b.nwd | 4 +++- sample/content/8a5deb88c0e97.nwd | 4 +++- sample/content/96b68994dfa3d.nwd | 4 +++- sample/content/974e400180a99.nwd | 4 +++- sample/content/ae7339df26ded.nwd | 4 +++- sample/content/b3e74dbc1f584.nwd | 4 +++- sample/content/b8136a5a774a0.nwd | 4 +++- sample/content/ba8a28a246524.nwd | 4 +++- sample/content/bb2c23b3c42cc.nwd | 4 +++- sample/content/bc0cbd2a407f3.nwd | 4 +++- sample/content/edca4be2fcaf8.nwd | 4 +++- sample/content/f1471bef9f2ae.nwd | 4 +++- sample/nwProject.nwx | 6 +++--- 18 files changed, 51 insertions(+), 20 deletions(-) diff --git a/sample/content/14298de4d9524.nwd b/sample/content/14298de4d9524.nwd index af6c4e50..732e2078 100644 --- a/sample/content/14298de4d9524.nwd +++ b/sample/content/14298de4d9524.nwd @@ -1,4 +1,6 @@ -%%~ 14298de4d9524:f7e2d9f330615:f6622b4617424:CHARACTER:NOTE:John Smith +%%~name: John Smith +%%~path: f7e2d9f330615/14298de4d9524 +%%~kind: CHARACTER/NOTE # John Smith @tag: John diff --git a/sample/content/53b69b83cdafc.nwd b/sample/content/53b69b83cdafc.nwd index b25ce403..f33a2a98 100644 --- a/sample/content/53b69b83cdafc.nwd +++ b/sample/content/53b69b83cdafc.nwd @@ -1,4 +1,6 @@ -%%~ 53b69b83cdafc:7031beac91f75:NOVEL:TITLE:Title Page +%%~name: Title Page +%%~path: 7031beac91f75/53b69b83cdafc +%%~kind: NOVEL/TITLE # My Novel **By Jane Doh** diff --git a/sample/content/5eaea4e8cdee8.nwd b/sample/content/5eaea4e8cdee8.nwd index ba951a2b..1a7f3c79 100644 --- a/sample/content/5eaea4e8cdee8.nwd +++ b/sample/content/5eaea4e8cdee8.nwd @@ -1,4 +1,6 @@ -%%~ 5eaea4e8cdee8:15c4492bd5107:WORLD:NOTE:Mars +%%~name: Mars +%%~path: 15c4492bd5107/5eaea4e8cdee8 +%%~kind: WORLD/NOTE # Mars @tag: Mars diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index eb0545cf..6a1302ef 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -1,7 +1,6 @@ %%~name: Making a Scene %%~path: e7ded148d6e4a/636b6aa9b697b %%~kind: NOVEL/SCENE -%%~ ### Making a Scene @pov: Jane diff --git a/sample/content/6a2d6d5f4f401.nwd b/sample/content/6a2d6d5f4f401.nwd index 583f503f..ad4554d8 100644 --- a/sample/content/6a2d6d5f4f401.nwd +++ b/sample/content/6a2d6d5f4f401.nwd @@ -1,4 +1,6 @@ -%%~ 6a2d6d5f4f401:e7ded148d6e4a:7031beac91f75:NOVEL:CHAPTER:Chapter One +%%~name: Chapter One +%%~path: e7ded148d6e4a/6a2d6d5f4f401 +%%~kind: NOVEL/CHAPTER ## So it Begins @pov: Jane diff --git a/sample/content/88706ddc78b1b.nwd b/sample/content/88706ddc78b1b.nwd index 4f9a8d43..fbb5794a 100644 --- a/sample/content/88706ddc78b1b.nwd +++ b/sample/content/88706ddc78b1b.nwd @@ -1,4 +1,6 @@ -%%~ 88706ddc78b1b:e7ded148d6e4a:7031beac91f75:NOVEL:CHAPTER:Chapter Two +%%~name: Chapter Two +%%~path: e7ded148d6e4a/88706ddc78b1b +%%~kind: NOVEL/CHAPTER ## Where has John Gone? @pov: Jane diff --git a/sample/content/8a5deb88c0e97.nwd b/sample/content/8a5deb88c0e97.nwd index 2ed2609f..bc504b02 100644 --- a/sample/content/8a5deb88c0e97.nwd +++ b/sample/content/8a5deb88c0e97.nwd @@ -1,4 +1,6 @@ -%%~ 8a5deb88c0e97:6827118336ac1:NOVEL:SCENE:Old File +%%~name: Old File +%%~path: ae9bf3c3ea159/8a5deb88c0e97 +%%~kind: NOVEL/SCENE ### Discarded Scene If you have files you no longer want in your main project, you can move them to the “Outtakes” folder. This is equivalent to turning off the “Include when building project” switch, just that you also put the file away, although the switch can be ignored when building the project, this folder cannot. diff --git a/sample/content/96b68994dfa3d.nwd b/sample/content/96b68994dfa3d.nwd index 6ab833fd..849cd41d 100644 --- a/sample/content/96b68994dfa3d.nwd +++ b/sample/content/96b68994dfa3d.nwd @@ -1,4 +1,6 @@ -%%~ 96b68994dfa3d:e7ded148d6e4a:7031beac91f75:NOVEL:NOTE:A Note on Structure +%%~name: A Note on Structure +%%~path: e7ded148d6e4a/96b68994dfa3d +%%~kind: NOVEL/NOTE # A Note on Structure This file is just a note. You can save notes anywhere you like in the project tree. Notes can be filtered out when you export the project. diff --git a/sample/content/974e400180a99.nwd b/sample/content/974e400180a99.nwd index 07ca7817..0a4f5df8 100644 --- a/sample/content/974e400180a99.nwd +++ b/sample/content/974e400180a99.nwd @@ -1,4 +1,6 @@ -%%~ 974e400180a99:7031beac91f75:NOVEL:PAGE:Page +%%~name: Page +%%~path: 7031beac91f75/974e400180a99 +%%~kind: NOVEL/PAGE This is a plain page with some text on it. This file should receive no special formatting, but the text will always be left aligned and the content will always start on a fresh page when the project is exported. diff --git a/sample/content/ae7339df26ded.nwd b/sample/content/ae7339df26ded.nwd index ea4bb9ce..0443184e 100644 --- a/sample/content/ae7339df26ded.nwd +++ b/sample/content/ae7339df26ded.nwd @@ -1,4 +1,6 @@ -%%~ ae7339df26ded:e7ded148d6e4a:7031beac91f75:NOVEL:SCENE:We Found John! +%%~name: We Found John! +%%~path: e7ded148d6e4a/ae7339df26ded +%%~kind: NOVEL/SCENE ### We Found John! @pov: John diff --git a/sample/content/b3e74dbc1f584.nwd b/sample/content/b3e74dbc1f584.nwd index a7a8ba8a..6931a299 100644 --- a/sample/content/b3e74dbc1f584.nwd +++ b/sample/content/b3e74dbc1f584.nwd @@ -1,4 +1,6 @@ -%%~ b3e74dbc1f584:15c4492bd5107:WORLD:NOTE:Earth +%%~name: Earth +%%~path: 15c4492bd5107/b3e74dbc1f584 +%%~kind: WORLD/NOTE # Earth @tag: Earth diff --git a/sample/content/b8136a5a774a0.nwd b/sample/content/b8136a5a774a0.nwd index 8c6ddb77..8badd9bb 100644 --- a/sample/content/b8136a5a774a0.nwd +++ b/sample/content/b8136a5a774a0.nwd @@ -1,4 +1,6 @@ -%%~ b8136a5a774a0:98acd8c76c93a:NOVEL:SCENE:Delete Me! +%%~name: Delete Me! +%%~path: 98acd8c76c93a/b8136a5a774a0 +%%~kind: NOVEL/SCENE ### Delete Me! This scene is trash. \ No newline at end of file diff --git a/sample/content/ba8a28a246524.nwd b/sample/content/ba8a28a246524.nwd index 8cc5ae6c..83d441fc 100644 --- a/sample/content/ba8a28a246524.nwd +++ b/sample/content/ba8a28a246524.nwd @@ -1,4 +1,6 @@ -%%~ ba8a28a246524:e7ded148d6e4a:7031beac91f75:NOVEL:UNNUMBERED:Interlude +%%~name: Interlude +%%~path: e7ded148d6e4a/ba8a28a246524 +%%~kind: NOVEL/UNNUMBERED ## Interlude % Notice that this is a file with the flag ‘N.Un’. The ‘N’ means it’s a novel file, and the ‘Un’ means it’s an unnumbered chapter. Unnumbered chapters can be treated separately from numbered chapters during export. Perfect for when you want to add an interlude, or for a prologue or epilogue. diff --git a/sample/content/bb2c23b3c42cc.nwd b/sample/content/bb2c23b3c42cc.nwd index 126f549b..9827b61e 100644 --- a/sample/content/bb2c23b3c42cc.nwd +++ b/sample/content/bb2c23b3c42cc.nwd @@ -1,4 +1,6 @@ -%%~ bb2c23b3c42cc:f7e2d9f330615:f6622b4617424:CHARACTER:NOTE:Jane Smith +%%~name: Jane Smith +%%~path: f7e2d9f330615/bb2c23b3c42cc +%%~kind: CHARACTER/NOTE # Jane Smith @tag: Jane diff --git a/sample/content/bc0cbd2a407f3.nwd b/sample/content/bc0cbd2a407f3.nwd index 4a099dfc..17524248 100644 --- a/sample/content/bc0cbd2a407f3.nwd +++ b/sample/content/bc0cbd2a407f3.nwd @@ -1,4 +1,6 @@ -%%~ bc0cbd2a407f3:e7ded148d6e4a:7031beac91f75:NOVEL:SCENE:Another Scene +%%~name: Another Scene +%%~path: e7ded148d6e4a/bc0cbd2a407f3 +%%~kind: NOVEL/SCENE ### Another Scene @pov: John diff --git a/sample/content/edca4be2fcaf8.nwd b/sample/content/edca4be2fcaf8.nwd index 0200ab75..a033257d 100644 --- a/sample/content/edca4be2fcaf8.nwd +++ b/sample/content/edca4be2fcaf8.nwd @@ -1,4 +1,6 @@ -%%~ edca4be2fcaf8:7031beac91f75:NOVEL:PARTITION:Part One +%%~name: Part One +%%~path: 7031beac91f75/edca4be2fcaf8 +%%~kind: NOVEL/PARTITION # Part One The first part. \ No newline at end of file diff --git a/sample/content/f1471bef9f2ae.nwd b/sample/content/f1471bef9f2ae.nwd index 32db9cb0..afc65f03 100644 --- a/sample/content/f1471bef9f2ae.nwd +++ b/sample/content/f1471bef9f2ae.nwd @@ -1,4 +1,6 @@ -%%~ f1471bef9f2ae:15c4492bd5107:WORLD:NOTE:Space +%%~name: Space +%%~path: 15c4492bd5107/f1471bef9f2ae +%%~kind: WORLD/NOTE # Space @tag: Space diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index fa481af6..927a4cf9 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 765 + 766 148 - 37663 + 37687 False From de8ee126cf9725954014a773c8cd7f1cfb2da4e7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 24 Oct 2020 19:08:26 +0200 Subject: [PATCH 103/104] Add some debug output to the document meta parser --- nw/core/document.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nw/core/document.py b/nw/core/document.py index 2cbb16bd..2e22e389 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -250,7 +250,9 @@ class NWDoc(): if metaBits[1] in nwItemLayout.__members__: self._docMeta["layout"] = nwItemLayout[metaBits[1]] - # print(self._docMeta) + else: + logger.debug("Ignoring meta data: '%s'" % metaLine) + return # END Class NWDoc From f083ada3b3853af2f35a012120438491927ff995 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 24 Oct 2020 19:13:39 +0200 Subject: [PATCH 104/104] Add coverage of else condition in doc meta parser --- tests/test_project.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_project.py b/tests/test_project.py index dbe414bb..9be85a67 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -488,6 +488,7 @@ def testProjectOrphanedFiles(nwDummy, nwLipsum): outFile.write("%%~name:Mars\n") outFile.write("%%~path:5eaea4e8cdee8/636b6aa9b697b\n") outFile.write("%%~kind:WORLD/NOTE\n") + outFile.write("%%~invalid\n") outFile.write("\n") # Second Item without Meta Data