diff --git a/applets/devicenotifier/package/contents/ui/ActionItem.qml b/applets/devicenotifier/package/contents/ui/ActionItem.qml deleted file mode 100644 --- a/applets/devicenotifier/package/contents/ui/ActionItem.qml +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2011 Viranch Mehta - * Copyright 2012 Jacopo De Simoi - * Copyright 2016 Kai Uwe Broulik - * - * 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 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.0 -import QtQuick.Layouts 1.1 - -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 2.0 as PlasmaComponents - -MouseArea { - id: area - property string icon - property alias label: actionText.text - property string predicate - - height: row.height + 2 * row.y - hoverEnabled: true - - onContainsMouseChanged: { - area.ListView.view.currentIndex = (containsMouse ? index : -1) - } - - onClicked: { - var service = hpSource.serviceForSource(udi); - var operation = service.operationDescription("invokeAction"); - operation.predicate = predicate; - service.startOperationCall(operation); - devicenotifier.expandedDevice = ""; - devicenotifier.currentIndex = -1; - } - - RowLayout { - id: row - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - 2 * units.smallSpacing - y: units.smallSpacing - spacing: units.smallSpacing - - PlasmaCore.IconItem { - source: area.icon - Layout.preferredWidth: units.iconSizes.smallMedium - Layout.preferredHeight: width - } - - PlasmaComponents.Label { - id: actionText - Layout.fillWidth: true - Layout.fillHeight: true - verticalAlignment: Text.AlignVCenter - wrapMode: Text.WordWrap - elide: Text.ElideRight - maximumLineCount: 2 - } - } -} diff --git a/applets/devicenotifier/package/contents/ui/DeviceItem.qml b/applets/devicenotifier/package/contents/ui/DeviceItem.qml --- a/applets/devicenotifier/package/contents/ui/DeviceItem.qml +++ b/applets/devicenotifier/package/contents/ui/DeviceItem.qml @@ -2,6 +2,7 @@ * Copyright 2011 Viranch Mehta * Copyright 2012 Jacopo De Simoi * Copyright 2016 Kai Uwe Broulik + * 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 @@ -20,105 +21,82 @@ */ import QtQuick 2.0 -import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.12 +import QtQml.Models 2.14 import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras + import org.kde.kquickcontrolsaddons 2.0 -MouseArea { +// TODO: fix expanding when a new device is connected; done in devicenotifier.qml +// TODO: clean up list item expanding code in devicenotifier.qml +// TODO: make the free space calculation/display work again for newly-mounted volumes +// TODO: make sure message display works +// TODO: make fix multiple highlight effects when an item is unmounted + +PlasmaExtras.ExpandableListItem { id: deviceItem property string udi - property alias icon: deviceIcon.source - property alias deviceName: deviceLabel.text - property string emblemIcon - property int state - - property bool mounted - property bool isRoot - property bool expanded: devicenotifier.expandedDevice == udi - property alias percentUsage: freeSpaceBar.value - property string freeSpaceText - - signal actionTriggered - - property alias actionIcon: actionButton.iconName - property alias actionToolTip: actionButton.tooltip - property bool actionVisible + readonly property int state: sdSource.data[udi] ? sdSource.data[udi].State : 0 + readonly property int operationResult: (model["Operation result"]) + readonly property bool isMounted: devicenotifier.isMounted(udi) + readonly property bool isRoot: sdSource.data[udi]["File Path"] === "/" readonly property bool hasMessage: statusSource.lastUdi == udi && statusSource.data[statusSource.last] ? true : false readonly property var message: hasMessage ? statusSource.data[statusSource.last] || ({}) : ({}) - height: row.childrenRect.height + 2 * row.y - hoverEnabled: true - - onHasMessageChanged: { - if (hasMessage) { - messageHighlight.highlight(this) - } - } - - onContainsMouseChanged: { - if (containsMouse) { - devicenotifier.currentIndex = index - } - - // this is done to hide the highlight if the mouse moves out of the list view - // and we are not hovering anything - if (deviceItem.ListView.view.highlightItem) { - deviceItem.ListView.view.highlightItem.opacity = (containsMouse ? 1 : 0) + readonly property double freeSpace: sdSource.data[udi] && sdSource.data[udi]["Free Space"] ? sdSource.data[udi]["Free Space"] : -1.0 + readonly property double totalSpace: sdSource.data[udi] && sdSource.data[udi]["Size"] ? sdSource.data[udi]["Size"] : -1.0 + readonly property string freeSpaceText: sdSource.data[udi] && sdSource.data[udi]["Free Space Text"] ? sdSource.data[udi]["Free Space Text"] : "" + readonly property string totalSpaceText: sdSource.data[udi] && sdSource.data[udi]["Size Text"] ? sdSource.data[udi]["Size Text"] : "" + property bool freeSpaceKnown: freeSpace > 0 && totalSpace > 0 + + onOperationResultChanged: { + if (operationResult == 1) { + devicenotifier.popupIcon = "dialog-ok" + popupIconTimer.restart() + } else if (operationResult == 2) { + devicenotifier.popupIcon = "dialog-error" + popupIconTimer.restart() } } - onClicked: { - var data = hpSource.data[udi] - if (!data) { - return - } - - var actions = data.actions - if (actions.length === 1) { - var service = hpSource.serviceForSource(udi) - var operation = service.operationDescription("invokeAction") - operation.predicate = actions[0].predicate - service.startOperationCall(operation) - } else { - devicenotifier.expandedDevice = (expanded ? "" : udi) + onHasMessageChanged: { + if (deviceItem.hasMessage) { + messageHighlight.highlight(this) } } Connections { target: unmountAll onClicked: { - if (model["Removable"] && mounted) { + if (model["Removable"] && isMounted) { actionTriggered(); } } } // this keeps the delegate around for 5 seconds after the device has been // removed in case there was a message, such as "you can now safely remove this" ListView.onRemove: { + deviceItem.isEnabled = false if (devicenotifier.expandedDevice == udi) { devicenotifier.expandedDevice = "" } if (deviceItem.hasMessage) { ListView.delayRemove = true keepDelegateTimer.restart() - statusMessage.opacity = 1 // HACK seems the Column animation breaksf - freeSpaceBar.visible = false - actionButton.visible = false - ++devicenotifier.pendingDelegateRemoval // QTBUG-50380 } } Timer { id: keepDelegateTimer - interval: 3000 // same interval as the auto hide / passive timer + interval: 5000 // same interval as the auto hide / passive timer onTriggered: { deviceItem.ListView.delayRemove = false // otherwise the last message will show again when this device reappears @@ -132,211 +110,126 @@ id: updateStorageSpaceTimer interval: 5000 repeat: true - running: mounted && plasmoid.expanded + running: isMounted && plasmoid.expanded triggeredOnStart: true // Update the storage space as soon as we open the plasmoid onTriggered: { var service = sdSource.serviceForSource(udi); var operation = service.operationDescription("updateFreespace"); service.startOperationCall(operation); } } - RowLayout { - id: row - anchors.horizontalCenter: parent.horizontalCenter - y: units.smallSpacing - width: parent.width - 2 * units.smallSpacing - spacing: units.smallSpacing - - // FIXME: Device item loses focus on mounting/unmounting it, - // or specifically, when some UI element changes. - PlasmaCore.IconItem { - id: deviceIcon - Layout.alignment: Qt.AlignTop - Layout.preferredWidth: units.iconSizes.medium - Layout.preferredHeight: width - enabled: deviceItem.state == 0 - active: iconToolTip.containsMouse - - PlasmaCore.IconItem { - id: deviceEmblem - anchors { - left: parent.left - bottom: parent.bottom - } - width: units.iconSizes.small - height: width - source: { - if (deviceItem.hasMessage) { - if (deviceItem.message.solidError === 0) { - return "emblem-information" - } else { - return "emblem-error" - } - } else if (deviceItem.state == 0) { - return emblemIcon - } else { - return "" - } - } - } + Component { + id: deviceActionComponent + Action { } + } - PlasmaCore.ToolTipArea { - id: iconToolTip - anchors.fill: parent - subText: { - if ((mounted || deviceItem.state != 0) && model["Available Content"] !== "Audio") { - if (model["Removable"]) { - return i18n("It is currently not safe to remove this device: applications may be accessing it. Click the eject button to safely remove this device.") - } else { - return i18n("This device is currently accessible.") - } - } else { - if (model["Removable"]) { - if (model["In Use"]) { - return i18n("It is currently not safe to remove this device: applications may be accessing other volumes on this device. Click the eject button on these other volumes to safely remove this device."); - } else { - return i18n("It is currently safe to remove this device.") - } - } else { - return i18n("This device is not currently accessible.") - } - } - } - } + // We need a JS array full of QQC2 actions; this function creates them + // from the actions list of the data source + function populateActions() { + var deviceActions = []; + + for (let rawAction of hpSource.data[udi].actions) { + const newAction = deviceActionComponent.createObject(deviceItem, { + text: rawAction.text, + 'icon.name': rawAction.icon + }); + newAction.triggered.connect(() => { + var service = hpSource.serviceForSource(udi); + var operation = service.operationDescription('invokeAction'); + operation.predicate = rawAction.predicate; + service.startOperationCall(operation); + devicenotifier.expandedDevice = ''; + devicenotifier.currentIndex = -1; + }); + deviceActions.push(newAction); } + return deviceActions; + } - Column { - Layout.fillWidth: true - - move: Transition { - NumberAnimation { property: "y"; duration: units.longDuration; easing.type: Easing.InOutQuad } - // ensure opacity values return to 1.0 if the add transition animation has been interrupted - NumberAnimation { property: "opacity"; to: 1.0 } - } - - add: Transition { - NumberAnimation { - property: "opacity" - from: 0 - to: 1 - duration: units.longDuration - easing.type: Easing.InOutQuad - } - } - - PlasmaComponents.Label { - id: deviceLabel - width: parent.width - height: undefined // reset PlasmaComponent.Label's strange default height - elide: Text.ElideRight - } - - PlasmaComponents.ProgressBar { - id: freeSpaceBar - width: parent.width - height: units.gridUnit // default is * 1.6 - visible: deviceItem.state == 0 && mounted - minimumValue: 0 - maximumValue: 100 - - PlasmaCore.ToolTipArea { - anchors.fill: parent - subText: freeSpaceText != "" ? i18nc("@info:status Free disk space", "%1 free", freeSpaceText) : "" - } + function actionTriggered() { + var wasMounted = isMounted; + var operationName = wasMounted ? "unmount" : "mount"; + var service = sdSource.serviceForSource(udi); + var operation = service.operationDescription(operationName); + service.startOperationCall(operation); + if (wasMounted) { + deviceItem.collapse(); + } + } - // ProgressBar eats click events, so we'll forward them manually here... - // setting enabled to false will also make the ProgressBar *look* disabled - MouseArea { - anchors.fill: parent - onClicked: deviceItem.clicked(mouse) - } - } - PlasmaComponents.Label { - id: actionMessage - width: parent.width - height: undefined - opacity: 0.6 - font.pointSize: theme.smallestFont.pointSize - visible: deviceItem.state != 0 || (!actionsList.visible && !deviceItem.hasMessage) - text: { - if (deviceItem.state == 0) { - if (!hpSource.data[udi]) { - return "" - } + icon: sdSource.data[udi] == undefined ? "" : sdSource.data[udi].Icon - var actions = hpSource.data[udi].actions - if (actions.length > 1) { - return i18np("1 action for this device", "%1 actions for this device", actions.length); - } else { - return actions[0].text - } - } else if (deviceItem.state == 1) { - return i18nc("Accessing is a less technical word for Mounting; translation should be short and mean \'Currently mounting this device\'", "Accessing...") - } else { - return i18nc("Removing is a less technical word for Unmounting; translation should be short and mean \'Currently unmounting this device\'", "Removing...") - } + iconEmblem: { + if (sdSource.data[udi] != undefined) { + if (deviceItem.hasMessage) { + if (deviceItem.message.solidError === 0) { + return "emblem-information" + } else { + return "emblem-error" } + } else if (deviceItem.state == 0 && Emblems && Emblems[0]) { + return Emblems[0] + } else { + return "" } + } + return "" + } - PlasmaComponents.Label { - id: statusMessage - width: parent.width - height: undefined - font.pointSize: theme.smallestFont.pointSize - text: deviceItem.hasMessage ? (deviceItem.message.error || "") : "" - wrapMode: Text.WordWrap - maximumLineCount: 10 - elide: Text.ElideRight - visible: deviceItem.hasMessage - } + title: sdSource.data[udi] == undefined ? "" : sdSource.data[udi].Description - Item { // spacer - width: 1 - height: units.smallSpacing - visible: actionsList.visible + subtitle: { + if (deviceItem.hasMessage) { + return deviceItem.message.error + } + if (deviceItem.state == 0) { + if (!hpSource.data[udi]) { + return "" } - - ListView { - id: actionsList - width: parent.width - interactive: false - model: hpSource.data[udi] ? hpSource.data[udi].actions : null - height: deviceItem.expanded ? actionsList.contentHeight : 0 - visible: height > 0 - cacheBuffer: 50000 // create all items - delegate: ActionItem { - width: actionsList.width - icon: modelData.icon - label: modelData.text - predicate: modelData.predicate - } - highlight: PlasmaComponents.Highlight {} - highlightMoveDuration: 0 - highlightResizeDuration: 0 + if (freeSpaceKnown) { + return i18nc("@info:status Free disk space", "%1 free of %2", freeSpaceText, totalSpaceText) } + } else if (deviceItem.state == 1) { + return i18nc("Accessing is a less technical word for Mounting; translation should be short and mean \'Currently mounting this device\'", "Accessing...") + } else { + return i18nc("Removing is a less technical word for Unmounting; translation should be short and mean \'Currently unmounting this device\'", "Removing...") } + } - Item { - Layout.preferredWidth: units.iconSizes.medium - Layout.fillHeight: true + subtitleCanWrap: true - PlasmaComponents.ToolButton { - id: actionButton - visible: !busyIndicator.visible && deviceItem.actionVisible - enabled: !isRoot - onClicked: actionTriggered() - y: mounted ? deviceLabel.height + (freeSpaceBar.height - height - units.smallSpacing) / 2 : (deviceLabel.height + actionMessage.height - height) / 2 + // Color the subtitle red for disks with less than 5% free space + subtitleColor: { + if (freeSpaceKnown) { + if (freeSpace / totalSpace <= 0.05) { + return theme.negativeTextColor } + } + return theme.textColor + } - PlasmaComponents.BusyIndicator { - id: busyIndicator - width: parent.width - height: width - running: visible - visible: deviceItem.state != 0 + defaultActionButtonAction: Action { + icon.name: isMounted ? "media-eject" : "media-mount" + text: { + var types = model["Device Types"]; + if (!isMounted) { + return i18n("Mount") + } else if (types && types.indexOf("OpticalDisc") !== -1) { + return i18n("Eject") + } else { + return i18n("Safely remove") } } + onTriggered: actionTriggered() } + // Don't let the user try to unmount root + defaultActionButtonVisible: !isRoot + // don't show for media players since mount/unmount don't really apply + && model["Device Types"].indexOf("Portable Media Player") === -1 + + isBusy: deviceItem.state != 0 + + contextualActionsModel: hpSource.data[udi] ? populateActions() : null } diff --git a/applets/devicenotifier/package/contents/ui/FullRepresentation.qml b/applets/devicenotifier/package/contents/ui/FullRepresentation.qml --- a/applets/devicenotifier/package/contents/ui/FullRepresentation.qml +++ b/applets/devicenotifier/package/contents/ui/FullRepresentation.qml @@ -3,6 +3,7 @@ * Copyright 2012 Jacopo De Simoi * Copyright 2014 David Edmundson * Copyright 2014 Marco Martin + * Copyright 2020 Nate Graham * * * This program is free software; you can redistribute it and/or modify @@ -137,11 +138,10 @@ model: filterModel - delegate: deviceItem + delegate: DeviceItem { + udi: DataEngineSource + } highlight: PlasmaComponents.Highlight { } - highlightMoveDuration: 0 - highlightResizeDuration: 0 - spacing: units.smallSpacing currentIndex: devicenotifier.currentIndex @@ -164,68 +164,4 @@ } } } - - Component { - id: deviceItem - - DeviceItem { - width: notifierDialog.width - udi: DataEngineSource - Binding on icon { - when: sdSource.data[udi] !== undefined - value: sdSource.data[udi].Icon - } - Binding on deviceName { - when: sdSource.data[udi] !== undefined - value: sdSource.data[udi].Description - } - emblemIcon: Emblems && Emblems[0] ? Emblems[0] : "" - state: sdSource.data[udi] ? sdSource.data[udi].State : 0 - isRoot: sdSource.data[udi]["File Path"] === "/" - - percentUsage: { - if (!sdSource.data[udi]) { - return 0 - } - var freeSpace = new Number(sdSource.data[udi]["Free Space"]); - var size = new Number(sdSource.data[udi]["Size"]); - var used = size-freeSpace; - return used*100/size; - } - freeSpaceText: sdSource.data[udi] && sdSource.data[udi]["Free Space Text"] ? sdSource.data[udi]["Free Space Text"] : "" - - actionIcon: mounted ? "media-eject" : "media-mount" - actionVisible: model["Device Types"].indexOf("Portable Media Player") === -1 - actionToolTip: { - var types = model["Device Types"]; - if (!mounted) { - return i18n("Click to access this device from other applications.") - } else if (types && types.indexOf("OpticalDisc") !== -1) { - return i18n("Click to eject this disc.") - } else { - return i18n("Click to safely remove this device.") - } - } - mounted: devicenotifier.isMounted(udi) - - onActionTriggered: { - var operationName = mounted ? "unmount" : "mount"; - var service = sdSource.serviceForSource(udi); - var operation = service.operationDescription(operationName); - service.startOperationCall(operation); - } - property int operationResult: (model["Operation result"]) - - onOperationResultChanged: { - if (operationResult == 1) { - devicenotifier.popupIcon = "dialog-ok" - popupIconTimer.restart() - } else if (operationResult == 2) { - devicenotifier.popupIcon = "dialog-error" - popupIconTimer.restart() - } - } - Behavior on height { NumberAnimation { duration: units.shortDuration } } - } - } } diff --git a/applets/devicenotifier/package/contents/ui/devicenotifier.qml b/applets/devicenotifier/package/contents/ui/devicenotifier.qml --- a/applets/devicenotifier/package/contents/ui/devicenotifier.qml +++ b/applets/devicenotifier/package/contents/ui/devicenotifier.qml @@ -259,7 +259,7 @@ } function expandDevice(udi) { - if (hpSource.data[udi]["actions"].length > 1) { + if (hpSource.data[udi]["actions"].length > 0) { expandedDevice = udi } diff --git a/dataengines/soliddevice/soliddeviceengine.cpp b/dataengines/soliddevice/soliddeviceengine.cpp --- a/dataengines/soliddevice/soliddeviceengine.cpp +++ b/dataengines/soliddevice/soliddeviceengine.cpp @@ -572,9 +572,10 @@ timer->stop(); if (!job->error()) { - setData(udi, I18N_NOOP("Free Space"), QVariant(available)); + setData(udi, I18N_NOOP("Free Space"), QVariant(available).toDouble()); setData(udi, I18N_NOOP("Free Space Text"), KFormat().formatByteSize(available)); - setData(udi, I18N_NOOP("Size"), QVariant(size)); + setData(udi, I18N_NOOP("Size"), QVariant(size).toDouble()); + setData(udi, I18N_NOOP("Size Text"), KFormat().formatByteSize(size)); } m_paths.remove(path);