diff --git a/nw/assets/fonts/Cantarell/Cantarell-Bold.ttf b/nw/assets/fonts/Cantarell/Cantarell-Bold.ttf
new file mode 100644
index 00000000..ca364663
Binary files /dev/null and b/nw/assets/fonts/Cantarell/Cantarell-Bold.ttf differ
diff --git a/nw/assets/fonts/Cantarell/Cantarell-BoldItalic.ttf b/nw/assets/fonts/Cantarell/Cantarell-BoldItalic.ttf
new file mode 100644
index 00000000..a54eab4c
Binary files /dev/null and b/nw/assets/fonts/Cantarell/Cantarell-BoldItalic.ttf differ
diff --git a/nw/assets/fonts/Cantarell/Cantarell-Italic.ttf b/nw/assets/fonts/Cantarell/Cantarell-Italic.ttf
new file mode 100644
index 00000000..725b1986
Binary files /dev/null and b/nw/assets/fonts/Cantarell/Cantarell-Italic.ttf differ
diff --git a/nw/assets/fonts/Cantarell/Cantarell-Regular.ttf b/nw/assets/fonts/Cantarell/Cantarell-Regular.ttf
new file mode 100644
index 00000000..ceb92751
Binary files /dev/null and b/nw/assets/fonts/Cantarell/Cantarell-Regular.ttf differ
diff --git a/nw/assets/fonts/Cantarell/OFL.txt b/nw/assets/fonts/Cantarell/OFL.txt
new file mode 100644
index 00000000..4e495019
--- /dev/null
+++ b/nw/assets/fonts/Cantarell/OFL.txt
@@ -0,0 +1,93 @@
+Copyright (c) 2009-2010, Understanding Limited (dave@understandinglimited.com)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/nw/assets/icons/fallback/check-dark.svg b/nw/assets/icons/fallback/check-dark.svg
new file mode 100644
index 00000000..86e45c5d
--- /dev/null
+++ b/nw/assets/icons/fallback/check-dark.svg
@@ -0,0 +1,56 @@
+
+
diff --git a/nw/assets/icons/fallback/check.svg b/nw/assets/icons/fallback/check.svg
new file mode 100644
index 00000000..f815ea12
--- /dev/null
+++ b/nw/assets/icons/fallback/check.svg
@@ -0,0 +1,56 @@
+
+
diff --git a/nw/assets/icons/fallback/cross-dark.svg b/nw/assets/icons/fallback/cross-dark.svg
new file mode 100644
index 00000000..64410dfb
--- /dev/null
+++ b/nw/assets/icons/fallback/cross-dark.svg
@@ -0,0 +1,56 @@
+
+
diff --git a/nw/assets/icons/fallback/cross.svg b/nw/assets/icons/fallback/cross.svg
new file mode 100644
index 00000000..ff91c26a
--- /dev/null
+++ b/nw/assets/icons/fallback/cross.svg
@@ -0,0 +1,55 @@
+
+
diff --git a/nw/assets/icons/typicons_colour_dark/icons.conf b/nw/assets/icons/typicons_colour_dark/icons.conf
index db622835..f3c96935 100644
--- a/nw/assets/icons/typicons_colour_dark/icons.conf
+++ b/nw/assets/icons/typicons_colour_dark/icons.conf
@@ -37,3 +37,5 @@ search-replace = search-replace.svg
clear = backspace.svg
save = download.svg
edit = pencil.svg
+check = tick.svg
+cross = times.svg
diff --git a/nw/assets/icons/typicons_colour_dark/tick.svg b/nw/assets/icons/typicons_colour_dark/tick.svg
new file mode 100644
index 00000000..ced8362e
--- /dev/null
+++ b/nw/assets/icons/typicons_colour_dark/tick.svg
@@ -0,0 +1,56 @@
+
+
diff --git a/nw/assets/icons/typicons_colour_light/icons.conf b/nw/assets/icons/typicons_colour_light/icons.conf
index 9ee1d84a..d3df7d01 100644
--- a/nw/assets/icons/typicons_colour_light/icons.conf
+++ b/nw/assets/icons/typicons_colour_light/icons.conf
@@ -37,3 +37,5 @@ search-replace = search-replace.svg
clear = backspace.svg
save = download.svg
edit = pencil.svg
+check = tick.svg
+cross = times.svg
diff --git a/nw/assets/icons/typicons_colour_light/tick.svg b/nw/assets/icons/typicons_colour_light/tick.svg
new file mode 100644
index 00000000..82ced41d
--- /dev/null
+++ b/nw/assets/icons/typicons_colour_light/tick.svg
@@ -0,0 +1,56 @@
+
+
diff --git a/nw/assets/icons/typicons_grey_dark/icons.conf b/nw/assets/icons/typicons_grey_dark/icons.conf
index 3753f2d2..75268e23 100644
--- a/nw/assets/icons/typicons_grey_dark/icons.conf
+++ b/nw/assets/icons/typicons_grey_dark/icons.conf
@@ -36,3 +36,5 @@ search-replace = search-replace.svg
clear = backspace.svg
save = download.svg
edit = pencil.svg
+check = tick.svg
+cross = times.svg
diff --git a/nw/assets/icons/typicons_grey_dark/tick.svg b/nw/assets/icons/typicons_grey_dark/tick.svg
new file mode 100644
index 00000000..86e45c5d
--- /dev/null
+++ b/nw/assets/icons/typicons_grey_dark/tick.svg
@@ -0,0 +1,56 @@
+
+
diff --git a/nw/assets/icons/typicons_grey_light/icons.conf b/nw/assets/icons/typicons_grey_light/icons.conf
index 1479df9e..766aed98 100644
--- a/nw/assets/icons/typicons_grey_light/icons.conf
+++ b/nw/assets/icons/typicons_grey_light/icons.conf
@@ -36,3 +36,5 @@ search-replace = search-replace.svg
clear = backspace.svg
save = download.svg
edit = pencil.svg
+check = tick.svg
+cross = times.svg
diff --git a/nw/assets/icons/typicons_grey_light/tick.svg b/nw/assets/icons/typicons_grey_light/tick.svg
new file mode 100644
index 00000000..f815ea12
--- /dev/null
+++ b/nw/assets/icons/typicons_grey_light/tick.svg
@@ -0,0 +1,56 @@
+
+
diff --git a/nw/config.py b/nw/config.py
index 18529f61..985eca43 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -36,7 +36,6 @@ from time import time
from PyQt5.Qt import PYQT_VERSION_STR
from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
-from PyQt5.QtWidgets import QErrorMessage
from nw.constants import nwFiles, nwUnicode
from nw.common import splitVersionNumber, formatTimeStamp
@@ -82,11 +81,13 @@ class Config:
self.confChanged = False
## General
- self.guiTheme = "default"
- self.guiSyntax = "default_light"
- self.guiIcons = "typicons_grey_light"
- self.guiDark = False
- self.guiLang = "en" # Hardcoded for now
+ self.guiTheme = "default"
+ self.guiSyntax = "default_light"
+ self.guiIcons = "typicons_grey_light"
+ self.guiDark = False
+ self.guiLang = "en" # Hardcoded for now
+ self.guiFont = ""
+ self.guiFontSize = 11
## Sizes
self.winGeometry = [1100, 650]
@@ -202,7 +203,6 @@ class Config:
"""Initialise the config class. The manual setting of confPath
and dataPath is mainly intended for the test suite.
"""
-
if confPath is None:
confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)
self.confPath = path.join(path.abspath(confRoot), self.appHandle)
@@ -318,6 +318,12 @@ class Config:
self.guiDark = self._parseLine(
cnfParse, cnfSec, "guidark", self.CNF_BOOL, self.guiDark
)
+ self.guiFont = self._parseLine(
+ cnfParse, cnfSec, "guifont", self.CNF_STR, self.guiFont
+ )
+ self.guiFontSize = self._parseLine(
+ cnfParse, cnfSec, "guifontsize", self.CNF_INT, self.guiFontSize
+ )
## Sizes
cnfSec = "Sizes"
@@ -464,11 +470,13 @@ class Config:
## Main
cnfSec = "Main"
cnfParse.add_section(cnfSec)
- cnfParse.set(cnfSec,"timestamp", formatTimeStamp(time()))
- cnfParse.set(cnfSec,"theme", str(self.guiTheme))
- cnfParse.set(cnfSec,"syntax", str(self.guiSyntax))
- cnfParse.set(cnfSec,"icons", str(self.guiIcons))
- cnfParse.set(cnfSec,"guidark", str(self.guiDark))
+ cnfParse.set(cnfSec,"timestamp", formatTimeStamp(time()))
+ cnfParse.set(cnfSec,"theme", str(self.guiTheme))
+ cnfParse.set(cnfSec,"syntax", str(self.guiSyntax))
+ cnfParse.set(cnfSec,"icons", str(self.guiIcons))
+ cnfParse.set(cnfSec,"guidark", str(self.guiDark))
+ cnfParse.set(cnfSec,"guifont", str(self.guiFont))
+ cnfParse.set(cnfSec,"guifontsize", str(self.guiFontSize))
## Sizes
cnfSec = "Sizes"
diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index cf0ab4eb..92378a73 100644
--- a/nw/constants/constants.py
+++ b/nw/constants/constants.py
@@ -45,6 +45,7 @@ class nwFiles():
INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json"
RECENT_FILE = "recentProjects.json"
+ BUILD_CACHE = "prevBuild.json"
# END Class nwFiles
diff --git a/nw/core/document.py b/nw/core/document.py
index 6ab4d500..8e2ae571 100644
--- a/nw/core/document.py
+++ b/nw/core/document.py
@@ -71,6 +71,9 @@ class NWDoc():
"""Open a document from handle, capturing potential file system
errors and parse meta data.
"""
+ if not isHandle(tHandle):
+ return None
+
# Always clear first, since the object will often be reused.
self.clearDocument()
@@ -170,7 +173,10 @@ class NWDoc():
"""Permanently delete a document source file and its backups
from the project data folder.
"""
- docFile = self.docHandle+".nwd"
+ if not isHandle(tHandle):
+ return False
+
+ docFile = tHandle+".nwd"
chkList = []
chkList.append(path.join(self.theProject.projContent, docFile))
diff --git a/nw/core/project.py b/nw/core/project.py
index 20ca89e9..be62ffae 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -152,15 +152,19 @@ class NWProject():
self.projTree.append(None, pHandle, newItem)
return newItem.itemHandle
- def addTrash(self):
+ def trashFolder(self):
"""Add the special trash root folder to the project.
"""
- newItem = NWItem(self)
- newItem.setName("Trash")
- newItem.setType(nwItemType.TRASH)
- newItem.setClass(nwItemClass.TRASH)
- self.projTree.append(None, None, newItem)
- return newItem.itemHandle
+ trashHandle = self.projTree.trashRoot()
+ if trashHandle is None:
+ newItem = NWItem(self)
+ newItem.setName("Trash")
+ newItem.setType(nwItemType.TRASH)
+ newItem.setClass(nwItemClass.TRASH)
+ self.projTree.append(None, None, newItem)
+ return newItem.itemHandle
+
+ return trashHandle
##
# Project Methods
@@ -179,6 +183,7 @@ class NWProject():
hScene = self.newFile("New Scene", nwItemClass.NOVEL, hChapt)
self.projOpened = time()
self.setProjectChanged(True)
+ self.saveProject(autoSave=True)
return True
def clearProject(self):
@@ -674,7 +679,7 @@ class NWProject():
# Setters
##
- def setProjectPath(self, projPath):
+ def setProjectPath(self, projPath, newProject=False):
"""Set the project storage path, and also expand ~ to the user
directory using the path library.
"""
@@ -684,7 +689,18 @@ class NWProject():
if projPath.startswith("~"):
projPath = path.expanduser(projPath)
self.projPath = path.abspath(projPath)
+
+ if newProject and self.mainConf.showGUI:
+ if listdir(self.projPath):
+ self.theParent.makeAlert((
+ "New project folder is not empty. "
+ "Each project requires a dedicated project folder."
+ ), nwAlert.ERROR)
+ return False
+
+ self.ensureFolderStructure()
self.setProjectChanged(True)
+
return True
def setProjectName(self, projName):
diff --git a/nw/gui/additions/qconfiglayout.py b/nw/gui/additions/qconfiglayout.py
index be7571b5..70b3042d 100644
--- a/nw/gui/additions/qconfiglayout.py
+++ b/nw/gui/additions/qconfiglayout.py
@@ -107,7 +107,7 @@ class QConfigLayout(QGridLayout):
return
- def addRow(self, theLabel, theWidget, helpText=None, theUnit=None):
+ def addRow(self, theLabel, theWidget, helpText=None, theUnit=None, theButton=None):
"""Add a label and a widget as a new row of the grid.
"""
thisEntry = {
@@ -152,6 +152,12 @@ class QConfigLayout(QGridLayout):
controlBox.addWidget(QLabel(theUnit), 0, Qt.AlignVCenter)
controlBox.setSpacing(8)
self.addLayout(controlBox, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignVCenter)
+ elif theButton is not None:
+ controlBox = QHBoxLayout()
+ controlBox.addWidget(qWidget, 0, Qt.AlignVCenter)
+ controlBox.addWidget(theButton, 0, Qt.AlignVCenter)
+ controlBox.setSpacing(8)
+ self.addLayout(controlBox, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignVCenter)
else:
self.addWidget(qWidget, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignVCenter)
diff --git a/nw/gui/build.py b/nw/gui/build.py
index 0c539f35..3c2e73da 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -26,6 +26,7 @@
"""
import logging
+import json
import nw
from os import path
@@ -39,25 +40,27 @@ from PyQt5.QtGui import (
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction,
- QFileDialog, QFontComboBox, QSpinBox
+ QFileDialog, QFontDialog, QSpinBox
)
from nw.gui.additions import QSwitch
from nw.core import ToHtml
from nw.constants import (
- nwAlert, nwItemType, nwItemLayout, nwItemClass
+ nwAlert, nwFiles, nwItemType, nwItemLayout, nwItemClass
)
logger = logging.getLogger(__name__)
class GuiBuildNovel(QDialog):
- FMT_ODT = 1
- FMT_PDF = 2
- FMT_HTM = 3
- FMT_MD = 4
- FMT_NWD = 5
- FMT_TXT = 6
+ FMT_ODT = 1
+ FMT_PDF = 2
+ FMT_HTM = 3
+ FMT_MD = 4
+ FMT_NWD = 5
+ FMT_TXT = 6
+ FMT_JSON_H = 7
+ FMT_JSON_M = 8
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
@@ -170,19 +173,21 @@ class GuiBuildNovel(QDialog):
self.textForm = QGridLayout(self)
self.textGroup.setLayout(self.textForm)
- self.textFont = QFontComboBox()
- self.textFont.setFixedWidth(220)
- self.textFont.setToolTip(
- "The font is used for PDF and printing. Other formats have no font set."
- )
- self.textFont.setCurrentFont(
- QFont(self.optState.getString("GuiBuildNovel", "textFont", self.mainConf.textFont))
+ ## Font Family
+ self.textFont = QLineEdit()
+ self.textFont.setReadOnly(True)
+ self.textFont.setFixedWidth(182)
+ self.textFont.setText(
+ self.optState.getString("GuiBuildNovel", "textFont", self.mainConf.textFont)
)
+ self.fontButton = QPushButton("...")
+ self.fontButton.setMaximumWidth(30)
+ self.fontButton.clicked.connect(self._selectFont)
self.textSize = QSpinBox(self)
self.textSize.setFixedWidth(60)
- self.textSize.setMinimum(5)
- self.textSize.setMaximum(48)
+ self.textSize.setMinimum(6)
+ self.textSize.setMaximum(72)
self.textSize.setSingleStep(1)
self.textSize.setToolTip(
"The size is used for PDF and printing. Other formats have no size set."
@@ -201,13 +206,15 @@ class GuiBuildNovel(QDialog):
self.textForm.addWidget(QLabel("Font family"), 0, 0, 1, 1, Qt.AlignLeft)
self.textForm.addWidget(self.textFont, 0, 1, 1, 1, Qt.AlignRight)
+ self.textForm.addWidget(self.fontButton, 0, 2, 1, 1, Qt.AlignRight)
self.textForm.addWidget(QLabel("Font size"), 1, 0, 1, 1, Qt.AlignLeft)
- self.textForm.addWidget(self.textSize, 1, 1, 1, 1, Qt.AlignRight)
+ self.textForm.addWidget(self.textSize, 1, 1, 1, 2, Qt.AlignRight)
self.textForm.addWidget(QLabel("Justify text"), 2, 0, 1, 1, Qt.AlignLeft)
- self.textForm.addWidget(self.justifyText, 2, 1, 1, 1, Qt.AlignRight)
+ self.textForm.addWidget(self.justifyText, 2, 1, 1, 2, Qt.AlignRight)
self.textForm.setColumnStretch(0, 1)
self.textForm.setColumnStretch(1, 0)
+ self.textForm.setColumnStretch(2, 0)
# Include Switches
# ================
@@ -329,7 +336,7 @@ class GuiBuildNovel(QDialog):
self.saveMD.triggered.connect(lambda: self._saveDocument(self.FMT_MD))
self.saveMenu.addAction(self.saveMD)
- self.saveNWD = QAction("novelWriter Markdown (.nwd)", self)
+ self.saveNWD = QAction("%s Markdown (.nwd)" % nw.__package__, self)
self.saveNWD.triggered.connect(lambda: self._saveDocument(self.FMT_NWD))
self.saveMenu.addAction(self.saveNWD)
@@ -337,6 +344,14 @@ class GuiBuildNovel(QDialog):
self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT))
self.saveMenu.addAction(self.saveTXT)
+ self.saveJsonH = QAction("JSON + %s HTML (.json)" % nw.__package__, self)
+ self.saveJsonH.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_H))
+ self.saveMenu.addAction(self.saveJsonH)
+
+ self.saveJsonM = QAction("JSON + %s Markdown (.json)" % nw.__package__, self)
+ self.saveJsonM.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_M))
+ self.saveMenu.addAction(self.saveJsonM)
+
self.btnClose = QPushButton("Close")
self.btnClose.clicked.connect(self._doClose)
@@ -368,6 +383,20 @@ class GuiBuildNovel(QDialog):
logger.debug("GuiBuildNovel initialisation complete")
+ # Load from Cache
+ if self._loadCache():
+ textFont = self.textFont.text()
+ textSize = self.textSize.value()
+ justifyText = self.justifyText.isChecked()
+ self.docView.setTextFont(textFont, textSize)
+ self.docView.setJustify(justifyText)
+ self.docView.setStyleSheet(self.htmlStyle)
+ self.docView.setContent(self.htmlText)
+ else:
+ self.htmlText = []
+ self.htmlStyle = []
+ self.nwdText = []
+
return
##
@@ -385,7 +414,7 @@ class GuiBuildNovel(QDialog):
fmtScene = self.fmtScene.text().strip()
fmtSection = self.fmtSection.text().strip()
justifyText = self.justifyText.isChecked()
- textFont = self.textFont.currentFont().family()
+ textFont = self.textFont.text()
textSize = self.textSize.value()
incSynopsis = self.includeSynopsis.isChecked()
incComments = self.includeComments.isChecked()
@@ -455,6 +484,8 @@ class GuiBuildNovel(QDialog):
self.docView.setStyleSheet(self.htmlStyle)
self.docView.setContent(self.htmlText)
+ self._saveCache()
+
return
def _checkInclude(self, theItem, noteFiles, novelFiles, ignoreFlag):
@@ -530,7 +561,7 @@ class GuiBuildNovel(QDialog):
elif theFormat == self.FMT_NWD:
fileExt = "nwd"
- textFmt = "%s markdown" % nw.__package__
+ textFmt = "%s Markdown" % nw.__package__
outTool = "NW"
elif theFormat == self.FMT_TXT:
@@ -539,6 +570,16 @@ class GuiBuildNovel(QDialog):
textFmt = "Plain Text"
outTool = "Qt"
+ elif theFormat == self.FMT_JSON_H:
+ fileExt = "json"
+ textFmt = "JSON + %s HTML" % nw.__package__
+ outTool = "NW"
+
+ elif theFormat == self.FMT_JSON_M:
+ fileExt = "json"
+ textFmt = "JSON + %s Markdown" % nw.__package__
+ outTool = "NW"
+
else:
return False
@@ -612,6 +653,33 @@ class GuiBuildNovel(QDialog):
for aLine in self.nwdText:
outFile.write(aLine)
+ elif theFormat == self.FMT_JSON_H or theFormat == self.FMT_JSON_M:
+ jsonData = {
+ "meta" : {
+ "workingTitle" : self.theProject.projName,
+ "novelTitle" : self.theProject.bookTitle,
+ "authors" : self.theProject.bookAuthors,
+ }
+ }
+
+ if theFormat == self.FMT_JSON_H:
+ theBody = []
+ for htmlPage in self.htmlText:
+ theBody.append(htmlPage.rstrip("\n").split("\n"))
+ jsonData["text"] = {
+ "css" : self.htmlStyle,
+ "html" : theBody,
+ }
+ elif theFormat == self.FMT_JSON_M:
+ theBody = []
+ for nwdPage in self.nwdText:
+ theBody.append(nwdPage.split("\n"))
+ jsonData["text"] = {
+ "nwd" : theBody,
+ }
+
+ outFile.write(json.dumps(jsonData, indent=2))
+
wSuccess = True
except Exception as e:
@@ -667,6 +735,72 @@ class GuiBuildNovel(QDialog):
self.docView.qDocument.print(thePrinter)
return
+ def _selectFont(self):
+ """Open the QFontDialog and set a font for the font style.
+ """
+ currFont = QFont()
+ currFont.setFamily(self.textFont.text())
+ currFont.setPointSize(self.textSize.value())
+ theFont, theStatus = QFontDialog.getFont(currFont, self)
+ if theStatus:
+ self.textFont.setText(theFont.family())
+ self.textSize.setValue(theFont.pointSize())
+ return
+
+ def _loadCache(self):
+ """Save the current data to cache.
+ """
+ buildCache = path.join(self.theProject.projCache, nwFiles.BUILD_CACHE)
+ dataCount = 0
+ if path.isfile(buildCache):
+
+ logger.debug("Loading build cache")
+ try:
+ with open(buildCache, mode="r", encoding="utf8") as inFile:
+ theJson = inFile.read()
+ theData = json.loads(theJson)
+ except Exception as e:
+ logger.error("Failed to load build cache")
+ logger.error(str(e))
+ return False
+
+ if "htmlText" in theData.keys():
+ self.htmlText = theData["htmlText"]
+ dataCount += 1
+ if "htmlStyle" in theData.keys():
+ self.htmlStyle = theData["htmlStyle"]
+ dataCount += 1
+ if "nwdText" in theData.keys():
+ self.nwdText = theData["nwdText"]
+ dataCount += 1
+
+ return dataCount == 3
+
+ def _saveCache(self):
+ """Save the current data to cache.
+ """
+ buildCache = path.join(self.theProject.projCache, nwFiles.BUILD_CACHE)
+
+ if self.mainConf.debugInfo:
+ nIndent = 2
+ else:
+ nIndent = None
+
+ logger.debug("Saving build cache")
+ try:
+ with open(buildCache, mode="w+", encoding="utf8") as outFile:
+ outFile.write(json.dumps({
+ "htmlText" : self.htmlText,
+ "htmlStyle" : self.htmlStyle,
+ "nwdText" : self.nwdText,
+ }, indent=nIndent))
+ except Exception as e:
+ logger.error("Failed to save build cache")
+ logger.error(str(e))
+ return False
+
+ return True
+
def _doClose(self):
"""Close button was clicked.
"""
@@ -709,7 +843,7 @@ class GuiBuildNovel(QDialog):
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", "textFont", self.textFont.currentFont().family())
+ 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())
@@ -746,6 +880,11 @@ class GuiBuildNovelDocView(QTextBrowser):
self.qDocument = self.document()
self.qDocument.setDocumentMargin(self.mainConf.textMargin)
+ self.setPlaceholderText(
+ "This area will show the content of the document to be "
+ "exported or printed. Press the \"Build Novel Project\" "
+ "button to generate content."
+ )
theFont = QFont()
if self.mainConf.textFont is None:
diff --git a/nw/gui/dialogs/preferences.py b/nw/gui/dialogs/preferences.py
index eaa034d1..be45b396 100644
--- a/nw/gui/dialogs/preferences.py
+++ b/nw/gui/dialogs/preferences.py
@@ -33,9 +33,8 @@ from os import path
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import (
- QDialog, QWidget, QHBoxLayout, QVBoxLayout, QTabWidget, QComboBox, QSpinBox,
- QPushButton, QFontComboBox, QLineEdit, QDialogButtonBox, QMessageBox,
- QFileDialog
+ QDialog, QWidget, QComboBox, QSpinBox, QPushButton, QLineEdit, QMessageBox,
+ QDialogButtonBox, QFileDialog, QFontDialog
)
from nw.gui.additions import QSwitch, QConfigLayout, PagedDialog
@@ -183,6 +182,34 @@ class GuiConfigEditGeneralTab(QWidget):
"This may improve the look of icons on dark themes."
)
+ ## Font Family
+ self.guiFont = QLineEdit()
+ self.guiFont.setReadOnly(True)
+ self.guiFont.setFixedWidth(162)
+ self.guiFont.setText(self.mainConf.guiFont)
+ self.fontButton = QPushButton("...")
+ self.fontButton.setMaximumWidth(30)
+ self.fontButton.clicked.connect(self._selectFont)
+ self.mainForm.addRow(
+ "Font family",
+ self.guiFont,
+ "Changing this requires restarting %s." % nw.__package__,
+ theButton = self.fontButton
+ )
+
+ ## Font Size
+ self.guiFontSize = QSpinBox(self)
+ self.guiFontSize.setMinimum(8)
+ self.guiFontSize.setMaximum(60)
+ self.guiFontSize.setSingleStep(1)
+ self.guiFontSize.setValue(self.mainConf.guiFontSize)
+ self.mainForm.addRow(
+ "Font size",
+ self.guiFontSize,
+ "Changing this requires restarting %s." % nw.__package__,
+ theUnit = "pt"
+ )
+
# GUI Settings
# ============
self.mainForm.addGroupLabel("GUI Settings")
@@ -266,6 +293,8 @@ class GuiConfigEditGeneralTab(QWidget):
guiTheme = self.selectTheme.currentData()
guiIcons = self.selectIcons.currentData()
guiDark = self.preferDarkIcons.isChecked()
+ guiFont = self.guiFont.text()
+ guiFontSize = self.guiFontSize.value()
showFullPath = self.showFullPath.isChecked()
autoSaveDoc = self.autoSaveDoc.value()
autoSaveProj = self.autoSaveProj.value()
@@ -276,10 +305,14 @@ class GuiConfigEditGeneralTab(QWidget):
# Check if restart is needed
needsRestart |= self.mainConf.guiTheme != guiTheme
needsRestart |= self.mainConf.guiIcons != guiIcons
+ needsRestart |= self.mainConf.guiFont != guiFont
+ needsRestart |= self.mainConf.guiFontSize != guiFontSize
self.mainConf.guiTheme = guiTheme
self.mainConf.guiIcons = guiIcons
self.mainConf.guiDark = guiDark
+ self.mainConf.guiFont = guiFont
+ self.mainConf.guiFontSize = guiFontSize
self.mainConf.showFullPath = showFullPath
self.mainConf.autoSaveDoc = autoSaveDoc
self.mainConf.autoSaveProj = autoSaveProj
@@ -321,6 +354,18 @@ class GuiConfigEditGeneralTab(QWidget):
self.askBeforeBackup.setEnabled(theState)
return
+ def _selectFont(self):
+ """Open the QFontDialog and set a font for the font style.
+ """
+ currFont = QFont()
+ currFont.setFamily(self.mainConf.guiFont)
+ currFont.setPointSize(self.mainConf.guiFontSize)
+ theFont, theStatus = QFontDialog.getFont(currFont, self)
+ if theStatus:
+ self.guiFont.setText(theFont.family())
+ self.guiFontSize.setValue(theFont.pointSize())
+ return
+
# END Class GuiConfigEditGeneralTab
class GuiConfigEditLayoutTab(QWidget):
@@ -341,26 +386,32 @@ class GuiConfigEditLayoutTab(QWidget):
# ==========
self.mainForm.addGroupLabel("Text Style")
+
## Font Family
- self.textStyleFont = QFontComboBox()
- self.textStyleFont.setMaximumWidth(200)
- self.textStyleFont.setCurrentFont(QFont(self.mainConf.textFont))
+ self.textStyleFont = QLineEdit()
+ self.textStyleFont.setReadOnly(True)
+ self.textStyleFont.setFixedWidth(162)
+ self.textStyleFont.setText(self.mainConf.textFont)
+ self.fontButton = QPushButton("...")
+ self.fontButton.setMaximumWidth(30)
+ self.fontButton.clicked.connect(self._selectFont)
self.mainForm.addRow(
"Font family",
self.textStyleFont,
- "Font for the document editor and viewer."
+ "Font for the document editor and viewer.",
+ theButton = self.fontButton
)
## Font Size
self.textStyleSize = QSpinBox(self)
- self.textStyleSize.setMinimum(5)
- self.textStyleSize.setMaximum(120)
+ self.textStyleSize.setMinimum(8)
+ self.textStyleSize.setMaximum(60)
self.textStyleSize.setSingleStep(1)
self.textStyleSize.setValue(self.mainConf.textSize)
self.mainForm.addRow(
"Font size",
self.textStyleSize,
- theUnit="pt"
+ theUnit = "pt"
)
# Text Flow
@@ -461,7 +512,7 @@ class GuiConfigEditLayoutTab(QWidget):
validEntries = True
needsRestart = False
- textFont = self.textStyleFont.currentFont().family()
+ textFont = self.textStyleFont.text()
textSize = self.textStyleSize.value()
textWidth = self.textFlowMax.value()
zenWidth = self.zenDocWidth.value()
@@ -487,6 +538,22 @@ class GuiConfigEditLayoutTab(QWidget):
return validEntries, needsRestart
+ ##
+ # Slots
+ ##
+
+ def _selectFont(self):
+ """Open the QFontDialog and set a font for the font style.
+ """
+ currFont = QFont()
+ currFont.setFamily(self.mainConf.textFont)
+ currFont.setPointSize(self.mainConf.textSize)
+ theFont, theStatus = QFontDialog.getFont(currFont, self)
+ if theStatus:
+ self.textStyleFont.setText(theFont.family())
+ self.textStyleSize.setValue(theFont.pointSize())
+ return
+
# END Class GuiConfigEditLayoutTab
class GuiConfigEditEditingTab(QWidget):
diff --git a/nw/gui/elements/docdetails.py b/nw/gui/elements/docdetails.py
index e72f6486..39bc6fe6 100644
--- a/nw/gui/elements/docdetails.py
+++ b/nw/gui/elements/docdetails.py
@@ -29,7 +29,7 @@ import logging
import nw
from PyQt5.QtCore import Qt
-from PyQt5.QtGui import QFont
+from PyQt5.QtGui import QFont, QIcon, QPixmap
from PyQt5.QtWidgets import QFrame, QGridLayout, QLabel
from nw.constants import (
@@ -47,30 +47,34 @@ class GuiDocDetails(QFrame):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
+ self.theTheme = theParent.theTheme
+ self.theHandle = None
self.mainBox = QGridLayout(self)
self.mainBox.setVerticalSpacing(1)
self.mainBox.setHorizontalSpacing(6)
self.setLayout(self.mainBox)
- self.fntLabel = QFont()
- self.fntLabel.setPointSize(10)
- self.fntLabel.setBold(True)
+ self.pS = 0.9*self.theTheme.fontPointSize
+ self.iPx = self.theTheme.textIconSize
+ self.sPx = int(round(0.8*self.theTheme.textIconSize))
- self.fntFixed = QFont()
- self.fntFixed.setFamily("Monospace")
- self.fntFixed.setPointSize(10)
+ self.expCheck = self.theTheme.getPixmap("check", (self.iPx, self.iPx))
+ self.expCross = self.theTheme.getPixmap("cross", (self.iPx, self.iPx))
+
+ self.fntLabel = QFont()
+ self.fntLabel.setBold(True)
+ self.fntLabel.setPointSizeF(self.pS)
self.fntValue = QFont()
- self.fntValue.setPointSize(10)
+ self.fntValue.setPointSizeF(self.pS)
# Label
- self.labelName = QLabel("Label ")
+ self.labelName = QLabel("Label")
self.labelName.setFont(self.fntLabel)
self.labelName.setAlignment(Qt.AlignLeft | Qt.AlignBaseline)
self.labelFlag = QLabel("")
- self.labelFlag.setFont(self.fntFixed)
self.labelFlag.setAlignment(Qt.AlignRight | Qt.AlignBaseline)
self.labelData = QLabel("")
@@ -79,12 +83,11 @@ class GuiDocDetails(QFrame):
self.labelData.setWordWrap(True)
# Status
- self.statusName = QLabel("Status ")
+ self.statusName = QLabel("Status")
self.statusName.setFont(self.fntLabel)
self.statusName.setAlignment(Qt.AlignLeft)
self.statusFlag = QLabel("")
- self.statusFlag.setFont(self.fntFixed)
self.statusFlag.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.statusData = QLabel("")
@@ -92,12 +95,12 @@ class GuiDocDetails(QFrame):
self.statusData.setAlignment(Qt.AlignLeft)
# Class
- self.className = QLabel("Class ")
+ self.className = QLabel("Class")
self.className.setFont(self.fntLabel)
self.className.setAlignment(Qt.AlignLeft)
self.classFlag = QLabel("")
- self.classFlag.setFont(self.fntFixed)
+ self.classFlag.setFont(self.fntValue)
self.classFlag.setAlignment(Qt.AlignRight)
self.classData = QLabel("")
@@ -105,12 +108,12 @@ class GuiDocDetails(QFrame):
self.classData.setAlignment(Qt.AlignLeft)
# Layout
- self.layoutName = QLabel("Layout ")
+ self.layoutName = QLabel("Layout")
self.layoutName.setFont(self.fntLabel)
self.layoutName.setAlignment(Qt.AlignLeft)
self.layoutFlag = QLabel("")
- self.layoutFlag.setFont(self.fntFixed)
+ self.layoutFlag.setFont(self.fntValue)
self.layoutFlag.setAlignment(Qt.AlignRight)
self.layoutData = QLabel("")
@@ -173,6 +176,12 @@ class GuiDocDetails(QFrame):
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)
+ countWidth = self.theTheme.getTextWidth("99,999", self.fntValue)
+ self.mainBox.setColumnMinimumWidth(1, flagWidth)
+ self.mainBox.setColumnMinimumWidth(4, countWidth)
+
logger.debug("DocDetails initialisation complete")
return
@@ -181,9 +190,20 @@ class GuiDocDetails(QFrame):
# Class Methods
##
+ def updateCounts(self, tHandle, cC, wC, pC):
+ """Just update the counts if the handle is the same as the one
+ we're already showing.
+ """
+ if tHandle == self.theHandle:
+ self.cCountData.setText("{:n}".format(cC))
+ self.wCountData.setText("{:n}".format(wC))
+ self.pCountData.setText("{:n}".format(pC))
+ return
+
def updateViewBox(self, tHandle):
"""Populate the details box from a given handle.
"""
+ self.theHandle = tHandle
nwItem = self.theProject.projTree[tHandle]
if nwItem is None:
@@ -214,14 +234,12 @@ class GuiDocDetails(QFrame):
if nwItem.itemType == nwItemType.FILE:
if nwItem.isExported:
- exportFlag = nwUnicode.U_CHECK
+ self.labelFlag.setPixmap(self.expCheck)
else:
- exportFlag = " "
+ self.labelFlag.setPixmap(self.expCross)
else:
- exportFlag = "-"
-
- self.labelFlag.setText(exportFlag)
- self.statusFlag.setPixmap(flagIcon.pixmap(10, 10))
+ self.labelFlag.setPixmap(QPixmap(1,1))
+ self.statusFlag.setPixmap(flagIcon.pixmap(self.sPx, self.sPx))
self.classFlag.setText(nwLabels.CLASS_FLAG[nwItem.itemClass])
if nwItem.itemLayout == nwItemLayout.NO_LAYOUT:
self.layoutFlag.setText("-")
diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py
index aa3ea1c0..8ceb297e 100644
--- a/nw/gui/elements/doceditor.py
+++ b/nw/gui/elements/doceditor.py
@@ -300,15 +300,16 @@ class GuiDocEditor(QTextEdit):
docText = self.getText()
cursPos = self.getCursorPosition()
- theItem = self.nwDocument.theItem
- theItem.setCharCount(self.charCount)
- theItem.setWordCount(self.wordCount)
- theItem.setParaCount(self.paraCount)
- theItem.setCursorPos(cursPos)
+ self.nwDocument.theItem.setCharCount(self.charCount)
+ self.nwDocument.theItem.setWordCount(self.wordCount)
+ self.nwDocument.theItem.setParaCount(self.paraCount)
+ self.nwDocument.theItem.setCursorPos(cursPos)
self.nwDocument.saveDocument(docText)
self.setDocumentChanged(False)
- self.theParent.theIndex.scanText(theItem.itemHandle, docText)
+ self.theParent.theIndex.scanText(
+ self.nwDocument.theItem.itemHandle, docText
+ )
return True
@@ -737,15 +738,24 @@ class GuiDocEditor(QTextEdit):
def _updateCounts(self):
"""Slot for the word counter's finished signal
"""
+ if self.theHandle is None or self.nwDocument.theItem is None:
+ return
+
logger.verbose("Updating word count")
- tHandle = self.nwDocument.docHandle
self.charCount = self.wCounter.charCount
self.wordCount = self.wCounter.wordCount
self.paraCount = self.wCounter.paraCount
- self.theParent.statusBar.setCounts(self.charCount,self.wordCount,self.paraCount)
- self.theParent.treeView.propagateCount(tHandle, self.wordCount)
+ self.nwDocument.theItem.setCharCount(self.charCount)
+ self.nwDocument.theItem.setWordCount(self.wordCount)
+ self.nwDocument.theItem.setParaCount(self.paraCount)
+
+ self.theParent.statusBar.setCounts(self.charCount, self.wordCount, self.paraCount)
+ self.theParent.treeView.propagateCount(self.theHandle, self.wordCount)
self.theParent.treeView.projectWordCount()
+ self.theParent.treeMeta.updateCounts(
+ self.theHandle, self.charCount, self.wordCount, self.paraCount
+ )
self._checkDocSize(self.charCount)
return
diff --git a/nw/gui/elements/doctitlebar.py b/nw/gui/elements/doctitlebar.py
index 38425961..8c5208e4 100644
--- a/nw/gui/elements/doctitlebar.py
+++ b/nw/gui/elements/doctitlebar.py
@@ -52,19 +52,20 @@ class GuiDocTitleBar(QLabel):
self.setText("")
self.setIndent(0)
self.setMargin(0)
- self.setContentsMargins(0,0,0,0)
+ self.setContentsMargins(0, 0, 0, 0)
self.setAutoFillBackground(True)
self.setAlignment(Qt.AlignCenter)
self.setWordWrap(True)
self.setFrameShape(QFrame.NoFrame)
self.setLineWidth(0)
+
lblPalette = self.palette()
lblPalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack))
lblPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
self.setPalette(lblPalette)
lblFont = self.font()
- lblFont.setPointSizeF(0.9*self.theTheme.defFontSize)
+ lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize)
self.setFont(lblFont)
logger.debug("DocTitleBar initialisation complete")
diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py
index 02db9bde..62d5fd2f 100644
--- a/nw/gui/elements/doctree.py
+++ b/nw/gui/elements/doctree.py
@@ -29,7 +29,7 @@ import logging
import nw
from PyQt5.QtCore import Qt, QSize
-from PyQt5.QtGui import QFont, QColor
+from PyQt5.QtGui import QFont, QColor, QIcon
from PyQt5.QtWidgets import (
qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMessageBox
)
@@ -45,8 +45,9 @@ class GuiDocTree(QTreeWidget):
C_NAME = 0
C_COUNT = 1
- C_FLAGS = 2
- C_HANDLE = 3
+ C_EXPORT = 2
+ C_FLAGS = 3
+ C_HANDLE = 4
def __init__(self, theParent, theProject):
QTreeWidget.__init__(self, theParent)
@@ -64,15 +65,20 @@ class GuiDocTree(QTreeWidget):
self.clearTree()
# Build GUI
- self.setIconSize(QSize(13,13))
+ iPx = self.theTheme.textIconSize
+ self.setIconSize(QSize(iPx, iPx))
self.setExpandsOnDoubleClick(True)
- self.setIndentation(13)
- self.setColumnCount(4)
- self.setHeaderLabels(["Label","Words","Flags","Handle"])
+ self.setIndentation(iPx)
+ self.setColumnCount(5)
+ self.setHeaderLabels(["Label", "Words", "Inc", "Flags", "Handle"])
self.hideColumn(self.C_HANDLE)
treeHead = self.headerItem()
- treeHead.setTextAlignment(self.C_COUNT,Qt.AlignRight)
+ treeHead.setTextAlignment(self.C_COUNT, Qt.AlignRight)
+ treeHead.setToolTip(self.C_NAME, "Item label")
+ treeHead.setToolTip(self.C_COUNT, "Word count")
+ treeHead.setToolTip(self.C_EXPORT, "Include in build")
+ treeHead.setToolTip(self.C_FLAGS, "Status, class, and layout flags")
# Allow Move by Drag & Drop
self.setDragEnabled(True)
@@ -87,11 +93,11 @@ class GuiDocTree(QTreeWidget):
# self.setSelectionMode(QAbstractItemView.ExtendedSelection)
# self.setSelectionBehavior(QAbstractItemView.SelectRows)
- for colN in range(len(self.mainConf.treeColWidth)):
- self.setColumnWidth(colN,self.mainConf.treeColWidth[colN])
+ for colN, colW in enumerate(self.mainConf.treeColWidth):
+ self.setColumnWidth(colN, colW)
- self.fontFlags = QFont("Monospace",10)
- self.fontCount = QFont("Monospace",10)
+ self.resizeColumnToContents(self.C_EXPORT)
+ self.resizeColumnToContents(self.C_FLAGS)
logger.debug("DocTree initialisation complete")
@@ -200,8 +206,8 @@ class GuiDocTree(QTreeWidget):
def revealTreeItem(self, tHandle):
"""Reveal a newly added project item in the project tree.
"""
- nwItem = self.theProject.projTree[tHandle]
- trItem = self._addTreeItem(nwItem)
+ nwItem = self.theProject.projTree[tHandle]
+ trItem = self._addTreeItem(nwItem)
pHandle = nwItem.parHandle
if pHandle is not None and pHandle in self.theMap.keys():
self.theMap[pHandle].setExpanded(True)
@@ -271,7 +277,6 @@ class GuiDocTree(QTreeWidget):
retVals = [
self.columnWidth(0),
self.columnWidth(1),
- self.columnWidth(2),
]
return retVals
@@ -422,30 +427,30 @@ class GuiDocTree(QTreeWidget):
tHandle = nwItem.itemHandle
pHandle = nwItem.parHandle
- stExport = " "
- stClass = nwLabels.CLASS_FLAG[nwItem.itemClass]
- stLayout = ""
+ expIcon = QIcon()
+ stClass = nwLabels.CLASS_FLAG[nwItem.itemClass]
+ stLayout = ""
if nwItem.itemType == nwItemType.FILE:
stLayout = "."+nwLabels.LAYOUT_FLAG[nwItem.itemLayout]
if nwItem.isExported:
- stExport = nwUnicode.U_CHECK
- else:
- stExport = "-"
-
- tStatus = stExport+" "+stClass+stLayout
+ expIcon = self.theTheme.getIcon("check")
+ else:
+ expIcon = self.theTheme.getIcon("cross")
+ tStatus = stClass+stLayout
iStatus = nwItem.itemStatus
if tClass == nwItemClass.NOVEL:
- iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid
+ iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid
flagIcon = self.theParent.statusIcons[iStatus]
else:
- iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid
+ iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid
flagIcon = self.theParent.importIcons[iStatus]
trItem.setText(self.C_NAME, tName)
- trItem.setText(self.C_FLAGS,tStatus)
- trItem.setIcon(self.C_FLAGS,flagIcon)
+ trItem.setIcon(self.C_EXPORT, expIcon)
+ trItem.setText(self.C_FLAGS, tStatus)
+ trItem.setIcon(self.C_FLAGS, flagIcon)
return
@@ -569,12 +574,11 @@ class GuiDocTree(QTreeWidget):
newItem.setText(self.C_NAME, "")
newItem.setText(self.C_COUNT, "0")
+ newItem.setText(self.C_EXPORT, "")
newItem.setText(self.C_FLAGS, "")
newItem.setText(self.C_HANDLE, tHandle)
- # newItem.setForeground(self.C_COUNT,QColor(*self.theParent.theTheme.treeWCount))
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
- newItem.setFont(self.C_FLAGS, self.fontFlags)
self.theMap[tHandle] = newItem
if pHandle is None:
@@ -608,15 +612,15 @@ class GuiDocTree(QTreeWidget):
"""Adds the trash root folder if it doesn't already exist in the
project tree.
"""
- trashHandle = self.theProject.projTree.trashRoot()
+ trashHandle = self.theProject.trashFolder()
if trashHandle is None:
- self.theProject.addTrash()
+ return None
+ trItem = self._getTreeItem(trashHandle)
+ if trItem is None:
trItem = self._addTreeItem(
self.theProject.projTree[trashHandle]
)
trItem.setExpanded(True)
- else:
- trItem = self._getTreeItem(trashHandle)
return trItem
def _addOrphanedRoot(self):
@@ -627,6 +631,7 @@ class GuiDocTree(QTreeWidget):
newItem = QTreeWidgetItem([""]*4)
newItem.setText(self.C_NAME, "Orphaned Files")
newItem.setText(self.C_COUNT, "")
+ newItem.setText(self.C_EXPORT, "")
newItem.setText(self.C_FLAGS, "")
newItem.setText(self.C_HANDLE, "")
self.addTopLevelItem(newItem)
diff --git a/nw/gui/icons.py b/nw/gui/icons.py
index db5d6e9e..f1a33145 100644
--- a/nw/gui/icons.py
+++ b/nw/gui/icons.py
@@ -93,6 +93,8 @@ class GuiIcons:
"clear" : (QStyle.SP_LineEditClearButton, "clear_left"),
"save" : (QStyle.SP_DialogSaveButton, "document-save"),
"edit" : (None, None),
+ "check" : (None, None),
+ "cross" : (None, None),
## Other Icons
"warning" : (QStyle.SP_MessageBoxWarning, "dialog-warning"),
diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py
index d268ab74..129ab9a8 100644
--- a/nw/gui/statusbar.py
+++ b/nw/gui/statusbar.py
@@ -48,6 +48,7 @@ class GuiMainStatus(QStatusBar):
self.mainConf = nw.CONFIG
self.theParent = theParent
+ self.theTheme = theParent.theTheme
self.refTime = None
self.charCount = 0
@@ -56,11 +57,11 @@ class GuiMainStatus(QStatusBar):
self.projWords = 0
self.sessWords = 0
- self.monoFont = QFont("Monospace",10)
+ colNone = QColor(*self.theTheme.statNone)
+ colTrue = QColor(*self.theTheme.statUnsaved)
+ colFalse = QColor(*self.theTheme.statSaved)
- colNone = QColor(*self.theParent.theTheme.statNone)
- colTrue = QColor(*self.theParent.theTheme.statUnsaved)
- colFalse = QColor(*self.theParent.theTheme.statSaved)
+ iPx = self.theTheme.textIconSize
# Permanent Widgets
# =================
@@ -68,14 +69,14 @@ class GuiMainStatus(QStatusBar):
## The Spell Checker Language
self.langIcon = QLabel("")
self.langText = QLabel("None")
- self.langIcon.setPixmap(self.theParent.theTheme.getPixmap("status_lang",(14,14)))
+ 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.addPermanentWidget(self.langIcon)
self.addPermanentWidget(self.langText)
## The Editor Status
- self.docIcon = StatusLED(colNone, colTrue, colFalse, 14, 14, self)
+ 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)
@@ -83,7 +84,7 @@ class GuiMainStatus(QStatusBar):
self.addPermanentWidget(self.docText)
## The Project Status
- self.projIcon = StatusLED(colNone, colTrue, colFalse, 14, 14, self)
+ 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)
@@ -93,19 +94,19 @@ class GuiMainStatus(QStatusBar):
## The Project and Session Stats
self.statsIcon = QLabel()
self.statsText = QLabel("")
- self.statsIcon.setPixmap(self.theParent.theTheme.getPixmap("status_stats",(14,14)))
+ 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.addPermanentWidget(self.statsIcon)
self.addPermanentWidget(self.statsText)
## The Session Clock
+ ### Set the mimimum width so the label doesn't rescale every second
self.timeIcon = QLabel()
self.timeText = QLabel("")
- self.timeIcon.setPixmap(self.theParent.theTheme.getPixmap("status_time",(14,14)))
+ self.timeIcon.setPixmap(self.theTheme.getPixmap("status_time", (iPx, iPx)))
self.timeText.setToolTip("Session Time")
- self.timeText.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
- self.timeText.setFont(self.monoFont)
+ self.timeText.setMinimumWidth(self.theTheme.getTextWidth("00:00:00:"))
self.timeIcon.setContentsMargins(0, 0, 0, 0)
self.timeText.setContentsMargins(0, 0, 0, 0)
self.addPermanentWidget(self.timeIcon)
@@ -200,7 +201,7 @@ class GuiMainStatus(QStatusBar):
"""Update statistics.
"""
self.statsText.setToolTip(
- "D: Document word count
P: Project word count"
+ "D: Document word count
P: Project word count (session change)"
)
self.statsText.setText((
"D:{wC:n} P:{pWC:n} ({sWC:+n})"
diff --git a/nw/gui/theme.py b/nw/gui/theme.py
index 04a00c0e..a376e1b1 100644
--- a/nw/gui/theme.py
+++ b/nw/gui/theme.py
@@ -30,9 +30,12 @@ import configparser
import nw
from os import path, listdir
+from math import ceil
from PyQt5.QtWidgets import qApp
-from PyQt5.QtGui import QPalette, QColor, QIcon
+from PyQt5.QtGui import (
+ QPalette, QColor, QIcon, QFont, QFontMetrics, QFontDatabase
+)
from nw.constants import nwAlert
from nw.gui.icons import GuiIcons
@@ -48,7 +51,7 @@ class GuiTheme:
self.theIcons = GuiIcons(self.theParent)
self.guiPalette = QPalette()
self.guiPath = "gui"
- self.iconPath = "icons"
+ self.fontPath = "fonts"
self.syntaxPath = "syntax"
self.cssName = "style.qss"
self.confName = "theme.conf"
@@ -110,7 +113,10 @@ class GuiTheme:
self.syntaxFile = None
self.confFile = None
self.cssFile = None
+ self.guiFontDB = QFontDatabase()
+ self.loadFonts()
+ self.updateFont()
self.updateTheme()
self.theIcons.updateTheme()
@@ -119,19 +125,86 @@ class GuiTheme:
self.loadDecoration = self.theIcons.loadDecoration
# Extract Other Info
- self.defFont = qApp.font()
- self.defFontSize = self.defFont.pointSizeF()
+ self.guiDPI = qApp.primaryScreen().physicalDotsPerInch()
+ 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()))
+
+ 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)
return
+ ##
+ # Methods
+ ##
+
+ def getTextWidth(self, theText, theFont=None):
+ """Returns the width needed to contain a given piece of text.
+ """
+ if isinstance(theFont, QFont):
+ qMetrics = QFontMetrics(theFont)
+ else:
+ qMetrics = QFontMetrics(self.guiFont)
+ return int(ceil(qMetrics.boundingRect(theText).width()))
+
##
# Actions
##
+ def loadFonts(self):
+ """Add the fonts in the assets fonts folder to the app.
+ """
+ ttfList = []
+ fontAssets = path.join(self.mainConf.assetPath, self.fontPath)
+ for fontFam in listdir(fontAssets):
+ fontDir = path.join(fontAssets, fontFam)
+ if path.isdir(fontDir):
+ if not fontFam in self.guiFontDB.families():
+ for fontFile in listdir(fontDir):
+ ttfFile = path.join(fontDir, fontFile)
+ if path.isfile(ttfFile) and fontFile.endswith(".ttf"):
+ ttfList.append(ttfFile)
+
+ for ttfFile in ttfList:
+ logger.verbose("Font asset: %s" % path.relpath(ttfFile))
+ fontID = self.guiFontDB.addApplicationFont(ttfFile)
+ if fontID < 0:
+ logger.error("Failed to add font: %s" % path.relpath(ttfFile))
+
+ return
+
+ def updateFont(self):
+ """Updated the GUI's font style from settings,
+ """
+ theFont = QFont()
+ if self.mainConf.guiFont not in self.guiFontDB.families():
+ if self.mainConf.osWindows:
+ # On Windows, default to Cantarell provided by novelWriter
+ theFont.setFamily("Cantarell")
+ theFont.setPointSize(11)
+ else:
+ theFont = self.guiFontDB.systemFont(QFontDatabase.GeneralFont)
+ self.mainConf.guiFont = theFont.family()
+ self.mainConf.guiFontSize = theFont.pointSize()
+ else:
+ theFont.setFamily(self.mainConf.guiFont)
+ theFont.setPointSize(self.mainConf.guiFontSize)
+
+ qApp.setFont(theFont)
+
+ return
+
def updateTheme(self):
"""Update the GUI theme from theme files.
"""
-
self.guiTheme = self.mainConf.guiTheme
self.guiSyntax = self.mainConf.guiSyntax
self.themeRoot = self.mainConf.themeRoot
diff --git a/nw/guimain.py b/nw/guimain.py
index a3b7d46e..bfc98c71 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -294,12 +294,14 @@ class GuiMain(QMainWindow):
return False
logger.info("Creating new project")
- self.theProject.newProject()
- self.theProject.setProjectPath(projPath)
- self.rebuildTree()
- self.saveProject()
- self.hasProject = True
- self.statusBar.setRefTime(self.theProject.projOpened)
+ if self.theProject.setProjectPath(projPath, newProject=True):
+ self.theProject.newProject()
+ self.rebuildTree()
+ self.saveProject()
+ self.hasProject = True
+ self.statusBar.setRefTime(self.theProject.projOpened)
+ else:
+ return False
return True
@@ -1017,25 +1019,33 @@ class GuiMain(QMainWindow):
return
def _treeDoubleClick(self, tItem, colNo):
- tHandle = tItem.text(3)
+ """The user double-clicked an item in the tree. If it is a file,
+ we open it. Otherwise, we do nothing.
+ """
+ tHandle = tItem.text(self.treeView.C_HANDLE)
logger.verbose("User double clicked tree item with handle %s" % tHandle)
nwItem = self.theProject.projTree[tHandle]
- if nwItem.itemType == nwItemType.FILE:
- logger.verbose("Requested item %s is a file" % tHandle)
- self.openDocument(tHandle)
- else:
- logger.verbose("Requested item %s is a folder" % tHandle)
+ if nwItem is not None:
+ if nwItem.itemType == nwItemType.FILE:
+ logger.verbose("Requested item %s is a file" % tHandle)
+ self.openDocument(tHandle)
+ else:
+ logger.verbose("Requested item %s is a folder" % tHandle)
return
def _treeKeyPressReturn(self):
+ """The user pressed return an item in the tree. If it is a file,
+ we open it. Otherwise, we do nothing.
+ """
tHandle = self.treeView.getSelectedHandle()
logger.verbose("User pressed return on tree item with handle %s" % tHandle)
nwItem = self.theProject.projTree[tHandle]
- if nwItem.itemType == nwItemType.FILE:
- logger.verbose("Requested item %s is a file" % tHandle)
- self.openDocument(tHandle)
- else:
- logger.verbose("Requested item %s is a folder" % tHandle)
+ if nwItem is not None:
+ if nwItem.itemType == nwItemType.FILE:
+ logger.verbose("Requested item %s is a file" % tHandle)
+ self.openDocument(tHandle)
+ else:
+ logger.verbose("Requested item %s is a folder" % tHandle)
return
def _keyPressEscape(self):
diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx
index a09a089f..ab090c92 100644
--- a/tests/reference/gui/1_nwProject.nwx
+++ b/tests/reference/gui/1_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
@@ -9,7 +9,7 @@
True
True
31489056e0916
- 31489056e0916
+ None
86
@@ -35,7 +35,7 @@
Main
-
+
-
Novel
ROOT
@@ -119,5 +119,12 @@
1
68
+ -
+ Trash
+ TRASH
+ TRASH
+ None
+ True
+
diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf
index acc95a94..f86bddc0 100644
--- a/tests/reference/novelwriter.conf
+++ b/tests/reference/novelwriter.conf
@@ -4,6 +4,8 @@ theme = default
syntax = default_light
icons = typicons_grey_light
guidark = False
+guifont =
+guifontsize = 11
[Sizes]
geometry = 1100, 650
diff --git a/tests/test_gui.py b/tests/test_gui.py
index c638e77f..e006cabb 100644
--- a/tests/test_gui.py
+++ b/tests/test_gui.py
@@ -235,6 +235,15 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
assert nwGUI.closeDocViewer()
qtbot.wait(stepDelay)
+ # Check a Quick Create and Delete
+ assert nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
+ newHandle = nwGUI.treeView.getSelectedHandle()
+ assert nwGUI.theProject.projTree["031b4af5197ec"] is not None
+ assert nwGUI.treeView.deleteItem()
+ assert nwGUI.treeView.setSelectedHandle(newHandle)
+ assert nwGUI.treeView.deleteItem()
+ assert nwGUI.saveProject()
+
# Check the files
refFile = path.join(nwTempGUI, "nwProject.nwx")
assert cmpFiles(refFile, path.join(nwRef, "gui", "1_nwProject.nwx"), [2])