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