diff --git a/applets/devicenotifier/package/contents/ui/DeviceItem.qml b/applets/devicenotifier/package/contents/ui/DeviceItem.qml index 7dbe7b5fc..c2ee95f57 100644 --- a/applets/devicenotifier/package/contents/ui/DeviceItem.qml +++ b/applets/devicenotifier/package/contents/ui/DeviceItem.qml @@ -1,322 +1,333 @@ /* * 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 import org.kde.kquickcontrolsaddons 2.0 MouseArea { 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 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) } } 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) } } // 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: { 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 onTriggered: { deviceItem.ListView.delayRemove = false // otherwise the last message will show again when this device reappears statusSource.clearMessage() --devicenotifier.pendingDelegateRemoval // QTBUG-50380 } } + Timer { + id: updateStorageSpaceTimer + interval: 5000 + repeat: true + running: mounted && 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 "" } } } 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.") } } } } } 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) : "" } // 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: { - // FIXME: state changes do not reach the plasmoid if the - // device was already attached when the plasmoid was initialized if (deviceItem.state == 0) { if (!hpSource.data[udi]) { return "" } 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...") } } } 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 } Item { // spacer width: 1 height: units.smallSpacing visible: actionsList.visible } 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 } } Item { Layout.preferredWidth: units.iconSizes.medium Layout.fillHeight: 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 } PlasmaComponents.BusyIndicator { id: busyIndicator width: parent.width height: width running: visible visible: deviceItem.state != 0 } } } } diff --git a/applets/devicenotifier/package/contents/ui/devicenotifier.qml b/applets/devicenotifier/package/contents/ui/devicenotifier.qml index 1e89d1b83..16e441928 100644 --- a/applets/devicenotifier/package/contents/ui/devicenotifier.qml +++ b/applets/devicenotifier/package/contents/ui/devicenotifier.qml @@ -1,283 +1,283 @@ /* * Copyright 2011 Viranch Mehta * Copyright 2012 Jacopo De Simoi * Copyright 2014 David Edmundson * 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.plasmoid 2.0 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 Item { id: devicenotifier readonly property string automounterKcmName: "device_automounter_kcm" property string devicesType: { if (plasmoid.configuration.allDevices) { return "all" } else if (plasmoid.configuration.removableDevices) { return "removable" } else { return "nonRemovable" } } property string expandedDevice property string popupIcon: "device-notifier" property bool itemClicked: false property int currentIndex: -1 // QTBUG-50380: As soon as the item gets removed from the model, all of ListView's // properties (count, contentHeight) pretend the delegate doesn't exist anymore // causing our "No devices" heading to overlap with the remaining device property int pendingDelegateRemoval: 0 Plasmoid.switchWidth: units.gridUnit * 10 Plasmoid.switchHeight: units.gridUnit * 10 Plasmoid.toolTipMainText: filterModel.count > 0 && filterModel.get(0) ? i18n("Most Recent Device") : i18n("No Devices Available") Plasmoid.toolTipSubText: { if (filterModel.count > 0) { var data = filterModel.get(0) if (data && data.Description) { return data.Description } } return "" } Plasmoid.icon: { if (filterModel.count > 0) { var data = filterModel.get(0) if (data && data.Icon) { return data.Icon } } return "device-notifier" } Plasmoid.status: (filterModel.count > 0 || pendingDelegateRemoval > 0) ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.PassiveStatus PlasmaCore.DataSource { id: hpSource engine: "hotplug" connectedSources: sources interval: 0 onSourceAdded: { disconnectSource(source); connectSource(source); } onSourceRemoved: { disconnectSource(source); } } Plasmoid.compactRepresentation: PlasmaCore.IconItem { source: devicenotifier.popupIcon width: units.iconSizes.medium; height: units.iconSizes.medium; MouseArea { anchors.fill: parent onClicked: plasmoid.expanded = !plasmoid.expanded } } Plasmoid.fullRepresentation: FullRepresentation {} PlasmaCore.DataSource { id: sdSource engine: "soliddevice" connectedSources: hpSource.sources - interval: plasmoid.expanded ? 5000 : 0 + interval: 0 property string last onSourceAdded: { disconnectSource(source); connectSource(source); last = source; processLastDevice(true); } onSourceRemoved: { if (expandedDevice == source) { expandedDevice = ""; } disconnectSource(source); } onDataChanged: { processLastDevice(true); } onNewData: { last = sourceName; processLastDevice(false); } function isViableDevice(udi) { if (devicesType === "all") { return true; } var device = data[udi]; if (!device) { return false; } return (devicesType === "removable" && device.Removable) || (devicesType === "nonRemovable" && !device.Removable); } function processLastDevice(expand) { if (last) { if (isViableDevice(last)) { if (expand && hpSource.data[last].added) { expandDevice(last); } last = ""; } } } } PlasmaCore.SortFilterModel { id: filterModel sourceModel: PlasmaCore.DataModel { dataSource: sdSource } filterRole: "Removable" filterRegExp: { if (devicesType === "removable") { return "true" } else if (devicesType === "nonRemovable") { return "false" } else { return "" } } sortRole: "Timestamp" sortOrder: Qt.DescendingOrder } PlasmaCore.DataSource { id: statusSource engine: "devicenotifications" property string last property string lastUdi onSourceAdded: { last = source; disconnectSource(source); connectSource(source); } onSourceRemoved: disconnectSource(source) onDataChanged: { if (last) { lastUdi = data[last].udi if (sdSource.isViableDevice(lastUdi)) { plasmoid.expanded = true plasmoid.fullRepresentationItem.spontaneousOpen = true; } } } function clearMessage() { last = "" lastUdi = "" } } Component.onCompleted: { if (sdSource.connectedSources.count == 0) { Plasmoid.status = PlasmaCore.Types.PassiveStatus; } if (KCMShell.authorize(devicenotifier.automounterKcmName + ".desktop").length > 0) { plasmoid.setAction("openAutomounterKcm", i18nc("Open auto mounter kcm", "Configure Removable Devices"), "drive-removable-media") } } function action_openAutomounterKcm() { KCMShell.open([devicenotifier.automounterKcmName]) } Plasmoid.onExpandedChanged: { popupEventSlot(plasmoid.expanded); } function popupEventSlot(popped) { if (!popped) { // reset the property that lets us remember if an item was clicked // (versus only hovered) for autohide purposes devicenotifier.itemClicked = true; expandedDevice = ""; devicenotifier.currentIndex = -1; } } function expandDevice(udi) { if (hpSource.data[udi]["actions"].length > 1) { expandedDevice = udi } // reset the property that lets us remember if an item was clicked // (versus only hovered) for autohide purposes devicenotifier.itemClicked = false; devicenotifier.popupIcon = "preferences-desktop-notification"; //plasmoid.expanded = true; expandTimer.restart(); popupIconTimer.restart() } function isMounted(udi) { if (!sdSource.data[udi]) { return false; } var types = sdSource.data[udi]["Device Types"]; if (types.indexOf("Storage Access") >= 0) { return sdSource.data[udi]["Accessible"]; } return (types.indexOf("Storage Volume") >= 0 && types.indexOf("OpticalDisc") >= 0) } Timer { id: popupIconTimer interval: 3000 onTriggered: devicenotifier.popupIcon = "device-notifier"; } Timer { id: expandTimer interval: 250 onTriggered: { if (plasmoid.configuration.popupOnNewDevice) { // Bug 351592 plasmoid.expanded = true; plasmoid.fullRepresentationItem.spontaneousOpen = true; } } } }