659 lines
22 KiB
QML
659 lines
22 KiB
QML
import QtQuick
|
|
import Qt.labs.folderlistmodel
|
|
import Quickshell
|
|
import Quickshell.Bluetooth
|
|
import Quickshell.Io
|
|
import Quickshell.Services.Pipewire
|
|
import "../.."
|
|
|
|
Item {
|
|
id: root
|
|
width: parent ? parent.width : 1280
|
|
height: parent ? parent.height : 820
|
|
focus: true
|
|
|
|
/* ------------------------------
|
|
PIXEL CONSTANTS (DO NOT TOUCH)
|
|
------------------------------ */
|
|
|
|
property int menuLeft: 64
|
|
property int menuTop: 140
|
|
property int lineHeight: 38 + 40 + 1
|
|
property int visibleRows: Math.max(1, Math.floor((height - menuTop - 64) / lineHeight))
|
|
property int scrollOffset: 0
|
|
|
|
property int nameFontSize: 32
|
|
property int stateFontSize: 28
|
|
|
|
property int stateColumnX: 824
|
|
|
|
property int soulOffsetX: -36 - 32
|
|
property int soulOffsetY: -26
|
|
|
|
/* ------------------------------ */
|
|
|
|
property ShellStateManager manager: null
|
|
property int activeSelection: 0
|
|
property bool inBluetoothMenu: false
|
|
property bool inWallpaperMenu: false
|
|
property bool inNotificationsMenu: false
|
|
property bool inPowerMenu: false
|
|
property bool inPowerConfirm: false
|
|
|
|
property bool isSelected: false
|
|
|
|
property string wallpapersDir: (Quickshell.env("HOME") || "") + "/Pictures/Wallpapers"
|
|
property string wallpaperCachePath: (Quickshell.env("HOME") || "") + "/.cache/.wallpaper"
|
|
property string currentWallpaperPath: ""
|
|
|
|
function clamp(v, lo, hi) {
|
|
return Math.max(lo, Math.min(hi, v));
|
|
}
|
|
|
|
function menuLength() {
|
|
if (root.inBluetoothMenu && bluetoothRepeater)
|
|
return bluetoothRepeater.count;
|
|
if (root.inWallpaperMenu && wallpaperFolderModel)
|
|
return wallpaperFolderModel.count;
|
|
if (!menuModel)
|
|
return 0;
|
|
if (typeof menuModel.count === "function")
|
|
return menuModel.count();
|
|
if (menuModel.count !== undefined)
|
|
return menuModel.count;
|
|
if (menuModel.length !== undefined)
|
|
return menuModel.length;
|
|
return 0;
|
|
}
|
|
|
|
function menuAt(index) {
|
|
if (!menuModel)
|
|
return null;
|
|
if (menuModel.get)
|
|
return menuModel.get(index);
|
|
if (menuModel.length !== undefined)
|
|
return menuModel[index];
|
|
return null;
|
|
}
|
|
|
|
function wrapIndex(i) {
|
|
const length = menuLength();
|
|
if (length === 0)
|
|
return 0;
|
|
return (i + length) % length;
|
|
}
|
|
|
|
function currentAction() {
|
|
if (root.inBluetoothMenu || root.inWallpaperMenu || root.inNotificationsMenu || root.inPowerMenu)
|
|
return null;
|
|
return menuLength() > 0 ? menuAt(activeSelection) : null;
|
|
}
|
|
|
|
function clampSelection() {
|
|
const length = menuLength();
|
|
if (length === 0) {
|
|
activeSelection = 0;
|
|
scrollOffset = 0;
|
|
return;
|
|
}
|
|
activeSelection = clamp(activeSelection, 0, length - 1);
|
|
const maxOffset = Math.max(0, length - visibleRows);
|
|
scrollOffset = clamp(scrollOffset, 0, maxOffset);
|
|
}
|
|
|
|
function ensureVisible() {
|
|
if (!root.inWallpaperMenu)
|
|
return;
|
|
const length = menuLength();
|
|
if (length === 0) {
|
|
scrollOffset = 0;
|
|
return;
|
|
}
|
|
if (activeSelection < scrollOffset)
|
|
scrollOffset = activeSelection;
|
|
else if (activeSelection >= scrollOffset + visibleRows)
|
|
scrollOffset = Math.max(0, activeSelection - visibleRows + 1);
|
|
}
|
|
|
|
function bluetoothDisplayName(device) {
|
|
if (!device)
|
|
return "";
|
|
if (device.name && device.name.length > 0)
|
|
return device.name;
|
|
return device.deviceName || "";
|
|
}
|
|
|
|
function bluetoothDisplayState(device) {
|
|
if (!device)
|
|
return "";
|
|
// dont change symbols
|
|
return device.connected ? "✓" : "";
|
|
}
|
|
|
|
function bluetoothDeviceAt(index) {
|
|
const item = bluetoothRepeater.itemAt(index);
|
|
if (!item)
|
|
return null;
|
|
return item.device || null;
|
|
}
|
|
|
|
function trimText(text) {
|
|
return (text || "").replace(/^\s+|\s+$/g, "");
|
|
}
|
|
|
|
function normalizedPath(path) {
|
|
var value = trimText(path);
|
|
if (value.indexOf("file://") === 0)
|
|
value = value.slice(7);
|
|
return value;
|
|
}
|
|
|
|
function wallpaperNameAt(index) {
|
|
if (!wallpaperFolderModel || index < 0 || index >= wallpaperFolderModel.count)
|
|
return "";
|
|
var name = wallpaperFolderModel.get(index, "fileName") || "";
|
|
return name.replace(/\.[^/.]+$/, "");
|
|
}
|
|
|
|
function wallpaperPathAt(index) {
|
|
if (!wallpaperFolderModel || index < 0 || index >= wallpaperFolderModel.count)
|
|
return "";
|
|
return wallpaperFolderModel.get(index, "filePath") || "";
|
|
}
|
|
|
|
function currentWallpaperIndex() {
|
|
var current = normalizedPath(currentWallpaperPath);
|
|
if (!current || !wallpaperFolderModel)
|
|
return -1;
|
|
for (var i = 0; i < wallpaperFolderModel.count; i++) {
|
|
if (normalizedPath(wallpaperPathAt(i)) === current)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function refreshCurrentWallpaper() {
|
|
root.currentWallpaperPath = normalizedPath(wallpaperFile.text());
|
|
}
|
|
|
|
function applyWallpaper(path) {
|
|
var normalized = normalizedPath(path);
|
|
if (!normalized || normalized.length === 0)
|
|
return;
|
|
|
|
root.currentWallpaperPath = normalized;
|
|
wallpaperFile.setText(normalized + "\n");
|
|
|
|
wallpaperApplyProcess.running = false;
|
|
wallpaperApplyProcess.environment = {
|
|
"WALLPAPER_PATH": normalized
|
|
};
|
|
wallpaperApplyProcess.command = ["bash", "-lc", "rm -rf \"$HOME/.cache/wal\"; if [[ \"$(hostname)\" != \"gentoo\" ]]; then swww img \"$WALLPAPER_PATH\" --transition-type none; else HYPRPAPER_PID=\"$(pidof hyprpaper)\"; if [ ${#HYPRPAPER_PID} -lt 1 ]; then hyprctl dispatch exec hyprpaper; sleep 1; fi; hyprctl hyprpaper unload all; hyprctl hyprpaper preload \"$WALLPAPER_PATH\"; hyprctl hyprpaper wallpaper ,\"$WALLPAPER_PATH\"; fi"];
|
|
wallpaperApplyProcess.running = true;
|
|
}
|
|
|
|
PwObjectTracker {
|
|
objects: [Pipewire.defaultAudioSink]
|
|
}
|
|
|
|
property int bluetoothActionIndex: 1
|
|
property int wallpaperActionIndex: 2
|
|
property int notificationsActionIndex: 3
|
|
property int powerActionIndex: 4
|
|
property int pendingPowerActionIndex: 0
|
|
property var pendingPowerCommand: []
|
|
property var actions: [
|
|
{
|
|
name: "Master Volume",
|
|
arr: function (dir) {
|
|
const sink = Pipewire.defaultAudioSink;
|
|
if (!sink || !sink.audio)
|
|
return;
|
|
const step = 0.05;
|
|
sink.audio.muted = false;
|
|
sink.audio.volume = clamp(sink.audio.volume + dir * step, 0, 1);
|
|
},
|
|
getState: function () {
|
|
const sink = Pipewire.defaultAudioSink;
|
|
if (!sink || !sink.audio)
|
|
return "—";
|
|
return Math.round(sink.audio.volume * 100) + "%";
|
|
}
|
|
},
|
|
{
|
|
name: "Bluetooth",
|
|
ent: function () {
|
|
root.inBluetoothMenu = true;
|
|
root.inWallpaperMenu = false;
|
|
root.inNotificationsMenu = false;
|
|
root.isSelected = false;
|
|
root.activeSelection = 0;
|
|
root.clampSelection();
|
|
},
|
|
getState: function () {
|
|
return "";
|
|
}
|
|
},
|
|
{
|
|
name: "Wallpaper",
|
|
ent: function () {
|
|
root.inWallpaperMenu = true;
|
|
root.inBluetoothMenu = false;
|
|
root.inNotificationsMenu = false;
|
|
root.isSelected = false;
|
|
var currentIndex = root.currentWallpaperIndex();
|
|
root.activeSelection = currentIndex >= 0 ? currentIndex : 0;
|
|
root.scrollOffset = 0;
|
|
root.clampSelection();
|
|
root.ensureVisible();
|
|
},
|
|
getState: function () {
|
|
return "";
|
|
}
|
|
},
|
|
{
|
|
name: "Notifications",
|
|
ent: function () {
|
|
root.inNotificationsMenu = true;
|
|
root.inBluetoothMenu = false;
|
|
root.inWallpaperMenu = false;
|
|
root.inPowerMenu = false;
|
|
root.inPowerConfirm = false;
|
|
root.isSelected = false;
|
|
root.activeSelection = 0;
|
|
root.clampSelection();
|
|
},
|
|
getState: function () {
|
|
return "";
|
|
}
|
|
},
|
|
{
|
|
name: "Power",
|
|
ent: function () {
|
|
root.inPowerMenu = true;
|
|
root.inPowerConfirm = false;
|
|
root.inBluetoothMenu = false;
|
|
root.inWallpaperMenu = false;
|
|
root.inNotificationsMenu = false;
|
|
root.isSelected = false;
|
|
root.activeSelection = 0;
|
|
root.pendingPowerActionIndex = 0;
|
|
root.pendingPowerCommand = [];
|
|
root.clampSelection();
|
|
},
|
|
getState: function () {
|
|
return "";
|
|
}
|
|
}
|
|
]
|
|
|
|
property var powerActions: [
|
|
{
|
|
name: "Sleep",
|
|
command: ["systemctl", "suspend"]
|
|
},
|
|
{
|
|
name: "Shutdown",
|
|
command: ["systemctl", "poweroff"]
|
|
},
|
|
{
|
|
name: "Reboot",
|
|
command: ["systemctl", "reboot"]
|
|
},
|
|
{
|
|
name: "Logout",
|
|
command: ["hyprctl", "dispatch", "exit"]
|
|
}
|
|
]
|
|
|
|
property var powerConfirmActions: [
|
|
{
|
|
name: "Yes"
|
|
},
|
|
{
|
|
name: "No"
|
|
}
|
|
]
|
|
|
|
property var notificationActions: [
|
|
{
|
|
name: "Stack Preview",
|
|
ent: function () {
|
|
if (manager && manager.toggleNotificationStackPreviewEnabled)
|
|
manager.toggleNotificationStackPreviewEnabled();
|
|
},
|
|
getState: function () {
|
|
if (!manager || !manager.notificationStackPreviewEnabled)
|
|
return "ON";
|
|
return manager.notificationStackPreviewEnabled() ? "ON" : "OFF";
|
|
}
|
|
}
|
|
]
|
|
|
|
property var menuModel: root.inBluetoothMenu ? Bluetooth.devices : (root.inWallpaperMenu ? wallpaperFolderModel : (root.inNotificationsMenu ? notificationActions : (root.inPowerMenu ? (root.inPowerConfirm ? powerConfirmActions : powerActions) : actions)))
|
|
|
|
FolderListModel {
|
|
id: wallpaperFolderModel
|
|
folder: "file://" + root.wallpapersDir
|
|
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.webp", "*.bmp", "*.gif"]
|
|
showDirs: false
|
|
showDotAndDotDot: false
|
|
sortField: FolderListModel.Name
|
|
sortReversed: false
|
|
onCountChanged: {
|
|
if (root.inWallpaperMenu) {
|
|
root.clampSelection();
|
|
root.ensureVisible();
|
|
}
|
|
}
|
|
}
|
|
|
|
FileView {
|
|
id: wallpaperFile
|
|
path: root.wallpaperCachePath
|
|
watchChanges: true
|
|
onLoaded: root.refreshCurrentWallpaper()
|
|
onFileChanged: reload()
|
|
onLoadFailed: root.currentWallpaperPath = ""
|
|
}
|
|
|
|
Process {
|
|
id: wallpaperApplyProcess
|
|
}
|
|
|
|
Process {
|
|
id: powerCommandProcess
|
|
}
|
|
|
|
function executePowerCommand(command) {
|
|
if (!command || command.length === 0)
|
|
return;
|
|
powerCommandProcess.running = false;
|
|
powerCommandProcess.command = command;
|
|
powerCommandProcess.running = true;
|
|
if (manager && manager.closeShell)
|
|
manager.closeShell();
|
|
}
|
|
|
|
function pendingPowerActionName() {
|
|
if (pendingPowerActionIndex < 0 || pendingPowerActionIndex >= powerActions.length)
|
|
return "";
|
|
var action = powerActions[pendingPowerActionIndex];
|
|
return action && action.name ? String(action.name) : "";
|
|
}
|
|
|
|
Connections {
|
|
target: Bluetooth.devices
|
|
ignoreUnknownSignals: true
|
|
function onCountChanged() {
|
|
if (root.inBluetoothMenu)
|
|
root.clampSelection();
|
|
}
|
|
function onModelReset() {
|
|
if (root.inBluetoothMenu)
|
|
root.clampSelection();
|
|
}
|
|
function onRowsInserted() {
|
|
if (root.inBluetoothMenu)
|
|
root.clampSelection();
|
|
}
|
|
function onRowsRemoved() {
|
|
if (root.inBluetoothMenu)
|
|
root.clampSelection();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: wallpaperFolderModel
|
|
ignoreUnknownSignals: true
|
|
function onModelReset() {
|
|
if (root.inWallpaperMenu) {
|
|
root.clampSelection();
|
|
root.ensureVisible();
|
|
}
|
|
}
|
|
function onRowsInserted() {
|
|
if (root.inWallpaperMenu) {
|
|
root.clampSelection();
|
|
root.ensureVisible();
|
|
}
|
|
}
|
|
function onRowsRemoved() {
|
|
if (root.inWallpaperMenu) {
|
|
root.clampSelection();
|
|
root.ensureVisible();
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
text: root.inPowerConfirm ? "CONFIRM" : (root.inPowerMenu ? "POWER" : (root.inBluetoothMenu ? "BLUETOOTH" : (root.inWallpaperMenu ? "WALLPAPER" : (root.inNotificationsMenu ? "NOTIFICATIONS" : "CONFIG"))))
|
|
font.family: "8bitoperator JVE"
|
|
font.pixelSize: 71
|
|
renderType: Text.NativeRendering
|
|
font.hintingPreference: Font.PreferNoHinting
|
|
smooth: false
|
|
antialiasing: false
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
color: "#ffffff"
|
|
y: 32
|
|
}
|
|
|
|
Text {
|
|
visible: root.inPowerConfirm
|
|
text: root.pendingPowerActionName().toUpperCase() + "?"
|
|
font.family: "8bitoperator JVE"
|
|
font.pixelSize: 44
|
|
renderType: Text.NativeRendering
|
|
font.hintingPreference: Font.PreferNoHinting
|
|
smooth: false
|
|
antialiasing: false
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
color: "#fefe00"
|
|
y: 102
|
|
}
|
|
|
|
Repeater {
|
|
id: bluetoothRepeater
|
|
model: root.menuModel
|
|
|
|
delegate: Item {
|
|
width: root.width
|
|
height: lineHeight
|
|
x: 0
|
|
y: menuTop + (root.inWallpaperMenu ? (index - root.scrollOffset) * lineHeight : index * lineHeight)
|
|
visible: !root.inWallpaperMenu || (index >= root.scrollOffset && index < root.scrollOffset + root.visibleRows)
|
|
|
|
property var device: root.inBluetoothMenu ? modelData : null
|
|
|
|
Image {
|
|
source: "./soul.png"
|
|
width: 36
|
|
height: 36
|
|
x: 182
|
|
y: 8 + 14
|
|
visible: root.activeSelection == index
|
|
}
|
|
|
|
Text {
|
|
x: 239
|
|
y: 0
|
|
text: root.inBluetoothMenu ? root.bluetoothDisplayName(modelData) : (root.inWallpaperMenu ? root.wallpaperNameAt(index) : modelData.name)
|
|
width: root.width - 239 - (root.inWallpaperMenu ? 96 : 300)
|
|
font.family: "8bitoperator JVE"
|
|
font.pixelSize: 71
|
|
font.letterSpacing: 1
|
|
renderType: Text.NativeRendering
|
|
font.hintingPreference: Font.PreferNoHinting
|
|
smooth: false
|
|
antialiasing: false
|
|
wrapMode: Text.NoWrap
|
|
elide: Text.ElideRight
|
|
color: (root.activeSelection == index && (root.isSelected == true || root.inBluetoothMenu || root.inWallpaperMenu || root.inPowerMenu)) ? "#fefe00" : "#ffffff"
|
|
}
|
|
|
|
// Option state
|
|
Text {
|
|
x: menuLeft + stateColumnX
|
|
y: 4
|
|
visible: !root.inWallpaperMenu
|
|
text: root.inBluetoothMenu ? root.bluetoothDisplayState(modelData) : (modelData.getState ? modelData.getState() : "")
|
|
font.family: "8bitoperator JVE"
|
|
font.pixelSize: 71
|
|
font.letterSpacing: 1
|
|
renderType: Text.NativeRendering
|
|
font.hintingPreference: Font.PreferNoHinting
|
|
smooth: false
|
|
antialiasing: false
|
|
color: (root.activeSelection == index && (root.isSelected == true || root.inBluetoothMenu || root.inWallpaperMenu || root.inPowerMenu)) ? "#fefe00" : "#ffffff"
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ------------------------------
|
|
INPUT HANDLING
|
|
------------------------------ */
|
|
|
|
function handleKey(key) {
|
|
switch (key) {
|
|
case Qt.Key_Up:
|
|
if (root.inBluetoothMenu || root.inWallpaperMenu || root.inPowerMenu || root.isSelected === false) {
|
|
activeSelection = wrapIndex(activeSelection - 1);
|
|
root.ensureVisible();
|
|
}
|
|
return true;
|
|
case Qt.Key_Down:
|
|
if (root.inBluetoothMenu || root.inWallpaperMenu || root.inPowerMenu || root.isSelected === false) {
|
|
activeSelection = wrapIndex(activeSelection + 1);
|
|
root.ensureVisible();
|
|
}
|
|
return true;
|
|
case Qt.Key_Left:
|
|
{
|
|
const a = currentAction();
|
|
if (a && a.arr && root.isSelected === true)
|
|
a.arr(-1);
|
|
return true;
|
|
}
|
|
case Qt.Key_Right:
|
|
{
|
|
const a = currentAction();
|
|
if (a && a.arr && root.isSelected === true)
|
|
a.arr(1);
|
|
return true;
|
|
}
|
|
case Qt.Key_Z:
|
|
case Qt.Key_Return:
|
|
case Qt.Key_Enter:
|
|
{
|
|
if (root.inBluetoothMenu) {
|
|
const device = bluetoothDeviceAt(activeSelection);
|
|
console.log(root.inBluetoothMenu, activeSelection, device);
|
|
if (device) {
|
|
if (!device.connected) {
|
|
device.connect();
|
|
} else {
|
|
device.disconnect();
|
|
}
|
|
// device.connected = !device.connected;
|
|
}
|
|
} else if (root.inWallpaperMenu) {
|
|
const wallpaperPath = wallpaperPathAt(activeSelection);
|
|
if (wallpaperPath && wallpaperPath.length > 0)
|
|
root.applyWallpaper(wallpaperPath);
|
|
} else if (root.inNotificationsMenu) {
|
|
const notificationAction = menuAt(activeSelection);
|
|
if (notificationAction && notificationAction.ent)
|
|
notificationAction.ent();
|
|
} else if (root.inPowerConfirm) {
|
|
const confirmAction = menuAt(activeSelection);
|
|
if (confirmAction && confirmAction.name === "Yes") {
|
|
root.executePowerCommand(root.pendingPowerCommand);
|
|
} else {
|
|
root.inPowerConfirm = false;
|
|
root.activeSelection = root.pendingPowerActionIndex;
|
|
root.clampSelection();
|
|
}
|
|
} else if (root.inPowerMenu) {
|
|
const powerAction = menuAt(activeSelection);
|
|
if (powerAction && powerAction.command) {
|
|
root.pendingPowerActionIndex = activeSelection;
|
|
root.pendingPowerCommand = powerAction.command;
|
|
root.inPowerConfirm = true;
|
|
root.activeSelection = 0;
|
|
root.clampSelection();
|
|
}
|
|
} else {
|
|
const a = currentAction();
|
|
if (a && a.ent) {
|
|
a.ent();
|
|
} else {
|
|
root.isSelected = !root.isSelected;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
case Qt.Key_X:
|
|
case Qt.Key_Shift:
|
|
case Qt.Key_Escape:
|
|
if (root.inWallpaperMenu) {
|
|
root.inWallpaperMenu = false;
|
|
root.isSelected = false;
|
|
root.scrollOffset = 0;
|
|
root.activeSelection = root.wallpaperActionIndex;
|
|
root.clampSelection();
|
|
} else if (root.inPowerConfirm) {
|
|
root.inPowerConfirm = false;
|
|
root.isSelected = false;
|
|
root.activeSelection = root.pendingPowerActionIndex;
|
|
root.clampSelection();
|
|
} else if (root.inPowerMenu) {
|
|
root.inPowerMenu = false;
|
|
root.inPowerConfirm = false;
|
|
root.isSelected = false;
|
|
root.pendingPowerActionIndex = 0;
|
|
root.pendingPowerCommand = [];
|
|
root.activeSelection = root.powerActionIndex;
|
|
root.clampSelection();
|
|
} else if (root.inNotificationsMenu) {
|
|
root.inNotificationsMenu = false;
|
|
root.isSelected = false;
|
|
root.activeSelection = root.notificationsActionIndex;
|
|
root.clampSelection();
|
|
} else if (root.inBluetoothMenu) {
|
|
root.inBluetoothMenu = false;
|
|
root.isSelected = false;
|
|
root.activeSelection = root.bluetoothActionIndex;
|
|
root.clampSelection();
|
|
} else if (root.isSelected === true) {
|
|
root.isSelected = false;
|
|
} else {
|
|
if (manager && manager.closeQuickSettings) {
|
|
manager.closeQuickSettings();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
root.activeSelection = 0;
|
|
root.isSelected = false;
|
|
root.inBluetoothMenu = false;
|
|
root.inWallpaperMenu = false;
|
|
root.inNotificationsMenu = false;
|
|
root.inPowerMenu = false;
|
|
root.inPowerConfirm = false;
|
|
root.pendingPowerActionIndex = 0;
|
|
root.pendingPowerCommand = [];
|
|
root.scrollOffset = 0;
|
|
root.refreshCurrentWallpaper();
|
|
ShellInputManager.registerHandler("quickSettings", handleKey);
|
|
}
|
|
|
|
Component.onDestruction: {
|
|
ShellInputManager.unregisterHandler("quickSettings");
|
|
}
|
|
}
|