Files
Gabriel Brown d96863b687 Convert British spellings to American across the repo
colour -> color, behaviour -> behavior, centre -> center, favourite ->
favorite, and about twenty other pairs, applied consistently across
comments, docs, error/UI copy, and a handful of QML identifiers that
used the British spelling as their actual name: SystemSettings'
serialiseValue/serialiseTable/normaliseGradient, Displays'
normaliseModes, Wallpaper's normalisePolicy, SettingsBackup's
serialiseHomeState, DateTime's ntpSynchronised property, Clipboard's
_normalise helper, and ShortcutCapture's cancelled signal (with its
onCancelled handler in ShortcutsPage.qml). Every call site and the two
tests that assert on the literal source text (settings-ownership and
settings-backup-live contracts) were updated in lockstep.

Left untouched: config/dot/espanso/match/packages/misspell-en/ is a
vendored third-party autocorrect dictionary -- its entries are typo
corrections, not our prose, and rewriting them would fight the
package's own purpose (and any future re-sync from upstream).

The already-American `favorites` property (Home page pinned
accessories) was never actually misspelled -- only nearby comments and
error strings said "favourites" -- so no data migration was needed
there.

Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
2026-08-19 08:07:55 -04:00

279 lines
9.1 KiB
QML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Selection mode: a GNOME-style rubber band over the frozen screen.
//
// Behaviors copied from GNOME 42: the overlay opens with a sensible centered
// selection already in place (so the shutter is immediately useful), dragging
// inside moves it, dragging a corner resizes from the opposite corner, and
// dragging anywhere else starts a fresh band.
//
// A single MouseArea owns all of it. Nested MouseAreas for the handles would
// fight the move/create area for the press, and the hit priorities are much
// easier to reason about in one place.
import QtQuick
import qs.config
Item {
id: root
// Anything smaller than this on release is treated as a stray click and the
// previous selection is restored.
readonly property real minSize: 12
// Pointer distance from a corner that counts as grabbing its handle.
readonly property real handleGrab: 24
readonly property real handleSize: 14
property real selX: 0
property real selY: 0
property real selW: 0
property real selH: 0
readonly property bool hasSelection: root.selW >= root.minSize && root.selH >= root.minSize
readonly property rect selection: Qt.rect(root.selX, root.selY, root.selW, root.selH)
// Double-clicking inside the band captures, like confirming a crop.
signal committed
// "none" | "new" | "move" | "nw" | "ne" | "sw" | "se"
property string _grab: "none"
property real _anchorX: 0
property real _anchorY: 0
property real _pressX: 0
property real _pressY: 0
property real _origX: 0
property real _origY: 0
property rect _prev: Qt.rect(0, 0, 0, 0)
onWidthChanged: root._resetIfEmpty()
onHeightChanged: root._resetIfEmpty()
Component.onCompleted: root._resetIfEmpty()
function _resetIfEmpty(): void {
if (root.selW === 0 && root.selH === 0)
root.resetSelection();
}
// Centered, roughly the middle half of the screen.
function resetSelection(): void {
if (root.width <= 0 || root.height <= 0)
return;
root.selW = Math.round(root.width * 0.55);
root.selH = Math.round(root.height * 0.55);
root.selX = Math.round((root.width - root.selW) / 2);
root.selY = Math.round((root.height - root.selH) / 2);
}
function _clampX(v: real): real {
return Math.max(0, Math.min(root.width, v));
}
function _clampY(v: real): real {
return Math.max(0, Math.min(root.height, v));
}
function _near(a: real, b: real): bool {
return Math.abs(a - b) <= root.handleGrab;
}
function _hitTest(px: real, py: real): string {
const x2 = root.selX + root.selW;
const y2 = root.selY + root.selH;
if (root.hasSelection) {
if (root._near(px, root.selX) && root._near(py, root.selY))
return "nw";
if (root._near(px, x2) && root._near(py, root.selY))
return "ne";
if (root._near(px, root.selX) && root._near(py, y2))
return "sw";
if (root._near(px, x2) && root._near(py, y2))
return "se";
if (px > root.selX && px < x2 && py > root.selY && py < y2)
return "move";
}
return "new";
}
function _cursorFor(m: string): int {
switch (m) {
case "nw":
case "se":
return Qt.SizeFDiagCursor;
case "ne":
case "sw":
return Qt.SizeBDiagCursor;
case "move":
return Qt.SizeAllCursor;
default:
return Qt.CrossCursor;
}
}
// Rebuild the band from a fixed anchor corner and the live pointer.
function _dragTo(px: real, py: real): void {
const cx = root._clampX(px);
const cy = root._clampY(py);
root.selX = Math.min(root._anchorX, cx);
root.selY = Math.min(root._anchorY, cy);
root.selW = Math.abs(cx - root._anchorX);
root.selH = Math.abs(cy - root._anchorY);
}
Spotlight {
anchors.fill: parent
holeVisible: root.hasSelection
hole: root.selection
}
Frame {
x: root.selX
y: root.selY
width: root.selW
height: root.selH
visible: root.hasSelection
strokeWidth: 2
strokeColor: Theme.accent
}
// ── Corner handles ──────────────────────────────────────────────────────
Repeater {
model: [
{
gx: 0,
gy: 0
},
{
gx: 1,
gy: 0
},
{
gx: 0,
gy: 1
},
{
gx: 1,
gy: 1
}
]
Rectangle {
required property var modelData
visible: root.hasSelection
width: root.handleSize
height: root.handleSize
radius: width / 2
border.width: 0
color: Theme.accent
x: root.selX + modelData.gx * root.selW - width / 2
y: root.selY + modelData.gy * root.selH - height / 2
// A dark core keeps the handle readable on top of a light photo.
Rectangle {
anchors.centerIn: parent
width: parent.width - 6
height: width
radius: width / 2
border.width: 0
color: Theme.bgDark
}
}
}
// ── Live size readout ───────────────────────────────────────────────────
Rectangle {
id: readout
visible: root.hasSelection
implicitWidth: readoutText.implicitWidth + 24
implicitHeight: 34
radius: Theme.pillRadius
border.width: 0
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
x: Math.max(0, Math.min(root.width - width, root.selX + (root.selW - width) / 2))
// Inside the band when there is room, otherwise tucked just above it.
y: root.selH > 110 ? root.selY + (root.selH - height) / 2 : Math.max(0, root.selY - height - 10)
Text {
id: readoutText
anchors.centerIn: parent
text: Math.round(root.selW) + " × " + Math.round(root.selH)
color: Theme.fg
// Tabular figures so the readout doesn't twitch while dragging.
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSize
}
}
MouseArea {
id: area
anchors.fill: parent
hoverEnabled: true
cursorShape: root._cursorFor(root._grab !== "none" ? root._grab : root._hitTest(area.mouseX, area.mouseY))
onPressed: event => {
root._prev = root.selection;
root._grab = root._hitTest(event.x, event.y);
root._pressX = event.x;
root._pressY = event.y;
root._origX = root.selX;
root._origY = root.selY;
const x2 = root.selX + root.selW;
const y2 = root.selY + root.selH;
switch (root._grab) {
case "nw":
root._anchorX = x2;
root._anchorY = y2;
break;
case "ne":
root._anchorX = root.selX;
root._anchorY = y2;
break;
case "sw":
root._anchorX = x2;
root._anchorY = root.selY;
break;
case "se":
root._anchorX = root.selX;
root._anchorY = root.selY;
break;
case "new":
root._anchorX = root._clampX(event.x);
root._anchorY = root._clampY(event.y);
root.selX = root._anchorX;
root.selY = root._anchorY;
root.selW = 0;
root.selH = 0;
break;
}
}
onPositionChanged: event => {
if (root._grab === "none")
return;
if (root._grab === "move") {
root.selX = Math.max(0, Math.min(root.width - root.selW, root._origX + (event.x - root._pressX)));
root.selY = Math.max(0, Math.min(root.height - root.selH, root._origY + (event.y - root._pressY)));
} else {
root._dragTo(event.x, event.y);
}
}
onReleased: {
// A plain click outside the band would otherwise wipe the selection
// down to nothing; treat it as "no change" instead.
if (root._grab !== "move" && !root.hasSelection) {
root.selX = root._prev.x;
root.selY = root._prev.y;
root.selW = root._prev.width;
root.selH = root._prev.height;
}
root._grab = "none";
}
onDoubleClicked: event => {
if (root.hasSelection && event.x > root.selX && event.x < root.selX + root.selW && event.y > root.selY && event.y < root.selY + root.selH)
root.committed();
}
}
}