From 8199c2f4e936b2126004c83e090f5b53924fe11d Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Tue, 2 Jun 2020 22:00:23 +0200
Subject: [PATCH 01/32] Added outline details panel, and removed project in
constructor of GUI classes
---
nw/gui/__init__.py | 6 +++--
nw/gui/docdetails.py | 4 +--
nw/gui/doceditor.py | 6 ++---
nw/gui/docviewer.py | 10 +++----
nw/gui/itemdetails.py | 10 +++----
nw/gui/mainmenu.py | 8 +++---
nw/gui/outline.py | 26 +++++++++----------
nw/gui/outlinedetails.py | 56 ++++++++++++++++++++++++++++++++++++++++
nw/gui/projtree.py | 4 +--
nw/guimain.py | 24 +++++++++--------
10 files changed, 107 insertions(+), 47 deletions(-)
create mode 100644 nw/gui/outlinedetails.py
diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py
index 4df869b5..981173f2 100644
--- a/nw/gui/__init__.py
+++ b/nw/gui/__init__.py
@@ -13,7 +13,8 @@ from nw.gui.docviewer import GuiDocViewer
from nw.gui.itemdetails import GuiItemDetails
from nw.gui.itemeditor import GuiItemEditor
from nw.gui.mainmenu import GuiMainMenu
-from nw.gui.outline import GuiProjectOutline
+from nw.gui.outline import GuiOutline
+from nw.gui.outlinedetails import GuiOutlineDetails
from nw.gui.preferences import GuiPreferences
from nw.gui.projload import GuiProjectLoad
from nw.gui.projsettings import GuiProjectSettings
@@ -37,7 +38,8 @@ __all__ = [
"GuiItemDetails",
"GuiItemEditor",
"GuiMainMenu",
- "GuiProjectOutline",
+ "GuiOutline",
+ "GuiOutlineDetails",
"GuiPreferences",
"GuiProjectLoad",
"GuiProjectSettings",
diff --git a/nw/gui/docdetails.py b/nw/gui/docdetails.py
index d4b2621c..09b43426 100644
--- a/nw/gui/docdetails.py
+++ b/nw/gui/docdetails.py
@@ -37,13 +37,13 @@ logger = logging.getLogger(__name__)
class GuiDocViewDetails(QWidget):
- def __init__(self, theParent, theProject):
+ def __init__(self, theParent):
QWidget.__init__(self, theParent)
logger.debug("Initialising DocViewDetails ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
- self.theProject = theProject
+ self.theProject = theParent.theProject
self.currHandle = None
self.outerBox = QGridLayout(self)
diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py
index bf511e07..1b3f81bb 100644
--- a/nw/gui/doceditor.py
+++ b/nw/gui/doceditor.py
@@ -50,16 +50,16 @@ logger = logging.getLogger(__name__)
class GuiDocEditor(QTextEdit):
- def __init__(self, theParent, theProject):
- QTextEdit.__init__(self)
+ def __init__(self, theParent):
+ QTextEdit.__init__(self, theParent)
logger.debug("Initialising GuiDocEditor ...")
# Class Variables
self.mainConf = nw.CONFIG
- self.theProject = theProject
self.theParent = theParent
self.theTheme = theParent.theTheme
+ self.theProject = theParent.theProject
self.docChanged = False
self.spellCheck = False
self.nwDocument = NWDoc(self.theProject, self.theParent)
diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py
index 1be66bba..446426e4 100644
--- a/nw/gui/docviewer.py
+++ b/nw/gui/docviewer.py
@@ -40,16 +40,16 @@ logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser):
- def __init__(self, theParent, theProject):
- QTextBrowser.__init__(self)
+ def __init__(self, theParent):
+ QTextBrowser.__init__(self, theParent)
- logger.debug("Initialising DocViewer ...")
+ logger.debug("Initialising GuiDocViewer ...")
# Class Variables
self.mainConf = nw.CONFIG
- self.theProject = theProject
self.theParent = theParent
self.theTheme = theParent.theTheme
+ self.theProject = theParent.theProject
self.theHandle = None
self.qDocument = self.document()
@@ -70,7 +70,7 @@ class GuiDocViewer(QTextBrowser):
self.anchorClicked.connect(self._linkClicked)
self.setFocusPolicy(Qt.StrongFocus)
- logger.debug("DocViewer initialisation complete")
+ logger.debug("GuiDocViewer initialisation complete")
# Connect Functions
self.setSelectedHandle = self.theParent.treeView.setSelectedHandle
diff --git a/nw/gui/itemdetails.py b/nw/gui/itemdetails.py
index 93ffb788..7335a596 100644
--- a/nw/gui/itemdetails.py
+++ b/nw/gui/itemdetails.py
@@ -30,7 +30,7 @@ import nw
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QIcon, QPixmap
-from PyQt5.QtWidgets import QFrame, QGridLayout, QLabel
+from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel
from nw.constants import (
nwLabels, nwItemClass, nwItemType, nwItemLayout, nwUnicode
@@ -38,15 +38,15 @@ from nw.constants import (
logger = logging.getLogger(__name__)
-class GuiItemDetails(QFrame):
+class GuiItemDetails(QWidget):
- def __init__(self, theParent, theProject):
- QFrame.__init__(self, theParent)
+ def __init__(self, theParent):
+ QWidget.__init__(self, theParent)
logger.debug("Initialising GuiItemDetails ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
- self.theProject = theProject
+ self.theProject = theParent.theProject
self.theTheme = theParent.theTheme
self.theHandle = None
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index 5ff4d918..fc170377 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -39,13 +39,13 @@ logger = logging.getLogger(__name__)
class GuiMainMenu(QMenuBar):
- def __init__(self, theParent, theProject):
+ def __init__(self, theParent):
QMenuBar.__init__(self, theParent)
- logger.debug("Initialising Main Menu ...")
+ logger.debug("Initialising GuiMainMenu ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
- self.theProject = theProject
+ self.theProject = theParent.theProject
self._buildProjectMenu()
self._buildDocumentMenu()
@@ -60,7 +60,7 @@ class GuiMainMenu(QMenuBar):
self._moveTreeItem = self.theParent.treeView.moveTreeItem
self._newTreeItem = self.theParent.treeView.newTreeItem
- logger.debug("Main Menu initialisation complete")
+ logger.debug("GuiMainMenu initialisation complete")
return
diff --git a/nw/gui/outline.py b/nw/gui/outline.py
index 12c26383..c3c81e4f 100644
--- a/nw/gui/outline.py
+++ b/nw/gui/outline.py
@@ -39,7 +39,7 @@ from nw.constants import nwKeyWords, nwLabels, nwOutline
logger = logging.getLogger(__name__)
-class GuiProjectOutline(QTreeWidget):
+class GuiOutline(QTreeWidget):
DEF_WIDTH = {
nwOutline.TITLE : 200,
@@ -79,17 +79,17 @@ class GuiProjectOutline(QTreeWidget):
nwOutline.SYNOP : False,
}
- def __init__(self, theParent, theProject):
+ def __init__(self, theParent):
QTreeWidget.__init__(self, theParent)
- logger.debug("Initialising ProjectOutline ...")
+ logger.debug("Initialising GuiOutline ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
- self.theProject = theProject
+ self.theProject = theParent.theProject
self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
- self.optState = theProject.optState
+ self.optState = theParent.theProject.optState
self.headerMenu = GuiOutlineHeaderMenu(self)
self.firstView = True
@@ -120,7 +120,7 @@ class GuiProjectOutline(QTreeWidget):
self.clearOutline()
self.headerMenu.setHiddenState(self.colHidden)
- logger.debug("ProjectOutline initialisation complete")
+ logger.debug("GuiOutline initialisation complete")
return
@@ -233,7 +233,7 @@ class GuiProjectOutline(QTreeWidget):
# Load whatever we saved last time, regardless of wether it
# contains the correct names or number of columns. The names
# must be valid though.
- tempOrder = self.optState.getValue("GuiProjectOutline", "headerOrder", [])
+ tempOrder = self.optState.getValue("GuiOutline", "headerOrder", [])
treeOrder = []
for hName in tempOrder:
try:
@@ -256,14 +256,14 @@ class GuiProjectOutline(QTreeWidget):
# We load whatever column widths and hidden states we find in
# the file, and leave the rest in their default state.
- tmpWidth = self.optState.getValue("GuiProjectOutline", "columnWidth", {})
+ tmpWidth = self.optState.getValue("GuiOutline", "columnWidth", {})
for hName in tmpWidth:
try:
self.colWidth[nwOutline[hName]] = tmpWidth[hName]
except:
logger.warning("Ignored unknown outline column '%s'" % str(hName))
- tmpHidden = self.optState.getValue("GuiProjectOutline", "columnHidden", {})
+ tmpHidden = self.optState.getValue("GuiOutline", "columnHidden", {})
for hName in tmpHidden:
try:
self.colHidden[nwOutline[hName]] = tmpHidden[hName]
@@ -304,9 +304,9 @@ class GuiProjectOutline(QTreeWidget):
if not logHidden and logWidth > 0:
colWidth[hName] = logWidth
- self.optState.setValue("GuiProjectOutline", "headerOrder", treeOrder)
- self.optState.setValue("GuiProjectOutline", "columnWidth", colWidth)
- self.optState.setValue("GuiProjectOutline", "columnHidden", colHidden)
+ self.optState.setValue("GuiOutline", "headerOrder", treeOrder)
+ self.optState.setValue("GuiOutline", "columnWidth", colWidth)
+ self.optState.setValue("GuiOutline", "columnHidden", colHidden)
self.optState.saveSettings()
return
@@ -438,7 +438,7 @@ class GuiProjectOutline(QTreeWidget):
return newItem
-# END Class GuiProjectOutline
+# END Class GuiOutline
class GuiOutlineHeaderMenu(QMenu):
diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py
new file mode 100644
index 00000000..d4701cd5
--- /dev/null
+++ b/nw/gui/outlinedetails.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+"""novelWriter GUI Project Outline
+
+ novelWriter – GUI Project Outline
+===================================
+ Class holding the project outline view
+
+ File History:
+ Created: 2020-06-02 [0.7.0]
+
+ 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
+from PyQt5.QtWidgets import (
+ QWidget
+)
+
+logger = logging.getLogger(__name__)
+
+class GuiOutlineDetails(QWidget):
+
+ def __init__(self, theParent):
+ QWidget.__init__(self, theParent)
+
+ logger.debug("Initialising GuiOutlineDetails ...")
+
+ self.mainConf = nw.CONFIG
+ self.theParent = theParent
+ self.theProject = theParent.theProject
+ self.theTheme = theParent.theTheme
+ self.theIndex = theParent.theIndex
+ self.optState = theParent.theProject.optState
+
+ logger.debug("GuiOutlineDetails initialisation complete")
+
+ return
+
+# END Class GuiOutlineDetails
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index 72246c3a..1dd065cc 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -49,14 +49,14 @@ class GuiProjectTree(QTreeWidget):
C_EXPORT = 2
C_FLAGS = 3
- def __init__(self, theParent, theProject):
+ def __init__(self, theParent):
QTreeWidget.__init__(self, theParent)
logger.debug("Initialising GuiProjectTree ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
- self.theProject = theProject
+ self.theProject = theParent.theProject
# Tree Settings
self.theMap = None
diff --git a/nw/guimain.py b/nw/guimain.py
index ea6c050e..639a015a 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -40,10 +40,10 @@ from PyQt5.QtWidgets import (
)
from nw.gui import (
- GuiMainMenu, GuiMainStatus, GuiTheme, GuiProjectTree, GuiDocEditor,
- GuiDocViewer, GuiItemDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails,
- GuiPreferences, GuiProjectSettings, GuiItemEditor, GuiProjectOutline,
- GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad, GuiBuildNovel
+ GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails,
+ GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus,
+ GuiNoticeBar, GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad,
+ GuiProjectSettings, GuiProjectTree, GuiSearchBar, GuiSessionLogView, GuiTheme
)
from nw.core import NWProject, NWDoc, NWIndex
from nw.constants import nwFiles, nwItemType, nwAlert
@@ -91,14 +91,15 @@ class GuiMain(QMainWindow):
# Main GUI Elements
self.statusBar = GuiMainStatus(self)
self.noticeBar = GuiNoticeBar(self)
- self.treeView = GuiProjectTree(self, self.theProject)
- self.docEditor = GuiDocEditor(self, self.theProject)
- self.docViewer = GuiDocViewer(self, self.theProject)
- self.viewMeta = GuiDocViewDetails(self, self.theProject)
+ self.treeView = GuiProjectTree(self)
+ self.docEditor = GuiDocEditor(self)
+ self.docViewer = GuiDocViewer(self)
+ self.viewMeta = GuiDocViewDetails(self)
self.searchBar = GuiSearchBar(self)
- self.treeMeta = GuiItemDetails(self, self.theProject)
- self.projView = GuiProjectOutline(self, self.theProject)
- self.mainMenu = GuiMainMenu(self, self.theProject)
+ self.treeMeta = GuiItemDetails(self)
+ self.projView = GuiOutline(self)
+ self.projMeta = GuiOutlineDetails(self)
+ self.mainMenu = GuiMainMenu(self)
# Minor Gui Elements
self.statusIcons = []
@@ -137,6 +138,7 @@ class GuiMain(QMainWindow):
self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.projView)
+ self.splitOutline.addWidget(self.projMeta)
self.tabWidget = QTabWidget()
self.tabWidget.setTabPosition(QTabWidget.East)
From 4334e621480e89d25e71c733484bc28a1367f48e Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 3 Jun 2020 20:40:25 +0200
Subject: [PATCH 02/32] Details panel now has all the data
---
nw/config.py | 22 ++--
nw/gui/docviewer.py | 26 +++--
nw/gui/outline.py | 14 +++
nw/gui/outlinedetails.py | 210 ++++++++++++++++++++++++++++++++++++++-
nw/guimain.py | 2 +
5 files changed, 256 insertions(+), 18 deletions(-)
diff --git a/nw/config.py b/nw/config.py
index 985eca43..66fdb649 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -95,6 +95,7 @@ class Config:
self.projColWidth = [140, 55, 140]
self.mainPanePos = [300, 800]
self.docPanePos = [400, 400]
+ self.outlnPanePos = [500, 150]
self.isFullScreen = False
## Project
@@ -342,6 +343,9 @@ class Config:
self.docPanePos = self._parseLine(
cnfParse, cnfSec, "docpane", self.CNF_LIST, self.docPanePos
)
+ self.outlnPanePos = self._parseLine(
+ cnfParse, cnfSec, "outlinepane", self.CNF_LIST, self.outlnPanePos
+ )
self.isFullScreen = self._parseLine(
cnfParse, cnfSec, "fullscreen", self.CNF_BOOL, self.isFullScreen
)
@@ -481,12 +485,13 @@ class Config:
## Sizes
cnfSec = "Sizes"
cnfParse.add_section(cnfSec)
- cnfParse.set(cnfSec,"geometry", self._packList(self.winGeometry))
- cnfParse.set(cnfSec,"treecols", self._packList(self.treeColWidth))
- cnfParse.set(cnfSec,"projcols", self._packList(self.projColWidth))
- cnfParse.set(cnfSec,"mainpane", self._packList(self.mainPanePos))
- cnfParse.set(cnfSec,"docpane", self._packList(self.docPanePos))
- cnfParse.set(cnfSec,"fullscreen", str(self.isFullScreen))
+ cnfParse.set(cnfSec,"geometry", self._packList(self.winGeometry))
+ cnfParse.set(cnfSec,"treecols", self._packList(self.treeColWidth))
+ cnfParse.set(cnfSec,"projcols", self._packList(self.projColWidth))
+ cnfParse.set(cnfSec,"mainpane", self._packList(self.mainPanePos))
+ cnfParse.set(cnfSec,"docpane", self._packList(self.docPanePos))
+ cnfParse.set(cnfSec,"outlinepane", self._packList(self.outlnPanePos))
+ cnfParse.set(cnfSec,"fullscreen", str(self.isFullScreen))
## Project
cnfSec = "Project"
@@ -700,6 +705,11 @@ class Config:
self.confChanged = True
return True
+ def setOutlinePanePos(self, panePos):
+ self.outlnPanePos = panePos
+ self.confChanged = True
+ return True
+
def setShowRefPanel(self, checkState):
self.showRefPanel = checkState
self.confChanged = True
diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py
index 446426e4..fb1a9c99 100644
--- a/nw/gui/docviewer.py
+++ b/nw/gui/docviewer.py
@@ -268,6 +268,21 @@ class GuiDocViewer(QTextBrowser):
logger.verbose("Cursor moved to line %d" % theLine)
return True
+ ##
+ # Slots
+ ##
+
+ def _linkClicked(self, theURL):
+ """Slot for a link in the document being clicked.
+ """
+ theLink = theURL.url()
+ logger.verbose("Clicked link: '%s'" % theLink)
+ if len(theLink) > 0:
+ theBits = theLink.split("=")
+ if len(theBits) == 2:
+ self.loadFromTag(theBits[1])
+ return
+
##
# Events
##
@@ -293,17 +308,6 @@ class GuiDocViewer(QTextBrowser):
self.setTextCursor(theCursor)
return
- def _linkClicked(self, theURL):
- """Slot for a link in the document being clicked.
- """
- theLink = theURL.url()
- logger.verbose("Clicked link: '%s'" % theLink)
- if len(theLink) > 0:
- theBits = theLink.split("=")
- if len(theBits) == 2:
- self.loadFromTag(theBits[1])
- return
-
def _makeStyleSheet(self):
"""Generate an appropriate style sheet for the document viewer,
based on the current syntax highlighter theme,
diff --git a/nw/gui/outline.py b/nw/gui/outline.py
index c3c81e4f..368b0403 100644
--- a/nw/gui/outline.py
+++ b/nw/gui/outline.py
@@ -100,6 +100,7 @@ class GuiOutline(QTreeWidget):
self.setExpandsOnDoubleClick(False)
self.setDragEnabled(False)
self.itemDoubleClicked.connect(self._treeDoubleClick)
+ self.itemSelectionChanged.connect(self._itemSelected)
iPx = self.theTheme.textIconSize
self.setIconSize(QSize(iPx, iPx))
@@ -198,6 +199,18 @@ class GuiOutline(QTreeWidget):
self.theParent.openDocument(tHandle, tLine - 1)
return
+ def _itemSelected(self):
+ """Extract the handle and line number of the currently selected
+ title, and send it to the details panel.
+ """
+ selItems = self.selectedItems()
+ if selItems:
+ tHandle = selItems[0].data(self.colIndex[nwOutline.TITLE], Qt.UserRole)
+ sTitle = selItems[0].data(self.colIndex[nwOutline.LINE], Qt.UserRole)
+ logger.verbose("User selected entry %s:%s" % (tHandle, sTitle))
+ self.theParent.projMeta.showItem(tHandle, sTitle)
+ return
+
def _headerRightClick(self, clickPos):
"""Show the header column menu.
"""
@@ -415,6 +428,7 @@ class GuiOutline(QTreeWidget):
newItem.setText(self.colIndex[nwOutline.LABEL], nwItem.itemName)
newItem.setIcon(self.colIndex[nwOutline.LABEL], self.theTheme.getIcon("proj_document"))
newItem.setText(self.colIndex[nwOutline.LINE], sTitle[1:].lstrip("0"))
+ newItem.setData(self.colIndex[nwOutline.LINE], Qt.UserRole, sTitle)
newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"])
newItem.setText(self.colIndex[nwOutline.CCOUNT], str(novIdx["cCount"]))
newItem.setText(self.colIndex[nwOutline.WCOUNT], str(novIdx["wCount"]))
diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py
index d4701cd5..66450155 100644
--- a/nw/gui/outlinedetails.py
+++ b/nw/gui/outlinedetails.py
@@ -30,13 +30,22 @@ import nw
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
- QWidget
+ QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel, QSizePolicy
)
+from nw.constants import nwLabels, nwKeyWords
+
logger = logging.getLogger(__name__)
class GuiOutlineDetails(QWidget):
+ LVL_MAP = {
+ "H1" : "Title",
+ "H2" : "Chapter",
+ "H3" : "Scene",
+ "H4" : "Section"
+ }
+
def __init__(self, theParent):
QWidget.__init__(self, theParent)
@@ -49,8 +58,207 @@ class GuiOutlineDetails(QWidget):
self.theIndex = theParent.theIndex
self.optState = theParent.theProject.optState
+ # Sizes
+ minTitle = self.theTheme.getTextWidth("X"*30)
+ maxTitle = self.theTheme.getTextWidth("X"*50)
+ wCount = self.theTheme.getTextWidth("99,999")
+ hSpace = int(0.5*self.theTheme.fontPixelSize)
+ vSpace = int(0.3*self.theTheme.fontPixelSize)
+
+ # Details Area
+ self.titleLabel = QLabel("Title")
+ self.levelLabel = QLabel("Level")
+ self.fileLabel = QLabel("Document")
+ self.titleValue = QLabel("")
+ self.levelValue = QLabel("")
+ self.fileValue = QLabel("")
+ self.titleValue.setMinimumWidth(minTitle)
+ self.titleValue.setMaximumWidth(maxTitle)
+ self.levelValue.setMinimumWidth(minTitle)
+ self.levelValue.setMaximumWidth(maxTitle)
+ self.fileValue.setMinimumWidth(minTitle)
+ self.fileValue.setMaximumWidth(maxTitle)
+
+ # Stats Area
+ self.cCLabel = QLabel("Characters")
+ self.wCLabel = QLabel("Words")
+ self.pCLabel = QLabel("Paragraphs")
+ self.cCValue = QLabel("")
+ self.wCValue = QLabel("")
+ self.pCValue = QLabel("")
+ self.cCValue.setMinimumWidth(wCount)
+ self.wCValue.setMinimumWidth(wCount)
+ self.pCValue.setMinimumWidth(wCount)
+ self.cCValue.setAlignment(Qt.AlignRight)
+ self.wCValue.setAlignment(Qt.AlignRight)
+ self.pCValue.setAlignment(Qt.AlignRight)
+
+ # Synopsis
+ self.synopLabel = QLabel("Synopsis")
+ self.synopValue = QLabel("")
+ self.synopLWrap = QHBoxLayout()
+ self.synopValue.setWordWrap(True)
+ self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft)
+ self.synopValue.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
+ self.synopLWrap.addWidget(self.synopValue, 1)
+
+ # Tags
+ self.povKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
+ self.chrKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY])
+ self.pltKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])
+ self.timKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.TIME_KEY])
+ self.wldKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY])
+ self.objKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY])
+ self.entKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY])
+ self.cstKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY])
+ self.povKeyValue = QLabel("")
+ self.chrKeyValue = QLabel("")
+ self.pltKeyValue = QLabel("")
+ self.timKeyValue = QLabel("")
+ self.wldKeyValue = QLabel("")
+ self.objKeyValue = QLabel("")
+ self.entKeyValue = QLabel("")
+ self.cstKeyValue = QLabel("")
+ self.povKeyValue.linkActivated.connect(self._tagClicked)
+ self.chrKeyValue.linkActivated.connect(self._tagClicked)
+ self.pltKeyValue.linkActivated.connect(self._tagClicked)
+ self.timKeyValue.linkActivated.connect(self._tagClicked)
+ self.wldKeyValue.linkActivated.connect(self._tagClicked)
+ self.objKeyValue.linkActivated.connect(self._tagClicked)
+ self.entKeyValue.linkActivated.connect(self._tagClicked)
+ self.cstKeyValue.linkActivated.connect(self._tagClicked)
+
+ # Selected Item Details
+ self.mainGroup = QGroupBox("Title Details", self)
+ self.mainForm = QGridLayout()
+ self.mainGroup.setLayout(self.mainForm)
+
+ self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
+ self.mainForm.addWidget(self.levelLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.levelValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
+ self.mainForm.addWidget(self.fileLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.fileValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
+ self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
+
+ self.mainForm.setColumnStretch(1, 1)
+ self.mainForm.setRowStretch(4, 1)
+ self.mainForm.setHorizontalSpacing(hSpace)
+ self.mainForm.setVerticalSpacing(vSpace)
+
+ # Selected Item Tags
+ self.tagsGroup = QGroupBox("Tags", self)
+ self.tagsForm = QGridLayout()
+ self.tagsGroup.setLayout(self.tagsForm)
+
+ self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.povKeyValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.chrKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.chrKeyValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.pltKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.pltKeyValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.timKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.timKeyValue, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.wldKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.wldKeyValue, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.objKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.objKeyValue, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.entKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.entKeyValue, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.cstKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.cstKeyValue, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+
+ self.tagsForm.setColumnStretch(1, 1)
+ self.tagsForm.setRowStretch(8, 1)
+ self.tagsForm.setHorizontalSpacing(hSpace)
+ self.tagsForm.setVerticalSpacing(vSpace)
+
+ # Assemble
+ self.outerBox = QHBoxLayout()
+ self.outerBox.addWidget(self.mainGroup, 0)
+ self.outerBox.addWidget(self.tagsGroup, 1)
+ self.outerBox.addStretch(1)
+
+ self.setLayout(self.outerBox)
+
logger.debug("GuiOutlineDetails initialisation complete")
return
+ def showItem(self, tHandle, sTitle):
+ """Update the content of the tree with the given handle and line
+ number pointing to a header.
+ """
+ try:
+ nwItem = self.theProject.projTree[tHandle]
+ novIdx = self.theIndex.novelIndex[tHandle][sTitle]
+ theRefs = self.theIndex.getReferences(tHandle, sTitle)
+ except:
+ return False
+
+ self.titleValue.setText(novIdx["title"])
+ if novIdx["level"] in self.LVL_MAP:
+ self.levelValue.setText(self.LVL_MAP[novIdx["level"]])
+ else:
+ self.levelValue.setText("Unknown")
+ self.fileValue.setText(nwItem.itemName)
+
+ self.cCValue.setText("{:n}".format(novIdx["cCount"]))
+ self.wCValue.setText("{:n}".format(novIdx["pCount"]))
+ self.pCValue.setText("{:n}".format(novIdx["wCount"]))
+
+ self.synopValue.setText(novIdx["synopsis"])
+ self.synopValue.adjustSize()
+ # print(self.mainForm.sizeHint().width())
+ # self.synopValue.setSizePolicy() (self.mainForm.sizeHint().width())
+
+ self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY))
+ self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY))
+ self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY))
+ self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY))
+ self.wldKeyValue.setText(self._formatTags(theRefs, nwKeyWords.WORLD_KEY))
+ self.objKeyValue.setText(self._formatTags(theRefs, nwKeyWords.OBJECT_KEY))
+ self.entKeyValue.setText(self._formatTags(theRefs, nwKeyWords.ENTITY_KEY))
+ self.cstKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CUSTOM_KEY))
+
+ return True
+
+ ##
+ # Slots
+ ##
+
+ def _tagClicked(self, theLink):
+ """Capture the click of a tag in the right-most column.
+ """
+ logger.verbose("Clicked link: '%s'" % theLink)
+ if len(theLink) > 0:
+ theBits = theLink.split("=")
+ if len(theBits) == 2:
+ self.theParent.docViewer.loadFromTag(theBits[1])
+ self.theParent.tabWidget.setCurrentWidget(self.theParent.splitView)
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _formatTags(self, theRefs, theKey):
+ """Format the tags as clickable links.
+ """
+ if theKey not in theKey:
+ return ""
+ refTags = []
+ for tTag in theRefs[theKey]:
+ refTags.append("%s" % (
+ theKey[1:], tTag, tTag
+ ))
+ return ", ".join(refTags)
+
# END Class GuiOutlineDetails
diff --git a/nw/guimain.py b/nw/guimain.py
index 639a015a..ebbcc984 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -139,6 +139,7 @@ class GuiMain(QMainWindow):
self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.projView)
self.splitOutline.addWidget(self.projMeta)
+ self.splitOutline.setSizes(self.mainConf.outlnPanePos)
self.tabWidget = QTabWidget()
self.tabWidget.setTabPosition(QTabWidget.East)
@@ -849,6 +850,7 @@ class GuiMain(QMainWindow):
if not self.isZenMode:
self.mainConf.setMainPanePos(self.splitMain.sizes())
self.mainConf.setDocPanePos(self.splitView.sizes())
+ self.mainConf.setOutlinePanePos(self.splitOutline.sizes())
self.mainConf.saveConfig()
self.reportConfErr()
From 4167d2e85470cb2e64da8806476d3e7cdff3f569 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 3 Jun 2020 20:43:12 +0200
Subject: [PATCH 03/32] Fixed tests
---
tests/reference/novelwriter.conf | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf
index f86bddc0..2c17e47f 100644
--- a/tests/reference/novelwriter.conf
+++ b/tests/reference/novelwriter.conf
@@ -1,5 +1,5 @@
[Main]
-timestamp = 2020-05-21 14:52:36
+timestamp = 2020-06-03 20:42:03
theme = default
syntax = default_light
icons = typicons_grey_light
@@ -13,6 +13,7 @@ treecols = 120, 30, 50
projcols = 140, 55, 140
mainpane = 300, 800
docpane = 400, 400
+outlinepane = 500, 150
fullscreen = False
[Project]
From e402789892cf084f768fb5c349b81a9d6f0a311e Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 3 Jun 2020 22:14:19 +0200
Subject: [PATCH 04/32] Made the outline details panel a scroll area
---
nw/gui/outlinedetails.py | 82 ++++++++++++++++++++++++++++------------
nw/gui/theme.py | 2 +
2 files changed, 60 insertions(+), 24 deletions(-)
diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py
index 66450155..7458a25e 100644
--- a/nw/gui/outlinedetails.py
+++ b/nw/gui/outlinedetails.py
@@ -30,14 +30,14 @@ import nw
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
- QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel, QSizePolicy
+ QScrollArea, QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel, QSizePolicy
)
from nw.constants import nwLabels, nwKeyWords
logger = logging.getLogger(__name__)
-class GuiOutlineDetails(QWidget):
+class GuiOutlineDetails(QScrollArea):
LVL_MAP = {
"H1" : "Title",
@@ -47,7 +47,7 @@ class GuiOutlineDetails(QWidget):
}
def __init__(self, theParent):
- QWidget.__init__(self, theParent)
+ QScrollArea.__init__(self, theParent)
logger.debug("Initialising GuiOutlineDetails ...")
@@ -59,11 +59,11 @@ class GuiOutlineDetails(QWidget):
self.optState = theParent.theProject.optState
# Sizes
- minTitle = self.theTheme.getTextWidth("X"*30)
- maxTitle = self.theTheme.getTextWidth("X"*50)
- wCount = self.theTheme.getTextWidth("99,999")
- hSpace = int(0.5*self.theTheme.fontPixelSize)
- vSpace = int(0.3*self.theTheme.fontPixelSize)
+ minTitle = 30*self.theTheme.textNWidth
+ maxTitle = 40*self.theTheme.textNWidth
+ wCount = self.theTheme.getTextWidth("999,999")
+ hSpace = int(0.8*self.theTheme.textNWidth)
+ vSpace = int(0.2*self.theTheme.textNHeight)
# Details Area
self.titleLabel = QLabel("Title")
@@ -72,6 +72,7 @@ class GuiOutlineDetails(QWidget):
self.titleValue = QLabel("")
self.levelValue = QLabel("")
self.fileValue = QLabel("")
+
self.titleValue.setMinimumWidth(minTitle)
self.titleValue.setMaximumWidth(maxTitle)
self.levelValue.setMinimumWidth(minTitle)
@@ -86,6 +87,7 @@ class GuiOutlineDetails(QWidget):
self.cCValue = QLabel("")
self.wCValue = QLabel("")
self.pCValue = QLabel("")
+
self.cCValue.setMinimumWidth(wCount)
self.wCValue.setMinimumWidth(wCount)
self.pCValue.setMinimumWidth(wCount)
@@ -99,7 +101,6 @@ class GuiOutlineDetails(QWidget):
self.synopLWrap = QHBoxLayout()
self.synopValue.setWordWrap(True)
self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft)
- self.synopValue.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
self.synopLWrap.addWidget(self.synopValue, 1)
# Tags
@@ -111,6 +112,16 @@ class GuiOutlineDetails(QWidget):
self.objKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY])
self.entKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY])
self.cstKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY])
+
+ self.povKeyLWrap = QHBoxLayout()
+ self.chrKeyLWrap = QHBoxLayout()
+ self.pltKeyLWrap = QHBoxLayout()
+ self.timKeyLWrap = QHBoxLayout()
+ self.wldKeyLWrap = QHBoxLayout()
+ self.objKeyLWrap = QHBoxLayout()
+ self.entKeyLWrap = QHBoxLayout()
+ self.cstKeyLWrap = QHBoxLayout()
+
self.povKeyValue = QLabel("")
self.chrKeyValue = QLabel("")
self.pltKeyValue = QLabel("")
@@ -119,6 +130,16 @@ class GuiOutlineDetails(QWidget):
self.objKeyValue = QLabel("")
self.entKeyValue = QLabel("")
self.cstKeyValue = QLabel("")
+
+ self.povKeyValue.setWordWrap(True)
+ self.chrKeyValue.setWordWrap(True)
+ self.pltKeyValue.setWordWrap(True)
+ self.timKeyValue.setWordWrap(True)
+ self.wldKeyValue.setWordWrap(True)
+ self.objKeyValue.setWordWrap(True)
+ self.entKeyValue.setWordWrap(True)
+ self.cstKeyValue.setWordWrap(True)
+
self.povKeyValue.linkActivated.connect(self._tagClicked)
self.chrKeyValue.linkActivated.connect(self._tagClicked)
self.pltKeyValue.linkActivated.connect(self._tagClicked)
@@ -128,6 +149,15 @@ class GuiOutlineDetails(QWidget):
self.entKeyValue.linkActivated.connect(self._tagClicked)
self.cstKeyValue.linkActivated.connect(self._tagClicked)
+ self.povKeyLWrap.addWidget(self.povKeyValue, 1)
+ self.chrKeyLWrap.addWidget(self.chrKeyValue, 1)
+ self.pltKeyLWrap.addWidget(self.pltKeyValue, 1)
+ self.timKeyLWrap.addWidget(self.timKeyValue, 1)
+ self.wldKeyLWrap.addWidget(self.wldKeyValue, 1)
+ self.objKeyLWrap.addWidget(self.objKeyValue, 1)
+ self.entKeyLWrap.addWidget(self.entKeyValue, 1)
+ self.cstKeyLWrap.addWidget(self.cstKeyValue, 1)
+
# Selected Item Details
self.mainGroup = QGroupBox("Title Details", self)
self.mainForm = QGridLayout()
@@ -154,39 +184,46 @@ class GuiOutlineDetails(QWidget):
self.mainForm.setVerticalSpacing(vSpace)
# Selected Item Tags
- self.tagsGroup = QGroupBox("Tags", self)
+ self.tagsGroup = QGroupBox("Reference Tags", self)
self.tagsForm = QGridLayout()
self.tagsGroup.setLayout(self.tagsForm)
self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.povKeyValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.chrKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.chrKeyValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.chrKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.pltKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.pltKeyValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.pltKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.timKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.timKeyValue, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.timKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.wldKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.wldKeyValue, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.wldKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.objKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.objKeyValue, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.objKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.entKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.entKeyValue, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.entKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.cstKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.cstKeyValue, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.cstKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.setColumnStretch(1, 1)
- self.tagsForm.setRowStretch(8, 1)
+ self.tagsForm.setRowStretch(7, 1)
self.tagsForm.setHorizontalSpacing(hSpace)
self.tagsForm.setVerticalSpacing(vSpace)
# Assemble
+ self.outerWidget = QWidget()
self.outerBox = QHBoxLayout()
self.outerBox.addWidget(self.mainGroup, 0)
self.outerBox.addWidget(self.tagsGroup, 1)
- self.outerBox.addStretch(1)
+ # self.outerBox.addStretch(1)
- self.setLayout(self.outerBox)
+ self.outerWidget.setLayout(self.outerBox)
+ self.setWidget(self.outerWidget)
+ self.show()
+
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+ self.setWidgetResizable(True)
logger.debug("GuiOutlineDetails initialisation complete")
@@ -215,9 +252,6 @@ class GuiOutlineDetails(QWidget):
self.pCValue.setText("{:n}".format(novIdx["wCount"]))
self.synopValue.setText(novIdx["synopsis"])
- self.synopValue.adjustSize()
- # print(self.mainForm.sizeHint().width())
- # self.synopValue.setSizePolicy() (self.mainForm.sizeHint().width())
self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY))
self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY))
diff --git a/nw/gui/theme.py b/nw/gui/theme.py
index 49ea367c..8c736719 100644
--- a/nw/gui/theme.py
+++ b/nw/gui/theme.py
@@ -140,6 +140,8 @@ class GuiTheme:
self.fontPixelSize = int(round(qMetric.height()))
self.baseIconSize = int(round(qMetric.ascent()))
self.textIconSize = int(round(qMetric.ascent() + qMetric.leading()))
+ self.textNHeight = qMetric.boundingRect("N").height()
+ self.textNWidth = qMetric.boundingRect("N").width()
logger.verbose("GUI Font Family: %s" % self.guiFont.family())
logger.verbose("GUI Font Point Size: %.2f" % self.fontPointSize)
From 81d03d86e72cfe0241bcf864d985ea8ab4869eb5 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 3 Jun 2020 22:40:01 +0200
Subject: [PATCH 05/32] Replace QFrame with QWidget on main gui
---
nw/guimain.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/nw/guimain.py b/nw/guimain.py
index ebbcc984..e9d9b81d 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -35,7 +35,7 @@ from time import time
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence, QCursor
from PyQt5.QtWidgets import (
- qApp, QMainWindow, QVBoxLayout, QFrame, QSplitter, QFileDialog, QShortcut,
+ qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut,
QMessageBox, QDialog, QTabWidget
)
@@ -106,14 +106,14 @@ class GuiMain(QMainWindow):
self.importIcons = []
# Assemble Main Window
- self.treePane = QFrame()
+ self.treePane = QWidget()
self.treeBox = QVBoxLayout()
self.treeBox.setContentsMargins(0,0,0,0)
self.treeBox.addWidget(self.treeView)
self.treeBox.addWidget(self.treeMeta)
self.treePane.setLayout(self.treeBox)
- self.editPane = QFrame()
+ self.editPane = QWidget()
self.docEdit = QVBoxLayout()
self.docEdit.setContentsMargins(0,0,0,0)
self.docEdit.setSpacing(2)
@@ -122,7 +122,7 @@ class GuiMain(QMainWindow):
self.docEdit.addWidget(self.docEditor)
self.editPane.setLayout(self.docEdit)
- self.viewPane = QFrame()
+ self.viewPane = QWidget()
self.docView = QVBoxLayout()
self.docView.setContentsMargins(0,0,0,0)
self.docView.setSpacing(2)
From 184d0f62f9d78390e5f73fab92eacde6c4c99e54 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 3 Jun 2020 23:01:15 +0200
Subject: [PATCH 06/32] Made a few minor improvements to outline details
---
nw/gui/outlinedetails.py | 33 ++++++++++++++++++---------------
1 file changed, 18 insertions(+), 15 deletions(-)
diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py
index 7458a25e..33048837 100644
--- a/nw/gui/outlinedetails.py
+++ b/nw/gui/outlinedetails.py
@@ -34,6 +34,7 @@ from PyQt5.QtWidgets import (
)
from nw.constants import nwLabels, nwKeyWords
+from nw.common import checkInt
logger = logging.getLogger(__name__)
@@ -67,18 +68,18 @@ class GuiOutlineDetails(QScrollArea):
# Details Area
self.titleLabel = QLabel("Title")
- self.levelLabel = QLabel("Level")
self.fileLabel = QLabel("Document")
+ self.itemLabel = QLabel("Status")
self.titleValue = QLabel("")
- self.levelValue = QLabel("")
self.fileValue = QLabel("")
+ self.itemValue = QLabel("")
self.titleValue.setMinimumWidth(minTitle)
self.titleValue.setMaximumWidth(maxTitle)
- self.levelValue.setMinimumWidth(minTitle)
- self.levelValue.setMaximumWidth(maxTitle)
self.fileValue.setMinimumWidth(minTitle)
self.fileValue.setMaximumWidth(maxTitle)
+ self.itemValue.setMinimumWidth(minTitle)
+ self.itemValue.setMaximumWidth(maxTitle)
# Stats Area
self.cCLabel = QLabel("Characters")
@@ -167,12 +168,12 @@ class GuiOutlineDetails(QScrollArea):
self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
- self.mainForm.addWidget(self.levelLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.mainForm.addWidget(self.levelValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.fileLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.fileValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
- self.mainForm.addWidget(self.fileLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.mainForm.addWidget(self.fileValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.itemLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.itemValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
@@ -240,16 +241,18 @@ class GuiOutlineDetails(QScrollArea):
except:
return False
- self.titleValue.setText(novIdx["title"])
if novIdx["level"] in self.LVL_MAP:
- self.levelValue.setText(self.LVL_MAP[novIdx["level"]])
+ self.titleLabel.setText("%s" % self.LVL_MAP[novIdx["level"]])
else:
- self.levelValue.setText("Unknown")
- self.fileValue.setText(nwItem.itemName)
+ self.titleLabel.setText("Title")
+ self.titleValue.setText(novIdx["title"])
- self.cCValue.setText("{:n}".format(novIdx["cCount"]))
- self.wCValue.setText("{:n}".format(novIdx["pCount"]))
- self.pCValue.setText("{:n}".format(novIdx["wCount"]))
+ self.fileValue.setText(nwItem.itemName)
+ self.itemValue.setText(nwItem.itemStatus)
+
+ self.cCValue.setText("{:n}".format(checkInt(novIdx["cCount"], 0)))
+ self.wCValue.setText("{:n}".format(checkInt(novIdx["pCount"], 0)))
+ self.pCValue.setText("{:n}".format(checkInt(novIdx["wCount"], 0)))
self.synopValue.setText(novIdx["synopsis"])
From 0c18f1d3c90655fcc6b827e97fa6c5065cbc1836 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 4 Jun 2020 10:57:30 +0200
Subject: [PATCH 07/32] Added project tree context menu
---
nw/gui/projtree.py | 207 ++++++++++++++++++++++++++++++---------------
1 file changed, 140 insertions(+), 67 deletions(-)
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index 72246c3a..b6b26143 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -6,7 +6,8 @@
Class holding the left side document tree view
File History:
- Created: 2018-09-29 [0.0.1]
+ Created: 2018-09-29 [0.0.1] GuiProjectTree
+ Created: 2020-06-04 [0.7.0] GuiProjectTreeMenu
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
@@ -32,7 +33,7 @@ from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QFont, QColor, QIcon
from PyQt5.QtWidgets import (
qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMessageBox,
- QHeaderView
+ QHeaderView, QMenu, QAction
)
from nw.core import NWDoc
@@ -57,6 +58,7 @@ class GuiProjectTree(QTreeWidget):
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theProject
+ self.ctxMenu = GuiProjectTreeMenu(self)
# Tree Settings
self.theMap = None
@@ -71,6 +73,8 @@ class GuiProjectTree(QTreeWidget):
self.setIndentation(iPx)
self.setColumnCount(4)
self.setHeaderLabels(["Label", "Words", "Inc", "Flags"])
+ self.setContextMenuPolicy(Qt.CustomContextMenu)
+ self.customContextMenuRequested.connect(self._rightClickMenu)
treeHeadItem = self.headerItem()
treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
@@ -556,6 +560,83 @@ class GuiProjectTree(QTreeWidget):
return True
return False
+ ##
+ # Slots
+ ##
+
+ def _rightClickMenu(self, clickPos):
+ """The user right clicked an element in the project tree, so we
+ open a context menu in-place.
+ """
+ selItem = self.itemAt(clickPos)
+ self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
+ return
+
+ ##
+ # Events
+ ##
+
+ def mousePressEvent(self, theEvent):
+ """Overload mousePressEvent to clear selection if clicking the
+ mouse in a blank area of the tree view.
+ """
+ QTreeWidget.mousePressEvent(self, theEvent)
+ selItem = self.indexAt(theEvent.pos())
+ if not selItem.isValid():
+ self.clearSelection()
+ return
+
+ def dropEvent(self, theEvent):
+ """Overload the drop of dragged item event to check whether the
+ drop is allowed or not. Disallowed drops are cancelled.
+ """
+ sHandle = self.getSelectedHandle()
+ if sHandle is None:
+ logger.error("No handle selected")
+ return
+
+ dIndex = self.indexAt(theEvent.pos())
+ if not dIndex.isValid():
+ logger.error("Invalid drop index")
+ return
+
+ dItem = self.itemFromIndex(dIndex)
+ dHandle = dItem.data(self.C_NAME, Qt.UserRole)
+ snItem = self.theProject.projTree[sHandle]
+ dnItem = self.theProject.projTree[dHandle]
+ if dnItem is None:
+ self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
+ return
+
+ isSame = snItem.itemClass == dnItem.itemClass
+ isNone = snItem.itemClass == nwItemClass.NO_CLASS
+ isNote = snItem.itemLayout == nwItemLayout.NOTE
+ onFile = dnItem.itemType == nwItemType.FILE
+ isRoot = snItem.itemType == nwItemType.ROOT
+ onRoot = dnItem.itemType == nwItemType.ROOT
+ isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem
+ if (isSame or isNone or isNote) and not (onFile and isOnTop) and not isRoot:
+ logger.debug("Drag'n'drop of item %s accepted" % sHandle)
+ QTreeWidget.dropEvent(self, theEvent)
+ if isNone:
+ self._moveOrphanedItem(sHandle, dHandle)
+ self._cleanOrphanedRoot()
+ else:
+ self._updateItemParent(sHandle)
+ if not isSame:
+ logger.debug("Item %s class has been changed from %s to %s" % (
+ sHandle,
+ snItem.itemClass.name,
+ dnItem.itemClass.name
+ ))
+ snItem.setClass(dnItem.itemClass)
+ self.setTreeItemValues(sHandle)
+ else:
+ logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
+ self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
+
+ return
+
##
# Internal Functions
##
@@ -717,69 +798,61 @@ class GuiProjectTree(QTreeWidget):
self.theProject.setProjectChanged(True)
return
- ##
- # Event Overloading
- ##
-
- def mousePressEvent(self, theEvent):
- """Overload mousePressEvent to clear selection if clicking the
- mouse in a blank area of the tree view.
- """
- QTreeWidget.mousePressEvent(self, theEvent)
- selItem = self.indexAt(theEvent.pos())
- if not selItem.isValid():
- self.clearSelection()
- return
-
- def dropEvent(self, theEvent):
- """Overload the drop of dragged item event to check whether the
- drop is allowed or not. Disallowed drops are cancelled.
- """
- sHandle = self.getSelectedHandle()
- if sHandle is None:
- logger.error("No handle selected")
- return
-
- dIndex = self.indexAt(theEvent.pos())
- if not dIndex.isValid():
- logger.error("Invalid drop index")
- return
-
- dItem = self.itemFromIndex(dIndex)
- dHandle = dItem.data(self.C_NAME, Qt.UserRole)
- snItem = self.theProject.projTree[sHandle]
- dnItem = self.theProject.projTree[dHandle]
- if dnItem is None:
- self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
- return
-
- isSame = snItem.itemClass == dnItem.itemClass
- isNone = snItem.itemClass == nwItemClass.NO_CLASS
- isNote = snItem.itemLayout == nwItemLayout.NOTE
- onFile = dnItem.itemType == nwItemType.FILE
- isRoot = snItem.itemType == nwItemType.ROOT
- onRoot = dnItem.itemType == nwItemType.ROOT
- isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem
- if (isSame or isNone or isNote) and not (onFile and isOnTop) and not isRoot:
- logger.debug("Drag'n'drop of item %s accepted" % sHandle)
- QTreeWidget.dropEvent(self, theEvent)
- if isNone:
- self._moveOrphanedItem(sHandle, dHandle)
- self._cleanOrphanedRoot()
- else:
- self._updateItemParent(sHandle)
- if not isSame:
- logger.debug("Item %s class has been changed from %s to %s" % (
- sHandle,
- snItem.itemClass.name,
- dnItem.itemClass.name
- ))
- snItem.setClass(dnItem.itemClass)
- self.setTreeItemValues(sHandle)
- else:
- logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
- self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
-
- return
-
# END Class GuiProjectTree
+
+class GuiProjectTreeMenu(QMenu):
+
+ def __init__(self, theTree):
+ QMenu.__init__(self, theTree)
+
+ self.theTree = theTree
+ self.theItem = theItem
+
+ self.editItem = QAction("Edit Item", self)
+ self.editItem.triggered.connect(self._doEditItem)
+ self.addAction(self.editItem)
+
+ self.toggleExp = QAction("Toggle Exported", self)
+ self.toggleExp.triggered.connect(self._doToggleExported)
+ self.addAction(self.toggleExp)
+
+ self.addSeparator()
+
+ self.newFolder = QAction("New Folder", self)
+ self.newFolder.triggered.connect(self._doMakeFolder)
+ self.addAction(self.newFolder)
+
+ self.newFile = QAction("New File", self)
+ self.newFile.triggered.connect(self._doMakeFile)
+ self.addAction(self.newFile)
+
+ self.deleteItem = QAction("Delete Item", self)
+ self.deleteItem.triggered.connect(self._doDeleteItem)
+ self.addAction(self.deleteItem)
+
+ return
+
+ def updateFromItem(self, theItem):
+ self.theItem = theItem
+ return
+
+ ##
+ # Slots
+ ##
+
+ def _doEditItem(self):
+ return
+
+ def _doDeleteItem(self):
+ return
+
+ def _doMakeFolder(self):
+ return
+
+ def _doMakeFile(self):
+ return
+
+ def _doToggleExported(self):
+ return
+
+# END Class GuiProjectTreeMenu
From 72e5778e4f9fff6d2a032a1e048ef56934e1442d Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 4 Jun 2020 17:09:36 +0200
Subject: [PATCH 08/32] Context menu now works for basic functions
---
nw/gui/projtree.py | 125 +++++++++++++++++++++++++++++++++++----------
1 file changed, 98 insertions(+), 27 deletions(-)
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index b6b26143..5a7c9032 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -339,7 +339,7 @@ class GuiProjectTree(QTreeWidget):
return True
- def deleteItem(self, tHandle=None, alreadyAsked=False):
+ def deleteItem(self, tHandle=None, alreadyAsked=False, askForTrash=False):
"""Delete items from the tree. Note that this does not delete
the item from the item tree in the project object. However,
since this is only meta data, there isn't really a need to do
@@ -370,7 +370,6 @@ class GuiProjectTree(QTreeWidget):
if pHandle is not None and pHandle == self.theProject.projTree.trashRoot():
# If the file is in the trash folder already, as the
# user if they want to permanently delete the file.
-
doPermanent = False
if self.mainConf.showGUI and not alreadyAsked:
msgBox = QMessageBox()
@@ -399,17 +398,28 @@ class GuiProjectTree(QTreeWidget):
else:
# The file is not already in the trash folder, so we
# move it there.
+ doTrash = False
+ if self.mainConf.showGUI and askForTrash:
+ msgBox = QMessageBox()
+ msgRes = msgBox.question(
+ self, "Delete File", "Move file '%s' to Trash?" % nwItemS.itemName
+ )
+ if msgRes == QMessageBox.Yes:
+ doTrash = True
+ else:
+ doTrash = True
- if pHandle is None:
- logger.warning("File has no parent item")
+ if doTrash:
+ if pHandle is None:
+ logger.warning("File has no parent item")
- tIndex = trItemP.indexOfChild(trItemS)
- trItemC = trItemP.takeChild(tIndex)
- trItemT.addChild(trItemC)
- nwItemS.setParent(self.theProject.projTree.trashRoot())
+ tIndex = trItemP.indexOfChild(trItemS)
+ trItemC = trItemP.takeChild(tIndex)
+ trItemT.addChild(trItemC)
+ nwItemS.setParent(self.theProject.projTree.trashRoot())
- self.theProject.setProjectChanged(True)
- self.theParent.theIndex.deleteHandle(tHandle)
+ self.theProject.setProjectChanged(True)
+ self.theParent.theIndex.deleteHandle(tHandle)
elif nwItemS.itemType == nwItemType.FOLDER:
logger.debug("User requested folder %s deleted" % tHandle)
@@ -569,7 +579,13 @@ class GuiProjectTree(QTreeWidget):
open a context menu in-place.
"""
selItem = self.itemAt(clickPos)
- self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
+ if isinstance(selItem, QTreeWidgetItem):
+ tHandle = selItem.data(self.C_NAME, Qt.UserRole)
+ tItem = self.theProject.projTree[tHandle]
+ self.setSelectedHandle(tHandle) # Just to be safe
+ if self.ctxMenu.filterActions(tItem):
+ # Only open menu if any actions remain after filter
+ self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
return
##
@@ -806,53 +822,108 @@ class GuiProjectTreeMenu(QMenu):
QMenu.__init__(self, theTree)
self.theTree = theTree
- self.theItem = theItem
+ self.theItem = None
self.editItem = QAction("Edit Item", self)
self.editItem.triggered.connect(self._doEditItem)
self.addAction(self.editItem)
- self.toggleExp = QAction("Toggle Exported", self)
+ self.toggleExp = QAction("Toggle Included Flag", self)
self.toggleExp.triggered.connect(self._doToggleExported)
self.addAction(self.toggleExp)
- self.addSeparator()
-
- self.newFolder = QAction("New Folder", self)
- self.newFolder.triggered.connect(self._doMakeFolder)
- self.addAction(self.newFolder)
-
self.newFile = QAction("New File", self)
self.newFile.triggered.connect(self._doMakeFile)
self.addAction(self.newFile)
+ self.newFolder = QAction("New Folder", self)
+ self.newFolder.triggered.connect(self._doMakeFolder)
+ self.addAction(self.newFolder)
+
self.deleteItem = QAction("Delete Item", self)
self.deleteItem.triggered.connect(self._doDeleteItem)
self.addAction(self.deleteItem)
+ self.emptyTrash = QAction("Empty Trash", self)
+ self.emptyTrash.triggered.connect(self._doEmptyTrash)
+ self.addAction(self.emptyTrash)
+
return
- def updateFromItem(self, theItem):
+ def filterActions(self, theItem):
+ """Update item settings from the nwItem.
+ """
self.theItem = theItem
- return
+ trashHandle = self.theTree.theProject.projTree.trashRoot()
+
+ if theItem is None:
+ return False
+
+ inTrash = theItem.parHandle == trashHandle
+ isTrash = theItem.itemHandle == trashHandle
+ isFile = theItem.itemType == nwItemType.FILE
+ isOrph = isFile and theItem.parHandle is None
+
+ showEdit = not isTrash and not isOrph
+ showExport = isFile and not inTrash and not isOrph
+ showNewFile = not isTrash and not inTrash and not isOrph
+ showNewFolder = not isTrash and not inTrash and not isOrph
+ showDelete = not isTrash
+ showEmpty = isTrash
+
+ self.editItem.setVisible(showEdit)
+ self.toggleExp.setVisible(showExport)
+ self.newFile.setVisible(showNewFile)
+ self.newFolder.setVisible(showNewFolder)
+ self.deleteItem.setVisible(showDelete)
+ self.emptyTrash.setVisible(showEmpty)
+
+ return True
##
# Slots
##
def _doEditItem(self):
- return
-
- def _doDeleteItem(self):
- return
-
- def _doMakeFolder(self):
+ """Forward the edit item call to the main GUI window.
+ """
+ if self.theItem is not None:
+ self.theTree.theParent.editItem()
return
def _doMakeFile(self):
+ """Forward the new file call to the project tree.
+ """
+ if self.theItem is not None:
+ self.theTree.newTreeItem(nwItemType.FILE, None)
+ return
+
+ def _doMakeFolder(self):
+ """Forward the new folder call to the project tree.
+ """
+ if self.theItem is not None:
+ self.theTree.newTreeItem(nwItemType.FOLDER, None)
return
def _doToggleExported(self):
+ """Flip the isExported flag of the current item.
+ """
+ if self.theItem is not None:
+ self.theItem.setExported(not self.theItem.isExported)
+ self.theTree.setTreeItemValues(self.theItem.itemHandle)
+ return
+
+ def _doDeleteItem(self):
+ """Forward the delete item call to the project tree.
+ """
+ if self.theItem is not None:
+ self.theTree.deleteItem(askForTrash=True)
+ return
+
+ def _doEmptyTrash(self):
+ """Forward the delete item call to the project tree.
+ """
+ self.theTree.emptyTrash()
return
# END Class GuiProjectTreeMenu
From 322665680c9931910d97066b2ed19b0b4bd569e6 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 4 Jun 2020 18:03:18 +0200
Subject: [PATCH 09/32] Bumped dev branch to 0.8rc1 and updated changelog
---
CHANGELOG.md | 7 +++++++
docs/source/conf.py | 4 ++--
nw/__init__.py | 4 ++--
setup.py | 2 +-
4 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 841723c8..4d90087a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# novelWriter ChangeLog
+## Version 0.8 [2020-xx-xx]
+
+**User Interface**
+
+* A details panel below the Outline tree view has been added. The panel shows all the information of a selected row in the tree view above, including hidden columns, and some additional information. The tags and references also become clickable links that when clicked will open in the document viewer. PR #281.
+
+
## Version 0.7 [2020-06-01]
**Bugfixes**
diff --git a/docs/source/conf.py b/docs/source/conf.py
index d54df48e..6c122457 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -24,9 +24,9 @@ copyright = "2018-2020, Veronica Berglyd Olsen"
author = "Veronica Berglyd Olsen"
# The short X.Y version
-version = "0.7.0"
+version = "0.8.0"
# The full version, including alpha/beta/rc tags
-release = "0.7.0"
+release = "0.8.0rc1"
# -- General configuration ---------------------------------------------------
diff --git a/nw/__init__.py b/nw/__init__.py
index e0696373..0b01a8f1 100644
--- a/nw/__init__.py
+++ b/nw/__init__.py
@@ -40,8 +40,8 @@ __package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen"
__license__ = "GPLv3"
-__version__ = "0.7.0"
-__hexversion__ = "0x000700f0"
+__version__ = "0.8.0rc1"
+__hexversion__ = "0x000800c1"
__date__ = "2020-06-01"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
diff --git a/setup.py b/setup.py
index 46e916af..137ae661 100755
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ with open("README.md", "r") as inFile:
setuptools.setup(
name = "novelWriter",
- version = "0.7",
+ version = "0.8rc1",
author = "Veronica Berglyd Olsen",
author_email = "code@vkbo.net",
description = "A markdown-like document editor for writing novels",
From f75028e4a76df41364e0c12e075e337dd0ce7037 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 4 Jun 2020 18:04:40 +0200
Subject: [PATCH 10/32] Updated changelog
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d90087a..bfc35434 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
**User Interface**
* A details panel below the Outline tree view has been added. The panel shows all the information of a selected row in the tree view above, including hidden columns, and some additional information. The tags and references also become clickable links that when clicked will open in the document viewer. PR #281.
+* Added a context menu to the project tree for easier access to some of the most use actions on the tree. PR #282.
## Version 0.7 [2020-06-01]
From 4c9e3f3440a347594085e5b4f9d4672347b8985b Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 4 Jun 2020 19:15:22 +0200
Subject: [PATCH 11/32] New files are now inserted after the file selected, if
a file is selected
---
nw/gui/projtree.py | 23 +++++++++++++++++------
sample/nwProject.nwx | 2 +-
2 files changed, 18 insertions(+), 7 deletions(-)
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index 9b95d8bb..97b87a2e 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -145,6 +145,7 @@ class GuiProjectTree(QTreeWidget):
meta data is set correctly to ensure a valid project tree.
"""
pHandle = self.getSelectedHandle()
+ nHandle = None
if not self.theParent.hasProject:
return False
@@ -193,6 +194,7 @@ class GuiProjectTree(QTreeWidget):
# the new file will be a sibling
pItem = self.theProject.projTree[pHandle]
if pItem.itemType == nwItemType.FILE:
+ nHandle = pHandle
pHandle = pItem.parHandle
# If we again has no home, give up
@@ -218,18 +220,18 @@ class GuiProjectTree(QTreeWidget):
return False
# Add the new item to the tree
- self.revealTreeItem(tHandle)
+ self.revealTreeItem(tHandle, nHandle)
self.theParent.editItem()
return True
- def revealTreeItem(self, tHandle):
+ def revealTreeItem(self, tHandle, nHandle=None):
"""Reveal a newly added project item in the project tree.
"""
nwItem = self.theProject.projTree[tHandle]
- trItem = self._addTreeItem(nwItem)
+ trItem = self._addTreeItem(nwItem, nHandle)
pHandle = nwItem.parHandle
- if pHandle is not None and pHandle in self.theMap.keys():
+ if pHandle is not None and pHandle in self.theMap:
self.theMap[pHandle].setExpanded(True)
self.clearSelection()
trItem.setSelected(True)
@@ -677,7 +679,7 @@ class GuiProjectTree(QTreeWidget):
self._scanChildren(theList, theItem.child(i), i)
return theList
- def _addTreeItem(self, nwItem):
+ def _addTreeItem(self, nwItem, nHandle=None):
"""Create a QTreeWidgetItem from an NWItem and add it to the
project tree.
"""
@@ -713,7 +715,16 @@ class GuiProjectTree(QTreeWidget):
self._addOrphanedRoot()
self.orphRoot.addChild(newItem)
else:
- self.theMap[pHandle].addChild(newItem)
+ byIndex = -1
+ if nHandle is not None and nHandle in self.theMap:
+ try:
+ byIndex = self.theMap[pHandle].indexOfChild(self.theMap[nHandle])
+ except:
+ logger.error("Failed to get index of item with handle %s" % nHandle)
+ if byIndex >= 0:
+ self.theMap[pHandle].insertChild(byIndex+1, newItem)
+ else:
+ self.theMap[pHandle].addChild(newItem)
self.propagateCount(tHandle, nwItem.wordCount)
self.setTreeItemValues(tHandle)
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index 092d28d1..b7b0bde0 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Sample Project
Sample Project
From 8e7c3dcb4b4603123fc3ebe544e38b13b060ae0d Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 4 Jun 2020 20:44:51 +0200
Subject: [PATCH 12/32] Remove debug message
---
nw/gui/outline.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/nw/gui/outline.py b/nw/gui/outline.py
index 368b0403..bb080c1a 100644
--- a/nw/gui/outline.py
+++ b/nw/gui/outline.py
@@ -207,7 +207,6 @@ class GuiOutline(QTreeWidget):
if selItems:
tHandle = selItems[0].data(self.colIndex[nwOutline.TITLE], Qt.UserRole)
sTitle = selItems[0].data(self.colIndex[nwOutline.LINE], Qt.UserRole)
- logger.verbose("User selected entry %s:%s" % (tHandle, sTitle))
self.theParent.projMeta.showItem(tHandle, sTitle)
return
From 0e467d5fb18cf9fd249c5560302491454dd11710 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 4 Jun 2020 21:27:56 +0200
Subject: [PATCH 13/32] Added functions for calculating scaled sizes
---
nw/config.py | 13 ++++++++++++-
nw/gui/theme.py | 2 ++
2 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/nw/config.py b/nw/config.py
index 66fdb649..b668f42d 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -88,6 +88,7 @@ class Config:
self.guiLang = "en" # Hardcoded for now
self.guiFont = ""
self.guiFontSize = 11
+ self.guiScale = 1.0
## Sizes
self.winGeometry = [1100, 650]
@@ -197,7 +198,17 @@ class Config:
return
##
- # Actions
+ # Methods
+ ##
+
+ def pxInt(self, theSize):
+ return int(self.guiScale*theSize)
+
+ def pxFloat(self, theSize):
+ return self.guiScale*theSize
+
+ ##
+ # Config Actions
##
def initConfig(self, confPath=None, dataPath=None):
diff --git a/nw/gui/theme.py b/nw/gui/theme.py
index 8c736719..c7e66a2f 100644
--- a/nw/gui/theme.py
+++ b/nw/gui/theme.py
@@ -148,6 +148,8 @@ class GuiTheme:
logger.verbose("GUI Font Pixel Size: %d" % self.fontPixelSize)
logger.verbose("GUI Base Icon Size: %d" % self.baseIconSize)
logger.verbose("GUI Text Icon Size: %d" % self.textIconSize)
+ logger.verbose("Text 'N' Height: %d" % self.textNHeight)
+ logger.verbose("Text 'N' Width: %d" % self.textNWidth)
return
From 8b22deb1f47eb09d72843fe13de74301d994538a Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 4 Jun 2020 21:58:35 +0200
Subject: [PATCH 14/32] All fixed sizes should no be passed through the pxInt
function
---
nw/gui/about.py | 16 ++++++------
nw/gui/build.py | 28 ++++++++++++---------
nw/gui/docbars.py | 48 +++++++++++++++++++-----------------
nw/gui/docdetails.py | 15 ++++++++----
nw/gui/doceditor.py | 2 +-
nw/gui/docmerge.py | 8 +++---
nw/gui/docsplit.py | 8 +++---
nw/gui/docviewer.py | 2 +-
nw/gui/itemdetails.py | 10 ++++----
nw/gui/itemeditor.py | 6 ++---
nw/gui/outlinedetails.py | 4 +--
nw/gui/preferences.py | 20 ++++++++-------
nw/gui/projload.py | 17 +++++++------
nw/gui/projsettings.py | 53 ++++++++++++++++++++++++----------------
nw/gui/sessionlog.py | 26 ++++++++------------
nw/gui/statusbar.py | 10 +++++---
nw/guimain.py | 25 ++++++++++---------
17 files changed, 164 insertions(+), 134 deletions(-)
diff --git a/nw/gui/about.py b/nw/gui/about.py
index 653f87ae..5d869c9a 100644
--- a/nw/gui/about.py
+++ b/nw/gui/about.py
@@ -48,22 +48,24 @@ class GuiAbout(QDialog):
self.mainConf = nw.CONFIG
self.theParent = theParent
+ self.theTheme = theParent.theTheme
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
- self.innerBox.setSpacing(16)
+ self.innerBox.setSpacing(self.mainConf.pxInt(16))
self.setWindowTitle("About %s" % nw.__package__)
- self.setMinimumWidth(700)
- self.setMinimumHeight(600)
+ self.setMinimumWidth(self.mainConf.pxInt(650))
+ self.setMinimumHeight(self.mainConf.pxInt(600))
- self.guiDeco = self.theParent.theTheme.loadDecoration("nwicon", (96, 96))
+ iPx = self.mainConf.pxInt(96)
+ self.guiDeco = self.theParent.theTheme.loadDecoration("nwicon", (iPx, iPx))
self.lblName = QLabel("%s" % nw.__package__)
self.lblVers = QLabel("v%s" % nw.__version__)
self.lblDate = QLabel(datetime.strptime(nw.__date__, "%Y-%m-%d").strftime("%x"))
self.leftBox = QVBoxLayout()
- self.leftBox.setSpacing(4)
+ self.leftBox.setSpacing(self.mainConf.pxInt(4))
self.leftBox.addWidget(self.guiDeco, 0, Qt.AlignCenter)
self.leftBox.addWidget(self.lblName, 0, Qt.AlignCenter)
self.leftBox.addWidget(self.lblVers, 0, Qt.AlignCenter)
@@ -74,7 +76,7 @@ class GuiAbout(QDialog):
# Pages
self.pageAbout = QTextBrowser()
self.pageAbout.setOpenExternalLinks(True)
- self.pageAbout.document().setDocumentMargin(16)
+ self.pageAbout.document().setDocumentMargin(self.mainConf.pxInt(16))
# self.pageCredit = QTextBrowser()
# self.pageCredit.setOpenExternalLinks(True)
@@ -82,7 +84,7 @@ class GuiAbout(QDialog):
self.pageLicense = QTextBrowser()
self.pageLicense.setOpenExternalLinks(True)
- self.pageLicense.document().setDocumentMargin(16)
+ self.pageLicense.document().setDocumentMargin(self.mainConf.pxInt(16))
# Main Tab Area
self.tabBox = QTabWidget()
diff --git a/nw/gui/build.py b/nw/gui/build.py
index f072b6ea..30bd4cf5 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -77,13 +77,17 @@ class GuiBuildNovel(QDialog):
self.htmlStyle = [] # List of html styles
self.nwdText = [] # List of markdown documents
+ x800 = self.mainConf.pxInt(800)
+ x900 = self.mainConf.pxInt(900)
+ xFmt = self.mainConf.pxInt(220)
+
self.setWindowTitle("Build Novel Project")
- self.setMinimumWidth(900)
- self.setMinimumHeight(800)
+ self.setMinimumWidth(x900)
+ self.setMinimumHeight(x800)
self.resize(
- self.optState.getInt("GuiBuildNovel", "winWidth", 900),
- self.optState.getInt("GuiBuildNovel", "winHeight", 800)
+ self.optState.getInt("GuiBuildNovel", "winWidth", x900),
+ self.optState.getInt("GuiBuildNovel", "winHeight", x800)
)
self.outerBox = QHBoxLayout()
@@ -115,7 +119,7 @@ class GuiBuildNovel(QDialog):
self.fmtTitle = QLineEdit()
self.fmtTitle.setMaxLength(200)
- self.fmtTitle.setFixedWidth(220)
+ self.fmtTitle.setFixedWidth(xFmt)
self.fmtTitle.setToolTip(fmtHelp)
self.fmtTitle.setText(
self._reFmtCodes(self.theProject.titleFormat["title"])
@@ -123,7 +127,7 @@ class GuiBuildNovel(QDialog):
self.fmtChapter = QLineEdit()
self.fmtChapter.setMaxLength(200)
- self.fmtChapter.setFixedWidth(220)
+ self.fmtChapter.setFixedWidth(xFmt)
self.fmtChapter.setToolTip(fmtHelp)
self.fmtChapter.setText(
self._reFmtCodes(self.theProject.titleFormat["chapter"])
@@ -131,7 +135,7 @@ class GuiBuildNovel(QDialog):
self.fmtUnnumbered = QLineEdit()
self.fmtUnnumbered.setMaxLength(200)
- self.fmtUnnumbered.setFixedWidth(220)
+ self.fmtUnnumbered.setFixedWidth(xFmt)
self.fmtUnnumbered.setToolTip(fmtHelp)
self.fmtUnnumbered.setText(
self._reFmtCodes(self.theProject.titleFormat["unnumbered"])
@@ -139,7 +143,7 @@ class GuiBuildNovel(QDialog):
self.fmtScene = QLineEdit()
self.fmtScene.setMaxLength(200)
- self.fmtScene.setFixedWidth(220)
+ self.fmtScene.setFixedWidth(xFmt)
self.fmtScene.setToolTip(fmtHelp + fmtScHelp)
self.fmtScene.setText(
self._reFmtCodes(self.theProject.titleFormat["scene"])
@@ -147,7 +151,7 @@ class GuiBuildNovel(QDialog):
self.fmtSection = QLineEdit()
self.fmtSection.setMaxLength(200)
- self.fmtSection.setFixedWidth(220)
+ self.fmtSection.setFixedWidth(xFmt)
self.fmtSection.setToolTip(fmtHelp + fmtScHelp)
self.fmtSection.setText(
self._reFmtCodes(self.theProject.titleFormat["section"])
@@ -176,7 +180,7 @@ class GuiBuildNovel(QDialog):
## Font Family
self.textFont = QLineEdit()
self.textFont.setReadOnly(True)
- self.textFont.setFixedWidth(182)
+ self.textFont.setFixedWidth(self.mainConf.pxInt(182))
self.textFont.setText(
self.optState.getString("GuiBuildNovel", "textFont", self.mainConf.textFont)
)
@@ -185,7 +189,7 @@ class GuiBuildNovel(QDialog):
self.fontButton.clicked.connect(self._selectFont)
self.textSize = QSpinBox(self)
- self.textSize.setFixedWidth(60)
+ self.textSize.setFixedWidth(5*self.theTheme.textNWidth)
self.textSize.setMinimum(6)
self.textSize.setMaximum(72)
self.textSize.setSingleStep(1)
@@ -894,7 +898,7 @@ class GuiBuildNovelDocView(QTextBrowser):
self.theProject = theProject
self.theParent = theParent
- self.setMinimumWidth(400)
+ self.setMinimumWidth(40*self.theParent.theTheme.textNWidth)
self.setOpenExternalLinks(False)
self.qDocument = self.document()
diff --git a/nw/gui/docbars.py b/nw/gui/docbars.py
index 0923cb91..3e65855d 100644
--- a/nw/gui/docbars.py
+++ b/nw/gui/docbars.py
@@ -53,7 +53,7 @@ class GuiSearchBar(QFrame):
self.theTheme = theParent.theTheme
self.repVisible = False
- self.setContentsMargins(0,0,0,0)
+ self.setContentsMargins(0, 0, 0, 0)
self.mainBox = QGridLayout(self)
self.setLayout(self.mainBox)
@@ -72,24 +72,25 @@ class GuiSearchBar(QFrame):
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.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)
+ 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)
- self.searchBox.setMinimumWidth(180)
- self.replaceBox.setMinimumWidth(180)
+ boxWidth = 16*self.theTheme.textNWidth
+ self.searchBox.setMinimumWidth(boxWidth)
+ self.replaceBox.setMinimumWidth(boxWidth)
self._replaceVisible(False)
@@ -175,15 +176,18 @@ class GuiNoticeBar(QFrame):
logger.debug("Initialising GuiNoticeBar ...")
- self.mainConf = nw.CONFIG
- self.theParent = theParent
- self.theTheme = theParent.theTheme
+ self.mainConf = nw.CONFIG
+ self.theParent = theParent
+ self.theTheme = theParent.theTheme
- self.setContentsMargins(0,0,0,0)
+ self.setContentsMargins(0, 0, 0, 0)
self.setFrameShape(QFrame.Box)
+ m8 = self.mainConf.pxInt(8)
+ m2 = self.mainConf.pxInt(2)
+
self.mainBox = QHBoxLayout(self)
- self.mainBox.setContentsMargins(8,2,2,2)
+ self.mainBox.setContentsMargins(m8, m2, m2, m2)
self.noteLabel = QLabel("")
diff --git a/nw/gui/docdetails.py b/nw/gui/docdetails.py
index 09b43426..7c4ec8f7 100644
--- a/nw/gui/docdetails.py
+++ b/nw/gui/docdetails.py
@@ -44,12 +44,17 @@ class GuiDocViewDetails(QWidget):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
+ self.theTheme = theParent.theTheme
self.currHandle = None
+ x4 = self.mainConf.pxInt(4)
+ x80 = self.mainConf.pxInt(80)
+ iPx = self.theTheme.textIconSize
+
self.outerBox = QGridLayout(self)
- self.outerBox.setContentsMargins(0,0,0,0)
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
self.outerBox.setHorizontalSpacing(0)
- self.outerBox.setVerticalSpacing(4)
+ self.outerBox.setVerticalSpacing(x4)
self.refLabel = QLabel("Referenced By", self)
@@ -58,7 +63,7 @@ class GuiDocViewDetails(QWidget):
self.showHide.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.showHide.setArrowType(Qt.DownArrow)
self.showHide.setCheckable(True)
- self.showHide.setIconSize(QSize(16,16))
+ self.showHide.setIconSize(QSize(iPx, iPx))
self.showHide.toggled.connect(self._doShowHide)
self.isSticky = QCheckBox("Sticky")
@@ -75,7 +80,7 @@ class GuiDocViewDetails(QWidget):
self.scrollBox.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scrollBox.setFrameStyle(QFrame.NoFrame)
self.scrollBox.setWidgetResizable(True)
- self.scrollBox.setFixedHeight(80)
+ self.scrollBox.setFixedHeight(x80)
self.scrollBox.setWidget(self.refList)
self.outerBox.addWidget(self.showHide, 0, 0)
@@ -85,7 +90,7 @@ class GuiDocViewDetails(QWidget):
self.outerBox.setColumnStretch(1, 1)
self.setLayout(self.outerBox)
- self.setContentsMargins(0,0,0,0)
+ self.setContentsMargins(0, 0, 0, 0)
self._doShowHide(self.mainConf.showRefPanel)
diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py
index 1b3f81bb..884c4ae3 100644
--- a/nw/gui/doceditor.py
+++ b/nw/gui/doceditor.py
@@ -98,7 +98,7 @@ class GuiDocEditor(QTextEdit):
# Editor State
self.hasSelection = False
- self.setMinimumWidth(300)
+ self.setMinimumWidth(self.mainConf.pxInt(300))
self.setAcceptRichText(False)
# Custom Shortcuts
diff --git a/nw/gui/docmerge.py b/nw/gui/docmerge.py
index 73b15a6a..1294501f 100644
--- a/nw/gui/docmerge.py
+++ b/nw/gui/docmerge.py
@@ -62,8 +62,8 @@ class GuiDocMerge(QDialog):
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
- self.listBox.setMinimumWidth(400)
- self.listBox.setMinimumHeight(180)
+ self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
+ self.listBox.setMinimumHeight(self.mainConf.pxInt(180))
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doMerge)
@@ -72,9 +72,9 @@ class GuiDocMerge(QDialog):
self.outerBox.setSpacing(0)
self.outerBox.addWidget(self.headLabel)
self.outerBox.addWidget(self.helpLabel)
- self.outerBox.addSpacing(8)
+ self.outerBox.addSpacing(self.mainConf.pxInt(8))
self.outerBox.addWidget(self.listBox)
- self.outerBox.addSpacing(12)
+ self.outerBox.addSpacing(self.mainConf.pxInt(12))
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
diff --git a/nw/gui/docsplit.py b/nw/gui/docsplit.py
index 702e8f17..98ef95d7 100644
--- a/nw/gui/docsplit.py
+++ b/nw/gui/docsplit.py
@@ -62,8 +62,8 @@ class GuiDocSplit(QDialog):
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
- self.listBox.setMinimumWidth(400)
- self.listBox.setMinimumHeight(180)
+ self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
+ self.listBox.setMinimumHeight(self.mainConf.pxInt(180))
self.splitLevel = QComboBox(self)
self.splitLevel.addItem("Split on Header Level 1 (Title)", 1)
@@ -84,10 +84,10 @@ class GuiDocSplit(QDialog):
self.outerBox.setSpacing(0)
self.outerBox.addWidget(self.headLabel)
self.outerBox.addWidget(self.helpLabel)
- self.outerBox.addSpacing(8)
+ self.outerBox.addSpacing(self.mainConf.pxInt(8))
self.outerBox.addWidget(self.listBox)
self.outerBox.addWidget(self.splitLevel)
- self.outerBox.addSpacing(12)
+ self.outerBox.addSpacing(self.mainConf.pxInt(12))
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py
index fb1a9c99..70163b7b 100644
--- a/nw/gui/docviewer.py
+++ b/nw/gui/docviewer.py
@@ -53,7 +53,7 @@ class GuiDocViewer(QTextBrowser):
self.theHandle = None
self.qDocument = self.document()
- self.setMinimumWidth(300)
+ self.setMinimumWidth(self.mainConf.pxInt(300))
self.setOpenExternalLinks(False)
self.initViewer()
diff --git a/nw/gui/itemdetails.py b/nw/gui/itemdetails.py
index 7335a596..182fb3fd 100644
--- a/nw/gui/itemdetails.py
+++ b/nw/gui/itemdetails.py
@@ -170,11 +170,11 @@ class GuiItemDetails(QWidget):
self.mainBox.addWidget(self.pCountName, 3, 3, 1, 1)
self.mainBox.addWidget(self.pCountData, 3, 4, 1, 1)
- self.mainBox.setColumnStretch(0,0)
- self.mainBox.setColumnStretch(1,0)
- self.mainBox.setColumnStretch(2,1)
- self.mainBox.setColumnStretch(3,0)
- self.mainBox.setColumnStretch(4,0)
+ self.mainBox.setColumnStretch(0, 0)
+ self.mainBox.setColumnStretch(1, 0)
+ self.mainBox.setColumnStretch(2, 1)
+ self.mainBox.setColumnStretch(3, 0)
+ self.mainBox.setColumnStretch(4, 0)
# Make sure the columns for flags and counts don't resize too often
flagWidth = self.theTheme.getTextWidth("Mm", self.fntValue)
diff --git a/nw/gui/itemeditor.py b/nw/gui/itemeditor.py
index 7eb54b14..65c4cde9 100644
--- a/nw/gui/itemeditor.py
+++ b/nw/gui/itemeditor.py
@@ -61,8 +61,8 @@ class GuiItemEditor(QDialog):
# Item Label
self.editName = QLineEdit()
- self.editName.setMinimumWidth(220)
- self.editName.setMaxLength(200)
+ self.editName.setMinimumWidth(self.mainConf.pxInt(220))
+ self.editName.setMaxLength(self.mainConf.pxInt(200))
# Item Status
self.editStatus = QComboBox()
@@ -135,7 +135,7 @@ class GuiItemEditor(QDialog):
self.mainForm.addWidget(self.textExport, 3, 0, 1, 2)
self.mainForm.addWidget(self.editExport, 3, 2, 1, 1)
- self.outerBox.setSpacing(16)
+ self.outerBox.setSpacing(self.mainConf.pxInt(16))
self.outerBox.addLayout(self.mainForm)
self.outerBox.addStretch(1)
self.outerBox.addWidget(self.buttonBox)
diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py
index 33048837..739b1c37 100644
--- a/nw/gui/outlinedetails.py
+++ b/nw/gui/outlinedetails.py
@@ -63,8 +63,8 @@ class GuiOutlineDetails(QScrollArea):
minTitle = 30*self.theTheme.textNWidth
maxTitle = 40*self.theTheme.textNWidth
wCount = self.theTheme.getTextWidth("999,999")
- hSpace = int(0.8*self.theTheme.textNWidth)
- vSpace = int(0.2*self.theTheme.textNHeight)
+ hSpace = int(self.mainConf.pxInt(10))
+ vSpace = int(self.mainConf.pxInt(4))
# Details Area
self.titleLabel = QLabel("Title")
diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py
index a05e5eb0..55d62fe9 100644
--- a/nw/gui/preferences.py
+++ b/nw/gui/preferences.py
@@ -143,7 +143,7 @@ class GuiConfigEditGeneralTab(QWidget):
## Select Theme
self.selectTheme = QComboBox()
- self.selectTheme.setMinimumWidth(200)
+ self.selectTheme.setMinimumWidth(self.mainConf.pxInt(200))
self.theThemes = self.theTheme.listThemes()
for themeDir, themeName in self.theThemes:
self.selectTheme.addItem(themeName, themeDir)
@@ -159,7 +159,7 @@ class GuiConfigEditGeneralTab(QWidget):
## Select Icon Theme
self.selectIcons = QComboBox()
- self.selectIcons.setMinimumWidth(200)
+ self.selectIcons.setMinimumWidth(self.mainConf.pxInt(200))
self.theIcons = self.theTheme.theIcons.listThemes()
for iconDir, iconName in self.theIcons:
self.selectIcons.addItem(iconName, iconDir)
@@ -185,7 +185,7 @@ class GuiConfigEditGeneralTab(QWidget):
## Font Family
self.guiFont = QLineEdit()
self.guiFont.setReadOnly(True)
- self.guiFont.setFixedWidth(162)
+ self.guiFont.setFixedWidth(self.mainConf.pxInt(162))
self.guiFont.setText(self.mainConf.guiFont)
self.fontButton = QPushButton("...")
self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
@@ -389,7 +389,7 @@ class GuiConfigEditLayoutTab(QWidget):
## Font Family
self.textStyleFont = QLineEdit()
self.textStyleFont.setReadOnly(True)
- self.textStyleFont.setFixedWidth(162)
+ self.textStyleFont.setFixedWidth(self.mainConf.pxInt(162))
self.textStyleFont.setText(self.mainConf.textFont)
self.fontButton = QPushButton("...")
self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
@@ -575,7 +575,7 @@ class GuiConfigEditEditingTab(QWidget):
## Syntax Highlighting
self.selectSyntax = QComboBox()
- self.selectSyntax.setMinimumWidth(200)
+ self.selectSyntax.setMinimumWidth(self.mainConf.pxInt(200))
self.theSyntaxes = self.theTheme.listSyntax()
for syntaxFile, syntaxName in self.theSyntaxes:
self.selectSyntax.addItem(syntaxName, syntaxFile)
@@ -789,10 +789,12 @@ class GuiConfigEditAutoReplaceTab(QWidget):
# ===============
self.mainForm.addGroupLabel("Quotation Style")
+ qWidth = self.mainConf.pxInt(40)
+
## Single Quote Style
self.quoteSingleStyleO = QLineEdit()
self.quoteSingleStyleO.setMaxLength(1)
- self.quoteSingleStyleO.setFixedWidth(40)
+ self.quoteSingleStyleO.setFixedWidth(qWidth)
self.quoteSingleStyleO.setAlignment(Qt.AlignCenter)
self.quoteSingleStyleO.setText(self.mainConf.fmtSingleQuotes[0])
self.mainForm.addRow(
@@ -803,7 +805,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.quoteSingleStyleC = QLineEdit()
self.quoteSingleStyleC.setMaxLength(1)
- self.quoteSingleStyleC.setFixedWidth(40)
+ self.quoteSingleStyleC.setFixedWidth(qWidth)
self.quoteSingleStyleC.setAlignment(Qt.AlignCenter)
self.quoteSingleStyleC.setText(self.mainConf.fmtSingleQuotes[1])
self.mainForm.addRow(
@@ -815,7 +817,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
## Double Quote Style
self.quoteDoubleStyleO = QLineEdit()
self.quoteDoubleStyleO.setMaxLength(1)
- self.quoteDoubleStyleO.setFixedWidth(40)
+ self.quoteDoubleStyleO.setFixedWidth(qWidth)
self.quoteDoubleStyleO.setAlignment(Qt.AlignCenter)
self.quoteDoubleStyleO.setText(self.mainConf.fmtDoubleQuotes[0])
self.mainForm.addRow(
@@ -826,7 +828,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.quoteDoubleStyleC = QLineEdit()
self.quoteDoubleStyleC.setMaxLength(1)
- self.quoteDoubleStyleC.setFixedWidth(40)
+ self.quoteDoubleStyleC.setFixedWidth(qWidth)
self.quoteDoubleStyleC.setAlignment(Qt.AlignCenter)
self.quoteDoubleStyleC.setText(self.mainConf.fmtDoubleQuotes[1])
self.mainForm.addRow(
diff --git a/nw/gui/projload.py b/nw/gui/projload.py
index 63f0be9f..361b5ba8 100644
--- a/nw/gui/projload.py
+++ b/nw/gui/projload.py
@@ -61,17 +61,20 @@ class GuiProjectLoad(QDialog):
self.openState = self.NONE_STATE
self.openPath = None
+ xSp = self.mainConf.pxInt(16)
+ xIc = self.mainConf.pxInt(96)
+
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
- self.outerBox.setSpacing(16)
- self.innerBox.setSpacing(16)
+ self.outerBox.setSpacing(xSp)
+ self.innerBox.setSpacing(xSp)
self.setWindowTitle("Open Project")
- self.setMinimumWidth(650)
- self.setMinimumHeight(400)
+ self.setMinimumWidth(self.mainConf.pxInt(650))
+ self.setMinimumHeight(self.mainConf.pxInt(400))
self.setModal(True)
- self.guiDeco = self.theTheme.loadDecoration("nwicon", (96, 96))
+ self.guiDeco = self.theTheme.loadDecoration("nwicon", (xIc, xIc))
self.innerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.projectForm = QGridLayout()
@@ -113,8 +116,8 @@ class GuiProjectLoad(QDialog):
self.projectForm.setColumnStretch(0, 0)
self.projectForm.setColumnStretch(1, 1)
self.projectForm.setColumnStretch(2, 0)
- self.projectForm.setVerticalSpacing(4)
- self.projectForm.setHorizontalSpacing(8)
+ self.projectForm.setVerticalSpacing(self.mainConf.pxInt(4))
+ self.projectForm.setHorizontalSpacing(self.mainConf.pxInt(8))
self.innerBox.addLayout(self.projectForm)
diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py
index a1c28ca4..2a4dc680 100644
--- a/nw/gui/projsettings.py
+++ b/nw/gui/projsettings.py
@@ -119,6 +119,7 @@ class GuiProjectEditMain(QWidget):
def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
+ self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
@@ -129,9 +130,12 @@ class GuiProjectEditMain(QWidget):
self.mainForm.addGroupLabel("Project Settings")
+ xW = self.mainConf.pxInt(250)
+ xH = self.mainConf.pxInt(100)
+
self.editName = QLineEdit()
self.editName.setMaxLength(200)
- self.editName.setFixedWidth(250)
+ self.editName.setFixedWidth(xW)
self.editName.setText(self.theProject.projName)
self.mainForm.addRow(
"Working title",
@@ -141,7 +145,7 @@ class GuiProjectEditMain(QWidget):
self.editTitle = QLineEdit()
self.editTitle.setMaxLength(200)
- self.editTitle.setFixedWidth(250)
+ self.editTitle.setFixedWidth(xW)
self.editTitle.setText(self.theProject.bookTitle)
self.mainForm.addRow(
"Novel title",
@@ -154,8 +158,8 @@ class GuiProjectEditMain(QWidget):
for bookAuthor in self.theProject.bookAuthors:
bookAuthors += bookAuthor+"\n"
self.editAuthors.setPlainText(bookAuthors)
- self.editAuthors.setFixedHeight(100)
- self.editAuthors.setFixedWidth(250)
+ self.editAuthors.setFixedHeight(xH)
+ self.editAuthors.setFixedWidth(xW)
self.mainForm.addRow(
"Author(s)",
self.editAuthors,
@@ -179,9 +183,12 @@ class GuiProjectEditMeta(QWidget):
def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
+ self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
+ xInd = self.mainConf.pxInt(8)
+
# The Form
self.mainForm = QGridLayout()
self.setLayout(self.mainForm)
@@ -189,17 +196,17 @@ class GuiProjectEditMeta(QWidget):
self.headLabel = QLabel("Project Details")
self.nameLabel = QLabel("Working title:")
- self.nameLabel.setIndent(8)
+ self.nameLabel.setIndent(xInd)
self.nameValue = QLabel(self.theProject.projName)
self.nameValue.setWordWrap(True)
self.pathLabel = QLabel("Project path:")
- self.pathLabel.setIndent(8)
+ self.pathLabel.setIndent(xInd)
self.pathValue = QLabel(self.theProject.projPath)
self.pathValue.setWordWrap(True)
self.revLabel = QLabel("Revision count:")
- self.revLabel.setIndent(8)
+ self.revLabel.setIndent(xInd)
self.revValue = QLabel("{:n}".format(self.theProject.saveCount))
self.statsLabel = QLabel("Project Stats")
@@ -207,19 +214,19 @@ class GuiProjectEditMeta(QWidget):
nR, nD, nF = self.theProject.projTree.countTypes()
self.nRootLabel = QLabel("Root folders:")
- self.nRootLabel.setIndent(8)
+ self.nRootLabel.setIndent(xInd)
self.nRootValue = QLabel("{:n}".format(nR))
self.nDirLabel = QLabel("Folders:")
- self.nDirLabel.setIndent(8)
+ self.nDirLabel.setIndent(xInd)
self.nDirValue = QLabel("{:n}".format(nD))
self.nFileLabel = QLabel("Documents:")
- self.nFileLabel.setIndent(8)
+ self.nFileLabel.setIndent(xInd)
self.nFileValue = QLabel("{:n}".format(nF))
self.wordsLabel = QLabel("Word count:")
- self.wordsLabel.setIndent(8)
+ self.wordsLabel.setIndent(xInd)
self.wordsValue = QLabel("{:n}".format(self.theProject.currWCount))
self.mainForm.addWidget(self.headLabel, 0, 0, 1, 2, Qt.AlignTop)
@@ -240,8 +247,8 @@ class GuiProjectEditMeta(QWidget):
self.mainForm.addWidget(self.wordsLabel, 8, 0, 1, 1, Qt.AlignTop)
self.mainForm.addWidget(self.wordsValue, 8, 1, 1, 1, Qt.AlignTop)
- self.mainForm.setVerticalSpacing(6)
- self.mainForm.setHorizontalSpacing(12)
+ self.mainForm.setVerticalSpacing(self.mainConf.pxInt(6))
+ self.mainForm.setHorizontalSpacing(self.mainConf.pxInt(12))
self.mainForm.setColumnStretch(0, 0)
self.mainForm.setColumnStretch(1, 1)
self.mainForm.setRowStretch(10, 1)
@@ -255,8 +262,10 @@ class GuiProjectEditStatus(QWidget):
def __init__(self, theParent, theProject, isStatus):
QWidget.__init__(self, theParent)
+ self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
+ self.theTheme = theParent.theTheme
if isStatus:
self.theStatus = self.theProject.statusItems
else:
@@ -267,6 +276,8 @@ class GuiProjectEditStatus(QWidget):
self.colChanged = False
self.selColour = None
+ self.iPx = self.theTheme.textIconSize
+
self.outerBox = QVBoxLayout()
self.mainBox = QHBoxLayout()
self.mainForm = QVBoxLayout()
@@ -285,8 +296,8 @@ class GuiProjectEditStatus(QWidget):
self.newButton = QPushButton("New")
self.delButton = QPushButton("Delete")
self.saveButton = QPushButton("Save")
- self.colPixmap = QPixmap(16,16)
- self.colPixmap.fill(QColor(120,120,120))
+ self.colPixmap = QPixmap(self.iPx, self.iPx)
+ self.colPixmap.fill(QColor(120, 120, 120))
self.colButton = QPushButton(QIcon(self.colPixmap),"Colour")
self.colButton.setIconSize(self.colPixmap.rect().size())
@@ -339,7 +350,7 @@ class GuiProjectEditStatus(QWidget):
)
if newCol:
self.selColour = newCol
- colPixmap = QPixmap(16,16)
+ colPixmap = QPixmap(self.iPx, self.iPx)
colPixmap.fill(newCol)
self.colButton.setIcon(QIcon(colPixmap))
self.colButton.setIconSize(colPixmap.rect().size())
@@ -348,7 +359,7 @@ class GuiProjectEditStatus(QWidget):
def _newItem(self):
logger.verbose("New item button clicked")
newItem = self._addItem("New Item", (0, 0, 0), None, 0)
- newItem.setBackground(QBrush(QColor(0,255,0,80)))
+ newItem.setBackground(QBrush(QColor(0, 255, 0, 80)))
self.colChanged = True
return
@@ -387,14 +398,14 @@ class GuiProjectEditStatus(QWidget):
return
def _addItem(self, iName, iCol, oName, nUse):
- newIcon = QPixmap(16,16)
+ newIcon = QPixmap(self.iPx, self.iPx)
newIcon.fill(QColor(*iCol))
newItem = QListWidgetItem()
newItem.setText("%s [%d]" % (iName, nUse))
newItem.setIcon(QIcon(newIcon))
newItem.setData(Qt.UserRole, len(self.colData))
self.listBox.addItem(newItem)
- self.colData.append((iName,iCol[0],iCol[1],iCol[2],oName))
+ self.colData.append((iName, iCol[0], iCol[1], iCol[2], oName))
self.colCounts.append(nUse)
return newItem
@@ -404,8 +415,8 @@ class GuiProjectEditStatus(QWidget):
if selItem is not None:
selIdx = selItem.data(Qt.UserRole)
selVal = self.colData[selIdx]
- self.selColour = QColor(selVal[1],selVal[2],selVal[3])
- newIcon = QPixmap(16,16)
+ self.selColour = QColor(selVal[1], selVal[2], selVal[3])
+ newIcon = QPixmap(self.iPx, self.iPx)
newIcon.fill(self.selColour)
self.editName.setText(selVal[0])
self.colButton.setIcon(QIcon(newIcon))
diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py
index fdda4166..2e31d02f 100644
--- a/nw/gui/sessionlog.py
+++ b/nw/gui/sessionlog.py
@@ -61,32 +61,26 @@ class GuiSessionLogView(QDialog):
self.bottomBox = QHBoxLayout()
self.setWindowTitle("Session Log")
- self.setMinimumWidth(420)
- self.setMinimumHeight(400)
+ self.setMinimumWidth(self.mainConf.pxInt(420))
+ self.setMinimumHeight(self.mainConf.pxInt(400))
- widthCol0 = self.optState.validIntRange(
- self.optState.getInt("GuiSession", "widthCol0", 180), 30, 999, 180
- )
- widthCol1 = self.optState.validIntRange(
- self.optState.getInt("GuiSession", "widthCol1", 80), 30, 999, 80
- )
- widthCol2 = self.optState.validIntRange(
- self.optState.getInt("GuiSession", "widthCol2", 80), 30, 999, 80
- )
+ widthCol0 = self.optState.getInt("GuiSession", "widthCol0", self.mainConf.pxInt(180))
+ widthCol1 = self.optState.getInt("GuiSession", "widthCol1", self.mainConf.pxInt(80))
+ widthCol2 = self.optState.getInt("GuiSession", "widthCol2", self.mainConf.pxInt(80))
self.listBox = QTreeWidget()
self.listBox.setHeaderLabels(["Session Start","Length","Words",""])
self.listBox.setIndentation(0)
- self.listBox.setColumnWidth(0,widthCol0)
- self.listBox.setColumnWidth(1,widthCol1)
- self.listBox.setColumnWidth(2,widthCol2)
- self.listBox.setColumnWidth(3,0)
+ self.listBox.setColumnWidth(0, widthCol0)
+ self.listBox.setColumnWidth(1, widthCol1)
+ self.listBox.setColumnWidth(2, widthCol2)
+ self.listBox.setColumnWidth(3, 0)
hHeader = self.listBox.headerItem()
hHeader.setTextAlignment(1,Qt.AlignRight)
hHeader.setTextAlignment(2,Qt.AlignRight)
- self.monoFont = QFont("Monospace",10)
+ self.monoFont = QFont("Monospace", 10)
sortValid = (Qt.AscendingOrder, Qt.DescendingOrder)
sortCol = self.optState.validIntRange(
diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py
index 129ab9a8..a7fbc342 100644
--- a/nw/gui/statusbar.py
+++ b/nw/gui/statusbar.py
@@ -66,12 +66,14 @@ class GuiMainStatus(QStatusBar):
# Permanent Widgets
# =================
+ xM = self.mainConf.pxInt(8)
+
## The Spell Checker Language
self.langIcon = QLabel("")
self.langText = QLabel("None")
self.langIcon.setPixmap(self.theTheme.getPixmap("status_lang", (iPx, iPx)))
self.langIcon.setContentsMargins(0, 0, 0, 0)
- self.langText.setContentsMargins(0, 0, 8, 0)
+ self.langText.setContentsMargins(0, 0, xM, 0)
self.addPermanentWidget(self.langIcon)
self.addPermanentWidget(self.langText)
@@ -79,7 +81,7 @@ class GuiMainStatus(QStatusBar):
self.docIcon = StatusLED(colNone, colTrue, colFalse, iPx, iPx, self)
self.docText = QLabel("Editor")
self.docIcon.setContentsMargins(0, 0, 0, 0)
- self.docText.setContentsMargins(0, 0, 8, 0)
+ self.docText.setContentsMargins(0, 0, xM, 0)
self.addPermanentWidget(self.docIcon)
self.addPermanentWidget(self.docText)
@@ -87,7 +89,7 @@ class GuiMainStatus(QStatusBar):
self.projIcon = StatusLED(colNone, colTrue, colFalse, iPx, iPx, self)
self.projText = QLabel("Project")
self.projIcon.setContentsMargins(0, 0, 0, 0)
- self.projText.setContentsMargins(0, 0, 8, 0)
+ self.projText.setContentsMargins(0, 0, xM, 0)
self.addPermanentWidget(self.projIcon)
self.addPermanentWidget(self.projText)
@@ -96,7 +98,7 @@ class GuiMainStatus(QStatusBar):
self.statsText = QLabel("")
self.statsIcon.setPixmap(self.theTheme.getPixmap("status_stats", (iPx, iPx)))
self.statsIcon.setContentsMargins(0, 0, 0, 0)
- self.statsText.setContentsMargins(0, 0, 8, 0)
+ self.statsText.setContentsMargins(0, 0, xM, 0)
self.addPermanentWidget(self.statsIcon)
self.addPermanentWidget(self.statsText)
diff --git a/nw/guimain.py b/nw/guimain.py
index e9d9b81d..c74d4e1c 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -108,15 +108,15 @@ class GuiMain(QMainWindow):
# Assemble Main Window
self.treePane = QWidget()
self.treeBox = QVBoxLayout()
- self.treeBox.setContentsMargins(0,0,0,0)
+ self.treeBox.setContentsMargins(0, 0, 0, 0)
self.treeBox.addWidget(self.treeView)
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(2)
+ self.docEdit.setContentsMargins(0, 0, 0, 0)
+ self.docEdit.setSpacing(self.mainConf.pxInt(2))
self.docEdit.addWidget(self.searchBar)
self.docEdit.addWidget(self.noticeBar)
self.docEdit.addWidget(self.docEditor)
@@ -124,8 +124,8 @@ class GuiMain(QMainWindow):
self.viewPane = QWidget()
self.docView = QVBoxLayout()
- self.docView.setContentsMargins(0,0,0,0)
- self.docView.setSpacing(2)
+ self.docView.setContentsMargins(0, 0, 0, 0)
+ self.docView.setSpacing(self.mainConf.pxInt(2))
self.docView.addWidget(self.docViewer)
self.docView.addWidget(self.viewMeta)
self.docView.setStretch(0, 1)
@@ -148,8 +148,9 @@ class GuiMain(QMainWindow):
self.tabWidget.addTab(self.splitOutline, "Outline")
self.tabWidget.currentChanged.connect(self._mainTabChanged)
+ xCM = self.mainConf.pxInt(4)
self.splitMain = QSplitter(Qt.Horizontal)
- self.splitMain.setContentsMargins(4,4,4,4)
+ self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM)
self.splitMain.setOpaqueResize(False)
self.splitMain.addWidget(self.treePane)
self.splitMain.addWidget(self.tabWidget)
@@ -513,9 +514,9 @@ class GuiMain(QMainWindow):
if not self.viewPane.isVisible():
bPos = self.splitMain.sizes()
self.viewPane.setVisible(True)
- vPos = [0,0]
+ vPos = [0, 0]
vPos[0] = int(bPos[1]/2)
- vPos[1] = bPos[1]-vPos[0]
+ vPos[1] = bPos[1] - vPos[0]
self.splitView.setSizes(vPos)
self.docViewer.navigateTo(navLink)
@@ -877,7 +878,7 @@ class GuiMain(QMainWindow):
self.theProject.setLastViewed(None)
bPos = self.splitMain.sizes()
self.viewPane.setVisible(False)
- vPos = [bPos[1],0]
+ vPos = [bPos[1], 0]
self.splitView.setSizes(vPos)
return not self.viewPane.isVisible()
@@ -988,16 +989,18 @@ class GuiMain(QMainWindow):
def _makeStatusIcons(self):
self.statusIcons = {}
+ iPx = self.mainConf.pxInt(32)
for sLabel, sCol, _ in self.theProject.statusItems:
- theIcon = QPixmap(32,32)
+ theIcon = QPixmap(iPx, iPx)
theIcon.fill(QColor(*sCol))
self.statusIcons[sLabel] = QIcon(theIcon)
return
def _makeImportIcons(self):
self.importIcons = {}
+ iPx = self.mainConf.pxInt(32)
for sLabel, sCol, _ in self.theProject.importItems:
- theIcon = QPixmap(32,32)
+ theIcon = QPixmap(iPx, iPx)
theIcon.fill(QColor(*sCol))
self.importIcons[sLabel] = QIcon(theIcon)
return
From ba4922a426ac0e69b31f4d7d16e59d5f06c67182 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 4 Jun 2020 22:36:31 +0200
Subject: [PATCH 15/32] Scale factor calculated from logical dpi
---
nw/__init__.py | 4 ++--
nw/gui/theme.py | 8 ++++++--
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/nw/__init__.py b/nw/__init__.py
index 0b01a8f1..2b8b15a1 100644
--- a/nw/__init__.py
+++ b/nw/__init__.py
@@ -172,7 +172,7 @@ def main(sysArgs=None):
debugLevel = logging.INFO
elif inOpt == "--debug":
debugLevel = logging.DEBUG
- logFormat = "[{asctime:}] {name:>20}:{lineno:<4d} {levelname:8} {message:}"
+ logFormat = "[{asctime:}] {name:>22}:{lineno:<4d} {levelname:8} {message:}"
elif inOpt == "--logfile":
logFile = inArg
toFile = True
@@ -180,7 +180,7 @@ def main(sysArgs=None):
toStd = False
elif inOpt == "--verbose":
debugLevel = VERBOSE
- logFormat = "[{asctime:}] {name:>20}:{lineno:<4d} {levelname:8} {message:}"
+ logFormat = "[{asctime:}] {name:>22}:{lineno:<4d} {levelname:8} {message:}"
elif inOpt == "--style":
qtStyle = inArg
elif inOpt == "--config":
diff --git a/nw/gui/theme.py b/nw/gui/theme.py
index c7e66a2f..b17b7055 100644
--- a/nw/gui/theme.py
+++ b/nw/gui/theme.py
@@ -132,9 +132,13 @@ class GuiTheme:
self.loadDecoration = self.theIcons.loadDecoration
# Extract Other Info
- self.guiDPI = qApp.primaryScreen().physicalDotsPerInch()
- self.guiFont = qApp.font()
+ self.guiDPI = qApp.primaryScreen().physicalDotsPerInchX()
+ self.guiScale = qApp.primaryScreen().logicalDotsPerInchX()/96.0
+ self.mainConf.guiScale = self.guiScale
+ logger.verbose("GUI DPI: %.1f" % self.guiDPI)
+ logger.verbose("GUI Scale: %.2f" % self.guiScale)
+ self.guiFont = qApp.font()
qMetric = QFontMetrics(self.guiFont)
self.fontPointSize = self.guiFont.pointSizeF()
self.fontPixelSize = int(round(qMetric.height()))
From 10ef9d3453c4e4337ad98f792a83da655f5f9f49 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 4 Jun 2020 22:36:47 +0200
Subject: [PATCH 16/32] QSwitch now scales
---
nw/gui/custom.py | 36 ++++++++++++++++++++++--------------
1 file changed, 22 insertions(+), 14 deletions(-)
diff --git a/nw/gui/custom.py b/nw/gui/custom.py
index 96e0c482..264a2c98 100644
--- a/nw/gui/custom.py
+++ b/nw/gui/custom.py
@@ -215,14 +215,22 @@ class QHelpLabel(QLabel):
class QSwitch(QAbstractButton):
- def __init__(self, parent=None):
+ def __init__(self, parent=None, width=40, height=20):
super().__init__(parent=parent)
+ self._xW = int(nw.CONFIG.guiScale*width)
+ self._xH = int(nw.CONFIG.guiScale*height)
+ self._xR = int(self._xH*0.5)
+ self._xT = int(self._xH*0.6)
+ self._rB = int(nw.CONFIG.guiScale*2)
+ self._rH = self._xH - 2*self._rB
+ self._rR = self._xR - self._rB
+
self.setCheckable(True)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
- self.setFixedWidth(40)
- self.setFixedHeight(20)
- self._offset = 10
+ self.setFixedWidth(self._xW)
+ self.setFixedHeight(self._xH)
+ self._offset = self._xR
return
@@ -249,9 +257,9 @@ class QSwitch(QAbstractButton):
"""
super().setChecked(isChecked)
if isChecked:
- self.offset = 30
+ self.offset = self._xW - self._xR
else:
- self.offset = 10
+ self.offset = self._xR
return
##
@@ -263,9 +271,9 @@ class QSwitch(QAbstractButton):
"""
super().resizeEvent(theEvent)
if self.isChecked():
- self.offset = 30
+ self.offset = self._xW - self._xR
else:
- self.offset = 10
+ self.offset = self._xR
return
def paintEvent(self, event):
@@ -297,17 +305,17 @@ class QSwitch(QAbstractButton):
qPaint.setBrush(trackBrush)
qPaint.setOpacity(trackOpacity)
- qPaint.drawRoundedRect(0, 0, 40, 20, 10, 10)
+ qPaint.drawRoundedRect(0, 0, self._xW, self._xH, self._xR, self._xR)
qPaint.setBrush(thumbBrush)
- qPaint.drawEllipse(self.offset - 8, 2, 16, 16)
+ qPaint.drawEllipse(self.offset - self._rR, self._rB, self._rH, self._rH)
theFont = qPaint.font()
- theFont.setPixelSize(12)
+ theFont.setPixelSize(self._xT)
qPaint.setPen(textColor)
qPaint.setFont(theFont)
qPaint.drawText(
- QRectF(self.offset - 8, 2, 16, 16),
+ QRectF(self.offset - self._rR, self._rB, self._rH, self._rH),
Qt.AlignCenter, thumbText
)
@@ -322,9 +330,9 @@ class QSwitch(QAbstractButton):
doAnim.setDuration(120)
doAnim.setStartValue(self.offset)
if self.isChecked():
- doAnim.setEndValue(30)
+ doAnim.setEndValue(self._xW - self._xR)
else:
- doAnim.setEndValue(10)
+ doAnim.setEndValue(self._xR)
doAnim.start()
return
From 0a3751046b83b471221455e0504fe89c3cbef33c Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 4 Jun 2020 22:49:27 +0200
Subject: [PATCH 17/32] A few improvements here and there
---
nw/config.py | 11 ++++++++---
nw/gui/preferences.py | 2 ++
nw/gui/projload.py | 21 ++++++++++-----------
3 files changed, 20 insertions(+), 14 deletions(-)
diff --git a/nw/config.py b/nw/config.py
index b668f42d..58c81ee9 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -88,7 +88,7 @@ class Config:
self.guiLang = "en" # Hardcoded for now
self.guiFont = ""
self.guiFontSize = 11
- self.guiScale = 1.0
+ self.guiScale = 1.0 # Set automatically by Theme class
## Sizes
self.winGeometry = [1100, 650]
@@ -126,8 +126,8 @@ class Config:
self.highlightQuotes = True
self.fmtApostrophe = nwUnicode.U_RSQUO
- self.fmtSingleQuotes = [nwUnicode.U_LSQUO,nwUnicode.U_RSQUO]
- self.fmtDoubleQuotes = [nwUnicode.U_LDQUO,nwUnicode.U_RDQUO]
+ self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO]
+ self.fmtDoubleQuotes = [nwUnicode.U_LDQUO, nwUnicode.U_RDQUO]
self.spellTool = None
self.spellLanguage = None
@@ -202,9 +202,14 @@ class Config:
##
def pxInt(self, theSize):
+ """Used to scale fixed gui sizes by the screen scale factor.
+ This function returns an int, which is always rounded down.
+ """
return int(self.guiScale*theSize)
def pxFloat(self, theSize):
+ """Used to scale fixed gui sizes by the screen scale factor.
+ """
return self.guiScale*theSize
##
diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py
index 55d62fe9..17487a31 100644
--- a/nw/gui/preferences.py
+++ b/nw/gui/preferences.py
@@ -329,6 +329,8 @@ class GuiConfigEditGeneralTab(QWidget):
##
def _backupFolder(self):
+ """Open a dialog to select the backup folder.
+ """
currDir = self.backupPath
if not path.isdir(currDir):
diff --git a/nw/gui/projload.py b/nw/gui/projload.py
index 361b5ba8..0a9999bc 100644
--- a/nw/gui/projload.py
+++ b/nw/gui/projload.py
@@ -61,20 +61,20 @@ class GuiProjectLoad(QDialog):
self.openState = self.NONE_STATE
self.openPath = None
- xSp = self.mainConf.pxInt(16)
- xIc = self.mainConf.pxInt(96)
+ sPx = self.mainConf.pxInt(16)
+ iPx = self.mainConf.pxInt(96)
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
- self.outerBox.setSpacing(xSp)
- self.innerBox.setSpacing(xSp)
+ self.outerBox.setSpacing(sPx)
+ self.innerBox.setSpacing(sPx)
self.setWindowTitle("Open Project")
self.setMinimumWidth(self.mainConf.pxInt(650))
self.setMinimumHeight(self.mainConf.pxInt(400))
self.setModal(True)
- self.guiDeco = self.theTheme.loadDecoration("nwicon", (xIc, xIc))
+ self.guiDeco = self.theTheme.loadDecoration("nwicon", (iPx, iPx))
self.innerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.projectForm = QGridLayout()
@@ -84,10 +84,9 @@ class GuiProjectLoad(QDialog):
self.listBox = QTreeWidget()
self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
- self.listBox.setColumnCount(4)
- self.listBox.setHeaderLabels(["Working Title","Words","Last Opened","Path"])
+ self.listBox.setColumnCount(3)
+ self.listBox.setHeaderLabels(["Working Title", "Words", "Last Opened"])
self.listBox.setRootIsDecorated(False)
- self.listBox.setColumnHidden(3, True)
self.listBox.itemSelectionChanged.connect(self._doSelectRecent)
self.listBox.itemDoubleClicked.connect(self._doOpenRecent)
self.listBox.setIconSize(QSize(iPx, iPx))
@@ -154,7 +153,7 @@ class GuiProjectLoad(QDialog):
self._saveDialogState()
selItems = self.listBox.selectedItems()
if selItems:
- self.openPath = selItems[0].text(3)
+ self.openPath = selItems[0].data(0, Qt.UserRole)
self.openState = self.OPEN_STATE
self.accept()
else:
@@ -167,7 +166,7 @@ class GuiProjectLoad(QDialog):
"""
selList = self.listBox.selectedItems()
if selList:
- self.selPath.setText(selList[0].text(3))
+ self.selPath.setText(selList[0].data(0, Qt.UserRole))
return
def _doBrowse(self):
@@ -257,9 +256,9 @@ class GuiProjectLoad(QDialog):
newItem = QTreeWidgetItem([""]*4)
newItem.setIcon(0, self.theParent.theTheme.getIcon("proj_nwx"))
newItem.setText(0, listData[timeStamp][0])
+ newItem.setData(0, Qt.UserRole, listData[timeStamp][2])
newItem.setText(1, formatInt(listData[timeStamp][1]))
newItem.setText(2, datetime.fromtimestamp(timeStamp).strftime("%x %X"))
- newItem.setText(3, listData[timeStamp][2])
newItem.setTextAlignment(0, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setTextAlignment(1, Qt.AlignRight | Qt.AlignVCenter)
newItem.setTextAlignment(2, Qt.AlignRight | Qt.AlignVCenter)
From d95e8caa50a7aacc3dc40cd17d1fab0276a4b364 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Fri, 5 Jun 2020 16:18:20 +0200
Subject: [PATCH 18/32] Gui sizes should now scale between high and low DPI
screens
---
nw/config.py | 57 ++++++++++++++++++++++++++------
nw/core/project.py | 50 ++++++++++++++++++----------
nw/gui/about.py | 7 +---
nw/gui/build.py | 77 +++++++++++++++++++++++++++-----------------
nw/gui/doceditor.py | 22 ++++++-------
nw/gui/docviewer.py | 4 +--
nw/gui/outline.py | 6 ++--
nw/gui/projload.py | 3 +-
nw/gui/projtree.py | 5 +--
nw/gui/sessionlog.py | 30 ++++++++---------
nw/guimain.py | 6 ++--
11 files changed, 167 insertions(+), 100 deletions(-)
diff --git a/nw/config.py b/nw/config.py
index 58c81ee9..4995cafa 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -205,12 +205,13 @@ class Config:
"""Used to scale fixed gui sizes by the screen scale factor.
This function returns an int, which is always rounded down.
"""
- return int(self.guiScale*theSize)
+ return int(theSize*self.guiScale)
- def pxFloat(self, theSize):
- """Used to scale fixed gui sizes by the screen scale factor.
+ def rpxInt(self, theSize):
+ """Used to un-scale fixed gui sizes by the screen scale factor.
+ This function returns an int, which is always rounded down.
"""
- return self.guiScale*theSize
+ return int(theSize/self.guiScale)
##
# Config Actions
@@ -663,7 +664,7 @@ class Config:
return True
##
- # Setters and Getters
+ # Setters
##
def setConfPath(self, newPath):
@@ -693,6 +694,8 @@ class Config:
return True
def setWinSize(self, newWidth, newHeight):
+ newWidth = int(newWidth/self.guiScale)
+ newHeight = int(newHeight/self.guiScale)
if abs(self.winGeometry[0] - newWidth) > 5:
self.winGeometry[0] = newWidth
self.confChanged = True
@@ -702,27 +705,27 @@ class Config:
return True
def setTreeColWidths(self, colWidths):
- self.treeColWidth = colWidths
+ self.treeColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True
return True
def setProjColWidths(self, colWidths):
- self.projColWidth = colWidths
+ self.projColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True
return True
def setMainPanePos(self, panePos):
- self.mainPanePos = panePos
+ self.mainPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True
return True
def setDocPanePos(self, panePos):
- self.docPanePos = panePos
+ self.docPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True
return True
def setOutlinePanePos(self, panePos):
- self.outlnPanePos = panePos
+ self.outlnPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True
return True
@@ -742,6 +745,40 @@ class Config:
self.errData = []
return errMessage
+ ##
+ # Getters
+ ##
+
+ def getWinSize(self):
+ return [int(x*self.guiScale) for x in self.winGeometry]
+
+ def getTreeColWidths(self):
+ return [int(x*self.guiScale) for x in self.treeColWidth]
+
+ def getProjColWidths(self):
+ return [int(x*self.guiScale) for x in self.projColWidth]
+
+ def getMainPanePos(self):
+ return [int(x*self.guiScale) for x in self.mainPanePos]
+
+ def getDocPanePos(self):
+ return [int(x*self.guiScale) for x in self.docPanePos]
+
+ def getOutlinePanePos(self):
+ return [int(x*self.guiScale) for x in self.outlnPanePos]
+
+ def getTextWidth(self):
+ return self.pxInt(self.textWidth)
+
+ def getTextMargin(self):
+ return self.pxInt(self.textMargin)
+
+ def getTabWidth(self):
+ return self.pxInt(self.tabWidth)
+
+ def getZenWidth(self):
+ return self.pxInt(self.zenWidth)
+
##
# Internal Functions
##
diff --git a/nw/core/project.py b/nw/core/project.py
index 43efa057..213426d8 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -213,14 +213,11 @@ class NWProject():
self.bookAuthors = []
self.autoReplace = {}
self.titleFormat = {
- "title" : r"%title%",
- "chapter" : r"Chapter %ch%: %title%",
- "unnumbered" : r"%title%",
- "scene" : r"* * *",
- "section" : r"",
- "withSynopsis" : False,
- "withComments" : False,
- "withKeywords" : False,
+ "title" : r"%title%",
+ "chapter" : r"Chapter %ch%: %title%",
+ "unnumbered" : r"%title%",
+ "scene" : r"* * *",
+ "section" : r"",
}
self.spellCheck = False
self.autoOutline = True
@@ -835,10 +832,8 @@ class NWProject():
"""Set the formatting of titles in the project.
"""
for valKey, valEntry in titleFormat.items():
- if valKey in ("title","chapter","unnumbered","scene","section"):
+ if valKey in self.titleFormat:
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False)
- elif valKey in ("withSynopsis","withComments","withKeywords"):
- self.titleFormat[valKey] = checkBool(valEntry, False, False)
return
def setProjectChanged(self, bValue):
@@ -2018,6 +2013,7 @@ class OptionState():
def __init__(self, theProject):
+ self.mainConf = nw.CONFIG
self.theProject = theProject
self.theState = {}
self.stringOpt = ()
@@ -2026,6 +2022,10 @@ class OptionState():
return
+ ##
+ # Load and Save Cache
+ ##
+
def loadSettings(self):
"""Load the options dictionary from the project settings file.
"""
@@ -2038,7 +2038,7 @@ class OptionState():
if path.isfile(stateFile):
logger.debug("Loading GUI options file")
try:
- with open(stateFile,mode="r",encoding="utf8") as inFile:
+ with open(stateFile, mode="r", encoding="utf8") as inFile:
theJson = inFile.read()
theState = json.loads(theJson)
except Exception as e:
@@ -2060,7 +2060,7 @@ class OptionState():
logger.debug("Saving GUI options file")
try:
- with open(stateFile,mode="w+",encoding="utf8") as outFile:
+ with open(stateFile, mode="w+", encoding="utf8") as outFile:
outFile.write(json.dumps(self.theState, indent=2))
except Exception as e:
logger.error("Failed to save GUI options file")
@@ -2069,6 +2069,10 @@ class OptionState():
return True
+ ##
+ # Setters
+ ##
+
def setValue(self, setGroup, setName, setValue):
"""Saves a value, with a given group and name.
"""
@@ -2077,6 +2081,10 @@ class OptionState():
self.theState[setGroup][setName] = setValue
return True
+ ##
+ # Getters
+ ##
+
def getValue(self, getGroup, getName, defaultValue):
"""Return an arbitrary type value, if it exists. Otherwise,
return the default value.
@@ -2085,7 +2093,8 @@ class OptionState():
if getName in self.theState[getGroup]:
try:
return self.theState[getGroup][getName]
- except:
+ except Exception as e:
+ logger.warning(str(e))
return defaultValue
return defaultValue
@@ -2109,7 +2118,8 @@ class OptionState():
if getName in self.theState[getGroup]:
try:
return int(self.theState[getGroup][getName])
- except:
+ except Exception as e:
+ logger.warning(str(e))
return defaultValue
return defaultValue
@@ -2121,7 +2131,8 @@ class OptionState():
if getName in self.theState[getGroup]:
try:
return float(self.theState[getGroup][getName])
- except:
+ except Exception as e:
+ logger.warning(str(e))
return defaultValue
return defaultValue
@@ -2133,10 +2144,15 @@ class OptionState():
if getName in self.theState[getGroup]:
try:
return bool(self.theState[getGroup][getName])
- except:
+ except Exception as e:
+ logger.warning(str(e))
return defaultValue
return defaultValue
+ ##
+ # Validators
+ ##
+
def validIntRange(self, theValue, intA, intB, intDefault):
"""Check that an int is in a given range. If it isn't, return
the default value.
diff --git a/nw/gui/about.py b/nw/gui/about.py
index 5d869c9a..d432a4d9 100644
--- a/nw/gui/about.py
+++ b/nw/gui/about.py
@@ -64,7 +64,7 @@ class GuiAbout(QDialog):
self.lblVers = QLabel("v%s" % nw.__version__)
self.lblDate = QLabel(datetime.strptime(nw.__date__, "%Y-%m-%d").strftime("%x"))
- self.leftBox = QVBoxLayout()
+ self.leftBox = QVBoxLayout()
self.leftBox.setSpacing(self.mainConf.pxInt(4))
self.leftBox.addWidget(self.guiDeco, 0, Qt.AlignCenter)
self.leftBox.addWidget(self.lblName, 0, Qt.AlignCenter)
@@ -78,10 +78,6 @@ class GuiAbout(QDialog):
self.pageAbout.setOpenExternalLinks(True)
self.pageAbout.document().setDocumentMargin(self.mainConf.pxInt(16))
- # self.pageCredit = QTextBrowser()
- # self.pageCredit.setOpenExternalLinks(True)
- # self.pageCredit.document().setDocumentMargin(16)
-
self.pageLicense = QTextBrowser()
self.pageLicense.setOpenExternalLinks(True)
self.pageLicense.document().setDocumentMargin(self.mainConf.pxInt(16))
@@ -89,7 +85,6 @@ class GuiAbout(QDialog):
# Main Tab Area
self.tabBox = QTabWidget()
self.tabBox.addTab(self.pageAbout, "About")
- # self.tabBox.addTab(self.pageCredit, "Credit")
self.tabBox.addTab(self.pageLicense, "License")
self.innerBox.addWidget(self.tabBox)
diff --git a/nw/gui/build.py b/nw/gui/build.py
index 30bd4cf5..269c49d7 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -77,17 +77,13 @@ class GuiBuildNovel(QDialog):
self.htmlStyle = [] # List of html styles
self.nwdText = [] # List of markdown documents
- x800 = self.mainConf.pxInt(800)
- x900 = self.mainConf.pxInt(900)
- xFmt = self.mainConf.pxInt(220)
-
self.setWindowTitle("Build Novel Project")
- self.setMinimumWidth(x900)
- self.setMinimumHeight(x800)
+ self.setMinimumWidth(self.mainConf.pxInt(900))
+ self.setMinimumHeight(self.mainConf.pxInt(800))
self.resize(
- self.optState.getInt("GuiBuildNovel", "winWidth", x900),
- self.optState.getInt("GuiBuildNovel", "winHeight", x800)
+ self.mainConf.pxInt(self.optState.getInt("GuiBuildNovel", "winWidth", 900)),
+ self.mainConf.pxInt(self.optState.getInt("GuiBuildNovel", "winHeight", 800))
)
self.outerBox = QHBoxLayout()
@@ -116,6 +112,7 @@ class GuiBuildNovel(QDialog):
r"be centred automatically and only appear between sections of "
r"the same type."
)
+ xFmt = self.mainConf.pxInt(220)
self.fmtTitle = QLineEdit()
self.fmtTitle.setMaxLength(200)
@@ -240,26 +237,32 @@ class GuiBuildNovel(QDialog):
self.includeSynopsis.setToolTip(
"Include synopsis comments in the output."
)
- self.includeSynopsis.setChecked(self.theProject.titleFormat["withSynopsis"])
+ self.includeSynopsis.setChecked(
+ self.optState.getBool("GuiBuildNovel", "incSynopsis", False)
+ )
self.includeComments = QSwitch()
self.includeComments.setToolTip(
"Include plain comments in the output."
)
- self.includeComments.setChecked(self.theProject.titleFormat["withComments"])
+ self.includeComments.setChecked(
+ self.optState.getBool("GuiBuildNovel", "incComments", False)
+ )
self.includeKeywords = QSwitch()
self.includeKeywords.setToolTip(
"Include meta keywords (tags, references) in the output."
)
- self.includeKeywords.setChecked(self.theProject.titleFormat["withKeywords"])
+ self.includeKeywords.setChecked(
+ self.optState.getBool("GuiBuildNovel", "incKeywords", False)
+ )
self.includeBody = QSwitch()
self.includeBody.setToolTip(
"Include body text in the output."
)
self.includeBody.setChecked(
- self.optState.getBool("GuiBuildNovel", "includeBody", True)
+ self.optState.getBool("GuiBuildNovel", "incBodyText", True)
)
self.textForm.addWidget(QLabel("Include synopsis"), 0, 0, 1, 1, Qt.AlignLeft)
@@ -851,27 +854,41 @@ class GuiBuildNovel(QDialog):
# Formatting
self.theProject.setTitleFormat({
- "title" : self.fmtTitle.text().strip(),
- "chapter" : self.fmtChapter.text().strip(),
- "unnumbered" : self.fmtUnnumbered.text().strip(),
- "scene" : self.fmtScene.text().strip(),
- "section" : self.fmtSection.text().strip(),
- "withSynopsis" : self.includeSynopsis.isChecked(),
- "withComments" : self.includeComments.isChecked(),
- "withKeywords" : self.includeKeywords.isChecked(),
+ "title" : self.fmtTitle.text().strip(),
+ "chapter" : self.fmtChapter.text().strip(),
+ "unnumbered" : self.fmtUnnumbered.text().strip(),
+ "scene" : self.fmtScene.text().strip(),
+ "section" : self.fmtSection.text().strip(),
})
+ winWidth = self.mainConf.pxInt(self.width())
+ winHeight = self.mainConf.pxInt(self.height())
+ justifyText = self.justifyText.isChecked()
+ noStyling = self.noStyling.isChecked()
+ textFont = self.textFont.text()
+ textSize = self.textSize.value()
+ novelFiles = self.novelFiles.isChecked()
+ noteFiles = self.noteFiles.isChecked()
+ ignoreFlag = self.ignoreFlag.isChecked()
+ incSynopsis = self.includeSynopsis.isChecked()
+ incComments = self.includeComments.isChecked()
+ incKeywords = self.includeKeywords.isChecked()
+ incBodyText = self.includeBody.isChecked()
+
# GUI Settings
- self.optState.setValue("GuiBuildNovel", "winWidth", self.width())
- self.optState.setValue("GuiBuildNovel", "winHeight", self.height())
- self.optState.setValue("GuiBuildNovel", "justifyText", self.justifyText.isChecked())
- self.optState.setValue("GuiBuildNovel", "noStyling", self.noStyling.isChecked())
- self.optState.setValue("GuiBuildNovel", "textFont", self.textFont.text())
- self.optState.setValue("GuiBuildNovel", "textSize", self.textSize.value())
- self.optState.setValue("GuiBuildNovel", "addNovel", self.novelFiles.isChecked())
- self.optState.setValue("GuiBuildNovel", "addNotes", self.noteFiles.isChecked())
- self.optState.setValue("GuiBuildNovel", "ignoreFlag", self.ignoreFlag.isChecked())
- self.optState.setValue("GuiBuildNovel", "includeBody", self.includeBody.isChecked())
+ self.optState.setValue("GuiBuildNovel", "winWidth", winWidth)
+ self.optState.setValue("GuiBuildNovel", "winHeight", winHeight)
+ self.optState.setValue("GuiBuildNovel", "justifyText", justifyText)
+ self.optState.setValue("GuiBuildNovel", "noStyling", noStyling)
+ self.optState.setValue("GuiBuildNovel", "textFont", textFont)
+ self.optState.setValue("GuiBuildNovel", "textSize", textSize)
+ self.optState.setValue("GuiBuildNovel", "addNovel", novelFiles)
+ self.optState.setValue("GuiBuildNovel", "addNotes", noteFiles)
+ self.optState.setValue("GuiBuildNovel", "ignoreFlag", ignoreFlag)
+ self.optState.setValue("GuiBuildNovel", "incSynopsis", incSynopsis)
+ self.optState.setValue("GuiBuildNovel", "incComments", incComments)
+ self.optState.setValue("GuiBuildNovel", "incKeywords", incKeywords)
+ self.optState.setValue("GuiBuildNovel", "incBodyText", incBodyText)
self.optState.saveSettings()
return
diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py
index 884c4ae3..7b6ef81b 100644
--- a/nw/gui/doceditor.py
+++ b/nw/gui/doceditor.py
@@ -81,7 +81,7 @@ class GuiDocEditor(QTextEdit):
# Core Elements
self.qDocument = self.document()
- self.qDocument.setDocumentMargin(self.mainConf.textMargin)
+ self.qDocument.setDocumentMargin(self.mainConf.getTextMargin())
self.qDocument.contentsChange.connect(self._docChange)
# Document Title
@@ -192,14 +192,13 @@ class GuiDocEditor(QTextEdit):
self.setPalette(docPalette)
# Set default text margins
- self.qDocument.setDocumentMargin(self.mainConf.textMargin)
+ self.qDocument.setDocumentMargin(self.mainConf.getTextMargin())
# Also set the document text options for the document text flow
theOpt = QTextOption()
- if self.mainConf.tabWidth is not None:
- if self.mainConf.verQtValue >= 51000:
- theOpt.setTabStopDistance(self.mainConf.tabWidth)
+ if self.mainConf.verQtValue >= 51000:
+ theOpt.setTabStopDistance(self.mainConf.getTabWidth())
if self.mainConf.doJustify:
theOpt.setAlignment(Qt.AlignJustify)
if self.mainConf.showTabsNSpaces:
@@ -319,6 +318,7 @@ class GuiDocEditor(QTextEdit):
Config.textFixedW is enabled or we're in Zen mode. Otherwise,
just ensure the margins are set correctly.
"""
+ cM = self.mainConf.getTextMargin()
if self.mainConf.textFixedW or self.theParent.isZenMode:
vBar = self.verticalScrollBar()
if vBar.isVisible():
@@ -326,20 +326,20 @@ class GuiDocEditor(QTextEdit):
else:
sW = 0
if self.theParent.isZenMode:
- tW = self.mainConf.zenWidth
+ tW = self.mainConf.getZenWidth()
else:
- tW = self.mainConf.textWidth
+ tW = self.mainConf.getTextWidth()
wW = self.width()
tM = int((wW - sW - tW)/2)
- if tM < self.mainConf.textMargin:
- tM = self.mainConf.textMargin
+ if tM < cM:
+ tM = cM
else:
- tM = self.mainConf.textMargin
+ tM = cM
tB = self.lineWidth()
tW = self.width() - 2*tB
tH = self.docTitle.height()
- tT = self.mainConf.textMargin - tH
+ tT = cM - tH
self.docTitle.setGeometry(tB, tB, tW, tH)
self.setViewportMargins(0, tH, 0, 0)
diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py
index 70163b7b..a3d6eeda 100644
--- a/nw/gui/docviewer.py
+++ b/nw/gui/docviewer.py
@@ -105,7 +105,7 @@ class GuiDocViewer(QTextBrowser):
docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
self.setPalette(docPalette)
- self.qDocument.setDocumentMargin(self.mainConf.textMargin)
+ self.qDocument.setDocumentMargin(self.mainConf.getTextMargin())
theOpt = QTextOption()
if self.mainConf.doJustify:
theOpt.setAlignment(Qt.AlignJustify)
@@ -216,7 +216,7 @@ class GuiDocViewer(QTextBrowser):
tB = self.lineWidth()
tW = self.width() - 2*tB
tH = self.docTitle.height()
- tT = self.mainConf.textMargin - tH
+ tT = self.mainConf.getTextMargin() - tH
self.docTitle.setGeometry(tB, tB, tW, tH)
self.setViewportMargins(0, tH, 0, 0)
diff --git a/nw/gui/outline.py b/nw/gui/outline.py
index bb080c1a..81cd805d 100644
--- a/nw/gui/outline.py
+++ b/nw/gui/outline.py
@@ -271,7 +271,7 @@ class GuiOutline(QTreeWidget):
tmpWidth = self.optState.getValue("GuiOutline", "columnWidth", {})
for hName in tmpWidth:
try:
- self.colWidth[nwOutline[hName]] = tmpWidth[hName]
+ self.colWidth[nwOutline[hName]] = self.mainConf.pxInt(tmpWidth[hName])
except:
logger.warning("Ignored unknown outline column '%s'" % str(hName))
@@ -301,7 +301,7 @@ class GuiOutline(QTreeWidget):
colHidden = {}
for hItem in nwOutline:
- colWidth[hItem.name] = self.colWidth[hItem]
+ colWidth[hItem.name] = self.mainConf.rpxInt(self.colWidth[hItem])
colHidden[hItem.name] = self.colHidden[hItem]
for iCol in range(self.columnCount()):
@@ -309,7 +309,7 @@ class GuiOutline(QTreeWidget):
treeOrder.append(hName)
iLog = self.treeHead.logicalIndex(iCol)
- logWidth = self.columnWidth(iLog)
+ logWidth = self.mainConf.rpxInt(self.columnWidth(iLog))
logHidden = self.isColumnHidden(iLog)
colHidden[hName] = logHidden
diff --git a/nw/gui/projload.py b/nw/gui/projload.py
index 0a9999bc..45300332 100644
--- a/nw/gui/projload.py
+++ b/nw/gui/projload.py
@@ -270,8 +270,9 @@ class GuiProjectLoad(QDialog):
newItem.setSelected(True)
hasSelection = True
+ projColWidth = self.mainConf.getProjColWidths()
for i in range(3):
- self.listBox.setColumnWidth(i, self.mainConf.projColWidth[i])
+ self.listBox.setColumnWidth(i, projColWidth[i])
return
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index 97b87a2e..5bef07bf 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -112,8 +112,9 @@ class GuiProjectTree(QTreeWidget):
# self.setSelectionBehavior(QAbstractItemView.SelectRows)
# Get user's column width preferences for NAME and COUNT
- if len(self.mainConf.treeColWidth) <= 4:
- for colN, colW in enumerate(self.mainConf.treeColWidth):
+ treeColWidth = self.mainConf.getTreeColWidths()
+ if len(treeColWidth) <= 4:
+ for colN, colW in enumerate(treeColWidth):
self.setColumnWidth(colN, colW)
# The last column should just auto-scale
diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py
index 2e31d02f..5f3c61f0 100644
--- a/nw/gui/sessionlog.py
+++ b/nw/gui/sessionlog.py
@@ -64,16 +64,16 @@ class GuiSessionLogView(QDialog):
self.setMinimumWidth(self.mainConf.pxInt(420))
self.setMinimumHeight(self.mainConf.pxInt(400))
- widthCol0 = self.optState.getInt("GuiSession", "widthCol0", self.mainConf.pxInt(180))
- widthCol1 = self.optState.getInt("GuiSession", "widthCol1", self.mainConf.pxInt(80))
- widthCol2 = self.optState.getInt("GuiSession", "widthCol2", self.mainConf.pxInt(80))
+ wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol0", 180))
+ wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol1", 80))
+ wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol2", 80))
self.listBox = QTreeWidget()
self.listBox.setHeaderLabels(["Session Start","Length","Words",""])
self.listBox.setIndentation(0)
- self.listBox.setColumnWidth(0, widthCol0)
- self.listBox.setColumnWidth(1, widthCol1)
- self.listBox.setColumnWidth(2, widthCol2)
+ self.listBox.setColumnWidth(0, wCol0)
+ self.listBox.setColumnWidth(1, wCol1)
+ self.listBox.setColumnWidth(2, wCol2)
self.listBox.setColumnWidth(3, 0)
hHeader = self.listBox.headerItem()
@@ -220,20 +220,20 @@ class GuiSessionLogView(QDialog):
def _doClose(self):
- widthCol0 = self.listBox.columnWidth(0)
- widthCol1 = self.listBox.columnWidth(1)
- widthCol2 = self.listBox.columnWidth(2)
+ widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0))
+ widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1))
+ widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2))
sortCol = self.listBox.sortColumn()
sortOrder = self.listBox.header().sortIndicatorOrder()
hideZeros = self.hideZeros.isChecked()
hideNegative = self.hideNegative.isChecked()
- self.optState.setValue("GuiSession", "widthCol0", widthCol0)
- self.optState.setValue("GuiSession", "widthCol1", widthCol1)
- self.optState.setValue("GuiSession", "widthCol2", widthCol2)
- self.optState.setValue("GuiSession", "sortCol", sortCol)
- self.optState.setValue("GuiSession", "sortOrder", sortOrder)
- self.optState.setValue("GuiSession", "hideZeros", hideZeros)
+ self.optState.setValue("GuiSession", "widthCol0", widthCol0)
+ self.optState.setValue("GuiSession", "widthCol1", widthCol1)
+ self.optState.setValue("GuiSession", "widthCol2", widthCol2)
+ self.optState.setValue("GuiSession", "sortCol", sortCol)
+ self.optState.setValue("GuiSession", "sortOrder", sortOrder)
+ self.optState.setValue("GuiSession", "hideZeros", hideZeros)
self.optState.setValue("GuiSession", "hideNegative", hideNegative)
self.optState.saveSettings()
diff --git a/nw/guimain.py b/nw/guimain.py
index c74d4e1c..549e25b5 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -81,7 +81,7 @@ class GuiMain(QMainWindow):
self.isZenMode = False
# Prepare main window
- self.resize(*self.mainConf.winGeometry)
+ self.resize(*self.mainConf.getWinSize())
self._setWindowTitle()
self.setWindowIcon(QIcon(self.mainConf.appIcon))
@@ -139,7 +139,7 @@ class GuiMain(QMainWindow):
self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.projView)
self.splitOutline.addWidget(self.projMeta)
- self.splitOutline.setSizes(self.mainConf.outlnPanePos)
+ self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
self.tabWidget = QTabWidget()
self.tabWidget.setTabPosition(QTabWidget.East)
@@ -154,7 +154,7 @@ class GuiMain(QMainWindow):
self.splitMain.setOpaqueResize(False)
self.splitMain.addWidget(self.treePane)
self.splitMain.addWidget(self.tabWidget)
- self.splitMain.setSizes(self.mainConf.mainPanePos)
+ self.splitMain.setSizes(self.mainConf.getMainPanePos())
self.setCentralWidget(self.splitMain)
From f029a0ff5fef5300e9b01342b141ef4795a7cdd0 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Fri, 5 Jun 2020 16:20:49 +0200
Subject: [PATCH 19/32] Fixed tests
---
tests/reference/gui/0_nwProject.nwx | 3 ---
tests/reference/gui/1_nwProject.nwx | 3 ---
tests/reference/gui/2_nwProject.nwx | 3 ---
tests/reference/gui/3_nwProject.nwx | 3 ---
tests/reference/proj/1_nwProject.nwx | 3 ---
tests/reference/proj/2_nwProject.nwx | 3 ---
tests/reference/proj/3_nwProject.nwx | 3 ---
7 files changed, 21 deletions(-)
diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx
index dcf46837..c564337a 100644
--- a/tests/reference/gui/0_nwProject.nwx
+++ b/tests/reference/gui/0_nwProject.nwx
@@ -18,9 +18,6 @@
%title%
* * *
- False
- False
- False
New
diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx
index ab090c92..db077dde 100644
--- a/tests/reference/gui/1_nwProject.nwx
+++ b/tests/reference/gui/1_nwProject.nwx
@@ -18,9 +18,6 @@
%title%
* * *
- False
- False
- False
New
diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx
index 11fc5dbe..1f63cb60 100644
--- a/tests/reference/gui/2_nwProject.nwx
+++ b/tests/reference/gui/2_nwProject.nwx
@@ -22,9 +22,6 @@
%title%
* * *
- False
- False
- False
New
diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx
index e7f9e514..88a00d4b 100644
--- a/tests/reference/gui/3_nwProject.nwx
+++ b/tests/reference/gui/3_nwProject.nwx
@@ -18,9 +18,6 @@
%title%
* * *
- False
- False
- False
New
diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx
index 5ba28cae..8156cf1e 100644
--- a/tests/reference/proj/1_nwProject.nwx
+++ b/tests/reference/proj/1_nwProject.nwx
@@ -18,9 +18,6 @@
%title%
* * *
- False
- False
- False
New
diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx
index fe5bf878..b8e454d6 100644
--- a/tests/reference/proj/2_nwProject.nwx
+++ b/tests/reference/proj/2_nwProject.nwx
@@ -18,9 +18,6 @@
%title%
* * *
- False
- False
- False
New
diff --git a/tests/reference/proj/3_nwProject.nwx b/tests/reference/proj/3_nwProject.nwx
index 2e3cb6c0..130933e0 100644
--- a/tests/reference/proj/3_nwProject.nwx
+++ b/tests/reference/proj/3_nwProject.nwx
@@ -18,9 +18,6 @@
%title%
* * *
- False
- False
- False
New
From 9258d49bc8984fc61f7e10005a86985eaa6ac78a Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Fri, 5 Jun 2020 19:17:00 +0200
Subject: [PATCH 20/32] Some minor fixes and improvements to the OptionState
class
---
nw/core/project.py | 64 +++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 57 insertions(+), 7 deletions(-)
diff --git a/nw/core/project.py b/nw/core/project.py
index 213426d8..b163cbd1 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -2015,10 +2015,43 @@ class OptionState():
self.mainConf = nw.CONFIG
self.theProject = theProject
- self.theState = {}
- self.stringOpt = ()
- self.boolOpt = ()
- self.intOpt = ()
+
+ self.theState = {}
+ self.validMap = {
+ "GuiSession": set([
+ "widthCol0",
+ "widthCol1",
+ "widthCol2",
+ "sortCol",
+ "sortOrder",
+ "hideZeros",
+ "hideNegative",
+ ]),
+ "GuiDocSplit": set([
+ "spLevel",
+ ]),
+ "GuiBuildNovel": set([
+ "winWidth",
+ "winHeight",
+ "addNovel",
+ "addNotes",
+ "ignoreFlag",
+ "justifyText",
+ "excludeBody",
+ "textFont",
+ "textSize",
+ "noStyling",
+ "incSynopsis",
+ "incComments",
+ "incKeywords",
+ "incBodyText",
+ ]),
+ "GuiOutline": set([
+ "headerOrder",
+ "columnWidth",
+ "columnHidden",
+ ])
+ }
return
@@ -2045,8 +2078,14 @@ class OptionState():
logger.error("Failed to load GUI options file")
logger.error(str(e))
return False
- for anOpt in theState:
- self.theState[anOpt] = theState[anOpt]
+
+ # Filter out unused variables
+ for aGroup in theState:
+ if aGroup in self.validMap:
+ self.theState[aGroup] = {}
+ for anOpt in theState[aGroup]:
+ if anOpt in self.validMap[aGroup]:
+ self.theState[aGroup][anOpt] = theState[aGroup][anOpt]
return True
@@ -2076,9 +2115,19 @@ class OptionState():
def setValue(self, setGroup, setName, setValue):
"""Saves a value, with a given group and name.
"""
+ if not setGroup in self.validMap:
+ logger.error("Unknown option group '%s'" % setGroup)
+ return False
+
+ if not setName in self.validMap[setGroup]:
+ logger.error("Unknown option name '%s'" % setName)
+ return False
+
if not setGroup in self.theState:
self.theState[setGroup] = {}
+
self.theState[setGroup][setName] = setValue
+
return True
##
@@ -2106,7 +2155,8 @@ class OptionState():
if getName in self.theState[getGroup]:
try:
return str(self.theState[getGroup][getName])
- except:
+ except Exception as e:
+ logger.warning(str(e))
return defaultValue
return defaultValue
From 213889e9d5a7ab40eddd8ef6490e93e82f9133e3 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Fri, 5 Jun 2020 19:46:51 +0200
Subject: [PATCH 21/32] Added open and view document to the project tree
context menu
---
nw/gui/mainmenu.py | 4 ++--
nw/gui/projtree.py | 28 +++++++++++++++++++++++++++-
2 files changed, 29 insertions(+), 3 deletions(-)
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index fc170377..7cd20565 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -233,14 +233,14 @@ class GuiMainMenu(QMenuBar):
self.projMenu.addSeparator()
# Project > Edit
- self.aEditItem = QAction("&Edit Item", self)
+ self.aEditItem = QAction("&Edit Project Item", self)
self.aEditItem.setStatusTip("Change item settings")
self.aEditItem.setShortcuts(["Ctrl+E", "F2"])
self.aEditItem.triggered.connect(self.theParent.editItem)
self.projMenu.addAction(self.aEditItem)
# Project > Delete
- self.aDeleteItem = QAction("&Delete Item", self)
+ self.aDeleteItem = QAction("&Delete Project Item", self)
self.aDeleteItem.setStatusTip("Delete selected item")
self.aDeleteItem.setShortcut("Ctrl+Del")
self.aDeleteItem.triggered.connect(lambda : self.theParent.treeView.deleteItem(None))
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index 5bef07bf..66ba4639 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -836,7 +836,15 @@ class GuiProjectTreeMenu(QMenu):
self.theTree = theTree
self.theItem = None
- self.editItem = QAction("Edit Item", self)
+ self.openItem = QAction("Open Document", self)
+ self.openItem.triggered.connect(self._doOpenItem)
+ self.addAction(self.openItem)
+
+ self.viewItem = QAction("View Document", self)
+ self.viewItem.triggered.connect(self._doViewItem)
+ self.addAction(self.viewItem)
+
+ self.editItem = QAction("Edit Project Item", self)
self.editItem.triggered.connect(self._doEditItem)
self.addAction(self.editItem)
@@ -876,6 +884,8 @@ class GuiProjectTreeMenu(QMenu):
isFile = theItem.itemType == nwItemType.FILE
isOrph = isFile and theItem.parHandle is None
+ showOpen = isFile
+ showView = isFile
showEdit = not isTrash and not isOrph
showExport = isFile and not inTrash and not isOrph
showNewFile = not isTrash and not inTrash and not isOrph
@@ -883,6 +893,8 @@ class GuiProjectTreeMenu(QMenu):
showDelete = not isTrash
showEmpty = isTrash
+ self.openItem.setVisible(showOpen)
+ self.viewItem.setVisible(showView)
self.editItem.setVisible(showEdit)
self.toggleExp.setVisible(showExport)
self.newFile.setVisible(showNewFile)
@@ -896,6 +908,20 @@ class GuiProjectTreeMenu(QMenu):
# Slots
##
+ def _doOpenItem(self):
+ """Forward the open document call to the main GUI window.
+ """
+ if self.theItem is not None:
+ self.theTree.theParent.openDocument(self.theItem.itemHandle)
+ return
+
+ def _doViewItem(self):
+ """Forward the view document call to the main GUI window.
+ """
+ if self.theItem is not None:
+ self.theTree.theParent.viewDocument(self.theItem.itemHandle)
+ return
+
def _doEditItem(self):
"""Forward the edit item call to the main GUI window.
"""
From 50630df705073cbd40c08ebc9e1de5f44c0c8dab Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Fri, 5 Jun 2020 20:58:14 +0200
Subject: [PATCH 22/32] Moved a few settings around in the project xml, but
retaining compatibility
---
nw/core/project.py | 24 +++++++++++++++++++-----
sample/nwProject.nwx | 10 +++++-----
2 files changed, 24 insertions(+), 10 deletions(-)
diff --git a/nw/core/project.py b/nw/core/project.py
index 213426d8..dce4d0fb 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -331,6 +331,8 @@ class NWProject():
hexVersion = xRoot.attrib["hexVersion"]
if "fileVersion" in xRoot.attrib:
fileVersion = xRoot.attrib["fileVersion"]
+
+ # The following are deprecated and will be removed
if "saveCount" in xRoot.attrib:
self.saveCount = checkInt(xRoot.attrib["saveCount"], 0, False)
if "autoCount" in xRoot.attrib:
@@ -409,6 +411,14 @@ class NWProject():
elif xItem.tag == "author":
logger.verbose("Author: '%s'" % xItem.text)
self.bookAuthors.append(xItem.text)
+ elif xItem.tag == "saveCount":
+ self.saveCount = checkInt(xItem.text, 0)
+ elif xItem.tag == "autoCount":
+ self.autoCount = checkInt(xItem.text, 0)
+ elif xItem.tag == "editTime":
+ self.editTime = checkInt(xItem.text, 0)
+
+ # The following is deprecated, and will be removed
elif xItem.tag == "backup":
self.doBackup = checkBool(xItem.text, False)
@@ -417,7 +427,9 @@ class NWProject():
for xItem in xChild:
if xItem.text is None:
continue
- if xItem.tag == "spellCheck":
+ if xItem.tag == "doBackup":
+ self.doBackup = checkBool(xItem.text, False)
+ elif xItem.tag == "spellCheck":
self.spellCheck = checkBool(xItem.text, False)
elif xItem.tag == "autoOutline":
self.autoOutline = checkBool(xItem.text, True)
@@ -489,21 +501,23 @@ class NWProject():
"appVersion" : str(nw.__version__),
"hexVersion" : str(nw.__hexversion__),
"fileVersion" : "1.1",
- "saveCount" : str(self.saveCount),
- "autoCount" : str(self.autoCount),
"timeStamp" : formatTimeStamp(saveTime),
- "editTime" : str(int(self.editTime + saveTime - self.projOpened)),
})
+ editTime = int(self.editTime + saveTime - self.projOpened)
+
# Save Project Meta
xProject = etree.SubElement(nwXML, "project")
self._packProjectValue(xProject, "name", self.projName, True)
self._packProjectValue(xProject, "title", self.bookTitle, True)
self._packProjectValue(xProject, "author", self.bookAuthors)
- self._packProjectValue(xProject, "backup", self.doBackup)
+ self._packProjectValue(xProject, "saveCount", str(self.saveCount))
+ self._packProjectValue(xProject, "autoCount", str(self.autoCount))
+ self._packProjectValue(xProject, "editTime", str(editTime))
# Save Project Settings
xSettings = etree.SubElement(nwXML, "settings")
+ self._packProjectValue(xSettings, "doBackup", self.doBackup)
self._packProjectValue(xSettings, "spellCheck", self.spellCheck)
self._packProjectValue(xSettings, "autoOutline", self.autoOutline)
self._packProjectValue(xSettings, "lastEdited", self.lastEdited)
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index b7b0bde0..88ce6d2f 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,13 +1,16 @@
-
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- False
+ 273
+ 40
+ 4125
+ False
True
True
636b6aa9b697b
@@ -24,9 +27,6 @@
%title%
Scene %ch%.%sc%: %title%
- True
- True
- False
New
From a05f77e6c080b7b14fb7814ddb2409539e90bac2 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Fri, 5 Jun 2020 21:09:04 +0200
Subject: [PATCH 23/32] Fix tests
---
tests/reference/gui/0_nwProject.nwx | 7 +++++--
tests/reference/gui/1_nwProject.nwx | 7 +++++--
tests/reference/gui/2_nwProject.nwx | 7 +++++--
tests/reference/gui/3_nwProject.nwx | 7 +++++--
tests/reference/proj/1_nwProject.nwx | 7 +++++--
tests/reference/proj/2_nwProject.nwx | 7 +++++--
tests/reference/proj/3_nwProject.nwx | 7 +++++--
tests/test_gui.py | 8 ++++----
tests/test_project.py | 10 +++++-----
9 files changed, 44 insertions(+), 23 deletions(-)
diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx
index c564337a..63535bfa 100644
--- a/tests/reference/gui/0_nwProject.nwx
+++ b/tests/reference/gui/0_nwProject.nwx
@@ -1,11 +1,14 @@
-
+
New Project
- True
+ 2
+ 1
+ 0
+ True
False
True
None
diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx
index db077dde..17180919 100644
--- a/tests/reference/gui/1_nwProject.nwx
+++ b/tests/reference/gui/1_nwProject.nwx
@@ -1,11 +1,14 @@
-
+
New Project
- True
+ 5
+ 1
+ 11
+ True
True
True
31489056e0916
diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx
index 1f63cb60..316e30c1 100644
--- a/tests/reference/gui/2_nwProject.nwx
+++ b/tests/reference/gui/2_nwProject.nwx
@@ -1,13 +1,16 @@
-
+
Project Name
Project Title
Jane Doe
John Doh
- True
+ 2
+ 1
+ 1
+ True
False
True
None
diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx
index 88a00d4b..7ae01352 100644
--- a/tests/reference/gui/3_nwProject.nwx
+++ b/tests/reference/gui/3_nwProject.nwx
@@ -1,11 +1,14 @@
-
+
New Project
- True
+ 2
+ 1
+ 0
+ True
False
True
None
diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx
index 8156cf1e..8516baae 100644
--- a/tests/reference/proj/1_nwProject.nwx
+++ b/tests/reference/proj/1_nwProject.nwx
@@ -1,11 +1,14 @@
-
+
New Project
- True
+ 1
+ 0
+ 0
+ True
False
True
None
diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx
index b8e454d6..da82f956 100644
--- a/tests/reference/proj/2_nwProject.nwx
+++ b/tests/reference/proj/2_nwProject.nwx
@@ -1,11 +1,14 @@
-
+
New Project
- True
+ 4
+ 0
+ 0
+ True
False
True
None
diff --git a/tests/reference/proj/3_nwProject.nwx b/tests/reference/proj/3_nwProject.nwx
index 130933e0..8adfadb8 100644
--- a/tests/reference/proj/3_nwProject.nwx
+++ b/tests/reference/proj/3_nwProject.nwx
@@ -1,11 +1,14 @@
-
+
New Project
- True
+ 5
+ 0
+ 0
+ True
False
True
None
diff --git a/tests/test_gui.py b/tests/test_gui.py
index fe52cabd..cd375cd8 100644
--- a/tests/test_gui.py
+++ b/tests/test_gui.py
@@ -45,7 +45,7 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
# Check the files
projFile = path.join(nwTempGUI,"nwProject.nwx")
- assert cmpFiles(projFile, path.join(nwRef,"gui","0_nwProject.nwx"), [2])
+ assert cmpFiles(projFile, path.join(nwRef,"gui","0_nwProject.nwx"), [2, 6, 7, 8])
qtbot.wait(stepDelay)
# qtbot.stopForInteraction()
@@ -246,7 +246,7 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
# Check the files
refFile = path.join(nwTempGUI, "nwProject.nwx")
- assert cmpFiles(refFile, path.join(nwRef, "gui", "1_nwProject.nwx"), [2])
+ assert cmpFiles(refFile, path.join(nwRef, "gui", "1_nwProject.nwx"), [2, 6, 7, 8])
refFile = path.join(nwTempGUI, "content", "0e17daca5f3e1.nwd")
assert cmpFiles(refFile, path.join(nwRef, "gui", "1_0e17daca5f3e1.nwd"))
refFile = path.join(nwTempGUI, "content", "98010bd9270f9.nwd")
@@ -342,7 +342,7 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp):
# Check the files
projFile = path.join(nwTempGUI,"nwProject.nwx")
- assert cmpFiles(projFile, path.join(nwRef, "gui", "2_nwProject.nwx"), [2])
+ assert cmpFiles(projFile, path.join(nwRef, "gui", "2_nwProject.nwx"), [2, 8, 9, 10])
nwGUI.closeMain()
# qtbot.stopForInteraction()
@@ -391,7 +391,7 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
# Check the files
projFile = path.join(nwTempGUI,"nwProject.nwx")
- assert cmpFiles(projFile, path.join(nwRef, "gui", "3_nwProject.nwx"), [2])
+ assert cmpFiles(projFile, path.join(nwRef, "gui", "3_nwProject.nwx"), [2, 6, 7, 8])
nwGUI.closeMain()
# qtbot.stopForInteraction()
diff --git a/tests/test_project.py b/tests/test_project.py
index 44821856..a4ae401e 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -30,7 +30,7 @@ def testProjectNew(nwTempProj,nwRef,nwTemp):
assert theProject.setProjectPath(nwTempProj)
assert theProject.saveProject()
assert theProject.closeProject()
- assert cmpFiles(projFile, refFile, [2])
+ assert cmpFiles(projFile, refFile, [2, 6, 7, 8])
@pytest.mark.project
def testProjectOpen(nwTempProj):
@@ -43,7 +43,7 @@ def testProjectSave(nwTempProj,nwRef):
refFile = path.join(nwRef,"proj","1_nwProject.nwx")
assert theProject.saveProject()
assert theProject.closeProject()
- assert cmpFiles(projFile, refFile, [2])
+ assert cmpFiles(projFile, refFile, [2, 6, 7, 8])
assert not theProject.projChanged
@pytest.mark.project
@@ -55,7 +55,7 @@ def testProjectOpenTwice(nwTempProj,nwRef):
assert theProject.openProject(projFile, overrideLock=True)
assert theProject.saveProject()
assert theProject.closeProject()
- assert cmpFiles(projFile, refFile, [2])
+ assert cmpFiles(projFile, refFile, [2, 6, 7, 8])
@pytest.mark.project
def testProjectNewRoot(nwTempProj,nwRef):
@@ -73,7 +73,7 @@ def testProjectNewRoot(nwTempProj,nwRef):
assert theProject.projChanged
assert theProject.saveProject()
assert theProject.closeProject()
- assert cmpFiles(projFile, refFile, [2])
+ assert cmpFiles(projFile, refFile, [2, 6, 7, 8])
assert not theProject.projChanged
@pytest.mark.project
@@ -86,7 +86,7 @@ def testProjectNewFile(nwTempProj,nwRef):
assert theProject.projChanged
assert theProject.saveProject()
assert theProject.closeProject()
- assert cmpFiles(projFile, refFile, [2])
+ assert cmpFiles(projFile, refFile, [2, 6, 7, 8])
assert not theProject.projChanged
@pytest.mark.project
From 3228d1723bd5ac3c4d3b901cf6fd4e0c0c6b2e48 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Fri, 5 Jun 2020 21:18:47 +0200
Subject: [PATCH 24/32] Updated changelog
---
CHANGELOG.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e16cdd1c..193fe15e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,12 @@
* A details panel below the Outline tree view has been added. The panel shows all the information of a selected row in the tree view above, including hidden columns, and some additional information. The tags and references also become clickable links that when clicked will open in the document viewer. PR #281.
* Added a context menu to the project tree for easier access to some of the most use actions on the tree. PR #282.
+* Improved the support for High DPI screens. Margins and box sizes that are hardcoded should now scale. User settings should also scale back and forth when switching between scale factors. Issue #280, PR #285.
+**Project Structure**
+
+* The way GUI states of switches, column widths, etc., is saved has been improved a bit during the High DPI updates. PRs #285 and #286.
+* Some settings have been moved around to more appropriate sections in the project XML file. The project load function still reads the values from the previous location if opening an older project file. PR #288.
## Version 0.7 [2020-06-01]
From 152cf60f0503d6edb50a70690c3e20dcf0212855 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Fri, 5 Jun 2020 21:19:03 +0200
Subject: [PATCH 25/32] Updated changelog
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 193fe15e..97078902 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@
* The way GUI states of switches, column widths, etc., is saved has been improved a bit during the High DPI updates. PRs #285 and #286.
* Some settings have been moved around to more appropriate sections in the project XML file. The project load function still reads the values from the previous location if opening an older project file. PR #288.
+
## Version 0.7 [2020-06-01]
**Bugfixes**
From fa8d3ff816f41b312e7e271d0ff78af9b0b0470c Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sat, 6 Jun 2020 15:14:13 +0200
Subject: [PATCH 26/32] Moved NWItem to core/item.py
---
nw/core/item.py | 277 +++++++++++++++++++++++++++++++++++++++++++++
nw/core/project.py | 249 +---------------------------------------
2 files changed, 278 insertions(+), 248 deletions(-)
create mode 100644 nw/core/item.py
diff --git a/nw/core/item.py b/nw/core/item.py
new file mode 100644
index 00000000..5c0f26e0
--- /dev/null
+++ b/nw/core/item.py
@@ -0,0 +1,277 @@
+# -*- coding: utf-8 -*-
+"""novelWriter Project Item Class
+
+ novelWriter – Project Item Class
+==================================
+ Class holding the data off a project tree item
+
+ File History:
+ Created: 2018-10-27 [0.0.1] NWItem
+
+ 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 lxml import etree
+
+from nw.common import checkInt
+from nw.constants import nwItemType, nwItemClass, nwItemLayout
+
+logger = logging.getLogger(__name__)
+
+class NWItem():
+
+ def __init__(self, theProject):
+
+ self.theProject = theProject
+
+ self.itemName = ""
+ self.itemHandle = None
+ self.parHandle = None
+ self.itemOrder = None
+ self.itemType = nwItemType.NO_TYPE
+ self.itemClass = nwItemClass.NO_CLASS
+ self.itemLayout = nwItemLayout.NO_LAYOUT
+ self.itemStatus = None
+ self.isExpanded = False
+ self.isExported = True
+
+ # Document Meta Data
+ self.charCount = 0
+ self.wordCount = 0
+ self.paraCount = 0
+ self.cursorPos = 0
+
+ return
+
+ ##
+ # XML Pack/Unpack
+ ##
+
+ def packXML(self, xParent):
+ """Packs all the data in the class instance into an XML object.
+ """
+ xPack = etree.SubElement(xParent,"item",attrib={
+ "handle" : str(self.itemHandle),
+ "order" : str(self.itemOrder),
+ "parent" : str(self.parHandle),
+ })
+ xSub = self._subPack(xPack,"name", text=str(self.itemName))
+ xSub = self._subPack(xPack,"type", text=str(self.itemType.name))
+ xSub = self._subPack(xPack,"class", text=str(self.itemClass.name))
+ xSub = self._subPack(xPack,"status", text=str(self.itemStatus))
+ if self.itemType == nwItemType.FILE:
+ xSub = self._subPack(xPack,"exported", text=str(self.isExported))
+ xSub = self._subPack(xPack,"layout", text=str(self.itemLayout.name))
+ xSub = self._subPack(xPack,"charCount", text=str(self.charCount), none=False)
+ xSub = self._subPack(xPack,"wordCount", text=str(self.wordCount), none=False)
+ xSub = self._subPack(xPack,"paraCount", text=str(self.paraCount), none=False)
+ xSub = self._subPack(xPack,"cursorPos", text=str(self.cursorPos), none=False)
+ else:
+ xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded))
+ return
+
+ def unpackXML(self, xItem):
+ """Sets the values from an XML entry of type 'item'.
+ """
+ if xItem.tag != "item":
+ logger.error("XML entry is not an NWItem")
+ return False
+
+ if "handle" in xItem.attrib:
+ self.itemHandle = xItem.attrib["handle"]
+ else:
+ logger.error("XML item entry does not have a handle")
+ return False
+
+ if "parent" in xItem.attrib:
+ self.parHandle = xItem.attrib["parent"]
+
+ setMap = {
+ "name" : self.setName,
+ "order" : self.setOrder,
+ "type" : self.setType,
+ "class" : self.setClass,
+ "layout" : self.setLayout,
+ "status" : self.setStatus,
+ "expanded" : self.setExpanded,
+ "exported" : self.setExported,
+ "charCount" : self.setCharCount,
+ "wordCount" : self.setWordCount,
+ "paraCount" : self.setParaCount,
+ "cursorPos" : self.setCursorPos,
+ }
+ for xValue in xItem:
+ if xValue.tag in setMap:
+ setMap[xValue.tag](xValue.text)
+ else:
+ logger.error("Unknown tag '%s'" % xValue.tag)
+
+ return True
+
+ @staticmethod
+ def _subPack(xParent, name, attrib=None, text=None, none=True):
+ """Packs the values into an xml element.
+ """
+ if not none and (text == None or text == "None"):
+ return None
+ xSub = etree.SubElement(xParent, name, attrib=attrib)
+ if text is not None:
+ xSub.text = text
+ return xSub
+
+ ##
+ # Set Item Values
+ ##
+
+ def setName(self, theName):
+ """Set the item name.
+ """
+ self.itemName = theName.strip()
+ return
+
+ def setHandle(self, theHandle):
+ """Set the item handle, and ensure it is valid.
+ """
+ if isinstance(theHandle, str):
+ if len(theHandle) == 13:
+ self.itemHandle = theHandle
+ else:
+ self.itemHandle = None
+ else:
+ self.itemHandle = None
+ return
+
+ def setParent(self, theParent):
+ """Set the parent handle, and ensure that it is valid.
+ """
+ if theParent is None:
+ self.parHandle = None
+ elif isinstance(theParent, str):
+ if len(theParent) == 13:
+ self.parHandle = theParent
+ else:
+ self.parHandle = None
+ else:
+ self.parHandle = None
+ return
+
+ def setOrder(self, theOrder):
+ """Set the item order, and ensure that it is valid. This value
+ is purely a meta value, not actually used by novelWriter.
+ """
+ self.itemOrder = checkInt(theOrder, 0)
+ return
+
+ def setType(self, theType):
+ """Set the item type from either a proper nwItemType, or set it
+ from a string representing a nwItemType.
+ """
+ if isinstance(theType, nwItemType):
+ self.itemType = theType
+ elif theType in nwItemType.__members__:
+ self.itemType = nwItemType[theType]
+ else:
+ logger.error("Unrecognised item type '%s'" % theType)
+ self.itemType = nwItemType.NO_TYPE
+ return
+
+ def setClass(self, theClass):
+ """Set the item class from either a proper nwItemClass, or set
+ it from a string representing a nwItemClass.
+ """
+ if isinstance(theClass, nwItemClass):
+ self.itemClass = theClass
+ elif theClass in nwItemClass.__members__:
+ self.itemClass = nwItemClass[theClass]
+ else:
+ logger.error("Unrecognised item class '%s'" % theClass)
+ self.itemClass = nwItemClass.NO_CLASS
+ return
+
+ def setLayout(self, theLayout):
+ """Set the item layout from either a proper nwItemLayout, or set
+ it from a string representing a nwItemLayout.
+ """
+ if isinstance(theLayout, nwItemLayout):
+ self.itemLayout = theLayout
+ elif theLayout in nwItemLayout.__members__:
+ self.itemLayout = nwItemLayout[theLayout]
+ else:
+ logger.error("Unrecognised item layout '%s'" % theLayout)
+ self.itemLayout = nwItemLayout.NO_LAYOUT
+ return
+
+ def setStatus(self, theStatus):
+ """Set the item status by looking it up in the valid status
+ items of the current project.
+ """
+ if self.itemClass == nwItemClass.NOVEL:
+ self.itemStatus = self.theProject.statusItems.checkEntry(theStatus)
+ else:
+ self.itemStatus = self.theProject.importItems.checkEntry(theStatus)
+ return
+
+ def setExpanded(self, expState):
+ """Save the expanded status of an item in the project tree.
+ """
+ if isinstance(expState, str):
+ self.isExpanded = expState == str(True)
+ else:
+ self.isExpanded = expState == True
+ return
+
+ def setExported(self, expState):
+ """Save the export flag.
+ """
+ if isinstance(expState, str):
+ self.isExported = expState == str(True)
+ else:
+ self.isExported = expState == True
+ return
+
+ ##
+ # Set Document Meta Data
+ ##
+
+ def setCharCount(self, theCount):
+ """Set the character count, and ensure that it is an integer.
+ """
+ self.charCount = checkInt(theCount, 0)
+ return
+
+ def setWordCount(self, theCount):
+ """Set the word count, and ensure that it is an integer.
+ """
+ self.wordCount = checkInt(theCount, 0)
+ return
+
+ def setParaCount(self, theCount):
+ """Set the paragraph count, and ensure that it is an integer.
+ """
+ self.paraCount = checkInt(theCount, 0)
+ return
+
+ def setCursorPos(self, thePosition):
+ """Set the cursor position, and ensure that it is an integer.
+ """
+ self.cursorPos = checkInt(thePosition, 0)
+ return
+
+# END Class NWItem
diff --git a/nw/core/project.py b/nw/core/project.py
index f7e412db..f6564012 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -7,10 +7,8 @@
File History:
Created: 2018-09-29 [0.0.1] NWProject
- Created: 2018-10-27 [0.0.1] NWItem
Created: 2019-05-19 [0.1.3] NWStatus
Created: 2019-10-21 [0.3.1] OptionState
- Merged: 2020-05-07 [0.4.5] Moved NWItem class to this file
Merged: 2020-05-07 [0.4.5] Moved NWStatus class to this file
Added: 2020-05-07 [0.4.5] NWTree
Rewritten: 2020-02-19 [0.4.5] OptionState
@@ -44,6 +42,7 @@ from shutil import make_archive
from PyQt5.QtWidgets import QMessageBox
+from nw.core.item import NWItem
from nw.core.document import NWDoc
from nw.common import checkString, checkBool, checkInt, formatTimeStamp
from nw.constants import (
@@ -1608,252 +1607,6 @@ class NWTree():
# END Class NWTree
-# =============================================================================================== #
-# NWItem
-# Class holding the project items making up the NWProject
-# =============================================================================================== #
-
-class NWItem():
-
- def __init__(self, theProject):
-
- self.theProject = theProject
-
- self.itemName = ""
- self.itemHandle = None
- self.parHandle = None
- self.itemOrder = None
- self.itemType = nwItemType.NO_TYPE
- self.itemClass = nwItemClass.NO_CLASS
- self.itemLayout = nwItemLayout.NO_LAYOUT
- self.itemStatus = None
- self.isExpanded = False
- self.isExported = True
-
- # Document Meta Data
- self.charCount = 0
- self.wordCount = 0
- self.paraCount = 0
- self.cursorPos = 0
-
- return
-
- ##
- # XML Pack/Unpack
- ##
-
- def packXML(self, xParent):
- """Packs all the data in the class instance into an XML object.
- """
- xPack = etree.SubElement(xParent,"item",attrib={
- "handle" : str(self.itemHandle),
- "order" : str(self.itemOrder),
- "parent" : str(self.parHandle),
- })
- xSub = self._subPack(xPack,"name", text=str(self.itemName))
- xSub = self._subPack(xPack,"type", text=str(self.itemType.name))
- xSub = self._subPack(xPack,"class", text=str(self.itemClass.name))
- xSub = self._subPack(xPack,"status", text=str(self.itemStatus))
- if self.itemType == nwItemType.FILE:
- xSub = self._subPack(xPack,"exported", text=str(self.isExported))
- xSub = self._subPack(xPack,"layout", text=str(self.itemLayout.name))
- xSub = self._subPack(xPack,"charCount", text=str(self.charCount), none=False)
- xSub = self._subPack(xPack,"wordCount", text=str(self.wordCount), none=False)
- xSub = self._subPack(xPack,"paraCount", text=str(self.paraCount), none=False)
- xSub = self._subPack(xPack,"cursorPos", text=str(self.cursorPos), none=False)
- else:
- xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded))
- return
-
- def unpackXML(self, xItem):
- """Sets the values from an XML entry of type 'item'.
- """
- if xItem.tag != "item":
- logger.error("XML entry is not an NWItem")
- return False
-
- if "handle" in xItem.attrib:
- self.itemHandle = xItem.attrib["handle"]
- else:
- logger.error("XML item entry does not have a handle")
- return False
-
- if "parent" in xItem.attrib:
- self.parHandle = xItem.attrib["parent"]
-
- setMap = {
- "name" : self.setName,
- "order" : self.setOrder,
- "type" : self.setType,
- "class" : self.setClass,
- "layout" : self.setLayout,
- "status" : self.setStatus,
- "expanded" : self.setExpanded,
- "exported" : self.setExported,
- "charCount" : self.setCharCount,
- "wordCount" : self.setWordCount,
- "paraCount" : self.setParaCount,
- "cursorPos" : self.setCursorPos,
- }
- for xValue in xItem:
- if xValue.tag in setMap:
- setMap[xValue.tag](xValue.text)
- else:
- logger.error("Unknown tag '%s'" % xValue.tag)
-
- return True
-
- @staticmethod
- def _subPack(xParent, name, attrib=None, text=None, none=True):
- """Packs the values into an xml element.
- """
- if not none and (text == None or text == "None"):
- return None
- xSub = etree.SubElement(xParent, name, attrib=attrib)
- if text is not None:
- xSub.text = text
- return xSub
-
- ##
- # Set Item Values
- ##
-
- def setName(self, theName):
- """Set the item name.
- """
- self.itemName = theName.strip()
- return
-
- def setHandle(self, theHandle):
- """Set the item handle, and ensure it is valid.
- """
- if isinstance(theHandle, str):
- if len(theHandle) == 13:
- self.itemHandle = theHandle
- else:
- self.itemHandle = None
- else:
- self.itemHandle = None
- return
-
- def setParent(self, theParent):
- """Set the parent handle, and ensure that it is valid.
- """
- if theParent is None:
- self.parHandle = None
- elif isinstance(theParent, str):
- if len(theParent) == 13:
- self.parHandle = theParent
- else:
- self.parHandle = None
- else:
- self.parHandle = None
- return
-
- def setOrder(self, theOrder):
- """Set the item order, and ensure that it is valid. This value
- is purely a meta value, not actually used by novelWriter.
- """
- self.itemOrder = checkInt(theOrder, 0)
- return
-
- def setType(self, theType):
- """Set the item type from either a proper nwItemType, or set it
- from a string representing a nwItemType.
- """
- if isinstance(theType, nwItemType):
- self.itemType = theType
- elif theType in nwItemType.__members__:
- self.itemType = nwItemType[theType]
- else:
- logger.error("Unrecognised item type '%s'" % theType)
- self.itemType = nwItemType.NO_TYPE
- return
-
- def setClass(self, theClass):
- """Set the item class from either a proper nwItemClass, or set
- it from a string representing a nwItemClass.
- """
- if isinstance(theClass, nwItemClass):
- self.itemClass = theClass
- elif theClass in nwItemClass.__members__:
- self.itemClass = nwItemClass[theClass]
- else:
- logger.error("Unrecognised item class '%s'" % theClass)
- self.itemClass = nwItemClass.NO_CLASS
- return
-
- def setLayout(self, theLayout):
- """Set the item layout from either a proper nwItemLayout, or set
- it from a string representing a nwItemLayout.
- """
- if isinstance(theLayout, nwItemLayout):
- self.itemLayout = theLayout
- elif theLayout in nwItemLayout.__members__:
- self.itemLayout = nwItemLayout[theLayout]
- else:
- logger.error("Unrecognised item layout '%s'" % theLayout)
- self.itemLayout = nwItemLayout.NO_LAYOUT
- return
-
- def setStatus(self, theStatus):
- """Set the item status by looking it up in the valid status
- items of the current project.
- """
- if self.itemClass == nwItemClass.NOVEL:
- self.itemStatus = self.theProject.statusItems.checkEntry(theStatus)
- else:
- self.itemStatus = self.theProject.importItems.checkEntry(theStatus)
- return
-
- def setExpanded(self, expState):
- """Save the expanded status of an item in the project tree.
- """
- if isinstance(expState, str):
- self.isExpanded = expState == str(True)
- else:
- self.isExpanded = expState == True
- return
-
- def setExported(self, expState):
- """Save the export flag.
- """
- if isinstance(expState, str):
- self.isExported = expState == str(True)
- else:
- self.isExported = expState == True
- return
-
- ##
- # Set Document Meta Data
- ##
-
- def setCharCount(self, theCount):
- """Set the character count, and ensure that it is an integer.
- """
- self.charCount = checkInt(theCount, 0)
- return
-
- def setWordCount(self, theCount):
- """Set the word count, and ensure that it is an integer.
- """
- self.wordCount = checkInt(theCount, 0)
- return
-
- def setParaCount(self, theCount):
- """Set the paragraph count, and ensure that it is an integer.
- """
- self.paraCount = checkInt(theCount, 0)
- return
-
- def setCursorPos(self, thePosition):
- """Set the cursor position, and ensure that it is an integer.
- """
- self.cursorPos = checkInt(thePosition, 0)
- return
-
-# END Class NWItem
-
# =============================================================================================== #
# NWStatus
# Class holding the item status values stored in the NWProject
From 017d9c3801eff61b5aafdb2efad466b6a889b789 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sat, 6 Jun 2020 15:18:49 +0200
Subject: [PATCH 27/32] Moved NWTree to core/tree.py
---
nw/core/item.py | 2 +-
nw/core/project.py | 387 +----------------------------------------
nw/core/tree.py | 421 +++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 424 insertions(+), 386 deletions(-)
create mode 100644 nw/core/tree.py
diff --git a/nw/core/item.py b/nw/core/item.py
index 5c0f26e0..c9fdfee6 100644
--- a/nw/core/item.py
+++ b/nw/core/item.py
@@ -3,7 +3,7 @@
novelWriter – Project Item Class
==================================
- Class holding the data off a project tree item
+ Class holding the data of a project tree item
File History:
Created: 2018-10-27 [0.0.1] NWItem
diff --git a/nw/core/project.py b/nw/core/project.py
index f6564012..8d9bcec0 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -3,14 +3,13 @@
novelWriter – Project Wrapper
===============================
- Class wrapping the data if a novelWriter project
+ Class wrapping the data of a novelWriter project
File History:
Created: 2018-09-29 [0.0.1] NWProject
Created: 2019-05-19 [0.1.3] NWStatus
Created: 2019-10-21 [0.3.1] OptionState
Merged: 2020-05-07 [0.4.5] Moved NWStatus class to this file
- Added: 2020-05-07 [0.4.5] NWTree
Rewritten: 2020-02-19 [0.4.5] OptionState
This file is a part of novelWriter
@@ -42,6 +41,7 @@ from shutil import make_archive
from PyQt5.QtWidgets import QMessageBox
+from nw.core.tree import NWTree
from nw.core.item import NWItem
from nw.core.document import NWDoc
from nw.common import checkString, checkBool, checkInt, formatTimeStamp
@@ -1224,389 +1224,6 @@ class NWProject():
# END Class NWProject
-# =============================================================================================== #
-# NWTree
-# Class holding the project tree for the NWProject
-# =============================================================================================== #
-
-class NWTree():
-
- def __init__(self, theProject):
-
- self.theProject = theProject
-
- self._projTree = {} # Holds all the items of the project
- self._treeOrder = [] # The order of the tree items on the tree view
- self._treeRoots = [] # The root items of the tree
- self._trashRoot = None # The handle of the trash root folder
- self._theLength = 0 # Always the length of _treeOrder
- self._theIndex = 0 # The current iterator index
- self._treeChanged = False # True if tree structure has changed
- self._handleSeed = None # Used for generating handles for testing
-
- return
-
- ##
- # Class Methods
- ##
-
- def clear(self):
- """Clear the item tree entirely.
- """
- self._projTree = {}
- self._treeOrder = []
- self._treeRoots = []
- self._trashRoot = None
- self._theLength = 0
- self._theIndex = 0
- self._treeChanged = False
- return
-
- def handles(self):
- """Returns a copy of the list of all the active handles.
- """
- return self._treeOrder.copy()
-
- def append(self, tHandle, pHandle, nwItem):
- """Add a new item to the end of the tree.
- """
- tHandle = checkString(tHandle, None, True)
- pHandle = checkString(pHandle, None, True)
- if tHandle is None:
- tHandle = self._makeHandle()
-
- logger.verbose("Adding entry %s with parent %s" % (str(tHandle), str(pHandle)))
-
- nwItem.setHandle(tHandle)
- nwItem.setParent(pHandle)
-
- self._projTree[tHandle] = nwItem
- self._treeOrder.append(tHandle)
-
- if nwItem.itemType == nwItemType.ROOT:
- logger.verbose("Entry %s is a root item" % str(tHandle))
- self._treeRoots.append(tHandle)
-
- if nwItem.itemType == nwItemType.TRASH:
- if self._trashRoot is None:
- logger.verbose("Entry %s is the trash folder" % str(tHandle))
- self._trashRoot = tHandle
- else:
- logger.error("Only one trash folder allowed")
-
- self._theLength = len(self._treeOrder)
- self._setTreeChanged(True)
-
- return
-
- def packXML(self, xParent):
- """Pack the content of the tree into an XML object.
- """
- xContent = etree.SubElement(xParent, "content", attrib={
- "count":str(self._theLength)}
- )
- for tHandle in self._treeOrder:
- tItem = self.__getitem__(tHandle)
- tItem.packXML(xContent)
- return
-
- def unpackXML(self, xContent):
- """Iterate through all items of a content XML object and add
- them to the project tree.
- """
- if xContent.tag != "content":
- logger.error("XML entry is not a NWTree")
- return False
-
- self.clear()
- for xItem in xContent:
- nwItem = NWItem(self.theProject)
- if nwItem.unpackXML(xItem):
- self.append(nwItem.itemHandle, nwItem.parHandle, nwItem)
-
- return True
-
- def writeToCFiles(self):
- """Write the convenience table of contents files in the root of
- the project directory. These files are there to assist the user
- if they wish to browse the stored files.
- """
- tocText = path.join(self.theProject.projPath, nwFiles.TOC_TXT)
- tocJson = path.join(self.theProject.projPath, nwFiles.TOC_JSON)
-
- jsonData = []
- try:
- # Dump the text
- with open(tocText, mode="w", encoding="utf8") as outFile:
- outFile.write("\n")
- outFile.write(" Table of Contents\n")
- outFile.write("===================\n")
- outFile.write("\n")
- outFile.write(" %-25s %-9s %s\n" %("File Name","Class","Document Label"))
- outFile.write("-"*80+"\n")
- for tHandle in sorted(self._treeOrder):
- tItem = self.__getitem__(tHandle)
- if tItem is None:
- continue
- tFile = tHandle+".nwd"
- if path.isfile(path.join(self.theProject.projContent, tFile)):
- outFile.write(" %-25s %-9s %s\n" %(
- path.join("content", tFile),
- tItem.itemClass.name,
- tItem.itemName,
- ))
- jsonData.append([
- path.join("content", tFile),
- tItem.itemClass.name,
- tItem.itemName,
- ])
- outFile.write("\n")
-
- # Dump the JSON
- with open(tocJson, mode="w+", encoding="utf8") as outFile:
- outFile.write(json.dumps(jsonData, indent=2))
-
- except Exception as e:
- logger.error(str(e))
-
- return
-
- ##
- # Tree Structure Methods
- ##
-
- def trashRoot(self):
- """Returns the handle of the trash folder, or None if there
- isn't one.
- """
- if self._trashRoot:
- return self._trashRoot
- return None
-
- def findRoot(self, theClass):
- """Find the root item for a given class.
- Note: This returns the first item for class CUSTOM.
- """
- for aRoot in self._treeRoots:
- tItem = self.__getitem__(aRoot)
- if tItem is None:
- continue
- if theClass == tItem.itemClass:
- return tItem.itemHandle
- return None
-
- def checkRootUnique(self, theClass):
- """Checks if there already is a root entry of class 'theClass'
- in the root of the project tree. CUSTOM class is skipped as it
- is not required to be unique.
- """
- if theClass == nwItemClass.CUSTOM:
- return True
- for aRoot in self._treeRoots:
- tItem = self.__getitem__(aRoot)
- if theClass == tItem.itemClass:
- return False
- return True
-
- def getRootItem(self, tHandle):
- """Iterate upwards in the tree until we find the item with
- parent None, the root item. We do this with a for loop with a
- maximum depth of 200 to make infinite loops impossible.
- """
- tItem = self.__getitem__(tHandle)
- if tItem is not None:
- for i in range(200):
- if tItem.parHandle is None:
- return tHandle
- else:
- tHandle = tItem.parHandle
- tItem = self.__getitem__(tHandle)
- if tItem is None:
- return tHandle
- return None
-
- def getItemPath(self, tHandle):
- """Iterate upwards in the tree until we find the item with
- parent None, the root item, and return the list of handles.
- We do this with a for loop with a maximum depth of 200 to make
- infinite loops impossible.
- """
- tTree = []
- tItem = self.__getitem__(tHandle)
- if tItem is not None:
- tTree.append(tHandle)
- for i in range(200):
- if tItem.parHandle is None:
- return tTree
- else:
- tHandle = tItem.parHandle
- tItem = self.__getitem__(tHandle)
- if tItem is None:
- return tTree
- else:
- tTree.append(tHandle)
- return tTree
-
- ##
- # Setters
- ##
-
- def setOrder(self, newOrder):
- """Reorders the tree based on a list of items.
- """
- tmpOrder = []
-
- # Add all known elements to a new temp list
- for tHandle in newOrder:
- if tHandle in self._projTree:
- tmpOrder.append(tHandle)
- else:
- logger.error("Handle %s in new tree order is not in project tree" % tHandle)
-
- # Do a reverse lookup to check for items that will be lost
- # This is mainly for debugging purposes
- for tHandle in self._treeOrder:
- if tHandle not in tmpOrder:
- logger.warning("Handle %s in old tree order is not in new tree order" % tHandle)
-
- # Save the temp list
- self._treeOrder = tmpOrder
- self._theLength = len(self._treeOrder)
- self._setTreeChanged(True)
- logger.verbose("Project tree order updated")
-
- return
-
- def setSeed(self, theSeed):
- """Used for debugging!
- Sets a seed for generating handles so that they always come out
- in a predictable order.
- """
- self._handleSeed = theSeed
- return
-
- ##
- # Getters
- ##
-
- def countTypes(self):
- """Count the number of files, folders and roots in the project.
- """
- nRoot = 0
- nFolder = 0
- nFile = 0
-
- for tHandle in self._treeOrder:
- tItem = self.__getitem__(tHandle)
- if tItem is None:
- continue
- elif tItem.itemType == nwItemType.ROOT:
- nRoot += 1
- elif tItem.itemType == nwItemType.FOLDER:
- nFolder += 1
- elif tItem.itemType == nwItemType.FILE:
- nFile += 1
-
- return nRoot, nFolder, nFile
-
- ##
- # Meta Methods
- ##
-
- def __len__(self):
- """Return the length counter. Does not check that it is correct!
- """
- return self._theLength
-
- def __bool__(self):
- """Returns True if the tree has any entries.
- """
- return self._theLength > 0
-
- ##
- # Item Access Methods
- ##
-
- def __getitem__(self, tHandle):
- """Return a project item based on its handle. Returns None if
- the handle doesn't exist in the project.
- """
- if tHandle in self._projTree:
- return self._projTree[tHandle]
- logger.error("No tree item with handle %s" % str(tHandle))
- return None
-
- def __delitem__(self, tHandle):
- """This only removes the item from the order list, but not from
- the project tree.
- """
- if tHandle not in self._treeOrder:
- logger.warning(
- "Could not remove item %s from project tree as it does not exist" % tHandle
- )
- return False
- self._treeOrder.remove(tHandle)
- self._theLength = len(self._treeOrder)
- self._setTreeChanged(True)
- return True
-
- def __contains__(self, tHandle):
- """Checks if a handle exists in the tree.
- """
- return tHandle in self._treeOrder
-
- ##
- # Iterator Methods
- ##
-
- def __iter__(self):
- """Initiates the iterator.
- """
- self._theIndex = 0
- return self
-
- def __next__(self):
- """Returns the item from the next entry in the _treeOrder list.
- """
- if self._theIndex < self._theLength:
- theItem = self.__getitem__(self._treeOrder[self._theIndex])
- self._theIndex += 1
- return theItem
- else:
- raise StopIteration
-
- ##
- # Internal Functions
- ##
-
- def _setTreeChanged(self, theState):
- """Set the changed flag to theState, and if being set to True,
- propagate that state change to the parent NWProject class.
- """
- self._treeChanged = theState
- if theState:
- self.theProject.setProjectChanged(True)
- return
-
- def _makeHandle(self, addSeed=""):
- """Generate a unique item handle. In the unlikely event that the
- key already exists, salt the seed and generate a new handle.
- """
- if self._handleSeed is None:
- newSeed = str(time()) + addSeed
- else:
- # This is used for debugging
- newSeed = str(self._handleSeed)
- self._handleSeed += 1
- logger.verbose("Generating handle with seed '%s'" % newSeed)
- itemHandle = sha256(newSeed.encode()).hexdigest()[0:13]
- if itemHandle in self._projTree:
- logger.warning("Duplicate handle encountered! Retrying ...")
- itemHandle = self._makeHandle(addSeed+"!")
- return itemHandle
-
-# END Class NWTree
-
# =============================================================================================== #
# NWStatus
# Class holding the item status values stored in the NWProject
diff --git a/nw/core/tree.py b/nw/core/tree.py
new file mode 100644
index 00000000..be99f307
--- /dev/null
+++ b/nw/core/tree.py
@@ -0,0 +1,421 @@
+# -*- coding: utf-8 -*-
+"""novelWriter Project Tree Class
+
+ novelWriter – Project Tree Class
+==================================
+ Class holding the data of the project tree
+
+ File History:
+ Created: 2020-05-07 [0.4.5] NWTree
+
+ 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 json
+import nw
+
+from os import path
+from lxml import etree
+from hashlib import sha256
+from time import time
+
+from PyQt5.QtWidgets import QMessageBox
+
+from nw.core.item import NWItem
+from nw.common import checkString
+from nw.constants import nwFiles, nwItemType, nwItemClass
+
+logger = logging.getLogger(__name__)
+
+class NWTree():
+
+ def __init__(self, theProject):
+
+ self.theProject = theProject
+
+ self._projTree = {} # Holds all the items of the project
+ self._treeOrder = [] # The order of the tree items on the tree view
+ self._treeRoots = [] # The root items of the tree
+ self._trashRoot = None # The handle of the trash root folder
+ self._theLength = 0 # Always the length of _treeOrder
+ self._theIndex = 0 # The current iterator index
+ self._treeChanged = False # True if tree structure has changed
+ self._handleSeed = None # Used for generating handles for testing
+
+ return
+
+ ##
+ # Class Methods
+ ##
+
+ def clear(self):
+ """Clear the item tree entirely.
+ """
+ self._projTree = {}
+ self._treeOrder = []
+ self._treeRoots = []
+ self._trashRoot = None
+ self._theLength = 0
+ self._theIndex = 0
+ self._treeChanged = False
+ return
+
+ def handles(self):
+ """Returns a copy of the list of all the active handles.
+ """
+ return self._treeOrder.copy()
+
+ def append(self, tHandle, pHandle, nwItem):
+ """Add a new item to the end of the tree.
+ """
+ tHandle = checkString(tHandle, None, True)
+ pHandle = checkString(pHandle, None, True)
+ if tHandle is None:
+ tHandle = self._makeHandle()
+
+ logger.verbose("Adding entry %s with parent %s" % (str(tHandle), str(pHandle)))
+
+ nwItem.setHandle(tHandle)
+ nwItem.setParent(pHandle)
+
+ self._projTree[tHandle] = nwItem
+ self._treeOrder.append(tHandle)
+
+ if nwItem.itemType == nwItemType.ROOT:
+ logger.verbose("Entry %s is a root item" % str(tHandle))
+ self._treeRoots.append(tHandle)
+
+ if nwItem.itemType == nwItemType.TRASH:
+ if self._trashRoot is None:
+ logger.verbose("Entry %s is the trash folder" % str(tHandle))
+ self._trashRoot = tHandle
+ else:
+ logger.error("Only one trash folder allowed")
+
+ self._theLength = len(self._treeOrder)
+ self._setTreeChanged(True)
+
+ return
+
+ def packXML(self, xParent):
+ """Pack the content of the tree into an XML object.
+ """
+ xContent = etree.SubElement(xParent, "content", attrib={
+ "count":str(self._theLength)}
+ )
+ for tHandle in self._treeOrder:
+ tItem = self.__getitem__(tHandle)
+ tItem.packXML(xContent)
+ return
+
+ def unpackXML(self, xContent):
+ """Iterate through all items of a content XML object and add
+ them to the project tree.
+ """
+ if xContent.tag != "content":
+ logger.error("XML entry is not a NWTree")
+ return False
+
+ self.clear()
+ for xItem in xContent:
+ nwItem = NWItem(self.theProject)
+ if nwItem.unpackXML(xItem):
+ self.append(nwItem.itemHandle, nwItem.parHandle, nwItem)
+
+ return True
+
+ def writeToCFiles(self):
+ """Write the convenience table of contents files in the root of
+ the project directory. These files are there to assist the user
+ if they wish to browse the stored files.
+ """
+ tocText = path.join(self.theProject.projPath, nwFiles.TOC_TXT)
+ tocJson = path.join(self.theProject.projPath, nwFiles.TOC_JSON)
+
+ jsonData = []
+ try:
+ # Dump the text
+ with open(tocText, mode="w", encoding="utf8") as outFile:
+ outFile.write("\n")
+ outFile.write(" Table of Contents\n")
+ outFile.write("===================\n")
+ outFile.write("\n")
+ outFile.write(" %-25s %-9s %s\n" %("File Name","Class","Document Label"))
+ outFile.write("-"*80+"\n")
+ for tHandle in sorted(self._treeOrder):
+ tItem = self.__getitem__(tHandle)
+ if tItem is None:
+ continue
+ tFile = tHandle+".nwd"
+ if path.isfile(path.join(self.theProject.projContent, tFile)):
+ outFile.write(" %-25s %-9s %s\n" %(
+ path.join("content", tFile),
+ tItem.itemClass.name,
+ tItem.itemName,
+ ))
+ jsonData.append([
+ path.join("content", tFile),
+ tItem.itemClass.name,
+ tItem.itemName,
+ ])
+ outFile.write("\n")
+
+ # Dump the JSON
+ with open(tocJson, mode="w+", encoding="utf8") as outFile:
+ outFile.write(json.dumps(jsonData, indent=2))
+
+ except Exception as e:
+ logger.error(str(e))
+
+ return
+
+ ##
+ # Tree Structure Methods
+ ##
+
+ def trashRoot(self):
+ """Returns the handle of the trash folder, or None if there
+ isn't one.
+ """
+ if self._trashRoot:
+ return self._trashRoot
+ return None
+
+ def findRoot(self, theClass):
+ """Find the root item for a given class.
+ Note: This returns the first item for class CUSTOM.
+ """
+ for aRoot in self._treeRoots:
+ tItem = self.__getitem__(aRoot)
+ if tItem is None:
+ continue
+ if theClass == tItem.itemClass:
+ return tItem.itemHandle
+ return None
+
+ def checkRootUnique(self, theClass):
+ """Checks if there already is a root entry of class 'theClass'
+ in the root of the project tree. CUSTOM class is skipped as it
+ is not required to be unique.
+ """
+ if theClass == nwItemClass.CUSTOM:
+ return True
+ for aRoot in self._treeRoots:
+ tItem = self.__getitem__(aRoot)
+ if theClass == tItem.itemClass:
+ return False
+ return True
+
+ def getRootItem(self, tHandle):
+ """Iterate upwards in the tree until we find the item with
+ parent None, the root item. We do this with a for loop with a
+ maximum depth of 200 to make infinite loops impossible.
+ """
+ tItem = self.__getitem__(tHandle)
+ if tItem is not None:
+ for i in range(200):
+ if tItem.parHandle is None:
+ return tHandle
+ else:
+ tHandle = tItem.parHandle
+ tItem = self.__getitem__(tHandle)
+ if tItem is None:
+ return tHandle
+ return None
+
+ def getItemPath(self, tHandle):
+ """Iterate upwards in the tree until we find the item with
+ parent None, the root item, and return the list of handles.
+ We do this with a for loop with a maximum depth of 200 to make
+ infinite loops impossible.
+ """
+ tTree = []
+ tItem = self.__getitem__(tHandle)
+ if tItem is not None:
+ tTree.append(tHandle)
+ for i in range(200):
+ if tItem.parHandle is None:
+ return tTree
+ else:
+ tHandle = tItem.parHandle
+ tItem = self.__getitem__(tHandle)
+ if tItem is None:
+ return tTree
+ else:
+ tTree.append(tHandle)
+ return tTree
+
+ ##
+ # Setters
+ ##
+
+ def setOrder(self, newOrder):
+ """Reorders the tree based on a list of items.
+ """
+ tmpOrder = []
+
+ # Add all known elements to a new temp list
+ for tHandle in newOrder:
+ if tHandle in self._projTree:
+ tmpOrder.append(tHandle)
+ else:
+ logger.error("Handle %s in new tree order is not in project tree" % tHandle)
+
+ # Do a reverse lookup to check for items that will be lost
+ # This is mainly for debugging purposes
+ for tHandle in self._treeOrder:
+ if tHandle not in tmpOrder:
+ logger.warning("Handle %s in old tree order is not in new tree order" % tHandle)
+
+ # Save the temp list
+ self._treeOrder = tmpOrder
+ self._theLength = len(self._treeOrder)
+ self._setTreeChanged(True)
+ logger.verbose("Project tree order updated")
+
+ return
+
+ def setSeed(self, theSeed):
+ """Used for debugging!
+ Sets a seed for generating handles so that they always come out
+ in a predictable order.
+ """
+ self._handleSeed = theSeed
+ return
+
+ ##
+ # Getters
+ ##
+
+ def countTypes(self):
+ """Count the number of files, folders and roots in the project.
+ """
+ nRoot = 0
+ nFolder = 0
+ nFile = 0
+
+ for tHandle in self._treeOrder:
+ tItem = self.__getitem__(tHandle)
+ if tItem is None:
+ continue
+ elif tItem.itemType == nwItemType.ROOT:
+ nRoot += 1
+ elif tItem.itemType == nwItemType.FOLDER:
+ nFolder += 1
+ elif tItem.itemType == nwItemType.FILE:
+ nFile += 1
+
+ return nRoot, nFolder, nFile
+
+ ##
+ # Meta Methods
+ ##
+
+ def __len__(self):
+ """Return the length counter. Does not check that it is correct!
+ """
+ return self._theLength
+
+ def __bool__(self):
+ """Returns True if the tree has any entries.
+ """
+ return self._theLength > 0
+
+ ##
+ # Item Access Methods
+ ##
+
+ def __getitem__(self, tHandle):
+ """Return a project item based on its handle. Returns None if
+ the handle doesn't exist in the project.
+ """
+ if tHandle in self._projTree:
+ return self._projTree[tHandle]
+ logger.error("No tree item with handle %s" % str(tHandle))
+ return None
+
+ def __delitem__(self, tHandle):
+ """This only removes the item from the order list, but not from
+ the project tree.
+ """
+ if tHandle not in self._treeOrder:
+ logger.warning(
+ "Could not remove item %s from project tree as it does not exist" % tHandle
+ )
+ return False
+ self._treeOrder.remove(tHandle)
+ self._theLength = len(self._treeOrder)
+ self._setTreeChanged(True)
+ return True
+
+ def __contains__(self, tHandle):
+ """Checks if a handle exists in the tree.
+ """
+ return tHandle in self._treeOrder
+
+ ##
+ # Iterator Methods
+ ##
+
+ def __iter__(self):
+ """Initiates the iterator.
+ """
+ self._theIndex = 0
+ return self
+
+ def __next__(self):
+ """Returns the item from the next entry in the _treeOrder list.
+ """
+ if self._theIndex < self._theLength:
+ theItem = self.__getitem__(self._treeOrder[self._theIndex])
+ self._theIndex += 1
+ return theItem
+ else:
+ raise StopIteration
+
+ ##
+ # Internal Functions
+ ##
+
+ def _setTreeChanged(self, theState):
+ """Set the changed flag to theState, and if being set to True,
+ propagate that state change to the parent NWProject class.
+ """
+ self._treeChanged = theState
+ if theState:
+ self.theProject.setProjectChanged(True)
+ return
+
+ def _makeHandle(self, addSeed=""):
+ """Generate a unique item handle. In the unlikely event that the
+ key already exists, salt the seed and generate a new handle.
+ """
+ if self._handleSeed is None:
+ newSeed = str(time()) + addSeed
+ else:
+ # This is used for debugging
+ newSeed = str(self._handleSeed)
+ self._handleSeed += 1
+ logger.verbose("Generating handle with seed '%s'" % newSeed)
+ itemHandle = sha256(newSeed.encode()).hexdigest()[0:13]
+ if itemHandle in self._projTree:
+ logger.warning("Duplicate handle encountered! Retrying ...")
+ itemHandle = self._makeHandle(addSeed+"!")
+ return itemHandle
+
+# END Class NWTree
From 59795e62f746a49147a7620b7f047d6ef965f1e9 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sat, 6 Jun 2020 15:23:03 +0200
Subject: [PATCH 28/32] Moved NWStatus to core/status.py
---
nw/core/item.py | 2 +-
nw/core/project.py | 167 +-------------------------------------
nw/core/status.py | 194 +++++++++++++++++++++++++++++++++++++++++++++
nw/core/tree.py | 4 +-
4 files changed, 197 insertions(+), 170 deletions(-)
create mode 100644 nw/core/status.py
diff --git a/nw/core/item.py b/nw/core/item.py
index c9fdfee6..4dd9ddb9 100644
--- a/nw/core/item.py
+++ b/nw/core/item.py
@@ -6,7 +6,7 @@
Class holding the data of a project tree item
File History:
- Created: 2018-10-27 [0.0.1] NWItem
+ Created: 2018-10-27 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
diff --git a/nw/core/project.py b/nw/core/project.py
index 8d9bcec0..534a2554 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -7,9 +7,7 @@
File History:
Created: 2018-09-29 [0.0.1] NWProject
- Created: 2019-05-19 [0.1.3] NWStatus
Created: 2019-10-21 [0.3.1] OptionState
- Merged: 2020-05-07 [0.4.5] Moved NWStatus class to this file
Rewritten: 2020-02-19 [0.4.5] OptionState
This file is a part of novelWriter
@@ -44,6 +42,7 @@ from PyQt5.QtWidgets import QMessageBox
from nw.core.tree import NWTree
from nw.core.item import NWItem
from nw.core.document import NWDoc
+from nw.core.status import NWStatus
from nw.common import checkString, checkBool, checkInt, formatTimeStamp
from nw.constants import (
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwAlert
@@ -1224,170 +1223,6 @@ class NWProject():
# END Class NWProject
-# =============================================================================================== #
-# NWStatus
-# Class holding the item status values stored in the NWProject
-# =============================================================================================== #
-
-class NWStatus():
-
- def __init__(self):
- self.theLabels = []
- self.theColours = []
- self.theCounts = []
- self.theMap = {}
- self.theLength = 0
- self.theIndex = 0
- return
-
- def addEntry(self, theLabel, theColours):
- """Add a status entry to the status object, but ensure it isn't
- a duplicate.
- """
- theLabel = theLabel.strip()
- if self.lookupEntry(theLabel) is None:
- self.theLabels.append(theLabel)
- self.theColours.append(theColours)
- self.theCounts.append(0)
- self.theMap[theLabel] = self.theLength
- self.theLength += 1
- return True
-
- def lookupEntry(self, theLabel):
- """Look up a status entry in the object lists, and return it if
- it exists.
- """
- if theLabel is None:
- return None
- theLabel = theLabel.strip()
- if theLabel in self.theMap.keys():
- return self.theMap[theLabel]
- return None
-
- def checkEntry(self, theStatus):
- """Check if a status value is valid, and returns the safe
- reference to be used internally.
- """
- if isinstance(theStatus, str):
- theStatus = theStatus.strip()
- if self.lookupEntry(theStatus) is not None:
- return theStatus
- theStatus = checkInt(theStatus, 0, False)
- if theStatus >= 0 and theStatus < self.theLength:
- return self.theLabels[theStatus]
-
- def setNewEntries(self, newList):
- """Update the list of entries after they have been modified by
- the GUI tool.
- """
- replaceMap = {}
-
- if newList is not None:
- self.theLabels = []
- self.theColours = []
- self.theCounts = []
- self.theMap = {}
- self.theLength = 0
- self.theIndex = 0
-
- for nName, nR, nG, nB, oName in newList:
- self.addEntry(nName, (nR, nG, nB))
- if nName != oName and oName is not None:
- replaceMap[oName] = nName
-
- return replaceMap
-
- def resetCounts(self):
- """Clear the counts of references to the status entries.
- """
- self.theCounts = [0]*self.theLength
- return
-
- def countEntry(self, theLabel):
- """Lookup the usage count of a given entry.
- """
- theIndex = self.lookupEntry(theLabel)
- if theIndex is not None:
- self.theCounts[theIndex] += 1
- return
-
- def packEntries(self, xParent):
- """Pack the status entries into an XML object for saving to the
- main project file.
- """
- for n in range(self.theLength):
- xSub = etree.SubElement(xParent,"entry",attrib={
- "blue" : str(self.theColours[n][2]),
- "green" : str(self.theColours[n][1]),
- "red" : str(self.theColours[n][0]),
- })
- xSub.text = self.theLabels[n]
- return True
-
- def unpackEntries(self, xParent):
- """Unpack an XML tree and set the class values.
- """
- theLabels = []
- theColours = []
-
- for xChild in xParent:
- theLabels.append(xChild.text)
- if "red" in xChild.attrib:
- cR = checkInt(xChild.attrib["red"],0,False)
- else:
- cR = 0
- if "green" in xChild.attrib:
- cG = checkInt(xChild.attrib["green"],0,False)
- else:
- cG = 0
- if "blue" in xChild.attrib:
- cB = checkInt(xChild.attrib["blue"],0,False)
- else:
- cB = 0
- theColours.append((cR,cG,cB))
-
- if len(theLabels) > 0:
- self.theLabels = []
- self.theColours = []
- self.theCounts = []
- self.theMap = {}
- self.theLength = 0
- self.theIndex = 0
-
- for n in range(len(theLabels)):
- self.addEntry(theLabels[n], theColours[n])
-
- return True
-
- ##
- # Iterator Bits
- ##
-
- def __getitem__(self, n):
- """Return an entry by its index.
- """
- if n >= 0 and n < self.theLength:
- return self.theLabels[n], self.theColours[n], self.theCounts[n]
- return None, None, None
-
- def __iter__(self):
- """Initialise the iterator.
- """
- self.theIndex = 0
- return self
-
- def __next__(self):
- """Return the next entry for the iterator.
- """
- if self.theIndex < self.theLength:
- theLabel, theColour, theCount = self.__getitem__(self.theIndex)
- self.theIndex += 1
- return theLabel, theColour, theCount
- else:
- raise StopIteration
-
-# END Class NWStatus
-
# =============================================================================================== #
# OptionState
# Save the project-wise state of options that don't go into project XML or main config
diff --git a/nw/core/status.py b/nw/core/status.py
new file mode 100644
index 00000000..3dd26b35
--- /dev/null
+++ b/nw/core/status.py
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 -*-
+"""novelWriter Project Item Status Class
+
+ novelWriter – Project Item Status Class
+=========================================
+ Class holding the status elements of a project item
+
+ File History:
+ Created: 2019-05-19 [0.1.3]
+
+ 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 lxml import etree
+
+from nw.common import checkInt
+
+logger = logging.getLogger(__name__)
+
+class NWStatus():
+
+ def __init__(self):
+ self.theLabels = []
+ self.theColours = []
+ self.theCounts = []
+ self.theMap = {}
+ self.theLength = 0
+ self.theIndex = 0
+ return
+
+ def addEntry(self, theLabel, theColours):
+ """Add a status entry to the status object, but ensure it isn't
+ a duplicate.
+ """
+ theLabel = theLabel.strip()
+ if self.lookupEntry(theLabel) is None:
+ self.theLabels.append(theLabel)
+ self.theColours.append(theColours)
+ self.theCounts.append(0)
+ self.theMap[theLabel] = self.theLength
+ self.theLength += 1
+ return True
+
+ def lookupEntry(self, theLabel):
+ """Look up a status entry in the object lists, and return it if
+ it exists.
+ """
+ if theLabel is None:
+ return None
+ theLabel = theLabel.strip()
+ if theLabel in self.theMap.keys():
+ return self.theMap[theLabel]
+ return None
+
+ def checkEntry(self, theStatus):
+ """Check if a status value is valid, and returns the safe
+ reference to be used internally.
+ """
+ if isinstance(theStatus, str):
+ theStatus = theStatus.strip()
+ if self.lookupEntry(theStatus) is not None:
+ return theStatus
+ theStatus = checkInt(theStatus, 0, False)
+ if theStatus >= 0 and theStatus < self.theLength:
+ return self.theLabels[theStatus]
+
+ def setNewEntries(self, newList):
+ """Update the list of entries after they have been modified by
+ the GUI tool.
+ """
+ replaceMap = {}
+
+ if newList is not None:
+ self.theLabels = []
+ self.theColours = []
+ self.theCounts = []
+ self.theMap = {}
+ self.theLength = 0
+ self.theIndex = 0
+
+ for nName, nR, nG, nB, oName in newList:
+ self.addEntry(nName, (nR, nG, nB))
+ if nName != oName and oName is not None:
+ replaceMap[oName] = nName
+
+ return replaceMap
+
+ def resetCounts(self):
+ """Clear the counts of references to the status entries.
+ """
+ self.theCounts = [0]*self.theLength
+ return
+
+ def countEntry(self, theLabel):
+ """Lookup the usage count of a given entry.
+ """
+ theIndex = self.lookupEntry(theLabel)
+ if theIndex is not None:
+ self.theCounts[theIndex] += 1
+ return
+
+ def packEntries(self, xParent):
+ """Pack the status entries into an XML object for saving to the
+ main project file.
+ """
+ for n in range(self.theLength):
+ xSub = etree.SubElement(xParent,"entry",attrib={
+ "blue" : str(self.theColours[n][2]),
+ "green" : str(self.theColours[n][1]),
+ "red" : str(self.theColours[n][0]),
+ })
+ xSub.text = self.theLabels[n]
+ return True
+
+ def unpackEntries(self, xParent):
+ """Unpack an XML tree and set the class values.
+ """
+ theLabels = []
+ theColours = []
+
+ for xChild in xParent:
+ theLabels.append(xChild.text)
+ if "red" in xChild.attrib:
+ cR = checkInt(xChild.attrib["red"],0,False)
+ else:
+ cR = 0
+ if "green" in xChild.attrib:
+ cG = checkInt(xChild.attrib["green"],0,False)
+ else:
+ cG = 0
+ if "blue" in xChild.attrib:
+ cB = checkInt(xChild.attrib["blue"],0,False)
+ else:
+ cB = 0
+ theColours.append((cR,cG,cB))
+
+ if len(theLabels) > 0:
+ self.theLabels = []
+ self.theColours = []
+ self.theCounts = []
+ self.theMap = {}
+ self.theLength = 0
+ self.theIndex = 0
+
+ for n in range(len(theLabels)):
+ self.addEntry(theLabels[n], theColours[n])
+
+ return True
+
+ ##
+ # Iterator Bits
+ ##
+
+ def __getitem__(self, n):
+ """Return an entry by its index.
+ """
+ if n >= 0 and n < self.theLength:
+ return self.theLabels[n], self.theColours[n], self.theCounts[n]
+ return None, None, None
+
+ def __iter__(self):
+ """Initialise the iterator.
+ """
+ self.theIndex = 0
+ return self
+
+ def __next__(self):
+ """Return the next entry for the iterator.
+ """
+ if self.theIndex < self.theLength:
+ theLabel, theColour, theCount = self.__getitem__(self.theIndex)
+ self.theIndex += 1
+ return theLabel, theColour, theCount
+ else:
+ raise StopIteration
+
+# END Class NWStatus
diff --git a/nw/core/tree.py b/nw/core/tree.py
index be99f307..7f1482fe 100644
--- a/nw/core/tree.py
+++ b/nw/core/tree.py
@@ -6,7 +6,7 @@
Class holding the data of the project tree
File History:
- Created: 2020-05-07 [0.4.5] NWTree
+ Created: 2020-05-07 [0.4.5]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
@@ -34,8 +34,6 @@ from lxml import etree
from hashlib import sha256
from time import time
-from PyQt5.QtWidgets import QMessageBox
-
from nw.core.item import NWItem
from nw.common import checkString
from nw.constants import nwFiles, nwItemType, nwItemClass
From da0d4017cc7276aff9107ba60323791c500a3111 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sat, 6 Jun 2020 15:27:23 +0200
Subject: [PATCH 29/32] Moved OptionState to core/options.py
---
nw/core/options.py | 251 +++++++++++++++++++++++++++++++++++++++++++++
nw/core/project.py | 220 +--------------------------------------
2 files changed, 252 insertions(+), 219 deletions(-)
create mode 100644 nw/core/options.py
diff --git a/nw/core/options.py b/nw/core/options.py
new file mode 100644
index 00000000..12593c0c
--- /dev/null
+++ b/nw/core/options.py
@@ -0,0 +1,251 @@
+# -*- coding: utf-8 -*-
+"""novelWriter Project Options Cache
+
+ novelWriter – Project Options Cache
+=====================================
+ Class wrapping the project options state
+
+ File History:
+ Created: 2019-10-21 [0.3.1]
+ Rewritten: 2020-02-19 [0.4.5]
+
+ 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 json
+import nw
+
+from os import path
+
+from nw.constants import nwFiles
+
+logger = logging.getLogger(__name__)
+
+class OptionState():
+
+ def __init__(self, theProject):
+
+ self.mainConf = nw.CONFIG
+ self.theProject = theProject
+
+ self.theState = {}
+ self.validMap = {
+ "GuiSession": set([
+ "widthCol0",
+ "widthCol1",
+ "widthCol2",
+ "sortCol",
+ "sortOrder",
+ "hideZeros",
+ "hideNegative",
+ ]),
+ "GuiDocSplit": set([
+ "spLevel",
+ ]),
+ "GuiBuildNovel": set([
+ "winWidth",
+ "winHeight",
+ "addNovel",
+ "addNotes",
+ "ignoreFlag",
+ "justifyText",
+ "excludeBody",
+ "textFont",
+ "textSize",
+ "noStyling",
+ "incSynopsis",
+ "incComments",
+ "incKeywords",
+ "incBodyText",
+ ]),
+ "GuiOutline": set([
+ "headerOrder",
+ "columnWidth",
+ "columnHidden",
+ ])
+ }
+
+ return
+
+ ##
+ # Load and Save Cache
+ ##
+
+ def loadSettings(self):
+ """Load the options dictionary from the project settings file.
+ """
+ if self.theProject.projMeta is None:
+ return False
+
+ stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
+ theState = {}
+
+ if path.isfile(stateFile):
+ logger.debug("Loading GUI options file")
+ try:
+ with open(stateFile, mode="r", encoding="utf8") as inFile:
+ theJson = inFile.read()
+ theState = json.loads(theJson)
+ except Exception as e:
+ logger.error("Failed to load GUI options file")
+ logger.error(str(e))
+ return False
+
+ # Filter out unused variables
+ for aGroup in theState:
+ if aGroup in self.validMap:
+ self.theState[aGroup] = {}
+ for anOpt in theState[aGroup]:
+ if anOpt in self.validMap[aGroup]:
+ self.theState[aGroup][anOpt] = theState[aGroup][anOpt]
+
+ return True
+
+ def saveSettings(self):
+ """Save the options dictionary to the project settings file.
+ """
+ if self.theProject.projMeta is None:
+ return False
+
+ stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
+ logger.debug("Saving GUI options file")
+
+ try:
+ with open(stateFile, mode="w+", encoding="utf8") as outFile:
+ outFile.write(json.dumps(self.theState, indent=2))
+ except Exception as e:
+ logger.error("Failed to save GUI options file")
+ logger.error(str(e))
+ return False
+
+ return True
+
+ ##
+ # Setters
+ ##
+
+ def setValue(self, setGroup, setName, setValue):
+ """Saves a value, with a given group and name.
+ """
+ if not setGroup in self.validMap:
+ logger.error("Unknown option group '%s'" % setGroup)
+ return False
+
+ if not setName in self.validMap[setGroup]:
+ logger.error("Unknown option name '%s'" % setName)
+ return False
+
+ if not setGroup in self.theState:
+ self.theState[setGroup] = {}
+
+ self.theState[setGroup][setName] = setValue
+
+ return True
+
+ ##
+ # Getters
+ ##
+
+ def getValue(self, getGroup, getName, defaultValue):
+ """Return an arbitrary type value, if it exists. Otherwise,
+ return the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return self.theState[getGroup][getName]
+ except Exception as e:
+ logger.warning(str(e))
+ return defaultValue
+ return defaultValue
+
+ def getString(self, getGroup, getName, defaultValue):
+ """Return the value as a string, if it exists. Otherwise, return
+ the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return str(self.theState[getGroup][getName])
+ except Exception as e:
+ logger.warning(str(e))
+ return defaultValue
+ return defaultValue
+
+ def getInt(self, getGroup, getName, defaultValue):
+ """Return the value as an int, if it exists. Otherwise, return
+ the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return int(self.theState[getGroup][getName])
+ except Exception as e:
+ logger.warning(str(e))
+ return defaultValue
+ return defaultValue
+
+ def getFloat(self, getGroup, getName, defaultValue):
+ """Return the value as a float, if it exists. Otherwise, return
+ the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return float(self.theState[getGroup][getName])
+ except Exception as e:
+ logger.warning(str(e))
+ return defaultValue
+ return defaultValue
+
+ def getBool(self, getGroup, getName, defaultValue):
+ """Return the value as a bool, if it exists. Otherwise, return
+ the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return bool(self.theState[getGroup][getName])
+ except Exception as e:
+ logger.warning(str(e))
+ return defaultValue
+ return defaultValue
+
+ ##
+ # Validators
+ ##
+
+ def validIntRange(self, theValue, intA, intB, intDefault):
+ """Check that an int is in a given range. If it isn't, return
+ the default value.
+ """
+ if isinstance(theValue, int):
+ if theValue >= intA and theValue <= intB:
+ return theValue
+ return intDefault
+
+ def validIntTuple(self, theValue, theTuple, intDefault):
+ """Check that an int is an element of a tuple. If it isn't,
+ return the default value.
+ """
+ if isinstance(theValue, int):
+ if theValue in theTuple:
+ return theValue
+ return intDefault
+
+# END Class OptionState
diff --git a/nw/core/project.py b/nw/core/project.py
index 534a2554..2f700f4f 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -43,6 +43,7 @@ from nw.core.tree import NWTree
from nw.core.item import NWItem
from nw.core.document import NWDoc
from nw.core.status import NWStatus
+from nw.core.options import OptionState
from nw.common import checkString, checkBool, checkInt, formatTimeStamp
from nw.constants import (
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwAlert
@@ -1222,222 +1223,3 @@ class NWProject():
return
# END Class NWProject
-
-# =============================================================================================== #
-# OptionState
-# Save the project-wise state of options that don't go into project XML or main config
-# =============================================================================================== #
-
-class OptionState():
-
- def __init__(self, theProject):
-
- self.mainConf = nw.CONFIG
- self.theProject = theProject
-
- self.theState = {}
- self.validMap = {
- "GuiSession": set([
- "widthCol0",
- "widthCol1",
- "widthCol2",
- "sortCol",
- "sortOrder",
- "hideZeros",
- "hideNegative",
- ]),
- "GuiDocSplit": set([
- "spLevel",
- ]),
- "GuiBuildNovel": set([
- "winWidth",
- "winHeight",
- "addNovel",
- "addNotes",
- "ignoreFlag",
- "justifyText",
- "excludeBody",
- "textFont",
- "textSize",
- "noStyling",
- "incSynopsis",
- "incComments",
- "incKeywords",
- "incBodyText",
- ]),
- "GuiOutline": set([
- "headerOrder",
- "columnWidth",
- "columnHidden",
- ])
- }
-
- return
-
- ##
- # Load and Save Cache
- ##
-
- def loadSettings(self):
- """Load the options dictionary from the project settings file.
- """
- if self.theProject.projMeta is None:
- return False
-
- stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
- theState = {}
-
- if path.isfile(stateFile):
- logger.debug("Loading GUI options file")
- try:
- with open(stateFile, mode="r", encoding="utf8") as inFile:
- theJson = inFile.read()
- theState = json.loads(theJson)
- except Exception as e:
- logger.error("Failed to load GUI options file")
- logger.error(str(e))
- return False
-
- # Filter out unused variables
- for aGroup in theState:
- if aGroup in self.validMap:
- self.theState[aGroup] = {}
- for anOpt in theState[aGroup]:
- if anOpt in self.validMap[aGroup]:
- self.theState[aGroup][anOpt] = theState[aGroup][anOpt]
-
- return True
-
- def saveSettings(self):
- """Save the options dictionary to the project settings file.
- """
- if self.theProject.projMeta is None:
- return False
-
- stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
- logger.debug("Saving GUI options file")
-
- try:
- with open(stateFile, mode="w+", encoding="utf8") as outFile:
- outFile.write(json.dumps(self.theState, indent=2))
- except Exception as e:
- logger.error("Failed to save GUI options file")
- logger.error(str(e))
- return False
-
- return True
-
- ##
- # Setters
- ##
-
- def setValue(self, setGroup, setName, setValue):
- """Saves a value, with a given group and name.
- """
- if not setGroup in self.validMap:
- logger.error("Unknown option group '%s'" % setGroup)
- return False
-
- if not setName in self.validMap[setGroup]:
- logger.error("Unknown option name '%s'" % setName)
- return False
-
- if not setGroup in self.theState:
- self.theState[setGroup] = {}
-
- self.theState[setGroup][setName] = setValue
-
- return True
-
- ##
- # Getters
- ##
-
- def getValue(self, getGroup, getName, defaultValue):
- """Return an arbitrary type value, if it exists. Otherwise,
- return the default value.
- """
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- try:
- return self.theState[getGroup][getName]
- except Exception as e:
- logger.warning(str(e))
- return defaultValue
- return defaultValue
-
- def getString(self, getGroup, getName, defaultValue):
- """Return the value as a string, if it exists. Otherwise, return
- the default value.
- """
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- try:
- return str(self.theState[getGroup][getName])
- except Exception as e:
- logger.warning(str(e))
- return defaultValue
- return defaultValue
-
- def getInt(self, getGroup, getName, defaultValue):
- """Return the value as an int, if it exists. Otherwise, return
- the default value.
- """
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- try:
- return int(self.theState[getGroup][getName])
- except Exception as e:
- logger.warning(str(e))
- return defaultValue
- return defaultValue
-
- def getFloat(self, getGroup, getName, defaultValue):
- """Return the value as a float, if it exists. Otherwise, return
- the default value.
- """
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- try:
- return float(self.theState[getGroup][getName])
- except Exception as e:
- logger.warning(str(e))
- return defaultValue
- return defaultValue
-
- def getBool(self, getGroup, getName, defaultValue):
- """Return the value as a bool, if it exists. Otherwise, return
- the default value.
- """
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- try:
- return bool(self.theState[getGroup][getName])
- except Exception as e:
- logger.warning(str(e))
- return defaultValue
- return defaultValue
-
- ##
- # Validators
- ##
-
- def validIntRange(self, theValue, intA, intB, intDefault):
- """Check that an int is in a given range. If it isn't, return
- the default value.
- """
- if isinstance(theValue, int):
- if theValue >= intA and theValue <= intB:
- return theValue
- return intDefault
-
- def validIntTuple(self, theValue, theTuple, intDefault):
- """Check that an int is an element of a tuple. If it isn't,
- return the default value.
- """
- if isinstance(theValue, int):
- if theValue in theTuple:
- return theValue
- return intDefault
-
-# END Class OptionState
From 260b2aa7b9711e88b6b3f49f07592e8522665900 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sat, 6 Jun 2020 15:31:17 +0200
Subject: [PATCH 30/32] Some final cleanup
---
nw/core/project.py | 5 +----
nw/gui/__init__.py | 7 ++-----
2 files changed, 3 insertions(+), 9 deletions(-)
diff --git a/nw/core/project.py b/nw/core/project.py
index 2f700f4f..45480820 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -6,9 +6,7 @@
Class wrapping the data of a novelWriter project
File History:
- Created: 2018-09-29 [0.0.1] NWProject
- Created: 2019-10-21 [0.3.1] OptionState
- Rewritten: 2020-02-19 [0.4.5] OptionState
+ Created: 2018-09-29 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
@@ -33,7 +31,6 @@ import nw
from os import path, mkdir, listdir, unlink, rename, rmdir
from lxml import etree
-from hashlib import sha256
from time import time
from shutil import make_archive
diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py
index 981173f2..3f3d056a 100644
--- a/nw/gui/__init__.py
+++ b/nw/gui/__init__.py
@@ -2,9 +2,7 @@
from nw.gui.about import GuiAbout
from nw.gui.build import GuiBuildNovel
-from nw.gui.docbars import GuiDocTitleBar
-from nw.gui.docbars import GuiNoticeBar
-from nw.gui.docbars import GuiSearchBar
+from nw.gui.docbars import GuiDocTitleBar, GuiNoticeBar, GuiSearchBar
from nw.gui.docdetails import GuiDocViewDetails
from nw.gui.doceditor import GuiDocEditor
from nw.gui.docmerge import GuiDocMerge
@@ -21,8 +19,7 @@ from nw.gui.projsettings import GuiProjectSettings
from nw.gui.projtree import GuiProjectTree
from nw.gui.sessionlog import GuiSessionLogView
from nw.gui.statusbar import GuiMainStatus
-from nw.gui.theme import GuiIcons
-from nw.gui.theme import GuiTheme
+from nw.gui.theme import GuiIcons, GuiTheme
__all__ = [
"GuiAbout",
From 8773098bebd937d3234c7f06acf887ba73bfa81b Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sun, 7 Jun 2020 17:15:36 +0200
Subject: [PATCH 31/32] Theme class should report logical DPI, not physical
---
nw/gui/theme.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/nw/gui/theme.py b/nw/gui/theme.py
index b17b7055..2f5a896f 100644
--- a/nw/gui/theme.py
+++ b/nw/gui/theme.py
@@ -132,7 +132,7 @@ class GuiTheme:
self.loadDecoration = self.theIcons.loadDecoration
# Extract Other Info
- self.guiDPI = qApp.primaryScreen().physicalDotsPerInchX()
+ self.guiDPI = qApp.primaryScreen().logicalDotsPerInchX()
self.guiScale = qApp.primaryScreen().logicalDotsPerInchX()/96.0
self.mainConf.guiScale = self.guiScale
logger.verbose("GUI DPI: %.1f" % self.guiDPI)
From 02c8e032c92e4ffa86ed836f7de0854dc1a9d49a Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Sun, 7 Jun 2020 18:06:10 +0200
Subject: [PATCH 32/32] Updated readme
---
README.md | 77 ++++++++++++++++++++++++++++++++++++-------------------
1 file changed, 51 insertions(+), 26 deletions(-)
diff --git a/README.md b/README.md
index bf1efa1c..bce21ca7 100644
--- a/README.md
+++ b/README.md
@@ -6,27 +6,41 @@
novelWriter is a markdown-like text editor designed for writing novels and larger projects of many smaller plain text documents.
+It uses its own flavour of markdown that supports a meta data syntax for comments, synopsis and cross-referencing between files.
+The idea is to have a simple text editor which allows for easy organisation of text files and notes, built on a plain text file project repository for robustness.
+The plain text storage is suitable for version control software, and also well suited for file synchronisation tools.
+The core project structure is stored in a project XML file.
+Other meta data is primarily saved in JSON files.
-novelWriter uses its own flavour of markdown that supports a meta data syntax for comments, synopsis and cross-referencing between files.
-
-novelWriter is licensed under GPLv3, see the [GNU General Public License website](https://www.gnu.org/licenses/gpl-3.0.en.html) for more details, or consult the [LICENSE](LICENSE.md) file.
-The bundled Typicon-based icon themes are licensed under [CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/).
-
-Below is a short overview of the features of novelWriter.
The full documentation is available at [novelwriter.readthedocs.io](https://novelwriter.readthedocs.io/).
### Note
-The application is under initial development, and not all planned features are included.
-The core functionality is, however, in place and has been used for a while by the author and collaborators.
+The application is still under initial development, but all core features have now been added.
+The core functionality has been in place for a while, and novelWriter is being used for writing projects by the author and collaborators.
-New features are being added regularly, until the core toolset is complete.
-When all planned initial features are in place, a release 1.0 will be made.
-Until then, novelWriter is in a pre-release alpha state, and should be considered experimental.
-If you do use it for real projects, please run backups frequently to avoid data losses.
+No new major features will be added at this time, until the application is stable.
+Until then, novelWriter is in a beta state.
+Please report any issues you may encounter in the repository issue tracker.
+
+You should be able to use novelWriter for real projects, but as with all software, please make regular backups.
There is a built in backup feature that can pack the entire project into a zip file on close.
Please check the documentation for further details.
+
+## License
+
+This is Open Source software, and novelWriter is licensed under GPLv3.
+See the [GNU General Public License website](https://www.gnu.org/licenses/gpl-3.0.en.html) for more details, or consult the [LICENSE](LICENSE.md) file.
+
+Bundled assets have the following licenses:
+
+* The Typicon-based icon themes by Stephen Hutchings are licensed under [CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/). The icons have been altered in size and colour for use with novelWriter, and some additional icons added. The original icon set is available at [stephenhutchings/typicons.font](https://github.com/stephenhutchings/typicons.font).
+* The Cantarell font by Dave Crossland is licensed under [OPEN FONT LICENSE Version 1.1](http://scripts.sil.org/OFL). It is available at [Google Fonts](https://fonts.google.com/specimen/Cantarell).
+* The Tomorrow syntax themes use colour schemes taken from Chris Kempson's collection of code editor themes, licensed with the [MIT License](https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md), and the main repo is available at [chriskempson/tomorrow-theme](https://github.com/chriskempson/tomorrow-theme).
+* Likewise, the Owl syntax themes use colours from Sarah Drasner's code editor themes, licensed with the [MIT License](https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE), and the main repo is available at [sdras/night-owl-vscode-theme](https://github.com/sdras/night-owl-vscode-theme).
+
+
## Markdown Flavour
novelWriter is **not** a full-feature Markdown editor.
@@ -49,31 +63,42 @@ In addition, novelWriter adds the following, which is otherwise not supported by
* Non-breaking spaces are supported as long as your system is using at least Qt 5.9.
For earlier version, non-breaking spaces are converted to normal spaces when saving the document.
This is done by the Qt library.
+ From Qt 5.9 and on, it is possible to extract the raw text from a document, which preserves non-breaking spaces.
* Tabs may be rendered, depending on export format.
+ With Qt 5.10 or higher, the width of a tab in pixels can be changed in Preferences.
+
+The core export format of novelWriter is HTML5.
+You can also export the entire project as a single novelWriter flavour document.
+In addition, other exports to Open Document, PDF, and plain text is offered through the Qt library, although with limitations to formatting.
+
+Even though novelWriter can export to Open Document, the result is actually better when using the HTML output and then importing the HTML document into for instance Libre Office.
+The HTML output is also suitable for conversion with tools like Pandoc.
-The core export format that should render properly all supported features is the HTML export.
-This format also forms the basis of conversion to Office type document formats with Pandoc.
-Note that Pandoc itself strips some formatting from the document during conversion, so the final result may be different than expected.
## Implementation
The application is written in Python3 using Qt5 via PyQt5.
It is developed on Linux, but it should in principle work fine on other operating systems as well as long as dependencies are met.
+It is regularly tested on Windows 10.
-The application can be started from the source folder with the command:
+The application can be started from the source folder with one of the commands:
```
./novelWriter.py
+python novelWriter.py
+python3 novelWriter.py
```
It also takes a few parameters for debugging and such, which can be listed with the switch `--help`.
-There are no launcher icons yet.
-Consult your operating system documentation for how to make those.
-These will be added at some point, and I would appreciate any assistance from people working on Windows and MacOS as I don't use either of those operating systems.
+In the root assets folder there are icons and scripts and a template for setting up a launcher on Gnome desktops.
+You may need to modify those scripts slightly, but as they are, they work on Debian and Ubuntu.
+For other operating systems, please consult your operating system documentation for how to make those.
+Feel free to submit more if you are able to make them.
+
## Package Dependencies
-It is recommended that novelWriter runs with Qt 5.9 or later, and Python 3.6 or later.
+It is recommended that novelWriter runs with Qt 5.10 or later, and Python 3.6 or later.
Running with Qt as low as 5.2.1 and Python 3.4.3 has been tested, and worked in the past, but there are no guarantees that this will keep working as these are not a part of the test builds.
For the apt package manager on Debian systems, the following Python3 packages are needed:
@@ -88,15 +113,15 @@ These are optional, but recommended:
Alternatively, the packages can be installed with `pip` by running
```
-python3 -m pip install -r requirements.txt
+pip install -r requirements.txt
```
in the application folder.
You can also do them one at a time, skipping the ones you don't need:
```
-python3 -m pip install pyqt5
-python3 -m pip install lxml
-python3 -m pip install pyenchant
+pip install pyqt5
+pip install lxml
+pip install pyenchant
```
PyQt/Qt should be at least 5.2.1, but ideally 5.10 or higher for nearly all features to work.
@@ -136,7 +161,7 @@ Have a look in the existing folders for examples of how to define the colours.
### Auto-Saving and Document Stats
-Open documents and the project file itself is saved regularly on a timer – if they have been altered.
+Open documents and the project file itself is saved regularly on a timer.
The status of this is indicated by two indicators on the right hand side of the status bar.
Unsaved changes are in yellow, and saved is indicated by green in the default theme.
Latest word count for the document and project is shown next to these indicators in the status bar.
@@ -168,7 +193,7 @@ These are optional files.
### Visualisation of Story Elements
-The different notes can be assigned tags, which the novel files can refer back to using special meta keywords.
+The different notes can be assigned tags, which other files can refer back to using special meta keywords.
This information can be used to display an outline of the story, showing where each scene connects to the plot, and which characters, etc. occur in them.
In addition, the tags themselves are clickable in the document view pane, and control-clickable in the editor.
They make it possible to quickly navigate between the documents while editing.