From a71d0d8cfe3bfdff0adbc0586e7b58803af122f7 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 20 Dec 2023 20:21:51 +0100
Subject: [PATCH] Add project fill options to new project page on welcome
dialog
---
.../assets/icons/typicons_dark/icons.conf | 1 +
.../icons/typicons_dark/typ_document.svg | 4 +
.../assets/icons/typicons_light/icons.conf | 1 +
.../icons/typicons_light/typ_document.svg | 4 +
novelwriter/config.py | 36 +++---
novelwriter/extensions/configlayout.py | 24 ++--
novelwriter/gui/sidebar.py | 3 +-
novelwriter/gui/theme.py | 6 +-
novelwriter/shared.py | 16 ++-
novelwriter/tools/welcome.py | 103 +++++++++++++++---
10 files changed, 145 insertions(+), 53 deletions(-)
create mode 100644 novelwriter/assets/icons/typicons_dark/typ_document.svg
create mode 100644 novelwriter/assets/icons/typicons_light/typ_document.svg
diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf
index d90e35a7..44317317 100644
--- a/novelwriter/assets/icons/typicons_dark/icons.conf
+++ b/novelwriter/assets/icons/typicons_dark/icons.conf
@@ -44,6 +44,7 @@ cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg
cls_world = typ_location.svg
cross = typ_times.svg
+document = typ_document.svg
down = typ_chevron-down.svg
edit = typ_pencil.svg
export = typ_export.svg
diff --git a/novelwriter/assets/icons/typicons_dark/typ_document.svg b/novelwriter/assets/icons/typicons_dark/typ_document.svg
new file mode 100644
index 00000000..20121f32
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_dark/typ_document.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf
index 757567a4..7579bb70 100644
--- a/novelwriter/assets/icons/typicons_light/icons.conf
+++ b/novelwriter/assets/icons/typicons_light/icons.conf
@@ -44,6 +44,7 @@ cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg
cls_world = typ_location.svg
cross = typ_times.svg
+document = typ_document.svg
down = typ_chevron-down.svg
edit = typ_pencil.svg
export = typ_export.svg
diff --git a/novelwriter/assets/icons/typicons_light/typ_document.svg b/novelwriter/assets/icons/typicons_light/typ_document.svg
new file mode 100644
index 00000000..3d42b98b
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_light/typ_document.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/config.py b/novelwriter/config.py
index 501c7f22..000c85c1 100644
--- a/novelwriter/config.py
+++ b/novelwriter/config.py
@@ -421,10 +421,10 @@ class Config:
"""Compile and return error messages from the initialisation of
the Config class, and clear the error buffer.
"""
- errMessage = "
".join(self._errData)
+ message = "
".join(self._errData)
self._hasError = False
self._errData = []
- return errMessage
+ return message
def listLanguages(self, lngSet: int) -> list[tuple[str, str]]:
"""List localisation files in the i18n folder. The default GUI
@@ -647,7 +647,7 @@ class Config:
conf = NWConfigParser()
conf["Meta"] = {
- "timestamp": formatTimeStamp(time()),
+ "timestamp": formatTimeStamp(time()),
}
conf["Main"] = {
@@ -787,22 +787,20 @@ class RecentProjects:
self._data = {}
cacheFile = self._conf.dataPath(nwFiles.RECENT_FILE)
- if not cacheFile.is_file():
- return True
-
- try:
- with open(cacheFile, mode="r", encoding="utf-8") as inFile:
- theData = json.load(inFile)
- for projPath, theEntry in theData.items():
- self._data[projPath] = {
- "title": theEntry.get("title", ""),
- "words": theEntry.get("words", 0),
- "time": theEntry.get("time", 0),
- }
- except Exception:
- logger.error("Could not load recent project cache")
- logException()
- return False
+ if cacheFile.is_file():
+ try:
+ with open(cacheFile, mode="r", encoding="utf-8") as inFile:
+ data = json.load(inFile)
+ for path, entry in data.items():
+ self._data[path] = {
+ "title": entry.get("title", ""),
+ "words": entry.get("words", 0),
+ "time": entry.get("time", 0),
+ }
+ except Exception:
+ logger.error("Could not load recent project cache")
+ logException()
+ return False
return True
diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py
index 3d23fede..82abaf84 100644
--- a/novelwriter/extensions/configlayout.py
+++ b/novelwriter/extensions/configlayout.py
@@ -33,6 +33,8 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG
FONT_SCALE = 0.9
+RIGHT_TOP = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop
+LEFT_TOP = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
class NConfigLayout(QGridLayout):
@@ -103,31 +105,31 @@ class NConfigLayout(QGridLayout):
labelBox.addWidget(qHelp)
labelBox.setSpacing(0)
labelBox.addStretch(1)
- self.addLayout(labelBox, self._nextRow, 0, 1, 1, Qt.AlignLeft | Qt.AlignTop)
+ self.addLayout(labelBox, self._nextRow, 0, 1, 1, LEFT_TOP)
else:
- self.addWidget(qLabel, self._nextRow, 0, 1, 1, Qt.AlignLeft | Qt.AlignTop)
+ self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP)
if isinstance(unit, str):
controlBox = QHBoxLayout()
controlBox.addWidget(widget, 0, Qt.AlignVCenter)
controlBox.addWidget(QLabel(unit), 0, Qt.AlignVCenter)
controlBox.setSpacing(wSp)
- self.addLayout(controlBox, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignTop)
+ self.addLayout(controlBox, self._nextRow, 1, 1, 1, RIGHT_TOP)
elif isinstance(button, QAbstractButton):
controlBox = QHBoxLayout()
controlBox.addWidget(widget, 0, Qt.AlignVCenter)
controlBox.addWidget(button, 0, Qt.AlignVCenter)
controlBox.setSpacing(wSp)
- self.addLayout(controlBox, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignTop)
+ self.addLayout(controlBox, self._nextRow, 1, 1, 1, RIGHT_TOP)
else:
if isinstance(widget, QLineEdit):
qLayout = QHBoxLayout()
qLayout.addWidget(widget)
- self.addLayout(qLayout, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignTop)
+ self.addLayout(qLayout, self._nextRow, 1, 1, 1, RIGHT_TOP)
else:
- self.addWidget(widget, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignTop)
+ self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP)
self.setRowStretch(self._nextRow, 0)
self.setRowStretch(self._nextRow+1, 1)
@@ -145,14 +147,14 @@ class NSimpleLayout(QGridLayout):
column layout.
"""
- def __init__(self) -> None:
+ def __init__(self, stretcColumn: int = 0) -> None:
super().__init__()
self._nextRow = 0
wSp = CONFIG.pxInt(8)
self.setHorizontalSpacing(wSp)
self.setVerticalSpacing(wSp)
- self.setColumnStretch(0, 1)
+ self.setColumnStretch(stretcColumn, 1)
return
@@ -176,14 +178,14 @@ class NSimpleLayout(QGridLayout):
wSp = CONFIG.pxInt(8)
qLabel = QLabel(label)
qLabel.setIndent(wSp)
- self.addWidget(qLabel, self._nextRow, 0, 1, 1, Qt.AlignLeft | Qt.AlignTop)
+ 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, Qt.AlignRight | Qt.AlignTop)
+ self.addLayout(qLayout, self._nextRow, 1, 1, 1, RIGHT_TOP)
else:
- self.addWidget(widget, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignTop)
+ self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP)
qLabel.setBuddy(widget)
diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py
index 11f61c27..1026b974 100644
--- a/novelwriter/gui/sidebar.py
+++ b/novelwriter/gui/sidebar.py
@@ -167,8 +167,7 @@ class _PopRightMenu(QMenu):
def event(self, event: QEvent) -> bool:
"""Overload the show event and move the menu popup location."""
if event.type() == QEvent.Show:
- parent = self.parent()
- if isinstance(parent, QWidget):
+ if isinstance(parent := self.parent(), QWidget):
offset = QPoint(parent.width(), parent.height() - self.height())
self.move(parent.mapToGlobal(offset))
return super(_PopRightMenu, self).event(event)
diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py
index dc42eca5..6c8b4fca 100644
--- a/novelwriter/gui/theme.py
+++ b/novelwriter/gui/theme.py
@@ -467,9 +467,9 @@ class GuiIcons:
# General Button Icons
"add", "add_document", "backward", "bookmark", "browse", "checked", "close", "cross",
- "down", "edit", "export", "forward", "maximise", "menu", "minimise", "noncheckable",
- "panel", "refresh", "remove", "revert", "search_replace", "search", "settings", "star",
- "unchecked", "up", "view",
+ "document", "down", "edit", "export", "forward", "maximise", "menu", "minimise",
+ "noncheckable", "panel", "refresh", "remove", "revert", "search_replace", "search",
+ "settings", "star", "unchecked", "up", "view",
# Switches
"sticky-on", "sticky-off",
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index 7df04ec6..3afe10b0 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -31,8 +31,9 @@ from typing import TYPE_CHECKING
from pathlib import Path
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal
-from PyQt5.QtWidgets import QMessageBox, QWidget
+from PyQt5.QtWidgets import QFileDialog, QMessageBox, QWidget
+from novelwriter.constants import nwFiles
from novelwriter.core.spellcheck import NWSpellEnchant
if TYPE_CHECKING: # pragma: no cover
@@ -215,6 +216,19 @@ class SharedData(QObject):
QThreadPool.globalInstance().start(runnable, priority=priority)
return
+ def getProjectPath(self, parent: QWidget, allowZip: bool = False) -> Path | None:
+ """Open the file dialog and select a novelWriter project file."""
+ ext = []
+ ext.append(self.tr("novelWriter Project File ({0})").format(nwFiles.PROJ_FILE))
+ if allowZip:
+ ext.append(self.tr("Zip Archives ({0})").format("*.zip"))
+ ext.append(self.tr("All files ({0})").format("*"))
+
+ projFile, _ = QFileDialog.getOpenFileName(
+ parent, self.tr("Open Project"), "", filter=";;".join(ext)
+ )
+ return Path(projFile) if projFile else None
+
##
# Signal Proxy
##
diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py
index 1048bf2c..b3ed009f 100644
--- a/novelwriter/tools/welcome.py
+++ b/novelwriter/tools/welcome.py
@@ -30,11 +30,11 @@ from pathlib import Path
from datetime import datetime
from PyQt5.QtGui import QCloseEvent, QPaintEvent, QPainter
-from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
+from PyQt5.QtCore import QEvent, QPoint, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout,
- QHBoxLayout, QLabel, QLineEdit, QPushButton, QScrollArea, QSpinBox,
- QStackedWidget, QTreeWidget, QVBoxLayout, QWidget
+ QHBoxLayout, QLabel, QLineEdit, QMenu, QPushButton, QScrollArea, QSpinBox,
+ QStackedWidget, QToolButton, QTreeWidget, QVBoxLayout, QWidget
)
from novelwriter import CONFIG, SHARED, __version__, __date__
@@ -265,18 +265,17 @@ class _NewProjectForm(QWidget):
FILL_BLANK = 0
FILL_SAMPLE = 1
- FILL_TEMPLATE = 2
+ FILL_COPY = 2
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
self._basePath = CONFIG.lastPath()
self._fillMode = self.FILL_BLANK
- self._tmplPath = None
+ self._copyPath = None
# Project Settings
# ================
- posLast = QLineEdit.ActionPosition.TrailingPosition
# Project Name
self.projName = QLineEdit()
@@ -303,22 +302,51 @@ class _NewProjectForm(QWidget):
# Project Path
self.projPath = QLineEdit(self)
self.projPath.setReadOnly(True)
- self.projPath.setPlaceholderText(self.tr("Required"))
- action = self.projPath.addAction(SHARED.theme.getIcon("browse"), posLast)
- action.triggered.connect(self._doBrowse)
+
+ self.browsePath = QToolButton(self)
+ self.browsePath.setIcon(SHARED.theme.getIcon("browse"))
+ self.browsePath.clicked.connect(self._doBrowse)
+
+ self.pathBox = QHBoxLayout()
+ self.pathBox.addWidget(self.projPath)
+ self.pathBox.addWidget(self.browsePath)
# Fill Project
self.projFill = QLineEdit(self)
self.projFill.setReadOnly(True)
- action = self.projFill.addAction(SHARED.theme.getIcon("add_document"), posLast)
+
+ self.browseFill = QToolButton(self)
+ self.browseFill.setIcon(SHARED.theme.getIcon("add_document"))
+
+ self.fillMenu = _PopLeftDirectionMenu(self.browseFill)
+
+ self.fillBlank = self.fillMenu.addAction(self.tr("Create a fresh project"))
+ self.fillBlank.setIcon(SHARED.theme.getIcon("document"))
+ self.fillBlank.triggered.connect(self._setFillBlank)
+
+ self.fillSample = self.fillMenu.addAction(self.tr("Create an example project"))
+ self.fillSample.setIcon(SHARED.theme.getIcon("add_document"))
+ self.fillSample.triggered.connect(self._setFillSample)
+
+ self.fillCopy = self.fillMenu.addAction(self.tr("Copy an existing project"))
+ self.fillCopy.setIcon(SHARED.theme.getIcon("browse"))
+ self.fillCopy.triggered.connect(self._setFillCopy)
+
+ self.browseFill.setMenu(self.fillMenu)
+ self.browseFill.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
+
+ self.fillBox = QHBoxLayout()
+ self.fillBox.addWidget(self.projFill)
+ self.fillBox.addWidget(self.browseFill)
# Project Form
self.projectForm = QFormLayout()
+ self.projectForm.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.projectForm.addRow(self.tr("Project Name"), self.projName)
- self.projectForm.addRow(self.tr("Author(s)"), self.projAuthor)
+ self.projectForm.addRow(self.tr("Author"), self.projAuthor)
self.projectForm.addRow(self.tr("Language"), self.projLang)
- self.projectForm.addRow(self.tr("Project Path"), self.projPath)
- self.projectForm.addRow(self.tr("Prefill Project"), self.projFill)
+ self.projectForm.addRow(self.tr("Project Path"), self.pathBox)
+ self.projectForm.addRow(self.tr("Prefill Project"), self.fillBox)
# Chapters and Scenes
# ===================
@@ -367,6 +395,7 @@ class _NewProjectForm(QWidget):
self.addNotes.setChecked(False)
self.notesForm = QFormLayout()
+ self.notesForm.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.notesForm.addRow(self.tr("Add a folder for plot notes"), self.addPlot)
self.notesForm.addRow(self.tr("Add a folder for character notes"), self.addChar)
self.notesForm.addRow(self.tr("Add a folder for location notes"), self.addWorld)
@@ -426,18 +455,45 @@ class _NewProjectForm(QWidget):
self.addNotes.setChecked(False)
return
+ @pyqtSlot()
+ def _setFillBlank(self) -> None:
+ """Set fill mode to blank project."""
+ self._fillMode = self.FILL_BLANK
+ self._updateFillInfo()
+ return
+
+ @pyqtSlot()
+ def _setFillSample(self) -> None:
+ """Set fill mode to sample project."""
+ self._fillMode = self.FILL_SAMPLE
+ self._updateFillInfo()
+ return
+
+ @pyqtSlot()
+ def _setFillCopy(self) -> None:
+ """Set fill mode to copy project."""
+ self._fillMode = self.FILL_COPY
+ self._copyPath = SHARED.getProjectPath(self, allowZip=True)
+ self._updateFillInfo()
+ return
+
##
# Internal Functions
##
def _updateFillInfo(self) -> None:
"""Update the text of the project fill box."""
+ text = ""
if self._fillMode == self.FILL_BLANK:
- self.projFill.setText(self.tr("Fresh Project"))
+ text = self.tr("Fresh Project")
elif self._fillMode == self.FILL_SAMPLE:
- self.projFill.setText(self.tr("Example Project"))
- elif self._fillMode == self.FILL_TEMPLATE:
- self.projFill.setText(self.tr("Template: {0}").format(str(self._tmplPath)))
+ text = self.tr("Example Project")
+ elif self._fillMode == self.FILL_COPY:
+ text = self.tr("Template: {0}").format(str(self._copyPath))
+
+ self.projFill.setText(text)
+ self.projFill.setToolTip(text)
+ self.projFill.setCursorPosition(0)
isBlank = self._fillMode == self.FILL_BLANK
self.numChapters.setEnabled(isBlank)
@@ -450,3 +506,16 @@ class _NewProjectForm(QWidget):
return
# END Class _NewProjectForm
+
+
+class _PopLeftDirectionMenu(QMenu):
+
+ def event(self, event: QEvent) -> bool:
+ """Overload the show event and move the menu popup location."""
+ if event.type() == QEvent.Show:
+ if isinstance(parent := self.parent(), QWidget):
+ offset = QPoint(parent.width() - self.width(), parent.height())
+ self.move(parent.mapToGlobal(offset))
+ return super(_PopLeftDirectionMenu, self).event(event)
+
+# END Class _PopLeftDirectionMenu