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