From 6179161e712d3136ef34c21898c1cd16c5a70bf4 Mon Sep 17 00:00:00 2001 From: Benjamin Munoz-Palma Date: Wed, 19 Aug 2026 00:15:43 -0400 Subject: [PATCH] Select the playback speed from a list of presets (#569) Replace the playback speed spin box with an editable combo box offering 25%, 50%, 75% and 100% presets, mirroring the existing zoom selector. Any other speed can still be entered by typing directly into the box, so the minimum was lowered from 50% to 25% to accommodate the new preset. PercentageValidator now takes the valid range as constructor arguments so that it can be shared between the zoom and speed selectors. Unlike the zoom box, the speed is applied when the value is committed rather than on every keystroke, so that playback doesn't audibly lurch through the intermediate values while a custom speed is being typed. --- source/widgets/playback/playbackwidget.cpp | 89 +++++++++++++++++++--- source/widgets/playback/playbackwidget.h | 4 + source/widgets/playback/playbackwidget.ui | 10 ++- 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/source/widgets/playback/playbackwidget.cpp b/source/widgets/playback/playbackwidget.cpp index 00b9c045..409ec6ae 100644 --- a/source/widgets/playback/playbackwidget.cpp +++ b/source/widgets/playback/playbackwidget.cpp @@ -29,6 +29,8 @@ #include #include +#include +#include static QString getShortcutHint(const QAction &action) { @@ -58,8 +60,11 @@ percentToString(double zoom) class PercentageValidator : public QValidator { public: - PercentageValidator(QObject *parent) - : QValidator(parent), myNumberValidator(1, 500, 2) + PercentageValidator(QObject *parent, int min_percent, int max_percent) + : QValidator(parent), + myNumberValidator(1, 500, 2), + myMinPercent(min_percent), + myMaxPercent(max_percent) { } @@ -73,14 +78,22 @@ class PercentageValidator : public QValidator QLocale locale; double percent = locale.toDouble(number); parent()->setProperty("acceptableInput", - (percent >= ViewOptions::MIN_ZOOM && percent <= ViewOptions::MAX_ZOOM)); + (percent >= myMinPercent && percent <= myMaxPercent)); return state; } private: QDoubleValidator myNumberValidator; + int myMinPercent; + int myMaxPercent; }; +/// Playback speeds (percent) offered in the speed dropdown. Any other value in +/// the [MIN_SPEED, MAX_SPEED] range can still be typed in directly. +static constexpr int SPEED_PRESETS[] = { 25, 50, 75, 100 }; +static constexpr int MIN_SPEED = 25; +static constexpr int MAX_SPEED = 125; + PlaybackWidget::PlaybackWidget(const QAction &play_pause_command, const QAction &rewind_command, const QAction &stop_command, @@ -99,10 +112,7 @@ PlaybackWidget::PlaybackWidget(const QAction &play_pause_command, Q_ASSERT(myVoices->buttons().length() == Staff::NUM_VOICES); ui->voice1Button->setChecked(true); - ui->speedSpinner->setMinimum(50); - ui->speedSpinner->setMaximum(125); - ui->speedSpinner->setSuffix(QStringLiteral("%")); - ui->speedSpinner->setValue(100); + setupSpeedComboBox(); ui->rewindToStartButton->setIcon( style()->standardIcon(QStyle::SP_MediaSkipBackward)); @@ -144,8 +154,6 @@ PlaybackWidget::PlaybackWidget(const QAction &play_pause_command, }); connect(myVoices, &QButtonGroup::idClicked, this, &PlaybackWidget::activeVoiceChanged); - connect(ui->speedSpinner, qOverload(&QSpinBox::valueChanged), this, - &PlaybackWidget::playbackSpeedChanged); connect(ui->filterComboBox, qOverload(&QComboBox::currentIndexChanged), this, [&](int index) { @@ -214,7 +222,7 @@ void PlaybackWidget::reset(const Document &doc) int PlaybackWidget::getPlaybackSpeed() const { - return ui->speedSpinner->value(); + return myPlaybackSpeed; } void PlaybackWidget::setPlaybackMode(bool isPlaying) @@ -236,6 +244,64 @@ void PlaybackWidget::updateLocationLabel(const std::string &location) ui->locationLabel->setText(QString::fromStdString(location)); } +void +PlaybackWidget::setupSpeedComboBox() +{ + for (int speed : SPEED_PRESETS) + ui->speedComboBox->addItem(percentToString(speed)); + + ui->speedComboBox->setCurrentText(percentToString(myPlaybackSpeed)); + + // Display a different style for invalid speed values. + ui->speedComboBox->setStyleSheet( + QStringLiteral("QComboBox[acceptableInput=false] { color: red; }")); + + ui->speedComboBox->setValidator( + new PercentageValidator(ui->speedComboBox, MIN_SPEED, MAX_SPEED)); + + // Only refresh the styling as the user types. Unlike the zoom box, the + // speed is applied when the value is committed rather than on every + // keystroke, so that playback doesn't lurch through the intermediate + // values of e.g. "125". + connect(ui->speedComboBox, &QComboBox::currentTextChanged, this, + [this]() + { + ui->speedComboBox->style()->unpolish(ui->speedComboBox); + ui->speedComboBox->style()->polish(ui->speedComboBox); + }); + + // Committed by picking a preset, or by pressing enter / leaving the box + // after typing a custom value. + connect(ui->speedComboBox, &QComboBox::activated, this, + &PlaybackWidget::updatePlaybackSpeed); + connect(ui->speedComboBox->lineEdit(), &QLineEdit::editingFinished, this, + &PlaybackWidget::updatePlaybackSpeed); +} + +void +PlaybackWidget::updatePlaybackSpeed() +{ + QLocale locale; + const int speed = std::clamp( + locale.toInt(extractPercent(ui->speedComboBox->currentText(), locale)), + MIN_SPEED, MAX_SPEED); + + // Normalize the displayed text, e.g. "63" -> "63%", or an out-of-range + // value to the clamped one. + const QString text = percentToString(speed); + if (ui->speedComboBox->currentText() != text) + { + QSignalBlocker blocker(ui->speedComboBox); + ui->speedComboBox->setCurrentText(text); + } + + if (speed == myPlaybackSpeed) + return; + + myPlaybackSpeed = speed; + emit playbackSpeedChanged(speed); +} + void PlaybackWidget::setupZoomComboBox(double initial_zoom) { @@ -249,7 +315,8 @@ PlaybackWidget::setupZoomComboBox(double initial_zoom) ui->zoomComboBox->setStyleSheet( QStringLiteral("QComboBox[acceptableInput=false] { color: red; }")); - ui->zoomComboBox->setValidator(new PercentageValidator(ui->zoomComboBox)); + ui->zoomComboBox->setValidator(new PercentageValidator( + ui->zoomComboBox, ViewOptions::MIN_ZOOM, ViewOptions::MAX_ZOOM)); connect(ui->zoomComboBox, &QComboBox::currentTextChanged, [this](const QString &text) { diff --git a/source/widgets/playback/playbackwidget.h b/source/widgets/playback/playbackwidget.h index 48a9b044..e2aa017c 100644 --- a/source/widgets/playback/playbackwidget.h +++ b/source/widgets/playback/playbackwidget.h @@ -60,11 +60,15 @@ class PlaybackWidget : public QWidget private: void setupZoomComboBox(double initial_zoom); + void setupSpeedComboBox(); + /// Applies the speed currently shown in the speed combo box. + void updatePlaybackSpeed(); void onSettingChanged(const std::string &setting); Ui::PlaybackWidget *ui; QButtonGroup *myVoices; int myPlayerFilterStart = 0; + int myPlaybackSpeed = 100; }; #endif diff --git a/source/widgets/playback/playbackwidget.ui b/source/widgets/playback/playbackwidget.ui index 7a630766..6ee98969 100644 --- a/source/widgets/playback/playbackwidget.ui +++ b/source/widgets/playback/playbackwidget.ui @@ -95,7 +95,7 @@ - + 0 @@ -105,8 +105,14 @@ Qt::FocusPolicy::StrongFocus + + true + + + QComboBox::InsertPolicy::NoInsert + - Adjusts the current playback speed. + Adjusts the current playback speed. Pick a preset, or type any value between 25% and 125%.