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 }