Complete the create new project code path
This commit is contained in:
+101
-107
@@ -30,6 +30,7 @@ import shutil
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
from pathlib import Path
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt5.QtCore import QCoreApplication
|
from PyQt5.QtCore import QCoreApplication
|
||||||
@@ -39,6 +40,7 @@ from novelwriter.common import minmax, simplified
|
|||||||
from novelwriter.constants import nwItemClass
|
from novelwriter.constants import nwItemClass
|
||||||
from novelwriter.core.item import NWItem
|
from novelwriter.core.item import NWItem
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
|
from novelwriter.core.storage import NWStorageCreate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -310,9 +312,15 @@ class ProjectBuilder:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
self._path = None
|
||||||
self.tr = partial(QCoreApplication.translate, "NWProject")
|
self.tr = partial(QCoreApplication.translate, "NWProject")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def projPath(self) -> Path | None:
|
||||||
|
"""The path of the newly created project."""
|
||||||
|
return self._path
|
||||||
|
|
||||||
##
|
##
|
||||||
# Methods
|
# Methods
|
||||||
##
|
##
|
||||||
@@ -325,40 +333,56 @@ class ProjectBuilder:
|
|||||||
logger.error("Invalid call to newProject function")
|
logger.error("Invalid call to newProject function")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
popMinimal = data.get("popMinimal", True)
|
path = data.get("path", None)
|
||||||
popCustom = data.get("popCustom", False)
|
if path is None:
|
||||||
popSample = data.get("popSample", False)
|
SHARED.error("A project path is required.")
|
||||||
|
|
||||||
# Check if we're extracting the sample project. This is handled
|
|
||||||
# differently as it isn't actually a new project, so we forward
|
|
||||||
# this to another function and return here.
|
|
||||||
if popSample:
|
|
||||||
return self._extractSampleProject(data)
|
|
||||||
|
|
||||||
projPath = data.get("projPath", None)
|
|
||||||
if projPath is None:
|
|
||||||
logger.error("No project path set for the new project")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if data.get("sample", False):
|
||||||
|
self._path = path
|
||||||
|
return self._extractSampleProject(path)
|
||||||
|
elif data.get("template"):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self._buildAndPopulate(path, data)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
##
|
||||||
|
# Internal Functions
|
||||||
|
##
|
||||||
|
|
||||||
|
def _buildAndPopulate(self, path: Path, data: dict) -> bool:
|
||||||
|
"""Build a project from a data dictionary of specifications
|
||||||
|
provided by the wizard.
|
||||||
|
"""
|
||||||
project = NWProject()
|
project = NWProject()
|
||||||
if not project.storage.createNewProject(projPath):
|
status = project.storage.createNewProject(path)
|
||||||
|
if status == NWStorageCreate.NOT_EMPTY:
|
||||||
|
SHARED.error(self.tr(
|
||||||
|
"A project already exists in that location. "
|
||||||
|
"Please choose another folder."
|
||||||
|
))
|
||||||
return False
|
return False
|
||||||
|
elif status == NWStorageCreate.OS_ERROR:
|
||||||
|
SHARED.error(self.tr(
|
||||||
|
"An error occured while trying to create the project."
|
||||||
|
), exc=project.storage.exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._path = project.storage.storagePath
|
||||||
|
|
||||||
lblNewProject = self.tr("New Project")
|
lblNewProject = self.tr("New Project")
|
||||||
lblNewChapter = self.tr("New Chapter")
|
|
||||||
lblNewScene = self.tr("New Scene")
|
|
||||||
lblTitlePage = self.tr("Title Page")
|
lblTitlePage = self.tr("Title Page")
|
||||||
lblByAuthors = self.tr("By")
|
lblByAuthors = self.tr("By")
|
||||||
|
|
||||||
# Settings
|
# Settings
|
||||||
projName = data.get("projName", lblNewProject)
|
projName = data.get("name", lblNewProject)
|
||||||
projTitle = data.get("projTitle", lblNewProject)
|
projAuthor = data.get("author", "")
|
||||||
projAuthor = data.get("projAuthor", "")
|
projLang = data.get("language", "en_GB")
|
||||||
projLang = data.get("projLang", "en_GB")
|
|
||||||
|
|
||||||
project.data.setUuid(None)
|
project.data.setUuid(None)
|
||||||
project.data.setName(projName)
|
project.data.setName(projName)
|
||||||
project.data.setTitle(projTitle)
|
|
||||||
project.data.setAuthor(projAuthor)
|
project.data.setAuthor(projAuthor)
|
||||||
project.data.setLanguage(projLang)
|
project.data.setLanguage(projLang)
|
||||||
project.setDefaultStatusImport()
|
project.setDefaultStatusImport()
|
||||||
@@ -376,110 +400,80 @@ class ProjectBuilder:
|
|||||||
aDoc = project.storage.getDocument(hTitlePage)
|
aDoc = project.storage.getDocument(hTitlePage)
|
||||||
aDoc.writeDocument(titlePage)
|
aDoc.writeDocument(titlePage)
|
||||||
|
|
||||||
if popMinimal:
|
# Create a project structure based on selected root folders
|
||||||
# Creating a minimal project with a few root folders and a
|
# and a number of chapters and scenes selected in the
|
||||||
# single chapter with a single scene.
|
# wizard's custom page.
|
||||||
hChapter = project.newFile(lblNewChapter, hNovelRoot)
|
|
||||||
aDoc = project.storage.getDocument(hChapter)
|
|
||||||
aDoc.writeDocument(f"## {lblNewChapter}\n\n")
|
|
||||||
|
|
||||||
if hChapter:
|
# Create chapters and scenes
|
||||||
hScene = project.newFile(lblNewScene, hChapter)
|
numChapters = data.get("chapters", 0)
|
||||||
aDoc = project.storage.getDocument(hScene)
|
numScenes = data.get("scenes", 0)
|
||||||
aDoc.writeDocument(f"### {lblNewScene}\n\n")
|
|
||||||
|
|
||||||
project.newRoot(nwItemClass.PLOT)
|
chSynop = self.tr("Summary of the chapter.")
|
||||||
project.newRoot(nwItemClass.CHARACTER)
|
scSynop = self.tr("Summary of the scene.")
|
||||||
project.newRoot(nwItemClass.WORLD)
|
bfNote = self.tr("A short description.")
|
||||||
project.newRoot(nwItemClass.ARCHIVE)
|
|
||||||
|
|
||||||
project.saveProject()
|
# Create chapters
|
||||||
project.closeProject()
|
if numChapters > 0:
|
||||||
|
for ch in range(numChapters):
|
||||||
|
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
|
||||||
|
cHandle = project.newFile(chTitle, hNovelRoot)
|
||||||
|
aDoc = project.storage.getDocument(cHandle)
|
||||||
|
aDoc.writeDocument(f"## {chTitle}\n\n%Synopsis: {chSynop}\n\n")
|
||||||
|
|
||||||
elif popCustom:
|
# Create chapter scenes
|
||||||
# Create a project structure based on selected root folders
|
if numScenes > 0 and cHandle:
|
||||||
# and a number of chapters and scenes selected in the
|
for sc in range(numScenes):
|
||||||
# wizard's custom page.
|
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
|
||||||
|
sHandle = project.newFile(scTitle, cHandle)
|
||||||
|
aDoc = project.storage.getDocument(sHandle)
|
||||||
|
aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {scSynop}\n\n")
|
||||||
|
|
||||||
# Create chapters and scenes
|
# Create scenes (no chapters)
|
||||||
numChapters = data.get("numChapters", 0)
|
elif numScenes > 0:
|
||||||
numScenes = data.get("numScenes", 0)
|
for sc in range(numScenes):
|
||||||
|
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
|
||||||
|
sHandle = project.newFile(scTitle, hNovelRoot)
|
||||||
|
aDoc = project.storage.getDocument(sHandle)
|
||||||
|
aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {scSynop}\n\n")
|
||||||
|
|
||||||
chSynop = self.tr("Summary of the chapter.")
|
# Create notes folders
|
||||||
scSynop = self.tr("Summary of the scene.")
|
noteTitles = {
|
||||||
bfNote = self.tr("A short description.")
|
nwItemClass.PLOT: self.tr("Main Plot"),
|
||||||
|
nwItemClass.CHARACTER: self.tr("Protagonist"),
|
||||||
|
nwItemClass.WORLD: self.tr("Main Location"),
|
||||||
|
}
|
||||||
|
|
||||||
# Create chapters
|
addNotes = data.get("notes", False)
|
||||||
if numChapters > 0:
|
for newRoot in data.get("roots", []):
|
||||||
for ch in range(numChapters):
|
if newRoot in nwItemClass:
|
||||||
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
|
rHandle = project.newRoot(newRoot)
|
||||||
cHandle = project.newFile(chTitle, hNovelRoot)
|
if addNotes:
|
||||||
aDoc = project.storage.getDocument(cHandle)
|
aHandle = project.newFile(noteTitles[newRoot], rHandle)
|
||||||
aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n")
|
ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
|
||||||
|
aDoc = project.storage.getDocument(aHandle)
|
||||||
|
aDoc.writeDocument(
|
||||||
|
f"# {noteTitles[newRoot]}\n\n"
|
||||||
|
f"@tag: {ntTag}\n\n"
|
||||||
|
f"%Short: {bfNote}\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
# Create chapter scenes
|
# Also add the archive and trash folders
|
||||||
if numScenes > 0 and cHandle:
|
project.newRoot(nwItemClass.ARCHIVE)
|
||||||
for sc in range(numScenes):
|
project.trashFolder()
|
||||||
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
|
|
||||||
sHandle = project.newFile(scTitle, cHandle)
|
|
||||||
aDoc = project.storage.getDocument(sHandle)
|
|
||||||
aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
|
|
||||||
|
|
||||||
# Create scenes (no chapters)
|
project.saveProject()
|
||||||
elif numScenes > 0:
|
project.closeProject()
|
||||||
for sc in range(numScenes):
|
|
||||||
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
|
|
||||||
sHandle = project.newFile(scTitle, hNovelRoot)
|
|
||||||
aDoc = project.storage.getDocument(sHandle)
|
|
||||||
aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
|
|
||||||
|
|
||||||
# Create notes folders
|
|
||||||
noteTitles = {
|
|
||||||
nwItemClass.PLOT: self.tr("Main Plot"),
|
|
||||||
nwItemClass.CHARACTER: self.tr("Protagonist"),
|
|
||||||
nwItemClass.WORLD: self.tr("Main Location"),
|
|
||||||
}
|
|
||||||
|
|
||||||
addNotes = data.get("addNotes", False)
|
|
||||||
for newRoot in data.get("addRoots", []):
|
|
||||||
if newRoot in nwItemClass:
|
|
||||||
rHandle = project.newRoot(newRoot)
|
|
||||||
if addNotes:
|
|
||||||
aHandle = project.newFile(noteTitles[newRoot], rHandle)
|
|
||||||
ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
|
|
||||||
aDoc = project.storage.getDocument(aHandle)
|
|
||||||
aDoc.writeDocument(
|
|
||||||
f"# {noteTitles[newRoot]}\n\n"
|
|
||||||
f"@tag: {ntTag}\n\n"
|
|
||||||
f"% Short: {bfNote}\n\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Also add the archive and trash folders
|
|
||||||
project.newRoot(nwItemClass.ARCHIVE)
|
|
||||||
project.trashFolder()
|
|
||||||
|
|
||||||
project.saveProject()
|
|
||||||
project.closeProject()
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
##
|
def _extractSampleProject(self, path: Path) -> bool:
|
||||||
# Internal Functions
|
|
||||||
##
|
|
||||||
|
|
||||||
def _extractSampleProject(self, data: dict) -> bool:
|
|
||||||
"""Make a copy of the sample project by extracting the
|
"""Make a copy of the sample project by extracting the
|
||||||
sample.zip file to the new path.
|
sample.zip file to the new path.
|
||||||
"""
|
"""
|
||||||
projPath = data.get("projPath", None)
|
|
||||||
if projPath is None:
|
|
||||||
logger.error("No project path set for the example project")
|
|
||||||
return False
|
|
||||||
|
|
||||||
pkgSample = CONFIG.assetPath("sample.zip")
|
pkgSample = CONFIG.assetPath("sample.zip")
|
||||||
if pkgSample.is_file():
|
if pkgSample.is_file():
|
||||||
try:
|
try:
|
||||||
shutil.unpack_archive(pkgSample, projPath)
|
shutil.unpack_archive(pkgSample, path)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
SHARED.error(self.tr(
|
SHARED.error(self.tr(
|
||||||
"Failed to create a new example project."
|
"Failed to create a new example project."
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ class NWStorageOpen(Enum):
|
|||||||
# END Enum NWStorageOpen
|
# END Enum NWStorageOpen
|
||||||
|
|
||||||
|
|
||||||
|
class NWStorageCreate(Enum):
|
||||||
|
|
||||||
|
NOT_EMPTY = 0
|
||||||
|
OS_ERROR = 1
|
||||||
|
READY = 2
|
||||||
|
|
||||||
|
# END Enum NWStorageCreate
|
||||||
|
|
||||||
|
|
||||||
class NWStorage:
|
class NWStorage:
|
||||||
"""Core: Project Storage Class
|
"""Core: Project Storage Class
|
||||||
|
|
||||||
@@ -133,12 +142,12 @@ class NWStorage:
|
|||||||
"""Check if the storage location is open."""
|
"""Check if the storage location is open."""
|
||||||
return self._ready and self._runtimePath is not None
|
return self._ready and self._runtimePath is not None
|
||||||
|
|
||||||
def createNewProject(self, path: str | Path) -> bool:
|
def createNewProject(self, path: str | Path) -> NWStorageCreate:
|
||||||
"""Create a new project at the given location."""
|
"""Create a new project at the given location."""
|
||||||
inPath = Path(path).resolve()
|
inPath = Path(path).resolve()
|
||||||
if inPath.is_dir() and len(list(inPath.iterdir())) > 0:
|
if inPath.is_dir() and len(list(inPath.iterdir())) > 0:
|
||||||
logger.error("Folder is not empty: %s", inPath)
|
logger.error("Folder is not empty: %s", inPath)
|
||||||
return False
|
return NWStorageCreate.NOT_EMPTY
|
||||||
|
|
||||||
self._storagePath = inPath
|
self._storagePath = inPath
|
||||||
self._runtimePath = inPath
|
self._runtimePath = inPath
|
||||||
@@ -156,11 +165,11 @@ class NWStorage:
|
|||||||
self._exception = exc
|
self._exception = exc
|
||||||
logger.error("Failed to create project folders", exc_info=exc)
|
logger.error("Failed to create project folders", exc_info=exc)
|
||||||
self.clear()
|
self.clear()
|
||||||
return False
|
return NWStorageCreate.OS_ERROR
|
||||||
|
|
||||||
self._ready = True
|
self._ready = True
|
||||||
|
|
||||||
return True
|
return NWStorageCreate.READY
|
||||||
|
|
||||||
def initProjectStorage(self, path: str | Path, clearLock: bool = False) -> NWStorageOpen:
|
def initProjectStorage(self, path: str | Path, clearLock: bool = False) -> NWStorageOpen:
|
||||||
"""Initialise a novelWriter project location."""
|
"""Initialise a novelWriter project location."""
|
||||||
|
|||||||
@@ -132,14 +132,10 @@ class GuiMainMenu(QMenuBar):
|
|||||||
# Project
|
# Project
|
||||||
self.projMenu = self.addMenu(self.tr("&Project"))
|
self.projMenu = self.addMenu(self.tr("&Project"))
|
||||||
|
|
||||||
# Project > New Project
|
# Project > Create or Open Project
|
||||||
self.aNewProject = self.projMenu.addAction(self.tr("New Project"))
|
self.aOpenProject = self.projMenu.addAction(self.tr("Create or Open Project"))
|
||||||
self.aNewProject.triggered.connect(lambda: self.mainGui.newProject(None))
|
|
||||||
|
|
||||||
# Project > Open Project
|
|
||||||
self.aOpenProject = self.projMenu.addAction(self.tr("Open Project"))
|
|
||||||
self.aOpenProject.setShortcut("Ctrl+Shift+O")
|
self.aOpenProject.setShortcut("Ctrl+Shift+O")
|
||||||
self.aOpenProject.triggered.connect(lambda: self.mainGui.showProjectLoadDialog())
|
self.aOpenProject.triggered.connect(self.mainGui.showWelcomeDialog)
|
||||||
|
|
||||||
# Project > Save Project
|
# Project > Save Project
|
||||||
self.aSaveProject = self.projMenu.addAction(self.tr("Save Project"))
|
self.aSaveProject = self.projMenu.addAction(self.tr("Save Project"))
|
||||||
|
|||||||
+20
-10
@@ -51,7 +51,7 @@ from novelwriter.gui.itemdetails import GuiItemDetails
|
|||||||
from novelwriter.gui.docviewerpanel import GuiDocViewerPanel
|
from novelwriter.gui.docviewerpanel import GuiDocViewerPanel
|
||||||
from novelwriter.dialogs.about import GuiAbout
|
from novelwriter.dialogs.about import GuiAbout
|
||||||
from novelwriter.dialogs.updates import GuiUpdates
|
from novelwriter.dialogs.updates import GuiUpdates
|
||||||
# from novelwriter.dialogs.projload import GuiProjectLoad
|
from novelwriter.dialogs.projload import GuiProjectLoad
|
||||||
from novelwriter.dialogs.wordlist import GuiWordList
|
from novelwriter.dialogs.wordlist import GuiWordList
|
||||||
from novelwriter.dialogs.preferences import GuiPreferences
|
from novelwriter.dialogs.preferences import GuiPreferences
|
||||||
from novelwriter.dialogs.projdetails import GuiProjectDetails
|
from novelwriter.dialogs.projdetails import GuiProjectDetails
|
||||||
@@ -360,7 +360,7 @@ class GuiMain(QMainWindow):
|
|||||||
self.openProject(cmdOpen)
|
self.openProject(cmdOpen)
|
||||||
|
|
||||||
if not SHARED.hasProject:
|
if not SHARED.hasProject:
|
||||||
self.showProjectLoadDialog()
|
self.showWelcomeDialog()
|
||||||
|
|
||||||
# Determine whether release notes need to be shown or not
|
# Determine whether release notes need to be shown or not
|
||||||
if hexToInt(CONFIG.lastNotes) < hexToInt(__hexversion__):
|
if hexToInt(CONFIG.lastNotes) < hexToInt(__hexversion__):
|
||||||
@@ -844,18 +844,22 @@ class GuiMain(QMainWindow):
|
|||||||
browse button for projects not yet cached. Selecting to create a
|
browse button for projects not yet cached. Selecting to create a
|
||||||
new project is forwarded to the new project wizard.
|
new project is forwarded to the new project wizard.
|
||||||
"""
|
"""
|
||||||
# dlgProj = GuiProjectLoad(self)
|
dlgProj = GuiProjectLoad(self)
|
||||||
# dlgProj.exec_()
|
dlgProj.exec_()
|
||||||
|
|
||||||
# if dlgProj.result() == QDialog.Accepted:
|
if dlgProj.result() == QDialog.Accepted:
|
||||||
# if dlgProj.openState == GuiProjectLoad.OPEN_STATE:
|
if dlgProj.openState == GuiProjectLoad.OPEN_STATE:
|
||||||
# self.openProject(dlgProj.openPath)
|
self.openProject(dlgProj.openPath)
|
||||||
# elif dlgProj.openState == GuiProjectLoad.NEW_STATE:
|
elif dlgProj.openState == GuiProjectLoad.NEW_STATE:
|
||||||
# self.newProject()
|
self.newProject()
|
||||||
|
return
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def showWelcomeDialog(self) -> None:
|
||||||
|
"""Open the welcome dialog."""
|
||||||
dialog = GuiWelcome(self)
|
dialog = GuiWelcome(self)
|
||||||
|
dialog.openProjectRequest.connect(self._openProject)
|
||||||
dialog.exec_()
|
dialog.exec_()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def showNewProjectDialog(self) -> dict | None:
|
def showNewProjectDialog(self) -> dict | None:
|
||||||
@@ -1208,6 +1212,12 @@ class GuiMain(QMainWindow):
|
|||||||
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
|
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@pyqtSlot(Path)
|
||||||
|
def _openProject(self, path: Path) -> None:
|
||||||
|
"""Handle an open project request."""
|
||||||
|
self.openProject(path)
|
||||||
|
return
|
||||||
|
|
||||||
@pyqtSlot(str, nwDocMode, str, bool)
|
@pyqtSlot(str, nwDocMode, str, bool)
|
||||||
def _openDocument(self, tHandle: str, mode: nwDocMode, sTitle: str, setFocus: bool) -> None:
|
def _openDocument(self, tHandle: str, mode: nwDocMode, sTitle: str, setFocus: bool) -> None:
|
||||||
"""Handle an open document request."""
|
"""Handle an open document request."""
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ from PyQt5.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter import CONFIG, SHARED, __version__, __date__
|
from novelwriter import CONFIG, SHARED, __version__, __date__
|
||||||
|
from novelwriter.enum import nwItemClass
|
||||||
from novelwriter.common import makeFileNameSafe
|
from novelwriter.common import makeFileNameSafe
|
||||||
from novelwriter.constants import nwUnicode
|
from novelwriter.constants import nwUnicode
|
||||||
|
from novelwriter.core.coretools import ProjectBuilder
|
||||||
from novelwriter.extensions.switch import NSwitch
|
from novelwriter.extensions.switch import NSwitch
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
@@ -50,6 +52,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class GuiWelcome(QDialog):
|
class GuiWelcome(QDialog):
|
||||||
|
|
||||||
|
openProjectRequest = pyqtSignal(Path)
|
||||||
|
|
||||||
def __init__(self, mainGui: GuiMain) -> None:
|
def __init__(self, mainGui: GuiMain) -> None:
|
||||||
super().__init__(parent=mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
@@ -84,6 +88,7 @@ class GuiWelcome(QDialog):
|
|||||||
self.tabOpen = _OpenProjectPage(self)
|
self.tabOpen = _OpenProjectPage(self)
|
||||||
self.tabNew = _NewProjectPage(self)
|
self.tabNew = _NewProjectPage(self)
|
||||||
self.tabNew.cancelNewProject.connect(self._showOpenProjectPage)
|
self.tabNew.cancelNewProject.connect(self._showOpenProjectPage)
|
||||||
|
self.tabNew.openProjectRequest.connect(self._setProjectPath)
|
||||||
|
|
||||||
self.mainStack = QStackedWidget()
|
self.mainStack = QStackedWidget()
|
||||||
self.mainStack.addWidget(self.tabOpen)
|
self.mainStack.addWidget(self.tabOpen)
|
||||||
@@ -167,6 +172,15 @@ class GuiWelcome(QDialog):
|
|||||||
self.mainStack.setCurrentWidget(self.tabOpen)
|
self.mainStack.setCurrentWidget(self.tabOpen)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
@pyqtSlot(Path)
|
||||||
|
def _setProjectPath(self, path: Path | None = None) -> None:
|
||||||
|
"""Set the path variable for the project to open."""
|
||||||
|
if isinstance(path, Path):
|
||||||
|
self.openProjectRequest.emit(path)
|
||||||
|
self.close()
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
@@ -207,6 +221,7 @@ class _OpenProjectPage(QWidget):
|
|||||||
class _NewProjectPage(QWidget):
|
class _NewProjectPage(QWidget):
|
||||||
|
|
||||||
cancelNewProject = pyqtSignal()
|
cancelNewProject = pyqtSignal()
|
||||||
|
openProjectRequest = pyqtSignal(Path)
|
||||||
|
|
||||||
def __init__(self, parent: QWidget) -> None:
|
def __init__(self, parent: QWidget) -> None:
|
||||||
super().__init__(parent=parent)
|
super().__init__(parent=parent)
|
||||||
@@ -225,13 +240,14 @@ class _NewProjectPage(QWidget):
|
|||||||
# Controls
|
# Controls
|
||||||
# ========
|
# ========
|
||||||
|
|
||||||
self.createButton = QPushButton(self.tr("Create Project"), self)
|
|
||||||
self.createButton.setIcon(SHARED.theme.getIcon("star"))
|
|
||||||
|
|
||||||
self.cancelButton = QPushButton(self.tr("Go Back"), self)
|
self.cancelButton = QPushButton(self.tr("Go Back"), self)
|
||||||
self.cancelButton.setIcon(SHARED.theme.getIcon("backward"))
|
self.cancelButton.setIcon(SHARED.theme.getIcon("backward"))
|
||||||
self.cancelButton.clicked.connect(lambda: self.cancelNewProject.emit())
|
self.cancelButton.clicked.connect(lambda: self.cancelNewProject.emit())
|
||||||
|
|
||||||
|
self.createButton = QPushButton(self.tr("Create Project"), self)
|
||||||
|
self.createButton.setIcon(SHARED.theme.getIcon("star"))
|
||||||
|
self.createButton.clicked.connect(self._createNewProject)
|
||||||
|
|
||||||
self.buttonBox = QHBoxLayout()
|
self.buttonBox = QHBoxLayout()
|
||||||
self.buttonBox.addStretch(1)
|
self.buttonBox.addStretch(1)
|
||||||
self.buttonBox.addWidget(self.cancelButton, 0)
|
self.buttonBox.addWidget(self.cancelButton, 0)
|
||||||
@@ -258,6 +274,22 @@ class _NewProjectPage(QWidget):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Private Slots
|
||||||
|
##
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _createNewProject(self) -> None:
|
||||||
|
"""Create a new project from the data in the form."""
|
||||||
|
data = self.projectForm.getProjectData()
|
||||||
|
if not data.get("name"):
|
||||||
|
SHARED.error(self.tr("A project name is required."))
|
||||||
|
return
|
||||||
|
builder = ProjectBuilder()
|
||||||
|
if builder.buildProject(data) and (path := builder.projPath):
|
||||||
|
self.openProjectRequest.emit(path)
|
||||||
|
return
|
||||||
|
|
||||||
# END Class _NewProjectPage
|
# END Class _NewProjectPage
|
||||||
|
|
||||||
|
|
||||||
@@ -422,6 +454,29 @@ class _NewProjectForm(QWidget):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def getProjectData(self) -> dict:
|
||||||
|
"""Collect form data and return it as a dictionary."""
|
||||||
|
roots = []
|
||||||
|
if self.addPlot.isChecked():
|
||||||
|
roots.append(nwItemClass.PLOT)
|
||||||
|
if self.addChar.isChecked():
|
||||||
|
roots.append(nwItemClass.CHARACTER)
|
||||||
|
if self.addWorld.isChecked():
|
||||||
|
roots.append(nwItemClass.WORLD)
|
||||||
|
return {
|
||||||
|
"name": self.projName.text().strip(),
|
||||||
|
"author": self.projAuthor.text().strip(),
|
||||||
|
"language": self.projLang.currentData(),
|
||||||
|
"path": self.projPath.text(),
|
||||||
|
"blank": self._fillMode == self.FILL_BLANK,
|
||||||
|
"sample": self._fillMode == self.FILL_SAMPLE,
|
||||||
|
"template": self._copyPath if self._fillMode == self.FILL_COPY else None,
|
||||||
|
"chapters": self.numChapters.value(),
|
||||||
|
"scenes": self.numScenes.value(),
|
||||||
|
"roots": roots,
|
||||||
|
"notes": self.addNotes.isChecked(),
|
||||||
|
}
|
||||||
|
|
||||||
##
|
##
|
||||||
# Private Slots
|
# Private Slots
|
||||||
##
|
##
|
||||||
@@ -472,9 +527,10 @@ class _NewProjectForm(QWidget):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _setFillCopy(self) -> None:
|
def _setFillCopy(self) -> None:
|
||||||
"""Set fill mode to copy project."""
|
"""Set fill mode to copy project."""
|
||||||
self._fillMode = self.FILL_COPY
|
if copyPath := SHARED.getProjectPath(self, allowZip=True):
|
||||||
self._copyPath = SHARED.getProjectPath(self, allowZip=True)
|
self._fillMode = self.FILL_COPY
|
||||||
self._updateFillInfo()
|
self._copyPath = copyPath
|
||||||
|
self._updateFillInfo()
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from mocked import causeOSError
|
|||||||
from novelwriter import CONFIG
|
from novelwriter import CONFIG
|
||||||
from novelwriter.constants import nwFiles
|
from novelwriter.constants import nwFiles
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
from novelwriter.core.storage import NWStorage, NWStorageOpen, _LegacyStorage
|
from novelwriter.core.storage import NWStorage, NWStorageOpen, NWStorageCreate, _LegacyStorage
|
||||||
from novelwriter.core.document import NWDocument
|
from novelwriter.core.document import NWDocument
|
||||||
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
|
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
|
||||||
|
|
||||||
@@ -67,16 +67,16 @@ def testCoreStorage_CreateNewProject(mockGUI, fncPath):
|
|||||||
|
|
||||||
# Cannot prepare a non-empty folder
|
# Cannot prepare a non-empty folder
|
||||||
(fncPath / "foobar.txt").touch()
|
(fncPath / "foobar.txt").touch()
|
||||||
assert storage.createNewProject(fncPath) is False
|
assert storage.createNewProject(fncPath) == NWStorageCreate.NOT_EMPTY
|
||||||
|
|
||||||
# Try creating in a non-existent subfolder instead
|
# Try creating in a non-existent subfolder instead
|
||||||
assert storage.createNewProject(fncPath / "project1") is True
|
assert storage.createNewProject(fncPath / "project1") == NWStorageCreate.READY
|
||||||
assert (fncPath / "project1").is_dir()
|
assert (fncPath / "project1").is_dir()
|
||||||
assert (fncPath / "project1" / "meta").is_dir()
|
assert (fncPath / "project1" / "meta").is_dir()
|
||||||
assert (fncPath / "project1" / "content").is_dir()
|
assert (fncPath / "project1" / "content").is_dir()
|
||||||
|
|
||||||
# However, the parent folder must exist
|
# However, the parent folder must exist
|
||||||
assert storage.createNewProject(fncPath / "foobar" / "project1") is False
|
assert storage.createNewProject(fncPath / "foobar" / "project1") == NWStorageCreate.OS_ERROR
|
||||||
assert isinstance(storage.exc, FileNotFoundError)
|
assert isinstance(storage.exc, FileNotFoundError)
|
||||||
|
|
||||||
project.closeProject()
|
project.closeProject()
|
||||||
|
|||||||
Reference in New Issue
Block a user