Merge branch 'end-4:main' into patch-1

This commit is contained in:
Yosuke Nishiyama
2025-10-20 18:33:38 +01:00
committed by GitHub
6 changed files with 607 additions and 584 deletions
+6 -5
View File
@@ -58,7 +58,8 @@ bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs -c $
# Screenshot, Record, OCR, Color picker, Clipboard history
bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # [hidden] Clipboard history >> clipboard (fallback)
bindd = Super, Period, Copy an emoji, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback)
bindd = Super+Shift, S, Screen snip, exec, qs -p ~/.config/quickshell/$qsConfig/screenshot.qml || pidof slurp || hyprshot --freeze --clipboard-only --mode region --silent # Screen snip
bind = Super+Shift, S, global, quickshell:regionScreenshot # Screen snip
bind = Super+Shift, S, exec, qs -c $qsConfig ipc call TEST_ALIVE || pidof slurp || hyprshot --freeze --clipboard-only --mode region --silent # [hidden] Screen snip (fallback)
# OCR
bindd = Super+Shift, T, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden]
# Color picker
@@ -67,9 +68,9 @@ bindd = Super+Shift, C, Color picker, exec, hyprpicker -a # Pick color (Hex) >>
bindld = ,Print, Screenshot >> clipboard ,exec,grim - | wl-copy # Screenshot >> clipboard
bindld = Ctrl,Print, Screenshot >> clipboard & save, exec, mkdir -p $(xdg-user-dir PICTURES)/Screenshots && grim $(xdg-user-dir PICTURES)/Screenshots/Screenshot_"$(date '+%Y-%m-%d_%H.%M.%S')".png # Screenshot >> clipboard & file
# Recording stuff
bindd = Super+Alt, R, Record region (no sound), exec, ~/.config/hypr/hyprland/scripts/record.sh # Record region (no sound)
bindd = Ctrl+Alt, R, Record screen (no sound), exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen # [hidden] Record screen (no sound)
bindd = Super+Shift+Alt, R, Record screen (with sound), exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen-sound # Record screen (with sound)
bindl = Super+Alt, R, exec, ~/.config/hypr/hyprland/scripts/record.sh # Record region (no sound)
bindl = Ctrl+Alt, R, exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen # [hidden] Record screen (no sound)
bindl = Super+Shift+Alt, R, exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen-sound # Record screen (with sound)
# AI
bindd = Super+Shift+Alt, mouse:273, Generate AI summary for selected text, exec, ~/.config/hypr/hyprland/scripts/ai/primary-buffer-query.sh # AI summary for selected text
@@ -247,7 +248,7 @@ bind = Ctrl+Alt, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available
bind = Super, E, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "dolphin" "nautilus" "nemo" "thunar" "${TERMINAL}" "kitty -1 fish -c yazi" # File manager
bind = Super, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "google-chrome-stable" "zen-browser" "firefox" "brave" "chromium" "microsoft-edge-stable" "opera" "librewolf" # Browser
bind = Super, C, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "code" "codium" "cursor" "zed" "zedit" "zeditor" "kate" "gnome-text-editor" "emacs" "command -v nvim && kitty -1 nvim" "command -v micro && kitty -1 micro" # Code editor
bind = Super+Shift, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "wps" "onlyoffice-desktopeditors" # Office software
bind = Ctrl+Super+Shift+Alt, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "wps" "onlyoffice-desktopeditors" "libreoffice" # Office software
bind = Super, X, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kate" "gnome-text-editor" "emacs" # Text editor
bind = Ctrl+Super, V, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol" # Volume mixer
bind = Super, I, exec, XDG_CURRENT_DESKTOP=gnome ~/.config/hypr/hyprland/scripts/launch_first_available.sh "qs -p ~/.config/quickshell/$qsConfig/settings.qml" "systemsettings" "gnome-control-center" "better-control" # Settings app
+1
View File
@@ -141,6 +141,7 @@ layerrule = noanim, quickshell:lockWindowPusher
layerrule = animation fade, quickshell:notificationPopup
layerrule = noanim, quickshell:overview
layerrule = animation slide bottom, quickshell:osk
layerrule = noanim, quickshell:regionSelector
layerrule = noanim, quickshell:screenshot
layerrule = blur, quickshell:session
layerrule = noanim, quickshell:session
+2 -1
View File
@@ -18,13 +18,14 @@ Singleton {
property bool osdVolumeOpen: false
property bool oskOpen: false
property bool overviewOpen: false
property bool wallpaperSelectorOpen: false
property bool regionSelectorOpen: false
property bool screenLocked: false
property bool screenLockContainsCharacters: false
property bool screenUnlockFailed: false
property bool sessionOpen: false
property bool superDown: false
property bool superReleaseMightTrigger: true
property bool wallpaperSelectorOpen: false
property bool workspaceShowNumbers: false
onSidebarRightOpenChanged: {
@@ -0,0 +1,595 @@
pragma ComponentBehavior: Bound
import qs
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import Quickshell.Hyprland
Scope {
id: root
property string screenshotDir: Directories.screenshotTemp
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: "#dda0c0da"
property color windowFillColor: "#22a0c0da"
property color imageBorderColor: "#ddf1d1ff"
property color imageFillColor: "#33f1d1ff"
property color onBorderColor: "#ff000000"
property real standardRounding: 4
readonly property var windows: [...HyprlandData.windowList].sort((a, b) => {
// Sort floating=true windows before others
if (a.floating === b.floating) return 0;
return a.floating ? -1 : 1;
})
readonly property var layers: HyprlandData.layers
readonly property real falsePositivePreventionRatio: 0.5
function dismiss() {
GlobalStates.regionSelectorOpen = false
}
component TargetRegion: Rectangle {
id: regionRect
property bool showIcon: false
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: regionInfoRow.implicitWidth + horizontalPadding * 2
implicitHeight: regionInfoRow.implicitHeight + verticalPadding * 2
Row {
id: regionInfoRow
anchors.centerIn: parent
spacing: 4
Loader {
id: regionIconLoader
active: regionRect.showIcon
visible: active
sourceComponent: IconImage {
implicitSize: Appearance.font.pixelSize.larger
source: Quickshell.iconPath(AppSearch.guessIcon(regionRect.text), "image-missing")
}
}
StyledText {
id: regionText
text: regionRect.text
color: root.genericContentForeground
}
}
}
}
Variants {
model: Quickshell.screens
delegate: Loader {
id: regionSelectorLoader
required property var modelData
active: GlobalStates.regionSelectorOpen
sourceComponent: PanelWindow {
id: panelWindow
screen: regionSelectorLoader.modelData
visible: false
WlrLayershell.namespace: "quickshell:regionSelector"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
exclusionMode: ExclusionMode.Ignore
anchors {
left: true
right: true
top: true
bottom: true
}
readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen)
readonly property real monitorScale: hyprlandMonitor.scale
readonly property real monitorOffsetX: hyprlandMonitor.x
readonly property real monitorOffsetY: hyprlandMonitor.y
property int activeWorkspaceId: hyprlandMonitor.activeWorkspace?.id ?? 0
property string screenshotPath: `${root.screenshotDir}/image-${screen.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: []
readonly property list<var> windowRegions: filterWindowRegionsByLayers(
root.windows.filter(w => w.workspace.id === panelWindow.activeWorkspaceId),
panelWindow.layerRegions
).map(window => {
return {
at: [window.at[0] - panelWindow.monitorOffsetX, window.at[1] - panelWindow.monitorOffsetY],
size: [window.size[0], window.size[1]],
class: window.class,
title: window.title,
}
})
readonly property list<var> layerRegions: {
const layersOfThisMonitor = root.layers[panelWindow.hyprlandMonitor.name]
const topLayers = layersOfThisMonitor?.levels["2"]
if (!topLayers) return [];
const nonBarTopLayers = topLayers
.filter(layer => !(layer.namespace.includes(":bar") || layer.namespace.includes(":verticalBar") || layer.namespace.includes(":dock")))
.map(layer => {
return {
at: [layer.x, layer.y],
size: [layer.w, layer.h],
namespace: layer.namespace,
}
})
const offsetAdjustedLayers = nonBarTopLayers.map(layer => {
return {
at: [layer.at[0] - panelWindow.monitorOffsetX, layer.at[1] - panelWindow.monitorOffsetY],
size: layer.size,
namespace: layer.namespace,
}
});
return offsetAdjustedLayers;
}
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 filterOverlappingImageRegions(regions) {
let keep = [];
let removed = new Set();
for (let i = 0; i < regions.length; ++i) {
if (removed.has(i)) continue;
let regionA = regions[i];
for (let j = i + 1; j < regions.length; ++j) {
if (removed.has(j)) continue;
let regionB = regions[j];
if (intersectionOverUnion(regionA, regionB) > 0) {
// Compare areas
let areaA = regionA.size[0] * regionA.size[1];
let areaB = regionB.size[0] * regionB.size[1];
if (areaA <= areaB) {
removed.add(j);
} else {
removed.add(i);
}
}
}
}
for (let i = 0; i < regions.length; ++i) {
if (!removed.has(i)) keep.push(regions[i]);
}
return keep;
}
function filterWindowRegionsByLayers(windowRegions, layerRegions) {
return windowRegions.filter(windowRegion => {
for (let i = 0; i < layerRegions.length; ++i) {
if (intersectionOverUnion(windowRegion, layerRegions[i]) > 0)
return false;
}
return true;
});
}
function filterImageRegions(regions, windowRegions, threshold = 0.1) {
// Remove image regions that overlap too much with any window region
let filtered = regions.filter(region => {
for (let i = 0; i < windowRegions.length; ++i) {
if (intersectionOverUnion(region, windowRegions[i]) > threshold)
return false;
}
return true;
});
// Remove overlapping image regions, keep only the smaller one
return filterOverlappingImageRegions(filtered);
}
function updateTargetedRegion(x, y) {
// Image regions
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;
}
// Layer regions
const clickedLayer = panelWindow.layerRegions.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 (clickedLayer) {
panelWindow.targetedRegionX = clickedLayer.at[0];
panelWindow.targetedRegionY = clickedLayer.at[1];
panelWindow.targetedRegionWidth = clickedLayer.size[0];
panelWindow.targetedRegionHeight = clickedLayer.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)
Process {
id: screenshotProcess
running: true
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(panelWindow.screen.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-venv.sh `
+ `--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 = 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.");
root.dismiss();
}
snipProc.startDetached();
root.dismiss();
}
command: ["bash", "-c",
`magick ${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)} `
+ `-crop ${panelWindow.regionWidth * panelWindow.monitorScale}x${panelWindow.regionHeight * panelWindow.monitorScale}+${panelWindow.regionX * panelWindow.monitorScale}+${panelWindow.regionY * panelWindow.monitorScale} - `
+ `| ${panelWindow.mouseButton === Qt.LeftButton ? "wl-copy" : "swappy -f -"}`]
}
ScreencopyView {
anchors.fill: parent
live: false
captureSource: panelWindow.screen
focus: panelWindow.visible
Keys.onPressed: (event) => { // Esc to close
if (event.key === Qt.Key_Escape) {
root.dismiss();
}
}
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: darkenOverlay
z: 1
anchors {
left: parent.left
top: parent.top
leftMargin: panelWindow.regionX - darkenOverlay.border.width
topMargin: panelWindow.regionY - darkenOverlay.border.width
}
width: panelWindow.regionWidth + darkenOverlay.border.width * 2
height: panelWindow.regionHeight + darkenOverlay.border.width * 2
color: "transparent"
// border.color: root.selectionBorderColor
border.color: root.overlayColor
border.width: Math.max(panelWindow.width, panelWindow.height)
radius: root.standardRounding
}
Rectangle {
id: selectionBorder
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
radius: 0 // TODO: figure out how to make the overlay thing work with rounding
}
StyledText {
z: 2
anchors {
top: selectionBorder.bottom
right: selectionBorder.right
margins: 8
}
color: root.selectionBorderColor
text: `${Math.round(panelWindow.regionWidth)} x ${Math.round(panelWindow.regionHeight)}`
}
// Instructions
Rectangle {
z: 9999
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
Row {
id: instructionsRow
anchors.centerIn: parent
spacing: 4
MaterialSymbol {
id: screenshotRegionIcon
// anchors.centerIn: parent
iconSize: Appearance.font.pixelSize.larger
text: "screenshot_region"
color: root.genericContentForeground
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: Translation.tr("Drag or click a region • LMB: Copy • RMB: Edit")
color: root.genericContentForeground
}
}
}
// Window regions
Repeater {
model: ScriptModel {
values: panelWindow.windowRegions
}
delegate: TargetRegion {
z: 2
required property var modelData
showIcon: true
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
}
}
// Layer regions
Repeater {
model: ScriptModel {
values: panelWindow.layerRegions
}
delegate: TargetRegion {
z: 3
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.namespace}`
radius: Appearance.rounding.windowRounding
}
}
// Image regions
Repeater {
model: ScriptModel {
values: Config.options.screenshotTool.showContentRegions ? panelWindow.imageRegions : []
}
delegate: TargetRegion {
z: 4
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"
}
}
}
}
}
}
}
function screenshot() {
GlobalStates.regionSelectorOpen = true
}
IpcHandler {
target: "region"
function screenshot() {
root.screenshot()
}
}
GlobalShortcut {
name: "regionScreenshot"
description: "Takes a screenshot of the selected region"
onPressed: {
root.screenshot()
}
}
}
-578
View File
@@ -1,578 +0,0 @@
//@ pragma UseQApplication
//@ pragma Env QS_NO_RELOAD_POPUP=1
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000
// Adjust this to make it smaller or larger
//@ pragma Env QT_SCALE_FACTOR=1
pragma ComponentBehavior: "Bound"
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
ShellRoot {
id: root
property string screenshotDir: Directories.screenshotTemp
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: "#dda0c0da"
property color windowFillColor: "#22a0c0da"
property color imageBorderColor: "#ddf1d1ff"
property color imageFillColor: "#33f1d1ff"
property color onBorderColor: "#ff000000"
property real standardRounding: 4
readonly property var windows: [...HyprlandData.windowList].sort((a, b) => {
// Sort floating=true windows before others
if (a.floating === b.floating) return 0;
return a.floating ? -1 : 1;
})
readonly property var layers: HyprlandData.layers
readonly property real falsePositivePreventionRatio: 0.5
// Force initialization of some singletons
Component.onCompleted: {
MaterialThemeLoader.reapplyTheme();
}
component TargetRegion: Rectangle {
id: regionRect
property bool showIcon: false
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: regionInfoRow.implicitWidth + horizontalPadding * 2
implicitHeight: regionInfoRow.implicitHeight + verticalPadding * 2
Row {
id: regionInfoRow
anchors.centerIn: parent
spacing: 4
Loader {
id: regionIconLoader
active: regionRect.showIcon
visible: active
sourceComponent: IconImage {
implicitSize: Appearance.font.pixelSize.larger
source: Quickshell.iconPath(AppSearch.guessIcon(regionRect.text), "image-missing")
}
}
StyledText {
id: regionText
text: regionRect.text
color: root.genericContentForeground
}
}
}
}
Variants {
model: Quickshell.screens
PanelWindow {
id: panelWindow
required property var modelData
readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(modelData)
readonly property real monitorScale: hyprlandMonitor.scale
readonly property real monitorOffsetX: hyprlandMonitor.x
readonly property real monitorOffsetY: hyprlandMonitor.y
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: []
readonly property list<var> windowRegions: filterWindowRegionsByLayers(
root.windows.filter(w => w.workspace.id === panelWindow.activeWorkspaceId),
panelWindow.layerRegions
).map(window => {
return {
at: [window.at[0] - panelWindow.monitorOffsetX, window.at[1] - panelWindow.monitorOffsetY],
size: [window.size[0], window.size[1]],
class: window.class,
title: window.title,
}
})
readonly property list<var> layerRegions: {
const layersOfThisMonitor = root.layers[panelWindow.hyprlandMonitor.name]
const topLayers = layersOfThisMonitor?.levels["2"]
if (!topLayers) return [];
const nonBarTopLayers = topLayers
.filter(layer => !(layer.namespace.includes(":bar") || layer.namespace.includes(":verticalBar") || layer.namespace.includes(":dock")))
.map(layer => {
return {
at: [layer.x, layer.y],
size: [layer.w, layer.h],
namespace: layer.namespace,
}
})
const offsetAdjustedLayers = nonBarTopLayers.map(layer => {
return {
at: [layer.at[0] - panelWindow.monitorOffsetX, layer.at[1] - panelWindow.monitorOffsetY],
size: layer.size,
namespace: layer.namespace,
}
});
return offsetAdjustedLayers;
}
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 filterOverlappingImageRegions(regions) {
let keep = [];
let removed = new Set();
for (let i = 0; i < regions.length; ++i) {
if (removed.has(i)) continue;
let regionA = regions[i];
for (let j = i + 1; j < regions.length; ++j) {
if (removed.has(j)) continue;
let regionB = regions[j];
if (intersectionOverUnion(regionA, regionB) > 0) {
// Compare areas
let areaA = regionA.size[0] * regionA.size[1];
let areaB = regionB.size[0] * regionB.size[1];
if (areaA <= areaB) {
removed.add(j);
} else {
removed.add(i);
}
}
}
}
for (let i = 0; i < regions.length; ++i) {
if (!removed.has(i)) keep.push(regions[i]);
}
return keep;
}
function filterWindowRegionsByLayers(windowRegions, layerRegions) {
return windowRegions.filter(windowRegion => {
for (let i = 0; i < layerRegions.length; ++i) {
if (intersectionOverUnion(windowRegion, layerRegions[i]) > 0)
return false;
}
return true;
});
}
function filterImageRegions(regions, windowRegions, threshold = 0.1) {
// Remove image regions that overlap too much with any window region
let filtered = regions.filter(region => {
for (let i = 0; i < windowRegions.length; ++i) {
if (intersectionOverUnion(region, windowRegions[i]) > threshold)
return false;
}
return true;
});
// Remove overlapping image regions, keep only the smaller one
return filterOverlappingImageRegions(filtered);
}
function updateTargetedRegion(x, y) {
// Image regions
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;
}
// Layer regions
const clickedLayer = panelWindow.layerRegions.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 (clickedLayer) {
panelWindow.targetedRegionX = clickedLayer.at[0];
panelWindow.targetedRegionY = clickedLayer.at[1];
panelWindow.targetedRegionWidth = clickedLayer.size[0];
panelWindow.targetedRegionHeight = clickedLayer.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-venv.sh `
+ `--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 = 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 * panelWindow.monitorScale}x${panelWindow.regionHeight * panelWindow.monitorScale}+${panelWindow.regionX * panelWindow.monitorScale}+${panelWindow.regionY * panelWindow.monitorScale} - `
+ `| ${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: darkenOverlay
z: 1
anchors {
left: parent.left
top: parent.top
leftMargin: panelWindow.regionX - darkenOverlay.border.width
topMargin: panelWindow.regionY - darkenOverlay.border.width
}
width: panelWindow.regionWidth + darkenOverlay.border.width * 2
height: panelWindow.regionHeight + darkenOverlay.border.width * 2
color: "transparent"
// border.color: root.selectionBorderColor
border.color: root.overlayColor
border.width: Math.max(panelWindow.width, panelWindow.height)
radius: root.standardRounding
}
Rectangle {
id: selectionBorder
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
radius: 0 // TODO: figure out how to make the overlay thing work with rounding
}
StyledText {
z: 2
anchors {
top: selectionBorder.bottom
right: selectionBorder.right
margins: 8
}
color: root.selectionBorderColor
text: `${Math.round(panelWindow.regionWidth)} x ${Math.round(panelWindow.regionHeight)}`
}
// Instructions
Rectangle {
z: 9999
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
Row {
id: instructionsRow
anchors.centerIn: parent
spacing: 4
MaterialSymbol {
id: screenshotRegionIcon
// anchors.centerIn: parent
iconSize: Appearance.font.pixelSize.larger
text: "screenshot_region"
color: root.genericContentForeground
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: Translation.tr("Drag or click a region • LMB: Copy • RMB: Edit")
color: root.genericContentForeground
}
}
}
// Window regions
Repeater {
model: ScriptModel {
values: panelWindow.windowRegions
}
delegate: TargetRegion {
z: 2
required property var modelData
showIcon: true
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
}
}
// Layer regions
Repeater {
model: ScriptModel {
values: panelWindow.layerRegions
}
delegate: TargetRegion {
z: 3
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.namespace}`
radius: Appearance.rounding.windowRounding
}
}
// Image regions
Repeater {
model: ScriptModel {
values: Config.options.screenshotTool.showContentRegions ? panelWindow.imageRegions : []
}
delegate: TargetRegion {
z: 4
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"
}
}
}
}
}
}
}
+3
View File
@@ -19,6 +19,7 @@ import "./modules/notificationPopup/"
import "./modules/onScreenDisplay/"
import "./modules/onScreenKeyboard/"
import "./modules/overview/"
import "./modules/regionSelector/"
import "./modules/screenCorners/"
import "./modules/sessionScreen/"
import "./modules/sidebarLeft/"
@@ -45,6 +46,7 @@ ShellRoot {
property bool enableOnScreenDisplay: true
property bool enableOnScreenKeyboard: true
property bool enableOverview: true
property bool enableRegionSelector: true
property bool enableReloadPopup: true
property bool enableScreenCorners: true
property bool enableSessionScreen: true
@@ -74,6 +76,7 @@ ShellRoot {
LazyLoader { active: enableOnScreenDisplay; component: OnScreenDisplay {} }
LazyLoader { active: enableOnScreenKeyboard; component: OnScreenKeyboard {} }
LazyLoader { active: enableOverview; component: Overview {} }
LazyLoader { active: enableRegionSelector; component: RegionSelector {} }
LazyLoader { active: enableReloadPopup; component: ReloadPopup {} }
LazyLoader { active: enableScreenCorners; component: ScreenCorners {} }
LazyLoader { active: enableSessionScreen; component: SessionScreen {} }