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:
ActionGroupDelegate(QObject * parent)89     ActionGroupDelegate(QObject *parent)
90         : QItemDelegate(parent) {}
91 
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const92     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 
drawFocus(QPainter *,const QStyleOptionViewItem &,const QRect &) const100     void drawFocus(QPainter *, const QStyleOptionViewItem &, const QRect &) const override {}
101 };
102 
103 //--------  ActionEditor
104 ObjectNamingMode ActionEditor::m_objectNamingMode = Underscore; // fixme Qt 6: CamelCase
105 
ActionEditor(QDesignerFormEditorInterface * core,QWidget * parent,Qt::WindowFlags flags)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("New..."), this)),
112     m_actionEdit(new QAction(tr("Edit..."), this)),
113     m_actionNavigateToSlot(new QAction(tr("Go to slot..."), this)),
114 #if QT_CONFIG(clipboard)
115     m_actionCopy(new QAction(tr("Copy"), this)),
116     m_actionCut(new QAction(tr("Cut"), this)),
117     m_actionPaste(new QAction(tr("Paste"), this)),
118 #endif
119     m_actionSelectAll(new QAction(tr("Select all"), this)),
120     m_actionDelete(new QAction(tr("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(m_core);
127     m_actionView->setSelectionMode(QAbstractItemView::ExtendedSelection);
128     setWindowTitle(tr("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(QSizePolicy::Expanding, QSizePolicy::Minimum);
137     l->addWidget(toolbar);
138     // edit actions
139     QIcon documentNewIcon = QIcon::fromTheme(QStringLiteral("document-new"), createIconSet(QStringLiteral("filenew.png")));
140     m_actionNew->setIcon(documentNewIcon);
141     m_actionNew->setEnabled(false);
142     connect(m_actionNew, &QAction::triggered, this, &ActionEditor::slotNewAction);
143     toolbar->addAction(m_actionNew);
144 
145     connect(m_actionSelectAll, &QAction::triggered, m_actionView, &ActionView::selectAll);
146 
147 #if QT_CONFIG(clipboard)
148     m_actionCut->setEnabled(false);
149     connect(m_actionCut, &QAction::triggered, this, &ActionEditor::slotCut);
150     QIcon editCutIcon = QIcon::fromTheme(QStringLiteral("edit-cut"), createIconSet(QStringLiteral("editcut.png")));
151     m_actionCut->setIcon(editCutIcon);
152 
153     m_actionCopy->setEnabled(false);
154     connect(m_actionCopy, &QAction::triggered, this, &ActionEditor::slotCopy);
155     QIcon editCopyIcon = QIcon::fromTheme(QStringLiteral("edit-copy"), createIconSet(QStringLiteral("editcopy.png")));
156     m_actionCopy->setIcon(editCopyIcon);
157     toolbar->addAction(m_actionCopy);
158 
159     connect(m_actionPaste, &QAction::triggered, this, &ActionEditor::slotPaste);
160     QIcon editPasteIcon = QIcon::fromTheme(QStringLiteral("edit-paste"), createIconSet(QStringLiteral("editpaste.png")));
161     m_actionPaste->setIcon(editPasteIcon);
162     toolbar->addAction(m_actionPaste);
163 #endif
164 
165     m_actionEdit->setEnabled(false);
166     connect(m_actionEdit, &QAction::triggered, this, &ActionEditor::editCurrentAction);
167 
168     connect(m_actionNavigateToSlot, &QAction::triggered, this, &ActionEditor::navigateToSlotCurrentAction);
169 
170     QIcon editDeleteIcon = QIcon::fromTheme(QStringLiteral("edit-delete"), createIconSet(QStringLiteral("editdelete.png")));
171     m_actionDelete->setIcon(editDeleteIcon);
172     m_actionDelete->setEnabled(false);
173     connect(m_actionDelete, &QAction::triggered, this, &ActionEditor::slotDelete);
174     toolbar->addAction(m_actionDelete);
175 
176     // Toolbutton with menu containing action group for detailed/icon view. Steal the icons from the file dialog.
177     //
178     QMenu *configureMenu;
179     toolbar->addWidget(createConfigureMenuButton(tr("Configure Action Editor"), &configureMenu));
180 
181     connect(m_viewModeGroup, &QActionGroup::triggered, this, &ActionEditor::slotViewMode);
182     m_iconViewAction = m_viewModeGroup->addAction(tr("Icon View"));
183     m_iconViewAction->setData(QVariant(ActionView::IconView));
184     m_iconViewAction->setCheckable(true);
185     m_iconViewAction->setIcon(style()->standardIcon (QStyle::SP_FileDialogListView));
186     configureMenu->addAction(m_iconViewAction);
187 
188     m_listViewAction = m_viewModeGroup->addAction(tr("Detailed View"));
189     m_listViewAction->setData(QVariant(ActionView::DetailedView));
190     m_listViewAction->setCheckable(true);
191     m_listViewAction->setIcon(style()->standardIcon (QStyle::SP_FileDialogDetailedView));
192     configureMenu->addAction(m_listViewAction);
193     // filter
194     m_filterWidget = new QWidget(toolbar);
195     QHBoxLayout *filterLayout = new QHBoxLayout(m_filterWidget);
196     filterLayout->setContentsMargins(0, 0, 0, 0);
197     QLineEdit *filterLineEdit = new QLineEdit(m_filterWidget);
198     connect(filterLineEdit, &QLineEdit::textChanged, this, &ActionEditor::setFilter);
199     filterLineEdit->setPlaceholderText(tr("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(m_filterWidget);
205 
206     // main layout
207     QSplitter *splitter = new QSplitter(Qt::Horizontal);
208     splitter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
209 
210     splitter->addWidget(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(m_actionView, &ActionView::resourceImageDropped,
226             this, &ActionEditor::resourceImageDropped);
227 
228     connect(m_actionView, &ActionView::currentChanged,this, &ActionEditor::slotCurrentItemChanged);
229     // make it possible for vs integration to reimplement edit action dialog
230     connect(m_actionView, &ActionView::activated, this, &ActionEditor::itemActivated);
231 
232     connect(m_actionView, &ActionView::selectionChanged,
233             this, &ActionEditor::slotSelectionChanged);
234 
235     connect(m_actionView, &ActionView::contextMenuRequested,
236             this, &ActionEditor::slotContextMenuRequested);
237 
238     connect(this, &ActionEditor::itemActivated, this, &ActionEditor::editAction);
239 
240     restoreSettings();
241     updateViewModeActions();
242 }
243 
244 // Utility to create a configure button with menu for usage on toolbars
createConfigureMenuButton(const QString & t,QMenu ** ptrToMenu)245 QToolButton *ActionEditor::createConfigureMenuButton(const QString &t, QMenu **ptrToMenu)
246 {
247     QToolButton *configureButton = new QToolButton;
248     QAction *configureAction = new QAction(t, configureButton);
249     QIcon configureIcon = QIcon::fromTheme(QStringLiteral("document-properties"), createIconSet(QStringLiteral("configure.png")));
250     configureAction->setIcon(configureIcon);
251     QMenu *configureMenu = new QMenu;
252     configureAction->setMenu(configureMenu);
253     configureButton->setDefaultAction(configureAction);
254     configureButton->setPopupMode(QToolButton::InstantPopup);
255     *ptrToMenu = configureMenu;
256     return configureButton;
257 }
258 
~ActionEditor()259 ActionEditor::~ActionEditor()
260 {
261     saveSettings();
262 }
263 
actionNew() const264 QAction *ActionEditor::actionNew() const
265 {
266     return m_actionNew;
267 }
268 
actionDelete() const269 QAction *ActionEditor::actionDelete() const
270 {
271     return m_actionDelete;
272 }
273 
formWindow() const274 QDesignerFormWindowInterface *ActionEditor::formWindow() const
275 {
276     return m_formWindow;
277 }
278 
setFormWindow(QDesignerFormWindowInterface * formWindow)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(action, &QAction::changed, this, &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(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(action);
320             connect(action, &QAction::changed, this, &ActionEditor::slotActionChanged);
321         }
322 
323     setFilter(m_filter);
324 }
325 
slotSelectionChanged(const QItemSelection & selected,const QItemSelection &)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 
slotCurrentItemChanged(QAction * action)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 *>(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(false);
355         if (oi)
356             oi->clearSelection();
357         core()->propertyEditor()->setObject(action);
358     } else {
359         if (oi)
360             oi->selectObject(action);
361     }
362 }
363 
slotActionChanged()364 void ActionEditor::slotActionChanged()
365 {
366     QAction *action = qobject_cast<QAction*>(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(action);
374     } else if (action->menu() != nullptr) { // action got its menu created, remove item
375         model->removeRow(row);
376     } else {
377         // action text or icon changed, update item
378         model->update(row);
379     }
380 }
381 
core() const382 QDesignerFormEditorInterface *ActionEditor::core() const
383 {
384     return m_core;
385 }
386 
filter() const387 QString ActionEditor::filter() const
388 {
389     return m_filter;
390 }
391 
setFilter(const QString & f)392 void ActionEditor::setFilter(const QString &f)
393 {
394     m_filter = f;
395     m_actionView->filter(m_filter);
396 }
397 
398 // Set changed state of icon property,  reset when icon is cleared
refreshIconPropertyChanged(const QAction * action,QDesignerPropertySheetExtension * sheet)399 static void refreshIconPropertyChanged(const QAction *action, QDesignerPropertySheetExtension *sheet)
400 {
401     sheet->setChanged(sheet->indexOf(QLatin1String(iconPropertyC)), !action->icon().isNull());
402 }
403 
manageAction(QAction * action)404 void ActionEditor::manageAction(QAction *action)
405 {
406     action->setParent(formWindow()->mainContainer());
407     core()->metaDataBase()->add(action);
408 
409     if (action->isSeparator() || action->menu() != nullptr)
410         return;
411 
412     QDesignerPropertySheetExtension *sheet = qt_extension<QDesignerPropertySheetExtension*>(core()->extensionManager(), action);
413     sheet->setChanged(sheet->indexOf(QLatin1String(objectNamePropertyC)), true);
414     sheet->setChanged(sheet->indexOf(QLatin1String(textPropertyC)), true);
415     refreshIconPropertyChanged(action, sheet);
416 
417     m_actionView->setCurrentIndex(m_actionView->model()->addAction(action));
418     connect(action, &QAction::changed, this, &ActionEditor::slotActionChanged);
419 }
420 
unmanageAction(QAction * action)421 void ActionEditor::unmanageAction(QAction *action)
422 {
423     core()->metaDataBase()->remove(action);
424     action->setParent(nullptr);
425 
426     disconnect(action, &QAction::changed, this, &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
setInitialProperty(QDesignerPropertySheetExtension * sheet,const QString & name,const QVariant & value)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, true);
440 }
441 
slotNewAction()442 void ActionEditor::slotNewAction()
443 {
444     NewActionDialog dlg(this);
445     dlg.setWindowTitle(tr("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(action);
453         action->setText(actionData.text);
454 
455         QDesignerPropertySheetExtension *sheet = qt_extension<QDesignerPropertySheetExtension*>(core()->extensionManager(), action);
456         if (!actionData.toolTip.isEmpty())
457             setInitialProperty(sheet, QLatin1String(toolTipPropertyC), actionData.toolTip);
458 
459         if (actionData.checkable)
460             setInitialProperty(sheet, QLatin1String(checkablePropertyC), QVariant(true));
461 
462         if (!actionData.keysequence.value().isEmpty())
463             setInitialProperty(sheet, QLatin1String(shortcutPropertyC), QVariant::fromValue(actionData.keysequence));
464 
465         sheet->setProperty(sheet->indexOf(QLatin1String(iconPropertyC)), QVariant::fromValue(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 
setIconPropertyCommand(const PropertySheetIconValue & newIcon,QAction * action,QDesignerFormWindowInterface * fw)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(action, iconProperty);
482         return cmd;
483     }
484     SetPropertyCommand *cmd = new SetPropertyCommand(fw);
485     cmd->init(action, iconProperty, QVariant::fromValue(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 
setKeySequencePropertyCommand(const PropertySheetKeySequenceValue & ks,QAction * action,QDesignerFormWindowInterface * fw)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(action, shortcutProperty);
498         return cmd;
499     }
500     SetPropertyCommand *cmd = new SetPropertyCommand(fw);
501     cmd->init(action, shortcutProperty, QVariant::fromValue(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>
setPropertyCommand(const QString & name,T value,T defaultValue,QObject * o,QDesignerFormWindowInterface * fw)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(o, name);
515         return cmd;
516     }
517     SetPropertyCommand *cmd = new SetPropertyCommand(fw);
518     cmd->init(o, name, QVariant(value));
519     return cmd;
520 }
521 
522 // Return the text value of a string property via PropertySheetStringValue
textPropertyValue(const QDesignerPropertySheetExtension * sheet,const QString & name)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>(sheet->property(index));
528     return ps.value();
529 }
530 
editAction(QAction * action,int column)531 void ActionEditor::editAction(QAction *action, int column)
532 {
533     if (!action)
534         return;
535 
536     NewActionDialog dlg(this);
537     dlg.setWindowTitle(tr("Edit action"));
538 
539     ActionData oldActionData;
540     QDesignerPropertySheetExtension *sheet = qt_extension<QDesignerPropertySheetExtension*>(core()->extensionManager(), action);
541     oldActionData.name = action->objectName();
542     oldActionData.text = action->text();
543     oldActionData.toolTip = textPropertyValue(sheet, QLatin1String(toolTipPropertyC));
544     oldActionData.icon = qvariant_cast<PropertySheetIconValue>(sheet->property(sheet->indexOf(QLatin1String(iconPropertyC))));
545     oldActionData.keysequence = ActionModel::actionShortCut(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(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(createTextPropertyCommand(QLatin1String(objectNamePropertyC), newActionData.name, action, fw));
587 
588     if (changeMask & ActionData::TextChanged)
589         undoStack->push(createTextPropertyCommand(QLatin1String(textPropertyC), newActionData.text, action, fw));
590 
591     if (changeMask & ActionData::ToolTipChanged)
592         undoStack->push(createTextPropertyCommand(QLatin1String(toolTipPropertyC), newActionData.toolTip, action, fw));
593 
594     if (changeMask & ActionData::IconChanged)
595         undoStack->push(setIconPropertyCommand(newActionData.icon, action, fw));
596 
597     if (changeMask & ActionData::CheckableChanged)
598         undoStack->push(setPropertyCommand(QLatin1String(checkablePropertyC), newActionData.checkable, false, action, fw));
599 
600     if (changeMask & ActionData::KeysequenceChanged)
601         undoStack->push(setKeySequencePropertyCommand(newActionData.keysequence, action, fw));
602 
603     if (severalChanges)
604         fw->endCommand();
605 }
606 
editCurrentAction()607 void ActionEditor::editCurrentAction()
608 {
609     if (QAction *a = m_actionView->currentAction())
610         editAction(a);
611 }
612 
navigateToSlotCurrentAction()613 void ActionEditor::navigateToSlotCurrentAction()
614 {
615     if (QAction *a = m_actionView->currentAction())
616         QDesignerTaskMenu::navigateToSlot(m_core, a, QStringLiteral("triggered()"));
617 }
618 
deleteActions(QDesignerFormWindowInterface * fw,const ActionList & actions)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("Remove action '%1'").arg(actions.constFirst()->objectName())
625         : tr("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)
copyActions(QDesignerFormWindowInterface * fwi,const ActionList & actions)636 void ActionEditor::copyActions(QDesignerFormWindowInterface *fwi, const ActionList &actions)
637 {
638     FormWindowBase *fw = qobject_cast<FormWindowBase *>(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(QIODevice::WriteOnly))
653         if (formBuilder->copy(&buffer, clipboard))
654             qApp->clipboard()->setText(QString::fromUtf8(buffer.buffer()), QClipboard::Clipboard);
655     delete formBuilder;
656 }
657 #endif
658 
slotDelete()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,  selection);
670 }
671 
672 // UnderScore: "Open file" -> actionOpen_file
underscore(QString text)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(nonAsciiPattern, underscore);
679     static const QRegularExpression multipleSpacePattern(QStringLiteral("__*"));
680     Q_ASSERT(multipleSpacePattern.isValid());
681     text.replace(multipleSpacePattern, underscore);
682     if (text.endsWith(underscore.at(0)))
683         text.chop(1);
684     return text;
685 }
686 
687 // CamelCase: "Open file" -> actionOpenFile, ignoring non-ASCII letters.
688 
689 enum CharacterCategory { OtherCharacter, DigitOrAsciiLetter, NonAsciiLetter };
690 
category(QChar c)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 
camelCase(const QString & text)703 static QString camelCase(const QString &text)
704 {
705     QString result;
706     result.reserve(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(lastCharAccepted ? c : c.toUpper()); // New word starts
714             lastCharAccepted = acceptable;
715         }
716     }
717     return result;
718 }
719 
actionTextToName(const QString & text,const QString & prefix)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 
resourceImageDropped(const QString & path,QAction * action)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*>(core()->extensionManager(), action);
736     const PropertySheetIconValue oldIcon =
737             qvariant_cast<PropertySheetIconValue>(sheet->property(sheet->indexOf(QLatin1String(iconPropertyC))));
738     PropertySheetIconValue newIcon;
739     newIcon.setPixmap(QIcon::Normal, QIcon::Off, PropertySheetPixmapValue(path));
740     if (newIcon.paths().isEmpty() || newIcon.paths() == oldIcon.paths())
741         return;
742 
743     fw->commandHistory()->push(setIconPropertyCommand(newIcon , action, fw));
744 }
745 
mainContainerChanged()746 void ActionEditor::mainContainerChanged()
747 {
748     // Invalidate references to objects kept in model
749     if (sender() == formWindow())
750         setFormWindow(nullptr);
751 }
752 
slotViewMode(QAction * a)753 void ActionEditor::slotViewMode(QAction *a)
754 {
755     m_actionView->setViewMode(a->data().toInt());
756     updateViewModeActions();
757 }
758 
slotSelectAssociatedWidget(QWidget * w)759 void ActionEditor::slotSelectAssociatedWidget(QWidget *w)
760 {
761     QDesignerFormWindowInterface *fw = formWindow();
762     if (!fw )
763         return;
764 
765     QDesignerObjectInspector *oi = qobject_cast<QDesignerObjectInspector *>(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(w);
771 }
772 
restoreSettings()773 void ActionEditor::restoreSettings()
774 {
775     QDesignerSettingsInterface *settings = m_core->settingsManager();
776     m_actionView->setViewMode(settings->value(QLatin1String(actionEditorViewModeKey), 0).toInt());
777     updateViewModeActions();
778 }
779 
saveSettings()780 void ActionEditor::saveSettings()
781 {
782     QDesignerSettingsInterface *settings = m_core->settingsManager();
783     settings->setValue(QLatin1String(actionEditorViewModeKey), m_actionView->viewMode());
784 }
785 
updateViewModeActions()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)
slotCopy()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(fw, selection);
810 }
811 
slotCut()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(fw, selection);
823     deleteActions(fw, selection);
824 }
825 
slotPaste()826 void ActionEditor::slotPaste()
827 {
828     FormWindowBase *fw = qobject_cast<FormWindowBase *>(formWindow());
829     if (!fw)
830         return;
831     m_actionView->clearSelection();
832     fw->paste(FormWindowBase::PasteActionsOnly);
833 }
834 #endif
835 
slotContextMenuRequested(QContextMenuEvent * e,QAction * item)836 void ActionEditor::slotContextMenuRequested(QContextMenuEvent *e, QAction *item)
837 {
838     QMenu menu(this);
839     menu.addAction(m_actionNew);
840     menu.addSeparator();
841     menu.addAction(m_actionEdit);
842     if (QDesignerTaskMenu::isSlotNavigationEnabled(m_core))
843         menu.addAction(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 *associatedWidgetsSubMenu =  menu.addMenu(tr("Used In"));
850             for (QWidget *w : associatedWidgets) {
851                 associatedWidgetsSubMenu->addAction(w->objectName(),
852                                                     this, [this, w] { this->slotSelectAssociatedWidget(w); });
853             }
854         }
855     }
856 
857     menu.addSeparator();
858 #if QT_CONFIG(clipboard)
859     menu.addAction(m_actionCut);
860     menu.addAction(m_actionCopy);
861     menu.addAction(m_actionPaste);
862 #endif
863     menu.addAction(m_actionSelectAll);
864     menu.addAction(m_actionDelete);
865     menu.addSeparator();
866     menu.addAction(m_iconViewAction);
867     menu.addAction(m_listViewAction);
868 
869     emit contextMenuRequested(&menu, item);
870 
871     menu.exec(e->globalPos());
872     e->accept();
873 }
874 
875 } // namespace qdesigner_internal
876 
877 QT_END_NAMESPACE
878 
879