/* 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 "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 "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 namespace Ui { namespace { constexpr auto kDisableElement = "disable"_cs; [[nodiscard]] QImage GeneratePreview(not_null theme) { const auto &background = theme->background(); const auto &colors = background.colors; const auto size = st::settingsThemePreviewSize; 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::settingsThemeBubbleSize.width() - st::settingsThemeBubblePosition.x()), st::settingsThemeBubblePosition.y()), st::settingsThemeBubbleSize); const auto received = QRect( st::settingsThemeBubblePosition.x(), sent.y() + sent.height() + st::settingsThemeBubbleSkip, sent.width(), sent.height()); const auto radius = st::settingsThemeBubbleRadius; 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); Images::prepareRound(bubble, corners); p.drawImage(sent, bubble); } else { p.setBrush(theme->palette()->msgOutBg()->c); p.drawRoundedRect(sent, radius, radius); } p.setBrush(theme->palette()->msgInBg()->c); p.drawRoundedRect(received, radius, radius); } Images::prepareRound(result, ImageRoundRadius::Large); return result; } [[nodiscard]] QImage GenerateEmptyPreview() { auto result = QImage( st::settingsThemePreviewSize * 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::normalFont->height / 2; const auto width = st::settingsThemePreviewSize.width(); const auto height = st::settingsThemePreviewSize.height() - top; p.drawText( QRect(0, top, width, height), tr::lng_chat_theme_none(tr::now), style::al_top); } Images::prepareRound(result, ImageRoundRadius::Large); return result; } } // namespace struct ChooseThemeController::Entry { Ui::ChatThemeKey key; std::shared_ptr theme; std::shared_ptr media; QImage preview; EmojiPtr emoji = nullptr; QRect geometry; bool chosen = false; }; ChooseThemeController::ChooseThemeController( not_null parent, not_null window, not_null peer) : _controller(window) , _peer(peer) , _wrap(std::make_unique(parent)) , _topShadow(std::make_unique(parent)) , _content(_wrap->add(object_ptr(_wrap.get()))) , _inner(CreateChild(_content.get())) , _dark(Window::Theme::IsThemeDarkValue()) { init(parent->sizeValue()); } ChooseThemeController::~ChooseThemeController() { _controller->clearPeerThemeOverride(_peer); } void ChooseThemeController::init(rpl::producer 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 skip = st::normalFont->spacew * 4; const auto titleWrap = _wrap->insert( 0, object_ptr( _wrap.get(), skip + st::boxTitle.style.font->height + skip)); auto title = CreateChild( titleWrap, tr::lng_chat_theme_title(), st::boxTitle); _wrap->paintRequest( ) | rpl::start_with_next([=](QRect clip) { QPainter(_wrap.get()).fillRect(clip, st::windowBg); }, lifetime()); initButtons(); initList(); _inner->positionValue( ) | rpl::start_with_next([=](QPoint position) { title->move(std::max(position.x(), skip) + skip, skip); }, 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(_wrap.get())); const auto cancel = CreateChild( controls, tr::lng_cancel(), st::defaultLightButton); const auto apply = CreateChild( controls, tr::lng_chat_theme_apply(), st::defaultActiveButton); const auto skip = st::normalFont->spacew * 2; controls->resize( skip + cancel->width() + skip + apply->width() + skip, apply->height() + skip * 2); rpl::combine( controls->widthValue(), cancel->widthValue(), apply->widthValue() ) | rpl::start_with_next([=]( int outer, int cancelWidth, int applyWidth) { const auto inner = skip + cancelWidth + skip + applyWidth + skip; const auto left = (outer - inner) / 2; cancel->moveToLeft(left, 0); apply->moveToRight(left, 0); }, controls->lifetime()); cancel->setClickedCallback([=] { close(); }); apply->setClickedCallback([=] { if (const auto chosen = findChosen()) { if (Ui::Emoji::Find(_peer->themeEmoji()) != chosen->emoji) { const auto now = chosen->key ? _chosen : QString(); _peer->setThemeEmoji(now); if (chosen->theme) { // Remember while changes propagate through event loop. _controller->pushLastUsedChatTheme(chosen->theme); } const auto api = &_peer->session().api(); api->request(MTPmessages_SetChatTheme( _peer->input, MTP_string(now) )).done([=](const MTPUpdates &result) { api->applyUpdates(result); }).send(); } } _controller->toggleChooseChatTheme(_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::normalFont->spacew * 2); 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::defaultFlatInput.borderWidth; 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(), 8 * st::normalFont->spacew + st::settingsThemePreviewSize.height()); _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.utf16(); } }; _inner->events( ) | rpl::start_with_next([=](not_null event) { const auto type = event->type(); if (type == QEvent::MouseMove) { const auto mouse = static_cast(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(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(event.get()); const auto entry = byPoint(mouse->pos()); const auto chosen = chosenText(entry); if (entry && chosen == _pressed && chosen != _chosen) { clearCurrentBackgroundState(); if (const auto was = findChosen()) { was->chosen = false; } _chosen = chosen; entry->chosen = true; if (entry->theme || !entry->key) { _controller->overridePeerTheme(_peer, entry->theme); } _inner->update(); } _pressed = QString(); } else if (type == QEvent::Wheel) { const auto wheel = static_cast(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 event) { const auto type = event->type(); if (type == QEvent::KeyPress) { const auto key = static_cast(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* { if (_chosen.isEmpty()) { return nullptr; } for (auto &entry : _entries) { if (!entry.key && _chosen == kDisableElement.utf16()) { return &entry; } else if (_chosen == entry.emoji->text()) { return &entry; } } return nullptr; } auto ChooseThemeController::findChosen() const -> const Entry* { return const_cast(this)->findChosen(); } void ChooseThemeController::fill( const std::vector &themes) { if (themes.empty()) { return; } const auto count = int(themes.size()) + 1; const auto single = st::settingsThemePreviewSize; const auto skip = st::normalFont->spacew * 2; const auto full = single.width() * count + skip * (count + 3); _inner->resize(full, skip + single.height() + skip); const auto initial = Ui::Emoji::Find(_peer->themeEmoji()); _dark.value( ) | rpl::start_with_next([=](bool dark) { clearCurrentBackgroundState(); if (_chosen.isEmpty() && initial) { _chosen = initial->text(); } _cachingLifetime.destroy(); const auto old = base::take(_entries); auto x = skip * 2; _entries.push_back({ .preview = GenerateEmptyPreview(), .emoji = Ui::Emoji::Find(QString::fromUtf8("\xe2\x9d\x8c")), .geometry = QRect(QPoint(x, skip), single), .chosen = (_chosen == kDisableElement.utf16()), }); 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 == emoji->text()); _entries.push_back({ .key = key, .emoji = emoji, .geometry = QRect(QPoint(x, skip), single), .chosen = isChosen, }); _controller->cachedChatThemeValue( theme, type ) | rpl::filter([=](const std::shared_ptr &data) { return data && (data->key() == key); }) | rpl::take( 1 ) | rpl::start_with_next([=](std::shared_ptr &&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 == i->emoji->text()) { _controller->overridePeerTheme(_peer, i->theme); } _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 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