diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 03a102d7..ba7c8432 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -124,7 +124,7 @@ class GuiOutlineView(QWidget): def openProjectTasks(self) -> None: """Run open project tasks.""" lastOutline = SHARED.project.data.getLastHandle("outline") - if not (lastOutline is None or lastOutline in SHARED.project.tree): + if not lastOutline or lastOutline not in SHARED.project.tree: lastOutline = SHARED.project.tree.findRoot(nwItemClass.NOVEL) logger.debug("Setting outline tree to root item '%s'", lastOutline) @@ -504,10 +504,9 @@ class GuiOutlineTree(QTreeWidget): """Get the currently selected handle. If multiple items are selected, return the first. """ - selItem = self.selectedItems() - if selItem: - tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE) - sTitle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE) + if item := self.selectedItems(): + tHandle = item[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE) + sTitle = item[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE) return tHandle, sTitle return None, None @@ -878,18 +877,15 @@ class GuiOutlineDetails(QScrollArea): self.entKeyValue.setWordWrap(True) self.cstKeyValue.setWordWrap(True) - def tagClicked(link): - self.itemTagClicked.emit(link) - - self.povKeyValue.linkActivated.connect(tagClicked) - self.focKeyValue.linkActivated.connect(tagClicked) - self.chrKeyValue.linkActivated.connect(tagClicked) - self.pltKeyValue.linkActivated.connect(tagClicked) - self.timKeyValue.linkActivated.connect(tagClicked) - self.wldKeyValue.linkActivated.connect(tagClicked) - self.objKeyValue.linkActivated.connect(tagClicked) - self.entKeyValue.linkActivated.connect(tagClicked) - self.cstKeyValue.linkActivated.connect(tagClicked) + self.povKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) + self.focKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) + self.chrKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) + self.pltKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) + self.timKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) + self.wldKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) + self.objKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) + self.entKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) + self.cstKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) self.povKeyLWrap.addWidget(self.povKeyValue, 1) self.focKeyLWrap.addWidget(self.focKeyValue, 1) @@ -1018,7 +1014,7 @@ class GuiOutlineDetails(QScrollArea): ## @pyqtSlot(str, str) - def showItem(self, tHandle: str, sTitle: str) -> bool: + def showItem(self, tHandle: str, sTitle: str) -> None: """Update the content of the tree with the given handle and line number pointing to a header. """ @@ -1026,41 +1022,32 @@ class GuiOutlineDetails(QScrollArea): nwItem = SHARED.project.tree[tHandle] novIdx = pIndex.getItemHeader(tHandle, sTitle) novRefs = pIndex.getReferences(tHandle, sTitle) - if nwItem is None or novIdx is None: - return False + if nwItem and novIdx: + self.titleLabel.setText("%s" % self.tr(self.LVL_MAP.get(novIdx.level, "H1"))) + self.titleValue.setText(novIdx.title) - if novIdx.level in self.LVL_MAP: - self.titleLabel.setText("%s" % self.tr(self.LVL_MAP[novIdx.level])) - else: - self.titleLabel.setText("%s" % self.tr("Title")) - self.titleValue.setText(novIdx.title) + itemStatus, _ = nwItem.getImportStatus(incIcon=False) - itemStatus, _ = nwItem.getImportStatus(incIcon=False) + self.fileValue.setText(nwItem.itemName) + self.itemValue.setText(itemStatus) - self.fileValue.setText(nwItem.itemName) - self.itemValue.setText(itemStatus) + self.cCValue.setText(f"{checkInt(novIdx.charCount, 0):n}") + self.wCValue.setText(f"{checkInt(novIdx.wordCount, 0):n}") + self.pCValue.setText(f"{checkInt(novIdx.paraCount, 0):n}") - cC = checkInt(novIdx.charCount, 0) - wC = checkInt(novIdx.wordCount, 0) - pC = checkInt(novIdx.paraCount, 0) + self.synopValue.setText(novIdx.synopsis) - self.cCValue.setText(f"{cC:n}") - self.wCValue.setText(f"{wC:n}") - self.pCValue.setText(f"{pC:n}") + self.povKeyValue.setText(self._formatTags(novRefs, nwKeyWords.POV_KEY)) + self.focKeyValue.setText(self._formatTags(novRefs, nwKeyWords.FOCUS_KEY)) + self.chrKeyValue.setText(self._formatTags(novRefs, nwKeyWords.CHAR_KEY)) + self.pltKeyValue.setText(self._formatTags(novRefs, nwKeyWords.PLOT_KEY)) + self.timKeyValue.setText(self._formatTags(novRefs, nwKeyWords.TIME_KEY)) + self.wldKeyValue.setText(self._formatTags(novRefs, nwKeyWords.WORLD_KEY)) + self.objKeyValue.setText(self._formatTags(novRefs, nwKeyWords.OBJECT_KEY)) + self.entKeyValue.setText(self._formatTags(novRefs, nwKeyWords.ENTITY_KEY)) + self.cstKeyValue.setText(self._formatTags(novRefs, nwKeyWords.CUSTOM_KEY)) - self.synopValue.setText(novIdx.synopsis) - - self.povKeyValue.setText(self._formatTags(novRefs, nwKeyWords.POV_KEY)) - self.focKeyValue.setText(self._formatTags(novRefs, nwKeyWords.FOCUS_KEY)) - self.chrKeyValue.setText(self._formatTags(novRefs, nwKeyWords.CHAR_KEY)) - self.pltKeyValue.setText(self._formatTags(novRefs, nwKeyWords.PLOT_KEY)) - self.timKeyValue.setText(self._formatTags(novRefs, nwKeyWords.TIME_KEY)) - self.wldKeyValue.setText(self._formatTags(novRefs, nwKeyWords.WORLD_KEY)) - self.objKeyValue.setText(self._formatTags(novRefs, nwKeyWords.OBJECT_KEY)) - self.entKeyValue.setText(self._formatTags(novRefs, nwKeyWords.ENTITY_KEY)) - self.cstKeyValue.setText(self._formatTags(novRefs, nwKeyWords.CUSTOM_KEY)) - - return True + return @pyqtSlot() def updateClasses(self) -> None: diff --git a/tests/reference/guiOutline_Content_outline.csv b/tests/reference/guiOutline_Content_outline.csv new file mode 100644 index 00000000..34032aae --- /dev/null +++ b/tests/reference/guiOutline_Content_outline.csv @@ -0,0 +1,13 @@ +"Title","Document","Words","Pars","POV","Characters","Plot","Locations","Synopsis" +"Lorem Ipsum","Lorem Ipsum","40","3","","","","","" +"Prologue","Prologue","92","1","","","","","Explanation from the lipsum.com website." +"Act One","Act One","6","1","","","","","" +"Chapter One","Chapter One","67","1","Bod","","Main","Europe","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam." +"Scene One","Scene One","174","2","Bod","","Main","Europe","Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur." +"Scene One, Section Two","Scene One","230","2","","","","","" +"Scene Two","Scene Two","299","3","Bod","","Main","Europe","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci." +"Scene Two, Section Two","Scene Two","301","3","","","","","" +"Chapter Two","Chapter Two","70","1","Bod","","Main","Europe","Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue." +"Scene Three","Scene Three","439","4","Bod","","Main","Europe","Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos." +"Scene Four","Scene Four","563","6","Bod","","Main","Europe","Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo." +"Scene Five","Scene Five","543","5","Bod","","Main","Europe","Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus." diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 71cdeecb..68c6aa55 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -507,6 +507,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): errPos = currPos - 13 if not sys.platform.startswith("win32"): # Skip on Windows as spell checking is off there + # This check will fail without an 'en' dictionary, like aspell-en word, cPos, cLen, suggest = docEditor._qDocument.spellErrorAtPos(errPos) assert word == "tesst" assert cPos == 15 diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index f428e81d..48e4a06e 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -23,10 +23,12 @@ from __future__ import annotations import time import pytest -from tools import buildTestProject, writeFile +from shutil import copyfile + +from tools import buildTestProject, cmpFiles, writeFile from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QWidget, QAction +from PyQt5.QtWidgets import QFileDialog, QWidget, QAction from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemClass, nwOutline, nwView @@ -72,6 +74,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, projPath): # Option State # ============ + pOptions = SHARED.project.options colNames = [h.name for h in nwOutline] colItems = [h for h in nwOutline] @@ -156,13 +159,25 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, projPath): assert len(hiddenStates) == len(columnState) assert not any(hiddenStates) + # Move Columns + # ============ + + # Current Order + order = [outlineTree._colIdx[col] for col in outlineTree._treeOrder] + assert order == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + + # Move 3 to 0 + outlineTree._columnMoved(0, 3, 0) + order = [outlineTree._colIdx[col] for col in outlineTree._treeOrder] + assert order == [3, 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + # qtbot.stop() # END Test testGuiOutline_Main @pytest.mark.gui -def testGuiOutline_Content(qtbot, nwGUI, prjLipsum): +def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, prjLipsum, fncPath, tstPaths): """Test the outline view.""" assert nwGUI.openProject(prjLipsum) @@ -234,6 +249,8 @@ def testGuiOutline_Content(qtbot, nwGUI, prjLipsum): # Scene One selItem = outlineTree.topLevelItem(4) + outlineTree.clearSelection() + assert outlineTree.getSelectedHandle() == (None, None) # No selection outlineTree.setCurrentItem(selItem) tHandle, sTitle = outlineTree.getSelectedHandle() assert tHandle == "88243afbe5ed8" @@ -265,6 +282,19 @@ def testGuiOutline_Content(qtbot, nwGUI, prjLipsum): outlineTree._treeDoubleClick(selItem, 0) assert nwGUI.docEditor.docHandle == "88243afbe5ed8" + # Dump to CSV + # =========== + with monkeypatch.context() as mp: + csvFile = fncPath / "outline.csv" + mp.setattr(QFileDialog, "getSaveFileName", lambda *a, **k: (str(csvFile), "")) + outlineBar.aExport.trigger() + + testFile = tstPaths.outDir / "guiOutline_Content_outline.csv" + compFile = tstPaths.refDir / "guiOutline_Content_outline.csv" + + copyfile(csvFile, testFile) + assert cmpFiles(testFile, compFile) + # qtbot.stop() # END Test testGuiOutline_Content