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
58 static const char *buddyPropertyC = "buddy";
59 static const char *fieldWidgetBaseClasses[] = {
60 "QLineEdit", "QComboBox", "QSpinBox", "QDoubleSpinBox", "QCheckBox",
61 "QDateEdit", "QTimeEdit", "QDateTimeEdit", "QDial", "QWidget"
62 };
63
64 QT_BEGIN_NAMESPACE
65
66 namespace qdesigner_internal {
67
68 // Struct that describes a row of controls (descriptive label and control) to
69 // be added to a form layout.
70 struct 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.
83 class FormLayoutRowDialog : public QDialog {
84 Q_DISABLE_COPY_MOVE(FormLayoutRowDialog)
85 Q_OBJECT
86 public:
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
105 private 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
112 private:
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
FormLayoutRowDialog(QDesignerFormEditorInterface * core,QWidget * parent)126 FormLayoutRowDialog::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(m_ui.labelTextLineEdit, &QLineEdit::textEdited, this, &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(m_ui.labelNameLineEdit, &QLineEdit::textEdited,
146 this, &FormLayoutRowDialog::labelNameEdited);
147
148 m_ui.fieldNameLineEdit->setValidator(nameValidator);
149 connect(m_ui.fieldNameLineEdit, &QLineEdit::textEdited,
150 this, &FormLayoutRowDialog::fieldNameEdited);
151
152 connect(m_ui.buddyCheckBox, &QAbstractButton::clicked, this, &FormLayoutRowDialog::buddyClicked);
153
154 m_ui.fieldClassComboBox->addItems(fieldWidgetClasses(core));
155 m_ui.fieldClassComboBox->setCurrentIndex(0);
156 connect(m_ui.fieldClassComboBox,
157 QOverload<int>::of(&QComboBox::currentIndexChanged),
158 this, &FormLayoutRowDialog::fieldClassChanged);
159
160 updateOkButton();
161 }
162
formLayoutRow() const163 FormLayoutRow 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
buddy() const174 bool FormLayoutRowDialog::buddy() const
175 {
176 return m_ui.buddyCheckBox->checkState() == Qt::Checked;
177 }
178
setBuddy(bool b)179 void 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
row() const185 int FormLayoutRowDialog::row() const
186 {
187 return m_ui.rowSpinBox->value() - 1;
188 }
189
setRow(int row)190 void FormLayoutRowDialog::setRow(int row)
191 {
192 m_ui.rowSpinBox->setValue(row + 1);
193 }
194
setRowRange(int from,int to)195 void 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
fieldClass() const202 QString FormLayoutRowDialog::fieldClass() const
203 {
204 return m_ui.fieldClassComboBox->itemText(m_ui.fieldClassComboBox->currentIndex());
205 }
206
labelText() const207 QString FormLayoutRowDialog::labelText() const
208 {
209 return m_ui.labelTextLineEdit->text();
210 }
211
isValid() const212 bool 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(m_buddyMarkerRegexp))
219 return false;
220 return true;
221 }
222
updateOkButton()223 void FormLayoutRowDialog::updateOkButton()
224 {
225 m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isValid());
226 }
227
labelTextEdited(const QString & text)228 void FormLayoutRowDialog::labelTextEdited(const QString &text)
229 {
230 updateObjectNames(true, true);
231 // Set buddy if '&' is present unless the user changed it
232 if (!m_buddyClicked)
233 setBuddy(text.contains(m_buddyMarkerRegexp));
234
235 updateOkButton();
236 }
237
238 // Get a suitable object name postfix from a class name:
239 // "namespace::QLineEdit"->"LineEdit"
postFixFromClassName(QString className)240 static inline QString postFixFromClassName(QString className)
241 {
242 const int index = className.lastIndexOf(QStringLiteral("::"));
243 if (index != -1)
244 className.remove(0, index + 2);
245 if (className.size() > 2)
246 if (className.at(0) == QLatin1Char('Q') || className.at(0) == QLatin1Char('K'))
247 if (className.at(1).isUpper())
248 className.remove(0, 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
255 enum PrefixCharacterKind { PC_Digit, PC_UpperCaseLetter, PC_LowerCaseLetter,
256 PC_Other, PC_Invalid };
257
prefixCharacterKind(const QChar & c)258 static 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
prefixFromLabel(const QString & prefix)289 static 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
updateObjectNames(bool updateLabel,bool updateField)312 void 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(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(fieldClass()));
328 }
329
fieldClassChanged(int)330 void FormLayoutRowDialog::fieldClassChanged(int)
331 {
332 updateObjectNames(false, true);
333 }
334
labelNameEdited(const QString &)335 void FormLayoutRowDialog::labelNameEdited(const QString & /*text*/)
336 {
337 m_labelNameEdited = true; // stop auto-updating after user change
338 updateOkButton();
339 }
340
fieldNameEdited(const QString &)341 void FormLayoutRowDialog::fieldNameEdited(const QString & /*text*/)
342 {
343 m_fieldNameEdited = true; // stop auto-updating after user change
344 updateOkButton();
345 }
346
buddyClicked()347 void 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"...). */
fieldWidgetClasses(QDesignerFormEditorInterface * core)356 QStringList 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(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 *>(core->extensionManager(), 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(w);
378 if (!dbItem->isPromoted() && !dbItem->isContainer() && dbItem->isCustom()) {
379 const int index = baseClasses.indexOf(dbItem->extends());
380 if (index != -1)
381 customClassMap.insert(baseClasses.at(index), 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(baseClass);
389 rc += customClassMap.values(baseClass);
390 }
391 }
392 return rc;
393 }
394
395 // ------------------ Utilities
396
managedFormLayout(const QDesignerFormEditorInterface * core,const QWidget * w)397 static QFormLayout *managedFormLayout(const QDesignerFormEditorInterface *core, const QWidget *w)
398 {
399 QLayout *l = nullptr;
400 if (LayoutInfo::managedLayoutType(core, w, &l) == LayoutInfo::Form)
401 return qobject_cast<QFormLayout *>(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()
407 static QPair<QWidget *,QWidget *>
createWidgets(const FormLayoutRow & row,QWidget * parent,QDesignerFormWindowInterface * formWindow)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"), parent),
415 wf->createWidget(row.fieldClassName, parent));
416 // Set up properties of the label
417 const QString objectNameProperty = QStringLiteral("objectName");
418 QDesignerPropertySheetExtension *labelSheet = qt_extension<QDesignerPropertySheetExtension*>(core->extensionManager(), rc.first);
419 int nameIndex = labelSheet->indexOf(objectNameProperty);
420 labelSheet->setProperty(nameIndex, QVariant::fromValue(PropertySheetStringValue(row.labelName)));
421 labelSheet->setChanged(nameIndex, true);
422 formWindow->ensureUniqueObjectName(rc.first);
423 const int textIndex = labelSheet->indexOf(QStringLiteral("text"));
424 labelSheet->setProperty(textIndex, QVariant::fromValue(PropertySheetStringValue(row.labelText)));
425 labelSheet->setChanged(textIndex, true);
426 // Set up properties of the control
427 QDesignerPropertySheetExtension *controlSheet = qt_extension<QDesignerPropertySheetExtension*>(core->extensionManager(), rc.second);
428 nameIndex = controlSheet->indexOf(objectNameProperty);
429 controlSheet->setProperty(nameIndex, QVariant::fromValue(PropertySheetStringValue(row.fieldName)));
430 controlSheet->setChanged(nameIndex, true);
431 formWindow->ensureUniqueObjectName(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.
addFormLayoutRow(const FormLayoutRow & formLayoutRow,int row,QWidget * w,QDesignerFormWindowInterface * formWindow)437 static void addFormLayoutRow(const FormLayoutRow &formLayoutRow, int row, QWidget *w,
438 QDesignerFormWindowInterface *formWindow)
439 {
440 QFormLayout *formLayout = managedFormLayout(formWindow->core(), w);
441 Q_ASSERT(formLayout);
442 QUndoStack *undoStack = formWindow->commandHistory();
443 const QString macroName = QCoreApplication::translate("Command", "Add '%1' to '%2'").arg(formLayoutRow.labelText, formLayout->objectName());
444 undoStack->beginMacro(macroName);
445
446 // Create a list of widget insertion commands and pass them a cell position
447 const QPair<QWidget *,QWidget *> widgetPair = createWidgets(formLayoutRow, w, formWindow);
448
449 InsertWidgetCommand *labelCmd = new InsertWidgetCommand(formWindow);
450 labelCmd->init(widgetPair.first, false, row, 0);
451 undoStack->push(labelCmd);
452 InsertWidgetCommand *controlCmd = new InsertWidgetCommand(formWindow);
453 controlCmd->init(widgetPair.second, false, row, 1);
454 undoStack->push(controlCmd);
455 if (formLayoutRow.buddy) {
456 SetPropertyCommand *buddyCommand = new SetPropertyCommand(formWindow);
457 buddyCommand->init(widgetPair.first, QLatin1String(buddyPropertyC), widgetPair.second->objectName());
458 undoStack->push(buddyCommand);
459 }
460 undoStack->endMacro();
461 }
462
463 // ---------------- FormLayoutMenu
FormLayoutMenu(QObject * parent)464 FormLayoutMenu::FormLayoutMenu(QObject *parent) :
465 QObject(parent),
466 m_separator1(new QAction(this)),
467 m_populateFormAction(new QAction(tr("Add form layout row..."), this)),
468 m_separator2(new QAction(this))
469 {
470 m_separator1->setSeparator(true);
471 connect(m_populateFormAction, &QAction::triggered, this, &FormLayoutMenu::slotAddRow);
472 m_separator2->setSeparator(true);
473 }
474
populate(QWidget * w,QDesignerFormWindowInterface * fw,ActionList & actions)475 void FormLayoutMenu::populate(QWidget *w, QDesignerFormWindowInterface *fw, ActionList &actions)
476 {
477 switch (LayoutInfo::managedLayoutType(fw->core(), w)) {
478 case LayoutInfo::Form:
479 if (!actions.isEmpty() && !actions.constLast()->isSeparator())
480 actions.push_back(m_separator1);
481 actions.push_back(m_populateFormAction);
482 actions.push_back(m_separator2);
483 m_widget = w;
484 break;
485 default:
486 m_widget = nullptr;
487 break;
488 }
489 }
490
slotAddRow()491 void FormLayoutMenu::slotAddRow()
492 {
493 QDesignerFormWindowInterface *fw = QDesignerFormWindowInterface::findFormWindow(m_widget);
494 Q_ASSERT(m_widget && fw);
495 const int rowCount = managedFormLayout(fw->core(), m_widget)->rowCount();
496
497 FormLayoutRowDialog dialog(fw->core(), fw);
498 dialog.setRowRange(0, rowCount);
499 dialog.setRow(rowCount);
500
501 if (dialog.exec() != QDialog::Accepted)
502 return;
503 addFormLayoutRow(dialog.formLayoutRow(), dialog.row(), m_widget, fw);
504 }
505
preferredEditAction(QWidget * w,QDesignerFormWindowInterface * fw)506 QAction *FormLayoutMenu::preferredEditAction(QWidget *w, QDesignerFormWindowInterface *fw)
507 {
508 if (LayoutInfo::managedLayoutType(fw->core(), w) == LayoutInfo::Form) {
509 m_widget = w;
510 return m_populateFormAction;
511 }
512 return nullptr;
513 }
514 }
515
516 QT_END_NAMESPACE
517
518 #include "formlayoutmenu.moc"
519
520