Merge branch 'dev' into project_handling

This commit is contained in:
Veronica Berglyd Olsen
2024-01-27 14:42:43 +01:00
68 changed files with 2045 additions and 2222 deletions
File diff suppressed because it is too large Load Diff
Binary file not shown.
+4 -4
View File
@@ -42,9 +42,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen" __author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net" __email__ = "code@vkbo.net"
__version__ = "2.3-alpha1" __version__ = "2.3a2"
__hexversion__ = "0x020300a1" __hexversion__ = "0x020300a2"
__date__ = "2023-12-17" __date__ = "2024-01-26"
__status__ = "Stable" __status__ = "Stable"
__domain__ = "novelwriter.io" __domain__ = "novelwriter.io"
@@ -131,7 +131,7 @@ def main(sysArgs: list | None = None):
elif inOpt == "--debug": elif inOpt == "--debug":
CONFIG.isDebug = True CONFIG.isDebug = True
logLevel = logging.DEBUG logLevel = logging.DEBUG
logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}" logFormat = "[{asctime:}] {filename:>18}:{lineno:<4d} {levelname:8} {message:}"
elif inOpt == "--style": elif inOpt == "--style":
qtStyle = inArg qtStyle = inArg
elif inOpt == "--config": elif inOpt == "--config":
+5
View File
@@ -250,6 +250,11 @@ def formatTime(t: int) -> str:
return "ERROR" return "ERROR"
def formatVersion(value: str) -> str:
"""Format a version number into a more human readable form."""
return value.lower().replace("a", " Alpha ").replace("b", " Beta ").replace("rc", " RC ")
# =============================================================================================== # # =============================================================================================== #
# String Functions # String Functions
# =============================================================================================== # # =============================================================================================== #
+1 -2
View File
@@ -374,7 +374,6 @@ class ProjectBuilder:
project.data.setUuid(None) project.data.setUuid(None)
project.data.setName(projName) project.data.setName(projName)
project.data.setTitle(projName)
project.data.setAuthor(projAuthor) project.data.setAuthor(projAuthor)
project.data.setLanguage(projLang) project.data.setLanguage(projLang)
project.setDefaultStatusImport() project.setDefaultStatusImport()
@@ -383,7 +382,7 @@ class ProjectBuilder:
# Add Root Folders # Add Root Folders
hNovelRoot = project.newRoot(nwItemClass.NOVEL) hNovelRoot = project.newRoot(nwItemClass.NOVEL)
hTitlePage = project.newFile(lblTitlePage, hNovelRoot) hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
novelTitle = project.data.title if project.data.title else project.data.name novelTitle = project.data.name
titlePage = f"#! {novelTitle}\n\n" titlePage = f"#! {novelTitle}\n\n"
if project.data.author: if project.data.author:
+12 -9
View File
@@ -528,17 +528,20 @@ class NWIndex:
yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem
return return
def getNovelWordCount(self, activeOnly: bool = True) -> int: def getNovelWordCount(self, rootHandle: str | None = None, activeOnly: bool = True) -> int:
"""Count the number of words in the novel project.""" """Count the number of words in one or all novel roots."""
wCount = 0 return sum(hItem.wordCount for _, _, hItem in self._itemIndex.iterNovelStructure(
for _, _, hItem in self._itemIndex.iterNovelStructure(activeOnly=activeOnly): rHandle=rootHandle, activeOnly=activeOnly
wCount += hItem.wordCount ))
return wCount
def getNovelTitleCounts(self, activeOnly: bool = True) -> list[int]: def getNovelTitleCounts(
"""Count the number of titles in the novel project.""" self, rootHandle: str | None = None, activeOnly: bool = True
) -> list[int]:
"""Count the number of titles in one or all novel roots."""
hCount = [0, 0, 0, 0, 0] hCount = [0, 0, 0, 0, 0]
for _, _, hItem in self._itemIndex.iterNovelStructure(activeOnly=activeOnly): for _, _, hItem in self._itemIndex.iterNovelStructure(
rHandle=rootHandle, activeOnly=activeOnly
):
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
hCount[iLevel] += 1 hCount[iLevel] += 1
return hCount return hCount
+6 -5
View File
@@ -53,10 +53,6 @@ VALID_MAP: dict[str, set[str]] = {
"GuiProjectSettings": { "GuiProjectSettings": {
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW", "winWidth", "winHeight", "replaceColW", "statusColW", "importColW",
}, },
"GuiProjectDetails": {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
},
"GuiWordList": {"winWidth", "winHeight"}, "GuiWordList": {"winWidth", "winHeight"},
"GuiNovelView": {"lastCol", "lastColSize"}, "GuiNovelView": {"lastCol", "lastColSize"},
"GuiBuildSettings": { "GuiBuildSettings": {
@@ -71,7 +67,12 @@ VALID_MAP: dict[str, set[str]] = {
}, },
"GuiDocViewerPanel": { "GuiDocViewerPanel": {
"colWidths", "hideInactive", "colWidths", "hideInactive",
} },
"GuiNovelDetails": {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
"novelRoot",
},
} }
+1 -14
View File
@@ -53,7 +53,6 @@ class NWProjectData:
# Project Meta # Project Meta
self._uuid = "" self._uuid = ""
self._name = "" self._name = ""
self._title = ""
self._author = "" self._author = ""
self._saveCount = 0 self._saveCount = 0
self._autoCount = 0 self._autoCount = 0
@@ -102,11 +101,6 @@ class NWProjectData:
"""Return the project name.""" """Return the project name."""
return self._name return self._name
@property
def title(self) -> str:
"""Return the project title."""
return self._title
@property @property
def author(self) -> str: def author(self) -> str:
"""Return the project author.""" """Return the project author."""
@@ -228,16 +222,9 @@ class NWProjectData:
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return return
def setTitle(self, value: str | None) -> None:
"""Set a new novel title."""
if value != self._title:
self._title = simplified(str(value or ""))
self._project.setProjectChanged(True)
return
def setAuthor(self, value: str | None) -> None: def setAuthor(self, value: str | None) -> None:
"""Set the author value.""" """Set the author value."""
if value != self._title: if value != self._author:
self._author = simplified(str(value or "")) self._author = simplified(str(value or ""))
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return return
+3 -5
View File
@@ -46,7 +46,7 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
FILE_VERSION = "1.5" # The current project file format version FILE_VERSION = "1.5" # The current project file format version
FILE_REVISION = "1" # The current project file format revision FILE_REVISION = "2" # The current project file format revision
HEX_VERSION = 0x0105 HEX_VERSION = 0x0105
NUM_VERSION = { NUM_VERSION = {
@@ -105,7 +105,8 @@ class ProjectXMLReader:
the project or the content into their respective section nodes the project or the content into their respective section nodes
as attributes. The id attribute was also added to the project. as attributes. The id attribute was also added to the project.
Rev 1: Drops the titleFormat section of settings. Rev 1: Drops the titleFormat node from settings. 2.1 Beta 1.
Rev 2: Drops the title node from project. 2.3 Beta 1.
""" """
def __init__(self, path: str | Path) -> None: def __init__(self, path: str | Path) -> None:
@@ -232,8 +233,6 @@ class ProjectXMLReader:
for xItem in xSection: for xItem in xSection:
if xItem.tag == "name": if xItem.tag == "name":
data.setName(xItem.text) data.setName(xItem.text)
elif xItem.tag == "title":
data.setTitle(xItem.text)
elif xItem.tag == "author": elif xItem.tag == "author":
data.setAuthor(xItem.text) data.setAuthor(xItem.text)
else: else:
@@ -505,7 +504,6 @@ class ProjectXMLWriter:
xProject = ET.SubElement(xRoot, "project", attrib=projAttr) xProject = ET.SubElement(xRoot, "project", attrib=projAttr)
self._packSingleValue(xProject, "name", data.name) self._packSingleValue(xProject, "name", data.name)
self._packSingleValue(xProject, "title", data.title)
self._packSingleValue(xProject, "author", data.author) self._packSingleValue(xProject, "author", data.author)
# Save Project Settings # Save Project Settings
-1
View File
@@ -338,7 +338,6 @@ class ToHtml(Tokenizer):
data = { data = {
"meta": { "meta": {
"projectName": self._project.data.name, "projectName": self._project.data.name,
"novelTitle": self._project.data.title,
"novelAuthor": self._project.data.author, "novelAuthor": self._project.data.author,
"buildTime": int(timeStamp), "buildTime": int(timeStamp),
"buildTimeStr": formatTimeStamp(timeStamp), "buildTimeStr": formatTimeStamp(timeStamp),
-1
View File
@@ -759,7 +759,6 @@ class Tokenizer(ABC):
data = { data = {
"meta": { "meta": {
"projectName": self._project.data.name, "projectName": self._project.data.name,
"novelTitle": self._project.data.title,
"novelAuthor": self._project.data.author, "novelAuthor": self._project.data.author,
"buildTime": int(timeStamp), "buildTime": int(timeStamp),
"buildTimeStr": formatTimeStamp(timeStamp), "buildTimeStr": formatTimeStamp(timeStamp),
+2 -4
View File
@@ -283,9 +283,7 @@ class ToOdt(Tokenizer):
# =============== # ===============
if self._headerText == "": if self._headerText == "":
theTitle = self._project.data.title or self._project.data.name self._headerText = f"{self._project.data.name} / {self._project.data.author} /"
theAuth = self._project.data.author
self._headerText = f"{theTitle} / {theAuth} /"
# Create Roots # Create Roots
# ============ # ============
@@ -373,7 +371,7 @@ class ToOdt(Tokenizer):
# Dublin Core Meta Data # Dublin Core Meta Data
xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "title")) xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "title"))
xMeta.text = self._project.data.title or self._project.data.name xMeta.text = self._project.data.name
xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "date")) xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "date"))
xMeta.text = timeStamp xMeta.text = timeStamp
+11 -9
View File
@@ -220,23 +220,25 @@ class GuiAbout(QDialog):
def _setStyleSheet(self) -> None: def _setStyleSheet(self) -> None:
"""Set stylesheet for all browser tabs.""" """Set stylesheet for all browser tabs."""
colHead = SHARED.theme.colHead
colKey = SHARED.theme.colKey
styleSheet = ( styleSheet = (
"h1, h2, h3, h4 {{" "h1, h2, h3, h4 {{"
" color: rgb({hColR},{hColG},{hColB});" " color: rgb({hColR}, {hColG}, {hColB});"
"}}\n" "}}\n"
"a {{" "a {{"
" color: rgb({hColR},{hColG},{hColB});" " color: rgb({hColR}, {hColG}, {hColB});"
"}}\n" "}}\n"
".alt {{" ".alt {{"
" color: rgb({kColR},{kColG},{kColB});" " color: rgb({kColR}, {kColG}, {kColB});"
"}}\n" "}}\n"
).format( ).format(
hColR=SHARED.theme.colHead[0], hColR=colHead.red(),
hColG=SHARED.theme.colHead[1], hColG=colHead.green(),
hColB=SHARED.theme.colHead[2], hColB=colHead.blue(),
kColR=SHARED.theme.colKey[0], kColR=colKey.red(),
kColG=SHARED.theme.colKey[1], kColG=colKey.green(),
kColB=SHARED.theme.colKey[2], kColB=colKey.blue(),
) )
self.pageAbout.document().setDefaultStyleSheet(styleSheet) self.pageAbout.document().setDefaultStyleSheet(styleSheet)
self.pageNotes.document().setDefaultStyleSheet(styleSheet) self.pageNotes.document().setDefaultStyleSheet(styleSheet)
+5 -4
View File
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.configlayout import NHelpLabel from novelwriter.extensions.configlayout import NColourLabel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -54,9 +54,10 @@ class GuiDocMerge(QDialog):
self._data = {} self._data = {}
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge"))) self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
self.helpLabel = NHelpLabel(self.tr( self.helpLabel = NColourLabel(
"Drag and drop items to change the order, or uncheck to exclude." self.tr("Drag and drop items to change the order, or uncheck to exclude."),
), SHARED.theme.helpText) SHARED.theme.helpText, parent=self, wrap=True
)
iPx = SHARED.theme.baseIconSize iPx = SHARED.theme.baseIconSize
hSp = CONFIG.pxInt(12) hSp = CONFIG.pxInt(12)
+3 -3
View File
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.configlayout import NHelpLabel from novelwriter.extensions.configlayout import NColourLabel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -58,9 +58,9 @@ class GuiDocSplit(QDialog):
self.setWindowTitle(self.tr("Split Document")) self.setWindowTitle(self.tr("Split Document"))
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers"))) self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers")))
self.helpLabel = NHelpLabel( self.helpLabel = NColourLabel(
self.tr("Select the maximum level to split into files."), self.tr("Select the maximum level to split into files."),
SHARED.theme.helpText SHARED.theme.helpText, parent=self, wrap=True
) )
# Values # Values
+16 -22
View File
@@ -26,11 +26,11 @@ from __future__ import annotations
import logging import logging
from PyQt5.QtGui import QCloseEvent, QColor, QFont, QKeyEvent, QKeySequence, QPalette from PyQt5.QtGui import QCloseEvent, QFont, QKeyEvent, QKeySequence
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractButton, QComboBox, QCompleter, QDialog, QDialogButtonBox, QAbstractButton, QComboBox, QCompleter, QDialog, QDialogButtonBox,
QDoubleSpinBox, QFileDialog, QFontDialog, QHBoxLayout, QLabel, QLineEdit, QDoubleSpinBox, QFileDialog, QFontDialog, QHBoxLayout, QLineEdit,
QPushButton, QSpinBox, QToolButton, QVBoxLayout, QWidget, qApp QPushButton, QSpinBox, QToolButton, QVBoxLayout, QWidget, qApp
) )
@@ -38,7 +38,7 @@ from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwConst, nwUnicode from novelwriter.constants import nwConst, nwUnicode
from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.dialogs.quotes import GuiQuoteSelect
from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.configlayout import NScrollableForm from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm
from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.pagedsidebar import NPagedSideBar
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -58,21 +58,10 @@ class GuiPreferences(QDialog):
self.resize(*CONFIG.preferencesWinSize) self.resize(*CONFIG.preferencesWinSize)
# Title # Title
font = self.font() self.titleLabel = NColourLabel(
font.setPointSizeF(1.25*SHARED.theme.fontPointSize) self.tr("Preferences"), SHARED.theme.helpText,
parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
palette = self.palette() )
palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.helpText))
self.titleLabel = QLabel(self.tr("Preferences"), self)
self.titleLabel.setFont(font)
self.titleLabel.setPalette(palette)
self.titleLabel.setIndent(CONFIG.pxInt(4))
# SideBar
self.sidebar = NPagedSideBar(self)
self.sidebar.setLabelColor(SHARED.theme.helpText)
self.sidebar.buttonClicked.connect(self._sidebarClicked)
# Search Box # Search Box
self.searchText = QLineEdit(self) self.searchText = QLineEdit(self)
@@ -83,10 +72,10 @@ class GuiPreferences(QDialog):
) )
self.searchAction.triggered.connect(self._gotoSearch) self.searchAction.triggered.connect(self._gotoSearch)
self.searchBox = QHBoxLayout() # SideBar
self.searchBox.addWidget(self.titleLabel) self.sidebar = NPagedSideBar(self)
self.searchBox.addStretch(1) self.sidebar.setLabelColor(SHARED.theme.helpText)
self.searchBox.addWidget(self.searchText, 1) self.sidebar.buttonClicked.connect(self._sidebarClicked)
# Form # Form
self.mainForm = NScrollableForm(self) self.mainForm = NScrollableForm(self)
@@ -101,6 +90,11 @@ class GuiPreferences(QDialog):
self.buttonBox.clicked.connect(self._dialogButtonClicked) self.buttonBox.clicked.connect(self._dialogButtonClicked)
# Assemble # Assemble
self.searchBox = QHBoxLayout()
self.searchBox.addWidget(self.titleLabel)
self.searchBox.addStretch(1)
self.searchBox.addWidget(self.searchText, 1)
self.mainBox = QHBoxLayout() self.mainBox = QHBoxLayout()
self.mainBox.addWidget(self.sidebar) self.mainBox.addWidget(self.sidebar)
self.mainBox.addWidget(self.mainForm) self.mainBox.addWidget(self.mainForm)
-518
View File
@@ -1,518 +0,0 @@
"""
novelWriter GUI Project Details
=================================
File History:
Created: 2021-01-03 [1.1rc1] GuiProjectDetails
This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import math
import logging
from PyQt5.QtGui import QCloseEvent, QFont
from PyQt5.QtCore import Qt, QSize, pyqtSlot
from PyQt5.QtWidgets import (
QAbstractItemView, QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel,
QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import formatTime, numberToRoman
from novelwriter.constants import nwUnicode
from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.pageddialog import NPagedDialog
from novelwriter.extensions.novelselector import NovelSelector
logger = logging.getLogger(__name__)
class GuiProjectDetails(NPagedDialog):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
logger.debug("Create: GuiProjectDetails")
self.setObjectName("GuiProjectDetails")
self.setWindowTitle(self.tr("Project Details"))
wW = CONFIG.pxInt(600)
wH = CONFIG.pxInt(400)
pOptions = SHARED.project.options
self.setMinimumWidth(wW)
self.setMinimumHeight(wH)
self.resize(
CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "winWidth", wW)),
CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "winHeight", wH))
)
self.tabMain = GuiProjectDetailsMain(self)
self.tabContents = GuiProjectDetailsContents(self)
self.addTab(self.tabMain, self.tr("Overview"))
self.addTab(self.tabContents, self.tr("Contents"))
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
self.buttonBox.rejected.connect(self.close)
self.rejected.connect(self.close)
self.addControls(self.buttonBox)
logger.debug("Ready: GuiProjectDetails")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiProjectDetails")
return
def updateValues(self) -> None:
"""Set all the values of the pages."""
self.tabMain.updateValues()
self.tabContents.updateValues()
return
##
# Events
##
def closeEvent(self, event: QCloseEvent) -> None:
"""Capture the close event and perform cleanup."""
self._saveGuiSettings()
event.accept()
self.deleteLater()
return
##
# Internal Functions
##
def _saveGuiSettings(self) -> None:
"""Save GUI settings."""
winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height())
cColWidth = self.tabContents.getColumnSizes()
widthCol0 = CONFIG.rpxInt(cColWidth[0])
widthCol1 = CONFIG.rpxInt(cColWidth[1])
widthCol2 = CONFIG.rpxInt(cColWidth[2])
widthCol3 = CONFIG.rpxInt(cColWidth[3])
widthCol4 = CONFIG.rpxInt(cColWidth[4])
wordsPerPage = self.tabContents.wpValue.value()
countFrom = self.tabContents.poValue.value()
clearDouble = self.tabContents.dblValue.isChecked()
logger.debug("Saving State: GuiProjectDetails")
pOptions = SHARED.project.options
pOptions.setValue("GuiProjectDetails", "winWidth", winWidth)
pOptions.setValue("GuiProjectDetails", "winHeight", winHeight)
pOptions.setValue("GuiProjectDetails", "widthCol0", widthCol0)
pOptions.setValue("GuiProjectDetails", "widthCol1", widthCol1)
pOptions.setValue("GuiProjectDetails", "widthCol2", widthCol2)
pOptions.setValue("GuiProjectDetails", "widthCol3", widthCol3)
pOptions.setValue("GuiProjectDetails", "widthCol4", widthCol4)
pOptions.setValue("GuiProjectDetails", "wordsPerPage", wordsPerPage)
pOptions.setValue("GuiProjectDetails", "countFrom", countFrom)
pOptions.setValue("GuiProjectDetails", "clearDouble", clearDouble)
return
# END Class GuiProjectDetails
class GuiProjectDetailsMain(QWidget):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
fPx = SHARED.theme.fontPixelSize
fPt = SHARED.theme.fontPointSize
vPx = CONFIG.pxInt(4)
hPx = CONFIG.pxInt(12)
# Header
# ======
self.bookTitle = QLabel("")
bookFont = self.bookTitle.font()
bookFont.setPointSizeF(2.2*fPt)
bookFont.setWeight(QFont.Bold)
self.bookTitle.setFont(bookFont)
self.bookTitle.setAlignment(Qt.AlignHCenter)
self.bookTitle.setWordWrap(True)
self.projName = QLabel("")
workFont = self.projName.font()
workFont.setPointSizeF(0.8*fPt)
workFont.setItalic(True)
self.projName.setFont(workFont)
self.projName.setAlignment(Qt.AlignHCenter)
self.projName.setWordWrap(True)
self.bookAuthors = QLabel("")
authFont = self.bookAuthors.font()
authFont.setPointSizeF(1.2*fPt)
self.bookAuthors.setFont(authFont)
self.bookAuthors.setAlignment(Qt.AlignHCenter)
self.bookAuthors.setWordWrap(True)
# Stats
# =====
self.wordCountLbl = QLabel("<b>%s:</b>" % self.tr("Words"))
self.wordCountVal = QLabel("")
self.chapCountLbl = QLabel("<b>%s:</b>" % self.tr("Chapters"))
self.chapCountVal = QLabel("")
self.sceneCountLbl = QLabel("<b>%s:</b>" % self.tr("Scenes"))
self.sceneCountVal = QLabel("")
self.revCountLbl = QLabel("<b>%s:</b>" % self.tr("Revisions"))
self.revCountVal = QLabel("")
self.editTimeLbl = QLabel("<b>%s:</b>" % self.tr("Editing Time"))
self.editTimeVal = QLabel("")
self.statsGrid = QGridLayout()
self.statsGrid.addWidget(self.wordCountLbl, 0, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.wordCountVal, 0, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.addWidget(self.chapCountLbl, 1, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.chapCountVal, 1, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.addWidget(self.sceneCountLbl, 2, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.sceneCountVal, 2, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.addWidget(self.revCountLbl, 3, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.revCountVal, 3, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.addWidget(self.editTimeLbl, 4, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.editTimeVal, 4, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.setHorizontalSpacing(hPx)
self.statsGrid.setVerticalSpacing(vPx)
# Meta
# ====
self.projPathLbl = QLabel("<b>%s:</b>" % self.tr("Path"))
self.projPathVal = QLineEdit()
self.projPathVal.setReadOnly(True)
self.projPathBox = QHBoxLayout()
self.projPathBox.addWidget(self.projPathLbl)
self.projPathBox.addWidget(self.projPathVal)
self.projPathBox.setSpacing(hPx)
# Assemble
# ========
self.outerBox = QVBoxLayout()
self.outerBox.addSpacing(fPx)
self.outerBox.addWidget(self.bookTitle)
self.outerBox.addWidget(self.projName)
self.outerBox.addWidget(self.bookAuthors)
self.outerBox.addSpacing(2*fPx)
self.outerBox.addLayout(self.statsGrid)
self.outerBox.addSpacing(fPx)
self.outerBox.addStretch(1)
self.outerBox.addLayout(self.projPathBox)
self.setLayout(self.outerBox)
return
def updateValues(self) -> None:
"""Set all the values."""
project = SHARED.project
pIndex = project.index
hCounts = pIndex.getNovelTitleCounts()
nwCount = pIndex.getNovelWordCount()
edTime = project.currentEditTime
self.bookTitle.setText(project.data.title or project.data.name)
self.projName.setText(self.tr("Project: {0}").format(project.data.name))
self.bookAuthors.setText(self.tr("By {0}").format(project.data.author))
self.wordCountVal.setText(f"{nwCount:n}")
self.chapCountVal.setText(f"{hCounts[2]:n}")
self.sceneCountVal.setText(f"{hCounts[3]:n}")
self.revCountVal.setText(f"{project.data.saveCount:n}")
self.editTimeVal.setText(formatTime(edTime))
self.projPathVal.setText(str(project.storage.storagePath))
return
# END Class GuiProjectDetailsMain
class GuiProjectDetailsContents(QWidget):
C_TITLE = 0
C_WORDS = 1
C_PAGES = 2
C_PAGE = 3
C_PROG = 4
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
# Internal
self._theToC = []
self._currentRoot = None
iPx = SHARED.theme.baseIconSize
hPx = CONFIG.pxInt(12)
vPx = CONFIG.pxInt(4)
pOptions = SHARED.project.options
# Header
# ======
self.tocLabel = QLabel("<b>%s</b>" % self.tr("Table of Contents"))
self.novelValue = NovelSelector(self)
self.novelValue.setMinimumWidth(CONFIG.pxInt(200))
self.novelValue.novelSelectionChanged.connect(self._novelValueChanged)
self.headBox = QHBoxLayout()
self.headBox.addWidget(self.tocLabel)
self.headBox.addWidget(self.novelValue)
# Contents Tree
# =============
self.tocTree = QTreeWidget()
self.tocTree.setIconSize(QSize(iPx, iPx))
self.tocTree.setIndentation(0)
self.tocTree.setColumnCount(6)
self.tocTree.setSelectionMode(QAbstractItemView.NoSelection)
self.tocTree.setHeaderLabels([
self.tr("Title"),
self.tr("Words"),
self.tr("Pages"),
self.tr("Page"),
self.tr("Progress"),
""
])
treeHeadItem = self.tocTree.headerItem()
if treeHeadItem:
treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignRight)
treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignRight)
treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignRight)
treeHeader = self.tocTree.header()
treeHeader.setStretchLastSection(True)
treeHeader.setMinimumSectionSize(hPx)
wCol0 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol0", 200))
wCol1 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol1", 60))
wCol2 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol2", 60))
wCol3 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol3", 60))
wCol4 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol4", 90))
self.tocTree.setColumnWidth(0, wCol0)
self.tocTree.setColumnWidth(1, wCol1)
self.tocTree.setColumnWidth(2, wCol2)
self.tocTree.setColumnWidth(3, wCol3)
self.tocTree.setColumnWidth(4, wCol4)
self.tocTree.setColumnWidth(5, hPx)
# Options
# =======
wordsPerPage = pOptions.getInt("GuiProjectDetails", "wordsPerPage", 350)
countFrom = pOptions.getInt("GuiProjectDetails", "countFrom", 1)
clearDouble = pOptions.getBool("GuiProjectDetails", "clearDouble", True)
wordsHelp = (
self.tr("Typical word count for a 5 by 8 inch book page with 11 pt font is 350.")
)
offsetHelp = (
self.tr("Start counting page numbers from this page.")
)
dblHelp = (
self.tr("Assume a new chapter or partition always start on an odd numbered page.")
)
self.wpLabel = QLabel(self.tr("Words per page"))
self.wpLabel.setToolTip(wordsHelp)
self.wpValue = QSpinBox()
self.wpValue.setMinimum(10)
self.wpValue.setMaximum(1000)
self.wpValue.setSingleStep(10)
self.wpValue.setValue(wordsPerPage)
self.wpValue.setToolTip(wordsHelp)
self.wpValue.valueChanged.connect(self._populateTree)
self.poLabel = QLabel(self.tr("Count pages from"))
self.poLabel.setToolTip(offsetHelp)
self.poValue = QSpinBox()
self.poValue.setMinimum(1)
self.poValue.setMaximum(9999)
self.poValue.setSingleStep(1)
self.poValue.setValue(countFrom)
self.poValue.setToolTip(offsetHelp)
self.poValue.valueChanged.connect(self._populateTree)
self.dblLabel = QLabel(self.tr("Clear double pages"))
self.dblLabel.setToolTip(dblHelp)
self.dblValue = NSwitch(self, 2*iPx, iPx)
self.dblValue.setChecked(clearDouble)
self.dblValue.setToolTip(dblHelp)
self.dblValue.clicked.connect(self._populateTree)
self.optionsBox = QGridLayout()
self.optionsBox.addWidget(self.wpLabel, 0, 0)
self.optionsBox.addWidget(self.wpValue, 0, 1)
self.optionsBox.addWidget(self.dblLabel, 0, 3)
self.optionsBox.addWidget(self.dblValue, 0, 4)
self.optionsBox.addWidget(self.poLabel, 1, 0)
self.optionsBox.addWidget(self.poValue, 1, 1)
self.optionsBox.setHorizontalSpacing(hPx)
self.optionsBox.setVerticalSpacing(vPx)
self.optionsBox.setColumnStretch(2, 1)
# Assemble
# ========
self.outerBox = QVBoxLayout()
self.outerBox.addLayout(self.headBox)
self.outerBox.addWidget(self.tocTree)
self.outerBox.addLayout(self.optionsBox)
self.setLayout(self.outerBox)
return
def getColumnSizes(self) -> list[int]:
"""Return the column widths for the tree columns."""
retVals = [
self.tocTree.columnWidth(0),
self.tocTree.columnWidth(1),
self.tocTree.columnWidth(2),
self.tocTree.columnWidth(3),
self.tocTree.columnWidth(4),
]
return retVals
def updateValues(self) -> None:
"""Populate the tree."""
self._currentRoot = None
self.novelValue.updateList()
self.novelValue.setHandle(self.novelValue.firstHandle)
self._prepareData(self.novelValue.firstHandle)
self._populateTree()
return
##
# Internal Functions
##
def _prepareData(self, rootHandle: str | None) -> None:
"""Extract the information from the project index."""
logger.debug("Populating ToC from handle '%s'", rootHandle)
self._theToC = SHARED.project.index.getTableOfContents(rootHandle, 2)
self._theToC.append(("", 0, self.tr("END"), 0))
return
##
# Slots
##
@pyqtSlot(str)
def _novelValueChanged(self, tHandle: str) -> None:
"""Refresh the tree with another root item."""
if tHandle != self._currentRoot:
self._prepareData(tHandle)
self._populateTree()
self._currentRoot = self.novelValue.handle
return
@pyqtSlot()
def _populateTree(self) -> None:
"""Set the content of the chapter/page tree."""
dblPages = self.dblValue.isChecked()
wpPage = self.wpValue.value()
fstPage = self.poValue.value() - 1
pTotal = 0
tPages = 1
theList = []
for _, tLevel, tTitle, wCount in self._theToC:
pCount = math.ceil(wCount/wpPage)
if dblPages:
pCount += pCount%2
pTotal += pCount
theList.append((tLevel, tTitle, wCount, pCount))
pMax = pTotal - fstPage
self.tocTree.clear()
for tLevel, tTitle, wCount, pCount in theList:
newItem = QTreeWidgetItem()
if tPages <= fstPage:
progPage = numberToRoman(tPages, True)
progText = ""
else:
cPage = tPages - fstPage
pgProg = 100.0*(cPage - 1)/pMax if pMax > 0 else 0.0
progPage = f"{cPage:n}"
progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"
hDec = SHARED.theme.getHeaderDecoration(tLevel)
if tTitle.strip() == "":
tTitle = self.tr("Untitled")
newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
newItem.setText(self.C_TITLE, tTitle)
newItem.setText(self.C_WORDS, f"{wCount:n}")
newItem.setText(self.C_PAGES, f"{pCount:n}")
newItem.setText(self.C_PAGE, progPage)
newItem.setText(self.C_PROG, progText)
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
newItem.setTextAlignment(self.C_PAGES, Qt.AlignRight)
newItem.setTextAlignment(self.C_PAGE, Qt.AlignRight)
newItem.setTextAlignment(self.C_PROG, Qt.AlignRight)
# Make pages and titles/partitions stand out
if tLevel < 2:
bFont = newItem.font(self.C_TITLE)
if tLevel == 0:
bFont.setItalic(True)
else:
bFont.setBold(True)
bFont.setUnderline(True)
newItem.setFont(self.C_TITLE, bFont)
tPages += pCount
self.tocTree.addTopLevelItem(newItem)
return
# END Class GuiProjectDetailsContents
@@ -3,7 +3,8 @@ novelWriter GUI Project Settings
================================== ==================================
File History: File History:
Created: 2018-09-29 [0.0.1] GuiProjectSettings Created: 2018-09-29 [0.0.1] GuiProjectSettings
Rewritten: 2024-01-26 [2.3b1] GuiProjectSettings
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen Copyright 20182024, Veronica Berglyd Olsen
@@ -25,75 +26,101 @@ from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING from PyQt5.QtGui import QCloseEvent, QColor, QIcon, QPixmap
from PyQt5.QtGui import QCloseEvent, QIcon, QPixmap, QColor
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QColorDialog, QComboBox, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QColorDialog, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QLineEdit,
QPushButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, qApp QPushButton, QStackedWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
QWidget, qApp
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import simplified from novelwriter.common import simplified
from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.pageddialog import NPagedDialog from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollableForm
from novelwriter.extensions.configlayout import NConfigLayout from novelwriter.extensions.pagedsidebar import NPagedSideBar
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuiProjectSettings(NPagedDialog): class GuiProjectSettings(QDialog):
TAB_MAIN = 0 PAGE_SETTINGS = 0
TAB_STATUS = 1 PAGE_STATUS = 1
TAB_IMPORT = 2 PAGE_IMPORT = 2
TAB_REPLACE = 3 PAGE_REPLACE = 3
newProjectSettingsReady = pyqtSignal() newProjectSettingsReady = pyqtSignal(bool)
def __init__(self, mainGui: GuiMain, focusTab: int = TAB_MAIN) -> None: def __init__(self, parent: QWidget, gotoPage: int = PAGE_SETTINGS) -> None:
super().__init__(parent=mainGui) super().__init__(parent=parent)
logger.debug("Create: GuiProjectSettings") logger.debug("Create: GuiProjectSettings")
self.setObjectName("GuiProjectSettings") self.setObjectName("GuiProjectSettings")
self.mainGui = mainGui
SHARED.project.countStatus()
self.setWindowTitle(self.tr("Project Settings")) self.setWindowTitle(self.tr("Project Settings"))
wW = CONFIG.pxInt(570) options = SHARED.project.options
wH = CONFIG.pxInt(375) self.setMinimumSize(CONFIG.pxInt(500), CONFIG.pxInt(400))
pOptions = SHARED.project.options
self.setMinimumWidth(wW)
self.setMinimumHeight(wH)
self.resize( self.resize(
CONFIG.pxInt(pOptions.getInt("GuiProjectSettings", "winWidth", wW)), CONFIG.pxInt(options.getInt("GuiProjectSettings", "winWidth", CONFIG.pxInt(650))),
CONFIG.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH)) CONFIG.pxInt(options.getInt("GuiProjectSettings", "winHeight", CONFIG.pxInt(500)))
) )
self.tabMain = GuiProjectEditMain(self) # Title
self.tabStatus = GuiProjectEditStatus(self, True) self.titleLabel = NColourLabel(
self.tabImport = GuiProjectEditStatus(self, False) self.tr("Project Settings"), SHARED.theme.helpText,
self.tabReplace = GuiProjectEditReplace(self) parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
)
self.addTab(self.tabMain, self.tr("Settings")) # SideBar
self.addTab(self.tabStatus, self.tr("Status")) self.sidebar = NPagedSideBar(self)
self.addTab(self.tabImport, self.tr("Importance")) self.sidebar.setLabelColor(SHARED.theme.helpText)
self.addTab(self.tabReplace, self.tr("Auto-Replace")) self.sidebar.addButton(self.tr("Settings"), self.PAGE_SETTINGS)
self.sidebar.addButton(self.tr("Status"), self.PAGE_STATUS)
self.sidebar.addButton(self.tr("Importance"), self.PAGE_IMPORT)
self.sidebar.addButton(self.tr("Auto-Replace"), self.PAGE_REPLACE)
self.sidebar.setSelected(gotoPage)
self.sidebar.buttonClicked.connect(self._sidebarClicked)
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) # Buttons
self.buttonBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel
)
self.buttonBox.accepted.connect(self._doSave) self.buttonBox.accepted.connect(self._doSave)
self.buttonBox.rejected.connect(self.close) self.buttonBox.rejected.connect(self.close)
self.rejected.connect(self.close)
self.addControls(self.buttonBox)
# Focus Tab # Content
self._focusTab(focusTab) SHARED.project.countStatus()
self.settingsPage = _SettingsPage(self)
self.statusPage = _StatusPage(self, True)
self.importPage = _StatusPage(self, False)
self.replacePage = _ReplacePage(self)
self.mainStack = QStackedWidget(self)
self.mainStack.addWidget(self.settingsPage)
self.mainStack.addWidget(self.statusPage)
self.mainStack.addWidget(self.importPage)
self.mainStack.addWidget(self.replacePage)
# Assemble
self.topBox = QHBoxLayout()
self.topBox.addWidget(self.titleLabel)
self.topBox.addStretch(1)
self.mainBox = QHBoxLayout()
self.mainBox.addWidget(self.sidebar)
self.mainBox.addWidget(self.mainStack)
self.mainBox.setContentsMargins(0, 0, 0, 0)
self.outerBox = QVBoxLayout()
self.outerBox.addLayout(self.topBox)
self.outerBox.addLayout(self.mainBox)
self.outerBox.addWidget(self.buttonBox)
self.outerBox.setSpacing(CONFIG.pxInt(8))
self.setLayout(self.outerBox)
self.setSizeGripEnabled(True)
logger.debug("Ready: GuiProjectSettings") logger.debug("Ready: GuiProjectSettings")
@@ -103,9 +130,13 @@ class GuiProjectSettings(NPagedDialog):
logger.debug("Delete: GuiProjectSettings") logger.debug("Delete: GuiProjectSettings")
return return
##
# Events
##
def closeEvent(self, event: QCloseEvent) -> None: def closeEvent(self, event: QCloseEvent) -> None:
"""Capture the close event and perform cleanup.""" """Capture the user closing the window and save settings."""
self._saveGuiSettings() self._saveSettings()
event.accept() event.accept()
self.deleteLater() self.deleteLater()
return return
@@ -114,40 +145,52 @@ class GuiProjectSettings(NPagedDialog):
# Private Slots # Private Slots
## ##
@pyqtSlot(int)
def _sidebarClicked(self, pageId: int) -> None:
"""Process a user request to switch page."""
if pageId == self.PAGE_SETTINGS:
self.mainStack.setCurrentWidget(self.settingsPage)
elif pageId == self.PAGE_STATUS:
self.mainStack.setCurrentWidget(self.statusPage)
elif pageId == self.PAGE_IMPORT:
self.mainStack.setCurrentWidget(self.importPage)
elif pageId == self.PAGE_REPLACE:
self.mainStack.setCurrentWidget(self.replacePage)
return
@pyqtSlot() @pyqtSlot()
def _doSave(self) -> None: def _doSave(self) -> None:
"""Save settings and close dialog.""" """Save settings and close dialog."""
project = SHARED.project project = SHARED.project
projName = self.tabMain.editName.text() projName = self.settingsPage.projName.text()
bookTitle = self.tabMain.editTitle.text() projAuthor = self.settingsPage.projAuthor.text()
bookAuthor = self.tabMain.editAuthor.text() projLang = self.settingsPage.projLang.currentData()
projLang = self.tabMain.projLang.currentData() spellLang = self.settingsPage.spellLang.currentData()
spellLang = self.tabMain.spellLang.currentData() doBackup = not self.settingsPage.doBackup.isChecked()
doBackup = not self.tabMain.doBackup.isChecked()
project.data.setName(projName) project.data.setName(projName)
project.data.setTitle(bookTitle) project.data.setAuthor(projAuthor)
project.data.setAuthor(bookAuthor)
project.data.setDoBackup(doBackup) project.data.setDoBackup(doBackup)
project.data.setSpellLang(spellLang) project.data.setSpellLang(spellLang)
project.setProjectLang(projLang) project.setProjectLang(projLang)
if self.tabStatus.colChanged: rebuildTrees = False
newList, delList = self.tabStatus.getNewList()
if self.statusPage.wasChanged:
newList, delList = self.statusPage.getNewList()
project.setStatusColours(newList, delList) project.setStatusColours(newList, delList)
rebuildTrees = True
if self.tabImport.colChanged: if self.importPage.wasChanged:
newList, delList = self.tabImport.getNewList() newList, delList = self.importPage.getNewList()
project.setImportColours(newList, delList) project.setImportColours(newList, delList)
rebuildTrees = True
if self.tabStatus.colChanged or self.tabImport.colChanged: if self.replacePage.wasChanged:
self.mainGui.rebuildTrees() newList = self.replacePage.getNewList()
if self.tabReplace.arChanged:
newList = self.tabReplace.getNewList()
project.data.setAutoReplace(newList) project.data.setAutoReplace(newList)
self.newProjectSettingsReady.emit() self.newProjectSettingsReady.emit(rebuildTrees)
qApp.processEvents() qApp.processEvents()
self.close() self.close()
@@ -157,134 +200,103 @@ class GuiProjectSettings(NPagedDialog):
# Internal Functions # Internal Functions
## ##
def _focusTab(self, tab: int) -> None: def _saveSettings(self) -> None:
"""Change which is the focused tab."""
if tab == self.TAB_MAIN:
self.setCurrentWidget(self.tabMain)
elif tab == self.TAB_STATUS:
self.setCurrentWidget(self.tabStatus)
elif tab == self.TAB_IMPORT:
self.setCurrentWidget(self.tabImport)
elif tab == self.TAB_REPLACE:
self.setCurrentWidget(self.tabReplace)
return
def _saveGuiSettings(self) -> None:
"""Save GUI settings.""" """Save GUI settings."""
winWidth = CONFIG.rpxInt(self.width()) winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height()) winHeight = CONFIG.rpxInt(self.height())
replaceColW = CONFIG.rpxInt(self.tabReplace.listBox.columnWidth(0)) statusColW = CONFIG.rpxInt(self.statusPage.columnWidth())
statusColW = CONFIG.rpxInt(self.tabStatus.listBox.columnWidth(0)) importColW = CONFIG.rpxInt(self.importPage.columnWidth())
importColW = CONFIG.rpxInt(self.tabImport.listBox.columnWidth(0)) replaceColW = CONFIG.rpxInt(self.replacePage.columnWidth())
logger.debug("Saving State: GuiProjectSettings") logger.debug("Saving State: GuiProjectSettings")
pOptions = SHARED.project.options options = SHARED.project.options
pOptions.setValue("GuiProjectSettings", "winWidth", winWidth) options.setValue("GuiProjectSettings", "winWidth", winWidth)
pOptions.setValue("GuiProjectSettings", "winHeight", winHeight) options.setValue("GuiProjectSettings", "winHeight", winHeight)
pOptions.setValue("GuiProjectSettings", "replaceColW", replaceColW) options.setValue("GuiProjectSettings", "statusColW", statusColW)
pOptions.setValue("GuiProjectSettings", "statusColW", statusColW) options.setValue("GuiProjectSettings", "importColW", importColW)
pOptions.setValue("GuiProjectSettings", "importColW", importColW) options.setValue("GuiProjectSettings", "replaceColW", replaceColW)
return return
# END Class GuiProjectSettings # END Class GuiProjectSettings
class GuiProjectEditMain(QWidget): class _SettingsPage(NScrollableForm):
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
# The Form xW = CONFIG.pxInt(200)
self.mainForm = NConfigLayout() data = SHARED.project.data
self.mainForm.setHelpTextStyle(SHARED.theme.helpText) self.setHelpTextStyle(SHARED.theme.helpText)
self.setLayout(self.mainForm) self.setRowIndent(0)
self.mainForm.addGroupLabel(self.tr("Project Settings")) # Project Name
self.projName = QLineEdit(self)
xW = CONFIG.pxInt(250) self.projName.setMaxLength(200)
pData = SHARED.project.data self.projName.setMinimumWidth(xW)
self.projName.setText(data.name)
self.editName = QLineEdit() self.addRow(
self.editName.setMaxLength(200) self.tr("Project name"), self.projName,
self.editName.setMaximumWidth(xW) self.tr("Changing this will affect the backup path."),
self.editName.setText(pData.name) stretch=(3, 2)
self.mainForm.addRow(
self.tr("Project name"),
self.editName,
self.tr("Should be set only once.")
) )
self.editTitle = QLineEdit() # Project Author
self.editTitle.setMaxLength(200) self.projAuthor = QLineEdit(self)
self.editTitle.setMaximumWidth(xW) self.projAuthor.setMaxLength(200)
self.editTitle.setText(pData.title) self.projAuthor.setMinimumWidth(xW)
self.mainForm.addRow( self.projAuthor.setText(data.author)
self.tr("Novel title"), self.addRow(
self.editTitle, self.tr("Author(s)"), self.projAuthor,
self.tr("Change whenever you want!") self.tr("Only used when building the manuscript."),
) stretch=(3, 2)
self.editAuthor = QLineEdit()
self.editAuthor.setMaxLength(200)
self.editAuthor.setMaximumWidth(xW)
self.editAuthor.setText(pData.author)
self.mainForm.addRow(
self.tr("Author(s)"),
self.editAuthor,
self.tr("Change whenever you want!")
) )
# Project Language
self.projLang = QComboBox(self) self.projLang = QComboBox(self)
self.projLang.setMaximumWidth(xW) self.projLang.setMinimumWidth(xW)
for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ): for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ):
self.projLang.addItem(language, tag) self.projLang.addItem(language, tag)
self.mainForm.addRow( self.addRow(
self.tr("Project language"), self.tr("Project language"), self.projLang,
self.projLang, self.tr("Only used when building the manuscript."),
self.tr("Used when building the manuscript.") stretch=(3, 2)
) )
if (idx := self.projLang.findData(data.language)) != -1:
self.projLang.setCurrentIndex(idx)
langIdx = 0 # Spell Check Language
if pData.language is not None:
langIdx = self.projLang.findData(pData.language)
if langIdx == -1:
langIdx = self.projLang.findData("en_GB")
if langIdx != -1:
self.projLang.setCurrentIndex(langIdx)
self.spellLang = QComboBox(self) self.spellLang = QComboBox(self)
self.spellLang.setMaximumWidth(xW) self.spellLang.setMinimumWidth(xW)
self.spellLang.addItem(self.tr("Default"), "None") self.spellLang.addItem(self.tr("Default"), "None")
if CONFIG.hasEnchant: if CONFIG.hasEnchant:
for tag, language in SHARED.spelling.listDictionaries(): for tag, language in SHARED.spelling.listDictionaries():
self.spellLang.addItem(language, tag) self.spellLang.addItem(language, tag)
self.mainForm.addRow( self.addRow(
self.tr("Spell check language"), self.tr("Spell check language"), self.spellLang,
self.spellLang, self.tr("Overrides main preferences."),
self.tr("Overrides main preferences.") stretch=(3, 2)
) )
if (idx := self.spellLang.findData(data.spellLang)) != -1:
self.spellLang.setCurrentIndex(idx)
langIdx = 0 # Backup on Close
if pData.spellLang is not None:
langIdx = self.spellLang.findData(pData.spellLang)
if langIdx != -1:
self.spellLang.setCurrentIndex(langIdx)
self.doBackup = NSwitch(self) self.doBackup = NSwitch(self)
self.doBackup.setChecked(not pData.doBackup) self.doBackup.setChecked(not data.doBackup)
self.mainForm.addRow( self.addRow(
self.tr("No backup on close"), self.tr("Disable backup on close"), self.doBackup,
self.doBackup,
self.tr("Overrides main preferences.") self.tr("Overrides main preferences.")
) )
self.finalise()
return return
# END Class GuiProjectEditMain # END Class _SettingsPage
class GuiProjectEditStatus(QWidget): class _StatusPage(NFixedPage):
COL_LABEL = 0 COL_LABEL = 0
COL_USAGE = 1 COL_USAGE = 1
@@ -297,56 +309,54 @@ class GuiProjectEditStatus(QWidget):
super().__init__(parent=parent) super().__init__(parent=parent)
if isStatus: if isStatus:
self.theStatus = SHARED.project.data.itemStatus status = SHARED.project.data.itemStatus
pageLabel = self.tr("Novel File Status Levels") pageLabel = self.tr("Novel Document Status Levels")
colSetting = "statusColW" colSetting = "statusColW"
else: else:
self.theStatus = SHARED.project.data.itemImport status = SHARED.project.data.itemImport
pageLabel = self.tr("Note File Importance Levels") pageLabel = self.tr("Project Note Importance Levels")
colSetting = "importColW" colSetting = "importColW"
wCol0 = CONFIG.pxInt( wCol0 = CONFIG.pxInt(
SHARED.project.options.getInt("GuiProjectSettings", colSetting, 130) SHARED.project.options.getInt("GuiProjectSettings", colSetting, 130)
) )
self.colDeleted = [] self._changed = False
self.colChanged = False self._colDeleted = []
self.selColour = QColor(100, 100, 100) self._selColour = QColor(100, 100, 100)
self.iPx = SHARED.theme.baseIconSize self.iPx = SHARED.theme.baseIconSize
# The List # Title
# ======== self.pageTitle = NColourLabel(
pageLabel, SHARED.theme.helpText, parent=self,
scale=NColourLabel.HEADER_SCALE
)
self.listBox = QTreeWidget() # List Box
self.listBox.setHeaderLabels([ self.listBox = QTreeWidget(self)
self.tr("Label"), self.tr("Usage"), self.listBox.setHeaderLabels([self.tr("Label"), self.tr("Usage")])
])
self.listBox.itemSelectionChanged.connect(self._selectedItem) self.listBox.itemSelectionChanged.connect(self._selectedItem)
self.listBox.setColumnWidth(self.COL_LABEL, wCol0) self.listBox.setColumnWidth(self.COL_LABEL, wCol0)
self.listBox.setIndentation(0) self.listBox.setIndentation(0)
for key, entry in self.theStatus.items(): for key, entry in status.items():
self._addItem(key, entry["name"], entry["cols"], entry["count"]) self._addItem(key, entry["name"], entry["cols"], entry["count"])
# List Controls # List Controls
# ============= self.addButton = QPushButton(SHARED.theme.getIcon("add"), "", self)
self.addButton = QPushButton(SHARED.theme.getIcon("add"), "")
self.addButton.clicked.connect(self._newItem) self.addButton.clicked.connect(self._newItem)
self.delButton = QPushButton(SHARED.theme.getIcon("remove"), "") self.delButton = QPushButton(SHARED.theme.getIcon("remove"), "", self)
self.delButton.clicked.connect(self._delItem) self.delButton.clicked.connect(self._delItem)
self.upButton = QPushButton(SHARED.theme.getIcon("up"), "") self.upButton = QPushButton(SHARED.theme.getIcon("up"), "", self)
self.upButton.clicked.connect(lambda: self._moveItem(-1)) self.upButton.clicked.connect(lambda: self._moveItem(-1))
self.dnButton = QPushButton(SHARED.theme.getIcon("down"), "") self.dnButton = QPushButton(SHARED.theme.getIcon("down"), "", self)
self.dnButton.clicked.connect(lambda: self._moveItem(1)) self.dnButton.clicked.connect(lambda: self._moveItem(1))
# Edit Form # Edit Form
# =========
self.editName = QLineEdit() self.editName = QLineEdit()
self.editName.setMaxLength(40) self.editName.setMaxLength(40)
self.editName.setPlaceholderText(self.tr("Select item to edit")) self.editName.setPlaceholderText(self.tr("Select item to edit"))
@@ -364,8 +374,6 @@ class GuiProjectEditStatus(QWidget):
self.saveButton.clicked.connect(self._saveItem) self.saveButton.clicked.connect(self._saveItem)
# Assemble # Assemble
# ========
self.listControls = QVBoxLayout() self.listControls = QVBoxLayout()
self.listControls.addWidget(self.addButton) self.listControls.addWidget(self.addButton)
self.listControls.addWidget(self.delButton) self.listControls.addWidget(self.delButton)
@@ -387,16 +395,25 @@ class GuiProjectEditStatus(QWidget):
self.innerBox.addLayout(self.listControls) self.innerBox.addLayout(self.listControls)
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
self.outerBox.addWidget(QLabel("<b>%s</b>" % pageLabel)) self.outerBox.addWidget(self.pageTitle)
self.outerBox.addLayout(self.innerBox) self.outerBox.addLayout(self.innerBox)
self.setLayout(self.outerBox) self.setCentralLayout(self.outerBox)
return return
@property
def wasChanged(self) -> bool:
"""The user changed these settings."""
return self._changed
##
# Methods
##
def getNewList(self) -> tuple[list, list]: def getNewList(self) -> tuple[list, list]:
"""Return list of entries.""" """Return list of entries."""
if self.colChanged: if self._changed:
newList = [] newList = []
for n in range(self.listBox.topLevelItemCount()): for n in range(self.listBox.topLevelItemCount()):
item = self.listBox.topLevelItem(n) item = self.listBox.topLevelItem(n)
@@ -406,10 +423,13 @@ class GuiProjectEditStatus(QWidget):
"name": item.text(self.COL_LABEL), "name": item.text(self.COL_LABEL),
"cols": item.data(self.COL_LABEL, self.COL_ROLE), "cols": item.data(self.COL_LABEL, self.COL_ROLE),
}) })
return newList, self.colDeleted return newList, self._colDeleted
return [], [] return [], []
def columnWidth(self) -> int:
"""Return the size of the header column."""
return self.listBox.columnWidth(0)
## ##
# Private Slots # Private Slots
## ##
@@ -417,12 +437,12 @@ class GuiProjectEditStatus(QWidget):
@pyqtSlot() @pyqtSlot()
def _selectColour(self) -> None: def _selectColour(self) -> None:
"""Open a dialog to select the status icon colour.""" """Open a dialog to select the status icon colour."""
if self.selColour is not None: if self._selColour is not None:
newCol = QColorDialog.getColor( newCol = QColorDialog.getColor(
self.selColour, self, self.tr("Select Colour") self._selColour, self, self.tr("Select Colour")
) )
if newCol.isValid(): if newCol.isValid():
self.selColour = newCol self._selColour = newCol
pixmap = QPixmap(self.iPx, self.iPx) pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(newCol) pixmap.fill(newCol)
self.colButton.setIcon(QIcon(pixmap)) self.colButton.setIcon(QIcon(pixmap))
@@ -433,7 +453,7 @@ class GuiProjectEditStatus(QWidget):
def _newItem(self) -> None: def _newItem(self) -> None:
"""Create a new status item.""" """Create a new status item."""
self._addItem(None, self.tr("New Item"), (100, 100, 100), 0) self._addItem(None, self.tr("New Item"), (100, 100, 100), 0)
self.colChanged = True self._changed = True
return return
@pyqtSlot() @pyqtSlot()
@@ -446,8 +466,8 @@ class GuiProjectEditStatus(QWidget):
SHARED.error(self.tr("Cannot delete a status item that is in use.")) SHARED.error(self.tr("Cannot delete a status item that is in use."))
else: else:
self.listBox.takeTopLevelItem(iRow) self.listBox.takeTopLevelItem(iRow)
self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE)) self._colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE))
self.colChanged = True self._changed = True
return return
@pyqtSlot() @pyqtSlot()
@@ -458,9 +478,9 @@ class GuiProjectEditStatus(QWidget):
selItem.setText(self.COL_LABEL, simplified(self.editName.text())) selItem.setText(self.COL_LABEL, simplified(self.editName.text()))
selItem.setIcon(self.COL_LABEL, self.colButton.icon()) selItem.setIcon(self.COL_LABEL, self.colButton.icon())
selItem.setData(self.COL_LABEL, self.COL_ROLE, ( selItem.setData(self.COL_LABEL, self.COL_ROLE, (
self.selColour.red(), self.selColour.green(), self.selColour.blue() self._selColour.red(), self._selColour.green(), self._selColour.blue()
)) ))
self.colChanged = True self._changed = True
return return
@pyqtSlot() @pyqtSlot()
@@ -474,7 +494,7 @@ class GuiProjectEditStatus(QWidget):
name = selItem.text(self.COL_LABEL) name = selItem.text(self.COL_LABEL)
pixmap = QPixmap(self.iPx, self.iPx) pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(*cols)) pixmap.fill(QColor(*cols))
self.selColour = QColor(*cols) self._selColour = QColor(*cols)
self.editName.setText(name) self.editName.setText(name)
self.colButton.setIcon(QIcon(pixmap)) self.colButton.setIcon(QIcon(pixmap))
self.editName.selectAll() self.editName.selectAll()
@@ -485,7 +505,7 @@ class GuiProjectEditStatus(QWidget):
else: else:
pixmap = QPixmap(self.iPx, self.iPx) pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(100, 100, 100)) pixmap.fill(QColor(100, 100, 100))
self.selColour = QColor(100, 100, 100) self._selColour = QColor(100, 100, 100)
self.editName.setText("") self.editName.setText("")
self.colButton.setIcon(QIcon(pixmap)) self.colButton.setIcon(QIcon(pixmap))
self.editName.setEnabled(False) self.editName.setEnabled(False)
@@ -498,16 +518,16 @@ class GuiProjectEditStatus(QWidget):
## ##
def _addItem(self, key: str | None, name: str, def _addItem(self, key: str | None, name: str,
cols: tuple[int, int, int], count: int) -> None: colour: tuple[int, int, int], count: int) -> None:
"""Add a status item to the list.""" """Add a status item to the list."""
pixmap = QPixmap(self.iPx, self.iPx) pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(*cols)) pixmap.fill(QColor(*colour))
item = QTreeWidgetItem() item = QTreeWidgetItem()
item.setText(self.COL_LABEL, name) item.setText(self.COL_LABEL, name)
item.setIcon(self.COL_LABEL, QIcon(pixmap)) item.setIcon(self.COL_LABEL, QIcon(pixmap))
item.setData(self.COL_LABEL, self.KEY_ROLE, key) item.setData(self.COL_LABEL, self.KEY_ROLE, key)
item.setData(self.COL_LABEL, self.COL_ROLE, cols) item.setData(self.COL_LABEL, self.COL_ROLE, colour)
item.setData(self.COL_LABEL, self.NUM_ROLE, count) item.setData(self.COL_LABEL, self.NUM_ROLE, count)
item.setText(self.COL_USAGE, self._usageString(count)) item.setText(self.COL_USAGE, self._usageString(count))
@@ -533,30 +553,29 @@ class GuiProjectEditStatus(QWidget):
if cItem is not None: if cItem is not None:
cItem.setSelected(True) cItem.setSelected(True)
self.colChanged = True self._changed = True
return return
def _getSelectedItem(self) -> QTreeWidgetItem | None: def _getSelectedItem(self) -> QTreeWidgetItem | None:
"""Get the currently selected item.""" """Get the currently selected item."""
selItem = self.listBox.selectedItems() if items := self.listBox.selectedItems():
if len(selItem) > 0: return items[0]
return selItem[0]
return None return None
def _usageString(self, nUse: int) -> str: def _usageString(self, count: int) -> str:
"""Generate usage string.""" """Generate usage string."""
if nUse == 0: if count == 0:
return self.tr("Not in use") return self.tr("Not in use")
elif nUse == 1: elif count == 1:
return self.tr("Used once") return self.tr("Used once")
else: else:
return self.tr("Used by {0} items").format(nUse) return self.tr("Used by {0} items").format(count)
# END Class GuiProjectEditStatus # END Class _StatusPage
class GuiProjectEditReplace(QWidget): class _ReplacePage(NFixedPage):
COL_KEY = 0 COL_KEY = 0
COL_REPL = 1 COL_REPL = 1
@@ -564,24 +583,24 @@ class GuiProjectEditReplace(QWidget):
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
self.arChanged = False self._changed = False
wCol0 = CONFIG.pxInt( wCol0 = CONFIG.pxInt(
SHARED.project.options.getInt("GuiProjectSettings", "replaceColW", 130) SHARED.project.options.getInt("GuiProjectSettings", "replaceColW", 130)
) )
pageLabel = self.tr("Text Replace List for Preview and Export")
# Title
self.pageTitle = NColourLabel(
self.tr("Text Auto-Replace for Preview and Build"),
SHARED.theme.helpText, parent=self, scale=NColourLabel.HEADER_SCALE
)
# List Box # List Box
# ========
self.listBox = QTreeWidget() self.listBox = QTreeWidget()
self.listBox.setHeaderLabels([ self.listBox.setHeaderLabels([self.tr("Keyword"), self.tr("Replace With")])
self.tr("Keyword"),
self.tr("Replace With"),
])
self.listBox.itemSelectionChanged.connect(self._selectedItem)
self.listBox.setColumnWidth(self.COL_KEY, wCol0) self.listBox.setColumnWidth(self.COL_KEY, wCol0)
self.listBox.setIndentation(0) self.listBox.setIndentation(0)
self.listBox.itemSelectionChanged.connect(self._selectedItem)
for aKey, aVal in SHARED.project.data.autoReplace.items(): for aKey, aVal in SHARED.project.data.autoReplace.items():
newItem = QTreeWidgetItem(["<%s>" % aKey, aVal]) newItem = QTreeWidgetItem(["<%s>" % aKey, aVal])
@@ -591,8 +610,6 @@ class GuiProjectEditReplace(QWidget):
self.listBox.setSortingEnabled(True) self.listBox.setSortingEnabled(True)
# List Controls # List Controls
# =============
self.addButton = QPushButton(SHARED.theme.getIcon("add"), "") self.addButton = QPushButton(SHARED.theme.getIcon("add"), "")
self.addButton.clicked.connect(self._addEntry) self.addButton.clicked.connect(self._addEntry)
@@ -600,8 +617,6 @@ class GuiProjectEditReplace(QWidget):
self.delButton.clicked.connect(self._delEntry) self.delButton.clicked.connect(self._delEntry)
# Edit Form # Edit Form
# =========
self.editKey = QLineEdit() self.editKey = QLineEdit()
self.editKey.setPlaceholderText(self.tr("Select item to edit")) self.editKey.setPlaceholderText(self.tr("Select item to edit"))
self.editKey.setEnabled(False) self.editKey.setEnabled(False)
@@ -615,8 +630,6 @@ class GuiProjectEditReplace(QWidget):
self.saveButton.clicked.connect(self._saveEntry) self.saveButton.clicked.connect(self._saveEntry)
# Assemble # Assemble
# ========
self.listControls = QVBoxLayout() self.listControls = QVBoxLayout()
self.listControls.addWidget(self.addButton) self.listControls.addWidget(self.addButton)
self.listControls.addWidget(self.delButton) self.listControls.addWidget(self.delButton)
@@ -636,50 +649,61 @@ class GuiProjectEditReplace(QWidget):
self.innerBox.addLayout(self.listControls) self.innerBox.addLayout(self.listControls)
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
self.outerBox.addWidget(QLabel("<b>%s</b>" % pageLabel)) self.outerBox.addWidget(self.pageTitle)
self.outerBox.addLayout(self.innerBox) self.outerBox.addLayout(self.innerBox)
self.setLayout(self.outerBox) self.setCentralLayout(self.outerBox)
return return
@property
def wasChanged(self) -> bool:
"""The user changed these settings."""
return self._changed
##
# Methods
##
def getNewList(self) -> dict: def getNewList(self) -> dict:
"""Extract the list from the widget.""" """Extract the list from the widget."""
new = {} new = {}
for n in range(self.listBox.topLevelItemCount()): for n in range(self.listBox.topLevelItemCount()):
tItem = self.listBox.topLevelItem(n) if tItem := self.listBox.topLevelItem(n):
if tItem is not None:
aKey = self._stripNotAllowed(tItem.text(0)) aKey = self._stripNotAllowed(tItem.text(0))
aVal = tItem.text(1) aVal = tItem.text(1)
if len(aKey) > 0: if len(aKey) > 0:
new[aKey] = aVal new[aKey] = aVal
return new return new
def columnWidth(self) -> int:
"""Return the size of the header column."""
return self.listBox.columnWidth(0)
## ##
# Internal Functions # Private Slots
## ##
def _selectedItem(self) -> bool: @pyqtSlot()
def _selectedItem(self) -> None:
"""Extract the details from the selected item and populate the """Extract the details from the selected item and populate the
edit form. edit form.
""" """
selItem = self._getSelectedItem() if selItem := self._getSelectedItem():
if selItem is None: editKey = self._stripNotAllowed(selItem.text(0))
return False editVal = selItem.text(1)
editKey = self._stripNotAllowed(selItem.text(0)) self.editKey.setText(editKey)
editVal = selItem.text(1) self.editValue.setText(editVal)
self.editKey.setText(editKey) self.editKey.setEnabled(True)
self.editValue.setText(editVal) self.editValue.setEnabled(True)
self.editKey.setEnabled(True) self.editKey.selectAll()
self.editValue.setEnabled(True) self.editKey.setFocus()
self.editKey.selectAll() return
self.editKey.setFocus()
return True
@pyqtSlot()
def _saveEntry(self) -> None: def _saveEntry(self) -> None:
"""Save the form data into the list widget.""" """Save the form data into the list widget."""
selItem = self._getSelectedItem() if selItem := self._getSelectedItem():
if selItem:
newKey = self.editKey.text() newKey = self.editKey.text()
newVal = self.editValue.text() newVal = self.editValue.text()
saveKey = self._stripNotAllowed(newKey) saveKey = self._stripNotAllowed(newKey)
@@ -691,38 +715,36 @@ class GuiProjectEditReplace(QWidget):
self.editKey.setEnabled(False) self.editKey.setEnabled(False)
self.editValue.setEnabled(False) self.editValue.setEnabled(False)
self.listBox.clearSelection() self.listBox.clearSelection()
self.arChanged = True self._changed = True
return return
@pyqtSlot()
def _addEntry(self) -> None: def _addEntry(self) -> None:
"""Add a new list entry.""" """Add a new list entry."""
saveKey = "<keyword%d>" % (self.listBox.topLevelItemCount() + 1) saveKey = "<keyword%d>" % (self.listBox.topLevelItemCount() + 1)
newVal = "" self.listBox.addTopLevelItem(QTreeWidgetItem([saveKey, ""]))
newItem = QTreeWidgetItem([saveKey, newVal])
self.listBox.addTopLevelItem(newItem)
return return
@pyqtSlot()
def _delEntry(self) -> None: def _delEntry(self) -> None:
"""Delete the selected entry.""" """Delete the selected entry."""
selItem = self._getSelectedItem() if selItem := self._getSelectedItem():
if selItem:
self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(selItem)) self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(selItem))
self.arChanged = True self._changed = True
return return
##
# Internal Functions
##
def _getSelectedItem(self) -> QTreeWidgetItem | None: def _getSelectedItem(self) -> QTreeWidgetItem | None:
"""Extract the currently selected item.""" """Extract the currently selected item."""
selItem = self.listBox.selectedItems() if items := self.listBox.selectedItems():
if len(selItem) == 0: return items[0]
return None return None
return selItem[0]
def _stripNotAllowed(self, key: str) -> str: def _stripNotAllowed(self, key: str) -> str:
"""Clean up the replace key string.""" """Clean up the replace key string."""
result = "" return "".join(c for c in key if c.isalnum())
for c in key:
if c.isalnum():
result += c
return result
# END Class GuiProjectEditReplace # END Class _ReplacePage
+113 -84
View File
@@ -3,9 +3,11 @@ novelWriter Custom Widget: Config Layout
========================================== ==========================================
File History: File History:
Created: 2020-05-03 [0.4.5] NConfigLayout, NHelpLabel Created: 2020-05-03 [0.4.5] NConfigLayout, NColourLabel
Created: 2023-05-23 [2.1b1] NSimpleLayout Created: 2023-05-23 [2.1b1] NSimpleLayout
Created: 2024-01-08 [2.3b1] NScrollableForm Created: 2024-01-08 [2.3b1] NScrollableForm
Created: 2024-01-26 [2.3b1] NScrollablePage
Created: 2024-01-26 [2.3b1] NFixedPage
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen Copyright 20182024, Veronica Berglyd Olsen
@@ -28,27 +30,74 @@ from __future__ import annotations
from PyQt5.QtGui import QColor, QPalette from PyQt5.QtGui import QColor, QPalette
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractButton, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QScrollArea, QSizePolicy, QAbstractButton, QFrame, QGridLayout, QHBoxLayout, QLabel, QLayout,
QVBoxLayout, QWidget QScrollArea, QSizePolicy, QVBoxLayout, QWidget
) )
from novelwriter import CONFIG from novelwriter import CONFIG
FONT_SCALE = 0.9 DEFAULT_SCALE = 0.9
RIGHT_TOP = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop RIGHT_TOP = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop
LEFT_TOP = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop LEFT_TOP = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
class NFixedPage(QFrame):
"""Extension: Fixed Page Widget
A custom widget that holds a layout. This is just a wrapper around a
QFrame that sets the same frame style as the other Page widgets.
"""
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
self.setFrameShadow(QFrame.Shadow.Sunken)
self.setFrameShape(QFrame.Shape.StyledPanel)
self.setCentralLayout = self.setLayout
return
# END Class NFixedPage
class NScrollablePage(QScrollArea):
"""Extension: Scrollable Page Widget
A custom widget that holds a layout within a scrollable area.
"""
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
self._widget = QWidget(self)
self.setWidget(self._widget)
self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setFrameShadow(QFrame.Shadow.Sunken)
self.setFrameShape(QFrame.Shape.StyledPanel)
return
def setCentralLayout(self, layout: QLayout) -> None:
"""Set the central layout of the scroll page."""
self._widget.setLayout(layout)
return
# END Class NScrollablePage
class NScrollableForm(QScrollArea): class NScrollableForm(QScrollArea):
"""Extension: Scrollable Form Widget
A custom widget that creates a form within a scrollable area.
"""
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
self._helpCol = QColor(0, 0, 0) self._helpCol = QColor(0, 0, 0)
self._fontScale = FONT_SCALE self._fontScale = DEFAULT_SCALE
self._first = True self._first = True
self._indent = CONFIG.pxInt(12)
self._sections: dict[int, QLabel] = {} self._sections: dict[int, QLabel] = {}
self._editable: dict[str, NHelpLabel] = {} self._editable: dict[str, NColourLabel] = {}
self._index: dict[str, QWidget] = {} self._index: dict[str, QWidget] = {}
self._layout = QVBoxLayout() self._layout = QVBoxLayout()
@@ -61,6 +110,8 @@ class NScrollableForm(QScrollArea):
self.setWidgetResizable(True) self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setFrameShadow(QFrame.Shadow.Sunken)
self.setFrameShape(QFrame.Shape.StyledPanel)
return return
@@ -76,9 +127,9 @@ class NScrollableForm(QScrollArea):
# Setters # Setters
## ##
def setHelpTextStyle(self, color: QColor | list | tuple, scale: float = FONT_SCALE) -> None: def setHelpTextStyle(self, color: QColor, scale: float = DEFAULT_SCALE) -> None:
"""Set the text color for the help text.""" """Set the text color for the help text."""
self._helpCol = color if isinstance(color, QColor) else QColor(*color) self._helpCol = color
self._fontScale = scale self._fontScale = scale
return return
@@ -88,6 +139,11 @@ class NScrollableForm(QScrollArea):
qHelp.setText(text) qHelp.setText(text)
return return
def setRowIndent(self, indent: int) -> None:
"""Set the indentation of each row."""
self._indent = max(indent, 0)
return
## ##
# Methods # Methods
## ##
@@ -119,37 +175,43 @@ class NScrollableForm(QScrollArea):
return return
def addRow(self, label: str, widget: QWidget, helpText: str = "", unit: str | None = None, def addRow(self, label: str, widget: QWidget, helpText: str = "", unit: str | None = None,
button: QWidget | None = None, editable: str | None = None) -> None: button: QWidget | None = None, editable: str | None = None,
stretch: tuple[int, int] = (0, 0)) -> None:
"""Add a label and a widget as a new row of the form.""" """Add a label and a widget as a new row of the form."""
row = QHBoxLayout() row = QHBoxLayout()
row.setSpacing(CONFIG.pxInt(4)) row.setSpacing(CONFIG.pxInt(12))
mPx = CONFIG.pxInt(12)
qLabel = QLabel(label, self) qLabel = QLabel(label, self)
qLabel.setIndent(mPx) qLabel.setIndent(self._indent)
qLabel.setBuddy(widget) qLabel.setBuddy(widget)
if helpText: if helpText:
qHelp = NHelpLabel(str(helpText), self._helpCol, self._fontScale) qHelp = NColourLabel(
qHelp.setIndent(mPx) str(helpText), self._helpCol, parent=self,
scale=self._fontScale, wrap=True, indent=self._indent
)
labelBox = QVBoxLayout() labelBox = QVBoxLayout()
labelBox.addWidget(qLabel) labelBox.addWidget(qLabel)
labelBox.addWidget(qHelp) labelBox.addWidget(qHelp)
labelBox.setSpacing(0) labelBox.setSpacing(0)
labelBox.addStretch(1) row.addLayout(labelBox, stretch[0])
row.addLayout(labelBox)
if editable: if editable:
self._editable[editable] = qHelp self._editable[editable] = qHelp
else: else:
row.addWidget(qLabel) row.addWidget(qLabel, stretch[0])
row.addSpacing(mPx)
row.addWidget(widget)
if isinstance(unit, str): if isinstance(unit, str):
row.addWidget(QLabel(unit, self)) box = QHBoxLayout()
box.addWidget(widget)
box.addWidget(QLabel(unit, self))
row.addLayout(box, stretch[1])
elif isinstance(button, QAbstractButton): elif isinstance(button, QAbstractButton):
row.addWidget(button) box = QHBoxLayout()
box.addWidget(widget)
box.addWidget(button)
row.addLayout(box, stretch[1])
else:
row.addWidget(widget, stretch[1])
self._layout.addLayout(row) self._layout.addLayout(row)
self._index[label.strip()] = widget self._index[label.strip()] = widget
@@ -173,7 +235,7 @@ class NConfigLayout(QGridLayout):
self._nextRow = 0 self._nextRow = 0
self._helpCol = QColor(0, 0, 0) self._helpCol = QColor(0, 0, 0)
self._fontScale = FONT_SCALE self._fontScale = DEFAULT_SCALE
self._itemMap = {} self._itemMap = {}
wSp = CONFIG.pxInt(8) wSp = CONFIG.pxInt(8)
@@ -183,24 +245,6 @@ class NConfigLayout(QGridLayout):
return return
##
# Getters and Setters
##
def setHelpTextStyle(self, color: QColor | list | tuple, scale: float = FONT_SCALE) -> None:
"""Set the text color for the help text."""
self._helpCol = color if isinstance(color, QColor) else QColor(*color)
self._fontScale = scale
return
def setHelpText(self, row: int, text: str) -> None:
"""Set the text for the help label."""
if row in self._itemMap:
qHelp = self._itemMap[row][1]
if isinstance(qHelp, NHelpLabel):
qHelp.setText(text)
return
## ##
# Class Methods # Class Methods
## ##
@@ -216,8 +260,8 @@ class NConfigLayout(QGridLayout):
self._nextRow += 1 self._nextRow += 1
return return
def addRow(self, label: str, widget: QWidget, helpText: str | None = None, def addRow(self, label: str, widget: QWidget, unit: str | None = None,
unit: str | None = None, button: QWidget | None = None) -> int: button: QWidget | None = None) -> int:
"""Add a label and a widget as a new row of the grid.""" """Add a label and a widget as a new row of the grid."""
wSp = CONFIG.pxInt(8) wSp = CONFIG.pxInt(8)
qLabel = QLabel(label) qLabel = QLabel(label)
@@ -225,17 +269,7 @@ class NConfigLayout(QGridLayout):
qLabel.setBuddy(widget) qLabel.setBuddy(widget)
qHelp = None qHelp = None
if helpText is not None: self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP)
qHelp = NHelpLabel(str(helpText), self._helpCol, self._fontScale)
qHelp.setIndent(wSp)
labelBox = QVBoxLayout()
labelBox.addWidget(qLabel)
labelBox.addWidget(qHelp)
labelBox.setSpacing(0)
labelBox.addStretch(1)
self.addLayout(labelBox, self._nextRow, 0, 1, 1, LEFT_TOP)
else:
self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP)
if isinstance(unit, str): if isinstance(unit, str):
controlBox = QHBoxLayout() controlBox = QHBoxLayout()
@@ -252,12 +286,7 @@ class NConfigLayout(QGridLayout):
self.addLayout(controlBox, self._nextRow, 1, 1, 1, RIGHT_TOP) self.addLayout(controlBox, self._nextRow, 1, 1, 1, RIGHT_TOP)
else: else:
if isinstance(widget, QLineEdit): self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP)
qLayout = QHBoxLayout()
qLayout.addWidget(widget)
self.addLayout(qLayout, self._nextRow, 1, 1, 1, RIGHT_TOP)
else:
self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP)
self.setRowStretch(self._nextRow, 0) self.setRowStretch(self._nextRow, 0)
self.setRowStretch(self._nextRow+1, 1) self.setRowStretch(self._nextRow+1, 1)
@@ -306,17 +335,9 @@ class NSimpleLayout(QGridLayout):
wSp = CONFIG.pxInt(8) wSp = CONFIG.pxInt(8)
qLabel = QLabel(label) qLabel = QLabel(label)
qLabel.setIndent(wSp) qLabel.setIndent(wSp)
self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP)
if isinstance(widget, QLineEdit):
qLayout = QHBoxLayout()
qLayout.addWidget(widget)
self.addLayout(qLayout, self._nextRow, 1, 1, 1, RIGHT_TOP)
else:
self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP)
qLabel.setBuddy(widget) qLabel.setBuddy(widget)
self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP)
self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP)
self.setRowStretch(self._nextRow, 0) self.setRowStretch(self._nextRow, 0)
self.setRowStretch(self._nextRow+1, 1) self.setRowStretch(self._nextRow+1, 1)
self._nextRow += 1 self._nextRow += 1
@@ -326,25 +347,33 @@ class NSimpleLayout(QGridLayout):
# END Class NSimpleLayout # END Class NSimpleLayout
class NHelpLabel(QLabel): class NColourLabel(QLabel):
"""Extension: A Coloured Label
def __init__(self, text: str, color: QColor | list | tuple, A custom widget that draws a label in a specific colour, and
fontSize: float = FONT_SCALE) -> None: optionally at a specific size, and word wrapped.
super().__init__(text) """
qCol = color if isinstance(color, QColor) else QColor(*color) HELP_SCALE = DEFAULT_SCALE
HEADER_SCALE = 1.25
lblCol = self.palette() def __init__(self, text: str, color: QColor, parent: QWidget | None = None,
lblCol.setColor(QPalette.WindowText, qCol) scale: float = HELP_SCALE, wrap: bool = False, indent: int = 0) -> None:
self.setPalette(lblCol) super().__init__(text, parent=parent)
lblFont = self.font() font = self.font()
lblFont.setPointSizeF(fontSize*lblFont.pointSizeF()) font.setPointSizeF(scale*font.pointSizeF())
self.setFont(lblFont) colour = self.palette()
colour.setColor(QPalette.WindowText, color)
self.setWordWrap(True) self.setPalette(colour)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.setFont(font)
self.setIndent(indent)
if wrap:
self.setWordWrap(True)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
return return
# END Class NHelpLabel # END Class NColourLabel
+26 -12
View File
@@ -3,7 +3,7 @@ novelWriter Custom Widget: Novel Selector
=========================================== ===========================================
File History: File History:
Created: 2022-11-17 [2.0] Created: 2022-11-17 [2.0] NovelSelector
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen Copyright 20182024, Veronica Berglyd Olsen
@@ -43,6 +43,8 @@ class NovelSelector(QComboBox):
super().__init__(parent=parent) super().__init__(parent=parent)
self._blockSignal = False self._blockSignal = False
self._firstHandle = None self._firstHandle = None
self._includeAll = False
self._listFormat = None
self.currentIndexChanged.connect(self._indexChanged) self.currentIndexChanged.connect(self._indexChanged)
return return
@@ -64,17 +66,29 @@ class NovelSelector(QComboBox):
def setHandle(self, tHandle: str | None, blockSignal: bool = True) -> None: def setHandle(self, tHandle: str | None, blockSignal: bool = True) -> None:
"""Set the currently selected handle.""" """Set the currently selected handle."""
self._blockSignal = blockSignal if (index := self.findData(tHandle) if tHandle else (self.count() - 1)) >= 0:
if tHandle is None: self._blockSignal = blockSignal
index = self.count() - 1
else:
index = self.findData(tHandle)
if index >= 0:
self.setCurrentIndex(index) self.setCurrentIndex(index)
self._blockSignal = False self._blockSignal = False
return return
def updateList(self, includeAll: bool = False, prefix: str | None = None) -> None: def setIncludeAll(self, value: bool) -> None:
"""Set flag to add an "All Novel Folders" option."""
self._includeAll = value
return
def setListFormat(self, value: str | None) -> None:
"""Set a format string for the list entries."""
if value is None or "{0}" in value:
self._listFormat = value
return
##
# Public Slots
##
@pyqtSlot()
def refreshNovelList(self) -> None:
"""Rebuild the list of novel items.""" """Rebuild the list of novel items."""
self._blockSignal = True self._blockSignal = True
self._firstHandle = None self._firstHandle = None
@@ -83,8 +97,8 @@ class NovelSelector(QComboBox):
icon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) icon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])
handle = self.currentData() handle = self.currentData()
for tHandle, nwItem in SHARED.project.tree.iterRoots(nwItemClass.NOVEL): for tHandle, nwItem in SHARED.project.tree.iterRoots(nwItemClass.NOVEL):
if prefix: if self._listFormat:
name = prefix.format(nwItem.itemName) name = self._listFormat.format(nwItem.itemName)
self.addItem(name, tHandle) self.addItem(name, tHandle)
else: else:
name = nwItem.itemName name = nwItem.itemName
@@ -92,7 +106,7 @@ class NovelSelector(QComboBox):
if self._firstHandle is None: if self._firstHandle is None:
self._firstHandle = tHandle self._firstHandle = tHandle
if includeAll: if self._includeAll:
self.insertSeparator(self.count()) self.insertSeparator(self.count())
self.addItem(icon, self.tr("All Novel Folders"), "") self.addItem(icon, self.tr("All Novel Folders"), "")
-130
View File
@@ -1,130 +0,0 @@
"""
novelWriter Custom Widget: Paged Dialog
=========================================
File History:
Created: 2020-05-17 [0.5.1]
This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
from PyQt5.QtGui import QPaintEvent
from PyQt5.QtCore import QRect, QPoint, QSize
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QStyle, QStyleOptionTab, QStylePainter, QTabBar,
QTabWidget, QVBoxLayout, QWidget
)
from novelwriter import CONFIG
class NPagedDialog(QDialog):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
self._tabBar = NVerticalTabBar(self)
self._tabBar.setExpanding(False)
self._tabBox = QTabWidget(self)
self._tabBox.setTabBar(self._tabBar)
self._tabBox.setTabPosition(QTabWidget.West)
self._buttonBox = QHBoxLayout()
self._outerBox = QVBoxLayout()
self._outerBox.addWidget(self._tabBox)
self._outerBox.addLayout(self._buttonBox)
# Default Margins
thisStyle = self.style()
mL = thisStyle.pixelMetric(QStyle.PM_LayoutLeftMargin)
mR = thisStyle.pixelMetric(QStyle.PM_LayoutRightMargin)
mT = thisStyle.pixelMetric(QStyle.PM_LayoutLeftMargin)
mB = thisStyle.pixelMetric(QStyle.PM_LayoutBottomMargin)
# Set Margins
self.setContentsMargins(0, 0, 0, 0)
self._outerBox.setContentsMargins(0, 0, 0, mB)
self._buttonBox.setContentsMargins(mL, 0, mR, 0)
self._outerBox.setSpacing(mT)
self.setLayout(self._outerBox)
return
def addTab(self, widget: QWidget, label: str) -> None:
"""Forward the adding of tabs to the QTabWidget."""
self._tabBox.addTab(widget, label)
return
def addControls(self, buttonBar: QWidget) -> None:
"""Add a button bar to the dialog."""
self._buttonBox.addWidget(buttonBar)
return
def setCurrentWidget(self, widget: QWidget) -> None:
"""Forward the changing of tab to the QTabWidget."""
self._tabBox.setCurrentWidget(widget)
return
# END Class NPagedDialog
class NVerticalTabBar(QTabBar):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
self._mW = CONFIG.pxInt(150)
return
def tabSizeHint(self, index: int) -> QSize:
"""Return a transposed size hint for the rotated bar."""
tSize = super().tabSizeHint(index)
tSize.transpose()
tSize.setWidth(min(tSize.width(), self._mW))
return tSize
def paintEvent(self, event: QPaintEvent) -> None:
"""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 NVerticalTabBar
+4 -6
View File
@@ -45,10 +45,9 @@ class NPagedSideBar(QToolBar):
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
self._buttons = []
self._actions = []
self._labelCol = None self._labelCol = None
self._spacerHeight = self.fontMetrics().height() // 2 self._spacerHeight = self.fontMetrics().height() // 2
self._buttons: dict[int, _NPagedToolButton] = {}
self._group = QButtonGroup(self) self._group = QButtonGroup(self)
self._group.setExclusive(True) self._group.setExclusive(True)
@@ -67,9 +66,9 @@ class NPagedSideBar(QToolBar):
"""Return a specific button.""" """Return a specific button."""
return self._buttons[buttonId] return self._buttons[buttonId]
def setLabelColor(self, color: list | QColor) -> None: def setLabelColor(self, color: QColor) -> None:
"""Set the text color for the labels.""" """Set the text color for the labels."""
self._labelCol = color if isinstance(color, QColor) else QColor(*color) self._labelCol = color
return return
def addSeparator(self) -> None: def addSeparator(self) -> None:
@@ -95,8 +94,7 @@ class NPagedSideBar(QToolBar):
action = self.insertWidget(self._stretchAction, button) action = self.insertWidget(self._stretchAction, button)
self._group.addButton(button, id=buttonId) self._group.addButton(button, id=buttonId)
self._buttons.append(button) self._buttons[buttonId] = button
self._actions.append(action)
return action return action
+17 -16
View File
@@ -276,14 +276,14 @@ class GuiDocEditor(QPlainTextEdit):
def updateSyntaxColours(self) -> None: def updateSyntaxColours(self) -> None:
"""Update the syntax highlighting theme.""" """Update the syntax highlighting theme."""
mainPalette = self.palette() mainPalette = self.palette()
mainPalette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) mainPalette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
mainPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack)) mainPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
mainPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) mainPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(mainPalette) self.setPalette(mainPalette)
docPalette = self.viewport().palette() docPalette = self.viewport().palette()
docPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack)) docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
docPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.viewport().setPalette(docPalette) self.viewport().setPalette(docPalette)
self.docHeader.matchColours() self.docHeader.matchColours()
@@ -2326,9 +2326,9 @@ class GuiDocToolBar(QWidget):
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Initialise GUI elements that depend on specific settings.""" """Initialise GUI elements that depend on specific settings."""
palette = QPalette() palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(palette) self.setPalette(palette)
self.tbBoldMD.setIcon(SHARED.theme.getIcon("fmt_bold-md")) self.tbBoldMD.setIcon(SHARED.theme.getIcon("fmt_bold-md"))
@@ -2866,10 +2866,11 @@ class GuiDocEditHeader(QWidget):
self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise")) self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
self.closeButton.setIcon(SHARED.theme.getIcon("close")) self.closeButton.setIcon(SHARED.theme.getIcon("close"))
colText = SHARED.theme.colText
buttonStyle = ( buttonStyle = (
"QToolButton {{border: none; background: transparent;}} " "QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
).format(*SHARED.theme.colText) ).format(colText.red(), colText.green(), colText.blue())
self.tbButton.setStyleSheet(buttonStyle) self.tbButton.setStyleSheet(buttonStyle)
self.searchButton.setStyleSheet(buttonStyle) self.searchButton.setStyleSheet(buttonStyle)
@@ -2885,9 +2886,9 @@ class GuiDocEditHeader(QWidget):
theme rather than the main GUI. theme rather than the main GUI.
""" """
palette = QPalette() palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(palette) self.setPalette(palette)
self.itemTitle.setPalette(palette) self.itemTitle.setPalette(palette)
@@ -3093,9 +3094,9 @@ class GuiDocEditFooter(QWidget):
theme rather than the main GUI. theme rather than the main GUI.
""" """
palette = QPalette() palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(palette) self.setPalette(palette)
self.statusText.setPalette(palette) self.statusText.setPalette(palette)
+44 -71
View File
@@ -48,6 +48,8 @@ SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
class GuiDocHighlighter(QSyntaxHighlighter): class GuiDocHighlighter(QSyntaxHighlighter):
__slots__ = ("_tItem", "_tHandle", "_spellCheck", "_spellErr", "_hRules", "_hStyles")
BLOCK_NONE = 0 BLOCK_NONE = 0
BLOCK_TEXT = 1 BLOCK_TEXT = 1
BLOCK_META = 2 BLOCK_META = 2
@@ -58,29 +60,14 @@ class GuiDocHighlighter(QSyntaxHighlighter):
logger.debug("Create: GuiDocHighlighter") logger.debug("Create: GuiDocHighlighter")
self._tItem = None self._tItem = None
self._tHandle = None self._tHandle = None
self._spellCheck = False self._spellCheck = False
self._spellErr = QTextCharFormat()
self._hRules: list[tuple[str, dict]] = [] self._hRules: list[tuple[str, dict]] = []
self._hStyles: dict[str, QTextCharFormat] = {} self._hStyles: dict[str, QTextCharFormat] = {}
self._colHead = QColor(0, 0, 0)
self._colHeadH = QColor(0, 0, 0)
self._colEmph = QColor(0, 0, 0)
self._colDialN = QColor(0, 0, 0)
self._colDialD = QColor(0, 0, 0)
self._colDialS = QColor(0, 0, 0)
self._colHidden = QColor(0, 0, 0)
self._colCode = QColor(0, 0, 0)
self._colKey = QColor(0, 0, 0)
self._colVal = QColor(0, 0, 0)
self._colSpell = QColor(0, 0, 0)
self._colError = QColor(0, 0, 0)
self._colRepTag = QColor(0, 0, 0)
self._colMod = QColor(0, 0, 0)
self._colBreak = QColor(0, 0, 0)
self.initHighlighter() self.initHighlighter()
logger.debug("Ready: GuiDocHighlighter") logger.debug("Ready: GuiDocHighlighter")
@@ -93,54 +80,42 @@ class GuiDocHighlighter(QSyntaxHighlighter):
""" """
logger.debug("Setting up highlighting rules") logger.debug("Setting up highlighting rules")
self._colHead = QColor(*SHARED.theme.colHead) colEmph = SHARED.theme.colEmph if CONFIG.highlightEmph else None
self._colHeadH = QColor(*SHARED.theme.colHeadH) colBreak = QColor(SHARED.theme.colEmph)
self._colDialN = QColor(*SHARED.theme.colDialN) colBreak.setAlpha(64)
self._colDialD = QColor(*SHARED.theme.colDialD)
self._colDialS = QColor(*SHARED.theme.colDialS)
self._colHidden = QColor(*SHARED.theme.colHidden)
self._colCode = QColor(*SHARED.theme.colCode)
self._colKey = QColor(*SHARED.theme.colKey)
self._colVal = QColor(*SHARED.theme.colVal)
self._colSpell = QColor(*SHARED.theme.colSpell)
self._colError = QColor(*SHARED.theme.colError)
self._colRepTag = QColor(*SHARED.theme.colRepTag)
self._colMod = QColor(*SHARED.theme.colMod)
self._colBreak = QColor(*SHARED.theme.colEmph)
self._colBreak.setAlpha(64)
self._colEmph = None
if CONFIG.highlightEmph:
self._colEmph = QColor(*SHARED.theme.colEmph)
self._hRules = []
self._hStyles = { self._hStyles = {
"header1": self._makeFormat(self._colHead, "bold", 1.8), "header1": self._makeFormat(SHARED.theme.colHead, "bold", 1.8),
"header2": self._makeFormat(self._colHead, "bold", 1.6), "header2": self._makeFormat(SHARED.theme.colHead, "bold", 1.6),
"header3": self._makeFormat(self._colHead, "bold", 1.4), "header3": self._makeFormat(SHARED.theme.colHead, "bold", 1.4),
"header4": self._makeFormat(self._colHead, "bold", 1.2), "header4": self._makeFormat(SHARED.theme.colHead, "bold", 1.2),
"header1h": self._makeFormat(self._colHeadH, "bold", 1.8), "header1h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.8),
"header2h": self._makeFormat(self._colHeadH, "bold", 1.6), "header2h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.6),
"header3h": self._makeFormat(self._colHeadH, "bold", 1.4), "header3h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.4),
"header4h": self._makeFormat(self._colHeadH, "bold", 1.2), "header4h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.2),
"bold": self._makeFormat(self._colEmph, "bold"), "bold": self._makeFormat(colEmph, "bold"),
"italic": self._makeFormat(self._colEmph, "italic"), "italic": self._makeFormat(colEmph, "italic"),
"strike": self._makeFormat(self._colHidden, "strike"), "strike": self._makeFormat(SHARED.theme.colHidden, "strike"),
"mspaces": self._makeFormat(self._colError, "errline"), "mspaces": self._makeFormat(SHARED.theme.colError, "errline"),
"nobreak": self._makeFormat(self._colBreak, "background"), "nobreak": self._makeFormat(colBreak, "background"),
"dialogue1": self._makeFormat(self._colDialN), "dialogue1": self._makeFormat(SHARED.theme.colDialN),
"dialogue2": self._makeFormat(self._colDialD), "dialogue2": self._makeFormat(SHARED.theme.colDialD),
"dialogue3": self._makeFormat(self._colDialS), "dialogue3": self._makeFormat(SHARED.theme.colDialS),
"replace": self._makeFormat(self._colRepTag), "replace": self._makeFormat(SHARED.theme.colRepTag),
"hidden": self._makeFormat(self._colHidden), "hidden": self._makeFormat(SHARED.theme.colHidden),
"code": self._makeFormat(self._colCode), "code": self._makeFormat(SHARED.theme.colCode),
"keyword": self._makeFormat(self._colKey), "keyword": self._makeFormat(SHARED.theme.colKey),
"modifier": self._makeFormat(self._colMod), "modifier": self._makeFormat(SHARED.theme.colMod),
"value": self._makeFormat(self._colVal, "underline"), "value": self._makeFormat(SHARED.theme.colVal, "underline"),
"codevalue": self._makeFormat(self._colVal), "codevalue": self._makeFormat(SHARED.theme.colVal),
"codeinval": self._makeFormat(None, "errline"), "codeinval": self._makeFormat(None, "errline"),
} }
self._hRules = [] # Cache Spell Error Format
self._spellErr = QTextCharFormat()
self._spellErr.setUnderlineColor(SHARED.theme.colSpell)
self._spellErr.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
# Multiple or Trailing Spaces # Multiple or Trailing Spaces
if CONFIG.showMultiSpaces: if CONFIG.showMultiSpaces:
@@ -305,12 +280,12 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setCurrentBlockState(self.BLOCK_META) self.setCurrentBlockState(self.BLOCK_META)
if self._tItem: if self._tItem:
pIndex = SHARED.project.index pIndex = SHARED.project.index
isValid, theBits, thePos = pIndex.scanThis(text) isValid, bits, pos = pIndex.scanThis(text)
isGood = pIndex.checkThese(theBits, self._tItem) isGood = pIndex.checkThese(bits, self._tItem)
if isValid: if isValid:
for n, theBit in enumerate(theBits): for n, bit in enumerate(bits):
xPos = thePos[n] xPos = pos[n]
xLen = len(theBit) xLen = len(bit)
if isGood[n]: if isGood[n]:
if n == 0: if n == 0:
self.setFormat(xPos, xLen, self._hStyles["keyword"]) self.setFormat(xPos, xLen, self._hStyles["keyword"])
@@ -318,8 +293,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setFormat(xPos, xLen, self._hStyles["value"]) self.setFormat(xPos, xLen, self._hStyles["value"])
else: else:
kwFmt = self.format(xPos) kwFmt = self.format(xPos)
kwFmt.setUnderlineColor(self._colError) kwFmt.merge(self._hStyles["codeinval"])
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
self.setFormat(xPos, xLen, kwFmt) self.setFormat(xPos, xLen, kwFmt)
# We never want to run the spell checker on keyword/values, # We never want to run the spell checker on keyword/values,
@@ -402,8 +376,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
for xPos, xLen in data.spellCheck(text): for xPos, xLen in data.spellCheck(text):
for x in range(xPos, xPos+xLen): for x in range(xPos, xPos+xLen):
spFmt = self.format(x) spFmt = self.format(x)
spFmt.setUnderlineColor(self._colSpell) spFmt.merge(self._spellErr)
spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
self.setFormat(x, 1, spFmt) self.setFormat(x, 1, spFmt)
return return
@@ -431,7 +404,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if "strike" in styles: if "strike" in styles:
charFormat.setFontStrikeOut(True) charFormat.setFontStrikeOut(True)
if "errline" in styles: if "errline" in styles:
charFormat.setUnderlineColor(self._colError) charFormat.setUnderlineColor(SHARED.theme.colError)
charFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) charFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
if "underline" in styles: if "underline" in styles:
charFormat.setFontUnderline(True) charFormat.setFontUnderline(True)
+46 -39
View File
@@ -33,7 +33,7 @@ from typing import TYPE_CHECKING
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QPoint, QSize, Qt, QUrl from PyQt5.QtCore import pyqtSignal, pyqtSlot, QPoint, QSize, Qt, QUrl
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QColor, QCursor, QFont, QMouseEvent, QPalette, QResizeEvent, QTextCursor, QCursor, QFont, QMouseEvent, QPalette, QResizeEvent, QTextCursor,
QTextOption QTextOption
) )
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
@@ -149,14 +149,14 @@ class GuiDocViewer(QTextBrowser):
# Set the widget colours to match syntax theme # Set the widget colours to match syntax theme
mainPalette = self.palette() mainPalette = self.palette()
mainPalette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) mainPalette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
mainPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack)) mainPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
mainPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) mainPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(mainPalette) self.setPalette(mainPalette)
docPalette = self.viewport().palette() docPalette = self.viewport().palette()
docPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack)) docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
docPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.viewport().setPalette(docPalette) self.viewport().setPalette(docPalette)
self.docHeader.matchColours() self.docHeader.matchColours()
@@ -463,7 +463,13 @@ class GuiDocViewer(QTextBrowser):
"""Generate an appropriate style sheet for the document viewer, """Generate an appropriate style sheet for the document viewer,
based on the current syntax highlighter theme, based on the current syntax highlighter theme,
""" """
pTheme = SHARED.theme colText = SHARED.theme.colText
colHead = SHARED.theme.colHead
colVal = SHARED.theme.colVal
colEmph = SHARED.theme.colEmph
colKey = SHARED.theme.colKey
colHidden = SHARED.theme.colHidden
colMod = SHARED.theme.colMod
styleSheet = ( styleSheet = (
"body {{" "body {{"
" color: rgb({tColR}, {tColG}, {tColB});" " color: rgb({tColR}, {tColG}, {tColB});"
@@ -490,27 +496,27 @@ class GuiDocViewer(QTextBrowser):
" text-align: center;" " text-align: center;"
"}}\n" "}}\n"
).format( ).format(
tColR=pTheme.colText[0], tColR=colText.red(),
tColG=pTheme.colText[1], tColG=colText.green(),
tColB=pTheme.colText[2], tColB=colText.blue(),
hColR=pTheme.colHead[0], hColR=colHead.red(),
hColG=pTheme.colHead[1], hColG=colHead.green(),
hColB=pTheme.colHead[2], hColB=colHead.blue(),
aColR=pTheme.colVal[0], aColR=colVal.red(),
aColG=pTheme.colVal[1], aColG=colVal.green(),
aColB=pTheme.colVal[2], aColB=colVal.blue(),
eColR=pTheme.colEmph[0], eColR=colEmph.red(),
eColG=pTheme.colEmph[1], eColG=colEmph.green(),
eColB=pTheme.colEmph[2], eColB=colEmph.blue(),
kColR=pTheme.colKey[0], kColR=colKey.red(),
kColG=pTheme.colKey[1], kColG=colKey.green(),
kColB=pTheme.colKey[2], kColB=colKey.blue(),
cColR=pTheme.colHidden[0], cColR=colHidden.red(),
cColG=pTheme.colHidden[1], cColG=colHidden.green(),
cColB=pTheme.colHidden[2], cColB=colHidden.blue(),
mColR=pTheme.colMod[0], mColR=colMod.red(),
mColG=pTheme.colMod[1], mColG=colMod.green(),
mColB=pTheme.colMod[2], mColB=colMod.blue(),
) )
self.document().setDefaultStyleSheet(styleSheet) self.document().setDefaultStyleSheet(styleSheet)
@@ -744,10 +750,11 @@ class GuiDocViewHeader(QWidget):
self.refreshButton.setIcon(SHARED.theme.getIcon("refresh")) self.refreshButton.setIcon(SHARED.theme.getIcon("refresh"))
self.closeButton.setIcon(SHARED.theme.getIcon("close")) self.closeButton.setIcon(SHARED.theme.getIcon("close"))
colText = SHARED.theme.colText
buttonStyle = ( buttonStyle = (
"QToolButton {{border: none; background: transparent;}} " "QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
).format(*SHARED.theme.colText) ).format(colText.red(), colText.green(), colText.blue())
self.backButton.setStyleSheet(buttonStyle) self.backButton.setStyleSheet(buttonStyle)
self.forwardButton.setStyleSheet(buttonStyle) self.forwardButton.setStyleSheet(buttonStyle)
@@ -763,9 +770,9 @@ class GuiDocViewHeader(QWidget):
theme rather than the main GUI. theme rather than the main GUI.
""" """
palette = QPalette() palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(palette) self.setPalette(palette)
self.docTitle.setPalette(palette) self.docTitle.setPalette(palette)
return return
@@ -937,11 +944,11 @@ class GuiDocViewFooter(QWidget):
self.showComments.setIcon(bulletIcon) self.showComments.setIcon(bulletIcon)
self.showSynopsis.setIcon(bulletIcon) self.showSynopsis.setIcon(bulletIcon)
# StyleSheets colText = SHARED.theme.colText
buttonStyle = ( buttonStyle = (
"QToolButton {{border: none; background: transparent;}} " "QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
).format(*SHARED.theme.colText) ).format(colText.red(), colText.green(), colText.blue())
self.showHide.setStyleSheet(buttonStyle) self.showHide.setStyleSheet(buttonStyle)
self.showComments.setStyleSheet(buttonStyle) self.showComments.setStyleSheet(buttonStyle)
@@ -956,9 +963,9 @@ class GuiDocViewFooter(QWidget):
theme rather than the main GUI. theme rather than the main GUI.
""" """
palette = QPalette() palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(palette) self.setPalette(palette)
return return
+4 -4
View File
@@ -155,10 +155,10 @@ class GuiMainMenu(QMenuBar):
self.aProjectSettings.setShortcut("Ctrl+Shift+,") self.aProjectSettings.setShortcut("Ctrl+Shift+,")
self.aProjectSettings.triggered.connect(self.mainGui.showProjectSettingsDialog) self.aProjectSettings.triggered.connect(self.mainGui.showProjectSettingsDialog)
# Project > Project Details # Project > Novel Details
self.aProjectDetails = self.projMenu.addAction(self.tr("Project Details")) self.aNovelDetails = self.projMenu.addAction(self.tr("Novel Details"))
self.aProjectDetails.setShortcut("Shift+F6") self.aNovelDetails.setShortcut("Shift+F6")
self.aProjectDetails.triggered.connect(self.mainGui.showProjectDetailsDialog) self.aNovelDetails.triggered.connect(self.mainGui.showNovelDetailsDialog)
# Project > Separator # Project > Separator
self.projMenu.addSeparator() self.projMenu.addSeparator()
+4 -3
View File
@@ -207,9 +207,10 @@ class GuiNovelToolBar(QWidget):
# Novel Selector # Novel Selector
selFont = self.font() selFont = self.font()
selFont.setWeight(QFont.Weight.Bold) selFont.setWeight(QFont.Weight.Bold)
self.novelPrefix = self.tr("Outline of {0}")
self.novelValue = NovelSelector(self) self.novelValue = NovelSelector(self)
self.novelValue.setFont(selFont) self.novelValue.setFont(selFont)
self.novelValue.setListFormat(self.tr("Outline of {0}"))
self.novelValue.setMinimumWidth(CONFIG.pxInt(150)) self.novelValue.setMinimumWidth(CONFIG.pxInt(150))
self.novelValue.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.novelValue.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot) self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot)
@@ -294,7 +295,7 @@ class GuiNovelToolBar(QWidget):
"QComboBox {border-style: none; padding-left: 0;} " "QComboBox {border-style: none; padding-left: 0;} "
"QComboBox::drop-down {border-style: none}" "QComboBox::drop-down {border-style: none}"
) )
self.novelValue.updateList(prefix=self.novelPrefix) self.novelValue.refreshNovelList()
self.tbNovel.setVisible(self.novelValue.count() > 1) self.tbNovel.setVisible(self.novelValue.count() > 1)
return return
@@ -307,7 +308,7 @@ class GuiNovelToolBar(QWidget):
def buildNovelRootMenu(self) -> None: def buildNovelRootMenu(self) -> None:
"""Build the novel root menu.""" """Build the novel root menu."""
self.novelValue.updateList(prefix=self.novelPrefix) self.novelValue.refreshNovelList()
self.tbNovel.setVisible(self.novelValue.count() > 1) self.tbNovel.setVisible(self.novelValue.count() > 1)
return return
+3 -2
View File
@@ -220,6 +220,7 @@ class GuiOutlineToolBar(QToolBar):
self.novelLabel.setContentsMargins(0, 0, mPx, 0) self.novelLabel.setContentsMargins(0, 0, mPx, 0)
self.novelValue = NovelSelector(self) self.novelValue = NovelSelector(self)
self.novelValue.setIncludeAll(True)
self.novelValue.setMinimumWidth(CONFIG.pxInt(200)) self.novelValue.setMinimumWidth(CONFIG.pxInt(200))
self.novelValue.novelSelectionChanged.connect(self._novelValueChanged) self.novelValue.novelSelectionChanged.connect(self._novelValueChanged)
@@ -258,7 +259,7 @@ class GuiOutlineToolBar(QToolBar):
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
self.setStyleSheet("QToolBar {border: 0px;}") self.setStyleSheet("QToolBar {border: 0px;}")
self.novelValue.updateList(includeAll=True) self.novelValue.refreshNovelList()
self.aRefresh.setIcon(SHARED.theme.getIcon("refresh")) self.aRefresh.setIcon(SHARED.theme.getIcon("refresh"))
self.tbColumns.setIcon(SHARED.theme.getIcon("menu")) self.tbColumns.setIcon(SHARED.theme.getIcon("menu"))
self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}") self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}")
@@ -266,7 +267,7 @@ class GuiOutlineToolBar(QToolBar):
def populateNovelList(self) -> None: def populateNovelList(self) -> None:
"""Reload the content of the novel list.""" """Reload the content of the novel list."""
self.novelValue.updateList(includeAll=True) self.novelValue.refreshNovelList()
return return
def setCurrentRoot(self, rootHandle: str | None) -> None: def setCurrentRoot(self, rootHandle: str | None) -> None:
+3 -3
View File
@@ -48,7 +48,7 @@ from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
from novelwriter.dialogs.docmerge import GuiDocMerge from novelwriter.dialogs.docmerge import GuiDocMerge
from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.dialogs.projsettings import GuiProjectSettings from novelwriter.dialogs.projectsettings import GuiProjectSettings
from novelwriter.enum import ( from novelwriter.enum import (
nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwWidget nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwWidget
) )
@@ -1748,7 +1748,7 @@ class _TreeContextMenu(QMenu):
menu.addSeparator() menu.addSeparator()
action = menu.addAction(self.tr("Manage Labels ...")) action = menu.addAction(self.tr("Manage Labels ..."))
action.triggered.connect( action.triggered.connect(
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_STATUS) lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_STATUS)
) )
else: else:
menu = self.addMenu(self.tr("Set Importance to ...")) menu = self.addMenu(self.tr("Set Importance to ..."))
@@ -1765,7 +1765,7 @@ class _TreeContextMenu(QMenu):
menu.addSeparator() menu.addSeparator()
action = menu.addAction(self.tr("Manage Labels ...")) action = menu.addAction(self.tr("Manage Labels ..."))
action.triggered.connect( action.triggered.connect(
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_IMPORT) lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_IMPORT)
) )
return return
+2 -2
View File
@@ -79,9 +79,9 @@ class GuiSideBar(QWidget):
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
self.tbDetails = QToolButton(self) self.tbDetails = QToolButton(self)
self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Project Details"))) self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details")))
self.tbDetails.setIconSize(iconSize) self.tbDetails.setIconSize(iconSize)
self.tbDetails.clicked.connect(self.mainGui.showProjectDetailsDialog) self.tbDetails.clicked.connect(self.mainGui.showNovelDetailsDialog)
self.tbStats = QToolButton(self) self.tbStats = QToolButton(self)
self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics"))) self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics")))
+3 -4
View File
@@ -29,7 +29,6 @@ from time import time
from typing import TYPE_CHECKING, Literal from typing import TYPE_CHECKING, Literal
from datetime import datetime from datetime import datetime
from PyQt5.QtGui import QColor
from PyQt5.QtCore import pyqtSlot, QLocale from PyQt5.QtCore import pyqtSlot, QLocale
from PyQt5.QtWidgets import qApp, QStatusBar, QLabel from PyQt5.QtWidgets import qApp, QStatusBar, QLabel
@@ -55,9 +54,9 @@ class GuiMainStatus(QStatusBar):
self._userIdle = False self._userIdle = False
self._debugInfo = False self._debugInfo = False
colNone = QColor(*SHARED.theme.statNone) colNone = SHARED.theme.statNone
colSaved = QColor(*SHARED.theme.statSaved) colSaved = SHARED.theme.statSaved
colUnsaved = QColor(*SHARED.theme.statUnsaved) colUnsaved = SHARED.theme.statUnsaved
iPx = SHARED.theme.baseIconSize iPx = SHARED.theme.baseIconSize
+26 -41
View File
@@ -70,10 +70,10 @@ class GuiTheme:
self.isLightTheme = True self.isLightTheme = True
# GUI # GUI
self.statNone = [120, 120, 120] self.statNone = QColor(120, 120, 120)
self.statUnsaved = [200, 15, 39] self.statUnsaved = QColor(200, 15, 39)
self.statSaved = [2, 133, 37] self.statSaved = QColor(2, 133, 37)
self.helpText = [0, 0, 0] self.helpText = QColor(0, 0, 0)
# Loaded Syntax Settings # Loaded Syntax Settings
# ====================== # ======================
@@ -88,23 +88,23 @@ class GuiTheme:
self.syntaxLicenseUrl = "" self.syntaxLicenseUrl = ""
# Colours # Colours
self.colBack = [255, 255, 255] self.colBack = QColor(255, 255, 255)
self.colText = [0, 0, 0] self.colText = QColor(0, 0, 0)
self.colLink = [0, 0, 0] self.colLink = QColor(0, 0, 0)
self.colHead = [0, 0, 0] self.colHead = QColor(0, 0, 0)
self.colHeadH = [0, 0, 0] self.colHeadH = QColor(0, 0, 0)
self.colEmph = [0, 0, 0] self.colEmph = QColor(0, 0, 0)
self.colDialN = [0, 0, 0] self.colDialN = QColor(0, 0, 0)
self.colDialD = [0, 0, 0] self.colDialD = QColor(0, 0, 0)
self.colDialS = [0, 0, 0] self.colDialS = QColor(0, 0, 0)
self.colHidden = [0, 0, 0] self.colHidden = QColor(0, 0, 0)
self.colCode = [0, 0, 0] self.colCode = QColor(0, 0, 0)
self.colKey = [0, 0, 0] self.colKey = QColor(0, 0, 0)
self.colVal = [0, 0, 0] self.colVal = QColor(0, 0, 0)
self.colSpell = [0, 0, 0] self.colSpell = QColor(0, 0, 0)
self.colError = [0, 0, 0] self.colError = QColor(0, 0, 0)
self.colRepTag = [0, 0, 0] self.colRepTag = QColor(0, 0, 0)
self.colMod = [0, 0, 0] self.colMod = QColor(0, 0, 0)
# Class Setup # Class Setup
# =========== # ===========
@@ -256,12 +256,12 @@ class GuiTheme:
backLNess = backCol.lightnessF() backLNess = backCol.lightnessF()
textLNess = textCol.lightnessF() textLNess = textCol.lightnessF()
self.isLightTheme = backLNess > textLNess self.isLightTheme = backLNess > textLNess
if self.helpText == [0, 0, 0]: if self.helpText == QColor(0, 0, 0):
if self.isLightTheme: if self.isLightTheme:
helpLCol = textLNess + 0.35*(backLNess - textLNess) helpLCol = textLNess + 0.35*(backLNess - textLNess)
else: else:
helpLCol = backLNess + 0.65*(textLNess - backLNess) helpLCol = backLNess + 0.65*(textLNess - backLNess)
self.helpText = [int(255*helpLCol)]*3 self.helpText = QColor.fromHsl(0, 0, int(255*helpLCol))
# Icons # Icons
defaultIcons = "typicons_light" if backLNess >= 0.5 else "typicons_dark" defaultIcons = "typicons_light" if backLNess >= 0.5 else "typicons_dark"
@@ -398,29 +398,14 @@ class GuiTheme:
return True return True
def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> list[int]: def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> QColor:
"""Parse a colour value from a config string.""" """Parse a colour value from a config string."""
if parser.has_option(section, name): return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255]))
values = parser.get(section, name).split(",")
result = []
try:
result.append(minmax(int(values[0]), 0, 255))
result.append(minmax(int(values[1]), 0, 255))
result.append(minmax(int(values[2]), 0, 255))
except Exception:
logger.error("Could not load theme colours for '%s' from config file", name)
result = [0, 0, 0]
else:
logger.warning("Could not find theme colours for '%s' in config file", name)
result = [0, 0, 0]
return result
def _setPalette(self, parser: NWConfigParser, section: str, def _setPalette(self, parser: NWConfigParser, section: str,
name: str, value: QPalette.ColorRole) -> None: name: str, value: QPalette.ColorRole) -> None:
"""Set a palette colour value from a config string.""" """Set a palette colour value from a config string."""
self._guiPalette.setColor( self._guiPalette.setColor(value, self._parseColour(parser, section, name))
value, QColor(*self._parseColour(parser, section, name))
)
return return
# End Class GuiTheme # End Class GuiTheme
+11 -16
View File
@@ -53,11 +53,11 @@ from novelwriter.dialogs.about import GuiAbout
from novelwriter.dialogs.updates import GuiUpdates from novelwriter.dialogs.updates import GuiUpdates
from novelwriter.dialogs.wordlist import GuiWordList from novelwriter.dialogs.wordlist import GuiWordList
from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.preferences import GuiPreferences
from novelwriter.dialogs.projdetails import GuiProjectDetails from novelwriter.dialogs.projectsettings import GuiProjectSettings
from novelwriter.dialogs.projsettings import GuiProjectSettings
from novelwriter.tools.welcome import GuiWelcome from novelwriter.tools.welcome import GuiWelcome
from novelwriter.tools.manuscript import GuiManuscript from novelwriter.tools.manuscript import GuiManuscript
from novelwriter.tools.dictionaries import GuiDictionaries from novelwriter.tools.dictionaries import GuiDictionaries
from novelwriter.tools.noveldetails import GuiNovelDetails
from novelwriter.tools.writingstats import GuiWritingStats from novelwriter.tools.writingstats import GuiWritingStats
from novelwriter.enum import ( from novelwriter.enum import (
@@ -79,13 +79,6 @@ class GuiMain(QMainWindow):
function. Also, the project instance and theme instance are created function. Also, the project instance and theme instance are created
here. These should be passed around to all other objects who need here. These should be passed around to all other objects who need
them and new instances of them should generally not be created. them and new instances of them should generally not be created.
* All other GUI classes that depend on any components from the
main GUI should be passed a reference to the instance of this
class.
* All non-GUI classes can be passed a reference to the NWProject
instance if the Main GUI is not needed (which it generally
shouldn't need).
""" """
def __init__(self) -> None: def __init__(self) -> None:
@@ -816,19 +809,19 @@ class GuiMain(QMainWindow):
@pyqtSlot() @pyqtSlot()
@pyqtSlot(int) @pyqtSlot(int)
def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.TAB_MAIN) -> None: def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.PAGE_SETTINGS) -> None:
"""Open the project settings dialog.""" """Open the project settings dialog."""
if SHARED.hasProject: if SHARED.hasProject:
dialog = GuiProjectSettings(self, focusTab=focusTab) dialog = GuiProjectSettings(self, gotoPage=focusTab)
dialog.newProjectSettingsReady.connect(self._processProjectSettingsChanges) dialog.newProjectSettingsReady.connect(self._processProjectSettingsChanges)
dialog.exec_() dialog.exec_()
return return
@pyqtSlot() @pyqtSlot()
def showProjectDetailsDialog(self) -> None: def showNovelDetailsDialog(self) -> None:
"""Open the project details dialog.""" """Open the novel details dialog."""
if SHARED.hasProject: if SHARED.hasProject:
dialog = GuiProjectDetails(self) dialog = GuiNovelDetails(self)
dialog.setModal(True) dialog.setModal(True)
dialog.show() dialog.show()
dialog.raise_() dialog.raise_()
@@ -1118,13 +1111,15 @@ class GuiMain(QMainWindow):
return return
@pyqtSlot() @pyqtSlot(bool)
def _processProjectSettingsChanges(self) -> None: def _processProjectSettingsChanges(self, rebuildTrees: bool) -> None:
"""Refresh data dependent on project settings.""" """Refresh data dependent on project settings."""
logger.debug("Applying new project settings") logger.debug("Applying new project settings")
SHARED.updateSpellCheckLanguage() SHARED.updateSpellCheckLanguage()
self.itemDetails.refreshDetails() self.itemDetails.refreshDetails()
self._updateWindowTitle(SHARED.project.data.name) self._updateWindowTitle(SHARED.project.data.name)
if rebuildTrees:
self.rebuildTrees()
return return
@pyqtSlot() @pyqtSlot()
+3 -5
View File
@@ -27,9 +27,7 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt5.QtGui import ( from PyQt5.QtGui import QFont, QIcon, QSyntaxHighlighter, QTextCharFormat, QTextDocument
QColor, QFont, QIcon, QSyntaxHighlighter, QTextCharFormat, QTextDocument
)
from PyQt5.QtCore import QEvent, QSize, Qt, pyqtSignal, pyqtSlot from PyQt5.QtCore import QEvent, QSize, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractButton, QAbstractItemView, QComboBox, QDialog, QDialogButtonBox, QAbstractButton, QAbstractItemView, QComboBox, QDialog, QDialogButtonBox,
@@ -862,9 +860,9 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter):
def __init__(self, document: QTextDocument) -> None: def __init__(self, document: QTextDocument) -> None:
super().__init__(document) super().__init__(document)
self._fmtSymbol = QTextCharFormat() self._fmtSymbol = QTextCharFormat()
self._fmtSymbol.setForeground(QColor(*SHARED.theme.colHead)) self._fmtSymbol.setForeground(SHARED.theme.colHead)
self._fmtFormat = QTextCharFormat() self._fmtFormat = QTextCharFormat()
self._fmtFormat.setForeground(QColor(*SHARED.theme.colEmph)) self._fmtFormat.setForeground(SHARED.theme.colEmph)
return return
def highlightBlock(self, text: str) -> None: def highlightBlock(self, text: str) -> None:
+525
View File
@@ -0,0 +1,525 @@
"""
novelWriter GUI Novel Info
============================
File History:
Created: 2024-01-18 [2.3b1] GuiNovelDetails
This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import math
import logging
from PyQt5.QtGui import QCloseEvent
from PyQt5.QtCore import QSize, Qt, pyqtSlot
from PyQt5.QtWidgets import (
QAbstractItemView, QDialog, QDialogButtonBox, QFormLayout, QGridLayout,
QHBoxLayout, QLabel, QSpinBox, QStackedWidget, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import formatTime, numberToRoman
from novelwriter.constants import nwUnicode
from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollablePage
from novelwriter.extensions.pagedsidebar import NPagedSideBar
from novelwriter.extensions.novelselector import NovelSelector
logger = logging.getLogger(__name__)
class GuiNovelDetails(QDialog):
PAGE_OVERVIEW = 1
PAGE_CONTENTS = 2
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
logger.debug("Create: GuiNovelDetails")
self.setObjectName("GuiNovelDetails")
self.setWindowTitle(self.tr("Novel Details"))
options = SHARED.project.options
self.setMinimumSize(CONFIG.pxInt(500), CONFIG.pxInt(400))
self.resize(
CONFIG.pxInt(options.getInt("GuiNovelDetails", "winWidth", CONFIG.pxInt(650))),
CONFIG.pxInt(options.getInt("GuiNovelDetails", "winHeight", CONFIG.pxInt(500)))
)
# Title
self.titleLabel = NColourLabel(
self.tr("Novel Details"), SHARED.theme.helpText,
parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
)
# Novel Selector
self.novelSelector = NovelSelector(self)
self.novelSelector.refreshNovelList()
self.novelSelector.setHandle(
options.getString("GuiNovelDetails", "novelRoot", self.novelSelector.firstHandle or "")
)
# SideBar
self.sidebar = NPagedSideBar(self)
self.sidebar.setLabelColor(SHARED.theme.helpText)
self.sidebar.addButton(self.tr("Overview"), self.PAGE_OVERVIEW)
self.sidebar.addButton(self.tr("Contents"), self.PAGE_CONTENTS)
self.sidebar.setSelected(self.PAGE_OVERVIEW)
self.sidebar.buttonClicked.connect(self._sidebarClicked)
# Content
self.overviewPage = _OverviewPage(self)
self.contentsPage = _ContentsPage(self)
self.mainStack = QStackedWidget(self)
self.mainStack.addWidget(self.overviewPage)
self.mainStack.addWidget(self.contentsPage)
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
self.buttonBox.rejected.connect(self.close)
# Assemble
self.topBox = QHBoxLayout()
self.topBox.addWidget(self.titleLabel)
self.topBox.addStretch(1)
self.topBox.addWidget(self.novelSelector, 1)
self.mainBox = QHBoxLayout()
self.mainBox.addWidget(self.sidebar)
self.mainBox.addWidget(self.mainStack)
self.mainBox.setContentsMargins(0, 0, 0, 0)
self.outerBox = QVBoxLayout()
self.outerBox.addLayout(self.topBox)
self.outerBox.addLayout(self.mainBox)
self.outerBox.addWidget(self.buttonBox)
self.outerBox.setSpacing(CONFIG.pxInt(8))
self.setLayout(self.outerBox)
self.setSizeGripEnabled(True)
# Connect Signals
self.novelSelector.novelSelectionChanged.connect(self.overviewPage.novelValueChanged)
self.novelSelector.novelSelectionChanged.connect(self.contentsPage.novelValueChanged)
logger.debug("Ready: GuiNovelDetails")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiNovelDetails")
return
##
# Methods
##
def updateValues(self) -> None:
"""Load the dialogs initial values."""
self.overviewPage.updateProjectData()
self.overviewPage.novelValueChanged(self.novelSelector.handle)
self.contentsPage.novelValueChanged(self.novelSelector.handle)
return
##
# Events
##
def closeEvent(self, event: QCloseEvent) -> None:
"""Capture the user closing the window and save settings."""
self._saveSettings()
event.accept()
self.deleteLater()
return
##
# Private Slots
##
@pyqtSlot(int)
def _sidebarClicked(self, pageId: int) -> None:
"""Process a user request to switch page."""
if pageId == self.PAGE_OVERVIEW:
self.mainStack.setCurrentWidget(self.overviewPage)
elif pageId == self.PAGE_CONTENTS:
self.mainStack.setCurrentWidget(self.contentsPage)
return
##
# Internal Functions
##
def _saveSettings(self) -> None:
"""Save the user GUI settings."""
winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height())
novelRoot = self.novelSelector.handle
logger.debug("Saving State: GuiNovelDetails")
options = SHARED.project.options
options.setValue("GuiNovelDetails", "winWidth", winWidth)
options.setValue("GuiNovelDetails", "winHeight", winHeight)
options.setValue("GuiNovelDetails", "novelRoot", novelRoot)
self.contentsPage.saveSettings()
return
# END Class GuiNovelDetails
class _OverviewPage(NScrollablePage):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
mPx = CONFIG.pxInt(8)
sPx = CONFIG.pxInt(16)
hPx = CONFIG.pxInt(24)
vPx = CONFIG.pxInt(4)
# Project Info
self.projLabel = NColourLabel(
self.tr("Project"), SHARED.theme.helpText,
parent=self, scale=NColourLabel.HEADER_SCALE
)
self.projName = QLabel("", self)
self.projWords = QLabel("", self)
self.projNovels = QLabel("", self)
self.projNotes = QLabel("", self)
self.projRevisions = QLabel("", self)
self.projEditTime = QLabel("", self)
self.projForm = QFormLayout()
self.projForm.addRow("<b>{0}</b>".format(self.tr("Name")), self.projName)
self.projForm.addRow("<b>{0}</b>".format(self.tr("Revisions")), self.projRevisions)
self.projForm.addRow("<b>{0}</b>".format(self.tr("Editing Time")), self.projEditTime)
self.projForm.addRow("<b>{0}</b>".format(self.tr("Word Count")), self.projWords)
self.projForm.addRow("<b>\u2026 {0}</b>".format(self.tr("In Novels")), self.projNovels)
self.projForm.addRow("<b>\u2026 {0}</b>".format(self.tr("In Notes ")), self.projNotes)
self.projForm.setContentsMargins(mPx, 0, 0, 0)
self.projForm.setHorizontalSpacing(hPx)
self.projForm.setVerticalSpacing(vPx)
# Novel Info
self.novelLabel = NColourLabel(
self.tr("Selected Novel"), SHARED.theme.helpText,
parent=self, scale=NColourLabel.HEADER_SCALE
)
self.novelName = QLabel("", self)
self.novelWords = QLabel("", self)
self.novelChapters = QLabel("", self)
self.novelScenes = QLabel("", self)
self.novelForm = QFormLayout()
self.novelForm.addRow("<b>{0}</b>".format(self.tr("Name")), self.novelName)
self.novelForm.addRow("<b>{0}</b>".format(self.tr("Word Count")), self.novelWords)
self.novelForm.addRow("<b>{0}</b>".format(self.tr("Chapters")), self.novelChapters)
self.novelForm.addRow("<b>{0}</b>".format(self.tr("Scenes")), self.novelScenes)
self.novelForm.setContentsMargins(mPx, 0, 0, 0)
self.novelForm.setHorizontalSpacing(hPx)
self.novelForm.setVerticalSpacing(vPx)
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.projLabel)
self.outerBox.addLayout(self.projForm)
self.outerBox.addWidget(self.novelLabel)
self.outerBox.addLayout(self.novelForm)
self.outerBox.setSpacing(sPx)
self.outerBox.addStretch(1)
self.setCentralLayout(self.outerBox)
return
##
# Methods
##
def updateProjectData(self) -> None:
"""Load information about the project."""
project = SHARED.project
project.updateWordCounts()
wcNovel, wcNotes = project.data.currCounts
self.projName.setText(project.data.name)
self.projRevisions.setText(f"{project.data.saveCount:n}")
self.projEditTime.setText(formatTime(project.currentEditTime))
self.projWords.setText(f"{wcNovel + wcNotes:n}")
self.projNovels.setText(f"{wcNovel:n}")
self.projNotes.setText(f"{wcNotes:n}")
return
##
# Public Slots
##
@pyqtSlot(str)
def novelValueChanged(self, tHandle: str) -> None:
"""Refresh the data for the selected novel."""
project = SHARED.project
if nwItem := project.tree[tHandle]:
self.novelName.setText(nwItem.itemName)
nwCount = project.index.getNovelWordCount(rootHandle=tHandle)
self.novelWords.setText(f"{nwCount:n}")
hCounts = project.index.getNovelTitleCounts(rootHandle=tHandle)
self.novelChapters.setText(f"{hCounts[2]:n}")
self.novelScenes.setText(f"{hCounts[3]:n}")
return
# END Class _OverviewPage
class _ContentsPage(NFixedPage):
C_TITLE = 0
C_WORDS = 1
C_PAGES = 2
C_PAGE = 3
C_PROG = 4
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
self._data = []
self._currentRoot = None
iPx = SHARED.theme.baseIconSize
hPx = CONFIG.pxInt(12)
vPx = CONFIG.pxInt(4)
options = SHARED.project.options
# Title
self.contentLabel = NColourLabel(
self.tr("Table of Contents"), SHARED.theme.helpText,
parent=self, scale=NColourLabel.HEADER_SCALE
)
# Contents Tree
self.tocTree = QTreeWidget(self)
self.tocTree.setIconSize(QSize(iPx, iPx))
self.tocTree.setIndentation(0)
self.tocTree.setColumnCount(6)
self.tocTree.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.tocTree.setHeaderLabels([
self.tr("Title"),
self.tr("Words"),
self.tr("Pages"),
self.tr("Page"),
self.tr("Progress"),
"",
])
treeHeadItem = self.tocTree.headerItem()
if treeHeadItem:
treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignmentFlag.AlignRight)
treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignmentFlag.AlignRight)
treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignmentFlag.AlignRight)
treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignmentFlag.AlignRight)
treeHeader = self.tocTree.header()
treeHeader.setStretchLastSection(True)
treeHeader.setMinimumSectionSize(hPx)
wCol0 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol0", 200))
wCol1 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol1", 60))
wCol2 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol2", 60))
wCol3 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol3", 60))
wCol4 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol4", 90))
self.tocTree.setColumnWidth(0, wCol0)
self.tocTree.setColumnWidth(1, wCol1)
self.tocTree.setColumnWidth(2, wCol2)
self.tocTree.setColumnWidth(3, wCol3)
self.tocTree.setColumnWidth(4, wCol4)
self.tocTree.setColumnWidth(5, hPx)
# Options
wordsPerPage = options.getInt("GuiNovelDetails", "wordsPerPage", 350)
countFrom = options.getInt("GuiNovelDetails", "countFrom", 1)
clearDouble = options.getBool("GuiNovelDetails", "clearDouble", True)
self.wpLabel = QLabel(self.tr("Words per page"))
self.wpValue = QSpinBox(self)
self.wpValue.setMinimum(10)
self.wpValue.setMaximum(1000)
self.wpValue.setSingleStep(10)
self.wpValue.setValue(wordsPerPage)
self.wpValue.valueChanged.connect(self._populateTree)
self.poLabel = QLabel(self.tr("First page offset"))
self.poValue = QSpinBox(self)
self.poValue.setMinimum(1)
self.poValue.setMaximum(9999)
self.poValue.setSingleStep(1)
self.poValue.setValue(countFrom)
self.poValue.valueChanged.connect(self._populateTree)
self.dblLabel = QLabel(self.tr("Chapters on odd pages"))
self.dblValue = NSwitch(self, 2*iPx, iPx)
self.dblValue.setChecked(clearDouble)
self.dblValue.clicked.connect(self._populateTree)
self.optionsBox = QGridLayout()
self.optionsBox.addWidget(self.wpLabel, 0, 0)
self.optionsBox.addWidget(self.wpValue, 0, 1)
self.optionsBox.addWidget(self.dblLabel, 0, 3)
self.optionsBox.addWidget(self.dblValue, 0, 4)
self.optionsBox.addWidget(self.poLabel, 1, 0)
self.optionsBox.addWidget(self.poValue, 1, 1)
self.optionsBox.setHorizontalSpacing(hPx)
self.optionsBox.setVerticalSpacing(vPx)
self.optionsBox.setColumnStretch(2, 1)
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.contentLabel)
self.outerBox.addWidget(self.tocTree)
self.outerBox.addLayout(self.optionsBox)
self.setCentralLayout(self.outerBox)
return
def saveSettings(self) -> None:
"""Save the user GUI settings."""
widthCol0 = CONFIG.rpxInt(self.tocTree.columnWidth(0))
widthCol1 = CONFIG.rpxInt(self.tocTree.columnWidth(1))
widthCol2 = CONFIG.rpxInt(self.tocTree.columnWidth(2))
widthCol3 = CONFIG.rpxInt(self.tocTree.columnWidth(3))
widthCol4 = CONFIG.rpxInt(self.tocTree.columnWidth(4))
options = SHARED.project.options
options.setValue("GuiNovelDetails", "widthCol0", widthCol0)
options.setValue("GuiNovelDetails", "widthCol1", widthCol1)
options.setValue("GuiNovelDetails", "widthCol2", widthCol2)
options.setValue("GuiNovelDetails", "widthCol3", widthCol3)
options.setValue("GuiNovelDetails", "widthCol4", widthCol4)
options.setValue("GuiNovelDetails", "wordsPerPage", self.wpValue.value())
options.setValue("GuiNovelDetails", "countFrom", self.poValue.value())
options.setValue("GuiNovelDetails", "clearDouble", self.dblValue.isChecked())
return
##
# Public Slots
##
@pyqtSlot(str)
def novelValueChanged(self, tHandle: str) -> None:
"""Refresh the tree with another root item."""
if tHandle != self._currentRoot:
self._prepareData(tHandle)
self._populateTree()
self._currentRoot = tHandle
return
##
# Private Slots
##
@pyqtSlot()
def _populateTree(self) -> None:
"""Set the content of the chapter/page tree."""
dblPages = self.dblValue.isChecked()
wpPage = self.wpValue.value()
fstPage = self.poValue.value() - 1
pTotal = 0
tPages = 1
theList = []
for _, tLevel, tTitle, wCount in self._data:
pCount = math.ceil(wCount/wpPage)
if dblPages:
pCount += pCount%2
pTotal += pCount
theList.append((tLevel, tTitle, wCount, pCount))
pMax = pTotal - fstPage
self.tocTree.clear()
for tLevel, tTitle, wCount, pCount in theList:
newItem = QTreeWidgetItem()
if tPages <= fstPage:
progPage = numberToRoman(tPages, True)
progText = ""
else:
cPage = tPages - fstPage
pgProg = 100.0*(cPage - 1)/pMax if pMax > 0 else 0.0
progPage = f"{cPage:n}"
progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"
hDec = SHARED.theme.getHeaderDecoration(tLevel)
if tTitle.strip() == "":
tTitle = self.tr("Untitled")
newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
newItem.setText(self.C_TITLE, tTitle)
newItem.setText(self.C_WORDS, f"{wCount:n}")
newItem.setText(self.C_PAGES, f"{pCount:n}")
newItem.setText(self.C_PAGE, progPage)
newItem.setText(self.C_PROG, progText)
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
newItem.setTextAlignment(self.C_PAGES, Qt.AlignRight)
newItem.setTextAlignment(self.C_PAGE, Qt.AlignRight)
newItem.setTextAlignment(self.C_PROG, Qt.AlignRight)
# Make pages and titles/partitions stand out
if tLevel < 2:
bFont = newItem.font(self.C_TITLE)
if tLevel == 0:
bFont.setItalic(True)
else:
bFont.setBold(True)
bFont.setUnderline(True)
newItem.setFont(self.C_TITLE, bFont)
tPages += pCount
self.tocTree.addTopLevelItem(newItem)
return
##
# Internal Functions
##
def _prepareData(self, rootHandle: str | None) -> None:
"""Extract the information from the project index."""
logger.debug("Populating ToC from handle '%s'", rootHandle)
self._data = SHARED.project.index.getTableOfContents(rootHandle, 2)
self._data.append(("", 0, self.tr("END"), 0))
return
# END Class _ContentsPage
+6 -9
View File
@@ -25,7 +25,6 @@ from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
@@ -43,14 +42,11 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED, __version__, __date__ from novelwriter import CONFIG, SHARED, __version__, __date__
from novelwriter.enum import nwItemClass from novelwriter.enum import nwItemClass
from novelwriter.common import formatInt, makeFileNameSafe from novelwriter.common import formatInt, formatVersion, makeFileNameSafe
from novelwriter.constants import nwUnicode from novelwriter.constants import nwUnicode
from novelwriter.core.coretools import ProjectBuilder from novelwriter.core.coretools import ProjectBuilder
from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switch import NSwitch
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -58,8 +54,8 @@ class GuiWelcome(QDialog):
openProjectRequest = pyqtSignal(Path) openProjectRequest = pyqtSignal(Path)
def __init__(self, mainGui: GuiMain) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=mainGui) super().__init__(parent=parent)
logger.debug("Create: GuiWelcome") logger.debug("Create: GuiWelcome")
self.setObjectName("GuiWelcome") self.setObjectName("GuiWelcome")
@@ -91,7 +87,8 @@ class GuiWelcome(QDialog):
self.nwLabel.setPixmap(self.nwImage) self.nwLabel.setPixmap(self.nwImage)
self.nwInfo = QLabel(self.tr("Version {0} {1} Released on {2}").format( self.nwInfo = QLabel(self.tr("Version {0} {1} Released on {2}").format(
__version__, nwUnicode.U_ENDASH, datetime.strptime(__date__, "%Y-%m-%d").strftime("%x") formatVersion(__version__), nwUnicode.U_ENDASH,
datetime.strptime(__date__, "%Y-%m-%d").strftime("%x")
)) ))
self.tabOpen = _OpenProjectPage(self) self.tabOpen = _OpenProjectPage(self)
@@ -343,7 +340,7 @@ class _ProjectListItem(QStyledItemDelegate):
self._dFont = qApp.font() self._dFont = qApp.font()
self._dFont.setPointSizeF(fPt) self._dFont.setPointSizeF(fPt)
self._dPen = QPen(QColor(*SHARED.theme.helpText)) self._dPen = QPen(SHARED.theme.helpText)
self._icon = SHARED.theme.getPixmap("proj_nwx", (iPx, iPx)) self._icon = SHARED.theme.getPixmap("proj_nwx", (iPx, iPx))
+108 -136
View File
@@ -23,6 +23,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations
import os import os
import sys import sys
@@ -42,7 +43,7 @@ OS_DARWIN = 3
# Utilities # Utilities
# =============================================================================================== # # =============================================================================================== #
def extractVersion(beQuiet=False): def extractVersion(beQuiet: bool = False) -> tuple[str, str, str]:
"""Extract the novelWriter version number without having to import """Extract the novelWriter version number without having to import
anything else from the main package. anything else from the main package.
""" """
@@ -73,34 +74,31 @@ def extractVersion(beQuiet=False):
return numVers, hexVers, relDate return numVers, hexVers, relDate
def compactVersion(version): def stripVersion(version: str) -> str:
"""Make the version number more compact.""" """Strip the pre-release part from a version number."""
return version.replace("-alpha", "a").replace("-beta", "b").replace("-rc", "rc") if "a" in version:
return version.partition("a")[0]
elif "b" in version:
return version.partition("b")[0]
elif "rc" in version:
return version.partition("rc")[0]
else:
return version
def sysCall(callArgs, cwd=None): def readFile(fileName: str) -> str:
"""Wrapper function for system calls."""
sysP = subprocess.Popen(
callArgs, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=True, cwd=cwd
)
stdOut, stdErr = sysP.communicate()
return stdOut.decode("utf-8"), stdErr.decode("utf-8"), sysP.returncode
def readFile(fileName):
"""Read an entire file and return as a string.""" """Read an entire file and return as a string."""
with open(fileName, mode="r", encoding="utf-8") as inFile: with open(fileName, mode="r", encoding="utf-8") as inFile:
return inFile.read() return inFile.read()
def writeFile(fileName, writeText): def writeFile(fileName: str, writeText: str) -> None:
"""Write string to file.""" """Write string to file."""
with open(fileName, mode="w+", encoding="utf-8") as outFile: with open(fileName, mode="w+", encoding="utf-8") as outFile:
outFile.write(writeText) outFile.write(writeText)
def toUpload(srcPath, dstName=None): def toUpload(srcPath: str, dstName: str | None = None) -> None:
"""Copy a file produced by one of the build functions to the upload """Copy a file produced by one of the build functions to the upload
directory. The file can optionally be given a new name. directory. The file can optionally be given a new name.
""" """
@@ -113,7 +111,7 @@ def toUpload(srcPath, dstName=None):
return return
def makeCheckSum(sumFile, cwd=None): def makeCheckSum(sumFile: str, cwd: str | None = None) -> str:
"""Create a SHA256 checksum file.""" """Create a SHA256 checksum file."""
try: try:
if cwd is None: if cwd is None:
@@ -139,7 +137,7 @@ def makeCheckSum(sumFile, cwd=None):
# Package Installer (pip) # Package Installer (pip)
## ##
def installPackages(hostOS): def installPackages(hostOS: int) -> None:
"""Install package dependencies both for this script and for running """Install package dependencies both for this script and for running
novelWriter itself. novelWriter itself.
""" """
@@ -172,7 +170,7 @@ def installPackages(hostOS):
# Clean Build and Dist Folders (build-clean) # Clean Build and Dist Folders (build-clean)
## ##
def cleanBuildDirs(): def cleanBuildDirs() -> None:
"""Recursively delete the 'build' and 'dist' folders.""" """Recursively delete the 'build' and 'dist' folders."""
print("") print("")
print("Cleaning up build environment ...") print("Cleaning up build environment ...")
@@ -208,7 +206,7 @@ def cleanBuildDirs():
# Build PDF Manual (manual) # Build PDF Manual (manual)
## ##
def buildPdfManual(): def buildPdfManual() -> None:
"""This function will build the documentation as manual.pdf.""" """This function will build the documentation as manual.pdf."""
print("") print("")
print("Building PDF Manual") print("Building PDF Manual")
@@ -262,7 +260,7 @@ def buildPdfManual():
# Qt Linguist QM Builder (qtlrelease) # Qt Linguist QM Builder (qtlrelease)
## ##
def buildQtI18n(): def buildQtI18n() -> None:
"""Build the lang.qm files for Qt Linguist.""" """Build the lang.qm files for Qt Linguist."""
print("") print("")
print("Building Qt Localisation Files") print("Building Qt Localisation Files")
@@ -315,7 +313,7 @@ def buildQtI18n():
# Qt Linguist TS Builder (qtlupdate) # Qt Linguist TS Builder (qtlupdate)
## ##
def buildQtI18nTS(sysArgs): def buildQtI18nTS(sysArgs: list[str]) -> None:
"""Build the lang.ts files for Qt Linguist.""" """Build the lang.ts files for Qt Linguist."""
print("") print("")
print("Building Qt Translation Files") print("Building Qt Translation Files")
@@ -392,16 +390,16 @@ def buildQtI18nTS(sysArgs):
## ##
# Generage MacOS PList # Generate MacOS PList
## ##
def genMacOSPlist(): def genMacOSPlist() -> None:
"""Set necessary values for .plist file for MacOS build.""" """Set necessary values for .plist file for MacOS build."""
outDir = "setup/macos" outDir = "setup/macos"
numVers = extractVersion()[0].partition("-")[0] numVers = stripVersion(extractVersion()[0])
copyrightYear = datetime.datetime.now().year copyrightYear = datetime.datetime.now().year
# These keys are no longer used but are present for compatability # These keys are no longer used but are present for compatibility
pkgVersMaj, pkgVersMin = numVers.split(".")[:2] pkgVersMaj, pkgVersMin = numVers.split(".")[:2]
plistXML = readFile(f"{outDir}/Info.plist.template").format( plistXML = readFile(f"{outDir}/Info.plist.template").format(
@@ -422,7 +420,7 @@ def genMacOSPlist():
# Sample Project ZIP File Builder (sample) # Sample Project ZIP File Builder (sample)
## ##
def buildSampleZip(): def buildSampleZip() -> None:
"""Bundle the sample project into a single zip file to be saved into """Bundle the sample project into a single zip file to be saved into
the novelwriter/assets folder for further bundling into builds. the novelwriter/assets folder for further bundling into builds.
""" """
@@ -459,7 +457,7 @@ def buildSampleZip():
return return
def cleanBuiltAssets(): def cleanBuiltAssets() -> None:
"""Remove assets built by this script.""" """Remove assets built by this script."""
print("") print("")
print("Removing Built Assets") print("Removing Built Assets")
@@ -488,7 +486,7 @@ def cleanBuiltAssets():
return return
def checkAssetsExist(): def checkAssetsExist() -> bool:
"""Check that the necessary compiled assets exist ahead of a build. """Check that the necessary compiled assets exist ahead of a build.
""" """
hasSample = False hasSample = False
@@ -523,7 +521,7 @@ def checkAssetsExist():
# Import Translations (import-i18n) # Import Translations (import-i18n)
## ##
def importI18nUpdates(sysArgs): def importI18nUpdates(sysArgs: list[str]) -> None:
"""Import new translation files from a zip file.""" """Import new translation files from a zip file."""
print("") print("")
print("Import Updated Translations") print("Import Updated Translations")
@@ -563,7 +561,7 @@ def importI18nUpdates(sysArgs):
# Make Minimal Package (minimal-zip) # Make Minimal Package (minimal-zip)
## ##
def makeMinimalPackage(targetOS): def makeMinimalPackage(targetOS: int) -> None:
"""Pack the core source file in a single zip file.""" """Pack the core source file in a single zip file."""
from zipfile import ZipFile, ZIP_DEFLATED from zipfile import ZipFile, ZIP_DEFLATED
@@ -598,8 +596,7 @@ def makeMinimalPackage(targetOS):
# Build Minimal Zip # Build Minimal Zip
# ================= # =================
numVers, _, _ = extractVersion() pkgVers, _, _ = extractVersion()
pkgVers = compactVersion(numVers)
zipFile = f"novelwriter-{pkgVers}-minimal{targName}.zip" zipFile = f"novelwriter-{pkgVers}-minimal{targName}.zip"
outFile = os.path.join(bldDir, zipFile) outFile = os.path.join(bldDir, zipFile)
if os.path.isfile(outFile): if os.path.isfile(outFile):
@@ -679,7 +676,8 @@ def makeMinimalPackage(targetOS):
# Make Debian Package (build-deb) # Make Debian Package (build-deb)
## ##
def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buildName=""): def makeDebianPackage(signKey: str | None = None, sourceBuild: bool = False,
distName: str = "unstable", buildName: str = "") -> str:
"""Build a Debian package.""" """Build a Debian package."""
print("") print("")
print("Build Debian Package") print("Build Debian Package")
@@ -692,13 +690,12 @@ def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buil
# ============ # ============
numVers, hexVers, relDate = extractVersion() numVers, hexVers, relDate = extractVersion()
pkgVers = compactVersion(numVers)
relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d") relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d")
pkgDate = email.utils.format_datetime(relDate.replace(hour=12, tzinfo=None)) pkgDate = email.utils.format_datetime(relDate.replace(hour=12, tzinfo=None))
print("") print("")
if buildName: pkgVers = numVers.replace("a", "~a").replace("b", "~b").replace("rc", "~rc")
pkgVers = f"{pkgVers}{buildName}" pkgVers = f"{pkgVers}+{buildName}" if buildName else pkgVers
# Set Up Folder # Set Up Folder
# ============= # =============
@@ -839,11 +836,7 @@ def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buil
print("") print("")
if sourceBuild: if sourceBuild:
if hexVers[-2] == "f": ppaName = "novelwriter" if hexVers[-2] == "f" else "novelwriter-pre"
ppaName = "novelwriter"
else:
ppaName = "novelwriter-pre"
return f"dput {ppaName}/{distName} {bldDir}/{bldPkg}_source.changes" return f"dput {ppaName}/{distName} {bldDir}/{bldPkg}_source.changes"
return "" return ""
@@ -853,14 +846,14 @@ def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buil
# Make Launchpad Package (build-ubuntu) # Make Launchpad Package (build-ubuntu)
## ##
def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False): def makeForLaunchpad(doSign: bool = False, isFirst: bool = False) -> None:
"""Wrapper for building Debian packages for Launchpad.""" """Wrapper for building Debian packages for Launchpad."""
print("") print("")
print("Launchpad Packages") print("Launchpad Packages")
print("==================") print("==================")
print("") print("")
if isFirst or isSnapshot: if isFirst:
bldNum = "0" bldNum = "0"
else: else:
bldNum = input("Build number [0]: ") bldNum = input("Build number [0]: ")
@@ -870,17 +863,12 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
distLoop = [ distLoop = [
("20.04", "focal"), ("20.04", "focal"),
("22.04", "jammy"), ("22.04", "jammy"),
("23.04", "lunar"),
("23.10", "mantic"), ("23.10", "mantic"),
("24.04", "noble"),
] ]
tStamp = datetime.datetime.now().strftime("%Y%m%d~%H%M%S") print("Building Ubuntu packages for:")
if isSnapshot: print("")
print(f"Building Ununtu SNAPSHOT~{tStamp} for:")
print("")
else:
print("Building Ubuntu packages for:")
print("")
for distNum, codeName in distLoop: for distNum, codeName in distLoop:
print(f" * Ubuntu {distNum} {codeName.title()}") print(f" * Ubuntu {distNum} {codeName.title()}")
print("") print("")
@@ -895,11 +883,7 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
dputCmd = [] dputCmd = []
for distNum, codeName in distLoop: for distNum, codeName in distLoop:
if isSnapshot: buildName = f"ubuntu{distNum}.{bldNum}"
buildName = f"+SNAPSHOT~{tStamp}~ubuntu{distNum}.0"
else:
buildName = f"~ubuntu{distNum}.{bldNum}"
dCmd = makeDebianPackage( dCmd = makeDebianPackage(
signKey=signKey, signKey=signKey,
sourceBuild=True, sourceBuild=True,
@@ -922,7 +906,7 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
# Make AppImage (build-appimage) # Make AppImage (build-appimage)
## ##
def makeAppImage(sysArgs): def makeAppImage(sysArgs: list[str]) -> list[str]:
"""Build an AppImage.""" """Build an AppImage."""
import glob import glob
import argparse import argparse
@@ -968,8 +952,7 @@ def makeAppImage(sysArgs):
# Version Info # Version Info
# ============ # ============
numVers, _, relDate = extractVersion() pkgVers, _, relDate = extractVersion()
pkgVers = compactVersion(numVers)
relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d") relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d")
print("") print("")
@@ -1139,7 +1122,7 @@ def makeAppImage(sysArgs):
# Make Windows Setup EXE (build-win-exe) # Make Windows Setup EXE (build-win-exe)
## ##
def makeWindowsEmbedded(sysArgs): def makeWindowsEmbedded(sysArgs: list[str]) -> None:
"""Set up a package with embedded Python and dependencies for """Set up a package with embedded Python and dependencies for
Windows installation. Windows installation.
""" """
@@ -1371,7 +1354,7 @@ def makeWindowsEmbedded(sysArgs):
# XDG Installation (xdg-install) # XDG Installation (xdg-install)
## ##
def xdgInstall(): def xdgInstall() -> None:
"""Will attempt to install icons and make a launcher.""" """Will attempt to install icons and make a launcher."""
print("") print("")
print("XDG Install") print("XDG Install")
@@ -1507,7 +1490,7 @@ def xdgInstall():
# XDG Uninstallation (xdg-uninstall) # XDG Uninstallation (xdg-uninstall)
## ##
def xdgUninstall(): def xdgUninstall() -> None:
"""Will attempt to uninstall icons and the launcher.""" """Will attempt to uninstall icons and the launcher."""
print("") print("")
print("XDG Uninstall") print("XDG Uninstall")
@@ -1577,7 +1560,7 @@ def xdgUninstall():
# WIN Installation (win-install) # WIN Installation (win-install)
## ##
def winInstall(): def winInstall() -> None:
"""Will attempt to install icons and make a launcher for Windows.""" """Will attempt to install icons and make a launcher for Windows."""
import winreg import winreg
try: try:
@@ -1704,7 +1687,7 @@ def winInstall():
# WIN Uninstallation (win-uninstall) # WIN Uninstallation (win-uninstall)
## ##
def winUninstall(): def winUninstall() -> None:
"""Will attempt to uninstall icons previously installed.""" """Will attempt to uninstall icons previously installed."""
import winreg import winreg
try: try:
@@ -1805,40 +1788,35 @@ if __name__ == "__main__":
else: else:
hostOS = OS_NONE hostOS = OS_NONE
sysArgs = sys.argv.copy()
# Set Target OS # Set Target OS
if "--target-linux" in sys.argv: if "--target-linux" in sysArgs:
sys.argv.remove("--target-linux") sysArgs.remove("--target-linux")
targetOS = OS_LINUX targetOS = OS_LINUX
elif "--target-darwin" in sys.argv: elif "--target-darwin" in sysArgs:
sys.argv.remove("--target-darwin") sysArgs.remove("--target-darwin")
targetOS = OS_DARWIN targetOS = OS_DARWIN
elif "--target-win" in sys.argv: elif "--target-win" in sysArgs:
sys.argv.remove("--target-win") sysArgs.remove("--target-win")
targetOS = OS_WIN targetOS = OS_WIN
else: else:
targetOS = hostOS targetOS = hostOS
# Sign package # Sign package
if "--sign" in sys.argv: if "--sign" in sysArgs:
sys.argv.remove("--sign") sysArgs.remove("--sign")
doSign = True doSign = True
else: else:
doSign = False doSign = False
# First build # First build
if "--first" in sys.argv: if "--first" in sysArgs:
sys.argv.remove("--first") sysArgs.remove("--first")
isFirstBuild = True isFirstBuild = True
else: else:
isFirstBuild = False isFirstBuild = False
# Build snapshot
if "--snapshot" in sys.argv:
sys.argv.remove("--snapshot")
isSnapshot = True
else:
isSnapshot = False
helpMsg = [ helpMsg = [
"", "",
"novelWriter Setup Tool", "novelWriter Setup Tool",
@@ -1856,7 +1834,7 @@ if __name__ == "__main__":
"", "",
" help Print the help message.", " help Print the help message.",
" pip Install all package dependencies for novelWriter using pip.", " pip Install all package dependencies for novelWriter using pip.",
" version Print the novelWriter version. Add -c for short version.", " version Print the novelWriter version.",
" build-clean Will attempt to delete 'build' and 'dist' folders.", " build-clean Will attempt to delete 'build' and 'dist' folders.",
"", "",
"Additional Builds:", "Additional Builds:",
@@ -1880,7 +1858,6 @@ if __name__ == "__main__":
" sign package.", " sign package.",
" build-ubuntu Build a .deb packages Launchpad. Add --sign to ", " build-ubuntu Build a .deb packages Launchpad. Add --sign to ",
" sign package. Add --first to set build number to 0.", " sign package. Add --first to set build number to 0.",
" Add --snapshot to make a snapshot package.",
" build-win-exe Build a setup.exe file with Python embedded for Windows.", " build-win-exe Build a setup.exe file with Python embedded for Windows.",
" The package must be built from a minimal windows zip file.", " The package must be built from a minimal windows zip file.",
" build-appimage Build an AppImage. Argument --linux-tag defaults to", " build-appimage Build an AppImage. Argument --linux-tag defaults to",
@@ -1905,71 +1882,66 @@ if __name__ == "__main__":
# General # General
# ======= # =======
if "help" in sys.argv: if "help" in sysArgs:
sys.argv.remove("help") sysArgs.remove("help")
print("\n".join(helpMsg)) print("\n".join(helpMsg))
sys.exit(0) sys.exit(0)
if "version" in sys.argv: if "version" in sysArgs:
sys.argv.remove("version") sysArgs.remove("version")
numVers, _, _ = extractVersion(beQuiet=True) print(extractVersion(beQuiet=True)[0], end=None)
if "-c" in sys.argv:
sys.argv.remove("-c")
print(compactVersion(numVers), end=None)
else:
print(numVers, end=None)
sys.exit(0) sys.exit(0)
if "pip" in sys.argv: if "pip" in sysArgs:
sys.argv.remove("pip") sysArgs.remove("pip")
installPackages(hostOS) installPackages(hostOS)
if "build-clean" in sys.argv: if "build-clean" in sysArgs:
sys.argv.remove("build-clean") sysArgs.remove("build-clean")
cleanBuildDirs() cleanBuildDirs()
# Additional Builds # Additional Builds
# ================= # =================
if "manual" in sys.argv: if "manual" in sysArgs:
sys.argv.remove("manual") sysArgs.remove("manual")
buildPdfManual() buildPdfManual()
if "qtlrelease" in sys.argv: if "qtlrelease" in sysArgs:
sys.argv.remove("qtlrelease") sysArgs.remove("qtlrelease")
buildQtI18n() buildQtI18n()
if "qtlupdate" in sys.argv: if "qtlupdate" in sysArgs:
sys.argv.remove("qtlupdate") sysArgs.remove("qtlupdate")
buildQtI18nTS(sys.argv) buildQtI18nTS(sysArgs)
sys.exit(0) # Don't continue execution sys.exit(0) # Don't continue execution
if "sample" in sys.argv: if "sample" in sysArgs:
sys.argv.remove("sample") sysArgs.remove("sample")
buildSampleZip() buildSampleZip()
if "clean-assets" in sys.argv: if "clean-assets" in sysArgs:
sys.argv.remove("clean-assets") sysArgs.remove("clean-assets")
cleanBuiltAssets() cleanBuiltAssets()
if "gen-plist" in sys.argv: if "gen-plist" in sysArgs:
sys.argv.remove("gen-plist") sysArgs.remove("gen-plist")
genMacOSPlist() genMacOSPlist()
# Python Packaging # Python Packaging
# ================ # ================
if "import-i18n" in sys.argv: if "import-i18n" in sysArgs:
sys.argv.remove("import-i18n") sysArgs.remove("import-i18n")
importI18nUpdates(sys.argv) importI18nUpdates(sysArgs)
sys.exit(0) # Don't continue execution sys.exit(0) # Don't continue execution
if "minimal-zip" in sys.argv: if "minimal-zip" in sysArgs:
sys.argv.remove("minimal-zip") sysArgs.remove("minimal-zip")
makeMinimalPackage(targetOS) makeMinimalPackage(targetOS)
if "build-deb" in sys.argv: if "build-deb" in sysArgs:
sys.argv.remove("build-deb") sysArgs.remove("build-deb")
if hostOS == OS_LINUX: if hostOS == OS_LINUX:
if doSign: if doSign:
signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08"
@@ -1980,23 +1952,23 @@ if __name__ == "__main__":
print("ERROR: Command 'build-deb' can only be used on Linux") print("ERROR: Command 'build-deb' can only be used on Linux")
sys.exit(1) sys.exit(1)
if "build-ubuntu" in sys.argv: if "build-ubuntu" in sysArgs:
sys.argv.remove("build-ubuntu") sysArgs.remove("build-ubuntu")
if hostOS == OS_LINUX: if hostOS == OS_LINUX:
makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild, isSnapshot=isSnapshot) makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild)
else: else:
print("ERROR: Command 'build-ubuntu' can only be used on Linux") print("ERROR: Command 'build-ubuntu' can only be used on Linux")
sys.exit(1) sys.exit(1)
if "build-win-exe" in sys.argv: if "build-win-exe" in sysArgs:
sys.argv.remove("build-win-exe") sysArgs.remove("build-win-exe")
makeWindowsEmbedded(sys.argv) makeWindowsEmbedded(sysArgs)
sys.exit(0) # Don't continue execution sys.exit(0) # Don't continue execution
if "build-appimage" in sys.argv: if "build-appimage" in sysArgs:
sys.argv.remove("build-appimage") sysArgs.remove("build-appimage")
if hostOS == OS_LINUX: if hostOS == OS_LINUX:
sys.argv = makeAppImage(sys.argv) # Build appimage and prune its args sysArgs = makeAppImage(sysArgs)
else: else:
print("ERROR: Command 'build-appimage' can only be used on Linux") print("ERROR: Command 'build-appimage' can only be used on Linux")
sys.exit(1) sys.exit(1)
@@ -2004,32 +1976,32 @@ if __name__ == "__main__":
# General Installers # General Installers
# ================== # ==================
if "xdg-install" in sys.argv: if "xdg-install" in sysArgs:
sys.argv.remove("xdg-install") sysArgs.remove("xdg-install")
if hostOS == OS_WIN: if hostOS == OS_WIN:
print("ERROR: Command 'xdg-install' cannot be used on Windows") print("ERROR: Command 'xdg-install' cannot be used on Windows")
sys.exit(1) sys.exit(1)
else: else:
xdgInstall() xdgInstall()
if "xdg-uninstall" in sys.argv: if "xdg-uninstall" in sysArgs:
sys.argv.remove("xdg-uninstall") sysArgs.remove("xdg-uninstall")
if hostOS == OS_WIN: if hostOS == OS_WIN:
print("ERROR: Command 'xdg-uninstall' cannot be used on Windows") print("ERROR: Command 'xdg-uninstall' cannot be used on Windows")
sys.exit(1) sys.exit(1)
else: else:
xdgUninstall() xdgUninstall()
if "win-install" in sys.argv: if "win-install" in sysArgs:
sys.argv.remove("win-install") sysArgs.remove("win-install")
if hostOS == OS_WIN: if hostOS == OS_WIN:
winInstall() winInstall()
else: else:
print("ERROR: Command 'win-install' can only be used on Windows") print("ERROR: Command 'win-install' can only be used on Windows")
sys.exit(1) sys.exit(1)
if "win-uninstall" in sys.argv: if "win-uninstall" in sysArgs:
sys.argv.remove("win-uninstall") sysArgs.remove("win-uninstall")
if hostOS == OS_WIN: if hostOS == OS_WIN:
winUninstall() winUninstall()
else: else:
+2 -3
View File
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.3-alpha1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-12-17 17:48:43"> <novelWriterXML appVersion="2.3a1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-26 22:42:30">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1617" autoCount="255" editTime="81245"> <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1620" autoCount="255" editTime="81255">
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
</project> </project>
<settings> <settings>
+1 -1
View File
@@ -24,7 +24,7 @@ echo "Build Dir: $BUILD_DIR"
pushd "$SRC_DIR" || exit 1 pushd "$SRC_DIR" || exit 1
VERSION="$(python3 pkgutils.py version -c)" VERSION="$(python3 pkgutils.py version)"
echo "novelWriter Version: $VERSION" echo "novelWriter Version: $VERSION"
# --- Prepare Files ----------------------------------------------------------------------------- # # --- Prepare Files ----------------------------------------------------------------------------- #
-28
View File
@@ -1,28 +0,0 @@
#!/bin/bash
set -e
ENVPATH=/tmp/nwBuild
if [ ! -f pkgutils.py ]; then
echo "Must be called from the root folder of the source"
exit 1
fi
echo ""
echo " Building Dependencies"
echo "================================================================================"
echo ""
if [ ! -d $ENVPATH ]; then
python3 -m venv $ENVPATH
fi
source $ENVPATH/bin/activate
pip3 install -r docs/source/requirements.txt
python3 pkgutils.py clean-assets
python3 pkgutils.py qtlrelease manual sample
deactivate
echo ""
echo " Building Linux Snapshots"
echo "================================================================================"
echo ""
python3 pkgutils.py build-ubuntu --sign --snapshot
+1 -2
View File
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="1" timeStamp="2022-11-07 13:00:48"> <novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="2" timeStamp="2022-11-07 13:00:48">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="5" autoCount="10" editTime="1000"> <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="5" autoCount="10" editTime="1000">
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
</project> </project>
<settings> <settings>
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0.7" hexVersion="0x020007f0" fileVersion="1.5" fileRevision="1" timeStamp="2023-06-03 18:18:23"> <novelWriterXML appVersion="2.3a1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-26 22:50:57">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0">
<name>New Project</name> <name>New Project</name>
<title>New Novel</title>
<author>Jane Doe</author> <author>Jane Doe</author>
</project> </project>
<settings> <settings>
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0.7" hexVersion="0x020007f0" fileVersion="1.5" fileRevision="1" timeStamp="2023-06-03 18:18:23"> <novelWriterXML appVersion="2.3a1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-26 22:50:57">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0">
<name>New Project</name> <name>New Project</name>
<title>New Novel</title>
<author>Jane Doe</author> <author>Jane Doe</author>
</project> </project>
<settings> <settings>
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.1-beta1" hexVersion="0x020100b1" fileVersion="1.5" fileRevision="1" timeStamp="2023-07-20 20:33:41"> <novelWriterXML appVersion="2.3a1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-26 22:53:40">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0">
<name>New Project</name> <name>New Project</name>
<title>New Novel</title>
<author>Jane Doe</author> <author>Jane Doe</author>
</project> </project>
<settings> <settings>
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.3-alpha1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-12-29 14:34:07"> <novelWriterXML appVersion="2.3a1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-26 22:53:40">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="0" editTime="0"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="0" editTime="0">
<name>Test Project A</name> <name>Test Project A</name>
<title>Test Project A</title>
<author>Jane Doe</author> <author>Jane Doe</author>
</project> </project>
<settings> <settings>
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.3-alpha1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-12-29 14:36:07"> <novelWriterXML appVersion="2.3a1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-26 22:53:40">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="0" editTime="0"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="0" editTime="0">
<name>Test Project B</name> <name>Test Project B</name>
<title>Test Project B</title>
<author>Jane Doe</author> <author>Jane Doe</author>
</project> </project>
<settings> <settings>
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-10-17 20:52:34"> <novelWriterXML appVersion="2.3a1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-26 22:48:13">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="4"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="3">
<name>New Project</name> <name>New Project</name>
<title>New Novel</title>
<author>Jane Doe</author> <author>Jane Doe</author>
</project> </project>
<settings> <settings>
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0.7" hexVersion="0x020007f0" fileVersion="1.5" fileRevision="1" timeStamp="2023-06-03 18:18:30"> <novelWriterXML appVersion="2.3a1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-26 22:45:36">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="2" autoCount="1" editTime="0"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="2" autoCount="1" editTime="0">
<name>New Project</name> <name>New Project</name>
<title>New Novel</title>
<author>Jane Doe</author> <author>Jane Doe</author>
</project> </project>
<settings> <settings>
@@ -1,10 +1,9 @@
{ {
"meta": { "meta": {
"projectName": "Lorem Ipsum", "projectName": "Lorem Ipsum",
"novelTitle": "Lorem Ipsum",
"novelAuthor": "lipsum.com", "novelAuthor": "lipsum.com",
"buildTime": 1687620769, "buildTime": 1706306020,
"buildTimeStr": "2023-06-24 17:32:49" "buildTimeStr": "2024-01-26 22:53:40"
}, },
"text": { "text": {
"css": [ "css": [
@@ -1,10 +1,9 @@
{ {
"meta": { "meta": {
"projectName": "Lorem Ipsum", "projectName": "Lorem Ipsum",
"novelTitle": "Lorem Ipsum",
"novelAuthor": "lipsum.com", "novelAuthor": "lipsum.com",
"buildTime": 1687621222, "buildTime": 1706306020,
"buildTimeStr": "2023-06-24 17:40:22" "buildTimeStr": "2024-01-26 22:53:40"
}, },
"text": { "text": {
"nwd": [ "nwd": [
+1 -2
View File
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="1" timeStamp="2020-05-28 09:59:15"> <novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="2" timeStamp="2020-05-28 09:59:15">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="0" autoCount="0" editTime="1000"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="0" autoCount="0" editTime="1000">
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title>
<author>Jay Doh</author> <author>Jay Doh</author>
</project> </project>
<settings> <settings>
+1 -2
View File
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="1" timeStamp="2020-06-26 21:20:24"> <novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="2" timeStamp="2020-06-26 21:20:24">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000">
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title>
<author>Jay Doh</author> <author>Jay Doh</author>
</project> </project>
<settings> <settings>
+1 -2
View File
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="1" timeStamp="2021-08-30 23:33:44"> <novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="2" timeStamp="2021-08-30 23:33:44">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000">
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title>
<author>Jay Doh</author> <author>Jay Doh</author>
</project> </project>
<settings> <settings>
+1 -2
View File
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="1" timeStamp="2022-10-25 18:26:15"> <novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="2" timeStamp="2022-10-25 18:26:15">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000">
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title>
<author>Jay Doh</author> <author>Jay Doh</author>
</project> </project>
<settings> <settings>
+1 -2
View File
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="1" timeStamp="2022-10-15 12:12:59"> <novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="2" timeStamp="2022-10-15 12:12:59">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000">
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title>
<author>Jay Doh</author> <author>Jay Doh</author>
</project> </project>
<settings> <settings>
+12 -1
View File
@@ -35,7 +35,7 @@ from PyQt5.QtCore import QUrl
from novelwriter.common import ( from novelwriter.common import (
checkBool, checkFloat, checkHandle, checkInt, checkIntTuple, checkPath, checkBool, checkFloat, checkHandle, checkInt, checkIntTuple, checkPath,
checkString, checkStringNone, checkUuid, formatInt, formatTime, checkString, checkStringNone, checkUuid, formatInt, formatTime,
formatTimeStamp, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass,
isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax, isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax,
numberToRoman, NWConfigParser, openExternalPath, readTextFile, simplified, numberToRoman, NWConfigParser, openExternalPath, readTextFile, simplified,
transferCase, xmlIndent, yesNo transferCase, xmlIndent, yesNo
@@ -348,6 +348,17 @@ def testBaseCommon_formatTime():
# END Test testBaseCommon_formatTime # END Test testBaseCommon_formatTime
@pytest.mark.base
def testBaseCommon_formatVersion():
"""Test the formatVersion function."""
assert formatVersion("1.2") == "1.2"
assert formatVersion("1.2a1") == "1.2 Alpha 1"
assert formatVersion("1.2b2") == "1.2 Beta 2"
assert formatVersion("1.2rc3") == "1.2 RC 3"
# END Test testBaseCommon_formatVersion
@pytest.mark.base @pytest.mark.base
def testBaseCommon_simplified(): def testBaseCommon_simplified():
"""Test the simplified function.""" """Test the simplified function."""
+2 -2
View File
@@ -230,7 +230,7 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
assert error == [] assert error == []
copyfile(docFile, tstFile) copyfile(docFile, tstFile)
assert cmpFiles(tstFile, cmpFile, ignoreLines=[6, 7]) assert cmpFiles(tstFile, cmpFile, ignoreLines=[5, 6])
# Check Error Handling # Check Error Handling
# ==================== # ====================
@@ -370,7 +370,7 @@ def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
assert error == [] assert error == []
copyfile(docFile, tstFile) copyfile(docFile, tstFile)
assert cmpFiles(tstFile, cmpFile, ignoreLines=[6, 7]) assert cmpFiles(tstFile, cmpFile, ignoreLines=[5, 6])
# Check Error Handling # Check Error Handling
# ==================== # ====================
+5
View File
@@ -695,6 +695,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
"This is a story about Jane Smith.\n\n" "This is a story about Jane Smith.\n\n"
"Well, not really. She's still awesome though.\n" "Well, not really. She's still awesome though.\n"
)) ))
# Whole document # Whole document
cC, wC, pC = index.getCounts(nHandle) cC, wC, pC = index.getCounts(nHandle)
assert cC == 152 assert cC == 152
@@ -798,6 +799,10 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
# Extract stats # Extract stats
assert index.getNovelWordCount(activeOnly=False) == 43 assert index.getNovelWordCount(activeOnly=False) == 43
assert index.getNovelWordCount(activeOnly=True) == 15 assert index.getNovelWordCount(activeOnly=True) == 15
assert index.getNovelWordCount(rootHandle=C.hNovelRoot, activeOnly=False) == 43
assert index.getNovelWordCount(rootHandle=C.hNovelRoot, activeOnly=True) == 15
assert index.getNovelWordCount(rootHandle=C.hWorldRoot, activeOnly=False) == 0
assert index.getNovelWordCount(rootHandle=C.hWorldRoot, activeOnly=True) == 0
assert index.getNovelTitleCounts(activeOnly=False) == [0, 3, 2, 3, 0] assert index.getNovelTitleCounts(activeOnly=False) == [0, 3, 2, 3, 0]
assert index.getNovelTitleCounts(activeOnly=True) == [0, 1, 2, 3, 0] assert index.getNovelTitleCounts(activeOnly=True) == [0, 1, 2, 3, 0]
+16 -16
View File
@@ -121,27 +121,27 @@ def testCoreOptions_SetGet(mockGUI):
assert options.setValue("GuiProjectSettings", "winWidth", 100) is True assert options.setValue("GuiProjectSettings", "winWidth", 100) is True
# Set some values of different types # Set some values of different types
assert options.setValue("GuiProjectDetails", "winWidth", 100) is True assert options.setValue("GuiNovelDetails", "winWidth", 100) is True
assert options.setValue("GuiProjectDetails", "winHeight", 12.34) is True assert options.setValue("GuiNovelDetails", "winHeight", 12.34) is True
assert options.setValue("GuiProjectDetails", "clearDouble", True) is True assert options.setValue("GuiNovelDetails", "clearDouble", True) is True
assert options.setValue("GuiNovelView", "lastCol", nwColHidden) is True assert options.setValue("GuiNovelView", "lastCol", nwColHidden) is True
# Generic get, doesn't check type # Generic get, doesn't check type
assert options.getValue("GuiProjectDetails", "winWidth", None) == 100 assert options.getValue("GuiNovelDetails", "winWidth", None) == 100
assert options.getValue("GuiProjectDetails", "winHeight", None) == 12.34 assert options.getValue("GuiNovelDetails", "winHeight", None) == 12.34
assert options.getValue("GuiProjectDetails", "clearDouble", None) is True assert options.getValue("GuiNovelDetails", "clearDouble", None) is True
assert options.getValue("GuiProjectDetails", "mockItem", None) is None assert options.getValue("GuiNovelDetails", "mockItem", None) is None
# Get type-specific # Get type-specific
assert options.getString("GuiProjectDetails", "winWidth", None) is None # type: ignore assert options.getString("GuiNovelDetails", "winWidth", None) is None # type: ignore
assert options.getString("GuiProjectDetails", "mockItem", None) is None # type: ignore assert options.getString("GuiNovelDetails", "mockItem", None) is None # type: ignore
assert options.getInt("GuiProjectDetails", "winWidth", None) == 100 # type: ignore assert options.getInt("GuiNovelDetails", "winWidth", None) == 100 # type: ignore
assert options.getInt("GuiProjectDetails", "textFont", None) is None # type: ignore assert options.getInt("GuiNovelDetails", "textFont", None) is None # type: ignore
assert options.getInt("GuiProjectDetails", "mockItem", None) is None # type: ignore assert options.getInt("GuiNovelDetails", "mockItem", None) is None # type: ignore
assert options.getFloat("GuiProjectDetails", "winWidth", None) == 100.0 # type: ignore assert options.getFloat("GuiNovelDetails", "winWidth", None) == 100.0 # type: ignore
assert options.getFloat("GuiProjectDetails", "mockItem", None) is None # type: ignore assert options.getFloat("GuiNovelDetails", "mockItem", None) is None # type: ignore
assert options.getBool("GuiProjectDetails", "clearDouble", None) is True # type: ignore assert options.getBool("GuiNovelDetails", "clearDouble", None) is True # type: ignore
assert options.getBool("GuiProjectDetails", "mockItem", None) is None # type: ignore assert options.getBool("GuiNovelDetails", "mockItem", None) is None # type: ignore
assert options.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, nwColHidden) == nwColHidden assert options.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, nwColHidden) == nwColHidden
# Get from non-existent groups # Get from non-existent groups
-4
View File
@@ -482,10 +482,6 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
project.data.setName(" A Name ") project.data.setName(" A Name ")
assert project.data.name == "A Name" assert project.data.name == "A Name"
# Project Title
project.data.setTitle(" A Title ")
assert project.data.title == "A Title"
# Project Author # Project Author
project.data.setAuthor(" Jane\tDoe ") project.data.setAuthor(" Jane\tDoe ")
assert project.data.author == "Jane Doe" assert project.data.author == "Jane Doe"
+1 -7
View File
@@ -131,13 +131,12 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
assert xmlReader.state == XMLReadState.PARSED_OK assert xmlReader.state == XMLReadState.PARSED_OK
assert xmlReader.xmlRoot == "novelWriterXML" assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0105 assert xmlReader.xmlVersion == 0x0105
assert xmlReader.xmlRevision == 1 assert xmlReader.xmlRevision == 2
assert xmlReader.appVersion == "2.0-rc1" assert xmlReader.appVersion == "2.0-rc1"
assert xmlReader.hexVersion == 0x020000c1 assert xmlReader.hexVersion == 0x020000c1
# Check loaded data # Check loaded data
assert data.name == "Sample Project" assert data.name == "Sample Project"
assert data.title == "Sample Project"
assert data.author == "Jane Smith" assert data.author == "Jane Smith"
assert data.saveCount == 5 assert data.saveCount == 5
assert data.autoCount == 10 assert data.autoCount == 10
@@ -256,7 +255,6 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd):
# Check loaded data # Check loaded data
assert data.name == "Sample Project" assert data.name == "Sample Project"
assert data.title == "Sample Project"
assert data.author == "Jay Doh" # Only last author is preserved assert data.author == "Jay Doh" # Only last author is preserved
assert data.saveCount == 0 # Doesn't exist in 1.0 assert data.saveCount == 0 # Doesn't exist in 1.0
assert data.autoCount == 0 # Doesn't exist in 1.0 assert data.autoCount == 0 # Doesn't exist in 1.0
@@ -391,7 +389,6 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd):
# Check loaded data # Check loaded data
assert data.name == "Sample Project" assert data.name == "Sample Project"
assert data.title == "Sample Project"
assert data.author == "Jay Doh" # Only last author is preserved assert data.author == "Jay Doh" # Only last author is preserved
assert data.saveCount == 5 assert data.saveCount == 5
assert data.autoCount == 10 assert data.autoCount == 10
@@ -526,7 +523,6 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd):
# Check loaded data # Check loaded data
assert data.name == "Sample Project" assert data.name == "Sample Project"
assert data.title == "Sample Project"
assert data.author == "Jay Doh" # Only last author is preserved assert data.author == "Jay Doh" # Only last author is preserved
assert data.saveCount == 5 assert data.saveCount == 5
assert data.autoCount == 10 assert data.autoCount == 10
@@ -664,7 +660,6 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd):
# Check loaded data # Check loaded data
assert data.name == "Sample Project" assert data.name == "Sample Project"
assert data.title == "Sample Project"
assert data.author == "Jay Doh" # Only last author is preserved assert data.author == "Jay Doh" # Only last author is preserved
assert data.saveCount == 5 assert data.saveCount == 5
assert data.autoCount == 10 assert data.autoCount == 10
@@ -802,7 +797,6 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd):
# Check loaded data # Check loaded data
assert data.name == "Sample Project" assert data.name == "Sample Project"
assert data.title == "Sample Project"
assert data.author == "Jay Doh" # Only last author is preserved assert data.author == "Jay Doh" # Only last author is preserved
assert data.saveCount == 5 assert data.saveCount == 5
assert data.autoCount == 10 assert data.autoCount == 10
+12 -6
View File
@@ -22,9 +22,11 @@ from __future__ import annotations
import pytest import pytest
from tools import getGuiItem
from PyQt5.QtGui import QFontDatabase, QKeyEvent from PyQt5.QtGui import QFontDatabase, QKeyEvent
from PyQt5.QtCore import QEvent, Qt from PyQt5.QtCore import QEvent, Qt
from PyQt5.QtWidgets import QDialogButtonBox, QFileDialog, QFontDialog from PyQt5.QtWidgets import QAction, QDialogButtonBox, QFileDialog, QFontDialog
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwConst, nwUnicode from novelwriter.constants import nwConst, nwUnicode
@@ -38,9 +40,13 @@ KEY_DELAY = 1
def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths): def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths):
"""Test the preferences dialog loading.""" """Test the preferences dialog loading."""
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")])
monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None)
# Load GUI with standard values # Load GUI with standard values
prefs = GuiPreferences(nwGUI) nwGUI.mainMenu.aPreferences.activate(QAction.ActionEvent.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiPreferences") is not None, timeout=1000)
prefs = getGuiItem("GuiPreferences")
assert isinstance(prefs, GuiPreferences)
prefs.show() prefs.show()
# Check Languages # Check Languages
@@ -95,10 +101,6 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
# Check Navigation # Check Navigation
vBar = prefs.mainForm.verticalScrollBar() vBar = prefs.mainForm.verticalScrollBar()
old = -1 old = -1
with qtbot.waitSignal(vBar.valueChanged) as value:
prefs.sidebar.button(0).click()
assert value.args[0] > old
old = value.args[0]
with qtbot.waitSignal(vBar.valueChanged) as value: with qtbot.waitSignal(vBar.valueChanged) as value:
prefs.sidebar.button(1).click() prefs.sidebar.button(1).click()
assert value.args[0] > old assert value.args[0] > old
@@ -107,6 +109,10 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
prefs.sidebar.button(2).click() prefs.sidebar.button(2).click()
assert value.args[0] > old assert value.args[0] > old
old = value.args[0] old = value.args[0]
with qtbot.waitSignal(vBar.valueChanged) as value:
prefs.sidebar.button(3).click()
assert value.args[0] > old
old = value.args[0]
# Check Search # Check Search
prefs.searchText.setText("Display language") prefs.searchText.setText("Display language")
-109
View File
@@ -1,109 +0,0 @@
"""
novelWriter Project Details Dialog Class Tester
=================================================
This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import pytest
from tools import getGuiItem
from PyQt5.QtWidgets import QAction
from novelwriter import SHARED
from novelwriter.dialogs.projdetails import GuiProjectDetails
@pytest.mark.gui
def testDlgProjDetails_Dialog(qtbot, nwGUI, prjLipsum):
"""Test the project details dialog.
"""
# Create a project to work on
assert nwGUI.openProject(prjLipsum)
assert nwGUI.rebuildIndex(beQuiet=True)
qtbot.wait(100)
# Open the Writing Stats dialog
nwGUI.mainMenu.aProjectDetails.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiProjectDetails") is not None, timeout=1000)
projDet = getGuiItem("GuiProjectDetails")
assert isinstance(projDet, GuiProjectDetails)
# Overview Page
# =============
assert projDet.tabMain.bookTitle.text() == "Lorem Ipsum"
assert projDet.tabMain.projName.text()[-11:] == "Lorem Ipsum"
assert projDet.tabMain.bookAuthors.text()[-10:] == "lipsum.com"
assert projDet.tabMain.wordCountVal.text() == f"{3000:n}"
assert projDet.tabMain.chapCountVal.text() == f"{3:n}"
assert projDet.tabMain.sceneCountVal.text() == f"{5:n}"
assert projDet.tabMain.revCountVal.text() == f"{SHARED.project.data.saveCount:n}"
assert projDet.tabMain.projPathVal.text() == str(prjLipsum)
# Contents Page
# =============
tocTab = projDet.tabContents
tocTree = tocTab.tocTree
assert tocTree.topLevelItemCount() == 7
assert tocTree.topLevelItem(0).text(tocTab.C_TITLE) == "Lorem Ipsum" # type: ignore
assert tocTree.topLevelItem(2).text(tocTab.C_TITLE) == "Prologue" # type: ignore
assert tocTree.topLevelItem(3).text(tocTab.C_TITLE) == "Act One" # type: ignore
assert tocTree.topLevelItem(4).text(tocTab.C_TITLE) == "Chapter One" # type: ignore
assert tocTree.topLevelItem(5).text(tocTab.C_TITLE) == "Chapter Two" # type: ignore
assert tocTree.topLevelItem(6).text(tocTab.C_TITLE) == "END" # type: ignore
# Count Pages
tocTab.wpValue.setValue(100)
tocTab.poValue.setValue(4)
tocTab.dblValue.setChecked(False)
tocTab._populateTree()
thePages = ["1", "2", "1", "1", "11", "17", "0"]
thePage = ["i", "ii", "1", "2", "3", "14", "31"]
for i in range(7):
assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] # type: ignore
assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] # type: ignore
tocTab.poValue.setValue(5)
tocTab.dblValue.setChecked(True)
tocTab._populateTree()
thePages = ["2", "2", "2", "2", "12", "18", "0"]
thePage = ["i", "iii", "1", "3", "5", "17", "35"]
for i in range(7):
assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] # type: ignore
assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] # type: ignore
# Re-populate
assert tocTab._currentRoot is None
tocTab._novelValueChanged("7a992350f3eb6") # Not a root
assert tocTab._currentRoot == "b3643d0f92e32" # The actual novel root
# qtbot.stop()
# Clean Up
projDet.close()
nwGUI.closeMain()
# END Test testDlgProjDetails_Dialog
@@ -31,7 +31,7 @@ from PyQt5.QtWidgets import QDialog, QAction, QColorDialog
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemType from novelwriter.enum import nwItemType
from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.dialogs.projsettings import GuiProjectSettings from novelwriter.dialogs.projectsettings import GuiProjectSettings
KEY_DELAY = 1 KEY_DELAY = 1
@@ -63,17 +63,17 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI):
qtbot.addWidget(projSettings) qtbot.addWidget(projSettings)
# Switch Tabs # Switch Tabs
projSettings._focusTab(GuiProjectSettings.TAB_REPLACE) projSettings.sidebar.button(GuiProjectSettings.PAGE_SETTINGS).click()
assert projSettings._tabBox.currentWidget() == projSettings.tabReplace assert projSettings.mainStack.currentWidget() == projSettings.settingsPage
projSettings._focusTab(GuiProjectSettings.TAB_IMPORT) projSettings.sidebar.button(GuiProjectSettings.PAGE_STATUS).click()
assert projSettings._tabBox.currentWidget() == projSettings.tabImport assert projSettings.mainStack.currentWidget() == projSettings.statusPage
projSettings._focusTab(GuiProjectSettings.TAB_STATUS) projSettings.sidebar.button(GuiProjectSettings.PAGE_IMPORT).click()
assert projSettings._tabBox.currentWidget() == projSettings.tabStatus assert projSettings.mainStack.currentWidget() == projSettings.importPage
projSettings._focusTab(GuiProjectSettings.TAB_MAIN) projSettings.sidebar.button(GuiProjectSettings.PAGE_REPLACE).click()
assert projSettings._tabBox.currentWidget() == projSettings.tabMain assert projSettings.mainStack.currentWidget() == projSettings.replacePage
# Clean Up # Clean Up
projSettings.close() projSettings.close()
@@ -83,9 +83,11 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI):
@pytest.mark.gui @pytest.mark.gui
def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): def testDlgProjSettings_SettingsPage(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
"""Test the main tab of the project settings dialog.""" """Test the settings page of the dialog."""
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) languages = [("en", "English"), ("de", "German")]
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda *a: languages)
monkeypatch.setattr(CONFIG, "listLanguages", lambda *a: languages)
# Create new project # Create new project
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
@@ -94,72 +96,57 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockR
# Set some values # Set some values
project = SHARED.project project = SHARED.project
project.data.setLanguage("en")
project.data.setSpellLang("en") project.data.setSpellLang("en")
project.data.setAuthor("Jane Smith") project.data.setAuthor("Jane Smith")
project.data.setAutoReplace({"A": "B", "C": "D"}) project.data.setAutoReplace({"A": "B", "C": "D"})
# Create Dialog # Create Dialog
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_MAIN) projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_SETTINGS)
projSettings.show() projSettings.show()
qtbot.addWidget(projSettings) qtbot.addWidget(projSettings)
# Settings Tab # Settings Tab
# ============ settings = projSettings.settingsPage
tabMain = projSettings.tabMain assert settings.projName.text() == "New Project"
assert settings.projAuthor.text() == "Jane Smith"
assert settings.projLang.currentData() == "en"
assert settings.spellLang.currentData() == "en"
assert settings.doBackup.isChecked() is False
assert tabMain.editName.text() == "New Project" settings.projName.setText("Project Name")
assert tabMain.editTitle.text() == "New Novel" settings.projAuthor.setText("Jane Doe")
assert tabMain.editAuthor.text() == "Jane Smith" settings.projLang.setCurrentIndex(settings.projLang.findData("de"))
assert tabMain.spellLang.currentData() == "en" settings.spellLang.setCurrentIndex(settings.spellLang.findData("de"))
assert tabMain.doBackup.isChecked() is False settings.doBackup.setChecked(True)
tabMain.editName.setText("")
for c in "Project Name":
qtbot.keyClick(tabMain.editName, c, delay=KEY_DELAY)
tabMain.editTitle.setText("")
for c in "Project Title":
qtbot.keyClick(tabMain.editTitle, c, delay=KEY_DELAY)
tabMain.editAuthor.clear()
for c in "Jane Doe":
qtbot.keyClick(tabMain.editAuthor, c, delay=KEY_DELAY)
assert tabMain.editName.text() == "Project Name"
assert tabMain.editTitle.text() == "Project Title"
assert tabMain.editAuthor.text() == "Jane Doe"
projSettings._doSave() projSettings._doSave()
assert project.data.name == "Project Name" assert project.data.name == "Project Name"
assert project.data.title == "Project Title"
assert project.data.author == "Jane Doe" assert project.data.author == "Jane Doe"
assert project.data.language == "de"
assert project.data.spellLang == "de"
assert project.data.doBackup is False
nwGUI._processProjectSettingsChanges() nwGUI._processProjectSettingsChanges(False)
assert nwGUI.windowTitle() == "novelWriter - Project Name" assert nwGUI.windowTitle() == "novelWriter - Project Name"
# qtbot.stop() # qtbot.stop()
# END Test testDlgProjSettings_Main # END Test testDlgProjSettings_SettingsPage
@pytest.mark.gui @pytest.mark.gui
def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the status and importance tabs of the project settings """Test the status and importance pages of the dialog."""
dialog.
"""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")])
# Create new project
mockRnd.reset()
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
CONFIG.setBackupPath(fncPath)
# Set some values # Set some values
theProject = SHARED.project project = SHARED.project
theProject.tree[C.hTitlePage].setStatus(C.sFinished) # type: ignore project.tree[C.hTitlePage].setStatus(C.sFinished) # type: ignore
theProject.tree[C.hChapterDoc].setStatus(C.sDraft) # type: ignore project.tree[C.hChapterDoc].setStatus(C.sDraft) # type: ignore
theProject.tree[C.hSceneDoc].setStatus(C.sDraft) # type: ignore project.tree[C.hSceneDoc].setStatus(C.sDraft) # type: ignore
nwGUI.projView.projTree.setSelectedHandle(C.hPlotRoot) nwGUI.projView.projTree.setSelectedHandle(C.hPlotRoot)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
@@ -172,51 +159,54 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
hCharNote = "0000000000011" hCharNote = "0000000000011"
hWorldNote = "0000000000012" hWorldNote = "0000000000012"
theProject.tree[hPlotNote].setImport(C.iMajor) # type: ignore project.tree[hPlotNote].setImport(C.iMajor) # type: ignore
theProject.tree[hCharNote].setImport(C.iMajor) # type: ignore project.tree[hCharNote].setImport(C.iMajor) # type: ignore
theProject.tree[hWorldNote].setImport(C.iMain) # type: ignore project.tree[hWorldNote].setImport(C.iMain) # type: ignore
nwGUI.rebuildTrees()
project.countStatus()
assert [e["count"] for _, e in project.data.itemStatus.items()] == [2, 0, 2, 1]
assert [e["count"] for _, e in project.data.itemImport.items()] == [3, 0, 2, 1]
# Create Dialog # Create Dialog
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_STATUS) projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_STATUS)
projSettings.show() projSettings.show()
qtbot.addWidget(projSettings) qtbot.addWidget(projSettings)
# Status Tab # Status Tab
# ========== # ==========
tabStatus = projSettings.tabStatus status = projSettings.statusPage
assert tabStatus.colChanged is False assert status.wasChanged is False
assert tabStatus.getNewList() == ([], []) assert status.getNewList() == ([], [])
assert tabStatus.listBox.topLevelItemCount() == 4 assert status.listBox.topLevelItemCount() == 4
# Can't delete the first item (it's in use) # Can't delete the first item (it's in use)
tabStatus.listBox.clearSelection() status.listBox.clearSelection()
tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(0)) status.listBox.setCurrentItem(status.listBox.topLevelItem(0))
qtbot.mouseClick(tabStatus.delButton, Qt.LeftButton) qtbot.mouseClick(status.delButton, Qt.LeftButton)
assert tabStatus.listBox.topLevelItemCount() == 4 assert status.listBox.topLevelItemCount() == 4
# Can delete the second item # Can delete the second item
tabStatus.listBox.clearSelection() status.listBox.clearSelection()
tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(1)) status.listBox.setCurrentItem(status.listBox.topLevelItem(1))
qtbot.mouseClick(tabStatus.delButton, Qt.LeftButton) qtbot.mouseClick(status.delButton, Qt.LeftButton)
assert tabStatus.listBox.topLevelItemCount() == 3 assert status.listBox.topLevelItemCount() == 3
# Add a new item # Add a new item
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40)) mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40))
qtbot.mouseClick(tabStatus.addButton, Qt.LeftButton) status.addButton.click()
tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(3)) status.listBox.setCurrentItem(status.listBox.topLevelItem(3))
for _ in range(8): status.editName.setText("Final")
qtbot.keyClick(tabStatus.editName, Qt.Key_Backspace, delay=KEY_DELAY) status.colButton.click()
for c in "Final": status.saveButton.click()
qtbot.keyClick(tabStatus.editName, c, delay=KEY_DELAY) assert status.listBox.topLevelItemCount() == 4
qtbot.mouseClick(tabStatus.colButton, Qt.LeftButton)
qtbot.mouseClick(tabStatus.saveButton, Qt.LeftButton)
assert tabStatus.listBox.topLevelItemCount() == 4
assert tabStatus.colChanged is True assert status.wasChanged is True
assert tabStatus.getNewList() == ( assert status.getNewList() == (
[ [
{ {
"key": C.sNew, "key": C.sNew,
@@ -241,62 +231,62 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
) )
# Move items, none selected -> no change # Move items, none selected -> no change
tabStatus.listBox.clearSelection() status.listBox.clearSelection()
tabStatus._moveItem(1) status._moveItem(1)
assert [x["key"] for x in tabStatus.getNewList()[0]] == [ assert [x["key"] for x in status.getNewList()[0]] == [
C.sNew, C.sDraft, C.sFinished, None C.sNew, C.sDraft, C.sFinished, None
] ]
# Move items, first selected, move up -> no change # Move items, first selected, move up -> no change
tabStatus.listBox.clearSelection() status.listBox.clearSelection()
tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(0)) status.listBox.setCurrentItem(status.listBox.topLevelItem(0))
tabStatus._moveItem(-1) status._moveItem(-1)
assert [x["key"] for x in tabStatus.getNewList()[0]] == [ assert [x["key"] for x in status.getNewList()[0]] == [
C.sNew, C.sDraft, C.sFinished, None C.sNew, C.sDraft, C.sFinished, None
] ]
# Move items, last selected, move up -> allowed # Move items, last selected, move up -> allowed
tabStatus.listBox.clearSelection() status.listBox.clearSelection()
tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(3)) status.listBox.setCurrentItem(status.listBox.topLevelItem(3))
tabStatus._moveItem(-1) status._moveItem(-1)
assert [x["key"] for x in tabStatus.getNewList()[0]] == [ assert [x["key"] for x in status.getNewList()[0]] == [
C.sNew, C.sDraft, None, C.sFinished C.sNew, C.sDraft, None, C.sFinished
] ]
# Move items, same selected, move down -> allowed # Move items, same selected, move down -> allowed
tabStatus._moveItem(1) status._moveItem(1)
assert [x["key"] for x in tabStatus.getNewList()[0]] == [ assert [x["key"] for x in status.getNewList()[0]] == [
C.sNew, C.sDraft, C.sFinished, None C.sNew, C.sDraft, C.sFinished, None
] ]
# Importance Tab # Importance Tab
# ============== # ==============
tabImport = projSettings.tabImport importance = projSettings.importPage
projSettings._focusTab(GuiProjectSettings.TAB_IMPORT) projSettings._sidebarClicked(GuiProjectSettings.PAGE_IMPORT)
# Delete unused entry # Delete unused entry
tabImport.listBox.clearSelection() importance.listBox.clearSelection()
tabImport.listBox.setCurrentItem(tabImport.listBox.topLevelItem(1)) importance.listBox.setCurrentItem(importance.listBox.topLevelItem(1))
qtbot.mouseClick(tabImport.delButton, Qt.LeftButton) qtbot.mouseClick(importance.delButton, Qt.LeftButton)
assert tabImport.listBox.topLevelItemCount() == 3 assert importance.listBox.topLevelItemCount() == 3
# Add a new entry # Add a new entry
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40)) mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40))
qtbot.mouseClick(tabImport.addButton, Qt.LeftButton) qtbot.mouseClick(importance.addButton, Qt.LeftButton)
tabImport.listBox.clearSelection() importance.listBox.clearSelection()
tabImport.listBox.setCurrentItem(tabImport.listBox.topLevelItem(3)) importance.listBox.setCurrentItem(importance.listBox.topLevelItem(3))
for _ in range(8): for _ in range(8):
qtbot.keyClick(tabImport.editName, Qt.Key_Backspace, delay=KEY_DELAY) qtbot.keyClick(importance.editName, Qt.Key_Backspace, delay=KEY_DELAY)
for c in "Final": for c in "Final":
qtbot.keyClick(tabImport.editName, c, delay=KEY_DELAY) qtbot.keyClick(importance.editName, c, delay=KEY_DELAY)
qtbot.mouseClick(tabImport.colButton, Qt.LeftButton) qtbot.mouseClick(importance.colButton, Qt.LeftButton)
qtbot.mouseClick(tabImport.saveButton, Qt.LeftButton) qtbot.mouseClick(importance.saveButton, Qt.LeftButton)
assert tabImport.listBox.topLevelItemCount() == 4 assert importance.listBox.topLevelItemCount() == 4
assert tabImport.colChanged is True assert importance.wasChanged is True
assert tabImport.getNewList() == ( assert importance.getNewList() == (
[ [
{ {
"key": C.iNew, "key": C.iNew,
@@ -323,13 +313,13 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
# Check Project # Check Project
projSettings._doSave() projSettings._doSave()
statusItems = dict(theProject.data.itemStatus.items()) statusItems = dict(project.data.itemStatus.items())
assert statusItems[C.sNew]["name"] == "New" assert statusItems[C.sNew]["name"] == "New"
assert statusItems[C.sDraft]["name"] == "Draft" assert statusItems[C.sDraft]["name"] == "Draft"
assert statusItems[C.sFinished]["name"] == "Finished" assert statusItems[C.sFinished]["name"] == "Finished"
assert statusItems["s000013"]["name"] == "Final" assert statusItems["s000013"]["name"] == "Final"
importItems = dict(theProject.data.itemImport.items()) importItems = dict(project.data.itemImport.items())
assert importItems[C.iNew]["name"] == "New" assert importItems[C.iNew]["name"] == "New"
assert importItems[C.iMajor]["name"] == "Major" assert importItems[C.iMajor]["name"] == "Major"
assert importItems[C.iMain]["name"] == "Main" assert importItems[C.iMain]["name"] == "Main"
@@ -341,83 +331,78 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
@pytest.mark.gui @pytest.mark.gui
def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the auto-replace tab of the project settings dialog.""" """Test the auto-replace page of the dialog."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")])
# Create new project
mockRnd.reset()
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
CONFIG.setBackupPath(fncPath)
# Set some values # Set some values
theProject = SHARED.project project = SHARED.project
theProject.data.setAutoReplace({ project.data.setAutoReplace({
"A": "B", "C": "D" "A": "B", "C": "D"
}) })
# Create Dialog # Create Dialog
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_REPLACE) projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_REPLACE)
projSettings.show() projSettings.show()
qtbot.addWidget(projSettings) qtbot.addWidget(projSettings)
# Auto-Replace Tab # Auto-Replace Tab
# ================ # ================
tabReplace = projSettings.tabReplace replace = projSettings.replacePage
assert tabReplace.listBox.topLevelItem(0).text(0) == "<A>" # type: ignore assert replace.listBox.topLevelItem(0).text(0) == "<A>" # type: ignore
assert tabReplace.listBox.topLevelItem(0).text(1) == "B" # type: ignore assert replace.listBox.topLevelItem(0).text(1) == "B" # type: ignore
assert tabReplace.listBox.topLevelItem(1).text(0) == "<C>" # type: ignore assert replace.listBox.topLevelItem(1).text(0) == "<C>" # type: ignore
assert tabReplace.listBox.topLevelItem(1).text(1) == "D" # type: ignore assert replace.listBox.topLevelItem(1).text(1) == "D" # type: ignore
assert tabReplace.listBox.topLevelItemCount() == 2 assert replace.listBox.topLevelItemCount() == 2
# Nothing to save or delete # Nothing to save or delete
tabReplace.listBox.clearSelection() replace.listBox.clearSelection()
tabReplace._saveEntry() replace._saveEntry()
tabReplace._delEntry() replace._delEntry()
assert tabReplace.listBox.topLevelItemCount() == 2 assert replace.listBox.topLevelItemCount() == 2
# Create a new entry # Create a new entry
qtbot.mouseClick(tabReplace.addButton, Qt.LeftButton) qtbot.mouseClick(replace.addButton, Qt.LeftButton)
assert tabReplace.listBox.topLevelItemCount() == 3 assert replace.listBox.topLevelItemCount() == 3
assert tabReplace.listBox.topLevelItem(2).text(0) == "<keyword3>" # type: ignore assert replace.listBox.topLevelItem(2).text(0) == "<keyword3>" # type: ignore
assert tabReplace.listBox.topLevelItem(2).text(1) == "" # type: ignore assert replace.listBox.topLevelItem(2).text(1) == "" # type: ignore
# Edit the entry # Edit the entry
tabReplace.listBox.setCurrentItem(tabReplace.listBox.topLevelItem(2)) replace.listBox.setCurrentItem(replace.listBox.topLevelItem(2))
tabReplace.editKey.setText("") replace.editKey.setText("")
for c in "Th is ": for c in "Th is ":
qtbot.keyClick(tabReplace.editKey, c, delay=KEY_DELAY) qtbot.keyClick(replace.editKey, c, delay=KEY_DELAY)
tabReplace.editValue.setText("") replace.editValue.setText("")
for c in "With This Stuff ": for c in "With This Stuff ":
qtbot.keyClick(tabReplace.editValue, c, delay=KEY_DELAY) qtbot.keyClick(replace.editValue, c, delay=KEY_DELAY)
qtbot.mouseClick(tabReplace.saveButton, Qt.LeftButton) qtbot.mouseClick(replace.saveButton, Qt.LeftButton)
assert tabReplace.listBox.topLevelItem(2).text(0) == "<This>" # type: ignore assert replace.listBox.topLevelItem(2).text(0) == "<This>" # type: ignore
assert tabReplace.listBox.topLevelItem(2).text(1) == "With This Stuff " # type: ignore assert replace.listBox.topLevelItem(2).text(1) == "With This Stuff " # type: ignore
# Create a new entry again # Create a new entry again
tabReplace.listBox.clearSelection() replace.listBox.clearSelection()
qtbot.mouseClick(tabReplace.addButton, Qt.LeftButton) qtbot.mouseClick(replace.addButton, Qt.LeftButton)
assert tabReplace.listBox.topLevelItemCount() == 4 assert replace.listBox.topLevelItemCount() == 4
# The list is sorted, so we must find it # The list is sorted, so we must find it
newIdx = -1 newIdx = -1
for i in range(tabReplace.listBox.topLevelItemCount()): for i in range(replace.listBox.topLevelItemCount()):
if tabReplace.listBox.topLevelItem(i).text(0) == "<keyword4>": # type: ignore if replace.listBox.topLevelItem(i).text(0) == "<keyword4>": # type: ignore
newIdx = i newIdx = i
break break
assert newIdx >= 0 assert newIdx >= 0
# Then delete the new item # Then delete the new item
tabReplace.listBox.setCurrentItem(tabReplace.listBox.topLevelItem(newIdx)) replace.listBox.setCurrentItem(replace.listBox.topLevelItem(newIdx))
qtbot.mouseClick(tabReplace.delButton, Qt.LeftButton) qtbot.mouseClick(replace.delButton, Qt.LeftButton)
assert tabReplace.listBox.topLevelItemCount() == 3 assert replace.listBox.topLevelItemCount() == 3
# Check Project # Check Project
projSettings._doSave() projSettings._doSave()
assert theProject.data.autoReplace == { assert project.data.autoReplace == {
"A": "B", "C": "D", "This": "With This Stuff" "A": "B", "C": "D", "This": "With This Stuff"
} }
+2 -4
View File
@@ -29,7 +29,7 @@ from tools import (
C, NWD_IGNORE, cmpFiles, buildTestProject, XML_IGNORE, getGuiItem C, NWD_IGNORE, cmpFiles, buildTestProject, XML_IGNORE, getGuiItem
) )
from PyQt5.QtGui import QColor, QPalette from PyQt5.QtGui import QPalette
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMenu, QInputDialog from PyQt5.QtWidgets import QMenu, QInputDialog
@@ -170,7 +170,7 @@ def testGuiMain_UpdateTheme(qtbot, nwGUI):
mainTheme.loadSyntax() mainTheme.loadSyntax()
nwGUI._processConfigChanges(True, True, True, True) nwGUI._processConfigChanges(True, True, True, True)
syntaxBack = QColor(*SHARED.theme.colBack) syntaxBack = SHARED.theme.colBack
assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntaxBack assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntaxBack
assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntaxBack assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntaxBack
@@ -200,7 +200,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
assert len(SHARED.project.tree._roots) == 0 assert len(SHARED.project.tree._roots) == 0
assert SHARED.project.tree.trashRoot is None assert SHARED.project.tree.trashRoot is None
assert SHARED.project.data.name == "" assert SHARED.project.data.name == ""
assert SHARED.project.data.title == ""
assert SHARED.project.data.author == "" assert SHARED.project.data.author == ""
assert SHARED.project.data.spellCheck is False assert SHARED.project.data.spellCheck is False
@@ -220,7 +219,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
assert len(SHARED.project.tree._roots) == 4 assert len(SHARED.project.tree._roots) == 4
assert SHARED.project.tree.trashRoot is None assert SHARED.project.tree.trashRoot is None
assert SHARED.project.data.name == "New Project" assert SHARED.project.data.name == "New Project"
assert SHARED.project.data.title == "New Novel"
assert SHARED.project.data.author == "Jane Doe" assert SHARED.project.data.author == "Jane Doe"
assert SHARED.project.data.spellCheck is False assert SHARED.project.data.spellCheck is False
+28 -19
View File
@@ -27,7 +27,7 @@ from pathlib import Path
from mocked import causeOSError from mocked import causeOSError
from tools import writeFile from tools import writeFile
from PyQt5.QtGui import QIcon, QPalette, QPixmap from PyQt5.QtGui import QColor, QIcon, QPalette, QPixmap
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
@@ -89,29 +89,38 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
parser = NWConfigParser() parser = NWConfigParser()
parser["Palette"] = { parser["Palette"] = {
"colour1": "100, 150, 200", "colour1": "100, 150, 200", # Valid
"colour2": "100, 150, 200, 250", "colour2": "100, 150, 200, 250", # With alpha
"colour3": "250, 250", "colour3": "100, 150, 200, 250, 300", # Too many values
"colour4": "-10, 127, 300", "colour4": "250, 250", # Missing blue
"colour5": "-10, 127, 300", # Invalid red and blue
"colour6": "bob, 127, 255", # Invalid red
} }
# Test the parser for several valid and invalid values # Test the parser for several valid and invalid values
assert mainTheme._parseColour(parser, "Palette", "colour1") == [100, 150, 200] assert mainTheme._parseColour(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255)
assert mainTheme._parseColour(parser, "Palette", "colour2") == [100, 150, 200] assert mainTheme._parseColour(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250)
assert mainTheme._parseColour(parser, "Palette", "colour3") == [0, 0, 0] assert mainTheme._parseColour(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250)
assert mainTheme._parseColour(parser, "Palette", "colour4") == [0, 127, 255] assert mainTheme._parseColour(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255)
assert mainTheme._parseColour(parser, "Palette", "colour5") == [0, 0, 0] assert mainTheme._parseColour(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0)
assert mainTheme._parseColour(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255)
# The palette should load with the parsed values # The palette should load with the parsed values
mainTheme._setPalette(parser, "Palette", "colour1", QPalette.Window) mainTheme._setPalette(parser, "Palette", "colour1", QPalette.Window)
assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 255) assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 255)
mainTheme._setPalette(parser, "Palette", "colour2", QPalette.Window) mainTheme._setPalette(parser, "Palette", "colour2", QPalette.Window)
assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 255) assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 250)
mainTheme._setPalette(parser, "Palette", "colour3", QPalette.Window) mainTheme._setPalette(parser, "Palette", "colour3", QPalette.Window)
assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 0, 0, 255) assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 250)
mainTheme._setPalette(parser, "Palette", "colour4", QPalette.Window) mainTheme._setPalette(parser, "Palette", "colour4", QPalette.Window)
assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 127, 255, 255) assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (250, 250, 0, 255)
mainTheme._setPalette(parser, "Palette", "colour5", QPalette.Window) mainTheme._setPalette(parser, "Palette", "colour5", QPalette.Window)
assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 0, 0, 0)
mainTheme._setPalette(parser, "Palette", "colour6", QPalette.Window)
assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 127, 255, 255)
# Non-existing value should return default colour
mainTheme._setPalette(parser, "Palette", "stuff", QPalette.Window)
assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 0, 0, 255) assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 0, 0, 255)
# qtbot.stop() # qtbot.stop()
@@ -240,9 +249,9 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
# Check some values # Check some values
assert mainTheme.syntaxName == "Default Light" assert mainTheme.syntaxName == "Default Light"
assert mainTheme.colBack == [255, 255, 255] assert mainTheme.colBack == QColor(255, 255, 255)
assert mainTheme.colText == [0, 0, 0] assert mainTheme.colText == QColor(0, 0, 0)
assert mainTheme.colLink == [0, 0, 200] assert mainTheme.colLink == QColor(0, 0, 200)
# Load Default Dark Theme # Load Default Dark Theme
# ======================= # =======================
@@ -253,9 +262,9 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
# Check some values # Check some values
assert mainTheme.syntaxName == "Default Dark" assert mainTheme.syntaxName == "Default Dark"
assert mainTheme.colBack == [54, 54, 54] assert mainTheme.colBack == QColor(54, 54, 54)
assert mainTheme.colText == [199, 207, 208] assert mainTheme.colText == QColor(199, 207, 208)
assert mainTheme.colLink == [184, 200, 0] assert mainTheme.colLink == QColor(184, 200, 0)
# qtbot.stop() # qtbot.stop()
+136
View File
@@ -0,0 +1,136 @@
"""
novelWriter Novel Details Tool Tester
=======================================
This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import pytest
from tools import getGuiItem
from PyQt5.QtWidgets import QAction
from novelwriter import SHARED
from novelwriter.enum import nwItemClass
from novelwriter.tools.noveldetails import GuiNovelDetails
@pytest.mark.gui
def testToolNovelDetails_Main(qtbot, nwGUI, prjLipsum, ipsumText):
"""Test the Novel Details main dialog."""
nwGUI.openProject(prjLipsum)
nHandle = "b3643d0f92e32"
# Add a second Novel folder
project = SHARED.project
secondText = "#! Second\n\n" + "\n\n".join(ipsumText)
sHandle = project.newRoot(nwItemClass.NOVEL, "Second")
dHandle = project.newFile("Document", sHandle)
project.storage.getDocument(dHandle).writeDocument(secondText)
project.index.reIndexHandle(dHandle)
nwGUI.projView.projTree.revealNewTreeItem(sHandle)
nwGUI.projView.projTree.revealNewTreeItem(dHandle)
# Create the dialog
nwGUI.mainMenu.aNovelDetails.activate(QAction.ActionEvent.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiNovelDetails") is not None, timeout=1000)
details = getGuiItem("GuiNovelDetails")
assert isinstance(details, GuiNovelDetails)
# Overview Page
# =============
overview = details.overviewPage
# The selector should default to the first entry
assert details.novelSelector.handle == nHandle
# Check project data
assert overview.projName.text() == "Lorem Ipsum"
assert overview.projWords.text() == f"{4376:n}"
assert overview.projNovels.text() == f"{3638:n}"
assert overview.projNotes.text() == f"{738:n}"
assert overview.projRevisions.text() != ""
assert overview.projEditTime.text() != ""
# Check novel data for "Novel"
assert overview.novelName.text() == "Novel"
assert overview.novelWords.text() == f"{3000:n}"
assert overview.novelChapters.text() == f"{3:n}"
assert overview.novelScenes.text() == f"{5:n}"
# Check novel data for "Second"
details.novelSelector.setHandle(sHandle, blockSignal=False)
assert overview.novelName.text() == "Second"
assert overview.novelWords.text() == f"{529:n}"
assert overview.novelChapters.text() == f"{0:n}"
assert overview.novelScenes.text() == f"{0:n}"
# Contents Page
# =============
details.novelSelector.setHandle(nHandle, blockSignal=False)
details.sidebar.button(details.PAGE_CONTENTS).click()
assert details.mainStack.currentIndex() == 1
contents = details.contentsPage
# Check defaults
words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]]
pages = [f"{v:n}" for v in [2, 2, 2, 2, 4, 6, 0]]
page = [f"{v:n}" for v in [1, 3, 5, 7, 9, 13, 19]]
for i in range(6):
item = contents.tocTree.topLevelItem(i)
assert item is not None
assert item.text(contents.C_WORDS) == words[i]
assert item.text(contents.C_PAGES) == pages[i]
assert item.text(contents.C_PAGE) == page[i]
# Change Settings
contents.poValue.setValue(7)
contents.wpValue.setValue(50)
words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]]
pages = [f"{v:n}" for v in [2, 4, 2, 2, 22, 34, 0]]
page = ["i", "iii"] + [f"{v:n}" for v in [1, 3, 5, 27, 61]]
for i in range(6):
item = contents.tocTree.topLevelItem(i)
assert item is not None
assert item.text(contents.C_WORDS) == words[i]
assert item.text(contents.C_PAGES) == pages[i]
assert item.text(contents.C_PAGE) == page[i]
# Turn off use odd pages
contents.dblValue.setChecked(False)
contents.poValue.setValue(0)
contents.wpValue.setValue(100)
words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]]
pages = [f"{v:n}" for v in [1, 2, 1, 1, 11, 17, 0]]
page = [f"{v:n}" for v in [1, 2, 4, 5, 6, 17, 34]]
for i in range(6):
item = contents.tocTree.topLevelItem(i)
assert item is not None
assert item.text(contents.C_WORDS) == words[i]
assert item.text(contents.C_PAGES) == pages[i]
assert item.text(contents.C_PAGE) == page[i]
# Revert to Overview page
details.sidebar.button(details.PAGE_OVERVIEW).click()
assert details.mainStack.currentIndex() == 0
# qtbot.stop()
details.close()
# END Test testToolNovelDetails_Main
+20 -18
View File
@@ -156,52 +156,54 @@ def cleanProject(path: str | Path):
return return
def buildTestProject(obj, projPath): def buildTestProject(obj: object, projPath: Path) -> None:
"""Build a standard test project in projPath using the project """Build a standard test project in projPath using the project
object as the parent. object as the parent.
""" """
from novelwriter.enum import nwItemClass from novelwriter.enum import nwItemClass
from novelwriter.guimain import GuiMain
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
if isinstance(obj, NWProject): if isinstance(obj, NWProject):
nwGUI = None nwGUI = None
project = obj project = obj
else: elif isinstance(obj, GuiMain):
from novelwriter import SHARED from novelwriter import SHARED
nwGUI = obj nwGUI = obj
project = SHARED.project project = SHARED.project
else:
return
project.storage.createNewProject(projPath) project.storage.createNewProject(projPath)
project.setDefaultStatusImport() project.setDefaultStatusImport()
project.data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") project.data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
project.data.setName("New Project") project.data.setName("New Project")
project.data.setTitle("New Novel")
project.data.setAuthor("Jane Doe") project.data.setAuthor("Jane Doe")
# Creating a minimal project with a few root folders and a # Creating a minimal project with a few root folders and a
# single chapter folder with a single file. # single chapter folder with a single file.
xHandle = {} nrHandle = project.newRoot(nwItemClass.NOVEL, "Novel")
xHandle[1] = project.newRoot(nwItemClass.NOVEL, "Novel") project.newRoot(nwItemClass.PLOT, "Plot")
xHandle[2] = project.newRoot(nwItemClass.PLOT, "Plot") project.newRoot(nwItemClass.CHARACTER, "Characters")
xHandle[3] = project.newRoot(nwItemClass.CHARACTER, "Characters") project.newRoot(nwItemClass.WORLD, "World")
xHandle[4] = project.newRoot(nwItemClass.WORLD, "World")
xHandle[5] = project.newFile("Title Page", xHandle[1])
xHandle[6] = project.newFolder("New Chapter", xHandle[1])
xHandle[7] = project.newFile("New Chapter", xHandle[6])
xHandle[8] = project.newFile("New Scene", xHandle[6])
aDoc = project.storage.getDocument(xHandle[5]) tdHandle = project.newFile("Title Page", nrHandle)
cfHandle = project.newFolder("New Chapter", nrHandle) or ""
cdHandle = project.newFile("New Chapter", cfHandle)
sdHandle = project.newFile("New Scene", cfHandle)
aDoc = project.storage.getDocument(tdHandle)
aDoc.writeDocument("#! New Novel\n\n>> By Jane Doe <<\n") aDoc.writeDocument("#! New Novel\n\n>> By Jane Doe <<\n")
project.index.reIndexHandle(xHandle[5]) project.index.reIndexHandle(tdHandle)
aDoc = project.storage.getDocument(xHandle[7]) aDoc = project.storage.getDocument(cdHandle)
aDoc.writeDocument("## %s\n\n" % project.tr("New Chapter")) aDoc.writeDocument("## %s\n\n" % project.tr("New Chapter"))
project.index.reIndexHandle(xHandle[7]) project.index.reIndexHandle(cdHandle)
aDoc = project.storage.getDocument(xHandle[8]) aDoc = project.storage.getDocument(sdHandle)
aDoc.writeDocument("### %s\n\n" % project.tr("New Scene")) aDoc.writeDocument("### %s\n\n" % project.tr("New Scene"))
project.index.reIndexHandle(xHandle[8]) project.index.reIndexHandle(sdHandle)
project.session.startSession() project.session.startSession()
project.setProjectChanged(True) project.setProjectChanged(True)