tdesktop/Telegram/SourceFiles/boxes/background_preview_box.cpp

633 lines
17 KiB
C++

/*
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/boxes/confirm_box.h"
#include "ui/controls/chat_service_checkbox.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/painter.h"
#include "ui/ui_utility.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.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/background_preview_box.h"
#include "window/window_session_controller.h"
#include "settings/settings_common.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include <QtGui/QClipboard>
#include <QtGui/QGuiApplication>
namespace {
constexpr auto kMaxWallPaperSlugLength = 255;
[[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 GenerateServiceItem(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<History*> history,
const QString &text,
bool out) {
Expects(history->peer->isUser());
const auto flags = MessageFlag::FakeHistoryItem
| MessageFlag::HasFromId
| (out ? MessageFlag::Outgoing : MessageFlag(0));
const auto item = history->makeMessage(
history->owner().nextLocalMessageId(),
flags,
base::unixtime::now(),
PreparedServiceText{ { text } });
return AdminLog::OwnedItem(delegate, item);
}
[[nodiscard]] AdminLog::OwnedItem GenerateTextItem(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<History*> history,
const QString &text,
bool out) {
Expects(history->peer->isUser());
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(
history->nextNonHistoryEntryId(),
flags,
replyTo,
viaBotId,
base::unixtime::now(),
out ? history->session().userId() : peerToUser(history->peer->id),
QString(),
TextWithEntities{ text },
MTP_messageMediaEmpty(),
HistoryMessageMarkupData(),
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);
const auto ratio = style::DevicePixelRatio();
return Images::Prepare(image, QSize(takeWidth, takeHeight) * ratio, {
.options = Images::Option::TransparentBackground | blur,
.outer = { size, size },
});
}
[[nodiscard]] QImage PrepareScaledFromFull(
const QImage &image,
bool isPattern,
const std::vector<QColor> &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<Window::SessionController*> controller,
const Data::WallPaper &paper,
BackgroundPreviewArgs args)
: SimpleElementDelegate(controller, [=] { update(); })
, _controller(controller)
, _forPeer(args.forPeer)
, _fromMessageId(args.fromMessageId)
, _chatStyle(std::make_unique<Ui::ChatStyle>())
, _serviceHistory(_controller->session().data().history(
PeerData::kServiceNotificationsId))
, _service(nullptr)
, _text1(GenerateTextItem(
delegate(),
_serviceHistory,
(_forPeer
? tr::lng_background_apply1(tr::now)
: tr::lng_background_text1(tr::now)),
false))
, _text2(GenerateTextItem(
delegate(),
_serviceHistory,
(_forPeer
? tr::lng_background_apply2(tr::now)
: 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<HistoryView::ElementDelegate*> BackgroundPreviewBox::delegate() {
return static_cast<HistoryView::ElementDelegate*>(this);
}
void BackgroundPreviewBox::prepare() {
setTitle(tr::lng_background_header());
addButton(_forPeer
? tr::lng_background_apply_button()
: tr::lng_background_apply(), [=] { apply(); });
addButton(tr::lng_cancel(), [=] { closeBox(); });
if (!_forPeer && _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(false);
_text1->initDimensions();
_text1->resizeGetHeight(st::boxWideWidth);
_text2->initDimensions();
_text2->resizeGetHeight(st::boxWideWidth);
setDimensions(st::boxWideWidth, st::boxWideWidth);
}
void BackgroundPreviewBox::createBlurCheckbox() {
_blur = Ui::MakeChatServiceCheckbox(
this,
tr::lng_background_blur(tr::now),
st::backgroundCheckbox,
st::backgroundCheck,
_paper.isBlurred(),
[=] { return _serviceBg.value_or(QColor(255, 255, 255, 0)); });
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->checkedChanges(
) | rpl::start_with_next([=](bool checked) {
checkBlurAnimationStart();
update();
}, lifetime());
_blur->setDisabled(true);
}
void BackgroundPreviewBox::apply() {
const auto weak = Ui::MakeWeak(this);
if (_forPeer) {
applyForPeer();
} else {
applyForEveryone();
}
if (weak) {
closeBox();
}
}
void BackgroundPreviewBox::applyForPeer() {
Expects(_forPeer != nullptr);
const auto api = &_controller->session().api();
using Flag = MTPmessages_SetChatWallPaper::Flag;
api->request(MTPmessages_SetChatWallPaper(
MTP_flags((_fromMessageId ? Flag::f_id : Flag())
| (_fromMessageId ? Flag() : Flag::f_wallpaper)
| Flag::f_settings),
_forPeer->input,
_paper.mtpInput(&_controller->session()),
_paper.mtpSettings(),
MTP_int(_fromMessageId.msg)
)).done([=](const MTPUpdates &result) {
api->applyUpdates(result);
}).send();
_forPeer->setWallPaper(_paper);
_controller->finishChatThemeEdit(_forPeer);
}
void BackgroundPreviewBox::applyForEveryone() {
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();
}
}
void BackgroundPreviewBox::share() {
QGuiApplication::clipboard()->setText(
_paper.shareUrl(&_controller->session()));
Ui::Toast::Show(
Ui::BoxShow(this).toastParent(),
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
- (_service ? _service->height() : 0)
- _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 heights = _service ? _service->height() : 0;
const auto height1 = _text1->height();
const auto height2 = _text2->height();
auto context = _controller->defaultChatTheme()->preparePaintContext(
_chatStyle.get(),
rect(),
rect(),
_controller->isGifPausedAtLeastFor(Window::GifPauseReason::Layer));
p.translate(0, textsTop());
if (_service) {
_service->draw(p, context);
p.translate(0, heights);
}
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::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::Blur : Images::Option());
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<QColor> &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();
}
rpl::single(
rpl::empty
) | rpl::then(
style::PaletteChanged()
) | rpl::start_with_next([=] {
_serviceBg = Ui::ThemeAdjustedColor(
st::msgServiceBg->c,
QColor(red / count, green / count, blue / count));
_chatStyle->applyAdjustedServiceBg(*_serviceBg);
}, lifetime());
_service = GenerateServiceItem(
delegate(),
_serviceHistory,
((_forPeer && !_fromMessageId)
? tr::lng_background_other_info(
tr::now,
lt_user,
_forPeer->shortName())
: ItemDateText(_text1->data(), false)),
false);
_service->initDimensions();
_service->resizeGetHeight(st::boxWideWidth);
}
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<Window::SessionController*> controller,
const QString &slug,
const QMap<QString, QString> &params) {
if (const auto paper = Data::WallPaper::FromColorsSlug(slug)) {
controller->show(Box<BackgroundPreviewBox>(
controller,
paper->withUrlParams(params)));
return true;
}
if (!IsValidWallPaperSlug(slug)) {
controller->show(Ui::MakeInformBox(tr::lng_background_bad_link()));
return false;
}
controller->session().api().requestWallPaper(slug, crl::guard(controller, [=](
const Data::WallPaper &result) {
controller->show(Box<BackgroundPreviewBox>(
controller,
result.withUrlParams(params)));
}), crl::guard(controller, [=] {
controller->show(Ui::MakeInformBox(tr::lng_background_bad_link()));
}));
return true;
}
HistoryView::Context BackgroundPreviewBox::elementContext() {
return HistoryView::Context::ContactPreview;
}