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