/* This file is part of Telegram Desktop, the official desktop version of Telegram messaging app, see https://telegram.org Telegram Desktop is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. In addition, as a special exception, the copyright holders give permission to link the code of portions of this program with the OpenSSL library. Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #include "info/media/info_media_list_widget.h" #include "info/info_controller.h" #include "overview/overview_layout.h" #include "history/history_media_types.h" #include "history/history_item.h" #include "window/themes/window_theme.h" #include "window/window_controller.h" #include "storage/file_download.h" #include "ui/widgets/popup_menu.h" #include "lang/lang_keys.h" #include "auth_session.h" #include "mainwidget.h" #include "window/main_window.h" #include "styles/style_overview.h" #include "styles/style_info.h" #include "boxes/peer_list_controllers.h" #include "boxes/confirm_box.h" #include "info/info_top_bar_override.h" #include "core/file_utilities.h" namespace Layout = Overview::Layout; namespace Info { namespace Media { namespace { constexpr auto kPreloadedScreensCount = 4; constexpr auto kPreloadIfLessThanScreens = 2; constexpr auto kPreloadedScreensCountFull = kPreloadedScreensCount + 1 + kPreloadedScreensCount; constexpr auto kMediaCountForSearch = 10; UniversalMsgId GetUniversalId(FullMsgId itemId) { return (itemId.channel != 0) ? UniversalMsgId(itemId.msg) : UniversalMsgId(itemId.msg - ServerMaxMsgId); } UniversalMsgId GetUniversalId(not_null item) { return GetUniversalId(item->fullId()); } UniversalMsgId GetUniversalId(not_null layout) { return GetUniversalId(layout->getItem()->fullId()); } } // namespace struct ListWidget::Context { Layout::PaintContext layoutContext; not_null selected; not_null dragSelected; DragSelectAction dragSelectAction; }; class ListWidget::Section { public: Section(Type type) : _type(type) { } bool addItem(not_null item); bool empty() const { return _items.empty(); } UniversalMsgId minId() const { Expects(!empty()); return _items.back().first; } UniversalMsgId maxId() const { Expects(!empty()); return _items.front().first; } void setTop(int top) { _top = top; } int top() const { return _top; } void resizeToWidth(int newWidth); int height() const { return _height; } bool removeItem(UniversalMsgId universalId); FoundItem findItemNearId(UniversalMsgId universalId) const; FoundItem findItemByPoint(QPoint point) const; void paint( Painter &p, const Context &context, QRect clip, int outerWidth) const; static int MinItemHeight(Type type, int width); private: using Items = base::flat_map< UniversalMsgId, not_null, std::greater<>>; int headerHeight() const; void appendItem(not_null item); void setHeader(not_null item); bool belongsHere(not_null item) const; Items::iterator findItemAfterTop(int top); Items::const_iterator findItemAfterTop(int top) const; Items::const_iterator findItemAfterBottom( Items::const_iterator from, int bottom) const; QRect findItemRect(not_null item) const; FoundItem completeResult( not_null item, bool exact) const; TextSelection itemSelection( not_null item, const Context &context) const; int recountHeight() const; void refreshHeight(); Type _type = Type::Photo; Text _header; Items _items; int _itemsLeft = 0; int _itemsTop = 0; int _itemWidth = 0; int _itemsInRow = 1; mutable int _rowsCount = 0; int _top = 0; int _height = 0; }; bool ListWidget::IsAfter( const CursorState &a, const CursorState &b) { if (a.itemId != b.itemId) { return (a.itemId < b.itemId); } auto xAfter = a.cursor.x() - b.cursor.x(); auto yAfter = a.cursor.y() - b.cursor.y(); return (xAfter + yAfter >= 0); } bool ListWidget::SkipSelectFromItem(const CursorState &state) { if (state.cursor.y() >= state.size.height() || state.cursor.x() >= state.size.width()) { return true; } return false; } bool ListWidget::SkipSelectTillItem(const CursorState &state) { if (state.cursor.x() < 0 || state.cursor.y() < 0) { return true; } return false; } ListWidget::CachedItem::CachedItem(std::unique_ptr item) : item(std::move(item)) { } ListWidget::CachedItem::~CachedItem() = default; bool ListWidget::Section::addItem(not_null item) { if (_items.empty() || belongsHere(item)) { if (_items.empty()) setHeader(item); appendItem(item); return true; } return false; } void ListWidget::Section::setHeader(not_null item) { auto text = [&] { auto date = item->getItem()->date.date(); switch (_type) { case Type::Photo: case Type::Video: case Type::RoundFile: case Type::VoiceFile: case Type::File: return langMonthFull(date); case Type::Link: return langDayOfMonthFull(date); case Type::MusicFile: return QString(); } Unexpected("Type in ListWidget::Section::setHeader()"); }(); _header.setText(st::infoMediaHeaderStyle, text); } bool ListWidget::Section::belongsHere( not_null item) const { Expects(!_items.empty()); auto date = item->getItem()->date.date(); auto myDate = _items.back().second->getItem()->date.date(); switch (_type) { case Type::Photo: case Type::Video: case Type::RoundFile: case Type::VoiceFile: case Type::File: return date.year() == myDate.year() && date.month() == myDate.month(); case Type::Link: return date.year() == myDate.year() && date.month() == myDate.month() && date.day() == myDate.day(); case Type::MusicFile: return true; } Unexpected("Type in ListWidget::Section::belongsHere()"); } void ListWidget::Section::appendItem(not_null item) { _items.emplace(GetUniversalId(item), item); } bool ListWidget::Section::removeItem(UniversalMsgId universalId) { if (auto it = _items.find(universalId); it != _items.end()) { it = _items.erase(it); refreshHeight(); return true; } return false; } QRect ListWidget::Section::findItemRect( not_null item) const { auto position = item->position(); auto top = position / _itemsInRow; auto indexInRow = position % _itemsInRow; auto left = _itemsLeft + indexInRow * (_itemWidth + st::infoMediaSkip); return QRect(left, top, _itemWidth, item->height()); } auto ListWidget::Section::completeResult( not_null item, bool exact) const -> FoundItem { return { item, findItemRect(item), exact }; } auto ListWidget::Section::findItemByPoint( QPoint point) const -> FoundItem { Expects(!_items.empty()); auto itemIt = findItemAfterTop(point.y()); if (itemIt == _items.end()) { --itemIt; } auto item = itemIt->second; auto rect = findItemRect(item); if (point.y() >= rect.top()) { auto shift = floorclamp( point.x(), (_itemWidth + st::infoMediaSkip), 0, _itemsInRow); while (shift-- && itemIt != _items.end()) { ++itemIt; } if (itemIt == _items.end()) { --itemIt; } item = itemIt->second; rect = findItemRect(item); } return { item, rect, rect.contains(point) }; } auto ListWidget::Section::findItemNearId( UniversalMsgId universalId) const -> FoundItem { Expects(!_items.empty()); auto itemIt = base::lower_bound( _items, universalId, [this](const auto &item, UniversalMsgId universalId) { return (item.first > universalId); }); if (itemIt == _items.end()) { --itemIt; } auto item = itemIt->second; auto exact = (GetUniversalId(item) == universalId); return { item, findItemRect(item), exact }; } auto ListWidget::Section::findItemAfterTop( int top) -> Items::iterator { return base::lower_bound( _items, top, [this](const auto &item, int top) { auto itemTop = item.second->position() / _itemsInRow; return (itemTop + item.second->height()) <= top; }); } auto ListWidget::Section::findItemAfterTop( int top) const -> Items::const_iterator { return base::lower_bound( _items, top, [this](const auto &item, int top) { auto itemTop = item.second->position() / _itemsInRow; return (itemTop + item.second->height()) <= top; }); } auto ListWidget::Section::findItemAfterBottom( Items::const_iterator from, int bottom) const -> Items::const_iterator { return std::lower_bound( from, _items.end(), bottom, [this](const auto &item, int bottom) { auto itemTop = item.second->position() / _itemsInRow; return itemTop < bottom; }); } void ListWidget::Section::paint( Painter &p, const Context &context, QRect clip, int outerWidth) const { auto baseIndex = 0; auto header = headerHeight(); if (QRect(0, 0, outerWidth, header).intersects(clip)) { p.setPen(st::infoMediaHeaderFg); _header.drawLeftElided( p, st::infoMediaHeaderPosition.x(), st::infoMediaHeaderPosition.y(), outerWidth - 2 * st::infoMediaHeaderPosition.x(), outerWidth); } auto top = header + _itemsTop; auto fromcol = floorclamp( clip.x() - _itemsLeft, _itemWidth, 0, _itemsInRow); auto tillcol = ceilclamp( clip.x() + clip.width() - _itemsLeft, _itemWidth, 0, _itemsInRow); auto localContext = context.layoutContext; localContext.isAfterDate = (header > 0); auto fromIt = findItemAfterTop(clip.y()); auto tillIt = findItemAfterBottom( fromIt, clip.y() + clip.height()); for (auto it = fromIt; it != tillIt; ++it) { auto item = it->second; auto rect = findItemRect(item); localContext.isAfterDate = (header > 0) && (rect.y() <= header + _itemsTop); if (rect.intersects(clip)) { p.translate(rect.topLeft()); item->paint( p, clip.translated(-rect.topLeft()), itemSelection(item, context), &localContext); p.translate(-rect.topLeft()); } } } TextSelection ListWidget::Section::itemSelection( not_null item, const Context &context) const { auto universalId = GetUniversalId(item); auto dragSelectAction = context.dragSelectAction; if (dragSelectAction != DragSelectAction::None) { auto i = context.dragSelected->find(universalId); if (i != context.dragSelected->end()) { return (dragSelectAction == DragSelectAction::Selecting) ? FullSelection : TextSelection(); } } auto i = context.selected->find(universalId); return (i == context.selected->cend()) ? TextSelection() : i->second.text; } int ListWidget::Section::headerHeight() const { return _header.isEmpty() ? 0 : st::infoMediaHeaderHeight; } void ListWidget::Section::resizeToWidth(int newWidth) { auto minWidth = st::infoMediaMinGridSize + st::infoMediaSkip * 2; if (newWidth < minWidth) { return; } auto resizeOneColumn = [&](int itemsLeft, int itemWidth) { _itemsLeft = itemsLeft; _itemsTop = 0; _itemsInRow = 1; _itemWidth = itemWidth; for (auto &item : _items) { item.second->resizeGetHeight(_itemWidth); } }; switch (_type) { case Type::Photo: case Type::Video: case Type::RoundFile: { _itemsLeft = st::infoMediaSkip; _itemsTop = st::infoMediaSkip; _itemsInRow = (newWidth - _itemsLeft) / (st::infoMediaMinGridSize + st::infoMediaSkip); _itemWidth = ((newWidth - _itemsLeft) / _itemsInRow) - st::infoMediaSkip; for (auto &item : _items) { item.second->resizeGetHeight(_itemWidth); } } break; case Type::VoiceFile: case Type::MusicFile: resizeOneColumn(0, newWidth); break; case Type::File: case Type::Link: { auto itemsLeft = st::infoMediaHeaderPosition.x(); auto itemWidth = newWidth - 2 * itemsLeft; resizeOneColumn(itemsLeft, itemWidth); } break; } refreshHeight(); } int ListWidget::Section::MinItemHeight(Type type, int width) { auto &songSt = st::overviewFileLayout; switch (type) { case Type::Photo: case Type::Video: case Type::RoundFile: { auto itemsLeft = st::infoMediaSkip; auto itemsInRow = (width - itemsLeft) / (st::infoMediaMinGridSize + st::infoMediaSkip); return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow; } break; case Type::VoiceFile: return songSt.songPadding.top() + songSt.songThumbSize + songSt.songPadding.bottom() + st::lineWidth; case Type::File: return songSt.filePadding.top() + songSt.fileThumbSize + songSt.filePadding.bottom() + st::lineWidth; case Type::MusicFile: return songSt.songPadding.top() + songSt.songThumbSize + songSt.songPadding.bottom(); case Type::Link: return st::linksPhotoSize + st::linksMargin.top() + st::linksMargin.bottom() + st::linksBorder; } Unexpected("Type in ListWidget::Section::MinItemHeight()"); } int ListWidget::Section::recountHeight() const { auto result = headerHeight(); switch (_type) { case Type::Photo: case Type::Video: case Type::RoundFile: { auto itemHeight = _itemWidth + st::infoMediaSkip; auto index = 0; result += _itemsTop; for (auto &item : _items) { item.second->setPosition(_itemsInRow * result + index); if (++index == _itemsInRow) { result += itemHeight; index = 0; } } if (_items.size() % _itemsInRow) { _rowsCount = int(_items.size()) / _itemsInRow + 1; result += itemHeight; } else { _rowsCount = int(_items.size()) / _itemsInRow; } } break; case Type::VoiceFile: case Type::File: case Type::MusicFile: case Type::Link: for (auto &item : _items) { item.second->setPosition(result); result += item.second->height(); } _rowsCount = _items.size(); break; } return result; } void ListWidget::Section::refreshHeight() { _height = recountHeight(); } ListWidget::ListWidget( QWidget *parent, not_null controller) : RpWidget(parent) , _controller(controller) , _peer(_controller->peer()) , _migrated(_controller->migrated()) , _type(_controller->section().mediaType()) , _slice(sliceKey(_universalAroundId)) { setAttribute(Qt::WA_MouseTracking); start(); refreshViewer(); } void ListWidget::start() { _controller->setSearchEnabledByContent(false); ObservableViewer(*Window::Theme::Background()) | rpl::start_with_next([this](const auto &update) { if (update.paletteChanged()) { invalidatePaletteCache(); } }, lifetime()); ObservableViewer(Auth().downloader().taskFinished()) | rpl::start_with_next([this] { update(); }, lifetime()); Auth().data().itemLayoutChanged() | rpl::start_with_next([this](auto item) { itemLayoutChanged(item); }, lifetime()); Auth().data().itemRemoved() | rpl::start_with_next([this](auto item) { itemRemoved(item); }, lifetime()); Auth().data().itemRepaintRequest() | rpl::start_with_next([this](auto item) { repaintItem(item); }, lifetime()); _controller->mediaSourceChanged() | rpl::start_with_next([this]{ restart(); }, lifetime()); } void ListWidget::restart() { mouseActionCancel(); _overLayout = nullptr; _sections.clear(); _layouts.clear(); _universalAroundId = kDefaultAroundId; _idsLimit = kMinimalIdsLimit; _slice = SparseIdsMergedSlice(sliceKey(_universalAroundId)); refreshViewer(); } void ListWidget::itemRemoved(not_null item) { if (isMyItem(item)) { auto universalId = GetUniversalId(item); auto sectionIt = findSectionByItem(universalId); if (sectionIt != _sections.end()) { if (sectionIt->removeItem(universalId)) { auto top = sectionIt->top(); if (sectionIt->empty()) { _sections.erase(sectionIt); } refreshHeight(); } } if (isItemLayout(item, _overLayout)) { _overLayout = nullptr; } _layouts.erase(universalId); _dragSelected.remove(universalId); auto i = _selected.find(universalId); if (i != _selected.cend()) { removeItemSelection(i); } mouseActionUpdate(_mousePosition); } } FullMsgId ListWidget::computeFullId( UniversalMsgId universalId) const { Expects(universalId != 0); auto peerChannel = [&] { return _peer->isChannel() ? _peer->bareId() : NoChannel; }; return (universalId > 0) ? FullMsgId(peerChannel(), universalId) : FullMsgId(NoChannel, ServerMaxMsgId + universalId); } auto ListWidget::collectSelectedItems() const -> SelectedItems { auto convert = [&]( UniversalMsgId universalId, const SelectionData &selection) { auto result = SelectedItem(computeFullId(universalId)); result.canDelete = selection.canDelete; result.canForward = selection.canForward; return result; }; auto transformation = [&](const auto &item) { return convert(item.first, item.second); }; auto items = SelectedItems(_type); if (hasSelectedItems()) { items.list.reserve(_selected.size()); std::transform( _selected.begin(), _selected.end(), std::back_inserter(items.list), transformation); } return items; } SelectedItemSet ListWidget::collectSelectedSet() const { auto items = SelectedItemSet(); if (hasSelectedItems()) { for (auto &data : _selected) { auto fullId = computeFullId(data.first); if (auto item = App::histItemById(fullId)) { items.insert(items.size(), item); } } } return items; } void ListWidget::pushSelectedItems() { _selectedListStream.fire(collectSelectedItems()); } bool ListWidget::hasSelected() const { return !_selected.empty(); } bool ListWidget::isSelectedItem( const SelectedMap::const_iterator &i) const { return (i != _selected.end()) && (i->second.text == FullSelection); } void ListWidget::removeItemSelection( const SelectedMap::const_iterator &i) { Expects(i != _selected.cend()); _selected.erase(i); if (_selected.empty()) { update(); } pushSelectedItems(); } bool ListWidget::hasSelectedText() const { return hasSelected() && !hasSelectedItems(); } bool ListWidget::hasSelectedItems() const { return isSelectedItem(_selected.cbegin()); } void ListWidget::itemLayoutChanged( not_null item) { if (isItemLayout(item, _overLayout)) { mouseActionUpdate(); } } void ListWidget::repaintItem(const HistoryItem *item) { if (item && isMyItem(item)) { repaintItem(GetUniversalId(item)); } } void ListWidget::repaintItem(UniversalMsgId universalId) { if (auto item = findItemById(universalId)) { repaintItem(item->geometry); } } void ListWidget::repaintItem(const BaseLayout *item) { if (item) { repaintItem(GetUniversalId(item)); } } void ListWidget::repaintItem(QRect itemGeometry) { rtlupdate(itemGeometry); } bool ListWidget::isMyItem(not_null item) const { auto peer = item->history()->peer; return (_peer == peer) || (_migrated == peer); } bool ListWidget::isPossiblyMyId(FullMsgId fullId) const { return (fullId.channel != 0) ? (_peer->isChannel() && _peer->bareId() == fullId.channel) : (!_peer->isChannel() || _migrated); } bool ListWidget::isItemLayout( not_null item, BaseLayout *layout) const { return layout && (layout->getItem() == item); } void ListWidget::invalidatePaletteCache() { for (auto &layout : _layouts) { layout.second.item->invalidateCache(); } } SparseIdsMergedSlice::Key ListWidget::sliceKey( UniversalMsgId universalId) const { using Key = SparseIdsMergedSlice::Key; if (_migrated) { return Key(_peer->id, _migrated->id, universalId); } if (universalId < 0) { // Convert back to plain id for non-migrated histories. universalId += ServerMaxMsgId; } return Key(_peer->id, 0, universalId); } void ListWidget::refreshViewer() { _viewerLifetime.destroy(); _controller->mediaSource( sliceKey(_universalAroundId).universalId, _idsLimit, _idsLimit) | rpl::start_with_next([this]( SparseIdsMergedSlice &&slice) { _slice = std::move(slice); if (auto nearest = _slice.nearest(_universalAroundId)) { _universalAroundId = *nearest; } refreshRows(); }, _viewerLifetime); } BaseLayout *ListWidget::getLayout(UniversalMsgId universalId) { auto it = _layouts.find(universalId); if (it == _layouts.end()) { if (auto layout = createLayout(universalId, _type)) { layout->initDimensions(); it = _layouts.emplace( universalId, std::move(layout)).first; } else { return nullptr; } } it->second.stale = false; return it->second.item.get(); } BaseLayout *ListWidget::getExistingLayout( UniversalMsgId universalId) const { auto it = _layouts.find(universalId); return (it != _layouts.end()) ? it->second.item.get() : nullptr; } std::unique_ptr ListWidget::createLayout( UniversalMsgId universalId, Type type) { auto item = App::histItemById(computeFullId(universalId)); if (!item) { return nullptr; } auto getPhoto = [&]() -> PhotoData* { if (auto media = item->getMedia()) { if (media->type() == MediaTypePhoto) { return static_cast(media)->photo(); } } return nullptr; }; auto getFile = [&]() -> DocumentData* { if (auto media = item->getMedia()) { return media->getDocument(); } return nullptr; }; auto &songSt = st::overviewFileLayout; using namespace Layout; switch (type) { case Type::Photo: if (auto photo = getPhoto()) { return std::make_unique(item, photo); } return nullptr; case Type::Video: if (auto file = getFile()) { return std::make_unique