Kirigami2

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

KDE's Doxygen guidelines are available online.