Add project fill options to new project page on welcome dialog

This commit is contained in:
Veronica Berglyd Olsen
2023-12-20 20:21:51 +01:00
parent 956abf3e8e
commit a71d0d8cfe
10 changed files with 145 additions and 53 deletions
@@ -44,6 +44,7 @@ cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg cls_trash = typ_trash.svg
cls_world = typ_location.svg cls_world = typ_location.svg
cross = typ_times.svg cross = typ_times.svg
document = typ_document.svg
down = typ_chevron-down.svg down = typ_chevron-down.svg
edit = typ_pencil.svg edit = typ_pencil.svg
export = typ_export.svg export = typ_export.svg
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m21.42 6.247-4.8889-4.8889c-0.22856-0.22978-0.539-0.35811-0.86411-0.35811h-9.7778c-2.0216 0-3.6667 1.6451-3.6667 3.6667v14.667c0 2.0216 1.6451 3.6667 3.6667 3.6667h12.222c2.0216 0 3.6667-1.6451 3.6667-3.6667v-12.222c0-0.32511-0.12833-0.63556-0.35811-0.86411zm-2.5923 0.86411h-1.3273c-1.0108 0-1.8333-0.82256-1.8333-1.8333v-1.3273zm-0.71622 13.444h-12.222c-0.67467 0-1.2222-0.54756-1.2222-1.2222v-14.667c0-0.67467 0.54756-1.2222 1.2222-1.2222h8.5556v1.8333c0 1.6854 1.3701 3.0556 3.0556 3.0556h1.8333v11c0 0.67467-0.54756 1.2222-1.2222 1.2222z" fill="#aeaeae" stroke-width="1.2222"/>
</svg>

After

Width:  |  Height:  |  Size: 737 B

@@ -44,6 +44,7 @@ cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg cls_trash = typ_trash.svg
cls_world = typ_location.svg cls_world = typ_location.svg
cross = typ_times.svg cross = typ_times.svg
document = typ_document.svg
down = typ_chevron-down.svg down = typ_chevron-down.svg
edit = typ_pencil.svg edit = typ_pencil.svg
export = typ_export.svg export = typ_export.svg
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m21.42 6.247-4.8889-4.8889c-0.22856-0.22978-0.539-0.35811-0.86411-0.35811h-9.7778c-2.0216 0-3.6667 1.6451-3.6667 3.6667v14.667c0 2.0216 1.6451 3.6667 3.6667 3.6667h12.222c2.0216 0 3.6667-1.6451 3.6667-3.6667v-12.222c0-0.32511-0.12833-0.63556-0.35811-0.86411zm-2.5923 0.86411h-1.3273c-1.0108 0-1.8333-0.82256-1.8333-1.8333v-1.3273zm-0.71622 13.444h-12.222c-0.67467 0-1.2222-0.54756-1.2222-1.2222v-14.667c0-0.67467 0.54756-1.2222 1.2222-1.2222h8.5556v1.8333c0 1.6854 1.3701 3.0556 3.0556 3.0556h1.8333v11c0 0.67467-0.54756 1.2222-1.2222 1.2222z" fill-opacity=".78039" stroke-width="1.2222"/>
</svg>

After

Width:  |  Height:  |  Size: 744 B

