Scroll Using Click-and-Drag with Middle Mouse Button (#1559)

* ChannelView: Rename mouse event related members

This is more in line with the naming of the other members as well as
future members.

* ChannelView: Add ability to scroll with middle mouse button

* Add scrolling resources

* Use custom icons for scroll cursor

* Slightly refactor scrolling logic

* Respect screen scaling when calculating scroll offset

* Nicer scrolling UX

This change allows scrolling to be feel smoother when close to the
starting point.

* Add scrolling with keeping middle mouse pressed

This mimics the behavior of browsers as well.

* Refactor ChannelView::enableScrolling

* Disable drag-scrolling on left or right click
This commit is contained in:
Leon Richardt 2020-04-18 11:09:22 +02:00 committed by GitHub
parent 2076715e23
commit b4a2ced180
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 448 additions and 13 deletions

View file

@ -28,8 +28,9 @@
<file>buttons/unmod.png</file>
<file>buttons/update.png</file>
<file>buttons/updateError.png</file>
<file>com.chatterino.chatterino.desktop</file>
<file>chatterino.icns</file>
<file>com.chatterino.chatterino.appdata.xml</file>
<file>com.chatterino.chatterino.desktop</file>
<file>contributors.txt</file>
<file>emoji.json</file>
<file>emojidata.txt</file>
@ -50,6 +51,12 @@
<file>licenses/websocketpp.txt</file>
<file>pajaDank.png</file>
<file>qss/settings.qss</file>
<file>scrolling/downScroll.png</file>
<file>scrolling/downScroll.svg</file>
<file>scrolling/neutralScroll.png</file>
<file>scrolling/neutralScroll.svg</file>
<file>scrolling/upScroll.png</file>
<file>scrolling/upScroll.svg</file>
<file>settings/about.svg</file>
<file>settings/aboutlogo.png</file>
<file>settings/accounts.svg</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666665 8.4666669"
version="1.1"
id="svg8"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="downScroll.svg"
inkscape:export-filename="/home/leon/Projects/Chatterino/chatterino2/resources/scrolling/neutralScroll.png"
inkscape:export-xdpi="7.6051788"
inkscape:export-ydpi="7.6051788">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="14.417768"
inkscape:cy="15.865661"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1918"
inkscape:window-height="1053"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
units="px"
inkscape:pagecheckerboard="true">
<inkscape:grid
type="xygrid"
id="grid4532" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-288.53332)">
<circle
style="fill:#f6f6f6;fill-opacity:1;stroke:#000000;stroke-width:0.03761128;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3719"
cx="4.2333331"
cy="292.76666"
r="4.2145276" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.05049507;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4526"
cx="4.2333317"
cy="292.76425"
r="0.72627902" />
<path
sodipodi:type="star"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56499994;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4538"
sodipodi:sides="3"
sodipodi:cx="99.21875"
sodipodi:cy="155.44792"
sodipodi:r1="13.229166"
sodipodi:r2="6.614583"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 112.44792,155.44792 -19.843753,11.4568 v -22.91359 z"
inkscape:transform-center-x="5.0649804e-06"
inkscape:transform-center-y="2.2264812"
transform="matrix(0,0.09239709,-0.09239709,0,18.596269,286.19867)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666665 8.4666669"
version="1.1"
id="svg8"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="neutralScroll.svg"
inkscape:export-filename="/home/leon/Projects/Chatterino/chatterino2/resources/scrolling/neutralScroll.png"
inkscape:export-xdpi="7.6051788"
inkscape:export-ydpi="7.6051788">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="25.340042"
inkscape:cy="15.865661"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1918"
inkscape:window-height="1053"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
units="px"
inkscape:pagecheckerboard="true">
<inkscape:grid
type="xygrid"
id="grid4532" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-288.53332)">
<g
id="g5271"
transform="matrix(0.07922061,0,0,0.07922061,-4.1782222,281.17363)">
<circle
r="53.199886"
cy="146.33852"
cx="106.17888"
id="path3719"
style="fill:#f6f6f6;fill-opacity:1;stroke:#000000;stroke-width:0.47476631;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<g
transform="matrix(1.7324984,0,0,1.7324984,-77.775866,-107.19273)"
id="g4665">
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.36790693;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4526"
cx="106.17887"
cy="146.32092"
r="5.2916665" />
<path
sodipodi:type="star"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56499994;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4538"
sodipodi:sides="3"
sodipodi:cx="99.21875"
sodipodi:cy="155.44792"
sodipodi:r1="13.229166"
sodipodi:r2="6.614583"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 112.44792,155.44792 -19.843753,11.4568 v -22.91359 z"
inkscape:transform-center-x="5.0649804e-06"
inkscape:transform-center-y="2.2264812"
transform="matrix(0,0.67320493,-0.67320493,0,210.82719,98.484167)" />
<path
transform="matrix(0,-0.67320493,-0.67320493,0,210.82719,194.19287)"
inkscape:transform-center-y="-2.2264854"
inkscape:transform-center-x="5.0649804e-06"
d="m 112.44792,155.44792 -19.843753,11.4568 v -22.91359 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="true"
sodipodi:arg2="1.0471976"
sodipodi:arg1="0"
sodipodi:r2="6.614583"
sodipodi:r1="13.229166"
sodipodi:cy="155.44792"
sodipodi:cx="99.21875"
sodipodi:sides="3"
id="path4540"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56499994;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="star" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666665 8.4666669"
version="1.1"
id="svg8"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="upScroll.svg"
inkscape:export-filename="/home/leon/Projects/Chatterino/chatterino2/resources/scrolling/neutralScroll.png"
inkscape:export-xdpi="7.6051788"
inkscape:export-ydpi="7.6051788">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="14.417768"
inkscape:cy="15.865661"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1918"
inkscape:window-height="1053"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
units="px"
inkscape:pagecheckerboard="true">
<inkscape:grid
type="xygrid"
id="grid4532" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-288.53332)">
<circle
style="fill:#f6f6f6;fill-opacity:1;stroke:#000000;stroke-width:0.03761128;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3719"
cx="4.2333331"
cy="292.76666"
r="4.2145276" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.05049507;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4526"
cx="4.2333317"
cy="292.76425"
r="0.72627902" />
<path
transform="matrix(0,-0.09239709,-0.09239709,0,18.596269,299.33465)"
inkscape:transform-center-y="-2.2264854"
inkscape:transform-center-x="5.0649804e-06"
d="m 112.44792,155.44792 -19.843753,11.4568 v -22.91359 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="true"
sodipodi:arg2="1.0471976"
sodipodi:arg1="0"
sodipodi:r2="6.614583"
sodipodi:r1="13.229166"
sodipodi:cy="155.44792"
sodipodi:cx="99.21875"
sodipodi:sides="3"
id="path4540"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56499994;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="star" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -30,6 +30,9 @@ Resources2::Resources2()
this->error = QPixmap(":/error.png");
this->icon = QPixmap(":/icon.png");
this->pajaDank = QPixmap(":/pajaDank.png");
this->scrolling.downScroll = QPixmap(":/scrolling/downScroll.png");
this->scrolling.neutralScroll = QPixmap(":/scrolling/neutralScroll.png");
this->scrolling.upScroll = QPixmap(":/scrolling/upScroll.png");
this->settings.aboutlogo = QPixmap(":/settings/aboutlogo.png");
this->split.down = QPixmap(":/split/down.png");
this->split.left = QPixmap(":/split/left.png");
@ -50,4 +53,4 @@ Resources2::Resources2()
this->twitch.vip = QPixmap(":/twitch/vip.png");
}
} // namespace chatterino
} // namespace chatterino

