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 }