Commit 3841d2fc authored by Eike Ziller's avatar Eike Ziller
Browse files

Locator: Fix diverse issues with drawing highlighted text



To draw the result items with highlighted parts, we split the text to
draw into "before highlight", "highlight" and "after highlight", and
painted them separately. This had several issues:

It breaks the text layout. Characters within a text are often started at
subpixels, which was not so visible in the static search results, but
lead to ugly artifacts while typing in Locator (enter a search term, and
then continue adding characters that still match the first found item,
and watch the effect in the "after highlight" part).

It needs a lot of custom painting code. Properly supporting text elide
modes is hard, scaling to more than one highlight as well. Reusing parts
of the QItemDelegate base functions also has its issues, e.g. that
clipping doesn't work well.

Instead, QItemDelegate::drawDisplay should make it possible to set
format ranges for the text it draws. This patch copies part of
QItemDelegate to be able to add this parameter. Unfortunately Qt
currently has a bug (QTBUG-62019) that character backgrounds are not
painted far enough (1 pixel to the right has the wrong background), which
looks very ugly in selected items in the search results. So we use the
new delegate only for Locator for now, to be used later for the search
results too, when that bug is fixed.

Task-number: QTCREATORBUG-18532
Change-Id: Idf59b2c2bcfa6b188a810f7a3128a81e7e6fffb1
Reviewed-by: default avatarAndré Hartmann <aha_1980@gmx.de>
Reviewed-by: Orgad Shaneh's avatarOrgad Shaneh <orgads@gmail.com>
Reviewed-by: David Schulz's avatarDavid Schulz <david.schulz@qt.io>
parent 574b9913
/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "highlightingitemdelegate.h"
#include <QApplication>
#include <QModelIndex>
#include <QPainter>
const int kMinimumLineNumberDigits = 6;
namespace Utils {
HighlightingItemDelegate::HighlightingItemDelegate(int tabWidth, QObject *parent)
: QItemDelegate(parent)
{
setTabWidth(tabWidth);
}
void HighlightingItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
static const int iconSize = 16;
painter->save();
const QStyleOptionViewItem opt = setOptions(index, option);
painter->setFont(opt.font);
QItemDelegate::drawBackground(painter, opt, index);
// ---- do the layout
QRect checkRect;
QRect pixmapRect;
QRect textRect;
// check mark
const bool checkable = (index.model()->flags(index) & Qt::ItemIsUserCheckable);
Qt::CheckState checkState = Qt::Unchecked;
if (checkable) {
QVariant checkStateData = index.data(Qt::CheckStateRole);
checkState = static_cast<Qt::CheckState>(checkStateData.toInt());
checkRect = doCheck(opt, opt.rect, checkStateData);
}
// icon
const QIcon icon = index.model()->data(index, Qt::DecorationRole).value<QIcon>();
if (!icon.isNull()) {
const QSize size = icon.actualSize(QSize(iconSize, iconSize));
pixmapRect = QRect(0, 0, size.width(), size.height());
}
// text
textRect = opt.rect.adjusted(0, 0, checkRect.width() + pixmapRect.width(), 0);
// do layout
doLayout(opt, &checkRect, &pixmapRect, &textRect, false);
// ---- draw the items
// icon
if (!icon.isNull())
icon.paint(painter, pixmapRect, option.decorationAlignment);
// line numbers
const int lineNumberAreaWidth = drawLineNumber(painter, opt, textRect, index);
textRect.adjust(lineNumberAreaWidth, 0, 0, 0);
// text and focus/selection
drawText(painter, opt, textRect, index);
QItemDelegate::drawFocus(painter, opt, opt.rect);
// check mark
if (checkable)
QItemDelegate::drawCheck(painter, opt, checkRect, checkState);
painter->restore();
}
void HighlightingItemDelegate::setTabWidth(int width)
{
m_tabString = QString(width, ' ');
}
// returns the width of the line number area
int HighlightingItemDelegate::drawLineNumber(QPainter *painter, const QStyleOptionViewItem &option,
const QRect &rect,
const QModelIndex &index) const
{
static const int lineNumberAreaHorizontalPadding = 4;
const int lineNumber = index.model()->data(index, int(HighlightingItemRole::LineNumber)).toInt();
if (lineNumber < 1)
return 0;
const bool isSelected = option.state & QStyle::State_Selected;
const QString lineText = QString::number(lineNumber);
const int minimumLineNumberDigits = qMax(kMinimumLineNumberDigits, lineText.count());
const int fontWidth = painter->fontMetrics().width(QString(minimumLineNumberDigits, '0'));
const int lineNumberAreaWidth = lineNumberAreaHorizontalPadding + fontWidth
+ lineNumberAreaHorizontalPadding;
QRect lineNumberAreaRect(rect);
lineNumberAreaRect.setWidth(lineNumberAreaWidth);
QPalette::ColorGroup cg = QPalette::Normal;
if (!(option.state & QStyle::State_Active))
cg = QPalette::Inactive;
else if (!(option.state & QStyle::State_Enabled))
cg = QPalette::Disabled;
painter->fillRect(lineNumberAreaRect, QBrush(isSelected ?
option.palette.brush(cg, QPalette::Highlight) :
option.palette.color(cg, QPalette::Base).darker(111)));
QStyleOptionViewItem opt = option;
opt.displayAlignment = Qt::AlignRight | Qt::AlignVCenter;
opt.palette.setColor(cg, QPalette::Text, Qt::darkGray);
const QStyle *style = QApplication::style();
const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, 0, 0) + 1;
const QRect rowRect
= lineNumberAreaRect.adjusted(-textMargin, 0,
textMargin - lineNumberAreaHorizontalPadding, 0);
QItemDelegate::drawDisplay(painter, opt, rowRect, lineText);
return lineNumberAreaWidth;
}
void HighlightingItemDelegate::drawText(QPainter *painter,
const QStyleOptionViewItem &option,
const QRect &rect,
const QModelIndex &index) const
{
QString text = index.model()->data(index, Qt::DisplayRole).toString();
// show number of subresults in displayString
if (index.model()->hasChildren(index))
text += " (" + QString::number(index.model()->rowCount(index)) + ')';
int searchTermStart = index.model()->data(index, int(HighlightingItemRole::StartColumn)).toInt();
int searchTermLength = index.model()->data(index, int(HighlightingItemRole::Length)).toInt();
if (searchTermStart < 0 || searchTermStart >= text.length() || searchTermLength < 1) {
drawDisplay(painter, option, rect, text.replace('\t', m_tabString), {});
return;
}
// replace tabs with searchTerm bookkeeping
int searchTermEnd = searchTermStart + searchTermLength;
const int tabDiff = m_tabString.size() - 1;
for (int i = 0; i < text.length(); i++) {
if (text.at(i) == '\t') {
text.replace(i, 1, m_tabString);
if (i < searchTermStart) {
searchTermStart += tabDiff;
searchTermEnd += tabDiff;
} else if (i < searchTermEnd) {
searchTermEnd += tabDiff;
searchTermLength += tabDiff;
}
i += tabDiff;
}
}
const QColor highlightForeground =
index.model()->data(index, int(HighlightingItemRole::Foreground)).value<QColor>();
const QColor highlightBackground =
index.model()->data(index, int(HighlightingItemRole::Background)).value<QColor>();
QTextCharFormat highlightFormat;
highlightFormat.setForeground(highlightForeground);
highlightFormat.setBackground(highlightBackground);
drawDisplay(painter, option, rect, text, {{searchTermStart, searchTermLength, highlightFormat}});
}
// copied from QItemDelegate for drawDisplay
static QString replaceNewLine(QString text)
{
static const QChar nl = '\n';
for (int i = 0; i < text.count(); ++i)
if (text.at(i) == nl)
text[i] = QChar::LineSeparator;
return text;
}
// copied from QItemDelegate for drawDisplay
QSizeF doTextLayout(QTextLayout *textLayout, int lineWidth)
{
qreal height = 0;
qreal widthUsed = 0;
textLayout->beginLayout();
while (true) {
QTextLine line = textLayout->createLine();
if (!line.isValid())
break;
line.setLineWidth(lineWidth);
line.setPosition(QPointF(0, height));
height += line.height();
widthUsed = qMax(widthUsed, line.naturalTextWidth());
}
textLayout->endLayout();
return QSizeF(widthUsed, height);
}
// copied from QItemDelegate to be able to add the 'format' parameter
void HighlightingItemDelegate::drawDisplay(QPainter *painter,
const QStyleOptionViewItem &option,
const QRect &rect, const QString &text,
const QVector<QTextLayout::FormatRange> &format) const
{
QPalette::ColorGroup cg = option.state & QStyle::State_Enabled
? QPalette::Normal : QPalette::Disabled;
if (cg == QPalette::Normal && !(option.state & QStyle::State_Active))
cg = QPalette::Inactive;
if (option.state & QStyle::State_Selected) {
painter->fillRect(rect, option.palette.brush(cg, QPalette::Highlight));
painter->setPen(option.palette.color(cg, QPalette::HighlightedText));
} else {
painter->setPen(option.palette.color(cg, QPalette::Text));
}
if (text.isEmpty())
return;
if (option.state & QStyle::State_Editing) {
painter->save();
painter->setPen(option.palette.color(cg, QPalette::Text));
painter->drawRect(rect.adjusted(0, 0, -1, -1));
painter->restore();
}
const QStyleOptionViewItem opt = option;
const QWidget *widget = option.widget;
QStyle *style = widget ? widget->style() : QApplication::style();
const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, 0, widget) + 1;
QRect textRect = rect.adjusted(textMargin, 0, -textMargin, 0); // remove width padding
const bool wrapText = opt.features & QStyleOptionViewItem::WrapText;
QTextOption textOption;
textOption.setWrapMode(wrapText ? QTextOption::WordWrap : QTextOption::ManualWrap);
textOption.setTextDirection(option.direction);
textOption.setAlignment(QStyle::visualAlignment(option.direction, option.displayAlignment));
QTextLayout textLayout;
textLayout.setTextOption(textOption);
textLayout.setFont(option.font);
textLayout.setText(replaceNewLine(text));
QSizeF textLayoutSize = doTextLayout(&textLayout, textRect.width());
if (textRect.width() < textLayoutSize.width()
|| textRect.height() < textLayoutSize.height()) {
QString elided;
int start = 0;
int end = text.indexOf(QChar::LineSeparator, start);
if (end == -1) {
elided += option.fontMetrics.elidedText(text, option.textElideMode, textRect.width());
} else {
while (end != -1) {
elided += option.fontMetrics.elidedText(text.mid(start, end - start),
option.textElideMode, textRect.width());
elided += QChar::LineSeparator;
start = end + 1;
end = text.indexOf(QChar::LineSeparator, start);
}
// let's add the last line (after the last QChar::LineSeparator)
elided += option.fontMetrics.elidedText(text.mid(start),
option.textElideMode, textRect.width());
}
textLayout.setText(elided);
textLayoutSize = doTextLayout(&textLayout, textRect.width());
}
const QSize layoutSize(textRect.width(), int(textLayoutSize.height()));
const QRect layoutRect = QStyle::alignedRect(option.direction, option.displayAlignment,
layoutSize, textRect);
// if we still overflow even after eliding the text, enable clipping
if (!hasClipping() && (textRect.width() < textLayoutSize.width()
|| textRect.height() < textLayoutSize.height())) {
painter->save();
painter->setClipRect(layoutRect);
textLayout.draw(painter, layoutRect.topLeft(), format, layoutRect);
painter->restore();
} else {
textLayout.draw(painter, layoutRect.topLeft(), format, layoutRect);
}
}
} // namespace Utils
/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#pragma once
#include "utils_global.h"
#include <QItemDelegate>
#include <QTextLayout>
namespace Utils {
enum class HighlightingItemRole {
LineNumber = Qt::UserRole,
StartColumn,
Length,
Foreground,
Background,
User
};
class QTCREATOR_UTILS_EXPORT HighlightingItemDelegate : public QItemDelegate
{
public:
HighlightingItemDelegate(int tabWidth, QObject *parent = 0);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
void setTabWidth(int width);
private:
int drawLineNumber(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect, const QModelIndex &index) const;
void drawText(QPainter *painter, const QStyleOptionViewItem &option,
const QRect &rect, const QModelIndex &index) const;
using QItemDelegate::drawDisplay;
void drawDisplay(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect,
const QString &text, const QVector<QTextLayout::FormatRange> &format) const;
QString m_tabString;
};
} // namespace Utils
......@@ -113,7 +113,8 @@ SOURCES += $$PWD/environment.cpp \
$$PWD/port.cpp \
$$PWD/runextensions.cpp \
$$PWD/utilsicons.cpp \
$$PWD/guard.cpp
$$PWD/guard.cpp \
$$PWD/highlightingitemdelegate.cpp
win32:SOURCES += $$PWD/consoleprocess_win.cpp
else:SOURCES += $$PWD/consoleprocess_unix.cpp
......@@ -239,8 +240,9 @@ HEADERS += \
$$PWD/asconst.h \
$$PWD/smallstringfwd.h \
$$PWD/optional.h \
$$PWD/../3rdparty/optional/optional.hpp \
$$PWD/qtcfallthrough.h \
$$PWD/../3rdparty/optional/optional.hpp
$$PWD/highlightingitemdelegate.cpp
FORMS += $$PWD/filewizardpage.ui \
$$PWD/projectintropage.ui \
......
......@@ -117,6 +117,8 @@ Project {
"functiontraits.h",
"guard.cpp",
"guard.h",
"highlightingitemdelegate.cpp",
"highlightingitemdelegate.h",
"historycompleter.cpp",
"historycompleter.h",
"hostosinfo.h",
......
......@@ -33,14 +33,13 @@
#include <coreplugin/modemanager.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/fileiconprovider.h>
#include <coreplugin/find/searchresulttreeitemdelegate.h>
#include <coreplugin/find/searchresulttreeitemroles.h>
#include <coreplugin/icontext.h>
#include <coreplugin/mainwindow.h>
#include <utils/algorithm.h>
#include <utils/appmainwindow.h>
#include <utils/asconst.h>
#include <utils/fancylineedit.h>
#include <utils/highlightingitemdelegate.h>
#include <utils/hostosinfo.h>
#include <utils/itemviews.h>
#include <utils/progressindicator.h>
......@@ -67,6 +66,10 @@
Q_DECLARE_METATYPE(Core::LocatorFilterEntry)
using namespace Utils;
const int LocatorEntryRole = int(HighlightingItemRole::User);
namespace Core {
namespace Internal {
......@@ -99,7 +102,7 @@ private:
QColor mBackgroundColor;
};
class CompletionDelegate : public SearchResultTreeItemDelegate
class CompletionDelegate : public HighlightingItemDelegate
{
public:
CompletionDelegate(QObject *parent);
......@@ -193,7 +196,6 @@ QVariant LocatorModel::data(const QModelIndex &index, int role) const
+ QLatin1String("\n\n") + mEntries.at(index.row()).extraInfo);
break;
case Qt::DecorationRole:
case ItemDataRoles::ResultIconRole:
if (index.column() == DisplayNameColumn) {
LocatorFilterEntry &entry = mEntries[index.row()];
if (!entry.displayIcon && !entry.fileName.isEmpty())
......@@ -205,21 +207,21 @@ QVariant LocatorModel::data(const QModelIndex &index, int role) const
if (index.column() == ExtraInfoColumn)
return QColor(Qt::darkGray);
break;
case ItemDataRoles::ResultItemRole:
case LocatorEntryRole:
return qVariantFromValue(mEntries.at(index.row()));
case ItemDataRoles::ResultBeginColumnNumberRole:
case ItemDataRoles::SearchTermLengthRole: {
case int(HighlightingItemRole::StartColumn):
case int(HighlightingItemRole::Length): {
LocatorFilterEntry &entry = mEntries[index.row()];
const int highlightColumn = entry.highlightInfo.dataType == LocatorFilterEntry::HighlightInfo::DisplayName
? DisplayNameColumn
: ExtraInfoColumn;
if (highlightColumn == index.column()) {
const bool startIndexRole = role == ItemDataRoles::ResultBeginColumnNumberRole;
const bool startIndexRole = role == int(HighlightingItemRole::StartColumn);
return startIndexRole ? entry.highlightInfo.startIndex : entry.highlightInfo.length;
}
break;
}
case ItemDataRoles::ResultHighlightBackgroundColor:
case int(HighlightingItemRole::Background):
return mBackgroundColor;
}
......@@ -843,7 +845,7 @@ void LocatorWidget::acceptEntry(int row)
const QModelIndex index = m_locatorModel->index(row, 0);
if (!index.isValid())
return;
const LocatorFilterEntry entry = m_locatorModel->data(index, ItemDataRoles::ResultItemRole).value<LocatorFilterEntry>();
const LocatorFilterEntry entry = m_locatorModel->data(index, LocatorEntryRole).value<LocatorFilterEntry>();
Q_ASSERT(entry.filter != nullptr);
QString newText;
int selectionStart = -1;
......@@ -926,13 +928,13 @@ LocatorPopup *createLocatorPopup(Locator *locator, QWidget *parent)
}
CompletionDelegate::CompletionDelegate(QObject *parent)
: SearchResultTreeItemDelegate(0, parent)
: HighlightingItemDelegate(0, parent)
{
}
QSize CompletionDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
return SearchResultTreeItemDelegate::sizeHint(option, index) + QSize(0, 2);
return HighlightingItemDelegate::sizeHint(option, index) + QSize(0, 2);
}
} // namespace Internal
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment