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 "actionrepository_p.h"
30#include "qtresourceview_p.h"
31#include "iconloader_p.h"
32#include "qdesigner_utils_p.h"
33
34#include <QtDesigner/abstractformeditor.h>
35#include <QtDesigner/propertysheet.h>
36#include <QtDesigner/qextensionmanager.h>
37
38#include <QtGui/qdrag.h>
39#include <QtGui/qevent.h>
40#include <QtGui/qstandarditemmodel.h>
41#include <QtWidgets/qtoolbutton.h>
42#include <QtGui/qpixmap.h>
43#include <QtWidgets/qaction.h>
44#include <QtWidgets/qheaderview.h>
45#include <QtWidgets/qtoolbar.h>
46#include <QtWidgets/qmenu.h>
47#include <QtGui/qevent.h>
48#include <QtCore/qset.h>
49#include <QtCore/qdebug.h>
50
51Q_DECLARE_METATYPE(QAction*)
52
53QT_BEGIN_NAMESPACE
54
55namespace {
56 enum { listModeIconSize = 16, iconModeIconSize = 24 };
57}
58
59static const char *actionMimeType = "action-repository/actions";
60static const char *plainTextMimeType = "text/plain";
61
62static inline QAction *actionOfItem(const QStandardItem* item)
63{
64 return qvariant_cast<QAction*>(v: item->data(role: qdesigner_internal::ActionModel::ActionRole));
65}
66
67namespace qdesigner_internal {
68
69// ----------- ActionModel
70ActionModel::ActionModel(QWidget *parent ) :
71 QStandardItemModel(parent),
72 m_emptyIcon(emptyIcon())
73{
74 QStringList headers;
75 headers += tr(s: "Name");
76 headers += tr(s: "Used");
77 headers += tr(s: "Text");
78 headers += tr(s: "Shortcut");
79 headers += tr(s: "Checkable");
80 headers += tr(s: "ToolTip");
81 Q_ASSERT(NumColumns == headers.size());
82 setHorizontalHeaderLabels(headers);
83}
84
85void ActionModel::clearActions()
86{
87 removeRows(row: 0, count: rowCount());
88}
89
90int ActionModel::findAction(QAction *action) const
91{
92 const int rows = rowCount();
93 for (int i = 0; i < rows; i++)
94 if (action == actionOfItem(item: item(row: i)))
95 return i;
96 return -1;
97}
98
99void ActionModel::update(int row)
100{
101 Q_ASSERT(m_core);
102 // need to create the row list ... grrr..
103 if (row >= rowCount())
104 return;
105
106 QStandardItemList list;
107 for (int i = 0; i < NumColumns; i++)
108 list += item(row, column: i);
109
110 setItems(core: m_core, a: actionOfItem(item: list.constFirst()), defaultIcon: m_emptyIcon, sl&: list);
111}
112
113void ActionModel::remove(int row)
114{
115 qDeleteAll(c: takeRow(row));
116}
117
118QModelIndex ActionModel::addAction(QAction *action)
119{
120 Q_ASSERT(m_core);
121 QStandardItemList items;
122 const Qt::ItemFlags flags = Qt::ItemIsSelectable|Qt::ItemIsDropEnabled|Qt::ItemIsDragEnabled|Qt::ItemIsEnabled;
123
124 QVariant itemData;
125 itemData.setValue(action);
126
127 for (int i = 0; i < NumColumns; i++) {
128 QStandardItem *item = new QStandardItem;
129 item->setData(value: itemData, role: ActionRole);
130 item->setFlags(flags);
131 items.push_back(t: item);
132 }
133 setItems(core: m_core, a: action, defaultIcon: m_emptyIcon, sl&: items);
134 appendRow(items);
135 return indexFromItem(item: items.constFirst());
136}
137
138// Find the associated menus and toolbars, ignore toolbuttons
139QWidgetList ActionModel::associatedWidgets(const QAction *action)
140{
141 QWidgetList rc = action->associatedWidgets();
142 for (QWidgetList::iterator it = rc.begin(); it != rc.end(); )
143 if (qobject_cast<const QMenu *>(object: *it) || qobject_cast<const QToolBar *>(object: *it)) {
144 ++it;
145 } else {
146 it = rc.erase(it);
147 }
148 return rc;
149}
150
151// shortcut is a fake property, need to retrieve it via property sheet.
152PropertySheetKeySequenceValue ActionModel::actionShortCut(QDesignerFormEditorInterface *core, QAction *action)
153{
154 QDesignerPropertySheetExtension *sheet = qt_extension<QDesignerPropertySheetExtension*>(manager: core->extensionManager(), object: action);
155 if (!sheet)
156 return PropertySheetKeySequenceValue();
157 return actionShortCut(ps: sheet);
158}
159
160PropertySheetKeySequenceValue ActionModel::actionShortCut(const QDesignerPropertySheetExtension *sheet)
161{
162 const int index = sheet->indexOf(QStringLiteral("shortcut"));
163 if (index == -1)
164 return PropertySheetKeySequenceValue();
165 return qvariant_cast<PropertySheetKeySequenceValue>(v: sheet->property(index));
166}
167
168void ActionModel::setItems(QDesignerFormEditorInterface *core, QAction *action,
169 const QIcon &defaultIcon,
170 QStandardItemList &sl)
171{
172
173 // Tooltip, mostly for icon view mode
174 QString firstTooltip = action->objectName();
175 const QString text = action->text();
176 if (!text.isEmpty()) {
177 firstTooltip += QLatin1Char('\n');
178 firstTooltip += text;
179 }
180
181 Q_ASSERT(sl.size() == NumColumns);
182
183 QStandardItem *item = sl[NameColumn];
184 item->setText(action->objectName());
185 QIcon icon = action->icon();
186 if (icon.isNull())
187 icon = defaultIcon;
188 item->setIcon(icon);
189 item->setToolTip(firstTooltip);
190 item->setWhatsThis(firstTooltip);
191 // Used
192 const QWidgetList associatedDesignerWidgets = associatedWidgets(action);
193 const bool used = !associatedDesignerWidgets.isEmpty();
194 item = sl[UsedColumn];
195 item->setCheckState(used ? Qt::Checked : Qt::Unchecked);
196 if (used) {
197 QString usedToolTip;
198 const QString separator = QStringLiteral(", ");
199 const int count = associatedDesignerWidgets.size();
200 for (int i = 0; i < count; i++) {
201 if (i)
202 usedToolTip += separator;
203 usedToolTip += associatedDesignerWidgets.at(i)->objectName();
204 }
205 item->setToolTip(usedToolTip);
206 } else {
207 item->setToolTip(QString());
208 }
209 // text
210 item = sl[TextColumn];
211 item->setText(action->text());
212 item->setToolTip(action->text());
213 // shortcut
214 const QString shortcut = actionShortCut(core, action).value().toString(format: QKeySequence::NativeText);
215 item = sl[ShortCutColumn];
216 item->setText(shortcut);
217 item->setToolTip(shortcut);
218 // checkable
219 sl[CheckedColumn]->setCheckState(action->isCheckable() ? Qt::Checked : Qt::Unchecked);
220 // ToolTip. This might be multi-line, rich text
221 QString toolTip = action->toolTip();
222 item = sl[ToolTipColumn];
223 item->setToolTip(toolTip);
224 item->setText(toolTip.replace(before: QLatin1Char('\n'), after: QLatin1Char(' ')));
225}
226
227QMimeData *ActionModel::mimeData(const QModelIndexList &indexes ) const
228{
229 ActionRepositoryMimeData::ActionList actionList;
230
231 QSet<QAction*> actions;
232 for (const QModelIndex &index : indexes)
233 if (QStandardItem *item = itemFromIndex(index))
234 if (QAction *action = actionOfItem(item))
235 actions.insert(value: action);
236 return new ActionRepositoryMimeData(actions.values(), Qt::CopyAction);
237}
238
239// Resource images are plain text. The drag needs to be restricted, however.
240QStringList ActionModel::mimeTypes() const
241{
242 return QStringList(QLatin1String(plainTextMimeType));
243}
244
245QString ActionModel::actionName(int row) const
246{
247 return item(row, column: NameColumn)->text();
248}
249
250bool ActionModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &)
251{
252 if (action != Qt::CopyAction)
253 return false;
254
255 QStandardItem *droppedItem = item(row, column);
256 if (!droppedItem)
257 return false;
258
259
260 QtResourceView::ResourceType type;
261 QString path;
262 if (!QtResourceView::decodeMimeData(md: data, t: &type, file: &path) || type != QtResourceView::ResourceImage)
263 return false;
264
265 emit resourceImageDropped(path, action: actionOfItem(item: droppedItem));
266 return true;
267}
268
269QAction *ActionModel::actionAt(const QModelIndex &index) const
270{
271 if (!index.isValid())
272 return nullptr;
273 QStandardItem *i = itemFromIndex(index);
274 if (!i)
275 return nullptr;
276 return actionOfItem(item: i);
277}
278
279// helpers
280
281static bool handleImageDragEnterMoveEvent(QDropEvent *event)
282{
283 QtResourceView::ResourceType type;
284 const bool rc = QtResourceView::decodeMimeData(md: event->mimeData(), t: &type) && type == QtResourceView::ResourceImage;
285 if (rc)
286 event->acceptProposedAction();
287 else
288 event->ignore();
289 return rc;
290}
291
292static void handleImageDropEvent(const QAbstractItemView *iv, QDropEvent *event, ActionModel *am)
293{
294 const QModelIndex index = iv->indexAt(point: event->pos());
295 if (!index.isValid()) {
296 event->ignore();
297 return;
298 }
299
300 if (!handleImageDragEnterMoveEvent(event))
301 return;
302
303 am->dropMimeData(data: event->mimeData(), action: event->proposedAction(), row: index.row(), column: 0, iv->rootIndex());
304}
305
306// Basically mimic QAbstractItemView's startDrag routine, except that
307// another pixmap is used, we don't want the whole row.
308
309void startActionDrag(QWidget *dragParent, ActionModel *model, const QModelIndexList &indexes, Qt::DropActions supportedActions)
310{
311 if (indexes.isEmpty())
312 return;
313
314 QDrag *drag = new QDrag(dragParent);
315 QMimeData *data = model->mimeData(indexes);
316 drag->setMimeData(data);
317 if (ActionRepositoryMimeData *actionMimeData = qobject_cast<ActionRepositoryMimeData *>(object: data))
318 drag->setPixmap(ActionRepositoryMimeData::actionDragPixmap(action: actionMimeData->actionList().constFirst()));
319
320 drag->exec(supportedActions);
321}
322
323// ---------------- ActionTreeView:
324ActionTreeView::ActionTreeView(ActionModel *model, QWidget *parent) :
325 QTreeView(parent),
326 m_model(model)
327{
328 setDragEnabled(true);
329 setAcceptDrops(true);
330 setDropIndicatorShown(true);
331 setDragDropMode(DragDrop);
332 setModel(model);
333 setRootIsDecorated(false);
334 setTextElideMode(Qt::ElideMiddle);
335
336 setModel(model);
337 connect(sender: this, signal: &QTreeView::activated, receiver: this, slot: &ActionTreeView::slotActivated);
338 connect(sender: header(), signal: &QHeaderView::sectionDoubleClicked,
339 receiver: this, slot: &QTreeView::resizeColumnToContents);
340
341 setIconSize(QSize(listModeIconSize, listModeIconSize));
342
343}
344
345QAction *ActionTreeView::currentAction() const
346{
347 return m_model->actionAt(index: currentIndex());
348}
349
350void ActionTreeView::filter(const QString &text)
351{
352 const int rowCount = m_model->rowCount();
353 const bool empty = text.isEmpty();
354 const QModelIndex parent = rootIndex();
355 for (int i = 0; i < rowCount; i++)
356 setRowHidden(row: i, parent, hide: !empty && !m_model->actionName(row: i).contains(s: text, cs: Qt::CaseInsensitive));
357}
358
359void ActionTreeView::dragEnterEvent(QDragEnterEvent *event)
360{
361 handleImageDragEnterMoveEvent(event);
362}
363
364void ActionTreeView::dragMoveEvent(QDragMoveEvent *event)
365{
366 handleImageDragEnterMoveEvent(event);
367}
368
369void ActionTreeView::dropEvent(QDropEvent *event)
370{
371 handleImageDropEvent(iv: this, event, am: m_model);
372}
373
374void ActionTreeView::focusInEvent(QFocusEvent *event)
375{
376 QTreeView::focusInEvent(event);
377 // Make property editor display current action
378 if (QAction *a = currentAction())
379 emit currentActionChanged(action: a);
380}
381
382void ActionTreeView::contextMenuEvent(QContextMenuEvent *event)
383{
384 emit actionContextMenuRequested(event, m_model->actionAt(index: indexAt(p: event->pos())));
385}
386
387void ActionTreeView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
388{
389 emit currentActionChanged(action: m_model->actionAt(index: current));
390 QTreeView::currentChanged(current, previous);
391}
392
393void ActionTreeView::slotActivated(const QModelIndex &index)
394{
395 emit actionActivated(action: m_model->actionAt(index), column: index.column());
396}
397
398void ActionTreeView::startDrag(Qt::DropActions supportedActions)
399{
400 startActionDrag(dragParent: this, model: m_model, indexes: selectedIndexes(), supportedActions);
401}
402
403// ---------------- ActionListView:
404ActionListView::ActionListView(ActionModel *model, QWidget *parent) :
405 QListView(parent),
406 m_model(model)
407{
408 setDragEnabled(true);
409 setAcceptDrops(true);
410 setDropIndicatorShown(true);
411 setDragDropMode(DragDrop);
412 setModel(model);
413 setTextElideMode(Qt::ElideMiddle);
414 connect(sender: this, signal: &QListView::activated, receiver: this, slot: &ActionListView::slotActivated);
415
416 // We actually want 'Static' as the user should be able to
417 // drag away actions only (not to rearrange icons).
418 // We emulate that by not accepting our own
419 // drag data. 'Static' causes the list view to disable drag and drop
420 // on the viewport.
421 setMovement(Snap);
422 setViewMode(IconMode);
423 setIconSize(QSize(iconModeIconSize, iconModeIconSize));
424 setGridSize(QSize(4 * iconModeIconSize, 2 * iconModeIconSize));
425 setSpacing(iconModeIconSize / 3);
426}
427
428QAction *ActionListView::currentAction() const
429{
430 return m_model->actionAt(index: currentIndex());
431}
432
433void ActionListView::filter(const QString &text)
434{
435 const int rowCount = m_model->rowCount();
436 const bool empty = text.isEmpty();
437 for (int i = 0; i < rowCount; i++)
438 setRowHidden(row: i, hide: !empty && !m_model->actionName(row: i).contains(s: text, cs: Qt::CaseInsensitive));
439}
440
441void ActionListView::dragEnterEvent(QDragEnterEvent *event)
442{
443 handleImageDragEnterMoveEvent(event);
444}
445
446void ActionListView::dragMoveEvent(QDragMoveEvent *event)
447{
448 handleImageDragEnterMoveEvent(event);
449}
450
451void ActionListView::dropEvent(QDropEvent *event)
452{
453 handleImageDropEvent(iv: this, event, am: m_model);
454}
455
456void ActionListView::focusInEvent(QFocusEvent *event)
457{
458 QListView::focusInEvent(event);
459 // Make property editor display current action
460 if (QAction *a = currentAction())
461 emit currentActionChanged(action: a);
462}
463
464void ActionListView::contextMenuEvent(QContextMenuEvent *event)
465{
466 emit actionContextMenuRequested(event, m_model->actionAt(index: indexAt(p: event->pos())));
467}
468
469void ActionListView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
470{
471 emit currentActionChanged(action: m_model->actionAt(index: current));
472 QListView::currentChanged(current, previous);
473}
474
475void ActionListView::slotActivated(const QModelIndex &index)
476{
477 emit actionActivated(action: m_model->actionAt(index));
478}
479
480void ActionListView::startDrag(Qt::DropActions supportedActions)
481{
482 startActionDrag(dragParent: this, model: m_model, indexes: selectedIndexes(), supportedActions);
483}
484
485// ActionView
486ActionView::ActionView(QWidget *parent) :
487 QStackedWidget(parent),
488 m_model(new ActionModel(this)),
489 m_actionTreeView(new ActionTreeView(m_model)),
490 m_actionListView(new ActionListView(m_model))
491{
492 addWidget(w: m_actionListView);
493 addWidget(w: m_actionTreeView);
494 // Wire signals
495 connect(sender: m_actionTreeView, signal: &ActionTreeView::actionContextMenuRequested,
496 receiver: this, slot: &ActionView::contextMenuRequested);
497 connect(sender: m_actionListView, signal: &ActionListView::actionContextMenuRequested,
498 receiver: this, slot: &ActionView::contextMenuRequested);
499
500 // make it possible for vs integration to reimplement edit action dialog
501 // [which it shouldn't do actually]
502 connect(sender: m_actionListView, signal: &ActionListView::actionActivated,
503 context: this, slot: [this](QAction *a) { this->activated(action: a, column: -1); });
504 connect(sender: m_actionTreeView, signal: &ActionTreeView::actionActivated, receiver: this, slot: &ActionView::activated);
505
506 connect(sender: m_actionListView, signal: &ActionListView::currentActionChanged,
507 receiver: this, slot: &ActionView::slotCurrentChanged);
508 connect(sender: m_actionTreeView, signal: &ActionTreeView::currentActionChanged,
509 receiver: this, slot: &ActionView::slotCurrentChanged);
510
511 connect(sender: m_model, signal: &ActionModel::resourceImageDropped,
512 receiver: this, slot: &ActionView::resourceImageDropped);
513
514 // sync selection models
515 QItemSelectionModel *selectionModel = m_actionTreeView->selectionModel();
516 m_actionListView->setSelectionModel(selectionModel);
517 connect(sender: selectionModel, signal: &QItemSelectionModel::selectionChanged,
518 receiver: this, slot: &ActionView::selectionChanged);
519}
520
521int ActionView::viewMode() const
522{
523 return currentWidget() == m_actionListView ? IconView : DetailedView;
524}
525
526void ActionView::setViewMode(int lm)
527{
528 if (viewMode() == lm)
529 return;
530
531 switch (lm) {
532 case IconView:
533 setCurrentWidget(m_actionListView);
534 break;
535 case DetailedView:
536 setCurrentWidget(m_actionTreeView);
537 break;
538 default:
539 break;
540 }
541}
542
543void ActionView::slotCurrentChanged(QAction *action)
544{
545 // emit only for currently visible
546 if (sender() == currentWidget())
547 emit currentChanged(action);
548}
549
550void ActionView::filter(const QString &text)
551{
552 m_actionTreeView->filter(text);
553 m_actionListView->filter(text);
554}
555
556void ActionView::selectAll()
557{
558 m_actionTreeView->selectAll();
559}
560
561void ActionView::clearSelection()
562{
563 m_actionTreeView->selectionModel()->clearSelection();
564}
565
566void ActionView::setCurrentIndex(const QModelIndex &index)
567{
568 m_actionTreeView->setCurrentIndex(index);
569}
570
571QAction *ActionView::currentAction() const
572{
573 return m_actionListView->currentAction();
574}
575
576void ActionView::setSelectionMode(QAbstractItemView::SelectionMode sm)
577{
578 m_actionTreeView->setSelectionMode(sm);
579 m_actionListView->setSelectionMode(sm);
580}
581
582QAbstractItemView::SelectionMode ActionView::selectionMode() const
583{
584 return m_actionListView->selectionMode();
585}
586
587QItemSelection ActionView::selection() const
588{
589 return m_actionListView->selectionModel()->selection();
590}
591
592ActionView::ActionList ActionView::selectedActions() const
593{
594 ActionList rc;
595 const QModelIndexList &indexes = selection().indexes();
596 for (const QModelIndex &index : indexes) {
597 if (index.column() == 0)
598 rc += actionOfItem(item: m_model->itemFromIndex(index));
599 }
600 return rc;
601}
602// ---------- ActionRepositoryMimeData
603ActionRepositoryMimeData::ActionRepositoryMimeData(QAction *a, Qt::DropAction dropAction) :
604 m_dropAction(dropAction)
605{
606 m_actionList += a;
607}
608
609ActionRepositoryMimeData::ActionRepositoryMimeData(const ActionList &al, Qt::DropAction dropAction) :
610 m_dropAction(dropAction),
611 m_actionList(al)
612{
613}
614
615QStringList ActionRepositoryMimeData::formats() const
616{
617 return QStringList(QLatin1String(actionMimeType));
618}
619
620QPixmap ActionRepositoryMimeData::actionDragPixmap(const QAction *action)
621{
622
623 // Try to find a suitable pixmap. Grab either widget or icon.
624 const QIcon icon = action->icon();
625 if (!icon.isNull())
626 return icon.pixmap(size: QSize(22, 22));
627
628 const QWidgetList &associatedWidgets = action->associatedWidgets();
629 for (QWidget *w : associatedWidgets) {
630 if (QToolButton *tb = qobject_cast<QToolButton *>(object: w))
631 return tb->grab(rectangle: QRect(0, 0, -1, -1));
632 }
633
634 // Create a QToolButton
635 QToolButton *tb = new QToolButton;
636 tb->setText(action->text());
637 tb->setToolButtonStyle(Qt::ToolButtonTextOnly);
638 tb->adjustSize();
639 const QPixmap rc = tb->grab(rectangle: QRect(0, 0, -1, -1));
640 tb->deleteLater();
641 return rc;
642}
643
644void ActionRepositoryMimeData::accept(QDragMoveEvent *event) const
645{
646 if (event->proposedAction() == m_dropAction) {
647 event->acceptProposedAction();
648 } else {
649 event->setDropAction(m_dropAction);
650 event->accept();
651 }
652}
653
654} // namespace qdesigner_internal
655
656QT_END_NAMESPACE
657

source code of qttools/src/designer/src/lib/shared/actionrepository.cpp