diff --git a/dots/.config/quickshell/ii/modules/ii/regionSelector/RectCornersSelectionDetails.qml b/dots/.config/quickshell/ii/modules/ii/regionSelector/RectCornersSelectionDetails.qml index 3682e7d51..7bc7669d1 100644 --- a/dots/.config/quickshell/ii/modules/ii/regionSelector/RectCornersSelectionDetails.qml +++ b/dots/.config/quickshell/ii/modules/ii/regionSelector/RectCornersSelectionDetails.qml @@ -14,11 +14,14 @@ Item { required property color overlayColor property bool showAimLines: Config.options.regionSelector.rect.showAimLines + property bool breathingBorderOnly: false + // Overlay to darken screen // Base dark overlay around region Rectangle { id: darkenOverlay z: 1 + visible: !root.breathingBorderOnly anchors { left: parent.left top: parent.top @@ -32,25 +35,6 @@ Item { border.width: Math.max(root.width, root.height) } - // Selection border - // Rectangle { - // id: selectionBorder - // z: 1 - // anchors { - // left: parent.left - // top: parent.top - // leftMargin: root.regionX - // topMargin: root.regionY - // } - // width: root.regionWidth - // height: root.regionHeight - // color: "transparent" - // border.color: root.color - // border.width: 2 - // // radius: root.standardRounding - // radius: 0 // TODO: figure out how to make the overlay thing work with rounding - // } - DashedBorder { id: selectionBorder z: 9 @@ -64,13 +48,23 @@ Item { height: Math.round(root.regionHeight) + borderWidth * 2 color: root.color - dashLength: 6 - gapLength: 3 + dashLength: 8 + gapLength: 4 borderWidth: 1 + + // Breathing + opacity: 0.9 + SequentialAnimation on opacity { + running: root.breathingBorderOnly + loops: Animation.Infinite + NumberAnimation { from: 0.9; to: 0.3; duration: 1200; easing.type: Easing.InOutQuad } + NumberAnimation { from: 0.3; to: 0.9; duration: 1200; easing.type: Easing.InOutQuad } + } } StyledText { z: 2 + visible: !root.breathingBorderOnly anchors { top: selectionBorder.bottom right: selectionBorder.right @@ -82,7 +76,7 @@ Item { // Coord lines Rectangle { // Vertical - visible: root.showAimLines + visible: root.showAimLines && !root.breathingBorderOnly opacity: 0.2 z: 2 x: root.mouseX @@ -94,7 +88,7 @@ Item { color: root.color } Rectangle { // Horizontal - visible: root.showAimLines + visible: root.showAimLines && !root.breathingBorderOnly opacity: 0.2 z: 2 y: root.mouseY diff --git a/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelection.qml b/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelection.qml index 5f5ed79ab..4f96f2506 100644 --- a/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelection.qml +++ b/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelection.qml @@ -27,13 +27,17 @@ PanelWindow { bottom: true } + // Modes // TODO: Ask: sidebar AI enum SnipAction { Copy, Edit, Search, CharRecognition, Record, RecordWithSound } enum SelectionMode { RectCorners, Circle } + enum Phase { Select, Post } property var action: RegionSelection.SnipAction.Copy property var selectionMode: RegionSelection.SelectionMode.RectCorners + property var phase: RegionSelection.Phase.Select signal dismiss() + // Styles property string screenshotDir: Directories.screenshotTemp property color overlayColor: ColorUtils.transparentize("#000000", 0.4) property color brightText: Appearance.m3colors.darkmode ? Appearance.colors.colOnLayer0 : Appearance.colors.colLayer0 @@ -46,6 +50,10 @@ PanelWindow { property color imageBorderColor: brightTertiary property color imageFillColor: ColorUtils.transparentize(imageBorderColor, 0.85) property color onBorderColor: "#ff000000" + property real targetRegionOpacity: Config.options.regionSelector.targetRegions.opacity + property bool contentRegionOpacity: Config.options.regionSelector.targetRegions.contentRegionOpacity + + // Vars for indicators readonly property var windows: [...HyprlandData.windowList].sort((a, b) => { // Sort floating=true windows before others if (a.floating === b.floating) return 0; @@ -54,6 +62,7 @@ PanelWindow { readonly property var layers: HyprlandData.layers readonly property real falsePositivePreventionRatio: 0.5 + // Screen & interaction vars readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen) readonly property real monitorScale: hyprlandMonitor.scale readonly property real monitorOffsetX: hyprlandMonitor.x @@ -105,13 +114,13 @@ PanelWindow { return offsetAdjustedLayers; } + // Config 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 bool contentRegionOpacity: Config.options.regionSelector.targetRegions.contentRegionOpacity + // Target property real targetedRegionX: -1 property real targetedRegionY: -1 property real targetedRegionWidth: 0 @@ -175,6 +184,7 @@ PanelWindow { property real regionX: Math.min(dragStartX, draggingX) property real regionY: Math.min(dragStartY, draggingY) + // Screenshot stuff TempScreenshotProcess { id: screenshotProc running: true @@ -247,6 +257,7 @@ PanelWindow { } } + // Execution after selection function snip() { // Validity check if (root.regionWidth <= 0 || root.regionHeight <= 0) { @@ -277,21 +288,27 @@ PanelWindow { screenshotAction, // screenshotDir ) - snipProc.command = command; - - // Image post-processing - snipProc.startDetached(); - root.dismiss(); + Quickshell.execDetached(command); + if (root.action == RegionSelection.SnipAction.Record || root.action == RegionSelection.SnipAction.RecordWithSound) { + root.phase = RegionSelection.Phase.Post + } else { + root.dismiss(); + } } - Process { - id: snipProc + // Only clickable in Selection phase + mask: Region { + item: switch(root.phase) { + case RegionSelection.Phase.Select: return mouseArea; + case RegionSelection.Phase.Post: return null; + } } - ScreencopyView { + ScreencopyView { // For freezing anchors.fill: parent live: false captureSource: root.screen + visible: root.phase === RegionSelection.Phase.Select focus: root.visible Keys.onPressed: (event) => { // Esc to close @@ -299,220 +316,242 @@ PanelWindow { root.dismiss(); } } + } - MouseArea { - id: mouseArea - anchors.fill: parent - cursorShape: Qt.CrossCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton - hoverEnabled: true + MouseArea { + id: mouseArea + anchors.fill: parent + cursorShape: Qt.CrossCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true - // Controls - onPressed: (mouse) => { - root.dragStartX = mouse.x; - root.dragStartY = mouse.y; - root.draggingX = mouse.x; - root.draggingY = mouse.y; - root.dragging = true; - root.mouseButton = mouse.button; - } - onReleased: (mouse) => { - // Detect if it was a click -> Try to select targeted region - if (root.draggingX === root.dragStartX && root.draggingY === root.dragStartY) { - if (root.targetedRegionValid()) { - root.setRegionToTargeted(); - } - } - // Circle dragging? - else if (root.selectionMode === RegionSelection.SelectionMode.Circle) { - 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; - } - root.snip(); - } - onPositionChanged: (mouse) => { - root.updateTargetedRegion(mouse.x, mouse.y); - if (!root.dragging) return; - root.draggingX = mouse.x; - root.draggingY = mouse.y; - root.dragDiffX = mouse.x - root.dragStartX; - root.dragDiffY = mouse.y - root.dragStartY; - root.points.push({ x: mouse.x, y: mouse.y }); - } - - Loader { - z: 2 - anchors.fill: parent - active: root.selectionMode === RegionSelection.SelectionMode.RectCorners - sourceComponent: RectCornersSelectionDetails { - regionX: root.regionX - regionY: root.regionY - regionWidth: root.regionWidth - regionHeight: root.regionHeight - mouseX: mouseArea.mouseX - mouseY: mouseArea.mouseY - color: root.selectionBorderColor - overlayColor: root.overlayColor - } - } - - Loader { - z: 2 - anchors.fill: parent - active: root.selectionMode === RegionSelection.SelectionMode.Circle - sourceComponent: CircleSelectionDetails { - color: root.selectionBorderColor - overlayColor: root.overlayColor - points: root.points - } - } - - CursorGuide { - z: 9999 - x: root.dragging ? root.regionX + root.regionWidth : mouseArea.mouseX - y: root.dragging ? root.regionY + root.regionHeight : mouseArea.mouseY - action: root.action - selectionMode: root.selectionMode - } - - // Window regions - Repeater { - model: ScriptModel { - values: root.enableWindowRegions ? root.windowRegions : [] - } - delegate: TargetRegion { - z: 2 - required property var modelData - clientDimensions: modelData - showIcon: true - targeted: !root.draggedAway && - (root.targetedRegionX === modelData.at[0] - && root.targetedRegionY === modelData.at[1] - && root.targetedRegionWidth === modelData.size[0] - && root.targetedRegionHeight === modelData.size[1]) - - opacity: root.draggedAway ? 0 : root.targetRegionOpacity - borderColor: root.windowBorderColor - fillColor: targeted ? root.windowFillColor : "transparent" - text: `${modelData.class}` - radius: Appearance.rounding.windowRounding - } - } - - // Layer regions - Repeater { - model: ScriptModel { - values: root.enableLayerRegions ? root.layerRegions : [] - } - delegate: TargetRegion { - z: 3 - required property var modelData - clientDimensions: modelData - targeted: !root.draggedAway && - (root.targetedRegionX === modelData.at[0] - && root.targetedRegionY === modelData.at[1] - && root.targetedRegionWidth === modelData.size[0] - && root.targetedRegionHeight === modelData.size[1]) - - opacity: root.draggedAway ? 0 : root.targetRegionOpacity - borderColor: root.windowBorderColor - fillColor: targeted ? root.windowFillColor : "transparent" - text: `${modelData.namespace}` - radius: Appearance.rounding.windowRounding - } - } - - // Content regions - Repeater { - model: ScriptModel { - values: root.enableContentRegions ? root.imageRegions : [] - } - delegate: TargetRegion { - z: 4 - required property var modelData - clientDimensions: modelData - targeted: !root.draggedAway && - (root.targetedRegionX === modelData.at[0] - && root.targetedRegionY === modelData.at[1] - && root.targetedRegionWidth === modelData.size[0] - && root.targetedRegionHeight === modelData.size[1]) - - opacity: root.draggedAway ? 0 : root.contentRegionOpacity - borderColor: root.imageBorderColor - fillColor: targeted ? root.imageFillColor : "transparent" - text: Translation.tr("Content region") - } - } - - // Controls - Row { - id: regionSelectionControls - z: 10 - anchors { - horizontalCenter: parent.horizontalCenter - bottom: parent.bottom - bottomMargin: -height - } - opacity: 0 - Connections { - target: root - function onVisibleChanged() { - if (!visible) return; - regionSelectionControls.anchors.bottomMargin = 8; - regionSelectionControls.opacity = 1; - } - } - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - Behavior on anchors.bottomMargin { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - spacing: 6 - - OptionsToolbar { - Synchronizer on action { - property alias source: root.action - } - Synchronizer on selectionMode { - property alias source: root.selectionMode - } - onDismiss: root.dismiss(); - } - Item { - anchors { - verticalCenter: parent.verticalCenter - } - implicitWidth: closeFab.implicitWidth - implicitHeight: closeFab.implicitHeight - StyledRectangularShadow { - target: closeFab - radius: closeFab.buttonRadius - } - FloatingActionButton { - id: closeFab - baseSize: 48 - iconText: "close" - onClicked: root.dismiss(); - StyledToolTip { - text: Translation.tr("Close") - } - colBackground: Appearance.colors.colTertiaryContainer - colBackgroundHover: Appearance.colors.colTertiaryContainerHover - colRipple: Appearance.colors.colTertiaryContainerActive - colOnBackground: Appearance.colors.colOnTertiaryContainer - } - } - } - + // Controls + onPressed: (mouse) => { + root.dragStartX = mouse.x; + root.dragStartY = mouse.y; + root.draggingX = mouse.x; + root.draggingY = mouse.y; + root.dragging = true; + root.mouseButton = mouse.button; } + onReleased: (mouse) => { + // Detect if it was a click -> Try to select targeted region + if (root.draggingX === root.dragStartX && root.draggingY === root.dragStartY) { + if (root.targetedRegionValid()) { + root.setRegionToTargeted(); + } + } + // Circle dragging? + else if (root.selectionMode === RegionSelection.SelectionMode.Circle) { + 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; + } + root.snip(); + } + onPositionChanged: (mouse) => { + root.updateTargetedRegion(mouse.x, mouse.y); + if (!root.dragging) return; + root.draggingX = mouse.x; + root.draggingY = mouse.y; + root.dragDiffX = mouse.x - root.dragStartX; + root.dragDiffY = mouse.y - root.dragStartY; + root.points.push({ x: mouse.x, y: mouse.y }); + } + + Loader { + z: 2 + anchors.fill: parent + active: root.selectionMode === RegionSelection.SelectionMode.RectCorners + sourceComponent: RectCornersSelectionDetails { + regionX: root.regionX + regionY: root.regionY + regionWidth: root.regionWidth + regionHeight: root.regionHeight + mouseX: mouseArea.mouseX + mouseY: mouseArea.mouseY + color: root.selectionBorderColor + overlayColor: root.overlayColor + breathingBorderOnly: root.phase === RegionSelection.Phase.Post + } + } + + Loader { + z: 2 + anchors.fill: parent + active: root.selectionMode === RegionSelection.SelectionMode.Circle + sourceComponent: CircleSelectionDetails { + color: root.selectionBorderColor + overlayColor: root.overlayColor + points: root.points + } + } + + // The thing to the bottom-right with an icon + CursorGuide { + z: 9999 + visible: root.phase === RegionSelection.Phase.Select + x: root.dragging ? root.regionX + root.regionWidth : mouseArea.mouseX + y: root.dragging ? root.regionY + root.regionHeight : mouseArea.mouseY + action: root.action + selectionMode: root.selectionMode + } + + // Window regions + Repeater { + model: ScriptModel { + values: { + if (root.phase === RegionSelection.Phase.Select && root.enableWindowRegions) { + return root.windowRegions + } else { + return [] + } + } + } + delegate: TargetRegion { + z: 2 + required property var modelData + clientDimensions: modelData + showIcon: true + targeted: !root.draggedAway && // + (root.targetedRegionX === modelData.at[0] // + && root.targetedRegionY === modelData.at[1] // + && root.targetedRegionWidth === modelData.size[0] // + && root.targetedRegionHeight === modelData.size[1]) + + opacity: root.draggedAway ? 0 : root.targetRegionOpacity + borderColor: root.windowBorderColor + fillColor: targeted ? root.windowFillColor : "transparent" + text: `${modelData.class}` + radius: Appearance.rounding.windowRounding + } + } + + // Layer regions + Repeater { + model: ScriptModel { + values: { + if (root.phase === RegionSelection.Phase.Select && root.enableLayerRegions) { + return root.layerRegions + } else { + return [] + } + } + } + delegate: TargetRegion { + z: 3 + required property var modelData + clientDimensions: modelData + targeted: !root.draggedAway && + (root.targetedRegionX === modelData.at[0] + && root.targetedRegionY === modelData.at[1] + && root.targetedRegionWidth === modelData.size[0] + && root.targetedRegionHeight === modelData.size[1]) + + opacity: root.draggedAway ? 0 : root.targetRegionOpacity + borderColor: root.windowBorderColor + fillColor: targeted ? root.windowFillColor : "transparent" + text: `${modelData.namespace}` + radius: Appearance.rounding.windowRounding + } + } + + // Content regions + Repeater { + model: ScriptModel { + values: { + if (root.phase === RegionSelection.Phase.Select && root.enableContentRegions) { + return root.imageRegions + } else { + return [] + } + } + } + delegate: TargetRegion { + z: 4 + required property var modelData + clientDimensions: modelData + targeted: !root.draggedAway && + (root.targetedRegionX === modelData.at[0] + && root.targetedRegionY === modelData.at[1] + && root.targetedRegionWidth === modelData.size[0] + && root.targetedRegionHeight === modelData.size[1]) + + opacity: root.draggedAway ? 0 : root.contentRegionOpacity + borderColor: root.imageBorderColor + fillColor: targeted ? root.imageFillColor : "transparent" + text: Translation.tr("Content region") + } + } + + // Controls + Row { + id: regionSelectionControls + z: 10 + visible: root.phase === RegionSelection.Phase.Select + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: -height + } + opacity: 0 + Connections { + target: root + function onVisibleChanged() { + if (!visible) return; + regionSelectionControls.anchors.bottomMargin = 8; + regionSelectionControls.opacity = 1; + } + } + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + Behavior on anchors.bottomMargin { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + spacing: 6 + + OptionsToolbar { + Synchronizer on action { + property alias source: root.action + } + Synchronizer on selectionMode { + property alias source: root.selectionMode + } + onDismiss: root.dismiss(); + } + Item { + anchors { + verticalCenter: parent.verticalCenter + } + implicitWidth: closeFab.implicitWidth + implicitHeight: closeFab.implicitHeight + StyledRectangularShadow { + target: closeFab + radius: closeFab.buttonRadius + } + FloatingActionButton { + id: closeFab + baseSize: 48 + iconText: "close" + onClicked: root.dismiss(); + StyledToolTip { + text: Translation.tr("Close") + } + colBackground: Appearance.colors.colTertiaryContainer + colBackgroundHover: Appearance.colors.colTertiaryContainerHover + colRipple: Appearance.colors.colTertiaryContainerActive + colOnBackground: Appearance.colors.colOnTertiaryContainer + } + } + } + } } diff --git a/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelector.qml b/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelector.qml index 40440366a..61525e527 100644 --- a/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelector.qml +++ b/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelector.qml @@ -65,12 +65,16 @@ Scope { function record() { root.action = RegionSelection.SnipAction.Record root.selectionMode = RegionSelection.SelectionMode.RectCorners + // If already open then re-trigger to stop recording + if (GlobalStates.regionSelectorOpen) GlobalStates.regionSelectorOpen = false GlobalStates.regionSelectorOpen = true } function recordWithSound() { root.action = RegionSelection.SnipAction.RecordWithSound root.selectionMode = RegionSelection.SelectionMode.RectCorners + // If already open then re-trigger to stop recording + if (GlobalStates.regionSelectorOpen) GlobalStates.regionSelectorOpen = false GlobalStates.regionSelectorOpen = true }