Warning, /libraries/kirigami-addons/src/components/SearchPopupField.qml is written in an unsupported language. File is not indexed.

0001 // SPDX-FileCopyrightText: 2021 Jonah BrĂ¼chert <jbb@kaidan.im>
0002 // SPDX-FileCopyrightText: 2023 Mathis BrĂ¼chert <mbb@kaidan.im>
0003 // SPDX-FileCopyrightText: 2023 Carl Schwan <carl@carlschwan.eu>
0004 // SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
0005 //
0006 // SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0007 
0008 import QtQuick 2.15
0009 import QtQuick.Controls 2.15 as QQC2
0010 import QtQuick.Templates 2.15 as T
0011 import QtQuick.Layouts 1.15
0012 import Qt.labs.qmlmodels 1.0
0013 import org.kde.kirigami 2.20 as Kirigami
0014 
0015 /**
0016  * SearchField with a Popup to show autocompletion entries or search results
0017  *
0018  * @since KirigamiAddons.labs.components 1.0
0019  */
0020 QQC2.Control {
0021     id: root
0022 
0023     /**
0024      * This property holds the content item of the popup.
0025      *
0026      * Overflow will be automatically be handled as popupContentItem is
0027      * contained inside a ScrollView.
0028      *
0029      * This is the default element of SearchPopupField.
0030      *
0031      * ```qml
0032      * SearchPopupField {
0033      *     ListView {
0034      *         model: SearchModel {}
0035      *         delegate: QQC2.ItemDelegate {}
0036      *
0037      *         Kirigami.PlaceholderMessage {
0038      *             id: loadingPlaceholder
0039      *             anchors.centerIn: parent
0040      *             width: parent.width - Kirigami.Units.gridUnit * 4
0041      *             // ...
0042      *         }
0043      *     }
0044      * }
0045      * ```
0046      *
0047      * @since KirigamiAddons.labs.components 1.0
0048      */
0049     default property alias popupContentItem: scrollView.contentItem
0050 
0051     /**
0052      * This property holds the text of the search field.
0053      *
0054      * @since KirigamiAddons.labs.components 1.0
0055      */
0056     property alias text: root.searchField.text
0057 
0058     /**
0059      * @brief This property sets whether to delay automatic acceptance of the search input.
0060      *
0061      * Set this to true if your search is expensive (such as for online
0062      * operations or in exceptionally slow data sets) and want to delay it
0063      * for 2.5 seconds.
0064      *
0065      * @note If you must have immediate feedback (filter-style), use the
0066      * text property directly instead of accepted()
0067      *
0068      * default: ``false``
0069      *
0070      * @since KirigamiAddons.labs.components 1.0
0071      */
0072     property alias delaySearch: root.searchField.delaySearch
0073 
0074     /**
0075      * @brief This property sets whether the accepted signal is fired automatically
0076      * when the text is changed.
0077      *
0078      * Setting this to false will require that the user presses return or enter
0079      * (the same way a QtQuick.Controls.TextInput works).
0080      *
0081      * default: ``false``
0082      *
0083      * @since KirigamiAddons.labs.components 1.0
0084      */
0085     property alias autoAccept: root.searchField.autoAccept
0086 
0087     /**
0088      * This property holds whether there is space available on the left.
0089      *
0090      * This is used by the left shadow.
0091      *
0092      * @since KirigamiAddons.labs.components 1.0
0093      * @deprecated Was not really used by anything.
0094      */
0095     property bool spaceAvailableLeft: true
0096 
0097     /**
0098      * This property holds whether there is space available on the left.
0099      *
0100      * This is used by the right shadow.
0101      *
0102      * @since KirigamiAddons.labs.components 1.0
0103      * @deprecated Was not really used by anything.
0104      */
0105     property bool spaceAvailableRight: true
0106 
0107     /**
0108      * @brief This hold the focus state of the internal SearchField.
0109     */
0110     property alias fieldFocus: root.searchField.focus
0111 
0112     /**
0113      * This signal is triggered when the user trigger a search.
0114      */
0115     signal accepted()
0116 
0117     property alias popup: popup
0118 
0119     property Kirigami.SearchField searchField: Kirigami.SearchField {}
0120 
0121     contentItem: Item {
0122         implicitHeight: root.searchField ? root.searchField.implicitHeight : 0
0123         implicitWidth: root.searchField ? root.searchField.implicitWidth : 0
0124 
0125         // by default popup is hidden
0126         children: [root.searchField]
0127 
0128         states: State {
0129             when: root.searchField !== null // one the the only, fallback, always active state
0130             AnchorChanges {
0131                 target: root.searchField
0132                 anchors.left: root.searchField && root.searchField.parent ? root.searchField.parent.left : undefined
0133                 anchors.right: root.searchField && root.searchField.parent ? root.searchField.parent.right : undefined
0134             }
0135             PropertyChanges {
0136                 target: root.searchField ? root.searchField.KeyNavigation : null
0137                 tab: scrollView.contentItem
0138                 down: scrollView.contentItem
0139             }
0140         }
0141     }
0142 
0143     padding: 0
0144     topPadding: undefined
0145     leftPadding: undefined
0146     rightPadding: undefined
0147     bottomPadding: undefined
0148     verticalPadding: undefined
0149     horizontalPadding: undefined
0150 
0151     focusPolicy: Qt.NoFocus
0152     activeFocusOnTab: true
0153 
0154     onActiveFocusChanged: {
0155         if (searchField && activeFocus) {
0156             searchField.forceActiveFocus();
0157         }
0158     }
0159 
0160     onVisibleChanged: {
0161         if (!visible) {
0162             popup.close();
0163         }
0164     }
0165 
0166     onSearchFieldChanged: {
0167         __openPopupIfSearchFieldHasActiveFocus();
0168     }
0169 
0170     function __handoverChild(child: Item, oldParent: Item, newParent: Item) {
0171         // It used to be more complicated with QQC2.Control::contentItem
0172         // handover. But plain Items are very simple to deal with, and they
0173         // don't attempt to hide old contentItem by setting their visible=false.
0174         child.parent = newParent;
0175     }
0176 
0177     function __openPopupIfSearchFieldHasActiveFocus() {
0178         if (searchField && searchField.activeFocus && !popup.opened) {
0179             // Don't mess with popups and reparenting inside focus change handler.
0180             // Especially nested popups hate that: it may break all focus management
0181             // on a scene until restart.
0182             Qt.callLater(() => {
0183                 // TODO: Kirigami.OverlayZStacking fails to find and bind to
0184                 // parent logical popup when parent item is itself reparented
0185                 // on the fly.
0186                 //
0187                 // Catch a case of reopening during exit transition. But don't
0188                 // attempt to reorder a visible popup, it doesn't like that.
0189                 if (!popup.visible) {
0190                     if (typeof popup.Kirigami.OverlayZStacking !== "undefined") {
0191                         popup.z = Qt.binding(() => popup.Kirigami.OverlayZStacking.z);
0192                     }
0193                 }
0194                 popup.open();
0195             });
0196         }
0197     }
0198 
0199     function __searchFieldWasAccepted() {
0200         if (autoAccept) {
0201             if (searchField.text.length > 2) {
0202                 accepted()
0203             }
0204         } else if (searchField.text.length === 0) {
0205             popup.close();
0206         } else {
0207             accepted();
0208         }
0209     }
0210 
0211     Connections {
0212         target: root.searchField
0213 
0214         function onActiveFocusChanged() {
0215             root.__openPopupIfSearchFieldHasActiveFocus();
0216         }
0217 
0218         function onAccepted() {
0219             root.__searchFieldWasAccepted();
0220         }
0221     }
0222 
0223     T.Popup {
0224         id: popup
0225 
0226         Component.onCompleted: {
0227             // TODO KF6: port to declarative bindings.
0228             if (typeof Kirigami.OverlayZStacking !== "undefined") {
0229                 Kirigami.OverlayZStacking.layer = Kirigami.OverlayZStacking.Dialog;
0230                 z = Qt.binding(() => Kirigami.OverlayZStacking.z);
0231             }
0232         }
0233 
0234         readonly property real collapsedHeight: (root.searchField ? root.searchField.implicitHeight : 0)
0235             + topMargin + bottomMargin + topPadding + bottomPadding
0236 
0237         // How much vertical space this popup is actually going to take,
0238         // considering that margins will push it inside and shrink if needed.
0239         readonly property real realisticHeight: {
0240             const wantedHeight = (root.searchField ? root.searchField.implicitHeight : 0) + Kirigami.Units.gridUnit * 20;
0241             const overlay = root.QQC2.Overlay.overlay;
0242             if (!overlay) {
0243                 return 0;
0244             }
0245             return Math.min(wantedHeight, overlay.height - topMargin - bottomMargin);
0246         }
0247 
0248         readonly property real realisticContentHeight: realisticHeight - topPadding - bottomPadding
0249 
0250         // y offset from parent/root control if there's not enough space on
0251         // the bottom, so popup is being pushed upward.
0252         readonly property real yOffset: {
0253             const overlay = root.QQC2.Overlay.overlay;
0254             if (!overlay) {
0255                 return 0;
0256             }
0257             return Math.max(-root.Kirigami.ScenePosition.y, Math.min(0, overlay.height - root.Kirigami.ScenePosition.y - realisticHeight));
0258         }
0259 
0260         clip: false
0261         parent: root
0262 
0263         // make sure popup is being pushed in-bounds if it is too large or root control is (partially) out of bounds
0264         margins: 0
0265 
0266         leftPadding: dialogRoundedBackground.border.width
0267         rightPadding: dialogRoundedBackground.border.width
0268         bottomPadding: dialogRoundedBackground.border.width
0269         x: -leftPadding
0270         y: 0 // initial value, will be managed by enter/exit transitions
0271 
0272         implicitWidth: root.width + leftPadding + rightPadding
0273         height: popup.collapsedHeight // initial binding, will be managed by enter/exit transitions
0274 
0275         onVisibleChanged: {
0276             root.searchField.QQC2.ToolTip.hide();
0277             if (visible) {
0278                 root.__handoverChild(root.searchField, root.contentItem, fieldContainer);
0279                 root.searchField.forceActiveFocus();
0280             } else {
0281                 root.__handoverChild(root.searchField, fieldContainer, root.contentItem);
0282             }
0283         }
0284 
0285         onAboutToHide: {
0286             root.searchField.focus = false;
0287         }
0288 
0289         enter: Transition {
0290             SequentialAnimation {
0291                 // cross-fade search field's background with popup's bigger rounded background
0292                 ParallelAnimation {
0293                     NumberAnimation {
0294                         target: root.searchField.background
0295                         property: "opacity"
0296                         to: 0
0297                         easing.type: Easing.OutCubic
0298                         duration: Kirigami.Units.shortDuration
0299                     }
0300                     NumberAnimation {
0301                         target: dialogRoundedBackground
0302                         property: "opacity"
0303                         to: 1
0304                         easing.type: Easing.OutCubic
0305                         duration: Kirigami.Units.shortDuration
0306                     }
0307                 }
0308                 // push Y upward (if needed) and expand at the same time
0309                 ParallelAnimation {
0310                     NumberAnimation {
0311                         property: "y"
0312                         easing.type: Easing.OutCubic
0313                         duration: Kirigami.Units.longDuration
0314                         to: popup.yOffset
0315                     }
0316                     NumberAnimation {
0317                         property: "height"
0318                         easing.type: Easing.OutCubic
0319                         duration: Kirigami.Units.longDuration
0320                         to: popup.realisticHeight
0321                     }
0322                 }
0323             }
0324         }
0325 
0326         // Rebind animated properties in case enter/exit transition was skipped.
0327         onOpened: {
0328             root.searchField.background.opacity = 0;
0329             dialogRoundedBackground.opacity = 1;
0330             // Make sure height stays sensible if window is resized while popup is open.
0331             popup.y = Qt.binding(() => popup.yOffset);
0332             popup.height = Qt.binding(() => popup.realisticHeight);
0333         }
0334 
0335         exit: Transition {
0336             SequentialAnimation {
0337                 // return Y back to root control's position (if needed) and collapse at the same time
0338                 ParallelAnimation {
0339                     NumberAnimation {
0340                         property: "y"
0341                         easing.type: Easing.OutCubic
0342                         duration: Kirigami.Units.longDuration
0343                         to: 0
0344                     }
0345                     NumberAnimation {
0346                         property: "height"
0347                         easing.type: Easing.OutCubic
0348                         duration: Kirigami.Units.longDuration
0349                         to: popup.collapsedHeight
0350                     }
0351                 }
0352                 // cross-fade search field's background with popup's bigger rounded background
0353                 ParallelAnimation {
0354                     NumberAnimation {
0355                         target: root.searchField.background
0356                         property: "opacity"
0357                         to: 1
0358                         easing.type: Easing.OutCubic
0359                         duration: Kirigami.Units.shortDuration
0360                     }
0361                     NumberAnimation {
0362                         target: dialogRoundedBackground
0363                         property: "opacity"
0364                         to: 0
0365                         easing.type: Easing.OutCubic
0366                         duration: Kirigami.Units.shortDuration
0367                     }
0368                 }
0369             }
0370         }
0371 
0372         // Rebind animated properties in case enter/exit transition was skipped.
0373         onClosed: {
0374             root.searchField.background.opacity = 1;
0375             dialogRoundedBackground.opacity = 0;
0376             // Make sure height stays sensible if search field is resized while popup is closed.
0377             popup.y = 0;
0378             popup.height = Qt.binding(() => popup.collapsedHeight);
0379         }
0380 
0381         background: DialogRoundedBackground {
0382             id: dialogRoundedBackground
0383 
0384             // initial value, will be managed by enter/exit transitions
0385             opacity: 0
0386         }
0387 
0388         contentItem: Item {
0389             // clip with rounded corners
0390             layer.enabled: popup.enter.running || popup.exit.running
0391             layer.effect: Kirigami.ShadowedTexture {
0392                 // color is not needed, we are here for the clipping only.
0393                 color: "transparent"
0394                 // border is not needed, as is is already accounted by
0395                 // padding. But radius has to be adjusted for that padding.
0396                 radius: dialogRoundedBackground.radius - popup.leftPadding - popup.bottomPadding
0397             }
0398 
0399             ColumnLayout {
0400                 anchors {
0401                     top: parent.top
0402                     left: parent.left
0403                     right: parent.right
0404                 }
0405                 height: popup.realisticContentHeight
0406                 spacing: 0
0407 
0408                 Item {
0409                     id: fieldContainer
0410                     implicitWidth: root.searchField ? root.searchField.implicitWidth : 0
0411                     implicitHeight: root.searchField ? root.searchField.implicitHeight : 0
0412                     Layout.fillWidth: true
0413                 }
0414 
0415                 Kirigami.Separator {
0416                     Layout.fillWidth: true
0417                 }
0418 
0419                 QQC2.ScrollView {
0420                     id: scrollView
0421 
0422                     Kirigami.Theme.colorSet: Kirigami.Theme.View
0423                     Kirigami.Theme.inherit: false
0424 
0425                     Layout.fillHeight: true
0426                     Layout.fillWidth: true
0427                 }
0428             }
0429         }
0430     }
0431 }