diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 9cf0e418..6745db9d 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -58,12 +58,13 @@ logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser):
+ closeDocumentRequest = pyqtSignal()
documentLoaded = pyqtSignal(str)
loadDocumentTagRequest = pyqtSignal(str, Enum)
- closeDocumentRequest = pyqtSignal()
+ openDocumentRequest = pyqtSignal(str, Enum, str, bool)
reloadDocumentRequest = pyqtSignal()
- togglePanelVisibility = pyqtSignal()
requestProjectItemSelected = pyqtSignal(str, bool)
+ togglePanelVisibility = pyqtSignal()
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
@@ -640,6 +641,11 @@ class GuiDocViewHeader(QWidget):
self.forwardButton.setToolTip(self.tr("Go Forward"))
self.forwardButton.clicked.connect(self.docViewer.navForward)
+ self.editButton = NIconToolButton(self, iSz)
+ self.editButton.setVisible(False)
+ self.editButton.setToolTip(self.tr("Open in Editor"))
+ self.editButton.clicked.connect(self._editDocument)
+
self.refreshButton = NIconToolButton(self, iSz)
self.refreshButton.setVisible(False)
self.refreshButton.setToolTip(self.tr("Reload"))
@@ -658,7 +664,7 @@ class GuiDocViewHeader(QWidget):
self.outerBox.addSpacing(mPx)
self.outerBox.addWidget(self.itemTitle, 1)
self.outerBox.addSpacing(mPx)
- self.outerBox.addSpacing(iPx)
+ self.outerBox.addWidget(self.editButton, 0)
self.outerBox.addWidget(self.refreshButton, 0)
self.outerBox.addWidget(self.closeButton, 0)
self.outerBox.setSpacing(0)
@@ -692,8 +698,9 @@ class GuiDocViewHeader(QWidget):
self.outlineButton.setVisible(False)
self.backButton.setVisible(False)
self.forwardButton.setVisible(False)
- self.closeButton.setVisible(False)
+ self.editButton.setVisible(False)
self.refreshButton.setVisible(False)
+ self.closeButton.setVisible(False)
return
def setOutline(self, data: dict[str, tuple[str, int]]) -> None:
@@ -727,6 +734,7 @@ class GuiDocViewHeader(QWidget):
self.outlineButton.setThemeIcon("list")
self.backButton.setThemeIcon("backward")
self.forwardButton.setThemeIcon("forward")
+ self.editButton.setThemeIcon("edit")
self.refreshButton.setThemeIcon("refresh")
self.closeButton.setThemeIcon("close")
@@ -734,6 +742,7 @@ class GuiDocViewHeader(QWidget):
self.outlineButton.setStyleSheet(buttonStyle)
self.backButton.setStyleSheet(buttonStyle)
self.forwardButton.setStyleSheet(buttonStyle)
+ self.editButton.setStyleSheet(buttonStyle)
self.refreshButton.setStyleSheet(buttonStyle)
self.closeButton.setStyleSheet(buttonStyle)
@@ -776,8 +785,9 @@ class GuiDocViewHeader(QWidget):
self.backButton.setVisible(True)
self.forwardButton.setVisible(True)
self.outlineButton.setVisible(True)
- self.closeButton.setVisible(True)
+ self.editButton.setVisible(True)
self.refreshButton.setVisible(True)
+ self.closeButton.setVisible(True)
return
@@ -804,6 +814,13 @@ class GuiDocViewHeader(QWidget):
self.docViewer.reloadDocumentRequest.emit()
return
+ @pyqtSlot()
+ def _editDocument(self) -> None:
+ """Open the document in the editor."""
+ if tHandle := self._docHandle:
+ self.docViewer.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
+ return
+
##
# Events
##
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index cae1cec3..718caee0 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -141,8 +141,10 @@ class GuiOutlineView(QWidget):
def closeProjectTasks(self) -> None:
"""Run closing project tasks."""
+ if self.outlineTree.wasRendered:
+ # If the panel hasn't been drawn yet, those values are incorrect
+ self.outlineData.saveGuiSettings()
self.outlineTree.closeProjectTasks()
- self.outlineData.saveGuiSettings()
self.outlineData.updateClasses()
self.clearOutline()
return
@@ -434,6 +436,11 @@ class GuiOutlineTree(QTreeWidget):
def hiddenColumns(self) -> dict[nwOutline, bool]:
return self._colHidden
+ @property
+ def wasRendered(self) -> bool:
+ """Returns True after the Outline has been rendered once."""
+ return not self._firstView
+
##
# Methods
##
@@ -947,11 +954,12 @@ class GuiOutlineDetails(QScrollArea):
def loadGuiSettings(self) -> None:
"""Run open project tasks."""
- half = self.width() // 2
+ parent = self.outlineView.parent() # This widget is rendered already
+ width = parent.width() if isinstance(parent, QWidget) else 1000
pOptions = SHARED.project.options
self.mainSplit.setSizes([
- CONFIG.pxInt(pOptions.getInt("GuiOutlineDetails", "detailsWidth", half)),
- CONFIG.pxInt(pOptions.getInt("GuiOutlineDetails", "tagsWidth", half))
+ CONFIG.pxInt(pOptions.getInt("GuiOutlineDetails", "detailsWidth", width//3)),
+ CONFIG.pxInt(pOptions.getInt("GuiOutlineDetails", "tagsWidth", 2*width//3))
])
return
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 11dabf78..1bb744f6 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -270,6 +270,7 @@ class GuiMain(QMainWindow):
self.docViewer.closeDocumentRequest.connect(self.closeDocViewer)
self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle)
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
+ self.docViewer.openDocumentRequest.connect(self._openDocument)
self.docViewer.reloadDocumentRequest.connect(self._reloadViewer)
self.docViewer.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
self.docViewer.togglePanelVisibility.connect(self._toggleViewerPanelVisibility)
@@ -844,33 +845,33 @@ class GuiMain(QMainWindow):
def closeMain(self) -> bool:
"""Save everything, and close novelWriter."""
- if SHARED.hasProject and SHARED.question("%s
%s" % (
+ if SHARED.hasProject and not SHARED.question("%s
%s" % (
self.tr("Do you want to exit novelWriter?"),
self.tr("Changes are saved automatically.")
)):
- logger.info("Exiting novelWriter")
+ return False
- if not SHARED.focusMode:
- CONFIG.setMainPanePos(self.splitMain.sizes())
- CONFIG.setOutlinePanePos(self.outlineView.splitSizes())
- if self.docViewerPanel.isVisible():
- CONFIG.setViewPanePos(self.splitView.sizes())
+ logger.info("Exiting novelWriter")
- CONFIG.showViewerPanel = self.docViewerPanel.isVisible()
- wFull = Qt.WindowState.WindowFullScreen
- if self.windowState() & wFull != wFull:
- # Ignore window size if in full screen mode
- CONFIG.setMainWinSize(self.width(), self.height())
+ if not SHARED.focusMode:
+ CONFIG.setMainPanePos(self.splitMain.sizes())
+ CONFIG.setOutlinePanePos(self.outlineView.splitSizes())
+ if self.docViewerPanel.isVisible():
+ CONFIG.setViewPanePos(self.splitView.sizes())
- if SHARED.hasProject:
- self.closeProject(True)
- CONFIG.saveConfig()
+ CONFIG.showViewerPanel = self.docViewerPanel.isVisible()
+ wFull = Qt.WindowState.WindowFullScreen
+ if self.windowState() & wFull != wFull:
+ # Ignore window size if in full screen mode
+ CONFIG.setMainWinSize(self.width(), self.height())
- QApplication.quit()
+ if SHARED.hasProject:
+ self.closeProject(True)
+ CONFIG.saveConfig()
- return True
+ QApplication.quit()
- return False
+ return True
def closeViewerPanel(self, byUser: bool = True) -> bool:
"""Close the document view panel."""
diff --git a/pyproject.toml b/pyproject.toml
index 3a2f2c7c..08f4d999 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -65,3 +65,9 @@ exclude = ["docs/*"]
[tool.autopep8]
max_line_length = 99
ignore = ["E133", "E221", "E226", "E228", "E241", "W503"]
+
+[tool.coverage.run]
+branch = false
+
+[tool.coverage.report]
+precision = 2
diff --git a/tests/test_base/test_base_init.py b/tests/test_base/test_base_init.py
index 0ff69505..6fb691a0 100644
--- a/tests/test_base/test_base_init.py
+++ b/tests/test_base/test_base_init.py
@@ -85,6 +85,7 @@ def testBaseInit_Options(monkeypatch, fncPath):
# Defaults w/None Args
nwGUI = main()
+ assert nwGUI is not None
assert logger.getEffectiveLevel() == logging.WARNING
assert nwGUI.closeMain() == "closeMain"
@@ -92,6 +93,7 @@ def testBaseInit_Options(monkeypatch, fncPath):
nwGUI = main(
["--testmode", f"--config={fncPath}", f"--data={fncPath}", "--style=Fusion"]
)
+ assert nwGUI is not None
assert logger.getEffectiveLevel() == logging.WARNING
assert nwGUI.closeMain() == "closeMain"
@@ -99,12 +101,14 @@ def testBaseInit_Options(monkeypatch, fncPath):
nwGUI = main(
["--testmode", "--info", f"--config={fncPath}", f"--data={fncPath}"]
)
+ assert nwGUI is not None
assert logger.getEffectiveLevel() == logging.INFO
assert nwGUI.closeMain() == "closeMain"
nwGUI = main(
["--testmode", "--debug", f"--config={fncPath}", f"--data={fncPath}"]
)
+ assert nwGUI is not None
assert logger.getEffectiveLevel() == logging.DEBUG
assert nwGUI.closeMain() == "closeMain"
@@ -113,6 +117,7 @@ def testBaseInit_Options(monkeypatch, fncPath):
nwGUI = main(
["--testmode", "--help", f"--config={fncPath}", f"--data={fncPath}"]
)
+ assert nwGUI is not None
assert nwGUI.closeMain() == "closeMain"
assert ex.value.code == 0
@@ -120,6 +125,7 @@ def testBaseInit_Options(monkeypatch, fncPath):
nwGUI = main(
["--testmode", "--version", f"--config={fncPath}", f"--data={fncPath}"]
)
+ assert nwGUI is not None
assert nwGUI.closeMain() == "closeMain"
assert ex.value.code == 0
@@ -128,6 +134,7 @@ def testBaseInit_Options(monkeypatch, fncPath):
nwGUI = main(
["--testmode", "--invalid", f"--config={fncPath}", f"--data={fncPath}"]
)
+ assert nwGUI is not None
assert nwGUI.closeMain() == "closeMain"
assert ex.value.code == 2
@@ -135,6 +142,7 @@ def testBaseInit_Options(monkeypatch, fncPath):
nwGUI = main(
["--testmode", f"--config={fncPath}", f"--data={fncPath}", "sample/"]
)
+ assert nwGUI is not None
assert nwGUI.closeMain() == "closeMain"
diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py
index 30b0f89e..4f391cd8 100644
--- a/tests/test_gui/test_gui_docviewer.py
+++ b/tests/test_gui/test_gui_docviewer.py
@@ -31,7 +31,6 @@ from PyQt5.QtWidgets import QAction, QApplication, QMenu
from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction
from novelwriter.formats.toqdoc import ToQTextDocument
-from novelwriter.gui.docviewer import GuiDocViewer
from novelwriter.types import QtModNone, QtMouseLeft
from tests.mocked import causeException
@@ -42,7 +41,8 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
"""Test the document viewer."""
# Open project
assert nwGUI.openProject(prjLipsum)
- docViewer: GuiDocViewer = nwGUI.docViewer
+ docEditor = nwGUI.docEditor
+ docViewer = nwGUI.docViewer
# Select a document in the project tree
nwGUI.projView.setSelectedHandle("88243afbe5ed8")
@@ -74,6 +74,12 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
docViewer.docHeader._refreshDocument()
assert docViewer.toPlainText() == origText
+ # Open in editor
+ nwGUI.closeDocument()
+ assert docEditor.docHandle is None
+ docViewer.docHeader._editDocument()
+ assert docEditor.docHandle == docViewer.docHandle
+
# Select word
cursor = docViewer.textCursor()
cursor.setPosition(100)
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 6d151b81..8bdbc45e 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -100,6 +100,13 @@ def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, projPath):
assert nwGUI.openProject(projPath) is True
nwGUI.closeProject()
+ # Check that closes can be blocked
+ with monkeypatch.context() as mp:
+ mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
+ assert nwGUI.openProject(projPath) is True
+ assert nwGUI.closeMain() is False
+ nwGUI.closeProject()
+
# Check that latest release info updated
assert CONFIG.lastNotes != "0x0"