tdesktop/Telegram/SourceFiles/history/view/media/history_view_document.cpp

1557 lines
48 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 "history/view/media/history_view_document.h"
#include "base/random.h"
#include "lang/lang_keys.h"
#include "storage/localstorage.h"
#include "main/main_session.h"
#include "media/player/media_player_float.h" // Media::Player::RoundPainter.
#include "media/audio/media_audio.h"
#include "media/player/media_player_instance.h"
#include "history/history_item_components.h"
#include "history/history.h"
#include "core/click_handler_types.h" // kDocumentFilenameTooltipProperty.
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_transcribe_button.h"
#include "history/view/media/history_view_media_common.h"
#include "ui/image/image.h"
#include "ui/text/format_values.h"
#include "ui/text/format_song_document_name.h"
#include "ui/text/text_utilities.h"
#include "ui/chat/message_bubble.h"
#include "ui/chat/chat_style.h"
#include "ui/cached_round_corners.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/ui_utility.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_document_resolver.h"
#include "data/data_media_types.h"
#include "data/data_file_click_handler.h"
#include "data/data_file_origin.h"
#include "api/api_transcribes.h"
#include "apiwrap.h"
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
constexpr auto kAudioVoiceMsgUpdateView = crl::time(100);
[[nodiscard]] QString CleanTagSymbols(const QString &value) {
auto result = QString();
const auto begin = value.begin(), end = value.end();
auto from = begin;
for (auto ch = begin; ch != end; ++ch) {
if (ch->isHighSurrogate()
&& (ch + 1) != end
&& (ch + 1)->isLowSurrogate()
&& QChar::surrogateToUcs4(
ch->unicode(),
(ch + 1)->unicode()) >= 0xe0000) {
if (ch > from) {
if (result.isEmpty()) {
result.reserve(value.size());
}
result.append(from, ch - from);
}
++ch;
from = ch + 1;
}
}
if (from == begin) {
return value;
} else if (end > from) {
result.append(from, end - from);
}
return result;
}
void FillWaveform(VoiceData *roundData) {
if (!roundData->waveform.empty()) {
return;
}
const auto &size = ::Media::Player::kWaveformSamplesCount;
auto randomBytes = bytes::vector(size);
base::RandomFill(randomBytes.data(), randomBytes.size());
roundData->waveform.resize(size);
for (auto i = 1; i < size; i += 2) {
const auto peak = uchar(randomBytes[i]) % 31;
roundData->waveform[i - 1] = char(std::max(
0,
peak - (uchar(randomBytes[i - 1]) % 3 + 2)));
roundData->waveform[i] = char(peak);
}
roundData->wavemax = *ranges::max_element(roundData->waveform);
}
void PaintWaveform(
Painter &p,
const PaintContext &context,
const VoiceData *voiceData,
int availableWidth,
float64 progress) {
const auto wf = [&]() -> const VoiceWaveform* {
if (!voiceData) {
return nullptr;
}
if (voiceData->waveform.isEmpty()) {
return nullptr;
} else if (voiceData->waveform.at(0) < 0) {
return nullptr;
}
return &voiceData->waveform;
}();
const auto stm = context.messageStyle();
// Rescale waveform by going in waveform.size * bar_count 1D grid.
const auto active = stm->msgWaveformActive;
const auto inactive = stm->msgWaveformInactive;
const auto wfSize = wf
? int(wf->size())
: ::Media::Player::kWaveformSamplesCount;
const auto activeWidth = base::SafeRound(availableWidth * progress);
const auto &barWidth = st::msgWaveformBar;
const auto barCount = std::min(
availableWidth / (barWidth + st::msgWaveformSkip),
wfSize);
const auto barNormValue = (wf ? voiceData->wavemax : 0) + 1;
const auto maxDelta = st::msgWaveformMax - st::msgWaveformMin;
p.setPen(Qt::NoPen);
auto hq = PainterHighQualityEnabler(p);
for (auto i = 0, barLeft = 0, sum = 0, maxValue = 0; i < wfSize; ++i) {
const auto value = wf ? wf->at(i) : 0;
if (sum + barCount < wfSize) {
maxValue = std::max(maxValue, value);
sum += barCount;
continue;
}
// Draw bar.
sum = sum + barCount - wfSize;
if (sum < (barCount + 1) / 2) {
maxValue = std::max(maxValue, value);
}
const auto barValue = ((maxValue * maxDelta) + (barNormValue / 2))
/ barNormValue;
const auto barHeight = st::msgWaveformMin + barValue;
const auto barTop = st::lineWidth + (st::msgWaveformMax - barValue) / 2.;
if ((barLeft < activeWidth) && (barLeft + barWidth > activeWidth)) {
const auto leftWidth = activeWidth - barLeft;
const auto rightWidth = barWidth - leftWidth;
p.fillRect(
QRectF(barLeft, barTop, leftWidth, barHeight),
active);
p.fillRect(
QRectF(activeWidth, barTop, rightWidth, barHeight),
inactive);
} else {
const auto &color = (barLeft >= activeWidth) ? inactive : active;
p.fillRect(QRectF(barLeft, barTop, barWidth, barHeight), color);
}
barLeft += barWidth + st::msgWaveformSkip;
maxValue = (sum < (barCount + 1) / 2) ? 0 : value;
}
}
[[nodiscard]] int MaxStatusWidth(not_null<DocumentData*> document) {
using namespace Ui;
auto result = 0;
const auto add = [&](const QString &text) {
accumulate_max(result, st::normalFont->width(text));
};
add(FormatDownloadText(document->size, document->size));
const auto duration = document->duration() / 1000;
if (const auto song = document->song()) {
add(FormatPlayedText(duration, duration));
add(FormatDurationAndSizeText(duration, document->size));
} else if (const auto voice = document->voice() ? document->voice() : document->round()) {
add(FormatPlayedText(duration, duration));
add(FormatDurationAndSizeText(duration, document->size));
} else if (document->isVideoFile()) {
add(FormatDurationAndSizeText(duration, document->size));
} else {
add(FormatSizeText(document->size));
}
return result;
}
} // namespace
Document::Document(
not_null<Element*> parent,
not_null<HistoryItem*> realParent,
not_null<DocumentData*> document)
: File(parent, realParent)
, _data(document) {
if (_data->isVideoMessage()) {
const auto &entry = _data->session().api().transcribes().entry(
realParent);
_transcribedRound = entry.shown;
}
auto caption = createCaption();
createComponents(!caption.isEmpty());
if (const auto named = Get<HistoryDocumentNamed>()) {
fillNamedFromData(named);
_tooltipFilename.setTooltipText(named->name);
}
setDocumentLinks(_data, realParent);
setStatusSize(Ui::FileStatusSizeReady);
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
captioned->caption = std::move(caption);
}
}
Document::~Document() {
if (_dataMedia) {
_data->owner().keepAlive(base::take(_dataMedia));
_parent->checkHeavyPart();
}
}
float64 Document::dataProgress() const {
ensureDataMediaCreated();
return _dataMedia->progress();
}
bool Document::dataFinished() const {
return !_data->loading()
&& (!_data->uploading() || _data->waitingForAlbum());
}
bool Document::dataLoaded() const {
ensureDataMediaCreated();
return _dataMedia->loaded();
}
void Document::createComponents(bool caption) {
uint64 mask = 0;
if (_data->isVoiceMessage() || _transcribedRound) {
mask |= HistoryDocumentVoice::Bit();
} else {
mask |= HistoryDocumentNamed::Bit();
if (_data->hasThumbnail()) {
if (!_data->isSong()
&& !Data::IsExecutableName(_data->filename())) {
_data->loadThumbnail(_realParent->fullId());
mask |= HistoryDocumentThumbed::Bit();
}
}
}
if (caption) {
mask |= HistoryDocumentCaptioned::Bit();
}
UpdateComponents(mask);
if (const auto thumbed = Get<HistoryDocumentThumbed>()) {
thumbed->linksavel = std::make_shared<DocumentSaveClickHandler>(
_data,
_realParent->fullId());
thumbed->linkopenwithl = std::make_shared<DocumentOpenWithClickHandler>(
_data,
_realParent->fullId());
thumbed->linkcancell = std::make_shared<DocumentCancelClickHandler>(
_data,
crl::guard(this, [=](FullMsgId id) {
_parent->delegate()->elementCancelUpload(id);
}),
_realParent->fullId());
}
if (const auto voice = Get<HistoryDocumentVoice>()) {
voice->seekl = std::make_shared<VoiceSeekClickHandler>(
_data,
[](FullMsgId) {});
if (_transcribedRound) {
voice->round = std::make_unique<::Media::Player::RoundPainter>(
_realParent);
}
}
}
void Document::fillNamedFromData(not_null<HistoryDocumentNamed*> named) {
const auto nameString = named->name = CleanTagSymbols(
Ui::Text::FormatSongNameFor(_data).string());
named->namew = st::semiboldFont->width(nameString);
}
QSize Document::countOptimalSize() {
auto captioned = Get<HistoryDocumentCaptioned>();
if (_parent->media() != this && !_realParent->groupId()) {
if (captioned) {
RemoveComponents(HistoryDocumentCaptioned::Bit());
captioned = nullptr;
}
} else if (captioned && captioned->caption.hasSkipBlock()) {
captioned->caption.updateSkipBlock(
_parent->skipBlockWidth(),
_parent->skipBlockHeight());
}
auto hasTranscribe = false;
const auto voice = Get<HistoryDocumentVoice>();
if (voice) {
const auto session = &_realParent->history()->session();
if (!session->premium()) {
voice->transcribe = nullptr;
voice->transcribeText = {};
} else {
const auto creating = !voice->transcribe;
if (creating) {
voice->transcribe = std::make_unique<TranscribeButton>(
_realParent,
false);
}
const auto &entry = session->api().transcribes().entry(
_realParent);
const auto update = [=] { repaint(); };
voice->transcribe->setLoading(
entry.shown && (entry.requestId || entry.pending),
update);
auto text = (entry.requestId || !entry.shown)
? TextWithEntities()
: entry.toolong
? Ui::Text::Italic(tr::lng_audio_transcribe_long(tr::now))
: entry.failed
? Ui::Text::Italic(tr::lng_attach_failed(tr::now))
: TextWithEntities{
entry.result + (entry.pending ? " [...]" : ""),
};
voice->transcribe->setOpened(
!text.empty(),
creating ? Fn<void()>() : update);
if (text.empty()) {
voice->transcribeText = {};
} else {
const auto minResizeWidth = st::minPhotoSize
- st::msgPadding.left()
- st::msgPadding.right();
voice->transcribeText = Ui::Text::String(minResizeWidth);
voice->transcribeText.setMarkedText(
st::messageTextStyle,
text);
hasTranscribe = true;
if (const auto skipBlockWidth = captioned
? 0
: _parent->skipBlockWidth()) {
voice->transcribeText.updateSkipBlock(
skipBlockWidth,
_parent->skipBlockHeight());
}
}
}
}
auto thumbed = Get<HistoryDocumentThumbed>();
const auto &st = thumbed ? st::msgFileThumbLayout : st::msgFileLayout;
if (thumbed) {
const auto &location = _data->thumbnailLocation();
auto tw = style::ConvertScale(location.width());
auto th = style::ConvertScale(location.height());
if (tw > th) {
thumbed->thumbw = (tw * st.thumbSize) / th;
} else {
thumbed->thumbw = st.thumbSize;
}
}
auto maxWidth = st::msgFileMinWidth;
const auto tleft = st.padding.left() + st.thumbSize + st.thumbSkip;
const auto tright = st.padding.right();
if (thumbed) {
accumulate_max(maxWidth, tleft + MaxStatusWidth(_data) + tright);
} else {
auto unread = (_data->isVoiceMessage() || _transcribedRound)
? (st::mediaUnreadSkip + st::mediaUnreadSize)
: 0;
accumulate_max(maxWidth, tleft + MaxStatusWidth(_data) + unread + _parent->skipBlockWidth() + st::msgPadding.right());
}
if (auto named = Get<HistoryDocumentNamed>()) {
accumulate_max(maxWidth, tleft + named->namew + tright);
accumulate_min(maxWidth, st::msgMaxWidth);
}
if (voice && voice->transcribe) {
maxWidth += st::historyTranscribeSkip
+ voice->transcribe->size().width();
}
auto minHeight = st.padding.top() + st.thumbSize + st.padding.bottom();
if (!captioned && !hasTranscribe && _parent->bottomInfoIsWide()) {
minHeight += st::msgDateFont->height - st::msgDateDelta.y();
}
if (!isBubbleTop()) {
minHeight -= st::msgFileTopMinus;
}
if (hasTranscribe) {
auto captionw = maxWidth
- st::msgPadding.left()
- st::msgPadding.right();
minHeight += voice->transcribeText.countHeight(captionw);
if (captioned) {
minHeight += st::mediaCaptionSkip;
} else if (isBubbleBottom()) {
minHeight += st::msgPadding.bottom();
}
}
if (captioned) {
auto captionw = maxWidth
- st::msgPadding.left()
- st::msgPadding.right();
minHeight += captioned->caption.countHeight(captionw);
if (isBubbleBottom()) {
minHeight += st::msgPadding.bottom();
}
}
return { maxWidth, minHeight };
}
QSize Document::countCurrentSize(int newWidth) {
const auto captioned = Get<HistoryDocumentCaptioned>();
const auto voice = Get<HistoryDocumentVoice>();
const auto hasTranscribe = voice && !voice->transcribeText.isEmpty();
if (!captioned && !hasTranscribe) {
return File::countCurrentSize(newWidth);
}
accumulate_min(newWidth, maxWidth());
const auto thumbed = Get<HistoryDocumentThumbed>();
const auto &st = thumbed ? st::msgFileThumbLayout : st::msgFileLayout;
auto newHeight = st.padding.top() + st.thumbSize + st.padding.bottom();
if (!isBubbleTop()) {
newHeight -= st::msgFileTopMinus;
}
auto captionw = newWidth - st::msgPadding.left() - st::msgPadding.right();
if (hasTranscribe) {
newHeight += voice->transcribeText.countHeight(captionw);
if (captioned) {
newHeight += st::mediaCaptionSkip;
} else if (isBubbleBottom()) {
newHeight += st::msgPadding.bottom();
}
}
if (captioned) {
newHeight += captioned->caption.countHeight(captionw);
if (isBubbleBottom()) {
newHeight += st::msgPadding.bottom();
}
}
return { newWidth, newHeight };
}
void Document::draw(Painter &p, const PaintContext &context) const {
draw(p, context, width(), LayoutMode::Full, adjustedBubbleRounding());
}
void Document::draw(
Painter &p,
const PaintContext &context,
int width,
LayoutMode mode,
Ui::BubbleRounding outsideRounding) const {
if (width < st::msgPadding.left() + st::msgPadding.right() + 1) return;
ensureDataMediaCreated();
const auto cornerDownload = downloadInCorner();
if (!_dataMedia->canBePlayed(_realParent)) {
_dataMedia->automaticLoad(_realParent->fullId(), _realParent);
}
bool loaded = dataLoaded(), displayLoading = _data->displayLoading();
const auto sti = context.imageStyle();
const auto stm = context.messageStyle();
int captionw = width - st::msgPadding.left() - st::msgPadding.right();
if (displayLoading) {
ensureAnimation();
if (!_animation->radial.animating()) {
_animation->radial.start(dataProgress());
}
}
const auto showPause = updateStatusText();
const auto radial = isRadialAnimation();
const auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;
const auto thumbed = Get<HistoryDocumentThumbed>();
const auto &st = (mode == LayoutMode::Full)
? (thumbed ? st::msgFileThumbLayout : st::msgFileLayout)
: (thumbed ? st::msgFileThumbLayoutGrouped : st::msgFileLayoutGrouped);
const auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip;
const auto nametop = st.nameTop - topMinus;
const auto nameright = st.padding.right();
const auto statustop = st.statusTop - topMinus;
const auto linktop = st.linkTop - topMinus;
const auto bottom = st.padding.top() + st.thumbSize + st.padding.bottom() - topMinus;
const auto rthumb = style::rtlrect(st.padding.left(), st.padding.top() - topMinus, st.thumbSize, st.thumbSize, width);
const auto innerSize = st::msgFileLayout.thumbSize;
const auto inner = QRect(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize);
const auto radialOpacity = radial ? _animation->radial.opacity() : 1.;
if (thumbed) {
const auto rounding = thumbRounding(mode, outsideRounding);
validateThumbnail(thumbed, st.thumbSize, rounding);
p.drawImage(rthumb, thumbed->thumbnail);
if (context.selected()) {
fillThumbnailOverlay(p, rthumb, rounding, context);
}
if (radial || (!loaded && !_data->loading()) || _data->waitingForAlbum()) {
const auto backOpacity = (loaded && !_data->uploading()) ? radialOpacity : 1.;
p.setPen(Qt::NoPen);
p.setBrush(sti->msgDateImgBg);
p.setOpacity(backOpacity * p.opacity());
{
PainterHighQualityEnabler hq(p);
p.drawEllipse(inner);
}
const auto &icon = _data->waitingForAlbum()
? sti->historyFileThumbWaiting
: (radial || _data->loading())
? sti->historyFileThumbCancel
: sti->historyFileThumbDownload;
const auto previous = _data->waitingForAlbum()
? &sti->historyFileThumbCancel
: nullptr;
p.setOpacity(backOpacity);
if (previous && radialOpacity > 0. && radialOpacity < 1.) {
PaintInterpolatedIcon(p, icon, *previous, radialOpacity, inner);
} else {
icon.paintInCenter(p, inner);
}
p.setOpacity(1.);
if (radial) {
QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));
_animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg);
}
}
if (_data->status != FileUploadFailed) {
const auto &lnk = (_data->loading() || _data->uploading())
? thumbed->linkcancell
: dataLoaded()
? thumbed->linkopenwithl
: thumbed->linksavel;
bool over = ClickHandler::showAsActive(lnk);
p.setFont(over ? st::semiboldFont->underline() : st::semiboldFont);
p.setPen(stm->msgFileThumbLinkFg);
p.drawTextLeft(nameleft, linktop, width, thumbed->link, thumbed->linkw);
}
} else {
p.setPen(Qt::NoPen);
const auto coverDrawn = _data->isSongWithCover()
&& DrawThumbnailAsSongCover(
p,
context.st->songCoverOverlayFg(),
_dataMedia,
inner,
context.selected());
if (!coverDrawn) {
if (_transcribedRound) {
if (const auto voice = Get<HistoryDocumentVoice>()) {
if (const auto &round = voice->round) {
if (round->fillFrame(inner.size())) {
p.drawImage(inner.topLeft(), round->frame());
} else {
DrawThumbnailAsSongCover(
p,
st::transparent,
_dataMedia,
inner,
context.selected());
}
}
}
} else {
PainterHighQualityEnabler hq(p);
p.setBrush(stm->msgFileBg);
p.drawEllipse(inner);
}
}
const auto &icon = [&]() -> const style::icon& {
if (_data->waitingForAlbum()) {
return _data->isSongWithCover()
? sti->historyFileThumbWaiting
: stm->historyFileWaiting;
} else if (!cornerDownload
&& (_data->loading() || _data->uploading())) {
return _data->isSongWithCover()
? sti->historyFileThumbCancel
: stm->historyFileCancel;
} else if (showPause) {
return _data->isSongWithCover()
? sti->historyFileThumbPause
: stm->historyFilePause;
} else if (loaded || _dataMedia->canBePlayed(_realParent)) {
return _dataMedia->canBePlayed(_realParent)
? (_data->isSongWithCover()
? sti->historyFileThumbPlay
: stm->historyFilePlay)
: _data->isImage()
? stm->historyFileImage
: stm->historyFileDocument;
} else {
return _data->isSongWithCover()
? sti->historyFileThumbDownload
: stm->historyFileDownload;
}
}();
const auto previous = _data->waitingForAlbum()
? &stm->historyFileCancel
: nullptr;
const auto paintContent = [&](QPainter &q) {
if (previous && radialOpacity > 0. && radialOpacity < 1.) {
PaintInterpolatedIcon(q, icon, *previous, radialOpacity, inner);
} else {
icon.paintInCenter(q, inner);
}
if (radial && !cornerDownload) {
QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));
_animation->radial.draw(q, rinner, st::msgFileRadialLine, stm->historyFileRadialFg);
}
};
if (_data->isSongWithCover() || !usesBubblePattern(context)) {
paintContent(p);
} else {
Ui::PaintPatternBubblePart(
p,
context.viewport,
context.bubblesPattern->pixmap,
inner,
paintContent,
_iconCache);
}
drawCornerDownload(p, context, mode);
}
auto namewidth = width - nameleft - nameright;
auto statuswidth = namewidth;
auto voiceStatusOverride = QString();
const auto voice = Get<HistoryDocumentVoice>();
if (voice) {
ensureDataMediaCreated();
{
const auto voiceData = _data->isVideoMessage()
? _data->round()
: _data->voice();
if (voiceData && voiceData->waveform.isEmpty()) {
if (loaded) {
Local::countVoiceWaveform(_dataMedia.get());
}
}
}
const auto progress = [&] {
if (!context.outbg
&& !voice->playback
&& _realParent->hasUnreadMediaFlag()) {
return 1.;
}
if (voice->seeking()) {
return voice->seekingCurrent();
} else if (voice->playback) {
return voice->playback->progress.current();
}
return 0.;
}();
if (voice->seeking()) {
voiceStatusOverride = Ui::FormatPlayedText(
base::SafeRound(progress * voice->lastDurationMs) / 1000,
voice->lastDurationMs / 1000);
}
if (voice->transcribe) {
const auto size = voice->transcribe->size();
namewidth -= st::historyTranscribeSkip + size.width();
const auto x = nameleft + namewidth + st::historyTranscribeSkip;
const auto y = st.padding.top() - topMinus;
voice->transcribe->paint(p, x, y, context);
}
p.save();
p.translate(nameleft, st.padding.top() - topMinus);
if (_transcribedRound) {
FillWaveform(_data->round());
}
PaintWaveform(p,
context,
_transcribedRound ? _data->round() : _data->voice(),
namewidth + st::msgWaveformSkip,
progress);
p.restore();
} else if (auto named = Get<HistoryDocumentNamed>()) {
p.setFont(st::semiboldFont);
p.setPen(stm->historyFileNameFg);
const auto elided = (namewidth < named->namew);
if (elided) {
p.drawTextLeft(nameleft, nametop, width, st::semiboldFont->elided(named->name, namewidth, Qt::ElideMiddle));
} else {
p.drawTextLeft(nameleft, nametop, width, named->name, named->namew);
}
_tooltipFilename.setElided(elided);
}
auto statusText = voiceStatusOverride.isEmpty() ? _statusText : voiceStatusOverride;
p.setFont(st::normalFont);
p.setPen(stm->mediaFg);
p.drawTextLeft(nameleft, statustop, width, statusText);
if (_realParent->hasUnreadMediaFlag()) {
auto w = st::normalFont->width(statusText);
if (w + st::mediaUnreadSkip + st::mediaUnreadSize <= statuswidth) {
p.setPen(Qt::NoPen);
p.setBrush(stm->msgFileBg);
{
PainterHighQualityEnabler hq(p);
p.drawEllipse(style::rtlrect(nameleft + w + st::mediaUnreadSkip, statustop + st::mediaUnreadTop, st::mediaUnreadSize, st::mediaUnreadSize, width));
}
}
}
auto selection = context.selection;
auto captiontop = bottom;
if (voice && !voice->transcribeText.isEmpty()) {
p.setPen(stm->historyTextFg);
voice->transcribeText.draw(p, st::msgPadding.left(), bottom, captionw, style::al_left, 0, -1, selection);
captiontop += voice->transcribeText.countHeight(captionw) + st::mediaCaptionSkip;
selection = HistoryView::UnshiftItemSelection(selection, voice->transcribeText);
}
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
p.setPen(stm->historyTextFg);
_parent->prepareCustomEmojiPaint(p, context, captioned->caption);
captioned->caption.draw(p, {
.position = { st::msgPadding.left(), captiontop },
.availableWidth = captionw,
.palette = &stm->textPalette,
.pre = stm->preCache.get(),
.blockquote = stm->blockquoteCache.get(),
.colors = context.st->highlightColors(),
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.selection = selection,
});
}
}
Ui::BubbleRounding Document::thumbRounding(
LayoutMode mode,
Ui::BubbleRounding outsideRounding) const {
using Corner = Ui::BubbleCornerRounding;
if (mode != LayoutMode::Grouped && _parent->media() != this) {
return Ui::BubbleRounding(); // In a WebPage preview.
}
const auto hasCaption = Has<HistoryDocumentCaptioned>();
const auto adjust = [&](Corner already, bool skip = false) {
return (already == Corner::Large && !skip)
? Corner::Large
: Corner::Small;
};
auto result = Ui::BubbleRounding();
result.topLeft = adjust(outsideRounding.topLeft);
result.bottomLeft = adjust(outsideRounding.bottomLeft, hasCaption);
result.topRight = result.bottomRight = Corner::Small;
return result;
}
void Document::validateThumbnail(
not_null<const HistoryDocumentThumbed*> thumbed,
int size,
Ui::BubbleRounding rounding) const {
const auto normal = _dataMedia->thumbnail();
const auto blurred = _dataMedia->thumbnailInline();
if (!normal && !blurred) {
return;
}
const auto outer = QSize(size, size);
if ((thumbed->thumbnail.size() == outer * style::DevicePixelRatio())
&& (thumbed->blurred == !normal)
&& (thumbed->rounding == rounding)) {
return;
}
const auto small = (rounding == Ui::BubbleRounding());
auto image = normal ? normal : blurred;
const auto imageWidth = thumbed->thumbw * style::DevicePixelRatio();
auto thumbnail = Images::Prepare(image->original(), imageWidth, {
.options = (normal ? Images::Option() : Images::Option::Blur)
| (small ? Images::Option::RoundSmall : Images::Option()),
.outer = outer,
});
if (!small) {
using Corner = Ui::BubbleCornerRounding;
using Radius = Ui::CachedCornerRadius;
auto corners = std::array<QImage, 4>();
const auto &small = Ui::CachedCornersMasks(Radius::ThumbSmall);
const auto &large = Ui::CachedCornersMasks(Radius::ThumbLarge);
for (auto i = 0; i != 4; ++i) {
switch (rounding[i]) {
case Corner::Small: corners[i] = small[i]; break;
case Corner::Large: corners[i] = large[i]; break;
}
}
thumbnail = Images::Round(std::move(thumbnail), corners);
}
thumbed->thumbnail = std::move(thumbnail);
thumbed->blurred = !normal;
thumbed->rounding = rounding;
}
void Document::fillThumbnailOverlay(
QPainter &p,
QRect rect,
Ui::BubbleRounding rounding,
const PaintContext &context) const {
using Corner = Ui::BubbleCornerRounding;
using Radius = Ui::CachedCornerRadius;
auto corners = Ui::CornersPixmaps();
const auto &st = context.st;
const auto lookup = [&](Corner corner) {
switch (corner) {
case Corner::None: return Radius::Small;
case Corner::Small: return Radius::ThumbSmall;
case Corner::Large: return Radius::ThumbLarge;
}
Unexpected("Corner value in Document::fillThumbnailOverlay.");
};
for (auto i = 0; i != 4; ++i) {
corners.p[i] = st->msgSelectOverlayCorners(lookup(rounding[i])).p[i];
}
Ui::FillComplexOverlayRect(p, rect, st->msgSelectOverlay(), corners);
}
bool Document::hasHeavyPart() const {
return (_dataMedia != nullptr);
}
void Document::unloadHeavyPart() {
_dataMedia = nullptr;
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
captioned->caption.unloadPersistentAnimation();
}
}
void Document::ensureDataMediaCreated() const {
if (_dataMedia) {
return;
}
_dataMedia = _data->createMediaView();
if (Get<HistoryDocumentThumbed>()
|| _data->isSongWithCover()
|| _transcribedRound) {
_dataMedia->thumbnailWanted(_realParent->fullId());
}
history()->owner().registerHeavyViewPart(_parent);
}
bool Document::downloadInCorner() const {
return _data->isAudioFile()
&& _realParent->allowsForward()
&& _data->canBeStreamed(_realParent)
&& !_data->inappPlaybackFailed();
}
void Document::drawCornerDownload(
Painter &p,
const PaintContext &context,
LayoutMode mode) const {
if (dataLoaded()
|| _data->loadedInMediaCache()
|| !downloadInCorner()) {
return;
}
auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;
const auto stm = context.messageStyle();
const auto thumbed = false;
const auto &st = (mode == LayoutMode::Full)
? (thumbed ? st::msgFileThumbLayout : st::msgFileLayout)
: (thumbed ? st::msgFileThumbLayoutGrouped : st::msgFileLayoutGrouped);
const auto shift = st::historyAudioDownloadShift;
const auto size = st::historyAudioDownloadSize;
const auto inner = style::rtlrect(st.padding.left() + shift, st.padding.top() - topMinus + shift, size, size, width());
const auto bubblePattern = usesBubblePattern(context);
if (bubblePattern) {
p.setPen(Qt::NoPen);
} else {
auto pen = stm->msgBg->p;
pen.setWidth(st::lineWidth);
p.setPen(pen);
}
p.setBrush(stm->msgFileBg);
{
PainterHighQualityEnabler hq(p);
p.drawEllipse(inner);
}
const auto &icon = _data->loading()
? stm->historyAudioCancel
: stm->historyAudioDownload;
const auto paintContent = [&](QPainter &q) {
if (bubblePattern) {
auto hq = PainterHighQualityEnabler(q);
auto pen = stm->msgBg->p;
pen.setWidth(st::lineWidth);
q.setPen(pen);
q.setBrush(Qt::NoBrush);
q.drawEllipse(inner);
}
icon.paintInCenter(q, inner);
if (_animation && _animation->radial.animating()) {
const auto rinner = inner.marginsRemoved(QMargins(st::historyAudioRadialLine, st::historyAudioRadialLine, st::historyAudioRadialLine, st::historyAudioRadialLine));
_animation->radial.draw(q, rinner, st::historyAudioRadialLine, stm->historyFileRadialFg);
}
};
if (bubblePattern) {
const auto add = st::lineWidth * 2;
const auto target = inner.marginsAdded({ add, add, add, add });
Ui::PaintPatternBubblePart(
p,
context.viewport,
context.bubblesPattern->pixmap,
target,
paintContent,
_cornerDownloadCache);
} else {
paintContent(p);
}
}
TextState Document::cornerDownloadTextState(
QPoint point,
StateRequest request,
LayoutMode mode) const {
auto result = TextState(_parent);
if (dataLoaded()
|| _data->loadedInMediaCache()
|| !downloadInCorner()) {
return result;
}
auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;
const auto thumbed = false;
const auto &st = (mode == LayoutMode::Full)
? (thumbed ? st::msgFileThumbLayout : st::msgFileLayout)
: (thumbed ? st::msgFileThumbLayoutGrouped : st::msgFileLayoutGrouped);
const auto shift = st::historyAudioDownloadShift;
const auto size = st::historyAudioDownloadSize;
const auto inner = style::rtlrect(st.padding.left() + shift, st.padding.top() - topMinus + shift, size, size, width());
if (inner.contains(point)) {
result.link = _data->loading() ? _cancell : _savel;
}
return result;
}
TextState Document::textState(QPoint point, StateRequest request) const {
return textState(point, { width(), height() }, request, LayoutMode::Full);
}
TextState Document::textState(
QPoint point,
QSize layout,
StateRequest request,
LayoutMode mode) const {
const auto width = layout.width();
auto result = TextState(_parent);
if (width < st::msgPadding.left() + st::msgPadding.right() + 1) {
return result;
}
ensureDataMediaCreated();
bool loaded = dataLoaded();
updateStatusText();
const auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;
const auto thumbed = Get<HistoryDocumentThumbed>();
const auto &st = (mode == LayoutMode::Full)
? (thumbed ? st::msgFileThumbLayout : st::msgFileLayout)
: (thumbed ? st::msgFileThumbLayoutGrouped : st::msgFileLayoutGrouped);
const auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip;
const auto nametop = st.nameTop - topMinus;
const auto nameright = st.padding.right();
auto namewidth = width - nameleft - nameright;
const auto linktop = st.linkTop - topMinus;
auto bottom = st.padding.top() + st.thumbSize + st.padding.bottom() - topMinus;
const auto rthumb = style::rtlrect(st.padding.left(), st.padding.top() - topMinus, st.thumbSize, st.thumbSize, width);
const auto innerSize = st::msgFileLayout.thumbSize;
const auto inner = QRect(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize);
const auto filenameMoused = QRect(nameleft, nametop, namewidth, st::semiboldFont->height).contains(point);
_tooltipFilename.setMoused(filenameMoused);
if (const auto thumbed = Get<HistoryDocumentThumbed>()) {
if ((_data->loading() || _data->uploading()) && rthumb.contains(point)) {
result.link = _cancell;
return result;
}
if (_data->status != FileUploadFailed) {
if (style::rtlrect(nameleft, linktop, thumbed->linkw, st::semiboldFont->height, width).contains(point)) {
result.link = (_data->loading() || _data->uploading())
? thumbed->linkcancell
: dataLoaded()
? thumbed->linkopenwithl
: thumbed->linksavel;
return result;
}
}
} else {
if (const auto state = cornerDownloadTextState(point, request, mode); state.link) {
return state;
}
if ((_data->loading() || _data->uploading()) && inner.contains(point) && !downloadInCorner()) {
result.link = _cancell;
return result;
}
}
const auto voice = Get<HistoryDocumentVoice>();
auto transcribeLength = 0;
auto transcribeHeight = 0;
auto painth = layout.height();
if (voice) {
auto waveformbottom = st.padding.top() - topMinus + st::msgWaveformMax + st::msgWaveformMin;
if (voice->transcribe) {
const auto size = voice->transcribe->size();
namewidth -= st::historyTranscribeSkip + size.width();
const auto x = nameleft + namewidth + st::historyTranscribeSkip;
const auto y = st.padding.top() - topMinus;
if (QRect(QPoint(x, y), size).contains(point)) {
result.link = voice->transcribe->link();
return result;
}
}
if (QRect(nameleft, nametop, namewidth, waveformbottom - nametop).contains(point)) {
const auto state = ::Media::Player::instance()->getState(AudioMsgId::Type::Voice);
if (state.id == AudioMsgId(_data, _realParent->fullId(), state.id.externalPlayId())
&& !::Media::Player::IsStoppedOrStopping(state.state)) {
if (!voice->seeking()) {
voice->setSeekingStart((point.x() - nameleft) / float64(namewidth));
}
result.link = voice->seekl;
return result;
}
}
transcribeLength = voice->transcribeText.length();
if (transcribeLength > 0) {
auto captionw = width - st::msgPadding.left() - st::msgPadding.right();
transcribeHeight = voice->transcribeText.countHeight(captionw);
painth -= transcribeHeight;
if (point.y() >= bottom && point.y() < bottom + transcribeHeight) {
result = TextState(_parent, voice->transcribeText.getState(
point - QPoint(st::msgPadding.left(), bottom),
width - st::msgPadding.left() - st::msgPadding.right(),
request.forText()));
return result;
}
bottom += transcribeHeight;
}
}
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
if (point.y() >= bottom) {
result.symbol += transcribeLength;
}
if (transcribeHeight) {
painth -= st::mediaCaptionSkip;
bottom += st::mediaCaptionSkip;
}
if (point.y() >= bottom) {
result = TextState(_parent, captioned->caption.getState(
point - QPoint(st::msgPadding.left(), bottom),
width - st::msgPadding.left() - st::msgPadding.right(),
request.forText()));
result.symbol += transcribeLength;
return result;
}
auto captionw = width - st::msgPadding.left() - st::msgPadding.right();
painth -= captioned->caption.countHeight(captionw);
if (isBubbleBottom()) {
painth -= st::msgPadding.bottom();
}
} else if (transcribeHeight && isBubbleBottom()) {
painth -= st::msgPadding.bottom();
}
const auto till = voice ? (nameleft + namewidth) : width;
if (QRect(0, 0, till, painth).contains(point)
&& (!_data->loading() || downloadInCorner())
&& !_data->uploading()
&& !_data->isNull()) {
if (loaded || _dataMedia->canBePlayed(_realParent)) {
result.link = _openl;
} else {
result.link = _savel;
}
_tooltipFilename.updateTooltipForLink(result.link.get());
return result;
}
_tooltipFilename.updateTooltipForState(result);
return result;
}
void Document::updatePressed(QPoint point) {
// LayoutMode should be passed here.
if (const auto voice = Get<HistoryDocumentVoice>()) {
if (voice->seeking()) {
const auto thumbed = Get<HistoryDocumentThumbed>();
const auto &st = thumbed ? st::msgFileThumbLayout : st::msgFileLayout;
const auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip;
const auto nameright = st.padding.right();
voice->setSeekingCurrent(std::clamp(
(point.x() - nameleft)
/ float64(width() - nameleft - nameright),
0.,
1.));
repaint();
}
}
}
TextSelection Document::adjustSelection(
TextSelection selection,
TextSelectType type) const {
auto transcribe = (const Ui::Text::String*)nullptr;
auto caption = (const Ui::Text::String*)nullptr;
if (const auto voice = Get<HistoryDocumentVoice>()) {
transcribe = &voice->transcribeText;
}
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
caption = &captioned->caption;
}
const auto transcribeLength = transcribe ? transcribe->length() : 0;
if (transcribe && selection.from < transcribeLength) {
const auto adjusted = transcribe->adjustSelection(selection, type);
if (selection.to <= transcribeLength) {
return adjusted;
}
selection = TextSelection(adjusted.from, selection.to);
}
if (caption && selection.to > transcribeLength) {
auto unshifted = transcribe
? HistoryView::UnshiftItemSelection(selection, *transcribe)
: selection;
const auto adjusted = caption->adjustSelection(unshifted, type);
const auto shifted = transcribe
? HistoryView::ShiftItemSelection(adjusted, *transcribe)
: adjusted;
if (selection.from >= transcribeLength) {
return shifted;
}
selection = TextSelection(selection.from, shifted.to);
}
return selection;
}
uint16 Document::fullSelectionLength() const {
auto result = uint16();
if (const auto voice = Get<HistoryDocumentVoice>()) {
result += voice->transcribeText.length();
}
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
result += captioned->caption.length();
}
return result;
}
bool Document::hasTextForCopy() const {
if (const auto voice = Get<HistoryDocumentVoice>()) {
if (!voice->transcribeText.isEmpty()) {
return true;
}
}
return Has<HistoryDocumentCaptioned>();
}
TextForMimeData Document::selectedText(TextSelection selection) const {
auto result = TextForMimeData();
if (const auto voice = Get<HistoryDocumentVoice>()) {
const auto length = voice->transcribeText.length();
if (selection.from < length) {
result.append(
voice->transcribeText.toTextForMimeData(selection));
}
if (selection.to <= length) {
return result;
}
selection = HistoryView::UnshiftItemSelection(
selection,
voice->transcribeText);
}
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
if (!result.empty()) {
result.append("\n\n");
}
result.append(captioned->caption.toTextForMimeData(selection));
}
return result;
}
bool Document::uploading() const {
return _data->uploading();
}
void Document::setStatusSize(int64 newSize, TimeId realDuration) const {
const auto duration = (_data->isSong()
|| _data->isVoiceMessage()
|| _transcribedRound)
? _data->duration()
: -1;
File::setStatusSize(
newSize,
_data->size,
(duration >= 0) ? duration / 1000 : -1,
realDuration);
if (auto thumbed = Get<HistoryDocumentThumbed>()) {
if (_statusSize == Ui::FileStatusSizeReady) {
thumbed->link = tr::lng_media_download(tr::now).toUpper();
} else if (_statusSize == Ui::FileStatusSizeLoaded) {
thumbed->link = tr::lng_media_open_with(tr::now).toUpper();
} else if (_statusSize == Ui::FileStatusSizeFailed) {
thumbed->link = tr::lng_media_download(tr::now).toUpper();
} else if (_statusSize >= 0) {
thumbed->link = tr::lng_media_cancel(tr::now).toUpper();
} else {
thumbed->link = tr::lng_media_open_with(tr::now).toUpper();
}
thumbed->linkw = st::semiboldFont->width(thumbed->link);
}
}
bool Document::updateStatusText() const {
auto showPause = false;
auto statusSize = int64();
auto realDuration = TimeId();
if (_data->status == FileDownloadFailed || _data->status == FileUploadFailed) {
statusSize = Ui::FileStatusSizeFailed;
} else if (_data->uploading()) {
statusSize = _data->uploadingData->offset;
} else if (_data->loading()) {
statusSize = _data->loadOffset();
} else if (dataLoaded()) {
statusSize = Ui::FileStatusSizeLoaded;
} else {
statusSize = Ui::FileStatusSizeReady;
}
if (_data->isVoiceMessage() || _transcribedRound) {
const auto state = ::Media::Player::instance()->getState(AudioMsgId::Type::Voice);
if (state.id == AudioMsgId(_data, _realParent->fullId(), state.id.externalPlayId())
&& !::Media::Player::IsStoppedOrStopping(state.state)) {
if (auto voice = Get<HistoryDocumentVoice>()) {
bool was = (voice->playback != nullptr);
voice->ensurePlayback(this);
if (!was || state.position != voice->playback->position) {
auto prg = state.length
? std::clamp(
float64(state.position) / state.length,
0.,
1.)
: 0.;
if (voice->playback->position < state.position) {
voice->playback->progress.start(prg);
} else {
voice->playback->progress = anim::value(0., prg);
}
voice->playback->position = state.position;
voice->playback->progressAnimation.start();
}
voice->lastDurationMs = static_cast<int>((state.length * 1000LL) / state.frequency); // Bad :(
}
statusSize = -1 - (state.position / state.frequency);
realDuration = (state.length / state.frequency);
showPause = ::Media::Player::ShowPauseIcon(state.state);
} else {
if (auto voice = Get<HistoryDocumentVoice>()) {
voice->checkPlaybackFinished();
}
}
if (!showPause && (state.id == AudioMsgId(_data, _realParent->fullId(), state.id.externalPlayId()))) {
showPause = ::Media::Player::instance()->isSeeking(AudioMsgId::Type::Voice);
}
} else if (_data->isAudioFile()) {
const auto state = ::Media::Player::instance()->getState(AudioMsgId::Type::Song);
if (state.id == AudioMsgId(_data, _realParent->fullId(), state.id.externalPlayId())
&& !::Media::Player::IsStoppedOrStopping(state.state)) {
statusSize = -1 - (state.position / state.frequency);
realDuration = (state.length / state.frequency);
showPause = ::Media::Player::ShowPauseIcon(state.state);
} else {
}
if (!showPause && (state.id == AudioMsgId(_data, _realParent->fullId(), state.id.externalPlayId()))) {
showPause = ::Media::Player::instance()->isSeeking(AudioMsgId::Type::Song);
}
}
if (statusSize != _statusSize) {
setStatusSize(statusSize, realDuration);
}
return showPause;
}
QMargins Document::bubbleMargins() const {
if (!Has<HistoryDocumentThumbed>()) {
return st::msgPadding;
}
const auto padding = st::msgFileThumbLayout.padding;
return QMargins(padding.left(), padding.top(), padding.right(), padding.bottom());
}
QSize Document::sizeForGroupingOptimal(int maxWidth) const {
const auto thumbed = Get<HistoryDocumentThumbed>();
const auto &st = (thumbed ? st::msgFileThumbLayoutGrouped : st::msgFileLayoutGrouped);
auto height = st.padding.top() + st.thumbSize + st.padding.bottom();
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
auto captionw = maxWidth
- st::msgPadding.left()
- st::msgPadding.right();
height += captioned->caption.countHeight(captionw);
}
return { maxWidth, height };
}
QSize Document::sizeForGrouping(int width) const {
const auto thumbed = Get<HistoryDocumentThumbed>();
const auto &st = (thumbed ? st::msgFileThumbLayoutGrouped : st::msgFileLayoutGrouped);
auto height = st.padding.top() + st.thumbSize + st.padding.bottom();
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
auto captionw = width
- st::msgPadding.left()
- st::msgPadding.right();
height += captioned->caption.countHeight(captionw);
}
return { maxWidth(), height };
}
void Document::drawGrouped(
Painter &p,
const PaintContext &context,
const QRect &geometry,
RectParts sides,
Ui::BubbleRounding rounding,
float64 highlightOpacity,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const {
p.translate(geometry.topLeft());
draw(
p,
context.translated(-geometry.topLeft()),
geometry.width(),
LayoutMode::Grouped,
rounding);
p.translate(-geometry.topLeft());
}
TextState Document::getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const {
point -= geometry.topLeft();
return textState(
point,
geometry.size(),
request,
LayoutMode::Grouped);
}
bool Document::voiceProgressAnimationCallback(crl::time now) {
if (anim::Disabled()) {
now += (2 * kAudioVoiceMsgUpdateView);
}
if (const auto voice = Get<HistoryDocumentVoice>()) {
if (voice->playback) {
const auto dt = (now - voice->playback->progressAnimation.started())
/ float64(2 * kAudioVoiceMsgUpdateView);
if (dt >= 1.) {
voice->playback->progressAnimation.stop();
voice->playback->progress.finish();
} else {
voice->playback->progress.update(qMin(dt, 1.), anim::linear);
}
repaint();
return (dt < 1.);
}
}
return false;
}
void Document::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
if (auto voice = Get<HistoryDocumentVoice>()) {
if (pressed && p == voice->seekl && !voice->seeking()) {
voice->startSeeking();
} else if (!pressed && voice->seeking()) {
const auto type = AudioMsgId::Type::Voice;
const auto state = ::Media::Player::instance()->getState(type);
if (state.id == AudioMsgId(_data, _realParent->fullId(), state.id.externalPlayId()) && state.length) {
const auto currentProgress = voice->seekingCurrent();
::Media::Player::instance()->finishSeeking(
AudioMsgId::Type::Voice,
currentProgress);
voice->ensurePlayback(this);
voice->playback->position = 0;
voice->playback->progress = anim::value(currentProgress, currentProgress);
}
voice->stopSeeking();
}
}
File::clickHandlerPressedChanged(p, pressed);
}
void Document::refreshParentId(not_null<HistoryItem*> realParent) {
File::refreshParentId(realParent);
const auto fullId = realParent->fullId();
if (auto thumbed = Get<HistoryDocumentThumbed>()) {
if (thumbed->linksavel) {
thumbed->linksavel->setMessageId(fullId);
thumbed->linkcancell->setMessageId(fullId);
}
}
if (auto voice = Get<HistoryDocumentVoice>()) {
if (voice->seekl) {
voice->seekl->setMessageId(fullId);
}
}
}
void Document::parentTextUpdated() {
auto caption = (_parent->media() == this || _realParent->groupId())
? createCaption()
: Ui::Text::String();
if (!caption.isEmpty()) {
AddComponents(HistoryDocumentCaptioned::Bit());
auto captioned = Get<HistoryDocumentCaptioned>();
captioned->caption = std::move(caption);
} else {
RemoveComponents(HistoryDocumentCaptioned::Bit());
}
history()->owner().requestViewResize(_parent);
}
TextWithEntities Document::getCaption() const {
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
return captioned->caption.toTextWithEntities();
}
return TextWithEntities();
}
void Document::hideSpoilers() {
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
captioned->caption.setSpoilerRevealed(false, anim::type::instant);
}
}
Ui::Text::String Document::createCaption() {
return File::createCaption(_realParent);
}
void Document::TooltipFilename::setElided(bool value) {
if (_elided != value) {
_elided = value;
_stale = true;
}
}
void Document::TooltipFilename::setMoused(bool value) {
if (_moused != value) {
_moused = value;
_stale = true;
}
}
void Document::TooltipFilename::setTooltipText(QString text) {
if (_tooltip != text) {
_tooltip = text;
_stale = true;
}
}
void Document::TooltipFilename::updateTooltipForLink(ClickHandler *link) {
if (_lastLink != link) {
_lastLink = link;
_stale = true;
}
if (_stale && link) {
_stale = false;
link->setProperty(
kDocumentFilenameTooltipProperty,
(_elided && _moused) ? _tooltip : QString());
}
}
void Document::TooltipFilename::updateTooltipForState(
TextState &state) const {
if (_elided && _moused) {
state.customTooltip = true;
state.customTooltipText = _tooltip;
}
}
bool DrawThumbnailAsSongCover(
Painter &p,
const style::color &colored,
const std::shared_ptr<Data::DocumentMedia> &dataMedia,
const QRect &rect,
const bool selected) {
if (!dataMedia) {
return false;
}
auto cover = QPixmap();
const auto scaled = [&](not_null<Image*> image) {
const auto aspectRatio = Qt::KeepAspectRatioByExpanding;
return image->size().scaled(rect.size(), aspectRatio);
};
const auto args = Images::PrepareArgs{
.colored = &colored,
.options = Images::Option::RoundCircle,
.outer = rect.size(),
};
if (const auto normal = dataMedia->thumbnail()) {
cover = normal->pixSingle(scaled(normal), args);
} else if (const auto blurred = dataMedia->thumbnailInline()) {
cover = blurred->pixSingle(scaled(blurred), args.blurred());
} else {
return false;
}
if (selected) {
auto selectedCover = Images::Colored(
cover.toImage(),
p.textPalette().selectOverlay);
cover = QPixmap::fromImage(
std::move(selectedCover),
Qt::ColorOnly);
}
p.drawPixmap(rect.topLeft(), cover);
return true;
}
} // namespace HistoryView