/* 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/media/info_media_common.h" #include "info/media/info_media_provider.h" #include "info/media/info_media_list_section.h" #include "info/downloads/info_downloads_provider.h" #include "info/stories/info_stories_provider.h" #include "info/info_controller.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_stories.h" #include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_download_manager.h" #include "data/data_forum_topic.h" #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/history.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_service_message.h" #include "media/stories/media_stories_controller.h" // ...TogglePinnedToast. #include "media/stories/media_stories_share.h" // PrepareShareBox. #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "ui/widgets/popup_menu.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/delete_message_context_action.h" #include "ui/chat/chat_style.h" #include "ui/cached_round_corners.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "ui/inactive_press.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "main/main_account.h" #include "mainwidget.h" #include "mainwindow.h" #include "base/platform/base_platform_info.h" #include "base/weak_ptr.h" #include "base/call_delayed.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 "core/application.h" #include "ui/toast/toast.h" #include "styles/style_overview.h" #include "styles/style_info.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include "styles/style_chat.h" #include #include namespace Info { namespace Media { namespace { constexpr auto kMediaCountForSearch = 10; } // namespace struct ListWidget::DateBadge { DateBadge(Type type, Fn checkCallback, Fn 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; }; [[nodiscard]] std::unique_ptr MakeProvider( not_null controller) { if (controller->isDownloads()) { return std::make_unique(controller); } else if (controller->storiesPeer()) { return std::make_unique(controller); } return std::make_unique(controller); } bool ListWidget::isAfter( const MouseState &a, const MouseState &b) const { if (a.item != b.item) { return _provider->isAfter(a.item, b.item); } const auto xAfter = a.cursor.x() - b.cursor.x(); const 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::DateBadge::DateBadge( Type type, Fn checkCallback, Fn hideCallback) : check(std::move(checkCallback)) , hideTimer(std::move(hideCallback)) , goodType(type == Type::Photo || type == Type::Video || type == Type::PhotoVideo || type == Type::GIF) { } ListWidget::ListWidget( QWidget *parent, not_null controller) : RpWidget(parent) , _controller(controller) , _provider(MakeProvider(_controller)) , _dateBadge(std::make_unique( _provider->type(), [=] { scrollDateCheck(); }, [=] { scrollDateHide(); })) { start(); } Main::Session &ListWidget::session() const { return _controller->session(); } void ListWidget::start() { setMouseTracking(true); _controller->setSearchEnabledByContent(false); _provider->layoutRemoved( ) | rpl::start_with_next([=](not_null layout) { if (_overLayout == layout) { _overLayout = nullptr; } _heavyLayouts.remove(layout); }, lifetime()); _provider->refreshed( ) | rpl::start_with_next([=] { refreshRows(); }, lifetime()); if (_controller->isDownloads()) { _provider->refreshViewer(); _controller->searchQueryValue( ) | rpl::start_with_next([this](QString &&query) { _provider->setSearchQuery(std::move(query)); }, lifetime()); } else if (_controller->storiesPeer()) { trackSession(&session()); restart(); } else { trackSession(&session()); _controller->mediaSourceQueryValue( ) | rpl::start_with_next([this] { restart(); }, lifetime()); if (_provider->type() == Type::File) { // For downloads manager. session().data().itemVisibilityQueries( ) | rpl::filter([=]( const Data::Session::ItemVisibilityQuery &query) { return _provider->isPossiblyMyItem(query.item) && isVisible(); }) | rpl::start_with_next([=]( const Data::Session::ItemVisibilityQuery &query) { if (const auto found = findItemByItem(query.item)) { if (itemVisible(found->layout)) { *query.isVisible = true; } } }, lifetime()); } } setupSelectRestriction(); } void ListWidget::subscribeToSession( not_null session, rpl::lifetime &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); session->data().itemDataChanges( ) | rpl::start_with_next([=](not_null item) { if (const auto found = findItemByItem(item)) { found->layout->itemDataChanged(); } }, lifetime); } void ListWidget::setupSelectRestriction() { _provider->hasSelectRestrictionChanges( ) | rpl::filter([=] { return _provider->hasSelectRestriction() && hasSelectedItems(); }) | rpl::start_with_next([=] { clearSelected(); if (_mouseAction == MouseAction::PrepareSelect) { mouseActionCancel(); } }, lifetime()); } rpl::producer ListWidget::scrollToRequests() const { return _scrollToRequests.events(); } rpl::producer ListWidget::selectedListValue() const { return _selectedListStream.events_starting_with( collectSelectedItems()); } void ListWidget::selectionAction(SelectionAction action) { switch (action) { case SelectionAction::Clear: clearSelected(); return; case SelectionAction::Forward: forwardSelected(); return; case SelectionAction::Delete: deleteSelected(); return; case SelectionAction::ToggleStoryInProfile: toggleStoryInProfileSelected(); return; case SelectionAction::ToggleStoryPin: toggleStoryPinSelected(); return; } } QRect ListWidget::getCurrentSongGeometry() { const auto type = AudioMsgId::Type::Song; const auto current = ::Media::Player::instance()->current(type); if (const auto document = current.audio()) { const auto contextId = current.contextId(); if (const auto item = document->owner().message(contextId)) { if (const auto found = findItemByItem(item)) { return found->geometry; } } } return QRect(0, 0, width(), 0); } void ListWidget::restart() { mouseActionCancel(); _overLayout = nullptr; _sections.clear(); _heavyLayouts.clear(); _provider->restart(); } void ListWidget::itemRemoved(not_null item) { if (!_provider->isMyItem(item)) { return; } if (_contextItem == item) { _contextItem = nullptr; } auto needHeightRefresh = false; auto sectionIt = findSectionByItem(item); if (sectionIt != _sections.end()) { if (sectionIt->removeItem(item)) { if (sectionIt->empty()) { _sections.erase(sectionIt); } needHeightRefresh = true; } } if (isItemLayout(item, _overLayout)) { _overLayout = nullptr; } _dragSelected.remove(item); if (_pressState.item == item) { mouseActionCancel(); } if (_overState.item == item) { _mouseAction = MouseAction::None; _overState = {}; } if (const auto i = _selected.find(item); i != _selected.cend()) { removeItemSelection(i); } if (needHeightRefresh) { refreshHeight(); } mouseActionUpdate(_mousePosition); } auto ListWidget::collectSelectedItems() const -> SelectedItems { auto convert = [&]( not_null item, const SelectionData &selection) { auto result = SelectedItem(item->globalId()); result.canDelete = selection.canDelete; result.canForward = selection.canForward; result.canToggleStoryPin = selection.canToggleStoryPin; result.canUnpinStory = selection.canUnpinStory; return result; }; auto transformation = [&](const auto &item) { return convert(item.first, item.second); }; auto items = SelectedItems(_provider->type()); if (hasSelectedItems()) { items.list.reserve(_selected.size()); std::transform( _selected.begin(), _selected.end(), std::back_inserter(items.list), transformation); } if (_controller->storiesPeer() && items.list.size() > 1) { // Don't allow forwarding more than one story. for (auto &entry : items.list) { entry.canForward = false; } } return items; } MessageIdsList ListWidget::collectSelectedIds() const { return collectSelectedIds(collectSelectedItems()); } MessageIdsList ListWidget::collectSelectedIds( const SelectedItems &items) const { const auto session = &_controller->session(); return ranges::views::all( items.list ) | ranges::views::transform([](auto &&item) { return item.globalId; }) | ranges::views::filter([&](const GlobalMsgId &globalId) { return (globalId.sessionUniqueId == session->uniqueId()) && (session->data().message(globalId.itemId) != nullptr); }) | ranges::views::transform([](const GlobalMsgId &globalId) { return globalId.itemId; }) | 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 item) { if (isItemLayout(item, _overLayout)) { mouseActionUpdate(); } } void ListWidget::repaintItem(const HistoryItem *item) { if (const auto found = findItemByItem(item)) { repaintItem(found->geometry); } } void ListWidget::repaintItem(const BaseLayout *item) { if (item) { repaintItem(item->getItem()); } } void ListWidget::repaintItem(not_null item) { repaintItem(item->getItem()); } void ListWidget::repaintItem(QRect itemGeometry) { rtlupdate(itemGeometry); } bool ListWidget::isItemLayout( not_null item, BaseLayout *layout) const { return layout && (layout->getItem() == item); } void ListWidget::registerHeavyItem(not_null item) { if (!_heavyLayouts.contains(item)) { _heavyLayouts.emplace(item); _heavyLayoutsInvalidated = true; } } void ListWidget::unregisterHeavyItem(not_null item) { const auto i = _heavyLayouts.find(item); if (i != _heavyLayouts.end()) { _heavyLayouts.erase(i); _heavyLayoutsInvalidated = true; } } bool ListWidget::itemVisible(not_null item) { if (const auto &found = findItemByItem(item->getItem())) { const auto geometry = found->geometry; return (geometry.top() < _visibleBottom) && (geometry.top() + geometry.height() > _visibleTop); } return true; } QString ListWidget::tooltipText() const { if (const auto link = ClickHandler::getActive()) { return link->tooltip(); } return QString(); } QPoint ListWidget::tooltipPos() const { return _mousePosition; } bool ListWidget::tooltipWindowActive() const { return Ui::AppInFocus() && Ui::InFocusChain(window()); } void ListWidget::openPhoto(not_null photo, FullMsgId id) { using namespace Data; const auto tab = _controller->storiesTab(); const auto context = (tab == Stories::Tab::Archive) ? Data::StoriesContext{ Data::StoriesContextArchive() } : Data::StoriesContext{ Data::StoriesContextSaved() }; _controller->parentController()->openPhoto( photo, { id, topicRootId() }, _controller->storiesPeer() ? &context : nullptr); } void ListWidget::openDocument( not_null document, FullMsgId id, bool showInMediaView) { const auto tab = _controller->storiesTab(); const auto context = (tab == Stories::Tab::Archive) ? Data::StoriesContext{ Data::StoriesContextArchive() } : Data::StoriesContext{ Data::StoriesContextSaved() }; _controller->parentController()->openDocument( document, showInMediaView, { id, topicRootId() }, _controller->storiesPeer() ? &context : nullptr); } void ListWidget::trackSession(not_null session) { if (_trackedSessions.contains(session)) { return; } auto &lifetime = _trackedSessions.emplace(session).first->second; subscribeToSession(session, lifetime); session->account().sessionChanges( ) | rpl::take(1) | rpl::start_with_next([=] { _trackedSessions.remove(session); }, lifetime); } void ListWidget::refreshRows() { saveScrollState(); _sections.clear(); _sections = _provider->fillSections(this); if (_controller->isDownloads() && !_sections.empty()) { for (const auto &item : _sections.back().items()) { trackSession(&item->getItem()->history()->session()); } } if (const auto count = _provider->fullCount()) { if (*count > kMediaCountForSearch) { _controller->setSearchEnabledByContent(true); } } resizeToWidth(width()); restoreScrollState(); mouseActionUpdate(); update(); } bool ListWidget::preventAutoHide() const { return (_contextMenu != nullptr) || (_actionBoxWeak != nullptr); } void ListWidget::saveState(not_null memento) { _provider->saveState(memento, countScrollState()); _trackedSessions.clear(); } void ListWidget::restoreState(not_null memento) { _provider->restoreState(memento, [&](ListScrollTopState state) { _scrollTopState = state; }); } int ListWidget::resizeGetHeight(int newWidth) { if (newWidth > 0) { for (auto §ion : _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::findItemByItem(const HistoryItem *item) -> std::optional { if (!item || !_provider->isPossiblyMyItem(item)) { return std::nullopt; } auto sectionIt = findSectionByItem(item); if (sectionIt != _sections.end()) { if (const auto found = sectionIt->findItemByItem(item)) { return foundItemInSection(*found, *sectionIt); } } return std::nullopt; } auto ListWidget::findItemDetails(not_null item) -> FoundItem { const auto sectionIt = findSectionByItem(item->getItem()); Assert(sectionIt != _sections.end()); return foundItemInSection(sectionIt->findItemDetails(item), *sectionIt); } auto ListWidget::foundItemInSection( const FoundItem &item, const Section §ion) 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(); } } session().data().itemVisibilitiesUpdated(); } 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 preloadBefore = kPreloadIfLessThanScreens * visibleHeight; auto preloadTop = (_visibleTop < preloadBefore); auto preloadBottom = (height() - _visibleBottom < preloadBefore); _provider->checkPreload( { width(), visibleHeight }, topItem.layout, bottomItem.layout, preloadTop, preloadBottom); } 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(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(); } } ListScrollTopState ListWidget::countScrollState() const { if (_sections.empty() || _visibleTop <= 0) { return {}; } const auto topItem = findItemByPoint({ st::infoMediaSkip, _visibleTop }); const auto item = topItem.layout->getItem(); return { .position = _provider->scrollTopStatePosition(item), .item = item, .shift = _visibleTop - topItem.geometry.y(), }; } void ListWidget::saveScrollState() { if (!_scrollTopState.item) { _scrollTopState = countScrollState(); } } void ListWidget::restoreScrollState() { if (_sections.empty() || !_scrollTopState.position) { return; } _scrollTopState.item = _provider->scrollTopStateItem(_scrollTopState); if (!_scrollTopState.item) { return; } auto sectionIt = findSectionByItem(_scrollTopState.item); if (sectionIt == _sections.end()) { --sectionIt; } const auto found = sectionIt->findItemByItem(_scrollTopState.item); if (!found) { return; } auto item = foundItemInSection(*found, *sectionIt); auto newVisibleTop = item.geometry.y() + _scrollTopState.shift; if (_visibleTop != newVisibleTop) { _scrollToRequests.fire_copy(newVisibleTop); } _scrollTopState = ListScrollTopState(); } MsgId ListWidget::topicRootId() const { const auto topic = _controller->key().topic(); return topic ? topic->rootId() : MsgId(0); } 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()); const auto window = _controller->parentController(); const auto paused = window->isGifPausedAtLeastFor( Window::GifPauseReason::Layer); auto context = ListContext{ Overview::Layout::PaintContext(ms, hasSelectedItems(), paused), &_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); } 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(_contextItem); } if (e->reason() == QContextMenuEvent::Mouse) { mouseActionUpdate(e->globalPos()); } const auto item = _overState.item; if (!item || !_overState.inside) { return; } _contextItem = item; const auto globalId = item->globalId(); 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.item); if (isSelectedItem(it) && _overState.inside) { overSelected = SelectionState::OverSelectedItems; } else { overSelected = SelectionState::NotOverSelectedItems; } } const auto canDeleteAll = [&] { return ranges::none_of(_selected, [](auto &&item) { return !item.second.canDelete; }); }; const auto canForwardAll = [&] { return ranges::none_of(_selected, [](auto &&item) { return !item.second.canForward; }) && (!_controller->key().storiesPeer() || _selected.size() == 1); }; const auto canToggleStoryPinAll = [&] { return ranges::none_of(_selected, [](auto &&item) { return !item.second.canToggleStoryPin; }); }; const auto canUnpinStoryAll = [&] { return ranges::any_of(_selected, [](auto &&item) { return item.second.canUnpinStory; }); }; auto link = ClickHandler::getActive(); _contextMenu = base::make_unique_q( this, st::popupMenuWithIcons); if (item->isHistoryEntry()) { _contextMenu->addAction( tr::lng_context_to_msg(tr::now), [=] { if (const auto item = MessageByGlobalId(globalId)) { JumpToMessageClickHandler(item)->onClick({}); } }, &st::menuIconShowInChat); } const auto lnkPhoto = link ? reinterpret_cast( link->property(kPhotoLinkMediaProperty).toULongLong()) : nullptr; const auto lnkDocument = link ? reinterpret_cast( link->property(kDocumentLinkMediaProperty).toULongLong()) : nullptr; if (lnkPhoto || lnkDocument) { auto [isVideo, isVoice, isAudio] = [&] { if (lnkDocument) { return std::make_tuple( lnkDocument->isVideoFile(), lnkDocument->isVoiceMessage(), lnkDocument->isAudioFile() ); } return std::make_tuple(false, false, false); }(); if (lnkPhoto) { } else { if (lnkDocument->loading()) { _contextMenu->addAction( tr::lng_context_cancel_download(tr::now), [lnkDocument] { lnkDocument->cancel(); }, &st::menuIconCancel); } else { const auto filepath = _provider->showInFolderPath( item, lnkDocument); if (!filepath.isEmpty()) { auto handler = base::fn_delayed( 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), &st::menuIconShowInFolder); } auto handler = base::fn_delayed( st::defaultDropdownMenu.menu.ripple.hideDuration, this, [=] { DocumentSaveClickHandler::SaveAndTrack( globalId.itemId, lnkDocument, DocumentSaveClickHandler::Mode::ToNewFile); }); if (_provider->allowSaveFileAs(item, lnkDocument)) { _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), &st::menuIconDownload); } } } } else if (link) { const auto actionText = link->copyToClipboardContextItemText(); if (!actionText.isEmpty()) { _contextMenu->addAction( actionText, [text = link->copyToClipboardText()] { QGuiApplication::clipboard()->setText(text); }, &st::menuIconCopy); } } if (overSelected == SelectionState::OverSelectedItems) { if (canToggleStoryPinAll()) { const auto tab = _controller->key().storiesTab(); const auto toProfile = (tab == Stories::Tab::Archive); _contextMenu->addAction( (toProfile ? tr::lng_mediaview_save_to_profile : tr::lng_archived_add)(tr::now), crl::guard(this, [this] { toggleStoryInProfileSelected(); }), (toProfile ? &st::menuIconStoriesSave : &st::menuIconStoriesArchive)); if (!toProfile) { const auto unpin = canUnpinStoryAll(); _contextMenu->addAction( (unpin ? tr::lng_context_unpin_from_top : tr::lng_context_pin_to_top)(tr::now), crl::guard( this, [this] { toggleStoryPinSelected(); }), (unpin ? &st::menuIconUnpin : &st::menuIconPin)); } } if (canForwardAll()) { _contextMenu->addAction( tr::lng_context_forward_selected(tr::now), crl::guard(this, [this] { forwardSelected(); }), &st::menuIconForward); } if (canDeleteAll()) { _contextMenu->addAction( (_controller->isDownloads() ? tr::lng_context_delete_from_disk(tr::now) : tr::lng_context_delete_selected(tr::now)), crl::guard(this, [this] { deleteSelected(); }), &st::menuIconDelete); } _contextMenu->addAction( tr::lng_context_clear_selection(tr::now), crl::guard(this, [this] { clearSelected(); }), &st::menuIconSelect); } else { if (overSelected != SelectionState::NotOverSelectedItems) { const auto selectionData = _provider->computeSelectionData( item, FullSelection); if (selectionData.canToggleStoryPin) { const auto tab = _controller->key().storiesTab(); const auto toProfile = (tab == Stories::Tab::Archive); _contextMenu->addAction( (toProfile ? tr::lng_mediaview_save_to_profile : tr::lng_mediaview_archive_story)(tr::now), crl::guard(this, [=] { toggleStoryInProfile({ 1, globalId.itemId }); }), (toProfile ? &st::menuIconStoriesSave : &st::menuIconStoriesArchive)); if (!toProfile) { const auto unpin = selectionData.canUnpinStory; _contextMenu->addAction( (unpin ? tr::lng_context_unpin_from_top : tr::lng_context_pin_to_top)(tr::now), crl::guard(this, [=] { toggleStoryPin( { 1, globalId.itemId }, !unpin); }), (unpin ? &st::menuIconUnpin : &st::menuIconPin)); } } if (selectionData.canForward) { _contextMenu->addAction( tr::lng_context_forward_msg(tr::now), crl::guard(this, [=] { forwardItem(globalId); }), &st::menuIconForward); } if (selectionData.canDelete) { if (_controller->isDownloads()) { _contextMenu->addAction( tr::lng_context_delete_from_disk(tr::now), crl::guard(this, [=] { deleteItem(globalId); }), &st::menuIconDelete); } else { _contextMenu->addAction(Ui::DeleteMessageContextAction( _contextMenu->menu(), crl::guard(this, [=] { deleteItem(globalId); }), item->ttlDestroyAt(), [=] { _contextMenu = nullptr; })); } } } if (const auto peer = _controller->key().storiesPeer()) { if (!peer->isSelf() && IsStoryMsgId(globalId.itemId.msg)) { const auto storyId = FullStoryId{ globalId.itemId.peer, StoryIdFromMsgId(globalId.itemId.msg), }; _contextMenu->addAction( tr::lng_profile_report(tr::now), [=] { ::Media::Stories::ReportRequested( _controller->uiShow(), storyId); }, &st::menuIconReport); } } if (!_provider->hasSelectRestriction()) { _contextMenu->addAction( tr::lng_context_select_msg(tr::now), crl::guard(this, [=] { if (hasSelectedText()) { clearSelected(); } else if (_selected.size() == MaxSelectedItems) { return; } else if (_selected.empty()) { update(); } applyItemSelection( MessageByGlobalId(globalId), FullSelection); }), &st::menuIconSelect); } } _contextMenu->setDestroyedCallback(crl::guard( this, [=] { mouseActionUpdate(QCursor::pos()); repaintItem(MessageByGlobalId(globalId)); _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(GlobalMsgId globalId) { const auto session = &_controller->session(); if (globalId.sessionUniqueId == session->uniqueId()) { if (const auto item = session->data().message(globalId.itemId)) { forwardItems({ 1, item->fullId() }); } } } void ListWidget::forwardItems(MessageIdsList &&items) { if (_controller->storiesPeer()) { if (items.size() == 1 && IsStoryMsgId(items.front().msg)) { const auto id = items.front(); _controller->parentController()->show( ::Media::Stories::PrepareShareBox( _controller->parentController()->uiShow(), { id.peer, StoryIdFromMsgId(id.msg) })); } } else { 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() { deleteItems(collectSelectedItems(), crl::guard(this, [=]{ clearSelected(); })); } void ListWidget::toggleStoryInProfileSelected() { toggleStoryInProfile(collectSelectedIds(), crl::guard(this, [=] { clearSelected(); })); } void ListWidget::toggleStoryPinSelected() { const auto items = collectSelectedItems(); const auto pin = ranges::none_of( items.list, &SelectedItem::canUnpinStory); toggleStoryPin(collectSelectedIds(items), pin, crl::guard(this, [=] { clearSelected(); })); } void ListWidget::toggleStoryInProfile( MessageIdsList &&items, Fn confirmed) { auto list = std::vector(); for (const auto &id : items) { if (IsStoryMsgId(id.msg)) { list.push_back({ id.peer, StoryIdFromMsgId(id.msg) }); } } if (list.empty()) { return; } const auto channel = peerIsChannel(list.front().peer); const auto count = int(list.size()); const auto toProfile = (_controller->storiesTab() == Stories::Tab::Archive); const auto controller = _controller; const auto sure = [=](Fn close) { using namespace ::Media::Stories; controller->session().data().stories().toggleInProfileList( list, toProfile); controller->showToast( PrepareToggleInProfileToast(channel, count, toProfile)); close(); if (confirmed) { confirmed(); } }; const auto onePhrase = toProfile ? (channel ? tr::lng_stories_channel_save_sure : tr::lng_stories_save_sure) : (channel ? tr::lng_stories_channel_archive_sure : tr::lng_stories_archive_sure); const auto manyPhrase = toProfile ? (channel ? tr::lng_stories_channel_save_sure_many : tr::lng_stories_save_sure_many) : (channel ? tr::lng_stories_channel_archive_sure_many : tr::lng_stories_archive_sure_many); _controller->parentController()->show(Ui::MakeConfirmBox({ .text = (count == 1 ? onePhrase() : manyPhrase(lt_count, rpl::single(count) | tr::to_count())), .confirmed = sure, .confirmText = tr::lng_box_ok(), })); } void ListWidget::toggleStoryPin( MessageIdsList &&items, bool pin, Fn confirmed) { auto list = std::vector(); for (const auto &id : items) { if (IsStoryMsgId(id.msg)) { list.push_back({ id.peer, StoryIdFromMsgId(id.msg) }); } } if (list.empty()) { return; } const auto channel = peerIsChannel(list.front().peer); const auto count = int(list.size()); const auto controller = _controller; const auto stories = &controller->session().data().stories(); if (stories->canTogglePinnedList(list, pin)) { using namespace ::Media::Stories; stories->togglePinnedList(list, pin); controller->showToast(PrepareTogglePinToast(channel, count, pin)); if (confirmed) { confirmed(); } } else { const auto limit = stories->maxPinnedCount(); controller->showToast( tr::lng_mediaview_pin_limit(tr::now, lt_count, limit)); } } void ListWidget::deleteItem(GlobalMsgId globalId) { if (const auto item = MessageByGlobalId(globalId)) { auto items = SelectedItems(_provider->type()); items.list.push_back(SelectedItem(item->globalId())); const auto selectionData = _provider->computeSelectionData( item, FullSelection); items.list.back().canDelete = selectionData.canDelete; deleteItems(std::move(items)); } } void ListWidget::deleteItems(SelectedItems &&items, Fn confirmed) { const auto window = _controller->parentController(); if (items.list.empty()) { return; } else if (_controller->isDownloads()) { const auto count = items.list.size(); const auto allInCloud = ranges::all_of(items.list, []( const SelectedItem &entry) { const auto item = MessageByGlobalId(entry.globalId); return item && item->isHistoryEntry(); }); const auto phrase = (count == 1) ? tr::lng_downloads_delete_sure_one(tr::now) : tr::lng_downloads_delete_sure(tr::now, lt_count, count); const auto added = !allInCloud ? QString() : (count == 1 ? tr::lng_downloads_delete_in_cloud_one(tr::now) : tr::lng_downloads_delete_in_cloud(tr::now)); const auto deleteSure = [=] { Ui::PostponeCall(this, [=] { if (const auto box = _actionBoxWeak.data()) { box->closeBox(); } }); const auto ids = ranges::views::all( items.list ) | ranges::views::transform([](const SelectedItem &item) { return item.globalId; }) | ranges::to_vector; Core::App().downloadManager().deleteFiles(ids); if (confirmed) { confirmed(); } }; setActionBoxWeak(window->show(Ui::MakeConfirmBox({ .text = phrase + (added.isEmpty() ? QString() : "\n\n" + added), .confirmed = deleteSure, .confirmText = tr::lng_box_delete(tr::now), .confirmStyle = &st::attentionBoxButton, }))); } else if (_controller->storiesPeer()) { auto list = std::vector(); for (const auto &item : items.list) { const auto id = item.globalId.itemId; if (IsStoryMsgId(id.msg)) { list.push_back({ id.peer, StoryIdFromMsgId(id.msg) }); } } const auto session = &_controller->session(); const auto sure = [=](Fn close) { session->data().stories().deleteList(list); close(); if (confirmed) { confirmed(); } }; const auto count = int(list.size()); window->show(Ui::MakeConfirmBox({ .text = (count == 1 ? tr::lng_stories_delete_one_sure() : tr::lng_stories_delete_sure( lt_count, rpl::single(count) | tr::to_count())), .confirmed = sure, .confirmText = tr::lng_selected_delete(), .confirmStyle = &st::attentionBoxButton, })); } else if (auto list = collectSelectedIds(items); !list.empty()) { auto box = Box( &_controller->session(), std::move(list)); const auto weak = box.data(); window->show(std::move(box)); setActionBoxWeak(weak); if (confirmed) { weak->setDeleteConfirmedCallback(std::move(confirmed)); } } } void ListWidget::setActionBoxWeak(QPointer 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.item, selStatus); } mouseActionUpdate(); _trippleClickPoint = _mousePosition; _trippleClickStartTime = crl::now(); } void ListWidget::applyItemSelection( HistoryItem *item, TextSelection selection) { if (item && ChangeItemSelection( _selected, item, _provider->computeSelectionData(item, selection))) { repaintItem(item); pushSelectedItems(); } } void ListWidget::toggleItemSelection(not_null item) { auto it = _selected.find(item); if (it == _selected.cend()) { applyItemSelection(item, FullSelection); } else { removeItemSelection(it); } } bool ListWidget::isItemUnderPressSelected() const { return itemUnderPressSelection() != _selected.end(); } auto ListWidget::itemUnderPressSelection() -> SelectedMap::iterator { return (_pressState.item && _pressState.inside) ? _selected.find(_pressState.item) : _selected.end(); } auto ListWidget::itemUnderPressSelection() const -> SelectedMap::const_iterator { return (_pressState.item && _pressState.inside) ? _selected.find(_pressState.item) : _selected.end(); } bool ListWidget::requiredToStartDragging( not_null layout) const { if (_mouseCursorState == CursorState::Date) { return true; } // return dynamic_cast(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(); Ui::Tooltip::Hide(); 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{ layout->getItem(), 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.item == _pressState.item) && hasSelectedText(); if (_overLayout) { auto cursorDeltaLength = [&] { auto cursorDelta = (_overState.cursor - _pressState.cursor); return cursorDelta.manhattanLength(); }; auto dragStartLength = [] { return QApplication::startDragDistance(); }; if (_overState.item != _pressState.item || 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; } const auto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost); if (lnkChanged || dragState.cursor != _mouseCursorState) { Ui::Tooltip::Hide(); } if (dragState.link) { Ui::Tooltip::Show(1000, this); } 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.item, selState); auto hasSelection = (selState == FullSelection) || (selState.from != selState.to); if (!_wasSelectedText && hasSelection) { _wasSelectedText = true; setFocus(); } clearDragSelection(); } else if (_pressState.item) { 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.item || !tillState.item || _provider->hasSelectRestriction()) { clearDragSelection(); return; } _provider->applyDragSelection( _dragSelected, fromState.item, SkipSelectFromItem(fromState), tillState.item, SkipSelectTillItem(tillState)); _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.item != _overState.item) { repaintItem(_pressState.item); } _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.item, 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.item, selStatus); _mouseAction = MouseAction::Selecting; repaintItem(pressLayout); } else if (!_provider->hasSelectRestriction()) { _mouseAction = MouseAction::PrepareSelect; } } } } else if (!_pressWasInactive && !_provider->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.item && _pressState.inside) { if (hasSelectedItems()) { uponSelected = isItemUnderPressSelected(); } else if (const auto pressLayout = _provider->lookupLayout( _pressState.item)) { StateRequest request; request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol; const auto dragState = pressLayout->getState( _pressState.cursor, request); uponSelected = isPressInSelectedText(dragState); } } auto pressedHandler = ClickHandler::getPressed(); if (dynamic_cast(pressedHandler.get())) { return; } TextWithEntities sel; //QList 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(u"application/x-td-forward"_q, "1"); // } // } // _controller->parentController()->window()->launchDrag(std::move(mimeData)); // return; //} else { // auto forwardMimeType = QString(); // auto pressedMedia = static_cast(nullptr); // if (auto pressedItem = _pressState.layout) { // pressedMedia = pressedItem->getMedia(); // if (_mouseCursorState == CursorState::Date) { // session().data().setMimeForwardIds(session().data().itemOrItsGroup(pressedItem)); // forwardMimeType = u"application/x-td-forward"_q; // } // } // if (auto pressedLnkItem = App::pressedLinkItem()) { // if ((pressedMedia = pressedLnkItem->getMedia())) { // if (forwardMimeType.isEmpty() && pressedMedia->dragItemByHandler(pressedHandler)) { // session().data().setMimeForwardIds({ 1, pressedLnkItem->fullId() }); // forwardMimeType = u"application/x-td-forward"_q; // } // } // } // if (!forwardMimeType.isEmpty()) { // auto mimeData = std::make_unique(); // mimeData->setData(forwardMimeType, "1"); // if (auto document = (pressedMedia ? pressedMedia->getDocument() : nullptr)) { // auto filepath = document->filepath(true); // if (!filepath.isEmpty()) { // QList 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.item); auto simpleSelectionChange = pressState.item && 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 = findItemByItem(pressState.item); const auto fullId = found ? found->layout->getItem()->fullId() : FullMsgId(); ActivateClickHandler(window(), activated, { button, QVariant::fromValue(ClickHandlerContext{ .itemId = fullId, .sessionWindow = base::make_weak( _controller->parentController()), }) }); return; } if (needSelectionToggle) { toggleItemSelection(pressState.item); } 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 (!_provider->hasSelectRestriction()) { applyDragSelection(_selected); } clearDragSelection(); pushSelectedItems(); } void ListWidget::applyDragSelection(SelectedMap &applyTo) const { if (_dragSelectAction == DragSelectAction::Selecting) { for (auto &[item, data] : _dragSelected) { ChangeItemSelection( applyTo, item, _provider->computeSelectionData(item, FullSelection)); } } else if (_dragSelectAction == DragSelectAction::Deselecting) { for (auto &[item, data] : _dragSelected) { applyTo.remove(item); } } } void ListWidget::refreshHeight() { resize(width(), recountHeight()); update(); } int ListWidget::recountHeight() { if (_sections.empty()) { if (const auto count = _provider->fullCount()) { if (*count == 0) { return 0; } } } auto cachedPadding = padding(); auto result = cachedPadding.top(); for (auto §ion : _sections) { section.setTop(result); result += section.height(); } return result + cachedPadding.bottom(); } void ListWidget::mouseActionUpdate() { mouseActionUpdate(_mousePosition); } std::vector::iterator ListWidget::findSectionByItem( not_null item) { if (_sections.size() < 2) { return _sections.begin(); } Assert(!_controller->isDownloads()); return ranges::lower_bound( _sections, GetUniversalId(item), std::greater<>(), [](const Section §ion) { return section.minId(); }); } auto ListWidget::findSectionAfterTop( int top) -> std::vector
::iterator { return ranges::lower_bound( _sections, top, std::less_equal<>(), [](const Section §ion) { return section.bottom(); }); } auto ListWidget::findSectionAfterTop( int top) const -> std::vector
::const_iterator { return ranges::lower_bound( _sections, top, std::less_equal<>(), [](const Section §ion) { return section.bottom(); }); } auto ListWidget::findSectionAfterBottom( std::vector
::const_iterator from, int bottom) const -> std::vector
::const_iterator { return ranges::lower_bound( from, _sections.end(), bottom, std::less<>(), [](const Section §ion) { 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