diff options
| -rw-r--r-- | src/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/busyindicator.cpp | 65 | ||||
| -rw-r--r-- | src/busyindicator.h | 53 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 18 | ||||
| -rw-r--r-- | src/mainwindow.h | 4 | ||||
| -rw-r--r-- | tests/test_busyindicator.cpp | 136 |
6 files changed, 265 insertions, 12 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 197366d..b63ff3e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(qtmaildir_lib STATIC htmlbuilder.cpp cidschemehandler.cpp cardlayout.cpp + busyindicator.cpp marks.cpp carddelegate.cpp notmuchworker.cpp diff --git a/src/busyindicator.cpp b/src/busyindicator.cpp new file mode 100644 index 0000000..0ccc719 --- /dev/null +++ b/src/busyindicator.cpp @@ -0,0 +1,65 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "busyindicator.h" + +BusyIndicator::BusyIndicator(QWidget *parent) + : QProgressBar(parent) +{ + // Starts indeterminate and hidden, which is what both consumers want + // before anything is happening. A caller that wants a fraction calls + // setProgress() and never sees this range. + setRange(0, 0); + + // The percentage would be meaningless in the indeterminate mode and is + // redundant beside the phase text in the determinate one, where the popup + // says what is being waited for. + setTextVisible(false); + hide(); +} + +void BusyIndicator::setBusy(bool busy) +{ + setRange(0, 0); + setVisible(busy); +} + +void BusyIndicator::setProgress(int value, int total) +{ + if (total <= 0) { + // Not an empty determinate bar: setRange(0, 0) IS the indeterminate + // range, so falling through here would hand the caller an animating + // bar while it believed it had shown a fraction. + setBusy(true); + return; + } + + setRange(0, total); + setValue(qBound(0, value, total)); + show(); +} + +bool BusyIndicator::isBusy() const +{ + return isVisible(); +} + +bool BusyIndicator::isDeterminate() const +{ + return maximum() > minimum(); +} diff --git a/src/busyindicator.h b/src/busyindicator.h new file mode 100644 index 0000000..14ad6ee --- /dev/null +++ b/src/busyindicator.h @@ -0,0 +1,53 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include <QProgressBar> + +// A progress bar that carries both of the modes this application needs, so the +// two places that show one do not each configure a bare QProgressBar. The +// status bar's sync indicator is indeterminate, because mbsync reports no +// percentage; the composer's send popup drains a determinate bar through its +// undo countdown and then switches THE SAME widget to indeterminate when the +// command starts. That switch is why one class exists rather than two. +// +// Deliberately no label. MainWindow's status text is m_statusLabel, which is +// the window's and carries far more than this widget's state, and the send +// popup's phase text belongs to the popup. A label here would be a third +// owner of status text. +class BusyIndicator : public QProgressBar +{ + Q_OBJECT + +public: + explicit BusyIndicator(QWidget *parent = nullptr); + + // Indeterminate: an animating bar meaning "working, duration unknown". + // setRange(0, 0) is what Qt reads as indeterminate, and a bar left in that + // range shows no fraction however many times setValue() is called. + void setBusy(bool busy); + + // Determinate: a real fraction. Passing a total of 0 or less would be a + // silent request for the indeterminate range, so it is treated as busy + // instead of quietly drawing an empty bar forever. + void setProgress(int value, int total); + + bool isBusy() const; + bool isDeterminate() const; +}; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 58c82ca..dc416ca 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -36,7 +36,6 @@ #include <QMenuBar> #include <QMessageBox> #include <QPlainTextEdit> -#include <QProgressBar> #include <QPushButton> #include <QSettings> #include <QSplitter> @@ -54,6 +53,7 @@ #include "notmuchworker.h" #include "querycompleter.h" #include "carddelegate.h" +#include "busyindicator.h" #include "cardlayout.h" #include "searchterm.h" #include "tagchip.h" @@ -504,16 +504,14 @@ void MainWindow::buildUi() m_pendingLabel->hide(); statusBar()->addPermanentWidget(m_pendingLabel); - // Indeterminate: setRange(0, 0). A sync has no measurable progress, since - // mbsync reports no percentage and the script's output is unstructured, so - // a bar filling left to right would be inventing a fraction. This one - // animates to say "working, duration unknown". - m_syncProgress = new QProgressBar(this); + // Indeterminate, which BusyIndicator starts in: a sync has no measurable + // progress, since mbsync reports no percentage and the script's output is + // unstructured, so a bar filling left to right would be inventing a + // fraction. This one animates to say "working, duration unknown". The + // determinate half of the widget is the composer's, not this one's. + m_syncProgress = new BusyIndicator(this); m_syncProgress->setObjectName(QStringLiteral("syncProgress")); - m_syncProgress->setRange(0, 0); - m_syncProgress->setTextVisible(false); m_syncProgress->setMaximumWidth(120); - m_syncProgress->hide(); statusBar()->addPermanentWidget(m_syncProgress); // Query row. @@ -3677,7 +3675,7 @@ void MainWindow::updateSyncControls() // local run ending would re-enable it while cron still holds the lock. const bool busy = m_localSyncBusy || m_externalSyncBusy; - m_syncProgress->setVisible(busy); + m_syncProgress->setBusy(busy); // Disabled rather than left clickable: MailSync::start() already refuses a // second run and the script exits 75 when another holds the lock, but a diff --git a/src/mainwindow.h b/src/mainwindow.h index 5b1621f..8e483d2 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -52,11 +52,11 @@ class QPushButton; class QComboBox; class QPlainTextEdit; class QSplitter; -class QProgressBar; class QTimer; class QToolButton; class QVBoxLayout; +class BusyIndicator; class ThreadListModel; class MessageView; class MailSync; @@ -1074,7 +1074,7 @@ private: QLabel *m_pendingLabel = nullptr; /// Indeterminate, shown only while a sync runs. See setSyncBusy(). - QProgressBar *m_syncProgress = nullptr; + BusyIndicator *m_syncProgress = nullptr; /// The last counts the worker answered, one per placeholderLines() entry. /// Empty until the first reply, which renders the pane without its helper diff --git a/tests/test_busyindicator.cpp b/tests/test_busyindicator.cpp new file mode 100644 index 0000000..3f267c7 --- /dev/null +++ b/tests/test_busyindicator.cpp @@ -0,0 +1,136 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <QtTest> + +#include "busyindicator.h" + +class TestBusyIndicator : public QObject +{ + Q_OBJECT + +private slots: + void itStartsIndeterminateAndHidden(); + void itStaysHiddenInsideAShownParent(); + void setProgressShowsARealFraction(); + void aTotalOfZeroFallsBackToBusyRatherThanAnEmptyBar(); + void itSwitchesBackFromDeterminateToIndeterminate(); + void aValueOutsideTheRangeIsClamped(); +}; + +void TestBusyIndicator::itStartsIndeterminateAndHidden() +{ + BusyIndicator indicator; + + // setRange(0, 0) is what Qt reads as indeterminate, and it is the state + // the status bar's sync indicator lives in. + QCOMPARE(indicator.minimum(), 0); + QCOMPARE(indicator.maximum(), 0); + QVERIFY(!indicator.isDeterminate()); +} + +void TestBusyIndicator::itStaysHiddenInsideAShownParent() +{ + // The hidden-on-construction property needs a SHOWN PARENT to be + // observable at all, and both obvious probes measure nothing without one. + // Measured against a standalone Qt program: a parentless widget reports + // isVisible() false and isHidden() true whether or not hide() was ever + // called, so a mutation deleting the constructor's hide() survives both + // assertions. What differs is WA_WState_ExplicitShowHide, and the + // behaviour it produces appears only once a parent is shown, which is how + // MainWindow's status bar holds this widget. + QWidget parent; + BusyIndicator *indicator = new BusyIndicator(&parent); + parent.show(); + + QVERIFY2(!indicator->isBusy(), + "the indicator showed itself as soon as its parent appeared"); + + indicator->setBusy(true); + QVERIFY(indicator->isBusy()); +} + +void TestBusyIndicator::setProgressShowsARealFraction() +{ + BusyIndicator indicator; + indicator.setProgress(3, 10); + + QVERIFY2(indicator.isDeterminate(), + "a bar given a total stayed in the indeterminate range"); + QCOMPARE(indicator.maximum(), 10); + QCOMPARE(indicator.value(), 3); + + // Showing a fraction means being visible. A caller that set progress and + // got a hidden widget would have to remember a second call. + QVERIFY(indicator.isBusy()); +} + +void TestBusyIndicator::aTotalOfZeroFallsBackToBusyRatherThanAnEmptyBar() +{ + // The trap this guards: setRange(0, 0) IS the indeterminate range, so a + // total of zero passed straight through would hand the caller an + // animating bar while it believed it had drawn an empty fraction. Making + // it explicitly busy is the honest reading of "no total known". + BusyIndicator indicator; + indicator.setProgress(0, 0); + + QVERIFY(!indicator.isDeterminate()); + QVERIFY2(indicator.isBusy(), + "a total of zero left the indicator hidden and silent"); + + indicator.setProgress(5, -1); + QVERIFY(!indicator.isDeterminate()); + QVERIFY(indicator.isBusy()); +} + +void TestBusyIndicator::itSwitchesBackFromDeterminateToIndeterminate() +{ + // The send popup's whole reason for one widget rather than two: it drains + // a determinate bar through the undo countdown, then switches THE SAME + // widget to indeterminate when send_command starts and the duration stops + // being knowable. A setBusy() that left a stale range would keep drawing + // the countdown's last fraction. + BusyIndicator indicator; + indicator.setProgress(9, 10); + QVERIFY(indicator.isDeterminate()); + + indicator.setBusy(true); + QVERIFY2(!indicator.isDeterminate(), + "the bar kept the countdown's range after switching to busy"); + QCOMPARE(indicator.maximum(), 0); + QVERIFY(indicator.isBusy()); + + indicator.setBusy(false); + QVERIFY(!indicator.isBusy()); +} + +void TestBusyIndicator::aValueOutsideTheRangeIsClamped() +{ + // A countdown driven by a timer can overshoot its own total by a tick. + // Qt leaves an out-of-range value unset rather than clamping, which shows + // the PREVIOUS fraction, so the bar would appear to stall near the end. + BusyIndicator indicator; + indicator.setProgress(50, 10); + QCOMPARE(indicator.value(), 10); + + indicator.setProgress(-5, 10); + QCOMPARE(indicator.value(), 0); +} + +QTEST_MAIN(TestBusyIndicator) +#include "test_busyindicator.moc" |
