diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12494617..4ea7f124 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# 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.
+* 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.1 [2020-06-06]
**Bugfixes**
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.
diff --git a/docs/source/conf.py b/docs/source/conf.py
index a2f53984..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.1"
+version = "0.8.0"
# The full version, including alpha/beta/rc tags
-release = "0.7.1"
+release = "0.8.0rc1"
# -- General configuration ---------------------------------------------------
diff --git a/nw/__init__.py b/nw/__init__.py
index 9549226a..2b8b15a1 100644
--- a/nw/__init__.py
+++ b/nw/__init__.py
@@ -40,9 +40,9 @@ __package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen"
__license__ = "GPLv3"
-__version__ = "0.7.1"
-__hexversion__ = "0x000701f0"
-__date__ = "2020-06-06"
+__version__ = "0.8.0rc1"
+__hexversion__ = "0x000800c1"
+__date__ = "2020-06-01"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__status__ = "Pre-Release"
@@ -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/config.py b/nw/config.py
index 985eca43..4995cafa 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 # Set automatically by Theme class
## Sizes
self.winGeometry = [1100, 650]
@@ -95,6 +96,7 @@ class Config:
self.projColWidth = [140, 55, 140]
self.mainPanePos = [300, 800]
self.docPanePos = [400, 400]
+ self.outlnPanePos = [500, 150]
self.isFullScreen = False
## Project
@@ -124,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
@@ -196,7 +198,23 @@ class Config:
return
##
- # Actions
+ # Methods
+ ##
+
+ 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(theSize*self.guiScale)
+
+ 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 int(theSize/self.guiScale)
+
+ ##
+ # Config Actions
##
def initConfig(self, confPath=None, dataPath=None):
@@ -342,6 +360,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 +502,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"
@@ -642,7 +664,7 @@ class Config:
return True
##
- # Setters and Getters
+ # Setters
##
def setConfPath(self, newPath):
@@ -672,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
@@ -681,25 +705,30 @@ 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 = [int(x/self.guiScale) for x in panePos]
+ self.confChanged = True
+ return True
+
def setShowRefPanel(self, checkState):
self.showRefPanel = checkState
self.confChanged = True
@@ -716,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/item.py b/nw/core/item.py
new file mode 100644
index 00000000..4dd9ddb9
--- /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 of a project tree item
+
+ File History:
+ Created: 2018-10-27 [0.0.1]
+
+ This file is a part of novelWriter
+ Copyright 2020, Veronica Berglyd Olsen
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+"""
+
+import logging
+import nw
+
+from 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/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 43efa057..45480820 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -3,17 +3,10 @@
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: 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
+ Created: 2018-09-29 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
@@ -38,13 +31,16 @@ 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
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.core.options import OptionState
from nw.common import checkString, checkBool, checkInt, formatTimeStamp
from nw.constants import (
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwAlert
@@ -213,14 +209,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
@@ -334,6 +327,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:
@@ -412,6 +407,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)
@@ -420,7 +423,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)
@@ -492,21 +497,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)
@@ -835,10 +842,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):
@@ -1215,944 +1220,3 @@ class NWProject():
return
# 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
-
-# =============================================================================================== #
-# 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
-# =============================================================================================== #
-
-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
-# =============================================================================================== #
-
-class OptionState():
-
- def __init__(self, theProject):
-
- self.theProject = theProject
- self.theState = {}
- self.stringOpt = ()
- self.boolOpt = ()
- self.intOpt = ()
-
- return
-
- 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
- for anOpt in theState:
- self.theState[anOpt] = theState[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
-
- def setValue(self, setGroup, setName, setValue):
- """Saves a value, with a given group and name.
- """
- if not setGroup in self.theState:
- self.theState[setGroup] = {}
- self.theState[setGroup][setName] = setValue
- return True
-
- 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:
- 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:
- 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:
- 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:
- 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:
- return defaultValue
- return defaultValue
-
- 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/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
new file mode 100644
index 00000000..7f1482fe
--- /dev/null
+++ b/nw/core/tree.py
@@ -0,0 +1,419 @@
+# -*- 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]
+
+ 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 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
diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py
index 4df869b5..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
@@ -13,15 +11,15 @@ 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
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",
@@ -37,7 +35,8 @@ __all__ = [
"GuiItemDetails",
"GuiItemEditor",
"GuiMainMenu",
- "GuiProjectOutline",
+ "GuiOutline",
+ "GuiOutlineDetails",
"GuiPreferences",
"GuiProjectLoad",
"GuiProjectSettings",
diff --git a/nw/gui/about.py b/nw/gui/about.py
index 653f87ae..d432a4d9 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 = QVBoxLayout()
+ 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,20 +76,15 @@ class GuiAbout(QDialog):
# Pages
self.pageAbout = QTextBrowser()
self.pageAbout.setOpenExternalLinks(True)
- self.pageAbout.document().setDocumentMargin(16)
-
- # self.pageCredit = QTextBrowser()
- # self.pageCredit.setOpenExternalLinks(True)
- # self.pageCredit.document().setDocumentMargin(16)
+ self.pageAbout.document().setDocumentMargin(self.mainConf.pxInt(16))
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()
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 df812c95..87bb8e65 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -78,12 +78,12 @@ class GuiBuildNovel(QDialog):
self.nwdText = [] # List of markdown documents
self.setWindowTitle("Build Novel Project")
- self.setMinimumWidth(900)
- self.setMinimumHeight(800)
+ self.setMinimumWidth(self.mainConf.pxInt(900))
+ self.setMinimumHeight(self.mainConf.pxInt(800))
self.resize(
- self.optState.getInt("GuiBuildNovel", "winWidth", 900),
- self.optState.getInt("GuiBuildNovel", "winHeight", 800)
+ self.mainConf.pxInt(self.optState.getInt("GuiBuildNovel", "winWidth", 900)),
+ self.mainConf.pxInt(self.optState.getInt("GuiBuildNovel", "winHeight", 800))
)
self.outerBox = QHBoxLayout()
@@ -112,10 +112,11 @@ 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)
- self.fmtTitle.setFixedWidth(220)
+ self.fmtTitle.setFixedWidth(xFmt)
self.fmtTitle.setToolTip(fmtHelp)
self.fmtTitle.setText(
self._reFmtCodes(self.theProject.titleFormat["title"])
@@ -123,7 +124,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 +132,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 +140,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 +148,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 +177,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 +186,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)
@@ -236,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)
@@ -847,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
@@ -894,7 +915,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/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
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 d4b2621c..7c4ec8f7 100644
--- a/nw/gui/docdetails.py
+++ b/nw/gui/docdetails.py
@@ -37,19 +37,24 @@ 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.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 bf511e07..7b6ef81b 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)
@@ -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
@@ -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
@@ -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/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 1be66bba..a3d6eeda 100644
--- a/nw/gui/docviewer.py
+++ b/nw/gui/docviewer.py
@@ -40,20 +40,20 @@ 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()
- self.setMinimumWidth(300)
+ self.setMinimumWidth(self.mainConf.pxInt(300))
self.setOpenExternalLinks(False)
self.initViewer()
@@ -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
@@ -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)
@@ -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/itemdetails.py b/nw/gui/itemdetails.py
index 93ffb788..182fb3fd 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
@@ -170,11 +170,11 @@ class GuiItemDetails(QFrame):
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/mainmenu.py b/nw/gui/mainmenu.py
index 5ff4d918..7cd20565 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
@@ -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/outline.py b/nw/gui/outline.py
index ada3c0a8..2d2716f8 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
@@ -100,6 +100,7 @@ class GuiProjectOutline(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))
@@ -120,7 +121,7 @@ class GuiProjectOutline(QTreeWidget):
self.clearOutline()
self.headerMenu.setHiddenState(self.colHidden)
- logger.debug("ProjectOutline initialisation complete")
+ logger.debug("GuiOutline initialisation complete")
return
@@ -198,6 +199,17 @@ class GuiProjectOutline(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)
+ self.theParent.projMeta.showItem(tHandle, sTitle)
+ return
+
def _headerRightClick(self, clickPos):
"""Show the header column menu.
"""
@@ -233,7 +245,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 +268,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]
+ self.colWidth[nwOutline[hName]] = self.mainConf.pxInt(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]
@@ -289,7 +301,7 @@ class GuiProjectOutline(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()):
@@ -297,16 +309,16 @@ class GuiProjectOutline(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
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
@@ -413,6 +425,7 @@ class GuiProjectOutline(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"]))
@@ -433,7 +446,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..739b1c37
--- /dev/null
+++ b/nw/gui/outlinedetails.py
@@ -0,0 +1,301 @@
+# -*- 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 (
+ QScrollArea, QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel, QSizePolicy
+)
+
+from nw.constants import nwLabels, nwKeyWords
+from nw.common import checkInt
+
+logger = logging.getLogger(__name__)
+
+class GuiOutlineDetails(QScrollArea):
+
+ LVL_MAP = {
+ "H1" : "Title",
+ "H2" : "Chapter",
+ "H3" : "Scene",
+ "H4" : "Section"
+ }
+
+ def __init__(self, theParent):
+ QScrollArea.__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
+
+ # Sizes
+ minTitle = 30*self.theTheme.textNWidth
+ maxTitle = 40*self.theTheme.textNWidth
+ wCount = self.theTheme.getTextWidth("999,999")
+ hSpace = int(self.mainConf.pxInt(10))
+ vSpace = int(self.mainConf.pxInt(4))
+
+ # Details Area
+ self.titleLabel = QLabel("Title")
+ self.fileLabel = QLabel("Document")
+ self.itemLabel = QLabel("Status")
+ self.titleValue = QLabel("")
+ self.fileValue = QLabel("")
+ self.itemValue = QLabel("")
+
+ self.titleValue.setMinimumWidth(minTitle)
+ self.titleValue.setMaximumWidth(maxTitle)
+ self.fileValue.setMinimumWidth(minTitle)
+ self.fileValue.setMaximumWidth(maxTitle)
+ self.itemValue.setMinimumWidth(minTitle)
+ self.itemValue.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.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.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("")
+ self.timKeyValue = QLabel("")
+ self.wldKeyValue = QLabel("")
+ 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)
+ 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)
+
+ 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()
+ 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.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.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)
+ 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("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.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.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.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.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.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.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.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.addLayout(self.cstKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+
+ self.tagsForm.setColumnStretch(1, 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.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")
+
+ 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
+
+ if novIdx["level"] in self.LVL_MAP:
+ self.titleLabel.setText("%s" % self.LVL_MAP[novIdx["level"]])
+ else:
+ self.titleLabel.setText("Title")
+ self.titleValue.setText(novIdx["title"])
+
+ 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"])
+
+ 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/gui/preferences.py b/nw/gui/preferences.py
index a05e5eb0..17487a31 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("...")))
@@ -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):
@@ -389,7 +391,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 +577,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 +791,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 +807,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 +819,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 +830,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 282a69f7..06a32f72 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
+ sPx = self.mainConf.pxInt(16)
+ iPx = self.mainConf.pxInt(96)
+
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
- self.outerBox.setSpacing(16)
- self.innerBox.setSpacing(16)
+ self.outerBox.setSpacing(sPx)
+ self.innerBox.setSpacing(sPx)
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", (iPx, iPx))
self.innerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.projectForm = QGridLayout()
@@ -81,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))
@@ -110,8 +112,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)
@@ -148,7 +150,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:
@@ -161,7 +163,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):
@@ -251,9 +253,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)
@@ -262,8 +264,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/projsettings.py b/nw/gui/projsettings.py
index 3722d41c..f7766ad5 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/projtree.py b/nw/gui/projtree.py
index 312d3a75..7a3c98fb 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
@@ -49,19 +50,20 @@ 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
self.orphRoot = None
+ self.ctxMenu = GuiProjectTreeMenu(self)
self.clearTree()
# Build GUI
@@ -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)
@@ -100,8 +104,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
@@ -133,6 +138,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
@@ -181,6 +187,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
@@ -206,18 +213,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)
@@ -327,7 +334,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
@@ -358,7 +365,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()
@@ -387,17 +393,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)
@@ -548,6 +565,89 @@ 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)
+ 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
+
+ ##
+ # 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
##
@@ -572,7 +672,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.
"""
@@ -604,7 +704,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)
@@ -705,69 +814,142 @@ 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 = None
+
+ 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)
+
+ self.toggleExp = QAction("Toggle Included Flag", self)
+ self.toggleExp.triggered.connect(self._doToggleExported)
+ self.addAction(self.toggleExp)
+
+ 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 filterActions(self, theItem):
+ """Update item settings from the nwItem.
+ """
+ self.theItem = theItem
+ 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
+
+ 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
+ showNewFolder = not isTrash and not inTrash and not isOrph
+ 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)
+ self.newFolder.setVisible(showNewFolder)
+ self.deleteItem.setVisible(showDelete)
+ self.emptyTrash.setVisible(showEmpty)
+
+ return True
+
+ ##
+ # 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.
+ """
+ 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
diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py
index fdda4166..5f3c61f0 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
- )
+ 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(3,0)
+ self.listBox.setColumnWidth(0, wCol0)
+ self.listBox.setColumnWidth(1, wCol1)
+ self.listBox.setColumnWidth(2, wCol2)
+ 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(
@@ -226,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/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/gui/theme.py b/nw/gui/theme.py
index 49ea367c..2f5a896f 100644
--- a/nw/gui/theme.py
+++ b/nw/gui/theme.py
@@ -132,20 +132,28 @@ class GuiTheme:
self.loadDecoration = self.theIcons.loadDecoration
# Extract Other Info
- self.guiDPI = qApp.primaryScreen().physicalDotsPerInch()
- self.guiFont = qApp.font()
+ self.guiDPI = qApp.primaryScreen().logicalDotsPerInchX()
+ 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()))
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)
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
diff --git a/nw/guimain.py b/nw/guimain.py
index e50b23c9..80478624 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -35,15 +35,15 @@ 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
)
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
@@ -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))
@@ -91,40 +91,41 @@ 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 = []
self.importIcons = []
# Assemble Main Window
- self.treePane = QFrame()
+ 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 = QFrame()
+ 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)
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)
+ 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)
@@ -137,6 +138,8 @@ class GuiMain(QMainWindow):
self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.projView)
+ self.splitOutline.addWidget(self.projMeta)
+ self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
self.tabWidget = QTabWidget()
self.tabWidget.setTabPosition(QTabWidget.East)
@@ -145,12 +148,13 @@ 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)
- self.splitMain.setSizes(self.mainConf.mainPanePos)
+ self.splitMain.setSizes(self.mainConf.getMainPanePos())
self.setCentralWidget(self.splitMain)
@@ -512,9 +516,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)
@@ -849,6 +853,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()
@@ -875,7 +880,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()
@@ -986,16 +991,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
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index 092d28d1..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
diff --git a/setup.py b/setup.py
index 6c54a5f7..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.1",
+ version = "0.8rc1",
author = "Veronica Berglyd Olsen",
author_email = "code@vkbo.net",
description = "A markdown-like document editor for writing novels",
diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx
index dcf46837..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
@@ -18,9 +21,6 @@
%title%
* * *
- False
- False
- False
New
diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx
index ab090c92..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
@@ -18,9 +21,6 @@
%title%
* * *
- False
- False
- False
New
diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx
index 11fc5dbe..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
@@ -22,9 +25,6 @@
%title%
* * *
- False
- False
- False
New
diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx
index e7f9e514..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
@@ -18,9 +21,6 @@
%title%
* * *
- False
- False
- False
New
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]
diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx
index 5ba28cae..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
@@ -18,9 +21,6 @@
%title%
* * *
- False
- False
- False
New
diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx
index fe5bf878..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
@@ -18,9 +21,6 @@
%title%
* * *
- False
- False
- False
New
diff --git a/tests/reference/proj/3_nwProject.nwx b/tests/reference/proj/3_nwProject.nwx
index 2e3cb6c0..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
@@ -18,9 +21,6 @@
%title%
* * *
- False
- False
- False
New
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