ProjectExplorer: Redo the TaskView

- Make it a tree view.
- Use the default delegate for top-level items.
- If the task has details, show them in a child item.

Fixes: QTCREATORBUG-28850
Change-Id: Iafd65496f916b9e382ef2c6ade0f9b3a323f63ed
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
Christian Kandeler
2023-03-21 14:53:02 +01:00
parent e7d6501553
commit 778d7a9819
6 changed files with 286 additions and 360 deletions

View File

@@ -8,8 +8,8 @@
#include "projectexplorertr.h"
#include <app/app_version.h>
#include <texteditor/fontsettings.h>
#include <texteditor/textmark.h>
#include <utils/algorithm.h>
#include <utils/utilsicons.h>
#include <utils/qtcassert.h>
@@ -105,11 +105,16 @@ void Task::setFile(const Utils::FilePath &file_)
}
}
QString Task::description() const
QString Task::description(DescriptionTags tags) const
{
QString desc = summary;
if (!details.isEmpty())
desc.append('\n').append(details.join('\n'));
QString desc;
if (tags & WithSummary)
desc = summary;
if (!details.isEmpty()) {
if (!desc.isEmpty())
desc.append('\n');
desc.append(details.join('\n'));
}
return desc;
}
@@ -120,32 +125,37 @@ QIcon Task::icon() const
return m_icon;
}
QString Task::toolTip(const QString &extraHeading) const
QString Task::formattedDescription(DescriptionTags tags, const QString &extraHeading) const
{
if (isNull())
return {};
QString text = description();
QString text = description(tags);
const int offset = (tags & WithSummary) ? 0 : summary.size() + 1;
static const QString linkTagStartPlaceholder("__QTC_LINK_TAG_START__");
static const QString linkTagEndPlaceholder("__QTC_LINK_TAG_END__");
static const QString linkEndPlaceholder("__QTC_LINK_END__");
for (auto formatRange = formats.crbegin(); formatRange != formats.crend(); ++formatRange) {
if (!formatRange->format.isAnchor())
continue;
text.insert(formatRange->start + formatRange->length, linkEndPlaceholder);
text.insert(formatRange->start, QString::fromLatin1("%1%2%3").arg(
if (tags & WithLinks) {
for (auto formatRange = formats.crbegin(); formatRange != formats.crend(); ++formatRange) {
if (!formatRange->format.isAnchor())
continue;
text.insert(formatRange->start - offset + formatRange->length, linkEndPlaceholder);
text.insert(formatRange->start - offset, QString::fromLatin1("%1%2%3").arg(
linkTagStartPlaceholder, formatRange->format.anchorHref(), linkTagEndPlaceholder));
}
}
text = text.toHtmlEscaped();
text.replace(linkEndPlaceholder, "</a>");
text.replace(linkTagStartPlaceholder, "<a href=\"");
text.replace(linkTagEndPlaceholder, "\">");
if (tags & WithLinks) {
text.replace(linkEndPlaceholder, "</a>");
text.replace(linkTagStartPlaceholder, "<a href=\"");
text.replace(linkTagEndPlaceholder, "\">");
}
const QString htmlExtraHeading = extraHeading.isEmpty()
? QString()
: QString::fromUtf8("<b>%1</b><br/>").arg(extraHeading);
return QString::fromUtf8("<html><body>%1<code style=\"white-space:pre;font-family:monospace\">"
"%2</code></body></html>")
.arg(htmlExtraHeading, text);
return QString::fromUtf8("<html><body>%1<code style=\"white-space:pre;font-family:%2\">"
"%3</code></body></html>")
.arg(htmlExtraHeading, TextEditor::FontSettings::defaultFixedFontFamily(), text);
}
//

View File

@@ -38,6 +38,9 @@ public:
};
using Options = char;
enum DescriptionTag { WithSummary = 1, WithLinks = 2 };
using DescriptionTags = QFlags<DescriptionTag>;
Task() = default;
Task(TaskType type, const QString &description,
const Utils::FilePath &file, int line, Utils::Id category,
@@ -49,9 +52,9 @@ public:
bool isNull() const;
void clear();
void setFile(const Utils::FilePath &file);
QString description() const;
QString description(DescriptionTags tags = WithSummary) const;
QIcon icon() const;
QString toolTip(const QString &extraHeading = {}) const;
QString formattedDescription(DescriptionTags tags, const QString &extraHeading = {}) const;
friend PROJECTEXPLORER_EXPORT bool operator==(const Task &t1, const Task &t2);
friend PROJECTEXPLORER_EXPORT bool operator<(const Task &a, const Task &b);

View File

@@ -51,8 +51,9 @@ public:
: Tr::tr("Warning"));
setPriority(task.type == Task::Error ? TextEditor::TextMark::NormalPriority
: TextEditor::TextMark::LowPriority);
setToolTip(task.toolTip(task.category == Constants::TASK_CATEGORY_COMPILE
? Tr::tr("Build Issue") : QString()));
setToolTip(task.formattedDescription({Task::WithSummary | Task::WithLinks},
task.category == Constants::TASK_CATEGORY_COMPILE
? Tr::tr("Build Issue") : QString()));
setIcon(task.icon());
setVisible(!task.icon().isNull());
}

View File

@@ -210,58 +210,80 @@ void TaskModel::clearTasks(Utils::Id categoryId)
QModelIndex TaskModel::index(int row, int column, const QModelIndex &parent) const
{
if (parent.isValid())
return QModelIndex();
return createIndex(row, column, quintptr(parent.row() + 1));
return createIndex(row, column);
}
QModelIndex TaskModel::parent(const QModelIndex &child) const
{
Q_UNUSED(child)
return QModelIndex();
if (child.internalId())
return index(child.internalId() - 1, 0);
return {};
}
int TaskModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_tasks.count();
if (!parent.isValid())
return m_tasks.count();
if (parent.column() != 0)
return 0;
return task(parent).details.isEmpty() ? 0 : 1;
}
int TaskModel::columnCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : 1;
return parent.isValid() ? 1 : 2;
}
QVariant TaskModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
if (!index.isValid() || row < 0 || row >= m_tasks.count() || index.column() != 0)
return QVariant();
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index.parent())
|| index.column() >= columnCount(index.parent())) {
return {};
}
if (role == TaskModel::File)
return m_tasks.at(index.row()).file.toString();
else if (role == TaskModel::Line)
return m_tasks.at(index.row()).line;
else if (role == TaskModel::MovedLine)
return m_tasks.at(index.row()).movedLine;
else if (role == TaskModel::Description)
return m_tasks.at(index.row()).description();
else if (role == TaskModel::FileNotFound)
return m_fileNotFound.value(m_tasks.at(index.row()).file.toString());
else if (role == TaskModel::Type)
return (int)m_tasks.at(index.row()).type;
else if (role == TaskModel::Category)
return m_tasks.at(index.row()).category.uniqueIdentifier();
else if (role == TaskModel::Icon)
return m_tasks.at(index.row()).icon();
else if (role == TaskModel::Task_t)
return QVariant::fromValue(task(index));
return QVariant();
if (index.internalId()) {
const Task &task = m_tasks.at(index.internalId() - 1);
if (role != Qt::DisplayRole)
return {};
return task.formattedDescription(Task::WithLinks);
}
static const auto lineString = [](const Task &task) {
QString file = task.file.fileName();
const int line = task.movedLine > 0 ? task.movedLine : task.line;
if (line > 0)
file.append(':').append(QString::number(line));
return file;
};
const Task &task = m_tasks.at(index.row());
if (index.column() == 1) {
if (role == Qt::DisplayRole)
return lineString(task);
if (role == Qt::ToolTipRole)
return task.file.toUserOutput();
return {};
}
switch (role) {
case Qt::DecorationRole:
return task.icon();
case Qt::DisplayRole:
return task.summary;
case TaskModel::Description:
return task.description();
}
return {};
}
Task TaskModel::task(const QModelIndex &index) const
{
int row = index.row();
if (!index.isValid() || row < 0 || row >= m_tasks.count())
if (!index.isValid() || row < 0 || row >= m_tasks.count() || index.internalId()
|| index.column() > 0) {
return Task();
}
return m_tasks.at(row);
}

