Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion novelwriter/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "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",
Expand Down Expand Up @@ -239,6 +240,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

Expand Down
54 changes: 54 additions & 0 deletions novelwriter/core/projectdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import logging
import uuid


from datetime import date
from typing import TYPE_CHECKING, Any

from novelwriter.common import (
Expand Down Expand Up @@ -61,6 +63,10 @@ def __init__(self, project: NWProject) -> None:

# Project Settings
self._doBackup = True
self._projGoal = 1
self._projDeadline = date.today()
self._sessGoal = 1
self._sessGoalAuto = False
self._language = None
self._spellCheck = False
self._spellLang = None
Expand Down Expand Up @@ -132,6 +138,26 @@ 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) -> date | 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:
"""Return the project language setting."""
Expand Down Expand Up @@ -260,6 +286,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, 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:
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:
Expand Down
14 changes: 14 additions & 0 deletions novelwriter/core/projectxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import logging
import xml.etree.ElementTree as ET

import datetime
from enum import Enum
from pathlib import Path
from time import time
Expand Down Expand Up @@ -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 = datetime.date.fromisoformat(xItem.text)
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":
Expand Down Expand Up @@ -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.isoformat())
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)
Expand Down
87 changes: 83 additions & 4 deletions novelwriter/dialogs/projectsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@
import csv
import logging

from datetime import date
from pathlib import Path

from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
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
Expand All @@ -57,9 +58,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()

Expand Down Expand Up @@ -87,6 +89,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)
Expand All @@ -101,12 +104,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)
Expand Down Expand Up @@ -162,6 +167,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:
Expand All @@ -179,13 +186,29 @@ 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().toPyDate()
sessGoalAuto = self.goalsPage.sessGoalAuto.isChecked()
if sessGoalAuto:
days_remaining = (projDeadline - date.today()).days
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())
Expand Down Expand Up @@ -297,6 +320,62 @@ 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):

C_DATA = 0
Expand Down
90 changes: 90 additions & 0 deletions novelwriter/extensions/progressbars.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"""
from __future__ import annotations

import logging

from math import ceil

from PyQt6.QtCore import QRect
Expand All @@ -35,6 +37,8 @@
QtTransparent
)

logger = logging.getLogger(__name__)


class NProgressCircle(QProgressBar):
"""Extension: Circular Progress Widget
Expand Down Expand Up @@ -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
Loading