diff options
author | Lars Knoll <lars.knoll@nokia.com> | 2009-03-23 09:34:13 (GMT) |
---|---|---|
committer | Simon Hausmann <simon.hausmann@nokia.com> | 2009-03-23 09:34:13 (GMT) |
commit | 67ad0519fd165acee4a4d2a94fa502e9e4847bd0 (patch) | |
tree | 1dbf50b3dff8d5ca7e9344733968c72704eb15ff /tools/designer/src/lib/shared/morphmenu.cpp | |
download | Qt-67ad0519fd165acee4a4d2a94fa502e9e4847bd0.zip Qt-67ad0519fd165acee4a4d2a94fa502e9e4847bd0.tar.gz Qt-67ad0519fd165acee4a4d2a94fa502e9e4847bd0.tar.bz2 |
Long live Qt!
Diffstat (limited to 'tools/designer/src/lib/shared/morphmenu.cpp')
-rw-r--r-- | tools/designer/src/lib/shared/morphmenu.cpp | 635 |
1 files changed, 635 insertions, 0 deletions
diff --git a/tools/designer/src/lib/shared/morphmenu.cpp b/tools/designer/src/lib/shared/morphmenu.cpp new file mode 100644 index 0000000..0d41af0 --- /dev/null +++ b/tools/designer/src/lib/shared/morphmenu.cpp @@ -0,0 +1,635 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Designer of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "morphmenu_p.h" +#include "formwindowbase_p.h" +#include "widgetfactory_p.h" +#include "qdesigner_formwindowcommand_p.h" +#include "qlayout_widget_p.h" +#include "layoutinfo_p.h" +#include "qdesigner_propertycommand_p.h" + +#include <QtDesigner/QExtensionManager> +#include <QtDesigner/QDesignerContainerExtension> +#include <QtDesigner/QDesignerFormWindowInterface> +#include <QtDesigner/QDesignerFormEditorInterface> +#include <QtDesigner/QDesignerLanguageExtension> +#include <QtDesigner/QDesignerWidgetDataBaseInterface> +#include <QtDesigner/QDesignerMetaDataBaseInterface> +#include <QtDesigner/QDesignerPropertySheetExtension> + +#include <QtGui/QWidget> +#include <QtGui/QAction> +#include <QtGui/QMenu> +#include <QtGui/QApplication> +#include <QtGui/QLayout> +#include <QtGui/QUndoStack> + +#include <QtGui/QFrame> +#include <QtGui/QGroupBox> +#include <QtGui/QTabWidget> +#include <QtGui/QStackedWidget> +#include <QtGui/QToolBox> +#include <QtGui/QAbstractItemView> +#include <QtGui/QAbstractButton> +#include <QtGui/QAbstractSpinBox> +#include <QtGui/QTextEdit> +#include <QtGui/QPlainTextEdit> +#include <QtGui/QLabel> + +#include <QtCore/QStringList> +#include <QtCore/QMap> +#include <QtCore/QVariant> +#include <QtCore/QSignalMapper> +#include <QtCore/QDebug> + +Q_DECLARE_METATYPE(QWidgetList) + +QT_BEGIN_NAMESPACE + +// Helpers for the dynamic properties that store Z/Widget order +static const char *widgetOrderPropertyC = "_q_widgetOrder"; +static const char *zOrderPropertyC = "_q_zOrder"; + +/* Morphing in Designer: + * It is possible to morph: + * - Non-Containers into similar widgets by category + * - Simple page containers into similar widgets or page-based containers with + * a single page (in theory also into a QLayoutWidget, but this might + * not always be appropriate). + * - Page-based containers into page-based containers or simple containers if + * they have just one page + * [Page based containers meaning here having a container extension] + * Morphing types are restricted to the basic Qt types. Morphing custom + * widgets is considered risky since they might have unmanaged layouts + * or the like. + * + * Requirements: + * - The widget must be on a non-laid out parent or in a layout managed + * by Designer + * - Its child widgets must be non-laid out or in a layout managed + * by Designer + * Note that child widgets can be + * - On the widget itself in the case of simple containers + * - On several pages in the case of page-based containers + * This is what is called 'childContainers' in the code (the widget itself + * or the list of container extension pages). + * + * The Morphing process encompasses: + * - Create a target widget and apply properties as far as applicable + * If the target widget has a container extension, add a sufficient + * number of pages. + * - Transferring the child widgets over to the new childContainers. + * In the case of a managed layout on a childContainer, this is simply + * set on the target childContainer, which is a new Qt 4.5 + * functionality. + * - Replace the widget itself in the parent layout + */ + +namespace qdesigner_internal { + +enum MorphCategory { + MorphCategoryNone, MorphSimpleContainer, MorphPageContainer, MorphItemView, + MorphButton, MorphSpinBox, MorphTextEdit +}; + +// Determine category of a widget +static MorphCategory category(const QWidget *w) +{ + // Simple containers: Exact match + const QMetaObject *mo = w->metaObject(); + if (mo == &QWidget::staticMetaObject || mo == &QFrame::staticMetaObject || mo == &QGroupBox::staticMetaObject || mo == &QLayoutWidget::staticMetaObject) + return MorphSimpleContainer; + if (mo == &QTabWidget::staticMetaObject || mo == &QStackedWidget::staticMetaObject || mo == &QToolBox::staticMetaObject) + return MorphPageContainer; + if (qobject_cast<const QAbstractItemView*>(w)) + return MorphItemView; + if (qobject_cast<const QAbstractButton *>(w)) + return MorphButton; + if (qobject_cast<const QAbstractSpinBox *>(w)) + return MorphSpinBox; + if (qobject_cast<const QPlainTextEdit *>(w) || qobject_cast<const QTextEdit*>(w)) + return MorphTextEdit; + + return MorphCategoryNone; +} + +/* Return the similar classes of a category. This is currently restricted + * to the known Qt classes with no precautions to parse the Widget Database + * (which is too risky, custom classes might have container extensions + * or non-managed layouts, etc.). */ + +static QStringList classesOfCategory(MorphCategory cat) +{ + typedef QMap<MorphCategory, QStringList> CandidateCache; + static CandidateCache candidateCache; + CandidateCache::iterator it = candidateCache.find(cat); + if (it == candidateCache.end()) { + it = candidateCache.insert(cat, QStringList()); + QStringList &l = it.value(); + switch (cat) { + case MorphCategoryNone: + break; + case MorphSimpleContainer: + // Do not generally allow to morph into a layout. + // This can be risky in case of container pages,etc. + l << QLatin1String("QWidget") << QLatin1String("QFrame") << QLatin1String("QGroupBox"); + break; + case MorphPageContainer: + l << QLatin1String("QTabWidget") << QLatin1String("QStackedWidget") << QLatin1String("QToolBox"); + break; + case MorphItemView: + l << QLatin1String("QListView") << QLatin1String("QListWidget") + << QLatin1String("QTreeView") << QLatin1String("QTreeWidget") + << QLatin1String("QTableView") << QLatin1String("QTableWidget") + << QLatin1String("QColumnView"); + break; + case MorphButton: + l << QLatin1String("QCheckBox") << QLatin1String("QRadioButton") + << QLatin1String("QPushButton") << QLatin1String("QToolButton") + << QLatin1String("QCommandLinkButton"); + break; + case MorphSpinBox: + l << QLatin1String("QDateTimeEdit") << QLatin1String("QDateEdit") + << QLatin1String("QTimeEdit") + << QLatin1String("QSpinBox") << QLatin1String("QDoubleSpinBox"); + break; + case MorphTextEdit: + l << QLatin1String("QTextEdit") << QLatin1String("QPlainTextEdit"); + break; + } + } + return it.value(); +} + +// Return the widgets containing the children to be transferred to. This is the +// widget itself in most cases, except for QDesignerContainerExtension cases +static QWidgetList childContainers(const QDesignerFormEditorInterface *core, QWidget *w) +{ + if (const QDesignerContainerExtension *ce = qt_extension<QDesignerContainerExtension*>(core->extensionManager(), w)) { + QWidgetList children; + if (const int count = ce->count()) { + for (int i = 0; i < count; i++) + children.push_back(ce->widget(i)); + } + return children; + } + QWidgetList self; + self.push_back(w); + return self; +} + +// Suggest a suitable objectname for the widget to be morphed into +// Replace the class name parts: 'xxFrame' -> 'xxGroupBox', 'frame' -> 'groupBox' +static QString suggestObjectName(const QString &oldClassName, const QString &newClassName, const QString &oldName) +{ + QString oldClassPart = oldClassName; + QString newClassPart = newClassName; + if (oldClassPart.startsWith(QLatin1Char('Q'))) + oldClassPart.remove(0, 1); + if (newClassPart.startsWith(QLatin1Char('Q'))) + newClassPart.remove(0, 1); + + QString newName = oldName; + newName.replace(oldClassPart, newClassPart); + oldClassPart[0] = oldClassPart.at(0).toLower(); + newClassPart[0] = newClassPart.at(0).toLower(); + newName.replace(oldClassPart, newClassPart); + return newName; +} + +// Find the label whose buddy the widget is. +QLabel *buddyLabelOf(QDesignerFormWindowInterface *fw, QWidget *w) +{ + typedef QList<QLabel*> LabelList; + const LabelList labelList = qFindChildren<QLabel*>(fw); + if (labelList.empty()) + return 0; + const LabelList::const_iterator cend = labelList.constEnd(); + for (LabelList::const_iterator it = labelList.constBegin(); it != cend; ++it ) + if ( (*it)->buddy() == w) + return *it; + return 0; +} + +// Replace widgets in a widget-list type dynamic property of the parent +// used for Z-order, etc. +static void replaceWidgetListDynamicProperty(QWidget *parentWidget, + QWidget *oldWidget, QWidget *newWidget, + const char *name) +{ + QWidgetList list = qVariantValue<QWidgetList>(parentWidget->property(name)); + const int index = list.indexOf(oldWidget); + if (index != -1) { + list.replace(index, newWidget); + parentWidget->setProperty(name, qVariantFromValue(list)); + } +} + +/* Morph a widget into another class. Use the static addMorphMacro() to + * add a respective command sequence to the undo stack as it emits signals + * which cause other commands to be added. */ +class MorphWidgetCommand : public QDesignerFormWindowCommand +{ + Q_DISABLE_COPY(MorphWidgetCommand) +public: + + explicit MorphWidgetCommand(QDesignerFormWindowInterface *formWindow); + ~MorphWidgetCommand(); + + // Convenience to add a morph command sequence macro + static bool addMorphMacro(QDesignerFormWindowInterface *formWindow, QWidget *w, const QString &newClass); + + bool init(QWidget *widget, const QString &newClass); + + QString newWidgetName() const { return m_afterWidget->objectName(); } + + virtual void redo(); + virtual void undo(); + + static QStringList candidateClasses(QDesignerFormWindowInterface *fw, QWidget *w); + +private: + static bool canMorph(QDesignerFormWindowInterface *fw, QWidget *w, int *childContainerCount = 0, MorphCategory *cat = 0); + void morph(QWidget *before, QWidget *after); + + QWidget *m_beforeWidget; + QWidget *m_afterWidget; +}; + +bool MorphWidgetCommand::addMorphMacro(QDesignerFormWindowInterface *fw, QWidget *w, const QString &newClass) +{ + MorphWidgetCommand *morphCmd = new MorphWidgetCommand(fw); + if (!morphCmd->init(w, newClass)) { + qWarning("*** Unable to create a MorphWidgetCommand"); + delete morphCmd; + return false; + } + QLabel *buddyLabel = buddyLabelOf(fw, w); + // Need a macro since it adds further commands + QUndoStack *us = fw->commandHistory(); + us->beginMacro(morphCmd->text()); + // Have the signal slot/buddy editors add their commands to delete widget + if (FormWindowBase *fwb = qobject_cast<FormWindowBase*>(fw)) + fwb->emitWidgetRemoved(w); + + const QString newWidgetName = morphCmd->newWidgetName(); + us->push(morphCmd); + + // restore buddy using the QByteArray name. + if (buddyLabel) { + SetPropertyCommand *buddyCmd = new SetPropertyCommand(fw); + buddyCmd->init(buddyLabel, QLatin1String("buddy"), QVariant(newWidgetName.toUtf8())); + us->push(buddyCmd); + } + us->endMacro(); + return true; +} + +MorphWidgetCommand::MorphWidgetCommand(QDesignerFormWindowInterface *formWindow) : + QDesignerFormWindowCommand(QString(), formWindow), + m_beforeWidget(0), + m_afterWidget(0) +{ +} + +MorphWidgetCommand::~MorphWidgetCommand() +{ +} + +bool MorphWidgetCommand::init(QWidget *widget, const QString &newClassName) +{ + QDesignerFormWindowInterface *fw = formWindow(); + QDesignerFormEditorInterface *core = fw->core(); + + if (!canMorph(fw, widget)) + return false; + + const QString oldClassName = WidgetFactory::classNameOf(core, widget); + const QString oldName = widget->objectName(); + //: MorphWidgetCommand description + setText(QApplication::translate("Command", "Morph %1/'%2' into %3").arg(oldClassName, oldName, newClassName)); + + m_beforeWidget = widget; + m_afterWidget = core->widgetFactory()->createWidget(newClassName, fw); + if (!m_afterWidget) + return false; + + // Set object name. Do not unique it (as to maintain it). + m_afterWidget->setObjectName(suggestObjectName(oldClassName, newClassName, oldName)); + + // If the target has a container extension, we add enough new pages to take + // up the children of the before widget + if (QDesignerContainerExtension* c = qt_extension<QDesignerContainerExtension*>(core->extensionManager(), m_afterWidget)) { + if (const int pageCount = childContainers(core, m_beforeWidget).size()) { + const QString qWidget = QLatin1String("QWidget"); + const QString containerName = m_afterWidget->objectName(); + for (int i = 0; i < pageCount; i++) { + QString name = containerName; + name += QLatin1String("Page"); + name += QString::number(i + 1); + QWidget *page = core->widgetFactory()->createWidget(qWidget); + page->setObjectName(name); + fw->ensureUniqueObjectName(page); + c->addWidget(page); + core->metaDataBase()->add(page); + } + } + } + + // Copy over applicable properties + const QDesignerPropertySheetExtension *beforeSheet = qt_extension<QDesignerPropertySheetExtension*>(core->extensionManager(), widget); + QDesignerPropertySheetExtension *afterSheet = qt_extension<QDesignerPropertySheetExtension*>(core->extensionManager(), m_afterWidget); + const QString objectNameProperty = QLatin1String("objectName"); + const int count = beforeSheet->count(); + for (int i = 0; i < count; i++) + if (beforeSheet->isVisible(i) && beforeSheet->isChanged(i)) { + const QString name = beforeSheet->propertyName(i); + if (name != objectNameProperty) { + const int afterIndex = afterSheet->indexOf(name); + if (afterIndex != -1 && afterSheet->isVisible(afterIndex) && afterSheet->propertyGroup(afterIndex) == beforeSheet->propertyGroup(i)) { + afterSheet->setProperty(i, beforeSheet->property(i)); + afterSheet->setChanged(i, true); + } else { + // Some mismatch. The rest won't match, either + break; + } + } + } + return true; +} + +void MorphWidgetCommand::redo() +{ + morph(m_beforeWidget, m_afterWidget); +} + +void MorphWidgetCommand::undo() +{ + morph(m_afterWidget, m_beforeWidget); +} + +void MorphWidgetCommand::morph(QWidget *before, QWidget *after) +{ + QDesignerFormWindowInterface *fw = formWindow(); + + fw->unmanageWidget(before); + + const QRect oldGeom = before->geometry(); + QWidget *parent = before->parentWidget(); + Q_ASSERT(parent); + /* Morphing consists of main 2 steps + * 1) Move over children (laid out, non-laid out) + * 2) Register self with new parent (laid out, non-laid out) */ + + // 1) Move children. Loop over child containers + QWidgetList beforeChildContainers = childContainers(fw->core(), before); + QWidgetList afterChildContainers = childContainers(fw->core(), after); + Q_ASSERT(beforeChildContainers.size() == afterChildContainers.size()); + const int childContainerCount = beforeChildContainers.size(); + for (int i = 0; i < childContainerCount; i++) { + QWidget *beforeChildContainer = beforeChildContainers.at(i); + QWidget *afterChildContainer = afterChildContainers.at(i); + if (QLayout *childLayout = beforeChildContainer->layout()) { + // Laid-out: Move the layout (since 4.5) + afterChildContainer->setLayout(childLayout); + } else { + // Non-Laid-out: Reparent, move over + const QObjectList c = beforeChildContainer->children(); + const QObjectList::const_iterator cend = c.constEnd(); + for (QObjectList::const_iterator it = c.constBegin(); it != cend; ++it) { + if ( (*it)->isWidgetType()) { + QWidget *w = static_cast<QWidget*>(*it); + if (fw->isManaged(w)) { + const QRect geom = w->geometry(); + w->setParent(afterChildContainer); + w->setGeometry(geom); + } + } + } + } + afterChildContainer->setProperty(widgetOrderPropertyC, beforeChildContainer->property(widgetOrderPropertyC)); + afterChildContainer->setProperty(zOrderPropertyC, beforeChildContainer->property(zOrderPropertyC)); + } + + // 2) Replace the actual widget in the parent layout + after->setGeometry(oldGeom); + if (QLayout *containingLayout = LayoutInfo::managedLayout(fw->core(), parent)) { + LayoutHelper *lh = LayoutHelper::createLayoutHelper(LayoutInfo::layoutType(fw->core(), containingLayout)); + Q_ASSERT(lh); + lh->replaceWidget(containingLayout, before, after); + delete lh; + } else { + before->hide(); + before->setParent(0); + after->setParent(parent); + after->setGeometry(oldGeom); + } + + // Check various properties: Z order, form tab order + replaceWidgetListDynamicProperty(parent, before, after, widgetOrderPropertyC); + replaceWidgetListDynamicProperty(parent, before, after, zOrderPropertyC); + + QDesignerMetaDataBaseItemInterface *formItem = fw->core()->metaDataBase()->item(fw); + QWidgetList tabOrder = formItem->tabOrder(); + const int tabIndex = tabOrder.indexOf(before); + if (tabIndex != -1) { + tabOrder.replace(tabIndex, after); + formItem->setTabOrder(tabOrder); + } + + after->show(); + fw->manageWidget(after); + + fw->clearSelection(false); + fw->selectWidget(after); +} + +/* Check if morphing is possible. It must be a valid category and the parent/ + * child relationships must be either non-laidout or directly on + * Designer-managed layouts. */ +bool MorphWidgetCommand::canMorph(QDesignerFormWindowInterface *fw, QWidget *w, int *ptrToChildContainerCount, MorphCategory *ptrToCat) +{ + if (ptrToChildContainerCount) + *ptrToChildContainerCount = 0; + const MorphCategory cat = category(w); + if (ptrToCat) + *ptrToCat = cat; + if (cat == MorphCategoryNone) + return false; + + QDesignerFormEditorInterface *core = fw->core(); + // Don't know how to fiddle class names in Jambi.. + if (qt_extension<QDesignerLanguageExtension *>(core->extensionManager(), core)) + return false; + if (!fw->isManaged(w) || w == fw->mainContainer()) + return false; + // Check the parent relationship. We accept only managed parent widgets + // with a single, managed layout in which widget is a member. + QWidget *parent = w->parentWidget(); + if (parent == 0) + return false; + if (QLayout *pl = LayoutInfo::managedLayout(core, parent)) + if (pl->indexOf(w) < 0 || !core->metaDataBase()->item(pl)) + return false; + // Check Widget database + const QDesignerWidgetDataBaseInterface *wdb = core->widgetDataBase(); + const int wdbindex = wdb->indexOfObject(w); + if (wdbindex == -1) + return false; + const bool isContainer = wdb->item(wdbindex)->isContainer(); + if (!isContainer) + return true; + // Check children. All child containers must be non-laid-out or have managed layouts + const QWidgetList pages = childContainers(core, w); + const int pageCount = pages.size(); + if (ptrToChildContainerCount) + *ptrToChildContainerCount = pageCount; + if (pageCount) { + for (int i = 0; i < pageCount; i++) + if (QLayout *cl = pages.at(i)->layout()) + if (!core->metaDataBase()->item(cl)) + return false; + } + return true; +} + +QStringList MorphWidgetCommand::candidateClasses(QDesignerFormWindowInterface *fw, QWidget *w) +{ + int childContainerCount; + MorphCategory cat; + if (!canMorph(fw, w, &childContainerCount, &cat)) + return QStringList(); + + QStringList rc = classesOfCategory(cat); + switch (cat) { + // Frames, etc can always be morphed into one-page page containers + case MorphSimpleContainer: + rc += classesOfCategory(MorphPageContainer); + break; + // Multipage-Containers can be morphed into simple containers if they + // have 1 page. + case MorphPageContainer: + if (childContainerCount == 1) + rc += classesOfCategory(MorphSimpleContainer); + break; + default: + break; + } + return rc; +} + +// MorphMenu +MorphMenu::MorphMenu(QObject *parent) : + QObject(parent), + m_subMenuAction(0), + m_menu(0), + m_mapper(0), + m_widget(0), + m_formWindow(0) +{ +} + +void MorphMenu::populate(QWidget *w, QDesignerFormWindowInterface *fw, ActionList& al) +{ + if (populateMenu(w, fw)) + al.push_back(m_subMenuAction); +} + +void MorphMenu::populate(QWidget *w, QDesignerFormWindowInterface *fw, QMenu& m) +{ + if (populateMenu(w, fw)) + m.addAction(m_subMenuAction); +} + +void MorphMenu::slotMorph(const QString &newClassName) +{ + MorphWidgetCommand::addMorphMacro(m_formWindow, m_widget, newClassName); +} + +bool MorphMenu::populateMenu(QWidget *w, QDesignerFormWindowInterface *fw) +{ + m_widget = 0; + m_formWindow = 0; + + // Clear menu + if (m_subMenuAction) { + m_subMenuAction->setVisible(false); + m_menu->clear(); + } + + // Checks: Must not be main container + if (w == fw->mainContainer()) + return false; + + const QStringList c = MorphWidgetCommand::candidateClasses(fw, w); + if (c.empty()) + return false; + + // Pull up + m_widget = w; + m_formWindow = fw; + const QString oldClassName = WidgetFactory::classNameOf(fw->core(), w); + + if (!m_subMenuAction) { + m_subMenuAction = new QAction(tr("Morph into"), this); + m_menu = new QMenu; + m_subMenuAction->setMenu(m_menu); + m_mapper = new QSignalMapper(this); + connect(m_mapper , SIGNAL(mapped(QString)), this, SLOT(slotMorph(QString))); + } + + // Add actions + const QStringList::const_iterator cend = c.constEnd(); + for (QStringList::const_iterator it = c.constBegin(); it != cend; ++it) { + if (*it != oldClassName) { + QAction *a = m_menu->addAction(*it); + m_mapper->setMapping (a, *it); + connect(a, SIGNAL(triggered()), m_mapper, SLOT(map())); + } + } + m_subMenuAction->setVisible(true); + return true; +} + +} // namespace qdesigner_internal + +QT_END_NAMESPACE |