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

KDE's Doxygen guidelines are available online.