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 |
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 |
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(s: "%1 is not a promoted class." ).arg(a: className); |
70 | return -1; |
71 | } |
72 | return index; |
73 | } |
74 | |
75 | // Return widget database item of a promoted class or 0 with error message |
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. |
87 | QString classNameFromXml(QString xml) { |
88 | static const QString tag = QStringLiteral("class=\"" ); |
89 | const int pos = xml.indexOf(s: tag); |
90 | if (pos == -1) |
91 | return QString(); |
92 | xml.remove(i: 0, len: pos + tag.size()); |
93 | const int closingPos = xml.indexOf(c: QLatin1Char('"')); |
94 | if (closingPos == -1) |
95 | return QString(); |
96 | xml.remove(i: closingPos, len: xml.size() - closingPos); |
97 | return xml; |
98 | } |
99 | |
100 | // return a list of class names in the scratch pad |
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(cat_idx: 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( xml: category.widget(idx: w).domXml()); |
110 | if (!className.isEmpty()) |
111 | rc += className; |
112 | } |
113 | } |
114 | } |
115 | return rc; |
116 | } |
117 | } |
118 | |
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(index: f)->setDirty(true); |
124 | } |
125 | |
126 | namespace qdesigner_internal { |
127 | |
128 | QDesignerPromotion::QDesignerPromotion(QDesignerFormEditorInterface *core) : |
129 | m_core(core) { |
130 | } |
131 | |
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(className: baseClass); |
139 | |
140 | if (baseClassIndex == -1) { |
141 | *errorMessage = QCoreApplication::tr(s: "The base class %1 is invalid." ).arg(a: baseClass); |
142 | return false; |
143 | } |
144 | |
145 | const int existingClassIndex = widgetDataBase->indexOfClassName(className); |
146 | |
147 | if (existingClassIndex != -1) { |
148 | *errorMessage = QCoreApplication::tr(s: "The class %1 already exists." ).arg(a: className); |
149 | return false; |
150 | } |
151 | // Clone derived item. |
152 | QDesignerWidgetDataBaseItemInterface *promotedItem = WidgetDataBaseItem::clone(item: widgetDataBase->item(index: 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(s: "Promoted Widgets" )); |
158 | promotedItem->setCustom(true); |
159 | promotedItem->setPromoted(true); |
160 | promotedItem->setExtends(baseClass); |
161 | promotedItem->setIncludeFile(includeFile); |
162 | widgetDataBase->append(item: promotedItem); |
163 | markFormsDirty(core: m_core); |
164 | return true; |
165 | } |
166 | |
167 | 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(index: i); |
177 | if (canBePromoted(dbItem)) { |
178 | sortedDatabaseItemMap.insert(akey: dbItem->name(), avalue: dbItem); |
179 | } |
180 | } |
181 | |
182 | return sortedDatabaseItemMap.values(); |
183 | } |
184 | |
185 | |
186 | 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(value: 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 | |
203 | 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(index: i); |
216 | if (dbItem->isPromoted()) { |
217 | const QString baseClassName = dbItem->extends(); |
218 | BaseClassPromotedMap::iterator it = baseClassPromotedMap.find(akey: baseClassName); |
219 | if (it == baseClassPromotedMap.end()) { |
220 | it = baseClassPromotedMap.insert(akey: baseClassName, avalue: ClassNameItemMap()); |
221 | } |
222 | it.value().insert(akey: dbItem->name(), avalue: 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(className: bit.key()); |
234 | Q_ASSERT(baseIndex >= 0); |
235 | QDesignerWidgetDataBaseItemInterface *baseItem = widgetDataBase->item(index: 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(t: item); |
243 | } |
244 | } |
245 | |
246 | return rc; |
247 | } |
248 | |
249 | QSet<QString> QDesignerPromotion::referencedPromotedClassNames() const { |
250 | QSet<QString> rc; |
251 | const MetaDataBase *metaDataBase = qobject_cast<const MetaDataBase*>(object: 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(value: customClass); |
260 | |
261 | } |
262 | // check the scratchpad of the widget box |
263 | if (QDesignerWidgetBoxInterface *widgetBox = m_core->widgetBox()) { |
264 | const QStringList scratchPadClasses = getScratchPadClasses(wb: 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(className: *it); |
271 | if (index != -1 && widgetDataBase->item(index)->isPromoted()) |
272 | rc += *it; |
273 | } |
274 | } |
275 | } |
276 | return rc; |
277 | } |
278 | |
279 | bool QDesignerPromotion::removePromotedClass(const QString &className, QString *errorMessage) { |
280 | // check if it exists and is promoted |
281 | WidgetDataBase *widgetDataBase = qobject_cast<WidgetDataBase *>(object: m_core->widgetDataBase()); |
282 | if (!widgetDataBase) { |
283 | *errorMessage = QCoreApplication::tr(s: "The class %1 cannot be removed" ).arg(a: 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(value: className)) { |
292 | *errorMessage = QCoreApplication::tr(s: "The class %1 cannot be removed because it is still referenced." ).arg(a: 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(core: m_core); |
314 | return true; |
315 | } |
316 | |
317 | bool QDesignerPromotion::changePromotedClassName(const QString &oldclassName, const QString &newClassName, QString *errorMessage) { |
318 | const MetaDataBase *metaDataBase = qobject_cast<const MetaDataBase*>(object: m_core->metaDataBase()); |
319 | if (!metaDataBase) { |
320 | *errorMessage = QCoreApplication::tr(s: "The class %1 cannot be renamed" ).arg(a: 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(s: "The class %1 cannot be renamed to an empty name." ).arg(a: oldclassName); |
328 | return false; |
329 | } |
330 | const int existingIndex = widgetDataBase->indexOfClassName(className: newClassName); |
331 | if (existingIndex != -1) { |
332 | *errorMessage = QCoreApplication::tr(s: "There is already a class named %1." ).arg(a: newClassName); |
333 | return false; |
334 | } |
335 | // Check old class |
336 | QDesignerWidgetDataBaseItemInterface *dbItem = promotedWidgetDataBaseItem(widgetDataBase, className: 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(core: m_core); |
357 | return true; |
358 | } |
359 | |
360 | bool QDesignerPromotion::setPromotedClassIncludeFile(const QString &className, const QString &includeFile, QString *errorMessage) { |
361 | // check file |
362 | if (includeFile.isEmpty()) { |
363 | *errorMessage = QCoreApplication::tr(s: "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(core: m_core); |
374 | } |
375 | return true; |
376 | } |
377 | |
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 | |