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

KDE's Doxygen guidelines are available online.