mirror-chatterino2/src/widgets/Scrollbar.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

471 lines
12 KiB
C++
Raw Normal View History

2018-06-26 14:09:39 +02:00
#include "widgets/Scrollbar.hpp"
#include "common/QLogging.hpp"
#include "singletons/Settings.hpp"
2018-06-28 20:03:04 +02:00
#include "singletons/Theme.hpp"
#include "singletons/WindowManager.hpp"
2018-06-26 14:09:39 +02:00
#include "widgets/helper/ChannelView.hpp"
2017-01-03 21:19:33 +01:00
2017-01-26 04:26:40 +01:00
#include <QMouseEvent>
2017-01-18 04:52:47 +01:00
#include <QPainter>
#include <QTimer>
#include <cmath>
2017-01-18 04:52:47 +01:00
namespace {
constexpr int MIN_THUMB_HEIGHT = 10;
/// Amount of messages to move by when clicking on the track
constexpr qreal SCROLL_DELTA = 5.0;
bool areClose(auto a, auto b)
{
return std::abs(a - b) <= 0.0001;
}
} // namespace
2017-01-18 04:33:30 +01:00
2017-04-14 17:52:22 +02:00
namespace chatterino {
2017-01-18 21:30:23 +01:00
Scrollbar::Scrollbar(size_t messagesLimit, ChannelView *parent)
: BaseWidget(parent)
2018-06-19 20:34:50 +02:00
, currentValueAnimation_(this, "currentValue_")
, highlights_(messagesLimit)
2017-04-12 17:46:44 +02:00
{
this->resize(static_cast<int>(16 * this->scale()), 100);
2018-06-13 13:27:10 +02:00
this->currentValueAnimation_.setDuration(150);
this->currentValueAnimation_.setEasingCurve(
QEasingCurve(QEasingCurve::OutCubic));
connect(&this->currentValueAnimation_, &QAbstractAnimation::finished, this,
&Scrollbar::resetBounds);
2017-04-12 17:46:44 +02:00
this->setMouseTracking(true);
2017-01-03 21:19:33 +01:00
}
boost::circular_buffer<ScrollbarHighlight> Scrollbar::getHighlights() const
2017-01-03 21:19:33 +01:00
{
return this->highlights_;
2017-01-03 21:19:33 +01:00
}
void Scrollbar::addHighlight(ScrollbarHighlight highlight)
2017-01-03 21:19:33 +01:00
{
this->highlights_.push_back(std::move(highlight));
2017-01-03 21:19:33 +01:00
}
void Scrollbar::addHighlightsAtStart(
const std::vector<ScrollbarHighlight> &highlights)
2017-01-03 21:19:33 +01:00
{
size_t nItems = std::min(highlights.size(), this->highlights_.capacity() -
this->highlights_.size());
2018-06-13 13:27:10 +02:00
if (nItems == 0)
{
return;
}
for (size_t i = 0; i < nItems; i++)
{
this->highlights_.push_front(highlights[highlights.size() - 1 - i]);
}
2018-06-13 13:27:10 +02:00
}
void Scrollbar::replaceHighlight(size_t index, ScrollbarHighlight replacement)
2018-06-13 13:27:10 +02:00
{
if (this->highlights_.size() <= index)
{
return;
}
this->highlights_[index] = std::move(replacement);
2018-06-13 13:27:10 +02:00
}
void Scrollbar::clearHighlights()
{
this->highlights_.clear();
}
2018-01-06 03:48:56 +01:00
void Scrollbar::scrollToBottom(bool animate)
2017-06-06 17:18:23 +02:00
{
this->setDesiredValue(this->getBottom(), animate);
2017-06-06 17:18:23 +02:00
}
void Scrollbar::scrollToTop(bool animate)
{
this->setDesiredValue(this->minimum_, animate);
}
2018-01-06 03:48:56 +01:00
bool Scrollbar::isAtBottom() const
2017-06-06 17:18:23 +02:00
{
2018-06-13 13:27:10 +02:00
return this->atBottom_;
2017-06-06 17:18:23 +02:00
}
2018-01-06 03:48:56 +01:00
void Scrollbar::setMaximum(qreal value)
2017-04-12 17:46:44 +02:00
{
2018-06-13 13:27:10 +02:00
this->maximum_ = value;
2017-04-12 17:46:44 +02:00
this->updateScroll();
}
void Scrollbar::offsetMaximum(qreal value)
{
this->maximum_ += value;
this->updateScroll();
}
void Scrollbar::resetBounds()
{
if (this->minimum_ > 0)
{
this->maximum_ -= this->minimum_;
this->desiredValue_ -= this->minimum_;
this->currentValue_ -= this->minimum_;
this->minimum_ = 0;
}
2017-04-12 17:46:44 +02:00
}
2018-01-06 03:48:56 +01:00
void Scrollbar::setMinimum(qreal value)
2017-04-12 17:46:44 +02:00
{
2018-06-13 13:27:10 +02:00
this->minimum_ = value;
2017-04-12 17:46:44 +02:00
this->updateScroll();
}
void Scrollbar::offsetMinimum(qreal value)
{
this->minimum_ += value;
if (this->minimum_ > this->desiredValue_)
{
this->scrollToTop();
}
this->updateScroll();
2017-04-12 17:46:44 +02:00
}
void Scrollbar::setPageSize(qreal value)
2017-04-12 17:46:44 +02:00
{
this->pageSize_ = value;
2017-04-12 17:46:44 +02:00
this->updateScroll();
2017-04-12 17:46:44 +02:00
}
2018-01-06 03:48:56 +01:00
void Scrollbar::setDesiredValue(qreal value, bool animated)
2017-04-12 17:46:44 +02:00
{
// this can't use std::clamp, because minimum_ < getBottom() isn't always
// true, which is a precondition for std::clamp
value = std::max(this->minimum_, std::min(this->getBottom(), value));
if (areClose(this->currentValue_, value))
2018-06-13 13:27:10 +02:00
{
// value has not changed
return;
}
this->desiredValue_ = value;
2017-04-12 17:46:44 +02:00
this->desiredValueChanged_.invoke();
this->atBottom_ = areClose(this->getBottom(), value);
2018-06-19 20:34:50 +02:00
if (animated && getSettings()->enableSmoothScrolling)
{
// stop() does not emit QAbstractAnimation::finished().
this->currentValueAnimation_.stop();
this->currentValueAnimation_.setStartValue(this->currentValue_);
this->currentValueAnimation_.setEndValue(value);
this->currentValueAnimation_.start();
}
else
{
if (this->currentValueAnimation_.state() != QPropertyAnimation::Stopped)
{
2018-06-13 13:27:10 +02:00
this->currentValueAnimation_.setEndValue(value);
2017-04-12 17:46:44 +02:00
}
else
{
this->setCurrentValue(value);
this->resetBounds();
2017-04-12 17:46:44 +02:00
}
2017-01-03 21:19:33 +01:00
}
2017-04-12 17:46:44 +02:00
}
2018-01-06 03:48:56 +01:00
qreal Scrollbar::getMaximum() const
2017-04-12 17:46:44 +02:00
{
2018-06-13 13:27:10 +02:00
return this->maximum_;
2017-04-12 17:46:44 +02:00
}
2018-01-06 03:48:56 +01:00
qreal Scrollbar::getMinimum() const
2017-04-12 17:46:44 +02:00
{
2018-06-13 13:27:10 +02:00
return this->minimum_;
2017-04-12 17:46:44 +02:00
}
qreal Scrollbar::getPageSize() const
2017-04-12 17:46:44 +02:00
{
return this->pageSize_;
2017-04-12 17:46:44 +02:00
}
qreal Scrollbar::getBottom() const
{
return this->maximum_ - this->pageSize_;
2017-04-12 17:46:44 +02:00
}
2018-01-06 03:48:56 +01:00
qreal Scrollbar::getDesiredValue() const
2017-04-12 17:46:44 +02:00
{
return this->desiredValue_;
2017-04-12 17:46:44 +02:00
}
2018-01-06 03:48:56 +01:00
qreal Scrollbar::getCurrentValue() const
2017-04-12 17:46:44 +02:00
{
2018-06-13 13:27:10 +02:00
return this->currentValue_;
2017-04-12 17:46:44 +02:00
}
qreal Scrollbar::getRelativeCurrentValue() const
{
// currentValue - minimum can be negative if minimum is incremented while
// scrolling up to or down from the top when smooth scrolling is enabled.
return std::clamp(this->currentValue_ - this->minimum_, 0.0,
this->currentValue_);
}
2018-01-06 03:48:56 +01:00
void Scrollbar::offset(qreal value)
{
this->setDesiredValue(this->desiredValue_ + value);
}
pajlada::Signals::NoArgSignal &Scrollbar::getCurrentValueChanged()
2017-04-12 17:46:44 +02:00
{
2018-06-13 13:27:10 +02:00
return this->currentValueChanged_;
}
pajlada::Signals::NoArgSignal &Scrollbar::getDesiredValueChanged()
{
return this->desiredValueChanged_;
2017-04-12 17:46:44 +02:00
}
2018-01-06 03:48:56 +01:00
void Scrollbar::setCurrentValue(qreal value)
2017-04-12 17:46:44 +02:00
{
value = std::max(this->minimum_, std::min(this->getBottom(), value));
if (areClose(this->currentValue_, value))
{
// value has not changed
return;
}
2017-04-12 17:46:44 +02:00
this->currentValue_ = value;
2017-04-12 17:46:44 +02:00
this->updateScroll();
this->currentValueChanged_.invoke();
2017-01-03 21:19:33 +01:00
}
2018-01-06 03:48:56 +01:00
void Scrollbar::printCurrentState(const QString &prefix) const
2017-06-06 17:18:23 +02:00
{
qCDebug(chatterinoWidget).nospace().noquote()
<< prefix //
<< " { currentValue: " << this->getCurrentValue() //
<< ", desiredValue: " << this->getDesiredValue() //
<< ", maximum: " << this->getMaximum() //
<< ", minimum: " << this->getMinimum() //
<< ", pageSize: " << this->getPageSize() //
<< " }";
2017-06-06 17:18:23 +02:00
}
void Scrollbar::paintEvent(QPaintEvent * /*event*/)
2017-01-03 21:19:33 +01:00
{
bool mouseOver = this->mouseOverLocation_ != MouseLocation::Outside;
int xOffset =
mouseOver ? 0 : this->width() - static_cast<int>(4.0F * this->scale());
2017-01-03 21:19:33 +01:00
QPainter painter(this);
painter.fillRect(this->rect(), this->theme->scrollbars.background);
2017-01-03 21:19:33 +01:00
bool enableRedeemedHighlights = getSettings()->enableRedeemedHighlight;
bool enableFirstMessageHighlights =
getSettings()->enableFirstMessageHighlight;
bool enableElevatedMessageHighlights =
getSettings()->enableElevatedMessageHighlight;
2018-06-13 13:27:10 +02:00
this->thumbRect_.setX(xOffset);
2017-08-12 12:09:26 +02:00
// mouse over thumb
if (this->mouseDownLocation_ == MouseLocation::InsideThumb)
2018-06-13 13:27:10 +02:00
{
2018-07-06 17:11:37 +02:00
painter.fillRect(this->thumbRect_,
this->theme->scrollbars.thumbSelected);
2017-08-12 12:09:26 +02:00
}
// mouse not over thumb
else
{
2018-07-06 17:11:37 +02:00
painter.fillRect(this->thumbRect_, this->theme->scrollbars.thumb);
2017-08-12 12:09:26 +02:00
}
2017-01-26 04:26:40 +01:00
2018-01-06 03:48:56 +01:00
// draw highlights
if (this->highlights_.empty())
2018-01-06 20:57:01 +01:00
{
return;
}
size_t nHighlights = this->highlights_.size();
2018-01-06 03:48:56 +01:00
int w = this->width();
float dY =
static_cast<float>(this->height()) / static_cast<float>(nHighlights);
int highlightHeight =
static_cast<int>(std::ceil(std::max(this->scale() * 2.0F, dY)));
2018-01-06 03:48:56 +01:00
for (size_t i = 0; i < nHighlights; i++)
2018-05-26 17:11:09 +02:00
{
const auto &highlight = this->highlights_[i];
2018-01-06 03:48:56 +01:00
if (highlight.isNull())
2018-01-06 03:48:56 +01:00
{
continue;
}
if (highlight.isRedeemedHighlight() && !enableRedeemedHighlights)
{
continue;
2018-01-06 03:48:56 +01:00
}
2017-01-03 21:19:33 +01:00
if (highlight.isFirstMessageHighlight() &&
!enableFirstMessageHighlights)
{
continue;
}
if (highlight.isElevatedMessageHighlight() &&
!enableElevatedMessageHighlights)
{
continue;
}
QColor color = highlight.getColor();
color.setAlpha(255);
int y = static_cast<int>(dY * static_cast<float>(i));
switch (highlight.getStyle())
{
case ScrollbarHighlight::Default: {
painter.fillRect(w / 8 * 3, y, w / 4, highlightHeight, color);
}
break;
case ScrollbarHighlight::Line: {
painter.fillRect(0, y, w, 1, color);
}
break;
case ScrollbarHighlight::None:;
}
2018-01-06 03:48:56 +01:00
}
2017-01-18 04:33:30 +01:00
}
void Scrollbar::resizeEvent(QResizeEvent * /*event*/)
2017-12-26 16:54:39 +01:00
{
this->resize(static_cast<int>(16 * this->scale()), this->height());
2017-12-26 16:54:39 +01:00
}
2018-01-06 03:48:56 +01:00
void Scrollbar::mouseMoveEvent(QMouseEvent *event)
2017-01-26 04:26:40 +01:00
{
if (this->mouseDownLocation_ == MouseLocation::Outside)
2018-06-13 13:27:10 +02:00
{
auto moveLocation = this->locationOfMouseEvent(event);
if (this->mouseOverLocation_ != moveLocation)
2018-06-13 13:27:10 +02:00
{
this->mouseOverLocation_ = moveLocation;
this->update();
2017-01-26 04:26:40 +01:00
}
2018-06-13 13:27:10 +02:00
}
else if (this->mouseDownLocation_ == MouseLocation::InsideThumb)
2018-06-13 13:27:10 +02:00
{
qreal delta =
static_cast<qreal>(event->pos().y() - this->lastMousePosition_.y());
2017-01-26 04:26:40 +01:00
this->setDesiredValue(
2020-05-07 00:21:08 +02:00
this->desiredValue_ +
(delta / std::max<qreal>(0.00000002, this->trackHeight_)) *
2020-05-07 00:21:08 +02:00
this->maximum_);
2017-01-26 04:26:40 +01:00
}
2018-06-13 13:27:10 +02:00
this->lastMousePosition_ = event->pos();
2017-01-26 04:26:40 +01:00
}
2018-01-06 03:48:56 +01:00
void Scrollbar::mousePressEvent(QMouseEvent *event)
2017-01-26 04:26:40 +01:00
{
this->mouseDownLocation_ = this->locationOfMouseEvent(event);
this->update();
2017-01-26 04:26:40 +01:00
}
2018-01-06 03:48:56 +01:00
void Scrollbar::mouseReleaseEvent(QMouseEvent *event)
2017-01-26 04:26:40 +01:00
{
auto releaseLocation = this->locationOfMouseEvent(event);
if (this->mouseDownLocation_ != releaseLocation)
2018-06-13 13:27:10 +02:00
{
// Ignore event. User released the mouse from a different spot than
// they first clicked. For example, they clicked above the thumb,
// changed their mind, dragged the mouse below the thumb, and released.
this->mouseDownLocation_ = MouseLocation::Outside;
return;
2017-01-26 04:26:40 +01:00
}
switch (releaseLocation)
2018-06-13 13:27:10 +02:00
{
case MouseLocation::AboveThumb:
// Move scrollbar up a small bit.
this->setDesiredValue(this->desiredValue_ - SCROLL_DELTA, true);
break;
case MouseLocation::BelowThumb:
// Move scrollbar down a small bit.
this->setDesiredValue(this->desiredValue_ + SCROLL_DELTA, true);
break;
default:
break;
2017-01-26 04:26:40 +01:00
}
this->mouseDownLocation_ = MouseLocation::Outside;
this->update();
2017-01-26 04:26:40 +01:00
}
void Scrollbar::leaveEvent(QEvent * /*event*/)
2017-01-26 04:26:40 +01:00
{
this->mouseOverLocation_ = MouseLocation::Outside;
this->update();
2017-01-26 04:26:40 +01:00
}
2018-01-06 03:48:56 +01:00
void Scrollbar::updateScroll()
2017-01-18 04:33:30 +01:00
{
this->trackHeight_ = this->height() - MIN_THUMB_HEIGHT - 1;
2017-01-18 04:33:30 +01:00
auto div = std::max<qreal>(0.0000001, this->maximum_ - this->minimum_);
this->thumbRect_ =
QRect(0,
static_cast<int>((this->getRelativeCurrentValue()) / div *
this->trackHeight_) +
1,
this->width(),
static_cast<int>(this->pageSize_ / div * this->trackHeight_) +
MIN_THUMB_HEIGHT);
2017-01-18 04:33:30 +01:00
2018-06-13 13:27:10 +02:00
this->update();
2017-01-03 21:19:33 +01:00
}
2017-06-06 17:18:23 +02:00
Scrollbar::MouseLocation Scrollbar::locationOfMouseEvent(
QMouseEvent *event) const
{
int y = event->pos().y();
if (y < this->thumbRect_.y())
{
return MouseLocation::AboveThumb;
}
if (this->thumbRect_.contains(2, y))
{
return MouseLocation::InsideThumb;
}
return MouseLocation::BelowThumb;
}
2017-06-06 17:18:23 +02:00
} // namespace chatterino