#include "widgets/Scrollbar.hpp" #include "common/QLogging.hpp" #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" #include "widgets/helper/ChannelView.hpp" #include #include #include #include #define MIN_THUMB_HEIGHT 10 namespace chatterino { Scrollbar::Scrollbar(size_t messagesLimit, ChannelView *parent) : BaseWidget(parent) , currentValueAnimation_(this, "currentValue_") , highlights_(messagesLimit) { resize(int(16 * this->scale()), 100); this->currentValueAnimation_.setDuration(150); this->currentValueAnimation_.setEasingCurve( QEasingCurve(QEasingCurve::OutCubic)); setMouseTracking(true); } void Scrollbar::addHighlight(ScrollbarHighlight highlight) { this->highlights_.pushBack(highlight); } void Scrollbar::addHighlightsAtStart( const std::vector &_highlights) { this->highlights_.pushFront(_highlights); } void Scrollbar::replaceHighlight(size_t index, ScrollbarHighlight replacement) { this->highlights_.replaceItem(index, replacement); } void Scrollbar::pauseHighlights() { this->highlightsPaused_ = true; } void Scrollbar::unpauseHighlights() { this->highlightsPaused_ = false; } void Scrollbar::clearHighlights() { this->highlights_.clear(); } LimitedQueueSnapshot &Scrollbar::getHighlightSnapshot() { if (!this->highlightsPaused_) { this->highlightSnapshot_ = this->highlights_.getSnapshot(); } return this->highlightSnapshot_; } void Scrollbar::scrollToBottom(bool animate) { this->setDesiredValue(this->maximum_ - this->getLargeChange(), animate); } void Scrollbar::scrollToTop(bool animate) { this->setDesiredValue(this->minimum_ - this->getLargeChange(), animate); } bool Scrollbar::isAtBottom() const { return this->atBottom_; } void Scrollbar::setMaximum(qreal value) { this->maximum_ = value; updateScroll(); } void Scrollbar::setMinimum(qreal value) { this->minimum_ = value; updateScroll(); } void Scrollbar::setLargeChange(qreal value) { this->largeChange_ = value; updateScroll(); } void Scrollbar::setSmallChange(qreal value) { this->smallChange_ = value; updateScroll(); } void Scrollbar::setDesiredValue(qreal value, bool animated) { animated &= getSettings()->enableSmoothScrolling; value = std::max(this->minimum_, std::min(this->maximum_ - this->largeChange_, value)); if (std::abs(this->currentValue_ + this->smoothScrollingOffset_ - value) > 0.0001) { if (animated) { this->currentValueAnimation_.stop(); this->currentValueAnimation_.setStartValue( this->currentValue_ + this->smoothScrollingOffset_); // if (((this->getMaximum() - this->getLargeChange()) - // value) <= 0.01) { // value += 1; // } this->currentValueAnimation_.setEndValue(value); this->smoothScrollingOffset_ = 0; this->atBottom_ = ((this->getMaximum() - this->getLargeChange()) - value) <= 0.0001; this->currentValueAnimation_.start(); } else { if (this->currentValueAnimation_.state() == QPropertyAnimation::Running) { this->currentValueAnimation_.setEndValue(value); } else { this->smoothScrollingOffset_ = 0; this->desiredValue_ = value; this->currentValueAnimation_.stop(); this->atBottom_ = ((this->getMaximum() - this->getLargeChange()) - value) <= 0.0001; setCurrentValue(value); } } } this->smoothScrollingOffset_ = 0; this->desiredValue_ = value; this->desiredValueChanged_.invoke(); } qreal Scrollbar::getMaximum() const { return this->maximum_; } qreal Scrollbar::getMinimum() const { return this->minimum_; } qreal Scrollbar::getLargeChange() const { return this->largeChange_; } qreal Scrollbar::getSmallChange() const { return this->smallChange_; } qreal Scrollbar::getDesiredValue() const { return this->desiredValue_ + this->smoothScrollingOffset_; } qreal Scrollbar::getCurrentValue() const { return this->currentValue_; } const QPropertyAnimation &Scrollbar::getCurrentValueAnimation() const { return this->currentValueAnimation_; } void Scrollbar::offset(qreal value) { if (this->currentValueAnimation_.state() == QPropertyAnimation::Running) { this->smoothScrollingOffset_ += value; } else { this->setDesiredValue(this->getDesiredValue() + value); } } pajlada::Signals::NoArgSignal &Scrollbar::getCurrentValueChanged() { return this->currentValueChanged_; } pajlada::Signals::NoArgSignal &Scrollbar::getDesiredValueChanged() { return this->desiredValueChanged_; } void Scrollbar::setCurrentValue(qreal value) { value = std::max(this->minimum_, std::min(this->maximum_ - this->largeChange_, value + this->smoothScrollingOffset_)); if (std::abs(this->currentValue_ - value) > 0.0001) { this->currentValue_ = value; this->updateScroll(); this->currentValueChanged_.invoke(); this->update(); } } void Scrollbar::printCurrentState(const QString &prefix) const { qCDebug(chatterinoWidget) << prefix // << "Current value: " << this->getCurrentValue() // << ". Maximum: " << this->getMaximum() // << ". Minimum: " << this->getMinimum() // << ". Large change: " << this->getLargeChange(); // } void Scrollbar::paintEvent(QPaintEvent *) { bool mouseOver = this->mouseOverIndex_ != -1; int xOffset = mouseOver ? 0 : width() - int(4 * this->scale()); QPainter painter(this); painter.fillRect(rect(), this->theme->scrollbars.background); bool enableRedeemedHighlights = getSettings()->enableRedeemedHighlight; bool enableFirstMessageHighlights = getSettings()->enableFirstMessageHighlight; bool enableElevatedMessageHighlights = getSettings()->enableElevatedMessageHighlight; // painter.fillRect(QRect(xOffset, 0, width(), this->buttonHeight), // this->themeManager->ScrollbarArrow); // painter.fillRect(QRect(xOffset, height() - this->buttonHeight, // width(), this->buttonHeight), // this->themeManager->ScrollbarArrow); this->thumbRect_.setX(xOffset); // mouse over thumb if (this->mouseDownIndex_ == 2) { painter.fillRect(this->thumbRect_, this->theme->scrollbars.thumbSelected); } // mouse not over thumb else { painter.fillRect(this->thumbRect_, this->theme->scrollbars.thumb); } // draw highlights auto &snapshot = this->getHighlightSnapshot(); size_t snapshotLength = snapshot.size(); if (snapshotLength == 0) { return; } int w = this->width(); float y = 0; float dY = float(this->height()) / float(snapshotLength); int highlightHeight = int(std::ceil(std::max(this->scale() * 2, dY))); for (size_t i = 0; i < snapshotLength; i++, y += dY) { ScrollbarHighlight const &highlight = snapshot[i]; if (highlight.isNull()) { continue; } if (highlight.isRedeemedHighlight() && !enableRedeemedHighlights) { continue; } if (highlight.isFirstMessageHighlight() && !enableFirstMessageHighlights) { continue; } if (highlight.isElevatedMessageHighlight() && !enableElevatedMessageHighlights) { continue; } QColor color = highlight.getColor(); color.setAlpha(255); switch (highlight.getStyle()) { case ScrollbarHighlight::Default: { painter.fillRect(w / 8 * 3, int(y), w / 4, highlightHeight, color); } break; case ScrollbarHighlight::Line: { painter.fillRect(0, int(y), w, 1, color); } break; case ScrollbarHighlight::None:; } } } void Scrollbar::resizeEvent(QResizeEvent *) { this->resize(int(16 * this->scale()), this->height()); } void Scrollbar::mouseMoveEvent(QMouseEvent *event) { if (this->mouseDownIndex_ == -1) { int y = event->pos().y(); auto oldIndex = this->mouseOverIndex_; if (y < this->buttonHeight_) { this->mouseOverIndex_ = 0; } else if (y < this->thumbRect_.y()) { this->mouseOverIndex_ = 1; } else if (this->thumbRect_.contains(2, y)) { this->mouseOverIndex_ = 2; } else if (y < height() - this->buttonHeight_) { this->mouseOverIndex_ = 3; } else { this->mouseOverIndex_ = 4; } if (oldIndex != this->mouseOverIndex_) { update(); } } else if (this->mouseDownIndex_ == 2) { int delta = event->pos().y() - this->lastMousePosition_.y(); setDesiredValue( this->desiredValue_ + (qreal(delta) / std::max(0.00000002, this->trackHeight_)) * this->maximum_); } this->lastMousePosition_ = event->pos(); } void Scrollbar::mousePressEvent(QMouseEvent *event) { int y = event->pos().y(); if (y < this->buttonHeight_) { this->mouseDownIndex_ = 0; } else if (y < this->thumbRect_.y()) { this->mouseDownIndex_ = 1; } else if (this->thumbRect_.contains(2, y)) { this->mouseDownIndex_ = 2; } else if (y < height() - this->buttonHeight_) { this->mouseDownIndex_ = 3; } else { this->mouseDownIndex_ = 4; } } void Scrollbar::mouseReleaseEvent(QMouseEvent *event) { int y = event->pos().y(); if (y < this->buttonHeight_) { if (this->mouseDownIndex_ == 0) { setDesiredValue(this->desiredValue_ - this->smallChange_, true); } } else if (y < this->thumbRect_.y()) { if (this->mouseDownIndex_ == 1) { setDesiredValue(this->desiredValue_ - this->smallChange_, true); } } else if (this->thumbRect_.contains(2, y)) { // do nothing } else if (y < height() - this->buttonHeight_) { if (this->mouseDownIndex_ == 3) { setDesiredValue(this->desiredValue_ + this->smallChange_, true); } } else { if (this->mouseDownIndex_ == 4) { setDesiredValue(this->desiredValue_ + this->smallChange_, true); } } this->mouseDownIndex_ = -1; update(); } void Scrollbar::leaveEvent(QEvent *) { this->mouseOverIndex_ = -1; update(); } void Scrollbar::updateScroll() { this->trackHeight_ = this->height() - this->buttonHeight_ - this->buttonHeight_ - MIN_THUMB_HEIGHT - 1; auto div = std::max(0.0000001, this->maximum_); this->thumbRect_ = QRect( 0, int(this->currentValue_ / div * this->trackHeight_) + 1 + this->buttonHeight_, this->width(), int(this->largeChange_ / div * this->trackHeight_) + MIN_THUMB_HEIGHT); this->update(); } } // namespace chatterino