Added initial support of auto-scroll with middle click to history view.

Fixed #24518.
This commit is contained in:
23rd
2026-02-24 12:30:13 +03:00
committed by John Preston
parent 46758a82a7
commit d62f294494
7 changed files with 354 additions and 1 deletions
@@ -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()) {
@@ -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;
@@ -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);
@@ -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<FullMsgId> _requestedToEditMessage;
rpl::event_stream<ReplyToMessageRequest> _requestedToReplyToMessage;
@@ -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<void(int)> scrollBy,
Fn<void(const QCursor &)> applyCursor,
Fn<void()> restoreCursor,
Fn<bool()> 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<CachedCursor>();
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
@@ -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<void(int)> scrollBy,
Fn<void(const QCursor &)> applyCursor = nullptr,
Fn<void()> restoreCursor = nullptr,
Fn<bool()> 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<void(int)> _scrollBy;
Fn<void(const QCursor &)> _applyCursor;
Fn<void()> _restoreCursor;
Fn<bool()> _shouldContinue;
bool _active = false;
QPoint _startPosition;
crl::time _time = 0;
base::Timer _timer;
};
} // namespace Ui
+2
View File
@@ -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