diff --git a/.travis.yml b/.travis.yml index 89ca3727..873ded23 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ install: - pip install --upgrade pip - pip install -r requirements.txt # - pip install pytest-faulthandler - - pip install PyVirtualDisplay==0.2.5 + - pip install PyVirtualDisplay - pip install pytest-xvfb - pip install pytest-cov - pip install pytest-qt diff --git a/nw/assets/icons/fallback/search_cancel-dark.svg b/nw/assets/icons/fallback/search_cancel-dark.svg new file mode 100644 index 00000000..5a0b2182 --- /dev/null +++ b/nw/assets/icons/fallback/search_cancel-dark.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/fallback/search_cancel.svg b/nw/assets/icons/fallback/search_cancel.svg new file mode 100644 index 00000000..48630300 --- /dev/null +++ b/nw/assets/icons/fallback/search_cancel.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/fallback/search_case-dark.svg b/nw/assets/icons/fallback/search_case-dark.svg new file mode 100644 index 00000000..4701dfe1 --- /dev/null +++ b/nw/assets/icons/fallback/search_case-dark.svg @@ -0,0 +1,49 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/search_case.svg b/nw/assets/icons/fallback/search_case.svg new file mode 100644 index 00000000..ce7e7673 --- /dev/null +++ b/nw/assets/icons/fallback/search_case.svg @@ -0,0 +1,48 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/search_loop-dark.svg b/nw/assets/icons/fallback/search_loop-dark.svg new file mode 100644 index 00000000..60baf3c2 --- /dev/null +++ b/nw/assets/icons/fallback/search_loop-dark.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/fallback/search_loop.svg b/nw/assets/icons/fallback/search_loop.svg new file mode 100644 index 00000000..4d6d85c5 --- /dev/null +++ b/nw/assets/icons/fallback/search_loop.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/fallback/search_preserve-dark.svg b/nw/assets/icons/fallback/search_preserve-dark.svg new file mode 100644 index 00000000..e59a8616 --- /dev/null +++ b/nw/assets/icons/fallback/search_preserve-dark.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + image/svg+xml + + + + + + diff --git a/nw/assets/icons/fallback/search_preserve.svg b/nw/assets/icons/fallback/search_preserve.svg new file mode 100644 index 00000000..bcfd6082 --- /dev/null +++ b/nw/assets/icons/fallback/search_preserve.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + image/svg+xml + + + + + + diff --git a/nw/assets/icons/fallback/search_project-dark.svg b/nw/assets/icons/fallback/search_project-dark.svg new file mode 100644 index 00000000..1e57b60e --- /dev/null +++ b/nw/assets/icons/fallback/search_project-dark.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/fallback/search_project.svg b/nw/assets/icons/fallback/search_project.svg new file mode 100644 index 00000000..12afc290 --- /dev/null +++ b/nw/assets/icons/fallback/search_project.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/fallback/search_regex-dark.svg b/nw/assets/icons/fallback/search_regex-dark.svg new file mode 100644 index 00000000..1a81cfe8 --- /dev/null +++ b/nw/assets/icons/fallback/search_regex-dark.svg @@ -0,0 +1,49 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/search_regex.svg b/nw/assets/icons/fallback/search_regex.svg new file mode 100644 index 00000000..2bbe1c22 --- /dev/null +++ b/nw/assets/icons/fallback/search_regex.svg @@ -0,0 +1,48 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/search_word-dark.svg b/nw/assets/icons/fallback/search_word-dark.svg new file mode 100644 index 00000000..650c7dfc --- /dev/null +++ b/nw/assets/icons/fallback/search_word-dark.svg @@ -0,0 +1,45 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/search_word.svg b/nw/assets/icons/fallback/search_word.svg new file mode 100644 index 00000000..8f0aed52 --- /dev/null +++ b/nw/assets/icons/fallback/search_word.svg @@ -0,0 +1,45 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/nw/common.py b/nw/common.py index bd535a71..0019c38a 100644 --- a/nw/common.py +++ b/nw/common.py @@ -172,3 +172,24 @@ def splitVersionNumber(vString): vInt = vMajor*10000 + vMinor*100 + vPatch return [vMajor, vMinor, vPatch, vInt] + +def transferCase(theSource, theTarget): + """Transfers the case of the source word to the target word. This + will consider all upper or lower, and first char capitalisation. + """ + theResult = theTarget + + if not isinstance(theSource, str) or not isinstance(theTarget, str): + return theResult + if len(theTarget) < 1 or len(theSource) < 1: + return theResult + + if theSource[0] == theSource[0].upper(): + theResult = theTarget[0].upper() + theTarget[1:] + + if theSource == theSource.upper(): + theResult = theTarget.upper() + elif theSource == theSource.lower(): + theResult = theTarget.lower() + + return theResult diff --git a/nw/config.py b/nw/config.py index 4f690be3..09b3d363 100644 --- a/nw/config.py +++ b/nw/config.py @@ -133,6 +133,13 @@ class Config: self.spellTool = None self.spellLanguage = None + self.searchCase = False + self.searchWord = False + self.searchRegEx = False + self.searchLoop = False + self.searchNextFile = False + self.searchMatchCap = False + ## Backup self.backupPath = "" self.backupOnClose = False @@ -482,6 +489,24 @@ class Config: self.viewSynopsis = self._parseLine( cnfParse, cnfSec, "viewsynopsis", self.CNF_BOOL, self.viewSynopsis ) + self.searchCase = self._parseLine( + cnfParse, cnfSec, "searchcase", self.CNF_BOOL, self.searchCase + ) + self.searchWord = self._parseLine( + cnfParse, cnfSec, "searchword", self.CNF_BOOL, self.searchWord + ) + self.searchRegEx = self._parseLine( + cnfParse, cnfSec, "searchregex", self.CNF_BOOL, self.searchRegEx + ) + self.searchLoop = self._parseLine( + cnfParse, cnfSec, "searchloop", self.CNF_BOOL, self.searchLoop + ) + self.searchNextFile = self._parseLine( + cnfParse, cnfSec, "searchnextfile", self.CNF_BOOL, self.searchNextFile + ) + self.searchMatchCap = self._parseLine( + cnfParse, cnfSec, "searchmatchcap", self.CNF_BOOL, self.searchMatchCap + ) ## Path cnfSec = "Path" @@ -571,9 +596,15 @@ class Config: ## State cnfSec = "State" cnfParse.add_section(cnfSec) - cnfParse.set(cnfSec,"showrefpanel",str(self.showRefPanel)) - cnfParse.set(cnfSec,"viewcomments",str(self.viewComments)) - cnfParse.set(cnfSec,"viewsynopsis",str(self.viewSynopsis)) + cnfParse.set(cnfSec,"showrefpanel", str(self.showRefPanel)) + cnfParse.set(cnfSec,"viewcomments", str(self.viewComments)) + cnfParse.set(cnfSec,"viewsynopsis", str(self.viewSynopsis)) + cnfParse.set(cnfSec,"searchcase", str(self.searchCase)) + cnfParse.set(cnfSec,"searchword", str(self.searchWord)) + cnfParse.set(cnfSec,"searchregex", str(self.searchRegEx)) + cnfParse.set(cnfSec,"searchloop", str(self.searchLoop)) + cnfParse.set(cnfSec,"searchnextfile", str(self.searchNextFile)) + cnfParse.set(cnfSec,"searchmatchcap", str(self.searchMatchCap)) ## Path cnfSec = "Path" diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index 820a2a48..4e96b4ee 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -2,7 +2,6 @@ from nw.gui.about import GuiAbout from nw.gui.build import GuiBuildNovel -from nw.gui.docbars import GuiSearchBar from nw.gui.doceditor import GuiDocEditor from nw.gui.docmerge import GuiDocMerge from nw.gui.docsplit import GuiDocSplit @@ -23,7 +22,6 @@ from nw.gui.theme import GuiIcons, GuiTheme __all__ = [ "GuiAbout", "GuiBuildNovel", - "GuiSearchBar", "GuiDocEditor", "GuiDocMerge", "GuiDocSplit", diff --git a/nw/gui/build.py b/nw/gui/build.py index 70ea0703..51a9dd68 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -459,7 +459,7 @@ class GuiBuildNovel(QDialog): makeHtml.setStyles(not noStyling) # Make sure the tree order is correct - self.theParent.treeView.saveTreeOrder() + self.theParent.treeView.flushTreeOrder() self.buildProgress.setMaximum(len(self.theProject.projTree)) self.buildProgress.setValue(0) diff --git a/nw/gui/docbars.py b/nw/gui/docbars.py deleted file mode 100644 index 079f989a..00000000 --- a/nw/gui/docbars.py +++ /dev/null @@ -1,168 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter GUI Main Window SearchBar - - novelWriter – GUI Main Window SearchBar -========================================= - Class holding the main window search bar - - File History: - Created: 2019-09-29 [0.2.1] GuiSearchBar - - 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 PyQt5.QtCore import Qt, QSize -from PyQt5.QtGui import QPalette, QColor, QIcon -from PyQt5.QtWidgets import ( - qApp, QWidget, QFrame, QGridLayout, QLabel, QLineEdit, QPushButton, - QHBoxLayout, QToolButton, QScrollArea -) - -from nw.constants import nwDocAction, nwUnicode - -logger = logging.getLogger(__name__) - -class GuiSearchBar(QWidget): - - def __init__(self, theParent): - QWidget.__init__(self, theParent) - - logger.debug("Initialising GuiSearchBar ...") - - self.mainConf = nw.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.repVisible = False - - self.setContentsMargins(0, 0, 0, 0) - - self.mainBox = QGridLayout(self) - self.setLayout(self.mainBox) - - self.searchBox = QLineEdit() - self.replaceBox = QLineEdit() - self.searchLabel = QLabel("Search") - self.replaceLabel = QLabel("Replace") - self.closeButton = QPushButton(self.theTheme.getIcon("close"),"") - self.searchButton = QPushButton(self.theTheme.getIcon("search"),"") - self.replaceButton = QPushButton(self.theTheme.getIcon("search-replace"),"") - - self.closeButton.clicked.connect(self._doClose) - self.searchButton.clicked.connect(self._doSearch) - self.replaceButton.clicked.connect(self._doReplace) - self.searchBox.returnPressed.connect(self._doSearch) - self.replaceBox.returnPressed.connect(self._doSearch) - - self.mainBox.addWidget(QLabel(""), 0, 0) - self.mainBox.addWidget(self.searchLabel, 0, 1) - self.mainBox.addWidget(self.searchBox, 0, 2) - self.mainBox.addWidget(self.searchButton, 0, 3) - self.mainBox.addWidget(self.closeButton, 0, 4) - self.mainBox.addWidget(self.replaceLabel, 1, 1) - self.mainBox.addWidget(self.replaceBox, 1, 2) - self.mainBox.addWidget(self.replaceButton, 1, 3) - - self.mainBox.setColumnStretch(0, 1) - self.mainBox.setColumnStretch(1, 0) - self.mainBox.setColumnStretch(2, 0) - self.mainBox.setColumnStretch(3, 0) - self.mainBox.setColumnStretch(4, 0) - self.mainBox.setContentsMargins(0, 0, 0, 0) - - boxWidth = 16*self.theTheme.textNWidth - self.searchBox.setMinimumWidth(boxWidth) - self.replaceBox.setMinimumWidth(boxWidth) - - self._replaceVisible(False) - - logger.debug("GuiSearchBar initialisation complete") - - return - - ## - # Get and Set Functions - ## - - def setSearchText(self, theText): - """Open the search bar and set the search text to the text - provided, if any. - """ - if not self.isVisible(): - self.setVisible(True) - self.searchBox.setText(theText) - self.searchBox.setFocus() - logger.verbose("Setting search text to '%s'" % theText) - return True - - def setReplaceText(self, theText): - """Set the replace text. - """ - self._replaceVisible(True) - self.replaceBox.setFocus() - self.replaceBox.setText(theText) - return True - - def getSearchText(self): - """Return the current search text. - """ - return self.searchBox.text() - - def getReplaceText(self): - """Return the current replace text. - """ - return self.replaceBox.text() - - ## - # Internal Functions - ## - - def _doClose(self): - """Hide the search/replace bar. - """ - self._replaceVisible(False) - self.setVisible(False) - return - - def _doSearch(self): - """Call the search action function for the document editor. - """ - modKey = qApp.keyboardModifiers() - if modKey == Qt.ShiftModifier: - self.theParent.docEditor.docAction(nwDocAction.GO_PREV) - else: - self.theParent.docEditor.docAction(nwDocAction.GO_NEXT) - return - - def _doReplace(self): - """Call the replace action function for the document editor. - """ - self.theParent.docEditor.docAction(nwDocAction.REPL_NEXT) - return - - def _replaceVisible(self, isVisible): - """Set the visibility of all the replace widgets. - """ - self.replaceLabel.setVisible(isVisible) - self.replaceBox.setVisible(isVisible) - self.replaceButton.setVisible(isVisible) - self.repVisible = isVisible - return True - -# END Class GuiSearchBar diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 25ae5cbb..8bfd51c0 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -6,9 +6,11 @@ Class holding the document editor File History: - Created: 2018-09-29 [0.0.1] GuiDocEditor - Created: 2019-04-22 [0.0.1] WordCounter - Created: 2020-04-25 [0.4.5] GuiDocEditHeader + Created: 2018-09-29 [0.0.1] GuiDocEditor + Created: 2019-04-22 [0.0.1] BackgroundWordCounter + Created: 2019-09-29 [0.2.1] GuiDocEditSearch + Created: 2020-04-25 [0.4.5] GuiDocEditHeader + Rewritten: 2020-06-15 [0.9.0] GuiDocEditSearch This file is a part of novelWriter Copyright 2020, Veronica Berglyd Olsen @@ -32,20 +34,22 @@ import nw from time import time -from PyQt5.QtCore import Qt, QSize, QThread, QTimer, pyqtSlot +from PyQt5.QtCore import Qt, QSize, QThread, QTimer, pyqtSlot, QRegExp from PyQt5.QtGui import ( QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette, QIcon, QTextDocument, QCursor ) from PyQt5.QtWidgets import ( qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QWidget, QLabel, - QToolButton, QHBoxLayout + QToolBar, QToolButton, QHBoxLayout, QGridLayout, QLineEdit, QPushButton, + QFrame ) from nw.core import NWDoc from nw.gui.dochighlight import GuiDocHighlighter from nw.core import NWSpellSimple, countWords from nw.constants import nwUnicode, nwDocAction, nwItemClass +from nw.common import transferCase logger = logging.getLogger(__name__) @@ -88,6 +92,7 @@ class GuiDocEditor(QTextEdit): # Document Title self.docHeader = GuiDocEditHeader(self) self.docFooter = GuiDocEditFooter(self) + self.docSearch = GuiDocEditSearch(self) # Syntax self.hLight = GuiDocHighlighter(self.qDocument, self.theParent) @@ -99,6 +104,7 @@ class GuiDocEditor(QTextEdit): # Editor State self.hasSelection = False self.setMinimumWidth(self.mainConf.pxInt(300)) + self.setAutoFillBackground(True) self.setAcceptRichText(False) # Custom Shortcuts @@ -127,7 +133,7 @@ class GuiDocEditor(QTextEdit): self.wcTimer.setInterval(int(self.wcInterval*1000)) self.wcTimer.timeout.connect(self._runCounter) - self.wCounter = WordCounter(self) + self.wCounter = BackgroundWordCounter(self) self.wCounter.finished.connect(self._updateCounts) self.initEditor() @@ -183,12 +189,15 @@ class GuiDocEditor(QTextEdit): 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) # Set default text margins - self.qDocument.setDocumentMargin(self.mainConf.getTextMargin()) + cM = self.mainConf.getTextMargin() + self.qDocument.setDocumentMargin(0) + self.setViewportMargins(cM, cM, cM, cM) # Also set the document text options for the document text flow theOpt = QTextOption() @@ -311,6 +320,7 @@ class GuiDocEditor(QTextEdit): Config.textFixedW is enabled or we're in Zen mode. Otherwise, just ensure the margins are set correctly. """ + wW = self.width() cM = self.mainConf.getTextMargin() vBar = self.verticalScrollBar() @@ -324,42 +334,29 @@ class GuiDocEditor(QTextEdit): tW = self.mainConf.getZenWidth() else: tW = self.mainConf.getTextWidth() - wW = self.width() - tM = int((wW - sW - tW)/2) + tM = (wW - sW - tW)//2 if tM < cM: tM = cM else: tM = cM tB = self.frameWidth() - tW = self.width() - 2*tB - sW + tW = wW - 2*tB - sW 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.setLeftMargin(tM) - docFormat.setRightMargin(tM) - docFormat.setTopMargin(max(0, tT)) - docFormat.setBottomMargin(max(0, bT)) + if self.docSearch.isVisible(): + rH = self.docSearch.height() + rW = self.docSearch.width() + rL = wW - sW - rW - 2*tB + self.docSearch.move(rL, 2*tB) + else: + rH = 0 - # Updating root frame triggers a QTextDocument->contentsChange - # signal, which we do not want as it re-runs the syntax - # highlighter and spell checker, so we block it briefly. - # We then emit a signal that does not trigger re-highlighting. - self.qDocument.blockSignals(True) - self.qDocument.rootFrame().setFrameFormat(docFormat) - self.qDocument.blockSignals(False) - - # The line below causes issues with large documents as it - # triggers an early repaint that seems to only render a part of - # the document. Leaving it here as a warning for now. - # self.qDocument.contentsChange.emit(0, 0, 0) + self.setViewportMargins(tM, max(cM, tH, rH), tM, max(cM, fH)) return @@ -536,7 +533,7 @@ class GuiDocEditor(QTextEdit): elif theAction == nwDocAction.GO_NEXT: self._findNext() elif theAction == nwDocAction.GO_PREV: - self._findPrev() + self._findNext(isBackward=True) elif theAction == nwDocAction.REPL_NEXT: self._replaceNext() elif theAction == nwDocAction.BLOCK_H1: @@ -577,6 +574,12 @@ class GuiDocEditor(QTextEdit): )) return + def closeSearch(self): + """Close the search box. + """ + self.docSearch.closeSearch() + return self.docSearch.isVisible() + ## # Document Events and Maintenance ## @@ -594,6 +597,9 @@ class GuiDocEditor(QTextEdit): as it is triggered on every keypress when typing. """ self.hasSelection = self.textCursor().hasSelection() + if self.docSearch.searchBox.hasFocus(): + # Block the event when the focus is on the search bar. + return if keyEvent.modifiers() == Qt.ShiftModifier: theKey = keyEvent.key() @@ -1135,7 +1141,8 @@ class GuiDocEditor(QTextEdit): selText = theCursor.selectedText() else: selText = "" - self.theParent.searchBar.setSearchText(selText) + self.docSearch.setSearchText(selText) + self.updateDocMargins() return def _beginReplace(self): @@ -1143,54 +1150,82 @@ class GuiDocEditor(QTextEdit): text. """ self._beginSearch() - self.theParent.searchBar.setReplaceText("") + self.docSearch.setReplaceText("") return - def _findNext(self): - """Searches for the next occurrence of the search bar text in - the document. Wraps back to the top if not found. + def _findNext(self, isBackward=False): + """Searches for the next or previous occurrence of the search + bar text in the document. Wraps around if not found and loop is + enabled, or continues to next file if next file is enabled. """ - searchFor = self.theParent.searchBar.getSearchText() - wasFound = self.find(searchFor) - if not wasFound: - theCursor = self.textCursor() - theCursor.movePosition(QTextCursor.Start) - self.setTextCursor(theCursor) - return + if not self.docSearch.isVisible(): + self._beginSearch() + return - def _findPrev(self): - """Searches for the previous occurrence of the search bar text - in the document. Wraps back to the end if not found. - """ - searchFor = self.theParent.searchBar.getSearchText() - wasFound = self.find(searchFor, QTextDocument.FindBackward) + findOpt = QTextDocument.FindFlag(0) + if isBackward: + findOpt |= QTextDocument.FindBackward + if self.docSearch.isCaseSense: + findOpt |= QTextDocument.FindCaseSensitively + if self.docSearch.isWholeWord: + findOpt |= QTextDocument.FindWholeWords + + searchFor = self.docSearch.getSearchText() + wasFound = self.find(searchFor, findOpt) if not wasFound: - theCursor = self.textCursor() - theCursor.movePosition(QTextCursor.End) - self.setTextCursor(theCursor) + if self.docSearch.doNextFile and not isBackward: + self.theParent.openNextDocument( + self.theHandle, wrapAround=self.docSearch.doLoop + ) + elif self.docSearch.doLoop: + theCursor = self.textCursor() + theCursor.movePosition( + QTextCursor.End if isBackward else QTextCursor.Start + ) + self.setTextCursor(theCursor) + self.find(searchFor, findOpt) + return def _replaceNext(self): """Searches for the next occurrence of the search bar text in - the document and replaces it with the replace text. Wraps back - to the top if not found. + the document and replaces it with the replace text. Calls search + next automatically when done. """ + if not self.docSearch.isVisible(): + self._beginSearch() + return + + if not theCursor.hasSelection(): + return + theCursor = self.textCursor() - searchFor = self.theParent.searchBar.getSearchText() - replWith = self.theParent.searchBar.getReplaceText() - if theCursor.hasSelection() and theCursor.selectedText() == searchFor: - xPos = theCursor.selectionStart() + searchFor = self.docSearch.getSearchText() + replWith = self.docSearch.getReplaceText() + selText = theCursor.selectedText() + + if self.docSearch.doMatchCap: + replWith = transferCase(selText, replWith) + + if not self.docSearch.isCaseSense: + isMatch = searchFor.lower() == selText.lower() + else: + isMatch = searchFor == selText + + if isMatch: theCursor.beginEditBlock() theCursor.removeSelectedText() theCursor.insertText(replWith) theCursor.endEditBlock() - theCursor.setPosition(xPos) + theCursor.setPosition(theCursor.selectionEnd()) self.setTextCursor(theCursor) logger.verbose("Replaced occurrence of '%s' with '%s' on line %d" % ( searchFor, replWith, theCursor.blockNumber() )) - if searchFor != "": + + if searchFor: self._findNext() + return def _setupSpellChecking(self): @@ -1209,11 +1244,16 @@ class GuiDocEditor(QTextEdit): # END Class GuiDocEditor -class WordCounter(QThread): +# =============================================================================================== # +# The Off GUI Thread Word Counter +# Runs the word counter in the background for the DocEditor +# =============================================================================================== # - def __init__(self, theParent): - QThread.__init__(self, theParent) - self.theParent = theParent +class BackgroundWordCounter(QThread): + + def __init__(self, docEditor): + QThread.__init__(self, docEditor) + self.docEditor = docEditor self.charCount = 0 self.wordCount = 0 self.paraCount = 0 @@ -1223,16 +1263,357 @@ class WordCounter(QThread): """Overloaded run function for the word counter, forwarding the call to the function that does the actual counting. """ - theText = self.theParent.getText() + theText = self.docEditor.getText() cC, wC, pC = countWords(theText) - self.charCount = cC self.wordCount = wC self.paraCount = pC + return + +## END Class BackgroundWordCounter + +# =============================================================================================== # +# The Embedded Document Search/Replace Feature +# Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport +# =============================================================================================== # + +class GuiDocEditSearch(QFrame): + + def __init__(self, docEditor): + QFrame.__init__(self, docEditor) + + logger.debug("Initialising GuiDocEditSearch ...") + + self.mainConf = nw.CONFIG + self.docEditor = docEditor + self.theParent = docEditor.theParent + self.theProject = docEditor.theProject + self.theTheme = docEditor.theTheme + + self.repVisible = False + self.isCaseSense = self.mainConf.searchCase + self.isWholeWord = self.mainConf.searchWord + self.isRegEx = self.mainConf.searchRegEx + self.doLoop = self.mainConf.searchLoop + self.doNextFile = self.mainConf.searchNextFile + self.doMatchCap = self.mainConf.searchMatchCap + + mPx = self.mainConf.pxInt(6) + fPx = int(0.9*self.theTheme.fontPixelSize) + tPx = int(0.8*self.theTheme.fontPixelSize) + boxFont = self.theTheme.guiFont + boxFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + + self.setContentsMargins(0, 0, 0, 0) + self.setAutoFillBackground(True) + self.setFrameStyle(QFrame.StyledPanel | QFrame.Plain) + + self.mainBox = QGridLayout(self) + self.setLayout(self.mainBox) + + # Text Boxes + # ========== + self.searchBox = QLineEdit() + self.searchBox.setFont(boxFont) + self.searchBox.setPlaceholderText("Search") + self.searchBox.returnPressed.connect(self._doSearch) + + self.replaceBox = QLineEdit() + self.replaceBox.setFont(boxFont) + self.replaceBox.setPlaceholderText("Replace") + self.replaceBox.returnPressed.connect(self._doSearch) + + self.searchOpt = QToolBar(self) + self.searchOpt.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.searchOpt.setIconSize(QSize(tPx, tPx)) + self.searchOpt.setContentsMargins(0, 0, 0, 0) + self.searchOpt.setStyleSheet(r"QToolBar {padding: 0;}") + + self.searchLabel = QLabel("Search") + self.searchLabel.setFont(boxFont) + self.searchLabel.setIndent(self.mainConf.pxInt(6)) + + self.toggleCase = QAction("Case Sensitive", self) + self.toggleCase.setToolTip("Match case") + self.toggleCase.setIcon(self.theTheme.getIcon("search_case")) + self.toggleCase.setCheckable(True) + self.toggleCase.setChecked(self.isCaseSense) + self.toggleCase.toggled.connect(self._doToggleCase) + self.searchOpt.addAction(self.toggleCase) + + self.toggleWord = QAction("Whole Words Only", self) + self.toggleWord.setToolTip("Match whole words") + self.toggleWord.setIcon(self.theTheme.getIcon("search_word")) + self.toggleWord.setCheckable(True) + self.toggleWord.setChecked(self.isWholeWord) + self.toggleWord.toggled.connect(self._doToggleWord) + self.searchOpt.addAction(self.toggleWord) + + self.toggleRegEx = QAction("RegEx Mode", self) + self.toggleRegEx.setToolTip("Use regular expressions") + self.toggleRegEx.setIcon(self.theTheme.getIcon("search_regex")) + self.toggleRegEx.setCheckable(True) + self.toggleRegEx.setChecked(self.isRegEx) + self.toggleRegEx.toggled.connect(self._doToggleRegEx) + self.searchOpt.addAction(self.toggleRegEx) + + self.toggleLoop = QAction("Loop Search", self) + self.toggleLoop.setToolTip("Loop the search when reaching the end") + self.toggleLoop.setIcon(self.theTheme.getIcon("search_loop")) + self.toggleLoop.setCheckable(True) + self.toggleLoop.setChecked(self.doLoop) + self.toggleLoop.toggled.connect(self._doToggleLoop) + self.searchOpt.addAction(self.toggleLoop) + + self.toggleProject = QAction("Search Next File", self) + self.toggleProject.setToolTip("Continue searching in the next file") + self.toggleProject.setIcon(self.theTheme.getIcon("search_project")) + self.toggleProject.setCheckable(True) + self.toggleProject.setChecked(self.doNextFile) + self.toggleProject.toggled.connect(self._doToggleProject) + self.searchOpt.addAction(self.toggleProject) + + self.searchOpt.addSeparator() + + self.toggleMatchCap = QAction("Preserve Case", self) + self.toggleMatchCap.setToolTip("Preserve case on replace") + self.toggleMatchCap.setIcon(self.theTheme.getIcon("search_preserve")) + self.toggleMatchCap.setCheckable(True) + self.toggleMatchCap.setChecked(self.doMatchCap) + self.toggleMatchCap.toggled.connect(self._doToggleMatchCap) + self.searchOpt.addAction(self.toggleMatchCap) + + self.searchOpt.addSeparator() + + self.cancelSearch = QAction("Close Search", self) + self.cancelSearch.setToolTip("Close the search box [Esc]") + self.cancelSearch.setIcon(self.theTheme.getIcon("search_cancel")) + self.cancelSearch.triggered.connect(self._doClose) + self.searchOpt.addAction(self.cancelSearch) + + # Buttons + # ======= + bPx = self.searchBox.sizeHint().height() + + self.showReplace = QToolButton(self) + self.showReplace.setArrowType(Qt.RightArrow) + self.showReplace.setCheckable(True) + self.showReplace.setStyleSheet(r"QToolButton {border: none; background: transparent;}") + self.showReplace.toggled.connect(self._doToggleReplace) + + self.searchButton = QPushButton(self.theTheme.getIcon("search"),"") + self.searchButton.setFixedSize(QSize(bPx, bPx)) + self.searchButton.setToolTip("Find in current document") + self.searchButton.clicked.connect(self._doSearch) + + self.replaceButton = QPushButton(self.theTheme.getIcon("search-replace"),"") + self.replaceButton.setFixedSize(QSize(bPx, bPx)) + self.replaceButton.setToolTip("Find and replace in current document") + self.replaceButton.clicked.connect(self._doReplace) + + self.mainBox.addWidget(self.searchLabel, 0, 0, 1, 2, Qt.AlignLeft) + self.mainBox.addWidget(self.searchOpt, 0, 2, 1, 2, Qt.AlignRight) + self.mainBox.addWidget(self.showReplace, 1, 0, 1, 1) + self.mainBox.addWidget(self.searchBox, 1, 1, 1, 2) + self.mainBox.addWidget(self.searchButton, 1, 3, 1, 1) + self.mainBox.addWidget(self.replaceBox, 2, 1, 1, 2) + self.mainBox.addWidget(self.replaceButton, 2, 3, 1, 1) + + self.mainBox.setColumnStretch(0, 1) + self.mainBox.setColumnStretch(1, 0) + self.mainBox.setColumnStretch(2, 0) + self.mainBox.setColumnStretch(3, 0) + self.mainBox.setColumnStretch(4, 0) + self.mainBox.setSpacing(self.mainConf.pxInt(2)) + self.mainBox.setContentsMargins(mPx, mPx, mPx, mPx) + + boxWidth = self.mainConf.pxInt(200) + self.searchBox.setFixedWidth(boxWidth) + self.replaceBox.setFixedWidth(boxWidth) + self.replaceBox.setVisible(False) + self.replaceButton.setVisible(False) + self.adjustSize() + + # Construct Box Colours + qPalette = self.searchBox.palette() + baseCol = qPalette.base().color() + rCol = baseCol.redF() + 0.1 + gCol = baseCol.greenF() - 0.1 + bCol = baseCol.blueF() - 0.1 + + mCol = max(rCol, gCol, bCol, 1.0) + errCol = QColor() + errCol.setRedF(rCol/mCol) + errCol.setGreenF(gCol/mCol) + errCol.setBlueF(bCol/mCol) + + self.rxCol = { + True : baseCol, + False : errCol + } + + logger.debug("GuiDocEditSearch initialisation complete") return -## END Class WordCounter + def closeSearch(self): + """Close the search box. + """ + self.mainConf.searchCase = self.isCaseSense + self.mainConf.searchWord = self.isWholeWord + self.mainConf.searchRegEx = self.isRegEx + self.mainConf.searchLoop = self.doLoop + self.mainConf.searchNextFile = self.doNextFile + self.mainConf.searchMatchCap = self.doMatchCap + + self.showReplace.setChecked(False) + self.setVisible(False) + self.docEditor.updateDocMargins() + self.docEditor.setFocus() + + return + + ## + # Get and Set Functions + ## + + def setSearchText(self, theText): + """Open the search bar and set the search text to the text + provided, if any. + """ + if not self.isVisible(): + self.setVisible(True) + self.searchBox.setText(theText) + self.searchBox.setFocus() + if self.isRegEx: + self._alertSearchValid(True) + logger.verbose("Setting search text to '%s'" % theText) + return True + + def setReplaceText(self, theText): + """Set the replace text. + """ + self.showReplace.setChecked(True) + self.replaceBox.setFocus() + self.replaceBox.setText(theText) + return True + + def getSearchText(self): + """Return the current search text either as text or as a regular + expression object. + """ + theText = self.searchBox.text() + if self.isRegEx and self.mainConf.verQtValue >= 50300: + if self.isCaseSense: + rxCase = Qt.CaseSensitive + else: + rxCase = Qt.CaseInsensitive + theRegEx = QRegExp(theText, rxCase) + self._alertSearchValid(theRegEx.isValid()) + return theRegEx + + return theText + + def getReplaceText(self): + """Return the current replace text. + """ + return self.replaceBox.text() + + ## + # Slots + ## + + def _doClose(self): + """Hide the search/replace bar. + """ + self.closeSearch() + return + + def _doSearch(self): + """Call the search action function for the document editor. + """ + modKey = qApp.keyboardModifiers() + if modKey == Qt.ShiftModifier: + self.docEditor.docAction(nwDocAction.GO_PREV) + else: + self.docEditor.docAction(nwDocAction.GO_NEXT) + return + + def _doReplace(self): + """Call the replace action function for the document editor. + """ + self.docEditor.docAction(nwDocAction.REPL_NEXT) + return + + def _doToggleReplace(self, theState): + """Toggle the show/hide of the + """ + if theState: + self.showReplace.setArrowType(Qt.DownArrow) + else: + self.showReplace.setArrowType(Qt.RightArrow) + self.replaceBox.setVisible(theState) + self.replaceButton.setVisible(theState) + self.repVisible = theState + self.adjustSize() + self.docEditor.updateDocMargins() + return + + def _doToggleCase(self, theState): + """Enable/disable case sensitive mode. + """ + self.isCaseSense = theState + return + + def _doToggleWord(self, theState): + """Enable/disable whole word search mode. + """ + self.isWholeWord = theState + return + + def _doToggleRegEx(self, theState): + """Enable/disable regular expression search mode. + """ + self.isRegEx = theState + return + + def _doToggleLoop(self, theState): + """Enable/disable looping the search. + """ + self.doLoop = theState + return + + def _doToggleProject(self, theState): + """Enable/disable continuing search in next project file. + """ + self.doNextFile = theState + return + + def _doToggleMatchCap(self, theState): + """Enable/disable preserving capitalisation when replacing. + """ + self.doMatchCap = theState + return + + ## + # Internal Functions + ## + + def _alertSearchValid(self, isValid): + """Highlight the search box to indicate the search string is or + isn't valid. Take the colour from the replace box. + """ + qPalette = self.replaceBox.palette() + qPalette.setColor(QPalette.Base, self.rxCol[isValid]) + self.searchBox.setPalette(qPalette) + return + +# END Class GuiDocEditSearch + +# =============================================================================================== # +# The Embedded Document Header +# Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport +# =============================================================================================== # # =============================================================================================== # # The Embedded Document Header diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 18a7ab10..8c2ea236 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -559,16 +559,16 @@ class GuiDocViewHeader(QWidget): class GuiDocViewFooter(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, docViewer): + QWidget.__init__(self, docViewer) logger.debug("Initialising GuiDocViewFooter ...") self.mainConf = nw.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.optState = theParent.theProject.optState - self.viewMeta = theParent.theParent.viewMeta + self.docViewer = docViewer + self.theParent = docViewer.theParent + self.theTheme = docViewer.theTheme + self.viewMeta = docViewer.theParent.viewMeta self.theHandle = None # Make a QPalette that matches the Syntax Theme @@ -670,9 +670,9 @@ class GuiDocViewFooter(QWidget): """Toggle the sticky flag for the reference panel. """ logger.verbose("Reference sticky is %s" % str(theState)) - self.theParent.stickyRef = theState - if not theState and self.theParent.theHandle is not None: - self.viewMeta.refreshReferences(self.theParent.theHandle) + self.docViewer.stickyRef = theState + if not theState and self.docViewer.theHandle is not None: + self.viewMeta.refreshReferences(self.docViewer.theHandle) return # END Class GuiDocViewFooter diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 02e850ad..236b9f90 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -60,8 +60,9 @@ class GuiProjectTree(QTreeWidget): self.theProject = theParent.theProject # Tree Settings - self.theMap = None - self.orphRoot = None + self.theMap = None + self.orphRoot = None + self.treeChanged = False self.ctxMenu = GuiProjectTreeMenu(self) self.clearTree() @@ -256,7 +257,7 @@ class GuiProjectTree(QTreeWidget): pItem.insertChild(nIndex, cItem) self.clearSelection() cItem.setSelected(True) - self.theProject.setProjectChanged(True) + self._setTreeChanged(True) else: return False return True @@ -276,6 +277,16 @@ class GuiProjectTree(QTreeWidget): self.theProject.setTreeOrder(theList) return True + def flushTreeOrder(self): + """Calls saveTreeOrder if there are unsaved changes, otherwise + does nothing. + """ + if self.treeChanged: + logger.verbose("Flushing project tree to project class") + self.saveTreeOrder() + self._setTreeChanged(False) + return + def getTreeFromHandle(self, tHandle): """Recursively return all the children items starting from a given item handle. @@ -332,6 +343,9 @@ class GuiProjectTree(QTreeWidget): continue self.deleteItem(tHandle, True) + if nTrash > 0: + self._setTreeChanged(True) + return True def deleteItem(self, tHandle=None, alreadyAsked=False, askForTrash=False): @@ -413,7 +427,7 @@ class GuiProjectTree(QTreeWidget): trItemT.addChild(trItemC) nwItemS.setParent(self.theProject.projTree.trashRoot()) - self.theProject.setProjectChanged(True) + self._setTreeChanged(True) self.theParent.theIndex.deleteHandle(tHandle) elif nwItemS.itemType == nwItemType.FOLDER: @@ -436,7 +450,7 @@ class GuiProjectTree(QTreeWidget): if trItemS.childCount() == 0: self.takeTopLevelItem(tIndex) self.theParent.mainMenu.setAvailableRoot() - self.theProject.setProjectChanged(True) + self._setTreeChanged(True) else: self.makeAlert("Cannot delete root folder. It is not empty.", nwAlert.ERROR) return False @@ -732,6 +746,8 @@ class GuiProjectTree(QTreeWidget): elif nwItem.itemType == nwItemType.TRASH: newItem.setIcon(self.C_NAME, self.theTheme.getIcon(nwLabels.CLASS_ICON[tClass])) + self._setTreeChanged(True) + return newItem def _addTrashRoot(self): @@ -747,6 +763,7 @@ class GuiProjectTree(QTreeWidget): self.theProject.projTree[trashHandle] ) trItem.setExpanded(True) + self._setTreeChanged(True) return trItem def _addOrphanedRoot(self): @@ -793,7 +810,7 @@ class GuiProjectTree(QTreeWidget): nwItemS.setParent(pHandle) self.propagateCount(tHandle, wC) self.setTreeItemValues(tHandle) - self.theProject.setProjectChanged(True) + self._setTreeChanged(True) logger.debug("The parent of item %s has been changed to %s" % (tHandle,pHandle)) @@ -815,7 +832,15 @@ class GuiProjectTree(QTreeWidget): pHandle = trItemP.data(self.C_NAME, Qt.UserRole) nwItemS.setParent(pHandle) self.setTreeItemValues(tHandle) - self.theProject.setProjectChanged(True) + self._setTreeChanged(True) + return + + def _setTreeChanged(self, theState): + """Set the tree change flag, and propagate to the project. + """ + self.treeChanged = theState + if theState: + self.theProject.setProjectChanged(True) return # END Class GuiProjectTree diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 30bcb621..7df0c560 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -495,52 +495,58 @@ class GuiIcons: ICON_MAP = { # Project and GUI icons - "cls_none" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_novel" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_plot" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_character" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_world" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_timeline" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_object" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_entity" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_custom" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_trash" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "proj_document" : (QStyle.SP_FileIcon, "x-office-document"), - "proj_folder" : (QStyle.SP_DirIcon, "folder"), - "proj_orphan" : (QStyle.SP_MessageBoxWarning, "dialog-warning"), - "proj_nwx" : (None, None), - "status_lang" : (None, None), - "status_time" : (None, None), - "status_stats" : (None, None), - "doc_h1" : (QStyle.SP_FileIcon, "x-office-document"), - "doc_h2" : (QStyle.SP_FileIcon, "x-office-document"), - "doc_h3" : (QStyle.SP_FileIcon, "x-office-document"), - "doc_h4" : (QStyle.SP_FileIcon, "x-office-document"), + "cls_none" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_novel" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_plot" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_character" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_world" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_timeline" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_object" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_entity" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_custom" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_trash" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "proj_document" : (QStyle.SP_FileIcon, "x-office-document"), + "proj_folder" : (QStyle.SP_DirIcon, "folder"), + "proj_orphan" : (QStyle.SP_MessageBoxWarning, "dialog-warning"), + "proj_nwx" : (None, None), + "status_lang" : (None, None), + "status_time" : (None, None), + "status_stats" : (None, None), + "doc_h1" : (QStyle.SP_FileIcon, "x-office-document"), + "doc_h2" : (QStyle.SP_FileIcon, "x-office-document"), + "doc_h3" : (QStyle.SP_FileIcon, "x-office-document"), + "doc_h4" : (QStyle.SP_FileIcon, "x-office-document"), + "search_case" : (None, None), + "search_regex" : (None, None), + "search_word" : (None, None), + "search_loop" : (None, None), + "search_project" : (None, None), + "search_cancel" : (None, None), + "search_preserve" : (None, None), ## General Button Icons "folder-open" : (QStyle.SP_DirOpenIcon, "folder-open"), "delete" : (QStyle.SP_DialogDiscardButton, "edit-delete"), - "add" : (None, "list-add"), - "remove" : (None, "list-remove"), "close" : (QStyle.SP_DialogCloseButton, "window-close"), "done" : (QStyle.SP_DialogApplyButton, None), - "search" : (None, "edit-find"), - "search-replace" : (None, "edit-find-replace"), "clear" : (QStyle.SP_LineEditClearButton, "clear_left"), "save" : (QStyle.SP_DialogSaveButton, "document-save"), - "edit" : (None, None), - "check" : (None, None), - "cross" : (None, None), - "hash" : (None, None), - "maximise" : (None, None), - "minimise" : (None, None), - "refresh" : (None, None), - "reference" : (None, None), - "sticky-on" : (None, None), - "sticky-off" : (None, None), + "add" : (None, "list-add"), + "remove" : (None, "list-remove"), + "search" : (None, "edit-find"), + "search-replace" : (None, "edit-find-replace"), + "edit" : (None, None), + "check" : (None, None), + "cross" : (None, None), + "hash" : (None, None), + "maximise" : (None, None), + "minimise" : (None, None), + "refresh" : (None, None), + "reference" : (None, None), - ## Other Icons - "warning" : (QStyle.SP_MessageBoxWarning, "dialog-warning"), + ## Switches + "sticky-on" : (None, None), + "sticky-off" : (None, None), } DECO_MAP = { diff --git a/nw/guimain.py b/nw/guimain.py index 4bd58176..6a7cbd9c 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -42,8 +42,8 @@ from PyQt5.QtWidgets import ( from nw.gui import ( GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus, - GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, - GuiProjectSettings, GuiProjectTree, GuiSearchBar, GuiSessionLogView, GuiTheme + GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme, + GuiProjectSettings, GuiProjectTree, GuiSessionLogView ) from nw.core import NWProject, NWDoc, NWIndex from nw.constants import nwFiles, nwItemType, nwAlert @@ -93,7 +93,6 @@ class GuiMain(QMainWindow): self.docEditor = GuiDocEditor(self) self.viewMeta = GuiDocViewDetails(self) self.docViewer = GuiDocViewer(self) - self.searchBar = GuiSearchBar(self) self.treeMeta = GuiItemDetails(self) self.projView = GuiOutline(self) self.projMeta = GuiOutlineDetails(self) @@ -111,22 +110,13 @@ class GuiMain(QMainWindow): self.treeBox.addWidget(self.treeMeta) self.treePane.setLayout(self.treeBox) - self.editPane = QWidget() - self.docEdit = QVBoxLayout() - self.docEdit.setContentsMargins(0, 0, 0, 0) - self.docEdit.setSpacing(self.mainConf.pxInt(2)) - self.docEdit.addWidget(self.searchBar) - self.docEdit.addWidget(self.docEditor) - self.editPane.setLayout(self.docEdit) - self.splitView = QSplitter(Qt.Vertical) self.splitView.addWidget(self.docViewer) self.splitView.addWidget(self.viewMeta) self.splitView.setSizes(self.mainConf.getViewPanePos()) self.splitDocs = QSplitter(Qt.Horizontal) - self.splitDocs.setOpaqueResize(False) - self.splitDocs.addWidget(self.editPane) + self.splitDocs.addWidget(self.docEditor) self.splitDocs.addWidget(self.splitView) self.splitOutline = QSplitter(Qt.Vertical) @@ -144,7 +134,6 @@ class GuiMain(QMainWindow): xCM = self.mainConf.pxInt(4) self.splitMain = QSplitter(Qt.Horizontal) self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM) - self.splitMain.setOpaqueResize(False) self.splitMain.addWidget(self.treePane) self.splitMain.addWidget(self.tabWidget) self.splitMain.setSizes(self.mainConf.getMainPanePos()) @@ -153,7 +142,7 @@ class GuiMain(QMainWindow): self.idxTree = self.splitMain.indexOf(self.treePane) self.idxMain = self.splitMain.indexOf(self.tabWidget) - self.idxEditor = self.splitDocs.indexOf(self.editPane) + self.idxEditor = self.splitDocs.indexOf(self.docEditor) self.idxViewer = self.splitDocs.indexOf(self.splitView) self.idxViewDoc = self.splitView.indexOf(self.docViewer) self.idxViewMeta = self.splitView.indexOf(self.viewMeta) @@ -168,7 +157,7 @@ class GuiMain(QMainWindow): self.splitView.setCollapsible(self.idxViewMeta, False) self.splitView.setVisible(False) - self.searchBar.setVisible(False) + self.docEditor.closeSearch() # Build the Tree View self.treeView.itemSelectionChanged.connect(self._treeSingleClick) @@ -489,6 +478,37 @@ class GuiMain(QMainWindow): return False return True + def openNextDocument(self, tHandle, wrapAround=False): + """Opens the next document in the project tree, following the + document with the given handle. Stops when reaching the end. + """ + if self.hasProject: + self.treeView.flushTreeOrder() + nHandle = None # The next handle after tHandle + fHandle = None # The first file handle we encounter + foundIt = False # We've found tHandle, pick the next we see + for tItem in self.theProject.projTree: + if tItem is None: + continue + if tItem.itemType != nwItemType.FILE: + continue + if fHandle is None: + fHandle = tItem.itemHandle + if tItem.itemHandle == tHandle: + foundIt = True + elif foundIt: + nHandle = tItem.itemHandle + break + + if nHandle is not None: + self.openDocument(nHandle, tLine=0) + return True + elif wrapAround: + self.openDocument(fHandle, tLine=0) + return False + + return False + def saveDocument(self): """Save the current documents. """ @@ -603,15 +623,13 @@ class GuiMain(QMainWindow): return True def passDocumentAction(self, theAction): - """Pass on document action theAction to whatever document has - the focus. If no document has focus, the action is discarded. + """Pass on document action theAction to the document viewer if + it has focus, otherwise pass it to the document editor. """ - if self.docEditor.hasFocus(): - self.docEditor.docAction(theAction) - elif self.docViewer.hasFocus(): + if self.docViewer.hasFocus(): self.docViewer.docAction(theAction) else: - logger.debug("Document action requested, but no document has focus") + self.docEditor.docAction(theAction) return True ## @@ -1077,8 +1095,8 @@ class GuiMain(QMainWindow): """When the escape key is pressed somewhere in the main window, do the following, in order: """ - if self.searchBar.isVisible(): - self.searchBar.setVisible(False) + if self.docEditor.docSearch.isVisible(): + self.docEditor.closeSearch() return elif self.isZenMode: self.toggleZenMode() diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf index ba0f2db7..12e6ec11 100644 --- a/tests/reference/novelwriter.conf +++ b/tests/reference/novelwriter.conf @@ -1,5 +1,5 @@ [Main] -timestamp = 2020-06-13 22:59:36 +timestamp = 2020-06-17 19:45:53 theme = default syntax = default_light icons = typicons_colour_light @@ -55,6 +55,12 @@ askbeforebackup = True showrefpanel = True viewcomments = True viewsynopsis = True +searchcase = False +searchword = False +searchregex = False +searchloop = False +searchnextfile = False +searchmatchcap = False [Path] lastpath =