From d62f29449410e02e6bd3031fc0a45fcbf276d9ef Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 24 Feb 2026 12:30:13 +0300 Subject: [PATCH] Added initial support of auto-scroll with middle click to history view. Fixed #24518. --- .../history/history_inner_widget.cpp | 33 +++ .../history/history_inner_widget.h | 2 + .../history/view/history_view_list_widget.cpp | 35 ++- .../history/view/history_view_list_widget.h | 2 + .../ui/widgets/middle_click_autoscroll.cpp | 228 ++++++++++++++++++ .../ui/widgets/middle_click_autoscroll.h | 53 ++++ Telegram/cmake/td_ui.cmake | 2 + 7 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.cpp create mode 100644 Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.h diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index dc01301f7d..114d45fc4c 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -333,6 +333,11 @@ HistoryInner::HistoryInner( [=](QRect updated) { update(updated); })) , _touchSelectTimer([=] { onTouchSelect(); }) , _touchScrollTimer([=] { onTouchScrollTimer(); }) +, _middleClickAutoscroll( + [=](int d) { _scroll->scrollToY(_scroll->scrollTop() + d); }, + [=](const QCursor &cursor) { setCursor(cursor); }, + [=] { mouseActionUpdate(QCursor::pos()); setCursor(_cursor); }, + [=] { return window()->isActiveWindow(); }) , _scrollDateCheck([this] { scrollDateCheck(); }) , _scrollDateHideTimer([this] { scrollDateHideByTimer(); }) { _history->delegateMixin()->setCurrent(this); @@ -1734,6 +1739,14 @@ void HistoryInner::touchEvent(QTouchEvent *e) { void HistoryInner::mouseMoveEvent(QMouseEvent *e) { static auto lastGlobalPosition = e->globalPos(); auto reallyMoved = (lastGlobalPosition != e->globalPos()); + if (_middleClickAutoscroll.active()) { + if (reallyMoved) { + _mouseActive = true; + lastGlobalPosition = e->globalPos(); + } + e->accept(); + return; + } auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton)); if (!buttonsPressed && _mouseAction != MouseAction::None) { mouseReleaseEvent(e); @@ -1786,6 +1799,18 @@ void HistoryInner::mousePressEvent(QMouseEvent *e) { e->accept(); return; // ignore mouse press, that was hiding context menu } + if (_middleClickAutoscroll.active()) { + _middleClickAutoscroll.stop(); + e->accept(); + return; + } + if (e->button() == Qt::MiddleButton) { + mouseActionCancel(); + ClickHandler::unpressed(); + _middleClickAutoscroll.start(e->globalPos()); + e->accept(); + return; + } _mouseActive = true; mouseActionStart(e->globalPos(), e->button()); } @@ -2196,6 +2221,10 @@ void HistoryInner::mouseActionFinish( } void HistoryInner::mouseReleaseEvent(QMouseEvent *e) { + if (_middleClickAutoscroll.active()) { + e->accept(); + return; + } mouseActionFinish(e->globalPos(), e->button()); if (!rect().contains(e->pos())) { leaveEvent(e); @@ -3503,6 +3532,10 @@ TextForMimeData HistoryInner::getSelectedText() const { } void HistoryInner::keyPressEvent(QKeyEvent *e) { + if (_middleClickAutoscroll.active() && e->key() == Qt::Key_Escape) { + _middleClickAutoscroll.stop(); + return; + } if (e->key() == Qt::Key_Escape) { _widget->escape(); } else if (e == QKeySequence::Copy && !_selected.empty()) { diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 7debbea883..4141fb5b67 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/swipe_handler_data.h" #include "ui/effects/animations.h" #include "ui/dragging_scroll_manager.h" +#include "ui/widgets/middle_click_autoscroll.h" #include "ui/widgets/tooltip.h" #include "ui/widgets/scroll_area.h" #include "ui/userpic_view.h" @@ -560,6 +561,7 @@ private: crl::time _touchAccelerationTime = 0; crl::time _touchTime = 0; base::Timer _touchScrollTimer; + Ui::MiddleClickAutoscroll _middleClickAutoscroll; Ui::Controls::SwipeContextData _gestureHorizontal; Ui::Controls::SwipeBackResult _swipeBackData; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index a8fd3ea847..57de39bce0 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -413,7 +413,12 @@ ListWidget::ListWidget( [=](const HistoryItem *item) { return viewForItem(item); }, [=](const Element *view) { repaintItem(view); }) , _touchSelectTimer([=] { onTouchSelect(); }) -, _touchScrollTimer([=] { onTouchScrollTimer(); }) { +, _touchScrollTimer([=] { onTouchScrollTimer(); }) +, _middleClickAutoscroll( + [=](int d) { _delegate->listScrollTo(_visibleTop + d, false); }, + [=](const QCursor &cursor) { setCursor(cursor); }, + [=] { mouseActionUpdate(QCursor::pos()); setCursor(_cursor); }, + [=] { return window()->isActiveWindow(); }) { setAttribute(Qt::WA_AcceptTouchEvents); setMouseTracking(true); _scrollDateHideTimer.setCallback([this] { scrollDateHideByTimer(); }); @@ -2699,6 +2704,10 @@ void ListWidget::keyPressEvent(QKeyEvent *e) { const auto hasModifiers = (Qt::NoModifier != (e->modifiers() & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier))); + if (_middleClickAutoscroll.active() && key == Qt::Key_Escape) { + _middleClickAutoscroll.stop(); + return; + } if (key == Qt::Key_Escape || key == Qt::Key_Back) { if (hasSelectedText() || hasSelectedItems()) { cancelSelection(); @@ -3003,6 +3012,18 @@ void ListWidget::mousePressEvent(QMouseEvent *e) { e->accept(); return; // ignore mouse press, that was hiding context menu } + if (_middleClickAutoscroll.active()) { + _middleClickAutoscroll.stop(); + e->accept(); + return; + } + if (e->button() == Qt::MiddleButton) { + mouseActionCancel(); + ClickHandler::unpressed(); + _middleClickAutoscroll.start(e->globalPos()); + e->accept(); + return; + } _mouseActive = true; mouseActionStart(e->globalPos(), e->button()); } @@ -3206,6 +3227,14 @@ void ListWidget::touchEvent(QTouchEvent *e) { void ListWidget::mouseMoveEvent(QMouseEvent *e) { static auto lastGlobalPosition = e->globalPos(); auto reallyMoved = (lastGlobalPosition != e->globalPos()); + if (_middleClickAutoscroll.active()) { + if (reallyMoved) { + _mouseActive = true; + lastGlobalPosition = e->globalPos(); + } + e->accept(); + return; + } auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton)); if (!buttonsPressed && _mouseAction != MouseAction::None) { mouseReleaseEvent(e); @@ -3223,6 +3252,10 @@ void ListWidget::mouseMoveEvent(QMouseEvent *e) { } void ListWidget::mouseReleaseEvent(QMouseEvent *e) { + if (_middleClickAutoscroll.active()) { + e->accept(); + return; + } mouseActionFinish(e->globalPos(), e->button()); if (!rect().contains(e->pos())) { leaveEvent(e); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 4caeb5a855..a0d0bec38f 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rp_widget.h" #include "ui/effects/animations.h" #include "ui/dragging_scroll_manager.h" +#include "ui/widgets/middle_click_autoscroll.h" #include "ui/widgets/tooltip.h" #include "mtproto/sender.h" #include "data/data_messages.h" @@ -867,6 +868,7 @@ private: crl::time _touchAccelerationTime = 0; crl::time _touchTime = 0; base::Timer _touchScrollTimer; + Ui::MiddleClickAutoscroll _middleClickAutoscroll; rpl::event_stream _requestedToEditMessage; rpl::event_stream _requestedToReplyToMessage; diff --git a/Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.cpp b/Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.cpp new file mode 100644 index 0000000000..5dfe2889cd --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.cpp @@ -0,0 +1,228 @@ +/* +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 "ui/widgets/middle_click_autoscroll.h" + +#include "styles/style_widgets.h" +#include "ui/effects/animation_value.h" +#include "ui/rect.h" + +namespace Ui { +namespace { + +constexpr auto kDelay = 15; + +[[nodiscard]] int Deadzone() { + return std::max(1, int(std::lround(style::ConvertFloatScale(6.)))); +} + +[[nodiscard]] float64 SpeedScale() { + return style::ConvertFloatScale(30.); +} + +[[nodiscard]] float64 MaxSpeed() { + return style::ConvertFloatScale(3600.); +} + +[[nodiscard]] QImage PaintCursorImage( + int size, + int factor, + bool drawOuter, + bool drawTopArrow, + bool drawBottomArrow, + const QColor &inner, + const QColor &indicator, + const QColor &topArrow, + const QColor &bottomArrow) { + auto image = QImage( + QSize(size, size) * factor, + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(factor); + image.fill(Qt::transparent); + auto p = QPainter(&image); + p.setRenderHint(QPainter::Antialiasing); + p.setPen(Qt::NoPen); + + const auto outerRect = QRectF(QPointF(0.5, 0.5), Size(size - 1.)); + if (drawOuter) { + const auto outerStroke = std::max(1., style::ConvertFloatScale(1.5)); + auto pen = QPen(indicator); + pen.setWidthF(outerStroke); + p.setPen(pen); + p.setBrush(inner); + p.drawEllipse(outerRect.marginsRemoved(Margins(outerStroke * 0.5))); + } + p.setPen(Qt::NoPen); + const auto centerSkip = style::ConvertFloatScale(10.5); + const auto centerRect = outerRect - Margins(centerSkip); + p.setBrush(indicator); + p.drawEllipse(centerRect); + + const auto middle = size * 0.5; + const auto arrowGap = style::ConvertFloatScale(5.); + const auto headHalfWidth = style::ConvertFloatScale(4.5); + const auto headHeight = style::ConvertFloatScale(3.5); + const auto stroke = std::max(1., style::ConvertFloatScale(2.)); + const auto drawArrow = [&](bool up, const QColor &color) { + auto pen = QPen(color); + pen.setWidthF(stroke); + pen.setCapStyle(Qt::RoundCap); + pen.setJoinStyle(Qt::RoundJoin); + p.setPen(pen); + const auto dir = up ? -1. : 1.; + const auto tipY = middle + dir * (arrowGap + headHeight); + const auto neckY = tipY - dir * headHeight; + p.drawLine( + QPointF(middle, tipY), + QPointF(middle - headHalfWidth, neckY)); + p.drawLine( + QPointF(middle, tipY), + QPointF(middle + headHalfWidth, neckY)); + }; + if (drawTopArrow) { + drawArrow(true, topArrow); + } + if (drawBottomArrow) { + drawArrow(false, bottomArrow); + } + return image; +} + +} // namespace + +MiddleClickAutoscroll::MiddleClickAutoscroll( + Fn scrollBy, + Fn applyCursor, + Fn restoreCursor, + Fn shouldContinue) +: _scrollBy(std::move(scrollBy)) +, _applyCursor(std::move(applyCursor)) +, _restoreCursor(std::move(restoreCursor)) +, _shouldContinue(std::move(shouldContinue)) +, _timer([=] { onTimer(); }) { +} + +void MiddleClickAutoscroll::start(const QPoint &globalPosition) { + if (_active) { + stop(); + } + _active = true; + _startPosition = globalPosition; + _time = crl::now(); + updateCursor(QCursor::pos().y() - _startPosition.y()); + _timer.callEach(kDelay); +} + +void MiddleClickAutoscroll::stop() { + if (!_active) { + return; + } + _active = false; + _timer.cancel(); + if (_restoreCursor) { + _restoreCursor(); + } +} + +void MiddleClickAutoscroll::updateCursor(int delta) { + if (!_applyCursor) { + return; + } + const auto mode = (std::abs(delta) <= Deadzone()) + ? CursorMode::Neutral + : (delta < 0) + ? CursorMode::Up + : CursorMode::Down; + _applyCursor(makeCursor(mode)); +} + +QCursor MiddleClickAutoscroll::makeCursor(CursorMode mode) { + struct CachedCursor { + CursorMode mode = CursorMode::Neutral; + int size = 0; + int factor = 0; + QColor inner; + QColor indicator; + QCursor cursor; + }; + const auto size = std::max( + 1, + int(std::lround(style::ConvertFloatScale(27.)))); + const auto factor = style::DevicePixelRatio(); + const auto inner = anim::with_alpha(st::windowBg->c, (240. / 255.)); + const auto active = anim::with_alpha(st::windowFg->c, (210. / 255.)); + const auto passive = anim::with_alpha( + st::windowSubTextFg->c, + (210. / 255.)); + const auto neutral = (mode == CursorMode::Neutral); + const auto indicator = neutral ? passive : active; + static auto cache = std::vector(); + if (const auto i = ranges::find_if( + cache, + [&](const CachedCursor &entry) { + return (entry.mode == mode) + && (entry.size == size) + && (entry.factor == factor) + && (entry.inner == inner) + && (entry.indicator == indicator); + }); + i != cache.end()) { + return i->cursor; + } + const auto image = PaintCursorImage( + size, + factor, + neutral, + mode != CursorMode::Down, + mode != CursorMode::Up, + inner, + indicator, + indicator, + indicator); + const auto hot = int(std::floor(size * 0.5)); + auto cursor = QCursor(QPixmap::fromImage(image), hot, hot); + cache.push_back(CachedCursor{ + .mode = mode, + .size = size, + .factor = factor, + .inner = inner, + .indicator = indicator, + .cursor = cursor, + }); + return cursor; +} + +void MiddleClickAutoscroll::onTimer() { + if (!_active) { + return; + } else if (_shouldContinue && !_shouldContinue()) { + stop(); + return; + } + const auto now = crl::now(); + const auto elapsed = std::max(now - _time, 1LL); + _time = now; + const auto delta = QCursor::pos().y() - _startPosition.y(); + updateCursor(delta); + const auto absolute = std::abs(delta) - Deadzone(); + if (absolute <= 0) { + return; + } + const auto speed = std::min( + absolute * SpeedScale(), + MaxSpeed()); + auto scroll = int(std::lround((speed * elapsed) / 1000.)); + if (scroll <= 0) { + scroll = 1; + } + if (delta < 0) { + scroll = -scroll; + } + _scrollBy(scroll); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.h b/Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.h new file mode 100644 index 0000000000..62702ce10a --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/middle_click_autoscroll.h @@ -0,0 +1,53 @@ +/* +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 + +#include "base/timer.h" + +class QCursor; + +namespace Ui { + +class MiddleClickAutoscroll final { +public: + explicit MiddleClickAutoscroll( + Fn scrollBy, + Fn applyCursor = nullptr, + Fn restoreCursor = nullptr, + Fn shouldContinue = nullptr); + + [[nodiscard]] bool active() const { + return _active; + } + + void start(const QPoint &globalPosition); + void stop(); + +private: + enum class CursorMode { + Neutral, + Up, + Down, + }; + + void updateCursor(int delta); + void onTimer(); + [[nodiscard]] static QCursor makeCursor(CursorMode mode); + + Fn _scrollBy; + Fn _applyCursor; + Fn _restoreCursor; + Fn _shouldContinue; + bool _active = false; + QPoint _startPosition; + crl::time _time = 0; + base::Timer _timer; + +}; + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 0d3377c3a0..2aa6bbc804 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -513,6 +513,8 @@ PRIVATE ui/widgets/horizontal_fit_container.h ui/widgets/level_meter.cpp ui/widgets/level_meter.h + ui/widgets/middle_click_autoscroll.cpp + ui/widgets/middle_click_autoscroll.h ui/widgets/multi_select.cpp ui/widgets/multi_select.h ui/widgets/sent_code_field.cpp