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
@@ -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
}
}
}
}