diff --git a/shell/plugins/panels/monitor/Model.js b/shell/plugins/panels/monitor/Model.js index b37cf772..d67e25b6 100644 --- a/shell/plugins/panels/monitor/Model.js +++ b/shell/plugins/panels/monitor/Model.js @@ -33,6 +33,55 @@ function cleanScale(scale, width, height) { return normalizeScale(scaleUnits / 120) } +function matchingScaleIndex(scales, currentScale, width, height) { + var current = Number(currentScale) + if (!Array.isArray(scales) || !isFinite(current)) return -1 + + var bestIndex = -1 + var bestDistance = Infinity + var normalizedCurrent = normalizeScale(current) + for (var i = 0; i < scales.length; i++) { + if (cleanScale(scales[i], width, height) !== normalizedCurrent) continue + + var distance = Math.abs(Number(scales[i]) - current) + if (distance < bestDistance) { + bestIndex = i + bestDistance = distance + } + } + return bestIndex +} + +function availableScales(scales, width, height) { + if (!Array.isArray(scales) || Number(width) <= 0 || Number(height) <= 0) return scales || [] + + var byEffectiveScale = {} + for (var i = 0; i < scales.length; i++) { + var requested = Number(scales[i]) + var effective = Number(cleanScale(requested, width, height)) + + // Clean scales round upward. If the mode cannot reach the requested + // scale, cleanScale caps at its largest valid value and this preset is + // not actually available. + if (!isFinite(requested) || !isFinite(effective) || effective < requested) continue + + var key = normalizeScale(effective) + var existing = byEffectiveScale[key] + if (!existing || Math.abs(requested - effective) < existing.distance) { + byEffectiveScale[key] = { + value: String(scales[i]), + index: i, + distance: Math.abs(requested - effective) + } + } + } + + return Object.keys(byEffectiveScale) + .map(function(key) { return byEffectiveScale[key] }) + .sort(function(a, b) { return a.index - b.index }) + .map(function(candidate) { return candidate.value }) +} + function brightnessName(percent) { var p = Math.round(percent) if (p >= 95) return "Sun blast" @@ -70,6 +119,8 @@ if (typeof module !== "undefined") { clampBrightness: clampBrightness, normalizeScale: normalizeScale, cleanScale: cleanScale, + matchingScaleIndex: matchingScaleIndex, + availableScales: availableScales, brightnessName: brightnessName, parseDisplays: parseDisplays } diff --git a/shell/plugins/panels/monitor/Panel.qml b/shell/plugins/panels/monitor/Panel.qml index a7ac51ef..34cbd3bb 100644 --- a/shell/plugins/panels/monitor/Panel.qml +++ b/shell/plugins/panels/monitor/Panel.qml @@ -38,7 +38,15 @@ Panel { // j/k walks each row. // Mouse hover on a target updates root state via the components' `hovered` // signal so keyboard cursor and pointer share one highlight. - readonly property var scaleValues: ["1", "1.25", "1.6", "2", "3", "4"] + readonly property var scalePresets: ["1", "1.25", "1.6", "2", "3", "4"] + readonly property var scaleValues: { + for (var i = 0; i < displays.length; i++) { + var display = displays[i] + if (display && display.focused) + return Model.availableScales(scalePresets, display.width, display.height) + } + return scalePresets + } property string focusSection: "scale" property int selectedIndex: 0 property bool cursorActive: false @@ -246,13 +254,13 @@ Panel { return Model.normalizeScale(scale) } - function effectiveScale(scale) { + function activeScaleIndex() { for (var i = 0; i < displays.length; i++) { var display = displays[i] if (display && display.focused) - return Model.cleanScale(scale, display.width, display.height) + return Model.matchingScaleIndex(scaleValues, monitorScale, display.width, display.height) } - return normalizeScale(scale) + return -1 } // Playful mood-name for a given brightness percent. Bands intentionally @@ -342,6 +350,7 @@ Panel { onBrightnessAvailableChanged: clampCursor() onDisplaysChanged: clampCursor() + onScaleValuesChanged: clampCursor() onVisibleSectionsChanged: clampCursor() // Only poll while the panel is open; the bar glyph tracks monitor count via @@ -780,7 +789,7 @@ Panel { verticalPadding: Style.spacing.controlPaddingY bordered: true - active: root.normalizeScale(root.monitorScale) === root.effectiveScale(scaleValue) + active: root.activeScaleIndex() === scaleIndex hasCursor: root.cursorActive && root.focusSection === "scale" && root.selectedIndex === scaleIndex onClicked: root.setScale(scaleValue) diff --git a/test/shell.d/monitor-test.sh b/test/shell.d/monitor-test.sh index 2fddc9a5..1917c102 100644 --- a/test/shell.d/monitor-test.sh +++ b/test/shell.d/monitor-test.sh @@ -18,6 +18,31 @@ assertEqual(monitor.cleanScale(3, 1280, 800), '3.2', 'monitor matches clean VM s assertEqual(monitor.cleanScale(1.25, 1280, 800), '1.25', 'monitor preserves an already clean scale') assertEqual(monitor.cleanScale(1.25, 6016, 3384), '1.33', 'monitor matches clean physical display scale') assertEqual(monitor.cleanScale(1.6, 0, 800), '', 'monitor rejects a missing display mode') +assertEqual( + monitor.matchingScaleIndex(['1', '1.25', '1.6', '2', '3', '4'], 3.2, 1280, 800), + 4, + 'monitor selects requested 3x for effective VM scale' +) +assertEqual( + monitor.matchingScaleIndex(['1', '1.25', '1.6', '2', '3', '4'], 4, 4, 4), + 5, + 'monitor selects only the closest preset when clean scales collide' +) +assertDeepEqual( + monitor.availableScales(['1', '1.25', '1.6', '2', '3', '4'], 1280, 800), + ['1', '1.25', '1.6', '2', '3', '4'], + 'monitor keeps presets that produce unique reachable VM scales' +) +assertDeepEqual( + monitor.availableScales(['1', '1.25', '1.6', '2', '3', '4'], 5968, 3230), + ['1', '2'], + 'monitor hides presets the current mode cannot reach' +) +assertDeepEqual( + monitor.availableScales(['1', '1.25', '1.6', '2', '3', '4'], 0, 0), + ['1', '1.25', '1.6', '2', '3', '4'], + 'monitor keeps presets until display dimensions are known' +) assertEqual(monitor.brightnessName(96), 'Sun blast', 'monitor names very bright displays') assertEqual(monitor.brightnessName(12), 'Candlelit', 'monitor names dim displays')