Selection: fix untappable chip and the chip that never cleared
Two bugs from the first on-device run. The card sized itself from a layout it also centered, which resolved to zero, so mask claimed a zero-size input region: the chip painted but no tap landed. Card now sizes from the layout's implicit size and the background is a plain Item child, not layout-managed. wl-paste --watch replays the existing selection on start, so every watcher restart (every unlock) resurrected a stale selection that could never be cleared because the primary buffer never changed again. The first emission after a start now only sets the baseline, and a dismissed selection is remembered so a re-read cannot revive it.
This commit is contained in:
parent
808f06358e
commit
5a643d7059
2 changed files with 64 additions and 23 deletions
|
|
@ -92,12 +92,22 @@ Scope {
|
|||
return Math.min(win.hintY + 16, win.height - card.height - win.pad);
|
||||
}
|
||||
|
||||
// `card` takes its size from the layout's implicit size, and the
|
||||
// layout takes its size from its own — no path where a child's size
|
||||
// depends on the parent's. The earlier version centered the layout
|
||||
// in the card while the card sized from the layout, which resolved
|
||||
// to zero and silently made `mask` a zero-size input region: the
|
||||
// chip painted but nothing could be tapped. The background stays a
|
||||
// plain Item child, because anchors on a layout-managed item are
|
||||
// undefined behavior.
|
||||
Item {
|
||||
id: card
|
||||
x: win.targetX
|
||||
y: win.targetY
|
||||
width: content.implicitWidth
|
||||
height: content.implicitHeight
|
||||
implicitWidth: content.implicitWidth
|
||||
implicitHeight: content.implicitHeight
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
// Position eases so a re-selection slides rather than teleports,
|
||||
// but only once mapped — animating from 0,0 on first show reads
|
||||
|
|
@ -113,6 +123,7 @@ Scope {
|
|||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
radius: Appearance.rounding.normal
|
||||
color: Appearance.colors.colLayer1
|
||||
border.width: 1
|
||||
|
|
@ -121,33 +132,36 @@ Scope {
|
|||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.centerIn: parent
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
spacing: 0
|
||||
|
||||
// ── collapsed: the chip ──────────────────────────────
|
||||
SelectionChip {
|
||||
visible: !scope.expanded
|
||||
charCount: Selection.text.length
|
||||
onTapped: {
|
||||
DeviceEvidence.touched("selection-menu");
|
||||
Haptics.trigger("button-pressed");
|
||||
scope.expanded = true;
|
||||
}
|
||||
onDismissed: {
|
||||
DeviceEvidence.touched("selection-menu");
|
||||
Selection.dismiss();
|
||||
}
|
||||
// ── collapsed: the chip ──────────────────────────────────
|
||||
SelectionChip {
|
||||
visible: !scope.expanded
|
||||
charCount: Selection.text.length
|
||||
onTapped: {
|
||||
DeviceEvidence.touched("selection-menu");
|
||||
Haptics.trigger("button-pressed");
|
||||
scope.expanded = true;
|
||||
}
|
||||
onDismissed: {
|
||||
DeviceEvidence.touched("selection-menu");
|
||||
Selection.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
// ── expanded: the actions ────────────────────────────
|
||||
SelectionActions {
|
||||
visible: scope.expanded
|
||||
onActed: {
|
||||
DeviceEvidence.touched("selection-menu");
|
||||
Selection.dismiss();
|
||||
}
|
||||
// ── expanded: the actions ────────────────────────────────
|
||||
SelectionActions {
|
||||
visible: scope.expanded
|
||||
onActed: {
|
||||
DeviceEvidence.touched("selection-menu");
|
||||
Selection.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ Singleton {
|
|||
function _reconcile() {
|
||||
if (root._permitted) {
|
||||
if (!watcher.running) {
|
||||
root._primed = false;
|
||||
watcher.running = true;
|
||||
console.log("[Selection] watching primary selection");
|
||||
}
|
||||
|
|
@ -111,12 +112,26 @@ Singleton {
|
|||
|
||||
// Called by the menu when the user dismisses it or an action consumes the
|
||||
// selection. Does not touch the compositor's selection — only our view of it.
|
||||
//
|
||||
// The dismissed text is remembered: the compositor's selection is unchanged
|
||||
// by dismissing, so any later re-read of it (a watcher restart) would
|
||||
// otherwise resurrect the chip the user just closed.
|
||||
function dismiss() {
|
||||
settleTimer.running = false;
|
||||
root._dismissedText = root.text;
|
||||
root._clear();
|
||||
}
|
||||
|
||||
property string _pending: ""
|
||||
property string _dismissedText: ""
|
||||
|
||||
// wl-paste --watch fires ONCE IMMEDIATELY with whatever the selection
|
||||
// already holds, before any new user action. That replay is not a fresh
|
||||
// selection: it resurrected an hour-old selection every time the watcher
|
||||
// restarted (i.e. on every unlock), leaving a chip that could never be
|
||||
// cleared because the primary buffer never changed again. The first
|
||||
// emission after a start only establishes the baseline.
|
||||
property bool _primed: false
|
||||
|
||||
Component.onCompleted: root._reconcile()
|
||||
|
||||
|
|
@ -147,6 +162,16 @@ Singleton {
|
|||
function _onSelection(raw) {
|
||||
if (!root._permitted) return;
|
||||
const t = String(raw ?? "");
|
||||
// The startup replay: record it as already-seen and show nothing.
|
||||
if (!root._primed) {
|
||||
root._primed = true;
|
||||
root._dismissedText = t;
|
||||
console.log(`[Selection] baseline ${t.trim().length} chars (not shown)`);
|
||||
return;
|
||||
}
|
||||
// A re-read of something the user already dismissed is not a new
|
||||
// selection. Only an actual change re-opens the chip.
|
||||
if (t.length > 0 && t === root._dismissedText) return;
|
||||
if (t.trim().length < root._minChars) {
|
||||
// A cleared or trivial selection retires the menu rather than
|
||||
// leaving a stale one anchored over nothing.
|
||||
|
|
@ -154,6 +179,8 @@ Singleton {
|
|||
root._clear();
|
||||
return;
|
||||
}
|
||||
// A genuinely new selection clears the dismissal memory.
|
||||
root._dismissedText = "";
|
||||
root._pending = t;
|
||||
// Restart the settle window on every growth tick.
|
||||
settleTimer.restart();
|
||||
|
|
|
|||
Loading…
Reference in a new issue