Kirigami2

FormLayout.qml
1 /*
2  * SPDX-FileCopyrightText: 2017 Marco Martin <[email protected]>
3  * SPDX-FileCopyrightText: 2022 ivan tkachenko <[email protected]>
4  *
5  * SPDX-License-Identifier: LGPL-2.0-or-later
6  */
7 
8 import QtQuick 2.15
9 import QtQuick.Layouts 1.15
10 import QtQuick.Controls 2.15 as QQC2
11 import org.kde.kirigami 2.18 as Kirigami
12 
13 /**
14  * This is the base class for Form layouts conforming to the
15  * Kirigami Human Interface Guidelines. The layout consists
16  * of two columns: the left column contains only right-aligned
17  * labels provided by a Kirigami.FormData attached property,
18  * the right column contains left-aligned child types.
19  *
20  * Child types can be sectioned using an QtQuick.Item
21  * or Kirigami.Separator with a Kirigami.FormData
22  * attached property, see FormLayoutAttached::isSection for details.
23  *
24  * Example usage:
25  * @code
26  * import org.kde.kirigami 2.3 as Kirigami
27  * Kirigami.FormLayout {
28  * TextField {
29  * Kirigami.FormData.label: "Label:"
30  * }
31  * Kirigami.Separator {
32  * Kirigami.FormData.label: "Section Title"
33  * Kirigami.FormData.isSection: true
34  * }
35  * TextField {
36  * Kirigami.FormData.label: "Label:"
37  * }
38  * TextField {
39  * }
40  * }
41  * @endcode
42  * @see FormLayoutAttached
43  * @since 2.3
44  * @inherit QtQuick.Item
45  */
46 Item {
47  id: root
48 
49  /**
50  * @brief This property tells whether the form layout is in wide mode.
51  *
52  * If true, the layout will be optimized for a wide screen, such as
53  * a desktop machine (the labels will be on a left column,
54  * the fields on a right column beside it), if false (such as on a phone)
55  * everything is laid out in a single column.
56  *
57  * By default this property automatically adjusts the layout
58  * if there is enough screen space.
59  *
60  * Set this to true for a convergent design,
61  * set this to false for a mobile-only design.
62  */
63  property bool wideMode: width >= lay.wideImplicitWidth
64 
65  /**
66  * If for some implementation reason multiple FormLayouts have to appear
67  * on the same page, they can have each other in twinFormLayouts,
68  * so they will vertically align with each other perfectly
69  *
70  * @since 5.53
71  */
72  property list<Item> twinFormLayouts // should be list<FormLayout> but we can't have a recursive declaration
73 
74  onTwinFormLayoutsChanged: {
75  for (const i in twinFormLayouts) {
76  if (!(root in twinFormLayouts[i].children[0].reverseTwins)) {
77  twinFormLayouts[i].children[0].reverseTwins.push(root)
78  Qt.callLater(() => twinFormLayouts[i].children[0].reverseTwinsChanged());
79  }
80  }
81  }
82 
83  Component.onCompleted: {
84  relayoutTimer.triggered();
85  }
86 
87  Component.onDestruction: {
88  for (const i in twinFormLayouts) {
89  const twin = twinFormLayouts[i];
90  const child = twin.children[0];
91  child.reverseTwins = child.reverseTwins.filter(value => value !== root);
92  }
93  }
94 
95  implicitWidth: lay.wideImplicitWidth
96  implicitHeight: lay.implicitHeight
97  Layout.preferredHeight: lay.implicitHeight
98  Layout.fillWidth: true
99  Accessible.role: Accessible.Form
100 
101  GridLayout {
102  id: lay
103  property int wideImplicitWidth
104  columns: root.wideMode ? 2 : 1
105  rowSpacing: Kirigami.Units.smallSpacing
106  columnSpacing: Kirigami.Units.smallSpacing
107  width: root.wideMode ? undefined : root.width
108  anchors {
109  horizontalCenter: root.wideMode ? root.horizontalCenter : undefined
110  left: root.wideMode ? undefined : root.left
111  }
112 
113  property var reverseTwins: []
114  property var knownItems: []
115  property var buddies: []
116  property int knownItemsImplicitWidth: {
117  let hint = 0;
118  for (const i in knownItems) {
119  const item = knownItems[i];
120  if (typeof item.Layout === "undefined") {
121  // Items may have been dynamically destroyed. Even
122  // printing such zombie wrappers results in a
123  // meaningless "TypeError: Type error". Normally they
124  // should be cleaned up from the array, but it would
125  // trigger a binding loop if done here.
126  //
127  // This is, so far, the only way to detect them.
128  continue;
129  }
130  const actualWidth = item.Layout.preferredWidth > 0
131  ? item.Layout.preferredWidth
132  : item.implicitWidth;
133 
134  hint = Math.max(hint, item.Layout.minimumWidth, Math.min(actualWidth, item.Layout.maximumWidth));
135  }
136  return hint;
137  }
138  property int buddiesImplicitWidth: {
139  let hint = 0;
140 
141  for (const i in buddies) {
142  if (buddies[i].visible && buddies[i].item !== null && !buddies[i].item.Kirigami.FormData.isSection) {
143  hint = Math.max(hint, buddies[i].implicitWidth);
144  }
145  }
146  return hint;
147  }
148  readonly property var actualTwinFormLayouts: {
149  // We need to copy that array by value
150  const list = lay.reverseTwins.slice();
151  for (const i in twinFormLayouts) {
152  const parentLay = twinFormLayouts[i];
153  if (!parentLay || !parentLay.hasOwnProperty("children")) {
154  continue;
155  }
156  list.push(parentLay);
157  for (const j in parentLay.children[0].reverseTwins) {
158  const childLay = parentLay.children[0].reverseTwins[j];
159  if (childLay && !(childLay in list)) {
160  list.push(childLay);
161  }
162  }
163  }
164  return list;
165  }
166 
167  Timer {
168  id: hintCompression
169  interval: 0
170  onTriggered: {
171  if (root.wideMode) {
172  lay.wideImplicitWidth = lay.implicitWidth;
173  }
174  }
175  }
176  onImplicitWidthChanged: hintCompression.restart();
177  //This invisible row is used to sync alignment between multiple layouts
178 
179  Item {
180  Layout.preferredWidth: {
181  let hint = lay.buddiesImplicitWidth;
182  for (const i in lay.actualTwinFormLayouts) {
183  if (lay.actualTwinFormLayouts[i] && lay.actualTwinFormLayouts[i].hasOwnProperty("children")) {
184  hint = Math.max(hint, lay.actualTwinFormLayouts[i].children[0].buddiesImplicitWidth);
185  }
186  }
187  return hint;
188  }
189  Layout.preferredHeight: 2
190  }
191  Item {
192  Layout.preferredWidth: {
193  let hint = Math.min(root.width, lay.knownItemsImplicitWidth);
194  for (const i in lay.actualTwinFormLayouts) {
195  if (lay.actualTwinFormLayouts[i] && lay.actualTwinFormLayouts[i].hasOwnProperty("children")) {
196  hint = Math.max(hint, lay.actualTwinFormLayouts[i].children[0].knownItemsImplicitWidth);
197  }
198  }
199  return hint;
200  }
201  Layout.preferredHeight: 2
202  }
203  }
204 
205  Item {
206  id: temp
207 
208  /**
209  * The following two functions are used in the label buddy items.
210  *
211  * They're in this mostly unused item to keep them private to the FormLayout
212  * without creating another QObject.
213  *
214  * Normally, such complex things in bindings are kinda bad for performance
215  * but this is a fairly static property. If for some reason an application
216  * decides to obsessively change its alignment, V8's JIT hotspot optimisations
217  * will kick in.
218  */
219 
220  /**
221  * @param {Item} item
222  * @returns {Qt::Alignment}
223  */
224  function effectiveLayout(item) {
225  if (!item) {
226  return 0;
227  }
228  const verticalAlignment =
229  item.Kirigami.FormData.labelAlignment !== 0
230  ? item.Kirigami.FormData.labelAlignment
231  : Qt.AlignTop;
232 
233  if (item.Kirigami.FormData.isSection) {
234  return Qt.AlignHCenter;
235  }
236  if (root.wideMode) {
237  return Qt.AlignRight | verticalAlignment;
238  }
239  return Qt.AlignLeft | Qt.AlignBottom;
240  }
241 
242  /**
243  * @param {Item} item
244  * @returns vertical alignment of the item passed as an argument.
245  */
246  function effectiveTextLayout(item) {
247  if (!item) {
248  return 0;
249  }
250  if (root.wideMode) {
251  return item.Kirigami.FormData.labelAlignment !== 0 ? item.Kirigami.FormData.labelAlignment : Text.AlignVCenter;
252  }
253  return Text.AlignBottom;
254  }
255  }
256 
257  Timer {
258  id: relayoutTimer
259  interval: 0
260  onTriggered: {
261  const __items = root.children;
262  // exclude the layout and temp
263  for (let i = 2; i < __items.length; ++i) {
264  const item = __items[i];
265 
266  // skip items that are already there
267  if (lay.knownItems.indexOf(item) !== -1 || item instanceof Repeater) {
268  continue;
269  }
270  lay.knownItems.push(item);
271 
272  const itemContainer = itemComponent.createObject(temp, { item });
273 
274  // if it's a labeled section header, add extra spacing before it
275  if (item.Kirigami.FormData.label.length > 0 && item.Kirigami.FormData.isSection) {
276  placeHolderComponent.createObject(lay, { item });
277  }
278 
279  const buddy = item.Kirigami.FormData.checkable
280  ? checkableBuddyComponent.createObject(lay, { item })
281  : buddyComponent.createObject(lay, { item, index: i - 2 });
282 
283  itemContainer.parent = lay;
284  lay.buddies.push(buddy);
285  }
286  lay.knownItemsChanged();
287  lay.buddiesChanged();
288  hintCompression.triggered();
289  }
290  }
291 
292  onChildrenChanged: relayoutTimer.restart();
293 
294  Component {
295  id: itemComponent
296  Item {
297  id: container
298 
299  property Item item
300 
301  enabled: item !== null && item.enabled
302  visible: item !== null && item.visible
303 
304  // NOTE: work around a GridLayout quirk which doesn't lay out items with null size hints causing things to be laid out incorrectly in some cases
305  implicitWidth: item !== null ? Math.max(item.implicitWidth, 1) : 0
306  implicitHeight: item !== null ? Math.max(item.implicitHeight, 1) : 0
307  Layout.preferredWidth: item !== null ? Math.max(1, item.Layout.preferredWidth > 0 ? item.Layout.preferredWidth : Math.ceil(item.implicitWidth)) : 0
308  Layout.preferredHeight: item !== null ? Math.max(1, item.Layout.preferredHeight > 0 ? item.Layout.preferredHeight : Math.ceil(item.implicitHeight)) : 0
309 
310  Layout.minimumWidth: item !== null ? item.Layout.minimumWidth : 0
311  Layout.minimumHeight: item !== null ? item.Layout.minimumHeight : 0
312 
313  Layout.maximumWidth: item !== null ? item.Layout.maximumWidth : 0
314  Layout.maximumHeight: item !== null ? item.Layout.maximumHeight : 0
315 
316  Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
317  Layout.fillWidth: item !== null && (item instanceof TextInput || item.Layout.fillWidth || item.Kirigami.FormData.isSection)
318  Layout.columnSpan: item !== null && item.Kirigami.FormData.isSection ? lay.columns : 1
319  onItemChanged: {
320  if (!item) {
321  container.destroy();
322  }
323  }
324  onXChanged: if (item !== null) { item.x = x + lay.x; }
325  // Assume lay.y is always 0
326  onYChanged: if (item !== null) { item.y = y + lay.y; }
327  onWidthChanged: if (item !== null) { item.width = width; }
328  Component.onCompleted: item.x = x + lay.x;
329  Connections {
330  target: lay
331  function onXChanged() {
332  if (item !== null) {
333  item.x = x + lay.x;
334  }
335  }
336  }
337  }
338  }
339  Component {
340  id: placeHolderComponent
341  Item {
342  property Item item
343 
344  enabled: item !== null && item.enabled
345  visible: item !== null && item.visible
346 
347  width: Kirigami.Units.smallSpacing
348  height: Kirigami.Units.smallSpacing
349  Layout.topMargin: item !== null && item.height > 0 ? Kirigami.Units.smallSpacing : 0
350  onItemChanged: {
351  if (!item) {
352  labelItem.destroy();
353  }
354  }
355  }
356  }
357  Component {
358  id: buddyComponent
359  Kirigami.Heading {
360  id: labelItem
361 
362  property Item item
363  property int index
364 
365  enabled: item !== null && item.enabled && item.Kirigami.FormData.enabled
366  visible: item !== null && item.visible && (root.wideMode || text.length > 0)
367  Kirigami.MnemonicData.enabled: item !== null && item.Kirigami.FormData.buddyFor && item.Kirigami.FormData.buddyFor.activeFocusOnTab
368  Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.FormLabel
369  Kirigami.MnemonicData.label: item !== null ? item.Kirigami.FormData.label : ""
370  text: Kirigami.MnemonicData.richTextLabel
371  type: item !== null && item.Kirigami.FormData.isSection ? Kirigami.Heading.Type.Primary : Kirigami.Heading.Type.Normal
372 
373  level: item !== null && item.Kirigami.FormData.isSection ? 3 : 5
374 
375  Layout.columnSpan: item !== null && item.Kirigami.FormData.isSection ? lay.columns : 1
376  Layout.preferredHeight: {
377  if (!item) {
378  return 0;
379  }
380  if (item.Kirigami.FormData.label.length > 0) {
381  if (root.wideMode && !(item.Kirigami.FormData.buddyFor instanceof QQC2.TextArea)) {
382  return Math.max(implicitHeight, item.Kirigami.FormData.buddyFor.height)
383  }
384  return implicitHeight;
385  }
386  return Kirigami.Units.smallSpacing;
387  }
388 
389  Layout.alignment: temp.effectiveLayout(item)
390  verticalAlignment: temp.effectiveTextLayout(item)
391 
392  Layout.fillWidth: !root.wideMode
393  wrapMode: Text.Wrap
394 
395  Layout.topMargin: {
396  if (!item) {
397  return 0;
398  }
399  if (root.wideMode && item.Kirigami.FormData.buddyFor.parent !== root) {
400  return item.Kirigami.FormData.buddyFor.y;
401  }
402  if (index === 0 || root.wideMode) {
403  return 0;
404  }
405  return Kirigami.Units.largeSpacing * 2;
406  }
407  onItemChanged: {
408  if (!item) {
409  labelItem.destroy();
410  }
411  }
412  Shortcut {
413  sequence: labelItem.Kirigami.MnemonicData.sequence
414  onActivated: labelItem.item.Kirigami.FormData.buddyFor.forceActiveFocus()
415  }
416  }
417  }
418  Component {
419  id: checkableBuddyComponent
420  QQC2.CheckBox {
421  id: labelItem
422 
423  property Item item
424 
425  visible: item !== null && item.visible
426  Kirigami.MnemonicData.enabled: item !== null && item.Kirigami.FormData.buddyFor && item.Kirigami.FormData.buddyFor.activeFocusOnTab
427  Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.FormLabel
428  Kirigami.MnemonicData.label: item !== null ? item.Kirigami.FormData.label : ""
429 
430  Layout.columnSpan: item !== null && item.Kirigami.FormData.isSection ? lay.columns : 1
431  Layout.preferredHeight: {
432  if (!item) {
433  return 0;
434  }
435  if (item.Kirigami.FormData.label.length > 0) {
436  if (root.wideMode && !(item.Kirigami.FormData.buddyFor instanceof QQC2.TextArea)) {
437  return Math.max(implicitHeight, item.Kirigami.FormData.buddyFor.height);
438  }
439  return implicitHeight;
440  }
441  return Kirigami.Units.smallSpacing;
442  }
443 
444  Layout.alignment: temp.effectiveLayout(this)
445  Layout.topMargin: item !== null && item.Kirigami.FormData.buddyFor.height > implicitHeight * 2 ? Kirigami.Units.smallSpacing/2 : 0
446 
447  activeFocusOnTab: indicator.visible && indicator.enabled
448  // HACK: desktop style checkboxes have also the text in the background item
449  // text: Kirigami.MnemonicData.richTextLabel
450  enabled: item !== null && item.Kirigami.FormData.enabled
451  checked: item !== null && item.Kirigami.FormData.checked
452 
453  onItemChanged: {
454  if (!item) {
455  labelItem.destroy();
456  }
457  }
458  Shortcut {
459  sequence: labelItem.Kirigami.MnemonicData.sequence
460  onActivated: {
461  checked = !checked;
462  item.Kirigami.FormData.buddyFor.forceActiveFocus();
463  }
464  }
465  onCheckedChanged: {
466  item.Kirigami.FormData.checked = checked;
467  }
468  contentItem: Kirigami.Heading {
469  id: labelItemHeading
470  level: labelItem.item !== null && labelItem.item.Kirigami.FormData.isSection ? 3 : 5
471  text: labelItem.Kirigami.MnemonicData.richTextLabel
472  type: labelItem.item !== null && labelItem.item.Kirigami.FormData.isSection ? Kirigami.Heading.Type.Primary : Kirigami.Heading.Type.Normal
473  verticalAlignment: temp.effectiveTextLayout(labelItem.item)
474  enabled: labelItem.item !== null && labelItem.item.Kirigami.FormData.enabled
475  leftPadding: height // parent.indicator.width
476  }
477  Rectangle {
478  enabled: labelItem.indicator.enabled
479  anchors.left: labelItemHeading.left
480  anchors.right: labelItemHeading.right
481  anchors.top: labelItemHeading.bottom
482  anchors.leftMargin: labelItemHeading.leftPadding
483  height: 1
484  color: Kirigami.Theme.highlightColor
485  visible: labelItem.activeFocus && labelItem.indicator.visible
486  }
487  }
488  }
489 }
Type type(const QSqlDatabase &db)
QStringView level(QStringView ifopt)
KIOFILEWIDGETS_EXPORT QStringList list(const QString &fileClass)
QTextStream & left(QTextStream &s)
QAction * hint(const QObject *recvr, const char *slot, QObject *parent)
const QObjectList & children() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Jan 29 2023 04:11:03 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.