View File

@@ -44,7 +44,7 @@ public:
int sizeOfLineNumber(const QFont &font);
void setFileNotFound(const QModelIndex &index, bool b);
enum Roles { File = Qt::UserRole, Line, MovedLine, Description, FileNotFound, Type, Category, Icon, Task_t };
enum Roles { Description = Qt::UserRole, };
int taskCount(Utils::Id categoryId);
int errorTaskCount(Utils::Id categoryId);

View File

@@ -22,6 +22,7 @@
#include <utils/algorithm.h>
#include <utils/fileinprojectfinder.h>
#include <utils/hostosinfo.h>
#include <utils/itemviews.h>
#include <utils/outputformatter.h>
#include <utils/qtcassert.h>
@@ -30,22 +31,22 @@
#include <utils/tooltip/tooltip.h>
#include <utils/utilsicons.h>
#include <QAbstractTextDocumentLayout>
#include <QApplication>
#include <QDir>
#include <QLabel>
#include <QMenu>
#include <QPainter>
#include <QScrollBar>
#include <QStyledItemDelegate>
#include <QTextDocument>
#include <QToolButton>
#include <QVBoxLayout>
using namespace Utils;
namespace {
const int ELLIPSIS_GRADIENT_WIDTH = 16;
const char SESSION_FILTER_CATEGORIES[] = "TaskWindow.Categories";
const char SESSION_FILTER_WARNINGS[] = "TaskWindow.IncludeWarnings";
}
namespace ProjectExplorer {
@@ -87,175 +88,43 @@ bool ITaskHandler::canHandle(const Tasks &tasks) const
namespace Internal {
class TaskView : public ListView
class TaskView : public TreeView
{
public:
TaskView(QWidget *parent = nullptr);
~TaskView() override;
TaskView() { setMouseTracking(true); }
void resizeColumns();
private:
void resizeEvent(QResizeEvent *e) override;
void keyReleaseEvent(QKeyEvent *e) override;
bool event(QEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
QString anchorAt(const QPoint &pos);
void showToolTip(const Task &task, const QPoint &pos);
QString m_hoverAnchor;
QString m_clickAnchor;
};
class TaskDelegate : public QStyledItemDelegate
{
Q_OBJECT
friend class TaskView; // for using Positions::minimumSize()
public:
TaskDelegate(QObject * parent = nullptr);
~TaskDelegate() override;
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
using QStyledItemDelegate::QStyledItemDelegate;
QTextDocument &doc() { return m_doc; }
private:
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
// TaskView uses this method if the size of the taskview changes
void emitSizeHintChanged(const QModelIndex &index);
bool needsSpecialHandling(const QModelIndex &index) const;
private:
void generateGradientPixmap(int width, int height, QColor color, bool selected) const;
mutable int m_cachedHeight = 0;
mutable QFont m_cachedFont;
/*
+------------------------------------------------------------------------------------------+
| TASKICONAREA TEXTAREA FILEAREA LINEAREA |
+------------------------------------------------------------------------------------------+
*/
class Positions
{
public:
Positions(const QStyleOptionViewItem &options, TaskModel *model) :
m_totalWidth(options.rect.width()),
m_maxFileLength(model->sizeOfFile(options.font)),
m_maxLineLength(model->sizeOfLineNumber(options.font)),
m_realFileLength(m_maxFileLength),
m_top(options.rect.top()),
m_bottom(options.rect.bottom())
{
int flexibleArea = lineAreaLeft() - textAreaLeft() - ITEM_SPACING;
if (m_maxFileLength > flexibleArea / 2)
m_realFileLength = flexibleArea / 2;
m_fontHeight = QFontMetrics(options.font).height();
}
int top() const { return m_top + ITEM_MARGIN; }
int left() const { return ITEM_MARGIN; }
int right() const { return m_totalWidth - ITEM_MARGIN; }
int bottom() const { return m_bottom; }
int firstLineHeight() const { return m_fontHeight + 1; }
static int minimumHeight() { return taskIconHeight() + 2 * ITEM_MARGIN; }
int taskIconLeft() const { return left(); }
static int taskIconWidth() { return TASK_ICON_SIZE; }
static int taskIconHeight() { return TASK_ICON_SIZE; }
int taskIconRight() const { return taskIconLeft() + taskIconWidth(); }
QRect taskIcon() const { return QRect(taskIconLeft(), top(), taskIconWidth(), taskIconHeight()); }
int textAreaLeft() const { return taskIconRight() + ITEM_SPACING; }
int textAreaWidth() const { return textAreaRight() - textAreaLeft(); }
int textAreaRight() const { return fileAreaLeft() - ITEM_SPACING; }
QRect textArea() const { return QRect(textAreaLeft(), top(), textAreaWidth(), firstLineHeight()); }
int fileAreaLeft() const { return fileAreaRight() - fileAreaWidth(); }
int fileAreaWidth() const { return m_realFileLength; }
int fileAreaRight() const { return lineAreaLeft() - ITEM_SPACING; }
QRect fileArea() const { return QRect(fileAreaLeft(), top(), fileAreaWidth(), firstLineHeight()); }
int lineAreaLeft() const { return lineAreaRight() - lineAreaWidth(); }
int lineAreaWidth() const { return m_maxLineLength; }
int lineAreaRight() const { return right(); }
QRect lineArea() const { return QRect(lineAreaLeft(), top(), lineAreaWidth(), firstLineHeight()); }
private:
int m_totalWidth;
int m_maxFileLength;
int m_maxLineLength;
int m_realFileLength;
int m_top;
int m_bottom;
int m_fontHeight;
static const int TASK_ICON_SIZE = 16;
static const int ITEM_MARGIN = 2;
static const int ITEM_SPACING = 2 * ITEM_MARGIN;
};
mutable QTextDocument m_doc;
};
TaskView::TaskView(QWidget *parent)
: ListView(parent)
{
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
setAutoScroll(false); // QTCREATORBUG-25101
setUniformItemSizes(true);
QFontMetrics fm(font());
int vStepSize = fm.height() + 3;
if (vStepSize < TaskDelegate::Positions::minimumHeight())
vStepSize = TaskDelegate::Positions::minimumHeight();
verticalScrollBar()->setSingleStep(vStepSize);
}
TaskView::~TaskView() = default;
void TaskView::resizeEvent(QResizeEvent *e)
{
Q_UNUSED(e)
static_cast<TaskDelegate *>(itemDelegate())->emitSizeHintChanged(selectionModel()->currentIndex());
}
void TaskView::keyReleaseEvent(QKeyEvent *e)
{
ListView::keyReleaseEvent(e);
if (e->key() == Qt::Key_Space) {
const Task task = static_cast<TaskFilterModel *>(model())->task(currentIndex());
if (!task.isNull()) {
const QPoint toolTipPos = mapToGlobal(visualRect(currentIndex()).topLeft());
QMetaObject::invokeMethod(this, [this, task, toolTipPos] {
showToolTip(task, toolTipPos); }, Qt::QueuedConnection);
}
}
}
bool TaskView::event(QEvent *e)
{
if (e->type() != QEvent::ToolTip)
return QListView::event(e);
const auto helpEvent = static_cast<QHelpEvent*>(e);
const Task task = static_cast<TaskFilterModel *>(model())->task(indexAt(helpEvent->pos()));
if (task.isNull())
return QListView::event(e);
showToolTip(task, helpEvent->globalPos());
e->accept();
return true;
}
void TaskView::showToolTip(const Task &task, const QPoint &pos)
{
const QString toolTip = task.toolTip();
if (!toolTip.isEmpty()) {
const auto label = new QLabel(toolTip);
connect(label, &QLabel::linkActivated, [](const QString &link) {
Core::EditorManager::openEditorAt(OutputLineParser::parseLinkTarget(link), {},
Core::EditorManager::SwitchSplitIfAlreadyVisible);
});
const auto layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(label);
ToolTip::show(pos, layout);
} else {
ToolTip::hideImmediately();
}
}
/////
// TaskWindow
/////
@@ -271,7 +140,7 @@ public:
Internal::TaskModel *m_model;
Internal::TaskFilterModel *m_filter;
Internal::TaskView *m_listview;
TaskView m_treeView;
Core::IContext *m_taskWindowContext;
QMenu *m_contextMenu;
QMap<const QAction *, ITaskHandler *> m_actionToHandlerMap;
@@ -300,43 +169,46 @@ TaskWindow::TaskWindow() : d(std::make_unique<TaskWindowPrivate>())
{
d->m_model = new Internal::TaskModel(this);
d->m_filter = new Internal::TaskFilterModel(d->m_model);
d->m_listview = new Internal::TaskView;
auto agg = new Aggregation::Aggregate;
agg->add(d->m_listview);
agg->add(new Core::ItemViewFind(d->m_listview, TaskModel::Description));
agg->add(&d->m_treeView);
agg->add(new Core::ItemViewFind(&d->m_treeView, TaskModel::Description));
d->m_listview->setModel(d->m_filter);
d->m_listview->setFrameStyle(QFrame::NoFrame);
d->m_listview->setWindowTitle(displayName());
d->m_listview->setSelectionMode(QAbstractItemView::ExtendedSelection);
auto *tld = new Internal::TaskDelegate(this);
d->m_listview->setItemDelegate(tld);
d->m_listview->setWindowIcon(Icons::WINDOW.icon());
d->m_listview->setContextMenuPolicy(Qt::ActionsContextMenu);
d->m_listview->setAttribute(Qt::WA_MacShowFocusRect, false);
d->m_treeView.setHeaderHidden(true);
d->m_treeView.setExpandsOnDoubleClick(false);
d->m_treeView.setAlternatingRowColors(true);
d->m_treeView.setTextElideMode(Qt::ElideMiddle);
d->m_treeView.setItemDelegate(new TaskDelegate(this));
d->m_treeView.setModel(d->m_filter);
d->m_treeView.setFrameStyle(QFrame::NoFrame);
d->m_treeView.setWindowTitle(displayName());
d->m_treeView.setSelectionMode(QAbstractItemView::ExtendedSelection);
d->m_treeView.setWindowIcon(Icons::WINDOW.icon());
d->m_treeView.setContextMenuPolicy(Qt::ActionsContextMenu);
d->m_treeView.setAttribute(Qt::WA_MacShowFocusRect, false);
d->m_treeView.resizeColumns();
d->m_taskWindowContext = new Core::IContext(d->m_listview);
d->m_taskWindowContext->setWidget(d->m_listview);
d->m_taskWindowContext = new Core::IContext(&d->m_treeView);
d->m_taskWindowContext->setWidget(&d->m_treeView);
d->m_taskWindowContext->setContext(Core::Context(Core::Constants::C_PROBLEM_PANE));
Core::ICore::addContextObject(d->m_taskWindowContext);
connect(d->m_listview->selectionModel(), &QItemSelectionModel::currentChanged,
this, [this](const QModelIndex &index) { d->m_listview->scrollTo(index); });
connect(d->m_listview, &QAbstractItemView::activated,
connect(d->m_treeView.selectionModel(), &QItemSelectionModel::currentChanged,
this, [this](const QModelIndex &index) { d->m_treeView.scrollTo(index); });
connect(&d->m_treeView, &QAbstractItemView::activated,
this, &TaskWindow::triggerDefaultHandler);
connect(d->m_listview->selectionModel(), &QItemSelectionModel::selectionChanged,
connect(d->m_treeView.selectionModel(), &QItemSelectionModel::selectionChanged,
this, [this] {
const Tasks tasks = d->m_filter->tasks(d->m_listview->selectionModel()->selectedIndexes());
const Tasks tasks = d->m_filter->tasks(d->m_treeView.selectionModel()->selectedIndexes());
for (QAction * const action : std::as_const(d->m_actions)) {
ITaskHandler * const h = d->handler(action);
action->setEnabled(h && h->canHandle(tasks));
}
});
d->m_contextMenu = new QMenu(d->m_listview);
d->m_contextMenu = new QMenu(&d->m_treeView);
d->m_listview->setContextMenuPolicy(Qt::ActionsContextMenu);
d->m_treeView.setContextMenuPolicy(Qt::ActionsContextMenu);
d->m_filterWarningsButton = createFilterButton(
Utils::Icons::WARNING_TOOLBAR.icon(),
@@ -391,7 +263,6 @@ TaskWindow::TaskWindow() : d(std::make_unique<TaskWindowPrivate>())
TaskWindow::~TaskWindow()
{
delete d->m_filterWarningsButton;
delete d->m_listview;
delete d->m_filter;
delete d->m_model;
}
@@ -415,7 +286,7 @@ void TaskWindow::delayedInitialization()
connect(action, &QAction::triggered, this, [this, action] {
ITaskHandler *h = d->handler(action);
if (h)
h->handle(d->m_filter->tasks(d->m_listview->selectionModel()->selectedIndexes()));
h->handle(d->m_filter->tasks(d->m_treeView.selectionModel()->selectedIndexes()));
});
d->m_actions << action;
@@ -425,7 +296,7 @@ void TaskWindow::delayedInitialization()
Core::ActionManager::registerAction(action, id, d->m_taskWindowContext->context(), true);
action = cmd->action();
}
d->m_listview->addAction(action);
d->m_treeView.addAction(action);
}
}
@@ -441,7 +312,7 @@ QString TaskWindow::displayName() const
QWidget *TaskWindow::outputWidget(QWidget *)
{
return d->m_listview;
return &d->m_treeView;
}
void TaskWindow::clearTasks(Id categoryId)
@@ -545,7 +416,7 @@ void TaskWindow::showTask(const Task &task)
int sourceRow = d->m_model->rowForTask(task);
QModelIndex sourceIdx = d->m_model->index(sourceRow, 0);
QModelIndex filterIdx = d->m_filter->mapFromSource(sourceIdx);
d->m_listview->setCurrentIndex(filterIdx);
d->m_treeView.setCurrentIndex(filterIdx);
popup(Core::IOutputPane::ModeSwitch);
}
@@ -562,7 +433,10 @@ void TaskWindow::triggerDefaultHandler(const QModelIndex &index)
if (!index.isValid() || !d->m_defaultHandler)
return;
Task task(d->m_filter->task(index));
QModelIndex taskIndex = index;
if (index.parent().isValid())
taskIndex = index.parent();
Task task(d->m_filter->task(taskIndex));
if (task.isNull())
return;
@@ -579,7 +453,7 @@ void TaskWindow::triggerDefaultHandler(const QModelIndex &index)
d->m_defaultHandler->handle(task);
} else {
if (!task.file.exists())
d->m_model->setFileNotFound(index, true);
d->m_model->setFileNotFound(taskIndex, true);
}
}
@@ -645,7 +519,7 @@ void TaskWindow::clearContents()
bool TaskWindow::hasFocus() const
{
return d->m_listview->window()->focusWidget() == d->m_listview;
return d->m_treeView.window()->focusWidget() == &d->m_treeView;
}
bool TaskWindow::canFocus() const
@@ -656,9 +530,13 @@ bool TaskWindow::canFocus() const
void TaskWindow::setFocus()
{
if (d->m_filter->rowCount()) {
d->m_listview->setFocus();
if (d->m_listview->currentIndex() == QModelIndex())
d->m_listview->setCurrentIndex(d->m_filter->index(0,0, QModelIndex()));
d->m_treeView.setFocus();
if (!d->m_treeView.currentIndex().isValid())
d->m_treeView.setCurrentIndex(d->m_filter->index(0,0, QModelIndex()));
if (d->m_treeView.selectionModel()->selection().isEmpty()) {
d->m_treeView.selectionModel()->setCurrentIndex(d->m_treeView.currentIndex(),
QItemSelectionModel::Select);
}
}
}
@@ -676,7 +554,7 @@ void TaskWindow::goToNext()
{
if (!canNext())
return;
QModelIndex startIndex = d->m_listview->currentIndex();
QModelIndex startIndex = d->m_treeView.currentIndex();
QModelIndex currentIndex = startIndex;
if (startIndex.isValid()) {
@@ -691,7 +569,7 @@ void TaskWindow::goToNext()
} else {
currentIndex = d->m_filter->index(0, 0);
}
d->m_listview->setCurrentIndex(currentIndex);
d->m_treeView.setCurrentIndex(currentIndex);
triggerDefaultHandler(currentIndex);
}
@@ -699,7 +577,7 @@ void TaskWindow::goToPrev()
{
if (!canPrevious())
return;
QModelIndex startIndex = d->m_listview->currentIndex();
QModelIndex startIndex = d->m_treeView.currentIndex();
QModelIndex currentIndex = startIndex;
if (startIndex.isValid()) {
@@ -714,7 +592,7 @@ void TaskWindow::goToPrev()
} else {
currentIndex = d->m_filter->index(0, 0);
}
d->m_listview->setCurrentIndex(currentIndex);
d->m_treeView.setCurrentIndex(currentIndex);
triggerDefaultHandler(currentIndex);
}
@@ -729,149 +607,161 @@ bool TaskWindow::canNavigate() const
return true;
}
/////
// Delegate
/////
void TaskDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (!needsSpecialHandling(index)) {
QStyledItemDelegate::paint(painter, option, index);
return;
}
TaskDelegate::TaskDelegate(QObject *parent) :
QStyledItemDelegate(parent)
{ }
QStyleOptionViewItem options = option;
initStyleOption(&options, index);
TaskDelegate::~TaskDelegate() = default;
painter->save();
m_doc.setHtml(options.text);
options.text = "";
options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter);
painter->translate(options.rect.left(), options.rect.top());
QRect clip(0, 0, options.rect.width(), options.rect.height());
QAbstractTextDocumentLayout::PaintContext paintContext;
paintContext.palette = options.palette;
painter->setClipRect(clip);
paintContext.clip = clip;
if (qobject_cast<const QAbstractItemView *>(options.widget)
->selectionModel()->isSelected(index)) {
QAbstractTextDocumentLayout::Selection selection;
selection.cursor = QTextCursor(&m_doc);
selection.cursor.select(QTextCursor::Document);
selection.format.setBackground(options.palette.brush(QPalette::Highlight));
selection.format.setForeground(options.palette.brush(QPalette::HighlightedText));
paintContext.selections << selection;
}
m_doc.documentLayout()->draw(painter, paintContext);
painter->restore();
}
QSize TaskDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
if (!needsSpecialHandling(index))
return QStyledItemDelegate::sizeHint(option, index);
QSize s;
s.setWidth(option.rect.width());
if (option.font == m_cachedFont && m_cachedHeight > 0) {
s.setHeight(m_cachedHeight);
return s;
}
s.setHeight(option.fontMetrics.height() + 3);
if (s.height() < Positions::minimumHeight())
s.setHeight(Positions::minimumHeight());
m_cachedHeight = s.height();
m_cachedFont = option.font;
return s;
QStyleOptionViewItem options = option;
initStyleOption(&options, index);
m_doc.setHtml(options.text);
m_doc.setTextWidth(options.rect.width());
return QSize(m_doc.idealWidth(), m_doc.size().height());
}
void TaskDelegate::emitSizeHintChanged(const QModelIndex &index)
bool TaskDelegate::needsSpecialHandling(const QModelIndex &index) const
{
emit sizeHintChanged(index);
QModelIndex sourceIndex = index;
if (const auto proxyModel = qobject_cast<const QAbstractProxyModel *>(index.model()))
sourceIndex = proxyModel->mapToSource(index);
return sourceIndex.internalId();
}
void TaskDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
void TaskView::resizeColumns()
{
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
painter->save();
setColumnWidth(0, width() * 0.85);
setColumnWidth(1, width() * 0.15);
}
QFontMetrics fm(opt.font);
QColor backgroundColor;
QColor textColor;
void TaskView::resizeEvent(QResizeEvent *e)
{
TreeView::resizeEvent(e);
resizeColumns();
}
auto view = qobject_cast<const QAbstractItemView *>(opt.widget);
const bool selected = view->selectionModel()->isSelected(index);
void TaskView::mousePressEvent(QMouseEvent *e)
{
m_clickAnchor = anchorAt(e->pos());
if (m_clickAnchor.isEmpty())
TreeView::mousePressEvent(e);
}
if (selected) {
painter->setBrush(opt.palette.highlight().color());
backgroundColor = opt.palette.highlight().color();
} else {
painter->setBrush(opt.palette.window().color());
backgroundColor = opt.palette.window().color();
void TaskView::mouseMoveEvent(QMouseEvent *e)
{
const QString anchor = anchorAt(e->pos());
if (m_clickAnchor != anchor)
m_clickAnchor.clear();
if (m_hoverAnchor != anchor) {
m_hoverAnchor = anchor;
if (!m_hoverAnchor.isEmpty())
QApplication::setOverrideCursor(QCursor(Qt::PointingHandCursor));
else
QApplication::restoreOverrideCursor();
}
painter->setPen(Qt::NoPen);
painter->drawRect(opt.rect);
}
// Set Text Color
if (selected)
textColor = opt.palette.highlightedText().color();
else
textColor = opt.palette.text().color();
painter->setPen(textColor);
auto model = static_cast<TaskFilterModel *>(view->model())->taskModel();
Positions positions(opt, model);
// Paint TaskIconArea:
QIcon icon = index.data(TaskModel::Icon).value<QIcon>();
painter->drawPixmap(positions.left(), positions.top(),
icon.pixmap(Positions::taskIconWidth(), Positions::taskIconHeight()));
// Paint TextArea:
QString bottom = index.data(TaskModel::Description).toString().split(QLatin1Char('\n')).first();
painter->setClipRect(positions.textArea());
painter->drawText(positions.textAreaLeft(), positions.top() + fm.ascent(), bottom);
if (fm.horizontalAdvance(bottom) > positions.textAreaWidth()) {
// draw a gradient to mask the text
int gradientStart = positions.textAreaRight() - ELLIPSIS_GRADIENT_WIDTH + 1;
QLinearGradient lg(gradientStart, 0, gradientStart + ELLIPSIS_GRADIENT_WIDTH, 0);
lg.setColorAt(0, Qt::transparent);
lg.setColorAt(1, backgroundColor);
painter->fillRect(gradientStart, positions.top(), ELLIPSIS_GRADIENT_WIDTH, positions.firstLineHeight(), lg);
void TaskView::mouseReleaseEvent(QMouseEvent *e)
{
if (m_clickAnchor.isEmpty()) {
TreeView::mouseReleaseEvent(e);
return;
}
// Paint FileArea
QString file = index.data(TaskModel::File).toString();
const int pos = file.lastIndexOf(QLatin1Char('/'));
if (pos != -1)
file = file.mid(pos +1);
const int realFileWidth = fm.horizontalAdvance(file);
painter->setClipRect(positions.fileArea());
painter->drawText(qMin(positions.fileAreaLeft(), positions.fileAreaRight() - realFileWidth),
positions.top() + fm.ascent(), file);
if (realFileWidth > positions.fileAreaWidth()) {
// draw a gradient to mask the text
int gradientStart = positions.fileAreaLeft() - 1;
QLinearGradient lg(gradientStart + ELLIPSIS_GRADIENT_WIDTH, 0, gradientStart, 0);
lg.setColorAt(0, Qt::transparent);
lg.setColorAt(1, backgroundColor);
painter->fillRect(gradientStart, positions.top(), ELLIPSIS_GRADIENT_WIDTH, positions.firstLineHeight(), lg);
const QString anchor = anchorAt(e->pos());
if (anchor == m_clickAnchor) {
Core::EditorManager::openEditorAt(OutputLineParser::parseLinkTarget(m_clickAnchor), {},
Core::EditorManager::SwitchSplitIfAlreadyVisible);
}
m_clickAnchor.clear();
}
void TaskView::keyReleaseEvent(QKeyEvent *e)
{
TreeView::keyReleaseEvent(e);
if (e->key() == Qt::Key_Space) {
const Task task = static_cast<TaskFilterModel *>(model())->task(currentIndex());
if (!task.isNull()) {
const QPoint toolTipPos = mapToGlobal(visualRect(currentIndex()).topLeft());
QMetaObject::invokeMethod(this, [this, task, toolTipPos] {
showToolTip(task, toolTipPos); }, Qt::QueuedConnection);
}
}
}
bool TaskView::event(QEvent *e)
{
if (e->type() != QEvent::ToolTip)
return TreeView::event(e);
const auto helpEvent = static_cast<QHelpEvent*>(e);
const Task task = static_cast<TaskFilterModel *>(model())->task(indexAt(helpEvent->pos()));
if (task.isNull())
return TreeView::event(e);
showToolTip(task, helpEvent->globalPos());
e->accept();
return true;
}
void TaskView::showToolTip(const Task &task, const QPoint &pos)
{
if (task.details.isEmpty()) {
ToolTip::hideImmediately();
return;
}
// Paint LineArea
int line = index.data(TaskModel::Line).toInt();
int movedLine = index.data(TaskModel::MovedLine).toInt();
QString lineText;
const auto layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(new QLabel(task.formattedDescription({})));
ToolTip::show(pos, layout);
}
if (line == -1) {
// No line information at all
} else if (movedLine == -1) {
// removed the line, but we had line information, show the line in ()
QFont f = painter->font();
f.setItalic(true);
painter->setFont(f);
lineText = QLatin1Char('(') + QString::number(line) + QLatin1Char(')');
} else if (movedLine != line) {
// The line was moved
QFont f = painter->font();
f.setItalic(true);
painter->setFont(f);
lineText = QString::number(movedLine);
} else {
lineText = QString::number(line);
}
QString TaskView::anchorAt(const QPoint &pos)
{
const QModelIndex index = indexAt(pos);
if (!index.isValid() || !index.internalId())
return {};
painter->setClipRect(positions.lineArea());
const int realLineWidth = fm.horizontalAdvance(lineText);
painter->drawText(positions.lineAreaRight() - realLineWidth, positions.top() + fm.ascent(), lineText);
painter->setClipRect(opt.rect);
// Separator lines
painter->setPen(QColor::fromRgb(150,150,150));
const QRectF borderRect = QRectF(opt.rect).adjusted(0.5, 0.5, -0.5, -0.5);
painter->drawLine(borderRect.bottomLeft(), borderRect.bottomRight());
painter->restore();
const QRect itemRect = visualRect(index);
QTextDocument &doc = static_cast<TaskDelegate *>(itemDelegate())->doc();
doc.setHtml(model()->data(index, Qt::DisplayRole).toString());
const QAbstractTextDocumentLayout * const textLayout = doc.documentLayout();
QTC_ASSERT(textLayout, return {});
return textLayout->anchorAt(pos - itemRect.topLeft());
}
} // namespace Internal
} // namespace ProjectExplorer
#include "taskwindow.moc"