# -*- coding: utf-8 -*- """novelWriter GUI Document Viewer novelWriter – GUI Document Viewer =================================== Class holding the document html viewer File History: Created: 2019-05-10 [0.0.1] GuiDocViewer Created: 2019-10-31 [0.3.2] GuiDocViewDetails Created: 2020-04-25 [0.4.5] GuiDocViewHeader Created: 2020-06-09 [0.8.0] GuiDocViewFooter Created: 2020-09-08 [1.0b1] GuiDocViewHistory This file is a part of novelWriter Copyright 2018–2020, Veronica Berglyd Olsen This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import nw import logging from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot from PyQt5.QtGui import ( QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon ) from PyQt5.QtWidgets import ( QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton, QAction, QMenu ) from nw.core import ToHtml from nw.constants import nwAlert, nwItemType, nwDocAction, nwUnicode logger = logging.getLogger(__name__) class GuiDocViewer(QTextBrowser): def __init__(self, theParent): QTextBrowser.__init__(self, theParent) logger.debug("Initialising GuiDocViewer ...") # Class Variables self.mainConf = nw.CONFIG self.theParent = theParent self.theTheme = theParent.theTheme self.theProject = theParent.theProject self.theHandle = None self.qDocument = self.document() self.setMinimumWidth(self.mainConf.pxInt(300)) self.setAutoFillBackground(True) self.setOpenExternalLinks(False) self.setFocusPolicy(Qt.StrongFocus) self.initViewer() # Document Header and Footer self.docHeader = GuiDocViewHeader(self) self.docFooter = GuiDocViewFooter(self) self.docHistory = GuiDocViewHistory(self) self.stickyRef = False theOpt = QTextOption() if self.mainConf.doJustify: theOpt.setAlignment(Qt.AlignJustify) self.qDocument.setDefaultTextOption(theOpt) # Signals self.anchorClicked.connect(self._linkClicked) # Context Menu self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self._openContextMenu) logger.debug("GuiDocViewer initialisation complete") return def clearViewer(self): """Clear the content of the document and reset key variables. """ self.clear() self.setSearchPaths([""]) self.theHandle = None self.docHeader.setTitleFromHandle(self.theHandle) return True def initViewer(self): """Set editor settings from main config. """ self._makeStyleSheet() # Set Font theFont = QFont() if self.mainConf.textFont is None: # If none is defined, set the default back to config self.mainConf.textFont = self.qDocument.defaultFont().family() theFont.setFamily(self.mainConf.textFont) theFont.setPointSize(self.mainConf.textSize) self.setFont(theFont) docPalette = self.palette() docPalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack)) docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) self.setPalette(docPalette) self.qDocument.setDocumentMargin(0) theOpt = QTextOption() if self.mainConf.doJustify: theOpt.setAlignment(Qt.AlignJustify) self.qDocument.setDefaultTextOption(theOpt) # If we have a document open, we should reload it in case the font changed if self.theHandle is not None: tHandle = self.theHandle self.clearViewer() self.loadText(tHandle) return True def loadText(self, tHandle, updateHistory=True): """Load text into the viewer from an item handle. """ tItem = self.theProject.projTree[tHandle] if tItem is None: logger.warning("Item not found") return False if tItem.itemType != nwItemType.FILE: return False logger.debug("Generating preview for item %s" % tHandle) sPos = self.verticalScrollBar().value() aDoc = ToHtml(self.theProject, self.theParent) aDoc.setPreview(True, self.mainConf.viewComments, self.mainConf.viewSynopsis) aDoc.setLinkHeaders(True) # Be extra careful here to prevent crashes when first opening a # project as a crash here leaves no way of recovering. # See issue #298 try: aDoc.setText(tHandle) aDoc.doAutoReplace() aDoc.tokenizeText() aDoc.doConvert() aDoc.doPostProcessing() except Exception as e: logger.error("Failed to generate preview for document with handle '%s'" % tHandle) logger.error(str(e)) self.setText("An error occurred while generating the preview.") return False # Refresh the tab stops if self.mainConf.verQtValue >= 51000: self.setTabStopDistance(self.mainConf.getTabWidth()) else: self.setTabStopWidth(self.mainConf.getTabWidth()) # Must be before setHtml if updateHistory: self.docHistory.append(tHandle) self.setHtml(aDoc.theResult.replace("\t", "!!tab!!")) self.setDocumentTitle(tHandle) # Loop through the text and put back in the tabs. Tabs are removed by # the setHtml function, so the ToHtml class puts in a placeholder. while self.find("!!tab!!"): theCursor = self.textCursor() theCursor.insertText("\t") if self.theHandle == tHandle: self.verticalScrollBar().setValue(sPos) self.theHandle = tHandle self.theProject.setLastViewed(tHandle) self.docHeader.setTitleFromHandle(self.theHandle) self.updateDocMargins() # Make sure the main GUI knows we changed the content self.theParent.viewMeta.refreshReferences(tHandle) # Since we change the content while it may still be rendering, we mark # the document dirty again to make sure it's re-rendered properly. self.qDocument.markContentsDirty(0, self.qDocument.characterCount()) return True def reloadText(self): """Reload the text in the current document. """ self.loadText(self.theHandle, updateHistory=False) return def loadFromTag(self, theTag): """Load text in the document from a reference given by a meta tag rather than a known handle. This function depends on the index being up to date. """ logger.debug("Loading document from tag '%s'" % theTag) tHandle, _, sTitle = self.theParent.theIndex.getTagSource(theTag) if tHandle is None: self.theParent.makeAlert(( "Could not find the reference for tag '%s'. It either doesn't " "exist, or the index is out of date. The index can be updated " "from the Tools menu, or by pressing F9." ) % theTag, nwAlert.ERROR) return False else: # Let the parent handle the opening as it also ensures that # the doc view panel is visible in case this request comes # from outside this class. logger.verbose("Tag points to %s#%s" % (tHandle, sTitle)) self.theParent.viewDocument(tHandle, "#%s" % sTitle) return True def docAction(self, theAction): """Wrapper function for various document actions on the current document. """ logger.verbose("Requesting action: %s" % theAction.name) if self.theHandle is None: logger.error("No document open") return False if theAction == nwDocAction.CUT: self.copy() elif theAction == nwDocAction.COPY: self.copy() elif theAction == nwDocAction.SEL_ALL: self._makeSelection(QTextCursor.Document) elif theAction == nwDocAction.SEL_PARA: self._makeSelection(QTextCursor.BlockUnderCursor) else: logger.debug("Unknown or unsupported document action %s" % str(theAction)) return False return True def navigateTo(self, tAnchor): """Go to a specific #link in the document. """ if not isinstance(tAnchor, str): return False if tAnchor.startswith("#"): logger.verbose("Moving to anchor %s" % tAnchor) self.setSource(QUrl(tAnchor)) return True def navBackward(self): """Navigate backwards in the document view history. """ self.docHistory.backward() return def navForward(self): """Navigate forwards in the document view history. """ self.docHistory.forward() return def clearNavHistory(self): """Clear the navigation history. """ self.docHistory.clear() return def updateDocMargins(self): """Automatically adjust the margins so the text is centred if Config.textFixedW is enabled or we're in Focus Mode. Otherwise, just ensure the margins are set correctly. """ vBar = self.verticalScrollBar() sW = vBar.width() if vBar.isVisible() else 0 hBar = self.horizontalScrollBar() sH = hBar.height() if hBar.isVisible() else 0 cM = self.mainConf.getTextMargin() tB = self.frameWidth() tW = self.width() - 2*tB - sW tH = self.docHeader.height() fH = self.docFooter.height() fY = self.height() - fH - tB - sH self.docHeader.setGeometry(tB, tB, tW, tH) self.docFooter.setGeometry(tB, fY, tW, fH) self.setViewportMargins(cM, max(cM, tH), cM, max(cM, fH)) return def updateDocInfo(self, tHandle): """Called when an item label is changed to check if the document title bar needs updating, """ if tHandle == self.theHandle: self.docHeader.setTitleFromHandle(self.theHandle) self.updateDocMargins() return ## # Setters ## def setCursorPosition(self, thePosition): """Move the cursor to a given position in the document. """ if not isinstance(thePosition, int): return False if thePosition >= 0: theCursor = self.textCursor() theCursor.setPosition(thePosition) self.setTextCursor(theCursor) return True def setCursorLine(self, theLine): """Move the cursor to a given line in the document. """ if not isinstance(theLine, int): return False if theLine >= 0: theBlock = self.qDocument.findBlockByLineNumber(theLine) if theBlock: self.setCursorPosition(theBlock.position()) logger.verbose("Cursor moved to line %d" % theLine) return True def setScrollPosition(self, thePos): """Set the scrollbar position. """ vBar = self.verticalScrollBar() if vBar.isVisible(): vBar.setValue(thePos) return ## # Getters ## def getScrollPosition(self): """Get the scrollbar position. Returns 0 if no scrollbar. """ vBar = self.verticalScrollBar() if vBar.isVisible(): return vBar.value() return 0 ## # Slots ## @pyqtSlot("QUrl") 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 @pyqtSlot("QPoint") def _openContextMenu(self, thePos): """Triggered by right click to open the context menu. """ userCursor = self.textCursor() userSelection = userCursor.hasSelection() mnuContext = QMenu() # Cut, Copy and Paste # =================== if userSelection: mnuCopy = QAction("Copy", mnuContext) mnuCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY)) mnuContext.addAction(mnuCopy) mnuContext.addSeparator() # Selections # ========== mnuSelAll = QAction("Select All", mnuContext) mnuSelAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL)) mnuContext.addAction(mnuSelAll) mnuSelWord = QAction("Select Word", mnuContext) mnuSelWord.triggered.connect( lambda: self._makePosSelection(QTextCursor.WordUnderCursor, thePos) ) mnuContext.addAction(mnuSelWord) mnuSelPara = QAction("Select Paragraph", mnuContext) mnuSelPara.triggered.connect( lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, thePos) ) mnuContext.addAction(mnuSelPara) # Open the context menu mnuContext.exec_(self.viewport().mapToGlobal(thePos)) return ## # Events ## def resizeEvent(self, theEvent): """Make sure the document title is the same width as the window. """ QTextBrowser.resizeEvent(self, theEvent) self.updateDocMargins() return def mouseReleaseEvent(self, theEvent): """Capture mouse click events on the document. """ if theEvent.button() == Qt.BackButton: self.navBackward() elif theEvent.button() == Qt.ForwardButton: self.navForward() else: QTextBrowser.mouseReleaseEvent(self, theEvent) return ## # Internal Functions ## def _makeSelection(self, selMode): """Wrapper function to select text based on a selection mode. """ theCursor = self.textCursor() theCursor.clearSelection() theCursor.select(selMode) if selMode == QTextCursor.BlockUnderCursor: # This selection mode also selects the preceding oaragraph # separator, which we want to avoid. posS = theCursor.selectionStart() posE = theCursor.selectionEnd() selTxt = theCursor.selectedText() if selTxt.startswith(nwUnicode.U_PSEP): theCursor.setPosition(posS+1, QTextCursor.MoveAnchor) theCursor.setPosition(posE, QTextCursor.KeepAnchor) self.setTextCursor(theCursor) return def _makePosSelection(self, selMode, thePos): """Wrapper function to select text based on selection mode, but first move cursor to given position. """ theCursor = self.cursorForPosition(thePos) self.setTextCursor(theCursor) self._makeSelection(selMode) return def _makeStyleSheet(self): """Generate an appropriate style sheet for the document viewer, based on the current syntax highlighter theme, """ styleSheet = ( "body {{" " color: rgb({tColR},{tColG},{tColB});" " font-size: {textSize:.1f}pt;" "}}\n" "h1, h2, h3, h4 {{" " color: rgb({hColR},{hColG},{hColB});" "}}\n" "a {{" " color: rgb({aColR},{aColG},{aColB});" "}}\n" "mark {{" " color: rgb({eColR},{eColG},{eColB});" "}}\n" "table {{" " margin: 10px 0px;" "}}\n" "td {{" " padding: 0px 4px;" "}}\n" ".tags {{" " color: rgb({kColR},{kColG},{kColB});" " font-wright: bold;" "}}\n" ".comment {{" " color: rgb({cColR},{cColG},{cColB});" "}}\n" ".synopsis {{" " color: rgb({mColR},{mColG},{mColB});" " font-wright: bold;" "}}\n" ).format( textSize = self.mainConf.textSize, tColR = self.theTheme.colText[0], tColG = self.theTheme.colText[1], tColB = self.theTheme.colText[2], hColR = self.theTheme.colHead[0], hColG = self.theTheme.colHead[1], hColB = self.theTheme.colHead[2], cColR = self.theTheme.colComm[0], cColG = self.theTheme.colComm[1], cColB = self.theTheme.colComm[2], eColR = self.theTheme.colEmph[0], eColG = self.theTheme.colEmph[1], eColB = self.theTheme.colEmph[2], aColR = self.theTheme.colVal[0], aColG = self.theTheme.colVal[1], aColB = self.theTheme.colVal[2], kColR = self.theTheme.colKey[0], kColG = self.theTheme.colKey[1], kColB = self.theTheme.colKey[2], mColR = self.theTheme.colMod[0], mColG = self.theTheme.colMod[1], mColB = self.theTheme.colMod[2], ) self.qDocument.setDefaultStyleSheet(styleSheet) return True # END Class GuiDocViewer class GuiDocViewHistory(): def __init__(self, docViewer): self.docViewer = docViewer self._navHistory = [] self._posHistory = [] self._currPos = -1 self._prevPos = -1 return def clear(self): """Clear the view history. """ logger.verbose("View history cleared") self._navHistory = [] self._posHistory = [] self._currPos = -1 self._prevPos = -1 return def append(self, tHandle): """Append a document handle and its scroll bar position to the history, but only if the document is different than the current active entry. Any further entries are truncated. """ if self._currPos >= 0 and self._currPos < len(self._navHistory): if tHandle == self._navHistory[self._currPos]: logger.verbose("Not updating view hsitory") return False self._truncateHistory(self._currPos) self._navHistory.append(tHandle) self._posHistory.append(0) self._prevPos = self._currPos self._currPos = len(self._navHistory) - 1 self._updateScrollBar() self._updateNavButtons() self._dumpHistory() logger.verbose("Added %s to view history" % tHandle) return True def forward(self): """Navigate to the next entry in the view history. """ newPos = self._currPos + 1 if newPos < len(self._navHistory): logger.verbose("Move forward in view history") self._prevPos = self._currPos self._updateScrollBar() self.docViewer.loadText(self._navHistory[newPos], updateHistory=False) self.docViewer.setScrollPosition(self._posHistory[newPos]) self._currPos = newPos self._updateNavButtons() self._dumpHistory() return def backward(self): """Navigate to the previous entry in the view history. """ newPos = self._currPos - 1 if newPos >= 0: logger.verbose("Move backward in view history") self._prevPos = self._currPos self._updateScrollBar() self.docViewer.loadText(self._navHistory[newPos], updateHistory=False) self.docViewer.setScrollPosition(self._posHistory[newPos]) self._currPos = newPos self._updateNavButtons() self._dumpHistory() return ## # Internal Functions ## def _updateScrollBar(self): """Update the scrollbar position of the previous entry. """ if self._prevPos >= 0 and self._prevPos < len(self._posHistory): self._posHistory[self._prevPos] = self.docViewer.getScrollPosition() return def _updateNavButtons(self): """Update the navigation buttons in the document header. """ self.docViewer.docHeader.updateNavButtons(0, len(self._navHistory) - 1, self._currPos) return def _truncateHistory(self, atPos): """Truncate the navigation history to the given position. Also enforces a maximum length of the navigation history to 20. """ nSkip = 1 if atPos > 19 else 0 self._navHistory = self._navHistory[nSkip:atPos + 1] self._posHistory = self._posHistory[nSkip:atPos + 1] self._currPos -= nSkip self._prevPos -= nSkip return def _dumpHistory(self): """Debug function to dump history to the logger. Since it is a for loop, it is skipped entirely if log level isn't VERBOSE. """ if logger.getEffectiveLevel() < logging.DEBUG: for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory)): logger.verbose( "History %02d: %s %13s [x:%d]" % ( i + 1, ">" if i == self._currPos else " ", h, p ) ) return # END Class GuiDocViewHistory # =============================================================================================== # # The Embedded Document Header # Only used by DocViewer, and is at a fixed position in the QTextBrowser's viewport # =============================================================================================== # class GuiDocViewHeader(QWidget): def __init__(self, docViewer): QWidget.__init__(self, docViewer) logger.debug("Initialising GuiDocViewHeader ...") self.mainConf = nw.CONFIG self.docViewer = docViewer self.theParent = docViewer.theParent self.theProject = docViewer.theProject self.theTheme = docViewer.theTheme self.theHandle = None # Make a QPalette that matches the Syntax Theme self.thePalette = QPalette() self.thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) self.thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) fPx = int(0.9*self.theTheme.fontPixelSize) hSp = self.mainConf.pxInt(6) # Main Widget Settings self.setAutoFillBackground(True) self.setPalette(self.thePalette) # Title Label self.theTitle = QLabel() self.theTitle.setText("") self.theTitle.setIndent(0) self.theTitle.setMargin(0) self.theTitle.setContentsMargins(0, 0, 0, 0) self.theTitle.setAutoFillBackground(True) self.theTitle.setAlignment(Qt.AlignHCenter | Qt.AlignTop) self.theTitle.setFixedHeight(fPx) self.theTitle.setPalette(self.thePalette) lblFont = self.theTitle.font() lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) self.theTitle.setFont(lblFont) buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" ).format(*self.theTheme.colText) # Buttons self.backButton = QToolButton(self) self.backButton.setIcon(self.theTheme.getIcon("backward")) self.backButton.setContentsMargins(0, 0, 0, 0) self.backButton.setIconSize(QSize(fPx, fPx)) self.backButton.setFixedSize(fPx, fPx) self.backButton.setStyleSheet(buttonStyle) self.backButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.backButton.setVisible(False) self.backButton.setToolTip("Go backward") self.backButton.clicked.connect(self.docViewer.navBackward) self.forwardButton = QToolButton(self) self.forwardButton.setIcon(self.theTheme.getIcon("forward")) self.forwardButton.setContentsMargins(0, 0, 0, 0) self.forwardButton.setIconSize(QSize(fPx, fPx)) self.forwardButton.setFixedSize(fPx, fPx) self.forwardButton.setStyleSheet(buttonStyle) self.forwardButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.forwardButton.setVisible(False) self.forwardButton.setToolTip("Go forward") self.forwardButton.clicked.connect(self.docViewer.navForward) self.refreshButton = QToolButton(self) self.refreshButton.setIcon(self.theTheme.getIcon("refresh")) self.refreshButton.setContentsMargins(0, 0, 0, 0) self.refreshButton.setIconSize(QSize(fPx, fPx)) self.refreshButton.setFixedSize(fPx, fPx) self.refreshButton.setStyleSheet(buttonStyle) self.refreshButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.refreshButton.setVisible(False) self.refreshButton.setToolTip("Reload the document") self.refreshButton.clicked.connect(self._refreshDocument) self.closeButton = QToolButton(self) self.closeButton.setIcon(self.theTheme.getIcon("close")) self.closeButton.setContentsMargins(0, 0, 0, 0) self.closeButton.setIconSize(QSize(fPx, fPx)) self.closeButton.setFixedSize(fPx, fPx) self.closeButton.setStyleSheet(buttonStyle) self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.closeButton.setVisible(False) self.closeButton.setToolTip("Close the document") self.closeButton.clicked.connect(self._closeDocument) # Assemble Layout self.outerBox = QHBoxLayout() self.outerBox.setSpacing(hSp) self.outerBox.addWidget(self.backButton, 0) self.outerBox.addWidget(self.forwardButton, 0) self.outerBox.addWidget(self.theTitle, 1) self.outerBox.addWidget(self.refreshButton, 0) self.outerBox.addWidget(self.closeButton, 0) self.setLayout(self.outerBox) logger.debug("GuiDocViewHeader initialisation complete") return ## # Setters ## def setTitleFromHandle(self, tHandle): """Sets the document title from the handle, or alternatively, set the whole document path. """ self.theHandle = tHandle if tHandle is None: self.theTitle.setText("") self.backButton.setVisible(False) self.forwardButton.setVisible(False) self.closeButton.setVisible(False) self.refreshButton.setVisible(False) return True if self.mainConf.showFullPath: tTitle = [] tTree = self.theProject.projTree.getItemPath(tHandle) for aHandle in reversed(tTree): nwItem = self.theProject.projTree[aHandle] if nwItem is not None: tTitle.append(nwItem.itemName) sSep = " %s " % nwUnicode.U_RSAQUO self.theTitle.setText(sSep.join(tTitle)) else: nwItem = self.theProject.projTree[tHandle] if nwItem is None: return False self.theTitle.setText(nwItem.itemName) self.backButton.setVisible(True) self.forwardButton.setVisible(True) self.closeButton.setVisible(True) self.refreshButton.setVisible(True) return True def updateNavButtons(self, firstIdx, lastIdx, currIdx): """Enable and disable nav buttons based on index in history. """ self.backButton.setEnabled(currIdx > firstIdx) self.forwardButton.setEnabled(currIdx < lastIdx) return ## # Slots ## def _closeDocument(self): """Trigger the close editor/viewer on the main window. """ self.theParent.closeDocViewer() return def _refreshDocument(self): """Reload the content of the document. """ if self.docViewer.theHandle == self.theParent.docEditor.theHandle: self.theParent.saveDocument() self.docViewer.reloadText() return ## # Events ## def mousePressEvent(self, theEvent): """Capture a click on the title and ensure that the item is selected in the project tree. """ self.theParent.treeView.setSelectedHandle(self.theHandle, doScroll=True) return # END Class GuiDocViewHeader # =============================================================================================== # # The Embedded Document Footer # Only used by DocViewer, and is at a fixed position in the QTextBrowser's viewport # =============================================================================================== # class GuiDocViewFooter(QWidget): def __init__(self, docViewer): QWidget.__init__(self, docViewer) logger.debug("Initialising GuiDocViewFooter ...") self.mainConf = nw.CONFIG self.docViewer = docViewer self.theParent = docViewer.theParent self.theTheme = docViewer.theTheme self.viewMeta = docViewer.theParent.viewMeta self.theHandle = None # Make a QPalette that matches the Syntax Theme self.thePalette = QPalette() self.thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) self.thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) fPx = int(0.9*self.theTheme.fontPixelSize) bSp = self.mainConf.pxInt(2) hSp = self.mainConf.pxInt(8) # Icons stickyOn = self.theTheme.getPixmap("sticky-on", (fPx, fPx)) stickyOff = self.theTheme.getPixmap("sticky-off", (fPx, fPx)) stickyIcon = QIcon() stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On) stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off) bulletOn = self.theTheme.getPixmap("bullet-on", (fPx, fPx)) bulletOff = self.theTheme.getPixmap("bullet-off", (fPx, fPx)) bulletIcon = QIcon() bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On) bulletIcon.addPixmap(bulletOff, QIcon.Normal, QIcon.Off) # Main Widget Settings self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) self.setPalette(self.thePalette) buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" ).format(*self.theTheme.colText) # Show/Hide Details self.showHide = QToolButton(self) self.showHide.setToolButtonStyle(Qt.ToolButtonIconOnly) self.showHide.setStyleSheet(buttonStyle) self.showHide.setIcon(self.theTheme.getIcon("reference")) self.showHide.setIconSize(QSize(fPx, fPx)) self.showHide.setFixedSize(QSize(fPx, fPx)) self.showHide.clicked.connect(self._doShowHide) self.showHide.setToolTip("Show/hide the references panel") # Sticky Button self.stickyRefs = QToolButton(self) self.stickyRefs.setCheckable(True) self.stickyRefs.setToolButtonStyle(Qt.ToolButtonIconOnly) self.stickyRefs.setStyleSheet(buttonStyle) self.stickyRefs.setIcon(stickyIcon) self.stickyRefs.setIconSize(QSize(fPx, fPx)) self.stickyRefs.setFixedSize(QSize(fPx, fPx)) self.stickyRefs.toggled.connect(self._doToggleSticky) self.stickyRefs.setToolTip( "Activate to freeze the content of the references panel when changing document" ) # Show Comments self.showComments = QToolButton(self) self.showComments.setCheckable(True) self.showComments.setChecked(self.mainConf.viewComments) self.showComments.setToolButtonStyle(Qt.ToolButtonIconOnly) self.showComments.setStyleSheet(buttonStyle) self.showComments.setIcon(bulletIcon) self.showComments.setIconSize(QSize(fPx, fPx)) self.showComments.setFixedSize(QSize(fPx, fPx)) self.showComments.toggled.connect(self._doToggleComments) self.showComments.setToolTip("Show comments") # Show Synopsis self.showSynopsis = QToolButton(self) self.showSynopsis.setCheckable(True) self.showSynopsis.setChecked(self.mainConf.viewSynopsis) self.showSynopsis.setToolButtonStyle(Qt.ToolButtonIconOnly) self.showSynopsis.setStyleSheet(buttonStyle) self.showSynopsis.setIcon(bulletIcon) self.showSynopsis.setIconSize(QSize(fPx, fPx)) self.showSynopsis.setFixedSize(QSize(fPx, fPx)) self.showSynopsis.toggled.connect(self._doToggleSynopsis) self.showSynopsis.setToolTip("Show synopsis comments") # Labels self.lblRefs = QLabel("References") self.lblRefs.setBuddy(self.showHide) self.lblRefs.setIndent(0) self.lblRefs.setMargin(0) self.lblRefs.setContentsMargins(0, 0, 0, 0) self.lblRefs.setAutoFillBackground(True) self.lblRefs.setFixedHeight(fPx) self.lblRefs.setAlignment(Qt.AlignLeft | Qt.AlignTop) self.lblRefs.setPalette(self.thePalette) self.lblSticky = QLabel("Sticky") self.lblSticky.setBuddy(self.stickyRefs) self.lblSticky.setIndent(0) self.lblSticky.setMargin(0) self.lblSticky.setContentsMargins(0, 0, 0, 0) self.lblSticky.setAutoFillBackground(True) self.lblSticky.setFixedHeight(fPx) self.lblSticky.setAlignment(Qt.AlignLeft | Qt.AlignTop) self.lblSticky.setPalette(self.thePalette) self.lblComments = QLabel("Comments") self.lblComments.setBuddy(self.showComments) self.lblComments.setIndent(0) self.lblComments.setMargin(0) self.lblComments.setContentsMargins(0, 0, 0, 0) self.lblComments.setAutoFillBackground(True) self.lblComments.setFixedHeight(fPx) self.lblComments.setAlignment(Qt.AlignLeft | Qt.AlignTop) self.lblComments.setPalette(self.thePalette) self.lblSynopsis = QLabel("Synopsis") self.lblSynopsis.setBuddy(self.showSynopsis) self.lblSynopsis.setIndent(0) self.lblSynopsis.setMargin(0) self.lblSynopsis.setContentsMargins(0, 0, 0, 0) self.lblSynopsis.setAutoFillBackground(True) self.lblSynopsis.setFixedHeight(fPx) self.lblSynopsis.setAlignment(Qt.AlignLeft | Qt.AlignTop) self.lblSynopsis.setPalette(self.thePalette) lblFont = self.font() lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) self.lblRefs.setFont(lblFont) self.lblSticky.setFont(lblFont) self.lblComments.setFont(lblFont) self.lblSynopsis.setFont(lblFont) # Assemble Layout self.outerBox = QHBoxLayout() self.outerBox.setSpacing(bSp) self.outerBox.addWidget(self.showHide, 0) self.outerBox.addWidget(self.lblRefs, 0) self.outerBox.addSpacing(hSp) self.outerBox.addWidget(self.stickyRefs, 0) self.outerBox.addWidget(self.lblSticky, 0) self.outerBox.addStretch(1) self.outerBox.addWidget(self.showComments, 0) self.outerBox.addWidget(self.lblComments, 0) self.outerBox.addSpacing(hSp) self.outerBox.addWidget(self.showSynopsis, 0) self.outerBox.addWidget(self.lblSynopsis, 0) self.setLayout(self.outerBox) logger.debug("GuiDocViewFooter initialisation complete") return ## # Slots ## def _doShowHide(self): """Toggle the expand/collapse of the panel. """ isVisible = self.viewMeta.isVisible() self.viewMeta.setVisible(not isVisible) return def _doToggleSticky(self, theState): """Toggle the sticky flag for the reference panel. """ logger.verbose("Reference sticky is %s" % str(theState)) self.docViewer.stickyRef = theState if not theState and self.docViewer.theHandle is not None: self.viewMeta.refreshReferences(self.docViewer.theHandle) return def _doToggleComments(self, theState): """Toggle the view comment button and reload the document. """ self.mainConf.setViewComments(theState) self.docViewer.reloadText() return def _doToggleSynopsis(self, theState): """Toggle the view synopsis button and reload the document. """ self.mainConf.setViewSynopsis(theState) self.docViewer.reloadText() return # END Class GuiDocViewFooter # =============================================================================================== # # The Document Back-Reference Panel # Placed in a separate QSplitter position in the main GUI window # =============================================================================================== # class GuiDocViewDetails(QScrollArea): def __init__(self, theParent): QScrollArea.__init__(self, theParent) logger.debug("Initialising GuiDocViewDetails ...") self.mainConf = nw.CONFIG self.theParent = theParent self.theProject = theParent.theProject self.theTheme = theParent.theTheme self.currHandle = None self.refList = QLabel("") self.refList.setWordWrap(True) self.refList.setAlignment(Qt.AlignTop) self.refList.setScaledContents(True) self.refList.linkActivated.connect(self._linkClicked) self.linkStyle = "style='color: rgb({0},{1},{2})'".format( *self.theTheme.colLink ) # Assemble self.outerWidget = QWidget() self.outerBox = QHBoxLayout() self.outerBox.addWidget(self.refList, 1) self.outerWidget.setLayout(self.outerBox) self.setWidget(self.outerWidget) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setWidgetResizable(True) self.setMinimumHeight(self.mainConf.pxInt(50)) logger.debug("GuiDocViewDetails initialisation complete") return def refreshReferences(self, tHandle): """Update the current list of document references from the project index. """ self.currHandle = tHandle if self.theParent.docViewer.stickyRef: return theRefs = self.theParent.theIndex.getBackReferenceList(tHandle) theList = [] for tHandle in theRefs: tItem = self.theProject.projTree[tHandle] if tItem is not None: theList.append("%s" % ( tHandle, theRefs[tHandle], self.linkStyle, tItem.itemName )) self.refList.setText(", ".join(theList)) return ## # Internal Functions ## def _linkClicked(self, theLink): """Capture the link-click and forward it to the document viewer class for handling. """ logger.verbose("Clicked link: '%s'" % theLink) if len(theLink) == 21: tHandle = theLink[:13] tAnchor = theLink[13:] self.theParent.viewDocument(tHandle, tAnchor) return # END Class GuiDocViewDetails