Warning, /frameworks/kirigami/src/controls/FormLayout.qml is written in an unsupported language. File is not indexed.
0001 /* 0002 * SPDX-FileCopyrightText: 2017 Marco Martin <mart@kde.org> 0003 * SPDX-FileCopyrightText: 2022 ivan tkachenko <me@ratijas.tk> 0004 * 0005 * SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 import QtQuick 2.15 0009 import QtQuick.Layouts 1.15 0010 import QtQuick.Controls 2.15 as QQC2 0011 import org.kde.kirigami 2.18 as Kirigami 0012 0013 /** 0014 * This is the base class for Form layouts conforming to the 0015 * Kirigami Human Interface Guidelines. The layout consists 0016 * of two columns: the left column contains only right-aligned 0017 * labels provided by a kirigami::FormData attached property, 0018 * the right column contains left-aligned child types. 0019 * 0020 * Child types can be sectioned using an QtQuick.Item 0021 * or kirigami::Separator with a kirigami::FormData 0022 * attached property, see FormLayoutAttached::isSection for details. 0023 * 0024 * Example usage: 0025 * @include formlayout.qml 0026 * 0027 * @see FormLayoutAttached 0028 * @see <a href="https://develop.kde.org/docs/getting-started/kirigami/components-formlayouts">Form Layouts in Kirigami</a> 0029 * @see <a href="https://develop.kde.org/hig/patterns-content/form">KDE Human Interface Guidelines on Forms</a> 0030 * @since org.kde.kirigami 2.3 0031 * @inherit QtQuick.Item 0032 */ 0033 Item { 0034 id: root 0035 0036 /** 0037 * @brief This property specifies whether the form layout is in wide mode. 0038 * 0039 * If true, the layout will be optimized for a wide screen, such as 0040 * a desktop machine (the labels will be on a left column, 0041 * the fields on a right column beside it), if @c false (such as on a phone) 0042 * everything is laid out in a single column. 0043 * 0044 * By default, this property automatically adjusts the layout 0045 * if there is enough screen space. 0046 * 0047 * Set this to @c true for a convergent design, 0048 * set this to @c false for a mobile-only design. 0049 */ 0050 property bool wideMode: width >= lay.wideImplicitWidth 0051 0052 /** 0053 * If for some implementation reason multiple FormLayouts have to appear 0054 * on the same page, they can have each other in twinFormLayouts, 0055 * so they will vertically align with each other perfectly 0056 * 0057 * @since KDE Frameworks 5.53 0058 */ 0059 property list<Item> twinFormLayouts // should be list<FormLayout> but we can't have a recursive declaration 0060 0061 onTwinFormLayoutsChanged: { 0062 for (const i in twinFormLayouts) { 0063 if (!(root in twinFormLayouts[i].children[0].reverseTwins)) { 0064 twinFormLayouts[i].children[0].reverseTwins.push(root) 0065 Qt.callLater(() => twinFormLayouts[i].children[0].reverseTwinsChanged()); 0066 } 0067 } 0068 } 0069 0070 Component.onCompleted: { 0071 relayoutTimer.triggered(); 0072 } 0073 0074 Component.onDestruction: { 0075 for (const i in twinFormLayouts) { 0076 const twin = twinFormLayouts[i]; 0077 const child = twin.children[0]; 0078 child.reverseTwins = child.reverseTwins.filter(value => value !== root); 0079 } 0080 } 0081 0082 implicitWidth: lay.wideImplicitWidth 0083 implicitHeight: lay.implicitHeight 0084 Layout.preferredHeight: lay.implicitHeight 0085 Layout.fillWidth: true 0086 Accessible.role: Accessible.Form 0087 0088 GridLayout { 0089 id: lay 0090 property int wideImplicitWidth 0091 columns: root.wideMode ? 2 : 1 0092 rowSpacing: Kirigami.Units.smallSpacing 0093 columnSpacing: Kirigami.Units.smallSpacing 0094 width: root.wideMode ? undefined : root.width 0095 anchors { 0096 horizontalCenter: root.wideMode ? root.horizontalCenter : undefined 0097 left: root.wideMode ? undefined : root.left 0098 } 0099 0100 property var reverseTwins: [] 0101 property var knownItems: [] 0102 property var buddies: [] 0103 property int knownItemsImplicitWidth: { 0104 let hint = 0; 0105 for (const i in knownItems) { 0106 const item = knownItems[i]; 0107 if (typeof item.Layout === "undefined") { 0108 // Items may have been dynamically destroyed. Even 0109 // printing such zombie wrappers results in a 0110 // meaningless "TypeError: Type error". Normally they 0111 // should be cleaned up from the array, but it would 0112 // trigger a binding loop if done here. 0113 // 0114 // This is, so far, the only way to detect them. 0115 continue; 0116 } 0117 const actualWidth = item.Layout.preferredWidth > 0 0118 ? item.Layout.preferredWidth 0119 : item.implicitWidth; 0120 0121 hint = Math.max(hint, item.Layout.minimumWidth, Math.min(actualWidth, item.Layout.maximumWidth)); 0122 } 0123 return hint; 0124 } 0125 property int buddiesImplicitWidth: { 0126 let hint = 0; 0127 0128 for (const i in buddies) { 0129 if (buddies[i].visible && buddies[i].item !== null && !buddies[i].item.Kirigami.FormData.isSection) { 0130 hint = Math.max(hint, buddies[i].implicitWidth); 0131 } 0132 } 0133 return hint; 0134 } 0135 readonly property var actualTwinFormLayouts: { 0136 // We need to copy that array by value 0137 const list = lay.reverseTwins.slice(); 0138 for (const i in twinFormLayouts) { 0139 const parentLay = twinFormLayouts[i]; 0140 if (!parentLay || !parentLay.hasOwnProperty("children")) { 0141 continue; 0142 } 0143 list.push(parentLay); 0144 for (const j in parentLay.children[0].reverseTwins) { 0145 const childLay = parentLay.children[0].reverseTwins[j]; 0146 if (childLay && !(childLay in list)) { 0147 list.push(childLay); 0148 } 0149 } 0150 } 0151 return list; 0152 } 0153 0154 Timer { 0155 id: hintCompression 0156 interval: 0 0157 onTriggered: { 0158 if (root.wideMode) { 0159 lay.wideImplicitWidth = lay.implicitWidth; 0160 } 0161 } 0162 } 0163 onImplicitWidthChanged: hintCompression.restart(); 0164 //This invisible row is used to sync alignment between multiple layouts 0165 0166 Item { 0167 Layout.preferredWidth: { 0168 let hint = lay.buddiesImplicitWidth; 0169 for (const i in lay.actualTwinFormLayouts) { 0170 if (lay.actualTwinFormLayouts[i] && lay.actualTwinFormLayouts[i].hasOwnProperty("children")) { 0171 hint = Math.max(hint, lay.actualTwinFormLayouts[i].children[0].buddiesImplicitWidth); 0172 } 0173 } 0174 return hint; 0175 } 0176 Layout.preferredHeight: 2 0177 } 0178 Item { 0179 Layout.preferredWidth: { 0180 let hint = Math.min(root.width, lay.knownItemsImplicitWidth); 0181 for (const i in lay.actualTwinFormLayouts) { 0182 if (lay.actualTwinFormLayouts[i] && lay.actualTwinFormLayouts[i].hasOwnProperty("children")) { 0183 hint = Math.max(hint, lay.actualTwinFormLayouts[i].children[0].knownItemsImplicitWidth); 0184 } 0185 } 0186 return hint; 0187 } 0188 Layout.preferredHeight: 2 0189 } 0190 } 0191 0192 Item { 0193 id: temp 0194 0195 /** 0196 * The following two functions are used in the label buddy items. 0197 * 0198 * They're in this mostly unused item to keep them private to the FormLayout 0199 * without creating another QObject. 0200 * 0201 * Normally, such complex things in bindings are kinda bad for performance 0202 * but this is a fairly static property. If for some reason an application 0203 * decides to obsessively change its alignment, V8's JIT hotspot optimisations 0204 * will kick in. 0205 */ 0206 0207 /** 0208 * @param {Item} item 0209 * @returns {Qt::Alignment} 0210 */ 0211 function effectiveLayout(item) { 0212 if (!item) { 0213 return 0; 0214 } 0215 const verticalAlignment = 0216 item.Kirigami.FormData.labelAlignment !== 0 0217 ? item.Kirigami.FormData.labelAlignment 0218 : Qt.AlignTop; 0219 0220 if (item.Kirigami.FormData.isSection) { 0221 return Qt.AlignHCenter; 0222 } 0223 if (root.wideMode) { 0224 return Qt.AlignRight | verticalAlignment; 0225 } 0226 return Qt.AlignLeft | Qt.AlignBottom; 0227 } 0228 0229 /** 0230 * @param {Item} item 0231 * @returns vertical alignment of the item passed as an argument. 0232 */ 0233 function effectiveTextLayout(item) { 0234 if (!item) { 0235 return 0; 0236 } 0237 if (root.wideMode) { 0238 return item.Kirigami.FormData.labelAlignment !== 0 ? item.Kirigami.FormData.labelAlignment : Text.AlignVCenter; 0239 } 0240 return Text.AlignBottom; 0241 } 0242 } 0243 0244 Timer { 0245 id: relayoutTimer 0246 interval: 0 0247 onTriggered: { 0248 const __items = root.children; 0249 // exclude the layout and temp 0250 for (let i = 2; i < __items.length; ++i) { 0251 const item = __items[i]; 0252 0253 // skip items that are already there 0254 if (lay.knownItems.indexOf(item) !== -1 || item instanceof Repeater) { 0255 continue; 0256 } 0257 lay.knownItems.push(item); 0258 0259 const itemContainer = itemComponent.createObject(temp, { item }); 0260 0261 // if it's a labeled section header, add extra spacing before it 0262 if (item.Kirigami.FormData.label.length > 0 && item.Kirigami.FormData.isSection) { 0263 placeHolderComponent.createObject(lay, { item }); 0264 } 0265 0266 const buddy = item.Kirigami.FormData.checkable 0267 ? checkableBuddyComponent.createObject(lay, { item }) 0268 : buddyComponent.createObject(lay, { item, index: i - 2 }); 0269 0270 itemContainer.parent = lay; 0271 lay.buddies.push(buddy); 0272 } 0273 lay.knownItemsChanged(); 0274 lay.buddiesChanged(); 0275 hintCompression.triggered(); 0276 } 0277 } 0278 0279 onChildrenChanged: relayoutTimer.restart(); 0280 0281 Component { 0282 id: itemComponent 0283 Item { 0284 id: container 0285 0286 property Item item 0287 0288 enabled: item !== null && item.enabled 0289 visible: item !== null && item.visible 0290 0291 // 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 0292 implicitWidth: item !== null ? Math.max(item.implicitWidth, 1) : 0 0293 implicitHeight: item !== null ? Math.max(item.implicitHeight, 1) : 0 0294 Layout.preferredWidth: item !== null ? Math.max(1, item.Layout.preferredWidth > 0 ? item.Layout.preferredWidth : Math.ceil(item.implicitWidth)) : 0 0295 Layout.preferredHeight: item !== null ? Math.max(1, item.Layout.preferredHeight > 0 ? item.Layout.preferredHeight : Math.ceil(item.implicitHeight)) : 0 0296 0297 Layout.minimumWidth: item !== null ? item.Layout.minimumWidth : 0 0298 Layout.minimumHeight: item !== null ? item.Layout.minimumHeight : 0 0299 0300 Layout.maximumWidth: item !== null ? item.Layout.maximumWidth : 0 0301 Layout.maximumHeight: item !== null ? item.Layout.maximumHeight : 0 0302 0303 Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter 0304 Layout.fillWidth: item !== null && (item instanceof TextInput || item.Layout.fillWidth || item.Kirigami.FormData.isSection) 0305 Layout.columnSpan: item !== null && item.Kirigami.FormData.isSection ? lay.columns : 1 0306 onItemChanged: { 0307 if (!item) { 0308 container.destroy(); 0309 } 0310 } 0311 onXChanged: if (item !== null) { item.x = x + lay.x; } 0312 // Assume lay.y is always 0 0313 onYChanged: if (item !== null) { item.y = y + lay.y; } 0314 onWidthChanged: if (item !== null) { item.width = width; } 0315 Component.onCompleted: item.x = x + lay.x; 0316 Connections { 0317 target: lay 0318 function onXChanged() { 0319 if (item !== null) { 0320 item.x = x + lay.x; 0321 } 0322 } 0323 } 0324 } 0325 } 0326 Component { 0327 id: placeHolderComponent 0328 Item { 0329 property Item item 0330 0331 enabled: item !== null && item.enabled 0332 visible: item !== null && item.visible 0333 0334 width: Kirigami.Units.smallSpacing 0335 height: Kirigami.Units.smallSpacing 0336 Layout.topMargin: item !== null && item.height > 0 ? Kirigami.Units.smallSpacing : 0 0337 onItemChanged: { 0338 if (!item) { 0339 labelItem.destroy(); 0340 } 0341 } 0342 } 0343 } 0344 Component { 0345 id: buddyComponent 0346 Kirigami.Heading { 0347 id: labelItem 0348 0349 property Item item 0350 property int index 0351 0352 enabled: item !== null && item.enabled && item.Kirigami.FormData.enabled 0353 visible: item !== null && item.visible && (root.wideMode || text.length > 0) 0354 Kirigami.MnemonicData.enabled: item !== null && item.Kirigami.FormData.buddyFor && item.Kirigami.FormData.buddyFor.activeFocusOnTab 0355 Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.FormLabel 0356 Kirigami.MnemonicData.label: item !== null ? item.Kirigami.FormData.label : "" 0357 text: Kirigami.MnemonicData.richTextLabel 0358 type: item !== null && item.Kirigami.FormData.isSection ? Kirigami.Heading.Type.Primary : Kirigami.Heading.Type.Normal 0359 0360 level: item !== null && item.Kirigami.FormData.isSection ? 3 : 5 0361 0362 Layout.columnSpan: item !== null && item.Kirigami.FormData.isSection ? lay.columns : 1 0363 Layout.preferredHeight: { 0364 if (!item) { 0365 return 0; 0366 } 0367 if (item.Kirigami.FormData.label.length > 0) { 0368 if (root.wideMode && !(item.Kirigami.FormData.buddyFor instanceof QQC2.TextArea)) { 0369 return Math.max(implicitHeight, item.Kirigami.FormData.buddyFor.height) 0370 } 0371 return implicitHeight; 0372 } 0373 return Kirigami.Units.smallSpacing; 0374 } 0375 0376 Layout.alignment: temp.effectiveLayout(item) 0377 verticalAlignment: temp.effectiveTextLayout(item) 0378 0379 Layout.fillWidth: !root.wideMode 0380 wrapMode: Text.Wrap 0381 0382 Layout.topMargin: { 0383 if (!item) { 0384 return 0; 0385 } 0386 if (root.wideMode && item.Kirigami.FormData.buddyFor.parent !== root) { 0387 return item.Kirigami.FormData.buddyFor.y; 0388 } 0389 if (index === 0 || root.wideMode) { 0390 return 0; 0391 } 0392 return Kirigami.Units.largeSpacing * 2; 0393 } 0394 onItemChanged: { 0395 if (!item) { 0396 labelItem.destroy(); 0397 } 0398 } 0399 Shortcut { 0400 sequence: labelItem.Kirigami.MnemonicData.sequence 0401 onActivated: labelItem.item.Kirigami.FormData.buddyFor.forceActiveFocus() 0402 } 0403 } 0404 } 0405 Component { 0406 id: checkableBuddyComponent 0407 QQC2.CheckBox { 0408 id: labelItem 0409 0410 property Item item 0411 0412 visible: item !== null && item.visible 0413 Kirigami.MnemonicData.enabled: item !== null && item.Kirigami.FormData.buddyFor && item.Kirigami.FormData.buddyFor.activeFocusOnTab 0414 Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.FormLabel 0415 Kirigami.MnemonicData.label: item !== null ? item.Kirigami.FormData.label : "" 0416 0417 Layout.columnSpan: item !== null && item.Kirigami.FormData.isSection ? lay.columns : 1 0418 Layout.preferredHeight: { 0419 if (!item) { 0420 return 0; 0421 } 0422 if (item.Kirigami.FormData.label.length > 0) { 0423 if (root.wideMode && !(item.Kirigami.FormData.buddyFor instanceof QQC2.TextArea)) { 0424 return Math.max(implicitHeight, item.Kirigami.FormData.buddyFor.height); 0425 } 0426 return implicitHeight; 0427 } 0428 return Kirigami.Units.smallSpacing; 0429 } 0430 0431 Layout.alignment: temp.effectiveLayout(this) 0432 Layout.topMargin: item !== null && item.Kirigami.FormData.buddyFor.height > implicitHeight * 2 ? Kirigami.Units.smallSpacing/2 : 0 0433 0434 activeFocusOnTab: indicator.visible && indicator.enabled 0435 // HACK: desktop style checkboxes have also the text in the background item 0436 // text: Kirigami.MnemonicData.richTextLabel 0437 enabled: item !== null && item.Kirigami.FormData.enabled 0438 checked: item !== null && item.Kirigami.FormData.checked 0439 0440 onItemChanged: { 0441 if (!item) { 0442 labelItem.destroy(); 0443 } 0444 } 0445 Shortcut { 0446 sequence: labelItem.Kirigami.MnemonicData.sequence 0447 onActivated: { 0448 checked = !checked; 0449 item.Kirigami.FormData.buddyFor.forceActiveFocus(); 0450 } 0451 } 0452 onCheckedChanged: { 0453 item.Kirigami.FormData.checked = checked; 0454 } 0455 contentItem: Kirigami.Heading { 0456 id: labelItemHeading 0457 level: labelItem.item !== null && labelItem.item.Kirigami.FormData.isSection ? 3 : 5 0458 text: labelItem.Kirigami.MnemonicData.richTextLabel 0459 type: labelItem.item !== null && labelItem.item.Kirigami.FormData.isSection ? Kirigami.Heading.Type.Primary : Kirigami.Heading.Type.Normal 0460 verticalAlignment: temp.effectiveTextLayout(labelItem.item) 0461 enabled: labelItem.item !== null && labelItem.item.Kirigami.FormData.enabled 0462 leftPadding: height // parent.indicator.width 0463 } 0464 Rectangle { 0465 enabled: labelItem.indicator.enabled 0466 anchors.left: labelItemHeading.left 0467 anchors.right: labelItemHeading.right 0468 anchors.top: labelItemHeading.bottom 0469 anchors.leftMargin: labelItemHeading.leftPadding 0470 height: 1 0471 color: Kirigami.Theme.highlightColor 0472 visible: labelItem.activeFocus && labelItem.indicator.visible 0473 } 0474 } 0475 } 0476 }