/* 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 "boxes/background_preview_box.h" #include "lang/lang_keys.h" #include "mainwidget.h" #include "window/themes/window_theme.h" #include "ui/chat/chat_theme.h" #include "ui/chat/chat_style.h" #include "ui/toast/toast.h" #include "ui/image/image.h" #include "ui/widgets/checkbox.h" #include "ui/ui_utility.h" #include "history/history.h" #include "history/history_message.h" #include "history/view/history_view_message.h" #include "main/main_session.h" #include "apiwrap.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_document_resolver.h" #include "data/data_file_origin.h" #include "base/unixtime.h" #include "boxes/confirm_box.h" #include "boxes/background_preview_box.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include #include namespace { constexpr auto kMaxWallPaperSlugLength = 255; class ServiceCheck : public Ui::AbstractCheckView { public: ServiceCheck(const style::ServiceCheck &st, bool checked); QSize getSize() const override; void paint( Painter &p, int left, int top, int outerWidth) override; QImage prepareRippleMask() const override; bool checkRippleStartPosition(QPoint position) const override; private: class Generator { public: Generator(); void paintFrame( Painter &p, int left, int top, not_null st, float64 toggled); void invalidate(); private: struct Frames { QImage image; std::vector ready; }; not_null framesForStyle( not_null st); static void FillFrame( QImage &image, not_null st, int index, int count); static void PaintFillingFrame( Painter &p, not_null st, float64 progress); static void PaintCheckingFrame( Painter &p, not_null st, float64 progress); base::flat_map, Frames> _data; rpl::lifetime _lifetime; }; static Generator &Frames(); const style::ServiceCheck &_st; }; ServiceCheck::Generator::Generator() { style::PaletteChanged( ) | rpl::start_with_next([=] { invalidate(); }, _lifetime); } auto ServiceCheck::Generator::framesForStyle( not_null st) -> not_null { if (const auto i = _data.find(st); i != _data.end()) { return &i->second; } const auto result = &_data.emplace(st, Frames()).first->second; const auto size = st->diameter; const auto count = (st->duration / AnimationTimerDelta) + 2; result->image = QImage( QSize(count * size, size) * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied); result->image.fill(Qt::transparent); result->image.setDevicePixelRatio(cRetinaFactor()); result->ready.resize(count); return result; } void ServiceCheck::Generator::FillFrame( QImage &image, not_null st, int index, int count) { Expects(count > 1); Expects(index >= 0 && index < count); Painter p(&image); PainterHighQualityEnabler hq(p); p.translate(index * st->diameter, 0); const auto progress = index / float64(count - 1); if (progress > 0.5) { PaintCheckingFrame(p, st, (progress - 0.5) * 2); } else { PaintFillingFrame(p, st, progress * 2); } } void ServiceCheck::Generator::PaintFillingFrame( Painter &p, not_null st, float64 progress) { const auto shift = progress * st->shift; p.setBrush(st->color); p.setPen(Qt::NoPen); p.drawEllipse(QRectF( shift, shift, st->diameter - 2 * shift, st->diameter - 2 * shift)); if (progress < 1.) { const auto remove = progress * (st->diameter / 2. - st->thickness); p.setCompositionMode(QPainter::CompositionMode_Source); p.setPen(Qt::NoPen); p.setBrush(Qt::transparent); p.drawEllipse(QRectF( st->thickness + remove, st->thickness + remove, st->diameter - 2 * (st->thickness + remove), st->diameter - 2 * (st->thickness + remove))); } } void ServiceCheck::Generator::PaintCheckingFrame( Painter &p, not_null st, float64 progress) { const auto shift = (1. - progress) * st->shift; p.setBrush(st->color); p.setPen(Qt::NoPen); p.drawEllipse(QRectF( shift, shift, st->diameter - 2 * shift, st->diameter - 2 * shift)); if (progress > 0.) { const auto tip = QPointF(st->tip.x(), st->tip.y()); const auto left = tip - QPointF(st->small, st->small) * progress; const auto right = tip - QPointF(-st->large, st->large) * progress; p.setCompositionMode(QPainter::CompositionMode_Source); p.setBrush(Qt::NoBrush); auto pen = QPen(Qt::transparent); pen.setWidth(st->stroke); pen.setCapStyle(Qt::RoundCap); pen.setJoinStyle(Qt::RoundJoin); p.setPen(pen); auto path = QPainterPath(); path.moveTo(left); path.lineTo(tip); path.lineTo(right); p.drawPath(path); } } void ServiceCheck::Generator::paintFrame( Painter &p, int left, int top, not_null st, float64 toggled) { const auto frames = framesForStyle(st); auto &image = frames->image; const auto count = int(frames->ready.size()); const auto index = int(std::round(toggled * (count - 1))); Assert(index >= 0 && index < count); if (!frames->ready[index]) { frames->ready[index] = true; FillFrame(image, st, index, count); } const auto size = st->diameter; const auto part = size * cIntRetinaFactor(); p.drawImage( QPoint(left, top), image, QRect(index * part, 0, part, part)); } void ServiceCheck::Generator::invalidate() { _data.clear(); } ServiceCheck::Generator &ServiceCheck::Frames() { static const auto Instance = Ui::CreateChild( QCoreApplication::instance()); return *Instance; } ServiceCheck::ServiceCheck( const style::ServiceCheck &st, bool checked) : AbstractCheckView(st.duration, checked, nullptr) , _st(st) { } QSize ServiceCheck::getSize() const { const auto inner = QRect(0, 0, _st.diameter, _st.diameter); return inner.marginsAdded(_st.margin).size(); } void ServiceCheck::paint( Painter &p, int left, int top, int outerWidth) { Frames().paintFrame( p, left + _st.margin.left(), top + _st.margin.top(), &_st, currentAnimationValue()); } QImage ServiceCheck::prepareRippleMask() const { return QImage(); } bool ServiceCheck::checkRippleStartPosition(QPoint position) const { return false; } [[nodiscard]] bool IsValidWallPaperSlug(const QString &slug) { if (slug.isEmpty() || slug.size() > kMaxWallPaperSlugLength) { return false; } return ranges::none_of(slug, [](QChar ch) { return (ch != '.') && (ch != '_') && (ch != '-') && (ch < '0' || ch > '9') && (ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z'); }); } [[nodiscard]] AdminLog::OwnedItem GenerateTextItem( not_null delegate, not_null history, const QString &text, bool out) { Expects(history->peer->isUser()); static auto id = ServerMaxMsgId + (ServerMaxMsgId / 3); const auto flags = MessageFlag::FakeHistoryItem | MessageFlag::HasFromId | (out ? MessageFlag::Outgoing : MessageFlag(0)); const auto replyTo = MsgId(); const auto viaBotId = UserId(); const auto groupedId = uint64(); const auto item = history->makeMessage( ++id, flags, replyTo, viaBotId, base::unixtime::now(), out ? history->session().userId() : peerToUser(history->peer->id), QString(), TextWithEntities{ TextUtilities::Clean(text) }, MTP_messageMediaEmpty(), MTPReplyMarkup(), groupedId); return AdminLog::OwnedItem(delegate, item); } [[nodiscard]] QImage PrepareScaledNonPattern( const QImage &image, Images::Option blur) { const auto size = st::boxWideWidth; const auto width = std::max(image.width(), 1); const auto height = std::max(image.height(), 1); const auto takeWidth = (width > height) ? (width * size / height) : size; const auto takeHeight = (width > height) ? size : (height * size / width); return Images::prepare( image, takeWidth * cIntRetinaFactor(), takeHeight * cIntRetinaFactor(), Images::Option::Smooth | Images::Option::TransparentBackground | blur, size, size); } [[nodiscard]] QImage PrepareScaledFromFull( const QImage &image, bool isPattern, const std::vector &background, int gradientRotation, float64 patternOpacity, Images::Option blur = Images::Option(0)) { auto result = PrepareScaledNonPattern(image, blur); if (isPattern) { result = Ui::PreparePatternImage( std::move(result), background, gradientRotation, patternOpacity); } return std::move(result).convertToFormat( QImage::Format_ARGB32_Premultiplied); } [[nodiscard]] QImage BlackImage(QSize size) { auto result = QImage(size, QImage::Format_ARGB32_Premultiplied); result.fill(Qt::black); return result; } } // namespace BackgroundPreviewBox::BackgroundPreviewBox( QWidget*, not_null controller, const Data::WallPaper &paper) : SimpleElementDelegate(controller, [=] { update(); }) , _controller(controller) , _chatStyle(std::make_unique()) , _text1(GenerateTextItem( delegate(), _controller->session().data().history(PeerData::kServiceNotificationsId), tr::lng_background_text1(tr::now), false)) , _text2(GenerateTextItem( delegate(), _controller->session().data().history(PeerData::kServiceNotificationsId), tr::lng_background_text2(tr::now), true)) , _paper(paper) , _media(_paper.document() ? _paper.document()->createMediaView() : nullptr) , _radial([=](crl::time now) { radialAnimationCallback(now); }) { _chatStyle->apply(controller->defaultChatTheme().get()); if (_media) { _media->thumbnailWanted(_paper.fileOrigin()); } generateBackground(); _controller->session().downloaderTaskFinished( ) | rpl::start_with_next([=] { update(); }, lifetime()); } void BackgroundPreviewBox::generateBackground() { if (_paper.backgroundColors().empty()) { return; } const auto size = QSize(st::boxWideWidth, st::boxWideWidth) * cIntRetinaFactor(); _generated = Ui::PixmapFromImage((_paper.patternOpacity() >= 0.) ? Ui::GenerateBackgroundImage( size, _paper.backgroundColors(), _paper.gradientRotation()) : BlackImage(size)); _generated.setDevicePixelRatio(cRetinaFactor()); } not_null BackgroundPreviewBox::delegate() { return static_cast(this); } void BackgroundPreviewBox::prepare() { setTitle(tr::lng_background_header()); addButton(tr::lng_background_apply(), [=] { apply(); }); addButton(tr::lng_cancel(), [=] { closeBox(); }); if (_paper.hasShareUrl()) { addLeftButton(tr::lng_background_share(), [=] { share(); }); } updateServiceBg(_paper.backgroundColors()); _paper.loadDocument(); const auto document = _paper.document(); if (document && document->loading()) { _radial.start(_media->progress()); } if (!_paper.isPattern() && (_paper.localThumbnail() || (document && document->hasThumbnail()))) { createBlurCheckbox(); } setScaledFromThumb(); checkLoadedDocument(); _text1->setDisplayDate(true); _text1->initDimensions(); _text1->resizeGetHeight(st::boxWideWidth); _text2->initDimensions(); _text2->resizeGetHeight(st::boxWideWidth); setDimensions(st::boxWideWidth, st::boxWideWidth); } void BackgroundPreviewBox::createBlurCheckbox() { _blur.create( this, tr::lng_background_blur(tr::now), st::backgroundCheckbox, std::make_unique( st::backgroundCheck, _paper.isBlurred())); rpl::combine( sizeValue(), _blur->sizeValue() ) | rpl::start_with_next([=](QSize outer, QSize inner) { _blur->move( (outer.width() - inner.width()) / 2, outer.height() - st::historyPaddingBottom - inner.height()); }, _blur->lifetime()); _blur->paintRequest( ) | rpl::filter([=] { return _serviceBg.has_value(); }) | rpl::start_with_next([=] { Painter p(_blur.data()); PainterHighQualityEnabler hq(p); p.setPen(Qt::NoPen); p.setBrush(*_serviceBg); p.drawRoundedRect( _blur->rect(), st::historyMessageRadius, st::historyMessageRadius); }, _blur->lifetime()); _blur->checkedChanges( ) | rpl::start_with_next([=](bool checked) { checkBlurAnimationStart(); update(); }, lifetime()); _blur->setDisabled(true); } void BackgroundPreviewBox::apply() { const auto install = (_paper.id() != Window::Theme::Background()->id()) && Data::IsCloudWallPaper(_paper); _controller->content()->setChatBackground(_paper, std::move(_full)); if (install) { _controller->session().api().request(MTPaccount_InstallWallPaper( _paper.mtpInput(&_controller->session()), _paper.mtpSettings() )).send(); } closeBox(); } void BackgroundPreviewBox::share() { QGuiApplication::clipboard()->setText( _paper.shareUrl(&_controller->session())); Ui::Toast::Show(tr::lng_background_link_copied(tr::now)); } void BackgroundPreviewBox::paintEvent(QPaintEvent *e) { Painter p(this); const auto ms = crl::now(); if (_scaled.isNull()) { setScaledFromThumb(); } if (!_generated.isNull() && (_scaled.isNull() || (_fadeOutThumbnail.isNull() && _fadeIn.animating()))) { p.drawPixmap(0, 0, _generated); } if (!_scaled.isNull()) { paintImage(p); paintRadial(p); } else if (_generated.isNull()) { p.fillRect(e->rect(), st::boxBg); return; } else { // Progress of pattern loading. paintRadial(p); } paintTexts(p, ms); } void BackgroundPreviewBox::paintImage(Painter &p) { Expects(!_scaled.isNull()); const auto factor = cIntRetinaFactor(); const auto size = st::boxWideWidth; const auto from = QRect( 0, (size - height()) / 2 * factor, size * factor, height() * factor); const auto guard = gsl::finally([&] { p.setOpacity(1.); }); const auto fade = _fadeIn.value(1.); if (fade < 1. && !_fadeOutThumbnail.isNull()) { p.drawPixmap(rect(), _fadeOutThumbnail, from); } const auto &pixmap = (!_blurred.isNull() && _paper.isBlurred()) ? _blurred : _scaled; p.setOpacity(fade); p.drawPixmap(rect(), pixmap, from); checkBlurAnimationStart(); } void BackgroundPreviewBox::paintRadial(Painter &p) { const auto radial = _radial.animating(); const auto radialOpacity = radial ? _radial.opacity() : 0.; if (!radial) { return; } auto inner = radialRect(); p.setPen(Qt::NoPen); p.setOpacity(radialOpacity); p.setBrush(st::radialBg); { PainterHighQualityEnabler hq(p); p.drawEllipse(inner); } p.setOpacity(1); QRect arc(inner.marginsRemoved(QMargins(st::radialLine, st::radialLine, st::radialLine, st::radialLine))); _radial.draw(p, arc, st::radialLine, st::radialFg); } int BackgroundPreviewBox::textsTop() const { const auto bottom = _blur ? _blur->y() : height(); return bottom - st::historyPaddingBottom - _text1->height() - _text2->height(); } QRect BackgroundPreviewBox::radialRect() const { const auto available = textsTop() - st::historyPaddingBottom; return QRect( QPoint( (width() - st::radialSize.width()) / 2, (available - st::radialSize.height()) / 2), st::radialSize); } void BackgroundPreviewBox::paintTexts(Painter &p, crl::time ms) { const auto height1 = _text1->height(); const auto height2 = _text2->height(); auto context = _controller->defaultChatTheme()->preparePaintContext( _chatStyle.get(), rect(), rect()); p.translate(0, textsTop()); paintDate(p); context.outbg = _text1->hasOutLayout(); _text1->draw(p, context); p.translate(0, height1); context.outbg = _text2->hasOutLayout(); _text2->draw(p, context); p.translate(0, height2); } void BackgroundPreviewBox::paintDate(Painter &p) { const auto date = _text1->Get(); if (!date || !_serviceBg) { return; } auto hq = PainterHighQualityEnabler(p); const auto text = date->text; const auto bubbleHeight = st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom(); const auto bubbleTop = st::msgServiceMargin.top(); const auto textWidth = st::msgServiceFont->width(text); const auto bubbleWidth = st::msgServicePadding.left() + textWidth + st::msgServicePadding.right(); const auto bubbleLeft = (width() - bubbleWidth) / 2; const auto radius = bubbleHeight / 2; p.setPen(Qt::NoPen); p.setBrush(*_serviceBg); p.drawRoundedRect(bubbleLeft, bubbleTop, bubbleWidth, bubbleHeight, radius, radius); p.setPen(st::msgServiceFg); p.setFont(st::msgServiceFont); p.drawText(bubbleLeft + st::msgServicePadding.left(), bubbleTop + st::msgServicePadding.top() + st::msgServiceFont->ascent, text); } void BackgroundPreviewBox::radialAnimationCallback(crl::time now) { Expects(_paper.document() != nullptr); const auto document = _paper.document(); const auto wasAnimating = _radial.animating(); const auto updated = _radial.update( _media->progress(), !document->loading(), now); if ((wasAnimating || _radial.animating()) && (!anim::Disabled() || updated)) { update(radialRect()); } checkLoadedDocument(); } void BackgroundPreviewBox::setScaledFromThumb() { if (!_scaled.isNull()) { return; } const auto localThumbnail = _paper.localThumbnail(); const auto thumbnail = localThumbnail ? localThumbnail : _media ? _media->thumbnail() : nullptr; if (!thumbnail) { return; } else if (_paper.isPattern() && _paper.document() != nullptr) { return; } auto scaled = PrepareScaledFromFull( thumbnail->original(), _paper.isPattern(), _paper.backgroundColors(), _paper.gradientRotation(), _paper.patternOpacity(), _paper.document() ? Images::Option::Blurred : Images::Option(0)); auto blurred = (_paper.document() || _paper.isPattern()) ? QImage() : PrepareScaledNonPattern( Ui::PrepareBlurredBackground(thumbnail->original()), Images::Option(0)); setScaledFromImage(std::move(scaled), std::move(blurred)); } void BackgroundPreviewBox::setScaledFromImage( QImage &&image, QImage &&blurred) { updateServiceBg({ Ui::CountAverageColor(image) }); if (!_full.isNull()) { startFadeInFrom(std::move(_scaled)); } _scaled = Ui::PixmapFromImage(std::move(image)); _blurred = Ui::PixmapFromImage(std::move(blurred)); if (_blur && (!_paper.document() || !_full.isNull())) { _blur->setDisabled(false); } } void BackgroundPreviewBox::startFadeInFrom(QPixmap previous) { _fadeOutThumbnail = std::move(previous); _fadeIn.start([=] { update(); }, 0., 1., st::backgroundCheck.duration); } void BackgroundPreviewBox::checkBlurAnimationStart() { if (_fadeIn.animating() || _blurred.isNull() || !_blur || _paper.isBlurred() == _blur->checked()) { return; } _paper = _paper.withBlurred(_blur->checked()); startFadeInFrom(_paper.isBlurred() ? _scaled : _blurred); } void BackgroundPreviewBox::updateServiceBg(const std::vector &bg) { const auto count = int(bg.size()); if (!count) { return; } auto red = 0, green = 0, blue = 0; for (const auto &color : bg) { red += color.red(); green += color.green(); blue += color.blue(); } _serviceBg = Ui::ThemeAdjustedColor( st::msgServiceBg->c, QColor(red / count, green / count, blue / count)); } void BackgroundPreviewBox::checkLoadedDocument() { const auto document = _paper.document(); if (!_full.isNull() || !document || !_media->loaded(true) || _generating) { return; } const auto generateCallback = [=](QImage &&image) { if (image.isNull()) { return; } crl::async([ this, image = std::move(image), isPattern = _paper.isPattern(), background = _paper.backgroundColors(), gradientRotation = _paper.gradientRotation(), patternOpacity = _paper.patternOpacity(), guard = _generating.make_guard() ]() mutable { auto scaled = PrepareScaledFromFull( image, isPattern, background, gradientRotation, patternOpacity); auto blurred = !isPattern ? PrepareScaledNonPattern( Ui::PrepareBlurredBackground(image), Images::Option(0)) : QImage(); crl::on_main(std::move(guard), [ this, image = std::move(image), scaled = std::move(scaled), blurred = std::move(blurred) ]() mutable { _full = std::move(image); setScaledFromImage(std::move(scaled), std::move(blurred)); update(); }); }); }; _generating = Data::ReadBackgroundImageAsync( _media.get(), Ui::PreprocessBackgroundImage, generateCallback); } bool BackgroundPreviewBox::Start( not_null controller, const QString &slug, const QMap ¶ms) { if (const auto paper = Data::WallPaper::FromColorsSlug(slug)) { controller->show(Box( controller, paper->withUrlParams(params))); return true; } if (!IsValidWallPaperSlug(slug)) { controller->show( Box(tr::lng_background_bad_link(tr::now))); return false; } controller->session().api().requestWallPaper(slug, crl::guard(controller, [=]( const Data::WallPaper &result) { controller->show(Box( controller, result.withUrlParams(params))); }), crl::guard(controller, [=](const MTP::Error &error) { controller->show( Box(tr::lng_background_bad_link(tr::now))); })); return true; } HistoryView::Context BackgroundPreviewBox::elementContext() { return HistoryView::Context::ContactPreview; }