ProjectExplorer: Allow multi-selection in issues pane

Users should be able to copy or remove several issues at once.

Fixes: QTCREATORBUG-25547
Fixes: QTCREATORBUG-26720
Change-Id: I1ac75a3445c37200b6a01dd8c5d79d2b69c54a3c
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
Christian Kandeler
2021-12-13 12:06:11 +01:00
parent 2c96ee2ee4
commit 5207374d5d
9 changed files with 99 additions and 60 deletions

View File

@@ -25,8 +25,6 @@
#include "copytaskhandler.h"
#include "task.h"
#include <coreplugin/coreconstants.h>
#include <QAction>
@@ -36,25 +34,27 @@
using namespace ProjectExplorer;
using namespace ProjectExplorer::Internal;
void CopyTaskHandler::handle(const Task &task)
void CopyTaskHandler::handle(const Tasks &tasks)
{
QString type;
switch (task.type) {
case Task::Error:
//: Task is of type: error
type = tr("error:") + QLatin1Char(' ');
break;
case Task::Warning:
//: Task is of type: warning
type = tr("warning:") + QLatin1Char(' ');
break;
default:
break;
QStringList lines;
for (const Task &task : tasks) {
QString type;
switch (task.type) {
case Task::Error:
//: Task is of type: error
type = tr("error:") + QLatin1Char(' ');
break;
case Task::Warning:
//: Task is of type: warning
type = tr("warning:") + QLatin1Char(' ');
break;
default:
break;
}
lines << task.file.toUserOutput() + ':' + QString::number(task.line)
+ ": " + type + task.description();
}
QApplication::clipboard()->setText(task.file.toUserOutput() + QLatin1Char(':') +
QString::number(task.line) + QLatin1String(": ")
+ type + task.description());
QApplication::clipboard()->setText(lines.join('\n'));
}
Utils::Id CopyTaskHandler::actionManagerId() const

View File

@@ -35,8 +35,9 @@ class CopyTaskHandler : public ITaskHandler
Q_OBJECT
public:
bool canHandle(const Task &) const override { return true; }
void handle(const Task &task) override;
CopyTaskHandler() : ITaskHandler(true) {}
void handle(const Tasks &tasks) override;
Utils::Id actionManagerId() const override;
QAction *createAction(QObject *parent) const override;
};

View File

@@ -26,6 +26,7 @@
#pragma once
#include "projectexplorer_export.h"
#include "task.h"
#include <utils/id.h>
@@ -37,21 +38,26 @@ class QAction;
QT_END_NAMESPACE
namespace ProjectExplorer {
class Task;
class PROJECTEXPLORER_EXPORT ITaskHandler : public QObject
{
Q_OBJECT
public:
ITaskHandler();
explicit ITaskHandler(bool isMultiHandler = false);
~ITaskHandler() override;
virtual bool isDefaultHandler() const { return false; }
virtual bool canHandle(const Task &) const = 0;
virtual void handle(const Task &) = 0;
virtual bool canHandle(const Task &) const { return m_isMultiHandler; }
virtual void handle(const Task &); // Non-multi-handlers should implement this.
virtual void handle(const Tasks &tasks); // Multi-handlers should implement this.
virtual Utils::Id actionManagerId() const { return Utils::Id(); }
virtual QAction *createAction(QObject *parent) const = 0;
bool canHandle(const Tasks &tasks) const;
private:
const bool m_isMultiHandler;
};
} // namespace ProjectExplorer

View File

