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 @@ + + + + + + image/svg+xml + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + 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])