feat: add configurable widget appearance settings

This commit is contained in:
2026-04-15 20:13:10 +00:00
parent f7cc9882ef
commit 0798dd3ea2
4 changed files with 364 additions and 20 deletions
+28
View File
@@ -13,5 +13,33 @@
<label>Font family</label> <label>Font family</label>
<default>Noto Sans</default> <default>Noto Sans</default>
</entry> </entry>
<entry name="labelVisibilityMode" type="String">
<label>Now Playing label visibility mode</label>
<default>auto</default>
</entry>
<entry name="labelPlacement" type="String">
<label>Now Playing label placement</label>
<default>left</default>
</entry>
<entry name="backgroundStyle" type="String">
<label>Background style</label>
<default>custom</default>
</entry>
<entry name="backgroundColor" type="String">
<label>Background color</label>
<default>transparent</default>
</entry>
<entry name="backgroundRadius" type="Int">
<label>Background radius</label>
<default>0</default>
</entry>
<entry name="foregroundColor" type="String">
<label>Foreground color</label>
<default>white</default>
</entry>
<entry name="textShadowEnabled" type="Bool">
<label>Enable text shadow</label>
<default>false</default>
</entry>
</group> </group>
</kcfg> </kcfg>
+130 -26
View File
@@ -7,7 +7,35 @@ import org.kde.plasma.components as PlasmaComponents
MouseArea { MouseArea {
id: mediaControlsMouseArea id: mediaControlsMouseArea
property string configuredFontFamily: "Noto Sans"
property string configuredLabelVisibilityMode: "auto"
property string configuredLabelPlacement: "left"
property string configuredBackgroundStyle: "custom"
property string configuredBackgroundColor: "transparent"
property int configuredBackgroundRadius: 0
property string configuredForegroundColor: "white"
property bool configuredTextShadowEnabled: false
readonly property double buttonSize: 16 readonly property double buttonSize: 16
readonly property string effectiveLabelVisibilityMode: configuredLabelVisibilityMode === "always" || configuredLabelVisibilityMode === "never" ? configuredLabelVisibilityMode : "auto"
readonly property bool labelsOnRight: configuredLabelPlacement === "right"
readonly property bool hideNowPlayingArea: effectiveLabelVisibilityMode === "never"
readonly property bool usesCustomBackground: configuredBackgroundStyle === "custom"
readonly property int hideModeHorizontalPadding: 8
readonly property int labelRailWidth: 100
readonly property int separatorPadding: 4
readonly property string effectiveBackgroundColor: configuredBackgroundColor || "transparent"
readonly property int effectiveBackgroundRadius: Math.max(0, configuredBackgroundRadius)
readonly property string effectiveForegroundColor: configuredForegroundColor || "white"
readonly property bool hasTrackInfo: player.ready && (((player.title || "").trim().length > 0) || ((player.artists || "").trim().length > 0))
readonly property bool nowPlayingLabelsVisible: {
if (effectiveLabelVisibilityMode === "always")
return true;
if (effectiveLabelVisibilityMode === "never")
return false;
return hasTrackInfo;
}
focus: true focus: true
hoverEnabled: true hoverEnabled: true
@@ -25,13 +53,25 @@ MouseArea {
} }
} }
Rectangle {
anchors.fill: parent
visible: mediaControlsMouseArea.usesCustomBackground
color: mediaControlsMouseArea.effectiveBackgroundColor
radius: mediaControlsMouseArea.effectiveBackgroundRadius
}
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
layoutDirection: mediaControlsMouseArea.labelsOnRight ? Qt.RightToLeft : Qt.LeftToRight
ColumnLayout { ColumnLayout {
id: leftColumn id: leftColumn
visible: !mediaControlsMouseArea.hideNowPlayingArea
Layout.fillHeight: true Layout.fillHeight: true
Layout.minimumWidth: visible ? mediaControlsMouseArea.labelRailWidth : 0
Layout.preferredWidth: visible ? mediaControlsMouseArea.labelRailWidth : 0
Layout.maximumWidth: visible ? mediaControlsMouseArea.labelRailWidth : 0
states: [ states: [
State { State {
name: "buttonsVisible" name: "buttonsVisible"
@@ -63,29 +103,34 @@ MouseArea {
ColumnLayout { ColumnLayout {
id: nowPlayingLabels id: nowPlayingLabels
Layout.alignment: Qt.AlignRight Layout.alignment: mediaControlsMouseArea.labelsOnRight ? Qt.AlignLeft : Qt.AlignRight
spacing: 0 spacing: 0
visible: mediaControlsMouseArea.nowPlayingLabelsVisible
PlasmaComponents.Label { ShadowedLabel {
Layout.alignment: Qt.AlignRight Layout.alignment: mediaControlsMouseArea.labelsOnRight ? Qt.AlignLeft : Qt.AlignRight
shadowEnabled: mediaControlsMouseArea.configuredTextShadowEnabled
text: "NOW" text: "NOW"
lineHeight: 0.8 lineHeight: 0.8
font.pixelSize: 16 font.pixelSize: 16
font.bold: true font.bold: true
// qmllint disable missing-property font.family: mediaControlsMouseArea.configuredFontFamily
font.family: plasmoid.configuration.fontFamily color: mediaControlsMouseArea.effectiveForegroundColor
horizontalAlignment: mediaControlsMouseArea.labelsOnRight ? Text.AlignLeft : Text.AlignRight
} }
PlasmaComponents.Label { ShadowedLabel {
id: nowPlayingLabel2 id: nowPlayingLabel2
Layout.alignment: Qt.AlignRight Layout.alignment: mediaControlsMouseArea.labelsOnRight ? Qt.AlignLeft : Qt.AlignRight
shadowEnabled: mediaControlsMouseArea.configuredTextShadowEnabled
text: "PLAYING" text: "PLAYING"
lineHeight: 0.8 lineHeight: 0.8
font.bold: true font.bold: true
font.pixelSize: 16 font.pixelSize: 16
// qmllint disable missing-property font.family: mediaControlsMouseArea.configuredFontFamily
font.family: plasmoid.configuration.fontFamily color: mediaControlsMouseArea.effectiveForegroundColor
horizontalAlignment: mediaControlsMouseArea.labelsOnRight ? Text.AlignLeft : Text.AlignRight
} }
} }
@@ -93,9 +138,9 @@ MouseArea {
RowLayout { RowLayout {
id: mediaControls id: mediaControls
Layout.alignment: Qt.AlignHCenter Layout.alignment: mediaControlsMouseArea.labelsOnRight ? Qt.AlignLeft : Qt.AlignRight
opacity: mediaControlsMouseArea.containsMouse ? 1 : 0 opacity: mediaControlsMouseArea.containsMouse && mediaControlsMouseArea.nowPlayingLabelsVisible && !mediaControlsMouseArea.hideNowPlayingArea ? 1 : 0
visible: opacity > 0 visible: opacity > 0 && mediaControlsMouseArea.nowPlayingLabelsVisible && !mediaControlsMouseArea.hideNowPlayingArea
QQC2.Button { QQC2.Button {
Layout.preferredWidth: buttonSize Layout.preferredWidth: buttonSize
@@ -109,6 +154,7 @@ MouseArea {
contentItem: Kirigami.Icon { contentItem: Kirigami.Icon {
source: "media-skip-backward" source: "media-skip-backward"
color: mediaControlsMouseArea.effectiveForegroundColor
} }
} }
@@ -127,6 +173,7 @@ MouseArea {
contentItem: Kirigami.Icon { contentItem: Kirigami.Icon {
source: player.playbackStatus === 2 ? "media-playback-pause" : "media-playback-start" source: player.playbackStatus === 2 ? "media-playback-pause" : "media-playback-start"
color: mediaControlsMouseArea.effectiveForegroundColor
} }
} }
@@ -140,6 +187,7 @@ MouseArea {
contentItem: Kirigami.Icon { contentItem: Kirigami.Icon {
source: "media-skip-forward" source: "media-skip-forward"
color: mediaControlsMouseArea.effectiveForegroundColor
} }
} }
@@ -173,37 +221,93 @@ MouseArea {
Rectangle { Rectangle {
id: separator id: separator
visible: mediaControlsMouseArea.nowPlayingLabelsVisible && !mediaControlsMouseArea.hideNowPlayingArea
Layout.fillHeight: true Layout.fillHeight: true
width: 1 Layout.leftMargin: visible ? mediaControlsMouseArea.separatorPadding : 0
Layout.rightMargin: visible ? mediaControlsMouseArea.separatorPadding : 0
Layout.topMargin: visible ? mediaControlsMouseArea.separatorPadding : 0
Layout.bottomMargin: visible ? mediaControlsMouseArea.separatorPadding : 0
Layout.minimumWidth: visible ? 1 : 0
Layout.preferredWidth: visible ? 1 : 0
Layout.maximumWidth: visible ? 1 : 0
color: mediaControlsMouseArea.effectiveForegroundColor
} }
ColumnLayout { ColumnLayout {
id: infoColumn id: infoColumn
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: 0
Layout.leftMargin: mediaControlsMouseArea.hideNowPlayingArea ? mediaControlsMouseArea.hideModeHorizontalPadding : 0
Layout.rightMargin: mediaControlsMouseArea.hideNowPlayingArea ? mediaControlsMouseArea.hideModeHorizontalPadding : 0
PlasmaComponents.Label { ShadowedLabel {
Layout.fillWidth: true Layout.fillWidth: true
shadowEnabled: mediaControlsMouseArea.configuredTextShadowEnabled
text: player.title text: player.title
font.pixelSize: 28 font.pixelSize: 28
font.bold: true font.bold: true
// qmllint disable missing-property font.family: mediaControlsMouseArea.configuredFontFamily
font.family: plasmoid.configuration.fontFamily color: mediaControlsMouseArea.effectiveForegroundColor
lineHeight: 0.8 lineHeight: 0.8
elide: Text.ElideRight elide: Text.ElideRight
horizontalAlignment: mediaControlsMouseArea.labelsOnRight ? Text.AlignRight : Text.AlignLeft
}
ShadowedLabel {
Layout.maximumWidth: 300
Layout.fillWidth: true
Layout.alignment: mediaControlsMouseArea.labelsOnRight ? Qt.AlignRight : Qt.AlignLeft
shadowEnabled: mediaControlsMouseArea.configuredTextShadowEnabled
text: player.artists
font.pixelSize: 26
font.family: mediaControlsMouseArea.configuredFontFamily
color: mediaControlsMouseArea.effectiveForegroundColor
lineHeight: 0.8
elide: Text.ElideRight
horizontalAlignment: mediaControlsMouseArea.labelsOnRight ? Text.AlignRight : Text.AlignLeft
}
}
}
component ShadowedLabel: Item {
id: shadowedLabel
property alias text: foregroundLabel.text
property alias font: foregroundLabel.font
property alias lineHeight: foregroundLabel.lineHeight
property alias elide: foregroundLabel.elide
property alias horizontalAlignment: foregroundLabel.horizontalAlignment
property alias verticalAlignment: foregroundLabel.verticalAlignment
property alias color: foregroundLabel.color
property bool shadowEnabled: false
implicitWidth: foregroundLabel.implicitWidth + (shadowEnabled ? 1 : 0)
implicitHeight: foregroundLabel.implicitHeight + (shadowEnabled ? 1 : 0)
PlasmaComponents.Label {
id: shadowLabel
visible: shadowedLabel.shadowEnabled
x: 1
y: 1
width: foregroundLabel.width
height: foregroundLabel.height
text: foregroundLabel.text
font: foregroundLabel.font
lineHeight: foregroundLabel.lineHeight
elide: foregroundLabel.elide
horizontalAlignment: foregroundLabel.horizontalAlignment
verticalAlignment: foregroundLabel.verticalAlignment
color: "#99000000"
} }
PlasmaComponents.Label { PlasmaComponents.Label {
Layout.maximumWidth: 300 id: foregroundLabel
Layout.fillWidth: true
text: player.artists anchors.fill: parent
font.pixelSize: 26
// qmllint disable missing-property
font.family: plasmoid.configuration.fontFamily
lineHeight: 0.8
elide: Text.ElideRight
}
} }
} }
+168
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Dialogs as Dialogs
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kcmutils as KCM import org.kde.kcmutils as KCM
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@@ -9,13 +10,77 @@ KCM.SimpleKCM {
property alias cfg_opacity: opacitySpinBox.value property alias cfg_opacity: opacitySpinBox.value
property string cfg_fontFamily property string cfg_fontFamily
property string cfg_labelVisibilityMode
property string cfg_labelPlacement
property string cfg_backgroundStyle
property alias cfg_backgroundColor: backgroundColorField.text
property alias cfg_backgroundRadius: backgroundRadiusSpinBox.value
property alias cfg_foregroundColor: foregroundColorField.text
property alias cfg_textShadowEnabled: textShadowCheckBox.checked
readonly property var availableFonts: Qt.fontFamilies() readonly property var availableFonts: Qt.fontFamilies()
readonly property var labelVisibilityOptions: [{
"text": i18n("Auto hide when idle"),
"value": "auto"
}, {
"text": i18n("Always show"),
"value": "always"
}, {
"text": i18n("Always hide"),
"value": "never"
}]
readonly property var labelPlacementOptions: [{
"text": i18n("Left"),
"value": "left"
}, {
"text": i18n("Right"),
"value": "right"
}]
readonly property var backgroundStyleOptions: [{
"text": i18n("Default"),
"value": "custom"
}, {
"text": i18n("Solid background"),
"value": "default"
}]
function syncComboByValue(comboBox, options, configuredValue, fallbackValue) {
const requestedValue = configuredValue || fallbackValue;
const requestedIndex = options.findIndex((option) => {
return option.value === requestedValue;
});
const fallbackIndex = options.findIndex((option) => {
return option.value === fallbackValue;
});
const resolvedIndex = requestedIndex >= 0 ? requestedIndex : (fallbackIndex >= 0 ? fallbackIndex : 0);
if (comboBox.currentIndex !== resolvedIndex)
comboBox.currentIndex = resolvedIndex;
return options[resolvedIndex].value;
}
onCfg_fontFamilyChanged: { onCfg_fontFamilyChanged: {
const index = availableFonts.indexOf(cfg_fontFamily); const index = availableFonts.indexOf(cfg_fontFamily);
if (index >= 0 && fontFamilyComboBox.currentIndex !== index) if (index >= 0 && fontFamilyComboBox.currentIndex !== index)
fontFamilyComboBox.currentIndex = index; fontFamilyComboBox.currentIndex = index;
}
onCfg_labelVisibilityModeChanged: {
const resolvedValue = syncComboByValue(labelVisibilityModeComboBox, labelVisibilityOptions, cfg_labelVisibilityMode, "auto");
if (cfg_labelVisibilityMode !== resolvedValue)
cfg_labelVisibilityMode = resolvedValue;
}
onCfg_labelPlacementChanged: {
const resolvedValue = syncComboByValue(labelPlacementComboBox, labelPlacementOptions, cfg_labelPlacement, "left");
if (cfg_labelPlacement !== resolvedValue)
cfg_labelPlacement = resolvedValue;
}
onCfg_backgroundStyleChanged: {
const resolvedValue = syncComboByValue(backgroundStyleComboBox, backgroundStyleOptions, cfg_backgroundStyle, "custom");
if (cfg_backgroundStyle !== resolvedValue)
cfg_backgroundStyle = resolvedValue;
} }
Kirigami.FormLayout { Kirigami.FormLayout {
@@ -35,6 +100,109 @@ KCM.SimpleKCM {
onActivated: configRoot.cfg_fontFamily = currentText onActivated: configRoot.cfg_fontFamily = currentText
} }
QQC2.ComboBox {
id: labelVisibilityModeComboBox
Kirigami.FormData.label: i18n("Now Playing labels:")
model: configRoot.labelVisibilityOptions
textRole: "text"
valueRole: "value"
onActivated: configRoot.cfg_labelVisibilityMode = currentValue
}
QQC2.ComboBox {
id: labelPlacementComboBox
Kirigami.FormData.label: i18n("Label placement:")
model: configRoot.labelPlacementOptions
textRole: "text"
valueRole: "value"
onActivated: configRoot.cfg_labelPlacement = currentValue
}
QQC2.ComboBox {
id: backgroundStyleComboBox
Kirigami.FormData.label: i18n("Background style:")
model: configRoot.backgroundStyleOptions
textRole: "text"
valueRole: "value"
onActivated: configRoot.cfg_backgroundStyle = currentValue
}
RowLayout {
Kirigami.FormData.label: i18n("Background color:")
enabled: configRoot.cfg_backgroundStyle === "custom"
QQC2.TextField {
id: backgroundColorField
Layout.fillWidth: true
placeholderText: i18n("transparent or #AARRGGBB")
}
QQC2.Button {
text: i18n("Pick...")
onClicked: {
backgroundColorDialog.selectedColor = backgroundColorField.text || "transparent";
backgroundColorDialog.open();
}
}
}
RowLayout {
Kirigami.FormData.label: i18n("Foreground color:")
QQC2.TextField {
id: foregroundColorField
Layout.fillWidth: true
placeholderText: i18n("white or #RRGGBB")
}
QQC2.Button {
text: i18n("Pick...")
onClicked: {
foregroundColorDialog.selectedColor = foregroundColorField.text || "white";
foregroundColorDialog.open();
}
}
}
QQC2.SpinBox {
id: backgroundRadiusSpinBox
Kirigami.FormData.label: i18n("Background radius:")
enabled: configRoot.cfg_backgroundStyle === "custom"
from: 0
to: 100
}
QQC2.CheckBox {
id: textShadowCheckBox
Kirigami.FormData.label: i18n("Text shadow:")
text: i18n("Enable")
}
}
Dialogs.ColorDialog {
id: backgroundColorDialog
title: i18n("Select background color")
options: Dialogs.ColorDialog.ShowAlphaChannel
onAccepted: configRoot.cfg_backgroundColor = selectedColor.toString()
}
Dialogs.ColorDialog {
id: foregroundColorDialog
title: i18n("Select foreground color")
options: Dialogs.ColorDialog.ShowAlphaChannel
onAccepted: configRoot.cfg_foregroundColor = selectedColor.toString()
} }
} }
+45 -1
View File
@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.plasmoid import org.kde.plasma.plasmoid
PlasmoidItem { PlasmoidItem {
@@ -10,12 +11,37 @@ PlasmoidItem {
// "qmllint" sees `plasmoid` as QObject here, but Plasma injects `configuration` at runtime. // "qmllint" sees `plasmoid` as QObject here, but Plasma injects `configuration` at runtime.
// qmllint disable missing-property // qmllint disable missing-property
property int configuredOpacity: plasmoid.configuration.opacity property int configuredOpacity: plasmoid.configuration.opacity
// qmllint disable missing-property
property string configuredFontFamily: plasmoid.configuration.fontFamily
// qmllint disable missing-property
property string configuredLabelVisibilityMode: plasmoid.configuration.labelVisibilityMode
// qmllint disable missing-property
property string configuredLabelPlacement: plasmoid.configuration.labelPlacement
// qmllint disable missing-property
property string configuredBackgroundStyle: plasmoid.configuration.backgroundStyle
// qmllint disable missing-property
property string configuredBackgroundColor: plasmoid.configuration.backgroundColor
// qmllint disable missing-property
property int configuredBackgroundRadius: plasmoid.configuration.backgroundRadius
// qmllint disable missing-property
property string configuredForegroundColor: plasmoid.configuration.foregroundColor
// qmllint disable missing-property
property bool configuredTextShadowEnabled: plasmoid.configuration.textShadowEnabled
readonly property int resolvedBackgroundHints: {
if (configuredBackgroundStyle === "default")
return PlasmaCore.Types.DefaultBackground | PlasmaCore.Types.ConfigurableBackground;
if (configuredBackgroundStyle === "none")
return PlasmaCore.Types.NoBackground;
return PlasmaCore.Types.NoBackground;
}
width: Kirigami.Units.gridUnit * 25 width: Kirigami.Units.gridUnit * 25
height: Kirigami.Units.gridUnit * 5 height: Kirigami.Units.gridUnit * 5
Layout.minimumWidth: Kirigami.Units.gridUnit * 25 Layout.minimumWidth: Kirigami.Units.gridUnit * 25
Layout.minimumHeight: Kirigami.Units.gridUnit * 5 Layout.minimumHeight: Kirigami.Units.gridUnit * 5
Plasmoid.backgroundHints: "NoBackground" Plasmoid.backgroundHints: resolvedBackgroundHints
opacity: configuredOpacity / 100 opacity: configuredOpacity / 100
Player { Player {
@@ -27,11 +53,29 @@ PlasmoidItem {
Layout.minimumHeight: Kirigami.Units.gridUnit * 5 Layout.minimumHeight: Kirigami.Units.gridUnit * 5
Layout.preferredWidth: root.width Layout.preferredWidth: root.width
Layout.preferredHeight: root.height Layout.preferredHeight: root.height
configuredFontFamily: root.configuredFontFamily
configuredLabelVisibilityMode: root.configuredLabelVisibilityMode
configuredLabelPlacement: root.configuredLabelPlacement
configuredBackgroundStyle: root.configuredBackgroundStyle
configuredBackgroundColor: root.configuredBackgroundColor
configuredBackgroundRadius: root.configuredBackgroundRadius
configuredForegroundColor: root.configuredForegroundColor
configuredTextShadowEnabled: root.configuredTextShadowEnabled
} }
compactRepresentation: Representation { compactRepresentation: Representation {
Layout.minimumWidth: Kirigami.Units.gridUnit * 25 Layout.minimumWidth: Kirigami.Units.gridUnit * 25
Layout.minimumHeight: Kirigami.Units.gridUnit * 5 Layout.minimumHeight: Kirigami.Units.gridUnit * 5
Layout.preferredWidth: root.width
Layout.preferredHeight: root.height
configuredFontFamily: root.configuredFontFamily
configuredLabelVisibilityMode: root.configuredLabelVisibilityMode
configuredLabelPlacement: root.configuredLabelPlacement
configuredBackgroundStyle: root.configuredBackgroundStyle
configuredBackgroundColor: root.configuredBackgroundColor
configuredBackgroundRadius: root.configuredBackgroundRadius
configuredForegroundColor: root.configuredForegroundColor
configuredTextShadowEnabled: root.configuredTextShadowEnabled
} }
} }