Skip to content

Commit ae5222a

Browse files
committed
Add ActionsWidget and ActionsFilterModel for action management
Introduces ActionsWidget and ActionsFilterModel to provide a categorized, searchable UI for managing actions. Updates CMakeLists.txt to include new sources and integrates ActionsWidget into ExampleActionsPlugin. The new components support recursive filtering and dynamic detail views for actions.
1 parent b863c4a commit ae5222a

6 files changed

Lines changed: 335 additions & 1 deletion

File tree

ExampleActions/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../Common ${CMAKE_CURRENT_BINARY_DI
4040
set(PLUGIN_SOURCES
4141
src/ExampleActionsPlugin.h
4242
src/ExampleActionsPlugin.cpp
43+
src/ActionsWidget.h
44+
src/ActionsWidget.cpp
45+
src/ActionsFilterModel.h
46+
src/ActionsFilterModel.cpp
4347
PluginInfo.json
4448
)
4549

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#include "ActionsFilterModel.h"
2+
3+
ActionsFilterModel::ActionsFilterModel(QObject* parent) :
4+
QSortFilterProxyModel(parent)
5+
{
6+
}
7+
8+
void ActionsFilterModel::setSearchRoles(QVector<int> roles)
9+
{
10+
searchRoles = std::move(roles);
11+
}
12+
13+
bool ActionsFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
14+
{
15+
const QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent);
16+
17+
if (!idx.isValid())
18+
return false;
19+
20+
if (rowMatches(idx))
21+
return true;
22+
23+
const auto childCount = sourceModel()->rowCount(idx);
24+
25+
for (int i = 0; i < childCount; ++i)
26+
{
27+
if (filterAcceptsRow(i, idx))
28+
return true;
29+
}
30+
31+
return false;
32+
}
33+
34+
bool ActionsFilterModel::rowMatches(const QModelIndex& idx) const
35+
{
36+
const auto re = filterRegularExpression();
37+
if (!re.isValid() || re.pattern().isEmpty())
38+
return true;
39+
40+
// Always include DisplayRole at minimum
41+
const QString display = sourceModel()->data(idx, Qt::DisplayRole).toString();
42+
if (display.contains(re))
43+
return true;
44+
45+
for (int r : searchRoles) {
46+
const auto string = sourceModel()->data(idx, r).toString();
47+
48+
if (!string.isEmpty() && string.contains(re))
49+
return true;
50+
}
51+
52+
return false;
53+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#pragma once
2+
3+
#include <QSortFilterProxyModel>
4+
#include <QVector>
5+
6+
/**
7+
* A QSortFilterProxyModel that filters recursively through all child items.
8+
*
9+
* @author Thomas Kroes
10+
*/
11+
class ActionsFilterModel final : public QSortFilterProxyModel
12+
{
13+
Q_OBJECT
14+
public:
15+
16+
/**
17+
* Construct with optional parent.
18+
* @param parent The parent QObject.
19+
*/
20+
explicit ActionsFilterModel(QObject* parent = nullptr);
21+
22+
// Optional: include tooltips and ids in matching.
23+
void setSearchRoles(QVector<int> roles);
24+
25+
protected:
26+
27+
/**
28+
* Reimplemented to filter recursively.
29+
* @param sourceRow The row in the source model.
30+
* @param sourceParent The parent index in the source model.
31+
* @return True if the row should be included in the filtered model.
32+
*/
33+
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
34+
35+
private:
36+
37+
/**
38+
* Check if a row matches the current filter.
39+
* @param idx The index of the row to check.
40+
* @return True if the row matches the filter, false otherwise.
41+
*/
42+
bool rowMatches(const QModelIndex& idx) const;
43+
44+
45+
private:
46+
QVector<int> searchRoles; /** The roles to search in. */
47+
};
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#include "ActionsWidget.h"
2+
3+
ActionsWidget::ActionsWidget(QWidget* parent):
4+
QWidget(parent),
5+
_model(this),
6+
_proxy(this),
7+
_splitter(Qt::Horizontal, this)
8+
{
9+
_proxy.setSourceModel(&_model);
10+
_proxy.setFilterCaseSensitivity(Qt::CaseInsensitive);
11+
_proxy.setSearchRoles({Qt::ToolTipRole, Roles::ActionId});
12+
13+
_mainLayout.addWidget(&_splitter);
14+
15+
setLayout(&_mainLayout);
16+
17+
_leftPanelWidget.setLayout(&_leftPanelLayout);
18+
_rightPanelWidget.setLayout(&_rightPanelLayout);
19+
20+
_splitter.addWidget(&_leftPanelWidget);
21+
_splitter.addWidget(&_rightPanelWidget);
22+
23+
_splitter.setStretchFactor(0, 0);
24+
_splitter.setStretchFactor(1, 1);
25+
26+
_splitter.setSizes({ 320, 680 });
27+
28+
_leftPanelLayout.addWidget(&_search);
29+
_leftPanelLayout.addWidget(&_tree);
30+
31+
_rightPanelLayout.addWidget(&_detailsArea);
32+
33+
_search.setPlaceholderText("Search actions");
34+
35+
_tree.setModel(&_proxy);
36+
_tree.setHeaderHidden(true);
37+
_tree.setUniformRowHeights(true);
38+
_tree.setEditTriggers(QAbstractItemView::NoEditTriggers);
39+
_tree.setSelectionMode(QAbstractItemView::SingleSelection);
40+
_tree.setSelectionBehavior(QAbstractItemView::SelectRows);
41+
_tree.header()->setStretchLastSection(true);
42+
43+
_detailsLayout.setContentsMargins(0, 0, 0, 0);
44+
45+
_detailsArea.setWidgetResizable(true);
46+
_detailsArea.setWidget(&_detailsHost);
47+
48+
connect(&_search, &QLineEdit::textChanged, this, [this](const QString& text) {
49+
_proxy.setFilterRegularExpression(QRegularExpression(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption));
50+
51+
// UX: when searching, expand all; when empty, collapse to top-level
52+
if (!text.isEmpty())
53+
_tree.expandAll();
54+
else
55+
_tree.collapseAll();
56+
});
57+
58+
connect(_tree.selectionModel(), &QItemSelectionModel::currentChanged, this, &ActionsWidget::onCurrentChanged);
59+
}
60+
61+
void ActionsWidget::addAction(const QString& category, const QString& text, const QString& actionId, Factory factory, const QIcon& icon, const QString& toolTip)
62+
{
63+
const auto cat = category.trimmed().isEmpty() ? QStringLiteral("Uncategorized") : category.trimmed();
64+
65+
auto categoryItem = ensureCategory(cat);
66+
67+
auto actionItem = new QStandardItem(icon, text);
68+
69+
actionItem->setToolTip(toolTip);
70+
actionItem->setData(ActionNode, Roles::NodeType);
71+
actionItem->setData(actionId, Roles::ActionId);
72+
73+
categoryItem->appendRow(actionItem);
74+
75+
_factories.insert(actionId, std::move(factory));
76+
}
77+
78+
void ActionsWidget::onCurrentChanged(const QModelIndex& current, const QModelIndex& previous)
79+
{
80+
clearDetails();
81+
82+
if (!current.isValid())
83+
return;
84+
85+
const auto sourceIndex = _proxy.mapToSource(current);
86+
87+
auto item = _model.itemFromIndex(sourceIndex);
88+
89+
if (!item)
90+
return;
91+
92+
const auto type = item->data(Roles::NodeType).toInt();
93+
94+
if (type != ActionNode)
95+
return; // category selected: leave details empty (or show placeholder)
96+
97+
const auto actionId = item->data(Roles::ActionId).toString();
98+
99+
auto it = _factories.find(actionId);
100+
101+
if (it == _factories.end())
102+
return;
103+
104+
auto actionWidget = it.value()(&_detailsHost);
105+
106+
if (!actionWidget)
107+
return;
108+
109+
_detailsLayout.addWidget(actionWidget);
110+
_detailsLayout.addStretch(1);
111+
}
112+
113+
QStandardItem* ActionsWidget::ensureCategory(const QString& category)
114+
{
115+
if (auto it = _categories.find(category); it != _categories.end())
116+
return it.value();
117+
118+
auto catItem = new QStandardItem(category);
119+
120+
catItem->setData(CategoryNode, Roles::NodeType);
121+
catItem->setSelectable(true);
122+
123+
_model.appendRow(catItem);
124+
_categories.insert(category, catItem);
125+
126+
return catItem;
127+
}
128+
129+
void ActionsWidget::clearDetails()
130+
{
131+
while (auto child = _detailsLayout.takeAt(0))
132+
{
133+
if (auto actionWidget = child->widget())
134+
actionWidget->deleteLater();
135+
136+
delete child;
137+
}
138+
}

ExampleActions/src/ActionsWidget.h

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#pragma once
2+
3+
#include "ActionsFilterModel.h"
4+
5+
#include <QApplication>
6+
#include <QHash>
7+
#include <QHeaderView>
8+
#include <QLineEdit>
9+
#include <QScrollArea>
10+
#include <QSplitter>
11+
#include <QStandardItemModel>
12+
#include <QTreeView>
13+
#include <QVBoxLayout>
14+
#include <QWidget>
15+
16+
#include <functional>
17+
18+
/* Custom roles for action items */
19+
namespace Roles {
20+
static constexpr int NodeType = Qt::UserRole + 1; // int
21+
static constexpr int ActionId = Qt::UserRole + 2; // QString
22+
}
23+
24+
/* Node types for items in the model */
25+
enum NodeType { CategoryNode = 0, ActionNode = 1 };
26+
27+
/* Factory function type for creating detail widgets */
28+
using Factory = std::function<QWidget*(QWidget* parent)>;
29+
30+
class ActionsWidget final : public QWidget
31+
{
32+
Q_OBJECT
33+
public:
34+
35+
/**
36+
* Construct with optional parent.
37+
* @param parent The parent QWidget.
38+
*/
39+
explicit ActionsWidget(QWidget* parent = nullptr);
40+
41+
/**
42+
* Add an action to the widget.
43+
* @param category The category of the action.
44+
* @param text The display text of the action.
45+
* @param actionId The unique identifier of the action.
46+
* @param factory The factory function to create the detail widget.
47+
* @param icon The optional icon for the action.
48+
* @param toolTip The optional tooltip for the action.
49+
*/
50+
void addAction(const QString& category, const QString& text, const QString& actionId, Factory factory, const QIcon& icon = {}, const QString& toolTip = {});
51+
52+
private slots:
53+
54+
/**
55+
* Slot called when the current selection changes in the tree view.
56+
* @param current The new current index.
57+
* @param previous The previous current index (unused).
58+
*/
59+
void onCurrentChanged(const QModelIndex& current, const QModelIndex& previous);
60+
61+
private:
62+
63+
/*
64+
* Ensure that a category item exists in the model
65+
* @param category The category name
66+
* @return The QStandardItem representing the category
67+
*/
68+
QStandardItem* ensureCategory(const QString& category);
69+
70+
/** Clear the details area */
71+
void clearDetails();
72+
73+
private:
74+
QStandardItemModel _model; /** The underlying model */
75+
ActionsFilterModel _proxy; /** The filter proxy model */
76+
QVBoxLayout _mainLayout; /** The main layout */
77+
QSplitter _splitter; /** The main splitter */
78+
QWidget _leftPanelWidget; /** The left panel widget */
79+
QVBoxLayout _leftPanelLayout; /** The left panel layout */
80+
QWidget _rightPanelWidget; /** The right panel widget */
81+
QVBoxLayout _rightPanelLayout; /** The right panel layout */
82+
QLineEdit _search; /** The search box */
83+
QTreeView _tree; /** The tree view */
84+
QScrollArea _detailsArea; /** The details scroll area */
85+
QWidget _detailsHost; /** The details host widget */
86+
QVBoxLayout _detailsLayout; /** The details layout */
87+
QHash<QString, QStandardItem*> _categories; /** The category items */
88+
QHash<QString, Factory> _factories; /** The action detail widget factories */
89+
};

ExampleActions/src/ExampleActionsPlugin.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "ExampleActionsPlugin.h"
2+
#include "ActionsWidget.h"
23

34
#include "../Common/common.h"
45

@@ -15,10 +16,12 @@ ExampleActionsPlugin::ExampleActionsPlugin(const PluginFactory* factory) :
1516

1617
void ExampleActionsPlugin::init()
1718
{
18-
// Create layout
1919
auto layout = new QVBoxLayout();
2020

2121
layout->setContentsMargins(0, 0, 0, 0);
22+
layout->addWidget(new ActionsWidget());
23+
24+
getWidget().setLayout(layout);
2225
}
2326

2427
ExampleActionsPluginFactory::ExampleActionsPluginFactory()

0 commit comments

Comments
 (0)