+17 -19
View File
@@ -421,10 +421,10 @@ class Config:
"""Compile and return error messages from the initialisation of """Compile and return error messages from the initialisation of
the Config class, and clear the error buffer. the Config class, and clear the error buffer.
""" """
errMessage = "<br>".join(self._errData) message = "<br>".join(self._errData)
self._hasError = False self._hasError = False
self._errData = [] self._errData = []
return errMessage return message
def listLanguages(self, lngSet: int) -> list[tuple[str, str]]: def listLanguages(self, lngSet: int) -> list[tuple[str, str]]:
"""List localisation files in the i18n folder. The default GUI """List localisation files in the i18n folder. The default GUI
@@ -647,7 +647,7 @@ class Config:
conf = NWConfigParser() conf = NWConfigParser()
conf["Meta"] = { conf["Meta"] = {
"timestamp": formatTimeStamp(time()), "timestamp": formatTimeStamp(time()),
} }
conf["Main"] = { conf["Main"] = {
@@ -787,22 +787,20 @@ class RecentProjects:
self._data = {} self._data = {}
cacheFile = self._conf.dataPath(nwFiles.RECENT_FILE) cacheFile = self._conf.dataPath(nwFiles.RECENT_FILE)
if not cacheFile.is_file(): if cacheFile.is_file():
return True try:
with open(cacheFile, mode="r", encoding="utf-8") as inFile:
try: data = json.load(inFile)
with open(cacheFile, mode="r", encoding="utf-8") as inFile: for path, entry in data.items():
theData = json.load(inFile) self._data[path] = {
for projPath, theEntry in theData.items(): "title": entry.get("title", ""),
self._data[projPath] = { "words": entry.get("words", 0),
"title": theEntry.get("title", ""), "time": entry.get("time", 0),
"words": theEntry.get("words", 0), }
"time": theEntry.get("time", 0), except Exception:
} logger.error("Could not load recent project cache")
except Exception: logException()
logger.error("Could not load recent project cache") return False
logException()
return False
return True return True
+13 -11
View File
@@ -33,6 +33,8 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG from novelwriter import CONFIG
FONT_SCALE = 0.9 FONT_SCALE = 0.9
RIGHT_TOP = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop
LEFT_TOP = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
class NConfigLayout(QGridLayout): class NConfigLayout(QGridLayout):
@@ -103,31 +105,31 @@ class NConfigLayout(QGridLayout):
labelBox.addWidget(qHelp) labelBox.addWidget(qHelp)
labelBox.setSpacing(0) labelBox.setSpacing(0)
labelBox.addStretch(1) 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: 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): if isinstance(unit, str):
controlBox = QHBoxLayout() controlBox = QHBoxLayout()
controlBox.addWidget(widget, 0, Qt.AlignVCenter) controlBox.addWidget(widget, 0, Qt.AlignVCenter)
controlBox.addWidget(QLabel(unit), 0, Qt.AlignVCenter) controlBox.addWidget(QLabel(unit), 0, Qt.AlignVCenter)
controlBox.setSpacing(wSp) 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): elif isinstance(button, QAbstractButton):
controlBox = QHBoxLayout() controlBox = QHBoxLayout()
controlBox.addWidget(widget, 0, Qt.AlignVCenter) controlBox.addWidget(widget, 0, Qt.AlignVCenter)
controlBox.addWidget(button, 0, Qt.AlignVCenter) controlBox.addWidget(button, 0, Qt.AlignVCenter)
controlBox.setSpacing(wSp) 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: else:
if isinstance(widget, QLineEdit): if isinstance(widget, QLineEdit):
qLayout = QHBoxLayout() qLayout = QHBoxLayout()
qLayout.addWidget(widget) 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: 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, 0)
self.setRowStretch(self._nextRow+1, 1) self.setRowStretch(self._nextRow+1, 1)
@@ -145,14 +147,14 @@ class NSimpleLayout(QGridLayout):
column layout. column layout.
""" """
def __init__(self) -> None: def __init__(self, stretcColumn: int = 0) -> None:
super().__init__() super().__init__()
self._nextRow = 0 self._nextRow = 0
wSp = CONFIG.pxInt(8) wSp = CONFIG.pxInt(8)
self.setHorizontalSpacing(wSp) self.setHorizontalSpacing(wSp)
self.setVerticalSpacing(wSp) self.setVerticalSpacing(wSp)
self.setColumnStretch(0, 1) self.setColumnStretch(stretcColumn, 1)
return return
@@ -176,14 +178,14 @@ class NSimpleLayout(QGridLayout):
wSp = CONFIG.pxInt(8) wSp = CONFIG.pxInt(8)
qLabel = QLabel(label) qLabel = QLabel(label)
qLabel.setIndent(wSp) 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): if isinstance(widget, QLineEdit):
qLayout = QHBoxLayout() qLayout = QHBoxLayout()
qLayout.addWidget(widget) 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: 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) qLabel.setBuddy(widget)
+1 -2
View File
@@ -167,8 +167,7 @@ class _PopRightMenu(QMenu):
def event(self, event: QEvent) -> bool: def event(self, event: QEvent) -> bool:
"""Overload the show event and move the menu popup location.""" """Overload the show event and move the menu popup location."""
if event.type() == QEvent.Show: if event.type() == QEvent.Show:
parent = self.parent() if isinstance(parent := self.parent(), QWidget):
if isinstance(parent, QWidget):
offset = QPoint(parent.width(), parent.height() - self.height()) offset = QPoint(parent.width(), parent.height() - self.height())
self.move(parent.mapToGlobal(offset)) self.move(parent.mapToGlobal(offset))
return super(_PopRightMenu, self).event(event) return super(_PopRightMenu, self).event(event)
+3 -3
View File
@@ -467,9 +467,9 @@ class GuiIcons:
# General Button Icons # General Button Icons
"add", "add_document", "backward", "bookmark", "browse", "checked", "close", "cross", "add", "add_document", "backward", "bookmark", "browse", "checked", "close", "cross",
"down", "edit", "export", "forward", "maximise", "menu", "minimise", "noncheckable", "document", "down", "edit", "export", "forward", "maximise", "menu", "minimise",
"panel", "refresh", "remove", "revert", "search_replace", "search", "settings", "star", "noncheckable", "panel", "refresh", "remove", "revert", "search_replace", "search",
"unchecked", "up", "view", "settings", "star", "unchecked", "up", "view",
# Switches # Switches
"sticky-on", "sticky-off", "sticky-on", "sticky-off",
+15 -1
View File
@@ -31,8 +31,9 @@ from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal 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 from novelwriter.core.spellcheck import NWSpellEnchant
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
@@ -215,6 +216,19 @@ class SharedData(QObject):
QThreadPool.globalInstance().start(runnable, priority=priority) QThreadPool.globalInstance().start(runnable, priority=priority)
return 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 # Signal Proxy
## ##
+86 -17
View File
@@ -30,11 +30,11 @@ from pathlib import Path
from datetime import datetime from datetime import datetime
from PyQt5.QtGui import QCloseEvent, QPaintEvent, QPainter 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 ( from PyQt5.QtWidgets import (
QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout,
QHBoxLayout, QLabel, QLineEdit, QPushButton, QScrollArea, QSpinBox, QHBoxLayout, QLabel, QLineEdit, QMenu, QPushButton, QScrollArea, QSpinBox,
QStackedWidget, QTreeWidget, QVBoxLayout, QWidget QStackedWidget, QToolButton, QTreeWidget, QVBoxLayout, QWidget
) )
from novelwriter import CONFIG, SHARED, __version__, __date__ from novelwriter import CONFIG, SHARED, __version__, __date__
@@ -265,18 +265,17 @@ class _NewProjectForm(QWidget):
FILL_BLANK = 0 FILL_BLANK = 0
FILL_SAMPLE = 1 FILL_SAMPLE = 1
FILL_TEMPLATE = 2 FILL_COPY = 2
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
self._basePath = CONFIG.lastPath() self._basePath = CONFIG.lastPath()
self._fillMode = self.FILL_BLANK self._fillMode = self.FILL_BLANK
self._tmplPath = None self._copyPath = None
# Project Settings # Project Settings
# ================ # ================
posLast = QLineEdit.ActionPosition.TrailingPosition
# Project Name # Project Name
self.projName = QLineEdit() self.projName = QLineEdit()
@@ -303,22 +302,51 @@ class _NewProjectForm(QWidget):
# Project Path # Project Path
self.projPath = QLineEdit(self) self.projPath = QLineEdit(self)
self.projPath.setReadOnly(True) self.projPath.setReadOnly(True)
self.projPath.setPlaceholderText(self.tr("Required"))
action = self.projPath.addAction(SHARED.theme.getIcon("browse"), posLast) self.browsePath = QToolButton(self)
action.triggered.connect(self._doBrowse) 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 # Fill Project
self.projFill = QLineEdit(self) self.projFill = QLineEdit(self)
self.projFill.setReadOnly(True) 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 # Project Form
self.projectForm = QFormLayout() self.projectForm = QFormLayout()
self.projectForm.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.projectForm.addRow(self.tr("Project Name"), self.projName) 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("Language"), self.projLang)
self.projectForm.addRow(self.tr("Project Path"), self.projPath) self.projectForm.addRow(self.tr("Project Path"), self.pathBox)
self.projectForm.addRow(self.tr("Prefill Project"), self.projFill) self.projectForm.addRow(self.tr("Prefill Project"), self.fillBox)
# Chapters and Scenes # Chapters and Scenes
# =================== # ===================
@@ -367,6 +395,7 @@ class _NewProjectForm(QWidget):
self.addNotes.setChecked(False) self.addNotes.setChecked(False)
self.notesForm = QFormLayout() 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 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 character notes"), self.addChar)
self.notesForm.addRow(self.tr("Add a folder for location notes"), self.addWorld) self.notesForm.addRow(self.tr("Add a folder for location notes"), self.addWorld)
@@ -426,18 +455,45 @@ class _NewProjectForm(QWidget):
self.addNotes.setChecked(False) self.addNotes.setChecked(False)
return 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 # Internal Functions
## ##
def _updateFillInfo(self) -> None: def _updateFillInfo(self) -> None:
"""Update the text of the project fill box.""" """Update the text of the project fill box."""
text = ""
if self._fillMode == self.FILL_BLANK: if self._fillMode == self.FILL_BLANK:
self.projFill.setText(self.tr("Fresh Project")) text = self.tr("Fresh Project")
elif self._fillMode == self.FILL_SAMPLE: elif self._fillMode == self.FILL_SAMPLE:
self.projFill.setText(self.tr("Example Project")) text = self.tr("Example Project")
elif self._fillMode == self.FILL_TEMPLATE: elif self._fillMode == self.FILL_COPY:
self.projFill.setText(self.tr("Template: {0}").format(str(self._tmplPath))) 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 isBlank = self._fillMode == self.FILL_BLANK
self.numChapters.setEnabled(isBlank) self.numChapters.setEnabled(isBlank)
@@ -450,3 +506,16 @@ class _NewProjectForm(QWidget):
return return
# END Class _NewProjectForm # 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