+6
-1
@@ -87,6 +87,7 @@ def main(sysArgs=None):
|
||||
"logfile=",
|
||||
"version",
|
||||
"config=",
|
||||
"data=",
|
||||
"testmode",
|
||||
"style=",
|
||||
]
|
||||
@@ -105,6 +106,7 @@ def main(sysArgs=None):
|
||||
" -l, --logfile= Specify log file.\n"
|
||||
" --style= Set Qt5 style flag. Defaults to 'Fusion'.\n"
|
||||
" --config= Alternative config file.\n"
|
||||
" --data= Alternative user data path.\n"
|
||||
" --headless Do not display GUI. Useful for testing scripts.\n"
|
||||
).format(
|
||||
appname = __package__,
|
||||
@@ -120,6 +122,7 @@ def main(sysArgs=None):
|
||||
toFile = False
|
||||
toStd = True
|
||||
confPath = None
|
||||
dataPath = None
|
||||
testMode = False
|
||||
qtStyle = "Fusion"
|
||||
cmdOpen = None
|
||||
@@ -157,6 +160,8 @@ def main(sysArgs=None):
|
||||
qtStyle = inArg
|
||||
elif inOpt in ("--config"):
|
||||
confPath = inArg
|
||||
elif inOpt in ("--data"):
|
||||
dataPath = inArg
|
||||
elif inOpt in ("--testmode"):
|
||||
testMode = True
|
||||
|
||||
@@ -187,7 +192,7 @@ def main(sysArgs=None):
|
||||
|
||||
logger.setLevel(debugLevel)
|
||||
|
||||
CONFIG.initConfig(confPath)
|
||||
CONFIG.initConfig(confPath, dataPath)
|
||||
|
||||
if testMode:
|
||||
nwGUI = GuiMain()
|
||||
|
||||
@@ -88,6 +88,25 @@ def colRange(rgbStart, rgbEnd, nStep):
|
||||
|
||||
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):
|
||||
""" Splits a version string on the form aa.bb.cc into major, minor
|
||||
and patch, and computes an integer value aabbcc.
|
||||
|
||||
+126
-45
@@ -12,10 +12,11 @@
|
||||
|
||||
import logging
|
||||
import configparser
|
||||
import json
|
||||
import sys
|
||||
import nw
|
||||
|
||||
from os import path, mkdir
|
||||
from os import path, mkdir, unlink, rename
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.Qt import PYQT_VERSION_STR
|
||||
@@ -116,9 +117,6 @@ class Config:
|
||||
self.showRefPanel = True
|
||||
self.viewComments = True
|
||||
|
||||
## Path
|
||||
self.recentList = [""]*10
|
||||
|
||||
# Check Qt5 Versions
|
||||
verQt = splitVersionNumber(QT_VERSION_STR)
|
||||
self.verQtString = QT_VERSION_STR
|
||||
@@ -162,13 +160,19 @@ class Config:
|
||||
self.hasEnchant = False
|
||||
self.hasSymSpell = False
|
||||
|
||||
# Recent Cache
|
||||
self.recentProj = {}
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# 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:
|
||||
confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)
|
||||
@@ -177,11 +181,15 @@ class Config:
|
||||
logger.info("Setting config from alternative path: %s" % confPath)
|
||||
self.confPath = confPath
|
||||
|
||||
if self.verQtValue >= 50400:
|
||||
dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
|
||||
if dataPath is None:
|
||||
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:
|
||||
dataRoot = QStandardPaths.writableLocation(QStandardPaths.DataLocation)
|
||||
self.dataPath = path.join(path.abspath(dataRoot), self.appHandle)
|
||||
logger.info("Setting data path from alternative path: %s" % dataPath)
|
||||
self.dataPath = dataPath
|
||||
|
||||
logger.verbose("Config path: %s" % self.confPath)
|
||||
logger.verbose("Data path: %s" % self.dataPath)
|
||||
@@ -212,25 +220,30 @@ class Config:
|
||||
self.confPath = None
|
||||
|
||||
# Check if config file exists
|
||||
if path.isfile(path.join(self.confPath,self.confFile)):
|
||||
# If it exists, load it
|
||||
self.loadConfig()
|
||||
else:
|
||||
# If it does not exist, save a copy of the default values
|
||||
self.saveConfig()
|
||||
if self.confPath is not None:
|
||||
if path.isfile(path.join(self.confPath,self.confFile)):
|
||||
# If it exists, load it
|
||||
self.loadConfig()
|
||||
else:
|
||||
# If it does not exist, save a copy of the default values
|
||||
self.saveConfig()
|
||||
|
||||
# If data folder does not exist, make it.
|
||||
# This assumes that the os data folder itself exists.
|
||||
if not path.isdir(self.dataPath):
|
||||
try:
|
||||
mkdir(self.dataPath)
|
||||
except Exception as e:
|
||||
logger.error("Could not create folder: %s" % self.dataPath)
|
||||
logger.error(str(e))
|
||||
self.hasError = True
|
||||
self.errData.append("Could not create folder: %s" % self.dataPath)
|
||||
self.errData.append(str(e))
|
||||
self.dataPath = None
|
||||
if self.dataPath is not None:
|
||||
if not path.isdir(self.dataPath):
|
||||
try:
|
||||
mkdir(self.dataPath)
|
||||
except Exception as e:
|
||||
logger.error("Could not create folder: %s" % self.dataPath)
|
||||
logger.error(str(e))
|
||||
self.hasError = True
|
||||
self.errData.append("Could not create folder: %s" % self.dataPath)
|
||||
self.errData.append(str(e))
|
||||
self.dataPath = None
|
||||
|
||||
# Load recent projects cache
|
||||
self.loadRecentCache()
|
||||
|
||||
# Check the availability of optional packages
|
||||
self._checkOptionalPackages()
|
||||
@@ -389,10 +402,6 @@ class Config:
|
||||
self.lastPath = self._parseLine(
|
||||
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
|
||||
self.spellLanguage = self._checkNone(self.spellLanguage)
|
||||
@@ -471,8 +480,6 @@ class Config:
|
||||
cnfSec = "Path"
|
||||
cnfParse.add_section(cnfSec)
|
||||
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
|
||||
try:
|
||||
@@ -488,21 +495,86 @@ class Config:
|
||||
|
||||
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
|
||||
##
|
||||
|
||||
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):
|
||||
if newPath is None:
|
||||
return True
|
||||
@@ -513,6 +585,15 @@ class Config:
|
||||
self.confFile = path.basename(newPath)
|
||||
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):
|
||||
if lastPath is None or lastPath == "":
|
||||
self.lastPath = ""
|
||||
@@ -547,12 +628,12 @@ class Config:
|
||||
def setShowRefPanel(self, checkState):
|
||||
self.showRefPanel = checkState
|
||||
self.confChanged = True
|
||||
return
|
||||
return self.showRefPanel
|
||||
|
||||
def setViewComments(self, checkState):
|
||||
self.viewComments = checkState
|
||||
self.confChanged = True
|
||||
return
|
||||
return self.viewComments
|
||||
|
||||
def getErrData(self):
|
||||
errMessage = "<br>".join(self.errData)
|
||||
@@ -596,7 +677,7 @@ class Config:
|
||||
if checkVal is None:
|
||||
return None
|
||||
if isinstance(checkVal, str):
|
||||
if checkVal.lower == "none":
|
||||
if checkVal.lower() == "none":
|
||||
return None
|
||||
return checkVal
|
||||
|
||||
|
||||
@@ -20,12 +20,13 @@ class nwConst():
|
||||
|
||||
class nwFiles():
|
||||
|
||||
APP_ICON = "novelWriter.svg"
|
||||
PROJ_FILE = "nwProject.nwx"
|
||||
PROJ_DICT = "wordlist.txt"
|
||||
SESS_INFO = "sessionInfo.log"
|
||||
INDEX_FILE = "tagsIndex.json"
|
||||
OPTS_FILE = "guiOptions.json"
|
||||
APP_ICON = "novelWriter.svg"
|
||||
PROJ_FILE = "nwProject.nwx"
|
||||
PROJ_DICT = "wordlist.txt"
|
||||
SESS_INFO = "sessionInfo.log"
|
||||
INDEX_FILE = "tagsIndex.json"
|
||||
OPTS_FILE = "guiOptions.json"
|
||||
RECENT_FILE = "recentProjects.json"
|
||||
|
||||
# END Class nwFiles
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from nw.gui.dialogs.docsplit import GuiDocSplit
|
||||
from nw.gui.dialogs.export import GuiExport
|
||||
from nw.gui.dialogs.itemeditor import GuiItemEditor
|
||||
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.timelineview import GuiTimeLineView
|
||||
|
||||
@@ -40,6 +41,7 @@ __all__ = [
|
||||
"GuiExport",
|
||||
"GuiItemEditor",
|
||||
"GuiProjectEditor",
|
||||
"GuiProjectLoad",
|
||||
"GuiSessionLogView",
|
||||
"GuiTimeLineView",
|
||||
"GuiDocDetails",
|
||||
|
||||
@@ -6,6 +6,7 @@ from nw.gui.dialogs.docsplit import GuiDocSplit
|
||||
from nw.gui.dialogs.export import GuiExport
|
||||
from nw.gui.dialogs.itemeditor import GuiItemEditor
|
||||
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.timelineview import GuiTimeLineView
|
||||
|
||||
@@ -16,6 +17,7 @@ __all__ = [
|
||||
"GuiExport",
|
||||
"GuiItemEditor",
|
||||
"GuiProjectEditor",
|
||||
"GuiProjectLoad",
|
||||
"GuiSessionLogView",
|
||||
"GuiTimeLineView",
|
||||
]
|
||||
|
||||
@@ -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
@@ -45,10 +45,11 @@ class GuiIcons:
|
||||
}
|
||||
|
||||
DECO_MAP = {
|
||||
"export" : "export.svg",
|
||||
"merge" : "merge.svg",
|
||||
"settings" : "gear.svg",
|
||||
"split" : "split.svg",
|
||||
"nwicon" : ["icons", "novelWriter.svg"],
|
||||
"export" : ["graphics", "export.svg"],
|
||||
"merge" : ["graphics", "merge.svg"],
|
||||
"settings" : ["graphics", "gear.svg"],
|
||||
"split" : ["graphics", "split.svg"],
|
||||
}
|
||||
|
||||
def __init__(self, theParent):
|
||||
@@ -67,6 +68,9 @@ class GuiIcons:
|
||||
return
|
||||
|
||||
def initIcons(self, priPath):
|
||||
"""Load all icons listed in the icon map. Can be overridden by
|
||||
the selected theme.
|
||||
"""
|
||||
|
||||
self.priPath = priPath
|
||||
self.secPath = self.mainConf.iconPath
|
||||
@@ -78,12 +82,19 @@ class GuiIcons:
|
||||
return
|
||||
|
||||
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:
|
||||
logger.error("Decoration with name '%s' does not exist" % decoKey)
|
||||
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):
|
||||
logger.error("Decoration file '%s' not in assets folder" % self.DECO_MAP[decoKey])
|
||||
return QSvgWidget()
|
||||
@@ -95,11 +106,17 @@ class GuiIcons:
|
||||
return svgDeco
|
||||
|
||||
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:
|
||||
return self.qIcons[iconKey]
|
||||
return QIcon()
|
||||
|
||||
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:
|
||||
return self.qIcons[iconKey].pixmap(iconSize[0], iconSize[1], QIcon.Normal)
|
||||
return QPixmap()
|
||||
@@ -109,6 +126,11 @@ class GuiIcons:
|
||||
##
|
||||
|
||||
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:
|
||||
logger.error("Icon with name '%s' does not exist" % iconKey)
|
||||
|
||||
+1
-39
@@ -48,11 +48,6 @@ class GuiMainMenu(QMenuBar):
|
||||
|
||||
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):
|
||||
for itemClass in nwItemClass:
|
||||
if itemClass == nwItemClass.NO_CLASS: continue
|
||||
@@ -66,30 +61,6 @@ class GuiMainMenu(QMenuBar):
|
||||
# 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):
|
||||
"""Set the spell check check box to theMode. This is controlled
|
||||
by the document editor class, which holds the master spell check
|
||||
@@ -166,11 +137,6 @@ class GuiMainMenu(QMenuBar):
|
||||
QDesktopServices.openUrl(QUrl(nw.__docurl__))
|
||||
return True
|
||||
|
||||
def _clearRecentProjects(self):
|
||||
self.mainConf.clearRecent()
|
||||
self.updateRecentProjects()
|
||||
return True
|
||||
|
||||
def _showDocumentLocation(self):
|
||||
self.theParent.docEditor.revealLocation()
|
||||
return True
|
||||
@@ -194,7 +160,7 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aOpenProject = QAction("Open Project", self)
|
||||
self.aOpenProject.setStatusTip("Open project")
|
||||
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)
|
||||
|
||||
# Project > Save Project
|
||||
@@ -211,10 +177,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aCloseProject.triggered.connect(lambda : self.theParent.closeProject(False))
|
||||
self.projMenu.addAction(self.aCloseProject)
|
||||
|
||||
# Project > Recent Projects
|
||||
self.recentMenu = self.projMenu.addMenu("Recent Projects")
|
||||
self.updateRecentProjects()
|
||||
|
||||
# Project > Project Settings
|
||||
self.aProjectSettings = QAction("Project Settings", self)
|
||||
self.aProjectSettings.setStatusTip("Project settings")
|
||||
|
||||
+26
-9
@@ -27,7 +27,7 @@ from nw.gui import (
|
||||
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiExport,
|
||||
GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails,
|
||||
GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiTimeLineView,
|
||||
GuiSessionLogView, GuiDocMerge, GuiDocSplit
|
||||
GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad
|
||||
)
|
||||
from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup
|
||||
from nw.tools import countWords
|
||||
@@ -64,7 +64,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
self.resize(*self.mainConf.winGeometry)
|
||||
self._setWindowTitle()
|
||||
self.setWindowIcon(QIcon(path.join(self.mainConf.appIcon)))
|
||||
self.setWindowIcon(QIcon(self.mainConf.appIcon))
|
||||
|
||||
# Main GUI Elements
|
||||
self.statusBar = GuiMainStatus(self)
|
||||
@@ -183,6 +183,8 @@ class GuiMain(QMainWindow):
|
||||
if self.mainConf.cmdOpen is not None:
|
||||
logger.debug("Opening project from additional command line option")
|
||||
self.openProject(self.mainConf.cmdOpen)
|
||||
else:
|
||||
self.manageProjects()
|
||||
|
||||
return
|
||||
|
||||
@@ -204,13 +206,30 @@ class GuiMain(QMainWindow):
|
||||
# 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):
|
||||
"""Create new project with a few default files and folders.
|
||||
"""
|
||||
|
||||
if self.hasProject:
|
||||
msgBox = QMessageBox()
|
||||
msgRes = msgBox.warning(
|
||||
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
|
||||
|
||||
@@ -223,7 +242,7 @@ class GuiMain(QMainWindow):
|
||||
msgBox = QMessageBox()
|
||||
msgRes = msgBox.critical(
|
||||
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
|
||||
|
||||
@@ -238,9 +257,9 @@ class GuiMain(QMainWindow):
|
||||
return True
|
||||
|
||||
def closeProject(self, isYes=False):
|
||||
"""Closes the project if one is open.
|
||||
isYes is passed on from the close application event so the user
|
||||
doesn't get prompted twice.
|
||||
"""Closes the project if one is open. isYes is passed on from
|
||||
the close application event so the user doesn't get prompted
|
||||
twice.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
# There is no project loaded, everything OK
|
||||
@@ -314,7 +333,6 @@ class GuiMain(QMainWindow):
|
||||
self.docEditor.setDictionaries()
|
||||
self.docEditor.setSpellCheck(self.theProject.spellCheck)
|
||||
self.statusBar.setRefTime(self.theProject.projOpened)
|
||||
self.mainMenu.updateMenu()
|
||||
|
||||
# Restore previously open documents, if any
|
||||
if self.theProject.lastEdited is not None:
|
||||
@@ -344,7 +362,6 @@ class GuiMain(QMainWindow):
|
||||
self.treeView.saveTreeOrder()
|
||||
self.theProject.saveProject()
|
||||
self.theIndex.saveIndex()
|
||||
self.mainMenu.updateRecentProjects()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
+13
-3
@@ -297,7 +297,11 @@ class NWProject():
|
||||
self._appendItem(tHandle,pHandle,nwItem)
|
||||
|
||||
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._scanProjectFolder()
|
||||
@@ -319,6 +323,7 @@ class NWProject():
|
||||
return False
|
||||
|
||||
self.projMeta = path.join(self.projPath,"meta")
|
||||
saveTime = time()
|
||||
|
||||
if not self._checkFolder(self.projPath): return
|
||||
if not self._checkFolder(self.projMeta): return
|
||||
@@ -330,7 +335,7 @@ class NWProject():
|
||||
nwXML = etree.Element("novelWriterXML",attrib={
|
||||
"appVersion" : str(nw.__version__),
|
||||
"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
|
||||
@@ -386,8 +391,13 @@ class NWProject():
|
||||
rename(saveFile, backFile)
|
||||
rename(tempFile, saveFile)
|
||||
|
||||
# Save project GUI options
|
||||
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.setProjectChanged(False)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[Main]
|
||||
timestamp = 2019-11-19 21:49:29
|
||||
timestamp = 2020-02-26 18:10:35
|
||||
theme = default
|
||||
syntax = default_light
|
||||
guidark = False
|
||||
@@ -49,14 +49,4 @@ viewcomments = True
|
||||
|
||||
[Path]
|
||||
lastpath =
|
||||
recent0 =
|
||||
recent1 =
|
||||
recent2 =
|
||||
recent3 =
|
||||
recent4 =
|
||||
recent5 =
|
||||
recent6 =
|
||||
recent7 =
|
||||
recent8 =
|
||||
recent9 =
|
||||
|
||||
|
||||
+38
-2
@@ -13,7 +13,7 @@ theConf = Config()
|
||||
def testConfigInit(nwTemp,nwRef):
|
||||
tmpConf = path.join(nwTemp,"novelwriter.conf")
|
||||
refConf = path.join(nwRef, "novelwriter.conf")
|
||||
assert theConf.initConfig(nwTemp)
|
||||
assert theConf.initConfig(nwTemp, nwTemp)
|
||||
assert theConf.setLastPath("")
|
||||
assert theConf.saveConfig()
|
||||
assert cmpFiles(tmpConf, refConf, [2])
|
||||
@@ -37,6 +37,14 @@ def testConfigSetConfPath(nwTemp):
|
||||
assert theConf.confFile == "novelwriter.conf"
|
||||
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
|
||||
def testConfigLoad():
|
||||
assert theConf.loadConfig()
|
||||
@@ -67,7 +75,7 @@ def testConfigSetTreeColWidths(nwTemp,nwRef):
|
||||
assert not theConf.confChanged
|
||||
|
||||
@pytest.mark.core
|
||||
def testConfigSetMainPanePos(nwTemp,nwRef):
|
||||
def testConfigSetPanePos(nwTemp,nwRef):
|
||||
tmpConf = path.join(nwTemp,"novelwriter.conf")
|
||||
refConf = path.join(nwRef, "novelwriter.conf")
|
||||
assert theConf.setMainPanePos([0, 0])
|
||||
@@ -77,3 +85,31 @@ def testConfigSetMainPanePos(nwTemp,nwRef):
|
||||
assert theConf.saveConfig()
|
||||
assert cmpFiles(tmpConf, refConf, [2])
|
||||
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
@@ -18,8 +18,8 @@ keyDelay = 10
|
||||
stepDelay = 50
|
||||
|
||||
@pytest.mark.gui
|
||||
def testMainWindows(qtbot, nwTempGUI, nwRef):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI])
|
||||
def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -254,8 +254,8 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testTimeLineView(qtbot, nwTempGUI, nwRef):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI])
|
||||
def testTimeLineView(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -276,8 +276,8 @@ def testTimeLineView(qtbot, nwTempGUI, nwRef):
|
||||
nwGUI.closeMain()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testProjectEditor(qtbot, nwTempGUI, nwRef):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI])
|
||||
def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -363,8 +363,8 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef):
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testItemEditor(qtbot, nwTempGUI, nwRef):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI])
|
||||
def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
|
||||
@@ -22,10 +22,10 @@ theProject = NWProject(theMain)
|
||||
theProject.handleSeed = 42
|
||||
|
||||
@pytest.mark.project
|
||||
def testProjectNew(nwTempProj,nwRef):
|
||||
def testProjectNew(nwTempProj,nwRef,nwTemp):
|
||||
projFile = path.join(nwTempProj,"nwProject.nwx")
|
||||
refFile = path.join(nwRef,"proj","1_nwProject.nwx")
|
||||
assert theConf.initConfig(nwRef)
|
||||
assert theConf.initConfig(nwRef, nwTemp)
|
||||
assert theProject.newProject()
|
||||
assert theProject.setProjectPath(nwTempProj)
|
||||
assert theProject.saveProject()
|
||||
|
||||
Reference in New Issue
Block a user