/* 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_media_grouped.h" #include "history/history_item_components.h" #include "history/history_item.h" #include "history/history.h" #include "history/view/history_view_element.h" #include "history/view/history_view_cursor_state.h" #include "data/data_document.h" #include "data/data_media_types.h" #include "data/data_session.h" #include "storage/storage_shared_media.h" #include "lang/lang_keys.h" #include "ui/grouped_layout.h" #include "ui/chat/chat_style.h" #include "ui/chat/message_bubble.h" #include "ui/text/text_options.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "layout/layout_selection.h" #include "styles/style_chat.h" namespace HistoryView { namespace { std::vector LayoutPlaylist( const std::vector &sizes) { Expects(!sizes.empty()); auto result = std::vector(); result.reserve(sizes.size()); const auto width = ranges::max_element( sizes, std::less<>(), &QSize::width)->width(); auto top = 0; for (const auto &size : sizes) { result.push_back({ .geometry = QRect(0, top, width, size.height()), .sides = RectPart::Left | RectPart::Right }); top += size.height(); } result.front().sides |= RectPart::Top; result.back().sides |= RectPart::Bottom; return result; } } // namespace GroupedMedia::Part::Part( not_null parent, not_null media) : item(media->parent()) , content(media->createView(parent, item)) { Assert(media->canBeGrouped()); } GroupedMedia::GroupedMedia( not_null parent, const std::vector> &medias) : Media(parent) , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) { const auto truncated = ranges::views::all( medias ) | ranges::views::transform([](const std::unique_ptr &v) { return v.get(); }) | ranges::views::take(kMaxSize); const auto result = applyGroup(truncated); Ensures(result); } GroupedMedia::GroupedMedia( not_null parent, const std::vector> &items) : Media(parent) , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) { const auto medias = ranges::views::all( items ) | ranges::views::transform([](not_null item) { return item->media(); }) | ranges::views::take(kMaxSize); const auto result = applyGroup(medias); Ensures(result); } GroupedMedia::~GroupedMedia() { // Destroy all parts while the media object is still not destroyed. base::take(_parts); } GroupedMedia::Mode GroupedMedia::DetectMode(not_null media) { const auto document = media->document(); return (document && !document->isVideoFile()) ? Mode::Column : Mode::Grid; } QSize GroupedMedia::countOptimalSize() { if (_caption.hasSkipBlock()) { _caption.updateSkipBlock( _parent->skipBlockWidth(), _parent->skipBlockHeight()); } std::vector sizes; const auto partsCount = _parts.size(); sizes.reserve(partsCount); auto maxWidth = 0; if (_mode == Mode::Column) { for (const auto &part : _parts) { const auto &media = part.content; media->setBubbleRounding(bubbleRounding()); media->initDimensions(); accumulate_max(maxWidth, media->maxWidth()); } } for (const auto &part : _parts) { sizes.push_back(part.content->sizeForGroupingOptimal(maxWidth)); } const auto layout = (_mode == Mode::Grid) ? Ui::LayoutMediaGroup( sizes, st::historyGroupWidthMax, st::historyGroupWidthMin, st::historyGroupSkip) : LayoutPlaylist(sizes); Assert(layout.size() == _parts.size()); auto minHeight = 0; for (auto i = 0, count = int(layout.size()); i != count; ++i) { const auto &item = layout[i]; accumulate_max(maxWidth, item.geometry.x() + item.geometry.width()); accumulate_max(minHeight, item.geometry.y() + item.geometry.height()); _parts[i].initialGeometry = item.geometry; _parts[i].sides = item.sides; } if (!_caption.isEmpty()) { auto captionw = maxWidth - st::msgPadding.left() - st::msgPadding.right(); minHeight += st::mediaCaptionSkip + _caption.countHeight(captionw); if (isBubbleBottom()) { minHeight += st::msgPadding.bottom(); } } else if (_mode == Mode::Column && _parts.back().item->emptyText()) { const auto item = _parent->data(); const auto msgsigned = item->Get(); const auto views = item->Get(); if ((msgsigned && !msgsigned->isAnonymousRank) || (views && (views->views.count >= 0 || views->replies.count > 0)) || displayedEditBadge()) { minHeight += st::msgDateFont->height - st::msgDateDelta.y(); } } const auto groupPadding = groupedPadding(); minHeight += groupPadding.top() + groupPadding.bottom(); return { maxWidth, minHeight }; } QSize GroupedMedia::countCurrentSize(int newWidth) { accumulate_min(newWidth, maxWidth()); auto newHeight = 0; if (_mode == Mode::Grid && newWidth < st::historyGroupWidthMin) { return { newWidth, newHeight }; } else if (_mode == Mode::Column) { auto top = 0; for (auto &part : _parts) { const auto size = part.content->sizeForGrouping(newWidth); part.geometry = QRect(0, top, newWidth, size.height()); top += size.height(); } newHeight = top; } else { const auto initialSpacing = st::historyGroupSkip; const auto factor = newWidth / float64(maxWidth()); const auto scale = [&](int value) { return int(base::SafeRound(value * factor)); }; const auto spacing = scale(initialSpacing); for (auto &part : _parts) { const auto sides = part.sides; const auto initialGeometry = part.initialGeometry; const auto needRightSkip = !(sides & RectPart::Right); const auto needBottomSkip = !(sides & RectPart::Bottom); const auto initialLeft = initialGeometry.x(); const auto initialTop = initialGeometry.y(); const auto initialRight = initialLeft + initialGeometry.width() + (needRightSkip ? initialSpacing : 0); const auto initialBottom = initialTop + initialGeometry.height() + (needBottomSkip ? initialSpacing : 0); const auto left = scale(initialLeft); const auto top = scale(initialTop); const auto width = scale(initialRight) - left - (needRightSkip ? spacing : 0); const auto height = scale(initialBottom) - top - (needBottomSkip ? spacing : 0); part.geometry = QRect(left, top, width, height); accumulate_max(newHeight, top + height); } } if (!_caption.isEmpty()) { const auto captionw = newWidth - st::msgPadding.left() - st::msgPadding.right(); newHeight += st::mediaCaptionSkip + _caption.countHeight(captionw); if (isBubbleBottom()) { newHeight += st::msgPadding.bottom(); } } else if (_mode == Mode::Column && _parts.back().item->emptyText()) { const auto item = _parent->data(); const auto msgsigned = item->Get(); const auto views = item->Get(); if ((msgsigned && !msgsigned->isAnonymousRank) || (views && (views->views.count >= 0 || views->replies.count > 0)) || displayedEditBadge()) { newHeight += st::msgDateFont->height - st::msgDateDelta.y(); } } const auto groupPadding = groupedPadding(); newHeight += groupPadding.top() + groupPadding.bottom(); return { newWidth, newHeight }; } void GroupedMedia::refreshParentId( not_null realParent) { for (const auto &part : _parts) { part.content->refreshParentId(part.item); } } Ui::BubbleRounding GroupedMedia::applyRoundingSides( Ui::BubbleRounding already, RectParts sides) const { auto result = Ui::GetCornersFromSides(sides); if (!(result & RectPart::TopLeft)) { already.topLeft = Ui::BubbleCornerRounding::None; } if (!(result & RectPart::TopRight)) { already.topRight = Ui::BubbleCornerRounding::None; } if (!(result & RectPart::BottomLeft)) { already.bottomLeft = Ui::BubbleCornerRounding::None; } if (!(result & RectPart::BottomRight)) { already.bottomRight = Ui::BubbleCornerRounding::None; } return already; } QMargins GroupedMedia::groupedPadding() const { if (_mode != Mode::Column) { return QMargins(); } const auto normal = st::msgFileLayout.padding; const auto grouped = st::msgFileLayoutGrouped.padding; const auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus; const auto lastHasCaption = isBubbleBottom() && !_parts.back().item->emptyText(); const auto addToBottom = lastHasCaption ? st::msgPadding.bottom() : 0; return QMargins( 0, (normal.top() - grouped.top()) - topMinus, 0, (normal.bottom() - grouped.bottom()) + addToBottom); } void GroupedMedia::drawHighlight( Painter &p, const PaintContext &context, int top) const { if (_mode != Mode::Column) { return; } const auto skip = top + groupedPadding().top(); for (auto i = 0, count = int(_parts.size()); i != count; ++i) { const auto &part = _parts[i]; const auto rect = part.geometry.translated(0, skip); _parent->paintCustomHighlight( p, context, rect.y(), rect.height(), part.item); } } void GroupedMedia::draw(Painter &p, const PaintContext &context) const { auto wasCache = false; auto nowCache = false; const auto groupPadding = groupedPadding(); auto selection = context.selection; const auto fullSelection = (selection == FullSelection); const auto textSelection = (_mode == Mode::Column) && !fullSelection && !IsSubGroupSelection(selection); const auto inWebPage = (_parent->media() != this); constexpr auto kSmall = Ui::BubbleCornerRounding::Small; const auto rounding = inWebPage ? Ui::BubbleRounding{ kSmall, kSmall, kSmall, kSmall } : adjustedBubbleRoundingWithCaption(_caption); for (auto i = 0, count = int(_parts.size()); i != count; ++i) { const auto &part = _parts[i]; const auto partContext = context.withSelection(fullSelection ? FullSelection : textSelection ? selection : IsGroupItemSelection(selection, i) ? FullSelection : TextSelection()); if (textSelection) { selection = part.content->skipSelection(selection); } const auto highlightOpacity = (_mode == Mode::Grid) ? _parent->delegate()->elementHighlightOpacity(part.item) : 0.; if (!part.cache.isNull()) { wasCache = true; } part.content->drawGrouped( p, partContext, part.geometry.translated(0, groupPadding.top()), part.sides, applyRoundingSides(rounding, part.sides), highlightOpacity, &part.cacheKey, &part.cache); if (!part.cache.isNull()) { nowCache = true; } } if (nowCache && !wasCache) { history()->owner().registerHeavyViewPart(_parent); } // date if (!_caption.isEmpty()) { const auto captionw = width() - st::msgPadding.left() - st::msgPadding.right(); const auto captiony = height() - groupPadding.bottom() - (isBubbleBottom() ? st::msgPadding.bottom() : 0) - _caption.countHeight(captionw); const auto stm = context.messageStyle(); p.setPen(stm->historyTextFg); _parent->prepareCustomEmojiPaint(p, context, _caption); _caption.draw(p, { .position = QPoint( st::msgPadding.left(), captiony), .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 = context.selection, }); } else if (_parent->media() == this) { auto fullRight = width(); auto fullBottom = height(); if (needInfoDisplay()) { _parent->drawInfo( p, context, fullRight, fullBottom, width(), InfoDisplayType::Image); } if (const auto size = _parent->hasBubble() ? std::nullopt : _parent->rightActionSize()) { auto fastShareLeft = (fullRight + st::historyFastShareLeft); auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height()); _parent->drawRightAction(p, context, fastShareLeft, fastShareTop, width()); } } } TextState GroupedMedia::getPartState( QPoint point, StateRequest request) const { auto shift = 0; for (const auto &part : _parts) { if (part.geometry.contains(point)) { auto result = part.content->getStateGrouped( part.geometry, part.sides, point, request); result.symbol += shift; result.itemId = part.item->fullId(); return result; } shift += part.content->fullSelectionLength(); } return TextState(_parent->data()); } PointState GroupedMedia::pointState(QPoint point) const { if (!QRect(0, 0, width(), height()).contains(point)) { return PointState::Outside; } const auto groupPadding = groupedPadding(); point -= QPoint(0, groupPadding.top()); for (const auto &part : _parts) { if (part.geometry.contains(point)) { return PointState::GroupPart; } } return PointState::Inside; } TextState GroupedMedia::textState(QPoint point, StateRequest request) const { const auto groupPadding = groupedPadding(); auto result = getPartState(point - QPoint(0, groupPadding.top()), request); if (!result.link && !_caption.isEmpty()) { const auto captionw = width() - st::msgPadding.left() - st::msgPadding.right(); const auto captiony = height() - groupPadding.bottom() - (isBubbleBottom() ? st::msgPadding.bottom() : 0) - _caption.countHeight(captionw); if (QRect(st::msgPadding.left(), captiony, captionw, height() - captiony).contains(point)) { return TextState( _captionItem ? _captionItem : _parent->data().get(), _caption.getState( point - QPoint(st::msgPadding.left(), captiony), captionw, request.forText())); } } else if (_parent->media() == this) { auto fullRight = width(); auto fullBottom = height(); const auto bottomInfoResult = _parent->bottomInfoTextState( fullRight, fullBottom, point, InfoDisplayType::Image); if (bottomInfoResult.link || bottomInfoResult.cursor != CursorState::None || bottomInfoResult.customTooltip) { return bottomInfoResult; } if (const auto size = _parent->hasBubble() ? std::nullopt : _parent->rightActionSize()) { auto fastShareLeft = (fullRight + st::historyFastShareLeft); auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height()); if (QRect(fastShareLeft, fastShareTop, size->width(), size->height()).contains(point)) { result.link = _parent->rightActionLink(point - QPoint(fastShareLeft, fastShareTop)); } } } return result; } bool GroupedMedia::toggleSelectionByHandlerClick( const ClickHandlerPtr &p) const { for (const auto &part : _parts) { if (part.content->toggleSelectionByHandlerClick(p)) { return true; } } return false; } bool GroupedMedia::dragItemByHandler(const ClickHandlerPtr &p) const { for (const auto &part : _parts) { if (part.content->dragItemByHandler(p)) { return true; } } return false; } TextSelection GroupedMedia::adjustSelection( TextSelection selection, TextSelectType type) const { if (_mode != Mode::Column) { return _caption.adjustSelection(selection, type); } auto checked = 0; for (const auto &part : _parts) { const auto modified = ShiftItemSelection( part.content->adjustSelection( UnshiftItemSelection(selection, checked), type), checked); const auto till = checked + part.content->fullSelectionLength(); if (selection.from >= checked && selection.from < till) { selection.from = modified.from; } if (selection.to <= till) { selection.to = modified.to; return selection; } } return selection; } uint16 GroupedMedia::fullSelectionLength() const { if (_mode != Mode::Column) { return _caption.length(); } auto result = 0; for (const auto &part : _parts) { result += part.content->fullSelectionLength(); } return result; } bool GroupedMedia::hasTextForCopy() const { if (_mode != Mode::Column) { return !_caption.isEmpty(); } for (const auto &part : _parts) { if (part.content->hasTextForCopy()) { return true; } } return false; } TextForMimeData GroupedMedia::selectedText( TextSelection selection) const { if (_mode != Mode::Column) { return _caption.toTextForMimeData(selection); } auto result = TextForMimeData(); for (const auto &part : _parts) { auto text = part.content->selectedText(selection); if (!text.empty()) { if (result.empty()) { result = std::move(text); } else { result.append(u"\n\n"_q).append(std::move(text)); } } selection = part.content->skipSelection(selection); } return result; } auto GroupedMedia::getBubbleSelectionIntervals( TextSelection selection) const -> std::vector { if (_mode != Mode::Column) { return {}; } auto result = std::vector(); for (auto i = 0, count = int(_parts.size()); i != count; ++i) { const auto &part = _parts[i]; if (!IsGroupItemSelection(selection, i)) { continue; } const auto &geometry = part.geometry; if (result.empty() || (result.back().top + result.back().height < geometry.top()) || (result.back().top > geometry.top() + geometry.height())) { result.push_back({ geometry.top(), geometry.height() }); } else { auto &last = result.back(); const auto newTop = std::min(last.top, geometry.top()); const auto newHeight = std::max( last.top + last.height - newTop, geometry.top() + geometry.height() - newTop); last = Ui::BubbleSelectionInterval{ newTop, newHeight }; } } const auto groupPadding = groupedPadding(); for (auto &part : result) { part.top += groupPadding.top(); } if (IsGroupItemSelection(selection, 0)) { result.front().top -= groupPadding.top(); result.front().height += groupPadding.top(); } if (IsGroupItemSelection(selection, _parts.size() - 1)) { result.back().height = height() - result.back().top; } return result; } void GroupedMedia::clickHandlerActiveChanged( const ClickHandlerPtr &p, bool active) { for (const auto &part : _parts) { part.content->clickHandlerActiveChanged(p, active); } } void GroupedMedia::clickHandlerPressedChanged( const ClickHandlerPtr &p, bool pressed) { for (const auto &part : _parts) { part.content->clickHandlerPressedChanged(p, pressed); if (pressed && part.content->dragItemByHandler(p)) { // #TODO drag by item from album // App::pressedLinkItem(part.view); } } } template bool GroupedMedia::applyGroup(const DataMediaRange &medias) { if (validateGroupParts(medias)) { return true; } auto modeChosen = false; for (const auto media : medias) { const auto mediaMode = DetectMode(media); if (!modeChosen) { _mode = mediaMode; modeChosen = true; } else if (mediaMode != _mode) { continue; } _parts.push_back(Part(_parent, media)); } if (_parts.empty()) { return false; } refreshCaption(); Ensures(_parts.size() <= kMaxSize); return true; } template bool GroupedMedia::validateGroupParts( const DataMediaRange &medias) const { auto i = 0; const auto count = _parts.size(); for (const auto media : medias) { if (i >= count || _parts[i].item != media->parent()) { return false; } ++i; } return (i == count); } void GroupedMedia::refreshCaption() { using PartPtrOpt = std::optional; const auto captionPart = [&]() -> PartPtrOpt { if (_mode == Mode::Column) { return std::nullopt; } auto result = PartPtrOpt(); for (const auto &part : _parts) { if (!part.item->emptyText()) { if (result) { return std::nullopt; } else { result = ∂ } } } return result; }(); if (captionPart) { const auto &part = (*captionPart); _caption = createCaption(part->item); _captionItem = part->item; } else { _captionItem = nullptr; } } not_null GroupedMedia::main() const { Expects(!_parts.empty()); return _parts.back().content.get(); } TextWithEntities GroupedMedia::getCaption() const { return main()->getCaption(); } void GroupedMedia::hideSpoilers() { _caption.setSpoilerRevealed(false, anim::type::instant); for (const auto &part : _parts) { part.content->hideSpoilers(); } } Storage::SharedMediaTypesMask GroupedMedia::sharedMediaTypes() const { return main()->sharedMediaTypes(); } PhotoData *GroupedMedia::getPhoto() const { return main()->getPhoto(); } DocumentData *GroupedMedia::getDocument() const { return main()->getDocument(); } HistoryMessageEdited *GroupedMedia::displayedEditBadge() const { for (const auto &part : _parts) { if (!part.item->hideEditedBadge()) { if (const auto edited = part.item->Get()) { return edited; } } } return nullptr; } void GroupedMedia::updateNeedBubbleState() { _needBubble = computeNeedBubble(); } void GroupedMedia::stopAnimation() { for (const auto &part : _parts) { part.content->stopAnimation(); } } void GroupedMedia::checkAnimation() { for (const auto &part : _parts) { part.content->checkAnimation(); } } bool GroupedMedia::hasHeavyPart() const { for (const auto &part : _parts) { if (!part.cache.isNull() || part.content->hasHeavyPart()) { return true; } } return false; } void GroupedMedia::unloadHeavyPart() { for (const auto &part : _parts) { part.content->unloadHeavyPart(); part.cacheKey = 0; part.cache = QPixmap(); } _caption.unloadPersistentAnimation(); } void GroupedMedia::parentTextUpdated() { if (_parent->media() == this) { refreshCaption(); history()->owner().requestViewResize(_parent); } } bool GroupedMedia::needsBubble() const { return _needBubble; } QPoint GroupedMedia::resolveCustomInfoRightBottom() const { const auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x()); const auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y()); return QPoint(width() - skipx, height() - skipy); } bool GroupedMedia::computeNeedBubble() const { if (!_caption.isEmpty() || _mode == Mode::Column) { return true; } if (const auto item = _parent->data()) { if (item->repliesAreComments() || item->externalReply() || item->viaBot() || _parent->displayedReply() || _parent->displayForwardedFrom() || _parent->displayFromName() || _parent->displayedTopicButton() ) { return true; } } return false; } bool GroupedMedia::needInfoDisplay() const { return (_mode != Mode::Column) && (_parent->data()->isSending() || _parent->data()->hasFailed() || _parent->isUnderCursor() || _parent->isLastAndSelfMessage()); } } // namespace HistoryView