@@ -33,9 +33,10 @@
namespace ProjectExplorer {
namespace Internal {
void RemoveTaskHandler::handle(const Task &task)
void RemoveTaskHandler::handle(const Tasks &tasks)
{
TaskHub::removeTask(task);
for (const Task &task : tasks)
TaskHub::removeTask(task);
}
QAction *RemoveTaskHandler::createAction(QObject *parent) const

View File

@@ -35,8 +35,9 @@ class RemoveTaskHandler : public ITaskHandler
Q_OBJECT
public:
bool canHandle(const Task &) const override { return true; }
void handle(const Task &task) override;
RemoveTaskHandler() : ITaskHandler(true) {}
void handle(const Tasks &tasks) override;
QAction *createAction(QObject *parent) const override;
};

View File

@@ -29,6 +29,7 @@
#include "task.h"
#include "taskhub.h"
#include <utils/algorithm.h>
#include <utils/qtcassert.h>
#include <QFileInfo>
@@ -286,6 +287,13 @@ Task TaskModel::task(const QModelIndex &index) const
return m_tasks.at(row);
}
Tasks TaskModel::tasks(const QModelIndexList &indexes) const
{
return Utils::filtered(
Utils::transform<Tasks>(indexes, [this](const QModelIndex &i) { return task(i); }),
[](const Task &t) { return !t.isNull(); });
}
QList<Utils::Id> TaskModel::categoryIds() const
{
QList<Utils::Id> categories = m_categories.keys();
@@ -360,6 +368,12 @@ void TaskFilterModel::setFilterIncludesWarnings(bool b)
invalidateFilter();
}
Tasks TaskFilterModel::tasks(const QModelIndexList &indexes) const
{
return taskModel()->tasks(Utils::transform(indexes, [this](const QModelIndex &i) {
return mapToSource(i); }));
}
int TaskFilterModel::issuesCount(int startRow, int endRow) const
{
int count = 0;

View File

@@ -49,6 +49,7 @@ public:
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
Task task(const QModelIndex &index) const;
Tasks tasks(const QModelIndexList &indexes) const;
QList<Utils::Id> categoryIds() const;
QString categoryDisplayName(Utils::Id categoryId) const;
@@ -142,6 +143,7 @@ public:
void setFilteredCategories(const QList<Utils::Id> &categoryIds) { m_categoryIds = categoryIds; invalidateFilter(); }
Task task(const QModelIndex &index) const { return taskModel()->task(mapToSource(index)); }
Tasks tasks(const QModelIndexList &indexes) const;
int issuesCount(int startRow, int endRow) const;
bool hasFile(const QModelIndex &index) const

View File

@@ -67,7 +67,7 @@ namespace ProjectExplorer {
static QList<ITaskHandler *> g_taskHandlers;
ITaskHandler::ITaskHandler()
ITaskHandler::ITaskHandler(bool isMultiHandler) : m_isMultiHandler(isMultiHandler)
{
g_taskHandlers.append(this);
}
@@ -77,6 +77,30 @@ ITaskHandler::~ITaskHandler()
g_taskHandlers.removeOne(this);
}
void ITaskHandler::handle(const Task &task)
{
QTC_ASSERT(m_isMultiHandler, return);
handle(Tasks{task});
}
void ITaskHandler::handle(const Tasks &tasks)
{
QTC_ASSERT(canHandle(tasks), return);
QTC_ASSERT(!m_isMultiHandler, return);
handle(tasks.first());
}
bool ITaskHandler::canHandle(const Tasks &tasks) const
{
if (tasks.isEmpty())
return false;
if (m_isMultiHandler)
return true;
if (tasks.size() > 1)
return false;
return canHandle(tasks.first());
}
namespace Internal {
class TaskView : public Utils::ListView
@@ -322,7 +346,7 @@ TaskWindow::TaskWindow() : d(std::make_unique<TaskWindowPrivate>())
d->m_listview->setModel(d->m_filter);
d->m_listview->setFrameStyle(QFrame::NoFrame);
d->m_listview->setWindowTitle(displayName());
d->m_listview->setSelectionMode(QAbstractItemView::SingleSelection);
d->m_listview->setSelectionMode(QAbstractItemView::ExtendedSelection);
auto *tld = new Internal::TaskDelegate(this);
d->m_listview->setItemDelegate(tld);
d->m_listview->setWindowIcon(Icons::WINDOW.icon());
@@ -336,11 +360,18 @@ TaskWindow::TaskWindow() : d(std::make_unique<TaskWindowPrivate>())
connect(d->m_listview->selectionModel(), &QItemSelectionModel::currentChanged,
tld, &TaskDelegate::currentChanged);
connect(d->m_listview->selectionModel(), &QItemSelectionModel::currentChanged,
this, &TaskWindow::currentChanged);
this, [this](const QModelIndex &index) { d->m_listview->scrollTo(index); });
connect(d->m_listview, &QAbstractItemView::activated,
this, &TaskWindow::triggerDefaultHandler);
connect(d->m_listview->selectionModel(), &QItemSelectionModel::selectionChanged,
this, [this] {
const Tasks tasks = d->m_filter->tasks(d->m_listview->selectionModel()->selectedIndexes());
for (QAction * const action : qAsConst(d->m_actions)) {
ITaskHandler * const h = d->handler(action);
action->setEnabled(h && h->canHandle(tasks));
}
});
d->m_contextMenu = new QMenu(d->m_listview);
@@ -417,6 +448,7 @@ void TaskWindow::delayedInitialization()
d->m_defaultHandler = h;
QAction *action = h->createAction(this);
action->setEnabled(false);
QTC_ASSERT(action, continue);
d->m_actionToHandlerMap.insert(action, h);
connect(action, &QAction::triggered, this, &TaskWindow::actionTriggered);
@@ -430,9 +462,6 @@ void TaskWindow::delayedInitialization()
}
d->m_listview->addAction(action);
}
// Disable everything for now:
currentChanged(QModelIndex());
}
QList<QWidget*> TaskWindow::toolBarWidgets() const
@@ -468,16 +497,6 @@ void TaskWindow::setCategoryVisibility(Utils::Id categoryId, bool visible)
d->m_filter->setFilteredCategories(categories);
}
void TaskWindow::currentChanged(const QModelIndex &index)
{
const Task task = index.isValid() ? d->m_filter->task(index) : Task();
foreach (QAction *action, d->m_actions) {
ITaskHandler *h = d->handler(action);
action->setEnabled((task.isNull() || !h) ? false : h->canHandle(task));
}
d->m_listview->scrollTo(index);
}
void TaskWindow::saveSettings()
{
QStringList categories = Utils::transform(d->m_filter->filteredCategories(), &Utils::Id::toString);
@@ -605,12 +624,7 @@ void TaskWindow::actionTriggered()
if (!h)
return;
QModelIndex index = d->m_listview->selectionModel()->currentIndex();
Task task = d->m_filter->task(index);
if (task.isNull())
return;
h->handle(task);
h->handle(d->m_filter->tasks(d->m_listview->selectionModel()->selectedIndexes()));
}
void TaskWindow::setShowWarnings(bool show)
@@ -774,11 +788,11 @@ QSize TaskDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelInd
initStyleOption(&opt, index);
auto view = qobject_cast<const QAbstractItemView *>(opt.widget);
const bool selected = (view->selectionModel()->currentIndex() == index);
const bool current = view->selectionModel()->currentIndex() == index;
QSize s;
s.setWidth(option.rect.width());
if (!selected && option.font == m_cachedFont && m_cachedHeight > 0) {
if (!current && option.font == m_cachedFont && m_cachedHeight > 0) {
s.setHeight(m_cachedHeight);
return s;
}
@@ -790,7 +804,7 @@ QSize TaskDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelInd
auto model = static_cast<TaskFilterModel *>(view->model())->taskModel();
Positions positions(option, model);
if (selected) {
if (current) {
QString description = index.data(TaskModel::Description).toString();
// Layout the description
int leading = fontLeading;
@@ -817,7 +831,7 @@ QSize TaskDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelInd
if (s.height() < Positions::minimumHeight())
s.setHeight(Positions::minimumHeight());
if (!selected) {
if (!current) {
m_cachedHeight = s.height();
m_cachedFont = option.font;
}
@@ -856,7 +870,8 @@ void TaskDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
QColor textColor;
auto view = qobject_cast<const QAbstractItemView *>(opt.widget);
bool selected = view->selectionModel()->currentIndex() == index;
const bool selected = view->selectionModel()->isSelected(index);
const bool current = view->selectionModel()->currentIndex() == index;
if (selected) {
painter->setBrush(opt.palette.highlight().color());
@@ -885,7 +900,7 @@ void TaskDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
icon.pixmap(Positions::taskIconWidth(), Positions::taskIconHeight()));
// Paint TextArea:
if (!selected) {
if (!current) {
// in small mode we lay out differently
QString bottom = index.data(TaskModel::Description).toString().split(QLatin1Char('\n')).first();
painter->setClipRect(positions.textArea());
@@ -908,7 +923,7 @@ void TaskDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
QTextLayout tl(description);
QVector<QTextLayout::FormatRange> formats = index.data(TaskModel::Task_t).value<Task>().formats;
for (QTextLayout::FormatRange &format : formats)
format.format.setForeground(opt.palette.highlightedText());
format.format.setForeground(textColor);
tl.setFormats(formats);
tl.beginLayout();
while (true) {

View File

@@ -91,7 +91,6 @@ private:
void openTask(const Task &task);
void clearTasks(Utils::Id categoryId);
void setCategoryVisibility(Utils::Id categoryId, bool visible);
void currentChanged(const QModelIndex &index);
void saveSettings();
void loadSettings();