Merge branch 'dev' into project_handling
This commit is contained in:
+361
-382
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -42,9 +42,9 @@ __license__ = "GPLv3"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__version__ = "2.3-alpha1"
|
||||
__hexversion__ = "0x020300a1"
|
||||
__date__ = "2023-12-17"
|
||||
__version__ = "2.3a2"
|
||||
__hexversion__ = "0x020300a2"
|
||||
__date__ = "2024-01-26"
|
||||
__status__ = "Stable"
|
||||
__domain__ = "novelwriter.io"
|
||||
|
||||
@@ -131,7 +131,7 @@ def main(sysArgs: list | None = None):
|
||||
elif inOpt == "--debug":
|
||||
CONFIG.isDebug = True
|
||||
logLevel = logging.DEBUG
|
||||
logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}"
|
||||
logFormat = "[{asctime:}] {filename:>18}:{lineno:<4d} {levelname:8} {message:}"
|
||||
elif inOpt == "--style":
|
||||
qtStyle = inArg
|
||||
elif inOpt == "--config":
|
||||
|
||||
@@ -250,6 +250,11 @@ def formatTime(t: int) -> str:
|
||||
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
|
||||
# =============================================================================================== #
|
||||
|
||||
@@ -374,7 +374,6 @@ class ProjectBuilder:
|
||||
|
||||
project.data.setUuid(None)
|
||||
project.data.setName(projName)
|
||||
project.data.setTitle(projName)
|
||||
project.data.setAuthor(projAuthor)
|
||||
project.data.setLanguage(projLang)
|
||||
project.setDefaultStatusImport()
|
||||
@@ -383,7 +382,7 @@ class ProjectBuilder:
|
||||
# Add Root Folders
|
||||
hNovelRoot = project.newRoot(nwItemClass.NOVEL)
|
||||
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"
|
||||
if project.data.author:
|
||||
|
||||
@@ -528,17 +528,20 @@ class NWIndex:
|
||||
yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem
|
||||
return
|
||||
|
||||
def getNovelWordCount(self, activeOnly: bool = True) -> int:
|
||||
"""Count the number of words in the novel project."""
|
||||
wCount = 0
|
||||
for _, _, hItem in self._itemIndex.iterNovelStructure(activeOnly=activeOnly):
|
||||
wCount += hItem.wordCount
|
||||
return wCount
|
||||
def getNovelWordCount(self, rootHandle: str | None = None, activeOnly: bool = True) -> int:
|
||||
"""Count the number of words in one or all novel roots."""
|
||||
return sum(hItem.wordCount for _, _, hItem in self._itemIndex.iterNovelStructure(
|
||||
rHandle=rootHandle, activeOnly=activeOnly
|
||||
))
|
||||
|
||||
def getNovelTitleCounts(self, activeOnly: bool = True) -> list[int]:
|
||||
"""Count the number of titles in the novel project."""
|
||||
def getNovelTitleCounts(
|
||||
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]
|
||||
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)
|
||||
hCount[iLevel] += 1
|
||||
return hCount
|
||||
|
||||
@@ -53,10 +53,6 @@ VALID_MAP: dict[str, set[str]] = {
|
||||
"GuiProjectSettings": {
|
||||
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW",
|
||||
},
|
||||
"GuiProjectDetails": {
|
||||
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
|
||||
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
|
||||
},
|
||||
"GuiWordList": {"winWidth", "winHeight"},
|
||||
"GuiNovelView": {"lastCol", "lastColSize"},
|
||||
"GuiBuildSettings": {
|
||||
@@ -71,7 +67,12 @@ VALID_MAP: dict[str, set[str]] = {
|
||||
},
|
||||
"GuiDocViewerPanel": {
|
||||
"colWidths", "hideInactive",
|
||||
}
|
||||
},
|
||||
"GuiNovelDetails": {
|
||||
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
|
||||
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
|
||||
"novelRoot",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ class NWProjectData:
|
||||
# Project Meta
|
||||
self._uuid = ""
|
||||
self._name = ""
|
||||
self._title = ""
|
||||
self._author = ""
|
||||
self._saveCount = 0
|
||||
self._autoCount = 0
|
||||
@@ -102,11 +101,6 @@ class NWProjectData:
|
||||
"""Return the project name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
"""Return the project title."""
|
||||
return self._title
|
||||
|
||||
@property
|
||||
def author(self) -> str:
|
||||
"""Return the project author."""
|
||||
@@ -228,16 +222,9 @@ class NWProjectData:
|
||||
self._project.setProjectChanged(True)
|
||||
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:
|
||||
"""Set the author value."""
|
||||
if value != self._title:
|
||||
if value != self._author:
|
||||
self._author = simplified(str(value or ""))
|
||||
self._project.setProjectChanged(True)
|
||||
return
|
||||
|
||||
@@ -46,7 +46,7 @@ if TYPE_CHECKING: # pragma: no cover
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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
|
||||
|
||||
NUM_VERSION = {
|
||||
@@ -105,7 +105,8 @@ class ProjectXMLReader:
|
||||
the project or the content into their respective section nodes
|
||||
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:
|
||||
@@ -232,8 +233,6 @@ class ProjectXMLReader:
|
||||
for xItem in xSection:
|
||||
if xItem.tag == "name":
|
||||
data.setName(xItem.text)
|
||||
elif xItem.tag == "title":
|
||||
data.setTitle(xItem.text)
|
||||
elif xItem.tag == "author":
|
||||
data.setAuthor(xItem.text)
|
||||
else:
|
||||
@@ -505,7 +504,6 @@ class ProjectXMLWriter:
|
||||
|
||||
xProject = ET.SubElement(xRoot, "project", attrib=projAttr)
|
||||
self._packSingleValue(xProject, "name", data.name)
|
||||
self._packSingleValue(xProject, "title", data.title)
|
||||
self._packSingleValue(xProject, "author", data.author)
|
||||
|
||||
# Save Project Settings
|
||||
|
||||
@@ -338,7 +338,6 @@ class ToHtml(Tokenizer):
|
||||
data = {
|
||||
"meta": {
|
||||
"projectName": self._project.data.name,
|
||||
"novelTitle": self._project.data.title,
|
||||
"novelAuthor": self._project.data.author,
|
||||
"buildTime": int(timeStamp),
|
||||
"buildTimeStr": formatTimeStamp(timeStamp),
|
||||
|
||||
@@ -759,7 +759,6 @@ class Tokenizer(ABC):
|
||||
data = {
|
||||
"meta": {
|
||||
"projectName": self._project.data.name,
|
||||
"novelTitle": self._project.data.title,
|
||||
"novelAuthor": self._project.data.author,
|
||||
"buildTime": int(timeStamp),
|
||||
"buildTimeStr": formatTimeStamp(timeStamp),
|
||||
|
||||
@@ -283,9 +283,7 @@ class ToOdt(Tokenizer):
|
||||
# ===============
|
||||
|
||||
if self._headerText == "":
|
||||
theTitle = self._project.data.title or self._project.data.name
|
||||
theAuth = self._project.data.author
|
||||
self._headerText = f"{theTitle} / {theAuth} /"
|
||||
self._headerText = f"{self._project.data.name} / {self._project.data.author} /"
|
||||
|
||||
# Create Roots
|
||||
# ============
|
||||
@@ -373,7 +371,7 @@ class ToOdt(Tokenizer):
|
||||
|
||||
# Dublin Core Meta Data
|
||||
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.text = timeStamp
|
||||
|
||||
@@ -220,23 +220,25 @@ class GuiAbout(QDialog):
|
||||
|
||||
def _setStyleSheet(self) -> None:
|
||||
"""Set stylesheet for all browser tabs."""
|
||||
colHead = SHARED.theme.colHead
|
||||
colKey = SHARED.theme.colKey
|
||||
styleSheet = (
|
||||
"h1, h2, h3, h4 {{"
|
||||
" color: rgb({hColR},{hColG},{hColB});"
|
||||
" color: rgb({hColR}, {hColG}, {hColB});"
|
||||
"}}\n"
|
||||
"a {{"
|
||||
" color: rgb({hColR},{hColG},{hColB});"
|
||||
" color: rgb({hColR}, {hColG}, {hColB});"
|
||||
"}}\n"
|
||||
".alt {{"
|
||||
" color: rgb({kColR},{kColG},{kColB});"
|
||||
" color: rgb({kColR}, {kColG}, {kColB});"
|
||||
"}}\n"
|
||||
).format(
|
||||
hColR=SHARED.theme.colHead[0],
|
||||
hColG=SHARED.theme.colHead[1],
|
||||
hColB=SHARED.theme.colHead[2],
|
||||
kColR=SHARED.theme.colKey[0],
|
||||
kColG=SHARED.theme.colKey[1],
|
||||
kColB=SHARED.theme.colKey[2],
|
||||
hColR=colHead.red(),
|
||||
hColG=colHead.green(),
|
||||
hColB=colHead.blue(),
|
||||
kColR=colKey.red(),
|
||||
kColG=colKey.green(),
|
||||
kColB=colKey.blue(),
|
||||
)
|
||||
self.pageAbout.document().setDefaultStyleSheet(styleSheet)
|
||||
self.pageNotes.document().setDefaultStyleSheet(styleSheet)
|
||||
|
||||
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.extensions.switch import NSwitch
|
||||
from novelwriter.extensions.configlayout import NHelpLabel
|
||||
from novelwriter.extensions.configlayout import NColourLabel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,9 +54,10 @@ class GuiDocMerge(QDialog):
|
||||
self._data = {}
|
||||
|
||||
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
|
||||
self.helpLabel = NHelpLabel(self.tr(
|
||||
"Drag and drop items to change the order, or uncheck to exclude."
|
||||
), SHARED.theme.helpText)
|
||||
self.helpLabel = NColourLabel(
|
||||
self.tr("Drag and drop items to change the order, or uncheck to exclude."),
|
||||
SHARED.theme.helpText, parent=self, wrap=True
|
||||
)
|
||||
|
||||
iPx = SHARED.theme.baseIconSize
|
||||
hSp = CONFIG.pxInt(12)
|
||||
|
||||
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.extensions.switch import NSwitch
|
||||
from novelwriter.extensions.configlayout import NHelpLabel
|
||||
from novelwriter.extensions.configlayout import NColourLabel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,9 +58,9 @@ class GuiDocSplit(QDialog):
|
||||
self.setWindowTitle(self.tr("Split Document"))
|
||||
|
||||
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."),
|
||||
SHARED.theme.helpText
|
||||
SHARED.theme.helpText, parent=self, wrap=True
|
||||
)
|
||||
|
||||
# Values
|
||||
|
||||
@@ -26,11 +26,11 @@ from __future__ import annotations
|
||||
|
||||
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.QtWidgets import (
|
||||
QAbstractButton, QComboBox, QCompleter, QDialog, QDialogButtonBox,
|
||||
QDoubleSpinBox, QFileDialog, QFontDialog, QHBoxLayout, QLabel, QLineEdit,
|
||||
QDoubleSpinBox, QFileDialog, QFontDialog, QHBoxLayout, QLineEdit,
|
||||
QPushButton, QSpinBox, QToolButton, QVBoxLayout, QWidget, qApp
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.constants import nwConst, nwUnicode
|
||||
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -58,21 +58,10 @@ class GuiPreferences(QDialog):
|
||||
self.resize(*CONFIG.preferencesWinSize)
|
||||
|
||||
# Title
|
||||
font = self.font()
|
||||
font.setPointSizeF(1.25*SHARED.theme.fontPointSize)
|
||||
|
||||
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)
|
||||
self.titleLabel = NColourLabel(
|
||||
self.tr("Preferences"), SHARED.theme.helpText,
|
||||
parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
|
||||
)
|
||||
|
||||
# Search Box
|
||||
self.searchText = QLineEdit(self)
|
||||
@@ -83,10 +72,10 @@ class GuiPreferences(QDialog):
|
||||
)
|
||||
self.searchAction.triggered.connect(self._gotoSearch)
|
||||
|
||||
self.searchBox = QHBoxLayout()
|
||||
self.searchBox.addWidget(self.titleLabel)
|
||||
self.searchBox.addStretch(1)
|
||||
self.searchBox.addWidget(self.searchText, 1)
|
||||
# SideBar
|
||||
self.sidebar = NPagedSideBar(self)
|
||||
self.sidebar.setLabelColor(SHARED.theme.helpText)
|
||||
self.sidebar.buttonClicked.connect(self._sidebarClicked)
|
||||
|
||||
# Form
|
||||
self.mainForm = NScrollableForm(self)
|
||||
@@ -101,6 +90,11 @@ class GuiPreferences(QDialog):
|
||||
self.buttonBox.clicked.connect(self._dialogButtonClicked)
|
||||
|
||||
# Assemble
|
||||
self.searchBox = QHBoxLayout()
|
||||
self.searchBox.addWidget(self.titleLabel)
|
||||
self.searchBox.addStretch(1)
|
||||
self.searchBox.addWidget(self.searchText, 1)
|
||||
|
||||
self.mainBox = QHBoxLayout()
|
||||
self.mainBox.addWidget(self.sidebar)
|
||||
self.mainBox.addWidget(self.mainForm)
|
||||
|
||||
@@ -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 2018–2024, 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:
|
||||
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
|
||||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||
@@ -25,75 +26,101 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtGui import QCloseEvent, QIcon, QPixmap, QColor
|
||||
from PyQt5.QtGui import QCloseEvent, QColor, QIcon, QPixmap
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtWidgets import (
|
||||
QColorDialog, QComboBox, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, qApp
|
||||
QColorDialog, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QLineEdit,
|
||||
QPushButton, QStackedWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
||||
QWidget, qApp
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import simplified
|
||||
from novelwriter.extensions.switch import NSwitch
|
||||
from novelwriter.extensions.pageddialog import NPagedDialog
|
||||
from novelwriter.extensions.configlayout import NConfigLayout
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.guimain import GuiMain
|
||||
from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollableForm
|
||||
from novelwriter.extensions.pagedsidebar import NPagedSideBar
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiProjectSettings(NPagedDialog):
|
||||
class GuiProjectSettings(QDialog):
|
||||
|
||||
TAB_MAIN = 0
|
||||
TAB_STATUS = 1
|
||||
TAB_IMPORT = 2
|
||||
TAB_REPLACE = 3
|
||||
PAGE_SETTINGS = 0
|
||||
PAGE_STATUS = 1
|
||||
PAGE_IMPORT = 2
|
||||
PAGE_REPLACE = 3
|
||||
|
||||
newProjectSettingsReady = pyqtSignal()
|
||||
newProjectSettingsReady = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, mainGui: GuiMain, focusTab: int = TAB_MAIN) -> None:
|
||||
super().__init__(parent=mainGui)
|
||||
def __init__(self, parent: QWidget, gotoPage: int = PAGE_SETTINGS) -> None:
|
||||
super().__init__(parent=parent)
|
||||
|
||||
logger.debug("Create: GuiProjectSettings")
|
||||
self.setObjectName("GuiProjectSettings")
|
||||
|
||||
self.mainGui = mainGui
|
||||
SHARED.project.countStatus()
|
||||
self.setWindowTitle(self.tr("Project Settings"))
|
||||
|
||||
wW = CONFIG.pxInt(570)
|
||||
wH = CONFIG.pxInt(375)
|
||||
pOptions = SHARED.project.options
|
||||
|
||||
self.setMinimumWidth(wW)
|
||||
self.setMinimumHeight(wH)
|
||||
options = SHARED.project.options
|
||||
self.setMinimumSize(CONFIG.pxInt(500), CONFIG.pxInt(400))
|
||||
self.resize(
|
||||
CONFIG.pxInt(pOptions.getInt("GuiProjectSettings", "winWidth", wW)),
|
||||
CONFIG.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH))
|
||||
CONFIG.pxInt(options.getInt("GuiProjectSettings", "winWidth", CONFIG.pxInt(650))),
|
||||
CONFIG.pxInt(options.getInt("GuiProjectSettings", "winHeight", CONFIG.pxInt(500)))
|
||||
)
|
||||
|
||||
self.tabMain = GuiProjectEditMain(self)
|
||||
self.tabStatus = GuiProjectEditStatus(self, True)
|
||||
self.tabImport = GuiProjectEditStatus(self, False)
|
||||
self.tabReplace = GuiProjectEditReplace(self)
|
||||
# Title
|
||||
self.titleLabel = NColourLabel(
|
||||
self.tr("Project Settings"), SHARED.theme.helpText,
|
||||
parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
|
||||
)
|
||||
|
||||
self.addTab(self.tabMain, self.tr("Settings"))
|
||||
self.addTab(self.tabStatus, self.tr("Status"))
|
||||
self.addTab(self.tabImport, self.tr("Importance"))
|
||||
self.addTab(self.tabReplace, self.tr("Auto-Replace"))
|
||||
# SideBar
|
||||
self.sidebar = NPagedSideBar(self)
|
||||
self.sidebar.setLabelColor(SHARED.theme.helpText)
|
||||
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.rejected.connect(self.close)
|
||||
self.rejected.connect(self.close)
|
||||
self.addControls(self.buttonBox)
|
||||
|
||||
# Focus Tab
|
||||
self._focusTab(focusTab)
|
||||
# Content
|
||||
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")
|
||||
|
||||
@@ -103,9 +130,13 @@ class GuiProjectSettings(NPagedDialog):
|
||||
logger.debug("Delete: GuiProjectSettings")
|
||||
return
|
||||
|
||||
##
|
||||
# Events
|
||||
##
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None:
|
||||
"""Capture the close event and perform cleanup."""
|
||||
self._saveGuiSettings()
|
||||
"""Capture the user closing the window and save settings."""
|
||||
self._saveSettings()
|
||||
event.accept()
|
||||
self.deleteLater()
|
||||
return
|
||||
@@ -114,40 +145,52 @@ class GuiProjectSettings(NPagedDialog):
|
||||
# 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()
|
||||
def _doSave(self) -> None:
|
||||
"""Save settings and close dialog."""
|
||||
project = SHARED.project
|
||||
projName = self.tabMain.editName.text()
|
||||
bookTitle = self.tabMain.editTitle.text()
|
||||
bookAuthor = self.tabMain.editAuthor.text()
|
||||
projLang = self.tabMain.projLang.currentData()
|
||||
spellLang = self.tabMain.spellLang.currentData()
|
||||
doBackup = not self.tabMain.doBackup.isChecked()
|
||||
projName = self.settingsPage.projName.text()
|
||||
projAuthor = self.settingsPage.projAuthor.text()
|
||||
projLang = self.settingsPage.projLang.currentData()
|
||||
spellLang = self.settingsPage.spellLang.currentData()
|
||||
doBackup = not self.settingsPage.doBackup.isChecked()
|
||||
|
||||
project.data.setName(projName)
|
||||
project.data.setTitle(bookTitle)
|
||||
project.data.setAuthor(bookAuthor)
|
||||
project.data.setAuthor(projAuthor)
|
||||
project.data.setDoBackup(doBackup)
|
||||
project.data.setSpellLang(spellLang)
|
||||
project.setProjectLang(projLang)
|
||||
|
||||
if self.tabStatus.colChanged:
|
||||
newList, delList = self.tabStatus.getNewList()
|
||||
rebuildTrees = False
|
||||
|
||||
if self.statusPage.wasChanged:
|
||||
newList, delList = self.statusPage.getNewList()
|
||||
project.setStatusColours(newList, delList)
|
||||
rebuildTrees = True
|
||||
|
||||
if self.tabImport.colChanged:
|
||||
newList, delList = self.tabImport.getNewList()
|
||||
if self.importPage.wasChanged:
|
||||
newList, delList = self.importPage.getNewList()
|
||||
project.setImportColours(newList, delList)
|
||||
rebuildTrees = True
|
||||
|
||||
if self.tabStatus.colChanged or self.tabImport.colChanged:
|
||||
self.mainGui.rebuildTrees()
|
||||
|
||||
if self.tabReplace.arChanged:
|
||||
newList = self.tabReplace.getNewList()
|
||||
if self.replacePage.wasChanged:
|
||||
newList = self.replacePage.getNewList()
|
||||
project.data.setAutoReplace(newList)
|
||||
|
||||
self.newProjectSettingsReady.emit()
|
||||
self.newProjectSettingsReady.emit(rebuildTrees)
|
||||
qApp.processEvents()
|
||||
self.close()
|
||||
|
||||
@@ -157,134 +200,103 @@ class GuiProjectSettings(NPagedDialog):
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _focusTab(self, tab: int) -> 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:
|
||||
def _saveSettings(self) -> None:
|
||||
"""Save GUI settings."""
|
||||
winWidth = CONFIG.rpxInt(self.width())
|
||||
winHeight = CONFIG.rpxInt(self.height())
|
||||
replaceColW = CONFIG.rpxInt(self.tabReplace.listBox.columnWidth(0))
|
||||
statusColW = CONFIG.rpxInt(self.tabStatus.listBox.columnWidth(0))
|
||||
importColW = CONFIG.rpxInt(self.tabImport.listBox.columnWidth(0))
|
||||
statusColW = CONFIG.rpxInt(self.statusPage.columnWidth())
|
||||
importColW = CONFIG.rpxInt(self.importPage.columnWidth())
|
||||
replaceColW = CONFIG.rpxInt(self.replacePage.columnWidth())
|
||||
|
||||
logger.debug("Saving State: GuiProjectSettings")
|
||||
pOptions = SHARED.project.options
|
||||
pOptions.setValue("GuiProjectSettings", "winWidth", winWidth)
|
||||
pOptions.setValue("GuiProjectSettings", "winHeight", winHeight)
|
||||
pOptions.setValue("GuiProjectSettings", "replaceColW", replaceColW)
|
||||
pOptions.setValue("GuiProjectSettings", "statusColW", statusColW)
|
||||
pOptions.setValue("GuiProjectSettings", "importColW", importColW)
|
||||
options = SHARED.project.options
|
||||
options.setValue("GuiProjectSettings", "winWidth", winWidth)
|
||||
options.setValue("GuiProjectSettings", "winHeight", winHeight)
|
||||
options.setValue("GuiProjectSettings", "statusColW", statusColW)
|
||||
options.setValue("GuiProjectSettings", "importColW", importColW)
|
||||
options.setValue("GuiProjectSettings", "replaceColW", replaceColW)
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiProjectSettings
|
||||
|
||||
|
||||
class GuiProjectEditMain(QWidget):
|
||||
class _SettingsPage(NScrollableForm):
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent=parent)
|
||||
|
||||
# The Form
|
||||
self.mainForm = NConfigLayout()
|
||||
self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
|
||||
self.setLayout(self.mainForm)
|
||||
xW = CONFIG.pxInt(200)
|
||||
data = SHARED.project.data
|
||||
self.setHelpTextStyle(SHARED.theme.helpText)
|
||||
self.setRowIndent(0)
|
||||
|
||||
self.mainForm.addGroupLabel(self.tr("Project Settings"))
|
||||
|
||||
xW = CONFIG.pxInt(250)
|
||||
pData = SHARED.project.data
|
||||
|
||||
self.editName = QLineEdit()
|
||||
self.editName.setMaxLength(200)
|
||||
self.editName.setMaximumWidth(xW)
|
||||
self.editName.setText(pData.name)
|
||||
self.mainForm.addRow(
|
||||
self.tr("Project name"),
|
||||
self.editName,
|
||||
self.tr("Should be set only once.")
|
||||
# Project Name
|
||||
self.projName = QLineEdit(self)
|
||||
self.projName.setMaxLength(200)
|
||||
self.projName.setMinimumWidth(xW)
|
||||
self.projName.setText(data.name)
|
||||
self.addRow(
|
||||
self.tr("Project name"), self.projName,
|
||||
self.tr("Changing this will affect the backup path."),
|
||||
stretch=(3, 2)
|
||||
)
|
||||
|
||||
self.editTitle = QLineEdit()
|
||||
self.editTitle.setMaxLength(200)
|
||||
self.editTitle.setMaximumWidth(xW)
|
||||
self.editTitle.setText(pData.title)
|
||||
self.mainForm.addRow(
|
||||
self.tr("Novel title"),
|
||||
self.editTitle,
|
||||
self.tr("Change whenever you want!")
|
||||
)
|
||||
|
||||
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 Author
|
||||
self.projAuthor = QLineEdit(self)
|
||||
self.projAuthor.setMaxLength(200)
|
||||
self.projAuthor.setMinimumWidth(xW)
|
||||
self.projAuthor.setText(data.author)
|
||||
self.addRow(
|
||||
self.tr("Author(s)"), self.projAuthor,
|
||||
self.tr("Only used when building the manuscript."),
|
||||
stretch=(3, 2)
|
||||
)
|
||||
|
||||
# Project Language
|
||||
self.projLang = QComboBox(self)
|
||||
self.projLang.setMaximumWidth(xW)
|
||||
self.projLang.setMinimumWidth(xW)
|
||||
for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ):
|
||||
self.projLang.addItem(language, tag)
|
||||
self.mainForm.addRow(
|
||||
self.tr("Project language"),
|
||||
self.projLang,
|
||||
self.tr("Used when building the manuscript.")
|
||||
self.addRow(
|
||||
self.tr("Project language"), self.projLang,
|
||||
self.tr("Only used when building the manuscript."),
|
||||
stretch=(3, 2)
|
||||
)
|
||||
if (idx := self.projLang.findData(data.language)) != -1:
|
||||
self.projLang.setCurrentIndex(idx)
|
||||
|
||||
langIdx = 0
|
||||
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)
|
||||
|
||||
# Spell Check Language
|
||||
self.spellLang = QComboBox(self)
|
||||
self.spellLang.setMaximumWidth(xW)
|
||||
self.spellLang.setMinimumWidth(xW)
|
||||
self.spellLang.addItem(self.tr("Default"), "None")
|
||||
if CONFIG.hasEnchant:
|
||||
for tag, language in SHARED.spelling.listDictionaries():
|
||||
self.spellLang.addItem(language, tag)
|
||||
self.mainForm.addRow(
|
||||
self.tr("Spell check language"),
|
||||
self.spellLang,
|
||||
self.tr("Overrides main preferences.")
|
||||
self.addRow(
|
||||
self.tr("Spell check language"), self.spellLang,
|
||||
self.tr("Overrides main preferences."),
|
||||
stretch=(3, 2)
|
||||
)
|
||||
if (idx := self.spellLang.findData(data.spellLang)) != -1:
|
||||
self.spellLang.setCurrentIndex(idx)
|
||||
|
||||
langIdx = 0
|
||||
if pData.spellLang is not None:
|
||||
langIdx = self.spellLang.findData(pData.spellLang)
|
||||
if langIdx != -1:
|
||||
self.spellLang.setCurrentIndex(langIdx)
|
||||
|
||||
# Backup on Close
|
||||
self.doBackup = NSwitch(self)
|
||||
self.doBackup.setChecked(not pData.doBackup)
|
||||
self.mainForm.addRow(
|
||||
self.tr("No backup on close"),
|
||||
self.doBackup,
|
||||
self.doBackup.setChecked(not data.doBackup)
|
||||
self.addRow(
|
||||
self.tr("Disable backup on close"), self.doBackup,
|
||||
self.tr("Overrides main preferences.")
|
||||
)
|
||||
|
||||
self.finalise()
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiProjectEditMain
|
||||
# END Class _SettingsPage
|
||||
|
||||
|
||||
class GuiProjectEditStatus(QWidget):
|
||||
class _StatusPage(NFixedPage):
|
||||
|
||||
COL_LABEL = 0
|
||||
COL_USAGE = 1
|
||||
@@ -297,56 +309,54 @@ class GuiProjectEditStatus(QWidget):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
if isStatus:
|
||||
self.theStatus = SHARED.project.data.itemStatus
|
||||
pageLabel = self.tr("Novel File Status Levels")
|
||||
status = SHARED.project.data.itemStatus
|
||||
pageLabel = self.tr("Novel Document Status Levels")
|
||||
colSetting = "statusColW"
|
||||
else:
|
||||
self.theStatus = SHARED.project.data.itemImport
|
||||
pageLabel = self.tr("Note File Importance Levels")
|
||||
status = SHARED.project.data.itemImport
|
||||
pageLabel = self.tr("Project Note Importance Levels")
|
||||
colSetting = "importColW"
|
||||
|
||||
wCol0 = CONFIG.pxInt(
|
||||
SHARED.project.options.getInt("GuiProjectSettings", colSetting, 130)
|
||||
)
|
||||
|
||||
self.colDeleted = []
|
||||
self.colChanged = False
|
||||
self.selColour = QColor(100, 100, 100)
|
||||
self._changed = False
|
||||
self._colDeleted = []
|
||||
self._selColour = QColor(100, 100, 100)
|
||||
|
||||
self.iPx = SHARED.theme.baseIconSize
|
||||
|
||||
# The List
|
||||
# ========
|
||||
# Title
|
||||
self.pageTitle = NColourLabel(
|
||||
pageLabel, SHARED.theme.helpText, parent=self,
|
||||
scale=NColourLabel.HEADER_SCALE
|
||||
)
|
||||
|
||||
self.listBox = QTreeWidget()
|
||||
self.listBox.setHeaderLabels([
|
||||
self.tr("Label"), self.tr("Usage"),
|
||||
])
|
||||
# List Box
|
||||
self.listBox = QTreeWidget(self)
|
||||
self.listBox.setHeaderLabels([self.tr("Label"), self.tr("Usage")])
|
||||
self.listBox.itemSelectionChanged.connect(self._selectedItem)
|
||||
self.listBox.setColumnWidth(self.COL_LABEL, wCol0)
|
||||
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"])
|
||||
|
||||
# List Controls
|
||||
# =============
|
||||
|
||||
self.addButton = QPushButton(SHARED.theme.getIcon("add"), "")
|
||||
self.addButton = QPushButton(SHARED.theme.getIcon("add"), "", self)
|
||||
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.upButton = QPushButton(SHARED.theme.getIcon("up"), "")
|
||||
self.upButton = QPushButton(SHARED.theme.getIcon("up"), "", self)
|
||||
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))
|
||||
|
||||
# Edit Form
|
||||
# =========
|
||||
|
||||
self.editName = QLineEdit()
|
||||
self.editName.setMaxLength(40)
|
||||
self.editName.setPlaceholderText(self.tr("Select item to edit"))
|
||||
@@ -364,8 +374,6 @@ class GuiProjectEditStatus(QWidget):
|
||||
self.saveButton.clicked.connect(self._saveItem)
|
||||
|
||||
# Assemble
|
||||
# ========
|
||||
|
||||
self.listControls = QVBoxLayout()
|
||||
self.listControls.addWidget(self.addButton)
|
||||
self.listControls.addWidget(self.delButton)
|
||||
@@ -387,16 +395,25 @@ class GuiProjectEditStatus(QWidget):
|
||||
self.innerBox.addLayout(self.listControls)
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.addWidget(QLabel("<b>%s</b>" % pageLabel))
|
||||
self.outerBox.addWidget(self.pageTitle)
|
||||
self.outerBox.addLayout(self.innerBox)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
self.setCentralLayout(self.outerBox)
|
||||
|
||||
return
|
||||
|
||||
@property
|
||||
def wasChanged(self) -> bool:
|
||||
"""The user changed these settings."""
|
||||
return self._changed
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
|
||||
def getNewList(self) -> tuple[list, list]:
|
||||
"""Return list of entries."""
|
||||
if self.colChanged:
|
||||
if self._changed:
|
||||
newList = []
|
||||
for n in range(self.listBox.topLevelItemCount()):
|
||||
item = self.listBox.topLevelItem(n)
|
||||
@@ -406,10 +423,13 @@ class GuiProjectEditStatus(QWidget):
|
||||
"name": item.text(self.COL_LABEL),
|
||||
"cols": item.data(self.COL_LABEL, self.COL_ROLE),
|
||||
})
|
||||
return newList, self.colDeleted
|
||||
|
||||
return newList, self._colDeleted
|
||||
return [], []
|
||||
|
||||
def columnWidth(self) -> int:
|
||||
"""Return the size of the header column."""
|
||||
return self.listBox.columnWidth(0)
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
##
|
||||
@@ -417,12 +437,12 @@ class GuiProjectEditStatus(QWidget):
|
||||
@pyqtSlot()
|
||||
def _selectColour(self) -> None:
|
||||
"""Open a dialog to select the status icon colour."""
|
||||
if self.selColour is not None:
|
||||
if self._selColour is not None:
|
||||
newCol = QColorDialog.getColor(
|
||||
self.selColour, self, self.tr("Select Colour")
|
||||
self._selColour, self, self.tr("Select Colour")
|
||||
)
|
||||
if newCol.isValid():
|
||||
self.selColour = newCol
|
||||
self._selColour = newCol
|
||||
pixmap = QPixmap(self.iPx, self.iPx)
|
||||
pixmap.fill(newCol)
|
||||
self.colButton.setIcon(QIcon(pixmap))
|
||||
@@ -433,7 +453,7 @@ class GuiProjectEditStatus(QWidget):
|
||||
def _newItem(self) -> None:
|
||||
"""Create a new status item."""
|
||||
self._addItem(None, self.tr("New Item"), (100, 100, 100), 0)
|
||||
self.colChanged = True
|
||||
self._changed = True
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -446,8 +466,8 @@ class GuiProjectEditStatus(QWidget):
|
||||
SHARED.error(self.tr("Cannot delete a status item that is in use."))
|
||||
else:
|
||||
self.listBox.takeTopLevelItem(iRow)
|
||||
self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE))
|
||||
self.colChanged = True
|
||||
self._colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE))
|
||||
self._changed = True
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -458,9 +478,9 @@ class GuiProjectEditStatus(QWidget):
|
||||
selItem.setText(self.COL_LABEL, simplified(self.editName.text()))
|
||||
selItem.setIcon(self.COL_LABEL, self.colButton.icon())
|
||||
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
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -474,7 +494,7 @@ class GuiProjectEditStatus(QWidget):
|
||||
name = selItem.text(self.COL_LABEL)
|
||||
pixmap = QPixmap(self.iPx, self.iPx)
|
||||
pixmap.fill(QColor(*cols))
|
||||
self.selColour = QColor(*cols)
|
||||
self._selColour = QColor(*cols)
|
||||
self.editName.setText(name)
|
||||
self.colButton.setIcon(QIcon(pixmap))
|
||||
self.editName.selectAll()
|
||||
@@ -485,7 +505,7 @@ class GuiProjectEditStatus(QWidget):
|
||||
else:
|
||||
pixmap = QPixmap(self.iPx, self.iPx)
|
||||
pixmap.fill(QColor(100, 100, 100))
|
||||
self.selColour = QColor(100, 100, 100)
|
||||
self._selColour = QColor(100, 100, 100)
|
||||
self.editName.setText("")
|
||||
self.colButton.setIcon(QIcon(pixmap))
|
||||
self.editName.setEnabled(False)
|
||||
@@ -498,16 +518,16 @@ class GuiProjectEditStatus(QWidget):
|
||||
##
|
||||
|
||||
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."""
|
||||
pixmap = QPixmap(self.iPx, self.iPx)
|
||||
pixmap.fill(QColor(*cols))
|
||||
pixmap.fill(QColor(*colour))
|
||||
|
||||
item = QTreeWidgetItem()
|
||||
item.setText(self.COL_LABEL, name)
|
||||
item.setIcon(self.COL_LABEL, QIcon(pixmap))
|
||||
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.setText(self.COL_USAGE, self._usageString(count))
|
||||
|
||||
@@ -533,30 +553,29 @@ class GuiProjectEditStatus(QWidget):
|
||||
|
||||
if cItem is not None:
|
||||
cItem.setSelected(True)
|
||||
self.colChanged = True
|
||||
self._changed = True
|
||||
|
||||
return
|
||||
|
||||
def _getSelectedItem(self) -> QTreeWidgetItem | None:
|
||||
"""Get the currently selected item."""
|
||||
selItem = self.listBox.selectedItems()
|
||||
if len(selItem) > 0:
|
||||
return selItem[0]
|
||||
if items := self.listBox.selectedItems():
|
||||
return items[0]
|
||||
return None
|
||||
|
||||
def _usageString(self, nUse: int) -> str:
|
||||
def _usageString(self, count: int) -> str:
|
||||
"""Generate usage string."""
|
||||
if nUse == 0:
|
||||
if count == 0:
|
||||
return self.tr("Not in use")
|
||||
elif nUse == 1:
|
||||
elif count == 1:
|
||||
return self.tr("Used once")
|
||||
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_REPL = 1
|
||||
@@ -564,24 +583,24 @@ class GuiProjectEditReplace(QWidget):
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.arChanged = False
|
||||
self._changed = False
|
||||
|
||||
wCol0 = CONFIG.pxInt(
|
||||
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
|
||||
# ========
|
||||
|
||||
self.listBox = QTreeWidget()
|
||||
self.listBox.setHeaderLabels([
|
||||
self.tr("Keyword"),
|
||||
self.tr("Replace With"),
|
||||
])
|
||||
self.listBox.itemSelectionChanged.connect(self._selectedItem)
|
||||
self.listBox.setHeaderLabels([self.tr("Keyword"), self.tr("Replace With")])
|
||||
self.listBox.setColumnWidth(self.COL_KEY, wCol0)
|
||||
self.listBox.setIndentation(0)
|
||||
self.listBox.itemSelectionChanged.connect(self._selectedItem)
|
||||
|
||||
for aKey, aVal in SHARED.project.data.autoReplace.items():
|
||||
newItem = QTreeWidgetItem(["<%s>" % aKey, aVal])
|
||||
@@ -591,8 +610,6 @@ class GuiProjectEditReplace(QWidget):
|
||||
self.listBox.setSortingEnabled(True)
|
||||
|
||||
# List Controls
|
||||
# =============
|
||||
|
||||
self.addButton = QPushButton(SHARED.theme.getIcon("add"), "")
|
||||
self.addButton.clicked.connect(self._addEntry)
|
||||
|
||||
@@ -600,8 +617,6 @@ class GuiProjectEditReplace(QWidget):
|
||||
self.delButton.clicked.connect(self._delEntry)
|
||||
|
||||
# Edit Form
|
||||
# =========
|
||||
|
||||
self.editKey = QLineEdit()
|
||||
self.editKey.setPlaceholderText(self.tr("Select item to edit"))
|
||||
self.editKey.setEnabled(False)
|
||||
@@ -615,8 +630,6 @@ class GuiProjectEditReplace(QWidget):
|
||||
self.saveButton.clicked.connect(self._saveEntry)
|
||||
|
||||
# Assemble
|
||||
# ========
|
||||
|
||||
self.listControls = QVBoxLayout()
|
||||
self.listControls.addWidget(self.addButton)
|
||||
self.listControls.addWidget(self.delButton)
|
||||
@@ -636,50 +649,61 @@ class GuiProjectEditReplace(QWidget):
|
||||
self.innerBox.addLayout(self.listControls)
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.addWidget(QLabel("<b>%s</b>" % pageLabel))
|
||||
self.outerBox.addWidget(self.pageTitle)
|
||||
self.outerBox.addLayout(self.innerBox)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
self.setCentralLayout(self.outerBox)
|
||||
|
||||
return
|
||||
|
||||
@property
|
||||
def wasChanged(self) -> bool:
|
||||
"""The user changed these settings."""
|
||||
return self._changed
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
|
||||
def getNewList(self) -> dict:
|
||||
"""Extract the list from the widget."""
|
||||
new = {}
|
||||
for n in range(self.listBox.topLevelItemCount()):
|
||||
tItem = self.listBox.topLevelItem(n)
|
||||
if tItem is not None:
|
||||
if tItem := self.listBox.topLevelItem(n):
|
||||
aKey = self._stripNotAllowed(tItem.text(0))
|
||||
aVal = tItem.text(1)
|
||||
if len(aKey) > 0:
|
||||
new[aKey] = aVal
|
||||
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
|
||||
edit form.
|
||||
"""
|
||||
selItem = self._getSelectedItem()
|
||||
if selItem is None:
|
||||
return False
|
||||
editKey = self._stripNotAllowed(selItem.text(0))
|
||||
editVal = selItem.text(1)
|
||||
self.editKey.setText(editKey)
|
||||
self.editValue.setText(editVal)
|
||||
self.editKey.setEnabled(True)
|
||||
self.editValue.setEnabled(True)
|
||||
self.editKey.selectAll()
|
||||
self.editKey.setFocus()
|
||||
return True
|
||||
if selItem := self._getSelectedItem():
|
||||
editKey = self._stripNotAllowed(selItem.text(0))
|
||||
editVal = selItem.text(1)
|
||||
self.editKey.setText(editKey)
|
||||
self.editValue.setText(editVal)
|
||||
self.editKey.setEnabled(True)
|
||||
self.editValue.setEnabled(True)
|
||||
self.editKey.selectAll()
|
||||
self.editKey.setFocus()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _saveEntry(self) -> None:
|
||||
"""Save the form data into the list widget."""
|
||||
selItem = self._getSelectedItem()
|
||||
if selItem:
|
||||
if selItem := self._getSelectedItem():
|
||||
newKey = self.editKey.text()
|
||||
newVal = self.editValue.text()
|
||||
saveKey = self._stripNotAllowed(newKey)
|
||||
@@ -691,38 +715,36 @@ class GuiProjectEditReplace(QWidget):
|
||||
self.editKey.setEnabled(False)
|
||||
self.editValue.setEnabled(False)
|
||||
self.listBox.clearSelection()
|
||||
self.arChanged = True
|
||||
self._changed = True
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _addEntry(self) -> None:
|
||||
"""Add a new list entry."""
|
||||
saveKey = "<keyword%d>" % (self.listBox.topLevelItemCount() + 1)
|
||||
newVal = ""
|
||||
newItem = QTreeWidgetItem([saveKey, newVal])
|
||||
self.listBox.addTopLevelItem(newItem)
|
||||
self.listBox.addTopLevelItem(QTreeWidgetItem([saveKey, ""]))
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _delEntry(self) -> None:
|
||||
"""Delete the selected entry."""
|
||||
selItem = self._getSelectedItem()
|
||||
if selItem:
|
||||
if selItem := self._getSelectedItem():
|
||||
self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(selItem))
|
||||
self.arChanged = True
|
||||
self._changed = True
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _getSelectedItem(self) -> QTreeWidgetItem | None:
|
||||
"""Extract the currently selected item."""
|
||||
selItem = self.listBox.selectedItems()
|
||||
if len(selItem) == 0:
|
||||
return None
|
||||
return selItem[0]
|
||||
if items := self.listBox.selectedItems():
|
||||
return items[0]
|
||||
return None
|
||||
|
||||
def _stripNotAllowed(self, key: str) -> str:
|
||||
"""Clean up the replace key string."""
|
||||
result = ""
|
||||
for c in key:
|
||||
if c.isalnum():
|
||||
result += c
|
||||
return result
|
||||
return "".join(c for c in key if c.isalnum())
|
||||
|
||||
# END Class GuiProjectEditReplace
|
||||
# END Class _ReplacePage
|
||||
@@ -3,9 +3,11 @@ novelWriter – Custom Widget: Config Layout
|
||||
==========================================
|
||||
|
||||
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: 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
|
||||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||
@@ -28,27 +30,74 @@ from __future__ import annotations
|
||||
from PyQt5.QtGui import QColor, QPalette
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractButton, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QScrollArea, QSizePolicy,
|
||||
QVBoxLayout, QWidget
|
||||
QAbstractButton, QFrame, QGridLayout, QHBoxLayout, QLabel, QLayout,
|
||||
QScrollArea, QSizePolicy, QVBoxLayout, QWidget
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG
|
||||
|
||||
FONT_SCALE = 0.9
|
||||
DEFAULT_SCALE = 0.9
|
||||
RIGHT_TOP = Qt.AlignmentFlag.AlignRight | 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):
|
||||
"""Extension: Scrollable Form Widget
|
||||
|
||||
A custom widget that creates a form within a scrollable area.
|
||||
"""
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent=parent)
|
||||
self._helpCol = QColor(0, 0, 0)
|
||||
self._fontScale = FONT_SCALE
|
||||
self._fontScale = DEFAULT_SCALE
|
||||
self._first = True
|
||||
self._indent = CONFIG.pxInt(12)
|
||||
|
||||
self._sections: dict[int, QLabel] = {}
|
||||
self._editable: dict[str, NHelpLabel] = {}
|
||||
self._editable: dict[str, NColourLabel] = {}
|
||||
self._index: dict[str, QWidget] = {}
|
||||
|
||||
self._layout = QVBoxLayout()
|
||||
@@ -61,6 +110,8 @@ class NScrollableForm(QScrollArea):
|
||||
self.setWidgetResizable(True)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.setFrameShadow(QFrame.Shadow.Sunken)
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
|
||||
return
|
||||
|
||||
@@ -76,9 +127,9 @@ class NScrollableForm(QScrollArea):
|
||||
# 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."""
|
||||
self._helpCol = color if isinstance(color, QColor) else QColor(*color)
|
||||
self._helpCol = color
|
||||
self._fontScale = scale
|
||||
return
|
||||
|
||||
@@ -88,6 +139,11 @@ class NScrollableForm(QScrollArea):
|
||||
qHelp.setText(text)
|
||||
return
|
||||
|
||||
def setRowIndent(self, indent: int) -> None:
|
||||
"""Set the indentation of each row."""
|
||||
self._indent = max(indent, 0)
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -119,37 +175,43 @@ class NScrollableForm(QScrollArea):
|
||||
return
|
||||
|
||||
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."""
|
||||
row = QHBoxLayout()
|
||||
row.setSpacing(CONFIG.pxInt(4))
|
||||
row.setSpacing(CONFIG.pxInt(12))
|
||||
|
||||
mPx = CONFIG.pxInt(12)
|
||||
qLabel = QLabel(label, self)
|
||||
qLabel.setIndent(mPx)
|
||||
qLabel.setIndent(self._indent)
|
||||
qLabel.setBuddy(widget)
|
||||
|
||||
if helpText:
|
||||
qHelp = NHelpLabel(str(helpText), self._helpCol, self._fontScale)
|
||||
qHelp.setIndent(mPx)
|
||||
qHelp = NColourLabel(
|
||||
str(helpText), self._helpCol, parent=self,
|
||||
scale=self._fontScale, wrap=True, indent=self._indent
|
||||
)
|
||||
labelBox = QVBoxLayout()
|
||||
labelBox.addWidget(qLabel)
|
||||
labelBox.addWidget(qHelp)
|
||||
labelBox.setSpacing(0)
|
||||
labelBox.addStretch(1)
|
||||
row.addLayout(labelBox)
|
||||
row.addLayout(labelBox, stretch[0])
|
||||
if editable:
|
||||
self._editable[editable] = qHelp
|
||||
else:
|
||||
row.addWidget(qLabel)
|
||||
|
||||
row.addSpacing(mPx)
|
||||
row.addWidget(widget)
|
||||
row.addWidget(qLabel, stretch[0])
|
||||
|
||||
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):
|
||||
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._index[label.strip()] = widget
|
||||
@@ -173,7 +235,7 @@ class NConfigLayout(QGridLayout):
|
||||
|
||||
self._nextRow = 0
|
||||
self._helpCol = QColor(0, 0, 0)
|
||||
self._fontScale = FONT_SCALE
|
||||
self._fontScale = DEFAULT_SCALE
|
||||
self._itemMap = {}
|
||||
|
||||
wSp = CONFIG.pxInt(8)
|
||||
@@ -183,24 +245,6 @@ class NConfigLayout(QGridLayout):
|
||||
|
||||
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
|
||||
##
|
||||
@@ -216,8 +260,8 @@ class NConfigLayout(QGridLayout):
|
||||
self._nextRow += 1
|
||||
return
|
||||
|
||||
def addRow(self, label: str, widget: QWidget, helpText: str | None = None,
|
||||
unit: str | None = None, button: QWidget | None = None) -> int:
|
||||
def addRow(self, label: str, widget: QWidget, unit: str | None = None,
|
||||
button: QWidget | None = None) -> int:
|
||||
"""Add a label and a widget as a new row of the grid."""
|
||||
wSp = CONFIG.pxInt(8)
|
||||
qLabel = QLabel(label)
|
||||
@@ -225,17 +269,7 @@ class NConfigLayout(QGridLayout):
|
||||
qLabel.setBuddy(widget)
|
||||
|
||||
qHelp = None
|
||||
if helpText is not None:
|
||||
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)
|
||||
self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP)
|
||||
|
||||
if isinstance(unit, str):
|
||||
controlBox = QHBoxLayout()
|
||||
@@ -252,12 +286,7 @@ class NConfigLayout(QGridLayout):
|
||||
self.addLayout(controlBox, self._nextRow, 1, 1, 1, RIGHT_TOP)
|
||||
|
||||
else:
|
||||
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)
|
||||
self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP)
|
||||
|
||||
self.setRowStretch(self._nextRow, 0)
|
||||
self.setRowStretch(self._nextRow+1, 1)
|
||||
@@ -306,17 +335,9 @@ class NSimpleLayout(QGridLayout):
|
||||
wSp = CONFIG.pxInt(8)
|
||||
qLabel = QLabel(label)
|
||||
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)
|
||||
|
||||
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+1, 1)
|
||||
self._nextRow += 1
|
||||
@@ -326,25 +347,33 @@ class NSimpleLayout(QGridLayout):
|
||||
# END Class NSimpleLayout
|
||||
|
||||
|
||||
class NHelpLabel(QLabel):
|
||||
class NColourLabel(QLabel):
|
||||
"""Extension: A Coloured Label
|
||||
|
||||
def __init__(self, text: str, color: QColor | list | tuple,
|
||||
fontSize: float = FONT_SCALE) -> None:
|
||||
super().__init__(text)
|
||||
A custom widget that draws a label in a specific colour, and
|
||||
optionally at a specific size, and word wrapped.
|
||||
"""
|
||||
|
||||
qCol = color if isinstance(color, QColor) else QColor(*color)
|
||||
HELP_SCALE = DEFAULT_SCALE
|
||||
HEADER_SCALE = 1.25
|
||||
|
||||
lblCol = self.palette()
|
||||
lblCol.setColor(QPalette.WindowText, qCol)
|
||||
self.setPalette(lblCol)
|
||||
def __init__(self, text: str, color: QColor, parent: QWidget | None = None,
|
||||
scale: float = HELP_SCALE, wrap: bool = False, indent: int = 0) -> None:
|
||||
super().__init__(text, parent=parent)
|
||||
|
||||
lblFont = self.font()
|
||||
lblFont.setPointSizeF(fontSize*lblFont.pointSizeF())
|
||||
self.setFont(lblFont)
|
||||
font = self.font()
|
||||
font.setPointSizeF(scale*font.pointSizeF())
|
||||
colour = self.palette()
|
||||
colour.setColor(QPalette.WindowText, color)
|
||||
|
||||
self.setWordWrap(True)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.setPalette(colour)
|
||||
self.setFont(font)
|
||||
self.setIndent(indent)
|
||||
|
||||
if wrap:
|
||||
self.setWordWrap(True)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
return
|
||||
|
||||
# END Class NHelpLabel
|
||||
# END Class NColourLabel
|
||||
|
||||
@@ -3,7 +3,7 @@ novelWriter – Custom Widget: Novel Selector
|
||||
===========================================
|
||||
|
||||
File History:
|
||||
Created: 2022-11-17 [2.0]
|
||||
Created: 2022-11-17 [2.0] NovelSelector
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||
@@ -43,6 +43,8 @@ class NovelSelector(QComboBox):
|
||||
super().__init__(parent=parent)
|
||||
self._blockSignal = False
|
||||
self._firstHandle = None
|
||||
self._includeAll = False
|
||||
self._listFormat = None
|
||||
self.currentIndexChanged.connect(self._indexChanged)
|
||||
return
|
||||
|
||||
@@ -64,17 +66,29 @@ class NovelSelector(QComboBox):
|
||||
|
||||
def setHandle(self, tHandle: str | None, blockSignal: bool = True) -> None:
|
||||
"""Set the currently selected handle."""
|
||||
self._blockSignal = blockSignal
|
||||
if tHandle is None:
|
||||
index = self.count() - 1
|
||||
else:
|
||||
index = self.findData(tHandle)
|
||||
if index >= 0:
|
||||
if (index := self.findData(tHandle) if tHandle else (self.count() - 1)) >= 0:
|
||||
self._blockSignal = blockSignal
|
||||
self.setCurrentIndex(index)
|
||||
self._blockSignal = False
|
||||
self._blockSignal = False
|
||||
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."""
|
||||
self._blockSignal = True
|
||||
self._firstHandle = None
|
||||
@@ -83,8 +97,8 @@ class NovelSelector(QComboBox):
|
||||
icon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])
|
||||
handle = self.currentData()
|
||||
for tHandle, nwItem in SHARED.project.tree.iterRoots(nwItemClass.NOVEL):
|
||||
if prefix:
|
||||
name = prefix.format(nwItem.itemName)
|
||||
if self._listFormat:
|
||||
name = self._listFormat.format(nwItem.itemName)
|
||||
self.addItem(name, tHandle)
|
||||
else:
|
||||
name = nwItem.itemName
|
||||
@@ -92,7 +106,7 @@ class NovelSelector(QComboBox):
|
||||
if self._firstHandle is None:
|
||||
self._firstHandle = tHandle
|
||||
|
||||
if includeAll:
|
||||
if self._includeAll:
|
||||
self.insertSeparator(self.count())
|
||||
self.addItem(icon, self.tr("All Novel Folders"), "")
|
||||
|
||||
|
||||
@@ -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 2018–2024, 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
|
||||
@@ -45,10 +45,9 @@ class NPagedSideBar(QToolBar):
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self._buttons = []
|
||||
self._actions = []
|
||||
self._labelCol = None
|
||||
self._spacerHeight = self.fontMetrics().height() // 2
|
||||
self._buttons: dict[int, _NPagedToolButton] = {}
|
||||
|
||||
self._group = QButtonGroup(self)
|
||||
self._group.setExclusive(True)
|
||||
@@ -67,9 +66,9 @@ class NPagedSideBar(QToolBar):
|
||||
"""Return a specific button."""
|
||||
return self._buttons[buttonId]
|
||||
|
||||
def setLabelColor(self, color: list | QColor) -> None:
|
||||
def setLabelColor(self, color: QColor) -> None:
|
||||
"""Set the text color for the labels."""
|
||||
self._labelCol = color if isinstance(color, QColor) else QColor(*color)
|
||||
self._labelCol = color
|
||||
return
|
||||
|
||||
def addSeparator(self) -> None:
|
||||
@@ -95,8 +94,7 @@ class NPagedSideBar(QToolBar):
|
||||
action = self.insertWidget(self._stretchAction, button)
|
||||
self._group.addButton(button, id=buttonId)
|
||||
|
||||
self._buttons.append(button)
|
||||
self._actions.append(action)
|
||||
self._buttons[buttonId] = button
|
||||
|
||||
return action
|
||||
|
||||
|
||||
@@ -276,14 +276,14 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
def updateSyntaxColours(self) -> None:
|
||||
"""Update the syntax highlighting theme."""
|
||||
mainPalette = self.palette()
|
||||
mainPalette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
|
||||
mainPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
|
||||
mainPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
|
||||
mainPalette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
|
||||
mainPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
|
||||
mainPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
|
||||
self.setPalette(mainPalette)
|
||||
|
||||
docPalette = self.viewport().palette()
|
||||
docPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
|
||||
docPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
|
||||
docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
|
||||
docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
|
||||
self.viewport().setPalette(docPalette)
|
||||
|
||||
self.docHeader.matchColours()
|
||||
@@ -2326,9 +2326,9 @@ class GuiDocToolBar(QWidget):
|
||||
def updateTheme(self) -> None:
|
||||
"""Initialise GUI elements that depend on specific settings."""
|
||||
palette = QPalette()
|
||||
palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
|
||||
palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
|
||||
palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
|
||||
palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
|
||||
palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
|
||||
palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
|
||||
self.setPalette(palette)
|
||||
|
||||
self.tbBoldMD.setIcon(SHARED.theme.getIcon("fmt_bold-md"))
|
||||
@@ -2866,10 +2866,11 @@ class GuiDocEditHeader(QWidget):
|
||||
self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
|
||||
self.closeButton.setIcon(SHARED.theme.getIcon("close"))
|
||||
|
||||
colText = SHARED.theme.colText
|
||||
buttonStyle = (
|
||||
"QToolButton {{border: none; background: transparent;}} "
|
||||
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
|
||||
).format(*SHARED.theme.colText)
|
||||
"QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
|
||||
).format(colText.red(), colText.green(), colText.blue())
|
||||
|
||||
self.tbButton.setStyleSheet(buttonStyle)
|
||||
self.searchButton.setStyleSheet(buttonStyle)
|
||||
@@ -2885,9 +2886,9 @@ class GuiDocEditHeader(QWidget):
|
||||
theme rather than the main GUI.
|
||||
"""
|
||||
palette = QPalette()
|
||||
palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
|
||||
palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
|
||||
palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
|
||||
palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
|
||||
palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
|
||||
palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
|
||||
|
||||
self.setPalette(palette)
|
||||
self.itemTitle.setPalette(palette)
|
||||
@@ -3093,9 +3094,9 @@ class GuiDocEditFooter(QWidget):
|
||||
theme rather than the main GUI.
|
||||
"""
|
||||
palette = QPalette()
|
||||
palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
|
||||
palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
|
||||
palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
|
||||
palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
|
||||
palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
|
||||
palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
|
||||
|
||||
self.setPalette(palette)
|
||||
self.statusText.setPalette(palette)
|
||||
|
||||
@@ -48,6 +48,8 @@ SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
|
||||
|
||||
class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
__slots__ = ("_tItem", "_tHandle", "_spellCheck", "_spellErr", "_hRules", "_hStyles")
|
||||
|
||||
BLOCK_NONE = 0
|
||||
BLOCK_TEXT = 1
|
||||
BLOCK_META = 2
|
||||
@@ -58,29 +60,14 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
logger.debug("Create: GuiDocHighlighter")
|
||||
|
||||
self._tItem = None
|
||||
self._tHandle = None
|
||||
self._tItem = None
|
||||
self._tHandle = None
|
||||
self._spellCheck = False
|
||||
self._spellErr = QTextCharFormat()
|
||||
|
||||
self._hRules: list[tuple[str, dict]] = []
|
||||
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()
|
||||
|
||||
logger.debug("Ready: GuiDocHighlighter")
|
||||
@@ -93,54 +80,42 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
"""
|
||||
logger.debug("Setting up highlighting rules")
|
||||
|
||||
self._colHead = QColor(*SHARED.theme.colHead)
|
||||
self._colHeadH = QColor(*SHARED.theme.colHeadH)
|
||||
self._colDialN = QColor(*SHARED.theme.colDialN)
|
||||
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)
|
||||
colEmph = SHARED.theme.colEmph if CONFIG.highlightEmph else None
|
||||
colBreak = QColor(SHARED.theme.colEmph)
|
||||
colBreak.setAlpha(64)
|
||||
|
||||
self._hRules = []
|
||||
self._hStyles = {
|
||||
"header1": self._makeFormat(self._colHead, "bold", 1.8),
|
||||
"header2": self._makeFormat(self._colHead, "bold", 1.6),
|
||||
"header3": self._makeFormat(self._colHead, "bold", 1.4),
|
||||
"header4": self._makeFormat(self._colHead, "bold", 1.2),
|
||||
"header1h": self._makeFormat(self._colHeadH, "bold", 1.8),
|
||||
"header2h": self._makeFormat(self._colHeadH, "bold", 1.6),
|
||||
"header3h": self._makeFormat(self._colHeadH, "bold", 1.4),
|
||||
"header4h": self._makeFormat(self._colHeadH, "bold", 1.2),
|
||||
"bold": self._makeFormat(self._colEmph, "bold"),
|
||||
"italic": self._makeFormat(self._colEmph, "italic"),
|
||||
"strike": self._makeFormat(self._colHidden, "strike"),
|
||||
"mspaces": self._makeFormat(self._colError, "errline"),
|
||||
"nobreak": self._makeFormat(self._colBreak, "background"),
|
||||
"dialogue1": self._makeFormat(self._colDialN),
|
||||
"dialogue2": self._makeFormat(self._colDialD),
|
||||
"dialogue3": self._makeFormat(self._colDialS),
|
||||
"replace": self._makeFormat(self._colRepTag),
|
||||
"hidden": self._makeFormat(self._colHidden),
|
||||
"code": self._makeFormat(self._colCode),
|
||||
"keyword": self._makeFormat(self._colKey),
|
||||
"modifier": self._makeFormat(self._colMod),
|
||||
"value": self._makeFormat(self._colVal, "underline"),
|
||||
"codevalue": self._makeFormat(self._colVal),
|
||||
"header1": self._makeFormat(SHARED.theme.colHead, "bold", 1.8),
|
||||
"header2": self._makeFormat(SHARED.theme.colHead, "bold", 1.6),
|
||||
"header3": self._makeFormat(SHARED.theme.colHead, "bold", 1.4),
|
||||
"header4": self._makeFormat(SHARED.theme.colHead, "bold", 1.2),
|
||||
"header1h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.8),
|
||||
"header2h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.6),
|
||||
"header3h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.4),
|
||||
"header4h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.2),
|
||||
"bold": self._makeFormat(colEmph, "bold"),
|
||||
"italic": self._makeFormat(colEmph, "italic"),
|
||||
"strike": self._makeFormat(SHARED.theme.colHidden, "strike"),
|
||||
"mspaces": self._makeFormat(SHARED.theme.colError, "errline"),
|
||||
"nobreak": self._makeFormat(colBreak, "background"),
|
||||
"dialogue1": self._makeFormat(SHARED.theme.colDialN),
|
||||
"dialogue2": self._makeFormat(SHARED.theme.colDialD),
|
||||
"dialogue3": self._makeFormat(SHARED.theme.colDialS),
|
||||
"replace": self._makeFormat(SHARED.theme.colRepTag),
|
||||
"hidden": self._makeFormat(SHARED.theme.colHidden),
|
||||
"code": self._makeFormat(SHARED.theme.colCode),
|
||||
"keyword": self._makeFormat(SHARED.theme.colKey),
|
||||
"modifier": self._makeFormat(SHARED.theme.colMod),
|
||||
"value": self._makeFormat(SHARED.theme.colVal, "underline"),
|
||||
"codevalue": self._makeFormat(SHARED.theme.colVal),
|
||||
"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
|
||||
if CONFIG.showMultiSpaces:
|
||||
@@ -305,12 +280,12 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self.setCurrentBlockState(self.BLOCK_META)
|
||||
if self._tItem:
|
||||
pIndex = SHARED.project.index
|
||||
isValid, theBits, thePos = pIndex.scanThis(text)
|
||||
isGood = pIndex.checkThese(theBits, self._tItem)
|
||||
isValid, bits, pos = pIndex.scanThis(text)
|
||||
isGood = pIndex.checkThese(bits, self._tItem)
|
||||
if isValid:
|
||||
for n, theBit in enumerate(theBits):
|
||||
xPos = thePos[n]
|
||||
xLen = len(theBit)
|
||||
for n, bit in enumerate(bits):
|
||||
xPos = pos[n]
|
||||
xLen = len(bit)
|
||||
if isGood[n]:
|
||||
if n == 0:
|
||||
self.setFormat(xPos, xLen, self._hStyles["keyword"])
|
||||
@@ -318,8 +293,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self.setFormat(xPos, xLen, self._hStyles["value"])
|
||||
else:
|
||||
kwFmt = self.format(xPos)
|
||||
kwFmt.setUnderlineColor(self._colError)
|
||||
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
|
||||
kwFmt.merge(self._hStyles["codeinval"])
|
||||
self.setFormat(xPos, xLen, kwFmt)
|
||||
|
||||
# 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 x in range(xPos, xPos+xLen):
|
||||
spFmt = self.format(x)
|
||||
spFmt.setUnderlineColor(self._colSpell)
|
||||
spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
|
||||
spFmt.merge(self._spellErr)
|
||||
self.setFormat(x, 1, spFmt)
|
||||
|
||||
return
|
||||
@@ -431,7 +404,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
if "strike" in styles:
|
||||
charFormat.setFontStrikeOut(True)
|
||||
if "errline" in styles:
|
||||
charFormat.setUnderlineColor(self._colError)
|
||||
charFormat.setUnderlineColor(SHARED.theme.colError)
|
||||
charFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
|
||||
if "underline" in styles:
|
||||
charFormat.setFontUnderline(True)
|
||||
|
||||
@@ -33,7 +33,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QPoint, QSize, Qt, QUrl
|
||||
from PyQt5.QtGui import (
|
||||
QColor, QCursor, QFont, QMouseEvent, QPalette, QResizeEvent, QTextCursor,
|
||||
QCursor, QFont, QMouseEvent, QPalette, QResizeEvent, QTextCursor,
|
||||
QTextOption
|
||||
)
|
||||
from PyQt5.QtWidgets import (
|
||||
@@ -149,14 +149,14 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
# Set the widget colours to match syntax theme
|
||||
mainPalette = self.palette()
|
||||
mainPalette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
|
||||
mainPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
|
||||
mainPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
|
||||
mainPalette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
|
||||
mainPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
|
||||
mainPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
|
||||
self.setPalette(mainPalette)
|
||||
|
||||
docPalette = self.viewport().palette()
|
||||
docPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
|
||||
docPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
|
||||
docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
|
||||
docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
|
||||
self.viewport().setPalette(docPalette)
|
||||
|
||||
self.docHeader.matchColours()
|
||||
@@ -463,7 +463,13 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""Generate an appropriate style sheet for the document viewer,
|
||||
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 = (
|
||||
"body {{"
|
||||
" color: rgb({tColR}, {tColG}, {tColB});"
|
||||
@@ -490,27 +496,27 @@ class GuiDocViewer(QTextBrowser):
|
||||
" text-align: center;"
|
||||
"}}\n"
|
||||
).format(
|
||||
tColR=pTheme.colText[0],
|
||||
tColG=pTheme.colText[1],
|
||||
tColB=pTheme.colText[2],
|
||||
hColR=pTheme.colHead[0],
|
||||
hColG=pTheme.colHead[1],
|
||||
hColB=pTheme.colHead[2],
|
||||
aColR=pTheme.colVal[0],
|
||||
aColG=pTheme.colVal[1],
|
||||
aColB=pTheme.colVal[2],
|
||||
eColR=pTheme.colEmph[0],
|
||||
eColG=pTheme.colEmph[1],
|
||||
eColB=pTheme.colEmph[2],
|
||||
kColR=pTheme.colKey[0],
|
||||
kColG=pTheme.colKey[1],
|
||||
kColB=pTheme.colKey[2],
|
||||
cColR=pTheme.colHidden[0],
|
||||
cColG=pTheme.colHidden[1],
|
||||
cColB=pTheme.colHidden[2],
|
||||
mColR=pTheme.colMod[0],
|
||||
mColG=pTheme.colMod[1],
|
||||
mColB=pTheme.colMod[2],
|
||||
tColR=colText.red(),
|
||||
tColG=colText.green(),
|
||||
tColB=colText.blue(),
|
||||
hColR=colHead.red(),
|
||||
hColG=colHead.green(),
|
||||
hColB=colHead.blue(),
|
||||
aColR=colVal.red(),
|
||||
aColG=colVal.green(),
|
||||
aColB=colVal.blue(),
|
||||
eColR=colEmph.red(),
|
||||
eColG=colEmph.green(),
|
||||
eColB=colEmph.blue(),
|
||||
kColR=colKey.red(),
|
||||
kColG=colKey.green(),
|
||||
kColB=colKey.blue(),
|
||||
cColR=colHidden.red(),
|
||||
cColG=colHidden.green(),
|
||||
cColB=colHidden.blue(),
|
||||
mColR=colMod.red(),
|
||||
mColG=colMod.green(),
|
||||
mColB=colMod.blue(),
|
||||
)
|
||||
self.document().setDefaultStyleSheet(styleSheet)
|
||||
|
||||
@@ -744,10 +750,11 @@ class GuiDocViewHeader(QWidget):
|
||||
self.refreshButton.setIcon(SHARED.theme.getIcon("refresh"))
|
||||
self.closeButton.setIcon(SHARED.theme.getIcon("close"))
|
||||
|
||||
colText = SHARED.theme.colText
|
||||
buttonStyle = (
|
||||
"QToolButton {{border: none; background: transparent;}} "
|
||||
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
|
||||
).format(*SHARED.theme.colText)
|
||||
"QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
|
||||
).format(colText.red(), colText.green(), colText.blue())
|
||||
|
||||
self.backButton.setStyleSheet(buttonStyle)
|
||||
self.forwardButton.setStyleSheet(buttonStyle)
|
||||
@@ -763,9 +770,9 @@ class GuiDocViewHeader(QWidget):
|
||||
theme rather than the main GUI.
|
||||
"""
|
||||
palette = QPalette()
|
||||
palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
|
||||
palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
|
||||
palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
|
||||
palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
|
||||
palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
|
||||
palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
|
||||
self.setPalette(palette)
|
||||
self.docTitle.setPalette(palette)
|
||||
return
|
||||
@@ -937,11 +944,11 @@ class GuiDocViewFooter(QWidget):
|
||||
self.showComments.setIcon(bulletIcon)
|
||||
self.showSynopsis.setIcon(bulletIcon)
|
||||
|
||||
# StyleSheets
|
||||
colText = SHARED.theme.colText
|
||||
buttonStyle = (
|
||||
"QToolButton {{border: none; background: transparent;}} "
|
||||
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
|
||||
).format(*SHARED.theme.colText)
|
||||
"QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
|
||||
).format(colText.red(), colText.green(), colText.blue())
|
||||
|
||||
self.showHide.setStyleSheet(buttonStyle)
|
||||
self.showComments.setStyleSheet(buttonStyle)
|
||||
@@ -956,9 +963,9 @@ class GuiDocViewFooter(QWidget):
|
||||
theme rather than the main GUI.
|
||||
"""
|
||||
palette = QPalette()
|
||||
palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
|
||||
palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
|
||||
palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
|
||||
palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
|
||||
palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
|
||||
palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
|
||||
self.setPalette(palette)
|
||||
return
|
||||
|
||||
|
||||
@@ -155,10 +155,10 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aProjectSettings.setShortcut("Ctrl+Shift+,")
|
||||
self.aProjectSettings.triggered.connect(self.mainGui.showProjectSettingsDialog)
|
||||
|
||||
# Project > Project Details
|
||||
self.aProjectDetails = self.projMenu.addAction(self.tr("Project Details"))
|
||||
self.aProjectDetails.setShortcut("Shift+F6")
|
||||
self.aProjectDetails.triggered.connect(self.mainGui.showProjectDetailsDialog)
|
||||
# Project > Novel Details
|
||||
self.aNovelDetails = self.projMenu.addAction(self.tr("Novel Details"))
|
||||
self.aNovelDetails.setShortcut("Shift+F6")
|
||||
self.aNovelDetails.triggered.connect(self.mainGui.showNovelDetailsDialog)
|
||||
|
||||
# Project > Separator
|
||||
self.projMenu.addSeparator()
|
||||
|
||||
@@ -207,9 +207,10 @@ class GuiNovelToolBar(QWidget):
|
||||
# Novel Selector
|
||||
selFont = self.font()
|
||||
selFont.setWeight(QFont.Weight.Bold)
|
||||
self.novelPrefix = self.tr("Outline of {0}")
|
||||
|
||||
self.novelValue = NovelSelector(self)
|
||||
self.novelValue.setFont(selFont)
|
||||
self.novelValue.setListFormat(self.tr("Outline of {0}"))
|
||||
self.novelValue.setMinimumWidth(CONFIG.pxInt(150))
|
||||
self.novelValue.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot)
|
||||
@@ -294,7 +295,7 @@ class GuiNovelToolBar(QWidget):
|
||||
"QComboBox {border-style: none; padding-left: 0;} "
|
||||
"QComboBox::drop-down {border-style: none}"
|
||||
)
|
||||
self.novelValue.updateList(prefix=self.novelPrefix)
|
||||
self.novelValue.refreshNovelList()
|
||||
self.tbNovel.setVisible(self.novelValue.count() > 1)
|
||||
|
||||
return
|
||||
@@ -307,7 +308,7 @@ class GuiNovelToolBar(QWidget):
|
||||
|
||||
def buildNovelRootMenu(self) -> None:
|
||||
"""Build the novel root menu."""
|
||||
self.novelValue.updateList(prefix=self.novelPrefix)
|
||||
self.novelValue.refreshNovelList()
|
||||
self.tbNovel.setVisible(self.novelValue.count() > 1)
|
||||
return
|
||||
|
||||
|
||||
@@ -220,6 +220,7 @@ class GuiOutlineToolBar(QToolBar):
|
||||
self.novelLabel.setContentsMargins(0, 0, mPx, 0)
|
||||
|
||||
self.novelValue = NovelSelector(self)
|
||||
self.novelValue.setIncludeAll(True)
|
||||
self.novelValue.setMinimumWidth(CONFIG.pxInt(200))
|
||||
self.novelValue.novelSelectionChanged.connect(self._novelValueChanged)
|
||||
|
||||
@@ -258,7 +259,7 @@ class GuiOutlineToolBar(QToolBar):
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
self.setStyleSheet("QToolBar {border: 0px;}")
|
||||
self.novelValue.updateList(includeAll=True)
|
||||
self.novelValue.refreshNovelList()
|
||||
self.aRefresh.setIcon(SHARED.theme.getIcon("refresh"))
|
||||
self.tbColumns.setIcon(SHARED.theme.getIcon("menu"))
|
||||
self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}")
|
||||
@@ -266,7 +267,7 @@ class GuiOutlineToolBar(QToolBar):
|
||||
|
||||
def populateNovelList(self) -> None:
|
||||
"""Reload the content of the novel list."""
|
||||
self.novelValue.updateList(includeAll=True)
|
||||
self.novelValue.refreshNovelList()
|
||||
return
|
||||
|
||||
def setCurrentRoot(self, rootHandle: str | None) -> None:
|
||||
|
||||
@@ -48,7 +48,7 @@ from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
|
||||
from novelwriter.dialogs.docmerge import GuiDocMerge
|
||||
from novelwriter.dialogs.docsplit import GuiDocSplit
|
||||
from novelwriter.dialogs.editlabel import GuiEditLabel
|
||||
from novelwriter.dialogs.projsettings import GuiProjectSettings
|
||||
from novelwriter.dialogs.projectsettings import GuiProjectSettings
|
||||
from novelwriter.enum import (
|
||||
nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwWidget
|
||||
)
|
||||
@@ -1748,7 +1748,7 @@ class _TreeContextMenu(QMenu):
|
||||
menu.addSeparator()
|
||||
action = menu.addAction(self.tr("Manage Labels ..."))
|
||||
action.triggered.connect(
|
||||
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_STATUS)
|
||||
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_STATUS)
|
||||
)
|
||||
else:
|
||||
menu = self.addMenu(self.tr("Set Importance to ..."))
|
||||
@@ -1765,7 +1765,7 @@ class _TreeContextMenu(QMenu):
|
||||
menu.addSeparator()
|
||||
action = menu.addAction(self.tr("Manage Labels ..."))
|
||||
action.triggered.connect(
|
||||
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_IMPORT)
|
||||
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_IMPORT)
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@@ -79,9 +79,9 @@ class GuiSideBar(QWidget):
|
||||
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
|
||||
|
||||
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.clicked.connect(self.mainGui.showProjectDetailsDialog)
|
||||
self.tbDetails.clicked.connect(self.mainGui.showNovelDetailsDialog)
|
||||
|
||||
self.tbStats = QToolButton(self)
|
||||
self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics")))
|
||||
|
||||
@@ -29,7 +29,6 @@ from time import time
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtGui import QColor
|
||||
from PyQt5.QtCore import pyqtSlot, QLocale
|
||||
from PyQt5.QtWidgets import qApp, QStatusBar, QLabel
|
||||
|
||||
@@ -55,9 +54,9 @@ class GuiMainStatus(QStatusBar):
|
||||
self._userIdle = False
|
||||
self._debugInfo = False
|
||||
|
||||
colNone = QColor(*SHARED.theme.statNone)
|
||||
colSaved = QColor(*SHARED.theme.statSaved)
|
||||
colUnsaved = QColor(*SHARED.theme.statUnsaved)
|
||||
colNone = SHARED.theme.statNone
|
||||
colSaved = SHARED.theme.statSaved
|
||||
colUnsaved = SHARED.theme.statUnsaved
|
||||
|
||||
iPx = SHARED.theme.baseIconSize
|
||||
|
||||
|
||||
+26
-41
@@ -70,10 +70,10 @@ class GuiTheme:
|
||||
self.isLightTheme = True
|
||||
|
||||
# GUI
|
||||
self.statNone = [120, 120, 120]
|
||||
self.statUnsaved = [200, 15, 39]
|
||||
self.statSaved = [2, 133, 37]
|
||||
self.helpText = [0, 0, 0]
|
||||
self.statNone = QColor(120, 120, 120)
|
||||
self.statUnsaved = QColor(200, 15, 39)
|
||||
self.statSaved = QColor(2, 133, 37)
|
||||
self.helpText = QColor(0, 0, 0)
|
||||
|
||||
# Loaded Syntax Settings
|
||||
# ======================
|
||||
@@ -88,23 +88,23 @@ class GuiTheme:
|
||||
self.syntaxLicenseUrl = ""
|
||||
|
||||
# Colours
|
||||
self.colBack = [255, 255, 255]
|
||||
self.colText = [0, 0, 0]
|
||||
self.colLink = [0, 0, 0]
|
||||
self.colHead = [0, 0, 0]
|
||||
self.colHeadH = [0, 0, 0]
|
||||
self.colEmph = [0, 0, 0]
|
||||
self.colDialN = [0, 0, 0]
|
||||
self.colDialD = [0, 0, 0]
|
||||
self.colDialS = [0, 0, 0]
|
||||
self.colHidden = [0, 0, 0]
|
||||
self.colCode = [0, 0, 0]
|
||||
self.colKey = [0, 0, 0]
|
||||
self.colVal = [0, 0, 0]
|
||||
self.colSpell = [0, 0, 0]
|
||||
self.colError = [0, 0, 0]
|
||||
self.colRepTag = [0, 0, 0]
|
||||
self.colMod = [0, 0, 0]
|
||||
self.colBack = QColor(255, 255, 255)
|
||||
self.colText = QColor(0, 0, 0)
|
||||
self.colLink = QColor(0, 0, 0)
|
||||
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)
|
||||
|
||||
# Class Setup
|
||||
# ===========
|
||||
@@ -256,12 +256,12 @@ class GuiTheme:
|
||||
backLNess = backCol.lightnessF()
|
||||
textLNess = textCol.lightnessF()
|
||||
self.isLightTheme = backLNess > textLNess
|
||||
if self.helpText == [0, 0, 0]:
|
||||
if self.helpText == QColor(0, 0, 0):
|
||||
if self.isLightTheme:
|
||||
helpLCol = textLNess + 0.35*(backLNess - textLNess)
|
||||
else:
|
||||
helpLCol = backLNess + 0.65*(textLNess - backLNess)
|
||||
self.helpText = [int(255*helpLCol)]*3
|
||||
self.helpText = QColor.fromHsl(0, 0, int(255*helpLCol))
|
||||
|
||||
# Icons
|
||||
defaultIcons = "typicons_light" if backLNess >= 0.5 else "typicons_dark"
|
||||
@@ -398,29 +398,14 @@ class GuiTheme:
|
||||
|
||||
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."""
|
||||
if parser.has_option(section, name):
|
||||
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
|
||||
return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255]))
|
||||
|
||||
def _setPalette(self, parser: NWConfigParser, section: str,
|
||||
name: str, value: QPalette.ColorRole) -> None:
|
||||
"""Set a palette colour value from a config string."""
|
||||
self._guiPalette.setColor(
|
||||
value, QColor(*self._parseColour(parser, section, name))
|
||||
)
|
||||
self._guiPalette.setColor(value, self._parseColour(parser, section, name))
|
||||
return
|
||||
|
||||
# End Class GuiTheme
|
||||
|
||||
+11
-16
@@ -53,11 +53,11 @@ from novelwriter.dialogs.about import GuiAbout
|
||||
from novelwriter.dialogs.updates import GuiUpdates
|
||||
from novelwriter.dialogs.wordlist import GuiWordList
|
||||
from novelwriter.dialogs.preferences import GuiPreferences
|
||||
from novelwriter.dialogs.projdetails import GuiProjectDetails
|
||||
from novelwriter.dialogs.projsettings import GuiProjectSettings
|
||||
from novelwriter.dialogs.projectsettings import GuiProjectSettings
|
||||
from novelwriter.tools.welcome import GuiWelcome
|
||||
from novelwriter.tools.manuscript import GuiManuscript
|
||||
from novelwriter.tools.dictionaries import GuiDictionaries
|
||||
from novelwriter.tools.noveldetails import GuiNovelDetails
|
||||
from novelwriter.tools.writingstats import GuiWritingStats
|
||||
|
||||
from novelwriter.enum import (
|
||||
@@ -79,13 +79,6 @@ class GuiMain(QMainWindow):
|
||||
function. Also, the project instance and theme instance are created
|
||||
here. These should be passed around to all other objects who need
|
||||
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:
|
||||
@@ -816,19 +809,19 @@ class GuiMain(QMainWindow):
|
||||
|
||||
@pyqtSlot()
|
||||
@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."""
|
||||
if SHARED.hasProject:
|
||||
dialog = GuiProjectSettings(self, focusTab=focusTab)
|
||||
dialog = GuiProjectSettings(self, gotoPage=focusTab)
|
||||
dialog.newProjectSettingsReady.connect(self._processProjectSettingsChanges)
|
||||
dialog.exec_()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def showProjectDetailsDialog(self) -> None:
|
||||
"""Open the project details dialog."""
|
||||
def showNovelDetailsDialog(self) -> None:
|
||||
"""Open the novel details dialog."""
|
||||
if SHARED.hasProject:
|
||||
dialog = GuiProjectDetails(self)
|
||||
dialog = GuiNovelDetails(self)
|
||||
dialog.setModal(True)
|
||||
dialog.show()
|
||||
dialog.raise_()
|
||||
@@ -1118,13 +1111,15 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _processProjectSettingsChanges(self) -> None:
|
||||
@pyqtSlot(bool)
|
||||
def _processProjectSettingsChanges(self, rebuildTrees: bool) -> None:
|
||||
"""Refresh data dependent on project settings."""
|
||||
logger.debug("Applying new project settings")
|
||||
SHARED.updateSpellCheckLanguage()
|
||||
self.itemDetails.refreshDetails()
|
||||
self._updateWindowTitle(SHARED.project.data.name)
|
||||
if rebuildTrees:
|
||||
self.rebuildTrees()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
|
||||
@@ -27,9 +27,7 @@ import logging
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtGui import (
|
||||
QColor, QFont, QIcon, QSyntaxHighlighter, QTextCharFormat, QTextDocument
|
||||
)
|
||||
from PyQt5.QtGui import QFont, QIcon, QSyntaxHighlighter, QTextCharFormat, QTextDocument
|
||||
from PyQt5.QtCore import QEvent, QSize, Qt, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractButton, QAbstractItemView, QComboBox, QDialog, QDialogButtonBox,
|
||||
@@ -862,9 +860,9 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter):
|
||||
def __init__(self, document: QTextDocument) -> None:
|
||||
super().__init__(document)
|
||||
self._fmtSymbol = QTextCharFormat()
|
||||
self._fmtSymbol.setForeground(QColor(*SHARED.theme.colHead))
|
||||
self._fmtSymbol.setForeground(SHARED.theme.colHead)
|
||||
self._fmtFormat = QTextCharFormat()
|
||||
self._fmtFormat.setForeground(QColor(*SHARED.theme.colEmph))
|
||||
self._fmtFormat.setForeground(SHARED.theme.colEmph)
|
||||
return
|
||||
|
||||
def highlightBlock(self, text: str) -> None:
|
||||
|
||||
@@ -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 2018–2024, 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
|
||||
@@ -25,7 +25,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
@@ -43,14 +42,11 @@ from PyQt5.QtWidgets import (
|
||||
|
||||
from novelwriter import CONFIG, SHARED, __version__, __date__
|
||||
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.core.coretools import ProjectBuilder
|
||||
from novelwriter.extensions.switch import NSwitch
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.guimain import GuiMain
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -58,8 +54,8 @@ class GuiWelcome(QDialog):
|
||||
|
||||
openProjectRequest = pyqtSignal(Path)
|
||||
|
||||
def __init__(self, mainGui: GuiMain) -> None:
|
||||
super().__init__(parent=mainGui)
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent=parent)
|
||||
|
||||
logger.debug("Create: GuiWelcome")
|
||||
self.setObjectName("GuiWelcome")
|
||||
@@ -91,7 +87,8 @@ class GuiWelcome(QDialog):
|
||||
self.nwLabel.setPixmap(self.nwImage)
|
||||
|
||||
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)
|
||||
@@ -343,7 +340,7 @@ class _ProjectListItem(QStyledItemDelegate):
|
||||
|
||||
self._dFont = qApp.font()
|
||||
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))
|
||||
|
||||
|
||||
+108
-136
@@ -23,6 +23,7 @@ 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 os
|
||||
import sys
|
||||
@@ -42,7 +43,7 @@ OS_DARWIN = 3
|
||||
# Utilities
|
||||
# =============================================================================================== #
|
||||
|
||||
def extractVersion(beQuiet=False):
|
||||
def extractVersion(beQuiet: bool = False) -> tuple[str, str, str]:
|
||||
"""Extract the novelWriter version number without having to import
|
||||
anything else from the main package.
|
||||
"""
|
||||
@@ -73,34 +74,31 @@ def extractVersion(beQuiet=False):
|
||||
return numVers, hexVers, relDate
|
||||
|
||||
|
||||
def compactVersion(version):
|
||||
"""Make the version number more compact."""
|
||||
return version.replace("-alpha", "a").replace("-beta", "b").replace("-rc", "rc")
|
||||
def stripVersion(version: str) -> str:
|
||||
"""Strip the pre-release part from a version number."""
|
||||
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):
|
||||
"""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):
|
||||
def readFile(fileName: str) -> str:
|
||||
"""Read an entire file and return as a string."""
|
||||
with open(fileName, mode="r", encoding="utf-8") as inFile:
|
||||
return inFile.read()
|
||||
|
||||
|
||||
def writeFile(fileName, writeText):
|
||||
def writeFile(fileName: str, writeText: str) -> None:
|
||||
"""Write string to file."""
|
||||
with open(fileName, mode="w+", encoding="utf-8") as outFile:
|
||||
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
|
||||
directory. The file can optionally be given a new name.
|
||||
"""
|
||||
@@ -113,7 +111,7 @@ def toUpload(srcPath, dstName=None):
|
||||
return
|
||||
|
||||
|
||||
def makeCheckSum(sumFile, cwd=None):
|
||||
def makeCheckSum(sumFile: str, cwd: str | None = None) -> str:
|
||||
"""Create a SHA256 checksum file."""
|
||||
try:
|
||||
if cwd is None:
|
||||
@@ -139,7 +137,7 @@ def makeCheckSum(sumFile, cwd=None):
|
||||
# Package Installer (pip)
|
||||
##
|
||||
|
||||
def installPackages(hostOS):
|
||||
def installPackages(hostOS: int) -> None:
|
||||
"""Install package dependencies both for this script and for running
|
||||
novelWriter itself.
|
||||
"""
|
||||
@@ -172,7 +170,7 @@ def installPackages(hostOS):
|
||||
# Clean Build and Dist Folders (build-clean)
|
||||
##
|
||||
|
||||
def cleanBuildDirs():
|
||||
def cleanBuildDirs() -> None:
|
||||
"""Recursively delete the 'build' and 'dist' folders."""
|
||||
print("")
|
||||
print("Cleaning up build environment ...")
|
||||
@@ -208,7 +206,7 @@ def cleanBuildDirs():
|
||||
# Build PDF Manual (manual)
|
||||
##
|
||||
|
||||
def buildPdfManual():
|
||||
def buildPdfManual() -> None:
|
||||
"""This function will build the documentation as manual.pdf."""
|
||||
print("")
|
||||
print("Building PDF Manual")
|
||||
@@ -262,7 +260,7 @@ def buildPdfManual():
|
||||
# Qt Linguist QM Builder (qtlrelease)
|
||||
##
|
||||
|
||||
def buildQtI18n():
|
||||
def buildQtI18n() -> None:
|
||||
"""Build the lang.qm files for Qt Linguist."""
|
||||
print("")
|
||||
print("Building Qt Localisation Files")
|
||||
@@ -315,7 +313,7 @@ def buildQtI18n():
|
||||
# Qt Linguist TS Builder (qtlupdate)
|
||||
##
|
||||
|
||||
def buildQtI18nTS(sysArgs):
|
||||
def buildQtI18nTS(sysArgs: list[str]) -> None:
|
||||
"""Build the lang.ts files for Qt Linguist."""
|
||||
print("")
|
||||
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."""
|
||||
outDir = "setup/macos"
|
||||
numVers = extractVersion()[0].partition("-")[0]
|
||||
numVers = stripVersion(extractVersion()[0])
|
||||
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]
|
||||
|
||||
plistXML = readFile(f"{outDir}/Info.plist.template").format(
|
||||
@@ -422,7 +420,7 @@ def genMacOSPlist():
|
||||
# Sample Project ZIP File Builder (sample)
|
||||
##
|
||||
|
||||
def buildSampleZip():
|
||||
def buildSampleZip() -> None:
|
||||
"""Bundle the sample project into a single zip file to be saved into
|
||||
the novelwriter/assets folder for further bundling into builds.
|
||||
"""
|
||||
@@ -459,7 +457,7 @@ def buildSampleZip():
|
||||
return
|
||||
|
||||
|
||||
def cleanBuiltAssets():
|
||||
def cleanBuiltAssets() -> None:
|
||||
"""Remove assets built by this script."""
|
||||
print("")
|
||||
print("Removing Built Assets")
|
||||
@@ -488,7 +486,7 @@ def cleanBuiltAssets():
|
||||
return
|
||||
|
||||
|
||||
def checkAssetsExist():
|
||||
def checkAssetsExist() -> bool:
|
||||
"""Check that the necessary compiled assets exist ahead of a build.
|
||||
"""
|
||||
hasSample = False
|
||||
@@ -523,7 +521,7 @@ def checkAssetsExist():
|
||||
# Import Translations (import-i18n)
|
||||
##
|
||||
|
||||
def importI18nUpdates(sysArgs):
|
||||
def importI18nUpdates(sysArgs: list[str]) -> None:
|
||||
"""Import new translation files from a zip file."""
|
||||
print("")
|
||||
print("Import Updated Translations")
|
||||
@@ -563,7 +561,7 @@ def importI18nUpdates(sysArgs):
|
||||
# Make Minimal Package (minimal-zip)
|
||||
##
|
||||
|
||||
def makeMinimalPackage(targetOS):
|
||||
def makeMinimalPackage(targetOS: int) -> None:
|
||||
"""Pack the core source file in a single zip file."""
|
||||
from zipfile import ZipFile, ZIP_DEFLATED
|
||||
|
||||
@@ -598,8 +596,7 @@ def makeMinimalPackage(targetOS):
|
||||
# Build Minimal Zip
|
||||
# =================
|
||||
|
||||
numVers, _, _ = extractVersion()
|
||||
pkgVers = compactVersion(numVers)
|
||||
pkgVers, _, _ = extractVersion()
|
||||
zipFile = f"novelwriter-{pkgVers}-minimal{targName}.zip"
|
||||
outFile = os.path.join(bldDir, zipFile)
|
||||
if os.path.isfile(outFile):
|
||||
@@ -679,7 +676,8 @@ def makeMinimalPackage(targetOS):
|
||||
# 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."""
|
||||
print("")
|
||||
print("Build Debian Package")
|
||||
@@ -692,13 +690,12 @@ def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buil
|
||||
# ============
|
||||
|
||||
numVers, hexVers, relDate = extractVersion()
|
||||
pkgVers = compactVersion(numVers)
|
||||
relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d")
|
||||
pkgDate = email.utils.format_datetime(relDate.replace(hour=12, tzinfo=None))
|
||||
print("")
|
||||
|
||||
if buildName:
|
||||
pkgVers = f"{pkgVers}{buildName}"
|
||||
pkgVers = numVers.replace("a", "~a").replace("b", "~b").replace("rc", "~rc")
|
||||
pkgVers = f"{pkgVers}+{buildName}" if buildName else pkgVers
|
||||
|
||||
# Set Up Folder
|
||||
# =============
|
||||
@@ -839,11 +836,7 @@ def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buil
|
||||
print("")
|
||||
|
||||
if sourceBuild:
|
||||
if hexVers[-2] == "f":
|
||||
ppaName = "novelwriter"
|
||||
else:
|
||||
ppaName = "novelwriter-pre"
|
||||
|
||||
ppaName = "novelwriter" if hexVers[-2] == "f" else "novelwriter-pre"
|
||||
return f"dput {ppaName}/{distName} {bldDir}/{bldPkg}_source.changes"
|
||||
|
||||
return ""
|
||||
@@ -853,14 +846,14 @@ def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buil
|
||||
# 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."""
|
||||
print("")
|
||||
print("Launchpad Packages")
|
||||
print("==================")
|
||||
print("")
|
||||
|
||||
if isFirst or isSnapshot:
|
||||
if isFirst:
|
||||
bldNum = "0"
|
||||
else:
|
||||
bldNum = input("Build number [0]: ")
|
||||
@@ -870,17 +863,12 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
|
||||
distLoop = [
|
||||
("20.04", "focal"),
|
||||
("22.04", "jammy"),
|
||||
("23.04", "lunar"),
|
||||
("23.10", "mantic"),
|
||||
("24.04", "noble"),
|
||||
]
|
||||
|
||||
tStamp = datetime.datetime.now().strftime("%Y%m%d~%H%M%S")
|
||||
if isSnapshot:
|
||||
print(f"Building Ununtu SNAPSHOT~{tStamp} for:")
|
||||
print("")
|
||||
else:
|
||||
print("Building Ubuntu packages for:")
|
||||
print("")
|
||||
print("Building Ubuntu packages for:")
|
||||
print("")
|
||||
for distNum, codeName in distLoop:
|
||||
print(f" * Ubuntu {distNum} {codeName.title()}")
|
||||
print("")
|
||||
@@ -895,11 +883,7 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
|
||||
|
||||
dputCmd = []
|
||||
for distNum, codeName in distLoop:
|
||||
if isSnapshot:
|
||||
buildName = f"+SNAPSHOT~{tStamp}~ubuntu{distNum}.0"
|
||||
else:
|
||||
buildName = f"~ubuntu{distNum}.{bldNum}"
|
||||
|
||||
buildName = f"ubuntu{distNum}.{bldNum}"
|
||||
dCmd = makeDebianPackage(
|
||||
signKey=signKey,
|
||||
sourceBuild=True,
|
||||
@@ -922,7 +906,7 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
|
||||
# Make AppImage (build-appimage)
|
||||
##
|
||||
|
||||
def makeAppImage(sysArgs):
|
||||
def makeAppImage(sysArgs: list[str]) -> list[str]:
|
||||
"""Build an AppImage."""
|
||||
import glob
|
||||
import argparse
|
||||
@@ -968,8 +952,7 @@ def makeAppImage(sysArgs):
|
||||
# Version Info
|
||||
# ============
|
||||
|
||||
numVers, _, relDate = extractVersion()
|
||||
pkgVers = compactVersion(numVers)
|
||||
pkgVers, _, relDate = extractVersion()
|
||||
relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d")
|
||||
print("")
|
||||
|
||||
@@ -1139,7 +1122,7 @@ def makeAppImage(sysArgs):
|
||||
# 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
|
||||
Windows installation.
|
||||
"""
|
||||
@@ -1371,7 +1354,7 @@ def makeWindowsEmbedded(sysArgs):
|
||||
# XDG Installation (xdg-install)
|
||||
##
|
||||
|
||||
def xdgInstall():
|
||||
def xdgInstall() -> None:
|
||||
"""Will attempt to install icons and make a launcher."""
|
||||
print("")
|
||||
print("XDG Install")
|
||||
@@ -1507,7 +1490,7 @@ def xdgInstall():
|
||||
# XDG Uninstallation (xdg-uninstall)
|
||||
##
|
||||
|
||||
def xdgUninstall():
|
||||
def xdgUninstall() -> None:
|
||||
"""Will attempt to uninstall icons and the launcher."""
|
||||
print("")
|
||||
print("XDG Uninstall")
|
||||
@@ -1577,7 +1560,7 @@ def xdgUninstall():
|
||||
# WIN Installation (win-install)
|
||||
##
|
||||
|
||||
def winInstall():
|
||||
def winInstall() -> None:
|
||||
"""Will attempt to install icons and make a launcher for Windows."""
|
||||
import winreg
|
||||
try:
|
||||
@@ -1704,7 +1687,7 @@ def winInstall():
|
||||
# WIN Uninstallation (win-uninstall)
|
||||
##
|
||||
|
||||
def winUninstall():
|
||||
def winUninstall() -> None:
|
||||
"""Will attempt to uninstall icons previously installed."""
|
||||
import winreg
|
||||
try:
|
||||
@@ -1805,40 +1788,35 @@ if __name__ == "__main__":
|
||||
else:
|
||||
hostOS = OS_NONE
|
||||
|
||||
sysArgs = sys.argv.copy()
|
||||
|
||||
# Set Target OS
|
||||
if "--target-linux" in sys.argv:
|
||||
sys.argv.remove("--target-linux")
|
||||
if "--target-linux" in sysArgs:
|
||||
sysArgs.remove("--target-linux")
|
||||
targetOS = OS_LINUX
|
||||
elif "--target-darwin" in sys.argv:
|
||||
sys.argv.remove("--target-darwin")
|
||||
elif "--target-darwin" in sysArgs:
|
||||
sysArgs.remove("--target-darwin")
|
||||
targetOS = OS_DARWIN
|
||||
elif "--target-win" in sys.argv:
|
||||
sys.argv.remove("--target-win")
|
||||
elif "--target-win" in sysArgs:
|
||||
sysArgs.remove("--target-win")
|
||||
targetOS = OS_WIN
|
||||
else:
|
||||
targetOS = hostOS
|
||||
|
||||
# Sign package
|
||||
if "--sign" in sys.argv:
|
||||
sys.argv.remove("--sign")
|
||||
if "--sign" in sysArgs:
|
||||
sysArgs.remove("--sign")
|
||||
doSign = True
|
||||
else:
|
||||
doSign = False
|
||||
|
||||
# First build
|
||||
if "--first" in sys.argv:
|
||||
sys.argv.remove("--first")
|
||||
if "--first" in sysArgs:
|
||||
sysArgs.remove("--first")
|
||||
isFirstBuild = True
|
||||
else:
|
||||
isFirstBuild = False
|
||||
|
||||
# Build snapshot
|
||||
if "--snapshot" in sys.argv:
|
||||
sys.argv.remove("--snapshot")
|
||||
isSnapshot = True
|
||||
else:
|
||||
isSnapshot = False
|
||||
|
||||
helpMsg = [
|
||||
"",
|
||||
"novelWriter Setup Tool",
|
||||
@@ -1856,7 +1834,7 @@ if __name__ == "__main__":
|
||||
"",
|
||||
" help Print the help message.",
|
||||
" 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.",
|
||||
"",
|
||||
"Additional Builds:",
|
||||
@@ -1880,7 +1858,6 @@ if __name__ == "__main__":
|
||||
" sign package.",
|
||||
" build-ubuntu Build a .deb packages Launchpad. Add --sign to ",
|
||||
" 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.",
|
||||
" The package must be built from a minimal windows zip file.",
|
||||
" build-appimage Build an AppImage. Argument --linux-tag defaults to",
|
||||
@@ -1905,71 +1882,66 @@ if __name__ == "__main__":
|
||||
# General
|
||||
# =======
|
||||
|
||||
if "help" in sys.argv:
|
||||
sys.argv.remove("help")
|
||||
if "help" in sysArgs:
|
||||
sysArgs.remove("help")
|
||||
print("\n".join(helpMsg))
|
||||
sys.exit(0)
|
||||
|
||||
if "version" in sys.argv:
|
||||
sys.argv.remove("version")
|
||||
numVers, _, _ = extractVersion(beQuiet=True)
|
||||
if "-c" in sys.argv:
|
||||
sys.argv.remove("-c")
|
||||
print(compactVersion(numVers), end=None)
|
||||
else:
|
||||
print(numVers, end=None)
|
||||
if "version" in sysArgs:
|
||||
sysArgs.remove("version")
|
||||
print(extractVersion(beQuiet=True)[0], end=None)
|
||||
sys.exit(0)
|
||||
|
||||
if "pip" in sys.argv:
|
||||
sys.argv.remove("pip")
|
||||
if "pip" in sysArgs:
|
||||
sysArgs.remove("pip")
|
||||
installPackages(hostOS)
|
||||
|
||||
if "build-clean" in sys.argv:
|
||||
sys.argv.remove("build-clean")
|
||||
if "build-clean" in sysArgs:
|
||||
sysArgs.remove("build-clean")
|
||||
cleanBuildDirs()
|
||||
|
||||
# Additional Builds
|
||||
# =================
|
||||
|
||||
if "manual" in sys.argv:
|
||||
sys.argv.remove("manual")
|
||||
if "manual" in sysArgs:
|
||||
sysArgs.remove("manual")
|
||||
buildPdfManual()
|
||||
|
||||
if "qtlrelease" in sys.argv:
|
||||
sys.argv.remove("qtlrelease")
|
||||
if "qtlrelease" in sysArgs:
|
||||
sysArgs.remove("qtlrelease")
|
||||
buildQtI18n()
|
||||
|
||||
if "qtlupdate" in sys.argv:
|
||||
sys.argv.remove("qtlupdate")
|
||||
buildQtI18nTS(sys.argv)
|
||||
if "qtlupdate" in sysArgs:
|
||||
sysArgs.remove("qtlupdate")
|
||||
buildQtI18nTS(sysArgs)
|
||||
sys.exit(0) # Don't continue execution
|
||||
|
||||
if "sample" in sys.argv:
|
||||
sys.argv.remove("sample")
|
||||
if "sample" in sysArgs:
|
||||
sysArgs.remove("sample")
|
||||
buildSampleZip()
|
||||
|
||||
if "clean-assets" in sys.argv:
|
||||
sys.argv.remove("clean-assets")
|
||||
if "clean-assets" in sysArgs:
|
||||
sysArgs.remove("clean-assets")
|
||||
cleanBuiltAssets()
|
||||
|
||||
if "gen-plist" in sys.argv:
|
||||
sys.argv.remove("gen-plist")
|
||||
if "gen-plist" in sysArgs:
|
||||
sysArgs.remove("gen-plist")
|
||||
genMacOSPlist()
|
||||
|
||||
# Python Packaging
|
||||
# ================
|
||||
|
||||
if "import-i18n" in sys.argv:
|
||||
sys.argv.remove("import-i18n")
|
||||
importI18nUpdates(sys.argv)
|
||||
if "import-i18n" in sysArgs:
|
||||
sysArgs.remove("import-i18n")
|
||||
importI18nUpdates(sysArgs)
|
||||
sys.exit(0) # Don't continue execution
|
||||
|
||||
if "minimal-zip" in sys.argv:
|
||||
sys.argv.remove("minimal-zip")
|
||||
if "minimal-zip" in sysArgs:
|
||||
sysArgs.remove("minimal-zip")
|
||||
makeMinimalPackage(targetOS)
|
||||
|
||||
if "build-deb" in sys.argv:
|
||||
sys.argv.remove("build-deb")
|
||||
if "build-deb" in sysArgs:
|
||||
sysArgs.remove("build-deb")
|
||||
if hostOS == OS_LINUX:
|
||||
if doSign:
|
||||
signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08"
|
||||
@@ -1980,23 +1952,23 @@ if __name__ == "__main__":
|
||||
print("ERROR: Command 'build-deb' can only be used on Linux")
|
||||
sys.exit(1)
|
||||
|
||||
if "build-ubuntu" in sys.argv:
|
||||
sys.argv.remove("build-ubuntu")
|
||||
if "build-ubuntu" in sysArgs:
|
||||
sysArgs.remove("build-ubuntu")
|
||||
if hostOS == OS_LINUX:
|
||||
makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild, isSnapshot=isSnapshot)
|
||||
makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild)
|
||||
else:
|
||||
print("ERROR: Command 'build-ubuntu' can only be used on Linux")
|
||||
sys.exit(1)
|
||||
|
||||
if "build-win-exe" in sys.argv:
|
||||
sys.argv.remove("build-win-exe")
|
||||
makeWindowsEmbedded(sys.argv)
|
||||
if "build-win-exe" in sysArgs:
|
||||
sysArgs.remove("build-win-exe")
|
||||
makeWindowsEmbedded(sysArgs)
|
||||
sys.exit(0) # Don't continue execution
|
||||
|
||||
if "build-appimage" in sys.argv:
|
||||
sys.argv.remove("build-appimage")
|
||||
if "build-appimage" in sysArgs:
|
||||
sysArgs.remove("build-appimage")
|
||||
if hostOS == OS_LINUX:
|
||||
sys.argv = makeAppImage(sys.argv) # Build appimage and prune its args
|
||||
sysArgs = makeAppImage(sysArgs)
|
||||
else:
|
||||
print("ERROR: Command 'build-appimage' can only be used on Linux")
|
||||
sys.exit(1)
|
||||
@@ -2004,32 +1976,32 @@ if __name__ == "__main__":
|
||||
# General Installers
|
||||
# ==================
|
||||
|
||||
if "xdg-install" in sys.argv:
|
||||
sys.argv.remove("xdg-install")
|
||||
if "xdg-install" in sysArgs:
|
||||
sysArgs.remove("xdg-install")
|
||||
if hostOS == OS_WIN:
|
||||
print("ERROR: Command 'xdg-install' cannot be used on Windows")
|
||||
sys.exit(1)
|
||||
else:
|
||||
xdgInstall()
|
||||
|
||||
if "xdg-uninstall" in sys.argv:
|
||||
sys.argv.remove("xdg-uninstall")
|
||||
if "xdg-uninstall" in sysArgs:
|
||||
sysArgs.remove("xdg-uninstall")
|
||||
if hostOS == OS_WIN:
|
||||
print("ERROR: Command 'xdg-uninstall' cannot be used on Windows")
|
||||
sys.exit(1)
|
||||
else:
|
||||
xdgUninstall()
|
||||
|
||||
if "win-install" in sys.argv:
|
||||
sys.argv.remove("win-install")
|
||||
if "win-install" in sysArgs:
|
||||
sysArgs.remove("win-install")
|
||||
if hostOS == OS_WIN:
|
||||
winInstall()
|
||||
else:
|
||||
print("ERROR: Command 'win-install' can only be used on Windows")
|
||||
sys.exit(1)
|
||||
|
||||
if "win-uninstall" in sys.argv:
|
||||
sys.argv.remove("win-uninstall")
|
||||
if "win-uninstall" in sysArgs:
|
||||
sysArgs.remove("win-uninstall")
|
||||
if hostOS == OS_WIN:
|
||||
winUninstall()
|
||||
else:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?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">
|
||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1617" autoCount="255" editTime="81245">
|
||||
<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="1620" autoCount="255" editTime="81255">
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jane Smith</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -24,7 +24,7 @@ echo "Build Dir: $BUILD_DIR"
|
||||
|
||||
pushd "$SRC_DIR" || exit 1
|
||||
|
||||
VERSION="$(python3 pkgutils.py version -c)"
|
||||
VERSION="$(python3 pkgutils.py version)"
|
||||
echo "novelWriter Version: $VERSION"
|
||||
|
||||
# --- Prepare Files ----------------------------------------------------------------------------- #
|
||||
|
||||
@@ -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,8 +1,7 @@
|
||||
<?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">
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jane Smith</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?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">
|
||||
<name>New Project</name>
|
||||
<title>New Novel</title>
|
||||
<author>Jane Doe</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?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">
|
||||
<name>New Project</name>
|
||||
<title>New Novel</title>
|
||||
<author>Jane Doe</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?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">
|
||||
<name>New Project</name>
|
||||
<title>New Novel</title>
|
||||
<author>Jane Doe</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?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">
|
||||
<name>Test Project A</name>
|
||||
<title>Test Project A</title>
|
||||
<author>Jane Doe</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?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">
|
||||
<name>Test Project B</name>
|
||||
<title>Test Project B</title>
|
||||
<author>Jane Doe</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?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">
|
||||
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="4">
|
||||
<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="3">
|
||||
<name>New Project</name>
|
||||
<title>New Novel</title>
|
||||
<author>Jane Doe</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?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">
|
||||
<name>New Project</name>
|
||||
<title>New Novel</title>
|
||||
<author>Jane Doe</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"meta": {
|
||||
"projectName": "Lorem Ipsum",
|
||||
"novelTitle": "Lorem Ipsum",
|
||||
"novelAuthor": "lipsum.com",
|
||||
"buildTime": 1687620769,
|
||||
"buildTimeStr": "2023-06-24 17:32:49"
|
||||
"buildTime": 1706306020,
|
||||
"buildTimeStr": "2024-01-26 22:53:40"
|
||||
},
|
||||
"text": {
|
||||
"css": [
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"meta": {
|
||||
"projectName": "Lorem Ipsum",
|
||||
"novelTitle": "Lorem Ipsum",
|
||||
"novelAuthor": "lipsum.com",
|
||||
"buildTime": 1687621222,
|
||||
"buildTimeStr": "2023-06-24 17:40:22"
|
||||
"buildTime": 1706306020,
|
||||
"buildTimeStr": "2024-01-26 22:53:40"
|
||||
},
|
||||
"text": {
|
||||
"nwd": [
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?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">
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jay Doh</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?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">
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jay Doh</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?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">
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jay Doh</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?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">
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jay Doh</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?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">
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jay Doh</author>
|
||||
</project>
|
||||
<settings>
|
||||
|
||||
@@ -35,7 +35,7 @@ from PyQt5.QtCore import QUrl
|
||||
from novelwriter.common import (
|
||||
checkBool, checkFloat, checkHandle, checkInt, checkIntTuple, checkPath,
|
||||
checkString, checkStringNone, checkUuid, formatInt, formatTime,
|
||||
formatTimeStamp, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass,
|
||||
formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass,
|
||||
isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax,
|
||||
numberToRoman, NWConfigParser, openExternalPath, readTextFile, simplified,
|
||||
transferCase, xmlIndent, yesNo
|
||||
@@ -348,6 +348,17 @@ def 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
|
||||
def testBaseCommon_simplified():
|
||||
"""Test the simplified function."""
|
||||
|
||||
@@ -230,7 +230,7 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
|
||||
assert error == []
|
||||
|
||||
copyfile(docFile, tstFile)
|
||||
assert cmpFiles(tstFile, cmpFile, ignoreLines=[6, 7])
|
||||
assert cmpFiles(tstFile, cmpFile, ignoreLines=[5, 6])
|
||||
|
||||
# Check Error Handling
|
||||
# ====================
|
||||
@@ -370,7 +370,7 @@ def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
|
||||
assert error == []
|
||||
|
||||
copyfile(docFile, tstFile)
|
||||
assert cmpFiles(tstFile, cmpFile, ignoreLines=[6, 7])
|
||||
assert cmpFiles(tstFile, cmpFile, ignoreLines=[5, 6])
|
||||
|
||||
# Check Error Handling
|
||||
# ====================
|
||||
|
||||
@@ -695,6 +695,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
|
||||
"This is a story about Jane Smith.\n\n"
|
||||
"Well, not really. She's still awesome though.\n"
|
||||
))
|
||||
|
||||
# Whole document
|
||||
cC, wC, pC = index.getCounts(nHandle)
|
||||
assert cC == 152
|
||||
@@ -798,6 +799,10 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
|
||||
# Extract stats
|
||||
assert index.getNovelWordCount(activeOnly=False) == 43
|
||||
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=True) == [0, 1, 2, 3, 0]
|
||||
|
||||
|
||||
@@ -121,27 +121,27 @@ def testCoreOptions_SetGet(mockGUI):
|
||||
assert options.setValue("GuiProjectSettings", "winWidth", 100) is True
|
||||
|
||||
# Set some values of different types
|
||||
assert options.setValue("GuiProjectDetails", "winWidth", 100) is True
|
||||
assert options.setValue("GuiProjectDetails", "winHeight", 12.34) is True
|
||||
assert options.setValue("GuiProjectDetails", "clearDouble", True) is True
|
||||
assert options.setValue("GuiNovelDetails", "winWidth", 100) is True
|
||||
assert options.setValue("GuiNovelDetails", "winHeight", 12.34) is True
|
||||
assert options.setValue("GuiNovelDetails", "clearDouble", True) is True
|
||||
assert options.setValue("GuiNovelView", "lastCol", nwColHidden) is True
|
||||
|
||||
# Generic get, doesn't check type
|
||||
assert options.getValue("GuiProjectDetails", "winWidth", None) == 100
|
||||
assert options.getValue("GuiProjectDetails", "winHeight", None) == 12.34
|
||||
assert options.getValue("GuiProjectDetails", "clearDouble", None) is True
|
||||
assert options.getValue("GuiProjectDetails", "mockItem", None) is None
|
||||
assert options.getValue("GuiNovelDetails", "winWidth", None) == 100
|
||||
assert options.getValue("GuiNovelDetails", "winHeight", None) == 12.34
|
||||
assert options.getValue("GuiNovelDetails", "clearDouble", None) is True
|
||||
assert options.getValue("GuiNovelDetails", "mockItem", None) is None
|
||||
|
||||
# Get type-specific
|
||||
assert options.getString("GuiProjectDetails", "winWidth", None) is None # type: ignore
|
||||
assert options.getString("GuiProjectDetails", "mockItem", None) is None # type: ignore
|
||||
assert options.getInt("GuiProjectDetails", "winWidth", None) == 100 # type: ignore
|
||||
assert options.getInt("GuiProjectDetails", "textFont", None) is None # type: ignore
|
||||
assert options.getInt("GuiProjectDetails", "mockItem", None) is None # type: ignore
|
||||
assert options.getFloat("GuiProjectDetails", "winWidth", None) == 100.0 # type: ignore
|
||||
assert options.getFloat("GuiProjectDetails", "mockItem", None) is None # type: ignore
|
||||
assert options.getBool("GuiProjectDetails", "clearDouble", None) is True # type: ignore
|
||||
assert options.getBool("GuiProjectDetails", "mockItem", None) is None # type: ignore
|
||||
assert options.getString("GuiNovelDetails", "winWidth", None) is None # type: ignore
|
||||
assert options.getString("GuiNovelDetails", "mockItem", None) is None # type: ignore
|
||||
assert options.getInt("GuiNovelDetails", "winWidth", None) == 100 # type: ignore
|
||||
assert options.getInt("GuiNovelDetails", "textFont", None) is None # type: ignore
|
||||
assert options.getInt("GuiNovelDetails", "mockItem", None) is None # type: ignore
|
||||
assert options.getFloat("GuiNovelDetails", "winWidth", None) == 100.0 # type: ignore
|
||||
assert options.getFloat("GuiNovelDetails", "mockItem", None) is None # type: ignore
|
||||
assert options.getBool("GuiNovelDetails", "clearDouble", None) is True # type: ignore
|
||||
assert options.getBool("GuiNovelDetails", "mockItem", None) is None # type: ignore
|
||||
assert options.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, nwColHidden) == nwColHidden
|
||||
|
||||
# Get from non-existent groups
|
||||
|
||||
@@ -482,10 +482,6 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||
project.data.setName(" A Name ")
|
||||
assert project.data.name == "A Name"
|
||||
|
||||
# Project Title
|
||||
project.data.setTitle(" A Title ")
|
||||
assert project.data.title == "A Title"
|
||||
|
||||
# Project Author
|
||||
project.data.setAuthor(" Jane\tDoe ")
|
||||
assert project.data.author == "Jane Doe"
|
||||
|
||||
@@ -131,13 +131,12 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
|
||||
assert xmlReader.state == XMLReadState.PARSED_OK
|
||||
assert xmlReader.xmlRoot == "novelWriterXML"
|
||||
assert xmlReader.xmlVersion == 0x0105
|
||||
assert xmlReader.xmlRevision == 1
|
||||
assert xmlReader.xmlRevision == 2
|
||||
assert xmlReader.appVersion == "2.0-rc1"
|
||||
assert xmlReader.hexVersion == 0x020000c1
|
||||
|
||||
# Check loaded data
|
||||
assert data.name == "Sample Project"
|
||||
assert data.title == "Sample Project"
|
||||
assert data.author == "Jane Smith"
|
||||
assert data.saveCount == 5
|
||||
assert data.autoCount == 10
|
||||
@@ -256,7 +255,6 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd):
|
||||
|
||||
# Check loaded data
|
||||
assert data.name == "Sample Project"
|
||||
assert data.title == "Sample Project"
|
||||
assert data.author == "Jay Doh" # Only last author is preserved
|
||||
assert data.saveCount == 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
|
||||
assert data.name == "Sample Project"
|
||||
assert data.title == "Sample Project"
|
||||
assert data.author == "Jay Doh" # Only last author is preserved
|
||||
assert data.saveCount == 5
|
||||
assert data.autoCount == 10
|
||||
@@ -526,7 +523,6 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd):
|
||||
|
||||
# Check loaded data
|
||||
assert data.name == "Sample Project"
|
||||
assert data.title == "Sample Project"
|
||||
assert data.author == "Jay Doh" # Only last author is preserved
|
||||
assert data.saveCount == 5
|
||||
assert data.autoCount == 10
|
||||
@@ -664,7 +660,6 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd):
|
||||
|
||||
# Check loaded data
|
||||
assert data.name == "Sample Project"
|
||||
assert data.title == "Sample Project"
|
||||
assert data.author == "Jay Doh" # Only last author is preserved
|
||||
assert data.saveCount == 5
|
||||
assert data.autoCount == 10
|
||||
@@ -802,7 +797,6 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd):
|
||||
|
||||
# Check loaded data
|
||||
assert data.name == "Sample Project"
|
||||
assert data.title == "Sample Project"
|
||||
assert data.author == "Jay Doh" # Only last author is preserved
|
||||
assert data.saveCount == 5
|
||||
assert data.autoCount == 10
|
||||
|
||||
@@ -22,9 +22,11 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from tools import getGuiItem
|
||||
|
||||
from PyQt5.QtGui import QFontDatabase, QKeyEvent
|
||||
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.constants import nwConst, nwUnicode
|
||||
@@ -38,9 +40,13 @@ KEY_DELAY = 1
|
||||
def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths):
|
||||
"""Test the preferences dialog loading."""
|
||||
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")])
|
||||
monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None)
|
||||
|
||||
# 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()
|
||||
|
||||
# Check Languages
|
||||
@@ -95,10 +101,6 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
|
||||
# Check Navigation
|
||||
vBar = prefs.mainForm.verticalScrollBar()
|
||||
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:
|
||||
prefs.sidebar.button(1).click()
|
||||
assert value.args[0] > old
|
||||
@@ -107,6 +109,10 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
|
||||
prefs.sidebar.button(2).click()
|
||||
assert value.args[0] > old
|
||||
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
|
||||
prefs.searchText.setText("Display language")
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
"""
|
||||
novelWriter – Project Details Dialog Class Tester
|
||||
=================================================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, 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
|
||||
+135
-150
@@ -31,7 +31,7 @@ from PyQt5.QtWidgets import QDialog, QAction, QColorDialog
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.enum import nwItemType
|
||||
from novelwriter.dialogs.editlabel import GuiEditLabel
|
||||
from novelwriter.dialogs.projsettings import GuiProjectSettings
|
||||
from novelwriter.dialogs.projectsettings import GuiProjectSettings
|
||||
|
||||
KEY_DELAY = 1
|
||||
|
||||
@@ -63,17 +63,17 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI):
|
||||
qtbot.addWidget(projSettings)
|
||||
|
||||
# Switch Tabs
|
||||
projSettings._focusTab(GuiProjectSettings.TAB_REPLACE)
|
||||
assert projSettings._tabBox.currentWidget() == projSettings.tabReplace
|
||||
projSettings.sidebar.button(GuiProjectSettings.PAGE_SETTINGS).click()
|
||||
assert projSettings.mainStack.currentWidget() == projSettings.settingsPage
|
||||
|
||||
projSettings._focusTab(GuiProjectSettings.TAB_IMPORT)
|
||||
assert projSettings._tabBox.currentWidget() == projSettings.tabImport
|
||||
projSettings.sidebar.button(GuiProjectSettings.PAGE_STATUS).click()
|
||||
assert projSettings.mainStack.currentWidget() == projSettings.statusPage
|
||||
|
||||
projSettings._focusTab(GuiProjectSettings.TAB_STATUS)
|
||||
assert projSettings._tabBox.currentWidget() == projSettings.tabStatus
|
||||
projSettings.sidebar.button(GuiProjectSettings.PAGE_IMPORT).click()
|
||||
assert projSettings.mainStack.currentWidget() == projSettings.importPage
|
||||
|
||||
projSettings._focusTab(GuiProjectSettings.TAB_MAIN)
|
||||
assert projSettings._tabBox.currentWidget() == projSettings.tabMain
|
||||
projSettings.sidebar.button(GuiProjectSettings.PAGE_REPLACE).click()
|
||||
assert projSettings.mainStack.currentWidget() == projSettings.replacePage
|
||||
|
||||
# Clean Up
|
||||
projSettings.close()
|
||||
@@ -83,9 +83,11 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI):
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
|
||||
"""Test the main tab of the project settings dialog."""
|
||||
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")])
|
||||
def testDlgProjSettings_SettingsPage(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
|
||||
"""Test the settings page of the dialog."""
|
||||
languages = [("en", "English"), ("de", "German")]
|
||||
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda *a: languages)
|
||||
monkeypatch.setattr(CONFIG, "listLanguages", lambda *a: languages)
|
||||
|
||||
# Create new project
|
||||
buildTestProject(nwGUI, projPath)
|
||||
@@ -94,72 +96,57 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockR
|
||||
|
||||
# Set some values
|
||||
project = SHARED.project
|
||||
project.data.setLanguage("en")
|
||||
project.data.setSpellLang("en")
|
||||
project.data.setAuthor("Jane Smith")
|
||||
project.data.setAutoReplace({"A": "B", "C": "D"})
|
||||
|
||||
# Create Dialog
|
||||
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_MAIN)
|
||||
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_SETTINGS)
|
||||
projSettings.show()
|
||||
qtbot.addWidget(projSettings)
|
||||
|
||||
# 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"
|
||||
assert tabMain.editTitle.text() == "New Novel"
|
||||
assert tabMain.editAuthor.text() == "Jane Smith"
|
||||
assert tabMain.spellLang.currentData() == "en"
|
||||
assert tabMain.doBackup.isChecked() is False
|
||||
|
||||
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"
|
||||
settings.projName.setText("Project Name")
|
||||
settings.projAuthor.setText("Jane Doe")
|
||||
settings.projLang.setCurrentIndex(settings.projLang.findData("de"))
|
||||
settings.spellLang.setCurrentIndex(settings.spellLang.findData("de"))
|
||||
settings.doBackup.setChecked(True)
|
||||
|
||||
projSettings._doSave()
|
||||
assert project.data.name == "Project Name"
|
||||
assert project.data.title == "Project Title"
|
||||
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"
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
# END Test testDlgProjSettings_Main
|
||||
# END Test testDlgProjSettings_SettingsPage
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
|
||||
"""Test the status and importance tabs of the project settings
|
||||
dialog.
|
||||
"""
|
||||
def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
||||
"""Test the status and importance pages of the dialog."""
|
||||
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)
|
||||
CONFIG.setBackupPath(fncPath)
|
||||
|
||||
# Set some values
|
||||
theProject = SHARED.project
|
||||
theProject.tree[C.hTitlePage].setStatus(C.sFinished) # type: ignore
|
||||
theProject.tree[C.hChapterDoc].setStatus(C.sDraft) # type: ignore
|
||||
theProject.tree[C.hSceneDoc].setStatus(C.sDraft) # type: ignore
|
||||
project = SHARED.project
|
||||
project.tree[C.hTitlePage].setStatus(C.sFinished) # type: ignore
|
||||
project.tree[C.hChapterDoc].setStatus(C.sDraft) # type: ignore
|
||||
project.tree[C.hSceneDoc].setStatus(C.sDraft) # type: ignore
|
||||
|
||||
nwGUI.projView.projTree.setSelectedHandle(C.hPlotRoot)
|
||||
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
|
||||
@@ -172,51 +159,54 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
|
||||
hCharNote = "0000000000011"
|
||||
hWorldNote = "0000000000012"
|
||||
|
||||
theProject.tree[hPlotNote].setImport(C.iMajor) # type: ignore
|
||||
theProject.tree[hCharNote].setImport(C.iMajor) # type: ignore
|
||||
theProject.tree[hWorldNote].setImport(C.iMain) # type: ignore
|
||||
project.tree[hPlotNote].setImport(C.iMajor) # type: ignore
|
||||
project.tree[hCharNote].setImport(C.iMajor) # 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
|
||||
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_STATUS)
|
||||
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_STATUS)
|
||||
projSettings.show()
|
||||
qtbot.addWidget(projSettings)
|
||||
|
||||
# Status Tab
|
||||
# ==========
|
||||
|
||||
tabStatus = projSettings.tabStatus
|
||||
status = projSettings.statusPage
|
||||
|
||||
assert tabStatus.colChanged is False
|
||||
assert tabStatus.getNewList() == ([], [])
|
||||
assert tabStatus.listBox.topLevelItemCount() == 4
|
||||
assert status.wasChanged is False
|
||||
assert status.getNewList() == ([], [])
|
||||
assert status.listBox.topLevelItemCount() == 4
|
||||
|
||||
# Can't delete the first item (it's in use)
|
||||
tabStatus.listBox.clearSelection()
|
||||
tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(0))
|
||||
qtbot.mouseClick(tabStatus.delButton, Qt.LeftButton)
|
||||
assert tabStatus.listBox.topLevelItemCount() == 4
|
||||
status.listBox.clearSelection()
|
||||
status.listBox.setCurrentItem(status.listBox.topLevelItem(0))
|
||||
qtbot.mouseClick(status.delButton, Qt.LeftButton)
|
||||
assert status.listBox.topLevelItemCount() == 4
|
||||
|
||||
# Can delete the second item
|
||||
tabStatus.listBox.clearSelection()
|
||||
tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(1))
|
||||
qtbot.mouseClick(tabStatus.delButton, Qt.LeftButton)
|
||||
assert tabStatus.listBox.topLevelItemCount() == 3
|
||||
status.listBox.clearSelection()
|
||||
status.listBox.setCurrentItem(status.listBox.topLevelItem(1))
|
||||
qtbot.mouseClick(status.delButton, Qt.LeftButton)
|
||||
assert status.listBox.topLevelItemCount() == 3
|
||||
|
||||
# Add a new item
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40))
|
||||
qtbot.mouseClick(tabStatus.addButton, Qt.LeftButton)
|
||||
tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(3))
|
||||
for _ in range(8):
|
||||
qtbot.keyClick(tabStatus.editName, Qt.Key_Backspace, delay=KEY_DELAY)
|
||||
for c in "Final":
|
||||
qtbot.keyClick(tabStatus.editName, c, delay=KEY_DELAY)
|
||||
qtbot.mouseClick(tabStatus.colButton, Qt.LeftButton)
|
||||
qtbot.mouseClick(tabStatus.saveButton, Qt.LeftButton)
|
||||
assert tabStatus.listBox.topLevelItemCount() == 4
|
||||
status.addButton.click()
|
||||
status.listBox.setCurrentItem(status.listBox.topLevelItem(3))
|
||||
status.editName.setText("Final")
|
||||
status.colButton.click()
|
||||
status.saveButton.click()
|
||||
assert status.listBox.topLevelItemCount() == 4
|
||||
|
||||
assert tabStatus.colChanged is True
|
||||
assert tabStatus.getNewList() == (
|
||||
assert status.wasChanged is True
|
||||
assert status.getNewList() == (
|
||||
[
|
||||
{
|
||||
"key": C.sNew,
|
||||
@@ -241,62 +231,62 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
|
||||
)
|
||||
|
||||
# Move items, none selected -> no change
|
||||
tabStatus.listBox.clearSelection()
|
||||
tabStatus._moveItem(1)
|
||||
assert [x["key"] for x in tabStatus.getNewList()[0]] == [
|
||||
status.listBox.clearSelection()
|
||||
status._moveItem(1)
|
||||
assert [x["key"] for x in status.getNewList()[0]] == [
|
||||
C.sNew, C.sDraft, C.sFinished, None
|
||||
]
|
||||
|
||||
# Move items, first selected, move up -> no change
|
||||
tabStatus.listBox.clearSelection()
|
||||
tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(0))
|
||||
tabStatus._moveItem(-1)
|
||||
assert [x["key"] for x in tabStatus.getNewList()[0]] == [
|
||||
status.listBox.clearSelection()
|
||||
status.listBox.setCurrentItem(status.listBox.topLevelItem(0))
|
||||
status._moveItem(-1)
|
||||
assert [x["key"] for x in status.getNewList()[0]] == [
|
||||
C.sNew, C.sDraft, C.sFinished, None
|
||||
]
|
||||
|
||||
# Move items, last selected, move up -> allowed
|
||||
tabStatus.listBox.clearSelection()
|
||||
tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(3))
|
||||
tabStatus._moveItem(-1)
|
||||
assert [x["key"] for x in tabStatus.getNewList()[0]] == [
|
||||
status.listBox.clearSelection()
|
||||
status.listBox.setCurrentItem(status.listBox.topLevelItem(3))
|
||||
status._moveItem(-1)
|
||||
assert [x["key"] for x in status.getNewList()[0]] == [
|
||||
C.sNew, C.sDraft, None, C.sFinished
|
||||
]
|
||||
|
||||
# Move items, same selected, move down -> allowed
|
||||
tabStatus._moveItem(1)
|
||||
assert [x["key"] for x in tabStatus.getNewList()[0]] == [
|
||||
status._moveItem(1)
|
||||
assert [x["key"] for x in status.getNewList()[0]] == [
|
||||
C.sNew, C.sDraft, C.sFinished, None
|
||||
]
|
||||
|
||||
# Importance Tab
|
||||
# ==============
|
||||
|
||||
tabImport = projSettings.tabImport
|
||||
projSettings._focusTab(GuiProjectSettings.TAB_IMPORT)
|
||||
importance = projSettings.importPage
|
||||
projSettings._sidebarClicked(GuiProjectSettings.PAGE_IMPORT)
|
||||
|
||||
# Delete unused entry
|
||||
tabImport.listBox.clearSelection()
|
||||
tabImport.listBox.setCurrentItem(tabImport.listBox.topLevelItem(1))
|
||||
qtbot.mouseClick(tabImport.delButton, Qt.LeftButton)
|
||||
assert tabImport.listBox.topLevelItemCount() == 3
|
||||
importance.listBox.clearSelection()
|
||||
importance.listBox.setCurrentItem(importance.listBox.topLevelItem(1))
|
||||
qtbot.mouseClick(importance.delButton, Qt.LeftButton)
|
||||
assert importance.listBox.topLevelItemCount() == 3
|
||||
|
||||
# Add a new entry
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40))
|
||||
qtbot.mouseClick(tabImport.addButton, Qt.LeftButton)
|
||||
tabImport.listBox.clearSelection()
|
||||
tabImport.listBox.setCurrentItem(tabImport.listBox.topLevelItem(3))
|
||||
qtbot.mouseClick(importance.addButton, Qt.LeftButton)
|
||||
importance.listBox.clearSelection()
|
||||
importance.listBox.setCurrentItem(importance.listBox.topLevelItem(3))
|
||||
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":
|
||||
qtbot.keyClick(tabImport.editName, c, delay=KEY_DELAY)
|
||||
qtbot.mouseClick(tabImport.colButton, Qt.LeftButton)
|
||||
qtbot.mouseClick(tabImport.saveButton, Qt.LeftButton)
|
||||
assert tabImport.listBox.topLevelItemCount() == 4
|
||||
qtbot.keyClick(importance.editName, c, delay=KEY_DELAY)
|
||||
qtbot.mouseClick(importance.colButton, Qt.LeftButton)
|
||||
qtbot.mouseClick(importance.saveButton, Qt.LeftButton)
|
||||
assert importance.listBox.topLevelItemCount() == 4
|
||||
|
||||
assert tabImport.colChanged is True
|
||||
assert tabImport.getNewList() == (
|
||||
assert importance.wasChanged is True
|
||||
assert importance.getNewList() == (
|
||||
[
|
||||
{
|
||||
"key": C.iNew,
|
||||
@@ -323,13 +313,13 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
|
||||
# Check Project
|
||||
projSettings._doSave()
|
||||
|
||||
statusItems = dict(theProject.data.itemStatus.items())
|
||||
statusItems = dict(project.data.itemStatus.items())
|
||||
assert statusItems[C.sNew]["name"] == "New"
|
||||
assert statusItems[C.sDraft]["name"] == "Draft"
|
||||
assert statusItems[C.sFinished]["name"] == "Finished"
|
||||
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.iMajor]["name"] == "Major"
|
||||
assert importItems[C.iMain]["name"] == "Main"
|
||||
@@ -341,83 +331,78 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
|
||||
"""Test the auto-replace tab of the project settings dialog."""
|
||||
def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
||||
"""Test the auto-replace page of the dialog."""
|
||||
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)
|
||||
CONFIG.setBackupPath(fncPath)
|
||||
|
||||
# Set some values
|
||||
theProject = SHARED.project
|
||||
theProject.data.setAutoReplace({
|
||||
project = SHARED.project
|
||||
project.data.setAutoReplace({
|
||||
"A": "B", "C": "D"
|
||||
})
|
||||
|
||||
# Create Dialog
|
||||
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_REPLACE)
|
||||
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_REPLACE)
|
||||
projSettings.show()
|
||||
qtbot.addWidget(projSettings)
|
||||
|
||||
# Auto-Replace Tab
|
||||
# ================
|
||||
|
||||
tabReplace = projSettings.tabReplace
|
||||
replace = projSettings.replacePage
|
||||
|
||||
assert tabReplace.listBox.topLevelItem(0).text(0) == "<A>" # type: ignore
|
||||
assert tabReplace.listBox.topLevelItem(0).text(1) == "B" # type: ignore
|
||||
assert tabReplace.listBox.topLevelItem(1).text(0) == "<C>" # type: ignore
|
||||
assert tabReplace.listBox.topLevelItem(1).text(1) == "D" # type: ignore
|
||||
assert tabReplace.listBox.topLevelItemCount() == 2
|
||||
assert replace.listBox.topLevelItem(0).text(0) == "<A>" # type: ignore
|
||||
assert replace.listBox.topLevelItem(0).text(1) == "B" # type: ignore
|
||||
assert replace.listBox.topLevelItem(1).text(0) == "<C>" # type: ignore
|
||||
assert replace.listBox.topLevelItem(1).text(1) == "D" # type: ignore
|
||||
assert replace.listBox.topLevelItemCount() == 2
|
||||
|
||||
# Nothing to save or delete
|
||||
tabReplace.listBox.clearSelection()
|
||||
tabReplace._saveEntry()
|
||||
tabReplace._delEntry()
|
||||
assert tabReplace.listBox.topLevelItemCount() == 2
|
||||
replace.listBox.clearSelection()
|
||||
replace._saveEntry()
|
||||
replace._delEntry()
|
||||
assert replace.listBox.topLevelItemCount() == 2
|
||||
|
||||
# Create a new entry
|
||||
qtbot.mouseClick(tabReplace.addButton, Qt.LeftButton)
|
||||
assert tabReplace.listBox.topLevelItemCount() == 3
|
||||
assert tabReplace.listBox.topLevelItem(2).text(0) == "<keyword3>" # type: ignore
|
||||
assert tabReplace.listBox.topLevelItem(2).text(1) == "" # type: ignore
|
||||
qtbot.mouseClick(replace.addButton, Qt.LeftButton)
|
||||
assert replace.listBox.topLevelItemCount() == 3
|
||||
assert replace.listBox.topLevelItem(2).text(0) == "<keyword3>" # type: ignore
|
||||
assert replace.listBox.topLevelItem(2).text(1) == "" # type: ignore
|
||||
|
||||
# Edit the entry
|
||||
tabReplace.listBox.setCurrentItem(tabReplace.listBox.topLevelItem(2))
|
||||
tabReplace.editKey.setText("")
|
||||
replace.listBox.setCurrentItem(replace.listBox.topLevelItem(2))
|
||||
replace.editKey.setText("")
|
||||
for c in "Th is ":
|
||||
qtbot.keyClick(tabReplace.editKey, c, delay=KEY_DELAY)
|
||||
tabReplace.editValue.setText("")
|
||||
qtbot.keyClick(replace.editKey, c, delay=KEY_DELAY)
|
||||
replace.editValue.setText("")
|
||||
for c in "With This Stuff ":
|
||||
qtbot.keyClick(tabReplace.editValue, c, delay=KEY_DELAY)
|
||||
qtbot.mouseClick(tabReplace.saveButton, Qt.LeftButton)
|
||||
assert tabReplace.listBox.topLevelItem(2).text(0) == "<This>" # type: ignore
|
||||
assert tabReplace.listBox.topLevelItem(2).text(1) == "With This Stuff " # type: ignore
|
||||
qtbot.keyClick(replace.editValue, c, delay=KEY_DELAY)
|
||||
qtbot.mouseClick(replace.saveButton, Qt.LeftButton)
|
||||
assert replace.listBox.topLevelItem(2).text(0) == "<This>" # type: ignore
|
||||
assert replace.listBox.topLevelItem(2).text(1) == "With This Stuff " # type: ignore
|
||||
|
||||
# Create a new entry again
|
||||
tabReplace.listBox.clearSelection()
|
||||
qtbot.mouseClick(tabReplace.addButton, Qt.LeftButton)
|
||||
assert tabReplace.listBox.topLevelItemCount() == 4
|
||||
replace.listBox.clearSelection()
|
||||
qtbot.mouseClick(replace.addButton, Qt.LeftButton)
|
||||
assert replace.listBox.topLevelItemCount() == 4
|
||||
|
||||
# The list is sorted, so we must find it
|
||||
newIdx = -1
|
||||
for i in range(tabReplace.listBox.topLevelItemCount()):
|
||||
if tabReplace.listBox.topLevelItem(i).text(0) == "<keyword4>": # type: ignore
|
||||
for i in range(replace.listBox.topLevelItemCount()):
|
||||
if replace.listBox.topLevelItem(i).text(0) == "<keyword4>": # type: ignore
|
||||
newIdx = i
|
||||
break
|
||||
assert newIdx >= 0
|
||||
|
||||
# Then delete the new item
|
||||
tabReplace.listBox.setCurrentItem(tabReplace.listBox.topLevelItem(newIdx))
|
||||
qtbot.mouseClick(tabReplace.delButton, Qt.LeftButton)
|
||||
assert tabReplace.listBox.topLevelItemCount() == 3
|
||||
replace.listBox.setCurrentItem(replace.listBox.topLevelItem(newIdx))
|
||||
qtbot.mouseClick(replace.delButton, Qt.LeftButton)
|
||||
assert replace.listBox.topLevelItemCount() == 3
|
||||
|
||||
# Check Project
|
||||
projSettings._doSave()
|
||||
assert theProject.data.autoReplace == {
|
||||
assert project.data.autoReplace == {
|
||||
"A": "B", "C": "D", "This": "With This Stuff"
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ from tools import (
|
||||
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.QtWidgets import QMenu, QInputDialog
|
||||
|
||||
@@ -170,7 +170,7 @@ def testGuiMain_UpdateTheme(qtbot, nwGUI):
|
||||
mainTheme.loadSyntax()
|
||||
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.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 SHARED.project.tree.trashRoot is None
|
||||
assert SHARED.project.data.name == ""
|
||||
assert SHARED.project.data.title == ""
|
||||
assert SHARED.project.data.author == ""
|
||||
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 SHARED.project.tree.trashRoot is None
|
||||
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.spellCheck is False
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from pathlib import Path
|
||||
from mocked import causeOSError
|
||||
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 novelwriter import CONFIG, SHARED
|
||||
@@ -89,29 +89,38 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
|
||||
|
||||
parser = NWConfigParser()
|
||||
parser["Palette"] = {
|
||||
"colour1": "100, 150, 200",
|
||||
"colour2": "100, 150, 200, 250",
|
||||
"colour3": "250, 250",
|
||||
"colour4": "-10, 127, 300",
|
||||
"colour1": "100, 150, 200", # Valid
|
||||
"colour2": "100, 150, 200, 250", # With alpha
|
||||
"colour3": "100, 150, 200, 250, 300", # Too many values
|
||||
"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
|
||||
assert mainTheme._parseColour(parser, "Palette", "colour1") == [100, 150, 200]
|
||||
assert mainTheme._parseColour(parser, "Palette", "colour2") == [100, 150, 200]
|
||||
assert mainTheme._parseColour(parser, "Palette", "colour3") == [0, 0, 0]
|
||||
assert mainTheme._parseColour(parser, "Palette", "colour4") == [0, 127, 255]
|
||||
assert mainTheme._parseColour(parser, "Palette", "colour5") == [0, 0, 0]
|
||||
assert mainTheme._parseColour(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255)
|
||||
assert mainTheme._parseColour(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250)
|
||||
assert mainTheme._parseColour(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250)
|
||||
assert mainTheme._parseColour(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255)
|
||||
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
|
||||
mainTheme._setPalette(parser, "Palette", "colour1", QPalette.Window)
|
||||
assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 255)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
# qtbot.stop()
|
||||
@@ -240,9 +249,9 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
|
||||
|
||||
# Check some values
|
||||
assert mainTheme.syntaxName == "Default Light"
|
||||
assert mainTheme.colBack == [255, 255, 255]
|
||||
assert mainTheme.colText == [0, 0, 0]
|
||||
assert mainTheme.colLink == [0, 0, 200]
|
||||
assert mainTheme.colBack == QColor(255, 255, 255)
|
||||
assert mainTheme.colText == QColor(0, 0, 0)
|
||||
assert mainTheme.colLink == QColor(0, 0, 200)
|
||||
|
||||
# Load Default Dark Theme
|
||||
# =======================
|
||||
@@ -253,9 +262,9 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
|
||||
|
||||
# Check some values
|
||||
assert mainTheme.syntaxName == "Default Dark"
|
||||
assert mainTheme.colBack == [54, 54, 54]
|
||||
assert mainTheme.colText == [199, 207, 208]
|
||||
assert mainTheme.colLink == [184, 200, 0]
|
||||
assert mainTheme.colBack == QColor(54, 54, 54)
|
||||
assert mainTheme.colText == QColor(199, 207, 208)
|
||||
assert mainTheme.colLink == QColor(184, 200, 0)
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
novelWriter – Novel Details Tool Tester
|
||||
=======================================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, 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
@@ -156,52 +156,54 @@ def cleanProject(path: str | Path):
|
||||
return
|
||||
|
||||
|
||||
def buildTestProject(obj, projPath):
|
||||
def buildTestProject(obj: object, projPath: Path) -> None:
|
||||
"""Build a standard test project in projPath using the project
|
||||
object as the parent.
|
||||
"""
|
||||
from novelwriter.enum import nwItemClass
|
||||
from novelwriter.guimain import GuiMain
|
||||
from novelwriter.core.project import NWProject
|
||||
|
||||
if isinstance(obj, NWProject):
|
||||
nwGUI = None
|
||||
project = obj
|
||||
else:
|
||||
elif isinstance(obj, GuiMain):
|
||||
from novelwriter import SHARED
|
||||
nwGUI = obj
|
||||
project = SHARED.project
|
||||
else:
|
||||
return
|
||||
|
||||
project.storage.createNewProject(projPath)
|
||||
project.setDefaultStatusImport()
|
||||
|
||||
project.data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
|
||||
project.data.setName("New Project")
|
||||
project.data.setTitle("New Novel")
|
||||
project.data.setAuthor("Jane Doe")
|
||||
|
||||
# Creating a minimal project with a few root folders and a
|
||||
# single chapter folder with a single file.
|
||||
xHandle = {}
|
||||
xHandle[1] = project.newRoot(nwItemClass.NOVEL, "Novel")
|
||||
xHandle[2] = project.newRoot(nwItemClass.PLOT, "Plot")
|
||||
xHandle[3] = project.newRoot(nwItemClass.CHARACTER, "Characters")
|
||||
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])
|
||||
nrHandle = project.newRoot(nwItemClass.NOVEL, "Novel")
|
||||
project.newRoot(nwItemClass.PLOT, "Plot")
|
||||
project.newRoot(nwItemClass.CHARACTER, "Characters")
|
||||
project.newRoot(nwItemClass.WORLD, "World")
|
||||
|
||||
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")
|
||||
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"))
|
||||
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"))
|
||||
project.index.reIndexHandle(xHandle[8])
|
||||
project.index.reIndexHandle(sdHandle)
|
||||
|
||||
project.session.startSession()
|
||||
project.setProjectChanged(True)
|
||||
|
||||
Reference in New Issue
Block a user