forked from Shinonome/dots-hyprland
Merge remote-tracking branch 'refs/remotes/origin/main'
This commit is contained in:
@@ -58,7 +58,9 @@ bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs -c $
|
|||||||
# Screenshot, Record, OCR, Color picker, Clipboard history
|
# Screenshot, Record, OCR, Color picker, Clipboard history
|
||||||
bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # [hidden] Clipboard history >> clipboard (fallback)
|
bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # [hidden] Clipboard history >> clipboard (fallback)
|
||||||
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)
|
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)
|
||||||
bindd = Super+Shift, S, Screen snip, exec, qs -p ~/.config/quickshell/$qsConfig/screenshot.qml || pidof slurp || hyprshot --freeze --clipboard-only --mode region --silent # Screen snip
|
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
|
# OCR
|
||||||
bindd = Super+Shift, T, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden]
|
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
|
# Color picker
|
||||||
@@ -67,9 +69,9 @@ bindd = Super+Shift, C, Color picker, exec, hyprpicker -a # Pick color (Hex) >>
|
|||||||
bindld = ,Print, Screenshot >> clipboard ,exec,grim - | wl-copy # Screenshot >> clipboard
|
bindld = ,Print, Screenshot >> clipboard ,exec,grim - | wl-copy # Screenshot >> clipboard
|
||||||
bindld = Ctrl,Print, Screenshot >> clipboard & save, exec, mkdir -p $(xdg-user-dir PICTURES)/Screenshots && grim $(xdg-user-dir PICTURES)/Screenshots/Screenshot_"$(date '+%Y-%m-%d_%H.%M.%S')".png # Screenshot >> clipboard & file
|
bindld = Ctrl,Print, Screenshot >> clipboard & save, exec, mkdir -p $(xdg-user-dir PICTURES)/Screenshots && grim $(xdg-user-dir PICTURES)/Screenshots/Screenshot_"$(date '+%Y-%m-%d_%H.%M.%S')".png # Screenshot >> clipboard & file
|
||||||
# Recording stuff
|
# Recording stuff
|
||||||
bindd = Super+Alt, R, Record region (no sound), exec, ~/.config/hypr/hyprland/scripts/record.sh # Record region (no sound)
|
bindl = Super+Alt, R, exec, ~/.config/hypr/hyprland/scripts/record.sh # Record region (no sound)
|
||||||
bindd = Ctrl+Alt, R, Record screen (no sound), exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen # [hidden] Record screen (no sound)
|
bindl = Ctrl+Alt, R, exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen # [hidden] Record screen (no sound)
|
||||||
bindd = Super+Shift+Alt, R, Record screen (with sound), exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen-sound # Record screen (with sound)
|
bindl = Super+Shift+Alt, R, exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen-sound # Record screen (with sound)
|
||||||
# AI
|
# AI
|
||||||
bindd = Super+Shift+Alt, mouse:273, Generate AI summary for selected text, exec, ~/.config/hypr/hyprland/scripts/ai/primary-buffer-query.sh # AI summary for selected text
|
bindd = Super+Shift+Alt, mouse:273, Generate AI summary for selected text, exec, ~/.config/hypr/hyprland/scripts/ai/primary-buffer-query.sh # AI summary for selected text
|
||||||
|
|
||||||
@@ -247,7 +249,7 @@ bind = Ctrl+Alt, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available
|
|||||||
bind = Super, E, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "dolphin" "nautilus" "nemo" "thunar" "${TERMINAL}" "kitty -1 fish -c yazi" # File manager
|
bind = Super, E, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "dolphin" "nautilus" "nemo" "thunar" "${TERMINAL}" "kitty -1 fish -c yazi" # File manager
|
||||||
bind = Super, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "google-chrome-stable" "zen-browser" "firefox" "brave" "chromium" "microsoft-edge-stable" "opera" "librewolf" # Browser
|
bind = Super, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "google-chrome-stable" "zen-browser" "firefox" "brave" "chromium" "microsoft-edge-stable" "opera" "librewolf" # Browser
|
||||||
bind = Super, C, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "code" "codium" "cursor" "zed" "zedit" "zeditor" "kate" "gnome-text-editor" "emacs" "command -v nvim && kitty -1 nvim" "command -v micro && kitty -1 micro" # Code editor
|
bind = Super, C, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "code" "codium" "cursor" "zed" "zedit" "zeditor" "kate" "gnome-text-editor" "emacs" "command -v nvim && kitty -1 nvim" "command -v micro && kitty -1 micro" # Code editor
|
||||||
bind = Super+Shift, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "wps" "onlyoffice-desktopeditors" # Office software
|
bind = Ctrl+Super+Shift+Alt, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "wps" "onlyoffice-desktopeditors" "libreoffice" # Office software
|
||||||
bind = Super, X, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kate" "gnome-text-editor" "emacs" # Text editor
|
bind = Super, X, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kate" "gnome-text-editor" "emacs" # Text editor
|
||||||
bind = Ctrl+Super, V, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol" # Volume mixer
|
bind = Ctrl+Super, V, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol" # Volume mixer
|
||||||
bind = Super, I, exec, XDG_CURRENT_DESKTOP=gnome ~/.config/hypr/hyprland/scripts/launch_first_available.sh "qs -p ~/.config/quickshell/$qsConfig/settings.qml" "systemsettings" "gnome-control-center" "better-control" # Settings app
|
bind = Super, I, exec, XDG_CURRENT_DESKTOP=gnome ~/.config/hypr/hyprland/scripts/launch_first_available.sh "qs -p ~/.config/quickshell/$qsConfig/settings.qml" "systemsettings" "gnome-control-center" "better-control" # Settings app
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ layerrule = noanim, quickshell:lockWindowPusher
|
|||||||
layerrule = animation fade, quickshell:notificationPopup
|
layerrule = animation fade, quickshell:notificationPopup
|
||||||
layerrule = noanim, quickshell:overview
|
layerrule = noanim, quickshell:overview
|
||||||
layerrule = animation slide bottom, quickshell:osk
|
layerrule = animation slide bottom, quickshell:osk
|
||||||
|
layerrule = noanim, quickshell:regionSelector
|
||||||
layerrule = noanim, quickshell:screenshot
|
layerrule = noanim, quickshell:screenshot
|
||||||
layerrule = blur, quickshell:session
|
layerrule = blur, quickshell:session
|
||||||
layerrule = noanim, quickshell:session
|
layerrule = noanim, quickshell:session
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ Singleton {
|
|||||||
property bool osdVolumeOpen: false
|
property bool osdVolumeOpen: false
|
||||||
property bool oskOpen: false
|
property bool oskOpen: false
|
||||||
property bool overviewOpen: false
|
property bool overviewOpen: false
|
||||||
property bool wallpaperSelectorOpen: false
|
property bool regionSelectorOpen: false
|
||||||
property bool screenLocked: false
|
property bool screenLocked: false
|
||||||
property bool screenLockContainsCharacters: false
|
property bool screenLockContainsCharacters: false
|
||||||
property bool screenUnlockFailed: false
|
property bool screenUnlockFailed: false
|
||||||
property bool sessionOpen: false
|
property bool sessionOpen: false
|
||||||
property bool superDown: false
|
property bool superDown: false
|
||||||
property bool superReleaseMightTrigger: true
|
property bool superReleaseMightTrigger: true
|
||||||
|
property bool wallpaperSelectorOpen: false
|
||||||
property bool workspaceShowNumbers: false
|
property bool workspaceShowNumbers: false
|
||||||
|
|
||||||
onSidebarRightOpenChanged: {
|
onSidebarRightOpenChanged: {
|
||||||
|
|||||||
@@ -80,17 +80,17 @@ StyledPopup {
|
|||||||
ResourceItem {
|
ResourceItem {
|
||||||
icon: "clock_loader_60"
|
icon: "clock_loader_60"
|
||||||
label: Translation.tr("Used:")
|
label: Translation.tr("Used:")
|
||||||
value: formatKB(ResourceUsage.memoryUsed)
|
value: root.formatKB(ResourceUsage.memoryUsed)
|
||||||
}
|
}
|
||||||
ResourceItem {
|
ResourceItem {
|
||||||
icon: "check_circle"
|
icon: "check_circle"
|
||||||
label: Translation.tr("Free:")
|
label: Translation.tr("Free:")
|
||||||
value: formatKB(ResourceUsage.memoryFree)
|
value: root.formatKB(ResourceUsage.memoryFree)
|
||||||
}
|
}
|
||||||
ResourceItem {
|
ResourceItem {
|
||||||
icon: "empty_dashboard"
|
icon: "empty_dashboard"
|
||||||
label: Translation.tr("Total:")
|
label: Translation.tr("Total:")
|
||||||
value: formatKB(ResourceUsage.memoryTotal)
|
value: root.formatKB(ResourceUsage.memoryTotal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,17 +109,17 @@ StyledPopup {
|
|||||||
ResourceItem {
|
ResourceItem {
|
||||||
icon: "clock_loader_60"
|
icon: "clock_loader_60"
|
||||||
label: Translation.tr("Used:")
|
label: Translation.tr("Used:")
|
||||||
value: formatKB(ResourceUsage.swapUsed)
|
value: root.formatKB(ResourceUsage.swapUsed)
|
||||||
}
|
}
|
||||||
ResourceItem {
|
ResourceItem {
|
||||||
icon: "check_circle"
|
icon: "check_circle"
|
||||||
label: Translation.tr("Free:")
|
label: Translation.tr("Free:")
|
||||||
value: formatKB(ResourceUsage.swapFree)
|
value: root.formatKB(ResourceUsage.swapFree)
|
||||||
}
|
}
|
||||||
ResourceItem {
|
ResourceItem {
|
||||||
icon: "empty_dashboard"
|
icon: "empty_dashboard"
|
||||||
label: Translation.tr("Total:")
|
label: Translation.tr("Total:")
|
||||||
value: formatKB(ResourceUsage.swapTotal)
|
value: root.formatKB(ResourceUsage.swapTotal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,22 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: Config.options.bar.utilButtons.showScreenRecord
|
||||||
|
visible: Config.options.bar.utilButtons.showScreenRecord
|
||||||
|
sourceComponent: CircleUtilButton {
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
onClicked: Quickshell.execDetached(["bash", "-c", "~/.config/hypr/hyprland/scripts/record.sh"])
|
||||||
|
MaterialSymbol {
|
||||||
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
|
fill: 1
|
||||||
|
text: "videocam"
|
||||||
|
iconSize: Appearance.font.pixelSize.large
|
||||||
|
color: Appearance.colors.colOnLayer2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
active: Config.options.bar.utilButtons.showColorPicker
|
active: Config.options.bar.utilButtons.showColorPicker
|
||||||
visible: Config.options.bar.utilButtons.showColorPicker
|
visible: Config.options.bar.utilButtons.showColorPicker
|
||||||
|
|||||||
@@ -39,62 +39,57 @@ Singleton {
|
|||||||
property real contentTransparency: Config?.options.appearance.transparency.enable ? Config?.options.appearance.transparency.automatic ? autoContentTransparency : Config?.options.appearance.transparency.contentTransparency : 0
|
property real contentTransparency: Config?.options.appearance.transparency.enable ? Config?.options.appearance.transparency.automatic ? autoContentTransparency : Config?.options.appearance.transparency.contentTransparency : 0
|
||||||
|
|
||||||
m3colors: QtObject {
|
m3colors: QtObject {
|
||||||
property bool darkmode: false
|
property bool darkmode: true
|
||||||
property bool transparent: false
|
property bool transparent: false
|
||||||
property color m3primary_paletteKeyColor: "#91689E"
|
property color m3background: "#141313"
|
||||||
property color m3secondary_paletteKeyColor: "#837186"
|
property color m3onBackground: "#e6e1e1"
|
||||||
property color m3tertiary_paletteKeyColor: "#9D6A67"
|
property color m3surface: "#141313"
|
||||||
property color m3neutral_paletteKeyColor: "#7C757B"
|
property color m3surfaceDim: "#141313"
|
||||||
property color m3neutral_variant_paletteKeyColor: "#7D747D"
|
property color m3surfaceBright: "#3a3939"
|
||||||
property color m3background: "#161217"
|
property color m3surfaceContainerLowest: "#0f0e0e"
|
||||||
property color m3onBackground: "#EAE0E7"
|
property color m3surfaceContainerLow: "#1c1b1c"
|
||||||
property color m3surface: "#161217"
|
property color m3surfaceContainer: "#201f20"
|
||||||
property color m3surfaceDim: "#161217"
|
property color m3surfaceContainerHigh: "#2b2a2a"
|
||||||
property color m3surfaceBright: "#3D373D"
|
property color m3surfaceContainerHighest: "#363435"
|
||||||
property color m3surfaceContainerLowest: "#110D12"
|
property color m3onSurface: "#e6e1e1"
|
||||||
property color m3surfaceContainerLow: "#1F1A1F"
|
property color m3surfaceVariant: "#49464a"
|
||||||
property color m3surfaceContainer: "#231E23"
|
property color m3onSurfaceVariant: "#cbc5ca"
|
||||||
property color m3surfaceContainerHigh: "#2D282E"
|
property color m3inverseSurface: "#e6e1e1"
|
||||||
property color m3surfaceContainerHighest: "#383339"
|
property color m3inverseOnSurface: "#313030"
|
||||||
property color m3onSurface: "#EAE0E7"
|
property color m3outline: "#948f94"
|
||||||
property color m3surfaceVariant: "#4C444D"
|
property color m3outlineVariant: "#49464a"
|
||||||
property color m3onSurfaceVariant: "#CFC3CD"
|
|
||||||
property color m3inverseSurface: "#EAE0E7"
|
|
||||||
property color m3inverseOnSurface: "#342F34"
|
|
||||||
property color m3outline: "#988E97"
|
|
||||||
property color m3outlineVariant: "#4C444D"
|
|
||||||
property color m3shadow: "#000000"
|
property color m3shadow: "#000000"
|
||||||
property color m3scrim: "#000000"
|
property color m3scrim: "#000000"
|
||||||
property color m3surfaceTint: "#E5B6F2"
|
property color m3surfaceTint: "#cbc4cb"
|
||||||
property color m3primary: "#E5B6F2"
|
property color m3primary: "#cbc4cb"
|
||||||
property color m3onPrimary: "#452152"
|
property color m3onPrimary: "#322f34"
|
||||||
property color m3primaryContainer: "#5D386A"
|
property color m3primaryContainer: "#2d2a2f"
|
||||||
property color m3onPrimaryContainer: "#F9D8FF"
|
property color m3onPrimaryContainer: "#bcb6bc"
|
||||||
property color m3inversePrimary: "#775084"
|
property color m3inversePrimary: "#615d63"
|
||||||
property color m3secondary: "#D5C0D7"
|
property color m3secondary: "#cac5c8"
|
||||||
property color m3onSecondary: "#392C3D"
|
property color m3onSecondary: "#323032"
|
||||||
property color m3secondaryContainer: "#534457"
|
property color m3secondaryContainer: "#4d4b4d"
|
||||||
property color m3onSecondaryContainer: "#F2DCF3"
|
property color m3onSecondaryContainer: "#ece6e9"
|
||||||
property color m3tertiary: "#F5B7B3"
|
property color m3tertiary: "#d1c3c6"
|
||||||
property color m3onTertiary: "#4C2523"
|
property color m3onTertiary: "#372e30"
|
||||||
property color m3tertiaryContainer: "#BA837F"
|
property color m3tertiaryContainer: "#31292b"
|
||||||
property color m3onTertiaryContainer: "#000000"
|
property color m3onTertiaryContainer: "#c1b4b7"
|
||||||
property color m3error: "#FFB4AB"
|
property color m3error: "#ffb4ab"
|
||||||
property color m3onError: "#690005"
|
property color m3onError: "#690005"
|
||||||
property color m3errorContainer: "#93000A"
|
property color m3errorContainer: "#93000a"
|
||||||
property color m3onErrorContainer: "#FFDAD6"
|
property color m3onErrorContainer: "#ffdad6"
|
||||||
property color m3primaryFixed: "#F9D8FF"
|
property color m3primaryFixed: "#e7e0e7"
|
||||||
property color m3primaryFixedDim: "#E5B6F2"
|
property color m3primaryFixedDim: "#cbc4cb"
|
||||||
property color m3onPrimaryFixed: "#2E0A3C"
|
property color m3onPrimaryFixed: "#1d1b1f"
|
||||||
property color m3onPrimaryFixedVariant: "#5D386A"
|
property color m3onPrimaryFixedVariant: "#49454b"
|
||||||
property color m3secondaryFixed: "#F2DCF3"
|
property color m3secondaryFixed: "#e6e1e4"
|
||||||
property color m3secondaryFixedDim: "#D5C0D7"
|
property color m3secondaryFixedDim: "#cac5c8"
|
||||||
property color m3onSecondaryFixed: "#241727"
|
property color m3onSecondaryFixed: "#1d1b1d"
|
||||||
property color m3onSecondaryFixedVariant: "#514254"
|
property color m3onSecondaryFixedVariant: "#484648"
|
||||||
property color m3tertiaryFixed: "#FFDAD7"
|
property color m3tertiaryFixed: "#eddfe1"
|
||||||
property color m3tertiaryFixedDim: "#F5B7B3"
|
property color m3tertiaryFixedDim: "#d1c3c6"
|
||||||
property color m3onTertiaryFixed: "#331110"
|
property color m3onTertiaryFixed: "#211a1c"
|
||||||
property color m3onTertiaryFixedVariant: "#663B39"
|
property color m3onTertiaryFixedVariant: "#4e4447"
|
||||||
property color m3success: "#B5CCBA"
|
property color m3success: "#B5CCBA"
|
||||||
property color m3onSuccess: "#213528"
|
property color m3onSuccess: "#213528"
|
||||||
property color m3successContainer: "#374B3E"
|
property color m3successContainer: "#374B3E"
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ Singleton {
|
|||||||
property string networkEthernet: "kcmshell6 kcm_networkmanagement"
|
property string networkEthernet: "kcmshell6 kcm_networkmanagement"
|
||||||
property string taskManager: "plasma-systemmonitor --page-name Processes"
|
property string taskManager: "plasma-systemmonitor --page-name Processes"
|
||||||
property string terminal: "kitty -1" // This is only for shell actions
|
property string terminal: "kitty -1" // This is only for shell actions
|
||||||
|
property string volumeMixer: `~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol"`
|
||||||
}
|
}
|
||||||
|
|
||||||
property JsonObject background: JsonObject {
|
property JsonObject background: JsonObject {
|
||||||
@@ -211,6 +212,7 @@ Singleton {
|
|||||||
property bool showKeyboardToggle: true
|
property bool showKeyboardToggle: true
|
||||||
property bool showDarkModeToggle: true
|
property bool showDarkModeToggle: true
|
||||||
property bool showPerformanceProfileToggle: false
|
property bool showPerformanceProfileToggle: false
|
||||||
|
property bool showScreenRecord: false
|
||||||
}
|
}
|
||||||
property JsonObject tray: JsonObject {
|
property JsonObject tray: JsonObject {
|
||||||
property bool monochromeIcons: true
|
property bool monochromeIcons: true
|
||||||
@@ -244,6 +246,7 @@ Singleton {
|
|||||||
property JsonObject battery: JsonObject {
|
property JsonObject battery: JsonObject {
|
||||||
property int low: 20
|
property int low: 20
|
||||||
property int critical: 5
|
property int critical: 5
|
||||||
|
property int full: 101
|
||||||
property bool automaticSuspend: true
|
property bool automaticSuspend: true
|
||||||
property int suspend: 3
|
property int suspend: 3
|
||||||
}
|
}
|
||||||
@@ -364,6 +367,11 @@ Singleton {
|
|||||||
property string shellCommand: "$"
|
property string shellCommand: "$"
|
||||||
property string webSearch: "?"
|
property string webSearch: "?"
|
||||||
}
|
}
|
||||||
|
property JsonObject imageSearch: JsonObject {
|
||||||
|
property string imageSearchEngineBaseUrl: "https://lens.google.com/uploadbyurl?url="
|
||||||
|
property string fileUploadApiEndpoint: "https://uguu.se/upload"
|
||||||
|
property bool useCircleSelection: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property JsonObject sidebar: JsonObject {
|
property JsonObject sidebar: JsonObject {
|
||||||
@@ -399,12 +407,12 @@ Singleton {
|
|||||||
property JsonObject android: JsonObject {
|
property JsonObject android: JsonObject {
|
||||||
property int columns: 5
|
property int columns: 5
|
||||||
property list<var> toggles: [
|
property list<var> toggles: [
|
||||||
{ type: "network", size: 2 },
|
{ "size": 2, "type": "network" },
|
||||||
{ type: "bluetooth", size: 2 },
|
{ "size": 2, "type": "bluetooth" },
|
||||||
{ type: "idleInhibitor", size: 1 },
|
{ "size": 1, "type": "idleInhibitor" },
|
||||||
{ type: "easyEffects", size: 1 },
|
{ "size": 1, "type": "mic" },
|
||||||
{ type: "nightLight", size: 2 },
|
{ "size": 2, "type": "audio" },
|
||||||
{ type: "darkMode", size: 2 }
|
{ "size": 2, "type": "nightLight" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,13 +425,18 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property JsonObject sounds: JsonObject {
|
||||||
|
property bool battery: false
|
||||||
|
property bool pomodoro: false
|
||||||
|
property string theme: "freedesktop"
|
||||||
|
}
|
||||||
|
|
||||||
property JsonObject time: JsonObject {
|
property JsonObject time: JsonObject {
|
||||||
// https://doc.qt.io/qt-6/qtime.html#toString
|
// https://doc.qt.io/qt-6/qtime.html#toString
|
||||||
property string format: "hh:mm"
|
property string format: "hh:mm"
|
||||||
property string shortDateFormat: "dd/MM"
|
property string shortDateFormat: "dd/MM"
|
||||||
property string dateFormat: "ddd, dd/MM"
|
property string dateFormat: "ddd, dd/MM"
|
||||||
property JsonObject pomodoro: JsonObject {
|
property JsonObject pomodoro: JsonObject {
|
||||||
property string alertSound: ""
|
|
||||||
property int breakTime: 300
|
property int breakTime: 300
|
||||||
property int cyclesBeforeLongBreak: 4
|
property int cyclesBeforeLongBreak: 4
|
||||||
property int focus: 1500
|
property int focus: 1500
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ Button {
|
|||||||
property bool bounce: true
|
property bool bounce: true
|
||||||
property real baseWidth: contentItem.implicitWidth + horizontalPadding * 2
|
property real baseWidth: contentItem.implicitWidth + horizontalPadding * 2
|
||||||
property real baseHeight: contentItem.implicitHeight + verticalPadding * 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 clickedWidth: baseWidth + (isAtSide ? 10 : 20)
|
||||||
property real clickedHeight: baseHeight
|
property real clickedHeight: baseHeight
|
||||||
property var parentGroup: root.parent
|
property var parentGroup: root.parent
|
||||||
@@ -61,10 +63,12 @@ Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
|
enabled: root.enableImplicitWidthAnimation
|
||||||
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
|
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
|
enabled: root.enableImplicitHeightAnimation
|
||||||
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
|
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,18 +24,19 @@ MouseArea { // Notification group area
|
|||||||
|
|
||||||
property real dragConfirmThreshold: 70 // Drag further to discard notification
|
property real dragConfirmThreshold: 70 // Drag further to discard notification
|
||||||
property real dismissOvershoot: 20 // Account for gaps and bouncy animations
|
property real dismissOvershoot: 20 // Account for gaps and bouncy animations
|
||||||
property var qmlParent: root.parent.parent // There's something between this and the parent ListView
|
property var qmlParent: root?.parent?.parent // There's something between this and the parent ListView
|
||||||
property var parentDragIndex: qmlParent.dragIndex
|
property var parentDragIndex: qmlParent?.dragIndex
|
||||||
property var parentDragDistance: qmlParent.dragDistance
|
property var parentDragDistance: qmlParent?.dragDistance
|
||||||
property var dragIndexDiff: Math.abs(parentDragIndex - index)
|
property var dragIndexDiff: Math.abs(parentDragIndex - index)
|
||||||
property real xOffset: dragIndexDiff == 0 ? Math.max(0, parentDragDistance) :
|
property real xOffset: dragIndexDiff == 0 ? parentDragDistance :
|
||||||
parentDragDistance > dragConfirmThreshold ? 0 :
|
Math.abs(parentDragDistance) > dragConfirmThreshold ? 0 :
|
||||||
dragIndexDiff == 1 ? Math.max(0, parentDragDistance * 0.3) :
|
dragIndexDiff == 1 ? (parentDragDistance * 0.3) :
|
||||||
dragIndexDiff == 2 ? Math.max(0, parentDragDistance * 0.1) : 0
|
dragIndexDiff == 2 ? (parentDragDistance * 0.1) : 0
|
||||||
|
|
||||||
function destroyWithAnimation() {
|
function destroyWithAnimation(left = false) {
|
||||||
root.qmlParent.resetDrag()
|
root.qmlParent.resetDrag()
|
||||||
background.anchors.leftMargin = background.anchors.leftMargin; // Break binding
|
background.anchors.leftMargin = background.anchors.leftMargin; // Break binding
|
||||||
|
destroyAnimation.left = left;
|
||||||
destroyAnimation.running = true;
|
destroyAnimation.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,12 +53,13 @@ MouseArea { // Notification group area
|
|||||||
|
|
||||||
SequentialAnimation { // Drag finish animation
|
SequentialAnimation { // Drag finish animation
|
||||||
id: destroyAnimation
|
id: destroyAnimation
|
||||||
|
property bool left: true
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
target: background.anchors
|
target: background.anchors
|
||||||
property: "leftMargin"
|
property: "leftMargin"
|
||||||
to: root.width + root.dismissOvershoot
|
to: (root.width + root.dismissOvershoot) * (destroyAnimation.left ? -1 : 1)
|
||||||
duration: Appearance.animation.elementMove.duration
|
duration: Appearance.animation.elementMove.duration
|
||||||
easing.type: Appearance.animation.elementMove.type
|
easing.type: Appearance.animation.elementMove.type
|
||||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||||
@@ -102,8 +104,8 @@ MouseArea { // Notification group area
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDragReleased: (diffX, diffY) => {
|
onDragReleased: (diffX, diffY) => {
|
||||||
if (diffX > root.dragConfirmThreshold)
|
if (Math.abs(diffX) > root.dragConfirmThreshold)
|
||||||
root.destroyWithAnimation();
|
root.destroyWithAnimation(diffX < 0);
|
||||||
else
|
else
|
||||||
dragManager.resetDrag();
|
dragManager.resetDrag();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ Item { // Notification item area
|
|||||||
property var parentDragIndex: qmlParent?.dragIndex ?? -1
|
property var parentDragIndex: qmlParent?.dragIndex ?? -1
|
||||||
property var parentDragDistance: qmlParent?.dragDistance ?? 0
|
property var parentDragDistance: qmlParent?.dragDistance ?? 0
|
||||||
property var dragIndexDiff: Math.abs(parentDragIndex - index)
|
property var dragIndexDiff: Math.abs(parentDragIndex - index)
|
||||||
property real xOffset: dragIndexDiff == 0 ? Math.max(0, parentDragDistance) :
|
property real xOffset: dragIndexDiff == 0 ? parentDragDistance :
|
||||||
parentDragDistance > dragConfirmThreshold ? 0 :
|
Math.abs(parentDragDistance) > dragConfirmThreshold ? 0 :
|
||||||
dragIndexDiff == 1 ? Math.max(0, parentDragDistance * 0.3) :
|
dragIndexDiff == 1 ? (parentDragDistance * 0.3) :
|
||||||
dragIndexDiff == 2 ? Math.max(0, parentDragDistance * 0.1) : 0
|
dragIndexDiff == 2 ? (parentDragDistance * 0.1) : 0
|
||||||
|
|
||||||
implicitHeight: background.implicitHeight
|
implicitHeight: background.implicitHeight
|
||||||
|
|
||||||
@@ -53,9 +53,10 @@ Item { // Notification item area
|
|||||||
return processedBody
|
return processedBody
|
||||||
}
|
}
|
||||||
|
|
||||||
function destroyWithAnimation() {
|
function destroyWithAnimation(left = false) {
|
||||||
root.qmlParent.resetDrag()
|
root.qmlParent.resetDrag()
|
||||||
background.anchors.leftMargin = background.anchors.leftMargin; // Break binding
|
background.anchors.leftMargin = background.anchors.leftMargin; // Break binding
|
||||||
|
destroyAnimation.left = left;
|
||||||
destroyAnimation.running = true;
|
destroyAnimation.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,12 +68,13 @@ Item { // Notification item area
|
|||||||
|
|
||||||
SequentialAnimation { // Drag finish animation
|
SequentialAnimation { // Drag finish animation
|
||||||
id: destroyAnimation
|
id: destroyAnimation
|
||||||
|
property bool left: true
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
target: background.anchors
|
target: background.anchors
|
||||||
property: "leftMargin"
|
property: "leftMargin"
|
||||||
to: root.width + root.dismissOvershoot
|
to: (root.width + root.dismissOvershoot) * (destroyAnimation.left ? -1 : 1)
|
||||||
duration: Appearance.animation.elementMove.duration
|
duration: Appearance.animation.elementMove.duration
|
||||||
easing.type: Appearance.animation.elementMove.type
|
easing.type: Appearance.animation.elementMove.type
|
||||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||||
@@ -107,8 +109,8 @@ Item { // Notification item area
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDragReleased: (diffX, diffY) => {
|
onDragReleased: (diffX, diffY) => {
|
||||||
if (diffX > root.dragConfirmThreshold)
|
if (Math.abs(diffX) > root.dragConfirmThreshold)
|
||||||
root.destroyWithAnimation();
|
root.destroyWithAnimation(diffX < 0);
|
||||||
else
|
else
|
||||||
dragManager.resetDrag();
|
dragManager.resetDrag();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,15 +45,15 @@ Item {
|
|||||||
pathHints: ShapePath.PathSolid & ShapePath.PathNonIntersecting
|
pathHints: ShapePath.PathSolid & ShapePath.PathNonIntersecting
|
||||||
|
|
||||||
startX: switch (root.corner) {
|
startX: switch (root.corner) {
|
||||||
case RoundCorner.CornerEnum.TopLeft: return 0;
|
case RoundCorner.CornerEnum.TopLeft:
|
||||||
case RoundCorner.CornerEnum.TopRight: return root.implicitSize;
|
|
||||||
case RoundCorner.CornerEnum.BottomLeft: return 0;
|
case RoundCorner.CornerEnum.BottomLeft: return 0;
|
||||||
|
case RoundCorner.CornerEnum.TopRight:
|
||||||
case RoundCorner.CornerEnum.BottomRight: return root.implicitSize;
|
case RoundCorner.CornerEnum.BottomRight: return root.implicitSize;
|
||||||
}
|
}
|
||||||
startY: switch (root.corner) {
|
startY: switch (root.corner) {
|
||||||
case RoundCorner.CornerEnum.TopLeft: return 0;
|
case RoundCorner.CornerEnum.TopLeft:
|
||||||
case RoundCorner.CornerEnum.TopRight: return 0;
|
case RoundCorner.CornerEnum.TopRight: return 0;
|
||||||
case RoundCorner.CornerEnum.BottomLeft: return root.implicitSize;
|
case RoundCorner.CornerEnum.BottomLeft:
|
||||||
case RoundCorner.CornerEnum.BottomRight: return root.implicitSize;
|
case RoundCorner.CornerEnum.BottomRight: return root.implicitSize;
|
||||||
}
|
}
|
||||||
PathAngleArc {
|
PathAngleArc {
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ Slider {
|
|||||||
TrackDot {
|
TrackDot {
|
||||||
required property real modelData
|
required property real modelData
|
||||||
value: modelData
|
value: modelData
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent?.verticalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.functions
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Section"
|
||||||
|
font {
|
||||||
|
pixelSize: Appearance.font.pixelSize.large
|
||||||
|
family: Appearance.font.family.title
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -167,7 +167,7 @@ Item {
|
|||||||
scale: root.scale
|
scale: root.scale
|
||||||
availableWorkspaceWidth: root.workspaceImplicitWidth
|
availableWorkspaceWidth: root.workspaceImplicitWidth
|
||||||
availableWorkspaceHeight: root.workspaceImplicitHeight
|
availableWorkspaceHeight: root.workspaceImplicitHeight
|
||||||
widgetMonitorId: root.monitor.id
|
widgetMonitor: HyprlandData.monitors.find(m => m.id == root.monitor.id)
|
||||||
windowData: windowByAddress[address]
|
windowData: windowByAddress[address]
|
||||||
|
|
||||||
property bool atInitPosition: (initX == x && initY == y)
|
property bool atInitPosition: (initX == x && initY == y)
|
||||||
|
|||||||
@@ -17,14 +17,30 @@ Item { // Window
|
|||||||
property var availableWorkspaceWidth
|
property var availableWorkspaceWidth
|
||||||
property var availableWorkspaceHeight
|
property var availableWorkspaceHeight
|
||||||
property bool restrictToWorkspace: true
|
property bool restrictToWorkspace: true
|
||||||
property real initX: Math.max((windowData?.at[0] - (monitorData?.x ?? 0) - monitorData?.reserved[0]) * root.scale, 0) + xOffset
|
property real widthRatio: {
|
||||||
property real initY: Math.max((windowData?.at[1] - (monitorData?.y ?? 0) - monitorData?.reserved[1]) * root.scale, 0) + yOffset
|
const widgetWidth = widgetMonitor.transform & 1 ? widgetMonitor.height : widgetMonitor.width;
|
||||||
|
const monitorWidth = monitorData.transform & 1 ? monitorData.height : monitorData.width;
|
||||||
|
(widgetWidth * monitorData.scale) / (monitorWidth * widgetMonitor.scale);
|
||||||
|
}
|
||||||
|
property real heightRatio: {
|
||||||
|
const widgetHeight = widgetMonitor.transform & 1 ? widgetMonitor.width : widgetMonitor.height;
|
||||||
|
const monitorHeight = monitorData.transform & 1 ? monitorData.width : monitorData.height;
|
||||||
|
(widgetHeight * monitorData.scale) / (monitorHeight * widgetMonitor.scale);
|
||||||
|
}
|
||||||
|
property real initX: {
|
||||||
|
Math.max((windowData?.at[0] - (monitorData?.x ?? 0) - monitorData?.reserved[0]) * widthRatio * root.scale, 0) + xOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
property real initY: {
|
||||||
|
Math.max((windowData?.at[1] - (monitorData?.y ?? 0) - monitorData?.reserved[1]) * heightRatio * root.scale, 0) + yOffset;
|
||||||
|
}
|
||||||
property real xOffset: 0
|
property real xOffset: 0
|
||||||
property real yOffset: 0
|
property real yOffset: 0
|
||||||
property int widgetMonitorId: 0
|
property var widgetMonitor
|
||||||
|
property int widgetMonitorId: widgetMonitor.id
|
||||||
property var targetWindowWidth: windowData?.size[0] * scale
|
|
||||||
property var targetWindowHeight: windowData?.size[1] * scale
|
property var targetWindowWidth: windowData?.size[0] * scale * widthRatio
|
||||||
|
property var targetWindowHeight: windowData?.size[1] * scale * heightRatio
|
||||||
property bool hovered: false
|
property bool hovered: false
|
||||||
property bool pressed: false
|
property bool pressed: false
|
||||||
|
|
||||||
@@ -35,11 +51,11 @@ Item { // Window
|
|||||||
property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth
|
property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth
|
||||||
|
|
||||||
property bool indicateXWayland: windowData?.xwayland ?? false
|
property bool indicateXWayland: windowData?.xwayland ?? false
|
||||||
|
|
||||||
x: initX
|
x: initX
|
||||||
y: initY
|
y: initY
|
||||||
width: windowData?.size[0] * root.scale
|
width: targetWindowWidth
|
||||||
height: windowData?.size[1] * root.scale
|
height: targetWindowHeight
|
||||||
opacity: windowData.monitor == widgetMonitorId ? 1 : 0.4
|
opacity: windowData.monitor == widgetMonitorId ? 1 : 0.4
|
||||||
|
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
@@ -90,7 +106,7 @@ Item { // Window
|
|||||||
// console.log("Icon ratio:", root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio)
|
// console.log("Icon ratio:", root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio)
|
||||||
// console.log("Scale:", root.monitorData.scale)
|
// console.log("Scale:", root.monitorData.scale)
|
||||||
// console.log("Final:", Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / root.monitorData.scale)
|
// console.log("Final:", Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / root.monitorData.scale)
|
||||||
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / root.monitorData.scale;
|
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio);
|
||||||
}
|
}
|
||||||
// mipmap: true
|
// mipmap: true
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
@@ -107,4 +123,4 @@ Item { // Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import qs.modules.common.widgets
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Shapes
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
required property color color
|
||||||
|
required property color overlayColor
|
||||||
|
required property list<point> points
|
||||||
|
property int strokeWidth: 10
|
||||||
|
|
||||||
|
function updatePoints() {
|
||||||
|
if (!root.dragging) return;
|
||||||
|
root.points.push({ x: root.mouseX, y: root.mouseY });
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: darkenOverlay
|
||||||
|
z: 1
|
||||||
|
anchors.fill: parent
|
||||||
|
color: root.overlayColor
|
||||||
|
}
|
||||||
|
|
||||||
|
Shape {
|
||||||
|
id: shape
|
||||||
|
z: 2
|
||||||
|
anchors.fill: parent
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
|
||||||
|
ShapePath {
|
||||||
|
id: shapePath
|
||||||
|
strokeWidth: root.strokeWidth
|
||||||
|
pathHints: ShapePath.PathLinear
|
||||||
|
fillColor: "transparent"
|
||||||
|
strokeColor: root.color
|
||||||
|
capStyle: ShapePath.RoundCap
|
||||||
|
joinStyle: ShapePath.RoundJoin
|
||||||
|
|
||||||
|
PathPolyline {
|
||||||
|
path: root.points
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
z: 2
|
||||||
|
x: root.mouseX
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
}
|
||||||
|
width: 1
|
||||||
|
color: root.color
|
||||||
|
}
|
||||||
|
Rectangle { // Horizontal
|
||||||
|
z: 2
|
||||||
|
y: root.mouseY
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
}
|
||||||
|
height: 1
|
||||||
|
color: root.color
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,568 @@
|
|||||||
|
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: Config.options.search.imageSearch.fileUploadApiEndpoint
|
||||||
|
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"
|
||||||
|
readonly property var windows: [...HyprlandData.windowList].sort((a, b) => {
|
||||||
|
// Sort floating=true windows before others
|
||||||
|
if (a.floating === b.floating) return 0;
|
||||||
|
return a.floating ? -1 : 1;
|
||||||
|
})
|
||||||
|
readonly property var layers: HyprlandData.layers
|
||||||
|
readonly property real falsePositivePreventionRatio: 0.5
|
||||||
|
|
||||||
|
readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen)
|
||||||
|
readonly property real monitorScale: hyprlandMonitor.scale
|
||||||
|
readonly property real monitorOffsetX: hyprlandMonitor.x
|
||||||
|
readonly property real monitorOffsetY: hyprlandMonitor.y
|
||||||
|
property int activeWorkspaceId: hyprlandMonitor.activeWorkspace?.id ?? 0
|
||||||
|
property string screenshotPath: `${root.screenshotDir}/image-${screen.name}`
|
||||||
|
property real dragStartX: 0
|
||||||
|
property real dragStartY: 0
|
||||||
|
property real draggingX: 0
|
||||||
|
property real draggingY: 0
|
||||||
|
property real dragDiffX: 0
|
||||||
|
property real dragDiffY: 0
|
||||||
|
property bool draggedAway: (dragDiffX !== 0 || dragDiffY !== 0)
|
||||||
|
property bool dragging: false
|
||||||
|
property list<point> points: []
|
||||||
|
property var mouseButton: null
|
||||||
|
property var imageRegions: []
|
||||||
|
readonly property list<var> windowRegions: filterWindowRegionsByLayers(
|
||||||
|
root.windows.filter(w => w.workspace.id === root.activeWorkspaceId),
|
||||||
|
root.layerRegions
|
||||||
|
).map(window => {
|
||||||
|
return {
|
||||||
|
at: [window.at[0] - root.monitorOffsetX, window.at[1] - root.monitorOffsetY],
|
||||||
|
size: [window.size[0], window.size[1]],
|
||||||
|
class: window.class,
|
||||||
|
title: window.title,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
readonly property list<var> layerRegions: {
|
||||||
|
const layersOfThisMonitor = root.layers[root.hyprlandMonitor.name]
|
||||||
|
const topLayers = layersOfThisMonitor?.levels["2"]
|
||||||
|
if (!topLayers) return [];
|
||||||
|
const nonBarTopLayers = topLayers
|
||||||
|
.filter(layer => !(layer.namespace.includes(":bar") || layer.namespace.includes(":verticalBar") || layer.namespace.includes(":dock")))
|
||||||
|
.map(layer => {
|
||||||
|
return {
|
||||||
|
at: [layer.x, layer.y],
|
||||||
|
size: [layer.w, layer.h],
|
||||||
|
namespace: layer.namespace,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const offsetAdjustedLayers = nonBarTopLayers.map(layer => {
|
||||||
|
return {
|
||||||
|
at: [layer.at[0] - root.monitorOffsetX, layer.at[1] - root.monitorOffsetY],
|
||||||
|
size: layer.size,
|
||||||
|
namespace: layer.namespace,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return offsetAdjustedLayers;
|
||||||
|
}
|
||||||
|
|
||||||
|
property 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 = 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 maxX = Math.max(...root.points.map(p => p.x));
|
||||||
|
const minX = Math.min(...root.points.map(p => p.x));
|
||||||
|
const maxY = Math.max(...root.points.map(p => p.y));
|
||||||
|
const minY = Math.min(...root.points.map(p => p.y));
|
||||||
|
root.regionX = minX;
|
||||||
|
root.regionY = minY;
|
||||||
|
root.regionWidth = maxX - minX;
|
||||||
|
root.regionHeight = maxY - minY;
|
||||||
|
}
|
||||||
|
// Detect if it was a click -> Try to select targeted region
|
||||||
|
if (root.draggingX === root.dragStartX && root.draggingY === root.dragStartY) {
|
||||||
|
if (root.targetedRegionX >= 0 && root.targetedRegionY >= 0) {
|
||||||
|
root.regionX = root.targetedRegionX;
|
||||||
|
root.regionY = root.targetedRegionY;
|
||||||
|
root.regionWidth = root.targetedRegionWidth;
|
||||||
|
root.regionHeight = root.targetedRegionHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
Rectangle {
|
||||||
|
z: 9999
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
topMargin: (Appearance.sizes.barHeight - implicitHeight) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
opacity: root.dragging ? 0 : 1
|
||||||
|
visible: opacity > 0
|
||||||
|
Behavior on opacity {
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
color: root.genericContentColor
|
||||||
|
radius: 10
|
||||||
|
border.width: 1
|
||||||
|
border.color: Appearance.m3colors.m3outlineVariant
|
||||||
|
implicitWidth: instructionsRow.implicitWidth + 10 * 2
|
||||||
|
implicitHeight: instructionsRow.implicitHeight + 5 * 2
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: instructionsRow
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 4
|
||||||
|
MaterialSymbol {
|
||||||
|
id: screenshotRegionIcon
|
||||||
|
// anchors.centerIn: parent
|
||||||
|
iconSize: Appearance.font.pixelSize.larger
|
||||||
|
text: switch(root.selectionMode) {
|
||||||
|
case RegionSelection.SelectionMode.RectCorners:
|
||||||
|
return "activity_zone"
|
||||||
|
break;
|
||||||
|
case RegionSelection.SelectionMode.Circle:
|
||||||
|
return "gesture"
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return "activity_zone"
|
||||||
|
}
|
||||||
|
color: root.genericContentForeground
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: {
|
||||||
|
var instructionText = "";
|
||||||
|
var actionText = "";
|
||||||
|
if (root.selectionMode === RegionSelection.SelectionMode.RectCorners) {
|
||||||
|
instructionText = Translation.tr("Drag or click a region");
|
||||||
|
} else if (root.selectionMode === RegionSelection.SelectionMode.Circle) {
|
||||||
|
instructionText = Translation.tr("Circle");
|
||||||
|
}
|
||||||
|
switch (root.action) {
|
||||||
|
case RegionSelection.SnipAction.Copy:
|
||||||
|
case RegionSelection.SnipAction.Edit:
|
||||||
|
actionText = Translation.tr(" | LMB: Copy • RMB: Edit");
|
||||||
|
break;
|
||||||
|
case RegionSelection.SnipAction.Search:
|
||||||
|
actionText = Translation.tr(" to search");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
actionText = "";
|
||||||
|
}
|
||||||
|
return instructionText + actionText;
|
||||||
|
}
|
||||||
|
color: root.genericContentForeground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window regions
|
||||||
|
Repeater {
|
||||||
|
model: ScriptModel {
|
||||||
|
values: root.windowRegions
|
||||||
|
}
|
||||||
|
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 : 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: 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 : 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 ? 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 : 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
Scope {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
GlobalStates.regionSelectorOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
property var action: RegionSelection.SnipAction.Copy
|
||||||
|
property var selectionMode: RegionSelection.SelectionMode.RectCorners
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: Quickshell.screens
|
||||||
|
delegate: Loader {
|
||||||
|
id: regionSelectorLoader
|
||||||
|
required property var modelData
|
||||||
|
active: GlobalStates.regionSelectorOpen
|
||||||
|
|
||||||
|
sourceComponent: RegionSelection {
|
||||||
|
screen: regionSelectorLoader.modelData
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "region"
|
||||||
|
|
||||||
|
function screenshot() {
|
||||||
|
root.screenshot()
|
||||||
|
}
|
||||||
|
function search() {
|
||||||
|
root.search()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalShortcut {
|
||||||
|
name: "regionScreenshot"
|
||||||
|
description: "Takes a screenshot of the selected region"
|
||||||
|
onPressed: root.screenshot()
|
||||||
|
}
|
||||||
|
GlobalShortcut {
|
||||||
|
name: "regionSearch"
|
||||||
|
description: "Searches the selected region"
|
||||||
|
onPressed: root.search()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: regionRect
|
||||||
|
required property color colBackground
|
||||||
|
required property color colForeground
|
||||||
|
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
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: regionLabelBackground
|
||||||
|
property real verticalPadding: 5
|
||||||
|
property real horizontalPadding: 10
|
||||||
|
radius: 10
|
||||||
|
color: regionRect.colBackground
|
||||||
|
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: regionRect.colForeground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -225,6 +225,17 @@ ContentPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ConfigRow {
|
||||||
|
uniform: true
|
||||||
|
ConfigSwitch {
|
||||||
|
buttonIcon: "videocam"
|
||||||
|
text: Translation.tr("Record")
|
||||||
|
checked: Config.options.bar.utilButtons.showScreenRecord
|
||||||
|
onCheckedChanged: {
|
||||||
|
Config.options.bar.utilButtons.showScreenRecord = checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentSection {
|
ContentSection {
|
||||||
|
|||||||
@@ -113,6 +113,20 @@ ContentPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ConfigRow {
|
||||||
|
uniform: true
|
||||||
|
ConfigSpinBox {
|
||||||
|
icon: "charger"
|
||||||
|
text: Translation.tr("Full warning")
|
||||||
|
value: Config.options.battery.full
|
||||||
|
from: 0
|
||||||
|
to: 101
|
||||||
|
stepSize: 5
|
||||||
|
onValueChanged: {
|
||||||
|
Config.options.battery.full = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentSection {
|
ContentSection {
|
||||||
@@ -145,6 +159,7 @@ ContentPage {
|
|||||||
}
|
}
|
||||||
ContentSubsection {
|
ContentSubsection {
|
||||||
title: Translation.tr("Generate translation with Gemini")
|
title: Translation.tr("Generate translation with Gemini")
|
||||||
|
tooltip: Translation.tr("You'll need to enter your Gemini API key first.\nType /key on the sidebar for instructions.")
|
||||||
|
|
||||||
ConfigRow {
|
ConfigRow {
|
||||||
MaterialTextArea {
|
MaterialTextArea {
|
||||||
@@ -240,6 +255,30 @@ ContentPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ContentSection {
|
||||||
|
icon: "notification_sound"
|
||||||
|
title: Translation.tr("Sounds")
|
||||||
|
ConfigRow {
|
||||||
|
uniform: true
|
||||||
|
ConfigSwitch {
|
||||||
|
buttonIcon: "battery_android_full"
|
||||||
|
text: Translation.tr("Battery")
|
||||||
|
checked: Config.options.sounds.battery
|
||||||
|
onCheckedChanged: {
|
||||||
|
Config.options.sounds.battery = checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConfigSwitch {
|
||||||
|
buttonIcon: "av_timer"
|
||||||
|
text: Translation.tr("Pomodoro")
|
||||||
|
checked: Config.options.sounds.pomodoro
|
||||||
|
onCheckedChanged: {
|
||||||
|
Config.options.sounds.pomodoro = checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ContentSection {
|
ContentSection {
|
||||||
icon: "nest_clock_farsight_analog"
|
icon: "nest_clock_farsight_analog"
|
||||||
title: Translation.tr("Time")
|
title: Translation.tr("Time")
|
||||||
|
|||||||
@@ -147,5 +147,19 @@ ContentPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ContentSubsection {
|
||||||
|
title: Translation.tr("Google Lens")
|
||||||
|
|
||||||
|
ConfigSelectionArray {
|
||||||
|
currentValue: Config.options.search.imageSearch.useCircleSelection ? "circle" : "rectangles"
|
||||||
|
onSelected: newValue => {
|
||||||
|
Config.options.search.imageSearch.useCircleSelection = (newValue === "circle");
|
||||||
|
}
|
||||||
|
options: [
|
||||||
|
{ icon: "activity_zone", value: "rectangles", displayName: Translation.tr("Rectangular selection") },
|
||||||
|
{ icon: "gesture", value: "circle", displayName: Translation.tr("Circle to Search") }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,70 +13,8 @@ Rectangle {
|
|||||||
radius: Appearance.rounding.normal
|
radius: Appearance.rounding.normal
|
||||||
color: Appearance.colors.colLayer1
|
color: Appearance.colors.colLayer1
|
||||||
|
|
||||||
property int selectedTab: 0
|
NotificationList {
|
||||||
property var tabButtonList: [
|
|
||||||
{"icon": "notifications", "name": Translation.tr("Notifications")},
|
|
||||||
{"icon": "volume_up", "name": Translation.tr("Audio")}
|
|
||||||
]
|
|
||||||
|
|
||||||
Keys.onPressed: (event) => {
|
|
||||||
if (event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) {
|
|
||||||
if (event.key === Qt.Key_PageDown) {
|
|
||||||
root.selectedTab = Math.min(root.selectedTab + 1, root.tabButtonList.length - 1)
|
|
||||||
} else if (event.key === Qt.Key_PageUp) {
|
|
||||||
root.selectedTab = Math.max(root.selectedTab - 1, 0)
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
if (event.modifiers === Qt.ControlModifier) {
|
|
||||||
if (event.key === Qt.Key_Tab) {
|
|
||||||
root.selectedTab = (root.selectedTab + 1) % root.tabButtonList.length
|
|
||||||
} else if (event.key === Qt.Key_Backtab) {
|
|
||||||
root.selectedTab = (root.selectedTab - 1 + root.tabButtonList.length) % root.tabButtonList.length
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.margins: 5
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 0
|
anchors.margins: 5
|
||||||
|
|
||||||
PrimaryTabBar {
|
|
||||||
id: tabBar
|
|
||||||
tabButtonList: root.tabButtonList
|
|
||||||
externalTrackedTab: root.selectedTab
|
|
||||||
|
|
||||||
function onCurrentIndexChanged(currentIndex) {
|
|
||||||
root.selectedTab = currentIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SwipeView {
|
|
||||||
id: swipeView
|
|
||||||
Layout.topMargin: 5
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
spacing: 10
|
|
||||||
currentIndex: root.selectedTab
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
tabBar.enableIndicatorAnimation = true
|
|
||||||
root.selectedTab = currentIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
clip: true
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: OpacityMask {
|
|
||||||
maskSource: Rectangle {
|
|
||||||
width: swipeView.width
|
|
||||||
height: swipeView.height
|
|
||||||
radius: Appearance.rounding.small
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationList {}
|
|
||||||
VolumeMixer {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,17 @@ import "./quickToggles/"
|
|||||||
import "./quickToggles/classicStyle/"
|
import "./quickToggles/classicStyle/"
|
||||||
import "./wifiNetworks/"
|
import "./wifiNetworks/"
|
||||||
import "./bluetoothDevices/"
|
import "./bluetoothDevices/"
|
||||||
|
import "./volumeMixer/"
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
property int sidebarWidth: Appearance.sizes.sidebarWidth
|
property int sidebarWidth: Appearance.sizes.sidebarWidth
|
||||||
property int sidebarPadding: 12
|
property int sidebarPadding: 10
|
||||||
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
|
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
|
||||||
property bool showWifiDialog: false
|
property bool showWifiDialog: false
|
||||||
property bool showBluetoothDialog: false
|
property bool showBluetoothDialog: false
|
||||||
|
property bool showAudioOutputDialog: false
|
||||||
|
property bool showAudioInputDialog: false
|
||||||
property bool editMode: false
|
property bool editMode: false
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -29,6 +32,8 @@ Item {
|
|||||||
if (!GlobalStates.sidebarRightOpen) {
|
if (!GlobalStates.sidebarRightOpen) {
|
||||||
root.showWifiDialog = false;
|
root.showWifiDialog = false;
|
||||||
root.showBluetoothDialog = false;
|
root.showBluetoothDialog = false;
|
||||||
|
root.showAudioOutputDialog = false;
|
||||||
|
root.showAudioInputDialog = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +62,8 @@ Item {
|
|||||||
|
|
||||||
SystemButtonRow {
|
SystemButtonRow {
|
||||||
Layout.fillHeight: false
|
Layout.fillHeight: false
|
||||||
Layout.margins: 10
|
Layout.fillWidth: true
|
||||||
|
// Layout.margins: 10
|
||||||
Layout.topMargin: 5
|
Layout.topMargin: 5
|
||||||
Layout.bottomMargin: 0
|
Layout.bottomMargin: 0
|
||||||
}
|
}
|
||||||
@@ -102,53 +108,71 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onShowWifiDialogChanged: if (showWifiDialog) wifiDialogLoader.active = true;
|
ToggleDialog {
|
||||||
Loader {
|
|
||||||
id: wifiDialogLoader
|
id: wifiDialogLoader
|
||||||
anchors.fill: parent
|
shownPropertyString: "showWifiDialog"
|
||||||
|
dialog: WifiDialog {}
|
||||||
active: root.showWifiDialog || item.visible
|
onShownChanged: {
|
||||||
onActiveChanged: {
|
if (!shown) return;
|
||||||
if (active) {
|
Network.enableWifi();
|
||||||
item.show = true;
|
Network.rescanWifi();
|
||||||
item.forceActiveFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceComponent: WifiDialog {
|
|
||||||
onDismiss: {
|
|
||||||
show = false
|
|
||||||
root.showWifiDialog = false
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (!visible && !root.showWifiDialog) wifiDialogLoader.active = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onShowBluetoothDialogChanged: {
|
ToggleDialog {
|
||||||
if (showBluetoothDialog) bluetoothDialogLoader.active = true;
|
|
||||||
else Bluetooth.defaultAdapter.discovering = false;
|
|
||||||
}
|
|
||||||
Loader {
|
|
||||||
id: bluetoothDialogLoader
|
id: bluetoothDialogLoader
|
||||||
|
shownPropertyString: "showBluetoothDialog"
|
||||||
|
dialog: BluetoothDialog {}
|
||||||
|
onShownChanged: {
|
||||||
|
if (!shown) {
|
||||||
|
Bluetooth.defaultAdapter.discovering = false;
|
||||||
|
} else {
|
||||||
|
Bluetooth.defaultAdapter.enabled = true;
|
||||||
|
Bluetooth.defaultAdapter.discovering = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleDialog {
|
||||||
|
id: audioOutputDialogLoader
|
||||||
|
shownPropertyString: "showAudioOutputDialog"
|
||||||
|
dialog: VolumeDialog {
|
||||||
|
isSink: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleDialog {
|
||||||
|
id: audioInputDialogLoader
|
||||||
|
shownPropertyString: "showAudioInputDialog"
|
||||||
|
dialog: VolumeDialog {
|
||||||
|
isSink: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component ToggleDialog: Loader {
|
||||||
|
id: toggleDialogLoader
|
||||||
|
required property string shownPropertyString
|
||||||
|
property alias dialog: toggleDialogLoader.sourceComponent
|
||||||
|
readonly property bool shown: root[shownPropertyString]
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
active: root.showBluetoothDialog || item.visible
|
onShownChanged: if (shown) toggleDialogLoader.active = true;
|
||||||
|
active: shown
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (active) {
|
if (active) {
|
||||||
item.show = true;
|
item.show = true;
|
||||||
item.forceActiveFocus();
|
item.forceActiveFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Connections {
|
||||||
sourceComponent: BluetoothDialog {
|
target: toggleDialogLoader.item
|
||||||
onDismiss: {
|
function onDismiss() {
|
||||||
show = false
|
toggleDialogLoader.item.show = false
|
||||||
root.showBluetoothDialog = false
|
root[toggleDialogLoader.shownPropertyString] = false;
|
||||||
}
|
}
|
||||||
onVisibleChanged: {
|
function onVisibleChanged() {
|
||||||
if (!visible && !root.showBluetoothDialog) bluetoothDialogLoader.active = false;
|
if (!toggleDialogLoader.item.visible && !root[toggleDialogLoader.shownPropertyString]) toggleDialogLoader.active = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,42 +187,68 @@ Item {
|
|||||||
Connections {
|
Connections {
|
||||||
target: quickPanelImplLoader.item
|
target: quickPanelImplLoader.item
|
||||||
function onOpenWifiDialog() {
|
function onOpenWifiDialog() {
|
||||||
Network.enableWifi();
|
|
||||||
Network.rescanWifi();
|
|
||||||
root.showWifiDialog = true;
|
root.showWifiDialog = true;
|
||||||
}
|
}
|
||||||
function onOpenBluetoothDialog() {
|
function onOpenBluetoothDialog() {
|
||||||
Bluetooth.defaultAdapter.enabled = true;
|
|
||||||
Bluetooth.defaultAdapter.discovering = true;
|
|
||||||
root.showBluetoothDialog = true;
|
root.showBluetoothDialog = true;
|
||||||
}
|
}
|
||||||
|
function onOpenAudioOutputDialog() {
|
||||||
|
root.showAudioOutputDialog = true;
|
||||||
|
}
|
||||||
|
function onOpenAudioInputDialog() {
|
||||||
|
root.showAudioInputDialog = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
component SystemButtonRow: RowLayout {
|
component SystemButtonRow: Item {
|
||||||
spacing: 10
|
implicitHeight: Math.max(uptimeContainer.implicitHeight, systemButtonsRow.implicitHeight)
|
||||||
|
|
||||||
CustomIcon {
|
Rectangle {
|
||||||
id: distroIcon
|
id: uptimeContainer
|
||||||
width: 25
|
anchors {
|
||||||
height: 25
|
top: parent.top
|
||||||
source: SystemInfo.distroIcon
|
bottom: parent.bottom
|
||||||
colorize: true
|
left: parent.left
|
||||||
color: Appearance.colors.colOnLayer0
|
}
|
||||||
}
|
color: Appearance.colors.colLayer1
|
||||||
|
radius: height / 2
|
||||||
StyledText {
|
implicitWidth: uptimeRow.implicitWidth + 24
|
||||||
font.pixelSize: Appearance.font.pixelSize.normal
|
implicitHeight: uptimeRow.implicitHeight + 8
|
||||||
color: Appearance.colors.colOnLayer0
|
|
||||||
text: Translation.tr("Up %1").arg(DateTime.uptime)
|
Row {
|
||||||
textFormat: Text.MarkdownText
|
id: uptimeRow
|
||||||
}
|
anchors.centerIn: parent
|
||||||
|
spacing: 8
|
||||||
Item {
|
CustomIcon {
|
||||||
Layout.fillWidth: true
|
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 {
|
ButtonGroup {
|
||||||
|
id: systemButtonsRow
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
right: parent.right
|
||||||
|
}
|
||||||
|
color: Appearance.colors.colLayer1
|
||||||
|
padding: 4
|
||||||
|
|
||||||
QuickToggleButton {
|
QuickToggleButton {
|
||||||
toggled: root.editMode
|
toggled: root.editMode
|
||||||
visible: Config.options.sidebar.quickToggles.style === "android"
|
visible: Config.options.sidebar.quickToggles.style === "android"
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ Rectangle {
|
|||||||
|
|
||||||
signal openWifiDialog()
|
signal openWifiDialog()
|
||||||
signal openBluetoothDialog()
|
signal openBluetoothDialog()
|
||||||
|
signal openAudioOutputDialog()
|
||||||
|
signal openAudioInputDialog()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,14 +73,14 @@ AbstractQuickPanel {
|
|||||||
Repeater {
|
Repeater {
|
||||||
id: usedRowsRepeater
|
id: usedRowsRepeater
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
values: root.toggleRows
|
values: Array(root.toggleRows.length)
|
||||||
}
|
}
|
||||||
delegate: ButtonGroup {
|
delegate: ButtonGroup {
|
||||||
id: toggleRow
|
id: toggleRow
|
||||||
required property var modelData
|
|
||||||
required property int index
|
required property int index
|
||||||
|
property var modelData: root.toggleRows[index]
|
||||||
property int startingIndex: {
|
property int startingIndex: {
|
||||||
const rows = usedRowsRepeater.model.values;
|
const rows = root.toggleRows;
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (let i = 0; i < index; i++) {
|
for (let i = 0; i < index; i++) {
|
||||||
sum += rows[i].length;
|
sum += rows[i].length;
|
||||||
@@ -91,7 +91,8 @@ AbstractQuickPanel {
|
|||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
values: toggleRow.modelData
|
values: toggleRow?.modelData ?? []
|
||||||
|
objectProp: "type"
|
||||||
}
|
}
|
||||||
delegate: AndroidToggleDelegateChooser {
|
delegate: AndroidToggleDelegateChooser {
|
||||||
startingIndex: toggleRow.startingIndex
|
startingIndex: toggleRow.startingIndex
|
||||||
@@ -101,6 +102,8 @@ AbstractQuickPanel {
|
|||||||
spacing: root.spacing
|
spacing: root.spacing
|
||||||
onOpenWifiDialog: root.openWifiDialog()
|
onOpenWifiDialog: root.openWifiDialog()
|
||||||
onOpenBluetoothDialog: root.openBluetoothDialog()
|
onOpenBluetoothDialog: root.openBluetoothDialog()
|
||||||
|
onOpenAudioOutputDialog: root.openAudioOutputDialog()
|
||||||
|
onOpenAudioInputDialog: root.openAudioInputDialog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,16 +132,18 @@ AbstractQuickPanel {
|
|||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
values: root.unusedToggleRows
|
values: Array(root.unusedToggleRows.length)
|
||||||
}
|
}
|
||||||
delegate: ButtonGroup {
|
delegate: ButtonGroup {
|
||||||
id: unusedToggleRow
|
id: unusedToggleRow
|
||||||
required property var modelData
|
required property int index
|
||||||
|
property var modelData: root.unusedToggleRows[index]
|
||||||
spacing: root.spacing
|
spacing: root.spacing
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
values: unusedToggleRow.modelData
|
values: unusedToggleRow?.modelData ?? []
|
||||||
|
objectProp: "type"
|
||||||
}
|
}
|
||||||
delegate: AndroidToggleDelegateChooser {
|
delegate: AndroidToggleDelegateChooser {
|
||||||
startingIndex: -1
|
startingIndex: -1
|
||||||
|
|||||||
+6
-2
@@ -8,7 +8,7 @@ import Quickshell
|
|||||||
AndroidQuickToggleButton {
|
AndroidQuickToggleButton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
name: Translation.tr("Audio")
|
name: Translation.tr("Audio output")
|
||||||
statusText: toggled ? Translation.tr("Unmuted") : Translation.tr("Muted")
|
statusText: toggled ? Translation.tr("Unmuted") : Translation.tr("Muted")
|
||||||
toggled: !Audio.sink?.audio?.muted
|
toggled: !Audio.sink?.audio?.muted
|
||||||
buttonIcon: Audio.sink?.audio?.muted ? "volume_off" : "volume_up"
|
buttonIcon: Audio.sink?.audio?.muted ? "volume_off" : "volume_up"
|
||||||
@@ -16,7 +16,11 @@ AndroidQuickToggleButton {
|
|||||||
Audio.sink.audio.muted = !Audio.sink.audio.muted
|
Audio.sink.audio.muted = !Audio.sink.audio.muted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
altAction: () => {
|
||||||
|
root.openMenu()
|
||||||
|
}
|
||||||
|
|
||||||
StyledToolTip {
|
StyledToolTip {
|
||||||
text: Translation.tr("Audio")
|
text: Translation.tr("Audio output | Right-click for volume mixer & device selector")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -17,11 +17,14 @@ AndroidQuickToggleButton {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter?.enabled
|
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter?.enabled
|
||||||
}
|
}
|
||||||
|
altAction: () => {
|
||||||
|
root.openMenu()
|
||||||
|
}
|
||||||
StyledToolTip {
|
StyledToolTip {
|
||||||
text: Translation.tr("%1 | Right-click to configure").arg(
|
text: Translation.tr("%1 | Right-click to configure").arg(
|
||||||
(BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Bluetooth"))
|
(BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Bluetooth"))
|
||||||
+ (BluetoothStatus.activeDeviceCount > 1 ? ` +${BluetoothStatus.activeDeviceCount - 1}` : "")
|
+ (BluetoothStatus.activeDeviceCount > 1 ? ` +${BluetoothStatus.activeDeviceCount - 1}` : "")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -8,7 +8,7 @@ import Quickshell
|
|||||||
AndroidQuickToggleButton {
|
AndroidQuickToggleButton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
name: Translation.tr("Microphone")
|
name: Translation.tr("Audio input")
|
||||||
statusText: toggled ? Translation.tr("Enabled") : Translation.tr("Muted")
|
statusText: toggled ? Translation.tr("Enabled") : Translation.tr("Muted")
|
||||||
toggled: !Audio.source?.audio?.muted
|
toggled: !Audio.source?.audio?.muted
|
||||||
buttonIcon: Audio.source?.audio?.muted ? "mic_off" : "mic"
|
buttonIcon: Audio.source?.audio?.muted ? "mic_off" : "mic"
|
||||||
@@ -16,7 +16,11 @@ AndroidQuickToggleButton {
|
|||||||
Audio.source.audio.muted = !Audio.source.audio.muted
|
Audio.source.audio.muted = !Audio.source.audio.muted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
altAction: () => {
|
||||||
|
root.openMenu()
|
||||||
|
}
|
||||||
|
|
||||||
StyledToolTip {
|
StyledToolTip {
|
||||||
text: Translation.tr("Microphone")
|
text: Translation.tr("Audio input | Right-click for volume mixer & device selector")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -23,6 +23,21 @@ GroupButton {
|
|||||||
baseHeight: root.baseCellHeight
|
baseHeight: root.baseCellHeight
|
||||||
|
|
||||||
property bool editMode: false
|
property bool editMode: false
|
||||||
|
enableImplicitWidthAnimation: !editMode
|
||||||
|
enableImplicitHeightAnimation: !editMode
|
||||||
|
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()
|
signal openMenu()
|
||||||
|
|
||||||
@@ -65,7 +80,7 @@ GroupButton {
|
|||||||
MaterialSymbol {
|
MaterialSymbol {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
fill: root.toggled ? 1 : 0
|
fill: root.toggled ? 1 : 0
|
||||||
iconSize: Appearance.font.pixelSize.huge
|
iconSize: root.expandedSize ? 22 : 24
|
||||||
color: root.colIcon
|
color: root.colIcon
|
||||||
text: root.buttonIcon
|
text: root.buttonIcon
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-4
@@ -7,8 +7,6 @@ import QtQuick.Layouts
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Bluetooth
|
import Quickshell.Bluetooth
|
||||||
|
|
||||||
import "./androidStyle/"
|
|
||||||
|
|
||||||
DelegateChooser {
|
DelegateChooser {
|
||||||
id: root
|
id: root
|
||||||
property bool editMode: false
|
property bool editMode: false
|
||||||
@@ -18,6 +16,8 @@ DelegateChooser {
|
|||||||
required property int startingIndex
|
required property int startingIndex
|
||||||
signal openWifiDialog()
|
signal openWifiDialog()
|
||||||
signal openBluetoothDialog()
|
signal openBluetoothDialog()
|
||||||
|
signal openAudioOutputDialog()
|
||||||
|
signal openAudioInputDialog()
|
||||||
|
|
||||||
role: "type"
|
role: "type"
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ DelegateChooser {
|
|||||||
baseCellHeight: root.baseCellHeight
|
baseCellHeight: root.baseCellHeight
|
||||||
cellSpacing: root.spacing
|
cellSpacing: root.spacing
|
||||||
cellSize: modelData.size
|
cellSize: modelData.size
|
||||||
altAction: () => {
|
onOpenMenu: {
|
||||||
root.openWifiDialog()
|
root.openWifiDialog()
|
||||||
}
|
}
|
||||||
} }
|
} }
|
||||||
@@ -48,7 +48,7 @@ DelegateChooser {
|
|||||||
baseCellHeight: root.baseCellHeight
|
baseCellHeight: root.baseCellHeight
|
||||||
cellSpacing: root.spacing
|
cellSpacing: root.spacing
|
||||||
cellSize: modelData.size
|
cellSize: modelData.size
|
||||||
altAction: () => {
|
onOpenMenu: {
|
||||||
root.openBluetoothDialog()
|
root.openBluetoothDialog()
|
||||||
}
|
}
|
||||||
} }
|
} }
|
||||||
@@ -181,6 +181,9 @@ DelegateChooser {
|
|||||||
baseCellHeight: root.baseCellHeight
|
baseCellHeight: root.baseCellHeight
|
||||||
cellSpacing: root.spacing
|
cellSpacing: root.spacing
|
||||||
cellSize: modelData.size
|
cellSize: modelData.size
|
||||||
|
onOpenMenu: {
|
||||||
|
root.openAudioInputDialog()
|
||||||
|
}
|
||||||
} }
|
} }
|
||||||
|
|
||||||
DelegateChoice { roleValue: "audio"; AndroidAudioToggle {
|
DelegateChoice { roleValue: "audio"; AndroidAudioToggle {
|
||||||
@@ -194,6 +197,9 @@ DelegateChooser {
|
|||||||
baseCellHeight: root.baseCellHeight
|
baseCellHeight: root.baseCellHeight
|
||||||
cellSpacing: root.spacing
|
cellSpacing: root.spacing
|
||||||
cellSize: modelData.size
|
cellSize: modelData.size
|
||||||
|
onOpenMenu: {
|
||||||
|
root.openAudioOutputDialog()
|
||||||
|
}
|
||||||
} }
|
} }
|
||||||
|
|
||||||
DelegateChoice { roleValue: "notifications"; AndroidNotificationToggle {
|
DelegateChoice { roleValue: "notifications"; AndroidNotificationToggle {
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
import qs
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
|
||||||
|
WindowDialog {
|
||||||
|
id: root
|
||||||
|
property bool isSink: true
|
||||||
|
function correctType(node) {
|
||||||
|
return (node.isSink === root.isSink) && node.audio
|
||||||
|
}
|
||||||
|
readonly property list<var> appPwNodes: Pipewire.nodes.values.filter((node) => { // Should be list<PwNode> but it breaks ScriptModel
|
||||||
|
return root.correctType(node) && node.isStream
|
||||||
|
})
|
||||||
|
readonly property bool hasApps: appPwNodes.length > 0
|
||||||
|
backgroundHeight: 700
|
||||||
|
|
||||||
|
WindowDialogTitle {
|
||||||
|
text: root.isSink ? Translation.tr("Audio output") : Translation.tr("Audio input")
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowDialogSectionHeader {
|
||||||
|
visible: root.hasApps
|
||||||
|
text: Translation.tr("Applications")
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowDialogSeparator {
|
||||||
|
visible: root.hasApps
|
||||||
|
Layout.topMargin: -22
|
||||||
|
Layout.leftMargin: 0
|
||||||
|
Layout.rightMargin: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogSectionListView {
|
||||||
|
visible: root.hasApps
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
model: ScriptModel {
|
||||||
|
values: root.appPwNodes
|
||||||
|
}
|
||||||
|
delegate: VolumeMixerEntry {
|
||||||
|
anchors {
|
||||||
|
left: parent?.left
|
||||||
|
right: parent?.right
|
||||||
|
}
|
||||||
|
required property var modelData
|
||||||
|
node: modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowDialogSectionHeader {
|
||||||
|
text: Translation.tr("Devices")
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowDialogSeparator {
|
||||||
|
Layout.topMargin: -22
|
||||||
|
Layout.leftMargin: 0
|
||||||
|
Layout.rightMargin: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogSectionListView {
|
||||||
|
Layout.fillHeight: !root.hasApps
|
||||||
|
Layout.preferredHeight: 180
|
||||||
|
|
||||||
|
model: ScriptModel {
|
||||||
|
values: Pipewire.nodes.values.filter(node => {
|
||||||
|
return root.correctType(node) && !node.isStream
|
||||||
|
})
|
||||||
|
}
|
||||||
|
delegate: StyledRadioButton {
|
||||||
|
id: radioButton
|
||||||
|
required property var modelData
|
||||||
|
anchors {
|
||||||
|
left: parent?.left
|
||||||
|
right: parent?.right
|
||||||
|
}
|
||||||
|
|
||||||
|
description: modelData.description
|
||||||
|
checked: modelData.id === (root.isSink ? Pipewire.preferredDefaultAudioSink?.id : Pipewire.preferredDefaultAudioSource?.id)
|
||||||
|
|
||||||
|
onCheckedChanged: {
|
||||||
|
if (!checked) return;
|
||||||
|
if (root.isSink) {
|
||||||
|
Pipewire.preferredDefaultAudioSink = modelData
|
||||||
|
} else {
|
||||||
|
Pipewire.preferredDefaultAudioSource = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowDialogSeparator {
|
||||||
|
Layout.leftMargin: 0
|
||||||
|
Layout.rightMargin: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowDialogButtonRow {
|
||||||
|
DialogButton {
|
||||||
|
buttonText: Translation.tr("Details")
|
||||||
|
onClicked: {
|
||||||
|
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.volumeMixer}`]);
|
||||||
|
GlobalStates.sidebarRightOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogButton {
|
||||||
|
buttonText: Translation.tr("Done")
|
||||||
|
onClicked: root.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component DialogSectionListView: StyledListView {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: -22
|
||||||
|
Layout.bottomMargin: -16
|
||||||
|
Layout.leftMargin: -Appearance.rounding.large
|
||||||
|
Layout.rightMargin: -Appearance.rounding.large
|
||||||
|
topMargin: 12
|
||||||
|
bottomMargin: 12
|
||||||
|
leftMargin: 20
|
||||||
|
rightMargin: 20
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
spacing: 4
|
||||||
|
animateAppearance: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
import qs.modules.common
|
|
||||||
import qs.modules.common.widgets
|
|
||||||
import qs.services
|
|
||||||
import Qt5Compat.GraphicalEffects
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
property bool showDeviceSelector: false
|
|
||||||
property bool deviceSelectorInput
|
|
||||||
property int dialogMargins: 16
|
|
||||||
property PwNode selectedDevice
|
|
||||||
readonly property list<PwNode> appPwNodes: Pipewire.nodes.values.filter((node) => {
|
|
||||||
// return node.type == "21" // Alternative, not as clean
|
|
||||||
return node.isSink && node.isStream
|
|
||||||
})
|
|
||||||
|
|
||||||
function showDeviceSelectorDialog(input: bool) {
|
|
||||||
root.selectedDevice = null
|
|
||||||
root.showDeviceSelector = true
|
|
||||||
root.deviceSelectorInput = input
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onPressed: (event) => {
|
|
||||||
// Close dialog on pressing Esc if open
|
|
||||||
if (event.key === Qt.Key_Escape && root.showDeviceSelector) {
|
|
||||||
root.showDeviceSelector = false
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
StyledListView {
|
|
||||||
id: listView
|
|
||||||
model: root.appPwNodes
|
|
||||||
clip: true
|
|
||||||
anchors {
|
|
||||||
fill: parent
|
|
||||||
topMargin: 10
|
|
||||||
bottomMargin: 10
|
|
||||||
}
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
delegate: VolumeMixerEntry {
|
|
||||||
// Layout.fillWidth: true
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
right: parent.right
|
|
||||||
leftMargin: 10
|
|
||||||
rightMargin: 10
|
|
||||||
}
|
|
||||||
required property var modelData
|
|
||||||
node: modelData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placeholder when list is empty
|
|
||||||
Item {
|
|
||||||
anchors.fill: listView
|
|
||||||
|
|
||||||
visible: opacity > 0
|
|
||||||
opacity: (root.appPwNodes.length === 0) ? 1 : 0
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Appearance.animation.menuDecel.duration
|
|
||||||
easing.type: Appearance.animation.menuDecel.type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
MaterialSymbol {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
iconSize: 55
|
|
||||||
color: Appearance.m3colors.m3outline
|
|
||||||
text: "brand_awareness"
|
|
||||||
}
|
|
||||||
StyledText {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
font.pixelSize: Appearance.font.pixelSize.normal
|
|
||||||
color: Appearance.m3colors.m3outline
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
text: Translation.tr("No audio source")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Device selector
|
|
||||||
RowLayout {
|
|
||||||
id: deviceSelectorRowLayout
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: false
|
|
||||||
uniformCellSizes: true
|
|
||||||
|
|
||||||
AudioDeviceSelectorButton {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
input: false
|
|
||||||
downAction: () => root.showDeviceSelectorDialog(input)
|
|
||||||
}
|
|
||||||
AudioDeviceSelectorButton {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
input: true
|
|
||||||
downAction: () => root.showDeviceSelectorDialog(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Device selector dialog
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
z: 9999
|
|
||||||
|
|
||||||
visible: opacity > 0
|
|
||||||
opacity: root.showDeviceSelector ? 1 : 0
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Appearance.animation.elementMoveFast.duration
|
|
||||||
easing.type: Appearance.animation.elementMoveFast.type
|
|
||||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle { // Scrim
|
|
||||||
id: scrimOverlay
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Appearance.rounding.small
|
|
||||||
color: Appearance.colors.colScrim
|
|
||||||
MouseArea {
|
|
||||||
hoverEnabled: true
|
|
||||||
anchors.fill: parent
|
|
||||||
preventStealing: true
|
|
||||||
propagateComposedEvents: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle { // The dialog
|
|
||||||
id: dialog
|
|
||||||
color: Appearance.colors.colSurfaceContainerHigh
|
|
||||||
radius: Appearance.rounding.normal
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.margins: 30
|
|
||||||
implicitHeight: dialogColumnLayout.implicitHeight
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: dialogColumnLayout
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: 16
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: dialogTitle
|
|
||||||
Layout.topMargin: dialogMargins
|
|
||||||
Layout.leftMargin: dialogMargins
|
|
||||||
Layout.rightMargin: dialogMargins
|
|
||||||
Layout.alignment: Qt.AlignLeft
|
|
||||||
color: Appearance.m3colors.m3onSurface
|
|
||||||
font.pixelSize: Appearance.font.pixelSize.larger
|
|
||||||
text: root.deviceSelectorInput ? Translation.tr("Select input device") : Translation.tr("Select output device")
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
color: Appearance.m3colors.m3outline
|
|
||||||
implicitHeight: 1
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: dialogMargins
|
|
||||||
Layout.rightMargin: dialogMargins
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledFlickable {
|
|
||||||
id: dialogFlickable
|
|
||||||
Layout.fillWidth: true
|
|
||||||
clip: true
|
|
||||||
implicitHeight: Math.min(scrimOverlay.height - dialogMargins * 8 - dialogTitle.height - dialogButtonsRowLayout.height, devicesColumnLayout.implicitHeight)
|
|
||||||
|
|
||||||
contentHeight: devicesColumnLayout.implicitHeight
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: devicesColumnLayout
|
|
||||||
anchors.fill: parent
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: ScriptModel {
|
|
||||||
values: Pipewire.nodes.values.filter(node => {
|
|
||||||
return !node.isStream && node.isSink !== root.deviceSelectorInput && node.audio
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This could and should be refractored, but all data becomes null when passed wtf
|
|
||||||
delegate: StyledRadioButton {
|
|
||||||
id: radioButton
|
|
||||||
required property var modelData
|
|
||||||
Layout.leftMargin: root.dialogMargins
|
|
||||||
Layout.rightMargin: root.dialogMargins
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
description: modelData.description
|
|
||||||
checked: modelData.id === Pipewire.defaultAudioSink?.id
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onShowDeviceSelectorChanged() {
|
|
||||||
if(!root.showDeviceSelector) return;
|
|
||||||
radioButton.checked = (modelData.id === Pipewire.defaultAudioSink?.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCheckedChanged: {
|
|
||||||
if (checked) {
|
|
||||||
root.selectedDevice = modelData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Item {
|
|
||||||
implicitHeight: dialogMargins
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
color: Appearance.m3colors.m3outline
|
|
||||||
implicitHeight: 1
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: dialogMargins
|
|
||||||
Layout.rightMargin: dialogMargins
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: dialogButtonsRowLayout
|
|
||||||
Layout.bottomMargin: dialogMargins
|
|
||||||
Layout.leftMargin: dialogMargins
|
|
||||||
Layout.rightMargin: dialogMargins
|
|
||||||
Layout.alignment: Qt.AlignRight
|
|
||||||
|
|
||||||
DialogButton {
|
|
||||||
buttonText: Translation.tr("Cancel")
|
|
||||||
onClicked: {
|
|
||||||
root.showDeviceSelector = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DialogButton {
|
|
||||||
buttonText: Translation.tr("OK")
|
|
||||||
onClicked: {
|
|
||||||
root.showDeviceSelector = false
|
|
||||||
if (root.selectedDevice) {
|
|
||||||
if (root.deviceSelectorInput) {
|
|
||||||
Pipewire.preferredDefaultAudioSource = root.selectedDevice
|
|
||||||
} else {
|
|
||||||
Pipewire.preferredDefaultAudioSink = root.selectedDevice
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,7 @@ Item {
|
|||||||
spacing: 6
|
spacing: 6
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
property real size: slider.height * 0.9
|
property real size: 36
|
||||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||||
visible: source != ""
|
visible: source != ""
|
||||||
sourceSize.width: size
|
sourceSize.width: size
|
||||||
@@ -57,6 +57,7 @@ Item {
|
|||||||
id: slider
|
id: slider
|
||||||
value: root.node.audio.volume
|
value: root.node.audio.volume
|
||||||
onMoved: root.node.audio.volume = value
|
onMoved: root.node.audio.volume = value
|
||||||
|
configuration: StyledSlider.Configuration.S
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,578 +0,0 @@
|
|||||||
//@ pragma UseQApplication
|
|
||||||
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
|
||||||
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
|
|
||||||
//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000
|
|
||||||
|
|
||||||
// Adjust this to make it smaller or larger
|
|
||||||
//@ pragma Env QT_SCALE_FACTOR=1
|
|
||||||
|
|
||||||
pragma ComponentBehavior: "Bound"
|
|
||||||
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 Qt5Compat.GraphicalEffects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
|
|
||||||
ShellRoot {
|
|
||||||
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
|
|
||||||
|
|
||||||
// Force initialization of some singletons
|
|
||||||
Component.onCompleted: {
|
|
||||||
MaterialThemeLoader.reapplyTheme();
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: Quickshell.screens
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: panelWindow
|
|
||||||
required property var modelData
|
|
||||||
readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(modelData)
|
|
||||||
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-${modelData.name}`
|
|
||||||
property real dragStartX: 0
|
|
||||||
property real dragStartY: 0
|
|
||||||
property real draggingX: 0
|
|
||||||
property real draggingY: 0
|
|
||||||
property real dragDiffX: 0
|
|
||||||
property real dragDiffY: 0
|
|
||||||
property bool draggedAway: (dragDiffX !== 0 || dragDiffY !== 0)
|
|
||||||
property bool dragging: false
|
|
||||||
property var mouseButton: null
|
|
||||||
property var imageRegions: []
|
|
||||||
readonly property list<var> windowRegions: filterWindowRegionsByLayers(
|
|
||||||
root.windows.filter(w => w.workspace.id === panelWindow.activeWorkspaceId),
|
|
||||||
panelWindow.layerRegions
|
|
||||||
).map(window => {
|
|
||||||
return {
|
|
||||||
at: [window.at[0] - panelWindow.monitorOffsetX, window.at[1] - panelWindow.monitorOffsetY],
|
|
||||||
size: [window.size[0], window.size[1]],
|
|
||||||
class: window.class,
|
|
||||||
title: window.title,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
readonly property list<var> layerRegions: {
|
|
||||||
const layersOfThisMonitor = root.layers[panelWindow.hyprlandMonitor.name]
|
|
||||||
const topLayers = layersOfThisMonitor?.levels["2"]
|
|
||||||
if (!topLayers) return [];
|
|
||||||
const nonBarTopLayers = topLayers
|
|
||||||
.filter(layer => !(layer.namespace.includes(":bar") || layer.namespace.includes(":verticalBar") || layer.namespace.includes(":dock")))
|
|
||||||
.map(layer => {
|
|
||||||
return {
|
|
||||||
at: [layer.x, layer.y],
|
|
||||||
size: [layer.w, layer.h],
|
|
||||||
namespace: layer.namespace,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const offsetAdjustedLayers = nonBarTopLayers.map(layer => {
|
|
||||||
return {
|
|
||||||
at: [layer.at[0] - panelWindow.monitorOffsetX, layer.at[1] - panelWindow.monitorOffsetY],
|
|
||||||
size: layer.size,
|
|
||||||
namespace: layer.namespace,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return offsetAdjustedLayers;
|
|
||||||
}
|
|
||||||
|
|
||||||
property real targetedRegionX: -1
|
|
||||||
property real targetedRegionY: -1
|
|
||||||
property real targetedRegionWidth: 0
|
|
||||||
property real targetedRegionHeight: 0
|
|
||||||
|
|
||||||
function intersectionOverUnion(regionA, regionB) {
|
|
||||||
// region: { at: [x, y], size: [w, h] }
|
|
||||||
const ax1 = regionA.at[0], ay1 = regionA.at[1];
|
|
||||||
const ax2 = ax1 + regionA.size[0], ay2 = ay1 + regionA.size[1];
|
|
||||||
const bx1 = regionB.at[0], by1 = regionB.at[1];
|
|
||||||
const bx2 = bx1 + regionB.size[0], by2 = by1 + regionB.size[1];
|
|
||||||
|
|
||||||
const interX1 = Math.max(ax1, bx1);
|
|
||||||
const interY1 = Math.max(ay1, by1);
|
|
||||||
const interX2 = Math.min(ax2, bx2);
|
|
||||||
const interY2 = Math.min(ay2, by2);
|
|
||||||
|
|
||||||
const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1);
|
|
||||||
const areaA = (ax2 - ax1) * (ay2 - ay1);
|
|
||||||
const areaB = (bx2 - bx1) * (by2 - by1);
|
|
||||||
const unionArea = areaA + areaB - interArea;
|
|
||||||
|
|
||||||
return unionArea > 0 ? interArea / unionArea : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterOverlappingImageRegions(regions) {
|
|
||||||
let keep = [];
|
|
||||||
let removed = new Set();
|
|
||||||
for (let i = 0; i < regions.length; ++i) {
|
|
||||||
if (removed.has(i)) continue;
|
|
||||||
let regionA = regions[i];
|
|
||||||
for (let j = i + 1; j < regions.length; ++j) {
|
|
||||||
if (removed.has(j)) continue;
|
|
||||||
let regionB = regions[j];
|
|
||||||
if (intersectionOverUnion(regionA, regionB) > 0) {
|
|
||||||
// Compare areas
|
|
||||||
let areaA = regionA.size[0] * regionA.size[1];
|
|
||||||
let areaB = regionB.size[0] * regionB.size[1];
|
|
||||||
if (areaA <= areaB) {
|
|
||||||
removed.add(j);
|
|
||||||
} else {
|
|
||||||
removed.add(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let i = 0; i < regions.length; ++i) {
|
|
||||||
if (!removed.has(i)) keep.push(regions[i]);
|
|
||||||
}
|
|
||||||
return keep;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterWindowRegionsByLayers(windowRegions, layerRegions) {
|
|
||||||
return windowRegions.filter(windowRegion => {
|
|
||||||
for (let i = 0; i < layerRegions.length; ++i) {
|
|
||||||
if (intersectionOverUnion(windowRegion, layerRegions[i]) > 0)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterImageRegions(regions, windowRegions, threshold = 0.1) {
|
|
||||||
// Remove image regions that overlap too much with any window region
|
|
||||||
let filtered = regions.filter(region => {
|
|
||||||
for (let i = 0; i < windowRegions.length; ++i) {
|
|
||||||
if (intersectionOverUnion(region, windowRegions[i]) > threshold)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
// Remove overlapping image regions, keep only the smaller one
|
|
||||||
return filterOverlappingImageRegions(filtered);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTargetedRegion(x, y) {
|
|
||||||
// Image regions
|
|
||||||
const clickedRegion = panelWindow.imageRegions.find(region => {
|
|
||||||
return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1];
|
|
||||||
});
|
|
||||||
if (clickedRegion) {
|
|
||||||
panelWindow.targetedRegionX = clickedRegion.at[0];
|
|
||||||
panelWindow.targetedRegionY = clickedRegion.at[1];
|
|
||||||
panelWindow.targetedRegionWidth = clickedRegion.size[0];
|
|
||||||
panelWindow.targetedRegionHeight = clickedRegion.size[1];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layer regions
|
|
||||||
const clickedLayer = panelWindow.layerRegions.find(region => {
|
|
||||||
return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1];
|
|
||||||
});
|
|
||||||
if (clickedLayer) {
|
|
||||||
panelWindow.targetedRegionX = clickedLayer.at[0];
|
|
||||||
panelWindow.targetedRegionY = clickedLayer.at[1];
|
|
||||||
panelWindow.targetedRegionWidth = clickedLayer.size[0];
|
|
||||||
panelWindow.targetedRegionHeight = clickedLayer.size[1];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Window regions
|
|
||||||
const clickedWindow = panelWindow.windowRegions.find(region => {
|
|
||||||
return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1];
|
|
||||||
});
|
|
||||||
if (clickedWindow) {
|
|
||||||
panelWindow.targetedRegionX = clickedWindow.at[0];
|
|
||||||
panelWindow.targetedRegionY = clickedWindow.at[1];
|
|
||||||
panelWindow.targetedRegionWidth = clickedWindow.size[0];
|
|
||||||
panelWindow.targetedRegionHeight = clickedWindow.size[1];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
panelWindow.targetedRegionX = -1;
|
|
||||||
panelWindow.targetedRegionY = -1;
|
|
||||||
panelWindow.targetedRegionWidth = 0;
|
|
||||||
panelWindow.targetedRegionHeight = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
property real regionWidth: Math.abs(draggingX - dragStartX)
|
|
||||||
property real regionHeight: Math.abs(draggingY - dragStartY)
|
|
||||||
property real regionX: Math.min(dragStartX, draggingX)
|
|
||||||
property real regionY: Math.min(dragStartY, draggingY)
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
screen: modelData
|
|
||||||
WlrLayershell.namespace: "quickshell:screenshot"
|
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
|
||||||
anchors {
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
top: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: screenshotProcess
|
|
||||||
running: true
|
|
||||||
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(modelData.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.");
|
|
||||||
Qt.quit();
|
|
||||||
}
|
|
||||||
snipProc.startDetached();
|
|
||||||
Qt.quit();
|
|
||||||
}
|
|
||||||
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: modelData
|
|
||||||
|
|
||||||
focus: panelWindow.visible
|
|
||||||
Keys.onPressed: (event) => { // Esc to close
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
Qt.quit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,7 @@ Singleton {
|
|||||||
property PwNode sink: Pipewire.defaultAudioSink
|
property PwNode sink: Pipewire.defaultAudioSink
|
||||||
property PwNode source: Pipewire.defaultAudioSource
|
property PwNode source: Pipewire.defaultAudioSource
|
||||||
readonly property real hardMaxValue: 2.00 // People keep joking about setting volume to 5172% so...
|
readonly property real hardMaxValue: 2.00 // People keep joking about setting volume to 5172% so...
|
||||||
|
property string audioTheme: Config.options.sounds.theme
|
||||||
|
|
||||||
signal sinkProtectionTriggered(string reason);
|
signal sinkProtectionTriggered(string reason);
|
||||||
|
|
||||||
@@ -49,7 +50,28 @@ Singleton {
|
|||||||
}
|
}
|
||||||
lastVolume = sink.audio.volume;
|
lastVolume = sink.audio.volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playSystemSound(soundName) {
|
||||||
|
const ogaPath = `/usr/share/sounds/${root.audioTheme}/stereo/${soundName}.oga`;
|
||||||
|
const oggPath = `/usr/share/sounds/${root.audioTheme}/stereo/${soundName}.ogg`;
|
||||||
|
|
||||||
|
// Try playing .oga first
|
||||||
|
let command = [
|
||||||
|
"ffplay",
|
||||||
|
"-nodisp",
|
||||||
|
"-autoexit",
|
||||||
|
ogaPath
|
||||||
|
];
|
||||||
|
Quickshell.execDetached(command);
|
||||||
|
|
||||||
|
// Also try playing .ogg (ffplay will just fail silently if file doesn't exist)
|
||||||
|
command = [
|
||||||
|
"ffplay",
|
||||||
|
"-nodisp",
|
||||||
|
"-autoexit",
|
||||||
|
oggPath
|
||||||
|
];
|
||||||
|
Quickshell.execDetached(command);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,49 +8,79 @@ import QtQuick
|
|||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
|
id: root
|
||||||
property bool available: UPower.displayDevice.isLaptopBattery
|
property bool available: UPower.displayDevice.isLaptopBattery
|
||||||
property var chargeState: UPower.displayDevice.state
|
property var chargeState: UPower.displayDevice.state
|
||||||
property bool isCharging: chargeState == UPowerDeviceState.Charging
|
property bool isCharging: chargeState == UPowerDeviceState.Charging
|
||||||
property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge
|
property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge
|
||||||
property real percentage: UPower.displayDevice?.percentage ?? 1
|
property real percentage: UPower.displayDevice?.percentage ?? 1
|
||||||
readonly property bool allowAutomaticSuspend: Config.options.battery.automaticSuspend
|
readonly property bool allowAutomaticSuspend: Config.options.battery.automaticSuspend
|
||||||
|
readonly property bool soundEnabled: Config.options.sounds.battery
|
||||||
|
|
||||||
property bool isLow: available && (percentage <= Config.options.battery.low / 100)
|
property bool isLow: available && (percentage <= Config.options.battery.low / 100)
|
||||||
property bool isCritical: available && (percentage <= Config.options.battery.critical / 100)
|
property bool isCritical: available && (percentage <= Config.options.battery.critical / 100)
|
||||||
property bool isSuspending: available && (percentage <= Config.options.battery.suspend / 100)
|
property bool isSuspending: available && (percentage <= Config.options.battery.suspend / 100)
|
||||||
|
property bool isFull: available && (percentage >= Config.options.battery.full / 100)
|
||||||
|
|
||||||
property bool isLowAndNotCharging: isLow && !isCharging
|
property bool isLowAndNotCharging: isLow && !isCharging
|
||||||
property bool isCriticalAndNotCharging: isCritical && !isCharging
|
property bool isCriticalAndNotCharging: isCritical && !isCharging
|
||||||
property bool isSuspendingAndNotCharging: allowAutomaticSuspend && isSuspending && !isCharging
|
property bool isSuspendingAndNotCharging: allowAutomaticSuspend && isSuspending && !isCharging
|
||||||
|
property bool isFullAndCharging: isFull && isCharging
|
||||||
|
|
||||||
property real energyRate: UPower.displayDevice.changeRate
|
property real energyRate: UPower.displayDevice.changeRate
|
||||||
property real timeToEmpty: UPower.displayDevice.timeToEmpty
|
property real timeToEmpty: UPower.displayDevice.timeToEmpty
|
||||||
property real timeToFull: UPower.displayDevice.timeToFull
|
property real timeToFull: UPower.displayDevice.timeToFull
|
||||||
|
|
||||||
onIsLowAndNotChargingChanged: {
|
onIsLowAndNotChargingChanged: {
|
||||||
if (available && isLowAndNotCharging) Quickshell.execDetached([
|
if (!root.available || !isLowAndNotCharging) return;
|
||||||
|
Quickshell.execDetached([
|
||||||
"notify-send",
|
"notify-send",
|
||||||
Translation.tr("Low battery"),
|
Translation.tr("Low battery"),
|
||||||
Translation.tr("Consider plugging in your device"),
|
Translation.tr("Consider plugging in your device"),
|
||||||
"-u", "critical",
|
"-u", "critical",
|
||||||
"-a", "Shell"
|
"-a", "Shell"
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (root.soundEnabled) Audio.playSystemSound("dialog-warning");
|
||||||
}
|
}
|
||||||
|
|
||||||
onIsCriticalAndNotChargingChanged: {
|
onIsCriticalAndNotChargingChanged: {
|
||||||
if (available && isCriticalAndNotCharging) Quickshell.execDetached([
|
if (!root.available || !isCriticalAndNotCharging) return;
|
||||||
|
Quickshell.execDetached([
|
||||||
"notify-send",
|
"notify-send",
|
||||||
Translation.tr("Critically low battery"),
|
Translation.tr("Critically low battery"),
|
||||||
Translation.tr("Please charge!\nAutomatic suspend triggers at %1").arg(Config.options.battery.suspend),
|
Translation.tr("Please charge!\nAutomatic suspend triggers at %1%").arg(Config.options.battery.suspend),
|
||||||
"-u", "critical",
|
"-u", "critical",
|
||||||
"-a", "Shell"
|
"-a", "Shell"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (root.soundEnabled) Audio.playSystemSound("suspend-error");
|
||||||
}
|
}
|
||||||
|
|
||||||
onIsSuspendingAndNotChargingChanged: {
|
onIsSuspendingAndNotChargingChanged: {
|
||||||
if (available && isSuspendingAndNotCharging) {
|
if (root.available && isSuspendingAndNotCharging) {
|
||||||
Quickshell.execDetached(["bash", "-c", `systemctl suspend || loginctl suspend`]);
|
Quickshell.execDetached(["bash", "-c", `systemctl suspend || loginctl suspend`]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onIsFullAndChargingChanged: {
|
||||||
|
if (!root.available || !isFullAndCharging) return;
|
||||||
|
Quickshell.execDetached([
|
||||||
|
"notify-send",
|
||||||
|
Translation.tr("Battery full"),
|
||||||
|
Translation.tr("Please unplug the charger"),
|
||||||
|
"-a", "Shell"
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (root.soundEnabled) Audio.playSystemSound("complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
onIsPluggedInChanged: {
|
||||||
|
if (!root.available || !root.soundEnabled) return;
|
||||||
|
if (isPluggedIn) {
|
||||||
|
Audio.playSystemSound("power-plug")
|
||||||
|
} else {
|
||||||
|
Audio.playSystemSound("power-unplug")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import qs.services
|
||||||
import qs.modules.common
|
import qs.modules.common
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
@@ -17,7 +18,6 @@ Singleton {
|
|||||||
property int breakTime: Config.options.time.pomodoro.breakTime
|
property int breakTime: Config.options.time.pomodoro.breakTime
|
||||||
property int longBreakTime: Config.options.time.pomodoro.longBreak
|
property int longBreakTime: Config.options.time.pomodoro.longBreak
|
||||||
property int cyclesBeforeLongBreak: Config.options.time.pomodoro.cyclesBeforeLongBreak
|
property int cyclesBeforeLongBreak: Config.options.time.pomodoro.cyclesBeforeLongBreak
|
||||||
property string alertSound: Config.options.time.pomodoro.alertSound
|
|
||||||
|
|
||||||
property bool pomodoroRunning: Persistent.states.timer.pomodoro.running
|
property bool pomodoroRunning: Persistent.states.timer.pomodoro.running
|
||||||
property bool pomodoroBreak: Persistent.states.timer.pomodoro.isBreak
|
property bool pomodoroBreak: Persistent.states.timer.pomodoro.isBreak
|
||||||
@@ -64,8 +64,9 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Quickshell.execDetached(["notify-send", "Pomodoro", notificationMessage, "-a", "Shell"]);
|
Quickshell.execDetached(["notify-send", "Pomodoro", notificationMessage, "-a", "Shell"]);
|
||||||
if (alertSound)
|
if (Config.options.sounds.pomodoro) {
|
||||||
Quickshell.execDetached(["ffplay", "-nodisp", "-autoexit", alertSound]);
|
Audio.playSystemSound("alarm-clock-elapsed")
|
||||||
|
}
|
||||||
|
|
||||||
if (!pomodoroBreak) {
|
if (!pomodoroBreak) {
|
||||||
Persistent.states.timer.pomodoro.cycle = (Persistent.states.timer.pomodoro.cycle + 1) % root.cyclesBeforeLongBreak;
|
Persistent.states.timer.pomodoro.cycle = (Persistent.states.timer.pomodoro.cycle + 1) % root.cyclesBeforeLongBreak;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import "./modules/notificationPopup/"
|
|||||||
import "./modules/onScreenDisplay/"
|
import "./modules/onScreenDisplay/"
|
||||||
import "./modules/onScreenKeyboard/"
|
import "./modules/onScreenKeyboard/"
|
||||||
import "./modules/overview/"
|
import "./modules/overview/"
|
||||||
|
import "./modules/regionSelector/"
|
||||||
import "./modules/screenCorners/"
|
import "./modules/screenCorners/"
|
||||||
import "./modules/sessionScreen/"
|
import "./modules/sessionScreen/"
|
||||||
import "./modules/sidebarLeft/"
|
import "./modules/sidebarLeft/"
|
||||||
@@ -45,6 +46,7 @@ ShellRoot {
|
|||||||
property bool enableOnScreenDisplay: true
|
property bool enableOnScreenDisplay: true
|
||||||
property bool enableOnScreenKeyboard: true
|
property bool enableOnScreenKeyboard: true
|
||||||
property bool enableOverview: true
|
property bool enableOverview: true
|
||||||
|
property bool enableRegionSelector: true
|
||||||
property bool enableReloadPopup: true
|
property bool enableReloadPopup: true
|
||||||
property bool enableScreenCorners: true
|
property bool enableScreenCorners: true
|
||||||
property bool enableSessionScreen: true
|
property bool enableSessionScreen: true
|
||||||
@@ -74,6 +76,7 @@ ShellRoot {
|
|||||||
LazyLoader { active: enableOnScreenDisplay; component: OnScreenDisplay {} }
|
LazyLoader { active: enableOnScreenDisplay; component: OnScreenDisplay {} }
|
||||||
LazyLoader { active: enableOnScreenKeyboard; component: OnScreenKeyboard {} }
|
LazyLoader { active: enableOnScreenKeyboard; component: OnScreenKeyboard {} }
|
||||||
LazyLoader { active: enableOverview; component: Overview {} }
|
LazyLoader { active: enableOverview; component: Overview {} }
|
||||||
|
LazyLoader { active: enableRegionSelector; component: RegionSelector {} }
|
||||||
LazyLoader { active: enableReloadPopup; component: ReloadPopup {} }
|
LazyLoader { active: enableReloadPopup; component: ReloadPopup {} }
|
||||||
LazyLoader { active: enableScreenCorners; component: ScreenCorners {} }
|
LazyLoader { active: enableScreenCorners; component: ScreenCorners {} }
|
||||||
LazyLoader { active: enableSessionScreen; component: SessionScreen {} }
|
LazyLoader { active: enableSessionScreen; component: SessionScreen {} }
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# This script depends on `functions.sh' .
|
# This script depends on `functions.sh' .
|
||||||
# This is NOT a script for execution, but for loading functions, so NOT need execution permission or shebang.
|
# This script is not for direct execution, instead it should be sourced by other script. It does 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.
|
|
||||||
|
|
||||||
# shellcheck shell=bash
|
# 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 uv venv --prompt .venv $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV) -p 3.12
|
||||||
x source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate
|
x source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate
|
||||||
x uv pip install -r sdata/uv/requirements.txt
|
x uv pip install -r sdata/uv/requirements.txt
|
||||||
x deactivate # We don't need the virtual environment anymore
|
x deactivate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ Options for install:
|
|||||||
--skip-allsetups Skip the whole process setting up permissions/services etc
|
--skip-allsetups Skip the whole process setting up permissions/services etc
|
||||||
--skip-allfiles Skip the whole process copying configuration files
|
--skip-allfiles Skip the whole process copying configuration files
|
||||||
-s, --skip-sysupdate Skip system package upgrade e.g. \"sudo pacman -Syu\"
|
-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-hyprland Skip installing the config for Hyprland
|
||||||
--skip-fish Skip installing the config for Fish
|
--skip-fish Skip installing the config for Fish
|
||||||
--skip-plasmaintg Skip installing plasma-browser-integration
|
--skip-plasmaintg Skip installing plasma-browser-integration
|
||||||
--skip-miscconf Skip copying the dirs and files to \".configs\" except for
|
--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
|
--exp-files Use experimental script for the third step copying files
|
||||||
--fontset <set> (Unavailable yet) Use a set of pre-defined font and config
|
--fontset <set> (Unavailable yet) Use a set of pre-defined font and config
|
||||||
--via-nix (Unavailable yet) Use Nix to install dependencies
|
--via-nix (Unavailable yet) Use Nix to install dependencies
|
||||||
@@ -32,7 +33,7 @@ cleancache(){
|
|||||||
# `man getopt` to see more
|
# `man getopt` to see more
|
||||||
para=$(getopt \
|
para=$(getopt \
|
||||||
-o hfk:cs \
|
-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" -- "$@")
|
-n "$0" -- "$@")
|
||||||
[ $? != 0 ] && echo "$0: Error when getopt, please recheck parameters." && exit 1
|
[ $? != 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;;
|
-s|--skip-sysupdate) SKIP_SYSUPDATE=true;shift;;
|
||||||
--skip-hyprland) SKIP_HYPRLAND=true;shift;;
|
--skip-hyprland) SKIP_HYPRLAND=true;shift;;
|
||||||
--skip-fish) SKIP_FISH=true;shift;;
|
--skip-fish) SKIP_FISH=true;shift;;
|
||||||
|
--skip-quickshell) SKIP_QUICKSHELL=true;shift;;
|
||||||
--skip-miscconf) SKIP_MISCCONF=true;shift;;
|
--skip-miscconf) SKIP_MISCCONF=true;shift;;
|
||||||
--skip-plasmaintg) SKIP_PLASMAINTG=true;shift;;
|
--skip-plasmaintg) SKIP_PLASMAINTG=true;shift;;
|
||||||
--exp-files) EXPERIMENTAL_FILES_SCRIPT=true;shift;;
|
--exp-files) EXPERIMENTAL_FILES_SCRIPT=true;shift;;
|
||||||
|
|||||||
@@ -84,11 +84,11 @@ esac
|
|||||||
# original dotfiles and new ones in the SAME DIRECTORY
|
# original dotfiles and new ones in the SAME DIRECTORY
|
||||||
# (eg. in ~/.config/hypr) won't be mixed together
|
# (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
|
case $SKIP_MISCCONF in
|
||||||
true) sleep 0;;
|
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"
|
# i="dots/.config/$i"
|
||||||
echo "[$0]: Found target: 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/"
|
if [ -d "dots/.config/$i" ];then warning_rsync; v rsync -av --delete "dots/.config/$i/" "$XDG_CONFIG_HOME/$i/"
|
||||||
@@ -98,6 +98,13 @@ case $SKIP_MISCCONF in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
case $SKIP_QUICKSHELL in
|
||||||
|
true) sleep 0;;
|
||||||
|
*)
|
||||||
|
warning_rsync; v rsync -av --delete dots/.config/quickshell/ "$XDG_CONFIG_HOME"/quickshell/
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
case $SKIP_FISH in
|
case $SKIP_FISH in
|
||||||
true) sleep 0;;
|
true) sleep 0;;
|
||||||
*)
|
*)
|
||||||
|
|||||||
+15
-13
@@ -3,21 +3,17 @@
|
|||||||
|
|
||||||
EAPI=8
|
EAPI=8
|
||||||
|
|
||||||
inherit cmake
|
inherit cmake git-r3
|
||||||
|
|
||||||
DESCRIPTION="Toolkit for building desktop widgets using QtQuick"
|
DESCRIPTION="Toolkit for building desktop widgets using QtQuick"
|
||||||
HOMEPAGE="https://quickshell.org/"
|
HOMEPAGE="https://quickshell.org/"
|
||||||
|
|
||||||
if [[ "${PV}" = *9999 ]]; then
|
EGIT_REPO_URI="https://github.com/quickshell-mirror/quickshell.git"
|
||||||
inherit git-r3
|
EGIT_COMMIT="00858812f25b748d08b075a0d284093685fa3ffd"
|
||||||
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
|
|
||||||
|
|
||||||
|
KEYWORDS="~amd64 ~arm64 ~x86"
|
||||||
LICENSE="LGPL-3"
|
LICENSE="LGPL-3"
|
||||||
SLOT="0"
|
SLOT="0"
|
||||||
KEYWORDS="~amd64 ~arm64 ~x86"
|
|
||||||
# Upstream recommends leaving all build options enabled by default
|
# 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"
|
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 )
|
mpris? ( dev-qt/qtdbus )
|
||||||
pam? ( sys-libs/pam )
|
pam? ( sys-libs/pam )
|
||||||
bluetooth? ( net-wireless/bluez )
|
bluetooth? ( net-wireless/bluez )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"
|
"
|
||||||
DEPEND="${RDEPEND}"
|
DEPEND="${RDEPEND}"
|
||||||
BDEPEND="
|
BDEPEND="
|
||||||
|| ( >=sys-devel/gcc-14:* >=llvm-core/clang-17:* )
|
|| ( >=sys-devel/gcc-14:* >=llvm-core/clang-17:* )
|
||||||
dev-build/cmake
|
|
||||||
dev-build/ninja
|
|
||||||
virtual/pkgconfig
|
|
||||||
dev-cpp/cli11
|
|
||||||
dev-util/spirv-tools
|
dev-util/spirv-tools
|
||||||
dev-qt/qtshadertools:6
|
dev-qt/qtshadertools:6
|
||||||
breakpad? ( dev-util/breakpad )
|
|
||||||
wayland? (
|
wayland? (
|
||||||
dev-util/wayland-scanner
|
dev-util/wayland-scanner
|
||||||
dev-libs/wayland-protocols
|
dev-libs/wayland-protocols
|
||||||
)
|
)
|
||||||
|
dev-cpp/cli11
|
||||||
|
dev-build/ninja
|
||||||
|
dev-build/cmake
|
||||||
|
dev-vcs/git
|
||||||
|
virtual/pkgconfig
|
||||||
|
breakpad? ( dev-util/breakpad )
|
||||||
|
|
||||||
"
|
"
|
||||||
|
|
||||||
src_configure(){
|
src_configure(){
|
||||||
+5
-2
@@ -15,8 +15,11 @@ DEPEND=""
|
|||||||
RDEPEND="
|
RDEPEND="
|
||||||
gui-apps/fuzzel
|
gui-apps/fuzzel
|
||||||
dev-libs/glib
|
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
|
app-i18n/translate-shell
|
||||||
gui-apps/wlogout
|
gui-apps/wlogout
|
||||||
media-gfx/imagemagick
|
|
||||||
"
|
"
|
||||||
@@ -37,7 +37,7 @@ fi
|
|||||||
arch=$(portageq envvar ACCEPT_KEYWORDS)
|
arch=$(portageq envvar ACCEPT_KEYWORDS)
|
||||||
|
|
||||||
# Exclude hyprland, will deal with that separately
|
# 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"
|
ebuild_dir="/var/db/repos/localrepo"
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ app-misc/illogical-impulse-microtex-git
|
|||||||
app-misc/illogical-impulse-oneui4-icons-git
|
app-misc/illogical-impulse-oneui4-icons-git
|
||||||
app-misc/illogical-impulse-portal
|
app-misc/illogical-impulse-portal
|
||||||
app-misc/illogical-impulse-python
|
app-misc/illogical-impulse-python
|
||||||
|
app-misc/illogical-impulse-quickshell-git
|
||||||
app-misc/illogical-impulse-screencapture
|
app-misc/illogical-impulse-screencapture
|
||||||
app-misc/illogical-impulse-toolkit
|
app-misc/illogical-impulse-toolkit
|
||||||
app-misc/illogical-impulse-widgets
|
app-misc/illogical-impulse-widgets
|
||||||
@@ -39,3 +40,6 @@ gui-libs/hyprland-qt-support **
|
|||||||
gui-libs/hyprland-qtutils **
|
gui-libs/hyprland-qtutils **
|
||||||
gui-wm/hyprland **
|
gui-wm/hyprland **
|
||||||
x11-libs/libxkbcommon
|
x11-libs/libxkbcommon
|
||||||
|
dev-util/breakpad
|
||||||
|
dev-libs/linux-syscall-support
|
||||||
|
dev-embedded/libdisasm
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ sys-power/upower introspection
|
|||||||
gui-apps/fuzzel png svg
|
gui-apps/fuzzel png svg
|
||||||
dev-libs/glib dbus elf introspection mime xattr
|
dev-libs/glib dbus elf introspection mime xattr
|
||||||
# ngl idk about nm-connection-editor. Works fine without
|
# 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)
|
#app-i18n/translate-shell (nothing needed)
|
||||||
#gui-apps/wlogout (no use flags)
|
#gui-apps/wlogout (no use flags)
|
||||||
media-gfx/imagemagick xml
|
media-gfx/imagemagick xml
|
||||||
|
|||||||
Reference in New Issue
Block a user