Project manager dialog now lists recent projects and lets the user open one of these, or browse for a folder
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
+76
-1
@@ -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
|
||||||
@@ -162,6 +163,9 @@ class Config:
|
|||||||
self.hasEnchant = False
|
self.hasEnchant = False
|
||||||
self.hasSymSpell = False
|
self.hasSymSpell = False
|
||||||
|
|
||||||
|
# Recent Cache
|
||||||
|
self.recentProj = {}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -219,6 +223,9 @@ class Config:
|
|||||||
# If it does not exist, save a copy of the default values
|
# If it does not exist, save a copy of the default values
|
||||||
self.saveConfig()
|
self.saveConfig()
|
||||||
|
|
||||||
|
# Load re3cent projects cache
|
||||||
|
self.loadRecentCache()
|
||||||
|
|
||||||
# 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 not path.isdir(self.dataPath):
|
||||||
@@ -488,6 +495,74 @@ class Config:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def loadRecentCache(self):
|
||||||
|
"""Load the cache file for recent projects.
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
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
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,16 @@
|
|||||||
import logging
|
import logging
|
||||||
import nw
|
import nw
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QListWidget,
|
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QTreeWidget,
|
||||||
QAbstractItemView
|
QAbstractItemView, QTreeWidgetItem
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from nw.common import formatInt
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class GuiProjectLoad(QDialog):
|
class GuiProjectLoad(QDialog):
|
||||||
@@ -31,6 +35,7 @@ class GuiProjectLoad(QDialog):
|
|||||||
self.mainConf = nw.CONFIG
|
self.mainConf = nw.CONFIG
|
||||||
self.theParent = theParent
|
self.theParent = theParent
|
||||||
self.sourceItem = None
|
self.sourceItem = None
|
||||||
|
self.openPath = None
|
||||||
|
|
||||||
self.outerBox = QHBoxLayout()
|
self.outerBox = QHBoxLayout()
|
||||||
self.innerBox = QVBoxLayout()
|
self.innerBox = QVBoxLayout()
|
||||||
@@ -45,19 +50,35 @@ class GuiProjectLoad(QDialog):
|
|||||||
self.projectForm = QGridLayout()
|
self.projectForm = QGridLayout()
|
||||||
self.projectForm.setContentsMargins(0, 0, 0, 0)
|
self.projectForm.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
self.listBox = QListWidget()
|
self.listBox = QTreeWidget()
|
||||||
|
self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
|
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 = QPushButton("Close")
|
||||||
self.closeButton.clicked.connect(self._doClose)
|
self.closeButton.clicked.connect(self._doClose)
|
||||||
|
|
||||||
self.projectForm.addWidget(self.listBox, 0, 0, 1, 3)
|
self.projectForm.addWidget(self.listBox, 0, 0, 1, 4)
|
||||||
self.projectForm.addWidget(self.closeButton, 1, 2)
|
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.innerBox.addLayout(self.projectForm)
|
||||||
|
|
||||||
self.rejected.connect(self._doClose)
|
self.rejected.connect(self._doClose)
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
|
self.setMinimumWidth(750)
|
||||||
|
self.setMinimumHeight(450)
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
self._populateList()
|
self._populateList()
|
||||||
@@ -70,6 +91,29 @@ class GuiProjectLoad(QDialog):
|
|||||||
# Buttons
|
# 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):
|
def _doClose(self):
|
||||||
"""Close the dialog window without doing anything.
|
"""Close the dialog window without doing anything.
|
||||||
"""
|
"""
|
||||||
@@ -82,6 +126,40 @@ class GuiProjectLoad(QDialog):
|
|||||||
##
|
##
|
||||||
|
|
||||||
def _populateList(self):
|
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()
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.listBox.resizeColumnToContents(0)
|
||||||
|
self.listBox.resizeColumnToContents(1)
|
||||||
|
self.listBox.resizeColumnToContents(2)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# END Class GuiProjectLoad
|
# END Class GuiProjectLoad
|
||||||
|
|||||||
+1
-1
@@ -194,7 +194,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
|
||||||
|
|||||||
+11
-7
@@ -207,13 +207,17 @@ class GuiMain(QMainWindow):
|
|||||||
##
|
##
|
||||||
|
|
||||||
def manageProjects(self):
|
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
|
||||||
if self.mainConf.showGUI:
|
|
||||||
dlgProj = GuiProjectLoad(self)
|
|
||||||
dlgProj.exec_()
|
|
||||||
|
|
||||||
|
dlgProj = GuiProjectLoad(self)
|
||||||
|
dlgProj.exec_()
|
||||||
|
if dlgProj.result() == QDialog.Accepted:
|
||||||
|
self.openProject(dlgProj.openPath)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -223,7 +227,7 @@ class GuiMain(QMainWindow):
|
|||||||
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
|
||||||
|
|
||||||
@@ -236,7 +240,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
|
||||||
|
|
||||||
|
|||||||
@@ -319,6 +319,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 +331,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 +387,14 @@ class NWProject():
|
|||||||
rename(saveFile, backFile)
|
rename(saveFile, backFile)
|
||||||
rename(tempFile, saveFile)
|
rename(tempFile, saveFile)
|
||||||
|
|
||||||
|
# Save project GUI options
|
||||||
self.optState.saveSettings()
|
self.optState.saveSettings()
|
||||||
|
|
||||||
|
# Update recent projects
|
||||||
self.mainConf.setRecent(self.projPath)
|
self.mainConf.setRecent(self.projPath)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user