// Copyright (C) 2017 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "highlightscrollbarcontroller.h" #include #include #include #include #include #include using namespace Utils; namespace Core { /*! \class Core::Highlight \inmodule QtCreator \internal */ /*! \class Core::HighlightScrollBarController \inmodule QtCreator \internal */ class HighlightScrollBarOverlay : public QWidget { public: HighlightScrollBarOverlay(HighlightScrollBarController *scrollBarController) : QWidget(scrollBarController->scrollArea()) , m_scrollBar(scrollBarController->scrollBar()) , m_highlightController(scrollBarController) { setAttribute(Qt::WA_TransparentForMouseEvents); m_scrollBar->parentWidget()->installEventFilter(this); doResize(); doMove(); show(); } void doResize() { resize(m_scrollBar->size()); } void doMove() { move(parentWidget()->mapFromGlobal(m_scrollBar->mapToGlobal(m_scrollBar->pos()))); } void scheduleUpdate(); protected: void paintEvent(QPaintEvent *paintEvent) override; bool eventFilter(QObject *object, QEvent *event) override; private: void drawHighlights(QPainter *painter, int docStart, int docSize, double docSizeToHandleSizeRatio, int handleOffset, const QRect &viewport); void updateCache(); QRect overlayRect() const; QRect handleRect() const; // line start to line end QMap>> m_highlightCache; QScrollBar *m_scrollBar; HighlightScrollBarController *m_highlightController; bool m_isCacheUpdateScheduled = true; }; void HighlightScrollBarOverlay::scheduleUpdate() { if (m_isCacheUpdateScheduled) return; m_isCacheUpdateScheduled = true; QMetaObject::invokeMethod(this, QOverload<>::of(&QWidget::update), Qt::QueuedConnection); } void HighlightScrollBarOverlay::paintEvent(QPaintEvent *paintEvent) { QWidget::paintEvent(paintEvent); updateCache(); if (m_highlightCache.isEmpty()) return; QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, false); const QRect &gRect = overlayRect(); const QRect &hRect = handleRect(); constexpr int marginX = 3; constexpr int marginH = -2 * marginX + 1; const QRect aboveHandleRect = QRect(gRect.x() + marginX, gRect.y(), gRect.width() + marginH, hRect.y() - gRect.y()); const QRect handleRect = QRect(gRect.x() + marginX, hRect.y(), gRect.width() + marginH, hRect.height()); const QRect belowHandleRect = QRect(gRect.x() + marginX, hRect.y() + hRect.height(), gRect.width() + marginH, gRect.height() - hRect.height() + gRect.y() - hRect.y()); const int aboveValue = m_scrollBar->value(); const int belowValue = m_scrollBar->maximum() - m_scrollBar->value(); const int sizeDocAbove = int(aboveValue * m_highlightController->lineHeight()); const int sizeDocBelow = int(belowValue * m_highlightController->lineHeight()); const int sizeDocVisible = int(m_highlightController->visibleRange()); const int scrollBarBackgroundHeight = aboveHandleRect.height() + belowHandleRect.height(); const int sizeDocInvisible = sizeDocAbove + sizeDocBelow; const double backgroundRatio = sizeDocInvisible ? ((double)scrollBarBackgroundHeight / sizeDocInvisible) : 0; if (aboveValue) { drawHighlights(&painter, 0, sizeDocAbove, backgroundRatio, 0, aboveHandleRect); } if (belowValue) { // This is the hypothetical handle height if the handle would // be stretched using the background ratio. const double handleVirtualHeight = sizeDocVisible * backgroundRatio; // Skip the doc above and visible part. const int offset = qRound(aboveHandleRect.height() + handleVirtualHeight); drawHighlights(&painter, sizeDocAbove + sizeDocVisible, sizeDocBelow, backgroundRatio, offset, belowHandleRect); } const double handleRatio = sizeDocVisible ? ((double)handleRect.height() / sizeDocVisible) : 0; // This is the hypothetical handle position if the background would // be stretched using the handle ratio. const double aboveVirtualHeight = sizeDocAbove * handleRatio; // This is the accurate handle position (double) const double accurateHandlePos = sizeDocAbove * backgroundRatio; // The correction between handle position (int) and accurate position (double) const double correction = aboveHandleRect.height() - accurateHandlePos; // Skip the doc above and apply correction const int offset = qRound(aboveVirtualHeight + correction); drawHighlights(&painter, sizeDocAbove, sizeDocVisible, handleRatio, offset, handleRect); } void HighlightScrollBarOverlay::drawHighlights(QPainter *painter, int docStart, int docSize, double docSizeToHandleSizeRatio, int handleOffset, const QRect &viewport) { if (docSize <= 0) return; painter->save(); painter->setClipRect(viewport); const double lineHeight = m_highlightController->lineHeight(); for (const QMap> &colors : std::as_const(m_highlightCache)) { const auto itColorEnd = colors.constEnd(); for (auto itColor = colors.constBegin(); itColor != itColorEnd; ++itColor) { const QColor &color = creatorTheme()->color(itColor.key()); const QMap &positions = itColor.value(); const auto itPosEnd = positions.constEnd(); const auto firstPos = int(docStart / lineHeight); auto itPos = positions.upperBound(firstPos); if (itPos != positions.constBegin()) --itPos; while (itPos != itPosEnd) { const double posStart = itPos.key() * lineHeight; const double posEnd = (itPos.value() + 1) * lineHeight; if (posEnd < docStart) { ++itPos; continue; } if (posStart > docStart + docSize) break; const int height = qMax(qRound((posEnd - posStart) * docSizeToHandleSizeRatio), 1); const int top = qRound(posStart * docSizeToHandleSizeRatio) - handleOffset + viewport.y(); const QRect rect(viewport.left(), top, viewport.width(), height); painter->fillRect(rect, color); ++itPos; } } } painter->restore(); } bool HighlightScrollBarOverlay::eventFilter(QObject *object, QEvent *event) { switch (event->type()) { case QEvent::Move: doMove(); break; case QEvent::Resize: doResize(); break; case QEvent::ZOrderChange: raise(); break; case QEvent::Show: show(); break; case QEvent::Hide: hide(); break; default: break; } return QWidget::eventFilter(object, event); } static void insertPosition(QMap *map, int position) { auto itNext = map->upperBound(position); bool gluedWithPrev = false; if (itNext != map->begin()) { auto itPrev = std::prev(itNext); const int keyStart = itPrev.key(); const int keyEnd = itPrev.value(); if (position >= keyStart && position <= keyEnd) return; // pos is already included if (keyEnd + 1 == position) { // glue with prev (*itPrev)++; gluedWithPrev = true; } } if (itNext != map->end() && itNext.key() == position + 1) { const int keyEnd = itNext.value(); itNext = map->erase(itNext); if (gluedWithPrev) { // glue with prev and next auto itPrev = std::prev(itNext); *itPrev = keyEnd; } else { // glue with next itNext = map->insert(itNext, position, keyEnd); } return; // glued } if (gluedWithPrev) return; // glued map->insert(position, position); } void HighlightScrollBarOverlay::updateCache() { if (!m_isCacheUpdateScheduled) return; m_highlightCache.clear(); const QHash> highlightsForId = m_highlightController->highlights(); for (const QVector &highlights : highlightsForId) { for (const auto &highlight : highlights) { auto &highlightMap = m_highlightCache[highlight.priority][highlight.color]; insertPosition(&highlightMap, highlight.position); } } m_isCacheUpdateScheduled = false; } QRect HighlightScrollBarOverlay::overlayRect() const { QStyleOptionSlider opt = qt_qscrollbarStyleOption(m_scrollBar); return m_scrollBar->style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, m_scrollBar); } QRect HighlightScrollBarOverlay::handleRect() const { QStyleOptionSlider opt = qt_qscrollbarStyleOption(m_scrollBar); return m_scrollBar->style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, m_scrollBar); } ///////////// Highlight::Highlight(Id category_, int position_, Theme::Color color_, Highlight::Priority priority_) : category(category_) , position(position_) , color(color_) , priority(priority_) { } ///////////// HighlightScrollBarController::~HighlightScrollBarController() { if (m_overlay) delete m_overlay; } QScrollBar *HighlightScrollBarController::scrollBar() const { if (m_scrollArea) return m_scrollArea->verticalScrollBar(); return nullptr; } QAbstractScrollArea *HighlightScrollBarController::scrollArea() const { return m_scrollArea; } void HighlightScrollBarController::setScrollArea(QAbstractScrollArea *scrollArea) { if (m_scrollArea == scrollArea) return; if (m_overlay) { delete m_overlay; m_overlay = nullptr; } m_scrollArea = scrollArea; if (m_scrollArea) { m_overlay = new HighlightScrollBarOverlay(this); m_overlay->scheduleUpdate(); } } double HighlightScrollBarController::lineHeight() const { return ceil(m_lineHeight); } void HighlightScrollBarController::setLineHeight(double lineHeight) { m_lineHeight = lineHeight; } double HighlightScrollBarController::visibleRange() const { return m_visibleRange; } void HighlightScrollBarController::setVisibleRange(double visibleRange) { m_visibleRange = visibleRange; } double HighlightScrollBarController::margin() const { return m_margin; } void HighlightScrollBarController::setMargin(double margin) { m_margin = margin; } QHash> HighlightScrollBarController::highlights() const { return m_highlights; } void HighlightScrollBarController::addHighlight(Highlight highlight) { if (!m_overlay) return; m_highlights[highlight.category] << highlight; m_overlay->scheduleUpdate(); } void HighlightScrollBarController::removeHighlights(Id category) { if (!m_overlay) return; m_highlights.remove(category); m_overlay->scheduleUpdate(); } void HighlightScrollBarController::removeAllHighlights() { if (!m_overlay) return; m_highlights.clear(); m_overlay->scheduleUpdate(); } } // namespace Core