diff --git a/src/plugins/projectexplorer/task.cpp b/src/plugins/projectexplorer/task.cpp index c2937e76ed4..03c2a282be0 100644 --- a/src/plugins/projectexplorer/task.cpp +++ b/src/plugins/projectexplorer/task.cpp @@ -8,8 +8,8 @@ #include "projectexplorertr.h" #include +#include #include - #include #include #include @@ -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, ""); - text.replace(linkTagStartPlaceholder, ""); + if (tags & WithLinks) { + text.replace(linkEndPlaceholder, ""); + text.replace(linkTagStartPlaceholder, ""); + } const QString htmlExtraHeading = extraHeading.isEmpty() ? QString() : QString::fromUtf8("%1
").arg(extraHeading); - return QString::fromUtf8("%1" - "%2") - .arg(htmlExtraHeading, text); + return QString::fromUtf8("%1" + "%3") + .arg(htmlExtraHeading, TextEditor::FontSettings::defaultFixedFontFamily(), text); } // diff --git a/src/plugins/projectexplorer/task.h b/src/plugins/projectexplorer/task.h index 27adeebf039..830cea79397 100644 --- a/src/plugins/projectexplorer/task.h +++ b/src/plugins/projectexplorer/task.h @@ -38,6 +38,9 @@ public: }; using Options = char; + enum DescriptionTag { WithSummary = 1, WithLinks = 2 }; + using DescriptionTags = QFlags; + 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); diff --git a/src/plugins/projectexplorer/taskhub.cpp b/src/plugins/projectexplorer/taskhub.cpp index 7c1cff54542..58e797de803 100644 --- a/src/plugins/projectexplorer/taskhub.cpp +++ b/src/plugins/projectexplorer/taskhub.cpp @@ -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()); } diff --git a/src/plugins/projectexplorer/taskmodel.cpp b/src/plugins/projectexplorer/taskmodel.cpp index b8f7d723063..c05614a445f 100644 --- a/src/plugins/projectexplorer/taskmodel.cpp +++ b/src/plugins/projectexplorer/taskmodel.cpp @@ -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); } diff --git a/src/plugins/projectexplorer/taskmodel.h b/src/plugins/projectexplorer/taskmodel.h index e10833f6131..e60d8a30a51 100644 --- a/src/plugins/projectexplorer/taskmodel.h +++ b/src/plugins/projectexplorer/taskmodel.h @@ -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); diff --git a/src/plugins/projectexplorer/taskwindow.cpp b/src/plugins/projectexplorer/taskwindow.cpp index db8dbf7d5ba..5f1c6aa9e00 100644 --- a/src/plugins/projectexplorer/taskwindow.cpp +++ b/src/plugins/projectexplorer/taskwindow.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -30,22 +31,22 @@ #include #include +#include +#include #include #include #include #include #include #include +#include #include #include 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(itemDelegate())->emitSizeHintChanged(selectionModel()->currentIndex()); -} - -void TaskView::keyReleaseEvent(QKeyEvent *e) -{ - ListView::keyReleaseEvent(e); - if (e->key() == Qt::Key_Space) { - const Task task = static_cast(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(e); - const Task task = static_cast(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 m_actionToHandlerMap; @@ -300,43 +169,46 @@ TaskWindow::TaskWindow() : d(std::make_unique()) { 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()) 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(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(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(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(view->model())->taskModel(); - Positions positions(opt, model); - - // Paint TaskIconArea: - QIcon icon = index.data(TaskModel::Icon).value(); - 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(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(e); + const Task task = static_cast(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(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"