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 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
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
@@ -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
## ##
+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
+83 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
+8 -1
View File
@@ -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)