diff --git a/applet/contents/ui/DeviceListItem.qml b/applet/contents/ui/DeviceListItem.qml --- a/applet/contents/ui/DeviceListItem.qml +++ b/applet/contents/ui/DeviceListItem.qml @@ -24,20 +24,27 @@ ListItemBase { readonly property var currentPort: Ports[ActivePortIndex] - property bool onlyOne: false draggable: false label: { - if (!currentPort) { - return Description - } else { - if (onlyOne) { - return currentPort.description - } else { - return i18nc("label of device items", "%1 (%2)", currentPort.description, Description) + if (currentPort) { + var model = type === "sink" ? paSinkModel : paSourceModel; + var itemLength = currentPort.description.length; + for (var i = 0; i < model.rowCount(); i++) { + if (i !== index) { + var port = model.data(model.index(i, 0), model.role("Ports")) + [model.data(model.index(i, 0), model.role("ActivePortIndex"))]; + if (port.description) { + var length = Math.min(itemLength, port.description.length) + if (currentPort.description.substring(0, length) === port.description.substring(0, length)) { + return i18nc("label of device items", "%1 (%2)", currentPort.description, Description); + } + } + } } + return currentPort.description; + } else { + return Description; } } - labelOpacity: onlyOne ? 1 : 0.6 - icon: Icon.formFactorIcon(FormFactor) || IconName } diff --git a/applet/contents/ui/ListItemBase.qml b/applet/contents/ui/ListItemBase.qml --- a/applet/contents/ui/ListItemBase.qml +++ b/applet/contents/ui/ListItemBase.qml @@ -36,36 +36,36 @@ PlasmaComponents.ListItem { id: item - property alias label: textLabel.text - property alias labelOpacity: textLabel.opacity + property alias label: defaultButton.text property alias draggable: dragArea.enabled property alias icon: clientIcon.source property alias iconUsesPlasmaTheme: clientIcon.usesPlasmaTheme property string type checked: dropArea.containsDrag opacity: (draggedStream && draggedStream.deviceIndex == Index) ? 0.3 : 1.0 + separatorVisible: false ListView.delayRemove: dragArea.dragActive Item { width: parent.width - height: rowLayout.height + height: column.height RowLayout { id: rowLayout anchors.left: parent.left anchors.right: parent.right anchors.rightMargin: LayoutMirroring.enabled ? 0 : units.smallSpacing anchors.leftMargin: LayoutMirroring.enabled ? units.smallSpacing : 0 - spacing: units.smallSpacing PlasmaCore.IconItem { id: clientIcon Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.preferredHeight: column.height * 0.75 Layout.preferredWidth: Layout.preferredHeight source: "unknown" + visible: type === "sink-input" || type === "source-input" onSourceChanged: { if (!valid && source != "unknown") { @@ -107,72 +107,49 @@ ColumnLayout { id: column - spacing: 1 + spacing: 0 RowLayout { - Layout.fillWidth: true + Layout.minimumHeight: contextMenuButton.height - PlasmaExtras.Heading { - id: textLabel - Layout.fillWidth: true - height: undefined - level: 5 - opacity: 0.6 - wrapMode: Text.NoWrap - elide: Text.ElideRight - visible: !portbox.visible + PlasmaComponents3.RadioButton { + id: defaultButton + Layout.leftMargin: LayoutMirroring.enabled ? 0 : Math.round((muteButton.width - defaultButton.indicator.width) / 2) + Layout.rightMargin: LayoutMirroring.enabled ? Math.round((muteButton.width - defaultButton.indicator.width) / 2) : 0 + spacing: units.smallSpacing + Math.round((muteButton.width - defaultButton.indicator.width) / 2) + checked: PulseObject.default ? PulseObject.default : false + visible: (type == "sink" && sinkView.model.count > 1) || (type == "source" && sourceView.model.count > 1) + onClicked: PulseObject.default = true; } - PlasmaComponents3.ComboBox { - id: portbox - visible: portbox.count > 1 - Layout.minimumWidth: units.gridUnit * 10 - model: { - var items = []; - for (var i = 0; i < PulseObject.ports.length; ++i) { - var port = PulseObject.ports[i]; - if (port.availability != Port.Unavailable) { - items.push(port.description); - } - } - return items - } - currentIndex: ActivePortIndex - onActivated: ActivePortIndex = index + Label { + id: soloLabel + text: defaultButton.text + visible: !defaultButton.visible + elide: Text.ElideRight } Item { - visible: portbox.visible Layout.fillWidth: true } - PlasmaComponents3.ToolButton { - id: defaultButton - text: i18n("Default Device") - icon.name: PulseObject.default ? "starred-symbolic" : "non-starred-symbolic" - checkable: true - checked: PulseObject.default - visible: (type == "sink" && sinkView.model.count > 1) || (type == "source" && sourceView.model.count > 1) - onClicked: PulseObject.default = true; - } - SmallToolButton { id: contextMenuButton icon: "application-menu" checkable: true onClicked: contextMenu.show() - tooltip: i18n("Show additional options for %1", textLabel.text) + tooltip: i18n("Show additional options for %1", defaultButton.text) } } RowLayout { SmallToolButton { + id: muteButton readonly property bool isPlayback: type.substring(0, 4) == "sink" icon: Icon.name(Volume, Muted, isPlayback ? "audio-volume" : "microphone-sensitivity") onClicked: Muted = !Muted checked: Muted - tooltip: i18n("Mute %1", textLabel.text) - + tooltip: i18n("Mute %1", defaultButton.text) } PlasmaComponents.Slider { @@ -195,7 +172,7 @@ enabled: VolumeWritable opacity: Muted ? 0.5 : 1 - Accessible.name: i18nc("Accessibility data on volume slider", "Adjust volume for %1", textLabel.text) + Accessible.name: i18nc("Accessibility data on volume slider", "Adjust volume for %1", defaultButton.text) Component.onCompleted: { ignoreValueChange = false; @@ -320,15 +297,15 @@ contextMenu.addMenuItem(menuItem); // Switch all streams of the relevant kind to this device - if (type == "source") { + if (type == "source" && sourceView.model.count > 1) { menuItem = newMenuItem(); menuItem.text = i18n("Record all audio via this device"); menuItem.icon = "mic-on" // or "mic-ready" // or "audio-input-microphone-symbolic" menuItem.clicked.connect(function() { PulseObject.switchStreams(); }); contextMenu.addMenuItem(menuItem); - } else if (type == "sink") { + } else if (type == "sink" && sinkView.model.count > 1) { menuItem = newMenuItem(); menuItem.text = i18n("Play all audio via this device"); menuItem.icon = "audio-on" // or "audio-ready" // or "audio-speakers-symbolic" @@ -338,8 +315,48 @@ contextMenu.addMenuItem(menuItem); } + // Ports + // Intentionally only shown when there are at least two available ports. + if (PulseObject.ports && PulseObject.ports.length > 1) { + contextMenu.addMenuItem(newSeperator()); + + var menuItem = newMenuItem(); + menuItem.text = i18nc("Heading for a list of ports of a device (for example built-in laptop speakers or a plug for headphones)", "Ports"); + menuItem.section = true; + contextMenu.addMenuItem(menuItem); + menuItem.visible = false; + + var menuItemsPorts = []; + var availablePorts = 0; + for (var i = 0; i < PulseObject.ports.length; i++) { + var port = PulseObject.ports[i]; + if (port.availability != Port.Unavailable) { + menuItemsPorts[availablePorts] = newMenuItem(); + menuItemsPorts[availablePorts].text = port.description; + menuItemsPorts[availablePorts].checkable = true; + menuItemsPorts[availablePorts].checked = i === PulseObject.activePortIndex; + var setActivePort = function(portIndex) { + return function() { + PulseObject.activePortIndex = portIndex; + }; + }; + menuItemsPorts[availablePorts].clicked.connect(setActivePort(i)); + contextMenu.addMenuItem(menuItemsPorts[availablePorts]); + menuItemsPorts[availablePorts].visible = false; + availablePorts++; + } + } + + if (1 < availablePorts){ + menuItem.visible = true; + for (var i = 0; i < availablePorts; i++) { + menuItemsPorts[i].visible = true; + } + } + } + // Choose output / input device - // By choice only shown when there are at least two options + // Intentionally only shown when there are at least two options if ((type == "sink-input" && sinkView.model.count > 1) || (type == "source-input" && sourceView.model.count > 1)) { contextMenu.addMenuItem(newSeperator()); var menuItem = newMenuItem(); diff --git a/applet/contents/ui/StreamListItem.qml b/applet/contents/ui/StreamListItem.qml --- a/applet/contents/ui/StreamListItem.qml +++ b/applet/contents/ui/StreamListItem.qml @@ -25,20 +25,7 @@ import org.kde.plasma.private.volume 0.1 ListItemBase { - property bool onlyOne: false - - label: { - if (! Client) { - return Name - } else { - if (onlyOne) { - return Client.name - } else { - return i18nc("label of stream items", "%1 (%2)", Client.name, Name) - } - } - } - labelOpacity: onlyOne ? 1 : 0.6 + label: Client ? Client.name : Name icon: IconName iconUsesPlasmaTheme: false } diff --git a/applet/contents/ui/main.qml b/applet/contents/ui/main.qml --- a/applet/contents/ui/main.qml +++ b/applet/contents/ui/main.qml @@ -23,6 +23,7 @@ import org.kde.plasma.core 2.1 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.components 3.0 as PlasmaComponents3 import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.plasma.plasmoid 2.0 @@ -63,7 +64,16 @@ return i18n("Volume at %1%", volumePercent(sink.volume)); } } - Plasmoid.toolTipSubText: paSinkModel.preferredSink && !isDummyOutput(paSinkModel.preferredSink) ? paSinkModel.preferredSink.description : "" + Plasmoid.toolTipSubText: { + if (paSinkModel.preferredSink && !isDummyOutput(paSinkModel.preferredSink)) { + var port = paSinkModel.preferredSink.ports[paSinkModel.preferredSink.activePortIndex]; + if (port) { + return port.description + } + return paSinkModel.preferredSink.name + } + return "" + } function isDummyOutput(output) { return output && output.name === dummyOutputName; @@ -298,30 +308,33 @@ id: feedback } + PlasmaCore.Svg { + id: lineSvg + imagePath: "widgets/line" + } + Plasmoid.fullRepresentation: ColumnLayout { spacing: units.smallSpacing Layout.preferredHeight: main.Layout.preferredHeight Layout.preferredWidth: main.Layout.preferredWidth function beginMoveStream(type, stream) { if (type == "sink") { sourceView.visible = false; - sourceViewHeader.visible = false; } else if (type == "source") { sinkView.visible = false; - sinkViewHeader.visible = false; } + devicesLine.visible = false; tabBar.currentTab = devicesTab; } function endMoveStream() { tabBar.currentTab = streamsTab; sourceView.visible = true; - sourceViewHeader.visible = true; + devicesLine.visible = true; sinkView.visible = true; - sinkViewHeader.visible = true; } RowLayout { @@ -343,16 +356,6 @@ text: i18n("Applications") } } - - PlasmaComponents.ToolButton { - Layout.alignment: Qt.AlignBottom - tooltip: plasmoid.action("configure").text - iconName: "configure" - Accessible.name: tooltip - onClicked: { - plasmoid.action("configure").trigger(); - } - } } PlasmaExtras.ScrollArea { @@ -373,17 +376,13 @@ ColumnLayout { id: streamsView + spacing: 0 visible: tabBar.currentTab == streamsTab readonly property bool simpleMode: (sinkInputView.count >= 1 && sourceOutputView.count == 0) || (sinkInputView.count == 0 && sourceOutputView.count >= 1) property int maximumWidth: scrollView.viewport.width width: maximumWidth Layout.maximumWidth: maximumWidth - Header { - Layout.fillWidth: true - visible: sinkInputView.count > 0 && !streamsView.simpleMode - text: i18n("Playback Streams") - } ListView { id: sinkInputView @@ -399,15 +398,20 @@ delegate: StreamListItem { type: "sink-input" draggable: sinkView.count > 1 - onlyOne: streamsView.simpleMode } } - Header { - Layout.fillWidth: true - visible: sourceOutputView.count > 0 && !streamsView.simpleMode - text: i18n("Recording Streams") + PlasmaCore.SvgItem { + elementId: "horizontal-line" + Layout.preferredWidth: scrollView.viewport.width - units.smallSpacing * 4 + Layout.preferredHeight: naturalSize.height + Layout.leftMargin: units.smallSpacing * 2 + Layout.rightMargin: units.smallSpacing * 2 + Layout.topMargin: units.smallSpacing + svg: lineSvg + visible: sinkInputView.model.count > 0 && sourceOutputView.model.count > 0 } + ListView { id: sourceOutputView @@ -423,7 +427,6 @@ delegate: StreamListItem { type: "source-input" draggable: sourceView.count > 1 - onlyOne: streamsView.simpleMode } } } @@ -435,19 +438,15 @@ property int maximumWidth: scrollView.viewport.width width: maximumWidth Layout.maximumWidth: maximumWidth + spacing: 0 - Header { - id: sinkViewHeader - Layout.fillWidth: true - visible: sinkView.count > 0 && !devicesView.simpleMode - text: i18n("Playback Devices") - } ListView { id: sinkView Layout.fillWidth: true Layout.minimumHeight: contentHeight Layout.maximumHeight: contentHeight + spacing: 0 model: PlasmaCore.SortFilterModel { sortRole: "SortByDefault" @@ -467,16 +466,20 @@ boundsBehavior: Flickable.StopAtBounds; delegate: DeviceListItem { type: "sink" - onlyOne: devicesView.simpleMode } } - Header { - id: sourceViewHeader - Layout.fillWidth: true - visible: sourceView.count > 0 && !devicesView.simpleMode - text: i18n("Recording Devices") + PlasmaCore.SvgItem { + id: devicesLine + elementId: "horizontal-line" + Layout.preferredWidth: scrollView.viewport.width - units.smallSpacing * 4 + Layout.leftMargin: units.smallSpacing * 2 + Layout.rightMargin: Layout.leftMargin + Layout.topMargin: units.smallSpacing + svg: lineSvg + visible: sinkView.model.count > 0 && sourceView.model.count > 0 && (sinkView.model.count > 1 || sourceView.model.count > 1) } + ListView { id: sourceView @@ -492,7 +495,6 @@ boundsBehavior: Flickable.StopAtBounds; delegate: DeviceListItem { type: "source" - onlyOne: devicesView.simpleMode } } } @@ -522,6 +524,29 @@ } } } + + PlasmaCore.SvgItem { + elementId: "horizontal-line" + Layout.fillWidth: true + Layout.topMargin: 0 - units.smallSpacing / 2 + Layout.leftMargin: 0 - units.smallSpacing * 1.5 + Layout.rightMargin: Layout.leftMargin + svg: lineSvg + } + + RowLayout { + + Item { + Layout.fillWidth: true + } + + PlasmaComponents.ToolButton { + tooltip: plasmoid.action("configure").text + iconName: "configure" + Accessible.name: tooltip + onClicked: plasmoid.action("configure").trigger() + } + } } Component.onCompleted: {