mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-28 14:07:47 -04:00
brightness: move every device pinned on a screen together (#3939)
port 2.0
This commit is contained in:
1 parent
4a9ab29515
commit
5d53fdb684
8 files changed
+165
-68
No files matched your search
@@ -7,6 +7,7 @@ import qs.Modules.ControlCenter
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
import qs.Services
|
||||
import qs.DCommon.Widgets
|
||||
import "../../../Common/QmlUtils.js" as QmlUtils
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -27,6 +28,17 @@ Item {
|
||||
readonly property bool multipleDevices: devices.length > 1
|
||||
readonly property bool showPins: screenName.length > 0 && multipleDevices
|
||||
|
||||
readonly property Item headerActions: DActionButton {
|
||||
visible: root.showPins && QmlUtils.normalizePinList((CacheData.brightnessDevicePins || {})[root.getScreenPinKey()]).length > 0
|
||||
buttonSize: CcMetrics.headerActionSize
|
||||
iconSize: CcMetrics.headerActionIconSize
|
||||
iconName: "keep_off"
|
||||
iconColor: Theme.surfaceText
|
||||
tooltipText: I18n.tr("Clear All")
|
||||
Accessible.name: I18n.tr("Clear All")
|
||||
onClicked: root.clearScreenPins()
|
||||
}
|
||||
|
||||
signal deviceNameChanged(string newDeviceName)
|
||||
|
||||
function getScreenPinKey() {
|
||||
@@ -43,10 +55,9 @@ Item {
|
||||
function resolveCurrentDevice() {
|
||||
if (!BrightnessService.brightnessAvailable || devices.length === 0)
|
||||
return "";
|
||||
const pinKey = getScreenPinKey();
|
||||
const pinnedDevice = pinKey ? (CacheData.brightnessDevicePins || {})[pinKey] : "";
|
||||
if (pinnedDevice && devices.find(d => d.name === pinnedDevice))
|
||||
return pinnedDevice;
|
||||
const pinned = BrightnessService.pinnedDevices(getScreenPinKey());
|
||||
if (pinned.length > 0)
|
||||
return pinned[0];
|
||||
if (instanceId) {
|
||||
const widget = (SettingsData.controlCenterWidgets || []).find(w => w.id === "brightnessSlider" && w.instanceId === instanceId);
|
||||
if (widget?.deviceName && devices.find(d => d.name === widget.deviceName))
|
||||
@@ -68,15 +79,9 @@ Item {
|
||||
function selectDevice(deviceName) {
|
||||
if (!deviceName || deviceName === currentDeviceName)
|
||||
return;
|
||||
const pinKey = getScreenPinKey();
|
||||
if (pinKey) {
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
if (pins[pinKey] && pins[pinKey] !== deviceName) {
|
||||
const next = JSON.parse(JSON.stringify(pins));
|
||||
delete next[pinKey];
|
||||
CacheData.set("brightnessDevicePins", next);
|
||||
}
|
||||
}
|
||||
const pinned = QmlUtils.normalizePinList((CacheData.brightnessDevicePins || {})[getScreenPinKey()]);
|
||||
if (pinned.length > 0 && !pinned.includes(deviceName))
|
||||
clearScreenPins();
|
||||
currentDeviceName = deviceName;
|
||||
BrightnessService.setCurrentDevice(deviceName, true);
|
||||
deviceNameChanged(deviceName);
|
||||
@@ -86,18 +91,19 @@ Item {
|
||||
const pinKey = getScreenPinKey();
|
||||
if (!pinKey || !deviceName)
|
||||
return false;
|
||||
return (CacheData.brightnessDevicePins || {})[pinKey] === deviceName;
|
||||
return QmlUtils.normalizePinList((CacheData.brightnessDevicePins || {})[pinKey]).includes(deviceName);
|
||||
}
|
||||
|
||||
function togglePinForDevice(deviceName) {
|
||||
const pinKey = getScreenPinKey();
|
||||
if (!pinKey || !deviceName)
|
||||
return;
|
||||
CacheData.set("brightnessDevicePins", QmlUtils.togglePinEntry(CacheData.brightnessDevicePins, pinKey, deviceName, Infinity));
|
||||
}
|
||||
|
||||
function clearScreenPins() {
|
||||
const pins = JSON.parse(JSON.stringify(CacheData.brightnessDevicePins || {}));
|
||||
if (pins[pinKey] === deviceName)
|
||||
delete pins[pinKey];
|
||||
else
|
||||
pins[pinKey] = deviceName;
|
||||
delete pins[getScreenPinKey()];
|
||||
CacheData.set("brightnessDevicePins", pins);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,16 +9,18 @@ CcSliderRow {
|
||||
property string deviceName: widgetData.deviceName || ""
|
||||
property string screenName: host?.screenName ?? ""
|
||||
|
||||
readonly property var pinnedDevices: {
|
||||
if (!screenName)
|
||||
return [];
|
||||
const screen = Quickshell.screens.find(s => s.name === screenName);
|
||||
return BrightnessService.pinnedDevices(screen ? SettingsData.getScreenDisplayName(screen) : screenName);
|
||||
}
|
||||
|
||||
property string targetDeviceName: {
|
||||
if (!BrightnessService.brightnessAvailable || !BrightnessService.devices || BrightnessService.devices.length === 0)
|
||||
return "";
|
||||
if (screenName) {
|
||||
const screen = Quickshell.screens.find(s => s.name === screenName);
|
||||
const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName;
|
||||
const pinnedDevice = (CacheData.brightnessDevicePins || {})[pinKey];
|
||||
if (pinnedDevice && BrightnessService.devices.find(dev => dev.name === pinnedDevice))
|
||||
return pinnedDevice;
|
||||
}
|
||||
if (pinnedDevices.length > 0)
|
||||
return pinnedDevices[0];
|
||||
if (deviceName && BrightnessService.devices.find(dev => dev.name === deviceName))
|
||||
return deviceName;
|
||||
const currentDeviceName = BrightnessService.currentDevice;
|
||||
@@ -55,7 +57,7 @@ CcSliderRow {
|
||||
onSliderValueChanged: newValue => {
|
||||
if (!BrightnessService.brightnessAvailable || !targetDeviceName)
|
||||
return;
|
||||
BrightnessService.setBrightness(newValue, targetDeviceName, true);
|
||||
BrightnessService.setGroupBrightness(newValue, pinnedDevices.length > 0 ? pinnedDevices : [targetDeviceName], true);
|
||||
}
|
||||
|
||||
Binding {
|
||||
|
||||
@@ -188,16 +188,13 @@ BasePill {
|
||||
return root.screenName;
|
||||
}
|
||||
|
||||
function getPinnedBrightnessDevice() {
|
||||
const pinKey = getScreenPinKey();
|
||||
if (!pinKey)
|
||||
return "";
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
return pins[pinKey] || "";
|
||||
function getBrightnessTargets() {
|
||||
const pinned = BrightnessService.pinnedDevices(getScreenPinKey());
|
||||
return pinned.length > 0 ? pinned : [BrightnessService.getDefaultDevice()];
|
||||
}
|
||||
|
||||
function getEffectiveBrightnessDevice() {
|
||||
return getPinnedBrightnessDevice() || BrightnessService.getDefaultDevice();
|
||||
return getBrightnessTargets()[0];
|
||||
}
|
||||
|
||||
function handleVolumeWheel(delta) {
|
||||
@@ -267,7 +264,7 @@ BasePill {
|
||||
|
||||
const currentBrightness = BrightnessService.getDeviceBrightness(deviceName);
|
||||
const newBrightness = delta > 0 ? Math.min(100, currentBrightness + step) : Math.max(1, currentBrightness - step);
|
||||
BrightnessService.setBrightness(newBrightness, deviceName);
|
||||
BrightnessService.setGroupBrightness(newBrightness, getBrightnessTargets());
|
||||
}
|
||||
|
||||
function getBrightness() {
|
||||
|
||||
@@ -284,7 +284,7 @@ Item {
|
||||
return;
|
||||
SessionData.suppressOSDTemporarily();
|
||||
const next = Math.max(BrightnessService.brightnessMinimum(root.brightnessDevice), Math.min(root.brightnessMaximum, BrightnessService.brightnessLevel + direction * 5));
|
||||
BrightnessService.setBrightness(next, root.brightnessDevice.id, true);
|
||||
BrightnessService.setGroupBrightness(next, BrightnessService.lastIpcTargets(), true);
|
||||
}
|
||||
|
||||
function connectivityIconName(type) {
|
||||
|
||||
@@ -141,7 +141,7 @@ QtObject {
|
||||
AudioService.setMicVolume(Math.round(clampedRatio * maximum));
|
||||
return;
|
||||
case "brightness":
|
||||
BrightnessService.setBrightness(Math.round(clampedRatio * maximum), BrightnessService.lastIpcDevice, true);
|
||||
BrightnessService.setGroupBrightness(Math.round(clampedRatio * maximum), BrightnessService.lastIpcTargets(), true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ LevelOSD {
|
||||
|
||||
onLevelRequested: level => {
|
||||
displayLevel = level;
|
||||
BrightnessService.setBrightness(level, BrightnessService.lastIpcDevice, true);
|
||||
BrightnessService.setGroupBrightness(level, BrightnessService.lastIpcTargets(), true);
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -6,6 +6,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import "../Common/QmlUtils.js" as QmlUtils
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -21,6 +22,7 @@ Singleton {
|
||||
property int brightnessVersion: 0
|
||||
property string currentDevice: ""
|
||||
property string lastIpcDevice: ""
|
||||
property var lastIpcGroup: []
|
||||
property int brightnessLevel: {
|
||||
brightnessVersion;
|
||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice);
|
||||
@@ -235,6 +237,8 @@ Singleton {
|
||||
if (actualDevice !== lastIpcDevice) {
|
||||
lastIpcDevice = actualDevice;
|
||||
}
|
||||
if (lastIpcGroup.length > 0)
|
||||
lastIpcGroup = [];
|
||||
|
||||
const deviceInfo = getCurrentDeviceInfoByName(actualDevice);
|
||||
const isExponential = deviceInfo?.class !== "software" && SessionData.getBrightnessExponential(actualDevice);
|
||||
@@ -296,6 +300,25 @@ Singleton {
|
||||
});
|
||||
}
|
||||
|
||||
function setGroupBrightness(percentage, deviceIds, suppressOsd) {
|
||||
const lead = getCurrentDeviceInfoByName(deviceIds[0]);
|
||||
const leadMin = brightnessMinimum(lead);
|
||||
const leadMax = brightnessMaximum(lead);
|
||||
const ratio = leadMax > leadMin ? Math.max(0, Math.min(1, (percentage - leadMin) / (leadMax - leadMin))) : 1;
|
||||
|
||||
for (const id of deviceIds.slice(1)) {
|
||||
const info = getCurrentDeviceInfoByName(id);
|
||||
const min = brightnessMinimum(info);
|
||||
setBrightness(Math.round(min + ratio * (brightnessMaximum(info) - min)), id, true);
|
||||
}
|
||||
setBrightness(percentage, deviceIds[0], suppressOsd);
|
||||
lastIpcGroup = deviceIds;
|
||||
}
|
||||
|
||||
function lastIpcTargets() {
|
||||
return lastIpcGroup.length > 1 && lastIpcGroup[0] === lastIpcDevice ? lastIpcGroup : [lastIpcDevice];
|
||||
}
|
||||
|
||||
function setCurrentDevice(deviceName, saveToSession = false) {
|
||||
if (currentDevice === deviceName) {
|
||||
return;
|
||||
@@ -354,33 +377,17 @@ Singleton {
|
||||
return devices.length > 0 ? devices[0].id : "";
|
||||
}
|
||||
|
||||
function getPinnedDeviceForFocusedScreen() {
|
||||
const focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (!focusedScreen)
|
||||
return "";
|
||||
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
const screenKey = SettingsData.getScreenDisplayName(focusedScreen);
|
||||
if (!screenKey)
|
||||
return "";
|
||||
|
||||
const pinnedDevice = pins[screenKey];
|
||||
if (!pinnedDevice)
|
||||
return "";
|
||||
|
||||
const deviceExists = devices.some(d => d.id === pinnedDevice);
|
||||
if (!deviceExists)
|
||||
return "";
|
||||
|
||||
return pinnedDevice;
|
||||
function pinnedDevices(pinKey) {
|
||||
if (!pinKey)
|
||||
return [];
|
||||
const pinned = QmlUtils.normalizePinList((CacheData.brightnessDevicePins || {})[pinKey]);
|
||||
return pinned.filter(id => devices.some(d => d.id === id));
|
||||
}
|
||||
|
||||
function getPreferredDevice() {
|
||||
const pinned = getPinnedDeviceForFocusedScreen();
|
||||
if (pinned)
|
||||
return pinned;
|
||||
|
||||
return getDefaultDevice();
|
||||
function getPreferredDevices() {
|
||||
const focusedScreen = CompositorService.getFocusedScreen();
|
||||
const pinned = focusedScreen ? pinnedDevices(SettingsData.getScreenDisplayName(focusedScreen)) : [];
|
||||
return pinned.length > 0 ? pinned : [getDefaultDevice()];
|
||||
}
|
||||
|
||||
function getCurrentDeviceInfo() {
|
||||
@@ -595,7 +602,8 @@ Singleton {
|
||||
if (isNaN(value))
|
||||
return "Invalid brightness value: " + percentage;
|
||||
|
||||
const actualDevice = device || root.getPreferredDevice();
|
||||
const targets = device ? [device] : root.getPreferredDevices();
|
||||
const actualDevice = targets[0];
|
||||
|
||||
if (actualDevice && !root.devices.some(d => d.id === actualDevice))
|
||||
return "Device not found: " + actualDevice;
|
||||
@@ -608,7 +616,7 @@ Singleton {
|
||||
if (actualDevice && actualDevice !== root.currentDevice)
|
||||
root.setCurrentDevice(actualDevice, false);
|
||||
|
||||
root.setBrightness(clampedValue, actualDevice);
|
||||
root.setGroupBrightness(clampedValue, targets);
|
||||
|
||||
return actualDevice ? "Brightness set to " + clampedValue + "% on " + actualDevice : "Brightness set to " + clampedValue + "%";
|
||||
}
|
||||
@@ -617,7 +625,8 @@ Singleton {
|
||||
if (!root.brightnessAvailable)
|
||||
return "Brightness control not available";
|
||||
|
||||
const actualDevice = device || root.getPreferredDevice();
|
||||
const targets = device ? [device] : root.getPreferredDevices();
|
||||
const actualDevice = targets[0];
|
||||
|
||||
if (actualDevice && !root.devices.some(d => d.id === actualDevice))
|
||||
return "Device not found: " + actualDevice;
|
||||
@@ -633,7 +642,7 @@ Singleton {
|
||||
const maxValue = root.brightnessMaximum(deviceInfo);
|
||||
const newBrightness = Math.min(maxValue, currentBrightness + stepValue);
|
||||
|
||||
root.setBrightness(newBrightness, actualDevice);
|
||||
root.setGroupBrightness(newBrightness, targets);
|
||||
|
||||
return "Brightness increased by " + stepValue + "%" + (device ? " on " + actualDevice : "");
|
||||
}
|
||||
@@ -642,7 +651,8 @@ Singleton {
|
||||
if (!root.brightnessAvailable)
|
||||
return "Brightness control not available";
|
||||
|
||||
const actualDevice = device || root.getPreferredDevice();
|
||||
const targets = device ? [device] : root.getPreferredDevices();
|
||||
const actualDevice = targets[0];
|
||||
|
||||
if (actualDevice && !root.devices.some(d => d.id === actualDevice))
|
||||
return "Device not found: " + actualDevice;
|
||||
@@ -659,7 +669,7 @@ Singleton {
|
||||
|
||||
const newBrightness = Math.max(minValue, currentBrightness - stepValue);
|
||||
|
||||
root.setBrightness(newBrightness, actualDevice);
|
||||
root.setGroupBrightness(newBrightness, targets);
|
||||
|
||||
return "Brightness decreased by " + stepValue + "%" + (device ? " on " + actualDevice : "");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
import { loadScript } from "./qml-script.mjs";
|
||||
|
||||
const SERVICE = "../Services/BrightnessService.qml";
|
||||
const plain = value => JSON.parse(JSON.stringify(value));
|
||||
|
||||
function functionSource(file, name) {
|
||||
const source = readFileSync(new URL(file, import.meta.url), "utf8");
|
||||
const found = source.match(new RegExp(`^ function ${name}\\([^)]*\\) \\{\\n[\\s\\S]*?^ \\}`, "m"));
|
||||
if (!found)
|
||||
throw new Error(`${name} not found in ${file}`);
|
||||
return found[0];
|
||||
}
|
||||
|
||||
function service(globals) {
|
||||
const context = vm.createContext(Object.assign({
|
||||
QmlUtils: loadScript(new URL("../Common/QmlUtils.js", import.meta.url)),
|
||||
lastIpcGroup: [],
|
||||
lastIpcDevice: ""
|
||||
}, globals));
|
||||
for (const name of ["setGroupBrightness", "lastIpcTargets", "pinnedDevices"])
|
||||
vm.runInContext(functionSource(SERVICE, name), context);
|
||||
return context;
|
||||
}
|
||||
|
||||
function recordingService(ranges) {
|
||||
const calls = [];
|
||||
const context = service({
|
||||
getCurrentDeviceInfoByName: id => ranges[id],
|
||||
brightnessMinimum: info => info?.min ?? 1,
|
||||
brightnessMaximum: info => info?.max ?? 100,
|
||||
setBrightness(value, id, suppressOsd) {
|
||||
calls.push([value, id, suppressOsd]);
|
||||
context.lastIpcDevice = id;
|
||||
context.lastIpcGroup = [];
|
||||
}
|
||||
});
|
||||
return { context, calls };
|
||||
}
|
||||
|
||||
test("a group maps the lead's position onto each member's range and sets the lead last", () => {
|
||||
const { context, calls } = recordingService({
|
||||
"backlight:a": { min: 1, max: 100 },
|
||||
"ddc:i2c-11": { min: 1, max: 100 },
|
||||
"ddc:i2c-12": { min: 0, max: 50 }
|
||||
});
|
||||
|
||||
context.setGroupBrightness(51, ["ddc:i2c-11", "backlight:a", "ddc:i2c-12"], false);
|
||||
|
||||
assert.deepEqual(calls, [[51, "backlight:a", true], [25, "ddc:i2c-12", true], [51, "ddc:i2c-11", false]]);
|
||||
assert.deepEqual(plain(context.lastIpcTargets()), ["ddc:i2c-11", "backlight:a", "ddc:i2c-12"]);
|
||||
});
|
||||
|
||||
test("a single or default target is set once and never becomes a group", () => {
|
||||
const { context, calls } = recordingService({});
|
||||
|
||||
context.setGroupBrightness(40, [""], false);
|
||||
|
||||
assert.deepEqual(calls, [[40, "", false]]);
|
||||
assert.deepEqual(plain(context.lastIpcTargets()), [""]);
|
||||
});
|
||||
|
||||
test("follow-up controls drop the group once another device became the last target", () => {
|
||||
const context = service({ lastIpcGroup: ["ddc:i2c-11", "ddc:i2c-12"], lastIpcDevice: "backlight:a" });
|
||||
|
||||
assert.deepEqual(plain(context.lastIpcTargets()), ["backlight:a"]);
|
||||
});
|
||||
|
||||
test("pins read legacy single-device strings and skip unplugged devices", () => {
|
||||
const devices = [{ id: "backlight:a" }, { id: "ddc:i2c-11" }];
|
||||
const context = service({
|
||||
devices,
|
||||
CacheData: { brightnessDevicePins: { "eDP-1": "backlight:a", "DP-1": ["ddc:i2c-12", "ddc:i2c-11", "backlight:a"] } }
|
||||
});
|
||||
|
||||
assert.deepEqual(plain(context.pinnedDevices("eDP-1")), ["backlight:a"]);
|
||||
assert.deepEqual(plain(context.pinnedDevices("DP-1")), ["ddc:i2c-11", "backlight:a"]);
|
||||
assert.deepEqual(plain(context.pinnedDevices("")), []);
|
||||
});
|
||||
Reference in new issue
Block a user