From 19023b4cc2d9c0cfd0064bd93ff210f10d8ad996 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 29 Jun 2017 15:29:00 +0300 Subject: [PATCH] Display a toggle in Menu for Checkable actions. --- Telegram/SourceFiles/ui/animation.h | 7 +++ Telegram/SourceFiles/ui/widgets/checkbox.cpp | 56 +++++++++++++++++ Telegram/SourceFiles/ui/widgets/checkbox.h | 20 +++++++ Telegram/SourceFiles/ui/widgets/menu.cpp | 60 +++++++++++++++---- Telegram/SourceFiles/ui/widgets/menu.h | 16 ++++- Telegram/SourceFiles/ui/widgets/widgets.style | 36 +++++++++++ 6 files changed, 180 insertions(+), 15 deletions(-) diff --git a/Telegram/SourceFiles/ui/animation.h b/Telegram/SourceFiles/ui/animation.h index 39aafc804..d8359ce23 100644 --- a/Telegram/SourceFiles/ui/animation.h +++ b/Telegram/SourceFiles/ui/animation.h @@ -621,6 +621,13 @@ public: } } + template + void setUpdateCallback(Lambda &&updateCallback) { + if (_data) { + _data->updateCallback = std::forward(updateCallback); + } + } + private: struct Data { template diff --git a/Telegram/SourceFiles/ui/widgets/checkbox.cpp b/Telegram/SourceFiles/ui/widgets/checkbox.cpp index c9c5ac356..ad62d2113 100644 --- a/Telegram/SourceFiles/ui/widgets/checkbox.cpp +++ b/Telegram/SourceFiles/ui/widgets/checkbox.cpp @@ -270,4 +270,60 @@ Radiobutton::~Radiobutton() { _group->unregisterButton(this); } +ToggleView::ToggleView(const style::Toggle &st, bool toggled, base::lambda updateCallback) +: _st(&st) +, _toggled(toggled) +, _updateCallback(std::move(updateCallback)) { +} + +void ToggleView::setStyle(const style::Toggle &st) { + _st = &st; +} + +void ToggleView::setToggledFast(bool toggled) { + _toggled = toggled; + finishAnimation(); + if (_updateCallback) { + _updateCallback(); + } +} + +void ToggleView::setUpdateCallback(base::lambda updateCallback) { + _updateCallback = std::move(updateCallback); + if (_toggleAnimation.animating()) { + _toggleAnimation.setUpdateCallback(_updateCallback); + } +} + +void ToggleView::setToggledAnimated(bool toggled) { + if (_toggled != toggled) { + _toggled = toggled; + _toggleAnimation.start(_updateCallback, _toggled ? 0. : 1., _toggled ? 1. : 0., _st->duration); + } +} + +void ToggleView::finishAnimation() { + _toggleAnimation.finish(); +} + +void ToggleView::paint(Painter &p, int left, int top, int outerWidth, TimeMs ms) { + PainterHighQualityEnabler hq(p); + auto toggled = _toggleAnimation.current(ms, _toggled ? 1. : 0.); + auto fullWidth = _st->diameter + _st->width; + auto innerDiameter = _st->diameter - 2 * _st->shift; + auto innerRadius = float64(innerDiameter) / 2.; + auto bgRect = rtlrect(left + _st->shift, top + _st->shift, fullWidth - 2 * _st->shift, innerDiameter, outerWidth); + auto fgRect = rtlrect(left + anim::interpolate(0, fullWidth - _st->diameter, toggled), top, _st->diameter, _st->diameter, outerWidth); + + p.setPen(Qt::NoPen); + p.setBrush(anim::brush(_st->untoggledFg, _st->toggledFg, toggled)); + p.drawRoundedRect(bgRect, innerRadius, innerRadius); + + auto pen = anim::pen(_st->untoggledFg, _st->toggledFg, toggled); + pen.setWidth(_st->border); + p.setPen(pen); + p.setBrush(anim::brush(_st->untoggledBg, _st->toggledBg, toggled)); + p.drawEllipse(fgRect); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/checkbox.h b/Telegram/SourceFiles/ui/widgets/checkbox.h index d9807c451..e140e31c1 100644 --- a/Telegram/SourceFiles/ui/widgets/checkbox.h +++ b/Telegram/SourceFiles/ui/widgets/checkbox.h @@ -194,4 +194,24 @@ public: }; +class ToggleView { +public: + ToggleView(const style::Toggle &st, bool toggled, base::lambda updateCallback); + + void setToggledFast(bool toggled); + void setToggledAnimated(bool toggled); + void finishAnimation(); + void setStyle(const style::Toggle &st); + void setUpdateCallback(base::lambda updateCallback); + + void paint(Painter &p, int left, int top, int outerWidth, TimeMs ms); + +private: + gsl::not_null _st; + bool _toggled = false; + base::lambda _updateCallback; + Animation _toggleAnimation; + +}; + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/menu.cpp b/Telegram/SourceFiles/ui/widgets/menu.cpp index b55c6f2e7..c4ec5ef1d 100644 --- a/Telegram/SourceFiles/ui/widgets/menu.cpp +++ b/Telegram/SourceFiles/ui/widgets/menu.cpp @@ -18,9 +18,12 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "ui/widgets/menu.h" #include "ui/effects/ripple_animation.h" +#include "ui/widgets/checkbox.h" namespace Ui { +Menu::ActionData::~ActionData() = default; + Menu::Menu(QWidget *parent, const style::Menu &st) : TWidget(parent) , _st(st) , _itemHeight(_st.itemPadding.top() + _st.itemFont->height + _st.itemPadding.bottom()) @@ -43,7 +46,7 @@ Menu::Menu(QWidget *parent, QMenu *menu, const style::Menu &st) : TWidget(parent } void Menu::init() { - resize(_st.widthMin, _st.skip * 2); + resize(_forceWidth ? _forceWidth : _st.widthMin, _st.skip * 2); setMouseTracking(true); @@ -64,18 +67,22 @@ QAction *Menu::addAction(const QString &text, base::lambda callback, con QAction *Menu::addAction(QAction *action, const style::icon *icon, const style::icon *iconOver) { connect(action, SIGNAL(changed()), this, SLOT(actionChanged())); + connect(action, SIGNAL(toggled(bool)), this, SLOT(actionToggled(bool))); _actions.push_back(action); - ActionData data; - data.icon = icon; - data.iconOver = iconOver ? iconOver : icon; - data.hasSubmenu = (action->menu() != nullptr); - _actionsData.push_back(data); + auto createData = [icon, iconOver, action] { + auto data = ActionData(); + data.icon = icon; + data.iconOver = iconOver ? iconOver : icon; + data.hasSubmenu = (action->menu() != nullptr); + return data; + }; + _actionsData.push_back(createData()); auto newWidth = qMax(width(), _st.widthMin); newWidth = processAction(action, _actions.size() - 1, newWidth); auto newHeight = height() + (action->isSeparator() ? _separatorHeight : _itemHeight); - resize(newWidth, newHeight); + resize(_forceWidth ? _forceWidth : newWidth, newHeight); if (_resizedCallback) { _resizedCallback(); } @@ -97,7 +104,7 @@ void Menu::clearActions() { delete action; } } - resize(_st.widthMin, _st.skip * 2); + resize(_forceWidth ? _forceWidth : _st.widthMin, _st.skip * 2); if (_resizedCallback) { _resizedCallback(); } @@ -118,6 +125,18 @@ int Menu::processAction(QAction *action, int index, int width) { } else if (!actionShortcut.isEmpty()) { goodw += _st.itemPadding.left() + _st.itemFont->width(actionShortcut); } + if (action->isCheckable()) { + auto updateCallback = [this, index] { updateItem(index); }; + goodw += _st.itemPadding.left() + _st.itemToggle.diameter + _st.itemToggle.width - _st.itemToggleShift; + if (data.toggle) { + data.toggle->setUpdateCallback(updateCallback); + data.toggle->setToggledAnimated(action->isChecked()); + } else { + data.toggle = std::make_unique(_st.itemToggle, action->isChecked(), updateCallback); + } + } else { + data.toggle.reset(); + } width = snap(goodw, width, _st.widthMax); data.text = (width < goodw) ? _st.itemFont->elided(actionText, width - (goodw - textw)) : actionText; data.shortcut = actionShortcut; @@ -134,12 +153,17 @@ Menu::Actions &Menu::actions() { return _actions; } +void Menu::setForceWidth(int forceWidth) { + _forceWidth = forceWidth; + resize(_forceWidth, height()); +} + void Menu::actionChanged() { - int newWidth = _st.widthMin; - for (int i = 0, count = _actions.size(); i != count; ++i) { + auto newWidth = _st.widthMin; + for (auto i = 0, count = _actions.size(); i != count; ++i) { newWidth = processAction(_actions[i], i, newWidth); } - if (newWidth != width()) { + if (newWidth != width() && !_forceWidth) { resize(newWidth, height()); if (_resizedCallback) { _resizedCallback(); @@ -193,6 +217,9 @@ void Menu::paintEvent(QPaintEvent *e) { } else if (!data.shortcut.isEmpty()) { p.setPen(selected ? _st.itemFgShortcutOver : (enabled ? _st.itemFgShortcut : _st.itemFgShortcutDisabled)); p.drawTextRight(_st.itemPadding.right(), _st.itemPadding.top(), width(), data.shortcut); + } else if (data.toggle) { + auto toggleSize = _st.itemToggle.diameter + _st.itemToggle.width; + data.toggle->paint(p, width() - _st.itemPadding.right() - toggleSize + _st.itemToggleShift, (_itemHeight - _st.itemToggle.diameter) / 2, width(), ms); } } } @@ -220,7 +247,7 @@ void Menu::itemPressed(TriggeredSource source) { if (source == TriggeredSource::Mouse) { if (!_actionsData[_pressed].ripple) { auto mask = RippleAnimation::rectMask(QSize(width(), _itemHeight)); - _actionsData[_pressed].ripple = MakeShared(_st.ripple, std::move(mask), [this, selected = _pressed] { + _actionsData[_pressed].ripple = std::make_unique(_st.ripple, std::move(mask), [this, selected = _pressed] { updateItem(selected); }); } @@ -234,6 +261,9 @@ void Menu::itemPressed(TriggeredSource source) { void Menu::itemReleased(TriggeredSource source) { auto pressed = std::exchange(_pressed, -1); if (pressed >= 0 && pressed < _actions.size()) { + if (pressed != _selected && _actionsData[pressed].toggle) { + _actionsData[pressed].toggle->setStyle(_st.itemToggle); + } if (source == TriggeredSource::Mouse && _actionsData[pressed].ripple) { _actionsData[pressed].ripple->lastStop(); } @@ -316,7 +346,13 @@ void Menu::setSelected(int selected) { } if (_selected != selected) { updateSelectedItem(); + if (_selected >= 0 && _selected != _pressed && _actionsData[_selected].toggle) { + _actionsData[_selected].toggle->setStyle(_st.itemToggle); + } _selected = selected; + if (_selected >= 0 && _actionsData[_selected].toggle && _actions[_selected]->isEnabled()) { + _actionsData[_selected].toggle->setStyle(_st.itemToggleOver); + } updateSelectedItem(); if (_activatedCallback) { auto source = _mouseSelection ? TriggeredSource::Mouse : TriggeredSource::Keyboard; diff --git a/Telegram/SourceFiles/ui/widgets/menu.h b/Telegram/SourceFiles/ui/widgets/menu.h index e48580df3..51b7036bf 100644 --- a/Telegram/SourceFiles/ui/widgets/menu.h +++ b/Telegram/SourceFiles/ui/widgets/menu.h @@ -24,6 +24,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org namespace Ui { +class ToggleView; class RippleAnimation; class Menu : public TWidget { @@ -48,6 +49,7 @@ public: _childShown = shown; } void setShowSource(TriggeredSource source); + void setForceWidth(int forceWidth); using Actions = QList; Actions &actions(); @@ -123,19 +125,27 @@ private: base::lambda _mouseReleaseDelegate; struct ActionData { + ActionData() = default; + ActionData(const ActionData &other) = delete; + ActionData &operator=(const ActionData &other) = delete; + ActionData(ActionData &&other) = default; + ActionData &operator=(ActionData &&other) = default; + ~ActionData(); + bool hasSubmenu = false; QString text; QString shortcut; const style::icon *icon = nullptr; const style::icon *iconOver = nullptr; - QSharedPointer ripple; + std::unique_ptr ripple; + std::unique_ptr toggle; }; - using ActionsData = QList; QMenu *_wappedMenu = nullptr; Actions _actions; - ActionsData _actionsData; + std::vector _actionsData; + int _forceWidth = 0; int _itemHeight, _separatorHeight; bool _mouseSelection = false; diff --git a/Telegram/SourceFiles/ui/widgets/widgets.style b/Telegram/SourceFiles/ui/widgets/widgets.style index ef402c71b..bb5fa69c4 100644 --- a/Telegram/SourceFiles/ui/widgets/widgets.style +++ b/Telegram/SourceFiles/ui/widgets/widgets.style @@ -360,6 +360,18 @@ CallButton { outerBg: color; } +Toggle { + toggledBg: color; + toggledFg: color; + untoggledBg: color; + untoggledFg: color; + duration: int; + border: pixels; + shift: pixels; + diameter: pixels; + width: pixels; +} + Menu { skip: pixels; @@ -374,6 +386,9 @@ Menu { itemPadding: margins; itemIconPosition: point; itemFont: font; + itemToggle: Toggle; + itemToggleOver: Toggle; + itemToggleShift: pixels; separatorPadding: margins; separatorWidth: pixels; @@ -766,7 +781,25 @@ defaultRoundCheckbox: RoundCheckbox { duration: 150; } +defaultToggle: Toggle { + toggledBg: windowBg; + toggledFg: windowBgActive; + untoggledBg: windowBg; + untoggledFg: checkboxFg; + duration: 200; + border: 2px; + shift: 1px; + diameter: 20px; + width: 16px; +} + defaultMenuArrow: icon {{ "dropdown_submenu_arrow", menuSubmenuArrowFg }}; +defaultMenuToggle: Toggle(defaultToggle) { + untoggledFg: menuIconFg; +} +defaultMenuToggleOver: Toggle(defaultToggle) { + untoggledFg: menuIconFgOver; +} defaultMenu: Menu { skip: 0px; @@ -781,6 +814,9 @@ defaultMenu: Menu { itemIconPosition: point(0px, 0px); itemPadding: margins(17px, 8px, 17px, 7px); itemFont: normalFont; + itemToggle: defaultMenuToggle; + itemToggleOver: defaultMenuToggleOver; + itemToggleShift: 0px; separatorPadding: margins(0px, 5px, 0px, 5px); separatorWidth: 1px;