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
|
||||
|
||||
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
@@ -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
|
||||
##
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user