179 lines
5.9 KiB
QML
179 lines
5.9 KiB
QML
// The GNOME 42 screenshot UI.
|
|
//
|
|
// Print freezes the screen, this overlay paints that frozen frame full-bleed
|
|
// and puts the picker on top of it. Because the frame is a still, nothing under
|
|
// the overlay animates while the user aims, and the overlay can never end up in
|
|
// its own screenshot.
|
|
//
|
|
// The grab happens in services/Capture.qml *before* ShellState flips to
|
|
// "capture", so by the time this window maps the backdrop is already on disk.
|
|
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
PanelWindow {
|
|
id: win
|
|
|
|
visible: ShellState.captureOpen
|
|
color: "transparent"
|
|
|
|
anchors {
|
|
top: true
|
|
bottom: true
|
|
left: true
|
|
right: true
|
|
}
|
|
|
|
// Fullscreen overlays must not reserve space, or every window on the
|
|
// workspace gets resized as the picker opens and closes.
|
|
exclusiveZone: 0
|
|
|
|
WlrLayershell.namespace: "qs-capture" // matched by a layerrule in hypr/rules.lua
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
|
|
onVisibleChanged: {
|
|
if (!win.visible)
|
|
return;
|
|
// Hyprland reports window geometry in layout-global coordinates; the
|
|
// picker works in window-local ones. On a single monitor this is 0,0.
|
|
Capture.originX = win.screen ? win.screen.x : 0;
|
|
Capture.originY = win.screen ? win.screen.y : 0;
|
|
keys.forceActiveFocus();
|
|
}
|
|
|
|
// Fire whatever the current mode has selected.
|
|
function capture(): void {
|
|
if (Capture.mode === "screen") {
|
|
Capture.commit(null);
|
|
return;
|
|
}
|
|
if (Capture.mode === "window") {
|
|
const r = picker.selectedRect;
|
|
if (r)
|
|
Capture.commit(r);
|
|
return;
|
|
}
|
|
if (!selection.hasSelection)
|
|
return;
|
|
const s = selection.selection;
|
|
Capture.commit(Qt.rect(s.x + Capture.originX, s.y + Capture.originY, s.width, s.height));
|
|
}
|
|
|
|
function cycleMode(step: int): void {
|
|
const order = ["screen", "window", "selection"];
|
|
const i = order.indexOf(Capture.mode);
|
|
Capture.mode = order[(i + step + order.length) % order.length];
|
|
}
|
|
|
|
FocusScope {
|
|
id: keys
|
|
anchors.fill: parent
|
|
focus: true
|
|
|
|
Keys.onPressed: event => {
|
|
switch (event.key) {
|
|
case Qt.Key_Escape:
|
|
Capture.close();
|
|
break;
|
|
case Qt.Key_Return:
|
|
case Qt.Key_Enter:
|
|
case Qt.Key_Space:
|
|
win.capture();
|
|
break;
|
|
case Qt.Key_Left:
|
|
win.cycleMode(-1);
|
|
break;
|
|
case Qt.Key_Right:
|
|
win.cycleMode(1);
|
|
break;
|
|
case Qt.Key_1:
|
|
Capture.mode = "screen";
|
|
break;
|
|
case Qt.Key_2:
|
|
Capture.mode = "window";
|
|
break;
|
|
case Qt.Key_3:
|
|
Capture.mode = "selection";
|
|
break;
|
|
case Qt.Key_P:
|
|
Capture.showPointer = !Capture.showPointer;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
event.accepted = true;
|
|
}
|
|
|
|
// Painted under the frozen frame so a failed grab (no wlr-screencopy)
|
|
// still gives the picker something opaque to sit on.
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Theme.bgDark
|
|
border.width: 0
|
|
}
|
|
|
|
// The frozen screen. grim -o pins the grab to this output, so filling
|
|
// the window is an exact 1:1 mapping; the source is 4500x3000 device
|
|
// pixels drawn into 3000x2000 logical ones, i.e. full sharpness.
|
|
Image {
|
|
anchors.fill: parent
|
|
source: Capture.freezeUrl
|
|
fillMode: Image.Stretch
|
|
// Unique filename per grab plus cache:false — otherwise QML would
|
|
// happily repaint a previous session's frame.
|
|
cache: false
|
|
// Synchronous so the overlay never flashes black on the way in; a
|
|
// raw PPM decode is a parse, not a decompress.
|
|
asynchronous: false
|
|
visible: status === Image.Ready
|
|
}
|
|
|
|
// ── Screen mode ─────────────────────────────────────────────────────
|
|
// Nothing to pick, so just outline the output the way GNOME does.
|
|
Frame {
|
|
anchors.fill: parent
|
|
visible: Capture.mode === "screen"
|
|
strokeWidth: 3
|
|
strokeColor: Theme.accent
|
|
}
|
|
|
|
// ── Window mode ─────────────────────────────────────────────────────
|
|
WindowPicker {
|
|
id: picker
|
|
anchors.fill: parent
|
|
visible: Capture.mode === "window"
|
|
originX: Capture.originX
|
|
originY: Capture.originY
|
|
onPicked: win.capture()
|
|
}
|
|
|
|
// ── Selection mode ──────────────────────────────────────────────────
|
|
SelectionLayer {
|
|
id: selection
|
|
anchors.fill: parent
|
|
visible: Capture.mode === "selection"
|
|
onCommitted: win.capture()
|
|
}
|
|
|
|
CaptureBar {
|
|
anchors {
|
|
horizontalCenter: parent.horizontalCenter
|
|
bottom: parent.bottom
|
|
bottomMargin: 56
|
|
}
|
|
captureEnabled: {
|
|
if (Capture.mode === "window")
|
|
return picker.selectedRect !== null;
|
|
if (Capture.mode === "selection")
|
|
return selection.hasSelection;
|
|
return true;
|
|
}
|
|
onCaptureRequested: win.capture()
|
|
}
|
|
}
|
|
}
|