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 }