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 "formlayoutmenu_p.h"
30#include "layoutinfo_p.h"
31#include "qdesigner_command_p.h"
32#include "qdesigner_utils_p.h"
33#include "qdesigner_propertycommand_p.h"
34#include "ui_formlayoutrowdialog.h"
35
36#include <QtDesigner/abstractformwindow.h>
37#include <QtDesigner/abstractformeditor.h>
38#include <QtDesigner/abstractwidgetfactory.h>
39#include <QtDesigner/propertysheet.h>
40#include <QtDesigner/qextensionmanager.h>
41#include <QtDesigner/abstractwidgetdatabase.h>
42#include <QtDesigner/abstractlanguage.h>
43
44#include <QtWidgets/qaction.h>
45#include <QtWidgets/qwidget.h>
46#include <QtWidgets/qformlayout.h>
47#include <QtWidgets/qundostack.h>
48#include <QtWidgets/qdialog.h>
49#include <QtWidgets/qpushbutton.h>
50#include <QtGui/qvalidator.h>
51
52#include <QtCore/qpair.h>
53#include <QtCore/qcoreapplication.h>
54#include <QtCore/qregularexpression.h>
55#include <QtCore/qhash.h>
56#include <QtCore/qdebug.h>
57
58static const char *buddyPropertyC = "buddy";
59static const char *fieldWidgetBaseClasses[] = {
60 "QLineEdit", "QComboBox", "QSpinBox", "QDoubleSpinBox", "QCheckBox",
61 "QDateEdit", "QTimeEdit", "QDateTimeEdit", "QDial", "QWidget"
62};
63
64QT_BEGIN_NAMESPACE
65
66namespace qdesigner_internal {
67
68// Struct that describes a row of controls (descriptive label and control) to
69// be added to a form layout.
70struct FormLayoutRow {
71 QString labelName;
72 QString labelText;
73 QString fieldClassName;
74 QString fieldName;
75 bool buddy{false};
76};
77
78// A Dialog to edit a FormLayoutRow. Lets the user input a label text, label
79// name, field widget type, field object name and buddy setting. As the
80// user types the label text; the object names to be used for label and field
81// are updated. It also checks the buddy setting depending on whether the
82// label text contains a buddy marker.
83class FormLayoutRowDialog : public QDialog {
84 Q_DISABLE_COPY_MOVE(FormLayoutRowDialog)
85 Q_OBJECT
86public:
87 explicit FormLayoutRowDialog(QDesignerFormEditorInterface *core,
88 QWidget *parent);
89
90 FormLayoutRow formLayoutRow() const;
91
92 bool buddy() const;
93 void setBuddy(bool);
94
95 // Accessors for form layout row numbers using 0..[n-1] convention
96 int row() const;
97 void setRow(int);
98 void setRowRange(int, int);
99
100 QString fieldClass() const;
101 QString labelText() const;
102
103 static QStringList fieldWidgetClasses(QDesignerFormEditorInterface *core);
104
105private slots:
106 void labelTextEdited(const QString &text);
107 void labelNameEdited(const QString &text);
108 void fieldNameEdited(const QString &text);
109 void buddyClicked();
110 void fieldClassChanged(int);
111
112private:
113 bool isValid() const;
114 void updateObjectNames(bool updateLabel, bool updateField);
115 void updateOkButton();
116
117 // Check for buddy marker in string
118 const QRegularExpression m_buddyMarkerRegexp;
119
120 Ui::FormLayoutRowDialog m_ui;
121 bool m_labelNameEdited;
122 bool m_fieldNameEdited;
123 bool m_buddyClicked;
124};
125
126FormLayoutRowDialog::FormLayoutRowDialog(QDesignerFormEditorInterface *core,
127 QWidget *parent) :
128 QDialog(parent),
129 m_buddyMarkerRegexp(QStringLiteral("\\&[^&]")),
130 m_labelNameEdited(false),
131 m_fieldNameEdited(false),
132 m_buddyClicked(false)
133{
134 Q_ASSERT(m_buddyMarkerRegexp.isValid());
135
136 setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
137 setModal(true);
138 m_ui.setupUi(this);
139 connect(sender: m_ui.labelTextLineEdit, signal: &QLineEdit::textEdited, receiver: this, slot: &FormLayoutRowDialog::labelTextEdited);
140
141 QRegularExpressionValidator *nameValidator = new QRegularExpressionValidator(QRegularExpression(QStringLiteral("^[a-zA-Z0-9_]+$")), this);
142 Q_ASSERT(nameValidator->regularExpression().isValid());
143
144 m_ui.labelNameLineEdit->setValidator(nameValidator);
145 connect(sender: m_ui.labelNameLineEdit, signal: &QLineEdit::textEdited,
146 receiver: this, slot: &FormLayoutRowDialog::labelNameEdited);
147
148 m_ui.fieldNameLineEdit->setValidator(nameValidator);
149 connect(sender: m_ui.fieldNameLineEdit, signal: &QLineEdit::textEdited,
150 receiver: this, slot: &FormLayoutRowDialog::fieldNameEdited);
151
152 connect(sender: m_ui.buddyCheckBox, signal: &QAbstractButton::clicked, receiver: this, slot: &FormLayoutRowDialog::buddyClicked);
153
154 m_ui.fieldClassComboBox->addItems(texts: fieldWidgetClasses(core));
155 m_ui.fieldClassComboBox->setCurrentIndex(0);
156 connect(sender: m_ui.fieldClassComboBox,
157 signal: QOverload<int>::of(ptr: &QComboBox::currentIndexChanged),
158 receiver: this, slot: &FormLayoutRowDialog::fieldClassChanged);
159
160 updateOkButton();
161}
162
163FormLayoutRow FormLayoutRowDialog::formLayoutRow() const
164{
165 FormLayoutRow rc;
166 rc.labelText = labelText();
167 rc.labelName = m_ui.labelNameLineEdit->text();
168 rc.fieldClassName = fieldClass();
169 rc.fieldName = m_ui.fieldNameLineEdit->text();
170 rc.buddy = buddy();
171 return rc;
172}
173
174bool FormLayoutRowDialog::buddy() const
175{
176 return m_ui.buddyCheckBox->checkState() == Qt::Checked;
177}
178
179void FormLayoutRowDialog::setBuddy(bool b)
180{
181 m_ui.buddyCheckBox->setCheckState(b ? Qt::Checked : Qt::Unchecked);
182}
183
184// Convert rows to 1..n convention for users
185int FormLayoutRowDialog::row() const
186{
187 return m_ui.rowSpinBox->value() - 1;
188}
189
190void FormLayoutRowDialog::setRow(int row)
191{
192 m_ui.rowSpinBox->setValue(row + 1);
193}
194
195void FormLayoutRowDialog::setRowRange(int from, int to)
196{
197 m_ui.rowSpinBox->setMinimum(from + 1);
198 m_ui.rowSpinBox->setMaximum(to + 1);
199 m_ui.rowSpinBox->setEnabled(to - from > 0);
200}
201
202QString FormLayoutRowDialog::fieldClass() const
203{
204 return m_ui.fieldClassComboBox->itemText(index: m_ui.fieldClassComboBox->currentIndex());
205}
206
207QString FormLayoutRowDialog::labelText() const
208{
209 return m_ui.labelTextLineEdit->text();
210}
211
212bool FormLayoutRowDialog::isValid() const
213{
214 // Check for non-empty names and presence of buddy marker if checked
215 const QString name = labelText();
216 if (name.isEmpty() || m_ui.labelNameLineEdit->text().isEmpty() || m_ui.fieldNameLineEdit->text().isEmpty())
217 return false;
218 if (buddy() && !name.contains(re: m_buddyMarkerRegexp))
219 return false;
220 return true;
221}
222
223void FormLayoutRowDialog::updateOkButton()
224{
225 m_ui.buttonBox->button(which: QDialogButtonBox::Ok)->setEnabled(isValid());
226}
227
228void FormLayoutRowDialog::labelTextEdited(const QString &text)
229{
230 updateObjectNames(updateLabel: true, updateField: true);
231 // Set buddy if '&' is present unless the user changed it
232 if (!m_buddyClicked)
233 setBuddy(text.contains(re: m_buddyMarkerRegexp));
234
235 updateOkButton();
236}
237
238// Get a suitable object name postfix from a class name:
239// "namespace::QLineEdit"->"LineEdit"
240static inline QString postFixFromClassName(QString className)
241{
242 const int index = className.lastIndexOf(QStringLiteral("::"));
243 if (index != -1)
244 className.remove(i: 0, len: index + 2);
245 if (className.size() > 2)
246 if (className.at(i: 0) == QLatin1Char('Q') || className.at(i: 0) == QLatin1Char('K'))
247 if (className.at(i: 1).isUpper())
248 className.remove(i: 0, len: 1);
249 return className;
250}
251
252// Helper routines to filter out characters for converting texts into
253// class name prefixes. Only accepts ASCII characters/digits and underscores.
254
255enum PrefixCharacterKind { PC_Digit, PC_UpperCaseLetter, PC_LowerCaseLetter,
256 PC_Other, PC_Invalid };
257
258static inline PrefixCharacterKind prefixCharacterKind(const QChar &c)
259{
260 switch (c.category()) {
261 case QChar::Number_DecimalDigit:
262 return PC_Digit;
263 case QChar::Letter_Lowercase: {
264 const char a = c.toLatin1();
265 if (a >= 'a' && a <= 'z')
266 return PC_LowerCaseLetter;
267 }
268 break;
269 case QChar::Letter_Uppercase: {
270 const char a = c.toLatin1();
271 if (a >= 'A' && a <= 'Z')
272 return PC_UpperCaseLetter;
273 }
274 break;
275 case QChar::Punctuation_Connector:
276 if (c.toLatin1() == '_')
277 return PC_Other;
278 break;
279 default:
280 break;
281 }
282 return PC_Invalid;
283}
284
285// Convert the text the user types into a usable class name prefix by filtering
286// characters, lower-casing the first character and camel-casing subsequent
287// words. ("zip code:") --> ("zipCode").
288
289static QString prefixFromLabel(const QString &prefix)
290{
291 QString rc;
292 const int length = prefix.size();
293 bool lastWasAcceptable = false;
294 for (int i = 0 ; i < length; i++) {
295 const QChar c = prefix.at(i);
296 const PrefixCharacterKind kind = prefixCharacterKind(c);
297 const bool acceptable = kind != PC_Invalid;
298 if (acceptable) {
299 if (rc.isEmpty()) {
300 // Lower-case first character
301 rc += kind == PC_UpperCaseLetter ? c.toLower() : c;
302 } else {
303 // Camel-case words
304 rc += !lastWasAcceptable && kind == PC_LowerCaseLetter ? c.toUpper() : c;
305 }
306 }
307 lastWasAcceptable = acceptable;
308 }
309 return rc;
310}
311
312void FormLayoutRowDialog::updateObjectNames(bool updateLabel, bool updateField)
313{
314 // Generate label + field object names from the label text, that is,
315 // "&Zip code:" -> "zipcodeLabel", "zipcodeLineEdit" unless the user
316 // edited it.
317 const bool doUpdateLabel = !m_labelNameEdited && updateLabel;
318 const bool doUpdateField = !m_fieldNameEdited && updateField;
319 if (!doUpdateLabel && !doUpdateField)
320 return;
321
322 const QString prefix = prefixFromLabel(prefix: labelText());
323 // Set names
324 if (doUpdateLabel)
325 m_ui.labelNameLineEdit->setText(prefix + QStringLiteral("Label"));
326 if (doUpdateField)
327 m_ui.fieldNameLineEdit->setText(prefix + postFixFromClassName(className: fieldClass()));
328}
329
330void FormLayoutRowDialog::fieldClassChanged(int)
331{
332 updateObjectNames(updateLabel: false, updateField: true);
333}
334
335void FormLayoutRowDialog::labelNameEdited(const QString & /*text*/)
336{
337 m_labelNameEdited = true; // stop auto-updating after user change
338 updateOkButton();
339}
340
341void FormLayoutRowDialog::fieldNameEdited(const QString & /*text*/)
342{
343 m_fieldNameEdited = true; // stop auto-updating after user change
344 updateOkButton();
345}
346
347void FormLayoutRowDialog::buddyClicked()
348{
349 m_buddyClicked = true; // stop auto-updating after user change
350 updateOkButton();
351}
352
353/* Create a list of classes suitable for field widgets. Take the fixed base
354 * classes provided and look in the widget database for custom widgets derived
355 * from them ("QLineEdit", "CustomLineEdit", "QComboBox"...). */
356QStringList FormLayoutRowDialog::fieldWidgetClasses(QDesignerFormEditorInterface *core)
357{
358 // Base class -> custom widgets map
359 typedef QMultiHash<QString, QString> ClassMap;
360
361 static QStringList rc;
362 if (rc.isEmpty()) {
363 // Turn known base classes into list
364 QStringList baseClasses;
365 for (auto fw : fieldWidgetBaseClasses)
366 baseClasses.append(t: QLatin1String(fw));
367 // Scan for custom widgets that inherit them and store them in a
368 // multimap of base class->custom widgets unless we have a language
369 // extension installed which might do funny things with custom widgets.
370 ClassMap customClassMap;
371 if (qt_extension<QDesignerLanguageExtension *>(manager: core->extensionManager(), object: core) == nullptr) {
372 const QDesignerWidgetDataBaseInterface *wdb = core->widgetDataBase();
373 const int wdbCount = wdb->count();
374 for (int w = 0; w < wdbCount; ++w) {
375 // Check for non-container custom types that extend the
376 // respective base class.
377 const QDesignerWidgetDataBaseItemInterface *dbItem = wdb->item(index: w);
378 if (!dbItem->isPromoted() && !dbItem->isContainer() && dbItem->isCustom()) {
379 const int index = baseClasses.indexOf(t: dbItem->extends());
380 if (index != -1)
381 customClassMap.insert(akey: baseClasses.at(i: index), avalue: dbItem->name());
382 }
383 }
384 }
385 // Compile final list, taking each base class and append custom widgets
386 // based on it.
387 for (const auto &baseClass : baseClasses) {
388 rc.append(t: baseClass);
389 rc += customClassMap.values(akey: baseClass);
390 }
391 }
392 return rc;
393}
394
395// ------------------ Utilities
396
397static QFormLayout *managedFormLayout(const QDesignerFormEditorInterface *core, const QWidget *w)
398{
399 QLayout *l = nullptr;
400 if (LayoutInfo::managedLayoutType(core, w, layout: &l) == LayoutInfo::Form)
401 return qobject_cast<QFormLayout *>(object: l);
402 return nullptr;
403}
404
405// Create the widgets of a control row and apply text properties contained
406// in the struct, called by addFormLayoutRow()
407static QPair<QWidget *,QWidget *>
408 createWidgets(const FormLayoutRow &row, QWidget *parent,
409 QDesignerFormWindowInterface *formWindow)
410{
411 QDesignerFormEditorInterface *core = formWindow->core();
412 QDesignerWidgetFactoryInterface *wf = core->widgetFactory();
413
414 QPair<QWidget *,QWidget *> rc = QPair<QWidget *,QWidget *>(wf->createWidget(QStringLiteral("QLabel"), parentWidget: parent),
415 wf->createWidget(name: row.fieldClassName, parentWidget: parent));
416 // Set up properties of the label
417 const QString objectNameProperty = QStringLiteral("objectName");
418 QDesignerPropertySheetExtension *labelSheet = qt_extension<QDesignerPropertySheetExtension*>(manager: core->extensionManager(), object: rc.first);
419 int nameIndex = labelSheet->indexOf(name: objectNameProperty);
420 labelSheet->setProperty(index: nameIndex, value: QVariant::fromValue(value: PropertySheetStringValue(row.labelName)));
421 labelSheet->setChanged(index: nameIndex, changed: true);
422 formWindow->ensureUniqueObjectName(object: rc.first);
423 const int textIndex = labelSheet->indexOf(QStringLiteral("text"));
424 labelSheet->setProperty(index: textIndex, value: QVariant::fromValue(value: PropertySheetStringValue(row.labelText)));
425 labelSheet->setChanged(index: textIndex, changed: true);
426 // Set up properties of the control
427 QDesignerPropertySheetExtension *controlSheet = qt_extension<QDesignerPropertySheetExtension*>(manager: core->extensionManager(), object: rc.second);
428 nameIndex = controlSheet->indexOf(name: objectNameProperty);
429 controlSheet->setProperty(index: nameIndex, value: QVariant::fromValue(value: PropertySheetStringValue(row.fieldName)));
430 controlSheet->setChanged(index: nameIndex, changed: true);
431 formWindow->ensureUniqueObjectName(object: rc.second);
432 return rc;
433}
434
435// Create a command sequence on the undo stack of the form window that creates
436// the widgets of the row and inserts them into the form layout.
437static void addFormLayoutRow(const FormLayoutRow &formLayoutRow, int row, QWidget *w,
438 QDesignerFormWindowInterface *formWindow)
439{
440 QFormLayout *formLayout = managedFormLayout(core: formWindow->core(), w);
441 Q_ASSERT(formLayout);
442 QUndoStack *undoStack = formWindow->commandHistory();
443 const QString macroName = QCoreApplication::translate(context: "Command", key: "Add '%1' to '%2'").arg(args: formLayoutRow.labelText, args: formLayout->objectName());
444 undoStack->beginMacro(text: macroName);
445
446 // Create a list of widget insertion commands and pass them a cell position
447 const QPair<QWidget *,QWidget *> widgetPair = createWidgets(row: formLayoutRow, parent: w, formWindow);
448
449 InsertWidgetCommand *labelCmd = new InsertWidgetCommand(formWindow);
450 labelCmd->init(widget: widgetPair.first, already_in_form: false, layoutRow: row, layoutColumn: 0);
451 undoStack->push(cmd: labelCmd);
452 InsertWidgetCommand *controlCmd = new InsertWidgetCommand(formWindow);
453 controlCmd->init(widget: widgetPair.second, already_in_form: false, layoutRow: row, layoutColumn: 1);
454 undoStack->push(cmd: controlCmd);
455 if (formLayoutRow.buddy) {
456 SetPropertyCommand *buddyCommand = new SetPropertyCommand(formWindow);
457 buddyCommand->init(object: widgetPair.first, propertyName: QLatin1String(buddyPropertyC), newValue: widgetPair.second->objectName());
458 undoStack->push(cmd: buddyCommand);
459 }
460 undoStack->endMacro();
461}
462
463// ---------------- FormLayoutMenu
464FormLayoutMenu::FormLayoutMenu(QObject *parent) :
465 QObject(parent),
466 m_separator1(new QAction(this)),
467 m_populateFormAction(new QAction(tr(s: "Add form layout row..."), this)),
468 m_separator2(new QAction(this))
469{
470 m_separator1->setSeparator(true);
471 connect(sender: m_populateFormAction, signal: &QAction::triggered, receiver: this, slot: &FormLayoutMenu::slotAddRow);
472 m_separator2->setSeparator(true);
473}
474
475void FormLayoutMenu::populate(QWidget *w, QDesignerFormWindowInterface *fw, ActionList &actions)
476{
477 switch (LayoutInfo::managedLayoutType(core: fw->core(), w)) {
478 case LayoutInfo::Form:
479 if (!actions.isEmpty() && !actions.constLast()->isSeparator())
480 actions.push_back(t: m_separator1);
481 actions.push_back(t: m_populateFormAction);
482 actions.push_back(t: m_separator2);
483 m_widget = w;
484 break;
485 default:
486 m_widget = nullptr;
487 break;
488 }
489}
490
491void FormLayoutMenu::slotAddRow()
492{
493 QDesignerFormWindowInterface *fw = QDesignerFormWindowInterface::findFormWindow(w: m_widget);
494 Q_ASSERT(m_widget && fw);
495 const int rowCount = managedFormLayout(core: fw->core(), w: m_widget)->rowCount();
496
497 FormLayoutRowDialog dialog(fw->core(), fw);
498 dialog.setRowRange(from: 0, to: rowCount);
499 dialog.setRow(rowCount);
500
501 if (dialog.exec() != QDialog::Accepted)
502 return;
503 addFormLayoutRow(formLayoutRow: dialog.formLayoutRow(), row: dialog.row(), w: m_widget, formWindow: fw);
504}
505
506QAction *FormLayoutMenu::preferredEditAction(QWidget *w, QDesignerFormWindowInterface *fw)
507{
508 if (LayoutInfo::managedLayoutType(core: fw->core(), w) == LayoutInfo::Form) {
509 m_widget = w;
510 return m_populateFormAction;
511 }
512 return nullptr;
513}
514}
515
516QT_END_NAMESPACE
517
518#include "formlayoutmenu.moc"
519
520

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