Merge branch 'dev' into project_handling

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