Add a button to open manuscript build folder (#1613)
This commit is contained in:
+14
-1
@@ -33,8 +33,11 @@ from typing import Any, Literal
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from configparser import ConfigParser
|
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 PyQt5.QtWidgets import QWidget, qApp
|
||||||
|
|
||||||
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
||||||
@@ -495,6 +498,16 @@ def getFileSize(path: Path) -> int:
|
|||||||
return -1
|
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
|
# Other Functions
|
||||||
# =============================================================================================== #
|
# =============================================================================================== #
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ from novelwriter.core.sessions import NWSessionLog
|
|||||||
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
|
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
|
||||||
from novelwriter.core.projectdata import NWProjectData
|
from novelwriter.core.projectdata import NWProjectData
|
||||||
from novelwriter.common import (
|
from novelwriter.common import (
|
||||||
checkStringNone, formatInt, formatTimeStamp, hexToInt, makeFileNameSafe, minmax
|
checkStringNone, formatInt, formatTimeStamp, getFileSize, hexToInt, makeFileNameSafe, minmax
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
@@ -420,7 +420,7 @@ class NWProject:
|
|||||||
timeStamp = formatTimeStamp(time(), fileSafe=True)
|
timeStamp = formatTimeStamp(time(), fileSafe=True)
|
||||||
archName = baseDir / f"{cleanName} {timeStamp}.zip"
|
archName = baseDir / f"{cleanName} {timeStamp}.zip"
|
||||||
if self._storage.zipIt(archName, compression=2):
|
if self._storage.zipIt(archName, compression=2):
|
||||||
size = formatInt(archName.stat().st_size)
|
size = formatInt(getFileSize(archName))
|
||||||
if doNotify:
|
if doNotify:
|
||||||
SHARED.info(
|
SHARED.info(
|
||||||
self.tr("Created a backup of your project of size {0}B.").format(size),
|
self.tr("Created a backup of your project of size {0}B.").format(size),
|
||||||
|
|||||||
@@ -27,15 +27,14 @@ import logging
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from pathlib import Path
|
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.QtGui import QDesktopServices
|
||||||
|
from PyQt5.QtCore import QUrl, pyqtSignal, pyqtSlot
|
||||||
from PyQt5.QtWidgets import QMenuBar, QAction
|
from PyQt5.QtWidgets import QMenuBar, QAction
|
||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget
|
from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget
|
||||||
|
from novelwriter.common import openExternalPath
|
||||||
from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode
|
from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
@@ -111,9 +110,7 @@ class GuiMainMenu(QMenuBar):
|
|||||||
def _openUserManualFile(self) -> None:
|
def _openUserManualFile(self) -> None:
|
||||||
"""Open the documentation in PDF format."""
|
"""Open the documentation in PDF format."""
|
||||||
if isinstance(CONFIG.pdfDocs, Path):
|
if isinstance(CONFIG.pdfDocs, Path):
|
||||||
QDesktopServices.openUrl(
|
openExternalPath(CONFIG.pdfDocs)
|
||||||
QUrl(urljoin("file:", pathname2url(str(CONFIG.pdfDocs))))
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
|
|||||||
@@ -27,19 +27,17 @@ import logging
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
from urllib.parse import urljoin
|
|
||||||
from urllib.request import pathname2url
|
|
||||||
|
|
||||||
from PyQt5.QtGui import QCloseEvent, QDesktopServices, QTextCursor
|
from PyQt5.QtGui import QCloseEvent, QTextCursor
|
||||||
from PyQt5.QtCore import QUrl, pyqtSlot
|
from PyQt5.QtCore import pyqtSlot
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QDialog, QDialogButtonBox, QFileDialog, QFrame, QHBoxLayout, QLabel,
|
QDialog, QDialogButtonBox, QFileDialog, QFrame, QHBoxLayout, QLabel,
|
||||||
QLineEdit, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget, qApp
|
QLineEdit, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget, qApp
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
from novelwriter.common import formatInt, getFileSize
|
|
||||||
from novelwriter.error import formatException
|
from novelwriter.error import formatException
|
||||||
|
from novelwriter.common import openExternalPath, formatInt, getFileSize
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -217,12 +215,7 @@ class GuiDictionaries(QDialog):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _doOpenInstallLocation(self) -> None:
|
def _doOpenInstallLocation(self) -> None:
|
||||||
"""Open the dictionary folder."""
|
"""Open the dictionary folder."""
|
||||||
path = self.inPath.text()
|
if not openExternalPath(Path(self.inPath.text())):
|
||||||
if Path(path).is_dir():
|
|
||||||
QDesktopServices.openUrl(
|
|
||||||
QUrl(urljoin("file:", pathname2url(path)))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
SHARED.error("Path not found.")
|
SHARED.error("Path not found.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from PyQt5.QtWidgets import (
|
|||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
from novelwriter.enum import nwBuildFmt
|
from novelwriter.enum import nwBuildFmt
|
||||||
from novelwriter.common import makeFileNameSafe
|
from novelwriter.common import makeFileNameSafe, openExternalPath
|
||||||
from novelwriter.constants import nwLabels
|
from novelwriter.constants import nwLabels
|
||||||
from novelwriter.core.item import NWItem
|
from novelwriter.core.item import NWItem
|
||||||
from novelwriter.core.docbuild import NWBuildDocument
|
from novelwriter.core.docbuild import NWBuildDocument
|
||||||
@@ -176,9 +176,11 @@ class GuiManuscriptBuild(QDialog):
|
|||||||
self.buildBox.setVerticalSpacing(sp4)
|
self.buildBox.setVerticalSpacing(sp4)
|
||||||
|
|
||||||
# Dialog Buttons
|
# Dialog Buttons
|
||||||
|
self.btnOpen = QPushButton(SHARED.theme.getIcon("browse"), self.tr("Open Folder"))
|
||||||
self.btnBuild = QPushButton(SHARED.theme.getIcon("export"), self.tr("&Build"))
|
self.btnBuild = QPushButton(SHARED.theme.getIcon("export"), self.tr("&Build"))
|
||||||
self.dlgButtons = QDialogButtonBox(QDialogButtonBox.Close)
|
self.dlgButtons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
||||||
self.dlgButtons.addButton(self.btnBuild, QDialogButtonBox.ActionRole)
|
self.dlgButtons.addButton(self.btnOpen, QDialogButtonBox.ButtonRole.ActionRole)
|
||||||
|
self.dlgButtons.addButton(self.btnBuild, QDialogButtonBox.ButtonRole.ActionRole)
|
||||||
|
|
||||||
# Assemble GUI
|
# Assemble GUI
|
||||||
# ============
|
# ============
|
||||||
@@ -254,7 +256,10 @@ class GuiManuscriptBuild(QDialog):
|
|||||||
"""Handle button clicks from the dialog button box."""
|
"""Handle button clicks from the dialog button box."""
|
||||||
role = self.dlgButtons.buttonRole(button)
|
role = self.dlgButtons.buttonRole(button)
|
||||||
if role == QDialogButtonBox.ActionRole:
|
if role == QDialogButtonBox.ActionRole:
|
||||||
self._runBuild()
|
if button == self.btnBuild:
|
||||||
|
self._runBuild()
|
||||||
|
elif button == self.btnOpen:
|
||||||
|
self._openOutputFolder()
|
||||||
elif role == QDialogButtonBox.RejectRole:
|
elif role == QDialogButtonBox.RejectRole:
|
||||||
self.close()
|
self.close()
|
||||||
return
|
return
|
||||||
@@ -386,4 +391,9 @@ class GuiManuscriptBuild(QDialog):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def _openOutputFolder(self):
|
||||||
|
"""Open the build folder in the system's file explorer."""
|
||||||
|
openExternalPath(Path(self.buildPath.text()))
|
||||||
|
return
|
||||||
|
|
||||||
# END Class GuiManuscriptBuild
|
# END Class GuiManuscriptBuild
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from PyQt5.QtCore import QUrl
|
||||||
|
from PyQt5.QtGui import QDesktopServices
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -32,10 +34,10 @@ from novelwriter.guimain import GuiMain
|
|||||||
from novelwriter.common import (
|
from novelwriter.common import (
|
||||||
checkBool, checkFloat, checkHandle, checkInt, checkIntTuple, checkPath,
|
checkBool, checkFloat, checkHandle, checkInt, checkIntTuple, checkPath,
|
||||||
checkString, checkStringNone, checkUuid, formatInt, formatTime,
|
checkString, checkStringNone, checkUuid, formatInt, formatTime,
|
||||||
formatTimeStamp, fuzzyTime, getGuiItem, hexToInt, isHandle, isItemClass,
|
formatTimeStamp, fuzzyTime, getFileSize, getGuiItem, hexToInt, isHandle,
|
||||||
isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax,
|
isItemClass, isItemLayout, isItemType, isTitleTag, jsonEncode,
|
||||||
numberToRoman, NWConfigParser, readTextFile, simplified, transferCase,
|
makeFileNameSafe, minmax, numberToRoman, NWConfigParser, openExternalPath,
|
||||||
xmlIndent, yesNo
|
readTextFile, simplified, transferCase, xmlIndent, yesNo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -621,6 +623,37 @@ def testBaseCommon_makeFileNameSafe():
|
|||||||
# END Test 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
|
@pytest.mark.base
|
||||||
def testBaseCommon_getGuiItem(nwGUI):
|
def testBaseCommon_getGuiItem(nwGUI):
|
||||||
"""Check the GUI item function."""
|
"""Check the GUI item function."""
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QUrl
|
||||||
|
from PyQt5.QtGui import QDesktopServices
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -132,6 +134,19 @@ def testManuscriptBuild_Main(
|
|||||||
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
|
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
|
||||||
assert manus._runBuild() is False
|
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
|
# Finish
|
||||||
manus._dialogButtonClicked(manus.dlgButtons.button(QDialogButtonBox.Close))
|
manus._dialogButtonClicked(manus.dlgButtons.button(QDialogButtonBox.Close))
|
||||||
# qtbot.stop()
|
# qtbot.stop()
|
||||||
|
|||||||
Reference in New Issue
Block a user