ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
ecvShortcutDialog.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 #include "ecvShortcutDialog.h"
9 
10 // Local
11 #include "ecvPersistentSettings.h"
12 
13 // Qt
14 #include <QAction>
15 #include <QHeaderView>
16 #include <QKeySequenceEdit>
17 #include <QLineEdit>
18 #include <QMainWindow>
19 #include <QMenu>
20 #include <QMenuBar>
21 #include <QMessageBox>
22 #include <QSettings>
23 #include <QTableWidget>
24 #include <QTableWidgetItem>
25 
26 constexpr int ACTION_NAME_COLUMN = 0;
27 constexpr int KEY_SEQUENCE_COLUMN = 1;
28 
29 // Helper function to recursively find menu path for an action
30 static QString findMenuPathRecursive(QMenu* menu,
31  QAction* targetAction,
32  QStringList path) {
33  if (!menu) {
34  return QString();
35  }
36 
37  // Add current menu title to path (only if not already in path to avoid
38  // duplicates)
39  QString menuTitle = menu->title();
40  menuTitle.remove('&');
41  if (!menuTitle.isEmpty() && (path.isEmpty() || path.last() != menuTitle)) {
42  path.append(menuTitle);
43  }
44 
45  // Check if this menu contains the target action
46  for (QAction* action : menu->actions()) {
47  if (action == targetAction) {
48  // Found the action in this menu
49  return path.join(" > ");
50  }
51 
52  // Check if this action has a submenu
53  QMenu* submenu = action->menu();
54  if (submenu) {
55  // For submenus, use the submenu's title directly (don't add action
56  // text)
57  QStringList subPath = path;
58  QString submenuTitle = submenu->title();
59  submenuTitle.remove('&');
60  // Only add if different from last item to avoid duplicates
61  if (!submenuTitle.isEmpty() &&
62  (subPath.isEmpty() || subPath.last() != submenuTitle)) {
63  subPath.append(submenuTitle);
64  }
65  QString result =
66  findMenuPathRecursive(submenu, targetAction, subPath);
67  if (!result.isEmpty()) {
68  return result;
69  }
70  }
71  }
72 
73  return QString();
74 }
75 
76 // Helper function to get menu path for an action
77 static QString getMenuPath(QAction* action, QWidget* parentWidget) {
78  if (!action) {
79  return QString();
80  }
81 
82  // First try: traverse up the parent chain (fast path)
83  QStringList path;
84  QObject* parent = action->parent();
85 
86  while (parent) {
87  QMenu* menu = qobject_cast<QMenu*>(parent);
88  if (menu) {
89  QString menuTitle = menu->title();
90  menuTitle.remove('&');
91  // Only add if not empty and not duplicate
92  if (!menuTitle.isEmpty() &&
93  (path.isEmpty() || path.first() != menuTitle)) {
94  path.prepend(menuTitle);
95  }
96  parent = menu->parent();
97  } else {
98  // Check if parent is another action (for submenu actions)
99  QAction* parentAction = qobject_cast<QAction*>(parent);
100  if (parentAction) {
101  // For submenu actions, get the submenu title instead of action
102  // text
103  QMenu* submenu = parentAction->menu();
104  if (submenu) {
105  QString submenuTitle = submenu->title();
106  submenuTitle.remove('&');
107  if (!submenuTitle.isEmpty() &&
108  (path.isEmpty() || path.first() != submenuTitle)) {
109  path.prepend(submenuTitle);
110  }
111  parent = submenu->parent();
112  } else {
113  QString actionTitle = parentAction->text();
114  actionTitle.remove('&');
115  if (!actionTitle.isEmpty() &&
116  (path.isEmpty() || path.first() != actionTitle)) {
117  path.prepend(actionTitle);
118  }
119  parent = parentAction->parent();
120  }
121  } else {
122  break;
123  }
124  }
125  }
126 
127  if (!path.isEmpty()) {
128  return path.join(" > ");
129  }
130 
131  // Second try: search through menu bar if parent widget is available
132  if (parentWidget) {
133  QMenuBar* menuBar = parentWidget->findChild<QMenuBar*>();
134  if (!menuBar) {
135  // Try to get menuBar from QMainWindow
136  QMainWindow* mainWindow = qobject_cast<QMainWindow*>(parentWidget);
137  if (mainWindow) {
138  menuBar = mainWindow->menuBar();
139  }
140  }
141 
142  if (menuBar) {
143  QStringList emptyPath;
144  for (QAction* menuAction : menuBar->actions()) {
145  QMenu* menu = menuAction->menu();
146  if (menu) {
147  QString result =
148  findMenuPathRecursive(menu, action, emptyPath);
149  if (!result.isEmpty()) {
150  return result;
151  }
152  }
153  }
154  }
155  }
156 
157  return QString();
158 }
159 
161  : QDialog(parent), m_ui(new Ui_ShortcutEditDialog) {
162  m_ui->setupUi(this);
163  connect(m_ui->clearButton, &QPushButton::clicked, m_ui->keySequenceEdit,
164  &QKeySequenceEdit::clear);
165 }
166 
168 
170  return m_ui->keySequenceEdit->keySequence();
171 }
172 
173 void ecvShortcutEditDialog::setKeySequence(const QKeySequence& sequence) const {
174  m_ui->keySequenceEdit->setKeySequence(sequence);
175 }
176 
178  m_ui->keySequenceEdit->setFocus();
179  return QDialog::exec();
180 }
181 
182 ecvShortcutDialog::ecvShortcutDialog(const QList<QAction*>& actions,
183  QWidget* parent)
184  : QDialog(parent),
185  m_ui(new Ui_ShortcutDialog),
186  m_editDialog(new ecvShortcutEditDialog(this)),
187  m_allActions(actions) {
188  m_ui->setupUi(this);
189  m_ui->tableWidget->setRowCount(actions.count());
190  m_ui->tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
191 
192  connect(m_ui->tableWidget, &QTableWidget::itemDoubleClicked, this,
193  &ecvShortcutDialog::handleDoubleClick);
194  connect(m_ui->searchLineEdit, &QLineEdit::textChanged, this,
195  &ecvShortcutDialog::filterActions);
196 
197  int row = 0;
198  for (QAction* action : actions) {
199  // Build display text with full information
200  QString displayText = action->text();
201  QString menuPath = getMenuPath(action, parent);
202  QString toolTip = action->toolTip();
203 
204  // Remove accelerator markers from display text
205  QString cleanText = displayText;
206  cleanText.remove('&');
207 
208  // Build full description
209  QString fullDescription = cleanText;
210 
211  // Add menu path if available
212  if (!menuPath.isEmpty()) {
213  fullDescription += " (" + menuPath + ")";
214  }
215 
216  // Add tooltip if available and different from text
217  if (!toolTip.isEmpty() && toolTip != displayText &&
218  toolTip != cleanText) {
219  // Remove accelerator markers from tooltip
220  QString cleanToolTip = toolTip;
221  cleanToolTip.remove('&');
222  if (cleanToolTip != cleanText && !cleanToolTip.isEmpty()) {
223  fullDescription += " - " + cleanToolTip;
224  }
225  }
226 
227  auto* actionWidget =
228  new QTableWidgetItem(action->icon(), fullDescription);
229  actionWidget->setFlags(actionWidget->flags() & ~Qt::ItemIsEditable);
230 
231  // Store original text, menu path, and tooltip for searching
232  actionWidget->setData(Qt::UserRole + 1, cleanText); // Original text
233  actionWidget->setData(Qt::UserRole + 2, menuPath); // Menu path
234  actionWidget->setData(Qt::UserRole + 3, toolTip); // Tooltip
235 
236  m_ui->tableWidget->setItem(row, ACTION_NAME_COLUMN, actionWidget);
237 
238  auto* shortcutWidget =
239  new QTableWidgetItem(action->shortcut().toString());
240  shortcutWidget->setFlags(actionWidget->flags() & ~Qt::ItemIsEditable);
241  shortcutWidget->setData(Qt::UserRole, QVariant::fromValue(action));
242  m_ui->tableWidget->setItem(row, KEY_SEQUENCE_COLUMN, shortcutWidget);
243  row += 1;
244  }
245 
246  // Set column resize modes: first column stretches, second column has fixed
247  // width This ensures the first column (Action) is much wider than the
248  // second (Shortcut)
249 
250  // First, measure content to determine optimal width for shortcut column
251  m_ui->tableWidget->horizontalHeader()->setSectionResizeMode(
252  KEY_SEQUENCE_COLUMN, QHeaderView::ResizeToContents);
253  m_ui->tableWidget->resizeColumnToContents(KEY_SEQUENCE_COLUMN);
254 
255  // Get measured width for shortcut column
256  int shortcutContentWidth =
257  m_ui->tableWidget->columnWidth(KEY_SEQUENCE_COLUMN);
258 
259  // Set a fixed width for the shortcut column (with some margin)
260  const int minShortcutWidth = 200; // Minimum width for shortcut column
261  int shortcutWidth = qMax(shortcutContentWidth + 30, minShortcutWidth);
262 
263  // Set shortcut column to fixed width
264  m_ui->tableWidget->setColumnWidth(KEY_SEQUENCE_COLUMN, shortcutWidth);
265  m_ui->tableWidget->horizontalHeader()->setSectionResizeMode(
266  KEY_SEQUENCE_COLUMN, QHeaderView::Fixed);
267 
268  // Set action column to stretch mode - it will take all remaining space
269  m_ui->tableWidget->horizontalHeader()->setStretchLastSection(false);
270  m_ui->tableWidget->horizontalHeader()->setSectionResizeMode(
271  ACTION_NAME_COLUMN, QHeaderView::Stretch);
272 }
273 
275  delete m_ui;
276  delete m_editDialog;
277 }
278 
280  QSettings settings;
281  settings.beginGroup(ecvPS::Shortcuts());
282 
283  for (int i = 0; i < m_ui->tableWidget->rowCount(); i++) {
284  QTableWidgetItem* item =
285  m_ui->tableWidget->item(i, KEY_SEQUENCE_COLUMN);
286  auto* action = item->data(Qt::UserRole).value<QAction*>();
287 
288  if (settings.contains(action->text())) {
289  const QKeySequence defaultValue;
290  const auto sequence = settings.value(action->text(), defaultValue)
291  .value<QKeySequence>();
292 
293  item->setText(sequence.toString());
294  action->setShortcut(sequence);
295  }
296  }
297  settings.endGroup();
298 }
299 
300 const QAction* ecvShortcutDialog::checkConflict(
301  const QKeySequence& sequence) const {
302  for (int i = 0; i < m_ui->tableWidget->rowCount(); i++) {
303  const QTableWidgetItem* item = m_ui->tableWidget->item(i, 1);
304  const auto* action = item->data(Qt::UserRole).value<QAction*>();
305  if (action->shortcut() == sequence) {
306  return action;
307  }
308  }
309 
310  return nullptr;
311 }
312 
313 void ecvShortcutDialog::handleDoubleClick(QTableWidgetItem* item) {
314  if (!item) {
315  return;
316  }
317 
318  if (item->column() != KEY_SEQUENCE_COLUMN) {
319  item = m_ui->tableWidget->item(item->row(), KEY_SEQUENCE_COLUMN);
320  }
321 
322  auto* action = item->data(Qt::UserRole).value<QAction*>();
323  m_editDialog->setKeySequence(action->shortcut());
324 
325  if (m_editDialog->exec() == QDialog::Rejected) {
326  return;
327  }
328 
329  const QKeySequence keySequence = m_editDialog->keySequence();
330  if (keySequence == action->shortcut()) {
331  // User did not change it
332  return;
333  }
334 
335  if (!keySequence.isEmpty()) {
336  const QAction* conflict = checkConflict(keySequence);
337  if (conflict) {
338  QMessageBox::critical(
339  this, tr("Shortcut conflict"),
340  QString(tr("The shortcut entered would conflict with the "
341  "one for `%1`"))
342  .arg(conflict->text()));
343  return;
344  }
345  }
346 
347  item->setText(keySequence.toString());
348  action->setShortcut(keySequence);
349 
350  QSettings settings;
351  settings.beginGroup(ecvPS::Shortcuts());
352  settings.setValue(action->text(), keySequence);
353 }
354 
355 void ecvShortcutDialog::filterActions(const QString& searchText) {
356  if (searchText.isEmpty()) {
357  showAllRows();
358  return;
359  }
360 
361  QString searchLower = searchText.toLower();
362  for (int row = 0; row < m_ui->tableWidget->rowCount(); ++row) {
363  QTableWidgetItem* item =
364  m_ui->tableWidget->item(row, ACTION_NAME_COLUMN);
365  if (item) {
366  // Search in display text
367  bool matches = item->text().toLower().contains(searchLower);
368 
369  // Also search in stored data (original text, menu path, tooltip)
370  if (!matches) {
371  QString originalText =
372  item->data(Qt::UserRole + 1).toString().toLower();
373  QString menuPath =
374  item->data(Qt::UserRole + 2).toString().toLower();
375  QString toolTip =
376  item->data(Qt::UserRole + 3).toString().toLower();
377 
378  matches = originalText.contains(searchLower) ||
379  menuPath.contains(searchLower) ||
380  toolTip.contains(searchLower);
381  }
382 
383  m_ui->tableWidget->setRowHidden(row, !matches);
384  }
385  }
386 }
387 
388 void ecvShortcutDialog::showAllRows() {
389  for (int row = 0; row < m_ui->tableWidget->rowCount(); ++row) {
390  m_ui->tableWidget->setRowHidden(row, false);
391  }
392 }
core::Tensor result
Definition: VtkUtils.cpp:76
static const QString Shortcuts()
void restoreShortcutsFromQSettings() const
ecvShortcutDialog(const QList< QAction * > &actions, QWidget *parent=nullptr)
void setKeySequence(const QKeySequence &sequence) const
ecvShortcutEditDialog(QWidget *parent=nullptr)
QKeySequence keySequence() const
constexpr int ACTION_NAME_COLUMN
static QString getMenuPath(QAction *action, QWidget *parentWidget)
static QString findMenuPathRecursive(QMenu *menu, QAction *targetAction, QStringList path)
constexpr int KEY_SEQUENCE_COLUMN
static const std::string path
Definition: PointCloud.cpp:59