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