MauiKit Controls

SelectionBar.qml
1/*
2 * Copyright 2018 Camilo Higuita <milo.h@aol.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Library General Public License as
6 * published by the Free Software Foundation; either version 2, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details
13 *
14 * You should have received a copy of the GNU Library General Public
15 * License along with this program; if not, write to the
16 * Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 */
19
20import QtQuick
21import QtQuick.Controls
22import QtQuick.Layouts
23import QtQuick.Window
24
25import org.mauikit.controls as Maui
26
27import QtQuick.Effects
28
29/**
30 * @inherit QtQuick.Item
31 * @brief A bar to group selected items and a list of actions to perform to such selection.
32 *
33 * <a href="https://doc.qt.io/qt-6/qml-qtquick-controls-item.html">This controls inherits from QQC2 Item, to checkout its inherited properties refer to the Qt Docs.</a>
34 *
35 * The list of actions is represented by a set of buttons in a horizontal row layout.
36 * @see actions
37 *
38 * This control provides methods to append and query elements added to it. To add elements, it is necesary to map them,
39 * so each item has an unique ID referred here as an URI.
40 * @see append
41 *
42 *
43 * @image html Misc/selectionbar.png
44 *
45 * @section notes Notes
46 *
47 * By default it is hidden when the selection is empty.
48 * @see count
49 *
50 * The SelectionBar is usually placed as the `footer` element of a Page.
51 * This control is styled as a floating bar, so it can be placed on top of its parent contents; due to this behavior it is a good idea to pair it with a MauiKit Page as its footer, and set the Page property `floatingFooter: true`.
52 *
53 * Most of the times this will be uses along with a browsing view, such as a ListBrowser or GridBrowser, which will list the items to be added or removed from the selection.
54 *
55 * Here we have three components that can be intertwined: Page, ListBrowser and the SelectionBar.
56 * For the floating selection bar to not get on the way of the contents, the Page property `flickable` needs to be set to the flickable element of the browsing view, such as the ListBrowser::flickable.
57 * @see ListBrowser::flickable
58 * @see Page::flickable
59 *
60 * Below you will find a more complete example of the functionality of these three controls out together.
61 *
62 * @code
63 * Maui.Page
64 * {
65 * anchors.fill: parent
66 *
67 * Maui.Controls.showCSD: true
68 *
69 * floatingFooter: true
70 * flickable: _listBrowser.flickable //helps to keep the content from under the selection bar at the end.
71 *
72 * headBar.leftContent: Switch
73 * {
74 * text: "Single selection"
75 * checked: _selectionBar.singleSelection
76 * onToggled: _selectionBar.singleSelection = !_selectionBar.singleSelection
77 * }
78 *
79 * Maui.ListBrowser
80 * {
81 * id: _listBrowser
82 *
83 * anchors.fill: parent
84 *
85 * model: 60
86 *
87 * delegate: Maui.ListBrowserDelegate
88 * {
89 * id: _delegate
90 *
91 * property string id : index // we need an unique ID for the selectio nbar
92 *
93 * width: ListView.view.width
94 *
95 * label1.text: "An example delegate."
96 * label2.text: "The ID of this element is " + id
97 *
98 * iconSource: "folder"
99 *
100 * checkable: true
101 *
102 * Connections
103 * {
104 * target: _selectionBar
105 * function onUriRemoved(uri) //watch when a uri is removed from the selection bar
106 * {
107 * if(uri == _delegate.id)
108 * {
109 * _delegate.checked = false
110 * }
111 * }
112 *
113 * function onUriAdded(uri) //watch when an uri is successfully added and mark the delegate as checked
114 * {
115 * if(uri == _delegate.id)
116 * {
117 * _delegate.checked = true
118 * }
119 * }
120 *
121 * function onCleared() //watch when the selection has been cleared and uncheck all the delegates
122 * {
123 * _delegate.checked = false
124 * }
125 * }
126 *
127 * onToggled: (state) =>
128 * {
129 * if(state)
130 * {
131 * _selectionBar.append(_delegate.id, ({'title': "Testing"}))
132 * }else
133 * {
134 * _selectionBar.removeAtUri(_delegate.id)
135 * }
136 * } // when the item is toggled, we mark it as checked and add it to the selection bar, otherwise we unchecked it and remove it from selection.
137 * }
138 * }
139 *
140 * footer: Maui.SelectionBar
141 * {
142 * id: _selectionBar
143 *
144 * anchors.horizontalCenter: parent.horizontalCenter
145 * width: Math.min(parent.width-(Maui.Style.space.medium*2), implicitWidth)
146 * maxListHeight: root.height - (Maui.Style.contentMargins*2)
147 *
148 * Action
149 * {
150 * icon.name: "love"
151 * onTriggered: console.log(_selectionBar.getSelectedUrisString())
152 * }
153 *
154 * Action
155 * {
156 * icon.name: "folder"
157 * onTriggered: console.log(_selectionBar.contains("0"))
158 * }
159 *
160 * Action
161 * {
162 * icon.name: "list-add"
163 * }
164 *
165 * onExitClicked: clear()
166 * }
167 * }
168 * @endcode
170 * <a href="https://invent.kde.org/maui/mauikit/-/blob/qt6-2/examples/SelectionBar.qml">You can find a more complete example at this link.</a>
171 *
172 */
173Item
174{
175 id: control
176
177 Maui.Theme.inherit: false
178 Maui.Theme.colorSet: Maui.Theme.Complementary
179
180 implicitHeight: _layout.implicitHeight + Maui.Style.space.big
181 implicitWidth: _layout.implicitWidth
182
183 /**
184 * @brief Whether the bar is currently hidden. This is not the same as `visible`.
185 * It is hidden when there is not items in the selection.
186 */
187 readonly property bool hidden : count === 0
188
189 onHiddenChanged:
190 {
191 if(hidden && !singleSelection)
192 {
193 control.close()
194 }else
195 {
196 control.open()
198 }
199
200 visible: false
201 focus: true
202 Keys.enabled: true
203
204 /**
205 * @brief Default list of QQC2 Action; the actions are represented as buttons.
206 * All of the actions listed in here will be visible, to hide some use the `hiddenActions` property.
207 * @see hiddenActions
208 */
209 default property list<Action> actions
210
211 /**
212 * @brief List of action that won't be shown inside of the bar, but instead will always be hidden and listed in a dedicated overflow menu button.
213 */
214 property list<Action> hiddenActions
215
216 /**
217 * @brief The preferred display mode for the visible action buttons.
218 * By default this is set to `ToolButton.TextBesideIcon`.
219 */
220 property int display : ToolButton.TextBesideIcon
221
222 /**
223 * @brief The list of items can be displayed in a popup. This property defines the maximum height the popup list.
224 * By default this is set to `400`.
225 */
226 property int maxListHeight : 400
227
228 /**
229 * @brief By default the selection bar was designed to be floating and thus has rounded border corners.
230 * This property allows to change the border radius.
231 * By default this is set to `Style.radiusV`.
232 */
233 property int radius: Maui.Style.radiusV
234
235 /**
236 * @brief Whether the control only accepts a single item.
237 * If single selection is set to true then only a single item can be appended, if another item is added then it replaces the previous one.
238 * By default this is set to `false`.
239 **/
240 property bool singleSelection: false
241
242 /**
243 * @brief The array list of the URIs associated to the selected items.
244 * @see items
245 * @property var SelectionBar::uris
246 */
247 readonly property alias uris: _private._uris
248
249 /**
250 * @brief The array list of the items selected.
251 * @property var SelectionBar::items
252 */
253 readonly property alias items: _private._items
254
255 /**
256 * @brief Total amount of items selected.
257 * @property int SelectionBar::count
258 */
259 readonly property alias count : _urisModel.count
260
261 /**
262 * @brief Delegate to be used in popup list.
263 * By default this is set to use a MauiKit ListBrowserDelegate.
264 * The model use to feed the popup list is a QQC2 ListModel, populated by the `item` passed as the argument to the `append` method.
265 * @see append
266 */
267 property Component listDelegate: Maui.ListBrowserDelegate
268 {
269 id: delegate
271 width: ListView.view.width
272
273 Maui.Theme.backgroundColor: "transparent"
274 Maui.Theme.textColor: control.Maui.Theme.textColor
276 iconVisible: false
277 label1.text: model.uri
278
279 checkable: true
280 checked: true
281 onToggled: control.removeAtIndex(index)
282
283 onClicked: control.itemClicked(index)
284 onPressAndHold: control.itemPressAndHold(index)
286
287 /**
288 * @brief Emitted when the selection is cleared either by the constrain of the single selection or by manually calling the clear method.
289 */
290 signal cleared()
291
292 /**
293 * @brief Emitted when close button is pressed or the Escape keyboard shortcut invoked.
294 */
295 signal exitClicked()
296
297 /**
298 * @brief Emitted when an item in the popup list view is clicked.
299 * @paran index the index number of the item clicked. Use the `itemAt` function to get to the item.
300 * @see itemAt
301 */
302 signal itemClicked(int index)
303
304 /**
305 * @brief Emitted when an item in the popup list view is pressed for a long time.
306 * @paran index the index number of the item clicked. Use the `itemAt` function to get to the item.
307 * @see itemAt
308 */
309 signal itemPressAndHold(int index)
310
311 /**
312 * @brief Emitted when an item is newly added to the selection.
313 * @param item the item map passed to the `append` function.
314 */
315 signal itemAdded(var item)
316
317 /**
318 * @brief Emitted when an item has been removed from the selection.
319 * @param item the item map passed to the `append` function.
320 */
321 signal itemRemoved(var item)
322
323 /**
324 * @brief Emitted when an item is newly added to the selection. This signal only sends the URI of the item.
325 * @param uri the URI identifier
326 */
327 signal uriAdded(string uri)
328
329 /**
330 * @brief Emitted when an item has been removed from the selection. This signal only sends the URI of the item.
331 * @param uri the URI identifier
332 */
333 signal uriRemoved(string uri)
335 /**
336 * @brief Emitted when an empty area of the selection bar has been clicked.
337 * @param mouse the object with information of the event
338 */
339 signal clicked(var mouse)
340
341 /**
342 * @brief Emitted when an empty area of the selection bar has been right clicked.
343 * @param mouse the object with information of the event
344 */
345 signal rightClicked(var mouse)
346
347 /**
348 * @brief Emitted when a group of URLs has been dropped onto the selection bar area.
349 * @param uris the list of urls
350 */
351 signal urisDropped(var uris)
352
353 QtObject
354 {
355 id: _private
356 property var _uris : []
357 property var _items : []
358 }
359
360 ListModel
361 {
362 id: _urisModel
363 }
365 Loader
366 {
367 id: _loader
368 active: control.visible
369 }
370
371 Component
372 {
373 id: _listContainerComponent
374
375 Maui.PopupPage
376 {
377 persistent: false
378
379 stack: Maui.ListBrowser
380 {
381 id: selectionList
382
383 Layout.fillHeight: true
384 Layout.fillWidth: true
385 model: _urisModel
386
387 delegate: control.listDelegate
388 }
389
390 actions: control.actions
391 }
392 }
393
394 ParallelAnimation
395 {
396 id: _openAnimation
397 NumberAnimation
398 {
399 target: _layout
400 property: "y"
401 from: _layout.height
402 to: Maui.Style.space.big/2
403 duration: Maui.Style.units.longDuration*1
404 easing.type: Easing.OutBack
405 }
406
407 NumberAnimation
408 {
409 target: _layout
410 property: "scale"
411 from: 0.5
412 to: 1
413 duration: Maui.Style.units.longDuration*1
414 easing.type: Easing.OutQuad
415 }
416 }
417
418 ParallelAnimation
419 {
420 id: _closeAnimation
421 NumberAnimation
422 {
423 target: _layout
424 property: "y"
425 from: Maui.Style.space.big/2
426 to: _layout.height
427 duration: Maui.Style.units.longDuration*1
428 easing.type: Easing.InBack
429
430 }
431
432 NumberAnimation
433 {
434 target: _layout
435 property: "scale"
436 from: 1
437 to: 0.5
438 duration: Maui.Style.units.longDuration*1
439 easing.type: Easing.InQuad
440
441 }
442
443 onFinished: control.visible = false
444 }
445
446 Maui.ToolBar
447 {
448 id: _layout
449 width: control.width
450 padding: Maui.Style.space.medium
451
452 Maui.Theme.inherit: false
453 Maui.Theme.colorSet: Maui.Theme.Complementary
454
455 forceCenterMiddleContent: false
456 position: ToolBar.Footer
457
458 leftContent: Loader
459 {
460 asynchronous: true
461 active: !control.singleSelection && control.count > 1
462 visible: active
463
464 sourceComponent: ToolButton
465 {
466 id: _counter
467
468 text: control.count
469 font.bold: true
470 font.weight: Font.Black
471 Maui.Theme.colorSet: control.Maui.Theme.colorSet
472 Maui.Theme.backgroundColor: _loader.item && _loader.item.visible ?
473 Maui.Theme.highlightColor : Qt.tint(control.Maui.Theme.textColor, Qt.rgba(control.Maui.Theme.backgroundColor.r, control.Maui.Theme.backgroundColor.g, control.Maui.Theme.backgroundColor.b, 0.9))
474
475 flat: false
476
477 MouseArea
478 {
479 id: _mouseArea
480 anchors.fill: parent
481 propagateComposedEvents: true
482 property int startX
483 property int startY
484 Drag.active: drag.active
485 Drag.hotSpot.x: 0
486 Drag.hotSpot.y: 0
487 Drag.dragType: Drag.Automatic
488 Drag.supportedActions: Qt.CopyAction
489 Drag.keys: ["text/plain","text/uri-list"]
490
491 onPressed: (mouse) =>
492 {
493 if( mouse.source !== Qt.MouseEventSynthesizedByQt)
494 {
495 drag.target = _counter
496 _counter.grabToImage(function(result)
497 {
498 _mouseArea.Drag.imageSource = result.url
499 })
500
501 _mouseArea.Drag.mimeData = { "text/uri-list": control.uris.join("\n")}
502
503 startX = _counter.x
504 startY = _counter.y
505
506 }else
507 {
508 mouse.accepted = false
509 }
510 }
511
512 onReleased :
513 {
514 _counter.x = startX
515 _counter.y = startY
516 }
517
518 onClicked:
519 {
520 if(!_loader.item)
521 {
522 _loader.sourceComponent = _listContainerComponent
523 }
524 _loader.item.open()
525 console.log("Opening list")
526 }
527 }
528 }
529 }
530
531 Repeater
532 {
533 model: control.actions
534
535 ToolButton
536 {
537 Maui.Theme.inherit: false
538 Maui.Theme.colorSet: Maui.Theme.Complementary
539
540 action: modelData
541 display: control.display
542 ToolTip.delay: 1000
543 ToolTip.timeout: 5000
544 ToolTip.visible: hovered || pressed && action.text
545 ToolTip.text: action.text
546 }
547 }
548
549 Maui.ToolButtonMenu
550 {
551 Maui.Theme.inherit: false
552 Maui.Theme.colorSet: Maui.Theme.Complementary
553
554 icon.name: "overflow-menu"
555 visible: control.hiddenActions.length > 0
556 Repeater
557 {
558 model: control.hiddenActions
559 delegate: MenuItem
560 {
561 action: modelData
562 Maui.Controls.status: modelData.Maui.Controls.status
563 }
564 }
565 }
566
567 rightContent: Maui.CloseButton
568 {
569 Maui.Theme.inherit: false
570 Maui.Theme.colorSet: Maui.Theme.Complementary
571
572 onClicked:
573 {
574 control.exitClicked()
575 }
576 }
577
578 background: Rectangle
579 {
580 id: bg
581 color: Maui.Theme.backgroundColor
582 radius: control.radius
583
584 Behavior on color
585 {
586 Maui.ColorTransition {}
587 }
588
589 MouseArea
590 {
591 anchors.fill: parent
592 acceptedButtons: Qt.RightButton | Qt.LeftButton
593 propagateComposedEvents: false
594 preventStealing: true
595
596 onClicked: (mouse) =>
597 {
598 if(!Maui.Handy.isMobile && mouse.button === Qt.RightButton)
599 control.rightClicked(mouse)
600 else
601 control.clicked(mouse)
602 }
603
604 onPressAndHold : (mouse) =>
605 {
606 if(Maui.Handy.isMobile)
607 control.rightClicked(mouse)
608 }
609 }
610
611 Maui.Rectangle
612 {
613 opacity: 0.2
614 anchors.fill: parent
615 anchors.margins: 4
616 visible: _dropArea.containsDrag
617 color: "transparent"
618 borderColor: Maui.Theme.textColor
619 solidBorder: false
620 }
621
622 DropArea
623 {
624 id: _dropArea
625 anchors.fill: parent
626 onDropped:
627 {
628 control.urisDropped(drop.urls)
629 }
630 }
631
632 layer.enabled: GraphicsInfo.api !== GraphicsInfo.Software
633 layer.effect: MultiEffect
634 {
635 autoPaddingEnabled: true
636 shadowEnabled: true
637 shadowColor: "#80000000"
638 }
639 }
640 }
641
642 Keys.onEscapePressed:
643 {
644 control.exitClicked();
645 }
646
647 Keys.onBackPressed: (event) =>
648 {
649 control.exitClicked();
650 event.accepted = true
651 }
652
653 /**
654 * @brief Removes all the items from the selection.
655 * Triggers the `cleared` signal.
656 * @see cleared
657 */
658 function clear()
659 {
660 _private._uris = []
661 _private._items = []
662 _urisModel.clear()
663 control.cleared()
664 }
665
666 /**
667 * @brief Returns an item at the given index
668 * @param index the index number of the item.
669 * @note Note that this is the index of the internal list on how items were added, and not the original index of the source list used to make the selection.
670 * @return the requested item/map if it is found, otherwise null.
671 */
672 function itemAt(index)
673 {
674 if(index < 0 || index > control.count)
675 {
676 return
677 }
678 return _urisModel.get(index)
679 }
680
681 /**
682 * @brief Remove a single item at the given index
683 * @param index index of the item in the selection list
684 */
685 function removeAtIndex(index)
686 {
687 if(index < 0)
688 {
689 return
690 }
691
692 const item = _urisModel.get(index)
693 const uri = item.uri
694
695 if(contains(uri))
696 {
697 _private._uris.splice(index, 1)
698 _private._items.splice(index, 1)
699 _urisModel.remove(index)
700 control.itemRemoved(item)
701 control.uriRemoved(uri)
702 }
703 }
704
705 /**
706 * @brief Removes an item from the selection at the given URI
707 * @param uri the URI used to append the item
708 */
709 function removeAtUri(uri)
710 {
711 removeAtIndex(indexOf(uri))
712 }
713
714 /**
715 * @brief Returns the index of an item in the selection given its URI
716 * @param uri the URI used to append the item
717 * @return the index number of the found item, otherwise `-1`
718 *
719 */
720 function indexOf(uri)
721 {
722 return _private._uris.indexOf(uri)
723 }
724
725 /**
726 * @brief Appends a new item to the selection associated to the given URI
727 * @param uri the URI to be associated with the item
728 * @param item a map to be used to represent the item in the model. For example a valid item map would be: `({'title': "A title", 'icon': "love"})`
729 */
730 function append(uri, item)
731 {
732 if(control.singleSelection)
733 {
734 clear()
735 }
736
737 if(!contains(uri) || control.singleSelection)
738 {
739 _private._items.push(item)
740 _private._uris.push(uri)
741
742 item.uri = uri
743 _urisModel.append(item)
744 control.itemAdded(item)
745 control.uriAdded(uri)
746 }
747 }
748
749 /**
750 * @brief Returns a single string with all the URIs separated by a comma.
751 */
752 function getSelectedUrisString()
753 {
754 return String(""+_private._uris.join(","))
755 }
756
757 /**
758 * @brief Returns whether the selection contains an item associated to the given URI.
759 * @param uri the URI used to append the item
760 */
761 function contains(uri)
762 {
763 return _private._uris.includes(uri)
764 }
765
766 /**
767 * @brief Forces to open the selection bar, if hidden.
768 */
769 function open()
770 {
771 if(control.visible)
772 {
773 return;
774 }
775
776 control.visible = true
777 _openAnimation.start()
778 }
779
780 /**
781 * @brief Forces to close the selection bar, if visible.
782 */
783 function close()
784 {
785 if(!control.visible)
786 {
787 return
788 }
789 _closeAnimation.start()
790 }
791}
void uriRemoved(string uri)
Emitted when an item has been removed from the selection.
void itemPressAndHold(int index)
Emitted when an item in the popup list view is pressed for a long time.
int maxListHeight
The list of items can be displayed in a popup.
alias uris
The array list of the URIs associated to the selected items.
void itemRemoved(var item)
Emitted when an item has been removed from the selection.
alias count
Total amount of items selected.
int radius
By default the selection bar was designed to be floating and thus has rounded border corners.
void urisDropped(var uris)
Emitted when a group of URLs has been dropped onto the selection bar area.
void itemClicked(int index)
Emitted when an item in the popup list view is clicked.
void cleared()
Emitted when the selection is cleared either by the constrain of the single selection or by manually ...
int display
The preferred display mode for the visible action buttons.
list< Action > actions
Default list of QQC2 Action; the actions are represented as buttons.
void itemAdded(var item)
Emitted when an item is newly added to the selection.
Component listDelegate
Delegate to be used in popup list.
list< Action > hiddenActions
List of action that won't be shown inside of the bar, but instead will always be hidden and listed in...
void uriAdded(string uri)
Emitted when an item is newly added to the selection.
void clicked(var mouse)
Emitted when an empty area of the selection bar has been clicked.
void exitClicked()
Emitted when close button is pressed or the Escape keyboard shortcut invoked.
bool singleSelection
Whether the control only accepts a single item.
alias items
The array list of the items selected.
void rightClicked(var mouse)
Emitted when an empty area of the selection bar has been right clicked.
An alternative to QQC2 ToolBar, with a custom horizontal layout - divided into three main sections - ...
Definition ToolBar.qml:115
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
KIOWIDGETS_EXPORT DropJob * drop(const QDropEvent *dropEvent, const QUrl &destUrl, DropJobFlags dropjobFlags, JobFlags flags=DefaultFlags)
KGuiItem clear()
const QList< QKeySequence > & close()
const QList< QKeySequence > & open()
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri May 2 2025 11:57:11 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.