feat: screenshot tool

This commit is contained in:
kossLAN 2025-06-11 18:10:21 -04:00
parent de23a67917
commit 6f39dae2ea
Signed by: kossLAN
SSH key fingerprint: SHA256:bdV0x+wdQHGJ6LgmstH3KV8OpWY+OOFmJcPcB0wQPV8
8 changed files with 343 additions and 17 deletions

View file

@ -11,7 +11,7 @@ Singleton {
FileView { FileView {
path: `${Quickshell.env("XDG_DATA_HOME")}/quickshell/settings.json` path: `${Quickshell.env("XDG_DATA_HOME")}/quickshell/settings.json`
watchChanges: true watchChanges: true
// onFileChanged: reload() onFileChanged: reload()
onAdapterUpdated: writeAdapter() onAdapterUpdated: writeAdapter()
blockLoading: true blockLoading: true
@ -22,6 +22,7 @@ Singleton {
property int barHeight: 25 property int barHeight: 25
property string wallpaperUrl: Qt.resolvedUrl("root:resources/wallpapers/pixelart0.jpg") property string wallpaperUrl: Qt.resolvedUrl("root:resources/wallpapers/pixelart0.jpg")
property string colorScheme: "scheme-fruit-salad" property string colorScheme: "scheme-fruit-salad"
property string screenshotPath: "/home/koss/Pictures"
} }
property var colors: { property var colors: {

View file

@ -78,18 +78,18 @@ PanelWindow {
// bar: root // bar: root
// } // }
Text { // Text {
text: "home" // text: "home"
color: "white" // color: "white"
font.family: "Material Symbols Rounded" // font.family: "Material Symbols Rounded"
renderType: Text.NativeRendering // renderType: Text.NativeRendering
textFormat: Text.PlainText // textFormat: Text.PlainText
font.pointSize: 14 // font.pointSize: 14
//
font.variableAxes: { // font.variableAxes: {
"FILL": 0 // "FILL": 0
} // }
} // }
BatteryIndicator { BatteryIndicator {
id: batteryIndicator id: batteryIndicator

View file

@ -0,0 +1,101 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
import ".."
Singleton {
id: root
property bool windowOpen: false
IpcHandler {
target: "screencapture"
function screenshot(): void {
root.windowOpen = true;
}
}
// Just use this window to grab screen context
LazyLoader {
activeAsync: root.windowOpen
PanelWindow {
id: focusedScreen
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
WlrLayershell.namespace: "shell:screencapture"
anchors {
top: true
bottom: true
left: true
right: true
}
Item {
anchors.fill: parent
focus: true
Keys.onEscapePressed: root.windowOpen = false
// to get a freeze frame for now
ScreencopyView {
id: screenView
captureSource: focusedScreen.screen
anchors.fill: parent
SelectionRectangle {
id: selection
anchors.fill: parent
property string position
property bool running: false
onAreaSelected: selection => {
let screen = focusedScreen.screen;
const x = Math.floor(selection.x) + screen.x;
const y = Math.floor(selection.y) + screen.y;
const width = Math.floor(selection.width);
const height = Math.floor(selection.height);
position = `${x},${y} ${width}x${height}`;
running = true;
}
LazyLoader {
activeAsync: selection.running
Process {
id: grim
running: true
property var path: `${ShellSettings.settings.screenshotPath}/screenshot.png`
command: ["grim", "-g", selection.position, path]
onRunningChanged: {
if (!running) {
root.windowOpen = false;
}
}
stderr: SplitParser {
onRead: data => console.log(`line read: ${data}`)
}
}
}
}
}
}
}
}
function init() {
}
}

View file

@ -0,0 +1,3 @@
import QtQuick
Image {}

View file

@ -0,0 +1,152 @@
import QtQuick
import ".."
Canvas {
id: root
anchors.fill: parent
property color overlayColor: "#80000000"
property color borderColor: ShellSettings.colors["primary"]
property real borderWidth: 3
property real handleSize: 16
property var screen
property real centerX: width / 2
property real centerY: height / 2
property real minWidth: 400
property real minHeight: 300
// rect that holds positional data for the selection
property rect selectionRect: Qt.rect(centerX - minWidth / 2, centerY - minHeight / 2, minWidth, minHeight)
// handle positions
property point topLeftHandle: Qt.point(selectionRect.x, selectionRect.y)
property point topRightHandle: Qt.point(selectionRect.x + selectionRect.width, selectionRect.y)
property point bottomLeftHandle: Qt.point(selectionRect.x, selectionRect.y + selectionRect.height)
property point bottomRightHandle: Qt.point(selectionRect.x + selectionRect.width, selectionRect.y + selectionRect.height)
// dragging state
property int activeHandle: -1
property point dragStart: Qt.point(0, 0)
property rect initialRect: Qt.rect(0, 0, 0, 0)
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
// grey overlay
ctx.fillStyle = overlayColor;
ctx.fillRect(0, 0, width, height);
// cut out the selection rectangle
ctx.globalCompositeOperation = "destination-out";
ctx.fillRect(selectionRect.x, selectionRect.y, selectionRect.width, selectionRect.height);
ctx.globalCompositeOperation = "source-over";
// draw border
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
ctx.beginPath();
ctx.moveTo(topLeftHandle.x, topLeftHandle.y);
ctx.lineTo(topRightHandle.x, topRightHandle.y);
ctx.lineTo(bottomRightHandle.x, bottomRightHandle.y);
ctx.lineTo(bottomLeftHandle.x, bottomLeftHandle.y);
ctx.closePath();
ctx.stroke();
// draw handles
ctx.fillStyle = borderColor;
drawHandle(ctx, topLeftHandle);
drawHandle(ctx, topRightHandle);
drawHandle(ctx, bottomLeftHandle);
drawHandle(ctx, bottomRightHandle);
}
function drawHandle(ctx, center) {
var radius = handleSize / 2;
ctx.beginPath();
ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI);
ctx.fill();
}
function getHandleAt(x, y) {
var halfSize = handleSize / 2;
var handles = [topLeftHandle, topRightHandle, bottomLeftHandle, bottomRightHandle];
for (var i = 0; i < handles.length; i++) {
var handle = handles[i];
if (x >= handle.x - halfSize && x <= handle.x + halfSize && y >= handle.y - halfSize && y <= handle.y + halfSize) {
return i;
}
}
return -1;
}
function constrainRect(rect) {
// Ensure minimum size
var width = Math.max(rect.width, minWidth);
var height = Math.max(rect.height, minHeight);
// Ensure within canvas bounds
var x = Math.max(0, Math.min(rect.x, root.width - width));
var y = Math.max(0, Math.min(rect.y, root.height - height));
return Qt.rect(x, y, width, height);
}
MouseArea {
anchors.fill: parent
onPressed: function (mouse) {
activeHandle = root.getHandleAt(mouse.x, mouse.y);
if (root.activeHandle >= 0) {
dragStart = Qt.point(mouse.x, mouse.y);
initialRect = root.selectionRect;
}
}
// kinda stupid, should maybe bind a mouse area to each handle I don't know
onPositionChanged: function (mouse) {
if (root.activeHandle < 0)
return;
var dx = mouse.x - root.dragStart.x;
var dy = mouse.y - root.dragStart.y;
var newRect;
switch (root.activeHandle) {
// top left
case 0:
var newX = Math.max(0, Math.min(root.initialRect.x + dx, root.initialRect.x + root.initialRect.width - root.minWidth));
var newY = Math.max(0, Math.min(root.initialRect.y + dy, root.initialRect.y + root.initialRect.height - minHeight));
newRect = Qt.rect(newX, newY, root.initialRect.width - (newX - root.initialRect.x), root.initialRect.height - (newY - root.initialRect.y));
break;
// top right
case 1:
var newY = Math.max(0, Math.min(root.initialRect.y + dy, root.initialRect.y + root.initialRect.height - root.minHeight));
var newWidth = Math.max(root.minWidth, Math.min(root.initialRect.width + dx, root.width - root.initialRect.x));
newRect = Qt.rect(root.initialRect.x, newY, newWidth, root.initialRect.height - (newY - root.initialRect.y));
break;
// bottom left
case 2:
var newX = Math.max(0, Math.min(root.initialRect.x + dx, root.initialRect.x + root.initialRect.width - minWidth));
var newHeight = Math.max(root.minHeight, Math.min(root.initialRect.height + dy, root.height - root.initialRect.y));
newRect = Qt.rect(newX, root.initialRect.y, root.initialRect.width - (newX - root.initialRect.x), newHeight);
break;
// bottom right
case 3:
var newWidth = Math.max(root.minWidth, Math.min(root.initialRect.width + dx, root.width - root.initialRect.x));
var newHeight = Math.max(root.minHeight, Math.min(root.initialRect.height + dy, root.height - root.initialRect.y));
newRect = Qt.rect(root.initialRect.x, root.initialRect.y, newWidth, newHeight);
break;
}
selectionRect = root.constrainRect(newRect);
root.requestPaint();
}
onReleased: {
root.activeHandle = -1;
}
}
}

View file

@ -0,0 +1,54 @@
import QtQuick
import ".."
Canvas {
id: root
property color overlayColor: "#80000000"
property color outlineColor: ShellSettings.colors["primary"]
property rect selectionRect
property point startPosition
signal areaSelected(rect selection)
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
// grey overlay
ctx.fillStyle = overlayColor;
ctx.fillRect(0, 0, width, height);
// cut out the selection rectangle
ctx.globalCompositeOperation = "destination-out";
ctx.fillRect(selectionRect.x, selectionRect.y, selectionRect.width, selectionRect.height);
ctx.globalCompositeOperation = "source-over";
ctx.strokeStyle = outlineColor;
ctx.lineWidth = 2;
ctx.strokeRect(selectionRect.x, selectionRect.y, selectionRect.width, selectionRect.height);
}
MouseArea {
anchors.fill: parent
onPressed: mouse => {
root.startPosition = Qt.point(mouse.x, mouse.y);
}
onPositionChanged: mouse => {
if (pressed) {
var x = Math.min(root.startPosition.x, mouse.x);
var y = Math.min(root.startPosition.y, mouse.y);
var width = Math.abs(mouse.x - root.startPosition.x);
var height = Math.abs(mouse.y - root.startPosition.y);
root.selectionRect = Qt.rect(x, y, width, height);
root.requestPaint();
}
}
onReleased: mouse => {
root.visible = false;
root.areaSelected(root.selectionRect);
}
}
}

View file

@ -9,9 +9,15 @@ import "volume-osd" as VolumeOSD
import "settings" as Settings import "settings" as Settings
import "launcher" as Launcher import "launcher" as Launcher
import "wallpaper" as Wallpaper import "wallpaper" as Wallpaper
import "screencapture" as ScreenCapture
ShellRoot { ShellRoot {
Component.onCompleted: [Launcher.Controller.init(), Settings.Controller.init(), Notifications.NotificationCenter.init()] Component.onCompleted: {
Launcher.Controller.init();
Settings.Controller.init();
Notifications.NotificationCenter.init();
ScreenCapture.Controller.init();
}
Variants { Variants {
model: Quickshell.screens model: Quickshell.screens

View file

@ -5,7 +5,6 @@ import ".."
Scope { Scope {
id: root id: root
required property var screen
property string matugenConf: Qt.resolvedUrl("matugen.toml").toString().replace("file://", "") property string matugenConf: Qt.resolvedUrl("matugen.toml").toString().replace("file://", "")
LazyLoader { LazyLoader {
@ -54,8 +53,18 @@ Scope {
id: matugen id: matugen
running: false running: false
// Formatter is keeping me hostage frfr... // Don't format this lol
command: ["matugen", "image", ShellSettings.settings.wallpaperUrl.replace("file://", ""), "--type", ShellSettings.settings.colorScheme, "--json", "hex", "--config", root.matugenConf] command: [
"matugen",
"image",
ShellSettings.settings.wallpaperUrl.replace("file://", ""),
"--type",
ShellSettings.settings.colorScheme,
"--json",
"hex",
"--config",
root.matugenConf
]
stdout: SplitParser { stdout: SplitParser {
onRead: data => { onRead: data => {