diff --git a/src/declarativeimports/plasmacomponents3/ExpandableListItem.qml b/src/declarativeimports/plasmacomponents3/ExpandableListItem.qml new file mode 100644 --- /dev/null +++ b/src/declarativeimports/plasmacomponents3/ExpandableListItem.qml @@ -0,0 +1,321 @@ +/* + * Copyright 2020 Nate Graham + * + * 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 + +MouseArea { + id: listItem + + // Objects you feed it to define the behavior and appearance + property alias icon: listItemIcon.source + property alias iconEmblem: iconEmblem.source + property alias title: listItemTitle.text + property alias subtitle: listItemSubtitle.text + property alias defaultActionButtonAction: defaultActionButton.action + property list contextualActionsModel + property var contextMenu + property var customExpandedViewContent: actionsListComponent + + // Optional settings that can be overridden + property bool iconUsesPlasmaSVG: false + property bool isBusy: false + property bool isEnabled: true + property bool isDefault: false + property bool allowStyledText: false + property bool subtitleCanWrap: false + + signal itemExpanded(variant item) + + function expand() { + expandedView.visible = true + listItem.itemExpanded(listItem) + } + + function collapse() { + expandedView.visible = false + listItem.itemExpanded(null) + } + + function toggleExpanded() { + expandedView.visible ? listItem.collapse() : listItem.expand() + } + + 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 + + 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.containsMouse + && !busyIndicator.visible + + icon.width: units.iconSizes.smallMedium + icon.height: units.iconSizes.smallMedium + + // This just adds an additional action when the button is clicked; it + // also runs the default action by virtue of defaultActionButtonAction + // being an alias to defaultActionButton.action + onClicked: collapse() + } + + 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.longDuration * 3 + 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.height + 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 + } + } + } + } + } + } +} diff --git a/src/declarativeimports/plasmacomponents3/qmldir b/src/declarativeimports/plasmacomponents3/qmldir --- a/src/declarativeimports/plasmacomponents3/qmldir +++ b/src/declarativeimports/plasmacomponents3/qmldir @@ -9,6 +9,7 @@ Control 3.0 Control.qml Dial 3.0 Dial.qml Drawer 3.0 Drawer.qml +ExpandableListItem 3.0 ExpandableListItem.qml Frame 3.0 Frame.qml GroupBox 3.0 GroupBox.qml ItemDelegate 3.0 ItemDelegate.qml