Allow selecting custom filter icons.

This commit is contained in:
John Preston 2020-03-19 18:55:17 +04:00
parent ce7621fbd9
commit c4a0bc1fd5
11 changed files with 766 additions and 81 deletions

View file

@ -924,6 +924,8 @@ PRIVATE
ui/empty_userpic.h
ui/filter_icons.cpp
ui/filter_icons.h
ui/filter_icon_panel.cpp
ui/filter_icon_panel.h
ui/grouped_layout.cpp
ui/grouped_layout.h
ui/resize_area.h

View file

@ -2277,6 +2277,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_filters_type_no_archived" = "Archived";
"lng_filters_type_no_muted" = "Muted";
"lng_filters_type_no_read" = "Read";
"lng_filters_icon_header" = "Choose icon";
// Wnd specific

View file

@ -12,10 +12,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/input_fields.h"
#include "ui/effects/panel_animation.h"
#include "ui/filter_icons.h"
#include "ui/filter_icon_panel.h"
#include "data/data_chat_filters.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "settings/settings_common.h"
#include "base/event_filter.h"
#include "lang/lang_keys.h"
#include "history/history.h"
#include "main/main_session.h"
@ -91,39 +95,42 @@ private:
not_null<FilterChatsPreview*> SetupChatsPreview(
not_null<Ui::VerticalLayout*> content,
not_null<Data::ChatFilter*> data,
not_null<rpl::variable<Data::ChatFilter>*> data,
Flags flags,
ExceptionPeersGetter peers) {
const auto rules = data->current();
const auto preview = content->add(object_ptr<FilterChatsPreview>(
content,
data->flags() & flags,
(data->*peers)()));
rules.flags() & flags,
(rules.*peers)()));
preview->flagRemoved(
) | rpl::start_with_next([=](Flag flag) {
const auto rules = data->current();
*data = Data::ChatFilter(
data->id(),
data->title(),
data->iconEmoji(),
(data->flags() & ~flag),
data->always(),
data->pinned(),
data->never());
rules.id(),
rules.title(),
rules.iconEmoji(),
(rules.flags() & ~flag),
rules.always(),
rules.pinned(),
rules.never());
}, preview->lifetime());
preview->peerRemoved(
) | rpl::start_with_next([=](not_null<History*> history) {
auto always = data->always();
auto pinned = data->pinned();
auto never = data->never();
const auto rules = data->current();
auto always = rules.always();
auto pinned = rules.pinned();
auto never = rules.never();
always.remove(history);
pinned.erase(ranges::remove(pinned, history), end(pinned));
never.remove(history);
*data = Data::ChatFilter(
data->id(),
data->title(),
data->iconEmoji(),
data->flags(),
rules.id(),
rules.title(),
rules.iconEmoji(),
rules.flags(),
std::move(always),
std::move(pinned),
std::move(never));
@ -261,21 +268,23 @@ void EditExceptions(
not_null<Window::SessionController*> window,
not_null<QObject*> context,
Flags options,
not_null<Data::ChatFilter*> data,
not_null<rpl::variable<Data::ChatFilter>*> data,
Fn<void()> refresh) {
const auto include = (options & Flag::Contacts) != Flags(0);
const auto rules = data->current();
auto controller = std::make_unique<EditFilterChatsListController>(
window,
(include
? tr::lng_filters_include_title()
: tr::lng_filters_exclude_title()),
options,
data->flags() & options,
include ? data->always() : data->never());
rules.flags() & options,
include ? rules.always() : rules.never());
const auto rawController = controller.get();
auto initBox = [=](not_null<PeerListBox*> box) {
box->addButton(tr::lng_settings_save(), crl::guard(context, [=] {
const auto peers = box->peerListCollectSelectedRows();
const auto rules = data->current();
auto &&histories = ranges::view::all(
peers
) | ranges::view::transform([=](not_null<PeerData*> peer) {
@ -285,20 +294,22 @@ void EditExceptions(
histories.begin(),
histories.end()
};
auto removeFrom = include ? data->never() : data->always();
auto removeFrom = include ? rules.never() : rules.always();
for (const auto &history : changed) {
removeFrom.remove(history);
}
auto pinned = data->pinned();
pinned.erase(ranges::remove_if(pinned, [&](not_null<History*> history) {
auto pinned = rules.pinned();
pinned.erase(ranges::remove_if(pinned, [&](
not_null<History*> history) {
const auto contains = changed.contains(history);
return include ? !contains : contains;
}), end(pinned));
*data = Data::ChatFilter(
data->id(),
data->title(),
data->iconEmoji(),
(data->flags() & ~options) | rawController->chosenOptions(),
rules.id(),
rules.title(),
rules.iconEmoji(),
((rules.flags() & ~options)
| rawController->chosenOptions()),
include ? std::move(changed) : std::move(removeFrom),
std::move(pinned),
include ? std::move(removeFrom) : std::move(changed));
@ -314,6 +325,94 @@ void EditExceptions(
Ui::LayerOption::KeepOther);
}
[[nodiscard]] void CreateIconSelector(
not_null<QWidget*> outer,
not_null<QWidget*> box,
not_null<QWidget*> parent,
not_null<Ui::InputField*> input,
not_null<rpl::variable<Data::ChatFilter>*> data) {
const auto rules = data->current();
const auto toggle = Ui::CreateChild<Ui::AbstractButton>(parent.get());
toggle->resize(st::windowFilterIconToggleSize);
const auto type = toggle->lifetime().make_state<Ui::FilterIcon>();
data->value(
) | rpl::map([=](const Data::ChatFilter &filter) {
return Ui::ComputeFilterIcon(filter);
}) | rpl::start_with_next([=](Ui::FilterIcon icon) {
*type = icon;
toggle->update();
}, toggle->lifetime());
input->geometryValue(
) | rpl::start_with_next([=](QRect geometry) {
const auto left = geometry.x() + geometry.width() - toggle->width();
const auto position = st::windowFilterIconTogglePosition;
toggle->move(
left - position.x(),
geometry.y() + position.y());
}, toggle->lifetime());
toggle->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(toggle);
const auto icons = Ui::LookupFilterIcon(*type);
icons.normal->paintInCenter(p, toggle->rect(), st::emojiIconFg->c);
}, toggle->lifetime());
const auto panel = toggle->lifetime().make_state<Ui::FilterIconPanel>(
outer);
toggle->installEventFilter(panel);
toggle->addClickHandler([=] {
panel->toggleAnimated();
});
panel->chosen(
) | rpl::filter([=](Ui::FilterIcon icon) {
return icon != Ui::ComputeFilterIcon(data->current());
}) | rpl::start_with_next([=](Ui::FilterIcon icon) {
panel->hideAnimated();
const auto rules = data->current();
*data = Data::ChatFilter(
rules.id(),
rules.title(),
Ui::LookupFilterIcon(icon).emoji,
rules.flags(),
rules.always(),
rules.pinned(),
rules.never());
}, panel->lifetime());
const auto updatePanelGeometry = [=] {
const auto global = toggle->mapToGlobal({
toggle->width(),
toggle->height()
});
const auto local = outer->mapFromGlobal(global);
const auto position = st::windwoFilterIconPanelPosition;
const auto padding = panel->innerPadding();
panel->move(
local.x() - panel->width() + position.x() + padding.right(),
local.y() + position.y() - padding.top());
};
const auto filterForGeometry = [=](not_null<QEvent*> event) {
const auto type = event->type();
if (type == QEvent::Move || type == QEvent::Resize) {
// updatePanelGeometry uses not only container geometry, but
// also container children geometries that will be updated later.
crl::on_main(panel, [=] { updatePanelGeometry(); });
}
return base::EventFilterResult::Continue;
};
const auto installFilterForGeometry = [&](not_null<QWidget*> target) {
panel->lifetime().make_state<base::unique_qptr<QObject>>(
base::install_event_filter(target, filterForGeometry));
};
installFilterForGeometry(outer);
installFilterForGeometry(box);
}
} // namespace
void EditFilterBox(
@ -323,18 +422,28 @@ void EditFilterBox(
Fn<void(const Data::ChatFilter &)> doneCallback) {
const auto creating = filter.title().isEmpty();
box->setTitle(creating ? tr::lng_filters_new() : tr::lng_filters_edit());
box->setCloseByOutsideClick(false);
using State = rpl::variable<Data::ChatFilter>;
const auto data = box->lifetime().make_state<State>(filter);
const auto content = box->verticalLayout();
const auto name = content->add(
object_ptr<Ui::InputField>(
box,
st::defaultInputField,
st::windowFilterNameInput,
tr::lng_filters_new_name(),
filter.title()),
data->current().title()),
st::markdownLinkFieldPadding);
name->setMaxLength(kMaxFilterTitleLength);
const auto data = box->lifetime().make_state<Data::ChatFilter>(filter);
const auto outer = box->getDelegate()->outerContainer();
CreateIconSelector(
outer,
box,
content,
name,
data);
constexpr auto kTypes = Flag::Contacts
| Flag::NonContacts
@ -391,8 +500,12 @@ void EditFilterBox(
st::settingsDividerLabelPadding);
const auto refreshPreviews = [=] {
include->updateData(data->flags() & kTypes, data->always());
exclude->updateData(data->flags() & kExcludeTypes, data->never());
include->updateData(
data->current().flags() & kTypes,
data->current().always());
exclude->updateData(
data->current().flags() & kExcludeTypes,
data->current().never());
};
includeAdd->setClickedCallback([=] {
EditExceptions(window, box, kTypes, data, refreshPreviews);
@ -403,26 +516,27 @@ void EditFilterBox(
const auto save = [=] {
const auto title = name->getLastText().trimmed();
const auto rules = data->current();
const auto result = Data::ChatFilter(
rules.id(),
title,
rules.iconEmoji(),
rules.flags(),
rules.always(),
rules.pinned(),
rules.never());
if (title.isEmpty()) {
name->showError();
return;
} else if (!(data->flags() & kTypes) && data->always().empty()) {
} else if (!(rules.flags() & kTypes) && rules.always().empty()) {
window->window().showToast(tr::lng_filters_empty(tr::now));
return;
} else if ((data->flags() == (kTypes | Flag::NoArchived))
&& data->always().empty()
&& data->never().empty()) {
} else if ((rules.flags() == (kTypes | Flag::NoArchived))
&& rules.always().empty()
&& rules.never().empty()) {
window->window().showToast(tr::lng_filters_default(tr::now));
return;
}
const auto result = Data::ChatFilter(
data->id(),
title,
data->iconEmoji(),
data->flags(),
data->always(),
data->pinned(),
data->never());
box->closeBox();
doneCallback(result);

View file

@ -128,7 +128,7 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const {
never.push_back(history->peer->input);
}
return MTP_dialogFilter(
MTP_flags(flags),
MTP_flags(flags | TLFlag::f_emoticon),
MTP_int(replaceId ? replaceId : _id),
MTP_string(_title),
MTP_string(_iconEmoji),
@ -344,7 +344,8 @@ bool ChatFilters::applyChange(ChatFilter &filter, ChatFilter &&updated) {
const auto pinnedChanged = (filter.pinned() != updated.pinned());
if (!rulesChanged
&& !pinnedChanged
&& filter.title() == updated.title()) {
&& filter.title() == updated.title()
&& filter.iconEmoji() == updated.iconEmoji()) {
return false;
}
if (rulesChanged) {

View file

@ -75,6 +75,7 @@ private:
inline bool operator==(const ChatFilter &a, const ChatFilter &b) {
return (a.title() == b.title())
&& (a.iconEmoji() == b.iconEmoji())
&& (a.flags() == b.flags())
&& (a.always() == b.always())
&& (a.never() == b.never());

View file

@ -0,0 +1,455 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/filter_icon_panel.h"
#include "ui/widgets/shadow.h"
#include "ui/image/image_prepare.h"
#include "ui/effects/panel_animation.h"
#include "ui/ui_utility.h"
#include "ui/filter_icons.h"
#include "lang/lang_keys.h"
#include "core/application.h"
#include "app.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_window.h"
namespace Ui {
namespace {
constexpr auto kHideTimeoutMs = crl::time(300);
constexpr auto kDelayedHideTimeoutMs = 3 * crl::time(1000);
constexpr auto kIconsPerRow = 6;
constexpr auto kIcons = std::array{
FilterIcon::Cat,
FilterIcon::Crown,
FilterIcon::Favorite,
FilterIcon::Flower,
FilterIcon::Game,
FilterIcon::Home,
FilterIcon::Love,
FilterIcon::Mask,
FilterIcon::Party,
FilterIcon::Sport,
FilterIcon::Study,
FilterIcon::Trade,
FilterIcon::Travel,
FilterIcon::Work,
FilterIcon::All,
FilterIcon::Unread,
FilterIcon::Unmuted,
FilterIcon::Bots,
FilterIcon::Channels,
FilterIcon::Groups,
FilterIcon::Private,
FilterIcon::Custom,
FilterIcon::Setup,
};
} // namespace
FilterIconPanel::FilterIconPanel(QWidget *parent)
: RpWidget(parent)
, _inner(Ui::CreateChild<Ui::RpWidget>(this)) {
setup();
}
FilterIconPanel::~FilterIconPanel() {
hideFast();
}
rpl::producer<FilterIcon> FilterIconPanel::chosen() const {
return _chosen.events();
}
void FilterIconPanel::setup() {
setupInner();
resize(_inner->rect().marginsAdded(innerPadding()).size());
_inner->move(innerRect().topLeft());
_hideTimer.setCallback([=] { hideByTimerOrLeave(); });
macWindowDeactivateEvents(
) | rpl::filter([=] {
return !isHidden();
}) | rpl::start_with_next([=] {
hideAnimated();
}, lifetime());
setAttribute(Qt::WA_OpaquePaintEvent, false);
hideChildren();
hide();
}
void FilterIconPanel::setupInner() {
const auto count = kIcons.size();
const auto rows = (count / kIconsPerRow)
+ ((count % kIconsPerRow) ? 1 : 0);
const auto single = st::windowFilterIconSingle;
const auto size = QSize(
single.width() * kIconsPerRow,
single.height() * rows);
const auto full = QRect(QPoint(), size).marginsAdded(
st::windowFilterIconPadding).size();
_inner->resize(full);
_inner->paintRequest(
) | rpl::start_with_next([=](QRect clip) {
auto p = Painter(_inner);
App::roundRect(
p,
_inner->rect(),
st::emojiPanBg,
ImageRoundRadius::Small);
p.setFont(st::emojiPanHeaderFont);
p.setPen(st::emojiPanHeaderFg);
p.drawTextLeft(
st::windowFilterIconHeaderPosition.x(),
st::windowFilterIconHeaderPosition.y(),
_inner->width(),
tr::lng_filters_icon_header(tr::now));
const auto selected = (_pressed >= 0) ? _pressed : _selected;
for (auto i = 0; i != kIcons.size(); ++i) {
const auto rect = countRect(i);
if (!rect.intersects(clip)) {
continue;
}
if (i == selected) {
App::roundRect(
p,
rect,
st::emojiPanHover,
StickerHoverCorners);
}
const auto icon = LookupFilterIcon(kIcons[i]).normal;
icon->paintInCenter(p, rect, st::emojiIconFg->c);
}
}, _inner->lifetime());
_inner->setMouseTracking(true);
_inner->events(
) | rpl::start_with_next([=](not_null<QEvent*> e) {
switch (e->type()) {
case QEvent::Leave: setSelected(-1); break;
case QEvent::MouseMove:
mouseMove(static_cast<QMouseEvent*>(e.get())->pos());
break;
case QEvent::MouseButtonPress:
mousePress(static_cast<QMouseEvent*>(e.get())->button());
break;
case QEvent::MouseButtonRelease:
mouseRelease(static_cast<QMouseEvent*>(e.get())->button());
break;
}
}, _inner->lifetime());
}
void FilterIconPanel::setSelected(int selected) {
if (_selected == selected) {
return;
}
const auto was = (_selected >= 0);
updateRect(_selected);
_selected = selected;
updateRect(_selected);
const auto now = (_selected >= 0);
if (was != now) {
_inner->setCursor(now ? style::cur_pointer : style::cur_default);
}
}
void FilterIconPanel::setPressed(int pressed) {
if (_pressed == pressed) {
return;
}
updateRect(_pressed);
_pressed = pressed;
updateRect(_pressed);
}
QRect FilterIconPanel::countRect(int index) const {
Expects(index >= 0);
const auto row = index / kIconsPerRow;
const auto column = index % kIconsPerRow;
const auto single = st::windowFilterIconSingle;
const auto rect = QRect(
QPoint(column * single.width(), row * single.height()),
single);
const auto padding = st::windowFilterIconPadding;
return rect.translated(padding.left(), padding.top());
}
void FilterIconPanel::updateRect(int index) {
if (index < 0) {
return;
}
_inner->update(countRect(index));
}
void FilterIconPanel::mouseMove(QPoint position) {
const auto padding = st::windowFilterIconPadding;
if (!_inner->rect().marginsRemoved(padding).contains(position)) {
setSelected(-1);
} else {
const auto point = position - QPoint(padding.left(), padding.top());
const auto column = point.x() / st::windowFilterIconSingle.width();
const auto row = point.y() / st::windowFilterIconSingle.height();
const auto index = row * kIconsPerRow + column;
setSelected(index < kIcons.size() ? index : -1);
}
}
void FilterIconPanel::mousePress(Qt::MouseButton button) {
if (button != Qt::LeftButton) {
return;
}
setPressed(_selected);
}
void FilterIconPanel::mouseRelease(Qt::MouseButton button) {
if (button != Qt::LeftButton) {
return;
}
const auto pressed = _pressed;
setPressed(-1);
if (pressed == _selected && pressed >= 0) {
Assert(pressed < kIcons.size());
_chosen.fire_copy(kIcons[pressed]);
}
}
void FilterIconPanel::paintEvent(QPaintEvent *e) {
Painter p(this);
// This call can finish _a_show animation and destroy _showAnimation.
const auto opacityAnimating = _a_opacity.animating();
const auto showAnimating = _a_show.animating();
if (_showAnimation && !showAnimating) {
_showAnimation.reset();
if (!opacityAnimating) {
showChildren();
}
}
if (showAnimating) {
Assert(_showAnimation != nullptr);
if (auto opacity = _a_opacity.value(_hiding ? 0. : 1.)) {
_showAnimation->paintFrame(
p,
0,
0,
width(),
_a_show.value(1.),
opacity);
}
} else if (opacityAnimating) {
p.setOpacity(_a_opacity.value(_hiding ? 0. : 1.));
p.drawPixmap(0, 0, _cache);
} else if (_hiding || isHidden()) {
hideFinished();
} else {
if (!_cache.isNull()) _cache = QPixmap();
Ui::Shadow::paint(
p,
innerRect(),
width(),
st::emojiPanAnimation.shadow);
}
}
void FilterIconPanel::enterEventHook(QEvent *e) {
Core::App().registerLeaveSubscription(this);
showAnimated();
}
void FilterIconPanel::leaveEventHook(QEvent *e) {
Core::App().unregisterLeaveSubscription(this);
if (_a_show.animating() || _a_opacity.animating()) {
hideAnimated();
} else {
_hideTimer.callOnce(kHideTimeoutMs);
}
return TWidget::leaveEventHook(e);
}
void FilterIconPanel::otherEnter() {
showAnimated();
}
void FilterIconPanel::otherLeave() {
if (_a_opacity.animating()) {
hideByTimerOrLeave();
} else {
_hideTimer.callOnce(0);
}
}
void FilterIconPanel::hideFast() {
if (isHidden()) return;
_hideTimer.cancel();
_hiding = false;
_a_opacity.stop();
hideFinished();
}
void FilterIconPanel::opacityAnimationCallback() {
update();
if (!_a_opacity.animating()) {
if (_hiding) {
_hiding = false;
hideFinished();
} else if (!_a_show.animating()) {
showChildren();
}
}
}
void FilterIconPanel::hideByTimerOrLeave() {
if (isHidden()) {
return;
}
hideAnimated();
}
void FilterIconPanel::prepareCacheFor(bool hiding) {
if (_a_opacity.animating()) {
return;
}
auto showAnimation = base::take(_a_show);
auto showAnimationData = base::take(_showAnimation);
_hiding = false;
showChildren();
_cache = Ui::GrabWidget(this);
_a_show = base::take(showAnimation);
_showAnimation = base::take(showAnimationData);
_hiding = hiding;
if (_a_show.animating()) {
hideChildren();
}
}
void FilterIconPanel::startOpacityAnimation(bool hiding) {
prepareCacheFor(hiding);
hideChildren();
_a_opacity.start(
[=] { opacityAnimationCallback(); },
_hiding ? 1. : 0.,
_hiding ? 0. : 1.,
st::emojiPanDuration);
}
void FilterIconPanel::startShowAnimation() {
if (!_a_show.animating()) {
auto image = grabForAnimation();
_showAnimation = std::make_unique<Ui::PanelAnimation>(st::emojiPanAnimation, Ui::PanelAnimation::Origin::TopRight);
auto inner = rect().marginsRemoved(st::emojiPanMargins);
_showAnimation->setFinalImage(std::move(image), QRect(inner.topLeft() * cIntRetinaFactor(), inner.size() * cIntRetinaFactor()));
_showAnimation->setCornerMasks(Images::CornersMask(ImageRoundRadius::Small));
_showAnimation->start();
}
hideChildren();
_a_show.start([this] { update(); }, 0., 1., st::emojiPanShowDuration);
}
QImage FilterIconPanel::grabForAnimation() {
auto cache = base::take(_cache);
auto opacityAnimation = base::take(_a_opacity);
auto showAnimationData = base::take(_showAnimation);
auto showAnimation = base::take(_a_show);
showChildren();
Ui::SendPendingMoveResizeEvents(this);
auto result = QImage(
size() * cIntRetinaFactor(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(cRetinaFactor());
result.fill(Qt::transparent);
if (_inner) {
QPainter p(&result);
Ui::RenderWidget(p, _inner, _inner->pos());
}
_a_show = base::take(showAnimation);
_showAnimation = base::take(showAnimationData);
_a_opacity = base::take(opacityAnimation);
_cache = base::take(_cache);
return result;
}
void FilterIconPanel::hideAnimated() {
if (isHidden() || _hiding) {
return;
}
_hideTimer.cancel();
startOpacityAnimation(true);
}
void FilterIconPanel::toggleAnimated() {
if (isHidden() || _hiding || _hideAfterSlide) {
showAnimated();
} else {
hideAnimated();
}
}
void FilterIconPanel::hideFinished() {
hide();
_a_show.stop();
_showAnimation.reset();
_cache = QPixmap();
_hiding = false;
}
void FilterIconPanel::showAnimated() {
_hideTimer.cancel();
_hideAfterSlide = false;
showStarted();
}
void FilterIconPanel::showStarted() {
if (isHidden()) {
raise();
show();
startShowAnimation();
} else if (_hiding) {
startOpacityAnimation(false);
}
}
bool FilterIconPanel::eventFilter(QObject *obj, QEvent *e) {
if (e->type() == QEvent::Enter) {
otherEnter();
} else if (e->type() == QEvent::Leave) {
otherLeave();
}
return false;
}
style::margins FilterIconPanel::innerPadding() const {
return st::emojiPanMargins;
}
QRect FilterIconPanel::innerRect() const {
return rect().marginsRemoved(innerPadding());
}
} // namespace Ui

View file

@ -0,0 +1,87 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
namespace Ui {
enum class FilterIcon : uchar;
class PanelAnimation;
class FilterIconPanel final : public Ui::RpWidget {
public:
FilterIconPanel(QWidget *parent);
~FilterIconPanel();
void hideFast();
[[nodiscard]] bool hiding() const {
return _hiding || _hideTimer.isActive();
}
[[nodiscard]] style::margins innerPadding() const;
void showAnimated();
void hideAnimated();
void toggleAnimated();
[[nodiscard]] rpl::producer<FilterIcon> chosen() const;
private:
void enterEventHook(QEvent *e) override;
void leaveEventHook(QEvent *e) override;
void otherEnter();
void otherLeave();
void paintEvent(QPaintEvent *e) override;
bool eventFilter(QObject *obj, QEvent *e) override;
void setup();
void setupInner();
void hideByTimerOrLeave();
// Rounded rect which has shadow around it.
[[nodiscard]] QRect innerRect() const;
[[nodiscard]] QImage grabForAnimation();
void startShowAnimation();
void startOpacityAnimation(bool hiding);
void prepareCacheFor(bool hiding);
void opacityAnimationCallback();
void hideFinished();
void showStarted();
void setSelected(int selected);
void setPressed(int pressed);
[[nodiscard]] QRect countRect(int index) const;
void updateRect(int index);
void mouseMove(QPoint position);
void mousePress(Qt::MouseButton button);
void mouseRelease(Qt::MouseButton button);
const not_null<Ui::RpWidget*> _inner;
rpl::event_stream<FilterIcon> _chosen;
int _selected = -1;
int _pressed = -1;
std::unique_ptr<Ui::PanelAnimation> _showAnimation;
Ui::Animations::Simple _a_show;
bool _hiding = false;
bool _hideAfterSlide = false;
QPixmap _cache;
Ui::Animations::Simple _a_opacity;
base::Timer _hideTimer;
};
} // namespace Ui

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/filter_icons.h"
#include "ui/emoji_config.h"
#include "data/data_chat_filters.h"
#include "styles/style_filter_icons.h"
namespace Ui {
@ -62,7 +63,7 @@ const auto kIcons = std::vector<FilterIcons>{
{
&st::foldersCat,
&st::foldersCatActive,
"\xF0\x9F\x90\x88"_cs.utf16()
"\xF0\x9F\x90\xB1"_cs.utf16()
},
{
&st::foldersCrown,
@ -155,4 +156,43 @@ std::optional<FilterIcon> LookupFilterIconByEmoji(const QString &emoji) {
return (i != end(kMap)) ? std::make_optional(i->second) : std::nullopt;
}
FilterIcon ComputeDefaultFilterIcon(const Data::ChatFilter &filter) {
using Icon = FilterIcon;
using Flag = Data::ChatFilter::Flag;
const auto all = Flag::Contacts
| Flag::NonContacts
| Flag::Groups
| Flag::Channels
| Flag::Bots;
const auto removed = Flag::NoRead | Flag::NoMuted;
const auto people = Flag::Contacts | Flag::NonContacts;
const auto allNoArchive = all | Flag::NoArchived;
if (!filter.always().empty()
|| !filter.never().empty()
|| !(filter.flags() & all)) {
return Icon::Custom;
} else if ((filter.flags() & all) == Flag::Contacts
|| (filter.flags() & all) == Flag::NonContacts
|| (filter.flags() & all) == people) {
return Icon::Private;
} else if ((filter.flags() & all) == Flag::Groups) {
return Icon::Groups;
} else if ((filter.flags() & all) == Flag::Channels) {
return Icon::Channels;
} else if ((filter.flags() & all) == Flag::Bots) {
return Icon::Bots;
} else if ((filter.flags() & removed) == Flag::NoRead) {
return Icon::Unread;
} else if ((filter.flags() & removed) == Flag::NoMuted) {
return Icon::Unmuted;
}
return Icon::Custom;
}
FilterIcon ComputeFilterIcon(const Data::ChatFilter &filter) {
return LookupFilterIconByEmoji(filter.iconEmoji()).value_or(
ComputeDefaultFilterIcon(filter));
}
} // namespace Ui

View file

@ -13,6 +13,10 @@ class Icon;
} // namespace internal
} // namespace style
namespace Data {
class ChatFilter;
} // namespace Data
namespace Ui {
enum class FilterIcon : uchar {
@ -52,4 +56,8 @@ struct FilterIcons {
[[nodiscard]] std::optional<FilterIcon> LookupFilterIconByEmoji(
const QString &emoji);
[[nodiscard]] FilterIcon ComputeDefaultFilterIcon(
const Data::ChatFilter &filter);
[[nodiscard]] FilterIcon ComputeFilterIcon(const Data::ChatFilter &filter);
} // namespace Ui

View file

@ -284,6 +284,15 @@ windowFilterSmallList: PeerList(defaultPeerList) {
windowFilterSmallRemove: IconButton(notifyClose) {
}
windowFilterSmallRemoveRight: 10px;
windowFilterNameInput: InputField(defaultInputField) {
textMargins: margins(0px, 26px, 36px, 4px);
}
windowFilterIconToggleSize: size(36px, 36px);
windowFilterIconTogglePosition: point(-4px, 12px);
windwoFilterIconPanelPosition: point(-2px, -1px);
windowFilterIconSingle: size(44px, 42px);
windowFilterIconPadding: margins(10px, 36px, 10px, 8px);
windowFilterIconHeaderPosition: point(18px, 14px);
windowFilterTypeContacts: icon {{ "filters_type_contacts", historyPeerUserpicFg }};
windowFilterTypeNonContacts: icon {{ "filters_type_noncontacts", historyPeerUserpicFg }};
windowFilterTypeGroups: icon {{ "filters_type_groups", historyPeerUserpicFg }};

View file

@ -24,39 +24,6 @@ namespace {
using Icon = Ui::FilterIcon;
[[nodiscard]] Icon ComputeIcon(const Data::ChatFilter &filter) {
using Flag = Data::ChatFilter::Flag;
const auto all = Flag::Contacts
| Flag::NonContacts
| Flag::Groups
| Flag::Channels
| Flag::Bots;
const auto removed = Flag::NoRead | Flag::NoMuted;
const auto people = Flag::Contacts | Flag::NonContacts;
const auto allNoArchive = all | Flag::NoArchived;
if (!filter.always().empty()
|| !filter.never().empty()
|| !(filter.flags() & all)) {
return Icon::Custom;
} else if ((filter.flags() & all) == Flag::Contacts
|| (filter.flags() & all) == Flag::NonContacts
|| (filter.flags() & all) == people) {
return Icon::Private;
} else if ((filter.flags() & all) == Flag::Groups) {
return Icon::Groups;
} else if ((filter.flags() & all) == Flag::Channels) {
return Icon::Channels;
} else if ((filter.flags() & all) == Flag::Bots) {
return Icon::Bots;
} else if ((filter.flags() & removed) == Flag::NoRead) {
return Icon::Unread;
} else if ((filter.flags() & removed) == Flag::NoMuted) {
return Icon::Unmuted;
}
return Icon::Custom;
}
} // namespace
FiltersMenu::FiltersMenu(
@ -187,7 +154,7 @@ void FiltersMenu::refresh() {
prepare(
filter.id(),
filter.title(),
ComputeIcon(filter),
Ui::ComputeFilterIcon(filter),
QString());
}
prepare(-1, tr::lng_filters_setup(tr::now), Icon::Setup, {});