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 }