/**************************************************************************** | |
** | |
** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). | |
** All rights reserved. | |
** Contact: Nokia Corporation (qt-info@nokia.com) | |
** | |
** This file is part of the Qt Designer of the Qt Toolkit. | |
** | |
** $QT_BEGIN_LICENSE:LGPL$ | |
** GNU Lesser General Public License Usage | |
** This file may be used under the terms of the GNU Lesser General Public | |
** License version 2.1 as published by the Free Software Foundation and | |
** appearing in the file LICENSE.LGPL included in the packaging of this | |
** file. Please review the following information to ensure the GNU Lesser | |
** General Public License version 2.1 requirements will be met: | |
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. | |
** | |
** In addition, as a special exception, Nokia gives you certain additional | |
** rights. These rights are described in the Nokia Qt LGPL Exception | |
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. | |
** | |
** GNU General Public License Usage | |
** Alternatively, this file may be used under the terms of the GNU General | |
** Public License version 3.0 as published by the Free Software Foundation | |
** and appearing in the file LICENSE.GPL included in the packaging of this | |
** file. Please review the following information to ensure the GNU General | |
** Public License version 3.0 requirements will be met: | |
** http://www.gnu.org/copyleft/gpl.html. | |
** | |
** Other Usage | |
** Alternatively, this file may be used in accordance with the terms and | |
** conditions contained in a signed written agreement between you and Nokia. | |
** | |
** | |
** | |
** | |
** | |
** $QT_END_LICENSE$ | |
** | |
****************************************************************************/ | |
#include "actionrepository_p.h" | |
#include "qtresourceview_p.h" | |
#include "iconloader_p.h" | |
#include "qdesigner_utils_p.h" | |
#include <QtDesigner/QDesignerFormEditorInterface> | |
#include <QtDesigner/QDesignerPropertySheetExtension> | |
#include <QtDesigner/QExtensionManager> | |
#include <QtGui/QDrag> | |
#include <QtGui/QContextMenuEvent> | |
#include <QtGui/QStandardItemModel> | |
#include <QtGui/QToolButton> | |
#include <QtGui/QPixmap> | |
#include <QtGui/QAction> | |
#include <QtGui/QHeaderView> | |
#include <QtGui/QToolBar> | |
#include <QtGui/QMenu> | |
#include <QtGui/qevent.h> | |
#include <QtCore/QSet> | |
#include <QtCore/QDebug> | |
Q_DECLARE_METATYPE(QAction*) | |
QT_BEGIN_NAMESPACE | |
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<QAction*>(item->data(qdesigner_internal::ActionModel::ActionRole)); | |
} | |
namespace qdesigner_internal { | |
// ----------- ActionModel | |
ActionModel::ActionModel(QWidget *parent ) : | |
QStandardItemModel(parent), | |
m_emptyIcon(emptyIcon()), | |
m_core(0) | |
{ | |
QStringList headers; | |
headers += tr("Name"); | |
headers += tr("Used"); | |
headers += tr("Text"); | |
headers += tr("Shortcut"); | |
headers += tr("Checkable"); | |
headers += tr("ToolTip"); | |
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.front()), 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; | |
qVariantSetValue(itemData, 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.front()); | |
} | |
// Find the associated menus and toolbars, ignore toolbuttons | |
QWidgetList ActionModel::associatedWidgets(const QAction *action) | |
{ | |
QWidgetList rc = action->associatedWidgets(); | |
for (QWidgetList::iterator it = rc.begin(); it != rc.end(); ) | |
if (qobject_cast<const QMenu *>(*it) || qobject_cast<const QToolBar *>(*it)) { | |
++it; | |
} else { | |
it = rc.erase(it); | |
} | |
return rc; | |
} | |
// shortcut is a fake property, need to retrieve it via property sheet. | |
PropertySheetKeySequenceValue ActionModel::actionShortCut(QDesignerFormEditorInterface *core, QAction *action) | |
{ | |
QDesignerPropertySheetExtension *sheet = qt_extension<QDesignerPropertySheetExtension*>(core->extensionManager(), action); | |
if (!sheet) | |
return PropertySheetKeySequenceValue(); | |
return actionShortCut(sheet); | |
} | |
PropertySheetKeySequenceValue ActionModel::actionShortCut(const QDesignerPropertySheetExtension *sheet) | |
{ | |
const int index = sheet->indexOf(QLatin1String("shortcut")); | |
if (index == -1) | |
return PropertySheetKeySequenceValue(); | |
return qvariant_cast<PropertySheetKeySequenceValue>(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 += QLatin1Char('\n'); | |
firstTooltip += 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.empty(); | |
item = sl[UsedColumn]; | |
item->setCheckState(used ? Qt::Checked : Qt::Unchecked); | |
if (used) { | |
QString usedToolTip; | |
const QString separator = QLatin1String(", "); | |
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(QLatin1Char('\n'), QLatin1Char(' '))); | |
} | |
QMimeData *ActionModel::mimeData(const QModelIndexList &indexes ) const | |
{ | |
ActionRepositoryMimeData::ActionList actionList; | |
QSet<QAction*> actions; | |
foreach (const QModelIndex &index, indexes) | |
if (QStandardItem *item = itemFromIndex(index)) | |
if (QAction *action = actionOfItem(item)) | |
actions.insert(action); | |
return new ActionRepositoryMimeData(actions.toList(), 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 0; | |
QStandardItem *i = itemFromIndex(index); | |
if (!i) | |
return 0; | |
return actionOfItem(i); | |
} | |
// 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->pos()); | |
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.empty()) | |
return; | |
QDrag *drag = new QDrag(dragParent); | |
QMimeData *data = model->mimeData(indexes); | |
drag->setMimeData(data); | |
if (ActionRepositoryMimeData *actionMimeData = qobject_cast<ActionRepositoryMimeData *>(data)) | |
drag->setPixmap(ActionRepositoryMimeData::actionDragPixmap(actionMimeData->actionList().front())); | |
drag->start(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, SIGNAL(activated(QModelIndex)), this, SLOT(slotActivated(QModelIndex))); | |
connect(header(), SIGNAL(sectionDoubleClicked(int)), this, SLOT(resizeColumnToContents(int))); | |
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 currentChanged(a); | |
} | |
void ActionTreeView::contextMenuEvent(QContextMenuEvent *event) | |
{ | |
emit contextMenuRequested(event, m_model->actionAt(indexAt(event->pos()))); | |
} | |
void ActionTreeView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) | |
{ | |
emit currentChanged(m_model->actionAt(current)); | |
QTreeView::currentChanged(current, previous); | |
} | |
void ActionTreeView::slotActivated(const QModelIndex &index) | |
{ | |
emit activated(m_model->actionAt(index)); | |
} | |
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, SIGNAL(activated(QModelIndex)), this, SLOT(slotActivated(QModelIndex))); | |
// 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 currentChanged(a); | |
} | |
void ActionListView::contextMenuEvent(QContextMenuEvent *event) | |
{ | |
emit contextMenuRequested(event, m_model->actionAt(indexAt(event->pos()))); | |
} | |
void ActionListView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) | |
{ | |
emit currentChanged(m_model->actionAt(current)); | |
QListView::currentChanged(current, previous); | |
} | |
void ActionListView::slotActivated(const QModelIndex &index) | |
{ | |
emit activated(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, SIGNAL(contextMenuRequested(QContextMenuEvent*,QAction*)), | |
this, SIGNAL(contextMenuRequested(QContextMenuEvent*,QAction*))); | |
connect(m_actionListView, SIGNAL(contextMenuRequested(QContextMenuEvent*,QAction*)), | |
this, SIGNAL(contextMenuRequested(QContextMenuEvent*,QAction*))); | |
// make it possible for vs integration to reimplement edit action dialog | |
// [which it shouldn't do actually] | |
connect(m_actionListView, SIGNAL(activated(QAction*)), this, SIGNAL(activated(QAction*))); | |
connect(m_actionTreeView, SIGNAL(activated(QAction*)), this, SIGNAL(activated(QAction*))); | |
connect(m_actionListView, SIGNAL(currentChanged(QAction*)),this, SLOT(slotCurrentChanged(QAction*))); | |
connect(m_actionTreeView, SIGNAL(currentChanged(QAction*)),this, SLOT(slotCurrentChanged(QAction*))); | |
connect(m_model, SIGNAL(resourceImageDropped(QString,QAction*)), | |
this, SIGNAL(resourceImageDropped(QString,QAction*))); | |
// sync selection models | |
QItemSelectionModel *selectionModel = m_actionTreeView->selectionModel(); | |
m_actionListView->setSelectionModel(selectionModel); | |
connect(selectionModel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)), | |
this, SIGNAL(selectionChanged(QItemSelection,QItemSelection))); | |
} | |
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::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; | |
foreach (const QModelIndex &index, selection().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)); | |
foreach (QWidget *w, action->associatedWidgets()) | |
if (QToolButton *tb = qobject_cast<QToolButton *>(w)) | |
return QPixmap::grabWidget(tb); | |
// Create a QToolButton | |
QToolButton *tb = new QToolButton; | |
tb->setText(action->text()); | |
tb->setToolButtonStyle(Qt::ToolButtonTextOnly); | |
#ifdef Q_WS_WIN // Force alien off to make adjustSize() take the system minimumsize into account. | |
tb->createWinId(); | |
#endif | |
tb->adjustSize(); | |
const QPixmap rc = QPixmap::grabWidget(tb); | |
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 |