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 }