ControlCenter: new layout with all shortcuts on top for quicker access.

This commit is contained in:
ItsLemmy
2025-10-06 23:27:08 -04:00
parent 7989bcf93b
commit 6ae8db3068
11 changed files with 380 additions and 295 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ Item {
width: root.barSlotWidth * 0.5 // Creates a small gap between bars
height: Math.max(1, root.height * amp)
x: index * root.barSlotWidth
x: index * root.barSlotWidth + (root.barSlotWidth * 0.25)
y: root.height - height
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ Item {
width: root.barSlotWidth * 0.8 // Creates a small gap between bars
height: Math.max(1, barHeight)
x: index * root.barSlotWidth
x: index * root.barSlotWidth + (root.barSlotWidth * 0.25)
y: root.centerY - (barHeight / 2)
}
}
+1 -1
View File
@@ -151,7 +151,7 @@ NBox {
model: 3
Rectangle {
anchors.centerIn: parent
width: parent.width * (1.0+ index * 0.2)
width: parent.width * (1.0 + index * 0.2)
height: width
radius: width / 2
color: "transparent"
@@ -1,66 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.UPower
import qs.Commons
import qs.Services
import qs.Widgets
// Power Profiles: performance, balanced, eco
NBox {
property real spacing: 0
// Centralized service
readonly property bool hasPP: PowerProfileService.available
RowLayout {
id: powerRow
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: spacing
Item {
Layout.fillWidth: true
}
// Performance
NIconButton {
icon: PowerProfileService.getIcon(PowerProfile.Performance)
tooltipText: I18n.tr("tooltips.set-power-profile", {
"profile": PowerProfileService.getName(PowerProfile.Performance)
})
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mOnPrimary : Color.mPrimary
onClicked: PowerProfileService.setProfile(PowerProfile.Performance)
}
// Balanced
NIconButton {
icon: PowerProfileService.getIcon(PowerProfile.Balanced)
tooltipText: I18n.tr("tooltips.set-power-profile", {
"profile": PowerProfileService.getName(PowerProfile.Balanced)
})
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnPrimary : Color.mPrimary
onClicked: PowerProfileService.setProfile(PowerProfile.Balanced)
}
// Eco
NIconButton {
icon: PowerProfileService.getIcon(PowerProfile.PowerSaver)
tooltipText: I18n.tr("tooltips.set-power-profile", {
"profile": PowerProfileService.getName(PowerProfile.PowerSaver)
})
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mOnPrimary : Color.mPrimary
onClicked: PowerProfileService.setProfile(PowerProfile.PowerSaver)
}
Item {
Layout.fillWidth: true
}
}
}
-113
View File
@@ -1,113 +0,0 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Modules.Settings
import qs.Modules.ControlCenter
import qs.Commons
import qs.Services
import qs.Widgets
// Header card with avatar, user and quick actions
NBox {
id: root
property string uptimeText: "--"
RowLayout {
id: content
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
NImageCircled {
width: Style.baseWidgetSize * 1.25 * scaling
height: Style.baseWidgetSize * 1.25 * scaling
imagePath: Settings.data.general.avatarImage
fallbackIcon: "person"
borderColor: Color.mPrimary
borderWidth: Math.max(1, Style.borderM * scaling)
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS * scaling
NText {
text: Quickshell.env("USER") || "user"
font.weight: Style.fontWeightBold
font.capitalization: Font.Capitalize
}
NText {
text: I18n.tr("system.uptime", {
"uptime": uptimeText
})
pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
}
}
RowLayout {
spacing: Style.marginS * scaling
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Item {
Layout.fillWidth: true
}
NIconButton {
icon: "settings"
tooltipText: I18n.tr("tooltips.open-settings")
onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.General
settingsPanel.open()
}
}
NIconButton {
icon: "power"
tooltipText: I18n.tr("tooltips.session-menu")
onClicked: {
sessionMenuPanel.open()
controlCenterPanel.close()
}
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
onClicked: {
controlCenterPanel.close()
}
}
}
}
// ----------------------------------
// Uptime
Timer {
interval: 60000
repeat: true
running: true
onTriggered: uptimeProcess.running = true
}
Process {
id: uptimeProcess
command: ["cat", "/proc/uptime"]
running: true
stdout: StdioCollector {
onStreamFinished: {
var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0])
uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds)
uptimeProcess.running = false
}
}
}
function updateSystemInfo() {
uptimeProcess.running = true
}
}
+322
View File
@@ -0,0 +1,322 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import Quickshell.Services.UPower
import qs.Modules.Settings
import qs.Modules.ControlCenter
import qs.Commons
import qs.Services
import qs.Widgets
// Header card with avatar, user and quick actions
NBox {
id: root
property string uptimeText: "--"
property real spacing: Style.marginS * scaling
readonly property bool hasPP: PowerProfileService.available
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginM * scaling
// Profile, Uptime, Settings, SessionMenu, Close
RowLayout {
id: content
spacing: root.spacing
NImageCircled {
width: Style.baseWidgetSize * 1.25 * scaling
height: Style.baseWidgetSize * 1.25 * scaling
imagePath: Settings.data.general.avatarImage
fallbackIcon: "person"
borderColor: Color.mPrimary
borderWidth: Math.max(1, Style.borderM * scaling)
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS * scaling
NText {
text: Quickshell.env("USER") || "user"
font.weight: Style.fontWeightBold
font.capitalization: Font.Capitalize
}
NText {
text: I18n.tr("system.uptime", {
"uptime": uptimeText
})
pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
}
}
Item {
Layout.fillWidth: true
}
RowLayout {
spacing: root.spacing
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
NIconButton {
baseSize: Style.baseWidgetSize * 0.9
icon: "settings"
tooltipText: I18n.tr("tooltips.open-settings")
onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.General
settingsPanel.open()
}
}
NIconButton {
baseSize: Style.baseWidgetSize * 0.9
icon: "power"
tooltipText: I18n.tr("tooltips.session-menu")
onClicked: {
sessionMenuPanel.open()
controlCenterPanel.close()
}
}
NIconButton {
baseSize: Style.baseWidgetSize * 0.9
icon: "close"
tooltipText: I18n.tr("tooltips.close")
onClicked: {
controlCenterPanel.close()
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginS * scaling
Layout.bottomMargin: Style.marginS * scaling
}
RowLayout {
id: utilitiesRow
spacing: Style.marginM * scaling
// Wallpaper, Screen Rec.
RowLayout {
spacing: root.spacing
// Screen Recorder
NIconButton {
baseSize: Style.baseWidgetSize * 0.8
icon: "camera-video"
enabled: ScreenRecorderService.isAvailable
tooltipText: ScreenRecorderService.isAvailable ? (ScreenRecorderService.isRecording ? I18n.tr("tooltips.stop-screen-recording") : I18n.tr("tooltips.start-screen-recording")) : I18n.tr("tooltips.screen-recorder-not-installed")
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary
onClicked: {
if (!ScreenRecorderService.isAvailable)
return
ScreenRecorderService.toggleRecording()
// If we were not recording and we just initiated a start, close the panel
if (!ScreenRecorderService.isRecording) {
var panel = PanelService.getPanel("controlCenterPanel")
panel?.close()
}
}
}
// Wallpaper
NIconButton {
baseSize: Style.baseWidgetSize * 0.8
visible: Settings.data.wallpaper.enabled
icon: "wallpaper-selector"
tooltipText: I18n.tr("tooltips.wallpaper-selector")
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
onRightClicked: WallpaperService.setRandomWallpaper()
}
}
NDivider {
Layout.fillWidth: true
}
// Network
RowLayout {
spacing: root.spacing
// Wifi
NIconButton {
id: wifiButton
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("tooltips.manage-wifi")
icon: {
try {
if (NetworkService.ethernetConnected) {
return "ethernet"
}
let connected = false
let signalStrength = 0
for (const net in NetworkService.networks) {
if (NetworkService.networks[net].connected) {
connected = true
signalStrength = NetworkService.networks[net].signal
break
}
}
return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off"
} catch (error) {
Logger.error("Wi-Fi", "Error getting icon:", error)
return "signal_wifi_bad"
}
}
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
}
// Bluetooth
NIconButton {
baseSize: Style.baseWidgetSize * 0.8
tooltipText: I18n.tr("tooltips.bluetooth-devices")
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
}
}
NDivider {
Layout.fillWidth: true
}
// NightLight, Keep-Awake
RowLayout {
spacing: root.spacing
// Night Light
NIconButton {
baseSize: Style.baseWidgetSize * 0.8
colorBg: Settings.data.nightLight.forced ? Color.mPrimary : Color.transparent
colorFg: Settings.data.nightLight.forced ? Color.mOnPrimary : Color.mPrimary
icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
tooltipText: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? I18n.tr("tooltips.night-light-forced") : I18n.tr("tooltips.night-light-enabled")) : I18n.tr("tooltips.night-light-disabled")
onClicked: {
// Check if wlsunset is available before enabling night light
if (!ProgramCheckerService.wlsunsetAvailable) {
ToastService.showWarning(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.not-installed"))
return
}
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")
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
}
}
// Idle Inhibitor
NIconButton {
baseSize: Style.baseWidgetSize * 0.8
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.disable-keep-awake") : I18n.tr("tooltips.enable-keep-awake")
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
onClicked: {
IdleInhibitorService.manualToggle()
}
}
}
NDivider {
Layout.fillWidth: true
}
RowLayout {
spacing: root.spacing
// Performance
NIconButton {
baseSize: Style.baseWidgetSize * 0.8
icon: PowerProfileService.getIcon(PowerProfile.Performance)
tooltipText: I18n.tr("tooltips.set-power-profile", {
"profile": PowerProfileService.getName(PowerProfile.Performance)
})
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mOnPrimary : Color.mPrimary
onClicked: PowerProfileService.setProfile(PowerProfile.Performance)
}
// Balanced
NIconButton {
baseSize: Style.baseWidgetSize * 0.8
icon: PowerProfileService.getIcon(PowerProfile.Balanced)
tooltipText: I18n.tr("tooltips.set-power-profile", {
"profile": PowerProfileService.getName(PowerProfile.Balanced)
})
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnPrimary : Color.mPrimary
onClicked: PowerProfileService.setProfile(PowerProfile.Balanced)
}
// Eco
NIconButton {
baseSize: Style.baseWidgetSize * 0.8
icon: PowerProfileService.getIcon(PowerProfile.PowerSaver)
tooltipText: I18n.tr("tooltips.set-power-profile", {
"profile": PowerProfileService.getName(PowerProfile.PowerSaver)
})
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mOnPrimary : Color.mPrimary
onClicked: PowerProfileService.setProfile(PowerProfile.PowerSaver)
}
}
}
}
// ----------------------------------
// Uptime
Timer {
interval: 60000
repeat: true
running: true
onTriggered: uptimeProcess.running = true
}
Process {
id: uptimeProcess
command: ["cat", "/proc/uptime"]
running: true
stdout: StdioCollector {
onStreamFinished: {
var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0])
uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds)
uptimeProcess.running = false
}
}
}
function updateSystemInfo() {
uptimeProcess.running = true
}
}
@@ -1,66 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Settings
import qs.Services
import qs.Widgets
// Utilities: record & wallpaper
NBox {
property real spacing: 0
RowLayout {
id: utilRow
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: spacing
Item {
Layout.fillWidth: true
}
// Screen Recorder
NIconButton {
icon: "camera-video"
enabled: ScreenRecorderService.isAvailable
tooltipText: ScreenRecorderService.isAvailable ? (ScreenRecorderService.isRecording ? I18n.tr("tooltips.stop-screen-recording") : I18n.tr("tooltips.start-screen-recording")) : I18n.tr("tooltips.screen-recorder-not-installed")
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary
onClicked: {
if (!ScreenRecorderService.isAvailable)
return
ScreenRecorderService.toggleRecording()
// If we were not recording and we just initiated a start, close the panel
if (!ScreenRecorderService.isRecording) {
var panel = PanelService.getPanel("controlCenterPanel")
panel?.close()
}
}
}
// Idle Inhibitor
NIconButton {
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.disable-keep-awake") : I18n.tr("tooltips.enable-keep-awake")
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
onClicked: {
IdleInhibitorService.manualToggle()
}
}
// Wallpaper
NIconButton {
visible: Settings.data.wallpaper.enabled
icon: "wallpaper-selector"
tooltipText: I18n.tr("tooltips.wallpaper-selector")
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
onRightClicked: WallpaperService.setRandomWallpaper()
}
Item {
Layout.fillWidth: true
}
}
}
+1 -1
View File
@@ -85,7 +85,7 @@ NBox {
model: weatherReady ? LocationService.data.weather.daily.time : []
delegate: ColumnLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginL * scaling
spacing: Style.marginS * scaling
NText {
text: {
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"))
+8 -28
View File
@@ -11,7 +11,7 @@ NPanel {
id: root
preferredWidth: 460
preferredHeight: 734
preferredHeight: 688
panelKeyboardFocus: true
// Positioning
@@ -36,21 +36,22 @@ NPanel {
width: parent.width - (2 * content.cardSpacing)
spacing: content.cardSpacing
// Cards (consistent inter-card spacing via ColumnLayout spacing)
ProfileCard {
// Top Card: profile + utilities
TopCard {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(64 * scaling)
Layout.preferredHeight: Math.max(124 * scaling)
}
// Weather
WeatherCard {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(220 * scaling)
Layout.preferredHeight: Math.max(196 * scaling)
}
// Middle section: media + stats column
// Media + stats column
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(310 * scaling)
Layout.preferredHeight: Math.max(304 * scaling)
spacing: content.cardSpacing
// Media card
@@ -65,27 +66,6 @@ NPanel {
Layout.fillHeight: true
}
}
// Bottom actions (two grouped rows of round buttons)
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(60 * scaling)
spacing: content.cardSpacing
// Power Profiles switcher
PowerProfilesCard {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: content.cardSpacing
}
// Utilities buttons
UtilitiesCard {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: content.cardSpacing
}
}
}
}
}
+19 -1
View File
@@ -7,5 +7,23 @@ import qs.Services
Rectangle {
width: parent.width
height: Math.max(1, Style.borderS * scaling)
color: Color.mOutline
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0.0
color: Color.transparent
}
GradientStop {
position: 0.1
color: Color.mOutline
}
GradientStop {
position: 0.9
color: Color.mOutline
}
GradientStop {
position: 1.0
color: Color.transparent
}
}
}
+27 -17
View File
@@ -87,6 +87,8 @@ Loader {
root.buttonItem = buttonItem
root.buttonName = buttonName || ""
setPosition()
PanelService.willOpenPanel(root)
backgroundClickEnabled = true
@@ -106,6 +108,27 @@ Loader {
PanelService.closedPanel(root)
}
// -----------------------------------------
function setPosition() {
// If we have a button name, we are landing here from an IPC call.
// IPC calls have no idead on which screen they panel will spawn.
// Resolve the button name to a proper button item now that we have a screen.
if (buttonName !== "" && root.screen !== null) {
buttonItem = BarService.lookupWidget(buttonName, root.screen.name)
}
// Get the button position if provided
if (buttonItem !== undefined && buttonItem !== null) {
useButtonPosition = true
var itemPos = buttonItem.mapToItem(null, 0, 0)
buttonPosition = Qt.point(itemPos.x, itemPos.y)
buttonWidth = buttonItem.width
buttonHeight = buttonItem.height
} else {
useButtonPosition = false
}
}
// -----------------------------------------
sourceComponent: Component {
PanelWindow {
@@ -143,24 +166,11 @@ Loader {
panelContentLoader.active = false
panelContentLoader.active = true
// If we have a button name, we are landing here from an IPC call.
// IPC calls have no idead on which screen they panel will spawn.
// Resolve the button name to a proper button item now that we have a screen.
if (buttonName !== "") {
buttonItem = BarService.lookupWidget(buttonName, root.screen.name)
// If called from IPC always reposition if screen is updated
if (buttonName) {
setPosition()
}
// Get the button position if provided
if (buttonItem !== undefined && buttonItem !== null) {
useButtonPosition = true
var itemPos = buttonItem.mapToItem(null, 0, 0)
buttonPosition = Qt.point(itemPos.x, itemPos.y)
buttonWidth = buttonItem.width
buttonHeight = buttonItem.height
} else {
useButtonPosition = false
}
//Logger.log("NPanel", "OnScreenChanged", root.screen.name)
// Logger.log("NPanel", "OnScreenChanged", root.screen.name)
}
}