From 16faacec657c15bb3efe003700387f28d9c42851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20=C3=81ngel=20San=20Mart=C3=ADn?= Date: Sat, 3 Jun 2023 20:25:14 +0200 Subject: [PATCH] Debounce input from the search edit This makes writing there a little bit more pleasant --- YACReaderLibrary/YACReaderLibrary.pro | 1 + YACReaderLibrary/library_window.cpp | 15 +- third_party/KDToolBox/KDSignalThrottler.cpp | 183 ++++++++++++++++++++ third_party/KDToolBox/KDSignalThrottler.h | 130 ++++++++++++++ third_party/KDToolBox/KDToolBox.pri | 4 + 5 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 third_party/KDToolBox/KDSignalThrottler.cpp create mode 100644 third_party/KDToolBox/KDSignalThrottler.h create mode 100644 third_party/KDToolBox/KDToolBox.pri diff --git a/YACReaderLibrary/YACReaderLibrary.pro b/YACReaderLibrary/YACReaderLibrary.pro index 4ac749b7..bff7643e 100644 --- a/YACReaderLibrary/YACReaderLibrary.pro +++ b/YACReaderLibrary/YACReaderLibrary.pro @@ -264,6 +264,7 @@ include(./comic_vine/comic_vine.pri) include(../third_party/QsLog/QsLog.pri) include(../shortcuts_management/shortcuts_management.pri) include(../third_party/QrCode/QrCode.pri) +include(../third_party/KDToolBox/KDToolBox.pri) RESOURCES += images.qrc files.qrc win32:RESOURCES += images_win.qrc diff --git a/YACReaderLibrary/library_window.cpp b/YACReaderLibrary/library_window.cpp index 06794dd1..800ea572 100644 --- a/YACReaderLibrary/library_window.cpp +++ b/YACReaderLibrary/library_window.cpp @@ -95,6 +95,8 @@ extern YACReaderHttpServer *httpServer; #include #endif +#include + namespace { template void moveAndConnectRemoverToThread(Remover *remover, QThread *thread) @@ -1281,11 +1283,20 @@ void LibraryWindow::createConnections() connect(optionsDialog, &YACReaderOptionsDialog::optionsChanged, this, &LibraryWindow::reloadOptions); connect(optionsDialog, &YACReaderOptionsDialog::editShortcuts, editShortcutsDialog, &QWidget::show); + auto searchDebouncer = new KDToolBox::KDSignalDebouncer(this); + searchDebouncer->setTimeout(400); + // Search filter #ifdef Y_MAC_UI - connect(searchEdit, &YACReaderMacOSXSearchLineEdit::filterChanged, this, &LibraryWindow::setSearchFilter); + connect(searchEdit, &YACReaderMacOSXSearchLineEdit::textChanged, searchDebouncer, &KDToolBox::KDSignalThrottler::throttle); + connect(searchDebouncer, &KDToolBox::KDSignalThrottler::triggered, this, [=] { + setSearchFilter(searchEdit->text()); + }); #else - connect(searchEdit, &YACReaderSearchLineEdit::filterChanged, this, &LibraryWindow::setSearchFilter); + connect(searchEdit, &YACReaderSearchLineEdit::textChanged, searchDebouncer, &KDToolBox::KDSignalThrottler::throttle); + connect(searchDebouncer, &KDToolBox::KDSignalThrottler::triggered, this, [=] { + setSearchFilter(searchEdit->text()); + }); #endif connect(&comicQueryResultProcessor, &ComicQueryResultProcessor::newData, this, &LibraryWindow::setComicSearchFilterData); qRegisterMetaType("FolderItem *"); diff --git a/third_party/KDToolBox/KDSignalThrottler.cpp b/third_party/KDToolBox/KDSignalThrottler.cpp new file mode 100644 index 00000000..ddadf6d2 --- /dev/null +++ b/third_party/KDToolBox/KDSignalThrottler.cpp @@ -0,0 +1,183 @@ +/**************************************************************************** +** MIT License +** +** Copyright (C) 2020-2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Giuseppe D'Angelo +* +** +** This file is part of KDToolBox (https://github.com/KDAB/KDToolBox). +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, ** and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice (including the next paragraph) +** shall be included in all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF ** CONTRACT, TORT OR OTHERWISE, +** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +** DEALINGS IN THE SOFTWARE. +****************************************************************************/ + +#include "KDSignalThrottler.h" + +namespace KDToolBox +{ + +KDGenericSignalThrottler::KDGenericSignalThrottler(Kind kind, EmissionPolicy emissionPolicy, QObject *parent) + : QObject(parent) + , m_timer(this) + , m_kind(kind) + , m_emissionPolicy(emissionPolicy) + , m_hasPendingEmission(false) +{ + // For leading throttlers we use a repeated timer. This is in order + // to catch the case where a signal is received by the throttler + // just after it emitted a throttled/debounced signal. Even if leading, + // it shouldn't re-emit immediately, as it would be too close to the previous one. + // So we keep the timer running, and stop it later if it times out + // with no intervening timeout() emitted by it. + switch (m_emissionPolicy) + { + case EmissionPolicy::Leading: + m_timer.setSingleShot(false); + break; + case EmissionPolicy::Trailing: + m_timer.setSingleShot(true); + break; + } + connect(&m_timer, &QTimer::timeout, this, &KDGenericSignalThrottler::maybeEmitTriggered); +} + +KDGenericSignalThrottler::~KDGenericSignalThrottler() +{ + maybeEmitTriggered(); +} + +KDGenericSignalThrottler::Kind KDGenericSignalThrottler::kind() const +{ + return m_kind; +} + +KDGenericSignalThrottler::EmissionPolicy KDGenericSignalThrottler::emissionPolicy() const +{ + return m_emissionPolicy; +} + +int KDGenericSignalThrottler::timeout() const +{ + return m_timer.interval(); +} + +void KDGenericSignalThrottler::setTimeout(int timeout) +{ + if (m_timer.interval() == timeout) + return; + m_timer.setInterval(timeout); + Q_EMIT timeoutChanged(timeout); +} + +void KDGenericSignalThrottler::setTimeout(std::chrono::milliseconds timeout) +{ + setTimeout(int(timeout.count())); +} + +Qt::TimerType KDGenericSignalThrottler::timerType() const +{ + return m_timer.timerType(); +} + +void KDGenericSignalThrottler::setTimerType(Qt::TimerType timerType) +{ + if (m_timer.timerType() == timerType) + return; + m_timer.setTimerType(timerType); + Q_EMIT timerTypeChanged(timerType); +} + +void KDGenericSignalThrottler::throttle() +{ + m_hasPendingEmission = true; + + switch (m_emissionPolicy) + { + case EmissionPolicy::Leading: + // Emit only if we haven't emitted already. We know if that's + // the case by checking if the timer is running. + if (!m_timer.isActive()) + emitTriggered(); + break; + case EmissionPolicy::Trailing: + break; + } + + // The timer is started in all cases. If we got a signal, + // and we're Leading, and we did emit because of that, + // then we don't re-emit when the timer fires (unless we get ANOTHER + // signal). + switch (m_kind) + { + case Kind::Throttler: + if (!m_timer.isActive()) + m_timer.start(); // = actual start, not restart + break; + case Kind::Debouncer: + m_timer.start(); // = restart + break; + } + + Q_ASSERT(m_timer.isActive()); +} + +void KDGenericSignalThrottler::maybeEmitTriggered() +{ + if (m_hasPendingEmission) + emitTriggered(); + else + m_timer.stop(); +} + +void KDGenericSignalThrottler::emitTriggered() +{ + Q_ASSERT(m_hasPendingEmission); + m_hasPendingEmission = false; + Q_EMIT triggered(); +} + +// Convenience + +KDSignalThrottler::KDSignalThrottler(QObject *parent) + : KDGenericSignalThrottler(Kind::Throttler, EmissionPolicy::Trailing, parent) +{ +} + +KDSignalThrottler::~KDSignalThrottler() = default; + +KDSignalLeadingThrottler::KDSignalLeadingThrottler(QObject *parent) + : KDGenericSignalThrottler(Kind::Throttler, EmissionPolicy::Leading, parent) +{ +} + +KDSignalLeadingThrottler::~KDSignalLeadingThrottler() = default; + +KDSignalDebouncer::KDSignalDebouncer(QObject *parent) + : KDGenericSignalThrottler(Kind::Debouncer, EmissionPolicy::Trailing, parent) +{ +} + +KDSignalDebouncer::~KDSignalDebouncer() = default; + +KDSignalLeadingDebouncer::KDSignalLeadingDebouncer(QObject *parent) + : KDGenericSignalThrottler(Kind::Debouncer, EmissionPolicy::Leading, parent) +{ +} + +KDSignalLeadingDebouncer::~KDSignalLeadingDebouncer() = default; + +} // namespace KDToolBox diff --git a/third_party/KDToolBox/KDSignalThrottler.h b/third_party/KDToolBox/KDSignalThrottler.h new file mode 100644 index 00000000..60e0c1ee --- /dev/null +++ b/third_party/KDToolBox/KDSignalThrottler.h @@ -0,0 +1,130 @@ +/**************************************************************************** +** MIT License +** +** Copyright (C) 2020-2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Giuseppe D'Angelo +* +** +** This file is part of KDToolBox (https://github.com/KDAB/KDToolBox). +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, ** and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice (including the next paragraph) +** shall be included in all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF ** CONTRACT, TORT OR OTHERWISE, +** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +** DEALINGS IN THE SOFTWARE. +****************************************************************************/ + +#ifndef KDSIGNALTHROTTLER_H +#define KDSIGNALTHROTTLER_H + +#include +#include + +#include + +namespace KDToolBox +{ + +class KDGenericSignalThrottler : public QObject +{ + Q_OBJECT + + Q_PROPERTY(Kind kind READ kind CONSTANT) + Q_PROPERTY(EmissionPolicy emissionPolicy READ emissionPolicy CONSTANT) + Q_PROPERTY(int timeout READ timeout WRITE setTimeout NOTIFY timeoutChanged) + Q_PROPERTY(Qt::TimerType timerType READ timerType WRITE setTimerType NOTIFY timerTypeChanged) + +public: + enum class Kind + { + Throttler, + Debouncer, + }; + Q_ENUM(Kind) + + enum class EmissionPolicy + { + Trailing, + Leading, + }; + Q_ENUM(EmissionPolicy) + + explicit KDGenericSignalThrottler(Kind kind, EmissionPolicy emissionPolicy, QObject *parent = nullptr); + ~KDGenericSignalThrottler() override; + + Kind kind() const; + EmissionPolicy emissionPolicy() const; + + int timeout() const; + void setTimeout(int timeout); + void setTimeout(std::chrono::milliseconds timeout); + + Qt::TimerType timerType() const; + void setTimerType(Qt::TimerType timerType); + +public Q_SLOTS: + void throttle(); + +Q_SIGNALS: + void triggered(); + void timeoutChanged(int timeout); + void timerTypeChanged(Qt::TimerType timerType); + +private: + void maybeEmitTriggered(); + void emitTriggered(); + + QTimer m_timer; + Kind m_kind; + EmissionPolicy m_emissionPolicy; + bool m_hasPendingEmission; +}; + +// Convenience subclasses, e.g. for registering into QML + +class KDSignalThrottler : public KDGenericSignalThrottler +{ + Q_OBJECT +public: + explicit KDSignalThrottler(QObject *parent = nullptr); + ~KDSignalThrottler() override; +}; + +class KDSignalLeadingThrottler : public KDGenericSignalThrottler +{ + Q_OBJECT +public: + explicit KDSignalLeadingThrottler(QObject *parent = nullptr); + ~KDSignalLeadingThrottler() override; +}; + +class KDSignalDebouncer : public KDGenericSignalThrottler +{ + Q_OBJECT +public: + explicit KDSignalDebouncer(QObject *parent = nullptr); + ~KDSignalDebouncer() override; +}; + +class KDSignalLeadingDebouncer : public KDGenericSignalThrottler +{ + Q_OBJECT +public: + explicit KDSignalLeadingDebouncer(QObject *parent = nullptr); + ~KDSignalLeadingDebouncer() override; +}; + +} // namespace KDToolBox + +#endif // KDSIGNALTHROTTLER_H diff --git a/third_party/KDToolBox/KDToolBox.pri b/third_party/KDToolBox/KDToolBox.pri new file mode 100644 index 00000000..1cfbb745 --- /dev/null +++ b/third_party/KDToolBox/KDToolBox.pri @@ -0,0 +1,4 @@ +INCLUDEPATH += $$PWD + +SOURCES += $$PWD/KDSignalThrottler.cpp +HEADERS += $$PWD/KDSignalThrottler.h