forked from Shinonome/dots-hyprland
Merge branch 'end-4:main' into patch-1
This commit is contained in:
@@ -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 |
@@ -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
|
||||
|
||||
+1
-1
@@ -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()
|
||||
}
|
||||
|
||||
+27
-22
@@ -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
|
||||
|
||||
+2
-2
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-1
@@ -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
|
||||
}
|
||||
|
||||
+2
-1
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user