diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 013dcb1fe..5aac12247 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -7938,10 +7938,12 @@ QPoint HistoryWidget::clampMousePosition(QPoint point) { } bool HistoryWidget::touchScroll(const QPoint &delta) { - int32 scTop = _scroll->scrollTop(), scMax = _scroll->scrollTopMax(); + const auto scTop = _scroll->scrollTop(); + const auto scMax = _scroll->scrollTopMax(); const auto scNew = std::clamp(scTop - delta.y(), 0, scMax); - if (scNew == scTop) return false; - + if (scNew == scTop) { + return false; + } _scroll->scrollToY(scNew); return true; } diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index b106625c6..15fd316c3 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "base/qt/qt_key_modifiers.h" +#include "base/qt/qt_common_adapters.h" #include "history/history_message.h" #include "history/history_item_components.h" #include "history/history_item_text.h" @@ -39,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_peer_menu.h" #include "main/main_session.h" #include "ui/widgets/popup_menu.h" +#include "ui/widgets/scroll_area.h" #include "ui/toast/toast.h" #include "ui/toasts/common_toasts.h" #include "ui/inactive_press.h" @@ -291,7 +293,10 @@ ListWidget::ListWidget( , _highlighter( &session().data(), [=](const HistoryItem *item) { return viewForItem(item); }, - [=](const Element *view) { repaintItem(view); }) { + [=](const Element *view) { repaintItem(view); }) +, _touchSelectTimer([=] { onTouchSelect(); }) +, _touchScrollTimer([=] { onTouchScrollTimer(); }) { + setAttribute(Qt::WA_AcceptTouchEvents); setMouseTracking(true); _scrollDateHideTimer.setCallback([this] { scrollDateHideByTimer(); }); session().data().viewRepaintRequest( @@ -1904,6 +1909,20 @@ void ListWidget::paintEvent(QPaintEvent *e) { } } +bool ListWidget::eventHook(QEvent *e) { + if (e->type() == QEvent::TouchBegin + || e->type() == QEvent::TouchUpdate + || e->type() == QEvent::TouchEnd + || e->type() == QEvent::TouchCancel) { + QTouchEvent *ev = static_cast<QTouchEvent*>(e); + if (ev->device()->type() == base::TouchDevice::TouchScreen) { + touchEvent(ev); + return true; + } + } + return RpWidget::eventHook(e); +} + void ListWidget::applyDragSelection() { if (!hasSelectRestriction()) { applyDragSelection(_selected); @@ -2224,11 +2243,14 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { request.pointState = _overState.pointState; request.selectedText = _selectedText; request.selectedItems = collectSelectedItems(); - request.overSelection = showFromTouch - || (_overElement && isInsideSelection( - _overElement, - _overItemExact ? _overItemExact : _overElement->data().get(), - _overState)); + const auto hasSelection = !request.selectedItems.empty() + || !request.selectedText.empty(); + request.overSelection = (showFromTouch && hasSelection) + || (_overElement + && isInsideSelection( + _overElement, + _overItemExact ? _overItemExact : _overElement->data().get(), + _overState)); _menu = FillContextMenu(this, request); @@ -2301,6 +2323,188 @@ void ListWidget::mousePressEvent(QMouseEvent *e) { mouseActionStart(e->globalPos(), e->button()); } +void ListWidget::onTouchScrollTimer() { + auto nowTime = crl::now(); + if (_touchScrollState == Ui::TouchScrollState::Acceleration && _touchWaitingAcceleration && (nowTime - _touchAccelerationTime) > 40) { + _touchScrollState = Ui::TouchScrollState::Manual; + touchResetSpeed(); + } else if (_touchScrollState == Ui::TouchScrollState::Auto || _touchScrollState == Ui::TouchScrollState::Acceleration) { + const auto elapsed = int(nowTime - _touchTime); + const auto delta = _touchSpeed * elapsed / 1000; + const auto hasScrolled = _delegate->listScrollTo( + _visibleTop - delta.y()); + if (_touchSpeed.isNull() || !hasScrolled) { + _touchScrollState = Ui::TouchScrollState::Manual; + _touchScroll = false; + _touchScrollTimer.cancel(); + } else { + _touchTime = nowTime; + } + touchDeaccelerate(elapsed); + } +} + +void ListWidget::touchUpdateSpeed() { + const auto nowTime = crl::now(); + if (_touchPrevPosValid) { + const int elapsed = nowTime - _touchSpeedTime; + if (elapsed) { + const QPoint newPixelDiff = (_touchPos - _touchPrevPos); + const QPoint pixelsPerSecond = newPixelDiff * (1000 / elapsed); + + // fingers are inacurates, we ignore small changes to avoid stopping the autoscroll because + // of a small horizontal offset when scrolling vertically + const int newSpeedY = (qAbs(pixelsPerSecond.y()) > Ui::kFingerAccuracyThreshold) ? pixelsPerSecond.y() : 0; + const int newSpeedX = (qAbs(pixelsPerSecond.x()) > Ui::kFingerAccuracyThreshold) ? pixelsPerSecond.x() : 0; + if (_touchScrollState == Ui::TouchScrollState::Auto) { + const int oldSpeedY = _touchSpeed.y(); + const int oldSpeedX = _touchSpeed.x(); + if ((oldSpeedY <= 0 && newSpeedY <= 0) || ((oldSpeedY >= 0 && newSpeedY >= 0) + && (oldSpeedX <= 0 && newSpeedX <= 0)) || (oldSpeedX >= 0 && newSpeedX >= 0)) { + _touchSpeed.setY(std::clamp( + (oldSpeedY + (newSpeedY / 4)), + -Ui::kMaxScrollAccelerated, + +Ui::kMaxScrollAccelerated)); + _touchSpeed.setX(std::clamp( + (oldSpeedX + (newSpeedX / 4)), + -Ui::kMaxScrollAccelerated, + +Ui::kMaxScrollAccelerated)); + } else { + _touchSpeed = QPoint(); + } + } else { + // we average the speed to avoid strange effects with the last delta + if (!_touchSpeed.isNull()) { + _touchSpeed.setX(std::clamp( + (_touchSpeed.x() / 4) + (newSpeedX * 3 / 4), + -Ui::kMaxScrollFlick, + +Ui::kMaxScrollFlick)); + _touchSpeed.setY(std::clamp( + (_touchSpeed.y() / 4) + (newSpeedY * 3 / 4), + -Ui::kMaxScrollFlick, + +Ui::kMaxScrollFlick)); + } else { + _touchSpeed = QPoint(newSpeedX, newSpeedY); + } + } + } + } else { + _touchPrevPosValid = true; + } + _touchSpeedTime = nowTime; + _touchPrevPos = _touchPos; +} + +void ListWidget::touchResetSpeed() { + _touchSpeed = QPoint(); + _touchPrevPosValid = false; +} + +void ListWidget::touchDeaccelerate(int32 elapsed) { + int32 x = _touchSpeed.x(); + int32 y = _touchSpeed.y(); + _touchSpeed.setX((x == 0) ? x : (x > 0) ? qMax(0, x - elapsed) : qMin(0, x + elapsed)); + _touchSpeed.setY((y == 0) ? y : (y > 0) ? qMax(0, y - elapsed) : qMin(0, y + elapsed)); +} + +void ListWidget::touchEvent(QTouchEvent *e) { + if (e->type() == QEvent::TouchCancel) { // cancel + if (!_touchInProgress) return; + _touchInProgress = false; + _touchSelectTimer.cancel(); + _touchScroll = _touchSelect = false; + _touchScrollState = Ui::TouchScrollState::Manual; + mouseActionCancel(); + return; + } + + if (!e->touchPoints().isEmpty()) { + _touchPrevPos = _touchPos; + _touchPos = e->touchPoints().cbegin()->screenPos().toPoint(); + } + + switch (e->type()) { + case QEvent::TouchBegin: { + if (_menu) { + e->accept(); + return; // ignore mouse press, that was hiding context menu + } + if (_touchInProgress) return; + if (e->touchPoints().isEmpty()) return; + + _touchInProgress = true; + if (_touchScrollState == Ui::TouchScrollState::Auto) { + _touchScrollState = Ui::TouchScrollState::Acceleration; + _touchWaitingAcceleration = true; + _touchAccelerationTime = crl::now(); + touchUpdateSpeed(); + _touchStart = _touchPos; + } else { + _touchScroll = false; + _touchSelectTimer.callOnce(QApplication::startDragTime()); + } + _touchSelect = false; + _touchStart = _touchPrevPos = _touchPos; + } break; + + case QEvent::TouchUpdate: { + if (!_touchInProgress) return; + if (_touchSelect) { + mouseActionUpdate(_touchPos); + } else if (!_touchScroll && (_touchPos - _touchStart).manhattanLength() >= QApplication::startDragDistance()) { + _touchSelectTimer.cancel(); + _touchScroll = true; + touchUpdateSpeed(); + } + if (_touchScroll) { + if (_touchScrollState == Ui::TouchScrollState::Manual) { + touchScrollUpdated(_touchPos); + } else if (_touchScrollState == Ui::TouchScrollState::Acceleration) { + touchUpdateSpeed(); + _touchAccelerationTime = crl::now(); + if (_touchSpeed.isNull()) { + _touchScrollState = Ui::TouchScrollState::Manual; + } + } + } + } break; + + case QEvent::TouchEnd: { + if (!_touchInProgress) return; + _touchInProgress = false; + auto weak = Ui::MakeWeak(this); + if (_touchSelect) { + mouseActionFinish(_touchPos, Qt::RightButton); + QContextMenuEvent contextMenu(QContextMenuEvent::Mouse, mapFromGlobal(_touchPos), _touchPos); + showContextMenu(&contextMenu, true); + _touchScroll = false; + } else if (_touchScroll) { + if (_touchScrollState == Ui::TouchScrollState::Manual) { + _touchScrollState = Ui::TouchScrollState::Auto; + _touchPrevPosValid = false; + _touchScrollTimer.callEach(15); + _touchTime = crl::now(); + } else if (_touchScrollState == Ui::TouchScrollState::Auto) { + _touchScrollState = Ui::TouchScrollState::Manual; + _touchScroll = false; + touchResetSpeed(); + } else if (_touchScrollState == Ui::TouchScrollState::Acceleration) { + _touchScrollState = Ui::TouchScrollState::Auto; + _touchWaitingAcceleration = false; + _touchPrevPosValid = false; + } + } else { // One short tap is like left mouse click. + mouseActionStart(_touchPos, Qt::LeftButton); + mouseActionFinish(_touchPos, Qt::LeftButton); + } + if (weak) { + _touchSelectTimer.cancel(); + _touchSelect = false; + } + } break; + } +} + void ListWidget::mouseMoveEvent(QMouseEvent *e) { static auto lastGlobalPosition = e->globalPos(); auto reallyMoved = (lastGlobalPosition != e->globalPos()); @@ -2326,6 +2530,12 @@ void ListWidget::mouseReleaseEvent(QMouseEvent *e) { } } +void ListWidget::touchScrollUpdated(const QPoint &screenPos) { + _touchPos = screenPos; + _delegate->listScrollTo(_visibleTop - (_touchPos - _touchPrevPos).y()); + touchUpdateSpeed(); +} + void ListWidget::enterEventHook(QEnterEvent *e) { mouseActionUpdate(QCursor::pos()); return TWidget::enterEventHook(e); @@ -2378,6 +2588,11 @@ void ListWidget::updateDragSelection() { updateDragSelection(fromView, fromState, tillView, tillState); } +void ListWidget::onTouchSelect() { + _touchSelect = true; + mouseActionStart(_touchPos, Qt::LeftButton); +} + void ListWidget::updateDragSelection( const Element *fromView, const MouseState &fromState, diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 673c3c8b0..aeabfc562 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -26,6 +26,7 @@ namespace Ui { class PopupMenu; class ChatTheme; struct ChatPaintContext; +enum class TouchScrollState; } // namespace Ui namespace Window { @@ -86,7 +87,7 @@ using SelectedItems = std::vector<SelectedItem>; class ListDelegate { public: virtual Context listContext() = 0; - virtual void listScrollTo(int top) = 0; + virtual bool listScrollTo(int top) = 0; // true if scroll was changed. virtual void listCancelRequest() = 0; virtual void listDeleteRequest() = 0; virtual rpl::producer<Data::MessagesSlice> listSource( @@ -224,6 +225,8 @@ public: void selectItem(not_null<HistoryItem*> item); void selectItemAsGroup(not_null<HistoryItem*> item); + void touchScrollUpdated(const QPoint &screenPos); + [[nodiscard]] bool loadedAtTopKnown() const; [[nodiscard]] bool loadedAtTop() const; [[nodiscard]] bool loadedAtBottomKnown() const; @@ -312,6 +315,8 @@ protected: int visibleTop, int visibleBottom) override; + bool eventHook(QEvent *e) override; // calls touchEvent when necessary + void touchEvent(QTouchEvent *e); void paintEvent(QPaintEvent *e) override; void keyPressEvent(QKeyEvent *e) override; void mousePressEvent(QMouseEvent *e) override; @@ -381,6 +386,9 @@ private: using CursorState = HistoryView::CursorState; using ChosenReaction = HistoryView::Reactions::ChosenReaction; + void onTouchSelect(); + void onTouchScrollTimer(); + void refreshViewer(); void updateAroundPositionFromNearest(int nearestIndex); void refreshRows(const Data::MessagesSlice &old); @@ -417,6 +425,10 @@ private: void showContextMenu(QContextMenuEvent *e, bool showFromTouch = false); void reactionChosen(ChosenReaction reaction); + void touchResetSpeed(); + void touchUpdateSpeed(); + void touchDeaccelerate(int32 elapsed); + [[nodiscard]] int findItemIndexByY(int y) const; [[nodiscard]] not_null<Element*> findItemByY(int y) const; [[nodiscard]] Element *strictFindItemByY(int y) const; @@ -645,10 +657,26 @@ private: ElementHighlighter _highlighter; + // scroll by touch support (at least Windows Surface tablets) + bool _touchScroll = false; + bool _touchSelect = false; + bool _touchInProgress = false; + QPoint _touchStart, _touchPrevPos, _touchPos; + base::Timer _touchSelectTimer; + Ui::DraggingScrollManager _selectScroll; InfoTooltip _topToast; + Ui::TouchScrollState _touchScrollState = Ui::TouchScrollState(); + bool _touchPrevPosValid = false; + bool _touchWaitingAcceleration = false; + QPoint _touchSpeed; + crl::time _touchSpeedTime = 0; + crl::time _touchAccelerationTime = 0; + crl::time _touchTime = 0; + base::Timer _touchScrollTimer; + rpl::event_stream<FullMsgId> _requestedToEditMessage; rpl::event_stream<FullMsgId> _requestedToReplyToMessage; rpl::event_stream<FullMsgId> _requestedToReadMessage; diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index 959f4abfa..7ae95e74b 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -517,12 +517,14 @@ Context PinnedWidget::listContext() { return Context::Pinned; } -void PinnedWidget::listScrollTo(int top) { - if (_scroll->scrollTop() != top) { - _scroll->scrollToY(top); - } else { +bool PinnedWidget::listScrollTo(int top) { + top = std::clamp(top, 0, _scroll->scrollTopMax()); + if (_scroll->scrollTop() == top) { updateInnerVisibleArea(); + return false; } + _scroll->scrollToY(top); + return true; } void PinnedWidget::listCancelRequest() { diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.h b/Telegram/SourceFiles/history/view/history_view_pinned_section.h index 25a7bc617..9b370f39c 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.h +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.h @@ -77,7 +77,7 @@ public: // ListDelegate interface. Context listContext() override; - void listScrollTo(int top) override; + bool listScrollTo(int top) override; void listCancelRequest() override; void listDeleteRequest() override; rpl::producer<Data::MessagesSlice> listSource( diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 3a1b38450..07b32e33d 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -1425,11 +1425,7 @@ bool RepliesWidget::showAtPositionNow( : (std::abs(fullDelta) > limit) ? AnimatedScroll::Part : AnimatedScroll::Full; - _inner->scrollTo( - wanted, - use, - scrollDelta, - type); + _inner->scrollTo(wanted, use, scrollDelta, type); if (use != Data::MaxMessagePosition && use != Data::UnreadMessagePosition) { _inner->highlightMessage(use.fullId); @@ -1839,12 +1835,14 @@ Context RepliesWidget::listContext() { return Context::Replies; } -void RepliesWidget::listScrollTo(int top) { - if (_scroll->scrollTop() != top) { - _scroll->scrollToY(top); - } else { +bool RepliesWidget::listScrollTo(int top) { + top = std::clamp(top, 0, _scroll->scrollTopMax()); + if (_scroll->scrollTop() == top) { updateInnerVisibleArea(); + return false; } + _scroll->scrollToY(top); + return true; } void RepliesWidget::listCancelRequest() { diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h index 043197a62..071f4ff9c 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -114,7 +114,7 @@ public: // ListDelegate interface. Context listContext() override; - void listScrollTo(int top) override; + bool listScrollTo(int top) override; void listCancelRequest() override; void listDeleteRequest() override; rpl::producer<Data::MessagesSlice> listSource( diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 97acfa879..55fc9114f 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -1160,12 +1160,14 @@ Context ScheduledWidget::listContext() { return Context::History; } -void ScheduledWidget::listScrollTo(int top) { - if (_scroll->scrollTop() != top) { - _scroll->scrollToY(top); - } else { +bool ScheduledWidget::listScrollTo(int top) { + top = std::clamp(top, 0, _scroll->scrollTopMax()); + if (_scroll->scrollTop() == top) { updateInnerVisibleArea(); + return false; } + _scroll->scrollToY(top); + return true; } void ScheduledWidget::listCancelRequest() { diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index c321cf35b..a2787280e 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -99,7 +99,7 @@ public: // ListDelegate interface. Context listContext() override; - void listScrollTo(int top) override; + bool listScrollTo(int top) override; void listCancelRequest() override; void listDeleteRequest() override; rpl::producer<Data::MessagesSlice> listSource(