Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
formlayoutmenu.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3// Qt-Security score:significant reason:default
4
6#include "layoutinfo_p.h"
10#include "ui_formlayoutrowdialog.h"
11
12#include <QtDesigner/abstractformwindow.h>
13#include <QtDesigner/abstractformeditor.h>
14#include <QtDesigner/abstractwidgetfactory.h>
15#include <QtDesigner/propertysheet.h>
16#include <QtDesigner/qextensionmanager.h>
17#include <QtDesigner/abstractwidgetdatabase.h>
18#include <QtDesigner/abstractlanguage.h>
19
20#include <QtWidgets/qwidget.h>
21#include <QtWidgets/qformlayout.h>
22#include <QtWidgets/qdialog.h>
23#include <QtWidgets/qpushbutton.h>
24
25#include <QtGui/qaction.h>
26#include <QtGui/qvalidator.h>
27#include <QtGui/qundostack.h>
28
29#include <QtCore/qcoreapplication.h>
30#include <QtCore/qregularexpression.h>
31#include <QtCore/qhash.h>
32#include <QtCore/qdebug.h>
33
34#include <utility>
35
37
38using namespace Qt::StringLiterals;
39
40static constexpr auto buddyPropertyC = "buddy"_L1;
41static const char *fieldWidgetBaseClasses[] = {
42 "QLineEdit", "QComboBox", "QSpinBox", "QDoubleSpinBox", "QCheckBox",
43 "QDateEdit", "QTimeEdit", "QDateTimeEdit", "QDial", "QWidget"
44};
45
46namespace qdesigner_internal {
47
48// Struct that describes a row of controls (descriptive label and control) to
49// be added to a form layout.
51 QString labelName;
52 QString labelText;
54 QString fieldName;
55 bool buddy{false};
56};
57
58// A Dialog to edit a FormLayoutRow. Lets the user input a label text, label
59// name, field widget type, field object name and buddy setting. As the
60// user types the label text; the object names to be used for label and field
61// are updated. It also checks the buddy setting depending on whether the
62// label text contains a buddy marker.
66public:
69
71
72 bool buddy() const;
73 void setBuddy(bool);
74
75 // Accessors for form layout row numbers using 0..[n-1] convention
76 int row() const;
77 void setRow(int);
78 void setRowRange(int, int);
79
80 QString fieldClass() const;
81 QString labelText() const;
82
83 static QStringList fieldWidgetClasses(QDesignerFormEditorInterface *core);
84
85private slots:
86 void labelTextEdited(const QString &text);
87 void labelNameEdited(const QString &text);
88 void fieldNameEdited(const QString &text);
89 void buddyClicked();
90 void fieldClassChanged(int);
91
92private:
93 bool isValid() const;
94 void updateObjectNames(bool updateLabel, bool updateField);
95 void updateOkButton();
96
97 // Check for buddy marker in string
98 const QRegularExpression m_buddyMarkerRegexp;
99
100 QT_PREPEND_NAMESPACE(Ui)::FormLayoutRowDialog m_ui;
101 bool m_labelNameEdited;
102 bool m_fieldNameEdited;
103 bool m_buddyClicked;
104};
105
106FormLayoutRowDialog::FormLayoutRowDialog(QDesignerFormEditorInterface *core,
107 QWidget *parent) :
108 QDialog(parent),
109 m_buddyMarkerRegexp(u"\\&[^&]"_s),
110 m_labelNameEdited(false),
111 m_fieldNameEdited(false),
112 m_buddyClicked(false)
113{
114 Q_ASSERT(m_buddyMarkerRegexp.isValid());
115
116 setModal(true);
117 m_ui.setupUi(this);
118 connect(m_ui.labelTextLineEdit, &QLineEdit::textEdited, this, &FormLayoutRowDialog::labelTextEdited);
119
120 auto *nameValidator = new QRegularExpressionValidator(QRegularExpression(u"^[a-zA-Z0-9_]+$"_s), this);
121 Q_ASSERT(nameValidator->regularExpression().isValid());
122
123 m_ui.labelNameLineEdit->setValidator(nameValidator);
124 connect(m_ui.labelNameLineEdit, &QLineEdit::textEdited,
125 this, &FormLayoutRowDialog::labelNameEdited);
126
127 m_ui.fieldNameLineEdit->setValidator(nameValidator);
128 connect(m_ui.fieldNameLineEdit, &QLineEdit::textEdited,
129 this, &FormLayoutRowDialog::fieldNameEdited);
130
131 connect(m_ui.buddyCheckBox, &QAbstractButton::clicked, this, &FormLayoutRowDialog::buddyClicked);
132
133 m_ui.fieldClassComboBox->addItems(fieldWidgetClasses(core));
134 m_ui.fieldClassComboBox->setCurrentIndex(0);
135 connect(m_ui.fieldClassComboBox,
136 &QComboBox::currentIndexChanged,
137 this, &FormLayoutRowDialog::fieldClassChanged);
138
139 updateOkButton();
140}
141
143{
144 FormLayoutRow rc;
145 rc.labelText = labelText();
146 rc.labelName = m_ui.labelNameLineEdit->text();
147 rc.fieldClassName = fieldClass();
148 rc.fieldName = m_ui.fieldNameLineEdit->text();
149 rc.buddy = buddy();
150 return rc;
151}
152
154{
155 return m_ui.buddyCheckBox->checkState() == Qt::Checked;
156}
157
159{
160 m_ui.buddyCheckBox->setCheckState(b ? Qt::Checked : Qt::Unchecked);
161}
162
163// Convert rows to 1..n convention for users
165{
166 return m_ui.rowSpinBox->value() - 1;
167}
168
170{
171 m_ui.rowSpinBox->setValue(row + 1);
172}
173
174void FormLayoutRowDialog::setRowRange(int from, int to)
175{
176 m_ui.rowSpinBox->setMinimum(from + 1);
177 m_ui.rowSpinBox->setMaximum(to + 1);
178 m_ui.rowSpinBox->setEnabled(to - from > 0);
179}
180
182{
183 return m_ui.fieldClassComboBox->itemText(m_ui.fieldClassComboBox->currentIndex());
184}
185
187{
188 return m_ui.labelTextLineEdit->text();
189}
190
191bool FormLayoutRowDialog::isValid() const
192{
193 // Check for non-empty names and presence of buddy marker if checked
194 const QString name = labelText();
195 if (name.isEmpty() || m_ui.labelNameLineEdit->text().isEmpty() || m_ui.fieldNameLineEdit->text().isEmpty())
196 return false;
197 if (buddy() && !name.contains(m_buddyMarkerRegexp))
198 return false;
199 return true;
200}
201
202void FormLayoutRowDialog::updateOkButton()
203{
204 m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isValid());
205}
206
207void FormLayoutRowDialog::labelTextEdited(const QString &text)
208{
209 updateObjectNames(true, true);
210 // Set buddy if '&' is present unless the user changed it
211 if (!m_buddyClicked)
212 setBuddy(text.contains(m_buddyMarkerRegexp));
213
214 updateOkButton();
215}
216
217// Get a suitable object name postfix from a class name:
218// "namespace::QLineEdit"->"LineEdit"
219static inline QString postFixFromClassName(QString className)
220{
221 const int index = className.lastIndexOf("::"_L1);
222 if (index != -1)
223 className.remove(0, index + 2);
224 if (className.size() > 2)
225 if (className.at(0) == u'Q' || className.at(0) == u'K')
226 if (className.at(1).isUpper())
227 className.remove(0, 1);
228 return className;
229}
230
231// Helper routines to filter out characters for converting texts into
232// class name prefixes. Only accepts ASCII characters/digits and underscores.
233
236
237static inline PrefixCharacterKind prefixCharacterKind(const QChar &c)
238{
239 switch (c.category()) {
240 case QChar::Number_DecimalDigit:
241 return PC_Digit;
242 case QChar::Letter_Lowercase: {
243 const char a = c.toLatin1();
244 if (a >= 'a' && a <= 'z')
245 return PC_LowerCaseLetter;
246 }
247 break;
248 case QChar::Letter_Uppercase: {
249 const char a = c.toLatin1();
250 if (a >= 'A' && a <= 'Z')
251 return PC_UpperCaseLetter;
252 }
253 break;
254 case QChar::Punctuation_Connector:
255 if (c.toLatin1() == '_')
256 return PC_Other;
257 break;
258 default:
259 break;
260 }
261 return PC_Invalid;
262}
263
264// Convert the text the user types into a usable class name prefix by filtering
265// characters, lower-casing the first character and camel-casing subsequent
266// words. ("zip code:") --> ("zipCode").
267
268static QString prefixFromLabel(const QString &prefix)
269{
270 QString rc;
271 bool lastWasAcceptable = false;
272 for (const QChar &c : prefix) {
273 const PrefixCharacterKind kind = prefixCharacterKind(c);
274 const bool acceptable = kind != PC_Invalid;
275 if (acceptable) {
276 if (rc.isEmpty()) {
277 // Lower-case first character
278 rc += kind == PC_UpperCaseLetter ? c.toLower() : c;
279 } else {
280 // Camel-case words
281 rc += !lastWasAcceptable && kind == PC_LowerCaseLetter ? c.toUpper() : c;
282 }
283 }
284 lastWasAcceptable = acceptable;
285 }
286 return rc;
287}
288
289void FormLayoutRowDialog::updateObjectNames(bool updateLabel, bool updateField)
290{
291 // Generate label + field object names from the label text, that is,
292 // "&Zip code:" -> "zipcodeLabel", "zipcodeLineEdit" unless the user
293 // edited it.
294 const bool doUpdateLabel = !m_labelNameEdited && updateLabel;
295 const bool doUpdateField = !m_fieldNameEdited && updateField;
296 if (!doUpdateLabel && !doUpdateField)
297 return;
298
299 const QString prefix = prefixFromLabel(labelText());
300 // Set names
301 if (doUpdateLabel)
302 m_ui.labelNameLineEdit->setText(prefix + "Label"_L1);
303 if (doUpdateField)
304 m_ui.fieldNameLineEdit->setText(prefix + postFixFromClassName(fieldClass()));
305}
306
307void FormLayoutRowDialog::fieldClassChanged(int)
308{
309 updateObjectNames(false, true);
310}
311
312void FormLayoutRowDialog::labelNameEdited(const QString & /*text*/)
313{
314 m_labelNameEdited = true; // stop auto-updating after user change
315 updateOkButton();
316}
317
318void FormLayoutRowDialog::fieldNameEdited(const QString & /*text*/)
319{
320 m_fieldNameEdited = true; // stop auto-updating after user change
321 updateOkButton();
322}
323
324void FormLayoutRowDialog::buddyClicked()
325{
326 m_buddyClicked = true; // stop auto-updating after user change
327 updateOkButton();
328}
329
330/* Create a list of classes suitable for field widgets. Take the fixed base
331 * classes provided and look in the widget database for custom widgets derived
332 * from them ("QLineEdit", "CustomLineEdit", "QComboBox"...). */
333QStringList FormLayoutRowDialog::fieldWidgetClasses(QDesignerFormEditorInterface *core)
334{
335 static QStringList rc;
336 if (rc.isEmpty()) {
337 // Turn known base classes into list
338 QStringList baseClasses;
339 for (auto fw : fieldWidgetBaseClasses)
340 baseClasses.append(QLatin1StringView(fw));
341 // Scan for custom widgets that inherit them and store them in a
342 // multimap of base class->custom widgets unless we have a language
343 // extension installed which might do funny things with custom widgets.
344 QMultiHash<QString, QString> customClassMap; // Base class -> custom widgets map
345 if (qt_extension<QDesignerLanguageExtension *>(core->extensionManager(), core) == nullptr) {
346 const QDesignerWidgetDataBaseInterface *wdb = core->widgetDataBase();
347 const int wdbCount = wdb->count();
348 for (int w = 0; w < wdbCount; ++w) {
349 // Check for non-container custom types that extend the
350 // respective base class.
351 const QDesignerWidgetDataBaseItemInterface *dbItem = wdb->item(w);
352 if (!dbItem->isPromoted() && !dbItem->isContainer() && dbItem->isCustom()) {
353 const int index = baseClasses.indexOf(dbItem->extends());
354 if (index != -1)
355 customClassMap.insert(baseClasses.at(index), dbItem->name());
356 }
357 }
358 }
359 // Compile final list, taking each base class and append custom widgets
360 // based on it.
361 for (const auto &baseClass : std::as_const(baseClasses)) {
362 rc.append(baseClass);
363 rc += customClassMap.values(baseClass);
364 }
365 }
366 return rc;
367}
368
369// ------------------ Utilities
370
371static QFormLayout *managedFormLayout(const QDesignerFormEditorInterface *core, const QWidget *w)
372{
373 QLayout *l = nullptr;
374 if (LayoutInfo::managedLayoutType(core, w, &l) == LayoutInfo::Form)
375 return qobject_cast<QFormLayout *>(l);
376 return nullptr;
377}
378
379// Create the widgets of a control row and apply text properties contained
380// in the struct, called by addFormLayoutRow()
381static std::pair<QWidget *,QWidget *>
383 QDesignerFormWindowInterface *formWindow)
384{
385 QDesignerFormEditorInterface *core = formWindow->core();
386 QDesignerWidgetFactoryInterface *wf = core->widgetFactory();
387
388 std::pair<QWidget *,QWidget *> rc{wf->createWidget(u"QLabel"_s, parent),
389 wf->createWidget(row.fieldClassName, parent)};
390 // Set up properties of the label
391 const QString objectNameProperty = u"objectName"_s;
392 QDesignerPropertySheetExtension *labelSheet = qt_extension<QDesignerPropertySheetExtension*>(core->extensionManager(), rc.first);
393 int nameIndex = labelSheet->indexOf(objectNameProperty);
394 labelSheet->setProperty(nameIndex, QVariant::fromValue(PropertySheetStringValue(row.labelName)));
395 labelSheet->setChanged(nameIndex, true);
396 formWindow->ensureUniqueObjectName(rc.first);
397 const int textIndex = labelSheet->indexOf(u"text"_s);
398 labelSheet->setProperty(textIndex, QVariant::fromValue(PropertySheetStringValue(row.labelText)));
399 labelSheet->setChanged(textIndex, true);
400 // Set up properties of the control
401 QDesignerPropertySheetExtension *controlSheet = qt_extension<QDesignerPropertySheetExtension*>(core->extensionManager(), rc.second);
402 nameIndex = controlSheet->indexOf(objectNameProperty);
403 controlSheet->setProperty(nameIndex, QVariant::fromValue(PropertySheetStringValue(row.fieldName)));
404 controlSheet->setChanged(nameIndex, true);
405 formWindow->ensureUniqueObjectName(rc.second);
406 return rc;
407}
408
409// Create a command sequence on the undo stack of the form window that creates
410// the widgets of the row and inserts them into the form layout.
411static void addFormLayoutRow(const FormLayoutRow &formLayoutRow, int row, QWidget *w,
412 QDesignerFormWindowInterface *formWindow)
413{
414 QFormLayout *formLayout = managedFormLayout(formWindow->core(), w);
415 Q_ASSERT(formLayout);
416 QUndoStack *undoStack = formWindow->commandHistory();
417 const QString macroName = QCoreApplication::translate("Command", "Add '%1' to '%2'").arg(formLayoutRow.labelText, formLayout->objectName());
418 undoStack->beginMacro(macroName);
419
420 // Create a list of widget insertion commands and pass them a cell position
421 const auto widgetPair = createWidgets(formLayoutRow, w, formWindow);
422
423 InsertWidgetCommand *labelCmd = new InsertWidgetCommand(formWindow);
424 labelCmd->init(widgetPair.first, false, row, 0);
425 undoStack->push(labelCmd);
426 InsertWidgetCommand *controlCmd = new InsertWidgetCommand(formWindow);
427 controlCmd->init(widgetPair.second, false, row, 1);
428 undoStack->push(controlCmd);
429 if (formLayoutRow.buddy) {
430 SetPropertyCommand *buddyCommand = new SetPropertyCommand(formWindow);
431 buddyCommand->init(widgetPair.first, buddyPropertyC, widgetPair.second->objectName());
432 undoStack->push(buddyCommand);
433 }
434 undoStack->endMacro();
435}
436
437// ---------------- FormLayoutMenu
440 m_separator1(new QAction(this)),
441 m_populateFormAction(new QAction(tr("Add form layout row..."), this)),
442 m_separator2(new QAction(this))
443{
447}
448
450{
451 switch (LayoutInfo::managedLayoutType(fw->core(), w)) {
452 case LayoutInfo::Form:
457 m_widget = w;
458 break;
459 default:
460 m_widget = nullptr;
461 break;
462 }
463}
464
466{
468 Q_ASSERT(m_widget && fw);
470
474
475 if (dialog.exec() != QDialog::Accepted)
476 return;
478}
479
488}
489
490QT_END_NAMESPACE
491
492#include "formlayoutmenu.moc"
friend class QWidget
Definition qpainter.h:432
static QStringList fieldWidgetClasses(QDesignerFormEditorInterface *core)
static const char * fieldWidgetBaseClasses[]
static constexpr auto buddyPropertyC
Combined button and popup list for selecting options.
Auxiliary methods to store/retrieve settings.
static void addFormLayoutRow(const FormLayoutRow &formLayoutRow, int row, QWidget *w, QDesignerFormWindowInterface *formWindow)
static QString postFixFromClassName(QString className)
static QFormLayout * managedFormLayout(const QDesignerFormEditorInterface *core, const QWidget *w)
static QString prefixFromLabel(const QString &prefix)
static std::pair< QWidget *, QWidget * > createWidgets(const FormLayoutRow &row, QWidget *parent, QDesignerFormWindowInterface *formWindow)
static PrefixCharacterKind prefixCharacterKind(const QChar &c)