From 63c1212ef156fa2f31f2f63233e5c906cbf115fd Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 26 Jan 2018 18:40:11 +0300 Subject: [PATCH] Allow multiple items selection in HistoryView. --- Telegram/SourceFiles/boxes/confirm_box.cpp | 10 +- Telegram/SourceFiles/boxes/confirm_box.h | 6 + .../SourceFiles/data/data_media_types.cpp | 101 ++ Telegram/SourceFiles/data/data_media_types.h | 9 + .../admin_log/history_admin_log_inner.cpp | 10 +- .../admin_log/history_admin_log_inner.h | 4 +- .../history/feed/history_feed_section.cpp | 69 ++ .../history/feed/history_feed_section.h | 10 + .../history/history_inner_widget.cpp | 51 +- .../history/history_inner_widget.h | 2 +- Telegram/SourceFiles/history/history_item.cpp | 25 +- Telegram/SourceFiles/history/history_item.h | 10 +- .../history/history_item_components.cpp | 8 +- .../history/history_item_components.h | 4 +- .../SourceFiles/history/history_item_text.cpp | 106 ++ .../SourceFiles/history/history_item_text.h | 10 + Telegram/SourceFiles/history/history_media.h | 4 +- .../history/history_media_grouped.cpp | 20 +- .../history/history_media_types.cpp | 144 +-- .../SourceFiles/history/history_media_types.h | 17 - .../SourceFiles/history/history_message.cpp | 7 + .../SourceFiles/history/history_message.h | 1 + .../SourceFiles/history/history_widget.cpp | 68 +- Telegram/SourceFiles/history/history_widget.h | 9 +- .../view/history_view_context_menu.cpp | 5 +- .../history/view/history_view_element.cpp | 2 +- .../history/view/history_view_element.h | 1 + .../history/view/history_view_list_widget.cpp | 902 ++++++++++++++---- .../history/view/history_view_list_widget.h | 125 ++- .../history/view/history_view_message.cpp | 29 +- .../view/history_view_service_message.cpp | 3 +- .../view/history_view_top_bar_widget.cpp | 26 +- .../view/history_view_top_bar_widget.h | 19 +- Telegram/SourceFiles/info/info_top_bar.cpp | 19 +- .../info/media/info_media_list_widget.cpp | 58 +- .../info/media/info_media_list_widget.h | 10 +- Telegram/SourceFiles/mainwidget.cpp | 18 - Telegram/SourceFiles/mainwidget.h | 5 - Telegram/SourceFiles/ui/text/text_entity.cpp | 7 + Telegram/SourceFiles/ui/text/text_entity.h | 1 + .../SourceFiles/window/window_peer_menu.cpp | 2 +- Telegram/gyp/telegram_sources.txt | 2 + 42 files changed, 1402 insertions(+), 537 deletions(-) create mode 100644 Telegram/SourceFiles/history/history_item_text.cpp create mode 100644 Telegram/SourceFiles/history/history_item_text.h diff --git a/Telegram/SourceFiles/boxes/confirm_box.cpp b/Telegram/SourceFiles/boxes/confirm_box.cpp index cc218546e2..6ddeffd918 100644 --- a/Telegram/SourceFiles/boxes/confirm_box.cpp +++ b/Telegram/SourceFiles/boxes/confirm_box.cpp @@ -574,20 +574,20 @@ void DeleteMessagesBox::deleteAndClear() { } } - if (!_singleItem) { - App::main()->clearSelectedItems(); + if (_deleteConfirmedCallback) { + _deleteConfirmedCallback(); } QMap> idsByPeer; - for_const (auto fullId, _ids) { - if (auto item = App::histItemById(fullId)) { + for (const auto itemId : _ids) { + if (auto item = App::histItemById(itemId)) { auto history = item->history(); auto wasOnServer = (item->id > 0); auto wasLast = (history->lastMsg == item); item->destroy(); if (wasOnServer) { - idsByPeer[history->peer].push_back(MTP_int(fullId.msg)); + idsByPeer[history->peer].push_back(MTP_int(itemId.msg)); } else if (wasLast) { App::main()->checkPeerHistory(history->peer); } diff --git a/Telegram/SourceFiles/boxes/confirm_box.h b/Telegram/SourceFiles/boxes/confirm_box.h index d4be054df2..9b64eea404 100644 --- a/Telegram/SourceFiles/boxes/confirm_box.h +++ b/Telegram/SourceFiles/boxes/confirm_box.h @@ -166,6 +166,10 @@ public: bool suggestModerateActions); DeleteMessagesBox(QWidget*, MessageIdsList &&selected); + void setDeleteConfirmedCallback(base::lambda callback) { + _deleteConfirmedCallback = std::move(callback); + } + protected: void prepare() override; @@ -188,6 +192,8 @@ private: object_ptr _reportSpam = { nullptr }; object_ptr _deleteAll = { nullptr }; + base::lambda _deleteConfirmedCallback; + }; class ConfirmInviteBox : public BoxContent, public RPCSender { diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 087a53798c..a3a54bee44 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_location_manager.h" #include "history/view/history_view_element.h" +#include "ui/text_options.h" #include "storage/storage_shared_media.h" #include "storage/localstorage.h" #include "data/data_session.h" @@ -129,6 +130,19 @@ QString WithCaptionNotificationText( caption); } +TextWithEntities WithCaptionClipboardText( + const QString &attachType, + TextWithEntities &&caption) { + TextWithEntities result; + result.text.reserve(5 + attachType.size() + caption.text.size()); + result.text.append(qstr("[ ")).append(attachType).append(qstr(" ]")); + if (!caption.text.isEmpty()) { + result.text.append(qstr("\n")); + TextUtilities::Append(result, std::move(caption)); + } + return result; +} + } // namespace Media::Media(not_null parent) : _parent(parent) { @@ -298,6 +312,12 @@ QString MediaPhoto::pinnedTextSubstring() const { return lang(lng_action_pinned_media_photo); } +TextWithEntities MediaPhoto::clipboardText() const { + return WithCaptionClipboardText( + lang(lng_in_dlg_photo), + parent()->clipboardText()); +} + bool MediaPhoto::allowsEditCaption() const { return true; } @@ -536,6 +556,36 @@ QString MediaFile::pinnedTextSubstring() const { return lang(lng_action_pinned_media_file); } +TextWithEntities MediaFile::clipboardText() const { + const auto attachType = [&] { + const auto name = _document->composeNameString(); + const auto addName = !name.isEmpty() + ? qstr(" : ") + name + : QString(); + if (const auto sticker = _document->sticker()) { + if (!_emoji.isEmpty()) { + return lng_in_dlg_sticker_emoji(lt_emoji, _emoji); + } + return lang(lng_in_dlg_sticker); + } else if (_document->isAnimation()) { + if (_document->isVideoMessage()) { + return lang(lng_in_dlg_video_message); + } + return qsl("GIF"); + } else if (_document->isVideoFile()) { + return lang(lng_in_dlg_video); + } else if (_document->isVoiceMessage()) { + return lang(lng_in_dlg_audio) + addName; + } else if (_document->isSong()) { + return lang(lng_in_dlg_audio_file) + addName; + } + return lang(lng_in_dlg_file) + addName; + }(); + return WithCaptionClipboardText( + attachType, + parent()->clipboardText()); +} + bool MediaFile::allowsEditCaption() const { return !_document->isVideoMessage() && !_document->sticker(); } @@ -666,6 +716,18 @@ QString MediaContact::pinnedTextSubstring() const { return lang(lng_action_pinned_media_contact); } +TextWithEntities MediaContact::clipboardText() const { + const auto text = qsl("[ ") + lang(lng_in_dlg_contact) + qsl(" ]\n") + + lng_full_name( + lt_first_name, + _contact.firstName, + lt_last_name, + _contact.lastName).trimmed() + + '\n' + + _contact.phoneNumber; + return { text, EntitiesInText() }; +} + bool MediaContact::updateInlineResultMedia(const MTPMessageMedia &media) { return false; } @@ -734,6 +796,29 @@ QString MediaLocation::pinnedTextSubstring() const { return lang(lng_action_pinned_media_location); } +TextWithEntities MediaLocation::clipboardText() const { + TextWithEntities result = { + qsl("[ ") + lang(lng_maps_point) + qsl(" ]\n"), + EntitiesInText() + }; + auto titleResult = TextUtilities::ParseEntities( + TextUtilities::Clean(_title), + Ui::WebpageTextTitleOptions().flags); + auto descriptionResult = TextUtilities::ParseEntities( + TextUtilities::Clean(_description), + TextParseLinks | TextParseMultiline | TextParseRichText); + if (!titleResult.text.isEmpty()) { + TextUtilities::Append(result, std::move(titleResult)); + result.text.append('\n'); + } + if (!descriptionResult.text.isEmpty()) { + TextUtilities::Append(result, std::move(descriptionResult)); + result.text.append('\n'); + } + result.text += LocationClickHandler(_location->coords).dragText(); + return result; +} + bool MediaLocation::updateInlineResultMedia(const MTPMessageMedia &media) { return false; } @@ -783,6 +868,10 @@ QString MediaCall::pinnedTextSubstring() const { return QString(); } +TextWithEntities MediaCall::clipboardText() const { + return { qsl("[ ") + notificationText() + qsl(" ]"), EntitiesInText() }; +} + bool MediaCall::allowsForward() const { return false; } @@ -852,6 +941,10 @@ QString MediaWebPage::pinnedTextSubstring() const { return QString(); } +TextWithEntities MediaWebPage::clipboardText() const { + return TextWithEntities(); +} + bool MediaWebPage::allowsEdit() const { return true; } @@ -904,6 +997,10 @@ QString MediaGame::pinnedTextSubstring() const { return lng_action_pinned_media_game(lt_game, title); } +TextWithEntities MediaGame::clipboardText() const { + return TextWithEntities(); +} + QString MediaGame::errorTextForForward( not_null channel) const { if (channel->restricted(ChannelRestriction::f_send_games)) { @@ -965,6 +1062,10 @@ QString MediaInvoice::pinnedTextSubstring() const { return QString(); } +TextWithEntities MediaInvoice::clipboardText() const { + return TextWithEntities(); +} + bool MediaInvoice::updateInlineResultMedia(const MTPMessageMedia &media) { return true; } diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index dc10632fa9..141cb974b6 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -89,6 +89,7 @@ public: virtual QString chatsListText() const; virtual QString notificationText() const = 0; virtual QString pinnedTextSubstring() const = 0; + virtual TextWithEntities clipboardText() const = 0; virtual bool allowsForward() const; virtual bool allowsEdit() const; virtual bool allowsEditCaption() const; @@ -136,6 +137,7 @@ public: QString chatsListText() const override; QString notificationText() const override; QString pinnedTextSubstring() const override; + TextWithEntities clipboardText() const override; bool allowsEditCaption() const override; QString errorTextForForward( not_null channel) const override; @@ -169,6 +171,7 @@ public: QString chatsListText() const override; QString notificationText() const override; QString pinnedTextSubstring() const override; + TextWithEntities clipboardText() const override; bool allowsEditCaption() const override; bool forwardedBecomesUnread() const override; QString errorTextForForward( @@ -201,6 +204,7 @@ public: const SharedContact *sharedContact() const override; QString notificationText() const override; QString pinnedTextSubstring() const override; + TextWithEntities clipboardText() const override; bool updateInlineResultMedia(const MTPMessageMedia &media) override; bool updateSentMedia(const MTPMessageMedia &media) override; @@ -230,6 +234,7 @@ public: QString chatsListText() const override; QString notificationText() const override; QString pinnedTextSubstring() const override; + TextWithEntities clipboardText() const override; bool updateInlineResultMedia(const MTPMessageMedia &media) override; bool updateSentMedia(const MTPMessageMedia &media) override; @@ -255,6 +260,7 @@ public: const Call *call() const override; QString notificationText() const override; QString pinnedTextSubstring() const override; + TextWithEntities clipboardText() const override; bool allowsForward() const override; bool allowsRevoke() const override; @@ -286,6 +292,7 @@ public: QString chatsListText() const override; QString notificationText() const override; QString pinnedTextSubstring() const override; + TextWithEntities clipboardText() const override; bool allowsEdit() const override; bool updateInlineResultMedia(const MTPMessageMedia &media) override; @@ -311,6 +318,7 @@ public: QString notificationText() const override; QString pinnedTextSubstring() const override; + TextWithEntities clipboardText() const override; QString errorTextForForward( not_null channel) const override; @@ -343,6 +351,7 @@ public: QString notificationText() const override; QString pinnedTextSubstring() const override; + TextWithEntities clipboardText() const override; bool updateInlineResultMedia(const MTPMessageMedia &media) override; bool updateSentMedia(const MTPMessageMedia &media) override; diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index 947c64821c..00bf5b09e9 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_media_types.h" #include "history/history_message.h" #include "history/history_item_components.h" +#include "history/history_item_text.h" #include "history/admin_log/history_admin_log_section.h" #include "history/admin_log/history_admin_log_filter.h" #include "history/view/history_view_message.h" @@ -486,6 +487,11 @@ std::unique_ptr InnerWidget::elementCreate( return std::make_unique(this, message); } +bool InnerWidget::elementUnderCursor( + not_null view) { + return (App::hoveredItem() == view); +} + void InnerWidget::elementAnimationAutoplayAsync( not_null view) { crl::on_main(this, [this, msgId = view->data()->fullId()] { @@ -1121,9 +1127,7 @@ void InnerWidget::openContextGif(FullMsgId itemId) { void InnerWidget::copyContextText(FullMsgId itemId) { if (const auto item = App::histItemById(itemId)) { - if (const auto view = viewForItem(item)) { - SetClipboardWithEntities(view->selectedText(FullSelection)); - } + SetClipboardWithEntities(HistoryItemText(item)); } } diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index e1b89a145d..a2e073a56f 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -74,8 +74,10 @@ public: not_null message) override; std::unique_ptr elementCreate( not_null message) override; + bool elementUnderCursor( + not_null view) override; void elementAnimationAutoplayAsync( - not_null element) override; + not_null view) override; ~InnerWidget(); diff --git a/Telegram/SourceFiles/history/feed/history_feed_section.cpp b/Telegram/SourceFiles/history/feed/history_feed_section.cpp index 9232b580db..957a58ac2b 100644 --- a/Telegram/SourceFiles/history/feed/history_feed_section.cpp +++ b/Telegram/SourceFiles/history/feed/history_feed_section.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_element.h" #include "history/view/history_view_message.h" #include "history/view/history_view_service_message.h" +#include "history/history_item.h" #include "mainwidget.h" #include "lang/lang_keys.h" #include "ui/widgets/buttons.h" @@ -20,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/popup_menu.h" #include "boxes/confirm_box.h" #include "window/window_controller.h" +#include "window/window_peer_menu.h" #include "data/data_feed_messages.h" #include "data/data_photo.h" #include "data/data_document.h" @@ -69,6 +71,18 @@ Widget::Widget( _topBar->move(0, 0); _topBar->resizeToWidth(width()); _topBar->show(); + _topBar->forwardSelectionRequest( + ) | rpl::start_with_next([=] { + forwardSelected(); + }, _topBar->lifetime()); + _topBar->deleteSelectionRequest( + ) | rpl::start_with_next([=] { + confirmDeleteSelected(); + }, _topBar->lifetime()); + _topBar->clearSelectionRequest( + ) | rpl::start_with_next([=] { + clearSelected(); + }, _topBar->lifetime()); _topBarShadow->raise(); updateAdaptiveLayout(); @@ -175,6 +189,30 @@ rpl::producer Widget::listSource( limitAfter); } +bool Widget::listAllowsMultiSelect() { + return true; +} + +bool Widget::listIsLessInOrder( + not_null first, + not_null second) { + return first->position() < second->position(); +} + +void Widget::listSelectionChanged(HistoryView::SelectedItems &&items) { + HistoryView::TopBarWidget::SelectedState state; + state.count = items.size(); + for (const auto item : items) { + if (item.canForward) { + ++state.canForwardCount; + } + if (item.canDelete) { + ++state.canDeleteCount; + } + } + _topBar->showSelected(state); +} + std::unique_ptr Widget::createMemento() { auto result = std::make_unique(_feed); saveState(result.get()); @@ -280,4 +318,35 @@ QRect Widget::rectForFloatPlayer() const { return mapToGlobal(_scroll->geometry()); } +void Widget::forwardSelected() { + auto items = _inner->getSelectedItems(); + if (items.empty()) { + return; + } + const auto weak = make_weak(this); + Window::ShowForwardMessagesBox(std::move(items), [=] { + if (const auto strong = weak.data()) { + strong->clearSelected(); + } + }); +} + +void Widget::confirmDeleteSelected() { + auto items = _inner->getSelectedItems(); + if (items.empty()) { + return; + } + const auto weak = make_weak(this); + const auto box = Ui::show(Box(std::move(items))); + box->setDeleteConfirmedCallback([=] { + if (const auto strong = weak.data()) { + strong->clearSelected(); + } + }); +} + +void Widget::clearSelected() { + _inner->cancelSelection(); +} + } // namespace HistoryFeed diff --git a/Telegram/SourceFiles/history/feed/history_feed_section.h b/Telegram/SourceFiles/history/feed/history_feed_section.h index cf2ed7f6d6..043835e728 100644 --- a/Telegram/SourceFiles/history/feed/history_feed_section.h +++ b/Telegram/SourceFiles/history/feed/history_feed_section.h @@ -68,6 +68,12 @@ public: Data::MessagePosition aroundId, int limitBefore, int limitAfter) override; + bool listAllowsMultiSelect() override; + bool listIsLessInOrder( + not_null first, + not_null second) override; + void listSelectionChanged( + HistoryView::SelectedItems &&items) override; protected: void resizeEvent(QResizeEvent *e) override; @@ -86,6 +92,10 @@ private: void saveState(not_null memento); void restoreState(not_null memento); + void forwardSelected(); + void confirmDeleteSelected(); + void clearSelected(); + not_null _feed; object_ptr _scroll; QPointer _inner; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 00a33fde63..c6f93ae44e 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_message.h" #include "history/history_media_types.h" #include "history/history_item_components.h" +#include "history/history_item_text.h" #include "history/view/history_view_message.h" #include "history/view/history_view_service_message.h" #include "ui/text_options.h" @@ -1326,7 +1327,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { // -2 - has full selected items, but not over, -1 - has selection, but no over, 0 - no selection, 1 - over text, 2 - over full selected items auto isUponSelected = 0; - auto hasSelected = 0;; + auto hasSelected = 0; if (!_selected.empty()) { isUponSelected = -1; if (_selected.cbegin()->second == FullSelection) { @@ -1441,14 +1442,18 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } if (isUponSelected > 1) { if (selectedState.count > 0 && selectedState.canForwardCount == selectedState.count) { - _menu->addAction(lang(lng_context_forward_selected), _widget, SLOT(onForwardSelected())); + _menu->addAction(lang(lng_context_forward_selected), [=] { + _widget->forwardSelected(); + }); } if (selectedState.count > 0 && selectedState.canDeleteCount == selectedState.count) { _menu->addAction(lang(lng_context_delete_selected), [=] { - _widget->confirmDeleteSelectedItems(); + _widget->confirmDeleteSelected(); }); } - _menu->addAction(lang(lng_context_clear_selection), _widget, SLOT(onClearSelected())); + _menu->addAction(lang(lng_context_clear_selection), [=] { + _widget->clearSelected(); + }); } else if (item) { const auto itemId = item->fullId(); if (isUponSelected != -2) { @@ -1576,14 +1581,18 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } if (isUponSelected > 1) { if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) { - _menu->addAction(lang(lng_context_forward_selected), _widget, SLOT(onForwardSelected())); + _menu->addAction(lang(lng_context_forward_selected), [=] { + _widget->forwardSelected(); + }); } if (selectedState.count > 0 && selectedState.count == selectedState.canDeleteCount) { _menu->addAction(lang(lng_context_delete_selected), [=] { - _widget->confirmDeleteSelectedItems(); + _widget->confirmDeleteSelected(); }); } - _menu->addAction(lang(lng_context_clear_selection), _widget, SLOT(onClearSelected())); + _menu->addAction(lang(lng_context_clear_selection), [=] { + _widget->clearSelected(); + }); } else if (item && ((isUponSelected != -2 && (canForward || canDelete)) || item->id > 0)) { if (isUponSelected != -2) { if (canForward) { @@ -1709,10 +1718,8 @@ void HistoryInner::saveContextGif(FullMsgId itemId) { void HistoryInner::copyContextText(FullMsgId itemId) { if (const auto item = App::histItemById(itemId)) { - if (const auto view = item->mainView()) { - // #TODO check for a group - SetClipboardWithEntities(view->selectedText(FullSelection)); - } + // #TODO check for a group + SetClipboardWithEntities(HistoryItemText(item)); } } @@ -1743,13 +1750,11 @@ TextWithEntities HistoryInner::getSelectedText() const { auto fullSize = 0; auto texts = base::flat_map, TextWithEntities>(); - const auto addItem = [&]( - not_null view, - TextSelection selection) { + const auto addItem = [&](not_null view) { const auto item = view->data(); auto time = item->date.toString(timeFormat); auto part = TextWithEntities(); - auto unwrapped = view->selectedText(selection); + auto unwrapped = HistoryItemText(item); auto size = item->author()->name.size() + time.size() + unwrapped.text.size(); @@ -1780,17 +1785,17 @@ TextWithEntities HistoryInner::getSelectedText() const { // group->leader); //if (leaderSelection == FullSelection) { // groupLeadersAdded.emplace(group->leader); - // addItem(group->leader, FullSelection); + // addItem(group->leader); //} else if (view == group->leader) { // const auto leaderFullSelection = AddGroupItemSelection( // TextSelection(), // int(group->others.size())); // addItem(view, leaderFullSelection); //} else { - // addItem(view, FullSelection); + // addItem(view); //} } else { - addItem(view, FullSelection); + addItem(view); } } @@ -1820,7 +1825,7 @@ void HistoryInner::keyPressEvent(QKeyEvent *e) { auto selectedState = getSelectionState(); if (selectedState.count > 0 && selectedState.canDeleteCount == selectedState.count) { - _widget->confirmDeleteSelectedItems(); + _widget->confirmDeleteSelected(); } } else { e->ignore(); @@ -2106,7 +2111,7 @@ bool HistoryInner::focusNextPrevChild(bool next) { if (_selected.empty()) { return TWidget::focusNextPrevChild(next); } else { - clearSelectedItems(); + clearSelected(); return true; } } @@ -2208,7 +2213,7 @@ auto HistoryInner::getSelectionState() const return result; } -void HistoryInner::clearSelectedItems(bool onlyTextSelection) { +void HistoryInner::clearSelected(bool onlyTextSelection) { if (!_selected.empty() && (!onlyTextSelection || _selected.cbegin()->second != FullSelection)) { _selected.clear(); _widget->updateTopBarSelection(); @@ -2927,6 +2932,10 @@ not_null HistoryInner::ElementDelegate() { not_null message) override { return std::make_unique(this, message); } + bool elementUnderCursor( + not_null view) override { + return (App::hoveredItem() == view); + } void elementAnimationAutoplayAsync( not_null view) override { crl::on_main(&Auth(), [msgId = view->data()->fullId()] { diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 6e542e326b..c8037975fa 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -58,7 +58,7 @@ public: bool canDeleteSelected() const; HistoryView::TopBarWidget::SelectedState getSelectionState() const; - void clearSelectedItems(bool onlyTextSelection = false); + void clearSelected(bool onlyTextSelection = false); MessageIdsList getSelectedItems() const; void selectItem(not_null item); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index bbb3e077b9..8f1e9bf3a0 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -679,14 +679,25 @@ HistoryItem::~HistoryItem() { } } +ClickHandlerPtr goToMessageClickHandler( + not_null item, + FullMsgId returnToId) { + return goToMessageClickHandler( + item->history()->peer, + item->id, + returnToId); +} + ClickHandlerPtr goToMessageClickHandler( not_null peer, - MsgId msgId) { + MsgId msgId, + FullMsgId returnToId) { return std::make_shared([=] { - if (App::main()) { - auto view = App::mousedItem(); - if (view && view->data()->history()->peer == peer) { - App::main()->pushReplyReturn(view->data()); + if (const auto main = App::main()) { + if (const auto returnTo = App::histItemById(returnToId)) { + if (returnTo->history()->peer == peer) { + main->pushReplyReturn(returnTo); + } } App::wnd()->controller()->showPeerHistory( peer, @@ -696,10 +707,6 @@ ClickHandlerPtr goToMessageClickHandler( }); } -ClickHandlerPtr goToMessageClickHandler(not_null item) { - return goToMessageClickHandler(item->history()->peer, item->id); -} - not_null HistoryItem::Create( not_null history, const MTPMessage &message) { diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 2d1a38eb55..bf1ad42c8f 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -180,6 +180,9 @@ public: virtual TextWithEntities originalText() const { return { QString(), EntitiesInText() }; } + virtual TextWithEntities clipboardText() const { + return { QString(), EntitiesInText() }; + } virtual void setViewsCount(int32 count) { } @@ -302,5 +305,8 @@ private: ClickHandlerPtr goToMessageClickHandler( not_null peer, - MsgId msgId); -ClickHandlerPtr goToMessageClickHandler(not_null item); + MsgId msgId, + FullMsgId returnToId = FullMsgId()); +ClickHandlerPtr goToMessageClickHandler( + not_null item, + FullMsgId returnToId = FullMsgId()); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index addd5372d4..db32b4721a 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -125,7 +125,9 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { } } -bool HistoryMessageReply::updateData(HistoryMessage *holder, bool force) { +bool HistoryMessageReply::updateData( + not_null holder, + bool force) { if (!force) { if (replyToMsg || !replyToMsgId) { return true; @@ -152,7 +154,7 @@ bool HistoryMessageReply::updateData(HistoryMessage *holder, bool force) { updateName(); - replyToLnk = goToMessageClickHandler(replyToMsg); + replyToLnk = goToMessageClickHandler(replyToMsg, holder->fullId()); if (!replyToMsg->Has()) { if (auto bot = replyToMsg->viaBot()) { replyToVia = std::make_unique(); @@ -168,7 +170,7 @@ bool HistoryMessageReply::updateData(HistoryMessage *holder, bool force) { return (replyToMsg || !replyToMsgId); } -void HistoryMessageReply::clearData(HistoryMessage *holder) { +void HistoryMessageReply::clearData(not_null holder) { replyToVia = nullptr; if (replyToMsg) { App::historyUnregDependency(holder, replyToMsg); diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 695aaf61b3..76c8c3ba9a 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -84,10 +84,10 @@ struct HistoryMessageReply : public RuntimeComponent holder, bool force = false); // Must be called before destructor. - void clearData(HistoryMessage *holder); + void clearData(not_null holder); bool isNameUpdated() const; void updateName() const; diff --git a/Telegram/SourceFiles/history/history_item_text.cpp b/Telegram/SourceFiles/history/history_item_text.cpp new file mode 100644 index 0000000000..9ba0aa8bf1 --- /dev/null +++ b/Telegram/SourceFiles/history/history_item_text.cpp @@ -0,0 +1,106 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/history_item_text.h" + +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "data/data_media_types.h" +#include "data/data_web_page.h" +#include "lang/lang_keys.h" +#include "ui/text_options.h" + +TextWithEntities WrapAsReply( + TextWithEntities &&text, + not_null to) { + const auto name = to->author()->name; + auto result = TextWithEntities(); + result.text.reserve( + lang(lng_in_reply_to).size() + + name.size() + + 4 + + text.text.size()); + result.text.append('[' + ).append(lang(lng_in_reply_to) + ).append(' ' + ).append(name + ).append(qsl("]\n") + ); + TextUtilities::Append(result, std::move(text)); + return result; +} + +TextWithEntities WrapAsForwarded( + TextWithEntities &&text, + not_null forwarded) { + auto info = forwarded->text.originalTextWithEntities( + AllTextSelection, + ExpandLinksAll); + auto result = TextWithEntities(); + result.text.reserve( + info.text.size() + + 4 + + text.text.size()); + result.entities.reserve( + info.entities.size() + + text.entities.size()); + result.text.append('['); + TextUtilities::Append(result, std::move(info)); + result.text.append(qsl("]\n")); + TextUtilities::Append(result, std::move(text)); + return result; +} + +TextWithEntities HistoryItemText(not_null item) { + const auto media = item->media(); + + auto textResult = item->clipboardText(); + auto mediaResult = media ? media->clipboardText() : TextWithEntities(); + auto logEntryOriginalResult = [&] { + const auto entry = item->Get(); + if (!entry) { + return TextWithEntities(); + } + const auto title = TextUtilities::SingleLine(entry->page->title.isEmpty() + ? entry->page->author + : entry->page->title); + auto titleResult = TextUtilities::ParseEntities( + title, + Ui::WebpageTextTitleOptions().flags); + auto descriptionResult = entry->page->description; + if (titleResult.text.isEmpty()) { + return descriptionResult; + } else if (descriptionResult.text.isEmpty()) { + return titleResult; + } + titleResult.text += '\n'; + TextUtilities::Append(titleResult, std::move(descriptionResult)); + return titleResult; + }(); + auto result = textResult; + if (result.text.isEmpty()) { + result = std::move(mediaResult); + } else if (!mediaResult.text.isEmpty()) { + result.text += qstr("\n\n"); + TextUtilities::Append(result, std::move(mediaResult)); + } + if (result.text.isEmpty()) { + result = std::move(logEntryOriginalResult); + } else if (!logEntryOriginalResult.text.isEmpty()) { + result.text += qstr("\n\n"); + TextUtilities::Append(result, std::move(logEntryOriginalResult)); + } + if (const auto reply = item->Get()) { + if (const auto message = reply->replyToMsg) { + result = WrapAsReply(std::move(result), message); + } + } + if (const auto forwarded = item->Get()) { + result = WrapAsForwarded(std::move(result), forwarded); + } + return result; +} diff --git a/Telegram/SourceFiles/history/history_item_text.h b/Telegram/SourceFiles/history/history_item_text.h new file mode 100644 index 0000000000..3e61d308b9 --- /dev/null +++ b/Telegram/SourceFiles/history/history_item_text.h @@ -0,0 +1,10 @@ +/* +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 +*/ +#pragma once + +TextWithEntities HistoryItemText(not_null item); diff --git a/Telegram/SourceFiles/history/history_media.h b/Telegram/SourceFiles/history/history_media.h index c275087650..253c06741a 100644 --- a/Telegram/SourceFiles/history/history_media.h +++ b/Telegram/SourceFiles/history/history_media.h @@ -59,7 +59,9 @@ public: virtual HistoryMediaType type() const = 0; - virtual TextWithEntities selectedText(TextSelection selection) const = 0; + virtual TextWithEntities selectedText(TextSelection selection) const { + return TextWithEntities(); + } bool hasPoint(QPoint point) const { return QRect(0, 0, width(), height()).contains(point); diff --git a/Telegram/SourceFiles/history/history_media_grouped.cpp b/Telegram/SourceFiles/history/history_media_grouped.cpp index 4ed720af59..3cc4a3aef3 100644 --- a/Telegram/SourceFiles/history/history_media_grouped.cpp +++ b/Telegram/SourceFiles/history/history_media_grouped.cpp @@ -261,15 +261,17 @@ TextSelection HistoryGroupedMedia::adjustSelection( TextWithEntities HistoryGroupedMedia::selectedText( TextSelection selection) const { - if (!IsSubGroupSelection(selection)) { - return WithCaptionSelectedText( - lang(lng_in_dlg_album), - _caption, - selection); - } else if (IsGroupItemSelection(selection, int(_parts.size()) - 1)) { - return main()->selectedText(FullSelection); - } - return TextWithEntities(); + return _caption.originalTextWithEntities(selection, ExpandLinksAll); + // #TODO group select + //if (!IsSubGroupSelection(selection)) { + // return WithCaptionSelectedText( + // lang(lng_in_dlg_album), + // _caption, + // selection); + //} else if (IsGroupItemSelection(selection, int(_parts.size()) - 1)) { + // return main()->selectedText(FullSelection); + //} + //return TextWithEntities(); } void HistoryGroupedMedia::clickHandlerActiveChanged( diff --git a/Telegram/SourceFiles/history/history_media_types.cpp b/Telegram/SourceFiles/history/history_media_types.cpp index a0f2512001..61046e43a4 100644 --- a/Telegram/SourceFiles/history/history_media_types.cpp +++ b/Telegram/SourceFiles/history/history_media_types.cpp @@ -94,27 +94,6 @@ std::unique_ptr CreateAttach( } // namespace -TextWithEntities WithCaptionSelectedText( - const QString &attachType, - const Text &caption, - TextSelection selection) { - if (selection != FullSelection) { - return caption.originalTextWithEntities(selection, ExpandLinksAll); - } - - TextWithEntities result, original; - if (!caption.isEmpty()) { - original = caption.originalTextWithEntities(AllTextSelection, ExpandLinksAll); - } - result.text.reserve(5 + attachType.size() + original.text.size()); - result.text.append(qstr("[ ")).append(attachType).append(qstr(" ]")); - if (!caption.isEmpty()) { - result.text.append(qstr("\n")); - TextUtilities::Append(result, std::move(original)); - } - return result; -} - void HistoryFileMedia::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (p == _savel || p == _cancell) { if (active && !dataLoaded()) { @@ -682,10 +661,7 @@ void HistoryPhoto::validateGroupedCache( } TextWithEntities HistoryPhoto::selectedText(TextSelection selection) const { - return WithCaptionSelectedText( - lang(lng_in_dlg_photo), - _caption, - selection); + return _caption.originalTextWithEntities(selection, ExpandLinksAll); } bool HistoryPhoto::needsBubble() const { @@ -1122,10 +1098,7 @@ void HistoryVideo::setStatusSize(int newSize) const { } TextWithEntities HistoryVideo::selectedText(TextSelection selection) const { - return WithCaptionSelectedText( - lang(lng_in_dlg_video), - _caption, - selection); + return _caption.originalTextWithEntities(selection, ExpandLinksAll); } bool HistoryVideo::needsBubble() const { @@ -1715,38 +1688,11 @@ bool HistoryDocument::hasTextForCopy() const { } TextWithEntities HistoryDocument::selectedText(TextSelection selection) const { - TextWithEntities result; - buildStringRepresentation([&result, selection](const QString &type, const QString &fileName, const Text &caption) { - auto fullType = type; - if (!fileName.isEmpty()) { - fullType.append(qstr(" : ")).append(fileName); - } - result = WithCaptionSelectedText(fullType, caption, selection); - }); - return result; -} - -template -void HistoryDocument::buildStringRepresentation(Callback callback) const { - const Text emptyCaption; - const Text *caption = &emptyCaption; - if (auto captioned = Get()) { - caption = &captioned->_caption; + if (const auto captioned = Get()) { + const auto &caption = captioned->_caption; + return caption.originalTextWithEntities(selection, ExpandLinksAll); } - QString attachType = lang(lng_in_dlg_file); - if (Has()) { - attachType = lang(lng_in_dlg_audio); - } else if (_data->isAudioFile()) { - attachType = lang(lng_in_dlg_audio_file); - } - - QString attachFileName; - if (auto named = Get()) { - if (!named->_name.isEmpty()) { - attachFileName = named->_name; - } - } - return callback(attachType, attachFileName, *caption); + return TextWithEntities(); } void HistoryDocument::setStatusSize(int newSize, qint64 realDuration) const { @@ -2521,7 +2467,7 @@ HistoryTextState HistoryGif::getState(QPoint point, HistoryStateRequest request) } TextWithEntities HistoryGif::selectedText(TextSelection selection) const { - return WithCaptionSelectedText(mediaTypeString(), _caption, selection); + return _caption.originalTextWithEntities(selection, ExpandLinksAll); } bool HistoryGif::needsBubble() const { @@ -2979,17 +2925,6 @@ HistoryTextState HistorySticker::getState(QPoint point, HistoryStateRequest requ return result; } -QString HistorySticker::toString() const { - return _emoji.isEmpty() ? lang(lng_in_dlg_sticker) : lng_in_dlg_sticker_emoji(lt_emoji, _emoji); -} - -TextWithEntities HistorySticker::selectedText(TextSelection selection) const { - if (selection != FullSelection) { - return TextWithEntities(); - } - return { qsl("[ ") + toString() + qsl(" ]"), EntitiesInText() }; -} - ImagePtr HistorySticker::replyPreview() { return _data->makeReplyPreview(); } @@ -3197,13 +3132,6 @@ HistoryTextState HistoryContact::getState(QPoint point, HistoryStateRequest requ return result; } -TextWithEntities HistoryContact::selectedText(TextSelection selection) const { - if (selection != FullSelection) { - return TextWithEntities(); - } - return { qsl("[ ") + lang(lng_in_dlg_contact) + qsl(" ]\n") + _name.originalText() + '\n' + _phone, EntitiesInText() }; -} - HistoryCall::HistoryCall( not_null parent, not_null call) @@ -3291,13 +3219,6 @@ HistoryTextState HistoryCall::getState(QPoint point, HistoryStateRequest request return result; } -TextWithEntities HistoryCall::selectedText(TextSelection selection) const { - if (selection != FullSelection) { - return TextWithEntities(); - } - return { qsl("[ ") + _text + qsl(" ]"), EntitiesInText() }; -} - namespace { int articleThumbWidth(PhotoData *thumb, int height) { @@ -3841,11 +3762,12 @@ bool HistoryWebPage::isDisplayed() const { } TextWithEntities HistoryWebPage::selectedText(TextSelection selection) const { - if (selection == FullSelection && !isLogEntryOriginal()) { - return TextWithEntities(); - } - auto titleResult = _title.originalTextWithEntities((selection == FullSelection) ? AllTextSelection : selection, ExpandLinksAll); - auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection((selection == FullSelection) ? AllTextSelection : selection), ExpandLinksAll); + auto titleResult = _title.originalTextWithEntities( + selection, + ExpandLinksAll); + auto descriptionResult = _description.originalTextWithEntities( + toDescriptionSelection(selection), + ExpandLinksAll); if (titleResult.text.isEmpty()) { return descriptionResult; } else if (descriptionResult.text.isEmpty()) { @@ -4237,11 +4159,12 @@ void HistoryGame::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pres } TextWithEntities HistoryGame::selectedText(TextSelection selection) const { - if (selection == FullSelection) { - return TextWithEntities(); - } - auto titleResult = _title.originalTextWithEntities(selection, ExpandLinksAll); - auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection(selection), ExpandLinksAll); + auto titleResult = _title.originalTextWithEntities( + selection, + ExpandLinksAll); + auto descriptionResult = _description.originalTextWithEntities( + toDescriptionSelection(selection), + ExpandLinksAll); if (titleResult.text.isEmpty()) { return descriptionResult; } else if (descriptionResult.text.isEmpty()) { @@ -4637,11 +4560,12 @@ void HistoryInvoice::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool p } TextWithEntities HistoryInvoice::selectedText(TextSelection selection) const { - if (selection == FullSelection) { - return TextWithEntities(); - } - auto titleResult = _title.originalTextWithEntities(selection, ExpandLinksAll); - auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection(selection), ExpandLinksAll); + auto titleResult = _title.originalTextWithEntities( + selection, + ExpandLinksAll); + auto descriptionResult = _description.originalTextWithEntities( + toDescriptionSelection(selection), + ExpandLinksAll); if (titleResult.text.isEmpty()) { return descriptionResult; } else if (descriptionResult.text.isEmpty()) { @@ -4685,12 +4609,11 @@ HistoryLocation::HistoryLocation( Ui::WebpageTextTitleOptions()); } if (!description.isEmpty()) { - auto marked = TextWithEntities { TextUtilities::Clean(description) }; - auto parseFlags = TextParseLinks | TextParseMultiline | TextParseRichText; - TextUtilities::ParseEntities(marked, parseFlags); _description.setMarkedText( st::webPageDescriptionStyle, - marked, + TextUtilities::ParseEntities( + TextUtilities::Clean(description), + TextParseLinks | TextParseMultiline | TextParseRichText), Ui::WebpageTextDescriptionOptions()); } } @@ -4924,17 +4847,6 @@ TextSelection HistoryLocation::adjustSelection(TextSelection selection, TextSele } TextWithEntities HistoryLocation::selectedText(TextSelection selection) const { - if (selection == FullSelection) { - TextWithEntities result = { qsl("[ ") + lang(lng_maps_point) + qsl(" ]\n"), EntitiesInText() }; - auto info = selectedText(AllTextSelection); - if (!info.text.isEmpty()) { - TextUtilities::Append(result, std::move(info)); - result.text.append('\n'); - } - result.text += _link->dragText(); - return result; - } - auto titleResult = _title.originalTextWithEntities(selection); auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection(selection)); if (titleResult.text.isEmpty()) { diff --git a/Telegram/SourceFiles/history/history_media_types.h b/Telegram/SourceFiles/history/history_media_types.h index aa2cce7237..e094ed9182 100644 --- a/Telegram/SourceFiles/history/history_media_types.h +++ b/Telegram/SourceFiles/history/history_media_types.h @@ -41,11 +41,6 @@ namespace Ui { class EmptyUserpic; } // namespace Ui -TextWithEntities WithCaptionSelectedText( - const QString &attachType, - const Text &caption, - TextSelection selection); - class HistoryFileMedia : public HistoryMedia { public: using HistoryMedia::HistoryMedia; @@ -390,11 +385,6 @@ private: void setStatusSize(int newSize, qint64 realDuration = 0) const; bool updateStatusText() const; // returns showPause - // Callback is a void(const QString &, const QString &, const Text &) functor. - // It will be called as callback(attachType, attachFileName, attachCaption). - template - void buildStringRepresentation(Callback callback) const; - not_null _data; }; @@ -523,8 +513,6 @@ public: return true; } - TextWithEntities selectedText(TextSelection selection) const override; - DocumentData *getDocument() const override { return _data; } @@ -550,7 +538,6 @@ private: int additionalWidth(const HistoryMessageVia *via, const HistoryMessageReply *reply) const; int additionalWidth() const; - QString toString() const; int _pixw = 1; int _pixh = 1; @@ -584,8 +571,6 @@ public: return true; } - TextWithEntities selectedText(TextSelection selection) const override; - bool needsBubble() const override { return true; } @@ -643,8 +628,6 @@ public: return false; } - TextWithEntities selectedText(TextSelection selection) const override; - bool needsBubble() const override { return true; } diff --git a/Telegram/SourceFiles/history/history_message.cpp b/Telegram/SourceFiles/history/history_message.cpp index f841a626e3..5683d2f794 100644 --- a/Telegram/SourceFiles/history/history_message.cpp +++ b/Telegram/SourceFiles/history/history_message.cpp @@ -1017,6 +1017,13 @@ TextWithEntities HistoryMessage::originalText() const { return _text.originalTextWithEntities(); } +TextWithEntities HistoryMessage::clipboardText() const { + if (emptyText()) { + return { QString(), EntitiesInText() }; + } + return _text.originalTextWithEntities(AllTextSelection, ExpandLinksAll); +} + bool HistoryMessage::textHasLinks() const { return emptyText() ? false : _text.hasLinks(); } diff --git a/Telegram/SourceFiles/history/history_message.h b/Telegram/SourceFiles/history/history_message.h index 7e4ea11109..8b8e045d7b 100644 --- a/Telegram/SourceFiles/history/history_message.h +++ b/Telegram/SourceFiles/history/history_message.h @@ -121,6 +121,7 @@ public: void setText(const TextWithEntities &textWithEntities) override; TextWithEntities originalText() const override; + TextWithEntities clipboardText() const override; bool textHasLinks() const override; int viewsCount() const override; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 864e21c6ab..80e966906f 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -400,7 +400,10 @@ HistoryHider::~HistoryHider() { parent()->noHider(this); } -HistoryWidget::HistoryWidget(QWidget *parent, not_null controller) : Window::AbstractSectionWidget(parent, controller) +HistoryWidget::HistoryWidget( + QWidget *parent, + not_null controller) +: Window::AbstractSectionWidget(parent, controller) , _fieldBarCancel(this, st::historyReplyCancel) , _topBar(this, controller) , _scroll(this, st::historyScroll, false) @@ -672,9 +675,21 @@ HistoryWidget::HistoryWidget(QWidget *parent, not_null cont } }, lifetime()); _topBar->membersShowAreaActive( - ) | rpl::start_with_next([this](bool active) { + ) | rpl::start_with_next([=](bool active) { setMembersShowAreaActive(active); }, _topBar->lifetime()); + _topBar->forwardSelectionRequest( + ) | rpl::start_with_next([=] { + forwardSelected(); + }, _topBar->lifetime()); + _topBar->deleteSelectionRequest( + ) | rpl::start_with_next([=] { + confirmDeleteSelected(); + }, _topBar->lifetime()); + _topBar->clearSelectionRequest( + ) | rpl::start_with_next([=] { + clearSelected(); + }, _topBar->lifetime()); Auth().api().sendActions( ) | rpl::start_with_next([this](const ApiWrap::SendOptions &options) { @@ -1675,11 +1690,16 @@ void HistoryWidget::showHistory(const PeerId &peerId, MsgId showAtMsgId, bool re Auth().data().stopAutoplayAnimations(); } clearReplyReturns(); - clearAllLoadRequests(); if (_history) { - if (App::main()) App::main()->saveDraftToCloud(); + if (Ui::InFocusChain(_list)) { + // Removing focus from list clears selected and updates top bar. + setFocus(); + } + if (App::main()) { + App::main()->saveDraftToCloud(); + } if (_migrated) { _migrated->clearLocalDraft(); // use migrated draft only once _migrated->clearEditDraft(); @@ -1710,7 +1730,7 @@ void HistoryWidget::showHistory(const PeerId &peerId, MsgId showAtMsgId, bool re _fieldBarCancel->hide(); _membersDropdownShowTimer.stop(); - _scroll->takeWidget().destroyDelayed(); + _scroll->takeWidget().destroy(); _list = nullptr; clearInlineBot(); @@ -3972,7 +3992,9 @@ void HistoryWidget::onFieldResize() { } void HistoryWidget::onFieldFocused() { - if (_list) _list->clearSelectedItems(true); + if (_list) { + _list->clearSelected(true); + } } void HistoryWidget::onCheckFieldAutocomplete() { @@ -6178,25 +6200,37 @@ void HistoryWidget::handlePeerUpdate() { } } -void HistoryWidget::onForwardSelected() { - if (!_list) return; - auto weak = make_weak(this); +void HistoryWidget::forwardSelected() { + if (!_list) { + return; + } + const auto weak = make_weak(this); Window::ShowForwardMessagesBox(getSelectedItems(), [=] { - if (weak) { - weak->onClearSelected(); + if (const auto strong = weak.data()) { + strong->clearSelected(); } }); } -void HistoryWidget::confirmDeleteSelectedItems() { +void HistoryWidget::confirmDeleteSelected() { if (!_list) return; - App::main()->deleteLayer(_list->getSelectedItems()); + auto items = _list->getSelectedItems(); + if (items.empty()) { + return; + } + const auto weak = make_weak(this); + const auto box = Ui::show(Box(std::move(items))); + box->setDeleteConfirmedCallback([=] { + if (const auto strong = weak.data()) { + strong->clearSelected(); + } + }); } void HistoryWidget::onListEscapePressed() { if (_nonEmptySelection && _list) { - onClearSelected(); + clearSelected(); } else { onCancel(); } @@ -6208,8 +6242,10 @@ void HistoryWidget::onListEnterPressed() { } } -void HistoryWidget::onClearSelected() { - if (_list) _list->clearSelectedItems(); +void HistoryWidget::clearSelected() { + if (_list) { + _list->clearSelected(); + } } HistoryItem *HistoryWidget::getItemFromHistoryOrMigrated(MsgId genericMsgId) const { diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 4edf1d3d72..1e9dcfe5d6 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -324,9 +324,9 @@ public: void grapWithoutTopBarShadow(); void grabFinish() override; - bool isItemVisible(HistoryItem *item); - - void confirmDeleteSelectedItems(); + void forwardSelected(); + void confirmDeleteSelected(); + void clearSelected(); // Float player interface. bool wheelEventFromFloatPlayer(QEvent *e) override; @@ -415,9 +415,6 @@ public slots: void onCheckFieldAutocomplete(); void onScrollTimer(); - void onForwardSelected(); - void onClearSelected(); - void onDraftSaveDelayed(); void onDraftSave(bool delayed = false); void onCloudDraftSave(); diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index b96b8c4e28..81ffda7fbf 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_list_widget.h" #include "history/history_item.h" +#include "history/history_item_text.h" #include "history/history_media_types.h" #include "ui/widgets/popup_menu.h" #include "chat_helpers/message_field.h" @@ -218,7 +219,9 @@ base::unique_qptr FillContextMenu( } if (!link && (view->hasVisibleText() || mediaHasTextForCopy)) { result->addAction(lang(lng_context_copy_text), [=] { - SetClipboardWithEntities(list->getItemText(itemId)); + if (const auto item = App::histItemById(itemId)) { + SetClipboardWithEntities(HistoryItemText(item)); + } }); } } diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index c01ad7120b..e9bb6779d7 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -186,7 +186,7 @@ int Element::marginBottom() const { } bool Element::isUnderCursor() const { - return (App::hoveredItem() == this); + return _delegate->elementUnderCursor(this); } void Element::setPendingResize() { diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index f89fedfa21..92d53b913c 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -37,6 +37,7 @@ public: not_null message) = 0; virtual std::unique_ptr elementCreate( not_null message) = 0; + virtual bool elementUnderCursor(not_null view) = 0; virtual void elementAnimationAutoplayAsync( not_null element) = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 23a49d20c2..02873133b1 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -220,7 +220,8 @@ ListWidget::ListWidget( , _delegate(delegate) , _controller(controller) , _context(_delegate->listContext()) -, _scrollDateCheck([this] { scrollDateCheck(); }) { +, _scrollDateCheck([this] { scrollDateCheck(); }) +, _selectEnabled(_delegate->listAllowsMultiSelect()) { setMouseTracking(true); _scrollDateHideTimer.setCallback([this] { scrollDateHideByTimer(); }); Auth().data().viewRepaintRequest( @@ -245,7 +246,7 @@ ListWidget::ListWidget( ) | rpl::start_with_next([this](auto view) { if (view->delegate() == this) { if (view->isUnderCursor()) { - updateSelected(); + mouseActionUpdate(); } } }, lifetime()); @@ -327,6 +328,13 @@ void ListWidget::restoreScrollState() { _scrollTopState = ScrollTopState(); } +Element *ListWidget::viewForItem(FullMsgId itemId) const { + if (const auto item = App::histItemById(itemId)) { + return viewForItem(item); + } + return nullptr; +} + Element *ListWidget::viewForItem(const HistoryItem *item) const { if (item) { if (const auto i = _views.find(item); i != _views.end()) { @@ -441,6 +449,186 @@ void ListWidget::repaintScrollDateCallback() { update(0, updateTop, width(), updateHeight); } +auto ListWidget::collectSelectedItems() const -> SelectedItems { + auto transformation = [&](const auto &item) { + const auto [itemId, selection] = item; + auto result = SelectedItem(itemId); + result.canDelete = selection.canDelete; + result.canForward = selection.canForward; + return result; + }; + auto items = SelectedItems(); + if (hasSelectedItems()) { + items.reserve(_selected.size()); + std::transform( + _selected.begin(), + _selected.end(), + std::back_inserter(items), + transformation); + } + return items; +} + +MessageIdsList ListWidget::collectSelectedIds() const { + const auto selected = collectSelectedItems(); + return ranges::view::all( + selected + ) | ranges::view::transform([](const SelectedItem &item) { + return item.msgId; + }) | ranges::to_vector; +} + +void ListWidget::pushSelectedItems() { + _delegate->listSelectionChanged(collectSelectedItems()); +} + +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 (_selectedTextItem != nullptr) && !hasSelectedItems(); +} + +bool ListWidget::hasSelectedItems() const { + return !_selected.empty(); +} + +bool ListWidget::applyItemSelection( + SelectedMap &applyTo, + FullMsgId itemId) const { + if (applyTo.size() >= MaxSelectedItems) { + return false; + } + auto [iterator, ok] = applyTo.try_emplace( + itemId, + SelectionData()); + if (!ok) { + return false; + } + const auto item = App::histItemById(itemId); + if (!item) { + applyTo.erase(iterator); + return false; + } + iterator->second.canDelete = item->canDelete(); + iterator->second.canForward = item->allowsForward(); + return true; +} + +void ListWidget::toggleItemSelection(FullMsgId itemId) { + auto it = _selected.find(itemId); + if (it == _selected.cend()) { + if (_selectedTextItem) { + clearTextSelection(); + } + if (applyItemSelection(_selected, itemId)) { + repaintItem(itemId); + pushSelectedItems(); + } + } else { + removeItemSelection(it); + } +} + +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 view) const { + if (_mouseCursorState == HistoryInDateCursorState) { + return true; + } else if (const auto media = view->media()) { + return media->type() == MediaTypeSticker; + } + return false; +} + +bool ListWidget::isPressInSelectedText(HistoryTextState state) const { + if (state.cursor != HistoryInTextCursorState) { + return false; + } + if (!hasSelectedText() + || !_selectedTextItem + || _selectedTextItem->fullId() != _pressState.itemId) { + return false; + } + auto from = _selectedTextRange.from; + auto to = _selectedTextRange.to; + return (state.symbol >= from && state.symbol < to); +} + +void ListWidget::cancelSelection() { + clearSelected(); + clearTextSelection(); +} + +void ListWidget::clearSelected() { + if (_selected.empty()) { + return; + } + if (hasSelectedText()) { + repaintItem(_selected.begin()->first); + _selected.clear(); + } else { + _selected.clear(); + pushSelectedItems(); + update(); + } +} + +void ListWidget::clearTextSelection() { + if (_selectedTextItem) { + if (const auto view = viewForItem(_selectedTextItem)) { + repaintItem(view); + } + _selectedTextItem = nullptr; + _selectedTextRange = TextSelection(); + _selectedText = TextWithEntities(); + } +} + +void ListWidget::setTextSelection( + not_null view, + TextSelection selection) { + clearSelected(); + const auto item = view->data(); + if (_selectedTextItem != item) { + clearTextSelection(); + _selectedTextItem = view->data(); + } + _selectedTextRange = selection; + _selectedText = (selection.from != selection.to) + ? view->selectedText(selection) + : TextWithEntities(); + repaintItem(view); + if (!_wasSelectedText && !_selectedText.text.isEmpty()) { + _wasSelectedText = true; + setFocus(); + } +} + void ListWidget::checkMoveToOtherViewer() { auto visibleHeight = (_visibleBottom - _visibleTop); if (width() <= 0 @@ -504,19 +692,20 @@ void ListWidget::checkMoveToOtherViewer() { } QString ListWidget::tooltipText() const { - if (_mouseCursorState == HistoryInDateCursorState && _mouseAction == MouseAction::None) { - if (const auto view = App::hoveredItem()) { - auto dateText = view->data()->date.toString(QLocale::system().dateTimeFormat(QLocale::LongFormat)); - return dateText; + const auto item = (_overItem && _mouseAction == MouseAction::None) + ? _overItem->data().get() + : nullptr; + if (_mouseCursorState == HistoryInDateCursorState && item) { + return item->date.toString( + QLocale::system().dateTimeFormat(QLocale::LongFormat)); + } else if (_mouseCursorState == HistoryInForwardedCursorState && item) { + if (const auto forwarded = item->Get()) { + return forwarded->text.originalText( + AllTextSelection, + ExpandLinksNone); } - } else if (_mouseCursorState == HistoryInForwardedCursorState && _mouseAction == MouseAction::None) { - if (const auto view = App::hoveredItem()) { - if (const auto forwarded = view->data()->Get()) { - return forwarded->text.originalText(AllTextSelection, ExpandLinksNone); - } - } - } else if (const auto lnk = ClickHandler::getActive()) { - return lnk->tooltip(); + } else if (const auto link = ClickHandler::getActive()) { + return link->tooltip(); } return QString(); } @@ -539,14 +728,17 @@ std::unique_ptr ListWidget::elementCreate( return std::make_unique(this, message); } +bool ListWidget::elementUnderCursor( + not_null view) { + return (_overItem == view); +} + void ListWidget::elementAnimationAutoplayAsync( not_null view) { crl::on_main(this, [this, msgId = view->data()->fullId()]{ - if (const auto item = App::histItemById(msgId)) { - if (const auto view = viewForItem(item)) { - if (const auto media = view->media()) { - media->autoplayAnimation(); - } + if (const auto view = viewForItem(msgId)) { + if (const auto media = view->media()) { + media->autoplayAnimation(); } } }); @@ -620,6 +812,62 @@ void ListWidget::restoreScrollPosition() { _delegate->listScrollTo(newVisibleTop); } +TextSelection ListWidget::computeRenderSelection( + not_null selected, + not_null view) const { + const auto itemSelection = [&](not_null item) { + auto i = selected->find(item->fullId()); + if (i != selected->end()) { + return FullSelection; + } + return TextSelection(); + }; + const auto item = view->data(); + if (const auto group = Auth().data().groups().find(item)) { + if (group->items.back() != item) { + return TextSelection(); + } + auto result = TextSelection(); + auto allFullSelected = true; + const auto count = int(group->items.size()); + for (auto i = 0; i != count; ++i) { + if (itemSelection(group->items[i]) == FullSelection) { + result = AddGroupItemSelection(result, i); + } else { + allFullSelected = false; + } + } + if (allFullSelected) { + return FullSelection; + } + const auto leaderSelection = itemSelection(item); + if (leaderSelection != FullSelection + && leaderSelection != TextSelection()) { + return leaderSelection; + } + return result; + } + return itemSelection(item); +} + +TextSelection ListWidget::itemRenderSelection( + not_null view) const { + if (_dragSelectAction != DragSelectAction::None) { + const auto i = _dragSelected.find(view->data()->fullId()); + if (i != _dragSelected.end()) { + return (_dragSelectAction == DragSelectAction::Selecting) + ? FullSelection + : TextSelection(); + } + } + if (!_selected.empty() || !_dragSelected.empty()) { + return computeRenderSelection(&_selected, view); + } else if (view->data() == _selectedTextItem) { + return _selectedTextRange; + } + return TextSelection(); +} + void ListWidget::paintEvent(QPaintEvent *e) { if (Ui::skipPaintEvent(this, e)) { return; @@ -641,10 +889,11 @@ void ListWidget::paintEvent(QPaintEvent *e) { p.translate(0, top); for (auto i = from; i != to; ++i) { const auto view = *i; - const auto selection = (view == _selectedItem) - ? _selectedText - : TextSelection(); - view->draw(p, clip.translated(0, -top), selection, ms); + view->draw( + p, + clip.translated(0, -top), + itemRenderSelection(view), + ms); const auto height = view->height(); top += height; p.translate(0, height); @@ -716,10 +965,109 @@ void ListWidget::paintEvent(QPaintEvent *e) { } } +void ListWidget::applyDragSelection() { + applyDragSelection(_selected); + clearDragSelection(); + pushSelectedItems(); +} + +void ListWidget::applyDragSelection(SelectedMap &applyTo) const { + if (_dragSelectAction == DragSelectAction::Selecting) { + for (const auto itemId : _dragSelected) { + applyItemSelection(applyTo, itemId); + } + } else if (_dragSelectAction == DragSelectAction::Deselecting) { + for (const auto itemId : _dragSelected) { + applyTo.remove(itemId); + } + } +} + TextWithEntities ListWidget::getSelectedText() const { - return _selectedItem - ? _selectedItem->selectedText(_selectedText) - : TextWithEntities(); + auto selected = _selected; + + if (_mouseAction == MouseAction::Selecting && !_dragSelected.empty()) { + applyDragSelection(selected); + } + + if (selected.empty()) { + if (const auto view = viewForItem(_selectedTextItem)) { + return view->selectedText(_selectedTextRange); + } + return _selectedText; + } + + // #TODO selection + //const auto timeFormat = qsl(", [dd.MM.yy hh:mm]\n"); + //auto groupLeadersAdded = base::flat_set>(); + //auto fullSize = 0; + //auto texts = base::flat_map, TextWithEntities>(); + + //const auto addItem = [&]( + // not_null view, + // TextSelection selection) { + // const auto item = view->data(); + // auto time = item->date.toString(timeFormat); + // auto part = TextWithEntities(); + // auto unwrapped = view->selectedText(selection); + // auto size = item->author()->name.size() + // + time.size() + // + unwrapped.text.size(); + // part.text.reserve(size); + + // auto y = itemTop(view); + // if (y >= 0) { + // part.text.append(item->author()->name).append(time); + // TextUtilities::Append(part, std::move(unwrapped)); + // texts.emplace(std::make_pair(y, item->id), part); + // fullSize += size; + // } + //}; + + //for (const auto [item, selection] : selected) { + // const auto view = item->mainView(); + // if (!view) { + // continue; + // } + + // if (const auto group = Auth().data().groups().find(item)) { + // // #TODO group copy + // //if (groupLeadersAdded.contains(group->leader)) { + // // continue; + // //} + // //const auto leaderSelection = computeRenderSelection( + // // &selected, + // // group->leader); + // //if (leaderSelection == FullSelection) { + // // groupLeadersAdded.emplace(group->leader); + // // addItem(group->leader, FullSelection); + // //} else if (view == group->leader) { + // // const auto leaderFullSelection = AddGroupItemSelection( + // // TextSelection(), + // // int(group->others.size())); + // // addItem(view, leaderFullSelection); + // //} else { + // // addItem(view, FullSelection); + // //} + // } else { + // addItem(view, FullSelection); + // } + //} + + auto result = TextWithEntities(); + //auto sep = qsl("\n\n"); + //result.text.reserve(fullSize + (texts.size() - 1) * sep.size()); + //for (auto i = texts.begin(), e = texts.end(); i != e;) { + // TextUtilities::Append(result, std::move(i->second)); + // if (++i != e) { + // result.text.append(sep); + // } + //} + return result; +} + +MessageIdsList ListWidget::getSelectedItems() const { + return collectSelectedIds(); } not_null ListWidget::findItemByY(int y) const { @@ -761,7 +1109,8 @@ auto ListWidget::countScrollState() const -> ScrollTopState { void ListWidget::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Escape || e->key() == Qt::Key_Back) { _delegate->listCloseRequest(); - } else if (e == QKeySequence::Copy && _selectedItem != nullptr) { + } else if (e == QKeySequence::Copy + && (hasSelectedText() || hasSelectedItems())) { SetClipboardWithEntities(getSelectedText()); #ifdef Q_OS_MAC } else if (e->key() == Qt::Key_E @@ -775,23 +1124,51 @@ void ListWidget::keyPressEvent(QKeyEvent *e) { void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) { mouseActionStart(e->globalPos(), e->button()); - if (((_mouseAction == MouseAction::Selecting && _selectedItem != nullptr) || (_mouseAction == MouseAction::None)) && _mouseSelectType == TextSelectType::Letters && _mouseActionItem) { - HistoryStateRequest request; - request.flags |= Text::StateRequest::Flag::LookupSymbol; - auto dragState = _mouseActionItem->getState(_dragStartPosition, request); - if (dragState.cursor == HistoryInTextCursorState) { - _mouseTextSymbol = dragState.symbol; - _mouseSelectType = TextSelectType::Words; - if (_mouseAction == MouseAction::None) { - _mouseAction = MouseAction::Selecting; - auto selection = TextSelection { dragState.symbol, dragState.symbol }; - repaintItem(std::exchange(_selectedItem, _mouseActionItem)); - _selectedText = selection; - } - mouseMoveEvent(e); + trySwitchToWordSelection(); +} - _trippleClickPoint = e->globalPos(); - _trippleClickTimer.callOnce(QApplication::doubleClickInterval()); +void ListWidget::trySwitchToWordSelection() { + auto selectingSome = (_mouseAction == MouseAction::Selecting) + && hasSelectedText(); + auto willSelectSome = (_mouseAction == MouseAction::None) + && !hasSelectedItems(); + auto checkSwitchToWordSelection = _overItem + && (_mouseSelectType == TextSelectType::Letters) + && (selectingSome || willSelectSome); + if (checkSwitchToWordSelection) { + switchToWordSelection(); + } +} + +void ListWidget::switchToWordSelection() { + Expects(_overItem != nullptr); + + HistoryStateRequest request; + request.flags |= Text::StateRequest::Flag::LookupSymbol; + auto dragState = _overItem->getState(_pressState.cursor, request); + if (dragState.cursor != HistoryInTextCursorState) { + return; + } + _mouseTextSymbol = dragState.symbol; + _mouseSelectType = TextSelectType::Words; + if (_mouseAction == MouseAction::None) { + _mouseAction = MouseAction::Selecting; + setTextSelection(_overItem, TextSelection( + dragState.symbol, + dragState.symbol + )); + } + mouseActionUpdate(); + + _trippleClickPoint = _mousePosition; + _trippleClickStartTime = getms(); +} + +void ListWidget::validateTrippleClickStartTime() { + if (_trippleClickStartTime) { + const auto elapsed = (getms() - _trippleClickStartTime); + if (elapsed >= QApplication::doubleClickInterval()) { + _trippleClickStartTime = 0; } } } @@ -807,25 +1184,34 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { ContextMenuRequest request; request.link = ClickHandler::getActive(); - request.view = App::mousedItem(); - request.overView = (request.view == App::hoveredItem()); - request.selectedText = getSelectedText(); - if (_selectedItem - && _selectedItem == request.view + request.view = _overItem; + request.overView = _overItem && _overState.inside; + request.selectedText = _selectedText; + const auto itemId = request.view + ? request.view->data()->fullId() + : FullMsgId(); + if (!_selected.empty()) { + request.selectedItems = collectSelectedIds(); + if (request.overView && _selected.find(itemId) != end(_selected)) { + request.overSelection = true; + } + } else if (_selectedTextItem + && request.view + && _selectedTextItem == request.view->data() && request.overView) { - const auto mousePos = mapPointToItem( + const auto pointInItem = mapPointToItem( mapFromGlobal(_mousePosition), - _selectedItem); + request.view); HistoryStateRequest stateRequest; stateRequest.flags |= Text::StateRequest::Flag::LookupSymbol; - const auto dragState = _selectedItem->getState( - mousePos, + const auto dragState = request.view->getState( + pointInItem, stateRequest); if (dragState.cursor == HistoryInTextCursorState && base::in_range( dragState.symbol, - _selectedText.from, - _selectedText.to)) { + _selectedTextRange.from, + _selectedTextRange.to)) { request.overSelection = true; } } @@ -842,15 +1228,6 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } } -TextWithEntities ListWidget::getItemText(FullMsgId itemId) const { - if (const auto item = App::histItemById(itemId)) { - if (const auto view = viewForItem(item)) { - return view->selectedText(FullSelection); - } - } - return TextWithEntities(); -} - void ListWidget::mousePressEvent(QMouseEvent *e) { if (_menu) { e->accept(); @@ -880,9 +1257,11 @@ void ListWidget::enterEventHook(QEvent *e) { } void ListWidget::leaveEventHook(QEvent *e) { - if (const auto view = App::hoveredItem()) { - repaintItem(view); - App::hoveredItem(nullptr); + if (const auto view = _overItem) { + if (_overState.inside) { + repaintItem(view); + _overState.inside = false; + } } ClickHandler::clearActive(); Ui::Tooltip::Hide(); @@ -893,103 +1272,195 @@ void ListWidget::leaveEventHook(QEvent *e) { return TWidget::leaveEventHook(e); } -void ListWidget::mouseActionStart(const QPoint &screenPos, Qt::MouseButton button) { - mouseActionUpdate(screenPos); - if (button != Qt::LeftButton) return; - - ClickHandler::pressed(); - if (App::pressedItem() != App::hoveredItem()) { - repaintItem(App::pressedItem()); - App::pressedItem(App::hoveredItem()); - repaintItem(App::pressedItem()); +void ListWidget::updateDragSelection() { + if (!_overState.itemId || !_pressState.itemId) { + clearDragSelection(); + return; + } else if (_items.empty() || !_overItem || !_selectEnabled) { + return; + } + const auto pressItem = App::histItemById(_pressState.itemId); + if (!pressItem) { + return; } + const auto overView = _overItem; + const auto pressView = viewForItem(pressItem); + const auto selectingUp = _delegate->listIsLessInOrder( + overView->data(), + pressItem); + const auto fromView = selectingUp ? overView : pressView; + const auto tillView = selectingUp ? pressView : overView; + // #TODO skip-from / skip-till + const auto from = [&] { + if (fromView) { + const auto result = ranges::find( + _items, + fromView, + [](auto view) { return view.get(); }); + return (result == end(_items)) ? begin(_items) : result; + } + return begin(_items); + }(); + const auto till = tillView + ? ranges::find( + _items, + tillView, + [](auto view) { return view.get(); }) + : end(_items); + Assert(from <= till); + for (auto i = begin(_items); i != from; ++i) { + _dragSelected.remove((*i)->data()->fullId()); + } + for (auto i = from; i != till; ++i) { + _dragSelected.emplace((*i)->data()->fullId()); + } + for (auto i = till; i != end(_items); ++i) { + _dragSelected.remove((*i)->data()->fullId()); + } + _dragSelectAction = [&] { + if (_dragSelected.empty()) { + return DragSelectAction::None; + } else if (!pressView) { + return _dragSelectAction; + } + if (_selected.find(pressItem->fullId()) != end(_selected)) { + 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(_overState.itemId); + } + const auto pressedItem = _overItem; + _mouseAction = MouseAction::None; - _mouseActionItem = App::mousedItem(); - _dragStartPosition = mapPointToItem(mapFromGlobal(screenPos), _mouseActionItem); _pressWasInactive = _controller->window()->wasInactivePress(); if (_pressWasInactive) _controller->window()->setInactivePress(false); if (ClickHandler::getPressed()) { _mouseAction = MouseAction::PrepareDrag; } - if (_mouseAction == MouseAction::None && _mouseActionItem) { + if (_mouseAction == MouseAction::None && pressedItem) { + validateTrippleClickStartTime(); HistoryTextState dragState; - if (_trippleClickTimer.isActive() && (screenPos - _trippleClickPoint).manhattanLength() < QApplication::startDragDistance()) { + auto startDistance = (globalPosition - _trippleClickPoint).manhattanLength(); + auto validStartPoint = startDistance < QApplication::startDragDistance(); + if (_trippleClickStartTime != 0 && validStartPoint) { HistoryStateRequest request; request.flags = Text::StateRequest::Flag::LookupSymbol; - dragState = _mouseActionItem->getState(_dragStartPosition, request); + dragState = pressedItem->getState(_pressState.cursor, request); if (dragState.cursor == HistoryInTextCursorState) { - auto selection = TextSelection { dragState.symbol, dragState.symbol }; - repaintItem(std::exchange(_selectedItem, _mouseActionItem)); - _selectedText = selection; + setTextSelection(pressedItem, TextSelection( + dragState.symbol, + dragState.symbol + )); _mouseTextSymbol = dragState.symbol; _mouseAction = MouseAction::Selecting; _mouseSelectType = TextSelectType::Paragraphs; - mouseActionUpdate(_mousePosition); - _trippleClickTimer.callOnce(QApplication::doubleClickInterval()); + mouseActionUpdate(); + _trippleClickStartTime = getms(); } - } else if (App::pressedItem()) { + } else if (pressedItem) { HistoryStateRequest request; request.flags = Text::StateRequest::Flag::LookupSymbol; - dragState = _mouseActionItem->getState(_dragStartPosition, request); + dragState = pressedItem->getState(_pressState.cursor, request); } if (_mouseSelectType != TextSelectType::Paragraphs) { - if (App::pressedItem()) { - _mouseTextSymbol = dragState.symbol; - auto uponSelected = (dragState.cursor == HistoryInTextCursorState); - if (uponSelected) { - if (!_selectedItem || _selectedItem != _mouseActionItem) { - uponSelected = false; - } else if (_mouseTextSymbol < _selectedText.from || _mouseTextSymbol >= _selectedText.to) { - uponSelected = false; - } - } - if (uponSelected) { - _mouseAction = MouseAction::PrepareDrag; // start text drag - } else if (!_pressWasInactive) { + _mouseTextSymbol = dragState.symbol; + if (isPressInSelectedText(dragState)) { + _mouseAction = MouseAction::PrepareDrag; // start text drag + } else if (!_pressWasInactive) { + if (requiredToStartDragging(pressedItem)) { + _mouseAction = MouseAction::PrepareDrag; + } else { if (dragState.afterSymbol) ++_mouseTextSymbol; - auto selection = TextSelection { _mouseTextSymbol, _mouseTextSymbol }; - repaintItem(std::exchange(_selectedItem, _mouseActionItem)); - _selectedText = selection; - _mouseAction = MouseAction::Selecting; - repaintItem(_mouseActionItem); + ; + if (!hasSelectedItems()) { + setTextSelection(pressedItem, TextSelection( + _mouseTextSymbol, + _mouseTextSymbol)); + _mouseAction = MouseAction::Selecting; + } else { + _mouseAction = MouseAction::PrepareSelect; + } } } } } - - if (!_mouseActionItem) { + if (!pressedItem) { _mouseAction = MouseAction::None; } else if (_mouseAction == MouseAction::None) { - _mouseActionItem = nullptr; + mouseActionCancel(); } } -void ListWidget::mouseActionUpdate(const QPoint &screenPos) { - _mousePosition = screenPos; - updateSelected(); +void ListWidget::mouseActionUpdate(const QPoint &globalPosition) { + _mousePosition = globalPosition; + mouseActionUpdate(); } void ListWidget::mouseActionCancel() { - _mouseActionItem = nullptr; + _pressState = CursorState(); _mouseAction = MouseAction::None; - _dragStartPosition = QPoint(0, 0); + clearDragSelection(); _wasSelectedText = false; //_widget->noSelectingScroll(); // #TODO select scroll } -void ListWidget::mouseActionFinish(const QPoint &screenPos, Qt::MouseButton button) { - mouseActionUpdate(screenPos); +void ListWidget::mouseActionFinish( + const QPoint &globalPosition, + Qt::MouseButton button) { + mouseActionUpdate(globalPosition); auto activated = ClickHandler::unpressed(); if (_mouseAction == MouseAction::Dragging) { activated = nullptr; } - if (const auto view = App::pressedItem()) { - repaintItem(view); - App::pressedItem(nullptr); - } + 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 needItemSelectionToggle = simpleSelectionChange + && hasSelectedItems(); + auto needTextSelectionClear = simpleSelectionChange + && hasSelectedText(); _wasSelectedText = false; @@ -998,93 +1469,107 @@ void ListWidget::mouseActionFinish(const QPoint &screenPos, Qt::MouseButton butt App::activateClickHandler(activated, button); return; } - if (_mouseAction == MouseAction::PrepareDrag && !_pressWasInactive && button != Qt::RightButton) { - repaintItem(base::take(_selectedItem)); + if (needItemSelectionToggle) { + toggleItemSelection(pressState.itemId); + } else if (needTextSelectionClear) { + clearTextSelection(); } else if (_mouseAction == MouseAction::Selecting) { - if (_selectedItem && !_pressWasInactive) { - if (_selectedText.from == _selectedText.to) { - _selectedItem = nullptr; + if (!_dragSelected.empty()) { + applyDragSelection(); + } else if (_selectedTextItem && !_pressWasInactive) { + if (_selectedTextRange.from == _selectedTextRange.to) { + clearTextSelection(); App::wnd()->setInnerFocus(); } } } _mouseAction = MouseAction::None; - _mouseActionItem = nullptr; _mouseSelectType = TextSelectType::Letters; //_widget->noSelectingScroll(); // #TODO select scroll #if defined Q_OS_LINUX32 || defined Q_OS_LINUX64 - if (_selectedItem && _selectedText.from != _selectedText.to) { - SetClipboardWithEntities( - _selectedItem->selectedText(_selectedText), - QClipboard::Selection); + if (_selectedTextItem + && _selectedTextRange.from != _selectedTextRange.to) { + if (const auto view = viewForItem(_selectedTextItem)) { + SetClipboardWithEntities( + _selectedTextItem->selectedText(_selectedTextRange), + QClipboard::Selection); +} } #endif // Q_OS_LINUX32 || Q_OS_LINUX64 } -void ListWidget::updateSelected() { +void ListWidget::mouseActionUpdate() { auto mousePosition = mapFromGlobal(_mousePosition); auto point = QPoint(snap(mousePosition.x(), 0, width()), snap(mousePosition.y(), _visibleTop, _visibleBottom)); - auto itemPoint = QPoint(); const auto view = strictFindItemByY(point.y()); const auto item = view ? view->data().get() : nullptr; - if (view) { - App::mousedItem(view); - itemPoint = mapPointToItem(point, view); - if (view->hasPoint(itemPoint)) { - if (App::hoveredItem() != view) { - repaintItem(App::hoveredItem()); - App::hoveredItem(view); - repaintItem(view); - } - } else if (const auto view = App::hoveredItem()) { - repaintItem(view); - App::hoveredItem(nullptr); - } + const auto itemPoint = mapPointToItem(point, view); + _overState = CursorState{ + item ? item->fullId() : FullMsgId(), + view ? view->height() : 0, + itemPoint, + view ? view->hasPoint(itemPoint) : false + }; + if (_overItem != view) { + repaintItem(_overItem); + _overItem = view; + repaintItem(_overItem); } HistoryTextState dragState; ClickHandlerHost *lnkhost = nullptr; - auto selectingText = _selectedItem - && (view == _mouseActionItem) - && (view == App::hoveredItem()); + auto inTextSelection = _overState.inside + && (_overState.itemId == _pressState.itemId) + && hasSelectedText(); if (view) { - if (view != _mouseActionItem || (itemPoint - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) { + 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; } } HistoryStateRequest request; if (_mouseAction == MouseAction::Selecting) { request.flags |= Text::StateRequest::Flag::LookupSymbol; } else { - selectingText = false; + inTextSelection = false; } + // #TODO enumerate dates like HistoryInner dragState = view->getState(itemPoint, request); lnkhost = view; - if (!dragState.link && itemPoint.x() >= st::historyPhotoLeft && itemPoint.x() < st::historyPhotoLeft + st::msgPhotoSize) { - if (auto message = item->toHistoryMessage()) { - if (view->hasFromPhoto()) { - enumerateUserpics([&](not_null view, int userpicTop) -> bool { - // stop enumeration if the userpic is below our point - if (userpicTop > point.y()) { - return false; - } + if (!dragState.link + && itemPoint.x() >= st::historyPhotoLeft + && itemPoint.x() < st::historyPhotoLeft + st::msgPhotoSize) { + if (view->hasFromPhoto()) { + enumerateUserpics([&](not_null view, int userpicTop) { + // stop enumeration if the userpic is below our point + if (userpicTop > point.y()) { + return false; + } - // stop enumeration if we've found a userpic under the cursor - if (point.y() >= userpicTop && point.y() < userpicTop + st::msgPhotoSize) { - const auto message = view->data()->toHistoryMessage(); - Assert(message != nullptr); + // stop enumeration if we've found a userpic under the cursor + if (point.y() >= userpicTop && point.y() < userpicTop + st::msgPhotoSize) { + const auto message = view->data()->toHistoryMessage(); + Assert(message != nullptr); - dragState.link = message->from()->openLink(); - lnkhost = view; - return false; - } - return true; - }); - } + dragState.link = message->from()->openLink(); + lnkhost = view; + return false; + } + return true; + }); } } } @@ -1092,54 +1577,53 @@ void ListWidget::updateSelected() { if (lnkChanged || dragState.cursor != _mouseCursorState) { Ui::Tooltip::Hide(); } - if (dragState.link || dragState.cursor == HistoryInDateCursorState || dragState.cursor == HistoryInForwardedCursorState) { + if (dragState.link + || dragState.cursor == HistoryInDateCursorState + || dragState.cursor == HistoryInForwardedCursorState) { Ui::Tooltip::Show(1000, this); } auto cursor = style::cur_default; if (_mouseAction == MouseAction::None) { _mouseCursorState = dragState.cursor; - if (dragState.link) { - cursor = style::cur_pointer; - } else if (_mouseCursorState == HistoryInTextCursorState) { - cursor = style::cur_text; - } else if (_mouseCursorState == HistoryInDateCursorState) { -// cursor = style::cur_cross; + auto cursor = computeMouseCursor(); + if (_cursor != cursor) { + setCursor((_cursor = cursor)); } } else if (view) { if (_mouseAction == MouseAction::Selecting) { - if (selectingText) { + if (inTextSelection) { auto second = dragState.symbol; - if (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) { + if (dragState.afterSymbol + && _mouseSelectType == TextSelectType::Letters) { ++second; } - auto selection = TextSelection { qMin(second, _mouseTextSymbol), qMax(second, _mouseTextSymbol) }; + auto selection = TextSelection( + qMin(second, _mouseTextSymbol), + qMax(second, _mouseTextSymbol) + ); if (_mouseSelectType != TextSelectType::Letters) { - selection = _mouseActionItem->adjustSelection(selection, _mouseSelectType); - } - if (_selectedText != selection) { - _selectedText = selection; - repaintItem(_mouseActionItem); - } - if (!_wasSelectedText && (selection.from != selection.to)) { - _wasSelectedText = true; - setFocus(); + selection = view->adjustSelection( + selection, + _mouseSelectType); } + setTextSelection(view, selection); + clearDragSelection(); + } else if (_pressState.itemId) { + updateDragSelection(); } } else if (_mouseAction == MouseAction::Dragging) { } - - if (ClickHandler::getPressed()) { - cursor = style::cur_pointer; - } else if (_mouseAction == MouseAction::Selecting && _selectedItem) { - cursor = style::cur_text; - } } // Voice message seek support. - if (const auto pressedView = App::pressedLinkItem()) { - auto adjustedPoint = mapPointToItem(point, pressedView); - pressedView->updatePressed(adjustedPoint); + if (_pressState.inside && ClickHandler::getPressed()) { + if (const auto item = App::histItemById(_pressState.itemId)) { + if (const auto view = viewForItem(item)) { + auto adjustedPoint = mapPointToItem(point, view); + view->updatePressed(adjustedPoint); + } + } } //if (_mouseAction == MouseAction::Selecting) { @@ -1147,10 +1631,16 @@ void ListWidget::updateSelected() { //} else { // _widget->noSelectingScroll(); //} // #TODO select scroll +} - if (_mouseAction == MouseAction::None && (lnkChanged || cursor != _cursor)) { - setCursor(_cursor = cursor); +style::cursor ListWidget::computeMouseCursor() const { + if (ClickHandler::getPressed() || ClickHandler::getActive()) { + return style::cur_pointer; + } else if (!hasSelectedItems() + && (_mouseCursorState == HistoryInTextCursorState)) { + return style::cur_text; } + return style::cur_default; } void ListWidget::performDrag() { @@ -1213,7 +1703,7 @@ void ListWidget::performDrag() { //} else { // auto forwardMimeType = QString(); // auto pressedMedia = static_cast(nullptr); - // if (auto pressedItem = App::pressedItem()) { + // if (auto pressedItem = App::pressedItem()) { // #TODO no App:: // pressedMedia = pressedItem->media(); // if (_mouseCursorState == HistoryInDateCursorState // || (pressedMedia && pressedMedia->dragItem())) { @@ -1222,7 +1712,7 @@ void ListWidget::performDrag() { // forwardMimeType = qsl("application/x-td-forward"); // } // } - // if (auto pressedLnkItem = App::pressedLinkItem()) { + // if (auto pressedLnkItem = App::pressedLinkItem()) { // #TODO no App:: // if ((pressedMedia = pressedLnkItem->media())) { // if (forwardMimeType.isEmpty() // && pressedMedia->dragItemByHandler(pressedHandler)) { @@ -1262,6 +1752,12 @@ void ListWidget::repaintItem(const Element *view) { update(0, itemTop(view), width(), view->height()); } +void ListWidget::repaintItem(FullMsgId itemId) { + if (const auto view = viewForItem(itemId)) { + repaintItem(view); + } +} + void ListWidget::resizeItem(not_null view) { const auto index = ranges::find(_items, view) - begin(_items); if (index < int(_items.size())) { @@ -1339,11 +1835,13 @@ void ListWidget::refreshItem(not_null view) { void ListWidget::viewReplaced(not_null was, Element *now) { if (_visibleTopItem == was) _visibleTopItem = now; if (_scrollDateLastItem == was) _scrollDateLastItem = now; - if (_mouseActionItem == was) _mouseActionItem = now; - if (_selectedItem == was) _selectedItem = now; + if (_overItem == was) _overItem = now; } void ListWidget::itemRemoved(not_null item) { + if (_selectedTextItem == item) { + clearTextSelection(); + } const auto i = _views.find(item); if (i == end(_views)) { return; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 73885920ae..8b06839f82 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -27,6 +27,18 @@ namespace HistoryView { enum class Context : char; +struct SelectedItem { + explicit SelectedItem(FullMsgId msgId) : msgId(msgId) { + } + + FullMsgId msgId; + bool canDelete = false; + bool canForward = false; + +}; + +using SelectedItems = std::vector; + class ListDelegate { public: virtual Context listContext() = 0; @@ -36,9 +48,25 @@ public: Data::MessagePosition aroundId, int limitBefore, int limitAfter) = 0; + virtual bool listAllowsMultiSelect() = 0; + virtual bool listIsLessInOrder( + not_null first, + not_null second) = 0; + virtual void listSelectionChanged(SelectedItems &&items) = 0; }; +struct SelectionData { + bool canDelete = false; + bool canForward = false; + +}; + +using SelectedMap = base::flat_map< + FullMsgId, + SelectionData, + std::less<>>; + class ListMemento { public: struct ScrollTopState { @@ -100,7 +128,8 @@ public: void restoreState(not_null memento); TextWithEntities getSelectedText() const; - TextWithEntities getItemText(FullMsgId itemId) const; + MessageIdsList getSelectedItems() const; + void cancelSelection(); // AbstractTooltipShower interface QString tooltipText() const override; @@ -112,6 +141,7 @@ public: not_null message) override; std::unique_ptr elementCreate( not_null message) override; + bool elementUnderCursor(not_null view) override; void elementAnimationAutoplayAsync( not_null view) override; @@ -136,6 +166,21 @@ protected: int resizeGetHeight(int newWidth) override; private: + struct CursorState { + FullMsgId itemId; + int height = 0; + QPoint cursor; + bool inside = false; + + inline bool operator==(const CursorState &other) const { + return (itemId == other.itemId) + && (cursor == other.cursor); + } + inline bool operator!=(const CursorState &other) const { + return !(*this == other); + } + + }; enum class Direction { Up, Down, @@ -144,12 +189,18 @@ private: None, PrepareDrag, Dragging, + PrepareSelect, Selecting, }; enum class EnumItemsDirection { TopToBottom, BottomToTop, }; + enum class DragSelectAction { + None, + Selecting, + Deselecting, + }; using ScrollTopState = ListMemento::ScrollTopState; void refreshViewer(); @@ -159,16 +210,23 @@ private: void saveScrollState(); void restoreScrollState(); + Element *viewForItem(FullMsgId itemId) const; Element *viewForItem(const HistoryItem *item) const; not_null enforceViewForItem(not_null item); - void mouseActionStart(const QPoint &screenPos, Qt::MouseButton button); - void mouseActionUpdate(const QPoint &screenPos); - void mouseActionFinish(const QPoint &screenPos, Qt::MouseButton button); + void mouseActionStart( + const QPoint &globalPosition, + Qt::MouseButton button); + void mouseActionUpdate(const QPoint &globalPosition); + void mouseActionUpdate(); + void mouseActionFinish( + const QPoint &globalPosition, + Qt::MouseButton button); void mouseActionCancel(); - void updateSelected(); void performDrag(); + style::cursor computeMouseCursor() const; int itemTop(not_null view) const; + void repaintItem(FullMsgId itemId); void repaintItem(const Element *view); void resizeItem(not_null view); void refreshItem(not_null view); @@ -176,7 +234,6 @@ private: QPoint mapPointToItem(QPoint point, const Element *view) const; void showContextMenu(QContextMenuEvent *e, bool showFromTouch = false); - void showStickerPackInfo(not_null document); not_null findItemByY(int y) const; Element *strictFindItemByY(int y) const; @@ -197,6 +254,38 @@ private: void scrollDateCheck(); void scrollDateHideByTimer(); + void trySwitchToWordSelection(); + void switchToWordSelection(); + void validateTrippleClickStartTime(); + SelectedItems collectSelectedItems() const; + MessageIdsList collectSelectedIds() const; + void pushSelectedItems(); + void removeItemSelection( + const SelectedMap::const_iterator &i); + bool hasSelectedText() const; + bool hasSelectedItems() const; + void clearTextSelection(); + void clearSelected(); + void setTextSelection( + not_null view, + TextSelection selection); + bool applyItemSelection(SelectedMap &applyTo, FullMsgId itemId) const; + void toggleItemSelection(FullMsgId itemId); + SelectedMap::iterator itemUnderPressSelection(); + SelectedMap::const_iterator itemUnderPressSelection() const; + bool isItemUnderPressSelected() const; + bool requiredToStartDragging(not_null view) const; + bool isPressInSelectedText(HistoryTextState state) const; + void updateDragSelection(); + void clearDragSelection(); + void applyDragSelection(); + void applyDragSelection(SelectedMap &applyTo) const; + TextSelection itemRenderSelection( + not_null view) const; + TextSelection computeRenderSelection( + not_null selected, + not_null view) const; + // This function finds all history items that are displayed and calls template method // for each found message (in given direction) in the passed history with passed top offset. // @@ -231,7 +320,10 @@ private: int _idsLimit = kMinimalIdsLimit; Data::MessagesSlice _slice; std::vector> _items; - std::map, std::unique_ptr, std::less<>> _views; + std::map< + not_null, + std::unique_ptr, + std::less<>> _views; int _itemsTop = 0; int _itemsWidth = 0; int _itemsHeight = 0; @@ -252,22 +344,29 @@ private: MouseAction _mouseAction = MouseAction::None; TextSelectType _mouseSelectType = TextSelectType::Letters; - QPoint _dragStartPosition; QPoint _mousePosition; - Element *_mouseActionItem = nullptr; + CursorState _overState; + CursorState _pressState; + Element *_overItem = nullptr; HistoryCursorState _mouseCursorState = HistoryDefaultCursorState; uint16 _mouseTextSymbol = 0; bool _pressWasInactive = false; - Element *_selectedItem = nullptr; - TextSelection _selectedText; - bool _wasSelectedText = false; // was some text selected in current drag action + bool _selectEnabled = false; + HistoryItem *_selectedTextItem = nullptr; + TextSelection _selectedTextRange; + TextWithEntities _selectedText; + SelectedMap _selected; + base::flat_set _dragSelected; + DragSelectAction _dragSelectAction = DragSelectAction::None; + // Was some text selected in current drag action. + bool _wasSelectedText = false; Qt::CursorShape _cursor = style::cur_default; base::unique_qptr _menu; QPoint _trippleClickPoint; - base::Timer _trippleClickTimer; + TimeMs _trippleClickStartTime = 0; rpl::lifetime _viewerLifetime; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index cc6b02453a..ceaf9862dd 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -970,13 +970,8 @@ TextWithEntities Message::selectedText(TextSelection selection) const { const auto media = this->media(); TextWithEntities logEntryOriginalResult; - const auto textSelection = (selection == FullSelection) - ? AllTextSelection - : IsSubGroupSelection(selection) - ? TextSelection(0, 0) - : selection; auto textResult = item->_text.originalTextWithEntities( - textSelection, + selection, ExpandLinksAll); auto skipped = skipTextSelection(selection); auto mediaDisplayed = (media && media->isDisplayed()); @@ -1002,28 +997,6 @@ TextWithEntities Message::selectedText(TextSelection selection) const { result.text += qstr("\n\n"); TextUtilities::Append(result, std::move(logEntryOriginalResult)); } - if (auto reply = item->Get()) { - if (selection == FullSelection && reply->replyToMsg) { - TextWithEntities wrapped; - wrapped.text.reserve(lang(lng_in_reply_to).size() + reply->replyToMsg->author()->name.size() + 4 + result.text.size()); - wrapped.text.append('[').append(lang(lng_in_reply_to)).append(' ').append(reply->replyToMsg->author()->name).append(qsl("]\n")); - TextUtilities::Append(wrapped, std::move(result)); - result = wrapped; - } - } - if (auto forwarded = item->Get()) { - if (selection == FullSelection) { - auto fwdinfo = forwarded->text.originalTextWithEntities(AllTextSelection, ExpandLinksAll); - auto wrapped = TextWithEntities(); - wrapped.text.reserve(fwdinfo.text.size() + 4 + result.text.size()); - wrapped.entities.reserve(fwdinfo.entities.size() + result.entities.size()); - wrapped.text.append('['); - TextUtilities::Append(wrapped, std::move(fwdinfo)); - wrapped.text.append(qsl("]\n")); - TextUtilities::Append(wrapped, std::move(result)); - result = wrapped; - } - } return result; } diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index bbde21feac..6060aee7f7 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -508,8 +508,7 @@ void Service::updatePressed(QPoint point) { } TextWithEntities Service::selectedText(TextSelection selection) const { - return message()->_text.originalTextWithEntities( - (selection == FullSelection) ? AllTextSelection : selection); + return message()->_text.originalTextWithEntities(selection); } TextSelection Service::adjustSelection( diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 7a418d8318..805caf7cd5 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -43,7 +43,7 @@ TopBarWidget::TopBarWidget( not_null controller) : RpWidget(parent) , _controller(controller) -, _clearSelection(this, langFactory(lng_selected_clear), st::topBarClearButton) +, _clear(this, langFactory(lng_selected_clear), st::topBarClearButton) , _forward(this, langFactory(lng_selected_forward), st::defaultActiveButton) , _delete(this, langFactory(lng_selected_delete), st::defaultActiveButton) , _back(this, st::historyTopBarBack) @@ -55,11 +55,11 @@ TopBarWidget::TopBarWidget( subscribe(Lang::Current().updated(), [this] { refreshLang(); }); setAttribute(Qt::WA_OpaquePaintEvent); - _forward->setClickedCallback([this] { onForwardSelection(); }); + _forward->setClickedCallback([this] { _forwardSelection.fire({}); }); _forward->setWidthChangedCallback([this] { updateControlsGeometry(); }); - _delete->setClickedCallback([this] { onDeleteSelection(); }); + _delete->setClickedCallback([this] { _deleteSelection.fire({}); }); _delete->setWidthChangedCallback([this] { updateControlsGeometry(); }); - _clearSelection->setClickedCallback([this] { onClearSelection(); }); + _clear->setClickedCallback([this] { _clearSelection.fire({}); }); _call->setClickedCallback([this] { onCall(); }); _search->setClickedCallback([this] { onSearch(); }); _menuToggle->setClickedCallback([this] { showMenu(); }); @@ -132,18 +132,6 @@ void TopBarWidget::refreshLang() { InvokeQueued(this, [this] { updateControlsGeometry(); }); } -void TopBarWidget::onForwardSelection() { - if (App::main()) App::main()->forwardSelectedItems(); -} - -void TopBarWidget::onDeleteSelection() { - if (App::main()) App::main()->confirmDeleteSelectedItems(); -} - -void TopBarWidget::onClearSelection() { - if (App::main()) App::main()->clearSelectedItems(); -} - void TopBarWidget::onSearch() { if (_activeChat) { App::main()->searchInChat(_activeChat); @@ -399,7 +387,7 @@ void TopBarWidget::updateControlsGeometry() { auto selectedButtonsTop = countSelectedButtonsTop(_selectedShown.current(hasSelected ? 1. : 0.)); auto otherButtonsTop = selectedButtonsTop + st::topBarHeight; auto buttonsLeft = st::topBarActionSkip + (Adaptive::OneColumn() ? 0 : st::lineWidth); - auto buttonsWidth = _forward->contentWidth() + _delete->contentWidth() + _clearSelection->width(); + auto buttonsWidth = _forward->contentWidth() + _delete->contentWidth() + _clear->width(); buttonsWidth += buttonsLeft + st::topBarActionSkip * 3; auto widthLeft = qMin(width() - buttonsWidth, -2 * st::defaultActiveButton.width); @@ -414,7 +402,7 @@ void TopBarWidget::updateControlsGeometry() { } _delete->moveToLeft(buttonsLeft, selectedButtonsTop); - _clearSelection->moveToRight(st::topBarActionSkip, selectedButtonsTop); + _clear->moveToRight(st::topBarActionSkip, selectedButtonsTop); if (_unreadBadge) { _unreadBadge->setGeometryToLeft( @@ -469,7 +457,7 @@ void TopBarWidget::updateControlsVisibility() { hideChildren(); return; } - _clearSelection->show(); + _clear->show(); _delete->setVisible(_canDelete); _forward->setVisible(_canForward); diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h index f93a88cfa7..9df60669a6 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h @@ -48,6 +48,16 @@ public: void setActiveChat(Dialogs::Key chat); + rpl::producer<> forwardSelectionRequest() const { + return _forwardSelection.events(); + } + rpl::producer<> deleteSelectionRequest() const { + return _deleteSelection.events(); + } + rpl::producer<> clearSelectionRequest() const { + return _clearSelection.events(); + } + protected: void paintEvent(QPaintEvent *e) override; void mousePressEvent(QMouseEvent *e) override; @@ -62,9 +72,6 @@ private: void selectedShowCallback(); void updateInfoToggleActive(); - void onForwardSelection(); - void onDeleteSelection(); - void onClearSelection(); void onCall(); void onSearch(); void showMenu(); @@ -95,7 +102,7 @@ private: Animation _selectedShown; - object_ptr _clearSelection; + object_ptr _clear; object_ptr _forward, _delete; object_ptr _back; @@ -121,6 +128,10 @@ private: int _unreadCounterSubscription = 0; base::Timer _onlineUpdater; + rpl::event_stream<> _forwardSelection; + rpl::event_stream<> _deleteSelection; + rpl::event_stream<> _clearSelection; + }; } // namespace HistoryView diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index 3327704960..5e972dd956 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -509,11 +509,13 @@ void TopBar::performForward() { _cancelSelectionClicks.fire({}); return; } - Window::ShowForwardMessagesBox(std::move(items), [weak = make_weak(this)]{ - if (weak) { - weak->_cancelSelectionClicks.fire({}); - } - }); + Window::ShowForwardMessagesBox( + std::move(items), + [weak = make_weak(this)] { + if (weak) { + weak->_cancelSelectionClicks.fire({}); + } + }); } void TopBar::performDelete() { @@ -521,7 +523,12 @@ void TopBar::performDelete() { if (items.empty()) { _cancelSelectionClicks.fire({}); } else { - Ui::show(Box(std::move(items))); + const auto box = Ui::show(Box(std::move(items))); + box->setDeleteConfirmedCallback([weak = make_weak(this)] { + if (weak) { + weak->_cancelSelectionClicks.fire({}); + } + }); } } diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 6d1cf88874..53d5046457 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -1387,7 +1387,14 @@ void ListWidget::forwardItems(MessageIdsList &&items) { } void ListWidget::deleteSelected() { - deleteItems(collectSelectedIds()); + if (const auto box = deleteItems(collectSelectedIds())) { + const auto weak = make_weak(this); + box->setDeleteConfirmedCallback([=]{ + if (const auto strong = weak.data()) { + strong->clearSelected(); + } + }); + } } void ListWidget::deleteItem(UniversalMsgId universalId) { @@ -1396,11 +1403,14 @@ void ListWidget::deleteItem(UniversalMsgId universalId) { } } -void ListWidget::deleteItems(MessageIdsList &&items) { +DeleteMessagesBox *ListWidget::deleteItems(MessageIdsList &&items) { if (!items.empty()) { - const auto box = Ui::show(Box(std::move(items))); - setActionBoxWeak(box.data()); + const auto box = Ui::show( + Box(std::move(items))).data(); + setActionBoxWeak(box); + return box; } + return nullptr; } void ListWidget::setActionBoxWeak(QPointer box) { @@ -1575,7 +1585,7 @@ void ListWidget::enterEventHook(QEvent *e) { } void ListWidget::leaveEventHook(QEvent *e) { - if (auto item = _overLayout) { + if (const auto item = _overLayout) { if (_overState.inside) { repaintItem(item); _overState.inside = false; @@ -1596,12 +1606,12 @@ QPoint ListWidget::clampMousePosition(QPoint position) const { }; } -void ListWidget::mouseActionUpdate(const QPoint &screenPos) { +void ListWidget::mouseActionUpdate(const QPoint &globalPosition) { if (_sections.empty() || _visibleBottom <= _visibleTop) { return; } - _mousePosition = screenPos; + _mousePosition = globalPosition; auto local = mapFromGlobal(_mousePosition); auto point = clampMousePosition(local); @@ -1625,14 +1635,14 @@ void ListWidget::mouseActionUpdate(const QPoint &screenPos) { auto inTextSelection = _overState.inside && (_overState.itemId == _pressState.itemId) && hasSelectedText(); - auto cursorDeltaLength = [&] { - auto cursorDelta = (_overState.cursor - _pressState.cursor); - return cursorDelta.manhattanLength(); - }; - auto dragStartLength = [] { - return QApplication::startDragDistance(); - }; 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) { @@ -1770,9 +1780,13 @@ void ListWidget::clearDragSelection() { } } -void ListWidget::mouseActionStart(const QPoint &screenPos, Qt::MouseButton button) { - mouseActionUpdate(screenPos); - if (button != Qt::LeftButton) return; +void ListWidget::mouseActionStart( + const QPoint &globalPosition, + Qt::MouseButton button) { + mouseActionUpdate(globalPosition); + if (button != Qt::LeftButton) { + return; + } ClickHandler::pressed(); if (_pressState != _overState) { @@ -1799,9 +1813,9 @@ void ListWidget::mouseActionStart(const QPoint &screenPos, Qt::MouseButton butto } } if (_mouseAction == MouseAction::None && pressLayout) { - HistoryTextState dragState; validateTrippleClickStartTime(); - auto startDistance = (screenPos - _trippleClickPoint).manhattanLength(); + HistoryTextState dragState; + auto startDistance = (globalPosition - _trippleClickPoint).manhattanLength(); auto validStartPoint = startDistance < QApplication::startDragDistance(); if (_trippleClickStartTime != 0 && validStartPoint) { HistoryStateRequest request; @@ -1950,8 +1964,10 @@ void ListWidget::performDrag() { //} } -void ListWidget::mouseActionFinish(const QPoint &screenPos, Qt::MouseButton button) { - mouseActionUpdate(screenPos); +void ListWidget::mouseActionFinish( + const QPoint &globalPosition, + Qt::MouseButton button) { + mouseActionUpdate(globalPosition); auto pressState = base::take(_pressState); repaintItem(pressState.itemId); diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h index ba98e577f8..9f1d7d1e9b 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h @@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_shared_media.h" #include "history/view/history_view_cursor_state.h" +class DeleteMessagesBox; + namespace Ui { class PopupMenu; } // namespace Ui @@ -186,7 +188,7 @@ private: void forwardItems(MessageIdsList &&items); void deleteSelected(); void deleteItem(UniversalMsgId universalId); - void deleteItems(MessageIdsList &&items); + DeleteMessagesBox *deleteItems(MessageIdsList &&items); void applyItemSelection( UniversalMsgId universalId, TextSelection selection); @@ -233,12 +235,12 @@ private: QPoint clampMousePosition(QPoint position) const; void mouseActionStart( - const QPoint &screenPos, + const QPoint &globalPosition, Qt::MouseButton button); - void mouseActionUpdate(const QPoint &screenPos); + void mouseActionUpdate(const QPoint &globalPosition); void mouseActionUpdate(); void mouseActionFinish( - const QPoint &screenPos, + const QPoint &globalPosition, Qt::MouseButton button); void mouseActionCancel(); void performDrag(); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 3b3436f3cb..449aae2b5d 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -877,12 +877,6 @@ void MainWidget::showSendPathsLayer() { hiderLayer(object_ptr(this)); } -void MainWidget::deleteLayer(MessageIdsList &&items) { - if (!items.empty()) { - Ui::show(Box(std::move(items))); - } -} - void MainWidget::deleteLayer(FullMsgId itemId) { if (const auto item = App::histItemById(itemId)) { const auto suggestModerateActions = true; @@ -1349,18 +1343,6 @@ void MainWidget::onCacheBackground() { _cachedFor = _willCacheFor; } -void MainWidget::forwardSelectedItems() { - _history->onForwardSelected(); -} - -void MainWidget::confirmDeleteSelectedItems() { - _history->confirmDeleteSelectedItems(); -} - -void MainWidget::clearSelectedItems() { - _history->onClearSelected(); -} - Dialogs::IndexedList *MainWidget::contactsList() { return _dialogs->contactsList(); } diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 8a1e037b5a..28a387bc63 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -168,7 +168,6 @@ public: void showForwardLayer(MessageIdsList &&items); void showSendPathsLayer(); - void deleteLayer(MessageIdsList &&items); void deleteLayer(FullMsgId itemId); void cancelUploadLayer(not_null item); void shareUrlLayer(const QString &url, const QString &text); @@ -221,10 +220,6 @@ public: bool sendMessageFail(const RPCError &error); - void forwardSelectedItems(); - void confirmDeleteSelectedItems(); - void clearSelectedItems(); - Dialogs::IndexedList *contactsList(); Dialogs::IndexedList *dialogsList(); Dialogs::IndexedList *contactsNoDialogsList(); diff --git a/Telegram/SourceFiles/ui/text/text_entity.cpp b/Telegram/SourceFiles/ui/text/text_entity.cpp index 2edc715680..7d992b0a11 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.cpp +++ b/Telegram/SourceFiles/ui/text/text_entity.cpp @@ -1780,6 +1780,13 @@ void ParseMarkdown( } } +TextWithEntities ParseEntities(const QString &text, int32 flags) { + const auto rich = ((flags & TextParseRichText) != 0); + auto result = TextWithEntities{ text, EntitiesInText() }; + ParseEntities(result, flags, rich); + return result; +} + // Some code is duplicated in flattextarea.cpp! void ParseEntities(TextWithEntities &result, int32 flags, bool rich) { if (flags & TextParseMarkdown) { // parse markdown entities (bold, italic, code and pre) diff --git a/Telegram/SourceFiles/ui/text/text_entity.h b/Telegram/SourceFiles/ui/text/text_entity.h index 559f781280..797eb80afd 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.h +++ b/Telegram/SourceFiles/ui/text/text_entity.h @@ -214,6 +214,7 @@ MTPVector EntitiesToMTP(const EntitiesInText &entities, Conver // New entities are added to the ones that are already in result. // Changes text if (flags & TextParseMarkdown). +TextWithEntities ParseEntities(const QString &text, int32 flags); void ParseEntities(TextWithEntities &result, int32 flags, bool rich = false); QString ApplyEntities(const TextWithEntities &text); diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 33da830f9b..a306209fe1 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -349,7 +349,7 @@ void Filler::addChatActions(not_null chat) { void Filler::addChannelActions(not_null channel) { auto isGroup = channel->isMegagroup(); - if (false && !isGroup) { + if (!isGroup) { const auto grouped = (channel->feed() != nullptr); _addAction( lang(grouped ? lng_feed_ungroup : lng_feed_group), diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt index d8a15704b1..221b5f8830 100644 --- a/Telegram/gyp/telegram_sources.txt +++ b/Telegram/gyp/telegram_sources.txt @@ -252,6 +252,8 @@ <(src_loc)/history/history_item.h <(src_loc)/history/history_item_components.cpp <(src_loc)/history/history_item_components.h +<(src_loc)/history/history_item_text.cpp +<(src_loc)/history/history_item_text.h <(src_loc)/history/history_inner_widget.cpp <(src_loc)/history/history_inner_widget.h <(src_loc)/history/history_location_manager.cpp