1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

Compare commits

...

3 Commits

Author SHA1 Message Date
bbedward
983d185de6 Explicitly load all modals 2025-09-25 23:12:28 -04:00
bbedward
066d1847e4 cc: fixes to edit mode 2025-09-25 21:37:32 -04:00
purian23
3155bf24c1 Revert "refactor: Add Drag & Drop to Control Center"
This reverts commit a207ed74ff.
2025-09-25 20:59:22 -04:00
13 changed files with 771 additions and 1386 deletions

373
IPC.qml Normal file
View File

@@ -0,0 +1,373 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import qs.Common
import qs.Services
Item {
id: root
property var powermenu: null
property var processlist: null
property var controlCenter: null
property var dash: null
property var notepadVariants: null
property var spotlight: null
property var clipboard: null
property var notifications: null
property var settings: null
function getFocusedScreenName() {
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
return Hyprland.focusedWorkspace.monitor.name
}
if (CompositorService.isNiri && NiriService.currentOutput) {
return NiriService.currentOutput
}
return ""
}
function getActiveNotepadInstance() {
if (!notepadVariants || notepadVariants.instances.length === 0) {
return null
}
if (notepadVariants.instances.length === 1) {
return notepadVariants.instances[0]
}
var focusedScreen = getFocusedScreenName()
if (focusedScreen && notepadVariants.instances.length > 0) {
for (var i = 0; i < notepadVariants.instances.length; i++) {
var slideout = notepadVariants.instances[i]
if (slideout.modelData && slideout.modelData.name === focusedScreen) {
return slideout
}
}
}
for (var i = 0; i < notepadVariants.instances.length; i++) {
var slideout = notepadVariants.instances[i]
if (slideout.isVisible) {
return slideout
}
}
return notepadVariants.instances[0]
}
IpcHandler {
function open() {
powermenu.active = true
if (powermenu.item)
powermenu.item.open()
return "POWERMENU_OPEN_SUCCESS"
}
function close() {
if (powermenu.item) {
powermenu.item.close()
powermenu.active = false
}
return "POWERMENU_CLOSE_SUCCESS"
}
function toggle() {
powermenu.active = true
if (powermenu.item)
powermenu.item.toggle()
return "POWERMENU_TOGGLE_SUCCESS"
}
target: "powermenu"
}
IpcHandler {
function open(): string {
processlist.active = true
if (processlist.item)
processlist.item.show()
return "PROCESSLIST_OPEN_SUCCESS"
}
function close(): string {
if (processlist.item) {
processlist.item.hide()
processlist.active = false
}
return "PROCESSLIST_CLOSE_SUCCESS"
}
function toggle(): string {
processlist.active = true
if (processlist.item)
processlist.item.toggle()
return "PROCESSLIST_TOGGLE_SUCCESS"
}
target: "processlist"
}
IpcHandler {
function open(): string {
controlCenter.active = true
if (controlCenter.item) {
controlCenter.item.open()
return "CONTROL_CENTER_OPEN_SUCCESS"
}
return "CONTROL_CENTER_OPEN_FAILED"
}
function close(): string {
if (controlCenter.item) {
controlCenter.item.close()
controlCenter.active = false
return "CONTROL_CENTER_CLOSE_SUCCESS"
}
return "CONTROL_CENTER_CLOSE_FAILED"
}
function toggle(): string {
controlCenter.active = true
if (controlCenter.item) {
controlCenter.item.toggle()
return "CONTROL_CENTER_TOGGLE_SUCCESS"
}
return "CONTROL_CENTER_TOGGLE_FAILED"
}
target: "control-center"
}
IpcHandler {
function open(tab: string): string {
dash.active = true
if (dash.item) {
switch (tab.toLowerCase()) {
case "media":
dash.item.currentTabIndex = 1
break
case "weather":
dash.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
break
default:
dash.item.currentTabIndex = 0
break
}
dash.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
dash.item.dashVisible = true
return "DASH_OPEN_SUCCESS"
}
return "DASH_OPEN_FAILED"
}
function close(): string {
if (dash.item) {
dash.item.dashVisible = false
dash.active = false
return "DASH_CLOSE_SUCCESS"
}
return "DASH_CLOSE_FAILED"
}
function toggle(tab: string): string {
dash.active = true
if (dash.item) {
if (dash.item.dashVisible) {
dash.item.dashVisible = false
} else {
switch (tab.toLowerCase()) {
case "media":
dash.item.currentTabIndex = 1
break
case "weather":
dash.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
break
default:
dash.item.currentTabIndex = 0
break
}
dash.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
dash.item.dashVisible = true
}
return "DASH_TOGGLE_SUCCESS"
}
return "DASH_TOGGLE_FAILED"
}
target: "dash"
}
IpcHandler {
function open(): string {
var instance = getActiveNotepadInstance()
if (instance) {
instance.show()
return "NOTEPAD_OPEN_SUCCESS"
}
return "NOTEPAD_OPEN_FAILED"
}
function close(): string {
var instance = getActiveNotepadInstance()
if (instance) {
instance.hide()
return "NOTEPAD_CLOSE_SUCCESS"
}
return "NOTEPAD_CLOSE_FAILED"
}
function toggle(): string {
var instance = getActiveNotepadInstance()
if (instance) {
instance.toggle()
return "NOTEPAD_TOGGLE_SUCCESS"
}
return "NOTEPAD_TOGGLE_FAILED"
}
target: "notepad"
}
IpcHandler {
function open(): string {
spotlight.active = true
if (spotlight.item) {
spotlight.item.show()
return "SPOTLIGHT_OPEN_SUCCESS"
}
return "SPOTLIGHT_OPEN_FAILED"
}
function close(): string {
if (spotlight.item) {
spotlight.item.hide()
spotlight.active = false
return "SPOTLIGHT_CLOSE_SUCCESS"
}
return "SPOTLIGHT_CLOSE_FAILED"
}
function toggle(): string {
spotlight.active = true
if (spotlight.item) {
spotlight.item.toggle()
return "SPOTLIGHT_TOGGLE_SUCCESS"
}
return "SPOTLIGHT_TOGGLE_FAILED"
}
target: "spotlight"
}
IpcHandler {
function open(): string {
clipboard.active = true
if (clipboard.item) {
clipboard.item.show()
return "CLIPBOARD_OPEN_SUCCESS"
}
return "CLIPBOARD_OPEN_FAILED"
}
function close(): string {
if (clipboard.item) {
clipboard.item.hide()
clipboard.active = false
return "CLIPBOARD_CLOSE_SUCCESS"
}
return "CLIPBOARD_CLOSE_FAILED"
}
function toggle(): string {
clipboard.active = true
if (clipboard.item) {
clipboard.item.toggle()
return "CLIPBOARD_TOGGLE_SUCCESS"
}
return "CLIPBOARD_TOGGLE_FAILED"
}
target: "clipboard"
}
IpcHandler {
function open(): string {
notifications.active = true
if (notifications.item) {
notifications.item.show()
return "NOTIFICATION_MODAL_OPEN_SUCCESS"
}
return "NOTIFICATION_MODAL_OPEN_FAILED"
}
function close(): string {
if (notifications.item) {
notifications.item.hide()
notifications.active = false
return "NOTIFICATION_MODAL_CLOSE_SUCCESS"
}
return "NOTIFICATION_MODAL_CLOSE_FAILED"
}
function toggle(): string {
notifications.active = true
if (notifications.item) {
notifications.item.toggle()
return "NOTIFICATION_MODAL_TOGGLE_SUCCESS"
}
return "NOTIFICATION_MODAL_TOGGLE_FAILED"
}
target: "notifications"
}
IpcHandler {
function open(): string {
settings.active = true
if (settings.item) {
settings.item.show()
return "SETTINGS_OPEN_SUCCESS"
}
return "SETTINGS_OPEN_FAILED"
}
function close(): string {
if (settings.item) {
settings.item.hide()
settings.active = false
return "SETTINGS_CLOSE_SUCCESS"
}
return "SETTINGS_CLOSE_FAILED"
}
function toggle(): string {
settings.active = true
if (settings.item) {
settings.item.toggle()
return "SETTINGS_TOGGLE_SUCCESS"
}
return "SETTINGS_TOGGLE_FAILED"
}
target: "settings"
}
IpcHandler {
function browse(type: string) {
settings.active = true
if (settings.item) {
if (type === "wallpaper") {
settings.item.wallpaperBrowser.allowStacking = false
settings.item.wallpaperBrowser.open()
} else if (type === "profile") {
settings.item.profileBrowser.allowStacking = false
settings.item.profileBrowser.open()
}
}
}
target: "file"
}
}

