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

This commit is contained in:
Yosuke Nishiyama
2025-10-22 23:26:44 +01:00
committed by GitHub
76 changed files with 1818 additions and 802 deletions
+3 -2
View File
@@ -60,6 +60,7 @@ bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call T
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)
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)
bind = Super+Shift, A, global, quickshell:regionSearch # Google Lens
# 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
@@ -225,8 +226,8 @@ binde = Super, Equal, exec, qs -c $qsConfig ipc call zoom zoomIn # Zoom in
binde = Super, Minus, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh decrease 0.1 # [hidden] Zoom out
binde = Super, Equal, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh increase 0.1 # [hidden] Zoom in
# Zoom with keypad
binde = Super, code:82, exec, qs -c $qsConfig ipc call zoom zoomOut # Zoom out
binde = Super, code:86, exec, qs -c $qsConfig ipc call zoom zoomIn # Zoom in
binde = Super, code:82, exec, qs -c $qsConfig ipc call zoom zoomOut # [hidden] Zoom out
binde = Super, code:86, exec, qs -c $qsConfig ipc call zoom zoomIn # [hidden] Zoom in
binde = Super, code:82, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh decrease 0.1 # [hidden] Zoom out
binde = Super, code:86, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh increase 0.1 # [hidden] Zoom in
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="mdi-gentoo"
viewBox="0 0 20 20"
version="1.1"
sodipodi:docname="Pictogrammers-Material-Gentoo.svg"
width="20"
height="20"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="20.875"
inkscape:cx="9.508982"
inkscape:cy="9.9640719"
inkscape:window-width="1327"
inkscape:window-height="1068"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="mdi-gentoo" />
<path
d="m 8.2792823,-5.5983568e-4 c -0.35,0 -0.71,0.0299999957 -1.05,0.0999999957 -3.62,0.66 -6.17,3.79000004 -6.38,5.86000004 -0.11,1.01 0.44,1.77 0.74,2.1 0.81,0.91 2.44,1.6 3.48,2.1699998 -1.51,1.27 -2.2,1.91 -2.88,2.63 -1.02,1.07 -1.74,2.24 -1.74,3.09 0,0.27 -0.05,1.14 0.31,1.82 0.13,0.26 0.51,1.12 1.65,1.76 0.73,0.41 1.76,0.56 2.78,0.42 3.14,-0.45 7.3499997,-3.12 10.3599997,-5.6 1.91,-1.58 3.31,-3.12 3.71,-3.85 0.33,-0.6299998 0.37,-1.7199998 0.18,-2.4099998 -0.54,-1.95 -4.91,-5.94 -8.48,-7.54000004 -0.82,-0.37 -1.7599997,-0.54999999568 -2.6799997,-0.54999999568 m 1.06,2.91000003568 c 0.25,0 0.47,0.03 0.66,0.09 1.1499997,0.3 3.0799997,1.68 2.9099997,2.94 -0.23,1.66 -1.68,2.33 -3.3499997,2.09 -0.98,-0.13 -2.93,-1.23 -2.78,-3.14 0.11,-1.49 1.52,-1.99 2.56,-1.98 m -0.02,1.74 c -0.27,0 -0.48,0.06 -0.58,0.22 -0.47,0.72 -0.24,1.22 0.18,1.55 0.15,-0.38 1.7899997,0.03 1.8299997,0.37 1.42,-1.07 -0.39,-2.13 -1.4299997,-2.14 z"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+2 -1
View File
@@ -36,6 +36,7 @@ ApplicationWindow {
Component.onCompleted: {
Config.readWriteDelay = 0;
Config.blockWrites = true;
MaterialThemeLoader.reapplyTheme();
}
@@ -90,8 +91,8 @@ ApplicationWindow {
}
onClicked: {
Quickshell.execDetached(["killall", ...conflictGroup.programs])
conflictGroup.visible = false
conflictGroup.alwaysSelected()
conflictGroup.visible = false
}
}
RippleButton {
@@ -13,7 +13,7 @@ import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import "./cookieClock"
import qs.modules.background.cookieClock
Variants {
id: root
@@ -9,8 +9,8 @@ import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import "./dateIndicator"
import "./minuteMarks"
import qs.modules.background.cookieClock.dateIndicator
import qs.modules.background.cookieClock.minuteMarks
Item {
id: root
@@ -1,4 +1,4 @@
import "./weather"
import qs.modules.bar.weather
import QtQuick
import QtQuick.Layouts
import Quickshell
@@ -25,7 +25,7 @@ Item {
visible: Config.options.bar.utilButtons.showScreenSnip
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("screenshot.qml")])
onClicked: Hyprland.dispatch("global quickshell:regionScreenshot")
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 1
@@ -4,7 +4,7 @@ import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import "../"
import qs.modules.bar
StyledPopup {
id: root
@@ -101,4 +101,4 @@ StyledPopup {
}
}
}
}
}
@@ -10,6 +10,7 @@ Singleton {
property alias options: configOptionsJsonAdapter
property bool ready: false
property int readWriteDelay: 50 // milliseconds
property bool blockWrites: false
function setNestedValue(nestedKey, value) {
let keys = nestedKey.split(".");
@@ -63,6 +64,7 @@ Singleton {
id: configFileView
path: root.filePath
watchChanges: true
blockWrites: root.blockWrites
onFileChanged: fileReloadTimer.restart()
onAdapterUpdated: fileWriteTimer.restart()
onLoaded: root.ready = true
@@ -302,6 +304,9 @@ Singleton {
property string to: "06:30" // Format: "HH:mm", 24-hour time
property int colorTemperature: 5000
}
property JsonObject antiFlashbang: JsonObject {
property bool enable: false
}
}
property JsonObject lock: JsonObject {
@@ -349,6 +354,24 @@ 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.3
property real contentRegionOpacity: 0.8
}
property JsonObject rect: JsonObject {
property bool showAimLines: true
}
property JsonObject circle: JsonObject {
property int strokeWidth: 6
property int padding: 30
}
}
property JsonObject resources: JsonObject {
property int updateInterval: 3000
}
@@ -368,6 +391,10 @@ Singleton {
property string shellCommand: "$"
property string webSearch: "?"
}
property JsonObject imageSearch: JsonObject {
property string imageSearchEngineBaseUrl: "https://lens.google.com/uploadbyurl?url="
property bool useCircleSelection: false
}
}
property JsonObject sidebar: JsonObject {
@@ -454,10 +481,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
@@ -1,6 +1,6 @@
pragma Singleton
import Quickshell
import "./fuzzysort.js" as FuzzySort
import "fuzzysort.js" as FuzzySort
/**
* Wrapper for FuzzySort to play nicely with Quickshell's imports
@@ -1,6 +1,6 @@
pragma Singleton
import Quickshell
import "./levendist.js" as Levendist
import "levendist.js" as Levendist
/**
* Wrapper for levendist.js to play nicely with Quickshell's imports
@@ -7,6 +7,7 @@ import QtQuick.Controls
RippleButton {
id: root
property string buttonIcon
property alias iconSize: iconWidget.iconSize
Layout.fillWidth: true
implicitHeight: contentItem.implicitHeight + 8 * 2
@@ -17,6 +18,7 @@ RippleButton {
contentItem: RowLayout {
spacing: 10
OptionalMaterialSymbol {
id: iconWidget
icon: root.buttonIcon
opacity: root.enabled ? 1 : 0.4
iconSize: Appearance.font.pixelSize.larger
@@ -22,6 +22,8 @@ Button {
property bool bounce: true
property real baseWidth: contentItem.implicitWidth + horizontalPadding * 2
property real baseHeight: contentItem.implicitHeight + verticalPadding * 2
property bool enableImplicitWidthAnimation: true
property bool enableImplicitHeightAnimation: true
property real clickedWidth: baseWidth + (isAtSide ? 10 : 20)
property real clickedHeight: baseHeight
property var parentGroup: root.parent
@@ -61,10 +63,12 @@ Button {
}
Behavior on implicitWidth {
enabled: root.enableImplicitWidthAnimation
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
}
Behavior on implicitHeight {
enabled: root.enableImplicitHeightAnimation
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
}
@@ -75,7 +79,9 @@ Button {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
property alias mouseArea: buttonMouseArea
MouseArea {
id: buttonMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
@@ -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
}
}
}
@@ -0,0 +1,21 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
ToolbarButton {
id: iconBtn
implicitWidth: height
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 22
text: iconBtn.text
color: iconBtn.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
}
}
@@ -1,5 +1,5 @@
import qs.modules.common
import "./notification_utils.js" as NotificationUtils
import "notification_utils.js" as NotificationUtils
import Qt5Compat.GraphicalEffects
import QtQuick
import Quickshell
@@ -1,7 +1,7 @@
import qs.services
import qs.modules.common
import qs.modules.common.functions
import "./notification_utils.js" as NotificationUtils
import "notification_utils.js" as NotificationUtils
import QtQuick
import QtQuick.Layouts
import Quickshell
@@ -73,12 +73,13 @@ Slider {
component TrackDot: Rectangle {
required property real value
property real normalizedValue: (value - root.from) / (root.to - root.from)
anchors.verticalCenter: parent.verticalCenter
x: root.handleMargins + (value * root.effectiveDraggingWidth) - (root.trackDotSize / 2)
x: root.handleMargins + (normalizedValue * root.effectiveDraggingWidth) - (root.trackDotSize / 2)
width: root.trackDotSize
height: root.trackDotSize
radius: Appearance.rounding.full
color: value > root.visualPosition ? root.dotColor : root.dotColorHighlighted
color: normalizedValue > root.visualPosition ? root.dotColor : root.dotColorHighlighted
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
@@ -50,7 +50,7 @@ Rectangle {
property real targetY: root.height / 2 - root.backgroundHeight / 2
y: root.show ? targetY : (targetY - root.backgroundAnimationMovementDistance)
implicitWidth: 350
implicitHeight: 0
implicitHeight: contentColumn.implicitHeight + dialogBackground.radius * 2
Behavior on implicitHeight {
NumberAnimation {
id: dialogBackgroundHeightAnimation
@@ -0,0 +1,43 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Widgets
Column {
id: root
property alias text: sliderName.text
property alias from: sliderWidget.from
property alias to: sliderWidget.to
property alias value: sliderWidget.value
property alias tooltipContent: sliderWidget.tooltipContent
property alias stopIndicatorValues: sliderWidget.stopIndicatorValues
signal moved()
spacing: -2
ContentSubsectionLabel {
id: sliderName
visible: text?.length > 0
text: ""
anchors {
left: parent.left
right: parent.right
}
}
StyledSlider {
id: sliderWidget
anchors {
left: parent.left
right: parent.right
leftMargin: 4
rightMargin: leftMargin
}
configuration: StyledSlider.Configuration.S
onMoved: root.moved()
}
}
@@ -234,26 +234,26 @@ MouseArea {
color: (Battery.isLow && !Battery.isCharging) ? Appearance.colors.colError : Appearance.colors.colOnSurfaceVariant
}
ActionToolbarIconButton {
IconToolbarButton {
id: sleepButton
onClicked: Session.suspend()
text: "dark_mode"
}
PasswordGuardedActionToolbarIconButton {
PasswordGuardedIconToolbarButton {
id: powerButton
text: "power_settings_new"
targetAction: LockContext.ActionEnum.Poweroff
}
PasswordGuardedActionToolbarIconButton {
PasswordGuardedIconToolbarButton {
id: rebootButton
text: "restart_alt"
targetAction: LockContext.ActionEnum.Reboot
}
}
component PasswordGuardedActionToolbarIconButton: ActionToolbarIconButton {
component PasswordGuardedIconToolbarButton: IconToolbarButton {
id: guardedBtn
required property var targetAction
@@ -273,24 +273,6 @@ MouseArea {
}
}
component ActionToolbarIconButton: ToolbarButton {
id: iconBtn
implicitWidth: height
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 24
text: iconBtn.text
color: iconBtn.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
}
}
component IconAndTextPair: Row {
id: pair
required property string icon
@@ -159,7 +159,13 @@ Scope {
}
Item { // No player placeholder
Layout.fillWidth: true
Layout.alignment: {
if (mediaControlsRoot.anchors.left) return Qt.AlignLeft;
if (mediaControlsRoot.anchors.right) return Qt.AlignRight;
return Qt.AlignHCenter;
}
Layout.leftMargin: Appearance.sizes.hyprlandGapsOut
Layout.rightMargin: Appearance.sizes.hyprlandGapsOut
visible: root.meaningfulPlayers.length === 0
implicitWidth: placeholderBackground.implicitWidth + Appearance.sizes.elevationMargin
implicitHeight: placeholderBackground.implicitHeight + Appearance.sizes.elevationMargin
@@ -2,7 +2,7 @@ import qs.services
import QtQuick
import Quickshell
import Quickshell.Hyprland
import "../"
import qs.modules.onScreenDisplay
OsdValueIndicator {
id: root
@@ -1,6 +1,6 @@
import qs.services
import QtQuick
import "../"
import qs.modules.onScreenDisplay
OsdValueIndicator {
id: osdValues
@@ -0,0 +1,49 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Shapes
import Quickshell
Item {
id: root
required property color color
required property color overlayColor
required property list<point> points
property int strokeWidth: Config.options.regionSelector.circle.strokeWidth
function updatePoints() {
if (!root.dragging) return;
root.points.push({ x: root.mouseX, y: root.mouseY });
}
Rectangle {
id: darkenOverlay
z: 1
anchors.fill: parent
color: root.overlayColor
}
Shape {
id: shape
z: 2
anchors.fill: parent
layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: shapePath
strokeWidth: root.strokeWidth
pathHints: ShapePath.PathLinear
fillColor: "transparent"
strokeColor: root.color
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
PathPolyline {
path: root.points
}
}
}
}
@@ -0,0 +1,90 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
Item {
id: root
required property real regionX
required property real regionY
required property real regionWidth
required property real regionHeight
required property real mouseX
required property real mouseY
required property color color
required property color overlayColor
property bool showAimLines: Config.options.regionSelector.rect.showAimLines
// Overlay to darken screen
// Base dark overlay around region
Rectangle {
id: darkenOverlay
z: 1
anchors {
left: parent.left
top: parent.top
leftMargin: root.regionX - darkenOverlay.border.width
topMargin: root.regionY - darkenOverlay.border.width
}
width: root.regionWidth + darkenOverlay.border.width * 2
height: root.regionHeight + darkenOverlay.border.width * 2
color: "transparent"
border.color: root.overlayColor
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
}
StyledText {
z: 2
anchors {
top: selectionBorder.bottom
right: selectionBorder.right
margins: 8
}
color: root.color
text: `${Math.round(root.regionWidth)} x ${Math.round(root.regionHeight)}`
}
// Coord lines
Rectangle { // Vertical
visible: root.showAimLines
opacity: 0.2
z: 2
x: root.mouseX
anchors {
top: parent.top
bottom: parent.bottom
}
width: 1
color: root.color
}
Rectangle { // Horizontal
visible: root.showAimLines
opacity: 0.2
z: 2
y: root.mouseY
anchors {
left: parent.left
right: parent.right
}
height: 1
color: root.color
}
}
@@ -0,0 +1,601 @@
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
PanelWindow {
id: root
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
}
// TODO: Ask: sidebar AI; Ocr: tesseract
enum SnipAction { Copy, Edit, Search }
enum SelectionMode { RectCorners, Circle }
property var action: RegionSelection.SnipAction.Copy
property var selectionMode: RegionSelection.SelectionMode.RectCorners
signal dismiss()
property string screenshotDir: Directories.screenshotTemp
property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl
property string fileUploadApiEndpoint: "https://uguu.se/upload"
property color overlayColor: "#88111111"
property color genericContentColor: Qt.alpha(root.overlayColor, 0.9)
property color genericContentForeground: "#ddffffff"
property color brightText: Appearance.m3colors.darkmode ? Appearance.colors.colOnLayer0 : Appearance.colors.colLayer0
property color brightSecondary: Appearance.m3colors.darkmode ? Appearance.colors.colSecondary : Appearance.colors.colOnSecondary
property color brightTertiary: Appearance.m3colors.darkmode ? Appearance.colors.colTertiary : Qt.lighter(Appearance.colors.colPrimary)
property color selectionBorderColor: ColorUtils.mix(brightText, brightSecondary, 0.5)
property color selectionFillColor: "#33ffffff"
property color windowBorderColor: brightSecondary
property color windowFillColor: ColorUtils.transparentize(windowBorderColor, 0.85)
property color imageBorderColor: brightTertiary
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
if (a.floating === b.floating) return 0;
return a.floating ? -1 : 1;
})
readonly property var layers: HyprlandData.layers
readonly property real falsePositivePreventionRatio: 0.5
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 list<point> points: []
property var mouseButton: null
property var imageRegions: []
readonly property list<var> windowRegions: filterWindowRegionsByLayers(
root.windows.filter(w => w.workspace.id === root.activeWorkspaceId),
root.layerRegions
).map(window => {
return {
at: [window.at[0] - root.monitorOffsetX, window.at[1] - root.monitorOffsetY],
size: [window.size[0], window.size[1]],
class: window.class,
title: window.title,
}
})
readonly property list<var> layerRegions: {
const layersOfThisMonitor = root.layers[root.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] - root.monitorOffsetX, layer.at[1] - root.monitorOffsetY],
size: layer.size,
namespace: layer.namespace,
}
});
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 bool contentRegionOpacity: Config.options.regionSelector.targetRegions.contentRegionOpacity
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] }
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 = root.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) {
root.targetedRegionX = clickedRegion.at[0];
root.targetedRegionY = clickedRegion.at[1];
root.targetedRegionWidth = clickedRegion.size[0];
root.targetedRegionHeight = clickedRegion.size[1];
return;
}
// Layer regions
const clickedLayer = root.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) {
root.targetedRegionX = clickedLayer.at[0];
root.targetedRegionY = clickedLayer.at[1];
root.targetedRegionWidth = clickedLayer.size[0];
root.targetedRegionHeight = clickedLayer.size[1];
return;
}
// Window regions
const clickedWindow = root.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) {
root.targetedRegionX = clickedWindow.at[0];
root.targetedRegionY = clickedWindow.at[1];
root.targetedRegionWidth = clickedWindow.size[0];
root.targetedRegionHeight = clickedWindow.size[1];
return;
}
root.targetedRegionX = -1;
root.targetedRegionY = -1;
root.targetedRegionWidth = 0;
root.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(root.screen.name)}' '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`]
onExited: (exitCode, exitStatus) => {
root.visible = true;
imageDetectionProcess.running = true;
}
}
Process {
id: imageDetectionProcess
command: ["bash", "-c", `${Directories.scriptPath}/images/find-regions-venv.sh `
+ `--hyprctl `
+ `--image '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}' `
+ `--max-width ${Math.round(root.screen.width * root.falsePositivePreventionRatio)} `
+ `--max-height ${Math.round(root.screen.height * root.falsePositivePreventionRatio)} `]
stdout: StdioCollector {
id: imageDimensionCollector
onStreamFinished: {
imageRegions = filterImageRegions(
JSON.parse(imageDimensionCollector.text),
root.windowRegions
);
}
}
}
function snip() {
// Validity check
if (root.regionWidth <= 0 || root.regionHeight <= 0) {
console.warn("[Region Selector] Invalid region size, skipping snip.");
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;
}
// Set command for action
const cropBase = `magick ${StringUtils.shellSingleQuoteEscape(root.screenshotPath)} `
+ `-crop ${root.regionWidth * root.monitorScale}x${root.regionHeight * root.monitorScale}+${root.regionX * root.monitorScale}+${root.regionY * root.monitorScale}`
const cropToStdout = `${cropBase} -`
const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`
const cleanup = `rm '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`
const uploadAndGetUrl = (filePath) => {
return `curl -sF files[]=@'${StringUtils.shellSingleQuoteEscape(filePath)}' ${root.fileUploadApiEndpoint} | jq -r '.files[0].url'`
}
switch (root.action) {
case RegionSelection.SnipAction.Copy:
snipProc.command = ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`]
break;
case RegionSelection.SnipAction.Edit:
snipProc.command = ["bash", "-c", `${cropToStdout} | swappy -f - && ${cleanup}`]
break;
case RegionSelection.SnipAction.Search:
snipProc.command = ["bash", "-c", `${cropInPlace} && xdg-open "${root.imageSearchEngineBaseUrl}$(${uploadAndGetUrl(root.screenshotPath)})"`]
break;
default:
console.warn("[Region Selector] Unknown snip action, skipping snip.");
root.dismiss();
return;
}
// Image post-processing
snipProc.startDetached();
root.dismiss();
}
Process {
id: snipProc
}
ScreencopyView {
anchors.fill: parent
live: false
captureSource: root.screen
focus: root.visible
Keys.onPressed: (event) => { // Esc to close
if (event.key === Qt.Key_Escape) {
root.dismiss();
}
}
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) => {
// Circle dragging?
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;
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
else if (root.draggingX === root.dragStartX && root.draggingY === root.dragStartY) {
if (root.targetedRegionValid()) {
root.setRegionToTargeted();
}
}
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
}
}
// Window regions
Repeater {
model: ScriptModel {
values: root.enableWindowRegions ? root.windowRegions : []
}
delegate: TargetRegion {
z: 2
required property var 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])
colBackground: root.genericContentColor
colForeground: root.genericContentForeground
opacity: root.draggedAway ? 0 : root.targetRegionOpacity
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: root.enableLayerRegions ? root.layerRegions : []
}
delegate: TargetRegion {
z: 3
required property var modelData
targeted: !root.draggedAway &&
(root.targetedRegionX === modelData.at[0]
&& root.targetedRegionY === modelData.at[1]
&& root.targetedRegionWidth === modelData.size[0]
&& root.targetedRegionHeight === modelData.size[1])
colBackground: root.genericContentColor
colForeground: root.genericContentForeground
opacity: root.draggedAway ? 0 : root.targetRegionOpacity
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
}
}
// Content regions
Repeater {
model: ScriptModel {
values: root.enableContentRegions ? root.imageRegions : []
}
delegate: TargetRegion {
z: 4
required property var modelData
targeted: !root.draggedAway &&
(root.targetedRegionX === modelData.at[0]
&& root.targetedRegionY === modelData.at[1]
&& root.targetedRegionWidth === modelData.size[0]
&& root.targetedRegionHeight === modelData.size[1])
colBackground: root.genericContentColor
colForeground: root.genericContentForeground
opacity: root.draggedAway ? 0 : root.contentRegionOpacity
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: 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
animateChange: true
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();
}
}
}
}
}
@@ -16,83 +16,13 @@ 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
}
}
}
}
property var action: RegionSelection.SnipAction.Copy
property var selectionMode: RegionSelection.SelectionMode.RectCorners
Variants {
model: Quickshell.screens
@@ -101,478 +31,28 @@ Scope {
required property var modelData
active: GlobalStates.regionSelectorOpen
sourceComponent: PanelWindow {
id: panelWindow
sourceComponent: RegionSelection {
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"
}
}
}
}
onDismiss: root.dismiss()
action: root.action
selectionMode: root.selectionMode
}
}
}
function screenshot() {
root.action = RegionSelection.SnipAction.Copy
root.selectionMode = RegionSelection.SelectionMode.RectCorners
GlobalStates.regionSelectorOpen = true
}
function search() {
root.action = RegionSelection.SnipAction.Search
if (Config.options.search.imageSearch.useCircleSelection) {
root.selectionMode = RegionSelection.SelectionMode.Circle
} else {
root.selectionMode = RegionSelection.SelectionMode.RectCorners
}
GlobalStates.regionSelectorOpen = true
}
@@ -582,14 +62,19 @@ Scope {
function screenshot() {
root.screenshot()
}
function search() {
root.search()
}
}
GlobalShortcut {
name: "regionScreenshot"
description: "Takes a screenshot of the selected region"
onPressed: {
root.screenshot()
}
onPressed: root.screenshot()
}
GlobalShortcut {
name: "regionSearch"
description: "Searches the selected region"
onPressed: root.search()
}
}
@@ -0,0 +1,68 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
import Quickshell.Widgets
Rectangle {
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
property color fillColor: "transparent"
property string text: ""
property real textPadding: 10
z: 2
color: fillColor
border.color: borderColor
border.width: targeted ? 3 : 1
radius: 4
Loader {
anchors {
top: parent.top
left: parent.left
topMargin: root.textPadding
leftMargin: root.textPadding
}
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
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: root.text
color: root.colForeground
}
}
}
}
}
@@ -64,7 +64,6 @@ Scope {
id: sidebarCornerOpenInteractionLoader
active: {
if (!Config.options.sidebar.cornerOpen.enable) return false;
if (!Config.options.bar.vertical && Config.options.sidebar.cornerOpen.bottom == Config.options.bar.bottom) return false;
if (cornerPanelWindow.fullscreen) return false;
return (Config.options.sidebar.cornerOpen.bottom == cornerWidget.isBottom);
}
@@ -574,6 +574,100 @@ 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("Rectangular selection")
ConfigSwitch {
buttonIcon: "point_scan"
text: Translation.tr("Show aim lines")
checked: Config.options.regionSelector.rect.showAimLines
onCheckedChanged: {
Config.options.regionSelector.rect.showAimLines = checked;
}
}
}
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 +942,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")
@@ -3,7 +3,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "./aiChat/"
import qs.modules.sidebarLeft.aiChat
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -3,7 +3,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "./anime/"
import qs.modules.sidebarLeft.anime
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -10,7 +10,6 @@ import Quickshell.Hyprland
Scope { // Scope
id: root
property int sidebarPadding: 15
property bool detach: false
property Component contentComponent: SidebarLeftContent {}
property Item sidebarContent
@@ -9,6 +9,7 @@ import Qt5Compat.GraphicalEffects
Item {
id: root
required property var scopeRoot
property int sidebarPadding: 10
anchors.fill: parent
property bool aiChatEnabled: Config.options.policies.ai !== 0
property bool translatorEnabled: Config.options.sidebar.translator.enable
@@ -2,7 +2,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "./translator/"
import qs.modules.sidebarLeft.translator
import QtQuick
import QtQuick.Layouts
import Quickshell
@@ -3,8 +3,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "../"
import qs.services
import qs.modules.sidebarLeft
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -287,4 +286,4 @@ Rectangle {
}
}
}
}
}
@@ -1,9 +1,9 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import "./calendar"
import "./todo"
import "./pomodoro"
import qs.modules.sidebarRight.calendar
import qs.modules.sidebarRight.todo
import qs.modules.sidebarRight.pomodoro
import QtQuick
import QtQuick.Layouts
@@ -248,4 +248,4 @@ Rectangle {
anchors.margins: 5
}
}
}
}
@@ -1,8 +1,8 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import "./notifications"
import "./volumeMixer"
import qs.modules.sidebarRight.notifications
import qs.modules.sidebarRight.volumeMixer
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
@@ -9,21 +9,24 @@ import Quickshell
import Quickshell.Bluetooth
import Quickshell.Hyprland
import "./quickToggles/"
import "./quickToggles/classicStyle/"
import "./wifiNetworks/"
import "./bluetoothDevices/"
import "./volumeMixer/"
import qs.modules.sidebarRight.quickToggles
import qs.modules.sidebarRight.quickToggles.classicStyle
import qs.modules.sidebarRight.bluetoothDevices
import qs.modules.sidebarRight.nightLight
import qs.modules.sidebarRight.volumeMixer
import qs.modules.sidebarRight.wifiNetworks
Item {
id: root
property int sidebarWidth: Appearance.sizes.sidebarWidth
property int sidebarPadding: 12
property int sidebarPadding: 10
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
property bool showWifiDialog: false
property bool showBluetoothDialog: false
property bool showAudioOutputDialog: false
property bool showAudioInputDialog: false
property bool showBluetoothDialog: false
property bool showNightLightDialog: false
property bool showWifiDialog: false
property bool editMode: false
Connections {
@@ -62,7 +65,8 @@ Item {
SystemButtonRow {
Layout.fillHeight: false
Layout.margins: 10
Layout.fillWidth: true
// Layout.margins: 10
Layout.topMargin: 5
Layout.bottomMargin: 0
}
@@ -108,18 +112,20 @@ Item {
}
ToggleDialog {
id: wifiDialogLoader
shownPropertyString: "showWifiDialog"
dialog: WifiDialog {}
onShownChanged: {
if (!shown) return;
Network.enableWifi();
Network.rescanWifi();
shownPropertyString: "showAudioOutputDialog"
dialog: VolumeDialog {
isSink: true
}
}
ToggleDialog {
shownPropertyString: "showAudioInputDialog"
dialog: VolumeDialog {
isSink: false
}
}
ToggleDialog {
id: bluetoothDialogLoader
shownPropertyString: "showBluetoothDialog"
dialog: BluetoothDialog {}
onShownChanged: {
@@ -129,23 +135,21 @@ Item {
Bluetooth.defaultAdapter.enabled = true;
Bluetooth.defaultAdapter.discovering = true;
}
}
}
ToggleDialog {
id: audioOutputDialogLoader
shownPropertyString: "showAudioOutputDialog"
dialog: VolumeDialog {
isSink: true
}
shownPropertyString: "showNightLightDialog"
dialog: NightLightDialog {}
}
ToggleDialog {
id: audioInputDialogLoader
shownPropertyString: "showAudioInputDialog"
dialog: VolumeDialog {
isSink: false
shownPropertyString: "showWifiDialog"
dialog: WifiDialog {}
onShownChanged: {
if (!shown) return;
Network.enableWifi();
Network.rescanWifi();
}
}
@@ -185,45 +189,72 @@ Item {
active: Config.options.sidebar.quickToggles.style === styleName
Connections {
target: quickPanelImplLoader.item
function onOpenWifiDialog() {
root.showWifiDialog = true;
}
function onOpenBluetoothDialog() {
root.showBluetoothDialog = true;
}
function onOpenAudioOutputDialog() {
root.showAudioOutputDialog = true;
}
function onOpenAudioInputDialog() {
root.showAudioInputDialog = true;
}
function onOpenBluetoothDialog() {
root.showBluetoothDialog = true;
}
function onOpenNightLightDialog() {
root.showNightLightDialog = true;
}
function onOpenWifiDialog() {
root.showWifiDialog = true;
}
}
}
component SystemButtonRow: RowLayout {
spacing: 10
component SystemButtonRow: Item {
implicitHeight: Math.max(uptimeContainer.implicitHeight, systemButtonsRow.implicitHeight)
CustomIcon {
id: distroIcon
width: 25
height: 25
source: SystemInfo.distroIcon
colorize: true
color: Appearance.colors.colOnLayer0
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer0
text: Translation.tr("Up %1").arg(DateTime.uptime)
textFormat: Text.MarkdownText
}
Item {
Layout.fillWidth: true
Rectangle {
id: uptimeContainer
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
color: Appearance.colors.colLayer1
radius: height / 2
implicitWidth: uptimeRow.implicitWidth + 24
implicitHeight: uptimeRow.implicitHeight + 8
Row {
id: uptimeRow
anchors.centerIn: parent
spacing: 8
CustomIcon {
id: distroIcon
anchors.verticalCenter: parent.verticalCenter
width: 25
height: 25
source: SystemInfo.distroIcon
colorize: true
color: Appearance.colors.colOnLayer0
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer0
text: Translation.tr("Up %1").arg(DateTime.uptime)
textFormat: Text.MarkdownText
}
}
}
ButtonGroup {
id: systemButtonsRow
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
}
color: Appearance.colors.colLayer1
padding: 4
QuickToggleButton {
toggled: root.editMode
visible: Config.options.sidebar.quickToggles.style === "android"
@@ -1,7 +1,7 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import "./calendar_layout.js" as CalendarLayout
import "calendar_layout.js" as CalendarLayout
import QtQuick
import QtQuick.Layouts
@@ -0,0 +1,157 @@
import qs
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 Quickshell.Io
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
WindowDialog {
id: root
property var screen: root.QsWindow.window?.screen
property var brightnessMonitor: Brightness.getMonitorForScreen(screen)
WindowDialogTitle {
text: Translation.tr("Eye protection")
}
WindowDialogSectionHeader {
text: Translation.tr("Night Light")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
Column {
id: nightLightColumn
Layout.topMargin: -16
Layout.fillWidth: true
ConfigSwitch {
anchors {
left: parent.left
right: parent.right
}
iconSize: Appearance.font.pixelSize.larger
buttonIcon: "lightbulb"
text: Translation.tr("Enable now")
checked: Hyprsunset.active
onCheckedChanged: {
Hyprsunset.toggle(checked)
}
}
ConfigSwitch {
anchors {
left: parent.left
right: parent.right
}
iconSize: Appearance.font.pixelSize.larger
buttonIcon: "night_sight_auto"
text: Translation.tr("Automatic")
checked: Config.options.light.night.automatic
onCheckedChanged: {
Config.options.light.night.automatic = checked;
}
}
WindowDialogSlider {
anchors {
left: parent.left
right: parent.right
leftMargin: 4
rightMargin: 4
}
text: Translation.tr("Color temperature")
from: 1000
to: 20000
stopIndicatorValues: [6000, to]
value: Config.options.light.night.colorTemperature
onMoved: Config.options.light.night.colorTemperature = value
tooltipContent: `${Math.round(value)}K`
}
}
WindowDialogSectionHeader {
text: Translation.tr("Anti-flashbang (experimental)")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
Column {
id: antiFlashbangColumn
Layout.topMargin: -16
Layout.fillWidth: true
ConfigSwitch {
anchors {
left: parent.left
right: parent.right
}
iconSize: Appearance.font.pixelSize.larger
buttonIcon: "destruction"
text: Translation.tr("Enable")
checked: Config.options.light.antiFlashbang.enable
onCheckedChanged: {
Config.options.light.antiFlashbang.enable = checked;
}
StyledToolTip {
text: Translation.tr("Example use case: eroge on one workspace, dark Discord window on another")
}
}
}
WindowDialogSectionHeader {
text: Translation.tr("Brightness")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
Column {
id: brightnessColumn
Layout.topMargin: -16
Layout.fillWidth: true
Layout.fillHeight: true
WindowDialogSlider {
anchors {
left: parent.left
right: parent.right
leftMargin: 4
rightMargin: 4
}
// text: Translation.tr("Brightness")
value: root.brightnessMonitor.brightness
onMoved: root.brightnessMonitor.setBrightness(value)
}
}
WindowDialogButtonRow {
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
DialogButton {
buttonText: Translation.tr("Done")
onClicked: root.dismiss()
}
}
}
@@ -7,8 +7,9 @@ Rectangle {
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
signal openWifiDialog()
signal openBluetoothDialog()
signal openAudioOutputDialog()
signal openAudioInputDialog()
signal openBluetoothDialog()
signal openNightLightDialog()
signal openWifiDialog()
}
@@ -6,30 +6,20 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import "./androidStyle/"
import qs.modules.sidebarRight.quickToggles.androidStyle
AbstractQuickPanel {
id: root
property bool editMode: false
Layout.fillWidth: true
implicitHeight: (editMode ? contentItem.implicitHeight : usedRows.implicitHeight) + root.padding * 2
// Sizes
implicitHeight: (editMode ? contentItem.implicitHeight : usedRows.implicitHeight) + root.padding * 2
Behavior on implicitHeight {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
property real spacing: 6
property real padding: 6
readonly property list<string> availableToggleTypes: ["network", "bluetooth", "idleInhibitor", "easyEffects", "nightLight", "darkMode", "cloudflareWarp", "gameMode", "screenSnip", "colorPicker", "onScreenKeyboard", "mic", "audio", "notifications", "powerProfile"]
readonly property int columns: Config.options.sidebar.quickToggles.android.columns
readonly property list<var> toggles: Config.options.sidebar.quickToggles.android.toggles
readonly property list<var> toggleRows: toggleRowsForList(toggles)
readonly property list<var> unusedToggles: {
const types = availableToggleTypes.filter(type => !toggles.some(toggle => (toggle && toggle.type === type)))
return types.map(type => { return { type: type, size: 1 } })
}
readonly property list<var> unusedToggleRows: toggleRowsForList(unusedToggles)
readonly property real baseCellWidth: {
// This is the wrong calculation, but it looks correct in reality???
// (theoretically spacing should be multiplied by 1 column less)
@@ -38,6 +28,17 @@ AbstractQuickPanel {
}
readonly property real baseCellHeight: 56
// Toggles
readonly property list<string> availableToggleTypes: ["network", "bluetooth", "idleInhibitor", "easyEffects", "nightLight", "darkMode", "cloudflareWarp", "gameMode", "screenSnip", "colorPicker", "onScreenKeyboard", "mic", "audio", "notifications", "powerProfile"]
readonly property int columns: Config.options.sidebar.quickToggles.android.columns
readonly property list<var> toggles: Config.ready ? Config.options.sidebar.quickToggles.android.toggles : []
readonly property list<var> toggleRows: toggleRowsForList(toggles)
readonly property list<var> unusedToggles: {
const types = availableToggleTypes.filter(type => !toggles.some(toggle => (toggle && toggle.type === type)))
return types.map(type => { return { type: type, size: 1 } })
}
readonly property list<var> unusedToggleRows: toggleRowsForList(unusedToggles)
function toggleRowsForList(togglesList) {
var rows = [];
var row = [];
@@ -73,14 +74,14 @@ AbstractQuickPanel {
Repeater {
id: usedRowsRepeater
model: ScriptModel {
values: root.toggleRows
values: Array(root.toggleRows.length)
}
delegate: ButtonGroup {
id: toggleRow
required property var modelData
required property int index
property var modelData: root.toggleRows[index]
property int startingIndex: {
const rows = usedRowsRepeater.model.values;
const rows = root.toggleRows;
let sum = 0;
for (let i = 0; i < index; i++) {
sum += rows[i].length;
@@ -91,7 +92,8 @@ AbstractQuickPanel {
Repeater {
model: ScriptModel {
values: toggleRow.modelData
values: toggleRow?.modelData ?? []
objectProp: "type"
}
delegate: AndroidToggleDelegateChooser {
startingIndex: toggleRow.startingIndex
@@ -99,10 +101,11 @@ AbstractQuickPanel {
baseCellWidth: root.baseCellWidth
baseCellHeight: root.baseCellHeight
spacing: root.spacing
onOpenWifiDialog: root.openWifiDialog()
onOpenBluetoothDialog: root.openBluetoothDialog()
onOpenAudioOutputDialog: root.openAudioOutputDialog()
onOpenAudioInputDialog: root.openAudioInputDialog()
onOpenBluetoothDialog: root.openBluetoothDialog()
onOpenNightLightDialog: root.openNightLightDialog()
onOpenWifiDialog: root.openWifiDialog()
}
}
}
@@ -131,16 +134,18 @@ AbstractQuickPanel {
Repeater {
model: ScriptModel {
values: root.unusedToggleRows
values: Array(root.unusedToggleRows.length)
}
delegate: ButtonGroup {
id: unusedToggleRow
required property var modelData
required property int index
property var modelData: root.unusedToggleRows[index]
spacing: root.spacing
Repeater {
model: ScriptModel {
values: unusedToggleRow.modelData
values: unusedToggleRow?.modelData ?? []
objectProp: "type"
}
delegate: AndroidToggleDelegateChooser {
startingIndex: -1
@@ -5,7 +5,7 @@ import QtQuick
import QtQuick.Layouts
import Quickshell.Bluetooth
import "./classicStyle/"
import qs.modules.sidebarRight.quickToggles.classicStyle
AbstractQuickPanel {
id: root
@@ -19,7 +19,7 @@ AndroidQuickToggleButton {
}
altAction: () => {
Config.options.light.night.automatic = !Config.options.light.night.automatic
root.openMenu()
}
Component.onCompleted: {
@@ -27,7 +27,7 @@ AndroidQuickToggleButton {
}
StyledToolTip {
text: Translation.tr("Night Light | Right-click to toggle Auto mode")
text: Translation.tr("Night Light | Right-click to configure")
}
}
@@ -23,6 +23,21 @@ GroupButton {
baseHeight: root.baseCellHeight
property bool editMode: false
enableImplicitWidthAnimation: !editMode && root.mouseArea.containsMouse
enableImplicitHeightAnimation: !editMode && root.mouseArea.containsMouse
Behavior on baseWidth {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on baseHeight {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
opacity: 0
Component.onCompleted: {
opacity = 1
}
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
signal openMenu()
@@ -65,7 +80,7 @@ GroupButton {
MaterialSymbol {
anchors.centerIn: parent
fill: root.toggled ? 1 : 0
iconSize: Appearance.font.pixelSize.huge
iconSize: root.expandedSize ? 22 : 24
color: root.colIcon
text: root.buttonIcon
}
@@ -4,6 +4,7 @@ import qs.modules.common.widgets
import qs.services
import QtQuick
import Quickshell
import Quickshell.Hyprland
AndroidQuickToggleButton {
id: root
@@ -22,7 +23,7 @@ AndroidQuickToggleButton {
interval: 300
repeat: false
onTriggered: {
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("screenshot.qml")])
Hyprland.dispatch("global quickshell:regionScreenshot")
}
}
@@ -14,10 +14,11 @@ DelegateChooser {
required property real baseCellHeight
required property real spacing
required property int startingIndex
signal openWifiDialog()
signal openBluetoothDialog()
signal openAudioOutputDialog()
signal openAudioInputDialog()
signal openBluetoothDialog()
signal openNightLightDialog()
signal openWifiDialog()
role: "type"
@@ -90,6 +91,9 @@ DelegateChooser {
baseCellHeight: root.baseCellHeight
cellSpacing: root.spacing
cellSize: modelData.size
onOpenMenu: {
root.openNightLightDialog()
}
} }
DelegateChoice { roleValue: "darkMode"; AndroidDarkModeToggle {
@@ -2,7 +2,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "../"
import qs.modules.sidebarRight.quickToggles
import qs
import QtQuick
import Quickshell
@@ -14,7 +14,7 @@ GroupButton {
contentItem: MaterialSymbol {
anchors.centerIn: parent
iconSize: 20
iconSize: 22
fill: toggled ? 1 : 0
color: toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
horizontalAlignment: Text.AlignHCenter
@@ -3,7 +3,7 @@ import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import "./../bar" as Bar
import qs.modules.bar as Bar
MouseArea {
id: root
@@ -2,7 +2,7 @@ import qs.services
import qs.modules.common
import QtQuick
import QtQuick.Layouts
import "../bar" as Bar
import qs.modules.bar as Bar
MouseArea {
id: root
@@ -8,7 +8,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "../bar" as Bar
import qs.modules.bar as Bar
Item { // Bar content region
id: root
@@ -3,7 +3,7 @@ import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import "../bar" as Bar
import qs.modules.bar as Bar
Item {
id: root
@@ -4,7 +4,7 @@ import qs.services
import QtQuick
import QtQuick.Shapes
import QtQuick.Layouts
import "../bar" as Bar
import qs.modules.bar as Bar
Item { // Full hitbox
id: root
@@ -8,7 +8,7 @@ import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Mpris
import "../bar" as Bar
import qs.modules.bar as Bar
MouseArea {
id: root
@@ -316,7 +316,7 @@ MouseArea {
bottomMargin: 8
}
ToolbarButton {
IconToolbarButton {
implicitWidth: height
onClicked: {
Wallpapers.openFallbackPicker(root.useDarkMode);
@@ -327,42 +327,27 @@ MouseArea {
GlobalStates.wallpaperSelectorOpen = false;
Config.options.wallpaperSelector.useSystemFileDialog = true
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "open_in_new"
iconSize: Appearance.font.pixelSize.larger
}
text: "open_in_new"
StyledToolTip {
text: Translation.tr("Use the system file picker instead\nRight-click to make this the default behavior")
}
}
ToolbarButton {
IconToolbarButton {
implicitWidth: height
onClicked: {
Wallpapers.randomFromCurrentFolder();
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "ifl"
iconSize: Appearance.font.pixelSize.larger
}
text: "ifl"
StyledToolTip {
text: Translation.tr("Pick random from this folder")
}
}
ToolbarButton {
IconToolbarButton {
implicitWidth: height
onClicked: root.useDarkMode = !root.useDarkMode
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: root.useDarkMode ? "dark_mode" : "light_mode"
iconSize: Appearance.font.pixelSize.larger
}
text: root.useDarkMode ? "dark_mode" : "light_mode"
StyledToolTip {
text: Translation.tr("Click to toggle light/dark mode\n(applied when wallpaper is chosen)")
}
@@ -403,17 +388,12 @@ MouseArea {
}
}
ToolbarButton {
IconToolbarButton {
implicitWidth: height
onClicked: {
GlobalStates.wallpaperSelectorOpen = false;
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "cancel_presentation"
iconSize: Appearance.font.pixelSize.larger
}
text: "close"
StyledToolTip {
text: Translation.tr("Cancel wallpaper selection")
}
+1 -1
View File
@@ -7,7 +7,7 @@ import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
import "./ai/"
import qs.services.ai
/**
* Basic service to handle LLM chats. Supports Google's and OpenAI's API formats.
@@ -4,6 +4,8 @@ pragma ComponentBehavior: Bound
// From https://github.com/caelestia-dots/shell with modifications.
// License: GPLv3
import qs.modules.common
import qs.modules.common.functions
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
@@ -14,6 +16,7 @@ import QtQuick
*/
Singleton {
id: root
property real minimumBrightnessAllowed: 0.00001 // Setting to 0 would kind of turn off the screen. We don't want that.
signal brightnessChanged()
@@ -84,6 +87,8 @@ Singleton {
}
property int rawMaxBrightness: 100
property real brightness
property real brightnessMultiplier: 1.0
property real multipliedBrightness: Math.max(0, Math.min(1, brightness * brightnessMultiplier))
property bool ready: false
onBrightnessChanged: {
@@ -119,17 +124,23 @@ Singleton {
}
function syncBrightness() {
const rounded = Math.round(monitor.brightness * monitor.rawMaxBrightness);
const brightnessValue = monitor.multipliedBrightness
const rounded = Math.round(brightnessValue * monitor.rawMaxBrightness);
setProc.command = isDdc ? ["ddcutil", "-b", busNum, "setvcp", "10", rounded] : ["brightnessctl", "--class", "backlight", "s", rounded, "--quiet"];
setProc.startDetached();
}
function setBrightness(value: real): void {
value = Math.max(0.01, Math.min(1, value));
value = Math.max(root.minimumBrightnessAllowed, Math.min(1, value));
monitor.brightness = value;
setTimer.restart();
}
function setBrightnessMultiplier(value: real): void {
monitor.brightnessMultiplier = value;
setTimer.restart();
}
Component.onCompleted: {
initialize();
}
@@ -145,6 +156,61 @@ Singleton {
BrightnessMonitor {}
}
// Anti-flashbang
property string screenshotDir: "/tmp/quickshell/brightness/antiflashbang"
function brightnessMultiplierForLightness(x: real): real {
// 6.600135 + 216.360356 * e^(-0.0811129189x)
// Division by 100 is to normalize to [0, 1]
return (6.600135 + 216.360356 * Math.pow(Math.E, -0.0811129189 * x)) / 100.0;
}
Variants {
model: Quickshell.screens
Scope {
id: screenScope
required property var modelData
property string screenName: modelData.name
property string screenshotPath: `${root.screenshotDir}/screenshot-${screenName}.png`
Connections {
enabled: Config.options.light.antiFlashbang.enable
target: Hyprland
function onRawEvent(event) {
if (["workspacev2"].includes(event.name)) {
screenshotTimer.restart();
}
}
}
Timer {
id: screenshotTimer
interval: 700 // This is what I have for a Hyprland ws anim
onTriggered: {
screenshotProc.running = false;
screenshotProc.running = true;
}
}
Process {
id: screenshotProc
command: ["bash", "-c",
`mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}'`
+ ` && grim -o '${StringUtils.shellSingleQuoteEscape(screenScope.screenName)}' '${StringUtils.shellSingleQuoteEscape(screenScope.screenshotPath)}'`
+ ` && magick '${StringUtils.shellSingleQuoteEscape(screenScope.screenshotPath)}' -colorspace Gray -format "%[fx:mean*100]" info:`
]
stdout: StdioCollector {
id: lightnessCollector
onStreamFinished: {
Quickshell.execDetached(["rm", screenScope.screenshotPath]); // Cleanup
const lightness = lightnessCollector.text
const newMultiplier = root.brightnessMultiplierForLightness(parseFloat(lightness))
Brightness.getMonitorForScreen(screenScope.modelData).setBrightnessMultiplier(newMultiplier)
}
}
}
}
}
// External trigger points
IpcHandler {
target: "brightness"
@@ -4,6 +4,7 @@ import QtQuick
import qs.modules.common
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
/**
* Simple hyprsunset service with automatic mode.
@@ -111,18 +112,28 @@ Singleton {
}
}
function toggle() {
function toggle(active = undefined) {
if (root.manualActive === undefined) {
root.manualActive = root.active;
root.manualActiveHour = root.clockHour;
root.manualActiveMinute = root.clockMinute;
}
root.manualActive = !root.manualActive;
root.manualActive = active !== undefined ? active : !root.manualActive;
if (root.manualActive) {
root.enable();
} else {
root.disable();
}
}
// Change temp
Connections {
target: Config.options.light.night
function onColorTemperatureChanged() {
if (!root.active) return;
Hyprland.dispatch(`hyprctl hyprsunset temperature ${Config.options.light.night.colorTemperature}`);
Quickshell.execDetached(["hyprctl", "hyprsunset", "temperature", `${Config.options.light.night.colorTemperature}`]);
}
}
}
@@ -6,7 +6,7 @@ pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Io
import QtQuick
import "./network"
import qs.services.network
/**
* Network service with nmcli.
@@ -70,6 +70,8 @@ Singleton {
case "debian":
case "raspbian":
case "kali": distroIcon = "debian-symbolic"; break;
case "funtoo":
case "gentoo": distroIcon = "gentoo-symbolic"; break;
default: distroIcon = "linux-symbolic"; break;
}
if (textOsRelease.toLowerCase().includes("nyarch")) {
@@ -79,7 +79,7 @@ Singleton {
// Special cases
if (!text) return "";
var key = text.toString();
if (root.isLoading || (!root.translations.hasOwnProperty(key) && !root.generatedTranslations.hasOwnProperty(key)))
if (root.isLoading || (!root?.translations?.hasOwnProperty(key) && !root?.generatedTranslations?.hasOwnProperty(key)))
return key;
// Normal cases
+20 -20
View File
@@ -7,30 +7,30 @@
//@ pragma Env QT_SCALE_FACTOR=1
import "./modules/common/"
import "./modules/background/"
import "./modules/bar/"
import "./modules/cheatsheet/"
import "./modules/crosshair/"
import "./modules/dock/"
import "./modules/lock/"
import "./modules/mediaControls/"
import "./modules/notificationPopup/"
import "./modules/onScreenDisplay/"
import "./modules/onScreenKeyboard/"
import "./modules/overview/"
import "./modules/regionSelector/"
import "./modules/screenCorners/"
import "./modules/sessionScreen/"
import "./modules/sidebarLeft/"
import "./modules/sidebarRight/"
import "./modules/verticalBar/"
import "./modules/wallpaperSelector/"
import qs.modules.common
import qs.modules.background
import qs.modules.bar
import qs.modules.cheatsheet
import qs.modules.crosshair
import qs.modules.dock
import qs.modules.lock
import qs.modules.mediaControls
import qs.modules.notificationPopup
import qs.modules.onScreenDisplay
import qs.modules.onScreenKeyboard
import qs.modules.overview
import qs.modules.regionSelector
import qs.modules.screenCorners
import qs.modules.sessionScreen
import qs.modules.sidebarLeft
import qs.modules.sidebarRight
import qs.modules.verticalBar
import qs.modules.wallpaperSelector
import QtQuick
import QtQuick.Window
import Quickshell
import "./services/"
import qs.services
ShellRoot {
// Enable/disable modules here. False = not loaded at all, so rest assured