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 "newformwidget_p.h"
30#include "ui_newformwidget.h"
31#include "qdesigner_formbuilder_p.h"
32#include "sheet_delegate_p.h"
33#include "widgetdatabase_p.h"
34#include "shared_settings_p.h"
35
36#include <QtDesigner/abstractformeditor.h>
37#include <QtDesigner/abstractformwindow.h>
38#include <QtDesigner/qextensionmanager.h>
39#include <QtDesigner/abstractlanguage.h>
40#include <QtDesigner/abstractwidgetdatabase.h>
41
42#include <QtCore/qdir.h>
43#include <QtCore/qfile.h>
44#include <QtCore/qfileinfo.h>
45#include <QtCore/qdebug.h>
46#include <QtCore/qbytearray.h>
47#include <QtCore/qbuffer.h>
48#include <QtCore/qdir.h>
49#include <QtCore/qtextstream.h>
50
51#include <QtWidgets/qapplication.h>
52#include <QtWidgets/qdesktopwidget.h>
53#include <QtWidgets/qheaderview.h>
54#include <QtWidgets/qtreewidget.h>
55#include <QtGui/qpainter.h>
56#include <QtWidgets/qpushbutton.h>
57
58QT_BEGIN_NAMESPACE
59
60enum { profileComboIndexOffset = 1 };
61enum { debugNewFormWidget = 0 };
62
63enum NewForm_CustomRole {
64 // File name (templates from resources, paths)
65 TemplateNameRole = Qt::UserRole + 100,
66 // Class name (widgets from Widget data base)
67 ClassNameRole = Qt::UserRole + 101
68};
69
70static const char *newFormObjectNameC = "Form";
71
72// Create a form name for an arbitrary class. If it is Qt, qtify it,
73// else return "Form".
74static QString formName(const QString &className)
75{
76 if (!className.startsWith(c: QLatin1Char('Q')))
77 return QLatin1String(newFormObjectNameC);
78 QString rc = className;
79 rc.remove(i: 0, len: 1);
80 return rc;
81}
82
83namespace qdesigner_internal {
84
85struct TemplateSize {
86 const char *name;
87 int width;
88 int height;
89};
90
91static const struct TemplateSize templateSizes[] =
92{
93 { QT_TRANSLATE_NOOP("qdesigner_internal::NewFormWidget", "Default size"), .width: 0, .height: 0 },
94 { QT_TRANSLATE_NOOP("qdesigner_internal::NewFormWidget", "QVGA portrait (240x320)"), .width: 240, .height: 320 },
95 { QT_TRANSLATE_NOOP("qdesigner_internal::NewFormWidget", "QVGA landscape (320x240)"), .width: 320, .height: 240 },
96 { QT_TRANSLATE_NOOP("qdesigner_internal::NewFormWidget", "VGA portrait (480x640)"), .width: 480, .height: 640 },
97 { QT_TRANSLATE_NOOP("qdesigner_internal::NewFormWidget", "VGA landscape (640x480)"), .width: 640, .height: 480 }
98};
99
100/* -------------- NewForm dialog.
101 * Designer takes new form templates from:
102 * 1) Files located in directories specified in resources
103 * 2) Files located in directories specified as user templates
104 * 3) XML from container widgets deemed usable for form templates by the widget
105 * database
106 * 4) XML from custom container widgets deemed usable for form templates by the
107 * widget database
108 *
109 * The widget database provides helper functions to obtain lists of names
110 * and xml for 3,4.
111 *
112 * Fixed-size forms for embedded platforms are obtained as follows:
113 * 1) If the origin is a file:
114 * - Check if the file exists in the subdirectory "/<width>x<height>/" of
115 * the path (currently the case for the dialog box because the button box
116 * needs to be positioned)
117 * - Scale the form using the QWidgetDatabase::scaleFormTemplate routine.
118 * 2) If the origin is XML:
119 * - Scale the form using the QWidgetDatabase::scaleFormTemplate routine.
120 *
121 * The tree widget item roles indicate which type of entry it is
122 * (TemplateNameRole = file name 1,2, ClassNameRole = class name 3,4)
123 */
124
125NewFormWidget::NewFormWidget(QDesignerFormEditorInterface *core, QWidget *parentWidget) :
126 QDesignerNewFormWidgetInterface(parentWidget),
127 m_core(core),
128 m_ui(new Ui::NewFormWidget),
129 m_currentItem(nullptr),
130 m_acceptedItem(nullptr)
131{
132 m_ui->setupUi(this);
133 m_ui->treeWidget->setItemDelegate(new qdesigner_internal::SheetDelegate(m_ui->treeWidget, this));
134 m_ui->treeWidget->header()->hide();
135 m_ui->treeWidget->header()->setStretchLastSection(true);
136 m_ui->lblPreview->setBackgroundRole(QPalette::Base);
137 QDesignerSharedSettings settings(m_core);
138
139 QString uiExtension = QStringLiteral("ui");
140 QString templatePath = QStringLiteral(":/qt-project.org/designer/templates/forms");
141
142 QDesignerLanguageExtension *lang = qt_extension<QDesignerLanguageExtension *>(manager: core->extensionManager(), object: core);
143 if (lang) {
144 templatePath = QStringLiteral(":/templates/forms");
145 uiExtension = lang->uiExtension();
146 }
147
148 // Resource templates
149 const QString formTemplate = settings.formTemplate();
150 QTreeWidgetItem *selectedItem = nullptr;
151 loadFrom(path: templatePath, resourceFile: true, uiExtension, selectedItem: formTemplate, selectedItemFound&: selectedItem);
152 // Additional template paths
153 const QStringList formTemplatePaths = settings.formTemplatePaths();
154 const QStringList::const_iterator ftcend = formTemplatePaths.constEnd();
155 for (QStringList::const_iterator it = formTemplatePaths.constBegin(); it != ftcend; ++it)
156 loadFrom(path: *it, resourceFile: false, uiExtension, selectedItem: formTemplate, selectedItemFound&: selectedItem);
157
158 // Widgets/custom widgets
159 if (!lang) {
160 //: New Form Dialog Categories
161 loadFrom(title: tr(s: "Widgets"), nameList: qdesigner_internal::WidgetDataBase::formWidgetClasses(core), selectedItem: formTemplate, selectedItemFound&: selectedItem);
162 loadFrom(title: tr(s: "Custom Widgets"), nameList: qdesigner_internal::WidgetDataBase::customFormWidgetClasses(core), selectedItem: formTemplate, selectedItemFound&: selectedItem);
163 }
164
165 // Still no selection - default to first item
166 if (selectedItem == nullptr && m_ui->treeWidget->topLevelItemCount() != 0) {
167 QTreeWidgetItem *firstTopLevel = m_ui->treeWidget->topLevelItem(index: 0);
168 if (firstTopLevel->childCount() > 0)
169 selectedItem = firstTopLevel->child(index: 0);
170 }
171
172 // Open parent, select and make visible
173 if (selectedItem) {
174 m_ui->treeWidget->setCurrentItem(selectedItem);
175 selectedItem->setSelected(true);
176 m_ui->treeWidget->scrollToItem(item: selectedItem->parent());
177 }
178 // Fill profile combo
179 m_deviceProfiles = settings.deviceProfiles();
180 m_ui->profileComboBox->addItem(atext: tr(s: "None"));
181 connect(sender: m_ui->profileComboBox,
182 signal: QOverload<int>::of(ptr: &QComboBox::currentIndexChanged),
183 receiver: this, slot: &NewFormWidget::slotDeviceProfileIndexChanged);
184 if (m_deviceProfiles.isEmpty()) {
185 m_ui->profileComboBox->setEnabled(false);
186 } else {
187 for (const auto &deviceProfile : qAsConst(t&: m_deviceProfiles))
188 m_ui->profileComboBox->addItem(atext: deviceProfile.name());
189 const int ci = settings.currentDeviceProfileIndex();
190 if (ci >= 0)
191 m_ui->profileComboBox->setCurrentIndex(ci + profileComboIndexOffset);
192 }
193 // Fill size combo
194 for (const TemplateSize &t : templateSizes)
195 m_ui->sizeComboBox->addItem(atext: tr(s: t.name), auserData: QSize(t.width, t.height));
196
197 setTemplateSize(settings.newFormSize());
198
199 if (debugNewFormWidget)
200 qDebug() << Q_FUNC_INFO << "Leaving";
201}
202
203NewFormWidget::~NewFormWidget()
204{
205 QDesignerSharedSettings settings (m_core);
206 settings.setNewFormSize(templateSize());
207 // Do not change previously stored item if dialog was rejected
208 if (m_acceptedItem)
209 settings.setFormTemplate(m_acceptedItem->text(column: 0));
210 delete m_ui;
211}
212
213void NewFormWidget::on_treeWidget_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *)
214{
215 if (debugNewFormWidget)
216 qDebug() << Q_FUNC_INFO << current;
217 if (!current)
218 return;
219
220 if (!current->parent()) { // Top level item: Ensure expanded when browsing down
221 return;
222 }
223
224 m_currentItem = current;
225
226 emit currentTemplateChanged(templateSelected: showCurrentItemPixmap());
227}
228
229bool NewFormWidget::showCurrentItemPixmap()
230{
231 bool rc = false;
232 if (m_currentItem) {
233 const QPixmap pixmap = formPreviewPixmap(item: m_currentItem);
234 if (pixmap.isNull()) {
235 m_ui->lblPreview->setText(tr(s: "Error loading form"));
236 } else {
237 m_ui->lblPreview->setPixmap(pixmap);
238 rc = true;
239 }
240 }
241 return rc;
242}
243
244void NewFormWidget::on_treeWidget_itemActivated(QTreeWidgetItem *item)
245{
246 if (debugNewFormWidget)
247 qDebug() << Q_FUNC_INFO << item;
248
249 if (item->data(column: 0, role: TemplateNameRole).isValid() || item->data(column: 0, role: ClassNameRole).isValid())
250 emit templateActivated();
251}
252
253QPixmap NewFormWidget::formPreviewPixmap(const QTreeWidgetItem *item)
254{
255 // Cache pixmaps per item/device profile
256 const ItemPixmapCacheKey cacheKey(item, profileComboIndex());
257 ItemPixmapCache::iterator it = m_itemPixmapCache.find(akey: cacheKey);
258 if (it == m_itemPixmapCache.end()) {
259 // file or string?
260 const QVariant fileName = item->data(column: 0, role: TemplateNameRole);
261 QPixmap rc;
262 if (fileName.type() == QVariant::String) {
263 rc = formPreviewPixmap(fileName: fileName.toString());
264 } else {
265 const QVariant classNameV = item->data(column: 0, role: ClassNameRole);
266 Q_ASSERT(classNameV.type() == QVariant::String);
267 const QString className = classNameV.toString();
268 QByteArray data = qdesigner_internal::WidgetDataBase::formTemplate(core: m_core, className, objectName: formName(className)).toUtf8();
269 QBuffer buffer(&data);
270 buffer.open(openMode: QIODevice::ReadOnly);
271 rc = formPreviewPixmap(file&: buffer);
272 }
273 if (rc.isNull()) // Retry invalid ones
274 return rc;
275 it = m_itemPixmapCache.insert(akey: cacheKey, avalue: rc);
276 }
277 return it.value();
278}
279
280QPixmap NewFormWidget::formPreviewPixmap(const QString &fileName) const
281{
282 QFile f(fileName);
283 if (f.open(flags: QFile::ReadOnly)) {
284 QFileInfo fi(fileName);
285 const QPixmap rc = formPreviewPixmap(file&: f, workingDir: fi.absolutePath());
286 f.close();
287 return rc;
288 }
289 qWarning() << "The file " << fileName << " could not be opened: " << f.errorString();
290 return QPixmap();
291}
292
293QImage NewFormWidget::grabForm(QDesignerFormEditorInterface *core,
294 QIODevice &file,
295 const QString &workingDir,
296 const qdesigner_internal::DeviceProfile &dp)
297{
298 qdesigner_internal::NewFormWidgetFormBuilder
299 formBuilder(core, dp);
300 if (!workingDir.isEmpty())
301 formBuilder.setWorkingDirectory(workingDir);
302
303 QWidget *widget = formBuilder.load(dev: &file, parentWidget: nullptr);
304 if (!widget)
305 return QImage();
306
307 const QPixmap pixmap = widget->grab(rectangle: QRect(0, 0, -1, -1));
308 widget->deleteLater();
309 return pixmap.toImage();
310}
311
312QPixmap NewFormWidget::formPreviewPixmap(QIODevice &file, const QString &workingDir) const
313{
314 const QSizeF screenSize(QApplication::desktop()->screenGeometry(widget: this).size());
315 const int previewSize = qRound(d: screenSize.width() / 7.5); // 256 on 1920px screens.
316 const int margin = previewSize / 32 - 1; // 7 on 1920px screens.
317 const int shadow = margin;
318
319 const QImage wimage = grabForm(core: m_core, file, workingDir, dp: currentDeviceProfile());
320 if (wimage.isNull())
321 return QPixmap();
322 const qreal devicePixelRatio = wimage.devicePixelRatioF();
323 const QSize imageSize(previewSize - margin * 2, previewSize - margin * 2);
324 QImage image = wimage.scaled(s: (QSizeF(imageSize) * devicePixelRatio).toSize(),
325 aspectMode: Qt::KeepAspectRatio, mode: Qt::SmoothTransformation);
326 image.setDevicePixelRatio(devicePixelRatio);
327
328 QImage dest((QSizeF(previewSize, previewSize) * devicePixelRatio).toSize(),
329 QImage::Format_ARGB32_Premultiplied);
330 dest.setDevicePixelRatio(devicePixelRatio);
331 dest.fill(pixel: 0);
332
333 QPainter p(&dest);
334 p.drawImage(x: margin, y: margin, image);
335
336 p.setPen(QPen(palette().brush(cr: QPalette::WindowText), 0));
337
338 p.drawRect(rect: QRectF(margin - 1, margin - 1, imageSize.width() + 1.5, imageSize.height() + 1.5));
339
340 const QColor dark(Qt::darkGray);
341 const QColor light(Qt::transparent);
342
343 // right shadow
344 {
345 const QRect rect(margin + imageSize.width() + 1, margin + shadow, shadow, imageSize.height() - shadow + 1);
346 QLinearGradient lg(rect.topLeft(), rect.topRight());
347 lg.setColorAt(pos: 0, color: dark);
348 lg.setColorAt(pos: 1, color: light);
349 p.fillRect(rect, lg);
350 }
351
352 // bottom shadow
353 {
354 const QRect rect(margin + shadow, margin + imageSize.height() + 1, imageSize.width() - shadow + 1, shadow);
355 QLinearGradient lg(rect.topLeft(), rect.bottomLeft());
356 lg.setColorAt(pos: 0, color: dark);
357 lg.setColorAt(pos: 1, color: light);
358 p.fillRect(rect, lg);
359 }
360
361 // bottom/right corner shadow
362 {
363 const QRect rect(margin + imageSize.width() + 1, margin + imageSize.height() + 1, shadow, shadow);
364 QRadialGradient g(rect.topLeft(), shadow - 1);
365 g.setColorAt(pos: 0, color: dark);
366 g.setColorAt(pos: 1, color: light);
367 p.fillRect(rect, g);
368 }
369
370 // top/right corner
371 {
372 const QRect rect(margin + imageSize.width() + 1, margin, shadow, shadow);
373 QRadialGradient g(rect.bottomLeft(), shadow - 1);
374 g.setColorAt(pos: 0, color: dark);
375 g.setColorAt(pos: 1, color: light);
376 p.fillRect(rect, g);
377 }
378
379 // bottom/left corner
380 {
381 const QRect rect(margin, margin + imageSize.height() + 1, shadow, shadow);
382 QRadialGradient g(rect.topRight(), shadow - 1);
383 g.setColorAt(pos: 0, color: dark);
384 g.setColorAt(pos: 1, color: light);
385 p.fillRect(rect, g);
386 }
387
388 p.end();
389
390 return QPixmap::fromImage(image: dest);
391}
392
393void NewFormWidget::loadFrom(const QString &path, bool resourceFile, const QString &uiExtension,
394 const QString &selectedItem, QTreeWidgetItem *&selectedItemFound)
395{
396 const QDir dir(path);
397
398 if (!dir.exists())
399 return;
400
401 // Iterate through the directory and add the templates
402 const QFileInfoList list = dir.entryInfoList(nameFilters: QStringList(QStringLiteral("*.") + uiExtension),
403 filters: QDir::Files);
404
405 if (list.isEmpty())
406 return;
407
408 const QChar separator = resourceFile ? QChar(QLatin1Char('/'))
409 : QDir::separator();
410 QTreeWidgetItem *root = new QTreeWidgetItem(m_ui->treeWidget);
411 root->setFlags(root->flags() & ~Qt::ItemIsSelectable);
412 // Try to get something that is easy to read.
413 QString visiblePath = path;
414 int index = visiblePath.lastIndexOf(c: separator);
415 if (index != -1) {
416 // try to find a second slash, just to be a bit better.
417 const int index2 = visiblePath.lastIndexOf(c: separator, from: index - 1);
418 if (index2 != -1)
419 index = index2;
420 visiblePath = visiblePath.mid(position: index + 1);
421 visiblePath = QDir::toNativeSeparators(pathName: visiblePath);
422 }
423
424 const QChar underscore = QLatin1Char('_');
425 const QChar blank = QLatin1Char(' ');
426 root->setText(column: 0, atext: visiblePath.replace(before: underscore, after: blank));
427 root->setToolTip(column: 0, atoolTip: path);
428
429 const QFileInfoList::const_iterator lcend = list.constEnd();
430 for (QFileInfoList::const_iterator it = list.constBegin(); it != lcend; ++it) {
431 if (!it->isFile())
432 continue;
433
434 QTreeWidgetItem *item = new QTreeWidgetItem(root);
435 const QString text = it->baseName().replace(before: underscore, after: blank);
436 if (selectedItemFound == nullptr && text == selectedItem)
437 selectedItemFound = item;
438 item->setText(column: 0, atext: text);
439 item->setData(column: 0, role: TemplateNameRole, value: it->absoluteFilePath());
440 }
441}
442
443void NewFormWidget::loadFrom(const QString &title, const QStringList &nameList,
444 const QString &selectedItem, QTreeWidgetItem *&selectedItemFound)
445{
446 if (nameList.isEmpty())
447 return;
448 QTreeWidgetItem *root = new QTreeWidgetItem(m_ui->treeWidget);
449 root->setFlags(root->flags() & ~Qt::ItemIsSelectable);
450 root->setText(column: 0, atext: title);
451 const QStringList::const_iterator cend = nameList.constEnd();
452 for (QStringList::const_iterator it = nameList.constBegin(); it != cend; ++it) {
453 const QString text = *it;
454 QTreeWidgetItem *item = new QTreeWidgetItem(root);
455 item->setText(column: 0, atext: text);
456 if (selectedItemFound == nullptr && text == selectedItem)
457 selectedItemFound = item;
458 item->setData(column: 0, role: ClassNameRole, value: *it);
459 }
460}
461
462void NewFormWidget::on_treeWidget_itemPressed(QTreeWidgetItem *item)
463{
464 if (item && !item->parent())
465 item->setExpanded(!item->isExpanded());
466}
467
468QSize NewFormWidget::templateSize() const
469{
470 return m_ui->sizeComboBox->itemData(index: m_ui->sizeComboBox->currentIndex()).toSize();
471}
472
473void NewFormWidget::setTemplateSize(const QSize &s)
474{
475 const int index = s.isNull() ? 0 : m_ui->sizeComboBox->findData(data: s);
476 if (index != -1)
477 m_ui->sizeComboBox->setCurrentIndex(index);
478}
479
480static QString readAll(const QString &fileName, QString *errorMessage)
481{
482 QFile file(fileName);
483 if (!file.open(flags: QIODevice::ReadOnly|QIODevice::Text)) {
484 *errorMessage = NewFormWidget::tr(s: "Unable to open the form template file '%1': %2").arg(args: fileName, args: file.errorString());
485 return QString();
486 }
487 return QString::fromUtf8(str: file.readAll());
488}
489
490QString NewFormWidget::itemToTemplate(const QTreeWidgetItem *item, QString *errorMessage) const
491{
492 const QSize size = templateSize();
493 // file name or string contents?
494 const QVariant templateFileName = item->data(column: 0, role: TemplateNameRole);
495 if (templateFileName.type() == QVariant::String) {
496 const QString fileName = templateFileName.toString();
497 // No fixed size: just open.
498 if (size.isNull())
499 return readAll(fileName, errorMessage);
500 // try to find a file matching the size, like "../640x480/xx.ui"
501 const QFileInfo fiBase(fileName);
502 QString sizeFileName;
503 QTextStream(&sizeFileName) << fiBase.path() << QDir::separator()
504 << size.width() << QLatin1Char('x') << size.height() << QDir::separator()
505 << fiBase.fileName();
506 if (QFileInfo(sizeFileName).isFile())
507 return readAll(fileName: sizeFileName, errorMessage);
508 // Nothing found, scale via DOM/temporary file
509 QString contents = readAll(fileName, errorMessage);
510 if (!contents.isEmpty())
511 contents = qdesigner_internal::WidgetDataBase::scaleFormTemplate(xml: contents, size, fixed: false);
512 return contents;
513 }
514 // Content.
515 const QString className = item->data(column: 0, role: ClassNameRole).toString();
516 QString contents = qdesigner_internal::WidgetDataBase::formTemplate(core: m_core, className, objectName: formName(className));
517 if (!size.isNull())
518 contents = qdesigner_internal::WidgetDataBase::scaleFormTemplate(xml: contents, size, fixed: false);
519 return contents;
520}
521
522void NewFormWidget::slotDeviceProfileIndexChanged(int idx)
523{
524 // Store index for form windows to take effect and refresh pixmap
525 QDesignerSharedSettings settings(m_core);
526 settings.setCurrentDeviceProfileIndex(idx - profileComboIndexOffset);
527 showCurrentItemPixmap();
528}
529
530int NewFormWidget::profileComboIndex() const
531{
532 return m_ui->profileComboBox->currentIndex();
533}
534
535qdesigner_internal::DeviceProfile NewFormWidget::currentDeviceProfile() const
536{
537 const int ci = profileComboIndex();
538 if (ci > 0)
539 return m_deviceProfiles.at(i: ci - profileComboIndexOffset);
540 return qdesigner_internal::DeviceProfile();
541}
542
543bool NewFormWidget::hasCurrentTemplate() const
544{
545 return m_currentItem != nullptr;
546}
547
548QString NewFormWidget::currentTemplateI(QString *ptrToErrorMessage)
549{
550 if (m_currentItem == nullptr) {
551 *ptrToErrorMessage = tr(s: "Internal error: No template selected.");
552 return QString();
553 }
554 const QString contents = itemToTemplate(item: m_currentItem, errorMessage: ptrToErrorMessage);
555 if (contents.isEmpty())
556 return contents;
557
558 m_acceptedItem = m_currentItem;
559 return contents;
560}
561
562QString NewFormWidget::currentTemplate(QString *ptrToErrorMessage)
563{
564 if (ptrToErrorMessage)
565 return currentTemplateI(ptrToErrorMessage);
566 // Do not loose the error
567 QString errorMessage;
568 const QString contents = currentTemplateI(ptrToErrorMessage: &errorMessage);
569 if (!errorMessage.isEmpty())
570 qWarning(msg: "%s", errorMessage.toUtf8().constData());
571 return contents;
572}
573
574}
575
576QT_END_NAMESPACE
577

source code of qttools/src/designer/src/lib/shared/newformwidget.cpp