| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684 |
- /*
- 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/chat/choose_theme_controller.h"
- #include "boxes/background_box.h"
- #include "ui/rp_widget.h"
- #include "ui/widgets/shadow.h"
- #include "ui/widgets/labels.h"
- #include "ui/widgets/buttons.h"
- #include "ui/chat/chat_theme.h"
- #include "ui/chat/message_bubble.h"
- #include "ui/wrap/vertical_layout.h"
- #include "ui/painter.h"
- #include "main/main_session.h"
- #include "window/window_session_controller.h"
- #include "window/themes/window_theme.h"
- #include "data/data_session.h"
- #include "data/data_peer.h"
- #include "data/data_cloud_themes.h"
- #include "data/data_document.h"
- #include "data/data_document_media.h"
- #include "lang/lang_keys.h"
- #include "apiwrap.h"
- #include "styles/style_widgets.h"
- #include "styles/style_layers.h" // boxTitle.
- #include "styles/style_settings.h"
- #include "styles/style_window.h"
- #include <QtWidgets/QApplication>
- namespace Ui {
- namespace {
- const auto kDisableElement = [] { return u"disable"_q; };
- [[nodiscard]] QImage GeneratePreview(not_null<Ui::ChatTheme*> theme) {
- const auto &background = theme->background();
- const auto &colors = background.colors;
- const auto size = st::chatThemePreviewSize;
- auto prepared = background.prepared;
- const auto paintPattern = [&](QPainter &p, bool inverted) {
- if (prepared.isNull()) {
- return;
- }
- const auto w = prepared.width();
- const auto h = prepared.height();
- const auto scaled = size.scaled(
- st::windowMinWidth / 2,
- st::windowMinHeight / 2,
- Qt::KeepAspectRatio);
- const auto use = (scaled.width() > w || scaled.height() > h)
- ? scaled.scaled({ w, h }, Qt::KeepAspectRatio)
- : scaled;
- const auto good = QSize(
- std::max(use.width(), 1),
- std::max(use.height(), 1));
- auto small = prepared.copy(QRect(
- QPoint(
- (w - good.width()) / 2,
- (h - good.height()) / 2),
- good));
- if (inverted) {
- small = Ui::InvertPatternImage(std::move(small));
- }
- p.drawImage(
- QRect(QPoint(), size * style::DevicePixelRatio()),
- small);
- };
- const auto fullsize = size * style::DevicePixelRatio();
- auto result = background.waitingForNegativePattern()
- ? QImage(
- fullsize,
- QImage::Format_ARGB32_Premultiplied)
- : Ui::GenerateBackgroundImage(
- fullsize,
- colors.empty() ? std::vector{ 1, QColor(0, 0, 0) } : colors,
- background.gradientRotation,
- background.patternOpacity,
- paintPattern);
- if (background.waitingForNegativePattern()) {
- result.fill(Qt::black);
- }
- result.setDevicePixelRatio(style::DevicePixelRatio());
- {
- auto p = QPainter(&result);
- const auto sent = QRect(
- QPoint(
- (size.width()
- - st::chatThemeBubbleSize.width()
- - st::chatThemeBubblePosition.x()),
- st::chatThemeBubblePosition.y()),
- st::chatThemeBubbleSize);
- const auto received = QRect(
- st::chatThemeBubblePosition.x(),
- sent.y() + sent.height() + st::chatThemeBubbleSkip,
- sent.width(),
- sent.height());
- const auto radius = st::chatThemeBubbleRadius;
- PainterHighQualityEnabler hq(p);
- p.setPen(Qt::NoPen);
- if (const auto pattern = theme->bubblesBackgroundPattern()) {
- auto bubble = pattern->pixmap.toImage().scaled(
- sent.size() * style::DevicePixelRatio(),
- Qt::IgnoreAspectRatio,
- Qt::SmoothTransformation
- ).convertToFormat(QImage::Format_ARGB32_Premultiplied);
- const auto corners = Images::CornersMask(radius);
- p.drawImage(sent, Images::Round(std::move(bubble), corners));
- } else {
- p.setBrush(theme->palette()->msgOutBg()->c);
- p.drawRoundedRect(sent, radius, radius);
- }
- p.setBrush(theme->palette()->msgInBg()->c);
- p.drawRoundedRect(received, radius, radius);
- }
- return Images::Round(std::move(result), ImageRoundRadius::Large);
- }
- [[nodiscard]] QImage GenerateEmptyPreview() {
- auto result = QImage(
- st::chatThemePreviewSize * style::DevicePixelRatio(),
- QImage::Format_ARGB32_Premultiplied);
- result.fill(st::settingsThemeNotSupportedBg->c);
- result.setDevicePixelRatio(style::DevicePixelRatio());
- {
- auto p = QPainter(&result);
- p.setPen(st::menuIconFg);
- p.setFont(st::semiboldFont);
- const auto top = st::chatThemeEmptyPreviewTop;
- const auto width = st::chatThemePreviewSize.width();
- const auto height = st::chatThemePreviewSize.height() - top;
- p.drawText(
- QRect(0, top, width, height),
- tr::lng_chat_theme_none(tr::now),
- style::al_top);
- }
- return Images::Round(std::move(result), ImageRoundRadius::Large);
- }
- } // namespace
- struct ChooseThemeController::Entry {
- Ui::ChatThemeKey key;
- std::shared_ptr<Ui::ChatTheme> theme;
- std::shared_ptr<Data::DocumentMedia> media;
- QImage preview;
- EmojiPtr emoji = nullptr;
- QRect geometry;
- bool chosen = false;
- };
- ChooseThemeController::ChooseThemeController(
- not_null<RpWidget*> parent,
- not_null<Window::SessionController*> window,
- not_null<PeerData*> peer)
- : _controller(window)
- , _peer(peer)
- , _wrap(std::make_unique<VerticalLayout>(parent))
- , _topShadow(std::make_unique<PlainShadow>(parent))
- , _content(_wrap->add(object_ptr<RpWidget>(_wrap.get())))
- , _inner(CreateChild<RpWidget>(_content.get()))
- , _disabledEmoji(Ui::Emoji::Find(QString::fromUtf8("\xe2\x9d\x8c")))
- , _dark(Window::Theme::IsThemeDarkValue()) {
- init(parent->sizeValue());
- }
- ChooseThemeController::~ChooseThemeController() {
- _controller->clearPeerThemeOverride(_peer);
- }
- void ChooseThemeController::init(rpl::producer<QSize> outer) {
- using namespace rpl::mappers;
- const auto themes = &_controller->session().data().cloudThemes();
- const auto &list = themes->chatThemes();
- if (!list.empty()) {
- fill(list);
- } else {
- themes->refreshChatThemes();
- themes->chatThemesUpdated(
- ) | rpl::take(1) | rpl::start_with_next([=] {
- fill(themes->chatThemes());
- }, lifetime());
- }
- const auto titleWrap = _wrap->insert(
- 0,
- object_ptr<FixedHeightWidget>(
- _wrap.get(),
- st::boxTitle.style.font->height),
- st::chatThemeTitlePadding);
- auto title = CreateChild<FlatLabel>(
- titleWrap,
- tr::lng_chat_theme_title(),
- st::boxTitle);
- _wrap->paintRequest(
- ) | rpl::start_with_next([=](QRect clip) {
- QPainter(_wrap.get()).fillRect(clip, st::windowBg);
- }, lifetime());
- const auto close = Ui::CreateChild<Ui::IconButton>(
- _wrap.get(),
- st::boxTitleClose);
- close->setClickedCallback([=] { this->close(); });
- rpl::combine(
- _wrap->widthValue(),
- titleWrap->positionValue()
- ) | rpl::start_with_next([=](int width, QPoint position) {
- close->moveToRight(0, 0, width);
- }, close->lifetime());
- initButtons();
- initList();
- _inner->positionValue(
- ) | rpl::start_with_next([=](QPoint position) {
- title->move(std::max(position.x(), 0), 0);
- }, title->lifetime());
- std::move(
- outer
- ) | rpl::start_with_next([=](QSize outer) {
- _wrap->resizeToWidth(outer.width());
- _wrap->move(0, outer.height() - _wrap->height());
- const auto line = st::lineWidth;
- _topShadow->setGeometry(0, _wrap->y() - line, outer.width(), line);
- }, lifetime());
- rpl::combine(
- _shouldBeShown.value(),
- _forceHidden.value(),
- _1 && !_2
- ) | rpl::start_with_next([=](bool shown) {
- _wrap->setVisible(shown);
- _topShadow->setVisible(shown);
- }, lifetime());
- }
- void ChooseThemeController::initButtons() {
- const auto controls = _wrap->add(object_ptr<RpWidget>(_wrap.get()));
- const auto apply = CreateChild<RoundButton>(
- controls,
- tr::lng_chat_theme_apply(),
- st::defaultLightButton);
- apply->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
- const auto choose = CreateChild<RoundButton>(
- controls,
- tr::lng_chat_theme_change_wallpaper(),
- st::defaultLightButton);
- choose->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
- const auto &margin = st::chatThemeButtonMargin;
- controls->resize(
- margin.left() + choose->width() + margin.right(),
- margin.top() + choose->height() + margin.bottom());
- rpl::combine(
- controls->widthValue(),
- apply->widthValue(),
- choose->widthValue(),
- _chosen.value()
- ) | rpl::start_with_next([=](
- int outer,
- int applyWidth,
- int chooseWidth,
- QString chosen) {
- const auto was = _peer->themeEmoji();
- const auto now = (chosen == kDisableElement()) ? QString() : chosen;
- const auto changed = (now != was);
- apply->setVisible(changed);
- choose->setVisible(!changed);
- const auto shown = changed ? apply : choose;
- const auto shownWidth = changed ? applyWidth : chooseWidth;
- const auto inner = margin.left() + shownWidth + margin.right();
- const auto left = (outer - inner) / 2;
- shown->moveToLeft(left, margin.top());
- }, controls->lifetime());
- apply->setClickedCallback([=] {
- if (const auto chosen = findChosen()) {
- const auto was = _peer->themeEmoji();
- const auto now = chosen->key ? _chosen.current() : QString();
- if (was != now) {
- _peer->setThemeEmoji(now);
- const auto dropWallPaper = (_peer->wallPaper() != nullptr);
- if (dropWallPaper) {
- _peer->setWallPaper({});
- }
- if (chosen->theme) {
- // Remember while changes propagate through event loop.
- _controller->pushLastUsedChatTheme(chosen->theme);
- }
- const auto api = &_peer->session().api();
- api->request(MTPmessages_SetChatWallPaper(
- MTP_flags(0),
- _peer->input,
- MTPInputWallPaper(),
- MTPWallPaperSettings(),
- MTPint()
- )).afterDelay(10).done([=](const MTPUpdates &result) {
- api->applyUpdates(result);
- }).send();
- api->request(MTPmessages_SetChatTheme(
- _peer->input,
- MTP_string(now)
- )).done([=](const MTPUpdates &result) {
- api->applyUpdates(result);
- }).send();
- }
- }
- _controller->toggleChooseChatTheme(_peer);
- });
- choose->setClickedCallback([=] {
- _controller->show(Box<BackgroundBox>(_controller, _peer));
- });
- }
- void ChooseThemeController::paintEntry(QPainter &p, const Entry &entry) {
- const auto geometry = entry.geometry;
- p.drawImage(geometry, entry.preview);
- const auto size = Ui::Emoji::GetSizeLarge();
- const auto factor = style::DevicePixelRatio();
- const auto emojiLeft = geometry.x()
- + (geometry.width() - (size / factor)) / 2;
- const auto emojiTop = geometry.y()
- + geometry.height()
- - (size / factor)
- - st::chatThemeEmojiBottom;
- Ui::Emoji::Draw(p, entry.emoji, size, emojiLeft, emojiTop);
- if (entry.chosen) {
- auto hq = PainterHighQualityEnabler(p);
- auto pen = st::activeLineFg->p;
- const auto width = st::defaultInputField.borderActive;
- pen.setWidth(width);
- p.setPen(pen);
- const auto add = st::lineWidth + width;
- p.drawRoundedRect(
- entry.geometry.marginsAdded({ add, add, add, add }),
- st::roundRadiusLarge + add,
- st::roundRadiusLarge + add);
- }
- }
- void ChooseThemeController::initList() {
- _content->resize(
- _content->width(),
- (st::chatThemeEntryMargin.top()
- + st::chatThemePreviewSize.height()
- + st::chatThemeEntryMargin.bottom()));
- _inner->setMouseTracking(true);
- _inner->paintRequest(
- ) | rpl::start_with_next([=](QRect clip) {
- auto p = QPainter(_inner.get());
- for (const auto &entry : _entries) {
- if (entry.preview.isNull() || !clip.intersects(entry.geometry)) {
- continue;
- }
- paintEntry(p, entry);
- }
- }, lifetime());
- const auto byPoint = [=](QPoint position) -> Entry* {
- for (auto &entry : _entries) {
- if (entry.geometry.contains(position)) {
- return &entry;
- }
- }
- return nullptr;
- };
- const auto chosenText = [=](const Entry *entry) {
- if (!entry) {
- return QString();
- } else if (entry->key) {
- return entry->emoji->text();
- } else {
- return kDisableElement();
- }
- };
- _inner->events(
- ) | rpl::start_with_next([=](not_null<QEvent*> event) {
- const auto type = event->type();
- if (type == QEvent::MouseMove) {
- const auto mouse = static_cast<QMouseEvent*>(event.get());
- const auto skip = _inner->width() - _content->width();
- if (skip <= 0) {
- _dragStartPosition = _pressPosition = std::nullopt;
- } else if (_pressPosition.has_value()
- && ((mouse->globalPos() - *_pressPosition).manhattanLength()
- >= QApplication::startDragDistance())) {
- _dragStartPosition = base::take(_pressPosition);
- _dragStartInnerLeft = _inner->x();
- }
- if (_dragStartPosition.has_value()) {
- const auto shift = mouse->globalPos().x()
- - _dragStartPosition->x();
- updateInnerLeft(_dragStartInnerLeft + shift);
- } else {
- _inner->setCursor(byPoint(mouse->pos())
- ? style::cur_pointer
- : style::cur_default);
- }
- } else if (type == QEvent::MouseButtonPress) {
- const auto mouse = static_cast<QMouseEvent*>(event.get());
- if (mouse->button() == Qt::LeftButton) {
- _pressPosition = mouse->globalPos();
- }
- _pressed = chosenText(byPoint(mouse->pos()));
- } else if (type == QEvent::MouseButtonRelease) {
- _pressPosition = _dragStartPosition = std::nullopt;
- const auto mouse = static_cast<QMouseEvent*>(event.get());
- const auto entry = byPoint(mouse->pos());
- const auto chosen = chosenText(entry);
- if (entry && chosen == _pressed && chosen != _chosen.current()) {
- clearCurrentBackgroundState();
- if (const auto was = findChosen()) {
- was->chosen = false;
- }
- _chosen = chosen;
- entry->chosen = true;
- if (entry->theme || !entry->key) {
- _controller->overridePeerTheme(
- _peer,
- entry->theme,
- entry->emoji);
- }
- _inner->update();
- }
- _pressed = QString();
- } else if (type == QEvent::Wheel) {
- const auto wheel = static_cast<QWheelEvent*>(event.get());
- const auto was = _inner->x();
- updateInnerLeft((wheel->angleDelta().x() != 0)
- ? (was + (wheel->pixelDelta().x()
- ? wheel->pixelDelta().x()
- : wheel->angleDelta().x()))
- : (wheel->angleDelta().y() != 0)
- ? (was + (wheel->pixelDelta().y()
- ? wheel->pixelDelta().y()
- : wheel->angleDelta().y()))
- : was);
- }
- }, lifetime());
- _content->events(
- ) | rpl::start_with_next([=](not_null<QEvent*> event) {
- const auto type = event->type();
- if (type == QEvent::KeyPress) {
- const auto key = static_cast<QKeyEvent*>(event.get());
- if (key->key() == Qt::Key_Escape) {
- close();
- }
- }
- }, lifetime());
- rpl::combine(
- _content->widthValue(),
- _inner->widthValue()
- ) | rpl::start_with_next([=](int content, int inner) {
- if (!content || !inner) {
- return;
- } else if (!_entries.empty() && !_initialInnerLeftApplied) {
- applyInitialInnerLeft();
- } else {
- updateInnerLeft(_inner->x());
- }
- }, lifetime());
- }
- void ChooseThemeController::applyInitialInnerLeft() {
- if (const auto chosen = findChosen()) {
- updateInnerLeft(
- _content->width() / 2 - chosen->geometry.center().x());
- }
- _initialInnerLeftApplied = true;
- }
- void ChooseThemeController::updateInnerLeft(int now) {
- const auto skip = _content->width() - _inner->width();
- const auto clamped = (skip >= 0)
- ? (skip / 2)
- : std::clamp(now, skip, 0);
- _inner->move(clamped, 0);
- }
- void ChooseThemeController::close() {
- if (const auto chosen = findChosen()) {
- if (Ui::Emoji::Find(_peer->themeEmoji()) != chosen->emoji) {
- clearCurrentBackgroundState();
- }
- }
- _controller->toggleChooseChatTheme(_peer);
- }
- void ChooseThemeController::clearCurrentBackgroundState() {
- if (const auto entry = findChosen()) {
- if (entry->theme) {
- entry->theme->clearBackgroundState();
- }
- }
- }
- auto ChooseThemeController::findChosen() -> Entry* {
- const auto chosen = _chosen.current();
- if (chosen.isEmpty()) {
- return nullptr;
- }
- for (auto &entry : _entries) {
- if (!entry.key && chosen == kDisableElement()) {
- return &entry;
- } else if (chosen == entry.emoji->text()) {
- return &entry;
- }
- }
- return nullptr;
- }
- auto ChooseThemeController::findChosen() const -> const Entry* {
- return const_cast<ChooseThemeController*>(this)->findChosen();
- }
- void ChooseThemeController::fill(
- const std::vector<Data::CloudTheme> &themes) {
- if (themes.empty()) {
- return;
- }
- const auto count = int(themes.size()) + 1;
- const auto single = st::chatThemePreviewSize;
- const auto skip = st::chatThemeEntrySkip;
- const auto &margin = st::chatThemeEntryMargin;
- const auto full = margin.left()
- + single.width() * count
- + skip * (count - 1)
- + margin.right();
- _inner->resize(
- full,
- margin.top() + single.height() + margin.bottom());
- const auto initial = Ui::Emoji::Find(_peer->themeEmoji());
- if (!initial) {
- _chosen = kDisableElement();
- }
- _dark.value(
- ) | rpl::start_with_next([=](bool dark) {
- clearCurrentBackgroundState();
- if (_chosen.current().isEmpty() && initial) {
- _chosen = initial->text();
- }
- _cachingLifetime.destroy();
- const auto old = base::take(_entries);
- auto x = margin.left();
- _entries.push_back({
- .preview = GenerateEmptyPreview(),
- .emoji = _disabledEmoji,
- .geometry = QRect(QPoint(x, margin.top()), single),
- .chosen = (_chosen.current() == kDisableElement()),
- });
- Assert(_entries.front().emoji != nullptr);
- style::PaletteChanged(
- ) | rpl::start_with_next([=] {
- _entries.front().preview = GenerateEmptyPreview();
- }, _cachingLifetime);
- const auto type = dark
- ? Data::CloudThemeType::Dark
- : Data::CloudThemeType::Light;
- x += single.width() + skip;
- for (const auto &theme : themes) {
- const auto emoji = Ui::Emoji::Find(theme.emoticon);
- if (!emoji || !theme.settings.contains(type)) {
- continue;
- }
- const auto key = ChatThemeKey{ theme.id, dark };
- const auto isChosen = (_chosen.current() == emoji->text());
- _entries.push_back({
- .key = key,
- .emoji = emoji,
- .geometry = QRect(QPoint(x, skip), single),
- .chosen = isChosen,
- });
- _controller->cachedChatThemeValue(
- theme,
- Data::WallPaper(0),
- type
- ) | rpl::filter([=](const std::shared_ptr<ChatTheme> &data) {
- return data && (data->key() == key);
- }) | rpl::take(
- 1
- ) | rpl::start_with_next([=](std::shared_ptr<ChatTheme> &&data) {
- const auto key = data->key();
- const auto i = ranges::find(_entries, key, &Entry::key);
- if (i == end(_entries)) {
- return;
- }
- const auto theme = data.get();
- i->theme = std::move(data);
- i->preview = GeneratePreview(theme);
- if (_chosen.current() == i->emoji->text()) {
- _controller->overridePeerTheme(
- _peer,
- i->theme,
- i->emoji);
- }
- _inner->update();
- if (!theme->background().isPattern
- || !theme->background().prepared.isNull()) {
- return;
- }
- // Subscribe to pattern loading if needed.
- theme->repaintBackgroundRequests(
- ) | rpl::filter([=] {
- const auto i = ranges::find(
- _entries,
- key,
- &Entry::key);
- return (i == end(_entries))
- || !i->theme->background().prepared.isNull();
- }) | rpl::take(1) | rpl::start_with_next([=] {
- const auto i = ranges::find(
- _entries,
- key,
- &Entry::key);
- if (i == end(_entries)) {
- return;
- }
- i->preview = GeneratePreview(theme);
- _inner->update();
- }, _cachingLifetime);
- }, _cachingLifetime);
- x += single.width() + skip;
- }
- if (!_initialInnerLeftApplied && _content->width() > 0) {
- applyInitialInnerLeft();
- }
- }, lifetime());
- _shouldBeShown = true;
- }
- bool ChooseThemeController::shouldBeShown() const {
- return _shouldBeShown.current();
- }
- rpl::producer<bool> ChooseThemeController::shouldBeShownValue() const {
- return _shouldBeShown.value();
- }
- int ChooseThemeController::height() const {
- return shouldBeShown() ? _wrap->height() : 0;
- }
- void ChooseThemeController::hide() {
- _forceHidden = true;
- }
- void ChooseThemeController::show() {
- _forceHidden = false;
- }
- void ChooseThemeController::raise() {
- _wrap->raise();
- _topShadow->raise();
- }
- void ChooseThemeController::setFocus() {
- _content->setFocus();
- }
- rpl::lifetime &ChooseThemeController::lifetime() {
- return _wrap->lifetime();
- }
- } // namespace Ui
|