View File

@@ -187,24 +187,6 @@ DankModal {
filteredClipboardModel: filteredClipboardModel filteredClipboardModel: filteredClipboardModel
} }
IpcHandler {
function open(): string {
clipboardHistoryModal.show()
return "CLIPBOARD_OPEN_SUCCESS"
}
function close(): string {
clipboardHistoryModal.hide()
return "CLIPBOARD_CLOSE_SUCCESS"
}
function toggle(): string {
clipboardHistoryModal.toggle()
return "CLIPBOARD_TOGGLE_SUCCESS"
}
target: "clipboard"
}
clipboardContent: Component { clipboardContent: Component {
ClipboardContent { ClipboardContent {

View File

@@ -71,24 +71,6 @@ DankModal {
onClose: () => notificationModal.hide() onClose: () => notificationModal.hide()
} }
IpcHandler {
function open(): string {
notificationModal.show();
return "NOTIFICATION_MODAL_OPEN_SUCCESS";
}
function close(): string {
notificationModal.hide();
return "NOTIFICATION_MODAL_CLOSE_SUCCESS";
}
function toggle(): string {
notificationModal.toggle();
return "NOTIFICATION_MODAL_TOGGLE_SUCCESS";
}
target: "notifications"
}
content: Component { content: Component {
Item { Item {

View File

@@ -41,38 +41,6 @@ DankModal {
} }
content: settingsContent content: settingsContent
IpcHandler {
function open(): string {
settingsModal.show();
return "SETTINGS_OPEN_SUCCESS";
}
function close(): string {
settingsModal.hide();
return "SETTINGS_CLOSE_SUCCESS";
}
function toggle(): string {
settingsModal.toggle();
return "SETTINGS_TOGGLE_SUCCESS";
}
target: "settings"
}
IpcHandler {
function browse(type: string) {
if (type === "wallpaper") {
wallpaperBrowser.allowStacking = false;
wallpaperBrowser.open();
} else if (type === "profile") {
profileBrowser.allowStacking = false;
profileBrowser.open();
}
}
target: "file"
}
FileBrowserModal { FileBrowserModal {
id: profileBrowser id: profileBrowser

View File

@@ -82,24 +82,6 @@ DankModal {
target: ModalManager target: ModalManager
} }
IpcHandler {
function open(): string {
spotlightModal.show()
return "SPOTLIGHT_OPEN_SUCCESS"
}
function close(): string {
spotlightModal.hide()
return "SPOTLIGHT_CLOSE_SUCCESS"
}
function toggle(): string {
spotlightModal.toggle()
return "SPOTLIGHT_TOGGLE_SUCCESS"
}
target: "spotlight"
}
spotlightContent: Component { spotlightContent: Component {
SpotlightContent { SpotlightContent {

View File

@@ -69,8 +69,23 @@ Item {
Component { Component {
id: diskUsageDetailComponent id: diskUsageDetailComponent
DiskUsageDetail { DiskUsageDetail {
currentMountPath: root.expandedWidgetData?.currentMountPath || "/" currentMountPath: root.expandedWidgetData?.mountPath || "/"
instanceId: root.expandedWidgetData?.instanceId || "" instanceId: root.expandedWidgetData?.instanceId || ""
onMountPathChanged: (newMountPath) => {
if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
const widgets = SettingsData.controlCenterWidgets || []
const newWidgets = widgets.map(w => {
if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
const updatedWidget = Object.assign({}, w)
updatedWidget.mountPath = newMountPath
return updatedWidget
}
return w
})
SettingsData.setControlCenterWidgets(newWidgets)
}
}
} }
} }
} }

View File

@@ -3,8 +3,9 @@ import qs.Common
import qs.Services import qs.Services
import qs.Modules.ControlCenter.Widgets import qs.Modules.ControlCenter.Widgets
import qs.Modules.ControlCenter.Components import qs.Modules.ControlCenter.Components
import "../utils/layout.js" as LayoutUtils
Item { Column {
id: root id: root
property bool editMode: false property bool editMode: false
@@ -18,66 +19,23 @@ Item {
signal moveWidget(int fromIndex, int toIndex) signal moveWidget(int fromIndex, int toIndex)
signal toggleWidgetSize(int index) signal toggleWidgetSize(int index)
readonly property int gridColumns: 4 spacing: editMode ? Theme.spacingL : Theme.spacingS
readonly property real cellWidth: (width - (gridSpacing + 1) * (gridColumns - 1)) / gridColumns
readonly property real cellHeight: 60
readonly property real gridSpacing: 4
height: { property var currentRowWidgets: []
const dummy = [SettingsData.controlCenterWidgets?.length, widgetPositions.length] property real currentRowWidth: 0
return calculateGridHeight() + (detailHost.active ? detailHost.height + Theme.spacingL : 0) property int expandedRowIndex: -1
function calculateRowsAndWidgets() {
return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex)
} }
function calculateGridHeight() { property var layoutResult: {
const widgets = SettingsData.controlCenterWidgets || [] const dummy = [expandedSection, expandedWidgetIndex, model?.controlCenterWidgets]
if (widgets.length === 0) return calculateRowsAndWidgets()
return 0
let rows = []
let currentRow = []
let currentWidth = 0
const spacing = gridSpacing
const baseWidth = width
for (var i = 0; i < widgets.length; i++) {
const widget = widgets[i]
const widgetWidth = widget.width || 50
let itemWidth
if (widgetWidth <= 25) {
itemWidth = (baseWidth - spacing * 3) / 4
} else if (widgetWidth <= 50) {
itemWidth = (baseWidth - spacing) / 2
} else if (widgetWidth <= 75) {
itemWidth = (baseWidth - spacing * 2) * 0.75
} else {
itemWidth = baseWidth
}
if (currentRow.length > 0 && (currentWidth + spacing + itemWidth > baseWidth)) {
rows.push([...currentRow])
currentRow = [widget]
currentWidth = itemWidth
} else {
currentRow.push(widget)
currentWidth += (currentRow.length > 1 ? spacing : 0) + itemWidth
}
}
if (currentRow.length > 0) {
rows.push(currentRow)
}
return rows.length * cellHeight + (rows.length > 0 ? (rows.length - 1) * spacing : 0)
} }
DragDropDetailHost { onLayoutResultChanged: {
id: detailHost expandedRowIndex = layoutResult.expandedRowIndex
y: calculateGridHeight()
anchors.left: parent.left
anchors.right: parent.right
expandedSection: root.expandedSection
expandedWidgetData: root.expandedWidgetData
} }
function moveToTop(item) { function moveToTop(item) {
@@ -92,131 +50,115 @@ Item {
} }
Repeater { Repeater {
id: widgetRepeater model: root.layoutResult.rows
model: SettingsData.controlCenterWidgets || []
DragDropWidgetWrapper { Column {
id: widgetWrapper width: root.width
spacing: 0
editMode: root.editMode property int rowIndex: index
widgetData: modelData property var rowWidgets: modelData
widgetIndex: index property bool isSliderOnlyRow: {
gridCellWidth: root.cellWidth const widgets = rowWidgets || []
gridCellHeight: root.cellHeight if (widgets.length === 0) return false
gridColumns: root.gridColumns return widgets.every(w => w.id === "volumeSlider" || w.id === "brightnessSlider" || w.id === "inputVolumeSlider")
gridLayout: root
isSlider: {
const id = modelData.id || ""
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider"
} }
topPadding: isSliderOnlyRow ? (root.editMode ? 4 : -6) : 0
bottomPadding: isSliderOnlyRow ? (root.editMode ? 4 : -6) : 0
widgetComponent: { Flow {
const id = modelData.id || "" width: parent.width
if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") { spacing: Theme.spacingS
return compoundPillComponent
} else if (id === "volumeSlider") { Repeater {
return audioSliderComponent model: rowWidgets || []
} else if (id === "brightnessSlider") {
return brightnessSliderComponent DragDropWidgetWrapper {
} else if (id === "inputVolumeSlider") { widgetData: modelData
return inputAudioSliderComponent property int globalWidgetIndex: {
} else if (id === "battery") { const widgets = SettingsData.controlCenterWidgets || []
const widgetWidth = modelData.width || 50 for (var i = 0; i < widgets.length; i++) {
return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent if (widgets[i].id === modelData.id) {
} else if (id === "diskUsage") { if (modelData.id === "diskUsage") {
return diskUsagePillComponent if (widgets[i].instanceId === modelData.instanceId) {
} else { return i
const widgetWidth = modelData.width || 50 }
return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent } else {
return i
}
}
}
return -1
}
property int widgetWidth: modelData.width || 50
width: {
const baseWidth = root.width
const spacing = Theme.spacingS
if (widgetWidth <= 25) {
return (baseWidth - spacing * 3) / 4
} else if (widgetWidth <= 50) {
return (baseWidth - spacing) / 2
} else if (widgetWidth <= 75) {
return (baseWidth - spacing * 2) * 0.75
} else {
return baseWidth
}
}
height: isSliderOnlyRow ? 48 : 60
editMode: root.editMode
widgetIndex: globalWidgetIndex
gridCellWidth: width
gridCellHeight: height
gridColumns: 4
gridLayout: root
isSlider: {
const id = modelData.id || ""
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider"
}
widgetComponent: {
const id = modelData.id || ""
if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") {
return compoundPillComponent
} else if (id === "volumeSlider") {
return audioSliderComponent
} else if (id === "brightnessSlider") {
return brightnessSliderComponent
} else if (id === "inputVolumeSlider") {
return inputAudioSliderComponent
} else if (id === "battery") {
return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent
} else if (id === "diskUsage") {
return diskUsagePillComponent
} else {
return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent
}
}
onWidgetMoved: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
onRemoveWidget: index => root.removeWidget(index)
onToggleWidgetSize: index => root.toggleWidgetSize(index)
}
} }
} }
x: calculateWidgetX(index) DetailHost {
y: calculateWidgetY(index) width: parent.width
height: active ? (250 + Theme.spacingS) : 0
property bool active: {
if (root.expandedSection === "") return false
onWidgetMoved: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) if (root.expandedSection.startsWith("diskUsage_") && root.expandedWidgetData) {
onRemoveWidget: index => root.removeWidget(index) const expandedInstanceId = root.expandedWidgetData.instanceId
onToggleWidgetSize: index => root.toggleWidgetSize(index) return rowWidgets.some(w => w.id === "diskUsage" && w.instanceId === expandedInstanceId)
}
Behavior on x { return rowIndex === root.expandedRowIndex
enabled: !editMode
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Easing.OutCubic
} }
visible: active
expandedSection: root.expandedSection
expandedWidgetData: root.expandedWidgetData
} }
Behavior on y {
enabled: !editMode
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Easing.OutCubic
}
}
}
}
property var widgetPositions: calculateAllWidgetPositions()
function calculateAllWidgetPositions() {
const widgets = SettingsData.controlCenterWidgets || []
let positions = []
let currentX = 0
let currentY = 0
for (var i = 0; i < widgets.length; i++) {
const widget = widgets[i]
const widgetWidth = widget.width || 50
let cellsNeeded = 1
if (widgetWidth <= 25)
cellsNeeded = 1
else if (widgetWidth <= 50)
cellsNeeded = 2
else if (widgetWidth <= 75)
cellsNeeded = 3
else
cellsNeeded = 4
if (currentX + cellsNeeded > gridColumns) {
currentX = 0
currentY++
}
const horizontalSpacing = gridSpacing
positions[i] = {
"x": currentX * cellWidth + (currentX > 0 ? currentX * horizontalSpacing : 0),
"y": currentY * cellHeight + (currentY > 0 ? currentY * gridSpacing : 0),
"cellsUsed": cellsNeeded
}
currentX += cellsNeeded
if (currentX >= gridColumns) {
currentX = 0
currentY++
}
}
return positions
}
function calculateWidgetX(widgetIndex) {
if (widgetIndex < 0 || widgetIndex >= widgetPositions.length)
return 0
return widgetPositions[widgetIndex].x
}
function calculateWidgetY(widgetIndex) {
if (widgetIndex < 0 || widgetIndex >= widgetPositions.length)
return 0
return widgetPositions[widgetIndex].y
}
Connections {
target: SettingsData
function onControlCenterWidgetsChanged() {
widgetPositions = calculateAllWidgetPositions()
} }
} }
@@ -227,7 +169,7 @@ Item {
property int widgetIndex: parent.widgetIndex || 0 property int widgetIndex: parent.widgetIndex || 0
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
width: parent.width width: parent.width
height: cellHeight height: 60
iconName: { iconName: {
switch (widgetData.id || "") { switch (widgetData.id || "") {
case "wifi": case "wifi":
@@ -395,10 +337,9 @@ Item {
return false return false
} }
} }
enabled: !root.editMode && (widgetDef?.enabled ?? true) enabled: widgetDef?.enabled ?? true
onToggled: { onToggled: {
if (root.editMode) if (root.editMode) return
return
switch (widgetData.id || "") { switch (widgetData.id || "") {
case "wifi": case "wifi":
{ {
@@ -431,12 +372,11 @@ Item {
} }
} }
onExpandClicked: { onExpandClicked: {
if (!root.editMode) if (root.editMode) return
root.expandClicked(widgetData, widgetIndex) root.expandClicked(widgetData, widgetIndex)
} }
onWheelEvent: function (wheelEvent) { onWheelEvent: function (wheelEvent) {
if (root.editMode) if (root.editMode) return
return
const id = widgetData.id || "" const id = widgetData.id || ""
if (id === "audioOutput") { if (id === "audioOutput") {
if (!AudioService.sink || !AudioService.sink.audio) if (!AudioService.sink || !AudioService.sink.audio)
@@ -471,43 +411,67 @@ Item {
Component { Component {
id: audioSliderComponent id: audioSliderComponent
AudioSliderRow { Item {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width width: parent.width
height: 14 height: 16
enabled: !root.editMode
property color sliderTrackColor: Theme.surfaceContainerHigh AudioSliderRow {
anchors.centerIn: parent
width: parent.width
height: 14
property color sliderTrackColor: Theme.surfaceContainerHigh
}
} }
} }
Component { Component {
id: brightnessSliderComponent id: brightnessSliderComponent
BrightnessSliderRow { Item {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width width: parent.width
height: 14 height: 16
enabled: !root.editMode
property color sliderTrackColor: Theme.surfaceContainerHigh BrightnessSliderRow {
anchors.centerIn: parent
width: parent.width
height: 14
property color sliderTrackColor: Theme.surfaceContainerHigh
}
} }
} }
Component { Component {
id: inputAudioSliderComponent id: inputAudioSliderComponent
InputAudioSliderRow { Item {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width width: parent.width
height: 14 height: 16
enabled: !root.editMode
property color sliderTrackColor: Theme.surfaceContainerHigh InputAudioSliderRow {
anchors.centerIn: parent
width: parent.width
height: 14
property color sliderTrackColor: Theme.surfaceContainerHigh
}
} }
} }
Component { Component {
id: batteryPillComponent id: batteryPillComponent
BatteryPill { BatteryPill {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width width: parent.width
height: cellHeight height: 60
enabled: !root.editMode
onExpandClicked: { onExpandClicked: {
if (!root.editMode) if (!root.editMode) {
root.expandClicked(parent.widgetData, parent.widgetIndex) root.expandClicked(widgetData, widgetIndex)
}
} }
} }
} }
@@ -515,12 +479,15 @@ Item {
Component { Component {
id: smallBatteryComponent id: smallBatteryComponent
SmallBatteryButton { SmallBatteryButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width width: parent.width
height: 48 height: 48
enabled: !root.editMode
onClicked: { onClicked: {
if (!root.editMode) if (!root.editMode) {
root.expandClicked(parent.widgetData, parent.widgetIndex) root.expandClicked(widgetData, widgetIndex)
}
} }
} }
} }
@@ -530,9 +497,8 @@ Item {
ToggleButton { ToggleButton {
property var widgetData: parent.widgetData || {} property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0 property int widgetIndex: parent.widgetIndex || 0
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
width: parent.width width: parent.width
height: cellHeight height: 60
iconName: { iconName: {
switch (widgetData.id || "") { switch (widgetData.id || "") {
@@ -545,7 +511,7 @@ Item {
case "idleInhibitor": case "idleInhibitor":
return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle" return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
default: default:
return widgetDef?.icon || "help" return "help"
} }
} }
@@ -560,7 +526,7 @@ Item {
case "idleInhibitor": case "idleInhibitor":
return SessionService.idleInhibited ? "Keeping Awake" : "Keep Awake" return SessionService.idleInhibited ? "Keeping Awake" : "Keep Awake"
default: default:
return widgetDef?.text || "Unknown" return "Unknown"
} }
} }
@@ -581,7 +547,7 @@ Item {
} }
} }
enabled: !root.editMode && (widgetDef?.enabled ?? true) enabled: !root.editMode
onClicked: { onClicked: {
if (root.editMode) if (root.editMode)
@@ -618,7 +584,6 @@ Item {
SmallToggleButton { SmallToggleButton {
property var widgetData: parent.widgetData || {} property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0 property int widgetIndex: parent.widgetIndex || 0
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
width: parent.width width: parent.width
height: 48 height: 48
@@ -633,7 +598,7 @@ Item {
case "idleInhibitor": case "idleInhibitor":
return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle" return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
default: default:
return widgetDef?.icon || "help" return "help"
} }
} }
@@ -654,7 +619,7 @@ Item {
} }
} }
enabled: !root.editMode && (widgetDef?.enabled ?? true) enabled: !root.editMode
onClicked: { onClicked: {
if (root.editMode) if (root.editMode)
@@ -689,14 +654,18 @@ Item {
Component { Component {
id: diskUsagePillComponent id: diskUsagePillComponent
DiskUsagePill { DiskUsagePill {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width width: parent.width
height: cellHeight height: 60
enabled: !root.editMode
mountPath: parent.widgetData?.mountPath || "/" mountPath: widgetData.mountPath || "/"
instanceId: parent.widgetData?.instanceId || "" instanceId: widgetData.instanceId || ""
onExpandClicked: { onExpandClicked: {
if (!root.editMode) if (!root.editMode) {
root.expandClicked(parent.widgetData, parent.widgetIndex) root.expandClicked(widgetData, widgetIndex)
}
} }
} }
} }

View File

@@ -16,6 +16,8 @@ Item {
property int gridColumns: 4 property int gridColumns: 4
property var gridLayout: null property var gridLayout: null
z: dragArea.drag.active ? 10000 : 1
signal widgetMoved(int fromIndex, int toIndex) signal widgetMoved(int fromIndex, int toIndex)
signal removeWidget(int index) signal removeWidget(int index)
signal toggleWidgetSize(int index) signal toggleWidgetSize(int index)
@@ -27,7 +29,7 @@ Item {
else if (widgetWidth <= 75) return gridCellWidth * 3 else if (widgetWidth <= 75) return gridCellWidth * 3
else return gridCellWidth * 4 else return gridCellWidth * 4
} }
height: gridCellHeight height: isSlider ? 16 : gridCellHeight
Rectangle { Rectangle {
id: dragIndicator id: dragIndicator
@@ -37,7 +39,7 @@ Item {
border.width: dragArea.drag.active ? 2 : 0 border.width: dragArea.drag.active ? 2 : 0
radius: Theme.cornerRadius radius: Theme.cornerRadius
opacity: dragArea.drag.active ? 0.8 : 1.0 opacity: dragArea.drag.active ? 0.8 : 1.0
z: dragArea.drag.active ? 1000 : 1 z: dragArea.drag.active ? 10000 : 1
Behavior on border.width { Behavior on border.width {
NumberAnimation { duration: 150 } NumberAnimation { duration: 150 }
@@ -56,13 +58,14 @@ Item {
property int globalWidgetIndex: root.widgetIndex property int globalWidgetIndex: root.widgetIndex
property int widgetWidth: root.widgetData?.width || 50 property int widgetWidth: root.widgetData?.width || 50
MouseArea { MouseArea {
id: editModeBlocker id: editModeBlocker
anchors.fill: parent anchors.fill: parent
enabled: root.editMode enabled: root.editMode
acceptedButtons: Qt.AllButtons acceptedButtons: Qt.AllButtons
onPressed: mouse.accepted = true onPressed: function(mouse) { mouse.accepted = true }
onWheel: wheel.accepted = true onWheel: function(wheel) { wheel.accepted = true }
z: 100 z: 100
} }
} }
@@ -71,23 +74,23 @@ Item {
id: dragArea id: dragArea
anchors.fill: parent anchors.fill: parent
enabled: editMode enabled: editMode
cursorShape: editMode ? Qt.OpenHandCursor : Qt.ArrowCursor cursorShape: editMode ? Qt.OpenHandCursor : Qt.PointingHandCursor
drag.target: editMode ? root : null drag.target: editMode ? root : null
drag.axis: Drag.XAndYAxis drag.axis: Drag.XAndYAxis
drag.smoothed: true drag.smoothed: true
onPressed: { onPressed: function(mouse) {
if (editMode) { if (editMode) {
cursorShape = Qt.ClosedHandCursor cursorShape = Qt.ClosedHandCursor
root.z = 1000 if (root.gridLayout && root.gridLayout.moveToTop) {
root.parent.moveToTop(root) root.gridLayout.moveToTop(root)
}
} }
} }
onReleased: { onReleased: function(mouse) {
if (editMode) { if (editMode) {
cursorShape = Qt.OpenHandCursor cursorShape = Qt.OpenHandCursor
root.z = 1
root.snapToGrid() root.snapToGrid()
} }
} }
@@ -97,6 +100,19 @@ Item {
Drag.hotSpot.x: width / 2 Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2 Drag.hotSpot.y: height / 2
function swapIndices(i, j) {
if (i === j) return;
const arr = SettingsData.controlCenterWidgets;
if (!arr || i < 0 || j < 0 || i >= arr.length || j >= arr.length) return;
const copy = arr.slice();
const tmp = copy[i];
copy[i] = copy[j];
copy[j] = tmp;
SettingsData.setControlCenterWidgets(copy);
}
function snapToGrid() { function snapToGrid() {
if (!editMode || !gridLayout) return if (!editMode || !gridLayout) return
@@ -104,80 +120,90 @@ Item {
const cellWidth = gridLayout.width / gridColumns const cellWidth = gridLayout.width / gridColumns
const cellHeight = gridCellHeight + Theme.spacingS const cellHeight = gridCellHeight + Theme.spacingS
let targetCol = Math.max(0, Math.round(globalPos.x / cellWidth)) const centerX = globalPos.x + (root.width / 2)
let targetRow = Math.max(0, Math.round(globalPos.y / cellHeight)) const centerY = globalPos.y + (root.height / 2)
const widgetCells = Math.ceil(root.width / cellWidth) let targetCol = Math.max(0, Math.floor(centerX / cellWidth))
let targetRow = Math.max(0, Math.floor(centerY / cellHeight))
if (targetCol + widgetCells > gridColumns) { targetCol = Math.min(targetCol, gridColumns - 1)
targetCol = Math.max(0, gridColumns - widgetCells)
}
const newIndex = findBestInsertionIndex(targetRow, targetCol) const newIndex = findBestInsertionIndex(targetRow, targetCol)
if (newIndex !== widgetIndex && newIndex >= 0 && newIndex < (SettingsData.controlCenterWidgets?.length || 0)) { if (newIndex !== widgetIndex && newIndex >= 0 && newIndex < (SettingsData.controlCenterWidgets?.length || 0)) {
widgetMoved(widgetIndex, newIndex) swapIndices(widgetIndex, newIndex)
} }
} }
function findBestInsertionIndex(targetRow, targetCol) { function findBestInsertionIndex(targetRow, targetCol) {
const widgets = SettingsData.controlCenterWidgets || [] const widgets = SettingsData.controlCenterWidgets || [];
if (!widgets.length) return 0 const n = widgets.length;
if (!n || widgetIndex < 0 || widgetIndex >= n) return -1;
const targetPosition = targetRow * gridColumns + targetCol function spanFor(width) {
const w = width ?? 50;
if (w <= 25) return 1;
if (w <= 50) return 2;
if (w <= 75) return 3;
return 4;
}
// Find the widget position closest to our target const cols = gridColumns || 4;
let bestIndex = 0
let bestDistance = Infinity
for (let i = 0; i <= widgets.length; i++) { let row = 0, col = 0;
if (i === widgetIndex) continue let draggedOrigKey = null;
let currentPos = calculatePositionForIndex(i) const pos = [];
let distance = Math.abs(currentPos - targetPosition)
if (distance < bestDistance) { for (let i = 0; i < n; i++) {
bestDistance = distance const span = Math.min(spanFor(widgets[i].width), cols);
bestIndex = i > widgetIndex ? i - 1 : i
if (col + span > cols) {
row++;
col = 0;
}
const startCol = col;
const centerKey = row * cols + (startCol + (span - 1) / 2);
if (i === widgetIndex) {
draggedOrigKey = centerKey;
} else {
pos.push({ index: i, row, startCol, span, centerKey });
}
col += span;
if (col >= cols) {
row++;
col = 0;
} }
} }
return Math.max(0, Math.min(bestIndex, widgets.length - 1)) if (pos.length === 0) return -1;
}
function calculatePositionForIndex(index) { const centerColCoord = targetCol + 0.5;
const widgets = SettingsData.controlCenterWidgets || [] const targetKey = targetRow * cols + centerColCoord;
let currentX = 0
let currentY = 0
for (let i = 0; i < index && i < widgets.length; i++) { for (let k = 0; k < pos.length; k++) {
if (i === widgetIndex) continue const p = pos[k];
if (p.row === targetRow && centerColCoord >= p.startCol && centerColCoord < (p.startCol + p.span)) {
const widget = widgets[i] return p.index;
const widgetWidth = widget.width || 50
let cellsNeeded = widgetWidth <= 25 ? 1 : widgetWidth <= 50 ? 2 : widgetWidth <= 75 ? 3 : 4
if (currentX + cellsNeeded > gridColumns) {
currentX = 0
currentY++
}
currentX += cellsNeeded
if (currentX >= gridColumns) {
currentX = 0
currentY++
} }
} }
return currentY * gridColumns + currentX let lo = 0, hi = pos.length - 1;
} if (targetKey <= pos[0].centerKey) return pos[0].index;
if (targetKey >= pos[hi].centerKey) return pos[hi].index;
function getWidgetWidth(widgetWidth) { while (lo <= hi) {
const cellWidth = gridLayout ? gridLayout.width / gridColumns : gridCellWidth const mid = (lo + hi) >> 1;
if (widgetWidth <= 25) return cellWidth const mk = pos[mid].centerKey;
else if (widgetWidth <= 50) return cellWidth * 2 if (targetKey < mk) hi = mid - 1;
else if (widgetWidth <= 75) return cellWidth * 3 else if (targetKey > mk) lo = mid + 1;
else return cellWidth * 4 else return pos[mid].index;
}
const movingUp = (draggedOrigKey != null) ? (targetKey < draggedOrigKey) : false;
return (movingUp ? pos[lo].index : pos[hi].index);
} }
Rectangle { Rectangle {
@@ -200,11 +226,12 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: removeWidget(widgetIndex) onClicked: removeWidget(widgetIndex)
} }
} }
PieChartSizeControl { SizeControls {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.margins: -6 anchors.margins: -6

View File

@@ -0,0 +1,52 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Row {
id: root
property int currentSize: 50
property bool isSlider: false
property int widgetIndex: -1
signal sizeChanged(int newSize)
readonly property var availableSizes: isSlider ? [50, 100] : [25, 50, 75, 100]
spacing: 2
Repeater {
model: root.availableSizes
Rectangle {
width: 16
height: 16
radius: 3
color: modelData === root.currentSize ? Theme.primary : Theme.surfaceContainer
border.color: modelData === root.currentSize ? Theme.primary : Theme.outline
border.width: 1
StyledText {
anchors.centerIn: parent
text: modelData.toString()
font.pixelSize: 8
font.weight: Font.Medium
color: modelData === root.currentSize ? Theme.primaryContainer : Theme.surfaceText
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentSize = modelData
root.sizeChanged(modelData)
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
}

View File

@@ -1,719 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Modules.ControlCenter.Widgets
import qs.Modules.ControlCenter.Components
import "../utils/layout.js" as LayoutUtils
Column {
id: root
property bool editMode: false
property string expandedSection: ""
property int expandedWidgetIndex: -1
property var model: null
property var expandedWidgetData: null
signal expandClicked(var widgetData, int globalIndex)
signal removeWidget(int index)
signal moveWidget(int fromIndex, int toIndex)
signal toggleWidgetSize(int index)
spacing: editMode ? Theme.spacingL : Theme.spacingS
property var currentRowWidgets: []
property real currentRowWidth: 0
property int expandedRowIndex: -1
function calculateRowsAndWidgets() {
return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex)
}
property var layoutResult: {
const dummy = [expandedSection, expandedWidgetIndex, model?.controlCenterWidgets]
return calculateRowsAndWidgets()
}
onLayoutResultChanged: {
expandedRowIndex = layoutResult.expandedRowIndex
}
Repeater {
model: root.layoutResult.rows
Column {
width: root.width
spacing: 0
property int rowIndex: index
property var rowWidgets: modelData
property bool isSliderOnlyRow: {
const widgets = rowWidgets || []
if (widgets.length === 0) return false
return widgets.every(w => w.id === "volumeSlider" || w.id === "brightnessSlider" || w.id === "inputVolumeSlider")
}
topPadding: isSliderOnlyRow ? (root.editMode ? 4 : -12) : 0
bottomPadding: isSliderOnlyRow ? (root.editMode ? 4 : -12) : 0
Flow {
width: parent.width
spacing: Theme.spacingS
Repeater {
model: rowWidgets || []
Item {
property var widgetData: modelData
property int globalWidgetIndex: {
const widgets = SettingsData.controlCenterWidgets || []
for (var i = 0; i < widgets.length; i++) {
if (widgets[i].id === modelData.id) {
if (modelData.id === "diskUsage") {
if (widgets[i].instanceId === modelData.instanceId) {
return i
}
} else {
return i
}
}
}
return -1
}
property int widgetWidth: modelData.width || 50
width: {
const baseWidth = root.width
const spacing = Theme.spacingS
if (widgetWidth <= 25) {
return (baseWidth - spacing * 3) / 4
} else if (widgetWidth <= 50) {
return (baseWidth - spacing) / 2
} else if (widgetWidth <= 75) {
return (baseWidth - spacing * 2) * 0.75
} else {
return baseWidth
}
}
height: 60
Loader {
id: widgetLoader
anchors.fill: parent
property var widgetData: parent.widgetData
property int widgetIndex: parent.globalWidgetIndex
property int globalWidgetIndex: parent.globalWidgetIndex
property int widgetWidth: parent.widgetWidth
sourceComponent: {
const id = modelData.id || ""
if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") {
return compoundPillComponent
} else if (id === "volumeSlider") {
return audioSliderComponent
} else if (id === "brightnessSlider") {
return brightnessSliderComponent
} else if (id === "inputVolumeSlider") {
return inputAudioSliderComponent
} else if (id === "battery") {
return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent
} else if (id === "diskUsage") {
return diskUsagePillComponent
} else {
return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent
}
}
}
}
}
}
DetailHost {
width: parent.width
height: active ? (250 + Theme.spacingS) : 0
property bool active: {
if (root.expandedSection === "") return false
if (root.expandedSection.startsWith("diskUsage_") && root.expandedWidgetData) {
const expandedInstanceId = root.expandedWidgetData.instanceId
return rowWidgets.some(w => w.id === "diskUsage" && w.instanceId === expandedInstanceId)
}
return rowIndex === root.expandedRowIndex
}
visible: active
expandedSection: root.expandedSection
expandedWidgetData: root.expandedWidgetData
}
}
}
Component {
id: compoundPillComponent
CompoundPill {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
width: parent.width
height: 60
iconName: {
switch (widgetData.id || "") {
case "wifi": {
if (NetworkService.wifiToggling) {
return "sync"
}
if (NetworkService.networkStatus === "ethernet") {
return "settings_ethernet"
}
if (NetworkService.networkStatus === "wifi") {
return NetworkService.wifiSignalIcon
}
if (NetworkService.wifiEnabled) {
return "wifi_off"
}
return "wifi_off"
}
case "bluetooth": {
if (!BluetoothService.available) {
return "bluetooth_disabled"
}
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
return "bluetooth_disabled"
}
return "bluetooth"
}
case "audioOutput": {
if (!AudioService.sink) return "volume_off"
let volume = AudioService.sink.audio.volume
let muted = AudioService.sink.audio.muted
if (muted || volume === 0.0) return "volume_off"
if (volume <= 0.33) return "volume_down"
if (volume <= 0.66) return "volume_up"
return "volume_up"
}
case "audioInput": {
if (!AudioService.source) return "mic_off"
let muted = AudioService.source.audio.muted
return muted ? "mic_off" : "mic"
}
default: return widgetDef?.icon || "help"
}
}
primaryText: {
switch (widgetData.id || "") {
case "wifi": {
if (NetworkService.wifiToggling) {
return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
}
if (NetworkService.networkStatus === "ethernet") {
return "Ethernet"
}
if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) {
return NetworkService.currentWifiSSID
}
if (NetworkService.wifiEnabled) {
return "Not connected"
}
return "WiFi off"
}
case "bluetooth": {
if (!BluetoothService.available) {
return "Bluetooth"
}
if (!BluetoothService.adapter) {
return "No adapter"
}
if (!BluetoothService.adapter.enabled) {
return "Disabled"
}
return "Enabled"
}
case "audioOutput": return AudioService.sink?.description || "No output device"
case "audioInput": return AudioService.source?.description || "No input device"
default: return widgetDef?.text || "Unknown"
}
}
secondaryText: {
switch (widgetData.id || "") {
case "wifi": {
if (NetworkService.wifiToggling) {
return "Please wait..."
}
if (NetworkService.networkStatus === "ethernet") {
return "Connected"
}
if (NetworkService.networkStatus === "wifi") {
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"
}
if (NetworkService.wifiEnabled) {
return "Select network"
}
return ""
}
case "bluetooth": {
if (!BluetoothService.available) {
return "No adapters"
}
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
return "Off"
}
const primaryDevice = (() => {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices) {
return null
}
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
for (let device of devices) {
if (device && device.connected) {
return device
}
}
return null
})()
if (primaryDevice) {
return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device"
}
return "No devices"
}
case "audioOutput": {
if (!AudioService.sink) {
return "Select device"
}
if (AudioService.sink.audio.muted) {
return "Muted"
}
return Math.round(AudioService.sink.audio.volume * 100) + "%"
}
case "audioInput": {
if (!AudioService.source) {
return "Select device"
}
if (AudioService.source.audio.muted) {
return "Muted"
}
return Math.round(AudioService.source.audio.volume * 100) + "%"
}
default: return widgetDef?.description || ""
}
}
isActive: {
switch (widgetData.id || "") {
case "wifi": {
if (NetworkService.wifiToggling) {
return false
}
if (NetworkService.networkStatus === "ethernet") {
return true
}
if (NetworkService.networkStatus === "wifi") {
return true
}
return NetworkService.wifiEnabled
}
case "bluetooth": return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled)
case "audioOutput": return !!(AudioService.sink && !AudioService.sink.audio.muted)
case "audioInput": return !!(AudioService.source && !AudioService.source.audio.muted)
default: return false
}
}
enabled: (widgetDef?.enabled ?? true)
onToggled: {
if (root.editMode) return
switch (widgetData.id || "") {
case "wifi": {
if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) {
NetworkService.toggleWifiRadio()
}
break
}
case "bluetooth": {
if (BluetoothService.available && BluetoothService.adapter) {
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
}
break
}
case "audioOutput": {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
}
break
}
case "audioInput": {
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = !AudioService.source.audio.muted
}
break
}
}
}
onExpandClicked: {
if (root.editMode) return
root.expandClicked(widgetData, widgetIndex)
}
onWheelEvent: function (wheelEvent) {
const id = widgetData.id || ""
if (id === "audioOutput") {
if (!AudioService.sink || !AudioService.sink.audio) return
let delta = wheelEvent.angleDelta.y
let currentVolume = AudioService.sink.audio.volume * 100
let newVolume
if (delta > 0)
newVolume = Math.min(100, currentVolume + 5)
else
newVolume = Math.max(0, currentVolume - 5)
AudioService.sink.audio.muted = false
AudioService.sink.audio.volume = newVolume / 100
wheelEvent.accepted = true
} else if (id === "audioInput") {
if (!AudioService.source || !AudioService.source.audio) return
let delta = wheelEvent.angleDelta.y
let currentVolume = AudioService.source.audio.volume * 100
let newVolume
if (delta > 0)
newVolume = Math.min(100, currentVolume + 5)
else
newVolume = Math.max(0, currentVolume - 5)
AudioService.source.audio.muted = false
AudioService.source.audio.volume = newVolume / 100
wheelEvent.accepted = true
}
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: false
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
Component {
id: audioSliderComponent
Item {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
width: parent.width
height: 16
AudioSliderRow {
anchors.centerIn: parent
width: parent.width
height: 14
property color sliderTrackColor: Theme.surfaceContainerHigh
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: true
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
Component {
id: brightnessSliderComponent
Item {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 16
BrightnessSliderRow {
anchors.centerIn: parent
width: parent.width
height: 14
property color sliderTrackColor: Theme.surfaceContainerHigh
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: true
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
Component {
id: inputAudioSliderComponent
Item {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 16
InputAudioSliderRow {
anchors.centerIn: parent
width: parent.width
height: 14
property color sliderTrackColor: Theme.surfaceContainerHigh
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: true
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
Component {
id: batteryPillComponent
BatteryPill {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 60
onExpandClicked: {
if (!root.editMode) {
root.expandClicked(widgetData, widgetIndex)
}
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: false
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
Component {
id: smallBatteryComponent
SmallBatteryButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 48
onClicked: {
if (!root.editMode) {
root.expandClicked(widgetData, widgetIndex)
}
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: false
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
Component {
id: toggleButtonComponent
ToggleButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
width: parent.width
height: 60
iconName: {
switch (widgetData.id || "") {
case "nightMode": return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"
case "darkMode": return "contrast"
case "doNotDisturb": return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off"
case "idleInhibitor": return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
default: return widgetDef?.icon || "help"
}
}
text: {
switch (widgetData.id || "") {
case "nightMode": return "Night Mode"
case "darkMode": return SessionData.isLightMode ? "Light Mode" : "Dark Mode"
case "doNotDisturb": return "Do Not Disturb"
case "idleInhibitor": return SessionService.idleInhibited ? "Keeping Awake" : "Keep Awake"
default: return widgetDef?.text || "Unknown"
}
}
secondaryText: ""
iconRotation: widgetData.id === "darkMode" && SessionData.isLightMode ? 180 : 0
isActive: {
switch (widgetData.id || "") {
case "nightMode": return DisplayService.nightModeEnabled || false
case "darkMode": return !SessionData.isLightMode
case "doNotDisturb": return SessionData.doNotDisturb || false
case "idleInhibitor": return SessionService.idleInhibited || false
default: return false
}
}
enabled: (widgetDef?.enabled ?? true) && !root.editMode
onClicked: {
switch (widgetData.id || "") {
case "nightMode": {
if (DisplayService.automationAvailable) {
DisplayService.toggleNightMode()
}
break
}
case "darkMode": {
Theme.toggleLightMode()
break
}
case "doNotDisturb": {
SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
break
}
case "idleInhibitor": {
SessionService.toggleIdleInhibit()
break
}
}
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: false
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
Component {
id: smallToggleComponent
SmallToggleButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
width: parent.width
height: 48
iconName: {
switch (widgetData.id || "") {
case "nightMode": return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"
case "darkMode": return "contrast"
case "doNotDisturb": return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off"
case "idleInhibitor": return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
default: return widgetDef?.icon || "help"
}
}
iconRotation: widgetData.id === "darkMode" && SessionData.isLightMode ? 180 : 0
isActive: {
switch (widgetData.id || "") {
case "nightMode": return DisplayService.nightModeEnabled || false
case "darkMode": return !SessionData.isLightMode
case "doNotDisturb": return SessionData.doNotDisturb || false
case "idleInhibitor": return SessionService.idleInhibited || false
default: return false
}
}
enabled: (widgetDef?.enabled ?? true) && !root.editMode
onClicked: {
switch (widgetData.id || "") {
case "nightMode": {
if (DisplayService.automationAvailable) {
DisplayService.toggleNightMode()
}
break
}
case "darkMode": {
Theme.toggleLightMode()
break
}
case "doNotDisturb": {
SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
break
}
case "idleInhibitor": {
SessionService.toggleIdleInhibit()
break
}
}
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: false
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
Component {
id: diskUsagePillComponent
DiskUsagePill {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 60
mountPath: widgetData.mountPath || "/"
instanceId: widgetData.instanceId || ""
onExpandClicked: {
if (!root.editMode) {
root.expandClicked(widgetData, widgetIndex)
}
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: false
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
}

View File

@@ -208,7 +208,7 @@ PanelWindow {
"loader": controlCenterLoader, "loader": controlCenterLoader,
"prop": "shouldBeVisible" "prop": "shouldBeVisible"
}, { }, {
"loader": clipboardHistoryModalPopup, "loader": clipboardHistoryModalLoader.item,
"prop": "visible" "prop": "visible"
}, { }, {
"loader": systemUpdateLoader, "loader": systemUpdateLoader,
@@ -823,7 +823,9 @@ PanelWindow {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
clipboardHistoryModalPopup.toggle() clipboardHistoryModalLoader.active = true
if (clipboardHistoryModalLoader.item)
clipboardHistoryModalLoader.item.toggle()
} }
} }

View File

@@ -1,85 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
Item {
id: root
property int currentSize: 50
property bool isSlider: false
property int widgetIndex: -1
signal sizeChanged(int newSize)
width: 28
height: 28
readonly property var availableSizes: isSlider ? [50, 100] : [25, 50, 75, 100]
readonly property int currentSizeIndex: availableSizes.indexOf(currentSize)
Canvas {
id: pieCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const centerX = width / 2
const centerY = height / 2
const radius = Math.min(width, height) / 2 - 2
ctx.clearRect(0, 0, width, height)
ctx.strokeStyle = Theme.primary
ctx.lineWidth = 1.5
ctx.beginPath()
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI)
ctx.stroke()
if (availableSizes.length > 0 && currentSizeIndex >= 0) {
const segmentAngle = (2 * Math.PI) / availableSizes.length
const startAngle = -Math.PI / 2
const endAngle = startAngle + segmentAngle * (currentSizeIndex + 1)
ctx.fillStyle = Theme.primary
ctx.beginPath()
ctx.moveTo(centerX, centerY)
ctx.arc(centerX, centerY, radius - 1, startAngle, endAngle)
ctx.closePath()
ctx.fill()
}
}
}
Rectangle {
anchors.centerIn: parent
width: 12
height: 12
radius: 6
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
const nextIndex = (currentSizeIndex + 1) % availableSizes.length
const newSize = availableSizes[nextIndex]
currentSize = newSize
pieCanvas.requestPaint()
sizeChanged(newSize)
}
}
onCurrentSizeChanged: {
pieCanvas.requestPaint()
}
onIsSliderChanged: {
if (isSlider && currentSize !== 50 && currentSize !== 100) {
currentSize = 50
}
pieCanvas.requestPaint()
}
}

277
shell.qml
View File

@@ -52,7 +52,11 @@ ShellRoot {
delegate: TopBar { delegate: TopBar {
modelData: item modelData: item
notepadVariants: notepadSlideoutVariants notepadVariants: notepadSlideoutVariants
onColorPickerRequested: colorPickerModal.show() onColorPickerRequested: {
colorPickerModalLoader.active = true
if (colorPickerModalLoader.item)
colorPickerModalLoader.item.show()
}
} }
} }
@@ -244,8 +248,14 @@ ShellRoot {
} }
} }
SettingsModal { LazyLoader {
id: settingsModal id: settingsModalLoader
active: false
SettingsModal {
id: settingsModal
}
} }
LazyLoader { LazyLoader {
@@ -258,19 +268,44 @@ ShellRoot {
} }
} }
SpotlightModal { LazyLoader {
id: spotlightModal id: spotlightModalLoader
active: false
SpotlightModal {
id: spotlightModal
}
} }
ClipboardHistoryModal { LazyLoader {
id: clipboardHistoryModalPopup id: clipboardHistoryModalLoader
active: false
ClipboardHistoryModal {
id: clipboardHistoryModalPopup
}
} }
NotificationModal { LazyLoader {
id: notificationModal id: notificationModalLoader
active: false
NotificationModal {
id: notificationModal
}
} }
ColorPickerModal {
id: colorPickerModal LazyLoader {
id: colorPickerModalLoader
active: false
ColorPickerModal {
id: colorPickerModal
}
} }
LazyLoader { LazyLoader {
@@ -360,216 +395,18 @@ ShellRoot {
} }
} }
IpcHandler { IPC {
function open() { id: ipcHandlers
powerMenuModalLoader.active = true
if (powerMenuModalLoader.item)
powerMenuModalLoader.item.open()
return "POWERMENU_OPEN_SUCCESS" powermenu: powerMenuModalLoader
} processlist: processListModalLoader
controlCenter: controlCenterLoader
function close() { dash: dankDashPopoutLoader
if (powerMenuModalLoader.item) notepadVariants: notepadSlideoutVariants
powerMenuModalLoader.item.close() spotlight: spotlightModalLoader
clipboard: clipboardHistoryModalLoader
return "POWERMENU_CLOSE_SUCCESS" notifications: notificationModalLoader
} settings: settingsModalLoader
function toggle() {
powerMenuModalLoader.active = true
if (powerMenuModalLoader.item)
powerMenuModalLoader.item.toggle()
return "POWERMENU_TOGGLE_SUCCESS"
}
target: "powermenu"
}
IpcHandler {
function open(): string {
processListModalLoader.active = true
if (processListModalLoader.item)
processListModalLoader.item.show()
return "PROCESSLIST_OPEN_SUCCESS"
}
function close(): string {
if (processListModalLoader.item)
processListModalLoader.item.hide()
return "PROCESSLIST_CLOSE_SUCCESS"
}
function toggle(): string {
processListModalLoader.active = true
if (processListModalLoader.item)
processListModalLoader.item.toggle()
return "PROCESSLIST_TOGGLE_SUCCESS"
}
target: "processlist"
}
IpcHandler {
function open(): string {
controlCenterLoader.active = true
if (controlCenterLoader.item) {
controlCenterLoader.item.open()
return "CONTROL_CENTER_OPEN_SUCCESS"
}
return "CONTROL_CENTER_OPEN_FAILED"
}
function close(): string {
if (controlCenterLoader.item) {
controlCenterLoader.item.close()
return "CONTROL_CENTER_CLOSE_SUCCESS"
}
return "CONTROL_CENTER_CLOSE_FAILED"
}
function toggle(): string {
controlCenterLoader.active = true
if (controlCenterLoader.item) {
controlCenterLoader.item.toggle()
return "CONTROL_CENTER_TOGGLE_SUCCESS"
}
return "CONTROL_CENTER_TOGGLE_FAILED"
}
target: "control-center"
}
IpcHandler {
function open(tab: string): string {
dankDashPopoutLoader.active = true
if (dankDashPopoutLoader.item) {
switch (tab.toLowerCase()) {
case "media":
dankDashPopoutLoader.item.currentTabIndex = 1
break
case "weather":
dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
break
default:
dankDashPopoutLoader.item.currentTabIndex = 0
break
}
dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
dankDashPopoutLoader.item.dashVisible = true
return "DASH_OPEN_SUCCESS"
}
return "DASH_OPEN_FAILED"
}
function close(): string {
if (dankDashPopoutLoader.item) {
dankDashPopoutLoader.item.dashVisible = false
return "DASH_CLOSE_SUCCESS"
}
return "DASH_CLOSE_FAILED"
}
function toggle(tab: string): string {
dankDashPopoutLoader.active = true
if (dankDashPopoutLoader.item) {
if (dankDashPopoutLoader.item.dashVisible) {
dankDashPopoutLoader.item.dashVisible = false
} else {
switch (tab.toLowerCase()) {
case "media":
dankDashPopoutLoader.item.currentTabIndex = 1
break
case "weather":
dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
break
default:
dankDashPopoutLoader.item.currentTabIndex = 0
break
}
dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
dankDashPopoutLoader.item.dashVisible = true
}
return "DASH_TOGGLE_SUCCESS"
}
return "DASH_TOGGLE_FAILED"
}
target: "dash"
}
IpcHandler {
function getFocusedScreenName() {
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
return Hyprland.focusedWorkspace.monitor.name
}
if (CompositorService.isNiri && NiriService.currentOutput) {
return NiriService.currentOutput
}
return ""
}
function getActiveNotepadInstance() {
if (notepadSlideoutVariants.instances.length === 0) {
return null
}
if (notepadSlideoutVariants.instances.length === 1) {
return notepadSlideoutVariants.instances[0]
}
var focusedScreen = getFocusedScreenName()
if (focusedScreen && notepadSlideoutVariants.instances.length > 0) {
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
var slideout = notepadSlideoutVariants.instances[i]
if (slideout.modelData && slideout.modelData.name === focusedScreen) {
return slideout
}
}
}
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
var slideout = notepadSlideoutVariants.instances[i]
if (slideout.isVisible) {
return slideout
}
}
return notepadSlideoutVariants.instances[0]
}
function open(): string {
var instance = getActiveNotepadInstance()
if (instance) {
instance.show()
return "NOTEPAD_OPEN_SUCCESS"
}
return "NOTEPAD_OPEN_FAILED"
}
function close(): string {
var instance = getActiveNotepadInstance()
if (instance) {
instance.hide()
return "NOTEPAD_CLOSE_SUCCESS"
}
return "NOTEPAD_CLOSE_FAILED"
}
function toggle(): string {
var instance = getActiveNotepadInstance()
if (instance) {
instance.toggle()
return "NOTEPAD_TOGGLE_SUCCESS"
}
return "NOTEPAD_TOGGLE_FAILED"
}
target: "notepad"
} }
Variants { Variants {