Use touch handling from HistoryInner in ListWidget.

Fixes #24880.
This commit is contained in:
John Preston 2022-09-19 11:16:09 +04:00
parent 7727cdff92
commit 39294a7fe1
9 changed files with 277 additions and 30 deletions

View File

@ -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;
}

View File

@ -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,

View File

@ -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;

View File

@ -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() {

View File

@ -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(

View File

@ -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() {

View File

@ -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(

View File

@ -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() {

View File

@ -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(