This commit is contained in:
2026-05-28 21:45:21 +02:00
parent 653439ac24
commit a356a3d369
610 changed files with 205679 additions and 0 deletions
@@ -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
}
}
}
}
}
@@ -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);
}
}
}
}
@@ -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
}
@@ -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);
}
}
}
@@ -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();
}
}
}
}
@@ -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
}
@@ -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()
}
@@ -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);
}
}
}
@@ -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();
}
}
@@ -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()
}
@@ -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
}
@@ -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()
}
@@ -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()
}