diff --git a/nw/config.py b/nw/config.py index 5f4a1300..5ed12655 100644 --- a/nw/config.py +++ b/nw/config.py @@ -102,6 +102,10 @@ class Config: self.outlnPanePos = [500, 150] self.isFullScreen = False + ## Features + self.hideVScroll = False # Hide vertical scroll bars on main widgets + self.hideHScroll = False # Hide horizontal scroll bars on main widgets + ## Project self.autoSaveProj = 60 self.autoSaveDoc = 30 @@ -122,6 +126,9 @@ class Config: self.doReplaceDQuote = True self.doReplaceDash = True self.doReplaceDots = True + self.scrollPastEnd = True + self.scollWithCursor = False + self.wordCountTimer = 5.0 self.showTabsNSpaces = False self.showLineEndings = False @@ -391,6 +398,12 @@ class Config: self.isFullScreen = self._parseLine( cnfParse, cnfSec, "fullscreen", self.CNF_BOOL, self.isFullScreen ) + self.hideVScroll = self._parseLine( + cnfParse, cnfSec, "hidevscroll", self.CNF_BOOL, self.hideVScroll + ) + self.hideHScroll = self._parseLine( + cnfParse, cnfSec, "hidehscroll", self.CNF_BOOL, self.hideHScroll + ) ## Project cnfSec = "Project" @@ -448,6 +461,12 @@ class Config: self.doReplaceDots = self._parseLine( cnfParse, cnfSec, "repdots", self.CNF_BOOL, self.doReplaceDots ) + self.scrollPastEnd = self._parseLine( + cnfParse, cnfSec, "scrollpastend", self.CNF_BOOL, self.scrollPastEnd + ) + self.scollWithCursor = self._parseLine( + cnfParse, cnfSec, "scollwithcursor", self.CNF_BOOL, self.scollWithCursor + ) self.fmtSingleQuotes = self._parseLine( cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes ) @@ -565,6 +584,8 @@ class Config: cnfParse.set(cnfSec, "viewpane", self._packList(self.viewPanePos)) cnfParse.set(cnfSec, "outlinepane", self._packList(self.outlnPanePos)) cnfParse.set(cnfSec, "fullscreen", str(self.isFullScreen)) + cnfParse.set(cnfSec, "hidevscroll", str(self.hideVScroll)) + cnfParse.set(cnfSec, "hidehscroll", str(self.hideHScroll)) ## Project cnfSec = "Project" @@ -590,6 +611,8 @@ class Config: cnfParse.set(cnfSec, "repdquotes", str(self.doReplaceDQuote)) cnfParse.set(cnfSec, "repdash", str(self.doReplaceDash)) cnfParse.set(cnfSec, "repdots", str(self.doReplaceDots)) + cnfParse.set(cnfSec, "scrollpastend", str(self.scrollPastEnd)) + cnfParse.set(cnfSec, "scollwithcursor", str(self.scollWithCursor)) cnfParse.set(cnfSec, "fmtsinglequote", self._packList(self.fmtSingleQuotes)) cnfParse.set(cnfSec, "fmtdoublequote", self._packList(self.fmtDoubleQuotes)) cnfParse.set(cnfSec, "spelltool", str(self.spellTool)) diff --git a/nw/gui/build.py b/nw/gui/build.py index ad9af97b..754553d8 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -450,11 +450,19 @@ class GuiBuildNovel(QDialog): # Tool Box Scroll Area self.toolsArea = QScrollArea() self.toolsArea.setMinimumWidth(self.mainConf.pxInt(250)) - self.toolsArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.toolsArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.toolsArea.setWidgetResizable(True) self.toolsArea.setWidget(self.toolsWidget) + if self.mainConf.hideVScroll: + self.toolsArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.toolsArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + if self.mainConf.hideHScroll: + self.toolsArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.toolsArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + # Tools and Buttons Layout self.innerBox = QVBoxLayout() self.innerBox.addWidget(self.toolsArea) @@ -1116,6 +1124,17 @@ class GuiBuildNovelDocView(QTextBrowser): else: self.setTabStopWidth(self.mainConf.getTabWidth()) + # Scroll bars + if self.mainConf.hideVScroll: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + if self.mainConf.hideHScroll: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + docPalette = self.palette() docPalette.setColor(QPalette.Base, QColor(255, 255, 255)) docPalette.setColor(QPalette.Text, QColor(0, 0, 0)) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 1d1d2a4e..d65c89df 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -88,6 +88,8 @@ class GuiDocEditor(QTextEdit): self.bigDoc = False # Flag for very large document size self.doReplace = False # Switch to temporarily disable auto-replace self.queuePos = None # Used for delayed change of cursor position + self.cursorLast = 0 # The last known vertical position of the cursor + self.lengthLast = 0 # Typography self.typDQOpen = self.mainConf.fmtDoubleQuotes[0] @@ -100,6 +102,8 @@ class GuiDocEditor(QTextEdit): self.qDocument.contentsChange.connect(self._docChange) self.qDocument.documentLayout().documentSizeChanged.connect(self._docSizeChanged) + self.verticalScrollBar().sliderMoved.connect(self._doVerticalScroll) + # Document Title self.docHeader = GuiDocEditHeader(self) self.docFooter = GuiDocEditFooter(self) @@ -224,6 +228,17 @@ class GuiDocEditor(QTextEdit): self.qDocument.setDefaultTextOption(theOpt) + # Scroll bars + if self.mainConf.hideVScroll: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + if self.mainConf.hideHScroll: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + # Refresh the tab stops if self.mainConf.verQtValue >= 51000: self.setTabStopDistance(self.mainConf.getTabWidth()) @@ -313,6 +328,7 @@ class GuiDocEditor(QTextEdit): self.setCursorLine(tLine) self.docFooter.updateLineCount() + self.lengthLast = self.qDocument.characterCount() qApp.restoreOverrideCursor() @@ -360,9 +376,14 @@ class GuiDocEditor(QTextEdit): return False docText = self.getText() + + cC, wC, pC = countWords(docText) + self._updateCounts(cC, wC, pC) + theItem.setCharCount(self.charCount) theItem.setWordCount(self.wordCount) theItem.setParaCount(self.paraCount) + self.saveCursorPosition() self.nwDocument.saveDocument(docText) self.setDocumentChanged(False) @@ -377,6 +398,7 @@ class GuiDocEditor(QTextEdit): just ensure the margins are set correctly. """ wW = self.width() + wH = self.height() cM = self.mainConf.getTextMargin() vBar = self.verticalScrollBar() @@ -400,7 +422,7 @@ class GuiDocEditor(QTextEdit): tW = wW - 2*tB - sW tH = self.docHeader.height() fH = self.docFooter.height() - fY = self.height() - fH - tB - sH + fY = wH - fH - tB - sH self.docHeader.setGeometry(tB, tB, tW, tH) self.docFooter.setGeometry(tB, fY, tW, fH) @@ -412,7 +434,14 @@ class GuiDocEditor(QTextEdit): else: rH = 0 - self.setViewportMargins(tM, max(cM, tH, rH), tM, max(cM, fH)) + uM = max(cM, tH, rH) + lM = max(cM, fH) + self.setViewportMargins(tM, uM, tM, lM) + + if self.mainConf.scrollPastEnd: + docFrame = self.qDocument.rootFrame().frameFormat() + docFrame.setBottomMargin(wH - uM - lM - 4*tB - self.theTheme.fontPixelSize) + self.qDocument.rootFrame().setFrameFormat(docFrame) return @@ -460,11 +489,14 @@ class GuiDocEditor(QTextEdit): """ if not isinstance(thePosition, int): return False + if thePosition >= 0: theCursor = self.textCursor() theCursor.setPosition(thePosition) self.setTextCursor(theCursor) self.docFooter.updateLineCount() + self.cursorLast = self.cursorRect().center().y() + return True def getCursorPosition(self): @@ -743,6 +775,22 @@ class GuiDocEditor(QTextEdit): QTextEdit.keyPressEvent(self, keyEvent) self.docFooter.updateLineCount() + if self.mainConf.scollWithCursor: + docLen = self.qDocument.characterCount() + if docLen == self.lengthLast: + # No change, so just update last position + self.cursorLast = self.cursorRect().center().y() + else: + # The user typed something, so check if we need to + # scroll, and move the scroll bar the same distance + self.lengthLast = docLen + self.ensureCursorVisible() + cPos = self.cursorRect().center().y() + if cPos != self.cursorLast: + vBar = self.verticalScrollBar() + vBar.setValue(vBar.value() + cPos - self.cursorLast) + self.cursorLast = self.cursorRect().center().y() + return def focusNextPrevChild(self, toNext): @@ -768,9 +816,18 @@ class GuiDocEditor(QTextEdit): QTextEdit.mouseReleaseEvent(self, mEvent) self.docFooter.updateLineCount() + self.cursorLast = self.cursorRect().center().y() return + def wheelEvent(self, theEvent): + """Briefly capture the mouse wheel event to capture the cursor + position. + """ + QTextEdit.wheelEvent(self, theEvent) + self.cursorLast = self.cursorRect().center().y() + return + def resizeEvent(self, theEvent): """If the text editor is resize, we must make sure the document has its margins adjusted according to user preferences. @@ -805,6 +862,13 @@ class GuiDocEditor(QTextEdit): self._docAutoReplace(self.qDocument.findBlock(thePos)) return + @pyqtSlot(int) + def _doVerticalScroll(self, theChange): + """Update the cursor position on vertical scrolling. + """ + self.cursorLast = self.cursorRect().center().y() + return + @pyqtSlot("QPoint") def _openContextMenu(self, thePos): """Triggered by right click to open the context menu. Also diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index b7404e9c..2d9ac637 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -124,6 +124,17 @@ class GuiDocViewer(QTextBrowser): theOpt.setAlignment(Qt.AlignJustify) self.qDocument.setDefaultTextOption(theOpt) + # Scroll bars + if self.mainConf.hideVScroll: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + if self.mainConf.hideHScroll: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + # Refresh the tab stops if self.mainConf.verQtValue >= 51000: self.setTabStopDistance(self.mainConf.getTabWidth()) diff --git a/nw/gui/outline.py b/nw/gui/outline.py index 6d524992..d3dc4815 100644 --- a/nw/gui/outline.py +++ b/nw/gui/outline.py @@ -118,6 +118,7 @@ class GuiOutline(QTreeWidget): self.colIndex = {} self.treeNCols = 0 + self.initOutline() self.clearOutline() self.headerMenu.setHiddenState(self.colHidden) @@ -125,6 +126,22 @@ class GuiOutline(QTreeWidget): return + def initOutline(self): + """Set or update outline settings. + """ + # Scroll bars + if self.mainConf.hideVScroll: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + if self.mainConf.hideHScroll: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + return + def clearOutline(self): """Clear the tree and header and set the default values for the columns arrays. diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py index c6372826..49998dd9 100644 --- a/nw/gui/outlinedetails.py +++ b/nw/gui/outlinedetails.py @@ -224,10 +224,28 @@ class GuiOutlineDetails(QScrollArea): self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setWidgetResizable(True) + self.initDetails() + logger.debug("GuiOutlineDetails initialisation complete") return + def initDetails(self): + """Set or update outline settings. + """ + # Scroll bars + if self.mainConf.hideVScroll: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + if self.mainConf.hideHScroll: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + return + def showItem(self, tHandle, sTitle): """Update the content of the tree with the given handle and line number pointing to a header. diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index a946c289..f1848bb3 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -55,15 +55,17 @@ class GuiPreferences(PagedDialog): self.setWindowTitle("Preferences") - self.tabGeneral = GuiConfigEditGeneralTab(self.theParent) - self.tabLayout = GuiConfigEditLayoutTab(self.theParent) - self.tabEditing = GuiConfigEditEditingTab(self.theParent) - self.tabAutoRep = GuiConfigEditAutoReplaceTab(self.theParent) + self.tabGeneral = GuiConfigEditGeneralTab(self.theParent) + self.tabProjects = GuiConfigEditProjectsTab(self.theParent) + self.tabLayout = GuiConfigEditLayoutTab(self.theParent) + self.tabEditing = GuiConfigEditEditingTab(self.theParent) + self.tabAutoRep = GuiConfigEditAutoReplaceTab(self.theParent) - self.addTab(self.tabGeneral, "General") - self.addTab(self.tabLayout, "Text Layout") - self.addTab(self.tabEditing, "Editor") - self.addTab(self.tabAutoRep, "Auto-Replace") + self.addTab(self.tabGeneral, "General") + self.addTab(self.tabProjects, "Projects") + self.addTab(self.tabLayout, "Text Layout") + self.addTab(self.tabEditing, "Editor") + self.addTab(self.tabAutoRep, "Auto-Replace") self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox.accepted.connect(self._doSave) @@ -91,6 +93,10 @@ class GuiPreferences(PagedDialog): validEntries &= retA needsRestart |= retB + retA, retB = self.tabProjects.saveValues() + validEntries &= retA + needsRestart |= retB + retA, retB = self.tabLayout.saveValues() validEntries &= retA needsRestart |= retB @@ -219,9 +225,94 @@ class GuiConfigEditGeneralTab(QWidget): self.showFullPath.setChecked(self.mainConf.showFullPath) self.mainForm.addRow( "Show full path in document header", - self.showFullPath + self.showFullPath, + "Shows the document title and parent folder names." ) + self.hideVScroll = QSwitch() + self.hideVScroll.setChecked(self.mainConf.hideVScroll) + self.mainForm.addRow( + "Hide vertical scroll bars in main windows", + self.hideVScroll, + "Scrolling with mouse wheel and keys only." + ) + + self.hideHScroll = QSwitch() + self.hideHScroll.setChecked(self.mainConf.hideHScroll) + self.mainForm.addRow( + "Hide horizontal scroll bars in main windows", + self.hideHScroll, + "Scrolling with mouse wheel and keys only." + ) + + return + + def saveValues(self): + """Save the values set for this tab. + """ + validEntries = True + needsRestart = False + + guiTheme = self.selectTheme.currentData() + guiIcons = self.selectIcons.currentData() + guiDark = self.preferDarkIcons.isChecked() + guiFont = self.guiFont.text() + guiFontSize = self.guiFontSize.value() + showFullPath = self.showFullPath.isChecked() + hideVScroll = self.hideVScroll.isChecked() + hideHScroll = self.hideHScroll.isChecked() + + # Check if restart is needed + needsRestart |= self.mainConf.guiTheme != guiTheme + needsRestart |= self.mainConf.guiIcons != guiIcons + needsRestart |= self.mainConf.guiFont != guiFont + needsRestart |= self.mainConf.guiFontSize != guiFontSize + + self.mainConf.guiTheme = guiTheme + self.mainConf.guiIcons = guiIcons + self.mainConf.guiDark = guiDark + self.mainConf.guiFont = guiFont + self.mainConf.guiFontSize = guiFontSize + self.mainConf.showFullPath = showFullPath + self.mainConf.hideVScroll = hideVScroll + self.mainConf.hideHScroll = hideHScroll + + self.mainConf.confChanged = True + + return validEntries, needsRestart + + ## + # Slots + ## + + def _selectFont(self): + """Open the QFontDialog and set a font for the font style. + """ + currFont = QFont() + currFont.setFamily(self.mainConf.guiFont) + currFont.setPointSize(self.mainConf.guiFontSize) + theFont, theStatus = QFontDialog.getFont(currFont, self) + if theStatus: + self.guiFont.setText(theFont.family()) + self.guiFontSize.setValue(theFont.pointSize()) + return + +# END Class GuiConfigEditGeneralTab + +class GuiConfigEditProjectsTab(QWidget): + + def __init__(self, theParent): + QWidget.__init__(self, theParent) + + self.mainConf = nw.CONFIG + self.theParent = theParent + self.theTheme = theParent.theTheme + + # The Form + self.mainForm = QConfigLayout() + self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.setLayout(self.mainForm) + # AutoSave Settings # ================= self.mainForm.addGroupLabel("Automatic Save") @@ -292,30 +383,12 @@ class GuiConfigEditGeneralTab(QWidget): validEntries = True needsRestart = False - guiTheme = self.selectTheme.currentData() - guiIcons = self.selectIcons.currentData() - guiDark = self.preferDarkIcons.isChecked() - guiFont = self.guiFont.text() - guiFontSize = self.guiFontSize.value() - showFullPath = self.showFullPath.isChecked() autoSaveDoc = self.autoSaveDoc.value() autoSaveProj = self.autoSaveProj.value() backupPath = self.backupPath backupOnClose = self.backupOnClose.isChecked() askBeforeBackup = self.askBeforeBackup.isChecked() - # Check if restart is needed - needsRestart |= self.mainConf.guiTheme != guiTheme - needsRestart |= self.mainConf.guiIcons != guiIcons - needsRestart |= self.mainConf.guiFont != guiFont - needsRestart |= self.mainConf.guiFontSize != guiFontSize - - self.mainConf.guiTheme = guiTheme - self.mainConf.guiIcons = guiIcons - self.mainConf.guiDark = guiDark - self.mainConf.guiFont = guiFont - self.mainConf.guiFontSize = guiFontSize - self.mainConf.showFullPath = showFullPath self.mainConf.autoSaveDoc = autoSaveDoc self.mainConf.autoSaveProj = autoSaveProj self.mainConf.backupPath = backupPath @@ -357,19 +430,7 @@ class GuiConfigEditGeneralTab(QWidget): self.askBeforeBackup.setEnabled(theState) return - def _selectFont(self): - """Open the QFontDialog and set a font for the font style. - """ - currFont = QFont() - currFont.setFamily(self.mainConf.guiFont) - currFont.setPointSize(self.mainConf.guiFontSize) - theFont, theStatus = QFontDialog.getFont(currFont, self) - if theStatus: - self.guiFont.setText(theFont.family()) - self.guiFontSize.setValue(theFont.pointSize()) - return - -# END Class GuiConfigEditGeneralTab +# END Class GuiConfigEditProjectsTab class GuiConfigEditLayoutTab(QWidget): @@ -494,6 +555,24 @@ class GuiConfigEditLayoutTab(QWidget): theUnit="px" ) + ## Scroll Past End + self.scrollPastEnd = QSwitch() + self.scrollPastEnd.setChecked(self.mainConf.scrollPastEnd) + self.mainForm.addRow( + "Scroll past end of the document", + self.scrollPastEnd, + "Allows scrolling until last line is at the top." + ) + + ## Typewriter Scrolling + self.scollWithCursor = QSwitch() + self.scollWithCursor.setChecked(self.mainConf.scollWithCursor) + self.mainForm.addRow( + "Typewriter style scrolling", + self.scollWithCursor, + "Scrolls up when the cursor moves to a new line." + ) + return def saveValues(self): @@ -511,6 +590,8 @@ class GuiConfigEditLayoutTab(QWidget): doJustify = self.textJustify.isChecked() textMargin = self.textMargin.value() tabWidth = self.tabWidth.value() + scrollPastEnd = self.scrollPastEnd.isChecked() + scollWithCursor = self.scollWithCursor.isChecked() self.mainConf.textFont = textFont self.mainConf.textSize = textSize @@ -521,6 +602,8 @@ class GuiConfigEditLayoutTab(QWidget): self.mainConf.doJustify = doJustify self.mainConf.textMargin = textMargin self.mainConf.tabWidth = tabWidth + self.mainConf.scrollPastEnd = scrollPastEnd + self.mainConf.scollWithCursor = scollWithCursor self.mainConf.confChanged = True diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 858948dc..9f4bb5e6 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -115,6 +115,9 @@ class GuiProjectTree(QTreeWidget): # The last column should just auto-scale self.resizeColumnToContents(self.C_FLAGS) + # Set custom settings + self.initTree() + logger.debug("GuiProjectTree initialisation complete") # Internal Mapping @@ -122,6 +125,22 @@ class GuiProjectTree(QTreeWidget): return + def initTree(self): + """Set or update tree widget settings. + """ + # Scroll bars + if self.mainConf.hideVScroll: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + if self.mainConf.hideHScroll: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + return + ## # Class Methods ## @@ -241,7 +260,8 @@ class GuiProjectTree(QTreeWidget): # Add the new item to the tree if tHandle is not None: self.revealNewTreeItem(tHandle, nHandle) - self.theParent.editItem(tHandle) + if self.mainConf.showGUI: + self.theParent.editItem(tHandle) return True diff --git a/nw/guimain.py b/nw/guimain.py index 93c62210..18801727 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -790,6 +790,9 @@ class GuiMain(QMainWindow): self.saveDocument() self.docEditor.initEditor() self.docViewer.initViewer() + self.treeView.initTree() + self.projView.initOutline() + self.projMeta.initDetails() return diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf index 49054ae7..f4bc8d61 100644 --- a/tests/reference/novelwriter.conf +++ b/tests/reference/novelwriter.conf @@ -1,5 +1,5 @@ [Main] -timestamp = 2020-06-29 17:34:15 +timestamp = 2020-10-11 18:29:34 theme = default syntax = default_light icons = typicons_colour_light @@ -16,6 +16,8 @@ docpane = 400, 400 viewpane = 500, 150 outlinepane = 500, 150 fullscreen = False +hidevscroll = False +hidehscroll = False [Project] autosaveproject = 60 @@ -37,6 +39,8 @@ repsquotes = True repdquotes = True repdash = True repdots = True +scrollpastend = True +scollwithcursor = False fmtsinglequote = ‘, ’ fmtdoublequote = “, ” spelltool = internal diff --git a/tests/reference/novelwriter_prefs.conf b/tests/reference/novelwriter_prefs.conf index 608116ea..0e1d41f6 100644 --- a/tests/reference/novelwriter_prefs.conf +++ b/tests/reference/novelwriter_prefs.conf @@ -16,6 +16,8 @@ docpane = 400, 400 viewpane = 500, 150 outlinepane = 500, 150 fullscreen = False +hidevscroll = True +hidehscroll = True [Project] autosaveproject = 40 @@ -37,6 +39,8 @@ repsquotes = True repdquotes = True repdash = True repdots = True +scrollpastend = False +scollwithcursor = True fmtsinglequote = ‘, ’ fmtdoublequote = “, ” spelltool = internal diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 9d05f56f..ddc77738 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -23,7 +23,7 @@ from nw.gui import ( GuiProjectLoad, GuiPreferences ) from nw.gui.custom import QuotesDialog -from nw.constants import nwItemType, nwItemLayout, nwItemClass +from nw.constants import nwItemLayout, nwItemClass, nwFiles keyDelay = 2 typeDelay = 1 @@ -228,48 +228,20 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): # Create new, save, close project nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": nwFuncTemp}) + qtbot.wait(200) assert nwGUI.saveProject() assert nwGUI.closeProject() qtbot.wait(stepDelay) - # Check that we cannot open when there is no project - nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger) - assert getGuiItem("GuiWritingStats") is None - - assert nwGUI.openProject(nwFuncTemp) - qtbot.wait(stepDelay) - - # Add some text to the scene file - assert nwGUI.openDocument("0e17daca5f3e1") - assert nwGUI.docEditor.insertText( - "# Scene One\n\n" - "It was the best of times, it was the worst of times, it was the age of wisdom, it was " - "the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it " - "was the season of Light, it was the season of Darkness, it was the spring of hope, it " - "was the winter of despair, we had everything before us, we had nothing before us, we " - "were all going direct to Heaven, we were all going direct the other way – in short, the " - "period was so far like the present period, that some of its noisiest authorities " - "insisted on its being received, for good or for evil, in the superlative degree of " - "comparison only.\n\n" - ) - assert nwGUI.saveDocument() - - # Add a note file with some text - nwGUI.setFocus(1) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) - assert nwGUI.openSelectedItem() - assert nwGUI.docEditor.insertText( - "# Jane Doe\n\n" - "All about Jane.\n\n" - ) - assert nwGUI.saveDocument() - qtbot.wait(500) # Ensures that the session length is > 0 - - assert nwGUI.saveProject() - assert nwGUI.closeProject() - qtbot.wait(stepDelay) + sessFile = os.path.join(nwFuncTemp, "meta", nwFiles.SESS_STATS) + with open(sessFile, mode="w+", encoding="utf-8") as outFile: + outFile.write( + "# Start Time End Time Novel Notes\n" + "2020-01-01 21:00:00 2020-01-01 21:00:05 6 0\n" + "2020-01-03 21:00:00 2020-01-03 21:00:15 125 0\n" + "2020-01-03 21:30:00 2020-01-03 21:30:15 125 5\n" + "2020-01-06 21:00:00 2020-01-06 21:00:10 125 5\n" + ) # Open again, and check the stats assert nwGUI.openProject(nwFuncTemp) @@ -288,19 +260,21 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda ss, tt, pp, options: (pp, "")) assert sessLog._saveData(sessLog.FMT_CSV) - qtbot.wait(stepDelay) + qtbot.wait(100) assert sessLog._saveData(sessLog.FMT_JSON) - qtbot.wait(stepDelay) + qtbot.wait(100) jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) - assert len(jsonData) == 2 - assert jsonData[1]["length"] >= 0 - assert jsonData[1]["newWords"] == 126 - assert jsonData[1]["novelWords"] == 127 - assert jsonData[1]["noteWords"] == 5 + qtbot.wait(stepDelay) + + assert len(jsonData) == 3 + assert jsonData[1]["length"] >= 14.0 + assert jsonData[1]["newWords"] == 119 + assert jsonData[1]["novelWords"] == 125 + assert jsonData[1]["noteWords"] == 0 # No Novel Files qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton) @@ -313,9 +287,9 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): jsonData = json.loads(inFile.read()) assert len(jsonData) == 1 - assert jsonData[0]["length"] >= 0 + assert jsonData[0]["length"] >= 14.0 assert jsonData[0]["newWords"] == 5 - assert jsonData[0]["novelWords"] == 127 + assert jsonData[0]["novelWords"] == 125 assert jsonData[0]["noteWords"] == 5 # No Note Files @@ -330,10 +304,10 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): jsonData = json.loads(inFile.read()) assert len(jsonData) == 2 - assert jsonData[1]["length"] >= 0 - assert jsonData[1]["newWords"] == 121 - assert jsonData[1]["novelWords"] == 127 - assert jsonData[1]["noteWords"] == 5 + assert jsonData[1]["length"] >= 14.0 + assert jsonData[1]["newWords"] == 119 + assert jsonData[1]["novelWords"] == 125 + assert jsonData[1]["noteWords"] == 0 # No Negative Entries qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton) @@ -346,7 +320,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) - assert len(jsonData) == 2 + assert len(jsonData) == 3 # Un-hide Zero Entries qtbot.mouseClick(sessLog.hideNegative, Qt.LeftButton) @@ -359,7 +333,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) - assert len(jsonData) == 2 + assert len(jsonData) == 4 # Group by Day qtbot.mouseClick(sessLog.groupByDay, Qt.LeftButton) @@ -373,11 +347,13 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): # Check against both 1 and 2 as this can be 2 if test was started just before midnight. # A failed test should in any case produce a 4 - assert len(jsonData) in (1, 2) + assert len(jsonData) == 3 # qtbot.stopForInteraction() sessLog._doClose() + assert nwGUI.closeProject() + qtbot.wait(stepDelay) nwGUI.closeMain() @pytest.mark.gui @@ -1057,6 +1033,7 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC nwGUI.mainConf = tmpConf nwPrefs.mainConf = tmpConf nwPrefs.tabGeneral.mainConf = tmpConf + nwPrefs.tabProjects.mainConf = tmpConf nwPrefs.tabLayout.mainConf = tmpConf nwPrefs.tabEditing.mainConf = tmpConf nwPrefs.tabAutoRep.mainConf = tmpConf @@ -1065,7 +1042,6 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC qtbot.wait(keyDelay) tabGeneral = nwPrefs.tabGeneral nwPrefs._tabBox.setCurrentWidget(tabGeneral) - tabGeneral.backupPath = "no/where" qtbot.wait(keyDelay) assert not tabGeneral.preferDarkIcons.isChecked() @@ -1077,27 +1053,45 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC qtbot.mouseClick(tabGeneral.showFullPath, Qt.LeftButton) assert not tabGeneral.showFullPath.isChecked() - # Check Browse button - monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: "") - assert not tabGeneral._backupFolder() - monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: "some/dir") - qtbot.mouseClick(tabGeneral.backupGetPath, Qt.LeftButton) + qtbot.wait(keyDelay) + assert not tabGeneral.hideVScroll.isChecked() + qtbot.mouseClick(tabGeneral.hideVScroll, Qt.LeftButton) + assert tabGeneral.hideVScroll.isChecked() + + qtbot.wait(keyDelay) + assert not tabGeneral.hideHScroll.isChecked() + qtbot.mouseClick(tabGeneral.hideHScroll, Qt.LeftButton) + assert tabGeneral.hideHScroll.isChecked() # Check font button monkeypatch.setattr(QFontDialog, "getFont", lambda font, obj: (font, True)) qtbot.mouseClick(tabGeneral.fontButton, Qt.LeftButton) qtbot.wait(keyDelay) - assert not tabGeneral.backupOnClose.isChecked() - qtbot.mouseClick(tabGeneral.backupOnClose, Qt.LeftButton) - assert tabGeneral.backupOnClose.isChecked() + tabGeneral.guiFontSize.setValue(12) + + # Projects Settings + qtbot.wait(keyDelay) + tabProjects = nwPrefs.tabProjects + nwPrefs._tabBox.setCurrentWidget(tabProjects) + tabProjects.backupPath = "no/where" qtbot.wait(keyDelay) - tabGeneral.guiFontSize.setValue(12) - tabGeneral.autoSaveDoc.setValue(20) - tabGeneral.autoSaveProj.setValue(40) + assert not tabProjects.backupOnClose.isChecked() + qtbot.mouseClick(tabProjects.backupOnClose, Qt.LeftButton) + assert tabProjects.backupOnClose.isChecked() - # Text Layour Settings + # Check Browse button + monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: "") + assert not tabProjects._backupFolder() + monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: "some/dir") + qtbot.mouseClick(tabProjects.backupGetPath, Qt.LeftButton) + + qtbot.wait(keyDelay) + tabProjects.autoSaveDoc.setValue(20) + tabProjects.autoSaveProj.setValue(40) + + # Text Layout Settings qtbot.wait(keyDelay) tabLayout = nwPrefs.tabLayout nwPrefs._tabBox.setCurrentWidget(tabLayout) @@ -1127,6 +1121,16 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC qtbot.mouseClick(tabLayout.textJustify, Qt.LeftButton) assert not tabLayout.textJustify.isChecked() + qtbot.wait(keyDelay) + assert tabLayout.scrollPastEnd.isChecked() + qtbot.mouseClick(tabLayout.scrollPastEnd, Qt.LeftButton) + assert not tabLayout.scrollPastEnd.isChecked() + + qtbot.wait(keyDelay) + assert not tabLayout.scollWithCursor.isChecked() + qtbot.mouseClick(tabLayout.scollWithCursor, Qt.LeftButton) + assert tabLayout.scollWithCursor.isChecked() + # Editor Settings qtbot.wait(keyDelay) tabEditing = nwPrefs.tabEditing @@ -1198,7 +1202,7 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC ignoreLines = [ 2, # Timestamp 11, 12, 13, 14, 15, 16, 17, # Window sizes - 7, 25, # Fonts (depends on system default) + 7, 27, # Fonts (depends on system default) ] assert cmpFiles(testConf, refConf, ignoreLines)