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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 4c5d5ce7..5e0fb025 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,
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
+from nw.common import transferCase
logger = logging.getLogger(__name__)
@@ -87,6 +91,7 @@ class GuiDocEditor(QTextEdit):
# Document Title
self.docHeader = GuiDocEditHeader(self)
+ self.docSearch = GuiDocEditSearch(self)
# Syntax
self.hLight = GuiDocHighlighter(self.qDocument, self.theParent)
@@ -98,6 +103,7 @@ class GuiDocEditor(QTextEdit):
# Editor State
self.hasSelection = False
self.setMinimumWidth(self.mainConf.pxInt(300))
+ self.setAutoFillBackground(True)
self.setAcceptRichText(False)
# Custom Shortcuts
@@ -126,7 +132,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()
@@ -182,12 +188,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()
@@ -309,6 +318,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()
@@ -322,40 +332,27 @@ 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()
- tT = cM - tH
self.docHeader.setGeometry(tB, tB, tW, tH)
- self.setViewportMargins(0, tH, 0, 0)
- docFormat = self.qDocument.rootFrame().frameFormat()
- docFormat.setLeftMargin(tM)
- docFormat.setRightMargin(tM)
- if tT > 0:
- docFormat.setTopMargin(tT)
+ 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:
- docFormat.setTopMargin(0)
+ 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)
+ # print(tM, tH, rH, max(tM, tH, rH))
+ self.setViewportMargins(tM, max(cM, tH, rH), tM, cM)
return
@@ -532,7 +529,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:
@@ -573,6 +570,12 @@ class GuiDocEditor(QTextEdit):
))
return
+ def closeSearch(self):
+ """Close the search box.
+ """
+ self.docSearch.closeSearch()
+ return self.docSearch.isVisible()
+
##
# Document Events and Maintenance
##
@@ -590,6 +593,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()
@@ -1131,7 +1137,8 @@ class GuiDocEditor(QTextEdit):
selText = theCursor.selectedText()
else:
selText = ""
- self.theParent.searchBar.setSearchText(selText)
+ self.docSearch.setSearchText(selText)
+ self.updateDocMargins()
return
def _beginReplace(self):
@@ -1139,54 +1146,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):
@@ -1205,11 +1240,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
@@ -1219,16 +1259,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
+# =============================================================================================== #
class GuiDocEditHeader(QWidget):
diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py
index 1059c277..8c2ea236 100644
--- a/nw/gui/docviewer.py
+++ b/nw/gui/docviewer.py
@@ -236,13 +236,14 @@ class GuiDocViewer(QTextBrowser):
else:
sW = 0
+ cM = self.mainConf.getTextMargin()
tB = self.frameWidth()
tW = self.width() - 2*tB - sW
tH = self.docHeader.height()
fH = self.docFooter.height()
fY = self.height() - fH - tB
- tT = self.mainConf.getTextMargin() - tH
- bT = self.mainConf.getTextMargin() - fH
+ 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)
@@ -558,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
@@ -669,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 cb2f19a2..70a6ba0b 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)
@@ -487,6 +476,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.
"""
@@ -601,15 +621,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
##
@@ -1075,8 +1093,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 =