1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the Qt Designer of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include "actioneditor_p.h" |
30 | #include "actionrepository_p.h" |
31 | #include "iconloader_p.h" |
32 | #include "newactiondialog_p.h" |
33 | #include "qdesigner_menu_p.h" |
34 | #include "qdesigner_command_p.h" |
35 | #include "qdesigner_propertycommand_p.h" |
36 | #include "qdesigner_objectinspector_p.h" |
37 | #include "qdesigner_utils_p.h" |
38 | #include "qsimpleresource_p.h" |
39 | #include "formwindowbase_p.h" |
40 | #include "qdesigner_taskmenu_p.h" |
41 | |
42 | #include <QtDesigner/abstractformeditor.h> |
43 | #include <QtDesigner/abstractpropertyeditor.h> |
44 | #include <QtDesigner/propertysheet.h> |
45 | #include <QtDesigner/qextensionmanager.h> |
46 | #include <QtDesigner/abstractmetadatabase.h> |
47 | #include <QtDesigner/abstractsettings.h> |
48 | |
49 | #include <QtWidgets/qmenu.h> |
50 | #include <QtWidgets/qtoolbar.h> |
51 | #include <QtWidgets/qsplitter.h> |
52 | #include <QtWidgets/qaction.h> |
53 | #include <QtWidgets/qapplication.h> |
54 | #if QT_CONFIG(clipboard) |
55 | #include <QtGui/qclipboard.h> |
56 | #endif |
57 | #include <QtWidgets/qitemdelegate.h> |
58 | #include <QtGui/qpainter.h> |
59 | #include <QtWidgets/qboxlayout.h> |
60 | #include <QtWidgets/qlineedit.h> |
61 | #include <QtWidgets/qlabel.h> |
62 | #include <QtWidgets/qpushbutton.h> |
63 | #include <QtWidgets/qtoolbutton.h> |
64 | #include <QtGui/qevent.h> |
65 | #include <QtCore/qitemselectionmodel.h> |
66 | |
67 | #include <QtCore/qregularexpression.h> |
68 | #include <QtCore/qdebug.h> |
69 | #include <QtCore/qbuffer.h> |
70 | |
71 | Q_DECLARE_METATYPE(QAction*) |
72 | |
73 | QT_BEGIN_NAMESPACE |
74 | |
75 | static const char *actionEditorViewModeKey = "ActionEditorViewMode" ; |
76 | |
77 | static const char *iconPropertyC = "icon" ; |
78 | static const char *shortcutPropertyC = "shortcut" ; |
79 | static const char *toolTipPropertyC = "toolTip" ; |
80 | static const char *checkablePropertyC = "checkable" ; |
81 | static const char *objectNamePropertyC = "objectName" ; |
82 | static const char *textPropertyC = "text" ; |
83 | |
84 | namespace qdesigner_internal { |
85 | //-------- ActionGroupDelegate |
86 | class ActionGroupDelegate: public QItemDelegate |
87 | { |
88 | public: |
89 | ActionGroupDelegate(QObject *parent) |
90 | : QItemDelegate(parent) {} |
91 | |
92 | void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override |
93 | { |
94 | if (option.state & QStyle::State_Selected) |
95 | painter->fillRect(option.rect, option.palette.highlight()); |
96 | |
97 | QItemDelegate::paint(painter, option, index); |
98 | } |
99 | |
100 | void drawFocus(QPainter *, const QStyleOptionViewItem &, const QRect &) const override {} |
101 | }; |
102 | |
103 | //-------- ActionEditor |
104 | ObjectNamingMode ActionEditor::m_objectNamingMode = Underscore; // fixme Qt 6: CamelCase |
105 | |
106 | ActionEditor::ActionEditor(QDesignerFormEditorInterface *core, QWidget *parent, Qt::WindowFlags flags) : |
107 | QDesignerActionEditorInterface(parent, flags), |
108 | m_core(core), |
109 | m_actionGroups(nullptr), |
110 | m_actionView(new ActionView), |
111 | m_actionNew(new QAction(tr(s: "New..." ), this)), |
112 | m_actionEdit(new QAction(tr(s: "Edit..." ), this)), |
113 | m_actionNavigateToSlot(new QAction(tr(s: "Go to slot..." ), this)), |
114 | #if QT_CONFIG(clipboard) |
115 | m_actionCopy(new QAction(tr(s: "Copy" ), this)), |
116 | m_actionCut(new QAction(tr(s: "Cut" ), this)), |
117 | m_actionPaste(new QAction(tr(s: "Paste" ), this)), |
118 | #endif |
119 | m_actionSelectAll(new QAction(tr(s: "Select all" ), this)), |
120 | m_actionDelete(new QAction(tr(s: "Delete" ), this)), |
121 | m_viewModeGroup(new QActionGroup(this)), |
122 | m_iconViewAction(nullptr), |
123 | m_listViewAction(nullptr), |
124 | m_filterWidget(nullptr) |
125 | { |
126 | m_actionView->initialize(core: m_core); |
127 | m_actionView->setSelectionMode(QAbstractItemView::ExtendedSelection); |
128 | setWindowTitle(tr(s: "Actions" )); |
129 | |
130 | QVBoxLayout *l = new QVBoxLayout(this); |
131 | l->setContentsMargins(QMargins()); |
132 | l->setSpacing(0); |
133 | |
134 | QToolBar *toolbar = new QToolBar; |
135 | toolbar->setIconSize(QSize(22, 22)); |
136 | toolbar->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Minimum); |
137 | l->addWidget(toolbar); |
138 | // edit actions |
139 | QIcon documentNewIcon = QIcon::fromTheme(QStringLiteral("document-new" ), fallback: createIconSet(QStringLiteral("filenew.png" ))); |
140 | m_actionNew->setIcon(documentNewIcon); |
141 | m_actionNew->setEnabled(false); |
142 | connect(sender: m_actionNew, signal: &QAction::triggered, receiver: this, slot: &ActionEditor::slotNewAction); |
143 | toolbar->addAction(action: m_actionNew); |
144 | |
145 | connect(sender: m_actionSelectAll, signal: &QAction::triggered, receiver: m_actionView, slot: &ActionView::selectAll); |
146 | |
147 | #if QT_CONFIG(clipboard) |
148 | m_actionCut->setEnabled(false); |
149 | connect(sender: m_actionCut, signal: &QAction::triggered, receiver: this, slot: &ActionEditor::slotCut); |
150 | QIcon editCutIcon = QIcon::fromTheme(QStringLiteral("edit-cut" ), fallback: createIconSet(QStringLiteral("editcut.png" ))); |
151 | m_actionCut->setIcon(editCutIcon); |
152 | |
153 | m_actionCopy->setEnabled(false); |
154 | connect(sender: m_actionCopy, signal: &QAction::triggered, receiver: this, slot: &ActionEditor::slotCopy); |
155 | QIcon editCopyIcon = QIcon::fromTheme(QStringLiteral("edit-copy" ), fallback: createIconSet(QStringLiteral("editcopy.png" ))); |
156 | m_actionCopy->setIcon(editCopyIcon); |
157 | toolbar->addAction(action: m_actionCopy); |
158 | |
159 | connect(sender: m_actionPaste, signal: &QAction::triggered, receiver: this, slot: &ActionEditor::slotPaste); |
160 | QIcon editPasteIcon = QIcon::fromTheme(QStringLiteral("edit-paste" ), fallback: createIconSet(QStringLiteral("editpaste.png" ))); |
161 | m_actionPaste->setIcon(editPasteIcon); |
162 | toolbar->addAction(action: m_actionPaste); |
163 | #endif |
164 | |
165 | m_actionEdit->setEnabled(false); |
166 | connect(sender: m_actionEdit, signal: &QAction::triggered, receiver: this, slot: &ActionEditor::editCurrentAction); |
167 | |
168 | connect(sender: m_actionNavigateToSlot, signal: &QAction::triggered, receiver: this, slot: &ActionEditor::navigateToSlotCurrentAction); |
169 | |
170 | QIcon editDeleteIcon = QIcon::fromTheme(QStringLiteral("edit-delete" ), fallback: createIconSet(QStringLiteral("editdelete.png" ))); |
171 | m_actionDelete->setIcon(editDeleteIcon); |
172 | m_actionDelete->setEnabled(false); |
173 | connect(sender: m_actionDelete, signal: &QAction::triggered, receiver: this, slot: &ActionEditor::slotDelete); |
174 | toolbar->addAction(action: m_actionDelete); |
175 | |
176 | // Toolbutton with menu containing action group for detailed/icon view. Steal the icons from the file dialog. |
177 | // |
178 | QMenu *; |
179 | toolbar->addWidget(widget: createConfigureMenuButton(t: tr(s: "Configure Action Editor" ), ptrToMenu: &configureMenu)); |
180 | |
181 | connect(sender: m_viewModeGroup, signal: &QActionGroup::triggered, receiver: this, slot: &ActionEditor::slotViewMode); |
182 | m_iconViewAction = m_viewModeGroup->addAction(text: tr(s: "Icon View" )); |
183 | m_iconViewAction->setData(QVariant(ActionView::IconView)); |
184 | m_iconViewAction->setCheckable(true); |
185 | m_iconViewAction->setIcon(style()->standardIcon (standardIcon: QStyle::SP_FileDialogListView)); |
186 | configureMenu->addAction(action: m_iconViewAction); |
187 | |
188 | m_listViewAction = m_viewModeGroup->addAction(text: tr(s: "Detailed View" )); |
189 | m_listViewAction->setData(QVariant(ActionView::DetailedView)); |
190 | m_listViewAction->setCheckable(true); |
191 | m_listViewAction->setIcon(style()->standardIcon (standardIcon: QStyle::SP_FileDialogDetailedView)); |
192 | configureMenu->addAction(action: m_listViewAction); |
193 | // filter |
194 | m_filterWidget = new QWidget(toolbar); |
195 | QHBoxLayout *filterLayout = new QHBoxLayout(m_filterWidget); |
196 | filterLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0); |
197 | QLineEdit *filterLineEdit = new QLineEdit(m_filterWidget); |
198 | connect(sender: filterLineEdit, signal: &QLineEdit::textChanged, receiver: this, slot: &ActionEditor::setFilter); |
199 | filterLineEdit->setPlaceholderText(tr(s: "Filter" )); |
200 | filterLineEdit->setClearButtonEnabled(true); |
201 | filterLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Ignored)); |
202 | filterLayout->addWidget(filterLineEdit); |
203 | m_filterWidget->setEnabled(false); |
204 | toolbar->addWidget(widget: m_filterWidget); |
205 | |
206 | // main layout |
207 | QSplitter *splitter = new QSplitter(Qt::Horizontal); |
208 | splitter->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Expanding); |
209 | |
210 | splitter->addWidget(widget: m_actionView); |
211 | l->addWidget(splitter); |
212 | |
213 | #if 0 // ### implement me |
214 | m_actionGroups = new QListWidget(splitter); |
215 | splitter->addWidget(m_actionGroups); |
216 | m_actionGroups->setItemDelegate(new ActionGroupDelegate(m_actionGroups)); |
217 | m_actionGroups->setMovement(QListWidget::Static); |
218 | m_actionGroups->setResizeMode(QListWidget::Fixed); |
219 | m_actionGroups->setIconSize(QSize(48, 48)); |
220 | m_actionGroups->setFlow(QListWidget::TopToBottom); |
221 | m_actionGroups->setViewMode(QListWidget::IconMode); |
222 | m_actionGroups->setWrapping(false); |
223 | #endif |
224 | |
225 | connect(sender: m_actionView, signal: &ActionView::resourceImageDropped, |
226 | receiver: this, slot: &ActionEditor::resourceImageDropped); |
227 | |
228 | connect(sender: m_actionView, signal: &ActionView::currentChanged,receiver: this, slot: &ActionEditor::slotCurrentItemChanged); |
229 | // make it possible for vs integration to reimplement edit action dialog |
230 | connect(sender: m_actionView, signal: &ActionView::activated, receiver: this, slot: &ActionEditor::itemActivated); |
231 | |
232 | connect(sender: m_actionView, signal: &ActionView::selectionChanged, |
233 | receiver: this, slot: &ActionEditor::slotSelectionChanged); |
234 | |
235 | connect(sender: m_actionView, signal: &ActionView::contextMenuRequested, |
236 | receiver: this, slot: &ActionEditor::slotContextMenuRequested); |
237 | |
238 | connect(sender: this, signal: &ActionEditor::itemActivated, receiver: this, slot: &ActionEditor::editAction); |
239 | |
240 | restoreSettings(); |
241 | updateViewModeActions(); |
242 | } |
243 | |
244 | // Utility to create a configure button with menu for usage on toolbars |
245 | QToolButton *ActionEditor::(const QString &t, QMenu **) |
246 | { |
247 | QToolButton *configureButton = new QToolButton; |
248 | QAction *configureAction = new QAction(t, configureButton); |
249 | QIcon configureIcon = QIcon::fromTheme(QStringLiteral("document-properties" ), fallback: createIconSet(QStringLiteral("configure.png" ))); |
250 | configureAction->setIcon(configureIcon); |
251 | QMenu * = new QMenu; |
252 | configureAction->setMenu(configureMenu); |
253 | configureButton->setDefaultAction(configureAction); |
254 | configureButton->setPopupMode(QToolButton::InstantPopup); |
255 | *ptrToMenu = configureMenu; |
256 | return configureButton; |
257 | } |
258 | |
259 | ActionEditor::~ActionEditor() |
260 | { |
261 | saveSettings(); |
262 | } |
263 | |
264 | QAction *ActionEditor::actionNew() const |
265 | { |
266 | return m_actionNew; |
267 | } |
268 | |
269 | QAction *ActionEditor::actionDelete() const |
270 | { |
271 | return m_actionDelete; |
272 | } |
273 | |
274 | QDesignerFormWindowInterface *ActionEditor::formWindow() const |
275 | { |
276 | return m_formWindow; |
277 | } |
278 | |
279 | void ActionEditor::setFormWindow(QDesignerFormWindowInterface *formWindow) |
280 | { |
281 | if (formWindow != nullptr && formWindow->mainContainer() == nullptr) |
282 | formWindow = nullptr; |
283 | |
284 | // we do NOT rely on this function to update the action editor |
285 | if (m_formWindow == formWindow) |
286 | return; |
287 | |
288 | if (m_formWindow != nullptr) { |
289 | const ActionList actionList = m_formWindow->mainContainer()->findChildren<QAction*>(); |
290 | for (QAction *action : actionList) |
291 | disconnect(sender: action, signal: &QAction::changed, receiver: this, slot: &ActionEditor::slotActionChanged); |
292 | } |
293 | |
294 | m_formWindow = formWindow; |
295 | |
296 | m_actionView->model()->clearActions(); |
297 | |
298 | m_actionEdit->setEnabled(false); |
299 | #if QT_CONFIG(clipboard) |
300 | m_actionCopy->setEnabled(false); |
301 | m_actionCut->setEnabled(false); |
302 | #endif |
303 | m_actionDelete->setEnabled(false); |
304 | |
305 | if (!formWindow || !formWindow->mainContainer()) { |
306 | m_actionNew->setEnabled(false); |
307 | m_filterWidget->setEnabled(false); |
308 | return; |
309 | } |
310 | |
311 | m_actionNew->setEnabled(true); |
312 | m_filterWidget->setEnabled(true); |
313 | |
314 | const ActionList actionList = formWindow->mainContainer()->findChildren<QAction*>(); |
315 | for (QAction *action : actionList) |
316 | if (!action->isSeparator() && core()->metaDataBase()->item(object: action) != nullptr) { |
317 | // Show unless it has a menu. However, listen for change on menu actions also as it might be removed |
318 | if (!action->menu()) |
319 | m_actionView->model()->addAction(a: action); |
320 | connect(sender: action, signal: &QAction::changed, receiver: this, slot: &ActionEditor::slotActionChanged); |
321 | } |
322 | |
323 | setFilter(m_filter); |
324 | } |
325 | |
326 | void ActionEditor::slotSelectionChanged(const QItemSelection& selected, const QItemSelection& /*deselected*/) |
327 | { |
328 | const bool hasSelection = !selected.indexes().isEmpty(); |
329 | #if QT_CONFIG(clipboard) |
330 | m_actionCopy->setEnabled(hasSelection); |
331 | m_actionCut->setEnabled(hasSelection); |
332 | #endif |
333 | m_actionDelete->setEnabled(hasSelection); |
334 | } |
335 | |
336 | void ActionEditor::slotCurrentItemChanged(QAction *action) |
337 | { |
338 | QDesignerFormWindowInterface *fw = formWindow(); |
339 | if (!fw) |
340 | return; |
341 | |
342 | const bool hasCurrentAction = action != nullptr; |
343 | m_actionEdit->setEnabled(hasCurrentAction); |
344 | |
345 | if (!action) { |
346 | fw->clearSelection(); |
347 | return; |
348 | } |
349 | |
350 | QDesignerObjectInspector *oi = qobject_cast<QDesignerObjectInspector *>(object: core()->objectInspector()); |
351 | |
352 | if (action->associatedWidgets().isEmpty()) { |
353 | // Special case: action not in object tree. Deselect all and set in property editor |
354 | fw->clearSelection(changePropertyDisplay: false); |
355 | if (oi) |
356 | oi->clearSelection(); |
357 | core()->propertyEditor()->setObject(action); |
358 | } else { |
359 | if (oi) |
360 | oi->selectObject(o: action); |
361 | } |
362 | } |
363 | |
364 | void ActionEditor::slotActionChanged() |
365 | { |
366 | QAction *action = qobject_cast<QAction*>(object: sender()); |
367 | Q_ASSERT(action != nullptr); |
368 | |
369 | ActionModel *model = m_actionView->model(); |
370 | const int row = model->findAction(action); |
371 | if (row == -1) { |
372 | if (action->menu() == nullptr) // action got its menu deleted, create item |
373 | model->addAction(a: action); |
374 | } else if (action->menu() != nullptr) { // action got its menu created, remove item |
375 | model->removeRow(arow: row); |
376 | } else { |
377 | // action text or icon changed, update item |
378 | model->update(row); |
379 | } |
380 | } |
381 | |
382 | QDesignerFormEditorInterface *ActionEditor::core() const |
383 | { |
384 | return m_core; |
385 | } |
386 | |
387 | QString ActionEditor::filter() const |
388 | { |
389 | return m_filter; |
390 | } |
391 | |
392 | void ActionEditor::setFilter(const QString &f) |
393 | { |
394 | m_filter = f; |
395 | m_actionView->filter(text: m_filter); |
396 | } |
397 | |
398 | // Set changed state of icon property, reset when icon is cleared |
399 | static void refreshIconPropertyChanged(const QAction *action, QDesignerPropertySheetExtension *sheet) |
400 | { |
401 | sheet->setChanged(index: sheet->indexOf(name: QLatin1String(iconPropertyC)), changed: !action->icon().isNull()); |
402 | } |
403 | |
404 | void ActionEditor::manageAction(QAction *action) |
405 | { |
406 | action->setParent(formWindow()->mainContainer()); |
407 | core()->metaDataBase()->add(object: action); |
408 | |
409 | if (action->isSeparator() || action->menu() != nullptr) |
410 | return; |
411 | |
412 | QDesignerPropertySheetExtension *sheet = qt_extension<QDesignerPropertySheetExtension*>(manager: core()->extensionManager(), object: action); |
413 | sheet->setChanged(index: sheet->indexOf(name: QLatin1String(objectNamePropertyC)), changed: true); |
414 | sheet->setChanged(index: sheet->indexOf(name: QLatin1String(textPropertyC)), changed: true); |
415 | refreshIconPropertyChanged(action, sheet); |
416 | |
417 | m_actionView->setCurrentIndex(m_actionView->model()->addAction(a: action)); |
418 | connect(sender: action, signal: &QAction::changed, receiver: this, slot: &ActionEditor::slotActionChanged); |
419 | } |
420 | |
421 | void ActionEditor::unmanageAction(QAction *action) |
422 | { |
423 | core()->metaDataBase()->remove(object: action); |
424 | action->setParent(nullptr); |
425 | |
426 | disconnect(sender: action, signal: &QAction::changed, receiver: this, slot: &ActionEditor::slotActionChanged); |
427 | |
428 | const int row = m_actionView->model()->findAction(action); |
429 | if (row != -1) |
430 | m_actionView->model()->remove(row); |
431 | } |
432 | |
433 | // Set an initial property and mark it as changed in the sheet |
434 | static void setInitialProperty(QDesignerPropertySheetExtension *sheet, const QString &name, const QVariant &value) |
435 | { |
436 | const int index = sheet->indexOf(name); |
437 | Q_ASSERT(index != -1); |
438 | sheet->setProperty(index, value); |
439 | sheet->setChanged(index, changed: true); |
440 | } |
441 | |
442 | void ActionEditor::slotNewAction() |
443 | { |
444 | NewActionDialog dlg(this); |
445 | dlg.setWindowTitle(tr(s: "New action" )); |
446 | |
447 | if (dlg.exec() == QDialog::Accepted) { |
448 | const ActionData actionData = dlg.actionData(); |
449 | m_actionView->clearSelection(); |
450 | QAction *action = new QAction(formWindow()); |
451 | action->setObjectName(actionData.name); |
452 | formWindow()->ensureUniqueObjectName(object: action); |
453 | action->setText(actionData.text); |
454 | |
455 | QDesignerPropertySheetExtension *sheet = qt_extension<QDesignerPropertySheetExtension*>(manager: core()->extensionManager(), object: action); |
456 | if (!actionData.toolTip.isEmpty()) |
457 | setInitialProperty(sheet, name: QLatin1String(toolTipPropertyC), value: actionData.toolTip); |
458 | |
459 | if (actionData.checkable) |
460 | setInitialProperty(sheet, name: QLatin1String(checkablePropertyC), value: QVariant(true)); |
461 | |
462 | if (!actionData.keysequence.value().isEmpty()) |
463 | setInitialProperty(sheet, name: QLatin1String(shortcutPropertyC), value: QVariant::fromValue(value: actionData.keysequence)); |
464 | |
465 | sheet->setProperty(index: sheet->indexOf(name: QLatin1String(iconPropertyC)), value: QVariant::fromValue(value: actionData.icon)); |
466 | |
467 | AddActionCommand *cmd = new AddActionCommand(formWindow()); |
468 | cmd->init(action); |
469 | formWindow()->commandHistory()->push(cmd); |
470 | } |
471 | } |
472 | |
473 | // return a FormWindow command to apply an icon or a reset command in case it |
474 | // is empty. |
475 | |
476 | static QDesignerFormWindowCommand *setIconPropertyCommand(const PropertySheetIconValue &newIcon, QAction *action, QDesignerFormWindowInterface *fw) |
477 | { |
478 | const QString iconProperty = QLatin1String(iconPropertyC); |
479 | if (newIcon.isEmpty()) { |
480 | ResetPropertyCommand *cmd = new ResetPropertyCommand(fw); |
481 | cmd->init(object: action, propertyName: iconProperty); |
482 | return cmd; |
483 | } |
484 | SetPropertyCommand *cmd = new SetPropertyCommand(fw); |
485 | cmd->init(object: action, propertyName: iconProperty, newValue: QVariant::fromValue(value: newIcon)); |
486 | return cmd; |
487 | } |
488 | |
489 | // return a FormWindow command to apply a QKeySequence or a reset command |
490 | // in case it is empty. |
491 | |
492 | static QDesignerFormWindowCommand *setKeySequencePropertyCommand(const PropertySheetKeySequenceValue &ks, QAction *action, QDesignerFormWindowInterface *fw) |
493 | { |
494 | const QString shortcutProperty = QLatin1String(shortcutPropertyC); |
495 | if (ks.value().isEmpty()) { |
496 | ResetPropertyCommand *cmd = new ResetPropertyCommand(fw); |
497 | cmd->init(object: action, propertyName: shortcutProperty); |
498 | return cmd; |
499 | } |
500 | SetPropertyCommand *cmd = new SetPropertyCommand(fw); |
501 | cmd->init(object: action, propertyName: shortcutProperty, newValue: QVariant::fromValue(value: ks)); |
502 | return cmd; |
503 | } |
504 | |
505 | // return a FormWindow command to apply a POD value or reset command in case |
506 | // it is equal to the default value. |
507 | |
508 | template <class T> |
509 | QDesignerFormWindowCommand *setPropertyCommand(const QString &name, T value, T defaultValue, |
510 | QObject *o, QDesignerFormWindowInterface *fw) |
511 | { |
512 | if (value == defaultValue) { |
513 | ResetPropertyCommand *cmd = new ResetPropertyCommand(fw); |
514 | cmd->init(object: o, propertyName: name); |
515 | return cmd; |
516 | } |
517 | SetPropertyCommand *cmd = new SetPropertyCommand(fw); |
518 | cmd->init(object: o, propertyName: name, newValue: QVariant(value)); |
519 | return cmd; |
520 | } |
521 | |
522 | // Return the text value of a string property via PropertySheetStringValue |
523 | static inline QString textPropertyValue(const QDesignerPropertySheetExtension *sheet, const QString &name) |
524 | { |
525 | const int index = sheet->indexOf(name); |
526 | Q_ASSERT(index != -1); |
527 | const PropertySheetStringValue ps = qvariant_cast<PropertySheetStringValue>(v: sheet->property(index)); |
528 | return ps.value(); |
529 | } |
530 | |
531 | void ActionEditor::editAction(QAction *action, int column) |
532 | { |
533 | if (!action) |
534 | return; |
535 | |
536 | NewActionDialog dlg(this); |
537 | dlg.setWindowTitle(tr(s: "Edit action" )); |
538 | |
539 | ActionData oldActionData; |
540 | QDesignerPropertySheetExtension *sheet = qt_extension<QDesignerPropertySheetExtension*>(manager: core()->extensionManager(), object: action); |
541 | oldActionData.name = action->objectName(); |
542 | oldActionData.text = action->text(); |
543 | oldActionData.toolTip = textPropertyValue(sheet, name: QLatin1String(toolTipPropertyC)); |
544 | oldActionData.icon = qvariant_cast<PropertySheetIconValue>(v: sheet->property(index: sheet->indexOf(name: QLatin1String(iconPropertyC)))); |
545 | oldActionData.keysequence = ActionModel::actionShortCut(ps: sheet); |
546 | oldActionData.checkable = action->isCheckable(); |
547 | dlg.setActionData(oldActionData); |
548 | |
549 | switch (column) { |
550 | case qdesigner_internal::ActionModel::NameColumn: |
551 | dlg.focusName(); |
552 | break; |
553 | case qdesigner_internal::ActionModel::TextColumn: |
554 | dlg.focusText(); |
555 | break; |
556 | case qdesigner_internal::ActionModel::ShortCutColumn: |
557 | dlg.focusShortcut(); |
558 | break; |
559 | case qdesigner_internal::ActionModel::CheckedColumn: |
560 | dlg.focusCheckable(); |
561 | break; |
562 | case qdesigner_internal::ActionModel::ToolTipColumn: |
563 | dlg.focusTooltip(); |
564 | break; |
565 | } |
566 | |
567 | if (!dlg.exec()) |
568 | return; |
569 | |
570 | // figure out changes and whether to start a macro |
571 | const ActionData newActionData = dlg.actionData(); |
572 | const unsigned changeMask = newActionData.compare(rhs: oldActionData); |
573 | if (changeMask == 0u) |
574 | return; |
575 | |
576 | const bool severalChanges = (changeMask != ActionData::TextChanged) && (changeMask != ActionData::NameChanged) |
577 | && (changeMask != ActionData::ToolTipChanged) && (changeMask != ActionData::IconChanged) |
578 | && (changeMask != ActionData::CheckableChanged) && (changeMask != ActionData::KeysequenceChanged); |
579 | |
580 | QDesignerFormWindowInterface *fw = formWindow(); |
581 | QUndoStack *undoStack = fw->commandHistory(); |
582 | if (severalChanges) |
583 | fw->beginCommand(QStringLiteral("Edit action" )); |
584 | |
585 | if (changeMask & ActionData::NameChanged) |
586 | undoStack->push(cmd: createTextPropertyCommand(propertyName: QLatin1String(objectNamePropertyC), text: newActionData.name, object: action, fw)); |
587 | |
588 | if (changeMask & ActionData::TextChanged) |
589 | undoStack->push(cmd: createTextPropertyCommand(propertyName: QLatin1String(textPropertyC), text: newActionData.text, object: action, fw)); |
590 | |
591 | if (changeMask & ActionData::ToolTipChanged) |
592 | undoStack->push(cmd: createTextPropertyCommand(propertyName: QLatin1String(toolTipPropertyC), text: newActionData.toolTip, object: action, fw)); |
593 | |
594 | if (changeMask & ActionData::IconChanged) |
595 | undoStack->push(cmd: setIconPropertyCommand(newIcon: newActionData.icon, action, fw)); |
596 | |
597 | if (changeMask & ActionData::CheckableChanged) |
598 | undoStack->push(cmd: setPropertyCommand(name: QLatin1String(checkablePropertyC), value: newActionData.checkable, defaultValue: false, o: action, fw)); |
599 | |
600 | if (changeMask & ActionData::KeysequenceChanged) |
601 | undoStack->push(cmd: setKeySequencePropertyCommand(ks: newActionData.keysequence, action, fw)); |
602 | |
603 | if (severalChanges) |
604 | fw->endCommand(); |
605 | } |
606 | |
607 | void ActionEditor::editCurrentAction() |
608 | { |
609 | if (QAction *a = m_actionView->currentAction()) |
610 | editAction(action: a); |
611 | } |
612 | |
613 | void ActionEditor::navigateToSlotCurrentAction() |
614 | { |
615 | if (QAction *a = m_actionView->currentAction()) |
616 | QDesignerTaskMenu::navigateToSlot(core: m_core, o: a, QStringLiteral("triggered()" )); |
617 | } |
618 | |
619 | void ActionEditor::deleteActions(QDesignerFormWindowInterface *fw, const ActionList &actions) |
620 | { |
621 | // We need a macro even in the case of single action because the commands might cause the |
622 | // scheduling of other commands (signal slots connections) |
623 | const QString description = actions.size() == 1 |
624 | ? tr(s: "Remove action '%1'" ).arg(a: actions.constFirst()->objectName()) |
625 | : tr(s: "Remove actions" ); |
626 | fw->beginCommand(description); |
627 | for (QAction *action : actions) { |
628 | RemoveActionCommand *cmd = new RemoveActionCommand(fw); |
629 | cmd->init(action); |
630 | fw->commandHistory()->push(cmd); |
631 | } |
632 | fw->endCommand(); |
633 | } |
634 | |
635 | #if QT_CONFIG(clipboard) |
636 | void ActionEditor::copyActions(QDesignerFormWindowInterface *fwi, const ActionList &actions) |
637 | { |
638 | FormWindowBase *fw = qobject_cast<FormWindowBase *>(object: fwi); |
639 | if (!fw ) |
640 | return; |
641 | |
642 | FormBuilderClipboard clipboard; |
643 | clipboard.m_actions = actions; |
644 | |
645 | if (clipboard.empty()) |
646 | return; |
647 | |
648 | QEditorFormBuilder *formBuilder = fw->createFormBuilder(); |
649 | Q_ASSERT(formBuilder); |
650 | |
651 | QBuffer buffer; |
652 | if (buffer.open(openMode: QIODevice::WriteOnly)) |
653 | if (formBuilder->copy(dev: &buffer, selection: clipboard)) |
654 | qApp->clipboard()->setText(QString::fromUtf8(str: buffer.buffer()), mode: QClipboard::Clipboard); |
655 | delete formBuilder; |
656 | } |
657 | #endif |
658 | |
659 | void ActionEditor::slotDelete() |
660 | { |
661 | QDesignerFormWindowInterface *fw = formWindow(); |
662 | if (!fw) |
663 | return; |
664 | |
665 | const ActionView::ActionList selection = m_actionView->selectedActions(); |
666 | if (selection.isEmpty()) |
667 | return; |
668 | |
669 | deleteActions(fw, actions: selection); |
670 | } |
671 | |
672 | // UnderScore: "Open file" -> actionOpen_file |
673 | static QString underscore(QString text) |
674 | { |
675 | const QString underscore = QString(QLatin1Char('_')); |
676 | static const QRegularExpression nonAsciiPattern(QStringLiteral("[^a-zA-Z_0-9]" )); |
677 | Q_ASSERT(nonAsciiPattern.isValid()); |
678 | text.replace(re: nonAsciiPattern, after: underscore); |
679 | static const QRegularExpression multipleSpacePattern(QStringLiteral("__*" )); |
680 | Q_ASSERT(multipleSpacePattern.isValid()); |
681 | text.replace(re: multipleSpacePattern, after: underscore); |
682 | if (text.endsWith(c: underscore.at(i: 0))) |
683 | text.chop(n: 1); |
684 | return text; |
685 | } |
686 | |
687 | // CamelCase: "Open file" -> actionOpenFile, ignoring non-ASCII letters. |
688 | |
689 | enum CharacterCategory { OtherCharacter, DigitOrAsciiLetter, NonAsciiLetter }; |
690 | |
691 | static inline CharacterCategory category(QChar c) |
692 | { |
693 | if (c.isDigit()) |
694 | return DigitOrAsciiLetter; |
695 | if (c.isLetter()) { |
696 | const ushort uc = c.unicode(); |
697 | return (uc >= 'a' && uc <= 'z') || (uc >= 'A' && uc <= 'Z') |
698 | ? DigitOrAsciiLetter : NonAsciiLetter; |
699 | } |
700 | return OtherCharacter; |
701 | } |
702 | |
703 | static QString camelCase(const QString &text) |
704 | { |
705 | QString result; |
706 | result.reserve(asize: text.size()); |
707 | bool lastCharAccepted = false; |
708 | for (QChar c : text) { |
709 | const CharacterCategory cat = category(c); |
710 | if (cat != NonAsciiLetter) { |
711 | const bool acceptable = cat == DigitOrAsciiLetter; |
712 | if (acceptable) |
713 | result.append(c: lastCharAccepted ? c : c.toUpper()); // New word starts |
714 | lastCharAccepted = acceptable; |
715 | } |
716 | } |
717 | return result; |
718 | } |
719 | |
720 | QString ActionEditor::actionTextToName(const QString &text, const QString &prefix) |
721 | { |
722 | QString name = text; |
723 | if (name.isEmpty()) |
724 | return QString(); |
725 | return prefix + (m_objectNamingMode == CamelCase ? camelCase(text) : underscore(text)); |
726 | |
727 | } |
728 | |
729 | void ActionEditor::resourceImageDropped(const QString &path, QAction *action) |
730 | { |
731 | QDesignerFormWindowInterface *fw = formWindow(); |
732 | if (!fw) |
733 | return; |
734 | |
735 | QDesignerPropertySheetExtension *sheet = qt_extension<QDesignerPropertySheetExtension*>(manager: core()->extensionManager(), object: action); |
736 | const PropertySheetIconValue oldIcon = |
737 | qvariant_cast<PropertySheetIconValue>(v: sheet->property(index: sheet->indexOf(name: QLatin1String(iconPropertyC)))); |
738 | PropertySheetIconValue newIcon; |
739 | newIcon.setPixmap(mode: QIcon::Normal, state: QIcon::Off, path: PropertySheetPixmapValue(path)); |
740 | if (newIcon.paths().isEmpty() || newIcon.paths() == oldIcon.paths()) |
741 | return; |
742 | |
743 | fw->commandHistory()->push(cmd: setIconPropertyCommand(newIcon , action, fw)); |
744 | } |
745 | |
746 | void ActionEditor::mainContainerChanged() |
747 | { |
748 | // Invalidate references to objects kept in model |
749 | if (sender() == formWindow()) |
750 | setFormWindow(nullptr); |
751 | } |
752 | |
753 | void ActionEditor::slotViewMode(QAction *a) |
754 | { |
755 | m_actionView->setViewMode(a->data().toInt()); |
756 | updateViewModeActions(); |
757 | } |
758 | |
759 | void ActionEditor::slotSelectAssociatedWidget(QWidget *w) |
760 | { |
761 | QDesignerFormWindowInterface *fw = formWindow(); |
762 | if (!fw ) |
763 | return; |
764 | |
765 | QDesignerObjectInspector *oi = qobject_cast<QDesignerObjectInspector *>(object: core()->objectInspector()); |
766 | if (!oi) |
767 | return; |
768 | |
769 | fw->clearSelection(); // Actually, there are no widgets selected due to focus in event handling. Just to be sure. |
770 | oi->selectObject(o: w); |
771 | } |
772 | |
773 | void ActionEditor::restoreSettings() |
774 | { |
775 | QDesignerSettingsInterface *settings = m_core->settingsManager(); |
776 | m_actionView->setViewMode(settings->value(key: QLatin1String(actionEditorViewModeKey), defaultValue: 0).toInt()); |
777 | updateViewModeActions(); |
778 | } |
779 | |
780 | void ActionEditor::saveSettings() |
781 | { |
782 | QDesignerSettingsInterface *settings = m_core->settingsManager(); |
783 | settings->setValue(key: QLatin1String(actionEditorViewModeKey), value: m_actionView->viewMode()); |
784 | } |
785 | |
786 | void ActionEditor::updateViewModeActions() |
787 | { |
788 | switch (m_actionView->viewMode()) { |
789 | case ActionView::IconView: |
790 | m_iconViewAction->setChecked(true); |
791 | break; |
792 | case ActionView::DetailedView: |
793 | m_listViewAction->setChecked(true); |
794 | break; |
795 | } |
796 | } |
797 | |
798 | #if QT_CONFIG(clipboard) |
799 | void ActionEditor::slotCopy() |
800 | { |
801 | QDesignerFormWindowInterface *fw = formWindow(); |
802 | if (!fw ) |
803 | return; |
804 | |
805 | const ActionView::ActionList selection = m_actionView->selectedActions(); |
806 | if (selection.isEmpty()) |
807 | return; |
808 | |
809 | copyActions(fwi: fw, actions: selection); |
810 | } |
811 | |
812 | void ActionEditor::slotCut() |
813 | { |
814 | QDesignerFormWindowInterface *fw = formWindow(); |
815 | if (!fw ) |
816 | return; |
817 | |
818 | const ActionView::ActionList selection = m_actionView->selectedActions(); |
819 | if (selection.isEmpty()) |
820 | return; |
821 | |
822 | copyActions(fwi: fw, actions: selection); |
823 | deleteActions(fw, actions: selection); |
824 | } |
825 | |
826 | void ActionEditor::slotPaste() |
827 | { |
828 | FormWindowBase *fw = qobject_cast<FormWindowBase *>(object: formWindow()); |
829 | if (!fw) |
830 | return; |
831 | m_actionView->clearSelection(); |
832 | fw->paste(pasteMode: FormWindowBase::PasteActionsOnly); |
833 | } |
834 | #endif |
835 | |
836 | void ActionEditor::(QContextMenuEvent *e, QAction *item) |
837 | { |
838 | QMenu (this); |
839 | menu.addAction(action: m_actionNew); |
840 | menu.addSeparator(); |
841 | menu.addAction(action: m_actionEdit); |
842 | if (QDesignerTaskMenu::isSlotNavigationEnabled(core: m_core)) |
843 | menu.addAction(action: m_actionNavigateToSlot); |
844 | |
845 | // Associated Widgets |
846 | if (QAction *action = m_actionView->currentAction()) { |
847 | const QWidgetList associatedWidgets = ActionModel::associatedWidgets(action); |
848 | if (!associatedWidgets.isEmpty()) { |
849 | QMenu * = menu.addMenu(title: tr(s: "Used In" )); |
850 | for (QWidget *w : associatedWidgets) { |
851 | associatedWidgetsSubMenu->addAction(text: w->objectName(), |
852 | object: this, slot: [this, w] { this->slotSelectAssociatedWidget(w); }); |
853 | } |
854 | } |
855 | } |
856 | |
857 | menu.addSeparator(); |
858 | #if QT_CONFIG(clipboard) |
859 | menu.addAction(action: m_actionCut); |
860 | menu.addAction(action: m_actionCopy); |
861 | menu.addAction(action: m_actionPaste); |
862 | #endif |
863 | menu.addAction(action: m_actionSelectAll); |
864 | menu.addAction(action: m_actionDelete); |
865 | menu.addSeparator(); |
866 | menu.addAction(action: m_iconViewAction); |
867 | menu.addAction(action: m_listViewAction); |
868 | |
869 | emit contextMenuRequested(menu: &menu, item); |
870 | |
871 | menu.exec(pos: e->globalPos()); |
872 | e->accept(); |
873 | } |
874 | |
875 | } // namespace qdesigner_internal |
876 | |
877 | QT_END_NAMESPACE |
878 | |
879 | |