1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "window/window_media_preview.h"
9
10 #include "data/data_photo.h"
11 #include "data/data_photo_media.h"
12 #include "data/data_document.h"
13 #include "data/data_document_media.h"
14 #include "data/data_session.h"
15 #include "data/stickers/data_stickers.h"
16 #include "ui/image/image.h"
17 #include "ui/emoji_config.h"
18 #include "lottie/lottie_single_player.h"
19 #include "main/main_session.h"
20 #include "window/window_session_controller.h"
21 #include "styles/style_layers.h"
22 #include "styles/style_chat_helpers.h"
23 #include "styles/style_chat.h"
24
25 namespace Window {
26 namespace {
27
28 constexpr int kStickerPreviewEmojiLimit = 10;
29
30 } // namespace
31
MediaPreviewWidget(QWidget * parent,not_null<Window::SessionController * > controller)32 MediaPreviewWidget::MediaPreviewWidget(
33 QWidget *parent,
34 not_null<Window::SessionController*> controller)
35 : RpWidget(parent)
36 , _controller(controller)
37 , _emojiSize(Ui::Emoji::GetSizeLarge() / cIntRetinaFactor()) {
38 setAttribute(Qt::WA_TransparentForMouseEvents);
39 _controller->session().downloaderTaskFinished(
40 ) | rpl::start_with_next([=] {
41 update();
42 }, lifetime());
43 }
44
updateArea() const45 QRect MediaPreviewWidget::updateArea() const {
46 const auto size = currentDimensions();
47 return QRect(
48 QPoint((width() - size.width()) / 2, (height() - size.height()) / 2),
49 size);
50 }
51
paintEvent(QPaintEvent * e)52 void MediaPreviewWidget::paintEvent(QPaintEvent *e) {
53 Painter p(this);
54 QRect r(e->rect());
55
56 const auto image = [&] {
57 if (!_lottie || !_lottie->ready()) {
58 return QImage();
59 }
60 _lottie->markFrameShown();
61 return _lottie->frame();
62 }();
63 const auto pixmap = image.isNull() ? currentImage() : QPixmap();
64 const auto size = image.isNull() ? pixmap.size() : image.size();
65 int w = size.width() / cIntRetinaFactor(), h = size.height() / cIntRetinaFactor();
66 auto shown = _a_shown.value(_hiding ? 0. : 1.);
67 if (!_a_shown.animating()) {
68 if (_hiding) {
69 hide();
70 _controller->disableGifPauseReason(Window::GifPauseReason::MediaPreview);
71 return;
72 }
73 } else {
74 p.setOpacity(shown);
75 // w = qMax(qRound(w * (st::stickerPreviewMin + ((1. - st::stickerPreviewMin) * shown)) / 2.) * 2 + int(w % 2), 1);
76 // h = qMax(qRound(h * (st::stickerPreviewMin + ((1. - st::stickerPreviewMin) * shown)) / 2.) * 2 + int(h % 2), 1);
77 }
78 p.fillRect(r, st::stickerPreviewBg);
79 if (image.isNull()) {
80 p.drawPixmap((width() - w) / 2, (height() - h) / 2, pixmap);
81 } else {
82 p.drawImage(
83 QRect((width() - w) / 2, (height() - h) / 2, w, h),
84 image);
85 }
86 if (!_emojiList.empty()) {
87 const auto emojiCount = _emojiList.size();
88 const auto emojiWidth = (emojiCount * _emojiSize) + (emojiCount - 1) * st::stickerEmojiSkip;
89 auto emojiLeft = (width() - emojiWidth) / 2;
90 const auto esize = Ui::Emoji::GetSizeLarge();
91 for (const auto emoji : _emojiList) {
92 Ui::Emoji::Draw(
93 p,
94 emoji,
95 esize,
96 emojiLeft,
97 (height() - h) / 2 - (_emojiSize * 2));
98 emojiLeft += _emojiSize + st::stickerEmojiSkip;
99 }
100 }
101 }
102
resizeEvent(QResizeEvent * e)103 void MediaPreviewWidget::resizeEvent(QResizeEvent *e) {
104 update();
105 }
106
showPreview(Data::FileOrigin origin,not_null<DocumentData * > document)107 void MediaPreviewWidget::showPreview(
108 Data::FileOrigin origin,
109 not_null<DocumentData*> document) {
110 if (!document
111 || (!document->isAnimation() && !document->sticker())
112 || document->isVideoMessage()) {
113 hidePreview();
114 return;
115 }
116
117 startShow();
118 _origin = origin;
119 _photo = nullptr;
120 _photoMedia = nullptr;
121 _document = document;
122 _documentMedia = _document->createMediaView();
123 _documentMedia->thumbnailWanted(_origin);
124 _documentMedia->videoThumbnailWanted(_origin);
125 _documentMedia->automaticLoad(_origin, nullptr);
126 fillEmojiString();
127 resetGifAndCache();
128 }
129
showPreview(Data::FileOrigin origin,not_null<PhotoData * > photo)130 void MediaPreviewWidget::showPreview(
131 Data::FileOrigin origin,
132 not_null<PhotoData*> photo) {
133 startShow();
134 _origin = origin;
135 _document = nullptr;
136 _documentMedia = nullptr;
137 _photo = photo;
138 _photoMedia = _photo->createMediaView();
139 fillEmojiString();
140 resetGifAndCache();
141 }
142
startShow()143 void MediaPreviewWidget::startShow() {
144 _cache = QPixmap();
145 if (isHidden() || _a_shown.animating()) {
146 if (isHidden()) {
147 show();
148 _controller->enableGifPauseReason(Window::GifPauseReason::MediaPreview);
149 }
150 _hiding = false;
151 _a_shown.start([=] { update(); }, 0., 1., st::stickerPreviewDuration);
152 } else {
153 update();
154 }
155 }
156
hidePreview()157 void MediaPreviewWidget::hidePreview() {
158 if (isHidden()) {
159 return;
160 }
161 if (_gif || _gifThumbnail) {
162 _cache = currentImage();
163 }
164 _hiding = true;
165 _a_shown.start([=] { update(); }, 1., 0., st::stickerPreviewDuration);
166 _photo = nullptr;
167 _photoMedia = nullptr;
168 _document = nullptr;
169 _documentMedia = nullptr;
170 resetGifAndCache();
171 }
172
fillEmojiString()173 void MediaPreviewWidget::fillEmojiString() {
174 _emojiList.clear();
175 if (_photo) {
176 return;
177 }
178 if (auto sticker = _document->sticker()) {
179 if (auto list = _document->owner().stickers().getEmojiListFromSet(_document)) {
180 _emojiList = std::move(*list);
181 while (_emojiList.size() > kStickerPreviewEmojiLimit) {
182 _emojiList.pop_back();
183 }
184 } else if (const auto emoji = Ui::Emoji::Find(sticker->alt)) {
185 _emojiList.emplace_back(emoji);
186 }
187 }
188 }
189
resetGifAndCache()190 void MediaPreviewWidget::resetGifAndCache() {
191 _lottie = nullptr;
192 _gif.reset();
193 _gifThumbnail.reset();
194 _gifLastPosition = 0;
195 _cacheStatus = CacheNotLoaded;
196 _cachedSize = QSize();
197 }
198
currentDimensions() const199 QSize MediaPreviewWidget::currentDimensions() const {
200 if (!_cachedSize.isEmpty()) {
201 return _cachedSize;
202 }
203 if (!_document && !_photo) {
204 _cachedSize = QSize(_cache.width() / cIntRetinaFactor(), _cache.height() / cIntRetinaFactor());
205 return _cachedSize;
206 }
207
208 QSize result, box;
209 if (_photo) {
210 result = QSize(_photo->width(), _photo->height());
211 const auto skip = st::defaultBox.margin.top();
212 box = QSize(width() - 2 * skip, height() - 2 * skip);
213 } else {
214 result = _document->dimensions;
215 if (result.isEmpty()) {
216 const auto &gif = (_gif && _gif->ready()) ? _gif : _gifThumbnail;
217 if (gif && gif->ready()) {
218 result = QSize(gif->width(), gif->height());
219 }
220 }
221 if (_document->sticker()) {
222 box = QSize(st::maxStickerSize, st::maxStickerSize);
223 } else {
224 box = QSize(2 * st::maxStickerSize, 2 * st::maxStickerSize);
225 }
226 }
227 result = QSize(qMax(style::ConvertScale(result.width()), 1), qMax(style::ConvertScale(result.height()), 1));
228 if (result.width() > box.width()) {
229 result.setHeight(qMax((box.width() * result.height()) / result.width(), 1));
230 result.setWidth(box.width());
231 }
232 if (result.height() > box.height()) {
233 result.setWidth(qMax((box.height() * result.width()) / result.height(), 1));
234 result.setHeight(box.height());
235 }
236 if (_photo) {
237 _cachedSize = result;
238 }
239 return result;
240 }
241
setupLottie()242 void MediaPreviewWidget::setupLottie() {
243 Expects(_document != nullptr);
244
245 _lottie = std::make_unique<Lottie::SinglePlayer>(
246 Lottie::ReadContent(_documentMedia->bytes(), _document->filepath()),
247 Lottie::FrameRequest{ currentDimensions() * cIntRetinaFactor() },
248 Lottie::Quality::High);
249
250 _lottie->updates(
251 ) | rpl::start_with_next([=](Lottie::Update update) {
252 v::match(update.data, [&](const Lottie::Information &) {
253 this->update();
254 }, [&](const Lottie::DisplayFrameRequest &) {
255 this->update(updateArea());
256 });
257 }, lifetime());
258 }
259
currentImage() const260 QPixmap MediaPreviewWidget::currentImage() const {
261 if (_document) {
262 if (const auto sticker = _document->sticker()) {
263 if (_cacheStatus != CacheLoaded) {
264 if (sticker->animated && !_lottie && _documentMedia->loaded()) {
265 const_cast<MediaPreviewWidget*>(this)->setupLottie();
266 }
267 if (_lottie && _lottie->ready()) {
268 return QPixmap();
269 } else if (const auto image = _documentMedia->getStickerLarge()) {
270 QSize s = currentDimensions();
271 _cache = image->pix(s.width(), s.height());
272 _cacheStatus = CacheLoaded;
273 } else if (_cacheStatus != CacheThumbLoaded
274 && _document->hasThumbnail()
275 && _documentMedia->thumbnail()) {
276 QSize s = currentDimensions();
277 _cache = _documentMedia->thumbnail()->pixBlurred(s.width(), s.height());
278 _cacheStatus = CacheThumbLoaded;
279 }
280 }
281 } else {
282 const_cast<MediaPreviewWidget*>(this)->validateGifAnimation();
283 const auto &gif = (_gif && _gif->started())
284 ? _gif
285 : _gifThumbnail;
286 if (gif && gif->started()) {
287 auto s = currentDimensions();
288 auto paused = _controller->isGifPausedAtLeastFor(Window::GifPauseReason::MediaPreview);
289 return gif->current(s.width(), s.height(), s.width(), s.height(), ImageRoundRadius::None, RectPart::None, paused ? 0 : crl::now());
290 }
291 if (_cacheStatus != CacheThumbLoaded
292 && _document->hasThumbnail()) {
293 QSize s = currentDimensions();
294 const auto thumbnail = _documentMedia->thumbnail();
295 if (thumbnail) {
296 _cache = thumbnail->pixBlurred(s.width(), s.height());
297 _cacheStatus = CacheThumbLoaded;
298 } else if (const auto blurred = _documentMedia->thumbnailInline()) {
299 _cache = blurred->pixBlurred(s.width(), s.height());
300 _cacheStatus = CacheThumbLoaded;
301 }
302 }
303 }
304 } else if (_photo) {
305 if (_cacheStatus != CacheLoaded) {
306 if (_photoMedia->loaded()) {
307 QSize s = currentDimensions();
308 _cache = _photoMedia->image(Data::PhotoSize::Large)->pix(s.width(), s.height());
309 _cacheStatus = CacheLoaded;
310 } else {
311 _photo->load(_origin);
312 if (_cacheStatus != CacheThumbLoaded) {
313 QSize s = currentDimensions();
314 if (const auto thumbnail = _photoMedia->image(
315 Data::PhotoSize::Thumbnail)) {
316 _cache = thumbnail->pixBlurred(s.width(), s.height());
317 _cacheStatus = CacheThumbLoaded;
318 } else if (const auto small = _photoMedia->image(
319 Data::PhotoSize::Small)) {
320 _cache = small->pixBlurred(s.width(), s.height());
321 _cacheStatus = CacheThumbLoaded;
322 } else if (const auto blurred = _photoMedia->thumbnailInline()) {
323 _cache = blurred->pixBlurred(s.width(), s.height());
324 _cacheStatus = CacheThumbLoaded;
325 } else {
326 _photoMedia->wanted(Data::PhotoSize::Small, _origin);
327 }
328 }
329 }
330 }
331 }
332 return _cache;
333 }
334
startGifAnimation(const Media::Clip::ReaderPointer & gif)335 void MediaPreviewWidget::startGifAnimation(
336 const Media::Clip::ReaderPointer &gif) {
337 const auto s = currentDimensions();
338 gif->start(
339 s.width(),
340 s.height(),
341 s.width(),
342 s.height(),
343 ImageRoundRadius::None,
344 RectPart::None);
345 }
346
validateGifAnimation()347 void MediaPreviewWidget::validateGifAnimation() {
348 Expects(_documentMedia != nullptr);
349
350 if (_gifThumbnail && _gifThumbnail->started()) {
351 const auto position = _gifThumbnail->getPositionMs();
352 if (_gif
353 && _gif->ready()
354 && !_gif->started()
355 && (_gifLastPosition > position)) {
356 startGifAnimation(_gif);
357 _gifThumbnail.reset();
358 _gifLastPosition = 0;
359 return;
360 } else {
361 _gifLastPosition = position;
362 }
363 } else if (_gif || _gif.isBad()) {
364 return;
365 }
366
367 const auto contentLoaded = _documentMedia->loaded();
368 const auto thumbContent = _documentMedia->videoThumbnailContent();
369 const auto thumbLoaded = !thumbContent.isEmpty();
370 if (!contentLoaded
371 && (_gifThumbnail || _gifThumbnail.isBad() | !thumbLoaded)) {
372 return;
373 }
374 const auto callback = [=](Media::Clip::Notification notification) {
375 clipCallback(notification);
376 };
377 if (contentLoaded) {
378 _gif = Media::Clip::MakeReader(
379 _documentMedia->owner()->location(),
380 _documentMedia->bytes(),
381 std::move(callback));
382 } else {
383 _gifThumbnail = Media::Clip::MakeReader(
384 thumbContent,
385 std::move(callback));
386 }
387 }
388
clipCallback(Media::Clip::Notification notification)389 void MediaPreviewWidget::clipCallback(
390 Media::Clip::Notification notification) {
391 using namespace Media::Clip;
392 switch (notification) {
393 case NotificationReinit: {
394 if (_gifThumbnail && _gifThumbnail->state() == State::Error) {
395 _gifThumbnail.setBad();
396 }
397 if (_gif && _gif->state() == State::Error) {
398 _gif.setBad();
399 }
400
401 if (_gif
402 && _gif->ready()
403 && !_gif->started()
404 && (!_gifThumbnail || !_gifThumbnail->started())) {
405 startGifAnimation(_gif);
406 } else if (!_gif
407 && _gifThumbnail
408 && _gifThumbnail->ready()
409 && !_gifThumbnail->started()) {
410 startGifAnimation(_gifThumbnail);
411 }
412 update();
413 } break;
414
415 case NotificationRepaint: {
416 if ((_gif && _gif->started() && !_gif->currentDisplayed())
417 || (_gifThumbnail
418 && _gifThumbnail->started()
419 && !_gifThumbnail->currentDisplayed())) {
420 update(updateArea());
421 }
422 } break;
423 }
424 }
425
~MediaPreviewWidget()426 MediaPreviewWidget::~MediaPreviewWidget() {
427 }
428
429 } // namespace Window
430