// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "actionrepository_p.h" #include "qtresourceview_p.h" #include "iconloader_p.h" #include "qdesigner_utils_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; namespace { enum { listModeIconSize = 16, iconModeIconSize = 24 }; } static const char *actionMimeType = "action-repository/actions"; static const char *plainTextMimeType = "text/plain"; static inline QAction *actionOfItem(const QStandardItem* item) { return qvariant_cast(item->data(qdesigner_internal::ActionModel::ActionRole)); } namespace qdesigner_internal { // ----------- ActionModel ActionModel::ActionModel(QWidget *parent ) : QStandardItemModel(parent), m_emptyIcon(emptyIcon()) { QStringList headers; headers += tr("Name"); headers += tr("Used"); headers += tr("Text"); headers += tr("Shortcut"); headers += tr("Checkable"); headers += tr("ToolTip"); headers += tr("MenuRole"); Q_ASSERT(NumColumns == headers.size()); setHorizontalHeaderLabels(headers); } void ActionModel::clearActions() { removeRows(0, rowCount()); } int ActionModel::findAction(QAction *action) const { const int rows = rowCount(); for (int i = 0; i < rows; i++) if (action == actionOfItem(item(i))) return i; return -1; } void ActionModel::update(int row) { Q_ASSERT(m_core); // need to create the row list ... grrr.. if (row >= rowCount()) return; QStandardItemList list; for (int i = 0; i < NumColumns; i++) list += item(row, i); setItems(m_core, actionOfItem(list.constFirst()), m_emptyIcon, list); } void ActionModel::remove(int row) { qDeleteAll(takeRow(row)); } QModelIndex ActionModel::addAction(QAction *action) { Q_ASSERT(m_core); QStandardItemList items; const Qt::ItemFlags flags = Qt::ItemIsSelectable|Qt::ItemIsDropEnabled|Qt::ItemIsDragEnabled|Qt::ItemIsEnabled; QVariant itemData; itemData.setValue(action); for (int i = 0; i < NumColumns; i++) { QStandardItem *item = new QStandardItem; item->setData(itemData, ActionRole); item->setFlags(flags); items.push_back(item); } setItems(m_core, action, m_emptyIcon, items); appendRow(items); return indexFromItem(items.constFirst()); } // Find the associated menus and toolbars, ignore toolbuttons QWidgetList ActionModel::associatedWidgets(const QAction *action) { const QObjectList rc = action->associatedObjects(); QWidgetList result; result.reserve(rc.size()); for (QObject *obj : rc) { if (QWidget *w = qobject_cast(obj)) { if (qobject_cast(w) || qobject_cast(w)) result.push_back(w); } } return result; } // shortcut is a fake property, need to retrieve it via property sheet. PropertySheetKeySequenceValue ActionModel::actionShortCut(QDesignerFormEditorInterface *core, QAction *action) { QDesignerPropertySheetExtension *sheet = qt_extension(core->extensionManager(), action); if (!sheet) return PropertySheetKeySequenceValue(); return actionShortCut(sheet); } PropertySheetKeySequenceValue ActionModel::actionShortCut(const QDesignerPropertySheetExtension *sheet) { const int index = sheet->indexOf(u"shortcut"_s); if (index == -1) return PropertySheetKeySequenceValue(); return qvariant_cast(sheet->property(index)); } void ActionModel::setItems(QDesignerFormEditorInterface *core, QAction *action, const QIcon &defaultIcon, QStandardItemList &sl) { // Tooltip, mostly for icon view mode QString firstTooltip = action->objectName(); const QString text = action->text(); if (!text.isEmpty()) firstTooltip += u'\n' + text; Q_ASSERT(sl.size() == NumColumns); QStandardItem *item = sl[NameColumn]; item->setText(action->objectName()); QIcon icon = action->icon(); if (icon.isNull()) icon = defaultIcon; item->setIcon(icon); item->setToolTip(firstTooltip); item->setWhatsThis(firstTooltip); // Used const QWidgetList associatedDesignerWidgets = associatedWidgets(action); const bool used = !associatedDesignerWidgets.isEmpty(); item = sl[UsedColumn]; item->setCheckState(used ? Qt::Checked : Qt::Unchecked); if (used) { QString usedToolTip; const auto separator = ", "_L1; const int count = associatedDesignerWidgets.size(); for (int i = 0; i < count; i++) { if (i) usedToolTip += separator; usedToolTip += associatedDesignerWidgets.at(i)->objectName(); } item->setToolTip(usedToolTip); } else { item->setToolTip(QString()); } // text item = sl[TextColumn]; item->setText(action->text()); item->setToolTip(action->text()); // shortcut const QString shortcut = actionShortCut(core, action).value().toString(QKeySequence::NativeText); item = sl[ShortCutColumn]; item->setText(shortcut); item->setToolTip(shortcut); // checkable sl[CheckedColumn]->setCheckState(action->isCheckable() ? Qt::Checked : Qt::Unchecked); // ToolTip. This might be multi-line, rich text QString toolTip = action->toolTip(); item = sl[ToolTipColumn]; item->setToolTip(toolTip); item->setText(toolTip.replace(u'\n', u' ')); // menuRole const auto menuRole = action->menuRole(); item = sl[MenuRoleColumn]; item->setText(QLatin1StringView(QMetaEnum::fromType().valueToKey(menuRole))); } QMimeData *ActionModel::mimeData(const QModelIndexList &indexes ) const { ActionRepositoryMimeData::ActionList actionList; QSet actions; for (const QModelIndex &index : indexes) if (QStandardItem *item = itemFromIndex(index)) if (QAction *action = actionOfItem(item)) actions.insert(action); return new ActionRepositoryMimeData(actions.values(), Qt::CopyAction); } // Resource images are plain text. The drag needs to be restricted, however. QStringList ActionModel::mimeTypes() const { return QStringList(QLatin1String(plainTextMimeType)); } QString ActionModel::actionName(int row) const { return item(row, NameColumn)->text(); } bool ActionModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &) { if (action != Qt::CopyAction) return false; QStandardItem *droppedItem = item(row, column); if (!droppedItem) return false; QtResourceView::ResourceType type; QString path; if (!QtResourceView::decodeMimeData(data, &type, &path) || type != QtResourceView::ResourceImage) return false; emit resourceImageDropped(path, actionOfItem(droppedItem)); return true; } QAction *ActionModel::actionAt(const QModelIndex &index) const { if (!index.isValid()) return nullptr; QStandardItem *i = itemFromIndex(index); if (!i) return nullptr; return actionOfItem(i); } QModelIndex ActionModel::indexOf(QAction *a) const { for (int r = rowCount() - 1; r >= 0; --r) { QStandardItem *stdItem = item(r, 0); if (actionOfItem(stdItem) == a) return indexFromItem(stdItem); } return {}; } // helpers static bool handleImageDragEnterMoveEvent(QDropEvent *event) { QtResourceView::ResourceType type; const bool rc = QtResourceView::decodeMimeData(event->mimeData(), &type) && type == QtResourceView::ResourceImage; if (rc) event->acceptProposedAction(); else event->ignore(); return rc; } static void handleImageDropEvent(const QAbstractItemView *iv, QDropEvent *event, ActionModel *am) { const QModelIndex index = iv->indexAt(event->position().toPoint()); if (!index.isValid()) { event->ignore(); return; } if (!handleImageDragEnterMoveEvent(event)) return; am->dropMimeData(event->mimeData(), event->proposedAction(), index.row(), 0, iv->rootIndex()); } // Basically mimic QAbstractItemView's startDrag routine, except that // another pixmap is used, we don't want the whole row. void startActionDrag(QWidget *dragParent, ActionModel *model, const QModelIndexList &indexes, Qt::DropActions supportedActions) { if (indexes.isEmpty()) return; QDrag *drag = new QDrag(dragParent); QMimeData *data = model->mimeData(indexes); drag->setMimeData(data); if (ActionRepositoryMimeData *actionMimeData = qobject_cast(data)) drag->setPixmap(ActionRepositoryMimeData::actionDragPixmap(actionMimeData->actionList().constFirst())); drag->exec(supportedActions); } // ---------------- ActionTreeView: ActionTreeView::ActionTreeView(ActionModel *model, QWidget *parent) : QTreeView(parent), m_model(model) { setDragEnabled(true); setAcceptDrops(true); setDropIndicatorShown(true); setDragDropMode(DragDrop); setModel(model); setRootIsDecorated(false); setTextElideMode(Qt::ElideMiddle); setModel(model); connect(this, &QTreeView::activated, this, &ActionTreeView::slotActivated); connect(header(), &QHeaderView::sectionDoubleClicked, this, &QTreeView::resizeColumnToContents); setIconSize(QSize(listModeIconSize, listModeIconSize)); } QAction *ActionTreeView::currentAction() const { return m_model->actionAt(currentIndex()); } void ActionTreeView::filter(const QString &text) { const int rowCount = m_model->rowCount(); const bool empty = text.isEmpty(); const QModelIndex parent = rootIndex(); for (int i = 0; i < rowCount; i++) setRowHidden(i, parent, !empty && !m_model->actionName(i).contains(text, Qt::CaseInsensitive)); } void ActionTreeView::dragEnterEvent(QDragEnterEvent *event) { handleImageDragEnterMoveEvent(event); } void ActionTreeView::dragMoveEvent(QDragMoveEvent *event) { handleImageDragEnterMoveEvent(event); } void ActionTreeView::dropEvent(QDropEvent *event) { handleImageDropEvent(this, event, m_model); } void ActionTreeView::focusInEvent(QFocusEvent *event) { QTreeView::focusInEvent(event); // Make property editor display current action if (QAction *a = currentAction()) emit currentActionChanged(a); } void ActionTreeView::contextMenuEvent(QContextMenuEvent *event) { emit actionContextMenuRequested(event, m_model->actionAt(indexAt(event->pos()))); } void ActionTreeView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) { emit currentActionChanged(m_model->actionAt(current)); QTreeView::currentChanged(current, previous); } void ActionTreeView::slotActivated(const QModelIndex &index) { emit actionActivated(m_model->actionAt(index), index.column()); } void ActionTreeView::startDrag(Qt::DropActions supportedActions) { startActionDrag(this, m_model, selectedIndexes(), supportedActions); } // ---------------- ActionListView: ActionListView::ActionListView(ActionModel *model, QWidget *parent) : QListView(parent), m_model(model) { setDragEnabled(true); setAcceptDrops(true); setDropIndicatorShown(true); setDragDropMode(DragDrop); setModel(model); setTextElideMode(Qt::ElideMiddle); connect(this, &QListView::activated, this, &ActionListView::slotActivated); // We actually want 'Static' as the user should be able to // drag away actions only (not to rearrange icons). // We emulate that by not accepting our own // drag data. 'Static' causes the list view to disable drag and drop // on the viewport. setMovement(Snap); setViewMode(IconMode); setIconSize(QSize(iconModeIconSize, iconModeIconSize)); setGridSize(QSize(4 * iconModeIconSize, 2 * iconModeIconSize)); setSpacing(iconModeIconSize / 3); } QAction *ActionListView::currentAction() const { return m_model->actionAt(currentIndex()); } void ActionListView::filter(const QString &text) { const int rowCount = m_model->rowCount(); const bool empty = text.isEmpty(); for (int i = 0; i < rowCount; i++) setRowHidden(i, !empty && !m_model->actionName(i).contains(text, Qt::CaseInsensitive)); } void ActionListView::dragEnterEvent(QDragEnterEvent *event) { handleImageDragEnterMoveEvent(event); } void ActionListView::dragMoveEvent(QDragMoveEvent *event) { handleImageDragEnterMoveEvent(event); } void ActionListView::dropEvent(QDropEvent *event) { handleImageDropEvent(this, event, m_model); } void ActionListView::focusInEvent(QFocusEvent *event) { QListView::focusInEvent(event); // Make property editor display current action if (QAction *a = currentAction()) emit currentActionChanged(a); } void ActionListView::contextMenuEvent(QContextMenuEvent *event) { emit actionContextMenuRequested(event, m_model->actionAt(indexAt(event->pos()))); } void ActionListView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) { emit currentActionChanged(m_model->actionAt(current)); QListView::currentChanged(current, previous); } void ActionListView::slotActivated(const QModelIndex &index) { emit actionActivated(m_model->actionAt(index)); } void ActionListView::startDrag(Qt::DropActions supportedActions) { startActionDrag(this, m_model, selectedIndexes(), supportedActions); } // ActionView ActionView::ActionView(QWidget *parent) : QStackedWidget(parent), m_model(new ActionModel(this)), m_actionTreeView(new ActionTreeView(m_model)), m_actionListView(new ActionListView(m_model)) { addWidget(m_actionListView); addWidget(m_actionTreeView); // Wire signals connect(m_actionTreeView, &ActionTreeView::actionContextMenuRequested, this, &ActionView::contextMenuRequested); connect(m_actionListView, &ActionListView::actionContextMenuRequested, this, &ActionView::contextMenuRequested); // make it possible for vs integration to reimplement edit action dialog // [which it shouldn't do actually] connect(m_actionListView, &ActionListView::actionActivated, this, [this](QAction *a) { this->activated(a, -1); }); connect(m_actionTreeView, &ActionTreeView::actionActivated, this, &ActionView::activated); connect(m_actionListView, &ActionListView::currentActionChanged, this, &ActionView::slotCurrentChanged); connect(m_actionTreeView, &ActionTreeView::currentActionChanged, this, &ActionView::slotCurrentChanged); connect(m_model, &ActionModel::resourceImageDropped, this, &ActionView::resourceImageDropped); // sync selection models QItemSelectionModel *selectionModel = m_actionTreeView->selectionModel(); m_actionListView->setSelectionModel(selectionModel); connect(selectionModel, &QItemSelectionModel::selectionChanged, this, &ActionView::selectionChanged); } int ActionView::viewMode() const { return currentWidget() == m_actionListView ? IconView : DetailedView; } void ActionView::setViewMode(int lm) { if (viewMode() == lm) return; switch (lm) { case IconView: setCurrentWidget(m_actionListView); break; case DetailedView: setCurrentWidget(m_actionTreeView); break; default: break; } } void ActionView::slotCurrentChanged(QAction *action) { // emit only for currently visible if (sender() == currentWidget()) emit currentChanged(action); } void ActionView::filter(const QString &text) { m_actionTreeView->filter(text); m_actionListView->filter(text); } void ActionView::selectAll() { m_actionTreeView->selectAll(); } void ActionView::clearSelection() { m_actionTreeView->selectionModel()->clearSelection(); } void ActionView::selectAction(QAction *a) { const QModelIndex index = m_model->indexOf(a); if (index.isValid()) setCurrentIndex(index); } void ActionView::setCurrentIndex(const QModelIndex &index) { m_actionTreeView->setCurrentIndex(index); } QAction *ActionView::currentAction() const { return m_actionListView->currentAction(); } void ActionView::setSelectionMode(QAbstractItemView::SelectionMode sm) { m_actionTreeView->setSelectionMode(sm); m_actionListView->setSelectionMode(sm); } QAbstractItemView::SelectionMode ActionView::selectionMode() const { return m_actionListView->selectionMode(); } QItemSelection ActionView::selection() const { return m_actionListView->selectionModel()->selection(); } ActionView::ActionList ActionView::selectedActions() const { ActionList rc; const QModelIndexList &indexes = selection().indexes(); for (const QModelIndex &index : indexes) { if (index.column() == 0) rc += actionOfItem(m_model->itemFromIndex(index)); } return rc; } // ---------- ActionRepositoryMimeData ActionRepositoryMimeData::ActionRepositoryMimeData(QAction *a, Qt::DropAction dropAction) : m_dropAction(dropAction) { m_actionList += a; } ActionRepositoryMimeData::ActionRepositoryMimeData(const ActionList &al, Qt::DropAction dropAction) : m_dropAction(dropAction), m_actionList(al) { } QStringList ActionRepositoryMimeData::formats() const { return QStringList(QLatin1String(actionMimeType)); } QPixmap ActionRepositoryMimeData::actionDragPixmap(const QAction *action) { // Try to find a suitable pixmap. Grab either widget or icon. const QIcon icon = action->icon(); if (!icon.isNull()) return icon.pixmap(QSize(22, 22)); const QObjectList associatedObjects = action->associatedObjects(); for (QObject *o : associatedObjects) { if (QToolButton *tb = qobject_cast(o)) return tb->grab(QRect(0, 0, -1, -1)); } // Create a QToolButton QToolButton *tb = new QToolButton; tb->setText(action->text()); tb->setToolButtonStyle(Qt::ToolButtonTextOnly); tb->adjustSize(); const QPixmap rc = tb->grab(QRect(0, 0, -1, -1)); tb->deleteLater(); return rc; } void ActionRepositoryMimeData::accept(QDragMoveEvent *event) const { if (event->proposedAction() == m_dropAction) { event->acceptProposedAction(); } else { event->setDropAction(m_dropAction); event->accept(); } } } // namespace qdesigner_internal QT_END_NAMESPACE