diff --git a/nw/config.py b/nw/config.py index f65f9554..e7da0533 100644 --- a/nw/config.py +++ b/nw/config.py @@ -43,13 +43,14 @@ class Config: self.appPath = None self.appRoot = None self.guiPath = None + self.themeRoot = None self.themePath = None # Set default values self.confChanged = False ## General - self.guiTheme = "default" + self.guiTheme = "default_dark" self.winGeometry = [1100, 650] self.treeColWidth = [120, 30, 50] self.mainPanePos = [300, 800] @@ -63,10 +64,11 @@ class Config: self.autoSaveDoc = 30 ## Text Editor + self.textFont = None + self.textSize = 12 self.textFixedW = True self.textWidth = 600 self.textMargin = [40, 40] - self.textSize = 13 self.tabWidth = 40 self.doJustify = True self.autoSelect = True @@ -77,9 +79,8 @@ class Config: self.doReplaceDots = True self.wordCountTimer = 5.0 - self.fmtDoubleQuotes = ["“","”"] self.fmtSingleQuotes = ["‘","’"] - self.fmtApostrophe = "’" + self.fmtDoubleQuotes = ["“","”"] self.spellLanguage = "en_GB" @@ -106,7 +107,8 @@ class Config: self.appRoot = path.join(self.appPath,path.pardir) self.helpPath = path.join(self.appRoot,"help","en_GB") self.guiPath = path.join(self.appPath,"gui") - self.themePath = path.join(self.appPath,"themes") + self.themeRoot = path.join(self.appPath,"themes") + self.themePath = path.join(self.themeRoot) # If config folder does not exist, make it. # This assumes that the os config folder itself exists. @@ -153,19 +155,22 @@ class Config: ## Editor cnfSec = "Editor" - self.textFixedW = self._parseLine(cnfParse, cnfSec, "fixedwidth", self.CNF_BOOL, self.textFixedW) - self.textWidth = self._parseLine(cnfParse, cnfSec, "width", self.CNF_INT, self.textWidth) - self.textMargin = self._parseLine(cnfParse, cnfSec, "margins", self.CNF_LIST, self.textMargin) - self.textSize = self._parseLine(cnfParse, cnfSec, "textsize", self.CNF_INT, self.textSize) - self.tabWidth = self._parseLine(cnfParse, cnfSec, "tabwidth", self.CNF_INT, self.tabWidth) - self.doJustify = self._parseLine(cnfParse, cnfSec, "justify", self.CNF_BOOL, self.doJustify) - self.autoSelect = self._parseLine(cnfParse, cnfSec, "autoselect", self.CNF_BOOL, self.autoSelect) - self.doReplace = self._parseLine(cnfParse, cnfSec, "autoreplace", self.CNF_BOOL, self.doReplace) - self.doReplaceSQuote = self._parseLine(cnfParse, cnfSec, "repsquotes", self.CNF_BOOL, self.doReplaceSQuote) - self.doReplaceDQuote = self._parseLine(cnfParse, cnfSec, "repdquotes", self.CNF_BOOL, self.doReplaceDQuote) - self.doReplaceDash = self._parseLine(cnfParse, cnfSec, "repdash", self.CNF_BOOL, self.doReplaceDash) - self.doReplaceDots = self._parseLine(cnfParse, cnfSec, "repdots", self.CNF_BOOL, self.doReplaceDots) - self.spellLanguage = self._parseLine(cnfParse, cnfSec, "spellcheck", self.CNF_STR, self.spellLanguage) + self.textFont = self._parseLine(cnfParse, cnfSec, "textfont", self.CNF_STR, self.textFont) + self.textSize = self._parseLine(cnfParse, cnfSec, "textsize", self.CNF_INT, self.textSize) + self.textFixedW = self._parseLine(cnfParse, cnfSec, "fixedwidth", self.CNF_BOOL, self.textFixedW) + self.textWidth = self._parseLine(cnfParse, cnfSec, "width", self.CNF_INT, self.textWidth) + self.textMargin = self._parseLine(cnfParse, cnfSec, "margins", self.CNF_LIST, self.textMargin) + self.tabWidth = self._parseLine(cnfParse, cnfSec, "tabwidth", self.CNF_INT, self.tabWidth) + self.doJustify = self._parseLine(cnfParse, cnfSec, "justify", self.CNF_BOOL, self.doJustify) + self.autoSelect = self._parseLine(cnfParse, cnfSec, "autoselect", self.CNF_BOOL, self.autoSelect) + self.doReplace = self._parseLine(cnfParse, cnfSec, "autoreplace", self.CNF_BOOL, self.doReplace) + self.doReplaceSQuote = self._parseLine(cnfParse, cnfSec, "repsquotes", self.CNF_BOOL, self.doReplaceSQuote) + self.doReplaceDQuote = self._parseLine(cnfParse, cnfSec, "repdquotes", self.CNF_BOOL, self.doReplaceDQuote) + self.doReplaceDash = self._parseLine(cnfParse, cnfSec, "repdash", self.CNF_BOOL, self.doReplaceDash) + self.doReplaceDots = self._parseLine(cnfParse, cnfSec, "repdots", self.CNF_BOOL, self.doReplaceDots) + self.fmtSingleQuotes = self._parseLine(cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes) + self.fmtDoubleQuotes = self._parseLine(cnfParse, cnfSec, "fmtdoublequote", self.CNF_LIST, self.fmtDoubleQuotes) + self.spellLanguage = self._parseLine(cnfParse, cnfSec, "spellcheck", self.CNF_STR, self.spellLanguage) ## Path cnfSec = "Path" @@ -205,19 +210,22 @@ class Config: ## Editor cnfSec = "Editor" cnfParse.add_section(cnfSec) - cnfParse.set(cnfSec,"fixedwidth", str(self.textFixedW)) - cnfParse.set(cnfSec,"width", str(self.textWidth)) - cnfParse.set(cnfSec,"margins", self._packList(self.textMargin)) - cnfParse.set(cnfSec,"textsize", str(self.textSize)) - cnfParse.set(cnfSec,"tabwidth", str(self.tabWidth)) - cnfParse.set(cnfSec,"justify", str(self.doJustify)) - cnfParse.set(cnfSec,"autoselect", str(self.autoSelect)) - cnfParse.set(cnfSec,"autoreplace",str(self.doReplace)) - cnfParse.set(cnfSec,"repsquotes", str(self.doReplaceSQuote)) - cnfParse.set(cnfSec,"repdquotes", str(self.doReplaceDQuote)) - cnfParse.set(cnfSec,"repdash", str(self.doReplaceDash)) - cnfParse.set(cnfSec,"repdots", str(self.doReplaceDots)) - cnfParse.set(cnfSec,"spellcheck", str(self.spellLanguage)) + cnfParse.set(cnfSec,"textfont", str(self.textFont)) + cnfParse.set(cnfSec,"textsize", str(self.textSize)) + cnfParse.set(cnfSec,"fixedwidth", str(self.textFixedW)) + cnfParse.set(cnfSec,"width", str(self.textWidth)) + cnfParse.set(cnfSec,"margins", self._packList(self.textMargin)) + cnfParse.set(cnfSec,"tabwidth", str(self.tabWidth)) + cnfParse.set(cnfSec,"justify", str(self.doJustify)) + cnfParse.set(cnfSec,"autoselect", str(self.autoSelect)) + cnfParse.set(cnfSec,"autoreplace", str(self.doReplace)) + cnfParse.set(cnfSec,"repsquotes", str(self.doReplaceSQuote)) + cnfParse.set(cnfSec,"repdquotes", str(self.doReplaceDQuote)) + cnfParse.set(cnfSec,"repdash", str(self.doReplaceDash)) + cnfParse.set(cnfSec,"repdots", str(self.doReplaceDots)) + cnfParse.set(cnfSec,"fmtsinglequote",self._packList(self.fmtSingleQuotes)) + cnfParse.set(cnfSec,"fmtdoublequote",self._packList(self.fmtDoubleQuotes)) + cnfParse.set(cnfSec,"spellcheck", str(self.spellLanguage)) ## Path cnfSec = "Path" diff --git a/nw/constants.py b/nw/constants.py index 8a545d0c..9c56b012 100644 --- a/nw/constants.py +++ b/nw/constants.py @@ -70,3 +70,53 @@ class nwLabels(): } # END Class nwLabels + +class nwQuotes(): + """Allowed quotation marks. + Source: https://en.wikipedia.org/wiki/Quotation_mark + """ + + SYMBOLS = [ + "\u0022", # Quotation mark + "\u0027", # Apostrophe + "\u00ab", # Left-pointing double angle quotation mark + "\u00bb", # Right-pointing double angle quotation mark + "\u2018", # Left single quotation mark + "\u2019", # Right single quotation mark + "\u201a", # Single low-9 quotation mark + "\u201b", # Single high-reversed-9 quotation mark + "\u201c", # Left double quotation mark + "\u201d", # Right double quotation mark + "\u201e", # Double low-9 quotation mark + "\u201f", # Double high-reversed-9 quotation mark + "\u2039", # Single left-pointing angle quotation mark + "\u203a", # Single right-pointing angle quotation mark + "\u2e42", # Double low-reversed-9 quotation mark + "\u300c", # Left corner bracket + "\u300d", # Right corner bracket + "\u300e", # Left white corner bracket + "\u300f", # Right white corner bracket + ] + HTML = { + "\u0022" : """, + "\u0027" : "'", + "\u00ab" : "«", + "\u00bb" : "»", + "\u2018" : "‘", + "\u2019" : "’", + "\u201a" : "‚", + "\u201b" : "‛", + "\u201c" : "“", + "\u201d" : "”", + "\u201e" : "„", + "\u201f" : "‟", + "\u2039" : "‹", + "\u203a" : "›", + "\u2e42" : "⹂", + "\u300c" : "「", + "\u300d" : "」", + "\u300e" : "『", + "\u300f" : "』", + } + +# END Class nwQuotes \ No newline at end of file diff --git a/nw/gui/configeditor.py b/nw/gui/configeditor.py new file mode 100644 index 00000000..fa1d63f4 --- /dev/null +++ b/nw/gui/configeditor.py @@ -0,0 +1,492 @@ +# -*- coding: utf-8 -*- +"""novelWriter GUI Config Editor + + novelWriter – GUI Config Editor +================================= + Class holding the config dialog + + File History: + Created: 2019-06-10 [0.1.5] + +""" + +import logging +import enchant +import nw + +from os import path + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush, QStandardItemModel, QFont +from PyQt5.QtSvg import QSvgWidget +from PyQt5.QtWidgets import ( + QDialog, QHBoxLayout, QVBoxLayout, QFormLayout, QLineEdit, QPlainTextEdit, QLabel, + QWidget, QTabWidget, QDialogButtonBox, QSpinBox, QGroupBox, QComboBox, QMessageBox, + QCheckBox, QGridLayout, QFontComboBox +) +from nw.enum import nwAlert +from nw.constants import nwQuotes + +logger = logging.getLogger(__name__) + +class GuiConfigEditor(QDialog): + + def __init__(self, theParent, theProject): + QDialog.__init__(self, theParent) + + logger.debug("Initialising ConfigEditor ...") + + self.mainConf = nw.CONFIG + self.theParent = theParent + self.theProject = theProject + self.outerBox = QHBoxLayout() + self.innerBox = QVBoxLayout() + + self.setWindowTitle("Preferences") + + self.gradPath = path.abspath(path.join(self.mainConf.appPath,"graphics","block.svg")) + self.svgGradient = QSvgWidget(self.gradPath) + self.svgGradient.setFixedWidth(80) + + self.theProject.countStatus() + self.tabMain = GuiConfigEditGeneral(self.theParent) + self.tabEditor = GuiConfigEditEditor(self.theParent) + + self.tabWidget = QTabWidget() + self.tabWidget.addTab(self.tabMain, "General") + self.tabWidget.addTab(self.tabEditor, "Editor") + + self.setLayout(self.outerBox) + self.outerBox.addWidget(self.svgGradient) + self.outerBox.addLayout(self.innerBox) + + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.buttonBox.accepted.connect(self._doSave) + self.buttonBox.rejected.connect(self._doClose) + + self.innerBox.addWidget(self.tabWidget) + self.innerBox.addWidget(self.buttonBox) + + self.show() + + logger.debug("ProjectEditor ConfigEditor complete") + + return + + def _doSave(self): + + logger.verbose("ConfigEditor save button clicked") + + validEntries = True + needsRestart = False + + retA, retB = self.tabMain.saveValues() + validEntries &= retA + needsRestart |= retB + + retA, retB = self.tabEditor.saveValues() + validEntries &= retA + needsRestart |= retB + + if needsRestart: + msgBox = QMessageBox() + msgBox.information( + self, "Preferences", + "Some changes will not be applied until
%s has been restarted." % nw.__package__ + ) + + if validEntries: + self.accept() + + return + + def _doClose(self): + logger.verbose("ConfigEditor close button clicked") + self.close() + return + +# END Class GuiConfigEditor + +class GuiConfigEditGeneral(QWidget): + + def __init__(self, theParent): + QWidget.__init__(self, theParent) + + self.mainConf = nw.CONFIG + self.theParent = theParent + self.outerBox = QGridLayout() + + # User Interface + self.guiLook = QGroupBox("User Interface", self) + self.guiLookForm = QGridLayout(self) + self.guiLook.setLayout(self.guiLookForm) + + self.guiLookTheme = QComboBox() + self.guiLookTheme.setMinimumWidth(200) + self.theThemes = self.theParent.theTheme.listThemes() + for themeDir, themeName in self.theThemes: + self.guiLookTheme.addItem(themeName, themeDir) + themeIdx = self.guiLookTheme.findData(self.mainConf.guiTheme) + if themeIdx != -1: + self.guiLookTheme.setCurrentIndex(themeIdx) + + self.guiLookForm.addWidget(QLabel("Theme"), 0, 0) + self.guiLookForm.addWidget(self.guiLookTheme, 0, 1) + + # Spell Checking + self.spellLang = QGroupBox("Spell Checker", self) + self.spellLangForm = QGridLayout(self) + self.spellLang.setLayout(self.spellLangForm) + + self.spellLangList = QComboBox(self) + for spTag, spProvider in enchant.list_dicts(): + self.spellLangList.addItem("%s [%s]" % (spTag, spProvider.name), spTag) + spellIdx = self.spellLangList.findData(self.mainConf.spellLanguage) + if spellIdx != -1: + self.spellLangList.setCurrentIndex(spellIdx) + + self.spellLangForm.addWidget(QLabel("Language"), 0, 0) + self.spellLangForm.addWidget(self.spellLangList, 0, 1) + self.spellLangForm.setColumnStretch(1, 1) + + # AutoSave + self.autoSave = QGroupBox("Auto-Save", self) + self.autoSaveForm = QGridLayout(self) + self.autoSave.setLayout(self.autoSaveForm) + + self.autoSaveDoc = QSpinBox(self) + self.autoSaveDoc.setMinimum(5) + self.autoSaveDoc.setMaximum(600) + self.autoSaveDoc.setSingleStep(1) + self.autoSaveDoc.setValue(self.mainConf.autoSaveDoc) + + self.autoSaveProj = QSpinBox(self) + self.autoSaveProj.setMinimum(5) + self.autoSaveProj.setMaximum(600) + self.autoSaveProj.setSingleStep(1) + self.autoSaveProj.setValue(self.mainConf.autoSaveProj) + + self.autoSaveForm.addWidget(QLabel("Document"), 0, 0) + self.autoSaveForm.addWidget(self.autoSaveDoc, 0, 1) + self.autoSaveForm.addWidget(QLabel("seconds"), 0, 2) + self.autoSaveForm.addWidget(QLabel("Project"), 1, 0) + self.autoSaveForm.addWidget(self.autoSaveProj, 1, 1) + self.autoSaveForm.addWidget(QLabel("seconds"), 1, 2) + + # Assemble + self.outerBox.addWidget(self.guiLook, 0, 0) + self.outerBox.addWidget(self.spellLang, 1, 0) + self.outerBox.addWidget(self.autoSave, 2, 0) + self.outerBox.setColumnStretch(2, 1) + self.outerBox.setRowStretch(3, 1) + self.setLayout(self.outerBox) + + return + + def saveValues(self): + + validEntries = True + needsRestart = False + + guiTheme = self.guiLookTheme.currentData() + spellLanguage = self.spellLangList.currentData() + autoSaveDoc = self.autoSaveDoc.value() + autoSaveProj = self.autoSaveProj.value() + + # Check if restart is needed + needsRestart |= self.mainConf.guiTheme != guiTheme + + self.mainConf.guiTheme = guiTheme + self.mainConf.spellLanguage = spellLanguage + self.mainConf.autoSaveDoc = autoSaveDoc + self.mainConf.autoSaveProj = autoSaveProj + + self.mainConf.confChanged = True + + return validEntries, needsRestart + +# END Class GuiConfigEditGeneral + +class GuiConfigEditEditor(QWidget): + + def __init__(self, theParent): + QWidget.__init__(self, theParent) + + self.mainConf = nw.CONFIG + self.theParent = theParent + self.outerBox = QGridLayout() + + # Text Style + self.textStyle = QGroupBox("Text Style", self) + self.textStyleForm = QGridLayout(self) + self.textStyle.setLayout(self.textStyleForm) + + self.textStyleFont = QFontComboBox() + self.textStyleFont.setMaximumWidth(250) + self.textStyleFont.setCurrentFont(QFont(self.mainConf.textFont)) + + self.textStyleSize = QSpinBox(self) + self.textStyleSize.setMinimum(5) + self.textStyleSize.setMaximum(120) + self.textStyleSize.setSingleStep(1) + self.textStyleSize.setValue(self.mainConf.textSize) + + self.textStyleForm.addWidget(QLabel("Font Family"), 0, 0) + self.textStyleForm.addWidget(self.textStyleFont, 0, 1) + self.textStyleForm.addWidget(QLabel("Size"), 0, 2) + self.textStyleForm.addWidget(self.textStyleSize, 0, 3) + self.textStyleForm.setColumnStretch(4, 1) + + # Text Flow + self.textFlow = QGroupBox("Text Flow", self) + self.textFlowForm = QGridLayout(self) + self.textFlow.setLayout(self.textFlowForm) + + self.textFlowFixed = QCheckBox(self) + self.textFlowFixed.setToolTip("Make text in editor fixed width and scale margins instead.") + if self.mainConf.textFixedW: + self.textFlowFixed.setCheckState(Qt.Checked) + else: + self.textFlowFixed.setCheckState(Qt.Unchecked) + self.textFlowWidth = QSpinBox(self) + self.textFlowWidth.setMinimum(300) + self.textFlowWidth.setMaximum(10000) + self.textFlowWidth.setSingleStep(10) + self.textFlowWidth.setValue(self.mainConf.textWidth) + + self.textFlowJustify = QCheckBox(self) + self.textFlowJustify.setToolTip("Justify text in main document editor.") + if self.mainConf.doJustify: + self.textFlowJustify.setCheckState(Qt.Checked) + else: + self.textFlowJustify.setCheckState(Qt.Unchecked) + + self.testFlowAutoSelect = QCheckBox(self) + self.testFlowAutoSelect.setToolTip("Auto-select word under cursor when applying formatting.") + if self.mainConf.autoSelect: + self.testFlowAutoSelect.setCheckState(Qt.Checked) + else: + self.testFlowAutoSelect.setCheckState(Qt.Unchecked) + + self.textFlowForm.addWidget(QLabel("Fixed Width"), 0, 0) + self.textFlowForm.addWidget(self.textFlowFixed, 0, 1) + self.textFlowForm.addWidget(self.textFlowWidth, 0, 2) + self.textFlowForm.addWidget(QLabel("px"), 0, 3) + self.textFlowForm.addWidget(QLabel("Justify Text"), 1, 0) + self.textFlowForm.addWidget(self.textFlowJustify, 1, 1) + self.textFlowForm.addWidget(QLabel("Auto-Select Text"), 2, 0) + self.textFlowForm.addWidget(self.testFlowAutoSelect, 2, 1) + self.textFlowForm.setColumnStretch(4, 1) + + # Text Margins + self.textMargin = QGroupBox("Margins", self) + self.textMarginForm = QGridLayout(self) + self.textMargin.setLayout(self.textMarginForm) + + self.textMarginHor = QSpinBox(self) + self.textMarginHor.setMinimum(0) + self.textMarginHor.setMaximum(2000) + self.textMarginHor.setSingleStep(1) + self.textMarginHor.setValue(self.mainConf.textMargin[0]) + + self.textMarginVer = QSpinBox(self) + self.textMarginVer.setMinimum(0) + self.textMarginVer.setMaximum(2000) + self.textMarginVer.setSingleStep(1) + self.textMarginVer.setValue(self.mainConf.textMargin[1]) + + self.textMarginTab = QSpinBox(self) + self.textMarginTab.setMinimum(0) + self.textMarginTab.setMaximum(200) + self.textMarginTab.setSingleStep(1) + self.textMarginTab.setValue(self.mainConf.tabWidth) + + self.textMarginForm.addWidget(QLabel("Horizontal"), 0, 0) + self.textMarginForm.addWidget(self.textMarginHor, 0, 1) + self.textMarginForm.addWidget(QLabel("px"), 0, 2) + self.textMarginForm.addWidget(QLabel("Vertical"), 1, 0) + self.textMarginForm.addWidget(self.textMarginVer, 1, 1) + self.textMarginForm.addWidget(QLabel("px"), 1, 2) + self.textMarginForm.addWidget(QLabel("Tab Width"), 2, 0) + self.textMarginForm.addWidget(self.textMarginTab, 2, 1) + self.textMarginForm.addWidget(QLabel("px"), 2, 2) + self.textMarginForm.setColumnStretch(4, 1) + + # Auto-Replace + self.autoReplace = QGroupBox("Auto-Replace", self) + self.autoReplaceForm = QGridLayout(self) + self.autoReplace.setLayout(self.autoReplaceForm) + + self.autoReplaceMain = QCheckBox(self) + self.autoReplaceMain.setToolTip("Auto-replace text as you type.") + if self.mainConf.doReplace: + self.autoReplaceMain.setCheckState(Qt.Checked) + else: + self.autoReplaceMain.setCheckState(Qt.Unchecked) + + self.autoReplaceSQ = QCheckBox(self) + self.autoReplaceSQ.setToolTip("Auto-replace single quotes.") + if self.mainConf.doReplaceSQuote: + self.autoReplaceSQ.setCheckState(Qt.Checked) + else: + self.autoReplaceSQ.setCheckState(Qt.Unchecked) + + self.autoReplaceSStyleO = QLineEdit() + self.autoReplaceSStyleO.setMaxLength(1) + self.autoReplaceSStyleO.setFixedWidth(30) + self.autoReplaceSStyleO.setAlignment(Qt.AlignCenter) + self.autoReplaceSStyleO.setText(self.mainConf.fmtSingleQuotes[0]) + + self.autoReplaceSStyleC = QLineEdit() + self.autoReplaceSStyleC.setMaxLength(1) + self.autoReplaceSStyleC.setFixedWidth(30) + self.autoReplaceSStyleC.setAlignment(Qt.AlignCenter) + self.autoReplaceSStyleC.setText(self.mainConf.fmtSingleQuotes[1]) + + self.autoReplaceDQ = QCheckBox(self) + self.autoReplaceDQ.setToolTip("Auto-replace double quotes.") + if self.mainConf.doReplaceDQuote: + self.autoReplaceDQ.setCheckState(Qt.Checked) + else: + self.autoReplaceDQ.setCheckState(Qt.Unchecked) + + self.autoReplaceDStyleO = QLineEdit() + self.autoReplaceDStyleO.setMaxLength(1) + self.autoReplaceDStyleO.setFixedWidth(30) + self.autoReplaceDStyleO.setAlignment(Qt.AlignCenter) + self.autoReplaceDStyleO.setText(self.mainConf.fmtDoubleQuotes[0]) + + self.autoReplaceDStyleC = QLineEdit() + self.autoReplaceDStyleC.setMaxLength(1) + self.autoReplaceDStyleC.setFixedWidth(30) + self.autoReplaceDStyleC.setAlignment(Qt.AlignCenter) + self.autoReplaceDStyleC.setText(self.mainConf.fmtDoubleQuotes[1]) + + self.autoReplaceDash = QCheckBox(self) + self.autoReplaceDash.setToolTip("Auto-replace double and triple hyphens with short and long dash.") + if self.mainConf.doReplaceDash: + self.autoReplaceDash.setCheckState(Qt.Checked) + else: + self.autoReplaceDash.setCheckState(Qt.Unchecked) + + self.autoReplaceDots = QCheckBox(self) + self.autoReplaceDots.setToolTip("Auto-replace three dots with ellipsis.") + if self.mainConf.doReplaceDots: + self.autoReplaceDots.setCheckState(Qt.Checked) + else: + self.autoReplaceDots.setCheckState(Qt.Unchecked) + + self.autoReplaceForm.addWidget(QLabel("Enable Feature"), 0, 0) + self.autoReplaceForm.addWidget(self.autoReplaceMain, 0, 1) + self.autoReplaceForm.addWidget(QLabel("Single Quotes"), 1, 0) + self.autoReplaceForm.addWidget(self.autoReplaceSQ, 1, 1) + self.autoReplaceForm.addWidget(QLabel("Open"), 1, 2) + self.autoReplaceForm.addWidget(self.autoReplaceSStyleO, 1, 3) + self.autoReplaceForm.addWidget(QLabel("Close"), 1, 4) + self.autoReplaceForm.addWidget(self.autoReplaceSStyleC, 1, 5) + self.autoReplaceForm.addWidget(QLabel("Double Quotes"), 2, 0) + self.autoReplaceForm.addWidget(self.autoReplaceDQ, 2, 1) + self.autoReplaceForm.addWidget(QLabel("Open"), 2, 2) + self.autoReplaceForm.addWidget(self.autoReplaceDStyleO, 2, 3) + self.autoReplaceForm.addWidget(QLabel("Close"), 2, 4) + self.autoReplaceForm.addWidget(self.autoReplaceDStyleC, 2, 5) + self.autoReplaceForm.addWidget(QLabel("Hyphens with Dash"), 3, 0) + self.autoReplaceForm.addWidget(self.autoReplaceDash, 3, 1) + self.autoReplaceForm.addWidget(QLabel("Dots with Ellipsis"), 4, 0) + self.autoReplaceForm.addWidget(self.autoReplaceDots, 4, 1) + self.autoReplaceForm.setColumnStretch(6, 1) + + # Assemble + self.outerBox.addWidget(self.textStyle, 0, 0, 1, 2) + self.outerBox.addWidget(self.textFlow, 1, 0) + self.outerBox.addWidget(self.textMargin, 1, 1) + self.outerBox.addWidget(self.autoReplace, 2, 0, 1, 2) + self.outerBox.setColumnStretch(2, 1) + self.setLayout(self.outerBox) + + return + + def saveValues(self): + + validEntries = True + + textFont = self.textStyleFont.currentFont().family() + textSize = self.textStyleSize.value() + + self.mainConf.textFont = textFont + self.mainConf.textSize = textSize + + textWidth = self.textFlowWidth.value() + textFixedW = self.textFlowFixed.isChecked() + doJustify = self.textFlowJustify.isChecked() + autoSelect = self.testFlowAutoSelect.isChecked() + + self.mainConf.textWidth = textWidth + self.mainConf.textFixedW = textFixedW + self.mainConf.doJustify = doJustify + self.mainConf.autoSelect = autoSelect + + textMarginH = self.textMarginHor.value() + textMarginV = self.textMarginVer.value() + tabWidth = self.textMarginTab.value() + + self.mainConf.textMargin[0] = textMarginH + self.mainConf.textMargin[1] = textMarginV + self.mainConf.tabWidth = tabWidth + + doReplace = self.autoReplaceMain.isChecked() + doReplaceSQuote = self.autoReplaceSQ.isChecked() + doReplaceDQuote = self.autoReplaceDQ.isChecked() + doReplaceDash = self.autoReplaceDash.isChecked() + doReplaceDots = self.autoReplaceDash.isChecked() + fmtSingleQuotesO = self.autoReplaceSStyleO.text() + fmtSingleQuotesC = self.autoReplaceSStyleC.text() + fmtDoubleQuotesO = self.autoReplaceDStyleO.text() + fmtDoubleQuotesC = self.autoReplaceDStyleC.text() + + self.mainConf.doReplace = doReplace + self.mainConf.doReplaceSQuote = doReplaceSQuote + self.mainConf.doReplaceDQuote = doReplaceDQuote + self.mainConf.doReplaceDash = doReplaceDash + self.mainConf.doReplaceDots = doReplaceDots + + if self._checkQuoteSymbol(fmtSingleQuotesO): + self.mainConf.fmtSingleQuotes[0] = fmtSingleQuotesO + else: + self.theParent.makeAlert("Invalid quote symbol: %s" % fmtSingleQuotesO, nwAlert.ERROR) + validEntries = False + + if self._checkQuoteSymbol(fmtSingleQuotesC): + self.mainConf.fmtSingleQuotes[1] = fmtSingleQuotesC + else: + self.theParent.makeAlert("Invalid quote symbol: %s" % fmtSingleQuotesC, nwAlert.ERROR) + validEntries = False + + if self._checkQuoteSymbol(fmtDoubleQuotesO): + self.mainConf.fmtDoubleQuotes[0] = fmtDoubleQuotesO + else: + self.theParent.makeAlert("Invalid quote symbol: %s" % fmtDoubleQuotesO, nwAlert.ERROR) + validEntries = False + + if self._checkQuoteSymbol(fmtDoubleQuotesC): + self.mainConf.fmtDoubleQuotes[1] = fmtDoubleQuotesC + else: + self.theParent.makeAlert("Invalid quote symbol: %s" % fmtDoubleQuotesC, nwAlert.ERROR) + validEntries = False + + self.mainConf.confChanged = True + + return validEntries, False + + ## + # Internal Functions + ## + + def _checkQuoteSymbol(self, toCheck): + if len(toCheck) != 1: + return False + if toCheck in nwQuotes.SYMBOLS: + return True + return False + +# END Class GuiConfigEditEditor diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index b955acb1..9b36790a 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -18,7 +18,7 @@ from time import time from PyQt5.QtCore import Qt, QTimer from PyQt5.QtWidgets import QTextEdit, QAction, QMenu, QShortcut -from PyQt5.QtGui import QTextCursor, QTextOption, QIcon, QKeySequence +from PyQt5.QtGui import QTextCursor, QTextOption, QIcon, QKeySequence, QFont from nw.project.document import NWDoc from nw.gui.dochighlight import GuiDocHighlighter @@ -55,7 +55,6 @@ class GuiDocEditor(QTextEdit): self.typDQClose = self.mainConf.fmtDoubleQuotes[1] self.typSQOpen = self.mainConf.fmtSingleQuotes[0] self.typSQClose = self.mainConf.fmtSingleQuotes[1] - self.typApos = self.mainConf.fmtApostrophe # Core Elements self.theQDoc = self.document() @@ -113,7 +112,21 @@ class GuiDocEditor(QTextEdit): return True def initEditor(self): + """Initialise or re-initialise the editor with the user's settings. + This function is both called when the editor is created, and when the user changes the + main editor preferences. + """ + + # Set Font + if self.mainConf.textFont is not None: + self.setFontFamily(self.mainConf.textFont) + else: + # If none is defined, set the default back to config + theFont = self.theQDoc.defaultFont().family() + self.mainConf.textFont = theFont self.setFontPointSize(self.mainConf.textSize) + + # Set text fixed width, or alternatively, just margins if self.mainConf.textFixedW: self.setLineWrapMode(QTextEdit.FixedPixelWidth) self.setLineWrapColumnOrWidth(self.mainConf.textWidth) @@ -121,12 +134,22 @@ class GuiDocEditor(QTextEdit): mTB = self.mainConf.textMargin[0] mLR = self.mainConf.textMargin[1] self.setViewportMargins(mLR,mTB,mLR,mTB) + + # Also set the document text options for the document text flow theOpt = QTextOption() if self.mainConf.tabWidth is not None: theOpt.setTabStopDistance(self.mainConf.tabWidth) if self.mainConf.doJustify: theOpt.setAlignment(Qt.AlignJustify) self.theQDoc.setDefaultTextOption(theOpt) + + # If we have a document open, we should reload it in case the font changed + if self.theHandle is not None: + tHandle = self.theHandle + self.hLight.initHighlighter() + self.clearEditor() + self.loadText(tHandle) + return True def loadText(self, tHandle): diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 848f60a3..55ae8b05 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -32,7 +32,9 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.theDict = None self.theHandle = None self.spellCheck = False + self.spellRx = None self.hRules = [] + self.hStyles = {} self.colHead = QColor(*self.theTheme.colHead) self.colHeadH = QColor(*self.theTheme.colHeadH) @@ -47,6 +49,14 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.colTagErr = QColor(*self.theTheme.colTagErr) self.colRepTag = QColor(*self.theTheme.colRepTag) + self.initHighlighter() + + logger.debug("DocHighlighter initialisation complete") + + return + + def initHighlighter(self): + self.hStyles = { "header1" : self._makeFormat(self.colHead, "bold",1.8), "header2" : self._makeFormat(self.colHead, "bold",1.6), @@ -160,9 +170,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.rules = [(QRegularExpression(a),b) for (a,b) in self.hRules] self.spellRx = QRegularExpression(r"\b[^\s]+\b") - logger.debug("DocHighlighter initialisation complete") - - return + return True ## # Setters diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index e170b408..5c91f71c 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -492,10 +492,12 @@ class GuiMainMenu(QMenuBar): menuItem.triggered.connect(self.theParent.rebuildIndex) self.toolsMenu.addAction(menuItem) - # # Tools > Settings - # menuItem = QAction(QIcon.fromTheme("preferences-system"), "Preferences", self) - # menuItem.setStatusTip("Preferences") - # self.toolsMenu.addAction(menuItem) + # Tools > Settings + menuItem = QAction(QIcon.fromTheme("preferences-system"), "Preferences", self) + menuItem.setStatusTip("Preferences") + menuItem.setShortcut("Ctrl+,") + menuItem.triggered.connect(self.theParent.editConfigDialog) + self.toolsMenu.addAction(menuItem) return diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index 4d17b36e..0c8e8d76 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -19,7 +19,7 @@ from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QIcon, QPixmap, QColor from PyQt5.QtWidgets import ( qApp, QWidget, QMainWindow, QVBoxLayout, QFrame, QSplitter, QFileDialog, - QShortcut, QMessageBox, QProgressDialog + QShortcut, QMessageBox, QProgressDialog, QDialog ) from nw.gui.doctree import GuiDocTree @@ -27,6 +27,7 @@ from nw.gui.doceditor import GuiDocEditor from nw.gui.docviewer import GuiDocViewer from nw.gui.docdetails import GuiDocDetails from nw.gui.mainmenu import GuiMainMenu +from nw.gui.configeditor import GuiConfigEditor from nw.gui.projecteditor import GuiProjectEditor from nw.gui.itemeditor import GuiItemEditor from nw.gui.statusbar import GuiMainStatus @@ -49,7 +50,7 @@ class GuiMain(QMainWindow): logger.debug("Initialising GUI ...") self.mainConf = nw.CONFIG - self.theTheme = Theme() + self.theTheme = Theme(self) self.theProject = NWProject(self) self.theIndex = NWIndex(self.theProject, self) self.hasProject = False @@ -118,15 +119,11 @@ class GuiMain(QMainWindow): # Set Up Autosaving Project Timer self.asProjTimer = QTimer() - self.asProjTimer.setInterval(int(self.mainConf.autoSaveProj*1000)) self.asProjTimer.timeout.connect(self._autoSaveProject) - self.asProjTimer.start() # Set Up Autosaving Document Timer self.asDocTimer = QTimer() - self.asDocTimer.setInterval(int(self.mainConf.autoSaveDoc*1000)) self.asDocTimer.timeout.connect(self._autoSaveDocument) - self.asDocTimer.start() # Keyboard Shortcuts QShortcut(Qt.Key_Return, self.treeView, context=Qt.WidgetShortcut, activated=self._treeKeyPressReturn) @@ -138,6 +135,10 @@ class GuiMain(QMainWindow): if self.mainConf.showGUI: self.show() + self.initMain() + self.asProjTimer.start() + self.asDocTimer.start() + logger.debug("GUI initialisation complete") return @@ -149,6 +150,11 @@ class GuiMain(QMainWindow): self.statusBar.clearStatus() return True + def initMain(self): + self.asProjTimer.setInterval(int(self.mainConf.autoSaveProj*1000)) + self.asDocTimer.setInterval(int(self.mainConf.autoSaveDoc*1000)) + return True + ## # Project Actions ## @@ -454,6 +460,14 @@ class GuiMain(QMainWindow): return projPath return None + def editConfigDialog(self): + dlgConf = GuiConfigEditor(self, self.theProject) + if dlgConf.exec_() == QDialog.Accepted: + logger.debug("Applying new preferences") + self.initMain() + self.docEditor.initEditor() + return True + def editProjectDialog(self): if self.hasProject: dlgProj = GuiProjectEditor(self, self.theProject) diff --git a/nw/project/project.py b/nw/project/project.py index 81979f2e..d4035aa2 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -373,8 +373,9 @@ class NWProject(): return True def setSpellCheck(self, theMode): - self.spellCheck = theMode - self.setProjectChanged(True) + if self.spellCheck != theMode: + self.spellCheck = theMode + self.setProjectChanged(True) return True def setTreeOrder(self, newOrder): @@ -385,18 +386,21 @@ class NWProject(): return True def setLastEdited(self, tHandle): - self.lastEdited = tHandle - self.setProjectChanged(True) + if self.lastEdited != tHandle: + self.lastEdited = tHandle + self.setProjectChanged(True) return True def setLastViewed(self, tHandle): - self.lastViewed = tHandle - self.setProjectChanged(True) + if self.lastViewed != tHandle: + self.lastViewed = tHandle + self.setProjectChanged(True) return True def setProjectWordCount(self, theCount): - self.currWCount = theCount - self.setProjectChanged(True) + if self.currWCount != theCount: + self.currWCount = theCount + self.setProjectChanged(True) return True def getSessionWordCount(self): diff --git a/nw/theme.py b/nw/theme.py index 806e7b46..f0a8db3c 100644 --- a/nw/theme.py +++ b/nw/theme.py @@ -14,19 +14,30 @@ import logging import configparser import nw -from os import path +from os import path, listdir + +from nw.enum import nwAlert logger = logging.getLogger(__name__) class Theme: - def __init__(self): + def __init__(self, theParent): self.mainConf = nw.CONFIG + self.theParent = theParent self.cssName = "styles.css" self.confName = "theme.conf" + self.themeList = [] # Loaded Theme Settings + + ## Main + self.themeName = "No Name" + + ## Syntax + self.colText = [0,0,0] + self.colLink = [0,0,0] self.colHead = [0,0,0] self.colHeadH = [0,0,0] self.colEmph = [0,0,0] @@ -83,6 +94,11 @@ class Theme: logger.error("Could not load theme config file") return False + ## Main + cnfSec = "Main" + if confParser.has_section(cnfSec): + self.themeName = confParser.get(cnfSec,"name") + ## Syntax cnfSec = "Syntax" if confParser.has_section(cnfSec): @@ -103,6 +119,29 @@ class Theme: return True + def listThemes(self): + + if len(self.themeList) > 0: + return self.themeList + + confParser = configparser.ConfigParser() + for themeDir in listdir(self.mainConf.themeRoot): + themeConf = path.join(self.mainConf.themeRoot, themeDir, self.confName) + logger.verbose("Checking theme config for '%s'" % themeDir) + try: + confParser.read_file(open(themeConf)) + except Exception as e: + self.theParent.makeAlert(["Could not load theme config file",str(e)],nwAlert.ERROR) + return [] + themeName = "" + if confParser.has_section("Main"): + themeName = confParser.get("Main","name") + logger.verbose("Theme name is '%s'" % themeName) + if themeName != "": + self.themeList.append((themeDir, themeName)) + + return self.themeList + ## # Internal Functions ## diff --git a/nw/themes/default/styles.css b/nw/themes/default_dark/styles.css similarity index 87% rename from nw/themes/default/styles.css rename to nw/themes/default_dark/styles.css index ddf008ae..c81aecf3 100644 --- a/nw/themes/default/styles.css +++ b/nw/themes/default_dark/styles.css @@ -1,3 +1,7 @@ +/** + * Default Theme: Dark + */ + * { background-color: #999999; } diff --git a/nw/themes/default/theme.conf b/nw/themes/default_dark/theme.conf similarity index 92% rename from nw/themes/default/theme.conf rename to nw/themes/default_dark/theme.conf index 7f4729e8..86b04b45 100644 --- a/nw/themes/default/theme.conf +++ b/nw/themes/default_dark/theme.conf @@ -1,3 +1,6 @@ +[Main] +name = Default Dark + [Syntax] text = 199, 207, 208 link = 184, 200, 0 diff --git a/nw/themes/default_light/styles.css b/nw/themes/default_light/styles.css new file mode 100644 index 00000000..90ef623f --- /dev/null +++ b/nw/themes/default_light/styles.css @@ -0,0 +1,7 @@ +/** + * Default Theme: Light + */ + +QTreeView, QHeaderView { + font-size: 13px; +} diff --git a/nw/themes/default_light/theme.conf b/nw/themes/default_light/theme.conf new file mode 100644 index 00000000..b3d0d5be --- /dev/null +++ b/nw/themes/default_light/theme.conf @@ -0,0 +1,18 @@ +[Main] +name = Default Light + +[Syntax] +text = 0, 0, 0 +link = 0, 0, 0 +headertext = 0, 0, 0 +headertag = 0, 0, 0 +emphasis = 0, 0, 0 +straightquotes = 0, 0, 0 +doublequotes = 0, 0, 0 +singlequotes = 0, 0, 0 +hidden = 50, 50, 50 +keyword = 0, 0, 0 +value = 0, 0, 0 +spellcheckline = 0, 0, 0 +tagerror = 0, 0, 0 +replacetag = 0, 0, 0 diff --git a/sample/sampleNovel/meta/sessionInfo.log b/sample/sampleNovel/meta/sessionInfo.log index 4111f856..1defe611 100644 --- a/sample/sampleNovel/meta/sessionInfo.log +++ b/sample/sampleNovel/meta/sessionInfo.log @@ -187,3 +187,26 @@ Start: 2019-06-08 20:30:24 End: 2019-06-08 20:42:10 Words: 9 Start: 2019-06-08 20:42:14 End: 2019-06-08 20:42:37 Words: 0 Start: 2019-06-08 20:42:41 End: 2019-06-08 20:43:25 Words: 0 Start: 2019-06-08 20:43:29 End: 2019-06-08 20:43:42 Words: 0 +Start: 2019-06-10 13:26:52 End: 2019-06-10 13:30:22 Words: 0 +Start: 2019-06-10 13:30:28 End: 2019-06-10 13:31:44 Words: 0 +Start: 2019-06-10 14:17:18 End: 2019-06-10 14:17:48 Words: 0 +Start: 2019-06-10 14:20:17 End: 2019-06-10 14:21:11 Words: 0 +Start: 2019-06-10 20:41:35 End: 2019-06-10 20:41:48 Words: 0 +Start: 2019-06-10 20:42:15 End: 2019-06-10 20:42:27 Words: 0 +Start: 2019-06-10 20:42:43 End: 2019-06-10 20:42:47 Words: 0 +Start: 2019-06-10 20:45:02 End: 2019-06-10 20:45:07 Words: 0 +Start: 2019-06-10 20:45:44 End: 2019-06-10 20:46:03 Words: 0 +Start: 2019-06-10 20:46:08 End: 2019-06-10 20:47:15 Words: 0 +Start: 2019-06-10 20:47:24 End: 2019-06-10 20:47:28 Words: 0 +Start: 2019-06-10 20:47:46 End: 2019-06-10 20:47:59 Words: 0 +Start: 2019-06-10 20:48:02 End: 2019-06-10 20:48:08 Words: 0 +Start: 2019-06-10 20:57:47 End: 2019-06-10 20:58:01 Words: 0 +Start: 2019-06-10 21:29:47 End: 2019-06-10 21:33:22 Words: 0 +Start: 2019-06-10 21:33:26 End: 2019-06-10 21:34:59 Words: 0 +Start: 2019-06-10 21:39:54 End: 2019-06-10 21:45:25 Words: 0 +Start: 2019-06-10 21:45:29 End: 2019-06-10 21:47:40 Words: 0 +Start: 2019-06-10 21:48:06 End: 2019-06-10 21:48:47 Words: 0 +Start: 2019-06-10 21:48:51 End: 2019-06-10 21:49:07 Words: 0 +Start: 2019-06-10 22:17:58 End: 2019-06-10 22:18:44 Words: 0 +Start: 2019-06-10 22:46:35 End: 2019-06-10 22:49:34 Words: 0 +Start: 2019-06-11 14:44:55 End: 2019-06-11 14:53:47 Words: 0 diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 7fc31a8e..b7bbbada 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -80,7 +80,7 @@ 736 137 6 - 725 + 738 New File diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf index a7df1756..7c827a0c 100644 --- a/tests/reference/novelwriter.conf +++ b/tests/reference/novelwriter.conf @@ -1,6 +1,6 @@ [Main] -timestamp = 2019-05-25 23:33:23 -theme = default +timestamp = 2019-06-11 18:39:59 +theme = default_dark [Sizes] geometry = 1100, 650 @@ -14,10 +14,11 @@ autosaveproject = 60 autosavedoc = 30 [Editor] +textfont = None +textsize = 12 fixedwidth = True width = 600 margins = 40, 40 -textsize = 13 tabwidth = 40 justify = True autoselect = True @@ -26,6 +27,8 @@ repsquotes = True repdquotes = True repdash = True repdots = True +fmtsinglequote = ‘, ’ +fmtdoublequote = “, ” spellcheck = en_GB [Path]