lock: allow customization, including any desktop widget, to be added and

arranged on lock screen
This commit is contained in:
bbedward committed 2026-10-04 15:46:56 -04:00
1 parent 375aa2f492
commit d7dc8ae5d2
56 files changed
+4244 -2159

No files matched your search

+1 -1
View File
@@ -82,7 +82,7 @@ require (
)
require (
github.com/AvengeMedia/dankgo v1.6.3-0.20261002194355-b7285561e19f
github.com/AvengeMedia/dankgo v1.6.3-0.20261004193546-f560632e8bd2
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
+2 -2
View File
@@ -1,7 +1,7 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/AvengeMedia/dankgo v1.6.3-0.20261002194355-b7285561e19f h1:9n423TuyY2cF0/sZg0JjGSPx9yW6zSdgsVFoSVEHmek=
github.com/AvengeMedia/dankgo v1.6.3-0.20261002194355-b7285561e19f/go.mod h1:Fj+FTCumlv8YjKmdv75DREJuGmqU9EnGpBroZuCWc/k=
github.com/AvengeMedia/dankgo v1.6.3-0.20261004193546-f560632e8bd2 h1:RX0Dy4ays41fEEN86JVf7d4zYoCeq49ttgv7Ynjy56c=
github.com/AvengeMedia/dankgo v1.6.3-0.20261004193546-f560632e8bd2/go.mod h1:Fj+FTCumlv8YjKmdv75DREJuGmqU9EnGpBroZuCWc/k=
github.com/AvengeMedia/dgop v1.6.3-0.20260920163359-59c7d81ae3b9 h1:ssoiAng7ng500BgtZa1riJH6kKudQVM8Uk9a9kF7mDA=
github.com/AvengeMedia/dgop v1.6.3-0.20260920163359-59c7d81ae3b9/go.mod h1:8TFIP72NLxCbogF2hHU6sXj3RoTbHQW8beeCrisHs/Y=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
@@ -9,7 +9,7 @@ import (
)
func TestLockScreenPasswordFieldBypassesTextInputIME(t *testing.T) {
data, err := os.ReadFile("../../../quickshell/Modules/Lock/LockScreenContent.qml")
data, err := os.ReadFile("../../../quickshell/Modules/Lock/Widgets/LockAuthWidget.qml")
if err != nil {
t.Fatalf("read lock screen QML: %v", err)
}
Generated
+3 -3
View File
@@ -3,11 +3,11 @@
"dank-qml-common": {
"flake": false,
"locked": {
"lastModified": 1790974203,
"narHash": "sha256-C2DAGa/NkSye4NaANuxxkSeNb4xX1g3gZBsKO5KRIzk=",
"lastModified": 1791142152,
"narHash": "sha256-tVzNbgalwjVVyzYm5d6FjKkCaZ5DbZPw9uyvnTNC2EA=",
"owner": "AvengeMedia",
"repo": "dank-qml-common",
"rev": "13470a9b545f4c2c2fb8f237db6de2b5f7c26b41",
"rev": "07d634a3fc731afdcd309d385f3134ba8c22b37c",
"type": "github"
},
"original": {
+1 -1
View File
@@ -111,7 +111,7 @@
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-WFufeGR21sidUJVGQTygubcaBB/aOIkKcBKf5dtwwVk=";
vendorHash = "sha256-dScMtUqccp6C8b/hIOFTjdgm+mOy921bEbTuXqcp4ag=";
subPackages = [ "cmd/dms" ];
+4 -1
View File
@@ -24,7 +24,7 @@ Singleton {
readonly property var _pinKeys: ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"]
readonly property var _historyKeys: ["browserUsageHistory", "filePickerUsageHistory"]
readonly property var _dataKeys: ["fileBrowserSettings", "processFilterTypes", "pluginViewSort", "pluginViewFilter", "dashFocusCardId", "controlCenterCollapsedCategories", "mediaLyricsOpen", "matugenPreviews", "matugenSeedPreviews", "matugenAppliedKey"].concat(_pinKeys, _historyKeys)
readonly property var _dataKeys: ["fileBrowserSettings", "processFilterTypes", "pluginViewSort", "pluginViewFilter", "dashFocusCardId", "controlCenterCollapsedCategories", "mediaLyricsOpen", "matugenPreviews", "matugenSeedPreviews", "matugenAppliedKey", "lockScreenPlacementSamples"].concat(_pinKeys, _historyKeys)
property string pluginViewFilter: "enabled"
property string dashFocusCardId: ""
@@ -33,6 +33,7 @@ Singleton {
property var matugenPreviews: ({})
property var matugenSeedPreviews: ({})
property string matugenAppliedKey: ""
property var lockScreenPlacementSamples: ({})
property var pluginViewSort: ({
by: "modified",
descending: true
@@ -137,6 +138,7 @@ Singleton {
matugenPreviews = typeof cache.matugenPreviews?.key === "string" ? cache.matugenPreviews : {};
matugenSeedPreviews = cache.matugenSeedPreviews && typeof cache.matugenSeedPreviews === "object" && !Array.isArray(cache.matugenSeedPreviews) ? cache.matugenSeedPreviews : {};
matugenAppliedKey = typeof cache.matugenAppliedKey === "string" ? cache.matugenAppliedKey : "";
lockScreenPlacementSamples = cache.lockScreenPlacementSamples && typeof cache.lockScreenPlacementSamples === "object" && !Array.isArray(cache.lockScreenPlacementSamples) ? cache.lockScreenPlacementSamples : {};
const pluginSort = cache.pluginViewSort;
pluginViewSort = {
by: ["name", "author", "modified"].includes(pluginSort?.by) ? pluginSort.by : "modified",
@@ -218,6 +220,7 @@ Singleton {
"matugenPreviews": matugenPreviews,
"matugenSeedPreviews": matugenSeedPreviews,
"matugenAppliedKey": matugenAppliedKey,
"lockScreenPlacementSamples": lockScreenPlacementSamples,
"fileBrowserSettings": fileBrowserSettings,
"configVersion": cacheConfigVersion
};
+10
View File
@@ -292,6 +292,7 @@ Singleton {
property var activeDisplayProfileModes: ({})
property var desktopWidgetGridSettings: ({})
property var desktopWidgetInstancePositions: ({})
property var lockScreenAutoPositions: ({})
property var islandFreePositions: ({})
property var builtInPluginState: ({})
property bool greeterSyncPending: false
@@ -651,6 +652,15 @@ Singleton {
saveSettings();
}
function setLockScreenAutoPositions(screenKey, placement) {
if (JSON.stringify(lockScreenAutoPositions[screenKey] ?? null) === JSON.stringify(placement))
return;
const updated = Object.assign({}, lockScreenAutoPositions);
updated[screenKey] = placement;
lockScreenAutoPositions = updated;
saveSettings();
}
function removeDesktopWidgetInstancePositions(instanceId) {
if (!(instanceId in desktopWidgetInstancePositions))
return;
+60 -33
View File
@@ -21,7 +21,7 @@ Singleton {
id: root
readonly property var log: Log.scoped("SettingsData")
readonly property int settingsConfigVersion: 37
readonly property int settingsConfigVersion: 38
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -773,13 +773,7 @@ Singleton {
property bool modalDarkenBackground: Spec.SPEC.modalDarkenBackground.def
property bool lockScreenShowPowerActions: Spec.SPEC.lockScreenShowPowerActions.def
property bool lockScreenShowSystemIcons: Spec.SPEC.lockScreenShowSystemIcons.def
property bool lockScreenShowTime: Spec.SPEC.lockScreenShowTime.def
property string lockScreenClockStyle: Spec.SPEC.lockScreenClockStyle.def
property bool lockScreenShowDate: Spec.SPEC.lockScreenShowDate.def
property bool lockScreenShowProfileImage: Spec.SPEC.lockScreenShowProfileImage.def
property bool lockScreenShowPasswordField: Spec.SPEC.lockScreenShowPasswordField.def
property bool lockScreenShowMediaPlayer: Spec.SPEC.lockScreenShowMediaPlayer.def
property bool lockScreenShowWeather: Spec.SPEC.lockScreenShowWeather.def
property bool lockScreenPowerOffMonitorsOnLock: Spec.SPEC.lockScreenPowerOffMonitorsOnLock.def
property bool lockAtStartup: Spec.SPEC.lockAtStartup.def
@@ -813,7 +807,6 @@ Singleton {
property bool lockScreenSecurityKeyShortcutEnabled: Spec.SPEC.lockScreenSecurityKeyShortcutEnabled.def
property bool greeterPamExternallyManaged: Spec.SPEC.greeterPamExternallyManaged.def
property string lockScreenInactiveColor: Spec.SPEC.lockScreenInactiveColor.def
property int lockScreenNotificationMode: Spec.SPEC.lockScreenNotificationMode.def
property bool lockScreenVideoEnabled: Spec.SPEC.lockScreenVideoEnabled.def
property string lockScreenVideoPath: Spec.SPEC.lockScreenVideoPath.def
property bool lockScreenVideoCycling: Spec.SPEC.lockScreenVideoCycling.def
@@ -1119,6 +1112,34 @@ Singleton {
property var desktopWidgetInstances: Spec.SPEC.desktopWidgetInstances.def
property var desktopWidgetGroups: Spec.SPEC.desktopWidgetGroups.def
property var lockScreenWidgetInstances: Spec.SPEC.lockScreenWidgetInstances.def
readonly property var widgetInstanceListKeys: ["desktopWidgetInstances", "lockScreenWidgetInstances"]
// The greeter still reads these three shared keys, so they follow the lock widgets.
onLockScreenWidgetInstancesChanged: {
const status = lockWidgetInstance("lockStatus");
const auth = lockWidgetInstance("lockAuth");
const power = lockWidgetInstance("lockPower");
const mirror = (key, value) => {
if (root[key] !== value)
set(key, value);
};
mirror("lockScreenShowWeather", !!status && status.enabled !== false && (status.config?.showWeather ?? true));
mirror("lockScreenShowProfileImage", !!auth && (auth.config?.showProfileImage ?? true));
mirror("lockScreenShowPowerActions", !!power && power.enabled !== false);
}
function lockWidgetInstance(widgetType) {
return (lockScreenWidgetInstances || []).find(inst => inst.widgetType === widgetType) ?? null;
}
function resetLockScreenWidgets() {
for (const inst of lockScreenWidgetInstances || [])
SessionData.removeDesktopWidgetInstancePositions(inst.id);
for (const inst of Spec.SPEC.lockScreenWidgetInstances.def)
SessionData.removeDesktopWidgetInstancePositions(inst.id);
resetToDefault(["lockScreenWidgetInstances"]);
}
function getDefaultSystemMonitorConfig() {
return {
@@ -1149,45 +1170,53 @@ Singleton {
};
}
function createDesktopWidgetInstance(widgetType, name, config) {
const id = "dw_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
function widgetInstanceListKey(instanceId) {
return widgetInstanceListKeys.find(key => (root[key] || []).some(inst => inst.id === instanceId)) ?? "desktopWidgetInstances";
}
function createDesktopWidgetInstance(widgetType, name, config, listKey = "desktopWidgetInstances") {
const lockScreen = listKey === "lockScreenWidgetInstances";
const instance = {
id: id,
id: (lockScreen ? "lw_" : "dw_") + Date.now() + "_" + Math.random().toString(36).substr(2, 9),
widgetType: widgetType,
name: name || widgetType,
enabled: true,
config: config || {}
config: Object.assign(lockScreen ? {
syncPositionAcrossScreens: true
} : {}, config || {})
};
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
const instances = JSON.parse(JSON.stringify(root[listKey] || []));
instances.push(instance);
desktopWidgetInstances = instances;
root[listKey] = instances;
saveSettings();
return instance;
}
function updateDesktopWidgetInstance(instanceId, updates) {
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
const listKey = widgetInstanceListKey(instanceId);
const instances = JSON.parse(JSON.stringify(root[listKey] || []));
const idx = instances.findIndex(inst => inst.id === instanceId);
if (idx === -1)
return;
Object.assign(instances[idx], updates);
desktopWidgetInstances = instances;
root[listKey] = instances;
saveSettings();
}
function updateDesktopWidgetInstanceConfig(instanceId, configUpdates) {
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
const listKey = widgetInstanceListKey(instanceId);
const instances = JSON.parse(JSON.stringify(root[listKey] || []));
const idx = instances.findIndex(inst => inst.id === instanceId);
if (idx === -1)
return;
instances[idx].config = Object.assign({}, instances[idx].config || {}, configUpdates);
desktopWidgetInstances = instances;
root[listKey] = instances;
saveSettings();
}
function removeDesktopWidgetInstance(instanceId) {
const instances = (desktopWidgetInstances || []).filter(inst => inst.id !== instanceId);
desktopWidgetInstances = instances;
const listKey = widgetInstanceListKey(instanceId);
root[listKey] = (root[listKey] || []).filter(inst => inst.id !== instanceId);
SessionData.removeDesktopWidgetInstancePositions(instanceId);
saveSettings();
}
@@ -1196,23 +1225,21 @@ Singleton {
const source = getDesktopWidgetInstance(instanceId);
if (!source)
return null;
const newId = "dw_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
const instance = {
id: newId,
widgetType: source.widgetType,
name: source.name + " (Copy)",
enabled: source.enabled,
config: JSON.parse(JSON.stringify(source.config || {}))
};
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
instances.push(instance);
desktopWidgetInstances = instances;
saveSettings();
const instance = createDesktopWidgetInstance(source.widgetType, source.name + " (Copy)", JSON.parse(JSON.stringify(source.config || {})), widgetInstanceListKey(instanceId));
if (!source.enabled)
updateDesktopWidgetInstance(instance.id, {
enabled: false
});
return instance;
}
function getDesktopWidgetInstance(instanceId) {
return (desktopWidgetInstances || []).find(inst => inst.id === instanceId) || null;
for (const key of widgetInstanceListKeys) {
const found = (root[key] || []).find(inst => inst.id === instanceId);
if (found)
return found;
}
return null;
}
function moveDesktopWidgetInstanceToGroup(instanceId, groupId, newIndexInGroup) {
@@ -36,9 +36,6 @@ var LOCAL_SPEC = {
terminalOverride: {
def: ""
},
perModeWallpaper: {
def: false
},
wallpaperPathLight: {
def: ""
},
@@ -250,9 +247,6 @@ var LOCAL_SPEC = {
desktopWidgetGridSettings: {
def: {}
},
desktopWidgetInstancePositions: {
def: {}
},
islandFreePositions: {
def: {}
},
+4 -24
View File
@@ -3,6 +3,10 @@
.import "../../DankCommon/Common/settings/SpecUtil.js" as Util
.import "DockConfig.js" as DockConfig
function lockWidgetDefaults() {
return Shared.lockWidgetDefaults();
}
var LOCAL_SPEC = {
dockConfigs: {
def: [DockConfig.create("dock", "Dock")]
@@ -1143,24 +1147,6 @@ var LOCAL_SPEC = {
modalDarkenBackground: {
def: true
},
lockScreenShowSystemIcons: {
def: true
},
lockScreenShowTime: {
def: true
},
lockScreenClockStyle: {
def: "horizontal"
},
lockScreenShowDate: {
def: true
},
lockScreenShowPasswordField: {
def: true
},
lockScreenShowMediaPlayer: {
def: true
},
lockScreenPowerOffMonitorsOnLock: {
def: false
},
@@ -1204,9 +1190,6 @@ var LOCAL_SPEC = {
lockScreenInactiveColor: {
def: "#000000"
},
lockScreenNotificationMode: {
def: 0
},
lockScreenVideoEnabled: {
def: false
},
@@ -1379,9 +1362,6 @@ var LOCAL_SPEC = {
updaterIgnoredPackages: {
def: []
},
displayNameMode: {
def: "system"
},
screenPreferences: {
def: {}
},
@@ -713,9 +713,52 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 37;
}
if (currentVersion < 38 && targetVersion >= 38) {
migrateLockScreenWidgets(settings);
settings.configVersion = 38;
}
return settings;
}
var LOCK_WIDGET_MOVED_KEYS = ["lockScreenShowSystemIcons", "lockScreenShowTime", "lockScreenClockStyle", "lockScreenShowDate", "lockScreenShowPasswordField", "lockScreenShowMediaPlayer", "lockScreenNotificationMode"];
var LOCK_WIDGET_GREETER_KEYS = ["lockScreenShowPowerActions", "lockScreenShowProfileImage", "lockScreenShowWeather"];
function migrateLockScreenWidgets(settings) {
if (Array.isArray(settings.lockScreenWidgetInstances))
return;
const present = key => key in settings;
if (!LOCK_WIDGET_MOVED_KEYS.concat(LOCK_WIDGET_GREETER_KEYS).some(present))
return;
const instances = SpecModule.lockWidgetDefaults();
const byId = id => instances.find(inst => inst.id === id);
if (present("lockScreenShowTime"))
byId("lock_clock").enabled = !!settings.lockScreenShowTime;
if (present("lockScreenClockStyle"))
byId("lock_clock").config.style = settings.lockScreenClockStyle;
if (present("lockScreenShowDate"))
byId("lock_date").enabled = !!settings.lockScreenShowDate;
byId("lock_auth").config.profileVisibility = settings.lockScreenShowProfileImage === false ? "never" : "always";
byId("lock_auth").config.passwordVisibility = settings.lockScreenShowPasswordField === false ? "typing" : "always";
if (present("lockScreenNotificationMode")) {
const mode = Number(settings.lockScreenNotificationMode) || 0;
byId("lock_notifications").enabled = mode > 0;
if (mode > 0)
byId("lock_notifications").config.mode = mode;
}
if (present("lockScreenShowSystemIcons"))
byId("lock_status").enabled = !!settings.lockScreenShowSystemIcons;
if (present("lockScreenShowMediaPlayer"))
byId("lock_status").config.showMediaPlayer = !!settings.lockScreenShowMediaPlayer;
if (present("lockScreenShowWeather"))
byId("lock_status").config.showWeather = !!settings.lockScreenShowWeather;
if (present("lockScreenShowPowerActions"))
byId("lock_power").enabled = !!settings.lockScreenShowPowerActions;
settings.lockScreenWidgetInstances = instances;
for (const key of LOCK_WIDGET_MOVED_KEYS)
delete settings[key];
}
function migrateControlCenterHeader(widgets, fixedHeader, columns) {
const header = widgets.filter(widget => CC_HEADER_IDS.includes(widget?.id));
if (!fixedHeader && header.length === 0)
@@ -426,7 +426,7 @@ FocusScope {
"plugins_manage": "PluginsManageTab.qml"
})
readonly property var pagesWithParentModal: ["dankbar_widgets", "window_rules", "notification_rules", "display_config", "users", "time_weather", "weather", "lock_screen", "greeter", "dank_dash", "wallpaper_cycling", "theme_schedule", "surface_shadows", "keybinds", "dankbar_settings", "dankbar_appearance", "bar_widget", "dock_general", "dock_widgets", "dock_appearance", "dock_advanced", "launcher", "theme", "theme_apps", "media_player", "desktop_widgets", "autostart", "compositor_layout", "updater"]
readonly property var pagesWithParentModal: ["dankbar_widgets", "window_rules", "notification_rules", "display_config", "users", "time_weather", "weather", "lock_screen", "greeter", "dank_dash", "wallpaper_cycling", "theme_schedule", "surface_shadows", "keybinds", "dankbar_settings", "dankbar_appearance", "bar_widget", "dock_general", "dock_widgets", "dock_appearance", "dock_advanced", "launcher", "theme", "theme_apps", "media_player", "desktop_widgets", "desktop_widget", "autostart", "compositor_layout", "updater"]
Column {
anchors.fill: parent
@@ -11,6 +11,7 @@ Item {
property bool vertical: false
property bool canAdd: true
property bool hasWidgets: true
property bool canClear: true
property bool hasOptions: false
property bool hasCustomActions: false
property string pendingAction: ""
@@ -172,7 +173,7 @@ Item {
iconName: "clear_all"
armedColor: Theme.error
armedTextColor: Theme.onError
visible: root.hasWidgets
visible: root.hasWidgets && root.canClear
onConfirmed: root.clearRequested()
}
+2 -33
View File
@@ -3,7 +3,6 @@ import Quickshell
import qs.Common
import qs.Services
import qs.Modules.Plugins
import qs.Modules.BuiltinDesktopPlugins
Variants {
id: root
@@ -12,7 +11,7 @@ Variants {
Component.onCompleted: Qt.callLater(autoEnablePluginsForInstances)
function autoEnablePluginsForInstances() {
const instances = SettingsData.desktopWidgetInstances || [];
const instances = (SettingsData.desktopWidgetInstances || []).concat(SettingsData.lockScreenWidgetInstances || []);
const pluginTypes = new Set();
for (const inst of instances) {
@@ -45,17 +44,6 @@ Variants {
required property var modelData
readonly property var screen: modelData
readonly property string screenKey: SettingsData.getScreenDisplayName(screen)
function shouldShowOnScreen(prefs) {
if (!Array.isArray(prefs) || prefs.length === 0 || prefs.includes("all"))
return true;
return prefs.some(p => {
if (typeof p === "string")
return p === screenKey || p === modelData.name;
return p?.name === modelData.name || p === screenKey;
});
}
// Layer surfaces stack by map order, so recreate them in list order on
// reorder/enable/display-pref changes or once a plugin component loads (#2715).
@@ -104,14 +92,6 @@ Variants {
onTriggered: screenDelegate.rebuilding = false
}
property Component clockComponent: Component {
DesktopClockWidget {}
}
property Component systemMonitorComponent: Component {
SystemMonitorWidget {}
}
property Instantiator widgetInstantiator: Instantiator {
model: ScriptModel {
objectProp: "id"
@@ -133,23 +113,12 @@ Variants {
if (!liveInstanceData.enabled)
return false;
const prefs = liveInstanceData.config?.displayPreferences ?? ["all"];
return screenDelegate.shouldShowOnScreen(prefs);
return DesktopWidgetRegistry.showsOnScreen(prefs, screenDelegate.screen);
}
pluginId: liveInstanceData.widgetType
instanceId: instanceIdRef
instanceData: liveInstanceData
builtinComponent: {
switch (liveInstanceData.widgetType) {
case "desktopClock":
return screenDelegate.clockComponent;
case "systemMonitor":
return screenDelegate.systemMonitorComponent;
default:
return null;
}
}
pluginService: (liveInstanceData.widgetType !== "desktopClock" && liveInstanceData.widgetType !== "systemMonitor") ? PluginService : null
screen: screenDelegate.screen
widgetEnabled: shouldBeVisible
}
+10
View File
@@ -13,6 +13,7 @@ Scope {
readonly property var log: Log.scoped("Lock")
property string sharedPasswordBuffer: ""
property bool inputRevealed: false
property bool shouldLock: false
onSharedPasswordBufferChanged: {
@@ -22,6 +23,7 @@ Scope {
}
onShouldLockChanged: {
inputRevealed = false;
IdleService.isShellLocked = shouldLock;
}
@@ -223,6 +225,12 @@ Scope {
resumeRelockTimer.restart();
}
function onLockEditorRequested() {
if (shouldLock)
return;
demoWindow.showDemo();
}
function onLoginctlStateChanged() {
if (SessionService.active && pendingLock) {
pendingLock = false;
@@ -285,6 +293,8 @@ Scope {
lock: sessionLock
pam: sharedPam
sharedPasswordBuffer: root.sharedPasswordBuffer
inputRevealed: root.inputRevealed
onInputRevealRequested: revealed => root.inputRevealed = revealed
screenName: lockSurface.currentScreenName
isLocked: shouldLock
onUnlockRequested: root.unlock()
@@ -0,0 +1,96 @@
import QtQuick
import QtQuick.Window
import "LockPlacement.js" as Placement
Item {
id: root
required property Item sourceItem
property string renderKey: ""
property bool ready: false
property bool busy: false
property string capturedKey: ""
property var capture: null
readonly property size sourceSize: Qt.size(sourceItem.width, sourceItem.height)
readonly property real sampleScale: 128 / Math.max(1, sourceItem.width, sourceItem.height)
readonly property int sampleWidth: Math.max(1, Math.round(sourceItem.width * sampleScale))
readonly property int sampleHeight: Math.max(1, Math.round(sourceItem.height * sampleScale))
readonly property bool sourceVisible: sourceItem.visible && (sourceItem.Window.window?.visible ?? false)
signal sampled(var luminances, int sampleWidth, int sampleHeight)
signal failed
onRenderKeyChanged: schedule.restart()
onReadyChanged: schedule.restart()
onSourceVisibleChanged: schedule.restart()
onSourceSizeChanged: schedule.restart()
Component.onCompleted: schedule.restart()
function finish() {
if (capture)
canvas.unloadImage(capture.url);
capture = null;
busy = false;
if (capturedKey !== renderKey)
schedule.restart();
}
Timer {
id: schedule
interval: 0
onTriggered: {
if (!root.ready || !root.sourceVisible || root.busy || !canvas.available || root.sourceItem.width <= 0 || root.sourceItem.height <= 0)
return;
root.capturedKey = root.renderKey;
root.busy = true;
const started = root.sourceItem.grabToImage(result => {
if (!root.ready || root.capturedKey !== root.renderKey) {
root.finish();
return;
}
root.capture = result;
canvas.loadImage(result.url);
if (canvas.isImageLoaded(result.url))
canvas.requestPaint();
}, Qt.size(root.sampleWidth, root.sampleHeight));
if (!started) {
root.finish();
root.failed();
}
}
}
Canvas {
id: canvas
width: root.sampleWidth
height: root.sampleHeight
opacity: 0
onAvailableChanged: schedule.restart()
onImageLoaded: requestPaint()
onPaint: {
if (!root.capture)
return;
if (isImageError(root.capture.url)) {
root.finish();
root.failed();
return;
}
if (!root.ready || root.capturedKey !== root.renderKey) {
root.finish();
return;
}
if (!isImageLoaded(root.capture.url))
return;
const context = getContext("2d");
context.clearRect(0, 0, width, height);
context.drawImage(root.capture.url, 0, 0, width, height);
const pixels = context.getImageData(0, 0, width, height).data;
const luminances = [];
for (let i = 0; i < pixels.length; i += 4)
luminances.push(Placement.luminance(pixels[i] / 255, pixels[i + 1] / 255, pixels[i + 2] / 255));
context.clearRect(0, 0, width, height);
root.finish();
root.sampled(luminances, width, height);
}
}
}
+65
View File
@@ -0,0 +1,65 @@
.pragma library
function luminance(red, green, blue) {
const linear = value => value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
return 0.2126 * linear(red) + 0.7152 * linear(green) + 0.0722 * linear(blue);
}
function intersects(a, b) {
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
}
function candidates(width, height, blockWidth, blockHeight, inset, obstacles) {
const right = width - inset - blockWidth;
const bottom = height - inset - blockHeight;
if (right < inset || bottom < inset)
return [];
const result = [];
for (const y of [inset, (height - blockHeight) / 2, bottom]) {
for (const x of [(width - blockWidth) / 2, inset, right]) {
const rect = {
x,
y,
width: blockWidth,
height: blockHeight
};
if (!obstacles.some(obstacle => intersects(rect, obstacle)))
result.push(rect);
}
}
return result;
}
function choose(luma, sampleWidth, sampleHeight, width, height, choices, foregrounds) {
if (!choices.length || !foregrounds.length || !luma.length || width <= 0 || height <= 0)
return null;
let best = null;
let bestScore = -Infinity;
for (const rect of choices) {
const left = Math.max(0, Math.floor(rect.x * sampleWidth / width));
const top = Math.max(0, Math.floor(rect.y * sampleHeight / height));
const right = Math.min(sampleWidth, Math.ceil((rect.x + rect.width) * sampleWidth / width));
const bottom = Math.min(sampleHeight, Math.ceil((rect.y + rect.height) * sampleHeight / height));
const contrasts = [];
let edges = 0;
for (let y = top; y < bottom; y++) {
for (let x = left; x < right; x++) {
const value = luma[y * sampleWidth + x];
contrasts.push(Math.min(...foregrounds.map(foreground => (Math.max(value, foreground) + 0.05) / (Math.min(value, foreground) + 0.05))));
if (x > left)
edges += Math.abs(value - luma[y * sampleWidth + x - 1]);
if (y > top)
edges += Math.abs(value - luma[(y - 1) * sampleWidth + x]);
}
}
if (!contrasts.length)
continue;
contrasts.sort((a, b) => a - b);
const score = contrasts[Math.floor(contrasts.length / 10)] - edges / contrasts.length * 4;
if (score <= bestScore)
continue;
bestScore = score;
best = rect;
}
return best;
}
@@ -0,0 +1,61 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Services
Variants {
model: Quickshell.screens
Scope {
id: scope
required property var modelData
readonly property string backgroundKey: LockPlacementService.backgroundKey(modelData.name, modelData.width, modelData.height)
property string failedKey: ""
property bool sampling: false
onBackgroundKeyChanged: failedKey = ""
Loader {
id: preparation
active: scope.sampling || (!IdleService.isShellLocked && LockPlacementService.neededFor(scope.modelData) && scope.failedKey !== scope.backgroundKey && !LockPlacementService.hasSample(scope.modelData.name, scope.backgroundKey))
asynchronous: true
sourceComponent: PanelWindow {
screen: scope.modelData
anchors.top: true
anchors.bottom: true
anchors.left: true
anchors.right: true
color: "transparent"
WlrLayershell.namespace: "dms:lock-placement"
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
mask: Region {}
Item {
anchors.fill: parent
opacity: 0
LockScreenBackground {
id: background
anchors.fill: parent
screenName: scope.modelData.name
}
LockBackgroundSampler {
id: sampler
sourceItem: background
ready: background.ready && !IdleService.isShellLocked
renderKey: scope.backgroundKey
onBusyChanged: scope.sampling = busy
onFailed: scope.failedKey = scope.backgroundKey
onSampled: (luminances, sampleWidth, sampleHeight) => LockPlacementService.storeSample(scope.modelData.name, scope.backgroundKey, luminances, sampleWidth, sampleHeight)
}
}
}
}
}
}
@@ -0,0 +1,152 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.DankCommon.Session
Item {
id: root
property string screenName: ""
readonly property bool hasCustomWallpaper: SettingsData.lockScreenWallpaperPath !== ""
function encodeFileUrl(path) {
if (!path)
return "";
return "file://" + path.split('/').map(s => encodeURIComponent(s)).join('/');
}
readonly property bool ready: wallpaperBackground.active ? (wallpaperBackground.item?.ready ?? false) : (backdropLoader.item?.ready || backdropLoader.item?.liveReady || false)
readonly property string renderKey: LockPlacementService.backgroundKey(screenName, width, height)
Rectangle {
anchors.fill: parent
color: SettingsData.effectiveWallpaperBackgroundColor
}
Loader {
id: backdropLoader
anchors.fill: parent
active: {
if (root.hasCustomWallpaper)
return false;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
return !currentWallpaper || (currentWallpaper && currentWallpaper.startsWith("#"));
}
asynchronous: true
sourceComponent: DankBackdrop {
screenName: root.screenName
blur: Theme.lockScreenBlur
blurMax: Theme.lockScreenBlurMax
}
}
Loader {
id: wallpaperBackground
anchors.fill: parent
readonly property string wallpaperSource: {
if (root.hasCustomWallpaper)
return root.encodeFileUrl(SettingsData.lockScreenWallpaperPath);
var w = SessionData.getMonitorWallpaper(screenName);
return (w && !w.startsWith("#")) ? encodeFileUrl(w) : "";
}
readonly property string fillModeName: {
if (SettingsData.lockScreenWallpaperFillMode !== "")
return SettingsData.lockScreenWallpaperFillMode;
return root.hasCustomWallpaper ? "Fill" : SessionData.getMonitorWallpaperFillMode(root.screenName);
}
readonly property real screenScale: CompositorService.getScreenScale(Quickshell.screens.find(s => s.name === root.screenName) ?? null)
readonly property size decodeSize: Qt.size(Math.round(width * screenScale), Math.round(height * screenScale))
active: wallpaperSource !== "" && width > 0 && height > 0
asynchronous: false
sourceComponent: fillModeName === "Scrolling" ? scrollWallpaperComp : plainWallpaperComp
layer.enabled: true
layer.effect: MultiEffect {
autoPaddingEnabled: false
blurEnabled: true
blur: Theme.lockScreenBlur
blurMax: Theme.lockScreenBlurMax
blurMultiplier: 1
}
Behavior on opacity {
NumberAnimation {
duration: LockMetrics.effectsDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
}
Component {
id: plainWallpaperComp
Image {
readonly property bool ready: status === Image.Ready || status === Image.Error
source: wallpaperBackground.wallpaperSource
sourceSize: wallpaperBackground.decodeSize
fillMode: Theme.getFillMode(wallpaperBackground.fillModeName)
smooth: true
cache: true
asynchronous: false
}
}
Component {
id: scrollWallpaperComp
Item {
readonly property bool ready: scrollSource.status === Image.Ready || scrollSource.status === Image.Error
Image {
id: scrollSource
anchors.fill: parent
visible: false
source: wallpaperBackground.wallpaperSource
sourceSize: wallpaperBackground.decodeSize
asynchronous: false
cache: true
}
ShaderEffectSource {
id: scrollSrc
sourceItem: scrollSource
hideSource: true
live: false
}
ShaderEffect {
anchors.fill: parent
readonly property var scrollPos: SessionData.getMonitorScrollPosition(screenName)
property variant source1: scrollSrc
property variant source2: scrollSrc
property real progress: 0.0
property real fillMode: Theme.getShaderFillMode(wallpaperBackground.fillModeName)
property real scrollX: scrollPos.scrollX
property real scrollY: scrollPos.scrollY
property real imageWidth1: scrollSource.implicitWidth > 0 ? scrollSource.implicitWidth : 1
property real imageHeight1: scrollSource.implicitHeight > 0 ? scrollSource.implicitHeight : 1
property real imageWidth2: imageWidth1
property real imageHeight2: imageHeight1
property real screenWidth: width > 0 ? width : 1
property real screenHeight: height > 0 ? height : 1
property vector4d fillColor: Qt.vector4d(0, 0, 0, 1)
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_fade.frag.qsb")
}
}
}
Rectangle {
anchors.fill: parent
color: Theme.screenOffColor
opacity: Theme.lockScreenScrimAlpha
}
}
File diff suppressed because it is too large. Load diff
+32 -23
View File
@@ -5,27 +5,12 @@ import Quickshell
import Quickshell.Wayland
import qs.Services
PanelWindow {
Scope {
id: root
readonly property var log: Log.scoped("LockScreenDemo")
property bool demoActive: false
visible: demoActive
anchors {
top: true
bottom: true
left: true
right: true
}
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
function showDemo(): void {
log.debug("Showing lock screen demo");
demoActive = true;
@@ -36,13 +21,37 @@ PanelWindow {
demoActive = false;
}
Loader {
anchors.fill: parent
active: demoActive
sourceComponent: LockScreenContent {
demoMode: true
screenName: root.screen?.name ?? ""
onUnlockRequested: root.hideDemo()
Variants {
model: root.demoActive ? Quickshell.screens : []
PanelWindow {
id: demoWindow
required property var modelData
screen: modelData
visible: true
anchors {
top: true
bottom: true
left: true
right: true
}
WlrLayershell.namespace: "dms:lock-demo"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
LockScreenContent {
anchors.fill: parent
demoMode: true
screenName: demoWindow.screen?.name ?? ""
onUnlockRequested: root.hideDemo()
}
}
}
}
+5
View File
@@ -11,6 +11,9 @@ FocusScope {
required property var pam
required property string sharedPasswordBuffer
required property string screenName
property bool inputRevealed: false
signal inputRevealRequested(bool revealed)
required property bool isLocked
signal passwordChanged(string newPassword)
@@ -49,6 +52,8 @@ FocusScope {
sessionLock: root.lock
pam: root.pam
passwordBuffer: root.sharedPasswordBuffer
inputRevealed: root.inputRevealed
onInputRevealRequested: revealed => root.inputRevealRequested(revealed)
screenName: root.screenName
enabled: !videoScreensaver.active
focus: !videoScreensaver.active
+237
View File
@@ -0,0 +1,237 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
Item {
id: root
required property var instanceData
required property var screen
required property var hostLayer
property var lockHost: null
property bool editMode: false
readonly property string instanceId: instanceData?.id ?? ""
readonly property string widgetType: instanceData?.widgetType ?? ""
readonly property bool removable: widgetType !== "lockAuth"
readonly property bool selected: hostLayer.selectedInstanceId === instanceId
readonly property bool interacting: editChrome.item?.interacting ?? false
readonly property var contrastColors: content.item?.contrastColors ?? []
readonly property bool hasSavedPosition: geometry.hasSavedPosition
readonly property bool automaticPlacement: widgetType === "lockClock" && (instanceData?.config?.autoPosition ?? true) && !hasSavedPosition
readonly property bool dragging: editChrome.item?.dragging ?? false
readonly property var stock: hostLayer.stockRect(widgetType, root)
property bool snappedCenterX: false
property bool snappedCenterY: false
signal removeRequested
signal optionsRequested
readonly property bool loaded: content.item !== null
onLoadedChanged: {
if (loaded)
Qt.callLater(() => hostLayer.placeIfPending(root));
}
x: geometry.widgetX
y: geometry.widgetY
width: geometry.widgetWidth
height: geometry.widgetHeight
activeFocusOnTab: editMode
Accessible.name: instanceData?.name || I18n.tr("Widget")
Accessible.role: Accessible.Button
onActiveFocusChanged: {
if (editMode && activeFocus)
hostLayer.selectedInstanceId = instanceId;
}
Keys.onReturnPressed: {
if (editMode)
optionsRequested();
}
Keys.onSpacePressed: {
if (editMode)
optionsRequested();
}
function guideFor(value, guides) {
const threshold = geometry.gridEnabled ? geometry.gridSize : Theme.spacingXL;
let best;
let bestDistance = threshold;
for (const guide of guides) {
const distance = Math.abs(value - guide);
if (distance > bestDistance)
continue;
best = guide;
bestDistance = distance;
}
return best;
}
function guidedPosition(startX, startY, dx, dy) {
const rawX = Math.max(0, Math.min(startX + dx, geometry.screenWidth - width));
const rawY = Math.max(0, Math.min(startY + dy, geometry.screenHeight - height));
const snapped = geometry.dragMoveTo(startX, startY, dx, dy);
const centerX = (hostLayer.width - width) / 2;
const centerY = (hostLayer.height - height) / 2;
const guidesX = [centerX];
const guidesY = [centerY];
if (stock) {
guidesX.push(stock.x);
guidesY.push(stock.y);
}
const guideX = guideFor(rawX, guidesX);
const guideY = guideFor(rawY, guidesY);
snappedCenterX = guideX === centerX;
snappedCenterY = guideY === centerY;
return Qt.point(guideX ?? snapped.x, guideY ?? snapped.y);
}
function commitPosition(finalX, finalY) {
if (stock && finalX === stock.x && finalY === stock.y) {
SessionData.resetDesktopWidgetInstanceGeometry(instanceId, ["x", "y"]);
return;
}
geometry.savePosition(finalX, finalY);
if (widgetType === "lockClock" && (instanceData?.config?.autoPosition ?? true))
SettingsData.updateDesktopWidgetInstanceConfig(instanceId, {
autoPosition: false
});
}
DesktopWidgetGeometry {
id: geometry
instanceId: root.instanceId
instanceData: root.instanceData
screen: root.screen
minWidth: content.contentMinWidth
minHeight: content.contentMinHeight
forceSquare: content.contentForceSquare
defaultWidth: content.contentDefaultWidth
defaultHeight: content.contentDefaultHeight
defaultX: root.stock ? root.stock.x : screenWidth / 2 - savedWidth / 2
defaultY: root.stock ? root.stock.y : screenHeight / 2 - savedHeight / 2
}
DesktopWidgetContent {
id: content
anchors.fill: parent
active: root.visible
focus: root.widgetType === "lockAuth"
pluginId: root.widgetType
instanceId: root.instanceId
instanceData: root.instanceData
screen: root.screen
geometry: geometry
lockScreen: true
lockHost: root.lockHost
persistDefaults: false
}
Loader {
id: editChrome
anchors.fill: parent
active: root.editMode
sourceComponent: Item {
readonly property bool interacting: dragArea.pressed || chrome.resizing
readonly property bool dragging: dragArea.pressed
MouseArea {
id: dragArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
property point startPos
property real startX
property real startY
property bool moved: false
onPressed: mouse => {
root.hostLayer.selectedInstanceId = root.instanceId;
root.forceActiveFocus();
moved = false;
startPos = mapToItem(root.parent, mouse.x, mouse.y);
startX = geometry.widgetX;
startY = geometry.widgetY;
geometry.dragOverrideX = startX;
geometry.dragOverrideY = startY;
}
onPositionChanged: mouse => {
if (!pressed)
return;
const current = mapToItem(root.parent, mouse.x, mouse.y);
if (!moved && Math.hypot(current.x - startPos.x, current.y - startPos.y) < Qt.styleHints.startDragDistance)
return;
moved = true;
const next = root.guidedPosition(startX, startY, current.x - startPos.x, current.y - startPos.y);
geometry.dragOverrideX = next.x;
geometry.dragOverrideY = next.y;
}
onReleased: {
const finalX = geometry.dragOverrideX;
const finalY = geometry.dragOverrideY;
geometry.clearDragOverrides();
root.snappedCenterX = false;
root.snappedCenterY = false;
if (moved) {
root.commitPosition(finalX, finalY);
return;
}
root.optionsRequested();
}
onCanceled: {
geometry.clearDragOverrides();
root.snappedCenterX = false;
root.snappedCenterY = false;
}
}
DankGridEditChrome {
id: chrome
property real startWidth
property real startHeight
property point startPos
anchors.fill: parent
anchors.margins: -contentInset - Theme.spacingL
visible: root.selected
cornerRadius: Theme.cornerRadius
dragging: dragArea.pressed
removable: root.removable
hasOptions: true
cornerResize: content.contentResizable
sizeText: Math.round(root.width) + "×" + Math.round(root.height)
onRemoveRequested: root.removeRequested()
onOptionsRequested: root.optionsRequested()
onResizeStarted: (px, py) => {
startPos = Qt.point(px, py);
startWidth = geometry.widgetWidth;
startHeight = geometry.widgetHeight;
geometry.dragOverrideW = startWidth;
geometry.dragOverrideH = startHeight;
resizing = true;
}
onResizeMoved: (px, py) => {
const next = geometry.dragResizeTo(startWidth, startHeight, px - startPos.x, py - startPos.y);
geometry.dragOverrideW = next.width;
geometry.dragOverrideH = next.height;
}
onResizeEnded: {
resizing = false;
geometry.saveSize(geometry.dragOverrideW, geometry.dragOverrideH);
geometry.clearDragOverrides();
}
onResizeCanceled: {
resizing = false;
geometry.clearDragOverrides();
}
}
}
}
}
+272
View File
@@ -0,0 +1,272 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Modules.Plugins
import "LockPlacement.js" as Placement
import qs.DankCommon.Session
FocusScope {
id: root
required property string screenName
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 var instances: SettingsData.lockScreenWidgetInstances || []
readonly property string screenKey: SettingsData.getScreenDisplayName(screen)
property var _gridSettingsTrigger: SessionData.desktopWidgetGridSettings
readonly property int gridSize: {
void _gridSettingsTrigger;
return SessionData.getDesktopWidgetGridSetting(screenKey, "size", 40);
}
readonly property bool gridEnabled: {
void _gridSettingsTrigger;
return SessionData.getDesktopWidgetGridSetting(screenKey, "enabled", false);
}
signal focusStolen
signal optionsRequested(var instanceData)
// 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.setDesktopWidgetGridSetting(screenKey, "enabled", !gridEnabled);
}
function stepGrid(delta) {
SessionData.setDesktopWidgetGridSetting(screenKey, "size", Math.max(10, Math.min(200, gridSize + delta)));
}
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: ({})
readonly property string backgroundKey: LockPlacementService.backgroundKey(screenName, width, height)
readonly property var autoPositions: editMode && completed ? currentPlacement() : lockedPositions
function currentPlacement() {
return LockPlacementService.layoutFor(screenName, LockPlacementService.sampleFor(screenName, backgroundKey), placementKey, placeClocks);
}
function refreshPlacement() {
if (!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: 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) {
const centerX = (width - item.width) / 2;
switch (widgetType) {
case "lockClock":
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 + 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
};
}
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;
}
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
}
LockWidgetItem {
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)
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,267 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
import qs.DankCommon.Session
import "../../../DankCommon/Common/Hct.js" as Hct
Item {
id: root
property var instanceData: null
property var lockHost: null
readonly property var cfg: instanceData?.config ?? ({})
readonly property string style: cfg.style ?? "expressive"
readonly property bool textStyle: style === "horizontal" || style === "vertical"
readonly property string colorMode: cfg.colorMode ?? "default"
readonly property var primaryHct: Hct.toHct(Theme.primary)
readonly property color shadowFaceColor: {
if (colorMode !== "default")
return contentColor;
return Theme.isLightMode ? Hct.fromHct(primaryHct.hue, primaryHct.chroma, 65) : Theme.accentOnPrimaryContainer;
}
readonly property color shadowColor: {
if (colorMode === "default")
return Theme.isLightMode ? Hct.fromHct(primaryHct.hue, primaryHct.chroma, 30) : Theme.primaryContainer;
const source = Hct.toHct(shadowFaceColor);
return Hct.fromHct(source.hue, source.chroma, source.tone > 50 ? 30 : 80);
}
readonly property int weightOverride: cfg.weight ?? 0
readonly property int digitWeight: weightOverride > 0 ? weightOverride : Theme.fontWeight
readonly property color contentColor: lockHost?.contentColor(cfg.colorMode ?? "default", cfg.customColor ?? "#ffffff") ?? Theme.lockScreenContentColor
readonly property string fontFamily: cfg.fontFamily || SettingsData.lockScreenFontFamily
readonly property color minutesColor: (cfg.twoTone ?? true) ? Theme.primary : contentColor
readonly property var contrastColors: style === "vertical" ? [shadowFaceColor, shadowColor] : style === "horizontal" || style === "expressive" ? [contentColor, minutesColor] : [contentColor]
readonly property real clockSize: LockMetrics.clockSize * (style === "horizontal" ? 1.5 : 1)
readonly property real clockDigitWidth: clockSize * 0.625
readonly property bool resizable: true
readonly property bool forceSquare: style === "analog"
readonly property real boxWidth: style === "analog" ? LockMetrics.clockSize * 2 : LockMetrics.clockSize * 4
readonly property real boxHeight: style === "analog" ? LockMetrics.clockSize * 2 : LockMetrics.clockSize * 1.4
readonly property real naturalWidth: face.item?.implicitWidth ?? 0
readonly property real naturalHeight: face.item?.implicitHeight ?? 0
readonly property real defaultWidth: textStyle ? naturalWidth : boxWidth
readonly property real defaultHeight: textStyle ? naturalHeight : boxHeight
readonly property real minWidth: textStyle ? naturalWidth / 2 : LockMetrics.fieldHeight * 2
readonly property real minHeight: textStyle ? naturalHeight / 2 : LockMetrics.fieldHeight
readonly property real fitScale: textStyle && naturalWidth > 0 && naturalHeight > 0 ? Math.min(width / naturalWidth, height / naturalHeight) : 1
implicitWidth: textStyle ? naturalWidth : boxWidth
implicitHeight: textStyle ? naturalHeight : boxHeight
component ClockDigitText: StyledText {
font.pixelSize: root.clockSize
font.weight: root.digitWeight
font.italic: root.cfg.italic ?? false
font.features: ({
"tnum": 1
})
color: root.contentColor
horizontalAlignment: Text.AlignHCenter
font.family: root.fontFamily !== "" ? root.fontFamily : resolvedFontFamily
}
SystemClock {
id: systemClock
precision: (root.style === "analog" ? (root.cfg.showAnalogSeconds ?? true) : SettingsData.getEffectiveTimeFormat().includes("s")) ? SystemClock.Seconds : SystemClock.Minutes
}
QtObject {
id: time
readonly property string ampm: SettingsData.use24HourClock ? "" : systemClock.date.toLocaleTimeString(I18n.locale(), "AP")
readonly property string hours: {
const format = SettingsData.use24HourClock || SettingsData.padHours12Hour ? "hh" : "h";
const text = systemClock.date.toLocaleTimeString(I18n.locale(), ampm ? format + " AP" : format);
return ampm ? text.replace(ampm, "").trim() : text;
}
readonly property string minutes: systemClock.date.toLocaleTimeString(I18n.locale(), "mm")
readonly property string seconds: systemClock.date.toLocaleTimeString(I18n.locale(), "ss")
readonly property bool hasSeconds: SettingsData.showSeconds
}
Loader {
id: face
anchors.fill: root.textStyle ? undefined : parent
anchors.centerIn: root.textStyle ? parent : undefined
width: root.textStyle ? root.naturalWidth : undefined
height: root.textStyle ? root.naturalHeight : undefined
scale: root.fitScale
sourceComponent: {
switch (root.style) {
case "vertical":
return shadowFace;
case "expressive":
return expressiveFace;
case "analog":
return analogFace;
}
return horizontalFace;
}
}
Component {
id: horizontalFace
Item {
implicitWidth: row.implicitWidth
implicitHeight: row.implicitHeight
Row {
id: row
anchors.centerIn: parent
layoutDirection: Qt.LeftToRight
spacing: 0
ClockDigitText {
id: hourText
text: time.hours + ":"
}
ClockDigitText {
text: time.minutes
color: root.minutesColor
}
ClockDigitText {
text: ":" + time.seconds
color: root.minutesColor
visible: time.hasSeconds
}
ClockDigitText {
text: " " + time.ampm
font.pixelSize: root.clockSize / 3
anchors.baseline: hourText.baseline
visible: time.ampm !== ""
}
}
}
}
Component {
id: shadowFace
Item {
id: shadowItem
readonly property real digitSize: root.clockSize * 1.25
readonly property real tracking: -digitSize * 0.1
readonly property real rowAdvance: digitSize * 0.7
readonly property int weight: root.weightOverride > 0 ? root.weightOverride : 1000
readonly property bool variableFont: root.fontFamily === "" || root.fontFamily === Theme.defaultFontFamily
// One cell width for all four glyphs keeps the columns aligned in fonts without tabular figures.
readonly property real cellWidth: Math.max(hoursRow.firstAdvance, hoursRow.secondAdvance, minutesRow.firstAdvance, minutesRow.secondAdvance)
implicitWidth: Math.max(hoursRow.width, minutesRow.width)
implicitHeight: Math.max(hoursRow.height, minutesRow.y + minutesRow.height)
ShadowRow {
id: hoursRow
x: (shadowItem.width - shadowItem.implicitWidth) / 2
text: time.hours.padStart(2, "0")
}
ShadowRow {
id: minutesRow
x: hoursRow.x
y: hoursRow.baselineOffset + shadowItem.rowAdvance - baselineOffset
text: time.minutes
lightFirst: true
}
}
}
component ClockDigit: StyledText {
id: digit
readonly property rect inkBounds: metrics.tightBoundingRect
readonly property real advanceWidth: metrics.advanceWidth
font.pixelSize: shadowItem.digitSize
font.family: root.fontFamily !== "" ? root.fontFamily : Theme.defaultFontFamily
font.weight: shadowItem.variableFont ? Font.Normal : Math.min(Font.Black, shadowItem.weight)
font.variableAxes: shadowItem.variableFont ? {
"wght": shadowItem.weight,
"ROND": 0,
"opsz": 18
} : ({})
font.features: ({
"tnum": 1
})
textFormat: Text.PlainText
wrapMode: Text.NoWrap
elide: Text.ElideNone
verticalAlignment: Text.AlignTop
TextMetrics {
id: metrics
font: digit.font
text: digit.text
renderType: digit.renderType
}
}
component ShadowRow: Item {
id: shadowRow
property string text: ""
property bool lightFirst: false
readonly property real firstAdvance: firstDigit.advanceWidth
readonly property real secondAdvance: secondDigit.advanceWidth
readonly property real cell: shadowItem.cellWidth
readonly property real topBearing: Math.min(firstDigit.inkBounds.y, secondDigit.inkBounds.y)
readonly property real bottomBearing: Math.max(firstDigit.inkBounds.y + firstDigit.inkBounds.height, secondDigit.inkBounds.y + secondDigit.inkBounds.height)
implicitWidth: cell * 2 + shadowItem.tracking
implicitHeight: bottomBearing - topBearing
baselineOffset: -topBearing
ClockDigit {
id: firstDigit
x: (shadowRow.cell - advanceWidth) / 2
y: shadowRow.baselineOffset - baselineOffset
text: shadowRow.text[0] ?? ""
color: shadowRow.lightFirst ? root.shadowFaceColor : root.shadowColor
}
ClockDigit {
id: secondDigit
x: shadowRow.cell + shadowItem.tracking + (shadowRow.cell - advanceWidth) / 2
y: shadowRow.baselineOffset - baselineOffset
text: shadowRow.text[1] ?? ""
color: shadowRow.lightFirst ? root.shadowColor : root.shadowFaceColor
}
}
Component {
id: expressiveFace
DankClockFace {
readonly property bool tallBox: height > width * 0.6
hours: time.hours
minutes: time.minutes
seconds: time.hasSeconds && !tallBox ? time.seconds : ""
stacked: tallBox
color: root.contentColor
minutesColor: root.minutesColor
}
}
Component {
id: analogFace
DankAnalogClock {
hours: systemClock.date?.getHours() ?? 0
minutes: systemClock.date?.getMinutes() ?? 0
seconds: systemClock.date?.getSeconds() ?? 0
showSeconds: root.cfg.showAnalogSeconds ?? true
showNumbers: root.cfg.showAnalogNumbers ?? false
color: root.contentColor
backgroundColor: "transparent"
}
}
}
@@ -0,0 +1,40 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
Item {
id: root
property var instanceData: null
property var lockHost: null
readonly property var cfg: instanceData?.config ?? ({})
readonly property color contentColor: lockHost?.contentColor(cfg.colorMode ?? "default", cfg.customColor ?? "#ffffff") ?? Theme.lockScreenContentColor
readonly property string fontFamily: SettingsData.lockScreenFontFamily
readonly property bool resizable: true
readonly property real minWidth: implicitWidth / 2
readonly property real minHeight: implicitHeight / 2
readonly property real fitScale: implicitWidth > 0 && implicitHeight > 0 ? Math.min(width / implicitWidth, height / implicitHeight) : 1
implicitWidth: dateText.implicitWidth
implicitHeight: dateText.implicitHeight
SystemClock {
id: systemClock
precision: SystemClock.Minutes
}
StyledText {
id: dateText
anchors.centerIn: parent
scale: root.fitScale
text: {
if (SettingsData.lockDateFormat && SettingsData.lockDateFormat.length > 0)
return systemClock.date.toLocaleDateString(I18n.locale(), SettingsData.lockDateFormat);
return systemClock.date.toLocaleDateString(I18n.locale(), (root.cfg.format ?? "long") === "short" ? "ddd, d MMM" : Locale.LongFormat);
}
font.pixelSize: Theme.fontSizeXLarge
font.family: root.fontFamily !== "" ? root.fontFamily : resolvedFontFamily
color: root.contentColor
}
}
@@ -0,0 +1,254 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.Notifications as Notifications
import qs.Services
import qs.Widgets
import qs.DankCommon.Session
Item {
id: root
property var instanceData: null
property var lockHost: null
readonly property var cfg: instanceData?.config ?? ({})
readonly property int notificationMode: cfg.mode ?? 1
readonly property bool resizable: true
readonly property real minWidth: Theme.listItemTwoLineHeight * 2
readonly property real minHeight: Theme.listItemHeight
readonly property var notifications: NotificationService.groupedNotifications
readonly property int totalCount: {
let count = 0;
for (const group of notifications)
count += group.count || 0;
return count;
}
readonly property bool hasNotifications: totalCount > 0
readonly property var appNameGroups: {
const groups = {};
for (const group of notifications) {
const appName = (group.appName || "Unknown").toLowerCase();
if (!groups[appName]) {
groups[appName] = {
appName: group.appName || I18n.tr("Unknown"),
count: 0,
latestNotification: group.latestNotification
};
}
groups[appName].count += group.count || 0;
if (group.latestNotification && (!groups[appName].latestNotification || group.latestNotification.time > groups[appName].latestNotification.time))
groups[appName].latestNotification = group.latestNotification;
}
return Object.values(groups).sort((a, b) => b.count - a.count);
}
readonly property bool editPlaceholder: !hasNotifications && (lockHost?.demoMode ?? false)
readonly property bool chips: notificationMode === 2 && !editPlaceholder
readonly property var visibleGroups: appNameGroups.slice(0, LockMetrics.notificationLimit)
readonly property int hiddenGroups: Math.max(0, appNameGroups.length - LockMetrics.notificationLimit)
implicitWidth: chips ? contentLoader.implicitWidth : LockMetrics.notificationCardWidth
implicitHeight: hasNotifications || editPlaceholder ? contentLoader.implicitHeight : 0
clip: true
Loader {
id: contentLoader
anchors.left: parent.left
anchors.right: root.chips ? undefined : parent.right
anchors.top: parent.top
active: root.hasNotifications || root.editPlaceholder
sourceComponent: {
if (root.notificationMode === 1 || root.editPlaceholder)
return countOnlyComponent;
return root.chips ? chipRowComponent : notificationListComponent;
}
}
Component {
id: chipRowComponent
Row {
spacing: Theme.spacingS
Repeater {
model: root.visibleGroups
Rectangle {
id: chip
required property var modelData
readonly property string appIcon: NotificationService.notificationAppIcon(modelData.latestNotification?.appIcon || "", modelData.latestNotification?.desktopEntry || "")
height: Theme.buttonHeightXS
width: chipRow.implicitWidth + Theme.spacingM * 2
radius: Theme.cornerRadiusM
color: Theme.notificationFloatingSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineVariant
Accessible.name: modelData.appName
Row {
id: chipRow
anchors.centerIn: parent
spacing: Theme.spacingXS
Item {
width: Theme.iconSizeSmall
height: width
anchors.verticalCenter: parent.verticalCenter
Image {
id: appImage
anchors.fill: parent
asynchronous: true
smooth: true
fillMode: Image.PreserveAspectFit
sourceSize: Qt.size(width * 2, height * 2)
source: NotificationService.notificationImageSource("", chip.appIcon)
visible: status === Image.Ready
}
AppIconRenderer {
anchors.fill: parent
visible: !appImage.visible
iconValue: NotificationService.notificationFallbackIcon("", chip.appIcon)
iconSize: Theme.iconSizeSmall
iconColor: Theme.onSurfaceVariant
colorOverride: "transparent"
fallbackText: (chip.modelData.appName || "?").charAt(0).toUpperCase()
fallbackRadius: Theme.fullRadius(width, height)
fallbackBackgroundColor: Theme.secondaryContainer
fallbackTextColor: Theme.onSecondaryContainer
}
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: chip.modelData.appName
font.pixelSize: Theme.fontSizeSmall
font.weight: Theme.fontWeightMedium
color: Theme.onSurface
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
visible: chip.modelData.count > 1
width: Math.max(height, countText.implicitWidth + Theme.spacingXS * 2)
height: Theme.iconSizeSmall
radius: Theme.fullRadius(width, height)
color: Theme.primary
StyledText {
id: countText
anchors.centerIn: parent
text: chip.modelData.count
font.pixelSize: Theme.fontSizeSmall
font.weight: Theme.fontWeightMedium
color: Theme.onPrimary
}
}
}
}
}
Rectangle {
visible: root.hiddenGroups > 0
height: Theme.buttonHeightXS
width: moreText.implicitWidth + Theme.spacingM * 2
radius: Theme.cornerRadiusM
color: Theme.notificationFloatingSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineVariant
StyledText {
id: moreText
anchors.centerIn: parent
text: I18n.tr("+ %1 more", "lock screen notification list overflow, %1 is a count of hidden apps").arg(root.hiddenGroups)
font.pixelSize: Theme.fontSizeSmall
font.weight: Theme.fontWeightMedium
color: Theme.onSurfaceVariant
}
}
}
}
Component {
id: countOnlyComponent
LockNotificationCard {
implicitHeight: Theme.listItemHeight
color: Theme.notificationFloatingSurface
Accessible.name: countLabel.text
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "notifications"
size: Theme.iconSize
color: Theme.onSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: countLabel
text: root.editPlaceholder ? I18n.tr("Notifications") : root.totalCount === 1 ? I18n.tr("1 notification") : I18n.tr("%1 notifications").arg(root.totalCount)
font.pixelSize: Theme.fontSizeMedium
color: Theme.onSurface
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
Component {
id: notificationListComponent
DankFlickable {
implicitHeight: Math.min(notificationColumn.implicitHeight, LockMetrics.notificationMaxHeight, Math.max(0, (root.lockHost?.height ?? 0) - (root.parent?.y ?? 0) - Theme.spacingXL))
height: Math.min(notificationColumn.implicitHeight, root.height)
contentHeight: notificationColumn.implicitHeight
clip: true
Column {
id: notificationColumn
width: parent.width
spacing: Theme.groupedListGap
Repeater {
id: notificationRepeater
model: root.appNameGroups.slice(0, LockMetrics.notificationLimit)
Notifications.NotificationCard {
required property var modelData
required property int index
width: notificationColumn.width
notificationData: modelData.latestNotification
groupCount: modelData.count
interactive: false
headerOnly: false
showActions: false
showDismiss: false
showClose: false
showTime: !headerOnly
animateHeight: false
firstInGroup: index === 0
lastInGroup: index === notificationRepeater.count - 1
}
}
StyledText {
width: parent.width
visible: root.appNameGroups.length > LockMetrics.notificationLimit
text: I18n.tr("+ %1 more", "lock screen notification list overflow, %1 is a count of hidden apps").arg(root.appNameGroups.length - LockMetrics.notificationLimit)
font.pixelSize: Theme.fontSizeSmall
color: Theme.lockScreenContentColor
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}
@@ -0,0 +1,47 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Session
Item {
id: root
property var instanceData: null
property var lockHost: null
readonly property var cfg: instanceData?.config ?? ({})
readonly property string shape: cfg.shape ?? "round"
readonly property bool round: shape === "round"
readonly property bool resizable: false
readonly property real minWidth: implicitWidth
readonly property real minHeight: implicitHeight
implicitWidth: powerButton.width
implicitHeight: powerButton.height
DankMaterialShape {
anchors.fill: powerButton
visible: !root.round
shape: root.shape
color: Theme.secondaryContainer
scale: powerButton.pressed ? 0.9 : 1
Behavior on scale {
NumberAnimation {
duration: LockMetrics.shakeDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
}
LockActionButton {
id: powerButton
Accessible.name: I18n.tr("Power Options")
iconName: "power_settings_new"
iconColor: Theme.onSecondaryContainer
backgroundColor: root.round ? Theme.secondaryContainer : "transparent"
radius: pressed ? Theme.cornerRadiusS : Theme.fullRadius(width, height)
buttonSize: Theme.buttonHeightM
onClicked: root.lockHost?.showPowerMenu()
}
}
@@ -0,0 +1,39 @@
import QtQuick
import qs.Common
import qs.Modules.Lock
Item {
id: root
property var instanceData: null
property var lockHost: null
readonly property var cfg: instanceData?.config ?? ({})
readonly property bool background: cfg.background ?? false
readonly property real pad: background ? Theme.spacingM : 0
readonly property bool resizable: false
readonly property real minWidth: implicitWidth
readonly property real minHeight: implicitHeight
implicitWidth: statusRow.implicitWidth + pad * 2
implicitHeight: statusRow.implicitHeight + pad * 2
Rectangle {
anchors.fill: parent
visible: root.background
radius: Theme.fullRadius(width, height)
color: Theme.readableSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
}
LockStatusRow {
id: statusRow
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: root.pad
interactive: !(root.lockHost?.demoMode ?? true)
showMediaPlayer: root.cfg.showMediaPlayer ?? true
showWeather: root.cfg.showWeather ?? true
useFahrenheit: SettingsData.useFahrenheit
}
}
@@ -8,6 +8,7 @@ Item {
property string pluginId: ""
property string instanceId: ""
property var instanceData: null
property bool lockScreen: false
property real widgetWidth: 200
property real widgetHeight: 200
@@ -13,27 +13,20 @@ Item {
required property string pluginId
required property var screen
property var builtinComponent: null
property var pluginService: null
property string instanceId: ""
property var instanceData: null
onInstanceDataChanged: contentLoader.updateInstanceData()
property bool widgetEnabled: true
readonly property bool isBuiltin: pluginId === "desktopClock" || pluginId === "systemMonitor"
readonly property var activeComponent: isBuiltin ? builtinComponent : PluginService.pluginDesktopComponents[pluginId] ?? null
readonly property bool showOnOverlay: instanceData?.config?.showOnOverlay ?? false
readonly property bool showOnOverview: instanceData?.config?.showOnOverview ?? false
readonly property bool showOnOverviewOnly: instanceData?.config?.showOnOverviewOnly ?? false
readonly property bool overviewActive: CompositorService.isNiri && NiriService.inOverview
readonly property bool clickThrough: instanceData?.config?.clickThrough ?? false
readonly property bool syncPositionAcrossScreens: instanceData?.config?.syncPositionAcrossScreens ?? false
// Unmapping with the widget still in the last buffer leaves a stale image in
// Hyprland's blur cache behind transparent tiled windows (#2955), so hide the
// content, present a transparent frame, then unmap.
readonly property bool contentShowing: widgetEnabled && activeComponent !== null && (!showOnOverviewOnly || overviewActive)
readonly property bool contentShowing: widgetEnabled && content.activeComponent !== null && (!showOnOverviewOnly || overviewActive)
property bool surfaceLingering: false
onContentShowingChanged: {
@@ -52,157 +45,25 @@ Item {
onTriggered: root.surfaceLingering = false
}
readonly property string settingsKey: instanceId ? instanceId : pluginId
readonly property bool isInstance: instanceId !== "" && instanceData !== null
QtObject {
id: instanceScopedPluginService
readonly property var availablePlugins: PluginService.availablePlugins
readonly property var loadedPlugins: PluginService.loadedPlugins
readonly property var pluginDesktopComponents: PluginService.pluginDesktopComponents
signal pluginDataChanged(string pluginId)
signal pluginLoaded(string pluginId)
signal pluginUnloaded(string pluginId)
function loadPluginData(pluginId, key, defaultValue) {
const cfg = root.instanceData?.config;
if (cfg && key in cfg)
return cfg[key];
return SettingsData.getPluginSetting(pluginId, key, defaultValue);
}
function savePluginData(pluginId, key, value) {
if (!root.instanceId)
return false;
var updates = {};
updates[key] = value;
SettingsData.updateDesktopWidgetInstanceConfig(root.instanceId, updates);
Qt.callLater(() => pluginDataChanged(pluginId));
return true;
}
function getPluginVariants(pluginId) {
return PluginService.getPluginVariants(pluginId);
}
function isPluginLoaded(pluginId) {
return PluginService.isPluginLoaded(pluginId);
}
DesktopWidgetGeometry {
id: geometry
instanceId: root.instanceId
instanceData: root.instanceData
screen: root.screen
minWidth: content.contentMinWidth
minHeight: content.contentMinHeight
forceSquare: content.contentForceSquare
}
readonly property string screenKey: SettingsData.getScreenDisplayName(screen)
readonly property string positionKey: syncPositionAcrossScreens ? "_synced" : screenKey
readonly property var storedPositions: SessionData.desktopWidgetInstancePositions[instanceId] ?? null
readonly property int screenWidth: screen?.width ?? 1920
readonly property int screenHeight: screen?.height ?? 1080
readonly property bool useGhostPreview: !CompositorService.isNiri
property real previewX: widgetX
property real previewY: widgetY
property real previewWidth: widgetWidth
property real previewHeight: widgetHeight
property real previewX: geometry.widgetX
property real previewY: geometry.widgetY
property real previewWidth: geometry.widgetWidth
property real previewHeight: geometry.widgetHeight
function storedGeometry(key) {
if (!isInstance)
return undefined;
return storedPositions?.[positionKey]?.[key];
}
function storedCoordinate(key, extent, fallback) {
const val = storedGeometry(key);
if (val === undefined)
return fallback;
return syncPositionAcrossScreens ? val * extent : val;
}
readonly property bool hasSavedPosition: storedGeometry("x") !== undefined
readonly property bool hasSavedSize: storedGeometry("width") !== undefined
property real savedX: storedCoordinate("x", screenWidth, screenWidth / 2 - savedWidth / 2)
property real savedY: storedCoordinate("y", screenHeight, screenHeight / 2 - savedHeight / 2)
property real savedWidth: storedGeometry("width") ?? 280
property real savedHeight: forceSquare ? savedWidth : (storedGeometry("height") ?? 180)
property real dragOverrideX: -1
property real dragOverrideY: -1
property real dragOverrideW: -1
property real dragOverrideH: -1
readonly property real effectiveX: dragOverrideX >= 0 ? dragOverrideX : savedX
readonly property real effectiveY: dragOverrideY >= 0 ? dragOverrideY : savedY
readonly property real effectiveW: dragOverrideW >= 0 ? dragOverrideW : savedWidth
readonly property real effectiveH: dragOverrideH >= 0 ? dragOverrideH : savedHeight
readonly property real widgetX: Math.max(0, Math.min(effectiveX, screenWidth - widgetWidth))
readonly property real widgetY: Math.max(0, Math.min(effectiveY, screenHeight - widgetHeight))
readonly property real widgetWidth: Math.max(minWidth, Math.min(effectiveW, screenWidth))
readonly property real widgetHeight: Math.max(minHeight, Math.min(effectiveH, screenHeight))
function clearDragOverrides() {
dragOverrideX = -1;
dragOverrideY = -1;
dragOverrideW = -1;
dragOverrideH = -1;
}
function requestResize(width, height) {
if (width < minWidth || height < minHeight)
return;
if (width > screenWidth || height > screenHeight)
return;
dragOverrideW = width;
dragOverrideH = height;
}
function clearResize() {
dragOverrideW = -1;
dragOverrideH = -1;
}
property real minWidth: contentLoader.item?.minWidth ?? 100
property real minHeight: contentLoader.item?.minHeight ?? 100
property bool forceSquare: contentLoader.item?.forceSquare ?? false
property bool acceptsKeyboardFocus: contentLoader.item?.acceptsKeyboardFocus ?? false
property bool isInteracting: dragArea.pressed || resizeArea.pressed
property var _gridSettingsTrigger: SessionData.desktopWidgetGridSettings
readonly property int gridSize: {
void _gridSettingsTrigger;
return SessionData.getDesktopWidgetGridSetting(screenKey, "size", 40);
}
readonly property bool gridEnabled: {
void _gridSettingsTrigger;
return SessionData.getDesktopWidgetGridSetting(screenKey, "enabled", false);
}
function snapToGrid(value) {
return Math.round(value / gridSize) * gridSize;
}
function saveGeometry(updates) {
if (!isInstance)
return;
SessionData.updateDesktopWidgetInstancePosition(instanceId, positionKey, updates);
}
function savePosition(finalX, finalY) {
saveGeometry({
x: syncPositionAcrossScreens ? finalX / screenWidth : finalX,
y: syncPositionAcrossScreens ? finalY / screenHeight : finalY
});
}
function saveSize(finalW, finalH) {
const sizeVal = forceSquare ? Math.max(finalW, finalH) : finalW;
saveGeometry({
width: sizeVal,
height: forceSquare ? sizeVal : finalH
});
}
PanelWindow {
id: widgetWindow
screen: root.screen
@@ -234,7 +95,7 @@ Item {
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
if (root.acceptsKeyboardFocus)
if (content.acceptsKeyboardFocus)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.None;
}
@@ -253,15 +114,15 @@ Item {
return;
switch (event.key) {
case Qt.Key_G:
SessionData.setDesktopWidgetGridSetting(root.screenKey, "enabled", !root.gridEnabled);
SessionData.setDesktopWidgetGridSetting(geometry.screenKey, "enabled", !geometry.gridEnabled);
event.accepted = true;
break;
case Qt.Key_Z:
SessionData.setDesktopWidgetGridSetting(root.screenKey, "size", Math.max(10, root.gridSize - 10));
SessionData.setDesktopWidgetGridSetting(geometry.screenKey, "size", Math.max(10, geometry.gridSize - 10));
event.accepted = true;
break;
case Qt.Key_X:
SessionData.setDesktopWidgetGridSetting(root.screenKey, "size", Math.min(200, root.gridSize + 10));
SessionData.setDesktopWidgetGridSetting(geometry.screenKey, "size", Math.min(200, geometry.gridSize + 10));
event.accepted = true;
break;
}
@@ -274,75 +135,23 @@ Item {
}
WlrLayershell.margins {
left: root.widgetX
top: root.widgetY
left: geometry.widgetX
top: geometry.widgetY
}
implicitWidth: root.widgetWidth
implicitHeight: root.widgetHeight
implicitWidth: geometry.widgetWidth
implicitHeight: geometry.widgetHeight
Loader {
id: contentLoader
DesktopWidgetContent {
id: content
anchors.fill: parent
active: root.widgetEnabled && root.activeComponent !== null
active: root.widgetEnabled
visible: root.contentShowing
sourceComponent: root.activeComponent
opacity: 0
NumberAnimation {
id: revealFade
target: contentLoader
property: "opacity"
from: 0
to: 1
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
function updateInstanceData() {
if (!item || item.instanceData === undefined)
return;
item.instanceData = root.instanceData;
}
onLoaded: {
if (!item)
return;
revealFade.restart();
if (item.pluginService !== undefined) {
item.pluginService = root.isInstance ? instanceScopedPluginService : root.pluginService;
}
if (item.pluginId !== undefined)
item.pluginId = root.pluginId;
if (item.instanceId !== undefined)
item.instanceId = root.instanceId;
if (item.instanceData !== undefined)
item.instanceData = root.instanceData;
if (!root.hasSavedSize) {
const defW = item.defaultWidth ?? item.widgetWidth ?? 280;
const defH = item.defaultHeight ?? item.widgetHeight ?? 180;
const finalW = Math.max(root.minWidth, Math.min(defW, root.screenWidth));
const finalH = Math.max(root.minHeight, Math.min(defH, root.screenHeight));
root.saveSize(finalW, finalH);
}
if (!root.hasSavedPosition) {
const finalX = Math.max(0, Math.min(root.screenWidth / 2 - root.widgetWidth / 2, root.screenWidth - root.widgetWidth));
const finalY = Math.max(0, Math.min(root.screenHeight / 2 - root.widgetHeight / 2, root.screenHeight - root.widgetHeight));
root.savePosition(finalX, finalY);
}
if (item.widgetWidth !== undefined)
item.widgetWidth = Qt.binding(() => contentLoader.width);
if (item.widgetHeight !== undefined)
item.widgetHeight = Qt.binding(() => contentLoader.height);
if (item.screen !== undefined)
item.screen = Qt.binding(() => root.screen);
if (item.requestResize !== undefined)
item.requestResize = root.requestResize;
if (item.clearResize !== undefined)
item.clearResize = root.clearResize;
}
pluginId: root.pluginId
instanceId: root.instanceId
instanceData: root.instanceData
screen: root.screen
geometry: geometry
}
Rectangle {
@@ -380,38 +189,33 @@ Item {
onPressed: mouse => {
startPos = root.useGhostPreview ? Qt.point(mouse.x, mouse.y) : mapToGlobal(mouse.x, mouse.y);
startX = root.widgetX;
startY = root.widgetY;
root.previewX = root.widgetX;
root.previewY = root.widgetY;
root.dragOverrideX = root.widgetX;
root.dragOverrideY = root.widgetY;
startX = geometry.widgetX;
startY = geometry.widgetY;
root.previewX = geometry.widgetX;
root.previewY = geometry.widgetY;
geometry.dragOverrideX = geometry.widgetX;
geometry.dragOverrideY = geometry.widgetY;
}
onPositionChanged: mouse => {
if (!pressed)
return;
const currentPos = root.useGhostPreview ? Qt.point(mouse.x, mouse.y) : mapToGlobal(mouse.x, mouse.y);
let newX = Math.max(0, Math.min(startX + currentPos.x - startPos.x, root.screenWidth - root.widgetWidth));
let newY = Math.max(0, Math.min(startY + currentPos.y - startPos.y, root.screenHeight - root.widgetHeight));
if (root.gridEnabled) {
newX = Math.max(0, Math.min(root.snapToGrid(newX), root.screenWidth - root.widgetWidth));
newY = Math.max(0, Math.min(root.snapToGrid(newY), root.screenHeight - root.widgetHeight));
}
const next = geometry.dragMoveTo(startX, startY, currentPos.x - startPos.x, currentPos.y - startPos.y);
if (root.useGhostPreview) {
root.previewX = newX;
root.previewY = newY;
root.previewX = next.x;
root.previewY = next.y;
return;
}
root.dragOverrideX = newX;
root.dragOverrideY = newY;
geometry.dragOverrideX = next.x;
geometry.dragOverrideY = next.y;
}
onReleased: {
const finalX = root.useGhostPreview ? root.previewX : root.dragOverrideX;
const finalY = root.useGhostPreview ? root.previewY : root.dragOverrideY;
root.savePosition(finalX, finalY);
root.clearDragOverrides();
const finalX = root.useGhostPreview ? root.previewX : geometry.dragOverrideX;
const finalY = root.useGhostPreview ? root.previewY : geometry.dragOverrideY;
geometry.savePosition(finalX, finalY);
geometry.clearDragOverrides();
}
}
@@ -431,43 +235,33 @@ Item {
onPressed: mouse => {
startPos = root.useGhostPreview ? Qt.point(mouse.x, mouse.y) : mapToGlobal(mouse.x, mouse.y);
startWidth = root.widgetWidth;
startHeight = root.widgetHeight;
root.previewWidth = root.widgetWidth;
root.previewHeight = root.widgetHeight;
root.dragOverrideW = root.widgetWidth;
root.dragOverrideH = root.widgetHeight;
startWidth = geometry.widgetWidth;
startHeight = geometry.widgetHeight;
root.previewWidth = geometry.widgetWidth;
root.previewHeight = geometry.widgetHeight;
geometry.dragOverrideW = geometry.widgetWidth;
geometry.dragOverrideH = geometry.widgetHeight;
}
onPositionChanged: mouse => {
if (!pressed)
return;
const currentPos = root.useGhostPreview ? Qt.point(mouse.x, mouse.y) : mapToGlobal(mouse.x, mouse.y);
let newW = Math.max(root.minWidth, Math.min(startWidth + currentPos.x - startPos.x, root.screenWidth - root.widgetX));
let newH = Math.max(root.minHeight, Math.min(startHeight + currentPos.y - startPos.y, root.screenHeight - root.widgetY));
if (root.gridEnabled) {
newW = Math.max(root.minWidth, root.snapToGrid(newW));
newH = Math.max(root.minHeight, root.snapToGrid(newH));
}
if (root.forceSquare) {
const size = Math.max(newW, newH);
newW = Math.min(size, root.screenWidth - root.widgetX);
newH = Math.min(size, root.screenHeight - root.widgetY);
}
const next = geometry.dragResizeTo(startWidth, startHeight, currentPos.x - startPos.x, currentPos.y - startPos.y);
if (root.useGhostPreview) {
root.previewWidth = newW;
root.previewHeight = newH;
root.previewWidth = next.width;
root.previewHeight = next.height;
return;
}
root.dragOverrideW = newW;
root.dragOverrideH = newH;
geometry.dragOverrideW = next.width;
geometry.dragOverrideH = next.height;
}
onReleased: {
const finalW = root.useGhostPreview ? root.previewWidth : root.dragOverrideW;
const finalH = root.useGhostPreview ? root.previewHeight : root.dragOverrideH;
root.saveSize(finalW, finalH);
root.clearDragOverrides();
const finalW = root.useGhostPreview ? root.previewWidth : geometry.dragOverrideW;
const finalH = root.useGhostPreview ? root.previewHeight : geometry.dragOverrideH;
geometry.saveSize(finalW, finalH);
geometry.clearDragOverrides();
}
}
}
@@ -494,37 +288,13 @@ Item {
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
Item {
DesktopWidgetGridLines {
id: gridOverlay
anchors.fill: parent
visible: root.gridEnabled
opacity: 0.3
Repeater {
model: Math.ceil(root.screenWidth / root.gridSize)
Rectangle {
required property int index
x: index * root.gridSize
y: 0
width: 1
height: root.screenHeight
color: Theme.primary
}
}
Repeater {
model: Math.ceil(root.screenHeight / root.gridSize)
Rectangle {
required property int index
x: 0
y: index * root.gridSize
width: root.screenWidth
height: 1
color: Theme.primary
}
}
visible: geometry.gridEnabled
gridSize: geometry.gridSize
extentWidth: geometry.screenWidth
extentHeight: geometry.screenHeight
}
Rectangle {
@@ -554,7 +324,7 @@ Item {
}
Loader {
active: root.isInteracting && root.gridEnabled && !root.useGhostPreview
active: root.isInteracting && geometry.gridEnabled && !root.useGhostPreview
sourceComponent: PanelWindow {
screen: root.screen
@@ -574,35 +344,11 @@ Item {
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
Item {
DesktopWidgetGridLines {
anchors.fill: parent
opacity: 0.3
Repeater {
model: Math.ceil(root.screenWidth / root.gridSize)
Rectangle {
required property int index
x: index * root.gridSize
y: 0
width: 1
height: root.screenHeight
color: Theme.primary
}
}
Repeater {
model: Math.ceil(root.screenHeight / root.gridSize)
Rectangle {
required property int index
x: 0
y: index * root.gridSize
width: root.screenWidth
height: 1
color: Theme.primary
}
}
gridSize: geometry.gridSize
extentWidth: geometry.screenWidth
extentHeight: geometry.screenHeight
}
}
}
@@ -651,15 +397,15 @@ Item {
DankIcon {
name: "grid_on"
size: 16
color: root.gridEnabled ? Theme.primary : Theme.surfaceText
color: geometry.gridEnabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.gridEnabled ? I18n.tr("Grid: ON", "Widget grid snap status") : I18n.tr("Grid: OFF", "Widget grid snap status")
text: geometry.gridEnabled ? I18n.tr("Grid: ON", "Widget grid snap status") : I18n.tr("Grid: OFF", "Widget grid snap status")
font.pixelSize: Theme.fontSizeSmall
font.family: Theme.fontFamily
color: root.gridEnabled ? Theme.primary : Theme.surfaceText
color: geometry.gridEnabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
@@ -686,7 +432,7 @@ Item {
}
NumericText {
text: root.gridSize + "px"
text: geometry.gridSize + "px"
reserveText: "200px"
width: Math.ceil(reservedWidth)
horizontalAlignment: Text.AlignHCenter
@@ -0,0 +1,166 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Modules.BuiltinDesktopPlugins
import qs.Modules.Lock.Widgets
Loader {
id: root
required property string pluginId
required property DesktopWidgetGeometry geometry
property string instanceId: ""
property var instanceData: null
property var screen: null
property bool lockScreen: false
property var lockHost: null
property bool persistDefaults: true
readonly property bool isInstance: instanceId !== "" && instanceData !== null
readonly property var activeComponent: {
switch (pluginId) {
case "desktopClock":
return clockComponent;
case "systemMonitor":
return systemMonitorComponent;
case "lockClock":
return lockClockComponent;
case "lockDate":
return lockDateComponent;
case "lockAuth":
return lockAuthComponent;
case "lockNotifications":
return lockNotificationsComponent;
case "lockStatus":
return lockStatusComponent;
case "lockPower":
return lockPowerComponent;
}
return PluginService.pluginDesktopComponents[pluginId] ?? null;
}
readonly property real contentMinWidth: item?.minWidth ?? 100
readonly property real contentMinHeight: item?.minHeight ?? 100
readonly property bool contentForceSquare: item?.forceSquare ?? false
readonly property bool contentResizable: item?.resizable ?? true
readonly property real contentDefaultWidth: item?.defaultWidth ?? ((item?.implicitWidth ?? 0) > 0 ? item.implicitWidth : 280)
readonly property real contentDefaultHeight: item?.defaultHeight ?? ((item?.implicitHeight ?? 0) > 0 ? item.implicitHeight : 180)
readonly property bool acceptsKeyboardFocus: item?.acceptsKeyboardFocus ?? false
property Component clockComponent: Component {
DesktopClockWidget {}
}
property Component systemMonitorComponent: Component {
SystemMonitorWidget {}
}
property Component lockClockComponent: Component {
LockClockWidget {}
}
property Component lockDateComponent: Component {
LockDateWidget {}
}
property Component lockAuthComponent: Component {
LockAuthWidget {}
}
property Component lockNotificationsComponent: Component {
LockNotificationsWidget {}
}
property Component lockStatusComponent: Component {
LockStatusWidget {}
}
property Component lockPowerComponent: Component {
LockPowerWidget {}
}
onInstanceDataChanged: {
if (!item || item.instanceData === undefined)
return;
item.instanceData = instanceData;
}
sourceComponent: activeComponent
opacity: 0
NumberAnimation {
id: revealFade
target: root
property: "opacity"
from: 0
to: 1
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
QtObject {
id: instanceScopedPluginService
readonly property var availablePlugins: PluginService.availablePlugins
readonly property var loadedPlugins: PluginService.loadedPlugins
readonly property var pluginDesktopComponents: PluginService.pluginDesktopComponents
signal pluginDataChanged(string pluginId)
signal pluginLoaded(string pluginId)
signal pluginUnloaded(string pluginId)
function loadPluginData(pluginId, key, defaultValue) {
const cfg = root.instanceData?.config;
if (cfg && key in cfg)
return cfg[key];
return SettingsData.getPluginSetting(pluginId, key, defaultValue);
}
function savePluginData(pluginId, key, value) {
if (!root.instanceId)
return false;
var updates = {};
updates[key] = value;
SettingsData.updateDesktopWidgetInstanceConfig(root.instanceId, updates);
Qt.callLater(() => pluginDataChanged(pluginId));
return true;
}
function getPluginVariants(pluginId) {
return PluginService.getPluginVariants(pluginId);
}
function isPluginLoaded(pluginId) {
return PluginService.isPluginLoaded(pluginId);
}
}
onLoaded: {
if (!item)
return;
revealFade.restart();
if (item.pluginService !== undefined)
item.pluginService = instanceScopedPluginService;
if (item.pluginId !== undefined)
item.pluginId = root.pluginId;
if (item.instanceId !== undefined)
item.instanceId = root.instanceId;
if (item.instanceData !== undefined)
item.instanceData = root.instanceData;
if (item.lockScreen !== undefined)
item.lockScreen = root.lockScreen;
if (item.lockHost !== undefined)
item.lockHost = root.lockHost;
if (root.persistDefaults)
geometry.saveDefaultGeometry(item.defaultWidth ?? item.widgetWidth ?? 280, item.defaultHeight ?? item.widgetHeight ?? 180);
if (item.widgetWidth !== undefined)
item.widgetWidth = Qt.binding(() => root.width);
if (item.widgetHeight !== undefined)
item.widgetHeight = Qt.binding(() => root.height);
if (item.screen !== undefined)
item.screen = Qt.binding(() => root.screen);
if (item.requestResize !== undefined)
item.requestResize = geometry.requestResize;
if (item.clearResize !== undefined)
item.clearResize = geometry.clearResize;
}
}
@@ -0,0 +1,155 @@
import QtQuick
import qs.Common
QtObject {
id: root
property string instanceId: ""
property var instanceData: null
property var screen: null
property real minWidth: 100
property real minHeight: 100
property bool forceSquare: false
readonly property bool isInstance: instanceId !== "" && instanceData !== null
readonly property bool syncPositionAcrossScreens: instanceData?.config?.syncPositionAcrossScreens ?? false
readonly property string screenKey: SettingsData.getScreenDisplayName(screen)
readonly property string positionKey: syncPositionAcrossScreens ? "_synced" : screenKey
readonly property var storedPositions: SessionData.desktopWidgetInstancePositions[instanceId] ?? null
readonly property int screenWidth: screen?.width ?? 1920
readonly property int screenHeight: screen?.height ?? 1080
function storedGeometry(key) {
if (!isInstance)
return undefined;
return storedPositions?.[positionKey]?.[key];
}
function storedCoordinate(key, extent, fallback) {
const val = storedGeometry(key);
if (val === undefined)
return fallback;
return syncPositionAcrossScreens ? val * extent : val;
}
readonly property bool hasSavedPosition: storedGeometry("x") !== undefined
readonly property bool hasSavedSize: storedGeometry("width") !== undefined
property real defaultWidth: 280
property real defaultHeight: 180
property real defaultX: screenWidth / 2 - savedWidth / 2
property real defaultY: screenHeight / 2 - savedHeight / 2
readonly property real savedX: storedCoordinate("x", screenWidth, defaultX)
readonly property real savedY: storedCoordinate("y", screenHeight, defaultY)
readonly property real savedWidth: storedGeometry("width") ?? defaultWidth
readonly property real savedHeight: forceSquare ? savedWidth : (storedGeometry("height") ?? defaultHeight)
property real dragOverrideX: -1
property real dragOverrideY: -1
property real dragOverrideW: -1
property real dragOverrideH: -1
readonly property real effectiveX: dragOverrideX >= 0 ? dragOverrideX : savedX
readonly property real effectiveY: dragOverrideY >= 0 ? dragOverrideY : savedY
readonly property real effectiveW: dragOverrideW >= 0 ? dragOverrideW : savedWidth
readonly property real effectiveH: dragOverrideH >= 0 ? dragOverrideH : savedHeight
readonly property real widgetX: Math.max(0, Math.min(effectiveX, screenWidth - widgetWidth))
readonly property real widgetY: Math.max(0, Math.min(effectiveY, screenHeight - widgetHeight))
readonly property real widgetWidth: Math.max(minWidth, Math.min(effectiveW, screenWidth))
readonly property real widgetHeight: Math.max(minHeight, Math.min(effectiveH, screenHeight))
property var _gridSettingsTrigger: SessionData.desktopWidgetGridSettings
readonly property int gridSize: {
void _gridSettingsTrigger;
return SessionData.getDesktopWidgetGridSetting(screenKey, "size", 40);
}
readonly property bool gridEnabled: {
void _gridSettingsTrigger;
return SessionData.getDesktopWidgetGridSetting(screenKey, "enabled", false);
}
function snapToGrid(value) {
return Math.round(value / gridSize) * gridSize;
}
function clearDragOverrides() {
dragOverrideX = -1;
dragOverrideY = -1;
dragOverrideW = -1;
dragOverrideH = -1;
}
function requestResize(width, height) {
if (width < minWidth || height < minHeight)
return;
if (width > screenWidth || height > screenHeight)
return;
dragOverrideW = width;
dragOverrideH = height;
}
function clearResize() {
dragOverrideW = -1;
dragOverrideH = -1;
}
function saveGeometry(updates) {
if (!isInstance)
return;
SessionData.updateDesktopWidgetInstancePosition(instanceId, positionKey, updates);
}
function savePosition(finalX, finalY) {
saveGeometry({
x: syncPositionAcrossScreens ? finalX / screenWidth : finalX,
y: syncPositionAcrossScreens ? finalY / screenHeight : finalY
});
}
function saveSize(finalW, finalH) {
const sizeVal = forceSquare ? Math.max(finalW, finalH) : finalW;
saveGeometry({
width: sizeVal,
height: forceSquare ? sizeVal : finalH
});
}
function saveDefaultGeometry(defaultWidth, defaultHeight) {
if (!hasSavedSize) {
const finalW = Math.max(minWidth, Math.min(defaultWidth, screenWidth));
const finalH = Math.max(minHeight, Math.min(defaultHeight, screenHeight));
saveSize(finalW, finalH);
}
if (hasSavedPosition)
return;
const finalX = Math.max(0, Math.min(screenWidth / 2 - widgetWidth / 2, screenWidth - widgetWidth));
const finalY = Math.max(0, Math.min(screenHeight / 2 - widgetHeight / 2, screenHeight - widgetHeight));
savePosition(finalX, finalY);
}
function dragMoveTo(startX, startY, dx, dy) {
let newX = Math.max(0, Math.min(startX + dx, screenWidth - widgetWidth));
let newY = Math.max(0, Math.min(startY + dy, screenHeight - widgetHeight));
if (!gridEnabled)
return Qt.point(newX, newY);
newX = Math.max(0, Math.min(snapToGrid(newX), screenWidth - widgetWidth));
newY = Math.max(0, Math.min(snapToGrid(newY), screenHeight - widgetHeight));
return Qt.point(newX, newY);
}
function dragResizeTo(startWidth, startHeight, dx, dy) {
let newW = Math.max(minWidth, Math.min(startWidth + dx, screenWidth - widgetX));
let newH = Math.max(minHeight, Math.min(startHeight + dy, screenHeight - widgetY));
if (gridEnabled) {
newW = Math.max(minWidth, snapToGrid(newW));
newH = Math.max(minHeight, snapToGrid(newH));
}
if (!forceSquare)
return Qt.size(newW, newH);
const size = Math.max(newW, newH);
return Qt.size(Math.min(size, screenWidth - widgetX), Math.min(size, screenHeight - widgetY));
}
}
@@ -0,0 +1,38 @@
import QtQuick
import qs.Common
Item {
id: root
required property int gridSize
required property real extentWidth
required property real extentHeight
opacity: 0.3
Repeater {
model: Math.ceil(root.extentWidth / root.gridSize)
Rectangle {
required property int index
x: index * root.gridSize
y: 0
width: 1
height: root.extentHeight
color: Theme.primary
}
}
Repeater {
model: Math.ceil(root.extentHeight / root.gridSize)
Rectangle {
required property int index
x: 0
y: index * root.gridSize
width: root.extentWidth
height: 1
color: Theme.primary
}
}
}
@@ -8,20 +8,22 @@ import qs.Widgets
WidgetPickerWindow {
id: root
property string listKey: "desktopWidgetInstances"
signal widgetAdded(string widgetType)
function addWidget(widget) {
const widgetType = widget.id;
const defaultConfig = DesktopWidgetRegistry.getDefaultConfig(widgetType);
const name = widget.name || widgetType;
SettingsData.createDesktopWidgetInstance(widgetType, name, defaultConfig);
SettingsData.createDesktopWidgetInstance(widgetType, name, defaultConfig, listKey);
root.widgetAdded(widgetType);
root.hide();
}
objectName: "desktopWidgetBrowser"
title: I18n.tr("Add Desktop Widget")
widgets: DesktopWidgetRegistry.registeredWidgetsList || []
widgets: (DesktopWidgetRegistry.registeredWidgetsList || []).filter(widget => !widget.lockOnly)
featuredFirst: true
showEmptyState: true
widgetDelegate: tileDelegate
@@ -14,7 +14,8 @@ SettingsReorderRow {
readonly property string instanceId: instanceData?.id ?? ""
readonly property string widgetType: instanceData?.widgetType ?? ""
readonly property var widgetDef: DesktopWidgetRegistry.getWidget(widgetType)
readonly property string widgetName: instanceData?.name ?? widgetDef?.name ?? widgetType
readonly property string widgetName: instanceData?.name || widgetDef?.name || widgetType
property bool fixed: false
signal configureRequested
signal deleteRequested
@@ -40,6 +41,7 @@ SettingsReorderRow {
},
DankToggle {
anchors.verticalCenter: parent.verticalCenter
visible: !root.fixed
hideText: true
text: root.title
checked: root.instanceData?.enabled ?? true
@@ -51,6 +53,7 @@ SettingsReorderRow {
},
DankActionButton {
anchors.verticalCenter: parent.verticalCenter
visible: !root.fixed
iconName: "content_copy"
tooltipText: I18n.tr("Duplicate", "verb, desktop widget menu action")
onClicked: root.duplicateRequested()
@@ -58,6 +61,7 @@ SettingsReorderRow {
SettingsDeleteButton {
id: deleteButton
anchors.verticalCenter: parent.verticalCenter
visible: !root.fixed
onDeleteRequested: root.deleteRequested()
}
]
@@ -0,0 +1,120 @@
import QtQuick
import qs.Modules.Settings.LockWidgetSettings as LWS
Loader {
id: root
property string instanceId: ""
property var instanceData: null
property var widgetDef: null
property var parentModal: null
readonly property string widgetType: instanceData?.widgetType ?? ""
active: instanceData !== null
sourceComponent: {
switch (widgetType) {
case "desktopClock":
return clockSettings;
case "systemMonitor":
return systemMonitorSettings;
case "lockClock":
return lockClockSettings;
case "lockAuth":
return lockAuthSettings;
case "lockNotifications":
return lockNotificationsSettings;
case "lockStatus":
return lockStatusSettings;
case "lockDate":
return lockDateSettings;
case "lockPower":
return lockPowerSettings;
default:
return pluginSettings;
}
}
Component {
id: clockSettings
ClockSettings {
instanceId: root.instanceId
instanceData: root.instanceData
}
}
Component {
id: systemMonitorSettings
SystemMonitorSettings {
instanceId: root.instanceId
instanceData: root.instanceData
}
}
Component {
id: lockClockSettings
LWS.LockClockSettings {
instanceId: root.instanceId
instanceData: root.instanceData
}
}
Component {
id: lockAuthSettings
LWS.LockAuthSettings {
instanceId: root.instanceId
instanceData: root.instanceData
}
}
Component {
id: lockNotificationsSettings
LWS.LockNotificationsSettings {
instanceId: root.instanceId
instanceData: root.instanceData
}
}
Component {
id: lockStatusSettings
LWS.LockStatusSettings {
instanceId: root.instanceId
instanceData: root.instanceData
parentModal: root.parentModal
}
}
Component {
id: lockDateSettings
LWS.LockDateSettings {
instanceId: root.instanceId
instanceData: root.instanceData
}
}
Component {
id: lockPowerSettings
LWS.LockPowerSettings {
instanceId: root.instanceId
instanceData: root.instanceData
}
}
Component {
id: pluginSettings
PluginDesktopWidgetSettings {
instanceId: root.instanceId
instanceData: root.instanceData
widgetType: root.widgetType
widgetDef: root.widgetDef
}
}
}
@@ -11,11 +11,15 @@ import qs.Modules.Settings.DesktopWidgetSettings as DWS
Item {
id: root
property var parentModal: null
readonly property string instanceId: SettingsUiState.selectedDesktopWidgetId
readonly property var instanceData: (SettingsData.desktopWidgetInstances || []).find(instance => instance.id === instanceId) ?? null
readonly property bool lockScreenInstance: SettingsData.widgetInstanceListKey(instanceId) === "lockScreenWidgetInstances"
readonly property var instanceData: SettingsData.getDesktopWidgetInstance(instanceId)
readonly property string widgetType: instanceData?.widgetType ?? ""
readonly property var widgetDef: DesktopWidgetRegistry.getWidget(widgetType)
readonly property string widgetName: instanceData?.name ?? widgetDef?.name ?? widgetType
readonly property string widgetName: instanceData?.name || widgetDef?.name || widgetType
readonly property bool fixed: widgetType === "lockAuth"
readonly property var cfg: instanceData?.config ?? {}
readonly property string overlayCommand: "dms ipc call desktopWidget toggleOverlay " + instanceId
readonly property var groupOptions: [
@@ -36,40 +40,18 @@ Item {
onWidgetNameChanged: SettingsUiState.selectedWidgetTitle = widgetName
Component {
id: clockSettings
DWS.ClockSettings {
instanceId: root.instanceId
instanceData: root.instanceData
}
}
Component {
id: systemMonitorSettings
DWS.SystemMonitorSettings {
instanceId: root.instanceId
instanceData: root.instanceData
}
}
Component {
id: pluginSettings
DWS.PluginDesktopWidgetSettings {
instanceId: root.instanceId
instanceData: root.instanceData
widgetType: root.widgetType
widgetDef: root.widgetDef
}
}
SettingsPage {
visible: root.instanceData !== null
SettingsCard {
SettingsRow {
visible: root.fixed
iconName: root.widgetDef?.icon ?? "widgets"
title: root.widgetDef?.name ?? root.widgetType
}
SettingsToggleRow {
visible: !root.fixed
iconName: root.widgetDef?.icon ?? "widgets"
text: root.widgetDef?.name ?? root.widgetType
description: root.widgetDef?.description ?? ""
@@ -99,25 +81,19 @@ Item {
}
}
Loader {
DWS.DesktopWidgetTypeSettings {
width: parent.width
active: root.instanceData !== null
sourceComponent: {
switch (root.widgetType) {
case "desktopClock":
return clockSettings;
case "systemMonitor":
return systemMonitorSettings;
default:
return pluginSettings;
}
}
instanceId: root.instanceId
instanceData: root.instanceData
widgetDef: root.widgetDef
parentModal: root.parentModal
}
SettingsCard {
title: I18n.tr("Behavior")
SettingsToggleRow {
visible: !root.lockScreenInstance
text: I18n.tr("Show on overlay")
description: I18n.tr("Keeps the widget above windows instead of below them", "desktop widget show on overlay toggle description")
checked: root.cfg.showOnOverlay ?? false
@@ -125,20 +101,21 @@ Item {
}
SettingsToggleRow {
visible: CompositorService.isNiri
visible: CompositorService.isNiri && !root.lockScreenInstance
text: I18n.tr("Show on overview")
checked: root.cfg.showOnOverview ?? false
onToggled: checked => root.updateConfig("showOnOverview", checked)
}
SettingsToggleRow {
visible: CompositorService.isNiri
visible: CompositorService.isNiri && !root.lockScreenInstance
text: I18n.tr("Show on overview only")
checked: root.cfg.showOnOverviewOnly ?? false
onToggled: checked => root.updateConfig("showOnOverviewOnly", checked)
}
SettingsToggleRow {
visible: !root.lockScreenInstance
text: I18n.tr("Click through")
checked: root.cfg.clickThrough ?? false
onToggled: checked => root.updateConfig("clickThrough", checked)
@@ -156,6 +133,7 @@ Item {
}
SettingsCard {
visible: !root.lockScreenInstance
title: I18n.tr("Command")
SettingsRow {
+58 -90
View File
@@ -268,109 +268,77 @@ Item {
}
}
LazyLoader {
id: lockWidgetBrowserLoader
active: false
DesktopWidgetBrowser {
parentModal: root.parentModal
listKey: "lockScreenWidgetInstances"
title: I18n.tr("Add widget")
onWidgetAdded: ToastService.showInfo(I18n.tr("Widget added"))
}
}
SettingsPage {
id: mainColumn
SettingsCard {
width: parent.width
iconName: "lock"
title: I18n.tr("Layout")
settingKey: "lockLayout"
iconName: "widgets"
title: I18n.tr("Widgets")
settingKey: "lockScreenWidgets"
tags: ["lock", "screen", "widgets", "clock", "plugins"]
SettingsToggleRow {
settingKey: "lockScreenShowPowerActions"
tags: ["lock", "screen", "power", "actions", "shutdown", "reboot"]
text: I18n.tr("Show power actions")
checked: SettingsData.lockScreenShowPowerActions
onToggled: checked => SettingsData.set("lockScreenShowPowerActions", checked)
}
SettingsReorderList {
id: lockWidgetList
SettingsToggleRow {
settingKey: "lockScreenShowSystemIcons"
tags: ["lock", "screen", "system", "icons", "status"]
text: I18n.tr("Show system icons")
checked: SettingsData.lockScreenShowSystemIcons
onToggled: checked => SettingsData.set("lockScreenShowSystemIcons", checked)
}
model: SettingsData.lockScreenWidgetInstances || []
SettingsToggleRow {
settingKey: "lockScreenShowTime"
tags: ["lock", "screen", "time", "clock", "display"]
text: I18n.tr("Show time")
checked: SettingsData.lockScreenShowTime
onToggled: checked => SettingsData.set("lockScreenShowTime", checked)
}
delegate: DesktopWidgetInstanceCard {
required property var modelData
SettingsButtonGroupRow {
settingKey: "lockScreenClockStyle"
tags: ["lock", "screen", "time", "clock", "style", "vertical"]
text: I18n.tr("Clock style")
visible: SettingsData.lockScreenShowTime
model: [I18n.tr("Horizontal", "lock screen clock style option"), I18n.tr("Vertical", "lock screen clock style option")]
currentIndex: SettingsData.lockScreenClockStyle === "vertical" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("lockScreenClockStyle", index === 1 ? "vertical" : "horizontal");
}
}
reorderList: lockWidgetList
reorderEnabled: false
instanceData: modelData
fixed: modelData.widgetType === "lockAuth"
SettingsToggleRow {
settingKey: "lockScreenShowDate"
tags: ["lock", "screen", "date", "calendar", "display"]
text: I18n.tr("Show date")
checked: SettingsData.lockScreenShowDate
onToggled: checked => SettingsData.set("lockScreenShowDate", checked)
}
SettingsToggleRow {
settingKey: "lockScreenShowProfileImage"
tags: ["lock", "screen", "profile", "image", "avatar", "picture"]
text: I18n.tr("Show profile image")
checked: SettingsData.lockScreenShowProfileImage
onToggled: checked => SettingsData.set("lockScreenShowProfileImage", checked)
}
SettingsToggleRow {
settingKey: "lockScreenShowPasswordField"
tags: ["lock", "screen", "password", "field", "input", "visible"]
text: I18n.tr("Show password field")
description: I18n.tr("A hidden field appears as soon as a key is pressed")
checked: SettingsData.lockScreenShowPasswordField
onToggled: checked => SettingsData.set("lockScreenShowPasswordField", checked)
}
SettingsToggleRow {
settingKey: "lockScreenShowMediaPlayer"
tags: ["lock", "screen", "media", "player", "music", "mpris"]
text: I18n.tr("Show media player")
checked: SettingsData.lockScreenShowMediaPlayer
onToggled: checked => SettingsData.set("lockScreenShowMediaPlayer", checked)
}
SettingsSplitRow {
settingKey: "lockScreenShowWeather"
tab: "lock_screen"
tags: ["weather", "temperature"]
title: I18n.tr("Weather")
checked: SettingsData.lockScreenShowWeather
onToggled: checked => SettingsData.set("lockScreenShowWeather", checked)
onNavigated: keyboard => root.parentModal?.navigateTo("weather", keyboard)
}
SettingsDropdownRow {
settingKey: "lockScreenNotificationMode"
tags: ["lock", "screen", "notification", "notifications", "privacy"]
text: I18n.tr("Notifications")
options: [I18n.tr("Disabled", "lock screen notification mode option"), I18n.tr("Count only", "lock screen notification mode option"), I18n.tr("App names", "lock screen notification mode option"), I18n.tr("Full content", "lock screen notification mode option")]
currentValue: options[SettingsData.lockScreenNotificationMode] || options[0]
onValueChanged: value => {
const idx = options.indexOf(value);
if (idx >= 0) {
SettingsData.set("lockScreenNotificationMode", idx);
onConfigureRequested: {
SettingsUiState.selectedDesktopWidgetId = instanceId;
SettingsUiState.selectedWidgetTitle = widgetName;
root.parentModal?.navigateTo("desktop_widget");
}
onDuplicateRequested: SettingsData.duplicateDesktopWidgetInstance(instanceId)
onDeleteRequested: {
SettingsData.removeDesktopWidgetInstance(instanceId);
ToastService.showInfo(I18n.tr("Widget removed"));
}
}
}
SettingsRow {
iconName: "add"
title: I18n.tr("Add widget")
clickable: true
onClicked: {
lockWidgetBrowserLoader.active = true;
lockWidgetBrowserLoader.item?.show();
}
}
SettingsRow {
iconName: "edit"
title: I18n.tr("Edit widgets")
clickable: true
onClicked: SessionService.lockEditorRequested()
}
SettingsRow {
iconName: "restart_alt"
title: I18n.tr("Reset to default")
clickable: true
onClicked: SettingsData.resetLockScreenWidgets()
}
}
SettingsCard {
@@ -0,0 +1,51 @@
import QtQuick
import qs.Common
import qs.Modules.Settings.Widgets
import qs.Modules.Settings.DesktopWidgetSettings
DesktopWidgetInstanceSettings {
id: root
readonly property var styles: ["pill", "expressive", "ring", "minimal", "dots"]
readonly property var styleLabels: [I18n.tr("Pill", "lock screen password field style"), I18n.tr("Expressive"), I18n.tr("Ring"), I18n.tr("Minimal", "lock screen password field style"), I18n.tr("Dots", "lock screen password field style")]
readonly property var visibilityModes: ["always", "typing", "never"]
readonly property var visibilityLabels: [I18n.tr("Always show", "lock screen widget visibility"), I18n.tr("Hide until typing", "lock screen widget visibility"), I18n.tr("Never")]
readonly property string profileVisibility: cfg.profileVisibility ?? (cfg.showProfileImage === false ? "never" : "always")
readonly property string passwordVisibility: cfg.passwordVisibility ?? (cfg.showPasswordField === false ? "typing" : "always")
showAppearance: false
SettingsDropdownRow {
text: I18n.tr("Style")
options: root.styleLabels
currentValue: root.styleLabels[Math.max(0, root.styles.indexOf(root.cfg.style ?? "expressive"))]
onValueChanged: value => {
const idx = root.styleLabels.indexOf(value);
if (idx >= 0)
root.updateConfig("style", root.styles[idx]);
}
}
SettingsDropdownRow {
text: I18n.tr("Profile image")
options: root.visibilityLabels
currentValue: root.visibilityLabels[Math.max(0, root.visibilityModes.indexOf(root.profileVisibility))]
onValueChanged: value => {
const index = root.visibilityLabels.indexOf(value);
if (index >= 0)
root.updateConfig("profileVisibility", root.visibilityModes[index]);
}
}
SettingsDropdownRow {
text: I18n.tr("Password field", "lock screen password field visibility setting")
description: root.passwordVisibility === "typing" ? I18n.tr("Typing reveals the field. Escape hides it again. The editor always shows it.", "lock screen hide until typing behavior") : ""
options: root.visibilityLabels.slice(0, 2)
currentValue: root.visibilityLabels[Math.max(0, root.visibilityModes.indexOf(root.passwordVisibility))]
onValueChanged: value => {
const index = root.visibilityLabels.indexOf(value);
if (index >= 0 && index < 2)
root.updateConfig("passwordVisibility", root.visibilityModes[index]);
}
}
}
@@ -0,0 +1,92 @@
import QtQuick
import qs.Common
import qs.Modules.Settings.Widgets
import qs.Modules.Settings.DesktopWidgetSettings
DesktopWidgetInstanceSettings {
id: root
readonly property var styles: ["horizontal", "vertical", "expressive", "analog"]
readonly property var styleLabels: [I18n.tr("Horizontal", "lock screen clock style option"), I18n.tr("Vertical", "lock screen clock style option"), I18n.tr("Expressive"), I18n.tr("Analog")]
readonly property string style: cfg.style ?? "expressive"
readonly property bool textStyle: style === "horizontal" || style === "vertical"
showAppearance: false
SettingsDropdownRow {
text: I18n.tr("Clock style")
options: root.styleLabels
currentValue: root.styleLabels[Math.max(0, root.styles.indexOf(root.style))]
onValueChanged: value => {
const idx = root.styleLabels.indexOf(value);
if (idx >= 0)
root.updateConfig("style", root.styles[idx]);
}
}
SettingsSliderRow {
visible: root.textStyle
text: I18n.tr("Weight")
minimum: 0
maximum: 900
step: 100
minimumLabel: I18n.tr("Default")
value: root.cfg.weight ?? 0
onSliderValueChanged: newValue => root.updateConfig("weight", newValue)
}
SettingsFontDropdownRow {
visible: root.textStyle
text: I18n.tr("Font")
currentFont: root.cfg.fontFamily ?? ""
defaultFamily: SettingsData.lockScreenFontFamily || Theme.defaultFontFamily
onFontSelected: family => root.updateConfig("fontFamily", family)
}
SettingsToggleRow {
visible: root.style === "horizontal"
text: I18n.tr("Italic")
checked: root.cfg.italic ?? false
onToggled: checked => root.updateConfig("italic", checked)
}
SettingsToggleRow {
visible: root.style === "horizontal" || root.style === "expressive"
text: I18n.tr("Accent minutes", "lock screen clock colors")
checked: root.cfg.twoTone ?? true
onToggled: checked => root.updateConfig("twoTone", checked)
}
SettingsToggleRow {
text: I18n.tr("Automatic placement", "lock screen clock placement")
description: I18n.tr("Choose a clear area of the background. Drag the clock to place it yourself.", "lock screen automatic clock placement")
checked: root.cfg.autoPosition ?? true
onToggled: checked => {
if (checked)
SessionData.resetDesktopWidgetInstanceGeometry(root.instanceId, ["x", "y"]);
root.updateConfig("autoPosition", checked);
}
}
SettingsToggleRow {
visible: root.style === "analog"
text: I18n.tr("Show hour numbers")
checked: root.cfg.showAnalogNumbers ?? false
onToggled: checked => root.updateConfig("showAnalogNumbers", checked)
}
SettingsToggleRow {
visible: root.style === "analog"
text: I18n.tr("Show seconds")
checked: root.cfg.showAnalogSeconds ?? true
onToggled: checked => root.updateConfig("showAnalogSeconds", checked)
}
SettingsColorPicker {
showDefault: true
colorMode: root.cfg.colorMode ?? "default"
customColor: root.cfg.customColor ?? "#ffffff"
onColorModeSelected: mode => root.updateConfig("colorMode", mode)
onCustomColorSelected: selectedColor => root.updateConfig("customColor", selectedColor.toString())
}
}
@@ -0,0 +1,34 @@
import QtQuick
import qs.Common
import qs.Modules.Settings.Widgets
import qs.Modules.Settings.DesktopWidgetSettings
DesktopWidgetInstanceSettings {
id: root
showAppearance: false
SettingsToggleRow {
text: I18n.tr("Show date")
checked: root.instanceData?.enabled !== false
onToggled: checked => SettingsData.updateDesktopWidgetInstance(root.instanceId, {
enabled: checked
})
}
SettingsDropdownRow {
text: I18n.tr("Date format")
options: [I18n.tr("Short"), I18n.tr("Long")]
currentValue: (root.cfg.format ?? "long") === "short" ? I18n.tr("Short") : I18n.tr("Long")
enabled: SettingsData.lockDateFormat === ""
onValueChanged: value => root.updateConfig("format", value === I18n.tr("Short") ? "short" : "long")
}
SettingsColorPicker {
showDefault: true
colorMode: root.cfg.colorMode ?? "default"
customColor: root.cfg.customColor ?? "#ffffff"
onColorModeSelected: mode => root.updateConfig("colorMode", mode)
onCustomColorSelected: selectedColor => root.updateConfig("customColor", selectedColor.toString())
}
}
@@ -0,0 +1,22 @@
import QtQuick
import qs.Common
import qs.Modules.Settings.Widgets
import qs.Modules.Settings.DesktopWidgetSettings
DesktopWidgetInstanceSettings {
id: root
showAppearance: false
SettingsDropdownRow {
text: I18n.tr("Lock screen content")
options: [I18n.tr("Count only", "lock screen notification mode option"), I18n.tr("App names", "lock screen notification mode option"), I18n.tr("Full content", "lock screen notification mode option")]
currentValue: options[(root.cfg.mode ?? 1) - 1] ?? options[0]
onValueChanged: value => {
const idx = options.indexOf(value);
if (idx < 0)
return;
root.updateConfig("mode", idx + 1);
}
}
}
@@ -0,0 +1,66 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Settings.Widgets
import qs.Modules.Settings.DesktopWidgetSettings
DesktopWidgetInstanceSettings {
id: root
readonly property var shapes: ["round", "cookie9", "clover4", "sunny", "softBurst"]
readonly property string shape: cfg.shape ?? "round"
showAppearance: false
SettingsRow {
title: I18n.tr("Style")
body: Row {
width: parent.width
spacing: Theme.spacingS
Repeater {
model: root.shapes
Rectangle {
id: swatch
required property string modelData
readonly property bool selected: root.shape === modelData
width: (parent.width - Theme.spacingS * (root.shapes.length - 1)) / root.shapes.length
height: Theme.listItemHeight + Theme.spacingXS
radius: Theme.cornerRadius
color: selected ? Theme.primarySelected : SettingsMetrics.controlColor
border.color: selected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: Theme.outlineWidthFocused
Accessible.role: Accessible.RadioButton
Accessible.name: modelData
DankMaterialShape {
anchors.centerIn: parent
width: Theme.iconSizeLarge
height: Theme.iconSizeLarge
shape: swatch.modelData === "round" ? "circle" : swatch.modelData
color: Theme.secondaryContainer
DankIcon {
anchors.centerIn: parent
name: "power_settings_new"
size: Theme.iconSizeSmall
color: Theme.onSecondaryContainer
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.updateConfig("shape", swatch.modelData)
}
}
}
}
}
}
@@ -0,0 +1,31 @@
import QtQuick
import qs.Common
import qs.Modules.Settings.Widgets
import qs.Modules.Settings.DesktopWidgetSettings
DesktopWidgetInstanceSettings {
id: root
property var parentModal: null
showAppearance: false
SettingsToggleRow {
text: I18n.tr("Background")
checked: root.cfg.background ?? false
onToggled: checked => root.updateConfig("background", checked)
}
SettingsToggleRow {
text: I18n.tr("Show media player")
checked: root.cfg.showMediaPlayer ?? true
onToggled: checked => root.updateConfig("showMediaPlayer", checked)
}
SettingsSplitRow {
title: I18n.tr("Weather")
checked: root.cfg.showWeather ?? true
onToggled: checked => root.updateConfig("showWeather", checked)
onNavigated: keyboard => root.parentModal?.navigateTo("weather", keyboard)
}
}
@@ -319,16 +319,26 @@ Column {
}
SettingsDropdownRow {
settingKey: "lockScreenNotificationMode"
readonly property var lockNotifications: SettingsData.lockWidgetInstance("lockNotifications")
readonly property int currentMode: !lockNotifications || lockNotifications.enabled === false ? 0 : (lockNotifications.config?.mode ?? 1)
settingKey: "lockNotificationsMode"
tags: ["lock", "screen", "notification", "notifications", "privacy"]
visible: lockNotifications !== null
text: I18n.tr("Lock screen content")
options: [I18n.tr("Disabled", "lock screen notification mode option"), I18n.tr("Count only", "lock screen notification mode option"), I18n.tr("App names", "lock screen notification mode option"), I18n.tr("Full content", "lock screen notification mode option")]
currentValue: options[SettingsData.lockScreenNotificationMode] || options[0]
currentValue: options[currentMode] || options[0]
onValueChanged: value => {
const idx = options.indexOf(value);
if (idx < 0)
return;
SettingsData.set("lockScreenNotificationMode", idx);
SettingsData.updateDesktopWidgetInstance(lockNotifications.id, {
enabled: idx > 0
});
if (idx > 0)
SettingsData.updateDesktopWidgetInstanceConfig(lockNotifications.id, {
mode: idx
});
}
}
}
@@ -127,12 +127,17 @@ Item {
settingKey: "weatherDisplay"
SettingsToggleRow {
readonly property var lockStatus: SettingsData.lockWidgetInstance("lockStatus")
tab: "weather"
settingKey: "lockScreenShowWeather"
settingKey: "lockStatusWeather"
tags: ["weather", "lock", "screen"]
visible: lockStatus !== null
text: I18n.tr("Lock screen")
checked: SettingsData.lockScreenShowWeather
onToggled: checked => SettingsData.set("lockScreenShowWeather", checked)
checked: lockStatus?.config?.showWeather ?? true
onToggled: checked => SettingsData.updateDesktopWidgetInstanceConfig(lockStatus.id, {
showWeather: checked
})
}
SettingsNavRow {
@@ -11,6 +11,8 @@ SettingsRow {
property string colorMode: "primary"
property color customColor: "#ffffff"
property string pickerTitle: I18n.tr("Choose color", "color picker title")
property bool showDefault: false
property color defaultColor: Theme.lockScreenContentColor
signal colorModeSelected(string mode)
signal customColorSelected(color selectedColor)
@@ -22,7 +24,14 @@ SettingsRow {
spacing: Theme.spacingS
Repeater {
model: [
id: swatches
model: (root.showDefault ? [
{
id: "default",
label: I18n.tr("Default"),
color: root.defaultColor
}
] : []).concat([
{
id: "primary",
label: I18n.tr("Primary"),
@@ -38,13 +47,13 @@ SettingsRow {
label: I18n.tr("Custom"),
color: root.customColor
}
]
])
Rectangle {
required property var modelData
required property int index
width: (parent.width - Theme.spacingS * 2) / 3
width: (parent.width - Theme.spacingS * (swatches.count - 1)) / swatches.count
height: Theme.listItemHeight + Theme.spacingXS
radius: Theme.cornerRadius
color: root.colorMode === modelData.id ? Theme.primarySelected : SettingsMetrics.controlColor
@@ -41,6 +41,7 @@ SettingsRow {
DankDragHandle {
id: handle
visible: root.reorderEnabled
coordinateItem: root.reorderList
upDownKeysMove: false
label: root.title
+45 -11
View File
@@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import "../Common/settings/SettingsSpec.js" as Spec
Singleton {
id: root
@@ -15,6 +16,7 @@ Singleton {
Component.onCompleted: {
registerBuiltins();
registerLockBuiltins();
Qt.callLater(syncPluginWidgets);
}
@@ -64,6 +66,28 @@ Singleton {
});
}
function registerLockBuiltins() {
const lockWidget = (id, name, icon) => registerWidget({
id: id,
name: name,
icon: icon,
description: "",
type: "builtin",
lockOnly: true,
defaultConfig: Spec.SPEC.lockScreenWidgetInstances.def.find(inst => inst.widgetType === id)?.config ?? {},
defaultSize: {
width: 200,
height: 200
}
});
lockWidget("lockClock", I18n.tr("Clock"), "schedule");
lockWidget("lockDate", I18n.tr("Date"), "event");
lockWidget("lockAuth", I18n.tr("Password"), "lock");
lockWidget("lockNotifications", I18n.tr("Notifications"), "notifications");
lockWidget("lockStatus", I18n.tr("Status"), "wifi");
lockWidget("lockPower", I18n.tr("Power"), "power_settings_new");
}
function getDefaultClockConfig() {
return {
style: "analog",
@@ -133,18 +157,24 @@ Singleton {
if (!widget)
return {};
if (widget.type === "builtin") {
switch (widgetType) {
case "desktopClock":
return getDefaultClockConfig();
case "systemMonitor":
return getDefaultSystemMonitorConfig();
default:
return widget.defaultConfig ?? {};
}
switch (widgetType) {
case "desktopClock":
return getDefaultClockConfig();
case "systemMonitor":
return getDefaultSystemMonitorConfig();
}
return JSON.parse(JSON.stringify(widget.defaultConfig ?? {}));
}
return widget.defaultConfig ?? {};
function showsOnScreen(prefs, screen) {
if (!Array.isArray(prefs) || prefs.length === 0 || prefs.includes("all"))
return true;
const screenKey = SettingsData.getScreenDisplayName(screen);
return prefs.some(p => {
if (typeof p === "string")
return p === screenKey || p === screen?.name;
return p?.name === screen?.name || p === screenKey;
});
}
function getDefaultSize(widgetType) {
@@ -218,7 +248,11 @@ Singleton {
}
function getBuiltinWidgets() {
return registeredWidgetsList.filter(w => w.type === "builtin");
return registeredWidgetsList.filter(w => w.type === "builtin" && !w.lockOnly);
}
function getDesktopWidgets() {
return registeredWidgetsList.filter(w => !w.lockOnly);
}
function getPluginWidgets() {
@@ -0,0 +1,84 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Common
Singleton {
id: root
property var decodedSamples: ({})
property var layouts: ({})
function neededFor(screen) {
return SettingsData.lockScreenWidgetInstances.some(instance => {
if (instance.widgetType !== "lockClock" || instance.enabled === false || instance.config?.autoPosition === false || !DesktopWidgetRegistry.showsOnScreen(instance.config?.displayPreferences, screen))
return false;
const positionKey = instance.config?.syncPositionAcrossScreens ? "_synced" : SettingsData.getScreenDisplayName(screen);
return SessionData.desktopWidgetInstancePositions[instance.id]?.[positionKey]?.x === undefined;
});
}
function backgroundKey(screenName, width, height) {
const custom = SettingsData.lockScreenWallpaperPath !== "";
const source = custom ? SettingsData.lockScreenWallpaperPath : SessionData.getMonitorWallpaper(screenName);
const backdrop = !source || source.startsWith("#");
const fill = SettingsData.lockScreenWallpaperFillMode || (custom ? "Fill" : SessionData.getMonitorWallpaperFillMode(screenName));
return JSON.stringify([1, source, width, height, fill, fill === "Scrolling" ? SessionData.getMonitorScrollPosition(screenName) : null, backdrop ? SessionData.getMonitorMaterialWallpaper(screenName) : null, backdrop ? Theme.wallpaperPalette() : null, Theme.lockScreenBlur, Theme.lockScreenBlurMax, Theme.lockScreenScrimAlpha, Theme.screenOffColor.toString(), SettingsData.effectiveWallpaperBackgroundColor.toString()]);
}
function sampleFor(screenName, key) {
const sample = CacheData.lockScreenPlacementSamples[screenName];
if (!sample || sample.key !== key || typeof sample.key !== "string" || !Number.isInteger(sample.width) || !Number.isInteger(sample.height) || sample.width < 1 || sample.height < 1 || sample.width > 128 || sample.height > 128 || typeof sample.pixels !== "string" || sample.pixels.length !== sample.width * sample.height * 2)
return null;
return sample;
}
function hasSample(screenName, key) {
return sampleFor(screenName, key) !== null;
}
function storeSample(screenName, key, luminances, width, height) {
const pixels = luminances.map(value => Math.round(Math.max(0, Math.min(1, value)) * 255).toString(16).padStart(2, "0")).join("");
delete decodedSamples[screenName];
delete layouts[screenName];
CacheData.set("lockScreenPlacementSamples", Object.assign({}, CacheData.lockScreenPlacementSamples, {
[screenName]: {
key,
pixels,
width,
height
}
}));
}
function layoutFor(screenName, sample, key, calculate) {
if (!sample)
return {};
const cached = layouts[screenName];
if (cached?.sampleKey === sample.key && cached.key === key)
return cached.positions;
let decoded = decodedSamples[screenName];
if (decoded?.key !== sample.key) {
const luminances = [];
for (let i = 0; i < sample.pixels.length; i += 2) {
const value = parseInt(sample.pixels.slice(i, i + 2), 16);
if (!Number.isFinite(value))
return {};
luminances.push(value / 255);
}
decoded = {
key: sample.key,
luminances
};
decodedSamples[screenName] = decoded;
}
const positions = calculate(decoded.luminances, sample.width, sample.height);
layouts[screenName] = {
sampleKey: sample.key,
key,
positions
};
return positions;
}
}
+1
View File
@@ -43,6 +43,7 @@ Singleton {
signal sessionLocked
signal sessionUnlocked
signal sessionResumed
signal lockEditorRequested
signal lidOpened
signal loginctlStateChanged
+2
View File
@@ -12,6 +12,7 @@ import Quickshell
import qs.Common
import qs.DankCommon.Common as DC
import qs.Modules
import qs.Modules.Lock
import qs.Services
ShellRoot {
@@ -49,6 +50,7 @@ ShellRoot {
sourceComponent: Scope {
WallpaperBackground {}
LockPlacementPreparer {}
Loader {
active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri
@@ -1516,6 +1516,30 @@
"page": "personalization",
"description": "Requires restart"
},
{
"section": "lockNotificationsMode",
"label": "Lock screen content",
"tabIndex": -1,
"category": "Notifications",
"keywords": [
"alert",
"alerts",
"content",
"lock",
"lockscreen",
"login",
"messages",
"notif",
"notification",
"notifications",
"notifs",
"privacy",
"screen",
"security",
"toast"
],
"page": "notifications"
},
{
"section": "notificationHistorySaveLow",
"label": "Low",
@@ -7088,26 +7112,6 @@
"parentLabel": "Security",
"icon": "lock"
},
{
"section": "lockScreenClockStyle",
"label": "Clock style",
"tabIndex": 11,
"category": "Lock screen",
"keywords": [
"clock",
"lock",
"lockscreen",
"login",
"password",
"screen",
"security",
"style",
"time",
"vertical",
"watch"
],
"parentLabel": "Security"
},
{
"section": "lockDisplay",
"label": "Displays",
@@ -7174,33 +7178,6 @@
],
"parentLabel": "Security"
},
{
"section": "lockLayout",
"label": "Layout",
"tabIndex": 11,
"category": "Lock screen",
"keywords": [
"actions",
"appears",
"field",
"hidden",
"layout",
"lock",
"lockscreen",
"login",
"password",
"power",
"pressed",
"reboot",
"screen",
"security",
"shutdown",
"soon"
],
"parentLabel": "Security",
"icon": "lock",
"description": "A hidden field appears as soon as a key is pressed"
},
{
"section": "lockAtStartup",
"label": "Lock at startup",
@@ -7260,28 +7237,6 @@
"parentLabel": "Security",
"description": "Layout, authentication, screensaver"
},
{
"section": "lockScreenNotificationMode",
"label": "Notifications",
"tabIndex": 11,
"category": "Lock screen",
"keywords": [
"alert",
"alerts",
"lock",
"lockscreen",
"login",
"notif",
"notification",
"notifications",
"notifs",
"password",
"privacy",
"screen",
"security"
],
"parentLabel": "Security"
},
{
"section": "lockScreenVideoEnabled",
"label": "Play video on lock",
@@ -7431,153 +7386,6 @@
],
"parentLabel": "Security"
},
{
"section": "lockScreenShowDate",
"label": "Show date",
"tabIndex": 11,
"category": "Lock screen",
"keywords": [
"calendar",
"date",
"display",
"lock",
"lockscreen",
"login",
"password",
"screen",
"security",
"show"
],
"parentLabel": "Security"
},
{
"section": "lockScreenShowMediaPlayer",
"label": "Show media player",
"tabIndex": 11,
"category": "Lock screen",
"keywords": [
"audio",
"lock",
"lockscreen",
"login",
"media",
"mpris",
"music",
"password",
"playback",
"player",
"screen",
"security",
"show"
],
"parentLabel": "Security"
},
{
"section": "lockScreenShowPasswordField",
"label": "Show password field",
"tabIndex": 11,
"category": "Lock screen",
"keywords": [
"appears",
"field",
"hidden",
"input",
"lock",
"lockscreen",
"login",
"password",
"pressed",
"screen",
"security",
"show",
"soon",
"visible"
],
"parentLabel": "Security",
"description": "A hidden field appears as soon as a key is pressed"
},
{
"section": "lockScreenShowPowerActions",
"label": "Show power actions",
"tabIndex": 11,
"category": "Lock screen",
"keywords": [
"actions",
"hibernate",
"lock",
"lockscreen",
"login",
"password",
"power",
"reboot",
"restart",
"screen",
"security",
"show",
"shutdown",
"sleep",
"suspend"
],
"parentLabel": "Security"
},
{
"section": "lockScreenShowProfileImage",
"label": "Show profile image",
"tabIndex": 11,
"category": "Lock screen",
"keywords": [
"avatar",
"image",
"lock",
"lockscreen",
"login",
"password",
"picture",
"profile",
"screen",
"security",
"show"
],
"parentLabel": "Security"
},
{
"section": "lockScreenShowSystemIcons",
"label": "Show system icons",
"tabIndex": 11,
"category": "Lock screen",
"keywords": [
"icons",
"lock",
"lockscreen",
"login",
"password",
"screen",
"security",
"show",
"status",
"system"
],
"parentLabel": "Security"
},
{
"section": "lockScreenShowTime",
"label": "Show time",
"tabIndex": 11,
"category": "Lock screen",
"keywords": [
"clock",
"display",
"lock",
"lockscreen",
"login",
"password",
"screen",
"security",
"show",
"time"
],
"parentLabel": "Security"
},
{
"section": "lockPamExternallyManaged",
"label": "Use system PAM authentication",
@@ -7643,23 +7451,25 @@
"parentLabel": "Security"
},
{
"section": "lockScreenShowWeather",
"label": "Weather",
"section": "lockScreenWidgets",
"label": "Widgets",
"tabIndex": 11,
"category": "Lock screen",
"keywords": [
"climate",
"forecast",
"clock",
"components",
"lock",
"lockscreen",
"login",
"modules",
"password",
"plugins",
"screen",
"security",
"temperature",
"weather"
"widgets"
],
"parentLabel": "Security"
"parentLabel": "Security",
"icon": "widgets"
},
{
"section": "loginctlLockIntegration",
@@ -16145,6 +15955,27 @@
],
"parentLabel": "Date, time & region"
},
{
"section": "lockStatusWeather",
"label": "Lock screen",
"tabIndex": 56,
"category": "Weather",
"keywords": [
"climate",
"date,",
"forecast",
"lock",
"lockscreen",
"login",
"region",
"screen",
"security",
"temperature",
"time",
"weather"
],
"parentLabel": "Date, time & region"
},
{
"section": "weatherEnabled",
"label": "Weather",