diff --git a/novelwriter/gui/components.py b/novelwriter/gui/components.py new file mode 100644 index 00000000..35234d40 --- /dev/null +++ b/novelwriter/gui/components.py @@ -0,0 +1,165 @@ +""" +novelWriter – GUI Components Module +=================================== +A module of various small GUI components + +File History: +Created: 2020-05-17 [0.5.1] StatusLED +Created: 2022-11-17 [2.0rc2] NovelSelector + +This file is a part of novelWriter +Copyright 2018–2022, 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 logging + +from PyQt5.QtGui import QPainter +from PyQt5.QtCore import pyqtSignal, pyqtSlot +from PyQt5.QtWidgets import QAbstractButton, QComboBox + +from novelwriter.enum import nwItemClass +from novelwriter.constants import nwLabels + +logger = logging.getLogger(__name__) + + +class NovelSelector(QComboBox): + + novelSelectionChanged = pyqtSignal(str) + + def __init__(self, parent, project, theme): + super().__init__(parent=parent) + + self._project = project + self._theme = theme + self._blockSignal = False + self.currentIndexChanged.connect(self._indexChanged) + + return + + @property + def handle(self): + return self.currentData() + + def setHandle(self, tHandle, blockSignal=True): + """Set the currently selected handle. + """ + self._blockSignal = blockSignal + if tHandle is None: + index = self.count() - 1 + else: + index = self.findData(tHandle) + if index >= 0: + self.setCurrentIndex(index) + self._blockSignal = False + return + + def updateList(self, includeAll=False, prefix=None): + """Rebuild the list of novel items. + """ + self._blockSignal = True + self.clear() + + icon = self._theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) + handle = self.currentData() + for tHandle, nwItem in self._project.tree.iterRoots(nwItemClass.NOVEL): + if prefix: + name = prefix.format(nwItem.itemName) + self.addItem(name, tHandle) + else: + name = nwItem.itemName + self.addItem(icon, nwItem.itemName, tHandle) + + if includeAll: + self.insertSeparator(self.count()) + if prefix: + self.addItem(prefix.format(self.tr("All Novel Folders")), "") + else: + self.addItem(icon, self.tr("All Novel Folders"), "") + + self.setHandle(handle) + self.setEnabled(self.count() > 1) + self._blockSignal = False + + return + + ## + # Private Slots + ## + + @pyqtSlot(int) + def _indexChanged(self, index): + if not self._blockSignal: + self.novelSelectionChanged.emit(self.currentData()) + return + +# END Class NovelSelector + + +class StatusLED(QAbstractButton): + + S_NONE = 0 + S_BAD = 1 + S_GOOD = 2 + + def __init__(self, colNone, colGood, colBad, sW, sH, parent=None): + super().__init__(parent=parent) + + self._colNone = colNone + self._colGood = colGood + self._colBad = colBad + self._theCol = colNone + + self.setFixedWidth(sW) + self.setFixedHeight(sH) + + return + + ## + # Setters + ## + + def setState(self, theState): + """Set the colour state. + """ + if theState == self.S_GOOD: + self._theCol = self._colGood + elif theState == self.S_BAD: + self._theCol = self._colBad + else: + self._theCol = self._colNone + + self.update() + + return + + ## + # Events + ## + + def paintEvent(self, _): + """Drawing the LED. + """ + qPalette = self.palette() + qPaint = QPainter(self) + qPaint.setRenderHint(QPainter.Antialiasing, True) + qPaint.setPen(qPalette.dark().color()) + qPaint.setBrush(self._theCol) + qPaint.setOpacity(1.0) + qPaint.drawEllipse(1, 1, self.width() - 2, self.height() - 2) + return + +# END Class StatusLED diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index cc809e17..d7b38a21 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -31,16 +31,17 @@ import novelwriter from enum import Enum from time import time -from PyQt5.QtGui import QPalette +from PyQt5.QtGui import QFont, QPalette from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal from PyQt5.QtWidgets import ( - QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel, - QMenu, QSizePolicy, QToolButton, QToolTip, QTreeWidget, QTreeWidgetItem, + QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QMenu, + QSizePolicy, QToolButton, QToolTip, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter.enum import nwDocMode, nwItemClass, nwOutline from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst +from novelwriter.gui.components import NovelSelector logger = logging.getLogger(__name__) @@ -199,10 +200,15 @@ class GuiNovelToolBar(QWidget): self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) - # Widget Label - self.viewLabel = QLabel("%s" % self.tr("Novel Outline")) - self.viewLabel.setContentsMargins(0, 0, 0, 0) - self.viewLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + # Novel Selector + selFont = self.font() + selFont.setWeight(QFont.Bold) + self.novelPrefix = self.tr("Outline of {0}") + self.novelValue = NovelSelector(self, self.theProject, self.mainTheme) + self.novelValue.setFont(selFont) + self.novelValue.setMinimumWidth(self.mainConf.pxInt(150)) + self.novelValue.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot) # Refresh Button self.tbRefresh = QToolButton(self) @@ -210,17 +216,6 @@ class GuiNovelToolBar(QWidget): self.tbRefresh.setIconSize(QSize(iPx, iPx)) self.tbRefresh.clicked.connect(self._refreshNovelTree) - # Novel Root Menu - self.mRoot = QMenu() - self.gRoot = QActionGroup(self.mRoot) - self.aRoot = {} - - self.tbRoot = QToolButton(self) - self.tbRoot.setToolTip(self.tr("Novel Root")) - self.tbRoot.setIconSize(QSize(iPx, iPx)) - self.tbRoot.setMenu(self.mRoot) - self.tbRoot.setPopupMode(QToolButton.InstantPopup) - # More Options Menu self.mMore = QMenu() @@ -240,9 +235,8 @@ class GuiNovelToolBar(QWidget): # Assemble self.outerBox = QHBoxLayout() - self.outerBox.addWidget(self.viewLabel) + self.outerBox.addWidget(self.novelValue) self.outerBox.addWidget(self.tbRefresh) - self.outerBox.addWidget(self.tbRoot) self.outerBox.addWidget(self.tbMore) self.outerBox.setContentsMargins(mPx, mPx, 0, mPx) self.outerBox.setSpacing(0) @@ -264,7 +258,6 @@ class GuiNovelToolBar(QWidget): """ # Icons self.tbRefresh.setIcon(self.mainTheme.getIcon("refresh")) - self.tbRoot.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])) self.tbMore.setIcon(self.mainTheme.getIcon("menu")) qPalette = self.palette() @@ -279,39 +272,42 @@ class GuiNovelToolBar(QWidget): ).format(self.mainConf.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue()) self.tbRefresh.setStyleSheet(buttonStyle) - self.tbRoot.setStyleSheet(buttonStyle) self.tbMore.setStyleSheet(buttonStyle) + self.novelValue.setStyleSheet( + "QComboBox {border-style: none; padding-left: 0;} " + "QComboBox::drop-down {border-style: none}" + ) + self.novelValue.updateList(prefix=self.novelPrefix) + if self.novelValue.count() > 1: + self.novelValue.setToolTip(self.tr("Click to change root folder")) + else: + self.novelValue.setToolTip("") + return def clearContent(self): """Run clearing project tasks. """ - self.mRoot.clear() - self.aRoot = {} + self.novelValue.clear() + self.novelValue.setToolTip("") return def buildNovelRootMenu(self): """Build the novel root menu. """ - self.mRoot.clear() - self.aRoot = {} - for n, (tHandle, nwItem) in enumerate(self.theProject.tree.iterRoots(nwItemClass.NOVEL)): - aRoot = self.mRoot.addAction(nwItem.itemName) - aRoot.setData(tHandle) - aRoot.setCheckable(True) - aRoot.triggered.connect(lambda n, tHandle=tHandle: self.setCurrentRoot(tHandle)) - self.gRoot.addAction(aRoot) - self.aRoot[tHandle] = aRoot - + self.novelValue.updateList(prefix=self.novelPrefix) + if self.novelValue.count() > 1: + self.novelValue.setToolTip(self.tr("Click to change root folder")) + else: + self.novelValue.setToolTip("") return def setCurrentRoot(self, rootHandle): """Set the current active root handle. """ - if rootHandle in self.aRoot: - self.aRoot[rootHandle].setChecked(True) - self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) + self.novelValue.setHandle(rootHandle) + self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) return def setLastColType(self, colType, doRefresh=True): diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index ef898237..9cfd3d86 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -37,9 +37,9 @@ from PyQt5.QtCore import ( Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP ) from PyQt5.QtWidgets import ( - QAbstractItemView, QAction, QComboBox, QFrame, QGridLayout, QGroupBox, - QHBoxLayout, QLabel, QMenu, QScrollArea, QSizePolicy, QSplitter, QToolBar, - QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget + QAbstractItemView, QAction, QFrame, QGridLayout, QGroupBox, QHBoxLayout, + QLabel, QMenu, QScrollArea, QSizePolicy, QSplitter, QToolBar, QToolButton, + QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter.enum import ( @@ -47,6 +47,7 @@ from novelwriter.enum import ( ) from novelwriter.common import checkInt from novelwriter.constants import nwHeaders, trConst, nwKeyWords, nwLabels +from novelwriter.gui.components import NovelSelector logger = logging.getLogger(__name__) @@ -233,9 +234,9 @@ class GuiOutlineToolBar(QToolBar): self.novelLabel = QLabel(self.tr("Outline of")) self.novelLabel.setContentsMargins(0, 0, mPx, 0) - self.novelValue = QComboBox(self) + self.novelValue = NovelSelector(self, self.theProject, self.mainTheme) self.novelValue.setMinimumWidth(self.mainConf.pxInt(200)) - self.novelValue.currentIndexChanged.connect(self._novelValueChanged) + self.novelValue.novelSelectionChanged.connect(self._novelValueChanged) # Actions self.aRefresh = QAction(self.tr("Refresh"), self) @@ -274,31 +275,22 @@ class GuiOutlineToolBar(QToolBar): """ self.setStyleSheet("QToolBar {border: 0px;}") + self.novelValue.updateList(includeAll=True) self.aRefresh.setIcon(self.mainTheme.getIcon("refresh")) self.tbColumns.setIcon(self.mainTheme.getIcon("menu")) return def populateNovelList(self): - """Fill the novel combo box with a list of all novel folders. + """Relaod the content of the novel list. """ - self.novelValue.clear() - tIcon = self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) - for tHandle, nwItem in self.theProject.tree.iterRoots(nwItemClass.NOVEL): - self.novelValue.addItem(tIcon, nwItem.itemName, tHandle) - self.novelValue.insertSeparator(self.novelValue.count()) - self.novelValue.addItem(tIcon, self.tr("All Novel Folders"), "") + self.novelValue.updateList(includeAll=True) return def setCurrentRoot(self, rootHandle): """Set the current active root handle. """ - if rootHandle is None: - rootIdx = self.novelValue.count() - 1 - else: - rootIdx = self.novelValue.findData(rootHandle) - if rootIdx >= 0: - self.novelValue.setCurrentIndex(rootIdx) + self.novelValue.setHandle(rootHandle) return def setColumnHiddenState(self, hiddenState): @@ -311,19 +303,18 @@ class GuiOutlineToolBar(QToolBar): # Private Slots ## - @pyqtSlot(int) - def _novelValueChanged(self, index): + @pyqtSlot(str) + def _novelValueChanged(self, tHandle): """Emit a signal containing the handle of the selected item. """ - if index >= 0: - self.loadNovelRootRequest.emit(self.novelValue.currentData()) + self.loadNovelRootRequest.emit(tHandle) return @pyqtSlot() def _refreshRequested(self): """Emit a signal containing the handle of the selected item. """ - self.loadNovelRootRequest.emit(self.novelValue.currentData()) + self.loadNovelRootRequest.emit(self.novelValue.handle) return # END Class GuiOutlineToolBar @@ -679,6 +670,7 @@ class GuiOutlineTree(QTreeWidget): if they are hidden. This ensures that showing and hiding columns is fast and doesn't require a rebuild of the tree. """ + logger.debug("Rebuilding Outline tree") self.clear() if self._firstView: diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index 9dcb97af..a5824214 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -30,10 +30,11 @@ import novelwriter from time import time from PyQt5.QtCore import pyqtSlot, QLocale -from PyQt5.QtGui import QColor, QPainter -from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton +from PyQt5.QtGui import QColor +from PyQt5.QtWidgets import qApp, QStatusBar, QLabel from novelwriter.common import formatTime +from novelwriter.gui.components import StatusLED logger = logging.getLogger(__name__) @@ -246,59 +247,3 @@ class GuiMainStatus(QStatusBar): return # END Class GuiMainStatus - - -class StatusLED(QAbstractButton): - - S_NONE = 0 - S_BAD = 1 - S_GOOD = 2 - - def __init__(self, colNone, colGood, colBad, sW, sH, parent=None): - super().__init__(parent=parent) - - self._colNone = colNone - self._colGood = colGood - self._colBad = colBad - self._theCol = colNone - - self.setFixedWidth(sW) - self.setFixedHeight(sH) - - return - - ## - # Setters - ## - - def setState(self, theState): - """Set the colour state. - """ - if theState == self.S_GOOD: - self._theCol = self._colGood - elif theState == self.S_BAD: - self._theCol = self._colBad - else: - self._theCol = self._colNone - - self.update() - - return - - ## - # Events - ## - - def paintEvent(self, _): - """Drawing the LED. - """ - qPalette = self.palette() - qPaint = QPainter(self) - qPaint.setRenderHint(QPainter.Antialiasing, True) - qPaint.setPen(qPalette.dark().color()) - qPaint.setBrush(self._theCol) - qPaint.setOpacity(1.0) - qPaint.drawEllipse(1, 1, self.width() - 2, self.height() - 2) - return - -# END Class StatusLED