mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-16 00:04:01 -04:00
339 lines
12 KiB
QML
339 lines
12 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import qs.Common
|
|
import qs.Services
|
|
import "../../Common/WidgetPlacement.js" as Placement
|
|
import qs.DCommon.Session
|
|
|
|
FocusScope {
|
|
id: root
|
|
|
|
required property string screenName
|
|
property bool lockScreen: false
|
|
property bool greeter: false
|
|
property var lockHost: null
|
|
property bool editMode: false
|
|
property string selectedInstanceId: ""
|
|
|
|
readonly property var screen: Quickshell.screens.find(s => s.name === screenName) ?? null
|
|
readonly property string listKey: greeter ? "greeterWidgetInstances" : (lockScreen ? "lockScreenWidgetInstances" : "desktopWidgetInstances")
|
|
readonly property var instances: (greeter ? SettingsData.greeterWidgetInstances : (lockScreen ? SettingsData.lockScreenWidgetInstances : SettingsData.desktopWidgetInstances)) || []
|
|
readonly property string screenKey: SettingsData.getScreenDisplayName(screen)
|
|
property var _gridSettingsTrigger: lockScreen ? SessionData.lockScreenWidgetGridSettings : SessionData.desktopWidgetGridSettings
|
|
readonly property int gridSize: {
|
|
void _gridSettingsTrigger;
|
|
return SessionData.getWidgetGridSetting(lockScreen, screenKey, "size", 40);
|
|
}
|
|
readonly property bool gridEnabled: {
|
|
void _gridSettingsTrigger;
|
|
return SessionData.getWidgetGridSetting(lockScreen, screenKey, "enabled", false);
|
|
}
|
|
|
|
signal focusStolen
|
|
signal optionsRequested(string instanceId)
|
|
|
|
// Widgets added from the editor start in the nearest free area instead of on top of another widget.
|
|
property var pendingIds: []
|
|
property real bottomInset: 0
|
|
|
|
function rectOf(item) {
|
|
return {
|
|
x: item.x,
|
|
y: item.y,
|
|
width: item.width,
|
|
height: item.height
|
|
};
|
|
}
|
|
|
|
function placeIfPending(item) {
|
|
if (!pendingIds.includes(item.instanceId))
|
|
return;
|
|
pendingIds = pendingIds.filter(id => id !== item.instanceId);
|
|
if (!editMode || item.width <= 0 || item.height <= 0)
|
|
return;
|
|
const obstacles = [];
|
|
for (let i = 0; i < repeater.count; i++) {
|
|
const other = repeater.itemAt(i);
|
|
if (other && other !== item && other.visible)
|
|
obstacles.push(rectOf(other));
|
|
}
|
|
const rect = rectOf(item);
|
|
if (!obstacles.some(obstacle => Placement.intersects(rect, obstacle)))
|
|
return;
|
|
const free = Placement.candidates(width, height - bottomInset, item.width, item.height, Theme.spacingXL, obstacles);
|
|
if (!free.length)
|
|
return;
|
|
const centerX = width / 2;
|
|
const centerY = height / 2;
|
|
free.sort((a, b) => Math.hypot(a.x + a.width / 2 - centerX, a.y + a.height / 2 - centerY) - Math.hypot(b.x + b.width / 2 - centerX, b.y + b.height / 2 - centerY));
|
|
item.commitPosition(free[0].x, free[0].y);
|
|
}
|
|
|
|
function toggleGrid() {
|
|
SessionData.setWidgetGridSetting(lockScreen, screenKey, "enabled", !gridEnabled);
|
|
}
|
|
|
|
function stepGrid(delta) {
|
|
SessionData.setWidgetGridSetting(lockScreen, screenKey, "size", Math.max(10, Math.min(200, gridSize + delta)));
|
|
}
|
|
|
|
// Called by the common ancestor of this layer and the editor bar, since either side can hold focus.
|
|
function handleKey(event) {
|
|
if (!editMode)
|
|
return false;
|
|
switch (event.key) {
|
|
case Qt.Key_G:
|
|
toggleGrid();
|
|
return true;
|
|
case Qt.Key_Z:
|
|
stepGrid(-10);
|
|
return true;
|
|
case Qt.Key_X:
|
|
stepGrid(10);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
activeFocusOnTab: false
|
|
onActiveFocusChanged: {
|
|
if (activeFocus)
|
|
focusStolen();
|
|
}
|
|
|
|
function itemOfType(widgetType) {
|
|
for (let i = 0; i < repeater.count; i++) {
|
|
const item = repeater.itemAt(i);
|
|
if (item && item.visible && item.widgetType === widgetType)
|
|
return item;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
readonly property real edgeInset: Theme.spacingXL * 2
|
|
property bool completed: false
|
|
property var lockedPositions: ({})
|
|
// Snapshot taken when a drag or resize starts so the auto-placed clock cannot jump under the pointer.
|
|
property var heldPlacement: ({})
|
|
readonly property string backgroundKey: lockScreen ? LockPlacementService.backgroundKey(screenName, width, height) : ""
|
|
readonly property var autoPositions: {
|
|
if (!lockScreen || !editMode || !completed)
|
|
return lockedPositions;
|
|
if (interactingItem)
|
|
return heldPlacement;
|
|
return currentPlacement();
|
|
}
|
|
onInteractingItemChanged: {
|
|
if (lockScreen && interactingItem)
|
|
heldPlacement = currentPlacement();
|
|
}
|
|
|
|
function currentPlacement() {
|
|
return LockPlacementService.layoutFor(screenName, LockPlacementService.sampleFor(screenName, backgroundKey), placementKey, placeClocks);
|
|
}
|
|
|
|
// The greeter list never renders outside the editor, so the editor publishes its auto placement.
|
|
onAutoPositionsChanged: {
|
|
if (!greeter || !editMode || !completed || interactingItem || width <= 0 || height <= 0)
|
|
return;
|
|
SessionData.setGreeterAutoPositions(screenKey, {
|
|
width: width,
|
|
height: height,
|
|
positions: autoPositions
|
|
});
|
|
}
|
|
|
|
function refreshPlacement() {
|
|
if (!lockScreen || !completed || editMode || width <= 0 || height <= 0)
|
|
return;
|
|
lockedPositions = currentPlacement();
|
|
SessionData.setLockScreenAutoPositions(screenKey, {
|
|
width: width,
|
|
height: height,
|
|
positions: lockedPositions
|
|
});
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
completed = true;
|
|
refreshPlacement();
|
|
}
|
|
onWidthChanged: refreshPlacement()
|
|
onHeightChanged: refreshPlacement()
|
|
onEditModeChanged: refreshPlacement()
|
|
|
|
readonly property string placementKey: lockScreen ? JSON.stringify(instances.map(instance => {
|
|
const item = itemById(instance.id);
|
|
return [instance.id, instance.enabled, instance.config, item?.width, item?.height, item?.automaticPlacement ? null : item?.x, item?.automaticPlacement ? null : item?.y, item?.hasSavedPosition, item?.contrastColors];
|
|
}).concat([width, height, Theme.primary.toString(), Theme.secondary.toString(), Theme.lockScreenContentColor.toString(), SessionData.desktopWidgetInstancePositions])) : ""
|
|
|
|
function itemById(id) {
|
|
for (let i = 0; i < repeater.count; i++) {
|
|
const item = repeater.itemAt(i);
|
|
if (item && item.visible && item.instanceId === id)
|
|
return item;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function stockRect(widgetType, item) {
|
|
if (!lockScreen)
|
|
return null;
|
|
const centerX = (width - item.width) / 2;
|
|
switch (widgetType) {
|
|
case "desktopClock":
|
|
return item.automaticPlacement && autoPositions[item.instanceId] ? autoPositions[item.instanceId] : {
|
|
x: Math.max(0, centerX),
|
|
y: edgeInset
|
|
};
|
|
case "lockDate":
|
|
return {
|
|
x: edgeInset,
|
|
y: edgeInset
|
|
};
|
|
case "lockAuth":
|
|
return {
|
|
x: centerX,
|
|
y: height / 2 - item.height / 2
|
|
};
|
|
case "lockNotifications":
|
|
{
|
|
const auth = itemOfType("lockAuth");
|
|
return {
|
|
x: centerX,
|
|
y: auth ? auth.y + auth.height + auth.bottomOverflow + Theme.spacingL : height / 2 + Theme.spacingL
|
|
};
|
|
}
|
|
case "lockStatus":
|
|
return {
|
|
x: width - Theme.spacingXL - item.width,
|
|
y: Theme.spacingXL
|
|
};
|
|
case "lockPower":
|
|
return {
|
|
x: Theme.spacingXL,
|
|
y: height - Theme.spacingXL - item.height
|
|
};
|
|
case "greeterSession":
|
|
return {
|
|
x: width - Theme.spacingXL - item.width,
|
|
y: height - Theme.spacingXL - item.height
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function placeClocks(luminances, sampleWidth, sampleHeight) {
|
|
const clocks = [];
|
|
const obstacles = [];
|
|
for (let i = 0; i < repeater.count; i++) {
|
|
const item = repeater.itemAt(i);
|
|
if (!item || !item.visible)
|
|
continue;
|
|
if (item.automaticPlacement) {
|
|
clocks.push(item);
|
|
continue;
|
|
}
|
|
obstacles.push({
|
|
x: item.x - Theme.spacingL,
|
|
y: item.y - Theme.spacingL,
|
|
width: item.width + Theme.spacingL * 2,
|
|
height: item.height + Theme.spacingL * 2
|
|
});
|
|
}
|
|
const positions = {};
|
|
for (const clock of clocks) {
|
|
const choices = Placement.candidates(width, height, clock.width, clock.height, edgeInset, obstacles);
|
|
const foregrounds = clock.contrastColors.map(color => Placement.luminance(color.r, color.g, color.b));
|
|
const chosen = Placement.choose(luminances, sampleWidth, sampleHeight, width, height, choices, foregrounds);
|
|
if (!chosen)
|
|
continue;
|
|
positions[clock.instanceId] = {
|
|
x: chosen.x,
|
|
y: chosen.y
|
|
};
|
|
obstacles.push(chosen);
|
|
}
|
|
return positions;
|
|
}
|
|
|
|
readonly property var draggedItem: {
|
|
for (let i = 0; i < repeater.count; i++) {
|
|
const item = repeater.itemAt(i);
|
|
if (item && item.dragging)
|
|
return item;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
readonly property var interactingItem: {
|
|
for (let i = 0; i < repeater.count; i++) {
|
|
const item = repeater.itemAt(i);
|
|
if (item && item.interacting)
|
|
return item;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
enabled: root.editMode
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
onPressed: root.selectedInstanceId = ""
|
|
}
|
|
|
|
Rectangle {
|
|
x: (parent.width - width) / 2
|
|
width: Theme.dividerWidth
|
|
height: parent.height
|
|
visible: root.editMode && root.draggedItem !== null
|
|
color: root.draggedItem?.snappedCenterX ? Theme.primary : Theme.withAlpha(Theme.primary, Theme.stateLayerDrag)
|
|
}
|
|
|
|
Rectangle {
|
|
y: (parent.height - height) / 2
|
|
width: parent.width
|
|
height: Theme.dividerWidth
|
|
visible: root.editMode && root.draggedItem !== null
|
|
color: root.draggedItem?.snappedCenterY ? Theme.primary : Theme.withAlpha(Theme.primary, Theme.stateLayerDrag)
|
|
}
|
|
|
|
Loader {
|
|
anchors.fill: parent
|
|
active: root.editMode && root.gridEnabled
|
|
|
|
sourceComponent: DesktopWidgetGridLines {
|
|
gridSize: root.gridSize
|
|
extentWidth: root.width
|
|
extentHeight: root.height
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
id: repeater
|
|
model: ScriptModel {
|
|
objectProp: "id"
|
|
values: root.instances
|
|
}
|
|
|
|
WidgetEditItem {
|
|
required property var modelData
|
|
required property int index
|
|
|
|
readonly property var liveInstanceData: root.instances.find(inst => inst.id === modelData.id) ?? modelData
|
|
|
|
instanceData: liveInstanceData
|
|
screen: root.screen
|
|
hostLayer: root
|
|
lockHost: root.lockHost
|
|
editMode: root.editMode
|
|
visible: liveInstanceData.enabled !== false && DesktopWidgetRegistry.showsOnScreen(liveInstanceData.config?.displayPreferences, root.screen)
|
|
z: interacting ? repeater.count + 1 : repeater.count - index
|
|
onRemoveRequested: SettingsData.removeDesktopWidgetInstance(liveInstanceData.id)
|
|
onOptionsRequested: root.optionsRequested(liveInstanceData.id)
|
|
}
|
|
}
|
|
}
|