diff options
Diffstat (limited to 'window-switcher/Switcher.qml')
| -rw-r--r-- | window-switcher/Switcher.qml | 42 |
1 files changed, 39 insertions, 3 deletions
diff --git a/window-switcher/Switcher.qml b/window-switcher/Switcher.qml index e6eb792..1eebfa8 100644 --- a/window-switcher/Switcher.qml +++ b/window-switcher/Switcher.qml @@ -81,11 +81,16 @@ Scope { // Keys reach a focused item, not a window: Keys.onEscapePressed on // the PanelWindow above never fires. This item is what listens. - // Escape is the only key: the expose is a pointer feature. Item { anchors.fill: parent focus: true Keys.onEscapePressed: root.close() + Keys.onReturnPressed: if (scatter.selected) root.focusAfterClose(scatter.selected) + Keys.onEnterPressed: if (scatter.selected) root.focusAfterClose(scatter.selected) + Keys.onLeftPressed: scatter.move(-1, 0) + Keys.onRightPressed: scatter.move(1, 0) + Keys.onUpPressed: scatter.move(0, -1) + Keys.onDownPressed: scatter.move(0, 1) Rectangle { anchors.fill: parent @@ -127,12 +132,14 @@ Scope { // Real sizes come from the raw hyprctl object. A window // that has none yet stands in at 1280x800 until it does. + // The bottom 28px are kept free for the lowest card's + // app name, which hangs below its preview. readonly property var rects: Expose.layout( Windows.windows.map(t => { const o = t.lastIpcObject; const s = o && o.size; return s ? { w: s[0], h: s[1] } : { w: 1280, h: 800 }; - }), width, height) + }), width, height - 28) // ponytail: a fade, not a flight from each window's real // position; add the flight if the fade feels abrupt. @@ -140,8 +147,30 @@ Scope { Component.onCompleted: opacity = 1 Behavior on opacity { NumberAnimation { duration: 150 } } + // The keyboard selection, held as the window rather + // than an index, so a close that shifts the list + // does not move it onto a neighbour. Hovering a card + // selects it too, so pointer and keys never show two. + property var selected: null + readonly property int selectedIndex: Windows.windows.indexOf(selected) + + function move(dx, dy) { + const i = Expose.step(rects, selectedIndex, dx, dy); + if (i >= 0) selected = Windows.windows[i]; + } + + // Cards glide to their new rects when the scatter + // changes, but not while it fades in, or every card + // would fly in from the corner on open. + property bool settled: false + onOpacityChanged: if (opacity === 1) settled = true + + // A plain array model would rebuild every card on any + // change, leaving nothing to animate. ScriptModel diffs + // by identity, so a closed window removes one delegate + // and the rest keep theirs. Repeater { - model: Windows.windows + model: ScriptModel { values: Windows.windows } WindowCard { required property int index @@ -156,6 +185,13 @@ Scope { width: rect.w height: rect.h + Behavior on x { enabled: scatter.settled; NumberAnimation { duration: 250; easing.type: Easing.OutCubic } } + Behavior on y { enabled: scatter.settled; NumberAnimation { duration: 250; easing.type: Easing.OutCubic } } + Behavior on width { enabled: scatter.settled; NumberAnimation { duration: 250; easing.type: Easing.OutCubic } } + Behavior on height { enabled: scatter.settled; NumberAnimation { duration: 250; easing.type: Easing.OutCubic } } + + selected: scatter.selected === modelData + onHoveredChanged: if (hovered) scatter.selected = modelData onActivated: root.focusAfterClose(modelData) // Closing is a side errand, so the overlay // stays open: a dismissal would mean reopening |
