From 399b13dd3dd3021ea878fdd5cdaba04275a3c9ca Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 22:27:06 +0200 Subject: [PATCH 01/67] Make both panes use viewportmargin --- nw/gui/doceditor.py | 1 - nw/gui/docviewer.py | 17 +++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 5e0fb025..efe863a7 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -351,7 +351,6 @@ class GuiDocEditor(QTextEdit): else: rH = 0 - # print(tM, tH, rH, max(tM, tH, rH)) self.setViewportMargins(tM, max(cM, tH, rH), tM, cM) return diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 8c2ea236..b280aa01 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -60,6 +60,7 @@ class GuiDocViewer(QTextBrowser): self.qDocument = self.document() self.setMinimumWidth(self.mainConf.pxInt(300)) + self.setAutoFillBackground(True) self.setOpenExternalLinks(False) self.initViewer() @@ -104,11 +105,12 @@ class GuiDocViewer(QTextBrowser): self.setFont(theFont) docPalette = self.palette() + docPalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack)) docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) self.setPalette(docPalette) - self.qDocument.setDocumentMargin(self.mainConf.getTextMargin()) + self.qDocument.setDocumentMargin(0) theOpt = QTextOption() if self.mainConf.doJustify: theOpt.setAlignment(Qt.AlignJustify) @@ -242,19 +244,10 @@ class GuiDocViewer(QTextBrowser): tH = self.docHeader.height() fH = self.docFooter.height() fY = self.height() - fH - tB - tT = cM - tH - bT = cM - fH + self.docHeader.setGeometry(tB, tB, tW, tH) self.docFooter.setGeometry(tB, fY, tW, fH) - self.setViewportMargins(0, tH, 0, fH) - - docFormat = self.qDocument.rootFrame().frameFormat() - docFormat.setTopMargin(max(0, tT)) - docFormat.setBottomMargin(max(0, bT)) - - self.qDocument.blockSignals(True) - self.qDocument.rootFrame().setFrameFormat(docFormat) - self.qDocument.blockSignals(False) + self.setViewportMargins(cM, max(cM, tH), cM, max(cM, fH)) return From 1978d50e7e9ed7f99ddc2469329797f3e6043348 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 22:39:01 +0200 Subject: [PATCH 02/67] Some minor cleanup in doceditor --- nw/gui/doceditor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index efe863a7..ab76adfb 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -78,6 +78,9 @@ class GuiDocEditor(QTextEdit): self.bigDoc = False self.nonWord = "\"'" + # Document State + self.hasSelection = False + # Typography self.typDQOpen = self.mainConf.fmtDoubleQuotes[0] self.typDQClose = self.mainConf.fmtDoubleQuotes[1] @@ -86,7 +89,6 @@ class GuiDocEditor(QTextEdit): # Core Elements self.qDocument = self.document() - self.qDocument.setDocumentMargin(self.mainConf.getTextMargin()) self.qDocument.contentsChange.connect(self._docChange) # Document Title @@ -100,8 +102,7 @@ class GuiDocEditor(QTextEdit): self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self._openContextMenu) - # Editor State - self.hasSelection = False + # Editor Settings self.setMinimumWidth(self.mainConf.pxInt(300)) self.setAutoFillBackground(True) self.setAcceptRichText(False) From bbd6e708681fecd3fcb549e0252d91e23f753d40 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 22:39:52 +0200 Subject: [PATCH 03/67] Forgot to save the file ... --- 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 ab76adfb..eadce6ff 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -594,7 +594,8 @@ class GuiDocEditor(QTextEdit): """ self.hasSelection = self.textCursor().hasSelection() if self.docSearch.searchBox.hasFocus(): - # Block the event when the focus is on the search bar. + # Block the event when the focus is on the search bar as it + # seems to be capturing the return key. return if keyEvent.modifiers() == Qt.ShiftModifier: From a64d867ed42750948ff58f643be4ca846f13e631 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 19:30:02 +0200 Subject: [PATCH 04/67] Added a quote symbol selection dialog --- nw/constants/constants.py | 45 +++++++++-------- nw/gui/custom.py | 104 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 124 insertions(+), 25 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 9bd47110..b05f8e6d 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -169,27 +169,30 @@ class nwQuotes(): Source: https://en.wikipedia.org/wiki/Quotation_mark """ - SYMBOLS = [ - "\u0022", # Quotation mark - "\u0027", # Apostrophe - "\u00ab", # Left-pointing double angle quotation mark - "\u00bb", # Right-pointing double angle quotation mark - "\u2018", # Left single quotation mark - "\u2019", # Right single quotation mark - "\u201a", # Single low-9 quotation mark - "\u201b", # Single high-reversed-9 quotation mark - "\u201c", # Left double quotation mark - "\u201d", # Right double quotation mark - "\u201e", # Double low-9 quotation mark - "\u201f", # Double high-reversed-9 quotation mark - "\u2039", # Single left-pointing angle quotation mark - "\u203a", # Single right-pointing angle quotation mark - "\u2e42", # Double low-reversed-9 quotation mark - "\u300c", # Left corner bracket - "\u300d", # Right corner bracket - "\u300e", # Left white corner bracket - "\u300f", # Right white corner bracket - ] + SYMBOLS = { + "\u0027" : "Straight single quotation mark", + "\u0022" : "Straight double quotation mark", + + "\u2018" : "Left single quotation mark", + "\u2019" : "Right single quotation mark", + "\u201a" : "Single low-9 quotation mark", + "\u201b" : "Single high-reversed-9 quotation mark", + "\u201c" : "Left double quotation mark", + "\u201d" : "Right double quotation mark", + "\u201e" : "Double low-9 quotation mark", + "\u201f" : "Double high-reversed-9 quotation mark", + "\u2e42" : "Double low-reversed-9 quotation mark", + + "\u2039" : "Single left-pointing angle quotation mark", + "\u203a" : "Single right-pointing angle quotation mark", + "\u00ab" : "Left-pointing double angle quotation mark", + "\u00bb" : "Right-pointing double angle quotation mark", + + "\u300c" : "Left corner bracket", + "\u300d" : "Right corner bracket", + "\u300e" : "Left white corner bracket", + "\u300f" : "Right white corner bracket", + } # END Class nwQuotes diff --git a/nw/gui/custom.py b/nw/gui/custom.py index 5c3dfbd6..051fa1f3 100644 --- a/nw/gui/custom.py +++ b/nw/gui/custom.py @@ -30,17 +30,17 @@ import logging import nw -from PyQt5.QtGui import QColor, QPalette, QPainter +from PyQt5.QtGui import QColor, QPalette, QPainter, QFontMetrics from PyQt5.QtCore import ( Qt, QRect, QPoint, QSize, QRectF, QPropertyAnimation, pyqtProperty ) from PyQt5.QtWidgets import ( QGridLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QSizePolicy, - QAbstractButton, QDialog, QTabWidget, QTabBar, QStyle, - QStylePainter, QStyleOptionTab + QAbstractButton, QDialog, QTabWidget, QTabBar, QStyle, QDialogButtonBox, + QStylePainter, QStyleOptionTab, QListWidget, QListWidgetItem, QFrame ) -from nw.constants import nwUnicode +from nw.constants import nwUnicode, nwQuotes logger = logging.getLogger(__name__) @@ -436,3 +436,99 @@ class VerticalTabBar(QTabBar): return # END Class VerticalTabBar + +# =============================================================================================== # +# Quotes Dialog +# =============================================================================================== # + +class QuotesDialog(QDialog): + + def __init__(self, theParent=None, currentQuote="\""): + QDialog.__init__(self, parent=theParent) + + self.mainConf = nw.CONFIG + + self.outerBox = QVBoxLayout() + self.innerBox = QHBoxLayout() + self.labelBox = QVBoxLayout() + + self.selectedQuote = currentQuote + + qMetrics = QFontMetrics(self.font()) + pxW = 7*qMetrics.boundingRectChar("M").width() + pxH = 7*qMetrics.boundingRectChar("M").height() + pxH = 7*qMetrics.boundingRectChar("M").height() + + lblFont = self.font() + lblFont.setPointSizeF(4*lblFont.pointSizeF()) + + # Preview Label + self.previewLabel = QLabel(currentQuote) + self.previewLabel.setFont(lblFont) + self.previewLabel.setFixedSize(QSize(pxW, pxH)) + self.previewLabel.setAlignment(Qt.AlignCenter) + self.previewLabel.setFrameStyle(QFrame.Box | QFrame.Plain) + + # Quote Symbols + self.listBox = QListWidget() + self.listBox.itemSelectionChanged.connect(self._selectedSymbol) + + minSize = 100 + for sKey, sLabel in nwQuotes.SYMBOLS.items(): + theText = "[ %s ] %s" % (sKey, sLabel) + minSize = max(minSize, qMetrics.boundingRect(theText).width()) + qtItem = QListWidgetItem(theText) + qtItem.setData(Qt.UserRole, sKey) + self.listBox.addItem(qtItem) + if sKey == currentQuote: + self.listBox.setCurrentItem(qtItem) + + self.listBox.setMinimumWidth(minSize + self.mainConf.pxInt(40)) + self.listBox.setMinimumHeight(self.mainConf.pxInt(150)) + + # Buttons + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.buttonBox.accepted.connect(self._doAccept) + self.buttonBox.rejected.connect(self._doReject) + + # Assemble + self.labelBox.addWidget(self.previewLabel, 0, Qt.AlignTop) + self.labelBox.addStretch(1) + + self.innerBox.addLayout(self.labelBox) + self.innerBox.addWidget(self.listBox) + + self.outerBox.addLayout(self.innerBox) + self.outerBox.addWidget(self.buttonBox) + + self.setLayout(self.outerBox) + + return + + ## + # Slots + ## + + def _selectedSymbol(self): + """Update the preview label and the selected quote style. + """ + selItems = self.listBox.selectedItems() + if selItems: + theSymbol = selItems[0].data(Qt.UserRole) + self.previewLabel.setText(theSymbol) + self.selectedQuote = theSymbol + return + + def _doAccept(self): + """Ok button clicked. + """ + self.accept() + return + + def _doReject(self): + """Cancel button clicked. + """ + self.reject() + return + +# END Class QuotesDialog From 670f1c0b35bb56a3547bcbec55b792116db4740f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 19:30:34 +0200 Subject: [PATCH 05/67] Added the quote selection dialog to Preferences --- nw/gui/preferences.py | 98 ++++++++++++++++++++++++++---------------- nw/gui/projsettings.py | 2 - 2 files changed, 62 insertions(+), 38 deletions(-) diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index 5c0ef235..ff9f0469 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -37,7 +37,7 @@ from PyQt5.QtWidgets import ( QDialogButtonBox, QFileDialog, QFontDialog ) -from nw.gui.custom import QSwitch, QConfigLayout, PagedDialog +from nw.gui.custom import QSwitch, QConfigLayout, PagedDialog, QuotesDialog from nw.core import NWSpellCheck, NWSpellSimple, NWSpellEnchant from nw.constants import nwAlert, nwQuotes @@ -816,51 +816,72 @@ class GuiConfigEditAutoReplaceTab(QWidget): self.mainForm.addGroupLabel("Quotation Style") qWidth = self.mainConf.pxInt(40) + bWidth = int(2.5*self.theTheme.getTextWidth("...")) ## Single Quote Style self.quoteSingleStyleO = QLineEdit() self.quoteSingleStyleO.setMaxLength(1) + self.quoteSingleStyleO.setReadOnly(True) self.quoteSingleStyleO.setFixedWidth(qWidth) self.quoteSingleStyleO.setAlignment(Qt.AlignCenter) self.quoteSingleStyleO.setText(self.mainConf.fmtSingleQuotes[0]) + self.btnSingleStyleO = QPushButton("...") + self.btnSingleStyleO.setMaximumWidth(bWidth) + self.btnSingleStyleO.clicked.connect(self._getSingleOpen) self.mainForm.addRow( "Single quote open style", self.quoteSingleStyleO, - "Auto-replaces apostrophe before words." + "Auto-replaces apostrophe before words.", + theButton=self.btnSingleStyleO ) self.quoteSingleStyleC = QLineEdit() self.quoteSingleStyleC.setMaxLength(1) + self.quoteSingleStyleC.setReadOnly(True) self.quoteSingleStyleC.setFixedWidth(qWidth) self.quoteSingleStyleC.setAlignment(Qt.AlignCenter) self.quoteSingleStyleC.setText(self.mainConf.fmtSingleQuotes[1]) + self.btnSingleStyleC = QPushButton("...") + self.btnSingleStyleC.setMaximumWidth(bWidth) + self.btnSingleStyleC.clicked.connect(self._getSingleClose) self.mainForm.addRow( "Single quote close style", self.quoteSingleStyleC, - "Auto-replaces apostrophe after words." + "Auto-replaces apostrophe after words.", + theButton=self.btnSingleStyleC ) ## Double Quote Style self.quoteDoubleStyleO = QLineEdit() self.quoteDoubleStyleO.setMaxLength(1) + self.quoteDoubleStyleO.setReadOnly(True) self.quoteDoubleStyleO.setFixedWidth(qWidth) self.quoteDoubleStyleO.setAlignment(Qt.AlignCenter) self.quoteDoubleStyleO.setText(self.mainConf.fmtDoubleQuotes[0]) + self.btnDoubleStyleO = QPushButton("...") + self.btnDoubleStyleO.setMaximumWidth(bWidth) + self.btnDoubleStyleO.clicked.connect(self._getDoubleOpen) self.mainForm.addRow( "Double quote open style", self.quoteDoubleStyleO, - "Auto-replaces straight quotes before words." + "Auto-replaces straight quotes before words.", + theButton=self.btnDoubleStyleO ) self.quoteDoubleStyleC = QLineEdit() self.quoteDoubleStyleC.setMaxLength(1) + self.quoteDoubleStyleC.setReadOnly(True) self.quoteDoubleStyleC.setFixedWidth(qWidth) self.quoteDoubleStyleC.setAlignment(Qt.AlignCenter) self.quoteDoubleStyleC.setText(self.mainConf.fmtDoubleQuotes[1]) + self.btnDoubleStyleC = QPushButton("...") + self.btnDoubleStyleC.setMaximumWidth(bWidth) + self.btnDoubleStyleC.clicked.connect(self._getDoubleClose) self.mainForm.addRow( "Double quote close style", self.quoteDoubleStyleC, - "Auto-replaces straight quotes after words." + "Auto-replaces straight quotes after words.", + theButton=self.btnDoubleStyleC ) return @@ -889,37 +910,10 @@ class GuiConfigEditAutoReplaceTab(QWidget): fmtDoubleQuotesO = self.quoteDoubleStyleO.text() fmtDoubleQuotesC = self.quoteDoubleStyleC.text() - if self._checkQuoteSymbol(fmtSingleQuotesO): - self.mainConf.fmtSingleQuotes[0] = fmtSingleQuotesO - else: - self.theParent.makeAlert( - "Invalid quote symbol: %s" % fmtSingleQuotesO, nwAlert.ERROR - ) - validEntries = False - - if self._checkQuoteSymbol(fmtSingleQuotesC): - self.mainConf.fmtSingleQuotes[1] = fmtSingleQuotesC - else: - self.theParent.makeAlert( - "Invalid quote symbol: %s" % fmtSingleQuotesC, nwAlert.ERROR - ) - validEntries = False - - if self._checkQuoteSymbol(fmtDoubleQuotesO): - self.mainConf.fmtDoubleQuotes[0] = fmtDoubleQuotesO - else: - self.theParent.makeAlert( - "Invalid quote symbol: %s" % fmtDoubleQuotesO, nwAlert.ERROR - ) - validEntries = False - - if self._checkQuoteSymbol(fmtDoubleQuotesC): - self.mainConf.fmtDoubleQuotes[1] = fmtDoubleQuotesC - else: - self.theParent.makeAlert( - "Invalid quote symbol: %s" % fmtDoubleQuotesC, nwAlert.ERROR - ) - validEntries = False + self.mainConf.fmtSingleQuotes[0] = fmtSingleQuotesO + self.mainConf.fmtSingleQuotes[1] = fmtSingleQuotesC + self.mainConf.fmtDoubleQuotes[0] = fmtDoubleQuotesO + self.mainConf.fmtDoubleQuotes[1] = fmtDoubleQuotesC self.mainConf.confChanged = True @@ -939,6 +933,38 @@ class GuiConfigEditAutoReplaceTab(QWidget): self.autoReplaceDots.setEnabled(theState) return + def _getSingleOpen(self): + """Dialog for single quote open. + """ + qtBox = QuotesDialog(self, currentQuote=self.quoteSingleStyleO.text()) + if qtBox.exec_() == QDialog.Accepted: + self.quoteSingleStyleO.setText(qtBox.selectedQuote) + return + + def _getSingleClose(self): + """Dialog for single quote close. + """ + qtBox = QuotesDialog(self, currentQuote=self.quoteSingleStyleC.text()) + if qtBox.exec_() == QDialog.Accepted: + self.quoteSingleStyleC.setText(qtBox.selectedQuote) + return + + def _getDoubleOpen(self): + """Dialog for double quote open. + """ + qtBox = QuotesDialog(self, currentQuote=self.quoteDoubleStyleO.text()) + if qtBox.exec_() == QDialog.Accepted: + self.quoteDoubleStyleO.setText(qtBox.selectedQuote) + return + + def _getDoubleClose(self): + """Dialog for double quote close. + """ + qtBox = QuotesDialog(self, currentQuote=self.quoteDoubleStyleC.text()) + if qtBox.exec_() == QDialog.Accepted: + self.quoteDoubleStyleC.setText(qtBox.selectedQuote) + return + ## # Internal Functions ## diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index 0afaefbf..a9c19038 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -73,8 +73,6 @@ class GuiProjectSettings(PagedDialog): self.buttonBox.rejected.connect(self._doClose) self.addControls(self.buttonBox) - self.show() - logger.debug("GuiProjectSettings initialisation complete") return From 28641a7b13a796f6012d89e6be17884dec677c44 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 21:20:15 +0200 Subject: [PATCH 06/67] Fixed test and cleaned up imports --- nw/gui/preferences.py | 2 +- nw/gui/projsettings.py | 7 +++---- tests/test_gui.py | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index ff9f0469..56905307 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -39,7 +39,7 @@ from PyQt5.QtWidgets import ( from nw.gui.custom import QSwitch, QConfigLayout, PagedDialog, QuotesDialog from nw.core import NWSpellCheck, NWSpellSimple, NWSpellEnchant -from nw.constants import nwAlert, nwQuotes +from nw.constants import nwQuotes logger = logging.getLogger(__name__) diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index a9c19038..8f5dacb3 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -31,10 +31,9 @@ import nw from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush from PyQt5.QtWidgets import ( - QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QPlainTextEdit, - QLabel, QWidget, QTabWidget, QDialogButtonBox, QListWidget, QPushButton, - QListWidgetItem, QColorDialog, QAbstractItemView, QTreeWidget, QCheckBox, - QTreeWidgetItem + QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QPlainTextEdit, QLabel, + QWidget, QDialogButtonBox, QListWidget, QPushButton, QListWidgetItem, + QColorDialog, QAbstractItemView, QTreeWidget, QTreeWidgetItem ) from nw.constants import nwAlert diff --git a/tests/test_gui.py b/tests/test_gui.py index cd375cd8..1a18ec99 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -273,6 +273,7 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp): nwGUI.mainConf.backupPath = nwTempGUI projEdit = GuiProjectSettings(nwGUI, nwGUI.theProject) + projEdit.show() qtbot.addWidget(projEdit) projEdit.tabMain.editName.setText("") From 4dd27ba58c5bf3c6c5406c36a37ff90b1bf7eea8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 21:37:34 +0200 Subject: [PATCH 07/67] Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93f9a24f..f0bbdb6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ * The Open Project dialog will now ask before removing an entry from the recent projects list. PR #309. * The text emphasis functions, either selected from the menu or via keyboard shortcuts, will now try to respond to the command in a more meaningful way. That is, the text editor will try to toggle the bold or italics features independently of eachother on the selected text. A feature to apply both at the same time has also been added. PR #310. +* The document editor search tool has been completely rewritten. It now appears as a search box at the top of the document, and has a number of toggle switches added to it. You can modify the search tool to be case sensitive, select only whole words, use regular expression search strings, loop when reaching the end, and continue the search in next file. For the replace feature, you can also select to have the feature try to preserve the case of the replaced word. Issues #84 and #305, PR #314. +* A dialog has been added for selecting quotation mark style. These are now used in the Preferences dialog instead of a plain text box. PR #317. **Other Changes** From 70ce2b1226e570f7dbae22d8ae48a9bc180d2ff2 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 21:58:38 +0200 Subject: [PATCH 08/67] Update codecov settings --- .codecov.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index 8a775495..45ebc6c7 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -3,12 +3,14 @@ codecov: coverage: precision: 2 - round: down + round: up range: "70...100" status: - project: yes - patch: yes + project: + default: + threshold: 1% + patch: no changes: no parsers: From 8ff54ceaa48680014800ba577e8c59b55e7e3bac Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 22:36:36 +0200 Subject: [PATCH 09/67] Added some more checks in the gui tests for writing documents --- tests/reference/gui/3_nwProject.nwx | 16 ++++++++-------- tests/test_gui.py | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index 7ae01352..038ee8f2 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -11,9 +11,9 @@ True False True - None + 31489056e0916 None - 0 + 59 %title% @@ -41,14 +41,14 @@ ROOT NOVEL New - False + True New Chapter FOLDER NOVEL New - False + True Just a Page @@ -57,9 +57,9 @@ Note False PAGE - 0 - 0 - 0 + 331 + 59 + 2 0 diff --git a/tests/test_gui.py b/tests/test_gui.py index 1a18ec99..fb98c1eb 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -127,6 +127,11 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp): nwGUI.treeView.newTreeItem(nwItemType.FILE, None) assert nwGUI.openSelectedItem() + # Add Some Text + nwGUI.docEditor.replaceText("Hello World!") + assert nwGUI.docEditor.getText() == "Hello World!" + nwGUI.docEditor.replaceText("") + # Type something into the document nwGUI.setFocus(2) for c in "# Main Location": @@ -359,6 +364,7 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): # Create new, save, open project nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject(nwTempGUI, True) + assert nwGUI.openDocument("31489056e0916") itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916") qtbot.addWidget(itemEdit) @@ -375,7 +381,6 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): itemEdit.editExport.setChecked(False) assert not itemEdit.editExport.isChecked() - itemEdit._doSave() itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916") @@ -383,9 +388,16 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): assert itemEdit.editName.text() == "Just a Page" assert itemEdit.editStatus.currentData() == "Note" assert itemEdit.editLayout.currentData() == nwItemLayout.PAGE - itemEdit._doClose() + # Check that the header is updated + nwGUI.docEditor.updateDocTitle("31489056e0916") + assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel › New Chapter › Just a Page" + assert not nwGUI.docEditor.setCursorLine("where?") + assert nwGUI.docEditor.setCursorLine(2) + qtbot.wait(stepDelay) + assert nwGUI.docEditor.getCursorPosition() == 9 + qtbot.wait(stepDelay) assert nwGUI.saveProject() qtbot.wait(stepDelay) From 1135ca7f870a456c1904af3b75edb860436e8cca Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 23:00:47 +0200 Subject: [PATCH 10/67] Added document action test --- tests/test_gui.py | 133 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/tests/test_gui.py b/tests/test_gui.py index fb98c1eb..3ad703a5 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -623,3 +623,136 @@ def testSplitTool(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp): # qtbot.stopForInteraction() nwGUI.closeMain() + +@pytest.mark.gui +def testDocAction(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp): + + nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + nwGUI.theProject.projTree.setSeed(42) + assert nwGUI.openProject(nwLipsum) + qtbot.wait(stepDelay) + + # CUT = 3 + # COPY = 4 + # PASTE = 5 + # SEL_ALL = 12 + # SEL_PARA = 13 + # FIND = 14 + # REPLACE = 15 + # GO_NEXT = 16 + # GO_PREV = 17 + # REPL_NEXT = 18 + + # Split By Chapter + assert nwGUI.openDocument("4c4f28287af27") + assert nwGUI.docEditor.setCursorPosition(30) + + cleanText = nwGUI.docEditor.getText()[27:74] + + # Bold + assert nwGUI.passDocumentAction(nwDocAction.BOLD) + assert nwGUI.docEditor.getText()[27:78] == "**Pellentesque** nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BOLD) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Italic + assert nwGUI.passDocumentAction(nwDocAction.ITALIC) + assert nwGUI.docEditor.getText()[27:76] == "*Pellentesque* nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.ITALIC) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Bold-Italic + assert nwGUI.passDocumentAction(nwDocAction.BOLDITALIC) + assert nwGUI.docEditor.getText()[27:80] == "***Pellentesque*** nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BOLDITALIC) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Strikethrough + assert nwGUI.passDocumentAction(nwDocAction.STRIKE) + assert nwGUI.docEditor.getText()[27:78] == "~~Pellentesque~~ nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.STRIKE) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Should get us back to plain + assert nwGUI.passDocumentAction(nwDocAction.BOLD) + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.ITALIC) + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.ITALIC) + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BOLD) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Equivalent of the above + assert nwGUI.passDocumentAction(nwDocAction.BOLDITALIC) + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.ITALIC) + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BOLD) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Double Quotes + assert nwGUI.passDocumentAction(nwDocAction.D_QUOTE) + assert nwGUI.docEditor.getText()[27:76] == "“Pellentesque” nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.UNDO) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Single Quotes + assert nwGUI.passDocumentAction(nwDocAction.S_QUOTE) + assert nwGUI.docEditor.getText()[27:76] == "‘Pellentesque’ nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.UNDO) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Block Formats + assert nwGUI.docEditor.setCursorPosition(30) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H1) + assert nwGUI.docEditor.getText()[27:76] == "# Pellentesque nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H2) + assert nwGUI.docEditor.getText()[27:77] == "## Pellentesque nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H3) + assert nwGUI.docEditor.getText()[27:78] == "### Pellentesque nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H4) + assert nwGUI.docEditor.getText()[27:79] == "#### Pellentesque nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_TXT) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_COM) + assert nwGUI.docEditor.getText()[27:76] == "% Pellentesque nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_TXT) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Undo/Redo + assert nwGUI.passDocumentAction(nwDocAction.UNDO) + assert nwGUI.docEditor.getText()[27:76] == "% Pellentesque nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.REDO) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # qtbot.stopForInteraction() + nwGUI.closeMain() From 3bfe21468276eefeb65ffa0e27f24fb1113a0c96 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 23:45:23 +0200 Subject: [PATCH 11/67] Added thin space and thin non-breaking space to unicode class --- nw/constants/constants.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index b05f8e6d..90e49dfe 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -231,6 +231,8 @@ class nwUnicode: ## Other U_NBSP = "\u00a0" # Non-breaking space + U_THNSP = "\u2009" # Thin space + U_THNBSP = "\u202f" # Thin non-breaking space U_CHECK = "\u2714" # Heavy check mark U_MULT = "\u2715" # Multiplication x @@ -275,6 +277,8 @@ class nwUnicode: ## Other H_NBSP = " " + H_THNSP = " " + H_THNBSP = " " H_CHECK = "✔" H_MULT = "✕" From 61856cec5b60f2813c3e32015ef3ebeebc8e80a5 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 17:34:41 +0200 Subject: [PATCH 12/67] Added new constants for document insert feature --- nw/constants/__init__.py | 8 ++++++-- nw/constants/constants.py | 27 +++++++++++++++++++++++---- nw/constants/enum.py | 13 +++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/nw/constants/__init__.py b/nw/constants/__init__.py index 57761e42..0e90354a 100644 --- a/nw/constants/__init__.py +++ b/nw/constants/__init__.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- from nw.constants.iso import isoLanguage, isoCountry from nw.constants.constants import ( - nwConst, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode + nwConst, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode, + nwInsertSymbols ) from nw.constants.enum import ( - nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline + nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline, + nwDocInsert ) __all__ = [ @@ -17,10 +19,12 @@ __all__ = [ "nwLabels", "nwQuotes", "nwUnicode", + "nwInsertSymbols", "nwAlert", "nwDocAction", "nwItemClass", "nwItemLayout", "nwItemType", "nwOutline", + "nwDocInsert", ] diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 90e49dfe..1ff5da4f 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -25,7 +25,7 @@ along with this program. If not, see . """ -from nw.constants.enum import nwItemClass, nwItemLayout, nwOutline +from nw.constants.enum import nwItemClass, nwItemLayout, nwOutline, nwDocInsert class nwConst(): @@ -197,7 +197,7 @@ class nwQuotes(): # END Class nwQuotes class nwUnicode: - """Suppoted unicode character constants and translation maps for HTML. + """Supported unicode character constants and translation maps for HTML. """ # Unicode Constants @@ -229,10 +229,12 @@ class nwUnicode: U_EMDASH = "\u2014" # Long dash U_HELLIP = "\u2026" # Ellipsis - ## Other + ## Spaces U_NBSP = "\u00a0" # Non-breaking space U_THNSP = "\u2009" # Thin space U_THNBSP = "\u202f" # Thin non-breaking space + + ## Symbols U_CHECK = "\u2714" # Heavy check mark U_MULT = "\u2715" # Multiplication x @@ -275,10 +277,12 @@ class nwUnicode: H_EMDASH = "—" H_HELLIP = "…" - ## Other + ## Spaces H_NBSP = " " H_THNSP = " " H_THNBSP = " " + + ## Symbols H_CHECK = "✔" H_MULT = "✕" @@ -293,3 +297,18 @@ class nwUnicode: H_LTRIS = "◂" # END Class nwUnicode + +class nwInsertSymbols(): + + SYMBOLS = { + nwDocInsert.NO_INSERT : "", + nwDocInsert.HARD_BREAK : " \n", + nwDocInsert.NB_SPACE : nwUnicode.U_NBSP, + nwDocInsert.THIN_SPACE : nwUnicode.U_THNSP, + nwDocInsert.THIN_NB_SPACE : nwUnicode.U_THNBSP, + nwDocInsert.SHORT_DASH : nwUnicode.U_ENDASH, + nwDocInsert.LONG_DASH : nwUnicode.U_EMDASH, + nwDocInsert.ELLIPSIS : nwUnicode.U_HELLIP, + } + +# END Enum nwDocInsert diff --git a/nw/constants/enum.py b/nw/constants/enum.py index 1f90a928..84f8d4f5 100644 --- a/nw/constants/enum.py +++ b/nw/constants/enum.py @@ -96,6 +96,19 @@ class nwDocAction(Enum): # END Enum nwDocAction +class nwDocInsert(Enum): + + NO_INSERT = 0 + HARD_BREAK = 1 + NB_SPACE = 2 + THIN_SPACE = 3 + THIN_NB_SPACE = 4 + SHORT_DASH = 5 + LONG_DASH = 6 + ELLIPSIS = 7 + +# END Enum nwDocInsert + class nwAlert(Enum): INFO = 0 From 0fc0851def1dadd505a9577673ea407c05d164b3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 17:35:14 +0200 Subject: [PATCH 13/67] Added insert options and key bindings for seven different symbol and space inserts --- nw/core/tohtml.py | 2 + nw/gui/doceditor.py | 48 +++++------- nw/gui/dochighlight.py | 2 +- nw/gui/mainmenu.py | 161 ++++++++++++++++++++++++++++------------- nw/guimain.py | 27 ++++++- 5 files changed, 156 insertions(+), 84 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 6123ce21..1fd6f00d 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -54,6 +54,8 @@ class ToHtml(Tokenizer): nwUnicode.U_EMDASH : nwUnicode.H_EMDASH, nwUnicode.U_HELLIP : nwUnicode.H_HELLIP, nwUnicode.U_NBSP : nwUnicode.H_NBSP, + nwUnicode.U_THNSP : nwUnicode.H_THNSP, + nwUnicode.U_THNBSP : nwUnicode.H_THNBSP, } self.revDict = {} self.reReplace = [] diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index eadce6ff..35ac09d8 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -48,7 +48,7 @@ from PyQt5.QtWidgets import ( from nw.core import NWDoc from nw.gui.dochighlight import GuiDocHighlighter from nw.core import NWSpellSimple, countWords -from nw.constants import nwUnicode, nwDocAction +from nw.constants import nwUnicode, nwDocAction, nwDocInsert, nwInsertSymbols from nw.common import transferCase logger = logging.getLogger(__name__) @@ -570,6 +570,21 @@ class GuiDocEditor(QTextEdit): )) return + def insertText(self, theInsert): + """Insert a specific type of text at the cursor position. + """ + if isinstance(theInsert, str): + theText = theInsert + elif theInsert in nwInsertSymbols.SYMBOLS: + theText = nwInsertSymbols.SYMBOLS[theInsert] + else: + return False + theCursor = self.textCursor() + theCursor.beginEditBlock() + theCursor.insertText(theText) + theCursor.endEditBlock() + return True + def closeSearch(self): """Close the search box. """ @@ -598,18 +613,7 @@ class GuiDocEditor(QTextEdit): # seems to be capturing the return key. return - if keyEvent.modifiers() == Qt.ShiftModifier: - theKey = keyEvent.key() - if theKey == Qt.Key_Return: - self._insertHardBreak() - elif theKey == Qt.Key_Enter: - self._insertHardBreak() - elif theKey == Qt.Key_Space: - self._insertNonBreakingSpace() - else: - QTextEdit.keyPressEvent(self, keyEvent) - else: - QTextEdit.keyPressEvent(self, keyEvent) + QTextEdit.keyPressEvent(self, keyEvent) return @@ -797,24 +801,6 @@ class GuiDocEditor(QTextEdit): return True - def _insertHardBreak(self): - """Inserts a hard line break at the cursor position. - """ - theCursor = self.textCursor() - theCursor.beginEditBlock() - theCursor.insertText(" \n") - theCursor.endEditBlock() - return - - def _insertNonBreakingSpace(self): - """Inserts a non-breaking space at the cursor position. - """ - theCursor = self.textCursor() - theCursor.beginEditBlock() - theCursor.insertText(nwUnicode.U_NBSP) - theCursor.endEditBlock() - return - def _openSpellContext(self): """Opens the spell check context menu at the current point of the cursor. diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index a76c9646..4acc0e7a 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -132,7 +132,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): # Non-Breaking Spaces self.hRules.append(( - "[%s]+" % nwUnicode.U_NBSP, { + "[%s%s]+" % (nwUnicode.U_NBSP, nwUnicode.U_THNBSP), { 0 : self.hStyles["nobreak"], } )) diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 720eefae..7f7c9164 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -28,12 +28,12 @@ import logging import nw -from PyQt5.QtCore import QUrl +from PyQt5.QtCore import Qt, QUrl from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMenuBar, QAction, QMessageBox from nw.gui.about import GuiAbout -from nw.constants import nwItemType, nwItemClass, nwDocAction +from nw.constants import nwItemType, nwItemClass, nwDocAction, nwDocInsert logger = logging.getLogger(__name__) @@ -50,8 +50,9 @@ class GuiMainMenu(QMenuBar): self._buildProjectMenu() self._buildDocumentMenu() self._buildEditMenu() - self._buildViewMenu() + self._buildInsertMenu() self._buildFormatMenu() + self._buildViewMenu() self._buildToolsMenu() self._buildHelpMenu() @@ -59,6 +60,7 @@ class GuiMainMenu(QMenuBar): self._docAction = self.theParent.passDocumentAction self._moveTreeItem = self.theParent.treeView.moveTreeItem self._newTreeItem = self.theParent.treeView.newTreeItem + self._docInsert = self.theParent.docEditor.insertText logger.debug("GuiMainMenu initialisation complete") @@ -341,53 +343,6 @@ class GuiMainMenu(QMenuBar): return - def _buildViewMenu(self): - - # View - self.viewMenu = self.addMenu("&View") - - # View > TreeView - self.aFocusTree = QAction("Focus Project Tree", self) - self.aFocusTree.setStatusTip("Move focus to project tree") - self.aFocusTree.setShortcut("Alt+1") - self.aFocusTree.triggered.connect(lambda : self.theParent.setFocus(1)) - self.viewMenu.addAction(self.aFocusTree) - - # View > Document Pane 1 - self.aFocusEditor = QAction("Focus Document Editor", self) - self.aFocusEditor.setStatusTip("Move focus to left document pane") - self.aFocusEditor.setShortcut("Alt+2") - self.aFocusEditor.triggered.connect(lambda : self.theParent.setFocus(2)) - self.viewMenu.addAction(self.aFocusEditor) - - # View > Document Pane 2 - self.aFocusView = QAction("Focus Document Viewer", self) - self.aFocusView.setStatusTip("Move focus to right document pane") - self.aFocusView.setShortcut("Alt+3") - self.aFocusView.triggered.connect(lambda : self.theParent.setFocus(3)) - self.viewMenu.addAction(self.aFocusView) - - # View > Separator - self.viewMenu.addSeparator() - - # View > Toggle Distraction Free Mode - self.aZenMode = QAction("Zen Mode", self) - self.aZenMode.setStatusTip("Toggles distraction free mode, only showing text editor") - self.aZenMode.setShortcut("F8") - self.aZenMode.setCheckable(True) - self.aZenMode.setChecked(self.theParent.isZenMode) - self.aZenMode.toggled.connect(self.theParent.toggleZenMode) - self.viewMenu.addAction(self.aZenMode) - - # View > Toggle Full Screen - self.aFullScreen = QAction("Full Screen Mode", self) - self.aFullScreen.setStatusTip("Maximises the main window") - self.aFullScreen.setShortcut("F11") - self.aFullScreen.triggered.connect(self.theParent.toggleFullScreenMode) - self.viewMenu.addAction(self.aFullScreen) - - return - def _buildEditMenu(self): # Edit @@ -497,6 +452,65 @@ class GuiMainMenu(QMenuBar): return + def _buildInsertMenu(self): + + # Insert + self.insertMenu = self.addMenu("&Insert") + + # Insert > Short Dash + self.aInsENDash = QAction("Short Dash", self) + self.aInsENDash.setStatusTip("Insert short dash") + self.aInsENDash.setShortcut("Ctrl+K, -") + self.aInsENDash.triggered.connect(lambda: self._docInsert(nwDocInsert.SHORT_DASH)) + self.insertMenu.addAction(self.aInsENDash) + + # Insert > Long Dash + self.aInsEMDash = QAction("Long Dash", self) + self.aInsEMDash.setStatusTip("Insert long dash") + self.aInsEMDash.setShortcut("Ctrl+K, _") + self.aInsEMDash.triggered.connect(lambda: self._docInsert(nwDocInsert.LONG_DASH)) + self.insertMenu.addAction(self.aInsEMDash) + + # Insert > Ellipsis + self.aInsEllipsis = QAction("Ellipsis", self) + self.aInsEllipsis.setStatusTip("Insert ellipsis") + self.aInsEllipsis.setShortcut("Ctrl+K, .") + self.aInsEllipsis.triggered.connect(lambda: self._docInsert(nwDocInsert.ELLIPSIS)) + self.insertMenu.addAction(self.aInsEllipsis) + + # Insert > Separator + self.insertMenu.addSeparator() + + # Insert > Hard Line Break + self.aInsHardBreak = QAction("Hard Line Break", self) + self.aInsHardBreak.setStatusTip("Insert a hard line break") + self.aInsHardBreak.setShortcut("Ctrl+K, Return") + self.aInsHardBreak.triggered.connect(lambda: self._docInsert(nwDocInsert.HARD_BREAK)) + self.insertMenu.addAction(self.aInsHardBreak) + + # Insert > Non-Breaking Space + self.aInsNBSpace = QAction("Non-Breaking Space", self) + self.aInsNBSpace.setStatusTip("Insert a non-breaking space") + self.aInsNBSpace.setShortcut("Ctrl+K, Space") + self.aInsNBSpace.triggered.connect(lambda: self._docInsert(nwDocInsert.NB_SPACE)) + self.insertMenu.addAction(self.aInsNBSpace) + + # Insert > Thin Space + self.aInsThinSpace = QAction("Thin Space", self) + self.aInsThinSpace.setStatusTip("Insert a thin space") + self.aInsThinSpace.setShortcut("Ctrl+K, Shift+Space") + self.aInsThinSpace.triggered.connect(lambda: self._docInsert(nwDocInsert.THIN_SPACE)) + self.insertMenu.addAction(self.aInsThinSpace) + + # Insert > Thin Non-Breaking Space + self.aInsThinNBSpace = QAction("Thin Non-Breaking Space", self) + self.aInsThinNBSpace.setStatusTip("Insert a thin non-breaking space") + self.aInsThinNBSpace.setShortcut("Ctrl+K, Ctrl+Space") + self.aInsThinNBSpace.triggered.connect(lambda: self._docInsert(nwDocInsert.THIN_NB_SPACE)) + self.insertMenu.addAction(self.aInsThinNBSpace) + + return + def _buildFormatMenu(self): # Format @@ -594,6 +608,53 @@ class GuiMainMenu(QMenuBar): return + def _buildViewMenu(self): + + # View + self.viewMenu = self.addMenu("&View") + + # View > TreeView + self.aFocusTree = QAction("Focus Project Tree", self) + self.aFocusTree.setStatusTip("Move focus to project tree") + self.aFocusTree.setShortcut("Alt+1") + self.aFocusTree.triggered.connect(lambda : self.theParent.setFocus(1)) + self.viewMenu.addAction(self.aFocusTree) + + # View > Document Pane 1 + self.aFocusEditor = QAction("Focus Document Editor", self) + self.aFocusEditor.setStatusTip("Move focus to left document pane") + self.aFocusEditor.setShortcut("Alt+2") + self.aFocusEditor.triggered.connect(lambda : self.theParent.setFocus(2)) + self.viewMenu.addAction(self.aFocusEditor) + + # View > Document Pane 2 + self.aFocusView = QAction("Focus Document Viewer", self) + self.aFocusView.setStatusTip("Move focus to right document pane") + self.aFocusView.setShortcut("Alt+3") + self.aFocusView.triggered.connect(lambda : self.theParent.setFocus(3)) + self.viewMenu.addAction(self.aFocusView) + + # View > Separator + self.viewMenu.addSeparator() + + # View > Toggle Distraction Free Mode + self.aZenMode = QAction("Zen Mode", self) + self.aZenMode.setStatusTip("Toggles distraction free mode, only showing text editor") + self.aZenMode.setShortcut("F8") + self.aZenMode.setCheckable(True) + self.aZenMode.setChecked(self.theParent.isZenMode) + self.aZenMode.toggled.connect(self.theParent.toggleZenMode) + self.viewMenu.addAction(self.aZenMode) + + # View > Toggle Full Screen + self.aFullScreen = QAction("Full Screen Mode", self) + self.aFullScreen.setStatusTip("Maximises the main window") + self.aFullScreen.setShortcut("F11") + self.aFullScreen.triggered.connect(self.theParent.toggleFullScreenMode) + self.viewMenu.addAction(self.aFullScreen) + + return + def _buildToolsMenu(self): # Tools diff --git a/nw/guimain.py b/nw/guimain.py index 70a6ba0b..a82d5a99 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -966,12 +966,15 @@ class GuiMain(QMainWindow): """Connect to the main window all menu actions that need to be available also when the main menu is hidden. """ + # Project self.addAction(self.mainMenu.aSaveProject) self.addAction(self.mainMenu.aExitNW) + + # Document self.addAction(self.mainMenu.aSaveDoc) self.addAction(self.mainMenu.aFileDetails) - self.addAction(self.mainMenu.aZenMode) - self.addAction(self.mainMenu.aFullScreen) + + # Edit self.addAction(self.mainMenu.aEditUndo) self.addAction(self.mainMenu.aEditRedo) self.addAction(self.mainMenu.aEditCut) @@ -979,6 +982,17 @@ class GuiMain(QMainWindow): self.addAction(self.mainMenu.aEditPaste) self.addAction(self.mainMenu.aSelectAll) self.addAction(self.mainMenu.aSelectPar) + + # Insert + self.addAction(self.mainMenu.aInsENDash) + self.addAction(self.mainMenu.aInsEMDash) + self.addAction(self.mainMenu.aInsEllipsis) + self.addAction(self.mainMenu.aInsHardBreak) + self.addAction(self.mainMenu.aInsNBSpace) + self.addAction(self.mainMenu.aInsThinSpace) + self.addAction(self.mainMenu.aInsThinNBSpace) + + # Format self.addAction(self.mainMenu.aFmtItalic) self.addAction(self.mainMenu.aFmtBold) self.addAction(self.mainMenu.aFmtBoldIt) @@ -990,10 +1004,19 @@ class GuiMain(QMainWindow): self.addAction(self.mainMenu.aFmtHead4) self.addAction(self.mainMenu.aFmtComment) self.addAction(self.mainMenu.aFmtNoFormat) + + # View + self.addAction(self.mainMenu.aZenMode) + self.addAction(self.mainMenu.aFullScreen) + + # Tools self.addAction(self.mainMenu.aSpellCheck) self.addAction(self.mainMenu.aReRunSpell) self.addAction(self.mainMenu.aPreferences) + + # Help self.addAction(self.mainMenu.aHelp) + return True def _setWindowTitle(self, projName=None): From fdf79157471abb74781185dbb13d46cb263492c5 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 17:35:35 +0200 Subject: [PATCH 14/67] Updated sample text with extra descriptions --- sample/content/636b6aa9b697b.nwd | 4 +++- sample/nwProject.nwx | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 4916b753..0ae22a00 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -13,7 +13,9 @@ In addition, the editor supports automatic formatting of “quotes”, both doub If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, let’s auto-replace this A with , and this C with . While is just . Press Ctrl+R to see what this looks like in the view pane. -The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. +The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported – and can be automatically inserted when typing two hyphens.⎄ + +Thin spaces and thin non-breaking spaces are also supported from the Insert menu, and can be used to separate numbers from their units, like: 25 kg. #### Some Section Here diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index aeba369e..23daf66e 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,21 +1,21 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 332 - 56 - 10692 + 404 + 70 + 14966 False True True 636b6aa9b697b - 636b6aa9b697b - 927 + bb2c23b3c42cc + 967 B E @@ -114,10 +114,10 @@ 1st Draft True SCENE - 1240 - 223 - 7 - 403 + 1484 + 263 + 8 + 1087 Another Scene From ee23f937f20af10f5b3e016667529010ffa08723 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 19:51:23 +0200 Subject: [PATCH 15/67] Added issues tracker menu entry, and made a few minor changes --- nw/__init__.py | 1 + nw/gui/mainmenu.py | 34 ++++++++++++++++++++++++++------ sample/content/636b6aa9b697b.nwd | 2 +- sample/nwProject.nwx | 12 +++++------ 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/nw/__init__.py b/nw/__init__.py index 6ab06265..92fd0ac2 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -47,6 +47,7 @@ __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" __status__ = "Pre-Release" __url__ = "https://github.com/vkbo/novelWriter" +__issuesurl__ = "https://github.com/vkbo/novelWriter/issues" __domain__ = "novelwriter.io" __docurl__ = "https://novelwriter.readthedocs.io" __credits__ = [ diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 7f7c9164..d831da81 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -28,7 +28,7 @@ import logging import nw -from PyQt5.QtCore import Qt, QUrl +from PyQt5.QtCore import QUrl from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMenuBar, QAction, QMessageBox @@ -99,6 +99,8 @@ class GuiMainMenu(QMenuBar): ## def _menuExit(self): + """Exit novelWriter. + """ self.theParent.closeMain() return @@ -125,19 +127,33 @@ class GuiMainMenu(QMenuBar): return True def _showAboutQt(self): + """Show Qt's own About dialog. + """ msgBox = QMessageBox() msgBox.aboutQt(self.theParent,"About Qt") return True def _openHelp(self): + """Open the documentation URL in the system's default browser. + """ QDesktopServices.openUrl(QUrl(nw.__docurl__)) return True + def _openIssue(self): + """Open the issue tracker URL in the system's default browser. + """ + QDesktopServices.openUrl(QUrl(nw.__issuesurl__)) + return True + def _showDocumentLocation(self): + """Open the dialog showing the location of the editor document. + """ self.theParent.docEditor.revealLocation() return True def _doBackup(self): + """Call the backup function for the project. + """ self.theProject.zipIt(True) return True @@ -230,14 +246,14 @@ class GuiMainMenu(QMenuBar): self.projMenu.addSeparator() # Project > Edit - self.aEditItem = QAction("&Edit Project Item", self) + self.aEditItem = QAction("Edit Project Item", self) self.aEditItem.setStatusTip("Change item settings") self.aEditItem.setShortcuts(["Ctrl+E", "F2"]) self.aEditItem.triggered.connect(self.theParent.editItem) self.projMenu.addAction(self.aEditItem) # Project > Delete - self.aDeleteItem = QAction("&Delete Project Item", self) + self.aDeleteItem = QAction("Delete Project Item", self) self.aDeleteItem.setStatusTip("Delete selected item") self.aDeleteItem.setShortcut("Ctrl+Del") self.aDeleteItem.triggered.connect(lambda : self.theParent.treeView.deleteItem(None)) @@ -267,21 +283,21 @@ class GuiMainMenu(QMenuBar): self.docuMenu = self.addMenu("&Document") # Document > New - self.aNewDoc = QAction("&New Document", self) + self.aNewDoc = QAction("New Document", self) self.aNewDoc.setStatusTip("Create new document") self.aNewDoc.setShortcut("Ctrl+N") self.aNewDoc.triggered.connect(lambda : self._newTreeItem(nwItemType.FILE, None)) self.docuMenu.addAction(self.aNewDoc) # Document > Open - self.aOpenDoc = QAction("&Open Document", self) + self.aOpenDoc = QAction("Open Document", self) self.aOpenDoc.setStatusTip("Open selected document") self.aOpenDoc.setShortcut("Ctrl+O") self.aOpenDoc.triggered.connect(self.theParent.openSelectedItem) self.docuMenu.addAction(self.aOpenDoc) # Document > Save - self.aSaveDoc = QAction("&Save Document", self) + self.aSaveDoc = QAction("Save Document", self) self.aSaveDoc.setStatusTip("Save current document") self.aSaveDoc.setShortcut("Ctrl+S") self.aSaveDoc.triggered.connect(self.theParent.saveDocument) @@ -764,6 +780,12 @@ class GuiMainMenu(QMenuBar): self.aHelp.triggered.connect(self._openHelp) self.helpMenu.addAction(self.aHelp) + # Document > Report Issue + self.aIssue = QAction("Report an Issue", self) + self.aIssue.setStatusTip("View online documentation") + self.aIssue.triggered.connect(self._openIssue) + self.helpMenu.addAction(self.aIssue) + return # END Class GuiMainMenu diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 0ae22a00..394a96ce 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -13,7 +13,7 @@ In addition, the editor supports automatic formatting of “quotes”, both doub If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, let’s auto-replace this A with , and this C with . While is just . Press Ctrl+R to see what this looks like in the view pane. -The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported – and can be automatically inserted when typing two hyphens.⎄ +The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported – and can be automatically inserted when typing two hyphens. Thin spaces and thin non-breaking spaces are also supported from the Insert menu, and can be used to separate numbers from their units, like: 25 kg. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 23daf66e..4ce89213 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 404 - 70 - 14966 + 407 + 71 + 15118 False @@ -114,10 +114,10 @@ 1st Draft True SCENE - 1484 + 1483 263 8 - 1087 + 1086 Another Scene From b0380adc5f1876eaa39573fc1776abf9f18f00d1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 21:55:43 +0200 Subject: [PATCH 16/67] Added line and paragraph separatir unicodes back into nwUnicode class --- nw/constants/constants.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 1ff5da4f..e4e6af98 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -229,10 +229,12 @@ class nwUnicode: U_EMDASH = "\u2014" # Long dash U_HELLIP = "\u2026" # Ellipsis - ## Spaces + ## Spaces and Lines U_NBSP = "\u00a0" # Non-breaking space U_THNSP = "\u2009" # Thin space U_THNBSP = "\u202f" # Thin non-breaking space + U_LSEP = "\u2028" # Line separator + U_PSEP = "\u2029" # Paragraph separator ## Symbols U_CHECK = "\u2714" # Heavy check mark From 03f1142e40da2fbf05b614cd160adb0ea87c17e1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 21:56:37 +0200 Subject: [PATCH 17/67] Added replace quotes function, and cleaned up some buggy behaviour with undo and how changes are tracked --- nw/constants/enum.py | 2 + nw/gui/doceditor.py | 114 +++++++++++++++++++++++++++++++++++-------- nw/gui/mainmenu.py | 17 ++++++- 3 files changed, 111 insertions(+), 22 deletions(-) diff --git a/nw/constants/enum.py b/nw/constants/enum.py index 84f8d4f5..7177223c 100644 --- a/nw/constants/enum.py +++ b/nw/constants/enum.py @@ -93,6 +93,8 @@ class nwDocAction(Enum): BLOCK_H4 = 22 BLOCK_COM = 23 BLOCK_TXT = 24 + REPL_SNG = 25 + REPL_DBL = 26 # END Enum nwDocAction diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 35ac09d8..db989d45 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -45,11 +45,12 @@ from PyQt5.QtWidgets import ( QFrame ) -from nw.core import NWDoc +from nw.core import NWDoc, NWSpellSimple, countWords from nw.gui.dochighlight import GuiDocHighlighter -from nw.core import NWSpellSimple, countWords -from nw.constants import nwUnicode, nwDocAction, nwDocInsert, nwInsertSymbols from nw.common import transferCase +from nw.constants import ( + nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwInsertSymbols +) logger = logging.getLogger(__name__) @@ -76,6 +77,7 @@ class GuiDocEditor(QTextEdit): self.paraCount = 0 self.lastEdit = 0 self.bigDoc = False + self.doReplace = False self.nonWord = "\"'" # Document State @@ -157,6 +159,7 @@ class GuiDocEditor(QTextEdit): self.paraCount = 0 self.lastEdit = 0 self.bigDoc = False + self.doReplace = False self.hasSelection = False @@ -261,7 +264,9 @@ class GuiDocEditor(QTextEdit): self.hLight.spellCheck = False bfTime = time() + self._allowAutoReplace(False) self.setPlainText(theDoc) + self._allowAutoReplace(True) afTime = time() logger.debug("Document highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))) @@ -388,8 +393,8 @@ class GuiDocEditor(QTextEdit): """ if self.mainConf.verQtValue >= 50900: theText = self.qDocument.toRawText() - theText = theText.replace("\u2028", "\n") # Line separators - theText = theText.replace("\u2029", "\n") # Paragraph separators + theText = theText.replace(nwUnicode.U_LSEP, "\n") # Line separators + theText = theText.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators else: theText = self.toPlainText() return theText @@ -496,6 +501,7 @@ class GuiDocEditor(QTextEdit): if not self.theParent.hasProject: logger.error("No project open") return False + self._allowAutoReplace(False) if theAction == nwDocAction.UNDO: self.undo() elif theAction == nwDocAction.REDO: @@ -544,9 +550,15 @@ class GuiDocEditor(QTextEdit): self._formatBlock(nwDocAction.BLOCK_COM) elif theAction == nwDocAction.BLOCK_TXT: self._formatBlock(nwDocAction.BLOCK_TXT) + elif theAction == nwDocAction.REPL_SNG: + self._replaceQuotes("'", self.typSQOpen, self.typSQClose) + elif theAction == nwDocAction.REPL_DBL: + self._replaceQuotes("\"", self.typDQOpen, self.typDQClose) else: logger.debug("Unknown or unsupported document action %s" % str(theAction)) + self._allowAutoReplace(True) return False + self._allowAutoReplace(True) return True def isEmpty(self): @@ -597,24 +609,23 @@ class GuiDocEditor(QTextEdit): def keyPressEvent(self, keyEvent): """Intercept key press events. - We need to intercept key presses briefly to record the state of - selection. This is in order to know whether we had a selection - prior to triggering the _docChange slot, as we do not want to - trigger autoreplace on selections. Autoreplace on selections - messes with undo/redo history. - We also need to intercept the Shift key modifier for certain key - combinations that modifies standard keys like enter and space. - However, we don't want to spend a lot of time in this function - as it is triggered on every keypress when typing. + We need to intercept a few key sequences: + * The return key redirects here even if the search box has + focus. Since we need the return key to continue search, we + block any further interaction here while it's in focus. + * The undo sequence bypasses the doAction pathway from the + menu, so we redirect it back from here. + * The default redo sequence is Ctrl+Shift+Z, which we don't + use, so we block it. """ - self.hasSelection = self.textCursor().hasSelection() if self.docSearch.searchBox.hasFocus(): - # Block the event when the focus is on the search bar as it - # seems to be capturing the return key. return - - QTextEdit.keyPressEvent(self, keyEvent) - + elif keyEvent == QKeySequence.Redo: + return + elif keyEvent == QKeySequence.Undo: + self.docAction(nwDocAction.UNDO) + else: + QTextEdit.keyPressEvent(self, keyEvent) return def mouseReleaseEvent(self, mEvent): @@ -650,7 +661,7 @@ class GuiDocEditor(QTextEdit): self.setDocumentChanged(True) if not self.wcTimer.isActive(): self.wcTimer.start() - if self.mainConf.doReplace and not self.hasSelection: + if self.doReplace and charsAdded == 1: self._docAutoReplace(self.qDocument.findBlock(thePos)) return @@ -862,6 +873,58 @@ class GuiDocEditor(QTextEdit): return + def _replaceQuotes(self, sQuote, oQuote, cQuote): + """Replace all straight quotes in the selected text. + """ + theCursor = self.textCursor() + if theCursor.hasSelection(): + posS = theCursor.selectionStart() + posE = theCursor.selectionEnd() + closeCheck = ( + " ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP + ) + + self._allowAutoReplace(False) + for posC in range(posS, posE+1): + theCursor.setPosition(posC) + theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2) + selText = theCursor.selectedText() + + nS = len(selText) + if nS == 2: + pC = selText[0] + cC = selText[1] + elif nS == 1: + pC = " " + cC = selText[0] + else: + continue + + if cC != sQuote: + continue + + theCursor.clearSelection() + theCursor.setPosition(posC) + if pC in closeCheck: + theCursor.beginEditBlock() + theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1) + theCursor.insertText(oQuote) + theCursor.endEditBlock() + else: + theCursor.beginEditBlock() + theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1) + theCursor.insertText(cQuote) + theCursor.endEditBlock() + + self._allowAutoReplace(True) + + else: + self.theParent.makeAlert( + "Please selection some text before calling replace quotes.", nwAlert.ERROR + ) + + return + def _checkDocSize(self, theSize): """Check if document size crosses the big document limit set in config. If so, we will set the big document flag to True. @@ -1225,6 +1288,15 @@ class GuiDocEditor(QTextEdit): return + def _allowAutoReplace(self, theState): + """used to enable/disable the auto-replace feature temporarily. + """ + if theState: + self.doReplace = self.mainConf.doReplace + else: + self.doReplace = False + return + # END Class GuiDocEditor # =============================================================================================== # diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index d831da81..1f78212d 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -577,7 +577,7 @@ class GuiMainMenu(QMenuBar): self.aFmtSQuote.triggered.connect(lambda: self._docAction(nwDocAction.S_QUOTE)) self.fmtMenu.addAction(self.aFmtSQuote) - # Edit > Separator + # Format > Separator self.fmtMenu.addSeparator() # Format > Header 1 @@ -622,6 +622,21 @@ class GuiMainMenu(QMenuBar): self.aFmtNoFormat.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_TXT)) self.fmtMenu.addAction(self.aFmtNoFormat) + # Format > Separator + self.fmtMenu.addSeparator() + + # Format > Replace Single Quotes + self.aFmtReplSng = QAction("Replace Single Quotes", self) + self.aFmtReplSng.setStatusTip("Replace all straight single quotes in selected text") + self.aFmtReplSng.triggered.connect(lambda: self._docAction(nwDocAction.REPL_SNG)) + self.fmtMenu.addAction(self.aFmtReplSng) + + # Format > Replace Double Quotes + self.aFmtReplDbl = QAction("Replace Double Quotes", self) + self.aFmtReplDbl.setStatusTip("Replace all straight double quotes in selected text") + self.aFmtReplDbl.triggered.connect(lambda: self._docAction(nwDocAction.REPL_DBL)) + self.fmtMenu.addAction(self.aFmtReplDbl) + return def _buildViewMenu(self): From d6dd776056a4d0ea9faaa59b6325cd35f416da38 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 22:17:57 +0200 Subject: [PATCH 18/67] Delete unused variable --- nw/gui/doceditor.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index db989d45..3af27d07 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -80,9 +80,6 @@ class GuiDocEditor(QTextEdit): self.doReplace = False self.nonWord = "\"'" - # Document State - self.hasSelection = False - # Typography self.typDQOpen = self.mainConf.fmtDoubleQuotes[0] self.typDQClose = self.mainConf.fmtDoubleQuotes[1] @@ -161,8 +158,6 @@ class GuiDocEditor(QTextEdit): self.bigDoc = False self.doReplace = False - self.hasSelection = False - self.setDocumentChanged(False) self.docHeader.setTitleFromHandle(self.theHandle) From b19c106b45003cd3cbff7bf2ea9cf950f3f7dae7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 22:46:22 +0200 Subject: [PATCH 19/67] CHange the order of preference for the view document action --- nw/guimain.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/nw/guimain.py b/nw/guimain.py index a82d5a99..cd479c9e 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -518,20 +518,30 @@ class GuiMain(QMainWindow): """Load a document for viewing in the view panel. """ if tHandle is None: - tHandle = self.treeView.getSelectedHandle() - if tHandle is None: - logger.debug("No document selected, trying editor document") - tHandle = self.docEditor.theHandle - if tHandle is None: - logger.debug("No document selected, trying last viewed") - tHandle = self.theProject.lastViewed - if tHandle is None: - logger.debug("No document selected, giving up") - return False + logger.debug("Viewing document, but no handle provided") + + if self.docEditor.hasFocus(): + logger.verbose("Trying editor document") + tHandle = self.docEditor.theHandle + + if tHandle is not None: + self.saveDocument() + else: + logger.verbose("Trying selected document") + tHandle = self.treeView.getSelectedHandle() + + if tHandle is None: + logger.verbose("Trying last viewed document") + tHandle = self.theProject.lastViewed + + if tHandle is None: + logger.verbose("No document to view, giving up") + return False # Make sure main tab is in Editor view self.tabWidget.setCurrentWidget(self.splitDocs) + logger.debug("Viewing document with handle %s" % tHandle) if self.docViewer.loadText(tHandle): if not self.splitView.isVisible(): bPos = self.splitMain.sizes() From 747ff63a9247db4bb4c2c6d497f104f8da83de0c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 23:01:22 +0200 Subject: [PATCH 20/67] Also save first if pressing the relaod button from the doc viewer header bar --- nw/gui/docviewer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index b280aa01..93887fdf 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -529,6 +529,8 @@ class GuiDocViewHeader(QWidget): def _refreshDocument(self): """Reload the content of the document. """ + if self.docViewer.theHandle == self.theParent.docEditor.theHandle: + self.theParent.saveDocument() self.docViewer.reloadText() return From 5a373f1cda304ec05028466712e7801ad0911d92 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 23:18:17 +0200 Subject: [PATCH 21/67] Save the window size and replace column width in project settings --- nw/core/options.py | 5 +++++ nw/gui/build.py | 4 ++-- nw/gui/projsettings.py | 41 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index 25bf1fb4..0ff7c919 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -78,6 +78,11 @@ class OptionState(): "columnWidth", "columnHidden", }, + "GuiProjectSettings": { + "winWidth", + "winHeight", + "replaceColW", + } } return diff --git a/nw/gui/build.py b/nw/gui/build.py index 51a9dd68..e949a846 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -871,8 +871,8 @@ class GuiBuildNovel(QDialog): "section" : self.fmtSection.text().strip(), }) - winWidth = self.mainConf.pxInt(self.width()) - winHeight = self.mainConf.pxInt(self.height()) + winWidth = self.mainConf.rpxInt(self.width()) + winHeight = self.mainConf.rpxInt(self.height()) justifyText = self.justifyText.isChecked() noStyling = self.noStyling.isChecked() textFont = self.textFont.text() diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index 8f5dacb3..6f8f32ba 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -51,9 +51,17 @@ class GuiProjectSettings(PagedDialog): self.mainConf = nw.CONFIG self.theParent = theParent self.theProject = theProject + self.optState = theProject.optState self.theProject.countStatus() self.setWindowTitle("Project Settings") + self.setMinimumWidth(self.mainConf.pxInt(570)) + self.setMinimumHeight(self.mainConf.pxInt(355)) + + self.resize( + self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winWidth", 570)), + self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winHeight", 355)) + ) self.tabMain = GuiProjectEditMain(self.theParent, self.theProject) self.tabMeta = GuiProjectEditMeta(self.theParent, self.theProject) @@ -76,7 +84,13 @@ class GuiProjectSettings(PagedDialog): return + ## + # Slots + ## + def _doSave(self): + """Save settings and close dialog. + """ logger.verbose("GuiProjectSettings save button clicked") projName = self.tabMain.editName.text() @@ -100,13 +114,23 @@ class GuiProjectSettings(PagedDialog): newList = self.tabReplace.getNewList() self.theProject.setAutoReplace(newList) - self.close() + self._doClose() return def _doClose(self): - logger.verbose("GuiProjectSettings close button clicked") + """Close the dialog. + """ + winWidth = self.mainConf.rpxInt(self.width()) + winHeight = self.mainConf.rpxInt(self.height()) + replaceColW = self.mainConf.rpxInt(self.tabReplace.listBox.columnWidth(0)) + + self.optState.setValue("GuiProjectSettings", "winWidth", winWidth) + self.optState.setValue("GuiProjectSettings", "winHeight", winHeight) + self.optState.setValue("GuiProjectSettings", "replaceColW", replaceColW) + self.close() + return # END Class GuiProjectSettings @@ -452,16 +476,23 @@ class GuiProjectEditReplace(QWidget): def __init__(self, theParent, theProject): QWidget.__init__(self, theParent) + self.mainConf = nw.CONFIG self.theParent = theParent self.theTheme = theParent.theTheme self.theProject = theProject + self.optState = theProject.optState self.arChanged = False - self.outerBox = QVBoxLayout() - self.bottomBox = QHBoxLayout() - self.listBox = QTreeWidget() + self.outerBox = QVBoxLayout() + self.bottomBox = QHBoxLayout() + + wCol0 = self.mainConf.pxInt( + self.optState.getInt("GuiProjectSettings", "replaceColW", 100) + ) + self.listBox = QTreeWidget() self.listBox.setHeaderLabels(["Keyword","Replace With"]) self.listBox.itemSelectionChanged.connect(self._selectedItem) + self.listBox.setColumnWidth(0, wCol0) self.listBox.setIndentation(0) for aKey, aVal in self.theProject.autoReplace.items(): From 1d6f346955fd73b4b56c5a7bd497449e519c5c73 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 20 Jun 2020 18:17:48 +0200 Subject: [PATCH 22/67] Don't scroll project tree every time a document is opened --- nw/gui/doceditor.py | 2 +- nw/gui/docviewer.py | 2 +- nw/gui/projtree.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 3af27d07..b932b42c 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -1815,7 +1815,7 @@ class GuiDocEditHeader(QWidget): """Capture a click on the title and ensure that the item is selected in the project tree. """ - self.theParent.treeView.setSelectedHandle(self.theHandle) + self.theParent.treeView.setSelectedHandle(self.theHandle, doScroll=True) return # END Class GuiDocEditHeader diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 93887fdf..17a7c079 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -542,7 +542,7 @@ class GuiDocViewHeader(QWidget): """Capture a click on the title and ensure that the item is selected in the project tree. """ - self.theParent.treeView.setSelectedHandle(self.theHandle) + self.theParent.treeView.setSelectedHandle(self.theHandle, doScroll=True) return # END Class GuiDocViewHeader diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 236b9f90..7c2bc7a4 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -569,14 +569,14 @@ class GuiProjectTree(QTreeWidget): selHandles.append(selItems[n].data(self.C_NAME, Qt.UserRole)) return selHandles - def setSelectedHandle(self, tHandle): + def setSelectedHandle(self, tHandle, doScroll=False): """Set a specific handle as the selected item. """ if tHandle in self.theMap: self.clearSelection() self.theMap[tHandle].setSelected(True) selItems = self.selectedIndexes() - if selItems: + if selItems and doScroll: self.scrollTo( selItems[0], QAbstractItemView.PositionAtCenter ) From b46177b40b5709e746ee377303cba085001a8ca3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 20 Jun 2020 19:47:39 +0200 Subject: [PATCH 23/67] Fixed test --- tests/reference/gui/3_nwProject.nwx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index 038ee8f2..4d6ef96a 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -41,14 +41,14 @@ ROOT NOVEL New - True + False New Chapter FOLDER NOVEL New - True + False Just a Page From 24d79ade586f8ce8f42e41570694e5d67a018dd1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 20 Jun 2020 20:10:31 +0200 Subject: [PATCH 24/67] Updated changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0bbdb6a..059fd081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ * Underline text formatting has been removed. It is not standard HTML5, not Markdown, and was previously implemented using the double underscore notation that in standard Markdown is renderred as bold text. Instead, novelWriter now renders a single `*` or `-` wrapping a piece of text *within* a paragraphs as italicised text, and a double `**` or `__` as bold text. The keyboard shortcuts and automatic features **only** support the `*` notation. A triple set of `***` are treated as both bold and italicised. PR #310. * Strikethrough formatting has been added back into novelWriter using the standard Markdown `~~` wrapping. PR #310. +* Added support for thin spaces and non-breaking thin spaces. PR #319. +* The `Ctrl+Z` key sequence would not go through the wrapper function for document action for the document editor, but act directly on the document. This caused some of the logic preventing conflict between auto-replace and undo to be bypassed. This has now been resolved by blocking the keypress itself. Issue #320, PR #321. +* The dialog window size and column width setting for the auto-replace feature in Project Settings are now preserved between closing and opening the dialog. Issue #322, PR #324. **User Interface** @@ -13,6 +16,10 @@ * The text emphasis functions, either selected from the menu or via keyboard shortcuts, will now try to respond to the command in a more meaningful way. That is, the text editor will try to toggle the bold or italics features independently of eachother on the selected text. A feature to apply both at the same time has also been added. PR #310. * The document editor search tool has been completely rewritten. It now appears as a search box at the top of the document, and has a number of toggle switches added to it. You can modify the search tool to be case sensitive, select only whole words, use regular expression search strings, loop when reaching the end, and continue the search in next file. For the replace feature, you can also select to have the feature try to preserve the case of the replaced word. Issues #84 and #305, PR #314. * A dialog has been added for selecting quotation mark style. These are now used in the Preferences dialog instead of a plain text box. PR #317. +* Added an insert menu for inserting special symbols like dashes, ellipsis, thin and non-breaking spaces, and hard line breaks. PR #319. +* A menu option to replace straight single and double quotes in a selected piece of text has been added. This uses the same logic as the auto-replace feature. Issue #312, PR #321. +* When pressing `Ctrl+R` while the document editor has focus, the edited document will be viewed or refreshed in the document viewer. Previously, the selected document in the project tree had priority. The document is also now saved before loaded in the viewer, censuring that it shows the very latest changes. Issue #143, PR #323. +* The selection in the project tree should not scroll into view when just opening the document. This can be quite annoying if loading several documents in sequence by double-clicking, as the target may move just when you're about to click. PR #325. **Other Changes** From 51ddfb3cee068bf3cbf28bec5f6bb1151f15bedc Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 20 Jun 2020 20:12:12 +0200 Subject: [PATCH 25/67] Fixed a typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 059fd081..714ed4ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Core Functionality** -* Underline text formatting has been removed. It is not standard HTML5, not Markdown, and was previously implemented using the double underscore notation that in standard Markdown is renderred as bold text. Instead, novelWriter now renders a single `*` or `-` wrapping a piece of text *within* a paragraphs as italicised text, and a double `**` or `__` as bold text. The keyboard shortcuts and automatic features **only** support the `*` notation. A triple set of `***` are treated as both bold and italicised. PR #310. +* Underline text formatting has been removed. It is not standard HTML5, not Markdown, and was previously implemented using the double underscore notation that in standard Markdown is renderred as bold text. Instead, novelWriter now renders a single `*` or `_` wrapping a piece of text *within* a paragraphs as italicised text, and a double `**` or `__` as bold text. The keyboard shortcuts and automatic features **only** support the `*` notation. A triple set of `***` are treated as both bold and italicised. PR #310. * Strikethrough formatting has been added back into novelWriter using the standard Markdown `~~` wrapping. PR #310. * Added support for thin spaces and non-breaking thin spaces. PR #319. * The `Ctrl+Z` key sequence would not go through the wrapper function for document action for the document editor, but act directly on the document. This caused some of the logic preventing conflict between auto-replace and undo to be bypassed. This has now been resolved by blocking the keypress itself. Issue #320, PR #321. From 5eda8ddd13fed12e2ad7797aeb100b720fe1dff1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 20 Jun 2020 20:24:31 +0200 Subject: [PATCH 26/67] Bumped version to 0.9 and updated changelog --- CHANGELOG.md | 18 +++++++++--------- docs/source/conf.py | 2 +- nw/__init__.py | 6 +++--- setup.py | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 714ed4ee..365f492b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,29 @@ # novelWriter ChangeLog -## Version 0.9rc1 [2020-xx-xx] +## Version 0.9 [2020-06-21] **Core Functionality** -* Underline text formatting has been removed. It is not standard HTML5, not Markdown, and was previously implemented using the double underscore notation that in standard Markdown is renderred as bold text. Instead, novelWriter now renders a single `*` or `_` wrapping a piece of text *within* a paragraphs as italicised text, and a double `**` or `__` as bold text. The keyboard shortcuts and automatic features **only** support the `*` notation. A triple set of `***` are treated as both bold and italicised. PR #310. +* Underline text formatting has been removed. It is not standard HTML5, nor Markdown, and was previously implemented using the double underscore notation that in standard Markdown is renderred as bold text. Instead, novelWriter now renders a single `*` or `_` wrapping a piece of text *within* a paragraphs as italicised text, and a double `**` or `__` as bold text. The keyboard shortcuts and automatic features *only* support the `*` notation. A triple set of `***` are treated as both bold and italicised. PR #310. * Strikethrough formatting has been added back into novelWriter using the standard Markdown `~~` wrapping. PR #310. * Added support for thin spaces and non-breaking thin spaces. PR #319. -* The `Ctrl+Z` key sequence would not go through the wrapper function for document action for the document editor, but act directly on the document. This caused some of the logic preventing conflict between auto-replace and undo to be bypassed. This has now been resolved by blocking the keypress itself. Issue #320, PR #321. +* The `Ctrl+Z` key sequence (undo) would not go through the wrapper function for document action for the document editor, but act directly on the document. This caused some of the logic preventing conflict between auto-replace and undo to be bypassed. This has now been resolved by blocking the keypress itself and let the menu action handle the key sequence. Issue #320, PR #321. * The dialog window size and column width setting for the auto-replace feature in Project Settings are now preserved between closing and opening the dialog. Issue #322, PR #324. **User Interface** * The Open Project dialog will now ask before removing an entry from the recent projects list. PR #309. -* The text emphasis functions, either selected from the menu or via keyboard shortcuts, will now try to respond to the command in a more meaningful way. That is, the text editor will try to toggle the bold or italics features independently of eachother on the selected text. A feature to apply both at the same time has also been added. PR #310. -* The document editor search tool has been completely rewritten. It now appears as a search box at the top of the document, and has a number of toggle switches added to it. You can modify the search tool to be case sensitive, select only whole words, use regular expression search strings, loop when reaching the end, and continue the search in next file. For the replace feature, you can also select to have the feature try to preserve the case of the replaced word. Issues #84 and #305, PR #314. -* A dialog has been added for selecting quotation mark style. These are now used in the Preferences dialog instead of a plain text box. PR #317. +* The text emphasis functions, either selected from the menu or via keyboard shortcuts, will now try to respond to the command in a more meaningful way. That is, the text editor will try to toggle the bold or italics features independently of eachother on the selected text. A menu entry to apply both at the same time has also been added. PR #310. +* The document editor search tool has been completely rewritten. It now appears as a search box at the top of the document, and has a number of toggle switches added to it. You can modify the search tool to be case sensitive, select only whole words, use regular expression search strings, loop when reaching the end, and continue the search in the next file in the project tree. For the replace feature, you can also select to have the feature try to preserve the case of the replaced word. Issues #84 and #305, PR #314. +* A dialog has been added for selecting quotation mark style. These are now used in the Preferences dialog instead of a plain text edit box. PR #317. * Added an insert menu for inserting special symbols like dashes, ellipsis, thin and non-breaking spaces, and hard line breaks. PR #319. * A menu option to replace straight single and double quotes in a selected piece of text has been added. This uses the same logic as the auto-replace feature. Issue #312, PR #321. -* When pressing `Ctrl+R` while the document editor has focus, the edited document will be viewed or refreshed in the document viewer. Previously, the selected document in the project tree had priority. The document is also now saved before loaded in the viewer, censuring that it shows the very latest changes. Issue #143, PR #323. -* The selection in the project tree should not scroll into view when just opening the document. This can be quite annoying if loading several documents in sequence by double-clicking, as the target may move just when you're about to click. PR #325. +* When pressing `Ctrl+R` while the document editor has focus, the edited document will be viewed or refreshed in the document viewer. Previously, the selected document in the project tree had priority. The document is also now saved before loaded in the viewer, ensuring that it shows the very latest changes. Issue #143, PR #323. +* The selection in the project tree should not scroll into view when just opening the document. This can be quite annoying if loading several documents in sequence by double-clicking as the target may move just when you're about to click. PR #325. **Other Changes** -* Added the file's class and layout to the meta data string added as the first line of saved document files. This meta data is only used to restore the file meta information into the project if it was lost from the project file. It is also useful information when reading the file in external tools. PR #308. +* Added the file's class and layout to the meta data string of saved document files. This meta data is only used to restore the file meta information into the project if it was lost from the project file. It is also useful information when reading the file in external tools. PR #308. ## Version 0.8 [2020-06-14] diff --git a/docs/source/conf.py b/docs/source/conf.py index ddaebd44..d0ca4570 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,7 +26,7 @@ author = "Veronica Berglyd Olsen" # The short X.Y version version = "0.9" # The full version, including alpha/beta/rc tags -release = "0.9.0rc1" +release = "0.9" # -- General configuration --------------------------------------------------- diff --git a/nw/__init__.py b/nw/__init__.py index 92fd0ac2..430e5ec6 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -40,9 +40,9 @@ __package__ = "novelWriter" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen" __license__ = "GPLv3" -__version__ = "0.9.0rc1" -__hexversion__ = "0x000900c1" -__date__ = "2020-06-14" +__version__ = "0.9.0" +__hexversion__ = "0x000900f0" +__date__ = "2020-06-21" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" __status__ = "Pre-Release" diff --git a/setup.py b/setup.py index b8246fd5..13e29d76 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ with open("README.md", "r") as inFile: setuptools.setup( name = "novelWriter", - version = "0.9rc1", + version = "0.9", author = "Veronica Berglyd Olsen", author_email = "code@vkbo.net", description = "A markdown-like document editor for writing novels", From 02da62f120f33d4b17aeb5fdc03d4726be39211c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 20 Jun 2020 20:31:50 +0200 Subject: [PATCH 27/67] Fixed typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 365f492b..415a7075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,7 @@ * Icons have been added to the Title and Document columns in the Outline. The titles get a new icon indicating the header level, while the documents get the already existing document icon. PR #302. * Added a context menu to the project tree for easier access to some of the most use actions on the tree. PR #282. * Improved the support for High DPI screens. Margins and box sizes that were hardcoded should now scale. User settings should also scale back and forth when switching between scale factors. Issue #280, PR #285. -* The total edit time of a project is no displayed on the Details tab of the Project Settings dialog. PR #290. +* The total edit time of a project is now displayed on the Details tab of the Project Settings dialog. PR #290. * The title bar in the document editor now has a full screen button and a close button, and in the document viewer a reload button and a close button. The full screen button toggles the distraction free mode, and the reload button regenerates the document being viewed to update any changes that may have been made to it. PRs #293, #300, #303 and #306. * The References panel below the document viewer has been redesigned. It now sits in a resizeable panel below the document, and its controls sit in a footer bar in the document itself. The functionality of the feature is otherwise unchanged, but the buttons have received new icons. PRs #304 and #306. * The option to render comments and synopsis in the document view panel has been added to Preferences. The toggle option for comments that was previously in the menu has been removed. PR #311. From afd6c4660981f9ce4af1401bad55da55155d97bb Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 20 Jun 2020 20:43:12 +0200 Subject: [PATCH 28/67] Updated docs --- docs/source/interface.rst | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/source/interface.rst b/docs/source/interface.rst index 3174a888..005efd92 100644 --- a/docs/source/interface.rst +++ b/docs/source/interface.rst @@ -60,8 +60,10 @@ The editor also has a minimal set of keywords used for setting tags and referenc The editor and viewer also supports markdown standard hard line breaks, and preserves non-breaking spaces. A hard line break is achieved by leaving two or more spaces at the end of the line. -Alternatively, the user can press :kbd:`Shift-Enter` to insert this. -A non-breaking space is inserted with :kbd:`Shift-Space`. +Alternatively, the user can press :kbd:`Ctrl-K, Return` to insert this. +A non-breaking space is inserted with :kbd:`Ctrl-K, Space`. + +Thin spaces are also supported, and can be inserted with :kbd:`Ctrl-K, Shift-Space`, and the non-breaking version of it with :kbd:`Ctrl-K, Ctrl-Space`. Both hard line breaks and non-breaking spaces are highlighted by the syntax highlighter as an alternate coloured background, depending on the selected theme. @@ -133,7 +135,6 @@ These are as following: ":kbd:`Ctrl-Q`", "Exit novelWriter." ":kbd:`Ctrl-R`", "If in tree view, open a document for viewing. If editor pane has focus, open current document for viewing." ":kbd:`Ctrl-S`", "Save the current document in the editor." - ":kbd:`Ctrl-U`", "Format selected text, or word under cursor, as underline." ":kbd:`Ctrl-V`", "Paste text from clipboard to cursor position." ":kbd:`Ctrl-W`", "Close the current document in the editor." ":kbd:`Ctrl-X`", "Cut selected text to clipboard." @@ -174,3 +175,17 @@ These are as following: .. note:: On macOS, replace :kbd:`Ctrl` with :kbd:`Cmd`. + +A set of insert features are also available through shortcuts, but they require a double combination of shortcuts. + +.. csv-table:: Keyboard Shortcuts + :header: "Shortcut", "Description" + :widths: 30, 50 + + ":kbd:`Ctrl-K, -`", "Insert a short dash (en dash)." + ":kbd:`Ctrl-K, _`", "Insert a long dash (em dash)." + ":kbd:`Ctrl-K, .`", "Insert ellipsis." + ":kbd:`Ctrl-K, Return`", "Insert a hard line break." + ":kbd:`Ctrl-K, Space`", "Insert a non-breaking space." + ":kbd:`Ctrl-K, Shift-Space`", "Insert a thin space." + ":kbd:`Ctrl-K, Ctrl-Space`", "Insert a thin non-breaking space." From dac3643de6a3b9dca1397c43ea0951b32ef2746e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 21 Jun 2020 21:28:05 +0200 Subject: [PATCH 29/67] Fixed bug in search replace --- nw/gui/doceditor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index b932b42c..09e4f5e4 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -1237,10 +1237,10 @@ class GuiDocEditor(QTextEdit): self._beginSearch() return + theCursor = self.textCursor() if not theCursor.hasSelection(): return - theCursor = self.textCursor() searchFor = self.docSearch.getSearchText() replWith = self.docSearch.getReplaceText() selText = theCursor.selectedText() From 8b0e14e8aca81f7b38908432153a502912dd141a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 21 Jun 2020 21:36:49 +0200 Subject: [PATCH 30/67] Bumped version to 0.9.1 and updated changelog --- CHANGELOG.md | 7 +++++++ docs/source/conf.py | 4 ++-- nw/__init__.py | 4 ++-- setup.py | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 415a7075..0c295700 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # novelWriter ChangeLog +## Version 0.9.1 [2020-06-21] + +**Bugfixes** + +* Fixed a serious bug ib the replace part of the search/replace function that caused novelWriter to crash when the replace text function was called. Issue #327, PR #328. + + ## Version 0.9 [2020-06-21] **Core Functionality** diff --git a/docs/source/conf.py b/docs/source/conf.py index d0ca4570..b6c5da7a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -24,9 +24,9 @@ copyright = "2018-2020, Veronica Berglyd Olsen" author = "Veronica Berglyd Olsen" # The short X.Y version -version = "0.9" +version = "0.9.1" # The full version, including alpha/beta/rc tags -release = "0.9" +release = "0.9.1" # -- General configuration --------------------------------------------------- diff --git a/nw/__init__.py b/nw/__init__.py index 430e5ec6..22835b7f 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -40,8 +40,8 @@ __package__ = "novelWriter" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen" __license__ = "GPLv3" -__version__ = "0.9.0" -__hexversion__ = "0x000900f0" +__version__ = "0.9.1" +__hexversion__ = "0x000901f0" __date__ = "2020-06-21" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" diff --git a/setup.py b/setup.py index 13e29d76..c92fc797 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ with open("README.md", "r") as inFile: setuptools.setup( name = "novelWriter", - version = "0.9", + version = "0.9.1", author = "Veronica Berglyd Olsen", author_email = "code@vkbo.net", description = "A markdown-like document editor for writing novels", From 56aba17bb9dcf54ffa17d64a4cf0fcb03afa5b98 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 21 Jun 2020 21:38:03 +0200 Subject: [PATCH 31/67] Fixed typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c295700..c5d2c7b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Bugfixes** -* Fixed a serious bug ib the replace part of the search/replace function that caused novelWriter to crash when the replace text function was called. Issue #327, PR #328. +* Fixed a serious bug in the replace part of the search/replace function that caused novelWriter to crash when the replace text function was called. Issue #327, PR #328. ## Version 0.9 [2020-06-21] From a57242d26fa276bf489bc11091f3e5f4ef55806a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 22 Jun 2020 18:37:22 +0200 Subject: [PATCH 32/67] Different policy for scrolling to reveal tree item for different ways of opening a document --- nw/gui/docmerge.py | 2 +- nw/gui/outline.py | 2 +- nw/gui/projtree.py | 2 +- nw/guimain.py | 16 ++++++++-------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/nw/gui/docmerge.py b/nw/gui/docmerge.py index 1294501f..6b16102d 100644 --- a/nw/gui/docmerge.py +++ b/nw/gui/docmerge.py @@ -120,7 +120,7 @@ class GuiDocMerge(QDialog): self.theParent.treeView.revealTreeItem(nHandle) theDoc.openDocument(nHandle, False) theDoc.saveDocument(theText) - self.theParent.openDocument(nHandle) + self.theParent.openDocument(nHandle, doScroll=True) self.close() diff --git a/nw/gui/outline.py b/nw/gui/outline.py index 1b1aa11c..cd4ff718 100644 --- a/nw/gui/outline.py +++ b/nw/gui/outline.py @@ -196,7 +196,7 @@ class GuiOutline(QTreeWidget): except: tLine = 1 logger.verbose("User selected entry with handle %s on line %s" % (tHandle, tLine)) - self.theParent.openDocument(tHandle, tLine - 1) + self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True) return def _itemSelected(self): diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 7c2bc7a4..febe89a9 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -929,7 +929,7 @@ class GuiProjectTreeMenu(QMenu): """Forward the open document call to the main GUI window. """ if self.theItem is not None: - self.theTree.theParent.openDocument(self.theItem.itemHandle) + self.theTree.theParent.openDocument(self.theItem.itemHandle, doScroll=False) return def _doViewItem(self): diff --git a/nw/guimain.py b/nw/guimain.py index cd479c9e..13c1eeec 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -417,7 +417,7 @@ class GuiMain(QMainWindow): # Restore previously open documents, if any if self.theProject.lastEdited is not None: - self.openDocument(self.theProject.lastEdited) + self.openDocument(self.theProject.lastEdited, doScroll=True) if self.theProject.lastViewed is not None: self.viewDocument(self.theProject.lastViewed) @@ -461,7 +461,7 @@ class GuiMain(QMainWindow): self.docEditor.clearEditor() return True - def openDocument(self, tHandle, tLine=None, changeFocus=True): + def openDocument(self, tHandle, tLine=None, changeFocus=True, doScroll=False): """Open a specific document, optionally at a given line. """ if self.hasProject: @@ -471,7 +471,7 @@ class GuiMain(QMainWindow): if changeFocus: self.docEditor.setFocus() self.theProject.setLastEdited(tHandle) - self.treeView.setSelectedHandle(tHandle) + self.treeView.setSelectedHandle(tHandle, doScroll=doScroll) else: return False return True @@ -499,10 +499,10 @@ class GuiMain(QMainWindow): break if nHandle is not None: - self.openDocument(nHandle, tLine=0) + self.openDocument(nHandle, tLine=0, doScroll=True) return True elif wrapAround: - self.openDocument(fHandle, tLine=0) + self.openDocument(fHandle, tLine=0, doScroll=True) return False return False @@ -656,7 +656,7 @@ class GuiMain(QMainWindow): nwItem = self.theProject.projTree[tHandle] if nwItem.itemType == nwItemType.FILE: logger.verbose("Requested item %s is a file" % tHandle) - self.openDocument(tHandle) + self.openDocument(tHandle, doScroll=False) else: logger.verbose("Requested item %s is not a file" % tHandle) @@ -1101,7 +1101,7 @@ class GuiMain(QMainWindow): if nwItem is not None: if nwItem.itemType == nwItemType.FILE: logger.verbose("Requested item %s is a file" % tHandle) - self.openDocument(tHandle, changeFocus=False) + self.openDocument(tHandle, changeFocus=False, doScroll=False) else: logger.verbose("Requested item %s is a folder" % tHandle) return @@ -1117,7 +1117,7 @@ class GuiMain(QMainWindow): if nwItem is not None: if nwItem.itemType == nwItemType.FILE: logger.verbose("Requested item %s is a file" % tHandle) - self.openDocument(tHandle, changeFocus=False) + self.openDocument(tHandle, changeFocus=False, doScroll=False) else: logger.verbose("Requested item %s is a folder" % tHandle) return From bc6751ba00c93d9378a303536d707cd1e90f8e8c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 22 Jun 2020 19:07:30 +0200 Subject: [PATCH 33/67] Move to trash and drag'n'drop items now updates word counts --- nw/gui/projtree.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 7c2bc7a4..01f53390 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -237,8 +237,8 @@ class GuiProjectTree(QTreeWidget): """ if qApp.focusWidget() == self and self.theParent.hasProject: tHandle = self.getSelectedHandle() - tItem = self._getTreeItem(tHandle) - pItem = tItem.parent() + tItem = self._getTreeItem(tHandle) + pItem = tItem.parent() if pItem is None: tIndex = self.indexOfTopLevelItem(tItem) nChild = self.topLevelItemCount() @@ -367,6 +367,7 @@ class GuiProjectTree(QTreeWidget): if nwItemS is None: return False + wCount = int(trItemS.text(self.C_COUNT)) if nwItemS.itemType == nwItemType.FILE: logger.debug("User requested file %s moved to trash" % tHandle) trItemP = trItemS.parent() @@ -422,10 +423,12 @@ class GuiProjectTree(QTreeWidget): if pHandle is None: logger.warning("File has no parent item") + self.propagateCount(tHandle, 0) tIndex = trItemP.indexOfChild(trItemS) trItemC = trItemP.takeChild(tIndex) trItemT.addChild(trItemC) nwItemS.setParent(self.theProject.projTree.trashRoot()) + self.propagateCount(tHandle, wCount) self._setTreeChanged(True) self.theParent.theIndex.deleteHandle(tHandle) @@ -504,7 +507,7 @@ class GuiProjectTree(QTreeWidget): """ tItem = self._getTreeItem(tHandle) if tItem is not None: - tItem.setText(self.C_COUNT,str(theCount)) + tItem.setText(self.C_COUNT, str(theCount)) pItem = tItem.parent() if pItem is not None: pCount = 0 @@ -629,23 +632,26 @@ class GuiProjectTree(QTreeWidget): logger.error("Invalid drop index") return - dItem = self.itemFromIndex(dIndex) + sItem = self._getTreeItem(sHandle) + dItem = self.itemFromIndex(dIndex) dHandle = dItem.data(self.C_NAME, Qt.UserRole) - snItem = self.theProject.projTree[sHandle] - dnItem = self.theProject.projTree[dHandle] + snItem = self.theProject.projTree[sHandle] + dnItem = self.theProject.projTree[dHandle] if dnItem is None: self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) return - isSame = snItem.itemClass == dnItem.itemClass - isNone = snItem.itemClass == nwItemClass.NO_CLASS - isNote = snItem.itemLayout == nwItemLayout.NOTE - onFile = dnItem.itemType == nwItemType.FILE - isRoot = snItem.itemType == nwItemType.ROOT - onRoot = dnItem.itemType == nwItemType.ROOT + wCount = int(sItem.text(self.C_COUNT)) + isSame = snItem.itemClass == dnItem.itemClass + isNone = snItem.itemClass == nwItemClass.NO_CLASS + isNote = snItem.itemLayout == nwItemLayout.NOTE + onFile = dnItem.itemType == nwItemType.FILE + isRoot = snItem.itemType == nwItemType.ROOT + onRoot = dnItem.itemType == nwItemType.ROOT isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem if (isSame or isNone or isNote) and not (onFile and isOnTop) and not isRoot: logger.debug("Drag'n'drop of item %s accepted" % sHandle) + self.propagateCount(sHandle, 0) QTreeWidget.dropEvent(self, theEvent) if isNone: self._moveOrphanedItem(sHandle, dHandle) @@ -660,6 +666,7 @@ class GuiProjectTree(QTreeWidget): )) snItem.setClass(dnItem.itemClass) self.setTreeItemValues(sHandle) + self.propagateCount(sHandle, wCount) else: logger.debug("Drag'n'drop of item %s not accepted" % sHandle) self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) @@ -794,8 +801,7 @@ class GuiProjectTree(QTreeWidget): def _updateItemParent(self, tHandle): """Update the parent handle of an item so that the information - in the project is consistent with the treeView. Also move the - word count over to the new parent tree. + in the project is consistent with the treeView. """ trItemS = self._getTreeItem(tHandle) nwItemS = self.theProject.projTree[tHandle] @@ -805,10 +811,7 @@ class GuiProjectTree(QTreeWidget): return False pHandle = trItemP.data(self.C_NAME, Qt.UserRole) - wC = int(trItemS.text(self.C_COUNT)) - self.propagateCount(tHandle, -wC) nwItemS.setParent(pHandle) - self.propagateCount(tHandle, wC) self.setTreeItemValues(tHandle) self._setTreeChanged(True) From bce23c69e144c6d0f065d6c9137f0d5b8c8aa820 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 22 Jun 2020 19:22:26 +0200 Subject: [PATCH 34/67] Also remove word count when a file is permamently deleted --- nw/gui/projtree.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 01f53390..968cfc9b 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -394,7 +394,8 @@ class GuiProjectTree(QTreeWidget): if doPermanent: logger.debug("Permanently deleting file with handle %s" % tHandle) - tIndex = trItemP.indexOfChild(trItemS) + self.propagateCount(tHandle, 0) + tIndex = trItemP.indexOfChild(trItemS) trItemC = trItemP.takeChild(tIndex) if self.theParent.docEditor.theHandle == tHandle: @@ -523,7 +524,7 @@ class GuiProjectTree(QTreeWidget): relevant values in the project and on the status bar. This call is a fast way of getting this number, and depends on the propagateCount function being called when it should to maintain - the correct count. + the correct count. Orphan folder is not included in the total. """ nWords = 0 for n in range(self.topLevelItemCount()): From b47d6b65461f1ca7d4060a5f7761b0f1de4e810d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 23 Jun 2020 19:55:19 +0200 Subject: [PATCH 35/67] Added a document header to the build tool document stating when it was generated --- nw/gui/build.py | 84 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index e949a846..b7126580 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -31,6 +31,7 @@ import nw from os import path from time import time +from datetime import datetime from PyQt5.QtCore import Qt, QByteArray from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog @@ -39,7 +40,7 @@ from PyQt5.QtGui import ( ) from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel, - QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction, + QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction, QFrame, QFileDialog, QFontDialog, QSpinBox ) @@ -76,6 +77,7 @@ class GuiBuildNovel(QDialog): self.htmlText = [] # List of html document self.htmlStyle = [] # List of html styles self.nwdText = [] # List of markdown documents + self.buildTime = 0 # The timestamp of the last build self.setWindowTitle("Build Novel Project") self.setMinimumWidth(self.mainConf.pxInt(900)) @@ -411,11 +413,12 @@ class GuiBuildNovel(QDialog): self.docView.clearStyleSheet() else: self.docView.setStyleSheet(self.htmlStyle) - self.docView.setContent(self.htmlText) + self.docView.setContent(self.htmlText, self.buildTime) else: self.htmlText = [] self.htmlStyle = [] self.nwdText = [] + self.buildTime = 0 return @@ -509,6 +512,7 @@ class GuiBuildNovel(QDialog): tEnd = time() logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart))) self.htmlStyle = makeHtml.getStyleSheet() + self.buildTime = tEnd # Load the preview document with the html data self.docView.setTextFont(textFont, textSize) @@ -517,7 +521,7 @@ class GuiBuildNovel(QDialog): self.docView.clearStyleSheet() else: self.docView.setStyleSheet(self.htmlStyle) - self.docView.setContent(self.htmlText) + self.docView.setContent(self.htmlText, self.buildTime) self._saveCache() @@ -694,6 +698,7 @@ class GuiBuildNovel(QDialog): "workingTitle" : self.theProject.projName, "novelTitle" : self.theProject.bookTitle, "authors" : self.theProject.bookAuthors, + "buildTime" : self.buildTime, } } @@ -808,6 +813,8 @@ class GuiBuildNovel(QDialog): if "nwdText" in theData.keys(): self.nwdText = theData["nwdText"] dataCount += 1 + if "buildTime" in theData.keys(): + self.buildTime = theData["buildTime"] return dataCount == 3 @@ -828,6 +835,7 @@ class GuiBuildNovel(QDialog): "htmlText" : self.htmlText, "htmlStyle" : self.htmlStyle, "nwdText" : self.nwdText, + "buildTime" : self.buildTime, }, indent=nIndent)) except Exception as e: logger.error("Failed to save build cache") @@ -924,12 +932,13 @@ class GuiBuildNovelDocView(QTextBrowser): self.mainConf = nw.CONFIG self.theProject = theProject self.theParent = theParent + self.theTheme = theParent.theTheme self.setMinimumWidth(40*self.theParent.theTheme.textNWidth) self.setOpenExternalLinks(False) self.qDocument = self.document() - self.qDocument.setDocumentMargin(self.mainConf.textMargin) + self.qDocument.setDocumentMargin(self.mainConf.getTextMargin()) self.setPlaceholderText( "This area will show the content of the document to be " "exported or printed. Press the \"Build Novel Project\" " @@ -946,12 +955,29 @@ class GuiBuildNovelDocView(QTextBrowser): docPalette = self.palette() docPalette.setColor(QPalette.Base, QColor(255, 255, 255)) - docPalette.setColor(QPalette.Text, QColor( 0, 0, 0)) + docPalette.setColor(QPalette.Text, QColor(0, 0, 0)) self.setPalette(docPalette) - self.setStyleSheet() + lblPalette = self.palette() + lblPalette.setColor(QPalette.Background, lblPalette.toolTipBase().color()) + lblPalette.setColor(QPalette.Foreground, lblPalette.toolTipText().color()) - self.show() + lblFont = self.font() + lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + + fPx = int(1.1*self.theTheme.fontPixelSize) + mPx = self.mainConf.pxInt(4) + + self.theTitle = QLabel("", self) + self.theTitle.setIndent(0) + self.theTitle.setAutoFillBackground(True) + self.theTitle.setAlignment(Qt.AlignCenter) + self.theTitle.setFixedHeight(fPx) + self.theTitle.setPalette(lblPalette) + self.theTitle.setFont(lblFont) + + self._updateDocMargins() + self.setStyleSheet() logger.debug("GuiBuildNovelDocView initialisation complete") @@ -977,15 +1003,23 @@ class GuiBuildNovelDocView(QTextBrowser): self.setFont(theFont) return - def setContent(self, theText): + def setContent(self, theText, timeStamp): """Set the content, either from text or list of text. """ if isinstance(theText, list): theText = "".join(theText) + theText = theText.replace(" ", " "*4) theText = theText.replace("", "") theText = theText.replace("", "") self.setHtml(theText) + + if timeStamp > 0: + strBuildTime = datetime.fromtimestamp(timeStamp).strftime("%x %X") + else: + strBuildTime = "Unknown" + self.theTitle.setText("Build Time: %s" % strBuildTime) + return def setStyleSheet(self, theStyles=[]): @@ -1007,4 +1041,38 @@ class GuiBuildNovelDocView(QTextBrowser): self.qDocument.setDefaultStyleSheet("") return + ## + # Events + ## + + def resizeEvent(self, theEvent): + """Make sure the document title is the same width as the window. + """ + QTextBrowser.resizeEvent(self, theEvent) + self._updateDocMargins() + return + + ## + # Internal Functions + ## + + def _updateDocMargins(self): + """Automatically adjust the header to fill the top of the + document within the viewport. + """ + vBar = self.verticalScrollBar() + if vBar.isVisible(): + sW = vBar.width() + else: + sW = 0 + + tB = self.frameWidth() + tW = self.width() - 2*tB - sW + tH = self.theTitle.height() + + self.theTitle.setGeometry(tB, tB, tW, tH) + self.setViewportMargins(0, tH, 0, 0) + + return + # END Class GuiBuildNovelDocView From bc8873fdd44b9392e2ef0eedc6d0e88ca0639323 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 23 Jun 2020 20:23:52 +0200 Subject: [PATCH 36/67] Also added a fuzzy time string to the build doc header to make it easier to see the age of the build --- nw/common.py | 24 ++++++++++++++++++++++++ nw/gui/build.py | 32 +++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/nw/common.py b/nw/common.py index 0019c38a..8af90084 100644 --- a/nw/common.py +++ b/nw/common.py @@ -193,3 +193,27 @@ def transferCase(theSource, theTarget): theResult = theTarget.lower() return theResult + +def fuzzyTime(secDiff): + """Converts a time difference in seconds into a fuzzy time string. + """ + if secDiff < 0: + return "in the future" + elif secDiff < 15: + return "just now" + elif secDiff < 45: + return "a few seconds ago" + elif secDiff < 90: + return "a minute ago" + elif secDiff < 3300: # 55 minutes + return "%d minutes ago" % int(round(secDiff/60)) + elif secDiff < 5400: # 90 minutes + return "an hour ago" + elif secDiff < 84600: # 23.5 hours + return "%d hours ago" % int(round(secDiff/3600)) + elif secDiff < 129600: # 1.5 days + return "a day ago" + elif secDiff < 8640000: # 100 days + return "%d days ago" % int(round(secDiff/86400)) + else: + return "ages ago" diff --git a/nw/gui/build.py b/nw/gui/build.py index b7126580..4765bd7e 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -33,7 +33,7 @@ from os import path from time import time from datetime import datetime -from PyQt5.QtCore import Qt, QByteArray +from PyQt5.QtCore import Qt, QByteArray, QTimer from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog from PyQt5.QtGui import ( QPalette, QColor, QTextDocumentWriter, QFont @@ -44,6 +44,7 @@ from PyQt5.QtWidgets import ( QFileDialog, QFontDialog, QSpinBox ) +from nw.common import fuzzyTime from nw.gui.custom import QSwitch from nw.core import ToHtml from nw.constants import ( @@ -933,6 +934,7 @@ class GuiBuildNovelDocView(QTextBrowser): self.theProject = theProject self.theParent = theParent self.theTheme = theParent.theTheme + self.buildTime = 0 self.setMinimumWidth(40*self.theParent.theTheme.textNWidth) self.setOpenExternalLinks(False) @@ -979,6 +981,12 @@ class GuiBuildNovelDocView(QTextBrowser): self._updateDocMargins() self.setStyleSheet() + # Age Timer + self.ageTimer = QTimer() + self.ageTimer.setInterval(5000) + self.ageTimer.timeout.connect(self._updateBuildAge) + self.ageTimer.start() + logger.debug("GuiBuildNovelDocView initialisation complete") return @@ -1009,16 +1017,13 @@ class GuiBuildNovelDocView(QTextBrowser): if isinstance(theText, list): theText = "".join(theText) + self.buildTime = timeStamp + theText = theText.replace(" ", " "*4) theText = theText.replace("", "") theText = theText.replace("", "") self.setHtml(theText) - - if timeStamp > 0: - strBuildTime = datetime.fromtimestamp(timeStamp).strftime("%x %X") - else: - strBuildTime = "Unknown" - self.theTitle.setText("Build Time: %s" % strBuildTime) + self._updateBuildAge() return @@ -1056,6 +1061,19 @@ class GuiBuildNovelDocView(QTextBrowser): # Internal Functions ## + def _updateBuildAge(self): + """ + """ + if self.buildTime > 0: + strBuildTime = "%s (%s)" % ( + datetime.fromtimestamp(self.buildTime).strftime("%x %X"), + fuzzyTime(time() - self.buildTime) + ) + else: + strBuildTime = "Unknown" + self.theTitle.setText("Build Time: %s" % strBuildTime) + + def _updateDocMargins(self): """Automatically adjust the header to fill the top of the document within the viewport. From c19c93d60c726b052b63f7240cad4b7a607afa6b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 23 Jun 2020 20:41:10 +0200 Subject: [PATCH 37/67] Some last tweaks --- nw/common.py | 5 ++++- nw/gui/build.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/nw/common.py b/nw/common.py index 8af90084..7cc2fbd3 100644 --- a/nw/common.py +++ b/nw/common.py @@ -213,7 +213,10 @@ def fuzzyTime(secDiff): return "%d hours ago" % int(round(secDiff/3600)) elif secDiff < 129600: # 1.5 days return "a day ago" - elif secDiff < 8640000: # 100 days + elif secDiff < 31104000: # 360 days return "%d days ago" % int(round(secDiff/86400)) + elif secDiff < 36288000: # 420 days + return "a year ago" else: return "ages ago" + return "beyond time and space" diff --git a/nw/gui/build.py b/nw/gui/build.py index 4765bd7e..4fc0e42d 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -983,7 +983,7 @@ class GuiBuildNovelDocView(QTextBrowser): # Age Timer self.ageTimer = QTimer() - self.ageTimer.setInterval(5000) + self.ageTimer.setInterval(10000) self.ageTimer.timeout.connect(self._updateBuildAge) self.ageTimer.start() @@ -1071,7 +1071,7 @@ class GuiBuildNovelDocView(QTextBrowser): ) else: strBuildTime = "Unknown" - self.theTitle.setText("Build Time: %s" % strBuildTime) + self.theTitle.setText("Build Time: %s" % strBuildTime) def _updateDocMargins(self): From 918d9c6470f69ce0da0de893523b3ba2723822dd Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 23 Jun 2020 20:45:34 +0200 Subject: [PATCH 38/67] Forgot to clean up imports --- nw/gui/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index 4fc0e42d..bb800c28 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -40,7 +40,7 @@ from PyQt5.QtGui import ( ) from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel, - QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction, QFrame, + QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction, QFileDialog, QFontDialog, QSpinBox ) From ca04f31309575dd73063b777dff3e202e5c26bc3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 23 Jun 2020 22:02:19 +0200 Subject: [PATCH 39/67] Just let the fuzzy time run till it counts years --- nw/common.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/nw/common.py b/nw/common.py index 7cc2fbd3..872365df 100644 --- a/nw/common.py +++ b/nw/common.py @@ -199,10 +199,8 @@ def fuzzyTime(secDiff): """ if secDiff < 0: return "in the future" - elif secDiff < 15: + elif secDiff < 30: return "just now" - elif secDiff < 45: - return "a few seconds ago" elif secDiff < 90: return "a minute ago" elif secDiff < 3300: # 55 minutes @@ -213,10 +211,19 @@ def fuzzyTime(secDiff): return "%d hours ago" % int(round(secDiff/3600)) elif secDiff < 129600: # 1.5 days return "a day ago" - elif secDiff < 31104000: # 360 days + elif secDiff < 561600: # 6.5 days return "%d days ago" % int(round(secDiff/86400)) - elif secDiff < 36288000: # 420 days + elif secDiff < 907200: # 10.5 days + return "a week ago" + elif secDiff < 2419200: # 28 days + return "%d weeks ago" % int(round(secDiff/604800)) + elif secDiff < 3888000: # 45 days + return "a month ago" + elif secDiff < 31104000: # 360 days + return "%d months ago" % int(round(secDiff/2592000)) + elif secDiff < 47304000: # 1.5 years return "a year ago" else: - return "ages ago" + return "%d years ago" % int(round(secDiff/31557600)) + return "beyond time and space" From a1d7f8296feb7e0cc176f112d95f17440d864d23 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 23 Jun 2020 22:02:57 +0200 Subject: [PATCH 40/67] Set a default build doc header --- nw/gui/build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index bb800c28..ab5b3611 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -970,7 +970,7 @@ class GuiBuildNovelDocView(QTextBrowser): fPx = int(1.1*self.theTheme.fontPixelSize) mPx = self.mainConf.pxInt(4) - self.theTitle = QLabel("", self) + self.theTitle = QLabel("Build Time: Unknown", self) self.theTitle.setIndent(0) self.theTitle.setAutoFillBackground(True) self.theTitle.setAlignment(Qt.AlignCenter) @@ -1062,7 +1062,7 @@ class GuiBuildNovelDocView(QTextBrowser): ## def _updateBuildAge(self): - """ + """Update the build time and the fuzzy age. """ if self.buildTime > 0: strBuildTime = "%s (%s)" % ( From 5913c944d9737e2f9a30f3ddfe95b166232d93ca Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 23 Jun 2020 22:40:16 +0200 Subject: [PATCH 41/67] Make a note in the docd about bold/italic etc formatting --- docs/source/interface.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/interface.rst b/docs/source/interface.rst index 005efd92..5502ae22 100644 --- a/docs/source/interface.rst +++ b/docs/source/interface.rst @@ -58,6 +58,10 @@ The editor also has a minimal set of keywords used for setting tags and referenc "``% Synopsis: text...``", "A synopsis comment. Shows up in the Synopsis column of the Outline View, but is otherwise treated as a comment." "``@keyword: value``", "A keyword argument followed by a value, or a comma separated list of values." +.. note:: + The bold/italic/strikethrough formatting tags do not allow spaces between the words and the tag itself. + That is, ``**text**`` is valid, ``**text **`` is not. + The editor and viewer also supports markdown standard hard line breaks, and preserves non-breaking spaces. A hard line break is achieved by leaving two or more spaces at the end of the line. Alternatively, the user can press :kbd:`Ctrl-K, Return` to insert this. From 996bffb21b46be15ac581fa269608d039efbe0ed Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:01:32 +0200 Subject: [PATCH 42/67] Bumped version for dev branch --- CHANGELOG.md | 3 +++ docs/source/conf.py | 4 ++-- nw/__init__.py | 4 ++-- setup.py | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5d2c7b9..1a3dbb87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # novelWriter ChangeLog +## Version 0.10.0 RC1 [2020-xx-xx] + + ## Version 0.9.1 [2020-06-21] **Bugfixes** diff --git a/docs/source/conf.py b/docs/source/conf.py index b6c5da7a..d8307be6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -24,9 +24,9 @@ copyright = "2018-2020, Veronica Berglyd Olsen" author = "Veronica Berglyd Olsen" # The short X.Y version -version = "0.9.1" +version = "0.10.0" # The full version, including alpha/beta/rc tags -release = "0.9.1" +release = "0.10.0-rc1" # -- General configuration --------------------------------------------------- diff --git a/nw/__init__.py b/nw/__init__.py index 22835b7f..1ea3237e 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -40,8 +40,8 @@ __package__ = "novelWriter" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen" __license__ = "GPLv3" -__version__ = "0.9.1" -__hexversion__ = "0x000901f0" +__version__ = "0.10.0-rc1" +__hexversion__ = "0x001000c1" __date__ = "2020-06-21" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" diff --git a/setup.py b/setup.py index c92fc797..e93777ab 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ with open("README.md", "r") as inFile: setuptools.setup( name = "novelWriter", - version = "0.9.1", + version = "0.10.0", author = "Veronica Berglyd Olsen", author_email = "code@vkbo.net", description = "A markdown-like document editor for writing novels", From a5c83a45f50468bde2042b7fee287f853c2452d8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:20:22 +0200 Subject: [PATCH 43/67] First some code cleanup and fix of the filtered total --- nw/core/options.py | 1 - nw/gui/sessionlog.py | 86 ++++++++++++++++++++++---------------------- nw/gui/theme.py | 2 ++ 3 files changed, 45 insertions(+), 44 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index 0ff7c919..b1986505 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -48,7 +48,6 @@ class OptionState(): "GuiSession": { "widthCol0", "widthCol1", - "widthCol2", "sortCol", "sortOrder", "hideZeros", diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index 5f3c61f0..98172f0d 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -44,43 +44,45 @@ logger = logging.getLogger(__name__) class GuiSessionLogView(QDialog): + C_TIME = 0 + C_LENGTH = 1 + C_COUNT = 2 + def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) logger.debug("Initialising SessionLogView ...") self.mainConf = nw.CONFIG - self.theProject = theProject self.theParent = theParent - self.optState = self.theProject.optState + self.theProject = theProject + self.theTheme = theParent.theTheme + self.optState = theProject.optState self.timeFilter = 0.0 self.timeTotal = 0.0 - self.outerBox = QGridLayout() - self.bottomBox = QHBoxLayout() - self.setWindowTitle("Session Log") self.setMinimumWidth(self.mainConf.pxInt(420)) self.setMinimumHeight(self.mainConf.pxInt(400)) + # List Box wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol0", 180)) wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol1", 80)) - wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol2", 80)) self.listBox = QTreeWidget() - self.listBox.setHeaderLabels(["Session Start","Length","Words",""]) + self.listBox.setHeaderLabels(["Session Start","Length","Words"]) self.listBox.setIndentation(0) - self.listBox.setColumnWidth(0, wCol0) - self.listBox.setColumnWidth(1, wCol1) - self.listBox.setColumnWidth(2, wCol2) - self.listBox.setColumnWidth(3, 0) + self.listBox.setColumnWidth(self.C_TIME, wCol0) + self.listBox.setColumnWidth(self.C_LENGTH, wCol1) + self.listBox.resizeColumnToContents(self.C_COUNT) hHeader = self.listBox.headerItem() - hHeader.setTextAlignment(1,Qt.AlignRight) - hHeader.setTextAlignment(2,Qt.AlignRight) + hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight) + hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight) - self.monoFont = QFont("Monospace", 10) + self.monoFont = QFont() + self.monoFont.setFamily(self.theTheme.guiFontFixed.family()) sortValid = (Qt.AscendingOrder, Qt.DescendingOrder) sortCol = self.optState.validIntRange( @@ -95,9 +97,9 @@ class GuiSessionLogView(QDialog): self.listBox.setSortingEnabled(True) # Session Info - self.infoBox = QGroupBox("Sum Total Time", self) - self.infoBoxForm = QGridLayout(self) - self.infoBox.setLayout(self.infoBoxForm) + self.infoBox = QGroupBox("Sum Total Time", self) + self.infoForm = QGridLayout(self) + self.infoBox.setLayout(self.infoForm) self.labelTotal = QLabel(self._formatTime(0)) self.labelTotal.setFont(self.monoFont) @@ -107,15 +109,15 @@ class GuiSessionLogView(QDialog): self.labelFilter.setFont(self.monoFont) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) - self.infoBoxForm.addWidget(QLabel("All:"), 0, 0) - self.infoBoxForm.addWidget(self.labelTotal, 0, 1) - self.infoBoxForm.addWidget(QLabel("Filtered:"), 1, 0) - self.infoBoxForm.addWidget(self.labelFilter, 1, 1) + self.infoForm.addWidget(QLabel("All:"), 0, 0) + self.infoForm.addWidget(self.labelTotal, 0, 1) + self.infoForm.addWidget(QLabel("Filtered:"), 1, 0) + self.infoForm.addWidget(self.labelFilter, 1, 1) # Filter Options - self.filterBox = QGroupBox("Filters", self) - self.filterBoxForm = QGridLayout(self) - self.filterBox.setLayout(self.filterBoxForm) + self.filterBox = QGroupBox("Filters", self) + self.filterForm = QGridLayout(self) + self.filterBox.setLayout(self.filterForm) self.hideZeros = QCheckBox("Hide zero word count", self) self.hideZeros.setChecked( @@ -129,14 +131,15 @@ class GuiSessionLogView(QDialog): ) self.hideNegative.stateChanged.connect(self._doHideNegative) - self.filterBoxForm.addWidget(self.hideZeros, 0, 0) - self.filterBoxForm.addWidget(self.hideNegative, 1, 0) + self.filterForm.addWidget(self.hideZeros, 0, 0) + self.filterForm.addWidget(self.hideNegative, 1, 0) # Buttons self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) self.buttonBox.rejected.connect(self._doClose) # Assemble + self.outerBox = QGridLayout() self.outerBox.addWidget(self.listBox, 0, 0, 1, 2) self.outerBox.addWidget(self.infoBox, 1, 0) self.outerBox.addWidget(self.filterBox, 1, 1) @@ -144,8 +147,6 @@ class GuiSessionLogView(QDialog): self.setLayout(self.outerBox) - self.show() - logger.debug("SessionLogView initialisation complete") self._loadSessionLog() @@ -175,18 +176,16 @@ class GuiSessionLogView(QDialog): if len(inData) != 8: continue dStart = datetime.strptime( - "%s %s" % (inData[1],inData[2]), nwConst.tStampFmt + "%s %s" % (inData[1], inData[2]), nwConst.tStampFmt ) dEnd = datetime.strptime( - "%s %s" % (inData[4],inData[5]), nwConst.tStampFmt + "%s %s" % (inData[4], inData[5]), nwConst.tStampFmt ) nWords = int(inData[7]) tDiff = dEnd - dStart sDiff = tDiff.total_seconds() - self.timeTotal += sDiff - if abs(nWords) > 0: - self.timeFilter += sDiff + self.timeTotal += sDiff if hideZeros and nWords == 0: continue @@ -194,16 +193,19 @@ class GuiSessionLogView(QDialog): if hideNegative and nWords < 0: continue - newItem = QTreeWidgetItem( - [str(dStart), self._formatTime(sDiff), str(nWords), ""] - ) + self.timeFilter += sDiff - newItem.setTextAlignment(1,Qt.AlignRight) - newItem.setTextAlignment(2,Qt.AlignRight) + newItem = QTreeWidgetItem() + newItem.setText(self.C_TIME, str(dStart)) + newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) + newItem.setText(self.C_COUNT, str(nWords)) - newItem.setFont(0,self.monoFont) - newItem.setFont(1,self.monoFont) - newItem.setFont(2,self.monoFont) + newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight) + newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) + + newItem.setFont(self.C_TIME, self.monoFont) + newItem.setFont(self.C_LENGTH, self.monoFont) + newItem.setFont(self.C_COUNT, self.monoFont) self.listBox.addTopLevelItem(newItem) @@ -222,7 +224,6 @@ class GuiSessionLogView(QDialog): widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0)) widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1)) - widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2)) sortCol = self.listBox.sortColumn() sortOrder = self.listBox.header().sortIndicatorOrder() hideZeros = self.hideZeros.isChecked() @@ -230,7 +231,6 @@ class GuiSessionLogView(QDialog): self.optState.setValue("GuiSession", "widthCol0", widthCol0) self.optState.setValue("GuiSession", "widthCol1", widthCol1) - self.optState.setValue("GuiSession", "widthCol2", widthCol2) self.optState.setValue("GuiSession", "sortCol", sortCol) self.optState.setValue("GuiSession", "sortOrder", sortOrder) self.optState.setValue("GuiSession", "hideZeros", hideZeros) diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 7df0c560..0265df62 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -139,6 +139,8 @@ class GuiTheme: logger.verbose("GUI Scale: %.2f" % self.guiScale) self.guiFont = qApp.font() + self.guiFontFixed = QFontDatabase.systemFont(QFontDatabase.FixedFont) + qMetric = QFontMetrics(self.guiFont) self.fontPointSize = self.guiFont.pointSizeF() self.fontPixelSize = int(round(qMetric.height())) From 1110f40231b5e6736084cc58ab9c7dc2b11c09ea Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:36:08 +0200 Subject: [PATCH 44/67] Restructured the session log class and made it consistent with other source files --- nw/gui/sessionlog.py | 171 +++++++++++++++++++++++++------------------ 1 file changed, 101 insertions(+), 70 deletions(-) diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index 98172f0d..d58844d9 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -34,7 +34,7 @@ from datetime import datetime from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( - QDialog, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, + qApp, QDialog, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout, QLabel, QGroupBox, QCheckBox ) @@ -59,6 +59,7 @@ class GuiSessionLogView(QDialog): self.theTheme = theParent.theTheme self.optState = theProject.optState + self.logData = [] self.timeFilter = 0.0 self.timeTotal = 0.0 @@ -82,6 +83,7 @@ class GuiSessionLogView(QDialog): hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight) self.monoFont = QFont() + self.monoFont.setPointSizeF(0.9*self.theTheme.fontPointSize) self.monoFont.setFamily(self.theTheme.guiFontFixed.family()) sortValid = (Qt.AscendingOrder, Qt.DescendingOrder) @@ -149,78 +151,20 @@ class GuiSessionLogView(QDialog): logger.debug("SessionLogView initialisation complete") - self._loadSessionLog() + qApp.processEvents() + self._loadLogFile() + self._updateListBox() return - def _loadSessionLog(self): - - logFile = path.join(self.theProject.projMeta, nwFiles.SESS_INFO) - if not path.isfile(logFile): - logger.warning("No session log file found for this project.") - return False - - self.listBox.clear() - - self.timeFilter = 0.0 - self.timeTotal = 0.0 - - hideZeros = self.hideZeros.isChecked() - hideNegative = self.hideNegative.isChecked() - - logger.debug("Loading session log file") - try: - with open(logFile,mode="r",encoding="utf8") as inFile: - for inLine in inFile: - inData = inLine.split() - if len(inData) != 8: - continue - dStart = datetime.strptime( - "%s %s" % (inData[1], inData[2]), nwConst.tStampFmt - ) - dEnd = datetime.strptime( - "%s %s" % (inData[4], inData[5]), nwConst.tStampFmt - ) - nWords = int(inData[7]) - tDiff = dEnd - dStart - sDiff = tDiff.total_seconds() - - self.timeTotal += sDiff - - if hideZeros and nWords == 0: - continue - - if hideNegative and nWords < 0: - continue - - self.timeFilter += sDiff - - newItem = QTreeWidgetItem() - newItem.setText(self.C_TIME, str(dStart)) - newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) - newItem.setText(self.C_COUNT, str(nWords)) - - newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight) - newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) - - newItem.setFont(self.C_TIME, self.monoFont) - newItem.setFont(self.C_LENGTH, self.monoFont) - newItem.setFont(self.C_COUNT, self.monoFont) - - self.listBox.addTopLevelItem(newItem) - - except Exception as e: - self.theParent.makeAlert( - ["Failed to read session log file.",str(e)], nwAlert.ERROR - ) - return False - - self.labelFilter.setText(self._formatTime(self.timeFilter)) - self.labelTotal.setText(self._formatTime(self.timeTotal)) - - return True + ## + # Slots + ## def _doClose(self): + """Save the state of the window, clear cache, end close. + """ + self.logData = [] widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0)) widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1)) @@ -242,14 +186,101 @@ class GuiSessionLogView(QDialog): return def _doHideZeros(self, newState): - self._loadSessionLog() + """Reload the list box with the new filter settings in place. + """ + self._updateListBox() return def _doHideNegative(self, newState): - self._loadSessionLog() + """Reload the list box with the new filter settings in place. + """ + self._updateListBox() return + ## + # Internal Functions + ## + + def _loadLogFile(self): + """Load the content of the log file into a buffer. + """ + self.logData = [] + logger.debug("Loading session log file") + + try: + logFile = path.join(self.theProject.projMeta, nwFiles.SESS_INFO) + with open(logFile, mode="r", encoding="utf8") as inFile: + for inLine in inFile: + + inData = inLine.split() + if len(inData) != 8: + continue + + dStart = datetime.strptime( + "%s %s" % (inData[1], inData[2]), nwConst.tStampFmt + ) + dEnd = datetime.strptime( + "%s %s" % (inData[4], inData[5]), nwConst.tStampFmt + ) + + nWords = int(inData[7]) + tDiff = dEnd - dStart + sDiff = tDiff.total_seconds() + + self.logData.append((dStart, sDiff, nWords)) + + except Exception as e: + self.theParent.makeAlert( + ["Failed to read session log file.",str(e)], nwAlert.ERROR + ) + return False + + return True + + def _updateListBox(self): + """Load/reload the content of the list box. + """ + self.listBox.clear() + self.timeFilter = 0.0 + self.timeTotal = 0.0 + + hideZeros = self.hideZeros.isChecked() + hideNegative = self.hideNegative.isChecked() + + for dStart, sDiff, nWords in self.logData: + + self.timeTotal += sDiff + + if hideZeros and nWords == 0: + continue + + if hideNegative and nWords < 0: + continue + + self.timeFilter += sDiff + + newItem = QTreeWidgetItem() + newItem.setText(self.C_TIME, str(dStart)) + newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) + newItem.setText(self.C_COUNT, str(nWords)) + + newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight) + newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) + + newItem.setFont(self.C_TIME, self.monoFont) + newItem.setFont(self.C_LENGTH, self.monoFont) + newItem.setFont(self.C_COUNT, self.monoFont) + + self.listBox.addTopLevelItem(newItem) + + self.labelFilter.setText(self._formatTime(self.timeFilter)) + self.labelTotal.setText(self._formatTime(self.timeTotal)) + + 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 From 935971fa0ca59e6250c65a63783c0c6db3314d64 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:45:41 +0200 Subject: [PATCH 45/67] Give the session log class a shorter name --- nw/core/options.py | 2 +- nw/gui/__init__.py | 4 ++-- nw/gui/sessionlog.py | 40 +++++++++++++++++++--------------------- nw/guimain.py | 4 ++-- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index b1986505..b3a144d3 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -45,7 +45,7 @@ class OptionState(): self.theState = {} self.validMap = { - "GuiSession": { + "GuiSessionLog": { "widthCol0", "widthCol1", "sortCol", diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index 4e96b4ee..3a57b557 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -15,7 +15,7 @@ from nw.gui.preferences import GuiPreferences from nw.gui.projload import GuiProjectLoad from nw.gui.projsettings import GuiProjectSettings from nw.gui.projtree import GuiProjectTree -from nw.gui.sessionlog import GuiSessionLogView +from nw.gui.sessionlog import GuiSessionLog from nw.gui.statusbar import GuiMainStatus from nw.gui.theme import GuiIcons, GuiTheme @@ -36,7 +36,7 @@ __all__ = [ "GuiProjectLoad", "GuiProjectSettings", "GuiProjectTree", - "GuiSessionLogView", + "GuiSessionLog", "GuiMainStatus", "GuiIcons", "GuiTheme", diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index d58844d9..e7c62d5a 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -42,7 +42,7 @@ from nw.constants import nwConst, nwFiles, nwAlert logger = logging.getLogger(__name__) -class GuiSessionLogView(QDialog): +class GuiSessionLog(QDialog): C_TIME = 0 C_LENGTH = 1 @@ -51,7 +51,7 @@ class GuiSessionLogView(QDialog): def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) - logger.debug("Initialising SessionLogView ...") + logger.debug("Initialising GuiSessionLog ...") self.mainConf = nw.CONFIG self.theParent = theParent @@ -68,8 +68,8 @@ class GuiSessionLogView(QDialog): self.setMinimumHeight(self.mainConf.pxInt(400)) # List Box - wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol0", 180)) - wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol1", 80)) + wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "widthCol0", 180)) + wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "widthCol1", 80)) self.listBox = QTreeWidget() self.listBox.setHeaderLabels(["Session Start","Length","Words"]) @@ -88,10 +88,10 @@ class GuiSessionLogView(QDialog): sortValid = (Qt.AscendingOrder, Qt.DescendingOrder) sortCol = self.optState.validIntRange( - self.optState.getInt("GuiSession", "sortCol", 0), 0, 2, 0 + self.optState.getInt("GuiSessionLog", "sortCol", 0), 0, 2, 0 ) sortOrder = self.optState.validIntTuple( - self.optState.getInt("GuiSession", "sortOrder", Qt.DescendingOrder), + self.optState.getInt("GuiSessionLog", "sortOrder", Qt.DescendingOrder), sortValid, Qt.DescendingOrder ) @@ -123,13 +123,13 @@ class GuiSessionLogView(QDialog): self.hideZeros = QCheckBox("Hide zero word count", self) self.hideZeros.setChecked( - self.optState.getBool("GuiSession", "hideZeros", True) + self.optState.getBool("GuiSessionLog", "hideZeros", True) ) self.hideZeros.stateChanged.connect(self._doHideZeros) self.hideNegative = QCheckBox("Hide negative word count", self) self.hideNegative.setChecked( - self.optState.getBool("GuiSession", "hideNegative", False) + self.optState.getBool("GuiSessionLog", "hideNegative", False) ) self.hideNegative.stateChanged.connect(self._doHideNegative) @@ -149,7 +149,7 @@ class GuiSessionLogView(QDialog): self.setLayout(self.outerBox) - logger.debug("SessionLogView initialisation complete") + logger.debug("GuiSessionLog initialisation complete") qApp.processEvents() self._loadLogFile() @@ -173,12 +173,12 @@ class GuiSessionLogView(QDialog): hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() - self.optState.setValue("GuiSession", "widthCol0", widthCol0) - self.optState.setValue("GuiSession", "widthCol1", widthCol1) - self.optState.setValue("GuiSession", "sortCol", sortCol) - self.optState.setValue("GuiSession", "sortOrder", sortOrder) - self.optState.setValue("GuiSession", "hideZeros", hideZeros) - self.optState.setValue("GuiSession", "hideNegative", hideNegative) + self.optState.setValue("GuiSessionLog", "widthCol0", widthCol0) + self.optState.setValue("GuiSessionLog", "widthCol1", widthCol1) + self.optState.setValue("GuiSessionLog", "sortCol", sortCol) + self.optState.setValue("GuiSessionLog", "sortOrder", sortOrder) + self.optState.setValue("GuiSessionLog", "hideZeros", hideZeros) + self.optState.setValue("GuiSessionLog", "hideNegative", hideNegative) self.optState.saveSettings() self.close() @@ -226,6 +226,7 @@ class GuiSessionLogView(QDialog): nWords = int(inData[7]) tDiff = dEnd - dStart sDiff = tDiff.total_seconds() + self.timeTotal += sDiff self.logData.append((dStart, sDiff, nWords)) @@ -235,6 +236,8 @@ class GuiSessionLogView(QDialog): ) return False + self.labelTotal.setText(self._formatTime(self.timeTotal)) + return True def _updateListBox(self): @@ -242,18 +245,14 @@ class GuiSessionLogView(QDialog): """ self.listBox.clear() self.timeFilter = 0.0 - self.timeTotal = 0.0 hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() for dStart, sDiff, nWords in self.logData: - self.timeTotal += sDiff - if hideZeros and nWords == 0: continue - if hideNegative and nWords < 0: continue @@ -274,7 +273,6 @@ class GuiSessionLogView(QDialog): self.listBox.addTopLevelItem(newItem) self.labelFilter.setText(self._formatTime(self.timeFilter)) - self.labelTotal.setText(self._formatTime(self.timeTotal)) return True @@ -287,4 +285,4 @@ class GuiSessionLogView(QDialog): tS = tS - tM*60 - tH*3600 return "%02d:%02d:%02d" % (tH,tM,tS) -# END Class GuiSessionLogView +# END Class GuiSessionLog diff --git a/nw/guimain.py b/nw/guimain.py index 13c1eeec..091c659d 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -43,7 +43,7 @@ from nw.gui import ( GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus, GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme, - GuiProjectSettings, GuiProjectTree, GuiSessionLogView + GuiProjectSettings, GuiProjectTree, GuiSessionLog ) from nw.core import NWProject, NWDoc, NWIndex from nw.constants import nwFiles, nwItemType, nwAlert @@ -810,7 +810,7 @@ class GuiMain(QMainWindow): """Open the session log dialog. """ if self.hasProject: - dlgTLine = GuiSessionLogView(self, self.theProject) + dlgTLine = GuiSessionLog(self, self.theProject) dlgTLine.exec_() return True From 9f1826cb5956d7573ae26fdb0ba11c40d7aa67b0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:53:31 +0200 Subject: [PATCH 46/67] Replaced checkboxes with switches --- nw/gui/projtree.py | 1 + nw/gui/sessionlog.py | 33 +++++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index b42c19dc..d6eb8cdf 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -54,6 +54,7 @@ class GuiProjectTree(QTreeWidget): QTreeWidget.__init__(self, theParent) logger.debug("Initialising GuiProjectTree ...") + self.mainConf = nw.CONFIG self.theParent = theParent self.theTheme = theParent.theTheme diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index e7c62d5a..a880f45a 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -34,11 +34,12 @@ from datetime import datetime from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( - qApp, QDialog, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, - QGridLayout, QLabel, QGroupBox, QCheckBox + qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout, + QLabel, QGroupBox ) from nw.constants import nwConst, nwFiles, nwAlert +from nw.gui.custom import QSwitch logger = logging.getLogger(__name__) @@ -62,14 +63,19 @@ class GuiSessionLog(QDialog): self.logData = [] self.timeFilter = 0.0 self.timeTotal = 0.0 + self.maxWords = 0 self.setWindowTitle("Session Log") self.setMinimumWidth(self.mainConf.pxInt(420)) self.setMinimumHeight(self.mainConf.pxInt(400)) # List Box - wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "widthCol0", 180)) - wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "widthCol1", 80)) + wCol0 = self.mainConf.pxInt( + self.optState.getInt("GuiSessionLog", "widthCol0", 180) + ) + wCol1 = self.mainConf.pxInt( + self.optState.getInt("GuiSessionLog", "widthCol1", 80) + ) self.listBox = QTreeWidget() self.listBox.setHeaderLabels(["Session Start","Length","Words"]) @@ -117,24 +123,30 @@ class GuiSessionLog(QDialog): self.infoForm.addWidget(self.labelFilter, 1, 1) # Filter Options + sPx = self.theTheme.baseIconSize + self.filterBox = QGroupBox("Filters", self) self.filterForm = QGridLayout(self) self.filterBox.setLayout(self.filterForm) - self.hideZeros = QCheckBox("Hide zero word count", self) + self.labelZeros = QLabel("Hide zero word count") + self.hideZeros = QSwitch(width=2*sPx, height=sPx) self.hideZeros.setChecked( self.optState.getBool("GuiSessionLog", "hideZeros", True) ) - self.hideZeros.stateChanged.connect(self._doHideZeros) + self.hideZeros.toggled.connect(self._doHideZeros) - self.hideNegative = QCheckBox("Hide negative word count", self) + self.labelNegative = QLabel("Hide negative word count") + self.hideNegative = QSwitch(width=2*sPx, height=sPx) self.hideNegative.setChecked( self.optState.getBool("GuiSessionLog", "hideNegative", False) ) - self.hideNegative.stateChanged.connect(self._doHideNegative) + self.hideNegative.toggled.connect(self._doHideNegative) - self.filterForm.addWidget(self.hideZeros, 0, 0) - self.filterForm.addWidget(self.hideNegative, 1, 0) + self.filterForm.addWidget(self.labelZeros, 0, 0) + self.filterForm.addWidget(self.hideZeros, 0, 1) + self.filterForm.addWidget(self.labelNegative, 1, 0) + self.filterForm.addWidget(self.hideNegative, 1, 1) # Buttons self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) @@ -229,6 +241,7 @@ class GuiSessionLog(QDialog): self.timeTotal += sDiff self.logData.append((dStart, sDiff, nWords)) + self.maxWords = max(self.maxWords, nWords) except Exception as e: self.theParent.makeAlert( From 274956948283a7750d0837038a254f86a86fa49f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2020 22:49:08 +0200 Subject: [PATCH 47/67] Added histogram bars for relative word count in session log --- nw/core/options.py | 3 ++ nw/gui/sessionlog.py | 77 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index b3a144d3..5f6f75de 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -46,8 +46,11 @@ class OptionState(): self.theState = {} self.validMap = { "GuiSessionLog": { + "winWidth", + "winHeight", "widthCol0", "widthCol1", + "widthCol2", "sortCol", "sortOrder", "hideZeros", diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index a880f45a..9777605f 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -31,11 +31,11 @@ import nw from os import path from datetime import datetime -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QFont +from PyQt5.QtCore import Qt, QRect +from PyQt5.QtGui import QFont, QPixmap, QIcon, QColor, QPainter from PyQt5.QtWidgets import ( qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout, - QLabel, QGroupBox + QLabel, QGroupBox, QAbstractButton ) from nw.constants import nwConst, nwFiles, nwAlert @@ -48,6 +48,7 @@ class GuiSessionLog(QDialog): C_TIME = 0 C_LENGTH = 1 C_COUNT = 2 + C_BAR = 3 def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) @@ -68,6 +69,10 @@ class GuiSessionLog(QDialog): self.setWindowTitle("Session Log") self.setMinimumWidth(self.mainConf.pxInt(420)) self.setMinimumHeight(self.mainConf.pxInt(400)) + self.resize( + self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "winWidth", 550)), + self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "winHeight", 500)) + ) # List Box wCol0 = self.mainConf.pxInt( @@ -76,12 +81,16 @@ class GuiSessionLog(QDialog): wCol1 = self.mainConf.pxInt( self.optState.getInt("GuiSessionLog", "widthCol1", 80) ) + wCol2 = self.mainConf.pxInt( + self.optState.getInt("GuiSessionLog", "widthCol2", 80) + ) self.listBox = QTreeWidget() - self.listBox.setHeaderLabels(["Session Start","Length","Words"]) + self.listBox.setHeaderLabels(["Session Start","Length","Words","Histogram"]) self.listBox.setIndentation(0) self.listBox.setColumnWidth(self.C_TIME, wCol0) self.listBox.setColumnWidth(self.C_LENGTH, wCol1) + self.listBox.setColumnWidth(self.C_COUNT, wCol2) self.listBox.resizeColumnToContents(self.C_COUNT) hHeader = self.listBox.headerItem() @@ -104,6 +113,11 @@ class GuiSessionLog(QDialog): self.listBox.sortByColumn(sortCol, sortOrder) self.listBox.setSortingEnabled(True) + # Word Bar + self.barHeight = int(round(0.5*self.theTheme.fontPixelSize)) + self.barImage = QPixmap(self.barHeight, self.barHeight) + self.barImage.fill(self.palette().highlight().color()) + # Session Info self.infoBox = QGroupBox("Sum Total Time", self) self.infoForm = QGridLayout(self) @@ -178,18 +192,24 @@ class GuiSessionLog(QDialog): """ self.logData = [] + winWidth = self.mainConf.rpxInt(self.width()) + winHeight = self.mainConf.rpxInt(self.height()) widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0)) widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1)) + widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2)) sortCol = self.listBox.sortColumn() sortOrder = self.listBox.header().sortIndicatorOrder() hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() - self.optState.setValue("GuiSessionLog", "widthCol0", widthCol0) - self.optState.setValue("GuiSessionLog", "widthCol1", widthCol1) - self.optState.setValue("GuiSessionLog", "sortCol", sortCol) - self.optState.setValue("GuiSessionLog", "sortOrder", sortOrder) - self.optState.setValue("GuiSessionLog", "hideZeros", hideZeros) + self.optState.setValue("GuiSessionLog", "winWidth", winWidth) + self.optState.setValue("GuiSessionLog", "winHeight", winHeight) + self.optState.setValue("GuiSessionLog", "widthCol0", widthCol0) + self.optState.setValue("GuiSessionLog", "widthCol1", widthCol1) + self.optState.setValue("GuiSessionLog", "widthCol2", widthCol2) + self.optState.setValue("GuiSessionLog", "sortCol", sortCol) + self.optState.setValue("GuiSessionLog", "sortOrder", sortOrder) + self.optState.setValue("GuiSessionLog", "hideZeros", hideZeros) self.optState.setValue("GuiSessionLog", "hideNegative", hideNegative) self.optState.saveSettings() @@ -276,8 +296,18 @@ class GuiSessionLog(QDialog): newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) newItem.setText(self.C_COUNT, str(nWords)) + if nWords > 0: + theBar = self.barImage.scaled( + int(200*nWords/self.maxWords), + self.barHeight, + Qt.IgnoreAspectRatio, + Qt.FastTransformation + ) + newItem.setData(self.C_BAR, Qt.DecorationRole, theBar) + newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight) newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) + newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter) newItem.setFont(self.C_TIME, self.monoFont) newItem.setFont(self.C_LENGTH, self.monoFont) @@ -299,3 +329,32 @@ class GuiSessionLog(QDialog): return "%02d:%02d:%02d" % (tH,tM,tS) # END Class GuiSessionLog + +# class WordBar(QAbstractButton): + +# def __init__(self, sW, sH, parent=None): +# super().__init__(parent=parent) + +# self._theCol = self.palette().highlight().color() + +# self.setFixedWidth(sW) +# self.setFixedHeight(sH) + +# return + +# ## +# # Events +# ## + +# def paintEvent(self, event): +# """Drawing the bar. +# """ +# qPaint = QPainter(self) +# qPaint.setRenderHint(QPainter.Antialiasing, True) +# qPaint.setPen(self._theCol) +# qPaint.setBrush(self._theCol) +# qPaint.setOpacity(1.0) +# qPaint.drawRect(QRect(0, 0, self.width(), self.height())) +# return + +# # END Class StatusLED From 9e6feb6b05a7a8fd3c8360f637dd4d96142ff621 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2020 23:39:20 +0200 Subject: [PATCH 48/67] Session log entries can be grouped by day --- nw/constants/constants.py | 1 + nw/core/options.py | 1 + nw/gui/sessionlog.py | 117 ++++++++++++++++++++------------------ 3 files changed, 64 insertions(+), 55 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index e4e6af98..6c57d2d6 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -31,6 +31,7 @@ class nwConst(): tStampFmt = "%Y-%m-%d %H:%M:%S" # Default format fStampFmt = "%Y-%m-%d %H.%M.%S" # FileName safe format + dStampFmt = "%Y-%m-%d" # Date only format # END Class nwConst diff --git a/nw/core/options.py b/nw/core/options.py index 5f6f75de..42c2dbd0 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -55,6 +55,7 @@ class OptionState(): "sortOrder", "hideZeros", "hideNegative", + "groupByDay", }, "GuiDocSplit": { "spLevel", diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index 9777605f..cd9be467 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -31,11 +31,11 @@ import nw from os import path from datetime import datetime -from PyQt5.QtCore import Qt, QRect -from PyQt5.QtGui import QFont, QPixmap, QIcon, QColor, QPainter +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont, QPixmap from PyQt5.QtWidgets import ( qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout, - QLabel, QGroupBox, QAbstractButton + QLabel, QGroupBox ) from nw.constants import nwConst, nwFiles, nwAlert @@ -109,7 +109,6 @@ class GuiSessionLog(QDialog): self.optState.getInt("GuiSessionLog", "sortOrder", Qt.DescendingOrder), sortValid, Qt.DescendingOrder ) - self.listBox.sortByColumn(sortCol, sortOrder) self.listBox.setSortingEnabled(True) @@ -119,7 +118,7 @@ class GuiSessionLog(QDialog): self.barImage.fill(self.palette().highlight().color()) # Session Info - self.infoBox = QGroupBox("Sum Total Time", self) + self.infoBox = QGroupBox("Sum Totals", self) self.infoForm = QGridLayout(self) self.infoBox.setLayout(self.infoForm) @@ -131,10 +130,11 @@ class GuiSessionLog(QDialog): self.labelFilter.setFont(self.monoFont) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) - self.infoForm.addWidget(QLabel("All:"), 0, 0) - self.infoForm.addWidget(self.labelTotal, 0, 1) - self.infoForm.addWidget(QLabel("Filtered:"), 1, 0) - self.infoForm.addWidget(self.labelFilter, 1, 1) + self.infoForm.addWidget(QLabel("Total Time:"), 0, 0) + self.infoForm.addWidget(self.labelTotal, 0, 1) + self.infoForm.addWidget(QLabel("Filtered Time:"), 1, 0) + self.infoForm.addWidget(self.labelFilter, 1, 1) + self.infoForm.setRowStretch(2, 1) # Filter Options sPx = self.theTheme.baseIconSize @@ -148,19 +148,28 @@ class GuiSessionLog(QDialog): self.hideZeros.setChecked( self.optState.getBool("GuiSessionLog", "hideZeros", True) ) - self.hideZeros.toggled.connect(self._doHideZeros) + self.hideZeros.clicked.connect(self._updateListBox) self.labelNegative = QLabel("Hide negative word count") self.hideNegative = QSwitch(width=2*sPx, height=sPx) self.hideNegative.setChecked( self.optState.getBool("GuiSessionLog", "hideNegative", False) ) - self.hideNegative.toggled.connect(self._doHideNegative) + self.hideNegative.clicked.connect(self._updateListBox) + + self.labelByDay = QLabel("Group entries by day") + self.groupByDay = QSwitch(width=2*sPx, height=sPx) + self.groupByDay.setChecked( + self.optState.getBool("GuiSessionLog", "groupByDay", False) + ) + self.groupByDay.clicked.connect(self._updateListBox) self.filterForm.addWidget(self.labelZeros, 0, 0) self.filterForm.addWidget(self.hideZeros, 0, 1) self.filterForm.addWidget(self.labelNegative, 1, 0) self.filterForm.addWidget(self.hideNegative, 1, 1) + self.filterForm.addWidget(self.labelByDay, 2, 0) + self.filterForm.addWidget(self.groupByDay, 2, 1) # Buttons self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) @@ -172,6 +181,7 @@ class GuiSessionLog(QDialog): self.outerBox.addWidget(self.infoBox, 1, 0) self.outerBox.addWidget(self.filterBox, 1, 1) self.outerBox.addWidget(self.buttonBox, 2, 0, 1, 2) + self.outerBox.setRowStretch(0, 1) self.setLayout(self.outerBox) @@ -201,6 +211,7 @@ class GuiSessionLog(QDialog): sortOrder = self.listBox.header().sortIndicatorOrder() hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() + groupByDay = self.groupByDay.isChecked() self.optState.setValue("GuiSessionLog", "winWidth", winWidth) self.optState.setValue("GuiSessionLog", "winHeight", winHeight) @@ -211,24 +222,13 @@ class GuiSessionLog(QDialog): self.optState.setValue("GuiSessionLog", "sortOrder", sortOrder) self.optState.setValue("GuiSessionLog", "hideZeros", hideZeros) self.optState.setValue("GuiSessionLog", "hideNegative", hideNegative) + self.optState.setValue("GuiSessionLog", "groupByDay", groupByDay) self.optState.saveSettings() self.close() return - def _doHideZeros(self, newState): - """Reload the list box with the new filter settings in place. - """ - self._updateListBox() - return - - def _doHideNegative(self, newState): - """Reload the list box with the new filter settings in place. - """ - self._updateListBox() - return - ## # Internal Functions ## @@ -281,8 +281,39 @@ class GuiSessionLog(QDialog): hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() + groupByDay = self.groupByDay.isChecked() - for dStart, sDiff, nWords in self.logData: + # Group the data + if groupByDay: + listData = [] + listMax = 0 + + dayDate = None + daySDiff = 0 + dayWords = 0 + + for n, (dStart, sDiff, nWords) in enumerate(self.logData): + if n == 0: + dayDate = dStart.date() + if dayDate != dStart.date(): + listData.append((dayDate, daySDiff, dayWords)) + listMax = max(listMax, dayWords) + dayDate = dStart.date() + daySDiff = sDiff + dayWords = nWords + else: + daySDiff += sDiff + dayWords += nWords + + if dayDate is not None: + listData.append((dayDate, daySDiff, dayWords)) + + else: + listData = self.logData + listMax = self.maxWords + + # Populate the list + for dStart, sDiff, nWords in listData: if hideZeros and nWords == 0: continue @@ -291,14 +322,19 @@ class GuiSessionLog(QDialog): self.timeFilter += sDiff + if groupByDay: + sStart = dStart.strftime(nwConst.dStampFmt) + else: + sStart = dStart.strftime(nwConst.tStampFmt) + newItem = QTreeWidgetItem() - newItem.setText(self.C_TIME, str(dStart)) + newItem.setText(self.C_TIME, sStart) newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) newItem.setText(self.C_COUNT, str(nWords)) if nWords > 0: theBar = self.barImage.scaled( - int(200*nWords/self.maxWords), + int(200*nWords/listMax), self.barHeight, Qt.IgnoreAspectRatio, Qt.FastTransformation @@ -329,32 +365,3 @@ class GuiSessionLog(QDialog): return "%02d:%02d:%02d" % (tH,tM,tS) # END Class GuiSessionLog - -# class WordBar(QAbstractButton): - -# def __init__(self, sW, sH, parent=None): -# super().__init__(parent=parent) - -# self._theCol = self.palette().highlight().color() - -# self.setFixedWidth(sW) -# self.setFixedHeight(sH) - -# return - -# ## -# # Events -# ## - -# def paintEvent(self, event): -# """Drawing the bar. -# """ -# qPaint = QPainter(self) -# qPaint.setRenderHint(QPainter.Antialiasing, True) -# qPaint.setPen(self._theCol) -# qPaint.setBrush(self._theCol) -# qPaint.setOpacity(1.0) -# qPaint.drawRect(QRect(0, 0, self.width(), self.height())) -# return - -# # END Class StatusLED From 09be23a11ef01b4ad1d90bd568d57e6c34ecdd23 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 25 Jun 2020 21:11:37 +0200 Subject: [PATCH 49/67] Brand new session stats log with saved counts for novel and notes seperately, and new parsing code --- nw/constants/constants.py | 2 +- nw/core/options.py | 2 + nw/core/project.py | 38 +++++-- nw/core/tree.py | 19 +++- nw/gui/sessionlog.py | 181 +++++++++++++++++++++++-------- sample/content/636b6aa9b697b.nwd | 2 +- sample/nwProject.nwx | 20 ++-- 7 files changed, 195 insertions(+), 69 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 6c57d2d6..18443c1d 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -51,7 +51,7 @@ class nwFiles(): PROJ_LOCK = "nwProject.lock" TOC_TXT = "ToC.txt" TOC_JSON = "ToC.json" - SESS_INFO = "sessionInfo.log" + SESS_STATS = "sessionStats.log" INDEX_FILE = "tagsIndex.json" OPTS_FILE = "guiOptions.json" RECENT_FILE = "recentProjects.json" diff --git a/nw/core/options.py b/nw/core/options.py index 42c2dbd0..a9a06e0d 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -53,6 +53,8 @@ class OptionState(): "widthCol2", "sortCol", "sortOrder", + "incNovel", + "incNotes", "hideZeros", "hideNegative", "groupByDay", diff --git a/nw/core/project.py b/nw/core/project.py index 3db93bf2..0d43229c 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -93,6 +93,8 @@ class NWProject(): self.lastViewed = None # The handle of the last file to be viewed self.lastWCount = 0 # The project word count from last session self.currWCount = 0 # The project word count in current session + self.novelWCount = 0 # Total number of words in novel files + self.notesWCount = 0 # Total number of words in note files self.doBackup = True # Run project backup on exit # Set Defaults @@ -231,6 +233,8 @@ class NWProject(): self.lastViewed = None self.lastWCount = 0 self.currWCount = 0 + self.novelWCount = 0 + self.notesWCount = 0 return @@ -435,6 +439,10 @@ class NWProject(): self.lastViewed = checkString(xItem.text, None, True) elif xItem.tag == "lastWordCount": self.lastWCount = checkInt(xItem.text, 0, False) + elif xItem.tag == "novelWordCount": + self.novelWCount = checkInt(xItem.text, 0, False) + elif xItem.tag == "notesWordCount": + self.notesWCount = checkInt(xItem.text, 0, False) elif xItem.tag == "status": self.statusItems.unpackEntries(xItem) elif xItem.tag == "importance": @@ -501,6 +509,10 @@ class NWProject(): }) editTime = int(self.editTime + saveTime - self.projOpened) + wcNovel, wcNotes = self.projTree.sumWords() + self.novelWCount = wcNovel + self.notesWCount = wcNotes + self.setProjectWordCount(wcNovel + wcNotes) # Save Project Meta xProject = etree.SubElement(nwXML, "project") @@ -519,6 +531,8 @@ class NWProject(): self._packProjectValue(xSettings, "lastEdited", self.lastEdited) self._packProjectValue(xSettings, "lastViewed", self.lastViewed) self._packProjectValue(xSettings, "lastWordCount", self.currWCount) + self._packProjectValue(xSettings, "novelWordCount", wcNovel) + self._packProjectValue(xSettings, "notesWordCount", wcNotes) xAutoRep = etree.SubElement(xSettings, "autoReplace") for aKey, aValue in self.autoReplace.items(): @@ -1101,18 +1115,22 @@ class NWProject(): if not self.ensureFolderStructure(): return False - sessionFile = path.join(self.projMeta, nwFiles.SESS_INFO) + sessionFile = path.join(self.projMeta, nwFiles.SESS_STATS) + isFile = path.isfile(sessionFile) with open(sessionFile, mode="a+", encoding="utf8") as outFile: - print(( - "Start: {opened:s} " - "End: {closed:s} " - "Words: {words:8d}" - ).format( - opened = formatTimeStamp(self.projOpened), - closed = formatTimeStamp(time()), - words = self.getSessionWordCount(), - ), file=outFile) + if not isFile: + # It's a new file, so add a header + outFile.write("# Initial: %d\n# %-17s %-19s %8s %8s\n" % ( + self.lastWCount, "Start Time", "End Time", "Novel", "Notes" + )) + + outFile.write("%-19s %-19s %8d %8d\n" % ( + formatTimeStamp(self.projOpened), + formatTimeStamp(time()), + self.novelWCount, + self.notesWCount, + )) return True diff --git a/nw/core/tree.py b/nw/core/tree.py index 7f1482fe..beb5d2e3 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -36,7 +36,7 @@ from time import time from nw.core.item import NWItem from nw.common import checkString -from nw.constants import nwFiles, nwItemType, nwItemClass +from nw.constants import nwFiles, nwItemType, nwItemClass, nwItemLayout logger = logging.getLogger(__name__) @@ -182,6 +182,23 @@ class NWTree(): return + def sumWords(self): + """Loops over all entries and adds up the word counts. + """ + noteWords = 0 + novelWords = 0 + for tHandle in self._treeOrder: + tItem = self.__getitem__(tHandle) + if tItem is None: + continue + if tItem.itemLayout == nwItemLayout.NO_LAYOUT: + pass + elif tItem.itemLayout == nwItemLayout.NOTE: + noteWords += tItem.wordCount + else: + novelWords += tItem.wordCount + return novelWords, noteWords + ## # Tree Structure Methods ## diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index cd9be467..03ace4db 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -64,7 +64,7 @@ class GuiSessionLog(QDialog): self.logData = [] self.timeFilter = 0.0 self.timeTotal = 0.0 - self.maxWords = 0 + self.wordOffset = 0 self.setWindowTitle("Session Log") self.setMinimumWidth(self.mainConf.pxInt(420)) @@ -114,6 +114,7 @@ class GuiSessionLog(QDialog): # Word Bar self.barHeight = int(round(0.5*self.theTheme.fontPixelSize)) + self.barWidth = self.mainConf.pxInt(200) self.barImage = QPixmap(self.barHeight, self.barHeight) self.barImage.fill(self.palette().highlight().color()) @@ -130,11 +131,29 @@ class GuiSessionLog(QDialog): self.labelFilter.setFont(self.monoFont) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) - self.infoForm.addWidget(QLabel("Total Time:"), 0, 0) - self.infoForm.addWidget(self.labelTotal, 0, 1) - self.infoForm.addWidget(QLabel("Filtered Time:"), 1, 0) - self.infoForm.addWidget(self.labelFilter, 1, 1) - self.infoForm.setRowStretch(2, 1) + self.novelWords = QLabel("0") + self.novelWords.setFont(self.monoFont) + self.novelWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) + + self.notesWords = QLabel("0") + self.notesWords.setFont(self.monoFont) + self.notesWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) + + self.totalWords = QLabel("0") + self.totalWords.setFont(self.monoFont) + self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) + + self.infoForm.addWidget(QLabel("Total Time:"), 0, 0) + self.infoForm.addWidget(self.labelTotal, 0, 1) + self.infoForm.addWidget(QLabel("Filtered Time:"), 1, 0) + self.infoForm.addWidget(self.labelFilter, 1, 1) + self.infoForm.addWidget(QLabel("Novel Word Count:"), 2, 0) + self.infoForm.addWidget(self.novelWords, 2, 1) + self.infoForm.addWidget(QLabel("Notes Word Count:"), 3, 0) + self.infoForm.addWidget(self.notesWords, 3, 1) + self.infoForm.addWidget(QLabel("Total Word Count:"), 4, 0) + self.infoForm.addWidget(self.totalWords, 4, 1) + self.infoForm.setRowStretch(5, 1) # Filter Options sPx = self.theTheme.baseIconSize @@ -143,6 +162,20 @@ class GuiSessionLog(QDialog): self.filterForm = QGridLayout(self) self.filterBox.setLayout(self.filterForm) + self.labelNovel = QLabel("Count novel files") + self.incNovel = QSwitch(width=2*sPx, height=sPx) + self.incNovel.setChecked( + self.optState.getBool("GuiSessionLog", "incNovel", True) + ) + self.incNovel.clicked.connect(self._updateListBox) + + self.labelNotes = QLabel("Count note files") + self.incNotes = QSwitch(width=2*sPx, height=sPx) + self.incNotes.setChecked( + self.optState.getBool("GuiSessionLog", "incNotes", True) + ) + self.incNotes.clicked.connect(self._updateListBox) + self.labelZeros = QLabel("Hide zero word count") self.hideZeros = QSwitch(width=2*sPx, height=sPx) self.hideZeros.setChecked( @@ -164,12 +197,16 @@ class GuiSessionLog(QDialog): ) self.groupByDay.clicked.connect(self._updateListBox) - self.filterForm.addWidget(self.labelZeros, 0, 0) - self.filterForm.addWidget(self.hideZeros, 0, 1) - self.filterForm.addWidget(self.labelNegative, 1, 0) - self.filterForm.addWidget(self.hideNegative, 1, 1) - self.filterForm.addWidget(self.labelByDay, 2, 0) - self.filterForm.addWidget(self.groupByDay, 2, 1) + self.filterForm.addWidget(self.labelNovel, 0, 0) + self.filterForm.addWidget(self.incNovel, 0, 1) + self.filterForm.addWidget(self.labelNotes, 1, 0) + self.filterForm.addWidget(self.incNotes, 1, 1) + self.filterForm.addWidget(self.labelZeros, 2, 0) + self.filterForm.addWidget(self.hideZeros, 2, 1) + self.filterForm.addWidget(self.labelNegative, 3, 0) + self.filterForm.addWidget(self.hideNegative, 3, 1) + self.filterForm.addWidget(self.labelByDay, 4, 0) + self.filterForm.addWidget(self.groupByDay, 4, 1) # Buttons self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) @@ -209,6 +246,8 @@ class GuiSessionLog(QDialog): widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2)) sortCol = self.listBox.sortColumn() sortOrder = self.listBox.header().sortIndicatorOrder() + incNovel = self.incNovel.isChecked() + incNotes = self.incNotes.isChecked() hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() groupByDay = self.groupByDay.isChecked() @@ -220,6 +259,8 @@ class GuiSessionLog(QDialog): self.optState.setValue("GuiSessionLog", "widthCol2", widthCol2) self.optState.setValue("GuiSessionLog", "sortCol", sortCol) self.optState.setValue("GuiSessionLog", "sortOrder", sortOrder) + self.optState.setValue("GuiSessionLog", "incNovel", incNovel) + self.optState.setValue("GuiSessionLog", "incNotes", incNotes) self.optState.setValue("GuiSessionLog", "hideZeros", hideZeros) self.optState.setValue("GuiSessionLog", "hideNegative", hideNegative) self.optState.setValue("GuiSessionLog", "groupByDay", groupByDay) @@ -239,29 +280,55 @@ class GuiSessionLog(QDialog): self.logData = [] logger.debug("Loading session log file") + ttNovel = 0 + ttNotes = 0 + + isFirst = True + osNovel = 0 + osNotes = 0 + try: - logFile = path.join(self.theProject.projMeta, nwFiles.SESS_INFO) + logFile = path.join(self.theProject.projMeta, nwFiles.SESS_STATS) with open(logFile, mode="r", encoding="utf8") as inFile: for inLine in inFile: + if inLine.startswith("#"): + if inLine.startswith("# Initial:"): + self.wordOffset = int(inLine[11:].strip()) + logger.verbose( + "Initial word count when log was started is %d" % self.wordOffset + ) + continue inData = inLine.split() - if len(inData) != 8: + if len(inData) != 6: continue dStart = datetime.strptime( - "%s %s" % (inData[1], inData[2]), nwConst.tStampFmt + "%s %s" % (inData[0], inData[1]), nwConst.tStampFmt ) dEnd = datetime.strptime( - "%s %s" % (inData[4], inData[5]), nwConst.tStampFmt + "%s %s" % (inData[2], inData[3]), nwConst.tStampFmt ) - nWords = int(inData[7]) tDiff = dEnd - dStart sDiff = tDiff.total_seconds() self.timeTotal += sDiff - self.logData.append((dStart, sDiff, nWords)) - self.maxWords = max(self.maxWords, nWords) + wcNovel = int(inData[4]) + wcNotes = int(inData[5]) + ttNovel = wcNovel + ttNotes = wcNotes + + if isFirst: + isFirst = False + if self.wordOffset > 0: + # First entry is used as the reference word + # count, and the data is then discarded. + osNovel = wcNovel + osNotes = wcNotes + continue + + self.logData.append((dStart, sDiff, wcNovel-osNovel, wcNotes-osNotes)) except Exception as e: self.theParent.makeAlert( @@ -270,6 +337,9 @@ class GuiSessionLog(QDialog): return False self.labelTotal.setText(self._formatTime(self.timeTotal)) + self.novelWords.setText("{:n}".format(ttNovel)) + self.notesWords.setText("{:n}".format(ttNotes)) + self.totalWords.setText("{:n}".format(ttNovel + ttNotes)) return True @@ -279,49 +349,65 @@ class GuiSessionLog(QDialog): self.listBox.clear() self.timeFilter = 0.0 + incNovel = self.incNovel.isChecked() + incNotes = self.incNotes.isChecked() hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() groupByDay = self.groupByDay.isChecked() # Group the data if groupByDay: - listData = [] - listMax = 0 + tempData = [] + sessDate = None + sessTime = 0 + lstNovel = 0 + lstNotes = 0 - dayDate = None - daySDiff = 0 - dayWords = 0 - - for n, (dStart, sDiff, nWords) in enumerate(self.logData): + for n, (dStart, sDiff, wcNovel, wcNotes) in enumerate(self.logData): if n == 0: - dayDate = dStart.date() - if dayDate != dStart.date(): - listData.append((dayDate, daySDiff, dayWords)) - listMax = max(listMax, dayWords) - dayDate = dStart.date() - daySDiff = sDiff - dayWords = nWords + sessDate = dStart.date() + if sessDate != dStart.date(): + tempData.append((sessDate, sessTime, lstNovel, lstNotes)) + sessDate = dStart.date() + sessTime = sDiff + lstNovel = wcNovel + lstNotes = wcNotes else: - daySDiff += sDiff - dayWords += nWords + sessTime += sDiff + lstNovel = wcNovel + lstNotes = wcNotes - if dayDate is not None: - listData.append((dayDate, daySDiff, dayWords)) + if sessDate is not None: + tempData.append((sessDate, sessTime, lstNovel, lstNotes)) else: - listData = self.logData - listMax = self.maxWords + tempData = self.logData + + # Calculate Word Diff + listData = [] + pcTotal = 0 + listMax = 0 + for dStart, sDiff, wcNovel, wcNotes in tempData: + + wcTotal = 0 + if incNovel: + wcTotal += wcNovel + if incNotes: + wcTotal += wcNotes + + dwTotal = wcTotal - pcTotal + if hideZeros and dwTotal == 0: + continue + if hideNegative and dwTotal < 0: + continue + + listData.append((dStart, sDiff, dwTotal)) + listMax = max(listMax, dwTotal) + pcTotal = wcTotal # Populate the list for dStart, sDiff, nWords in listData: - if hideZeros and nWords == 0: - continue - if hideNegative and nWords < 0: - continue - - self.timeFilter += sDiff - if groupByDay: sStart = dStart.strftime(nwConst.dStampFmt) else: @@ -332,7 +418,7 @@ class GuiSessionLog(QDialog): newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) newItem.setText(self.C_COUNT, str(nWords)) - if nWords > 0: + if nWords > 0 and listMax > 0: theBar = self.barImage.scaled( int(200*nWords/listMax), self.barHeight, @@ -350,6 +436,7 @@ class GuiSessionLog(QDialog): newItem.setFont(self.C_COUNT, self.monoFont) self.listBox.addTopLevelItem(newItem) + self.timeFilter += sDiff self.labelFilter.setText(self._formatTime(self.timeFilter)) diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 394a96ce..0ab145f1 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -21,4 +21,4 @@ Thin spaces and thin non-breaking spaces are also supported from the Insert menu If you need to split a scene file up into further pieces, you can do so with the level four heading, like above. This is referred to as a section. -Both scene and section titles can be left out of the final exported document. The formatting of titles can be selected from the export dialog. +Both scene and section titles can be left out of the final exported document. The formatting of titles can be selected from the Build Novel Project dialog. You can also have them replaced with scene separators like “* * *”. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 4ce89213..c29a7788 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,21 +1,23 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 407 - 71 - 15118 + 565 + 99 + 23298 False True True 636b6aa9b697b - bb2c23b3c42cc - 967 + b3e74dbc1f584 + 982 + 606 + 376 B E @@ -114,10 +116,10 @@ 1st Draft True SCENE - 1483 - 263 + 1564 + 278 8 - 1086 + 1633 Another Scene From 98b822fa47da2f38e6c4288d75e25eea75056b3b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 11:19:40 +0200 Subject: [PATCH 50/67] Fix tests --- tests/reference/gui/0_nwProject.nwx | 2 ++ tests/reference/gui/1_nwProject.nwx | 2 ++ tests/reference/gui/2_nwProject.nwx | 2 ++ tests/reference/gui/3_nwProject.nwx | 2 ++ tests/reference/proj/1_nwProject.nwx | 2 ++ tests/reference/proj/2_nwProject.nwx | 2 ++ tests/reference/proj/3_nwProject.nwx | 2 ++ 7 files changed, 14 insertions(+) diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx index 63535bfa..d1a794e7 100644 --- a/tests/reference/gui/0_nwProject.nwx +++ b/tests/reference/gui/0_nwProject.nwx @@ -14,6 +14,8 @@ None None 0 + 0 + 0 %title% diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index 17180919..e8a705b1 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -14,6 +14,8 @@ 31489056e0916 None 86 + 59 + 27 %title% diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx index 316e30c1..2f4ff89c 100644 --- a/tests/reference/gui/2_nwProject.nwx +++ b/tests/reference/gui/2_nwProject.nwx @@ -16,6 +16,8 @@ None None 0 + 0 + 0 With This Stuff diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index 4d6ef96a..8eeb4dca 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -14,6 +14,8 @@ 31489056e0916 None 59 + 59 + 0 %title% diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx index 8516baae..12697dbe 100644 --- a/tests/reference/proj/1_nwProject.nwx +++ b/tests/reference/proj/1_nwProject.nwx @@ -14,6 +14,8 @@ None None 0 + 0 + 0 %title% diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx index da82f956..ed6bbbe4 100644 --- a/tests/reference/proj/2_nwProject.nwx +++ b/tests/reference/proj/2_nwProject.nwx @@ -14,6 +14,8 @@ None None 0 + 0 + 0 %title% diff --git a/tests/reference/proj/3_nwProject.nwx b/tests/reference/proj/3_nwProject.nwx index 8adfadb8..58bef95d 100644 --- a/tests/reference/proj/3_nwProject.nwx +++ b/tests/reference/proj/3_nwProject.nwx @@ -14,6 +14,8 @@ None None 0 + 0 + 0 %title% From 3258621456ed287c95a522d672519ea032618a2e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 11:21:01 +0200 Subject: [PATCH 51/67] No dash before rc1 in version number --- docs/source/conf.py | 2 +- nw/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index d8307be6..4e9b6401 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,7 +26,7 @@ author = "Veronica Berglyd Olsen" # The short X.Y version version = "0.10.0" # The full version, including alpha/beta/rc tags -release = "0.10.0-rc1" +release = "0.10.0rc1" # -- General configuration --------------------------------------------------- diff --git a/nw/__init__.py b/nw/__init__.py index 1ea3237e..0e41c176 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -40,7 +40,7 @@ __package__ = "novelWriter" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen" __license__ = "GPLv3" -__version__ = "0.10.0-rc1" +__version__ = "0.10.0rc1" __hexversion__ = "0x001000c1" __date__ = "2020-06-21" __maintainer__ = "Veronica Berglyd Olsen" From bdedc7a11c4b2e07158cf4e36a8a6894ccabaacc Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 18:23:44 +0200 Subject: [PATCH 52/67] The session data can now be exported to JSON and CSV files with the filters applied --- nw/gui/build.py | 3 - nw/gui/sessionlog.py | 156 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 132 insertions(+), 27 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index ab5b3611..dab7360e 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -568,9 +568,6 @@ class GuiBuildNovel(QDialog): def _saveDocument(self, theFormat): """Save the document to various formats. """ - - # FMT_PDF - byteFmt = QByteArray() fileExt = "" textFmt = "" diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index 03ace4db..709a0961 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -26,6 +26,7 @@ """ import logging +import json import nw from os import path @@ -35,7 +36,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont, QPixmap from PyQt5.QtWidgets import ( qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout, - QLabel, QGroupBox + QLabel, QGroupBox, QMenu, QAction, QFileDialog ) from nw.constants import nwConst, nwFiles, nwAlert @@ -50,6 +51,9 @@ class GuiSessionLog(QDialog): C_COUNT = 2 C_BAR = 3 + FMT_JSON = 0 + FMT_CSV = 1 + def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) @@ -62,6 +66,7 @@ class GuiSessionLog(QDialog): self.optState = theProject.optState self.logData = [] + self.filterData = [] self.timeFilter = 0.0 self.timeTotal = 0.0 self.wordOffset = 0 @@ -212,6 +217,18 @@ class GuiSessionLog(QDialog): self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) self.buttonBox.rejected.connect(self._doClose) + self.btnSave = self.buttonBox.addButton("Save As", QDialogButtonBox.ActionRole) + self.saveMenu = QMenu(self) + self.btnSave.setMenu(self.saveMenu) + + self.saveJSON = QAction("JSON Data File (.json)", self) + self.saveJSON.triggered.connect(lambda: self._saveData(self.FMT_JSON)) + self.saveMenu.addAction(self.saveJSON) + + self.saveCSV = QAction("CSV Data File (.csv)", self) + self.saveCSV.triggered.connect(lambda: self._saveData(self.FMT_CSV)) + self.saveMenu.addAction(self.saveCSV) + # Assemble self.outerBox = QGridLayout() self.outerBox.addWidget(self.listBox, 0, 0, 1, 2) @@ -270,6 +287,101 @@ class GuiSessionLog(QDialog): return + def _saveData(self, dataFmt): + """Save the content of the list box to a file. + """ + fileExt = "" + textFmt = "" + + if dataFmt == self.FMT_JSON: + fileExt = "json" + textFmt = "JSON Data File" + + elif dataFmt == self.FMT_CSV: + fileExt = "csv" + textFmt = "CSV Data File" + + else: + return False + + # Generate the file name + if fileExt: + fileName = "sessionStats.%s" % fileExt + saveDir = self.mainConf.lastPath + savePath = path.join(saveDir, fileName) + if not path.isdir(saveDir): + saveDir = self.mainConf.homePath + + if self.mainConf.showGUI: + dlgOpt = QFileDialog.Options() + dlgOpt |= QFileDialog.DontUseNativeDialog + saveTo = QFileDialog.getSaveFileName( + self, "Save Document As", savePath, options=dlgOpt + ) + if saveTo[0]: + savePath = saveTo[0] + else: + return False + + self.mainConf.setLastPath(savePath) + + else: + return False + + # Do the actual writing + wSuccess = False + errMsg = "" + + try: + with open(savePath, mode="w", encoding="utf8") as outFile: + if dataFmt == self.FMT_JSON: + jsonData = [] + for _, sD, tT, wD, wA, wB in self.filterData: + jsonData.append({ + "date": sD, + "length": tT, + "newWords": wD, + "novelWords": wA, + "noteWords": wB, + }) + outFile.write(json.dumps(jsonData, indent=2)) + wSuccess = True + + elif dataFmt == self.FMT_CSV: + outFile.write( + "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n" % ( + "Date", "Length (sec)", "Words Changed", "Novel Words", "Note Words" + ) + ) + for _, sD, tT, wD, wA, wB in self.filterData: + outFile.write( + "\"%s\",%d,%d,%d,%d\n" % (sD, tT, wD, wA, wB) + ) + wSuccess = True + + else: + errMsg = "Unknown format" + + except Exception as e: + errMsg = str(e) + + # Report to user + if self.mainConf.showGUI: + if wSuccess: + self.theParent.makeAlert( + "%s file successfully written to:
%s" % ( + textFmt, savePath + ), nwAlert.INFO + ) + else: + self.theParent.makeAlert( + "Failed to write %s file. %s" % ( + textFmt, errMsg + ), nwAlert.ERROR + ) + + return True + ## # Internal Functions ## @@ -277,16 +389,13 @@ class GuiSessionLog(QDialog): def _loadLogFile(self): """Load the content of the log file into a buffer. """ - self.logData = [] logger.debug("Loading session log file") + self.logData = [] + ttNovel = 0 ttNotes = 0 - isFirst = True - osNovel = 0 - osNotes = 0 - try: logFile = path.join(self.theProject.projMeta, nwFiles.SESS_STATS) with open(logFile, mode="r", encoding="utf8") as inFile: @@ -319,16 +428,7 @@ class GuiSessionLog(QDialog): ttNovel = wcNovel ttNotes = wcNotes - if isFirst: - isFirst = False - if self.wordOffset > 0: - # First entry is used as the reference word - # count, and the data is then discarded. - osNovel = wcNovel - osNotes = wcNotes - continue - - self.logData.append((dStart, sDiff, wcNovel-osNovel, wcNotes-osNotes)) + self.logData.append((dStart, sDiff, wcNovel, wcNotes)) except Exception as e: self.theParent.makeAlert( @@ -384,9 +484,10 @@ class GuiSessionLog(QDialog): tempData = self.logData # Calculate Word Diff - listData = [] + self.filterData = [] pcTotal = 0 listMax = 0 + isFirst = True for dStart, sDiff, wcNovel, wcNotes in tempData: wcTotal = 0 @@ -401,22 +502,29 @@ class GuiSessionLog(QDialog): if hideNegative and dwTotal < 0: continue - listData.append((dStart, sDiff, dwTotal)) - listMax = max(listMax, dwTotal) - pcTotal = wcTotal - - # Populate the list - for dStart, sDiff, nWords in listData: + if isFirst: + # Subtract the offset from the first list entry + dwTotal -= self.wordOffset + isFirst = False if groupByDay: sStart = dStart.strftime(nwConst.dStampFmt) else: sStart = dStart.strftime(nwConst.tStampFmt) + self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes)) + listMax = max(listMax, dwTotal) + pcTotal = wcTotal + + # Populate the list + for _, sStart, sDiff, nWords, _, _ in self.filterData: + newItem = QTreeWidgetItem() newItem.setText(self.C_TIME, sStart) newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) - newItem.setText(self.C_COUNT, str(nWords)) + newItem.setData(self.C_LENGTH, Qt.UserRole, sDiff) + newItem.setText(self.C_COUNT, "{:n}".format(nWords)) + newItem.setData(self.C_COUNT, Qt.UserRole, nWords) if nWords > 0 and listMax > 0: theBar = self.barImage.scaled( From b05d6f2dc12e46cf7c23f134a25a88435143b7fd Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 18:28:36 +0200 Subject: [PATCH 53/67] Remove some unused data and variables from the code --- nw/gui/sessionlog.py | 8 +++----- sample/nwProject.nwx | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index 709a0961..9c66e923 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -68,7 +68,6 @@ class GuiSessionLog(QDialog): self.logData = [] self.filterData = [] self.timeFilter = 0.0 - self.timeTotal = 0.0 self.wordOffset = 0 self.setWindowTitle("Session Log") @@ -395,6 +394,7 @@ class GuiSessionLog(QDialog): ttNovel = 0 ttNotes = 0 + ttTime = 0 try: logFile = path.join(self.theProject.projMeta, nwFiles.SESS_STATS) @@ -421,7 +421,7 @@ class GuiSessionLog(QDialog): tDiff = dEnd - dStart sDiff = tDiff.total_seconds() - self.timeTotal += sDiff + ttTime += sDiff wcNovel = int(inData[4]) wcNotes = int(inData[5]) @@ -436,7 +436,7 @@ class GuiSessionLog(QDialog): ) return False - self.labelTotal.setText(self._formatTime(self.timeTotal)) + 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)) @@ -522,9 +522,7 @@ class GuiSessionLog(QDialog): newItem = QTreeWidgetItem() newItem.setText(self.C_TIME, sStart) newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) - newItem.setData(self.C_LENGTH, Qt.UserRole, sDiff) newItem.setText(self.C_COUNT, "{:n}".format(nWords)) - newItem.setData(self.C_COUNT, Qt.UserRole, nWords) if nWords > 0 and listMax > 0: theBar = self.barImage.scaled( diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index c29a7788..8f8ee49c 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 565 - 99 - 23298 + 583 + 102 + 24189 False From 198836c261bf7caf91cce19e03ea990510378d77 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 18:37:17 +0200 Subject: [PATCH 54/67] No need to set the session log offset value if it's 0 --- nw/core/project.py | 6 ++++-- nw/gui/sessionlog.py | 4 ++-- sample/nwProject.nwx | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index 0d43229c..c981e4c8 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1121,8 +1121,10 @@ class NWProject(): with open(sessionFile, mode="a+", encoding="utf8") as outFile: if not isFile: # It's a new file, so add a header - outFile.write("# Initial: %d\n# %-17s %-19s %8s %8s\n" % ( - self.lastWCount, "Start Time", "End Time", "Novel", "Notes" + if self.lastWCount > 0: + outFile.write("# Offset %d\n" % self.lastWCount) + outFile.write("# %-17s %-19s %8s %8s\n" % ( + "Start Time", "End Time", "Novel", "Notes" )) outFile.write("%-19s %-19s %8d %8d\n" % ( diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index 9c66e923..cab4a92c 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -401,8 +401,8 @@ class GuiSessionLog(QDialog): with open(logFile, mode="r", encoding="utf8") as inFile: for inLine in inFile: if inLine.startswith("#"): - if inLine.startswith("# Initial:"): - self.wordOffset = int(inLine[11:].strip()) + if inLine.startswith("# Offset"): + self.wordOffset = int(inLine[9:].strip()) logger.verbose( "Initial word count when log was started is %d" % self.wordOffset ) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 8f8ee49c..ecdc1d0c 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 583 + 587 102 - 24189 + 24235 False From f78fbcbc8f644eda8beb7cd9b62d5441307c423e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 19:07:00 +0200 Subject: [PATCH 55/67] Added test for the session log tool --- tests/test_gui.py | 113 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 2 deletions(-) diff --git a/tests/test_gui.py b/tests/test_gui.py index 3ad703a5..d52c4250 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -2,7 +2,7 @@ """novelWriter Main GUI Class Tester """ -import nw, pytest, sys +import nw, pytest, sys, json from nwtools import * from os import path, unlink @@ -10,7 +10,7 @@ from PyQt5.QtCore import Qt from nw.gui import ( GuiProjectSettings, GuiItemEditor, GuiAbout, GuiBuildNovel, - GuiDocMerge, GuiDocSplit + GuiDocMerge, GuiDocSplit, GuiSessionLog ) from nw.constants import * @@ -409,6 +409,115 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): nwGUI.closeMain() # qtbot.stopForInteraction() +@pytest.mark.gui +def testSessionLogExport(qtbot, nwTempGUI, nwRef, nwTemp): + nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + assert nwGUI.openProject(nwTempGUI) + qtbot.wait(stepDelay) + + nwGUI.mainConf.lastPath = nwTempGUI + sessLog = GuiSessionLog(nwGUI, nwGUI.theProject) + sessLog.show() + qtbot.wait(stepDelay) + + assert sessLog._saveData(sessLog.FMT_CSV) + qtbot.wait(stepDelay) + assert sessLog._saveData(sessLog.FMT_JSON) + qtbot.wait(stepDelay) + + jsonStats = path.join(nwTempGUI, "sessionStats.json") + with open(jsonStats, mode="r", encoding="utf-8") as inFile: + jsonData = json.loads(inFile.read()) + + assert len(jsonData) == 3 + assert jsonData[0]["length"] > 0 + assert jsonData[0]["newWords"] == 86 + assert jsonData[0]["novelWords"] == 59 + assert jsonData[0]["noteWords"] == 27 + + # No Novel Files + qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton) + qtbot.wait(stepDelay) + assert sessLog._saveData(sessLog.FMT_JSON) + qtbot.wait(stepDelay) + + jsonStats = path.join(nwTempGUI, "sessionStats.json") + with open(jsonStats, mode="r", encoding="utf-8") as inFile: + jsonData = json.loads(inFile.read()) + + assert len(jsonData) == 2 + assert jsonData[0]["length"] > 0 + assert jsonData[0]["newWords"] == 27 + assert jsonData[0]["novelWords"] == 59 + assert jsonData[0]["noteWords"] == 27 + + # No Note Files + qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton) + qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton) + qtbot.wait(stepDelay) + assert sessLog._saveData(sessLog.FMT_JSON) + qtbot.wait(stepDelay) + + jsonStats = path.join(nwTempGUI, "sessionStats.json") + with open(jsonStats, mode="r", encoding="utf-8") as inFile: + jsonData = json.loads(inFile.read()) + + assert len(jsonData) == 3 + assert jsonData[0]["length"] > 0 + assert jsonData[0]["newWords"] == 59 + assert jsonData[0]["novelWords"] == 59 + assert jsonData[0]["noteWords"] == 27 + + # No Negative Entries + qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton) + qtbot.mouseClick(sessLog.hideNegative, Qt.LeftButton) + qtbot.wait(stepDelay) + assert sessLog._saveData(sessLog.FMT_JSON) + qtbot.wait(stepDelay) + + jsonStats = path.join(nwTempGUI, "sessionStats.json") + with open(jsonStats, mode="r", encoding="utf-8") as inFile: + jsonData = json.loads(inFile.read()) + + assert len(jsonData) == 1 + + # Un-hide Zero Entries + qtbot.mouseClick(sessLog.hideNegative, Qt.LeftButton) + qtbot.mouseClick(sessLog.hideZeros, Qt.LeftButton) + qtbot.wait(stepDelay) + assert sessLog._saveData(sessLog.FMT_JSON) + qtbot.wait(stepDelay) + + jsonStats = path.join(nwTempGUI, "sessionStats.json") + with open(jsonStats, mode="r", encoding="utf-8") as inFile: + jsonData = json.loads(inFile.read()) + + assert len(jsonData) == 4 + + # Group by Day + qtbot.mouseClick(sessLog.groupByDay, Qt.LeftButton) + qtbot.wait(stepDelay) + assert sessLog._saveData(sessLog.FMT_JSON) + qtbot.wait(stepDelay) + + jsonStats = path.join(nwTempGUI, "sessionStats.json") + with open(jsonStats, mode="r", encoding="utf-8") as inFile: + jsonData = json.loads(inFile.read()) + + # 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) + + # qtbot.stopForInteraction() + + sessLog._doClose() + nwGUI.closeMain() + @pytest.mark.gui def testAboutBox(qtbot, nwTempGUI, nwRef, nwTemp): nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp]) From e2f5ddf5f516f99f7a7c2e58fe396e32d15e88bb Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 19:30:51 +0200 Subject: [PATCH 56/67] Change the storage format of auto-replace in the XML --- nw/core/project.py | 30 ++++++++++++++++++++++-------- sample/nwProject.nwx | 12 ++++++------ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index c981e4c8..6b17b51a 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -449,7 +449,15 @@ class NWProject(): self.importItems.unpackEntries(xItem) elif xItem.tag == "autoReplace": for xEntry in xItem: - self.autoReplace[xEntry.tag] = checkString(xEntry.text, None, False) + if xEntry.tag == "entry": + if "key" in xEntry.attrib: + self.autoReplace[xEntry.attrib["key"]] = checkString( + xEntry.text, None, False + ) + else: # Old format + self.autoReplace[xEntry.tag] = checkString( + xEntry.text, None, False + ) elif xItem.tag == "titleFormat": titleFormat = self.titleFormat.copy() for xEntry in xItem: @@ -501,7 +509,7 @@ class NWProject(): # Root element and project details logger.debug("Writing project meta") - nwXML = etree.Element("novelWriterXML",attrib={ + nwXML = etree.Element("novelWriterXML", attrib={ "appVersion" : str(nw.__version__), "hexVersion" : str(nw.__hexversion__), "fileVersion" : "1.1", @@ -533,11 +541,7 @@ class NWProject(): self._packProjectValue(xSettings, "lastWordCount", self.currWCount) self._packProjectValue(xSettings, "novelWordCount", wcNovel) self._packProjectValue(xSettings, "notesWordCount", wcNotes) - - xAutoRep = etree.SubElement(xSettings, "autoReplace") - for aKey, aValue in self.autoReplace.items(): - if len(aKey) > 0: - self._packProjectValue(xAutoRep, aKey, aValue) + self._packProjectKeyValue(xSettings, "autoReplace", self.autoReplace) xTitleFmt = etree.SubElement(xSettings, "titleFormat") for aKey, aValue in self.titleFormat.items(): @@ -1041,10 +1045,20 @@ class NWProject(): if not isinstance(aValue, str): aValue = str(aValue) if aValue == "" and not allowNone: continue - xItem = etree.SubElement(xParent,theName) + xItem = etree.SubElement(xParent, theName) xItem.text = aValue return + def _packProjectKeyValue(self, xParent, theName, theDict): + """Pack the entries in the auto-replace dictionary. + """ + xAutoRep = etree.SubElement(xParent, theName) + for aKey, aValue in theDict.items(): + if len(aKey) > 0: + xEntry = etree.SubElement(xAutoRep, "entry", attrib={"key": aKey}) + xEntry.text = aValue + return + def _scanProjectFolder(self): """Scan the project folder and check that the files in it are also in the project XML file. If they aren't, import them as diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index ecdc1d0c..015816ff 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 587 + 589 102 - 24235 + 24247 False @@ -19,9 +19,9 @@ 606 376 - B - E - D + B + E + D %title% From a0d07409d457ffc5c59d595a93566df3529c6d3c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 19:33:42 +0200 Subject: [PATCH 57/67] Fixed test --- tests/reference/gui/2_nwProject.nwx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx index 2f4ff89c..0815f9ad 100644 --- a/tests/reference/gui/2_nwProject.nwx +++ b/tests/reference/gui/2_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -19,7 +19,7 @@ 0 0 - With This Stuff + With This Stuff %title% From 9609095790c0b9e873db9af02d2f10c8d5ec21a0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 21:21:08 +0200 Subject: [PATCH 58/67] Bumped version number to 0.9.2 --- docs/source/conf.py | 4 ++-- nw/__init__.py | 6 +++--- sample/nwProject.nwx | 6 +++--- setup.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b6c5da7a..e2e63927 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -24,9 +24,9 @@ copyright = "2018-2020, Veronica Berglyd Olsen" author = "Veronica Berglyd Olsen" # The short X.Y version -version = "0.9.1" +version = "0.9.2" # The full version, including alpha/beta/rc tags -release = "0.9.1" +release = "0.9.2" # -- General configuration --------------------------------------------------- diff --git a/nw/__init__.py b/nw/__init__.py index 22835b7f..9a838a22 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -40,9 +40,9 @@ __package__ = "novelWriter" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen" __license__ = "GPLv3" -__version__ = "0.9.1" -__hexversion__ = "0x000901f0" -__date__ = "2020-06-21" +__version__ = "0.9.2" +__hexversion__ = "0x000902f0" +__date__ = "2020-06-26" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" __status__ = "Pre-Release" diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 4ce89213..9e244c8b 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 407 + 408 71 - 15118 + 15120 False diff --git a/setup.py b/setup.py index c92fc797..4f609cb9 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ with open("README.md", "r") as inFile: setuptools.setup( name = "novelWriter", - version = "0.9.1", + version = "0.9.2", author = "Veronica Berglyd Olsen", author_email = "code@vkbo.net", description = "A markdown-like document editor for writing novels", From 9126a1de2bb7945b71b1b34175075cddb7d9d481 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 21:21:25 +0200 Subject: [PATCH 59/67] Updated changelog --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5d2c7b9..090ddfb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # novelWriter ChangeLog +## Version 0.9.2 [2020-06-26] + +**Bugfixes** + +* The project tree word counts were getting mixed up when a file was moved to the trash folder, or permanently deleted. This has now been fixed, and moving a file should give a zero net change of project word count. Permanently deleting it would result in a negative net change. Issue #333, PR #335. + +**User Interface** + +* There is a feature in the project tree class that ensures that the tree item being acted on is visible in the tree. It is called when you for instance click the header of an open document. It was also activated when opening a document from the tree view with either double-click by mouse, or by using the Enter key. This meant that the tree view would often move, which made it hard to mouse click on items after eachother since you ended up chasing a moving target. This feature is now disabled for document open. In addition, the scroll into view feature has been added to the search/replace call to move into the next document when reaching the end of the current document. This was requested in Issue #332. PR #334. +* The Build Novel Project tool will now display the build time of the document in the preview window in order for the user to know if it is potentially out of date. The timestamp is given, as well as a fuzzy time string, indicating the age of the content. Issue #331, PRs #336 and #337. + +**Documentation** + +* The documentation has been updated to clarify the correct formatting for italic, bold and strikethrough formatting tags. Issue #220, PR #338. + + ## Version 0.9.1 [2020-06-21] **Bugfixes** From c495825018a0a27ffb9a079ac13cb7cc27517351 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 21:23:39 +0200 Subject: [PATCH 60/67] Grammatical error --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 090ddfb1..2c963c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Bugfixes** -* The project tree word counts were getting mixed up when a file was moved to the trash folder, or permanently deleted. This has now been fixed, and moving a file should give a zero net change of project word count. Permanently deleting it would result in a negative net change. Issue #333, PR #335. +* The project tree word counts were getting mixed up when a file was moved to the trash folder, or permanently deleted. This has now been fixed, and moving a file should give a zero net change of project word count. Permanently deleting it will result in a negative net change. Issue #333, PR #335. **User Interface** From 24582f4c2c2a14a474b273e55df4d1fddbb44dc9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 21:36:30 +0200 Subject: [PATCH 61/67] Deleted the lib folder --- lib/textanalyse.py | 186 --------------------------------------------- 1 file changed, 186 deletions(-) delete mode 100644 lib/textanalyse.py diff --git a/lib/textanalyse.py b/lib/textanalyse.py deleted file mode 100644 index 6cfe85b2..00000000 --- a/lib/textanalyse.py +++ /dev/null @@ -1,186 +0,0 @@ -# -*- coding: utf-8 -*- -""" - novelWriter – Text Analysis Class -=================================== - Class for analysing bits of text. - - File History: - Created: 2018-09-22 [0.0.1] - - This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen - - This program 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. - - This program 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 GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -""" - -import logging -import nw - -from time import time - -logger = logging.getLogger(__name__) - -class TextAnalysis(): - - def __init__(self, theText, langCode): - self.theText = theText - self.langCode = langCode - return - - def getStats(self): - tStart = time() - wordCount = self._countWords() - tEnd = time()-tStart - logger.verbose("Words: %7d in %8.3f ms" % (wordCount,tEnd*1e3)) - tStart = time() - sentCount = self._countSentences() - tEnd = time()-tStart - logger.verbose("Sentences: %7d in %8.3f ms" % (sentCount,tEnd*1e3)) - tStart = time() - paraCount = self._countParagraphs() - tEnd = time()-tStart - logger.verbose("Paragraphs: %7d in %8.3f ms" % (paraCount,tEnd*1e3)) - return wordCount, sentCount, paraCount - - def getReadabilityScore(self): - """Calculate Flesch--Kincaid Readability Score. - """ - tStart = time() - wordCount = self._countWords() - sentCount = self._countSentences() - if self.langCode[:3] == "en_": - ratSyllWord = self._countSyllablesEN() - else: - ratSyllWord = -1.0 - rScore = 206.835 - 1.015*(wordCount/sentCount) - 84.6*(ratSyllWord) - gLevel = -15.59 + 0.390*(wordCount/sentCount) + 11.8*(ratSyllWord) - tEnd = time()-tStart - logger.verbose("Readability: %7.3f in %8.3f ms" % (rScore,tEnd*1e3)) - logger.verbose("Grade Level: %7.3f in %8.3f ms" % (gLevel,tEnd*1e3)) - logger.verbose("Assessment: %s" % self.getReadabilityText(rScore)) - - return rScore, gLevel - - def getReadabilityText(self, rScore): - if rScore >= 90.0: - return "Very Easy" - elif rScore >= 80.0: - return "Easy" - elif rScore >= 70.0: - return "Fairly Easy" - elif rScore >= 60.0: - return "Average" - elif rScore >= 50.0: - return "Fairly Difficult" - elif rScore >= 30.0: - return "Difficult" - else: - return "Very Difficult" - - # - # Internal Functions - # - - def _countWords(self): - """Counts the number of words in a text by simply splitting on - all white spaces. - """ - return len(self.theText.strip().split()) - - def _countSentences(self): - """Counts the number of non-repeated sentence endings seen in - the text. Note: This will count filenames and urls as multiple - sentences. - """ - nSent = 0 - sawEnd = False - for ch in self.theText.strip(): - if ch in ".!?": - if not sawEnd: - sawEnd = True - nSent += 1 - else: - sawEnd = False - return nSent - - def _countParagraphs(self, pThreshold=2): - """Counts the number of paragraphs by counting repeated line - breaks. - """ - nPara = 1 - sawEnd = 0 - for ch in self.theText.strip(): - if ch == "\r": # Ignore Windows line end chars - continue - if ch == "\n": # Count endlines - sawEnd += 1 - else: # If non-endline is encountered, check condition for paragraph - if sawEnd >= pThreshold: - nPara += 1 - sawEnd = 0 - return nPara - - def _countSyllablesEN(self): - """Attempt to count the syllables in a piece of English language - text. This function tends to slightly over-estimate the number - of syllables as it doesn't handle the complexity of silent - vowels in endings very well. It will count them all. - """ - - cleanText = "" - for ch in self.theText: - if ch in "abcdefghijklmnopqrstuvwxyz'’": - cleanText += ch - else: - cleanText += " " - - asVow = "aeiouy'’" - dExept = ("ei","ie","ua","ia","eo") - theWords = cleanText.lower().split() - allSylls = 0 - for inWord in theWords: - nChar = len(inWord) - nSyll = 0 - wasVow = False - wasY = False - if nChar == 0: - continue - if inWord[0] in asVow: - nSyll += 1 - wasVow = True - wasY = inWord[0] == "y" - for c in range(1,nChar): - isVow = False - if inWord[c] in asVow: - nSyll += 1 - isVow = True - if isVow and wasVow: - nSyll -= 1 - if isVow and wasY: - nSyll -= 1 - if inWord[c:c+2] in dExept: - nSyll += 1 - wasVow = isVow - wasY = inWord[c] == "y" - if inWord.endswith(("e")): - nSyll -= 1 - if inWord.endswith(("le","ea","io")): - nSyll += 1 - if nSyll < 1: - nSyll = 1 - allSylls += nSyll - - return allSylls/len(theWords) - -# END Class TextAnalysis From e6a8488e08e8e91771724e1758f88415027f4a1f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 27 Jun 2020 17:00:41 +0200 Subject: [PATCH 62/67] Some minor tweaks to the session log filter logic --- nw/gui/sessionlog.py | 11 ++++++----- sample/nwProject.nwx | 8 ++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index cab4a92c..9e620f13 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -497,16 +497,17 @@ class GuiSessionLog(QDialog): wcTotal += wcNotes dwTotal = wcTotal - pcTotal + if isFirst: + # Subtract the offset from the first list entry + dwTotal -= self.wordOffset + dwTotal = max(dwTotal, 0) # But don't go negative + isFirst = False + if hideZeros and dwTotal == 0: continue if hideNegative and dwTotal < 0: continue - if isFirst: - # Subtract the offset from the first list entry - dwTotal -= self.wordOffset - isFirst = False - if groupByDay: sStart = dStart.strftime(nwConst.dStampFmt) else: diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 015816ff..c88c5269 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 589 - 102 - 24247 + 591 + 103 + 24557 False From 1b773c1b44ef437d3f609c351bd1601acefb2674 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 27 Jun 2020 17:20:47 +0200 Subject: [PATCH 63/67] Bump the version of the project XML file to prevent data loss if going back to an older version of novelWriter --- nw/core/project.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index 6b17b51a..1a0a035e 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -356,6 +356,15 @@ class NWProject(): # Check Project Storage Version # ============================= + # Changes: + # 1.0 : Original file format. + # 1.1 : Changes the way documents are structure in the project + # folder from data_X, where X is the first hex value of + # the handle, to a single content folder. + # 1.2 : Changes the way autoReplace entries are stored. The 1.1 + # parser will lose the autoReplace settings if allowed to + # read the file. Introduced in version 0.10. + if fileVersion == "1.0": msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Old Project Version", ( @@ -369,10 +378,10 @@ class NWProject(): if msgRes != QMessageBox.Yes: return False - elif fileVersion != "1.1": + elif fileVersion != "1.1" and fileVersion != "1.2": self.makeAlert(( - "Unknown or unsupported %s project format. " - "The project cannot be opened by this version of %s." + "Unknown or unsupported %s project file format. " + "The project cannot be opened by this version of %s. " ) % ( nw.__package__, nw.__package__ ), nwAlert.ERROR) @@ -512,7 +521,7 @@ class NWProject(): nwXML = etree.Element("novelWriterXML", attrib={ "appVersion" : str(nw.__version__), "hexVersion" : str(nw.__hexversion__), - "fileVersion" : "1.1", + "fileVersion" : "1.2", "timeStamp" : formatTimeStamp(saveTime), }) From f4625647e15649ac76f4327bac35201ad5c2f1b1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 27 Jun 2020 17:21:03 +0200 Subject: [PATCH 64/67] Updated changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 413df241..f1a356d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## Version 0.10.0 RC1 [2020-xx-xx] +Note: If the project file is opened by this version of novelWriter, the project file can no longer be read by an earlier version due to the change of how autoReplace settings are stored. + +**User Interface** + +* The Session Log dialog has been redesigned and now has a few more filter options. The filters now also update the filtered time count properly. The dialog now shows a histogram of words added in a given session, or optionally, on a given date. The filtered log data can also be saved as a JSON or CSV file, the latter suitable for importing to a spread sheet. The new dialog tool required a new session log file format, so the new session log has been given a new file name. The old log file will be left untouched in the project's `meta` folder. For projects created prior to this change, the log will record a word count offset that will be subtracted from the first entry such that the first word diff will always be 0 instead of the total word count of the entire project. Such a large word diff would otherwise saturate the histogram. PR #339. + +**Other Changes** + +* The way the auto-replace settings are stored in the project XML file has been changed in order to be more consistent with other features, and to avoid a potential pitfall in defining the tag name from a user-entered string. The project class retains its ability to read the old format of the file, and will save in the new format. PR #344. + ## Version 0.9.2 [2020-06-26] From 3d162b880acc8f8fc554fd700571d44cadc72f39 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 27 Jun 2020 17:29:47 +0200 Subject: [PATCH 65/67] Alert the user to which novelWriter version the file was saved with. --- nw/core/project.py | 3 ++- sample/nwProject.nwx | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index 1a0a035e..20e12a2a 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -382,8 +382,9 @@ class NWProject(): self.makeAlert(( "Unknown or unsupported %s project file format. " "The project cannot be opened by this version of %s. " + "The file was saved with novelWriter version %s." ) % ( - nw.__package__, nw.__package__ + nw.__package__, nw.__package__, appVersion ), nwAlert.ERROR) return False diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index c88c5269..c5a194a5 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 591 + 593 103 - 24557 + 24563 False From c241e5f52e78b8a248246a0ac7b2f0e16e672527 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 27 Jun 2020 17:31:33 +0200 Subject: [PATCH 66/67] Do it right! --- nw/core/project.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index 20e12a2a..5e5400c9 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -380,11 +380,12 @@ class NWProject(): elif fileVersion != "1.1" and fileVersion != "1.2": self.makeAlert(( - "Unknown or unsupported %s project file format. " - "The project cannot be opened by this version of %s. " - "The file was saved with novelWriter version %s." - ) % ( - nw.__package__, nw.__package__, appVersion + "Unknown or unsupported {nw:s} project file format. " + "The project cannot be opened by this version of {nw:s}. " + "The file was saved with {nw:s} version {vers:s}." + ).format( + nw = nw.__package__, + vers = appVersion, ), nwAlert.ERROR) return False From 40e7213a398939f340b7c99220108a7f6c6ad300 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 27 Jun 2020 17:44:48 +0200 Subject: [PATCH 67/67] And another fix, plus some cleanup --- nw/gui/sessionlog.py | 49 ++++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index 9e620f13..e00feb21 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -95,7 +95,6 @@ class GuiSessionLog(QDialog): self.listBox.setColumnWidth(self.C_TIME, wCol0) self.listBox.setColumnWidth(self.C_LENGTH, wCol1) self.listBox.setColumnWidth(self.C_COUNT, wCol2) - self.listBox.resizeColumnToContents(self.C_COUNT) hHeader = self.listBox.headerItem() hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight) @@ -148,15 +147,15 @@ class GuiSessionLog(QDialog): self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.infoForm.addWidget(QLabel("Total Time:"), 0, 0) - self.infoForm.addWidget(self.labelTotal, 0, 1) self.infoForm.addWidget(QLabel("Filtered Time:"), 1, 0) - self.infoForm.addWidget(self.labelFilter, 1, 1) self.infoForm.addWidget(QLabel("Novel Word Count:"), 2, 0) - self.infoForm.addWidget(self.novelWords, 2, 1) self.infoForm.addWidget(QLabel("Notes Word Count:"), 3, 0) - self.infoForm.addWidget(self.notesWords, 3, 1) self.infoForm.addWidget(QLabel("Total Word Count:"), 4, 0) - self.infoForm.addWidget(self.totalWords, 4, 1) + self.infoForm.addWidget(self.labelTotal, 0, 1) + self.infoForm.addWidget(self.labelFilter, 1, 1) + self.infoForm.addWidget(self.novelWords, 2, 1) + self.infoForm.addWidget(self.notesWords, 3, 1) + self.infoForm.addWidget(self.totalWords, 4, 1) self.infoForm.setRowStretch(5, 1) # Filter Options @@ -166,51 +165,47 @@ class GuiSessionLog(QDialog): self.filterForm = QGridLayout(self) self.filterBox.setLayout(self.filterForm) - self.labelNovel = QLabel("Count novel files") self.incNovel = QSwitch(width=2*sPx, height=sPx) self.incNovel.setChecked( self.optState.getBool("GuiSessionLog", "incNovel", True) ) self.incNovel.clicked.connect(self._updateListBox) - self.labelNotes = QLabel("Count note files") self.incNotes = QSwitch(width=2*sPx, height=sPx) self.incNotes.setChecked( self.optState.getBool("GuiSessionLog", "incNotes", True) ) self.incNotes.clicked.connect(self._updateListBox) - self.labelZeros = QLabel("Hide zero word count") self.hideZeros = QSwitch(width=2*sPx, height=sPx) self.hideZeros.setChecked( self.optState.getBool("GuiSessionLog", "hideZeros", True) ) self.hideZeros.clicked.connect(self._updateListBox) - self.labelNegative = QLabel("Hide negative word count") self.hideNegative = QSwitch(width=2*sPx, height=sPx) self.hideNegative.setChecked( self.optState.getBool("GuiSessionLog", "hideNegative", False) ) self.hideNegative.clicked.connect(self._updateListBox) - self.labelByDay = QLabel("Group entries by day") self.groupByDay = QSwitch(width=2*sPx, height=sPx) self.groupByDay.setChecked( self.optState.getBool("GuiSessionLog", "groupByDay", False) ) self.groupByDay.clicked.connect(self._updateListBox) - self.filterForm.addWidget(self.labelNovel, 0, 0) - self.filterForm.addWidget(self.incNovel, 0, 1) - self.filterForm.addWidget(self.labelNotes, 1, 0) - self.filterForm.addWidget(self.incNotes, 1, 1) - self.filterForm.addWidget(self.labelZeros, 2, 0) - self.filterForm.addWidget(self.hideZeros, 2, 1) - self.filterForm.addWidget(self.labelNegative, 3, 0) - self.filterForm.addWidget(self.hideNegative, 3, 1) - self.filterForm.addWidget(self.labelByDay, 4, 0) - self.filterForm.addWidget(self.groupByDay, 4, 1) + self.filterForm.addWidget(QLabel("Count novel files"), 0, 0) + self.filterForm.addWidget(QLabel("Count note files"), 1, 0) + self.filterForm.addWidget(QLabel("Hide zero word count"), 2, 0) + self.filterForm.addWidget(QLabel("Hide negative word count"), 3, 0) + self.filterForm.addWidget(QLabel("Group entries by day"), 4, 0) + self.filterForm.addWidget(self.incNovel, 0, 1) + self.filterForm.addWidget(self.incNotes, 1, 1) + self.filterForm.addWidget(self.hideZeros, 2, 1) + self.filterForm.addWidget(self.hideNegative, 3, 1) + self.filterForm.addWidget(self.groupByDay, 4, 1) + self.filterForm.setRowStretch(5, 1) # Buttons self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) @@ -497,17 +492,17 @@ class GuiSessionLog(QDialog): wcTotal += wcNotes dwTotal = wcTotal - pcTotal - if isFirst: - # Subtract the offset from the first list entry - dwTotal -= self.wordOffset - dwTotal = max(dwTotal, 0) # But don't go negative - isFirst = False - if hideZeros and dwTotal == 0: continue if hideNegative and dwTotal < 0: continue + if isFirst: + # Subtract the offset from the first list entry + dwTotal -= self.wordOffset + dwTotal = max(dwTotal, 1) # Don't go zero or negative + isFirst = False + if groupByDay: sStart = dStart.strftime(nwConst.dStampFmt) else: