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"