forked from qt-creator/qt-creator
Refactor the highlight scrollbar painting
Scale the highlight on the handle according to the number of lines visible on screen. In case when document has huge amount of lines and only couple of lines are visible on screen, this generates a nice magnification effect. Change-Id: I43d7cce859cbc14da77272685a6f8d2350b41bb7 Reviewed-by: David Schulz <david.schulz@qt.io> Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
@@ -68,22 +68,30 @@ protected:
|
|||||||
bool eventFilter(QObject *object, QEvent *event) override;
|
bool eventFilter(QObject *object, QEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void drawHighlights(QPainter *painter,
|
||||||
|
int docStart,
|
||||||
|
int docSize,
|
||||||
|
double docSizeToHandleSizeRatio,
|
||||||
|
int handleOffset,
|
||||||
|
const QRect &viewport);
|
||||||
void updateCache();
|
void updateCache();
|
||||||
QRect overlayRect() const;
|
QRect overlayRect() const;
|
||||||
|
QRect handleRect() const;
|
||||||
|
|
||||||
bool m_cacheUpdateScheduled = true;
|
// line start to line end
|
||||||
QMap<int, Highlight> m_cache;
|
QMap<Highlight::Priority, QMap<Utils::Theme::Color, QMap<int, int>>> m_highlightCache;
|
||||||
|
|
||||||
QScrollBar *m_scrollBar;
|
QScrollBar *m_scrollBar;
|
||||||
HighlightScrollBarController *m_highlightController;
|
HighlightScrollBarController *m_highlightController;
|
||||||
|
bool m_isCacheUpdateScheduled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
void HighlightScrollBarOverlay::scheduleUpdate()
|
void HighlightScrollBarOverlay::scheduleUpdate()
|
||||||
{
|
{
|
||||||
if (m_cacheUpdateScheduled)
|
if (m_isCacheUpdateScheduled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_cacheUpdateScheduled = true;
|
m_isCacheUpdateScheduled = true;
|
||||||
QTimer::singleShot(0, this, static_cast<void (QWidget::*)()>(&QWidget::update));
|
QTimer::singleShot(0, this, static_cast<void (QWidget::*)()>(&QWidget::update));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,70 +101,133 @@ void HighlightScrollBarOverlay::paintEvent(QPaintEvent *paintEvent)
|
|||||||
|
|
||||||
updateCache();
|
updateCache();
|
||||||
|
|
||||||
if (m_cache.isEmpty())
|
if (m_highlightCache.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const QRect &rect = overlayRect();
|
|
||||||
|
|
||||||
Utils::Theme::Color previousColor = Utils::Theme::TextColorNormal;
|
|
||||||
Highlight::Priority previousPriority = Highlight::LowPriority;
|
|
||||||
QRect *previousRect = nullptr;
|
|
||||||
|
|
||||||
const int scrollbarRange = m_scrollBar->maximum() + m_scrollBar->pageStep();
|
|
||||||
const int range = qMax(m_highlightController->visibleRange(), float(scrollbarRange));
|
|
||||||
const int horizontalMargin = 3;
|
|
||||||
const int resultWidth = rect.width() - 2 * horizontalMargin + 1;
|
|
||||||
const int resultHeight = qMin(int(rect.height() / range) + 1, 4);
|
|
||||||
const int offset = rect.height() / range * m_highlightController->rangeOffset();
|
|
||||||
const int verticalMargin = ((rect.height() / range) - resultHeight) / 2;
|
|
||||||
int previousBottom = -1;
|
|
||||||
|
|
||||||
QHash<Utils::Theme::Color, QVector<QRect> > highlights;
|
|
||||||
for (const Highlight ¤tHighlight : qAsConst(m_cache)) {
|
|
||||||
// Calculate top and bottom
|
|
||||||
int top = rect.top() + offset + verticalMargin
|
|
||||||
+ float(currentHighlight.position) / range * rect.height();
|
|
||||||
const int bottom = top + resultHeight;
|
|
||||||
|
|
||||||
if (previousRect && previousColor == currentHighlight.color && previousBottom + 1 >= top) {
|
|
||||||
// If the previous highlight has the same color and is directly prior to this highlight
|
|
||||||
// we just extend the previous highlight.
|
|
||||||
previousRect->setBottom(bottom - 1);
|
|
||||||
|
|
||||||
} else { // create a new highlight
|
|
||||||
if (previousRect && previousBottom >= top) {
|
|
||||||
// make sure that highlights with higher priority are drawn on top of other highlights
|
|
||||||
// when rectangles are overlapping
|
|
||||||
|
|
||||||
if (previousPriority > currentHighlight.priority) {
|
|
||||||
// Moving the top of the current highlight when the previous
|
|
||||||
// highlight has a higher priority
|
|
||||||
top = previousBottom + 1;
|
|
||||||
if (top == bottom) // if this would result in an empty highlight just skip
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
previousRect->setBottom(top - 1); // move the end of the last highlight
|
|
||||||
if (previousRect->height() == 0) // if the result is an empty rect, remove it.
|
|
||||||
highlights[previousColor].removeLast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
highlights[currentHighlight.color] << QRect(rect.left() + horizontalMargin, top,
|
|
||||||
resultWidth, bottom - top);
|
|
||||||
previousRect = &highlights[currentHighlight.color].last();
|
|
||||||
previousColor = currentHighlight.color;
|
|
||||||
previousPriority = currentHighlight.priority;
|
|
||||||
}
|
|
||||||
previousBottom = previousRect->bottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
QPainter painter(this);
|
QPainter painter(this);
|
||||||
painter.setRenderHint(QPainter::Antialiasing, false);
|
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||||
const auto highlightEnd = highlights.cend();
|
|
||||||
for (auto highlightIt = highlights.cbegin(); highlightIt != highlightEnd; ++highlightIt) {
|
const QRect &gRect = overlayRect();
|
||||||
const QColor &color = creatorTheme()->color(highlightIt.key());
|
const QRect &hRect = handleRect();
|
||||||
for (const QRect &rect : highlightIt.value())
|
|
||||||
painter.fillRect(rect, color);
|
const int marginX = 3;
|
||||||
|
const 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 = aboveValue * int(m_highlightController->lineHeight());
|
||||||
|
const int sizeDocBelow = belowValue * int(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<Utils::Theme::Color, QMap<int, int>> &colors : qAsConst(m_highlightCache)) {
|
||||||
|
const auto itColorEnd = colors.constEnd();
|
||||||
|
for (auto itColor = colors.constBegin(); itColor != itColorEnd; ++itColor) {
|
||||||
|
const QColor &color = creatorTheme()->color(itColor.key());
|
||||||
|
const QMap<int, int> &positions = itColor.value();
|
||||||
|
const auto itPosEnd = positions.constEnd();
|
||||||
|
const int 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)
|
bool HighlightScrollBarOverlay::eventFilter(QObject *object, QEvent *event)
|
||||||
@@ -177,22 +248,61 @@ bool HighlightScrollBarOverlay::eventFilter(QObject *object, QEvent *event)
|
|||||||
return QWidget::eventFilter(object, event);
|
return QWidget::eventFilter(object, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void insertPosition(QMap<int, int> *map, int position)
|
||||||
|
{
|
||||||
|
auto itNext = map->upperBound(position);
|
||||||
|
|
||||||
|
bool gluedWithPrev = false;
|
||||||
|
if (itNext != map->begin()) {
|
||||||
|
auto itPrev = itNext - 1;
|
||||||
|
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 = itNext - 1;
|
||||||
|
*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()
|
void HighlightScrollBarOverlay::updateCache()
|
||||||
{
|
{
|
||||||
if (!m_cacheUpdateScheduled)
|
if (!m_isCacheUpdateScheduled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_cache.clear();
|
m_highlightCache.clear();
|
||||||
const QHash<Id, QVector<Highlight>> highlights = m_highlightController->highlights();
|
|
||||||
const QList<Id> &categories = highlights.keys();
|
const QHash<Id, QVector<Highlight>> highlightsForId = m_highlightController->highlights();
|
||||||
for (const Id &category : categories) {
|
for (QVector<Highlight> highlights : highlightsForId) {
|
||||||
for (const Highlight &highlight : highlights.value(category)) {
|
for (const auto &highlight : highlights) {
|
||||||
const Highlight oldHighlight = m_cache.value(highlight.position);
|
auto &highlightMap = m_highlightCache[highlight.priority][highlight.color];
|
||||||
if (highlight.priority > oldHighlight.priority)
|
insertPosition(&highlightMap, highlight.position);
|
||||||
m_cache[highlight.position] = highlight;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_cacheUpdateScheduled = false;
|
|
||||||
|
m_isCacheUpdateScheduled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QRect HighlightScrollBarOverlay::overlayRect() const
|
QRect HighlightScrollBarOverlay::overlayRect() const
|
||||||
@@ -201,6 +311,13 @@ QRect HighlightScrollBarOverlay::overlayRect() const
|
|||||||
return m_scrollBar->style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, 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_,
|
Highlight::Highlight(Id category_, int position_,
|
||||||
@@ -251,24 +368,34 @@ void HighlightScrollBarController::setScrollArea(QAbstractScrollArea *scrollArea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float HighlightScrollBarController::visibleRange() const
|
double HighlightScrollBarController::lineHeight() const
|
||||||
|
{
|
||||||
|
return m_lineHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HighlightScrollBarController::setLineHeight(double lineHeight)
|
||||||
|
{
|
||||||
|
m_lineHeight = lineHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
double HighlightScrollBarController::visibleRange() const
|
||||||
{
|
{
|
||||||
return m_visibleRange;
|
return m_visibleRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
void HighlightScrollBarController::setVisibleRange(float visibleRange)
|
void HighlightScrollBarController::setVisibleRange(double visibleRange)
|
||||||
{
|
{
|
||||||
m_visibleRange = visibleRange;
|
m_visibleRange = visibleRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
float HighlightScrollBarController::rangeOffset() const
|
double HighlightScrollBarController::margin() const
|
||||||
{
|
{
|
||||||
return m_rangeOffset;
|
return m_margin;
|
||||||
}
|
}
|
||||||
|
|
||||||
void HighlightScrollBarController::setRangeOffset(float offset)
|
void HighlightScrollBarController::setMargin(double margin)
|
||||||
{
|
{
|
||||||
m_rangeOffset = offset;
|
m_margin = margin;
|
||||||
}
|
}
|
||||||
|
|
||||||
QHash<Id, QVector<Highlight>> HighlightScrollBarController::highlights() const
|
QHash<Id, QVector<Highlight>> HighlightScrollBarController::highlights() const
|
||||||
|
|||||||
@@ -71,11 +71,14 @@ public:
|
|||||||
QAbstractScrollArea *scrollArea() const;
|
QAbstractScrollArea *scrollArea() const;
|
||||||
void setScrollArea(QAbstractScrollArea *scrollArea);
|
void setScrollArea(QAbstractScrollArea *scrollArea);
|
||||||
|
|
||||||
float visibleRange() const;
|
double lineHeight() const;
|
||||||
void setVisibleRange(float visibleRange);
|
void setLineHeight(double lineHeight);
|
||||||
|
|
||||||
float rangeOffset() const;
|
double visibleRange() const;
|
||||||
void setRangeOffset(float offset);
|
void setVisibleRange(double visibleRange);
|
||||||
|
|
||||||
|
double margin() const;
|
||||||
|
void setMargin(double margin);
|
||||||
|
|
||||||
QHash<Id, QVector<Highlight>> highlights() const;
|
QHash<Id, QVector<Highlight>> highlights() const;
|
||||||
void addHighlight(Highlight highlight);
|
void addHighlight(Highlight highlight);
|
||||||
@@ -85,8 +88,9 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
QHash<Id, QVector<Highlight> > m_highlights;
|
QHash<Id, QVector<Highlight> > m_highlights;
|
||||||
float m_visibleRange = 0.0;
|
double m_lineHeight = 0.0;
|
||||||
float m_rangeOffset = 0.0;
|
double m_visibleRange = 0.0; // in pixels
|
||||||
|
double m_margin = 0.0; // in pixels
|
||||||
QAbstractScrollArea *m_scrollArea = nullptr;
|
QAbstractScrollArea *m_scrollArea = nullptr;
|
||||||
QPointer<HighlightScrollBarOverlay> m_overlay;
|
QPointer<HighlightScrollBarOverlay> m_overlay;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6311,9 +6311,9 @@ void TextEditorWidgetPrivate::adjustScrollBarRanges()
|
|||||||
if (lineSpacing == 0)
|
if (lineSpacing == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const double offset = q->contentOffset().y();
|
m_highlightScrollBarController->setLineHeight(lineSpacing);
|
||||||
m_highlightScrollBarController->setVisibleRange(float((q->viewport()->rect().height() - offset) / lineSpacing));
|
m_highlightScrollBarController->setVisibleRange(q->viewport()->rect().height());
|
||||||
m_highlightScrollBarController->setRangeOffset(float(offset / lineSpacing));
|
m_highlightScrollBarController->setMargin(q->textDocument()->document()->documentMargin());
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextEditorWidgetPrivate::highlightSearchResultsInScrollBar()
|
void TextEditorWidgetPrivate::highlightSearchResultsInScrollBar()
|
||||||
|
|||||||
Reference in New Issue
Block a user