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 value: root.width
145 restoreMode: Binding.RestoreBinding
146 }
147 Binding {
148 when: root.wideMode
149 target: lay
150 property: "width"
151 value: root.implicitWidth
152 restoreMode: Binding.RestoreBinding
153 }
154 anchors {
155 horizontalCenter: root.wideMode ? root.horizontalCenter : undefined
156 left: root.wideMode ? undefined : root.left
157 }
158
159 property var reverseTwins: []
160 property var knownItems: []
161 property var buddies: []
162 property int knownItemsImplicitWidth: {
163 let hint = 0;
164 for (const item of knownItems) {
165 if (typeof item.Layout === "undefined") {
166 // Items may have been dynamically destroyed. Even
167 // printing such zombie wrappers results in a
168 // meaningless "TypeError: Type error". Normally they
169 // should be cleaned up from the array, but it would
170 // trigger a binding loop if done here.
171 //
172 // This is, so far, the only way to detect them.
173 continue;
174 }
175 const actualWidth = item.Layout.preferredWidth > 0
176 ? item.Layout.preferredWidth
177 : item.implicitWidth;
178
179 hint = Math.max(hint, item.Layout.minimumWidth, Math.min(actualWidth, item.Layout.maximumWidth));
180 }
181 return hint;
182 }
183 property int buddiesImplicitWidth: {
184 let hint = 0;
185
186 for (const buddy of buddies) {
187 if (buddy.visible && buddy.item !== null && !buddy.item.Kirigami.FormData.isSection) {
188 hint = Math.max(hint, buddy.implicitWidth);
189 }
190 }
191 return hint;
192 }
193 readonly property var actualTwinFormLayouts: {
194 // We need to copy that array by value
195 const list = lay.reverseTwins.slice();
196 for (const parentLay of root.twinFormLayouts) {
197 if (!parentLay || !parentLay.hasOwnProperty("children")) {
198 continue;
199 }
200 list.push(parentLay);
201 for (const childLay of parentLay.children[0].reverseTwins) {
202 if (childLay && !(childLay in list)) {
203 list.push(childLay);
204 }
205 }
206 }
207 return list;
208 }
209
210 Timer {
211 id: hintCompression
212 interval: 0
213 onTriggered: {
214 if (root.wideMode) {
215 lay.wideImplicitWidth = lay.implicitWidth;
216 }
217 }
218 }
219 onImplicitWidthChanged: hintCompression.restart();
220 //This invisible row is used to sync alignment between multiple layouts
221
222 Item {
223 Layout.preferredWidth: {
224 let hint = lay.buddiesImplicitWidth;
225 for (const item of lay.actualTwinFormLayouts) {
226 if (item && item.hasOwnProperty("children")) {
227 hint = Math.max(hint, item.children[0].buddiesImplicitWidth);
228 }
229 }
230 return hint;
231 }
232 Layout.preferredHeight: 2
233 }
234 Item {
235 Layout.preferredWidth: {
236 let hint = Math.min(root.width, lay.knownItemsImplicitWidth);
237 for (const item of lay.actualTwinFormLayouts) {
238 if (item.hasOwnProperty("children")) {
239 hint = Math.max(hint, item.children[0].knownItemsImplicitWidth);
240 }
241 }
242 return hint;
243 }
244 Layout.preferredHeight: 2
245 }
246 }
247
248 Item {
249 id: temp
250
251 /**
252 * The following two functions are used in the label buddy items.
253 *
254 * They're in this mostly unused item to keep them private to the FormLayout
255 * without creating another QObject.
256 *
257 * Normally, such complex things in bindings are kinda bad for performance
258 * but this is a fairly static property. If for some reason an application
259 * decides to obsessively change its alignment, V8's JIT hotspot optimisations
260 * will kick in.
261 */
262
263 /**
264 * @param {Item} item
265 * @returns {Qt::Alignment}
266 */
267 function effectiveLayout(item: Item): /*Qt.Alignment*/ int {
268 if (!item) {
269 return 0;
270 }
271 const verticalAlignment =
272 item.Kirigami.FormData.labelAlignment !== 0
273 ? item.Kirigami.FormData.labelAlignment
274 : Qt.AlignTop;
275
276 if (item.Kirigami.FormData.isSection) {
277 return Qt.AlignHCenter;
278 }
279 if (root.wideMode) {
280 return Qt.AlignRight | verticalAlignment;
281 }
282 return Qt.AlignLeft | Qt.AlignBottom;
283 }
284
285 /**
286 * @param {Item} item
287 * @returns vertical alignment of the item passed as an argument.
288 */
289 function effectiveTextLayout(item: Item): /*Qt.Alignment*/ int {
290 if (!item) {
291 return 0;
292 }
293 if (root.wideMode && !item.Kirigami.FormData.isSection) {
294 return item.Kirigami.FormData.labelAlignment !== 0 ? item.Kirigami.FormData.labelAlignment : Text.AlignVCenter;
295 }
296 return Text.AlignBottom;
297 }
298 }
299
300 Timer {
301 id: relayoutTimer
302 interval: 0
303 onTriggered: {
304 const __items = root.children;
305 // exclude the layout and temp
306 for (let i = 2; i < __items.length; ++i) {
307 const item = __items[i];
308
309 // skip items that are already there
310 if (lay.knownItems.indexOf(item) !== -1 || item instanceof Repeater) {
311 continue;
312 }
313 lay.knownItems.push(item);
314
315 const itemContainer = itemComponent.createObject(temp, { item });
316
317 // if it's a labeled section header, add extra spacing before it
318 if (item.Kirigami.FormData.label.length > 0 && item.Kirigami.FormData.isSection) {
319 placeHolderComponent.createObject(lay, { item });
320 }
321
322 const buddy = buddyComponent.createObject(lay, { item, index: i - 2 });
323
324 itemContainer.parent = lay;
325 lay.buddies.push(buddy);
326 }
327 lay.knownItemsChanged();
328 lay.buddiesChanged();
329 hintCompression.triggered();
330 }
331 }
332
333 onChildrenChanged: relayoutTimer.restart();
334
335 Component {
336 id: itemComponent
337 Item {
338 id: container
339
340 property Item item
341
342 enabled: item?.enabled ?? false
343 visible: item?.visible ?? false
344
345 // 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
346 implicitWidth: item !== null ? Math.max(item.implicitWidth, 1) : 0
347 implicitHeight: item !== null ? Math.max(item.implicitHeight, 1) : 0
348 Layout.preferredWidth: item !== null ? Math.max(1, item.Layout.preferredWidth > 0 ? item.Layout.preferredWidth : Math.ceil(item.implicitWidth)) : 0
349 Layout.preferredHeight: item !== null ? Math.max(1, item.Layout.preferredHeight > 0 ? item.Layout.preferredHeight : Math.ceil(item.implicitHeight)) : 0
350
351 Layout.minimumWidth: item?.Layout.minimumWidth ?? 0
352 Layout.minimumHeight: item?.Layout.minimumHeight ?? 0
353
354 Layout.maximumWidth: item?.Layout.maximumWidth ?? 0
355 Layout.maximumHeight: item?.Layout.maximumHeight ?? 0
356
357 Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
358 Layout.fillWidth: item !== null && (item instanceof TextInput || item.Layout.fillWidth || item.Kirigami.FormData.isSection)
359 Layout.columnSpan: item?.Kirigami.FormData.isSection ? lay.columns : 1
360 onItemChanged: {
361 if (!item) {
362 container.destroy();
363 }
364 }
365 onXChanged: if (item !== null) { item.x = x + lay.x; }
366 // Assume lay.y is always 0
367 onYChanged: if (item !== null) { item.y = y + lay.y; }
368 onWidthChanged: if (item !== null) { item.width = width; }
369 Component.onCompleted: item.x = x + lay.x;
370 Connections {
371 target: lay
372 function onXChanged(): void {
373 if (container.item !== null) {
374 container.item.x = container.x + lay.x;
375 }
376 }
377 }
378 }
379 }
380 Component {
381 id: placeHolderComponent
382 Item {
383 property Item item
384
385 enabled: item?.enabled ?? false
386 visible: item?.visible ?? false
387
388 width: Kirigami.Units.smallSpacing
389 height: Kirigami.Units.smallSpacing
390 Layout.topMargin: item?.height > 0 ? Kirigami.Units.smallSpacing : 0
391 onItemChanged: {
392 if (!item) {
393 destroy();
394 }
395 }
396 }
397 }
398 Component {
399 id: buddyComponent
400 Kirigami.Heading {
401 id: labelItem
402
403 property Item item
404 property int index
405
406 enabled: {
407 const buddy = item?.Kirigami.FormData.buddyFor;
408 if (buddy) {
409 return buddy.enabled;
410 } else {
411 return item?.enabled ?? false;
412 }
413 }
414 visible: (item?.visible && (root.wideMode || text.length > 0)) ?? false
415 Kirigami.MnemonicData.enabled: {
416 const buddy = item?.Kirigami.FormData.buddyFor;
417 if (buddy && buddy.enabled && buddy.visible && buddy.activeFocusOnTab) {
418 // Only set mnemonic if the buddy doesn't already have one.
419 const buddyMnemonic = buddy.Kirigami.MnemonicData;
420 return !buddyMnemonic.label || !buddyMnemonic.enabled;
421 } else {
422 return false;
423 }
424 }
425 Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.FormLabel
426 Kirigami.MnemonicData.label: item?.Kirigami.FormData.label ?? ""
427 text: Kirigami.MnemonicData.richTextLabel
428 Accessible.name: Kirigami.MnemonicData.plainTextLabel
429 type: item?.Kirigami.FormData.isSection ? Kirigami.Heading.Type.Primary : Kirigami.Heading.Type.Normal
430
431 level: item?.Kirigami.FormData.isSection ? 3 : 5
432
433 Layout.columnSpan: item?.Kirigami.FormData.isSection ? lay.columns : 1
434 Layout.preferredHeight: {
435 if (!item) {
436 return 0;
437 }
438 if (item.Kirigami.FormData.label.length > 0) {
439 // Add extra whitespace before textual section headers, which
440 // looks better than separator lines
441 if (item.Kirigami.FormData.isSection && labelItem.index !== 0) {
442 return implicitHeight + Kirigami.Units.largeSpacing * 2;
443 }
444 else if (root.wideMode && !(item.Kirigami.FormData.buddyFor instanceof TextEdit)) {
445 return Math.max(implicitHeight, item.Kirigami.FormData.buddyFor.height)
446 }
447 return implicitHeight;
448 }
449 return Kirigami.Units.smallSpacing;
450 }
451
452 Layout.alignment: temp.effectiveLayout(item)
453 verticalAlignment: temp.effectiveTextLayout(item)
454
455 Layout.fillWidth: !root.wideMode
456 wrapMode: Text.Wrap
457
458 Layout.topMargin: {
459 if (!item) {
460 return 0;
461 }
462 if (root.wideMode && item.Kirigami.FormData.buddyFor.parent !== root) {
463 return item.Kirigami.FormData.buddyFor.y;
464 }
465 if (index === 0 || root.wideMode) {
466 return 0;
467 }
468 return Kirigami.Units.largeSpacing * 2;
469 }
470 onItemChanged: {
471 if (!item) {
472 destroy();
473 }
474 }
475 Shortcut {
476 sequence: labelItem.Kirigami.MnemonicData.sequence
477 onActivated: {
478 const buddy = labelItem.item.Kirigami.FormData.buddyFor;
479
480 const buttonBuddy = buddy as T.AbstractButton;
481 // animateClick is only in Qt 6.8,
482 // it also takes into account focus policy.
483 if (buttonBuddy && buttonBuddy.animateClick) {
484 buttonBuddy.animateClick();
485 } else {
486 buddy.forceActiveFocus(Qt.ShortcutFocusReason);
487 }
488 }
489 }
490 }
491 }
492}
list< Item > twinFormLayouts
If for some implementation reason multiple FormLayouts have to appear on the same page,...
QAction * hint(const QObject *recvr, const char *slot, QObject *parent)
QStringView level(QStringView ifopt)
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
QTextStream & left(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Mar 21 2025 11:58:04 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.