From ce2ac9b1a14f476c169f1bd8104139b292cc56dc Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 18 Jan 2024 23:34:27 +0100
Subject: [PATCH 1/9] Add a novel info dialog
---
novelwriter/gui/mainmenu.py | 6 ++-
novelwriter/guimain.py | 13 +++++++
novelwriter/tools/novelinfo.py | 71 ++++++++++++++++++++++++++++++++++
novelwriter/tools/welcome.py | 8 +---
4 files changed, 91 insertions(+), 7 deletions(-)
create mode 100644 novelwriter/tools/novelinfo.py
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index 189cd72d..94cd1d20 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -157,9 +157,13 @@ class GuiMainMenu(QMenuBar):
# 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 Info
+ self.aNovelInfo = self.projMenu.addAction(self.tr("Novel Info"))
+ self.aNovelInfo.setShortcut("Shift+F6")
+ self.aNovelInfo.triggered.connect(self.mainGui.showNovelInfoDialog)
+
# Project > Separator
self.projMenu.addSeparator()
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 6717351a..fea65021 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -56,6 +56,7 @@ from novelwriter.dialogs.preferences import GuiPreferences
from novelwriter.dialogs.projdetails import GuiProjectDetails
from novelwriter.dialogs.projsettings import GuiProjectSettings
from novelwriter.tools.welcome import GuiWelcome
+from novelwriter.tools.novelinfo import GuiNovelInfo
from novelwriter.tools.manuscript import GuiManuscript
from novelwriter.tools.dictionaries import GuiDictionaries
from novelwriter.tools.writingstats import GuiWritingStats
@@ -836,6 +837,18 @@ class GuiMain(QMainWindow):
dialog.updateValues()
return
+ @pyqtSlot()
+ def showNovelInfoDialog(self) -> None:
+ """Open the novel info dialog."""
+ if SHARED.hasProject:
+ dialog = GuiNovelInfo(self)
+ dialog.setModal(True)
+ dialog.show()
+ dialog.raise_()
+ qApp.processEvents()
+ # dialog.updateValues()
+ return
+
@pyqtSlot()
def showBuildManuscriptDialog(self) -> None:
"""Open the build manuscript dialog."""
diff --git a/novelwriter/tools/novelinfo.py b/novelwriter/tools/novelinfo.py
new file mode 100644
index 00000000..611c80b3
--- /dev/null
+++ b/novelwriter/tools/novelinfo.py
@@ -0,0 +1,71 @@
+"""
+novelWriter – GUI Novel Info
+============================
+
+File History:
+Created: 2024-01-18 [2.3b1] GuiNovelInfo
+
+This file is a part of novelWriter
+Copyright 2018–2024, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+from __future__ import annotations
+
+import logging
+
+from PyQt5.QtGui import QCloseEvent
+from PyQt5.QtWidgets import QDialog, QWidget
+
+logger = logging.getLogger(__name__)
+
+
+class GuiNovelInfo(QDialog):
+
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
+
+ logger.debug("Create: GuiNovelInfo")
+ self.setObjectName("GuiNovelInfo")
+
+ self.setWindowTitle(self.tr("Novel Info"))
+ logger.debug("Ready: GuiNovelInfo")
+
+ return
+
+ def __del__(self) -> None: # pragma: no cover
+ logger.debug("Delete: GuiNovelInfo")
+ return
+
+ ##
+ # Events
+ ##
+
+ def closeEvent(self, event: QCloseEvent) -> None:
+ """Capture the user closing the window and save settings."""
+ self._saveSettings()
+ event.accept()
+ self.deleteLater()
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _saveSettings(self) -> None:
+ """Save the user GUI settings."""
+ logger.debug("Saving State: GuiNovelInfo")
+ return
+
+# END Class GuiNovelInfo
diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py
index d136cec6..4a7d65e4 100644
--- a/novelwriter/tools/welcome.py
+++ b/novelwriter/tools/welcome.py
@@ -25,7 +25,6 @@ from __future__ import annotations
import logging
-from typing import TYPE_CHECKING
from pathlib import Path
from datetime import datetime
@@ -48,9 +47,6 @@ 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")
From 9f2c5379b75fd94707090a21c1fc8a809fb91617 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 22 Jan 2024 23:55:37 +0100
Subject: [PATCH 2/9] Generalise the custom help label widget
---
novelwriter/dialogs/docmerge.py | 9 +++----
novelwriter/dialogs/docsplit.py | 6 ++---
novelwriter/dialogs/preferences.py | 18 +++++---------
novelwriter/extensions/configlayout.py | 33 +++++++++++++++++---------
4 files changed, 36 insertions(+), 30 deletions(-)
diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py
index 15b54660..a0c43611 100644
--- a/novelwriter/dialogs/docmerge.py
+++ b/novelwriter/dialogs/docmerge.py
@@ -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("{0}".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)
diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py
index 602b860c..b8add4c0 100644
--- a/novelwriter/dialogs/docsplit.py
+++ b/novelwriter/dialogs/docsplit.py
@@ -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("{0}".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
diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py
index 32769451..41663a42 100644
--- a/novelwriter/dialogs/preferences.py
+++ b/novelwriter/dialogs/preferences.py
@@ -26,11 +26,11 @@ from __future__ import annotations
import logging
-from PyQt5.QtGui import QCloseEvent, 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,15 +58,9 @@ 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, SHARED.theme.helpText)
-
- self.titleLabel = QLabel(self.tr("Preferences"), self)
- self.titleLabel.setFont(font)
- self.titleLabel.setPalette(palette)
+ self.titleLabel = NColourLabel(
+ self.tr("Preferences"), SHARED.theme.helpText, parent=self, scale=1.25
+ )
self.titleLabel.setIndent(CONFIG.pxInt(4))
# SideBar
diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py
index 54ecb3c8..0ba9b259 100644
--- a/novelwriter/extensions/configlayout.py
+++ b/novelwriter/extensions/configlayout.py
@@ -3,7 +3,7 @@ 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
@@ -40,6 +40,10 @@ LEFT_TOP = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
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)
@@ -48,7 +52,7 @@ class NScrollableForm(QScrollArea):
self._first = True
self._sections: dict[int, QLabel] = {}
- self._editable: dict[str, NHelpLabel] = {}
+ self._editable: dict[str, NColourLabel] = {}
self._index: dict[str, QWidget] = {}
self._layout = QVBoxLayout()
@@ -130,7 +134,7 @@ class NScrollableForm(QScrollArea):
qLabel.setBuddy(widget)
if helpText:
- qHelp = NHelpLabel(str(helpText), self._helpCol, self._fontScale)
+ qHelp = NColourLabel(str(helpText), self._helpCol, scale=self._fontScale, wrap=True)
qHelp.setIndent(mPx)
labelBox = QVBoxLayout()
labelBox.addWidget(qLabel)
@@ -197,7 +201,7 @@ class NConfigLayout(QGridLayout):
"""Set the text for the help label."""
if row in self._itemMap:
qHelp = self._itemMap[row][1]
- if isinstance(qHelp, NHelpLabel):
+ if isinstance(qHelp, NColourLabel):
qHelp.setText(text)
return
@@ -226,7 +230,7 @@ class NConfigLayout(QGridLayout):
qHelp = None
if helpText is not None:
- qHelp = NHelpLabel(str(helpText), self._helpCol, self._fontScale)
+ qHelp = NColourLabel(str(helpText), self._helpCol, scale=self._fontScale, wrap=True)
qHelp.setIndent(wSp)
labelBox = QVBoxLayout()
labelBox.addWidget(qLabel)
@@ -326,10 +330,16 @@ class NSimpleLayout(QGridLayout):
# END Class NSimpleLayout
-class NHelpLabel(QLabel):
+class NColourLabel(QLabel):
+ """Extension: A Coloured Label
- def __init__(self, text: str, color: QColor, scale: 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.
+ """
+
+ def __init__(self, text: str, color: QColor, parent: QWidget | None = None,
+ scale: float = FONT_SCALE, wrap: bool = False) -> None:
+ super().__init__(text, parent=parent)
lblCol = self.palette()
lblCol.setColor(QPalette.WindowText, color)
@@ -339,9 +349,10 @@ class NHelpLabel(QLabel):
lblFont.setPointSizeF(scale*lblFont.pointSizeF())
self.setFont(lblFont)
- self.setWordWrap(True)
- self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ if wrap:
+ self.setWordWrap(True)
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
return
-# END Class NHelpLabel
+# END Class NColourLabel
From cc4d92f0f73a35893d4ea2b24e75e32d168bf363 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 24 Jan 2024 17:05:14 +0100
Subject: [PATCH 3/9] Improve novel selector widget and add to novel details
dialog
---
novelwriter/dialogs/preferences.py | 18 ++--
novelwriter/dialogs/projdetails.py | 2 +-
novelwriter/extensions/novelselector.py | 28 +++++-
novelwriter/gui/mainmenu.py | 2 +-
novelwriter/gui/noveltree.py | 7 +-
novelwriter/gui/outline.py | 5 +-
novelwriter/guimain.py | 8 +-
novelwriter/tools/noveldetails.py | 122 ++++++++++++++++++++++++
novelwriter/tools/novelinfo.py | 71 --------------
9 files changed, 167 insertions(+), 96 deletions(-)
create mode 100644 novelwriter/tools/noveldetails.py
delete mode 100644 novelwriter/tools/novelinfo.py
diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py
index 41663a42..5ece68b1 100644
--- a/novelwriter/dialogs/preferences.py
+++ b/novelwriter/dialogs/preferences.py
@@ -63,11 +63,6 @@ class GuiPreferences(QDialog):
)
self.titleLabel.setIndent(CONFIG.pxInt(4))
- # SideBar
- self.sidebar = NPagedSideBar(self)
- self.sidebar.setLabelColor(SHARED.theme.helpText)
- self.sidebar.buttonClicked.connect(self._sidebarClicked)
-
# Search Box
self.searchText = QLineEdit(self)
self.searchText.setPlaceholderText(self.tr("Search"))
@@ -77,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)
@@ -95,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)
diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py
index a6e12163..31ffea62 100644
--- a/novelwriter/dialogs/projdetails.py
+++ b/novelwriter/dialogs/projdetails.py
@@ -419,7 +419,7 @@ class GuiProjectDetailsContents(QWidget):
def updateValues(self) -> None:
"""Populate the tree."""
self._currentRoot = None
- self.novelValue.updateList()
+ self.novelValue.refreshNovelList()
self.novelValue.setHandle(self.novelValue.firstHandle)
self._prepareData(self.novelValue.firstHandle)
self._populateTree()
diff --git a/novelwriter/extensions/novelselector.py b/novelwriter/extensions/novelselector.py
index 035ab570..793a475d 100644
--- a/novelwriter/extensions/novelselector.py
+++ b/novelwriter/extensions/novelselector.py
@@ -3,7 +3,7 @@ novelWriter – Custom Widget: Novel Selector
===========================================
File History:
-Created: 2022-11-17 [2.0]
+Created: 2022-11-17 [2.0] NovelSelector
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
@@ -43,6 +43,8 @@ class NovelSelector(QComboBox):
super().__init__(parent=parent)
self._blockSignal = False
self._firstHandle = None
+ self._includeAll = False
+ self._listFormat = None
self.currentIndexChanged.connect(self._indexChanged)
return
@@ -74,7 +76,23 @@ class NovelSelector(QComboBox):
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 +101,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 +110,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"), "")
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index 94cd1d20..4ad1ab09 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -162,7 +162,7 @@ class GuiMainMenu(QMenuBar):
# Project > Novel Info
self.aNovelInfo = self.projMenu.addAction(self.tr("Novel Info"))
self.aNovelInfo.setShortcut("Shift+F6")
- self.aNovelInfo.triggered.connect(self.mainGui.showNovelInfoDialog)
+ self.aNovelInfo.triggered.connect(self.mainGui.showNovelDetailsDialog)
# Project > Separator
self.projMenu.addSeparator()
diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py
index 1569262b..959ec7ae 100644
--- a/novelwriter/gui/noveltree.py
+++ b/novelwriter/gui/noveltree.py
@@ -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
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index db066cee..cd441bae 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -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:
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index fea65021..8f4c3e4d 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -56,9 +56,9 @@ from novelwriter.dialogs.preferences import GuiPreferences
from novelwriter.dialogs.projdetails import GuiProjectDetails
from novelwriter.dialogs.projsettings import GuiProjectSettings
from novelwriter.tools.welcome import GuiWelcome
-from novelwriter.tools.novelinfo import GuiNovelInfo
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 (
@@ -838,10 +838,10 @@ class GuiMain(QMainWindow):
return
@pyqtSlot()
- def showNovelInfoDialog(self) -> None:
- """Open the novel info dialog."""
+ def showNovelDetailsDialog(self) -> None:
+ """Open the novel details dialog."""
if SHARED.hasProject:
- dialog = GuiNovelInfo(self)
+ dialog = GuiNovelDetails(self)
dialog.setModal(True)
dialog.show()
dialog.raise_()
diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py
new file mode 100644
index 00000000..66eec029
--- /dev/null
+++ b/novelwriter/tools/noveldetails.py
@@ -0,0 +1,122 @@
+"""
+novelWriter – GUI Novel Info
+============================
+
+File History:
+Created: 2024-01-18 [2.3b1] GuiNovelDetails
+
+This file is a part of novelWriter
+Copyright 2018–2024, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+from __future__ import annotations
+
+import logging
+
+from PyQt5.QtGui import QCloseEvent
+from PyQt5.QtWidgets import (
+ QDialog, QDialogButtonBox, QHBoxLayout, QStackedWidget, QVBoxLayout,
+ QWidget
+)
+
+from novelwriter import CONFIG, SHARED
+from novelwriter.extensions.configlayout import NColourLabel
+from novelwriter.extensions.novelselector import NovelSelector
+from novelwriter.extensions.pagedsidebar import NPagedSideBar
+
+logger = logging.getLogger(__name__)
+
+
+class GuiNovelDetails(QDialog):
+
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
+
+ logger.debug("Create: GuiNovelDetails")
+ self.setObjectName("GuiNovelDetails")
+ self.setWindowTitle(self.tr("Novel Details"))
+ self.setMinimumSize(CONFIG.pxInt(600), CONFIG.pxInt(500))
+
+ # Title
+ self.titleLabel = NColourLabel(
+ self.tr("Novel Details"), SHARED.theme.helpText, parent=self, scale=1.25
+ )
+ self.titleLabel.setIndent(CONFIG.pxInt(4))
+
+ # Novel Selector
+ self.novelSelector = NovelSelector(self)
+ self.novelSelector.refreshNovelList()
+
+ # SideBar
+ self.sidebar = NPagedSideBar(self)
+ self.sidebar.setLabelColor(SHARED.theme.helpText)
+ # self.sidebar.buttonClicked.connect(self._sidebarClicked)
+
+ # Content
+ self.mainStack = QStackedWidget(self)
+
+ # 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)
+
+ logger.debug("Ready: GuiNovelDetails")
+
+ return
+
+ def __del__(self) -> None: # pragma: no cover
+ logger.debug("Delete: GuiNovelDetails")
+ return
+
+ ##
+ # Events
+ ##
+
+ def closeEvent(self, event: QCloseEvent) -> None:
+ """Capture the user closing the window and save settings."""
+ self._saveSettings()
+ event.accept()
+ self.deleteLater()
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _saveSettings(self) -> None:
+ """Save the user GUI settings."""
+ logger.debug("Saving State: GuiNovelDetails")
+ return
+
+# END Class GuiNovelDetails
diff --git a/novelwriter/tools/novelinfo.py b/novelwriter/tools/novelinfo.py
deleted file mode 100644
index 611c80b3..00000000
--- a/novelwriter/tools/novelinfo.py
+++ /dev/null
@@ -1,71 +0,0 @@
-"""
-novelWriter – GUI Novel Info
-============================
-
-File History:
-Created: 2024-01-18 [2.3b1] GuiNovelInfo
-
-This file is a part of novelWriter
-Copyright 2018–2024, Veronica Berglyd Olsen
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but
-WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-"""
-from __future__ import annotations
-
-import logging
-
-from PyQt5.QtGui import QCloseEvent
-from PyQt5.QtWidgets import QDialog, QWidget
-
-logger = logging.getLogger(__name__)
-
-
-class GuiNovelInfo(QDialog):
-
- def __init__(self, parent: QWidget) -> None:
- super().__init__(parent=parent)
-
- logger.debug("Create: GuiNovelInfo")
- self.setObjectName("GuiNovelInfo")
-
- self.setWindowTitle(self.tr("Novel Info"))
- logger.debug("Ready: GuiNovelInfo")
-
- return
-
- def __del__(self) -> None: # pragma: no cover
- logger.debug("Delete: GuiNovelInfo")
- return
-
- ##
- # Events
- ##
-
- def closeEvent(self, event: QCloseEvent) -> None:
- """Capture the user closing the window and save settings."""
- self._saveSettings()
- event.accept()
- self.deleteLater()
- return
-
- ##
- # Internal Functions
- ##
-
- def _saveSettings(self) -> None:
- """Save the user GUI settings."""
- logger.debug("Saving State: GuiNovelInfo")
- return
-
-# END Class GuiNovelInfo
From 6622a7e8e02232d4ebbd93941f2f11eafd73d4eb Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 24 Jan 2024 19:26:48 +0100
Subject: [PATCH 4/9] Begin to add content to the novel details overview page
---
novelwriter/core/options.py | 5 +-
novelwriter/guimain.py | 2 +-
novelwriter/tools/noveldetails.py | 126 +++++++++++++++++++++++++++++-
3 files changed, 128 insertions(+), 5 deletions(-)
diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py
index 8e50c30e..93ec4d8c 100644
--- a/novelwriter/core/options.py
+++ b/novelwriter/core/options.py
@@ -71,7 +71,10 @@ VALID_MAP: dict[str, set[str]] = {
},
"GuiDocViewerPanel": {
"colWidths", "hideInactive",
- }
+ },
+ "GuiNovelDetails": {
+ "winWidth", "winHeight",
+ },
}
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 8f4c3e4d..143d9c65 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -846,7 +846,7 @@ class GuiMain(QMainWindow):
dialog.show()
dialog.raise_()
qApp.processEvents()
- # dialog.updateValues()
+ dialog.updateValues()
return
@pyqtSlot()
diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py
index 66eec029..fdced6b9 100644
--- a/novelwriter/tools/noveldetails.py
+++ b/novelwriter/tools/noveldetails.py
@@ -24,14 +24,16 @@ along with this program. If not, see .
from __future__ import annotations
import logging
+from PyQt5.QtCore import pyqtSlot
from PyQt5.QtGui import QCloseEvent
from PyQt5.QtWidgets import (
- QDialog, QDialogButtonBox, QHBoxLayout, QStackedWidget, QVBoxLayout,
+ QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel, QStackedWidget, QVBoxLayout,
QWidget
)
from novelwriter import CONFIG, SHARED
+from novelwriter.common import formatTime
from novelwriter.extensions.configlayout import NColourLabel
from novelwriter.extensions.novelselector import NovelSelector
from novelwriter.extensions.pagedsidebar import NPagedSideBar
@@ -41,13 +43,25 @@ 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"))
- self.setMinimumSize(CONFIG.pxInt(600), CONFIG.pxInt(500))
+
+ wW = CONFIG.pxInt(500)
+ wH = CONFIG.pxInt(400)
+ options = SHARED.project.options
+
+ self.setMinimumSize(wW, wH)
+ self.resize(
+ CONFIG.pxInt(options.getInt("GuiNovelDetails", "winWidth", wW)),
+ CONFIG.pxInt(options.getInt("GuiNovelDetails", "winHeight", wH))
+ )
# Title
self.titleLabel = NColourLabel(
@@ -62,10 +76,16 @@ class GuiNovelDetails(QDialog):
# SideBar
self.sidebar = NPagedSideBar(self)
self.sidebar.setLabelColor(SHARED.theme.helpText)
- # self.sidebar.buttonClicked.connect(self._sidebarClicked)
+ 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.mainStack = QStackedWidget(self)
+ self.mainStack.addWidget(self.overviewPage)
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
@@ -99,6 +119,15 @@ class GuiNovelDetails(QDialog):
logger.debug("Delete: GuiNovelDetails")
return
+ ##
+ # Methods
+ ##
+
+ def updateValues(self) -> None:
+ """Load the dialogs initial values."""
+ self.overviewPage.updateProjectData()
+ return
+
##
# Events
##
@@ -110,13 +139,104 @@ class GuiNovelDetails(QDialog):
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)
+ return
+
##
# Internal Functions
##
def _saveSettings(self) -> None:
"""Save the user GUI settings."""
+ winWidth = CONFIG.rpxInt(self.width())
+ winHeight = CONFIG.rpxInt(self.height())
+
logger.debug("Saving State: GuiNovelDetails")
+ options = SHARED.project.options
+ options.setValue("GuiNovelDetails", "winWidth", winWidth)
+ options.setValue("GuiNovelDetails", "winHeight", winHeight)
return
# END Class GuiNovelDetails
+
+
+class _OverviewPage(QWidget):
+
+ 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, self, 1.5)
+
+ self.projName = QLabel("", self)
+ self.projWords = QLabel("", self)
+ self.projRevisions = QLabel("", self)
+ self.projEditTime = QLabel("", self)
+
+ self.projForm = QFormLayout()
+ self.projForm.addRow("%s" % self.tr("Name"), self.projName)
+ self.projForm.addRow("%s" % self.tr("Word Count"), self.projWords)
+ self.projForm.addRow("%s" % self.tr("Revisions"), self.projRevisions)
+ self.projForm.addRow("%s" % self.tr("Editing Time"), self.projEditTime)
+ self.projForm.setContentsMargins(mPx, 0, 0, 0)
+ self.projForm.setHorizontalSpacing(hPx)
+ self.projForm.setVerticalSpacing(vPx)
+
+ # Novel Info
+ self.novelLabel = NColourLabel(self.tr("Novel"), SHARED.theme.helpText, self, 1.5)
+
+ self.novelName = QLabel("", self)
+ self.novelWords = QLabel("", self)
+ self.novelChapters = QLabel("", self)
+ self.novelScenes = QLabel("", self)
+
+ self.novelForm = QFormLayout()
+ self.novelForm.addRow("%s" % self.tr("Name"), self.novelName)
+ self.novelForm.addRow("%s" % self.tr("Word Count"), self.novelWords)
+ self.novelForm.addRow("%s" % self.tr("Chapters"), self.novelChapters)
+ self.novelForm.addRow("%s" % 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.setContentsMargins(0, 0, 0, 0)
+ self.outerBox.setSpacing(sPx)
+ self.outerBox.addStretch(1)
+
+ self.setLayout(self.outerBox)
+
+ return
+
+ ##
+ # Methods
+ ##
+
+ def updateProjectData(self) -> None:
+ """Load information about the project."""
+ project = SHARED.project
+ self.projName.setText(project.data.name)
+ self.projWords.setText(f"{project.index.getNovelWordCount():n}")
+ self.projRevisions.setText(f"{project.data.saveCount:n}")
+ self.projEditTime.setText(formatTime(project.currentEditTime))
+ return
+
+# END Class _OverviewPage
From 23713bb79192cc9d330035d7411d005fbc9727a6 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 25 Jan 2024 21:25:43 +0100
Subject: [PATCH 5/9] Add table of contents code to novel details panel
---
novelwriter/core/index.py | 21 +-
novelwriter/core/options.py | 4 +-
novelwriter/tools/noveldetails.py | 322 +++++++++++++++++++++++++++--
tests/test_core/test_core_index.py | 5 +
4 files changed, 326 insertions(+), 26 deletions(-)
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index f58145f9..7f1598b6 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -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
diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py
index 93ec4d8c..e2e71296 100644
--- a/novelwriter/core/options.py
+++ b/novelwriter/core/options.py
@@ -73,7 +73,9 @@ VALID_MAP: dict[str, set[str]] = {
"colWidths", "hideInactive",
},
"GuiNovelDetails": {
- "winWidth", "winHeight",
+ "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
+ "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
+ "novelRoot",
},
}
diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py
index fdced6b9..9139a655 100644
--- a/novelwriter/tools/noveldetails.py
+++ b/novelwriter/tools/noveldetails.py
@@ -23,23 +23,29 @@ along with this program. If not, see .
"""
from __future__ import annotations
+import math
import logging
-from PyQt5.QtCore import pyqtSlot
from PyQt5.QtGui import QCloseEvent
+from PyQt5.QtCore import QSize, Qt, pyqtSlot
from PyQt5.QtWidgets import (
- QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel, QStackedWidget, QVBoxLayout,
- QWidget
+ QAbstractItemView, QDialog, QDialogButtonBox, QFormLayout, QGridLayout,
+ QHBoxLayout, QLabel, QSpinBox, QStackedWidget, QTreeWidget,
+ QTreeWidgetItem, QVBoxLayout, QWidget
)
from novelwriter import CONFIG, SHARED
-from novelwriter.common import formatTime
+from novelwriter.common import formatTime, numberToRoman
+from novelwriter.constants import nwUnicode
+from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.configlayout import NColourLabel
-from novelwriter.extensions.novelselector import NovelSelector
from novelwriter.extensions.pagedsidebar import NPagedSideBar
+from novelwriter.extensions.novelselector import NovelSelector
logger = logging.getLogger(__name__)
+HEADER_SIZE = 1.4
+
class GuiNovelDetails(QDialog):
@@ -72,6 +78,7 @@ class GuiNovelDetails(QDialog):
# Novel Selector
self.novelSelector = NovelSelector(self)
self.novelSelector.refreshNovelList()
+ self.novelSelector.setHandle(options.getString("GuiNovelDetails", "novelRoot", ""))
# SideBar
self.sidebar = NPagedSideBar(self)
@@ -83,9 +90,11 @@ class GuiNovelDetails(QDialog):
# 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)
@@ -111,6 +120,10 @@ class GuiNovelDetails(QDialog):
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
@@ -126,6 +139,8 @@ class GuiNovelDetails(QDialog):
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
##
@@ -148,6 +163,8 @@ class GuiNovelDetails(QDialog):
"""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
##
@@ -158,11 +175,16 @@ class GuiNovelDetails(QDialog):
"""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
@@ -179,24 +201,32 @@ class _OverviewPage(QWidget):
vPx = CONFIG.pxInt(4)
# Project Info
- self.projLabel = NColourLabel(self.tr("Project"), SHARED.theme.helpText, self, 1.5)
+ self.projLabel = NColourLabel(
+ self.tr("Project"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE
+ )
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("%s" % self.tr("Name"), self.projName)
- self.projForm.addRow("%s" % self.tr("Word Count"), self.projWords)
- self.projForm.addRow("%s" % self.tr("Revisions"), self.projRevisions)
- self.projForm.addRow("%s" % self.tr("Editing Time"), self.projEditTime)
+ self.projForm.addRow("{0}".format(self.tr("Name")), self.projName)
+ self.projForm.addRow("{0}".format(self.tr("Revisions")), self.projRevisions)
+ self.projForm.addRow("{0}".format(self.tr("Editing Time")), self.projEditTime)
+ self.projForm.addRow("{0}".format(self.tr("Word Count")), self.projWords)
+ self.projForm.addRow("\u2026 {0}".format(self.tr("In Novels")), self.projNovels)
+ self.projForm.addRow("\u2026 {0}".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("Novel"), SHARED.theme.helpText, self, 1.5)
+ self.novelLabel = NColourLabel(
+ self.tr("Selected Novel"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE
+ )
self.novelName = QLabel("", self)
self.novelWords = QLabel("", self)
@@ -204,10 +234,10 @@ class _OverviewPage(QWidget):
self.novelScenes = QLabel("", self)
self.novelForm = QFormLayout()
- self.novelForm.addRow("%s" % self.tr("Name"), self.novelName)
- self.novelForm.addRow("%s" % self.tr("Word Count"), self.novelWords)
- self.novelForm.addRow("%s" % self.tr("Chapters"), self.novelChapters)
- self.novelForm.addRow("%s" % self.tr("Scenes"), self.novelScenes)
+ self.novelForm.addRow("{0}".format(self.tr("Name")), self.novelName)
+ self.novelForm.addRow("{0}".format(self.tr("Word Count")), self.novelWords)
+ self.novelForm.addRow("{0}".format(self.tr("Chapters")), self.novelChapters)
+ self.novelForm.addRow("{0}".format(self.tr("Scenes")), self.novelScenes)
self.novelForm.setContentsMargins(mPx, 0, 0, 0)
self.novelForm.setHorizontalSpacing(hPx)
self.novelForm.setVerticalSpacing(vPx)
@@ -233,10 +263,270 @@ class _OverviewPage(QWidget):
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.projWords.setText(f"{project.index.getNovelWordCount():n}")
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(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)
+
+ 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=HEADER_SIZE
+ )
+
+ # 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.outerBox.setContentsMargins(0, 0, 0, 0)
+
+ self.setLayout(self.outerBox)
+
+ return
+
+ def saveSettings(self) -> None:
+ """Save the user GUI settings."""
+ options = SHARED.project.options
+ options.setValue("GuiNovelDetails", "widthCol0", self.tocTree.columnWidth(0))
+ options.setValue("GuiNovelDetails", "widthCol1", self.tocTree.columnWidth(1))
+ options.setValue("GuiNovelDetails", "widthCol2", self.tocTree.columnWidth(2))
+ options.setValue("GuiNovelDetails", "widthCol3", self.tocTree.columnWidth(3))
+ options.setValue("GuiNovelDetails", "widthCol4", self.tocTree.columnWidth(4))
+ options.setValue("GuiNovelDetails", "wordsPerPage", self.wpValue.value())
+ options.setValue("GuiNovelDetails", "countFrom", self.poValue.value())
+ options.setValue("GuiNovelDetails", "clearDouble", self.dblValue.isChecked())
+ 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
+
+ ##
+ # 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
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index 5e9dee3f..44836349 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -695,6 +695,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
"This is a story about Jane Smith.\n\n"
"Well, not really. She's still awesome though.\n"
))
+
# Whole document
cC, wC, pC = index.getCounts(nHandle)
assert cC == 152
@@ -798,6 +799,10 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
# Extract stats
assert index.getNovelWordCount(activeOnly=False) == 43
assert index.getNovelWordCount(activeOnly=True) == 15
+ assert index.getNovelWordCount(rootHandle=C.hNovelRoot, activeOnly=False) == 43
+ assert index.getNovelWordCount(rootHandle=C.hNovelRoot, activeOnly=True) == 15
+ assert index.getNovelWordCount(rootHandle=C.hWorldRoot, activeOnly=False) == 0
+ assert index.getNovelWordCount(rootHandle=C.hWorldRoot, activeOnly=True) == 0
assert index.getNovelTitleCounts(activeOnly=False) == [0, 3, 2, 3, 0]
assert index.getNovelTitleCounts(activeOnly=True) == [0, 1, 2, 3, 0]
From 4ef40f2c4d823aab2b1651b89e96ed7df6e0fd94 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 25 Jan 2024 21:30:53 +0100
Subject: [PATCH 6/9] Remove old project details dialog
---
novelwriter/core/options.py | 4 -
novelwriter/dialogs/projdetails.py | 518 ---------------------
novelwriter/gui/mainmenu.py | 12 +-
novelwriter/gui/sidebar.py | 4 +-
novelwriter/guimain.py | 13 -
tests/test_core/test_core_options.py | 32 +-
tests/test_dialogs/test_dlg_projdetails.py | 109 -----
7 files changed, 22 insertions(+), 670 deletions(-)
delete mode 100644 novelwriter/dialogs/projdetails.py
delete mode 100644 tests/test_dialogs/test_dlg_projdetails.py
diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py
index e2e71296..c48aaa13 100644
--- a/novelwriter/core/options.py
+++ b/novelwriter/core/options.py
@@ -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": {
diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py
deleted file mode 100644
index 31ffea62..00000000
--- a/novelwriter/dialogs/projdetails.py
+++ /dev/null
@@ -1,518 +0,0 @@
-"""
-novelWriter – GUI Project Details
-=================================
-
-File History:
-Created: 2021-01-03 [1.1rc1] GuiProjectDetails
-
-This file is a part of novelWriter
-Copyright 2018–2024, Veronica Berglyd Olsen
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but
-WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-"""
-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("%s:" % self.tr("Words"))
- self.wordCountVal = QLabel("")
-
- self.chapCountLbl = QLabel("%s:" % self.tr("Chapters"))
- self.chapCountVal = QLabel("")
-
- self.sceneCountLbl = QLabel("%s:" % self.tr("Scenes"))
- self.sceneCountVal = QLabel("")
-
- self.revCountLbl = QLabel("%s:" % self.tr("Revisions"))
- self.revCountVal = QLabel("")
-
- self.editTimeLbl = QLabel("%s:" % 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("%s:" % 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("%s" % 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.refreshNovelList()
- 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
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index 4ad1ab09..279fce07 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -155,14 +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.triggered.connect(self.mainGui.showProjectDetailsDialog)
-
- # Project > Novel Info
- self.aNovelInfo = self.projMenu.addAction(self.tr("Novel Info"))
- self.aNovelInfo.setShortcut("Shift+F6")
- self.aNovelInfo.triggered.connect(self.mainGui.showNovelDetailsDialog)
+ # 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()
diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py
index 92522542..d425ccf8 100644
--- a/novelwriter/gui/sidebar.py
+++ b/novelwriter/gui/sidebar.py
@@ -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")))
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 143d9c65..ad29b741 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -53,7 +53,6 @@ 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.tools.welcome import GuiWelcome
from novelwriter.tools.manuscript import GuiManuscript
@@ -825,18 +824,6 @@ class GuiMain(QMainWindow):
dialog.exec_()
return
- @pyqtSlot()
- def showProjectDetailsDialog(self) -> None:
- """Open the project details dialog."""
- if SHARED.hasProject:
- dialog = GuiProjectDetails(self)
- dialog.setModal(True)
- dialog.show()
- dialog.raise_()
- qApp.processEvents()
- dialog.updateValues()
- return
-
@pyqtSlot()
def showNovelDetailsDialog(self) -> None:
"""Open the novel details dialog."""
diff --git a/tests/test_core/test_core_options.py b/tests/test_core/test_core_options.py
index 7458c746..fd291d08 100644
--- a/tests/test_core/test_core_options.py
+++ b/tests/test_core/test_core_options.py
@@ -121,27 +121,27 @@ def testCoreOptions_SetGet(mockGUI):
assert options.setValue("GuiProjectSettings", "winWidth", 100) is True
# Set some values of different types
- assert options.setValue("GuiProjectDetails", "winWidth", 100) is True
- assert options.setValue("GuiProjectDetails", "winHeight", 12.34) is True
- assert options.setValue("GuiProjectDetails", "clearDouble", True) is True
+ assert options.setValue("GuiNovelDetails", "winWidth", 100) is True
+ assert options.setValue("GuiNovelDetails", "winHeight", 12.34) is True
+ assert options.setValue("GuiNovelDetails", "clearDouble", True) is True
assert options.setValue("GuiNovelView", "lastCol", nwColHidden) is True
# Generic get, doesn't check type
- assert options.getValue("GuiProjectDetails", "winWidth", None) == 100
- assert options.getValue("GuiProjectDetails", "winHeight", None) == 12.34
- assert options.getValue("GuiProjectDetails", "clearDouble", None) is True
- assert options.getValue("GuiProjectDetails", "mockItem", None) is None
+ assert options.getValue("GuiNovelDetails", "winWidth", None) == 100
+ assert options.getValue("GuiNovelDetails", "winHeight", None) == 12.34
+ assert options.getValue("GuiNovelDetails", "clearDouble", None) is True
+ assert options.getValue("GuiNovelDetails", "mockItem", None) is None
# Get type-specific
- assert options.getString("GuiProjectDetails", "winWidth", None) is None # type: ignore
- assert options.getString("GuiProjectDetails", "mockItem", None) is None # type: ignore
- assert options.getInt("GuiProjectDetails", "winWidth", None) == 100 # type: ignore
- assert options.getInt("GuiProjectDetails", "textFont", None) is None # type: ignore
- assert options.getInt("GuiProjectDetails", "mockItem", None) is None # type: ignore
- assert options.getFloat("GuiProjectDetails", "winWidth", None) == 100.0 # type: ignore
- assert options.getFloat("GuiProjectDetails", "mockItem", None) is None # type: ignore
- assert options.getBool("GuiProjectDetails", "clearDouble", None) is True # type: ignore
- assert options.getBool("GuiProjectDetails", "mockItem", None) is None # type: ignore
+ assert options.getString("GuiNovelDetails", "winWidth", None) is None # type: ignore
+ assert options.getString("GuiNovelDetails", "mockItem", None) is None # type: ignore
+ assert options.getInt("GuiNovelDetails", "winWidth", None) == 100 # type: ignore
+ assert options.getInt("GuiNovelDetails", "textFont", None) is None # type: ignore
+ assert options.getInt("GuiNovelDetails", "mockItem", None) is None # type: ignore
+ assert options.getFloat("GuiNovelDetails", "winWidth", None) == 100.0 # type: ignore
+ assert options.getFloat("GuiNovelDetails", "mockItem", None) is None # type: ignore
+ assert options.getBool("GuiNovelDetails", "clearDouble", None) is True # type: ignore
+ assert options.getBool("GuiNovelDetails", "mockItem", None) is None # type: ignore
assert options.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, nwColHidden) == nwColHidden
# Get from non-existent groups
diff --git a/tests/test_dialogs/test_dlg_projdetails.py b/tests/test_dialogs/test_dlg_projdetails.py
deleted file mode 100644
index 3d68f9d7..00000000
--- a/tests/test_dialogs/test_dlg_projdetails.py
+++ /dev/null
@@ -1,109 +0,0 @@
-"""
-novelWriter – Project Details Dialog Class Tester
-=================================================
-
-This file is a part of novelWriter
-Copyright 2018–2024, Veronica Berglyd Olsen
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but
-WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-"""
-from __future__ import annotations
-
-import pytest
-
-from tools import getGuiItem
-
-from PyQt5.QtWidgets import QAction
-
-from novelwriter import SHARED
-from novelwriter.dialogs.projdetails import GuiProjectDetails
-
-
-@pytest.mark.gui
-def testDlgProjDetails_Dialog(qtbot, nwGUI, prjLipsum):
- """Test the project details dialog.
- """
- # Create a project to work on
- assert nwGUI.openProject(prjLipsum)
- assert nwGUI.rebuildIndex(beQuiet=True)
- qtbot.wait(100)
-
- # Open the Writing Stats dialog
- nwGUI.mainMenu.aProjectDetails.activate(QAction.Trigger)
- qtbot.waitUntil(lambda: getGuiItem("GuiProjectDetails") is not None, timeout=1000)
-
- projDet = getGuiItem("GuiProjectDetails")
- assert isinstance(projDet, GuiProjectDetails)
-
- # Overview Page
- # =============
-
- assert projDet.tabMain.bookTitle.text() == "Lorem Ipsum"
- assert projDet.tabMain.projName.text()[-11:] == "Lorem Ipsum"
- assert projDet.tabMain.bookAuthors.text()[-10:] == "lipsum.com"
-
- assert projDet.tabMain.wordCountVal.text() == f"{3000:n}"
- assert projDet.tabMain.chapCountVal.text() == f"{3:n}"
- assert projDet.tabMain.sceneCountVal.text() == f"{5:n}"
- assert projDet.tabMain.revCountVal.text() == f"{SHARED.project.data.saveCount:n}"
-
- assert projDet.tabMain.projPathVal.text() == str(prjLipsum)
-
- # Contents Page
- # =============
-
- tocTab = projDet.tabContents
- tocTree = tocTab.tocTree
- assert tocTree.topLevelItemCount() == 7
- assert tocTree.topLevelItem(0).text(tocTab.C_TITLE) == "Lorem Ipsum" # type: ignore
- assert tocTree.topLevelItem(2).text(tocTab.C_TITLE) == "Prologue" # type: ignore
- assert tocTree.topLevelItem(3).text(tocTab.C_TITLE) == "Act One" # type: ignore
- assert tocTree.topLevelItem(4).text(tocTab.C_TITLE) == "Chapter One" # type: ignore
- assert tocTree.topLevelItem(5).text(tocTab.C_TITLE) == "Chapter Two" # type: ignore
- assert tocTree.topLevelItem(6).text(tocTab.C_TITLE) == "END" # type: ignore
-
- # Count Pages
- tocTab.wpValue.setValue(100)
- tocTab.poValue.setValue(4)
- tocTab.dblValue.setChecked(False)
- tocTab._populateTree()
-
- thePages = ["1", "2", "1", "1", "11", "17", "0"]
- thePage = ["i", "ii", "1", "2", "3", "14", "31"]
- for i in range(7):
- assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] # type: ignore
- assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] # type: ignore
-
- tocTab.poValue.setValue(5)
- tocTab.dblValue.setChecked(True)
- tocTab._populateTree()
-
- thePages = ["2", "2", "2", "2", "12", "18", "0"]
- thePage = ["i", "iii", "1", "3", "5", "17", "35"]
- for i in range(7):
- assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] # type: ignore
- assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] # type: ignore
-
- # Re-populate
- assert tocTab._currentRoot is None
- tocTab._novelValueChanged("7a992350f3eb6") # Not a root
- assert tocTab._currentRoot == "b3643d0f92e32" # The actual novel root
-
- # qtbot.stop()
-
- # Clean Up
- projDet.close()
- nwGUI.closeMain()
-
-# END Test testDlgProjDetails_Dialog
From 483528b3b2c2f1ed17d1b23da9bbb453a98552b5 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 25 Jan 2024 22:59:06 +0100
Subject: [PATCH 7/9] Modify some of the custom widgets after writing tests
---
novelwriter/extensions/novelselector.py | 10 +++-------
novelwriter/extensions/pagedsidebar.py | 6 ++----
novelwriter/tools/noveldetails.py | 15 +++------------
3 files changed, 8 insertions(+), 23 deletions(-)
diff --git a/novelwriter/extensions/novelselector.py b/novelwriter/extensions/novelselector.py
index 793a475d..44e5bf23 100644
--- a/novelwriter/extensions/novelselector.py
+++ b/novelwriter/extensions/novelselector.py
@@ -66,14 +66,10 @@ 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 setIncludeAll(self, value: bool) -> None:
diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py
index 185b4d8a..082e8a2a 100644
--- a/novelwriter/extensions/pagedsidebar.py
+++ b/novelwriter/extensions/pagedsidebar.py
@@ -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)
@@ -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
diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py
index 9139a655..3fcd54cc 100644
--- a/novelwriter/tools/noveldetails.py
+++ b/novelwriter/tools/noveldetails.py
@@ -78,7 +78,9 @@ class GuiNovelDetails(QDialog):
# Novel Selector
self.novelSelector = NovelSelector(self)
self.novelSelector.refreshNovelList()
- self.novelSelector.setHandle(options.getString("GuiNovelDetails", "novelRoot", ""))
+ self.novelSelector.setHandle(
+ options.getString("GuiNovelDetails", "novelRoot", self.novelSelector.firstHandle or "")
+ )
# SideBar
self.sidebar = NPagedSideBar(self)
@@ -424,17 +426,6 @@ class _ContentsPage(QWidget):
options.setValue("GuiNovelDetails", "clearDouble", self.dblValue.isChecked())
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
-
##
# Public Slots
##
From b6c9f1160fa46808ec06d33e9139f8bb3314ca12 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 25 Jan 2024 22:59:22 +0100
Subject: [PATCH 8/9] Add test coverage
---
tests/test_dialogs/test_dlg_preferences.py | 18 ++-
tests/test_tools/test_tools_noveldetails.py | 136 ++++++++++++++++++++
tests/tools.py | 37 +++---
3 files changed, 168 insertions(+), 23 deletions(-)
create mode 100644 tests/test_tools/test_tools_noveldetails.py
diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py
index c4c2e590..408865c7 100644
--- a/tests/test_dialogs/test_dlg_preferences.py
+++ b/tests/test_dialogs/test_dlg_preferences.py
@@ -22,9 +22,11 @@ from __future__ import annotations
import pytest
+from tools import getGuiItem
+
from PyQt5.QtGui import QFontDatabase, QKeyEvent
from PyQt5.QtCore import QEvent, Qt
-from PyQt5.QtWidgets import QDialogButtonBox, QFileDialog, QFontDialog
+from PyQt5.QtWidgets import QAction, QDialogButtonBox, QFileDialog, QFontDialog
from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwConst, nwUnicode
@@ -38,9 +40,13 @@ KEY_DELAY = 1
def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths):
"""Test the preferences dialog loading."""
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")])
+ monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None)
# Load GUI with standard values
- prefs = GuiPreferences(nwGUI)
+ nwGUI.mainMenu.aPreferences.activate(QAction.ActionEvent.Trigger)
+ qtbot.waitUntil(lambda: getGuiItem("GuiPreferences") is not None, timeout=1000)
+ prefs = getGuiItem("GuiPreferences")
+ assert isinstance(prefs, GuiPreferences)
prefs.show()
# Check Languages
@@ -95,10 +101,6 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
# Check Navigation
vBar = prefs.mainForm.verticalScrollBar()
old = -1
- with qtbot.waitSignal(vBar.valueChanged) as value:
- prefs.sidebar.button(0).click()
- assert value.args[0] > old
- old = value.args[0]
with qtbot.waitSignal(vBar.valueChanged) as value:
prefs.sidebar.button(1).click()
assert value.args[0] > old
@@ -107,6 +109,10 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
prefs.sidebar.button(2).click()
assert value.args[0] > old
old = value.args[0]
+ with qtbot.waitSignal(vBar.valueChanged) as value:
+ prefs.sidebar.button(3).click()
+ assert value.args[0] > old
+ old = value.args[0]
# Check Search
prefs.searchText.setText("Display language")
diff --git a/tests/test_tools/test_tools_noveldetails.py b/tests/test_tools/test_tools_noveldetails.py
new file mode 100644
index 00000000..c8925e53
--- /dev/null
+++ b/tests/test_tools/test_tools_noveldetails.py
@@ -0,0 +1,136 @@
+"""
+novelWriter – Novel Details Tool Tester
+=======================================
+
+This file is a part of novelWriter
+Copyright 2018–2024, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+from __future__ import annotations
+
+import pytest
+
+from tools import getGuiItem
+
+from PyQt5.QtWidgets import QAction
+
+from novelwriter import SHARED
+from novelwriter.enum import nwItemClass
+from novelwriter.tools.noveldetails import GuiNovelDetails
+
+
+@pytest.mark.gui
+def testToolNovelDetails_Main(qtbot, nwGUI, prjLipsum, ipsumText):
+ """Test the Novel Details main dialog."""
+ nwGUI.openProject(prjLipsum)
+ nHandle = "b3643d0f92e32"
+
+ # Add a second Novel folder
+ project = SHARED.project
+ secondText = "#! Second\n\n" + "\n\n".join(ipsumText)
+ sHandle = project.newRoot(nwItemClass.NOVEL, "Second")
+ dHandle = project.newFile("Document", sHandle)
+ project.storage.getDocument(dHandle).writeDocument(secondText)
+ project.index.reIndexHandle(dHandle)
+ nwGUI.projView.projTree.revealNewTreeItem(sHandle)
+ nwGUI.projView.projTree.revealNewTreeItem(dHandle)
+
+ # Create the dialog
+ nwGUI.mainMenu.aNovelDetails.activate(QAction.ActionEvent.Trigger)
+ qtbot.waitUntil(lambda: getGuiItem("GuiNovelDetails") is not None, timeout=1000)
+ details = getGuiItem("GuiNovelDetails")
+ assert isinstance(details, GuiNovelDetails)
+
+ # Overview Page
+ # =============
+ overview = details.overviewPage
+
+ # The selector should default to the first entry
+ assert details.novelSelector.handle == nHandle
+
+ # Check project data
+ assert overview.projName.text() == "Lorem Ipsum"
+ assert overview.projWords.text() == f"{4376:n}"
+ assert overview.projNovels.text() == f"{3638:n}"
+ assert overview.projNotes.text() == f"{738:n}"
+ assert overview.projRevisions.text() != ""
+ assert overview.projEditTime.text() != ""
+
+ # Check novel data for "Novel"
+ assert overview.novelName.text() == "Novel"
+ assert overview.novelWords.text() == f"{3000:n}"
+ assert overview.novelChapters.text() == f"{3:n}"
+ assert overview.novelScenes.text() == f"{5:n}"
+
+ # Check novel data for "Second"
+ details.novelSelector.setHandle(sHandle, blockSignal=False)
+ assert overview.novelName.text() == "Second"
+ assert overview.novelWords.text() == f"{529:n}"
+ assert overview.novelChapters.text() == f"{0:n}"
+ assert overview.novelScenes.text() == f"{0:n}"
+
+ # Contents Page
+ # =============
+ details.novelSelector.setHandle(nHandle, blockSignal=False)
+ details.sidebar.button(details.PAGE_CONTENTS).click()
+ assert details.mainStack.currentIndex() == 1
+ contents = details.contentsPage
+
+ # Check defaults
+ words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]]
+ pages = [f"{v:n}" for v in [2, 2, 2, 2, 4, 6, 0]]
+ page = [f"{v:n}" for v in [1, 3, 5, 7, 9, 13, 19]]
+ for i in range(6):
+ item = contents.tocTree.topLevelItem(i)
+ assert item is not None
+ assert item.text(contents.C_WORDS) == words[i]
+ assert item.text(contents.C_PAGES) == pages[i]
+ assert item.text(contents.C_PAGE) == page[i]
+
+ # Change Settings
+ contents.poValue.setValue(7)
+ contents.wpValue.setValue(50)
+ words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]]
+ pages = [f"{v:n}" for v in [2, 4, 2, 2, 22, 34, 0]]
+ page = ["i", "iii"] + [f"{v:n}" for v in [1, 3, 5, 27, 61]]
+ for i in range(6):
+ item = contents.tocTree.topLevelItem(i)
+ assert item is not None
+ assert item.text(contents.C_WORDS) == words[i]
+ assert item.text(contents.C_PAGES) == pages[i]
+ assert item.text(contents.C_PAGE) == page[i]
+
+ # Turn off use odd pages
+ contents.dblValue.setChecked(False)
+ contents.poValue.setValue(0)
+ contents.wpValue.setValue(100)
+ words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]]
+ pages = [f"{v:n}" for v in [1, 2, 1, 1, 11, 17, 0]]
+ page = [f"{v:n}" for v in [1, 2, 4, 5, 6, 17, 34]]
+ for i in range(6):
+ item = contents.tocTree.topLevelItem(i)
+ assert item is not None
+ assert item.text(contents.C_WORDS) == words[i]
+ assert item.text(contents.C_PAGES) == pages[i]
+ assert item.text(contents.C_PAGE) == page[i]
+
+ # Revert to Overview page
+ details.sidebar.button(details.PAGE_OVERVIEW).click()
+ assert details.mainStack.currentIndex() == 0
+
+ # qtbot.stop()
+ details.close()
+
+# END Test testToolNovelDetails_Main
diff --git a/tests/tools.py b/tests/tools.py
index 6bc21966..51cdcc19 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -156,20 +156,23 @@ def cleanProject(path: str | Path):
return
-def buildTestProject(obj, projPath):
+def buildTestProject(obj: object, projPath: Path) -> None:
"""Build a standard test project in projPath using the project
object as the parent.
"""
from novelwriter.enum import nwItemClass
+ from novelwriter.guimain import GuiMain
from novelwriter.core.project import NWProject
if isinstance(obj, NWProject):
nwGUI = None
project = obj
- else:
+ elif isinstance(obj, GuiMain):
from novelwriter import SHARED
nwGUI = obj
project = SHARED.project
+ else:
+ return
project.storage.createNewProject(projPath)
project.setDefaultStatusImport()
@@ -181,27 +184,27 @@ def buildTestProject(obj, projPath):
# Creating a minimal project with a few root folders and a
# single chapter folder with a single file.
- xHandle = {}
- xHandle[1] = project.newRoot(nwItemClass.NOVEL, "Novel")
- xHandle[2] = project.newRoot(nwItemClass.PLOT, "Plot")
- xHandle[3] = project.newRoot(nwItemClass.CHARACTER, "Characters")
- xHandle[4] = project.newRoot(nwItemClass.WORLD, "World")
- xHandle[5] = project.newFile("Title Page", xHandle[1])
- xHandle[6] = project.newFolder("New Chapter", xHandle[1])
- xHandle[7] = project.newFile("New Chapter", xHandle[6])
- xHandle[8] = project.newFile("New Scene", xHandle[6])
+ nrHandle = project.newRoot(nwItemClass.NOVEL, "Novel")
+ project.newRoot(nwItemClass.PLOT, "Plot")
+ project.newRoot(nwItemClass.CHARACTER, "Characters")
+ project.newRoot(nwItemClass.WORLD, "World")
- aDoc = project.storage.getDocument(xHandle[5])
+ tdHandle = project.newFile("Title Page", nrHandle)
+ cfHandle = project.newFolder("New Chapter", nrHandle) or ""
+ cdHandle = project.newFile("New Chapter", cfHandle)
+ sdHandle = project.newFile("New Scene", cfHandle)
+
+ aDoc = project.storage.getDocument(tdHandle)
aDoc.writeDocument("#! New Novel\n\n>> By Jane Doe <<\n")
- project.index.reIndexHandle(xHandle[5])
+ project.index.reIndexHandle(tdHandle)
- aDoc = project.storage.getDocument(xHandle[7])
+ aDoc = project.storage.getDocument(cdHandle)
aDoc.writeDocument("## %s\n\n" % project.tr("New Chapter"))
- project.index.reIndexHandle(xHandle[7])
+ project.index.reIndexHandle(cdHandle)
- aDoc = project.storage.getDocument(xHandle[8])
+ aDoc = project.storage.getDocument(sdHandle)
aDoc.writeDocument("### %s\n\n" % project.tr("New Scene"))
- project.index.reIndexHandle(xHandle[8])
+ project.index.reIndexHandle(sdHandle)
project.session.startSession()
project.setProjectChanged(True)
From 10012054be105f0a3e107c264345fef6e409948d Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 25 Jan 2024 23:07:10 +0100
Subject: [PATCH 9/9] Remove some unused code
---
novelwriter/extensions/configlayout.py | 20 ++------------------
1 file changed, 2 insertions(+), 18 deletions(-)
diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py
index 0ba9b259..c71036be 100644
--- a/novelwriter/extensions/configlayout.py
+++ b/novelwriter/extensions/configlayout.py
@@ -197,14 +197,6 @@ class NConfigLayout(QGridLayout):
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, NColourLabel):
- qHelp.setText(text)
- return
-
##
# Class Methods
##
@@ -310,17 +302,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