forked from Shinonome/dots-hyprland
screenshot: use quickshell, ksnip -> swappy
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
//@ pragma UseQApplication
|
||||
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
||||
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
|
||||
|
||||
// Adjust this to make it smaller or larger
|
||||
//@ pragma Env QT_SCALE_FACTOR=1
|
||||
|
||||
pragma ComponentBehavior: "Bound"
|
||||
import "./modules/common/"
|
||||
import "./modules/common/widgets"
|
||||
import "./modules/common/functions/string_utils.js" as StringUtils
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import "./services/"
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
property string screenshotDir: "/tmp/quickshell/media/screenshot"
|
||||
property color overlayColor: "#77111111"
|
||||
property color genericContentColor: Qt.alpha(root.overlayColor, 0.9)
|
||||
property color genericContentForeground: "#ddffffff"
|
||||
property color selectionBorderColor: "#ddf1f1f1"
|
||||
property color selectionFillColor: "#33ffffff"
|
||||
property color windowBorderColor: "#ddd4ecff"
|
||||
property color windowFillColor: "#33d4ecff"
|
||||
property color imageBorderColor: "#ddf1d1ff"
|
||||
property color imageFillColor: "#33f1d1ff"
|
||||
property color onBorderColor: "#ff000000"
|
||||
property real standardRounding: 4
|
||||
readonly property var windows: HyprlandData.windowList
|
||||
readonly property real falsePositivePreventionRatio: 0.5
|
||||
|
||||
// Force initialization of some singletons
|
||||
Component.onCompleted: {
|
||||
MaterialThemeLoader.reapplyTheme();
|
||||
ConfigLoader.loadConfig();
|
||||
}
|
||||
|
||||
component TargetRegion: Rectangle {
|
||||
id: regionRect
|
||||
property bool targeted: false
|
||||
property color borderColor
|
||||
property color fillColor: "transparent"
|
||||
property string text: ""
|
||||
property real textPadding: 10
|
||||
z: 2
|
||||
color: fillColor
|
||||
border.color: borderColor
|
||||
border.width: targeted ? 3 : 1
|
||||
radius: root.standardRounding
|
||||
|
||||
Rectangle {
|
||||
id: regionLabelBackground
|
||||
property real verticalPadding: 5
|
||||
property real horizontalPadding: 10
|
||||
radius: 10
|
||||
color: root.genericContentColor
|
||||
border.width: 1
|
||||
border.color: Appearance.m3colors.m3outlineVariant
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
topMargin: regionRect.textPadding
|
||||
leftMargin: regionRect.textPadding
|
||||
}
|
||||
implicitWidth: regionText.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: regionText.implicitHeight + verticalPadding * 2
|
||||
StyledText {
|
||||
id: regionText
|
||||
text: regionRect.text
|
||||
color: root.genericContentForeground
|
||||
anchors {
|
||||
centerIn: parent
|
||||
margins: regionLabelBackground.padding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
id: panelWindow
|
||||
required property var modelData
|
||||
property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(modelData)
|
||||
property int activeWorkspaceId: hyprlandMonitor.activeWorkspace?.id ?? 0
|
||||
property string screenshotPath: `${root.screenshotDir}/image-${modelData.name}`
|
||||
property real dragStartX: 0
|
||||
property real dragStartY: 0
|
||||
property real draggingX: 0
|
||||
property real draggingY: 0
|
||||
property real dragDiffX: 0
|
||||
property real dragDiffY: 0
|
||||
property bool draggedAway: (dragDiffX !== 0 || dragDiffY !== 0)
|
||||
property bool dragging: false
|
||||
property var mouseButton: null
|
||||
property var imageRegions: []
|
||||
property var windowRegions: root.windows.filter(w => {
|
||||
return w.workspace.id === panelWindow.activeWorkspaceId;
|
||||
})
|
||||
|
||||
property real targetedRegionX: -1
|
||||
property real targetedRegionY: -1
|
||||
property real targetedRegionWidth: 0
|
||||
property real targetedRegionHeight: 0
|
||||
|
||||
function intersectionOverUnion(regionA, regionB) {
|
||||
// region: { at: [x, y], size: [w, h] }
|
||||
const ax1 = regionA.at[0], ay1 = regionA.at[1];
|
||||
const ax2 = ax1 + regionA.size[0], ay2 = ay1 + regionA.size[1];
|
||||
const bx1 = regionB.at[0], by1 = regionB.at[1];
|
||||
const bx2 = bx1 + regionB.size[0], by2 = by1 + regionB.size[1];
|
||||
|
||||
const interX1 = Math.max(ax1, bx1);
|
||||
const interY1 = Math.max(ay1, by1);
|
||||
const interX2 = Math.min(ax2, bx2);
|
||||
const interY2 = Math.min(ay2, by2);
|
||||
|
||||
const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1);
|
||||
const areaA = (ax2 - ax1) * (ay2 - ay1);
|
||||
const areaB = (bx2 - bx1) * (by2 - by1);
|
||||
const unionArea = areaA + areaB - interArea;
|
||||
|
||||
return unionArea > 0 ? interArea / unionArea : 0;
|
||||
}
|
||||
|
||||
function filterImageRegions(regions, windowRegions, threshold = 0.1) {
|
||||
// Remove image regions that overlap too much with any window region
|
||||
return regions.filter(region => {
|
||||
for (let i = 0; i < windowRegions.length; ++i) {
|
||||
if (intersectionOverUnion(region, windowRegions[i]) > threshold)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function updateTargetedRegion(x, y) {
|
||||
const clickedRegion = panelWindow.imageRegions.find(region => {
|
||||
return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1];
|
||||
});
|
||||
if (clickedRegion) {
|
||||
panelWindow.targetedRegionX = clickedRegion.at[0];
|
||||
panelWindow.targetedRegionY = clickedRegion.at[1];
|
||||
panelWindow.targetedRegionWidth = clickedRegion.size[0];
|
||||
panelWindow.targetedRegionHeight = clickedRegion.size[1];
|
||||
return;
|
||||
}
|
||||
|
||||
// Window regions
|
||||
const clickedWindow = panelWindow.windowRegions.find(region => {
|
||||
return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1];
|
||||
});
|
||||
if (clickedWindow) {
|
||||
panelWindow.targetedRegionX = clickedWindow.at[0];
|
||||
panelWindow.targetedRegionY = clickedWindow.at[1];
|
||||
panelWindow.targetedRegionWidth = clickedWindow.size[0];
|
||||
panelWindow.targetedRegionHeight = clickedWindow.size[1];
|
||||
return;
|
||||
}
|
||||
|
||||
panelWindow.targetedRegionX = -1;
|
||||
panelWindow.targetedRegionY = -1;
|
||||
panelWindow.targetedRegionWidth = 0;
|
||||
panelWindow.targetedRegionHeight = 0;
|
||||
}
|
||||
|
||||
property real regionWidth: Math.abs(draggingX - dragStartX)
|
||||
property real regionHeight: Math.abs(draggingY - dragStartY)
|
||||
property real regionX: Math.min(dragStartX, draggingX)
|
||||
property real regionY: Math.min(dragStartY, draggingY)
|
||||
|
||||
visible: false
|
||||
screen: modelData
|
||||
WlrLayershell.namespace: "quickshell:screenshot"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
anchors {
|
||||
left: true
|
||||
right: true
|
||||
top: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: screenshotProcess
|
||||
running: true
|
||||
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(modelData.name)}' '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}'`]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
panelWindow.visible = true;
|
||||
imageDetectionProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: imageDetectionProcess
|
||||
command: ["bash", "-c", `${Directories.scriptPath}/images/find_regions.py `
|
||||
+ `--hyprctl `
|
||||
+ `--image '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}' `
|
||||
+ `--max-width ${Math.round(panelWindow.screen.width * root.falsePositivePreventionRatio)} `
|
||||
+ `--max-height ${Math.round(panelWindow.screen.height * root.falsePositivePreventionRatio)} `]
|
||||
stdout: StdioCollector {
|
||||
id: imageDimensionCollector
|
||||
onStreamFinished: {
|
||||
// imageRegions = JSON.parse(imageDimensionCollector.text);
|
||||
imageRegions = filterImageRegions(
|
||||
JSON.parse(imageDimensionCollector.text),
|
||||
panelWindow.windowRegions
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: snipProc
|
||||
function snip() {
|
||||
if (panelWindow.regionWidth <= 0 || panelWindow.regionHeight <= 0) {
|
||||
console.warn("Invalid region size, skipping snip.");
|
||||
Qt.quit();
|
||||
}
|
||||
snipProc.startDetached();
|
||||
Qt.quit();
|
||||
}
|
||||
command: ["bash", "-c", `magick ${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)} -crop ${panelWindow.regionWidth}x${panelWindow.regionHeight}+${panelWindow.regionX}+${panelWindow.regionY} - ` + `| ${panelWindow.mouseButton === Qt.LeftButton ? "wl-copy" : "swappy -f -"}`]
|
||||
}
|
||||
|
||||
ScreencopyView {
|
||||
anchors.fill: parent
|
||||
live: false
|
||||
captureSource: modelData
|
||||
|
||||
focus: panelWindow.visible
|
||||
Keys.onPressed: (event) => { // Esc to close
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
Qt.quit();
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.CrossCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
hoverEnabled: true
|
||||
|
||||
// Controls
|
||||
onPressed: mouse => {
|
||||
panelWindow.dragStartX = mouse.x;
|
||||
panelWindow.dragStartY = mouse.y;
|
||||
panelWindow.draggingX = mouse.x;
|
||||
panelWindow.draggingY = mouse.y;
|
||||
panelWindow.dragging = true;
|
||||
panelWindow.mouseButton = mouse.button;
|
||||
}
|
||||
onReleased: mouse => {
|
||||
// Detect if it was a click
|
||||
|
||||
// Image regions
|
||||
if (panelWindow.draggingX === panelWindow.dragStartX && panelWindow.draggingY === panelWindow.dragStartY) {
|
||||
if (panelWindow.targetedRegionX >= 0 && panelWindow.targetedRegionY >= 0) {
|
||||
panelWindow.regionX = panelWindow.targetedRegionX;
|
||||
panelWindow.regionY = panelWindow.targetedRegionY;
|
||||
panelWindow.regionWidth = panelWindow.targetedRegionWidth;
|
||||
panelWindow.regionHeight = panelWindow.targetedRegionHeight;
|
||||
}
|
||||
}
|
||||
snipProc.snip();
|
||||
}
|
||||
onPositionChanged: mouse => {
|
||||
if (panelWindow.dragging) {
|
||||
panelWindow.draggingX = mouse.x;
|
||||
panelWindow.draggingY = mouse.y;
|
||||
panelWindow.dragDiffX = mouse.x - panelWindow.dragStartX;
|
||||
panelWindow.dragDiffY = mouse.y - panelWindow.dragStartY;
|
||||
}
|
||||
panelWindow.updateTargetedRegion(mouse.x, mouse.y);
|
||||
}
|
||||
|
||||
// Overlay to darken screen
|
||||
Rectangle { // Base
|
||||
id: overlayRect
|
||||
z: 0
|
||||
anchors.fill: parent
|
||||
color: root.overlayColor
|
||||
layer.enabled: true
|
||||
}
|
||||
Rectangle {
|
||||
// TODO: Make this mask the base instead of just overlaying a border
|
||||
z: 1
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
leftMargin: panelWindow.regionX
|
||||
topMargin: panelWindow.regionY
|
||||
}
|
||||
width: panelWindow.regionWidth
|
||||
height: panelWindow.regionHeight
|
||||
color: "transparent"
|
||||
border.color: root.selectionBorderColor
|
||||
border.width: 2
|
||||
radius: root.standardRounding
|
||||
}
|
||||
|
||||
// Instructions
|
||||
Rectangle {
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
topMargin: (Appearance.sizes.barHeight - implicitHeight) / 2
|
||||
}
|
||||
|
||||
opacity: panelWindow.dragging ? 0 : 1
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
color: root.genericContentColor
|
||||
radius: 10
|
||||
border.width: 1
|
||||
border.color: Appearance.m3colors.m3outlineVariant
|
||||
implicitWidth: instructionsRow.implicitWidth + 10 * 2
|
||||
implicitHeight: instructionsRow.implicitHeight + 5 * 2
|
||||
|
||||
RowLayout {
|
||||
id: instructionsRow
|
||||
anchors.centerIn: parent
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: screenshotRegionIcon.implicitWidth
|
||||
MaterialSymbol {
|
||||
id: screenshotRegionIcon
|
||||
anchors.centerIn: parent
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
text: "screenshot_region"
|
||||
color: root.genericContentForeground
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
text: "Drag or click a region • LMB: Copy • RMB: Edit"
|
||||
color: root.genericContentForeground
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: panelWindow.windowRegions
|
||||
}
|
||||
delegate: TargetRegion {
|
||||
required property var modelData
|
||||
targeted: !panelWindow.draggedAway &&
|
||||
(panelWindow.targetedRegionX === modelData.at[0]
|
||||
&& panelWindow.targetedRegionY === modelData.at[1]
|
||||
&& panelWindow.targetedRegionWidth === modelData.size[0]
|
||||
&& panelWindow.targetedRegionHeight === modelData.size[1])
|
||||
|
||||
opacity: panelWindow.draggedAway ? 0 : 1
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
x: modelData.at[0]
|
||||
y: modelData.at[1]
|
||||
width: modelData.size[0]
|
||||
height: modelData.size[1]
|
||||
borderColor: root.windowBorderColor
|
||||
fillColor: targeted ? root.windowFillColor : "transparent"
|
||||
border.width: targeted ? 4 : 2
|
||||
text: `${modelData.class}`
|
||||
radius: Appearance.rounding.windowRounding
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: panelWindow.imageRegions
|
||||
}
|
||||
delegate: TargetRegion {
|
||||
required property var modelData
|
||||
targeted: !panelWindow.draggedAway &&
|
||||
(panelWindow.targetedRegionX === modelData.at[0]
|
||||
&& panelWindow.targetedRegionY === modelData.at[1]
|
||||
&& panelWindow.targetedRegionWidth === modelData.size[0]
|
||||
&& panelWindow.targetedRegionHeight === modelData.size[1])
|
||||
|
||||
opacity: panelWindow.draggedAway ? 0 : 1
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
x: modelData.at[0]
|
||||
y: modelData.at[1]
|
||||
width: modelData.size[0]
|
||||
height: modelData.size[1]
|
||||
borderColor: root.imageBorderColor
|
||||
fillColor: targeted ? root.imageFillColor : "transparent"
|
||||
border.width: targeted ? 4 : 2
|
||||
text: "Content region"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user