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

KDE's Doxygen guidelines are available online.