diff --git a/nw/core/project.py b/nw/core/project.py
index be62ffae..43efa057 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -6,12 +6,14 @@
Class wrapping the data if a novelWriter project
File History:
- Created: 2018-09-29 [0.0.1] NWProject
- Created: 2018-10-27 [0.0.1] NWItem
- Created: 2019-05-19 [0.1.3] NWStatus
- Merged: 2020-05-07 [0.4.5] Moved NWItem class to this file
- Merged: 2020-05-07 [0.4.5] Moved NWStatus class to this file
- Added: 2020-05-07 [0.4.5] NWTree
+ Created: 2018-09-29 [0.0.1] NWProject
+ Created: 2018-10-27 [0.0.1] NWItem
+ Created: 2019-05-19 [0.1.3] NWStatus
+ Created: 2019-10-21 [0.3.1] OptionState
+ Merged: 2020-05-07 [0.4.5] Moved NWItem class to this file
+ Merged: 2020-05-07 [0.4.5] Moved NWStatus class to this file
+ Added: 2020-05-07 [0.4.5] NWTree
+ Rewritten: 2020-02-19 [0.4.5] OptionState
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
@@ -42,7 +44,6 @@ from shutil import make_archive
from PyQt5.QtWidgets import QMessageBox
-from nw.gui.tools import OptionState
from nw.core.document import NWDoc
from nw.common import checkString, checkBool, checkInt, formatTimeStamp
from nw.constants import (
@@ -2007,3 +2008,151 @@ class NWStatus():
raise StopIteration
# END Class NWStatus
+
+# =============================================================================================== #
+# OptionState
+# Save the project-wise state of options that don't go into project XML or main config
+# =============================================================================================== #
+
+class OptionState():
+
+ def __init__(self, theProject):
+
+ self.theProject = theProject
+ self.theState = {}
+ self.stringOpt = ()
+ self.boolOpt = ()
+ self.intOpt = ()
+
+ return
+
+ def loadSettings(self):
+ """Load the options dictionary from the project settings file.
+ """
+ if self.theProject.projMeta is None:
+ return False
+
+ stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
+ theState = {}
+
+ if path.isfile(stateFile):
+ logger.debug("Loading GUI options file")
+ try:
+ with open(stateFile,mode="r",encoding="utf8") as inFile:
+ theJson = inFile.read()
+ theState = json.loads(theJson)
+ except Exception as e:
+ logger.error("Failed to load GUI options file")
+ logger.error(str(e))
+ return False
+ for anOpt in theState:
+ self.theState[anOpt] = theState[anOpt]
+
+ return True
+
+ def saveSettings(self):
+ """Save the options dictionary to the project settings file.
+ """
+ if self.theProject.projMeta is None:
+ return False
+
+ stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
+ logger.debug("Saving GUI options file")
+
+ try:
+ with open(stateFile,mode="w+",encoding="utf8") as outFile:
+ outFile.write(json.dumps(self.theState, indent=2))
+ except Exception as e:
+ logger.error("Failed to save GUI options file")
+ logger.error(str(e))
+ return False
+
+ return True
+
+ def setValue(self, setGroup, setName, setValue):
+ """Saves a value, with a given group and name.
+ """
+ if not setGroup in self.theState:
+ self.theState[setGroup] = {}
+ self.theState[setGroup][setName] = setValue
+ return True
+
+ def getValue(self, getGroup, getName, defaultValue):
+ """Return an arbitrary type value, if it exists. Otherwise,
+ return the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return self.theState[getGroup][getName]
+ except:
+ return defaultValue
+ return defaultValue
+
+ def getString(self, getGroup, getName, defaultValue):
+ """Return the value as a string, if it exists. Otherwise, return
+ the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return str(self.theState[getGroup][getName])
+ except:
+ return defaultValue
+ return defaultValue
+
+ def getInt(self, getGroup, getName, defaultValue):
+ """Return the value as an int, if it exists. Otherwise, return
+ the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return int(self.theState[getGroup][getName])
+ except:
+ return defaultValue
+ return defaultValue
+
+ def getFloat(self, getGroup, getName, defaultValue):
+ """Return the value as a float, if it exists. Otherwise, return
+ the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return float(self.theState[getGroup][getName])
+ except:
+ return defaultValue
+ return defaultValue
+
+ def getBool(self, getGroup, getName, defaultValue):
+ """Return the value as a bool, if it exists. Otherwise, return
+ the default value.
+ """
+ if getGroup in self.theState:
+ if getName in self.theState[getGroup]:
+ try:
+ return bool(self.theState[getGroup][getName])
+ except:
+ return defaultValue
+ return defaultValue
+
+ def validIntRange(self, theValue, intA, intB, intDefault):
+ """Check that an int is in a given range. If it isn't, return
+ the default value.
+ """
+ if isinstance(theValue, int):
+ if theValue >= intA and theValue <= intB:
+ return theValue
+ return intDefault
+
+ def validIntTuple(self, theValue, theTuple, intDefault):
+ """Check that an int is an element of a tuple. If it isn't,
+ return the default value.
+ """
+ if isinstance(theValue, int):
+ if theValue in theTuple:
+ return theValue
+ return intDefault
+
+# END Class OptionState
diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py
index 8360a460..28a7a654 100644
--- a/nw/gui/__init__.py
+++ b/nw/gui/__init__.py
@@ -1,62 +1,49 @@
# -*- coding: utf-8 -*-
-# Main Window Elements
+from nw.gui.about import GuiAbout
from nw.gui.build import GuiBuildNovel
-from nw.gui.icons import GuiIcons
+from nw.gui.docbars import GuiDocTitleBar
+from nw.gui.docbars import GuiNoticeBar
+from nw.gui.docbars import GuiSearchBar
+from nw.gui.docdetails import GuiDocDetails
+from nw.gui.doceditor import GuiDocEditor
+from nw.gui.docmerge import GuiDocMerge
+from nw.gui.docsplit import GuiDocSplit
+from nw.gui.doctree import GuiDocTree
+from nw.gui.docviewer import GuiDocViewer
+from nw.gui.itemeditor import GuiItemEditor
from nw.gui.mainmenu import GuiMainMenu
+from nw.gui.outline import GuiProjectOutline
+from nw.gui.preferences import GuiPreferences
+from nw.gui.projectload import GuiProjectLoad
+from nw.gui.projectsettings import GuiProjectSettings
+from nw.gui.sessionlog import GuiSessionLogView
from nw.gui.statusbar import GuiMainStatus
+from nw.gui.theme import GuiIcons
from nw.gui.theme import GuiTheme
-
-# Dialogs
-from nw.gui.dialogs.about import GuiAbout
-from nw.gui.dialogs.preferences import GuiPreferences
-from nw.gui.dialogs.docmerge import GuiDocMerge
-from nw.gui.dialogs.docsplit import GuiDocSplit
-from nw.gui.dialogs.itemeditor import GuiItemEditor
-from nw.gui.dialogs.projectsettings import GuiProjectSettings
-from nw.gui.dialogs.projectload import GuiProjectLoad
-from nw.gui.dialogs.sessionlog import GuiSessionLogView
-
-# GUI Elements
-from nw.gui.elements.docdetails import GuiDocDetails
-from nw.gui.elements.doceditor import GuiDocEditor
-from nw.gui.elements.doctitlebar import GuiDocTitleBar
-from nw.gui.elements.doctree import GuiDocTree
-from nw.gui.elements.docviewer import GuiDocViewer
-from nw.gui.elements.noticebar import GuiNoticeBar
-from nw.gui.elements.outline import GuiProjectOutline
-from nw.gui.elements.searchbar import GuiSearchBar
-from nw.gui.elements.viewdetails import GuiDocViewDetails
-
-# Tools
-from nw.gui.tools.dochighlight import GuiDocHighlighter
-from nw.gui.tools.optionstate import OptionState
-from nw.gui.tools.wordcounter import WordCounter
+from nw.gui.viewdetails import GuiDocViewDetails
__all__ = [
- "GuiBuildNovel",
- "GuiIcons",
- "GuiMainMenu",
- "GuiMainStatus",
- "GuiTheme",
"GuiAbout",
- "GuiPreferences",
- "GuiDocMerge",
- "GuiDocSplit",
- "GuiItemEditor",
- "GuiProjectSettings",
- "GuiProjectLoad",
- "GuiSessionLogView",
+ "GuiBuildNovel",
+ "GuiDocTitleBar",
+ "GuiNoticeBar",
+ "GuiSearchBar",
"GuiDocDetails",
"GuiDocEditor",
- "GuiDocTitleBar",
+ "GuiDocMerge",
+ "GuiDocSplit",
"GuiDocTree",
"GuiDocViewer",
- "GuiNoticeBar",
+ "GuiItemEditor",
+ "GuiMainMenu",
"GuiProjectOutline",
- "GuiSearchBar",
+ "GuiPreferences",
+ "GuiProjectLoad",
+ "GuiProjectSettings",
+ "GuiSessionLogView",
+ "GuiMainStatus",
+ "GuiIcons",
+ "GuiTheme",
"GuiDocViewDetails",
- "GuiDocHighlighter",
- "OptionState",
- "WordCounter",
]
diff --git a/nw/gui/dialogs/about.py b/nw/gui/about.py
similarity index 100%
rename from nw/gui/dialogs/about.py
rename to nw/gui/about.py
diff --git a/nw/gui/additions/__init__.py b/nw/gui/additions/__init__.py
deleted file mode 100644
index dbacd16e..00000000
--- a/nw/gui/additions/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# -*- coding: utf-8 -*-
-from nw.gui.additions.pageddialog import PagedDialog
-from nw.gui.additions.qconfiglayout import QConfigLayout
-from nw.gui.additions.qconfiglayout import QHelpLabel
-from nw.gui.additions.qswitch import QSwitch
-
-__all__ = [
- "PagedDialog",
- "QConfigLayout",
- "QHelpLabel",
- "QSwitch",
-]
diff --git a/nw/gui/additions/pageddialog.py b/nw/gui/additions/pageddialog.py
deleted file mode 100644
index 2b65cbb9..00000000
--- a/nw/gui/additions/pageddialog.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# -*- coding: utf-8 -*-
-"""novelWriter Paged Dialog
-
- novelWriter – Paged Dialog
-============================
- A custom QDialog with a built-in QTabWidget and vertical tabs.
-
- File History:
- Created: 2020-05-17 [0.5.1]
-
- This file is a part of novelWriter
- Copyright 2020, Veronica Berglyd Olsen
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful, but
- WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-"""
-
-import logging
-import nw
-
-from PyQt5.QtCore import Qt, QRect, QPoint
-from PyQt5.QtWidgets import (
- QDialog, QHBoxLayout, QVBoxLayout, QTabWidget, QTabBar, QStyle,
- QStylePainter, QStyleOptionTab
-)
-
-logger = logging.getLogger(__name__)
-
-class PagedDialog(QDialog):
-
- def __init__(self, theParent=None):
- QDialog.__init__(self, parent=theParent)
-
- self._outerBox = QVBoxLayout()
- self._buttonBox = QHBoxLayout()
- self._tabBox = QTabWidget()
-
- self._tabBar = VerticalTabBar(self)
- self._tabBox.setTabBar(self._tabBar)
- self._tabBox.setTabPosition(QTabWidget.West)
- self._tabBar.setExpanding(False)
-
- self._outerBox.addWidget(self._tabBox)
- self._outerBox.addLayout(self._buttonBox)
- self.setLayout(self._outerBox)
-
- # Default Margins
- qM = self._outerBox.contentsMargins()
- mL = qM.left()
- mR = qM.right()
- mT = qM.top()
- mB = qM.bottom()
-
- self.setContentsMargins(0, 0, 0, 0)
- self._outerBox.setContentsMargins(0, 0, 0, mB)
- self._buttonBox.setContentsMargins(mL, 0, mR, 0)
- self._outerBox.setSpacing(mT)
-
- return
-
- def addTab(self, tabWidget, tabLabel):
- """Forwards the adding of tabs to the QTabWidget.
- """
- self._tabBox.addTab(tabWidget, tabLabel)
- return
-
- def addControls(self, buttonBar):
- """Adds a button bar to the dialog.
- """
- self._buttonBox.addWidget(buttonBar)
- return
-
-# END Class PagedDialog
-
-class VerticalTabBar(QTabBar):
-
- def __init__(self, theParent=None):
- QTabBar.__init__(self, parent=theParent)
- return
-
- def tabSizeHint(self, theIndex):
- """Returns a transposed size hint for the rotated bar.
- """
- tSize = QTabBar.tabSizeHint(self, theIndex)
- tSize.transpose()
- return tSize
-
- def paintEvent(self, theEvent):
- """Custom implementation of the label painter that rotates the
- label 90 degrees.
- """
- pObj = QStylePainter(self)
- oObj = QStyleOptionTab()
-
- for i in range(self.count()):
- self.initStyleOption(oObj, i)
- pObj.drawControl(QStyle.CE_TabBarTabShape, oObj)
- pObj.save()
-
- oSize = oObj.rect.size()
- oSize.transpose()
- oRect = QRect(QPoint(), oSize)
- oRect.moveCenter(oObj.rect.center())
- oObj.rect = oRect
-
- oCenter = self.tabRect(i).center()
- pObj.translate(oCenter)
- pObj.rotate(90)
- pObj.translate(-oCenter)
- pObj.drawControl(QStyle.CE_TabBarTabLabel, oObj)
- pObj.restore()
-
- return
-
-# END Class VerticalTabBar
diff --git a/nw/gui/additions/qconfiglayout.py b/nw/gui/additions/qconfiglayout.py
deleted file mode 100644
index 70b3042d..00000000
--- a/nw/gui/additions/qconfiglayout.py
+++ /dev/null
@@ -1,200 +0,0 @@
-# -*- coding: utf-8 -*-
-"""novelWriter Addition QConfigLayout
-
- novelWriter – Addition QConfigLayout
-======================================
- A custom Qt grid layout for config forms similar to QFormLayout
-
- File History:
- Created: 2020-05-03 [0.4.5]
-
- This file is a part of novelWriter
- Copyright 2020, Veronica Berglyd Olsen
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful, but
- WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-"""
-
-import logging
-import nw
-
-from PyQt5.QtCore import Qt
-from PyQt5.QtGui import QColor, QPalette
-from PyQt5.QtWidgets import (
- QGridLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout
-)
-
-from nw.constants import nwUnicode
-
-logger = logging.getLogger(__name__)
-
-class QConfigLayout(QGridLayout):
-
- def __init__(self):
- super().__init__()
-
- self._nextRow = 0
- self._helpCol = QColor(0, 0, 0)
- self._fontScale = 0.9
-
- self._itemMap = {}
-
- self.setHorizontalSpacing(8)
- self.setVerticalSpacing(8)
-
- return
-
- ##
- # Getters and Setters
- ##
-
- def setHelpTextStyle(self, helpCol, fontScale=0.9):
- """Set the text color for the help text.
- """
- if isinstance(helpCol, QColor):
- self._helpCol = helpCol
- else:
- self._helpCol = QColor(*helpCol)
- self._fontScale = fontScale
- return
-
- def setHelpText(self, intRow, theText):
- """Set the text for the help label.
- """
- if intRow in self._itemMap:
- self._itemMap[intRow]["help"].setText(theText)
- return
-
- def setLabelText(self, intRow, theText):
- """Set the text for the main label.
- """
- if intRow in self._itemMap:
- self._itemMap[intRow]["label"].setText(theText)
- return
-
- ##
- # Class Methods
- ##
-
- def addGroupLabel(self, theLabel):
- """Adds a text label to separate groups of settings.
- """
- if isinstance(theLabel, QLabel):
- qLabel = theLabel
- elif isinstance(theLabel, str):
- qLabel = QLabel("%s" % theLabel)
- else:
- qLabel = None
- raise ValueError("theLabel must be a QLabel")
-
- qLabel.setContentsMargins(0, 4, 0, 4)
- self.addWidget(qLabel, self._nextRow, 0, 1, 2, Qt.AlignLeft)
-
- self.setRowStretch(self._nextRow, 0)
- self.setRowStretch(self._nextRow+1, 1)
-
- self._nextRow += 1
-
- return
-
- 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 = {
- "label" : None,
- "help" : None,
- "widget" : None,
- }
-
- if isinstance(theLabel, QLabel):
- qLabel = theLabel
- elif isinstance(theLabel, str):
- qLabel = QLabel(theLabel)
- else:
- qLabel = None
- raise ValueError("theLabel must be a QLabel")
-
- if isinstance(theWidget, QWidget):
- qWidget = theWidget
- else:
- qWidget = None
- raise ValueError("theWidget must be a QWidget")
-
- qLabel.setIndent(8)
- if helpText is not None:
- qHelp = QHelpLabel(str(helpText), self._helpCol, self._fontScale)
- qHelp.setIndent(8)
-
- labelBox = QVBoxLayout()
- labelBox.addWidget(qLabel)
- labelBox.addWidget(qHelp)
- labelBox.setSpacing(0)
-
- thisEntry["help"] = qHelp
- self.addLayout(labelBox, self._nextRow, 0, 1, 1, Qt.AlignLeft | Qt.AlignTop)
-
- else:
- self.addWidget(qLabel, self._nextRow, 0, 1, 1, Qt.AlignLeft | Qt.AlignTop)
-
- if theUnit is not None:
- controlBox = QHBoxLayout()
- controlBox.addWidget(qWidget, 0, Qt.AlignVCenter)
- 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)
-
- qLabel.setBuddy(qWidget)
-
- self.setRowStretch(self._nextRow, 0)
- self.setRowStretch(self._nextRow+1, 1)
-
- thisEntry["label"] = qLabel
- thisEntry["widget"] = qWidget
-
- self._itemMap[self._nextRow] = thisEntry
-
- self._nextRow += 1
-
- return self._nextRow - 1
-
-# END Class QConfigLayout
-
-class QHelpLabel(QLabel):
-
- def __init__(self, theText, textCol, fontSize=0.9):
- QLabel.__init__(self, theText)
-
- if isinstance(textCol, QColor):
- qCol = textCol
- else:
- qCol = QColor(*textCol)
-
- lblCol = self.palette()
- lblCol.setColor(QPalette.WindowText, qCol)
- self.setPalette(lblCol)
-
- lblFont = self.font()
- lblFont.setPointSizeF(fontSize*lblFont.pointSizeF())
- self.setFont(lblFont)
-
- return
-
-# END Class QHelpLabel
diff --git a/nw/gui/additions/qswitch.py b/nw/gui/additions/qswitch.py
deleted file mode 100644
index 7eb32830..00000000
--- a/nw/gui/additions/qswitch.py
+++ /dev/null
@@ -1,164 +0,0 @@
-# -*- coding: utf-8 -*-
-"""novelWriter Addition QSwitch
-
- novelWriter – Addition QSwitch
-================================
- A custom Qt switch button
-
- File History:
- Created: 2020-05-03 [0.4.5]
-
- The core code of this class is based on Stack Overflow example by
- Stefan Scherfke: https://stackoverflow.com/a/51825815/5825851
-
- This file is a part of novelWriter
- Copyright 2020, Veronica Berglyd Olsen
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful, but
- WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-"""
-
-import logging
-import nw
-
-from PyQt5.QtCore import Qt, QSize, QRectF, QPropertyAnimation, pyqtProperty
-from PyQt5.QtWidgets import QAbstractButton, QSizePolicy
-from PyQt5.QtGui import QPainter
-
-from nw.constants import nwUnicode
-
-logger = logging.getLogger(__name__)
-
-class QSwitch(QAbstractButton):
-
- def __init__(self, parent=None):
- super().__init__(parent=parent)
-
- self.setCheckable(True)
- self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
- self.setFixedWidth(40)
- self.setFixedHeight(20)
- self._offset = 10
-
- return
-
- ##
- # Properties
- ##
-
- @pyqtProperty(int)
- def offset(self):
- return self._offset
-
- @offset.setter
- def offset(self, theOffset):
- self._offset = theOffset
- self.update()
- return
-
- ##
- # Getters and Setters
- ##
-
- def setChecked(self, isChecked):
- """Overload setChecked to also alter the offset.
- """
- super().setChecked(isChecked)
- if isChecked:
- self.offset = 30
- else:
- self.offset = 10
- return
-
- ##
- # Events
- ##
-
- def resizeEvent(self, theEvent):
- """Overload resize to ensure correct offset.
- """
- super().resizeEvent(theEvent)
- if self.isChecked():
- self.offset = 30
- else:
- self.offset = 10
- return
-
- def paintEvent(self, event):
- """Drawing the switch itself.
- """
- qPaint = QPainter(self)
- qPaint.setRenderHint(QPainter.Antialiasing, True)
- qPaint.setPen(Qt.NoPen)
-
- qPalette = self.palette()
- if self.isChecked():
- trackBrush = qPalette.highlight()
- thumbBrush = qPalette.highlightedText()
- textColor = qPalette.highlight().color()
- thumbText = nwUnicode.U_CHECK
- else:
- trackBrush = qPalette.dark()
- thumbBrush = qPalette.light()
- textColor = qPalette.dark().color()
- thumbText = nwUnicode.U_MULT
-
- if self.isEnabled():
- trackOpacity = 1.0
- else:
- trackOpacity = 0.6
- trackBrush = qPalette.shadow()
- thumbBrush = qPalette.mid()
- textColor = qPalette.shadow().color()
-
- qPaint.setBrush(trackBrush)
- qPaint.setOpacity(trackOpacity)
- qPaint.drawRoundedRect(0, 0, 40, 20, 10, 10)
-
- qPaint.setBrush(thumbBrush)
- qPaint.drawEllipse(self.offset - 8, 2, 16, 16)
-
- theFont = qPaint.font()
- theFont.setPixelSize(12)
- qPaint.setPen(textColor)
- qPaint.setFont(theFont)
- qPaint.drawText(
- QRectF(self.offset - 8, 2, 16, 16),
- Qt.AlignCenter, thumbText
- )
-
- return
-
- def mouseReleaseEvent(self, event):
- """Animate the switch on mouse release.
- """
- super().mouseReleaseEvent(event)
- if event.button() == Qt.LeftButton:
- doAnim = QPropertyAnimation(self, b'offset', self)
- doAnim.setDuration(120)
- doAnim.setStartValue(self.offset)
- if self.isChecked():
- doAnim.setEndValue(30)
- else:
- doAnim.setEndValue(10)
- doAnim.start()
- return
-
- def enterEvent(self, event):
- """Change the cursor when hovering the button.
- """
- self.setCursor(Qt.PointingHandCursor)
- super().enterEvent(event)
- return
-
-# END Class QSwitch
diff --git a/nw/gui/build.py b/nw/gui/build.py
index 639f504d..791dff67 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -43,7 +43,7 @@ from PyQt5.QtWidgets import (
QFileDialog, QFontDialog, QSpinBox
)
-from nw.gui.additions import QSwitch
+from nw.gui.custom import QSwitch
from nw.core import ToHtml
from nw.constants import (
nwAlert, nwFiles, nwItemType, nwItemLayout, nwItemClass
diff --git a/nw/gui/custom.py b/nw/gui/custom.py
new file mode 100644
index 00000000..96e0c482
--- /dev/null
+++ b/nw/gui/custom.py
@@ -0,0 +1,430 @@
+# -*- coding: utf-8 -*-
+"""novelWriter Addition QConfigLayout
+
+ novelWriter – Addition QConfigLayout
+======================================
+ A custom Qt grid layout for config forms similar to QFormLayout
+
+ File History:
+ Created: 2020-05-03 [0.4.5] QConfigLayout
+ Created: 2020-05-03 [0.4.5] QSwitch
+ Created: 2020-05-17 [0.5.1] PagedDialog
+
+ This file is a part of novelWriter
+ Copyright 2020, Veronica Berglyd Olsen
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+"""
+
+import logging
+import nw
+
+from PyQt5.QtGui import QColor, QPalette, QPainter
+from PyQt5.QtCore import (
+ Qt, QRect, QPoint, QSize, QRectF, QPropertyAnimation, pyqtProperty
+)
+from PyQt5.QtWidgets import (
+ QGridLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QSizePolicy,
+ QAbstractButton, QDialog, QTabWidget, QTabBar, QStyle,
+ QStylePainter, QStyleOptionTab
+)
+
+from nw.constants import nwUnicode
+
+logger = logging.getLogger(__name__)
+
+# =============================================================================================== #
+# Config FOrm Layout
+# =============================================================================================== #
+
+class QConfigLayout(QGridLayout):
+
+ def __init__(self):
+ super().__init__()
+
+ self._nextRow = 0
+ self._helpCol = QColor(0, 0, 0)
+ self._fontScale = 0.9
+
+ self._itemMap = {}
+
+ self.setHorizontalSpacing(8)
+ self.setVerticalSpacing(8)
+
+ return
+
+ ##
+ # Getters and Setters
+ ##
+
+ def setHelpTextStyle(self, helpCol, fontScale=0.9):
+ """Set the text color for the help text.
+ """
+ if isinstance(helpCol, QColor):
+ self._helpCol = helpCol
+ else:
+ self._helpCol = QColor(*helpCol)
+ self._fontScale = fontScale
+ return
+
+ def setHelpText(self, intRow, theText):
+ """Set the text for the help label.
+ """
+ if intRow in self._itemMap:
+ self._itemMap[intRow]["help"].setText(theText)
+ return
+
+ def setLabelText(self, intRow, theText):
+ """Set the text for the main label.
+ """
+ if intRow in self._itemMap:
+ self._itemMap[intRow]["label"].setText(theText)
+ return
+
+ ##
+ # Class Methods
+ ##
+
+ def addGroupLabel(self, theLabel):
+ """Adds a text label to separate groups of settings.
+ """
+ if isinstance(theLabel, QLabel):
+ qLabel = theLabel
+ elif isinstance(theLabel, str):
+ qLabel = QLabel("%s" % theLabel)
+ else:
+ qLabel = None
+ raise ValueError("theLabel must be a QLabel")
+
+ qLabel.setContentsMargins(0, 4, 0, 4)
+ self.addWidget(qLabel, self._nextRow, 0, 1, 2, Qt.AlignLeft)
+
+ self.setRowStretch(self._nextRow, 0)
+ self.setRowStretch(self._nextRow+1, 1)
+
+ self._nextRow += 1
+
+ return
+
+ 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 = {
+ "label" : None,
+ "help" : None,
+ "widget" : None,
+ }
+
+ if isinstance(theLabel, QLabel):
+ qLabel = theLabel
+ elif isinstance(theLabel, str):
+ qLabel = QLabel(theLabel)
+ else:
+ qLabel = None
+ raise ValueError("theLabel must be a QLabel")
+
+ if isinstance(theWidget, QWidget):
+ qWidget = theWidget
+ else:
+ qWidget = None
+ raise ValueError("theWidget must be a QWidget")
+
+ qLabel.setIndent(8)
+ if helpText is not None:
+ qHelp = QHelpLabel(str(helpText), self._helpCol, self._fontScale)
+ qHelp.setIndent(8)
+
+ labelBox = QVBoxLayout()
+ labelBox.addWidget(qLabel)
+ labelBox.addWidget(qHelp)
+ labelBox.setSpacing(0)
+
+ thisEntry["help"] = qHelp
+ self.addLayout(labelBox, self._nextRow, 0, 1, 1, Qt.AlignLeft | Qt.AlignTop)
+
+ else:
+ self.addWidget(qLabel, self._nextRow, 0, 1, 1, Qt.AlignLeft | Qt.AlignTop)
+
+ if theUnit is not None:
+ controlBox = QHBoxLayout()
+ controlBox.addWidget(qWidget, 0, Qt.AlignVCenter)
+ 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)
+
+ qLabel.setBuddy(qWidget)
+
+ self.setRowStretch(self._nextRow, 0)
+ self.setRowStretch(self._nextRow+1, 1)
+
+ thisEntry["label"] = qLabel
+ thisEntry["widget"] = qWidget
+
+ self._itemMap[self._nextRow] = thisEntry
+
+ self._nextRow += 1
+
+ return self._nextRow - 1
+
+# END Class QConfigLayout
+
+class QHelpLabel(QLabel):
+
+ def __init__(self, theText, textCol, fontSize=0.9):
+ QLabel.__init__(self, theText)
+
+ if isinstance(textCol, QColor):
+ qCol = textCol
+ else:
+ qCol = QColor(*textCol)
+
+ lblCol = self.palette()
+ lblCol.setColor(QPalette.WindowText, qCol)
+ self.setPalette(lblCol)
+
+ lblFont = self.font()
+ lblFont.setPointSizeF(fontSize*lblFont.pointSizeF())
+ self.setFont(lblFont)
+
+ return
+
+# END Class QHelpLabel
+
+# =============================================================================================== #
+# Switch Widget
+# =============================================================================================== #
+
+class QSwitch(QAbstractButton):
+
+ def __init__(self, parent=None):
+ super().__init__(parent=parent)
+
+ self.setCheckable(True)
+ self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+ self.setFixedWidth(40)
+ self.setFixedHeight(20)
+ self._offset = 10
+
+ return
+
+ ##
+ # Properties
+ ##
+
+ @pyqtProperty(int)
+ def offset(self):
+ return self._offset
+
+ @offset.setter
+ def offset(self, theOffset):
+ self._offset = theOffset
+ self.update()
+ return
+
+ ##
+ # Getters and Setters
+ ##
+
+ def setChecked(self, isChecked):
+ """Overload setChecked to also alter the offset.
+ """
+ super().setChecked(isChecked)
+ if isChecked:
+ self.offset = 30
+ else:
+ self.offset = 10
+ return
+
+ ##
+ # Events
+ ##
+
+ def resizeEvent(self, theEvent):
+ """Overload resize to ensure correct offset.
+ """
+ super().resizeEvent(theEvent)
+ if self.isChecked():
+ self.offset = 30
+ else:
+ self.offset = 10
+ return
+
+ def paintEvent(self, event):
+ """Drawing the switch itself.
+ """
+ qPaint = QPainter(self)
+ qPaint.setRenderHint(QPainter.Antialiasing, True)
+ qPaint.setPen(Qt.NoPen)
+
+ qPalette = self.palette()
+ if self.isChecked():
+ trackBrush = qPalette.highlight()
+ thumbBrush = qPalette.highlightedText()
+ textColor = qPalette.highlight().color()
+ thumbText = nwUnicode.U_CHECK
+ else:
+ trackBrush = qPalette.dark()
+ thumbBrush = qPalette.light()
+ textColor = qPalette.dark().color()
+ thumbText = nwUnicode.U_MULT
+
+ if self.isEnabled():
+ trackOpacity = 1.0
+ else:
+ trackOpacity = 0.6
+ trackBrush = qPalette.shadow()
+ thumbBrush = qPalette.mid()
+ textColor = qPalette.shadow().color()
+
+ qPaint.setBrush(trackBrush)
+ qPaint.setOpacity(trackOpacity)
+ qPaint.drawRoundedRect(0, 0, 40, 20, 10, 10)
+
+ qPaint.setBrush(thumbBrush)
+ qPaint.drawEllipse(self.offset - 8, 2, 16, 16)
+
+ theFont = qPaint.font()
+ theFont.setPixelSize(12)
+ qPaint.setPen(textColor)
+ qPaint.setFont(theFont)
+ qPaint.drawText(
+ QRectF(self.offset - 8, 2, 16, 16),
+ Qt.AlignCenter, thumbText
+ )
+
+ return
+
+ def mouseReleaseEvent(self, event):
+ """Animate the switch on mouse release.
+ """
+ super().mouseReleaseEvent(event)
+ if event.button() == Qt.LeftButton:
+ doAnim = QPropertyAnimation(self, b'offset', self)
+ doAnim.setDuration(120)
+ doAnim.setStartValue(self.offset)
+ if self.isChecked():
+ doAnim.setEndValue(30)
+ else:
+ doAnim.setEndValue(10)
+ doAnim.start()
+ return
+
+ def enterEvent(self, event):
+ """Change the cursor when hovering the button.
+ """
+ self.setCursor(Qt.PointingHandCursor)
+ super().enterEvent(event)
+ return
+
+# END Class QSwitch
+
+# =============================================================================================== #
+# Paged Dialog w/Custom TabWidget
+# =============================================================================================== #
+
+class PagedDialog(QDialog):
+
+ def __init__(self, theParent=None):
+ QDialog.__init__(self, parent=theParent)
+
+ self._outerBox = QVBoxLayout()
+ self._buttonBox = QHBoxLayout()
+ self._tabBox = QTabWidget()
+
+ self._tabBar = VerticalTabBar(self)
+ self._tabBox.setTabBar(self._tabBar)
+ self._tabBox.setTabPosition(QTabWidget.West)
+ self._tabBar.setExpanding(False)
+
+ self._outerBox.addWidget(self._tabBox)
+ self._outerBox.addLayout(self._buttonBox)
+ self.setLayout(self._outerBox)
+
+ # Default Margins
+ qM = self._outerBox.contentsMargins()
+ mL = qM.left()
+ mR = qM.right()
+ mT = qM.top()
+ mB = qM.bottom()
+
+ self.setContentsMargins(0, 0, 0, 0)
+ self._outerBox.setContentsMargins(0, 0, 0, mB)
+ self._buttonBox.setContentsMargins(mL, 0, mR, 0)
+ self._outerBox.setSpacing(mT)
+
+ return
+
+ def addTab(self, tabWidget, tabLabel):
+ """Forwards the adding of tabs to the QTabWidget.
+ """
+ self._tabBox.addTab(tabWidget, tabLabel)
+ return
+
+ def addControls(self, buttonBar):
+ """Adds a button bar to the dialog.
+ """
+ self._buttonBox.addWidget(buttonBar)
+ return
+
+# END Class PagedDialog
+
+class VerticalTabBar(QTabBar):
+
+ def __init__(self, theParent=None):
+ QTabBar.__init__(self, parent=theParent)
+ return
+
+ def tabSizeHint(self, theIndex):
+ """Returns a transposed size hint for the rotated bar.
+ """
+ tSize = QTabBar.tabSizeHint(self, theIndex)
+ tSize.transpose()
+ return tSize
+
+ def paintEvent(self, theEvent):
+ """Custom implementation of the label painter that rotates the
+ label 90 degrees.
+ """
+ pObj = QStylePainter(self)
+ oObj = QStyleOptionTab()
+
+ for i in range(self.count()):
+ self.initStyleOption(oObj, i)
+ pObj.drawControl(QStyle.CE_TabBarTabShape, oObj)
+ pObj.save()
+
+ oSize = oObj.rect.size()
+ oSize.transpose()
+ oRect = QRect(QPoint(), oSize)
+ oRect.moveCenter(oObj.rect.center())
+ oObj.rect = oRect
+
+ oCenter = self.tabRect(i).center()
+ pObj.translate(oCenter)
+ pObj.rotate(90)
+ pObj.translate(-oCenter)
+ pObj.drawControl(QStyle.CE_TabBarTabLabel, oObj)
+ pObj.restore()
+
+ return
+
+# END Class VerticalTabBar
diff --git a/nw/gui/dialogs/__init__.py b/nw/gui/dialogs/__init__.py
deleted file mode 100644
index 5f4e4a03..00000000
--- a/nw/gui/dialogs/__init__.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from nw.gui.dialogs.about import GuiAbout
-from nw.gui.dialogs.preferences import GuiPreferences
-from nw.gui.dialogs.docmerge import GuiDocMerge
-from nw.gui.dialogs.docsplit import GuiDocSplit
-from nw.gui.dialogs.itemeditor import GuiItemEditor
-from nw.gui.dialogs.projectsettings import GuiProjectSettings
-from nw.gui.dialogs.projectload import GuiProjectLoad
-from nw.gui.dialogs.sessionlog import GuiSessionLogView
-
-__all__ = [
- "GuiAbout",
- "GuiPreferences",
- "GuiDocMerge",
- "GuiDocSplit",
- "GuiItemEditor",
- "GuiProjectSettings",
- "GuiProjectLoad",
- "GuiSessionLogView",
-]
diff --git a/nw/gui/elements/searchbar.py b/nw/gui/docbars.py
similarity index 55%
rename from nw/gui/elements/searchbar.py
rename to nw/gui/docbars.py
index 590d8d0a..0923cb91 100644
--- a/nw/gui/elements/searchbar.py
+++ b/nw/gui/docbars.py
@@ -6,7 +6,9 @@
Class holding the main window search bar
File History:
- Created: 2019-09-29 [0.2.1]
+ Created: 2019-09-29 [0.2.1] GuiSearchBar
+ Created: 2019-10-31 [0.3.2] GuiNoticeBar
+ Created: 2020-04-25 [0.4.5] GuiDocTitleBar
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
@@ -29,11 +31,13 @@ import logging
import nw
from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QPalette, QColor
from PyQt5.QtWidgets import (
- qApp, QFrame, QGridLayout, QLabel, QLineEdit, QPushButton
+ qApp, QFrame, QGridLayout, QLabel, QLineEdit, QPushButton,
+ QHBoxLayout
)
-from nw.constants import nwDocAction
+from nw.constants import nwDocAction, nwUnicode
logger = logging.getLogger(__name__)
@@ -42,7 +46,7 @@ class GuiSearchBar(QFrame):
def __init__(self, theParent):
QFrame.__init__(self, theParent)
- logger.debug("Initialising SearchBar ...")
+ logger.debug("Initialising GuiSearchBar ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
@@ -89,7 +93,7 @@ class GuiSearchBar(QFrame):
self._replaceVisible(False)
- logger.debug("SearchBar initialisation complete")
+ logger.debug("GuiSearchBar initialisation complete")
return
@@ -163,3 +167,133 @@ class GuiSearchBar(QFrame):
return True
# END Class GuiSearchBar
+
+class GuiNoticeBar(QFrame):
+
+ def __init__(self, theParent):
+ QFrame.__init__(self, theParent)
+
+ logger.debug("Initialising GuiNoticeBar ...")
+
+ self.mainConf = nw.CONFIG
+ self.theParent = theParent
+ self.theTheme = theParent.theTheme
+
+ self.setContentsMargins(0,0,0,0)
+ self.setFrameShape(QFrame.Box)
+
+ self.mainBox = QHBoxLayout(self)
+ self.mainBox.setContentsMargins(8,2,2,2)
+
+ self.noteLabel = QLabel("")
+
+ self.closeButton = QPushButton(self.theTheme.getIcon("close"),"")
+ self.closeButton.clicked.connect(self.hideNote)
+
+ self.mainBox.addWidget(self.noteLabel)
+ self.mainBox.addWidget(self.closeButton)
+ self.mainBox.setStretch(0, 1)
+
+ self.setLayout(self.mainBox)
+
+ self.hideNote()
+
+ logger.debug("GuiNoticeBar initialisation complete")
+
+ return
+
+ def showNote(self, theNote):
+ """Show the note on the noticebar.
+ """
+ self.noteLabel.setText("Note: %s" % theNote)
+ self.setVisible(True)
+ return
+
+ def hideNote(self):
+ """Clear the noticebar and hide it.
+ """
+ self.noteLabel.setText("")
+ self.setVisible(False)
+ return
+
+# END Class GuiNoticeBar
+
+class GuiDocTitleBar(QLabel):
+
+ def __init__(self, theParent, theProject):
+ QLabel.__init__(self, theParent)
+
+ logger.debug("Initialising GuiDocTitleBar ...")
+
+ self.mainConf = nw.CONFIG
+ self.theParent = theParent
+ self.theProject = theProject
+ self.theTheme = theParent.theTheme
+ self.theHandle = None
+
+ self.setText("")
+ self.setIndent(0)
+ self.setMargin(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.fontPointSize)
+ self.setFont(lblFont)
+
+ logger.debug("GuiDocTitleBar initialisation complete")
+
+ return
+
+ ##
+ # Setters
+ ##
+
+ def setTitleFromHandle(self, tHandle):
+ """Sets the document title from the handle, or alternatively,
+ set the whole document path.
+ """
+ self.setText("")
+ self.theHandle = tHandle
+ if tHandle is None:
+ return False
+
+ if self.mainConf.showFullPath:
+ tTitle = []
+ tTree = self.theProject.projTree.getItemPath(tHandle)
+ for aHandle in reversed(tTree):
+ nwItem = self.theProject.projTree[aHandle]
+ if nwItem is not None:
+ tTitle.append(nwItem.itemName)
+ sSep = " %s " % nwUnicode.U_RSAQUO
+ self.setText(sSep.join(tTitle))
+ else:
+ nwItem = self.theProject.projTree[tHandle]
+ if nwItem is None:
+ return False
+
+ self.setText(nwItem.itemName)
+
+ return True
+
+ ##
+ # Events
+ ##
+
+ def mousePressEvent(self, theEvent):
+ """Capture a click on the title and ensure that the item is
+ selected in the project tree.
+ """
+ self.theParent.setSelectedHandle(self.theHandle)
+ return
+
+# END Class GuiDocTitleBar
diff --git a/nw/gui/elements/docdetails.py b/nw/gui/docdetails.py
similarity index 100%
rename from nw/gui/elements/docdetails.py
rename to nw/gui/docdetails.py
diff --git a/nw/gui/elements/doceditor.py b/nw/gui/doceditor.py
similarity index 97%
rename from nw/gui/elements/doceditor.py
rename to nw/gui/doceditor.py
index 8ceb297e..bf511e07 100644
--- a/nw/gui/elements/doceditor.py
+++ b/nw/gui/doceditor.py
@@ -6,7 +6,8 @@
Class holding the document editor
File History:
- Created: 2018-09-29 [0.0.1]
+ Created: 2018-09-29 [0.0.1] GuiDocEditor
+ Created: 2019-04-22 [0.0.1] WordCounter
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
@@ -30,7 +31,7 @@ import nw
from time import time
-from PyQt5.QtCore import Qt, QTimer, pyqtSlot
+from PyQt5.QtCore import Qt, QThread, QTimer, pyqtSlot
from PyQt5.QtWidgets import (
qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox
)
@@ -40,9 +41,9 @@ from PyQt5.QtGui import (
)
from nw.core import NWDoc
-from nw.gui.tools import GuiDocHighlighter, WordCounter
-from nw.gui.elements.doctitlebar import GuiDocTitleBar
-from nw.core import NWSpellSimple
+from nw.gui.dochighlight import GuiDocHighlighter
+from nw.gui.docbars import GuiDocTitleBar
+from nw.core import NWSpellSimple, countWords
from nw.constants import nwUnicode, nwDocAction
logger = logging.getLogger(__name__)
@@ -52,7 +53,7 @@ class GuiDocEditor(QTextEdit):
def __init__(self, theParent, theProject):
QTextEdit.__init__(self)
- logger.debug("Initialising DocEditor ...")
+ logger.debug("Initialising GuiDocEditor ...")
# Class Variables
self.mainConf = nw.CONFIG
@@ -131,7 +132,7 @@ class GuiDocEditor(QTextEdit):
self.initEditor()
- logger.debug("DocEditor initialisation complete")
+ logger.debug("GuiDocEditor initialisation complete")
# Connect Functions
self.setSelectedHandle = self.theParent.treeView.setSelectedHandle
@@ -1082,3 +1083,28 @@ class GuiDocEditor(QTextEdit):
return
# END Class GuiDocEditor
+
+class WordCounter(QThread):
+
+ def __init__(self, theParent):
+ QThread.__init__(self, theParent)
+ self.theParent = theParent
+ self.charCount = 0
+ self.wordCount = 0
+ self.paraCount = 0
+ return
+
+ def run(self):
+ """Overloaded run function for the word counter, forwarding the
+ call to the function that does the actual counting.
+ """
+ theText = self.theParent.getText()
+ cC, wC, pC = countWords(theText)
+
+ self.charCount = cC
+ self.wordCount = wC
+ self.paraCount = pC
+
+ return
+
+## END Class WordCounter
diff --git a/nw/gui/tools/dochighlight.py b/nw/gui/dochighlight.py
similarity index 100%
rename from nw/gui/tools/dochighlight.py
rename to nw/gui/dochighlight.py
diff --git a/nw/gui/dialogs/docmerge.py b/nw/gui/docmerge.py
similarity index 99%
rename from nw/gui/dialogs/docmerge.py
rename to nw/gui/docmerge.py
index 1fc225a9..73b15a6a 100644
--- a/nw/gui/dialogs/docmerge.py
+++ b/nw/gui/docmerge.py
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
)
from nw.constants import nwAlert, nwItemType
-from nw.gui.additions import QHelpLabel
+from nw.gui.custom import QHelpLabel
from nw.core import NWDoc
logger = logging.getLogger(__name__)
diff --git a/nw/gui/dialogs/docsplit.py b/nw/gui/docsplit.py
similarity index 99%
rename from nw/gui/dialogs/docsplit.py
rename to nw/gui/docsplit.py
index e029a49b..702e8f17 100644
--- a/nw/gui/dialogs/docsplit.py
+++ b/nw/gui/docsplit.py
@@ -34,7 +34,7 @@ from PyQt5.QtWidgets import (
QListWidgetItem, QDialogButtonBox, QLabel
)
from nw.constants import nwAlert, nwItemType, nwItemClass, nwItemLayout
-from nw.gui.additions import QHelpLabel
+from nw.gui.custom import QHelpLabel
from nw.core import NWDoc
logger = logging.getLogger(__name__)
diff --git a/nw/gui/elements/doctree.py b/nw/gui/doctree.py
similarity index 100%
rename from nw/gui/elements/doctree.py
rename to nw/gui/doctree.py
diff --git a/nw/gui/elements/docviewer.py b/nw/gui/docviewer.py
similarity index 99%
rename from nw/gui/elements/docviewer.py
rename to nw/gui/docviewer.py
index 66f2a5d3..1be66bba 100644
--- a/nw/gui/elements/docviewer.py
+++ b/nw/gui/docviewer.py
@@ -34,7 +34,7 @@ from PyQt5.QtGui import QTextOption, QFont, QPalette, QColor, QTextCursor
from nw.core import ToHtml
from nw.constants import nwAlert, nwItemType, nwDocAction
-from nw.gui.elements.doctitlebar import GuiDocTitleBar
+from nw.gui.docbars import GuiDocTitleBar
logger = logging.getLogger(__name__)
diff --git a/nw/gui/elements/__init__.py b/nw/gui/elements/__init__.py
deleted file mode 100644
index b0938816..00000000
--- a/nw/gui/elements/__init__.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from nw.gui.elements.docdetails import GuiDocDetails
-from nw.gui.elements.doceditor import GuiDocEditor
-from nw.gui.elements.doctitlebar import GuiDocTitleBar
-from nw.gui.elements.doctree import GuiDocTree
-from nw.gui.elements.docviewer import GuiDocViewer
-from nw.gui.elements.noticebar import GuiNoticeBar
-from nw.gui.elements.outline import GuiProjectOutline
-from nw.gui.elements.searchbar import GuiSearchBar
-from nw.gui.elements.viewdetails import GuiDocViewDetails
-
-__all__ = [
- "GuiDocDetails",
- "GuiDocEditor",
- "GuiDocTitleBar",
- "GuiDocTree",
- "GuiDocViewer",
- "GuiNoticeBar",
- "GuiProjectOutline",
- "GuiSearchBar",
- "GuiDocViewDetails",
-]
diff --git a/nw/gui/elements/doctitlebar.py b/nw/gui/elements/doctitlebar.py
deleted file mode 100644
index 8c5208e4..00000000
--- a/nw/gui/elements/doctitlebar.py
+++ /dev/null
@@ -1,117 +0,0 @@
-# -*- coding: utf-8 -*-
-"""novelWriter GUI Document Title Bar
-
- novelWriter – GUI Document Title Bar
-======================================
- Class holding the document title bar class
-
- File History:
- Created: 2020-04-25 [0.4.5]
-
- This file is a part of novelWriter
- Copyright 2020, Veronica Berglyd Olsen
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful, but
- WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-"""
-
-import logging
-import nw
-
-from PyQt5.QtCore import Qt
-from PyQt5.QtGui import QPalette, QColor
-from PyQt5.QtWidgets import QLabel, QFrame
-
-from nw.constants import nwUnicode
-
-logger = logging.getLogger(__name__)
-
-class GuiDocTitleBar(QLabel):
-
- def __init__(self, theParent, theProject):
- QLabel.__init__(self, theParent)
-
- logger.debug("Initialising DocTitleBar ...")
-
- self.mainConf = nw.CONFIG
- self.theParent = theParent
- self.theProject = theProject
- self.theTheme = theParent.theTheme
- self.theHandle = None
-
- self.setText("")
- self.setIndent(0)
- self.setMargin(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.fontPointSize)
- self.setFont(lblFont)
-
- logger.debug("DocTitleBar initialisation complete")
-
- return
-
- ##
- # Setters
- ##
-
- def setTitleFromHandle(self, tHandle):
- """Sets the document title from the handle, or alternatively,
- set the whole document path.
- """
- self.setText("")
- self.theHandle = tHandle
- if tHandle is None:
- return False
-
- if self.mainConf.showFullPath:
- tTitle = []
- tTree = self.theProject.projTree.getItemPath(tHandle)
- for aHandle in reversed(tTree):
- nwItem = self.theProject.projTree[aHandle]
- if nwItem is not None:
- tTitle.append(nwItem.itemName)
- sSep = " %s " % nwUnicode.U_RSAQUO
- self.setText(sSep.join(tTitle))
- else:
- nwItem = self.theProject.projTree[tHandle]
- if nwItem is None:
- return False
-
- self.setText(nwItem.itemName)
-
- return True
-
- ##
- # Events
- ##
-
- def mousePressEvent(self, theEvent):
- """Capture a click on the title and ensure that the item is
- selected in the project tree.
- """
- self.theParent.setSelectedHandle(self.theHandle)
- return
-
-# END Class GuiDocTitleBar
diff --git a/nw/gui/elements/noticebar.py b/nw/gui/elements/noticebar.py
deleted file mode 100644
index 4b6d57aa..00000000
--- a/nw/gui/elements/noticebar.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# -*- coding: utf-8 -*-
-"""novelWriter GUI Main Window Notice Bar
-
- novelWriter – GUI Main Window Notice Bar
-==========================================
- Class holding the main window notice bar for the doc editor
-
- File History:
- Created: 2019-10-31 [0.3.2]
-
- This file is a part of novelWriter
- Copyright 2020, Veronica Berglyd Olsen
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful, but
- WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-"""
-
-import logging
-import nw
-
-from PyQt5.QtWidgets import QFrame, QHBoxLayout, QLabel, QPushButton
-
-logger = logging.getLogger(__name__)
-
-class GuiNoticeBar(QFrame):
-
- def __init__(self, theParent):
- QFrame.__init__(self, theParent)
-
- logger.debug("Initialising NoticeBar ...")
-
- self.mainConf = nw.CONFIG
- self.theParent = theParent
- self.theTheme = theParent.theTheme
-
- self.setContentsMargins(0,0,0,0)
- self.setFrameShape(QFrame.Box)
-
- self.mainBox = QHBoxLayout(self)
- self.mainBox.setContentsMargins(8,2,2,2)
-
- self.noteLabel = QLabel("")
-
- self.closeButton = QPushButton(self.theTheme.getIcon("close"),"")
- self.closeButton.clicked.connect(self.hideNote)
-
- self.mainBox.addWidget(self.noteLabel)
- self.mainBox.addWidget(self.closeButton)
- self.mainBox.setStretch(0, 1)
-
- self.setLayout(self.mainBox)
-
- self.hideNote()
-
- logger.debug("NoticeBar initialisation complete")
-
- return
-
- def showNote(self, theNote):
- """Show the note on the noticebar.
- """
- self.noteLabel.setText("Note: %s" % theNote)
- self.setVisible(True)
- return
-
- def hideNote(self):
- """Clear the noticebar and hide it.
- """
- self.noteLabel.setText("")
- self.setVisible(False)
- return
-
-# END Class GuiNoticeBar
diff --git a/nw/gui/icons.py b/nw/gui/icons.py
deleted file mode 100644
index f1a33145..00000000
--- a/nw/gui/icons.py
+++ /dev/null
@@ -1,327 +0,0 @@
-# -*- coding: utf-8 -*-
-"""novelWriter Icons Class
-
- novelWriter – Icons Class
-===========================
- This class manages the GUI icons
-
- File History:
- Created: 2019-11-08 [0.4.0]
-
- This file is a part of novelWriter
- Copyright 2020, Veronica Berglyd Olsen
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful, but
- WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-"""
-
-import logging
-import configparser
-import nw
-
-from os import path, listdir
-
-from PyQt5.QtCore import QSize
-from PyQt5.QtSvg import QSvgWidget
-from PyQt5.QtGui import QIcon, QPixmap
-from PyQt5.QtWidgets import QStyle, qApp
-
-logger = logging.getLogger(__name__)
-
-class GuiIcons:
- """The icon class manages the content of the assets/icons folder,
- and provides a simple interface for requesting icons. Only icons
- listed in the ICON_MAP are handled.
-
- Icons are loaded on first request, and then cached for further
- requests. Each icon key in the ICON_MAP has a series of fallbacks:
- * The first lookup is in the key-to-file map for the selected icon
- theme. The map is specified in the icons.conf file in the theme
- folder. The map makes it possible to preserve the original file
- name from the icon theme were the icons were extracted.
- * Second, if the icon does not exist in the theme map, the
- GuiIcons class will check if there is a QStyle icon specified in
- the ICON_MAP data tuple[0]. This will let Qt pull the closest
- system icon.
- * Third action is to look up the freedesktop icon theme name using
- the fromTheme Qt call. This generally produces the same results
- as the step above, but has more icons available in other cases.
- * Fourth, and finally, the icon is looked up in the fallback
- folder. Files in this folder must have the same file name as the
- novelWriter internal icon key, with '-dark' appended to it for
- the dark background version of the icon.
- """
-
- ICON_MAP = {
- # Project and GUI icons
- "cls_none" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
- "cls_novel" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
- "cls_plot" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
- "cls_character" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
- "cls_world" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
- "cls_timeline" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
- "cls_object" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
- "cls_entity" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
- "cls_custom" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
- "cls_trash" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
- "proj_document" : (QStyle.SP_FileIcon, "x-office-document"),
- "proj_folder" : (QStyle.SP_DirIcon, "folder"),
- "proj_nwx" : (None, None),
- "status_lang" : (None, None),
- "status_time" : (None, None),
- "status_stats" : (None, None),
-
- ## General Button Icons
- "folder-open" : (QStyle.SP_DirOpenIcon, "folder-open"),
- "delete" : (QStyle.SP_DialogDiscardButton, "edit-delete"),
- "add" : (None, "list-add"),
- "remove" : (None, "list-remove"),
- "close" : (QStyle.SP_DialogCloseButton, "window-close"),
- "done" : (QStyle.SP_DialogApplyButton, None),
- "search" : (None, "edit-find"),
- "search-replace" : (None, "edit-find-replace"),
- "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"),
- }
-
- DECO_MAP = {
- "nwicon" : ["icons", "novelwriter.svg"],
- }
-
- def __init__(self, theParent):
-
- self.mainConf = nw.CONFIG
- self.theParent = theParent
-
- # Storage
- self.qIcons = {}
- self.themeMap = {}
- self.themeList = []
- self.fbackName = "fallback"
- self.confName = "icons.conf"
-
- # Icon Theme Path
- self.iconPath = None
- self.confFile = None
-
- # Icon Theme Meta
- self.themeName = ""
- self.themeDescription = ""
- self.themeAuthor = ""
- self.themeCredit = ""
- self.themeUrl = ""
- self.themeLicense = ""
- self.themeLicenseUrl = ""
-
- return
-
- ##
- # Actions
- ##
-
- def updateTheme(self):
- """Update the theme map. This is more of an init, since many of
- the GUI icons cannot really be replaced without writing specific
- update functions for the classes where they're used.
- """
-
- logger.debug("Loading icon theme files")
-
- self.themeMap = {}
- checkPath = path.join(self.mainConf.iconPath, self.mainConf.guiIcons)
- if path.isdir(checkPath):
- logger.debug("Loading icon theme '%s'" % self.mainConf.guiIcons)
- self.iconPath = checkPath
- self.confFile = path.join(checkPath, self.confName)
- else:
- return False
-
- # Config File
- confParser = configparser.ConfigParser()
- try:
- confParser.read_file(open(self.confFile, mode="r", encoding="utf8"))
- except Exception as e:
- logger.error("Could not load icon theme settings from: %s" % self.confFile)
- return False
-
- ## Main
- cnfSec = "Main"
- if confParser.has_section(cnfSec):
- self.themeName = self._parseLine( confParser, cnfSec, "name", "")
- self.themeDescription = self._parseLine( confParser, cnfSec, "description", "")
- self.themeAuthor = self._parseLine( confParser, cnfSec, "author", "")
- self.themeCredit = self._parseLine( confParser, cnfSec, "credit", "")
- self.themeUrl = self._parseLine( confParser, cnfSec, "url", "")
- self.themeLicense = self._parseLine( confParser, cnfSec, "license", "")
- self.themeLicenseUrl = self._parseLine( confParser, cnfSec, "licenseurl", "")
-
- ## Palette
- cnfSec = "Map"
- if confParser.has_section(cnfSec):
- for iconName, iconFile in confParser.items(cnfSec):
- if iconName not in self.ICON_MAP:
- logger.error("Unknown icon name '%s' in config file" % iconName)
- else:
- iconPath = path.join(self.iconPath, iconFile)
- if path.isfile(iconPath):
- self.themeMap[iconName] = iconPath
- logger.verbose("Icon slot '%s' using file '%s'" % (iconName, iconFile))
- else:
- logger.error("Icon file '%s' not in theme folder" % iconFile)
-
- logger.info("Loaded icon theme '%s'" % self.mainConf.guiIcons)
-
- return True
-
- ##
- # Access Functions
- ##
-
- def loadDecoration(self, decoKey, decoSize=None):
- """Load graphical decoration element based on the decoration
- map. This function always returns a QSwgWidget.
- """
- if decoKey not in self.DECO_MAP:
- logger.error("Decoration with name '%s' does not exist" % decoKey)
- return QSvgWidget()
-
- svgPath = path.join(
- self.mainConf.assetPath,
- self.DECO_MAP[decoKey][0],
- self.DECO_MAP[decoKey][1]
- )
- if not path.isfile(svgPath):
- logger.error("Decoration file '%s' not in assets folder" % self.DECO_MAP[decoKey])
- return QSvgWidget()
-
- svgDeco = QSvgWidget(svgPath)
- if decoSize is not None:
- svgDeco.setFixedSize(QSize(decoSize[0],decoSize[1]))
-
- return svgDeco
-
- def getIcon(self, iconKey, iconSize=None):
- """Return an icon from the icon buffer. If it doesn't exist,
- return, load it, and if it still doesn't exist, return an empty
- icon.
- """
- if iconKey in self.qIcons:
- return self.qIcons[iconKey]
- else:
- qIcon = self._loadIcon(iconKey)
- self.qIcons[iconKey] = qIcon
- return qIcon
-
- def getPixmap(self, iconKey, iconSize):
- """Return an icon from the icon buffer as a QPixmap. If it
- doesn't exist, return an empty QPixmap.
- """
- qIcon = self.getIcon(iconKey)
- return qIcon.pixmap(iconSize[0], iconSize[1], QIcon.Normal)
-
- def listThemes(self):
- """Scan the icons themes folder and list all themes.
- """
- if self.themeList:
- return self.themeList
-
- confParser = configparser.ConfigParser()
- for themeDir in listdir(self.mainConf.iconPath):
- themePath = path.join(self.mainConf.iconPath, themeDir)
- if not path.isdir(themePath) or themeDir == self.fbackName:
- continue
- themeConf = path.join(themePath, self.confName)
- logger.verbose("Checking icon theme config for '%s'" % themeDir)
- try:
- confParser.read_file(open(themeConf, mode="r", encoding="utf8"))
- except Exception as e:
- self.theParent.makeAlert(
- ["Could not load theme config file.",str(e)], nwAlert.ERROR
- )
- continue
- themeName = ""
- if confParser.has_section("Main"):
- if confParser.has_option("Main", "name"):
- themeName = confParser.get("Main", "name")
- logger.verbose("Theme name is '%s'" % themeName)
- if themeName != "":
- self.themeList.append((themeDir, themeName))
-
- self.themeList = sorted(self.themeList, key=lambda x: x[1])
-
- return self.themeList
-
- ##
- # Internal Functions
- ##
-
- def _loadIcon(self, iconKey):
- """Load an icon from the assets or theme folder, with a
- preference for dark/light icons depending on theme type, if such
- an icon exists. Prefer svg files over png files. Always returns
- a QIcon.
- """
-
- if iconKey not in self.ICON_MAP:
- logger.error("Requested unknown icon name '%s'" % iconKey)
- return QIcon()
-
- if iconKey in self.themeMap:
- logger.verbose("Loading: %s" % path.relpath(self.themeMap[iconKey]))
- return QIcon(self.themeMap[iconKey])
-
- # Next, we try to load the Qt style icons
- if self.ICON_MAP[iconKey][0] is not None:
- logger.verbose("Loading icon '%s' from Qt QStyle.standardIcon" % iconKey)
- return qApp.style().standardIcon(self.ICON_MAP[iconKey][0])
-
- # If we're still here, try to set from system theme
- if self.ICON_MAP[iconKey][1] is not None:
- logger.verbose("Loading icon '%s' from system theme" % iconKey)
- if QIcon().hasThemeIcon(self.ICON_MAP[iconKey][1]):
- return QIcon().fromTheme(self.ICON_MAP[iconKey][1])
-
- # Finally. we check if we have a fallback icon
- if self.mainConf.guiDark:
- fbackIcon = path.join(
- self.mainConf.iconPath, self.fbackName, "%s-dark.svg" % iconKey
- )
- if path.isfile(fbackIcon):
- logger.verbose("Loading icon '%s' from fallback theme" % iconKey)
- return QIcon(fbackIcon)
- fbackIcon = path.join(self.mainConf.iconPath, self.fbackName, "%s.svg" % iconKey)
- if path.isfile(fbackIcon):
- logger.verbose("Loading icon '%s' from fallback theme" % iconKey)
- return QIcon(fbackIcon)
-
- # Give up and return an empty icon
- logger.warning("Did not load an icon for '%s'" % iconKey)
-
- return QIcon()
-
- def _parseLine(self, confParser, cnfSec, cnfName, cnfDefault):
- """Simple wrapper for the config parser check for entry existing
- before arrempting to load.
- """
- if confParser.has_section(cnfSec):
- if confParser.has_option(cnfSec, cnfName):
- return confParser.get(cnfSec, cnfName)
- return cnfDefault
-
-# END Class GuiIcons
diff --git a/nw/gui/dialogs/itemeditor.py b/nw/gui/itemeditor.py
similarity index 99%
rename from nw/gui/dialogs/itemeditor.py
rename to nw/gui/itemeditor.py
index ce1fac67..7eb54b14 100644
--- a/nw/gui/dialogs/itemeditor.py
+++ b/nw/gui/itemeditor.py
@@ -34,7 +34,7 @@ from PyQt5.QtWidgets import (
QDialogButtonBox
)
-from nw.gui.additions import QSwitch
+from nw.gui.custom import QSwitch
from nw.constants import nwLabels, nwItemLayout, nwItemClass, nwItemType
logger = logging.getLogger(__name__)
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index 31206d82..5ff4d918 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -32,7 +32,7 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QMenuBar, QAction, QMessageBox
-from nw.gui.dialogs import GuiAbout
+from nw.gui.about import GuiAbout
from nw.constants import nwItemType, nwItemClass, nwDocAction
logger = logging.getLogger(__name__)
diff --git a/nw/gui/elements/outline.py b/nw/gui/outline.py
similarity index 100%
rename from nw/gui/elements/outline.py
rename to nw/gui/outline.py
diff --git a/nw/gui/dialogs/preferences.py b/nw/gui/preferences.py
similarity index 99%
rename from nw/gui/dialogs/preferences.py
rename to nw/gui/preferences.py
index 280993f5..a05e5eb0 100644
--- a/nw/gui/dialogs/preferences.py
+++ b/nw/gui/preferences.py
@@ -37,7 +37,7 @@ from PyQt5.QtWidgets import (
QDialogButtonBox, QFileDialog, QFontDialog
)
-from nw.gui.additions import QSwitch, QConfigLayout, PagedDialog
+from nw.gui.custom import QSwitch, QConfigLayout, PagedDialog
from nw.core import NWSpellCheck, NWSpellSimple, NWSpellEnchant
from nw.constants import nwAlert, nwQuotes
diff --git a/nw/gui/dialogs/projectload.py b/nw/gui/projectload.py
similarity index 100%
rename from nw/gui/dialogs/projectload.py
rename to nw/gui/projectload.py
diff --git a/nw/gui/dialogs/projectsettings.py b/nw/gui/projectsettings.py
similarity index 99%
rename from nw/gui/dialogs/projectsettings.py
rename to nw/gui/projectsettings.py
index 66af994a..a1c28ca4 100644
--- a/nw/gui/dialogs/projectsettings.py
+++ b/nw/gui/projectsettings.py
@@ -38,7 +38,7 @@ from PyQt5.QtWidgets import (
)
from nw.constants import nwAlert
-from nw.gui.additions import QSwitch, PagedDialog, QConfigLayout
+from nw.gui.custom import QSwitch, PagedDialog, QConfigLayout
logger = logging.getLogger(__name__)
diff --git a/nw/gui/dialogs/sessionlog.py b/nw/gui/sessionlog.py
similarity index 100%
rename from nw/gui/dialogs/sessionlog.py
rename to nw/gui/sessionlog.py
diff --git a/nw/gui/theme.py b/nw/gui/theme.py
index a376e1b1..8613c683 100644
--- a/nw/gui/theme.py
+++ b/nw/gui/theme.py
@@ -6,7 +6,8 @@
This class reads and store the main theme
File History:
- Created: 2019-05-18 [0.1.3]
+ Created: 2019-05-18 [0.1.3] GuiTheme
+ Created: 2019-11-08 [0.4.0] GuiIcons
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
@@ -32,16 +33,22 @@ import nw
from os import path, listdir
from math import ceil
-from PyQt5.QtWidgets import qApp
+from PyQt5.QtCore import QSize
+from PyQt5.QtSvg import QSvgWidget
+from PyQt5.QtWidgets import QStyle, qApp
from PyQt5.QtGui import (
- QPalette, QColor, QIcon, QFont, QFontMetrics, QFontDatabase
+ QPalette, QColor, QIcon, QFont, QFontMetrics, QFontDatabase, QPixmap
)
from nw.constants import nwAlert
-from nw.gui.icons import GuiIcons
logger = logging.getLogger(__name__)
+# =============================================================================================== #
+# Gui Theme Class
+# Handles the look and feel of novelWriter
+# =============================================================================================== #
+
class GuiTheme:
def __init__(self, theParent):
@@ -447,3 +454,295 @@ class GuiTheme:
return cnfDefault
# End Class GuiTheme
+
+# =============================================================================================== #
+# Icons Class
+# =============================================================================================== #
+
+class GuiIcons:
+ """The icon class manages the content of the assets/icons folder,
+ and provides a simple interface for requesting icons. Only icons
+ listed in the ICON_MAP are handled.
+
+ Icons are loaded on first request, and then cached for further
+ requests. Each icon key in the ICON_MAP has a series of fallbacks:
+ * The first lookup is in the key-to-file map for the selected icon
+ theme. The map is specified in the icons.conf file in the theme
+ folder. The map makes it possible to preserve the original file
+ name from the icon theme were the icons were extracted.
+ * Second, if the icon does not exist in the theme map, the
+ GuiIcons class will check if there is a QStyle icon specified in
+ the ICON_MAP data tuple[0]. This will let Qt pull the closest
+ system icon.
+ * Third action is to look up the freedesktop icon theme name using
+ the fromTheme Qt call. This generally produces the same results
+ as the step above, but has more icons available in other cases.
+ * Fourth, and finally, the icon is looked up in the fallback
+ folder. Files in this folder must have the same file name as the
+ novelWriter internal icon key, with '-dark' appended to it for
+ the dark background version of the icon.
+ """
+
+ ICON_MAP = {
+ # Project and GUI icons
+ "cls_none" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
+ "cls_novel" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
+ "cls_plot" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
+ "cls_character" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
+ "cls_world" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
+ "cls_timeline" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
+ "cls_object" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
+ "cls_entity" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
+ "cls_custom" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
+ "cls_trash" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
+ "proj_document" : (QStyle.SP_FileIcon, "x-office-document"),
+ "proj_folder" : (QStyle.SP_DirIcon, "folder"),
+ "proj_nwx" : (None, None),
+ "status_lang" : (None, None),
+ "status_time" : (None, None),
+ "status_stats" : (None, None),
+
+ ## General Button Icons
+ "folder-open" : (QStyle.SP_DirOpenIcon, "folder-open"),
+ "delete" : (QStyle.SP_DialogDiscardButton, "edit-delete"),
+ "add" : (None, "list-add"),
+ "remove" : (None, "list-remove"),
+ "close" : (QStyle.SP_DialogCloseButton, "window-close"),
+ "done" : (QStyle.SP_DialogApplyButton, None),
+ "search" : (None, "edit-find"),
+ "search-replace" : (None, "edit-find-replace"),
+ "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"),
+ }
+
+ DECO_MAP = {
+ "nwicon" : ["icons", "novelwriter.svg"],
+ }
+
+ def __init__(self, theParent):
+
+ self.mainConf = nw.CONFIG
+ self.theParent = theParent
+
+ # Storage
+ self.qIcons = {}
+ self.themeMap = {}
+ self.themeList = []
+ self.fbackName = "fallback"
+ self.confName = "icons.conf"
+
+ # Icon Theme Path
+ self.iconPath = None
+ self.confFile = None
+
+ # Icon Theme Meta
+ self.themeName = ""
+ self.themeDescription = ""
+ self.themeAuthor = ""
+ self.themeCredit = ""
+ self.themeUrl = ""
+ self.themeLicense = ""
+ self.themeLicenseUrl = ""
+
+ return
+
+ ##
+ # Actions
+ ##
+
+ def updateTheme(self):
+ """Update the theme map. This is more of an init, since many of
+ the GUI icons cannot really be replaced without writing specific
+ update functions for the classes where they're used.
+ """
+
+ logger.debug("Loading icon theme files")
+
+ self.themeMap = {}
+ checkPath = path.join(self.mainConf.iconPath, self.mainConf.guiIcons)
+ if path.isdir(checkPath):
+ logger.debug("Loading icon theme '%s'" % self.mainConf.guiIcons)
+ self.iconPath = checkPath
+ self.confFile = path.join(checkPath, self.confName)
+ else:
+ return False
+
+ # Config File
+ confParser = configparser.ConfigParser()
+ try:
+ confParser.read_file(open(self.confFile, mode="r", encoding="utf8"))
+ except Exception as e:
+ logger.error("Could not load icon theme settings from: %s" % self.confFile)
+ return False
+
+ ## Main
+ cnfSec = "Main"
+ if confParser.has_section(cnfSec):
+ self.themeName = self._parseLine( confParser, cnfSec, "name", "")
+ self.themeDescription = self._parseLine( confParser, cnfSec, "description", "")
+ self.themeAuthor = self._parseLine( confParser, cnfSec, "author", "")
+ self.themeCredit = self._parseLine( confParser, cnfSec, "credit", "")
+ self.themeUrl = self._parseLine( confParser, cnfSec, "url", "")
+ self.themeLicense = self._parseLine( confParser, cnfSec, "license", "")
+ self.themeLicenseUrl = self._parseLine( confParser, cnfSec, "licenseurl", "")
+
+ ## Palette
+ cnfSec = "Map"
+ if confParser.has_section(cnfSec):
+ for iconName, iconFile in confParser.items(cnfSec):
+ if iconName not in self.ICON_MAP:
+ logger.error("Unknown icon name '%s' in config file" % iconName)
+ else:
+ iconPath = path.join(self.iconPath, iconFile)
+ if path.isfile(iconPath):
+ self.themeMap[iconName] = iconPath
+ logger.verbose("Icon slot '%s' using file '%s'" % (iconName, iconFile))
+ else:
+ logger.error("Icon file '%s' not in theme folder" % iconFile)
+
+ logger.info("Loaded icon theme '%s'" % self.mainConf.guiIcons)
+
+ return True
+
+ ##
+ # Access Functions
+ ##
+
+ def loadDecoration(self, decoKey, decoSize=None):
+ """Load graphical decoration element based on the decoration
+ map. This function always returns a QSwgWidget.
+ """
+ if decoKey not in self.DECO_MAP:
+ logger.error("Decoration with name '%s' does not exist" % decoKey)
+ return QSvgWidget()
+
+ svgPath = path.join(
+ self.mainConf.assetPath,
+ self.DECO_MAP[decoKey][0],
+ self.DECO_MAP[decoKey][1]
+ )
+ if not path.isfile(svgPath):
+ logger.error("Decoration file '%s' not in assets folder" % self.DECO_MAP[decoKey])
+ return QSvgWidget()
+
+ svgDeco = QSvgWidget(svgPath)
+ if decoSize is not None:
+ svgDeco.setFixedSize(QSize(decoSize[0],decoSize[1]))
+
+ return svgDeco
+
+ def getIcon(self, iconKey, iconSize=None):
+ """Return an icon from the icon buffer. If it doesn't exist,
+ return, load it, and if it still doesn't exist, return an empty
+ icon.
+ """
+ if iconKey in self.qIcons:
+ return self.qIcons[iconKey]
+ else:
+ qIcon = self._loadIcon(iconKey)
+ self.qIcons[iconKey] = qIcon
+ return qIcon
+
+ def getPixmap(self, iconKey, iconSize):
+ """Return an icon from the icon buffer as a QPixmap. If it
+ doesn't exist, return an empty QPixmap.
+ """
+ qIcon = self.getIcon(iconKey)
+ return qIcon.pixmap(iconSize[0], iconSize[1], QIcon.Normal)
+
+ def listThemes(self):
+ """Scan the icons themes folder and list all themes.
+ """
+ if self.themeList:
+ return self.themeList
+
+ confParser = configparser.ConfigParser()
+ for themeDir in listdir(self.mainConf.iconPath):
+ themePath = path.join(self.mainConf.iconPath, themeDir)
+ if not path.isdir(themePath) or themeDir == self.fbackName:
+ continue
+ themeConf = path.join(themePath, self.confName)
+ logger.verbose("Checking icon theme config for '%s'" % themeDir)
+ try:
+ confParser.read_file(open(themeConf, mode="r", encoding="utf8"))
+ except Exception as e:
+ self.theParent.makeAlert(
+ ["Could not load theme config file.",str(e)], nwAlert.ERROR
+ )
+ continue
+ themeName = ""
+ if confParser.has_section("Main"):
+ if confParser.has_option("Main", "name"):
+ themeName = confParser.get("Main", "name")
+ logger.verbose("Theme name is '%s'" % themeName)
+ if themeName != "":
+ self.themeList.append((themeDir, themeName))
+
+ self.themeList = sorted(self.themeList, key=lambda x: x[1])
+
+ return self.themeList
+
+ ##
+ # Internal Functions
+ ##
+
+ def _loadIcon(self, iconKey):
+ """Load an icon from the assets or theme folder, with a
+ preference for dark/light icons depending on theme type, if such
+ an icon exists. Prefer svg files over png files. Always returns
+ a QIcon.
+ """
+
+ if iconKey not in self.ICON_MAP:
+ logger.error("Requested unknown icon name '%s'" % iconKey)
+ return QIcon()
+
+ if iconKey in self.themeMap:
+ logger.verbose("Loading: %s" % path.relpath(self.themeMap[iconKey]))
+ return QIcon(self.themeMap[iconKey])
+
+ # Next, we try to load the Qt style icons
+ if self.ICON_MAP[iconKey][0] is not None:
+ logger.verbose("Loading icon '%s' from Qt QStyle.standardIcon" % iconKey)
+ return qApp.style().standardIcon(self.ICON_MAP[iconKey][0])
+
+ # If we're still here, try to set from system theme
+ if self.ICON_MAP[iconKey][1] is not None:
+ logger.verbose("Loading icon '%s' from system theme" % iconKey)
+ if QIcon().hasThemeIcon(self.ICON_MAP[iconKey][1]):
+ return QIcon().fromTheme(self.ICON_MAP[iconKey][1])
+
+ # Finally. we check if we have a fallback icon
+ if self.mainConf.guiDark:
+ fbackIcon = path.join(
+ self.mainConf.iconPath, self.fbackName, "%s-dark.svg" % iconKey
+ )
+ if path.isfile(fbackIcon):
+ logger.verbose("Loading icon '%s' from fallback theme" % iconKey)
+ return QIcon(fbackIcon)
+ fbackIcon = path.join(self.mainConf.iconPath, self.fbackName, "%s.svg" % iconKey)
+ if path.isfile(fbackIcon):
+ logger.verbose("Loading icon '%s' from fallback theme" % iconKey)
+ return QIcon(fbackIcon)
+
+ # Give up and return an empty icon
+ logger.warning("Did not load an icon for '%s'" % iconKey)
+
+ return QIcon()
+
+ def _parseLine(self, confParser, cnfSec, cnfName, cnfDefault):
+ """Simple wrapper for the config parser check for entry existing
+ before arrempting to load.
+ """
+ if confParser.has_section(cnfSec):
+ if confParser.has_option(cnfSec, cnfName):
+ return confParser.get(cnfSec, cnfName)
+ return cnfDefault
+
+# END Class GuiIcons
diff --git a/nw/gui/tools/__init__.py b/nw/gui/tools/__init__.py
deleted file mode 100644
index cc6cfdfa..00000000
--- a/nw/gui/tools/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from nw.gui.tools.dochighlight import GuiDocHighlighter
-from nw.gui.tools.optionstate import OptionState
-from nw.gui.tools.wordcounter import WordCounter
-
-__all__ = [
- "GuiDocHighlighter",
- "OptionState",
- "WordCounter",
-]
diff --git a/nw/gui/tools/optionstate.py b/nw/gui/tools/optionstate.py
deleted file mode 100644
index 5f265622..00000000
--- a/nw/gui/tools/optionstate.py
+++ /dev/null
@@ -1,180 +0,0 @@
-# -*- coding: utf-8 -*-
-"""novelWriter Options State
-
- novelWriter – Options State
-=============================
- Class holding the last state of GUI options
-
- File History:
- Created: 2019-10-21 [0.3.1] - Original version meant to be sub classed
- Created: 2020-02-19 [0.4.5] - Rewritten from superclass to single file tool
-
- This file is a part of novelWriter
- Copyright 2020, Veronica Berglyd Olsen
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful, but
- WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-"""
-
-import logging
-import json
-import nw
-
-from os import path
-
-from nw.constants import nwFiles
-
-logger = logging.getLogger(__name__)
-
-class OptionState():
-
- def __init__(self, theProject):
-
- self.theProject = theProject
- self.theState = {}
- self.stringOpt = ()
- self.boolOpt = ()
- self.intOpt = ()
-
- return
-
- def loadSettings(self):
- """Load the options dictionary from the project settings file.
- """
- if self.theProject.projMeta is None:
- return False
-
- stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
- theState = {}
-
- if path.isfile(stateFile):
- logger.debug("Loading GUI options file")
- try:
- with open(stateFile,mode="r",encoding="utf8") as inFile:
- theJson = inFile.read()
- theState = json.loads(theJson)
- except Exception as e:
- logger.error("Failed to load GUI options file")
- logger.error(str(e))
- return False
- for anOpt in theState:
- self.theState[anOpt] = theState[anOpt]
-
- return True
-
- def saveSettings(self):
- """Save the options dictionary to the project settings file.
- """
- if self.theProject.projMeta is None:
- return False
-
- stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
- logger.debug("Saving GUI options file")
-
- try:
- with open(stateFile,mode="w+",encoding="utf8") as outFile:
- outFile.write(json.dumps(self.theState, indent=2))
- except Exception as e:
- logger.error("Failed to save GUI options file")
- logger.error(str(e))
- return False
-
- return True
-
- def setValue(self, setGroup, setName, setValue):
- """Saves a value, with a given group and name.
- """
- if not setGroup in self.theState:
- self.theState[setGroup] = {}
- self.theState[setGroup][setName] = setValue
- return True
-
- def getValue(self, getGroup, getName, defaultValue):
- """Return an arbitrary type value, if it exists. Otherwise,
- return the default value.
- """
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- try:
- return self.theState[getGroup][getName]
- except:
- return defaultValue
- return defaultValue
-
- def getString(self, getGroup, getName, defaultValue):
- """Return the value as a string, if it exists. Otherwise, return
- the default value.
- """
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- try:
- return str(self.theState[getGroup][getName])
- except:
- return defaultValue
- return defaultValue
-
- def getInt(self, getGroup, getName, defaultValue):
- """Return the value as an int, if it exists. Otherwise, return
- the default value.
- """
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- try:
- return int(self.theState[getGroup][getName])
- except:
- return defaultValue
- return defaultValue
-
- def getFloat(self, getGroup, getName, defaultValue):
- """Return the value as a float, if it exists. Otherwise, return
- the default value.
- """
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- try:
- return float(self.theState[getGroup][getName])
- except:
- return defaultValue
- return defaultValue
-
- def getBool(self, getGroup, getName, defaultValue):
- """Return the value as a bool, if it exists. Otherwise, return
- the default value.
- """
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- try:
- return bool(self.theState[getGroup][getName])
- except:
- return defaultValue
- return defaultValue
-
- def validIntRange(self, theValue, intA, intB, intDefault):
- """Check that an int is in a given range. If it isn't, return
- the default value.
- """
- if isinstance(theValue, int):
- if theValue >= intA and theValue <= intB:
- return theValue
- return intDefault
-
- def validIntTuple(self, theValue, theTuple, intDefault):
- """Check that an int is an element of a tuple. If it isn't,
- return the default value.
- """
- if isinstance(theValue, int):
- if theValue in theTuple:
- return theValue
- return intDefault
-
-# END Class OptionState
diff --git a/nw/gui/tools/wordcounter.py b/nw/gui/tools/wordcounter.py
deleted file mode 100644
index 49255040..00000000
--- a/nw/gui/tools/wordcounter.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# -*- coding: utf-8 -*-
-"""novelWriter GUI Document Word Counter
-
- novelWriter – GUI Document Word Counter
-===================================
- A thread for counting words and characters in a document
-
- File History:
- Created: 2019-04-22 [0.0.1]
-
- This file is a part of novelWriter
- Copyright 2020, Veronica Berglyd Olsen
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful, but
- WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-"""
-
-import logging
-import nw
-
-from PyQt5.QtCore import QThread
-
-from nw.core.tools import countWords
-
-logger = logging.getLogger(__name__)
-
-class WordCounter(QThread):
-
- def __init__(self, theParent):
- QThread.__init__(self, theParent)
- self.theParent = theParent
- self.charCount = 0
- self.wordCount = 0
- self.paraCount = 0
- return
-
- def run(self):
- """Overloaded run function for the word counter, forwarding the
- call to the function that does the actual counting.
- """
- theText = self.theParent.getText()
- cC, wC, pC = countWords(theText)
-
- self.charCount = cC
- self.wordCount = wC
- self.paraCount = pC
-
- return
-
-## END Class WordCounter
diff --git a/nw/gui/elements/viewdetails.py b/nw/gui/viewdetails.py
similarity index 100%
rename from nw/gui/elements/viewdetails.py
rename to nw/gui/viewdetails.py