Move new project tool to core tools (resolves #1152)

This commit is contained in:
Veronica Berglyd Olsen
2022-11-03 23:47:51 +01:00
parent bd81ea0880
commit 2e590b742f
10 changed files with 398 additions and 461 deletions
+2 -1
View File
@@ -19,7 +19,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from novelwriter.core.coretools import DocMerger, DocSplitter
from novelwriter.core.coretools import DocMerger, DocSplitter, ProjectBuilder
from novelwriter.core.document import NWDoc
from novelwriter.core.index import countWords
from novelwriter.core.project import NWProject
@@ -31,6 +31,7 @@ from novelwriter.core.tomd import ToMarkdown
__all__ = [
"DocMerger",
"DocSplitter",
"ProjectBuilder",
"countWords",
"NWDoc",
"NWProject",
+214 -1
View File
@@ -24,15 +24,30 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import shutil
import logging
import novelwriter
from novelwriter.common import minmax
from time import time
from functools import partial
from PyQt5.QtCore import QCoreApplication
from novelwriter.enum import nwAlert
from novelwriter.common import minmax, simplified
from novelwriter.constants import nwItemClass
from novelwriter.core.project import NWProject
from novelwriter.core.document import NWDoc
logger = logging.getLogger(__name__)
class DocMerger:
"""Document tool for merging a set of documents into a single new
document. The parameters are defined by the user using the
GuiDocMerge dialog.
"""
def __init__(self, theProject):
@@ -122,6 +137,10 @@ class DocMerger:
class DocSplitter:
"""Document tool for splitting a document into a set of new
documents. The parameters are defined by the user using the
GuiDocSplit dialog.
"""
def __init__(self, theProject, sHandle):
@@ -242,3 +261,197 @@ class DocSplitter:
return
# END Class DocSplitter
class ProjectBuilder:
"""A class to build a new project from a set of user-defined
parameter provided by the New Projecty Wizard.
"""
def __init__(self, mainGui):
self.mainGui = mainGui
self.mainConf = novelwriter.CONFIG
self.tr = partial(QCoreApplication.translate, "NWProject")
return
##
# Methods
##
def buildProject(self, data):
"""Build a project from a data dictionary of specifications
provided by the wizard.
"""
if not isinstance(data, dict):
logger.error("Invalid call to newProject function")
return False
popMinimal = data.get("popMinimal", True)
popCustom = data.get("popCustom", False)
popSample = data.get("popSample", False)
# 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
project = NWProject(self.mainGui)
if not project.setProjectPath(projPath, newProject=True):
return False
if not project.storage.openProjectInPlace(projPath):
return False
lblNewProject = self.tr("New Project")
lblNewChapter = self.tr("New Chapter")
lblNewScene = self.tr("New Scene")
lblTitlePage = self.tr("Title Page")
lblByAuthors = self.tr("By")
# Settings
projName = data.get("projName", lblNewProject)
projTitle = data.get("projTitle", lblNewProject)
projAuthors = data.get("projAuthors", "")
project.data.setName(projName)
project.data.setTitle(projTitle)
project.data.setAuthors(projAuthors)
project.setDefaultStatusImport()
project._projOpened = int(time())
# Add Root Folders
hNovelRoot = project.newRoot(nwItemClass.NOVEL)
hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
novelTitle = project.data.title if project.data.title else project.data.name
titlePage = f"#! {novelTitle}\n\n"
if project.data.authors:
titlePage += f">> {lblByAuthors} {project.getFormattedAuthors()} <<\n\n"
aDoc = NWDoc(project, hTitlePage)
aDoc.writeDocument(titlePage)
if popMinimal:
# Creating a minimal project with a few root folders and a
# single chapter with a single scene.
hChapter = project.newFile(lblNewChapter, hNovelRoot)
aDoc = NWDoc(project, hChapter)
aDoc.writeDocument(f"## {lblNewChapter}\n\n")
hScene = project.newFile(lblNewScene, hChapter)
aDoc = NWDoc(project, hScene)
aDoc.writeDocument(f"### {lblNewScene}\n\n")
project.newRoot(nwItemClass.PLOT)
project.newRoot(nwItemClass.CHARACTER)
project.newRoot(nwItemClass.WORLD)
project.newRoot(nwItemClass.ARCHIVE)
project.saveProject()
project.closeProject()
elif popCustom:
# Create a project structure based on selected root folders
# and a number of chapters and scenes selected in the
# wizard's custom page.
# Create chapters and scenes
numChapters = data.get("numChapters", 0)
numScenes = data.get("numScenes", 0)
chSynop = self.tr("Summary of the chapter.")
scSynop = self.tr("Summary of the scene.")
# Create chapters
if numChapters > 0:
for ch in range(numChapters):
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
cHandle = project.newFile(chTitle, hNovelRoot)
aDoc = NWDoc(project, cHandle)
aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n")
# Create chapter scenes
if numScenes > 0:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
sHandle = project.newFile(scTitle, cHandle)
aDoc = NWDoc(project, sHandle)
aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
# Create scenes (no chapters)
elif numScenes > 0:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
sHandle = project.newFile(scTitle, hNovelRoot)
aDoc = NWDoc(project, 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 = NWDoc(project, aHandle)
aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n")
# Also add the archive and trash folders
project.newRoot(nwItemClass.ARCHIVE)
project.trashFolder()
project.saveProject()
project.closeProject()
return True
##
# Internal Functions
##
def _extractSampleProject(self, data):
"""Make a copy of the sample project by extracting the
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 = os.path.join(self.mainConf.assetPath, "sample.zip")
if os.path.isfile(pkgSample):
try:
shutil.unpack_archive(pkgSample, projPath)
except Exception as exc:
self.mainGui.makeAlert(self.tr(
"Failed to create a new example project."
), nwAlert.ERROR, exception=exc)
return False
else:
self.mainGui.makeAlert(self.tr(
"Failed to create a new example project. "
"Could not find the necessary files. "
"They seem to be missing from this installation."
), nwAlert.ERROR)
return False
return True
# END Class ProjectBuilder
+13 -206
View File
@@ -264,150 +264,6 @@ class NWProject(QObject):
return
def newProject(self, projData):
"""Create a new project by populating the project tree with a
few starter items.
"""
if not isinstance(projData, dict):
logger.error("Invalid call to newProject function")
return False
popMinimal = projData.get("popMinimal", True)
popCustom = projData.get("popCustom", False)
popSample = projData.get("popSample", False)
# 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(projData)
# Project Settings
projPath = projData.get("projPath", None)
projName = projData.get("projName", self.tr("New Project"))
projTitle = projData.get("projTitle", "")
projAuthors = projData.get("projAuthors", "")
if projPath is None:
logger.error("No project path set for the new project")
return False
self.clearProject()
self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100))
self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0))
self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0))
self._data.itemStatus.write(None, self.tr("Finished"), (50, 200, 0))
self._data.itemImport.write(None, self.tr("New"), (100, 100, 100))
self._data.itemImport.write(None, self.tr("Minor"), (200, 50, 0))
self._data.itemImport.write(None, self.tr("Major"), (200, 150, 0))
self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0))
if not self.setProjectPath(projPath, newProject=True):
return False
self._storage.openProjectInPlace(self.projPath)
self._data.setName(projName)
self._data.setTitle(projTitle)
self._data.setAuthors(projAuthors)
hNovelRoot = self.newRoot(nwItemClass.NOVEL)
hTitlePage = self.newFile(self.tr("Title Page"), hNovelRoot)
titlePage = "#! %s\n\n" % (
self._data.title if self._data.title else self._data.name
)
if self._data.authors:
titlePage = "%s>> %s %s <<\n" % (
titlePage, self.tr("By"), self.getFormattedAuthors()
)
aDoc = NWDoc(self, hTitlePage)
aDoc.writeDocument(titlePage)
if popMinimal:
# Creating a minimal project with a few root folders and a
# single chapter with a single scene.
hChapter = self.newFile(self.tr("New Chapter"), hNovelRoot)
aDoc = NWDoc(self, hChapter)
aDoc.writeDocument("## %s\n\n" % self.tr("New Chapter"))
hScene = self.newFile(self.tr("New Scene"), hChapter)
aDoc = NWDoc(self, hScene)
aDoc.writeDocument("### %s\n\n" % self.tr("New Scene"))
self.newRoot(nwItemClass.PLOT)
self.newRoot(nwItemClass.CHARACTER)
self.newRoot(nwItemClass.WORLD)
self.newRoot(nwItemClass.ARCHIVE)
elif popCustom:
# Create a project structure based on selected root folders
# and a number of chapters and scenes selected in the
# wizard's custom page.
# Create chapters and scenes
numChapters = projData.get("numChapters", 0)
numScenes = projData.get("numScenes", 0)
chSynop = self.tr("Summary of the chapter.")
scSynop = self.tr("Summary of the scene.")
# Create chapters
if numChapters > 0:
for ch in range(numChapters):
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
cHandle = self.newFile(chTitle, hNovelRoot)
aDoc = NWDoc(self, cHandle)
aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n")
# Create chapter scenes
if numScenes > 0:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
sHandle = self.newFile(scTitle, cHandle)
aDoc = NWDoc(self, sHandle)
aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
# Create scenes (no chapters)
elif numScenes > 0:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
sHandle = self.newFile(scTitle, hNovelRoot)
aDoc = NWDoc(self, 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 = projData.get("addNotes", False)
for newRoot in projData.get("addRoots", []):
if newRoot in nwItemClass:
rHandle = self.newRoot(newRoot)
if addNotes:
aHandle = self.newFile(noteTitles[newRoot], rHandle)
ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
aDoc = NWDoc(self, aHandle)
aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n")
# Also add the archive and trash folders
self.newRoot(nwItemClass.ARCHIVE)
self.trashFolder()
# Finalise
if popCustom or popMinimal:
self._projOpened = time()
self.setProjectChanged(True)
self.saveProject(autoSave=True)
return True
def openProject(self, fileName, overrideLock=False):
"""Open the project file provided. If it doesn't exist, assume
it is a folder and look for the file within it. If successful,
@@ -680,6 +536,19 @@ class NWProject(QObject):
return True
def setDefaultStatusImport(self):
"""Set the default status and importance values.
"""
self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100))
self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0))
self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0))
self._data.itemStatus.write(None, self.tr("Finished"), (50, 200, 0))
self._data.itemImport.write(None, self.tr("New"), (100, 100, 100))
self._data.itemImport.write(None, self.tr("Minor"), (200, 50, 0))
self._data.itemImport.write(None, self.tr("Major"), (200, 150, 0))
self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0))
return
##
# Zip/Unzip Project
##
@@ -753,68 +622,6 @@ class NWProject(QObject):
return True
def extractSampleProject(self, projData):
"""Make a copy of the sample project.
First, look for the sample.zip file in the assets folder and
unpack it. If it doesn't exist, try to copy the content of the
sample folder to the new project path. If neither exits, error.
"""
projPath = projData.get("projPath", None)
if projPath is None:
logger.error("No project path set for the example project")
return False
srcSample = os.path.abspath(os.path.join(self.mainConf.appRoot, "sample"))
pkgSample = os.path.join(self.mainConf.assetPath, "sample.zip")
isSuccess = False
if os.path.isfile(pkgSample):
self.setProjectPath(projPath, newProject=True)
try:
shutil.unpack_archive(pkgSample, projPath)
isSuccess = True
except Exception as exc:
self.mainGui.makeAlert(self.tr(
"Failed to create a new example project."
), nwAlert.ERROR, exception=exc)
elif os.path.isdir(srcSample):
self.setProjectPath(projPath, newProject=True)
try:
srcProj = os.path.join(srcSample, nwFiles.PROJ_FILE)
dstProj = os.path.join(projPath, nwFiles.PROJ_FILE)
shutil.copyfile(srcProj, dstProj)
srcContent = os.path.join(srcSample, "content")
dstContent = os.path.join(projPath, "content")
for srcFile in os.listdir(srcContent):
srcDoc = os.path.join(srcContent, srcFile)
dstDoc = os.path.join(dstContent, srcFile)
shutil.copyfile(srcDoc, dstDoc)
isSuccess = True
except Exception as exc:
self.mainGui.makeAlert(self.tr(
"Failed to create a new example project."
), nwAlert.ERROR, exception=exc)
else:
self.mainGui.makeAlert(self.tr(
"Failed to create a new example project. "
"Could not find the necessary files. "
"They seem to be missing from this installation."
), nwAlert.ERROR)
if isSuccess:
self.clearProject()
self.mainGui.openProject(projPath)
self.mainGui.rebuildIndex()
return isSuccess
##
# Setters
##
+5 -25
View File
@@ -50,9 +50,9 @@ from novelwriter.dialogs import (
from novelwriter.tools import (
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
)
from novelwriter.core import NWProject
from novelwriter.core import NWProject, ProjectBuilder
from novelwriter.enum import (
nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView
nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwView
)
from novelwriter.common import getGuiItem, hexToInt
from novelwriter.constants import nwFiles
@@ -365,30 +365,10 @@ class GuiMain(QMainWindow):
return False
logger.info("Creating new project")
if self.theProject.newProject(projData):
self.hasProject = True
self.idleRefTime = time()
self.idleTime = 0.0
self.rebuildTrees()
self.saveProject()
self.docEditor.setDictionaries()
self.projView.openProjectTasks()
self.novelView.openProjectTasks()
self.outlineView.openProjectTasks()
self.rebuildIndex(beQuiet=True)
self.mainStatus.setRefTime(self.theProject.projOpened)
self.mainStatus.setProjectStatus(nwState.GOOD)
self.mainStatus.setDocumentStatus(nwState.NONE)
self.mainStatus.setStatus(self.tr("New project created ..."))
self._updateWindowTitle(self.theProject.data.name)
nwProject = ProjectBuilder(self)
if nwProject.buildProject(projData):
self.openProject(projPath)
else:
self.theProject.clearProject()
return False
return True