From 7e56174b2a43048039a2544711a902c05772e3e4 Mon Sep 17 00:00:00 2001 From: Blayney Walshe Date: Thu, 20 Feb 2025 08:29:26 -0500 Subject: [PATCH 1/4] added session and project goal with progress bars for each in the status bar --- novelwriter/config.py | 4 +- novelwriter/core/projectdata.py | 53 ++++++++++++ novelwriter/core/projectxml.py | 14 ++++ novelwriter/dialogs/projectsettings.py | 85 +++++++++++++++++-- novelwriter/gui/statusbar.py | 108 ++++++++++++++++++++++++- novelwriter/guimain.py | 3 +- 6 files changed, 258 insertions(+), 9 deletions(-) diff --git a/novelwriter/config.py b/novelwriter/config.py index 15bce92be..0dafde079 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -79,7 +79,7 @@ class Config: "narratorDialog", "altDialogOpen", "altDialogClose", "highlightEmph", "stopWhenIdle", "userIdleTime", "incNotesWCount", "fmtApostrophe", "fmtSQuoteOpen", "fmtSQuoteClose", "fmtDQuoteOpen", "fmtDQuoteClose", "fmtPadBefore", "fmtPadAfter", "fmtPadThin", - "spellLanguage", "showViewerPanel", "showEditToolBar", "showSessionTime", "viewComments", + "spellLanguage", "showViewerPanel", "showEditToolBar", "showSessionTime", "showSessionGoal", "showProjectGoal", "viewComments", "viewSynopsis", "searchCase", "searchWord", "searchRegEx", "searchLoop", "searchNextFile", "searchMatchCap", "searchProjCase", "searchProjWord", "searchProjRegEx", "verQtString", "verQtValue", "verPyQtString", "verPyQtValue", "verPyString", "osType", "osLinux", @@ -239,6 +239,8 @@ def __init__(self) -> None: self.showViewerPanel = True # The panel for the viewer is visible self.showEditToolBar = False # The document editor toolbar visibility self.showSessionTime = True # Show the session time in the status bar + self.showSessionGoal = True # Show the session goal in the status bar + self.showProjectGoal = True # Show the project goal in the status bar self.viewComments = True # Comments are shown in the viewer self.viewSynopsis = True # Synopsis is shown in the viewer diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index 60bee01fe..50df58ede 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -27,6 +27,7 @@ import uuid from typing import TYPE_CHECKING, Any +from PyQt6.QtCore import QDate from novelwriter.common import ( checkBool, checkInt, checkStringNone, checkUuid, isHandle, @@ -61,6 +62,10 @@ def __init__(self, project: NWProject) -> None: # Project Settings self._doBackup = True + self._projGoal = 1 + self._projDeadline = QDate.currentDate() + self._sessGoal = 1 + self._sessGoalAuto = False self._language = None self._spellCheck = False self._spellLang = None @@ -131,6 +136,26 @@ def editTime(self) -> int: def doBackup(self) -> bool: """Return the backup setting.""" return self._doBackup + + @property + def projGoal(self) -> int: + """Return the project goal.""" + return self._projGoal + + @property + def projDeadline(self) -> QDate | None: + """Return the project deadline.""" + return self._projDeadline + + @property + def sessGoal(self) -> int: + """Return the session goal.""" + return self._sessGoal + + @property + def sessGoalAuto(self) -> bool: + """Return the automatic session goal setting.""" + return self._sessGoalAuto @property def language(self) -> str | None: @@ -260,6 +285,34 @@ def setDoBackup(self, value: Any) -> None: self._project.setProjectChanged(True) return + def setProjGoal(self, value: Any) -> None: + """Set the project goal.""" + if value != self._projGoal: + self._projGoal = checkInt(value, self._projGoal) + self._project.setProjectChanged(True) + return + + def setProjDeadline(self, value: Any) -> None: + """Set the project deadline.""" + if value != self._projDeadline and isinstance(value, QDate): + self._projDeadline = value + self._project.setProjectChanged(True) + return + + def setSessGoal(self, value: Any) -> None: + """Set the session goal.""" + if value != self._sessGoal: + self._sessGoal = checkInt(value, self._sessGoal) + self._project.setProjectChanged(True) + return + + def setSessGoalAuto(self, value: Any) -> None: + """Set the session goal.""" + if value != self._sessGoalAuto: + self._sessGoalAuto = checkBool(value, False) + self._project.setProjectChanged(True) + return + def setLanguage(self, value: str | None) -> None: """Set the project language.""" if value != self._language: diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 033bd463a..840c3b051 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -32,6 +32,7 @@ from pathlib import Path from time import time from typing import TYPE_CHECKING +from PyQt6.QtCore import QDate from novelwriter import __hexversion__, __version__ from novelwriter.common import ( @@ -259,6 +260,15 @@ def _parseProjectSettings(self, xSection: ET.Element, data: NWProjectData) -> No for xItem in xSection: if xItem.tag == "doBackup": data.setDoBackup(xItem.text) + elif xItem.tag == "projGoal": + data.setProjGoal(xItem.text) + elif xItem.tag == "projDeadline": + date = QDate.fromString(xItem.text, "yyyy-MM-dd") + data.setProjDeadline(date) + elif xItem.tag == "sessGoalAuto": + data.setSessGoalAuto(xItem.text) + elif xItem.tag == "sessGoal": + data.setSessGoal(xItem.text) elif xItem.tag == "language": data.setLanguage(xItem.text) elif xItem.tag == "spellChecking": @@ -510,6 +520,10 @@ def write(self, data: NWProjectData, content: list, saveTime: float, editTime: i # Save Project Settings xSettings = ET.SubElement(xRoot, "settings") self._packSingleValue(xSettings, "doBackup", yesNo(data.doBackup)) + self._packSingleValue(xSettings, "projGoal", data.projGoal) + self._packSingleValue(xSettings, "projDeadline", data.projDeadline.toString("yyyy-MM-dd")) + self._packSingleValue(xSettings, "sessGoalAuto", yesNo(data.sessGoalAuto)) + self._packSingleValue(xSettings, "sessGoal", data.sessGoal) self._packSingleValue(xSettings, "language", data.language) self._packSingleValue(xSettings, "spellChecking", data.spellLang, attrib={ "auto": yesNo(data.spellCheck) diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index beaaf4bac..9016d5ae8 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -29,12 +29,12 @@ from pathlib import Path -from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot +from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot, QDate from PyQt6.QtGui import QCloseEvent, QColor from PyQt6.QtWidgets import ( QAbstractItemView, QApplication, QColorDialog, QDialogButtonBox, QFileDialog, QGridLayout, QHBoxLayout, QLineEdit, QMenu, QStackedWidget, - QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget + QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, QDateEdit ) from novelwriter import CONFIG, SHARED @@ -57,9 +57,10 @@ class GuiProjectSettings(NDialog): PAGE_SETTINGS = 0 - PAGE_STATUS = 1 - PAGE_IMPORT = 2 - PAGE_REPLACE = 3 + PAGE_GOALS = 1 + PAGE_STATUS = 2 + PAGE_IMPORT = 3 + PAGE_REPLACE = 4 newProjectSettingsReady = pyqtSignal() @@ -87,6 +88,7 @@ def __init__(self, parent: QWidget, gotoPage: int = PAGE_SETTINGS) -> None: self.sidebar = NPagedSideBar(self) self.sidebar.setLabelColor(SHARED.theme.helpText) self.sidebar.addButton(self.tr("Settings"), self.PAGE_SETTINGS) + self.sidebar.addButton(self.tr("Goals"), self.PAGE_GOALS) self.sidebar.addButton(self.tr("Status"), self.PAGE_STATUS) self.sidebar.addButton(self.tr("Importance"), self.PAGE_IMPORT) self.sidebar.addButton(self.tr("Auto-Replace"), self.PAGE_REPLACE) @@ -101,12 +103,14 @@ def __init__(self, parent: QWidget, gotoPage: int = PAGE_SETTINGS) -> None: SHARED.project.countStatus() self.settingsPage = _SettingsPage(self) + self.goalsPage = _GoalsPage(self) self.statusPage = _StatusPage(self, True) self.importPage = _StatusPage(self, False) self.replacePage = _ReplacePage(self) self.mainStack = QStackedWidget(self) self.mainStack.addWidget(self.settingsPage) + self.mainStack.addWidget(self.goalsPage) self.mainStack.addWidget(self.statusPage) self.mainStack.addWidget(self.importPage) self.mainStack.addWidget(self.replacePage) @@ -162,6 +166,8 @@ def _sidebarClicked(self, pageId: int) -> None: """Process a user request to switch page.""" if pageId == self.PAGE_SETTINGS: self.mainStack.setCurrentWidget(self.settingsPage) + elif pageId == self.PAGE_GOALS: + self.mainStack.setCurrentWidget(self.goalsPage) elif pageId == self.PAGE_STATUS: self.mainStack.setCurrentWidget(self.statusPage) elif pageId == self.PAGE_IMPORT: @@ -179,13 +185,30 @@ def _doSave(self) -> None: projLang = self.settingsPage.projLang.currentData() spellLang = self.settingsPage.spellLang.currentData() doBackup = not self.settingsPage.noBackup.isChecked() + projGoal = int(self.goalsPage.projGoal.text()) + projDeadline = self.goalsPage.projDeadline.date() + sessGoalAuto = self.goalsPage.sessGoalAuto.isChecked() + if sessGoalAuto: + days_remaining = QDate.currentDate().daysTo(projDeadline) + if days_remaining == 0: + days_remaining == 1 + sessGoal = projGoal // days_remaining + else: + sessGoal = int(self.goalsPage.sessGoal.text()) project.data.setName(projName) project.data.setAuthor(projAuthor) project.data.setDoBackup(doBackup) + project.data.setProjGoal(projGoal) + project.data.setProjDeadline(projDeadline) + project.data.setSessGoalAuto(sessGoalAuto) + project.data.setSessGoal(sessGoal) project.data.setSpellLang(spellLang) project.setProjectLang(projLang) + SHARED.mainGui.mainStatus.setGoals(projGoal, sessGoal) + + if self.statusPage.changed: logger.debug("Updating status labels") project.updateStatus("s", self.statusPage.getNewList()) @@ -296,6 +319,58 @@ def __init__(self, parent: QWidget) -> None: return +class _GoalsPage(NScrollableForm): + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + + data = SHARED.project.data + self.setHelpTextStyle(SHARED.theme.helpText) + self.setRowIndent(0) + + # Project Goals + self.projGoal = QLineEdit(self) + self.projGoal.setMaxLength(200) + self.projGoal.setMinimumWidth(200) + self.projGoal.setText(str(data.projGoal)) + self.addRow( + self.tr("Project Goal"), self.projGoal, + self.tr("Full project goal in words."), + stretch=(3, 2) + ) + + # Project Deadline + self.projDeadline = QDateEdit(self, date=data.projDeadline) + self.projDeadline.setMinimumWidth(200) + self.addRow( + self.tr("Target Date"), self.projDeadline, + self.tr("Date to complete the project."), + stretch=(3, 2) + ) + + # Auto configure Session Goal + self.sessGoalAuto = NSwitch(self) + self.sessGoalAuto.setChecked(data.sessGoalAuto) + self.addRow( + self.tr("Auto populate session goal?"), self.sessGoalAuto, + self.tr("Calculate Session goal based on target date assuming daily sessions. Overrides configured session goal below.") + ) + + # Session Goal + self.sessGoal = QLineEdit(self) + self.sessGoal.setMaxLength(200) + self.sessGoal.setMinimumWidth(200) + self.sessGoal.setText(str(data.sessGoal)) + self.addRow( + self.tr("Session Goal"), self.sessGoal, + self.tr("Session goal in words."), + stretch=(3, 2) + ) + + + self.finalise() + + return class _StatusPage(NFixedPage): diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index 4f84b8c37..a2e4c6024 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -28,8 +28,8 @@ from datetime import datetime from time import time -from PyQt6.QtCore import QLocale, pyqtSlot -from PyQt6.QtWidgets import QApplication, QLabel, QStatusBar, QWidget +from PyQt6.QtCore import QLocale, pyqtSlot, QDate +from PyQt6.QtWidgets import QApplication, QLabel, QStatusBar, QWidget, QProgressBar from novelwriter import CONFIG, SHARED from novelwriter.common import formatTime @@ -39,6 +39,42 @@ logger = logging.getLogger(__name__) +default_style = """ + QProgressBar { + border: 2px solid grey; + border-radius: 5px; + text-align: center; + } + QProgressBar::chunk { + background-color: #007ACC; + width: 20px; + } +""" + +complete_style = """ + QProgressBar { + border: 2px solid grey; + border-radius: 5px; + text-align: center; + } + QProgressBar::chunk { + background-color: green; + width: 20px; + } +""" + +negative_style = """ + QProgressBar { + border: 2px solid red; + border-radius: 5px; + text-align: center; + } + QProgressBar::chunk { + background-color: #007ACC; + width: 20px; + } +""" + class GuiMainStatus(QStatusBar): @@ -56,6 +92,27 @@ def __init__(self, parent: QWidget) -> None: # Permanent Widgets # ================= + # The session goal tracker widget + self.sessBar = QProgressBar(self) + self.sessBar.setMaximumSize(SHARED.theme.getTextWidth("Session: 0000/0000"),20) + self.sessBar.setValue(0) + self.sessBar.setMaximum(1) + self.sessBar.setFormat("Session: %v/%m") + self.sessBar.setStyleSheet(default_style) + self.sessBar.setVisible(CONFIG.showSessionGoal) + self.addPermanentWidget(self.sessBar) + + # The project goal tracker widget + self.projBar = QProgressBar(self) + self.projBar.setMaximumSize(SHARED.theme.getTextWidth("Session: 0000/0000"),20) + self.projBar.setValue(0) + self.projBar.setMaximum(1) + self.projBar.setFormat("Project: %p%") + self.projBar.setToolTip(f"Project Word Count: {self.projBar.value()}/{self.projBar.maximum()}") + self.projBar.setStyleSheet(default_style) + self.projBar.setVisible(CONFIG.showProjectGoal) + self.addPermanentWidget(self.projBar) + # The Spell Checker Language self.langIcon = QLabel("", self) self.langText = QLabel(self.tr("None"), self) @@ -173,12 +230,59 @@ def setUserIdle(self, idle: bool) -> None: def setProjectStats(self, pWC: int, sWC: int) -> None: """Update the current project statistics.""" + self.statsText.setText(self.tr("Words: {0} ({1})").format(f"{pWC:n}", f"{sWC:+n}")) + + if SHARED.project.data.sessGoalAuto: + days_remaining = QDate.currentDate().daysTo(SHARED.project.data.projDeadline) + if days_remaining == 0: + days_remaining == 1 + logger.debug("Days remaining: %d", days_remaining) + SHARED.project.data._sessGoal = SHARED.project.data.projGoal // days_remaining + SHARED.project.setProjectChanged(True) + sessGoal = SHARED.project.data.sessGoal + self.setGoals(SHARED.project.data.projGoal, int(sessGoal)) + + val = sWC if sWC > 0 else 0 + if val > self.sessBar.maximum(): + val = self.sessBar.maximum() + self.sessBar.setValue(val) + self.sessBar.setFormat(f"Session: {sWC}/%m") + if sWC >= sessGoal: + self.sessBar.setStyleSheet(complete_style) + elif sWC < 0: + self.sessBar.setStyleSheet(negative_style) + else: + self.sessBar.setStyleSheet(default_style) + + val = pWC if pWC > 0 else 0 + if val > self.projBar.maximum(): + val = self.projBar.maximum() + self.projBar.setValue(val) + self.projBar.setToolTip(f"Project Word Count: {pWC}/{self.projBar.maximum()}") + if pWC >= SHARED.project.data.projGoal: + self.projBar.setStyleSheet(complete_style) + elif pWC < 0: + self.projBar.setStyleSheet(negative_style) + else: + self.projBar.setStyleSheet(default_style) + + + logger.debug("Project Stats: %d (%d)", pWC, sWC) if CONFIG.incNotesWCount: self.statsText.setToolTip(self.tr("Project word count (session change)")) else: self.statsText.setToolTip(self.tr("Novel word count (session change)")) return + + def setGoals(self, projGoal: int, sessGoal: int) -> None: + """Set the project and session goals.""" + if sessGoal <= 0: + sessGoal = 1 + self.sessBar.setMaximum(sessGoal) + self.projBar.setMaximum(projGoal) + self.projBar.setToolTip(f"Project Word Count: {self.projBar.value()}/{self.projBar.maximum()}") + return def updateTime(self, idleTime: float = 0.0) -> None: """Update the session clock.""" diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 607bec502..d6ccb7cbd 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -30,7 +30,7 @@ from pathlib import Path from time import time -from PyQt6.QtCore import Qt, QTimer, pyqtSlot +from PyQt6.QtCore import Qt, QTimer, pyqtSlot, QDate from PyQt6.QtGui import QCloseEvent, QCursor, QIcon, QShortcut from PyQt6.QtWidgets import ( QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, @@ -1242,6 +1242,7 @@ def _autoSaveDocument(self) -> None: @pyqtSlot() def _updateStatusWordCount(self) -> None: """Update the word count on the status bar.""" + if not SHARED.hasProject: self.mainStatus.setProjectStats(0, 0) From cfc587569abb815c2f592a79563f156ea04beb2a Mon Sep 17 00:00:00 2001 From: Blayney Walshe Date: Sat, 22 Feb 2025 14:12:52 -0500 Subject: [PATCH 2/4] converted QDate to datetime.date Added NProgressGoal class to handle goal progress bars. --- .vscode/settings.json | 7 ++ novelwriter/config.py | 3 +- novelwriter/core/projectdata.py | 21 +++--- novelwriter/core/projectxml.py | 6 +- novelwriter/dialogs/projectsettings.py | 14 ++-- novelwriter/extensions/progressbars.py | 90 +++++++++++++++++++++++++ novelwriter/gui/statusbar.py | 91 +++++++++----------------- novelwriter/guimain.py | 2 +- 8 files changed, 152 insertions(+), 82 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..9b388533a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/novelwriter/config.py b/novelwriter/config.py index 0dafde079..4cb210a6b 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -79,7 +79,8 @@ class Config: "narratorDialog", "altDialogOpen", "altDialogClose", "highlightEmph", "stopWhenIdle", "userIdleTime", "incNotesWCount", "fmtApostrophe", "fmtSQuoteOpen", "fmtSQuoteClose", "fmtDQuoteOpen", "fmtDQuoteClose", "fmtPadBefore", "fmtPadAfter", "fmtPadThin", - "spellLanguage", "showViewerPanel", "showEditToolBar", "showSessionTime", "showSessionGoal", "showProjectGoal", "viewComments", + "spellLanguage", "showViewerPanel", "showEditToolBar", "showSessionTime", + "showSessionGoal", "showProjectGoal", "viewComments", "viewSynopsis", "searchCase", "searchWord", "searchRegEx", "searchLoop", "searchNextFile", "searchMatchCap", "searchProjCase", "searchProjWord", "searchProjRegEx", "verQtString", "verQtValue", "verPyQtString", "verPyQtValue", "verPyString", "osType", "osLinux", diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index 50df58ede..f55efe900 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -26,8 +26,9 @@ import logging import uuid + +from datetime import date from typing import TYPE_CHECKING, Any -from PyQt6.QtCore import QDate from novelwriter.common import ( checkBool, checkInt, checkStringNone, checkUuid, isHandle, @@ -63,7 +64,7 @@ def __init__(self, project: NWProject) -> None: # Project Settings self._doBackup = True self._projGoal = 1 - self._projDeadline = QDate.currentDate() + self._projDeadline = date.today() self._sessGoal = 1 self._sessGoalAuto = False self._language = None @@ -136,14 +137,14 @@ def editTime(self) -> int: def doBackup(self) -> bool: """Return the backup setting.""" return self._doBackup - + @property def projGoal(self) -> int: """Return the project goal.""" return self._projGoal - + @property - def projDeadline(self) -> QDate | None: + def projDeadline(self) -> date | None: """Return the project deadline.""" return self._projDeadline @@ -151,7 +152,7 @@ def projDeadline(self) -> QDate | None: def sessGoal(self) -> int: """Return the session goal.""" return self._sessGoal - + @property def sessGoalAuto(self) -> bool: """Return the automatic session goal setting.""" @@ -291,21 +292,21 @@ def setProjGoal(self, value: Any) -> None: self._projGoal = checkInt(value, self._projGoal) self._project.setProjectChanged(True) return - + def setProjDeadline(self, value: Any) -> None: """Set the project deadline.""" - if value != self._projDeadline and isinstance(value, QDate): + if value != self._projDeadline and isinstance(value, date): self._projDeadline = value self._project.setProjectChanged(True) return - + def setSessGoal(self, value: Any) -> None: """Set the session goal.""" if value != self._sessGoal: self._sessGoal = checkInt(value, self._sessGoal) self._project.setProjectChanged(True) return - + def setSessGoalAuto(self, value: Any) -> None: """Set the session goal.""" if value != self._sessGoalAuto: diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 840c3b051..cb02962b4 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -28,11 +28,11 @@ import logging import xml.etree.ElementTree as ET +import datetime from enum import Enum from pathlib import Path from time import time from typing import TYPE_CHECKING -from PyQt6.QtCore import QDate from novelwriter import __hexversion__, __version__ from novelwriter.common import ( @@ -263,7 +263,7 @@ def _parseProjectSettings(self, xSection: ET.Element, data: NWProjectData) -> No elif xItem.tag == "projGoal": data.setProjGoal(xItem.text) elif xItem.tag == "projDeadline": - date = QDate.fromString(xItem.text, "yyyy-MM-dd") + date = datetime.date.fromisoformat(xItem.text) data.setProjDeadline(date) elif xItem.tag == "sessGoalAuto": data.setSessGoalAuto(xItem.text) @@ -521,7 +521,7 @@ def write(self, data: NWProjectData, content: list, saveTime: float, editTime: i xSettings = ET.SubElement(xRoot, "settings") self._packSingleValue(xSettings, "doBackup", yesNo(data.doBackup)) self._packSingleValue(xSettings, "projGoal", data.projGoal) - self._packSingleValue(xSettings, "projDeadline", data.projDeadline.toString("yyyy-MM-dd")) + self._packSingleValue(xSettings, "projDeadline", data.projDeadline.isoformat()) self._packSingleValue(xSettings, "sessGoalAuto", yesNo(data.sessGoalAuto)) self._packSingleValue(xSettings, "sessGoal", data.sessGoal) self._packSingleValue(xSettings, "language", data.language) diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index 9016d5ae8..0f1dacf73 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -27,9 +27,10 @@ import csv import logging +from datetime import date from pathlib import Path -from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot, QDate +from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt6.QtGui import QCloseEvent, QColor from PyQt6.QtWidgets import ( QAbstractItemView, QApplication, QColorDialog, QDialogButtonBox, @@ -186,10 +187,10 @@ def _doSave(self) -> None: spellLang = self.settingsPage.spellLang.currentData() doBackup = not self.settingsPage.noBackup.isChecked() projGoal = int(self.goalsPage.projGoal.text()) - projDeadline = self.goalsPage.projDeadline.date() + projDeadline = self.goalsPage.projDeadline.date().toPyDate() sessGoalAuto = self.goalsPage.sessGoalAuto.isChecked() if sessGoalAuto: - days_remaining = QDate.currentDate().daysTo(projDeadline) + days_remaining = (projDeadline - date.today()).days if days_remaining == 0: days_remaining == 1 sessGoal = projGoal // days_remaining @@ -208,7 +209,6 @@ def _doSave(self) -> None: SHARED.mainGui.mainStatus.setGoals(projGoal, sessGoal) - if self.statusPage.changed: logger.debug("Updating status labels") project.updateStatus("s", self.statusPage.getNewList()) @@ -319,6 +319,7 @@ def __init__(self, parent: QWidget) -> None: return + class _GoalsPage(NScrollableForm): def __init__(self, parent: QWidget) -> None: @@ -353,7 +354,8 @@ def __init__(self, parent: QWidget) -> None: self.sessGoalAuto.setChecked(data.sessGoalAuto) self.addRow( self.tr("Auto populate session goal?"), self.sessGoalAuto, - self.tr("Calculate Session goal based on target date assuming daily sessions. Overrides configured session goal below.") + self.tr("Calculate Session goal based on target date assuming daily sessions. \ + Overrides configured session goal below.") ) # Session Goal @@ -367,11 +369,11 @@ def __init__(self, parent: QWidget) -> None: stretch=(3, 2) ) - self.finalise() return + class _StatusPage(NFixedPage): C_DATA = 0 diff --git a/novelwriter/extensions/progressbars.py b/novelwriter/extensions/progressbars.py index 456d15500..cfe21aead 100644 --- a/novelwriter/extensions/progressbars.py +++ b/novelwriter/extensions/progressbars.py @@ -24,6 +24,8 @@ """ from __future__ import annotations +import logging + from math import ceil from PyQt6.QtCore import QRect @@ -35,6 +37,8 @@ QtTransparent ) +logger = logging.getLogger(__name__) + class NProgressCircle(QProgressBar): """Extension: Circular Progress Widget @@ -126,3 +130,89 @@ def paintEvent(self, event: QPaintEvent) -> None: painter.setBrush(self.palette().highlight()) painter.drawRect(0, 0, progress, self.height()) return + + +class NProgressGoal(QProgressBar): + """Extension: Goal Progress Widget + + A custom widget that paints a progress bar with custom styling and text. + """ + + __slots__ = ( + "_text", "_point", "_dRect", "_cRect", "_dPen", "_dBrush", + "_cPen", "_bPen", "_tColor" + ) + + def __init__(self, parent: QWidget, width: int, height: int, point: int) -> None: + super().__init__(parent=parent) + logger.debug(self.palette()) + self._text = None + self._point = point + self._dRect = QRect(0, 0, width, height) + self._cRect = QRect(2 * point, 2 * point, width - 2 * point, height - 2 * point) + self._dPen = QPen(QtTransparent) + self._dBrush = QBrush(QtTransparent) + self.setColors( + track=self.palette().base().color().darker(300), + bar=self.palette().highlight().color(), + text=self.palette().text().color(), + back=self.palette().base().color().darker(300) + ) + self.setSizePolicy(QtSizeFixed, QtSizeFixed) + self.setFixedWidth(width) + self.setFixedHeight(height) + return + + def resetColors(self) -> None: + """Reset the colours to the default values.""" + self.setColors( + track=self.palette().base().color().darker(300), + bar=self.palette().highlight().color(), + text=self.palette().text().color(), + back=self.palette().base().color().darker(300) + ) + return + + def setColors( + self, back: QColor | None = None, track: QColor | None = None, + bar: QColor | None = None, text: QColor | None = None + ) -> None: + """Set the colours of the widget.""" + if isinstance(back, QColor): + self._dPen = QPen(back) + self._dBrush = QBrush(back) + if isinstance(bar, QColor): + logger.debug(f"Setting bar colour: {bar}") + self._cPen = QPen(QBrush(bar), 2 * self._point, QtSolidLine) + self._cBrush = QBrush(bar) + if isinstance(track, QColor): + logger.debug(f"Setting track colour: {track}") + self._bPen = QPen(QBrush(track), 2 * self._point, QtSolidLine, QtRoundCap) + if isinstance(text, QColor): + self._tColor = text + return + + def setCentreText(self, text: str | None) -> None: + """Replace the progress text with a custom string.""" + self._text = text + self.setValue(self.value()) # Triggers a redraw + return + + def paintEvent(self, event: QPaintEvent) -> None: + """Custom painter for the progress bar.""" + progress = self.value()/self.maximum() + painter = QPainter(self) + painter.setRenderHint(QtPaintAntiAlias, True) + + painter.setPen(self._bPen) + painter.setBrush(self._dBrush) + painter.drawRect(self._dRect) + + painter.setPen(self._cPen) + painter.setBrush(self._cBrush) + x, y = self._cRect.topLeft().x(), self._cRect.topLeft().y() + painter.drawRect(x, y, ceil(self._cRect.width() * progress), self._cRect.height()) + + painter.setPen(self._tColor) + painter.drawText(self._cRect, QtAlignCenter, self._text or f"{(progress*100):.1f} %") + return diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index a2e4c6024..5bb615479 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -25,56 +25,22 @@ import logging -from datetime import datetime +from datetime import datetime, date from time import time -from PyQt6.QtCore import QLocale, pyqtSlot, QDate -from PyQt6.QtWidgets import QApplication, QLabel, QStatusBar, QWidget, QProgressBar +from PyQt6.QtCore import QLocale, pyqtSlot +from PyQt6.QtGui import QColor +from PyQt6.QtWidgets import QApplication, QLabel, QStatusBar, QWidget from novelwriter import CONFIG, SHARED from novelwriter.common import formatTime from novelwriter.constants import nwConst from novelwriter.extensions.modified import NClickableLabel +from novelwriter.extensions.progressbars import NProgressGoal from novelwriter.extensions.statusled import StatusLED logger = logging.getLogger(__name__) -default_style = """ - QProgressBar { - border: 2px solid grey; - border-radius: 5px; - text-align: center; - } - QProgressBar::chunk { - background-color: #007ACC; - width: 20px; - } -""" - -complete_style = """ - QProgressBar { - border: 2px solid grey; - border-radius: 5px; - text-align: center; - } - QProgressBar::chunk { - background-color: green; - width: 20px; - } -""" - -negative_style = """ - QProgressBar { - border: 2px solid red; - border-radius: 5px; - text-align: center; - } - QProgressBar::chunk { - background-color: #007ACC; - width: 20px; - } -""" - class GuiMainStatus(QStatusBar): @@ -93,23 +59,19 @@ def __init__(self, parent: QWidget) -> None: # ================= # The session goal tracker widget - self.sessBar = QProgressBar(self) - self.sessBar.setMaximumSize(SHARED.theme.getTextWidth("Session: 0000/0000"),20) + self.sessBar = NProgressGoal(self, SHARED.theme.getTextWidth("Session: 0000/0000"), 20, 1) self.sessBar.setValue(0) self.sessBar.setMaximum(1) - self.sessBar.setFormat("Session: %v/%m") - self.sessBar.setStyleSheet(default_style) self.sessBar.setVisible(CONFIG.showSessionGoal) self.addPermanentWidget(self.sessBar) # The project goal tracker widget - self.projBar = QProgressBar(self) - self.projBar.setMaximumSize(SHARED.theme.getTextWidth("Session: 0000/0000"),20) + self.projBar = NProgressGoal(self, SHARED.theme.getTextWidth("Session: 0000/0000"), 20, 1) self.projBar.setValue(0) self.projBar.setMaximum(1) - self.projBar.setFormat("Project: %p%") - self.projBar.setToolTip(f"Project Word Count: {self.projBar.value()}/{self.projBar.maximum()}") - self.projBar.setStyleSheet(default_style) + self.projBar.setToolTip( + f"Project Word Count: {self.projBar.value()}/{self.projBar.maximum()}" + ) self.projBar.setVisible(CONFIG.showProjectGoal) self.addPermanentWidget(self.projBar) @@ -234,7 +196,7 @@ def setProjectStats(self, pWC: int, sWC: int) -> None: self.statsText.setText(self.tr("Words: {0} ({1})").format(f"{pWC:n}", f"{sWC:+n}")) if SHARED.project.data.sessGoalAuto: - days_remaining = QDate.currentDate().daysTo(SHARED.project.data.projDeadline) + days_remaining = (SHARED.project.data.projDeadline - date.today()).days if days_remaining == 0: days_remaining == 1 logger.debug("Days remaining: %d", days_remaining) @@ -247,26 +209,30 @@ def setProjectStats(self, pWC: int, sWC: int) -> None: if val > self.sessBar.maximum(): val = self.sessBar.maximum() self.sessBar.setValue(val) - self.sessBar.setFormat(f"Session: {sWC}/%m") + self.sessBar.setCentreText(f"Session: {sWC}/{self.sessBar.maximum()}") + self.sessBar.resetColors() if sWC >= sessGoal: - self.sessBar.setStyleSheet(complete_style) + self.sessBar.setColors(bar=QColor(SHARED.theme.getIconColor("green")).darker(150)) elif sWC < 0: - self.sessBar.setStyleSheet(negative_style) - else: - self.sessBar.setStyleSheet(default_style) + self.sessBar.setColors( + track=QColor(SHARED.theme.getIconColor("red")), + bar=QColor(SHARED.theme.getIconColor("red")) + ) val = pWC if pWC > 0 else 0 if val > self.projBar.maximum(): val = self.projBar.maximum() self.projBar.setValue(val) self.projBar.setToolTip(f"Project Word Count: {pWC}/{self.projBar.maximum()}") + self.projBar.setCentreText(f"Project: {pWC/self.projBar.maximum():.0%}") + self.projBar.resetColors() if pWC >= SHARED.project.data.projGoal: - self.projBar.setStyleSheet(complete_style) + self.projBar.setColors(bar=QColor(SHARED.theme.getIconColor("green")).darker(150)) elif pWC < 0: - self.projBar.setStyleSheet(negative_style) - else: - self.projBar.setStyleSheet(default_style) - + self.projBar.setColors( + track=QColor(SHARED.theme.getIconColor("red")), + bar=QColor(SHARED.theme.getIconColor("red")) + ) logger.debug("Project Stats: %d (%d)", pWC, sWC) if CONFIG.incNotesWCount: @@ -274,14 +240,17 @@ def setProjectStats(self, pWC: int, sWC: int) -> None: else: self.statsText.setToolTip(self.tr("Novel word count (session change)")) return - + def setGoals(self, projGoal: int, sessGoal: int) -> None: """Set the project and session goals.""" if sessGoal <= 0: sessGoal = 1 self.sessBar.setMaximum(sessGoal) self.projBar.setMaximum(projGoal) - self.projBar.setToolTip(f"Project Word Count: {self.projBar.value()}/{self.projBar.maximum()}") + self.projBar.setToolTip( + f"Project Word Count: {self.projBar.value()}/{self.projBar.maximum()}" + ) + self.sessBar.setCentreText(f"Session: {self.sessBar.value()}/{self.sessBar.maximum()}") return def updateTime(self, idleTime: float = 0.0) -> None: diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index d6ccb7cbd..22f81c0f8 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -30,7 +30,7 @@ from pathlib import Path from time import time -from PyQt6.QtCore import Qt, QTimer, pyqtSlot, QDate +from PyQt6.QtCore import Qt, QTimer, pyqtSlot from PyQt6.QtGui import QCloseEvent, QCursor, QIcon, QShortcut from PyQt6.QtWidgets import ( QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, From 02cb9e90905d5423c467d631dfd375a244bdaa3b Mon Sep 17 00:00:00 2001 From: Blayney Walshe Date: Sat, 22 Feb 2025 15:17:36 -0500 Subject: [PATCH 3/4] Update novelwriter/dialogs/projectsettings.py Co-authored-by: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> --- novelwriter/dialogs/projectsettings.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index 0f1dacf73..dfd58febd 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -354,8 +354,10 @@ def __init__(self, parent: QWidget) -> None: self.sessGoalAuto.setChecked(data.sessGoalAuto) self.addRow( self.tr("Auto populate session goal?"), self.sessGoalAuto, - self.tr("Calculate Session goal based on target date assuming daily sessions. \ - Overrides configured session goal below.") + self.tr( + "Calculate Session goal based on target date assuming daily sessions. " + "Overrides configured session goal below." + ) ) # Session Goal From 58bbb2be38941560bbdad70d95ccc9f957579ab2 Mon Sep 17 00:00:00 2001 From: Blayney Walshe Date: Sat, 22 Feb 2025 15:22:05 -0500 Subject: [PATCH 4/4] removed local config files --- .vscode/settings.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 9b388533a..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "python.testing.pytestArgs": [ - "tests" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true -} \ No newline at end of file