// This is the source code of AyuGram for Desktop. // // We do not and cannot prevent the use of our code, // but be respectful and credit the original author. // // Copyright @Radolyn, 2024 #include "ayu/ui/sections/edited/edited_log_inner.h" #include "apiwrap.h" #include "mainwidget.h" #include "mainwindow.h" #include "api/api_attached_stickers.h" #include "ayu/data/messages_storage.h" #include "ayu/ui/sections/edited/edited_log_section.h" #include "base/call_delayed.h" #include "base/unixtime.h" #include "base/platform/base_platform_info.h" #include "base/qt/qt_key_modifiers.h" #include "boxes/sticker_set_box.h" #include "boxes/peers/edit_participant_box.h" #include "chat_helpers/message_field.h" #include "core/application.h" #include "core/click_handler_types.h" #include "core/file_utilities.h" #include "data/data_cloud_file.h" #include "data/data_document.h" #include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_media_types.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_session.h" #include "data/data_user.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_components.h" #include "history/history_item_text.h" #include "history/admin_log/history_admin_log_filter.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_message.h" #include "history/view/history_view_service_message.h" #include "history/view/media/history_view_media.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "styles/style_chat.h" #include "styles/style_menu_icons.h" #include "ui/inactive_press.h" #include "ui/painter.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/effects/path_shift_gradient.h" #include "ui/text/text_utilities.h" #include "ui/widgets/popup_menu.h" #include "window/window_session_controller.h" #include #include namespace EditedLog { namespace { // If we require to support more admins we'll have to rewrite this anyway. constexpr auto kMaxChannelAdmins = 200; constexpr auto kScrollDateHideTimeout = 1000; constexpr auto kEventsFirstPage = 20; constexpr auto kEventsPerPage = 50; constexpr auto kClearUserpicsAfter = 50; } // namespace template void InnerWidget::enumerateItems(Method method) { constexpr auto TopToBottom = (direction == EnumItemsDirection::TopToBottom); // No displayed messages in this history. if (_items.empty()) { return; } if (_visibleBottom <= _itemsTop || _itemsTop + _itemsHeight <= _visibleTop) { return; } auto begin = std::rbegin(_items), end = std::rend(_items); auto from = TopToBottom ? std::lower_bound(begin, end, _visibleTop, [this](auto &elem, int top) { return this->itemTop(elem) + elem->height() <= top; }) : std::upper_bound(begin, end, _visibleBottom, [this](int bottom, auto &elem) { return this->itemTop(elem) + elem->height() >= bottom; }); auto wasEnd = (from == end); if (wasEnd) { --from; } if (TopToBottom) { Assert(itemTop(from->get()) + from->get()->height() > _visibleTop); } else { Assert(itemTop(from->get()) < _visibleBottom); } while (true) { auto item = from->get(); auto itemtop = itemTop(item); auto itembottom = itemtop + item->height(); // Binary search should've skipped all the items that are above / below the visible area. if (TopToBottom) { Assert(itembottom > _visibleTop); } else { Assert(itemtop < _visibleBottom); } if (!method(item, itemtop, itembottom)) { return; } // Skip all the items that are below / above the visible area. if (TopToBottom) { if (itembottom >= _visibleBottom) { return; } } else { if (itemtop <= _visibleTop) { return; } } if (TopToBottom) { if (++from == end) { break; } } else { if (from == begin) { break; } --from; } } } template void InnerWidget::enumerateUserpics(Method method) { // Find and remember the top of an attached messages pack // -1 means we didn't find an attached to next message yet. int lowestAttachedItemTop = -1; auto userpicCallback = [&](not_null view, int itemtop, int itembottom) { // Skip all service messages. if (view->data()->isService()) { return true; } if (lowestAttachedItemTop < 0 && view->isAttachedToNext()) { lowestAttachedItemTop = itemtop + view->marginTop(); } // Call method on a userpic for all messages that have it and for those who are not showing it // because of their attachment to the next message if they are bottom-most visible. if (view->displayFromPhoto() || (view->hasFromPhoto() && itembottom >= _visibleBottom)) { if (lowestAttachedItemTop < 0) { lowestAttachedItemTop = itemtop + view->marginTop(); } // Attach userpic to the bottom of the visible area with the same margin as the last message. auto userpicMinBottomSkip = st::historyPaddingBottom + st::msgMargin.bottom(); auto userpicBottom = qMin(itembottom - view->marginBottom(), _visibleBottom - userpicMinBottomSkip); // Do not let the userpic go above the attached messages pack top line. userpicBottom = qMax(userpicBottom, lowestAttachedItemTop + st::msgPhotoSize); // Call the template callback function that was passed // and return if it finished everything it needed. if (!method(view, userpicBottom - st::msgPhotoSize)) { return false; } } // Forget the found top of the pack, search for the next one from scratch. if (!view->isAttachedToNext()) { lowestAttachedItemTop = -1; } return true; }; enumerateItems(userpicCallback); } template void InnerWidget::enumerateDates(Method method) { // Find and remember the bottom of an single-day messages pack // -1 means we didn't find a same-day with previous message yet. auto lowestInOneDayItemBottom = -1; auto dateCallback = [&](not_null view, int itemtop, int itembottom) { const auto item = view->data(); if (lowestInOneDayItemBottom < 0 && view->isInOneDayWithPrevious()) { lowestInOneDayItemBottom = itembottom - view->marginBottom(); } // Call method on a date for all messages that have it and for those who are not showing it // because they are in a one day together with the previous message if they are top-most visible. if (view->displayDate() || (!item->isEmpty() && itemtop <= _visibleTop)) { if (lowestInOneDayItemBottom < 0) { lowestInOneDayItemBottom = itembottom - view->marginBottom(); } // Attach date to the top of the visible area with the same margin as it has in service message. auto dateTop = qMax(itemtop, _visibleTop) + st::msgServiceMargin.top(); // Do not let the date go below the single-day messages pack bottom line. auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top(); dateTop = qMin(dateTop, lowestInOneDayItemBottom - dateHeight); // Call the template callback function that was passed // and return if it finished everything it needed. if (!method(view, itemtop, dateTop)) { return false; } } // Forget the found bottom of the pack, search for the next one from scratch. if (!view->isInOneDayWithPrevious()) { lowestInOneDayItemBottom = -1; } return true; }; enumerateItems(dateCallback); } InnerWidget::InnerWidget( QWidget *parent, not_null controller, not_null peer, not_null item) : RpWidget(parent), _controller(controller), _peer(peer), _item(item), _history(peer->owner().history(peer)), _api(&_peer->session().mtp()), _pathGradient( HistoryView::MakePathShiftGradient( controller->chatStyle(), [=] { update(); })), _scrollDateCheck([=] { scrollDateCheck(); }), _emptyText( st::historyAdminLogEmptyWidth - st::historyAdminLogEmptyPadding.left() - st::historyAdminLogEmptyPadding.left()) { Window::ChatThemeValueFromPeer( controller, peer ) | rpl::start_with_next([=](std::shared_ptr &&theme) { _theme = std::move(theme); controller->setChatStyleTheme(_theme); }, lifetime()); setMouseTracking(true); _scrollDateHideTimer.setCallback([=] { scrollDateHideByTimer(); }); session().data().viewRepaintRequest( ) | rpl::start_with_next([=](auto view) { if (view->delegate() == this) { repaintItem(view); } }, lifetime()); session().data().viewResizeRequest( ) | rpl::start_with_next([=](auto view) { if (view->delegate() == this) { resizeItem(view); } }, lifetime()); session().data().itemViewRefreshRequest( ) | rpl::start_with_next([=](auto item) { if (const auto view = viewForItem(item)) { refreshItem(view); } }, lifetime()); session().data().viewLayoutChanged( ) | rpl::start_with_next([=](auto view) { if (view->delegate() == this) { if (view->isUnderCursor()) { updateSelected(); } } }, lifetime()); session().data().itemDataChanges( ) | rpl::start_with_next([=](not_null item) { if (const auto view = viewForItem(item)) { view->itemDataChanged(); } }, lifetime()); session().data().itemVisibilityQueries( ) | rpl::filter([=]( const Data::Session::ItemVisibilityQuery &query) { return (_history == query.item->history()) && query.item->isAdminLogEntry() && isVisible(); }) | rpl::start_with_next([=]( const Data::Session::ItemVisibilityQuery &query) { if (const auto view = viewForItem(query.item)) { auto top = itemTop(view); if (top >= 0 && top + view->height() > _visibleTop && top < _visibleBottom) { *query.isVisible = true; } } }, lifetime()); controller->adaptive().chatWideValue( ) | rpl::start_with_next([=](bool wide) { _isChatWide = wide; }, lifetime()); updateEmptyText(); } Main::Session &InnerWidget::session() const { return _controller->session(); } rpl::producer InnerWidget::scrollToSignal() const { return _scrollToSignal.events(); } void InnerWidget::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { auto scrolledUp = (visibleTop < _visibleTop); _visibleTop = visibleTop; _visibleBottom = visibleBottom; // Unload userpics. if (_userpics.size() > kClearUserpicsAfter) { _userpicsCache = std::move(_userpics); } updateVisibleTopItem(); if (_items.size() == 0) { addEvents(Direction::Up); } if (scrolledUp) { _scrollDateCheck.call(); } else { scrollDateHideByTimer(); } _controller->floatPlayerAreaUpdated(); session().data().itemVisibilitiesUpdated(); } void InnerWidget::updateVisibleTopItem() { if (_visibleBottom == height()) { _visibleTopItem = nullptr; } else { auto begin = std::rbegin(_items), end = std::rend(_items); auto from = std::lower_bound(begin, end, _visibleTop, [this](auto &&elem, int top) { return this->itemTop(elem) + elem->height() <= top; }); if (from != end) { _visibleTopItem = *from; _visibleTopFromItem = _visibleTop - _visibleTopItem->y(); } else { _visibleTopItem = nullptr; _visibleTopFromItem = _visibleTop; } } } bool InnerWidget::displayScrollDate() const { return (_visibleTop <= height() - 2 * (_visibleBottom - _visibleTop)); } void InnerWidget::scrollDateCheck() { if (!_visibleTopItem) { _scrollDateLastItem = nullptr; _scrollDateLastItemTop = 0; scrollDateHide(); } else if (_visibleTopItem != _scrollDateLastItem || _visibleTopFromItem != _scrollDateLastItemTop) { // Show scroll date only if it is not the initial onScroll() event (with empty _scrollDateLastItem). if (_scrollDateLastItem && !_scrollDateShown) { toggleScrollDateShown(); } _scrollDateLastItem = _visibleTopItem; _scrollDateLastItemTop = _visibleTopFromItem; _scrollDateHideTimer.callOnce(kScrollDateHideTimeout); } } void InnerWidget::scrollDateHideByTimer() { _scrollDateHideTimer.cancel(); scrollDateHide(); } void InnerWidget::scrollDateHide() { if (_scrollDateShown) { toggleScrollDateShown(); } } void InnerWidget::toggleScrollDateShown() { _scrollDateShown = !_scrollDateShown; auto from = _scrollDateShown ? 0. : 1.; auto to = _scrollDateShown ? 1. : 0.; _scrollDateOpacity.start([this] { repaintScrollDateCallback(); }, from, to, st::historyDateFadeDuration); } void InnerWidget::repaintScrollDateCallback() { auto updateTop = _visibleTop; auto updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom(); update(0, updateTop, width(), updateHeight); } void InnerWidget::updateEmptyText() { auto hasSearch = false; auto hasFilter = false; auto text = Ui::Text::Semibold((hasSearch || hasFilter) ? tr::lng_admin_log_no_results_title(tr::now) : tr::lng_admin_log_no_events_title(tr::now)); auto description = _peer->isMegagroup() ? tr::lng_admin_log_no_events_text(tr::now) : tr::lng_admin_log_no_events_text_channel(tr::now); text.text.append(u"\n\n"_q + description); _emptyText.setMarkedText(st::defaultTextStyle, text); } QString InnerWidget::tooltipText() const { if (_mouseCursorState == CursorState::Date && _mouseAction == MouseAction::None) { if (const auto view = Element::Hovered()) { auto dateText = HistoryView::DateTooltipText(view); const auto sentIt = _itemDates.find(view->data()); if (sentIt != end(_itemDates)) { dateText += '\n' + tr::lng_sent_date( tr::now, lt_date, QLocale().toString( base::unixtime::parse(sentIt->second), QLocale::LongFormat)); } return dateText; } } else if (_mouseCursorState == CursorState::Forwarded && _mouseAction == MouseAction::None) { if (const auto view = Element::Hovered()) { if (const auto forwarded = view->data()->Get()) { return forwarded->text.toString(); } } } else if (const auto lnk = ClickHandler::getActive()) { return lnk->tooltip(); } return QString(); } QPoint InnerWidget::tooltipPos() const { return _mousePosition; } bool InnerWidget::tooltipWindowActive() const { return Ui::AppInFocus() && Ui::InFocusChain(window()); } HistoryView::Context InnerWidget::elementContext() { return HistoryView::Context::AdminLog; } bool InnerWidget::elementUnderCursor( not_null view) { return (Element::Hovered() == view); } bool InnerWidget::elementInSelectionMode() { return false; } bool InnerWidget::elementIntersectsRange( not_null view, int from, int till) { Expects(view->delegate() == this); const auto top = itemTop(view); const auto bottom = top + view->height(); return (top < till && bottom > from); } void InnerWidget::elementStartStickerLoop(not_null view) { } void InnerWidget::elementShowPollResults( not_null poll, FullMsgId context) { } void InnerWidget::elementOpenPhoto( not_null photo, FullMsgId context) { _controller->openPhoto(photo, {context}); } void InnerWidget::elementOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { _controller->openDocument(document, showInMediaView, {context}); } void InnerWidget::elementCancelUpload(const FullMsgId &context) { if (const auto item = session().data().message(context)) { _controller->cancelUploadLayer(item); } } void InnerWidget::elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) { } bool InnerWidget::elementAnimationsPaused() { return _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any); } bool InnerWidget::elementHideReply(not_null view) { return true; } bool InnerWidget::elementShownUnread(not_null view) { return false; } void InnerWidget::elementSendBotCommand( const QString &command, const FullMsgId &context) { } void InnerWidget::elementSearchInList( const QString &query, const FullMsgId &context) { } void InnerWidget::elementHandleViaClick(not_null bot) { } bool InnerWidget::elementIsChatWide() { return _isChatWide; } not_null InnerWidget::elementPathShiftGradient() { return _pathGradient.get(); } void InnerWidget::elementReplyTo(const FullReplyTo &to) { } void InnerWidget::elementStartInteraction(not_null view) { } void InnerWidget::elementStartPremium( not_null view, Element *replacing) { } void InnerWidget::elementCancelPremium(not_null view) { } QString InnerWidget::elementAuthorRank(not_null view) { return {}; } void InnerWidget::saveState(not_null memento) { if (!_filterChanged) { for (auto &item : _items) { item.clearView(); } memento->setItems( base::take(_items), base::take(_eventIds), _upLoaded, _downLoaded); base::take(_itemsByData); } _upLoaded = _downLoaded = true; // Don't load or handle anything anymore. } void InnerWidget::restoreState(not_null memento) { _items = memento->takeItems(); for (auto &item : _items) { item.refreshView(this); _itemsByData.emplace(item->data(), item.get()); } _eventIds = memento->takeEventIds(); _upLoaded = memento->upLoaded(); _downLoaded = memento->downLoaded(); _filterChanged = false; updateSize(); } void InnerWidget::addEvents(Direction direction) { auto messages = AyuMessages::getEditedMessages(_item); if (messages.empty()) { return; } const auto size = messages.size(); auto &container = _items; for (const auto &message : messages) { const auto addOne = [&]( OwnedItem item, TimeId sentDate, MsgId realId) { if (sentDate) { _itemDates.emplace(item->data(), sentDate); } _itemsByData.emplace(item->data(), item.get()); container.push_back(std::move(item)); }; GenerateItems( this, _history, message, addOne); } itemsAdded(direction, size); update(); repaint(); } void InnerWidget::itemsAdded(Direction direction, int addedCount) { Expects(addedCount >= 0); auto checkFrom = (direction == Direction::Up) ? (_items.size() - addedCount) : 1; // Should be ": 0", but zero is skipped anyway. auto checkTo = (direction == Direction::Up) ? (_items.size() + 1) : (addedCount + 1); for (auto i = checkFrom; i != checkTo; ++i) { if (i > 0) { const auto view = _items[i - 1].get(); if (i < _items.size()) { const auto previous = _items[i].get(); view->setDisplayDate(view->dateTime().date() != previous->dateTime().date()); const auto attach = view->computeIsAttachToPrevious(previous); view->setAttachToPrevious(attach, previous); previous->setAttachToNext(attach, view); } else { view->setDisplayDate(true); } } } updateSize(); } void InnerWidget::updateSize() { TWidget::resizeToWidth(width()); restoreScrollPosition(); updateVisibleTopItem(); } int InnerWidget::resizeGetHeight(int newWidth) { update(); const auto resizeAllItems = (_itemsWidth != newWidth); auto newHeight = 0; for (const auto &item : ranges::views::reverse(_items)) { item->setY(newHeight); if (item->pendingResize() || resizeAllItems) { newHeight += item->resizeGetHeight(newWidth); } else { newHeight += item->height(); } } _itemsWidth = newWidth; _itemsHeight = newHeight; _itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom) ? (_minHeight - _itemsHeight - st::historyPaddingBottom) : 0; return _itemsTop + _itemsHeight + st::historyPaddingBottom; } void InnerWidget::restoreScrollPosition() { const auto newVisibleTop = _visibleTopItem ? (itemTop(_visibleTopItem) + _visibleTopFromItem) : ScrollMax; _scrollToSignal.fire_copy(newVisibleTop); } void InnerWidget::paintEvent(QPaintEvent *e) { if (_controller->contentOverlapped(this, e)) { return; } const auto guard = gsl::finally([&] { _userpicsCache.clear(); }); Painter p(this); auto clip = e->rect(); auto context = _controller->preparePaintContext({ .theme = _theme.get(), .clip = clip, .visibleAreaPositionGlobal = mapToGlobal(QPoint(0, _visibleTop)), .visibleAreaTop = _visibleTop, .visibleAreaWidth = width(), }); if (_items.empty() && _upLoaded && _downLoaded) { paintEmpty(p, context.st); } else { _pathGradient->startFrame( 0, width(), std::min(st::msgMaxWidth / 2, width() / 2)); auto begin = std::rbegin(_items), end = std::rend(_items); auto from = std::lower_bound(begin, end, clip.top(), [this](auto &elem, int top) { return this->itemTop(elem) + elem->height() <= top; }); auto to = std::lower_bound(begin, end, clip.top() + clip.height(), [this](auto &elem, int bottom) { return this->itemTop(elem) < bottom; }); if (from != end) { auto top = itemTop(from->get()); context.translate(0, -top); p.translate(0, top); for (auto i = from; i != to; ++i) { const auto view = i->get(); context.outbg = view->hasOutLayout(); context.selection = (view == _selectedItem) ? _selectedText : TextSelection(); view->draw(p, context); const auto height = view->height(); top += height; context.translate(0, -height); p.translate(0, height); } context.translate(0, top); p.translate(0, -top); enumerateUserpics([&](not_null view, int userpicTop) { // stop the enumeration if the userpic is below the painted rect if (userpicTop >= clip.top() + clip.height()) { return false; } // paint the userpic if it intersects the painted rect if (userpicTop + st::msgPhotoSize > clip.top()) { const auto from = view->data()->from(); from->paintUserpicLeft( p, _userpics[from], st::historyPhotoLeft, userpicTop, view->width(), st::msgPhotoSize); } return true; }); auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top(); auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.); enumerateDates([&](not_null view, int itemtop, int dateTop) { // stop the enumeration if the date is above the painted rect if (dateTop + dateHeight <= clip.top()) { return false; } const auto displayDate = view->displayDate(); auto dateInPlace = displayDate; if (dateInPlace) { const auto correctDateTop = itemtop + st::msgServiceMargin.top(); dateInPlace = (dateTop < correctDateTop + dateHeight); } //bool noFloatingDate = (item->date.date() == lastDate && displayDate); //if (noFloatingDate) { // if (itemtop < showFloatingBefore) { // noFloatingDate = false; // } //} // paint the date if it intersects the painted rect if (dateTop < clip.top() + clip.height()) { auto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity; if (opacity > 0.) { p.setOpacity(opacity); const auto dateY = /*noFloatingDate ? itemtop :*/ (dateTop - st::msgServiceMargin.top()); const auto width = view->width(); if (const auto date = view->Get()) { date->paint(p, context.st, dateY, width, _isChatWide); } else { HistoryView::ServiceMessagePainter::PaintDate( p, context.st, view->dateTime(), dateY, width, _isChatWide); } } } return true; }); } } } auto InnerWidget::viewForItem(const HistoryItem *item) -> Element * { if (item) { const auto i = _itemsByData.find(item); if (i != _itemsByData.end()) { return i->second; } } return nullptr; } void InnerWidget::paintEmpty(Painter &p, not_null st) { auto rectWidth = st::historyAdminLogEmptyWidth; auto innerWidth = rectWidth - st::historyAdminLogEmptyPadding.left() - st::historyAdminLogEmptyPadding.right(); auto rectHeight = st::historyAdminLogEmptyPadding.top() + _emptyText.countHeight(innerWidth) + st::historyAdminLogEmptyPadding.bottom(); auto rect = QRect((width() - rectWidth) / 2, (height() - rectHeight) / 3, rectWidth, rectHeight); HistoryView::ServiceMessagePainter::PaintBubble(p, st, rect); p.setPen(st->msgServiceFg()); _emptyText.draw(p, rect.x() + st::historyAdminLogEmptyPadding.left(), rect.y() + st::historyAdminLogEmptyPadding.top(), innerWidth, style::al_top); } TextForMimeData InnerWidget::getSelectedText() const { return _selectedItem ? _selectedItem->selectedText(_selectedText) : TextForMimeData(); } void InnerWidget::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Escape) { _controller->showBackFromStack(); } else if (e == QKeySequence::Copy && _selectedItem != nullptr) { copySelectedText(); #ifdef Q_OS_MAC } else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) { TextUtilities::SetClipboardText(getSelectedText(), QClipboard::FindBuffer); #endif // Q_OS_MAC } else { e->ignore(); } } void InnerWidget::mouseDoubleClickEvent(QMouseEvent *e) { mouseActionStart(e->globalPos(), e->button()); if (((_mouseAction == MouseAction::Selecting && _selectedItem != nullptr) || (_mouseAction == MouseAction::None)) && _mouseSelectType == TextSelectType::Letters && _mouseActionItem) { StateRequest request; request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol; auto dragState = _mouseActionItem->textState(_dragStartPosition, request); if (dragState.cursor == CursorState::Text) { _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); _trippleClickPoint = e->globalPos(); _trippleClickTimer.callOnce(QApplication::doubleClickInterval()); } } } void InnerWidget::contextMenuEvent(QContextMenuEvent *e) { showContextMenu(e); } void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (e->reason() == QContextMenuEvent::Mouse) { mouseActionUpdate(e->globalPos()); } // -1 - has selection, but no over, 0 - no selection, 1 - over text auto isUponSelected = 0; auto hasSelected = 0; if (_selectedItem) { isUponSelected = -1; auto selFrom = _selectedText.from; auto selTo = _selectedText.to; hasSelected = (selTo > selFrom) ? 1 : 0; if (Element::Moused() && Element::Moused() == Element::Hovered()) { auto mousePos = mapPointToItem( mapFromGlobal(_mousePosition), Element::Moused()); StateRequest request; request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol; auto dragState = Element::Moused()->textState(mousePos, request); if (dragState.cursor == CursorState::Text && base::in_range(dragState.symbol, selFrom, selTo)) { isUponSelected = 1; } } } if (showFromTouch && hasSelected && isUponSelected < hasSelected) { isUponSelected = hasSelected; } _menu = base::make_unique_q( this, st::popupMenuExpandedSeparator); const auto link = ClickHandler::getActive(); auto view = Element::Hovered() ? Element::Hovered() : Element::HoveredLink(); const auto lnkPhoto = link ? reinterpret_cast( link->property(kPhotoLinkMediaProperty).toULongLong()) : nullptr; const auto lnkDocument = link ? reinterpret_cast( link->property(kDocumentLinkMediaProperty).toULongLong()) : nullptr; auto lnkIsVideo = lnkDocument ? lnkDocument->isVideoFile() : false; auto lnkIsVoice = lnkDocument ? lnkDocument->isVoiceMessage() : false; auto lnkIsAudio = lnkDocument ? lnkDocument->isAudioFile() : false; const auto fromId = PeerId(link ? link->property(kPeerLinkPeerIdProperty).toULongLong() : 0); if (lnkPhoto || lnkDocument) { if (isUponSelected > 0) { _menu->addAction(tr::lng_context_copy_selected(tr::now), [=] { copySelectedText(); }, &st::menuIconCopy); } if (lnkPhoto) { const auto media = lnkPhoto->activeMediaView(); if (!lnkPhoto->isNull() && media && media->loaded()) { _menu->addAction(tr::lng_context_save_image(tr::now), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [=] { savePhotoToFile(lnkPhoto); }), &st::menuIconSaveImage); _menu->addAction(tr::lng_context_copy_image(tr::now), [=] { copyContextImage(lnkPhoto); }, &st::menuIconCopy); } if (lnkPhoto->hasAttachedStickers()) { const auto controller = _controller; auto callback = [=] { auto &attached = session().api().attachedStickers(); attached.requestAttachedStickerSets(controller, lnkPhoto); }; _menu->addAction( tr::lng_context_attached_stickers(tr::now), std::move(callback), &st::menuIconStickers); } } else { if (lnkDocument->loading()) { _menu->addAction(tr::lng_context_cancel_download(tr::now), [=] { cancelContextDownload(lnkDocument); }, &st::menuIconCancel); } else { const auto itemId = view ? view->data()->fullId() : FullMsgId(); if (const auto item = session().data().message(itemId)) { const auto notAutoplayedGif = [&] { return lnkDocument->isGifv() && !Data::AutoDownload::ShouldAutoPlay( session().settings().autoDownload(), item->history()->peer, lnkDocument); }(); if (notAutoplayedGif) { _menu->addAction(tr::lng_context_open_gif(tr::now), [=] { openContextGif(itemId); }, &st::menuIconShowInChat); } } if (!lnkDocument->filepath(true).isEmpty()) { _menu->addAction(Platform::IsMac() ? tr::lng_context_show_in_finder(tr::now) : tr::lng_context_show_in_folder(tr::now), [=] { showContextInFolder(lnkDocument); }, &st::menuIconShowInFolder); } _menu->addAction(lnkIsVideo ? tr::lng_context_save_video(tr::now) : (lnkIsVoice ? tr::lng_context_save_audio(tr::now) : (lnkIsAudio ? tr::lng_context_save_audio_file( tr::now) : tr::lng_context_save_file(tr::now))), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, lnkDocument] { saveDocumentToFile(lnkDocument); }), &st::menuIconDownload); if (lnkDocument->hasAttachedStickers()) { const auto controller = _controller; auto callback = [=] { auto &attached = session().api().attachedStickers(); attached.requestAttachedStickerSets(controller, lnkDocument); }; _menu->addAction( tr::lng_context_attached_stickers(tr::now), std::move(callback), &st::menuIconStickers); } } } } else if (fromId) { // suggest to block // if (const auto participant = session().data().peer(fromId)) { // suggestRestrictParticipant(participant); // } } else { // maybe cursor on some text history item? const auto item = view ? view->data().get() : nullptr; const auto itemId = item ? item->fullId() : FullMsgId(); if (isUponSelected > 0) { _menu->addAction( tr::lng_context_copy_selected(tr::now), [this] { copySelectedText(); }, &st::menuIconCopy); } else { if (item && !isUponSelected) { const auto media = view->media(); const auto mediaHasTextForCopy = media && media->hasTextForCopy(); if (const auto document = media ? media->getDocument() : nullptr) { if (document->sticker()) { _menu->addAction(tr::lng_context_save_image(tr::now), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, document] { saveDocumentToFile(document); }), &st::menuIconDownload); } } if (!item->isService() && !link && (view->hasVisibleText() || mediaHasTextForCopy || item->Has())) { _menu->addAction(tr::lng_context_copy_text(tr::now), [=] { copyContextText(itemId); }, &st::menuIconCopy); } } } const auto actionText = link ? link->copyToClipboardContextItemText() : QString(); if (!actionText.isEmpty()) { _menu->addAction( actionText, [text = link->copyToClipboardText()] { QGuiApplication::clipboard()->setText(text); }, &st::menuIconCopy); } } if (_menu->empty()) { _menu = nullptr; } else { _menu->popup(e->globalPos()); e->accept(); } } void InnerWidget::savePhotoToFile(not_null photo) { const auto media = photo->activeMediaView(); if (photo->isNull() || !media || !media->loaded()) { return; } auto filter = u"JPEG Image (*.jpg);;"_q + FileDialog::AllFilesFilter(); FileDialog::GetWritePath( this, tr::lng_save_photo(tr::now), filter, filedialogDefaultName(u"photo"_q, u".jpg"_q), crl::guard(this, [=](const QString &result) { if (!result.isEmpty()) { media->saveToFile(result); } })); } void InnerWidget::saveDocumentToFile(not_null document) { DocumentSaveClickHandler::Save( Data::FileOrigin(), document, DocumentSaveClickHandler::Mode::ToNewFile); } void InnerWidget::copyContextImage(not_null photo) { const auto media = photo->activeMediaView(); if (photo->isNull() || !media || !media->loaded()) { return; } media->setToClipboard(); } void InnerWidget::copySelectedText() { TextUtilities::SetClipboardText(getSelectedText()); } void InnerWidget::showStickerPackInfo(not_null document) { StickerSetBox::Show(_controller->uiShow(), document); } void InnerWidget::cancelContextDownload(not_null document) { document->cancel(); } void InnerWidget::showContextInFolder(not_null document) { const auto filepath = document->filepath(true); if (!filepath.isEmpty()) { File::ShowInFolder(filepath); } } void InnerWidget::openContextGif(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { if (const auto media = item->media()) { if (const auto document = media->document()) { _controller->openDocument(document, true, {itemId}); } } } } void InnerWidget::copyContextText(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { TextUtilities::SetClipboardText(HistoryItemText(item)); } } void InnerWidget::mousePressEvent(QMouseEvent *e) { if (_menu) { e->accept(); return; // ignore mouse press, that was hiding context menu } mouseActionStart(e->globalPos(), e->button()); } void InnerWidget::mouseMoveEvent(QMouseEvent *e) { auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton)); if (!buttonsPressed && _mouseAction != MouseAction::None) { mouseReleaseEvent(e); } mouseActionUpdate(e->globalPos()); } void InnerWidget::mouseReleaseEvent(QMouseEvent *e) { mouseActionFinish(e->globalPos(), e->button()); if (!rect().contains(e->pos())) { leaveEvent(e); } } void InnerWidget::enterEventHook(QEnterEvent *e) { mouseActionUpdate(QCursor::pos()); return TWidget::enterEventHook(e); } void InnerWidget::leaveEventHook(QEvent *e) { if (const auto view = Element::Hovered()) { repaintItem(view); Element::Hovered(nullptr); } ClickHandler::clearActive(); Ui::Tooltip::Hide(); if (!ClickHandler::getPressed() && _cursor != style::cur_default) { _cursor = style::cur_default; setCursor(_cursor); } return TWidget::leaveEventHook(e); } void InnerWidget::mouseActionStart(const QPoint &screenPos, Qt::MouseButton button) { mouseActionUpdate(screenPos); if (button != Qt::LeftButton) return; ClickHandler::pressed(); if (Element::Pressed() != Element::Hovered()) { repaintItem(Element::Pressed()); Element::Pressed(Element::Hovered()); repaintItem(Element::Pressed()); } _mouseAction = MouseAction::None; _mouseActionItem = Element::Moused(); _dragStartPosition = mapPointToItem( mapFromGlobal(screenPos), _mouseActionItem); _pressWasInactive = Ui::WasInactivePress(_controller->widget()); if (_pressWasInactive) { Ui::MarkInactivePress(_controller->widget(), false); } if (ClickHandler::getPressed()) { _mouseAction = MouseAction::PrepareDrag; } if (_mouseAction == MouseAction::None && _mouseActionItem) { TextState dragState; if (_trippleClickTimer.isActive() && (screenPos - _trippleClickPoint).manhattanLength() < QApplication::startDragDistance()) { StateRequest request; request.flags = Ui::Text::StateRequest::Flag::LookupSymbol; dragState = _mouseActionItem->textState(_dragStartPosition, request); if (dragState.cursor == CursorState::Text) { auto selection = TextSelection{dragState.symbol, dragState.symbol}; repaintItem(std::exchange(_selectedItem, _mouseActionItem)); _selectedText = selection; _mouseTextSymbol = dragState.symbol; _mouseAction = MouseAction::Selecting; _mouseSelectType = TextSelectType::Paragraphs; mouseActionUpdate(_mousePosition); _trippleClickTimer.callOnce(QApplication::doubleClickInterval()); } } else if (Element::Pressed()) { StateRequest request; request.flags = Ui::Text::StateRequest::Flag::LookupSymbol; dragState = _mouseActionItem->textState(_dragStartPosition, request); } if (_mouseSelectType != TextSelectType::Paragraphs) { if (Element::Pressed()) { _mouseTextSymbol = dragState.symbol; auto uponSelected = (dragState.cursor == CursorState::Text); 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) { if (dragState.afterSymbol) ++_mouseTextSymbol; auto selection = TextSelection{_mouseTextSymbol, _mouseTextSymbol}; repaintItem(std::exchange(_selectedItem, _mouseActionItem)); _selectedText = selection; _mouseAction = MouseAction::Selecting; repaintItem(_mouseActionItem); } } } } if (!_mouseActionItem) { _mouseAction = MouseAction::None; } else if (_mouseAction == MouseAction::None) { _mouseActionItem = nullptr; } } void InnerWidget::mouseActionUpdate(const QPoint &screenPos) { _mousePosition = screenPos; updateSelected(); } void InnerWidget::mouseActionCancel() { _mouseActionItem = nullptr; _mouseAction = MouseAction::None; _dragStartPosition = QPoint(0, 0); _wasSelectedText = false; } void InnerWidget::mouseActionFinish(const QPoint &screenPos, Qt::MouseButton button) { mouseActionUpdate(screenPos); auto activated = ClickHandler::unpressed(); if (_mouseAction == MouseAction::Dragging) { activated = nullptr; } if (const auto view = Element::Pressed()) { repaintItem(view); Element::Pressed(nullptr); } _wasSelectedText = false; if (activated) { mouseActionCancel(); ActivateClickHandler(window(), activated, { button, QVariant::fromValue(ClickHandlerContext{ .elementDelegate = [weak = Ui::MakeWeak(this)] { return weak ? (ElementDelegate*) weak : nullptr; }, .sessionWindow = base::make_weak(_controller), }) }); return; } if (_mouseAction == MouseAction::PrepareDrag && !_pressWasInactive && button != Qt::RightButton) { repaintItem(base::take(_selectedItem)); } else if (_mouseAction == MouseAction::Selecting) { if (_selectedItem && !_pressWasInactive) { if (_selectedText.from == _selectedText.to) { _selectedItem = nullptr; _controller->widget()->setInnerFocus(); } } } _mouseAction = MouseAction::None; _mouseActionItem = nullptr; _mouseSelectType = TextSelectType::Letters; //_widget->noSelectingScroll(); // TODO if (QGuiApplication::clipboard()->supportsSelection() && _selectedItem && _selectedText.from != _selectedText.to) { TextUtilities::SetClipboardText( _selectedItem->selectedText(_selectedText), QClipboard::Selection); } } void InnerWidget::updateSelected() { auto mousePosition = mapFromGlobal(_mousePosition); auto point = QPoint( std::clamp(mousePosition.x(), 0, width()), std::clamp(mousePosition.y(), _visibleTop, _visibleBottom)); auto itemPoint = QPoint(); auto begin = std::rbegin(_items), end = std::rend(_items); auto from = (point.y() >= _itemsTop && point.y() < _itemsTop + _itemsHeight) ? std::lower_bound(begin, end, point.y(), [this](auto &elem, int top) { return this->itemTop(elem) + elem->height() <= top; }) : end; const auto view = (from != end) ? from->get() : nullptr; const auto item = view ? view->data().get() : nullptr; if (item) { Element::Moused(view); itemPoint = mapPointToItem(point, view); if (view->pointState(itemPoint) != PointState::Outside) { if (Element::Hovered() != view) { repaintItem(Element::Hovered()); Element::Hovered(view); repaintItem(view); } } else if (const auto view = Element::Hovered()) { repaintItem(view); Element::Hovered(nullptr); } } TextState dragState; ClickHandlerHost *lnkhost = nullptr; auto selectingText = _selectedItem && (view == _mouseActionItem) && (view == Element::Hovered()); if (view) { if (view != _mouseActionItem || (itemPoint - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) { if (_mouseAction == MouseAction::PrepareDrag) { _mouseAction = MouseAction::Dragging; InvokeQueued(this, [this] { performDrag(); }); } } StateRequest request; if (_mouseAction == MouseAction::Selecting) { request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol; } else { selectingText = false; } if (base::IsAltPressed()) { request.flags &= ~Ui::Text::StateRequest::Flag::LookupLink; } dragState = view->textState(itemPoint, request); lnkhost = view; if (!dragState.link && itemPoint.x() >= st::historyPhotoLeft && itemPoint.x() < st::historyPhotoLeft + st::msgPhotoSize) { if (!item->isService() && 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) { dragState.link = view->data()->from()->openLink(); lnkhost = view; return false; } return true; }); } } } auto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost); if (lnkChanged || dragState.cursor != _mouseCursorState) { Ui::Tooltip::Hide(); } if (dragState.link || dragState.cursor == CursorState::Date || dragState.cursor == CursorState::Forwarded) { Ui::Tooltip::Show(350, this); } auto cursor = style::cur_default; if (_mouseAction == MouseAction::None) { _mouseCursorState = dragState.cursor; if (dragState.link) { cursor = style::cur_pointer; } else if (_mouseCursorState == CursorState::Text) { cursor = style::cur_text; } else if (_mouseCursorState == CursorState::Date) { // cursor = style::cur_cross; } } else if (item) { if (_mouseAction == MouseAction::Selecting) { if (selectingText) { auto second = dragState.symbol; if (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) { ++second; } 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(); } } } 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 = Element::PressedLink()) { const auto adjustedPoint = mapPointToItem(point, pressedView); pressedView->updatePressed(adjustedPoint); } //if (_mouseAction == MouseAction::Selecting) { // _widget->checkSelectingScroll(mousePos); //} else { // _widget->noSelectingScroll(); //} // TODO if (_mouseAction == MouseAction::None && (lnkChanged || cursor != _cursor)) { setCursor(_cursor = cursor); } } void InnerWidget::performDrag() { if (_mouseAction != MouseAction::Dragging) return; } int InnerWidget::itemTop(not_null view) const { return _itemsTop + view->y(); } void InnerWidget::repaintItem(const Element *view) { if (!view) { return; } const auto top = itemTop(view); const auto range = view->verticalRepaintRange(); update(0, top + range.top, width(), range.height); } void InnerWidget::resizeItem(not_null view) { updateSize(); } void InnerWidget::refreshItem(not_null view) { // No need to refresh views in admin log. // sogl } QPoint InnerWidget::mapPointToItem(QPoint point, const Element *view) const { if (!view) { return QPoint(); } return point - QPoint(0, itemTop(view)); } InnerWidget::~InnerWidget() = default; } // namespace EditedLog