diff --git a/novelwriter/extensions/circularprogress.py b/novelwriter/extensions/circularprogress.py index af465aaf..dfaf586b 100644 --- a/novelwriter/extensions/circularprogress.py +++ b/novelwriter/extensions/circularprogress.py @@ -1,6 +1,6 @@ """ -novelWriter – Custom Widget: ProgressCircle -=========================================== +novelWriter – Custom Widget: Progress Circle +============================================ File History: Created: 2023-06-07 [2.1b1] @@ -24,39 +24,78 @@ along with this program. If not, see . from math import ceil -from PyQt5.QtGui import QPaintEvent, QPainter, QPen +from PyQt5.QtGui import QBrush, QColor, QPaintEvent, QPainter, QPen from PyQt5.QtCore import QRect, Qt from PyQt5.QtWidgets import QProgressBar, QSizePolicy, QWidget class NProgressCircle(QProgressBar): + """Extension: Circular Progress Widget - __slots__ = ("_mPx", "_bPen", "_cPen", "_cRect", "_tColor") + A custom widget that paints a circular progress indicator instead of + a straight bar. It is also possible to set custom text for iṫ. + """ + + __slots__ = ( + "_text", "_point", "_dRect", "_cRect", "_dPen", "_dBrush", + "_cPen", "_bPen", "_tColor" + ) def __init__(self, parent: QWidget, size: int, point: int): super().__init__(parent=parent) - self._mPx = point - self._bPen = QPen(self.palette().alternateBase(), point, Qt.SolidLine, Qt.RoundCap) - self._cPen = QPen(self.palette().highlight(), point, Qt.SolidLine, Qt.RoundCap) + self._text = None + self._point = point + self._dRect = QRect(0, 0, size, size) self._cRect = QRect(point, point, size - 2*point, size - 2*point) - self._tColor = self.palette().text().color() + self._dPen = QPen(Qt.transparent) + self._dBrush = QBrush(Qt.transparent) + self.setColours( + track=self.palette().alternateBase().color(), + bar=self.palette().highlight().color(), + text=self.palette().text().color() + ) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.setFixedWidth(size) self.setFixedHeight(size) return + def setColours( + self, back: QColor | None = None, track: QColor | None = None, + bar: QColor | None = None, text: QColor | None = None + ): + """Set the colours of the widget.""" + if isinstance(back, QColor): + self._dPen = QPen(back) + self._dBrush = QBrush(back) + if isinstance(bar, QColor): + self._cPen = QPen(QBrush(bar), self._point, Qt.SolidLine, Qt.RoundCap) + if isinstance(track, QColor): + self._bPen = QPen(QBrush(track), self._point, Qt.SolidLine, Qt.RoundCap) + if isinstance(text, QColor): + self._tColor = text + return + + def setCentreText(self, text: str | None): + """Replace the progress text with a custom string.""" + self._text = text + self.setValue(self.value()) # Triggers a redraw + return + def paintEvent(self, event: QPaintEvent): """Custom painter for the progress bar.""" progress = 100.0*self.value()/self.maximum() angle = ceil(16*3.6*progress) qPaint = QPainter(self) qPaint.setRenderHint(QPainter.Antialiasing, True) + qPaint.setPen(self._dPen) + qPaint.setBrush(self._dBrush) + qPaint.drawEllipse(self._dRect) qPaint.setPen(self._bPen) qPaint.drawArc(self._cRect, 0, 360*16) qPaint.setPen(self._cPen) qPaint.drawArc(self._cRect, 90*16, -angle) qPaint.setPen(self._tColor) - qPaint.drawText(self._cRect, Qt.AlignCenter, f"{progress:.1f} %") + qPaint.drawText(self._cRect, Qt.AlignCenter, self._text or f"{progress:.1f} %") return # END Class NProgressCircle diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index f10cd81d..2f5632db 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -144,10 +144,7 @@ class GuiManuscript(QDialog): # Process Controls # ================ - self.buildProgress = NProgressCircle(self, CONFIG.pxInt(140), CONFIG.pxInt(16)) - self.buildProgress.setValue(0) - - self.btnPreview = QPushButton(self.tr("Preview")) + self.btnPreview = QPushButton(self.tr("Build Preview")) self.btnPreview.clicked.connect(self._generatePreview) self.btnBuild = QPushButton(self.tr("Build")) @@ -163,17 +160,13 @@ class GuiManuscript(QDialog): self.btnClose = QPushButton(self.tr("Close")) self.btnClose.clicked.connect(self._doClose) - self.buildBox = QVBoxLayout() - self.buildBox.addStretch(1) - self.buildBox.addWidget(self.btnPreview, 0) - self.buildBox.addWidget(self.btnPrint, 0) + self.buildBox = QHBoxLayout() self.buildBox.addWidget(self.btnBuild, 0) + self.buildBox.addWidget(self.btnPrint, 0) self.buildBox.addWidget(self.btnClose, 0) - self.buildBox.addStretch(1) - self.buildBox.setSpacing(CONFIG.pxInt(2)) - self.processBox = QHBoxLayout() - self.processBox.addWidget(self.buildProgress) + self.processBox = QVBoxLayout() + self.processBox.addWidget(self.btnPreview) self.processBox.addLayout(self.buildBox) # Assemble GUI @@ -294,10 +287,11 @@ class GuiManuscript(QDialog): docBuild = NWBuildDocument(self.theProject, build) docBuild.queueAll() - self.buildProgress.setMaximum(len(docBuild)) - for step, status in docBuild.iterBuildHTML(None): - self.buildProgress.setValue(step + 1) + self.docPreview.beginNewBuild(len(docBuild)) + for step, _ in docBuild.iterBuildHTML(None): + self.docPreview.buildStep(step + 1) qApp.processEvents() + qApp.thread().msleep(5) buildObj = docBuild.lastBuild assert isinstance(buildObj, ToHtml) @@ -457,6 +451,8 @@ class _PreviewWidget(QTextBrowser): "Press the \"Build Preview\" button to generate ..." )) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) + # Document Age aPalette = self.palette() aPalette.setColor(QPalette.Background, aPalette.toolTipBase().color()) @@ -473,6 +469,17 @@ class _PreviewWidget(QTextBrowser): self.ageLabel.setAlignment(Qt.AlignCenter) self.ageLabel.setFixedHeight(int(2.1*self.mainTheme.fontPixelSize)) + # Progress + self.buildProgress = NProgressCircle(self, CONFIG.pxInt(160), CONFIG.pxInt(16)) + self.buildProgress.setVisible(False) + self.buildProgress.setMaximum(1) + self.buildProgress.setValue(0) + self.buildProgress.setColours( + back=QColor(255, 255, 255, 224), + track=QColor(196, 196, 196, 128), + text=QColor(0, 0, 0) + ) + self._updateDocMargins() self._updateBuildAge() @@ -516,11 +523,30 @@ class _PreviewWidget(QTextBrowser): # Methods ## + def beginNewBuild(self, length: int): + """Clear the document and show the progress bar.""" + self.buildProgress.setMaximum(length) + self.buildProgress.setValue(0) + self.buildProgress.setCentreText(None) + self.buildProgress.setVisible(True) + self.setPlaceholderText("") + self.clear() + return + + def buildStep(self, value: int): + """Update the progress bar value.""" + self.buildProgress.setValue(value) + qApp.processEvents() + return + def setContent(self, data: dict): """Set the content of the preview widget.""" sPos = self.verticalScrollBar().value() qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) + self.buildProgress.setCentreText(self.tr("Processing ...")) + qApp.processEvents() + styles = "\n".join(data.get("styles", [ "h1, h2 {color: rgb(66, 113, 174);}", "h3, h4 {color: rgb(50, 50, 50);}", @@ -544,9 +570,13 @@ class _PreviewWidget(QTextBrowser): self._updateBuildAge() # 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. + # the document as dirty again to make sure it's re-rendered properly. self.document().markContentsDirty(0, self.document().characterCount()) + + self.buildProgress.setCentreText(self.tr("Done")) qApp.restoreOverrideCursor() + qApp.processEvents() + QTimer.singleShot(300, self._hideProgress) return @@ -580,6 +610,12 @@ class _PreviewWidget(QTextBrowser): self.ageLabel.setText(text) return + @pyqtSlot() + def _hideProgress(self): + """Clean up the build progress bar.""" + self.buildProgress.setVisible(False) + return + ## # Internal Functions ## @@ -591,10 +627,13 @@ class _PreviewWidget(QTextBrowser): vBar = self.verticalScrollBar() sW = vBar.width() if vBar.isVisible() else 0 tB = self.frameWidth() - tW = self.width() - 2*tB - sW + vW = self.width() - 2*tB - sW + vH = self.height() - 2*tB tH = self.ageLabel.height() - self.ageLabel.setGeometry(tB, tB, tW, tH) + pS = self.buildProgress.width() + self.ageLabel.setGeometry(tB, tB, vW, tH) self.setViewportMargins(0, tH, 0, 0) + self.buildProgress.move((vW-pS)//2, (vH-pS)//2) return # END Class _PreviewWidget