Merge pull request #177 from vkbo/splash_screen

Project Load Screen
This commit is contained in:
Veronica K. Berglyd Olsen
2020-02-26 20:30:41 +01:00
committed by GitHub
15 changed files with 447 additions and 131 deletions
+6 -1
View File
@@ -87,6 +87,7 @@ def main(sysArgs=None):
"logfile=", "logfile=",
"version", "version",
"config=", "config=",
"data=",
"testmode", "testmode",
"style=", "style=",
] ]
@@ -105,6 +106,7 @@ def main(sysArgs=None):
" -l, --logfile= Specify log file.\n" " -l, --logfile= Specify log file.\n"
" --style= Set Qt5 style flag. Defaults to 'Fusion'.\n" " --style= Set Qt5 style flag. Defaults to 'Fusion'.\n"
" --config= Alternative config file.\n" " --config= Alternative config file.\n"
" --data= Alternative user data path.\n"
" --headless Do not display GUI. Useful for testing scripts.\n" " --headless Do not display GUI. Useful for testing scripts.\n"
).format( ).format(
appname = __package__, appname = __package__,
@@ -120,6 +122,7 @@ def main(sysArgs=None):
toFile = False toFile = False
toStd = True toStd = True
confPath = None confPath = None
dataPath = None
testMode = False testMode = False
qtStyle = "Fusion" qtStyle = "Fusion"
cmdOpen = None cmdOpen = None
@@ -157,6 +160,8 @@ def main(sysArgs=None):
qtStyle = inArg qtStyle = inArg
elif inOpt in ("--config"): elif inOpt in ("--config"):
confPath = inArg confPath = inArg
elif inOpt in ("--data"):
dataPath = inArg
elif inOpt in ("--testmode"): elif inOpt in ("--testmode"):
testMode = True testMode = True
@@ -187,7 +192,7 @@ def main(sysArgs=None):
logger.setLevel(debugLevel) logger.setLevel(debugLevel)
CONFIG.initConfig(confPath) CONFIG.initConfig(confPath, dataPath)
if testMode: if testMode:
nwGUI = GuiMain() nwGUI = GuiMain()
+19
View File
@@ -88,6 +88,25 @@ def colRange(rgbStart, rgbEnd, nStep):
return retCol return retCol
def formatInt(theInt):
"""Formats an integer with k, M, G etc.
"""
postFix = ["k","M","G","T","P","E"]
theVal = float(theInt)
if theVal > 1000.0:
for pF in postFix:
theVal /= 1000.0
if theVal < 1000.0:
if theVal < 10.0:
return "%4.2f%s" % (theVal,pF)
elif theVal < 100.0:
return "%4.1f%s" % (theVal,pF)
else:
return "%3.0f%s" % (theVal,pF)
return "%d" % theInt
def splitVersionNumber(vString): def splitVersionNumber(vString):
""" Splits a version string on the form aa.bb.cc into major, minor """ Splits a version string on the form aa.bb.cc into major, minor
and patch, and computes an integer value aabbcc. and patch, and computes an integer value aabbcc.
+126 -45
View File
@@ -12,10 +12,11 @@
import logging import logging
import configparser import configparser
import json
import sys import sys
import nw import nw
from os import path, mkdir from os import path, mkdir, unlink, rename
from datetime import datetime from datetime import datetime
from PyQt5.Qt import PYQT_VERSION_STR from PyQt5.Qt import PYQT_VERSION_STR
@@ -116,9 +117,6 @@ class Config:
self.showRefPanel = True self.showRefPanel = True
self.viewComments = True self.viewComments = True
## Path
self.recentList = [""]*10
# Check Qt5 Versions # Check Qt5 Versions
verQt = splitVersionNumber(QT_VERSION_STR) verQt = splitVersionNumber(QT_VERSION_STR)
self.verQtString = QT_VERSION_STR self.verQtString = QT_VERSION_STR
@@ -162,13 +160,19 @@ class Config:
self.hasEnchant = False self.hasEnchant = False
self.hasSymSpell = False self.hasSymSpell = False
# Recent Cache
self.recentProj = {}
return return
## ##
# Actions # Actions
## ##
def initConfig(self, confPath=None): def initConfig(self, confPath=None, dataPath=None):
"""Initialise the config class. The manual setting of confPath
and dataPath is mainly intended for the test suite.
"""
if confPath is None: if confPath is None:
confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation) confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)
@@ -177,11 +181,15 @@ class Config:
logger.info("Setting config from alternative path: %s" % confPath) logger.info("Setting config from alternative path: %s" % confPath)
self.confPath = confPath self.confPath = confPath
if self.verQtValue >= 50400: if dataPath is None:
dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation) if self.verQtValue >= 50400:
dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
else:
dataRoot = QStandardPaths.writableLocation(QStandardPaths.DataLocation)
self.dataPath = path.join(path.abspath(dataRoot), self.appHandle)
else: else:
dataRoot = QStandardPaths.writableLocation(QStandardPaths.DataLocation) logger.info("Setting data path from alternative path: %s" % dataPath)
self.dataPath = path.join(path.abspath(dataRoot), self.appHandle) self.dataPath = dataPath
logger.verbose("Config path: %s" % self.confPath) logger.verbose("Config path: %s" % self.confPath)
logger.verbose("Data path: %s" % self.dataPath) logger.verbose("Data path: %s" % self.dataPath)
@@ -212,25 +220,30 @@ class Config:
self.confPath = None self.confPath = None
# Check if config file exists # Check if config file exists
if path.isfile(path.join(self.confPath,self.confFile)): if self.confPath is not None:
# If it exists, load it if path.isfile(path.join(self.confPath,self.confFile)):
self.loadConfig() # If it exists, load it
else: self.loadConfig()
# If it does not exist, save a copy of the default values else:
self.saveConfig() # If it does not exist, save a copy of the default values
self.saveConfig()
# If data folder does not exist, make it. # If data folder does not exist, make it.
# This assumes that the os data folder itself exists. # This assumes that the os data folder itself exists.
if not path.isdir(self.dataPath): if self.dataPath is not None:
try: if not path.isdir(self.dataPath):
mkdir(self.dataPath) try:
except Exception as e: mkdir(self.dataPath)
logger.error("Could not create folder: %s" % self.dataPath) except Exception as e:
logger.error(str(e)) logger.error("Could not create folder: %s" % self.dataPath)
self.hasError = True logger.error(str(e))
self.errData.append("Could not create folder: %s" % self.dataPath) self.hasError = True
self.errData.append(str(e)) self.errData.append("Could not create folder: %s" % self.dataPath)
self.dataPath = None self.errData.append(str(e))
self.dataPath = None
# Load recent projects cache
self.loadRecentCache()
# Check the availability of optional packages # Check the availability of optional packages
self._checkOptionalPackages() self._checkOptionalPackages()
@@ -389,10 +402,6 @@ class Config:
self.lastPath = self._parseLine( self.lastPath = self._parseLine(
cnfParse, cnfSec, "lastpath", self.CNF_STR, self.lastPath cnfParse, cnfSec, "lastpath", self.CNF_STR, self.lastPath
) )
for i in range(10):
self.recentList[i] = self._parseLine(
cnfParse, cnfSec, "recent%d" % i,self.CNF_STR, self.recentList[i]
)
# Check Certain Values for None # Check Certain Values for None
self.spellLanguage = self._checkNone(self.spellLanguage) self.spellLanguage = self._checkNone(self.spellLanguage)
@@ -471,8 +480,6 @@ class Config:
cnfSec = "Path" cnfSec = "Path"
cnfParse.add_section(cnfSec) cnfParse.add_section(cnfSec)
cnfParse.set(cnfSec,"lastpath", str(self.lastPath)) cnfParse.set(cnfSec,"lastpath", str(self.lastPath))
for i in range(10):
cnfParse.set(cnfSec,"recent%d" % i, str(self.recentList[i]))
# Write config file # Write config file
try: try:
@@ -488,21 +495,86 @@ class Config:
return True return True
def loadRecentCache(self):
"""Load the cache file for recent projects.
"""
if self.dataPath is None:
return False
cacheFile = path.join(self.dataPath, nwFiles.RECENT_FILE)
self.recentProj = {}
if path.isfile(cacheFile):
try:
with open(cacheFile, mode="r", encoding="utf8") as inFile:
theJson = inFile.read()
theData = json.loads(theJson)
for projPath in theData.keys():
theEntry = theData[projPath]
theTitle = ""
lastTime = 0
wordCount = 0
if "title" in theEntry.keys():
theTitle = theEntry["title"]
if "time" in theEntry.keys():
lastTime = int(theEntry["time"])
if "words" in theEntry.keys():
wordCount = int(theEntry["words"])
self.recentProj[projPath] = {
"title" : theTitle,
"time" : lastTime,
"words" : wordCount,
}
except Exception as e:
self.hasError = True
self.errData.append("Could not load recent project cache")
self.errData.append(str(e))
return False
return True
def saveRecentCache(self):
"""Save the cache dictionary of recent projects.
"""
if self.dataPath is None:
return False
cacheFile = path.join(self.dataPath, nwFiles.RECENT_FILE)
cacheTemp = path.join(self.dataPath, nwFiles.RECENT_FILE+"~")
try:
with open(cacheTemp, mode="w+", encoding="utf8") as outFile:
outFile.write(json.dumps(self.recentProj, indent=2))
except Exception as e:
self.hasError = True
self.errData.append("Could not save recent project cache")
self.errData.append(str(e))
return False
if path.isfile(cacheFile):
unlink(cacheFile)
rename(cacheTemp, cacheFile)
return True
def updateRecentCache(self, projPath, projTitle, wordCount, saveTime):
"""Add or update recent cache information o9n a given project.
"""
self.recentProj[path.abspath(projPath)] = {
"title" : projTitle,
"time" : int(saveTime),
"words" : int(wordCount),
}
return True
## ##
# Setters and Getters # Setters and Getters
## ##
def setRecent(self, recentPath):
if recentPath == "": return
if recentPath in self.recentList[0:10]:
self.recentList.remove(recentPath)
self.recentList.insert(0,recentPath)
return
def clearRecent(self):
self.recentList = [""]*10
return
def setConfPath(self, newPath): def setConfPath(self, newPath):
if newPath is None: if newPath is None:
return True return True
@@ -513,6 +585,15 @@ class Config:
self.confFile = path.basename(newPath) self.confFile = path.basename(newPath)
return True return True
def setDataPath(self, newPath):
if newPath is None:
return True
if not path.isdir(newPath):
logger.error("Config: Path not found. Using default data path instead.")
return False
self.dataPath = path.abspath(newPath)
return True
def setLastPath(self, lastPath): def setLastPath(self, lastPath):
if lastPath is None or lastPath == "": if lastPath is None or lastPath == "":
self.lastPath = "" self.lastPath = ""
@@ -547,12 +628,12 @@ class Config:
def setShowRefPanel(self, checkState): def setShowRefPanel(self, checkState):
self.showRefPanel = checkState self.showRefPanel = checkState
self.confChanged = True self.confChanged = True
return return self.showRefPanel
def setViewComments(self, checkState): def setViewComments(self, checkState):
self.viewComments = checkState self.viewComments = checkState
self.confChanged = True self.confChanged = True
return return self.viewComments
def getErrData(self): def getErrData(self):
errMessage = "<br>".join(self.errData) errMessage = "<br>".join(self.errData)
@@ -596,7 +677,7 @@ class Config:
if checkVal is None: if checkVal is None:
return None return None
if isinstance(checkVal, str): if isinstance(checkVal, str):
if checkVal.lower == "none": if checkVal.lower() == "none":
return None return None
return checkVal return checkVal
+7 -6
View File
@@ -20,12 +20,13 @@ class nwConst():
class nwFiles(): class nwFiles():
APP_ICON = "novelWriter.svg" APP_ICON = "novelWriter.svg"
PROJ_FILE = "nwProject.nwx" PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt" PROJ_DICT = "wordlist.txt"
SESS_INFO = "sessionInfo.log" SESS_INFO = "sessionInfo.log"
INDEX_FILE = "tagsIndex.json" INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json" OPTS_FILE = "guiOptions.json"
RECENT_FILE = "recentProjects.json"
# END Class nwFiles # END Class nwFiles
+2
View File
@@ -13,6 +13,7 @@ from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.export import GuiExport from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor from nw.gui.dialogs.projecteditor import GuiProjectEditor
from nw.gui.dialogs.projectload import GuiProjectLoad
from nw.gui.dialogs.sessionlog import GuiSessionLogView from nw.gui.dialogs.sessionlog import GuiSessionLogView
from nw.gui.dialogs.timelineview import GuiTimeLineView from nw.gui.dialogs.timelineview import GuiTimeLineView
@@ -40,6 +41,7 @@ __all__ = [
"GuiExport", "GuiExport",
"GuiItemEditor", "GuiItemEditor",
"GuiProjectEditor", "GuiProjectEditor",
"GuiProjectLoad",
"GuiSessionLogView", "GuiSessionLogView",
"GuiTimeLineView", "GuiTimeLineView",
"GuiDocDetails", "GuiDocDetails",
+2
View File
@@ -6,6 +6,7 @@ from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.export import GuiExport from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor from nw.gui.dialogs.projecteditor import GuiProjectEditor
from nw.gui.dialogs.projectload import GuiProjectLoad
from nw.gui.dialogs.sessionlog import GuiSessionLogView from nw.gui.dialogs.sessionlog import GuiSessionLogView
from nw.gui.dialogs.timelineview import GuiTimeLineView from nw.gui.dialogs.timelineview import GuiTimeLineView
@@ -16,6 +17,7 @@ __all__ = [
"GuiExport", "GuiExport",
"GuiItemEditor", "GuiItemEditor",
"GuiProjectEditor", "GuiProjectEditor",
"GuiProjectLoad",
"GuiSessionLogView", "GuiSessionLogView",
"GuiTimeLineView", "GuiTimeLineView",
] ]
+169
View File
@@ -0,0 +1,169 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Open Project
novelWriter GUI Open Project
================================
New and open project dialog
File History:
Created: 2020-02-26 [0.4.5]
"""
import logging
import nw
from datetime import datetime
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QTreeWidget,
QAbstractItemView, QTreeWidgetItem
)
from nw.common import formatInt
logger = logging.getLogger(__name__)
class GuiProjectLoad(QDialog):
def __init__(self, theParent):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiProjectLoad ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.sourceItem = None
self.openPath = None
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Open Project")
self.setLayout(self.outerBox)
self.guiDeco = self.theParent.theTheme.loadDecoration("nwicon", (128, 128))
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.projectForm = QGridLayout()
self.projectForm.setContentsMargins(0, 0, 0, 0)
self.listBox = QTreeWidget()
self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.listBox.setColumnCount(4)
self.listBox.setHeaderLabels(["Working Title","Words","Accessed","Path"])
self.listBox.setRootIsDecorated(False)
treeHead = self.listBox.headerItem()
treeHead.setTextAlignment(1, Qt.AlignRight)
self.recentButton = QPushButton("Open")
self.recentButton.clicked.connect(self._doOpenRecent)
self.browseButton = QPushButton("Browse")
self.browseButton.clicked.connect(self._doBrowse)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.projectForm.addWidget(self.listBox, 0, 0, 1, 4)
self.projectForm.addWidget(self.recentButton, 1, 1)
self.projectForm.addWidget(self.browseButton, 1, 2)
self.projectForm.addWidget(self.closeButton, 1, 3)
self.projectForm.setColumnStretch(0, 1)
self.innerBox.addLayout(self.projectForm)
self.rejected.connect(self._doClose)
self.setModal(True)
self.setMinimumWidth(750)
self.setMinimumHeight(450)
self.show()
self._populateList()
logger.debug("GuiProjectLoad initialisation complete")
return
##
# Buttons
##
def _doOpenRecent(self):
"""Close the dialog window with a recent project selected.
"""
logger.verbose("GuiProjectLoad open button clicked")
selItems = self.listBox.selectedItems()
if selItems:
self.openPath = selItems[0].text(3)
self.accept()
else:
self.openPath = None
return
def _doBrowse(self):
"""Close the dialog window with no selected path, triggering the
project browser dialog.
"""
logger.verbose("GuiProjectLoad browse button clicked")
self.openPath = None
self.accept()
return
def _doClose(self):
"""Close the dialog window without doing anything.
"""
logger.verbose("GuiProjectLoad close button clicked")
self.close()
return
##
# Internal Functions
##
def _populateList(self):
"""Populate the list box with recent project data.
"""
listOrder = []
listData = {}
for projPath in self.mainConf.recentProj.keys():
theEntry = self.mainConf.recentProj[projPath]
theTitle = ""
theTime = 0
theWords = 0
if "title" in theEntry.keys():
theTitle = theEntry["title"]
if "time" in theEntry.keys():
theTime = theEntry["time"]
if "words" in theEntry.keys():
theWords = theEntry["words"]
if theTime > 0:
listOrder.append(theTime)
listData[theTime] = [theTitle, theWords, projPath]
self.listBox.clear()
hasSelection = False
for timeStamp in sorted(listOrder, reverse=True):
newItem = QTreeWidgetItem([""]*4)
newItem.setText(0, listData[timeStamp][0])
newItem.setText(1, formatInt(listData[timeStamp][1]))
newItem.setText(2, datetime.fromtimestamp(timeStamp).strftime("%x %X"))
newItem.setText(3, listData[timeStamp][2])
newItem.setTextAlignment(1, Qt.AlignRight)
self.listBox.addTopLevelItem(newItem)
if not hasSelection:
newItem.setSelected(True)
hasSelection = True
self.listBox.resizeColumnToContents(0)
self.listBox.resizeColumnToContents(1)
self.listBox.resizeColumnToContents(2)
return
# END Class GuiProjectLoad
+27 -5
View File
@@ -45,10 +45,11 @@ class GuiIcons:
} }
DECO_MAP = { DECO_MAP = {
"export" : "export.svg", "nwicon" : ["icons", "novelWriter.svg"],
"merge" : "merge.svg", "export" : ["graphics", "export.svg"],
"settings" : "gear.svg", "merge" : ["graphics", "merge.svg"],
"split" : "split.svg", "settings" : ["graphics", "gear.svg"],
"split" : ["graphics", "split.svg"],
} }
def __init__(self, theParent): def __init__(self, theParent):
@@ -67,6 +68,9 @@ class GuiIcons:
return return
def initIcons(self, priPath): def initIcons(self, priPath):
"""Load all icons listed in the icon map. Can be overridden by
the selected theme.
"""
self.priPath = priPath self.priPath = priPath
self.secPath = self.mainConf.iconPath self.secPath = self.mainConf.iconPath
@@ -78,12 +82,19 @@ class GuiIcons:
return return
def loadDecoration(self, decoKey, decoSize=None): def loadDecoration(self, decoKey, decoSize=None):
"""Load graphical decoration element based on the decoration
map. This function always returns a QSwgWidget.
"""
if decoKey not in self.DECO_MAP: if decoKey not in self.DECO_MAP:
logger.error("Decoration with name '%s' does not exist" % decoKey) logger.error("Decoration with name '%s' does not exist" % decoKey)
return QSvgWidget() return QSvgWidget()
svgPath = path.join(self.mainConf.graphPath, self.DECO_MAP[decoKey]) svgPath = path.join(
self.mainConf.assetPath,
self.DECO_MAP[decoKey][0],
self.DECO_MAP[decoKey][1]
)
if not path.isfile(svgPath): if not path.isfile(svgPath):
logger.error("Decoration file '%s' not in assets folder" % self.DECO_MAP[decoKey]) logger.error("Decoration file '%s' not in assets folder" % self.DECO_MAP[decoKey])
return QSvgWidget() return QSvgWidget()
@@ -95,11 +106,17 @@ class GuiIcons:
return svgDeco return svgDeco
def getIcon(self, iconKey, iconSize=None): def getIcon(self, iconKey, iconSize=None):
"""Return an icon from the icon buffer. If it doesn't exist,
return an empty icon.
"""
if iconKey in self.qIcons: if iconKey in self.qIcons:
return self.qIcons[iconKey] return self.qIcons[iconKey]
return QIcon() return QIcon()
def getPixmap(self, iconKey, iconSize): def getPixmap(self, iconKey, iconSize):
"""Return an icon from the icon buffer as a QPixmap. If it
doesn't exist, return an empty QPixmap.
"""
if iconKey in self.qIcons: if iconKey in self.qIcons:
return self.qIcons[iconKey].pixmap(iconSize[0], iconSize[1], QIcon.Normal) return self.qIcons[iconKey].pixmap(iconSize[0], iconSize[1], QIcon.Normal)
return QPixmap() return QPixmap()
@@ -109,6 +126,11 @@ class GuiIcons:
## ##
def _loadIcon(self, iconKey): def _loadIcon(self, iconKey):
"""Load an icon from the assets or theme folder, with a
preference for dark/light icons depending on theme type, if such
an icon exists. Prefer svg files over png files. Always returns
a QIcon.
"""
if iconKey not in self.ICON_MAP: if iconKey not in self.ICON_MAP:
logger.error("Icon with name '%s' does not exist" % iconKey) logger.error("Icon with name '%s' does not exist" % iconKey)
+1 -39
View File
@@ -48,11 +48,6 @@ class GuiMainMenu(QMenuBar):
return return
def openRecentProject(self, menuItem, recentItem):
logger.verbose("User requested opening recent project #%d" % recentItem)
self.theParent.openProject(self.mainConf.recentList[recentItem])
return True
def setAvailableRoot(self): def setAvailableRoot(self):
for itemClass in nwItemClass: for itemClass in nwItemClass:
if itemClass == nwItemClass.NO_CLASS: continue if itemClass == nwItemClass.NO_CLASS: continue
@@ -66,30 +61,6 @@ class GuiMainMenu(QMenuBar):
# Update Menu on Settings Changed # Update Menu on Settings Changed
## ##
def updateMenu(self):
self.updateRecentProjects()
return
def updateRecentProjects(self):
self.recentMenu.clear()
for n in range(len(self.mainConf.recentList)):
recentProject = self.mainConf.recentList[n]
if recentProject == "": continue
menuItem = QAction("%s" % recentProject, self.projMenu)
menuItem.triggered.connect(
lambda a1=menuItem, a2=n : self.openRecentProject(a1, a2)
)
self.recentMenu.addAction(menuItem)
self.recentMenu.addSeparator()
menuItem = QAction("Clear Recent Projects", self)
menuItem.setStatusTip("Clear the list of recent projects")
menuItem.triggered.connect(self._clearRecentProjects)
self.recentMenu.addAction(menuItem)
return
def setSpellCheck(self, theMode): def setSpellCheck(self, theMode):
"""Set the spell check check box to theMode. This is controlled """Set the spell check check box to theMode. This is controlled
by the document editor class, which holds the master spell check by the document editor class, which holds the master spell check
@@ -166,11 +137,6 @@ class GuiMainMenu(QMenuBar):
QDesktopServices.openUrl(QUrl(nw.__docurl__)) QDesktopServices.openUrl(QUrl(nw.__docurl__))
return True return True
def _clearRecentProjects(self):
self.mainConf.clearRecent()
self.updateRecentProjects()
return True
def _showDocumentLocation(self): def _showDocumentLocation(self):
self.theParent.docEditor.revealLocation() self.theParent.docEditor.revealLocation()
return True return True
@@ -194,7 +160,7 @@ class GuiMainMenu(QMenuBar):
self.aOpenProject = QAction("Open Project", self) self.aOpenProject = QAction("Open Project", self)
self.aOpenProject.setStatusTip("Open project") self.aOpenProject.setStatusTip("Open project")
self.aOpenProject.setShortcut("Ctrl+Shift+O") self.aOpenProject.setShortcut("Ctrl+Shift+O")
self.aOpenProject.triggered.connect(lambda : self.theParent.openProject(None)) self.aOpenProject.triggered.connect(self.theParent.manageProjects)
self.projMenu.addAction(self.aOpenProject) self.projMenu.addAction(self.aOpenProject)
# Project > Save Project # Project > Save Project
@@ -211,10 +177,6 @@ class GuiMainMenu(QMenuBar):
self.aCloseProject.triggered.connect(lambda : self.theParent.closeProject(False)) self.aCloseProject.triggered.connect(lambda : self.theParent.closeProject(False))
self.projMenu.addAction(self.aCloseProject) self.projMenu.addAction(self.aCloseProject)
# Project > Recent Projects
self.recentMenu = self.projMenu.addMenu("Recent Projects")
self.updateRecentProjects()
# Project > Project Settings # Project > Project Settings
self.aProjectSettings = QAction("Project Settings", self) self.aProjectSettings = QAction("Project Settings", self)
self.aProjectSettings.setStatusTip("Project settings") self.aProjectSettings.setStatusTip("Project settings")
+26 -9
View File
@@ -27,7 +27,7 @@ from nw.gui import (
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiExport, GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiExport,
GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails, GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails,
GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiTimeLineView, GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiTimeLineView,
GuiSessionLogView, GuiDocMerge, GuiDocSplit GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad
) )
from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup
from nw.tools import countWords from nw.tools import countWords
@@ -64,7 +64,7 @@ class GuiMain(QMainWindow):
self.resize(*self.mainConf.winGeometry) self.resize(*self.mainConf.winGeometry)
self._setWindowTitle() self._setWindowTitle()
self.setWindowIcon(QIcon(path.join(self.mainConf.appIcon))) self.setWindowIcon(QIcon(self.mainConf.appIcon))
# Main GUI Elements # Main GUI Elements
self.statusBar = GuiMainStatus(self) self.statusBar = GuiMainStatus(self)
@@ -183,6 +183,8 @@ class GuiMain(QMainWindow):
if self.mainConf.cmdOpen is not None: if self.mainConf.cmdOpen is not None:
logger.debug("Opening project from additional command line option") logger.debug("Opening project from additional command line option")
self.openProject(self.mainConf.cmdOpen) self.openProject(self.mainConf.cmdOpen)
else:
self.manageProjects()
return return
@@ -204,13 +206,30 @@ class GuiMain(QMainWindow):
# Project Actions # Project Actions
## ##
def manageProjects(self):
"""Opens the projects dialog for selecting either existing
projects from a cache of recently opened projects, or provide a
browse button for projects not yet cached.
"""
if not self.mainConf.showGUI:
return False
dlgProj = GuiProjectLoad(self)
dlgProj.exec_()
if dlgProj.result() == QDialog.Accepted:
self.openProject(dlgProj.openPath)
return True
def newProject(self, projPath=None, forceNew=False): def newProject(self, projPath=None, forceNew=False):
"""Create new project with a few default files and folders.
"""
if self.hasProject: if self.hasProject:
msgBox = QMessageBox() msgBox = QMessageBox()
msgRes = msgBox.warning( msgRes = msgBox.warning(
self, "New Project", self, "New Project",
"Please close the current project<br>before making a new one." "Please close the current project before making a new one."
) )
return False return False
@@ -223,7 +242,7 @@ class GuiMain(QMainWindow):
msgBox = QMessageBox() msgBox = QMessageBox()
msgRes = msgBox.critical( msgRes = msgBox.critical(
self, "New Project", self, "New Project",
"A project already exists in that location.<br>Please choose another folder." "A project already exists in that location. Please choose another folder."
) )
return False return False
@@ -238,9 +257,9 @@ class GuiMain(QMainWindow):
return True return True
def closeProject(self, isYes=False): def closeProject(self, isYes=False):
"""Closes the project if one is open. """Closes the project if one is open. isYes is passed on from
isYes is passed on from the close application event so the user the close application event so the user doesn't get prompted
doesn't get prompted twice. twice.
""" """
if not self.hasProject: if not self.hasProject:
# There is no project loaded, everything OK # There is no project loaded, everything OK
@@ -314,7 +333,6 @@ class GuiMain(QMainWindow):
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.docEditor.setSpellCheck(self.theProject.spellCheck) self.docEditor.setSpellCheck(self.theProject.spellCheck)
self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setRefTime(self.theProject.projOpened)
self.mainMenu.updateMenu()
# Restore previously open documents, if any # Restore previously open documents, if any
if self.theProject.lastEdited is not None: if self.theProject.lastEdited is not None:
@@ -344,7 +362,6 @@ class GuiMain(QMainWindow):
self.treeView.saveTreeOrder() self.treeView.saveTreeOrder()
self.theProject.saveProject() self.theProject.saveProject()
self.theIndex.saveIndex() self.theIndex.saveIndex()
self.mainMenu.updateRecentProjects()
return True return True
+13 -3
View File
@@ -297,7 +297,11 @@ class NWProject():
self._appendItem(tHandle,pHandle,nwItem) self._appendItem(tHandle,pHandle,nwItem)
self.optState.loadSettings() self.optState.loadSettings()
self.mainConf.setRecent(self.projPath)
# Update recent projects
self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time())
self.mainConf.saveRecentCache()
self.theParent.setStatus("Opened Project: %s" % self.projName) self.theParent.setStatus("Opened Project: %s" % self.projName)
self._scanProjectFolder() self._scanProjectFolder()
@@ -319,6 +323,7 @@ class NWProject():
return False return False
self.projMeta = path.join(self.projPath,"meta") self.projMeta = path.join(self.projPath,"meta")
saveTime = time()
if not self._checkFolder(self.projPath): return if not self._checkFolder(self.projPath): return
if not self._checkFolder(self.projMeta): return if not self._checkFolder(self.projMeta): return
@@ -330,7 +335,7 @@ class NWProject():
nwXML = etree.Element("novelWriterXML",attrib={ nwXML = etree.Element("novelWriterXML",attrib={
"appVersion" : str(nw.__version__), "appVersion" : str(nw.__version__),
"fileVersion" : "1.0", "fileVersion" : "1.0",
"timeStamp" : datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "timeStamp" : datetime.fromtimestamp(saveTime).strftime("%Y-%m-%d %H:%M:%S"),
}) })
# Save Project Meta # Save Project Meta
@@ -386,8 +391,13 @@ class NWProject():
rename(saveFile, backFile) rename(saveFile, backFile)
rename(tempFile, saveFile) rename(tempFile, saveFile)
# Save project GUI options
self.optState.saveSettings() self.optState.saveSettings()
self.mainConf.setRecent(self.projPath)
# Update recent projects
self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime)
self.mainConf.saveRecentCache()
self.theParent.setStatus("Saved Project: %s" % self.projName) self.theParent.setStatus("Saved Project: %s" % self.projName)
self.setProjectChanged(False) self.setProjectChanged(False)
+1 -11
View File
@@ -1,5 +1,5 @@
[Main] [Main]
timestamp = 2019-11-19 21:49:29 timestamp = 2020-02-26 18:10:35
theme = default theme = default
syntax = default_light syntax = default_light
guidark = False guidark = False
@@ -49,14 +49,4 @@ viewcomments = True
[Path] [Path]
lastpath = lastpath =
recent0 =
recent1 =
recent2 =
recent3 =
recent4 =
recent5 =
recent6 =
recent7 =
recent8 =
recent9 =
+38 -2
View File
@@ -13,7 +13,7 @@ theConf = Config()
def testConfigInit(nwTemp,nwRef): def testConfigInit(nwTemp,nwRef):
tmpConf = path.join(nwTemp,"novelwriter.conf") tmpConf = path.join(nwTemp,"novelwriter.conf")
refConf = path.join(nwRef, "novelwriter.conf") refConf = path.join(nwRef, "novelwriter.conf")
assert theConf.initConfig(nwTemp) assert theConf.initConfig(nwTemp, nwTemp)
assert theConf.setLastPath("") assert theConf.setLastPath("")
assert theConf.saveConfig() assert theConf.saveConfig()
assert cmpFiles(tmpConf, refConf, [2]) assert cmpFiles(tmpConf, refConf, [2])
@@ -37,6 +37,14 @@ def testConfigSetConfPath(nwTemp):
assert theConf.confFile == "novelwriter.conf" assert theConf.confFile == "novelwriter.conf"
assert not theConf.confChanged assert not theConf.confChanged
@pytest.mark.core
def testConfigSetDataPath(nwTemp):
assert theConf.setDataPath(None)
assert not theConf.setDataPath(path.join("somewhere","over","the","rainbow"))
assert theConf.setDataPath(nwTemp)
assert theConf.dataPath == nwTemp
assert not theConf.confChanged
@pytest.mark.core @pytest.mark.core
def testConfigLoad(): def testConfigLoad():
assert theConf.loadConfig() assert theConf.loadConfig()
@@ -67,7 +75,7 @@ def testConfigSetTreeColWidths(nwTemp,nwRef):
assert not theConf.confChanged assert not theConf.confChanged
@pytest.mark.core @pytest.mark.core
def testConfigSetMainPanePos(nwTemp,nwRef): def testConfigSetPanePos(nwTemp,nwRef):
tmpConf = path.join(nwTemp,"novelwriter.conf") tmpConf = path.join(nwTemp,"novelwriter.conf")
refConf = path.join(nwRef, "novelwriter.conf") refConf = path.join(nwRef, "novelwriter.conf")
assert theConf.setMainPanePos([0, 0]) assert theConf.setMainPanePos([0, 0])
@@ -77,3 +85,31 @@ def testConfigSetMainPanePos(nwTemp,nwRef):
assert theConf.saveConfig() assert theConf.saveConfig()
assert cmpFiles(tmpConf, refConf, [2]) assert cmpFiles(tmpConf, refConf, [2])
assert not theConf.confChanged assert not theConf.confChanged
@pytest.mark.core
def testConfigFlags(nwTemp,nwRef):
tmpConf = path.join(nwTemp,"novelwriter.conf")
refConf = path.join(nwRef, "novelwriter.conf")
assert not theConf.setShowRefPanel(False)
assert theConf.setShowRefPanel(True)
assert not theConf.setViewComments(False)
assert theConf.setViewComments(True)
assert theConf.confChanged
assert theConf.saveConfig()
assert cmpFiles(tmpConf, refConf, [2])
assert not theConf.confChanged
@pytest.mark.core
def testConfigErrors(nwTemp):
nonPath = path.join("somewhere","over","the","rainbow")
assert theConf.initConfig(nonPath, nonPath)
assert theConf.hasError
assert not theConf.loadConfig()
assert not theConf.saveConfig()
assert not theConf.loadRecentCache()
assert len(theConf.getErrData()) > 0
@pytest.mark.core
def testConfigInternals():
assert theConf._checkNone(None) is None
assert theConf._checkNone("None") is None
+8 -8
View File
@@ -18,8 +18,8 @@ keyDelay = 10
stepDelay = 50 stepDelay = 50
@pytest.mark.gui @pytest.mark.gui
def testMainWindows(qtbot, nwTempGUI, nwRef): def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI]) nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI) qtbot.addWidget(nwGUI)
nwGUI.show() nwGUI.show()
qtbot.waitForWindowShown(nwGUI) qtbot.waitForWindowShown(nwGUI)
@@ -254,8 +254,8 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
# qtbot.stopForInteraction() # qtbot.stopForInteraction()
@pytest.mark.gui @pytest.mark.gui
def testTimeLineView(qtbot, nwTempGUI, nwRef): def testTimeLineView(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI]) nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI) qtbot.addWidget(nwGUI)
nwGUI.show() nwGUI.show()
qtbot.waitForWindowShown(nwGUI) qtbot.waitForWindowShown(nwGUI)
@@ -276,8 +276,8 @@ def testTimeLineView(qtbot, nwTempGUI, nwRef):
nwGUI.closeMain() nwGUI.closeMain()
@pytest.mark.gui @pytest.mark.gui
def testProjectEditor(qtbot, nwTempGUI, nwRef): def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI]) nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI) qtbot.addWidget(nwGUI)
nwGUI.show() nwGUI.show()
qtbot.waitForWindowShown(nwGUI) qtbot.waitForWindowShown(nwGUI)
@@ -363,8 +363,8 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef):
# qtbot.stopForInteraction() # qtbot.stopForInteraction()
@pytest.mark.gui @pytest.mark.gui
def testItemEditor(qtbot, nwTempGUI, nwRef): def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI]) nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI) qtbot.addWidget(nwGUI)
nwGUI.show() nwGUI.show()
qtbot.waitForWindowShown(nwGUI) qtbot.waitForWindowShown(nwGUI)
+2 -2
View File
@@ -22,10 +22,10 @@ theProject = NWProject(theMain)
theProject.handleSeed = 42 theProject.handleSeed = 42
@pytest.mark.project @pytest.mark.project
def testProjectNew(nwTempProj,nwRef): def testProjectNew(nwTempProj,nwRef,nwTemp):
projFile = path.join(nwTempProj,"nwProject.nwx") projFile = path.join(nwTempProj,"nwProject.nwx")
refFile = path.join(nwRef,"proj","1_nwProject.nwx") refFile = path.join(nwRef,"proj","1_nwProject.nwx")
assert theConf.initConfig(nwRef) assert theConf.initConfig(nwRef, nwTemp)
assert theProject.newProject() assert theProject.newProject()
assert theProject.setProjectPath(nwTempProj) assert theProject.setProjectPath(nwTempProj)
assert theProject.saveProject() assert theProject.saveProject()