diff --git a/nw/config.py b/nw/config.py index 985eca43..66fdb649 100644 --- a/nw/config.py +++ b/nw/config.py @@ -95,6 +95,7 @@ class Config: self.projColWidth = [140, 55, 140] self.mainPanePos = [300, 800] self.docPanePos = [400, 400] + self.outlnPanePos = [500, 150] self.isFullScreen = False ## Project @@ -342,6 +343,9 @@ class Config: self.docPanePos = self._parseLine( cnfParse, cnfSec, "docpane", self.CNF_LIST, self.docPanePos ) + self.outlnPanePos = self._parseLine( + cnfParse, cnfSec, "outlinepane", self.CNF_LIST, self.outlnPanePos + ) self.isFullScreen = self._parseLine( cnfParse, cnfSec, "fullscreen", self.CNF_BOOL, self.isFullScreen ) @@ -481,12 +485,13 @@ class Config: ## Sizes cnfSec = "Sizes" cnfParse.add_section(cnfSec) - cnfParse.set(cnfSec,"geometry", self._packList(self.winGeometry)) - cnfParse.set(cnfSec,"treecols", self._packList(self.treeColWidth)) - cnfParse.set(cnfSec,"projcols", self._packList(self.projColWidth)) - cnfParse.set(cnfSec,"mainpane", self._packList(self.mainPanePos)) - cnfParse.set(cnfSec,"docpane", self._packList(self.docPanePos)) - cnfParse.set(cnfSec,"fullscreen", str(self.isFullScreen)) + cnfParse.set(cnfSec,"geometry", self._packList(self.winGeometry)) + cnfParse.set(cnfSec,"treecols", self._packList(self.treeColWidth)) + cnfParse.set(cnfSec,"projcols", self._packList(self.projColWidth)) + cnfParse.set(cnfSec,"mainpane", self._packList(self.mainPanePos)) + cnfParse.set(cnfSec,"docpane", self._packList(self.docPanePos)) + cnfParse.set(cnfSec,"outlinepane", self._packList(self.outlnPanePos)) + cnfParse.set(cnfSec,"fullscreen", str(self.isFullScreen)) ## Project cnfSec = "Project" @@ -700,6 +705,11 @@ class Config: self.confChanged = True return True + def setOutlinePanePos(self, panePos): + self.outlnPanePos = panePos + self.confChanged = True + return True + def setShowRefPanel(self, checkState): self.showRefPanel = checkState self.confChanged = True diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 446426e4..fb1a9c99 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -268,6 +268,21 @@ class GuiDocViewer(QTextBrowser): logger.verbose("Cursor moved to line %d" % theLine) return True + ## + # Slots + ## + + def _linkClicked(self, theURL): + """Slot for a link in the document being clicked. + """ + theLink = theURL.url() + logger.verbose("Clicked link: '%s'" % theLink) + if len(theLink) > 0: + theBits = theLink.split("=") + if len(theBits) == 2: + self.loadFromTag(theBits[1]) + return + ## # Events ## @@ -293,17 +308,6 @@ class GuiDocViewer(QTextBrowser): self.setTextCursor(theCursor) return - def _linkClicked(self, theURL): - """Slot for a link in the document being clicked. - """ - theLink = theURL.url() - logger.verbose("Clicked link: '%s'" % theLink) - if len(theLink) > 0: - theBits = theLink.split("=") - if len(theBits) == 2: - self.loadFromTag(theBits[1]) - return - def _makeStyleSheet(self): """Generate an appropriate style sheet for the document viewer, based on the current syntax highlighter theme, diff --git a/nw/gui/outline.py b/nw/gui/outline.py index c3c81e4f..368b0403 100644 --- a/nw/gui/outline.py +++ b/nw/gui/outline.py @@ -100,6 +100,7 @@ class GuiOutline(QTreeWidget): self.setExpandsOnDoubleClick(False) self.setDragEnabled(False) self.itemDoubleClicked.connect(self._treeDoubleClick) + self.itemSelectionChanged.connect(self._itemSelected) iPx = self.theTheme.textIconSize self.setIconSize(QSize(iPx, iPx)) @@ -198,6 +199,18 @@ class GuiOutline(QTreeWidget): self.theParent.openDocument(tHandle, tLine - 1) return + def _itemSelected(self): + """Extract the handle and line number of the currently selected + title, and send it to the details panel. + """ + selItems = self.selectedItems() + if selItems: + tHandle = selItems[0].data(self.colIndex[nwOutline.TITLE], Qt.UserRole) + sTitle = selItems[0].data(self.colIndex[nwOutline.LINE], Qt.UserRole) + logger.verbose("User selected entry %s:%s" % (tHandle, sTitle)) + self.theParent.projMeta.showItem(tHandle, sTitle) + return + def _headerRightClick(self, clickPos): """Show the header column menu. """ @@ -415,6 +428,7 @@ class GuiOutline(QTreeWidget): newItem.setText(self.colIndex[nwOutline.LABEL], nwItem.itemName) newItem.setIcon(self.colIndex[nwOutline.LABEL], self.theTheme.getIcon("proj_document")) newItem.setText(self.colIndex[nwOutline.LINE], sTitle[1:].lstrip("0")) + newItem.setData(self.colIndex[nwOutline.LINE], Qt.UserRole, sTitle) newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"]) newItem.setText(self.colIndex[nwOutline.CCOUNT], str(novIdx["cCount"])) newItem.setText(self.colIndex[nwOutline.WCOUNT], str(novIdx["wCount"])) diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py index d4701cd5..66450155 100644 --- a/nw/gui/outlinedetails.py +++ b/nw/gui/outlinedetails.py @@ -30,13 +30,22 @@ import nw from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( - QWidget + QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel, QSizePolicy ) +from nw.constants import nwLabels, nwKeyWords + logger = logging.getLogger(__name__) class GuiOutlineDetails(QWidget): + LVL_MAP = { + "H1" : "Title", + "H2" : "Chapter", + "H3" : "Scene", + "H4" : "Section" + } + def __init__(self, theParent): QWidget.__init__(self, theParent) @@ -49,8 +58,207 @@ class GuiOutlineDetails(QWidget): self.theIndex = theParent.theIndex self.optState = theParent.theProject.optState + # Sizes + minTitle = self.theTheme.getTextWidth("X"*30) + maxTitle = self.theTheme.getTextWidth("X"*50) + wCount = self.theTheme.getTextWidth("99,999") + hSpace = int(0.5*self.theTheme.fontPixelSize) + vSpace = int(0.3*self.theTheme.fontPixelSize) + + # Details Area + self.titleLabel = QLabel("Title") + self.levelLabel = QLabel("Level") + self.fileLabel = QLabel("Document") + self.titleValue = QLabel("") + self.levelValue = QLabel("") + self.fileValue = QLabel("") + self.titleValue.setMinimumWidth(minTitle) + self.titleValue.setMaximumWidth(maxTitle) + self.levelValue.setMinimumWidth(minTitle) + self.levelValue.setMaximumWidth(maxTitle) + self.fileValue.setMinimumWidth(minTitle) + self.fileValue.setMaximumWidth(maxTitle) + + # Stats Area + self.cCLabel = QLabel("Characters") + self.wCLabel = QLabel("Words") + self.pCLabel = QLabel("Paragraphs") + self.cCValue = QLabel("") + self.wCValue = QLabel("") + self.pCValue = QLabel("") + self.cCValue.setMinimumWidth(wCount) + self.wCValue.setMinimumWidth(wCount) + self.pCValue.setMinimumWidth(wCount) + self.cCValue.setAlignment(Qt.AlignRight) + self.wCValue.setAlignment(Qt.AlignRight) + self.pCValue.setAlignment(Qt.AlignRight) + + # Synopsis + self.synopLabel = QLabel("Synopsis") + self.synopValue = QLabel("") + self.synopLWrap = QHBoxLayout() + self.synopValue.setWordWrap(True) + self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft) + self.synopValue.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) + self.synopLWrap.addWidget(self.synopValue, 1) + + # Tags + self.povKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.POV_KEY]) + self.chrKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY]) + self.pltKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]) + self.timKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.TIME_KEY]) + self.wldKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY]) + self.objKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY]) + self.entKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY]) + self.cstKeyLabel = QLabel("%s" % nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY]) + self.povKeyValue = QLabel("") + self.chrKeyValue = QLabel("") + self.pltKeyValue = QLabel("") + self.timKeyValue = QLabel("") + self.wldKeyValue = QLabel("") + self.objKeyValue = QLabel("") + self.entKeyValue = QLabel("") + self.cstKeyValue = QLabel("") + self.povKeyValue.linkActivated.connect(self._tagClicked) + self.chrKeyValue.linkActivated.connect(self._tagClicked) + self.pltKeyValue.linkActivated.connect(self._tagClicked) + self.timKeyValue.linkActivated.connect(self._tagClicked) + self.wldKeyValue.linkActivated.connect(self._tagClicked) + self.objKeyValue.linkActivated.connect(self._tagClicked) + self.entKeyValue.linkActivated.connect(self._tagClicked) + self.cstKeyValue.linkActivated.connect(self._tagClicked) + + # Selected Item Details + self.mainGroup = QGroupBox("Title Details", self) + self.mainForm = QGridLayout() + self.mainGroup.setLayout(self.mainForm) + + self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, Qt.AlignTop | Qt.AlignRight) + self.mainForm.addWidget(self.levelLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.levelValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, Qt.AlignTop | Qt.AlignRight) + self.mainForm.addWidget(self.fileLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.fileValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, Qt.AlignTop | Qt.AlignRight) + self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft) + self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft) + + self.mainForm.setColumnStretch(1, 1) + self.mainForm.setRowStretch(4, 1) + self.mainForm.setHorizontalSpacing(hSpace) + self.mainForm.setVerticalSpacing(vSpace) + + # Selected Item Tags + self.tagsGroup = QGroupBox("Tags", self) + self.tagsForm = QGridLayout() + self.tagsGroup.setLayout(self.tagsForm) + + self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.povKeyValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.chrKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.chrKeyValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.pltKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.pltKeyValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.timKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.timKeyValue, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.wldKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.wldKeyValue, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.objKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.objKeyValue, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.entKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.entKeyValue, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.cstKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft) + self.tagsForm.addWidget(self.cstKeyValue, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft) + + self.tagsForm.setColumnStretch(1, 1) + self.tagsForm.setRowStretch(8, 1) + self.tagsForm.setHorizontalSpacing(hSpace) + self.tagsForm.setVerticalSpacing(vSpace) + + # Assemble + self.outerBox = QHBoxLayout() + self.outerBox.addWidget(self.mainGroup, 0) + self.outerBox.addWidget(self.tagsGroup, 1) + self.outerBox.addStretch(1) + + self.setLayout(self.outerBox) + logger.debug("GuiOutlineDetails initialisation complete") return + def showItem(self, tHandle, sTitle): + """Update the content of the tree with the given handle and line + number pointing to a header. + """ + try: + nwItem = self.theProject.projTree[tHandle] + novIdx = self.theIndex.novelIndex[tHandle][sTitle] + theRefs = self.theIndex.getReferences(tHandle, sTitle) + except: + return False + + self.titleValue.setText(novIdx["title"]) + if novIdx["level"] in self.LVL_MAP: + self.levelValue.setText(self.LVL_MAP[novIdx["level"]]) + else: + self.levelValue.setText("Unknown") + self.fileValue.setText(nwItem.itemName) + + self.cCValue.setText("{:n}".format(novIdx["cCount"])) + self.wCValue.setText("{:n}".format(novIdx["pCount"])) + self.pCValue.setText("{:n}".format(novIdx["wCount"])) + + self.synopValue.setText(novIdx["synopsis"]) + self.synopValue.adjustSize() + # print(self.mainForm.sizeHint().width()) + # self.synopValue.setSizePolicy() (self.mainForm.sizeHint().width()) + + self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY)) + self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY)) + self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY)) + self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY)) + self.wldKeyValue.setText(self._formatTags(theRefs, nwKeyWords.WORLD_KEY)) + self.objKeyValue.setText(self._formatTags(theRefs, nwKeyWords.OBJECT_KEY)) + self.entKeyValue.setText(self._formatTags(theRefs, nwKeyWords.ENTITY_KEY)) + self.cstKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CUSTOM_KEY)) + + return True + + ## + # Slots + ## + + def _tagClicked(self, theLink): + """Capture the click of a tag in the right-most column. + """ + logger.verbose("Clicked link: '%s'" % theLink) + if len(theLink) > 0: + theBits = theLink.split("=") + if len(theBits) == 2: + self.theParent.docViewer.loadFromTag(theBits[1]) + self.theParent.tabWidget.setCurrentWidget(self.theParent.splitView) + return + + ## + # Internal Functions + ## + + def _formatTags(self, theRefs, theKey): + """Format the tags as clickable links. + """ + if theKey not in theKey: + return "" + refTags = [] + for tTag in theRefs[theKey]: + refTags.append("%s" % ( + theKey[1:], tTag, tTag + )) + return ", ".join(refTags) + # END Class GuiOutlineDetails diff --git a/nw/guimain.py b/nw/guimain.py index 639a015a..ebbcc984 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -139,6 +139,7 @@ class GuiMain(QMainWindow): self.splitOutline = QSplitter(Qt.Vertical) self.splitOutline.addWidget(self.projView) self.splitOutline.addWidget(self.projMeta) + self.splitOutline.setSizes(self.mainConf.outlnPanePos) self.tabWidget = QTabWidget() self.tabWidget.setTabPosition(QTabWidget.East) @@ -849,6 +850,7 @@ class GuiMain(QMainWindow): if not self.isZenMode: self.mainConf.setMainPanePos(self.splitMain.sizes()) self.mainConf.setDocPanePos(self.splitView.sizes()) + self.mainConf.setOutlinePanePos(self.splitOutline.sizes()) self.mainConf.saveConfig() self.reportConfErr()