From 7e4cbaf5dfceaeae1999dbc5b369d943c40538b1 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 22 Oct 2025 01:43:13 +0200 Subject: [PATCH] revamp region selector --- .../quickshell/ii/modules/common/Config.qml | 19 +- .../widgets/IconAndTextToolbarButton.qml | 33 +++ .../regionSelector/CircleSelectionDetails.qml | 3 +- .../regionSelector/RegionSelection.qml | 224 ++++++++++-------- .../modules/regionSelector/TargetRegion.qml | 63 ++--- .../ii/modules/settings/InterfaceConfig.qml | 98 ++++++-- .../ii/modules/settings/ServicesConfig.qml | 14 -- 7 files changed, 290 insertions(+), 164 deletions(-) create mode 100644 dots/.config/quickshell/ii/modules/common/widgets/IconAndTextToolbarButton.qml diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index 26a7e48a0..44ae213b3 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/common/widgets/IconAndTextToolbarButton.qml b/dots/.config/quickshell/ii/modules/common/widgets/IconAndTextToolbarButton.qml new file mode 100644 index 000000000..45f90f8a1 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/IconAndTextToolbarButton.qml @@ -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 + } + } +} diff --git a/dots/.config/quickshell/ii/modules/regionSelector/CircleSelectionDetails.qml b/dots/.config/quickshell/ii/modules/regionSelector/CircleSelectionDetails.qml index bea353d1f..c0f823fbe 100644 --- a/dots/.config/quickshell/ii/modules/regionSelector/CircleSelectionDetails.qml +++ b/dots/.config/quickshell/ii/modules/regionSelector/CircleSelectionDetails.qml @@ -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 points - property int strokeWidth: 10 + property int strokeWidth: Config.options.regionSelector.circle.strokeWidth function updatePoints() { if (!root.dragging) return; diff --git a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml index b22ce4a58..ce6385331 100644 --- a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml +++ b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml @@ -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(); } } } diff --git a/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml b/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml index 627e84c70..f1043d13b 100644 --- a/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml +++ b/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml @@ -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 + } } } } diff --git a/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml b/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml index 72469843c..ac900ccbb 100644 --- a/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml @@ -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") diff --git a/dots/.config/quickshell/ii/modules/settings/ServicesConfig.qml b/dots/.config/quickshell/ii/modules/settings/ServicesConfig.qml index 8b6c1ff2a..fae5bf4de 100644 --- a/dots/.config/quickshell/ii/modules/settings/ServicesConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/ServicesConfig.qml @@ -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") } - ] - } - } } }