tdesktop/Telegram/SourceFiles/info/media/info_media_list_widget.cpp

2527 lines
66 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 "info/media/info_media_list_widget.h"
#include "info/info_controller.h"
#include "overview/overview_layout.h"
#include "layout/layout_mosaic.h"
#include "layout/layout_selection.h"
#include "data/data_media_types.h"
#include "data/data_photo.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_peer_values.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_file_click_handler.h"
#include "data/data_file_origin.h"
#include "history/history_item.h"
#include "history/history.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_service_message.h"
#include "window/window_session_controller.h"
#include "window/window_peer_menu.h"
#include "ui/widgets/popup_menu.h"
#include "ui/controls/delete_message_context_action.h"
#include "ui/chat/chat_style.h"
#include "ui/cached_round_corners.h"
#include "ui/ui_utility.h"
#include "ui/inactive_press.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "styles/style_overview.h"
#include "styles/style_info.h"
#include "base/platform/base_platform_info.h"
#include "base/weak_ptr.h"
#include "media/player/media_player_instance.h"
#include "boxes/delete_messages_box.h"
#include "boxes/peer_list_controllers.h"
#include "core/file_utilities.h"
#include "facades.h"
#include <QtWidgets/QApplication>
#include <QtGui/QClipboard>
namespace Info {
namespace Media {
namespace {
constexpr auto kFloatingHeaderAlpha = 0.9;
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<const HistoryItem*> item) {
return GetUniversalId(item->fullId());
}
UniversalMsgId GetUniversalId(not_null<const BaseLayout*> layout) {
return GetUniversalId(layout->getItem()->fullId());
}
bool HasFloatingHeader(Type type) {
switch (type) {
case Type::Photo:
case Type::GIF:
case Type::Video:
case Type::RoundFile:
case Type::RoundVoiceFile:
case Type::MusicFile:
return false;
case Type::File:
case Type::Link:
return true;
}
Unexpected("Type in HasFloatingHeader()");
}
} // namespace
struct ListWidget::Context {
Overview::Layout::PaintContext layoutContext;
not_null<SelectedMap*> selected;
not_null<SelectedMap*> dragSelected;
DragSelectAction dragSelectAction;
};
struct ListWidget::DateBadge {
DateBadge(Type type, Fn<void()> checkCallback, Fn<void()> hideCallback);
SingleQueuedInvokation check;
base::Timer hideTimer;
Ui::Animations::Simple opacity;
Ui::CornersPixmaps corners;
bool goodType = false;
bool shown = false;
QString text;
int textWidth = 0;
QRect rect;
};
class ListWidget::Section {
public:
Section(Type type)
: _type(type)
, _hasFloatingHeader(HasFloatingHeader(type))
, _mosaic(st::emojiPanWidth - st::inlineResultsLeft) {
}
bool addItem(not_null<BaseLayout*> item);
void finishSection();
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;
}
int bottom() const {
return top() + height();
}
bool removeItem(UniversalMsgId universalId);
FoundItem findItemNearId(UniversalMsgId universalId) const;
FoundItem findItemDetails(not_null<BaseLayout*> item) const;
FoundItem findItemByPoint(QPoint point) const;
void paint(
Painter &p,
const Context &context,
QRect clip,
int outerWidth) const;
void paintFloatingHeader(Painter &p, int visibleTop, int outerWidth);
static int MinItemHeight(Type type, int width);
private:
using Items = base::flat_map<
UniversalMsgId,
not_null<BaseLayout*>,
std::greater<>>;
int headerHeight() const;
void appendItem(not_null<BaseLayout*> item);
void setHeader(not_null<BaseLayout*> item);
bool belongsHere(not_null<BaseLayout*> 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<const BaseLayout*> item) const;
FoundItem completeResult(
not_null<BaseLayout*> item,
bool exact) const;
TextSelection itemSelection(
not_null<const BaseLayout*> item,
const Context &context) const;
int recountHeight();
void refreshHeight();
Type _type = Type::Photo;
bool _hasFloatingHeader = false;
Ui::Text::String _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;
Mosaic::Layout::MosaicLayout<BaseLayout> _mosaic;
};
bool ListWidget::IsAfter(
const MouseState &a,
const MouseState &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 MouseState &state) {
if (state.cursor.y() >= state.size.height()
|| state.cursor.x() >= state.size.width()) {
return true;
}
return false;
}
bool ListWidget::SkipSelectTillItem(const MouseState &state) {
if (state.cursor.x() < 0 || state.cursor.y() < 0) {
return true;
}
return false;
}
ListWidget::CachedItem::CachedItem(std::unique_ptr<BaseLayout> item)
: item(std::move(item)) {
}
ListWidget::CachedItem::CachedItem(CachedItem &&other) = default;
ListWidget::CachedItem &ListWidget::CachedItem::operator=(
CachedItem && other) = default;
ListWidget::CachedItem::~CachedItem() = default;
ListWidget::DateBadge::DateBadge(
Type type,
Fn<void()> checkCallback,
Fn<void()> hideCallback)
: check(std::move(checkCallback))
, hideTimer(std::move(hideCallback))
, goodType(type == Type::Photo
|| type == Type::Video
|| type == Type::GIF) {
}
bool ListWidget::Section::addItem(not_null<BaseLayout*> item) {
if (_items.empty() || belongsHere(item)) {
if (_items.empty()) setHeader(item);
appendItem(item);
return true;
}
return false;
}
void ListWidget::Section::finishSection() {
if (_type == Type::GIF) {
_mosaic.setOffset(st::infoMediaSkip, headerHeight());
_mosaic.setRightSkip(st::infoMediaSkip);
const auto items = ranges::views::values(_items) | ranges::to_vector;
_mosaic.addItems(items);
}
}
void ListWidget::Section::setHeader(not_null<BaseLayout*> item) {
auto text = [&] {
auto date = item->dateTime().date();
switch (_type) {
case Type::Photo:
case Type::GIF:
case Type::Video:
case Type::RoundFile:
case Type::RoundVoiceFile:
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<BaseLayout*> item) const {
Expects(!_items.empty());
auto date = item->dateTime().date();
auto myDate = _items.back().second->dateTime().date();
switch (_type) {
case Type::Photo:
case Type::GIF:
case Type::Video:
case Type::RoundFile:
case Type::RoundVoiceFile:
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<BaseLayout*> 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<const BaseLayout*> item) const {
auto position = item->position();
if (!_mosaic.empty()) {
return _mosaic.findRect(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<BaseLayout*> item,
bool exact) const -> FoundItem {
return { item, findItemRect(item), exact };
}
auto ListWidget::Section::findItemByPoint(
QPoint point) const -> FoundItem {
Expects(!_items.empty());
if (!_mosaic.empty()) {
const auto found = _mosaic.findByPoint(point);
Assert(found.index != -1);
const auto item = _mosaic.itemAt(found.index);
const auto rect = findItemRect(item);
return { item, rect, found.exact };
}
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 = ranges::lower_bound(
_items,
universalId,
std::greater<>(),
[](const auto &item) -> UniversalMsgId { return item.first; });
if (itemIt == _items.end()) {
--itemIt;
}
auto item = itemIt->second;
auto exact = (GetUniversalId(item) == universalId);
return { item, findItemRect(item), exact };
}
auto ListWidget::Section::findItemDetails(not_null<BaseLayout*> item) const
-> FoundItem {
return { item, findItemRect(item), true };
}
auto ListWidget::Section::findItemAfterTop(
int top) -> Items::iterator {
Expects(_mosaic.empty());
return ranges::lower_bound(
_items,
top,
std::less_equal<>(),
[this](const auto &item) {
auto itemTop = item.second->position() / _itemsInRow;
return itemTop + item.second->height();
});
}
auto ListWidget::Section::findItemAfterTop(
int top) const -> Items::const_iterator {
Expects(_mosaic.empty());
return ranges::lower_bound(
_items,
top,
std::less_equal<>(),
[this](const auto &item) {
auto itemTop = item.second->position() / _itemsInRow;
return itemTop + item.second->height();
});
}
auto ListWidget::Section::findItemAfterBottom(
Items::const_iterator from,
int bottom) const -> Items::const_iterator {
Expects(_mosaic.empty());
return ranges::lower_bound(
from,
_items.end(),
bottom,
std::less<>(),
[this](const auto &item) {
auto itemTop = item.second->position() / _itemsInRow;
return itemTop;
});
}
void ListWidget::Section::paint(
Painter &p,
const Context &context,
QRect clip,
int outerWidth) const {
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 localContext = context.layoutContext;
localContext.isAfterDate = (header > 0);
if (!_mosaic.empty()) {
auto paintItem = [&](not_null<BaseLayout*> item, QPoint point) {
p.translate(point.x(), point.y());
item->paint(
p,
clip.translated(-point),
itemSelection(item, context),
&localContext);
p.translate(-point.x(), -point.y());
};
_mosaic.paint(std::move(paintItem), clip);
return;
}
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());
}
}
}
void ListWidget::Section::paintFloatingHeader(
Painter &p,
int visibleTop,
int outerWidth) {
if (!_hasFloatingHeader) {
return;
}
const auto headerTop = st::infoMediaHeaderPosition.y() / 2;
if (visibleTop <= (_top + headerTop)) {
return;
}
const auto header = headerHeight();
const auto headerLeft = st::infoMediaHeaderPosition.x();
const auto floatingTop = std::min(
visibleTop,
bottom() - header + headerTop);
p.save();
p.resetTransform();
p.setOpacity(kFloatingHeaderAlpha);
p.fillRect(QRect(0, floatingTop, outerWidth, header), st::boxBg);
p.setOpacity(1.0);
p.setPen(st::infoMediaHeaderFg);
_header.drawLeftElided(
p,
headerLeft,
floatingTop + headerTop,
outerWidth - 2 * headerLeft,
outerWidth);
p.restore();
}
TextSelection ListWidget::Section::itemSelection(
not_null<const BaseLayout*> 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::GIF: {
_mosaic.setFullWidth(newWidth - st::infoMediaSkip);
} break;
case Type::RoundVoiceFile:
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::GIF:
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::RoundVoiceFile:
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() {
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::GIF: {
return _mosaic.countDesiredHeight(0) + result;
} break;
case Type::RoundVoiceFile:
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<AbstractController*> controller)
: RpWidget(parent)
, _controller(controller)
, _peer(_controller->key().peer())
, _migrated(_controller->migrated())
, _type(_controller->section().mediaType())
, _slice(sliceKey(_universalAroundId))
, _dateBadge(std::make_unique<DateBadge>(
_type,
[=] { scrollDateCheck(); },
[=] { scrollDateHide(); })) {
setMouseTracking(true);
start();
}
Main::Session &ListWidget::session() const {
return _controller->session();
}
void ListWidget::start() {
_controller->setSearchEnabledByContent(false);
style::PaletteChanged(
) | rpl::start_with_next([=] {
invalidatePaletteCache();
}, lifetime());
session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
update();
}, lifetime());
session().data().itemLayoutChanged(
) | rpl::start_with_next([this](auto item) {
itemLayoutChanged(item);
}, lifetime());
session().data().itemRemoved(
) | rpl::start_with_next([this](auto item) {
itemRemoved(item);
}, lifetime());
session().data().itemRepaintRequest(
) | rpl::start_with_next([this](auto item) {
repaintItem(item);
}, lifetime());
_controller->mediaSourceQueryValue(
) | rpl::start_with_next([this] {
restart();
}, lifetime());
setupSelectRestriction();
}
void ListWidget::setupSelectRestriction() {
if (_peer->isUser()) {
return;
}
const auto chat = _peer->asChat();
const auto channel = _peer->asChannel();
auto noForwards = chat
? Data::PeerFlagValue(chat, ChatDataFlag::NoForwards)
: Data::PeerFlagValue(
channel,
ChannelDataFlag::NoForwards
) | rpl::type_erased();
auto rights = chat
? chat->adminRightsValue()
: channel->adminRightsValue();
auto canDelete = std::move(
rights
) | rpl::map([=] {
return chat
? chat->canDeleteMessages()
: channel->canDeleteMessages();
});
rpl::combine(
std::move(noForwards),
std::move(canDelete)
) | rpl::filter([=] {
return hasSelectRestriction() && hasSelectedItems();
}) | rpl::start_with_next([=] {
clearSelected();
if (_mouseAction == MouseAction::PrepareSelect) {
mouseActionCancel();
}
}, lifetime());
}
rpl::producer<int> ListWidget::scrollToRequests() const {
return _scrollToRequests.events();
}
rpl::producer<SelectedItems> ListWidget::selectedListValue() const {
return _selectedListStream.events_starting_with(
collectSelectedItems());
}
QRect ListWidget::getCurrentSongGeometry() {
const auto type = AudioMsgId::Type::Song;
const auto current = ::Media::Player::instance()->current(type);
const auto fullMsgId = current.contextId();
if (fullMsgId && isPossiblyMyId(fullMsgId)) {
if (const auto item = findItemById(GetUniversalId(fullMsgId))) {
return item->geometry;
}
}
return QRect(0, 0, width(), 0);
}
void ListWidget::restart() {
mouseActionCancel();
_overLayout = nullptr;
_sections.clear();
_layouts.clear();
_heavyLayouts.clear();
_universalAroundId = kDefaultAroundId;
_idsLimit = kMinimalIdsLimit;
_slice = SparseIdsMergedSlice(sliceKey(_universalAroundId));
refreshViewer();
}
void ListWidget::itemRemoved(not_null<const HistoryItem*> item) {
if (!isMyItem(item)) {
return;
}
auto id = GetUniversalId(item);
auto needHeightRefresh = false;
auto sectionIt = findSectionByItem(id);
if (sectionIt != _sections.end()) {
if (sectionIt->removeItem(id)) {
if (sectionIt->empty()) {
_sections.erase(sectionIt);
}
needHeightRefresh = true;
}
}
if (isItemLayout(item, _overLayout)) {
_overLayout = nullptr;
}
if (const auto i = _layouts.find(id); i != _layouts.end()) {
_heavyLayouts.remove(i->second.item.get());
_layouts.erase(i);
}
_dragSelected.remove(id);
if (const auto i = _selected.find(id); i != _selected.cend()) {
removeItemSelection(i);
}
if (needHeightRefresh) {
refreshHeight();
}
mouseActionUpdate(_mousePosition);
}
FullMsgId ListWidget::computeFullId(
UniversalMsgId universalId) const {
Expects(universalId != 0);
return (universalId > 0)
? FullMsgId(peerToChannel(_peer->id), 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;
}
MessageIdsList ListWidget::collectSelectedIds() const {
const auto selected = collectSelectedItems();
return ranges::views::all(
selected.list
) | ranges::views::transform([](const SelectedItem &item) {
return item.msgId;
}) | ranges::to_vector;
}
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<const HistoryItem*> 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(not_null<const BaseLayout*> item) {
repaintItem(GetUniversalId(item));
}
void ListWidget::repaintItem(QRect itemGeometry) {
rtlupdate(itemGeometry);
}
bool ListWidget::isMyItem(not_null<const HistoryItem*> item) const {
auto peer = item->history()->peer;
return (_peer == peer) || (_migrated == peer);
}
bool ListWidget::isPossiblyMyId(FullMsgId fullId) const {
return fullId.channel
? (_peer->isChannel() && peerToChannel(_peer->id) == fullId.channel)
: (!_peer->isChannel() || _migrated);
}
bool ListWidget::isItemLayout(
not_null<const HistoryItem*> item,
BaseLayout *layout) const {
return layout && (layout->getItem() == item);
}
void ListWidget::invalidatePaletteCache() {
for (auto &layout : _layouts) {
layout.second.item->invalidateCache();
}
}
void ListWidget::registerHeavyItem(not_null<const BaseLayout*> item) {
if (!_heavyLayouts.contains(item)) {
_heavyLayouts.emplace(item);
_heavyLayoutsInvalidated = true;
}
}
void ListWidget::unregisterHeavyItem(not_null<const BaseLayout*> item) {
const auto i = _heavyLayouts.find(item);
if (i != _heavyLayouts.end()) {
_heavyLayouts.erase(i);
_heavyLayoutsInvalidated = true;
}
}
bool ListWidget::itemVisible(not_null<const BaseLayout*> item) {
if (const auto &found = findItemById(GetUniversalId(item))) {
const auto geometry = found->geometry;
return (geometry.top() < _visibleBottom)
&& (geometry.top() + geometry.height() > _visibleTop);
}
return true;
}
void ListWidget::openPhoto(not_null<PhotoData*> photo, FullMsgId id) {
_controller->parentController()->openPhoto(photo, id);
}
void ListWidget::openDocument(
not_null<DocumentData*> document,
FullMsgId id,
bool showInMediaView) {
_controller->parentController()->openDocument(
document,
id,
showInMediaView);
}
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 = universalId + ServerMaxMsgId;
}
return Key(_peer->id, 0, universalId);
}
void ListWidget::refreshViewer() {
_viewerLifetime.destroy();
const auto idForViewer = sliceKey(_universalAroundId).universalId;
_controller->mediaSource(
idForViewer,
_idsLimit,
_idsLimit
) | rpl::start_with_next([=](SparseIdsMergedSlice &&slice) {
if (!slice.fullCount()) {
// Don't display anything while full count is unknown.
return;
}
_slice = std::move(slice);
if (auto nearest = _slice.nearest(idForViewer)) {
_universalAroundId = GetUniversalId(*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<BaseLayout> ListWidget::createLayout(
UniversalMsgId universalId,
Type type) {
auto item = session().data().message(computeFullId(universalId));
if (!item) {
return nullptr;
}
auto getPhoto = [&]() -> PhotoData* {
if (const auto media = item->media()) {
return media->photo();
}
return nullptr;
};
auto getFile = [&]() -> DocumentData* {
if (auto media = item->media()) {
return media->document();
}
return nullptr;
};
auto &songSt = st::overviewFileLayout;
using namespace Overview::Layout;
switch (type) {
case Type::Photo:
if (const auto photo = getPhoto()) {
return std::make_unique<Photo>(this, item, photo);
}
return nullptr;
case Type::GIF:
if (const auto file = getFile()) {
return std::make_unique<Gif>(this, item, file);
}
return nullptr;
case Type::Video:
if (const auto file = getFile()) {
return std::make_unique<Video>(this, item, file);
}
return nullptr;
case Type::File:
if (const auto file = getFile()) {
return std::make_unique<Document>(this, item, file, songSt);
}
return nullptr;
case Type::MusicFile:
if (const auto file = getFile()) {
return std::make_unique<Document>(this, item, file, songSt);
}
return nullptr;
case Type::RoundVoiceFile:
if (const auto file = getFile()) {
return std::make_unique<Voice>(this, item, file, songSt);
}
return nullptr;
case Type::Link:
return std::make_unique<Link>(this, item, item->media());
case Type::RoundFile:
return nullptr;
}
Unexpected("Type in ListWidget::createLayout()");
}
void ListWidget::refreshRows() {
saveScrollState();
markLayoutsStale();
_sections.clear();
auto section = Section(_type);
auto count = _slice.size();
for (auto i = count; i != 0;) {
auto universalId = GetUniversalId(_slice[--i]);
if (auto layout = getLayout(universalId)) {
if (!section.addItem(layout)) {
section.finishSection();
_sections.push_back(std::move(section));
section = Section(_type);
section.addItem(layout);
}
}
}
if (!section.empty()) {
section.finishSection();
_sections.push_back(std::move(section));
}
if (auto count = _slice.fullCount()) {
if (*count > kMediaCountForSearch) {
_controller->setSearchEnabledByContent(true);
}
}
clearStaleLayouts();
resizeToWidth(width());
restoreScrollState();
mouseActionUpdate();
}
void ListWidget::markLayoutsStale() {
for (auto &layout : _layouts) {
layout.second.stale = true;
}
}
bool ListWidget::preventAutoHide() const {
return (_contextMenu != nullptr) || (_actionBoxWeak != nullptr);
}
void ListWidget::saveState(not_null<Memento*> memento) {
if (_universalAroundId != kDefaultAroundId) {
auto state = countScrollState();
if (state.item) {
memento->setAroundId(computeFullId(_universalAroundId));
memento->setIdsLimit(_idsLimit);
memento->setScrollTopItem(computeFullId(state.item));
memento->setScrollTopShift(state.shift);
}
}
}
void ListWidget::restoreState(not_null<Memento*> memento) {
if (auto limit = memento->idsLimit()) {
auto wasAroundId = memento->aroundId();
if (isPossiblyMyId(wasAroundId)) {
_idsLimit = limit;
_universalAroundId = GetUniversalId(wasAroundId);
_scrollTopState.item = GetUniversalId(memento->scrollTopItem());
_scrollTopState.shift = memento->scrollTopShift();
refreshViewer();
}
}
}
int ListWidget::resizeGetHeight(int newWidth) {
if (newWidth > 0) {
for (auto &section : _sections) {
section.resizeToWidth(newWidth);
}
}
return recountHeight();
}
auto ListWidget::findItemByPoint(QPoint point) const -> FoundItem {
Expects(!_sections.empty());
auto sectionIt = findSectionAfterTop(point.y());
if (sectionIt == _sections.end()) {
--sectionIt;
}
auto shift = QPoint(0, sectionIt->top());
return foundItemInSection(
sectionIt->findItemByPoint(point - shift),
*sectionIt);
}
auto ListWidget::findItemById(
UniversalMsgId universalId) -> std::optional<FoundItem> {
auto sectionIt = findSectionByItem(universalId);
if (sectionIt != _sections.end()) {
auto item = sectionIt->findItemNearId(universalId);
if (item.exact) {
return foundItemInSection(item, *sectionIt);
}
}
return std::nullopt;
}
auto ListWidget::findItemDetails(not_null<BaseLayout*> item)
-> FoundItem {
const auto sectionIt = findSectionByItem(GetUniversalId(item));
Assert(sectionIt != _sections.end());
return foundItemInSection(sectionIt->findItemDetails(item), *sectionIt);
}
auto ListWidget::foundItemInSection(
const FoundItem &item,
const Section &section) const
-> FoundItem {
return {
item.layout,
item.geometry.translated(0, section.top()),
item.exact };
}
void ListWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
_visibleTop = visibleTop;
_visibleBottom = visibleBottom;
checkMoveToOtherViewer();
clearHeavyItems();
if (_dateBadge->goodType) {
updateDateBadgeFor(_visibleTop);
if (!_visibleTop) {
if (_dateBadge->shown) {
scrollDateHide();
} else {
update(_dateBadge->rect);
}
} else {
_dateBadge->check.call();
}
}
}
void ListWidget::updateDateBadgeFor(int top) {
if (_sections.empty()) {
return;
}
const auto layout = findItemByPoint({ st::infoMediaSkip, top }).layout;
const auto rectHeight = st::msgServiceMargin.top()
+ st::msgServicePadding.top()
+ st::msgServiceFont->height
+ st::msgServicePadding.bottom();
_dateBadge->text = ItemDateText(layout->getItem(), false);
_dateBadge->textWidth = st::msgServiceFont->width(_dateBadge->text);
_dateBadge->rect = QRect(0, top, width(), rectHeight);
}
void ListWidget::scrollDateCheck() {
if (!_dateBadge->shown) {
toggleScrollDateShown();
}
_dateBadge->hideTimer.callOnce(st::infoScrollDateHideTimeout);
}
void ListWidget::scrollDateHide() {
if (_dateBadge->shown) {
toggleScrollDateShown();
}
}
void ListWidget::toggleScrollDateShown() {
_dateBadge->shown = !_dateBadge->shown;
_dateBadge->opacity.start(
[=] { update(_dateBadge->rect); },
_dateBadge->shown ? 0. : 1.,
_dateBadge->shown ? 1. : 0.,
st::infoDateFadeDuration);
}
void ListWidget::checkMoveToOtherViewer() {
auto visibleHeight = (_visibleBottom - _visibleTop);
if (width() <= 0
|| visibleHeight <= 0
|| _sections.empty()
|| _scrollTopState.item) {
return;
}
auto topItem = findItemByPoint({ st::infoMediaSkip, _visibleTop });
auto bottomItem = findItemByPoint({ st::infoMediaSkip, _visibleBottom });
auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;
auto minItemHeight = Section::MinItemHeight(_type, width());
auto preloadedCount = preloadedHeight / minItemHeight;
auto preloadIdsLimitMin = (preloadedCount / 2) + 1;
auto preloadIdsLimit = preloadIdsLimitMin
+ (visibleHeight / minItemHeight);
auto preloadBefore = kPreloadIfLessThanScreens * visibleHeight;
auto after = _slice.skippedAfter();
auto preloadTop = (_visibleTop < preloadBefore);
auto topLoaded = after && (*after == 0);
auto before = _slice.skippedBefore();
auto preloadBottom = (height() - _visibleBottom < preloadBefore);
auto bottomLoaded = before && (*before == 0);
auto minScreenDelta = kPreloadedScreensCount
- kPreloadIfLessThanScreens;
auto minUniversalIdDelta = (minScreenDelta * visibleHeight)
/ minItemHeight;
auto preloadAroundItem = [&](const FoundItem &item) {
auto preloadRequired = false;
auto universalId = GetUniversalId(item.layout);
if (!preloadRequired) {
preloadRequired = (_idsLimit < preloadIdsLimitMin);
}
if (!preloadRequired) {
auto delta = _slice.distance(
sliceKey(_universalAroundId),
sliceKey(universalId));
Assert(delta != std::nullopt);
preloadRequired = (qAbs(*delta) >= minUniversalIdDelta);
}
if (preloadRequired) {
_idsLimit = preloadIdsLimit;
_universalAroundId = universalId;
refreshViewer();
}
};
if (preloadTop && !topLoaded) {
preloadAroundItem(topItem);
} else if (preloadBottom && !bottomLoaded) {
preloadAroundItem(bottomItem);
}
}
void ListWidget::clearHeavyItems() {
const auto visibleHeight = _visibleBottom - _visibleTop;
if (!visibleHeight) {
return;
}
_heavyLayoutsInvalidated = false;
const auto above = _visibleTop - visibleHeight;
const auto below = _visibleBottom + visibleHeight;
for (auto i = _heavyLayouts.begin(); i != _heavyLayouts.end();) {
const auto item = const_cast<BaseLayout*>(i->get());
const auto rect = findItemDetails(item).geometry;
if (rect.top() + rect.height() <= above || rect.top() >= below) {
i = _heavyLayouts.erase(i);
item->clearHeavyPart();
if (_heavyLayoutsInvalidated) {
break;
}
} else {
++i;
}
}
if (_heavyLayoutsInvalidated) {
clearHeavyItems();
}
}
auto ListWidget::countScrollState() const -> ScrollTopState {
if (_sections.empty()) {
return { 0, 0 };
}
auto topItem = findItemByPoint({ st::infoMediaSkip, _visibleTop });
return {
GetUniversalId(topItem.layout),
_visibleTop - topItem.geometry.y()
};
}
void ListWidget::saveScrollState() {
if (!_scrollTopState.item) {
_scrollTopState = countScrollState();
}
}
void ListWidget::restoreScrollState() {
if (_sections.empty() || !_scrollTopState.item) {
return;
}
auto sectionIt = findSectionByItem(_scrollTopState.item);
if (sectionIt == _sections.end()) {
--sectionIt;
}
auto item = foundItemInSection(
sectionIt->findItemNearId(_scrollTopState.item),
*sectionIt);
auto newVisibleTop = item.geometry.y() + _scrollTopState.shift;
if (_visibleTop != newVisibleTop) {
_scrollToRequests.fire_copy(newVisibleTop);
}
_scrollTopState = ScrollTopState();
}
QMargins ListWidget::padding() const {
return st::infoMediaMargin;
}
void ListWidget::paintEvent(QPaintEvent *e) {
Painter p(this);
auto outerWidth = width();
auto clip = e->rect();
auto ms = crl::now();
auto fromSectionIt = findSectionAfterTop(clip.y());
auto tillSectionIt = findSectionAfterBottom(
fromSectionIt,
clip.y() + clip.height());
auto context = Context {
Overview::Layout::PaintContext(ms, hasSelectedItems()),
&_selected,
&_dragSelected,
_dragSelectAction
};
for (auto it = fromSectionIt; it != tillSectionIt; ++it) {
auto top = it->top();
p.translate(0, top);
it->paint(p, context, clip.translated(0, -top), outerWidth);
p.translate(0, -top);
}
if (fromSectionIt != _sections.end()) {
fromSectionIt->paintFloatingHeader(p, _visibleTop, outerWidth);
}
if (_dateBadge->goodType && clip.intersects(_dateBadge->rect)) {
const auto scrollDateOpacity =
_dateBadge->opacity.value(_dateBadge->shown ? 1. : 0.);
if (scrollDateOpacity > 0.) {
p.setOpacity(scrollDateOpacity);
if (_dateBadge->corners.p[0].isNull()) {
_dateBadge->corners = Ui::PrepareCornerPixmaps(
Ui::HistoryServiceMsgRadius(),
st::roundedBg,
nullptr);
}
HistoryView::ServiceMessagePainter::PaintDate(
p,
st::roundedBg,
_dateBadge->corners,
st::roundedFg,
_dateBadge->text,
_dateBadge->textWidth,
_visibleTop,
outerWidth,
false);
}
}
}
void ListWidget::mousePressEvent(QMouseEvent *e) {
if (_contextMenu) {
e->accept();
return; // ignore mouse press, that was hiding context menu
}
mouseActionStart(e->globalPos(), e->button());
}
void ListWidget::mouseMoveEvent(QMouseEvent *e) {
auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));
if (!buttonsPressed && _mouseAction != MouseAction::None) {
mouseReleaseEvent(e);
}
mouseActionUpdate(e->globalPos());
}
void ListWidget::mouseReleaseEvent(QMouseEvent *e) {
mouseActionFinish(e->globalPos(), e->button());
if (!rect().contains(e->pos())) {
leaveEvent(e);
}
}
void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) {
mouseActionStart(e->globalPos(), e->button());
trySwitchToWordSelection();
}
void ListWidget::showContextMenu(
QContextMenuEvent *e,
ContextMenuSource source) {
if (_contextMenu) {
_contextMenu = nullptr;
repaintItem(_contextUniversalId);
}
if (e->reason() == QContextMenuEvent::Mouse) {
mouseActionUpdate(e->globalPos());
}
auto item = session().data().message(computeFullId(_overState.itemId));
if (!item || !_overState.inside) {
return;
}
auto universalId = _contextUniversalId = _overState.itemId;
enum class SelectionState {
NoSelectedItems,
NotOverSelectedItems,
OverSelectedItems,
NotOverSelectedText,
OverSelectedText,
};
auto overSelected = SelectionState::NoSelectedItems;
if (source == ContextMenuSource::Touch) {
if (hasSelectedItems()) {
overSelected = SelectionState::OverSelectedItems;
} else if (hasSelectedText()) {
overSelected = SelectionState::OverSelectedItems;
}
} else if (hasSelectedText()) {
// #TODO text selection
} else if (hasSelectedItems()) {
auto it = _selected.find(_overState.itemId);
if (isSelectedItem(it) && _overState.inside) {
overSelected = SelectionState::OverSelectedItems;
} else {
overSelected = SelectionState::NotOverSelectedItems;
}
}
auto canDeleteAll = [&] {
return ranges::none_of(_selected, [](auto &&item) {
return !item.second.canDelete;
});
};
auto canForwardAll = [&] {
return ranges::none_of(_selected, [](auto &&item) {
return !item.second.canForward;
});
};
auto link = ClickHandler::getActive();
const auto itemFullId = item->fullId();
const auto owner = &session().data();
_contextMenu = base::make_unique_q<Ui::PopupMenu>(this);
_contextMenu->addAction(
tr::lng_context_to_msg(tr::now),
[=] {
if (const auto item = owner->message(itemFullId)) {
_controller->parentController()->showPeerHistoryAtItem(item);
}
});
auto photoLink = dynamic_cast<PhotoClickHandler*>(link.get());
auto fileLink = dynamic_cast<DocumentClickHandler*>(link.get());
if (photoLink || fileLink) {
auto [isVideo, isVoice, isAudio] = [&] {
if (fileLink) {
auto document = fileLink->document();
return std::make_tuple(
document->isVideoFile(),
document->isVoiceMessage(),
document->isAudioFile()
);
}
return std::make_tuple(false, false, false);
}();
if (photoLink) {
} else {
if (auto document = fileLink->document()) {
if (document->loading()) {
_contextMenu->addAction(
tr::lng_context_cancel_download(tr::now),
[document] {
document->cancel();
});
} else {
auto filepath = document->filepath(true);
if (!filepath.isEmpty()) {
auto handler = App::LambdaDelayed(
st::defaultDropdownMenu.menu.ripple.hideDuration,
this,
[filepath] {
File::ShowInFolder(filepath);
});
_contextMenu->addAction(
(Platform::IsMac()
? tr::lng_context_show_in_finder(tr::now)
: tr::lng_context_show_in_folder(tr::now)),
std::move(handler));
}
auto handler = App::LambdaDelayed(
st::defaultDropdownMenu.menu.ripple.hideDuration,
this,
[=] {
DocumentSaveClickHandler::Save(
itemFullId,
document,
DocumentSaveClickHandler::Mode::ToNewFile);
});
if (_peer->allowsForwarding()) {
_contextMenu->addAction(
(isVideo
? tr::lng_context_save_video(tr::now)
: isVoice
? tr::lng_context_save_audio(tr::now)
: isAudio
? tr::lng_context_save_audio_file(tr::now)
: tr::lng_context_save_file(tr::now)),
std::move(handler));
}
}
}
}
} else if (link) {
const auto actionText = link->copyToClipboardContextItemText();
if (!actionText.isEmpty()) {
_contextMenu->addAction(
actionText,
[text = link->copyToClipboardText()] {
QGuiApplication::clipboard()->setText(text);
});
}
}
if (overSelected == SelectionState::OverSelectedItems) {
if (canForwardAll()) {
_contextMenu->addAction(
tr::lng_context_forward_selected(tr::now),
crl::guard(this, [this] {
forwardSelected();
}));
}
if (canDeleteAll()) {
_contextMenu->addAction(
tr::lng_context_delete_selected(tr::now),
crl::guard(this, [this] {
deleteSelected();
}));
}
_contextMenu->addAction(
tr::lng_context_clear_selection(tr::now),
crl::guard(this, [this] {
clearSelected();
}));
} else {
if (overSelected != SelectionState::NotOverSelectedItems) {
if (item->allowsForward()) {
_contextMenu->addAction(
tr::lng_context_forward_msg(tr::now),
crl::guard(this, [this, universalId] {
forwardItem(universalId);
}));
}
if (item->canDelete()) {
_contextMenu->addAction(Ui::DeleteMessageContextAction(
_contextMenu->menu(),
[=] { deleteItem(universalId); },
item->ttlDestroyAt(),
[=] { _contextMenu = nullptr; }));
}
}
if (!hasSelectRestriction()) {
_contextMenu->addAction(
tr::lng_context_select_msg(tr::now),
crl::guard(this, [this, universalId] {
if (hasSelectedText()) {
clearSelected();
} else if (_selected.size() == MaxSelectedItems) {
return;
} else if (_selected.empty()) {
update();
}
applyItemSelection(universalId, FullSelection);
}));
}
}
_contextMenu->setDestroyedCallback(crl::guard(
this,
[this, universalId] {
mouseActionUpdate(QCursor::pos());
repaintItem(universalId);
_checkForHide.fire({});
}));
_contextMenu->popup(e->globalPos());
e->accept();
}
void ListWidget::contextMenuEvent(QContextMenuEvent *e) {
showContextMenu(
e,
(e->reason() == QContextMenuEvent::Mouse)
? ContextMenuSource::Mouse
: ContextMenuSource::Other);
}
void ListWidget::forwardSelected() {
if (auto items = collectSelectedIds(); !items.empty()) {
forwardItems(std::move(items));
}
}
void ListWidget::forwardItem(UniversalMsgId universalId) {
if (const auto item = session().data().message(computeFullId(universalId))) {
forwardItems({ 1, item->fullId() });
}
}
void ListWidget::forwardItems(MessageIdsList &&items) {
auto callback = [weak = Ui::MakeWeak(this)] {
if (const auto strong = weak.data()) {
strong->clearSelected();
}
};
setActionBoxWeak(Window::ShowForwardMessagesBox(
_controller,
std::move(items),
std::move(callback)));
}
void ListWidget::deleteSelected() {
if (const auto box = deleteItems(collectSelectedIds())) {
box->setDeleteConfirmedCallback(crl::guard(this, [=]{
clearSelected();
}));
}
}
void ListWidget::deleteItem(UniversalMsgId universalId) {
if (const auto item = session().data().message(computeFullId(universalId))) {
deleteItems({ 1, item->fullId() });
}
}
DeleteMessagesBox *ListWidget::deleteItems(MessageIdsList &&items) {
if (!items.empty()) {
const auto box = Ui::show(
Box<DeleteMessagesBox>(
&_controller->session(),
std::move(items))).data();
setActionBoxWeak(box);
return box;
}
return nullptr;
}
bool ListWidget::hasSelectRestriction() const {
if (_peer->allowsForwarding()) {
return false;
} else if (const auto chat = _peer->asChat()) {
return !chat->canDeleteMessages();
} else if (const auto channel = _peer->asChannel()) {
return !channel->canDeleteMessages();
}
return true;
}
void ListWidget::setActionBoxWeak(QPointer<Ui::RpWidget> box) {
if ((_actionBoxWeak = box)) {
_actionBoxWeakLifetime = _actionBoxWeak->alive(
) | rpl::start_with_done([weak = Ui::MakeWeak(this)]{
if (weak) {
weak->_checkForHide.fire({});
}
});
}
}
void ListWidget::trySwitchToWordSelection() {
auto selectingSome = (_mouseAction == MouseAction::Selecting)
&& hasSelectedText();
auto willSelectSome = (_mouseAction == MouseAction::None)
&& !hasSelectedItems();
auto checkSwitchToWordSelection = _overLayout
&& (_mouseSelectType == TextSelectType::Letters)
&& (selectingSome || willSelectSome);
if (checkSwitchToWordSelection) {
switchToWordSelection();
}
}
void ListWidget::switchToWordSelection() {
Expects(_overLayout != nullptr);
StateRequest request;
request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
auto dragState = _overLayout->getState(_pressState.cursor, request);
if (dragState.cursor != CursorState::Text) {
return;
}
_mouseTextSymbol = dragState.symbol;
_mouseSelectType = TextSelectType::Words;
if (_mouseAction == MouseAction::None) {
_mouseAction = MouseAction::Selecting;
clearSelected();
auto selStatus = TextSelection {
dragState.symbol,
dragState.symbol
};
applyItemSelection(_overState.itemId, selStatus);
}
mouseActionUpdate();
_trippleClickPoint = _mousePosition;
_trippleClickStartTime = crl::now();
}
void ListWidget::applyItemSelection(
UniversalMsgId universalId,
TextSelection selection) {
if (changeItemSelection(
_selected,
universalId,
selection)) {
repaintItem(universalId);
pushSelectedItems();
}
}
void ListWidget::toggleItemSelection(UniversalMsgId universalId) {
auto it = _selected.find(universalId);
if (it == _selected.cend()) {
applyItemSelection(universalId, FullSelection);
} else {
removeItemSelection(it);
}
}
bool ListWidget::changeItemSelection(
SelectedMap &selected,
UniversalMsgId universalId,
TextSelection selection) const {
auto changeExisting = [&](auto it) {
if (it == selected.cend()) {
return false;
} else if (it->second.text != selection) {
it->second.text = selection;
return true;
}
return false;
};
if (selected.size() < MaxSelectedItems) {
auto [iterator, ok] = selected.try_emplace(
universalId,
selection);
if (ok) {
auto item = session().data().message(computeFullId(universalId));
if (!item) {
selected.erase(iterator);
return false;
}
iterator->second.canDelete = item->canDelete();
iterator->second.canForward = item->allowsForward();
return true;
}
return changeExisting(iterator);
}
return changeExisting(selected.find(universalId));
}
bool ListWidget::isItemUnderPressSelected() const {
return itemUnderPressSelection() != _selected.end();
}
auto ListWidget::itemUnderPressSelection() -> SelectedMap::iterator {
return (_pressState.itemId && _pressState.inside)
? _selected.find(_pressState.itemId)
: _selected.end();
}
auto ListWidget::itemUnderPressSelection() const
-> SelectedMap::const_iterator {
return (_pressState.itemId && _pressState.inside)
? _selected.find(_pressState.itemId)
: _selected.end();
}
bool ListWidget::requiredToStartDragging(
not_null<BaseLayout*> layout) const {
if (_mouseCursorState == CursorState::Date) {
return true;
}
// return dynamic_cast<Sticker*>(layout->getMedia());
return false;
}
bool ListWidget::isPressInSelectedText(TextState state) const {
if (state.cursor != CursorState::Text) {
return false;
}
if (!hasSelectedText()
|| !isItemUnderPressSelected()) {
return false;
}
auto pressedSelection = itemUnderPressSelection();
auto from = pressedSelection->second.text.from;
auto to = pressedSelection->second.text.to;
return (state.symbol >= from && state.symbol < to);
}
void ListWidget::clearSelected() {
if (_selected.empty()) {
return;
}
if (hasSelectedText()) {
repaintItem(_selected.begin()->first);
_selected.clear();
} else {
_selected.clear();
pushSelectedItems();
update();
}
}
void ListWidget::validateTrippleClickStartTime() {
if (_trippleClickStartTime) {
auto elapsed = (crl::now() - _trippleClickStartTime);
if (elapsed >= QApplication::doubleClickInterval()) {
_trippleClickStartTime = 0;
}
}
}
void ListWidget::enterEventHook(QEnterEvent *e) {
mouseActionUpdate(QCursor::pos());
return RpWidget::enterEventHook(e);
}
void ListWidget::leaveEventHook(QEvent *e) {
if (const auto item = _overLayout) {
if (_overState.inside) {
repaintItem(item);
_overState.inside = false;
}
}
ClickHandler::clearActive();
if (!ClickHandler::getPressed() && _cursor != style::cur_default) {
_cursor = style::cur_default;
setCursor(_cursor);
}
return RpWidget::leaveEventHook(e);
}
QPoint ListWidget::clampMousePosition(QPoint position) const {
return {
std::clamp(position.x(), 0, qMax(0, width() - 1)),
std::clamp(position.y(), _visibleTop, _visibleBottom - 1)
};
}
void ListWidget::mouseActionUpdate(const QPoint &globalPosition) {
if (_sections.empty() || _visibleBottom <= _visibleTop) {
return;
}
_mousePosition = globalPosition;
auto local = mapFromGlobal(_mousePosition);
auto point = clampMousePosition(local);
auto [layout, geometry, inside] = findItemByPoint(point);
auto state = MouseState{
GetUniversalId(layout),
geometry.size(),
point - geometry.topLeft(),
inside
};
if (_overLayout != layout) {
repaintItem(_overLayout);
_overLayout = layout;
repaintItem(geometry);
}
_overState = state;
TextState dragState;
ClickHandlerHost *lnkhost = nullptr;
auto inTextSelection = _overState.inside
&& (_overState.itemId == _pressState.itemId)
&& hasSelectedText();
if (_overLayout) {
auto cursorDeltaLength = [&] {
auto cursorDelta = (_overState.cursor - _pressState.cursor);
return cursorDelta.manhattanLength();
};
auto dragStartLength = [] {
return QApplication::startDragDistance();
};
if (_overState.itemId != _pressState.itemId
|| cursorDeltaLength() >= dragStartLength()) {
if (_mouseAction == MouseAction::PrepareDrag) {
_mouseAction = MouseAction::Dragging;
InvokeQueued(this, [this] { performDrag(); });
} else if (_mouseAction == MouseAction::PrepareSelect) {
_mouseAction = MouseAction::Selecting;
}
}
StateRequest request;
if (_mouseAction == MouseAction::Selecting) {
request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
} else {
inTextSelection = false;
}
dragState = _overLayout->getState(_overState.cursor, request);
lnkhost = _overLayout;
}
ClickHandler::setActive(dragState.link, lnkhost);
if (_mouseAction == MouseAction::None) {
_mouseCursorState = dragState.cursor;
auto cursor = computeMouseCursor();
if (_cursor != cursor) {
setCursor(_cursor = cursor);
}
} else if (_mouseAction == MouseAction::Selecting) {
if (inTextSelection) {
auto second = dragState.symbol;
if (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) {
++second;
}
auto selState = TextSelection {
qMin(second, _mouseTextSymbol),
qMax(second, _mouseTextSymbol)
};
if (_mouseSelectType != TextSelectType::Letters) {
selState = _overLayout->adjustSelection(selState, _mouseSelectType);
}
applyItemSelection(_overState.itemId, selState);
auto hasSelection = (selState == FullSelection)
|| (selState.from != selState.to);
if (!_wasSelectedText && hasSelection) {
_wasSelectedText = true;
setFocus();
}
clearDragSelection();
} else if (_pressState.itemId) {
updateDragSelection();
}
} else if (_mouseAction == MouseAction::Dragging) {
}
// #TODO scroll by drag
//if (_mouseAction == MouseAction::Selecting) {
// _widget->checkSelectingScroll(mousePos);
//} else {
// clearDragSelection();
// _widget->noSelectingScroll();
//}
}
style::cursor ListWidget::computeMouseCursor() const {
if (ClickHandler::getPressed() || ClickHandler::getActive()) {
return style::cur_pointer;
} else if (!hasSelectedItems()
&& (_mouseCursorState == CursorState::Text)) {
return style::cur_text;
}
return style::cur_default;
}
void ListWidget::updateDragSelection() {
auto fromState = _pressState;
auto tillState = _overState;
auto swapStates = IsAfter(fromState, tillState);
if (swapStates) {
std::swap(fromState, tillState);
}
if (!fromState.itemId || !tillState.itemId || hasSelectRestriction()) {
clearDragSelection();
return;
}
auto fromId = SkipSelectFromItem(fromState)
? (fromState.itemId - 1)
: fromState.itemId;
auto tillId = SkipSelectTillItem(tillState)
? tillState.itemId
: (tillState.itemId - 1);
for (auto i = _dragSelected.begin(); i != _dragSelected.end();) {
auto itemId = i->first;
if (itemId > fromId || itemId <= tillId) {
i = _dragSelected.erase(i);
} else {
++i;
}
}
for (auto &layoutItem : _layouts) {
auto &&universalId = layoutItem.first;
if (universalId <= fromId && universalId > tillId) {
changeItemSelection(
_dragSelected,
universalId,
FullSelection);
}
}
_dragSelectAction = [&] {
if (_dragSelected.empty()) {
return DragSelectAction::None;
}
auto &[firstDragItem, data] = swapStates
? _dragSelected.front()
: _dragSelected.back();
if (isSelectedItem(_selected.find(firstDragItem))) {
return DragSelectAction::Deselecting;
} else {
return DragSelectAction::Selecting;
}
}();
if (!_wasSelectedText
&& !_dragSelected.empty()
&& _dragSelectAction == DragSelectAction::Selecting) {
_wasSelectedText = true;
setFocus();
}
update();
}
void ListWidget::clearDragSelection() {
_dragSelectAction = DragSelectAction::None;
if (!_dragSelected.empty()) {
_dragSelected.clear();
update();
}
}
void ListWidget::mouseActionStart(
const QPoint &globalPosition,
Qt::MouseButton button) {
mouseActionUpdate(globalPosition);
if (button != Qt::LeftButton) {
return;
}
ClickHandler::pressed();
if (_pressState != _overState) {
if (_pressState.itemId != _overState.itemId) {
repaintItem(_pressState.itemId);
}
_pressState = _overState;
repaintItem(_overLayout);
}
auto pressLayout = _overLayout;
_mouseAction = MouseAction::None;
_pressWasInactive = Ui::WasInactivePress(
_controller->parentController()->widget());
if (_pressWasInactive) {
Ui::MarkInactivePress(
_controller->parentController()->widget(),
false);
}
if (ClickHandler::getPressed() && !hasSelected()) {
_mouseAction = MouseAction::PrepareDrag;
} else if (hasSelectedItems()) {
if (isItemUnderPressSelected() && ClickHandler::getPressed()) {
// In shared media overview drag only by click handlers.
_mouseAction = MouseAction::PrepareDrag; // start items drag
} else if (!_pressWasInactive) {
_mouseAction = MouseAction::PrepareSelect; // start items select
}
}
if (_mouseAction == MouseAction::None && pressLayout) {
validateTrippleClickStartTime();
TextState dragState;
auto startDistance = (globalPosition - _trippleClickPoint).manhattanLength();
auto validStartPoint = startDistance < QApplication::startDragDistance();
if (_trippleClickStartTime != 0 && validStartPoint) {
StateRequest request;
request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
dragState = pressLayout->getState(_pressState.cursor, request);
if (dragState.cursor == CursorState::Text) {
TextSelection selStatus = { dragState.symbol, dragState.symbol };
if (selStatus != FullSelection && !hasSelectedItems()) {
clearSelected();
applyItemSelection(_pressState.itemId, selStatus);
_mouseTextSymbol = dragState.symbol;
_mouseAction = MouseAction::Selecting;
_mouseSelectType = TextSelectType::Paragraphs;
mouseActionUpdate(_mousePosition);
_trippleClickStartTime = crl::now();
}
}
} else {
StateRequest request;
request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
dragState = pressLayout->getState(_pressState.cursor, request);
}
if (_mouseSelectType != TextSelectType::Paragraphs) {
if (_pressState.inside) {
_mouseTextSymbol = dragState.symbol;
if (isPressInSelectedText(dragState)) {
_mouseAction = MouseAction::PrepareDrag; // start text drag
} else if (!_pressWasInactive) {
if (requiredToStartDragging(pressLayout)) {
_mouseAction = MouseAction::PrepareDrag;
} else {
if (dragState.afterSymbol) ++_mouseTextSymbol;
TextSelection selStatus = { _mouseTextSymbol, _mouseTextSymbol };
if (selStatus != FullSelection && !hasSelectedItems()) {
clearSelected();
applyItemSelection(_pressState.itemId, selStatus);
_mouseAction = MouseAction::Selecting;
repaintItem(pressLayout);
} else if (!hasSelectRestriction()) {
_mouseAction = MouseAction::PrepareSelect;
}
}
}
} else if (!_pressWasInactive && !hasSelectRestriction()) {
_mouseAction = MouseAction::PrepareSelect; // start items select
}
}
}
if (!pressLayout) {
_mouseAction = MouseAction::None;
} else if (_mouseAction == MouseAction::None) {
mouseActionCancel();
}
}
void ListWidget::mouseActionCancel() {
_pressState = MouseState();
_mouseAction = MouseAction::None;
clearDragSelection();
_wasSelectedText = false;
// _widget->noSelectingScroll(); // #TODO scroll by drag
}
void ListWidget::performDrag() {
if (_mouseAction != MouseAction::Dragging) return;
auto uponSelected = false;
if (_pressState.itemId && _pressState.inside) {
if (hasSelectedItems()) {
uponSelected = isItemUnderPressSelected();
} else if (auto pressLayout = getExistingLayout(
_pressState.itemId)) {
StateRequest request;
request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
auto dragState = pressLayout->getState(
_pressState.cursor,
request);
uponSelected = isPressInSelectedText(dragState);
}
}
auto pressedHandler = ClickHandler::getPressed();
if (dynamic_cast<VoiceSeekClickHandler*>(pressedHandler.get())) {
return;
}
TextWithEntities sel;
//QList<QUrl> urls;
if (uponSelected) {
// sel = getSelectedText();
} else if (pressedHandler) {
sel = { pressedHandler->dragText(), EntitiesInText() };
//if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {
// urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o
//}
}
//if (auto mimeData = MimeDataFromText(sel)) {
// clearDragSelection();
// _widget->noSelectingScroll();
// if (!urls.isEmpty()) mimeData->setUrls(urls);
// if (uponSelected && !Adaptive::OneColumn()) {
// auto selectedState = getSelectionState();
// if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {
// session().data().setMimeForwardIds(collectSelectedIds());
// mimeData->setData(qsl("application/x-td-forward"), "1");
// }
// }
// _controller->parentController()->window()->launchDrag(std::move(mimeData));
// return;
//} else {
// auto forwardMimeType = QString();
// auto pressedMedia = static_cast<HistoryView::Media*>(nullptr);
// if (auto pressedItem = _pressState.layout) {
// pressedMedia = pressedItem->getMedia();
// if (_mouseCursorState == CursorState::Date || (pressedMedia && pressedMedia->dragItem())) {
// session().data().setMimeForwardIds(session().data().itemOrItsGroup(pressedItem));
// forwardMimeType = qsl("application/x-td-forward");
// }
// }
// if (auto pressedLnkItem = App::pressedLinkItem()) {
// if ((pressedMedia = pressedLnkItem->getMedia())) {
// if (forwardMimeType.isEmpty() && pressedMedia->dragItemByHandler(pressedHandler)) {
// session().data().setMimeForwardIds({ 1, pressedLnkItem->fullId() });
// forwardMimeType = qsl("application/x-td-forward");
// }
// }
// }
// if (!forwardMimeType.isEmpty()) {
// auto mimeData = std::make_unique<QMimeData>();
// mimeData->setData(forwardMimeType, "1");
// if (auto document = (pressedMedia ? pressedMedia->getDocument() : nullptr)) {
// auto filepath = document->filepath(true);
// if (!filepath.isEmpty()) {
// QList<QUrl> urls;
// urls.push_back(QUrl::fromLocalFile(filepath));
// mimeData->setUrls(urls);
// }
// }
// // This call enters event loop and can destroy any QObject.
// _controller->parentController()->window()->launchDrag(std::move(mimeData));
// return;
// }
//}
}
void ListWidget::mouseActionFinish(
const QPoint &globalPosition,
Qt::MouseButton button) {
mouseActionUpdate(globalPosition);
auto pressState = base::take(_pressState);
repaintItem(pressState.itemId);
auto simpleSelectionChange = pressState.itemId
&& pressState.inside
&& !_pressWasInactive
&& (button != Qt::RightButton)
&& (_mouseAction == MouseAction::PrepareDrag
|| _mouseAction == MouseAction::PrepareSelect);
auto needSelectionToggle = simpleSelectionChange
&& hasSelectedItems();
auto needSelectionClear = simpleSelectionChange
&& hasSelectedText();
auto activated = ClickHandler::unpressed();
if (_mouseAction == MouseAction::Dragging
|| _mouseAction == MouseAction::Selecting) {
activated = nullptr;
} else if (needSelectionToggle) {
activated = nullptr;
}
_wasSelectedText = false;
if (activated) {
mouseActionCancel();
const auto found = findItemById(pressState.itemId);
const auto fullId = found
? found->layout->getItem()->fullId()
: FullMsgId();
ActivateClickHandler(window(), activated, {
button,
QVariant::fromValue(ClickHandlerContext{
.itemId = fullId,
.sessionWindow = base::make_weak(
_controller->parentController().get()),
})
});
return;
}
if (needSelectionToggle) {
toggleItemSelection(pressState.itemId);
} else if (needSelectionClear) {
clearSelected();
} else if (_mouseAction == MouseAction::Selecting) {
if (!_dragSelected.empty()) {
applyDragSelection();
} else if (!_selected.empty() && !_pressWasInactive) {
auto selection = _selected.cbegin()->second;
if (selection.text != FullSelection
&& selection.text.from == selection.text.to) {
clearSelected();
//_controller->parentController()->window()->setInnerFocus(); // #TODO focus
}
}
}
_mouseAction = MouseAction::None;
_mouseSelectType = TextSelectType::Letters;
//_widget->noSelectingScroll(); // #TODO scroll by drag
//_widget->updateTopBarSelection();
//if (QGuiApplication::clipboard()->supportsSelection() && hasSelectedText()) { // #TODO linux clipboard
// TextUtilities::SetClipboardText(_selected.cbegin()->first->selectedText(_selected.cbegin()->second), QClipboard::Selection);
//}
}
void ListWidget::applyDragSelection() {
if (!hasSelectRestriction()) {
applyDragSelection(_selected);
}
clearDragSelection();
pushSelectedItems();
}
void ListWidget::applyDragSelection(SelectedMap &applyTo) const {
if (_dragSelectAction == DragSelectAction::Selecting) {
for (auto &[universalId,data] : _dragSelected) {
changeItemSelection(applyTo, universalId, FullSelection);
}
} else if (_dragSelectAction == DragSelectAction::Deselecting) {
for (auto &[universalId,data] : _dragSelected) {
applyTo.remove(universalId);
}
}
}
void ListWidget::refreshHeight() {
resize(width(), recountHeight());
}
int ListWidget::recountHeight() {
if (_sections.empty()) {
if (auto count = _slice.fullCount()) {
if (*count == 0) {
return 0;
}
}
}
auto cachedPadding = padding();
auto result = cachedPadding.top();
for (auto &section : _sections) {
section.setTop(result);
result += section.height();
}
return result + cachedPadding.bottom();
}
void ListWidget::mouseActionUpdate() {
mouseActionUpdate(_mousePosition);
}
void ListWidget::clearStaleLayouts() {
for (auto i = _layouts.begin(); i != _layouts.end();) {
if (i->second.stale) {
if (i->second.item.get() == _overLayout) {
_overLayout = nullptr;
}
_heavyLayouts.erase(i->second.item.get());
i = _layouts.erase(i);
} else {
++i;
}
}
}
auto ListWidget::findSectionByItem(
UniversalMsgId universalId) -> std::vector<Section>::iterator {
return ranges::lower_bound(
_sections,
universalId,
std::greater<>(),
[](const Section &section) { return section.minId(); });
}
auto ListWidget::findSectionAfterTop(
int top) -> std::vector<Section>::iterator {
return ranges::lower_bound(
_sections,
top,
std::less_equal<>(),
[](const Section &section) { return section.bottom(); });
}
auto ListWidget::findSectionAfterTop(
int top) const -> std::vector<Section>::const_iterator {
return ranges::lower_bound(
_sections,
top,
std::less_equal<>(),
[](const Section &section) { return section.bottom(); });
}
auto ListWidget::findSectionAfterBottom(
std::vector<Section>::const_iterator from,
int bottom) const -> std::vector<Section>::const_iterator {
return ranges::lower_bound(
from,
_sections.end(),
bottom,
std::less<>(),
[](const Section &section) { return section.top(); });
}
ListWidget::~ListWidget() {
if (_contextMenu) {
// We don't want it to be called after ListWidget is destroyed.
_contextMenu->setDestroyedCallback(nullptr);
}
}
} // namespace Media
} // namespace Info