mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-08-22 08:34:12 +02:00
Make the shortcut list browsable and searchable
Replace the flat QTableWidget with a QTreeWidget that groups actions under one collapsible top-level node per category. Fix the search box so it also matches the current key sequence (exactly), accepts multi-keyword queries (AND, any word order) and is accent-insensitive, auto-expands matching groups and shows an "N actions" count. Add a quick filter (all / bound / unbound / conflicts) that combines with the text query. Conflict detection, per-row reset, reset-all and persistence are preserved. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
#include "../../qeticons.h"
|
#include "../../qeticons.h"
|
||||||
#include "../../shortcutmanager.h"
|
#include "../../shortcutmanager.h"
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
@@ -27,11 +28,33 @@
|
|||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QTableWidget>
|
#include <QRegularExpression>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
|
#include <QTreeWidget>
|
||||||
|
#include <QTreeWidgetItem>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief normalizedForSearch
|
||||||
|
Decompose \a text and strip every non-spacing combining mark, then fold the
|
||||||
|
case. Both the query terms and the row's searchable text are run through
|
||||||
|
this, so "general" matches "Général" and "Ctrl+S" matches "ctrl+s"
|
||||||
|
regardless of the keyboard layout the query was typed on.
|
||||||
|
*/
|
||||||
|
static QString normalizedForSearch(const QString &text)
|
||||||
|
{
|
||||||
|
const QString decomposed = text.normalized(QString::NormalizationForm_D);
|
||||||
|
QString stripped;
|
||||||
|
stripped.reserve(decomposed.size());
|
||||||
|
for (const QChar &c : decomposed) {
|
||||||
|
if (c.category() != QChar::Mark_NonSpacing) {
|
||||||
|
stripped.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stripped.toCaseFolded();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief ShortcutsConfigPage::ShortcutsConfigPage
|
@brief ShortcutsConfigPage::ShortcutsConfigPage
|
||||||
@param parent
|
@param parent
|
||||||
@@ -51,18 +74,33 @@ ShortcutsConfigPage::ShortcutsConfigPage(QWidget *parent) :
|
|||||||
m_filter_edit = new QLineEdit(this);
|
m_filter_edit = new QLineEdit(this);
|
||||||
m_filter_edit->setPlaceholderText(tr("Filtrer les raccourcis…"));
|
m_filter_edit->setPlaceholderText(tr("Filtrer les raccourcis…"));
|
||||||
connect(m_filter_edit, &QLineEdit::textChanged, this, &ShortcutsConfigPage::filterRows);
|
connect(m_filter_edit, &QLineEdit::textChanged, this, &ShortcutsConfigPage::filterRows);
|
||||||
vlayout->addWidget(m_filter_edit);
|
|
||||||
|
|
||||||
m_table = new QTableWidget(0, 4, this);
|
m_quick_filter = new QComboBox(this);
|
||||||
m_table->setHorizontalHeaderLabels({tr("Catégorie"), tr("Action"), tr("Raccourci"), QString()});
|
m_quick_filter->setObjectName(QStringLiteral("quickFilterCombo"));
|
||||||
m_table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
m_quick_filter->addItem(tr("Tous"));
|
||||||
m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
|
m_quick_filter->addItem(tr("Attribués uniquement"));
|
||||||
m_table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
m_quick_filter->addItem(tr("Non attribués uniquement"));
|
||||||
m_table->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
|
m_quick_filter->addItem(tr("Conflits uniquement"));
|
||||||
m_table->verticalHeader()->setVisible(false);
|
connect(m_quick_filter, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||||
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
this, &ShortcutsConfigPage::quickFilterChanged);
|
||||||
m_table->setSelectionMode(QAbstractItemView::NoSelection);
|
|
||||||
vlayout->addWidget(m_table);
|
m_count_label = new QLabel(this);
|
||||||
|
m_count_label->setObjectName(QStringLiteral("shortcutCountLabel"));
|
||||||
|
|
||||||
|
auto *filter_layout = new QHBoxLayout();
|
||||||
|
filter_layout->addWidget(m_filter_edit, 1);
|
||||||
|
filter_layout->addWidget(m_quick_filter);
|
||||||
|
filter_layout->addWidget(m_count_label);
|
||||||
|
vlayout->addLayout(filter_layout);
|
||||||
|
|
||||||
|
m_tree = new QTreeWidget(this);
|
||||||
|
m_tree->setHeaderLabels({tr("Action"), tr("Raccourci"), QString()});
|
||||||
|
m_tree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||||
|
m_tree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||||
|
m_tree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||||
|
m_tree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||||
|
m_tree->setSelectionMode(QAbstractItemView::NoSelection);
|
||||||
|
vlayout->addWidget(m_tree);
|
||||||
|
|
||||||
auto *reset_all_button = new QPushButton(tr("Tout réinitialiser"), this);
|
auto *reset_all_button = new QPushButton(tr("Tout réinitialiser"), this);
|
||||||
connect(reset_all_button, &QPushButton::clicked, this, &ShortcutsConfigPage::resetAllRows);
|
connect(reset_all_button, &QPushButton::clicked, this, &ShortcutsConfigPage::resetAllRows);
|
||||||
@@ -75,6 +113,7 @@ ShortcutsConfigPage::ShortcutsConfigPage(QWidget *parent) :
|
|||||||
setLayout(vlayout);
|
setLayout(vlayout);
|
||||||
|
|
||||||
populateTable();
|
populateTable();
|
||||||
|
applyFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
ShortcutsConfigPage::~ShortcutsConfigPage()
|
ShortcutsConfigPage::~ShortcutsConfigPage()
|
||||||
@@ -83,8 +122,8 @@ ShortcutsConfigPage::~ShortcutsConfigPage()
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
@brief ShortcutsConfigPage::populateTable
|
@brief ShortcutsConfigPage::populateTable
|
||||||
Fill the table with one row per shortcut known to ShortcutManager, sorted
|
Fill the tree with one collapsible top-level node per category and one child
|
||||||
by category then action name.
|
per shortcut known to ShortcutManager, sorted by category then action name.
|
||||||
*/
|
*/
|
||||||
void ShortcutsConfigPage::populateTable()
|
void ShortcutsConfigPage::populateTable()
|
||||||
{
|
{
|
||||||
@@ -97,33 +136,38 @@ void ShortcutsConfigPage::populateTable()
|
|||||||
return a.description < b.description;
|
return a.description < b.description;
|
||||||
});
|
});
|
||||||
|
|
||||||
m_table->setRowCount(shortcuts.size());
|
m_tree->clear();
|
||||||
m_rows.clear();
|
m_rows.clear();
|
||||||
m_rows.reserve(shortcuts.size());
|
m_rows.reserve(shortcuts.size());
|
||||||
|
|
||||||
for (int row = 0; row < shortcuts.size(); ++row) {
|
QHash<QString, QTreeWidgetItem *> category_nodes;
|
||||||
const ShortcutManager::ShortcutInfo &info = shortcuts.at(row);
|
|
||||||
|
|
||||||
auto *category_item = new QTableWidgetItem(info.category);
|
for (const ShortcutManager::ShortcutInfo &info : shortcuts) {
|
||||||
|
QTreeWidgetItem *category_item = category_nodes.value(info.category, nullptr);
|
||||||
|
if (!category_item) {
|
||||||
|
category_item = new QTreeWidgetItem(m_tree);
|
||||||
|
category_item->setText(0, info.category);
|
||||||
category_item->setFlags(category_item->flags() & ~Qt::ItemIsEditable);
|
category_item->setFlags(category_item->flags() & ~Qt::ItemIsEditable);
|
||||||
m_table->setItem(row, 0, category_item);
|
category_nodes.insert(info.category, category_item);
|
||||||
|
}
|
||||||
|
|
||||||
auto *description_item = new QTableWidgetItem(info.description);
|
auto *child = new QTreeWidgetItem(category_item);
|
||||||
description_item->setFlags(description_item->flags() & ~Qt::ItemIsEditable);
|
child->setText(0, info.description);
|
||||||
m_table->setItem(row, 1, description_item);
|
child->setFlags(child->flags() & ~Qt::ItemIsEditable);
|
||||||
|
|
||||||
auto *edit = new QKeySequenceEdit(info.current_sequence, m_table);
|
auto *edit = new QKeySequenceEdit(info.current_sequence, m_tree);
|
||||||
connect(edit, &QKeySequenceEdit::editingFinished, this, &ShortcutsConfigPage::checkConflicts);
|
connect(edit, &QKeySequenceEdit::editingFinished, this, &ShortcutsConfigPage::checkConflicts);
|
||||||
m_table->setCellWidget(row, 2, edit);
|
m_tree->setItemWidget(child, 1, edit);
|
||||||
|
|
||||||
auto *reset_button = new QToolButton(m_table);
|
auto *reset_button = new QToolButton(m_tree);
|
||||||
reset_button->setIcon(QET::Icons::EditUndo);
|
reset_button->setIcon(QET::Icons::EditUndo);
|
||||||
reset_button->setToolTip(tr("Réinitialiser ce raccourci"));
|
reset_button->setToolTip(tr("Réinitialiser ce raccourci"));
|
||||||
reset_button->setAutoRaise(true);
|
reset_button->setAutoRaise(true);
|
||||||
connect(reset_button, &QToolButton::clicked, this, [this, row]() { resetRow(row); });
|
const int row_index = m_rows.size();
|
||||||
m_table->setCellWidget(row, 3, reset_button);
|
connect(reset_button, &QToolButton::clicked, this, [this, row_index]() { resetRow(row_index); });
|
||||||
|
m_tree->setItemWidget(child, 2, reset_button);
|
||||||
|
|
||||||
m_rows << Row{info.id, info.default_sequence, edit};
|
m_rows << Row{info.id, info.category, info.description, info.default_sequence, edit, child, false};
|
||||||
}
|
}
|
||||||
|
|
||||||
checkConflicts();
|
checkConflicts();
|
||||||
@@ -131,17 +175,97 @@ void ShortcutsConfigPage::populateTable()
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
@brief ShortcutsConfigPage::filterRows
|
@brief ShortcutsConfigPage::filterRows
|
||||||
Hide every row whose category or action name doesn't contain \a filter_text.
|
Re-run the combined text + quick filter whenever the search box changes.
|
||||||
*/
|
*/
|
||||||
void ShortcutsConfigPage::filterRows(const QString &filter_text)
|
void ShortcutsConfigPage::filterRows(const QString &filter_text)
|
||||||
{
|
{
|
||||||
const QString needle = filter_text.trimmed();
|
Q_UNUSED(filter_text)
|
||||||
for (int row = 0; row < m_table->rowCount(); ++row) {
|
applyFilter();
|
||||||
const bool matches = needle.isEmpty()
|
|
||||||
|| m_table->item(row, 0)->text().contains(needle, Qt::CaseInsensitive)
|
|
||||||
|| m_table->item(row, 1)->text().contains(needle, Qt::CaseInsensitive);
|
|
||||||
m_table->setRowHidden(row, !matches);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief ShortcutsConfigPage::quickFilterChanged
|
||||||
|
Re-run the combined text + quick filter whenever the quick filter changes.
|
||||||
|
*/
|
||||||
|
void ShortcutsConfigPage::quickFilterChanged(int index)
|
||||||
|
{
|
||||||
|
Q_UNUSED(index)
|
||||||
|
applyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief ShortcutsConfigPage::applyFilter
|
||||||
|
Hide every row that doesn't match both the search box and the quick filter.
|
||||||
|
The text query is split on whitespace and every term must match the category,
|
||||||
|
action name or current sequence (accent- and case-insensitively). Matching
|
||||||
|
category nodes are expanded so hits are not hidden inside collapsed groups,
|
||||||
|
and the "N actions" label tracks how many actions remain visible.
|
||||||
|
*/
|
||||||
|
void ShortcutsConfigPage::applyFilter()
|
||||||
|
{
|
||||||
|
const QString needle = m_filter_edit->text().trimmed();
|
||||||
|
const QStringList terms = needle.isEmpty()
|
||||||
|
? QStringList()
|
||||||
|
: needle.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
|
||||||
|
|
||||||
|
const int quick_filter = m_quick_filter->currentIndex();
|
||||||
|
|
||||||
|
int visible_actions = 0;
|
||||||
|
for (const Row &row : qAsConst(m_rows)) {
|
||||||
|
// Text is matched by substring, but the key sequence is matched exactly:
|
||||||
|
// a substring match would let "Ctrl+S" also hit "Ctrl+Shift+F" (the "S"
|
||||||
|
// of "Shift"), which is precisely the kind of false positive that hides
|
||||||
|
// the one binding the user is looking for.
|
||||||
|
const QString text_haystack = normalizedForSearch(
|
||||||
|
row.category + QLatin1Char(' ') + row.description);
|
||||||
|
const QString sequence_text = normalizedForSearch(row.edit->keySequence().toString());
|
||||||
|
|
||||||
|
bool matches = true;
|
||||||
|
for (const QString &term : terms) {
|
||||||
|
const QString t = normalizedForSearch(term);
|
||||||
|
if (!text_haystack.contains(t) && sequence_text != t) {
|
||||||
|
matches = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
switch (quick_filter) {
|
||||||
|
case BoundOnly:
|
||||||
|
matches = !row.edit->keySequence().isEmpty();
|
||||||
|
break;
|
||||||
|
case UnboundOnly:
|
||||||
|
matches = row.edit->keySequence().isEmpty();
|
||||||
|
break;
|
||||||
|
case ConflictsOnly:
|
||||||
|
matches = row.conflicted;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
row.item->setHidden(!matches);
|
||||||
|
if (matches) {
|
||||||
|
++visible_actions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool filtering = !needle.isEmpty() || quick_filter != ShowAll;
|
||||||
|
for (int i = 0; i < m_tree->topLevelItemCount(); ++i) {
|
||||||
|
QTreeWidgetItem *top = m_tree->topLevelItem(i);
|
||||||
|
bool any_visible = false;
|
||||||
|
for (int j = 0; j < top->childCount(); ++j) {
|
||||||
|
if (!top->child(j)->isHidden()) {
|
||||||
|
any_visible = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
top->setHidden(!any_visible);
|
||||||
|
top->setExpanded(filtering && any_visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_count_label->setText(tr("%n action(s)", nullptr, visible_actions));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,27 +284,37 @@ void ShortcutsConfigPage::checkConflicts()
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (int row = 0; row < m_rows.size(); ++row) {
|
for (int row = 0; row < m_rows.size(); ++row) {
|
||||||
const Row ¤t_row = m_rows.at(row);
|
Row ¤t_row = m_rows[row];
|
||||||
const QString sequence_text = current_row.edit->keySequence().toString();
|
const QString sequence_text = current_row.edit->keySequence().toString();
|
||||||
const QList<int> &conflicting_rows = sequence_to_rows.value(sequence_text);
|
const QList<int> &conflicting_rows = sequence_to_rows.value(sequence_text);
|
||||||
const bool conflicted = !sequence_text.isEmpty() && conflicting_rows.size() > 1;
|
const bool conflicted = !sequence_text.isEmpty() && conflicting_rows.size() > 1;
|
||||||
|
|
||||||
QTableWidgetItem *description_item = m_table->item(row, 1);
|
current_row.conflicted = conflicted;
|
||||||
|
|
||||||
if (conflicted) {
|
if (conflicted) {
|
||||||
QStringList other_descriptions;
|
QStringList other_descriptions;
|
||||||
for (int other_row : conflicting_rows) {
|
for (int other_row : conflicting_rows) {
|
||||||
if (other_row != row) {
|
if (other_row != row) {
|
||||||
other_descriptions << m_table->item(other_row, 1)->text();
|
other_descriptions << m_rows.at(other_row).description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
description_item->setBackground(QColor(255, 205, 205));
|
current_row.item->setBackground(0, QColor(255, 205, 205));
|
||||||
current_row.edit->setToolTip(
|
current_row.edit->setToolTip(
|
||||||
tr("Ce raccourci est aussi utilisé par : %1").arg(other_descriptions.join(QStringLiteral(", "))));
|
tr("Ce raccourci est aussi utilisé par : %1").arg(other_descriptions.join(QStringLiteral(", "))));
|
||||||
} else {
|
} else {
|
||||||
description_item->setBackground(Qt::NoBrush);
|
current_row.item->setBackground(0, QBrush());
|
||||||
current_row.edit->setToolTip(QString());
|
current_row.edit->setToolTip(QString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A sequence edit can change while a filter is active (search-by-key or the
|
||||||
|
// conflicts-only quick filter); refresh the visible set so the list doesn't
|
||||||
|
// show stale results.
|
||||||
|
const bool filtering = !m_filter_edit->text().trimmed().isEmpty()
|
||||||
|
|| m_quick_filter->currentIndex() != ShowAll;
|
||||||
|
if (filtering) {
|
||||||
|
applyFilter();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,9 +22,12 @@
|
|||||||
|
|
||||||
#include <QKeySequence>
|
#include <QKeySequence>
|
||||||
|
|
||||||
|
class QComboBox;
|
||||||
class QKeySequenceEdit;
|
class QKeySequenceEdit;
|
||||||
|
class QLabel;
|
||||||
class QLineEdit;
|
class QLineEdit;
|
||||||
class QTableWidget;
|
class QTreeWidget;
|
||||||
|
class QTreeWidgetItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief The ShortcutsConfigPage class
|
@brief The ShortcutsConfigPage class
|
||||||
@@ -47,21 +50,36 @@ class ShortcutsConfigPage : public ConfigPage
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void filterRows(const QString &filter_text);
|
void filterRows(const QString &filter_text);
|
||||||
|
void quickFilterChanged(int index);
|
||||||
void checkConflicts();
|
void checkConflicts();
|
||||||
void resetAllRows();
|
void resetAllRows();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
enum QuickFilter {
|
||||||
|
ShowAll = 0,
|
||||||
|
BoundOnly,
|
||||||
|
UnboundOnly,
|
||||||
|
ConflictsOnly
|
||||||
|
};
|
||||||
|
|
||||||
struct Row {
|
struct Row {
|
||||||
QString id;
|
QString id;
|
||||||
|
QString category;
|
||||||
|
QString description;
|
||||||
QKeySequence default_sequence;
|
QKeySequence default_sequence;
|
||||||
QKeySequenceEdit *edit;
|
QKeySequenceEdit *edit;
|
||||||
|
QTreeWidgetItem *item;
|
||||||
|
bool conflicted;
|
||||||
};
|
};
|
||||||
|
|
||||||
void populateTable();
|
void populateTable();
|
||||||
|
void applyFilter();
|
||||||
void resetRow(int row_index);
|
void resetRow(int row_index);
|
||||||
|
|
||||||
QLineEdit *m_filter_edit;
|
QLineEdit *m_filter_edit;
|
||||||
QTableWidget *m_table;
|
QComboBox *m_quick_filter;
|
||||||
|
QLabel *m_count_label;
|
||||||
|
QTreeWidget *m_tree;
|
||||||
QList<Row> m_rows;
|
QList<Row> m_rows;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user