From d22f5f405f18b6983b7ae19c5c8caff1c88c8098 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 20 Sep 2019 11:04:55 +0300 Subject: [PATCH] Add toast notification control. --- gyp/sources.txt | 6 ++ ui/toast/toast.cpp | 84 ++++++++++++++++++++++++ ui/toast/toast.h | 66 +++++++++++++++++++ ui/toast/toast_manager.cpp | 127 +++++++++++++++++++++++++++++++++++++ ui/toast/toast_manager.h | 55 ++++++++++++++++ ui/toast/toast_widget.cpp | 73 +++++++++++++++++++++ ui/toast/toast_widget.h | 51 +++++++++++++++ 7 files changed, 462 insertions(+) create mode 100644 ui/toast/toast.cpp create mode 100644 ui/toast/toast.h create mode 100644 ui/toast/toast_manager.cpp create mode 100644 ui/toast/toast_manager.h create mode 100644 ui/toast/toast_widget.cpp create mode 100644 ui/toast/toast_widget.h diff --git a/gyp/sources.txt b/gyp/sources.txt index eeaedcc..9271d78 100644 --- a/gyp/sources.txt +++ b/gyp/sources.txt @@ -56,6 +56,12 @@ <(src_loc)/ui/text/text_isolated_emoji.h <(src_loc)/ui/text/text_utilities.cpp <(src_loc)/ui/text/text_utilities.h +<(src_loc)/ui/toast/toast.cpp +<(src_loc)/ui/toast/toast.h +<(src_loc)/ui/toast/toast_manager.cpp +<(src_loc)/ui/toast/toast_manager.h +<(src_loc)/ui/toast/toast_widget.cpp +<(src_loc)/ui/toast/toast_widget.h <(src_loc)/ui/widgets/box_content_divider.cpp <(src_loc)/ui/widgets/box_content_divider.h <(src_loc)/ui/widgets/buttons.cpp diff --git a/ui/toast/toast.cpp b/ui/toast/toast.cpp new file mode 100644 index 0000000..69cfc01 --- /dev/null +++ b/ui/toast/toast.cpp @@ -0,0 +1,84 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#include "ui/toast/toast.h" + +#include "ui/toast/toast_manager.h" +#include "ui/toast/toast_widget.h" + +namespace Ui { +namespace Toast { +namespace { + +QPointer DefaultParent; + +} // namespace + +Instance::Instance( + const Config &config, + not_null widgetParent, + const Private &) +: _hideAtMs(crl::now() + config.durationMs) { + _widget = std::make_unique(widgetParent, config); + _a_opacity.start( + [=] { opacityAnimationCallback(); }, + 0., + 1., + st::toastFadeInDuration); +} + +void SetDefaultParent(not_null parent) { + DefaultParent = parent; +} + +void Show(not_null parent, const Config &config) { + const auto manager = internal::Manager::instance(parent); + manager->addToast(std::make_unique( + config, + parent, + Instance::Private())); +} + +void Show(const Config &config) { + if (const auto parent = DefaultParent.data()) { + Show(parent, config); + } +} + +void Show(not_null parent, const QString &text) { + auto config = Config(); + config.text = text; + Show(parent, config); +} + +void Show(const QString &text) { + auto config = Config(); + config.text = text; + Show(config); +} + +void Instance::opacityAnimationCallback() { + _widget->setShownLevel(_a_opacity.value(_hiding ? 0. : 1.)); + _widget->update(); + if (!_a_opacity.animating()) { + if (_hiding) { + hide(); + } + } +} + +void Instance::hideAnimated() { + _hiding = true; + _a_opacity.start([this] { opacityAnimationCallback(); }, 1., 0., st::toastFadeOutDuration); +} + +void Instance::hide() { + _widget->hide(); + _widget->deleteLater(); +} + +} // namespace Toast +} // namespace Ui diff --git a/ui/toast/toast.h b/ui/toast/toast.h new file mode 100644 index 0000000..3ea36cc --- /dev/null +++ b/ui/toast/toast.h @@ -0,0 +1,66 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "ui/effects/animations.h" + +namespace Ui { +namespace Toast { + +namespace internal { +class Manager; +class Widget; +} // namespace internal + +static constexpr const int DefaultDuration = 1500; +struct Config { + QString text; + QMargins padding; + int durationMs = DefaultDuration; + int minWidth = 0; + int maxWidth = 0; + int maxLines = 16; + bool multiline = false; +}; +void SetDefaultParent(not_null parent); +void Show(not_null parent, const Config &config); +void Show(const Config &config); +void Show(not_null parent, const QString &text); +void Show(const QString &text); + +class Instance { + struct Private { + }; + +public: + Instance( + const Config &config, + not_null widgetParent, + const Private &); + Instance(const Instance &other) = delete; + Instance &operator=(const Instance &other) = delete; + + void hideAnimated(); + void hide(); + +private: + void opacityAnimationCallback(); + + bool _hiding = false; + Ui::Animations::Simple _a_opacity; + + const crl::time _hideAtMs; + + // ToastManager should reset _widget pointer if _widget is destroyed. + friend class internal::Manager; + friend void Show(not_null parent, const Config &config); + std::unique_ptr _widget; + +}; + +} // namespace Toast +} // namespace Ui diff --git a/ui/toast/toast_manager.cpp b/ui/toast/toast_manager.cpp new file mode 100644 index 0000000..03eb810 --- /dev/null +++ b/ui/toast/toast_manager.cpp @@ -0,0 +1,127 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#include "ui/toast/toast_manager.h" + +#include "ui/toast/toast_widget.h" + +namespace Ui { +namespace Toast { +namespace internal { +namespace { + +base::flat_map, not_null> ManagersMap; + +} // namespace + +Manager::Manager(not_null parent, const CreateTag &) +: QObject(parent) +, _hideTimer([=] { hideByTimer(); }) { +} + +bool Manager::eventFilter(QObject *o, QEvent *e) { + if (e->type() == QEvent::Resize) { + for (auto i = _toastByWidget.cbegin(), e = _toastByWidget.cend(); i != e; ++i) { + if (i.key()->parentWidget() == o) { + i.key()->onParentResized(); + } + } + } + return QObject::eventFilter(o, e); +} + +not_null Manager::instance(not_null parent) { + auto i = ManagersMap.find(parent); + if (i == end(ManagersMap)) { + i = ManagersMap.emplace( + parent, + Ui::CreateChild(parent.get(), CreateTag()) + ).first; + } + return i->second; +} + +void Manager::addToast(std::unique_ptr &&toast) { + _toasts.push_back(toast.release()); + Instance *t = _toasts.back(); + Widget *widget = t->_widget.get(); + + _toastByWidget.insert(widget, t); + connect(widget, SIGNAL(destroyed(QObject*)), this, SLOT(onToastWidgetDestroyed(QObject*))); + if (auto parent = widget->parentWidget()) { + auto found = false; + for (auto i = _toastParents.begin(); i != _toastParents.cend();) { + if (*i == parent) { + found = true; + break; + } else if (!*i) { + i = _toastParents.erase(i); + } else { + ++i; + } + } + if (!found) { + _toastParents.insert(parent); + parent->installEventFilter(this); + } + } + + auto oldHideNearestMs = _toastByHideTime.isEmpty() ? 0LL : _toastByHideTime.firstKey(); + _toastByHideTime.insert(t->_hideAtMs, t); + if (!oldHideNearestMs || _toastByHideTime.firstKey() < oldHideNearestMs) { + startNextHideTimer(); + } +} + +void Manager::hideByTimer() { + auto now = crl::now(); + for (auto i = _toastByHideTime.begin(); i != _toastByHideTime.cend();) { + if (i.key() <= now) { + auto toast = i.value(); + i = _toastByHideTime.erase(i); + toast->hideAnimated(); + } else { + break; + } + } + startNextHideTimer(); +} + +void Manager::onToastWidgetDestroyed(QObject *widget) { + auto i = _toastByWidget.find(static_cast(widget)); + if (i != _toastByWidget.cend()) { + auto toast = i.value(); + _toastByWidget.erase(i); + toast->_widget.release(); + + int index = _toasts.indexOf(toast); + if (index >= 0) { + _toasts.removeAt(index); + delete toast; + } + } +} + +void Manager::startNextHideTimer() { + if (_toastByHideTime.isEmpty()) return; + + auto ms = crl::now(); + if (ms >= _toastByHideTime.firstKey()) { + crl::on_main(this, [=] { + hideByTimer(); + }); + } else { + _hideTimer.callOnce(_toastByHideTime.firstKey() - ms); + } +} + +Manager::~Manager() { + ManagersMap.remove(parent()); +} + +} // namespace internal +} // namespace Toast +} // namespace Ui diff --git a/ui/toast/toast_manager.h b/ui/toast/toast_manager.h new file mode 100644 index 0000000..8c35efd --- /dev/null +++ b/ui/toast/toast_manager.h @@ -0,0 +1,55 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "ui/toast/toast.h" +#include "base/timer.h" + +namespace Ui { +namespace Toast { +namespace internal { + +class Widget; +class Manager : public QObject { + Q_OBJECT + + struct CreateTag { + }; + +public: + Manager(not_null parent, const CreateTag &); + Manager(const Manager &other) = delete; + Manager &operator=(const Manager &other) = delete; + ~Manager(); + + static not_null instance(not_null parent); + + void addToast(std::unique_ptr &&toast); + +protected: + bool eventFilter(QObject *o, QEvent *e); + +private slots: + void onToastWidgetDestroyed(QObject *widget); + +private: + void startNextHideTimer(); + void hideByTimer(); + + base::Timer _hideTimer; + crl::time _nextHide = 0; + + QMultiMap _toastByHideTime; + QMap _toastByWidget; + QList _toasts; + OrderedSet> _toastParents; + +}; + +} // namespace internal +} // namespace Toast +} // namespace Ui diff --git a/ui/toast/toast_widget.cpp b/ui/toast/toast_widget.cpp new file mode 100644 index 0000000..962ca5d --- /dev/null +++ b/ui/toast/toast_widget.cpp @@ -0,0 +1,73 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#include "ui/toast/toast_widget.h" + +#include "ui/image/image_prepare.h" +#include "styles/palette.h" + +namespace Ui { +namespace Toast { +namespace internal { + +Widget::Widget(QWidget *parent, const Config &config) +: TWidget(parent) +, _roundRect(ImageRoundRadius::Large, st::toastBg) +, _multiline(config.multiline) +, _maxWidth((config.maxWidth > 0) ? config.maxWidth : st::toastMaxWidth) +, _padding((config.padding.left() > 0) ? config.padding : st::toastPadding) +, _maxTextWidth(widthWithoutPadding(_maxWidth)) +, _maxTextHeight( + st::toastTextStyle.font->height * (_multiline ? config.maxLines : 1)) +, _text(_multiline ? widthWithoutPadding(config.minWidth) : QFIXED_MAX) { + const auto toastOptions = TextParseOptions{ + TextParseMultiline, + _maxTextWidth, + _maxTextHeight, + Qt::LayoutDirectionAuto + }; + _text.setText( + st::toastTextStyle, + _multiline ? config.text : TextUtilities::SingleLine(config.text), + toastOptions); + + setAttribute(Qt::WA_TransparentForMouseEvents); + + onParentResized(); + show(); +} + +void Widget::onParentResized() { + auto newWidth = _maxWidth; + accumulate_min(newWidth, _padding.left() + _text.maxWidth() + _padding.right()); + accumulate_min(newWidth, parentWidget()->width() - 2 * st::toastMinMargin); + _textWidth = widthWithoutPadding(newWidth); + const auto textHeight = _multiline + ? qMin(_text.countHeight(_textWidth), _maxTextHeight) + : _text.minHeight(); + const auto newHeight = _padding.top() + textHeight + _padding.bottom(); + setGeometry((parentWidget()->width() - newWidth) / 2, (parentWidget()->height() - newHeight) / 2, newWidth, newHeight); +} + +void Widget::setShownLevel(float64 shownLevel) { + _shownLevel = shownLevel; +} + +void Widget::paintEvent(QPaintEvent *e) { + Painter p(this); + PainterHighQualityEnabler hq(p); + + p.setOpacity(_shownLevel); + _roundRect.paint(p, rect()); + + const auto lines = _maxTextHeight / st::toastTextStyle.font->height; + p.setPen(st::toastFg); + _text.drawElided(p, _padding.left(), _padding.top(), _textWidth + 1, lines); +} + +} // namespace internal +} // namespace Toast +} // namespace Ui diff --git a/ui/toast/toast_widget.h b/ui/toast/toast_widget.h new file mode 100644 index 0000000..0dd8f69 --- /dev/null +++ b/ui/toast/toast_widget.h @@ -0,0 +1,51 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "ui/toast/toast.h" +#include "ui/text/text.h" +#include "ui/rp_widget.h" +#include "ui/round_rect.h" + +namespace Ui { +namespace Toast { +namespace internal { + +class Widget : public TWidget { +public: + Widget(QWidget *parent, const Config &config); + + // shownLevel=1 completely visible, shownLevel=0 completely invisible + void setShownLevel(float64 shownLevel); + + void onParentResized(); + +protected: + void paintEvent(QPaintEvent *e) override; + +private: + int widthWithoutPadding(int w) { + return w - _padding.left() - _padding.right(); + } + + RoundRect _roundRect; + + float64 _shownLevel = 0; + bool _multiline = false; + int _maxWidth = 0; + QMargins _padding; + + int _maxTextWidth = 0; + int _maxTextHeight = 0; + int _textWidth = 0; + Text::String _text; + +}; + +} // namespace internal +} // namespace Toast +} // namespace Ui