fedora
This commit is contained in:
@@ -0,0 +1,791 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Services.Media
|
||||
import qs.Widgets
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: Math.round(440 * Style.uiScaleRatio)
|
||||
preferredHeight: Math.round(420 * Style.uiScaleRatio)
|
||||
|
||||
panelContent: Item {
|
||||
id: panelContent
|
||||
|
||||
// Volume state (lazy-loaded with panelContent)
|
||||
property real localOutputVolume: AudioService.volume || 0
|
||||
property bool localOutputVolumeChanging: false
|
||||
property int lastSinkId: -1
|
||||
|
||||
property real localInputVolume: AudioService.inputVolume || 0
|
||||
property bool localInputVolumeChanging: false
|
||||
property int lastSourceId: -1
|
||||
|
||||
readonly property bool outputVolumeGuard: outputVolumeSlider.sliderActive || localOutputVolumeChanging
|
||||
readonly property bool inputVolumeGuard: inputVolumeSlider.sliderActive || localInputVolumeChanging
|
||||
|
||||
// UI state (lazy-loaded with panelContent)
|
||||
property int currentTabIndex: 0
|
||||
|
||||
Component.onCompleted: {
|
||||
var vol = AudioService.volume;
|
||||
localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0;
|
||||
var inputVol = AudioService.inputVolume;
|
||||
localInputVolume = (inputVol !== undefined && !isNaN(inputVol)) ? inputVol : 0;
|
||||
if (AudioService.sink) {
|
||||
lastSinkId = AudioService.sink.id;
|
||||
}
|
||||
if (AudioService.source) {
|
||||
lastSourceId = AudioService.source.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset local volume when device changes - use current device's volume
|
||||
Connections {
|
||||
target: AudioService
|
||||
function onSinkChanged() {
|
||||
if (AudioService.sink) {
|
||||
const newSinkId = AudioService.sink.id;
|
||||
if (newSinkId !== panelContent.lastSinkId) {
|
||||
panelContent.lastSinkId = newSinkId;
|
||||
// Immediately set local volume to current device's volume
|
||||
var vol = AudioService.volume;
|
||||
panelContent.localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0;
|
||||
}
|
||||
} else {
|
||||
panelContent.lastSinkId = -1;
|
||||
panelContent.localOutputVolume = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AudioService
|
||||
function onSourceChanged() {
|
||||
if (AudioService.source) {
|
||||
const newSourceId = AudioService.source.id;
|
||||
if (newSourceId !== panelContent.lastSourceId) {
|
||||
panelContent.lastSourceId = newSourceId;
|
||||
// Immediately set local volume to current device's volume
|
||||
var vol = AudioService.inputVolume;
|
||||
panelContent.localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0;
|
||||
}
|
||||
} else {
|
||||
panelContent.lastSourceId = -1;
|
||||
panelContent.localInputVolume = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connections to update local volumes when AudioService changes
|
||||
Connections {
|
||||
target: AudioService
|
||||
function onVolumeChanged() {
|
||||
if (!panelContent.outputVolumeGuard && !AudioService.isSettingOutputVolume && AudioService.sink && AudioService.sink.id === panelContent.lastSinkId) {
|
||||
var vol = AudioService.volume;
|
||||
panelContent.localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AudioService
|
||||
function onInputVolumeChanged() {
|
||||
if (!panelContent.inputVolumeGuard && !AudioService.isSettingInputVolume && AudioService.source && AudioService.source.id === panelContent.lastSourceId) {
|
||||
var vol = AudioService.inputVolume;
|
||||
panelContent.localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: outputVolumeSlider
|
||||
function onSliderActiveChanged() {
|
||||
if (!outputVolumeSlider.sliderActive && AudioService.sink && AudioService.sink.id === panelContent.lastSinkId) {
|
||||
var vol = AudioService.volume;
|
||||
panelContent.localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: inputVolumeSlider
|
||||
function onSliderActiveChanged() {
|
||||
if (!inputVolumeSlider.sliderActive && AudioService.source && AudioService.source.id === panelContent.lastSourceId) {
|
||||
var vol = AudioService.inputVolume;
|
||||
panelContent.localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to debounce volume changes
|
||||
// Only sync if the device hasn't changed (check by comparing IDs)
|
||||
Timer {
|
||||
interval: 100
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
// Only sync if sink hasn't changed
|
||||
if (AudioService.sink && AudioService.sink.id === panelContent.lastSinkId) {
|
||||
if (Math.abs(panelContent.localOutputVolume - AudioService.volume) >= 0.01) {
|
||||
AudioService.setVolume(panelContent.localOutputVolume);
|
||||
}
|
||||
}
|
||||
// Only sync if source hasn't changed
|
||||
if (AudioService.source && AudioService.source.id === panelContent.lastSourceId) {
|
||||
if (Math.abs(panelContent.localInputVolume - AudioService.inputVolume) >= 0.01) {
|
||||
AudioService.setInputVolume(panelContent.localInputVolume);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find application streams that are actually playing audio (connected to default sink)
|
||||
// Use linkGroups to find nodes connected to the default audio sink
|
||||
// Note: We need to use link IDs since source/target properties require binding
|
||||
readonly property var appStreams: AudioService.appStreams
|
||||
|
||||
// Use implicitHeight from content + margins to avoid binding loops
|
||||
property real contentPreferredHeight: mainColumn.implicitHeight + Style.margin2L
|
||||
|
||||
ColumnLayout {
|
||||
id: mainColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
// HEADER
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: header.implicitHeight + Style.margin2M
|
||||
|
||||
ColumnLayout {
|
||||
id: header
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
NIcon {
|
||||
icon: "settings-audio"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.audio.title")
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NTabBar {
|
||||
id: tabBar
|
||||
Layout.fillWidth: true
|
||||
margins: Style.marginS
|
||||
currentIndex: panelContent.currentTabIndex
|
||||
distributeEvenly: true
|
||||
onCurrentIndexChanged: panelContent.currentTabIndex = currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.volumes")
|
||||
tabIndex: 0
|
||||
checked: tabBar.currentIndex === 0
|
||||
}
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.devices")
|
||||
tabIndex: 1
|
||||
checked: tabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content Stack
|
||||
StackLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
currentIndex: panelContent.currentTabIndex
|
||||
|
||||
// Applications Tab (Volume)
|
||||
NScrollView {
|
||||
id: volumeScrollView
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
contentWidth: availableWidth
|
||||
reserveScrollbarSpace: false
|
||||
gradientColor: Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
width: volumeScrollView.availableWidth
|
||||
|
||||
// Output Volume
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: outputVolumeColumn.implicitHeight + Style.margin2M
|
||||
|
||||
ColumnLayout {
|
||||
id: outputVolumeColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("common.output")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: AudioService.sink ? (" - " + (AudioService.sink.description || AudioService.sink.name || "")) : ""
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NValueSlider {
|
||||
id: outputVolumeSlider
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: localOutputVolume
|
||||
stepSize: 0.01
|
||||
heightRatio: 0.5
|
||||
onMoved: function (value) {
|
||||
localOutputVolume = value;
|
||||
}
|
||||
onPressedChanged: function (pressed) {
|
||||
localOutputVolumeChanging = pressed;
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Math.round((panelContent.outputVolumeGuard ? localOutputVolume : AudioService.volume) * 100) + "%"
|
||||
pointSize: Style.fontSizeM
|
||||
family: Settings.data.ui.fontFixed
|
||||
color: Color.mOnSurface
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: 45 * Style.uiScaleRatio
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: AudioService.getOutputIcon()
|
||||
tooltipText: I18n.tr("tooltips.output-muted")
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
onClicked: {
|
||||
AudioService.suppressOutputOSD();
|
||||
AudioService.setOutputMuted(!AudioService.muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Input Volume
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: inputVolumeColumn.implicitHeight + Style.margin2M
|
||||
|
||||
ColumnLayout {
|
||||
id: inputVolumeColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("common.input")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: AudioService.source ? (" - " + (AudioService.source.description || AudioService.source.name || "")) : ""
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NValueSlider {
|
||||
id: inputVolumeSlider
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: localInputVolume
|
||||
stepSize: 0.01
|
||||
heightRatio: 0.5
|
||||
onMoved: function (value) {
|
||||
localInputVolume = value;
|
||||
}
|
||||
onPressedChanged: function (pressed) {
|
||||
localInputVolumeChanging = pressed;
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Math.round((panelContent.inputVolumeGuard ? localInputVolume : AudioService.inputVolume) * 100) + "%"
|
||||
pointSize: Style.fontSizeM
|
||||
family: Settings.data.ui.fontFixed
|
||||
color: Color.mOnSurface
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: 45 * Style.uiScaleRatio
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: AudioService.getInputIcon()
|
||||
tooltipText: I18n.tr("tooltips.input-muted")
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
onClicked: {
|
||||
AudioService.suppressInputOSD();
|
||||
AudioService.setInputMuted(!AudioService.inputMuted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bind all app stream nodes to access their audio properties
|
||||
PwObjectTracker {
|
||||
id: appStreamsTracker
|
||||
objects: panelContent.appStreams
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: panelContent.appStreams
|
||||
|
||||
NBox {
|
||||
id: appBox
|
||||
required property PwNode modelData
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: appRow.implicitHeight + Style.margin2M
|
||||
visible: !isCaptureStream
|
||||
|
||||
// Track individual node to ensure properties are bound
|
||||
PwObjectTracker {
|
||||
objects: modelData ? [modelData] : []
|
||||
}
|
||||
|
||||
property PwNodeAudio nodeAudio: (modelData && modelData.audio) ? modelData.audio : null
|
||||
property real appVolume: (nodeAudio && nodeAudio.volume !== undefined) ? nodeAudio.volume : 0.0
|
||||
property bool appMuted: (nodeAudio && nodeAudio.muted !== undefined) ? nodeAudio.muted : false
|
||||
|
||||
// Check if this is a capture stream (after node is bound)
|
||||
readonly property bool isCaptureStream: {
|
||||
if (!modelData || !modelData.properties)
|
||||
return false;
|
||||
const props = modelData.properties;
|
||||
// Exclude capture streams - check for stream.capture.sink property
|
||||
if (props["stream.capture.sink"] !== undefined) {
|
||||
return true;
|
||||
}
|
||||
const mediaClass = props["media.class"] || "";
|
||||
// Exclude Stream/Input (capture) but allow Stream/Output (playback)
|
||||
if (mediaClass.includes("Capture") || mediaClass === "Stream/Input" || mediaClass === "Stream/Input/Audio") {
|
||||
return true;
|
||||
}
|
||||
const mediaRole = props["media.role"] || "";
|
||||
if (mediaRole === "Capture") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper function to validate if a fuzzy match is actually related
|
||||
function isValidMatch(searchTerm, entry) {
|
||||
if (!entry)
|
||||
return false;
|
||||
var search = searchTerm.toLowerCase();
|
||||
var id = (entry.id || "").toLowerCase();
|
||||
var name = (entry.name || "").toLowerCase();
|
||||
var icon = (entry.icon || "").toLowerCase();
|
||||
// Match is valid if search term appears in entry or entry appears in search
|
||||
return id.includes(search) || name.includes(search) || icon.includes(search) || search.includes(id.split('.').pop()) || search.includes(name.replace(/\s+/g, ''));
|
||||
}
|
||||
|
||||
readonly property string appName: {
|
||||
if (!modelData)
|
||||
return "Unknown App";
|
||||
|
||||
var props = modelData.properties;
|
||||
var desc = modelData.description || "";
|
||||
var name = modelData.name || "";
|
||||
|
||||
if (!props) {
|
||||
if (desc)
|
||||
return desc;
|
||||
if (name) {
|
||||
var nameParts = name.split(/[-_]/);
|
||||
if (nameParts.length > 0 && nameParts[0])
|
||||
return nameParts[0].charAt(0).toUpperCase() + nameParts[0].slice(1);
|
||||
return name;
|
||||
}
|
||||
return "Unknown App";
|
||||
}
|
||||
|
||||
var binaryName = props["application.process.binary"] || "";
|
||||
|
||||
// Try binary name first (fixes Electron apps like vesktop)
|
||||
if (binaryName) {
|
||||
var binParts = binaryName.split("/");
|
||||
if (binParts.length > 0) {
|
||||
var binName = binParts[binParts.length - 1].toLowerCase();
|
||||
var entry = ThemeIcons.findAppEntry(binName);
|
||||
// Only use entry if it's actually related to binary name
|
||||
if (entry && entry.name && isValidMatch(binName, entry))
|
||||
return entry.name;
|
||||
}
|
||||
}
|
||||
|
||||
var computedAppName = props["application.name"] || "";
|
||||
var mediaName = props["media.name"] || "";
|
||||
var mediaTitle = props["media.title"] || "";
|
||||
var appId = props["application.id"] || "";
|
||||
|
||||
if (appId) {
|
||||
var entry = ThemeIcons.findAppEntry(appId);
|
||||
if (entry && entry.name && isValidMatch(appId, entry))
|
||||
return entry.name;
|
||||
if (!computedAppName) {
|
||||
var parts = appId.split(".");
|
||||
if (parts.length > 0 && parts[0])
|
||||
computedAppName = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!computedAppName && binaryName) {
|
||||
var binParts = binaryName.split("/");
|
||||
if (binParts.length > 0 && binParts[binParts.length - 1])
|
||||
computedAppName = binParts[binParts.length - 1].charAt(0).toUpperCase() + binParts[binParts.length - 1].slice(1);
|
||||
}
|
||||
|
||||
var result = computedAppName || mediaTitle || mediaName || binaryName || desc || name;
|
||||
|
||||
if (!result || result === "" || result === "Unknown App") {
|
||||
if (name) {
|
||||
var nameParts = name.split(/[-_]/);
|
||||
if (nameParts.length > 0 && nameParts[0])
|
||||
result = nameParts[0].charAt(0).toUpperCase() + nameParts[0].slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
return result || "Unknown App";
|
||||
}
|
||||
|
||||
readonly property string appIcon: {
|
||||
if (!modelData)
|
||||
return ThemeIcons.iconFromName("application-x-executable", "application-x-executable");
|
||||
|
||||
var props = modelData.properties;
|
||||
|
||||
if (!props) {
|
||||
var name = modelData.name || "";
|
||||
if (name) {
|
||||
var nameParts = name.split(/[-_]/);
|
||||
if (nameParts.length > 0) {
|
||||
var entry = ThemeIcons.findAppEntry(nameParts[0].toLowerCase());
|
||||
if (entry && entry.icon && isValidMatch(nameParts[0].toLowerCase(), entry))
|
||||
return ThemeIcons.iconFromName(entry.icon, "application-x-executable");
|
||||
}
|
||||
}
|
||||
return ThemeIcons.iconFromName("application-x-executable", "application-x-executable");
|
||||
}
|
||||
|
||||
var binaryName = props["application.process.binary"] || "";
|
||||
if (binaryName) {
|
||||
var binParts = binaryName.split("/");
|
||||
if (binParts.length > 0) {
|
||||
var binName = binParts[binParts.length - 1].toLowerCase();
|
||||
var entry = ThemeIcons.findAppEntry(binName);
|
||||
if (entry && entry.icon && isValidMatch(binName, entry))
|
||||
return ThemeIcons.iconFromName(entry.icon, "");
|
||||
}
|
||||
}
|
||||
|
||||
var iconName = props["application.icon-name"] || "";
|
||||
if (iconName && ThemeIcons.iconExists(iconName)) {
|
||||
var iconPath = ThemeIcons.iconFromName(iconName, "");
|
||||
if (iconPath && iconPath !== "")
|
||||
return iconPath;
|
||||
}
|
||||
|
||||
var appId = props["application.id"] || "";
|
||||
if (appId) {
|
||||
var entry = ThemeIcons.findAppEntry(appId);
|
||||
if (entry && entry.icon && isValidMatch(appId, entry))
|
||||
return ThemeIcons.iconFromName(entry.icon, "");
|
||||
}
|
||||
|
||||
var appName = props["application.name"] || "";
|
||||
if (appName) {
|
||||
var entry = ThemeIcons.findAppEntry(appName.toLowerCase());
|
||||
if (entry && entry.icon && isValidMatch(appName.toLowerCase(), entry))
|
||||
return ThemeIcons.iconFromName(entry.icon, "");
|
||||
}
|
||||
|
||||
var name = modelData.name || "";
|
||||
if (name) {
|
||||
var nameParts = name.split(/[-_]/);
|
||||
if (nameParts.length > 0) {
|
||||
var entry = ThemeIcons.findAppEntry(nameParts[0].toLowerCase());
|
||||
if (entry && entry.icon && isValidMatch(nameParts[0].toLowerCase(), entry))
|
||||
return ThemeIcons.iconFromName(entry.icon, "");
|
||||
}
|
||||
}
|
||||
|
||||
return ThemeIcons.iconFromName("application-x-executable", "application-x-executable");
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: appRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
// App Icon
|
||||
IconImage {
|
||||
id: appIconImage
|
||||
Layout.preferredWidth: Style.baseWidgetSize
|
||||
Layout.preferredHeight: Style.baseWidgetSize
|
||||
source: appBox.appIcon
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
|
||||
// Fallback icon if image fails to load
|
||||
NIcon {
|
||||
anchors.fill: parent
|
||||
icon: "apps"
|
||||
pointSize: Style.fontSizeXL
|
||||
color: Color.mPrimary
|
||||
visible: appIconImage.status === Image.Error || appIconImage.status === Image.Null || appBox.appIcon === ""
|
||||
}
|
||||
}
|
||||
|
||||
// App Name and Volume Slider
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXS
|
||||
|
||||
NText {
|
||||
text: appBox.appName || "Unknown App"
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: (appBox.appVolume !== undefined) ? appBox.appVolume : 0.0
|
||||
stepSize: 0.01
|
||||
heightRatio: 0.5
|
||||
enabled: !!(appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true)
|
||||
onMoved: function (value) {
|
||||
if (appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true) {
|
||||
appBox.nodeAudio.volume = value;
|
||||
var key = AudioService.getAppKey(appBox.modelData);
|
||||
if (key)
|
||||
AudioService.setAppStreamVolume(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Math.round((appBox.appVolume !== undefined ? appBox.appVolume : 0.0) * 100) + "%"
|
||||
pointSize: Style.fontSizeM
|
||||
family: Settings.data.ui.fontFixed
|
||||
color: Color.mOnSurface
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: 45 * Style.uiScaleRatio
|
||||
horizontalAlignment: Text.AlignRight
|
||||
enabled: !!(appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true)
|
||||
}
|
||||
|
||||
// Mute Button
|
||||
NIconButton {
|
||||
icon: (appBox.appMuted === true) ? "volume-mute" : "volume-high"
|
||||
tooltipText: (appBox.appMuted === true) ? I18n.tr("tooltips.unmute") : I18n.tr("tooltips.mute")
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
enabled: !!(appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true)
|
||||
onClicked: {
|
||||
if (appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true) {
|
||||
var newMuted = !appBox.appMuted;
|
||||
appBox.nodeAudio.muted = newMuted;
|
||||
var key = AudioService.getAppKey(appBox.modelData);
|
||||
if (key)
|
||||
AudioService.setAppStreamMuted(key, newMuted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
NText {
|
||||
visible: panelContent.appStreams.length === 0
|
||||
text: I18n.tr("panels.audio.panel-applications-empty")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Devices Tab
|
||||
NScrollView {
|
||||
id: devicesScrollView
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
contentWidth: availableWidth
|
||||
reserveScrollbarSpace: false
|
||||
gradientColor: Color.mSurface
|
||||
|
||||
// AudioService Devices
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
width: devicesScrollView.availableWidth
|
||||
|
||||
// -------------------------------
|
||||
// Output Devices
|
||||
ButtonGroup {
|
||||
id: sinks
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: outputColumn.implicitHeight + Style.margin2M
|
||||
|
||||
ColumnLayout {
|
||||
id: outputColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.audio.devices-output-device-label")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: AudioService.sinks
|
||||
NRadioButton {
|
||||
ButtonGroup.group: sinks
|
||||
required property PwNode modelData
|
||||
pointSize: Style.fontSizeS
|
||||
text: modelData.description
|
||||
checked: AudioService.sink?.id === modelData.id
|
||||
onClicked: {
|
||||
AudioService.setAudioSink(modelData);
|
||||
localOutputVolume = AudioService.volume;
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// Input Devices
|
||||
ButtonGroup {
|
||||
id: sources
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: inputColumn.implicitHeight + Style.margin2M
|
||||
|
||||
ColumnLayout {
|
||||
id: inputColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.audio.devices-input-device-label")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: AudioService.sources
|
||||
NRadioButton {
|
||||
ButtonGroup.group: sources
|
||||
required property PwNode modelData
|
||||
pointSize: Style.fontSizeS
|
||||
text: modelData.description
|
||||
checked: AudioService.source?.id === modelData.id
|
||||
onClicked: AudioService.setAudioSource(modelData)
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Services.Hardware
|
||||
import qs.Services.Networking
|
||||
import qs.Services.Power
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: Math.round(440 * Style.uiScaleRatio)
|
||||
preferredHeight: Math.round(460 * Style.uiScaleRatio)
|
||||
|
||||
panelContent: Item {
|
||||
id: panelContent
|
||||
|
||||
property real contentPreferredHeight: mainLayout.implicitHeight + Style.margin2L
|
||||
|
||||
property var batteryWidgetInstance: BarService.lookupWidget("Battery", screen ? screen.name : null)
|
||||
readonly property var batteryWidgetSettings: batteryWidgetInstance ? batteryWidgetInstance.widgetSettings : null
|
||||
readonly property var batteryWidgetMetadata: BarWidgetRegistry.widgetMetadata["Battery"]
|
||||
readonly property bool powerProfileAvailable: PowerProfileService.available
|
||||
readonly property var powerProfiles: [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
|
||||
readonly property bool profilesAvailable: PowerProfileService.available
|
||||
property int profileIndex: profileToIndex(PowerProfileService.profile)
|
||||
readonly property bool showPowerProfiles: panelID ? panelID.showPowerProfiles : resolveWidgetSetting("showPowerProfiles", false)
|
||||
readonly property bool showNoctaliaPerformance: panelID ? panelID.showNoctaliaPerformance : resolveWidgetSetting("showNoctaliaPerformance", false)
|
||||
readonly property bool isLowBattery: BatteryService.isLowBattery
|
||||
readonly property bool isCriticalBattery: BatteryService.isCriticalBattery
|
||||
readonly property var primaryDevice: BatteryService.primaryDevice
|
||||
|
||||
function profileToIndex(p) {
|
||||
return powerProfiles.indexOf(p) ?? 1;
|
||||
}
|
||||
|
||||
function indexToProfile(idx) {
|
||||
return powerProfiles[idx] ?? PowerProfile.Balanced;
|
||||
}
|
||||
|
||||
function setProfileByIndex(idx) {
|
||||
var prof = indexToProfile(idx);
|
||||
profileIndex = idx;
|
||||
PowerProfileService.setProfile(prof);
|
||||
}
|
||||
|
||||
function resolveWidgetSetting(key, defaultValue) {
|
||||
if (batteryWidgetSettings && batteryWidgetSettings[key] !== undefined)
|
||||
return batteryWidgetSettings[key];
|
||||
if (batteryWidgetMetadata && batteryWidgetMetadata[key] !== undefined)
|
||||
return batteryWidgetMetadata[key];
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PowerProfileService
|
||||
function onProfileChanged() {
|
||||
panelContent.profileIndex = panelContent.profileToIndex(PowerProfileService.profile);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BarService
|
||||
function onActiveWidgetsChanged() {
|
||||
panelContent.batteryWidgetInstance = BarService.lookupWidget("Battery", screen ? screen.name : null);
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
// HEADER
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: headerRow.implicitHeight + Style.margin2M
|
||||
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: (BatteryService.isCharging(primaryDevice) || BatteryService.isPluggedIn(primaryDevice)) ? Color.mPrimary : (BatteryService.isCriticalBattery(primaryDevice) || BatteryService.isLowBattery(primaryDevice)) ? Color.mError : Color.mOnSurface
|
||||
icon: BatteryService.getIcon(BatteryService.getPercentage(primaryDevice), BatteryService.isCharging(primaryDevice), BatteryService.isPluggedIn(primaryDevice), BatteryService.isDeviceReady(primaryDevice))
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: I18n.tr("common.battery")
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Charge level + health/time
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: chargeLayout.implicitHeight + Style.margin2L
|
||||
visible: BatteryService.laptopBatteries.length > 0 || BatteryService.bluetoothBatteries.length > 0
|
||||
|
||||
ColumnLayout {
|
||||
id: chargeLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginL
|
||||
|
||||
// Laptop batteries section
|
||||
Repeater {
|
||||
model: BatteryService.laptopBatteries
|
||||
delegate: ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
RowLayout {
|
||||
Item {
|
||||
id: batteryInfoItem
|
||||
implicitWidth: batteryInfoRow.implicitWidth
|
||||
implicitHeight: batteryInfoRow.implicitHeight
|
||||
|
||||
RowLayout {
|
||||
id: batteryInfoRow
|
||||
anchors.fill: parent
|
||||
|
||||
NIcon {
|
||||
icon: BatteryService.getIcon(BatteryService.getPercentage(modelData), BatteryService.isCharging(modelData), BatteryService.isPluggedIn(modelData), BatteryService.isDeviceReady(modelData))
|
||||
color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
readonly property string dName: BatteryService.getDeviceName(modelData)
|
||||
text: dName ? dName : I18n.tr("common.battery")
|
||||
color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface
|
||||
pointSize: Style.fontSizeS
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
if (modelData.healthSupported) {
|
||||
TooltipService.show(batteryInfoItem, `${I18n.tr("battery.battery-health")}: ${Math.round(modelData.healthPercentage)}%`);
|
||||
}
|
||||
}
|
||||
onExited: TooltipService.hide(batteryInfoItem)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: BatteryService.getTimeRemainingText(modelData)
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: Math.round(8 * Style.uiScaleRatio)
|
||||
radius: Math.min(Style.radiusL, height / 2)
|
||||
color: Color.mSurface
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
width: {
|
||||
var p = BatteryService.getPercentage(modelData);
|
||||
var ratio = Math.max(0, Math.min(1, p / 100));
|
||||
return parent.width * ratio;
|
||||
}
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.preferredWidth: 40 * Style.uiScaleRatio
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: `${BatteryService.getPercentage(modelData)}%`
|
||||
color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface
|
||||
pointSize: Style.fontSizeS
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
visible: BatteryService.laptopBatteries.length > 0 && BatteryService.bluetoothBatteries.length > 0
|
||||
}
|
||||
|
||||
// Other devices (Bluetooth) section
|
||||
Repeater {
|
||||
model: BatteryService.bluetoothBatteries
|
||||
delegate: ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
icon: BluetoothService.getDeviceIcon(modelData)
|
||||
color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
readonly property string dName: BatteryService.getDeviceName(modelData)
|
||||
text: dName ? dName : I18n.tr("common.bluetooth")
|
||||
color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface
|
||||
pointSize: Style.fontSizeS
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: Math.round(8 * Style.uiScaleRatio)
|
||||
radius: Math.min(Style.radiusL, height / 2)
|
||||
color: Color.mSurface
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
width: {
|
||||
var p = BatteryService.getPercentage(modelData);
|
||||
var ratio = Math.max(0, Math.min(1, p / 100));
|
||||
return parent.width * ratio;
|
||||
}
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.preferredWidth: 40 * Style.uiScaleRatio
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: `${BatteryService.getPercentage(modelData)}%`
|
||||
color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface
|
||||
pointSize: Style.fontSizeS
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
height: controlsLayout.implicitHeight + Style.margin2L
|
||||
visible: showPowerProfiles || showNoctaliaPerformance
|
||||
|
||||
ColumnLayout {
|
||||
id: controlsLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
ColumnLayout {
|
||||
visible: powerProfileAvailable && showPowerProfiles
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("battery.power-profile")
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: PowerProfileService.getName(profileIndex)
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 2
|
||||
stepSize: 1
|
||||
snapAlways: true
|
||||
heightRatio: 0.5
|
||||
value: profileIndex
|
||||
enabled: profilesAvailable
|
||||
onPressedChanged: (pressed, v) => {
|
||||
if (!pressed) {
|
||||
setProfileByIndex(v);
|
||||
}
|
||||
}
|
||||
onMoved: v => {
|
||||
profileIndex = v;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
icon: "powersaver"
|
||||
pointSize: Style.fontSizeS
|
||||
color: PowerProfileService.getIcon() === "powersaver" ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "balanced"
|
||||
pointSize: Style.fontSizeS
|
||||
color: PowerProfileService.getIcon() === "balanced" ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "performance"
|
||||
pointSize: Style.fontSizeS
|
||||
color: PowerProfileService.getIcon() === "performance" ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
visible: showPowerProfiles && PowerProfileService.available && showNoctaliaPerformance
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
visible: showNoctaliaPerformance
|
||||
|
||||
NText {
|
||||
text: I18n.tr("toast.noctalia-performance.label")
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: PowerProfileService.noctaliaPerformanceMode ? "rocket" : "rocket-off"
|
||||
pointSize: Style.fontSizeL
|
||||
color: PowerProfileService.noctaliaPerformanceMode ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NToggle {
|
||||
checked: PowerProfileService.noctaliaPerformanceMode
|
||||
onToggled: checked => PowerProfileService.noctaliaPerformanceMode = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import "../Settings/Tabs/Connections" as BluetoothPrefs
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.Hardware
|
||||
import qs.Services.Networking
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: Math.round(440 * Style.uiScaleRatio)
|
||||
preferredHeight: Math.round(500 * Style.uiScaleRatio)
|
||||
|
||||
panelContent: Rectangle {
|
||||
id: panelContent
|
||||
color: "transparent"
|
||||
|
||||
property real contentPreferredHeight: Math.min(root.preferredHeight, mainColumn.implicitHeight + Style.margin2L)
|
||||
|
||||
ColumnLayout {
|
||||
id: mainColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
// Header
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: headerRow.implicitHeight + Style.margin2M
|
||||
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: BluetoothService.enabled ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("common.bluetooth")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: bluetoothSwitch
|
||||
checked: BluetoothService.enabled
|
||||
enabled: !NetworkService.airplaneModeEnabled && BluetoothService.bluetoothAvailable
|
||||
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
|
||||
baseSize: Style.baseWidgetSize * 0.65
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: Settings.data.network.bluetoothAutoConnect ? "bluetooth-connected" : "bluetooth"
|
||||
tooltipText: Settings.data.network.bluetoothAutoConnect ? I18n.tr("tooltips.bluetooth-auto-connect-on") : I18n.tr("tooltips.bluetooth-auto-connect-off")
|
||||
colorFg: Settings.data.network.bluetoothAutoConnect ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: Settings.data.network.bluetoothAutoConnect = !Settings.data.network.bluetoothAutoConnect
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "settings"
|
||||
tooltipText: I18n.tr("tooltips.open-settings")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 1, screen)
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NScrollView {
|
||||
id: bluetoothScrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
reserveScrollbarSpace: false
|
||||
gradientColor: Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
id: devicesList
|
||||
width: bluetoothScrollView.availableWidth
|
||||
spacing: Style.marginM
|
||||
|
||||
// Adapter not available of disabled
|
||||
NBox {
|
||||
id: disabledBox
|
||||
visible: !BluetoothService.enabled
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: disabledColumn.implicitHeight + Style.margin2M
|
||||
|
||||
ColumnLayout {
|
||||
id: disabledColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginL
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "bluetooth-off"
|
||||
pointSize: 48
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("bluetooth.panel.disabled")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("bluetooth.panel.enable-message")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state when no paired devices
|
||||
NBox {
|
||||
id: emptyBox
|
||||
visible: {
|
||||
if (!BluetoothService.enabled || !BluetoothService.devices)
|
||||
return false;
|
||||
// Pulling pairedDevices count from the source component
|
||||
return (btSource.pairedDevices.length === 0 && btSource.connectedDevices.length === 0);
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: emptyColumn.implicitHeight + Style.margin2M
|
||||
|
||||
ColumnLayout {
|
||||
id: emptyColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginL
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "bluetooth"
|
||||
pointSize: 48
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("bluetooth.panel.no-devices")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.settings")
|
||||
icon: "settings"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 1, screen)
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pull connected/paired lists from BluetoothSubTab
|
||||
BluetoothPrefs.BluetoothSubTab {
|
||||
id: btSource
|
||||
Layout.fillWidth: true
|
||||
showOnlyLists: true
|
||||
visible: !disabledBox.visible && !emptyBox.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.Hardware
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: Math.round(440 * Style.uiScaleRatio)
|
||||
preferredHeight: Math.round(420 * Style.uiScaleRatio)
|
||||
|
||||
panelContent: Item {
|
||||
id: panelContent
|
||||
property real contentPreferredHeight: mainColumn.implicitHeight + Style.margin2L
|
||||
|
||||
property var brightnessWidgetInstance: BarService.lookupWidget("Brightness", screen ? screen.name : null)
|
||||
readonly property var brightnessWidgetSettings: brightnessWidgetInstance ? brightnessWidgetInstance.widgetSettings : null
|
||||
readonly property var brightnessWidgetMetadata: BarWidgetRegistry.widgetMetadata["Brightness"]
|
||||
|
||||
function resolveWidgetSetting(key, defaultValue) {
|
||||
if (brightnessWidgetSettings && brightnessWidgetSettings[key] !== undefined)
|
||||
return brightnessWidgetSettings[key];
|
||||
if (brightnessWidgetMetadata && brightnessWidgetMetadata[key] !== undefined)
|
||||
return brightnessWidgetMetadata[key];
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BarService
|
||||
function onActiveWidgetsChanged() {
|
||||
panelContent.brightnessWidgetInstance = BarService.lookupWidget("Brightness", screen ? screen.name : null);
|
||||
}
|
||||
}
|
||||
|
||||
property real globalBrightness: 0
|
||||
property bool globalBrightnessChanging: false
|
||||
property int globalBrightnessCapableMonitors: 0
|
||||
|
||||
function getIcon(brightness) {
|
||||
return brightness <= 0.5 ? "brightness-low" : "brightness-high";
|
||||
}
|
||||
|
||||
function getControllableMonitors() {
|
||||
var monitors = BrightnessService.monitors || [];
|
||||
return monitors.filter(m => m && m.brightnessControlAvailable);
|
||||
}
|
||||
|
||||
function updateGlobalBrightness() {
|
||||
var monitors = getControllableMonitors();
|
||||
panelContent.globalBrightnessCapableMonitors = monitors.length;
|
||||
|
||||
if (panelContent.globalBrightnessChanging)
|
||||
return;
|
||||
|
||||
if (monitors.length === 0) {
|
||||
panelContent.globalBrightness = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var total = 0;
|
||||
monitors.forEach(m => {
|
||||
var brightnessValue = isNaN(m.brightness) ? 0 : m.brightness;
|
||||
total += brightnessValue;
|
||||
});
|
||||
panelContent.globalBrightness = total / monitors.length;
|
||||
}
|
||||
|
||||
function applyGlobalBrightness(value) {
|
||||
var monitors = BrightnessService.monitors || [];
|
||||
monitors.forEach(m => {
|
||||
if (m && m.brightnessControlAvailable) {
|
||||
m.setBrightness(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Component.onCompleted: updateGlobalBrightness()
|
||||
|
||||
Connections {
|
||||
target: BrightnessService
|
||||
function onMonitorBrightnessChanged(monitor, newBrightness) {
|
||||
panelContent.updateGlobalBrightness();
|
||||
}
|
||||
function onMonitorsChanged() {
|
||||
panelContent.updateGlobalBrightness();
|
||||
}
|
||||
function onDdcMonitorsChanged() {
|
||||
panelContent.updateGlobalBrightness();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: mainColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
// HEADER
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: headerRow.implicitHeight + Style.margin2M
|
||||
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: "settings-display"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.display.title")
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NScrollView {
|
||||
id: brightnessScrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
contentWidth: availableWidth
|
||||
reserveScrollbarSpace: false
|
||||
gradientColor: Color.mSurface
|
||||
|
||||
// AudioService Devices
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
width: brightnessScrollView.availableWidth
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
visible: panelContent.globalBrightnessCapableMonitors > 1 && panelContent.resolveWidgetSetting("applyToAllMonitors", false)
|
||||
implicitHeight: globalBrightnessContent.implicitHeight + (Style.marginXL)
|
||||
|
||||
ColumnLayout {
|
||||
id: globalBrightnessContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.display.monitors-global-brightness-label")
|
||||
description: I18n.tr("panels.display.monitors-global-brightness-description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
icon: panelContent.getIcon(panelContent.globalBrightness)
|
||||
pointSize: Style.fontSizeXL
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
id: globalBrightnessSlider
|
||||
from: 0
|
||||
to: 1
|
||||
value: panelContent.globalBrightness
|
||||
stepSize: 0.01
|
||||
enabled: panelContent.globalBrightnessCapableMonitors > 0
|
||||
onMoved: value => {
|
||||
panelContent.globalBrightness = value;
|
||||
panelContent.applyGlobalBrightness(value);
|
||||
}
|
||||
onPressedChanged: (pressed, value) => {
|
||||
panelContent.globalBrightnessChanging = pressed;
|
||||
panelContent.globalBrightness = value;
|
||||
panelContent.applyGlobalBrightness(value);
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
text: ""
|
||||
}
|
||||
|
||||
NText {
|
||||
text: panelContent.globalBrightnessCapableMonitors > 0 ? Math.round(panelContent.globalBrightness * 100) + "%" : "N/A"
|
||||
Layout.preferredWidth: 55
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Quickshell.screens || []
|
||||
delegate: NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: outputColumn.implicitHeight + Style.margin2M
|
||||
|
||||
property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData)
|
||||
readonly property real compositorScale: {
|
||||
const info = CompositorService.displayScales[modelData.name];
|
||||
return (info && info.scale) ? info.scale : 1.0;
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: outputColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NLabel {
|
||||
label: modelData.name || "Unknown"
|
||||
labelColor: Color.mPrimary
|
||||
description: {
|
||||
I18n.tr("system.monitor-description", {
|
||||
"model": modelData.model,
|
||||
"width": modelData.width * compositorScale,
|
||||
"height": modelData.height * compositorScale,
|
||||
"scale": compositorScale
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
NIcon {
|
||||
icon: getIcon(brightnessMonitor ? brightnessMonitor.brightness : 0)
|
||||
pointSize: Style.fontSizeXL
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
id: brightnessSlider
|
||||
from: 0
|
||||
to: 1
|
||||
value: brightnessMonitor ? brightnessMonitor.brightness : 0.5
|
||||
stepSize: 0.01
|
||||
enabled: brightnessMonitor ? brightnessMonitor.brightnessControlAvailable : false
|
||||
onMoved: value => {
|
||||
if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) {
|
||||
brightnessMonitor.setBrightness(value);
|
||||
}
|
||||
}
|
||||
onPressedChanged: (pressed, value) => {
|
||||
if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) {
|
||||
brightnessMonitor.setBrightness(value);
|
||||
}
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
text: brightnessMonitor ? Math.round(brightnessSlider.value * 100) + "%" : "N/A"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: Math.round(820 * Style.uiScaleRatio)
|
||||
preferredHeight: Math.round(620 * Style.uiScaleRatio)
|
||||
panelAnchorHorizontalCenter: true
|
||||
panelAnchorVerticalCenter: true
|
||||
|
||||
panelContent: Rectangle {
|
||||
id: panelContent
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
|
||||
readonly property string currentVersion: UpdateService.currentVersion
|
||||
readonly property string previousVersion: UpdateService.previousVersion
|
||||
readonly property bool hasPreviousVersion: previousVersion && previousVersion.length > 0
|
||||
readonly property string releaseContent: UpdateService.releaseContent || ""
|
||||
|
||||
function colorizeHeaders(text) {
|
||||
if (!text)
|
||||
return "";
|
||||
const color = Color.mPrimary;
|
||||
return text.replace(/^# (.*)$/gm, `<br/><h1 style="color:${color}">$1</h1><br/>`).replace(/^## (.*)$/gm, `<br/><h2 style="color:${color}">$1</h2><br/>`);
|
||||
}
|
||||
readonly property string subtitleText: hasPreviousVersion ? I18n.tr("changelog.panel.subtitle-updated", {
|
||||
"previousVersion": previousVersion
|
||||
}) : I18n.tr("changelog.panel.subtitle-fresh")
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: "sparkles"
|
||||
color: Color.mPrimary
|
||||
pointSize: Style.fontSizeXXL
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("changelog.panel.title", {
|
||||
"version": currentVersion || UpdateService.currentVersion
|
||||
})
|
||||
pointSize: Style.fontSizeXL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
NText {
|
||||
text: subtitleText
|
||||
color: Color.mOnSurface
|
||||
opacity: Style.opacityMedium
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
onClicked: root.close()
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignRight
|
||||
Layout.preferredHeight: Style.baseWidgetSize
|
||||
Layout.preferredWidth: Style.baseWidgetSize
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
clip: true
|
||||
Layout.fillWidth: true
|
||||
color: Qt.alpha(Color.mPrimary, 0.08)
|
||||
radius: Style.radiusS
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderS
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: hasPreviousVersion ? previousVersion : I18n.tr("changelog.panel.version-new-user")
|
||||
font.weight: Style.fontWeightSemiBold
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "arrow-right"
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: currentVersion || UpdateService.currentVersion
|
||||
font.weight: Style.fontWeightSemiBold
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NScrollView {
|
||||
id: changelogScrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
padding: 0
|
||||
|
||||
ColumnLayout {
|
||||
width: changelogScrollView.availableWidth
|
||||
spacing: Style.marginM
|
||||
|
||||
NText {
|
||||
visible: UpdateService.fetchError !== ""
|
||||
text: UpdateService.fetchError
|
||||
color: Color.mError
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.fillWidth: true
|
||||
visible: releaseContent.length > 0
|
||||
text: panelContent.colorizeHeaders(releaseContent)
|
||||
wrapMode: Text.WordWrap
|
||||
elide: Text.ElideNone
|
||||
textFormat: Text.MarkdownText
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: releaseContent.length === 0
|
||||
text: I18n.tr("changelog.panel.empty")
|
||||
color: Color.mOnSurfaceVariant
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "brand-discord"
|
||||
text: I18n.tr("changelog.panel.buttons-discord")
|
||||
outlined: true
|
||||
onClicked: UpdateService.openDiscord()
|
||||
}
|
||||
|
||||
NButton {
|
||||
Layout.fillWidth: true
|
||||
visible: UpdateService.feedbackUrl !== ""
|
||||
icon: "forms"
|
||||
text: I18n.tr("changelog.panel.buttons-feedback")
|
||||
outlined: true
|
||||
onClicked: UpdateService.openFeedbackForm()
|
||||
}
|
||||
|
||||
NButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "check"
|
||||
text: I18n.tr("changelog.panel.buttons-dismiss")
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
if (UpdateService && UpdateService.changelogCurrentVersion) {
|
||||
UpdateService.markChangelogSeen(UpdateService.changelogCurrentVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.Cards
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Services.Location
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
panelContent: Item {
|
||||
id: panelContent
|
||||
anchors.fill: parent
|
||||
|
||||
readonly property real contentPreferredWidth: Math.round((Settings.data.location.showWeekNumberInCalendar ? 440 : 420) * Style.uiScaleRatio)
|
||||
readonly property real contentPreferredHeight: content.implicitHeight + Style.margin2L
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
x: Style.marginL
|
||||
y: Style.marginL
|
||||
width: parent.width - Style.margin2L
|
||||
spacing: Style.marginL
|
||||
|
||||
// All clock panel cards
|
||||
Repeater {
|
||||
model: Settings.data.calendar.cards
|
||||
Loader {
|
||||
active: modelData.enabled && (modelData.id !== "weather-card" || Settings.data.location.weatherEnabled)
|
||||
visible: active
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: {
|
||||
switch (modelData.id) {
|
||||
case "calendar-header-card":
|
||||
return calendarHeaderCard;
|
||||
case "calendar-month-card":
|
||||
return calendarMonthCard;
|
||||
case "weather-card":
|
||||
return weatherCard;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: calendarHeaderCard
|
||||
CalendarHeaderCard {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: calendarMonthCard
|
||||
CalendarMonthCard {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: weatherCard
|
||||
WeatherCard {
|
||||
Layout.fillWidth: true
|
||||
forecastDays: 5
|
||||
showLocation: false
|
||||
}
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Cards
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Services.Media
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
// Positioning
|
||||
readonly property string controlCenterPosition: Settings.data.controlCenter.position
|
||||
|
||||
// Check if there's a bar on this screen
|
||||
readonly property bool hasBarOnScreen: {
|
||||
var monitors = Settings.data.bar.monitors || [];
|
||||
return monitors.length === 0 || monitors.includes(screen?.name);
|
||||
}
|
||||
|
||||
// When position is "close_to_bar_button" but there's no bar, fall back to center
|
||||
readonly property bool shouldCenter: controlCenterPosition === "close_to_bar_button" && !hasBarOnScreen
|
||||
|
||||
panelAnchorHorizontalCenter: shouldCenter || (controlCenterPosition !== "close_to_bar_button" && (controlCenterPosition.endsWith("_center") || controlCenterPosition === "center"))
|
||||
panelAnchorVerticalCenter: shouldCenter || controlCenterPosition === "center"
|
||||
panelAnchorLeft: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_left")
|
||||
panelAnchorRight: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_right")
|
||||
panelAnchorBottom: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("bottom_")
|
||||
panelAnchorTop: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("top_")
|
||||
|
||||
preferredWidth: Math.round(440 * Style.uiScaleRatio)
|
||||
preferredHeight: {
|
||||
var height = 0;
|
||||
var count = 0;
|
||||
for (var i = 0; i < Settings.data.controlCenter.cards.length; i++) {
|
||||
const card = Settings.data.controlCenter.cards[i];
|
||||
if (!card.enabled)
|
||||
continue;
|
||||
const contributes = (card.id !== "weather-card" || Settings.data.location.weatherEnabled);
|
||||
if (!contributes)
|
||||
continue;
|
||||
count++;
|
||||
switch (card.id) {
|
||||
case "profile-card":
|
||||
height += profileHeight;
|
||||
break;
|
||||
case "shortcuts-card":
|
||||
height += shortcutsHeight;
|
||||
break;
|
||||
case "audio-card":
|
||||
height += audioHeight;
|
||||
break;
|
||||
case "brightness-card":
|
||||
height += brightnessHeight;
|
||||
break;
|
||||
case "weather-card":
|
||||
height += weatherHeight;
|
||||
break;
|
||||
case "media-sysmon-card":
|
||||
height += mediaSysMonHeight;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return height + (count + 1) * Style.marginL;
|
||||
}
|
||||
|
||||
readonly property int profileHeight: Math.round(64 * Style.uiScaleRatio)
|
||||
readonly property int shortcutsHeight: Math.round(52 * Style.uiScaleRatio)
|
||||
readonly property int audioHeight: Math.round(60 * Style.uiScaleRatio)
|
||||
readonly property int brightnessHeight: Math.round(60 * Style.uiScaleRatio)
|
||||
readonly property int mediaSysMonHeight: Math.round(260 * Style.uiScaleRatio)
|
||||
|
||||
// We keep a dynamic weather height due to a more complex layout and font scaling
|
||||
property int weatherHeight: Math.round(210 * Style.uiScaleRatio)
|
||||
|
||||
onOpened: {
|
||||
MediaService.autoSwitchingPaused = true;
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
MediaService.autoSwitchingPaused = false;
|
||||
}
|
||||
|
||||
panelContent: Item {
|
||||
id: panelContent
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
x: Style.marginL
|
||||
y: Style.marginL
|
||||
width: parent.width - Style.margin2L
|
||||
spacing: Style.marginL
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.controlCenter.cards
|
||||
Loader {
|
||||
active: modelData.enabled && (modelData.id !== "weather-card" || Settings.data.location.weatherEnabled)
|
||||
visible: active
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: {
|
||||
switch (modelData.id) {
|
||||
case "profile-card":
|
||||
return profileHeight;
|
||||
case "shortcuts-card":
|
||||
return shortcutsHeight;
|
||||
case "audio-card":
|
||||
return audioHeight;
|
||||
case "brightness-card":
|
||||
return brightnessHeight;
|
||||
case "weather-card":
|
||||
return weatherHeight;
|
||||
case "media-sysmon-card":
|
||||
return mediaSysMonHeight;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
sourceComponent: {
|
||||
switch (modelData.id) {
|
||||
case "profile-card":
|
||||
return profileCard;
|
||||
case "shortcuts-card":
|
||||
return shortcutsCard;
|
||||
case "audio-card":
|
||||
return audioCard;
|
||||
case "brightness-card":
|
||||
return brightnessCard;
|
||||
case "weather-card":
|
||||
return weatherCard;
|
||||
case "media-sysmon-card":
|
||||
return mediaSysMonCard;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: profileCard
|
||||
ProfileCard {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: shortcutsCard
|
||||
ShortcutsCard {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: audioCard
|
||||
AudioCard {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: brightnessCard
|
||||
BrightnessCard {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: weatherCard
|
||||
WeatherCard {
|
||||
Component.onCompleted: {
|
||||
root.weatherHeight = this.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mediaSysMonCard
|
||||
RowLayout {
|
||||
spacing: Style.marginL
|
||||
|
||||
// Media card
|
||||
MediaCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
// System monitors combined in one card
|
||||
SystemMonitorCard {
|
||||
Layout.preferredWidth: Math.round(Style.baseWidgetSize * 2.625)
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string widgetId
|
||||
required property var widgetScreen
|
||||
required property var widgetProps
|
||||
|
||||
property string section: widgetProps && widgetProps.section || ""
|
||||
property int sectionIndex: widgetProps && widgetProps.sectionWidgetIndex || 0
|
||||
|
||||
// Don't reserve space unless the loaded widget is really visible
|
||||
implicitWidth: getImplicitSize(loader.item, "implicitWidth")
|
||||
implicitHeight: getImplicitSize(loader.item, "implicitHeight")
|
||||
|
||||
function getImplicitSize(item, prop) {
|
||||
return (item && item.visible) ? item[prop] : 0;
|
||||
}
|
||||
|
||||
readonly property bool _isPlugin: ControlCenterWidgetRegistry.isPluginWidget(widgetId)
|
||||
|
||||
function _loadPluginWidget() {
|
||||
var comp = ControlCenterWidgetRegistry.getWidget(widgetId);
|
||||
if (!comp)
|
||||
return;
|
||||
var pluginId = widgetId.substring(7); // Remove "plugin:" prefix
|
||||
var api = PluginService.getPluginAPI(pluginId);
|
||||
loader.setSource(comp.url, api ? {
|
||||
"pluginApi": api
|
||||
} : {});
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
anchors.fill: parent
|
||||
asynchronous: false
|
||||
|
||||
// Core widgets use sourceComponent; plugin widgets use setSource()
|
||||
// so pluginApi is available from the first binding evaluation.
|
||||
Component.onCompleted: {
|
||||
if (root._isPlugin) {
|
||||
root._loadPluginWidget();
|
||||
} else {
|
||||
sourceComponent = Qt.binding(function () {
|
||||
return ControlCenterWidgetRegistry.getWidget(widgetId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
// Apply properties to loaded widget
|
||||
for (var prop in widgetProps) {
|
||||
if (item.hasOwnProperty(prop)) {
|
||||
item[prop] = widgetProps[prop];
|
||||
}
|
||||
}
|
||||
|
||||
// Set screen property
|
||||
if (item.hasOwnProperty("screen")) {
|
||||
item.screen = widgetScreen;
|
||||
}
|
||||
|
||||
// Call custom onLoaded if it exists
|
||||
if (item.hasOwnProperty("onLoaded")) {
|
||||
item.onLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
// Explicitly clear references
|
||||
widgetProps = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Error handling
|
||||
Component.onCompleted: {
|
||||
if (!ControlCenterWidgetRegistry.hasWidget(widgetId)) {
|
||||
Logger.w("ControlCenterWidgetLoader", "Widget not found in registry:", widgetId);
|
||||
// Retry briefly in case the registry initializes after this component
|
||||
retryTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
// Retry mechanism to cope with early evaluation before registry is ready
|
||||
Timer {
|
||||
id: retryTimer
|
||||
interval: 150
|
||||
repeat: true
|
||||
running: false
|
||||
property int attempts: 0
|
||||
onTriggered: {
|
||||
attempts += 1;
|
||||
if (ControlCenterWidgetRegistry.hasWidget(widgetId)) {
|
||||
loader.sourceComponent = ControlCenterWidgetRegistry.getWidget(widgetId);
|
||||
stop();
|
||||
attempts = 0;
|
||||
return;
|
||||
}
|
||||
if (attempts >= 20) { // ~3s max
|
||||
stop();
|
||||
attempts = 0;
|
||||
Logger.w("ControlCenterWidgetLoader", "Giving up waiting for widget:", widgetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Networking
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
|
||||
icon: !NetworkService.airplaneModeEnabled ? "plane-off" : "plane"
|
||||
hot: NetworkService.airplaneModeEnabled
|
||||
tooltipText: I18n.tr("toast.airplane-mode.title")
|
||||
onClicked: {
|
||||
NetworkService.setAirplaneMode(!NetworkService.airplaneModeEnabled);
|
||||
}
|
||||
enabled: NetworkService.wifiAvailable && BluetoothService.bluetoothAvailable
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Networking
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
|
||||
icon: !BluetoothService.enabled ? "bluetooth-off" : ((BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 0) ? "bluetooth-connected" : "bluetooth")
|
||||
tooltipText: I18n.tr("common.bluetooth")
|
||||
onClicked: {
|
||||
var p = PanelService.getPanel("bluetoothPanel", screen);
|
||||
if (p)
|
||||
p.toggle(this);
|
||||
}
|
||||
onRightClicked: {
|
||||
if (!NetworkService.airplaneModeEnabled) {
|
||||
BluetoothService.setBluetoothEnabled(!BluetoothService.enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string widgetId: "CustomButton"
|
||||
property var widgetSettings: null
|
||||
|
||||
property string onClickedCommand: ""
|
||||
property string onRightClickedCommand: ""
|
||||
property string onMiddleClickedCommand: ""
|
||||
property string stateChecksJson: "[]" // Store state checks as JSON string
|
||||
|
||||
property string generalTooltipText: ""
|
||||
property bool enableOnStateLogic: false
|
||||
property bool showExecTooltip: true
|
||||
property bool _hasCustomTooltip: false
|
||||
|
||||
property string _currentIcon: "heart" // Default icon
|
||||
property bool _isHot: false
|
||||
property var _parsedStateChecks: [] // Local array for parsed state checks
|
||||
|
||||
property int _currentStateCheckIndex: -1
|
||||
property string _activeStateIcon: ""
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
|
||||
function _updatePropertiesFromSettings() {
|
||||
if (!widgetSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClickedCommand = widgetSettings.onClicked || "";
|
||||
onRightClickedCommand = widgetSettings.onRightClicked || "";
|
||||
onMiddleClickedCommand = widgetSettings.onMiddleClicked || "";
|
||||
stateChecksJson = widgetSettings.stateChecksJson || "[]"; // Populate from widgetSettings
|
||||
try {
|
||||
_parsedStateChecks = JSON.parse(stateChecksJson);
|
||||
} catch (e) {
|
||||
Logger.e("CustomButton", "Failed to parse stateChecksJson:", e.message);
|
||||
_parsedStateChecks = [];
|
||||
}
|
||||
generalTooltipText = widgetSettings.generalTooltipText || "";
|
||||
_hasCustomTooltip = !!(widgetSettings.generalTooltipText && widgetSettings.generalTooltipText.trim() !== "");
|
||||
enableOnStateLogic = widgetSettings.enableOnStateLogic || false;
|
||||
showExecTooltip = widgetSettings.showExecTooltip !== undefined ? widgetSettings.showExecTooltip : true;
|
||||
|
||||
updateState();
|
||||
}
|
||||
|
||||
function onWidgetSettingsChanged() {
|
||||
if (widgetSettings) {
|
||||
_updatePropertiesFromSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: stateCheckProcessExecutor
|
||||
running: false
|
||||
command: _currentStateCheckIndex !== -1 && _parsedStateChecks.length > _currentStateCheckIndex ? ["sh", "-lc", _parsedStateChecks[_currentStateCheckIndex].command] : []
|
||||
onExited: function (exitCode, stdout, stderr) {
|
||||
var currentCheckItem = _parsedStateChecks[_currentStateCheckIndex];
|
||||
var currentCommand = currentCheckItem.command;
|
||||
if (exitCode === 0) {
|
||||
// Command succeeded, this is the active state
|
||||
_isHot = true;
|
||||
_activeStateIcon = currentCheckItem.icon || widgetSettings.icon || "heart";
|
||||
} else {
|
||||
// Command failed, try next one
|
||||
_currentStateCheckIndex++;
|
||||
_checkNextState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: stateUpdateTimer
|
||||
interval: 200
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (enableOnStateLogic && _parsedStateChecks.length > 0) {
|
||||
_currentStateCheckIndex = 0;
|
||||
_checkNextState();
|
||||
} else {
|
||||
_isHot = false;
|
||||
_activeStateIcon = widgetSettings.icon || "heart";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _checkNextState() {
|
||||
if (_currentStateCheckIndex < _parsedStateChecks.length) {
|
||||
var currentCheckItem = _parsedStateChecks[_currentStateCheckIndex];
|
||||
if (currentCheckItem && currentCheckItem.command) {
|
||||
stateCheckProcessExecutor.running = true;
|
||||
} else {
|
||||
_currentStateCheckIndex++;
|
||||
_checkNextState();
|
||||
}
|
||||
} else {
|
||||
// All checks failed
|
||||
_isHot = false;
|
||||
_activeStateIcon = widgetSettings.icon || "heart";
|
||||
}
|
||||
}
|
||||
|
||||
function updateState() {
|
||||
if (!enableOnStateLogic || _parsedStateChecks.length === 0) {
|
||||
_isHot = false;
|
||||
_activeStateIcon = widgetSettings.icon || "heart";
|
||||
return;
|
||||
}
|
||||
stateUpdateTimer.restart();
|
||||
}
|
||||
|
||||
function _buildTooltipText() {
|
||||
// Build tooltip based on settings
|
||||
let tooltip = "";
|
||||
|
||||
// If user set custom text, always show it
|
||||
// If no custom text and showExecTooltip is off, show i18n default
|
||||
if (_hasCustomTooltip) {
|
||||
tooltip = generalTooltipText;
|
||||
} else if (!showExecTooltip) {
|
||||
tooltip = I18n.tr("bar.custom-button.default-tooltip");
|
||||
}
|
||||
|
||||
// Add command details if enabled
|
||||
if (showExecTooltip) {
|
||||
if (onClickedCommand) {
|
||||
tooltip += (tooltip ? "\n" : "") + I18n.tr("bar.custom-button.left-click-label") + `: ${onClickedCommand}`;
|
||||
}
|
||||
if (onRightClickedCommand) {
|
||||
tooltip += (tooltip ? "\n" : "") + I18n.tr("bar.custom-button.right-click-label") + `: ${onRightClickedCommand}`;
|
||||
}
|
||||
if (onMiddleClickedCommand) {
|
||||
tooltip += (tooltip ? "\n" : "") + I18n.tr("bar.custom-button.middle-click-label") + `: ${onMiddleClickedCommand}`;
|
||||
}
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
NIconButtonHot {
|
||||
id: button
|
||||
icon: _activeStateIcon
|
||||
hot: _isHot
|
||||
tooltipText: _buildTooltipText()
|
||||
onClicked: {
|
||||
if (onClickedCommand) {
|
||||
Quickshell.execDetached(["sh", "-lc", onClickedCommand]);
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
onRightClicked: {
|
||||
if (onRightClickedCommand) {
|
||||
Quickshell.execDetached(["sh", "-lc", onRightClickedCommand]);
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
onMiddleClicked: {
|
||||
if (onMiddleClickedCommand) {
|
||||
Quickshell.execDetached(["sh", "-lc", onMiddleClickedCommand]);
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Power
|
||||
import qs.Widgets
|
||||
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
|
||||
icon: "dark-mode"
|
||||
tooltipText: Settings.data.colorSchemes.darkMode ? I18n.tr("tooltips.switch-to-light-mode") : I18n.tr("tooltips.switch-to-dark-mode")
|
||||
onClicked: Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Power
|
||||
import qs.Widgets
|
||||
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
|
||||
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
|
||||
hot: IdleInhibitorService.isInhibited
|
||||
tooltipText: I18n.tr("tooltips.keep-awake")
|
||||
onClicked: IdleInhibitorService.manualToggle()
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Networking
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
icon: NetworkService.getIcon()
|
||||
tooltipText: NetworkService.getStatusText(true)
|
||||
onClicked: {
|
||||
var panel = PanelService.getPanel("networkPanel", screen);
|
||||
panel?.toggle(this);
|
||||
}
|
||||
onRightClicked: {
|
||||
if (!NetworkService.airplaneModeEnabled) {
|
||||
NetworkService.setWifiEnabled(!NetworkService.wifiEnabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
|
||||
enabled: ProgramCheckerService.wlsunsetAvailable
|
||||
icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
|
||||
hot: Settings.data.nightLight.enabled
|
||||
tooltipText: I18n.tr("common.night-light")
|
||||
|
||||
onClicked: {
|
||||
if (!Settings.data.nightLight.enabled) {
|
||||
Settings.data.nightLight.enabled = true;
|
||||
Settings.data.nightLight.forced = false;
|
||||
} else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) {
|
||||
Settings.data.nightLight.forced = true;
|
||||
} else {
|
||||
Settings.data.nightLight.enabled = false;
|
||||
Settings.data.nightLight.forced = false;
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Display;
|
||||
settingsPanel.open();
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Power
|
||||
import qs.Widgets
|
||||
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
|
||||
icon: PowerProfileService.noctaliaPerformanceMode ? "rocket" : "rocket-off"
|
||||
tooltipText: I18n.tr("tooltips.noctalia-performance-enabled")
|
||||
hot: PowerProfileService.noctaliaPerformanceMode
|
||||
onClicked: PowerProfileService.toggleNoctaliaPerformance()
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
|
||||
icon: NotificationService.doNotDisturb ? "bell-off" : "bell"
|
||||
hot: NotificationService.doNotDisturb
|
||||
tooltipText: I18n.tr("common.notifications")
|
||||
onClicked: PanelService.getPanel("notificationHistoryPanel", screen)?.toggle(this)
|
||||
onRightClicked: NotificationService.doNotDisturb = !NotificationService.doNotDisturb
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Commons
|
||||
import qs.Services.Power
|
||||
import qs.Widgets
|
||||
|
||||
// Performance
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
|
||||
readonly property bool hasPP: PowerProfileService.available
|
||||
|
||||
enabled: hasPP
|
||||
icon: PowerProfileService.getIcon()
|
||||
hot: !PowerProfileService.isDefault()
|
||||
tooltipText: I18n.tr("control-center.power-profile.tooltip-action")
|
||||
onClicked: PowerProfileService.cycleProfile()
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
|
||||
enabled: Settings.data.wallpaper.enabled
|
||||
icon: "wallpaper-selector"
|
||||
tooltipText: I18n.tr("wallpaper.panel.title")
|
||||
onClicked: PanelService.getPanel("wallpaperPanel", screen)?.toggle()
|
||||
onRightClicked: WallpaperService.setRandomWallpaper()
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.Dock
|
||||
import qs.Modules.MainScreen
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
panelBackgroundColor: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
|
||||
|
||||
readonly property string dockPosition: Settings.data.dock.position
|
||||
readonly property bool isVertical: dockPosition === "left" || dockPosition === "right"
|
||||
readonly property bool hasBar: modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
|
||||
readonly property bool barAtSameEdge: hasBar && Settings.getBarPositionForScreen(modelData?.name) === dockPosition
|
||||
readonly property bool isFramed: Settings.data.bar.barType === "framed" && hasBar
|
||||
property bool isDockHovered: false
|
||||
property bool panelHovered: false
|
||||
readonly property int iconSize: Math.round(12 + 24 * (Settings.data.dock.size ?? 1))
|
||||
readonly property int maxWidth: screen ? screen.width * 0.8 : 1000
|
||||
readonly property int maxHeight: screen ? screen.height * 0.8 : 1000
|
||||
readonly property bool autoHide: false
|
||||
readonly property int hideDelay: 1000
|
||||
readonly property int showDelay: 100
|
||||
|
||||
// Shared state with dock content
|
||||
property bool dockHovered: false
|
||||
property bool anyAppHovered: false
|
||||
property bool menuHovered: false
|
||||
property bool hidden: false
|
||||
property bool peekHovered: false
|
||||
|
||||
// Track the currently open context menu
|
||||
property var currentContextMenu: null
|
||||
|
||||
// Combined model of running apps and pinned apps
|
||||
property var dockApps: []
|
||||
property var groupCycleIndices: ({})
|
||||
|
||||
// Track the session order of apps (transient reordering)
|
||||
property var sessionAppOrder: []
|
||||
|
||||
// Drag and Drop state for visual feedback
|
||||
property int dragSourceIndex: -1
|
||||
property int dragTargetIndex: -1
|
||||
|
||||
// Revision counter to force icon re-evaluation
|
||||
property int iconRevision: 0
|
||||
|
||||
property alias hideTimer: hideTimer
|
||||
property alias showTimer: showTimer
|
||||
|
||||
onMenuHoveredChanged: {
|
||||
if (!menuHovered && !panelHovered) {
|
||||
hoverCloseTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onAnyAppHoveredChanged: {
|
||||
if (anyAppHovered) {
|
||||
hoverCloseTimer.stop();
|
||||
} else if (!panelHovered && !menuHovered && !dockHovered && !isDockHovered) {
|
||||
hoverCloseTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
hoverCloseTimer.stop();
|
||||
isDockHovered = false;
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
// Don't auto-start close timer here — the peek zone's onExited in Dock.qml
|
||||
// starts hideTimer which checks panel.isDockHovered, giving the user time
|
||||
// to move into the panel without it closing prematurely.
|
||||
}
|
||||
|
||||
panelAnchorTop: dockPosition === "top"
|
||||
panelAnchorBottom: dockPosition === "bottom"
|
||||
panelAnchorLeft: dockPosition === "left"
|
||||
panelAnchorRight: dockPosition === "right"
|
||||
panelAnchorHorizontalCenter: !isVertical
|
||||
panelAnchorVerticalCenter: isVertical
|
||||
|
||||
forceAttachToBar: hasBar
|
||||
exclusiveKeyboard: false
|
||||
|
||||
// when dragging ended but the cursor is outside the dock area, restart the timer
|
||||
onDragSourceIndexChanged: {
|
||||
if (dragSourceIndex === -1) {
|
||||
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) {
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to close any open context menu
|
||||
function closeAllContextMenus() {
|
||||
if (currentContextMenu && currentContextMenu.visible) {
|
||||
currentContextMenu.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function getAppKey(appData) {
|
||||
if (!appData)
|
||||
return null;
|
||||
|
||||
if (Settings.data.dock.groupApps) {
|
||||
return appData.appId;
|
||||
}
|
||||
|
||||
// Use stable appId for pinned apps to maintain their slot regardless of running state
|
||||
if (appData.type === "pinned" || appData.type === "pinned-running") {
|
||||
return appData.appId;
|
||||
}
|
||||
|
||||
// prefer toplevel object identity for unpinned running apps to distinguish instances
|
||||
if (appData.toplevel)
|
||||
return appData.toplevel;
|
||||
|
||||
// fallback to appId
|
||||
return appData.appId;
|
||||
}
|
||||
|
||||
function sortDockApps(apps) {
|
||||
if (!sessionAppOrder || sessionAppOrder.length === 0) {
|
||||
return apps;
|
||||
}
|
||||
|
||||
const sorted = [];
|
||||
const remaining = [...apps];
|
||||
|
||||
// Pick apps that are in the session order
|
||||
for (let i = 0; i < sessionAppOrder.length; i++) {
|
||||
const key = sessionAppOrder[i];
|
||||
|
||||
// Pick ALL matching apps (e.g. all instances of a pinned app)
|
||||
while (true) {
|
||||
const idx = remaining.findIndex(app => getAppKey(app) === key);
|
||||
if (idx !== -1) {
|
||||
sorted.push(remaining[idx]);
|
||||
remaining.splice(idx, 1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append any new/remaining apps
|
||||
remaining.forEach(app => sorted.push(app));
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
function reorderApps(fromIndex, toIndex) {
|
||||
if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= dockApps.length || toIndex >= dockApps.length)
|
||||
return;
|
||||
|
||||
const list = [...dockApps];
|
||||
const item = list.splice(fromIndex, 1)[0];
|
||||
list.splice(toIndex, 0, item);
|
||||
|
||||
dockApps = list;
|
||||
sessionAppOrder = dockApps.map(getAppKey);
|
||||
savePinnedOrder();
|
||||
}
|
||||
|
||||
function savePinnedOrder() {
|
||||
const currentPinned = Settings.data.dock.pinnedApps || [];
|
||||
const newPinned = [];
|
||||
const seen = new Set();
|
||||
|
||||
// Extract pinned apps in their current visual order
|
||||
dockApps.forEach(app => {
|
||||
if (app.appId && !seen.has(app.appId)) {
|
||||
const isPinned = currentPinned.some(p => normalizeAppId(p) === normalizeAppId(app.appId));
|
||||
|
||||
if (isPinned) {
|
||||
newPinned.push(app.appId);
|
||||
seen.add(app.appId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check if any pinned apps were missed (unlikely if dockApps is correct)
|
||||
currentPinned.forEach(p => {
|
||||
if (!seen.has(p)) {
|
||||
newPinned.push(p);
|
||||
seen.add(p);
|
||||
}
|
||||
});
|
||||
|
||||
if (JSON.stringify(currentPinned) !== JSON.stringify(newPinned)) {
|
||||
Settings.data.dock.pinnedApps = newPinned;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to normalize app IDs for case-insensitive matching
|
||||
function normalizeAppId(appId) {
|
||||
if (!appId || typeof appId !== 'string')
|
||||
return "";
|
||||
let id = appId.toLowerCase().trim();
|
||||
if (id.endsWith(".desktop"))
|
||||
id = id.substring(0, id.length - 8);
|
||||
return id;
|
||||
}
|
||||
|
||||
// Helper function to check if an app ID matches a pinned app (case-insensitive)
|
||||
function isAppIdPinned(appId, pinnedApps) {
|
||||
if (!appId || !pinnedApps || pinnedApps.length === 0)
|
||||
return false;
|
||||
const normalizedId = normalizeAppId(appId);
|
||||
// Direct match
|
||||
if (pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId))
|
||||
return true;
|
||||
// Resolve via desktop entry lookup (handles StartupWMClass != .desktop filename)
|
||||
const resolved = resolveToDesktopEntryId(appId);
|
||||
if (resolved !== appId) {
|
||||
const normalizedResolved = normalizeAppId(resolved);
|
||||
return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedResolved);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Desktop entry ID resolution cache (cleared when DesktopEntries change)
|
||||
property var _desktopEntryIdCache: ({})
|
||||
|
||||
// Resolve a toplevel appId to its canonical .desktop entry ID via heuristic lookup.
|
||||
function resolveToDesktopEntryId(appId) {
|
||||
if (!appId)
|
||||
return appId;
|
||||
if (_desktopEntryIdCache.hasOwnProperty(appId))
|
||||
return _desktopEntryIdCache[appId];
|
||||
try {
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
|
||||
const entry = DesktopEntries.heuristicLookup(appId);
|
||||
if (entry && entry.id) {
|
||||
_desktopEntryIdCache[appId] = entry.id;
|
||||
return entry.id;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
_desktopEntryIdCache[appId] = appId;
|
||||
return appId;
|
||||
}
|
||||
|
||||
// Helper function to get app name from desktop entry
|
||||
function getAppNameFromDesktopEntry(appId) {
|
||||
if (!appId)
|
||||
return appId;
|
||||
|
||||
try {
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
|
||||
const entry = DesktopEntries.heuristicLookup(appId);
|
||||
if (entry && entry.name) {
|
||||
return entry.name;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId) {
|
||||
const entry = DesktopEntries.byId(appId);
|
||||
if (entry && entry.name) {
|
||||
return entry.name;
|
||||
}
|
||||
}
|
||||
} catch (e)
|
||||
// Fall through to return original appId
|
||||
{}
|
||||
|
||||
// Return original appId if we can't find a desktop entry
|
||||
return appId;
|
||||
}
|
||||
|
||||
function getToplevelsForEntry(appData) {
|
||||
if (!appData)
|
||||
return [];
|
||||
|
||||
if (appData.toplevels && appData.toplevels.length > 0) {
|
||||
return appData.toplevels.filter(toplevel => toplevel && (!Settings.data.dock.onlySameOutput || !toplevel.screens || toplevel.screens.includes(screen)));
|
||||
}
|
||||
|
||||
if (!appData.toplevel)
|
||||
return [];
|
||||
|
||||
if (Settings.data.dock.onlySameOutput && appData.toplevel.screens && !appData.toplevel.screens.includes(screen))
|
||||
return [];
|
||||
|
||||
return [appData.toplevel];
|
||||
}
|
||||
|
||||
function getPrimaryToplevelForEntry(appData) {
|
||||
const toplevels = getToplevelsForEntry(appData);
|
||||
if (toplevels.length === 0)
|
||||
return null;
|
||||
|
||||
if (ToplevelManager && ToplevelManager.activeToplevel && toplevels.includes(ToplevelManager.activeToplevel))
|
||||
return ToplevelManager.activeToplevel;
|
||||
|
||||
return toplevels[0];
|
||||
}
|
||||
|
||||
// Build grouped render model without mutating the raw toplevel list.
|
||||
function buildGroupedDockApps(apps) {
|
||||
if (!Settings.data.dock.groupApps) {
|
||||
return apps.map(app => {
|
||||
const entry = Object.assign({}, app);
|
||||
entry.toplevels = getToplevelsForEntry(app);
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
|
||||
const grouped = [];
|
||||
const groupedById = new Map();
|
||||
|
||||
apps.forEach(app => {
|
||||
const appId = app.appId;
|
||||
const toplevels = getToplevelsForEntry(app);
|
||||
const existing = groupedById.get(appId);
|
||||
|
||||
if (existing) {
|
||||
toplevels.forEach(toplevel => {
|
||||
if (!existing.toplevels.includes(toplevel)) {
|
||||
existing.toplevels.push(toplevel);
|
||||
}
|
||||
});
|
||||
if (app.type === "pinned" || app.type === "pinned-running") {
|
||||
existing.isPinned = true;
|
||||
}
|
||||
} else {
|
||||
const entry = {
|
||||
"type": app.type,
|
||||
"appId": appId,
|
||||
"title": app.title,
|
||||
"toplevels": toplevels.slice(),
|
||||
"isPinned": app.type === "pinned" || app.type === "pinned-running"
|
||||
};
|
||||
grouped.push(entry);
|
||||
groupedById.set(appId, entry);
|
||||
}
|
||||
});
|
||||
|
||||
grouped.forEach(entry => {
|
||||
entry.toplevel = getPrimaryToplevelForEntry(entry);
|
||||
if (entry.toplevels.length > 0 && entry.isPinned) {
|
||||
entry.type = "pinned-running";
|
||||
} else if (entry.toplevels.length > 0) {
|
||||
entry.type = "running";
|
||||
} else {
|
||||
entry.type = "pinned";
|
||||
}
|
||||
if (entry.toplevel && entry.toplevel.title && entry.toplevel.title.trim() !== "") {
|
||||
entry.title = entry.toplevel.title;
|
||||
}
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
// Function to update the combined dock apps model
|
||||
function updateDockApps() {
|
||||
const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : [];
|
||||
const pinnedApps = Settings.data.dock.pinnedApps || [];
|
||||
const combined = [];
|
||||
const processedToplevels = new Set();
|
||||
const processedPinnedAppIds = new Set();
|
||||
|
||||
// push an app onto combined with the given appType
|
||||
function pushApp(appType, toplevel, appId, title) {
|
||||
// Use canonical ID for pinned apps to ensure key stability
|
||||
const canonicalId = isAppIdPinned(appId, pinnedApps) ? (pinnedApps.find(p => normalizeAppId(p) === normalizeAppId(appId)) || appId) : appId;
|
||||
|
||||
// For running apps, track by toplevel object to allow multiple instances
|
||||
if (toplevel) {
|
||||
if (processedToplevels.has(toplevel)) {
|
||||
return;
|
||||
}
|
||||
if (Settings.data.dock.onlySameOutput && toplevel.screens && !toplevel.screens.includes(screen)) {
|
||||
return;
|
||||
}
|
||||
combined.push({
|
||||
"type": appType,
|
||||
"toplevel": toplevel,
|
||||
"toplevels": toplevel ? [toplevel] : [],
|
||||
"appId": canonicalId,
|
||||
"title": title
|
||||
});
|
||||
processedToplevels.add(toplevel);
|
||||
} else {
|
||||
// For pinned apps that aren't running, track by appId to avoid duplicates
|
||||
if (processedPinnedAppIds.has(canonicalId)) {
|
||||
return;
|
||||
}
|
||||
combined.push({
|
||||
"type": appType,
|
||||
"toplevel": toplevel,
|
||||
"toplevels": [],
|
||||
"appId": canonicalId,
|
||||
"title": title
|
||||
});
|
||||
processedPinnedAppIds.add(canonicalId);
|
||||
}
|
||||
}
|
||||
|
||||
function pushRunning(first) {
|
||||
runningApps.forEach(toplevel => {
|
||||
if (toplevel) {
|
||||
// Use robust matching to check if pinned
|
||||
const isPinned = isAppIdPinned(toplevel.appId, pinnedApps);
|
||||
if (!first && isPinned && processedToplevels.has(toplevel)) {
|
||||
return; // Already added by pushPinned()
|
||||
}
|
||||
pushApp((first && isPinned) ? "pinned-running" : "running", toplevel, toplevel.appId, toplevel.title);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function pushPinned() {
|
||||
pinnedApps.forEach(pinnedAppId => {
|
||||
// Find all running instances of this pinned app using robust matching
|
||||
// Also resolve toplevel appId via desktop entry lookup to handle
|
||||
// StartupWMClass != .desktop filename (e.g. zen -> zen-browser-bin)
|
||||
const normalizedPinned = normalizeAppId(pinnedAppId);
|
||||
const matchingToplevels = runningApps.filter(app => {
|
||||
if (!app)
|
||||
return false;
|
||||
if (normalizeAppId(app.appId) === normalizedPinned)
|
||||
return true;
|
||||
const resolved = resolveToDesktopEntryId(app.appId);
|
||||
return resolved !== app.appId && normalizeAppId(resolved) === normalizedPinned;
|
||||
});
|
||||
|
||||
if (matchingToplevels.length > 0) {
|
||||
// Add all running instances as pinned-running
|
||||
matchingToplevels.forEach(toplevel => {
|
||||
pushApp("pinned-running", toplevel, pinnedAppId, toplevel.title);
|
||||
});
|
||||
} else {
|
||||
// App is pinned but not running - add once
|
||||
pushApp("pinned", null, pinnedAppId, getAppNameFromDesktopEntry(pinnedAppId) || pinnedAppId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// if pinnedStatic then push all pinned and then all remaining running apps
|
||||
if (Settings.data.dock.pinnedStatic) {
|
||||
pushPinned();
|
||||
pushRunning(false);
|
||||
|
||||
// else add all running apps and then remaining pinned apps
|
||||
} else {
|
||||
pushRunning(true);
|
||||
pushPinned();
|
||||
}
|
||||
|
||||
const sortedApps = sortDockApps(combined);
|
||||
dockApps = buildGroupedDockApps(sortedApps);
|
||||
const cycleState = root.groupCycleIndices || {};
|
||||
const nextCycleState = {};
|
||||
dockApps.forEach(app => {
|
||||
if (app && app.appId && cycleState[app.appId] !== undefined) {
|
||||
nextCycleState[app.appId] = cycleState[app.appId];
|
||||
}
|
||||
});
|
||||
root.groupCycleIndices = nextCycleState;
|
||||
|
||||
// Sync session order if needed
|
||||
if (!sessionAppOrder || sessionAppOrder.length === 0) {
|
||||
sessionAppOrder = dockApps.map(getAppKey);
|
||||
} else {
|
||||
const currentKeys = new Set(dockApps.map(getAppKey));
|
||||
const existingKeys = new Set();
|
||||
const newOrder = [];
|
||||
|
||||
// Keep existing keys that are still present
|
||||
sessionAppOrder.forEach(key => {
|
||||
if (currentKeys.has(key)) {
|
||||
newOrder.push(key);
|
||||
existingKeys.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
// Add new keys at the end
|
||||
dockApps.forEach(app => {
|
||||
const key = getAppKey(app);
|
||||
if (!existingKeys.has(key)) {
|
||||
newOrder.push(key);
|
||||
existingKeys.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
if (JSON.stringify(newOrder) !== JSON.stringify(sessionAppOrder)) {
|
||||
sessionAppOrder = newOrder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update dock apps when toplevels change
|
||||
Connections {
|
||||
target: ToplevelManager ? ToplevelManager.toplevels : null
|
||||
function onValuesChanged() {
|
||||
updateDockApps();
|
||||
}
|
||||
}
|
||||
|
||||
// Update dock apps when pinned apps change
|
||||
Connections {
|
||||
target: Settings.data.dock
|
||||
function onPinnedAppsChanged() {
|
||||
updateDockApps();
|
||||
}
|
||||
function onOnlySameOutputChanged() {
|
||||
updateDockApps();
|
||||
}
|
||||
function onGroupAppsChanged() {
|
||||
updateDockApps();
|
||||
}
|
||||
}
|
||||
|
||||
// Initial update when component is ready
|
||||
Component.onCompleted: {
|
||||
if (ToplevelManager) {
|
||||
updateDockApps();
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh icons when DesktopEntries becomes available
|
||||
Connections {
|
||||
target: DesktopEntries.applications
|
||||
function onValuesChanged() {
|
||||
root.iconRevision++;
|
||||
root._desktopEntryIdCache = {};
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: hideDelay
|
||||
onTriggered: {}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: showDelay
|
||||
onTriggered: {}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hoverCloseTimer
|
||||
interval: hideDelay
|
||||
onTriggered: {
|
||||
if (root.dockHovered || root.isDockHovered || root.anyAppHovered || root.menuHovered || (root.currentContextMenu && root.currentContextMenu.visible)) {
|
||||
restart();
|
||||
return;
|
||||
}
|
||||
root.isDockHovered = false;
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
panelContent: Item {
|
||||
id: panelContent
|
||||
|
||||
property bool allowAttach: true
|
||||
property real frameThickness: isFramed && !barAtSameEdge && !Settings.data.dock.sitOnFrame ? Settings.data.bar.frameThickness : 0
|
||||
property real contentPreferredWidth: Math.round(dockContainerWrapper.width) - (isVertical ? frameThickness : 0)
|
||||
property real contentPreferredHeight: Math.round(dockContainerWrapper.height) - (!isVertical ? frameThickness : 0)
|
||||
|
||||
Item {
|
||||
id: hoverArea
|
||||
anchors.fill: dockContainerWrapper
|
||||
anchors.margins: -frameThickness
|
||||
|
||||
// Detect hover over dock area including frame thickness
|
||||
HoverHandler {
|
||||
id: dockHoverArea
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onHoveredChanged: {
|
||||
root.panelHovered = hovered;
|
||||
if (hovered) {
|
||||
root.isDockHovered = true;
|
||||
hoverCloseTimer.stop();
|
||||
} else {
|
||||
root.isDockHovered = false;
|
||||
if (root.menuHovered || (root.currentContextMenu && root.currentContextMenu.visible)) {
|
||||
hoverCloseTimer.stop();
|
||||
} else {
|
||||
hoverCloseTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dockContainerWrapper
|
||||
readonly property real frameThickness: isFramed ? Settings.data.bar.frameThickness : 0
|
||||
width: dockContent.dockContainer.width
|
||||
height: dockContent.dockContainer.height
|
||||
anchors.top: root.dockPosition === "bottom" ? parent.top : undefined
|
||||
anchors.bottom: root.dockPosition === "top" ? parent.bottom : undefined
|
||||
anchors.left: root.dockPosition === "right" ? parent.left : undefined
|
||||
anchors.right: root.dockPosition === "left" ? parent.right : undefined
|
||||
|
||||
DockContent {
|
||||
id: dockContent
|
||||
anchors.fill: parent
|
||||
dockRoot: root
|
||||
extraTop: 0
|
||||
extraBottom: 0
|
||||
extraLeft: 0
|
||||
extraRight: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Keyboard
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: previewPanel
|
||||
|
||||
property var currentItem: null
|
||||
property string fullContent: ""
|
||||
property string imageDataUrl: ""
|
||||
property bool loadingFullContent: false
|
||||
property bool isImageContent: false
|
||||
|
||||
implicitHeight: contentArea.implicitHeight + Style.margin2L
|
||||
|
||||
function loadContent() {
|
||||
if (!currentItem || !currentItem.clipboardId)
|
||||
return;
|
||||
|
||||
if (isImageContent) {
|
||||
// For images, check cache first then decode
|
||||
imageDataUrl = ClipboardService.getImageData(currentItem.clipboardId) || "";
|
||||
loadingFullContent = !imageDataUrl;
|
||||
if (!imageDataUrl && currentItem.mime) {
|
||||
ClipboardService.decodeToDataUrl(currentItem.clipboardId, currentItem.mime, null);
|
||||
}
|
||||
} else {
|
||||
// For text, check sync cache first
|
||||
const cached = ClipboardService.getContent(currentItem.clipboardId);
|
||||
if (cached) {
|
||||
fullContent = cached;
|
||||
loadingFullContent = false;
|
||||
} else {
|
||||
// Show preview while loading full content
|
||||
fullContent = currentItem.preview || "";
|
||||
loadingFullContent = true;
|
||||
// Async decode as fallback
|
||||
var requestedId = currentItem.clipboardId;
|
||||
ClipboardService.decode(requestedId, function (content) {
|
||||
if (!previewPanel || !previewPanel.currentItem) {
|
||||
return;
|
||||
}
|
||||
if (previewPanel.currentItem.clipboardId === requestedId) {
|
||||
var trimmed = content ? content.trim() : "";
|
||||
if (trimmed !== "") {
|
||||
previewPanel.fullContent = trimmed;
|
||||
}
|
||||
previewPanel.loadingFullContent = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: previewPanel
|
||||
function onCurrentItemChanged() {
|
||||
fullContent = "";
|
||||
imageDataUrl = "";
|
||||
loadingFullContent = false;
|
||||
isImageContent = currentItem && currentItem.isImage;
|
||||
|
||||
if (currentItem && currentItem.clipboardId) {
|
||||
loadContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property int _rev: ClipboardService.revision
|
||||
on_RevChanged: {
|
||||
// When cache updates, try to load content if we're still showing loading or preview
|
||||
if (currentItem && currentItem.clipboardId && !isImageContent && loadingFullContent) {
|
||||
const cached = ClipboardService.getContent(currentItem.clipboardId);
|
||||
if (cached) {
|
||||
fullContent = cached;
|
||||
loadingFullContent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: imageUpdateTimer
|
||||
interval: 200
|
||||
running: currentItem && currentItem.isImage && imageDataUrl === ""
|
||||
repeat: currentItem && currentItem.isImage && imageDataUrl === ""
|
||||
|
||||
onTriggered: {
|
||||
if (currentItem && currentItem.clipboardId) {
|
||||
const newData = ClipboardService.getImageData(currentItem.clipboardId) || "";
|
||||
if (newData !== imageDataUrl) {
|
||||
imageDataUrl = newData;
|
||||
if (newData) {
|
||||
loadingFullContent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentArea
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: loadingFullContent
|
||||
visible: loadingFullContent
|
||||
width: Style.baseWidgetSize
|
||||
height: width
|
||||
}
|
||||
|
||||
// Preview for image entry
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
spacing: Style.marginS
|
||||
visible: isImageContent && !loadingFullContent && imageDataUrl !== ""
|
||||
|
||||
NImageRounded {
|
||||
id: previewImage
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Style.marginS
|
||||
imagePath: imageDataUrl
|
||||
imageFillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginS
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
const meta = ClipboardService.parseImageMeta(currentItem?.preview);
|
||||
if (meta)
|
||||
return `${meta.fmt} • ${meta.w}×${meta.h} • ${meta.size}`;
|
||||
// Fallback to basic info
|
||||
const format = (currentItem?.mime || "image").split("/")[1]?.toUpperCase() || "Image";
|
||||
return `${format} • ${previewImage.implicitWidth}×${previewImage.implicitHeight}`;
|
||||
}
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
// Preview for text entry
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
spacing: Style.marginS
|
||||
visible: !isImageContent && !loadingFullContent
|
||||
|
||||
NScrollView {
|
||||
id: clipboardScrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
horizontalPolicy: Settings.data.appLauncher.clipboardWrapText ? ScrollBar.AlwaysOff : ScrollBar.AsNeeded
|
||||
|
||||
NText {
|
||||
text: fullContent
|
||||
width: Settings.data.appLauncher.clipboardWrapText ? clipboardScrollView.availableWidth : implicitWidth
|
||||
wrapMode: Settings.data.appLauncher.clipboardWrapText ? Text.Wrap : Text.NoWrap
|
||||
textFormat: Text.PlainText
|
||||
font.pointSize: Style.fontSizeM
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginS
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.fillWidth: true
|
||||
visible: fullContent.length > 0
|
||||
text: {
|
||||
const chars = fullContent.length;
|
||||
const words = fullContent.split(/\s+/).filter(w => w.length > 0).length;
|
||||
const lines = fullContent.split('\n').length;
|
||||
return `${chars} chars, ${words} words, ${lines} lines`;
|
||||
}
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
function selectNext(selectedIndex, resultsLength) {
|
||||
if (resultsLength > 0 && selectedIndex < resultsLength - 1)
|
||||
return selectedIndex + 1;
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
function selectPrevious(selectedIndex, resultsLength) {
|
||||
if (resultsLength > 0 && selectedIndex > 0)
|
||||
return selectedIndex - 1;
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
function selectNextWrapped(selectedIndex, resultsLength, allowWrap) {
|
||||
if (resultsLength > 0) {
|
||||
if (allowWrap)
|
||||
return (selectedIndex + 1) % resultsLength;
|
||||
return selectNext(selectedIndex, resultsLength);
|
||||
}
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
function selectPreviousWrapped(selectedIndex, resultsLength, allowWrap) {
|
||||
if (resultsLength > 0) {
|
||||
if (allowWrap)
|
||||
return (((selectedIndex - 1) % resultsLength) + resultsLength) % resultsLength;
|
||||
return selectPrevious(selectedIndex, resultsLength);
|
||||
}
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
function selectFirst() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function selectLast(resultsLength) {
|
||||
return resultsLength > 0 ? resultsLength - 1 : 0;
|
||||
}
|
||||
|
||||
function selectNextPage(selectedIndex, resultsLength, entryHeight) {
|
||||
if (resultsLength > 0) {
|
||||
var page = Math.max(1, Math.floor(600 / entryHeight));
|
||||
return Math.min(selectedIndex + page, resultsLength - 1);
|
||||
}
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
function selectPreviousPage(selectedIndex, resultsLength, entryHeight) {
|
||||
if (resultsLength > 0) {
|
||||
var page = Math.max(1, Math.floor(600 / entryHeight));
|
||||
return Math.max(selectedIndex - page, 0);
|
||||
}
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
function selectPreviousRow(selectedIndex, resultsLength, gridColumns) {
|
||||
if (resultsLength <= 0 || gridColumns <= 0)
|
||||
return selectedIndex;
|
||||
|
||||
var currentRow = Math.floor(selectedIndex / gridColumns);
|
||||
var currentCol = selectedIndex % gridColumns;
|
||||
|
||||
if (currentRow > 0) {
|
||||
var targetRow = currentRow - 1;
|
||||
var itemsInTargetRow = Math.min(gridColumns, resultsLength - targetRow * gridColumns);
|
||||
if (currentCol < itemsInTargetRow)
|
||||
return targetRow * gridColumns + currentCol;
|
||||
return targetRow * gridColumns + itemsInTargetRow - 1;
|
||||
}
|
||||
|
||||
// Wrap to last row, same column
|
||||
var totalRows = Math.ceil(resultsLength / gridColumns);
|
||||
var lastRow = totalRows - 1;
|
||||
var itemsInLastRow = Math.min(gridColumns, resultsLength - lastRow * gridColumns);
|
||||
if (currentCol < itemsInLastRow)
|
||||
return lastRow * gridColumns + currentCol;
|
||||
return resultsLength - 1;
|
||||
}
|
||||
|
||||
function selectNextRow(selectedIndex, resultsLength, gridColumns) {
|
||||
if (resultsLength <= 0 || gridColumns <= 0)
|
||||
return selectedIndex;
|
||||
|
||||
var currentRow = Math.floor(selectedIndex / gridColumns);
|
||||
var currentCol = selectedIndex % gridColumns;
|
||||
var totalRows = Math.ceil(resultsLength / gridColumns);
|
||||
|
||||
if (currentRow < totalRows - 1) {
|
||||
var targetRow = currentRow + 1;
|
||||
var targetIndex = targetRow * gridColumns + currentCol;
|
||||
if (targetIndex < resultsLength)
|
||||
return targetIndex;
|
||||
var itemsInTargetRow = resultsLength - targetRow * gridColumns;
|
||||
if (itemsInTargetRow > 0)
|
||||
return targetRow * gridColumns + itemsInTargetRow - 1;
|
||||
return Math.min(currentCol, resultsLength - 1);
|
||||
}
|
||||
|
||||
// Wrap to first row, same column
|
||||
return Math.min(currentCol, resultsLength - 1);
|
||||
}
|
||||
|
||||
function selectPreviousColumn(selectedIndex, resultsLength, gridColumns) {
|
||||
if (resultsLength <= 0)
|
||||
return selectedIndex;
|
||||
|
||||
var currentRow = Math.floor(selectedIndex / gridColumns);
|
||||
var currentCol = selectedIndex % gridColumns;
|
||||
|
||||
if (currentCol > 0)
|
||||
return currentRow * gridColumns + (currentCol - 1);
|
||||
if (currentRow > 0)
|
||||
return (currentRow - 1) * gridColumns + (gridColumns - 1);
|
||||
|
||||
var totalRows = Math.ceil(resultsLength / gridColumns);
|
||||
var lastRowIndex = (totalRows - 1) * gridColumns + (gridColumns - 1);
|
||||
return Math.min(lastRowIndex, resultsLength - 1);
|
||||
}
|
||||
|
||||
function selectNextColumn(selectedIndex, resultsLength, gridColumns) {
|
||||
if (resultsLength <= 0)
|
||||
return selectedIndex;
|
||||
|
||||
var currentRow = Math.floor(selectedIndex / gridColumns);
|
||||
var currentCol = selectedIndex % gridColumns;
|
||||
var itemsInCurrentRow = Math.min(gridColumns, resultsLength - currentRow * gridColumns);
|
||||
|
||||
if (currentCol < itemsInCurrentRow - 1)
|
||||
return currentRow * gridColumns + (currentCol + 1);
|
||||
|
||||
var totalRows = Math.ceil(resultsLength / gridColumns);
|
||||
if (currentRow < totalRows - 1)
|
||||
return (currentRow + 1) * gridColumns;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
// Disable when overlay mode is enabled (LauncherOverlayWindow handles it)
|
||||
enabled: !Settings.data.appLauncher.overviewLayer
|
||||
visible: !Settings.data.appLauncher.overviewLayer
|
||||
|
||||
// Reference to core (set after panelContent loads)
|
||||
property var launcherCoreRef: null
|
||||
|
||||
// Expose core launcher for external access (e.g., IPC)
|
||||
readonly property string searchText: launcherCoreRef ? launcherCoreRef.searchText : ""
|
||||
readonly property int selectedIndex: launcherCoreRef ? launcherCoreRef.selectedIndex : 0
|
||||
readonly property var results: launcherCoreRef ? launcherCoreRef.results : []
|
||||
readonly property var activeProvider: launcherCoreRef ? launcherCoreRef.activeProvider : null
|
||||
readonly property var currentProvider: launcherCoreRef ? launcherCoreRef.currentProvider : null
|
||||
readonly property bool isGridView: launcherCoreRef ? launcherCoreRef.isGridView : false
|
||||
readonly property int gridColumns: launcherCoreRef ? launcherCoreRef.gridColumns : 5
|
||||
|
||||
function setSearchText(text) {
|
||||
if (launcherCoreRef)
|
||||
launcherCoreRef.setSearchText(text);
|
||||
}
|
||||
|
||||
// Preview panel support
|
||||
readonly property bool previewActive: {
|
||||
if (!launcherCoreRef)
|
||||
return false;
|
||||
var provider = launcherCoreRef.activeProvider;
|
||||
if (!provider || !provider.hasPreview)
|
||||
return false;
|
||||
if (!Settings.data.appLauncher.enableClipPreview)
|
||||
return false;
|
||||
return selectedIndex >= 0 && results && !!results[selectedIndex];
|
||||
}
|
||||
readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio)
|
||||
|
||||
// Panel sizing
|
||||
readonly property int listPanelWidth: Math.round(500 * Style.uiScaleRatio)
|
||||
readonly property int totalBaseWidth: listPanelWidth + Style.margin2L
|
||||
|
||||
preferredWidth: totalBaseWidth
|
||||
preferredHeight: Math.round(600 * Style.uiScaleRatio)
|
||||
preferredWidthRatio: 0.25
|
||||
preferredHeightRatio: 0.5
|
||||
|
||||
// Positioning
|
||||
readonly property string screenBarPosition: Settings.getBarPositionForScreen(screen?.name)
|
||||
readonly property string panelPosition: {
|
||||
if (Settings.data.appLauncher.position === "follow_bar") {
|
||||
if (screenBarPosition === "left" || screenBarPosition === "right") {
|
||||
return `center_${screenBarPosition}`;
|
||||
} else {
|
||||
return `${screenBarPosition}_center`;
|
||||
}
|
||||
} else {
|
||||
return Settings.data.appLauncher.position;
|
||||
}
|
||||
}
|
||||
panelAnchorHorizontalCenter: !root.useButtonPosition && (panelPosition === "center" || panelPosition.endsWith("_center"))
|
||||
panelAnchorVerticalCenter: !root.useButtonPosition && panelPosition === "center"
|
||||
panelAnchorLeft: !root.useButtonPosition && panelPosition !== "center" && panelPosition.endsWith("_left")
|
||||
panelAnchorRight: !root.useButtonPosition && panelPosition !== "center" && panelPosition.endsWith("_right")
|
||||
panelAnchorBottom: !root.useButtonPosition && panelPosition.startsWith("bottom_")
|
||||
panelAnchorTop: !root.useButtonPosition && panelPosition.startsWith("top_")
|
||||
|
||||
panelContent: Rectangle {
|
||||
id: ui
|
||||
color: "transparent"
|
||||
opacity: launcherCore.resultsReady ? 1.0 : 0.0
|
||||
|
||||
Component.onCompleted: root.launcherCoreRef = launcherCore
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCirc
|
||||
}
|
||||
}
|
||||
|
||||
// Preview Panel (external) - uses provider's preview component
|
||||
NDropShadow {
|
||||
source: previewBox
|
||||
anchors.fill: previewBox
|
||||
autoPaddingEnabled: true
|
||||
visible: previewBox.visible
|
||||
z: previewBox.z - 1
|
||||
}
|
||||
|
||||
NBox {
|
||||
id: previewBox
|
||||
visible: root.previewActive
|
||||
width: root.previewPanelWidth
|
||||
height: Math.round(400 * Style.uiScaleRatio)
|
||||
forceOpaque: true // no blur for now
|
||||
x: root.panelAnchorRight ? -(root.previewPanelWidth + Style.marginM) : ui.width + Style.marginM
|
||||
y: {
|
||||
var view = launcherCore.resultsView;
|
||||
if (!view)
|
||||
return Style.marginL;
|
||||
var row = launcherCore.isGridView ? Math.floor(launcherCore.selectedIndex / launcherCore.gridColumns) : launcherCore.selectedIndex;
|
||||
var gridCellSize = Math.floor((root.listPanelWidth - (2 * Style.marginXS) - ((launcherCore.targetGridColumns - 1) * Style.marginS)) / launcherCore.targetGridColumns);
|
||||
var itemHeight = launcherCore.isGridView ? (gridCellSize + Style.marginXXS) : (launcherCore.entryHeight + (view.spacing || 0));
|
||||
var yPos = row * itemHeight - (view.contentY || 0);
|
||||
var mapped = view.mapToItem(ui, 0, yPos);
|
||||
return Math.max(Style.marginL, Math.min(mapped.y, ui.height - previewBox.height - Style.marginL));
|
||||
}
|
||||
z: -1
|
||||
|
||||
opacity: visible ? 1.0 : 0.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: previewLoader
|
||||
anchors.fill: parent
|
||||
active: root.previewActive
|
||||
source: {
|
||||
if (!active)
|
||||
return "";
|
||||
var provider = launcherCore.activeProvider;
|
||||
if (provider && provider.previewComponentPath)
|
||||
return provider.previewComponentPath;
|
||||
return "";
|
||||
}
|
||||
|
||||
onLoaded: updatePreviewItem()
|
||||
onItemChanged: updatePreviewItem()
|
||||
|
||||
function updatePreviewItem() {
|
||||
if (!item || launcherCore.selectedIndex < 0 || !launcherCore.results[launcherCore.selectedIndex])
|
||||
return;
|
||||
var provider = launcherCore.activeProvider;
|
||||
if (provider && provider.getPreviewData) {
|
||||
item.currentItem = provider.getPreviewData(launcherCore.results[launcherCore.selectedIndex]);
|
||||
} else {
|
||||
item.currentItem = launcherCore.results[launcherCore.selectedIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Core launcher (state, providers, UI)
|
||||
LauncherCore {
|
||||
id: launcherCore
|
||||
anchors.fill: parent
|
||||
screen: root.screen
|
||||
isOpen: root.isPanelOpen
|
||||
onRequestClose: root.close()
|
||||
// Defer so the signal emission completes before SmartPanel
|
||||
// sets isPanelOpen=false and the contentLoader destroys us.
|
||||
onRequestCloseImmediately: Qt.callLater(root.closeImmediately)
|
||||
}
|
||||
|
||||
// Update preview when selection changes
|
||||
Connections {
|
||||
target: launcherCore
|
||||
function onSelectedIndexChanged() {
|
||||
if (previewLoader.item)
|
||||
previewLoader.updatePreviewItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,909 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import "Helpers/LauncherNavigation.js" as LauncherNav
|
||||
|
||||
import "Providers"
|
||||
import qs.Commons
|
||||
import qs.Services.Keyboard
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
// Core launcher logic and UI - shared between SmartPanel (Launcher.qml) and overlay (LauncherOverlayWindow.qml)
|
||||
Rectangle {
|
||||
id: root
|
||||
color: "transparent"
|
||||
|
||||
// External interface - set by parent
|
||||
property var screen: null
|
||||
property bool isOpen: false
|
||||
signal requestClose
|
||||
signal requestCloseImmediately
|
||||
|
||||
function closeImmediately() {
|
||||
requestCloseImmediately();
|
||||
}
|
||||
|
||||
// Expose for preview panel positioning
|
||||
readonly property var resultsView: resultsSwapView.item
|
||||
|
||||
// State
|
||||
property string searchText: ""
|
||||
property int selectedIndex: 0
|
||||
property var results: []
|
||||
property var providers: []
|
||||
property var activeProvider: null
|
||||
property bool resultsReady: false
|
||||
property var pluginProviderInstances: ({})
|
||||
property bool ignoreMouseHover: true // Transient flag, should always be true on init
|
||||
|
||||
// Global mouse tracking for movement detection across delegates
|
||||
property real globalLastMouseX: 0
|
||||
property real globalLastMouseY: 0
|
||||
property bool globalMouseInitialized: false
|
||||
property bool mouseTrackingReady: false // Delay tracking until panel is settled
|
||||
|
||||
readonly property bool animationsDisabled: Settings.data.general.animationDisabled
|
||||
|
||||
Timer {
|
||||
id: mouseTrackingDelayTimer
|
||||
interval: root.animationsDisabled ? 0 : (Style.animationNormal + 50) // Wait for panel animation to complete + safety margin
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.mouseTrackingReady = true;
|
||||
root.globalMouseInitialized = false; // Reset so we get fresh initial position
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var defaultProvider: appsProvider
|
||||
readonly property var currentProvider: activeProvider || defaultProvider
|
||||
|
||||
readonly property string launcherDensity: (currentProvider && currentProvider.ignoreDensity === false) ? (Settings.data.appLauncher.density || "default") : "comfortable"
|
||||
readonly property int effectiveIconSize: launcherDensity === "comfortable" ? 48 : (launcherDensity === "default" ? 36 : 24)
|
||||
readonly property int badgeSize: Math.round(effectiveIconSize * Style.uiScaleRatio)
|
||||
readonly property int entryHeight: Math.round(badgeSize + (launcherDensity === "compact" ? (Style.marginL + Style.marginXXS) : (Style.marginXL + Style.marginS)))
|
||||
|
||||
readonly property bool providerShowsCategories: (currentProvider.showsCategories !== undefined ? currentProvider.showsCategories : true) && providerCategories.length > 0
|
||||
|
||||
readonly property var providerCategories: {
|
||||
if (currentProvider.availableCategories && currentProvider.availableCategories.length > 0) {
|
||||
return currentProvider.availableCategories;
|
||||
}
|
||||
return currentProvider.categories || [];
|
||||
}
|
||||
|
||||
readonly property bool showProviderCategories: {
|
||||
if (!providerShowsCategories || providerCategories.length === 0)
|
||||
return false;
|
||||
if (currentProvider === defaultProvider)
|
||||
return Settings.data.appLauncher.showCategories;
|
||||
return true;
|
||||
}
|
||||
|
||||
readonly property bool providerHasDisplayString: results.length > 0 && !!results[0].displayString
|
||||
|
||||
readonly property string providerSupportedLayouts: {
|
||||
if (activeProvider && activeProvider.supportedLayouts)
|
||||
return activeProvider.supportedLayouts;
|
||||
if (results.length > 0 && results[0].provider && results[0].provider.supportedLayouts)
|
||||
return results[0].provider.supportedLayouts;
|
||||
if (defaultProvider && defaultProvider.supportedLayouts)
|
||||
return defaultProvider.supportedLayouts;
|
||||
return "both";
|
||||
}
|
||||
|
||||
readonly property bool showLayoutToggle: !providerHasDisplayString && providerSupportedLayouts === "both"
|
||||
|
||||
readonly property string layoutMode: {
|
||||
if (searchText === ">")
|
||||
return "list";
|
||||
if (providerSupportedLayouts === "grid")
|
||||
return "grid";
|
||||
if (providerSupportedLayouts === "list")
|
||||
return "list";
|
||||
if (providerSupportedLayouts === "single")
|
||||
return "single";
|
||||
if (providerHasDisplayString)
|
||||
return "grid";
|
||||
return Settings.data.appLauncher.viewMode;
|
||||
}
|
||||
|
||||
readonly property bool isGridView: layoutMode === "grid"
|
||||
readonly property bool isSingleView: layoutMode === "single"
|
||||
readonly property bool isCompactDensity: launcherDensity === "compact"
|
||||
|
||||
readonly property int targetGridColumns: {
|
||||
let base = 5;
|
||||
if (launcherDensity === "comfortable")
|
||||
base = 4;
|
||||
else if (launcherDensity === "compact")
|
||||
base = 6;
|
||||
|
||||
if (!activeProvider || activeProvider === defaultProvider)
|
||||
return base;
|
||||
|
||||
if (activeProvider.preferredGridColumns) {
|
||||
let multiplier = base / 5.0;
|
||||
return Math.max(1, Math.round(activeProvider.preferredGridColumns * multiplier));
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
readonly property int listPanelWidth: Math.round(500 * Style.uiScaleRatio)
|
||||
readonly property int gridContentWidth: listPanelWidth - (2 * Style.marginXS)
|
||||
readonly property int gridCellSize: Math.floor((gridContentWidth - ((targetGridColumns - 1) * Style.marginS)) / targetGridColumns)
|
||||
|
||||
readonly property int gridColumns: targetGridColumns
|
||||
|
||||
// Check if current provider allows wrap navigation (default true)
|
||||
readonly property bool allowWrapNavigation: {
|
||||
var provider = activeProvider || currentProvider;
|
||||
return provider && provider.wrapNavigation !== undefined ? provider.wrapNavigation : true;
|
||||
}
|
||||
|
||||
// Listen for plugin provider registry changes
|
||||
Connections {
|
||||
target: LauncherProviderRegistry
|
||||
function onPluginProviderRegistryUpdated() {
|
||||
root.syncPluginProviders();
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onIsOpenChanged: {
|
||||
if (isOpen) {
|
||||
onOpened();
|
||||
} else {
|
||||
onClosed();
|
||||
}
|
||||
}
|
||||
|
||||
onSearchTextChanged: {
|
||||
if (isOpen) {
|
||||
updateResults();
|
||||
}
|
||||
}
|
||||
|
||||
function onOpened() {
|
||||
ignoreMouseHover = true;
|
||||
globalMouseInitialized = false;
|
||||
mouseTrackingReady = false;
|
||||
mouseTrackingDelayTimer.restart();
|
||||
|
||||
// Show launcher immediately, results will populate asynchronously
|
||||
resultsReady = true;
|
||||
focusSearchInput();
|
||||
|
||||
Qt.callLater(() => {
|
||||
syncPluginProviders();
|
||||
for (let provider of providers) {
|
||||
if (provider.onOpened)
|
||||
provider.onOpened();
|
||||
}
|
||||
updateResults();
|
||||
});
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
searchText = "";
|
||||
ignoreMouseHover = true;
|
||||
if (resultsSwapView)
|
||||
resultsSwapView.resetVisuals();
|
||||
for (let provider of providers) {
|
||||
if (provider.onClosed)
|
||||
provider.onClosed();
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
requestClose();
|
||||
}
|
||||
|
||||
function applyCategorySelection(tabIndex, categories) {
|
||||
const categoryList = categories || providerCategories;
|
||||
if (!categoryList || tabIndex < 0 || tabIndex >= categoryList.length)
|
||||
return false;
|
||||
|
||||
currentProvider.selectCategory(categoryList[tabIndex]);
|
||||
categoryTabs.currentIndex = tabIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectCategoryWithSlide(tabIndex) {
|
||||
if (!showProviderCategories || !currentProvider || !currentProvider.selectCategory)
|
||||
return;
|
||||
|
||||
const cats = providerCategories;
|
||||
if (!cats || tabIndex < 0 || tabIndex >= cats.length)
|
||||
return;
|
||||
|
||||
const currentIdx = cats.indexOf(currentProvider.selectedCategory);
|
||||
if (tabIndex === currentIdx)
|
||||
return;
|
||||
|
||||
const canAnimate = !animationsDisabled && resultsSwapView.width > 0 && resultsSwapView.height > 0;
|
||||
if (!canAnimate) {
|
||||
applyCategorySelection(tabIndex, cats);
|
||||
return;
|
||||
}
|
||||
|
||||
const direction = tabIndex > currentIdx ? 1 : -1;
|
||||
resultsSwapView.swap(direction, () => applyCategorySelection(tabIndex, providerCategories));
|
||||
}
|
||||
|
||||
// Public API
|
||||
function setSearchText(text) {
|
||||
searchText = text;
|
||||
}
|
||||
|
||||
function focusSearchInput() {
|
||||
if (searchInput.inputItem) {
|
||||
searchInput.inputItem.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
// Provider registration
|
||||
function registerProvider(provider) {
|
||||
providers.push(provider);
|
||||
provider.launcher = root;
|
||||
if (provider.init)
|
||||
provider.init();
|
||||
}
|
||||
|
||||
function syncPluginProviders() {
|
||||
var registeredIds = LauncherProviderRegistry.getPluginProviders();
|
||||
var changed = false;
|
||||
|
||||
// Remove providers that are no longer registered
|
||||
for (var existingId in pluginProviderInstances) {
|
||||
if (registeredIds.indexOf(existingId) === -1) {
|
||||
var idx = providers.indexOf(pluginProviderInstances[existingId]);
|
||||
if (idx >= 0)
|
||||
providers.splice(idx, 1);
|
||||
delete pluginProviderInstances[existingId];
|
||||
Logger.d("Launcher", "Removed plugin provider:", existingId);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Adopt persistent instances from the registry
|
||||
for (var i = 0; i < registeredIds.length; i++) {
|
||||
var providerId = registeredIds[i];
|
||||
if (!pluginProviderInstances[providerId]) {
|
||||
var instance = LauncherProviderRegistry.getProviderInstance(providerId);
|
||||
if (instance) {
|
||||
pluginProviderInstances[providerId] = instance;
|
||||
providers.push(instance);
|
||||
instance.launcher = root;
|
||||
Logger.d("Launcher", "Adopted plugin provider:", providerId);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update results only if providers changed
|
||||
if (changed && root.isOpen) {
|
||||
updateResults();
|
||||
}
|
||||
}
|
||||
|
||||
// Search handling
|
||||
function updateResults() {
|
||||
results = [];
|
||||
var newActiveProvider = null;
|
||||
|
||||
// Check for command mode
|
||||
if (searchText.startsWith(">")) {
|
||||
for (let provider of providers) {
|
||||
if (provider.handleCommand && provider.handleCommand(searchText)) {
|
||||
newActiveProvider = provider;
|
||||
results = provider.getResults(searchText);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Show available commands if just ">" or filter commands if partial match
|
||||
if (!newActiveProvider) {
|
||||
let allCommands = [];
|
||||
for (let provider of providers) {
|
||||
if (provider.commands)
|
||||
allCommands = allCommands.concat(provider.commands());
|
||||
}
|
||||
if (searchText === ">") {
|
||||
results = allCommands;
|
||||
} else if (searchText.length > 1) {
|
||||
const query = searchText.substring(1);
|
||||
if (typeof FuzzySort !== 'undefined') {
|
||||
const fuzzyResults = FuzzySort.go(query, allCommands, {
|
||||
"keys": ["name"],
|
||||
"limit": 50
|
||||
});
|
||||
results = fuzzyResults.map(result => result.obj);
|
||||
} else {
|
||||
const queryLower = query.toLowerCase();
|
||||
results = allCommands.filter(cmd => (cmd.name || "").toLowerCase().includes(queryLower));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Regular search - let providers contribute results
|
||||
let allResults = [];
|
||||
for (let provider of providers) {
|
||||
if (provider.handleSearch) {
|
||||
const providerResults = provider.getResults(searchText);
|
||||
allResults = allResults.concat(providerResults);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by _score (higher = better match), items without _score go first
|
||||
if (searchText.trim() !== "") {
|
||||
const boostByUsage = Settings.data.appLauncher.sortByMostUsed;
|
||||
|
||||
allResults.sort((a, b) => {
|
||||
let sa = a._score !== undefined ? a._score : 0;
|
||||
let sb = b._score !== undefined ? b._score : 0;
|
||||
|
||||
// Boost scores for frequently used items from tracked providers
|
||||
// _score is normalized 0–1, so boost is scaled to nudge, not overwhelm
|
||||
if (boostByUsage) {
|
||||
if (a.provider && a.provider.trackUsage && a.usageKey) {
|
||||
sa += 0.1 * Math.log2(1 + ShellState.getLauncherUsageCount(a.usageKey));
|
||||
}
|
||||
if (b.provider && b.provider.trackUsage && b.usageKey) {
|
||||
sb += 0.1 * Math.log2(1 + ShellState.getLauncherUsageCount(b.usageKey));
|
||||
}
|
||||
}
|
||||
|
||||
return sb - sa;
|
||||
});
|
||||
}
|
||||
results = allResults;
|
||||
}
|
||||
|
||||
// Update activeProvider only after computing new state to avoid UI flicker
|
||||
activeProvider = newActiveProvider;
|
||||
selectedIndex = 0;
|
||||
}
|
||||
|
||||
// Navigation functions (delegated to LauncherNavigation.js)
|
||||
function selectNext() {
|
||||
selectedIndex = LauncherNav.selectNext(selectedIndex, results.length);
|
||||
}
|
||||
function selectPrevious() {
|
||||
selectedIndex = LauncherNav.selectPrevious(selectedIndex, results.length);
|
||||
}
|
||||
function selectNextWrapped() {
|
||||
selectedIndex = LauncherNav.selectNextWrapped(selectedIndex, results.length, allowWrapNavigation);
|
||||
}
|
||||
function selectPreviousWrapped() {
|
||||
selectedIndex = LauncherNav.selectPreviousWrapped(selectedIndex, results.length, allowWrapNavigation);
|
||||
}
|
||||
function selectFirst() {
|
||||
selectedIndex = LauncherNav.selectFirst();
|
||||
}
|
||||
function selectLast() {
|
||||
selectedIndex = LauncherNav.selectLast(results.length);
|
||||
}
|
||||
function selectNextPage() {
|
||||
selectedIndex = LauncherNav.selectNextPage(selectedIndex, results.length, entryHeight);
|
||||
}
|
||||
function selectPreviousPage() {
|
||||
selectedIndex = LauncherNav.selectPreviousPage(selectedIndex, results.length, entryHeight);
|
||||
}
|
||||
function selectPreviousRow() {
|
||||
selectedIndex = LauncherNav.selectPreviousRow(selectedIndex, results.length, gridColumns);
|
||||
}
|
||||
function selectNextRow() {
|
||||
selectedIndex = LauncherNav.selectNextRow(selectedIndex, results.length, gridColumns);
|
||||
}
|
||||
function selectPreviousColumn() {
|
||||
selectedIndex = LauncherNav.selectPreviousColumn(selectedIndex, results.length, gridColumns);
|
||||
}
|
||||
function selectNextColumn() {
|
||||
selectedIndex = LauncherNav.selectNextColumn(selectedIndex, results.length, gridColumns);
|
||||
}
|
||||
|
||||
function activate() {
|
||||
if (results.length > 0 && results[selectedIndex]) {
|
||||
const item = results[selectedIndex];
|
||||
const provider = item.provider || currentProvider;
|
||||
|
||||
// Track usage for providers that opt in (cross-provider "most used" tracking)
|
||||
if (Settings.data.appLauncher.sortByMostUsed && provider && provider.trackUsage && item.usageKey) {
|
||||
ShellState.recordLauncherUsage(item.usageKey);
|
||||
}
|
||||
|
||||
// Check if auto-paste is enabled and provider/item supports it
|
||||
if (Settings.data.appLauncher.autoPasteClipboard && provider && provider.supportsAutoPaste && item.autoPasteText) {
|
||||
if (item.onAutoPaste)
|
||||
item.onAutoPaste();
|
||||
closeImmediately();
|
||||
Qt.callLater(() => {
|
||||
ClipboardService.pasteText(item.autoPasteText);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.onActivate)
|
||||
item.onActivate();
|
||||
}
|
||||
}
|
||||
|
||||
function checkKey(event, settingName) {
|
||||
return Keybinds.checkKey(event, settingName, Settings);
|
||||
}
|
||||
|
||||
// Keyboard handler
|
||||
function handleKeyPress(event) {
|
||||
if (checkKey(event, 'escape')) {
|
||||
close();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkKey(event, 'enter')) {
|
||||
activate();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkKey(event, 'up')) {
|
||||
if (!isSingleView) {
|
||||
isGridView ? selectPreviousRow() : selectPreviousWrapped();
|
||||
}
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkKey(event, 'down')) {
|
||||
if (!isSingleView) {
|
||||
isGridView ? selectNextRow() : selectNextWrapped();
|
||||
}
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkKey(event, 'left')) {
|
||||
if (isGridView) {
|
||||
selectPreviousColumn();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkKey(event, 'right')) {
|
||||
if (isGridView) {
|
||||
selectNextColumn();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Static bindings
|
||||
switch (event.key) {
|
||||
case Qt.Key_Tab:
|
||||
if (showProviderCategories) {
|
||||
var cats = providerCategories;
|
||||
var idx = cats.indexOf(currentProvider.selectedCategory);
|
||||
var nextIdx = (idx + 1) % cats.length;
|
||||
selectCategoryWithSlide(nextIdx);
|
||||
} else {
|
||||
selectNextWrapped();
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Backtab:
|
||||
if (showProviderCategories) {
|
||||
var cats2 = providerCategories;
|
||||
var idx2 = cats2.indexOf(currentProvider.selectedCategory);
|
||||
var prevIdx = ((idx2 - 1) % cats2.length + cats2.length) % cats2.length;
|
||||
selectCategoryWithSlide(prevIdx);
|
||||
} else {
|
||||
selectPreviousWrapped();
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Home:
|
||||
selectFirst();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_End:
|
||||
selectLast();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_PageUp:
|
||||
selectPreviousPage();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_PageDown:
|
||||
selectNextPage();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Delete:
|
||||
if (selectedIndex >= 0 && results && results[selectedIndex]) {
|
||||
var item = results[selectedIndex];
|
||||
var provider = item.provider || currentProvider;
|
||||
if (provider && provider.canDeleteItem && provider.canDeleteItem(item))
|
||||
provider.deleteItem(item);
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Provider components
|
||||
// -----------------------
|
||||
ApplicationsProvider {
|
||||
id: appsProvider
|
||||
Component.onCompleted: {
|
||||
registerProvider(this);
|
||||
Logger.d("Launcher", "Registered: ApplicationsProvider");
|
||||
}
|
||||
}
|
||||
|
||||
ClipboardProvider {
|
||||
id: clipProvider
|
||||
Component.onCompleted: {
|
||||
if (Settings.data.appLauncher.enableClipboardHistory) {
|
||||
registerProvider(this);
|
||||
Logger.d("Launcher", "Registered: ClipboardProvider");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CommandProvider {
|
||||
id: cmdProvider
|
||||
Component.onCompleted: {
|
||||
registerProvider(this);
|
||||
Logger.d("Launcher", "Registered: CommandProvider");
|
||||
}
|
||||
}
|
||||
|
||||
EmojiProvider {
|
||||
id: emojiProvider
|
||||
Component.onCompleted: {
|
||||
registerProvider(this);
|
||||
Logger.d("Launcher", "Registered: EmojiProvider");
|
||||
}
|
||||
}
|
||||
|
||||
CalculatorProvider {
|
||||
id: calcProvider
|
||||
Component.onCompleted: {
|
||||
registerProvider(this);
|
||||
Logger.d("Launcher", "Registered: CalculatorProvider");
|
||||
}
|
||||
}
|
||||
|
||||
SettingsProvider {
|
||||
id: settingsProvider
|
||||
Component.onCompleted: {
|
||||
registerProvider(this);
|
||||
Logger.d("Launcher", "Registered: SettingsProvider");
|
||||
}
|
||||
}
|
||||
|
||||
SessionProvider {
|
||||
id: sessionProvider
|
||||
Component.onCompleted: {
|
||||
registerProvider(this);
|
||||
Logger.d("Launcher", "Registered: SessionProvider");
|
||||
}
|
||||
}
|
||||
|
||||
WindowsProvider {
|
||||
id: windowsProvider
|
||||
Component.onCompleted: {
|
||||
registerProvider(this);
|
||||
Logger.d("Launcher", "Registered: WindowsProvider");
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== UI Content ====================
|
||||
|
||||
opacity: resultsReady ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCirc
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: globalHoverHandler
|
||||
enabled: !Settings.data.appLauncher.ignoreMouseInput
|
||||
|
||||
onPointChanged: {
|
||||
if (!root.mouseTrackingReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!root.globalMouseInitialized) {
|
||||
root.globalLastMouseX = point.position.x;
|
||||
root.globalLastMouseY = point.position.y;
|
||||
root.globalMouseInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = Math.abs(point.position.x - root.globalLastMouseX);
|
||||
const deltaY = Math.abs(point.position.y - root.globalLastMouseY);
|
||||
if (deltaX + deltaY >= 5) {
|
||||
root.ignoreMouseHover = false;
|
||||
root.globalLastMouseX = point.position.x;
|
||||
root.globalLastMouseY = point.position.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Style.marginL
|
||||
anchors.bottomMargin: Style.marginL
|
||||
spacing: Style.marginL
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginL
|
||||
Layout.rightMargin: Style.marginL
|
||||
spacing: Style.marginS
|
||||
|
||||
NTextInput {
|
||||
id: searchInput
|
||||
Layout.fillWidth: true
|
||||
radius: Style.iRadiusM
|
||||
text: root.searchText
|
||||
placeholderText: I18n.tr("placeholders.search-launcher")
|
||||
fontSize: Style.fontSizeM
|
||||
onTextChanged: root.searchText = text
|
||||
|
||||
Component.onCompleted: {
|
||||
if (searchInput.inputItem) {
|
||||
searchInput.inputItem.forceActiveFocus();
|
||||
searchInput.inputItem.Keys.onPressed.connect(function (event) {
|
||||
root.handleKeyPress(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
visible: root.showLayoutToggle
|
||||
icon: Settings.data.appLauncher.viewMode === "grid" ? "layout-list" : "layout-grid"
|
||||
tooltipText: Settings.data.appLauncher.viewMode === "grid" ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view")
|
||||
customRadius: Style.iRadiusM
|
||||
Layout.preferredWidth: searchInput.height
|
||||
Layout.preferredHeight: searchInput.height
|
||||
onClicked: Settings.data.appLauncher.viewMode = Settings.data.appLauncher.viewMode === "grid" ? "list" : "grid"
|
||||
}
|
||||
}
|
||||
|
||||
// Unified category tabs (works with any provider that has categories)
|
||||
NTabBar {
|
||||
id: categoryTabs
|
||||
visible: root.showProviderCategories
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginL
|
||||
Layout.rightMargin: Style.marginL
|
||||
margins: 0
|
||||
border.color: Style.boxBorderColor
|
||||
border.width: Style.borderS
|
||||
|
||||
property int computedCurrentIndex: visible && root.providerCategories.length > 0 ? root.providerCategories.indexOf(root.currentProvider.selectedCategory) : 0
|
||||
currentIndex: computedCurrentIndex
|
||||
|
||||
Repeater {
|
||||
model: root.providerCategories
|
||||
NTabButton {
|
||||
required property string modelData
|
||||
required property int index
|
||||
icon: root.currentProvider.categoryIcons ? (root.currentProvider.categoryIcons[modelData] || "star") : "star"
|
||||
tooltipText: root.currentProvider.getCategoryName ? root.currentProvider.getCategoryName(modelData) : modelData
|
||||
tabIndex: index
|
||||
checked: categoryTabs.currentIndex === index
|
||||
onClicked: root.selectCategoryWithSlide(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Results view
|
||||
NSlideSwapView {
|
||||
id: resultsSwapView
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginL
|
||||
Layout.rightMargin: Style.marginL
|
||||
Layout.fillHeight: true
|
||||
animationsEnabled: !root.animationsDisabled
|
||||
sourceComponent: root.isSingleView ? singleViewComponent : (root.isGridView ? gridViewComponent : listViewComponent)
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
// LIST VIEW
|
||||
Component {
|
||||
id: listViewComponent
|
||||
NListView {
|
||||
id: resultsList
|
||||
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AlwaysOff
|
||||
reserveScrollbarSpace: false
|
||||
gradientColor: Settings.data.ui.panelBackgroundOpacity < 1 ? "transparent" : Color.mSurface
|
||||
wheelScrollMultiplier: 4.0
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
spacing: Style.marginS
|
||||
model: root.results
|
||||
currentIndex: root.selectedIndex
|
||||
cacheBuffer: resultsList.height * 2
|
||||
interactive: !Settings.data.appLauncher.ignoreMouseInput
|
||||
onCurrentIndexChanged: {
|
||||
cancelFlick();
|
||||
if (currentIndex >= 0) {
|
||||
positionViewAtIndex(currentIndex, ListView.Contain);
|
||||
}
|
||||
}
|
||||
onModelChanged: {}
|
||||
|
||||
delegate: LauncherListDelegate {
|
||||
launcher: root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
// SINGLE ITEM VIEW
|
||||
Component {
|
||||
id: singleViewComponent
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
NBox {
|
||||
anchors.fill: parent
|
||||
color: Color.mSurfaceVariant
|
||||
forceOpaque: true
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||
NText {
|
||||
text: root.results.length > 0 ? root.results[0].name : ""
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Font.Bold
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
|
||||
NScrollView {
|
||||
id: descriptionScrollView
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||
Layout.topMargin: Style.fontSizeL + Style.marginXL
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
reserveScrollbarSpace: false
|
||||
|
||||
NText {
|
||||
width: descriptionScrollView.availableWidth
|
||||
text: root.results.length > 0 ? root.results[0].description : ""
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Font.Bold
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: Text.AlignHLeft
|
||||
verticalAlignment: Text.AlignTop
|
||||
wrapMode: Text.Wrap
|
||||
markdownTextEnabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // --------------------------
|
||||
// GRID VIEW
|
||||
Component {
|
||||
id: gridViewComponent
|
||||
NGridView {
|
||||
id: resultsGrid
|
||||
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AlwaysOff
|
||||
reserveScrollbarSpace: false
|
||||
gradientColor: Settings.data.ui.panelBackgroundOpacity < 1 ? "transparent" : Color.mSurface
|
||||
wheelScrollMultiplier: 4.0
|
||||
trackedSelectionIndex: root.selectedIndex
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
cellWidth: parent.width / root.targetGridColumns
|
||||
cellHeight: {
|
||||
var cellWidth = parent.width / root.targetGridColumns;
|
||||
// Use provider's preferred ratio if available
|
||||
if (root.currentProvider && root.currentProvider.preferredGridCellRatio) {
|
||||
return cellWidth * root.currentProvider.preferredGridCellRatio;
|
||||
}
|
||||
return cellWidth;
|
||||
}
|
||||
leftMargin: 0
|
||||
rightMargin: 0
|
||||
topMargin: 0
|
||||
bottomMargin: 0
|
||||
model: root.results
|
||||
cacheBuffer: resultsGrid.height * 2
|
||||
keyNavigationEnabled: false
|
||||
focus: false
|
||||
interactive: !Settings.data.appLauncher.ignoreMouseInput
|
||||
|
||||
// Completely disable GridView key handling
|
||||
Keys.enabled: false
|
||||
|
||||
// Handle scrolling to show selected item when it changes
|
||||
Connections {
|
||||
target: root
|
||||
enabled: root.isGridView
|
||||
function onSelectedIndexChanged() {
|
||||
if (!root.isGridView || root.selectedIndex < 0 || !resultsGrid) {
|
||||
return;
|
||||
}
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (root.isGridView && resultsGrid && resultsGrid.cancelFlick) {
|
||||
resultsGrid.cancelFlick();
|
||||
resultsGrid.positionViewAtIndex(root.selectedIndex, GridView.Contain);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
delegate: LauncherGridDelegate {
|
||||
launcher: root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.leftMargin: Style.marginL
|
||||
Layout.rightMargin: Style.marginL
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginS
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
if (root.results.length === 0) {
|
||||
if (root.searchText) {
|
||||
return I18n.tr("common.no-results");
|
||||
}
|
||||
// Use provider's empty browsing message if available
|
||||
var provider = root.currentProvider;
|
||||
if (provider && provider.emptyBrowsingMessage) {
|
||||
return provider.emptyBrowsingMessage;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
var prefix = root.activeProvider && root.activeProvider.name ? root.activeProvider.name + ": " : "";
|
||||
return prefix + I18n.trp("common.result-count", root.results.length);
|
||||
}
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Widgets
|
||||
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: gridEntryContainer
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
required property var launcher
|
||||
|
||||
width: GridView.view.cellWidth
|
||||
height: GridView.view.cellHeight
|
||||
|
||||
property bool isSelected: (!launcher.ignoreMouseHover && mouseArea.containsMouse) || (index === launcher.selectedIndex)
|
||||
|
||||
// Prepare item when it becomes visible (e.g., decode images)
|
||||
Component.onCompleted: {
|
||||
var provider = modelData.provider;
|
||||
if (provider && provider.prepareItem) {
|
||||
provider.prepareItem(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
NBox {
|
||||
id: gridEntry
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginXXS
|
||||
color: gridEntryContainer.isSelected ? Color.mHover : Color.mSurfaceVariant
|
||||
forceOpaque: gridEntryContainer.isSelected
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCirc
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: launcher.isCompactDensity ? Style.marginXS : Style.marginS
|
||||
anchors.bottomMargin: launcher.isCompactDensity ? Style.marginXS : Style.marginS
|
||||
spacing: launcher.isCompactDensity ? 0 : Style.marginXXS
|
||||
|
||||
// Icon badge or Image preview or Emoji
|
||||
Item {
|
||||
// Size image at 65% of cell dimensions.
|
||||
Layout.preferredWidth: Math.round(gridEntry.width * 0.65)
|
||||
Layout.preferredHeight: Math.round(gridEntry.height * 0.65)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
// Icon background
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Style.radiusM
|
||||
color: Color.mSurface
|
||||
visible: Settings.data.appLauncher.showIconBackground && !modelData.isImage
|
||||
}
|
||||
|
||||
// Image preview - uses provider's getImageUrl if available
|
||||
NImageRounded {
|
||||
id: gridImagePreview
|
||||
anchors.fill: parent
|
||||
visible: !!modelData.isImage && !modelData.displayString
|
||||
radius: Style.radiusM
|
||||
|
||||
// Use provider's image revision for reactive updates
|
||||
readonly property int _rev: modelData.provider && modelData.provider.imageRevision ? modelData.provider.imageRevision : 0
|
||||
|
||||
// Get image URL from provider
|
||||
imagePath: {
|
||||
_rev;
|
||||
var provider = modelData.provider;
|
||||
if (provider && provider.getImageUrl) {
|
||||
return provider.getImageUrl(modelData);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: parent.status === Image.Loading
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: true
|
||||
width: Style.baseWidgetSize * 0.5
|
||||
height: width
|
||||
}
|
||||
}
|
||||
|
||||
onStatusChanged: status => {
|
||||
if (status === Image.Error) {
|
||||
gridIconLoader.visible = true;
|
||||
gridImagePreview.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: gridIconLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginXS
|
||||
|
||||
visible: (!modelData.isImage && !modelData.displayString) || (!!modelData.isImage && gridImagePreview.status === Image.Error)
|
||||
active: visible
|
||||
|
||||
sourceComponent: Settings.data.appLauncher.iconMode === "tabler" && modelData.isTablerIcon ? gridTablerIconComponent : gridSystemIconComponent
|
||||
|
||||
Component {
|
||||
id: gridTablerIconComponent
|
||||
NIcon {
|
||||
icon: modelData.icon
|
||||
pointSize: Style.fontSizeXXXL
|
||||
visible: modelData.icon && !modelData.displayString
|
||||
color: (gridEntryContainer.isSelected && !Settings.data.appLauncher.showIconBackground) ? Color.mOnHover : Color.mOnSurface
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: gridSystemIconComponent
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
|
||||
visible: modelData.icon && source !== "" && !modelData.displayString
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// String display
|
||||
NText {
|
||||
id: gridStringDisplay
|
||||
anchors.centerIn: parent
|
||||
visible: !!modelData.displayString || (!gridImagePreview.visible && !gridIconLoader.visible)
|
||||
text: modelData.displayString ? modelData.displayString : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?")
|
||||
pointSize: {
|
||||
if (modelData.displayString) {
|
||||
// Use custom size if provided, otherwise default scaling
|
||||
if (modelData.displayStringSize) {
|
||||
return modelData.displayStringSize * Style.uiScaleRatio;
|
||||
}
|
||||
if (launcher.providerHasDisplayString) {
|
||||
// Scale with cell width but cap at reasonable maximum
|
||||
const cellBasedSize = gridEntry.width * 0.4;
|
||||
const maxSize = Style.fontSizeXXXL * Style.uiScaleRatio;
|
||||
return Math.min(cellBasedSize, maxSize);
|
||||
}
|
||||
return Style.fontSizeXXL * 2 * Style.uiScaleRatio;
|
||||
}
|
||||
// Scale font size relative to cell width for low res, but cap at maximum
|
||||
const cellBasedSize = gridEntry.width * 0.25;
|
||||
const baseSize = Style.fontSizeXL * Style.uiScaleRatio;
|
||||
const maxSize = Style.fontSizeXXL * Style.uiScaleRatio;
|
||||
return Math.min(Math.max(cellBasedSize, baseSize), maxSize);
|
||||
}
|
||||
font.weight: Style.fontWeightBold
|
||||
color: modelData.displayString ? Color.mOnSurface : Color.mOnPrimary
|
||||
}
|
||||
|
||||
// Badge icon overlay (generic indicator for any provider)
|
||||
Rectangle {
|
||||
visible: !!modelData.badgeIcon
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 2
|
||||
width: height
|
||||
height: Style.fontSizeM + Style.marginXS
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusXXS
|
||||
NIcon {
|
||||
anchors.centerIn: parent
|
||||
icon: modelData.badgeIcon || ""
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text content (hidden when hideLabel is true)
|
||||
NText {
|
||||
visible: !modelData.hideLabel
|
||||
text: modelData.name || "Unknown"
|
||||
pointSize: {
|
||||
if (launcher.providerHasDisplayString && modelData.displayString) {
|
||||
return Style.fontSizeS * Style.uiScaleRatio;
|
||||
}
|
||||
// Scale font size relative to cell width for low res, but cap at maximum
|
||||
const cellBasedSize = gridEntry.width * 0.1;
|
||||
const baseSize = Style.fontSizeXS * Style.uiScaleRatio;
|
||||
const maxSize = Style.fontSizeS * Style.uiScaleRatio;
|
||||
return Math.min(Math.max(cellBasedSize, baseSize), maxSize);
|
||||
}
|
||||
font.weight: Style.fontWeightSemiBold
|
||||
color: gridEntryContainer.isSelected ? Color.mOnHover : Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: gridEntry.width - 8
|
||||
Layout.leftMargin: (launcher.providerHasDisplayString && modelData.displayString) ? Style.marginS : 0
|
||||
Layout.rightMargin: (launcher.providerHasDisplayString && modelData.displayString) ? Style.marginS : 0
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons (overlay in top-right corner) - dynamically populated from provider
|
||||
Row {
|
||||
visible: gridEntryContainer.isSelected && gridItemActions.length > 0
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Style.marginXS
|
||||
z: 10
|
||||
spacing: Style.marginXXS
|
||||
|
||||
property var gridItemActions: {
|
||||
if (!gridEntryContainer.isSelected)
|
||||
return [];
|
||||
var provider = modelData.provider || launcher.currentProvider;
|
||||
if (provider && provider.getItemActions) {
|
||||
return provider.getItemActions(modelData);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: parent.gridItemActions
|
||||
NIconButton {
|
||||
required property var modelData
|
||||
icon: modelData.icon
|
||||
baseSize: Style.baseWidgetSize * 0.75
|
||||
tooltipText: modelData.tooltip
|
||||
z: 11
|
||||
handleWheel: true
|
||||
onClicked: {
|
||||
if (modelData.action) {
|
||||
modelData.action();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: !Settings.data.appLauncher.ignoreMouseInput
|
||||
|
||||
onEntered: {
|
||||
if (!launcher.ignoreMouseHover) {
|
||||
launcher.selectedIndex = gridEntryContainer.index;
|
||||
}
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
launcher.selectedIndex = gridEntryContainer.index;
|
||||
launcher.activate();
|
||||
mouse.accepted = true;
|
||||
}
|
||||
}
|
||||
acceptedButtons: Qt.LeftButton
|
||||
}
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Widgets
|
||||
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
NBox {
|
||||
id: entry
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
required property var launcher
|
||||
|
||||
property bool isSelected: (!launcher.ignoreMouseHover && mouseArea.containsMouse) || (index === launcher.selectedIndex)
|
||||
|
||||
width: ListView.view.width
|
||||
implicitHeight: launcher.entryHeight
|
||||
clip: true
|
||||
color: entry.isSelected ? Color.mHover : Color.mSurfaceVariant
|
||||
forceOpaque: entry.isSelected
|
||||
|
||||
// Prepare item when it becomes visible (e.g., decode images)
|
||||
Component.onCompleted: {
|
||||
var provider = modelData.provider;
|
||||
if (provider && provider.prepareItem) {
|
||||
provider.prepareItem(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCirc
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: launcher.isCompactDensity ? Style.marginXS : Style.marginM
|
||||
spacing: launcher.isCompactDensity ? Style.marginXS : Style.marginM
|
||||
|
||||
// Top row - Main entry content with action buttons
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: launcher.isCompactDensity ? Style.marginS : Style.marginM
|
||||
|
||||
// Icon badge or Image preview or Emoji
|
||||
Item {
|
||||
visible: !modelData.hideIcon
|
||||
Layout.preferredWidth: modelData.hideIcon ? 0 : launcher.badgeSize
|
||||
Layout.preferredHeight: modelData.hideIcon ? 0 : launcher.badgeSize
|
||||
|
||||
// Icon background
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Style.radiusXS
|
||||
color: Color.mSurface
|
||||
visible: Settings.data.appLauncher.showIconBackground && !modelData.isImage
|
||||
}
|
||||
|
||||
// Image preview - uses provider's getImageUrl if available
|
||||
NImageRounded {
|
||||
id: imagePreview
|
||||
anchors.fill: parent
|
||||
visible: !!modelData.isImage && !modelData.displayString
|
||||
radius: Style.radiusXS
|
||||
borderColor: Color.mOnSurface
|
||||
borderWidth: Style.borderM
|
||||
imageFillMode: Image.PreserveAspectCrop
|
||||
|
||||
// Use provider's image revision for reactive updates
|
||||
readonly property int _rev: modelData.provider && modelData.provider.imageRevision ? modelData.provider.imageRevision : 0
|
||||
|
||||
// Get image URL from provider
|
||||
imagePath: {
|
||||
_rev;
|
||||
var provider = modelData.provider;
|
||||
if (provider && provider.getImageUrl) {
|
||||
return provider.getImageUrl(modelData);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: parent.status === Image.Loading
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: true
|
||||
width: Style.baseWidgetSize * 0.5
|
||||
height: width
|
||||
}
|
||||
}
|
||||
|
||||
onStatusChanged: status => {
|
||||
if (status === Image.Error) {
|
||||
iconLoader.visible = true;
|
||||
imagePreview.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Color swatch - shown for clipboard color entries
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Style.radiusXS
|
||||
color: modelData.colorHex || "transparent"
|
||||
visible: !!modelData.colorHex
|
||||
border.color: Color.mOnSurface
|
||||
border.width: Style.borderM
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: iconLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginXS
|
||||
|
||||
visible: (!modelData.isImage && !modelData.displayString && !modelData.colorHex) || (!!modelData.isImage && imagePreview.status === Image.Error)
|
||||
active: visible
|
||||
|
||||
sourceComponent: Component {
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
sourceComponent: Settings.data.appLauncher.iconMode === "tabler" && modelData.isTablerIcon ? tablerIconComponent : systemIconComponent
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: tablerIconComponent
|
||||
NIcon {
|
||||
icon: modelData.icon
|
||||
pointSize: Style.fontSizeXXXL
|
||||
visible: modelData.icon && !modelData.displayString
|
||||
color: (entry.isSelected && !Settings.data.appLauncher.showIconBackground) ? Color.mOnHover : Color.mOnSurface
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: systemIconComponent
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
|
||||
visible: modelData.icon && source !== "" && !modelData.displayString
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// String display - takes precedence when displayString is present
|
||||
NText {
|
||||
id: stringDisplay
|
||||
anchors.centerIn: parent
|
||||
visible: !!modelData.displayString || (!imagePreview.visible && !iconLoader.visible)
|
||||
text: modelData.displayString ? modelData.displayString : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?")
|
||||
pointSize: modelData.displayString ? (modelData.displayStringSize || Style.fontSizeXXXL) : Style.fontSizeXXL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: modelData.displayString ? Color.mOnSurface : Color.mOnPrimary
|
||||
}
|
||||
|
||||
// Image type indicator overlay
|
||||
Rectangle {
|
||||
visible: !!modelData.isImage && imagePreview.visible
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 2
|
||||
width: formatLabel.width + Style.marginXS
|
||||
height: formatLabel.height + Style.marginXXS
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusXXS
|
||||
NText {
|
||||
id: formatLabel
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
if (!modelData.isImage)
|
||||
return "";
|
||||
const desc = modelData.description || "";
|
||||
const parts = desc.split(" \u2022 ");
|
||||
return parts[0] || "IMG";
|
||||
}
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
// Badge icon overlay (generic indicator for any provider)
|
||||
Rectangle {
|
||||
visible: !!modelData.badgeIcon
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 2
|
||||
width: height
|
||||
height: Style.fontSizeM + Style.marginXS
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusXXS
|
||||
NIcon {
|
||||
anchors.centerIn: parent
|
||||
icon: modelData.badgeIcon || ""
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text content
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
NText {
|
||||
text: modelData.name || "Unknown"
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: entry.isSelected ? Color.mOnHover : Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
wrapMode: Text.Wrap
|
||||
clip: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.description || ""
|
||||
pointSize: Style.fontSizeS
|
||||
color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
Layout.fillWidth: true
|
||||
visible: text !== "" && !launcher.isCompactDensity
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons row - dynamically populated from provider
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
spacing: Style.marginXS
|
||||
visible: entry.isSelected && itemActions.length > 0
|
||||
|
||||
property var itemActions: {
|
||||
if (!entry.isSelected)
|
||||
return [];
|
||||
var provider = modelData.provider || launcher.currentProvider;
|
||||
if (provider && provider.getItemActions) {
|
||||
return provider.getItemActions(modelData);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: parent.itemActions
|
||||
NIconButton {
|
||||
required property var modelData
|
||||
icon: modelData.icon
|
||||
baseSize: Style.baseWidgetSize * 0.75
|
||||
tooltipText: modelData.tooltip
|
||||
z: 1
|
||||
handleWheel: true
|
||||
onClicked: {
|
||||
if (modelData.action) {
|
||||
modelData.action();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: !Settings.data.appLauncher.ignoreMouseInput
|
||||
onEntered: {
|
||||
if (!launcher.ignoreMouseHover) {
|
||||
launcher.selectedIndex = entry.index;
|
||||
}
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
launcher.selectedIndex = entry.index;
|
||||
launcher.activate();
|
||||
mouse.accepted = true;
|
||||
}
|
||||
}
|
||||
acceptedButtons: Qt.LeftButton
|
||||
}
|
||||
}
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen.Backgrounds
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
// Standalone launcher window for Overlay layer mode.
|
||||
// This window appears above fullscreen windows and does not attach to the bar.
|
||||
Variants {
|
||||
id: launcherVariants
|
||||
|
||||
model: Quickshell.screens.filter(screen => Settings.data.appLauncher.overviewLayer)
|
||||
|
||||
delegate: Loader {
|
||||
id: windowLoader
|
||||
|
||||
required property ShellScreen modelData
|
||||
|
||||
active: PanelService.overlayLauncherOpen && PanelService.overlayLauncherScreen === modelData
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: launcherWindow
|
||||
screen: windowLoader.modelData
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.namespace: "noctalia-launcher-overlay-" + (screen?.name || "unknown")
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
BackgroundEffect.blurRegion: Settings.data.general.enableBlurBehind ? launcherBlurRegion : null
|
||||
Region {
|
||||
id: launcherBlurRegion
|
||||
|
||||
Region {
|
||||
x: Math.round(launcherPanel.x)
|
||||
y: Math.round(launcherPanel.y)
|
||||
width: Math.round(launcherPanel.width)
|
||||
height: Math.round(launcherPanel.height)
|
||||
radius: Style.radiusL
|
||||
topLeftCorner: launcherPanel.topLeftCornerState
|
||||
topRightCorner: launcherPanel.topRightCornerState
|
||||
bottomLeftCorner: launcherPanel.bottomLeftCornerState
|
||||
bottomRightCorner: launcherPanel.bottomRightCornerState
|
||||
}
|
||||
|
||||
Region {
|
||||
x: Math.round(previewBox.visible ? previewBox.x : 0)
|
||||
y: Math.round(previewBox.visible ? previewBox.y : 0)
|
||||
width: Math.round(previewBox.visible ? previewBox.width : 0)
|
||||
height: Math.round(previewBox.visible ? previewBox.height : 0)
|
||||
radius: Style.radiusL
|
||||
}
|
||||
}
|
||||
|
||||
// Positioning logic (respects settings but doesn't attach to bar)
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property int barThickness: Math.round(Style.barHeight + Style.marginL)
|
||||
|
||||
readonly property string panelPosition: {
|
||||
var pos = Settings.data.appLauncher.position;
|
||||
if (pos === "follow_bar") {
|
||||
if (barIsVertical) {
|
||||
return "center_" + barPosition;
|
||||
} else {
|
||||
return barPosition + "_center";
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Preview panel support
|
||||
readonly property int listPanelWidth: Math.round(500 * Style.uiScaleRatio)
|
||||
readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio)
|
||||
readonly property bool previewActive: {
|
||||
if (!launcherCore)
|
||||
return false;
|
||||
var provider = launcherCore.activeProvider;
|
||||
if (!provider || !provider.hasPreview)
|
||||
return false;
|
||||
if (!Settings.data.appLauncher.enableClipPreview)
|
||||
return false;
|
||||
return launcherCore.selectedIndex >= 0 && launcherCore.results && !!launcherCore.results[launcherCore.selectedIndex];
|
||||
}
|
||||
|
||||
// Dimmer background (click to close)
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.alpha(Color.mSurface, Settings.data.general.dimmerOpacity)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: PanelService.closeOverlayLauncher()
|
||||
}
|
||||
}
|
||||
|
||||
// Shadow for launcher panel
|
||||
NDropShadow {
|
||||
source: launcherPanel
|
||||
anchors.fill: launcherPanel
|
||||
autoPaddingEnabled: true
|
||||
}
|
||||
|
||||
// Launcher panel with position-based anchoring
|
||||
Item {
|
||||
id: launcherPanel
|
||||
width: Math.round(Math.max(parent.width * 0.25, launcherWindow.listPanelWidth + Style.margin2L * 2))
|
||||
height: Math.round(Math.max(parent.height * 0.5, 600 * Style.uiScaleRatio))
|
||||
clip: false
|
||||
|
||||
// Entrance animation
|
||||
opacity: 0
|
||||
transformOrigin: {
|
||||
if (touchingTop && touchingLeft)
|
||||
return Item.TopLeft;
|
||||
if (touchingTop && touchingRight)
|
||||
return Item.TopRight;
|
||||
if (touchingBottom && touchingLeft)
|
||||
return Item.BottomLeft;
|
||||
if (touchingBottom && touchingRight)
|
||||
return Item.BottomRight;
|
||||
if (touchingTop)
|
||||
return Item.Top;
|
||||
if (touchingBottom)
|
||||
return Item.Bottom;
|
||||
if (touchingLeft)
|
||||
return Item.Left;
|
||||
if (touchingRight)
|
||||
return Item.Right;
|
||||
return Item.Center;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal positioning
|
||||
anchors.horizontalCenter: (panelPosition === "center" || panelPosition.endsWith("_center")) ? parent.horizontalCenter : undefined
|
||||
anchors.left: panelPosition.endsWith("_left") ? parent.left : undefined
|
||||
anchors.right: panelPosition.endsWith("_right") ? parent.right : undefined
|
||||
|
||||
// Vertical positioning
|
||||
anchors.verticalCenter: (panelPosition === "center" || panelPosition.startsWith("center_")) ? parent.verticalCenter : undefined
|
||||
anchors.top: panelPosition.startsWith("top_") ? parent.top : undefined
|
||||
anchors.bottom: panelPosition.startsWith("bottom_") ? parent.bottom : undefined
|
||||
|
||||
// Margins - only add bar clearance on the bar's edge
|
||||
anchors.leftMargin: barPosition === "left" ? barThickness : 0
|
||||
anchors.rightMargin: barPosition === "right" ? barThickness : 0
|
||||
anchors.topMargin: barPosition === "top" ? barThickness : 0
|
||||
anchors.bottomMargin: barPosition === "bottom" ? barThickness : 0
|
||||
|
||||
// Edge detection - based on position setting and bar location
|
||||
readonly property bool touchingLeft: panelPosition.endsWith("_left") && barPosition !== "left"
|
||||
readonly property bool touchingRight: panelPosition.endsWith("_right") && barPosition !== "right"
|
||||
readonly property bool touchingTop: panelPosition.startsWith("top_") && barPosition !== "top"
|
||||
readonly property bool touchingBottom: panelPosition.startsWith("bottom_") && barPosition !== "bottom"
|
||||
|
||||
// Corner states based on edge touching
|
||||
// State 0: Normal rounded, State 1: Horizontal inversion, State 2: Vertical inversion
|
||||
readonly property int topLeftCornerState: {
|
||||
if (touchingLeft && touchingTop)
|
||||
return 0;
|
||||
if (touchingLeft)
|
||||
return 2;
|
||||
if (touchingTop)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
readonly property int topRightCornerState: {
|
||||
if (touchingRight && touchingTop)
|
||||
return 0;
|
||||
if (touchingRight)
|
||||
return 2;
|
||||
if (touchingTop)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
readonly property int bottomLeftCornerState: {
|
||||
if (touchingLeft && touchingBottom)
|
||||
return 0;
|
||||
if (touchingLeft)
|
||||
return 2;
|
||||
if (touchingBottom)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
readonly property int bottomRightCornerState: {
|
||||
if (touchingRight && touchingBottom)
|
||||
return 0;
|
||||
if (touchingRight)
|
||||
return 2;
|
||||
if (touchingBottom)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Background with inverted corners - extends beyond panel for inverted corners
|
||||
Shape {
|
||||
id: panelShape
|
||||
// Extend shape to allow inverted corners to render outside panel bounds
|
||||
x: -radius
|
||||
y: -radius
|
||||
width: launcherPanel.width + radius * 2
|
||||
height: launcherPanel.height + radius * 2
|
||||
visible: panelW > 0 && panelH > 0
|
||||
opacity: launcherPanel.opacity
|
||||
layer.enabled: true
|
||||
|
||||
readonly property real radius: Style.radiusL
|
||||
|
||||
// Panel dimensions (for path calculations)
|
||||
readonly property real panelW: launcherPanel.width
|
||||
readonly property real panelH: launcherPanel.height
|
||||
|
||||
// Helper functions for corner rendering
|
||||
function getMultX(state) {
|
||||
return state === 1 ? -1 : 1;
|
||||
}
|
||||
function getMultY(state) {
|
||||
return state === 2 ? -1 : 1;
|
||||
}
|
||||
function getArcDir(multX, multY) {
|
||||
return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise;
|
||||
}
|
||||
|
||||
readonly property real tlMultX: getMultX(launcherPanel.topLeftCornerState)
|
||||
readonly property real tlMultY: getMultY(launcherPanel.topLeftCornerState)
|
||||
readonly property real trMultX: getMultX(launcherPanel.topRightCornerState)
|
||||
readonly property real trMultY: getMultY(launcherPanel.topRightCornerState)
|
||||
readonly property real blMultX: getMultX(launcherPanel.bottomLeftCornerState)
|
||||
readonly property real blMultY: getMultY(launcherPanel.bottomLeftCornerState)
|
||||
readonly property real brMultX: getMultX(launcherPanel.bottomRightCornerState)
|
||||
readonly property real brMultY: getMultY(launcherPanel.bottomRightCornerState)
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: -1
|
||||
fillColor: Qt.alpha(Color.mSurface, Color.adaptiveOpacity(Settings.data.ui.panelBackgroundOpacity))
|
||||
|
||||
// Offset by radius to account for Shape's extended bounds
|
||||
startX: panelShape.radius + panelShape.radius * panelShape.tlMultX
|
||||
startY: panelShape.radius
|
||||
|
||||
// Top edge
|
||||
PathLine {
|
||||
relativeX: panelShape.panelW - panelShape.radius * panelShape.tlMultX - panelShape.radius * panelShape.trMultX
|
||||
relativeY: 0
|
||||
}
|
||||
// Top-right corner
|
||||
PathArc {
|
||||
relativeX: panelShape.radius * panelShape.trMultX
|
||||
relativeY: panelShape.radius * panelShape.trMultY
|
||||
radiusX: panelShape.radius
|
||||
radiusY: panelShape.radius
|
||||
direction: panelShape.getArcDir(panelShape.trMultX, panelShape.trMultY)
|
||||
}
|
||||
// Right edge
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: panelShape.panelH - panelShape.radius * panelShape.trMultY - panelShape.radius * panelShape.brMultY
|
||||
}
|
||||
// Bottom-right corner
|
||||
PathArc {
|
||||
relativeX: -panelShape.radius * panelShape.brMultX
|
||||
relativeY: panelShape.radius * panelShape.brMultY
|
||||
radiusX: panelShape.radius
|
||||
radiusY: panelShape.radius
|
||||
direction: panelShape.getArcDir(panelShape.brMultX, panelShape.brMultY)
|
||||
}
|
||||
// Bottom edge
|
||||
PathLine {
|
||||
relativeX: -(panelShape.panelW - panelShape.radius * panelShape.brMultX - panelShape.radius * panelShape.blMultX)
|
||||
relativeY: 0
|
||||
}
|
||||
// Bottom-left corner
|
||||
PathArc {
|
||||
relativeX: -panelShape.radius * panelShape.blMultX
|
||||
relativeY: -panelShape.radius * panelShape.blMultY
|
||||
radiusX: panelShape.radius
|
||||
radiusY: panelShape.radius
|
||||
direction: panelShape.getArcDir(panelShape.blMultX, panelShape.blMultY)
|
||||
}
|
||||
// Left edge
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: -(panelShape.panelH - panelShape.radius * panelShape.blMultY - panelShape.radius * panelShape.tlMultY)
|
||||
}
|
||||
// Top-left corner
|
||||
PathArc {
|
||||
relativeX: panelShape.radius * panelShape.tlMultX
|
||||
relativeY: -panelShape.radius * panelShape.tlMultY
|
||||
radiusX: panelShape.radius
|
||||
radiusY: panelShape.radius
|
||||
direction: panelShape.getArcDir(panelShape.tlMultX, panelShape.tlMultY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Border
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
radius: Style.radiusL
|
||||
border.color: Style.boxBorderColor
|
||||
border.width: Style.borderS
|
||||
visible: !launcherPanel.touchingLeft && !launcherPanel.touchingRight && !launcherPanel.touchingTop && !launcherPanel.touchingBottom
|
||||
}
|
||||
|
||||
LauncherCore {
|
||||
id: launcherCore
|
||||
anchors.fill: parent
|
||||
screen: windowLoader.modelData
|
||||
isOpen: true
|
||||
onRequestClose: PanelService.closeOverlayLauncher()
|
||||
onRequestCloseImmediately: PanelService.closeOverlayLauncherImmediately()
|
||||
|
||||
Component.onCompleted: PanelService.overlayLauncherCore = launcherCore
|
||||
Component.onDestruction: PanelService.overlayLauncherCore = null
|
||||
}
|
||||
}
|
||||
|
||||
// Preview Panel - positioned as sibling of launcherPanel to avoid shadow bleed
|
||||
NDropShadow {
|
||||
source: previewBox
|
||||
anchors.fill: previewBox
|
||||
autoPaddingEnabled: true
|
||||
visible: previewBox.visible
|
||||
z: previewBox.z - 1
|
||||
}
|
||||
|
||||
NBox {
|
||||
id: previewBox
|
||||
visible: launcherWindow.previewActive
|
||||
width: launcherWindow.previewPanelWidth
|
||||
height: Math.round(400 * Style.uiScaleRatio)
|
||||
forceOpaque: true
|
||||
x: {
|
||||
if (panelPosition.endsWith("_right"))
|
||||
return launcherPanel.x - launcherWindow.previewPanelWidth - Style.marginM;
|
||||
return launcherPanel.x + launcherPanel.width + Style.marginM;
|
||||
}
|
||||
y: {
|
||||
var view = launcherCore.resultsView;
|
||||
if (!view)
|
||||
return launcherPanel.y + Style.marginL;
|
||||
var row = launcherCore.isGridView ? Math.floor(launcherCore.selectedIndex / launcherCore.gridColumns) : launcherCore.selectedIndex;
|
||||
var gridCellSize = Math.floor((launcherWindow.listPanelWidth - (2 * Style.marginXS) - ((launcherCore.targetGridColumns - 1) * Style.marginS)) / launcherCore.targetGridColumns);
|
||||
var itemHeight = launcherCore.isGridView ? (gridCellSize + Style.marginXXS) : (launcherCore.entryHeight + (view.spacing || 0));
|
||||
var yPos = row * itemHeight - (view.contentY || 0);
|
||||
var mapped = view.mapToItem(launcherWindow.contentItem, 0, yPos);
|
||||
return Math.max(launcherPanel.y + Style.marginL, Math.min(mapped.y, launcherPanel.y + launcherPanel.height - previewBox.height - Style.marginL));
|
||||
}
|
||||
|
||||
opacity: visible ? 1.0 : 0.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: previewLoader
|
||||
anchors.fill: parent
|
||||
active: launcherWindow.previewActive
|
||||
source: {
|
||||
if (!active)
|
||||
return "";
|
||||
var provider = launcherCore.activeProvider;
|
||||
if (provider && provider.previewComponentPath)
|
||||
return provider.previewComponentPath;
|
||||
return "";
|
||||
}
|
||||
|
||||
onLoaded: updatePreviewItem()
|
||||
onItemChanged: updatePreviewItem()
|
||||
|
||||
function updatePreviewItem() {
|
||||
if (!item || launcherCore.selectedIndex < 0 || !launcherCore.results[launcherCore.selectedIndex])
|
||||
return;
|
||||
var provider = launcherCore.activeProvider;
|
||||
if (provider && provider.getPreviewData) {
|
||||
item.currentItem = provider.getPreviewData(launcherCore.results[launcherCore.selectedIndex]);
|
||||
} else {
|
||||
item.currentItem = launcherCore.results[launcherCore.selectedIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update preview when selection changes
|
||||
Connections {
|
||||
target: launcherCore
|
||||
function onSelectedIndexChanged() {
|
||||
if (previewLoader.item)
|
||||
previewLoader.updatePreviewItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+654
@@ -0,0 +1,654 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.System
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var launcher: null
|
||||
property string name: I18n.tr("launcher.providers.applications")
|
||||
property bool handleSearch: true
|
||||
property var entries: []
|
||||
property string supportedLayouts: "both"
|
||||
property bool isDefaultProvider: true // This provider handles empty search
|
||||
property bool ignoreDensity: false // Apps should scale with launcher density
|
||||
property bool trackUsage: true // Track usage frequency for "most used" sorting
|
||||
|
||||
// Category support
|
||||
property string selectedCategory: "all"
|
||||
property bool showsCategories: true // Default to showing categories
|
||||
property var categories: ["all", "Pinned", "AudioVideo", "Chat", "Development", "Education", "Game", "Graphics", "Network", "Office", "System", "Misc", "WebBrowser"]
|
||||
property var availableCategories: ["all"] // Reactive property for available categories
|
||||
|
||||
property var categoryIcons: ({
|
||||
"all": "apps",
|
||||
"Pinned": "pin",
|
||||
"AudioVideo": "music",
|
||||
"Chat": "message-circle",
|
||||
"Development": "code",
|
||||
"Education": "school" // Includes Science
|
||||
,
|
||||
"Game": "device-gamepad",
|
||||
"Graphics": "brush",
|
||||
"Network": "wifi",
|
||||
"Office": "file-text",
|
||||
"System": "device-desktop" // Includes Settings and Utility
|
||||
,
|
||||
"Misc": "dots",
|
||||
"WebBrowser": "world"
|
||||
})
|
||||
|
||||
function getCategoryName(category) {
|
||||
const names = {
|
||||
"all": I18n.tr("launcher.categories.all"),
|
||||
"Pinned": I18n.tr("launcher.categories.pinned"),
|
||||
"AudioVideo": I18n.tr("launcher.categories.audiovideo"),
|
||||
"Chat": I18n.tr("launcher.categories.chat"),
|
||||
"Development": I18n.tr("launcher.categories.development"),
|
||||
"Education": I18n.tr("launcher.categories.education"),
|
||||
"Game": I18n.tr("launcher.categories.game"),
|
||||
"Graphics": I18n.tr("launcher.categories.graphics"),
|
||||
"Network": I18n.tr("common.network"),
|
||||
"Office": I18n.tr("launcher.categories.office"),
|
||||
"System": I18n.tr("launcher.categories.system"),
|
||||
"Misc": I18n.tr("launcher.categories.misc"),
|
||||
"WebBrowser": I18n.tr("launcher.categories.webbrowser")
|
||||
};
|
||||
return names[category] || category;
|
||||
}
|
||||
|
||||
function init() {
|
||||
loadApplications();
|
||||
migrateLegacyUsageKeys();
|
||||
}
|
||||
|
||||
function onOpened() {
|
||||
// Just update available categories in case pinned apps changed
|
||||
updateAvailableCategories();
|
||||
// Default to Pinned if there are pinned apps, otherwise all
|
||||
if (availableCategories.includes("Pinned")) {
|
||||
selectedCategory = "Pinned";
|
||||
} else {
|
||||
selectedCategory = "all";
|
||||
}
|
||||
// Set category mode initially (will be updated when getResults is called)
|
||||
showsCategories = true;
|
||||
}
|
||||
|
||||
// Reload applications when desktop entries change on disk
|
||||
Connections {
|
||||
target: typeof DesktopEntries !== 'undefined' ? DesktopEntries.applications : null
|
||||
function onValuesChanged() {
|
||||
Logger.d("ApplicationsProvider", "Desktop entries changed, reloading applications");
|
||||
loadApplications();
|
||||
}
|
||||
}
|
||||
|
||||
function selectCategory(category) {
|
||||
selectedCategory = category;
|
||||
if (launcher) {
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
|
||||
function getAppCategories(app) {
|
||||
if (!app)
|
||||
return [];
|
||||
|
||||
const result = [];
|
||||
|
||||
if (app.categories) {
|
||||
if (Array.isArray(app.categories)) {
|
||||
for (let cat of app.categories) {
|
||||
if (cat && cat.trim && cat.trim() !== '') {
|
||||
result.push(cat.trim());
|
||||
} else if (cat && typeof cat === 'string' && cat.trim() !== '') {
|
||||
result.push(cat.trim());
|
||||
}
|
||||
}
|
||||
} else if (typeof app.categories === 'string') {
|
||||
const cats = app.categories.split(';').filter(c => c && c.trim() !== '');
|
||||
for (let cat of cats) {
|
||||
const trimmed = cat.trim();
|
||||
if (trimmed && !result.includes(trimmed)) {
|
||||
result.push(trimmed);
|
||||
}
|
||||
}
|
||||
} else if (app.categories.length !== undefined) {
|
||||
try {
|
||||
for (let i = 0; i < app.categories.length; i++) {
|
||||
const cat = app.categories[i];
|
||||
if (cat && cat.trim && typeof cat.trim === 'function' && cat.trim() !== '') {
|
||||
result.push(cat.trim());
|
||||
} else if (cat && typeof cat === 'string' && cat.trim() !== '') {
|
||||
result.push(cat.trim());
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (app.Categories) {
|
||||
const cats = app.Categories.split(';').filter(c => c && c.trim() !== '');
|
||||
for (let cat of cats) {
|
||||
const trimmed = cat.trim();
|
||||
if (trimmed && !result.includes(trimmed)) {
|
||||
result.push(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getAppCategory(app) {
|
||||
const appCategories = getAppCategories(app);
|
||||
if (appCategories.length === 0)
|
||||
return null;
|
||||
|
||||
const priorityCategories = ["AudioVideo", "Chat", "WebBrowser", "Game", "Development", "Graphics", "Office", "Education", "System", "Network", "Misc"];
|
||||
|
||||
for (let cat of appCategories) {
|
||||
if (cat === "AudioVideo" || cat === "Audio" || cat === "Video") {
|
||||
return "AudioVideo";
|
||||
}
|
||||
}
|
||||
|
||||
if (appCategories.includes("Chat") || appCategories.includes("InstantMessaging")) {
|
||||
return "Chat";
|
||||
}
|
||||
|
||||
if (appCategories.includes("WebBrowser")) {
|
||||
return "WebBrowser";
|
||||
}
|
||||
|
||||
// Map Science to Education
|
||||
if (appCategories.includes("Science")) {
|
||||
return "Education";
|
||||
}
|
||||
|
||||
// Map Settings to System
|
||||
if (appCategories.includes("Settings")) {
|
||||
return "System";
|
||||
}
|
||||
|
||||
// Map Utility to System
|
||||
if (appCategories.includes("Utility")) {
|
||||
return "System";
|
||||
}
|
||||
|
||||
for (let priorityCat of priorityCategories) {
|
||||
if (appCategories.includes(priorityCat) && root.categories.includes(priorityCat)) {
|
||||
return priorityCat;
|
||||
}
|
||||
}
|
||||
|
||||
return "Misc";
|
||||
}
|
||||
|
||||
// Helper function to normalize app IDs for case-insensitive matching
|
||||
function normalizeAppId(appId) {
|
||||
if (!appId || typeof appId !== 'string')
|
||||
return "";
|
||||
return appId.toLowerCase().trim();
|
||||
}
|
||||
|
||||
// Helper function to check if an app is pinned
|
||||
function isAppPinned(app) {
|
||||
if (!app)
|
||||
return false;
|
||||
const pinnedApps = Settings.data.appLauncher.pinnedApps || [];
|
||||
const appId = getAppKey(app);
|
||||
const normalizedId = normalizeAppId(appId);
|
||||
return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId);
|
||||
}
|
||||
|
||||
function appMatchesCategory(app, category) {
|
||||
// Check if app matches the selected category
|
||||
if (category === "all")
|
||||
return true;
|
||||
|
||||
// Handle Pinned category separately
|
||||
if (category === "Pinned") {
|
||||
return isAppPinned(app);
|
||||
}
|
||||
|
||||
// Get the primary category for this app (first matching standard category)
|
||||
const primaryCategory = getAppCategory(app);
|
||||
|
||||
// If app has no matching standard category, don't show it in any category (only in "all")
|
||||
if (!primaryCategory)
|
||||
return false;
|
||||
|
||||
// Map Audio/Video to AudioVideo
|
||||
if (category === "AudioVideo") {
|
||||
const appCategories = getAppCategories(app);
|
||||
// Show if app has AudioVideo, Audio, or Video
|
||||
return appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video");
|
||||
}
|
||||
|
||||
// Map Science to Education
|
||||
if (category === "Education") {
|
||||
const appCategories = getAppCategories(app);
|
||||
return appCategories.includes("Education") || appCategories.includes("Science");
|
||||
}
|
||||
|
||||
// Map Settings and Utility to System
|
||||
if (category === "System") {
|
||||
const appCategories = getAppCategories(app);
|
||||
return appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility");
|
||||
}
|
||||
|
||||
// Only show app in its primary category to avoid overlap
|
||||
// This ensures each app appears in exactly one category tab
|
||||
return category === primaryCategory;
|
||||
}
|
||||
|
||||
function getAvailableCategories() {
|
||||
const categorySet = new Set();
|
||||
let hasAudioVideo = false;
|
||||
let hasEducation = false;
|
||||
let hasSystem = false;
|
||||
let hasPinned = false;
|
||||
|
||||
// Check if there are any pinned apps
|
||||
const pinnedApps = Settings.data.appLauncher.pinnedApps || [];
|
||||
if (pinnedApps.length > 0) {
|
||||
// Verify that at least one pinned app exists in entries
|
||||
for (let app of entries) {
|
||||
if (isAppPinned(app)) {
|
||||
hasPinned = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let app of entries) {
|
||||
const appCategories = getAppCategories(app);
|
||||
const primaryCategory = getAppCategory(app);
|
||||
|
||||
if (appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video")) {
|
||||
hasAudioVideo = true;
|
||||
} else if (appCategories.includes("Education") || appCategories.includes("Science")) {
|
||||
hasEducation = true;
|
||||
} else if (appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility")) {
|
||||
hasSystem = true;
|
||||
} else if (primaryCategory && root.categories.includes(primaryCategory)) {
|
||||
categorySet.add(primaryCategory);
|
||||
}
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
// Add Pinned category first if there are pinned apps
|
||||
if (hasPinned) {
|
||||
result.push("Pinned");
|
||||
}
|
||||
|
||||
result.push("all");
|
||||
|
||||
if (hasAudioVideo) {
|
||||
categorySet.add("AudioVideo");
|
||||
}
|
||||
if (hasEducation) {
|
||||
categorySet.add("Education");
|
||||
}
|
||||
if (hasSystem) {
|
||||
categorySet.add("System");
|
||||
}
|
||||
|
||||
for (let cat of root.categories) {
|
||||
if (cat !== "all" && cat !== "Pinned" && cat !== "Misc" && categorySet.has(cat)) {
|
||||
result.push(cat);
|
||||
}
|
||||
}
|
||||
|
||||
if (categorySet.has("Misc")) {
|
||||
result.push("Misc");
|
||||
}
|
||||
|
||||
if (result.length === 1) {
|
||||
const fallback = root.categories.filter(c => c !== "Misc");
|
||||
fallback.push("Misc");
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function loadApplications() {
|
||||
if (typeof DesktopEntries === 'undefined') {
|
||||
Logger.w("ApplicationsProvider", "DesktopEntries service not available");
|
||||
return;
|
||||
}
|
||||
|
||||
const allApps = DesktopEntries.applications.values || [];
|
||||
const seen = new Map(); // Map of appId -> exec command
|
||||
|
||||
entries = allApps.filter(app => {
|
||||
if (!app || app.noDisplay || app.hidden)
|
||||
return false;
|
||||
|
||||
const appId = app.id || app.name;
|
||||
const execCmd = getExecutableName(app);
|
||||
|
||||
// Check if we've seen this app ID before
|
||||
if (seen.has(appId)) {
|
||||
const previousExec = seen.get(appId);
|
||||
|
||||
// If exec is different, it's a legitimate different entry - keep it
|
||||
if (previousExec !== execCmd) {
|
||||
Logger.d("ApplicationsProvider", `Keeping variant of ${appId}: ${execCmd} (differs from ${previousExec})`);
|
||||
// Add with modified ID to make it unique
|
||||
app.id = `${appId}_${execCmd}`;
|
||||
seen.set(app.id, execCmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Same appId AND same exec = true duplicate, skip it
|
||||
Logger.d("ApplicationsProvider", `Skipping duplicate: ${appId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
seen.set(appId, execCmd);
|
||||
return true;
|
||||
}).map(app => {
|
||||
app.executableName = getExecutableName(app);
|
||||
return app;
|
||||
});
|
||||
|
||||
Logger.d("ApplicationsProvider", `Loaded ${entries.length} applications`);
|
||||
updateAvailableCategories();
|
||||
}
|
||||
|
||||
function updateAvailableCategories() {
|
||||
availableCategories = getAvailableCategories();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings.data.appLauncher
|
||||
function onPinnedAppsChanged() {
|
||||
const wasViewingPinned = selectedCategory === "Pinned";
|
||||
updateAvailableCategories();
|
||||
|
||||
// If we were viewing Pinned category and it's no longer available, switch to "all"
|
||||
if (wasViewingPinned && !availableCategories.includes("Pinned")) {
|
||||
selectedCategory = "all";
|
||||
}
|
||||
|
||||
// Update results if we're currently viewing the Pinned category
|
||||
if (selectedCategory === "Pinned" && launcher) {
|
||||
launcher.updateResults();
|
||||
} else if (wasViewingPinned && selectedCategory === "all" && launcher) {
|
||||
// Also update results when switching to "all"
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getExecutableName(app) {
|
||||
if (!app)
|
||||
return "";
|
||||
|
||||
// Try to get executable name from command array
|
||||
if (app.command && Array.isArray(app.command) && app.command.length > 0) {
|
||||
const cmd = app.command[0];
|
||||
// Extract just the executable name from the full path
|
||||
const parts = cmd.split('/');
|
||||
const executable = parts[parts.length - 1];
|
||||
// Remove any arguments or parameters
|
||||
return executable.split(' ')[0];
|
||||
}
|
||||
|
||||
// Try to get from exec property if available
|
||||
if (app.exec) {
|
||||
const parts = app.exec.split('/');
|
||||
const executable = parts[parts.length - 1];
|
||||
return executable.split(' ')[0];
|
||||
}
|
||||
|
||||
// Fallback to app id (desktop file name without .desktop)
|
||||
if (app.id) {
|
||||
return app.id.replace('.desktop', '');
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
if (!entries || entries.length === 0)
|
||||
return [];
|
||||
|
||||
// Set category mode based on whether there's a query
|
||||
const isSearching = !!(query && query.trim() !== "");
|
||||
showsCategories = !isSearching;
|
||||
|
||||
// Filter by category only when NOT searching
|
||||
let filteredEntries = entries;
|
||||
if (!isSearching && selectedCategory && selectedCategory !== "all") {
|
||||
filteredEntries = entries.filter(app => appMatchesCategory(app, selectedCategory));
|
||||
}
|
||||
|
||||
if (!query || query.trim() === "") {
|
||||
// Return filtered apps, optionally sorted by usage
|
||||
let sorted;
|
||||
if (Settings.data.appLauncher.sortByMostUsed) {
|
||||
sorted = filteredEntries.slice().sort((a, b) => {
|
||||
// Pinned first
|
||||
const aPinned = isAppPinned(a);
|
||||
const bPinned = isAppPinned(b);
|
||||
if (aPinned !== bPinned)
|
||||
return aPinned ? -1 : 1;
|
||||
|
||||
const ua = getUsageCount(a);
|
||||
const ub = getUsageCount(b);
|
||||
if (ub !== ua)
|
||||
return ub - ua;
|
||||
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
|
||||
});
|
||||
} else {
|
||||
sorted = filteredEntries.slice().sort((a, b) => {
|
||||
const aPinned = isAppPinned(a);
|
||||
const bPinned = isAppPinned(b);
|
||||
if (aPinned !== bPinned)
|
||||
return aPinned ? -1 : 1;
|
||||
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
|
||||
});
|
||||
}
|
||||
return sorted.map(app => createResultEntry(app));
|
||||
}
|
||||
|
||||
// Use fuzzy search if available, fallback to simple search
|
||||
if (typeof FuzzySort !== 'undefined') {
|
||||
const fuzzyResults = FuzzySort.go(query, filteredEntries, {
|
||||
"keys": ["name", "comment", "genericName", "executableName"],
|
||||
"limit": 20
|
||||
});
|
||||
|
||||
// Sort pinned first within fuzzy results while preserving fuzzysort order otherwise
|
||||
const pinned = [];
|
||||
const nonPinned = [];
|
||||
for (const r of fuzzyResults) {
|
||||
const app = r.obj;
|
||||
if (isAppPinned(app))
|
||||
pinned.push(r);
|
||||
else
|
||||
nonPinned.push(r);
|
||||
}
|
||||
return pinned.concat(nonPinned).map(result => createResultEntry(result.obj, result.score));
|
||||
} else {
|
||||
// Fallback to simple search
|
||||
const searchTerm = query.toLowerCase();
|
||||
return filteredEntries.filter(app => {
|
||||
const name = (app.name || "").toLowerCase();
|
||||
const comment = (app.comment || "").toLowerCase();
|
||||
const generic = (app.genericName || "").toLowerCase();
|
||||
const executable = getExecutableName(app).toLowerCase();
|
||||
return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm) || executable.includes(searchTerm);
|
||||
}).sort((a, b) => {
|
||||
// Prioritize name matches, then executable matches
|
||||
const aName = a.name.toLowerCase();
|
||||
const bName = b.name.toLowerCase();
|
||||
const aExecutable = getExecutableName(a).toLowerCase();
|
||||
const bExecutable = getExecutableName(b).toLowerCase();
|
||||
const aStarts = aName.startsWith(searchTerm);
|
||||
const bStarts = bName.startsWith(searchTerm);
|
||||
const aExecStarts = aExecutable.startsWith(searchTerm);
|
||||
const bExecStarts = bExecutable.startsWith(searchTerm);
|
||||
|
||||
// Prioritize name matches first
|
||||
if (aStarts && !bStarts)
|
||||
return -1;
|
||||
if (!aStarts && bStarts)
|
||||
return 1;
|
||||
|
||||
// Then prioritize executable matches
|
||||
if (aExecStarts && !bExecStarts)
|
||||
return -1;
|
||||
if (!aExecStarts && bExecStarts)
|
||||
return 1;
|
||||
|
||||
return aName.localeCompare(bName);
|
||||
}).slice(0, 20).map(app => createResultEntry(app));
|
||||
}
|
||||
}
|
||||
|
||||
function createResultEntry(app, score) {
|
||||
return {
|
||||
"appId": getAppKey(app),
|
||||
"usageKey": getAppKey(app),
|
||||
"name": app.name || "Unknown",
|
||||
"description": app.genericName || app.comment || "",
|
||||
"icon": app.icon || "application-x-executable",
|
||||
"isImage": false,
|
||||
"_score": (score !== undefined ? score : 0),
|
||||
"provider": root,
|
||||
"onActivate": function () {
|
||||
// Close the launcher/SmartPanel immediately without any animations.
|
||||
// Ensures we are not preventing the future focusing of the app
|
||||
launcher.closeImmediately();
|
||||
|
||||
// Defer execution to next event loop iteration to ensure panel is fully closed
|
||||
Qt.callLater(() => {
|
||||
Logger.d("ApplicationsProvider", `Launching: ${app.name} (App ID: ${app.id || "unknown"})`);
|
||||
|
||||
const execString = (app.exec !== undefined && app.exec !== null) ? String(app.exec) : "";
|
||||
const commandArgs = Array.isArray(app.command) ? app.command : (app.command && app.command.length !== undefined) ? Array.from(app.command) : [];
|
||||
let hasQuotedArgs = execString.includes("\"") || execString.includes("'");
|
||||
let hasSpaceArgs = false;
|
||||
if (!hasQuotedArgs) {
|
||||
hasQuotedArgs = commandArgs.some(arg => {
|
||||
const text = String(arg);
|
||||
return text.includes("\"") || text.includes("'");
|
||||
});
|
||||
}
|
||||
if (!hasSpaceArgs) {
|
||||
hasSpaceArgs = commandArgs.some(arg => String(arg).includes(" "));
|
||||
}
|
||||
if (app.execute && (hasQuotedArgs || hasSpaceArgs)) {
|
||||
Logger.d("ApplicationsProvider", `Detected quoted/space arguments in Exec for ${app.name}, using app.execute()`);
|
||||
app.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings.data.appLauncher.customLaunchPrefixEnabled && Settings.data.appLauncher.customLaunchPrefix) {
|
||||
// Use custom launch prefix
|
||||
const prefix = Settings.data.appLauncher.customLaunchPrefix.split(" ");
|
||||
Logger.d("ApplicationsProvider", `Using custom launch prefix: ${Settings.data.appLauncher.customLaunchPrefix}`);
|
||||
|
||||
if (app.runInTerminal) {
|
||||
const terminal = Settings.data.appLauncher.terminalCommand.split(" ");
|
||||
const command = prefix.concat(terminal.concat(app.command));
|
||||
Logger.d("ApplicationsProvider", `Executing command (with prefix and terminal): ${command.join(" ")}`);
|
||||
Quickshell.execDetached(command);
|
||||
} else {
|
||||
const command = prefix.concat(app.command);
|
||||
Logger.d("ApplicationsProvider", `Executing command (with prefix): ${command.join(" ")}`);
|
||||
Quickshell.execDetached(command);
|
||||
}
|
||||
} else {
|
||||
if (app.runInTerminal) {
|
||||
Logger.d("ApplicationsProvider", "Executing terminal app manually: " + app.name);
|
||||
const terminal = Settings.data.appLauncher.terminalCommand.split(" ");
|
||||
const command = terminal.concat(app.command);
|
||||
Logger.d("ApplicationsProvider", "Executing command (manual terminal): " + command.join(" "));
|
||||
CompositorService.spawn(command);
|
||||
} else if (app.command && app.command.length > 0) {
|
||||
Logger.d("ApplicationsProvider", "Executing command: " + app.command.join(" "));
|
||||
CompositorService.spawn(app.command);
|
||||
} else if (app.execute) {
|
||||
Logger.d("ApplicationsProvider", "Calling app.execute() for: " + app.name);
|
||||
app.execute();
|
||||
} else {
|
||||
Logger.w("ApplicationsProvider", `Could not launch: ${app.name}. No valid launch method.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// Item actions for launcher delegate
|
||||
function getItemActions(item) {
|
||||
if (!item || !item.appId)
|
||||
return [];
|
||||
return [
|
||||
{
|
||||
"icon": isAppPinned({
|
||||
"id": item.appId
|
||||
}) ? "unpin" : "pin",
|
||||
"tooltip": isAppPinned({
|
||||
"id": item.appId
|
||||
}) ? I18n.tr("common.unpin") : I18n.tr("common.pin"),
|
||||
"action": function () {
|
||||
togglePin(item.appId);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function togglePin(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
const normalizedId = normalizeAppId(appId);
|
||||
let arr = (Settings.data.appLauncher.pinnedApps || []).slice();
|
||||
const idx = arr.findIndex(pinnedId => normalizeAppId(pinnedId) === normalizedId);
|
||||
if (idx >= 0)
|
||||
arr.splice(idx, 1);
|
||||
else
|
||||
arr.push(appId);
|
||||
Settings.data.appLauncher.pinnedApps = arr;
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// Usage tracking helpers
|
||||
function getAppKey(app) {
|
||||
if (app && app.id)
|
||||
return String(app.id);
|
||||
if (app && app.command && app.command.join)
|
||||
return app.command.join(" ");
|
||||
return String(app && app.name ? app.name : "unknown");
|
||||
}
|
||||
|
||||
function getUsageCount(app) {
|
||||
return ShellState.getLauncherUsageCount(getAppKey(app));
|
||||
}
|
||||
|
||||
// Migrate legacy command-based usage keys to canonical app-id keys at startup
|
||||
function migrateLegacyUsageKeys() {
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const app = entries[i];
|
||||
if (app && app.id && app.command && app.command.join) {
|
||||
const key = getAppKey(app);
|
||||
const legacyKey = app.command.join(" ");
|
||||
if (legacyKey !== key && ShellState.getLauncherUsageCount(legacyKey) > 0) {
|
||||
ShellState.migrateLauncherUsage(legacyKey, key);
|
||||
Logger.d("ApplicationsProvider", `Migrated usage: "${legacyKey}" → "${key}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "../../../../Helpers/AdvancedMath.js" as AdvancedMath
|
||||
import qs.Commons
|
||||
import qs.Services.Keyboard
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Provider metadata
|
||||
property string name: I18n.tr("launcher.providers.calculator")
|
||||
property var launcher: null
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
property bool handleSearch: true // Contribute to regular search
|
||||
property string supportedLayouts: "list"
|
||||
|
||||
// Initialize provider
|
||||
function init() {
|
||||
Logger.d("CalculatorProvider", "Initialized");
|
||||
}
|
||||
|
||||
// Get search results - evaluates math expressions inline
|
||||
function getResults(query) {
|
||||
if (!query)
|
||||
return [];
|
||||
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed || !isMathExpression(trimmed))
|
||||
return [];
|
||||
|
||||
try {
|
||||
const result = AdvancedMath.evaluate(trimmed);
|
||||
const formattedResult = AdvancedMath.formatResult(result);
|
||||
return [
|
||||
{
|
||||
"name": formattedResult,
|
||||
"description": I18n.tr("launcher.providers.calculator-press-enter-to-copy"),
|
||||
"icon": iconMode === "tabler" ? "calculator" : "accessories-calculator",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"provider": root,
|
||||
"onActivate": function () {
|
||||
// Copy result to clipboard via xclip
|
||||
Quickshell.execDetached(["sh", "-c", "echo -n '" + formattedResult.replace(/'/g, "'\\''") + "' | wl-copy"]);
|
||||
ToastService.showNotice(I18n.tr("common.copied-to-clipboard"), formattedResult);
|
||||
if (launcher)
|
||||
launcher.close();
|
||||
}
|
||||
}
|
||||
];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a string is a valid math expression
|
||||
function isMathExpression(expr) {
|
||||
// Allow: digits, operators, parentheses, decimal points, whitespace, letters (for functions), commas
|
||||
if (!/^[\d\s\+\-\*\/\(\)\.\%\^a-zA-Z,]+$/.test(expr))
|
||||
return false;
|
||||
|
||||
// Must contain at least one operator OR a function call (letter followed by parenthesis)
|
||||
if (!/[+\-*/%\^]/.test(expr) && !/[a-zA-Z]\s*\(/.test(expr))
|
||||
return false;
|
||||
|
||||
// Reject if ends with an operator (incomplete expression)
|
||||
if (/[+\-*/%\^]\s*$/.test(expr))
|
||||
return false;
|
||||
|
||||
// Reject if it's just letters (would match app names)
|
||||
if (/^[a-zA-Z\s]+$/.test(expr))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Keyboard
|
||||
import qs.Services.Noctalia
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Provider metadata
|
||||
property string name: I18n.tr("launcher.providers.clipboard")
|
||||
property var launcher: null
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
property string supportedLayouts: "list" // List view for clipboard content
|
||||
property bool wrapNavigation: false // Don't wrap at end of list
|
||||
|
||||
// Provider capabilities
|
||||
property bool handleSearch: false // Don't handle regular search
|
||||
|
||||
// Preview support
|
||||
property bool hasPreview: Settings.data.appLauncher.enableClipPreview
|
||||
property string previewComponentPath: "./ClipboardPreview.qml"
|
||||
|
||||
// Image handling - expose revision for reactive updates in delegates
|
||||
readonly property int imageRevision: ClipboardService.revision
|
||||
|
||||
// Categories
|
||||
property var availableCategories: Settings.data.appLauncher.enableClipboardChips ? ["All", "Images", "Links", "Files", "Code", "Colors"] : []
|
||||
property string selectedCategory: "All"
|
||||
|
||||
function selectCategory(cat) {
|
||||
if (selectedCategory !== cat) {
|
||||
selectedCategory = cat;
|
||||
if (launcher) {
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var categoryIcons: {
|
||||
"All": iconMode === "tabler" ? "border-all" : "view-grid",
|
||||
"Images": iconMode === "tabler" ? "photo" : "image",
|
||||
"Links": iconMode === "tabler" ? "link" : "insert-link",
|
||||
"Files": iconMode === "tabler" ? "file" : "text-x-generic",
|
||||
"Code": iconMode === "tabler" ? "code" : "text-x-script",
|
||||
"Colors": iconMode === "tabler" ? "palette" : "color-picker"
|
||||
}
|
||||
|
||||
// Internal state
|
||||
property bool isWaitingForData: false
|
||||
property bool gotResults: false
|
||||
property string lastSearchText: ""
|
||||
|
||||
// Listen for clipboard data updates
|
||||
Connections {
|
||||
target: ClipboardService
|
||||
function onListCompleted() {
|
||||
if (gotResults && (lastSearchText === searchText)) {
|
||||
// Do not update results after the first fetch.
|
||||
// This will avoid the list resetting every 2seconds when the service updates.
|
||||
return;
|
||||
}
|
||||
// Refresh results if we're waiting for data or if clipboard plugin is active
|
||||
if (isWaitingForData || (launcher && launcher.searchText.startsWith(">clip"))) {
|
||||
isWaitingForData = false;
|
||||
gotResults = true;
|
||||
if (launcher) {
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
function onActiveChanged() {
|
||||
// When active state changes (e.g. dependency check completes), refresh results
|
||||
if (ClipboardService.active && launcher && launcher.searchText.startsWith(">clip")) {
|
||||
isWaitingForData = true;
|
||||
gotResults = false;
|
||||
ClipboardService.list(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize provider
|
||||
function init() {
|
||||
Logger.d("ClipboardProvider", "Initialized");
|
||||
// Pre-load clipboard data if service is active
|
||||
if (ClipboardService.active) {
|
||||
ClipboardService.list(100);
|
||||
}
|
||||
}
|
||||
|
||||
// Called when launcher opens
|
||||
function onOpened() {
|
||||
isWaitingForData = true;
|
||||
gotResults = false;
|
||||
lastSearchText = "";
|
||||
|
||||
// Refresh clipboard history when launcher opens
|
||||
if (ClipboardService.active) {
|
||||
ClipboardService.list(100);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this provider handles the command
|
||||
function handleCommand(searchText) {
|
||||
return searchText.startsWith(">clip");
|
||||
}
|
||||
|
||||
// Return available commands when user types ">"
|
||||
function commands() {
|
||||
return [
|
||||
{
|
||||
"name": ">clip",
|
||||
"description": I18n.tr("launcher.providers.clipboard-search-description"),
|
||||
"icon": iconMode === "tabler" ? "clipboard" : "diodon",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.setSearchText(">clip ");
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": ">clip clear",
|
||||
"description": I18n.tr("launcher.providers.clipboard-clear-description"),
|
||||
"icon": iconMode === "tabler" ? "trash" : "user-trash",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
ClipboardService.wipeAll();
|
||||
launcher.close();
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Get search results
|
||||
function getResults(searchText) {
|
||||
if (!searchText.startsWith(">clip")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
lastSearchText = searchText;
|
||||
const results = [];
|
||||
const query = searchText.slice(5).trim();
|
||||
|
||||
// Check if clipboard service is not active
|
||||
if (!ClipboardService.active) {
|
||||
// If dependency check hasn't completed yet, show loading instead of disabled
|
||||
if (!ClipboardService.dependencyChecked) {
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("launcher.providers.clipboard-loading"),
|
||||
"description": I18n.tr("launcher.providers.emoji-loading-description"),
|
||||
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("launcher.providers.clipboard-history-disabled"),
|
||||
"description": I18n.tr("launcher.providers.clipboard-history-disabled-description"),
|
||||
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Special command: clear
|
||||
if (query === "clear") {
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("launcher.providers.clipboard-clear-history"),
|
||||
"description": I18n.tr("launcher.providers.clipboard-clear-description-full"),
|
||||
"icon": iconMode === "tabler" ? "trash" : "user-trash",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
ClipboardService.wipeAll();
|
||||
launcher.close();
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Show loading state if data is being loaded
|
||||
if (ClipboardService.loading || isWaitingForData) {
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("launcher.providers.clipboard-loading"),
|
||||
"description": I18n.tr("launcher.providers.emoji-loading-description"),
|
||||
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Get clipboard items
|
||||
const items = ClipboardService.items || [];
|
||||
|
||||
// If no items and we haven't tried loading yet, trigger a load
|
||||
if (items.count === 0 && !ClipboardService.loading) {
|
||||
isWaitingForData = true;
|
||||
ClipboardService.list(100);
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("launcher.providers.clipboard-loading"),
|
||||
"description": I18n.tr("launcher.providers.emoji-loading-description"),
|
||||
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Search clipboard items
|
||||
const searchTerm = query.toLowerCase();
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
|
||||
const catMap = {
|
||||
"Images": "image",
|
||||
"Links": "link",
|
||||
"Files": "file",
|
||||
"Code": "code",
|
||||
"Colors": "color"
|
||||
};
|
||||
|
||||
// Filter and format results
|
||||
items.forEach(function (item) {
|
||||
// Category filter
|
||||
if (Settings.data.appLauncher.enableClipboardChips && root.selectedCategory !== "All") {
|
||||
if (item.contentType !== catMap[root.selectedCategory]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const preview = (item.preview || "").toLowerCase();
|
||||
|
||||
// Skip if search term doesn't match
|
||||
if (searchTerm && preview.indexOf(searchTerm) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstSeen = ClipboardService.firstSeenById[item.id] || now;
|
||||
|
||||
// Format the result based on type
|
||||
let entry;
|
||||
if (item.isImage) {
|
||||
entry = formatImageEntry(item, firstSeen);
|
||||
} else {
|
||||
entry = formatTextEntry(item, firstSeen);
|
||||
}
|
||||
|
||||
// Add activation handler
|
||||
entry.onActivate = function () {
|
||||
if (Settings.data.appLauncher.autoPasteClipboard) {
|
||||
launcher.closeImmediately();
|
||||
Qt.callLater(() => {
|
||||
ClipboardService.pasteFromClipboard(item.id, item.mime);
|
||||
});
|
||||
} else {
|
||||
ClipboardService.copyToClipboard(item.id);
|
||||
launcher.close();
|
||||
}
|
||||
};
|
||||
|
||||
results.push(entry);
|
||||
});
|
||||
|
||||
// Show empty state if no results
|
||||
if (results.length === 0) {
|
||||
results.push({
|
||||
"name": searchTerm ? "No matching clipboard items" : "Clipboard is empty",
|
||||
"description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here",
|
||||
"icon": iconMode === "tabler" ? "clipboard" : "text-x-generic",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {// Do nothing
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Logger.i("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`)
|
||||
return results;
|
||||
}
|
||||
|
||||
function formatImageEntry(item, firstSeen) {
|
||||
const meta = ClipboardService.parseImageMeta(item.preview);
|
||||
const timeStr = Time.formatRelativeTime(new Date(firstSeen * 1000));
|
||||
let desc = meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data";
|
||||
if (timeStr)
|
||||
desc += ` • ${timeStr}`;
|
||||
|
||||
return {
|
||||
"name": meta ? `Image ${meta.w}×${meta.h}` : "Image",
|
||||
"description": desc,
|
||||
"icon": iconMode === "tabler" ? "photo" : "image",
|
||||
"isTablerIcon": true,
|
||||
"isImage": true,
|
||||
"imageWidth": meta ? meta.w : 0,
|
||||
"imageHeight": meta ? meta.h : 0,
|
||||
"clipboardId": item.id,
|
||||
"mime": item.mime,
|
||||
"preview": item.preview,
|
||||
"provider": root
|
||||
};
|
||||
}
|
||||
|
||||
function formatTextEntry(item, firstSeen) {
|
||||
const preview = (item.preview || "").trim();
|
||||
const lines = preview.split('\n').filter(l => l.trim());
|
||||
|
||||
let title = lines[0] || "Empty text";
|
||||
if (title.length > 60) {
|
||||
title = title.substring(0, 57) + "...";
|
||||
}
|
||||
|
||||
let description = "";
|
||||
if (lines.length > 1) {
|
||||
description = lines[1];
|
||||
if (description.length > 80) {
|
||||
description = description.substring(0, 77) + "...";
|
||||
}
|
||||
} else {
|
||||
// Preview is truncated at ~100 chars, so we can't show exact count
|
||||
if (preview.length >= 100) {
|
||||
description = I18n.tr("toast.clipboard.long-text");
|
||||
} else {
|
||||
const chars = preview.length;
|
||||
const words = preview.split(/\s+/).length;
|
||||
description = `${chars} characters, ${words} word${words !== 1 ? 's' : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
const timeStr = Time.formatRelativeTime(new Date(firstSeen * 1000));
|
||||
if (timeStr)
|
||||
description += ` • ${timeStr}`;
|
||||
|
||||
let defaultIcon = iconMode === "tabler" ? "clipboard" : "text-x-generic";
|
||||
let colorHex = "";
|
||||
if (Settings.data.appLauncher.enableClipboardSmartIcons) {
|
||||
if (item.contentType === "link")
|
||||
defaultIcon = iconMode === "tabler" ? "link" : "insert-link";
|
||||
else if (item.contentType === "file")
|
||||
defaultIcon = iconMode === "tabler" ? "file" : "text-x-generic";
|
||||
else if (item.contentType === "code")
|
||||
defaultIcon = iconMode === "tabler" ? "code" : "text-x-script";
|
||||
else if (item.contentType === "color") {
|
||||
defaultIcon = iconMode === "tabler" ? "palette" : "color-picker";
|
||||
colorHex = preview;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"name": title,
|
||||
"description": description,
|
||||
"icon": defaultIcon,
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"clipboardId": item.id,
|
||||
"preview": preview,
|
||||
"contentType": item.contentType,
|
||||
"colorHex": colorHex,
|
||||
"provider": root
|
||||
};
|
||||
}
|
||||
|
||||
function getImageForItem(clipboardId) {
|
||||
return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null;
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// Item actions for launcher delegate
|
||||
function getItemActions(item) {
|
||||
if (!item || !item.clipboardId)
|
||||
return [];
|
||||
|
||||
var actions = [];
|
||||
|
||||
// Annotation tool for images
|
||||
if (item.isImage && Settings.data.appLauncher.screenshotAnnotationTool !== "") {
|
||||
actions.push({
|
||||
"icon": "pencil",
|
||||
"tooltip": I18n.tr("tooltips.open-annotation-tool"),
|
||||
"action": function () {
|
||||
var tool = Settings.data.appLauncher.screenshotAnnotationTool;
|
||||
Quickshell.execDetached(["sh", "-c", "cliphist decode " + item.clipboardId + " | " + tool]);
|
||||
if (launcher)
|
||||
launcher.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Delete action
|
||||
actions.push({
|
||||
"icon": "trash",
|
||||
"tooltip": I18n.tr("launcher.providers.clipboard-delete"),
|
||||
"action": function () {
|
||||
deleteItem(item);
|
||||
}
|
||||
});
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
function canDeleteItem(item) {
|
||||
return item && !!item.clipboardId;
|
||||
}
|
||||
|
||||
function deleteItem(item) {
|
||||
if (!item || !item.clipboardId)
|
||||
return;
|
||||
|
||||
// Set provider state before deletion so refresh works
|
||||
gotResults = false;
|
||||
isWaitingForData = true;
|
||||
lastSearchText = launcher ? launcher.searchText : "";
|
||||
|
||||
// Delete the item
|
||||
ClipboardService.deleteById(String(item.clipboardId));
|
||||
}
|
||||
|
||||
// Prepare item for display (handles image decoding)
|
||||
function prepareItem(item) {
|
||||
if (item && item.isImage && item.clipboardId) {
|
||||
if (!ClipboardService.getImageData(item.clipboardId)) {
|
||||
ClipboardService.decodeToDataUrl(item.clipboardId, item.mime, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get image URL for item (used by delegates)
|
||||
function getImageUrl(item) {
|
||||
if (!item || !item.clipboardId)
|
||||
return "";
|
||||
return ClipboardService.getImageData(item.clipboardId) || "";
|
||||
}
|
||||
|
||||
// Get preview data for the preview panel
|
||||
function getPreviewData(item) {
|
||||
if (!item)
|
||||
return null;
|
||||
return {
|
||||
"clipboardId": item.clipboardId,
|
||||
"isImage": item.isImage,
|
||||
"mime": item.mime,
|
||||
"preview": item.preview
|
||||
};
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
property var launcher: null
|
||||
property string name: I18n.tr("common.command")
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
|
||||
function handleCommand(query) {
|
||||
return query.startsWith(">cmd");
|
||||
}
|
||||
|
||||
function commands() {
|
||||
return [
|
||||
{
|
||||
"name": ">cmd",
|
||||
"description": I18n.tr("launcher.providers.command-description"),
|
||||
"icon": iconMode === "tabler" ? "terminal" : "utilities-terminal",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.setSearchText(">cmd ");
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
if (!query.startsWith(">cmd"))
|
||||
return [];
|
||||
|
||||
let expression = query.substring(4).trim();
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("common.command"),
|
||||
"description": I18n.tr("launcher.providers.command-description"),
|
||||
"icon": iconMode === "tabler" ? "terminal" : "utilities-terminal",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.closeImmediately();
|
||||
Qt.callLater(() => {
|
||||
Logger.d("CommandProvider", "Executing shell command: " + expression);
|
||||
Quickshell.execDetached(["sh", "-c", expression]);
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Keyboard
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Provider metadata
|
||||
property string name: I18n.tr("launcher.providers.emoji")
|
||||
property var launcher: null
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
property bool handleSearch: false
|
||||
property string supportedLayouts: "grid" // Only grid layout for emoji
|
||||
property int preferredGridColumns: 7 // More columns for compact emoji display
|
||||
property real preferredGridCellRatio: 1.15 // Slightly taller than wide to accommodate label
|
||||
property bool supportsAutoPaste: true // Emoji can be auto-pasted
|
||||
property bool ignoreDensity: false // Emoji should scale with launcher density
|
||||
|
||||
property string selectedCategory: "recent"
|
||||
property bool showsCategories: true // Default to showing categories
|
||||
|
||||
// Empty state message for category view
|
||||
readonly property string emptyBrowsingMessage: selectedCategory === "recent" ? I18n.tr("launcher.providers.emoji-no-recent") : ""
|
||||
|
||||
property var categoryIcons: ({
|
||||
"all": "apps",
|
||||
"recent": "clock",
|
||||
"people": "user",
|
||||
"animals": "paw",
|
||||
"nature": "leaf",
|
||||
"food": "apple",
|
||||
"activity": "run",
|
||||
"travel": "plane",
|
||||
"objects": "home",
|
||||
"symbols": "star",
|
||||
"flags": "flag"
|
||||
})
|
||||
|
||||
property var categories: ["all", "recent", "people", "animals", "nature", "food", "activity", "travel", "objects", "symbols", "flags"]
|
||||
|
||||
function getCategoryName(category) {
|
||||
const names = {
|
||||
"all": I18n.tr("launcher.categories.all"),
|
||||
"recent": I18n.tr("launcher.categories.emoji-recent"),
|
||||
"people": I18n.tr("launcher.categories.emoji-people"),
|
||||
"animals": I18n.tr("launcher.categories.emoji-animals"),
|
||||
"nature": I18n.tr("launcher.categories.emoji-nature"),
|
||||
"food": I18n.tr("launcher.categories.emoji-food"),
|
||||
"activity": I18n.tr("launcher.categories.emoji-activity"),
|
||||
"travel": I18n.tr("launcher.categories.emoji-travel"),
|
||||
"objects": I18n.tr("launcher.categories.emoji-objects"),
|
||||
"symbols": I18n.tr("launcher.categories.emoji-symbols"),
|
||||
"flags": I18n.tr("launcher.categories.emoji-flags")
|
||||
};
|
||||
return names[category] || category;
|
||||
}
|
||||
|
||||
// Force update results when emoji service loads
|
||||
Connections {
|
||||
target: EmojiService
|
||||
function onLoadedChanged() {
|
||||
if (EmojiService.loaded && root.launcher) {
|
||||
root.launcher.updateResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize provider
|
||||
function init() {
|
||||
Logger.d("EmojiProvider", "Initialized");
|
||||
}
|
||||
|
||||
function selectCategory(category) {
|
||||
selectedCategory = category;
|
||||
if (launcher) {
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
|
||||
function onOpened() {
|
||||
// Always reset to "recent" category when opening
|
||||
selectedCategory = "recent";
|
||||
}
|
||||
|
||||
// Check if this provider handles the command
|
||||
function handleCommand(searchText) {
|
||||
return searchText.startsWith(">emoji");
|
||||
}
|
||||
|
||||
// Return available commands when user types ">"
|
||||
function commands() {
|
||||
return [
|
||||
{
|
||||
"name": ">emoji",
|
||||
"description": I18n.tr("launcher.providers.emoji-search-description"),
|
||||
"icon": iconMode === "tabler" ? "mood-smile" : "face-smile",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.setSearchText(">emoji ");
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Get search results
|
||||
function getResults(searchText) {
|
||||
if (!searchText.startsWith(">emoji")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!EmojiService.loaded) {
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("launcher.providers.emoji-loading"),
|
||||
"description": I18n.tr("launcher.providers.emoji-loading-description"),
|
||||
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
var query = searchText.slice(6).trim();
|
||||
var emojis = [];
|
||||
|
||||
if (query !== "" || selectedCategory === "all") {
|
||||
emojis = EmojiService.search(query);
|
||||
} else {
|
||||
emojis = EmojiService.getEmojisByCategory(selectedCategory);
|
||||
}
|
||||
return emojis.map(formatEmojiEntry);
|
||||
}
|
||||
|
||||
// Format an emoji entry for the results list
|
||||
function formatEmojiEntry(emoji) {
|
||||
let title = emoji.name;
|
||||
let description = emoji.keywords.join(", ");
|
||||
|
||||
if (emoji.category) {
|
||||
description += " • Category: " + emoji.category;
|
||||
}
|
||||
|
||||
const emojiChar = emoji.emoji;
|
||||
|
||||
return {
|
||||
"name": title,
|
||||
"description": description,
|
||||
"icon": null,
|
||||
"isImage": false,
|
||||
"displayString": emojiChar,
|
||||
"autoPasteText": emojiChar,
|
||||
"provider": root,
|
||||
"onAutoPaste": function () {
|
||||
EmojiService.recordUsage(emojiChar);
|
||||
},
|
||||
"onActivate": function () {
|
||||
EmojiService.copy(emojiChar);
|
||||
launcher.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Provider metadata
|
||||
property string name: I18n.tr("tooltips.session-menu")
|
||||
property var launcher: null
|
||||
property bool handleSearch: Settings.data.appLauncher.enableSessionSearch
|
||||
property string supportedLayouts: "list"
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
|
||||
// Session actions with search keywords
|
||||
readonly property var sessionActions: [
|
||||
{
|
||||
"action": "lock",
|
||||
"labelKey": "common.lock",
|
||||
"icon": iconMode === "tabler" ? "lock" : "system-lock-screen",
|
||||
"keywords": ["lock", "screen", "secure"]
|
||||
},
|
||||
{
|
||||
"action": "suspend",
|
||||
"labelKey": "common.suspend",
|
||||
"icon": iconMode === "tabler" ? "suspend" : "system-suspend",
|
||||
"keywords": ["suspend", "sleep", "standby"]
|
||||
},
|
||||
{
|
||||
"action": "hibernate",
|
||||
"labelKey": "common.hibernate",
|
||||
"icon": iconMode === "tabler" ? "hibernate" : "system-suspend-hibernate",
|
||||
"keywords": ["hibernate", "disk"]
|
||||
},
|
||||
{
|
||||
"action": "reboot",
|
||||
"labelKey": "common.reboot",
|
||||
"icon": iconMode === "tabler" ? "reboot" : "system-reboot",
|
||||
"keywords": ["reboot", "restart", "reload"]
|
||||
},
|
||||
{
|
||||
"action": "rebootToUefi",
|
||||
"labelKey": "common.reboot-to-uefi",
|
||||
"icon": iconMode === "tabler" ? "reboot" : "system-reboot",
|
||||
"keywords": ["reboot", "uefi", "firmware", "bios"]
|
||||
},
|
||||
{
|
||||
"action": "userspaceReboot",
|
||||
"labelKey": "common.userspace-reboot",
|
||||
"icon": iconMode === "tabler" ? "rotate" : "system-reboot",
|
||||
"keywords": ["reboot", "restart", "soft", "userspace"]
|
||||
},
|
||||
{
|
||||
"action": "logout",
|
||||
"labelKey": "common.logout",
|
||||
"icon": iconMode === "tabler" ? "logout" : "system-log-out",
|
||||
"keywords": ["logout", "sign out", "exit", "leave"]
|
||||
},
|
||||
{
|
||||
"action": "shutdown",
|
||||
"labelKey": "common.shutdown",
|
||||
"icon": iconMode === "tabler" ? "shutdown" : "system-shutdown",
|
||||
"keywords": ["shutdown", "power off", "turn off", "poweroff"]
|
||||
}
|
||||
]
|
||||
|
||||
function init() {
|
||||
Logger.d("SessionProvider", "Initialized");
|
||||
}
|
||||
|
||||
function getEnabledActions() {
|
||||
var powerOptions = Settings.data.sessionMenu.powerOptions || [];
|
||||
var enabledSet = {};
|
||||
|
||||
for (var i = 0; i < powerOptions.length; i++) {
|
||||
if (powerOptions[i].enabled) {
|
||||
enabledSet[powerOptions[i].action] = powerOptions[i];
|
||||
}
|
||||
}
|
||||
|
||||
var enabled = [];
|
||||
for (var j = 0; j < sessionActions.length; j++) {
|
||||
var action = sessionActions[j];
|
||||
if (enabledSet[action.action]) {
|
||||
enabled.push({
|
||||
"action": action.action,
|
||||
"labelKey": action.labelKey,
|
||||
"icon": action.icon,
|
||||
"keywords": action.keywords,
|
||||
"command": enabledSet[action.action].command || ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return enabled;
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
if (!query)
|
||||
return [];
|
||||
|
||||
var trimmed = query.trim();
|
||||
if (!trimmed || trimmed.length < 2)
|
||||
return [];
|
||||
|
||||
var enabledActions = getEnabledActions();
|
||||
if (enabledActions.length === 0)
|
||||
return [];
|
||||
|
||||
// Build searchable items with resolved translations
|
||||
var items = [];
|
||||
for (var i = 0; i < enabledActions.length; i++) {
|
||||
var action = enabledActions[i];
|
||||
var label = I18n.tr(action.labelKey);
|
||||
items.push({
|
||||
"action": action.action,
|
||||
"icon": action.icon,
|
||||
"label": label,
|
||||
"command": action.command,
|
||||
"searchText": [label.toLowerCase()].concat(action.keywords).join(" ")
|
||||
});
|
||||
}
|
||||
|
||||
var results = FuzzySort.go(trimmed, items, {
|
||||
"keys": ["label", "searchText"],
|
||||
"limit": 6
|
||||
});
|
||||
|
||||
var launcherItems = [];
|
||||
for (var j = 0; j < results.length; j++) {
|
||||
var entry = results[j].obj;
|
||||
var score = results[j].score;
|
||||
|
||||
launcherItems.push({
|
||||
"name": entry.label,
|
||||
"description": I18n.tr("tooltips.session-menu"),
|
||||
"icon": entry.icon,
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"_score": score - 1,
|
||||
"provider": root,
|
||||
"onActivate": createActivateHandler(entry.action, entry.command)
|
||||
});
|
||||
}
|
||||
|
||||
return launcherItems;
|
||||
}
|
||||
|
||||
function createActivateHandler(action, command) {
|
||||
return function () {
|
||||
if (launcher)
|
||||
launcher.close();
|
||||
|
||||
// Execute via Qt.callLater, but reference only singletons
|
||||
// (root may be destroyed after launcher.close() unloads the panel)
|
||||
Qt.callLater(() => {
|
||||
switch (action) {
|
||||
case "lock":
|
||||
if (PanelService.lockScreen && !PanelService.lockScreen.active) {
|
||||
PanelService.lockScreen.active = true;
|
||||
}
|
||||
break;
|
||||
case "suspend":
|
||||
if (Settings.data.general.lockOnSuspend) {
|
||||
CompositorService.lockAndSuspend();
|
||||
} else {
|
||||
CompositorService.suspend();
|
||||
}
|
||||
break;
|
||||
case "hibernate":
|
||||
CompositorService.hibernate();
|
||||
break;
|
||||
case "reboot":
|
||||
CompositorService.reboot();
|
||||
break;
|
||||
case "rebootToUefi":
|
||||
CompositorService.rebootToUefi();
|
||||
break;
|
||||
case "userspaceReboot":
|
||||
CompositorService.userspaceReboot();
|
||||
break;
|
||||
case "logout":
|
||||
CompositorService.logout();
|
||||
break;
|
||||
case "shutdown":
|
||||
CompositorService.shutdown();
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Provider metadata
|
||||
property string name: I18n.tr("common.settings")
|
||||
property var launcher: null
|
||||
property bool handleSearch: Settings.data.appLauncher.enableSettingsSearch
|
||||
property string supportedLayouts: "list"
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
|
||||
function init() {
|
||||
Logger.d("SettingsProvider", "Initialized");
|
||||
}
|
||||
|
||||
// Check if this provider handles the command
|
||||
function handleCommand(searchText) {
|
||||
return searchText.startsWith(">settings");
|
||||
}
|
||||
|
||||
// Return available commands when user types ">"
|
||||
function commands() {
|
||||
return [
|
||||
{
|
||||
"name": ">settings",
|
||||
"description": I18n.tr("launcher.providers.settings-search-description"),
|
||||
"icon": iconMode === "tabler" ? "settings" : "preferences-system",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.setSearchText(">settings ");
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
if (!query || SettingsSearchService.searchIndex.length === 0)
|
||||
return [];
|
||||
|
||||
var trimmed = query.trim();
|
||||
|
||||
// Handle command mode: ">settings" or ">settings <search>"
|
||||
var isCommandMode = trimmed.startsWith(">settings");
|
||||
if (isCommandMode) {
|
||||
// Extract search term after ">settings "
|
||||
var searchTerm = trimmed.substring(9).trim();
|
||||
// In command mode, show all settings if no search term
|
||||
if (searchTerm.length === 0) {
|
||||
return getAllSettings();
|
||||
}
|
||||
trimmed = searchTerm;
|
||||
} else {
|
||||
// Regular search mode - require at least 2 chars
|
||||
if (!trimmed || trimmed.length < 2)
|
||||
return [];
|
||||
}
|
||||
|
||||
// Build searchable items with resolved translations, filtering out invisible entries
|
||||
let items = [];
|
||||
for (let j = 0; j < SettingsSearchService.searchIndex.length; j++) {
|
||||
const entry = SettingsSearchService.searchIndex[j];
|
||||
if (!SettingsSearchService.isEntryVisible(entry))
|
||||
continue;
|
||||
items.push({
|
||||
"labelKey": entry.labelKey,
|
||||
"descriptionKey": entry.descriptionKey,
|
||||
"widget": entry.widget,
|
||||
"tab": entry.tab,
|
||||
"tabLabel": entry.tabLabel,
|
||||
"subTab": entry.subTab,
|
||||
"subTabLabel": entry.subTabLabel || null,
|
||||
"label": I18n.tr(entry.labelKey),
|
||||
"description": entry.descriptionKey ? I18n.tr(entry.descriptionKey) : "",
|
||||
"subTabName": entry.subTabLabel ? I18n.tr(entry.subTabLabel) : ""
|
||||
});
|
||||
}
|
||||
|
||||
const results = FuzzySort.go(trimmed, items, {
|
||||
"keys": ["label", "subTabName", "description"],
|
||||
"limit": 10,
|
||||
"scoreFn": function (r) {
|
||||
const labelScore = r[0].score;
|
||||
const subTabScore = r[1].score * 1.5;
|
||||
const descScore = r[2].score;
|
||||
return Math.max(labelScore, subTabScore, descScore);
|
||||
}
|
||||
});
|
||||
|
||||
let launcherItems = [];
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const entry = results[i].obj;
|
||||
const score = results[i].score;
|
||||
const tabName = I18n.tr(entry.tabLabel);
|
||||
const subTabName = entry.subTabName || "";
|
||||
const breadcrumb = subTabName ? (tabName + " › " + subTabName) : tabName;
|
||||
|
||||
launcherItems.push({
|
||||
"name": entry.label,
|
||||
"description": breadcrumb,
|
||||
"icon": iconMode === "tabler" ? "settings" : "preferences-system",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"_score": score - 2,
|
||||
"provider": root,
|
||||
"onActivate": createActivateHandler(entry)
|
||||
});
|
||||
}
|
||||
|
||||
return launcherItems;
|
||||
}
|
||||
|
||||
function getAllSettings() {
|
||||
var launcherItems = [];
|
||||
|
||||
for (var j = 0; j < SettingsSearchService.searchIndex.length; j++) {
|
||||
var entry = SettingsSearchService.searchIndex[j];
|
||||
if (!SettingsSearchService.isEntryVisible(entry))
|
||||
continue;
|
||||
var label = I18n.tr(entry.labelKey);
|
||||
var tabName = I18n.tr(entry.tabLabel);
|
||||
var subTabName = entry.subTabLabel ? I18n.tr(entry.subTabLabel) : "";
|
||||
var breadcrumb = subTabName ? (tabName + " › " + subTabName) : tabName;
|
||||
|
||||
launcherItems.push({
|
||||
"name": label,
|
||||
"description": breadcrumb,
|
||||
"icon": iconMode === "tabler" ? "settings" : "preferences-system",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"_score": 0,
|
||||
"provider": root,
|
||||
"onActivate": createActivateHandler({
|
||||
"labelKey": entry.labelKey,
|
||||
"descriptionKey": entry.descriptionKey,
|
||||
"widget": entry.widget,
|
||||
"tab": entry.tab,
|
||||
"tabLabel": entry.tabLabel,
|
||||
"subTab": entry.subTab,
|
||||
"subTabLabel": entry.subTabLabel || null
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return launcherItems;
|
||||
}
|
||||
|
||||
function createActivateHandler(entry) {
|
||||
return function () {
|
||||
if (launcher)
|
||||
launcher.close();
|
||||
|
||||
Qt.callLater(() => {
|
||||
SettingsPanelService.openToEntry(entry, launcher.screen);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string name: I18n.tr("common.windows")
|
||||
property var launcher: null
|
||||
property bool handleSearch: Settings.data.appLauncher.enableWindowsSearch
|
||||
property string supportedLayouts: "list"
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
|
||||
function init() {
|
||||
Logger.d("WindowsProvider", "Initialized");
|
||||
}
|
||||
|
||||
// Check if this provider handles the command
|
||||
function handleCommand(searchText) {
|
||||
return searchText.startsWith(">win");
|
||||
}
|
||||
|
||||
// Return available commands when user types ">"
|
||||
function commands() {
|
||||
return [
|
||||
{
|
||||
"name": ">win",
|
||||
"description": I18n.tr("launcher.providers.windows-search-description"),
|
||||
"icon": iconMode === "tabler" ? "app-window" : "preferences-system-windows",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.setSearchText(">win ");
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
if (!query)
|
||||
return [];
|
||||
|
||||
var trimmed = query.trim();
|
||||
|
||||
// Handle command mode: ">win" or ">win <search>"
|
||||
var isCommandMode = trimmed.startsWith(">win");
|
||||
if (isCommandMode) {
|
||||
// Extract search term after ">win "
|
||||
var searchTerm = trimmed.substring(4).trim();
|
||||
// In command mode, show all windows if no search term
|
||||
if (searchTerm.length === 0) {
|
||||
return getAllWindows();
|
||||
}
|
||||
trimmed = searchTerm;
|
||||
} else {
|
||||
// Regular search mode - require at least 2 chars
|
||||
if (trimmed.length < 2)
|
||||
return [];
|
||||
}
|
||||
|
||||
var items = [];
|
||||
|
||||
// Collect all windows from CompositorService
|
||||
for (var i = 0; i < CompositorService.windows.count; i++) {
|
||||
var win = CompositorService.windows.get(i);
|
||||
items.push({
|
||||
"id": win.id,
|
||||
"title": win.title || "",
|
||||
"appId": win.appId || "",
|
||||
"workspaceId": win.workspaceId,
|
||||
"isFocused": win.isFocused,
|
||||
"searchText": (win.title + " " + win.appId).toLowerCase()
|
||||
});
|
||||
}
|
||||
|
||||
// Fuzzy search on title and appId
|
||||
var results = FuzzySort.go(trimmed, items, {
|
||||
"keys": ["title", "appId"],
|
||||
"limit": 10
|
||||
});
|
||||
|
||||
// Map to launcher items
|
||||
var launcherItems = [];
|
||||
for (var j = 0; j < results.length; j++) {
|
||||
var entry = results[j].obj;
|
||||
var score = results[j].score;
|
||||
|
||||
// Get icon name from DesktopEntry if available, otherwise use appId
|
||||
var iconName = entry.appId;
|
||||
var appEntry = ThemeIcons.findAppEntry(entry.appId);
|
||||
if (appEntry && appEntry.icon) {
|
||||
iconName = appEntry.icon;
|
||||
}
|
||||
|
||||
launcherItems.push({
|
||||
"name": entry.title || entry.appId,
|
||||
"description": entry.appId,
|
||||
"icon": iconName || "application-x-executable",
|
||||
"isTablerIcon": false,
|
||||
"badgeIcon": "app-window",
|
||||
"_score": score,
|
||||
"provider": root,
|
||||
"windowId": entry.id,
|
||||
"onActivate": createActivateHandler(entry)
|
||||
});
|
||||
}
|
||||
|
||||
return launcherItems;
|
||||
}
|
||||
|
||||
function getAllWindows() {
|
||||
var launcherItems = [];
|
||||
|
||||
for (var i = 0; i < CompositorService.windows.count; i++) {
|
||||
var win = CompositorService.windows.get(i);
|
||||
|
||||
var iconName = win.appId;
|
||||
var appEntry = ThemeIcons.findAppEntry(win.appId);
|
||||
if (appEntry && appEntry.icon) {
|
||||
iconName = appEntry.icon;
|
||||
}
|
||||
|
||||
launcherItems.push({
|
||||
"name": win.title || win.appId,
|
||||
"description": win.appId,
|
||||
"icon": iconName || "application-x-executable",
|
||||
"isTablerIcon": false,
|
||||
"badgeIcon": "app-window",
|
||||
"_score": 0,
|
||||
"provider": root,
|
||||
"windowId": win.id,
|
||||
"onActivate": createActivateHandler({
|
||||
"id": win.id
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return launcherItems;
|
||||
}
|
||||
|
||||
function createActivateHandler(windowEntry) {
|
||||
return function () {
|
||||
if (launcher)
|
||||
launcher.close();
|
||||
|
||||
Qt.callLater(() => {
|
||||
// Find the actual window object to pass to focusWindow
|
||||
for (var i = 0; i < CompositorService.windows.count; i++) {
|
||||
var win = CompositorService.windows.get(i);
|
||||
if (win.id === windowEntry.id) {
|
||||
CompositorService.focusWindow(win);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Services.Media
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
import qs.Widgets.AudioSpectrum
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: Math.round((root.isSideBySide ? 480 : 360) * Style.uiScaleRatio)
|
||||
// Fallback only; SmartPanel uses panelContent.contentPreferredHeight when set.
|
||||
preferredHeight: Math.round((root.compactMode ? 240 : 400) * Style.uiScaleRatio)
|
||||
|
||||
property var mediaMiniSettings: {
|
||||
const widget = BarService.lookupWidget("MediaMini", screen?.name);
|
||||
return widget ? widget.widgetSettings : null;
|
||||
}
|
||||
|
||||
function refreshMediaMiniSettings() {
|
||||
const widget = BarService.lookupWidget("MediaMini", screen?.name);
|
||||
root.mediaMiniSettings = widget ? widget.widgetSettings : null;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BarService
|
||||
function onActiveWidgetsChanged() {
|
||||
root.refreshMediaMiniSettings();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings
|
||||
function onSettingsSaved() {
|
||||
root.refreshMediaMiniSettings();
|
||||
}
|
||||
}
|
||||
|
||||
readonly property string visualizerType: (mediaMiniSettings && mediaMiniSettings.visualizerType !== undefined) ? mediaMiniSettings.visualizerType : "linear"
|
||||
readonly property bool showArtistFirst: !!(mediaMiniSettings && mediaMiniSettings.showArtistFirst !== undefined ? mediaMiniSettings.showArtistFirst : true)
|
||||
readonly property bool showAlbumArt: !!(mediaMiniSettings && mediaMiniSettings.panelShowAlbumArt !== undefined ? mediaMiniSettings.panelShowAlbumArt : true)
|
||||
readonly property bool showVisualizer: !!(mediaMiniSettings && mediaMiniSettings.showVisualizer !== undefined ? mediaMiniSettings.showVisualizer : true)
|
||||
readonly property bool compactMode: !!(mediaMiniSettings && mediaMiniSettings.compactMode !== undefined ? mediaMiniSettings.compactMode : false)
|
||||
readonly property string scrollingMode: (mediaMiniSettings && mediaMiniSettings.scrollingMode !== undefined) ? mediaMiniSettings.scrollingMode : "hover"
|
||||
|
||||
readonly property bool isSideBySide: root.compactMode && root.showAlbumArt
|
||||
|
||||
readonly property bool needsSpectrum: root.showVisualizer && root.visualizerType !== "" && root.visualizerType !== "none" && root.isPanelOpen
|
||||
|
||||
onNeedsSpectrumChanged: {
|
||||
if (root.needsSpectrum) {
|
||||
SpectrumService.registerComponent("mediaplayerpanel");
|
||||
} else {
|
||||
SpectrumService.unregisterComponent("mediaplayerpanel");
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.needsSpectrum) {
|
||||
SpectrumService.registerComponent("mediaplayerpanel");
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
SpectrumService.unregisterComponent("mediaplayerpanel");
|
||||
}
|
||||
|
||||
panelContent: Item {
|
||||
id: playerContent
|
||||
anchors.fill: parent
|
||||
|
||||
// implicitHeight + mainLayout vertical anchors.margins (each Style.marginL).
|
||||
readonly property real contentPreferredHeight: {
|
||||
const m = mainLayout.implicitHeight + 2 * Style.marginL;
|
||||
const scale = Style.uiScaleRatio;
|
||||
if (root.compactMode)
|
||||
return Math.max(m, 240 * scale);
|
||||
if (!root.showAlbumArt)
|
||||
return Math.max(m, 300 * scale);
|
||||
return Math.max(m, 260 * scale);
|
||||
}
|
||||
|
||||
property Component visualizerSource: {
|
||||
switch (root.visualizerType) {
|
||||
case "linear":
|
||||
return linearComponent;
|
||||
case "mirrored":
|
||||
return mirroredComponent;
|
||||
case "wave":
|
||||
return waveComponent;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: root.compactMode ? Style.marginL : Style.marginM
|
||||
z: 1
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: headerRow.implicitHeight + Style.margin2M
|
||||
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: "music"
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("common.media-player")
|
||||
font.weight: Style.fontWeightBold
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
radius: Style.radiusS
|
||||
color: playerSelectorMouse.containsMouse ? Color.mPrimary : "transparent"
|
||||
implicitWidth: playerRow.implicitWidth + Style.marginM
|
||||
implicitHeight: Style.baseWidgetSize * 0.8
|
||||
visible: MediaService.getAvailablePlayers().length > 1
|
||||
|
||||
RowLayout {
|
||||
id: playerRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXS
|
||||
|
||||
NText {
|
||||
text: MediaService.currentPlayer ? MediaService.currentPlayer.identity : "Select Player"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: playerSelectorMouse.containsMouse ? Color.mOnPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
NIcon {
|
||||
icon: "chevron-down"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: playerSelectorMouse.containsMouse ? Color.mOnPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: playerSelectorMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: playerContextMenu.open()
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: playerContextMenu
|
||||
x: 0
|
||||
y: parent.height
|
||||
width: 160
|
||||
padding: Style.marginS
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
radius: Style.iRadiusM
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
Repeater {
|
||||
model: MediaService.getAvailablePlayers()
|
||||
delegate: Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: itemMouse.containsMouse ? Color.mPrimary : "transparent"
|
||||
radius: Style.iRadiusS
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
visible: MediaService.currentPlayer && MediaService.currentPlayer.identity === modelData.identity
|
||||
icon: "check"
|
||||
color: itemMouse.containsMouse ? Color.mOnPrimary : Color.mPrimary
|
||||
pointSize: Style.fontSizeS
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.identity
|
||||
pointSize: Style.fontSizeS
|
||||
color: itemMouse.containsMouse ? Color.mOnPrimary : Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
MediaService.currentPlayer = modelData;
|
||||
playerContextMenu.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: mediaContentGrid.implicitHeight + Style.marginM + Style.marginM
|
||||
|
||||
// Visualizer background for content area
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
z: 0
|
||||
active: !!(root.needsSpectrum && !root.showAlbumArt)
|
||||
sourceComponent: visualizerSource
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: mediaContentGrid
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: root.compactMode ? Style.marginL : Style.marginM
|
||||
anchors.rightMargin: root.compactMode ? Style.marginL : Style.marginM
|
||||
anchors.topMargin: Style.marginM
|
||||
anchors.bottomMargin: Style.marginM
|
||||
columns: root.isSideBySide ? 2 : 1
|
||||
columnSpacing: Style.marginL
|
||||
rowSpacing: root.compactMode ? Style.marginL : Style.marginM
|
||||
|
||||
// Album Art (Vertical in normal, Horizontal in compact)
|
||||
Item {
|
||||
id: albumArtItem
|
||||
readonly property real compactArtSize: Math.round(110 * Style.uiScaleRatio)
|
||||
readonly property bool artSizeKnown: artSizeProbe.status === Image.Ready && artSizeProbe.sourceSize.width > 0 && artSizeProbe.sourceSize.height > 0
|
||||
readonly property real artAspectRatio: artSizeKnown ? artSizeProbe.sourceSize.width / artSizeProbe.sourceSize.height : 1
|
||||
// Non-compact: height from width÷aspect so grid implicit height does not depend on panel height (no layout loop).
|
||||
readonly property real artBoxW: root.compactMode ? compactArtSize : Math.max(parent.width, 1)
|
||||
readonly property real artBoxH: root.compactMode ? compactArtSize : (artBoxW / Math.max(artAspectRatio, 0.001))
|
||||
readonly property real fitArtW: artBoxW / artBoxH > artAspectRatio ? artBoxH * artAspectRatio : artBoxW
|
||||
readonly property real fitArtH: artBoxW / artBoxH > artAspectRatio ? artBoxH : artBoxW / artAspectRatio
|
||||
|
||||
Layout.preferredWidth: fitArtW
|
||||
Layout.preferredHeight: fitArtH
|
||||
Layout.minimumWidth: fitArtW
|
||||
Layout.maximumWidth: fitArtW
|
||||
Layout.minimumHeight: fitArtH
|
||||
Layout.maximumHeight: fitArtH
|
||||
Layout.fillWidth: false
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: root.showAlbumArt
|
||||
|
||||
Image {
|
||||
id: artSizeProbe
|
||||
visible: false
|
||||
asynchronous: true
|
||||
source: MediaService.trackArtUrl
|
||||
}
|
||||
|
||||
NImageRounded {
|
||||
anchors.fill: parent
|
||||
radius: root.compactMode ? Style.radiusM : Style.radiusL
|
||||
imagePath: MediaService.trackArtUrl
|
||||
imageFillMode: Image.PreserveAspectCrop
|
||||
fallbackIcon: "disc"
|
||||
fallbackIconSize: root.compactMode ? Style.fontSizeXXXL * 3 : Style.fontSizeXXXL * 6
|
||||
borderWidth: 0
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
z: 2
|
||||
active: !!(root.needsSpectrum && root.showAlbumArt)
|
||||
sourceComponent: visualizerSource
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: controlsLayout
|
||||
Layout.preferredWidth: root.compactMode ? -1 : albumArtItem.width
|
||||
Layout.fillWidth: root.compactMode
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: root.compactMode
|
||||
spacing: root.compactMode ? Style.marginXS : Style.marginS
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
NScrollText {
|
||||
Layout.fillWidth: true
|
||||
maxWidth: parent.width
|
||||
text: {
|
||||
if (root.showArtistFirst) {
|
||||
return MediaService.trackArtist || (MediaService.trackAlbum || "Unknown Artist");
|
||||
} else {
|
||||
return MediaService.trackTitle || "No Media";
|
||||
}
|
||||
}
|
||||
|
||||
scrollMode: {
|
||||
if (root.scrollingMode === "always")
|
||||
return NScrollText.ScrollMode.Always;
|
||||
if (root.scrollingMode === "hover")
|
||||
return NScrollText.ScrollMode.Hover;
|
||||
return NScrollText.ScrollMode.Never;
|
||||
}
|
||||
fadeExtent: 0.01
|
||||
fadeCornerRadius: Style.radiusM
|
||||
|
||||
delegate: NText {
|
||||
pointSize: root.compactMode ? Style.fontSizeL : Style.fontSizeXL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: root.isSideBySide ? Text.AlignLeft : Text.AlignHCenter
|
||||
elide: Text.ElideNone
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
NScrollText {
|
||||
Layout.fillWidth: true
|
||||
maxWidth: parent.width
|
||||
text: {
|
||||
if (root.showArtistFirst) {
|
||||
return MediaService.trackTitle || "No Media";
|
||||
} else {
|
||||
return MediaService.trackArtist || (MediaService.trackAlbum || "Unknown Artist");
|
||||
}
|
||||
}
|
||||
|
||||
scrollMode: {
|
||||
if (root.scrollingMode === "always")
|
||||
return NScrollText.ScrollMode.Always;
|
||||
if (root.scrollingMode === "hover")
|
||||
return NScrollText.ScrollMode.Hover;
|
||||
return NScrollText.ScrollMode.Never;
|
||||
}
|
||||
fadeExtent: 0.01
|
||||
fadeCornerRadius: Style.radiusM
|
||||
|
||||
delegate: NText {
|
||||
pointSize: root.compactMode ? Style.fontSizeS : Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: root.isSideBySide ? Text.AlignLeft : Text.AlignHCenter
|
||||
elide: Text.ElideNone
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: progressWrapper
|
||||
visible: (MediaService.currentPlayer && MediaService.trackLength > 0)
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: progressColumn.implicitHeight
|
||||
|
||||
property real localSeekRatio: -1
|
||||
property real lastSentSeekRatio: -1
|
||||
property real seekEpsilon: 0.01
|
||||
property real progressRatio: {
|
||||
if (!MediaService.currentPlayer || MediaService.trackLength <= 0)
|
||||
return 0;
|
||||
const r = MediaService.currentPosition / MediaService.trackLength;
|
||||
if (isNaN(r) || !isFinite(r))
|
||||
return 0;
|
||||
return Math.max(0, Math.min(1, r));
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: seekDebounce
|
||||
interval: 75
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (MediaService.isSeeking && progressWrapper.localSeekRatio >= 0) {
|
||||
const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio));
|
||||
if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) {
|
||||
MediaService.seekByRatio(next);
|
||||
progressWrapper.lastSentSeekRatio = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: progressColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: 2
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.compactMode ? (Style.baseWidgetSize * 0.4) : (Style.baseWidgetSize * 0.5)
|
||||
|
||||
NSlider {
|
||||
id: progressSlider
|
||||
anchors.fill: parent
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0
|
||||
snapAlways: false
|
||||
enabled: MediaService.trackLength > 0 && MediaService.canSeek
|
||||
heightRatio: 0.4
|
||||
|
||||
value: (!MediaService.isSeeking) ? progressWrapper.progressRatio : (progressWrapper.localSeekRatio >= 0 ? progressWrapper.localSeekRatio : 0)
|
||||
|
||||
onMoved: {
|
||||
progressWrapper.localSeekRatio = value;
|
||||
seekDebounce.restart();
|
||||
}
|
||||
onPressedChanged: {
|
||||
if (pressed) {
|
||||
MediaService.isSeeking = true;
|
||||
progressWrapper.localSeekRatio = value;
|
||||
MediaService.seekByRatio(value);
|
||||
progressWrapper.lastSentSeekRatio = value;
|
||||
} else {
|
||||
seekDebounce.stop();
|
||||
MediaService.seekByRatio(value);
|
||||
MediaService.isSeeking = false;
|
||||
progressWrapper.localSeekRatio = -1;
|
||||
progressWrapper.lastSentSeekRatio = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
NText {
|
||||
text: MediaService.positionString || "0:00"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
visible: progressWrapper.visible
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: MediaService.lengthString || "0:00"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignRight
|
||||
visible: progressWrapper.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: root.isSideBySide ? Style.marginM : Style.marginS
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: root.isSideBySide ? Style.marginL : Style.marginXL
|
||||
|
||||
NIconButton {
|
||||
icon: "media-prev"
|
||||
baseSize: root.compactMode ? (Style.baseWidgetSize * 0.9) : (Style.baseWidgetSize * 1.2)
|
||||
onClicked: MediaService.previous()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: root.compactMode ? (Style.baseWidgetSize * 1.3) : (Style.baseWidgetSize * 1.8)
|
||||
implicitHeight: root.compactMode ? (Style.baseWidgetSize * 1.3) : (Style.baseWidgetSize * 1.8)
|
||||
radius: root.compactMode ? Style.iRadiusM : Style.iRadiusL
|
||||
color: Color.mPrimary
|
||||
|
||||
NIcon {
|
||||
anchors.centerIn: parent
|
||||
icon: MediaService.isPlaying ? "media-pause" : "media-play"
|
||||
pointSize: root.compactMode ? Style.fontSizeL : Style.fontSizeXXL
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onEntered: parent.color = Color.mPrimary
|
||||
onClicked: MediaService.playPause()
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "media-next"
|
||||
baseSize: root.compactMode ? (Style.baseWidgetSize * 0.9) : (Style.baseWidgetSize * 1.2)
|
||||
onClicked: MediaService.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visualizer Components
|
||||
Component {
|
||||
id: linearComponent
|
||||
NLinearSpectrum {
|
||||
width: parent.width - Style.marginS
|
||||
height: 20
|
||||
values: SpectrumService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
barPosition: Settings.getBarPositionForScreen(root.screen?.name)
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mirroredComponent
|
||||
NMirroredSpectrum {
|
||||
width: parent.width - Style.marginS
|
||||
height: parent.height - Style.marginS
|
||||
values: SpectrumService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: waveComponent
|
||||
NWaveSpectrum {
|
||||
width: parent.width - Style.marginS
|
||||
height: parent.height - Style.marginS
|
||||
values: SpectrumService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,950 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "../Settings/Tabs/Connections" as WifiPrefs
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.Networking
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: Math.round(440 * Style.uiScaleRatio)
|
||||
preferredHeight: Math.round(500 * Style.uiScaleRatio)
|
||||
|
||||
// Info panel collapsed by default, view mode persisted in settings
|
||||
// Ethernet details UI state (mirrors Wi‑Fi info behavior)
|
||||
property bool ethernetInfoExpanded: false
|
||||
property bool ethernetDetailsGrid: (Settings.data.network.wifiDetailsViewMode === "grid")
|
||||
property int ipVersion: 4
|
||||
|
||||
// Unified panel view mode: "wifi" | "ethernet" (persisted)
|
||||
property string panelViewMode: "wifi"
|
||||
property bool panelViewPersistEnabled: false
|
||||
|
||||
onPanelViewModeChanged: {
|
||||
// Persist last view (only after restored the initial value)
|
||||
if (panelViewPersistEnabled) {
|
||||
Settings.data.network.networkPanelView = panelViewMode;
|
||||
}
|
||||
if (panelViewMode === "wifi") {
|
||||
ethernetInfoExpanded = false;
|
||||
if (NetworkService.wifiEnabled && !NetworkService.scanningActive) {
|
||||
NetworkService.scan();
|
||||
NetworkService.refreshActiveWifiDetails();
|
||||
}
|
||||
} else {
|
||||
if (NetworkService.ethernetConnected) {
|
||||
NetworkService.refreshActiveEthernetDetails();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Effectively visible tracking
|
||||
readonly property bool effectivelyVisible: root.visible && Window.window && Window.window.visible
|
||||
|
||||
onEffectivelyVisibleChanged: {
|
||||
if (effectivelyVisible) {
|
||||
SystemStatService.registerComponent("network-panel");
|
||||
if (NetworkService.wifiEnabled && !NetworkService.scanningActive) {
|
||||
NetworkService.scan();
|
||||
NetworkService.refreshActiveWifiDetails();
|
||||
}
|
||||
if (NetworkService.ethernetConnected) {
|
||||
NetworkService.refreshActiveEthernetDetails();
|
||||
}
|
||||
} else {
|
||||
SystemStatService.unregisterComponent("network-panel");
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
// Restore last view if valid, otherwise choose what's available (prefer Wi‑Fi when both exist)
|
||||
if (Settings.data.network.networkPanelView) {
|
||||
const last = Settings.data.network.networkPanelView;
|
||||
if (last === "ethernet" && NetworkService.ethernetAvailable) {
|
||||
panelViewMode = "ethernet";
|
||||
} else {
|
||||
panelViewMode = "wifi";
|
||||
}
|
||||
} else {
|
||||
if (!NetworkService.wifiEnabled && NetworkService.ethernetAvailable) {
|
||||
panelViewMode = "ethernet";
|
||||
} else {
|
||||
panelViewMode = "wifi";
|
||||
}
|
||||
}
|
||||
panelViewPersistEnabled = true;
|
||||
}
|
||||
|
||||
panelContent: Rectangle {
|
||||
color: "transparent"
|
||||
|
||||
property real contentPreferredHeight: Math.min(root.preferredHeight, mainColumn.implicitHeight + Style.margin2L)
|
||||
|
||||
ColumnLayout {
|
||||
id: mainColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
// Header
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: header.implicitHeight + Style.margin2M
|
||||
|
||||
ColumnLayout {
|
||||
id: header
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
NIcon {
|
||||
id: modeIcon
|
||||
icon: panelViewMode === "wifi" ? (NetworkService.wifiEnabled ? "wifi" : "wifi-off") : (NetworkService.ethernetAvailable ? (NetworkService.ethernetConnected ? "ethernet" : "ethernet") : "ethernet-off")
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: {
|
||||
if (panelViewMode === "wifi") {
|
||||
return NetworkService.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant;
|
||||
} else {
|
||||
return NetworkService.ethernetConnected ? Color.mPrimary : Color.mOnSurfaceVariant;
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
if (panelViewMode === "wifi") {
|
||||
if (NetworkService.ethernetAvailable) {
|
||||
panelViewMode = "ethernet";
|
||||
} else {
|
||||
TooltipService.show(parent, I18n.tr("wifi.panel.no-ethernet-devices"));
|
||||
}
|
||||
} else {
|
||||
panelViewMode = "wifi";
|
||||
}
|
||||
}
|
||||
onEntered: TooltipService.show(parent, panelViewMode === "wifi" ? I18n.tr("common.wifi") : I18n.tr("common.ethernet"))
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: panelViewMode === "wifi" ? I18n.tr("common.wifi") : I18n.tr("common.ethernet")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: wifiSwitch
|
||||
visible: panelViewMode === "wifi"
|
||||
checked: NetworkService.wifiEnabled
|
||||
enabled: !NetworkService.airplaneModeEnabled && NetworkService.wifiAvailable
|
||||
onToggled: checked => NetworkService.setWifiEnabled(checked)
|
||||
baseSize: Style.baseWidgetSize * 0.7 // Slightly smaller
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "settings"
|
||||
tooltipText: I18n.tr("tooltips.open-settings")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 0, screen)
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Mode switch (Wi‑Fi / Ethernet)
|
||||
NTabBar {
|
||||
id: modeTabBar
|
||||
visible: NetworkService.ethernetAvailable && NetworkService.wifiAvailable
|
||||
margins: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: root.panelViewMode === "wifi" ? 0 : 1
|
||||
onCurrentIndexChanged: {
|
||||
root.panelViewMode = (currentIndex === 0) ? "wifi" : "ethernet";
|
||||
}
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.wifi")
|
||||
tabIndex: 0
|
||||
checked: modeTabBar.currentIndex === 0
|
||||
}
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.ethernet")
|
||||
tabIndex: 1
|
||||
checked: modeTabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unified scrollable content (Wi‑Fi or Ethernet view)
|
||||
ColumnLayout {
|
||||
id: wifiSectionContainer
|
||||
visible: true
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
// Error message
|
||||
Rectangle {
|
||||
visible: panelViewMode === "wifi" && NetworkService.lastError.length > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: errorRow.implicitHeight + Style.margin2M
|
||||
color: Qt.alpha(Color.mError, 0.1)
|
||||
radius: Style.radiusS
|
||||
border.width: Style.borderS
|
||||
border.color: Color.mError
|
||||
|
||||
RowLayout {
|
||||
id: errorRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
icon: "warning"
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mError
|
||||
}
|
||||
|
||||
NText {
|
||||
text: NetworkService.lastError
|
||||
color: Color.mError
|
||||
pointSize: Style.fontSizeS
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
baseSize: Style.baseWidgetSize * 0.6
|
||||
onClicked: NetworkService.lastError = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unified scrollable content
|
||||
NScrollView {
|
||||
id: contentScroll
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
reserveScrollbarSpace: false
|
||||
gradientColor: Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
width: contentScroll.availableWidth
|
||||
spacing: Style.marginM
|
||||
|
||||
// Wi‑Fi disabled state
|
||||
NBox {
|
||||
id: disabledBox
|
||||
visible: panelViewMode === "wifi" && !NetworkService.wifiEnabled
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: disabledColumn.implicitHeight + Style.margin2M
|
||||
|
||||
ColumnLayout {
|
||||
id: disabledColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginL
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "wifi-off"
|
||||
pointSize: 48
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("wifi.panel.disabled")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("wifi.panel.enable-message")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scanning state (show when no networks and we haven't had any yet)
|
||||
NBox {
|
||||
id: scanningBox
|
||||
visible: panelViewMode === "wifi" && NetworkService.wifiEnabled && Object.keys(NetworkService.networks).length === 0 && NetworkService.scanningActive
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: scanningColumn.implicitHeight + Style.margin2M
|
||||
|
||||
ColumnLayout {
|
||||
id: scanningColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginL
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NBusyIndicator {
|
||||
running: visible && root.effectivelyVisible
|
||||
color: Color.mPrimary
|
||||
size: Style.baseWidgetSize
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("wifi.panel.searching")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state when no networks (only show after we've had networks before, meaning a real empty result)
|
||||
NBox {
|
||||
id: emptyBox
|
||||
visible: panelViewMode === "wifi" && NetworkService.wifiEnabled && Object.keys(NetworkService.networks).length === 0 && !NetworkService.scanningActive
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: emptyColumn.implicitHeight + Style.margin2M
|
||||
|
||||
ColumnLayout {
|
||||
id: emptyColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginL
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "wifi-question"
|
||||
pointSize: 48
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("wifi.panel.no-networks")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Networks list container (Wi‑Fi)
|
||||
ColumnLayout {
|
||||
id: networksList
|
||||
visible: panelViewMode === "wifi" && NetworkService.wifiEnabled && Object.keys(NetworkService.networks).length > 0
|
||||
width: parent.width
|
||||
spacing: Style.marginM
|
||||
|
||||
WifiPrefs.WifiSubTab {
|
||||
showOnlyLists: true
|
||||
}
|
||||
}
|
||||
|
||||
// Ethernet view
|
||||
NBox {
|
||||
id: ethernetSection
|
||||
visible: panelViewMode === "ethernet"
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: ethernetColumn.implicitHeight + Style.margin2M
|
||||
|
||||
ColumnLayout {
|
||||
id: ethernetColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
// Section label
|
||||
NLabel {
|
||||
label: I18n.tr("wifi.panel.available-interfaces")
|
||||
visible: (NetworkService.ethernetInterfaces && NetworkService.ethernetInterfaces.length > 0)
|
||||
}
|
||||
|
||||
// Empty state when no Ethernet devices
|
||||
ColumnLayout {
|
||||
id: emptyEthColumn
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
|
||||
Layout.preferredHeight: emptyEthColumn.implicitHeight + Style.margin2M
|
||||
visible: !(NetworkService.ethernetInterfaces && NetworkService.ethernetInterfaces.length > 0)
|
||||
spacing: Style.marginL
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "ethernet-off"
|
||||
pointSize: 48
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("wifi.panel.no-ethernet-devices")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
// Interfaces list
|
||||
ColumnLayout {
|
||||
id: ethIfacesList
|
||||
visible: NetworkService.ethernetInterfaces && NetworkService.ethernetInterfaces.length > 0
|
||||
width: parent.width
|
||||
spacing: Style.marginXS
|
||||
|
||||
Repeater {
|
||||
model: NetworkService.ethernetInterfaces || []
|
||||
delegate: NBox {
|
||||
id: ethItem
|
||||
|
||||
function getContentColors(defaultColors = [Color.mSurface, Color.mOnSurface]) {
|
||||
if (modelData.connected) {
|
||||
return [Color.mPrimary, Color.mOnPrimary];
|
||||
}
|
||||
return defaultColors;
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginXS
|
||||
Layout.rightMargin: Style.marginXS
|
||||
implicitHeight: ethItemColumn.implicitHeight + Style.margin2M
|
||||
radius: Style.radiusM
|
||||
forceOpaque: true
|
||||
color: ethItem.getContentColors()[0]
|
||||
|
||||
ColumnLayout {
|
||||
id: ethItemColumn
|
||||
width: parent.width - Style.margin2M
|
||||
x: Style.marginM
|
||||
y: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
// Main row matching Wi‑Fi card style
|
||||
// Click handling for the whole header row is provided by a sibling MouseArea
|
||||
// anchored to this row (defined right after this RowLayout).
|
||||
RowLayout {
|
||||
id: ethHeaderRow
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
icon: NetworkService.getIcon(true)
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: ethItem.getContentColors()[1]
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
NText {
|
||||
text: modelData.connectionName || modelData.ifname
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
|
||||
color: ethItem.getContentColors()[1]
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginXS
|
||||
|
||||
NText {
|
||||
text: {
|
||||
if (modelData.connected) {
|
||||
switch (NetworkService.networkConnectivity) {
|
||||
case "full":
|
||||
return I18n.tr("common.connected");
|
||||
case "limited":
|
||||
case "unknown":
|
||||
return I18n.tr("wifi.panel.internet-limited");
|
||||
case "portal":
|
||||
return I18n.tr("wifi.panel.action-required");
|
||||
default:
|
||||
return NetworkService.networkConnectivity;
|
||||
}
|
||||
}
|
||||
return I18n.tr("common.disconnected");
|
||||
}
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy)
|
||||
}
|
||||
|
||||
// Network speed indicators (visible when connected and speed > 0)
|
||||
RowLayout {
|
||||
visible: (modelData.connected && NetworkService.networkConnectivity === "full") && (SystemStatService.rxSpeed > 0 || SystemStatService.txSpeed > 0)
|
||||
spacing: 2
|
||||
Layout.leftMargin: Style.marginXS
|
||||
Layout.fillWidth: false
|
||||
|
||||
NIcon {
|
||||
visible: SystemStatService.rxSpeed > 0
|
||||
icon: "arrow-down"
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy)
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: SystemStatService.rxSpeed > 0
|
||||
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy)
|
||||
elide: Text.ElideNone
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: SystemStatService.rxSpeed > 0 && SystemStatService.txSpeed > 0
|
||||
width: Style.marginXS
|
||||
height: 1
|
||||
}
|
||||
|
||||
NIcon {
|
||||
visible: SystemStatService.txSpeed > 0
|
||||
icon: "arrow-up"
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy)
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: SystemStatService.txSpeed > 0
|
||||
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy)
|
||||
elide: Text.ElideNone
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info button on the right
|
||||
NIconButton {
|
||||
icon: "info"
|
||||
tooltipText: I18n.tr("common.info")
|
||||
baseSize: Style.baseWidgetSize * 0.75
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
enabled: true
|
||||
visible: NetworkService.ethernetConnected
|
||||
onClicked: {
|
||||
if (NetworkService.activeEthernetIf === modelData.ifname && ethernetInfoExpanded) {
|
||||
ethernetInfoExpanded = false;
|
||||
return;
|
||||
}
|
||||
if (NetworkService.activeEthernetIf !== modelData.ifname) {
|
||||
NetworkService.activeEthernetIf = modelData.ifname;
|
||||
NetworkService.activeEthernetDetailsTimestamp = 0;
|
||||
}
|
||||
ethernetInfoExpanded = true;
|
||||
NetworkService.refreshActiveEthernetDetails();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Click handling without anchors in a Layout-managed item
|
||||
TapHandler {
|
||||
target: ethHeaderRow
|
||||
onTapped: {
|
||||
if (NetworkService.activeEthernetIf === modelData.ifname && ethernetInfoExpanded) {
|
||||
ethernetInfoExpanded = false;
|
||||
return;
|
||||
}
|
||||
if (NetworkService.activeEthernetIf !== modelData.ifname) {
|
||||
NetworkService.activeEthernetIf = modelData.ifname;
|
||||
NetworkService.activeEthernetDetailsTimestamp = 0;
|
||||
}
|
||||
ethernetInfoExpanded = true;
|
||||
NetworkService.refreshActiveEthernetDetails();
|
||||
}
|
||||
}
|
||||
|
||||
// Inline Ethernet details
|
||||
Rectangle {
|
||||
id: ethInfoInline
|
||||
visible: ethernetInfoExpanded && NetworkService.activeEthernetIf === modelData.ifname
|
||||
Layout.fillWidth: true
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusXS
|
||||
border.width: Style.borderS
|
||||
border.color: Style.boxBorderColor
|
||||
implicitHeight: ethInfoGrid.implicitHeight + Style.margin2S
|
||||
clip: true
|
||||
Layout.topMargin: Style.marginXS
|
||||
|
||||
// Grid/List toggle
|
||||
NIconButton {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Style.marginS
|
||||
icon: ethernetDetailsGrid ? "layout-list" : "layout-grid"
|
||||
tooltipText: ethernetDetailsGrid ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view")
|
||||
baseSize: Style.baseWidgetSize * 0.65
|
||||
onClicked: {
|
||||
ethernetDetailsGrid = !ethernetDetailsGrid;
|
||||
Settings.data.network.wifiDetailsViewMode = ethernetDetailsGrid ? "grid" : "list";
|
||||
}
|
||||
z: 1
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: ethInfoGrid
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
anchors.rightMargin: Style.baseWidgetSize
|
||||
flow: ethernetDetailsGrid ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: ethernetDetailsGrid ? 3 : 6
|
||||
columns: ethernetDetailsGrid ? 2 : 1
|
||||
columnSpacing: Style.marginM
|
||||
rowSpacing: Style.marginXS
|
||||
onColumnsChanged: {
|
||||
if (ethInfoGrid.forceLayout) {
|
||||
Qt.callLater(function () {
|
||||
ethInfoGrid.forceLayout();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Item 1: Interface ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
spacing: Style.marginXS
|
||||
NIcon {
|
||||
icon: "ethernet"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.interface"))
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
NText {
|
||||
text: (NetworkService.activeEthernetDetails.ifname && NetworkService.activeEthernetDetails.ifname.length > 0) ? NetworkService.activeEthernetDetails.ifname : (NetworkService.activeEthernetIf || "-")
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere
|
||||
elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone
|
||||
maximumLineCount: ethernetDetailsGrid ? 1 : 6
|
||||
clip: true
|
||||
|
||||
// Click-to-copy Ethernet interface name
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
// Guard against undefined by normalizing to empty strings
|
||||
enabled: ((NetworkService.activeEthernetDetails.ifname || "").length > 0) || ((NetworkService.activeEthernetIf || "").length > 0)
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address"))
|
||||
onExited: TooltipService.hide()
|
||||
onClicked: {
|
||||
const value = (NetworkService.activeEthernetDetails.ifname && NetworkService.activeEthernetDetails.ifname.length > 0) ? NetworkService.activeEthernetDetails.ifname : (NetworkService.activeEthernetIf || "");
|
||||
if (value.length > 0) {
|
||||
Quickshell.execDetached(["wl-copy", value]);
|
||||
ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Item 2: Hardware Address ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
spacing: Style.marginXS
|
||||
NIcon {
|
||||
icon: "hash"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(parent, I18n.tr("bluetooth.panel.device-address"))
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
NText {
|
||||
text: NetworkService.activeEthernetDetails.hwAddr || "-"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere
|
||||
elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone
|
||||
maximumLineCount: ethernetDetailsGrid ? 1 : 6
|
||||
clip: true
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: (NetworkService.activeEthernetDetails.hwAddr || "").length > 0
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address"))
|
||||
onExited: TooltipService.hide()
|
||||
onClicked: {
|
||||
const value = NetworkService.activeEthernetDetails.hwAddr || "";
|
||||
if (value.length > 0) {
|
||||
Quickshell.execDetached(["wl-copy", value]);
|
||||
ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Item 3: Link speed ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
spacing: Style.marginXS
|
||||
NIcon {
|
||||
icon: "gauge"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.link-speed"))
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
NText {
|
||||
text: (NetworkService.activeEthernetDetails.speed && NetworkService.activeEthernetDetails.speed.length > 0) ? NetworkService.activeEthernetDetails.speed : "-"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere
|
||||
elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone
|
||||
maximumLineCount: ethernetDetailsGrid ? 1 : 6
|
||||
clip: true
|
||||
}
|
||||
}
|
||||
|
||||
// --- Item 4: IPv4 || IPv6 ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
spacing: Style.marginXS
|
||||
NIcon {
|
||||
icon: "network"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.ipv4") : I18n.tr("wifi.panel.ipv6"))
|
||||
onExited: TooltipService.hide()
|
||||
onClicked: {
|
||||
root.ipVersion = root.ipVersion === 4 ? 6 : 4;
|
||||
TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.ipv4") : I18n.tr("wifi.panel.ipv6"));
|
||||
}
|
||||
}
|
||||
}
|
||||
NText {
|
||||
text: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.ipv4 || "-") : ((NetworkService.activeEthernetDetails.ipv6 || []).join(", ") || "-")
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere
|
||||
elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone
|
||||
maximumLineCount: ethernetDetailsGrid ? 1 : 6
|
||||
clip: true
|
||||
|
||||
// Click-to-copy Ethernet IP address
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.ipv4 || "").length > 0 : (NetworkService.activeEthernetDetails.ipv6 || []).length > 0
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address"))
|
||||
onExited: TooltipService.hide()
|
||||
onClicked: {
|
||||
const value = root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.ipv4 || "") : ((NetworkService.activeEthernetDetails.ipv6 || []).join(", ") || "");
|
||||
if (value.length > 0) {
|
||||
Quickshell.execDetached(["wl-copy", value]);
|
||||
ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Item 5: DNS ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
spacing: Style.marginXS
|
||||
NIcon {
|
||||
icon: "world"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv6") + ")")
|
||||
onExited: TooltipService.hide()
|
||||
onClicked: {
|
||||
root.ipVersion = root.ipVersion === 4 ? 6 : 4;
|
||||
TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv6") + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
NText {
|
||||
text: root.ipVersion === 4 ? ((NetworkService.activeEthernetDetails.dns4 || []).join(", ") || "-") : ((NetworkService.activeEthernetDetails.dns6 || []).join(", ") || "-")
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere
|
||||
elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone
|
||||
maximumLineCount: ethernetDetailsGrid ? 1 : 6
|
||||
clip: true
|
||||
|
||||
// Click-to-copy Ethernet DNS
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.dns4 || []).length > 0 : (NetworkService.activeEthernetDetails.dns6 || []).length > 0
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address"))
|
||||
onExited: TooltipService.hide()
|
||||
onClicked: {
|
||||
const value = root.ipVersion === 4 ? ((NetworkService.activeEthernetDetails.dns4 || []).join(", ") || "") : ((NetworkService.activeEthernetDetails.dns6 || []).join(", ") || "");
|
||||
if (value.length > 0) {
|
||||
Quickshell.execDetached(["wl-copy", value]);
|
||||
ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Item 6: Gateway ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
spacing: Style.marginXS
|
||||
NIcon {
|
||||
icon: "router"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv6") + ")")
|
||||
onExited: TooltipService.hide()
|
||||
onClicked: {
|
||||
root.ipVersion = root.ipVersion === 4 ? 6 : 4;
|
||||
TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv6") + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
NText {
|
||||
text: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.gateway4 || "-") : ((NetworkService.activeEthernetDetails.gateway6 || []).join(", ") || "-")
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere
|
||||
elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone
|
||||
maximumLineCount: ethernetDetailsGrid ? 1 : 6
|
||||
clip: true
|
||||
|
||||
// Click-to-copy Ethernet Gateway
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.gateway4 || "").length > 0 : (NetworkService.activeEthernetDetails.gateway6 || []).length > 0
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address"))
|
||||
onExited: TooltipService.hide()
|
||||
onClicked: {
|
||||
const value = root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.gateway4 || "") : ((NetworkService.activeEthernetDetails.gateway6 || []).join(", ") || "");
|
||||
if (value.length > 0) {
|
||||
Quickshell.execDetached(["wl-copy", value]);
|
||||
ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1022
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,228 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
|
||||
/**
|
||||
* Generic plugin panel slot that can be reused for different plugins
|
||||
*/
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
// Which plugin slot this is (1 or 2)
|
||||
property int slotNumber: 1
|
||||
|
||||
// Currently loaded plugin ID (empty if no plugin using this slot)
|
||||
property string currentPluginId: ""
|
||||
|
||||
// Plugin instance
|
||||
property var pluginInstance: null
|
||||
|
||||
// Reference to the plugin content loader (set when panel content is created)
|
||||
property var contentLoader: null
|
||||
|
||||
// Pass through anchor properties from plugin panel content
|
||||
panelAnchorHorizontalCenter: pluginInstance?.panelAnchorHorizontalCenter ?? false
|
||||
panelAnchorVerticalCenter: pluginInstance?.panelAnchorVerticalCenter ?? false
|
||||
panelAnchorTop: pluginInstance?.panelAnchorTop ?? false
|
||||
panelAnchorBottom: pluginInstance?.panelAnchorBottom ?? false
|
||||
panelAnchorLeft: pluginInstance?.panelAnchorLeft ?? false
|
||||
panelAnchorRight: pluginInstance?.panelAnchorRight ?? false
|
||||
|
||||
// Panel background color
|
||||
panelBackgroundColor: pluginInstance?.panelBackgroundColor ?? Color.mSurface
|
||||
|
||||
// Panel content is dynamically loaded
|
||||
panelContent: Component {
|
||||
Item {
|
||||
id: panelContainer
|
||||
|
||||
// Required by SmartPanel for background rendering geometry
|
||||
readonly property var geometryPlaceholder: pluginContentItem
|
||||
|
||||
// Panel properties expected by SmartPanel
|
||||
// A plugin can opt out of attachment (allowAttach: false) but cannot override
|
||||
// the global "attach panels to bar" setting — if that setting is off, panels
|
||||
// stay detached regardless of what the plugin requests.
|
||||
readonly property bool allowAttach: {
|
||||
var globalAllow = Settings.data.ui.panelsAttachedToBar || root.forceAttachToBar;
|
||||
if (!globalAllow)
|
||||
return false;
|
||||
if (pluginContentLoader.item && pluginContentLoader.item.allowAttach !== undefined)
|
||||
return pluginContentLoader.item.allowAttach;
|
||||
return globalAllow;
|
||||
}
|
||||
// Expose preferred dimensions from plugin panel content
|
||||
// Only define these if the plugin provides them
|
||||
property var contentPreferredWidth: {
|
||||
if (pluginContentLoader.item && pluginContentLoader.item.contentPreferredWidth !== undefined && pluginContentLoader.item.contentPreferredWidth > 0) {
|
||||
return pluginContentLoader.item.contentPreferredWidth;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
property var contentPreferredHeight: {
|
||||
if (pluginContentLoader.item && pluginContentLoader.item.contentPreferredHeight !== undefined && pluginContentLoader.item.contentPreferredHeight > 0) {
|
||||
return pluginContentLoader.item.contentPreferredHeight;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
// Dynamic plugin content
|
||||
Item {
|
||||
id: pluginContentItem
|
||||
anchors.fill: parent
|
||||
|
||||
// Plugin content loader, pluginApi is injected synchronously in loadPluginPanel()
|
||||
Loader {
|
||||
id: pluginContentLoader
|
||||
anchors.fill: parent
|
||||
active: false
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Store reference to the loader so loadPluginPanel can access it
|
||||
root.contentLoader = pluginContentLoader;
|
||||
|
||||
// Load plugin panel content if assigned
|
||||
if (root.currentPluginId !== "") {
|
||||
root.loadPluginPanel(root.currentPluginId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load a plugin's panel content
|
||||
function loadPluginPanel(pluginId) {
|
||||
if (!PluginService.isPluginLoaded(pluginId)) {
|
||||
Logger.w("PluginPanelSlot", "Plugin not loaded:", pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var plugin = PluginService.loadedPlugins[pluginId];
|
||||
if (!plugin || !plugin.manifest) {
|
||||
Logger.w("PluginPanelSlot", "Plugin data not found:", pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!plugin.manifest.entryPoints || !plugin.manifest.entryPoints.panel) {
|
||||
Logger.w("PluginPanelSlot", "Plugin does not provide a panel:", pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if loader is available
|
||||
if (!root.contentLoader) {
|
||||
Logger.e("PluginPanelSlot", "Content loader not available yet");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear any stale pluginInstance before loading new content
|
||||
root.pluginInstance = null;
|
||||
|
||||
var pluginDir = PluginRegistry.getPluginDir(pluginId);
|
||||
var panelPath = pluginDir + "/" + plugin.manifest.entryPoints.panel;
|
||||
|
||||
Logger.i("PluginPanelSlot", "Loading panel for plugin:", pluginId, "in slot", root.slotNumber);
|
||||
|
||||
// Load the panel component with cache-busting version parameter
|
||||
var loadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
|
||||
var component = Qt.createComponent("file://" + panelPath + "?v=" + loadVersion);
|
||||
|
||||
if (component.status === Component.Ready) {
|
||||
finalizePluginLoad(pluginId, component);
|
||||
return true;
|
||||
} else if (component.status === Component.Loading) {
|
||||
// Handle async component loading - wait for it to be ready
|
||||
Logger.d("PluginPanelSlot", "Component loading asynchronously for:", pluginId);
|
||||
component.statusChanged.connect(function () {
|
||||
if (component.status === Component.Ready) {
|
||||
finalizePluginLoad(pluginId, component);
|
||||
// Force SmartPanel to recalculate position now that content is ready
|
||||
if (root.isPanelVisible) {
|
||||
root.setPosition();
|
||||
}
|
||||
} else if (component.status === Component.Error) {
|
||||
PluginService.recordPluginError(pluginId, "panel", component.errorString());
|
||||
}
|
||||
});
|
||||
return true; // Will be loaded asynchronously
|
||||
} else if (component.status === Component.Error) {
|
||||
PluginService.recordPluginError(pluginId, "panel", component.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper function to finalize plugin content loading
|
||||
function finalizePluginLoad(pluginId, component) {
|
||||
// Get plugin API
|
||||
var api = PluginService.getPluginAPI(pluginId);
|
||||
|
||||
// Use setSource with initial properties so pluginApi is available
|
||||
// from the first binding evaluation (before onLoaded)
|
||||
root.contentLoader.active = true;
|
||||
root.contentLoader.setSource(component.url, api ? {
|
||||
"pluginApi": api
|
||||
} : {});
|
||||
|
||||
if (root.contentLoader.item) {
|
||||
root.pluginInstance = root.contentLoader.item;
|
||||
root.currentPluginId = pluginId;
|
||||
|
||||
Logger.i("PluginPanelSlot", "Panel loaded for:", pluginId);
|
||||
} else {
|
||||
Logger.e("PluginPanelSlot", "Failed to get loader item for:", pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
// Unload current plugin panel
|
||||
function unloadPluginPanel() {
|
||||
if (root.currentPluginId === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.i("PluginPanelSlot", "Unloading panel from slot", root.slotNumber);
|
||||
|
||||
if (root.contentLoader) {
|
||||
root.contentLoader.active = false;
|
||||
root.contentLoader.sourceComponent = null;
|
||||
}
|
||||
root.pluginInstance = null;
|
||||
root.currentPluginId = "";
|
||||
}
|
||||
|
||||
// Register with PanelService
|
||||
Component.onCompleted: {
|
||||
PanelService.registerPanel(root);
|
||||
}
|
||||
|
||||
// Update plugin's panelOpenScreen when this panel opens/closes
|
||||
onOpened: {
|
||||
if (root.currentPluginId !== "") {
|
||||
var api = PluginService.getPluginAPI(root.currentPluginId);
|
||||
if (api) {
|
||||
api.panelOpenScreen = root.screen;
|
||||
Logger.d("PluginPanelSlot", "Set panelOpenScreen for", root.currentPluginId, "to", root.screen?.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
if (root.currentPluginId !== "") {
|
||||
var api = PluginService.getPluginAPI(root.currentPluginId);
|
||||
if (api) {
|
||||
api.panelOpenScreen = null;
|
||||
Logger.d("PluginPanelSlot", "Cleared panelOpenScreen for", root.currentPluginId);
|
||||
}
|
||||
}
|
||||
// Clear stale references when panel closes (content is destroyed by SmartPanel)
|
||||
root.pluginInstance = null;
|
||||
root.contentLoader = null;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+193
@@ -0,0 +1,193 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
// Widget Settings Dialog Component
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property int widgetIndex: -1
|
||||
property var widgetData: null
|
||||
property string widgetId: ""
|
||||
property string sectionId: ""
|
||||
property var screen: null
|
||||
property var settingsCache: ({})
|
||||
|
||||
readonly property real maxHeight: (screen ? screen.height : (parent ? parent.height : 800)) * 0.8
|
||||
|
||||
signal updateWidgetSettings(string section, int index, var settings)
|
||||
|
||||
width: Math.max(content.implicitWidth + padding * 2, 640)
|
||||
height: Math.min(content.implicitHeight + padding * 2, maxHeight)
|
||||
padding: Style.marginXL
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
dim: false
|
||||
anchors.centerIn: parent
|
||||
|
||||
onOpened: {
|
||||
// Load settings when popup opens with data
|
||||
if (widgetData && widgetId) {
|
||||
loadWidgetSettings();
|
||||
}
|
||||
// Request focus to ensure keyboard input works
|
||||
forceActiveFocus();
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
id: bgRect
|
||||
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderM
|
||||
}
|
||||
|
||||
contentItem: FocusScope {
|
||||
id: focusScope
|
||||
focus: true
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginM
|
||||
|
||||
// Title
|
||||
RowLayout {
|
||||
id: titleRow
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
|
||||
NText {
|
||||
text: I18n.tr("system.widget-settings-title", {
|
||||
"widget": root.widgetId
|
||||
})
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
onClicked: saveAndClose()
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
id: separator
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Color.mOutline
|
||||
}
|
||||
|
||||
// Scrollable settings area
|
||||
NScrollView {
|
||||
id: scrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 100
|
||||
gradientColor: Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
width: scrollView.availableWidth
|
||||
spacing: Style.marginM
|
||||
|
||||
// Settings based on widget type
|
||||
// Will be triggered via settingsLoader.setSource()
|
||||
Loader {
|
||||
id: settingsLoader
|
||||
Layout.fillWidth: true
|
||||
onLoaded: {
|
||||
// Try to focus the first focusable item in the loaded settings
|
||||
if (item) {
|
||||
Qt.callLater(() => {
|
||||
var firstInput = findFirstFocusable(item);
|
||||
if (firstInput) {
|
||||
firstInput.forceActiveFocus();
|
||||
} else {
|
||||
focusScope.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function findFirstFocusable(item) {
|
||||
if (!item)
|
||||
return null;
|
||||
// Check if this item can accept focus
|
||||
if (item.focus !== undefined && item.focus === true)
|
||||
return item;
|
||||
// Check children
|
||||
if (item.children) {
|
||||
for (var i = 0; i < item.children.length; i++) {
|
||||
var child = item.children[i];
|
||||
if (child && child.focus !== undefined && child.focus === true)
|
||||
return child;
|
||||
var found = findFirstFocusable(child);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveTimer
|
||||
running: false
|
||||
interval: 150
|
||||
onTriggered: {
|
||||
root.updateWidgetSettings(root.sectionId, root.widgetIndex, root.settingsCache);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: settingsLoader.item
|
||||
ignoreUnknownSignals: true
|
||||
function onSettingsChanged(newSettings) {
|
||||
if (newSettings) {
|
||||
root.settingsCache = newSettings;
|
||||
saveTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveAndClose() {
|
||||
if (settingsLoader.item && typeof settingsLoader.item.saveSettings === 'function') {
|
||||
var newSettings = settingsLoader.item.saveSettings();
|
||||
if (newSettings) {
|
||||
root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings);
|
||||
}
|
||||
}
|
||||
root.close();
|
||||
}
|
||||
|
||||
function loadWidgetSettings() {
|
||||
const source = BarWidgetRegistry.widgetSettingsMap[widgetId];
|
||||
if (source) {
|
||||
var currentWidgetData = widgetData;
|
||||
if (sectionId && widgetIndex >= 0) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screen?.name || "")[sectionId];
|
||||
if (widgets && widgetIndex < widgets.length) {
|
||||
currentWidgetData = widgets[widgetIndex];
|
||||
}
|
||||
}
|
||||
settingsLoader.setSource(source, {
|
||||
"screen": screen,
|
||||
"widgetData": currentWidgetData,
|
||||
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "../../../../Helpers/QtObj2JS.js" as QtObj2JS
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
// Monitor Widgets Configuration (inline)
|
||||
NBox {
|
||||
id: root
|
||||
|
||||
required property var screen
|
||||
readonly property string screenName: screen?.name || ""
|
||||
// determine bar orientation
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
|
||||
|
||||
color: Color.mSurfaceVariant
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: content.implicitHeight + Style.margin2L
|
||||
|
||||
// Helper to get widgets for this screen (ensures override exists)
|
||||
function _getWidgetsContainer() {
|
||||
if (!Settings.hasScreenOverride(screenName, "widgets")) {
|
||||
var currentWidgets = Settings.getBarWidgetsForScreen(screenName);
|
||||
var widgetsCopy = QtObj2JS.qtObjectToPlainObject(currentWidgets);
|
||||
Settings.setScreenOverride(screenName, "widgets", widgetsCopy);
|
||||
}
|
||||
var entry = Settings.getScreenOverrideEntry(screenName);
|
||||
return entry ? entry.widgets : Settings.data.bar.widgets;
|
||||
}
|
||||
|
||||
// Persist widget changes by reassigning the override (triggers change detection)
|
||||
function _saveWidgets(widgets) {
|
||||
Settings.setScreenOverride(screenName, "widgets", widgets);
|
||||
}
|
||||
|
||||
// Widget manipulation functions
|
||||
function _addWidgetToSection(widgetId, section) {
|
||||
var newWidget = {
|
||||
"id": widgetId
|
||||
};
|
||||
if (BarWidgetRegistry.widgetHasUserSettings(widgetId)) {
|
||||
var metadata = BarWidgetRegistry.widgetMetadata[widgetId];
|
||||
if (metadata) {
|
||||
Object.keys(metadata).forEach(function (key) {
|
||||
newWidget[key] = metadata[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
var widgets = _getWidgetsContainer();
|
||||
widgets[section].push(newWidget);
|
||||
_saveWidgets(widgets);
|
||||
BarService.widgetsRevision++;
|
||||
}
|
||||
|
||||
function _removeWidgetFromSection(section, index) {
|
||||
var widgets = _getWidgetsContainer();
|
||||
if (index >= 0 && index < widgets[section].length) {
|
||||
var newArray = widgets[section].slice();
|
||||
var removedWidgets = newArray.splice(index, 1);
|
||||
widgets[section] = newArray;
|
||||
_saveWidgets(widgets);
|
||||
BarService.widgetsRevision++;
|
||||
|
||||
if (removedWidgets[0].id === "ControlCenter" && BarService.lookupWidget("ControlCenter") === undefined) {
|
||||
ToastService.showWarning(I18n.tr("toast.missing-control-center.label"), I18n.tr("toast.missing-control-center.description"), 12000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _reorderWidgetInSection(section, fromIndex, toIndex) {
|
||||
var widgets = _getWidgetsContainer();
|
||||
if (fromIndex >= 0 && fromIndex < widgets[section].length && toIndex >= 0 && toIndex < widgets[section].length) {
|
||||
var newArray = widgets[section].slice();
|
||||
var item = newArray[fromIndex];
|
||||
newArray.splice(fromIndex, 1);
|
||||
newArray.splice(toIndex, 0, item);
|
||||
widgets[section] = newArray;
|
||||
_saveWidgets(widgets);
|
||||
BarService.widgetsRevision++;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: _updateWidgetSettingsInSection does NOT increment revision
|
||||
// because it only changes settings, not widget structure
|
||||
function _updateWidgetSettingsInSection(section, index, settings) {
|
||||
var widgets = _getWidgetsContainer();
|
||||
widgets[section][index] = settings;
|
||||
_saveWidgets(widgets);
|
||||
}
|
||||
|
||||
function _moveWidgetBetweenSections(fromSection, index, toSection) {
|
||||
var widgets = _getWidgetsContainer();
|
||||
if (index >= 0 && index < widgets[fromSection].length) {
|
||||
var widget = widgets[fromSection][index];
|
||||
var sourceArray = widgets[fromSection].slice();
|
||||
sourceArray.splice(index, 1);
|
||||
widgets[fromSection] = sourceArray;
|
||||
var targetArray = widgets[toSection].slice();
|
||||
targetArray.push(widget);
|
||||
widgets[toSection] = targetArray;
|
||||
_saveWidgets(widgets);
|
||||
BarService.widgetsRevision++;
|
||||
}
|
||||
}
|
||||
|
||||
// Available widgets ListModel
|
||||
function updateAvailableWidgetsModel() {
|
||||
availableWidgetsModel.clear();
|
||||
var widgetIds = BarWidgetRegistry.getAvailableWidgets();
|
||||
if (!widgetIds)
|
||||
return;
|
||||
for (var i = 0; i < widgetIds.length; i++) {
|
||||
var id = widgetIds[i];
|
||||
var displayName = id;
|
||||
const badges = [];
|
||||
if (BarWidgetRegistry.isPluginWidget(id)) {
|
||||
var pluginId = id.replace("plugin:", "");
|
||||
var manifest = PluginRegistry.getPluginManifest(pluginId);
|
||||
if (manifest && manifest.name) {
|
||||
displayName = manifest.name;
|
||||
} else {
|
||||
displayName = pluginId;
|
||||
}
|
||||
badges.push({
|
||||
"icon": "plugin",
|
||||
"color": Color.mSecondary
|
||||
});
|
||||
}
|
||||
if (BarWidgetRegistry.isCpuIntensive(id)) {
|
||||
badges.push({
|
||||
"icon": "cpu-intensive",
|
||||
"color": Color.mSecondary
|
||||
});
|
||||
}
|
||||
availableWidgetsModel.append({
|
||||
"key": id,
|
||||
"name": displayName,
|
||||
"badges": badges
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: Qt.callLater(updateAvailableWidgetsModel)
|
||||
|
||||
ListModel {
|
||||
id: availableWidgetsModel
|
||||
}
|
||||
|
||||
// Get effective widgets for this screen
|
||||
readonly property var effectiveWidgets: Settings.getBarWidgetsForScreen(screenName)
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.bar.widgets-desc")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Left Section
|
||||
NSectionEditor {
|
||||
sectionName: root.barIsVertical ? I18n.tr("positions.top") : I18n.tr("positions.left")
|
||||
sectionId: "left"
|
||||
barIsVertical: root.barIsVertical
|
||||
screen: root.screen
|
||||
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
|
||||
widgetRegistry: BarWidgetRegistry
|
||||
widgetModel: root.effectiveWidgets.left
|
||||
availableWidgets: availableWidgetsModel
|
||||
onAddWidget: (widgetId, section) => root._addWidgetToSection(widgetId, section)
|
||||
onRemoveWidget: (section, index) => root._removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => root._reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => root._updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => root._moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onOpenPluginSettingsRequested: manifest => pluginSettingsDialog.openPluginSettings(manifest)
|
||||
}
|
||||
|
||||
// Center Section
|
||||
NSectionEditor {
|
||||
sectionName: I18n.tr("positions.center")
|
||||
sectionId: "center"
|
||||
barIsVertical: root.barIsVertical
|
||||
screen: root.screen
|
||||
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
|
||||
widgetRegistry: BarWidgetRegistry
|
||||
widgetModel: root.effectiveWidgets.center
|
||||
availableWidgets: availableWidgetsModel
|
||||
onAddWidget: (widgetId, section) => root._addWidgetToSection(widgetId, section)
|
||||
onRemoveWidget: (section, index) => root._removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => root._reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => root._updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => root._moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onOpenPluginSettingsRequested: manifest => pluginSettingsDialog.openPluginSettings(manifest)
|
||||
}
|
||||
|
||||
// Right Section
|
||||
NSectionEditor {
|
||||
sectionName: root.barIsVertical ? I18n.tr("positions.bottom") : I18n.tr("positions.right")
|
||||
sectionId: "right"
|
||||
barIsVertical: root.barIsVertical
|
||||
screen: root.screen
|
||||
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
|
||||
widgetRegistry: BarWidgetRegistry
|
||||
widgetModel: root.effectiveWidgets.right
|
||||
availableWidgets: availableWidgetsModel
|
||||
onAddWidget: (widgetId, section) => root._addWidgetToSection(widgetId, section)
|
||||
onRemoveWidget: (section, index) => root._removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => root._reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => root._updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => root._moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onOpenPluginSettingsRequested: manifest => pluginSettingsDialog.openPluginSettings(manifest)
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin settings dialog
|
||||
NPluginSettingsPopup {
|
||||
id: pluginSettingsDialog
|
||||
parent: Overlay.overlay
|
||||
showToastOnSave: false
|
||||
}
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon
|
||||
property bool valueShowText: widgetData.showText !== undefined ? widgetData.showText : widgetMetadata.showText
|
||||
property string valueHideMode: widgetData.hideMode !== undefined ? widgetData.hideMode : widgetMetadata.hideMode
|
||||
property string valueScrollingMode: widgetData.scrollingMode || widgetMetadata.scrollingMode
|
||||
property int valueMaxWidth: widgetData.maxWidth !== undefined ? widgetData.maxWidth : widgetMetadata.maxWidth
|
||||
property bool valueUseFixedWidth: widgetData.useFixedWidth !== undefined ? widgetData.useFixedWidth : widgetMetadata.useFixedWidth
|
||||
property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons
|
||||
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
|
||||
|
||||
Component.onCompleted: {
|
||||
if (widgetData && widgetData.hideMode !== undefined) {
|
||||
valueHideMode = widgetData.hideMode;
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.hideMode = valueHideMode;
|
||||
settings.showIcon = valueShowIcon;
|
||||
settings.showText = valueShowText;
|
||||
settings.scrollingMode = valueScrollingMode;
|
||||
settings.maxWidth = parseInt(widthInput.text) || widgetMetadata.maxWidth;
|
||||
settings.useFixedWidth = valueUseFixedWidth;
|
||||
settings.colorizeIcons = valueColorizeIcons;
|
||||
settings.textColor = valueTextColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.hide-mode-label")
|
||||
description: I18n.tr("bar.active-window.hide-mode-description")
|
||||
model: [
|
||||
{
|
||||
"key": "visible",
|
||||
"name": I18n.tr("hide-modes.visible")
|
||||
},
|
||||
{
|
||||
"key": "hidden",
|
||||
"name": I18n.tr("hide-modes.hidden")
|
||||
},
|
||||
{
|
||||
"key": "transparent",
|
||||
"name": I18n.tr("hide-modes.transparent")
|
||||
}
|
||||
]
|
||||
currentKey: root.valueHideMode
|
||||
onSelected: key => {
|
||||
root.valueHideMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.hideMode
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-color")
|
||||
currentKey: valueTextColor
|
||||
onSelected: key => {
|
||||
valueTextColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.textColor
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.active-window.show-app-text-label")
|
||||
description: I18n.tr("bar.active-window.show-app-text-description")
|
||||
checked: root.valueShowText
|
||||
onToggled: checked => {
|
||||
root.valueShowText = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showText
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.active-window.show-app-icon-label")
|
||||
description: I18n.tr("bar.active-window.show-app-icon-description")
|
||||
checked: root.valueShowIcon
|
||||
onToggled: checked => {
|
||||
root.valueShowIcon = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showIcon
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.tray.colorize-icons-label")
|
||||
description: I18n.tr("bar.active-window.colorize-icons-description")
|
||||
checked: root.valueColorizeIcons
|
||||
onToggled: checked => {
|
||||
root.valueColorizeIcons = checked;
|
||||
saveSettings();
|
||||
}
|
||||
visible: root.valueShowIcon
|
||||
defaultValue: widgetMetadata.colorizeIcons
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: widthInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.max-width-label")
|
||||
description: I18n.tr("bar.media-mini.max-width-description")
|
||||
placeholderText: widgetMetadata.maxWidth
|
||||
text: valueMaxWidth
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: String(widgetMetadata.maxWidth)
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.media-mini.use-fixed-width-label")
|
||||
description: I18n.tr("bar.media-mini.use-fixed-width-description")
|
||||
checked: valueUseFixedWidth
|
||||
onToggled: checked => {
|
||||
valueUseFixedWidth = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.useFixedWidth
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("bar.media-mini.scrolling-mode-label")
|
||||
description: I18n.tr("bar.active-window.scrolling-mode-description")
|
||||
model: [
|
||||
{
|
||||
"key": "always",
|
||||
"name": I18n.tr("options.scrolling-modes.always")
|
||||
},
|
||||
{
|
||||
"key": "hover",
|
||||
"name": I18n.tr("options.scrolling-modes.hover")
|
||||
},
|
||||
{
|
||||
"key": "never",
|
||||
"name": I18n.tr("options.scrolling-modes.never")
|
||||
}
|
||||
]
|
||||
currentKey: valueScrollingMode
|
||||
defaultValue: widgetMetadata.scrollingMode
|
||||
onSelected: key => {
|
||||
valueScrollingMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
minimumWidth: 200
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property bool valueHideWhenIdle: widgetData.hideWhenIdle !== undefined ? widgetData.hideWhenIdle : widgetMetadata.hideWhenIdle
|
||||
property string valueColorName: widgetData.colorName !== undefined ? widgetData.colorName : widgetMetadata.colorName
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.width = parseInt(widthInput.text) || widgetMetadata.width;
|
||||
settings.hideWhenIdle = valueHideWhenIdle;
|
||||
settings.colorName = valueColorName;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: widthInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("common.width")
|
||||
description: I18n.tr("bar.audio-visualizer.width-description")
|
||||
text: widgetData.width || widgetMetadata.width
|
||||
placeholderText: I18n.tr("placeholders.enter-width-pixels")
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: String(widgetMetadata.width)
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.audio-visualizer.color-name-label")
|
||||
description: I18n.tr("bar.audio-visualizer.color-name-description")
|
||||
currentKey: root.valueColorName
|
||||
onSelected: key => {
|
||||
root.valueColorName = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.colorName
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.audio-visualizer.hide-when-idle-label")
|
||||
description: I18n.tr("bar.audio-visualizer.hide-when-idle-description")
|
||||
checked: valueHideWhenIdle
|
||||
onToggled: checked => {
|
||||
valueHideWhenIdle = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.hideWhenIdle
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Hardware
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
|
||||
property string valueDeviceNativePath: widgetData.deviceNativePath !== undefined ? widgetData.deviceNativePath : widgetMetadata.deviceNativePath
|
||||
property bool valueShowPowerProfiles: widgetData.showPowerProfiles !== undefined ? widgetData.showPowerProfiles : widgetMetadata.showPowerProfiles
|
||||
property bool valueShowNoctaliaPerformance: widgetData.showNoctaliaPerformance !== undefined ? widgetData.showNoctaliaPerformance : widgetMetadata.showNoctaliaPerformance
|
||||
property bool valueHideIfNotDetected: widgetData.hideIfNotDetected !== undefined ? widgetData.hideIfNotDetected : widgetMetadata.hideIfNotDetected
|
||||
property bool valueHideIfIdle: widgetData.hideIfIdle !== undefined ? widgetData.hideIfIdle : widgetMetadata.hideIfIdle
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
if (widgetData && widgetData.id) {
|
||||
settings.id = widgetData.id;
|
||||
}
|
||||
settings.displayMode = valueDisplayMode;
|
||||
settings.showPowerProfiles = valueShowPowerProfiles;
|
||||
settings.showNoctaliaPerformance = valueShowNoctaliaPerformance;
|
||||
settings.hideIfNotDetected = valueHideIfNotDetected;
|
||||
settings.hideIfIdle = valueHideIfIdle;
|
||||
settings.deviceNativePath = valueDeviceNativePath;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
id: deviceComboBox
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.battery.device-label")
|
||||
description: I18n.tr("bar.battery.device-description")
|
||||
minimumWidth: 240
|
||||
model: BatteryService.deviceModel
|
||||
currentKey: root.valueDeviceNativePath
|
||||
onSelected: key => {
|
||||
root.valueDeviceNativePath = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.deviceNativePath
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("bar.battery.display-mode-description")
|
||||
minimumWidth: 240
|
||||
model: [
|
||||
{
|
||||
"key": "graphic",
|
||||
"name": I18n.tr("bar.battery.display-mode-graphic")
|
||||
},
|
||||
{
|
||||
"key": "graphic-clean",
|
||||
"name": I18n.tr("bar.battery.display-mode-graphic-clean")
|
||||
},
|
||||
{
|
||||
"key": "icon-hover",
|
||||
"name": I18n.tr("bar.battery.display-mode-icon-hover")
|
||||
},
|
||||
{
|
||||
"key": "icon-always",
|
||||
"name": I18n.tr("bar.battery.display-mode-icon-always")
|
||||
},
|
||||
{
|
||||
"key": "icon-only",
|
||||
"name": I18n.tr("bar.battery.display-mode-icon-only")
|
||||
}
|
||||
]
|
||||
currentKey: root.valueDisplayMode
|
||||
onSelected: key => {
|
||||
root.valueDisplayMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.displayMode
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.battery.hide-if-not-detected-label")
|
||||
description: I18n.tr("bar.battery.hide-if-not-detected-description")
|
||||
checked: valueHideIfNotDetected
|
||||
onToggled: checked => {
|
||||
valueHideIfNotDetected = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.hideIfNotDetected
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.battery.hide-if-idle-label")
|
||||
description: I18n.tr("bar.battery.hide-if-idle-description")
|
||||
checked: valueHideIfIdle
|
||||
onToggled: checked => {
|
||||
valueHideIfIdle = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.hideIfIdle
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.battery.show-power-profile-label")
|
||||
description: I18n.tr("bar.battery.show-power-profile-description")
|
||||
checked: valueShowPowerProfiles
|
||||
onToggled: checked => {
|
||||
valueShowPowerProfiles = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showPowerProfiles
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.battery.show-noctalia-performance-label")
|
||||
description: I18n.tr("bar.battery.show-noctalia-performance-description")
|
||||
checked: valueShowNoctaliaPerformance
|
||||
onToggled: checked => {
|
||||
valueShowNoctaliaPerformance = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showNoctaliaPerformance
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.displayMode = valueDisplayMode;
|
||||
settings.iconColor = valueIconColor;
|
||||
settings.textColor = valueTextColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("bar.volume.display-mode-description")
|
||||
minimumWidth: 200
|
||||
model: [
|
||||
{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("display-modes.on-hover")
|
||||
},
|
||||
{
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("display-modes.always-show")
|
||||
},
|
||||
{
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("display-modes.always-hide")
|
||||
}
|
||||
]
|
||||
currentKey: root.valueDisplayMode
|
||||
onSelected: key => {
|
||||
root.valueDisplayMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.displayMode
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
currentKey: valueTextColor
|
||||
onSelected: key => {
|
||||
valueTextColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.textColor
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
|
||||
property bool valueApplyToAllMonitors: widgetData.applyToAllMonitors !== undefined ? widgetData.applyToAllMonitors : widgetMetadata.applyToAllMonitors
|
||||
|
||||
readonly property bool hasMultipleMonitors: (Quickshell.screens || []).length > 1
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.displayMode = valueDisplayMode;
|
||||
settings.iconColor = valueIconColor;
|
||||
settings.textColor = valueTextColor;
|
||||
settings.applyToAllMonitors = valueApplyToAllMonitors;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("bar.volume.display-mode-description")
|
||||
minimumWidth: 200
|
||||
model: [
|
||||
{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("display-modes.on-hover")
|
||||
},
|
||||
{
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("display-modes.always-show")
|
||||
},
|
||||
{
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("display-modes.always-hide")
|
||||
}
|
||||
]
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => {
|
||||
valueDisplayMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.displayMode
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
currentKey: valueTextColor
|
||||
onSelected: key => {
|
||||
valueTextColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.textColor
|
||||
}
|
||||
|
||||
NToggle {
|
||||
visible: hasMultipleMonitors
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.brightness.apply-all-label")
|
||||
description: I18n.tr("bar.brightness.apply-all-description")
|
||||
checked: valueApplyToAllMonitors
|
||||
onToggled: checked => {
|
||||
valueApplyToAllMonitors = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.applyToAllMonitors
|
||||
}
|
||||
}
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
width: 700
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property string valueClockColor: widgetData.clockColor !== undefined ? widgetData.clockColor : widgetMetadata.clockColor
|
||||
property bool valueUseCustomFont: widgetData.useCustomFont !== undefined ? widgetData.useCustomFont : widgetMetadata.useCustomFont
|
||||
property string valueCustomFont: widgetData.customFont !== undefined ? widgetData.customFont : widgetMetadata.customFont
|
||||
property string valueFormatHorizontal: widgetData.formatHorizontal !== undefined ? widgetData.formatHorizontal : widgetMetadata.formatHorizontal
|
||||
property string valueFormatVertical: widgetData.formatVertical !== undefined ? widgetData.formatVertical : widgetMetadata.formatVertical
|
||||
property string valueTooltipFormat: widgetData.tooltipFormat !== undefined ? widgetData.tooltipFormat : widgetMetadata.tooltipFormat
|
||||
|
||||
readonly property color textColor: Color.resolveColorKey(valueClockColor)
|
||||
|
||||
// Track the currently focused input field
|
||||
property var focusedInput: null
|
||||
property int focusedLineIndex: -1
|
||||
|
||||
readonly property var now: Time.now
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.clockColor = valueClockColor;
|
||||
settings.useCustomFont = valueUseCustomFont;
|
||||
settings.customFont = valueCustomFont;
|
||||
settings.formatHorizontal = valueFormatHorizontal.trim();
|
||||
settings.formatVertical = valueFormatVertical.trim();
|
||||
settings.tooltipFormat = valueTooltipFormat.trim();
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
// Function to insert token at cursor position in the focused input
|
||||
function insertToken(token) {
|
||||
if (!focusedInput || !focusedInput.inputItem) {
|
||||
// If no input is focused, default to horiz
|
||||
if (inputHoriz.inputItem) {
|
||||
inputHoriz.inputItem.focus = true;
|
||||
focusedInput = inputHoriz;
|
||||
}
|
||||
}
|
||||
|
||||
if (focusedInput && focusedInput.inputItem) {
|
||||
var input = focusedInput.inputItem;
|
||||
var cursorPos = input.cursorPosition;
|
||||
var currentText = input.text;
|
||||
|
||||
// Insert token at cursor position
|
||||
var newText = currentText.substring(0, cursorPos) + token + currentText.substring(cursorPos);
|
||||
input.text = newText + " ";
|
||||
|
||||
// Move cursor after the inserted token
|
||||
input.cursorPosition = cursorPos + token.length + 1;
|
||||
|
||||
// Ensure the input keeps focus
|
||||
input.focus = true;
|
||||
}
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
currentKey: valueClockColor
|
||||
onSelected: key => {
|
||||
valueClockColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.clockColor
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.clock.use-custom-font-label")
|
||||
description: I18n.tr("bar.clock.use-custom-font-description")
|
||||
checked: valueUseCustomFont
|
||||
onToggled: checked => {
|
||||
valueUseCustomFont = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.useCustomFont
|
||||
}
|
||||
|
||||
NSearchableComboBox {
|
||||
Layout.fillWidth: true
|
||||
visible: valueUseCustomFont
|
||||
label: I18n.tr("bar.clock.custom-font-label")
|
||||
description: I18n.tr("bar.clock.custom-font-description")
|
||||
model: FontService.availableFonts
|
||||
currentKey: valueCustomFont
|
||||
placeholder: I18n.tr("bar.clock.custom-font-placeholder")
|
||||
searchPlaceholder: I18n.tr("bar.clock.custom-font-search-placeholder")
|
||||
popupHeight: 420
|
||||
minimumWidth: 300
|
||||
onSelected: function (key) {
|
||||
valueCustomFont = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: Settings.data.ui.fontDefault
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("bar.clock.clock-display-label")
|
||||
description: I18n.tr("bar.clock.clock-display-description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: main
|
||||
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1 // Equal sizing hint
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
|
||||
NTextInput {
|
||||
id: inputHoriz
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.clock.horizontal-bar-label")
|
||||
description: I18n.tr("bar.clock.horizontal-bar-description")
|
||||
placeholderText: "HH:mm ddd, MMM dd"
|
||||
text: valueFormatHorizontal
|
||||
onTextChanged: {
|
||||
valueFormatHorizontal = text;
|
||||
saveSettings();
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (inputItem) {
|
||||
inputItem.onActiveFocusChanged.connect(function () {
|
||||
if (inputItem.activeFocus) {
|
||||
root.focusedInput = inputHoriz;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
defaultValue: widgetMetadata.formatHorizontal
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: inputVert
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.clock.vertical-bar-label")
|
||||
description: I18n.tr("bar.clock.vertical-bar-description")
|
||||
// Tokens are Qt format tokens and must not be localized
|
||||
placeholderText: "HH mm dd MM"
|
||||
text: valueFormatVertical
|
||||
onTextChanged: {
|
||||
valueFormatVertical = text;
|
||||
saveSettings();
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (inputItem) {
|
||||
inputItem.onActiveFocusChanged.connect(function () {
|
||||
if (inputItem.activeFocus) {
|
||||
root.focusedInput = inputVert;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
defaultValue: widgetMetadata.formatVertical
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: inputTooltip
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.clock.tooltip-format-label")
|
||||
description: I18n.tr("bar.clock.tooltip-format-description")
|
||||
placeholderText: "HH:mm, ddd MMM dd"
|
||||
text: valueTooltipFormat
|
||||
onTextChanged: {
|
||||
valueTooltipFormat = text;
|
||||
saveSettings();
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (inputItem) {
|
||||
inputItem.onActiveFocusChanged.connect(function () {
|
||||
if (inputItem.activeFocus) {
|
||||
root.focusedInput = inputTooltip;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
defaultValue: widgetMetadata.tooltipFormat
|
||||
}
|
||||
}
|
||||
|
||||
// --------------
|
||||
// Preview
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
Layout.fillWidth: false
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("bar.clock.preview")
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 320
|
||||
Layout.preferredHeight: 160 // Fixed height instead of fillHeight
|
||||
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM
|
||||
border.color: Color.mSecondary
|
||||
border.width: Style.borderS
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
anchors.centerIn: parent
|
||||
|
||||
ColumnLayout {
|
||||
spacing: -2
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
// Horizontal
|
||||
Repeater {
|
||||
Layout.topMargin: Style.marginM
|
||||
model: I18n.locale.toString(now, valueFormatHorizontal.trim()).split("\\n")
|
||||
delegate: NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
family: valueUseCustomFont && valueCustomFont ? valueCustomFont : Settings.data.ui.fontDefault
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightBold
|
||||
color: textColor
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Vertical
|
||||
ColumnLayout {
|
||||
spacing: -2
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Repeater {
|
||||
Layout.topMargin: Style.marginM
|
||||
model: I18n.locale.toString(now, valueFormatVertical.trim()).split(" ")
|
||||
delegate: NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
family: valueUseCustomFont && valueCustomFont ? valueCustomFont : Settings.data.ui.fontDefault
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightBold
|
||||
color: textColor
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.topMargin: Style.marginM
|
||||
Layout.bottomMargin: Style.marginM
|
||||
}
|
||||
|
||||
NDateTimeTokens {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 300
|
||||
|
||||
// Connect to token clicked signal if NDateTimeTokens provides it
|
||||
onTokenClicked: token => root.insertToken(token)
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
|
||||
property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo
|
||||
property string valueCustomIconPath: widgetData.customIconPath !== undefined ? widgetData.customIconPath : widgetMetadata.customIconPath
|
||||
property bool valueEnableColorization: widgetData.enableColorization !== undefined ? widgetData.enableColorization : widgetMetadata.enableColorization
|
||||
property string valueColorizeSystemIcon: widgetData.colorizeSystemIcon !== undefined ? widgetData.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.icon = valueIcon;
|
||||
settings.useDistroLogo = valueUseDistroLogo;
|
||||
settings.customIconPath = valueCustomIconPath;
|
||||
settings.enableColorization = valueEnableColorization;
|
||||
settings.colorizeSystemIcon = valueColorizeSystemIcon;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.control-center.use-distro-logo-label")
|
||||
description: I18n.tr("bar.control-center.use-distro-logo-description")
|
||||
checked: valueUseDistroLogo
|
||||
onToggled: checked => {
|
||||
valueUseDistroLogo = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.useDistroLogo
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.custom-button.enable-colorization-label")
|
||||
description: I18n.tr("bar.control-center.enable-colorization-description")
|
||||
checked: valueEnableColorization
|
||||
onToggled: checked => {
|
||||
valueEnableColorization = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.enableColorization
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
visible: valueEnableColorization
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
description: I18n.tr("bar.control-center.color-selection-description")
|
||||
currentKey: valueColorizeSystemIcon
|
||||
onSelected: function (key) {
|
||||
valueColorizeSystemIcon = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.colorizeSystemIcon
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("common.icon")
|
||||
description: I18n.tr("bar.control-center.icon-description")
|
||||
}
|
||||
|
||||
NImageRounded {
|
||||
Layout.preferredWidth: Style.fontSizeXL * 2
|
||||
Layout.preferredHeight: Style.fontSizeXL * 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: Math.min(Style.radiusL, Layout.preferredWidth / 2)
|
||||
imagePath: valueCustomIconPath
|
||||
visible: valueCustomIconPath !== "" && !valueUseDistroLogo
|
||||
}
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: valueIcon
|
||||
pointSize: Style.fontSizeXXL * 1.5
|
||||
visible: valueIcon !== "" && valueCustomIconPath === "" && !valueUseDistroLogo
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
NButton {
|
||||
enabled: !valueUseDistroLogo
|
||||
text: I18n.tr("bar.control-center.browse-library")
|
||||
onClicked: iconPicker.open()
|
||||
}
|
||||
|
||||
NButton {
|
||||
enabled: !valueUseDistroLogo
|
||||
text: I18n.tr("bar.control-center.browse-file")
|
||||
onClicked: imagePicker.openFilePicker()
|
||||
}
|
||||
}
|
||||
|
||||
NIconPicker {
|
||||
id: iconPicker
|
||||
initialIcon: valueIcon
|
||||
onIconSelected: iconName => {
|
||||
valueIcon = iconName;
|
||||
valueCustomIconPath = "";
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: imagePicker
|
||||
title: I18n.tr("bar.control-center.select-custom-icon")
|
||||
selectionMode: "files"
|
||||
nameFilters: ImageCacheService.basicImageFilters.concat(["*.svg"])
|
||||
initialPath: Quickshell.env("HOME")
|
||||
onAccepted: paths => {
|
||||
if (paths.length > 0) {
|
||||
valueCustomIconPath = paths[0]; // Use first selected file
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+512
@@ -0,0 +1,512 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
|
||||
property bool valueTextStream: widgetData.textStream !== undefined ? widgetData.textStream : widgetMetadata.textStream
|
||||
property bool valueParseJson: widgetData.parseJson !== undefined ? widgetData.parseJson : widgetMetadata.parseJson
|
||||
property int valueMaxTextLengthHorizontal: widgetData?.maxTextLength?.horizontal ?? widgetMetadata?.maxTextLength?.horizontal
|
||||
property int valueMaxTextLengthVertical: widgetData?.maxTextLength?.vertical ?? widgetMetadata?.maxTextLength?.vertical
|
||||
property string valueHideMode: (widgetData.hideMode !== undefined) ? widgetData.hideMode : widgetMetadata.hideMode
|
||||
property bool valueShowIcon: (widgetData.showIcon !== undefined) ? widgetData.showIcon : widgetMetadata.showIcon
|
||||
property bool valueShowExecTooltip: widgetData.showExecTooltip !== undefined ? widgetData.showExecTooltip : widgetMetadata.showExecTooltip
|
||||
property bool valueShowTextTooltip: widgetData.showTextTooltip !== undefined ? widgetData.showTextTooltip : widgetMetadata.showTextTooltip
|
||||
property bool valueEnableColorization: widgetData.enableColorization !== undefined ? widgetData.enableColorization : widgetMetadata.enableColorization
|
||||
property string valueColorizeSystemIcon: widgetData.colorizeSystemIcon !== undefined ? widgetData.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon
|
||||
property string valueIpcIdentifier: widgetData.ipcIdentifier !== undefined ? widgetData.ipcIdentifier : widgetMetadata.ipcIdentifier
|
||||
property string valueGeneralTooltipText: widgetData.generalTooltipText !== undefined ? widgetData.generalTooltipText : widgetMetadata.generalTooltipText
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.icon = valueIcon;
|
||||
settings.leftClickExec = leftClickExecInput.text;
|
||||
settings.leftClickUpdateText = leftClickUpdateText.checked;
|
||||
settings.rightClickExec = rightClickExecInput.text;
|
||||
settings.rightClickUpdateText = rightClickUpdateText.checked;
|
||||
settings.middleClickExec = middleClickExecInput.text;
|
||||
settings.middleClickUpdateText = middleClickUpdateText.checked;
|
||||
settings.wheelMode = separateWheelToggle.internalChecked ? "separate" : "unified";
|
||||
settings.wheelExec = wheelExecInput.text;
|
||||
settings.wheelUpExec = wheelUpExecInput.text;
|
||||
settings.wheelDownExec = wheelDownExecInput.text;
|
||||
settings.wheelUpdateText = wheelUpdateText.checked;
|
||||
settings.wheelUpUpdateText = wheelUpUpdateText.checked;
|
||||
settings.wheelDownUpdateText = wheelDownUpdateText.checked;
|
||||
settings.textCommand = textCommandInput.text;
|
||||
settings.textCollapse = textCollapseInput.text;
|
||||
settings.textStream = valueTextStream;
|
||||
settings.parseJson = valueParseJson;
|
||||
settings.showIcon = valueShowIcon;
|
||||
settings.showExecTooltip = valueShowExecTooltip;
|
||||
settings.showTextTooltip = valueShowTextTooltip;
|
||||
settings.hideMode = valueHideMode;
|
||||
settings.maxTextLength = {
|
||||
"horizontal": valueMaxTextLengthHorizontal,
|
||||
"vertical": valueMaxTextLengthVertical
|
||||
};
|
||||
settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10);
|
||||
settings.enableColorization = valueEnableColorization;
|
||||
settings.colorizeSystemIcon = valueColorizeSystemIcon;
|
||||
settings.ipcIdentifier = valueIpcIdentifier;
|
||||
settings.generalTooltipText = valueGeneralTooltipText;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("common.icon")
|
||||
description: I18n.tr("bar.custom-button.icon-description")
|
||||
}
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: valueIcon
|
||||
pointSize: Style.fontSizeXL
|
||||
visible: valueIcon !== ""
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.browse")
|
||||
onClicked: iconPicker.open()
|
||||
}
|
||||
}
|
||||
|
||||
NIconPicker {
|
||||
id: iconPicker
|
||||
initialIcon: valueIcon
|
||||
onIconSelected: function (iconName) {
|
||||
valueIcon = iconName;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showIconToggle
|
||||
label: I18n.tr("bar.custom-button.show-icon-label")
|
||||
description: I18n.tr("bar.custom-button.show-icon-description")
|
||||
checked: valueShowIcon
|
||||
onToggled: checked => {
|
||||
valueShowIcon = checked;
|
||||
saveSettings();
|
||||
}
|
||||
visible: textCommandInput.text !== ""
|
||||
defaultValue: widgetMetadata.showIcon
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.custom-button.enable-colorization-label")
|
||||
description: I18n.tr("bar.custom-button.enable-colorization-description")
|
||||
checked: valueEnableColorization
|
||||
onToggled: checked => {
|
||||
valueEnableColorization = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.enableColorization
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
visible: valueEnableColorization
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
description: I18n.tr("bar.custom-button.color-selection-description")
|
||||
currentKey: valueColorizeSystemIcon
|
||||
onSelected: key => {
|
||||
valueColorizeSystemIcon = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.colorizeSystemIcon
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.general-tooltip-text-label")
|
||||
description: I18n.tr("bar.custom-button.general-tooltip-text-description")
|
||||
placeholderText: I18n.tr("placeholders.enter-tooltip")
|
||||
text: valueGeneralTooltipText
|
||||
onTextChanged: {
|
||||
valueGeneralTooltipText = text;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.generalTooltipText
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showExecTooltipToggle
|
||||
label: I18n.tr("bar.custom-button.show-exec-tooltip-label")
|
||||
description: I18n.tr("bar.custom-button.show-exec-tooltip-description")
|
||||
checked: valueShowExecTooltip
|
||||
onToggled: checked => {
|
||||
valueShowExecTooltip = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showExecTooltip
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showTextTooltipToggle
|
||||
label: I18n.tr("bar.custom-button.show-text-tooltip-label")
|
||||
description: I18n.tr("bar.custom-button.show-text-tooltip-description")
|
||||
checked: valueShowTextTooltip
|
||||
onToggled: checked => {
|
||||
valueShowTextTooltip = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showTextTooltip
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.ipc-identifier-label")
|
||||
description: I18n.tr("bar.custom-button.ipc-identifier-description")
|
||||
placeholderText: I18n.tr("placeholders.enter-ipc-identifier")
|
||||
text: valueIpcIdentifier
|
||||
onTextChanged: {
|
||||
valueIpcIdentifier = text;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.ipcIdentifier
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NTextInput {
|
||||
id: leftClickExecInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.left-click-label")
|
||||
description: I18n.tr("bar.custom-button.left-click-description")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: widgetData?.leftClickExec || widgetMetadata.leftClickExec
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: widgetMetadata.leftClickExec
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: leftClickUpdateText
|
||||
enabled: !valueTextStream
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||
Layout.bottomMargin: Style.marginS
|
||||
onEntered: TooltipService.show(leftClickUpdateText, I18n.tr("bar.custom-button.left-click-update-text"))
|
||||
onExited: TooltipService.hide()
|
||||
checked: widgetData?.leftClickUpdateText ?? widgetMetadata.leftClickUpdateText
|
||||
onToggled: isChecked => {
|
||||
checked = isChecked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.leftClickUpdateText
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NTextInput {
|
||||
id: rightClickExecInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.right-click-label")
|
||||
description: I18n.tr("bar.custom-button.right-click-description")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: widgetData?.rightClickExec || widgetMetadata.rightClickExec
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: widgetMetadata.rightClickExec
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: rightClickUpdateText
|
||||
enabled: !valueTextStream
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||
Layout.bottomMargin: Style.marginS
|
||||
onEntered: TooltipService.show(rightClickUpdateText, I18n.tr("bar.custom-button.right-click-update-text"))
|
||||
onExited: TooltipService.hide()
|
||||
checked: widgetData?.rightClickUpdateText ?? widgetMetadata.rightClickUpdateText
|
||||
onToggled: isChecked => {
|
||||
checked = isChecked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.rightClickUpdateText
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NTextInput {
|
||||
id: middleClickExecInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.middle-click-label")
|
||||
description: I18n.tr("bar.custom-button.middle-click-description")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: widgetData.middleClickExec || widgetMetadata.middleClickExec
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: widgetMetadata.middleClickExec
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: middleClickUpdateText
|
||||
enabled: !valueTextStream
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||
Layout.bottomMargin: Style.marginS
|
||||
onEntered: TooltipService.show(middleClickUpdateText, I18n.tr("bar.custom-button.middle-click-update-text"))
|
||||
onExited: TooltipService.hide()
|
||||
checked: widgetData?.middleClickUpdateText ?? widgetMetadata.middleClickUpdateText
|
||||
onToggled: isChecked => {
|
||||
checked = isChecked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.middleClickUpdateText
|
||||
}
|
||||
}
|
||||
|
||||
// Wheel command settings
|
||||
NToggle {
|
||||
id: separateWheelToggle
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.wheel-mode-separate-label")
|
||||
description: I18n.tr("bar.custom-button.wheel-mode-separate-description")
|
||||
property bool internalChecked: (widgetData?.wheelMode || widgetMetadata?.wheelMode) === "separate"
|
||||
checked: internalChecked
|
||||
onToggled: checked => {
|
||||
internalChecked = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.wheelMode === "separate"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
id: unifiedWheelLayout
|
||||
visible: !separateWheelToggle.checked
|
||||
spacing: Style.marginM
|
||||
|
||||
NTextInput {
|
||||
id: wheelExecInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.wheel-label")
|
||||
description: I18n.tr("bar.custom-button.wheel-description")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: widgetData?.wheelExec || widgetMetadata?.wheelExec
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: widgetMetadata.wheelExec
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: wheelUpdateText
|
||||
enabled: !valueTextStream
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||
Layout.bottomMargin: Style.marginS
|
||||
onEntered: TooltipService.show(wheelUpdateText, I18n.tr("bar.custom-button.wheel-update-text"))
|
||||
onExited: TooltipService.hide()
|
||||
checked: widgetData?.wheelUpdateText ?? widgetMetadata?.wheelUpdateText
|
||||
onToggled: isChecked => {
|
||||
checked = isChecked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.wheelUpdateText
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: separatedWheelLayout
|
||||
Layout.fillWidth: true
|
||||
visible: separateWheelToggle.checked
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NTextInput {
|
||||
id: wheelUpExecInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.wheel-up-label")
|
||||
description: I18n.tr("bar.custom-button.wheel-up-description")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: widgetData?.wheelUpExec || widgetMetadata?.wheelUpExec
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: widgetMetadata.wheelUpExec
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: wheelUpUpdateText
|
||||
enabled: !valueTextStream
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||
Layout.bottomMargin: Style.marginS
|
||||
onEntered: TooltipService.show(wheelUpUpdateText, I18n.tr("bar.custom-button.wheel-update-text"))
|
||||
onExited: TooltipService.hide()
|
||||
checked: (widgetData?.wheelUpUpdateText !== undefined) ? widgetData.wheelUpUpdateText : widgetMetadata?.wheelUpUpdateText
|
||||
onToggled: isChecked => {
|
||||
checked = isChecked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.wheelUpUpdateText
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NTextInput {
|
||||
id: wheelDownExecInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.wheel-down-label")
|
||||
description: I18n.tr("bar.custom-button.wheel-down-description")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: widgetData?.wheelDownExec || widgetMetadata?.wheelDownExec
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: widgetMetadata.wheelDownExec
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: wheelDownUpdateText
|
||||
enabled: !valueTextStream
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||
Layout.bottomMargin: Style.marginS
|
||||
onEntered: TooltipService.show(wheelDownUpdateText, I18n.tr("bar.custom-button.wheel-update-text"))
|
||||
onExited: TooltipService.hide()
|
||||
checked: (widgetData?.wheelDownUpdateText !== undefined) ? widgetData.wheelDownUpdateText : widgetMetadata?.wheelDownUpdateText
|
||||
onToggled: isChecked => {
|
||||
checked = isChecked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.wheelDownUpdateText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("bar.custom-button.dynamic-text")
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
label: I18n.tr("bar.custom-button.max-text-length-horizontal-label")
|
||||
description: I18n.tr("bar.custom-button.max-text-length-horizontal-description")
|
||||
from: 0
|
||||
to: 100
|
||||
value: valueMaxTextLengthHorizontal
|
||||
onValueChanged: {
|
||||
valueMaxTextLengthHorizontal = value;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.maxTextLength.horizontal
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
label: I18n.tr("bar.custom-button.max-text-length-vertical-label")
|
||||
description: I18n.tr("bar.custom-button.max-text-length-vertical-description")
|
||||
from: 0
|
||||
to: 100
|
||||
value: valueMaxTextLengthVertical
|
||||
onValueChanged: {
|
||||
valueMaxTextLengthVertical = value;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.maxTextLength.vertical
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: textStreamInput
|
||||
label: I18n.tr("bar.custom-button.text-stream-label")
|
||||
description: I18n.tr("bar.custom-button.text-stream-description")
|
||||
checked: valueTextStream
|
||||
onToggled: checked => {
|
||||
valueTextStream = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.textStream
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: parseJsonInput
|
||||
label: I18n.tr("bar.custom-button.parse-json-label")
|
||||
description: I18n.tr("bar.custom-button.parse-json-description")
|
||||
checked: valueParseJson
|
||||
onToggled: checked => {
|
||||
valueParseJson = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.parseJson
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: textCommandInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.display-command-output-label")
|
||||
description: valueTextStream ? I18n.tr("bar.custom-button.display-command-output-stream-description") : I18n.tr("bar.custom-button.display-command-output-description")
|
||||
placeholderText: I18n.tr("placeholders.command-example")
|
||||
text: widgetData?.textCommand || widgetMetadata.textCommand
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: widgetMetadata.textCommand
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: textCollapseInput
|
||||
Layout.fillWidth: true
|
||||
visible: valueTextStream
|
||||
label: I18n.tr("bar.custom-button.collapse-condition-label")
|
||||
description: I18n.tr("bar.custom-button.collapse-condition-description")
|
||||
placeholderText: I18n.tr("placeholders.enter-text-to-collapse")
|
||||
text: widgetData?.textCollapse || widgetMetadata.textCollapse
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: widgetMetadata.textCollapse
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: textIntervalInput
|
||||
Layout.fillWidth: true
|
||||
visible: !valueTextStream
|
||||
label: I18n.tr("bar.custom-button.refresh-interval-label")
|
||||
description: I18n.tr("bar.custom-button.refresh-interval-description")
|
||||
placeholderText: String(widgetMetadata.textIntervalMs)
|
||||
text: widgetData && widgetData.textIntervalMs !== undefined ? String(widgetData.textIntervalMs) : ""
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: String(widgetMetadata.textIntervalMs)
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
id: hideModeComboBox
|
||||
label: I18n.tr("bar.custom-button.hide-mode-label")
|
||||
description: I18n.tr("bar.custom-button.hide-mode-description")
|
||||
model: [
|
||||
{
|
||||
name: I18n.tr("bar.custom-button.hide-mode-always-expanded"),
|
||||
key: "alwaysExpanded"
|
||||
},
|
||||
{
|
||||
name: I18n.tr("bar.custom-button.hide-mode-expand-with-output"),
|
||||
key: "expandWithOutput"
|
||||
},
|
||||
{
|
||||
name: I18n.tr("bar.custom-button.hide-mode-max-transparent"),
|
||||
key: "maxTransparent"
|
||||
}
|
||||
]
|
||||
currentKey: valueHideMode
|
||||
onSelected: key => {
|
||||
valueHideMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
visible: textCommandInput.text !== "" && valueTextStream == true
|
||||
defaultValue: widgetMetadata.hideMode
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.iconColor = valueIconColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.iconColor = valueIconColor;
|
||||
settings.textColor = valueTextColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
currentKey: valueTextColor
|
||||
onSelected: key => {
|
||||
valueTextColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.textColor
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
|
||||
property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.displayMode = valueDisplayMode;
|
||||
settings.showIcon = valueShowIcon;
|
||||
settings.iconColor = valueIconColor;
|
||||
settings.textColor = valueTextColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
visible: valueShowIcon // Hide display mode setting when icon is disabled
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("bar.volume.display-mode-description")
|
||||
minimumWidth: 200
|
||||
model: [
|
||||
{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("display-modes.on-hover")
|
||||
},
|
||||
{
|
||||
"key": "forceOpen",
|
||||
"name": I18n.tr("display-modes.force-open")
|
||||
},
|
||||
{
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("display-modes.always-hide")
|
||||
}
|
||||
]
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => {
|
||||
valueDisplayMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.displayMode
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.custom-button.show-icon-label")
|
||||
description: I18n.tr("bar.keyboard-layout.show-icon-description")
|
||||
checked: valueShowIcon
|
||||
onToggled: checked => {
|
||||
valueShowIcon = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showIcon
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
currentKey: valueTextColor
|
||||
onSelected: key => {
|
||||
valueTextColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.textColor
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
|
||||
property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo
|
||||
property string valueCustomIconPath: widgetData.customIconPath !== undefined ? widgetData.customIconPath : widgetMetadata.customIconPath
|
||||
property bool valueEnableColorization: widgetData.enableColorization !== undefined ? widgetData.enableColorization : widgetMetadata.enableColorization
|
||||
property string valueColorizeSystemIcon: widgetData.colorizeSystemIcon !== undefined ? widgetData.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.icon = valueIcon;
|
||||
settings.useDistroLogo = valueUseDistroLogo;
|
||||
settings.customIconPath = valueCustomIconPath;
|
||||
settings.enableColorization = valueEnableColorization;
|
||||
settings.colorizeSystemIcon = valueColorizeSystemIcon;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.control-center.use-distro-logo-label")
|
||||
description: I18n.tr("bar.control-center.use-distro-logo-description")
|
||||
checked: valueUseDistroLogo
|
||||
onToggled: checked => {
|
||||
valueUseDistroLogo = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.useDistroLogo
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.custom-button.enable-colorization-label")
|
||||
description: I18n.tr("bar.control-center.enable-colorization-description")
|
||||
checked: valueEnableColorization
|
||||
onToggled: checked => {
|
||||
valueEnableColorization = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.enableColorization
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
visible: valueEnableColorization
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
description: I18n.tr("bar.control-center.color-selection-description")
|
||||
currentKey: valueColorizeSystemIcon
|
||||
onSelected: function (key) {
|
||||
valueColorizeSystemIcon = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.colorizeSystemIcon
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("common.icon")
|
||||
description: I18n.tr("bar.control-center.icon-description")
|
||||
}
|
||||
|
||||
NImageRounded {
|
||||
Layout.preferredWidth: Style.fontSizeXL * 2
|
||||
Layout.preferredHeight: Style.fontSizeXL * 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: Math.min(Style.radiusL, Layout.preferredWidth / 2)
|
||||
imagePath: valueCustomIconPath
|
||||
visible: valueCustomIconPath !== "" && !valueUseDistroLogo
|
||||
}
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: valueIcon
|
||||
pointSize: Style.fontSizeXXL * 1.5
|
||||
visible: valueIcon !== "" && valueCustomIconPath === "" && !valueUseDistroLogo
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
NButton {
|
||||
enabled: !valueUseDistroLogo
|
||||
text: I18n.tr("bar.control-center.browse-library")
|
||||
onClicked: iconPicker.open()
|
||||
}
|
||||
|
||||
NButton {
|
||||
enabled: !valueUseDistroLogo
|
||||
text: I18n.tr("bar.control-center.browse-file")
|
||||
onClicked: imagePicker.openFilePicker()
|
||||
}
|
||||
}
|
||||
|
||||
NIconPicker {
|
||||
id: iconPicker
|
||||
initialIcon: valueIcon
|
||||
onIconSelected: iconName => {
|
||||
valueIcon = iconName;
|
||||
valueCustomIconPath = "";
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: imagePicker
|
||||
title: I18n.tr("bar.control-center.select-custom-icon")
|
||||
selectionMode: "files"
|
||||
nameFilters: ImageCacheService.basicImageFilters.concat(["*.svg"])
|
||||
initialPath: Quickshell.env("HOME")
|
||||
onAccepted: paths => {
|
||||
if (paths.length > 0) {
|
||||
valueCustomIconPath = paths[0];
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property bool valueShowCapsLock: widgetData.showCapsLock !== undefined ? widgetData.showCapsLock : widgetMetadata.showCapsLock
|
||||
property bool valueShowNumLock: widgetData.showNumLock !== undefined ? widgetData.showNumLock : widgetMetadata.showNumLock
|
||||
property bool valueShowScrollLock: widgetData.showScrollLock !== undefined ? widgetData.showScrollLock : widgetMetadata.showScrollLock
|
||||
|
||||
property string capsIcon: widgetData.capsLockIcon !== undefined ? widgetData.capsLockIcon : widgetMetadata.capsLockIcon
|
||||
property string numIcon: widgetData.numLockIcon !== undefined ? widgetData.numLockIcon : widgetMetadata.numLockIcon
|
||||
property string scrollIcon: widgetData.scrollLockIcon !== undefined ? widgetData.scrollLockIcon : widgetMetadata.scrollLockIcon
|
||||
|
||||
property bool valueHideWhenOff: widgetData.hideWhenOff !== undefined ? widgetData.hideWhenOff : (widgetMetadata.hideWhenOff !== undefined ? widgetMetadata.hideWhenOff : false)
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.showCapsLock = valueShowCapsLock;
|
||||
settings.showNumLock = valueShowNumLock;
|
||||
settings.showScrollLock = valueShowScrollLock;
|
||||
settings.capsLockIcon = capsIcon;
|
||||
settings.numLockIcon = numIcon;
|
||||
settings.scrollLockIcon = scrollIcon;
|
||||
settings.hideWhenOff = valueHideWhenOff;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.lock-keys.show-caps-lock-label")
|
||||
description: I18n.tr("bar.lock-keys.show-caps-lock-description")
|
||||
checked: valueShowCapsLock
|
||||
onToggled: checked => {
|
||||
valueShowCapsLock = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showCapsLock
|
||||
}
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: capsIcon
|
||||
pointSize: Style.fontSizeXL
|
||||
visible: capsIcon !== ""
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.browse")
|
||||
onClicked: capsPicker.open()
|
||||
enabled: valueShowCapsLock
|
||||
}
|
||||
}
|
||||
|
||||
NIconPicker {
|
||||
id: capsPicker
|
||||
initialIcon: capsIcon
|
||||
query: "letter-c"
|
||||
onIconSelected: function (iconName) {
|
||||
capsIcon = iconName;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.lock-keys.show-num-lock-label")
|
||||
description: I18n.tr("bar.lock-keys.show-num-lock-description")
|
||||
checked: valueShowNumLock
|
||||
onToggled: checked => {
|
||||
valueShowNumLock = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showNumLock
|
||||
}
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: numIcon
|
||||
pointSize: Style.fontSizeXL
|
||||
visible: numIcon !== ""
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.browse")
|
||||
onClicked: numPicker.open()
|
||||
enabled: valueShowNumLock
|
||||
}
|
||||
}
|
||||
|
||||
NIconPicker {
|
||||
id: numPicker
|
||||
initialIcon: numIcon
|
||||
query: "letter-n"
|
||||
onIconSelected: function (iconName) {
|
||||
numIcon = iconName;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.lock-keys.show-scroll-lock-label")
|
||||
description: I18n.tr("bar.lock-keys.show-scroll-lock-description")
|
||||
checked: valueShowScrollLock
|
||||
onToggled: checked => {
|
||||
valueShowScrollLock = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showScrollLock
|
||||
}
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: scrollIcon
|
||||
pointSize: Style.fontSizeXL
|
||||
visible: scrollIcon !== ""
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.browse")
|
||||
onClicked: scrollPicker.open()
|
||||
enabled: valueShowScrollLock
|
||||
}
|
||||
}
|
||||
|
||||
NIconPicker {
|
||||
id: scrollPicker
|
||||
initialIcon: scrollIcon
|
||||
query: "letter-s"
|
||||
onIconSelected: function (iconName) {
|
||||
scrollIcon = iconName;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.lock-keys.hide-when-off-label")
|
||||
description: I18n.tr("bar.lock-keys.hide-when-off-description")
|
||||
checked: valueHideWhenOff
|
||||
onToggled: checked => {
|
||||
valueHideWhenOff = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.hideWhenOff
|
||||
}
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property string valueHideMode: widgetData.hideMode !== undefined ? widgetData.hideMode : widgetMetadata.hideMode
|
||||
// Deprecated: hideWhenIdle now folded into hideMode = "idle"
|
||||
property bool valueHideWhenIdle: widgetData.hideWhenIdle !== undefined ? widgetData.hideWhenIdle : widgetMetadata.hideWhenIdle
|
||||
property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt
|
||||
property bool valuePanelShowAlbumArt: widgetData.panelShowAlbumArt !== undefined ? widgetData.panelShowAlbumArt : widgetMetadata.panelShowAlbumArt
|
||||
property bool valueShowArtistFirst: widgetData.showArtistFirst !== undefined ? widgetData.showArtistFirst : widgetMetadata.showArtistFirst
|
||||
property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer
|
||||
property string valueVisualizerType: widgetData.visualizerType !== undefined ? widgetData.visualizerType : widgetMetadata.visualizerType
|
||||
property string valueScrollingMode: widgetData.scrollingMode !== undefined ? widgetData.scrollingMode : widgetMetadata.scrollingMode
|
||||
property int valueMaxWidth: widgetData.maxWidth !== undefined ? widgetData.maxWidth : widgetMetadata.maxWidth
|
||||
property bool valueUseFixedWidth: widgetData.useFixedWidth !== undefined ? widgetData.useFixedWidth : widgetMetadata.useFixedWidth
|
||||
property bool valueShowProgressRing: widgetData.showProgressRing !== undefined ? widgetData.showProgressRing : widgetMetadata.showProgressRing
|
||||
property bool valueCompactMode: widgetData.compactMode !== undefined ? widgetData.compactMode : widgetMetadata.compactMode
|
||||
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
|
||||
|
||||
Component.onCompleted: {
|
||||
if (widgetData && widgetData.hideMode !== undefined) {
|
||||
valueHideMode = widgetData.hideMode;
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.hideMode = valueHideMode;
|
||||
// No longer store hideWhenIdle separately; kept for backward compatibility only
|
||||
settings.showAlbumArt = valueShowAlbumArt;
|
||||
settings.panelShowAlbumArt = valuePanelShowAlbumArt;
|
||||
settings.showArtistFirst = valueShowArtistFirst;
|
||||
settings.showVisualizer = valueShowVisualizer;
|
||||
settings.visualizerType = valueVisualizerType;
|
||||
settings.scrollingMode = valueScrollingMode;
|
||||
settings.maxWidth = parseInt(widthInput.text) || widgetMetadata.maxWidth;
|
||||
settings.useFixedWidth = valueUseFixedWidth;
|
||||
settings.showProgressRing = valueShowProgressRing;
|
||||
settings.compactMode = valueCompactMode;
|
||||
settings.textColor = valueTextColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.hide-mode-label")
|
||||
description: I18n.tr("bar.media-mini.hide-mode-description")
|
||||
model: [
|
||||
{
|
||||
"key": "visible",
|
||||
"name": I18n.tr("hide-modes.visible")
|
||||
},
|
||||
{
|
||||
"key": "hidden",
|
||||
"name": I18n.tr("hide-modes.hidden")
|
||||
},
|
||||
{
|
||||
"key": "transparent",
|
||||
"name": I18n.tr("hide-modes.transparent")
|
||||
},
|
||||
{
|
||||
"key": "idle",
|
||||
"name": I18n.tr("hide-modes.idle")
|
||||
}
|
||||
]
|
||||
currentKey: root.valueHideMode
|
||||
onSelected: key => {
|
||||
root.valueHideMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.hideMode
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.media-mini.show-album-art-label")
|
||||
description: I18n.tr("bar.media-mini.show-album-art-description")
|
||||
checked: valueShowAlbumArt
|
||||
onToggled: checked => {
|
||||
valueShowAlbumArt = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showAlbumArt
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.media-mini.show-artist-first-label")
|
||||
description: I18n.tr("bar.media-mini.show-artist-first-description")
|
||||
checked: valueShowArtistFirst
|
||||
onToggled: checked => {
|
||||
valueShowArtistFirst = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showArtistFirst
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.media-mini.show-visualizer-label")
|
||||
description: I18n.tr("bar.media-mini.show-visualizer-description")
|
||||
checked: valueShowVisualizer
|
||||
onToggled: checked => {
|
||||
valueShowVisualizer = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showVisualizer
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
visible: valueShowVisualizer
|
||||
label: I18n.tr("bar.media-mini.visualizer-type-label")
|
||||
description: I18n.tr("bar.media-mini.visualizer-type-description")
|
||||
model: [
|
||||
{
|
||||
"key": "linear",
|
||||
"name": I18n.tr("options.visualizer-types.linear")
|
||||
},
|
||||
{
|
||||
"key": "mirrored",
|
||||
"name": I18n.tr("options.visualizer-types.mirrored")
|
||||
},
|
||||
{
|
||||
"key": "wave",
|
||||
"name": I18n.tr("options.visualizer-types.wave")
|
||||
}
|
||||
]
|
||||
currentKey: valueVisualizerType
|
||||
onSelected: key => {
|
||||
valueVisualizerType = key;
|
||||
saveSettings();
|
||||
}
|
||||
minimumWidth: 200
|
||||
defaultValue: widgetMetadata.visualizerType
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: widthInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.max-width-label")
|
||||
description: I18n.tr("bar.media-mini.max-width-description")
|
||||
placeholderText: widgetMetadata.maxWidth
|
||||
text: valueMaxWidth
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: String(widgetMetadata.maxWidth)
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.media-mini.use-fixed-width-label")
|
||||
description: I18n.tr("bar.media-mini.use-fixed-width-description")
|
||||
checked: valueUseFixedWidth
|
||||
onToggled: checked => {
|
||||
valueUseFixedWidth = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.useFixedWidth
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.media-mini.show-progress-ring-label")
|
||||
description: I18n.tr("bar.media-mini.show-progress-ring-description")
|
||||
checked: valueShowProgressRing
|
||||
onToggled: checked => {
|
||||
valueShowProgressRing = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showProgressRing
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
currentKey: valueTextColor
|
||||
onSelected: key => {
|
||||
valueTextColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.textColor
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("bar.media-mini.scrolling-mode-label")
|
||||
description: I18n.tr("bar.media-mini.scrolling-mode-description")
|
||||
model: [
|
||||
{
|
||||
"key": "always",
|
||||
"name": I18n.tr("options.scrolling-modes.always")
|
||||
},
|
||||
{
|
||||
"key": "hover",
|
||||
"name": I18n.tr("options.scrolling-modes.hover")
|
||||
},
|
||||
{
|
||||
"key": "never",
|
||||
"name": I18n.tr("options.scrolling-modes.never")
|
||||
}
|
||||
]
|
||||
currentKey: valueScrollingMode
|
||||
onSelected: key => {
|
||||
valueScrollingMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
minimumWidth: 200
|
||||
defaultValue: widgetMetadata.scrollingMode
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginS
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("bar.media-mini.panel-section-label")
|
||||
description: I18n.tr("bar.media-mini.panel-section-description")
|
||||
labelColor: Color.mPrimary
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.media-mini.show-album-art-label")
|
||||
description: I18n.tr("bar.media-mini.show-album-art-description")
|
||||
checked: valuePanelShowAlbumArt
|
||||
onToggled: checked => {
|
||||
valuePanelShowAlbumArt = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.panelShowAlbumArt
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.media-mini.compact-mode-label")
|
||||
description: I18n.tr("bar.media-mini.compact-mode-description")
|
||||
checked: valueCompactMode
|
||||
onToggled: checked => {
|
||||
valueCompactMode = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.compactMode
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
|
||||
property string valueMiddleClickCommand: widgetData.middleClickCommand !== undefined ? widgetData.middleClickCommand : widgetMetadata.middleClickCommand
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.displayMode = valueDisplayMode;
|
||||
settings.middleClickCommand = valueMiddleClickCommand;
|
||||
settings.iconColor = valueIconColor;
|
||||
settings.textColor = valueTextColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("bar.volume.display-mode-description")
|
||||
minimumWidth: 200
|
||||
model: [
|
||||
{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("display-modes.on-hover")
|
||||
},
|
||||
{
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("display-modes.always-show")
|
||||
},
|
||||
{
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("display-modes.always-hide")
|
||||
}
|
||||
]
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => {
|
||||
valueDisplayMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.displayMode
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
currentKey: valueTextColor
|
||||
onSelected: key => {
|
||||
valueTextColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.textColor
|
||||
}
|
||||
|
||||
// Middle click command
|
||||
NTextInput {
|
||||
label: I18n.tr("bar.custom-button.middle-click-label")
|
||||
description: I18n.tr("panels.audio.on-middle-clicked-description")
|
||||
placeholderText: I18n.tr("panels.audio.external-mixer-placeholder")
|
||||
text: valueMiddleClickCommand
|
||||
onTextChanged: {
|
||||
valueMiddleClickCommand = text;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.middleClickCommand
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.displayMode = valueDisplayMode;
|
||||
settings.iconColor = valueIconColor;
|
||||
settings.textColor = valueTextColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("bar.volume.display-mode-description")
|
||||
minimumWidth: 200
|
||||
model: [
|
||||
{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("display-modes.on-hover")
|
||||
},
|
||||
{
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("display-modes.always-show")
|
||||
},
|
||||
{
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("display-modes.always-hide")
|
||||
}
|
||||
]
|
||||
currentKey: root.valueDisplayMode
|
||||
onSelected: key => {
|
||||
root.valueDisplayMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.displayMode
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
currentKey: valueTextColor
|
||||
onSelected: key => {
|
||||
valueTextColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.textColor
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.iconColor = valueIconColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.iconColor = valueIconColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property bool valueShowUnreadBadge: widgetData.showUnreadBadge !== undefined ? widgetData.showUnreadBadge : widgetMetadata.showUnreadBadge
|
||||
property bool valueHideWhenZero: widgetData.hideWhenZero !== undefined ? widgetData.hideWhenZero : widgetMetadata.hideWhenZero
|
||||
property bool valueHideWhenZeroUnread: widgetData.hideWhenZeroUnread !== undefined ? widgetData.hideWhenZeroUnread : widgetMetadata.hideWhenZeroUnread
|
||||
property string valueUnreadBadgeColor: widgetData.unreadBadgeColor !== undefined ? widgetData.unreadBadgeColor : widgetMetadata.unreadBadgeColor
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.showUnreadBadge = valueShowUnreadBadge;
|
||||
settings.hideWhenZero = valueHideWhenZero;
|
||||
settings.hideWhenZeroUnread = valueHideWhenZeroUnread;
|
||||
settings.unreadBadgeColor = valueUnreadBadgeColor;
|
||||
settings.iconColor = valueIconColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.notification-history.show-unread-badge-label")
|
||||
description: I18n.tr("bar.notification-history.show-unread-badge-description")
|
||||
checked: valueShowUnreadBadge
|
||||
onToggled: checked => {
|
||||
valueShowUnreadBadge = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showUnreadBadge
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("bar.notification-history.unread-badge-color-label")
|
||||
description: I18n.tr("bar.notification-history.unread-badge-color-description")
|
||||
currentKey: valueUnreadBadgeColor
|
||||
onSelected: key => {
|
||||
valueUnreadBadgeColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
visible: valueShowUnreadBadge
|
||||
defaultValue: widgetMetadata.unreadBadgeColor
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.notification-history.hide-widget-when-zero-label")
|
||||
description: I18n.tr("bar.notification-history.hide-widget-when-zero-description")
|
||||
checked: valueHideWhenZero
|
||||
onToggled: checked => {
|
||||
valueHideWhenZero = checked;
|
||||
saveSettings();
|
||||
}
|
||||
enabled: !valueHideWhenZeroUnread
|
||||
defaultValue: widgetMetadata.hideWhenZero
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.notification-history.hide-widget-when-zero-unread-label")
|
||||
description: I18n.tr("bar.notification-history.hide-widget-when-zero-unread-description")
|
||||
checked: valueHideWhenZeroUnread
|
||||
onToggled: checked => {
|
||||
valueHideWhenZeroUnread = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.hideWhenZeroUnread
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.iconColor = valueIconColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
width: 700
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.iconColor = valueIconColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: root.valueIconColor
|
||||
onSelected: key => {
|
||||
root.valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.iconColor = valueIconColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
valueDefault: widgetMetadata.iconColor
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.width = parseInt(widthInput.text) || widgetMetadata.width;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: widthInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("common.width")
|
||||
description: I18n.tr("bar.spacer.width-description")
|
||||
text: widgetData.width || widgetMetadata.width
|
||||
placeholderText: I18n.tr("placeholders.enter-width-pixels")
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: String(widgetMetadata.width)
|
||||
}
|
||||
}
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
|
||||
// Local, editable state for checkboxes
|
||||
property bool valueCompactMode: widgetData.compactMode !== undefined ? widgetData.compactMode : widgetMetadata.compactMode
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
|
||||
property bool valueUseMonospaceFont: widgetData.useMonospaceFont !== undefined ? widgetData.useMonospaceFont : widgetMetadata.useMonospaceFont
|
||||
property bool valueUsePadding: widgetData.usePadding !== undefined ? widgetData.usePadding : widgetMetadata.usePadding
|
||||
property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage
|
||||
property bool valueShowCpuCores: widgetData.showCpuCores !== undefined ? widgetData.showCpuCores : widgetMetadata.showCpuCores
|
||||
property bool valueShowCpuFreq: widgetData.showCpuFreq !== undefined ? widgetData.showCpuFreq : widgetMetadata.showCpuFreq
|
||||
property bool valueShowCpuTemp: widgetData.showCpuTemp !== undefined ? widgetData.showCpuTemp : widgetMetadata.showCpuTemp
|
||||
property bool valueShowGpuTemp: widgetData.showGpuTemp !== undefined ? widgetData.showGpuTemp : widgetMetadata.showGpuTemp
|
||||
property bool valueShowLoadAverage: widgetData.showLoadAverage !== undefined ? widgetData.showLoadAverage : widgetMetadata.showLoadAverage
|
||||
property bool valueShowMemoryUsage: widgetData.showMemoryUsage !== undefined ? widgetData.showMemoryUsage : widgetMetadata.showMemoryUsage
|
||||
property bool valueShowMemoryAsPercent: widgetData.showMemoryAsPercent !== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
|
||||
property bool valueShowSwapUsage: widgetData.showSwapUsage !== undefined ? widgetData.showSwapUsage : widgetMetadata.showSwapUsage
|
||||
property bool valueShowNetworkStats: widgetData.showNetworkStats !== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats
|
||||
property bool valueShowDiskUsage: widgetData.showDiskUsage !== undefined ? widgetData.showDiskUsage : widgetMetadata.showDiskUsage
|
||||
property bool valueShowDiskUsageAsPercent: widgetData.showDiskUsageAsPercent !== undefined ? widgetData.showDiskUsageAsPercent : widgetMetadata.showDiskUsageAsPercent
|
||||
property bool valueShowDiskAvailable: widgetData.showDiskAvailable !== undefined ? widgetData.showDiskAvailable : widgetMetadata.showDiskAvailable
|
||||
property string valueDiskPath: widgetData.diskPath !== undefined ? widgetData.diskPath : widgetMetadata.diskPath
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.compactMode = valueCompactMode;
|
||||
settings.iconColor = valueIconColor;
|
||||
settings.textColor = valueTextColor;
|
||||
settings.useMonospaceFont = valueUseMonospaceFont;
|
||||
settings.usePadding = valueUsePadding;
|
||||
settings.showCpuUsage = valueShowCpuUsage;
|
||||
settings.showCpuCores = valueShowCpuCores;
|
||||
settings.showCpuFreq = valueShowCpuFreq;
|
||||
settings.showCpuTemp = valueShowCpuTemp;
|
||||
settings.showGpuTemp = valueShowGpuTemp;
|
||||
settings.showLoadAverage = valueShowLoadAverage;
|
||||
settings.showMemoryUsage = valueShowMemoryUsage;
|
||||
settings.showMemoryAsPercent = valueShowMemoryAsPercent;
|
||||
settings.showSwapUsage = valueShowSwapUsage;
|
||||
settings.showNetworkStats = valueShowNetworkStats;
|
||||
settings.showDiskUsage = valueShowDiskUsage;
|
||||
settings.showDiskUsageAsPercent = valueShowDiskUsageAsPercent;
|
||||
settings.showDiskAvailable = valueShowDiskAvailable;
|
||||
settings.diskPath = valueDiskPath;
|
||||
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.compact-mode-label")
|
||||
description: I18n.tr("bar.system-monitor.compact-mode-description")
|
||||
checked: valueCompactMode
|
||||
onToggled: checked => {
|
||||
valueCompactMode = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.compactMode
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
currentKey: valueTextColor
|
||||
onSelected: key => {
|
||||
valueTextColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
visible: !valueCompactMode
|
||||
defaultValue: widgetMetadata.textColor
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.use-monospace-font-label")
|
||||
description: I18n.tr("bar.system-monitor.use-monospace-font-description")
|
||||
checked: valueUseMonospaceFont
|
||||
onToggled: checked => {
|
||||
valueUseMonospaceFont = checked;
|
||||
saveSettings();
|
||||
}
|
||||
visible: !valueCompactMode
|
||||
defaultValue: widgetMetadata.useMonospaceFont
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.use-padding-label")
|
||||
description: isVerticalBar ? I18n.tr("bar.system-monitor.use-padding-description-disabled-vertical") : !valueUseMonospaceFont ? I18n.tr("bar.system-monitor.use-padding-description-disabled-monospace-font") : I18n.tr("bar.system-monitor.use-padding-description")
|
||||
checked: valueUsePadding && !isVerticalBar && valueUseMonospaceFont
|
||||
onToggled: checked => {
|
||||
valueUsePadding = checked;
|
||||
saveSettings();
|
||||
}
|
||||
visible: !valueCompactMode
|
||||
enabled: !isVerticalBar && valueUseMonospaceFont
|
||||
defaultValue: widgetMetadata.usePadding
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showCpuUsage
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.cpu-usage-label")
|
||||
description: I18n.tr("bar.system-monitor.cpu-usage-description")
|
||||
checked: valueShowCpuUsage
|
||||
onToggled: checked => {
|
||||
valueShowCpuUsage = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showCpuUsage
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showCpuCores
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.cpu-cores-label")
|
||||
description: I18n.tr("bar.system-monitor.cpu-cores-description")
|
||||
checked: valueShowCpuCores
|
||||
onToggled: checked => {
|
||||
valueShowCpuCores = checked;
|
||||
saveSettings();
|
||||
}
|
||||
visible: valueCompactMode
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showCpuFreq
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.cpu-frequency-label")
|
||||
description: I18n.tr("bar.system-monitor.cpu-frequency-description")
|
||||
checked: valueShowCpuFreq
|
||||
onToggled: checked => {
|
||||
valueShowCpuFreq = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showCpuFreq
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showCpuTemp
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.cpu-temperature-label")
|
||||
description: I18n.tr("bar.system-monitor.cpu-temperature-description")
|
||||
checked: valueShowCpuTemp
|
||||
onToggled: checked => {
|
||||
valueShowCpuTemp = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showCpuTemp
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showLoadAverage
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.load-average-label")
|
||||
description: I18n.tr("bar.system-monitor.load-average-description")
|
||||
checked: valueShowLoadAverage
|
||||
onToggled: checked => {
|
||||
valueShowLoadAverage = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showLoadAverage
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showGpuTemp
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.system-monitor.gpu-section-label")
|
||||
description: I18n.tr("bar.system-monitor.gpu-temperature-description")
|
||||
checked: valueShowGpuTemp
|
||||
onToggled: checked => {
|
||||
valueShowGpuTemp = checked;
|
||||
saveSettings();
|
||||
}
|
||||
visible: SystemStatService.gpuAvailable
|
||||
defaultValue: widgetMetadata.showGpuTemp
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showMemoryUsage
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.memory-usage-label")
|
||||
description: I18n.tr("bar.system-monitor.memory-usage-description")
|
||||
checked: valueShowMemoryUsage
|
||||
onToggled: checked => {
|
||||
valueShowMemoryUsage = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showMemoryUsage
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showMemoryAsPercent
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.memory-percentage-label")
|
||||
description: I18n.tr("bar.system-monitor.memory-percentage-description")
|
||||
checked: valueShowMemoryAsPercent
|
||||
onToggled: checked => {
|
||||
valueShowMemoryAsPercent = checked;
|
||||
saveSettings();
|
||||
}
|
||||
visible: valueShowMemoryUsage
|
||||
defaultValue: widgetMetadata.showMemoryAsPercent
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showSwapUsage
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.swap-usage-label")
|
||||
description: I18n.tr("bar.system-monitor.swap-usage-description")
|
||||
checked: valueShowSwapUsage
|
||||
onToggled: checked => {
|
||||
valueShowSwapUsage = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showSwapUsage
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showNetworkStats
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.network-traffic-label")
|
||||
description: I18n.tr("bar.system-monitor.network-traffic-description")
|
||||
checked: valueShowNetworkStats
|
||||
onToggled: checked => {
|
||||
valueShowNetworkStats = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showNetworkStats
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showDiskUsage
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.storage-usage-label")
|
||||
description: I18n.tr("bar.system-monitor.storage-usage-description")
|
||||
checked: valueShowDiskUsage
|
||||
onToggled: checked => {
|
||||
valueShowDiskUsage = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showDiskUsage
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showDiskUsageAsPercent
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.storage-as-percentage-label")
|
||||
description: I18n.tr("bar.system-monitor.storage-as-percentage-description")
|
||||
checked: valueShowDiskUsageAsPercent
|
||||
onToggled: checked => {
|
||||
valueShowDiskUsageAsPercent = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showDiskUsageAsPercent
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showDiskAvailable
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.storage-available-label")
|
||||
description: I18n.tr("bar.system-monitor.storage-available-description")
|
||||
checked: valueShowDiskAvailable
|
||||
onToggled: checked => {
|
||||
valueShowDiskAvailable = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showDiskAvailable
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
id: diskPathComboBox
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.system-monitor.disk-path-label")
|
||||
description: I18n.tr("bar.system-monitor.disk-path-description")
|
||||
model: {
|
||||
const paths = Object.keys(SystemStatService.diskPercents).sort();
|
||||
return paths.map(path => ({
|
||||
key: path,
|
||||
name: path
|
||||
}));
|
||||
}
|
||||
currentKey: valueDiskPath
|
||||
onSelected: key => {
|
||||
valueDiskPath = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.diskPath
|
||||
}
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
readonly property bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
|
||||
// Local state
|
||||
property string valueHideMode: widgetData.hideMode !== undefined ? widgetData.hideMode : widgetMetadata.hideMode
|
||||
property bool valueOnlyActiveWorkspaces: widgetData.onlyActiveWorkspaces !== undefined ? widgetData.onlyActiveWorkspaces : widgetMetadata.onlyActiveWorkspaces
|
||||
property bool valueOnlySameOutput: widgetData.onlySameOutput !== undefined ? widgetData.onlySameOutput : widgetMetadata.onlySameOutput
|
||||
property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons
|
||||
property bool valueShowTitle: isVerticalBar ? false : widgetData.showTitle !== undefined ? widgetData.showTitle : widgetMetadata.showTitle
|
||||
property bool valueSmartWidth: widgetData.smartWidth !== undefined ? widgetData.smartWidth : widgetMetadata.smartWidth
|
||||
property int valueMaxTaskbarWidth: widgetData.maxTaskbarWidth !== undefined ? widgetData.maxTaskbarWidth : widgetMetadata.maxTaskbarWidth
|
||||
property int valueTitleWidth: widgetData.titleWidth !== undefined ? widgetData.titleWidth : widgetMetadata.titleWidth
|
||||
property bool valueShowPinnedApps: widgetData.showPinnedApps !== undefined ? widgetData.showPinnedApps : widgetMetadata.showPinnedApps
|
||||
property real valueIconScale: widgetData.iconScale !== undefined ? widgetData.iconScale : widgetMetadata.iconScale
|
||||
|
||||
Component.onCompleted: {
|
||||
if (widgetData && widgetData.hideMode !== undefined) {
|
||||
valueHideMode = widgetData.hideMode;
|
||||
} else if (widgetMetadata && widgetMetadata.hideMode !== undefined) {
|
||||
valueHideMode = widgetMetadata.hideMode;
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.hideMode = valueHideMode;
|
||||
settings.onlySameOutput = valueOnlySameOutput;
|
||||
settings.onlyActiveWorkspaces = valueOnlyActiveWorkspaces;
|
||||
settings.colorizeIcons = valueColorizeIcons;
|
||||
settings.showTitle = valueShowTitle;
|
||||
settings.smartWidth = valueSmartWidth;
|
||||
settings.maxTaskbarWidth = valueMaxTaskbarWidth;
|
||||
settings.titleWidth = parseInt(titleWidthInput.text) || widgetMetadata.titleWidth;
|
||||
settings.showPinnedApps = valueShowPinnedApps;
|
||||
settings.iconScale = valueIconScale;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.hide-mode-label")
|
||||
description: I18n.tr("bar.taskbar.hide-mode-description")
|
||||
model: [
|
||||
{
|
||||
"key": "visible",
|
||||
"name": I18n.tr("hide-modes.visible")
|
||||
},
|
||||
{
|
||||
"key": "hidden",
|
||||
"name": I18n.tr("hide-modes.hidden")
|
||||
},
|
||||
{
|
||||
"key": "transparent",
|
||||
"name": I18n.tr("hide-modes.transparent")
|
||||
}
|
||||
]
|
||||
currentKey: root.valueHideMode
|
||||
onSelected: key => {
|
||||
root.valueHideMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.hideMode
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.only-same-monitor-label")
|
||||
description: I18n.tr("bar.taskbar.only-same-monitor-description")
|
||||
checked: root.valueOnlySameOutput
|
||||
onToggled: checked => {
|
||||
root.valueOnlySameOutput = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.onlySameOutput
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.only-active-workspaces-label")
|
||||
description: I18n.tr("bar.taskbar.only-active-workspaces-description")
|
||||
checked: root.valueOnlyActiveWorkspaces
|
||||
onToggled: checked => {
|
||||
root.valueOnlyActiveWorkspaces = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.onlyActiveWorkspaces
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.tray.colorize-icons-label")
|
||||
description: I18n.tr("bar.taskbar.colorize-icons-description")
|
||||
checked: root.valueColorizeIcons
|
||||
onToggled: checked => {
|
||||
root.valueColorizeIcons = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.colorizeIcons
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.show-pinned-apps-label")
|
||||
description: I18n.tr("bar.taskbar.show-pinned-apps-description")
|
||||
checked: root.valueShowPinnedApps
|
||||
onToggled: checked => {
|
||||
root.valueShowPinnedApps = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showPinnedApps
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.icon-scale-label")
|
||||
description: I18n.tr("bar.taskbar.icon-scale-description")
|
||||
from: 0.5
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: root.valueIconScale
|
||||
defaultValue: widgetMetadata.iconScale
|
||||
onMoved: value => {
|
||||
root.valueIconScale = value;
|
||||
saveSettings();
|
||||
}
|
||||
text: Math.round(root.valueIconScale * 100) + "%"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.show-title-label")
|
||||
description: isVerticalBar ? I18n.tr("bar.taskbar.show-title-description-disabled") : I18n.tr("bar.taskbar.show-title-description")
|
||||
checked: root.valueShowTitle
|
||||
onToggled: checked => {
|
||||
root.valueShowTitle = checked;
|
||||
saveSettings();
|
||||
}
|
||||
enabled: !isVerticalBar
|
||||
defaultValue: widgetMetadata.showTitle
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: titleWidthInput
|
||||
visible: root.valueShowTitle && !isVerticalBar
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.title-width-label")
|
||||
description: I18n.tr("bar.taskbar.title-width-description")
|
||||
text: widgetData.titleWidth || widgetMetadata.titleWidth
|
||||
placeholderText: I18n.tr("placeholders.enter-width-pixels")
|
||||
onTextChanged: saveSettings()
|
||||
defaultValue: String(widgetMetadata.titleWidth)
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
visible: !isVerticalBar && root.valueShowTitle
|
||||
label: I18n.tr("bar.taskbar.smart-width-label")
|
||||
description: I18n.tr("bar.taskbar.smart-width-description")
|
||||
checked: root.valueSmartWidth
|
||||
onToggled: checked => {
|
||||
root.valueSmartWidth = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.smartWidth
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
visible: root.valueSmartWidth && !isVerticalBar
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.max-width-label")
|
||||
description: I18n.tr("bar.taskbar.max-width-description")
|
||||
from: 10
|
||||
to: 100
|
||||
stepSize: 5
|
||||
showReset: true
|
||||
value: root.valueMaxTaskbarWidth
|
||||
defaultValue: widgetMetadata.maxTaskbarWidth
|
||||
onMoved: value => {
|
||||
root.valueMaxTaskbarWidth = Math.round(value);
|
||||
saveSettings();
|
||||
}
|
||||
text: Math.round(root.valueMaxTaskbarWidth) + "%"
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property var localBlacklist: widgetData.blacklist || []
|
||||
property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons
|
||||
property string valueChevronColor: widgetData.chevronColor !== undefined ? widgetData.chevronColor : widgetMetadata.chevronColor
|
||||
property bool valueDrawerEnabled: widgetData.drawerEnabled !== undefined ? widgetData.drawerEnabled : widgetMetadata.drawerEnabled
|
||||
property bool valueHidePassive: widgetData.hidePassive !== undefined ? widgetData.hidePassive : widgetMetadata.hidePassive
|
||||
|
||||
ListModel {
|
||||
id: blacklistModel
|
||||
}
|
||||
|
||||
function populateBlacklist() {
|
||||
for (var i = 0; i < localBlacklist.length; i++) {
|
||||
blacklistModel.append({
|
||||
"rule": localBlacklist[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(populateBlacklist);
|
||||
}
|
||||
|
||||
spacing: Style.marginM
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.tray.drawer-enabled-label")
|
||||
description: I18n.tr("bar.tray.drawer-enabled-description")
|
||||
checked: root.valueDrawerEnabled
|
||||
onToggled: checked => {
|
||||
root.valueDrawerEnabled = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.drawerEnabled
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("bar.tray.chevron-color-label")
|
||||
description: I18n.tr("bar.tray.chevron-color-description")
|
||||
currentKey: root.valueChevronColor
|
||||
onSelected: key => {
|
||||
root.valueChevronColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
visible: root.valueDrawerEnabled
|
||||
defaultValue: widgetMetadata.chevronColor
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.tray.colorize-icons-label")
|
||||
description: I18n.tr("bar.tray.colorize-icons-description")
|
||||
checked: root.valueColorizeIcons
|
||||
onToggled: checked => {
|
||||
root.valueColorizeIcons = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.colorizeIcons
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.tray.hide-passive-label")
|
||||
description: I18n.tr("bar.tray.hide-passive-description")
|
||||
checked: root.valueHidePassive
|
||||
onToggled: checked => {
|
||||
root.valueHidePassive = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.hidePassive
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.bar.tray-blacklist-label")
|
||||
description: I18n.tr("panels.bar.tray-blacklist-description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NTextInputButton {
|
||||
id: newRuleInput
|
||||
Layout.fillWidth: true
|
||||
placeholderText: I18n.tr("panels.bar.tray-blacklist-placeholder")
|
||||
buttonIcon: "add"
|
||||
onButtonClicked: {
|
||||
if (newRuleInput.text.length > 0) {
|
||||
var newRule = newRuleInput.text.trim();
|
||||
var exists = false;
|
||||
for (var i = 0; i < blacklistModel.count; i++) {
|
||||
if (blacklistModel.get(i).rule === newRule) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exists) {
|
||||
blacklistModel.append({
|
||||
"rule": newRule
|
||||
});
|
||||
newRuleInput.text = "";
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// List of current blacklist items
|
||||
NListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 150
|
||||
Layout.topMargin: Style.marginL // Increased top margin
|
||||
gradientColor: Color.mSurface
|
||||
|
||||
model: blacklistModel
|
||||
delegate: Item {
|
||||
width: ListView.width
|
||||
height: 40
|
||||
|
||||
Rectangle {
|
||||
id: itemBackground
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginXS
|
||||
color: "transparent" // Make background transparent
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
radius: Style.radiusS
|
||||
visible: model.rule !== undefined && model.rule !== "" // Only visible if rule exists
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginS
|
||||
anchors.rightMargin: Style.marginS
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: model.rule
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
icon: "close"
|
||||
baseSize: 12 * Style.uiScaleRatio
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurfaceVariant
|
||||
colorBgHover: Color.mError
|
||||
colorFgHover: Color.mOnError
|
||||
onClicked: {
|
||||
blacklistModel.remove(index);
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function will be called by the dialog to get the new settings
|
||||
function saveSettings() {
|
||||
var newBlacklist = [];
|
||||
for (var i = 0; i < blacklistModel.count; i++) {
|
||||
newBlacklist.push(blacklistModel.get(i).rule);
|
||||
}
|
||||
|
||||
// Return the updated settings for this widget instance
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.blacklist = newBlacklist;
|
||||
settings.colorizeIcons = root.valueColorizeIcons;
|
||||
settings.chevronColor = root.valueChevronColor;
|
||||
settings.drawerEnabled = root.valueDrawerEnabled;
|
||||
settings.hidePassive = root.valueHidePassive;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.displayMode = valueDisplayMode;
|
||||
settings.iconColor = valueIconColor;
|
||||
settings.textColor = valueTextColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("bar.volume.display-mode-description")
|
||||
minimumWidth: 200
|
||||
model: [
|
||||
{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("display-modes.on-hover")
|
||||
},
|
||||
{
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("display-modes.always-show")
|
||||
},
|
||||
{
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("display-modes.always-hide")
|
||||
}
|
||||
]
|
||||
currentKey: root.valueDisplayMode
|
||||
onSelected: key => {
|
||||
root.valueDisplayMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
currentKey: valueTextColor
|
||||
onSelected: key => {
|
||||
valueTextColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
// Local state
|
||||
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
|
||||
property string valueMiddleClickCommand: widgetData.middleClickCommand !== undefined ? widgetData.middleClickCommand : widgetMetadata.middleClickCommand
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.displayMode = valueDisplayMode;
|
||||
settings.middleClickCommand = valueMiddleClickCommand;
|
||||
settings.iconColor = valueIconColor;
|
||||
settings.textColor = valueTextColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("bar.volume.display-mode-description")
|
||||
minimumWidth: 200
|
||||
model: [
|
||||
{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("display-modes.on-hover")
|
||||
},
|
||||
{
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("display-modes.always-show")
|
||||
},
|
||||
{
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("display-modes.always-hide")
|
||||
}
|
||||
]
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => {
|
||||
valueDisplayMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.displayMode
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.iconColor
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
currentKey: valueTextColor
|
||||
onSelected: key => {
|
||||
valueTextColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.textColor
|
||||
}
|
||||
|
||||
// Middle click command
|
||||
NTextInput {
|
||||
label: I18n.tr("bar.custom-button.middle-click-label")
|
||||
description: I18n.tr("panels.audio.on-middle-clicked-description")
|
||||
placeholderText: I18n.tr("panels.audio.external-mixer-placeholder")
|
||||
text: valueMiddleClickCommand
|
||||
onTextChanged: {
|
||||
valueMiddleClickCommand = text;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.middleClickCommand
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.iconColor = valueIconColor;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-icon-color")
|
||||
currentKey: valueIconColor
|
||||
onSelected: key => {
|
||||
valueIconColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var screen: null
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property string valueLabelMode: widgetData.labelMode !== undefined ? widgetData.labelMode : widgetMetadata.labelMode
|
||||
property bool valueHideUnoccupied: widgetData.hideUnoccupied !== undefined ? widgetData.hideUnoccupied : widgetMetadata.hideUnoccupied
|
||||
property bool valueFollowFocusedScreen: widgetData.followFocusedScreen !== undefined ? widgetData.followFocusedScreen : widgetMetadata.followFocusedScreen
|
||||
property int valueCharacterCount: widgetData.characterCount !== undefined ? widgetData.characterCount : widgetMetadata.characterCount
|
||||
|
||||
// Grouped mode settings
|
||||
property bool valueShowApplications: widgetData.showApplications !== undefined ? widgetData.showApplications : widgetMetadata.showApplications
|
||||
property bool valueShowApplicationsHover: widgetData.showApplicationsHover !== undefined ? widgetData.showApplicationsHover : widgetMetadata.showApplicationsHover
|
||||
property bool valueShowLabelsOnlyWhenOccupied: widgetData.showLabelsOnlyWhenOccupied !== undefined ? widgetData.showLabelsOnlyWhenOccupied : widgetMetadata.showLabelsOnlyWhenOccupied
|
||||
property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons
|
||||
property real valueUnfocusedIconsOpacity: widgetData.unfocusedIconsOpacity !== undefined ? widgetData.unfocusedIconsOpacity : widgetMetadata.unfocusedIconsOpacity
|
||||
property real valueGroupedBorderOpacity: widgetData.groupedBorderOpacity !== undefined ? widgetData.groupedBorderOpacity : widgetMetadata.groupedBorderOpacity
|
||||
property bool valueEnableScrollWheel: widgetData.enableScrollWheel !== undefined ? widgetData.enableScrollWheel : widgetMetadata.enableScrollWheel
|
||||
property real valueIconScale: widgetData.iconScale !== undefined ? widgetData.iconScale : widgetMetadata.iconScale
|
||||
property string valueFocusedColor: widgetData.focusedColor !== undefined ? widgetData.focusedColor : widgetMetadata.focusedColor
|
||||
property string valueOccupiedColor: widgetData.occupiedColor !== undefined ? widgetData.occupiedColor : widgetMetadata.occupiedColor
|
||||
property string valueEmptyColor: widgetData.emptyColor !== undefined ? widgetData.emptyColor : widgetMetadata.emptyColor
|
||||
property bool valueShowBadge: widgetData.showBadge !== undefined ? widgetData.showBadge : widgetMetadata.showBadge
|
||||
property real valuePillSize: widgetData.pillSize !== undefined ? widgetData.pillSize : widgetMetadata.pillSize
|
||||
property string valueFontWeight: widgetData.fontWeight !== undefined ? widgetData.fontWeight : widgetMetadata.fontWeight
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.labelMode = valueLabelMode;
|
||||
settings.hideUnoccupied = valueHideUnoccupied;
|
||||
settings.characterCount = valueCharacterCount;
|
||||
settings.followFocusedScreen = valueFollowFocusedScreen;
|
||||
settings.showApplications = valueShowApplications;
|
||||
settings.showApplicationsHover = valueShowApplicationsHover;
|
||||
settings.showLabelsOnlyWhenOccupied = valueShowLabelsOnlyWhenOccupied;
|
||||
settings.colorizeIcons = valueColorizeIcons;
|
||||
settings.unfocusedIconsOpacity = valueUnfocusedIconsOpacity;
|
||||
settings.groupedBorderOpacity = valueGroupedBorderOpacity;
|
||||
settings.enableScrollWheel = valueEnableScrollWheel;
|
||||
settings.iconScale = valueIconScale;
|
||||
settings.focusedColor = valueFocusedColor;
|
||||
settings.occupiedColor = valueOccupiedColor;
|
||||
settings.emptyColor = valueEmptyColor;
|
||||
settings.showBadge = valueShowBadge;
|
||||
settings.pillSize = valuePillSize;
|
||||
settings.fontWeight = valueFontWeight;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
id: labelModeCombo
|
||||
label: I18n.tr("bar.workspace.label-mode-label")
|
||||
description: I18n.tr("bar.workspace.label-mode-description")
|
||||
model: [
|
||||
{
|
||||
"key": "none",
|
||||
"name": I18n.tr("common.none")
|
||||
},
|
||||
{
|
||||
"key": "index",
|
||||
"name": I18n.tr("options.workspace-labels.index")
|
||||
},
|
||||
{
|
||||
"key": "name",
|
||||
"name": I18n.tr("options.workspace-labels.name")
|
||||
},
|
||||
{
|
||||
"key": "index+name",
|
||||
"name": I18n.tr("options.workspace-labels.index-and-name")
|
||||
}
|
||||
]
|
||||
currentKey: widgetData.labelMode || widgetMetadata.labelMode
|
||||
onSelected: key => {
|
||||
valueLabelMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
minimumWidth: 200
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
label: I18n.tr("bar.workspace.character-count-label")
|
||||
description: I18n.tr("bar.workspace.character-count-description")
|
||||
from: 1
|
||||
to: 10
|
||||
value: valueCharacterCount
|
||||
onValueChanged: {
|
||||
valueCharacterCount = value;
|
||||
saveSettings();
|
||||
}
|
||||
visible: valueLabelMode === "name"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
label: I18n.tr("bar.workspace.pill-size-label")
|
||||
description: I18n.tr("bar.workspace.pill-size-description")
|
||||
from: 0.4
|
||||
to: 1.0
|
||||
stepSize: 0.01
|
||||
value: valuePillSize
|
||||
defaultValue: widgetMetadata.pillSize
|
||||
showReset: true
|
||||
onMoved: value => {
|
||||
valuePillSize = value;
|
||||
saveSettings();
|
||||
}
|
||||
text: Math.round(valuePillSize * 100) + "%"
|
||||
visible: !valueShowApplications
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
id: fontWeightCombo
|
||||
label: I18n.tr("bar.workspace.font-weight-label")
|
||||
description: I18n.tr("bar.workspace.font-weight-description")
|
||||
model: [
|
||||
{
|
||||
"key": "regular",
|
||||
"name": I18n.tr("common.font-weight-regular")
|
||||
},
|
||||
{
|
||||
"key": "medium",
|
||||
"name": I18n.tr("common.font-weight-medium")
|
||||
},
|
||||
{
|
||||
"key": "semibold",
|
||||
"name": I18n.tr("common.font-weight-semibold")
|
||||
},
|
||||
{
|
||||
"key": "bold",
|
||||
"name": I18n.tr("common.font-weight-bold")
|
||||
},
|
||||
]
|
||||
currentKey: widgetData.fontWeight || widgetMetadata.fontWeight
|
||||
onSelected: key => {
|
||||
valueFontWeight = key;
|
||||
saveSettings();
|
||||
}
|
||||
minimumWidth: 200
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.workspace.hide-unoccupied-label")
|
||||
description: I18n.tr("bar.workspace.hide-unoccupied-description")
|
||||
checked: valueHideUnoccupied
|
||||
onToggled: checked => {
|
||||
valueHideUnoccupied = checked;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.workspace.show-labels-only-when-occupied-label")
|
||||
description: I18n.tr("bar.workspace.show-labels-only-when-occupied-description")
|
||||
checked: valueShowLabelsOnlyWhenOccupied
|
||||
onToggled: checked => {
|
||||
valueShowLabelsOnlyWhenOccupied = checked;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.workspace.follow-focused-screen-label")
|
||||
description: I18n.tr("bar.workspace.follow-focused-screen-description")
|
||||
checked: valueFollowFocusedScreen
|
||||
onToggled: checked => {
|
||||
valueFollowFocusedScreen = checked;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.workspace.enable-scrollwheel-label")
|
||||
description: I18n.tr("bar.workspace.enable-scrollwheel-description")
|
||||
checked: valueEnableScrollWheel
|
||||
onToggled: checked => {
|
||||
valueEnableScrollWheel = checked;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.workspace.show-applications-label")
|
||||
description: I18n.tr("bar.workspace.show-applications-description")
|
||||
checked: valueShowApplications
|
||||
onToggled: checked => {
|
||||
valueShowApplications = checked;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.workspace.show-applications-hover-label")
|
||||
description: I18n.tr("bar.workspace.show-applications-hover-description")
|
||||
checked: valueShowApplicationsHover
|
||||
onToggled: checked => {
|
||||
valueShowApplicationsHover = checked;
|
||||
saveSettings();
|
||||
}
|
||||
visible: valueShowApplications
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.workspace.show-badge-label")
|
||||
description: I18n.tr("bar.workspace.show-badge-description")
|
||||
checked: valueShowBadge
|
||||
onToggled: checked => {
|
||||
valueShowBadge = checked;
|
||||
saveSettings();
|
||||
}
|
||||
visible: valueShowApplications
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.tray.colorize-icons-label")
|
||||
description: I18n.tr("bar.active-window.colorize-icons-description")
|
||||
checked: valueColorizeIcons
|
||||
onToggled: checked => {
|
||||
valueColorizeIcons = checked;
|
||||
saveSettings();
|
||||
}
|
||||
visible: valueShowApplications
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
label: I18n.tr("bar.workspace.unfocused-icons-opacity-label")
|
||||
description: I18n.tr("bar.workspace.unfocused-icons-opacity-description")
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: valueUnfocusedIconsOpacity
|
||||
defaultValue: widgetMetadata.unfocusedIconsOpacity
|
||||
onMoved: value => {
|
||||
valueUnfocusedIconsOpacity = value;
|
||||
saveSettings();
|
||||
}
|
||||
text: Math.floor(valueUnfocusedIconsOpacity * 100) + "%"
|
||||
visible: valueShowApplications
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
label: I18n.tr("bar.workspace.grouped-border-opacity-label")
|
||||
description: I18n.tr("bar.workspace.grouped-border-opacity-description")
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: valueGroupedBorderOpacity
|
||||
defaultValue: widgetMetadata.groupedBorderOpacity
|
||||
onMoved: value => {
|
||||
valueGroupedBorderOpacity = value;
|
||||
saveSettings();
|
||||
}
|
||||
text: Math.floor(valueGroupedBorderOpacity * 100) + "%"
|
||||
visible: valueShowApplications
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
label: I18n.tr("bar.taskbar.icon-scale-label")
|
||||
description: I18n.tr("bar.taskbar.icon-scale-description")
|
||||
from: 0.5
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: valueIconScale
|
||||
defaultValue: widgetMetadata.iconScale
|
||||
onMoved: value => {
|
||||
valueIconScale = value;
|
||||
saveSettings();
|
||||
}
|
||||
text: Math.round(valueIconScale * 100) + "%"
|
||||
visible: valueShowApplications
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("bar.workspace.focused-color-label")
|
||||
description: I18n.tr("bar.workspace.focused-color-description")
|
||||
currentKey: valueFocusedColor
|
||||
onSelected: key => {
|
||||
valueFocusedColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("bar.workspace.occupied-color-label")
|
||||
description: I18n.tr("bar.workspace.occupied-color-description")
|
||||
currentKey: valueOccupiedColor
|
||||
onSelected: key => {
|
||||
valueOccupiedColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("bar.workspace.empty-color-label")
|
||||
description: I18n.tr("bar.workspace.empty-color-description")
|
||||
currentKey: valueEmptyColor
|
||||
onSelected: key => {
|
||||
valueEmptyColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
// Widget Settings Dialog Component
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property int widgetIndex: -1
|
||||
property var widgetData: null
|
||||
property string widgetId: ""
|
||||
property string sectionId: ""
|
||||
property var screen: null
|
||||
property var settingsCache: ({})
|
||||
|
||||
signal updateWidgetSettings(string section, int index, var settings)
|
||||
|
||||
readonly property real maxHeight: (screen ? screen.height : (parent ? parent.height : 800)) * 0.8
|
||||
readonly property real defaultContentWidth: Math.round(600 * Style.uiScaleRatio)
|
||||
readonly property real settingsContentWidth: {
|
||||
if (settingsLoader.item && settingsLoader.item.implicitWidth > 0) {
|
||||
return settingsLoader.item.implicitWidth;
|
||||
}
|
||||
return defaultContentWidth;
|
||||
}
|
||||
|
||||
readonly property real dialogPadding: Style.marginXL
|
||||
width: Math.max(settingsContentWidth + dialogPadding * 2, 500)
|
||||
height: Math.min(content.implicitHeight + dialogPadding * 2, maxHeight)
|
||||
padding: 0
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
anchors.centerIn: parent
|
||||
|
||||
onOpened: {
|
||||
if (widgetData && widgetId) {
|
||||
loadWidgetSettings();
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderM
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
anchors.margins: dialogPadding
|
||||
spacing: Style.marginM
|
||||
|
||||
// Title
|
||||
RowLayout {
|
||||
id: titleRow
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
|
||||
NText {
|
||||
text: I18n.tr("system.widget-settings-title", {
|
||||
"widget": root.widgetId
|
||||
})
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
onClicked: saveAndClose()
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
id: separator
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Color.mOutline
|
||||
}
|
||||
|
||||
// Scrollable settings area
|
||||
NScrollView {
|
||||
id: scrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 100
|
||||
gradientColor: Color.mSurface
|
||||
reserveScrollbarSpace: false
|
||||
|
||||
ColumnLayout {
|
||||
width: scrollView.availableWidth
|
||||
spacing: Style.marginM
|
||||
|
||||
Loader {
|
||||
id: settingsLoader
|
||||
Layout.fillWidth: true
|
||||
onItemChanged: {
|
||||
if (item) {
|
||||
// Force width recalculation when content loads
|
||||
Qt.callLater(() => {
|
||||
root.width = Math.max(root.settingsContentWidth + root.dialogPadding * 2, 500);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveTimer
|
||||
running: false
|
||||
interval: 150
|
||||
onTriggered: {
|
||||
root.updateWidgetSettings(root.sectionId, root.widgetIndex, root.settingsCache);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: settingsLoader.item
|
||||
ignoreUnknownSignals: true
|
||||
function onSettingsChanged(newSettings) {
|
||||
if (newSettings) {
|
||||
root.settingsCache = newSettings;
|
||||
saveTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveAndClose() {
|
||||
if (settingsLoader.item && typeof settingsLoader.item.saveSettings === 'function') {
|
||||
var newSettings = settingsLoader.item.saveSettings();
|
||||
if (newSettings) {
|
||||
root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings);
|
||||
}
|
||||
}
|
||||
root.close();
|
||||
}
|
||||
|
||||
function loadWidgetSettings() {
|
||||
const widgetSettingsMap = {
|
||||
"CustomButton": "WidgetSettings/CustomButtonSettings.qml"
|
||||
};
|
||||
|
||||
const source = widgetSettingsMap[widgetId];
|
||||
if (source) {
|
||||
settingsLoader.setSource(source, {
|
||||
"widgetData": widgetData,
|
||||
"widgetMetadata": ControlCenterWidgetRegistry.widgetMetadata[widgetId]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
import QtQml.Models
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
Layout.preferredWidth: Math.round(600 * Style.uiScaleRatio)
|
||||
implicitWidth: Layout.preferredWidth
|
||||
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
property var rootSettings: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
QtObject {
|
||||
id: _settings
|
||||
|
||||
property string icon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
|
||||
property string onClicked: widgetData.onClicked !== undefined ? widgetData.onClicked : widgetMetadata.onClicked
|
||||
property string onRightClicked: widgetData.onRightClicked !== undefined ? widgetData.onRightClicked : widgetMetadata.onRightClicked
|
||||
property string onMiddleClicked: widgetData.onMiddleClicked !== undefined ? widgetData.onMiddleClicked : widgetMetadata.onMiddleClicked
|
||||
property ListModel _stateChecksListModel: ListModel {}
|
||||
property string stateChecksJson: "[]"
|
||||
property string generalTooltipText: widgetData.generalTooltipText !== undefined ? widgetData.generalTooltipText : widgetMetadata.generalTooltipText
|
||||
property bool enableOnStateLogic: widgetData.enableOnStateLogic !== undefined ? widgetData.enableOnStateLogic : widgetMetadata.enableOnStateLogic
|
||||
property bool showExecTooltip: widgetData.showExecTooltip !== undefined ? widgetData.showExecTooltip : widgetMetadata.showExecTooltip
|
||||
|
||||
function populateStateChecks() {
|
||||
try {
|
||||
var initialChecks = JSON.parse(stateChecksJson);
|
||||
if (initialChecks && Array.isArray(initialChecks)) {
|
||||
for (var i = 0; i < initialChecks.length; i++) {
|
||||
var item = initialChecks[i];
|
||||
if (item && typeof item === "object") {
|
||||
_settings._stateChecksListModel.append({
|
||||
"command": item.command || "",
|
||||
"icon": item.icon || ""
|
||||
});
|
||||
} else {
|
||||
Logger.w("CustomButtonSettings", "Invalid stateChecks entry at index " + i + ":", item);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.e("CustomButtonSettings", "Failed to parse stateChecksJson:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.rootSettings = _settings;
|
||||
stateChecksJson = (widgetData && widgetData.stateChecksJson !== undefined) ? widgetData.stateChecksJson : (widgetMetadata && widgetMetadata.stateChecksJson ? widgetMetadata.stateChecksJson : "[]");
|
||||
Qt.callLater(populateStateChecks);
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
var savedStateChecksArray = [];
|
||||
for (var i = 0; i < _settings._stateChecksListModel.count; i++) {
|
||||
savedStateChecksArray.push(_settings._stateChecksListModel.get(i));
|
||||
}
|
||||
_settings.stateChecksJson = JSON.stringify(savedStateChecksArray);
|
||||
|
||||
return {
|
||||
"id": widgetData.id,
|
||||
"icon": _settings.icon,
|
||||
"onClicked": _settings.onClicked,
|
||||
"onRightClicked": _settings.onRightClicked,
|
||||
"onMiddleClicked": _settings.onMiddleClicked,
|
||||
"stateChecksJson": _settings.stateChecksJson,
|
||||
"generalTooltipText": _settings.generalTooltipText,
|
||||
"enableOnStateLogic": _settings.enableOnStateLogic,
|
||||
"showExecTooltip": _settings.showExecTooltip
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
return _settings.saveSettings();
|
||||
}
|
||||
|
||||
function updateStateCheck(index, command, icon) {
|
||||
_settings._stateChecksListModel.set(index, {
|
||||
"command": command,
|
||||
"icon": icon
|
||||
});
|
||||
_settings.saveSettings();
|
||||
}
|
||||
|
||||
function removeStateCheck(index) {
|
||||
_settings._stateChecksListModel.remove(index);
|
||||
_settings.saveSettings();
|
||||
}
|
||||
|
||||
function addStateCheck() {
|
||||
_settings._stateChecksListModel.append({
|
||||
"command": "",
|
||||
"icon": ""
|
||||
});
|
||||
_settings.saveSettings();
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style?.marginM ?? 8
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("common.icon")
|
||||
description: I18n.tr("panels.control-center.shortcuts-custom-button-icon-description")
|
||||
}
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: _settings.icon || (widgetMetadata && widgetMetadata.icon ? widgetMetadata.icon : "")
|
||||
pointSize: Style?.fontSizeXL ?? 24
|
||||
visible: (_settings.icon || (widgetMetadata && widgetMetadata.icon ? widgetMetadata.icon : "")) !== ""
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("common.browse")
|
||||
onClicked: iconPicker.open()
|
||||
}
|
||||
}
|
||||
|
||||
NIconPicker {
|
||||
id: iconPicker
|
||||
initialIcon: _settings.icon
|
||||
onIconSelected: function (iconName) {
|
||||
_settings.icon = iconName;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.general-tooltip-text-label")
|
||||
description: I18n.tr("bar.custom-button.general-tooltip-text-description")
|
||||
placeholderText: I18n.tr("placeholders.enter-tooltip")
|
||||
text: _settings.generalTooltipText
|
||||
onTextChanged: {
|
||||
_settings.generalTooltipText = text;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.generalTooltipText
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.show-exec-tooltip-label")
|
||||
description: I18n.tr("bar.custom-button.show-exec-tooltip-description")
|
||||
checked: _settings.showExecTooltip
|
||||
onToggled: checked => {
|
||||
_settings.showExecTooltip = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showExecTooltip
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.left-click-label")
|
||||
description: I18n.tr("bar.custom-button.left-click-description")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: _settings.onClicked
|
||||
onTextChanged: {
|
||||
_settings.onClicked = text;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.onClicked
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.right-click-label")
|
||||
description: I18n.tr("bar.custom-button.right-click-description")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: _settings.onRightClicked
|
||||
onTextChanged: {
|
||||
_settings.onRightClicked = text;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.onRightClicked
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.custom-button.middle-click-label")
|
||||
description: I18n.tr("bar.custom-button.middle-click-description")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: _settings.onMiddleClicked
|
||||
onTextChanged: {
|
||||
_settings.onMiddleClicked = text;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.onMiddleClicked
|
||||
}
|
||||
|
||||
NDivider {}
|
||||
|
||||
NToggle {
|
||||
id: enableOnStateLogicToggle
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.control-center.shortcuts-custom-button-enable-on-state-logic-label")
|
||||
description: I18n.tr("panels.control-center.shortcuts-custom-button-enable-on-state-logic-description")
|
||||
checked: _settings.enableOnStateLogic
|
||||
onToggled: checked => {
|
||||
_settings.enableOnStateLogic = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.enableOnStateLogic
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: root.rootSettings && root.rootSettings.enableOnStateLogic
|
||||
spacing: Style?.marginM ?? 8
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.control-center.shortcuts-custom-button-state-checks-label")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.rootSettings ? root.rootSettings._stateChecksListModel : null
|
||||
delegate: Item {
|
||||
property int currentIndex: index
|
||||
|
||||
implicitHeight: contentRow.implicitHeight
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
id: contentRow
|
||||
anchors.fill: parent
|
||||
spacing: Style?.marginM ?? 8
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: I18n.tr("panels.control-center.shortcuts-custom-button-state-checks-command")
|
||||
text: model.command
|
||||
onTextChanged: {
|
||||
updateStateCheck(currentIndex, text, model.icon);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Style?.marginS ?? 4
|
||||
|
||||
NIcon {
|
||||
icon: model.icon
|
||||
pointSize: Style?.fontSizeL ?? 20
|
||||
visible: model.icon !== undefined && model.icon !== ""
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "folder"
|
||||
tooltipText: I18n.tr("common.browse")
|
||||
baseSize: Style?.buttonSizeS ?? 24
|
||||
onClicked: iconPickerDelegate.open()
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("panels.control-center.shortcuts-custom-button-state-checks-remove")
|
||||
baseSize: Style?.buttonSizeS ?? 24
|
||||
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
|
||||
colorBg: Color.mError
|
||||
colorFg: Color.mOnError
|
||||
colorBgHover: Qt.alpha(Color.mError, Style.opacityMedium)
|
||||
colorFgHover: Color.mOnError
|
||||
onClicked: {
|
||||
removeStateCheck(currentIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconPicker {
|
||||
id: iconPickerDelegate
|
||||
initialIcon: model.icon
|
||||
onIconSelected: function (iconName) {
|
||||
updateStateCheck(currentIndex, model.command, iconName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style?.marginM ?? 8
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("panels.control-center.shortcuts-custom-button-state-checks-add")
|
||||
onClicked: addStateCheck()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property int widgetIndex: -1
|
||||
property var widgetData: null
|
||||
property string widgetId: ""
|
||||
property string sectionId: "" // Not used for desktop widgets, but required by NSectionEditor
|
||||
property var screen: null
|
||||
property var settingsCache: ({})
|
||||
|
||||
readonly property real maxHeight: (screen ? screen.height : (parent ? parent.height : 800)) * 0.8
|
||||
|
||||
signal updateWidgetSettings(string section, int index, var settings)
|
||||
|
||||
width: Math.max(content.implicitWidth + padding * 2, 500)
|
||||
height: Math.min(content.implicitHeight + padding * 2, maxHeight)
|
||||
padding: Style.marginXL
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
dim: false
|
||||
|
||||
// Center in parent
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
|
||||
onOpened: {
|
||||
if (widgetData && widgetId) {
|
||||
loadWidgetSettings();
|
||||
}
|
||||
forceActiveFocus();
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
id: bgRect
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderM
|
||||
}
|
||||
|
||||
contentItem: FocusScope {
|
||||
id: focusScope
|
||||
focus: true
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
id: titleRow
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
|
||||
NText {
|
||||
text: I18n.tr("system.widget-settings-title", {
|
||||
"widget": DesktopWidgetRegistry.getWidgetDisplayName(root.widgetId)
|
||||
})
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
onClicked: saveAndClose()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: separator
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Color.mOutline
|
||||
}
|
||||
|
||||
// Scrollable settings area
|
||||
NScrollView {
|
||||
id: scrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 100
|
||||
gradientColor: Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
width: scrollView.availableWidth
|
||||
spacing: Style.marginM
|
||||
|
||||
Loader {
|
||||
id: settingsLoader
|
||||
Layout.fillWidth: true
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
Qt.callLater(() => {
|
||||
var firstInput = findFirstFocusable(item);
|
||||
if (firstInput) {
|
||||
firstInput.forceActiveFocus();
|
||||
} else {
|
||||
focusScope.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function findFirstFocusable(item) {
|
||||
if (!item)
|
||||
return null;
|
||||
if (item.focus !== undefined && item.focus === true)
|
||||
return item;
|
||||
if (item.children) {
|
||||
for (var i = 0; i < item.children.length; i++) {
|
||||
var child = item.children[i];
|
||||
if (child && child.focus !== undefined && child.focus === true)
|
||||
return child;
|
||||
var found = findFirstFocusable(child);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse area for a draggable popup
|
||||
MouseArea {
|
||||
x: titleRow.x
|
||||
y: titleRow.y
|
||||
width: titleRow.width
|
||||
height: titleRow.height
|
||||
z: -1
|
||||
|
||||
cursorShape: Qt.OpenHandCursor
|
||||
|
||||
property real pressX: 0
|
||||
property real pressY: 0
|
||||
|
||||
onPressed: mouse => {
|
||||
pressX = mouse.x;
|
||||
pressY = mouse.y;
|
||||
cursorShape = Qt.ClosedHandCursor;
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
cursorShape = Qt.OpenHandCursor;
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (pressed) {
|
||||
var deltaX = mouse.x - pressX;
|
||||
var deltaY = mouse.y - pressY;
|
||||
|
||||
root.x += deltaX;
|
||||
root.y += deltaY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveTimer
|
||||
running: false
|
||||
interval: 150
|
||||
onTriggered: {
|
||||
root.updateWidgetSettings(root.sectionId, root.widgetIndex, root.settingsCache);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: settingsLoader.item
|
||||
ignoreUnknownSignals: true
|
||||
function onSettingsChanged(newSettings) {
|
||||
if (newSettings) {
|
||||
root.settingsCache = newSettings;
|
||||
saveTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
function onSettingsSaved(newSettings) {
|
||||
if (newSettings) {
|
||||
root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveAndClose() {
|
||||
if (settingsLoader.item && typeof settingsLoader.item.saveSettings === 'function') {
|
||||
var newSettings = settingsLoader.item.saveSettings();
|
||||
if (newSettings) {
|
||||
root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings);
|
||||
}
|
||||
}
|
||||
root.close();
|
||||
}
|
||||
|
||||
function loadWidgetSettings() {
|
||||
// Handle plugin widgets
|
||||
if (DesktopWidgetRegistry.isPluginWidget(widgetId)) {
|
||||
var pluginId = widgetId.replace("plugin:", "");
|
||||
var manifest = PluginRegistry.getPluginManifest(pluginId);
|
||||
|
||||
var pluginDir = PluginRegistry.getPluginDir(pluginId);
|
||||
var loadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
|
||||
var api = PluginService.getPluginAPI(pluginId);
|
||||
|
||||
var settingsPath;
|
||||
if (manifest && manifest.entryPoints && manifest.entryPoints.desktopWidgetSettings) {
|
||||
settingsPath = "file://" + pluginDir + "/" + manifest.entryPoints.desktopWidgetSettings;
|
||||
|
||||
var widgetSettings = {};
|
||||
widgetSettings.data = widgetData || {};
|
||||
widgetSettings.metadata = DesktopWidgetRegistry.widgetMetadata[widgetId] || {};
|
||||
widgetSettings.save = function () {
|
||||
var newSettings = Object.assign({}, widgetSettings.data);
|
||||
root.settingsCache = newSettings;
|
||||
saveTimer.start();
|
||||
};
|
||||
|
||||
settingsLoader.setSource(settingsPath + "?v=" + loadVersion, {
|
||||
"pluginApi": api,
|
||||
"widgetSettings": widgetSettings
|
||||
});
|
||||
} else {
|
||||
Logger.w("DesktopWidgetSettingsDialog", "Plugin does not have desktop widget settings:", pluginId);
|
||||
|
||||
// Fallback to the plugin settings
|
||||
if (manifest && manifest.entryPoints && manifest.entryPoints.settings) {
|
||||
settingsPath = "file://" + pluginDir + "/" + manifest.entryPoints.settings;
|
||||
|
||||
settingsLoader.setSource(settingsPath + "?v=" + loadVersion, {
|
||||
"pluginApi": api
|
||||
});
|
||||
} else {
|
||||
Logger.w("DesktopWidgetSettingsDialog", "Plugin does not have settings:", pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle core widgets
|
||||
const source = DesktopWidgetRegistry.widgetSettingsMap[widgetId];
|
||||
if (source) {
|
||||
var currentWidgetData = widgetData;
|
||||
var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || [];
|
||||
for (var i = 0; i < monitorWidgets.length; i++) {
|
||||
if (monitorWidgets[i].name === sectionId) {
|
||||
var widgets = monitorWidgets[i].widgets || [];
|
||||
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
||||
currentWidgetData = widgets[widgetIndex];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
var fullPath = Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/DesktopWidgets/" + source);
|
||||
settingsLoader.setSource(fullPath, {
|
||||
"widgetData": currentWidgetData,
|
||||
"widgetMetadata": DesktopWidgetRegistry.widgetMetadata[widgetId]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property int valueWidth: widgetData.width !== undefined ? widgetData.width : widgetMetadata.width
|
||||
property int valueHeight: widgetData.height !== undefined ? widgetData.height : widgetMetadata.height
|
||||
property string valueVisualizerType: widgetData.visualizerType !== undefined ? widgetData.visualizerType : widgetMetadata.visualizerType
|
||||
property string valueColorName: widgetData.colorName !== undefined ? widgetData.colorName : widgetMetadata.colorName
|
||||
property bool valueHideWhenIdle: widgetData.hideWhenIdle !== undefined ? widgetData.hideWhenIdle : widgetMetadata.hideWhenIdle
|
||||
property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground
|
||||
property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.width = valueWidth;
|
||||
settings.height = valueHeight;
|
||||
settings.visualizerType = valueVisualizerType;
|
||||
settings.colorName = valueColorName;
|
||||
settings.hideWhenIdle = valueHideWhenIdle;
|
||||
settings.showBackground = valueShowBackground;
|
||||
settings.roundedCorners = valueRoundedCorners;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: widthInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("common.width")
|
||||
description: I18n.tr("bar.audio-visualizer.width-description")
|
||||
text: String(valueWidth)
|
||||
placeholderText: I18n.tr("placeholders.enter-width-pixels")
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
onEditingFinished: {
|
||||
const parsed = parseInt(text);
|
||||
if (!isNaN(parsed) && parsed > 0) {
|
||||
valueWidth = parsed;
|
||||
saveSettings();
|
||||
} else {
|
||||
text = String(valueWidth);
|
||||
}
|
||||
}
|
||||
defaultValue: String(widgetMetadata.width)
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: heightInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("common.height")
|
||||
description: I18n.tr("bar.audio-visualizer.height-description")
|
||||
text: String(valueHeight)
|
||||
placeholderText: I18n.tr("placeholders.enter-width-pixels")
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
onEditingFinished: {
|
||||
const parsed = parseInt(text);
|
||||
if (!isNaN(parsed) && parsed > 0) {
|
||||
valueHeight = parsed;
|
||||
saveSettings();
|
||||
} else {
|
||||
text = String(valueHeight);
|
||||
}
|
||||
}
|
||||
defaultValue: String(widgetMetadata.height)
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.audio.visualizer-type-label")
|
||||
description: I18n.tr("panels.desktop-widgets.media-player-visualizer-type-description")
|
||||
model: [
|
||||
{
|
||||
"key": "linear",
|
||||
"name": I18n.tr("options.visualizer-types.linear")
|
||||
},
|
||||
{
|
||||
"key": "mirrored",
|
||||
"name": I18n.tr("options.visualizer-types.mirrored")
|
||||
},
|
||||
{
|
||||
"key": "wave",
|
||||
"name": I18n.tr("options.visualizer-types.wave")
|
||||
}
|
||||
]
|
||||
currentKey: valueVisualizerType
|
||||
onSelected: key => {
|
||||
valueVisualizerType = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.visualizerType
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.audio-visualizer.color-name-label")
|
||||
description: I18n.tr("bar.audio-visualizer.color-name-description")
|
||||
currentKey: valueColorName
|
||||
onSelected: key => {
|
||||
valueColorName = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.colorName
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.audio-visualizer.hide-when-idle-label")
|
||||
description: I18n.tr("bar.audio-visualizer.hide-when-idle-description")
|
||||
checked: valueHideWhenIdle
|
||||
onToggled: checked => {
|
||||
valueHideWhenIdle = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.hideWhenIdle
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.desktop-widgets.clock-show-background-label")
|
||||
description: I18n.tr("panels.desktop-widgets.media-player-show-background-description")
|
||||
checked: valueShowBackground
|
||||
onToggled: checked => {
|
||||
valueShowBackground = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showBackground
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
visible: valueShowBackground
|
||||
label: I18n.tr("panels.desktop-widgets.clock-rounded-corners-label")
|
||||
description: I18n.tr("panels.desktop-widgets.media-player-rounded-corners-description")
|
||||
checked: valueRoundedCorners
|
||||
onToggled: checked => {
|
||||
valueRoundedCorners = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.roundedCorners
|
||||
}
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
width: 700
|
||||
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground
|
||||
property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners
|
||||
property string valueClockStyle: widgetData.clockStyle !== undefined ? widgetData.clockStyle : widgetMetadata.clockStyle
|
||||
property string valueClockColor: widgetData.clockColor !== undefined ? widgetData.clockColor : widgetMetadata.clockColor
|
||||
property bool valueUseCustomFont: widgetData.useCustomFont !== undefined ? widgetData.useCustomFont : widgetMetadata.useCustomFont
|
||||
property string valueCustomFont: widgetData.customFont !== undefined ? widgetData.customFont : widgetMetadata.custonFont
|
||||
property string valueFormat: widgetData.format !== undefined ? widgetData.format : widgetMetadata.format
|
||||
|
||||
// Track the currently focused input field
|
||||
property var focusedInput: null
|
||||
|
||||
readonly property bool isMinimalMode: valueClockStyle === "minimal"
|
||||
readonly property var now: Time.now
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.showBackground = valueShowBackground;
|
||||
settings.roundedCorners = valueRoundedCorners;
|
||||
settings.clockStyle = valueClockStyle;
|
||||
settings.clockColor = valueClockColor;
|
||||
settings.useCustomFont = valueUseCustomFont;
|
||||
settings.customFont = valueCustomFont;
|
||||
settings.format = valueFormat.trim();
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
// Function to insert token at cursor position in the focused input
|
||||
function insertToken(token) {
|
||||
if (!focusedInput || !focusedInput.inputItem) {
|
||||
// If no input is focused, default to format input
|
||||
if (formatInput.inputItem) {
|
||||
formatInput.inputItem.focus = true;
|
||||
focusedInput = formatInput;
|
||||
}
|
||||
}
|
||||
|
||||
if (focusedInput && focusedInput.inputItem) {
|
||||
var input = focusedInput.inputItem;
|
||||
var cursorPos = input.cursorPosition;
|
||||
var currentText = input.text;
|
||||
|
||||
// Insert token at cursor position
|
||||
var newText = currentText.substring(0, cursorPos) + token + currentText.substring(cursorPos);
|
||||
input.text = newText + " ";
|
||||
|
||||
// Move cursor after the inserted token
|
||||
input.cursorPosition = cursorPos + token.length + 1;
|
||||
|
||||
// Ensure the input keeps focus
|
||||
input.focus = true;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.desktop-widgets.clock-style-label")
|
||||
description: I18n.tr("panels.desktop-widgets.clock-style-description")
|
||||
currentKey: valueClockStyle
|
||||
minimumWidth: 260 * Style.uiScaleRatio
|
||||
model: [
|
||||
{
|
||||
"key": "minimal",
|
||||
"name": I18n.tr("panels.desktop-widgets.clock-style-minimal")
|
||||
},
|
||||
{
|
||||
"key": "digital",
|
||||
"name": I18n.tr("panels.desktop-widgets.clock-style-digital")
|
||||
},
|
||||
{
|
||||
"key": "analog",
|
||||
"name": I18n.tr("panels.desktop-widgets.clock-style-analog")
|
||||
},
|
||||
{
|
||||
"key": "binary",
|
||||
"name": I18n.tr("panels.desktop-widgets.clock-style-binary")
|
||||
}
|
||||
]
|
||||
onSelected: key => {
|
||||
valueClockStyle = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.clockStyle
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: I18n.tr("common.select-color")
|
||||
description: I18n.tr("common.select-color-description")
|
||||
currentKey: valueClockColor
|
||||
onSelected: key => {
|
||||
valueClockColor = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.clockColor
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.clock.use-custom-font-label")
|
||||
description: I18n.tr("bar.clock.use-custom-font-description")
|
||||
checked: valueUseCustomFont
|
||||
onToggled: checked => {
|
||||
valueUseCustomFont = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.useCustomFont
|
||||
}
|
||||
|
||||
NSearchableComboBox {
|
||||
Layout.fillWidth: true
|
||||
visible: valueUseCustomFont
|
||||
label: I18n.tr("bar.clock.custom-font-label")
|
||||
description: I18n.tr("bar.clock.custom-font-description")
|
||||
model: FontService.availableFonts
|
||||
currentKey: valueCustomFont
|
||||
placeholder: I18n.tr("bar.clock.custom-font-placeholder")
|
||||
searchPlaceholder: I18n.tr("bar.clock.custom-font-search-placeholder")
|
||||
popupHeight: 420
|
||||
minimumWidth: 300
|
||||
onSelected: function (key) {
|
||||
valueCustomFont = key;
|
||||
saveSettings();
|
||||
}
|
||||
enabled: valueClockStyle === "minimal"
|
||||
defaultValue: Settings.data.ui.fontDefault
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
visible: isMinimalMode
|
||||
}
|
||||
|
||||
NHeader {
|
||||
visible: isMinimalMode
|
||||
label: I18n.tr("bar.clock.clock-display-label")
|
||||
description: I18n.tr("bar.clock.clock-display-description")
|
||||
}
|
||||
|
||||
// Format editor - only visible in minimal mode
|
||||
RowLayout {
|
||||
id: main
|
||||
visible: isMinimalMode
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
|
||||
NTextInput {
|
||||
id: formatInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.desktop-widgets.clock-format-label")
|
||||
description: I18n.tr("bar.clock.horizontal-bar-description")
|
||||
placeholderText: "HH:mm\\nd MMMM yyyy"
|
||||
text: valueFormat
|
||||
onTextChanged: {
|
||||
valueFormat = text;
|
||||
settingsChanged(saveSettings());
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (inputItem) {
|
||||
inputItem.onActiveFocusChanged.connect(function () {
|
||||
if (inputItem.activeFocus) {
|
||||
root.focusedInput = formatInput;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
defaultValue: widgetMetadata.format
|
||||
}
|
||||
}
|
||||
|
||||
// Preview
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
Layout.fillWidth: false
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("bar.clock.preview")
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 320
|
||||
Layout.preferredHeight: 160
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM
|
||||
border.color: Color.mSecondary
|
||||
border.width: Style.borderS
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
anchors.centerIn: parent
|
||||
|
||||
ColumnLayout {
|
||||
spacing: -2
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Repeater {
|
||||
Layout.topMargin: Style.marginM
|
||||
model: I18n.locale.toString(now, valueFormat.trim()).split("\\n")
|
||||
delegate: NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
family: valueUseCustomFont && valueCustomFont ? valueCustomFont : Settings.data.ui.fontDefault
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.resolveColorKey(valueClockColor)
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.topMargin: Style.marginM
|
||||
Layout.bottomMargin: Style.marginM
|
||||
visible: isMinimalMode
|
||||
}
|
||||
|
||||
NDateTimeTokens {
|
||||
Layout.fillWidth: true
|
||||
height: 200
|
||||
visible: isMinimalMode
|
||||
onTokenClicked: token => root.insertToken(token)
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.desktop-widgets.clock-show-background-label")
|
||||
description: I18n.tr("panels.desktop-widgets.clock-show-background-description")
|
||||
checked: valueShowBackground
|
||||
onToggled: checked => {
|
||||
valueShowBackground = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showBackground
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
visible: valueShowBackground
|
||||
label: I18n.tr("panels.desktop-widgets.clock-rounded-corners-label")
|
||||
description: I18n.tr("panels.desktop-widgets.clock-rounded-corners-description")
|
||||
checked: valueRoundedCorners
|
||||
onToggled: checked => {
|
||||
valueRoundedCorners = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.roundedCorners
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground
|
||||
property string valueVisualizerType: widgetData.visualizerType ? widgetData.visualizerType : widgetMetadata.visualizerType
|
||||
property string valueHideMode: widgetData.hideMode !== undefined ? widgetData.hideMode : widgetMetadata.hideMode
|
||||
property bool valueShowButtons: widgetData.showButtons !== undefined ? widgetData.showButtons : widgetMetadata.showButtons
|
||||
property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt
|
||||
property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer
|
||||
property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.showBackground = valueShowBackground;
|
||||
settings.visualizerType = valueVisualizerType;
|
||||
settings.hideMode = valueHideMode;
|
||||
settings.showButtons = valueShowButtons;
|
||||
settings.showAlbumArt = valueShowAlbumArt;
|
||||
settings.showVisualizer = valueShowVisualizer;
|
||||
settings.roundedCorners = valueRoundedCorners;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.desktop-widgets.clock-show-background-label")
|
||||
description: I18n.tr("panels.desktop-widgets.media-player-show-background-description")
|
||||
checked: valueShowBackground
|
||||
onToggled: checked => {
|
||||
valueShowBackground = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showBackground
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.desktop-widgets.clock-rounded-corners-label")
|
||||
description: I18n.tr("panels.desktop-widgets.media-player-rounded-corners-description")
|
||||
checked: valueRoundedCorners
|
||||
onToggled: checked => {
|
||||
valueRoundedCorners = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.roundedCorners
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.desktop-widgets.media-player-show-album-art-label")
|
||||
description: I18n.tr("panels.desktop-widgets.media-player-show-album-art-description")
|
||||
checked: valueShowAlbumArt
|
||||
onToggled: checked => {
|
||||
valueShowAlbumArt = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showAlbumArt
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.media-mini.show-visualizer-label")
|
||||
description: I18n.tr("panels.desktop-widgets.media-player-show-visualizer-description")
|
||||
checked: valueShowVisualizer
|
||||
onToggled: checked => {
|
||||
valueShowVisualizer = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showVisualizer
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.desktop-widgets.media-player-show-buttons-label")
|
||||
description: I18n.tr("panels.desktop-widgets.media-player-show-buttons-description")
|
||||
checked: valueShowButtons
|
||||
onToggled: checked => {
|
||||
valueShowButtons = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showButtons
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.audio.visualizer-type-label")
|
||||
description: I18n.tr("panels.desktop-widgets.media-player-visualizer-type-description")
|
||||
enabled: valueShowVisualizer
|
||||
model: [
|
||||
{
|
||||
"key": "linear",
|
||||
"name": I18n.tr("options.visualizer-types.linear")
|
||||
},
|
||||
{
|
||||
"key": "mirrored",
|
||||
"name": I18n.tr("options.visualizer-types.mirrored")
|
||||
},
|
||||
{
|
||||
"key": "wave",
|
||||
"name": I18n.tr("options.visualizer-types.wave")
|
||||
}
|
||||
]
|
||||
currentKey: valueVisualizerType
|
||||
onSelected: key => {
|
||||
valueVisualizerType = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.visualizerType
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.taskbar.hide-mode-label")
|
||||
description: I18n.tr("bar.media-mini.hide-mode-description")
|
||||
model: [
|
||||
{
|
||||
"key": "hidden",
|
||||
"name": I18n.tr("hide-modes.hidden")
|
||||
},
|
||||
{
|
||||
"key": "idle",
|
||||
"name": I18n.tr("hide-modes.idle")
|
||||
},
|
||||
{
|
||||
"key": "visible",
|
||||
"name": I18n.tr("hide-modes.visible")
|
||||
}
|
||||
]
|
||||
currentKey: valueHideMode
|
||||
onSelected: key => {
|
||||
valueHideMode = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.hideMode
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
width: 700
|
||||
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property string valueStatType: widgetData.statType !== undefined ? widgetData.statType : widgetMetadata.statType
|
||||
property string valueDiskPath: widgetData.diskPath !== undefined ? widgetData.diskPath : widgetMetadata.diskPath
|
||||
property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground
|
||||
property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners
|
||||
property string valueLayout: widgetData.layout !== undefined ? widgetData.layout : widgetMetadata.layout
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.statType = valueStatType;
|
||||
settings.diskPath = valueDiskPath;
|
||||
settings.showBackground = valueShowBackground;
|
||||
settings.roundedCorners = valueRoundedCorners;
|
||||
settings.layout = valueLayout;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.desktop-widgets.system-stat-stat-type-label")
|
||||
description: I18n.tr("panels.desktop-widgets.system-stat-stat-type-description")
|
||||
currentKey: valueStatType
|
||||
minimumWidth: 260 * Style.uiScaleRatio
|
||||
model: {
|
||||
let items = [
|
||||
{
|
||||
"key": "CPU",
|
||||
"name": I18n.tr("system-monitor.cpu-usage")
|
||||
},
|
||||
{
|
||||
"key": "Memory",
|
||||
"name": I18n.tr("common.memory")
|
||||
},
|
||||
{
|
||||
"key": "Network",
|
||||
"name": I18n.tr("bar.system-monitor.network-traffic-label")
|
||||
},
|
||||
{
|
||||
"key": "Disk",
|
||||
"name": I18n.tr("system-monitor.disk")
|
||||
}
|
||||
];
|
||||
if (Settings.data.systemMonitor.enableDgpuMonitoring)
|
||||
items.push({
|
||||
"key": "GPU",
|
||||
"name": I18n.tr("panels.system-monitor.gpu-section-label")
|
||||
});
|
||||
return items;
|
||||
}
|
||||
onSelected: key => {
|
||||
valueStatType = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.statType
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
visible: valueStatType === "Disk"
|
||||
label: I18n.tr("bar.system-monitor.disk-path-label")
|
||||
description: I18n.tr("bar.system-monitor.disk-path-description")
|
||||
model: {
|
||||
const paths = Object.keys(SystemStatService.diskPercents).sort();
|
||||
return paths.map(path => ({
|
||||
key: path,
|
||||
name: path
|
||||
}));
|
||||
}
|
||||
currentKey: valueDiskPath
|
||||
onSelected: key => {
|
||||
valueDiskPath = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.diskPath
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.desktop-widgets.system-stat-show-background-label")
|
||||
description: I18n.tr("panels.desktop-widgets.system-stat-show-background-description")
|
||||
checked: valueShowBackground
|
||||
onToggled: checked => {
|
||||
valueShowBackground = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showBackground
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
visible: valueShowBackground
|
||||
label: I18n.tr("panels.desktop-widgets.system-stat-rounded-corners-label")
|
||||
description: I18n.tr("panels.desktop-widgets.system-stat-rounded-corners-description")
|
||||
checked: valueRoundedCorners
|
||||
onToggled: checked => {
|
||||
valueRoundedCorners = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.roundedCorners
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.desktop-widgets.system-stat-layout-label")
|
||||
description: I18n.tr("panels.desktop-widgets.system-stat-layout-description")
|
||||
currentKey: valueLayout
|
||||
minimumWidth: 260 * Style.uiScaleRatio
|
||||
model: [
|
||||
{
|
||||
"key": "side",
|
||||
"name": I18n.tr("panels.desktop-widgets.system-stat-layout-side")
|
||||
},
|
||||
{
|
||||
"key": "bottom",
|
||||
"name": I18n.tr("panels.desktop-widgets.system-stat-layout-bottom")
|
||||
}
|
||||
]
|
||||
onSelected: key => {
|
||||
valueLayout = key;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.layout
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM
|
||||
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
signal settingsChanged(var settings)
|
||||
|
||||
property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground
|
||||
property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {});
|
||||
settings.showBackground = valueShowBackground;
|
||||
settings.roundedCorners = valueRoundedCorners;
|
||||
settingsChanged(settings);
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.desktop-widgets.clock-show-background-label")
|
||||
description: I18n.tr("panels.desktop-widgets.weather-show-background-description")
|
||||
checked: valueShowBackground
|
||||
onToggled: checked => {
|
||||
valueShowBackground = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.showBackground
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
visible: valueShowBackground
|
||||
label: I18n.tr("panels.desktop-widgets.clock-rounded-corners-label")
|
||||
description: I18n.tr("panels.desktop-widgets.clock-rounded-corners-description")
|
||||
checked: valueRoundedCorners
|
||||
onToggled: checked => {
|
||||
valueRoundedCorners = checked;
|
||||
saveSettings();
|
||||
}
|
||||
defaultValue: widgetMetadata.roundedCorners
|
||||
}
|
||||
}
|
||||
+1331
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,290 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.MainScreen
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
SmartPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: Math.round(840 * Style.uiScaleRatio)
|
||||
preferredHeight: Math.round(910 * Style.uiScaleRatio)
|
||||
|
||||
// Settings panel mode: "centered", "attached", "window"
|
||||
readonly property string settingsPanelMode: Settings.data.ui.settingsPanelMode
|
||||
readonly property bool isWindowMode: settingsPanelMode === "window"
|
||||
readonly property bool attachToBar: settingsPanelMode === "attached"
|
||||
|
||||
readonly property string barDensity: Settings.data.bar.density
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
|
||||
readonly property bool barFloating: Settings.data.bar.barType === "floating"
|
||||
readonly property real barMarginH: barFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0
|
||||
readonly property real barMarginV: barFloating ? Math.ceil(Settings.data.bar.marginVertical) : 0
|
||||
|
||||
forceAttachToBar: attachToBar
|
||||
panelAnchorHorizontalCenter: !root.useButtonPosition && (attachToBar ? (barPosition === "top" || barPosition === "bottom") : true)
|
||||
panelAnchorVerticalCenter: !root.useButtonPosition && (attachToBar ? (barPosition === "left" || barPosition === "right") : true)
|
||||
panelAnchorTop: !root.useButtonPosition && attachToBar && barPosition === "top"
|
||||
panelAnchorBottom: !root.useButtonPosition && attachToBar && barPosition === "bottom"
|
||||
panelAnchorLeft: !root.useButtonPosition && attachToBar && barPosition === "left"
|
||||
panelAnchorRight: !root.useButtonPosition && attachToBar && barPosition === "right"
|
||||
|
||||
onAttachToBarChanged: {
|
||||
if (isPanelOpen) {
|
||||
Qt.callLater(root.setPosition);
|
||||
}
|
||||
}
|
||||
|
||||
onBarPositionChanged: {
|
||||
if (isPanelOpen) {
|
||||
Qt.callLater(root.setPosition);
|
||||
}
|
||||
}
|
||||
|
||||
onBarDensityChanged: {
|
||||
if (isPanelOpen) {
|
||||
Qt.callLater(root.setPosition);
|
||||
}
|
||||
}
|
||||
|
||||
onBarFloatingChanged: {
|
||||
if (isPanelOpen) {
|
||||
Qt.callLater(root.setPosition);
|
||||
}
|
||||
}
|
||||
|
||||
onBarMarginHChanged: {
|
||||
if (isPanelOpen) {
|
||||
Qt.callLater(root.setPosition);
|
||||
}
|
||||
}
|
||||
|
||||
onBarMarginVChanged: {
|
||||
if (isPanelOpen) {
|
||||
Qt.callLater(root.setPosition);
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs enumeration, order is NOT relevant
|
||||
enum Tab {
|
||||
About,
|
||||
Audio,
|
||||
Bar,
|
||||
ColorScheme,
|
||||
LockScreen,
|
||||
ControlCenter,
|
||||
DesktopWidgets,
|
||||
OSD,
|
||||
Display,
|
||||
Dock,
|
||||
General,
|
||||
Hooks,
|
||||
Idle,
|
||||
Launcher,
|
||||
Location,
|
||||
Connections,
|
||||
Notifications,
|
||||
Plugins,
|
||||
SessionMenu,
|
||||
System,
|
||||
UserInterface,
|
||||
Wallpaper
|
||||
}
|
||||
|
||||
property int requestedTab: SettingsPanel.Tab.General
|
||||
property int requestedSubTab: -1
|
||||
property var requestedEntry: null
|
||||
|
||||
// Content state - these are synced with SettingsContent when panel opens
|
||||
property int currentTabIndex: 0
|
||||
property var tabsModel: []
|
||||
property var activeScrollView: null
|
||||
|
||||
// Internal reference to the content (set when panel content loads)
|
||||
property var _settingsContent: null
|
||||
|
||||
// Override toggle to handle window mode
|
||||
function toggle(buttonItem, buttonName) {
|
||||
if (isWindowMode) {
|
||||
SettingsPanelService.toggleWindow(requestedTab);
|
||||
return;
|
||||
}
|
||||
// Call parent toggle
|
||||
if (isPanelOpen) {
|
||||
close();
|
||||
} else {
|
||||
open(buttonItem, buttonName);
|
||||
}
|
||||
}
|
||||
|
||||
// Override open to handle window mode
|
||||
function open(buttonItem, buttonName) {
|
||||
if (isWindowMode) {
|
||||
SettingsPanelService.openWindow(requestedTab);
|
||||
return;
|
||||
}
|
||||
|
||||
// Panel mode: replicate SmartPanel.open() logic
|
||||
if (!buttonItem && buttonName) {
|
||||
if (typeof buttonName === "object" && buttonName.x !== undefined && buttonName.y !== undefined) {
|
||||
root.buttonItem = null;
|
||||
root.buttonPosition = buttonName;
|
||||
root.buttonWidth = 0;
|
||||
root.buttonHeight = 0;
|
||||
root.useButtonPosition = true;
|
||||
} else {
|
||||
buttonItem = BarService.lookupWidget(buttonName, screen.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (buttonItem) {
|
||||
root.buttonItem = buttonItem;
|
||||
var buttonPos = buttonItem.mapToItem(null, 0, 0);
|
||||
root.buttonPosition = Qt.point(buttonPos.x, buttonPos.y);
|
||||
root.buttonWidth = buttonItem.width;
|
||||
root.buttonHeight = buttonItem.height;
|
||||
root.useButtonPosition = true;
|
||||
} else if (!(buttonName && typeof buttonName === "object" && buttonName.x !== undefined && buttonName.y !== undefined)) {
|
||||
root.buttonItem = null;
|
||||
root.useButtonPosition = false;
|
||||
}
|
||||
|
||||
isPanelOpen = true;
|
||||
PanelService.willOpenPanel(root);
|
||||
}
|
||||
|
||||
// Open to a specific tab and optionally a subtab
|
||||
function openToTab(tab, subTab, buttonItem, buttonName) {
|
||||
requestedTab = tab !== undefined ? tab : SettingsPanel.Tab.General;
|
||||
requestedSubTab = subTab !== undefined ? subTab : -1;
|
||||
open(buttonItem, buttonName);
|
||||
}
|
||||
|
||||
// When the panel opens, initialize content
|
||||
onOpened: {
|
||||
if (_settingsContent) {
|
||||
if (requestedEntry) {
|
||||
_settingsContent.requestedTab = requestedEntry.tab;
|
||||
_settingsContent.initialize();
|
||||
const entry = requestedEntry;
|
||||
requestedEntry = null;
|
||||
Qt.callLater(() => _settingsContent.navigateToResult(entry));
|
||||
} else {
|
||||
_settingsContent.requestedTab = requestedTab;
|
||||
if (requestedSubTab >= 0) {
|
||||
_settingsContent._pendingSubTab = requestedSubTab;
|
||||
requestedSubTab = -1;
|
||||
}
|
||||
_settingsContent.initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll functions - delegate to content
|
||||
function scrollDown() {
|
||||
if (_settingsContent)
|
||||
_settingsContent.scrollDown();
|
||||
}
|
||||
|
||||
function scrollUp() {
|
||||
if (_settingsContent)
|
||||
_settingsContent.scrollUp();
|
||||
}
|
||||
|
||||
function scrollPageDown() {
|
||||
if (_settingsContent)
|
||||
_settingsContent.scrollPageDown();
|
||||
}
|
||||
|
||||
function scrollPageUp() {
|
||||
if (_settingsContent)
|
||||
_settingsContent.scrollPageUp();
|
||||
}
|
||||
|
||||
// Navigation functions - delegate to content
|
||||
function selectNextTab() {
|
||||
if (_settingsContent)
|
||||
_settingsContent.selectNextTab();
|
||||
}
|
||||
|
||||
function selectPreviousTab() {
|
||||
if (_settingsContent)
|
||||
_settingsContent.selectPreviousTab();
|
||||
}
|
||||
|
||||
// Override keyboard handlers from SmartPanel
|
||||
function onTabPressed() {
|
||||
selectNextTab();
|
||||
}
|
||||
|
||||
function onBackTabPressed() {
|
||||
selectPreviousTab();
|
||||
}
|
||||
|
||||
function onUpPressed() {
|
||||
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
|
||||
_settingsContent.searchSelectPrevious();
|
||||
} else {
|
||||
scrollUp();
|
||||
}
|
||||
}
|
||||
|
||||
function onDownPressed() {
|
||||
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
|
||||
_settingsContent.searchSelectNext();
|
||||
} else {
|
||||
scrollDown();
|
||||
}
|
||||
}
|
||||
|
||||
function onPageUpPressed() {
|
||||
scrollPageUp();
|
||||
}
|
||||
|
||||
function onPageDownPressed() {
|
||||
scrollPageDown();
|
||||
}
|
||||
|
||||
function onCtrlJPressed() {
|
||||
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
|
||||
_settingsContent.searchSelectNext();
|
||||
} else {
|
||||
scrollDown();
|
||||
}
|
||||
}
|
||||
|
||||
function onCtrlKPressed() {
|
||||
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
|
||||
_settingsContent.searchSelectPrevious();
|
||||
} else {
|
||||
scrollUp();
|
||||
}
|
||||
}
|
||||
|
||||
panelContent: Rectangle {
|
||||
id: panelContent
|
||||
color: "transparent"
|
||||
|
||||
SettingsContent {
|
||||
id: settingsContent
|
||||
anchors.fill: parent
|
||||
screen: root.screen
|
||||
onCloseRequested: root.close()
|
||||
Component.onCompleted: {
|
||||
root._settingsContent = settingsContent;
|
||||
root.tabsModel = Qt.binding(function () {
|
||||
return settingsContent.tabsModel;
|
||||
});
|
||||
root.currentTabIndex = Qt.binding(function () {
|
||||
return settingsContent.currentTabIndex;
|
||||
});
|
||||
root.activeScrollView = Qt.binding(function () {
|
||||
return settingsContent.activeScrollView;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
FloatingWindow {
|
||||
id: root
|
||||
|
||||
title: "Noctalia"
|
||||
minimumSize: Qt.size(840 * Style.uiScaleRatio, 910 * Style.uiScaleRatio)
|
||||
implicitWidth: Math.round(840 * Style.uiScaleRatio)
|
||||
implicitHeight: Math.round(910 * Style.uiScaleRatio)
|
||||
color: "transparent"
|
||||
|
||||
visible: false
|
||||
|
||||
// Register with SettingsPanelService
|
||||
Component.onCompleted: {
|
||||
SettingsPanelService.settingsWindow = root;
|
||||
}
|
||||
|
||||
property bool isInitialized: false
|
||||
|
||||
// Navigate to a specific tab and optional subtab.
|
||||
// Works whether the window is already visible or just becoming visible.
|
||||
function navigateTo(tab, subTab) {
|
||||
const tabId = tab !== undefined ? tab : 0;
|
||||
const subTabId = (subTab !== undefined && subTab !== null && subTab >= 0) ? subTab : -1;
|
||||
if (isInitialized) {
|
||||
settingsContent.navigateToTab(tabId, subTabId);
|
||||
} else {
|
||||
settingsContent.requestedTab = tabId;
|
||||
if (subTabId >= 0)
|
||||
settingsContent._pendingSubTab = subTabId;
|
||||
settingsContent.initialize();
|
||||
isInitialized = true;
|
||||
// Tab content persists in window mode; if no subtab specified and the
|
||||
// tab content is still loaded (same tab), reset to first subtab
|
||||
if (subTabId < 0 && settingsContent.activeTabContent)
|
||||
settingsContent.setSubTabIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to a search result entry.
|
||||
// Works whether the window is already visible or just becoming visible.
|
||||
function navigateToEntry(entry) {
|
||||
if (isInitialized) {
|
||||
Qt.callLater(() => settingsContent.navigateToResult(entry));
|
||||
} else {
|
||||
settingsContent.requestedTab = entry.tab;
|
||||
settingsContent.initialize();
|
||||
Qt.callLater(() => settingsContent.navigateToResult(entry));
|
||||
isInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Sync visibility with service
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
SettingsPanelService.isWindowOpen = true;
|
||||
} else {
|
||||
isInitialized = false;
|
||||
SettingsPanelService.isWindowOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
Shortcut {
|
||||
sequence: "Escape"
|
||||
enabled: !PanelService.isKeybindRecording
|
||||
onActivated: SettingsPanelService.closeWindow()
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Tab"
|
||||
enabled: !PanelService.isKeybindRecording
|
||||
onActivated: settingsContent.selectNextTab()
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Backtab"
|
||||
enabled: !PanelService.isKeybindRecording
|
||||
onActivated: settingsContent.selectPreviousTab()
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: Settings.data.general.keybinds.keyUp || []
|
||||
Shortcut {
|
||||
sequence: modelData
|
||||
enabled: !PanelService.isKeybindRecording
|
||||
onActivated: {
|
||||
if (settingsContent.searchText.trim() !== "")
|
||||
settingsContent.searchSelectPrevious();
|
||||
else
|
||||
settingsContent.scrollUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: Settings.data.general.keybinds.keyDown || []
|
||||
Shortcut {
|
||||
sequence: modelData
|
||||
enabled: !PanelService.isKeybindRecording
|
||||
onActivated: {
|
||||
if (settingsContent.searchText.trim() !== "")
|
||||
settingsContent.searchSelectNext();
|
||||
else
|
||||
settingsContent.scrollDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main content
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity)
|
||||
radius: Style.radiusL
|
||||
|
||||
SettingsContent {
|
||||
id: settingsContent
|
||||
anchors.fill: parent
|
||||
onCloseRequested: SettingsPanelService.closeWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.info")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.contributors")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.supporters")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
VersionSubTab {}
|
||||
ContributorsSubTab {}
|
||||
SupportersSubTab {}
|
||||
}
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var contributors: GitHubService.contributors
|
||||
property int avatarCacheVersion: 0
|
||||
|
||||
readonly property int topContributorsCount: 20
|
||||
|
||||
Connections {
|
||||
target: GitHubService
|
||||
function onCachedAvatarsChanged() {
|
||||
root.avatarCacheVersion++;
|
||||
}
|
||||
}
|
||||
|
||||
spacing: Style.marginL
|
||||
|
||||
NHeader {
|
||||
description: I18n.trp("panels.about.contributors-description", root.contributors.length)
|
||||
enableDescriptionRichText: true
|
||||
}
|
||||
|
||||
// Top 20 contributors with full cards (avoids GridView shader crashes on Qt 6.8)
|
||||
Flow {
|
||||
id: topContributorsFlow
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
Repeater {
|
||||
model: Math.min(root.contributors.length, root.topContributorsCount)
|
||||
|
||||
delegate: Rectangle {
|
||||
width: Math.max(Math.round(topContributorsFlow.width / 2 - Style.marginM - 1), Math.round(Style.baseWidgetSize * 4))
|
||||
height: Math.round(Style.baseWidgetSize * 2.3)
|
||||
radius: Style.radiusM
|
||||
color: contributorArea.containsMouse ? Color.mHover : "transparent"
|
||||
border.width: 1
|
||||
border.color: contributorArea.containsMouse ? Color.mPrimary : Color.mOutline
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
// Avatar container with rectangular design (modern, no shader issues)
|
||||
Item {
|
||||
id: wrapper
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: Style.baseWidgetSize * 1.8
|
||||
Layout.preferredHeight: Style.baseWidgetSize * 1.8
|
||||
|
||||
property bool isRounded: false
|
||||
|
||||
// Background and image container
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
// Simple circular image (pre-rendered, no shaders)
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
// Depend on avatarCacheVersion to trigger re-evaluation
|
||||
var _ = root.avatarCacheVersion;
|
||||
// Try cached circular version first
|
||||
var username = root.contributors[index].login;
|
||||
var cached = GitHubService.getAvatarPath(username);
|
||||
if (cached) {
|
||||
wrapper.isRounded = true;
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Fall back to original avatar URL
|
||||
return root.contributors[index].avatar_url || "";
|
||||
}
|
||||
fillMode: Image.PreserveAspectFit // Fit since image is already circular with transparency
|
||||
mipmap: true
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: root.contributors[index].avatar_url !== undefined && root.contributors[index].avatar_url !== ""
|
||||
opacity: status === Image.Ready ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback icon
|
||||
NIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: !root.contributors[index].avatar_url || root.contributors[index].avatar_url === ""
|
||||
icon: "person"
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: wrapper.isRounded
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
radius: width * 0.5
|
||||
border.width: Style.borderM
|
||||
border.color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
|
||||
// Info column
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: root.contributors[index].login || "Unknown"
|
||||
font.weight: Style.fontWeightBold
|
||||
color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
pointSize: Style.fontSizeS
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NIcon {
|
||||
icon: "git-commit"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: `${(root.contributors[index].contributions || 0).toString()} commits`
|
||||
pointSize: Style.fontSizeXS
|
||||
color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hover indicator
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: "arrow-right"
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mPrimary
|
||||
opacity: contributorArea.containsMouse ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: contributorArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.contributors[index].html_url)
|
||||
Quickshell.execDetached(["xdg-open", root.contributors[index].html_url]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remaining contributors (simple text links)
|
||||
Flow {
|
||||
id: remainingContributorsFlow
|
||||
visible: root.contributors.length > root.topContributorsCount
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginL
|
||||
spacing: Style.marginS
|
||||
|
||||
Repeater {
|
||||
model: Math.max(0, root.contributors.length - root.topContributorsCount)
|
||||
|
||||
delegate: Rectangle {
|
||||
width: nameText.implicitWidth + Style.margin2M
|
||||
height: nameText.implicitHeight + Style.margin2S
|
||||
radius: Style.radiusS
|
||||
color: nameArea.containsMouse ? Color.mHover : "transparent"
|
||||
border.width: Style.borderS
|
||||
border.color: nameArea.containsMouse ? Color.mPrimary : Color.mOutline
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: nameText
|
||||
anchors.centerIn: parent
|
||||
text: root.contributors[index + root.topContributorsCount].login || "Unknown"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: nameArea.containsMouse ? Color.mOnHover : Color.mOnSurface
|
||||
font.weight: Style.fontWeightMedium
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nameArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.contributors[index + root.topContributorsCount].html_url)
|
||||
Quickshell.execDetached(["xdg-open", root.contributors[index + root.topContributorsCount].html_url]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var supporters: SupporterService.supporters
|
||||
property int avatarCacheVersion: 0
|
||||
|
||||
Connections {
|
||||
target: SupporterService
|
||||
function onCachedAvatarsChanged() {
|
||||
root.avatarCacheVersion++;
|
||||
}
|
||||
}
|
||||
|
||||
spacing: Style.marginL
|
||||
|
||||
NHeader {
|
||||
description: root.supporters.length === 0 ? I18n.tr("panels.about.supporters-loading") : I18n.trp("panels.about.supporters-desc", root.supporters.length)
|
||||
enableDescriptionRichText: true
|
||||
}
|
||||
|
||||
NButton {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
icon: "heart"
|
||||
text: I18n.tr("panels.about.become-supporter")
|
||||
outlined: true
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["xdg-open", "https://buymeacoffee.com/noctalia"]);
|
||||
ToastService.showNotice(I18n.tr("panels.about.support"), I18n.tr("toast.donation-opened"));
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
// Supporter cards
|
||||
Flow {
|
||||
id: supportersFlow
|
||||
visible: root.supporters.length > 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginL
|
||||
|
||||
Repeater {
|
||||
model: root.supporters.length
|
||||
|
||||
delegate: Rectangle {
|
||||
id: supporterCard
|
||||
|
||||
property bool hasGithub: !!root.supporters[index].github_username
|
||||
|
||||
width: Math.max(Math.round(supportersFlow.width / 2 - Style.marginL - 1), Math.round(Style.baseWidgetSize * 4.5))
|
||||
height: Math.round(Style.baseWidgetSize * 2.6)
|
||||
radius: Style.radiusM
|
||||
color: supporterArea.containsMouse && hasGithub ? Color.mHover : Qt.alpha(Color.mPrimary, 0.05)
|
||||
border.width: Style.borderM
|
||||
border.color: supporterArea.containsMouse && hasGithub ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.5)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
// Avatar with heart badge
|
||||
Item {
|
||||
id: avatarWrapper
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: Style.baseWidgetSize * 2.0
|
||||
Layout.preferredHeight: Style.baseWidgetSize * 2.0
|
||||
|
||||
property bool isRounded: false
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
visible: supporterCard.hasGithub
|
||||
source: {
|
||||
if (!supporterCard.hasGithub)
|
||||
return "";
|
||||
var _ = root.avatarCacheVersion;
|
||||
var username = root.supporters[index].github_username;
|
||||
var cached = SupporterService.getAvatarPath(username);
|
||||
if (cached) {
|
||||
avatarWrapper.isRounded = true;
|
||||
return cached;
|
||||
}
|
||||
return "https://github.com/" + username + ".png?size=256";
|
||||
}
|
||||
fillMode: Image.PreserveAspectFit
|
||||
mipmap: true
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
opacity: status === Image.Ready ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback logo for supporters without GitHub
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !supporterCard.hasGithub
|
||||
radius: width * 0.5
|
||||
color: Qt.alpha(Color.mPrimary, 0.15)
|
||||
|
||||
Image {
|
||||
anchors.centerIn: parent
|
||||
source: "../../../../../Assets/noctalia.svg"
|
||||
width: parent.width * 0.75
|
||||
height: width
|
||||
fillMode: Image.PreserveAspectFit
|
||||
mipmap: true
|
||||
smooth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: avatarWrapper.isRounded || !supporterCard.hasGithub
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
radius: width * 0.5
|
||||
border.width: Style.borderM
|
||||
border.color: Color.mPrimary
|
||||
}
|
||||
|
||||
// Heart badge
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -2
|
||||
width: Style.fontSizeM + Style.marginS
|
||||
height: width
|
||||
radius: width * 0.5
|
||||
color: Color.mPrimary
|
||||
|
||||
NIcon {
|
||||
anchors.centerIn: parent
|
||||
icon: "heart-filled"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info column
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: root.supporters[index].name || root.supporters[index].github_username || "Unknown"
|
||||
font.weight: Style.fontWeightBold
|
||||
color: supporterArea.containsMouse && supporterCard.hasGithub ? Color.mOnHover : Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
pointSize: Style.fontSizeS
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.about.supporter-badge")
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mPrimary
|
||||
font.weight: Style.fontWeightMedium
|
||||
}
|
||||
}
|
||||
|
||||
// Hover indicator (only for supporters with GitHub)
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: "arrow-right"
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mPrimary
|
||||
visible: supporterCard.hasGithub
|
||||
opacity: supporterArea.containsMouse ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: supporterArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: supporterCard.hasGithub ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
var username = root.supporters[index].github_username;
|
||||
if (username) {
|
||||
Quickshell.execDetached(["xdg-open", "https://github.com/" + username]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+985
@@ -0,0 +1,985 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
opacity: 0
|
||||
|
||||
onSystemInfoLoadingChanged: {
|
||||
if (!systemInfoLoading)
|
||||
tabAppearAnim.start();
|
||||
}
|
||||
|
||||
NumberAnimation on opacity {
|
||||
id: tabAppearAnim
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
running: false
|
||||
}
|
||||
|
||||
property string latestVersion: GitHubService.latestVersion
|
||||
property string currentVersion: UpdateService.currentVersion
|
||||
property string commitInfo: ""
|
||||
property string qsVersion: ""
|
||||
property string qsRevision: ""
|
||||
|
||||
readonly property bool isGitVersion: root.currentVersion.endsWith("-git")
|
||||
readonly property int gigaB: (1024 * 1024 * 1024)
|
||||
readonly property int gigaD: (1000 * 1000 * 1000)
|
||||
|
||||
// Update status: compare versions
|
||||
readonly property bool updateAvailable: {
|
||||
if (!root.latestVersion || !root.currentVersion || root.latestVersion === I18n.tr("common.unknown"))
|
||||
return false;
|
||||
return UpdateService.compareVersions(root.latestVersion, root.currentVersion) > 0 && !root.isGitVersion;
|
||||
}
|
||||
readonly property bool isUpToDate: {
|
||||
if (!root.latestVersion || !root.currentVersion || root.latestVersion === I18n.tr("common.unknown"))
|
||||
return false;
|
||||
return UpdateService.compareVersions(root.latestVersion, root.currentVersion) <= 0;
|
||||
}
|
||||
|
||||
readonly property bool qsUpdateAvailable: {
|
||||
if (!GitHubService.latestQSVersion || !root.qsVersion || GitHubService.latestQSVersion === I18n.tr("common.unknown"))
|
||||
return false;
|
||||
return UpdateService.compareVersions(GitHubService.latestQSVersion, root.qsVersion) > 0;
|
||||
}
|
||||
|
||||
readonly property bool qsIsUpToDate: {
|
||||
if (!GitHubService.latestQSVersion || !root.qsVersion || GitHubService.latestQSVersion === I18n.tr("common.unknown"))
|
||||
return false;
|
||||
return UpdateService.compareVersions(GitHubService.latestQSVersion, root.qsVersion) <= 0;
|
||||
}
|
||||
|
||||
// System info properties
|
||||
property var systemInfo: null
|
||||
property bool systemInfoLoading: true
|
||||
property bool systemInfoAvailable: true
|
||||
|
||||
spacing: Style.marginL
|
||||
|
||||
function getModule(type) {
|
||||
if (!root.systemInfo)
|
||||
return null;
|
||||
return root.systemInfo.find(m => m.type === type);
|
||||
}
|
||||
|
||||
function getMonitorsText(separator) {
|
||||
const sep = separator || "\n";
|
||||
const screens = Quickshell.screens || [];
|
||||
const scales = CompositorService.displayScales || {};
|
||||
let lines = [];
|
||||
for (let i = 0; i < screens.length; i++) {
|
||||
const screen = screens[i];
|
||||
const name = screen.name || "Unknown";
|
||||
const scaleData = scales[name];
|
||||
const scaleValue = (typeof scaleData === "object" && scaleData !== null) ? (scaleData.scale || 1.0) : (scaleData || 1.0);
|
||||
lines.push(name + ": " + screen.width + "x" + screen.height + " @ " + scaleValue + "x");
|
||||
}
|
||||
return lines.join(sep);
|
||||
}
|
||||
|
||||
function getTelemetryPayload() {
|
||||
const screens = Quickshell.screens || [];
|
||||
const scales = CompositorService.displayScales || {};
|
||||
const monitors = [];
|
||||
for (let i = 0; i < screens.length; i++) {
|
||||
const screen = screens[i];
|
||||
const name = screen.name || "Unknown";
|
||||
const scaleData = scales[name];
|
||||
const scaleValue = (typeof scaleData === "object" && scaleData !== null) ? (scaleData.scale || 1.0) : (scaleData || 1.0);
|
||||
monitors.push({
|
||||
width: screen.width || 0,
|
||||
height: screen.height || 0,
|
||||
scale: scaleValue
|
||||
});
|
||||
}
|
||||
return {
|
||||
instanceId: TelemetryService.getInstanceId(),
|
||||
version: UpdateService.currentVersion,
|
||||
compositor: TelemetryService.getCompositorType(),
|
||||
os: HostService.osPretty || "Unknown",
|
||||
ramGb: Math.round((root.getModule("Memory")?.result?.total || 0) / root.gigaB),
|
||||
monitors: monitors,
|
||||
ui: {
|
||||
scaleRatio: Settings.data.general.scaleRatio,
|
||||
fontDefaultScale: Settings.data.ui.fontDefaultScale,
|
||||
fontFixedScale: Settings.data.ui.fontFixedScale
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function copyTelemetryData() {
|
||||
const payload = getTelemetryPayload();
|
||||
const json = JSON.stringify(payload, null, 2);
|
||||
Quickshell.execDetached(["wl-copy", json]);
|
||||
ToastService.showNotice(I18n.tr("panels.about.telemetry-title"), I18n.tr("panels.about.telemetry-data-copied"));
|
||||
}
|
||||
|
||||
function copyInfoToClipboard() {
|
||||
let info = "Noctalia Shell: " + root.currentVersion;
|
||||
if (root.isGitVersion && root.commitInfo) {
|
||||
info += " (" + root.commitInfo + ")";
|
||||
}
|
||||
info += "\n";
|
||||
|
||||
if (root.qsVersion) {
|
||||
let qsV = root.qsVersion.startsWith("v") ? root.qsVersion : "v" + root.qsVersion;
|
||||
info += "Noctalia QS: " + qsV;
|
||||
if (root.qsRevision) {
|
||||
info += " (" + root.qsRevision + ")";
|
||||
}
|
||||
info += "\n";
|
||||
}
|
||||
|
||||
info += "\nSystem Information\n";
|
||||
info += "==================\n";
|
||||
if (root.systemInfo) {
|
||||
const os = root.getModule("OS");
|
||||
const kernel = root.getModule("Kernel");
|
||||
const title = root.getModule("Title");
|
||||
const product = root.getModule("Host");
|
||||
const board = root.getModule("Board");
|
||||
const cpu = root.getModule("CPU");
|
||||
const gpu = root.getModule("GPU");
|
||||
const mem = root.getModule("Memory");
|
||||
const wm = root.getModule("WM");
|
||||
info += "OS: " + (os?.result?.prettyName || "N/A") + "\n";
|
||||
info += "Kernel: " + (kernel?.result?.release || "N/A") + "\n";
|
||||
info += "Host: " + (title?.result?.hostName || "N/A") + "\n";
|
||||
info += "Product: " + (product?.result?.name || "N/A") + "\n";
|
||||
info += "Board: " + (board?.result?.name || "N/A") + "\n";
|
||||
info += "CPU: " + (cpu?.result?.cpu || "N/A") + "\n";
|
||||
if (gpu?.result && Array.isArray(gpu.result) && gpu.result.length > 0) {
|
||||
info += "GPU: " + gpu.result.map(g => g.name || "Unknown").join(", ") + "\n";
|
||||
}
|
||||
if (mem?.result) {
|
||||
info += "Memory: " + (mem.result.total / root.gigaB).toFixed(1) + " GB \n";
|
||||
}
|
||||
if (wm?.result) {
|
||||
info += "WM: " + (wm.result.prettyName || wm.result.processName || "N/A") + "\n";
|
||||
}
|
||||
}
|
||||
const monitors = getMonitorsText("\n").split("\n");
|
||||
for (const mon of monitors) {
|
||||
info += "Monitor: " + mon + "\n";
|
||||
}
|
||||
info += "\nSettings\n";
|
||||
info += "========\n";
|
||||
info += "UI Scale: " + Settings.data.general.scaleRatio + "\n";
|
||||
info += "Default Font: " + (Settings.data.ui.fontDefault || "default") + " @ " + Settings.data.ui.fontDefaultScale + "x\n";
|
||||
info += "Fixed Font: " + (Settings.data.ui.fontFixed || "default") + " @ " + Settings.data.ui.fontFixedScale + "x\n";
|
||||
Quickshell.execDetached(["wl-copy", info]);
|
||||
ToastService.showNotice(I18n.tr("panels.about.title"), I18n.tr("panels.about.info-copied"));
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Check if fastfetch is available before trying to run it
|
||||
checkFastfetchProcess.running = true;
|
||||
qsVersionProcess.running = true;
|
||||
|
||||
Logger.d("VersionSubTab", "Current version:", root.currentVersion);
|
||||
Logger.d("VersionSubTab", "Is git version:", root.isGitVersion);
|
||||
// Only fetch commit info for -git versions
|
||||
if (root.isGitVersion) {
|
||||
// On NixOS, extract commit hash from the store path first
|
||||
if (HostService.isNixOS) {
|
||||
var shellDir = Quickshell.shellDir || "";
|
||||
Logger.d("VersionSubTab", "Component.onCompleted - NixOS detected, shellDir:", shellDir);
|
||||
if (shellDir) {
|
||||
// Extract commit hash from path like: /nix/store/...-noctalia-shell-2025-11-30_225e6d3/share/noctalia-shell
|
||||
// Pattern matches: noctalia-shell-YYYY-MM-DD_<commit_hash>
|
||||
var match = shellDir.match(/noctalia-shell-\d{4}-\d{2}-\d{2}_([0-9a-f]{7,})/i);
|
||||
if (match && match[1]) {
|
||||
// Use first 7 characters of the commit hash
|
||||
root.commitInfo = match[1].substring(0, 7);
|
||||
Logger.d("VersionSubTab", "Component.onCompleted - Extracted commit from NixOS path:", root.commitInfo);
|
||||
return;
|
||||
} else {
|
||||
Logger.d("VersionSubTab", "Component.onCompleted - Could not extract commit from NixOS path, trying fallback");
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchGitCommit();
|
||||
}
|
||||
}
|
||||
|
||||
function fetchGitCommit() {
|
||||
var shellDir = Quickshell.shellDir || "";
|
||||
Logger.d("VersionSubTab", "fetchGitCommit - shellDir:", shellDir);
|
||||
if (!shellDir) {
|
||||
Logger.d("VersionSubTab", "fetchGitCommit - Cannot determine shell directory, skipping git commit fetch");
|
||||
return;
|
||||
}
|
||||
|
||||
gitProcess.workingDirectory = shellDir;
|
||||
gitProcess.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gitProcess
|
||||
command: ["git", "rev-parse", "--short", "HEAD"]
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
Logger.d("VersionSubTab", "gitProcess - Process exited with code:", exitCode);
|
||||
if (exitCode === 0) {
|
||||
var gitOutput = stdout.text.trim();
|
||||
Logger.d("VersionSubTab", "gitProcess - gitOutput:", gitOutput);
|
||||
if (gitOutput) {
|
||||
root.commitInfo = gitOutput;
|
||||
Logger.d("VersionSubTab", "gitProcess - Set commitInfo to:", root.commitInfo);
|
||||
}
|
||||
} else {
|
||||
Logger.d("VersionSubTab", "gitProcess - Git command failed. Exit code:", exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: qsVersionProcess
|
||||
command: ["qs", "--version"]
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
var output = stdout.text.trim();
|
||||
// Format (old): "noctalia-qs 0.3.0, revision abc12345, distributed by: ..."
|
||||
// Format (new): "noctalia-qs 0.0.9 (revision b602b69c81d96a1d7c645328feb7b1e1d4b7b7a4, distributed by Unset)"
|
||||
// Only set if this is actually noctalia-qs; leave empty for upstream quickshell
|
||||
var match = output.match(/noctalia-qs\s+(\S+?)[\s,(]+revision\s*([0-9a-f]*)/i);
|
||||
if (match) {
|
||||
root.qsVersion = match[1];
|
||||
root.qsRevision = match[2] ? match[2].substring(0, 9) : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
// Check if fastfetch is available before attempting to run it
|
||||
Process {
|
||||
id: checkFastfetchProcess
|
||||
command: ["sh", "-c", "command -v fastfetch"]
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
// fastfetch is available, run it
|
||||
Logger.d("VersionSubTab", "fastfetch found, running it");
|
||||
fastfetchProcess.running = true;
|
||||
} else {
|
||||
// fastfetch not found, show error state immediately
|
||||
Logger.w("VersionSubTab", "fastfetch not found");
|
||||
root.systemInfoLoading = false;
|
||||
root.systemInfoAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fastfetchProcess
|
||||
command: ["fastfetch", "--format", "json", "--config", Quickshell.shellDir + "/Assets/Services/fastfetch/system-info.jsonc"]
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
root.systemInfoLoading = false;
|
||||
if (exitCode === 0) {
|
||||
try {
|
||||
root.systemInfo = JSON.parse(stdout.text);
|
||||
root.systemInfoAvailable = true;
|
||||
} catch (e) {
|
||||
Logger.w("VersionSubTab", "Failed to parse fastfetch JSON: " + e);
|
||||
root.systemInfoAvailable = false;
|
||||
}
|
||||
} else {
|
||||
root.systemInfoAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginXL
|
||||
|
||||
// Noctalia logo
|
||||
Image {
|
||||
source: "../../../../../Assets/noctalia.svg"
|
||||
width: 96 * Style.uiScaleRatio
|
||||
height: width
|
||||
fillMode: Image.PreserveAspectFit
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
mipmap: true
|
||||
smooth: true
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
rotation: Settings.isDebug ? 180 : 0
|
||||
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
|
||||
property int debugTapCount: 0
|
||||
|
||||
Timer {
|
||||
id: debugTapTimer
|
||||
interval: 5000
|
||||
onTriggered: parent.debugTapCount = 0
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
if (parent.debugTapCount === 0) {
|
||||
debugTapTimer.restart();
|
||||
}
|
||||
parent.debugTapCount++;
|
||||
if (parent.debugTapCount >= 8) {
|
||||
parent.debugTapCount = 0;
|
||||
debugTapTimer.stop();
|
||||
Settings.isDebug = !Settings.isDebug;
|
||||
if (Settings.isDebug) {
|
||||
ToastService.showNotice("Debug", I18n.tr("panels.about.debug-enabled"));
|
||||
} else {
|
||||
ToastService.showNotice("Debug", I18n.tr("panels.about.debug-disabled"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
NHeader {
|
||||
label: "Noctalia Shell"
|
||||
}
|
||||
|
||||
// Versions
|
||||
GridLayout {
|
||||
columns: 2
|
||||
rowSpacing: Style.marginXS
|
||||
columnSpacing: Style.marginM
|
||||
|
||||
// Installed Version (Shell)
|
||||
NText {
|
||||
text: "Noctalia Shell:"
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: root.currentVersion
|
||||
color: Color.mOnSurface
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
// Git commit in parentheses
|
||||
NText {
|
||||
id: commitText
|
||||
visible: root.isGitVersion
|
||||
text: "(" + (root.commitInfo || I18n.tr("common.loading")) + ")"
|
||||
color: commitMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
pointSize: Style.fontSizeXS
|
||||
font.underline: commitMouseArea.containsMouse && root.commitInfo
|
||||
|
||||
MouseArea {
|
||||
id: commitMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: root.commitInfo ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onEntered: {
|
||||
if (root.commitInfo) {
|
||||
TooltipService.show(commitText, I18n.tr("panels.about.view-commit"));
|
||||
}
|
||||
}
|
||||
onExited: TooltipService.hide()
|
||||
onClicked: {
|
||||
if (root.commitInfo) {
|
||||
Quickshell.execDetached(["xdg-open", "https://github.com/noctalia-dev/noctalia-shell/commit/" + root.commitInfo]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update status indicator
|
||||
NIcon {
|
||||
id: upToDateIcon
|
||||
visible: root.isUpToDate
|
||||
icon: "circle-check"
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mPrimary
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(upToDateIcon, I18n.tr("panels.about.up-to-date"))
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
id: updateAvailableIcon
|
||||
visible: root.updateAvailable
|
||||
icon: "arrow-up-circle"
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mPrimary
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(updateAvailableIcon, I18n.tr("panels.about.update-available"))
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Latest Version (Shell)
|
||||
NText {
|
||||
visible: root.updateAvailable
|
||||
text: I18n.tr("panels.about.noctalia-available")
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: root.updateAvailable
|
||||
text: root.latestVersion
|
||||
color: Color.mOnSurface
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
// Divider-like spacing
|
||||
Item {
|
||||
visible: root.qsUpdateAvailable || root.updateAvailable
|
||||
Layout.columnSpan: 2
|
||||
Layout.preferredHeight: Style.marginXS
|
||||
}
|
||||
|
||||
// Quickshell Version
|
||||
NText {
|
||||
visible: root.qsVersion !== ""
|
||||
text: "Noctalia QS:"
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: root.qsVersion !== ""
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: root.qsVersion.startsWith("v") ? root.qsVersion : "v" + root.qsVersion
|
||||
color: Color.mOnSurface
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
// Git revision in parentheses
|
||||
NText {
|
||||
id: qsRevisionText
|
||||
visible: root.qsRevision !== ""
|
||||
text: "(" + root.qsRevision + ")"
|
||||
color: qsRevisionMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
pointSize: Style.fontSizeXS
|
||||
font.underline: qsRevisionMouseArea.containsMouse
|
||||
|
||||
MouseArea {
|
||||
id: qsRevisionMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: TooltipService.show(qsRevisionText, I18n.tr("panels.about.view-commit"))
|
||||
onExited: TooltipService.hide()
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["xdg-open", "https://github.com/noctalia-dev/noctalia-qs/commit/" + root.qsRevision]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update status indicator
|
||||
NIcon {
|
||||
id: qsUpToDateIcon
|
||||
visible: root.qsIsUpToDate
|
||||
icon: "circle-check"
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mPrimary
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(qsUpToDateIcon, I18n.tr("panels.about.up-to-date"))
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
id: qsUpdateAvailableIcon
|
||||
visible: root.qsUpdateAvailable
|
||||
icon: "arrow-up-circle"
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mPrimary
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: TooltipService.show(qsUpdateAvailableIcon, I18n.tr("panels.about.update-available"))
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Latest Quickshell Version
|
||||
NText {
|
||||
visible: root.qsUpdateAvailable
|
||||
text: I18n.tr("panels.about.noctalia-available")
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: root.qsUpdateAvailable
|
||||
text: GitHubService.latestQSVersion
|
||||
color: Color.mOnSurface
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: actionsGrid
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Style.marginM
|
||||
Layout.bottomMargin: Style.marginM
|
||||
rowSpacing: Style.marginM
|
||||
columnSpacing: Style.marginM
|
||||
|
||||
columns: (changelogBtn.implicitWidth + copyBtn.implicitWidth + supportBtn.implicitWidth + 2 * columnSpacing) < root.width ? 3 : 1
|
||||
|
||||
NButton {
|
||||
id: changelogBtn
|
||||
icon: "sparkles"
|
||||
text: I18n.tr("panels.about.changelog")
|
||||
outlined: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: {
|
||||
var screen = PanelService.openedPanel?.screen || SettingsPanelService.settingsWindow?.screen || PanelService.findScreenForPanels();
|
||||
SettingsPanelService.close(screen);
|
||||
UpdateService.viewChangelog(screen);
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
id: copyBtn
|
||||
icon: "copy"
|
||||
text: I18n.tr("panels.about.copy-info")
|
||||
outlined: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: root.copyInfoToClipboard()
|
||||
}
|
||||
|
||||
NButton {
|
||||
id: supportBtn
|
||||
icon: "heart"
|
||||
text: I18n.tr("panels.about.support")
|
||||
outlined: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["xdg-open", "https://buymeacoffee.com/noctalia"]);
|
||||
ToastService.showNotice(I18n.tr("panels.about.support"), I18n.tr("toast.donation-opened"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.about.changelog-on-startup")
|
||||
description: I18n.tr("panels.about.changelog-on-startup-desc")
|
||||
checked: Settings.data.general.showChangelogOnStartup
|
||||
onToggled: checked => Settings.data.general.showChangelogOnStartup = checked
|
||||
}
|
||||
|
||||
// System Information Section
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("panels.about.system-title")
|
||||
}
|
||||
|
||||
// Error state (fastfetch not installed)
|
||||
ColumnLayout {
|
||||
visible: !root.systemInfoAvailable
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-not-installed")
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-install-hint")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: Style.fontSizeXS
|
||||
}
|
||||
}
|
||||
|
||||
// System info grid
|
||||
GridLayout {
|
||||
id: sysInfo
|
||||
readonly property real textSize: Style.fontSizeS
|
||||
|
||||
visible: root.systemInfoAvailable && root.systemInfo
|
||||
Layout.fillWidth: true
|
||||
columns: 2
|
||||
rowSpacing: Style.marginXS
|
||||
columnSpacing: Style.marginM
|
||||
|
||||
// OS
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-os")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const os = root.getModule("OS");
|
||||
return os?.result?.prettyName || "N/A";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Kernel
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-kernel")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const kernel = root.getModule("Kernel");
|
||||
return kernel?.result?.release || "N/A";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Host
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-host")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const title = root.getModule("Title");
|
||||
return title?.result?.hostName || "N/A";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Product name
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-product")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const title = root.getModule("Host");
|
||||
return title?.result?.name || "N/A";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Board name
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-board")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const title = root.getModule("Board");
|
||||
return title?.result?.name || "N/A";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Uptime
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-uptime")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const value = root.getModule("Uptime")?.result?.uptime;
|
||||
return value ? Time.formatVagueHumanReadableDuration(value / 1000) : "-";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// CPU
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-cpu")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const cpu = root.getModule("CPU");
|
||||
if (!cpu?.result)
|
||||
return "N/A";
|
||||
let cpuText = cpu.result.cpu || "N/A";
|
||||
const cores = cpu.result.cores;
|
||||
if (cores?.logical) {
|
||||
cpuText += " (" + cores.logical + " threads)";
|
||||
}
|
||||
return cpuText;
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// GPU
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-gpu")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const gpu = root.getModule("GPU");
|
||||
if (!gpu?.result || !Array.isArray(gpu.result) || gpu.result.length === 0)
|
||||
return "N/A";
|
||||
return gpu.result.map(g => g.name || "Unknown").join(", ");
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Memory
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-memory")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const mem = root.getModule("Memory");
|
||||
if (!mem?.result)
|
||||
return "N/A";
|
||||
const used = (mem.result.used / root.gigaB).toFixed(1);
|
||||
const total = (mem.result.total / root.gigaB).toFixed(1);
|
||||
return used + " GiB / " + total + " GiB";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Disk
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-disk")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const disk = root.getModule("Disk");
|
||||
if (!disk?.result || !Array.isArray(disk.result) || disk.result.length === 0)
|
||||
return "N/A";
|
||||
const rootDisk = disk.result.find(d => d.mountpoint === "/");
|
||||
if (!rootDisk?.bytes)
|
||||
return "N/A";
|
||||
const used = (rootDisk.bytes.used / root.gigaD).toFixed(1);
|
||||
const total = (rootDisk.bytes.total / root.gigaD).toFixed(1);
|
||||
return used + " GB / " + total + " GB" + " (" + rootDisk.filesystem + ")";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// WM
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-wm")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const wm = root.getModule("WM");
|
||||
if (!wm?.result)
|
||||
return "N/A";
|
||||
let wmText = wm.result.prettyName || wm.result.processName || "N/A";
|
||||
if (wm.result.protocolName) {
|
||||
wmText += " (" + wm.result.protocolName + ")";
|
||||
}
|
||||
return wmText;
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Packages
|
||||
NText {
|
||||
text: I18n.tr("panels.about.system-packages")
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: sysInfo.textSize
|
||||
}
|
||||
NText {
|
||||
text: {
|
||||
const pkg = root.getModule("Packages");
|
||||
if (!pkg?.result)
|
||||
return "N/A";
|
||||
const result = pkg.result;
|
||||
if (result.all) {
|
||||
const managers = [];
|
||||
if (result.rpm > 0)
|
||||
managers.push("rpm: " + result.rpm);
|
||||
if (result.pacman > 0)
|
||||
managers.push("pacman: " + result.pacman);
|
||||
if (result.dpkg > 0)
|
||||
managers.push("dpkg: " + result.dpkg);
|
||||
if (result.flatpakSystem > 0 || result.flatpakUser > 0) {
|
||||
const flatpak = (result.flatpakSystem || 0) + (result.flatpakUser || 0);
|
||||
managers.push("flatpak: " + flatpak);
|
||||
}
|
||||
if (result.snap > 0)
|
||||
managers.push("snap: " + result.snap);
|
||||
if (result.nixSystem > 0 || result.nixUser > 0 || result.nixDefault > 0) {
|
||||
const nix = (result.nixSystem || 0) + (result.nixUser || 0) + (result.nixDefault || 0);
|
||||
managers.push("nix: " + nix);
|
||||
}
|
||||
if (result.brew > 0)
|
||||
managers.push("brew: " + result.brew);
|
||||
if (managers.length > 0) {
|
||||
return result.all + " (" + managers.join(", ") + ")";
|
||||
}
|
||||
return result.all.toString();
|
||||
}
|
||||
return "N/A";
|
||||
}
|
||||
color: Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
// Monitors (2 items per screen: label + value)
|
||||
Repeater {
|
||||
model: Quickshell.screens.length * 2
|
||||
|
||||
NText {
|
||||
readonly property int screenIndex: Math.floor(index / 2)
|
||||
readonly property bool isLabel: index % 2 === 0
|
||||
readonly property var screen: Quickshell.screens[screenIndex]
|
||||
|
||||
text: {
|
||||
if (isLabel)
|
||||
return I18n.tr("panels.about.system-monitor");
|
||||
const name = screen?.name || "Unknown";
|
||||
const scales = CompositorService.displayScales || {};
|
||||
const scaleData = scales[name];
|
||||
const scaleValue = (typeof scaleData === "object" && scaleData !== null) ? (scaleData.scale || 1.0) : (scaleData || 1.0);
|
||||
return name + ": " + (screen?.width || 0) + "x" + (screen?.height || 0) + " @ " + scaleValue + "x";
|
||||
}
|
||||
color: isLabel ? Color.mOnSurfaceVariant : Color.mOnSurface
|
||||
pointSize: sysInfo.textSize
|
||||
Layout.fillWidth: !isLabel
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Telemetry Section
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginL
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("panels.about.telemetry-title")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.about.telemetry-enabled")
|
||||
description: I18n.tr("panels.about.telemetry-desc")
|
||||
checked: Settings.data.general.telemetryEnabled
|
||||
onToggled: checked => Settings.data.general.telemetryEnabled = checked
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM
|
||||
|
||||
NButton {
|
||||
icon: "eye"
|
||||
text: I18n.tr("panels.about.telemetry-show-data")
|
||||
outlined: true
|
||||
onClicked: root.copyTelemetryData()
|
||||
}
|
||||
|
||||
NButton {
|
||||
icon: "shield-lock"
|
||||
text: I18n.tr("panels.about.privacy-policy")
|
||||
outlined: true
|
||||
onClicked: Quickshell.execDetached(["xdg-open", "https://noctalia.dev/privacy"])
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.volumes")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.devices")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.media")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.visualizer")
|
||||
tabIndex: 3
|
||||
checked: subTabBar.currentIndex === 3
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
VolumesSubTab {}
|
||||
DevicesSubTab {}
|
||||
MediaSubTab {}
|
||||
VisualizerSubTab {}
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Services.Media
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Output Devices
|
||||
ButtonGroup {
|
||||
id: sinks
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXS
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginL
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.audio.devices-output-device-label")
|
||||
description: I18n.tr("panels.audio.devices-output-device-description")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: AudioService.sinks
|
||||
NRadioButton {
|
||||
ButtonGroup.group: sinks
|
||||
required property PwNode modelData
|
||||
text: modelData.description
|
||||
checked: AudioService.sink?.id === modelData.id
|
||||
onClicked: {
|
||||
AudioService.setAudioSink(modelData);
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Input Devices
|
||||
ButtonGroup {
|
||||
id: sources
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.audio.devices-input-device-label")
|
||||
description: I18n.tr("panels.audio.devices-input-device-description")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: AudioService.sources
|
||||
NRadioButton {
|
||||
ButtonGroup.group: sources
|
||||
required property PwNode modelData
|
||||
text: modelData.description
|
||||
checked: AudioService.source?.id === modelData.id
|
||||
onClicked: AudioService.setAudioSource(modelData)
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Media
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Preferred player
|
||||
NTextInput {
|
||||
label: I18n.tr("panels.audio.media-primary-player-label")
|
||||
description: I18n.tr("panels.audio.media-primary-player-description")
|
||||
placeholderText: I18n.tr("panels.audio.media-primary-player-placeholder")
|
||||
text: Settings.data.audio.preferredPlayer
|
||||
defaultValue: Settings.getDefaultValue("audio.preferredPlayer")
|
||||
onTextChanged: {
|
||||
Settings.data.audio.preferredPlayer = text;
|
||||
MediaService.updateCurrentPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
// Blacklist editor
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NTextInputButton {
|
||||
id: blacklistInput
|
||||
label: I18n.tr("panels.audio.media-excluded-player-label")
|
||||
description: I18n.tr("panels.audio.media-excluded-player-description")
|
||||
placeholderText: I18n.tr("panels.audio.media-excluded-player-placeholder")
|
||||
buttonIcon: "add"
|
||||
Layout.fillWidth: true
|
||||
onButtonClicked: {
|
||||
const val = (blacklistInput.text || "").trim();
|
||||
if (val !== "") {
|
||||
const arr = (Settings.data.audio.mprisBlacklist || []);
|
||||
if (!arr.find(x => String(x).toLowerCase() === val.toLowerCase())) {
|
||||
Settings.data.audio.mprisBlacklist = [...arr, val];
|
||||
blacklistInput.text = "";
|
||||
MediaService.updateCurrentPlayer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Current blacklist entries
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginS
|
||||
spacing: Style.marginS
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.audio.mprisBlacklist
|
||||
delegate: Rectangle {
|
||||
required property string modelData
|
||||
property real pad: Style.marginS
|
||||
color: Qt.alpha(Color.mOnSurface, 0.125)
|
||||
border.color: Qt.alpha(Color.mOnSurface, Style.opacityLight)
|
||||
border.width: Style.borderS
|
||||
|
||||
RowLayout {
|
||||
id: chipRow
|
||||
spacing: Style.marginXS
|
||||
anchors.fill: parent
|
||||
anchors.margins: pad
|
||||
|
||||
NText {
|
||||
text: modelData
|
||||
color: Color.mOnSurface
|
||||
pointSize: Style.fontSizeS
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: Style.marginS
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: Style.marginXS
|
||||
onClicked: {
|
||||
const arr = (Settings.data.audio.mprisBlacklist || []);
|
||||
const idx = arr.findIndex(x => String(x) === modelData);
|
||||
if (idx >= 0) {
|
||||
arr.splice(idx, 1);
|
||||
Settings.data.audio.mprisBlacklist = arr;
|
||||
MediaService.updateCurrentPlayer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitWidth: chipRow.implicitWidth + pad * 2
|
||||
implicitHeight: Math.max(chipRow.implicitHeight + pad * 2, Style.baseWidgetSize * 0.8)
|
||||
radius: Style.radiusM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("panels.audio.visualizer-type-label")
|
||||
description: I18n.tr("panels.audio.visualizer-type-description")
|
||||
model: [
|
||||
{
|
||||
"key": "none",
|
||||
"name": I18n.tr("common.none")
|
||||
},
|
||||
{
|
||||
"key": "linear",
|
||||
"name": I18n.tr("options.visualizer-types.linear")
|
||||
},
|
||||
{
|
||||
"key": "mirrored",
|
||||
"name": I18n.tr("options.visualizer-types.mirrored")
|
||||
},
|
||||
{
|
||||
"key": "wave",
|
||||
"name": I18n.tr("options.visualizer-types.wave")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.audio.visualizerType
|
||||
defaultValue: Settings.getDefaultValue("audio.visualizerType")
|
||||
onSelected: key => Settings.data.audio.visualizerType = key
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.audio.spectrum-mirrored-label")
|
||||
description: I18n.tr("panels.audio.spectrum-mirrored-description")
|
||||
checked: Settings.data.audio.spectrumMirrored
|
||||
defaultValue: Settings.getDefaultValue("audio.spectrumMirrored")
|
||||
onToggled: Settings.data.audio.spectrumMirrored = checked
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("panels.audio.media-frame-rate-label")
|
||||
description: I18n.tr("panels.audio.media-frame-rate-description")
|
||||
model: [
|
||||
{
|
||||
"key": "30",
|
||||
"name": I18n.tr("options.frame-rates-fps", {
|
||||
"fps": "30"
|
||||
})
|
||||
},
|
||||
{
|
||||
"key": "60",
|
||||
"name": I18n.tr("options.frame-rates-fps", {
|
||||
"fps": "60"
|
||||
})
|
||||
},
|
||||
{
|
||||
"key": "100",
|
||||
"name": I18n.tr("options.frame-rates-fps", {
|
||||
"fps": "100"
|
||||
})
|
||||
},
|
||||
{
|
||||
"key": "120",
|
||||
"name": I18n.tr("options.frame-rates-fps", {
|
||||
"fps": "120"
|
||||
})
|
||||
},
|
||||
{
|
||||
"key": "144",
|
||||
"name": I18n.tr("options.frame-rates-fps", {
|
||||
"fps": "144"
|
||||
})
|
||||
},
|
||||
{
|
||||
"key": "165",
|
||||
"name": I18n.tr("options.frame-rates-fps", {
|
||||
"fps": "165"
|
||||
})
|
||||
},
|
||||
{
|
||||
"key": "240",
|
||||
"name": I18n.tr("options.frame-rates-fps", {
|
||||
"fps": "240"
|
||||
})
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.audio.spectrumFrameRate
|
||||
defaultValue: Settings.getDefaultValue("audio.spectrumFrameRate")
|
||||
onSelected: key => Settings.data.audio.spectrumFrameRate = key
|
||||
}
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Media
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property real localVolume: AudioService.volume
|
||||
|
||||
Connections {
|
||||
target: AudioService
|
||||
function onSinkChanged() {
|
||||
localVolume = AudioService.volume;
|
||||
}
|
||||
function onVolumeChanged() {
|
||||
localVolume = AudioService.volume;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
|
||||
function onVolumeChanged() {
|
||||
localVolume = AudioService.volume;
|
||||
}
|
||||
}
|
||||
|
||||
// Output Volume
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.osd.types-volume-label")
|
||||
description: I18n.tr("panels.audio.volumes-output-volume-description")
|
||||
from: 0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: localVolume
|
||||
stepSize: 0.01
|
||||
text: Math.round(AudioService.volume * 100) + "%"
|
||||
onMoved: value => localVolume = value
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 100
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (!AudioService.isSwitchingSink && Math.abs(localVolume - AudioService.volume) >= 0.01) {
|
||||
AudioService.setVolume(localVolume);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mute Toggle
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.audio.volumes-mute-output-label")
|
||||
description: I18n.tr("panels.audio.volumes-mute-output-description")
|
||||
checked: AudioService.muted
|
||||
onToggled: checked => AudioService.setOutputMuted(checked)
|
||||
}
|
||||
}
|
||||
|
||||
// Volume Feedback sound Toggle
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.audio.volumes-volume-feedback-label")
|
||||
description: I18n.tr("panels.audio.volumes-volume-feedback-description")
|
||||
checked: Settings.data.audio.volumeFeedback
|
||||
defaultValue: Settings.getDefaultValue("audio.volumeFeedback")
|
||||
onToggled: checked => Settings.data.audio.volumeFeedback = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
enabled: SoundService.multimediaAvailable && Settings.data.audio.volumeFeedback
|
||||
spacing: Style.marginXXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.audio.volumes-feedback-sound-file-label")
|
||||
description: I18n.tr("panels.audio.volumes-feedback-sound-file-description")
|
||||
}
|
||||
|
||||
NTextInputButton {
|
||||
enabled: parent.enabled
|
||||
Layout.fillWidth: true
|
||||
placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder")
|
||||
text: Settings.data.audio.volumeFeedbackSoundFile ?? ""
|
||||
buttonIcon: "folder-open"
|
||||
buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file")
|
||||
onInputTextChanged: text => Settings.data.audio.volumeFeedbackSoundFile = text
|
||||
onButtonClicked: volumeFeedbackFilePicker.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Input Volume
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.osd.types-input-volume-label")
|
||||
description: I18n.tr("panels.audio.volumes-input-volume-description")
|
||||
from: 0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: AudioService.inputVolume
|
||||
stepSize: 0.01
|
||||
text: Math.round(AudioService.inputVolume * 100) + "%"
|
||||
onMoved: value => AudioService.setInputVolume(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Input Mute Toggle
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.audio.volumes-mute-input-label")
|
||||
description: I18n.tr("panels.audio.volumes-mute-input-description")
|
||||
checked: AudioService.inputMuted
|
||||
onToggled: checked => AudioService.setInputMuted(checked)
|
||||
}
|
||||
}
|
||||
|
||||
// Volume Step Size
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NSpinBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.audio.volumes-step-size-label")
|
||||
description: I18n.tr("panels.audio.volumes-step-size-description")
|
||||
minimum: 1
|
||||
maximum: 25
|
||||
value: Settings.data.audio.volumeStep
|
||||
stepSize: 1
|
||||
suffix: "%"
|
||||
defaultValue: Settings.getDefaultValue("audio.volumeStep")
|
||||
onValueChanged: Settings.data.audio.volumeStep = value
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Raise maximum volume above 100%
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.audio.volumes-volume-overdrive-label")
|
||||
description: I18n.tr("panels.audio.volumes-volume-overdrive-description")
|
||||
checked: Settings.data.audio.volumeOverdrive
|
||||
defaultValue: Settings.getDefaultValue("audio.volumeOverdrive")
|
||||
onToggled: checked => Settings.data.audio.volumeOverdrive = checked
|
||||
}
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: volumeFeedbackFilePicker
|
||||
title: I18n.tr("panels.audio.volumes-feedback-sound-file-select-title")
|
||||
selectionMode: "files"
|
||||
initialPath: Quickshell.env("HOME")
|
||||
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
|
||||
onAccepted: paths => {
|
||||
if (paths.length > 0) {
|
||||
Settings.data.audio.volumeFeedbackSoundFile = paths[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-position-label")
|
||||
description: I18n.tr("panels.bar.appearance-position-description")
|
||||
model: [
|
||||
{
|
||||
"key": "top",
|
||||
"name": I18n.tr("positions.top")
|
||||
},
|
||||
{
|
||||
"key": "bottom",
|
||||
"name": I18n.tr("positions.bottom")
|
||||
},
|
||||
{
|
||||
"key": "left",
|
||||
"name": I18n.tr("positions.left")
|
||||
},
|
||||
{
|
||||
"key": "right",
|
||||
"name": I18n.tr("positions.right")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.bar.position
|
||||
defaultValue: Settings.getDefaultValue("bar.position")
|
||||
onSelected: key => Settings.data.bar.position = key
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-density-label")
|
||||
description: I18n.tr("panels.bar.appearance-density-description")
|
||||
model: [
|
||||
{
|
||||
"key": "mini",
|
||||
"name": I18n.tr("options.bar.density-mini")
|
||||
},
|
||||
{
|
||||
"key": "compact",
|
||||
"name": I18n.tr("options.bar.density-compact")
|
||||
},
|
||||
{
|
||||
"key": "default",
|
||||
"name": I18n.tr("options.bar.density-default")
|
||||
},
|
||||
{
|
||||
"key": "comfortable",
|
||||
"name": I18n.tr("options.bar.density-comfortable")
|
||||
},
|
||||
{
|
||||
"key": "spacious",
|
||||
"name": I18n.tr("options.bar.density-spacious")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.bar.density
|
||||
defaultValue: Settings.getDefaultValue("bar.density")
|
||||
onSelected: key => Settings.data.bar.density = key
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-type-label")
|
||||
description: I18n.tr("panels.bar.appearance-type-description")
|
||||
model: [
|
||||
{
|
||||
"key": "simple",
|
||||
"name": I18n.tr("options.bar.type-simple")
|
||||
},
|
||||
{
|
||||
"key": "floating",
|
||||
"name": I18n.tr("options.bar.type-floating")
|
||||
},
|
||||
{
|
||||
"key": "framed",
|
||||
"name": I18n.tr("options.bar.type-framed")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.bar.barType
|
||||
defaultValue: Settings.getDefaultValue("bar.barType")
|
||||
onSelected: key => {
|
||||
Settings.data.bar.barType = key;
|
||||
}
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("panels.bar.appearance-display-mode-description")
|
||||
model: [
|
||||
{
|
||||
"key": "always_visible",
|
||||
"name": I18n.tr("hide-modes.visible")
|
||||
},
|
||||
{
|
||||
"key": "non_exclusive",
|
||||
"name": I18n.tr("hide-modes.non-exclusive")
|
||||
},
|
||||
{
|
||||
"key": "auto_hide",
|
||||
"name": I18n.tr("hide-modes.auto-hide")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.data.bar.displayMode
|
||||
defaultValue: Settings.getDefaultValue("bar.displayMode")
|
||||
onSelected: key => Settings.data.bar.displayMode = key
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.bar.appearance-use-separate-opacity-label")
|
||||
description: I18n.tr("panels.bar.appearance-use-separate-opacity-description")
|
||||
checked: Settings.data.bar.useSeparateOpacity
|
||||
defaultValue: Settings.getDefaultValue("bar.useSeparateOpacity")
|
||||
onToggled: checked => Settings.data.bar.useSeparateOpacity = checked
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.bar.useSeparateOpacity
|
||||
label: I18n.tr("panels.bar.appearance-background-opacity-label")
|
||||
description: I18n.tr("panels.bar.appearance-background-opacity-description")
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.bar.backgroundOpacity
|
||||
defaultValue: Settings.getDefaultValue("bar.backgroundOpacity")
|
||||
onMoved: value => Settings.data.bar.backgroundOpacity = value
|
||||
text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-font-scale-label")
|
||||
description: I18n.tr("panels.bar.appearance-font-scale-description")
|
||||
from: 0.5
|
||||
to: 2.0
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.bar.fontScale
|
||||
defaultValue: Settings.getDefaultValue("bar.fontScale")
|
||||
onMoved: value => Settings.data.bar.fontScale = value
|
||||
text: Math.floor(Settings.data.bar.fontScale * 100) + "%"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-widget-spacing-label")
|
||||
description: I18n.tr("panels.bar.appearance-widget-spacing-description")
|
||||
from: 0
|
||||
to: 30
|
||||
stepSize: 1
|
||||
showReset: true
|
||||
value: Settings.data.bar.widgetSpacing
|
||||
defaultValue: Settings.getDefaultValue("bar.widgetSpacing")
|
||||
onMoved: value => Settings.data.bar.widgetSpacing = value
|
||||
text: Settings.data.bar.widgetSpacing + "px"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-content-padding-label")
|
||||
description: I18n.tr("panels.bar.appearance-content-padding-description")
|
||||
from: 0
|
||||
to: 30
|
||||
stepSize: 1
|
||||
showReset: true
|
||||
value: Settings.data.bar.contentPadding
|
||||
defaultValue: Settings.getDefaultValue("bar.contentPadding")
|
||||
onMoved: value => Settings.data.bar.contentPadding = value
|
||||
text: Settings.data.bar.contentPadding + "px"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-show-outline-label")
|
||||
description: I18n.tr("panels.bar.appearance-show-outline-description")
|
||||
checked: Settings.data.bar.showOutline
|
||||
defaultValue: Settings.getDefaultValue("bar.showOutline")
|
||||
onToggled: checked => Settings.data.bar.showOutline = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-show-capsule-label")
|
||||
description: I18n.tr("panels.bar.appearance-show-capsule-description")
|
||||
checked: Settings.data.bar.showCapsule
|
||||
defaultValue: Settings.getDefaultValue("bar.showCapsule")
|
||||
onToggled: checked => Settings.data.bar.showCapsule = checked
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.bar.showCapsule
|
||||
label: I18n.tr("panels.bar.appearance-capsule-color-label")
|
||||
description: I18n.tr("panels.bar.appearance-capsule-color-description")
|
||||
noneColor: Color.mSurfaceVariant
|
||||
noneOnColor: Color.mOnSurfaceVariant
|
||||
currentKey: Settings.data.bar.capsuleColorKey
|
||||
onSelected: key => Settings.data.bar.capsuleColorKey = key
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.bar.showCapsule
|
||||
label: I18n.tr("panels.bar.appearance-capsule-opacity-label")
|
||||
description: I18n.tr("panels.bar.appearance-capsule-opacity-description")
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.01
|
||||
showReset: true
|
||||
value: Settings.data.bar.capsuleOpacity
|
||||
defaultValue: Settings.getDefaultValue("bar.capsuleOpacity")
|
||||
onMoved: value => Settings.data.bar.capsuleOpacity = value
|
||||
text: Math.floor(Settings.data.bar.capsuleOpacity * 100) + "%"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-enable-exclusion-zone-inset-label")
|
||||
description: I18n.tr("panels.bar.appearance-enable-exclusion-zone-inset-description")
|
||||
checked: Settings.data.bar.enableExclusionZoneInset
|
||||
defaultValue: Settings.getDefaultValue("bar.enableExclusionZoneInset")
|
||||
onToggled: checked => Settings.data.bar.enableExclusionZoneInset = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
visible: CompositorService.isNiri
|
||||
label: I18n.tr("panels.bar.appearance-hide-on-overview-label")
|
||||
description: I18n.tr("panels.bar.appearance-hide-on-overview-description")
|
||||
checked: Settings.data.bar.hideOnOverview
|
||||
defaultValue: Settings.getDefaultValue("bar.hideOnOverview")
|
||||
onToggled: checked => Settings.data.bar.hideOnOverview = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-outer-corners-label")
|
||||
description: I18n.tr("panels.bar.appearance-outer-corners-description")
|
||||
checked: Settings.data.bar.outerCorners
|
||||
visible: Settings.data.bar.barType === "simple"
|
||||
defaultValue: Settings.getDefaultValue("bar.outerCorners")
|
||||
onToggled: checked => Settings.data.bar.outerCorners = checked
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: Settings.data.bar.barType === "framed"
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.bar.appearance-frame-settings-label")
|
||||
description: I18n.tr("panels.bar.appearance-frame-settings-description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginL
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-frame-thickness")
|
||||
from: 4
|
||||
to: 24
|
||||
stepSize: 1
|
||||
showReset: true
|
||||
value: Settings.data.bar.frameThickness
|
||||
defaultValue: Settings.getDefaultValue("bar.frameThickness")
|
||||
onMoved: value => Settings.data.bar.frameThickness = value
|
||||
text: Settings.data.bar.frameThickness + "px"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-frame-radius")
|
||||
from: 4
|
||||
to: 24
|
||||
stepSize: 1
|
||||
showReset: true
|
||||
value: Settings.data.bar.frameRadius
|
||||
defaultValue: Settings.getDefaultValue("bar.frameRadius")
|
||||
onMoved: value => Settings.data.bar.frameRadius = value
|
||||
text: Settings.data.bar.frameRadius + "px"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: Settings.data.bar.barType === "floating"
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
label: I18n.tr("panels.bar.appearance-margins-vertical")
|
||||
description: I18n.tr("panels.bar.appearance-margins-description")
|
||||
from: 0
|
||||
to: 500
|
||||
suffix: "px"
|
||||
value: Settings.data.bar.marginVertical
|
||||
defaultValue: Settings.getDefaultValue("bar.marginVertical")
|
||||
onValueChanged: Settings.data.bar.marginVertical = value
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
label: I18n.tr("panels.bar.appearance-margins-horizontal")
|
||||
description: I18n.tr("panels.bar.appearance-margins-description")
|
||||
from: 0
|
||||
to: 500
|
||||
suffix: "px"
|
||||
value: Settings.data.bar.marginHorizontal
|
||||
defaultValue: Settings.getDefaultValue("bar.marginHorizontal")
|
||||
onValueChanged: Settings.data.bar.marginHorizontal = value
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginS
|
||||
visible: Settings.data.bar.displayMode === "auto_hide"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: Settings.data.bar.displayMode === "auto_hide"
|
||||
spacing: Style.marginS
|
||||
Layout.fillWidth: true
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-auto-hide-delay-label")
|
||||
description: I18n.tr("panels.bar.appearance-auto-hide-delay-description")
|
||||
from: 100
|
||||
to: 2000
|
||||
stepSize: 100
|
||||
showReset: true
|
||||
value: Settings.data.bar.autoHideDelay
|
||||
defaultValue: Settings.getDefaultValue("bar.autoHideDelay")
|
||||
onMoved: value => Settings.data.bar.autoHideDelay = value
|
||||
text: Settings.data.bar.autoHideDelay + "ms"
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-auto-show-delay-label")
|
||||
description: I18n.tr("panels.bar.appearance-auto-show-delay-description")
|
||||
from: 0
|
||||
to: 500
|
||||
stepSize: 50
|
||||
showReset: true
|
||||
value: Settings.data.bar.autoShowDelay
|
||||
defaultValue: Settings.getDefaultValue("bar.autoShowDelay")
|
||||
onMoved: value => Settings.data.bar.autoShowDelay = value
|
||||
text: Settings.data.bar.autoShowDelay + "ms"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-show-on-workspace-switch-label")
|
||||
description: I18n.tr("panels.bar.appearance-show-on-workspace-switch-description")
|
||||
checked: Settings.data.bar.showOnWorkspaceSwitch
|
||||
defaultValue: Settings.getDefaultValue("bar.showOnWorkspaceSwitch")
|
||||
onToggled: checked => Settings.data.bar.showOnWorkspaceSwitch = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
// Helper functions to update arrays immutably
|
||||
function addMonitor(list, name) {
|
||||
const arr = (list || []).slice();
|
||||
if (!arr.includes(name))
|
||||
arr.push(name);
|
||||
return arr;
|
||||
}
|
||||
function removeMonitor(list, name) {
|
||||
return (list || []).filter(function (n) {
|
||||
return n !== name;
|
||||
});
|
||||
}
|
||||
|
||||
// Signal functions for widgets sub-tab (global widgets only).
|
||||
// These intentionally edit Settings.data.bar.widgets (global defaults),
|
||||
// not per-screen overrides. Per-screen editing is handled by MonitorWidgetsConfig.qml.
|
||||
function _addWidgetToSection(widgetId, section) {
|
||||
var newWidget = {
|
||||
"id": widgetId
|
||||
};
|
||||
if (BarWidgetRegistry.widgetHasUserSettings(widgetId)) {
|
||||
var metadata = BarWidgetRegistry.widgetMetadata[widgetId];
|
||||
if (metadata) {
|
||||
Object.keys(metadata).forEach(function (key) {
|
||||
newWidget[key] = metadata[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
Settings.data.bar.widgets[section].push(newWidget);
|
||||
BarService.widgetsRevision++;
|
||||
}
|
||||
|
||||
function _removeWidgetFromSection(section, index) {
|
||||
var widgets = Settings.data.bar.widgets;
|
||||
if (index >= 0 && index < widgets[section].length) {
|
||||
var newArray = widgets[section].slice();
|
||||
var removedWidgets = newArray.splice(index, 1);
|
||||
widgets[section] = newArray;
|
||||
BarService.widgetsRevision++;
|
||||
|
||||
if (removedWidgets[0].id === "ControlCenter" && BarService.lookupWidget("ControlCenter") === undefined) {
|
||||
ToastService.showWarning(I18n.tr("toast.missing-control-center.label"), I18n.tr("toast.missing-control-center.description"), 6000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _reorderWidgetInSection(section, fromIndex, toIndex) {
|
||||
var widgets = Settings.data.bar.widgets;
|
||||
if (fromIndex >= 0 && fromIndex < widgets[section].length && toIndex >= 0 && toIndex < widgets[section].length) {
|
||||
var newArray = widgets[section].slice();
|
||||
var item = newArray[fromIndex];
|
||||
newArray.splice(fromIndex, 1);
|
||||
newArray.splice(toIndex, 0, item);
|
||||
widgets[section] = newArray;
|
||||
BarService.widgetsRevision++;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: _updateWidgetSettingsInSection does NOT increment revision
|
||||
// because it only changes settings, not widget structure
|
||||
function _updateWidgetSettingsInSection(section, index, settings) {
|
||||
Settings.data.bar.widgets[section][index] = settings;
|
||||
}
|
||||
|
||||
function _moveWidgetBetweenSections(fromSection, index, toSection) {
|
||||
var widgets = Settings.data.bar.widgets;
|
||||
if (index >= 0 && index < widgets[fromSection].length) {
|
||||
var widget = widgets[fromSection][index];
|
||||
var sourceArray = widgets[fromSection].slice();
|
||||
sourceArray.splice(index, 1);
|
||||
widgets[fromSection] = sourceArray;
|
||||
var targetArray = widgets[toSection].slice();
|
||||
targetArray.push(widget);
|
||||
widgets[toSection] = targetArray;
|
||||
BarService.widgetsRevision++;
|
||||
Logger.d("BarTab", "_moveWidgetBetweenSections: revision now", BarService.widgetsRevision);
|
||||
}
|
||||
}
|
||||
|
||||
function getWidgetLocations(widgetId) {
|
||||
if (!BarService)
|
||||
return [];
|
||||
const instances = BarService.getAllRegisteredWidgets();
|
||||
const locations = {};
|
||||
for (var i = 0; i < instances.length; i++) {
|
||||
if (instances[i].widgetId === widgetId) {
|
||||
const section = instances[i].section;
|
||||
if (section === "left")
|
||||
locations["arrow-bar-to-left"] = true;
|
||||
else if (section === "center")
|
||||
locations["layout-columns"] = true;
|
||||
else if (section === "right")
|
||||
locations["arrow-bar-to-right"] = true;
|
||||
}
|
||||
}
|
||||
return Object.keys(locations);
|
||||
}
|
||||
|
||||
function createBadges(isPlugin, locations) {
|
||||
const badges = [];
|
||||
if (isPlugin) {
|
||||
badges.push({
|
||||
"icon": "plugin",
|
||||
"color": Color.mSecondary
|
||||
});
|
||||
}
|
||||
locations.forEach(function (location) {
|
||||
badges.push({
|
||||
"icon": location,
|
||||
"color": Color.mOnSurfaceVariant
|
||||
});
|
||||
});
|
||||
return badges;
|
||||
}
|
||||
|
||||
function updateAvailableWidgetsModel() {
|
||||
availableWidgets.clear();
|
||||
const widgets = BarWidgetRegistry.getAvailableWidgets();
|
||||
widgets.forEach(entry => {
|
||||
const isPlugin = BarWidgetRegistry.isPluginWidget(entry);
|
||||
let displayName = entry;
|
||||
if (isPlugin) {
|
||||
const pluginId = entry.replace("plugin:", "");
|
||||
const manifest = PluginRegistry.getPluginManifest(pluginId);
|
||||
if (manifest && manifest.name) {
|
||||
displayName = manifest.name;
|
||||
} else {
|
||||
displayName = pluginId;
|
||||
}
|
||||
}
|
||||
availableWidgets.append({
|
||||
"key": entry,
|
||||
"name": displayName,
|
||||
"badges": createBadges(isPlugin, getWidgetLocations(entry))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: availableWidgets
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(updateAvailableWidgetsModel);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BarService
|
||||
function onActiveWidgetsChanged() {
|
||||
updateAvailableWidgetsModel();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BarWidgetRegistry
|
||||
function onPluginWidgetRegistryUpdated() {
|
||||
updateAvailableWidgetsModel();
|
||||
}
|
||||
}
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.appearance")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.widgets")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.behavior")
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.monitors")
|
||||
tabIndex: 3
|
||||
checked: subTabBar.currentIndex === 3
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginS
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
AppearanceSubTab {}
|
||||
WidgetsSubTab {
|
||||
availableWidgets: availableWidgets
|
||||
addWidgetToSection: root._addWidgetToSection
|
||||
removeWidgetFromSection: root._removeWidgetFromSection
|
||||
reorderWidgetInSection: root._reorderWidgetInSection
|
||||
updateWidgetSettingsInSection: root._updateWidgetSettingsInSection
|
||||
moveWidgetBetweenSections: root._moveWidgetBetweenSections
|
||||
onOpenPluginSettings: manifest => pluginSettingsDialog.openPluginSettings(manifest)
|
||||
}
|
||||
BehaviorSubTab {}
|
||||
MonitorsSubTab {
|
||||
addMonitor: root.addMonitor
|
||||
removeMonitor: root.removeMonitor
|
||||
}
|
||||
}
|
||||
|
||||
NPluginSettingsPopup {
|
||||
id: pluginSettingsDialog
|
||||
parent: Overlay.overlay
|
||||
showToastOnSave: false
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
readonly property string effectiveWheelAction: Settings.data.bar.mouseWheelAction || "none"
|
||||
readonly property string effectiveMiddleClickAction: Settings.data.bar.middleClickAction || "none"
|
||||
readonly property string effectiveRightClickAction: Settings.data.bar.rightClickAction || "controlCenter"
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-workspace-scroll-label")
|
||||
description: I18n.tr("panels.bar.behavior-workspace-scroll-description")
|
||||
model: {
|
||||
var items = [
|
||||
{
|
||||
"key": "none",
|
||||
"name": I18n.tr("common.none")
|
||||
},
|
||||
{
|
||||
"key": "volume",
|
||||
"name": I18n.tr("common.volume")
|
||||
},
|
||||
{
|
||||
"key": "workspace",
|
||||
"name": I18n.tr("panels.bar.behavior-workspace-scroll-option-workspace")
|
||||
}
|
||||
];
|
||||
if (CompositorService.isNiri) {
|
||||
items.push({
|
||||
"key": "content",
|
||||
"name": I18n.tr("panels.bar.behavior-workspace-scroll-option-content")
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
currentKey: root.effectiveWheelAction
|
||||
defaultValue: Settings.getDefaultValue("bar.mouseWheelAction")
|
||||
onSelected: key => Settings.data.bar.mouseWheelAction = key
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.reverse-scrolling-label")
|
||||
description: I18n.tr("panels.general.reverse-scrolling-description")
|
||||
checked: Settings.data.bar.reverseScroll
|
||||
defaultValue: Settings.getDefaultValue("bar.reverseScroll")
|
||||
onToggled: checked => Settings.data.bar.reverseScroll = checked
|
||||
visible: Settings.data.bar.mouseWheelAction !== "none"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-wheel-wrap-label")
|
||||
description: I18n.tr("panels.bar.behavior-wheel-wrap-description")
|
||||
checked: Settings.data.bar.mouseWheelWrap
|
||||
defaultValue: Settings.getDefaultValue("bar.mouseWheelWrap")
|
||||
onToggled: checked => Settings.data.bar.mouseWheelWrap = checked
|
||||
visible: Settings.data.bar.mouseWheelAction === "workspace"
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-middle-click-label")
|
||||
description: I18n.tr("panels.bar.behavior-middle-click-description")
|
||||
model: [
|
||||
{
|
||||
"key": "none",
|
||||
"name": I18n.tr("common.none")
|
||||
},
|
||||
{
|
||||
"key": "controlCenter",
|
||||
"name": I18n.tr("tooltips.open-control-center")
|
||||
},
|
||||
{
|
||||
"key": "settings",
|
||||
"name": I18n.tr("tooltips.open-settings")
|
||||
},
|
||||
{
|
||||
"key": "launcherPanel",
|
||||
"name": I18n.tr("actions.open-launcher")
|
||||
},
|
||||
{
|
||||
"key": "command",
|
||||
"name": I18n.tr("actions.run-custom-command")
|
||||
}
|
||||
]
|
||||
currentKey: root.effectiveMiddleClickAction
|
||||
defaultValue: Settings.getDefaultValue("bar.middleClickAction")
|
||||
onSelected: key => Settings.data.bar.middleClickAction = key
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-middle-click-command-label")
|
||||
description: I18n.tr("panels.bar.behavior-middle-click-command-description")
|
||||
placeholderText: I18n.tr("panels.bar.behavior-middle-click-command-placeholder")
|
||||
text: Settings.data.bar.middleClickCommand
|
||||
fontFamily: Settings.data.ui.fontFixed
|
||||
onTextChanged: Settings.data.bar.middleClickCommand = text
|
||||
visible: Settings.data.bar.middleClickAction === "command"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-middle-click-follow-mouse-label")
|
||||
description: I18n.tr("panels.bar.behavior-middle-click-follow-mouse-description")
|
||||
checked: Settings.data.bar.middleClickFollowMouse
|
||||
defaultValue: Settings.getDefaultValue("bar.middleClickFollowMouse")
|
||||
onToggled: checked => Settings.data.bar.middleClickFollowMouse = checked
|
||||
visible: Settings.data.bar.middleClickAction !== "none" && Settings.data.bar.middleClickAction !== "command" && !(Settings.data.bar.middleClickAction === "settings" && Settings.data.ui.settingsPanelMode === "window")
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-right-click-label")
|
||||
description: I18n.tr("panels.bar.behavior-right-click-description")
|
||||
model: [
|
||||
{
|
||||
"key": "none",
|
||||
"name": I18n.tr("common.none")
|
||||
},
|
||||
{
|
||||
"key": "controlCenter",
|
||||
"name": I18n.tr("tooltips.open-control-center")
|
||||
},
|
||||
{
|
||||
"key": "settings",
|
||||
"name": I18n.tr("tooltips.open-settings")
|
||||
},
|
||||
{
|
||||
"key": "launcherPanel",
|
||||
"name": I18n.tr("actions.open-launcher")
|
||||
},
|
||||
{
|
||||
"key": "command",
|
||||
"name": I18n.tr("actions.run-custom-command")
|
||||
}
|
||||
]
|
||||
currentKey: root.effectiveRightClickAction
|
||||
defaultValue: Settings.getDefaultValue("bar.rightClickAction")
|
||||
onSelected: key => Settings.data.bar.rightClickAction = key
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-right-click-command-label")
|
||||
description: I18n.tr("panels.bar.behavior-right-click-command-description")
|
||||
placeholderText: I18n.tr("panels.bar.behavior-right-click-command-placeholder")
|
||||
text: Settings.data.bar.rightClickCommand
|
||||
fontFamily: Settings.data.ui.fontFixed
|
||||
onTextChanged: Settings.data.bar.rightClickCommand = text
|
||||
visible: Settings.data.bar.rightClickAction === "command"
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.behavior-right-click-follow-mouse-label")
|
||||
description: I18n.tr("panels.bar.behavior-right-click-follow-mouse-description")
|
||||
checked: Settings.data.bar.rightClickFollowMouse
|
||||
defaultValue: Settings.getDefaultValue("bar.rightClickFollowMouse")
|
||||
onToggled: checked => Settings.data.bar.rightClickFollowMouse = checked
|
||||
visible: Settings.data.bar.rightClickAction !== "none" && Settings.data.bar.rightClickAction !== "command" && !(Settings.data.bar.rightClickAction === "settings" && Settings.data.ui.settingsPanelMode === "window")
|
||||
}
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "../../Bar" as BarSettings
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property var addMonitor
|
||||
property var removeMonitor
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.bar.monitors-desc-new")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Monitor cards
|
||||
Repeater {
|
||||
model: Quickshell.screens || []
|
||||
delegate: NBox {
|
||||
id: monitorCard
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: cardContent.implicitHeight + Style.margin2L
|
||||
color: Color.mSurface
|
||||
|
||||
required property var modelData
|
||||
readonly property string screenName: modelData.name || "Unknown"
|
||||
readonly property real compositorScale: {
|
||||
const info = CompositorService.displayScales[screenName];
|
||||
return (info && info.scale) ? info.scale : 1.0;
|
||||
}
|
||||
readonly property bool barEnabled: (Settings.data.bar.monitors || []).indexOf(screenName) !== -1
|
||||
readonly property bool hasOverride: Settings.hasScreenOverride(screenName)
|
||||
|
||||
// Track if override is enabled (controls both visibility AND whether overrides are applied)
|
||||
readonly property bool overrideEnabled: Settings.isScreenOverrideEnabled(screenName)
|
||||
|
||||
// Get effective values for this screen
|
||||
readonly property string effectivePosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property string effectiveDensity: Settings.getBarDensityForScreen(screenName)
|
||||
|
||||
ColumnLayout {
|
||||
id: cardContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Header: Monitor name and specs
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXXS
|
||||
|
||||
NText {
|
||||
Layout.fillWidth: true
|
||||
text: monitorCard.screenName
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: {
|
||||
return I18n.tr("system.monitor-description", {
|
||||
"model": monitorCard.modelData.model || I18n.tr("common.unknown"),
|
||||
"width": Math.round(monitorCard.modelData.width * monitorCard.compositorScale),
|
||||
"height": Math.round(monitorCard.modelData.height * monitorCard.compositorScale),
|
||||
"scale": monitorCard.compositorScale
|
||||
});
|
||||
}
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
// Enable bar toggle
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
checked: monitorCard.barEnabled
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
Settings.data.bar.monitors = root.addMonitor(Settings.data.bar.monitors, monitorCard.screenName);
|
||||
} else {
|
||||
Settings.data.bar.monitors = root.removeMonitor(Settings.data.bar.monitors, monitorCard.screenName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
visible: Settings.data.bar.monitors.includes(monitorCard.screenName)
|
||||
}
|
||||
|
||||
// Override section (only visible when bar is enabled)
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
visible: monitorCard.barEnabled
|
||||
|
||||
// Override toggle
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.monitor-override-settings")
|
||||
description: I18n.tr("panels.bar.monitor-override-settings-description")
|
||||
checked: monitorCard.overrideEnabled
|
||||
onToggled: checked => {
|
||||
Settings.setScreenOverride(monitorCard.screenName, "enabled", checked);
|
||||
BarService.widgetsRevision++;
|
||||
}
|
||||
}
|
||||
|
||||
// Override controls (only visible when override toggle is on)
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
visible: monitorCard.overrideEnabled
|
||||
|
||||
// Position override
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-position-label")
|
||||
description: I18n.tr("panels.bar.appearance-position-description")
|
||||
model: [
|
||||
{
|
||||
"key": "top",
|
||||
"name": I18n.tr("positions.top")
|
||||
},
|
||||
{
|
||||
"key": "bottom",
|
||||
"name": I18n.tr("positions.bottom")
|
||||
},
|
||||
{
|
||||
"key": "left",
|
||||
"name": I18n.tr("positions.left")
|
||||
},
|
||||
{
|
||||
"key": "right",
|
||||
"name": I18n.tr("positions.right")
|
||||
}
|
||||
]
|
||||
currentKey: monitorCard.effectivePosition
|
||||
onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "position", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Density override
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.bar.appearance-density-label")
|
||||
description: I18n.tr("panels.bar.appearance-density-description")
|
||||
model: [
|
||||
{
|
||||
"key": "mini",
|
||||
"name": I18n.tr("options.bar.density-mini")
|
||||
},
|
||||
{
|
||||
"key": "compact",
|
||||
"name": I18n.tr("options.bar.density-compact")
|
||||
},
|
||||
{
|
||||
"key": "default",
|
||||
"name": I18n.tr("options.bar.density-default")
|
||||
},
|
||||
{
|
||||
"key": "comfortable",
|
||||
"name": I18n.tr("options.bar.density-comfortable")
|
||||
},
|
||||
{
|
||||
"key": "spacious",
|
||||
"name": I18n.tr("options.bar.density-spacious")
|
||||
}
|
||||
]
|
||||
currentKey: monitorCard.effectiveDensity
|
||||
onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "density", key)
|
||||
}
|
||||
}
|
||||
|
||||
// DisplayMode override
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("panels.bar.appearance-display-mode-description")
|
||||
model: [
|
||||
{
|
||||
"key": "always_visible",
|
||||
"name": I18n.tr("hide-modes.visible")
|
||||
},
|
||||
{
|
||||
"key": "non_exclusive",
|
||||
"name": I18n.tr("hide-modes.non-exclusive")
|
||||
},
|
||||
{
|
||||
"key": "auto_hide",
|
||||
"name": I18n.tr("hide-modes.auto-hide")
|
||||
}
|
||||
]
|
||||
currentKey: Settings.getBarDisplayModeForScreen(monitorCard.screenName)
|
||||
onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "displayMode", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Widgets configuration button and Reset all
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NButton {
|
||||
id: widgetConfigButton
|
||||
property bool expanded: false
|
||||
Layout.fillWidth: true
|
||||
fontSize: Style.fontSizeS
|
||||
text: I18n.tr("panels.bar.monitor-configure-widgets")
|
||||
icon: expanded ? "chevron-up" : "layout-grid"
|
||||
onClicked: expanded = !expanded
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: Settings.hasScreenOverride(monitorCard.screenName, "widgets")
|
||||
Layout.fillWidth: true
|
||||
fontSize: Style.fontSizeS
|
||||
text: I18n.tr("panels.bar.use-global-widgets")
|
||||
icon: "refresh"
|
||||
onClicked: {
|
||||
Settings.clearScreenOverride(monitorCard.screenName, "widgets");
|
||||
BarService.widgetsRevision++;
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
Layout.fillWidth: true
|
||||
fontSize: Style.fontSizeS
|
||||
text: I18n.tr("panels.bar.monitor-reset-all")
|
||||
icon: "restore"
|
||||
onClicked: {
|
||||
Settings.clearScreenOverride(monitorCard.screenName);
|
||||
BarService.widgetsRevision++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inline widget configuration
|
||||
BarSettings.MonitorWidgetsConfig {
|
||||
visible: widgetConfigButton.expanded
|
||||
screen: monitorCard.modelData
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property var availableWidgets
|
||||
property var addWidgetToSection
|
||||
property var removeWidgetFromSection
|
||||
property var reorderWidgetInSection
|
||||
property var updateWidgetSettingsInSection
|
||||
property var moveWidgetBetweenSections
|
||||
|
||||
signal openPluginSettings(var manifest)
|
||||
|
||||
// This sub-tab edits the global default widget configuration (Settings.data.bar.widgets).
|
||||
// Per-screen widget overrides are edited in MonitorWidgetsConfig.qml (Monitors sub-tab).
|
||||
|
||||
// determine bar orientation
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
|
||||
|
||||
function getSectionIcons() {
|
||||
return {
|
||||
"left": "arrow-bar-to-up",
|
||||
"center": "layout-distribute-horizontal",
|
||||
"right": "arrow-bar-to-down"
|
||||
};
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.bar.widgets-desc")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Left Section
|
||||
NSectionEditor {
|
||||
sectionName: root.barIsVertical ? I18n.tr("positions.top") : I18n.tr("positions.left")
|
||||
sectionId: "left"
|
||||
barIsVertical: root.barIsVertical
|
||||
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
|
||||
widgetRegistry: BarWidgetRegistry
|
||||
widgetModel: Settings.data.bar.widgets.left
|
||||
sectionIcons: root.getSectionIcons()
|
||||
availableWidgets: root.availableWidgets
|
||||
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
|
||||
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest)
|
||||
}
|
||||
|
||||
// Center Section
|
||||
NSectionEditor {
|
||||
sectionName: I18n.tr("positions.center")
|
||||
sectionId: "center"
|
||||
barIsVertical: root.barIsVertical
|
||||
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
|
||||
widgetRegistry: BarWidgetRegistry
|
||||
widgetModel: Settings.data.bar.widgets.center
|
||||
sectionIcons: root.getSectionIcons()
|
||||
availableWidgets: root.availableWidgets
|
||||
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
|
||||
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest)
|
||||
}
|
||||
|
||||
// Right Section
|
||||
NSectionEditor {
|
||||
sectionName: root.barIsVertical ? I18n.tr("positions.bottom") : I18n.tr("positions.right")
|
||||
sectionId: "right"
|
||||
barIsVertical: root.barIsVertical
|
||||
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
|
||||
widgetRegistry: BarWidgetRegistry
|
||||
widgetModel: Settings.data.bar.widgets.right
|
||||
sectionIcons: root.getSectionIcons()
|
||||
availableWidgets: root.availableWidgets
|
||||
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
|
||||
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest)
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "."
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Services.Theming
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
// Time dropdown options (00:00 .. 23:30)
|
||||
ListModel {
|
||||
id: timeOptions
|
||||
}
|
||||
|
||||
property var screen
|
||||
|
||||
function populateTimeOptions() {
|
||||
for (var h = 0; h < 24; h++) {
|
||||
for (var m = 0; m < 60; m += 30) {
|
||||
var hh = ("0" + h).slice(-2);
|
||||
var mm = ("0" + m).slice(-2);
|
||||
var key = hh + ":" + mm;
|
||||
timeOptions.append({
|
||||
"key": key,
|
||||
"name": key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(populateTimeOptions);
|
||||
}
|
||||
|
||||
// Download popup
|
||||
Loader {
|
||||
id: downloadPopupLoader
|
||||
active: false
|
||||
sourceComponent: SchemeDownloader {
|
||||
parent: Overlay.overlay
|
||||
}
|
||||
|
||||
property bool pendingOpen: false
|
||||
|
||||
function open() {
|
||||
pendingOpen = true;
|
||||
active = true;
|
||||
if (item) {
|
||||
item.open();
|
||||
pendingOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
onItemChanged: {
|
||||
if (item && pendingOpen) {
|
||||
item.open();
|
||||
pendingOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.colors")
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.templates")
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: subTabBar.currentIndex
|
||||
|
||||
ColorsSubTab {
|
||||
screen: root.screen
|
||||
timeOptions: timeOptions
|
||||
onOpenDownloadPopup: downloadPopupLoader.open()
|
||||
}
|
||||
TemplatesSubTab {}
|
||||
}
|
||||
}
|
||||
+458
@@ -0,0 +1,458 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Services.Theming
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Layout.fillWidth: true
|
||||
|
||||
property var timeOptions
|
||||
property var schemeColorsCache: ({})
|
||||
property int cacheVersion: 0
|
||||
property var screen
|
||||
|
||||
signal openDownloadPopup
|
||||
|
||||
function extractSchemeName(schemePath) {
|
||||
var pathParts = schemePath.split("/");
|
||||
var filename = pathParts[pathParts.length - 1];
|
||||
var schemeName = filename.replace(".json", "");
|
||||
|
||||
if (schemeName === "Noctalia-default") {
|
||||
schemeName = "Noctalia (default)";
|
||||
} else if (schemeName === "Noctalia-legacy") {
|
||||
schemeName = "Noctalia (legacy)";
|
||||
} else if (schemeName === "Tokyo-Night") {
|
||||
schemeName = "Tokyo Night";
|
||||
} else if (schemeName === "Rosepine") {
|
||||
schemeName = "Rose Pine";
|
||||
}
|
||||
|
||||
return schemeName;
|
||||
}
|
||||
|
||||
function getSchemeColor(schemeName, colorKey) {
|
||||
var _ = cacheVersion;
|
||||
|
||||
if (schemeColorsCache[schemeName]) {
|
||||
var entry = schemeColorsCache[schemeName];
|
||||
var variant = entry;
|
||||
|
||||
if (entry.dark || entry.light) {
|
||||
variant = Settings.data.colorSchemes.darkMode ? (entry.dark || entry.light) : (entry.light || entry.dark);
|
||||
}
|
||||
|
||||
if (variant && variant[colorKey]) {
|
||||
return variant[colorKey];
|
||||
}
|
||||
}
|
||||
|
||||
if (colorKey === "mSurface")
|
||||
return Color.mSurfaceVariant;
|
||||
if (colorKey === "mPrimary")
|
||||
return Color.mPrimary;
|
||||
if (colorKey === "mSecondary")
|
||||
return Color.mSecondary;
|
||||
if (colorKey === "mTertiary")
|
||||
return Color.mTertiary;
|
||||
if (colorKey === "mError")
|
||||
return Color.mError;
|
||||
return Color.mOnSurfaceVariant;
|
||||
}
|
||||
|
||||
function schemeLoaded(schemeName, jsonData) {
|
||||
var value = jsonData || {};
|
||||
schemeColorsCache[schemeName] = value;
|
||||
cacheVersion++;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ColorSchemeService
|
||||
function onSchemesChanged() {
|
||||
root.schemeColorsCache = {};
|
||||
root.cacheVersion++;
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: fileLoaders
|
||||
visible: false
|
||||
|
||||
Repeater {
|
||||
model: ColorSchemeService.schemes
|
||||
delegate: Item {
|
||||
FileView {
|
||||
path: modelData
|
||||
blockLoading: false
|
||||
onLoaded: {
|
||||
var schemeName = root.extractSchemeName(path);
|
||||
|
||||
try {
|
||||
var jsonData = JSON.parse(text());
|
||||
root.schemeLoaded(schemeName, jsonData);
|
||||
} catch (e) {
|
||||
Logger.w("ColorSchemeTab", "Failed to parse JSON for scheme:", schemeName, e);
|
||||
root.schemeLoaded(schemeName, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("tooltips.switch-to-dark-mode")
|
||||
description: I18n.tr("panels.color-scheme.dark-mode-switch-description")
|
||||
checked: Settings.data.colorSchemes.darkMode
|
||||
onToggled: checked => {
|
||||
Settings.data.colorSchemes.darkMode = checked;
|
||||
root.cacheVersion++;
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.color-scheme.sync-gsettings-label")
|
||||
description: I18n.tr("panels.color-scheme.sync-gsettings-description")
|
||||
checked: Settings.data.colorSchemes.syncGsettings
|
||||
onToggled: checked => {
|
||||
Settings.data.colorSchemes.syncGsettings = checked;
|
||||
if (checked)
|
||||
ColorSchemeService.pushSystemColorScheme();
|
||||
}
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("panels.color-scheme.dark-mode-mode-label")
|
||||
description: I18n.tr("panels.color-scheme.dark-mode-mode-description")
|
||||
|
||||
model: [
|
||||
{
|
||||
"name": I18n.tr("panels.color-scheme.dark-mode-mode-off"),
|
||||
"key": "off"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("panels.color-scheme.dark-mode-mode-manual"),
|
||||
"key": "manual"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("common.location"),
|
||||
"key": "location"
|
||||
}
|
||||
]
|
||||
|
||||
currentKey: Settings.data.colorSchemes.schedulingMode
|
||||
|
||||
onSelected: key => {
|
||||
Settings.data.colorSchemes.schedulingMode = key;
|
||||
AppThemeService.generate();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS
|
||||
visible: Settings.data.colorSchemes.schedulingMode === "manual"
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("panels.display.night-light-manual-schedule-label")
|
||||
description: I18n.tr("panels.display.night-light-manual-schedule-description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: false
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.display.night-light-manual-schedule-sunrise")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
model: root.timeOptions
|
||||
currentKey: Settings.data.colorSchemes.manualSunrise
|
||||
placeholder: I18n.tr("panels.display.night-light-manual-schedule-select-start")
|
||||
onSelected: key => Settings.data.colorSchemes.manualSunrise = key
|
||||
minimumWidth: 120
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 20
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.display.night-light-manual-schedule-sunset")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
model: root.timeOptions
|
||||
currentKey: Settings.data.colorSchemes.manualSunset
|
||||
placeholder: I18n.tr("panels.display.night-light-manual-schedule-select-stop")
|
||||
onSelected: key => Settings.data.colorSchemes.manualSunset = key
|
||||
minimumWidth: 120
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.color-scheme.color-source-use-wallpaper-colors-label")
|
||||
description: I18n.tr("panels.color-scheme.color-source-use-wallpaper-colors-description")
|
||||
checked: Settings.data.colorSchemes.useWallpaperColors
|
||||
onToggled: checked => {
|
||||
Settings.data.colorSchemes.useWallpaperColors = checked;
|
||||
if (checked) {
|
||||
AppThemeService.generate();
|
||||
} else {
|
||||
ToastService.showNotice(I18n.tr("toast.wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.disabled"), "settings-color-scheme");
|
||||
if (Settings.data.colorSchemes.predefinedScheme) {
|
||||
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.color-scheme.wallpaper-monitor-source-label")
|
||||
description: I18n.tr("panels.color-scheme.wallpaper-monitor-source-description")
|
||||
enabled: Settings.data.colorSchemes.useWallpaperColors
|
||||
model: {
|
||||
var m = [];
|
||||
if (Quickshell.screens) {
|
||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||
var screen = Quickshell.screens[i];
|
||||
var name = screen.name;
|
||||
var displayName = name + " (" + screen.width + "x" + screen.height + ")";
|
||||
m.push({
|
||||
"key": name,
|
||||
"name": displayName
|
||||
});
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
currentKey: Settings.data.colorSchemes.monitorForColors || (screen ? screen.name : "")
|
||||
onSelected: key => {
|
||||
Settings.data.colorSchemes.monitorForColors = key;
|
||||
AppThemeService.generate();
|
||||
}
|
||||
defaultValue: ""
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.color-scheme.wallpaper-method-label")
|
||||
description: I18n.tr("panels.color-scheme.wallpaper-method-description")
|
||||
enabled: Settings.data.colorSchemes.useWallpaperColors
|
||||
model: TemplateProcessor.schemeTypes
|
||||
currentKey: Settings.data.colorSchemes.generationMethod
|
||||
onSelected: key => {
|
||||
Settings.data.colorSchemes.generationMethod = key;
|
||||
AppThemeService.generate();
|
||||
}
|
||||
}
|
||||
|
||||
NBox {
|
||||
visible: Settings.data.colorSchemes.useWallpaperColors
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: descriptionColumn.implicitHeight + Style.margin2L
|
||||
color: Color.mSurface
|
||||
|
||||
Column {
|
||||
id: descriptionColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
NText {
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
text: I18n.tr("panels.color-scheme.method-description." + Settings.data.colorSchemes.generationMethod)
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
Row {
|
||||
id: colorPreviewRow
|
||||
spacing: Style.marginS
|
||||
|
||||
property int diameter: 16 * Style.uiScaleRatio
|
||||
|
||||
Repeater {
|
||||
model: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError]
|
||||
|
||||
Rectangle {
|
||||
width: colorPreviewRow.diameter
|
||||
height: colorPreviewRow.diameter
|
||||
radius: width * 0.5
|
||||
color: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
enabled: !Settings.data.colorSchemes.useWallpaperColors
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("panels.color-scheme.predefined-title")
|
||||
description: I18n.tr("panels.color-scheme.predefined-desc")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
rowSpacing: Style.marginM
|
||||
columnSpacing: Style.marginM
|
||||
Layout.fillWidth: true
|
||||
|
||||
Repeater {
|
||||
model: ColorSchemeService.schemes
|
||||
|
||||
Rectangle {
|
||||
id: schemeItem
|
||||
|
||||
property string schemePath: modelData
|
||||
property string schemeName: root.extractSchemeName(modelData)
|
||||
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
height: 50 * Style.uiScaleRatio
|
||||
radius: Style.radiusS
|
||||
color: root.getSchemeColor(schemeName, "mSurface")
|
||||
border.width: Style.borderL
|
||||
border.color: {
|
||||
if ((Settings.data.colorSchemes.predefinedScheme === schemeName) && schemeItem.enabled) {
|
||||
return Color.mSecondary;
|
||||
}
|
||||
if (itemMouseArea.containsMouse) {
|
||||
return Color.mHover;
|
||||
}
|
||||
return Color.mOutline;
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: scheme
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: schemeItem.schemeName
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
property int diameter: 16 * Style.uiScaleRatio
|
||||
|
||||
Rectangle {
|
||||
width: scheme.diameter
|
||||
height: scheme.diameter
|
||||
radius: scheme.diameter * 0.5
|
||||
color: root.getSchemeColor(schemeItem.schemeName, "mPrimary")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: scheme.diameter
|
||||
height: scheme.diameter
|
||||
radius: scheme.diameter * 0.5
|
||||
color: root.getSchemeColor(schemeItem.schemeName, "mSecondary")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: scheme.diameter
|
||||
height: scheme.diameter
|
||||
radius: scheme.diameter * 0.5
|
||||
color: root.getSchemeColor(schemeItem.schemeName, "mTertiary")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: scheme.diameter
|
||||
height: scheme.diameter
|
||||
radius: scheme.diameter * 0.5
|
||||
color: root.getSchemeColor(schemeItem.schemeName, "mError")
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemMouseArea
|
||||
anchors.fill: parent
|
||||
enabled: schemeItem.enabled
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
Settings.data.colorSchemes.useWallpaperColors = false;
|
||||
Logger.i("ColorSchemeTab", "Disabled wallpaper colors");
|
||||
|
||||
Settings.data.colorSchemes.predefinedScheme = schemeItem.schemeName;
|
||||
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: (Settings.data.colorSchemes.predefinedScheme === schemeItem.schemeName) && schemeItem.enabled
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.rightMargin: 0
|
||||
anchors.topMargin: -3
|
||||
width: 20
|
||||
height: 20
|
||||
radius: Math.min(Style.radiusL, width / 2)
|
||||
color: Color.mSecondary
|
||||
border.width: Style.borderS
|
||||
border.color: Color.mOnSecondary
|
||||
|
||||
NIcon {
|
||||
icon: "check"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSecondary
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("panels.color-scheme.download-button")
|
||||
icon: "download"
|
||||
onClicked: root.openDownloadPopup()
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.topMargin: Style.marginS
|
||||
}
|
||||
}
|
||||
}
|
||||
+972
@@ -0,0 +1,972 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Theming
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property var availableSchemes: []
|
||||
property bool fetching: false
|
||||
property bool downloading: false
|
||||
property bool hasInitialData: false // Track if we've loaded data at least once
|
||||
property string downloadError: ""
|
||||
property string downloadingScheme: ""
|
||||
property string pendingApplyScheme: "" // Scheme name to apply after reload
|
||||
property string lastStderrOutput: "" // Store stderr from download process
|
||||
property real lastApiFetchTime: 0 // Track when we last fetched from API to prevent rapid calls
|
||||
property int minApiFetchInterval: 60 // Minimum seconds between API fetches (1 minute)
|
||||
|
||||
// Cache for remote scheme colors
|
||||
property var schemeColorsCache: ({})
|
||||
property int cacheVersion: 0
|
||||
|
||||
// Cache for available schemes list (uses ShellState singleton)
|
||||
property int schemesCacheUpdateFrequency: 2 * 60 * 60 // 2 hours in seconds
|
||||
|
||||
// Cache for repo branch info (to reduce API calls during downloads)
|
||||
property string cachedBranch: "main"
|
||||
property string cachedBranchSha: ""
|
||||
|
||||
width: Math.max(500, contentColumn.implicitWidth + Style.margin2XL)
|
||||
height: Math.min(800, contentColumn.implicitHeight + Style.margin2XL)
|
||||
padding: Style.marginXL
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
dim: false
|
||||
anchors.centerIn: parent
|
||||
|
||||
// Helper function to get color from cached scheme data
|
||||
function getSchemeColor(schemeName, colorKey) {
|
||||
var _ = cacheVersion; // Create dependency
|
||||
|
||||
if (schemeColorsCache[schemeName]) {
|
||||
var entry = schemeColorsCache[schemeName];
|
||||
var variant = entry;
|
||||
|
||||
// Check if scheme has dark/light variants
|
||||
if (entry.dark || entry.light) {
|
||||
variant = Settings.data.colorSchemes.darkMode ? (entry.dark || entry.light) : (entry.light || entry.dark);
|
||||
}
|
||||
|
||||
if (variant && variant[colorKey]) {
|
||||
return variant[colorKey];
|
||||
}
|
||||
}
|
||||
|
||||
// Return visible defaults while loading
|
||||
var defaults = {
|
||||
"mSurface": Color.mSurfaceVariant,
|
||||
"mPrimary": Color.mPrimary,
|
||||
"mSecondary": Color.mSecondary,
|
||||
"mTertiary": Color.mTertiary,
|
||||
"mError": Color.mError,
|
||||
"mOnSurface": Color.mOnSurfaceVariant
|
||||
};
|
||||
return defaults[colorKey] || Color.mOnSurfaceVariant;
|
||||
}
|
||||
|
||||
// Colors are now provided directly in the registry, no need to fetch individual files
|
||||
function fetchSchemeColors(scheme) {
|
||||
return;
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderM
|
||||
}
|
||||
|
||||
function loadSchemesFromCache() {
|
||||
try {
|
||||
const now = Time.timestamp;
|
||||
const cacheData = ShellState.getColorSchemesList();
|
||||
const cachedSchemes = cacheData.schemes || [];
|
||||
const cachedTimestamp = cacheData.timestamp || 0;
|
||||
|
||||
// Check if cache is expired or missing
|
||||
if (!cachedTimestamp || (now >= cachedTimestamp + schemesCacheUpdateFrequency)) {
|
||||
// Migration is now handled in Settings.qml
|
||||
|
||||
// Only fetch from API if we haven't fetched recently (prevent rapid repeated calls)
|
||||
const timeSinceLastFetch = now - lastApiFetchTime;
|
||||
if (timeSinceLastFetch >= minApiFetchInterval) {
|
||||
Logger.d("ColorSchemeDownload", "Cache expired or missing, fetching new schemes");
|
||||
fetchAvailableSchemesFromAPI();
|
||||
return;
|
||||
} else {
|
||||
// Use cached data even if expired, to avoid rate limits
|
||||
Logger.d("ColorSchemeDownload", "Cache expired but recent API call detected, using cached data");
|
||||
if (cachedSchemes.length > 0) {
|
||||
availableSchemes = cachedSchemes;
|
||||
hasInitialData = true;
|
||||
fetching = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ageMinutes = Math.round((now - cachedTimestamp) / 60);
|
||||
Logger.d("ColorSchemeDownload", "Loading cached schemes from ShellState (age:", ageMinutes, "minutes)");
|
||||
|
||||
if (cachedSchemes.length > 0) {
|
||||
availableSchemes = cachedSchemes;
|
||||
// Restore color cache from cached schemes
|
||||
for (var i = 0; i < cachedSchemes.length; i++) {
|
||||
var cachedScheme = cachedSchemes[i];
|
||||
if (cachedScheme.dark || cachedScheme.light) {
|
||||
schemeColorsCache[cachedScheme.name] = {
|
||||
"dark": cachedScheme.dark || null,
|
||||
"light": cachedScheme.light || null
|
||||
};
|
||||
}
|
||||
}
|
||||
hasInitialData = true;
|
||||
cacheVersion++;
|
||||
fetching = false;
|
||||
} else {
|
||||
// Cache is empty, only fetch if we haven't fetched recently
|
||||
const timeSinceLastFetch = now - lastApiFetchTime;
|
||||
if (timeSinceLastFetch >= minApiFetchInterval) {
|
||||
fetchAvailableSchemesFromAPI();
|
||||
} else {
|
||||
Logger.d("ColorSchemeDownload", "Cache empty but recent API call detected, skipping fetch");
|
||||
fetching = false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.e("ColorSchemeDownload", "Failed to load schemes from cache:", error);
|
||||
fetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
function saveSchemesToCache() {
|
||||
try {
|
||||
ShellState.setColorSchemesList({
|
||||
schemes: availableSchemes,
|
||||
timestamp: Time.timestamp
|
||||
});
|
||||
Logger.d("ColorSchemeDownload", "Schemes list saved to ShellState");
|
||||
} catch (error) {
|
||||
Logger.e("ColorSchemeDownload", "Failed to save schemes to cache:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function fetchAvailableSchemes() {
|
||||
if (fetching) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to load from ShellState cache first
|
||||
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
|
||||
loadSchemesFromCache();
|
||||
} else {
|
||||
// ShellState not ready, fetch directly from API
|
||||
fetchAvailableSchemesFromAPI();
|
||||
}
|
||||
}
|
||||
|
||||
function fetchAvailableSchemesFromAPI() {
|
||||
if (fetching) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we've fetched recently to prevent rapid repeated calls
|
||||
const now = Time.timestamp;
|
||||
const timeSinceLastFetch = now - lastApiFetchTime;
|
||||
if (timeSinceLastFetch < minApiFetchInterval) {
|
||||
Logger.d("ColorSchemeDownload", "Skipping API fetch - too soon since last fetch (", Math.round(timeSinceLastFetch), "s ago)");
|
||||
return;
|
||||
}
|
||||
|
||||
fetching = true;
|
||||
lastApiFetchTime = now;
|
||||
// Don't clear availableSchemes immediately to prevent flicker - keep showing old list while fetching
|
||||
// availableSchemes = [];
|
||||
downloadError = "";
|
||||
|
||||
// Fetch registry.json
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
fetching = false;
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var registry = JSON.parse(xhr.responseText);
|
||||
if (registry && registry.themes && Array.isArray(registry.themes)) {
|
||||
var schemes = [];
|
||||
// Process themes
|
||||
for (var i = 0; i < registry.themes.length; i++) {
|
||||
var theme = registry.themes[i];
|
||||
schemes.push({
|
||||
"name": theme.name,
|
||||
"path": theme.path,
|
||||
"dark": theme.dark || null,
|
||||
"light": theme.light || null
|
||||
});
|
||||
schemeColorsCache[theme.name] = {
|
||||
"dark": theme.dark || null,
|
||||
"light": theme.light || null
|
||||
};
|
||||
}
|
||||
availableSchemes = schemes;
|
||||
hasInitialData = true;
|
||||
cacheVersion++;
|
||||
Logger.d("ColorSchemeDownload", "Fetched", schemes.length, "available schemes from registry.json");
|
||||
// Save to cache
|
||||
saveSchemesToCache();
|
||||
} else {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-invalid-response");
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
} catch (e) {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-parse-failed", {
|
||||
"error": e.toString()
|
||||
});
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
} else if (xhr.status === 403) {
|
||||
// Rate limit hit - try to use cache if available
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-rate-limit");
|
||||
Logger.w("ColorSchemeDownload", downloadError);
|
||||
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
|
||||
const cacheData = ShellState.getColorSchemesList();
|
||||
const cachedSchemes = cacheData.schemes || [];
|
||||
if (cachedSchemes.length > 0) {
|
||||
availableSchemes = cachedSchemes;
|
||||
// Restore color cache from cached schemes
|
||||
for (var j = 0; j < cachedSchemes.length; j++) {
|
||||
var cachedScheme = cachedSchemes[j];
|
||||
if (cachedScheme.dark || cachedScheme.light) {
|
||||
schemeColorsCache[cachedScheme.name] = {
|
||||
"dark": cachedScheme.dark || null,
|
||||
"light": cachedScheme.light || null
|
||||
};
|
||||
}
|
||||
}
|
||||
hasInitialData = true;
|
||||
cacheVersion++;
|
||||
Logger.i("ColorSchemeDownload", "Using cached schemes due to rate limit");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-api-error", {
|
||||
"status": xhr.status
|
||||
});
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open("GET", "https://raw.githubusercontent.com/noctalia-dev/noctalia-colorschemes/main/registry.json");
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function downloadScheme(scheme) {
|
||||
if (downloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
downloading = true;
|
||||
downloadingScheme = scheme.name;
|
||||
downloadError = "";
|
||||
|
||||
Logger.i("ColorSchemeDownload", "Downloading scheme:", scheme.name);
|
||||
|
||||
// Use cached branch/SHA if available, otherwise fetch
|
||||
if (cachedBranchSha) {
|
||||
// Use cached SHA directly
|
||||
getSchemeTreeWithSha(scheme, cachedBranch, cachedBranchSha);
|
||||
} else if (cachedBranch) {
|
||||
// We have branch name, just need SHA
|
||||
getSchemeTree(scheme, cachedBranch);
|
||||
} else {
|
||||
// Need to fetch branch info first
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var repoInfo = JSON.parse(xhr.responseText);
|
||||
var defaultBranch = repoInfo.default_branch || "main";
|
||||
cachedBranch = defaultBranch;
|
||||
// Now get the tree for the scheme directory
|
||||
getSchemeTree(scheme, defaultBranch);
|
||||
} catch (e) {
|
||||
// Fallback: try to get files directly
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
} else {
|
||||
// Fallback: try to get files directly
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes");
|
||||
xhr.send();
|
||||
}
|
||||
}
|
||||
|
||||
function getSchemeTree(scheme, branch) {
|
||||
// First get the SHA of the branch
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var refResponse = JSON.parse(xhr.responseText);
|
||||
var sha = refResponse.object ? refResponse.object.sha : null;
|
||||
if (sha) {
|
||||
// Cache the SHA for future downloads
|
||||
cachedBranchSha = sha;
|
||||
// Now get the tree
|
||||
getSchemeTreeWithSha(scheme, branch, sha);
|
||||
} else {
|
||||
// Fallback to direct method
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to direct method
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
} else {
|
||||
// Fallback to direct method
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes/git/refs/heads/" + branch);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function getSchemeTreeWithSha(scheme, branch, sha) {
|
||||
// Use git trees API to get all files recursively
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
if (response.tree && Array.isArray(response.tree)) {
|
||||
// Filter files that belong to this scheme
|
||||
var files = [];
|
||||
for (var i = 0; i < response.tree.length; i++) {
|
||||
var item = response.tree[i];
|
||||
if (item.type === "blob" && item.path.startsWith(scheme.path + "/")) {
|
||||
files.push({
|
||||
"path": item.path,
|
||||
"url": "https://raw.githubusercontent.com/noctalia-dev/noctalia-colorschemes/" + branch + "/" + item.path,
|
||||
"name": item.path.split("/").pop()
|
||||
});
|
||||
}
|
||||
}
|
||||
downloadSchemeFiles(scheme.name, files);
|
||||
} else {
|
||||
// Fallback to direct method
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
} catch (e) {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-parse-failed", {
|
||||
"error": e.toString()
|
||||
});
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
} else {
|
||||
// Fallback to direct method
|
||||
getSchemeFilesDirect(scheme);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes/git/trees/" + sha + "?recursive=1");
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function getSchemeFilesDirect(scheme) {
|
||||
// Fallback: get files directly using contents API (non-recursive, but works)
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
if (Array.isArray(response)) {
|
||||
// Recursively get all files
|
||||
getAllFilesRecursive(scheme, response, []);
|
||||
} else {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-invalid-response");
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
} catch (e) {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-parse-failed", {
|
||||
"error": e.toString()
|
||||
});
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
} else {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-api-error", {
|
||||
"status": xhr.status
|
||||
});
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes/contents/" + scheme.path);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function getAllFilesRecursive(scheme, items, allFiles, callback) {
|
||||
if (!callback) {
|
||||
callback = function () {
|
||||
downloadSchemeFiles(scheme.name, allFiles);
|
||||
};
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var pending = 0;
|
||||
var completed = 0;
|
||||
|
||||
function checkComplete() {
|
||||
completed++;
|
||||
if (completed === items.length && pending === 0) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
if (item.type === "file") {
|
||||
allFiles.push({
|
||||
"path": item.path,
|
||||
"url": item.download_url,
|
||||
"name": item.name
|
||||
});
|
||||
checkComplete();
|
||||
} else if (item.type === "dir") {
|
||||
pending++;
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function (dirItem) {
|
||||
return function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
pending--;
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var dirResponse = JSON.parse(xhr.responseText);
|
||||
if (Array.isArray(dirResponse)) {
|
||||
getAllFilesRecursive(scheme, dirResponse, allFiles, function () {
|
||||
checkComplete();
|
||||
});
|
||||
} else {
|
||||
checkComplete();
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.e("ColorSchemeDownload", "Failed to parse directory response:", e);
|
||||
checkComplete();
|
||||
}
|
||||
} else {
|
||||
checkComplete();
|
||||
}
|
||||
}
|
||||
};
|
||||
}(item);
|
||||
xhr.open("GET", item.url);
|
||||
xhr.send();
|
||||
} else {
|
||||
checkComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function downloadSchemeFiles(schemeName, files) {
|
||||
if (files.length === 0) {
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-no-files");
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
return;
|
||||
}
|
||||
|
||||
var targetDir = ColorSchemeService.downloadedSchemesDirectory + "/" + schemeName;
|
||||
var downloadScript = "mkdir -p '" + targetDir + "'\n";
|
||||
|
||||
// Build download script for all files
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var file = files[i];
|
||||
var filePath = file.path;
|
||||
// Remove scheme name and leading / from path
|
||||
var relativePath = filePath;
|
||||
if (filePath.startsWith(schemeName + "/")) {
|
||||
relativePath = filePath.substring(schemeName.length + 1);
|
||||
} else if (filePath.startsWith("/" + schemeName + "/")) {
|
||||
relativePath = filePath.substring(schemeName.length + 2);
|
||||
}
|
||||
var localPath = targetDir + "/" + relativePath;
|
||||
var localDir = localPath.substring(0, localPath.lastIndexOf('/'));
|
||||
|
||||
downloadScript += "mkdir -p '" + localDir + "'\n";
|
||||
var downloadUrl = file.url || file.download_url;
|
||||
if (downloadUrl) {
|
||||
downloadScript += "curl -L -s -o '" + localPath + "' '" + downloadUrl + "' || wget -q -O '" + localPath + "' '" + downloadUrl + "'\n";
|
||||
}
|
||||
}
|
||||
|
||||
Logger.d("ColorSchemeDownload", "Downloading", files.length, "files for scheme", schemeName);
|
||||
|
||||
// Execute download script
|
||||
var stderrOutput = "";
|
||||
var downloadProcess = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
Process {
|
||||
id: downloadProcess
|
||||
command: ["sh", "-c", ` + JSON.stringify(downloadScript) + `]
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
Logger.e("ColorSchemeDownload", "Download stderr:", text);
|
||||
root.lastStderrOutput = text.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, root, "DownloadProcess_" + schemeName);
|
||||
|
||||
downloadProcess.exited.connect(function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
Logger.i("ColorSchemeDownload", "Scheme downloaded successfully:", schemeName);
|
||||
ToastService.showNotice(I18n.tr("panels.color-scheme.download-success-title"), I18n.tr("panels.color-scheme.download-success-description", {
|
||||
"scheme": schemeName
|
||||
}), "settings-color-scheme");
|
||||
// Set pending scheme to apply after reload
|
||||
pendingApplyScheme = schemeName;
|
||||
// Reload color schemes
|
||||
ColorSchemeService.loadColorSchemes();
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
} else {
|
||||
var errorDetails = "Exit code: " + exitCode;
|
||||
if (root.lastStderrOutput) {
|
||||
errorDetails += " - " + root.lastStderrOutput;
|
||||
}
|
||||
downloadError = I18n.tr("panels.color-scheme.download-error-download-failed", {
|
||||
"code": exitCode
|
||||
}) + "\n" + errorDetails;
|
||||
Logger.e("ColorSchemeDownload", downloadError);
|
||||
ToastService.showError(I18n.tr("panels.color-scheme.download-error-title"), I18n.tr("panels.color-scheme.download-error-description", {
|
||||
"scheme": schemeName
|
||||
}) + "\n" + errorDetails);
|
||||
// Clean up the partially downloaded directory on failure
|
||||
var cleanupScript = "rm -rf '" + targetDir + "'";
|
||||
var cleanupProcess = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
id: cleanupProcess
|
||||
command: ["sh", "-c", ` + JSON.stringify(cleanupScript) + `]
|
||||
}
|
||||
`, root, "CleanupProcess_" + schemeName);
|
||||
|
||||
cleanupProcess.exited.connect(function (cleanupExitCode) {
|
||||
if (cleanupExitCode === 0) {
|
||||
Logger.d("ColorSchemeDownload", "Partially downloaded scheme directory cleaned up:", targetDir);
|
||||
} else {
|
||||
Logger.w("ColorSchemeDownload", "Failed to clean up partially downloaded scheme directory:", targetDir);
|
||||
}
|
||||
downloading = false;
|
||||
downloadingScheme = "";
|
||||
cleanupProcess.destroy();
|
||||
});
|
||||
|
||||
cleanupProcess.running = true;
|
||||
}
|
||||
root.lastStderrOutput = "";
|
||||
downloadProcess.destroy();
|
||||
});
|
||||
|
||||
downloadProcess.running = true;
|
||||
}
|
||||
|
||||
function isSchemeInstalled(schemeName) {
|
||||
// Check if scheme already exists in ColorSchemeService
|
||||
for (var i = 0; i < ColorSchemeService.schemes.length; i++) {
|
||||
var path = ColorSchemeService.schemes[i];
|
||||
if (path.indexOf("/" + schemeName + "/") !== -1 || path.indexOf("/" + schemeName + ".json") !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isSchemeDownloaded(schemeName) {
|
||||
// Check if scheme is in the downloaded directory (not preinstalled)
|
||||
for (var i = 0; i < ColorSchemeService.schemes.length; i++) {
|
||||
var path = ColorSchemeService.schemes[i];
|
||||
if ((path.indexOf("/" + schemeName + "/") !== -1 || path.indexOf("/" + schemeName + ".json") !== -1) && path.indexOf(ColorSchemeService.downloadedSchemesDirectory) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function deleteScheme(schemeName) {
|
||||
if (downloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.i("ColorSchemeDownload", "Deleting scheme:", schemeName);
|
||||
|
||||
// Check if the deleted scheme is the currently active one
|
||||
var currentScheme = Settings.data.colorSchemes.predefinedScheme || "";
|
||||
var deletedSchemeDisplayName = ColorSchemeService.getBasename(schemeName);
|
||||
var needsReset = (currentScheme === deletedSchemeDisplayName);
|
||||
|
||||
// Only allow deleting downloaded schemes, not preinstalled ones
|
||||
var targetDir = ColorSchemeService.downloadedSchemesDirectory + "/" + schemeName;
|
||||
var deleteScript = "rm -rf '" + targetDir + "'";
|
||||
|
||||
var deleteProcess = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
id: deleteProcess
|
||||
command: ["sh", "-c", ` + JSON.stringify(deleteScript) + `]
|
||||
}
|
||||
`, root, "DeleteProcess_" + schemeName);
|
||||
|
||||
deleteProcess.exited.connect(function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
Logger.i("ColorSchemeDownload", "Scheme deleted successfully:", schemeName);
|
||||
ToastService.showNotice(I18n.tr("panels.color-scheme.delete-success-title"), I18n.tr("panels.color-scheme.delete-success-description", {
|
||||
"scheme": schemeName
|
||||
}), "settings-color-scheme");
|
||||
|
||||
// If the deleted scheme was the active one, reset to default BEFORE reloading
|
||||
if (needsReset) {
|
||||
Logger.i("ColorSchemeDownload", "Deleted scheme was active, resetting to Noctalia (default)");
|
||||
// Clear the setting immediately so ColorSchemeService won't try to apply the deleted scheme
|
||||
Settings.data.colorSchemes.predefinedScheme = "Noctalia (default)";
|
||||
// Apply the default scheme immediately
|
||||
ColorSchemeService.setPredefinedScheme("Noctalia (default)");
|
||||
}
|
||||
|
||||
// Reload color schemes
|
||||
ColorSchemeService.loadColorSchemes();
|
||||
} else {
|
||||
Logger.e("ColorSchemeDownload", "Delete failed with exit code:", exitCode);
|
||||
ToastService.showError(I18n.tr("panels.color-scheme.delete-error-title"), I18n.tr("panels.color-scheme.delete-error-description", {
|
||||
"scheme": schemeName
|
||||
}));
|
||||
}
|
||||
deleteProcess.destroy();
|
||||
});
|
||||
|
||||
deleteProcess.running = true;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ColorSchemeService
|
||||
function onScanningChanged() {
|
||||
// When scanning completes and we have a pending scheme, apply it
|
||||
if (!ColorSchemeService.scanning && pendingApplyScheme !== "") {
|
||||
var schemeToApply = pendingApplyScheme;
|
||||
pendingApplyScheme = ""; // Clear pending before applying
|
||||
|
||||
// Wait a tiny bit to ensure schemes array is updated
|
||||
applyTimer.schemeName = schemeToApply;
|
||||
applyTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: applyTimer
|
||||
property string schemeName: ""
|
||||
interval: 100 // Small delay to ensure schemes array is populated
|
||||
onTriggered: {
|
||||
if (schemeName !== "") {
|
||||
Logger.i("ColorSchemeDownload", "Auto-applying downloaded scheme:", schemeName);
|
||||
// Use setPredefinedScheme which will apply and set it as the current scheme
|
||||
ColorSchemeService.setPredefinedScheme(schemeName);
|
||||
schemeName = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
fetchAvailableSchemes();
|
||||
}
|
||||
|
||||
function preFetchSchemeColors() {
|
||||
if (availableSchemes.length > 0 && visible) {
|
||||
Qt.callLater(function () {
|
||||
// Just trigger a cache version update to ensure UI refreshes
|
||||
cacheVersion++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onAvailableSchemesChanged: preFetchSchemeColors()
|
||||
onVisibleChanged: {
|
||||
preFetchSchemeColors();
|
||||
|
||||
// Load schemes from ShellState when popup becomes visible
|
||||
if (visible) {
|
||||
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
|
||||
loadSchemesFromCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: typeof ShellState !== 'undefined' ? ShellState : null
|
||||
function onIsLoadedChanged() {
|
||||
if (root.visible && ShellState.isLoaded) {
|
||||
loadSchemesFromCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: contentColumn
|
||||
width: parent.width
|
||||
spacing: Style.marginL
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.color-scheme.download-title")
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
tooltipText: I18n.tr("common.refresh")
|
||||
enabled: !fetching && !downloading
|
||||
onClicked: {
|
||||
// Force refresh by clearing cache timestamp and fetching directly from API
|
||||
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
|
||||
ShellState.setColorSchemesList({
|
||||
schemes: [],
|
||||
timestamp: 0
|
||||
});
|
||||
}
|
||||
// Fetch directly from API to avoid cache check delay
|
||||
fetchAvailableSchemesFromAPI();
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Color.mOutline
|
||||
}
|
||||
|
||||
// Error message
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: errorText.implicitHeight + Style.marginM
|
||||
visible: downloadError !== ""
|
||||
color: Color.mError
|
||||
radius: Style.radiusS
|
||||
|
||||
NText {
|
||||
id: errorText
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
text: downloadError
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnError
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator - only show on initial load, not during refresh
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: fetching && !hasInitialData
|
||||
spacing: Style.marginM
|
||||
|
||||
NBusyIndicator {
|
||||
Layout.preferredWidth: 20
|
||||
Layout.preferredHeight: 20
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.color-scheme.download-fetching")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
// Schemes list - keep visible during refresh to prevent flicker
|
||||
NScrollView {
|
||||
id: schemesScrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 400
|
||||
visible: hasInitialData && availableSchemes.length > 0
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
gradientColor: Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
width: schemesScrollView.availableWidth
|
||||
spacing: Style.marginS
|
||||
|
||||
Repeater {
|
||||
model: availableSchemes
|
||||
|
||||
Rectangle {
|
||||
id: schemeItem
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 50 * Style.uiScaleRatio
|
||||
radius: Style.radiusS
|
||||
property string schemeName: modelData.name
|
||||
color: root.getSchemeColor(schemeName, "mSurface")
|
||||
border.width: Style.borderL
|
||||
border.color: hoverHandler.hovered ? root.getSchemeColor(schemeName, "mPrimary") : Color.mOutline
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.visible && !root.schemeColorsCache[schemeName]) {
|
||||
root.fetchSchemeColors(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: schemeRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Style.marginL
|
||||
anchors.rightMargin: Style.marginL
|
||||
spacing: Style.marginS
|
||||
|
||||
property string schemeName: modelData.name
|
||||
property int diameter: 16 * Style.uiScaleRatio
|
||||
property var colorKeys: ["mPrimary", "mSecondary", "mTertiary", "mError"]
|
||||
|
||||
NText {
|
||||
text: schemeRow.schemeName
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Color swatches
|
||||
Repeater {
|
||||
model: schemeRow.colorKeys
|
||||
Rectangle {
|
||||
width: schemeRow.diameter
|
||||
height: schemeRow.diameter
|
||||
radius: schemeRow.diameter * 0.5
|
||||
color: root.getSchemeColor(schemeRow.schemeName, modelData)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Download/Delete button
|
||||
NIconButton {
|
||||
property bool isDownloading: downloading && downloadingScheme === schemeRow.schemeName
|
||||
property bool isInstalled: root.isSchemeInstalled(schemeRow.schemeName)
|
||||
property bool isDownloaded: root.isSchemeDownloaded(schemeRow.schemeName)
|
||||
|
||||
icon: isDownloading ? "" : (isDownloaded ? "trash" : "download")
|
||||
tooltipText: isDownloading ? I18n.tr("panels.color-scheme.download-downloading") : (isDownloaded ? I18n.tr("common.delete") : I18n.tr("common.download"))
|
||||
enabled: !downloading
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: !isInstalled || isDownloaded // Show button only if not installed (can download) or if downloaded (can delete)
|
||||
onClicked: isDownloaded ? root.deleteScheme(schemeRow.schemeName) : root.downloadScheme(modelData)
|
||||
|
||||
NBusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
width: 16 * Style.uiScaleRatio
|
||||
height: 16 * Style.uiScaleRatio
|
||||
visible: parent.isDownloading
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginM
|
||||
visible: !fetching && availableSchemes.length === 0 && downloadError === ""
|
||||
|
||||
NIcon {
|
||||
icon: "package"
|
||||
pointSize: 48
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.color-scheme.download-empty")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user