// The contents of the quick settings popover — GNOME's system menu, in order: // toggle grid, detail lists, sliders, footer. // // Kept separate from QuickSettings.qml (the PanelWindow) so it can be dropped // into a FloatingWindow for testing: layer-shell surfaces cannot map outside a // wlroots compositor. import QtQuick import Quickshell import Quickshell.Widgets import Quickshell.Networking import Quickshell.Bluetooth import Quickshell.Services.Pipewire import qs.config import qs.services import qs.widgets Item { id: root implicitWidth: Theme.controlCenterWidth implicitHeight: content.implicitHeight + Theme.popoverPadding * 2 // "" | "wifi" | "vpn" | "bluetooth" | "sink" | "source". Only one detail // list is open at a time, so the panel never grows past the screen. property string expandedSection: "" function expand(name: string): void { root.expandedSection = root.expandedSection === name ? "" : name; } // Collapse everything — called when the popover closes so it reopens in a // known state. function reset(): void { root.expandedSection = ""; } // ── System state ──────────────────────────────────────────────────────── readonly property var wifiDevice: { for (const device of Networking.devices.values) { if (device.type === DeviceType.Wifi) return device; } return null; } readonly property var wiredDevice: { for (const device of Networking.devices.values) { if (device.type === DeviceType.Wired && device.connected) return device; } return null; } readonly property string ssid: { if (!root.wifiDevice || !root.wifiDevice.networks) return ""; for (const network of root.wifiDevice.networks.values) { if (network.connected) return network.name; } return ""; } readonly property var btAdapter: Bluetooth.defaultAdapter readonly property string btSummary: { if (!root.btAdapter) return "Unavailable"; if (!root.btAdapter.enabled) return "Off"; for (const device of Bluetooth.devices.values) { if (device.connected) return device.name || device.address; } return "On"; } readonly property int cellWidth: (content.width - Theme.itemSpacing) / 2 Column { id: content anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: Theme.popoverPadding spacing: Theme.itemSpacing // ── Toggle grid ───────────────────────────────────────────────────── Grid { columns: 2 spacing: Theme.itemSpacing Toggle { width: root.cellWidth icon: Networking.wifiEnabled ? "network-wireless-symbolic" : "network-wireless-offline-symbolic" label: "Wi-Fi" active: Networking.wifiEnabled enabled: Networking.wifiHardwareEnabled sublabel: { if (!Networking.wifiEnabled) return "Off"; return root.ssid !== "" ? root.ssid : "Not connected"; } onToggled: Networking.wifiEnabled = !Networking.wifiEnabled onExpanded: root.expand("wifi") } // Only present when there is a wired device at all -- most machines // this runs on have one, but a laptop without a dock does not, and // an Ethernet tile there would be a control for absent hardware. Toggle { width: root.cellWidth visible: Connectivity.wiredDevice !== null icon: Connectivity.wiredOn ? "network-wired-symbolic" : "network-wired-disconnected-symbolic" label: "Ethernet" active: Connectivity.wiredOn enabled: Connectivity.wiredAvailable sublabel: { if (Connectivity.wiredOn) return Connectivity.wiredDevice.linkSpeed > 0 ? Connectivity.wiredDevice.linkSpeed + " Mb/s" : "Connected"; return "Off"; } onToggled: Connectivity.setWired(!Connectivity.wiredOn) } // Only when a VPN profile is saved at all: a machine with none has // nothing to toggle, and the tile would be a control for absent // configuration -- same reasoning as the Ethernet tile above. Toggle { width: root.cellWidth visible: Vpn.available icon: "network-vpn-symbolic" label: "VPN" active: Vpn.anyActive enabled: !Vpn.busy sublabel: { if (Vpn.busy) return "Working…"; return Vpn.anyActive ? Vpn.activeSummary : "Off"; } onToggled: Vpn.toggle() onExpanded: root.expand("vpn") } Toggle { width: root.cellWidth icon: root.btAdapter && root.btAdapter.enabled ? "bluetooth-active-symbolic" : "bluetooth-disabled-symbolic" label: "Bluetooth" active: root.btAdapter !== null && root.btAdapter.enabled enabled: root.btAdapter !== null sublabel: root.btSummary onToggled: { if (root.btAdapter) root.btAdapter.enabled = !root.btAdapter.enabled; } onExpanded: root.expand("bluetooth") } Toggle { width: root.cellWidth icon: "preferences-desktop-screensaver-symbolic" label: "Caffeine" sublabel: Caffeine.enabled ? "Staying awake" : "Off" active: Caffeine.enabled onToggled: Caffeine.toggle() } // Do Not Disturb on its own, beside Presentation. The service, the // IPC verb and the settings row all existed; only the tile was // missing, so the one-press way to silence banners was a shortcut // you had to already know. Presentation keeps its combined role -- // this is the half of it people want without the awake half. Toggle { width: root.cellWidth icon: Notifs.doNotDisturb ? "notifications-disabled-symbolic" : "preferences-system-notifications-symbolic" label: "Do Not Disturb" sublabel: Notifs.doNotDisturb ? "Banners held" : "Off" active: Notifs.doNotDisturb onToggled: Notifs.doNotDisturb = !Notifs.doNotDisturb } // Caffeine plus Do Not Disturb as one switch, for the projector: // the half you forget to arm is the one that fires a message // preview onto the big screen. Restores both exactly as found. Toggle { width: root.cellWidth icon: "video-display-symbolic" label: "Presentation" sublabel: PresentationMode.active ? "Awake · notifications held" : "Off" active: PresentationMode.active onToggled: PresentationMode.toggle() } Toggle { width: root.cellWidth icon: ColorScheme.dark ? "weather-clear-night-symbolic" : "weather-clear-symbolic" label: "Appearance" sublabel: ColorScheme.dark ? "Dark" : "Light" // "active" reads as the non-default state across this grid, and // dark is what Panama ships, so light is the lit one. active: !ColorScheme.dark // Writes the preference and stops. ColorScheme propagates it to // GTK, the portal, the terminals, the launcher, btop, tmux and // the lock screen; nothing here needs to know that list. onToggled: SystemSettings.commitPreference("colorScheme", ColorScheme.dark ? "light" : "dark") } Toggle { width: root.cellWidth icon: "night-light-symbolic" label: "Night Light" active: NightLight.active sublabel: { if (NightLight.automatic) return NightLight.active ? "Scheduled · on" : "Scheduled"; return NightLight.active ? NightLight.temperature + "K" : "Off"; } onToggled: NightLight.toggle() // No detail list to open, so the secondary action flips the // schedule on and off instead. onExpanded: NightLight.automatic = !NightLight.automatic } } RowButton { width: content.width visible: Settings.ccShowFocus icon: "preferences-system-time-symbolic" iconFallback: "appointment-soon-symbolic" label: FocusSession.active ? "Focus · " + FocusSession.workspaceLabel : "Start focus session" sublabel: FocusSession.active ? FocusSession.remainingText + (FocusSession.paused ? " · paused" : " remaining") : Settings.focusDurationMinutes + " minutes · current workspace" selected: FocusSession.active onClicked: { ShellState.close(); FocusSession.reveal(); } } // ── Detail lists ──────────────────────────────────────────────────── Section { width: content.width expanded: root.expandedSection === "wifi" WifiList { anchors.left: parent.left anchors.right: parent.right device: root.wifiDevice active: root.expandedSection === "wifi" } } Section { width: content.width expanded: root.expandedSection === "vpn" VpnList { anchors.left: parent.left anchors.right: parent.right } } Section { width: content.width expanded: root.expandedSection === "bluetooth" BluetoothList { anchors.left: parent.left anchors.right: parent.right active: root.expandedSection === "bluetooth" } } Rectangle { width: content.width height: 1 color: Theme.alpha(Theme.fg, 0.1) } // ── Power ─────────────────────────────────────────────────────────── // Only where a power-profiles daemon is actually running. A desktop // without one should not show a control that cannot do anything. RowButton { visible: PowerProfiles.available width: content.width icon: { switch (PowerProfiles.active) { case "power-saver": return "power-profile-power-saver-symbolic"; case "performance": return "power-profile-performance-symbolic"; default: return "power-profile-balanced-symbolic"; } } iconFallback: "preferences-system-power-symbolic" label: "Power profile" sublabel: PowerProfiles.degraded !== "" ? PowerProfiles.label(PowerProfiles.active) + " · limited" : PowerProfiles.label(PowerProfiles.active) selected: root.expandedSection === "power" onClicked: root.expand("power") } Section { width: content.width expanded: root.expandedSection === "power" PowerProfileList { anchors.left: parent.left anchors.right: parent.right } } // ── Sliders ───────────────────────────────────────────────────────── AudioSlider { width: content.width node: Pipewire.defaultAudioSink output: true expanded: root.expandedSection === "sink" onToggleExpanded: root.expand("sink") } Section { width: content.width expanded: root.expandedSection === "sink" AudioDeviceList { anchors.left: parent.left anchors.right: parent.right output: true } } AudioSlider { width: content.width node: Pipewire.defaultAudioSource output: false expanded: root.expandedSection === "source" onToggleExpanded: root.expand("source") } Section { width: content.width expanded: root.expandedSection === "source" AudioDeviceList { anchors.left: parent.left anchors.right: parent.right output: false } } BrightnessControl { width: content.width } // ── Connected life ───────────────────────────────────────────────── // Hidden sections leave no gap: Column skips invisible children // outright, so the panel closes up around them. HomeControls { width: content.width visible: Settings.ccShowHome expanded: root.expandedSection === "home" onToggleExpanded: root.expand("home") } PhoneControls { width: content.width visible: Settings.ccShowPhone expanded: root.expandedSection === "phone" onToggleExpanded: root.expand("phone") } Rectangle { width: content.width height: 1 color: Theme.alpha(Theme.fg, 0.1) } // ── Footer ────────────────────────────────────────────────────────── Item { width: content.width height: 36 // The account's own picture, the same file the lock screen and the // login screen read. A generic glyph sat here while a real avatar // was already set, which made the desktop look like it did not know // whose it was. Item { id: avatar anchors.left: parent.left anchors.leftMargin: 4 anchors.verticalCenter: parent.verticalCenter width: 22 height: 22 ClippingRectangle { anchors.fill: parent radius: width / 2 color: "transparent" visible: UserAccounts.avatarUrl !== "" Image { anchors.fill: parent source: UserAccounts.avatarUrl fillMode: Image.PreserveAspectCrop // Replaced in place when the picture changes, so the // cache has to be told to let go of the old one. cache: false asynchronous: true sourceSize.width: 44 sourceSize.height: 44 } } ThemedIcon { anchors.centerIn: parent visible: UserAccounts.avatarUrl === "" size: 20 icon: "avatar-default-symbolic" iconFallback: "user-info-symbolic" } } Text { anchors.left: avatar.right anchors.leftMargin: 10 anchors.right: actions.left anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter text: UserAccounts.me ? UserAccounts.displayName(UserAccounts.me) : (Quickshell.env("USER") || "user") color: Theme.fg elide: Text.ElideRight font.family: Theme.fontFamily font.pixelSize: Theme.fontSize font.weight: Font.Medium } Row { id: actions anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 4 IconButton { size: 32 iconSize: 17 icon: "preferences-system-symbolic" onClicked: { ShellState.close(); ShellState.openSettings("home"); } } IconButton { size: 32 iconSize: 17 icon: "system-shutdown-symbolic" onClicked: ShellState.open("powermenu") } } } } }