Create ExpandableListItem
Summary: This patch creates the `ExpandableListItem`, a re-usable PlasmaExtras item that can be used for the views in various Plasma applet pop-ups, such as Vaults, Printers, Bluetooth, Networks, and Device Notifier. This way those applets can share more code and not have to implement this paradigm themselves in five different ways, as they currently do. All of these applets currently use slightly different visual styles. For example the network applet uses a pushbutton with no icon as its "default action" button, while other applets use icons-only toolbuttons. I tried my best to create a component that's flexible but also consistent, so various applets that adopt it will see minor visual changes as a result. Hopefully this is acceptable. Closes T12812 Depends on D28144 Test Plan: {F8183650} Reviewers: #vdg, #plasma, davidedmundson Reviewed By: #plasma, davidedmundson Subscribers: mart, davidedmundson, bruns, niccolove, cblack, davidre, kde-frameworks-devel Tags: #frameworks Maniphest Tasks: T12812 Differential Revision: https://phabricator.kde.org/D28033
This commit is contained in:
parent
0739113a44
commit
3bd31386ab
@ -0,0 +1,565 @@
|
||||
/*
|
||||
* Copyright 2020 Nate Graham <nate@kde.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Library General Public License as
|
||||
* published by the Free Software Foundation; either version 2, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Library General Public License for more details
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this program; if not, write to the
|
||||
* Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Layouts 1.1
|
||||
import org.kde.plasma.core 2.0 as PlasmaCore
|
||||
import org.kde.plasma.components 2.0 as PlasmaComponents // for Highlight
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents3
|
||||
import org.kde.plasma.extras 2.0 as PlasmaExtras
|
||||
|
||||
/**
|
||||
* A list item that expands when clicked to show additional actions or a custom
|
||||
* view. The list item has a standardized appearance, with an icon on the left
|
||||
* badged with an optional emblem, a title and optional subtitle to the right,
|
||||
* an optional default action button, and a button to expand and collapse the
|
||||
* list item.
|
||||
*
|
||||
* When expanded, the list item shows one of two views:
|
||||
* - A list of contextually-appropriate actions if contextualActionsModel has
|
||||
* been defined and customExpandedViewContent has not been defined.
|
||||
* - A custom view if customExpandedViewContent has been defined and
|
||||
* contextualActionsModel has not been defined.
|
||||
*
|
||||
* It is not valid to define both or neither; only define one.
|
||||
*
|
||||
* Note: this component should only be used for lists where the maximum number
|
||||
* of items is very low, ideally less than 10. For longer lists, consider using
|
||||
* a different paradigm.
|
||||
*
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* @code
|
||||
* import org.kde.plasma.extras 2.0 as PlasmaExtras
|
||||
* [...]
|
||||
* PlasmaExtras.ScrollArea {
|
||||
* ListView {
|
||||
* anchors.fill: parent
|
||||
* focus: true
|
||||
* currentIndex: -1
|
||||
* clip: true
|
||||
* model: myModel
|
||||
* highlight: PlasmaComponents.Highlight {}
|
||||
* highlightMoveDuration: units.longDuration
|
||||
* highlightResizeDuration: units.longDuration
|
||||
* delegate: PlasmaExtras.ExpandableListItem {
|
||||
* icon: model.iconName
|
||||
* iconEmblem: model.isPaused ? "emblem-pause" : ""
|
||||
* title: model.name
|
||||
* subtitle: model.subtitle
|
||||
* isDefault: model.isDefault
|
||||
* defaultActionButtonAction: Action {
|
||||
* icon.name: model.isPaused ? "media-playback-start" : "media-playback-pause"
|
||||
* text: model.isPaused ? i18n("Resume") : i18n("Pause")
|
||||
* onTriggered: {
|
||||
* if (model.isPaused) {
|
||||
* model.resume(model.name);
|
||||
* } else {
|
||||
* model.pause(model.name);
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* contextualActionsModel: [
|
||||
* Action {
|
||||
* icon.name: "configure"
|
||||
* text: i18n("Configure...")
|
||||
* onTriggered: model.configure(model.name);
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* [...]
|
||||
* @endcode
|
||||
*/
|
||||
MouseArea {
|
||||
id: listItem
|
||||
|
||||
/*
|
||||
* icon: string
|
||||
* The name of the icon used in the list item.
|
||||
*
|
||||
* Required.
|
||||
*/
|
||||
property alias icon: listItemIcon.source
|
||||
|
||||
/*
|
||||
* iconUsesPlasmaSVG: bool
|
||||
* Whether to load the icon from the Plasma theme instead of the icon theme.
|
||||
*
|
||||
* Optional, defaults to false.
|
||||
*/
|
||||
property bool iconUsesPlasmaSVG: false
|
||||
|
||||
/*
|
||||
* iconEmblem: string
|
||||
* The name of the emblem to badge the icon with.
|
||||
*
|
||||
* Optional, defaults to nothing, in which case there is no emblem.
|
||||
*/
|
||||
property alias iconEmblem: iconEmblem.source
|
||||
|
||||
/*
|
||||
* title: string
|
||||
* The name or title for this list item.
|
||||
*
|
||||
* Optional; if not defined, there will be no title and the subtitle will be
|
||||
* vertically centered in the list item.
|
||||
*/
|
||||
property alias title: listItemTitle.text
|
||||
|
||||
/*
|
||||
* subtitle: string
|
||||
* The subtitle for this list item, displayed under the title.
|
||||
*
|
||||
* Optional; if not defined, there will be no subtitle and the title will be
|
||||
* vertically centered in the list item.
|
||||
*/
|
||||
property alias subtitle: listItemSubtitle.text
|
||||
|
||||
/*
|
||||
* subtitleCanWrap: bool
|
||||
* Whether to allow the subtitle to become a multi-line string instead of
|
||||
* eliding when the text is very long.
|
||||
*
|
||||
* Optional, defaults to false.
|
||||
*/
|
||||
property bool subtitleCanWrap: false
|
||||
|
||||
/*
|
||||
* subtitleColor: color
|
||||
* The color of the subtitle text
|
||||
*
|
||||
* Optional; if not defined, the subtitle will use the default text color
|
||||
*/
|
||||
property alias subtitleColor: listItemSubtitle.color
|
||||
|
||||
/*
|
||||
* allowStyledText: bool
|
||||
* Whether to allow the title, subtitle, and tooltip to contain styled text.
|
||||
* For performance and security reasons, keep this off unless needed.
|
||||
*
|
||||
* Optional, defaults to false.
|
||||
*/
|
||||
property bool allowStyledText: false
|
||||
|
||||
/*
|
||||
* defaultActionButtonAction: Action
|
||||
* The Action to execute when the default button is clicked.
|
||||
*
|
||||
* Optional; if not defined, no default action button will be displayed.
|
||||
*/
|
||||
property alias defaultActionButtonAction: defaultActionButton.action
|
||||
|
||||
/*
|
||||
* defaultActionButtonVisible: bool
|
||||
* When/whether to show to default action button. Useful for making it
|
||||
* conditionally appear or disappear.
|
||||
*
|
||||
* Optional; defaults to true
|
||||
*/
|
||||
property bool defaultActionButtonVisible: true
|
||||
|
||||
/*
|
||||
* contextualActionsModel: list<QtObject>
|
||||
* A list of Action objects that describes additional actions that can be
|
||||
* performed on this list item. The actions should define appropriate
|
||||
* "text:", icon.name:", and "onTriggered:" properties. For example:
|
||||
*
|
||||
* @code
|
||||
* contextualActionsModel: [
|
||||
* Action {
|
||||
* text: i18n("Do something")
|
||||
* icon.name: "document-edit"
|
||||
* onTriggered: doSomething()
|
||||
* }
|
||||
* Action {
|
||||
* text: i18n("Do something else")
|
||||
* icon.name: "draw-polygon"
|
||||
* onTriggered: doSomethingElse()
|
||||
* }
|
||||
* Action {
|
||||
* text: i18n("Do something completely different")
|
||||
* icon.name: "games-highscores"
|
||||
* onTriggered: doSomethingCompletelyDifferent()
|
||||
* }
|
||||
* ]
|
||||
* @endcode
|
||||
*
|
||||
* Optional; if not defined, no contextual actions will be displayed and
|
||||
* you should instead assign a custom view to customExpandedViewContent,
|
||||
* which will be shown when the user expands the list item.
|
||||
*/
|
||||
property list<QtObject> contextualActionsModel
|
||||
|
||||
/*
|
||||
* menu: PlasmaComponents.Menu
|
||||
* The context menu to show when the user right-clicks on this list item.
|
||||
* For example:
|
||||
*
|
||||
* @code
|
||||
* contextMenu: PlasmaComponents.Menu {
|
||||
* PlasmaComponents.MenuItem {
|
||||
* text: i18n("Do something")
|
||||
* icon: "document-edit"
|
||||
* onClicked: doSomething()
|
||||
* }
|
||||
* PlasmaComponents.MenuItem {
|
||||
* text: i18n("Do something else")
|
||||
* icon: "draw-polygon"
|
||||
* onClicked: doSomethingElse()
|
||||
* }
|
||||
* PlasmaComponents.MenuItem {
|
||||
* text: i18n("Do something completely different")
|
||||
* icon: "games-highscores"
|
||||
* onClicked: doSomethingCompletelyDifferent()
|
||||
* }
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* Optional; if not defined, no context menu will be displayed when the user
|
||||
* right-clicks on the list item.
|
||||
*/
|
||||
property var contextMenu
|
||||
|
||||
/*
|
||||
* A custom view to display when the user expands the list item.
|
||||
*
|
||||
* This component must define width: and height: values. Width: should be
|
||||
* equal to the width of the list item itself, while height: will depend
|
||||
* on the component itself.
|
||||
*
|
||||
* Optional; if not defined, the expanded view will show contextual actions
|
||||
* instead.
|
||||
*/
|
||||
property var customExpandedViewContent: actionsListComponent
|
||||
|
||||
/*
|
||||
* isBusy: bool
|
||||
* Whether or not to display a busy indicator on the list item. Set to true
|
||||
* while the item should be non-interactive because things are processing.
|
||||
*
|
||||
* Optional; defaults to false.
|
||||
*/
|
||||
property bool isBusy: false
|
||||
|
||||
/*
|
||||
* isEnabled: bool
|
||||
* Whether or not this list item should be enabled and interactive.
|
||||
*
|
||||
* Optional; defaults to true.
|
||||
*/
|
||||
property bool isEnabled: true
|
||||
|
||||
/*
|
||||
* isDefault: bool
|
||||
* Whether or not this list item should be considered the "default" or
|
||||
* "Current" item in the list. When set to true, and the list itself has
|
||||
* more than one item in it, the list item's title and subtitle will be
|
||||
* drawn in a bold style.
|
||||
*
|
||||
* Optional; defaults to false.
|
||||
*/
|
||||
property bool isDefault: false
|
||||
|
||||
/*
|
||||
* expand()
|
||||
* Show the expanded view, growing the list item to its taller size.
|
||||
*/
|
||||
function expand() {
|
||||
expandedView.visible = true
|
||||
listItem.itemExpanded(listItem)
|
||||
}
|
||||
|
||||
/*
|
||||
* collapse()
|
||||
* Hide the expanded view and collapse the list item to its shorter size.
|
||||
*/
|
||||
function collapse() {
|
||||
expandedView.visible = false
|
||||
listItem.itemExpanded(null)
|
||||
}
|
||||
|
||||
/*
|
||||
* toggleExpanded()
|
||||
* Expand or collapse the list item depending on its current state.
|
||||
*/
|
||||
function toggleExpanded() {
|
||||
expandedView.visible ? listItem.collapse() : listItem.expand()
|
||||
}
|
||||
|
||||
signal itemExpanded(variant item)
|
||||
|
||||
width: parent.width // Assume that we will be used as a delegate, not placed in a layout
|
||||
height: mainLayout.height
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor // To indicate that the whole thing is clickable
|
||||
|
||||
onContainsMouseChanged: listItem.ListView.view.currentIndex = (containsMouse ? index : -1)
|
||||
|
||||
onIsEnabledChanged: if (!listItem.isEnabled) { collapse() }
|
||||
|
||||
onClicked: {
|
||||
// Item is disabled: do nothing
|
||||
if (!listItem.isEnabled) return
|
||||
|
||||
// Left click: toggle expanded state
|
||||
if (mouse.button & Qt.LeftButton) {
|
||||
listItem.toggleExpanded()
|
||||
}
|
||||
|
||||
// Right-click: show context menu, if defined
|
||||
if (contextMenu != undefined) {
|
||||
if (mouse.button & Qt.RightButton) {
|
||||
contextMenu.visualParent = parent
|
||||
contextMenu.prepare();
|
||||
contextMenu.open(mouse.x, mouse.y)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
id: mainRowLayout
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: units.smallSpacing
|
||||
// Otherwise it becomes taller when the button appears
|
||||
Layout.minimumHeight: defaultActionButton.height
|
||||
|
||||
// Icon and optional emblem
|
||||
PlasmaCore.IconItem {
|
||||
id: listItemIcon
|
||||
|
||||
usesPlasmaTheme: listItem.iconUsesPlasmaSVG
|
||||
|
||||
implicitWidth: units.iconSizes.medium
|
||||
implicitHeight: units.iconSizes.medium
|
||||
|
||||
PlasmaCore.IconItem {
|
||||
id: iconEmblem
|
||||
|
||||
visible: source != undefined && source.length > 0
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
implicitWidth: units.iconSizes.small
|
||||
implicitHeight: units.iconSizes.small
|
||||
}
|
||||
}
|
||||
|
||||
// Title and subtitle
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
spacing: 0
|
||||
|
||||
PlasmaExtras.Heading {
|
||||
id: listItemTitle
|
||||
|
||||
visible: text.length > 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
level: 5
|
||||
|
||||
textFormat: listItem.allowStyledText ? Text.StyledText : Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
// Even if it's the default item, only make it bold when
|
||||
// there's more than one item in the list, or else there's
|
||||
// only one item and it's bold, which is a little bit weird
|
||||
font.weight: listItem.isDefault && listItem.ListView.count > 1
|
||||
? Font.Bold
|
||||
: Font.Normal
|
||||
}
|
||||
|
||||
PlasmaComponents3.Label {
|
||||
id: listItemSubtitle
|
||||
|
||||
enabled: false
|
||||
visible: text.length > 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
textFormat: listItem.allowStyledText ? Text.StyledText : Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: subtitleCanWrap ? 9999 : 1
|
||||
wrapMode: subtitleCanWrap ? Text.WordWrap : Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
// Default action button
|
||||
PlasmaComponents3.Button {
|
||||
id: defaultActionButton
|
||||
|
||||
enabled: listItem.isEnabled
|
||||
visible: defaultActionButtonAction
|
||||
&& listItem.defaultActionButtonVisible
|
||||
&& listItem.containsMouse
|
||||
&& !busyIndicator.visible
|
||||
|
||||
icon.width: units.iconSizes.smallMedium
|
||||
icon.height: units.iconSizes.smallMedium
|
||||
}
|
||||
|
||||
PlasmaComponents3.BusyIndicator {
|
||||
id: busyIndicator
|
||||
|
||||
visible: listItem.isBusy
|
||||
|
||||
// Otherwise it makes the list item taller when it appears
|
||||
Layout.maximumHeight: defaultActionButton.implicitHeight
|
||||
Layout.maximumWidth: Layout.maximumHeight
|
||||
}
|
||||
|
||||
// Expand/collapse button
|
||||
PlasmaComponents3.Button {
|
||||
visible: listItem.containsMouse
|
||||
|
||||
// TODO: "collapse-all" and "expand-all" would be more
|
||||
// semantically appropriate, but they have an extra horizontal
|
||||
// line and don't look right here
|
||||
icon.name: expandedView.visible? "go-up" : "go-down"
|
||||
icon.width: units.iconSizes.smallMedium
|
||||
icon.height: units.iconSizes.smallMedium
|
||||
|
||||
onClicked: listItem.toggleExpanded()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Expanded view, by default showing the actions list
|
||||
Loader {
|
||||
id: expandedView
|
||||
|
||||
visible: false
|
||||
opacity: visible ? 1.0 : 0
|
||||
|
||||
active: customExpandedViewContent != undefined
|
||||
sourceComponent: customExpandedViewContent
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: units.smallSpacing
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: units.veryLongDuration
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default expanded view content: contextual actions list
|
||||
Component {
|
||||
id: actionsListComponent
|
||||
|
||||
// Container for actions list, so that we can add left and right margins to it
|
||||
Item {
|
||||
height: actionsList.contentHeight
|
||||
width: mainRowLayout.width
|
||||
|
||||
// TODO: Implement keyboard focus
|
||||
// TODO: Don't highlight the first item by default, unless it has focus
|
||||
// TODO: Animate the highlight moving, as in the printers applet
|
||||
ListView {
|
||||
id: actionsList
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: listItemIcon.width + units.smallSpacing
|
||||
anchors.rightMargin: listItemIcon.width + units.smallSpacing * 2
|
||||
|
||||
height: (units.iconSizes.smallMedium + units.smallSpacing * 2) * actionsList.count
|
||||
|
||||
focus: true
|
||||
clip: true
|
||||
|
||||
model: listItem.contextualActionsModel
|
||||
|
||||
highlight: PlasmaComponents.Highlight {}
|
||||
|
||||
delegate: MouseArea {
|
||||
id: actionItem
|
||||
|
||||
enabled: model.enabled
|
||||
|
||||
width: actionsList.width
|
||||
height: actionItemLayout.height + units.smallSpacing * 2
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
onContainsMouseChanged: actionItem.ListView.view.currentIndex = (containsMouse ? index : -1)
|
||||
|
||||
onClicked: {
|
||||
modelData.trigger()
|
||||
collapse()
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: actionItemLayout
|
||||
|
||||
enabled: model.enabled
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: units.smallSpacing
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: units.smallSpacing
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
PlasmaCore.IconItem {
|
||||
implicitWidth: units.iconSizes.smallMedium
|
||||
implicitHeight: units.iconSizes.smallMedium
|
||||
|
||||
source: model.icon.name
|
||||
}
|
||||
|
||||
PlasmaExtras.Heading {
|
||||
Layout.fillWidth: true
|
||||
|
||||
level: 5
|
||||
|
||||
text: model.text
|
||||
textFormat: listItem.allowStyledText ? Text.StyledText : Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ plugin plasmaextracomponentsplugin
|
||||
|
||||
App 2.0 App.qml
|
||||
ConditionalLoader 2.0 ConditionalLoader.qml
|
||||
ExpandableListItem 2.0 ExpandableListItem.qml
|
||||
Heading 2.0 Heading.qml
|
||||
Paragraph 2.0 Paragraph.qml
|
||||
PageRow 2.0 PageRow.qml
|
||||
|
Loading…
Reference in New Issue
Block a user