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
+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))