Kirigami-addons

SearchPopupField.qml
1// SPDX-FileCopyrightText: 2021 Jonah BrĂ¼chert <jbb@kaidan.im>
2// SPDX-FileCopyrightText: 2023 Mathis BrĂ¼chert <mbb@kaidan.im>
3// SPDX-FileCopyrightText: 2023 Carl Schwan <carl@carlschwan.eu>
4// SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
5//
6// SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
7
8import QtQuick 2.15
9import QtQuick.Controls 2.15 as QQC2
10import QtQuick.Templates 2.15 as T
11import QtQuick.Layouts 1.15
12import Qt.labs.qmlmodels 1.0
13import org.kde.kirigami 2.20 as Kirigami
14
15/**
16 * SearchField with a Popup to show autocompletion entries or search results
17 *
18 * @since KirigamiAddons.labs.components 1.0
19 */
20QQC2.Control {
21 id: root
22
23 /**
24 * This property holds the content item of the popup.
25 *
26 * Overflow will be automatically be handled as popupContentItem is
27 * contained inside a ScrollView.
28 *
29 * This is the default element of SearchPopupField.
30 *
31 * ```qml
32 * SearchPopupField {
33 * ListView {
34 * model: SearchModel {}
35 * delegate: QQC2.ItemDelegate {}
36 *
37 * Kirigami.PlaceholderMessage {
38 * id: loadingPlaceholder
39 * anchors.centerIn: parent
40 * width: parent.width - Kirigami.Units.gridUnit * 4
41 * // ...
42 * }
43 * }
44 * }
45 * ```
46 *
47 * @since KirigamiAddons.labs.components 1.0
48 */
49 default property alias popupContentItem: scrollView.contentItem
50
51 /**
52 * This property holds the text of the search field.
53 *
54 * @since KirigamiAddons.labs.components 1.0
55 */
56 property alias text: root.searchField.text
57
58 /**
59 * @brief This property sets whether to delay automatic acceptance of the search input.
60 *
61 * Set this to true if your search is expensive (such as for online
62 * operations or in exceptionally slow data sets) and want to delay it
63 * for 2.5 seconds.
64 *
65 * @note If you must have immediate feedback (filter-style), use the
66 * text property directly instead of accepted()
67 *
68 * default: ``false``
69 *
70 * @since KirigamiAddons.labs.components 1.0
71 */
72 property alias delaySearch: root.searchField.delaySearch
73
74 /**
75 * @brief This property sets whether the accepted signal is fired automatically
76 * when the text is changed.
77 *
78 * Setting this to false will require that the user presses return or enter
79 * (the same way a QtQuick.Controls.TextInput works).
80 *
81 * default: ``false``
82 *
83 * @since KirigamiAddons.labs.components 1.0
84 */
85 property alias autoAccept: root.searchField.autoAccept
86
87 /**
88 * This property holds whether there is space available on the left.
89 *
90 * This is used by the left shadow.
91 *
92 * @since KirigamiAddons.labs.components 1.0
93 * @deprecated Was not really used by anything.
94 */
95 property bool spaceAvailableLeft: true
96
97 /**
98 * This property holds whether there is space available on the left.
99 *
100 * This is used by the right shadow.
101 *
102 * @since KirigamiAddons.labs.components 1.0
103 * @deprecated Was not really used by anything.
104 */
105 property bool spaceAvailableRight: true
107 /**
108 * @brief This hold the focus state of the internal SearchField.
109 */
110 property alias fieldFocus: root.searchField.focus
111
112 /**
113 * This signal is triggered when the user trigger a search.
114 */
115 signal accepted()
116
117 property alias popup: popup
118
119 property Kirigami.SearchField searchField: Kirigami.SearchField {}
120
121 contentItem: Item {
122 implicitHeight: root.searchField ? root.searchField.implicitHeight : 0
123 implicitWidth: root.searchField ? root.searchField.implicitWidth : 0
124
125 // by default popup is hidden
126 children: [root.searchField]
127
128 states: State {
129 when: root.searchField !== null // one the the only, fallback, always active state
130 AnchorChanges {
131 target: root.searchField
132 anchors.left: root.searchField && root.searchField.parent ? root.searchField.parent.left : undefined
133 anchors.right: root.searchField && root.searchField.parent ? root.searchField.parent.right : undefined
134 }
135 PropertyChanges {
136 target: root.searchField ? root.searchField.KeyNavigation : null
137 tab: scrollView.contentItem
138 down: scrollView.contentItem
139 }
140 }
141 }
142
143 padding: 0
144 topPadding: undefined
145 leftPadding: undefined
146 rightPadding: undefined
147 bottomPadding: undefined
148 verticalPadding: undefined
149 horizontalPadding: undefined
150
151 focusPolicy: Qt.NoFocus
152 activeFocusOnTab: true
153
154 onActiveFocusChanged: {
155 if (searchField && activeFocus) {
156 searchField.forceActiveFocus();
157 }
158 }
159
160 onVisibleChanged: {
161 if (!visible) {
162 popup.close();
163 }
164 }
165
166 onSearchFieldChanged: {
167 __openPopupIfSearchFieldHasActiveFocus();
168 }
169
170 function __handoverChild(child: Item, oldParent: Item, newParent: Item) {
171 // It used to be more complicated with QQC2.Control::contentItem
172 // handover. But plain Items are very simple to deal with, and they
173 // don't attempt to hide old contentItem by setting their visible=false.
174 child.parent = newParent;
175 }
176
177 function __openPopupIfSearchFieldHasActiveFocus() {
178 if (searchField && searchField.activeFocus && !popup.opened) {
179 // Don't mess with popups and reparenting inside focus change handler.
180 // Especially nested popups hate that: it may break all focus management
181 // on a scene until restart.
182 Qt.callLater(() => {
183 // TODO: Kirigami.OverlayZStacking fails to find and bind to
184 // parent logical popup when parent item is itself reparented
185 // on the fly.
186 //
187 // Catch a case of reopening during exit transition. But don't
188 // attempt to reorder a visible popup, it doesn't like that.
189 if (!popup.visible) {
190 if (typeof popup.Kirigami.OverlayZStacking !== "undefined") {
191 popup.z = Qt.binding(() => popup.Kirigami.OverlayZStacking.z);
192 }
193 }
194 popup.open();
195 });
196 }
197 }
198
199 function __searchFieldWasAccepted() {
200 if (autoAccept) {
201 if (searchField.text.length > 2) {
202 accepted()
203 }
204 } else if (searchField.text.length === 0) {
205 popup.close();
206 } else {
207 accepted();
208 }
209 }
210
211 Connections {
212 target: root.searchField
213
214 function onActiveFocusChanged() {
215 root.__openPopupIfSearchFieldHasActiveFocus();
216 }
217
218 function onAccepted() {
219 root.__searchFieldWasAccepted();
220 }
221 }
222
223 T.Popup {
224 id: popup
225
226 Component.onCompleted: {
227 // TODO KF6: port to declarative bindings.
228 if (typeof Kirigami.OverlayZStacking !== "undefined") {
229 Kirigami.OverlayZStacking.layer = Kirigami.OverlayZStacking.Dialog;
230 z = Qt.binding(() => Kirigami.OverlayZStacking.z);
231 }
232 }
233
234 readonly property real collapsedHeight: (root.searchField ? root.searchField.implicitHeight : 0)
235 + topMargin + bottomMargin + topPadding + bottomPadding
236
237 // How much vertical space this popup is actually going to take,
238 // considering that margins will push it inside and shrink if needed.
239 readonly property real realisticHeight: {
240 const wantedHeight = (root.searchField ? root.searchField.implicitHeight : 0) + Kirigami.Units.gridUnit * 20;
241 const overlay = root.QQC2.Overlay.overlay;
242 if (!overlay) {
243 return 0;
244 }
245 return Math.min(wantedHeight, overlay.height - topMargin - bottomMargin);
246 }
247
248 readonly property real realisticContentHeight: realisticHeight - topPadding - bottomPadding
249
250 // y offset from parent/root control if there's not enough space on
251 // the bottom, so popup is being pushed upward.
252 readonly property real yOffset: {
253 const overlay = root.QQC2.Overlay.overlay;
254 if (!overlay) {
255 return 0;
256 }
257 return Math.max(-root.Kirigami.ScenePosition.y, Math.min(0, overlay.height - root.Kirigami.ScenePosition.y - realisticHeight));
258 }
259
260 clip: false
261 parent: root
262
263 // make sure popup is being pushed in-bounds if it is too large or root control is (partially) out of bounds
264 margins: 0
265
266 leftPadding: dialogRoundedBackground.border.width
267 rightPadding: dialogRoundedBackground.border.width
268 bottomPadding: dialogRoundedBackground.border.width
269 x: -leftPadding
270 y: 0 // initial value, will be managed by enter/exit transitions
271
272 implicitWidth: root.width + leftPadding + rightPadding
273 height: popup.collapsedHeight // initial binding, will be managed by enter/exit transitions
274
275 onVisibleChanged: {
276 root.searchField.QQC2.ToolTip.hide();
277 if (visible) {
278 root.__handoverChild(root.searchField, root.contentItem, fieldContainer);
279 root.searchField.forceActiveFocus();
280 } else {
281 root.__handoverChild(root.searchField, fieldContainer, root.contentItem);
282 }
283 }
284
285 onAboutToHide: {
286 root.searchField.focus = false;
287 }
288
289 enter: Transition {
290 SequentialAnimation {
291 // cross-fade search field's background with popup's bigger rounded background
292 ParallelAnimation {
293 NumberAnimation {
294 target: root.searchField.background
295 property: "opacity"
296 to: 0
297 easing.type: Easing.OutCubic
298 duration: Kirigami.Units.shortDuration
299 }
300 NumberAnimation {
301 target: dialogRoundedBackground
302 property: "opacity"
303 to: 1
304 easing.type: Easing.OutCubic
305 duration: Kirigami.Units.shortDuration
306 }
307 }
308 // push Y upward (if needed) and expand at the same time
309 ParallelAnimation {
310 NumberAnimation {
311 property: "y"
312 easing.type: Easing.OutCubic
313 duration: Kirigami.Units.longDuration
314 to: popup.yOffset
315 }
316 NumberAnimation {
317 property: "height"
318 easing.type: Easing.OutCubic
319 duration: Kirigami.Units.longDuration
320 to: popup.realisticHeight
321 }
322 }
323 }
324 }
325
326 // Rebind animated properties in case enter/exit transition was skipped.
327 onOpened: {
328 root.searchField.background.opacity = 0;
329 dialogRoundedBackground.opacity = 1;
330 // Make sure height stays sensible if window is resized while popup is open.
331 popup.y = Qt.binding(() => popup.yOffset);
332 popup.height = Qt.binding(() => popup.realisticHeight);
333 }
334
335 exit: Transition {
336 SequentialAnimation {
337 // return Y back to root control's position (if needed) and collapse at the same time
338 ParallelAnimation {
339 NumberAnimation {
340 property: "y"
341 easing.type: Easing.OutCubic
342 duration: Kirigami.Units.longDuration
343 to: 0
344 }
345 NumberAnimation {
346 property: "height"
347 easing.type: Easing.OutCubic
348 duration: Kirigami.Units.longDuration
349 to: popup.collapsedHeight
350 }
351 }
352 // cross-fade search field's background with popup's bigger rounded background
353 ParallelAnimation {
354 NumberAnimation {
355 target: root.searchField.background
356 property: "opacity"
357 to: 1
358 easing.type: Easing.OutCubic
359 duration: Kirigami.Units.shortDuration
360 }
361 NumberAnimation {
362 target: dialogRoundedBackground
363 property: "opacity"
364 to: 0
365 easing.type: Easing.OutCubic
366 duration: Kirigami.Units.shortDuration
367 }
368 }
369 }
370 }
371
372 // Rebind animated properties in case enter/exit transition was skipped.
373 onClosed: {
374 root.searchField.background.opacity = 1;
375 dialogRoundedBackground.opacity = 0;
376 // Make sure height stays sensible if search field is resized while popup is closed.
377 popup.y = 0;
378 popup.height = Qt.binding(() => popup.collapsedHeight);
379 }
380
381 background: DialogRoundedBackground {
382 id: dialogRoundedBackground
383
384 // initial value, will be managed by enter/exit transitions
385 opacity: 0
386 }
387
388 contentItem: Item {
389 // clip with rounded corners
390 layer.enabled: popup.enter.running || popup.exit.running
391 layer.effect: Kirigami.ShadowedTexture {
392 // color is not needed, we are here for the clipping only.
393 color: "transparent"
394 // border is not needed, as is is already accounted by
395 // padding. But radius has to be adjusted for that padding.
396 radius: dialogRoundedBackground.radius - popup.leftPadding - popup.bottomPadding
397 }
398
399 ColumnLayout {
400 anchors {
401 top: parent.top
402 left: parent.left
403 right: parent.right
404 }
405 height: popup.realisticContentHeight
406 spacing: 0
407
408 Item {
409 id: fieldContainer
410 implicitWidth: root.searchField ? root.searchField.implicitWidth : 0
411 implicitHeight: root.searchField ? root.searchField.implicitHeight : 0
412 Layout.fillWidth: true
413 }
414
415 Kirigami.Separator {
416 Layout.fillWidth: true
417 }
418
419 QQC2.ScrollView {
420 id: scrollView
421
422 Kirigami.Theme.colorSet: Kirigami.Theme.View
423 Kirigami.Theme.inherit: false
424
425 Layout.fillHeight: true
426 Layout.fillWidth: true
427 }
428 }
429 }
430 }
431}
Stylish background for dialogs.
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 3 2024 11:46:57 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.