From 30b4e5511a843360499472f31b5b31ba494d181b Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 23 Nov 2023 16:43:56 +0100
Subject: [PATCH 1/3] Add a common function to open file paths externally
---
novelwriter/common.py | 15 ++++++++++++++-
novelwriter/core/project.py | 4 ++--
novelwriter/gui/mainmenu.py | 9 +++------
novelwriter/tools/dictionaries.py | 15 ++++-----------
4 files changed, 23 insertions(+), 20 deletions(-)
diff --git a/novelwriter/common.py b/novelwriter/common.py
index 2627a1b0..0f61bd89 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -33,8 +33,11 @@ from typing import Any, Literal
from pathlib import Path
from datetime import datetime
from configparser import ConfigParser
+from urllib.parse import urljoin
+from urllib.request import pathname2url
-from PyQt5.QtCore import QCoreApplication
+from PyQt5.QtGui import QDesktopServices
+from PyQt5.QtCore import QCoreApplication, QUrl
from PyQt5.QtWidgets import QWidget, qApp
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
@@ -495,6 +498,16 @@ def getFileSize(path: Path) -> int:
return -1
+def openExternalPath(path: Path) -> bool:
+ """Open a path by passing it to the desktop environment."""
+ if Path(path).exists():
+ QDesktopServices.openUrl(
+ QUrl(urljoin("file:", pathname2url(str(path))))
+ )
+ return True
+ return False
+
+
# =============================================================================================== #
# Other Functions
# =============================================================================================== #
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 4cba0c82..f66f4549 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -45,7 +45,7 @@ from novelwriter.core.sessions import NWSessionLog
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
from novelwriter.core.projectdata import NWProjectData
from novelwriter.common import (
- checkStringNone, formatInt, formatTimeStamp, hexToInt, makeFileNameSafe, minmax
+ checkStringNone, formatInt, formatTimeStamp, getFileSize, hexToInt, makeFileNameSafe, minmax
)
if TYPE_CHECKING: # pragma: no cover
@@ -420,7 +420,7 @@ class NWProject:
timeStamp = formatTimeStamp(time(), fileSafe=True)
archName = baseDir / f"{cleanName} {timeStamp}.zip"
if self._storage.zipIt(archName, compression=2):
- size = formatInt(archName.stat().st_size)
+ size = formatInt(getFileSize(archName))
if doNotify:
SHARED.info(
self.tr("Created a backup of your project of size {0}B.").format(size),
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index 8c74f270..9238d3f1 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -27,15 +27,14 @@ import logging
from typing import TYPE_CHECKING
from pathlib import Path
-from urllib.parse import urljoin
-from urllib.request import pathname2url
-from PyQt5.QtCore import QUrl, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QDesktopServices
+from PyQt5.QtCore import QUrl, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QMenuBar, QAction
from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget
+from novelwriter.common import openExternalPath
from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode
if TYPE_CHECKING: # pragma: no cover
@@ -111,9 +110,7 @@ class GuiMainMenu(QMenuBar):
def _openUserManualFile(self) -> None:
"""Open the documentation in PDF format."""
if isinstance(CONFIG.pdfDocs, Path):
- QDesktopServices.openUrl(
- QUrl(urljoin("file:", pathname2url(str(CONFIG.pdfDocs))))
- )
+ openExternalPath(CONFIG.pdfDocs)
return
@pyqtSlot(str)
diff --git a/novelwriter/tools/dictionaries.py b/novelwriter/tools/dictionaries.py
index 847925a0..c14bd163 100644
--- a/novelwriter/tools/dictionaries.py
+++ b/novelwriter/tools/dictionaries.py
@@ -27,19 +27,17 @@ import logging
from pathlib import Path
from zipfile import ZipFile
-from urllib.parse import urljoin
-from urllib.request import pathname2url
-from PyQt5.QtGui import QCloseEvent, QDesktopServices, QTextCursor
-from PyQt5.QtCore import QUrl, pyqtSlot
+from PyQt5.QtGui import QCloseEvent, QTextCursor
+from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import (
QDialog, QDialogButtonBox, QFileDialog, QFrame, QHBoxLayout, QLabel,
QLineEdit, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget, qApp
)
from novelwriter import CONFIG, SHARED
-from novelwriter.common import formatInt, getFileSize
from novelwriter.error import formatException
+from novelwriter.common import openExternalPath, formatInt, getFileSize
logger = logging.getLogger(__name__)
@@ -217,12 +215,7 @@ class GuiDictionaries(QDialog):
@pyqtSlot()
def _doOpenInstallLocation(self) -> None:
"""Open the dictionary folder."""
- path = self.inPath.text()
- if Path(path).is_dir():
- QDesktopServices.openUrl(
- QUrl(urljoin("file:", pathname2url(path)))
- )
- else:
+ if not openExternalPath(Path(self.inPath.text())):
SHARED.error("Path not found.")
return
From 4bd830f0d62ac9c3692a8ed560ad23f3b7950fbd Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 23 Nov 2023 16:45:08 +0100
Subject: [PATCH 2/3] Add an "Open Folder" button to build dialog (#1554)
---
novelwriter/tools/manusbuild.py | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py
index 70235ebf..3decd730 100644
--- a/novelwriter/tools/manusbuild.py
+++ b/novelwriter/tools/manusbuild.py
@@ -37,7 +37,7 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwBuildFmt
-from novelwriter.common import makeFileNameSafe
+from novelwriter.common import makeFileNameSafe, openExternalPath
from novelwriter.constants import nwLabels
from novelwriter.core.item import NWItem
from novelwriter.core.docbuild import NWBuildDocument
@@ -176,9 +176,11 @@ class GuiManuscriptBuild(QDialog):
self.buildBox.setVerticalSpacing(sp4)
# Dialog Buttons
+ self.btnOpen = QPushButton(SHARED.theme.getIcon("browse"), self.tr("Open Folder"))
self.btnBuild = QPushButton(SHARED.theme.getIcon("export"), self.tr("&Build"))
- self.dlgButtons = QDialogButtonBox(QDialogButtonBox.Close)
- self.dlgButtons.addButton(self.btnBuild, QDialogButtonBox.ActionRole)
+ self.dlgButtons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
+ self.dlgButtons.addButton(self.btnOpen, QDialogButtonBox.ButtonRole.ActionRole)
+ self.dlgButtons.addButton(self.btnBuild, QDialogButtonBox.ButtonRole.ActionRole)
# Assemble GUI
# ============
@@ -254,7 +256,10 @@ class GuiManuscriptBuild(QDialog):
"""Handle button clicks from the dialog button box."""
role = self.dlgButtons.buttonRole(button)
if role == QDialogButtonBox.ActionRole:
- self._runBuild()
+ if button == self.btnBuild:
+ self._runBuild()
+ elif button == self.btnOpen:
+ self._openOutputFolder()
elif role == QDialogButtonBox.RejectRole:
self.close()
return
@@ -386,4 +391,9 @@ class GuiManuscriptBuild(QDialog):
return
+ def _openOutputFolder(self):
+ """Open the build folder in the system's file explorer."""
+ openExternalPath(Path(self.buildPath.text()))
+ return
+
# END Class GuiManuscriptBuild
From 2998f2c3631fc7138a7297ae5302224e762674a3 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 23 Nov 2023 16:45:14 +0100
Subject: [PATCH 3/3] Update tests
---
tests/test_base/test_base_common.py | 41 ++++++++++++++++++++---
tests/test_tools/test_tools_manusbuild.py | 15 +++++++++
2 files changed, 52 insertions(+), 4 deletions(-)
diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py
index 8ac8f357..08e8b3e9 100644
--- a/tests/test_base/test_base_common.py
+++ b/tests/test_base/test_base_common.py
@@ -20,6 +20,8 @@ along with this program. If not, see .
"""
import time
+from PyQt5.QtCore import QUrl
+from PyQt5.QtGui import QDesktopServices
import pytest
from pathlib import Path
@@ -32,10 +34,10 @@ from novelwriter.guimain import GuiMain
from novelwriter.common import (
checkBool, checkFloat, checkHandle, checkInt, checkIntTuple, checkPath,
checkString, checkStringNone, checkUuid, formatInt, formatTime,
- formatTimeStamp, fuzzyTime, getGuiItem, hexToInt, isHandle, isItemClass,
- isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax,
- numberToRoman, NWConfigParser, readTextFile, simplified, transferCase,
- xmlIndent, yesNo
+ formatTimeStamp, fuzzyTime, getFileSize, getGuiItem, hexToInt, isHandle,
+ isItemClass, isItemLayout, isItemType, isTitleTag, jsonEncode,
+ makeFileNameSafe, minmax, numberToRoman, NWConfigParser, openExternalPath,
+ readTextFile, simplified, transferCase, xmlIndent, yesNo
)
@@ -621,6 +623,37 @@ def testBaseCommon_makeFileNameSafe():
# END Test testBaseCommon_makeFileNameSafe
+@pytest.mark.base
+def testBaseCommon_getFileSize(fncPath):
+ """Test the getFileSize function."""
+ (fncPath / "one.txt").write_bytes(b"foobar")
+ (fncPath / "two.txt").touch()
+
+ assert getFileSize(fncPath / "nope.txt") == -1
+ assert getFileSize(fncPath / "one.txt") == 6
+ assert getFileSize(fncPath / "two.txt") == 0
+
+# END Test testBaseCommon_getFileSize
+
+
+@pytest.mark.base
+def testBaseCommon_openExternalPath(monkeypatch, tstPaths):
+ """Test the openExternalPath function."""
+ lastUrl = ""
+
+ def mockOpenUrl(url: QUrl) -> None:
+ nonlocal lastUrl
+ lastUrl = url.toString()
+ return
+
+ monkeypatch.setattr(QDesktopServices, "openUrl", mockOpenUrl)
+ assert openExternalPath(Path("/foo/bar")) is False
+ assert openExternalPath(tstPaths.tmpDir) is True
+ assert lastUrl.startswith("file://")
+
+# END Test testBaseCommon_openExternalPath
+
+
@pytest.mark.base
def testBaseCommon_getGuiItem(nwGUI):
"""Check the GUI item function."""
diff --git a/tests/test_tools/test_tools_manusbuild.py b/tests/test_tools/test_tools_manusbuild.py
index 271f9b0f..a23f383e 100644
--- a/tests/test_tools/test_tools_manusbuild.py
+++ b/tests/test_tools/test_tools_manusbuild.py
@@ -19,6 +19,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+from PyQt5.QtCore import QUrl
+from PyQt5.QtGui import QDesktopServices
import pytest
from pathlib import Path
@@ -132,6 +134,19 @@ def testManuscriptBuild_Main(
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert manus._runBuild() is False
+ # Test that the open button works
+ lastUrl = ""
+
+ def mockOpenUrl(url: QUrl) -> None:
+ nonlocal lastUrl
+ lastUrl = url.toString()
+ return
+
+ with monkeypatch.context() as mp:
+ mp.setattr(QDesktopServices, "openUrl", mockOpenUrl)
+ manus.btnOpen.click()
+ assert lastUrl.startswith("file://")
+
# Finish
manus._dialogButtonClicked(manus.dlgButtons.button(QDialogButtonBox.Close))
# qtbot.stop()