Warning, /plasma/plasma-workspace/applets/mediacontroller/package/contents/ui/ExpandedRepresentation.qml is written in an unsupported language. File is not indexed.

0001 /*
0002     SPDX-FileCopyrightText: 2013 Sebastian Kügler <sebas@kde.org>
0003     SPDX-FileCopyrightText: 2014, 2016 Kai Uwe Broulik <kde@privat.broulik.de>
0004     SPDX-FileCopyrightText: 2020 Carson Black <uhhadd@gmail.com>
0005     SPDX-FileCopyrightText: 2020 Ismael Asensio <isma.af@gmail.com>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 import QtQuick
0011 import QtQuick.Layouts
0012 import Qt5Compat.GraphicalEffects
0013 
0014 import org.kde.plasma.plasmoid 2.0
0015 import org.kde.plasma.components 3.0 as PlasmaComponents3
0016 import org.kde.plasma.extras 2.0 as PlasmaExtras
0017 import org.kde.coreaddons 1.0 as KCoreAddons
0018 import org.kde.kirigami 2 as Kirigami
0019 import org.kde.plasma.private.mpris as Mpris
0020 
0021 PlasmaExtras.Representation {
0022     id: expandedRepresentation
0023 
0024     Layout.minimumWidth: switchWidth
0025     Layout.minimumHeight: switchHeight
0026     Layout.preferredWidth: Kirigami.Units.gridUnit * 20
0027     Layout.preferredHeight: Kirigami.Units.gridUnit * 20
0028     Layout.maximumWidth: Kirigami.Units.gridUnit * 40
0029     Layout.maximumHeight: Kirigami.Units.gridUnit * 40
0030 
0031     collapseMarginsHint: true
0032 
0033     readonly property alias playerSelector: playerSelector
0034     readonly property int controlSize: Kirigami.Units.iconSizes.medium
0035 
0036     readonly property bool softwareRendering: GraphicsInfo.api === GraphicsInfo.Software
0037     readonly property var appletInterface: root
0038     property real rate: mpris2Model.currentPlayer?.rate ?? 1
0039     property double length: mpris2Model.currentPlayer?.length ?? 0
0040     property double position: mpris2Model.currentPlayer?.position ?? 0
0041     property bool canSeek: mpris2Model.currentPlayer?.canSeek ?? false
0042 
0043     // only show hours (the default for KFormat) when track is actually longer than an hour
0044     readonly property int durationFormattingOptions: length >= 60*60*1000*1000 ? 0 : KCoreAddons.FormatTypes.FoldHours
0045 
0046     property bool disablePositionUpdate: false
0047     property bool keyPressed: false
0048 
0049     KeyNavigation.tab: playerSelector.count ? playerSelector.currentItem : (seekSlider.visible ? seekSlider : seekSlider.KeyNavigation.down)
0050     KeyNavigation.down: KeyNavigation.tab
0051 
0052     onPositionChanged: {
0053         // we don't want to interrupt the user dragging the slider
0054         if (!seekSlider.pressed && !keyPressed) {
0055             // we also don't want passive position updates
0056             disablePositionUpdate = true
0057             // Slider refuses to set value beyond its end, make sure "to" is up-to-date first
0058             seekSlider.to = length;
0059             seekSlider.value = position
0060             disablePositionUpdate = false
0061         }
0062     }
0063 
0064     onLengthChanged: {
0065         disablePositionUpdate = true
0066         // When reducing maximumValue, value is clamped to it, however
0067         // when increasing it again it gets its old value back.
0068         // To keep us from seeking to the end of the track when moving
0069         // to a new track, we'll reset the value to zero and ask for the position again
0070         seekSlider.value = 0
0071         seekSlider.to = length
0072         mpris2Model.currentPlayer?.updatePosition();
0073         disablePositionUpdate = false
0074     }
0075 
0076     Keys.onPressed: keyPressed = true
0077 
0078     Keys.onReleased: event => {
0079         keyPressed = false
0080 
0081         if ((event.key == Qt.Key_Tab || event.key == Qt.Key_Backtab) && event.modifiers & Qt.ControlModifier) {
0082             event.accepted = true;
0083             if (playerList.count > 2) {
0084                 let nextIndex = mpris2Model.currentIndex + 1;
0085                 if (event.key == Qt.Key_Backtab || event.modifiers & Qt.ShiftModifier) {
0086                     nextIndex -= 2;
0087                 }
0088                 if (nextIndex == playerList.count) {
0089                     nextIndex = 0;
0090                 }
0091                 if (nextIndex < 0) {
0092                     nextIndex = playerList.count - 1;
0093                 }
0094                 mpris2Model.currentIndex = nextIndex;
0095             }
0096         }
0097 
0098         if (!event.modifiers) {
0099             event.accepted = true
0100 
0101             if (event.key === Qt.Key_Space || event.key === Qt.Key_K) {
0102                 // K is YouTube's key for "play/pause" :)
0103                 root.togglePlaying()
0104             } else if (event.key === Qt.Key_P) {
0105                 root.previous()
0106             } else if (event.key === Qt.Key_N) {
0107                 root.next()
0108             } else if (event.key === Qt.Key_S) {
0109                 root.stop()
0110             } else if (event.key === Qt.Key_J) { // TODO ltr languages
0111                 // seek back 5s
0112                 seekSlider.value = Math.max(0, seekSlider.value - 5000000) // microseconds
0113                 seekSlider.moved();
0114             } else if (event.key === Qt.Key_L) {
0115                 // seek forward 5s
0116                 seekSlider.value = Math.min(seekSlider.to, seekSlider.value + 5000000)
0117                 seekSlider.moved();
0118             } else if (event.key === Qt.Key_Home) {
0119                 seekSlider.value = 0
0120                 seekSlider.moved();
0121             } else if (event.key === Qt.Key_End) {
0122                 seekSlider.value = seekSlider.to
0123                 seekSlider.moved();
0124             } else if (event.key >= Qt.Key_0 && event.key <= Qt.Key_9) {
0125                 // jump to percentage, ie. 0 = beginnign, 1 = 10% of total length etc
0126                 seekSlider.value = seekSlider.to * (event.key - Qt.Key_0) / 10
0127                 seekSlider.moved();
0128             } else {
0129                 event.accepted = false
0130             }
0131         }
0132     }
0133 
0134     Timer {
0135         id: queuedPositionUpdate
0136         interval: 100
0137         onTriggered: {
0138             if (expandedRepresentation.position == seekSlider.value) {
0139                 return;
0140             }
0141             mpris2Model.currentPlayer.position = seekSlider.value;
0142         }
0143     }
0144 
0145     // Album Art Background + Details + Touch area to adjust position or volume
0146     MultiPointTouchArea {
0147         id: touchArea
0148         anchors.fill: parent
0149         clip: true
0150 
0151         maximumTouchPoints: 1
0152         minimumTouchPoints: 1
0153         mouseEnabled: false
0154         touchPoints: [
0155             TouchPoint {
0156                 id: point1
0157 
0158                 property bool seeking: false
0159                 property bool adjustingVolume: false
0160 
0161                 onPressedChanged: if (!pressed) {
0162                     seeking = false;
0163                     adjustingVolume = false;
0164                 }
0165                 onSeekingChanged: if (seeking) {
0166                     queuedPositionUpdate.stop();
0167                 } else {
0168                     seekSlider.moved();
0169                 }
0170             }
0171         ]
0172 
0173         Connections {
0174             enabled: seekSlider.visible && point1.pressed && !point1.adjustingVolume
0175             target: point1
0176             // Control seek slider
0177             function onXChanged() {
0178                 if (!point1.seeking && Math.abs(point1.x - point1.startX) < touchArea.width / 20) {
0179                     return;
0180                 }
0181                 point1.seeking = true;
0182                 seekSlider.value = seekSlider.valueAt(Math.max(0, Math.min(1, seekSlider.position + (point1.x - point1.previousX) / touchArea.width))); // microseconds
0183             }
0184         }
0185 
0186         Connections {
0187             enabled: point1.pressed && !point1.seeking
0188             target: point1
0189             function onYChanged() {
0190                 if (!point1.adjustingVolume && Math.abs(point1.y - point1.startY) < touchArea.height / 20) {
0191                     return;
0192                 }
0193                 point1.adjustingVolume = true;
0194                 mpris2Model.currentPlayer.changeVolume((point1.previousY - point1.y) / touchArea.height, false);
0195             }
0196         }
0197 
0198         ShaderEffect {
0199             id: backgroundImage
0200             property real scaleFactor: 1.0
0201             property ShaderEffectSource source: ShaderEffectSource {
0202                 id: shaderEffectSource
0203                 sourceItem: albumArt.albumArt
0204             }
0205 
0206             anchors.centerIn: parent
0207             visible: (albumArt.animating || albumArt.hasImage) && !softwareRendering
0208 
0209             layer.enabled: !softwareRendering
0210             layer.effect: HueSaturation {
0211                 cached: true
0212 
0213                 lightness: -0.5
0214                 saturation: 0.9
0215 
0216                 layer.enabled: true
0217                 layer.effect: FastBlur {
0218                     cached: true
0219 
0220                     radius: 128
0221 
0222                     transparentBorder: false
0223                 }
0224             }
0225             // use State to avoid unnecessary reevaluation of width and height
0226             states: State {
0227                 name: "albumArtReady"
0228                 when: root.expanded && backgroundImage.visible && shaderEffectSource.sourceItem.currentItem?.paintedWidth > 0
0229                 PropertyChanges {
0230                     target: backgroundImage
0231                     scaleFactor: Math.max(parent.width / shaderEffectSource.sourceItem.currentItem.paintedWidth, parent.height / shaderEffectSource.sourceItem.currentItem.paintedHeight)
0232                     width: Math.round(shaderEffectSource.sourceItem.currentItem.paintedWidth * scaleFactor)
0233                     height: Math.round(shaderEffectSource.sourceItem.currentItem.paintedHeight * scaleFactor)
0234                 }
0235                 PropertyChanges {
0236                     target: shaderEffectSource
0237                     // HACK: Fix background ratio when DPI > 1
0238                     sourceRect: Qt.rect(shaderEffectSource.sourceItem.width - shaderEffectSource.sourceItem.currentItem.paintedWidth,
0239                                     Math.round((shaderEffectSource.sourceItem.height - shaderEffectSource.sourceItem.currentItem.paintedHeight) / 2),
0240                                     shaderEffectSource.sourceItem.currentItem.paintedWidth,
0241                                     shaderEffectSource.sourceItem.currentItem.paintedHeight)
0242                 }
0243             }
0244         }
0245         Item { // Album Art + Details
0246             id: albumRow
0247 
0248             anchors {
0249                 fill: parent
0250                 leftMargin: Kirigami.Units.gridUnit
0251                 rightMargin: Kirigami.Units.gridUnit
0252             }
0253 
0254             AlbumArtStackView {
0255                 id: albumArt
0256 
0257                 anchors {
0258                     top: parent.top
0259                     bottom: parent.bottom
0260                     left: parent.left
0261                     right: detailsColumn.visible ? parent.horizontalCenter : parent.right
0262                     rightMargin: Kirigami.Units.gridUnit / 2
0263                 }
0264 
0265                 Connections {
0266                     enabled: root.expanded
0267                     target: root
0268 
0269                     function onAlbumArtChanged() {
0270                         albumArt.loadAlbumArt();
0271                     }
0272                 }
0273 
0274                 Connections {
0275                     target: root
0276 
0277                     function onExpandedChanged() {
0278                         if (!root.expanded) {
0279                             return;
0280                         } else if (albumArt.albumArt.currentItem instanceof Image && albumArt.albumArt.currentItem.source.toString() === Qt.resolvedUrl(root.albumArt).toString()) {
0281                             // QTBUG-119904 StackView ignores transitions when it's invisible
0282                             albumArt.albumArt.currentItem.opacity = 1;
0283                         } else {
0284                             albumArt.loadAlbumArt();
0285                         }
0286                     }
0287                 }
0288             }
0289 
0290             ColumnLayout { // Details Column
0291                 id: detailsColumn
0292                 anchors {
0293                     top: parent.top
0294                     bottom: parent.bottom
0295                     left: parent.horizontalCenter
0296                     leftMargin: Kirigami.Units.gridUnit / 2
0297                     right: parent.right
0298                 }
0299                 visible: root.track.length > 0
0300 
0301                 Item {
0302                     Layout.fillHeight: true
0303                 }
0304 
0305                 Kirigami.Heading { // Song Title
0306                     id: songTitle
0307                     level: 1
0308 
0309                     color: (softwareRendering || !albumArt.hasImage) ? Kirigami.Theme.textColor : "white"
0310 
0311                     textFormat: Text.PlainText
0312                     wrapMode: Text.Wrap
0313                     fontSizeMode: Text.VerticalFit
0314                     elide: Text.ElideRight
0315 
0316                     text: root.track
0317 
0318                     Layout.fillWidth: true
0319                     Layout.maximumHeight: Kirigami.Units.gridUnit * 5
0320                 }
0321                 Kirigami.Heading { // Song Artist
0322                     id: songArtist
0323                     visible: root.artist
0324                     level: 2
0325 
0326                     color: (softwareRendering || !albumArt.hasImage) ? Kirigami.Theme.textColor : "white"
0327 
0328                     textFormat: Text.PlainText
0329                     wrapMode: Text.Wrap
0330                     fontSizeMode: Text.VerticalFit
0331                     elide: Text.ElideRight
0332 
0333                     text: root.artist
0334                     Layout.fillWidth: true
0335                     Layout.maximumHeight: Kirigami.Units.gridUnit * 2
0336                 }
0337                 Kirigami.Heading { // Song Album
0338                     color: (softwareRendering || !albumArt.hasImage) ? Kirigami.Theme.textColor : "white"
0339 
0340                     level: 3
0341                     opacity: 0.6
0342 
0343                     textFormat: Text.PlainText
0344                     wrapMode: Text.Wrap
0345                     fontSizeMode: Text.VerticalFit
0346                     elide: Text.ElideRight
0347 
0348                     visible: text.length > 0
0349                     text: root.album
0350                     Layout.fillWidth: true
0351                     Layout.maximumHeight: Kirigami.Units.gridUnit * 2
0352                 }
0353 
0354                 Item {
0355                     Layout.fillHeight: true
0356                 }
0357             }
0358         }
0359     }
0360 
0361     footer: PlasmaExtras.PlasmoidHeading {
0362         id: footerItem
0363         position: PlasmaComponents3.ToolBar.Footer
0364         ColumnLayout { // Main Column Layout
0365             anchors.fill: parent
0366             RowLayout { // Seek Bar
0367                 spacing: Kirigami.Units.smallSpacing
0368 
0369                 // if there's no "mpris:length" in the metadata, we cannot seek, so hide it in that case
0370                 enabled: playerList.count > 0 && root.track.length > 0 && expandedRepresentation.length > 0 ? true : false
0371                 opacity: enabled ? 1 : 0
0372                 Behavior on opacity {
0373                     NumberAnimation { duration: Kirigami.Units.longDuration }
0374                 }
0375 
0376                 Layout.alignment: Qt.AlignHCenter
0377                 Layout.fillWidth: true
0378                 Layout.maximumWidth: Math.min(Kirigami.Units.gridUnit * 45, Math.round(expandedRepresentation.width * (7 / 10)))
0379 
0380                 // ensure the layout doesn't shift as the numbers change and measure roughly the longest text that could occur with the current song
0381                 TextMetrics {
0382                     id: timeMetrics
0383                     text: i18nc("Remaining time for song e.g -5:42", "-%1",
0384                                 KCoreAddons.Format.formatDuration(seekSlider.to / 1000, expandedRepresentation.durationFormattingOptions))
0385                     font: Kirigami.Theme.smallFont
0386                 }
0387 
0388                 PlasmaComponents3.Label { // Time Elapsed
0389                     Layout.preferredWidth: timeMetrics.width
0390                     verticalAlignment: Text.AlignVCenter
0391                     horizontalAlignment: Text.AlignRight
0392                     text: KCoreAddons.Format.formatDuration(seekSlider.value / 1000, expandedRepresentation.durationFormattingOptions)
0393                     opacity: 0.9
0394                     font: Kirigami.Theme.smallFont
0395                     color: Kirigami.Theme.textColor
0396                     textFormat: Text.PlainText
0397                 }
0398 
0399                 PlasmaComponents3.Slider { // Slider
0400                     id: seekSlider
0401                     Layout.fillWidth: true
0402                     z: 999
0403                     value: 0
0404                     visible: canSeek
0405 
0406                     KeyNavigation.backtab: playerSelector.currentItem
0407                     KeyNavigation.up: KeyNavigation.backtab
0408                     KeyNavigation.down: playPauseButton.enabled ? playPauseButton : (playPauseButton.KeyNavigation.left.enabled ? playPauseButton.KeyNavigation.left : playPauseButton.KeyNavigation.right)
0409                     Keys.onLeftPressed: {
0410                         seekSlider.value = Math.max(0, seekSlider.value - 5000000) // microseconds
0411                         seekSlider.moved();
0412                     }
0413                     Keys.onRightPressed: {
0414                         seekSlider.value = Math.max(0, seekSlider.value + 5000000) // microseconds
0415                         seekSlider.moved();
0416                     }
0417 
0418                     onMoved: {
0419                         if (!disablePositionUpdate) {
0420                             // delay setting the position to avoid race conditions
0421                             queuedPositionUpdate.restart()
0422                         }
0423                     }
0424                     onPressedChanged: {
0425                         // Property binding evaluation is non-deterministic
0426                         // so binding visible to pressed and delay to 0 when pressed
0427                         // will not make the tooltip show up immediately.
0428                         if (pressed) {
0429                             seekToolTip.delay = 0;
0430                             seekToolTip.visible = true;
0431                         } else {
0432                             seekToolTip.delay = Qt.binding(() => Kirigami.Units.toolTipDelay);
0433                             seekToolTip.visible = Qt.binding(() => seekToolTipHandler.hovered);
0434                         }
0435                     }
0436 
0437                     HoverHandler {
0438                         id: seekToolTipHandler
0439                     }
0440 
0441                     PlasmaComponents3.ToolTip {
0442                         id: seekToolTip
0443                         readonly property real position: {
0444                             if (seekSlider.pressed) {
0445                                 return seekSlider.visualPosition;
0446                             }
0447                             // does not need mirroring since we work on raw mouse coordinates
0448                             const mousePos = seekToolTipHandler.point.position.x - seekSlider.handle.width / 2;
0449                             return Math.max(0, Math.min(1, mousePos / (seekSlider.width - seekSlider.handle.width)));
0450                         }
0451                         x: Math.round(seekSlider.handle.width / 2 + position * (seekSlider.width - seekSlider.handle.width) - width / 2)
0452                         // Never hide (not on press, no timeout) as long as the mouse is hovered
0453                         closePolicy: PlasmaComponents3.Popup.NoAutoClose
0454                         timeout: -1
0455                         text: {
0456                             // Label text needs mirrored position again
0457                             const effectivePosition = seekSlider.mirrored ? (1 - position) : position;
0458                             return KCoreAddons.Format.formatDuration((seekSlider.to - seekSlider.from) * effectivePosition / 1000, expandedRepresentation.durationFormattingOptions)
0459                         }
0460                         // NOTE also controlled in onPressedChanged handler above
0461                         visible: seekToolTipHandler.hovered
0462                     }
0463 
0464                     Timer {
0465                         id: seekTimer
0466                         interval: 1000 / expandedRepresentation.rate
0467                         repeat: true
0468                         running: root.isPlaying && root.expanded && !keyPressed && interval > 0 && seekSlider.to >= 1000000
0469                         onTriggered: {
0470                             // some players don't continuously update the seek slider position via mpris
0471                             // add one second; value in microseconds
0472                             if (!seekSlider.pressed) {
0473                                 disablePositionUpdate = true
0474                                 if (seekSlider.value == seekSlider.to) {
0475                                     mpris2Model.currentPlayer.updatePosition();
0476                                 } else {
0477                                     seekSlider.value += 1000000
0478                                 }
0479                                 disablePositionUpdate = false
0480                             }
0481                         }
0482                     }
0483                 }
0484 
0485                 RowLayout {
0486                     visible: !canSeek
0487 
0488                     Layout.fillWidth: true
0489                     Layout.preferredHeight: seekSlider.height
0490 
0491                     PlasmaComponents3.ProgressBar { // Time Remaining
0492                         value: seekSlider.value
0493                         from: seekSlider.from
0494                         to: seekSlider.to
0495 
0496                         Layout.fillWidth: true
0497                         Layout.fillHeight: false
0498                         Layout.alignment: Qt.AlignVCenter
0499                     }
0500                 }
0501 
0502                 PlasmaComponents3.Label {
0503                     Layout.preferredWidth: timeMetrics.width
0504                     verticalAlignment: Text.AlignVCenter
0505                     horizontalAlignment: Text.AlignLeft
0506                     text: i18nc("Remaining time for song e.g -5:42", "-%1",
0507                                 KCoreAddons.Format.formatDuration((seekSlider.to - seekSlider.value) / 1000, expandedRepresentation.durationFormattingOptions))
0508                     opacity: 0.9
0509                     font: Kirigami.Theme.smallFont
0510                     color: Kirigami.Theme.textColor
0511                     textFormat: Text.PlainText
0512                 }
0513             }
0514 
0515             RowLayout { // Player Controls
0516                 id: playerControls
0517 
0518                 property int controlsSize: Kirigami.Units.gridUnit * 3
0519 
0520                 Layout.alignment: Qt.AlignHCenter
0521                 Layout.bottomMargin: Kirigami.Units.smallSpacing
0522                 spacing: Kirigami.Units.smallSpacing
0523 
0524                 PlasmaComponents3.ToolButton {
0525                     id: shuffleButton
0526                     Layout.rightMargin: LayoutMirroring.enabled ? 0 : Kirigami.Units.gridUnit - playerControls.spacing
0527                     Layout.leftMargin: LayoutMirroring.enabled ? Kirigami.Units.gridUnit - playerControls.spacing : 0
0528                     icon.name: "media-playlist-shuffle"
0529                     icon.width: expandedRepresentation.controlSize
0530                     icon.height: expandedRepresentation.controlSize
0531                     checked: root.shuffle === Mpris.ShuffleStatus.On
0532                     enabled: root.canControl && root.shuffle !== Mpris.ShuffleStatus.Unknown
0533 
0534                     display: PlasmaComponents3.AbstractButton.IconOnly
0535                     text: i18nc("@action:button", "Shuffle")
0536 
0537                     KeyNavigation.right: previousButton.enabled ? previousButton : previousButton.KeyNavigation.right
0538                     KeyNavigation.up: playPauseButton.KeyNavigation.up
0539 
0540                     onClicked: {
0541                         mpris2Model.currentPlayer.shuffle =
0542                             root.shuffle === Mpris.ShuffleStatus.On ? Mpris.ShuffleStatus.Off : Mpris.ShuffleStatus.On;
0543                     }
0544 
0545                     PlasmaComponents3.ToolTip {
0546                         text: parent.text
0547                     }
0548                 }
0549 
0550                 PlasmaComponents3.ToolButton { // Previous
0551                     id: previousButton
0552                     icon.width: expandedRepresentation.controlSize
0553                     icon.height: expandedRepresentation.controlSize
0554                     Layout.alignment: Qt.AlignVCenter
0555                     enabled: root.canGoPrevious
0556                     icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward"
0557 
0558                     display: PlasmaComponents3.AbstractButton.IconOnly
0559                     text: i18nc("Play previous track", "Previous Track")
0560 
0561                     KeyNavigation.left: shuffleButton
0562                     KeyNavigation.right: playPauseButton.enabled ? playPauseButton : playPauseButton.KeyNavigation.right
0563                     KeyNavigation.up: playPauseButton.KeyNavigation.up
0564 
0565                     onClicked: {
0566                         seekSlider.value = 0    // Let the media start from beginning. Bug 362473
0567                         root.previous()
0568                     }
0569                 }
0570 
0571                 PlasmaComponents3.ToolButton { // Pause/Play
0572                     id: playPauseButton
0573                     icon.width: expandedRepresentation.controlSize
0574                     icon.height: expandedRepresentation.controlSize
0575 
0576                     Layout.alignment: Qt.AlignVCenter
0577                     enabled: root.isPlaying ? root.canPause : root.canPlay
0578                     icon.name: root.isPlaying ? "media-playback-pause" : "media-playback-start"
0579 
0580                     display: PlasmaComponents3.AbstractButton.IconOnly
0581                     text: root.isPlaying ? i18nc("Pause playback", "Pause") : i18nc("Start playback", "Play")
0582 
0583                     KeyNavigation.left: previousButton.enabled ? previousButton : previousButton.KeyNavigation.left
0584                     KeyNavigation.right: nextButton.enabled ? nextButton : nextButton.KeyNavigation.right
0585                     KeyNavigation.up: seekSlider.visible ? seekSlider : seekSlider.KeyNavigation.up
0586 
0587                     onClicked: root.togglePlaying()
0588                 }
0589 
0590                 PlasmaComponents3.ToolButton { // Next
0591                     id: nextButton
0592                     icon.width: expandedRepresentation.controlSize
0593                     icon.height: expandedRepresentation.controlSize
0594                     Layout.alignment: Qt.AlignVCenter
0595                     enabled: root.canGoNext
0596                     icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward"
0597 
0598                     display: PlasmaComponents3.AbstractButton.IconOnly
0599                     text: i18nc("Play next track", "Next Track")
0600 
0601                     KeyNavigation.left: playPauseButton.enabled ? playPauseButton : playPauseButton.KeyNavigation.left
0602                     KeyNavigation.right: repeatButton
0603                     KeyNavigation.up: playPauseButton.KeyNavigation.up
0604 
0605                     onClicked: {
0606                         seekSlider.value = 0    // Let the media start from beginning. Bug 362473
0607                         root.next()
0608                     }
0609                 }
0610 
0611                 PlasmaComponents3.ToolButton {
0612                     id: repeatButton
0613                     Layout.leftMargin: LayoutMirroring.enabled ? 0 : Kirigami.Units.gridUnit - playerControls.spacing
0614                     Layout.rightMargin: LayoutMirroring.enabled ? Kirigami.Units.gridUnit - playerControls.spacing : 0
0615                     icon.name: root.loopStatus === Mpris.LoopStatus.Track ? "media-playlist-repeat-song" : "media-playlist-repeat"
0616                     icon.width: expandedRepresentation.controlSize
0617                     icon.height: expandedRepresentation.controlSize
0618                     checked: root.loopStatus !== Mpris.LoopStatus.Unknown && root.loopStatus !== Mpris.LoopStatus.None
0619                     enabled: root.canControl && root.loopStatus !== Mpris.LoopStatus.Unknown
0620 
0621                     display: PlasmaComponents3.AbstractButton.IconOnly
0622                     text: root.loopStatus === Mpris.LoopStatus.Track ? i18n("Repeat Track") : i18n("Repeat")
0623 
0624                     KeyNavigation.left: nextButton.enabled ? nextButton : nextButton.KeyNavigation.left
0625                     KeyNavigation.up: playPauseButton.KeyNavigation.up
0626 
0627                     onClicked: {
0628                         let status;
0629                         switch (root.loopStatus) {
0630                         case Mpris.LoopStatus.Playlist:
0631                             status = Mpris.LoopStatus.Track;
0632                             break;
0633                         case Mpris.LoopStatus.Track:
0634                             status = Mpris.LoopStatus.None;
0635                             break;
0636                         default:
0637                             status = Mpris.LoopStatus.Playlist;
0638                         }
0639                         mpris2Model.currentPlayer.loopStatus = status;
0640                     }
0641 
0642                     PlasmaComponents3.ToolTip {
0643                         text: parent.text
0644                     }
0645                 }
0646             }
0647         }
0648     }
0649 
0650     header: PlasmaExtras.PlasmoidHeading {
0651         id: headerItem
0652         position: PlasmaComponents3.ToolBar.Header
0653         visible: playerList.count > 2
0654         //this removes top padding to allow tabbar to touch the edge
0655         topPadding: topInset
0656         bottomPadding: -bottomInset
0657         implicitHeight: Kirigami.Units.gridUnit * 2
0658 
0659         PlasmaComponents3.TabBar {
0660             id: playerSelector
0661             objectName: "playerSelector"
0662 
0663             anchors.fill: parent
0664             implicitHeight: contentHeight
0665             currentIndex: playerSelector.count, mpris2Model.currentIndex
0666             position: PlasmaComponents3.TabBar.Header
0667 
0668             Repeater {
0669                 id: playerList
0670                 model: mpris2Model
0671                 delegate: PlasmaComponents3.TabButton {
0672                     anchors.top: parent.top
0673                     anchors.bottom: parent.bottom
0674                     implicitWidth: 1 // HACK: suppress binding loop warnings
0675                     readonly property QtObject m: model
0676                     display: PlasmaComponents3.AbstractButton.IconOnly
0677                     icon.name: model.iconName
0678                     icon.height: Kirigami.Units.iconSizes.smallMedium
0679                     text: model.identity
0680                     // Keep the delegate centered by offsetting the padding removed in the parent
0681                     bottomPadding: verticalPadding + headerItem.bottomPadding
0682                     topPadding: verticalPadding - headerItem.bottomPadding
0683 
0684                     Accessible.onPressAction: clicked()
0685                     KeyNavigation.down: seekSlider.visible ? seekSlider : seekSlider.KeyNavigation.down
0686 
0687                     onClicked: {
0688                         mpris2Model.currentIndex = index;
0689                     }
0690 
0691                     PlasmaComponents3.ToolTip.text: text
0692                     PlasmaComponents3.ToolTip.delay: Kirigami.Units.toolTipDelay
0693                     PlasmaComponents3.ToolTip.visible: hovered || (activeFocus && (focusReason === Qt.TabFocusReason || focusReason === Qt.BacktabFocusReason))
0694                 }
0695             }
0696         }
0697     }
0698 }