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 "qdesigner_promotion_p.h"
30 #include "widgetdatabase_p.h"
31 #include "metadatabase_p.h"
32 #include "widgetdatabase_p.h"
33 
34 #include <QtDesigner/abstractformeditor.h>
35 #include <QtDesigner/abstractformwindow.h>
36 #include <QtDesigner/abstractformwindowmanager.h>
37 #include <QtDesigner/abstractobjectinspector.h>
38 #include <QtDesigner/abstractwidgetbox.h>
39 #include <QtDesigner/abstractwidgetdatabase.h>
40 
41 #include <QtCore/qmap.h>
42 #include <QtCore/qcoreapplication.h>
43 #include <qdebug.h>
44 
45 QT_BEGIN_NAMESPACE
46 
47 namespace {
48     // Return a set of on-promotable classes
nonPromotableClasses()49     const QSet<QString> &nonPromotableClasses() {
50         static QSet<QString> rc;
51         if (rc.isEmpty()) {
52             rc.insert(QStringLiteral("Line"));
53             rc.insert(QStringLiteral("QAction"));
54             rc.insert(QStringLiteral("Spacer"));
55             rc.insert(QStringLiteral("QMainWindow"));
56             rc.insert(QStringLiteral("QDialog"));
57             rc.insert(QStringLiteral("QMdiArea"));
58             rc.insert(QStringLiteral("QMdiSubWindow"));
59         }
60         return rc;
61     }
62 
63     // Return widget database index of a promoted class or -1 with error message
promotedWidgetDataBaseIndex(const QDesignerWidgetDataBaseInterface * widgetDataBase,const QString & className,QString * errorMessage)64     int promotedWidgetDataBaseIndex(const QDesignerWidgetDataBaseInterface *widgetDataBase,
65                                                                  const QString &className,
66                                                                  QString *errorMessage) {
67         const int index = widgetDataBase->indexOfClassName(className);
68         if (index == -1 || !widgetDataBase->item(index)->isPromoted()) {
69             *errorMessage = QCoreApplication::tr("%1 is not a promoted class.").arg(className);
70             return -1;
71         }
72         return index;
73     }
74 
75     // Return widget database item of a promoted class or 0 with error message
promotedWidgetDataBaseItem(const QDesignerWidgetDataBaseInterface * widgetDataBase,const QString & className,QString * errorMessage)76     QDesignerWidgetDataBaseItemInterface *promotedWidgetDataBaseItem(const QDesignerWidgetDataBaseInterface *widgetDataBase,
77                                                                      const QString &className,
78                                                                      QString *errorMessage) {
79 
80         const int index =  promotedWidgetDataBaseIndex(widgetDataBase, className, errorMessage);
81         if (index == -1)
82             return nullptr;
83         return widgetDataBase->item(index);
84     }
85 
86     // extract class name from xml  "<widget class="QWidget" ...>". Quite a hack.
classNameFromXml(QString xml)87     QString classNameFromXml(QString xml) {
88         static const QString tag = QStringLiteral("class=\"");
89         const int pos = xml.indexOf(tag);
90         if (pos == -1)
91             return QString();
92         xml.remove(0, pos + tag.size());
93         const int closingPos = xml.indexOf(QLatin1Char('"'));
94         if (closingPos == -1)
95             return QString();
96         xml.remove(closingPos, xml.size() - closingPos);
97         return xml;
98     }
99 
100     // return a list of class names in the scratch pad
getScratchPadClasses(const QDesignerWidgetBoxInterface * wb)101     QStringList getScratchPadClasses(const QDesignerWidgetBoxInterface *wb) {
102         QStringList rc;
103         const int catCount =  wb->categoryCount();
104         for (int c = 0; c <  catCount; c++) {
105             const QDesignerWidgetBoxInterface::Category category = wb->category(c);
106             if (category.type() == QDesignerWidgetBoxInterface::Category::Scratchpad) {
107                 const int widgetCount = category.widgetCount();
108                 for (int w = 0; w < widgetCount; w++) {
109                     const QString className = classNameFromXml( category.widget(w).domXml());
110                     if (!className.isEmpty())
111                         rc += className;
112                 }
113             }
114         }
115         return rc;
116     }
117 }
118 
markFormsDirty(const QDesignerFormEditorInterface * core)119 static void markFormsDirty(const QDesignerFormEditorInterface *core)
120 {
121     const QDesignerFormWindowManagerInterface *fwm = core->formWindowManager();
122     for (int f = 0, count = fwm->formWindowCount(); f < count; ++f)
123         fwm->formWindow(f)->setDirty(true);
124 }
125 
126 namespace qdesigner_internal {
127 
QDesignerPromotion(QDesignerFormEditorInterface * core)128     QDesignerPromotion::QDesignerPromotion(QDesignerFormEditorInterface *core) :
129         m_core(core)  {
130     }
131 
addPromotedClass(const QString & baseClass,const QString & className,const QString & includeFile,QString * errorMessage)132     bool  QDesignerPromotion::addPromotedClass(const QString &baseClass,
133                                                const QString &className,
134                                                const QString &includeFile,
135                                                QString *errorMessage)
136     {
137         QDesignerWidgetDataBaseInterface *widgetDataBase = m_core->widgetDataBase();
138         const int baseClassIndex = widgetDataBase->indexOfClassName(baseClass);
139 
140         if (baseClassIndex == -1) {
141             *errorMessage = QCoreApplication::tr("The base class %1 is invalid.").arg(baseClass);
142             return false;
143         }
144 
145         const int existingClassIndex = widgetDataBase->indexOfClassName(className);
146 
147         if (existingClassIndex != -1) {
148             *errorMessage = QCoreApplication::tr("The class %1 already exists.").arg(className);
149             return false;
150         }
151         // Clone derived item.
152         QDesignerWidgetDataBaseItemInterface *promotedItem = WidgetDataBaseItem::clone(widgetDataBase->item(baseClassIndex));
153         // Also inherit the container flag in case of QWidget-derived classes
154         // as it is most likely intended for stacked pages.
155         // set new props
156         promotedItem->setName(className);
157         promotedItem->setGroup(QCoreApplication::tr("Promoted Widgets"));
158         promotedItem->setCustom(true);
159         promotedItem->setPromoted(true);
160         promotedItem->setExtends(baseClass);
161         promotedItem->setIncludeFile(includeFile);
162         widgetDataBase->append(promotedItem);
163         markFormsDirty(m_core);
164         return true;
165     }
166 
promotionBaseClasses() const167     QList<QDesignerWidgetDataBaseItemInterface *> QDesignerPromotion::promotionBaseClasses() const
168     {
169         using SortedDatabaseItemMap = QMap<QString, QDesignerWidgetDataBaseItemInterface *>;
170         SortedDatabaseItemMap sortedDatabaseItemMap;
171 
172         QDesignerWidgetDataBaseInterface *widgetDataBase = m_core->widgetDataBase();
173 
174         const int cnt = widgetDataBase->count();
175         for (int i = 0; i <  cnt; i++) {
176             QDesignerWidgetDataBaseItemInterface *dbItem = widgetDataBase->item(i);
177             if (canBePromoted(dbItem)) {
178                 sortedDatabaseItemMap.insert(dbItem->name(), dbItem);
179             }
180         }
181 
182         return sortedDatabaseItemMap.values();
183     }
184 
185 
canBePromoted(const QDesignerWidgetDataBaseItemInterface * dbItem) const186     bool QDesignerPromotion::canBePromoted(const QDesignerWidgetDataBaseItemInterface *dbItem) const
187     {
188         if (dbItem->isPromoted() ||  !dbItem->extends().isEmpty())
189             return false;
190 
191         const QString name = dbItem->name();
192 
193         if (nonPromotableClasses().contains(name))
194             return false;
195 
196         if (name.startsWith(QStringLiteral("QDesigner")) ||
197             name.startsWith(QStringLiteral("QLayout")))
198             return false;
199 
200         return true;
201     }
202 
promotedClasses() const203     QDesignerPromotion::PromotedClasses QDesignerPromotion::promotedClasses()  const
204     {
205         using ClassNameItemMap = QMap<QString, QDesignerWidgetDataBaseItemInterface *>;
206         // A map containing base classes and their promoted classes.
207         using BaseClassPromotedMap = QMap<QString, ClassNameItemMap>;
208 
209         BaseClassPromotedMap baseClassPromotedMap;
210 
211         QDesignerWidgetDataBaseInterface *widgetDataBase = m_core->widgetDataBase();
212         // Look for promoted classes and insert into map according to base class.
213         const  int cnt = widgetDataBase->count();
214         for (int i = 0; i < cnt; i++) {
215             QDesignerWidgetDataBaseItemInterface *dbItem = widgetDataBase->item(i);
216             if (dbItem->isPromoted()) {
217                 const QString baseClassName = dbItem->extends();
218                 BaseClassPromotedMap::iterator it = baseClassPromotedMap.find(baseClassName);
219                 if (it == baseClassPromotedMap.end()) {
220                     it = baseClassPromotedMap.insert(baseClassName, ClassNameItemMap());
221                 }
222                 it.value().insert(dbItem->name(), dbItem);
223             }
224         }
225         // convert map into list.
226         PromotedClasses rc;
227 
228         if (baseClassPromotedMap.isEmpty())
229             return rc;
230 
231         const BaseClassPromotedMap::const_iterator bcend = baseClassPromotedMap.constEnd();
232         for (BaseClassPromotedMap::const_iterator bit = baseClassPromotedMap.constBegin(); bit !=  bcend; ++bit) {
233             const int baseIndex = widgetDataBase->indexOfClassName(bit.key());
234             Q_ASSERT(baseIndex >= 0);
235             QDesignerWidgetDataBaseItemInterface *baseItem = widgetDataBase->item(baseIndex);
236             // promoted
237             const ClassNameItemMap::const_iterator pcend = bit.value().constEnd();
238             for (ClassNameItemMap::const_iterator pit = bit.value().constBegin(); pit != pcend; ++pit) {
239                 PromotedClass item;
240                 item.baseItem = baseItem;
241                 item.promotedItem = pit.value();
242                 rc.push_back(item);
243             }
244         }
245 
246         return rc;
247     }
248 
referencedPromotedClassNames() const249     QSet<QString> QDesignerPromotion::referencedPromotedClassNames()  const {
250         QSet<QString> rc;
251         const MetaDataBase *metaDataBase = qobject_cast<const MetaDataBase*>(m_core->metaDataBase());
252         if (!metaDataBase)
253             return rc;
254 
255         const QObjectList &objects = metaDataBase->objects();
256         for (QObject *object : objects) {
257             const QString customClass = metaDataBase->metaDataBaseItem(object)->customClassName();
258             if (!customClass.isEmpty())
259                 rc.insert(customClass);
260 
261         }
262         // check the scratchpad of the widget box
263         if (QDesignerWidgetBoxInterface *widgetBox = m_core->widgetBox()) {
264             const QStringList scratchPadClasses = getScratchPadClasses(widgetBox);
265             if (!scratchPadClasses.isEmpty()) {
266                 // Check whether these are actually promoted
267                 QDesignerWidgetDataBaseInterface *widgetDataBase = m_core->widgetDataBase();
268                 QStringList::const_iterator cend = scratchPadClasses.constEnd();
269                 for (QStringList::const_iterator it = scratchPadClasses.constBegin(); it != cend; ++it ) {
270                     const int index = widgetDataBase->indexOfClassName(*it);
271                     if (index != -1 && widgetDataBase->item(index)->isPromoted())
272                         rc += *it;
273                 }
274             }
275         }
276         return rc;
277     }
278 
removePromotedClass(const QString & className,QString * errorMessage)279     bool QDesignerPromotion::removePromotedClass(const QString &className, QString *errorMessage) {
280         // check if it exists and is promoted
281         WidgetDataBase *widgetDataBase = qobject_cast<WidgetDataBase *>(m_core->widgetDataBase());
282         if (!widgetDataBase) {
283             *errorMessage = QCoreApplication::tr("The class %1 cannot be removed").arg(className);
284             return false;
285         }
286 
287         const int index = promotedWidgetDataBaseIndex(widgetDataBase, className, errorMessage);
288         if (index == -1)
289             return false;
290 
291         if (referencedPromotedClassNames().contains(className)) {
292             *errorMessage = QCoreApplication::tr("The class %1 cannot be removed because it is still referenced.").arg(className);
293             return false;
294         }
295         // QTBUG-52963: Check for classes that specify the to-be-removed class as
296         // base class of a promoted class. This should not happen in the normal case
297         // as promoted classes cannot serve as base for further promotion. It is possible
298         // though if a class provided by a plugin (say Qt WebKit's QWebView) is used as
299         // a base class for a promoted widget B and the plugin is removed in the next
300         // launch. QWebView will then appear as promoted class itself and the promoted
301         // class B will depend on it. When removing QWebView, the base class of B will
302         // be changed to that of QWebView by the below code.
303         const PromotedClasses promotedList = promotedClasses();
304         for (PromotedClasses::const_iterator it = promotedList.constBegin(), end = promotedList.constEnd(); it != end; ++it) {
305             if (it->baseItem->name() == className) {
306                 const QString extends = widgetDataBase->item(index)->extends();
307                 qWarning().nospace() << "Warning: Promoted class " << it->promotedItem->name()
308                     << " extends " << className << ", changing its base class to " <<  extends << '.';
309                 it->promotedItem->setExtends(extends);
310             }
311         }
312         widgetDataBase->remove(index);
313         markFormsDirty(m_core);
314         return true;
315     }
316 
changePromotedClassName(const QString & oldclassName,const QString & newClassName,QString * errorMessage)317     bool QDesignerPromotion::changePromotedClassName(const QString &oldclassName, const QString &newClassName, QString *errorMessage) {
318         const MetaDataBase *metaDataBase = qobject_cast<const MetaDataBase*>(m_core->metaDataBase());
319         if (!metaDataBase) {
320             *errorMessage = QCoreApplication::tr("The class %1 cannot be renamed").arg(oldclassName);
321             return false;
322         }
323         QDesignerWidgetDataBaseInterface *widgetDataBase = m_core->widgetDataBase();
324 
325         // check the new name
326         if (newClassName.isEmpty()) {
327             *errorMessage = QCoreApplication::tr("The class %1 cannot be renamed to an empty name.").arg(oldclassName);
328             return false;
329         }
330         const int existingIndex = widgetDataBase->indexOfClassName(newClassName);
331         if (existingIndex != -1) {
332             *errorMessage = QCoreApplication::tr("There is already a class named %1.").arg(newClassName);
333             return false;
334         }
335         // Check old class
336         QDesignerWidgetDataBaseItemInterface *dbItem = promotedWidgetDataBaseItem(widgetDataBase, oldclassName, errorMessage);
337         if (!dbItem)
338             return false;
339 
340         // Change the name in the data base and change all referencing objects in the meta database
341         dbItem->setName(newClassName);
342         bool foundReferences = false;
343         const QObjectList &dbObjects = metaDataBase->objects();
344         for (QObject* object : dbObjects) {
345             MetaDataBaseItem *item =  metaDataBase->metaDataBaseItem(object);
346             Q_ASSERT(item);
347             if (item->customClassName() == oldclassName) {
348                 item->setCustomClassName(newClassName);
349                 foundReferences = true;
350             }
351         }
352         // set state
353         if (foundReferences)
354             refreshObjectInspector();
355 
356         markFormsDirty(m_core);
357         return true;
358     }
359 
setPromotedClassIncludeFile(const QString & className,const QString & includeFile,QString * errorMessage)360     bool QDesignerPromotion::setPromotedClassIncludeFile(const QString &className, const QString &includeFile, QString *errorMessage) {
361         // check file
362         if (includeFile.isEmpty()) {
363             *errorMessage = QCoreApplication::tr("Cannot set an empty include file.");
364             return false;
365         }
366         // check item
367         QDesignerWidgetDataBaseInterface *widgetDataBase = m_core->widgetDataBase();
368         QDesignerWidgetDataBaseItemInterface *dbItem = promotedWidgetDataBaseItem(widgetDataBase, className, errorMessage);
369         if (!dbItem)
370             return false;
371         if (dbItem->includeFile() != includeFile) {
372             dbItem->setIncludeFile(includeFile);
373             markFormsDirty(m_core);
374         }
375         return true;
376     }
377 
refreshObjectInspector()378     void QDesignerPromotion::refreshObjectInspector() {
379         if (QDesignerFormWindowManagerInterface *fwm = m_core->formWindowManager()) {
380             if (QDesignerFormWindowInterface *fw = fwm->activeFormWindow())
381                 if ( QDesignerObjectInspectorInterface *oi = m_core->objectInspector()) {
382                     oi->setFormWindow(fw);
383                 }
384         }
385     }
386 }
387 
388 QT_END_NAMESPACE
389