Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remember last used path for each tool individually #1934

Merged
merged 9 commits into from
Jun 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 67 additions & 15 deletions novelwriter/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
File History:
Created: 2018-09-22 [0.0.1] Config
Created: 2022-11-09 [2.0rc2] RecentProjects
Created: 2024-06-16 [2.5rc1] RecentPaths

This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Expand Down Expand Up @@ -103,7 +104,8 @@ def __init__(self) -> None:
# User Settings
# =============

self._recentObj = RecentProjects(self)
self._recentProjects = RecentProjects(self)
self._recentPaths = RecentPaths(self)

# General GUI Settings
self.guiLocale = self._qLocale.name()
Expand Down Expand Up @@ -180,7 +182,6 @@ def __init__(self) -> None:
self.fmtPadThin = False

# User Paths
self._lastPath = self._homePath # The user's last used path
self._backupPath = self._backPath # Backup path to use, can be none

# Spell Checking Settings
Expand Down Expand Up @@ -253,7 +254,7 @@ def hasError(self) -> bool:

@property
def recentProjects(self) -> RecentProjects:
return self._recentObj
return self._recentProjects

@property
def mainWinSize(self) -> list[int]:
Expand Down Expand Up @@ -343,7 +344,7 @@ def setOutlinePanePos(self, pos: list[int]) -> None:
self._outlnPanePos = [int(x/self.guiScale) for x in pos]
return

def setLastPath(self, path: str | Path) -> None:
def setLastPath(self, key: str, path: str | Path) -> None:
"""Set the last used path. Only the folder is saved, so if the
path is not a folder, the parent of the path is used instead.
"""
Expand All @@ -352,8 +353,7 @@ def setLastPath(self, path: str | Path) -> None:
if not path.is_dir():
path = path.parent
if path.is_dir():
self._lastPath = path
logger.debug("Last path updated: %s" % self._lastPath)
self._recentPaths.setPath(key, path)
return

def setBackupPath(self, path: Path | str) -> None:
Expand Down Expand Up @@ -438,11 +438,12 @@ def assetPath(self, target: str | None = None) -> Path:
return self._appPath / "assets" / target
return self._appPath / "assets"

def lastPath(self) -> Path:
def lastPath(self, key: str) -> Path:
"""Return the last path used by the user, if it exists."""
if isinstance(self._lastPath, Path):
if self._lastPath.is_dir():
return self._lastPath
if path := self._recentPaths.getPath(key):
asPath = Path(path)
if asPath.is_dir():
return asPath
return self._homePath