View file

@ -1,5 +1,4 @@
#include <QPixmap>
#include "common/Singleton.hpp"
namespace chatterino {
@ -39,6 +38,11 @@ public:
QPixmap error;
QPixmap icon;
QPixmap pajaDank;
struct {
QPixmap downScroll;
QPixmap neutralScroll;
QPixmap upScroll;
} scrolling;
struct {
QPixmap aboutlogo;
} settings;
@ -65,4 +69,4 @@ public:
} twitch;
};
} // namespace chatterino
} // namespace chatterino

View file

@ -6,6 +6,7 @@
#include <QGraphicsBlurEffect>
#include <QMessageBox>
#include <QPainter>
#include <QScreen>
#include <algorithm>
#include <chrono>
#include <cmath>
@ -25,6 +26,7 @@
#include "messages/layouts/MessageLayoutElement.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Resources.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "singletons/TooltipPreviewImage.hpp"
@ -114,6 +116,10 @@ ChannelView::ChannelView(BaseWidget *parent)
this->initializeScrollbar();
this->initializeSignals();
this->cursors_.neutral = QCursor(getResources().scrolling.neutralScroll);
this->cursors_.up = QCursor(getResources().scrolling.upScroll);
this->cursors_.down = QCursor(getResources().scrolling.downScroll);
this->pauseTimer_.setSingleShot(true);
QObject::connect(&this->pauseTimer_, &QTimer::timeout, this, [this] {
/// remove elements that are finite
@ -131,6 +137,10 @@ ChannelView::ChannelView(BaseWidget *parent)
this->clickTimer_->setSingleShot(true);
this->clickTimer_->setInterval(500);
this->scrollTimer_.setInterval(20);
QObject::connect(&this->scrollTimer_, &QTimer::timeout, this,
&ChannelView::scrollUpdateRequested);
this->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
}
@ -1060,8 +1070,13 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
return;
}
if (this->isScrolling_)
{
this->currentMousePosition_ = event->screenPos();
}
// is selecting
if (this->isMouseDown_)
if (this->isLeftMouseDown_)
{
// this->pause(PauseReason::Selecting, 300);
int index = layout->getSelectionIndex(relativePos);
@ -1326,8 +1341,11 @@ void ChannelView::mousePressEvent(QMouseEvent *event)
switch (event->button())
{
case Qt::LeftButton: {
this->lastPressPosition_ = event->screenPos();
this->isMouseDown_ = true;
if (this->isScrolling_)
this->disableScrolling();
this->lastLeftPressPosition_ = event->screenPos();
this->isLeftMouseDown_ = true;
if (layout->flags.has(MessageLayoutFlag::Collapsed))
return;
@ -1344,11 +1362,22 @@ void ChannelView::mousePressEvent(QMouseEvent *event)
break;
case Qt::RightButton: {
if (this->isScrolling_)
this->disableScrolling();
this->lastRightPressPosition_ = event->screenPos();
this->isRightMouseDown_ = true;
}
break;
case Qt::MiddleButton: {
if (this->isScrolling_)
this->disableScrolling();
else
this->enableScrolling(event->screenPos());
}
break;
default:;
}
@ -1373,11 +1402,11 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event)
return;
}
}
else if (this->isMouseDown_)
else if (this->isLeftMouseDown_)
{
this->isMouseDown_ = false;
this->isLeftMouseDown_ = false;
if (fabsf(distanceBetweenPoints(this->lastPressPosition_,
if (fabsf(distanceBetweenPoints(this->lastLeftPressPosition_,
event->screenPos())) > 15.f)
{
return;
@ -1405,6 +1434,13 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event)
return;
}
}
else if (event->button() == Qt::MiddleButton)
{
if (event->screenPos() == this->lastMiddlePressPosition_)
this->enableScrolling(event->screenPos());
else
this->disableScrolling();
}
else
{
// not left or right button
@ -1638,7 +1674,7 @@ void ChannelView::mouseDoubleClickEvent(QMouseEvent *event)
return;
}
if (!this->isMouseDown_)
if (!this->isLeftMouseDown_)
{
this->isDoubleClick_ = true;
@ -1803,4 +1839,59 @@ void ChannelView::getWordBounds(MessageLayout *layout,
wordEnd = wordStart + length;
}
void ChannelView::enableScrolling(const QPointF &scrollStart)
{
this->isScrolling_ = true;
this->lastMiddlePressPosition_ = scrollStart;
// The line below prevents a sudden jerk at the beginning
this->currentMousePosition_ = scrollStart;
this->scrollTimer_.start();
if (!QGuiApplication::overrideCursor())
QGuiApplication::setOverrideCursor(this->cursors_.neutral);
}
void ChannelView::disableScrolling()
{
this->isScrolling_ = false;
this->scrollTimer_.stop();
QGuiApplication::restoreOverrideCursor();
}
void ChannelView::scrollUpdateRequested()
{
const qreal dpi =
QGuiApplication::screenAt(this->pos())->devicePixelRatio();
const qreal delta = dpi * (this->currentMousePosition_.y() -
this->lastMiddlePressPosition_.y());
const int cursorHeight = this->cursors_.neutral.pixmap().height();
if (fabs(delta) <= cursorHeight * dpi)
{
/*
* If within an area close to the initial position, don't do any
* scrolling at all.
*/
QGuiApplication::changeOverrideCursor(this->cursors_.neutral);
return;
}
qreal offset;
if (delta > 0)
{
QGuiApplication::changeOverrideCursor(this->cursors_.down);
offset = delta - cursorHeight;
}
else
{
QGuiApplication::changeOverrideCursor(this->cursors_.up);
offset = delta + cursorHeight;
}
// "Good" feeling multiplier found by trial-and-error
const qreal multiplier = qreal(0.02);
this->scrollBar_->offset(multiplier * offset);
}
} // namespace chatterino

View file

@ -148,6 +148,9 @@ private:
void updatePauses();
void unpaused();
void enableScrolling(const QPointF &scrollStart);
void disableScrolling();
QTimer *layoutCooldown_;
bool layoutQueued_;
@ -183,15 +186,26 @@ private:
bool onlyUpdateEmotes_ = false;
// Mouse event variables
bool isMouseDown_ = false;
bool isLeftMouseDown_ = false;
bool isRightMouseDown_ = false;
bool isDoubleClick_ = false;
DoubleClickSelection doubleClickSelection_;
QPointF lastPressPosition_;
QPointF lastLeftPressPosition_;
QPointF lastRightPressPosition_;
QPointF lastDClickPosition_;
QTimer *clickTimer_;
bool isScrolling_ = false;
QPointF lastMiddlePressPosition_;
QPointF currentMousePosition_;
QTimer scrollTimer_;
struct {
QCursor neutral;
QCursor up;
QCursor down;
} cursors_;
Selection selection_;
bool selecting_ = false;
@ -211,6 +225,8 @@ private slots:
queueLayout();
update();
}
void scrollUpdateRequested();
};
} // namespace chatterino