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:
Veronica K. B. Olsen
2020-02-26 17:44:11 +01:00
parent 6867f38637
commit c688c66d5b
7 changed files with 205 additions and 21 deletions
+19
View File
@@ -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.
+76 -1
View File
@@ -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
@@ -162,6 +163,9 @@ class Config:
self.hasEnchant = False
self.hasSymSpell = False
# Recent Cache
self.recentProj = {}
return
##
@@ -219,6 +223,9 @@ class Config:
# If it does not exist, save a copy of the default values
self.saveConfig()
# Load re3cent projects cache
self.loadRecentCache()
# If data folder does not exist, make it.
# This assumes that the os data folder itself exists.
if not path.isdir(self.dataPath):
@@ -488,6 +495,74 @@ class Config:
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
##
+7 -6
View File
@@ -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
+83 -5
View File
@@ -13,12 +13,16 @@
import logging
import nw
from datetime import datetime
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QListWidget,
QAbstractItemView
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QTreeWidget,
QAbstractItemView, QTreeWidgetItem
)
from nw.common import formatInt
logger = logging.getLogger(__name__)
class GuiProjectLoad(QDialog):
@@ -31,6 +35,7 @@ class GuiProjectLoad(QDialog):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.sourceItem = None
self.openPath = None
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
@@ -45,19 +50,35 @@ class GuiProjectLoad(QDialog):
self.projectForm = QGridLayout()
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.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, 3)
self.projectForm.addWidget(self.closeButton, 1, 2)
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()
@@ -70,6 +91,29 @@ class GuiProjectLoad(QDialog):
# 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.
"""
@@ -82,6 +126,40 @@ class GuiProjectLoad(QDialog):
##
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
# END Class GuiProjectLoad
+1 -1
View File
@@ -194,7 +194,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
+11 -7
View File
@@ -207,13 +207,17 @@ class GuiMain(QMainWindow):
##
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 self.mainConf.showGUI:
dlgProj = GuiProjectLoad(self)
dlgProj.exec_()
if not self.mainConf.showGUI:
return False
dlgProj = GuiProjectLoad(self)
dlgProj.exec_()
if dlgProj.result() == QDialog.Accepted:
self.openProject(dlgProj.openPath)
return True
@@ -223,7 +227,7 @@ class GuiMain(QMainWindow):
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
@@ -236,7 +240,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
+8 -1
View File
@@ -319,6 +319,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 +331,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 +387,14 @@ class NWProject():
rename(saveFile, backFile)
rename(tempFile, saveFile)
# Save project GUI options
self.optState.saveSettings()
# Update recent projects
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.setProjectChanged(False)