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

KDE's Doxygen guidelines are available online.