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

KDE's Doxygen guidelines are available online.