Commit 6406fd36 authored by Christian Kandeler's avatar Christian Kandeler

Analyzer: Re-design the diagnostics view.

The old one had a number of problems, mainly due to the awkward
delegate that was used for presenting the data. For instance:
    - Only one diagnostic at a time could be looked at
      in detail.
    - Once it had been opened, it was not possible to close
      such a detailed view again, other than by opening a new one.
We now use a tree view for showing the diagnostics, so users
can show and hide details about as many diagnostics as they
wish. That also gets us sensible item selection capabilities,
so features like suppressing several diagnostics at once can
be implemented in the future.

Change-Id: I840fdbfeca4d936ce600c8f6dde58b2ab93b0d00
Reviewed-by: default avatarNikolai Kosjar <nikolai.kosjar@theqtcompany.com>
parent 48b4509c
...@@ -14,6 +14,7 @@ SOURCES += \ ...@@ -14,6 +14,7 @@ SOURCES += \
analyzerrunconfigwidget.cpp \ analyzerrunconfigwidget.cpp \
analyzerutils.cpp \ analyzerutils.cpp \
detailederrorview.cpp \ detailederrorview.cpp \
diagnosticlocation.cpp \
startremotedialog.cpp startremotedialog.cpp
HEADERS += \ HEADERS += \
...@@ -27,6 +28,7 @@ HEADERS += \ ...@@ -27,6 +28,7 @@ HEADERS += \
analyzerrunconfigwidget.h \ analyzerrunconfigwidget.h \
analyzerutils.h \ analyzerutils.h \
detailederrorview.h \ detailederrorview.h \
diagnosticlocation.h \
startremotedialog.h startremotedialog.h
RESOURCES += \ RESOURCES += \
......
...@@ -29,6 +29,8 @@ QtcPlugin { ...@@ -29,6 +29,8 @@ QtcPlugin {
"analyzerutils.h", "analyzerutils.h",
"detailederrorview.cpp", "detailederrorview.cpp",
"detailederrorview.h", "detailederrorview.h",
"diagnosticlocation.cpp",
"diagnosticlocation.h",
"ianalyzertool.cpp", "ianalyzertool.cpp",
"ianalyzertool.h", "ianalyzertool.h",
"startremotedialog.cpp", "startremotedialog.cpp",
......
...@@ -30,247 +30,132 @@ ...@@ -30,247 +30,132 @@
#include "detailederrorview.h" #include "detailederrorview.h"
#include "diagnosticlocation.h"
#include <coreplugin/coreconstants.h> #include <coreplugin/coreconstants.h>
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <utils/qtcassert.h> #include <utils/qtcassert.h>
#include <QAbstractTextDocumentLayout>
#include <QAction> #include <QAction>
#include <QApplication> #include <QApplication>
#include <QClipboard> #include <QClipboard>
#include <QContextMenuEvent> #include <QContextMenuEvent>
#include <QFontMetrics> #include <QFileInfo>
#include <QHeaderView>
#include <QMenu> #include <QMenu>
#include <QPainter> #include <QPainter>
#include <QScrollBar> #include <QSharedPointer>
#include <QTextDocument>
namespace Analyzer { namespace Analyzer {
namespace Internal {
DetailedErrorDelegate::DetailedErrorDelegate(QListView *parent) class DetailedErrorDelegate : public QStyledItemDelegate
: QStyledItemDelegate(parent),
m_detailsWidget(0)
{
connect(parent->verticalScrollBar(), &QScrollBar::valueChanged,
this, &DetailedErrorDelegate::onVerticalScroll);
}
QSize DetailedErrorDelegate::sizeHint(const QStyleOptionViewItem &opt,
const QModelIndex &index) const
{ {
if (!index.isValid()) Q_OBJECT
return QStyledItemDelegate::sizeHint(opt, index);
const QListView *view = qobject_cast<const QListView *>(parent()); public:
const int viewportWidth = view->viewport()->width(); DetailedErrorDelegate(QTreeView *parent) : QStyledItemDelegate(parent) { }
const bool isSelected = view->selectionModel()->currentIndex() == index;
const int dy = 2 * s_itemMargin;
if (!isSelected) { private:
QFontMetrics fm(opt.font); QString actualText(const QModelIndex &index) const
return QSize(viewportWidth, fm.height() + dy); {
const auto location = index.model()->data(index, DetailedErrorView::LocationRole)
.value<DiagnosticLocation>();
return location.isValid()
? QString::fromLatin1("<a href=\"file://%1\">%2:%3")
.arg(location.filePath, QFileInfo(location.filePath).fileName())
.arg(location.line)
: QString();
} }
if (m_detailsWidget && m_detailsIndex != index) { using DocConstPtr = QSharedPointer<const QTextDocument>;
m_detailsWidget->deleteLater(); DocConstPtr document(const QStyleOptionViewItem &option) const
m_detailsWidget = 0; {
const auto doc = QSharedPointer<QTextDocument>::create();
doc->setHtml(option.text);
doc->setTextWidth(option.rect.width());
doc->setDocumentMargin(0);
return doc;
} }
if (!m_detailsWidget) { QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
m_detailsWidget = createDetailsWidget(opt.font, index, view->viewport()); {
QTC_ASSERT(m_detailsWidget->parent() == view->viewport(), QStyleOptionViewItem opt = option;
m_detailsWidget->setParent(view->viewport())); opt.text = actualText(index);
m_detailsIndex = index; initStyleOption(&opt, index);
} else {
QTC_ASSERT(m_detailsIndex == index, /**/); const DocConstPtr doc = document(opt);
return QSize(doc->idealWidth(), doc->size().height());
} }
const int widthExcludingMargins = viewportWidth - 2 * s_itemMargin;
m_detailsWidget->setFixedWidth(widthExcludingMargins);
m_detailsWidgetHeight = m_detailsWidget->heightForWidth(widthExcludingMargins);
// HACK: it's a bug in QLabel(?) that we have to force the widget to have the size it said
// it would have.
m_detailsWidget->setFixedHeight(m_detailsWidgetHeight);
return QSize(viewportWidth, dy + m_detailsWidget->heightForWidth(widthExcludingMargins));
}
void DetailedErrorDelegate::paint(QPainter *painter, const QStyleOptionViewItem &basicOption, void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const const QModelIndex &index) const override
{ {
QStyleOptionViewItemV4 opt(basicOption); QStyleOptionViewItem opt = option;
initStyleOption(&opt, index); initStyleOption(&opt, index);
const QListView *const view = qobject_cast<const QListView *>(parent()); QStyle *style = opt.widget? opt.widget->style() : QApplication::style();
const bool isSelected = view->selectionModel()->currentIndex() == index;
QFontMetrics fm(opt.font); // Painting item without text
QPoint pos = opt.rect.topLeft(); opt.text.clear();
style->drawControl(QStyle::CE_ItemViewItem, &opt, painter);
opt.text = actualText(index);
painter->save(); QAbstractTextDocumentLayout::PaintContext ctx;
const QColor bgColor = isSelected // Highlighting text if item is selected
? opt.palette.highlight().color() if (opt.state & QStyle::State_Selected) {
: opt.palette.background().color(); ctx.palette.setColor(QPalette::Text, opt.palette.color(QPalette::Active,
painter->setBrush(bgColor); QPalette::HighlightedText));
// clear background
painter->setPen(Qt::NoPen);
painter->drawRect(opt.rect);
pos.rx() += s_itemMargin;
pos.ry() += s_itemMargin;
if (isSelected) {
// only show detailed widget and let it handle everything
QTC_ASSERT(m_detailsIndex == index, /**/);
QTC_ASSERT(m_detailsWidget, return); // should have been set in sizeHint()
m_detailsWidget->move(pos);
// when scrolling quickly, the widget can get stuck in a visible part of the scroll area
// even though it should not be visible. therefore we hide it every time the scroll value
// changes and un-hide it when the item with details widget is paint()ed, i.e. visible.
m_detailsWidget->show();
const int viewportWidth = view->viewport()->width();
const int widthExcludingMargins = viewportWidth - 2 * s_itemMargin;
QTC_ASSERT(m_detailsWidget->width() == widthExcludingMargins, /**/);
QTC_ASSERT(m_detailsWidgetHeight == m_detailsWidget->height(), /**/);
} else {
// the reference coordinate for text drawing is the text baseline; move it inside the view rect.
pos.ry() += fm.ascent();
const QColor textColor = opt.palette.text().color();
painter->setPen(textColor);
// draw only text + location
const SummaryLineInfo info = summaryInfo(index);
const QString errorText = info.errorText;
painter->drawText(pos, errorText);
const int whatWidth = QFontMetrics(opt.font).width(errorText);
const int space = 10;
const int widthLeft = opt.rect.width() - (pos.x() + whatWidth + space + s_itemMargin);
if (widthLeft > 0) {
QFont monospace = opt.font;
monospace.setFamily(QLatin1String("monospace"));
QFontMetrics metrics(monospace);
QColor nameColor = textColor;
nameColor.setAlphaF(0.7);
painter->setFont(monospace);
painter->setPen(nameColor);
QPoint namePos = pos;
namePos.rx() += whatWidth + space;
painter->drawText(namePos, metrics.elidedText(info.errorLocation, Qt::ElideLeft,
widthLeft));
}
} }
// Separator lines (like Issues pane) QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &opt);
painter->setPen(QColor::fromRgb(150,150,150)); painter->save();
painter->drawLine(0, opt.rect.bottom(), opt.rect.right(), opt.rect.bottom()); painter->translate(textRect.topLeft());
painter->setClipRect(textRect.translated(-textRect.topLeft()));
document(opt)->documentLayout()->draw(painter, ctx);
painter->restore(); painter->restore();
}
void DetailedErrorDelegate::onCurrentSelectionChanged(const QModelIndex &now,
const QModelIndex &previous)
{
if (m_detailsWidget) {
m_detailsWidget->deleteLater();
m_detailsWidget = 0;
}
m_detailsIndex = QModelIndex();
if (now.isValid())
emit sizeHintChanged(now);
if (previous.isValid())
emit sizeHintChanged(previous);
}
void DetailedErrorDelegate::onLayoutChanged()
{
if (m_detailsWidget) {
m_detailsWidget->deleteLater();
m_detailsWidget = 0;
m_detailsIndex = QModelIndex();
} }
} };
void DetailedErrorDelegate::onViewResized()
{
const QListView *view = qobject_cast<const QListView *>(parent());
if (m_detailsWidget)
emit sizeHintChanged(view->selectionModel()->currentIndex());
}
void DetailedErrorDelegate::onVerticalScroll()
{
if (m_detailsWidget)
m_detailsWidget->hide();
}
// Expects "file://some/path[:line[:column]]" - the line/column part is optional } // namespace Internal
void DetailedErrorDelegate::openLinkInEditor(const QString &link)
{
const QString linkWithoutPrefix = link.mid(int(strlen("file://")));
const QChar separator = QLatin1Char(':');
const int lineColon = linkWithoutPrefix.indexOf(separator, /*after drive letter + colon =*/ 2);
const QString path = linkWithoutPrefix.left(lineColon);
const QString lineColumn = linkWithoutPrefix.mid(lineColon + 1);
const int line = lineColumn.section(separator, 0, 0).toInt();
const int column = lineColumn.section(separator, 1, 1).toInt();
Core::EditorManager::openEditorAt(path, qMax(line, 0), qMax(column, 0));
}
void DetailedErrorDelegate::copyToClipboard()
{
QApplication::clipboard()->setText(textualRepresentation());
}
DetailedErrorView::DetailedErrorView(QWidget *parent) : DetailedErrorView::DetailedErrorView(QWidget *parent) :
QListView(parent), QTreeView(parent),
m_copyAction(0) m_copyAction(new QAction(this))
{ {
} header()->setSectionResizeMode(QHeaderView::ResizeToContents);
setItemDelegateForColumn(LocationColumn, new Internal::DetailedErrorDelegate(this));
DetailedErrorView::~DetailedErrorView()
{
itemDelegate()->deleteLater();
}
void DetailedErrorView::setItemDelegate(QAbstractItemDelegate *delegate)
{
QListView::setItemDelegate(delegate);
DetailedErrorDelegate *myDelegate = qobject_cast<DetailedErrorDelegate *>(itemDelegate());
connect(this, &DetailedErrorView::resized, myDelegate, &DetailedErrorDelegate::onViewResized);
m_copyAction = new QAction(this);
m_copyAction->setText(tr("Copy")); m_copyAction->setText(tr("Copy"));
m_copyAction->setIcon(QIcon(QLatin1String(Core::Constants::ICON_COPY))); m_copyAction->setIcon(QIcon(QLatin1String(Core::Constants::ICON_COPY)));
m_copyAction->setShortcut(QKeySequence::Copy); m_copyAction->setShortcut(QKeySequence::Copy);
m_copyAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); m_copyAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
connect(m_copyAction, &QAction::triggered, myDelegate, &DetailedErrorDelegate::copyToClipboard); connect(m_copyAction, &QAction::triggered, [this] {
addAction(m_copyAction); const QModelIndexList selectedRows = selectionModel()->selectedRows();
} QTC_ASSERT(selectedRows.count() == 1, return);
QApplication::clipboard()->setText(model()->data(selectedRows.first(),
void DetailedErrorView::setModel(QAbstractItemModel *model) FullTextRole).toString());
{ });
QListView::setModel(model); connect(this, &QAbstractItemView::clicked, [](const QModelIndex &index) {
if (index.column() == LocationColumn) {
DetailedErrorDelegate *delegate = qobject_cast<DetailedErrorDelegate *>(itemDelegate()); const auto loc = index.model()
QTC_ASSERT(delegate, return); ->data(index, Analyzer::DetailedErrorView::LocationRole)
.value<DiagnosticLocation>();
if (loc.isValid())
Core::EditorManager::openEditorAt(loc.filePath, loc.line, loc.column - 1);
}
});
connect(selectionModel(), &QItemSelectionModel::currentChanged, addAction(m_copyAction);
delegate, &DetailedErrorDelegate::onCurrentSelectionChanged);
connect(model, &QAbstractItemModel::layoutChanged,
delegate, &DetailedErrorDelegate::onLayoutChanged);
} }
void DetailedErrorView::resizeEvent(QResizeEvent *e) DetailedErrorView::~DetailedErrorView()
{ {
emit resized();
QListView::resizeEvent(e);
} }
void DetailedErrorView::contextMenuEvent(QContextMenuEvent *e) void DetailedErrorView::contextMenuEvent(QContextMenuEvent *e)
...@@ -288,19 +173,6 @@ void DetailedErrorView::contextMenuEvent(QContextMenuEvent *e) ...@@ -288,19 +173,6 @@ void DetailedErrorView::contextMenuEvent(QContextMenuEvent *e)
menu.exec(e->globalPos()); menu.exec(e->globalPos());
} }
void DetailedErrorView::updateGeometries()
{
if (model()) {
QModelIndex index = model()->index(0, modelColumn(), rootIndex());
QStyleOptionViewItem option = viewOptions();
// delegate for row / column
QSize step = itemDelegate()->sizeHint(option, index);
horizontalScrollBar()->setSingleStep(step.width() + spacing());
verticalScrollBar()->setSingleStep(step.height() + spacing());
}
QListView::updateGeometries();
}
void DetailedErrorView::goNext() void DetailedErrorView::goNext()
{ {
QTC_ASSERT(rowCount(), return); QTC_ASSERT(rowCount(), return);
...@@ -346,3 +218,5 @@ void DetailedErrorView::setCurrentRow(int row) ...@@ -346,3 +218,5 @@ void DetailedErrorView::setCurrentRow(int row)
} }
} // namespace Analyzer } // namespace Analyzer
#include "detailederrorview.moc"
...@@ -33,59 +33,12 @@ ...@@ -33,59 +33,12 @@
#include "analyzerbase_global.h" #include "analyzerbase_global.h"
#include <QListView> #include <QTreeView>
#include <QStyledItemDelegate> #include <QStyledItemDelegate>
namespace Analyzer { namespace Analyzer {
// Provides the details widget for the DetailedErrorView class ANALYZER_EXPORT DetailedErrorView : public QTreeView
class ANALYZER_EXPORT DetailedErrorDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
struct SummaryLineInfo {
QString errorText;
QString errorLocation;
};
public:
/// This delegate can only work on one view at a time, parent. parent will also be the parent
/// in the QObject parent-child system.
explicit DetailedErrorDelegate(QListView *parent);
virtual SummaryLineInfo summaryInfo(const QModelIndex &index) const = 0;
void copyToClipboard();
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const;
public slots:
void onCurrentSelectionChanged(const QModelIndex &now, const QModelIndex &previous);
void onViewResized();
void onLayoutChanged();
private slots:
void onVerticalScroll();
protected:
void openLinkInEditor(const QString &link);
mutable QPersistentModelIndex m_detailsIndex;
private:
// the constness of this function is a necessary lie because it is called from paint() const.
virtual QWidget *createDetailsWidget(const QFont &font, const QModelIndex &errorIndex,
QWidget *parent) const = 0;
virtual QString textualRepresentation() const = 0;
static const int s_itemMargin = 2;
mutable QWidget *m_detailsWidget;
mutable int m_detailsWidgetHeight;
};
// A QListView that displays additional details for the currently selected item
class ANALYZER_EXPORT DetailedErrorView : public QListView
{ {
Q_OBJECT Q_OBJECT
...@@ -93,21 +46,18 @@ public: ...@@ -93,21 +46,18 @@ public:
DetailedErrorView(QWidget *parent = 0); DetailedErrorView(QWidget *parent = 0);
~DetailedErrorView(); ~DetailedErrorView();
// Reimplemented to connect delegate to connection model after it has
// been set by superclass implementation.
void setModel(QAbstractItemModel *model);
void setItemDelegate(QAbstractItemDelegate *delegate); // Takes ownership
void goNext(); void goNext();
void goBack(); void goBack();
signals: enum ItemRole {
void resized(); LocationRole = Qt::UserRole,
FullTextRole
};
protected: enum Column {
void updateGeometries(); DiagnosticColumn,
void resizeEvent(QResizeEvent *e); LocationColumn,
};
private: private:
void contextMenuEvent(QContextMenuEvent *e) override; void contextMenuEvent(QContextMenuEvent *e) override;
...@@ -119,7 +69,7 @@ private: ...@@ -119,7 +69,7 @@ private:
QList<QAction *> commonActions() const; QList<QAction *> commonActions() const;
virtual QList<QAction *> customActions() const; virtual QList<QAction *> customActions() const;
QAction *m_copyAction; QAction * const m_copyAction;
}; };
} // namespace Analyzer } // namespace Analyzer
......
/****************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://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 http://www.qt.io/terms-conditions. For further information
** use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, The Qt Company gives you certain additional
** rights. These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
****************************************************************************/
#include "diagnosticlocation.h"
namespace Analyzer {
DiagnosticLocation::DiagnosticLocation() : line(0), column(0)
{
}
DiagnosticLocation::DiagnosticLocation(const QString &filePath, int line, int column)
: filePath(filePath), line(line), column(column)
{
}
bool DiagnosticLocation::isValid() const
{