revamp region selector

This commit is contained in:
end-4
2025-10-22 01:43:13 +02:00
parent bce8b6f9a8
commit 7e4cbaf5df
7 changed files with 290 additions and 164 deletions
@@ -350,6 +350,20 @@ Singleton {
property real columns: 5
}
property JsonObject regionSelector: JsonObject {
property JsonObject targetRegions: JsonObject {
property bool windows: true
property bool layers: false
property bool content: true
property bool showLabel: false
property real opacity: 0.6
}
property JsonObject circle: JsonObject {
property int strokeWidth: 6
property int padding: 40
}
}
property JsonObject resources: JsonObject {
property int updateInterval: 3000
}
@@ -371,7 +385,6 @@ Singleton {
}
property JsonObject imageSearch: JsonObject {
property string imageSearchEngineBaseUrl: "https://lens.google.com/uploadbyurl?url="
property string fileUploadApiEndpoint: "https://uguu.se/upload"
property bool useCircleSelection: false
}
}
@@ -460,10 +473,6 @@ Singleton {
property int arbitraryRaceConditionDelay: 20 // milliseconds
}
property JsonObject screenshotTool: JsonObject {
property bool showContentRegions: true
}
property JsonObject workSafety: JsonObject {
property JsonObject enable: JsonObject {
property bool wallpaper: true
@@ -0,0 +1,33 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
ToolbarButton {
id: iconBtn
required property string iconText
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
property color colText: toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
contentItem: Row {
anchors.centerIn: parent
spacing: 6
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 22
text: iconBtn.iconText
color: iconBtn.colText
}
StyledText {
visible: iconBtn.iconText.length > 0 && iconBtn.text.length > 0
anchors.verticalCenter: parent.verticalCenter
color: iconBtn.colText
text: iconBtn.text
}
}
}
@@ -1,3 +1,4 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Shapes
@@ -8,7 +9,7 @@ Item {
required property color color
required property color overlayColor
required property list<point> points
property int strokeWidth: 10
property int strokeWidth: Config.options.regionSelector.circle.strokeWidth
function updatePoints() {
if (!root.dragging) return;
@@ -37,16 +37,16 @@ PanelWindow {
property string screenshotDir: Directories.screenshotTemp
property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl
property string fileUploadApiEndpoint: Config.options.search.imageSearch.fileUploadApiEndpoint
property string fileUploadApiEndpoint: "https://uguu.se/upload"
property color overlayColor: "#77111111"
property color genericContentColor: Qt.alpha(root.overlayColor, 0.9)
property color genericContentForeground: "#ddffffff"
property color selectionBorderColor: "#ddf1f1f1"
property color selectionBorderColor: ColorUtils.mix(Appearance.colors.colOnLayer0, Appearance.colors.colSecondary, 0.5)
property color selectionFillColor: "#33ffffff"
property color windowBorderColor: "#dda0c0da"
property color windowFillColor: "#22a0c0da"
property color imageBorderColor: "#ddf1d1ff"
property color imageFillColor: "#33f1d1ff"
property color windowBorderColor: Appearance.colors.colSecondary
property color windowFillColor: ColorUtils.transparentize(windowBorderColor, 0.85)
property color imageBorderColor: Appearance.colors.colTertiary
property color imageFillColor: ColorUtils.transparentize(imageBorderColor, 0.85)
property color onBorderColor: "#ff000000"
readonly property var windows: [...HyprlandData.windowList].sort((a, b) => {
// Sort floating=true windows before others
@@ -107,10 +107,25 @@ PanelWindow {
return offsetAdjustedLayers;
}
property bool isCircleSelection: (root.selectionMode === RegionSelection.SelectionMode.Circle)
property bool enableWindowRegions: Config.options.regionSelector.targetRegions.windows && !isCircleSelection
property bool enableLayerRegions: Config.options.regionSelector.targetRegions.layers && !isCircleSelection
property bool enableContentRegions: Config.options.regionSelector.targetRegions.content
property real targetRegionOpacity: Config.options.regionSelector.targetRegions.opacity
property real targetedRegionX: -1
property real targetedRegionY: -1
property real targetedRegionWidth: 0
property real targetedRegionHeight: 0
function targetedRegionValid() {
return (root.targetedRegionX >= 0 && root.targetedRegionY >= 0)
}
function setRegionToTargeted() {
root.regionX = root.targetedRegionX;
root.regionY = root.targetedRegionY;
root.regionWidth = root.targetedRegionWidth;
root.regionHeight = root.targetedRegionHeight;
}
function intersectionOverUnion(regionA, regionB) {
// region: { at: [x, y], size: [w, h] }
@@ -265,6 +280,12 @@ PanelWindow {
root.dismiss();
}
// Clamp region to screen bounds
root.regionX = Math.max(0, Math.min(root.regionX, root.screen.width - root.regionWidth));
root.regionY = Math.max(0, Math.min(root.regionY, root.screen.height - root.regionHeight));
root.regionWidth = Math.max(0, Math.min(root.regionWidth, root.screen.width - root.regionX));
root.regionHeight = Math.max(0, Math.min(root.regionHeight, root.screen.height - root.regionY));
// Adjust action
if (root.action === RegionSelection.SnipAction.Copy || root.action === RegionSelection.SnipAction.Edit) {
root.action = root.mouseButton === Qt.RightButton ? RegionSelection.SnipAction.Edit : RegionSelection.SnipAction.Copy;
@@ -335,22 +356,29 @@ PanelWindow {
onReleased: (mouse) => {
// Circle dragging?
if (root.selectionMode === RegionSelection.SelectionMode.Circle) {
const maxX = Math.max(...root.points.map(p => p.x));
const minX = Math.min(...root.points.map(p => p.x));
const maxY = Math.max(...root.points.map(p => p.y));
const minY = Math.min(...root.points.map(p => p.y));
root.regionX = minX;
root.regionY = minY;
root.regionWidth = maxX - minX;
root.regionHeight = maxY - minY;
const padding = Config.options.regionSelector.circle.padding + Config.options.regionSelector.circle.strokeWidth / 2;
const dragPoints = (root.points.length > 0) ? root.points : [{ x: mouseArea.mouseX, y: mouseArea.mouseY }];
const maxX = Math.max(...dragPoints.map(p => p.x));
const minX = Math.min(...dragPoints.map(p => p.x));
const maxY = Math.max(...dragPoints.map(p => p.y));
const minY = Math.min(...dragPoints.map(p => p.y));
root.regionX = minX - padding;
root.regionY = minY - padding;
root.regionWidth = maxX - minX + padding * 2;
root.regionHeight = maxY - minY + padding * 2;
if (root.targetedRegionValid() && imageRegions.find(region => {
return (region.at[0] === root.targetedRegionX
&& region.at[1] === root.targetedRegionY
&& region.size[0] === root.targetedRegionWidth
&& region.size[1] === root.targetedRegionHeight)
})) {
root.setRegionToTargeted();
}
}
// Detect if it was a click -> Try to select targeted region
if (root.draggingX === root.dragStartX && root.draggingY === root.dragStartY) {
if (root.targetedRegionX >= 0 && root.targetedRegionY >= 0) {
root.regionX = root.targetedRegionX;
root.regionY = root.targetedRegionY;
root.regionWidth = root.targetedRegionWidth;
root.regionHeight = root.targetedRegionHeight;
else if (root.draggingX === root.dragStartX && root.draggingY === root.dragStartY) {
if (root.targetedRegionValid()) {
root.setRegionToTargeted();
}
}
root.snip();
@@ -392,80 +420,10 @@ PanelWindow {
}
}
// Instructions
Rectangle {
z: 9999
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
topMargin: (Appearance.sizes.barHeight - implicitHeight) / 2
}
opacity: root.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: switch(root.selectionMode) {
case RegionSelection.SelectionMode.RectCorners:
return "activity_zone"
break;
case RegionSelection.SelectionMode.Circle:
return "gesture"
break;
default:
return "activity_zone"
}
color: root.genericContentForeground
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: {
var instructionText = "";
var actionText = "";
if (root.selectionMode === RegionSelection.SelectionMode.RectCorners) {
instructionText = Translation.tr("Drag or click a region");
} else if (root.selectionMode === RegionSelection.SelectionMode.Circle) {
instructionText = Translation.tr("Circle");
}
switch (root.action) {
case RegionSelection.SnipAction.Copy:
case RegionSelection.SnipAction.Edit:
actionText = Translation.tr(" | LMB: Copy • RMB: Edit");
break;
case RegionSelection.SnipAction.Search:
actionText = Translation.tr(" to search");
break;
default:
actionText = "";
}
return instructionText + actionText;
}
color: root.genericContentForeground
}
}
}
// Window regions
Repeater {
model: ScriptModel {
values: root.windowRegions
values: root.enableWindowRegions ? root.windowRegions : []
}
delegate: TargetRegion {
z: 2
@@ -479,7 +437,7 @@ PanelWindow {
colBackground: root.genericContentColor
colForeground: root.genericContentForeground
opacity: root.draggedAway ? 0 : 1
opacity: root.draggedAway ? 0 : root.targetRegionOpacity
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
@@ -500,7 +458,7 @@ PanelWindow {
// Layer regions
Repeater {
model: ScriptModel {
values: root.layerRegions
values: root.enableLayerRegions ? root.layerRegions : []
}
delegate: TargetRegion {
z: 3
@@ -513,7 +471,7 @@ PanelWindow {
colBackground: root.genericContentColor
colForeground: root.genericContentForeground
opacity: root.draggedAway ? 0 : 1
opacity: root.draggedAway ? 0 : root.targetRegionOpacity
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
@@ -531,10 +489,10 @@ PanelWindow {
}
}
// Image regions
// Content regions
Repeater {
model: ScriptModel {
values: Config.options.screenshotTool.showContentRegions ? root.imageRegions : []
values: root.enableContentRegions ? root.imageRegions : []
}
delegate: TargetRegion {
z: 4
@@ -547,7 +505,7 @@ PanelWindow {
colBackground: root.genericContentColor
colForeground: root.genericContentForeground
opacity: root.draggedAway ? 0 : 1
opacity: root.draggedAway ? 0 : root.targetRegionOpacity
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
@@ -560,7 +518,77 @@ PanelWindow {
borderColor: root.imageBorderColor
fillColor: targeted ? root.imageFillColor : "transparent"
border.width: targeted ? 4 : 2
text: "Content region"
text: Translation.tr("Content region")
}
}
// Options toolbar
Toolbar {
id: toolbar
z: 9999
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: -height
}
opacity: 0
Connections {
target: root
function onVisibleChanged() {
if (!visible) return;
toolbar.anchors.bottomMargin = 8;
toolbar.opacity = 1;
}
}
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.bottomMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
MaterialCookie {
Layout.fillHeight: true
Layout.leftMargin: 2
Layout.rightMargin: 2
implicitSize: 36 // Intentionally smaller because this one is brighter than others
sides: 10
amplitude: implicitSize / 44
color: Appearance.colors.colPrimary
MaterialSymbol {
anchors.centerIn: parent
iconSize: 22
color: Appearance.colors.colOnPrimary
text: switch (root.action) {
case RegionSelection.SnipAction.Copy:
case RegionSelection.SnipAction.Edit:
return "content_cut";
case RegionSelection.SnipAction.Search:
return "image_search";
default:
return "";
}
}
}
IconAndTextToolbarButton {
iconText: "activity_zone"
text: Translation.tr("Rect")
toggled: root.selectionMode === RegionSelection.SelectionMode.RectCorners
onClicked: root.selectionMode = RegionSelection.SelectionMode.RectCorners
}
IconAndTextToolbarButton {
iconText: "gesture"
text: Translation.tr("Circle")
toggled: root.selectionMode === RegionSelection.SelectionMode.Circle
onClicked: root.selectionMode = RegionSelection.SelectionMode.Circle
}
IconToolbarButton {
text: "close"
colBackground: Appearance.colors.colLayer3
onClicked: root.dismiss();
}
}
}
@@ -7,9 +7,10 @@ import Quickshell
import Quickshell.Widgets
Rectangle {
id: regionRect
id: root
required property color colBackground
required property color colForeground
property bool showLabel: Config.options.regionSelector.targetRegions.showLabel
property bool showIcon: false
property bool targeted: false
property color borderColor
@@ -22,41 +23,45 @@ Rectangle {
border.width: targeted ? 3 : 1
radius: 4
Rectangle {
id: regionLabelBackground
property real verticalPadding: 5
property real horizontalPadding: 10
radius: 10
color: regionRect.colBackground
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
Loader {
anchors {
top: parent.top
left: parent.left
topMargin: regionRect.textPadding
leftMargin: regionRect.textPadding
topMargin: root.textPadding
leftMargin: root.textPadding
}
implicitWidth: regionInfoRow.implicitWidth + horizontalPadding * 2
implicitHeight: regionInfoRow.implicitHeight + verticalPadding * 2
Row {
id: regionInfoRow
anchors.centerIn: parent
spacing: 4
active: root.showLabel
sourceComponent: Rectangle {
property real verticalPadding: 5
property real horizontalPadding: 10
radius: 10
color: root.colBackground
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
implicitWidth: regionInfoRow.implicitWidth + horizontalPadding * 2
implicitHeight: regionInfoRow.implicitHeight + verticalPadding * 2
Loader {
id: regionIconLoader
active: regionRect.showIcon
visible: active
sourceComponent: IconImage {
implicitSize: Appearance.font.pixelSize.larger
source: Quickshell.iconPath(AppSearch.guessIcon(regionRect.text), "image-missing")
Row {
id: regionInfoRow
anchors.centerIn: parent
spacing: 4
Loader {
id: regionIconLoader
active: root.showIcon
visible: active
sourceComponent: IconImage {
implicitSize: Appearance.font.pixelSize.larger
source: Quickshell.iconPath(AppSearch.guessIcon(root.text), "image-missing")
}
}
}
StyledText {
id: regionText
text: regionRect.text
color: regionRect.colForeground
StyledText {
id: regionText
text: root.text
color: root.colForeground
}
}
}
}
@@ -574,6 +574,87 @@ ContentPage {
}
}
ContentSection {
icon: "screenshot_frame_2"
title: Translation.tr("Region selector (screen snipping/Google Lens)")
ContentSubsection {
title: Translation.tr("Hint target regions")
ConfigRow {
ConfigSwitch {
buttonIcon: "select_window"
text: Translation.tr('Windows')
checked: Config.options.regionSelector.targetRegions.windows
onCheckedChanged: {
Config.options.regionSelector.targetRegions.windows = checked;
}
}
ConfigSwitch {
buttonIcon: "right_panel_open"
text: Translation.tr('Layers')
checked: Config.options.regionSelector.targetRegions.layers
onCheckedChanged: {
Config.options.regionSelector.targetRegions.layers = checked;
}
}
ConfigSwitch {
buttonIcon: "nearby"
text: Translation.tr('Content')
checked: Config.options.regionSelector.targetRegions.content
onCheckedChanged: {
Config.options.regionSelector.targetRegions.content = checked;
}
StyledToolTip {
text: Translation.tr("Could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.")
}
}
}
}
ContentSubsection {
title: Translation.tr("Google Lens")
ConfigSelectionArray {
currentValue: Config.options.search.imageSearch.useCircleSelection ? "circle" : "rectangles"
onSelected: newValue => {
Config.options.search.imageSearch.useCircleSelection = (newValue === "circle");
}
options: [
{ icon: "activity_zone", value: "rectangles", displayName: Translation.tr("Rectangular selection") },
{ icon: "gesture", value: "circle", displayName: Translation.tr("Circle to Search") }
]
}
}
ContentSubsection {
title: Translation.tr("Circle selection")
ConfigSpinBox {
icon: "eraser_size_3"
text: Translation.tr("Stroke width")
value: Config.options.regionSelector.circle.strokeWidth
from: 1
to: 20
stepSize: 1
onValueChanged: {
Config.options.regionSelector.circle.strokeWidth = value;
}
}
ConfigSpinBox {
icon: "screenshot_frame_2"
text: Translation.tr("Padding")
value: Config.options.regionSelector.circle.padding
from: 0
to: 100
stepSize: 5
onValueChanged: {
Config.options.regionSelector.circle.padding = value;
}
}
}
}
ContentSection {
icon: "side_navigation"
title: Translation.tr("Sidebars")
@@ -848,23 +929,6 @@ ContentPage {
}
}
ContentSection {
icon: "screenshot_frame_2"
title: Translation.tr("Screenshot tool")
ConfigSwitch {
buttonIcon: "nearby"
text: Translation.tr('Show regions of potential interest')
checked: Config.options.screenshotTool.showContentRegions
onCheckedChanged: {
Config.options.screenshotTool.showContentRegions = checked;
}
StyledToolTip {
text: Translation.tr("Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.")
}
}
}
ContentSection {
icon: "wallpaper_slideshow"
title: Translation.tr("Wallpaper selector")
@@ -147,19 +147,5 @@ ContentPage {
}
}
}
ContentSubsection {
title: Translation.tr("Google Lens")
ConfigSelectionArray {
currentValue: Config.options.search.imageSearch.useCircleSelection ? "circle" : "rectangles"
onSelected: newValue => {
Config.options.search.imageSearch.useCircleSelection = (newValue === "circle");
}
options: [
{ icon: "activity_zone", value: "rectangles", displayName: Translation.tr("Rectangular selection") },
{ icon: "gesture", value: "circle", displayName: Translation.tr("Circle to Search") }
]
}
}
}
}