diff --git a/.github/workflows/auto-close-issue.yml b/.github/workflows/auto-close-issue.yml new file mode 100644 index 000000000..35afe6e4b --- /dev/null +++ b/.github/workflows/auto-close-issue.yml @@ -0,0 +1,167 @@ +on: + issues: + types: [opened] + +name: Close issues when the "ticked without reading" checkbox is checked + +permissions: + issues: write + +jobs: + detect-and-close: + runs-on: ubuntu-latest + steps: + - name: Detect checked "ticked without reading" checkbox, comment, close and lock + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_BODY: ${{ toJson(github.event.issue.body) }} + ISSUE_USER: ${{ github.event.issue.user.login }} + run: | + set -euo pipefail + + # Normalize the JSON-encoded body into plain text + BODY=$(printf '%s' "$ISSUE_BODY" | sed -E 's/^"(.*)"$/\1/' | sed 's/\\"/"/g' | sed 's/\\n/\n/g') + + echo "Checking issue #${ISSUE_NUMBER} for the target checked checkbox..." + # Use -- to stop option parsing so the leading - in the pattern isn't treated as an option + if printf '%s' "$BODY" | grep -Fiq -- "- [x] I've ticked the checkboxes without reading their contents"; then + echo "Target checkbox is checked. Proceeding to comment, close and lock the issue." + + # --- Get issue node id via GraphQL --- + QUERY='query($owner: String!, $name: String!, $number: Int!) { repository(owner: $owner, name: $name) { issue(number: $number) { id } } }' + GET_ID_PAYLOAD=$(jq -n --arg q "$QUERY" --arg owner "$OWNER" --arg name "$REPO" --argjson number "$ISSUE_NUMBER" '{query:$q, variables:{owner:$owner, name:$name, number:$number}}') + + echo "GraphQL: fetching issue node id..." + RES=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$GET_ID_PAYLOAD" https://api.github.com/graphql) + echo "GraphQL response (get id):" + printf '%s\n' "$RES" + + ISSUE_ID=$(printf '%s' "$RES" | jq -r '.data.repository.issue.id // empty') + + if [ -z "$ISSUE_ID" ]; then + echo "Failed to get issue id from GraphQL response. Aborting." + exit 1 + fi + echo "Issue node id: $ISSUE_ID" + + # --- Post a comment to the issue --- + COMMENT_BODY="Hi @${ISSUE_USER} — I noticed you checked \"I've ticked the checkboxes without reading their contents\" in the issue template. To help others assist you effectively, please read the template and provide the requested diagnostic information (Step 2 & Step 3). I will close this issue now. If you create a new issue with the required information, we can re-evaluate. Thank you!" + MUT_ADD_COMMENT='mutation($id: ID!, $body: String!) { addComment(input: {subjectId: $id, body: $body}) { clientMutationId } }' + ADD_COMMENT_PAYLOAD=$(jq -n --arg q "$MUT_ADD_COMMENT" --arg id "$ISSUE_ID" --arg body "$COMMENT_BODY" '{query:$q, variables:{id:$id, body:$body}}') + + echo "GraphQL: adding comment..." + RES_COMMENT=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$ADD_COMMENT_PAYLOAD" https://api.github.com/graphql) + echo "GraphQL response (add comment):" + printf '%s\n' "$RES_COMMENT" + + ERR_COMMENT=$(printf '%s' "$RES_COMMENT" | jq -r '.errors[]?.message // empty') + if [ -n "$ERR_COMMENT" ]; then + echo "addComment error: $ERR_COMMENT" + exit 1 + fi + echo "Comment posted." + + # --- Attempt to close via GraphQL updateIssue --- + MUT_UPDATE_ISSUE='mutation($id: ID!) { updateIssue(input: {id: $id, state: CLOSED, stateReason: NOT_PLANNED}) { issue { number, state, stateReason } } }' + UPDATE_PAYLOAD=$(jq -n --arg q "$MUT_UPDATE_ISSUE" --arg id "$ISSUE_ID" '{query:$q, variables:{id:$id}}') + + echo "GraphQL: updating issue (close with NOT_PLANNED)..." + RES_UPDATE=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$UPDATE_PAYLOAD" https://api.github.com/graphql) + echo "GraphQL response (update issue):" + printf '%s\n' "$RES_UPDATE" + + ERR_UPDATE=$(printf '%s' "$RES_UPDATE" | jq -r '.errors[]?.message // empty') + UPDATED_STATE=$(printf '%s' "$RES_UPDATE" | jq -r '.data.updateIssue.issue.state // empty') + UPDATED_REASON=$(printf '%s' "$RES_UPDATE" | jq -r '.data.updateIssue.issue.stateReason // empty') + + CLOSED_OK=false + + if [ -n "$ERR_UPDATE" ]; then + echo "GraphQL updateIssue returned errors: $ERR_UPDATE" + fi + + if [ "$UPDATED_STATE" = "CLOSED" ]; then + echo "Issue closed via GraphQL: state=$UPDATED_STATE, stateReason=$UPDATED_REASON" + CLOSED_OK=true + else + echo "GraphQL update did not confirm the issue is closed. Falling back to REST API PATCH to ensure the issue is closed." + + # REST fallback to close the issue with state_reason "not_planned" + REST_PAYLOAD=$(jq -n --arg state "closed" --arg sr "not_planned" '{state:$state, state_reason:$sr}') + echo "REST: PATCH /repos/$OWNER/$REPO/issues/$ISSUE_NUMBER payload: $REST_PAYLOAD" + RES_REST=$(curl -s -w "\n%{http_code}" -X PATCH \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -H "Content-Type: application/json" \ + -d "$REST_PAYLOAD" \ + "https://api.github.com/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER") + + HTTP_STATUS=$(printf '%s' "$RES_REST" | tail -n1) + RESP_BODY=$(printf '%s' "$RES_REST" | sed '$d') + + echo "REST response body:" + printf '%s\n' "$RESP_BODY" + echo "REST HTTP status: $HTTP_STATUS" + + if [ "$HTTP_STATUS" -ge 200 ] && [ "$HTTP_STATUS" -lt 300 ]; then + CLOSED_STATE=$(printf '%s' "$RESP_BODY" | jq -r '.state // empty') + CLOSED_REASON=$(printf '%s' "$RESP_BODY" | jq -r '.state_reason // empty') + echo "Issue closed via REST: state=$CLOSED_STATE, state_reason=$CLOSED_REASON" + if [ "$CLOSED_STATE" = "closed" ]; then + CLOSED_OK=true + fi + else + echo "REST fallback failed to close the issue. See REST response above." + exit 1 + fi + fi + + # --- Attempt to lock the conversation (GraphQL first, then REST fallback) --- + if [ "$CLOSED_OK" = "true" ]; then + echo "Attempting to lock the conversation via GraphQL with reason NO_REASON..." + + MUT_LOCK='mutation($id: ID!, $reason: LockReason) { lockLockable(input:{lockableId:$id, lockReason:$reason}) { clientMutationId } }' + LOCK_PAYLOAD=$(jq -n --arg q "$MUT_LOCK" --arg id "$ISSUE_ID" --arg reason "NO_REASON" '{query:$q, variables:{id:$id, reason:$reason}}') + + RES_LOCK=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$LOCK_PAYLOAD" https://api.github.com/graphql) + echo "GraphQL response (lock):" + printf '%s\n' "$RES_LOCK" + + LOCK_ERR=$(printf '%s' "$RES_LOCK" | jq -r '.errors[]?.message // empty') + + if [ -n "$LOCK_ERR" ]; then + echo "GraphQL lockLockable returned errors: $LOCK_ERR" + echo "Falling back to REST API to lock the conversation (no explicit reason)." + + # REST fallback to lock the issue (no lock_reason to indicate "no reason") + RES_REST_LOCK=$(curl -s -w "\n%{http_code}" -X PUT \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/lock" -d '{}') + + HTTP_STATUS_LOCK=$(printf '%s' "$RES_REST_LOCK" | tail -n1) + RESP_BODY_LOCK=$(printf '%s' "$RES_REST_LOCK" | sed '$d') + + echo "REST lock response body:" + printf '%s\n' "$RESP_BODY_LOCK" + echo "REST lock HTTP status: $HTTP_STATUS_LOCK" + + if [ "$HTTP_STATUS_LOCK" -ge 200 ] && [ "$HTTP_STATUS_LOCK" -lt 300 ]; then + echo "Issue conversation locked via REST (no explicit reason)." + else + echo "REST fallback failed to lock the conversation. See REST response above." + exit 1 + fi + else + echo "Lock via GraphQL succeeded (or returned no errors)." + fi + else + echo "Issue was not successfully closed; skipping lock." + fi + + else + echo "Checkbox not present/checked. Nothing to do." + fi diff --git a/dots/.config/hypr/hyprland/keybinds.conf b/dots/.config/hypr/hyprland/keybinds.conf index 38fc05caf..5e6ff1e80 100644 --- a/dots/.config/hypr/hyprland/keybinds.conf +++ b/dots/.config/hypr/hyprland/keybinds.conf @@ -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 diff --git a/dots/.config/quickshell/ii/assets/icons/gentoo-symbolic.svg b/dots/.config/quickshell/ii/assets/icons/gentoo-symbolic.svg new file mode 100644 index 000000000..741be9b6b --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/gentoo-symbolic.svg @@ -0,0 +1,37 @@ + + + + + + diff --git a/dots/.config/quickshell/ii/killDialog.qml b/dots/.config/quickshell/ii/killDialog.qml index ff36fb5e6..cf961ef38 100644 --- a/dots/.config/quickshell/ii/killDialog.qml +++ b/dots/.config/quickshell/ii/killDialog.qml @@ -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 { diff --git a/dots/.config/quickshell/ii/modules/background/Background.qml b/dots/.config/quickshell/ii/modules/background/Background.qml index c997d847b..4aed1c08c 100644 --- a/dots/.config/quickshell/ii/modules/background/Background.qml +++ b/dots/.config/quickshell/ii/modules/background/Background.qml @@ -13,7 +13,7 @@ import Quickshell.Io import Quickshell.Wayland import Quickshell.Hyprland -import "./cookieClock" +import qs.modules.background.cookieClock Variants { id: root diff --git a/dots/.config/quickshell/ii/modules/background/cookieClock/CookieClock.qml b/dots/.config/quickshell/ii/modules/background/cookieClock/CookieClock.qml index 9b5ae0265..d08056909 100644 --- a/dots/.config/quickshell/ii/modules/background/cookieClock/CookieClock.qml +++ b/dots/.config/quickshell/ii/modules/background/cookieClock/CookieClock.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/bar/BarContent.qml b/dots/.config/quickshell/ii/modules/bar/BarContent.qml index 7e8ca9e77..77ed8db69 100644 --- a/dots/.config/quickshell/ii/modules/bar/BarContent.qml +++ b/dots/.config/quickshell/ii/modules/bar/BarContent.qml @@ -1,4 +1,4 @@ -import "./weather" +import qs.modules.bar.weather import QtQuick import QtQuick.Layouts import Quickshell diff --git a/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml b/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml index 2930329c1..d7f73a4e3 100644 --- a/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml +++ b/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/bar/weather/WeatherPopup.qml b/dots/.config/quickshell/ii/modules/bar/weather/WeatherPopup.qml index bd93c2662..0c06932f7 100644 --- a/dots/.config/quickshell/ii/modules/bar/weather/WeatherPopup.qml +++ b/dots/.config/quickshell/ii/modules/bar/weather/WeatherPopup.qml @@ -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 { } } } -} \ No newline at end of file +} diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index 9714b23f4..e10e7ac38 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/common/functions/Fuzzy.qml b/dots/.config/quickshell/ii/modules/common/functions/Fuzzy.qml index 7a132ada1..00891ed36 100644 --- a/dots/.config/quickshell/ii/modules/common/functions/Fuzzy.qml +++ b/dots/.config/quickshell/ii/modules/common/functions/Fuzzy.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/common/functions/Levendist.qml b/dots/.config/quickshell/ii/modules/common/functions/Levendist.qml index a327c3c78..0d6a37481 100644 --- a/dots/.config/quickshell/ii/modules/common/functions/Levendist.qml +++ b/dots/.config/quickshell/ii/modules/common/functions/Levendist.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/common/widgets/ConfigSwitch.qml b/dots/.config/quickshell/ii/modules/common/widgets/ConfigSwitch.qml index 13c43c552..02e0eabad 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/ConfigSwitch.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/ConfigSwitch.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/common/widgets/GroupButton.qml b/dots/.config/quickshell/ii/modules/common/widgets/GroupButton.qml index bc02e77e1..ed93eb77f 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/GroupButton.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/GroupButton.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/common/widgets/IconAndTextToolbarButton.qml b/dots/.config/quickshell/ii/modules/common/widgets/IconAndTextToolbarButton.qml new file mode 100644 index 000000000..45f90f8a1 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/IconAndTextToolbarButton.qml @@ -0,0 +1,33 @@ +import QtQuick +import QtQuick.Layouts +import qs.modules.common + +ToolbarButton { + id: iconBtn + required property string iconText + + colBackgroundToggled: Appearance.colors.colSecondaryContainer + colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover + colRippleToggled: Appearance.colors.colSecondaryContainerActive + property color colText: toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant + + contentItem: Row { + anchors.centerIn: parent + spacing: 6 + + MaterialSymbol { + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + iconSize: 22 + text: iconBtn.iconText + color: iconBtn.colText + } + StyledText { + visible: iconBtn.iconText.length > 0 && iconBtn.text.length > 0 + anchors.verticalCenter: parent.verticalCenter + color: iconBtn.colText + text: iconBtn.text + } + } +} diff --git a/dots/.config/quickshell/ii/modules/common/widgets/IconToolbarButton.qml b/dots/.config/quickshell/ii/modules/common/widgets/IconToolbarButton.qml new file mode 100644 index 000000000..8532d0cd4 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/IconToolbarButton.qml @@ -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 + } +} diff --git a/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml b/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml index 9155bd879..8635f4f78 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml b/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml index 6e8e1cecc..b4c96978b 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml b/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml index 5473d4628..a3fe23387 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml @@ -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) diff --git a/dots/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml b/dots/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml index 92f6353e3..a0ca64e98 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/common/widgets/WindowDialogSlider.qml b/dots/.config/quickshell/ii/modules/common/widgets/WindowDialogSlider.qml new file mode 100644 index 000000000..5c6db1443 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/WindowDialogSlider.qml @@ -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() + } +} diff --git a/dots/.config/quickshell/ii/modules/lock/LockSurface.qml b/dots/.config/quickshell/ii/modules/lock/LockSurface.qml index 7a4340705..5feba6c72 100644 --- a/dots/.config/quickshell/ii/modules/lock/LockSurface.qml +++ b/dots/.config/quickshell/ii/modules/lock/LockSurface.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/mediaControls/MediaControls.qml b/dots/.config/quickshell/ii/modules/mediaControls/MediaControls.qml index ea90be154..62426a36f 100644 --- a/dots/.config/quickshell/ii/modules/mediaControls/MediaControls.qml +++ b/dots/.config/quickshell/ii/modules/mediaControls/MediaControls.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/onScreenDisplay/indicators/BrightnessIndicator.qml b/dots/.config/quickshell/ii/modules/onScreenDisplay/indicators/BrightnessIndicator.qml index f127440f8..30661b911 100644 --- a/dots/.config/quickshell/ii/modules/onScreenDisplay/indicators/BrightnessIndicator.qml +++ b/dots/.config/quickshell/ii/modules/onScreenDisplay/indicators/BrightnessIndicator.qml @@ -2,7 +2,7 @@ import qs.services import QtQuick import Quickshell import Quickshell.Hyprland -import "../" +import qs.modules.onScreenDisplay OsdValueIndicator { id: root diff --git a/dots/.config/quickshell/ii/modules/onScreenDisplay/indicators/VolumeIndicator.qml b/dots/.config/quickshell/ii/modules/onScreenDisplay/indicators/VolumeIndicator.qml index 7f7b5f47f..487befdac 100644 --- a/dots/.config/quickshell/ii/modules/onScreenDisplay/indicators/VolumeIndicator.qml +++ b/dots/.config/quickshell/ii/modules/onScreenDisplay/indicators/VolumeIndicator.qml @@ -1,6 +1,6 @@ import qs.services import QtQuick -import "../" +import qs.modules.onScreenDisplay OsdValueIndicator { id: osdValues diff --git a/dots/.config/quickshell/ii/modules/regionSelector/CircleSelectionDetails.qml b/dots/.config/quickshell/ii/modules/regionSelector/CircleSelectionDetails.qml new file mode 100644 index 000000000..c0f823fbe --- /dev/null +++ b/dots/.config/quickshell/ii/modules/regionSelector/CircleSelectionDetails.qml @@ -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 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 + } + } + } + +} diff --git a/dots/.config/quickshell/ii/modules/regionSelector/RectCornersSelectionDetails.qml b/dots/.config/quickshell/ii/modules/regionSelector/RectCornersSelectionDetails.qml new file mode 100644 index 000000000..40069131d --- /dev/null +++ b/dots/.config/quickshell/ii/modules/regionSelector/RectCornersSelectionDetails.qml @@ -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 + } +} diff --git a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml new file mode 100644 index 000000000..2a3c024ab --- /dev/null +++ b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml @@ -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 points: [] + property var mouseButton: null + property var imageRegions: [] + readonly property list 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 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(); + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml index 6eecc365e..7cfd37349 100644 --- a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml +++ b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml @@ -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 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 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() } } diff --git a/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml b/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml new file mode 100644 index 000000000..f1043d13b --- /dev/null +++ b/dots/.config/quickshell/ii/modules/regionSelector/TargetRegion.qml @@ -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 + } + } + } + } +} \ No newline at end of file diff --git a/dots/.config/quickshell/ii/modules/screenCorners/ScreenCorners.qml b/dots/.config/quickshell/ii/modules/screenCorners/ScreenCorners.qml index 67bd899ef..55173b664 100644 --- a/dots/.config/quickshell/ii/modules/screenCorners/ScreenCorners.qml +++ b/dots/.config/quickshell/ii/modules/screenCorners/ScreenCorners.qml @@ -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); } diff --git a/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml b/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml index 72469843c..bd08cfffd 100644 --- a/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml @@ -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") diff --git a/dots/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml b/dots/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml index 280c57583..6d00752bc 100644 --- a/dots/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml +++ b/dots/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/sidebarLeft/Anime.qml b/dots/.config/quickshell/ii/modules/sidebarLeft/Anime.qml index 42c314a10..cab251cb8 100644 --- a/dots/.config/quickshell/ii/modules/sidebarLeft/Anime.qml +++ b/dots/.config/quickshell/ii/modules/sidebarLeft/Anime.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/sidebarLeft/SidebarLeft.qml b/dots/.config/quickshell/ii/modules/sidebarLeft/SidebarLeft.qml index 8d0a43a05..6d8009f0b 100644 --- a/dots/.config/quickshell/ii/modules/sidebarLeft/SidebarLeft.qml +++ b/dots/.config/quickshell/ii/modules/sidebarLeft/SidebarLeft.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/sidebarLeft/SidebarLeftContent.qml b/dots/.config/quickshell/ii/modules/sidebarLeft/SidebarLeftContent.qml index 37014b4b7..ae19f07a8 100644 --- a/dots/.config/quickshell/ii/modules/sidebarLeft/SidebarLeftContent.qml +++ b/dots/.config/quickshell/ii/modules/sidebarLeft/SidebarLeftContent.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/sidebarLeft/Translator.qml b/dots/.config/quickshell/ii/modules/sidebarLeft/Translator.qml index 18a8cf5b9..41f4fffab 100644 --- a/dots/.config/quickshell/ii/modules/sidebarLeft/Translator.qml +++ b/dots/.config/quickshell/ii/modules/sidebarLeft/Translator.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/sidebarLeft/anime/BooruResponse.qml b/dots/.config/quickshell/ii/modules/sidebarLeft/anime/BooruResponse.qml index cfb2a7f9d..9258dbc9d 100644 --- a/dots/.config/quickshell/ii/modules/sidebarLeft/anime/BooruResponse.qml +++ b/dots/.config/quickshell/ii/modules/sidebarLeft/anime/BooruResponse.qml @@ -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 { } } } -} \ No newline at end of file +} diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/BottomWidgetGroup.qml b/dots/.config/quickshell/ii/modules/sidebarRight/BottomWidgetGroup.qml index a7a895af1..96313289a 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/BottomWidgetGroup.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/BottomWidgetGroup.qml @@ -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 } } -} \ No newline at end of file +} diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml b/dots/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml index 85d3b823a..007006ca9 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml b/dots/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml index 1eee4d335..f2aee10fe 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml @@ -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" diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/calendar/CalendarWidget.qml b/dots/.config/quickshell/ii/modules/sidebarRight/calendar/CalendarWidget.qml index 66055acae..002a9e31f 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/calendar/CalendarWidget.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/calendar/CalendarWidget.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/nightLight/NightLightDialog.qml b/dots/.config/quickshell/ii/modules/sidebarRight/nightLight/NightLightDialog.qml new file mode 100644 index 000000000..535aded59 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/sidebarRight/nightLight/NightLightDialog.qml @@ -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() + } + } +} diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AbstractQuickPanel.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AbstractQuickPanel.qml index 6f2861409..c6ef4b9c8 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AbstractQuickPanel.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AbstractQuickPanel.qml @@ -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() } diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml index 5aa95ad35..a7d743585 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml @@ -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 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 toggles: Config.options.sidebar.quickToggles.android.toggles - readonly property list toggleRows: toggleRowsForList(toggles) - readonly property list 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 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 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 toggles: Config.ready ? Config.options.sidebar.quickToggles.android.toggles : [] + readonly property list toggleRows: toggleRowsForList(toggles) + readonly property list 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 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 diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/ClassicQuickPanel.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/ClassicQuickPanel.qml index c6855dfaa..cf52886a0 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/ClassicQuickPanel.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/ClassicQuickPanel.qml @@ -5,7 +5,7 @@ import QtQuick import QtQuick.Layouts import Quickshell.Bluetooth -import "./classicStyle/" +import qs.modules.sidebarRight.quickToggles.classicStyle AbstractQuickPanel { id: root diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidNightLightToggle.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidNightLightToggle.qml index 378dd0284..699d93dc3 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidNightLightToggle.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidNightLightToggle.qml @@ -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") } } diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidQuickToggleButton.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidQuickToggleButton.qml index ee9d58f45..9165bcf56 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidQuickToggleButton.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidQuickToggleButton.qml @@ -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 } diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidScreenSnipToggle.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidScreenSnipToggle.qml index c00570e87..5a22b616a 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidScreenSnipToggle.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidScreenSnipToggle.qml @@ -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") } } diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidToggleDelegateChooser.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidToggleDelegateChooser.qml index 2cfaa9585..a8f79c843 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidToggleDelegateChooser.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidToggleDelegateChooser.qml @@ -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 { diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/classicStyle/NetworkToggle.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/classicStyle/NetworkToggle.qml index 5fd8e3e8d..cc3ac3fca 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/classicStyle/NetworkToggle.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/classicStyle/NetworkToggle.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/classicStyle/QuickToggleButton.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/classicStyle/QuickToggleButton.qml index 25a53de1a..11ca7cb97 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/classicStyle/QuickToggleButton.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/classicStyle/QuickToggleButton.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/verticalBar/BatteryIndicator.qml b/dots/.config/quickshell/ii/modules/verticalBar/BatteryIndicator.qml index 27d704884..b134b12fe 100644 --- a/dots/.config/quickshell/ii/modules/verticalBar/BatteryIndicator.qml +++ b/dots/.config/quickshell/ii/modules/verticalBar/BatteryIndicator.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/verticalBar/Resources.qml b/dots/.config/quickshell/ii/modules/verticalBar/Resources.qml index e4bba0187..ddbb1c399 100644 --- a/dots/.config/quickshell/ii/modules/verticalBar/Resources.qml +++ b/dots/.config/quickshell/ii/modules/verticalBar/Resources.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/verticalBar/VerticalBarContent.qml b/dots/.config/quickshell/ii/modules/verticalBar/VerticalBarContent.qml index e594a0ae4..ac6be80cd 100644 --- a/dots/.config/quickshell/ii/modules/verticalBar/VerticalBarContent.qml +++ b/dots/.config/quickshell/ii/modules/verticalBar/VerticalBarContent.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/verticalBar/VerticalClockWidget.qml b/dots/.config/quickshell/ii/modules/verticalBar/VerticalClockWidget.qml index 1c19f2828..391d2e78c 100644 --- a/dots/.config/quickshell/ii/modules/verticalBar/VerticalClockWidget.qml +++ b/dots/.config/quickshell/ii/modules/verticalBar/VerticalClockWidget.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/verticalBar/VerticalDateWidget.qml b/dots/.config/quickshell/ii/modules/verticalBar/VerticalDateWidget.qml index 8ff9386c3..aaf17ca4f 100644 --- a/dots/.config/quickshell/ii/modules/verticalBar/VerticalDateWidget.qml +++ b/dots/.config/quickshell/ii/modules/verticalBar/VerticalDateWidget.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/verticalBar/VerticalMedia.qml b/dots/.config/quickshell/ii/modules/verticalBar/VerticalMedia.qml index 24eed8b4f..7a512564a 100644 --- a/dots/.config/quickshell/ii/modules/verticalBar/VerticalMedia.qml +++ b/dots/.config/quickshell/ii/modules/verticalBar/VerticalMedia.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/dots/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 9d89dfd7d..a093d38ca 100644 --- a/dots/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/dots/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -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") } diff --git a/dots/.config/quickshell/ii/services/Ai.qml b/dots/.config/quickshell/ii/services/Ai.qml index e348d4459..26657b0d1 100644 --- a/dots/.config/quickshell/ii/services/Ai.qml +++ b/dots/.config/quickshell/ii/services/Ai.qml @@ -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. diff --git a/dots/.config/quickshell/ii/services/Brightness.qml b/dots/.config/quickshell/ii/services/Brightness.qml index 3f9d93767..a71f8f487 100644 --- a/dots/.config/quickshell/ii/services/Brightness.qml +++ b/dots/.config/quickshell/ii/services/Brightness.qml @@ -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" diff --git a/dots/.config/quickshell/ii/services/Hyprsunset.qml b/dots/.config/quickshell/ii/services/Hyprsunset.qml index 2f7b3569f..8f0e36529 100644 --- a/dots/.config/quickshell/ii/services/Hyprsunset.qml +++ b/dots/.config/quickshell/ii/services/Hyprsunset.qml @@ -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}`]); + } + } } diff --git a/dots/.config/quickshell/ii/services/Network.qml b/dots/.config/quickshell/ii/services/Network.qml index 181e76cf4..7d16a9450 100644 --- a/dots/.config/quickshell/ii/services/Network.qml +++ b/dots/.config/quickshell/ii/services/Network.qml @@ -6,7 +6,7 @@ pragma ComponentBehavior: Bound import Quickshell import Quickshell.Io import QtQuick -import "./network" +import qs.services.network /** * Network service with nmcli. diff --git a/dots/.config/quickshell/ii/services/SystemInfo.qml b/dots/.config/quickshell/ii/services/SystemInfo.qml index a8da8e191..1d5c0bf6e 100644 --- a/dots/.config/quickshell/ii/services/SystemInfo.qml +++ b/dots/.config/quickshell/ii/services/SystemInfo.qml @@ -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")) { diff --git a/dots/.config/quickshell/ii/services/Translation.qml b/dots/.config/quickshell/ii/services/Translation.qml index 71e176869..c504ba21c 100644 --- a/dots/.config/quickshell/ii/services/Translation.qml +++ b/dots/.config/quickshell/ii/services/Translation.qml @@ -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 diff --git a/dots/.config/quickshell/ii/shell.qml b/dots/.config/quickshell/ii/shell.qml index 1dd1627f4..9cb90f102 100644 --- a/dots/.config/quickshell/ii/shell.qml +++ b/dots/.config/quickshell/ii/shell.qml @@ -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 diff --git a/sdata/lib/package-installers.sh b/sdata/lib/package-installers.sh index c89d8009e..54da9833d 100644 --- a/sdata/lib/package-installers.sh +++ b/sdata/lib/package-installers.sh @@ -1,6 +1,5 @@ # This script depends on `functions.sh' . -# This is NOT a script for execution, but for loading functions, so NOT need execution permission or shebang. -# NOTE that you NOT need to `cd ..' because the `$0' is NOT this file, but the script file which will source this file. +# This script is not for direct execution, instead it should be sourced by other script. It does not need execution permission or shebang. # shellcheck shell=bash @@ -105,5 +104,5 @@ install-python-packages(){ x uv venv --prompt .venv $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV) -p 3.12 x source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate x uv pip install -r sdata/uv/requirements.txt - x deactivate # We don't need the virtual environment anymore + x deactivate } diff --git a/sdata/options/install.sh b/sdata/options/install.sh index 0093e106b..fa3f2b4e3 100644 --- a/sdata/options/install.sh +++ b/sdata/options/install.sh @@ -14,11 +14,12 @@ Options for install: --skip-allsetups Skip the whole process setting up permissions/services etc --skip-allfiles Skip the whole process copying configuration files -s, --skip-sysupdate Skip system package upgrade e.g. \"sudo pacman -Syu\" + --skip-quickshell Skip installing the config for Quickshell --skip-hyprland Skip installing the config for Hyprland --skip-fish Skip installing the config for Fish --skip-plasmaintg Skip installing plasma-browser-integration --skip-miscconf Skip copying the dirs and files to \".configs\" except for - AGS, Fish and Hyprland + Quickshell, Fish and Hyprland --exp-files Use experimental script for the third step copying files --fontset (Unavailable yet) Use a set of pre-defined font and config --via-nix (Unavailable yet) Use Nix to install dependencies @@ -32,7 +33,7 @@ cleancache(){ # `man getopt` to see more para=$(getopt \ -o hfk:cs \ - -l help,force,fontset:,clean,skip-allgreeting,skip-alldeps,skip-allsetups,skip-allfiles,skip-sysupdate,skip-fish,skip-hyprland,skip-plasmaintg,skip-miscconf,exp-files,via-nix \ + -l help,force,fontset:,clean,skip-allgreeting,skip-alldeps,skip-allsetups,skip-allfiles,skip-sysupdate,skip-quickshell,skip-fish,skip-hyprland,skip-plasmaintg,skip-miscconf,exp-files,via-nix \ -n "$0" -- "$@") [ $? != 0 ] && echo "$0: Error when getopt, please recheck parameters." && exit 1 ##################################################################################### @@ -64,6 +65,7 @@ while true ; do -s|--skip-sysupdate) SKIP_SYSUPDATE=true;shift;; --skip-hyprland) SKIP_HYPRLAND=true;shift;; --skip-fish) SKIP_FISH=true;shift;; + --skip-quickshell) SKIP_QUICKSHELL=true;shift;; --skip-miscconf) SKIP_MISCCONF=true;shift;; --skip-plasmaintg) SKIP_PLASMAINTG=true;shift;; --exp-files) EXPERIMENTAL_FILES_SCRIPT=true;shift;; diff --git a/sdata/step/3.install-files.sh b/sdata/step/3.install-files.sh index 50b4b5692..eaea5017a 100644 --- a/sdata/step/3.install-files.sh +++ b/sdata/step/3.install-files.sh @@ -53,21 +53,27 @@ function ask_backup_configs(){ showfun backup_clashing_targets printf "${STY_RED}" printf "Would you like to backup clashing dirs/files under \"$XDG_CONFIG_HOME\" and \"$XDG_DATA_HOME\" to \"$BACKUP_DIR\"?" - read -p "[y/N] " backup_confirm - case $backup_confirm in - [yY][eE][sS]|[yY]) - backup_clashing_targets dots/.config $XDG_CONFIG_HOME "${BACKUP_DIR}/.config" - backup_clashing_targets dots/.local/share $XDG_DATA_HOME "${BACKUP_DIR}/.local/share" - ;; - *) echo "Skipping backup..." ;; - esac printf "${STY_RST}" + while true;do + echo " y = Yes, backup" + echo " n = No, skip to next" + local p; read -p "====> " p + case $p in + [yY]) echo -e "${STY_BLUE}OK, doing backup...${STY_RST}" ;local backup=true;break ;; + [nN]) echo -e "${STY_BLUE}Alright, skipping...${STY_RST}" ;local backup=false;break ;; + *) echo -e "${STY_RED}Please enter [y/n].${STY_RST}";; + esac + done + if $backup;then + backup_clashing_targets dots/.config $XDG_CONFIG_HOME "${BACKUP_DIR}/.config" + backup_clashing_targets dots/.local/share $XDG_DATA_HOME "${BACKUP_DIR}/.local/share" + fi } ##################################################################################### # In case some dirs does not exists -v mkdir -p $XDG_BIN_HOME $XDG_CACHE_HOME $XDG_CONFIG_HOME $XDG_DATA_HOME +v mkdir -p $XDG_BIN_HOME $XDG_CACHE_HOME $XDG_CONFIG_HOME/quickshell $XDG_DATA_HOME case $ask in false) sleep 0 ;; @@ -84,11 +90,11 @@ esac # original dotfiles and new ones in the SAME DIRECTORY # (eg. in ~/.config/hypr) won't be mixed together -# MISC (For dots/.config/* but not fish, not Hyprland) +# MISC (For dots/.config/* but not quickshell, not fish, not Hyprland) case $SKIP_MISCCONF in true) sleep 0;; *) - for i in $(find dots/.config/ -mindepth 1 -maxdepth 1 ! -name 'fish' ! -name 'hypr' -exec basename {} \;); do + for i in $(find dots/.config/ -mindepth 1 -maxdepth 1 ! -name 'quickshell' ! -name 'fish' ! -name 'hypr' -exec basename {} \;); do # i="dots/.config/$i" echo "[$0]: Found target: dots/.config/$i" if [ -d "dots/.config/$i" ];then warning_rsync; v rsync -av --delete "dots/.config/$i/" "$XDG_CONFIG_HOME/$i/" @@ -98,6 +104,13 @@ case $SKIP_MISCCONF in ;; esac +case $SKIP_QUICKSHELL in + true) sleep 0;; + *) + warning_rsync; v rsync -av --delete dots/.config/quickshell/ii/ "$XDG_CONFIG_HOME"/quickshell/ii/ + ;; +esac + case $SKIP_FISH in true) sleep 0;; *) diff --git a/sdist/arch/illogical-impulse-quickshell-git/PKGBUILD b/sdist/arch/illogical-impulse-quickshell-git/PKGBUILD index f84d63079..f018b1564 100644 --- a/sdist/arch/illogical-impulse-quickshell-git/PKGBUILD +++ b/sdist/arch/illogical-impulse-quickshell-git/PKGBUILD @@ -1,4 +1,4 @@ -_commit='00858812f25b748d08b075a0d284093685fa3ffd' +_commit='3e2ce40b18af943f9ba370ed73565e9f487663ef' # Useful links: # https://git.outfoxxed.me/quickshell/quickshell/commits/branch/master # https://aur.archlinux.org/packages/quickshell-git diff --git a/sdist/gentoo/illogical-impulse-widgets/quickshell-9999.ebuild b/sdist/gentoo/illogical-impulse-quickshell-git/illogical-impulse-quickshell-git-9999-r1.ebuild similarity index 90% rename from sdist/gentoo/illogical-impulse-widgets/quickshell-9999.ebuild rename to sdist/gentoo/illogical-impulse-quickshell-git/illogical-impulse-quickshell-git-9999-r1.ebuild index 89a092516..d87712f46 100644 --- a/sdist/gentoo/illogical-impulse-widgets/quickshell-9999.ebuild +++ b/sdist/gentoo/illogical-impulse-quickshell-git/illogical-impulse-quickshell-git-9999-r1.ebuild @@ -3,21 +3,17 @@ EAPI=8 -inherit cmake +inherit cmake git-r3 DESCRIPTION="Toolkit for building desktop widgets using QtQuick" HOMEPAGE="https://quickshell.org/" -if [[ "${PV}" = *9999 ]]; then - inherit git-r3 - EGIT_REPO_URI="https://github.com/quickshell-mirror/${PN^}.git" -else - SRC_URI="https://github.com/quickshell-mirror/${PN}/archive/refs/tags/v${PV}.tar.gz -> ${P}.tar.gz" -fi +EGIT_REPO_URI="https://github.com/quickshell-mirror/quickshell.git" +EGIT_COMMIT="3e2ce40b18af943f9ba370ed73565e9f487663ef" +KEYWORDS="~amd64 ~arm64 ~x86" LICENSE="LGPL-3" SLOT="0" -KEYWORDS="~amd64 ~arm64 ~x86" # Upstream recommends leaving all build options enabled by default IUSE="+breakpad +jemalloc +sockets +wayland +layer-shell +session-lock +toplevel-management +screencopy +X +pipewire +tray +mpris +pam +hyprland +hyprland-global-shortcuts +hyprland-focus-grab +i3 +i3-ipc +bluetooth" @@ -38,21 +34,27 @@ RDEPEND=" mpris? ( dev-qt/qtdbus ) pam? ( sys-libs/pam ) bluetooth? ( net-wireless/bluez ) + + + " DEPEND="${RDEPEND}" BDEPEND=" || ( >=sys-devel/gcc-14:* >=llvm-core/clang-17:* ) - dev-build/cmake - dev-build/ninja - virtual/pkgconfig - dev-cpp/cli11 + dev-util/spirv-tools dev-qt/qtshadertools:6 - breakpad? ( dev-util/breakpad ) wayland? ( dev-util/wayland-scanner dev-libs/wayland-protocols ) + dev-cpp/cli11 + dev-build/ninja + dev-build/cmake + dev-vcs/git + virtual/pkgconfig + breakpad? ( dev-util/breakpad ) + " src_configure(){ diff --git a/sdist/gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r1.ebuild b/sdist/gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r2.ebuild similarity index 82% rename from sdist/gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r1.ebuild rename to sdist/gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r2.ebuild index e2b878a74..fe6109223 100644 --- a/sdist/gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r1.ebuild +++ b/sdist/gentoo/illogical-impulse-widgets/illogical-impulse-widgets-1.0-r2.ebuild @@ -15,8 +15,11 @@ DEPEND="" RDEPEND=" gui-apps/fuzzel dev-libs/glib - gui-apps/quickshell + media-gfx/imagemagick + gui-apps/hypridle + gui-libs/hyprutils + gui-apps/hyprlock + gui-apps/hyprpicker app-i18n/translate-shell gui-apps/wlogout - media-gfx/imagemagick " diff --git a/sdist/gentoo/install-deps.sh b/sdist/gentoo/install-deps.sh index 949bbf8eb..7be8f30a9 100644 --- a/sdist/gentoo/install-deps.sh +++ b/sdist/gentoo/install-deps.sh @@ -37,7 +37,7 @@ fi arch=$(portageq envvar ACCEPT_KEYWORDS) # Exclude hyprland, will deal with that separately -metapkgs=(illogical-impulse-{audio,backlight,basic,bibata-modern-classic-bin,fonts-themes,hyprland,kde,microtex-git,oneui4-icons-git,portal,python,screencapture,toolkit,widgets}) +metapkgs=(illogical-impulse-{audio,backlight,basic,bibata-modern-classic-bin,fonts-themes,hyprland,kde,microtex-git,oneui4-icons-git,portal,python,quickshell-git,screencapture,toolkit,widgets}) ebuild_dir="/var/db/repos/localrepo" diff --git a/sdist/gentoo/keywords b/sdist/gentoo/keywords index 9716ace7a..a10d1111e 100644 --- a/sdist/gentoo/keywords +++ b/sdist/gentoo/keywords @@ -9,6 +9,7 @@ app-misc/illogical-impulse-microtex-git app-misc/illogical-impulse-oneui4-icons-git app-misc/illogical-impulse-portal app-misc/illogical-impulse-python +app-misc/illogical-impulse-quickshell-git app-misc/illogical-impulse-screencapture app-misc/illogical-impulse-toolkit app-misc/illogical-impulse-widgets @@ -39,3 +40,6 @@ gui-libs/hyprland-qt-support ** gui-libs/hyprland-qtutils ** gui-wm/hyprland ** x11-libs/libxkbcommon +dev-util/breakpad +dev-libs/linux-syscall-support +dev-embedded/libdisasm diff --git a/sdist/gentoo/useflags b/sdist/gentoo/useflags index c3e12ed8e..e7c9755fc 100644 --- a/sdist/gentoo/useflags +++ b/sdist/gentoo/useflags @@ -111,7 +111,7 @@ sys-power/upower introspection gui-apps/fuzzel png svg dev-libs/glib dbus elf introspection mime xattr # ngl idk about nm-connection-editor. Works fine without -gui-apps/quickshell -X -i3 -i3-ipc -breakpad bluetooth hyprland hyprland-focus-grab hyprland-global-shortcuts jemalloc layer-shell mpris pam pipewire screencopy session-lock sockets toplevel-management tray wayland +gui-apps/quickshell -X -i3 -i3-ipc breakpad bluetooth hyprland hyprland-focus-grab hyprland-global-shortcuts jemalloc layer-shell mpris pam pipewire screencopy session-lock sockets toplevel-management tray wayland #app-i18n/translate-shell (nothing needed) #gui-apps/wlogout (no use flags) media-gfx/imagemagick xml diff --git a/setup b/setup index 88fb487e1..ebbab3988 100755 --- a/setup +++ b/setup @@ -18,6 +18,9 @@ Syntax: Subcommands: install (Default) Install/Reinstall/Update illogical-impulse. + install-deps Run the install step \"1. Install dependencies\" + install-setups Run the install step \"2. Setup for permissions/services etc\" + install-files Run the install step \"3. Copying config files\" exp-uninstall (Experimental) Uninstall illogical-impulse. exp-update (Experimental) Update illogical-impulse without fully reinstall. help Show this help message. @@ -30,7 +33,8 @@ case $1 in # Global help help|--help|-h)showhelp_global;exit;; # Correct subcommand - install|exp-uninstall|exp-update)SCRIPT_SUBCOMMAND=$1;shift;; + install|install-deps|install-setups|install-files|exp-uninstall|exp-update) + SCRIPT_SUBCOMMAND=$1;shift;; # No subcommand -*|"")SCRIPT_SUBCOMMAND=install;; # Wrong subcommand @@ -60,6 +64,31 @@ case ${SCRIPT_SUBCOMMAND} in fi fi ;; + install-deps) + source ./sdata/options/install.sh + if [[ "${SKIP_ALLDEPS}" != true ]]; then + printf "${STY_CYAN}[$0]: 1. Install dependencies\n${STY_RST}" + source ./sdata/step/1.install-deps-selector.sh + fi + ;; + install-setups) + source ./sdata/options/install.sh + if [[ "${SKIP_ALLSETUPS}" != true ]]; then + printf "${STY_CYAN}[$0]: 2. Setup for permissions/services etc\n${STY_RST}" + source ./sdata/step/2.install-setups-selector.sh + fi + ;; + install-files) + source ./sdata/options/install.sh + if [[ "${SKIP_ALLFILES}" != true ]]; then + printf "${STY_CYAN}[$0]: 3. Copying config files\n${STY_RST}" + if [[ "${EXPERIMENTAL_FILES_SCRIPT}" == true ]]; then + source ./sdata/step/3.install-files.experimental.sh + else + source ./sdata/step/3.install-files.sh + fi + fi + ;; exp-uninstall) source ./sdata/options/exp-uninstall.sh source ./sdata/step/exp-uninstall.sh