Merge pull request #284 from vkbo/dev

Dev Branch to Master for 0.8
This commit is contained in:
Veronica K. Berglyd Olsen
2020-06-07 18:09:50 +02:00
committed by GitHub
45 changed files with 2300 additions and 1429 deletions
+14
View File
@@ -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**
+51 -26
View File
@@ -6,27 +6,41 @@
<img align="left" style="margin: 0 16px 4px 0;" src="assets/icons/96x96/novelwriter.png">
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.
+2 -2
View File
@@ -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 ---------------------------------------------------
+5 -5
View File
@@ -40,9 +40,9 @@ __package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 20182020, 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":
+77 -14
View File
@@ -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
##
+277
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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
+251
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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
+31 -967
View File
File diff suppressed because it is too large Load Diff
+194
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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
+419
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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
+6 -7
View File
@@ -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",
+10 -13
View File
@@ -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("<b>%s</b>" % 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)
+55 -34
View File
@@ -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()
+22 -14
View File
@@ -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
+26 -22
View File
@@ -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("")
+12 -7
View File
@@ -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)
+15 -15
View File
@@ -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)
+4 -4
View File
@@ -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)
+4 -4
View File
@@ -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)
+23 -19
View File
@@ -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,
+10 -10
View File
@@ -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)
+3 -3
View File
@@ -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)
+6 -6
View File
@@ -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))
+29 -16
View File
@@ -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):
+301
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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("<b>Title</b>")
self.fileLabel = QLabel("<b>Document</b>")
self.itemLabel = QLabel("<b>Status</b>")
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("<b>Characters</b>")
self.wCLabel = QLabel("<b>Words</b>")
self.pCLabel = QLabel("<b>Paragraphs</b>")
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("<b>Synopsis</b>")
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("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
self.chrKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY])
self.pltKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])
self.timKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.TIME_KEY])
self.wldKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY])
self.objKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY])
self.entKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY])
self.cstKeyLabel = QLabel("<b>%s</b>" % 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("<b>%s</b>" % self.LVL_MAP[novIdx["level"]])
else:
self.titleLabel.setText("<b>Title</b>")
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("<a href='#%s=%s'>%s</a>" % (
theKey[1:], tTag, tTag
))
return ", ".join(refTags)
# END Class GuiOutlineDetails
+13 -9
View File
@@ -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(
+17 -14
View File
@@ -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
+32 -21
View File
@@ -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("<b>Project Details</b>")
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("<b>Project Stats</b>")
@@ -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))
+269 -87
View File
@@ -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
+19 -25
View File
@@ -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()
+6 -4
View File
@@ -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)
+10 -2
View File
@@ -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
+35 -28
View File
@@ -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
+5 -5
View File
@@ -1,13 +1,16 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.7.0" hexVersion="0x000700f0" fileVersion="1.1" saveCount="228" autoCount="31" timeStamp="2020-06-01 12:54:18" editTime="540">
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 20:53:20">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
<author>Jay Doh</author>
<backup>False</backup>
<saveCount>273</saveCount>
<autoCount>40</autoCount>
<editTime>4125</editTime>
</project>
<settings>
<doBackup>False</doBackup>
<spellCheck>True</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited>
@@ -24,9 +27,6 @@
<unnumbered>%title%</unnumbered>
<scene>Scene %ch%.%sc%: %title%</scene>
<section></section>
<withSynopsis>True</withSynopsis>
<withComments>True</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
+1 -1
View File
@@ -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",
+5 -5
View File
@@ -1,11 +1,14 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="2" autoCount="0" timeStamp="2020-05-29 23:05:28">
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 21:05:07">
<project>
<name>New Project</name>
<title></title>
<backup>True</backup>
<saveCount>2</saveCount>
<autoCount>1</autoCount>
<editTime>0</editTime>
</project>
<settings>
<doBackup>True</doBackup>
<spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
@@ -18,9 +21,6 @@
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
<withSynopsis>False</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
+5 -5
View File
@@ -1,11 +1,14 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="4" autoCount="1" timeStamp="2020-05-30 15:31:30" editTime="12">
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 21:06:00">
<project>
<name>New Project</name>
<title></title>
<backup>True</backup>
<saveCount>5</saveCount>
<autoCount>1</autoCount>
<editTime>11</editTime>
</project>
<settings>
<doBackup>True</doBackup>
<spellCheck>True</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>31489056e0916</lastEdited>
@@ -18,9 +21,6 @@
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
<withSynopsis>False</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
+5 -5
View File
@@ -1,13 +1,16 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="2" autoCount="0" timeStamp="2020-05-29 23:07:05">
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 21:07:03">
<project>
<name>Project Name</name>
<title>Project Title</title>
<author>Jane Doe</author>
<author>John Doh</author>
<backup>True</backup>
<saveCount>2</saveCount>
<autoCount>1</autoCount>
<editTime>1</editTime>
</project>
<settings>
<doBackup>True</doBackup>
<spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
@@ -22,9 +25,6 @@
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
<withSynopsis>False</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
+5 -5
View File
@@ -1,11 +1,14 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="2" autoCount="0" timeStamp="2020-05-29 23:07:37">
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 21:07:51">
<project>
<name>New Project</name>
<title></title>
<backup>True</backup>
<saveCount>2</saveCount>
<autoCount>1</autoCount>
<editTime>0</editTime>
</project>
<settings>
<doBackup>True</doBackup>
<spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
@@ -18,9 +21,6 @@
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
<withSynopsis>False</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
+2 -1
View File
@@ -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]
+5 -5
View File
@@ -1,11 +1,14 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="1" autoCount="0" timeStamp="2020-05-29 23:03:58">
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 20:58:52">
<project>
<name>New Project</name>
<title></title>
<backup>True</backup>
<saveCount>1</saveCount>
<autoCount>0</autoCount>
<editTime>0</editTime>
</project>
<settings>
<doBackup>True</doBackup>
<spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
@@ -18,9 +21,6 @@
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
<withSynopsis>False</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
+5 -5
View File
@@ -1,11 +1,14 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="4" autoCount="0" timeStamp="2020-05-29 23:04:31">
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 21:03:18">
<project>
<name>New Project</name>
<title></title>
<backup>True</backup>
<saveCount>4</saveCount>
<autoCount>0</autoCount>
<editTime>0</editTime>
</project>
<settings>
<doBackup>True</doBackup>
<spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
@@ -18,9 +21,6 @@
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
<withSynopsis>False</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
+5 -5
View File
@@ -1,11 +1,14 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="5" autoCount="0" timeStamp="2020-05-29 23:04:50">
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 21:04:10">
<project>
<name>New Project</name>
<title></title>
<backup>True</backup>
<saveCount>5</saveCount>
<autoCount>0</autoCount>
<editTime>0</editTime>
</project>
<settings>
<doBackup>True</doBackup>
<spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
@@ -18,9 +21,6 @@
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
<withSynopsis>False</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
+4 -4
View File
@@ -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()
+5 -5
View File
@@ -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