From 25b6d1c00146110b15c0bc32443bc4c9d2b20b0c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 15 Feb 2024 21:09:41 +0100 Subject: [PATCH] Add CSV export to outline --- novelwriter/constants.py | 1 + novelwriter/gui/outline.py | 77 +++++++++++++++++++++++++++----------- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index c012349f..39fd7a69 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -274,6 +274,7 @@ class nwLabels: "*.txt": QT_TRANSLATE_NOOP("Constant", "Text files"), "*.md": QT_TRANSLATE_NOOP("Constant", "Markdown files"), "*.nwd": QT_TRANSLATE_NOOP("Constant", "novelWriter files"), + "*.csv": QT_TRANSLATE_NOOP("Constant", "CSV files"), "*": QT_TRANSLATE_NOOP("Constant", "All files"), } UNIT_NAME = { diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 5e601990..03a102d7 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -27,18 +27,17 @@ along with this program. If not, see . """ from __future__ import annotations +import csv import logging from time import time from enum import Enum -from PyQt5.QtCore import ( - Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP -) +from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP from PyQt5.QtWidgets import ( - QAbstractItemView, QAction, QFrame, QGridLayout, QGroupBox, QHBoxLayout, - QLabel, QMenu, QScrollArea, QSizePolicy, QSplitter, QToolBar, QToolButton, - QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget + QAbstractItemView, QAction, QFileDialog, QFrame, QGridLayout, QGroupBox, + QHBoxLayout, QLabel, QMenu, QScrollArea, QSizePolicy, QSplitter, QToolBar, + QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED @@ -46,7 +45,7 @@ from novelwriter.enum import ( nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline ) from novelwriter.error import logException -from novelwriter.common import checkInt +from novelwriter.common import checkInt, formatFileFilter, makeFileNameSafe from novelwriter.constants import nwHeaders, trConst, nwKeyWords, nwLabels from novelwriter.extensions.novelselector import NovelSelector @@ -88,6 +87,7 @@ class GuiOutlineView(QWidget): self.outlineData.itemTagClicked.connect(self._tagClicked) self.outlineBar.loadNovelRootRequest.connect(self._rootItemChanged) self.outlineBar.viewColumnToggled.connect(self.outlineTree.menuColumnToggled) + self.outlineBar.outlineExportRequest.connect(self.outlineTree.exportOutline) # Function Mappings self.getSelectedHandle = self.outlineTree.getSelectedHandle @@ -198,6 +198,7 @@ class GuiOutlineView(QWidget): class GuiOutlineToolBar(QToolBar): loadNovelRootRequest = pyqtSignal(str) + outlineExportRequest = pyqtSignal() viewColumnToggled = pyqtSignal(bool, Enum) def __init__(self, outlineView: GuiOutlineView) -> None: @@ -228,6 +229,9 @@ class GuiOutlineToolBar(QToolBar): self.aRefresh = QAction(self.tr("Refresh"), self) self.aRefresh.triggered.connect(self._refreshRequested) + self.aExport = QAction(self.tr("Export CSV"), self) + self.aExport.triggered.connect(self._exportRequested) + # Column Menu self.mColumns = GuiOutlineHeaderMenu(self) self.mColumns.columnToggled.connect( @@ -243,6 +247,7 @@ class GuiOutlineToolBar(QToolBar): self.addWidget(self.novelValue) self.addSeparator() self.addAction(self.aRefresh) + self.addAction(self.aExport) self.addWidget(self.tbColumns) self.addWidget(stretch) @@ -261,6 +266,7 @@ class GuiOutlineToolBar(QToolBar): self.setStyleSheet("QToolBar {border: 0px;}") self.novelValue.refreshNovelList() self.aRefresh.setIcon(SHARED.theme.getIcon("refresh")) + self.aExport.setIcon(SHARED.theme.getIcon("export")) self.tbColumns.setIcon(SHARED.theme.getIcon("menu")) self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}") return @@ -296,6 +302,12 @@ class GuiOutlineToolBar(QToolBar): self.loadNovelRootRequest.emit(self.novelValue.handle) return + @pyqtSlot() + def _exportRequested(self) -> None: + """Emit a signal that an export of the outline was requested.""" + self.outlineExportRequest.emit() + return + # END Class GuiOutlineToolBar @@ -500,7 +512,41 @@ class GuiOutlineTree(QTreeWidget): return None, None ## - # Slots + # Public Slots + ## + + @pyqtSlot(bool, Enum) + def menuColumnToggled(self, isChecked: bool, hItem: nwOutline) -> None: + """Receive the changes to column visibility forwarded by the + column selection menu. + """ + if hItem in self._colIdx: + self.setColumnHidden(self._colIdx[hItem], not isChecked) + self._saveHeaderState() + return + + @pyqtSlot() + def exportOutline(self) -> None: + """Export the outline as a CSV file.""" + path = CONFIG.lastPath() / f"{makeFileNameSafe(SHARED.project.data.name)}.csv" + path, _ = QFileDialog.getSaveFileName( + self, self.tr("Save Outline As"), str(path), formatFileFilter(["*.csv", "*"]) + ) + if path: + CONFIG.setLastPath(path) + logger.info("Writing CSV file: %s", path) + cols = [col for col in self._treeOrder if not self._colHidden[col]] + order = [self._colIdx[col] for col in cols] + with open(path, mode="w") as csvFile: + writer = csv.writer(csvFile, dialect="excel", quoting=csv.QUOTE_ALL) + writer.writerow([trConst(nwLabels.OUTLINE_COLS[col]) for col in cols]) + for i in range(self.topLevelItemCount()): + if item := self.topLevelItem(i): + writer.writerow(item.text(i) for i in order) + return + + ## + # Private Slots ## @pyqtSlot("QTreeWidgetItem*", int) @@ -510,9 +556,8 @@ class GuiOutlineTree(QTreeWidget): document editor. """ tHandle, sTitle = self.getSelectedHandle() - if tHandle is None: - return - self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True) + if tHandle: + self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True) return @pyqtSlot() @@ -536,16 +581,6 @@ class GuiOutlineTree(QTreeWidget): self._saveHeaderState() return - @pyqtSlot(bool, Enum) - def menuColumnToggled(self, isChecked: bool, hItem: nwOutline) -> None: - """Receive the changes to column visibility forwarded by the - column selection menu. - """ - if hItem in self._colIdx: - self.setColumnHidden(self._colIdx[hItem], not isChecked) - self._saveHeaderState() - return - ## # Internal Functions ##