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 }