def backupPath(self) -> Path:
Expand Down Expand Up @@ -516,7 +517,6 @@ def initConfig(self, confPath: str | Path | None = None,
logger.debug("Data Path: %s", self._dataPath)
logger.debug("App Root: %s", self._appRoot)
logger.debug("App Path: %s", self._appPath)
logger.debug("Last Path: %s", self._lastPath)
logger.debug("PDF Manual: %s", self.pdfDocs)

# If the config and data folders don't exist, create them
Expand All @@ -531,7 +531,8 @@ def initConfig(self, confPath: str | Path | None = None,
(self._dataPath / "syntax").mkdir(exist_ok=True)
(self._dataPath / "themes").mkdir(exist_ok=True)

self._recentObj.loadCache()
self._recentPaths.loadCache()
self._recentProjects.loadCache()
self._checkOptionalPackages()

logger.debug("Config instance initialised")
Expand Down Expand Up @@ -600,7 +601,6 @@ def loadConfig(self) -> bool:
self.hideHScroll = conf.rdBool(sec, "hidehscroll", self.hideHScroll)
self.lastNotes = conf.rdStr(sec, "lastnotes", self.lastNotes)
self.nativeFont = conf.rdBool(sec, "nativefont", self.nativeFont)
self._lastPath = conf.rdPath(sec, "lastpath", self._lastPath)

# Sizes
sec = "Sizes"
Expand Down Expand Up @@ -710,7 +710,6 @@ def saveConfig(self) -> bool:
"hidehscroll": str(self.hideHScroll),
"lastnotes": str(self.lastNotes),
"nativefont": str(self.nativeFont),
"lastpath": str(self._lastPath),
}

conf["Sizes"] = {
Expand Down Expand Up @@ -811,7 +810,7 @@ def _packList(self, data: list) -> str:
"""Pack a list of items into a comma-separated string for saving
to the config file.
"""
return ", ".join([str(inVal) for inVal in data])
return ", ".join(str(inVal) for inVal in data)

def _checkOptionalPackages(self) -> None:
"""Check optional packages used by some features."""
Expand Down Expand Up @@ -893,3 +892,56 @@ def remove(self, path: str | Path) -> None:
logger.debug("Removed recent: %s", path)
self.saveCache()
return


class RecentPaths:

KEYS = ["default", "project", "import", "outline", "stats"]

def __init__(self, config: Config) -> None:
self._conf = config
self._data = {}
return

def setPath(self, key: str, path: Path | str) -> None:
"""Set a path for a given key, and save the cache."""
if key in self.KEYS:
self._data[key] = str(path)
self.saveCache()
return

def getPath(self, key: str) -> str | None:
"""Get a path for a given key, or return None."""
return self._data.get(key)

def loadCache(self) -> bool:
"""Load the cache file for recent paths."""
self._data = {}
cacheFile = self._conf.dataPath(nwFiles.RECENT_PATH)
if cacheFile.is_file():
try:
with open(cacheFile, mode="r", encoding="utf-8") as inFile:
data = json.load(inFile)
if isinstance(data, dict):
for key, path in data.items():
if key in self.KEYS and isinstance(path, str):
self._data[key] = path
except Exception:
logger.error("Could not load recent paths cache")
logException()
return False
return True

def saveCache(self) -> bool:
"""Save the cache dictionary of recent paths."""
cacheFile = self._conf.dataPath(nwFiles.RECENT_PATH)
cacheTemp = cacheFile.with_suffix(".tmp")
try:
with open(cacheTemp, mode="w+", encoding="utf-8") as outFile:
json.dump(self._data, outFile, indent=2)
cacheTemp.replace(cacheFile)
except Exception:
logger.error("Could not save recent paths cache")
logException()
return False
return True
1 change: 1 addition & 0 deletions novelwriter/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class nwFiles:
# Config Files
CONF_FILE = "novelwriter.conf"
RECENT_FILE = "recentProjects.json"
RECENT_PATH = "recentPaths.json"

# Project Root Files
PROJ_FILE = "nwProject.nwx"
Expand Down
6 changes: 3 additions & 3 deletions novelwriter/core/buildsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ def order(self) -> int:
return self._order

@property
def lastPath(self) -> Path:
def lastBuildPath(self) -> Path:
"""The last used build path."""
if self._path.is_dir():
return self._path
Expand Down Expand Up @@ -293,7 +293,7 @@ def setOrder(self, value: int) -> None:
self._order = value
return

def setLastPath(self, path: Path | str | None) -> None:
def setLastBuildPath(self, path: Path | str | None) -> None:
"""Set the last used build path."""
if isinstance(path, str):
path = Path(path)
Expand Down Expand Up @@ -461,7 +461,7 @@ def unpack(self, data: dict) -> None:
self.setName(data.get("name", ""))
self.setBuildID(data.get("uuid", ""))
self.setOrder(data.get("order", 0))
self.setLastPath(data.get("path", None))
self.setLastBuildPath(data.get("path", None))
self.setLastBuildName(data.get("build", ""))

buildFmt = str(data.get("format", ""))
Expand Down
4 changes: 2 additions & 2 deletions novelwriter/gui/outline.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,12 +523,12 @@ def menuColumnToggled(self, isChecked: bool, hItem: nwOutline) -> None:
@pyqtSlot()
def exportOutline(self) -> None:
"""Export the outline as a CSV file."""
path = CONFIG.lastPath() / f"{makeFileNameSafe(SHARED.project.data.name)}.csv"
path = CONFIG.lastPath("outline") / f"{makeFileNameSafe(SHARED.project.data.name)}.csv"
path, _ = QFileDialog.getSaveFileName(
self, self.tr("Save Outline As"), str(path), formatFileFilter(["*.csv", "*"])
)
if path:
CONFIG.setLastPath(path)
CONFIG.setLastPath("outline", path)
logger.info("Writing CSV file: %s", path)
cols = [col for col in self._treeOrder if not self._colHidden[col]]
order = [self._colIdx[col] for col in cols]
Expand Down
4 changes: 2 additions & 2 deletions novelwriter/guimain.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ def importDocument(self) -> bool:
logger.error("No project open")
return False

lastPath = CONFIG.lastPath()
lastPath = CONFIG.lastPath("import")
ffilter = formatFileFilter(["*.txt", "*.md", "*.nwd", "*"])
loadFile, _ = QFileDialog.getOpenFileName(
self, self.tr("Import File"), str(lastPath), filter=ffilter
Expand All @@ -667,7 +667,7 @@ def importDocument(self) -> bool:
try:
with open(loadFile, mode="rt", encoding="utf-8") as inFile:
text = inFile.read()
CONFIG.setLastPath(loadFile)
CONFIG.setLastPath("import", loadFile)
except Exception as exc:
SHARED.error(self.tr(
"Could not read file. The file must be an existing text file."
Expand Down
6 changes: 3 additions & 3 deletions novelwriter/tools/manusbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ def __init__(self, parent: QWidget, build: BuildSettings) -> None:

self.btnBuild.setFocus()
self._populateContentList()
self.buildPath.setText(str(self._build.lastPath))
self.buildPath.setText(str(self._build.lastBuildPath))
if self._build.lastBuildName:
self.buildName.setText(self._build.lastBuildName)
else:
Expand Down Expand Up @@ -274,7 +274,7 @@ def _dialogButtonClicked(self, button: QAbstractButton) -> None:
def _doSelectPath(self) -> None:
"""Select a folder for output."""
bPath = Path(self.buildPath.text())
bPath = bPath if bPath.is_dir() else self._build.lastPath
bPath = bPath if bPath.is_dir() else self._build.lastBuildPath
savePath = QFileDialog.getExistingDirectory(
self, self.tr("Select Folder"), str(bPath)
)
Expand Down Expand Up @@ -336,7 +336,7 @@ def _runBuild(self) -> bool:
for i, _ in docBuild.iterBuild(buildPath, bFormat):
self.buildProgress.setValue(i+1)

self._build.setLastPath(bPath)
self._build.setLastBuildPath(bPath)
self._build.setLastBuildName(bName)
self._build.setLastFormat(bFormat)

Expand Down
10 changes: 5 additions & 5 deletions novelwriter/tools/welcome.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,7 @@ def _showOpenProjectPage(self) -> None:
@pyqtSlot()
def _browseForProject(self) -> None:
"""Browse for a project to open."""
if path := SHARED.getProjectPath(self, path=CONFIG.lastPath(), allowZip=False):
CONFIG.setLastPath(path)
if path := SHARED.getProjectPath(self, path=CONFIG.homePath(), allowZip=False):
self._openProjectPath(path)
return

Expand Down Expand Up @@ -550,7 +549,7 @@ class _NewProjectForm(QWidget):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)

self._basePath = CONFIG.homePath()
self._basePath = CONFIG.lastPath("project")
self._fillMode = self.FILL_BLANK
self._copyPath = None

Expand Down Expand Up @@ -726,12 +725,13 @@ def getProjectData(self) -> dict:
@pyqtSlot()
def _doBrowse(self) -> None:
"""Select a project folder."""
if projDir := QFileDialog.getExistingDirectory(
if path := QFileDialog.getExistingDirectory(
self, self.tr("Select Project Folder"),
str(self._basePath), options=QFileDialog.Option.ShowDirsOnly
):
self._basePath = Path(projDir)
self._basePath = Path(path)
self._updateProjPath()
CONFIG.setLastPath("project", path)
return

@pyqtSlot()
Expand Down
4 changes: 2 additions & 2 deletions novelwriter/tools/writingstats.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,14 +384,14 @@ def _saveData(self, dataFmt: int) -> bool:
return False

# Generate the file name
savePath = CONFIG.lastPath() / f"sessionStats.{fileExt}"
savePath = CONFIG.lastPath("stats") / f"sessionStats.{fileExt}"
savePath, _ = QFileDialog.getSaveFileName(
self, self.tr("Save Data As"), str(savePath), f"{textFmt} (*.{fileExt})"
)
if not savePath:
return False

CONFIG.setLastPath(savePath)
CONFIG.setLastPath("stats", savePath)

# Do the actual writing
wSuccess = False
Expand Down
1 change: 0 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ def resetConfigVars():
"""Reset the CONFIG object and set various values for testing to
prevent interfering with local OS.
"""
CONFIG.setLastPath(_TMP_ROOT)
CONFIG.setBackupPath(_TMP_ROOT)
CONFIG.setGuiFont(None)
CONFIG.setTextFont(None)
Expand Down
3 changes: 1 addition & 2 deletions tests/reference/baseConfig_novelwriter.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[Meta]
timestamp = 2024-05-20 16:48:20
timestamp = 2024-06-16 00:36:27

[Main]
font =
Expand All @@ -10,7 +10,6 @@ hidevscroll = False
hidehscroll = False
lastnotes = 0x0
nativefont = True
lastpath =

[Sizes]
mainwindow = 1200, 650
Expand Down
Loading