2727"""
2828from __future__ import annotations
2929
30+ import csv
3031import logging
3132
3233from time import time
3334from enum import Enum
3435
35- from PyQt5 .QtCore import (
36- Qt , pyqtSignal , pyqtSlot , QSize , QT_TRANSLATE_NOOP
37- )
36+ from PyQt5 .QtCore import Qt , pyqtSignal , pyqtSlot , QSize , QT_TRANSLATE_NOOP
3837from PyQt5 .QtWidgets import (
39- QAbstractItemView , QAction , QFrame , QGridLayout , QGroupBox , QHBoxLayout ,
40- QLabel , QMenu , QScrollArea , QSizePolicy , QSplitter , QToolBar , QToolButton ,
41- QTreeWidget , QTreeWidgetItem , QVBoxLayout , QWidget
38+ QAbstractItemView , QAction , QFileDialog , QFrame , QGridLayout , QGroupBox ,
39+ QHBoxLayout , QLabel , QMenu , QScrollArea , QSizePolicy , QSplitter , QToolBar ,
40+ QToolButton , QTreeWidget , QTreeWidgetItem , QVBoxLayout , QWidget
4241)
4342
4443from novelwriter import CONFIG , SHARED
4544from novelwriter .enum import (
4645 nwDocMode , nwItemClass , nwItemLayout , nwItemType , nwOutline
4746)
4847from novelwriter .error import logException
49- from novelwriter .common import checkInt
48+ from novelwriter .common import checkInt , formatFileFilter , makeFileNameSafe
5049from novelwriter .constants import nwHeaders , trConst , nwKeyWords , nwLabels
5150from novelwriter .extensions .novelselector import NovelSelector
5251
@@ -88,6 +87,7 @@ def __init__(self, parent: QWidget) -> None:
8887 self .outlineData .itemTagClicked .connect (self ._tagClicked )
8988 self .outlineBar .loadNovelRootRequest .connect (self ._rootItemChanged )
9089 self .outlineBar .viewColumnToggled .connect (self .outlineTree .menuColumnToggled )
90+ self .outlineBar .outlineExportRequest .connect (self .outlineTree .exportOutline )
9191
9292 # Function Mappings
9393 self .getSelectedHandle = self .outlineTree .getSelectedHandle
@@ -124,7 +124,7 @@ def clearOutline(self) -> None:
124124 def openProjectTasks (self ) -> None :
125125 """Run open project tasks."""
126126 lastOutline = SHARED .project .data .getLastHandle ("outline" )
127- if not ( lastOutline is None or lastOutline in SHARED .project .tree ) :
127+ if not lastOutline or lastOutline not in SHARED .project .tree :
128128 lastOutline = SHARED .project .tree .findRoot (nwItemClass .NOVEL )
129129
130130 logger .debug ("Setting outline tree to root item '%s'" , lastOutline )
@@ -198,6 +198,7 @@ def _rootItemChanged(self, tHandle) -> None:
198198class GuiOutlineToolBar (QToolBar ):
199199
200200 loadNovelRootRequest = pyqtSignal (str )
201+ outlineExportRequest = pyqtSignal ()
201202 viewColumnToggled = pyqtSignal (bool , Enum )
202203
203204 def __init__ (self , outlineView : GuiOutlineView ) -> None :
@@ -228,6 +229,9 @@ def __init__(self, outlineView: GuiOutlineView) -> None:
228229 self .aRefresh = QAction (self .tr ("Refresh" ), self )
229230 self .aRefresh .triggered .connect (self ._refreshRequested )
230231
232+ self .aExport = QAction (self .tr ("Export CSV" ), self )
233+ self .aExport .triggered .connect (self ._exportRequested )
234+
231235 # Column Menu
232236 self .mColumns = GuiOutlineHeaderMenu (self )
233237 self .mColumns .columnToggled .connect (
@@ -243,6 +247,7 @@ def __init__(self, outlineView: GuiOutlineView) -> None:
243247 self .addWidget (self .novelValue )
244248 self .addSeparator ()
245249 self .addAction (self .aRefresh )
250+ self .addAction (self .aExport )
246251 self .addWidget (self .tbColumns )
247252 self .addWidget (stretch )
248253
@@ -261,6 +266,7 @@ def updateTheme(self) -> None:
261266 self .setStyleSheet ("QToolBar {border: 0px;}" )
262267 self .novelValue .refreshNovelList ()
263268 self .aRefresh .setIcon (SHARED .theme .getIcon ("refresh" ))
269+ self .aExport .setIcon (SHARED .theme .getIcon ("export" ))
264270 self .tbColumns .setIcon (SHARED .theme .getIcon ("menu" ))
265271 self .tbColumns .setStyleSheet ("QToolButton::menu-indicator {image: none;}" )
266272 return
@@ -296,6 +302,12 @@ def _refreshRequested(self) -> None:
296302 self .loadNovelRootRequest .emit (self .novelValue .handle )
297303 return
298304
305+ @pyqtSlot ()
306+ def _exportRequested (self ) -> None :
307+ """Emit a signal that an export of the outline was requested."""
308+ self .outlineExportRequest .emit ()
309+ return
310+
299311# END Class GuiOutlineToolBar
300312
301313
@@ -492,15 +504,48 @@ def getSelectedHandle(self) -> tuple[str | None, str | None]:
492504 """Get the currently selected handle. If multiple items are
493505 selected, return the first.
494506 """
495- selItem = self .selectedItems ()
496- if selItem :
497- tHandle = selItem [0 ].data (self ._colIdx [nwOutline .TITLE ], self .D_HANDLE )
498- sTitle = selItem [0 ].data (self ._colIdx [nwOutline .TITLE ], self .D_TITLE )
507+ if item := self .selectedItems ():
508+ tHandle = item [0 ].data (self ._colIdx [nwOutline .TITLE ], self .D_HANDLE )
509+ sTitle = item [0 ].data (self ._colIdx [nwOutline .TITLE ], self .D_TITLE )
499510 return tHandle , sTitle
500511 return None , None
501512
502513 ##
503- # Slots
514+ # Public Slots
515+ ##
516+
517+ @pyqtSlot (bool , Enum )
518+ def menuColumnToggled (self , isChecked : bool , hItem : nwOutline ) -> None :
519+ """Receive the changes to column visibility forwarded by the
520+ column selection menu.
521+ """
522+ if hItem in self ._colIdx :
523+ self .setColumnHidden (self ._colIdx [hItem ], not isChecked )
524+ self ._saveHeaderState ()
525+ return
526+
527+ @pyqtSlot ()
528+ def exportOutline (self ) -> None :
529+ """Export the outline as a CSV file."""
530+ path = CONFIG .lastPath () / f"{ makeFileNameSafe (SHARED .project .data .name )} .csv"
531+ path , _ = QFileDialog .getSaveFileName (
532+ self , self .tr ("Save Outline As" ), str (path ), formatFileFilter (["*.csv" , "*" ])
533+ )
534+ if path :
535+ CONFIG .setLastPath (path )
536+ logger .info ("Writing CSV file: %s" , path )
537+ cols = [col for col in self ._treeOrder if not self ._colHidden [col ]]
538+ order = [self ._colIdx [col ] for col in cols ]
539+ with open (path , mode = "w" , newline = "" ) as csvFile :
540+ writer = csv .writer (csvFile , dialect = "excel" , quoting = csv .QUOTE_ALL )
541+ writer .writerow ([trConst (nwLabels .OUTLINE_COLS [col ]) for col in cols ])
542+ for i in range (self .topLevelItemCount ()):
543+ if item := self .topLevelItem (i ):
544+ writer .writerow (item .text (i ) for i in order )
545+ return
546+
547+ ##
548+ # Private Slots
504549 ##
505550
506551 @pyqtSlot ("QTreeWidgetItem*" , int )
@@ -510,9 +555,8 @@ def _treeDoubleClick(self, tItem: QTreeWidgetItem, tCol: int) -> None:
510555 document editor.
511556 """
512557 tHandle , sTitle = self .getSelectedHandle ()
513- if tHandle is None :
514- return
515- self .outlineView .openDocumentRequest .emit (tHandle , nwDocMode .EDIT , sTitle or "" , True )
558+ if tHandle :
559+ self .outlineView .openDocumentRequest .emit (tHandle , nwDocMode .EDIT , sTitle or "" , True )
516560 return
517561
518562 @pyqtSlot ()
@@ -536,16 +580,6 @@ def _columnMoved(self, logIdx: int, oldVisualIdx: int, newVisualIdx: int) -> Non
536580 self ._saveHeaderState ()
537581 return
538582
539- @pyqtSlot (bool , Enum )
540- def menuColumnToggled (self , isChecked : bool , hItem : nwOutline ) -> None :
541- """Receive the changes to column visibility forwarded by the
542- column selection menu.
543- """
544- if hItem in self ._colIdx :
545- self .setColumnHidden (self ._colIdx [hItem ], not isChecked )
546- self ._saveHeaderState ()
547- return
548-
549583 ##
550584 # Internal Functions
551585 ##
@@ -843,18 +877,15 @@ def __init__(self, outlineView: GuiOutlineView) -> None:
843877 self .entKeyValue .setWordWrap (True )
844878 self .cstKeyValue .setWordWrap (True )
845879
846- def tagClicked (link ):
847- self .itemTagClicked .emit (link )
848-
849- self .povKeyValue .linkActivated .connect (tagClicked )
850- self .focKeyValue .linkActivated .connect (tagClicked )
851- self .chrKeyValue .linkActivated .connect (tagClicked )
852- self .pltKeyValue .linkActivated .connect (tagClicked )
853- self .timKeyValue .linkActivated .connect (tagClicked )
854- self .wldKeyValue .linkActivated .connect (tagClicked )
855- self .objKeyValue .linkActivated .connect (tagClicked )
856- self .entKeyValue .linkActivated .connect (tagClicked )
857- self .cstKeyValue .linkActivated .connect (tagClicked )
880+ self .povKeyValue .linkActivated .connect (lambda x : self .itemTagClicked .emit (x ))
881+ self .focKeyValue .linkActivated .connect (lambda x : self .itemTagClicked .emit (x ))
882+ self .chrKeyValue .linkActivated .connect (lambda x : self .itemTagClicked .emit (x ))
883+ self .pltKeyValue .linkActivated .connect (lambda x : self .itemTagClicked .emit (x ))
884+ self .timKeyValue .linkActivated .connect (lambda x : self .itemTagClicked .emit (x ))
885+ self .wldKeyValue .linkActivated .connect (lambda x : self .itemTagClicked .emit (x ))
886+ self .objKeyValue .linkActivated .connect (lambda x : self .itemTagClicked .emit (x ))
887+ self .entKeyValue .linkActivated .connect (lambda x : self .itemTagClicked .emit (x ))
888+ self .cstKeyValue .linkActivated .connect (lambda x : self .itemTagClicked .emit (x ))
858889
859890 self .povKeyLWrap .addWidget (self .povKeyValue , 1 )
860891 self .focKeyLWrap .addWidget (self .focKeyValue , 1 )
@@ -983,49 +1014,40 @@ def clearDetails(self) -> None:
9831014 ##
9841015
9851016 @pyqtSlot (str , str )
986- def showItem (self , tHandle : str , sTitle : str ) -> bool :
1017+ def showItem (self , tHandle : str , sTitle : str ) -> None :
9871018 """Update the content of the tree with the given handle and line
9881019 number pointing to a header.
9891020 """
9901021 pIndex = SHARED .project .index
9911022 nwItem = SHARED .project .tree [tHandle ]
9921023 novIdx = pIndex .getItemHeader (tHandle , sTitle )
9931024 novRefs = pIndex .getReferences (tHandle , sTitle )
994- if nwItem is None or novIdx is None :
995- return False
996-
997- if novIdx .level in self .LVL_MAP :
998- self .titleLabel .setText ("<b>%s</b>" % self .tr (self .LVL_MAP [novIdx .level ]))
999- else :
1000- self .titleLabel .setText ("<b>%s</b>" % self .tr ("Title" ))
1001- self .titleValue .setText (novIdx .title )
1025+ if nwItem and novIdx :
1026+ self .titleLabel .setText ("<b>%s</b>" % self .tr (self .LVL_MAP .get (novIdx .level , "H1" )))
1027+ self .titleValue .setText (novIdx .title )
10021028
1003- itemStatus , _ = nwItem .getImportStatus (incIcon = False )
1029+ itemStatus , _ = nwItem .getImportStatus (incIcon = False )
10041030
1005- self .fileValue .setText (nwItem .itemName )
1006- self .itemValue .setText (itemStatus )
1031+ self .fileValue .setText (nwItem .itemName )
1032+ self .itemValue .setText (itemStatus )
10071033
1008- cC = checkInt (novIdx .charCount , 0 )
1009- wC = checkInt (novIdx .wordCount , 0 )
1010- pC = checkInt (novIdx .paraCount , 0 )
1034+ self . cCValue . setText ( f" { checkInt (novIdx .charCount , 0 ):n } " )
1035+ self . wCValue . setText ( f" { checkInt (novIdx .wordCount , 0 ):n } " )
1036+ self . pCValue . setText ( f" { checkInt (novIdx .paraCount , 0 ):n } " )
10111037
1012- self .cCValue .setText (f"{ cC :n} " )
1013- self .wCValue .setText (f"{ wC :n} " )
1014- self .pCValue .setText (f"{ pC :n} " )
1038+ self .synopValue .setText (novIdx .synopsis )
10151039
1016- self .synopValue .setText (novIdx .synopsis )
1040+ self .povKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .POV_KEY ))
1041+ self .focKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .FOCUS_KEY ))
1042+ self .chrKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .CHAR_KEY ))
1043+ self .pltKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .PLOT_KEY ))
1044+ self .timKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .TIME_KEY ))
1045+ self .wldKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .WORLD_KEY ))
1046+ self .objKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .OBJECT_KEY ))
1047+ self .entKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .ENTITY_KEY ))
1048+ self .cstKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .CUSTOM_KEY ))
10171049
1018- self .povKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .POV_KEY ))
1019- self .focKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .FOCUS_KEY ))
1020- self .chrKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .CHAR_KEY ))
1021- self .pltKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .PLOT_KEY ))
1022- self .timKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .TIME_KEY ))
1023- self .wldKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .WORLD_KEY ))
1024- self .objKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .OBJECT_KEY ))
1025- self .entKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .ENTITY_KEY ))
1026- self .cstKeyValue .setText (self ._formatTags (novRefs , nwKeyWords .CUSTOM_KEY ))
1027-
1028- return True
1050+ return
10291051
10301052 @pyqtSlot ()
10311053 def updateClasses (self ) -> None :
0 commit comments