mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-31 08:52:49 -05:00
Compare commits
16 Commits
8838fd67b9
...
deaac3fdf0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
deaac3fdf0 | ||
|
|
b7062fe40c | ||
|
|
64d5e99b9d | ||
|
|
f9d8a7d22b | ||
|
|
52fcd3ad98 | ||
|
|
9d1e0ee29b | ||
|
|
de62f48f50 | ||
|
|
f47b19274c | ||
|
|
bb7f7083b9 | ||
|
|
cd580090dc | ||
|
|
ddb74b598d | ||
|
|
29571fc3aa | ||
|
|
57ee0fb2bd | ||
|
|
3ef10e73a5 | ||
|
|
dc40492fc7 | ||
|
|
e606a76a86 |
30
.github/workflows/nix-pr-check.yml
vendored
Normal file
30
.github/workflows/nix-pr-check.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Check nix flake
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [master, main]
|
||||||
|
paths:
|
||||||
|
- "flake.*"
|
||||||
|
- "distro/nix/**"
|
||||||
|
jobs:
|
||||||
|
check-flake:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create GitHub App token
|
||||||
|
id: app_token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@v31
|
||||||
|
|
||||||
|
- name: Update vendorHash in flake.nix
|
||||||
|
run: nix flake check
|
||||||
@@ -35,7 +35,7 @@ Modes:
|
|||||||
full - Capture the focused output
|
full - Capture the focused output
|
||||||
all - Capture all outputs combined
|
all - Capture all outputs combined
|
||||||
output - Capture a specific output by name
|
output - Capture a specific output by name
|
||||||
window - Capture the focused window (Hyprland only)
|
window - Capture the focused window (Hyprland/DWL)
|
||||||
last - Capture the last selected region
|
last - Capture the last selected region
|
||||||
|
|
||||||
Output format (--format):
|
Output format (--format):
|
||||||
@@ -91,8 +91,7 @@ If no previous region exists, falls back to interactive selection.`,
|
|||||||
var ssWindowCmd = &cobra.Command{
|
var ssWindowCmd = &cobra.Command{
|
||||||
Use: "window",
|
Use: "window",
|
||||||
Short: "Capture the focused window",
|
Short: "Capture the focused window",
|
||||||
Long: `Capture the currently focused window.
|
Long: `Capture the currently focused window. Supported on Hyprland and DWL.`,
|
||||||
Currently only supported on Hyprland.`,
|
|
||||||
Run: runScreenshotWindow,
|
Run: runScreenshotWindow,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +295,14 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
|
|||||||
|
|
||||||
data := buf.Data()
|
data := buf.Data()
|
||||||
rgb := make([]byte, dstW*dstH*3)
|
rgb := make([]byte, dstW*dstH*3)
|
||||||
swapRB := pixelFormat == uint32(screenshot.FormatARGB8888) || pixelFormat == uint32(screenshot.FormatXRGB8888) || pixelFormat == 0
|
|
||||||
|
var swapRB bool
|
||||||
|
switch pixelFormat {
|
||||||
|
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888):
|
||||||
|
swapRB = false
|
||||||
|
default:
|
||||||
|
swapRB = true
|
||||||
|
}
|
||||||
|
|
||||||
for y := 0; y < dstH; y++ {
|
for y := 0; y < dstH; y++ {
|
||||||
srcY := int(float64(y) / scale)
|
srcY := int(float64(y) / scale)
|
||||||
@@ -310,7 +316,9 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
|
|||||||
}
|
}
|
||||||
si := srcY*buf.Stride + srcX*4
|
si := srcY*buf.Stride + srcX*4
|
||||||
di := (y*dstW + x) * 3
|
di := (y*dstW + x) * 3
|
||||||
if si+2 < len(data) {
|
if si+3 >= len(data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if swapRB {
|
if swapRB {
|
||||||
rgb[di+0] = data[si+2]
|
rgb[di+0] = data[si+2]
|
||||||
rgb[di+1] = data[si+1]
|
rgb[di+1] = data[si+1]
|
||||||
@@ -322,7 +330,6 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return rgb, dstW, dstH
|
return rgb, dstW, dstH
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,7 +378,37 @@ func runScreenshotList(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range outputs {
|
for _, o := range outputs {
|
||||||
fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n",
|
scaleStr := fmt.Sprintf("%.2f", o.FractionalScale)
|
||||||
o.Name, o.Width, o.Height, o.X, o.Y, o.Scale)
|
if o.FractionalScale == float64(int(o.FractionalScale)) {
|
||||||
|
scaleStr = fmt.Sprintf("%d", int(o.FractionalScale))
|
||||||
|
}
|
||||||
|
|
||||||
|
transformStr := transformName(o.Transform)
|
||||||
|
|
||||||
|
fmt.Printf("%s: %dx%d+%d+%d scale=%s transform=%s\n",
|
||||||
|
o.Name, o.Width, o.Height, o.X, o.Y, scaleStr, transformStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformName(t int32) string {
|
||||||
|
switch t {
|
||||||
|
case 0:
|
||||||
|
return "normal"
|
||||||
|
case 1:
|
||||||
|
return "90"
|
||||||
|
case 2:
|
||||||
|
return "180"
|
||||||
|
case 3:
|
||||||
|
return "270"
|
||||||
|
case 4:
|
||||||
|
return "flipped"
|
||||||
|
case 5:
|
||||||
|
return "flipped-90"
|
||||||
|
case 6:
|
||||||
|
return "flipped-180"
|
||||||
|
case 7:
|
||||||
|
return "flipped-270"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d", t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package colorpicker
|
package colorpicker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -79,6 +80,10 @@ func (s *SurfaceState) OnScreencopyBuffer(format PixelFormat, width, height, str
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if stride < width*4 {
|
||||||
|
return fmt.Errorf("invalid stride %d for width %d", stride, width)
|
||||||
|
}
|
||||||
|
|
||||||
if s.screenBuf != nil {
|
if s.screenBuf != nil {
|
||||||
s.screenBuf.Close()
|
s.screenBuf.Close()
|
||||||
s.screenBuf = nil
|
s.screenBuf = nil
|
||||||
@@ -279,10 +284,10 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
|
|||||||
drawMagnifierWithInversion(
|
drawMagnifierWithInversion(
|
||||||
dst.Data(), dst.Stride, dst.Width, dst.Height,
|
dst.Data(), dst.Stride, dst.Width, dst.Height,
|
||||||
s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height,
|
s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height,
|
||||||
px, py, picked, s.yInverted,
|
px, py, picked, s.yInverted, s.screenFormat,
|
||||||
)
|
)
|
||||||
|
|
||||||
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase)
|
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase, s.screenFormat)
|
||||||
|
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
@@ -390,6 +395,7 @@ func drawMagnifierWithInversion(
|
|||||||
cx, cy int,
|
cx, cy int,
|
||||||
borderColor Color,
|
borderColor Color,
|
||||||
yInverted bool,
|
yInverted bool,
|
||||||
|
format PixelFormat,
|
||||||
) {
|
) {
|
||||||
if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 {
|
if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 {
|
||||||
return
|
return
|
||||||
@@ -407,6 +413,14 @@ func drawMagnifierWithInversion(
|
|||||||
innerRadius := float64(outerRadius - borderThickness)
|
innerRadius := float64(outerRadius - borderThickness)
|
||||||
outerRadiusF := float64(outerRadius)
|
outerRadiusF := float64(outerRadius)
|
||||||
|
|
||||||
|
var rOff, bOff int
|
||||||
|
switch format {
|
||||||
|
case FormatABGR8888, FormatXBGR8888:
|
||||||
|
rOff, bOff = 0, 2
|
||||||
|
default:
|
||||||
|
rOff, bOff = 2, 0
|
||||||
|
}
|
||||||
|
|
||||||
for dy := -outerRadius - 2; dy <= outerRadius+2; dy++ {
|
for dy := -outerRadius - 2; dy <= outerRadius+2; dy++ {
|
||||||
y := cy + dy
|
y := cy + dy
|
||||||
if y < 0 || y >= dstH {
|
if y < 0 || y >= dstH {
|
||||||
@@ -431,9 +445,9 @@ func drawMagnifierWithInversion(
|
|||||||
}
|
}
|
||||||
|
|
||||||
bgColor := Color{
|
bgColor := Color{
|
||||||
B: dst[dstOff+0],
|
R: dst[dstOff+rOff],
|
||||||
G: dst[dstOff+1],
|
G: dst[dstOff+1],
|
||||||
R: dst[dstOff+2],
|
B: dst[dstOff+bOff],
|
||||||
A: dst[dstOff+3],
|
A: dst[dstOff+3],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +476,7 @@ func drawMagnifierWithInversion(
|
|||||||
}
|
}
|
||||||
srcOff := sy*srcStride + sx*4
|
srcOff := sy*srcStride + sx*4
|
||||||
if srcOff+4 <= len(src) {
|
if srcOff+4 <= len(src) {
|
||||||
magColor := Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255}
|
magColor := Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
|
||||||
finalColor = blendColors(magColor, borderColor, alpha)
|
finalColor = blendColors(magColor, borderColor, alpha)
|
||||||
} else {
|
} else {
|
||||||
finalColor = borderColor
|
finalColor = borderColor
|
||||||
@@ -483,24 +497,25 @@ func drawMagnifierWithInversion(
|
|||||||
}
|
}
|
||||||
srcOff := sy*srcStride + sx*4
|
srcOff := sy*srcStride + sx*4
|
||||||
if srcOff+4 <= len(src) {
|
if srcOff+4 <= len(src) {
|
||||||
finalColor = Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255}
|
finalColor = Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
|
||||||
} else {
|
} else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dst[dstOff+0] = finalColor.B
|
dst[dstOff+rOff] = finalColor.R
|
||||||
dst[dstOff+1] = finalColor.G
|
dst[dstOff+1] = finalColor.G
|
||||||
dst[dstOff+2] = finalColor.R
|
dst[dstOff+bOff] = finalColor.B
|
||||||
dst[dstOff+3] = 255
|
dst[dstOff+3] = 255
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius)
|
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius, format)
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawMagnifierCrosshair(
|
func drawMagnifierCrosshair(
|
||||||
data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int,
|
data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int,
|
||||||
|
format PixelFormat,
|
||||||
) {
|
) {
|
||||||
if width <= 0 || height <= 0 {
|
if width <= 0 || height <= 0 {
|
||||||
return
|
return
|
||||||
@@ -998,7 +1013,7 @@ var fontGlyphs = map[rune][fontH]uint8{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool) {
|
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool, pixelFormat PixelFormat) {
|
||||||
text := formatColorForPreview(c, format, lowercase)
|
text := formatColorForPreview(c, format, lowercase)
|
||||||
if len(text) == 0 {
|
if len(text) == 0 {
|
||||||
return
|
return
|
||||||
@@ -1033,9 +1048,8 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
|
|||||||
y = height - boxH
|
y = height - boxH
|
||||||
}
|
}
|
||||||
|
|
||||||
drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c)
|
drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c, pixelFormat)
|
||||||
|
|
||||||
// Use contrasting text color based on luminance
|
|
||||||
lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
|
lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
|
||||||
var fg Color
|
var fg Color
|
||||||
if lum > 128 {
|
if lum > 128 {
|
||||||
@@ -1043,7 +1057,7 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
|
|||||||
} else {
|
} else {
|
||||||
fg = Color{R: 255, G: 255, B: 255, A: 255}
|
fg = Color{R: 255, G: 255, B: 255, A: 255}
|
||||||
}
|
}
|
||||||
drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg)
|
drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg, pixelFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string {
|
func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string {
|
||||||
@@ -1064,7 +1078,7 @@ func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color) {
|
func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color, format PixelFormat) {
|
||||||
if w <= 0 || h <= 0 {
|
if w <= 0 || h <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1073,6 +1087,14 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
|
|||||||
x = clamp(x, 0, width)
|
x = clamp(x, 0, width)
|
||||||
y = clamp(y, 0, height)
|
y = clamp(y, 0, height)
|
||||||
|
|
||||||
|
var rOff, bOff int
|
||||||
|
switch format {
|
||||||
|
case FormatABGR8888, FormatXBGR8888:
|
||||||
|
rOff, bOff = 0, 2
|
||||||
|
default:
|
||||||
|
rOff, bOff = 2, 0
|
||||||
|
}
|
||||||
|
|
||||||
for yy := y; yy < yEnd; yy++ {
|
for yy := y; yy < yEnd; yy++ {
|
||||||
rowOff := yy * stride
|
rowOff := yy * stride
|
||||||
for xx := x; xx < xEnd; xx++ {
|
for xx := x; xx < xEnd; xx++ {
|
||||||
@@ -1080,26 +1102,34 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
|
|||||||
if off+4 > len(data) {
|
if off+4 > len(data) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
data[off+0] = col.B
|
data[off+rOff] = col.R
|
||||||
data[off+1] = col.G
|
data[off+1] = col.G
|
||||||
data[off+2] = col.R
|
data[off+bOff] = col.B
|
||||||
data[off+3] = 255
|
data[off+3] = 255
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawText(data []byte, stride, width, height, x, y int, text string, col Color) {
|
func drawText(data []byte, stride, width, height, x, y int, text string, col Color, format PixelFormat) {
|
||||||
for i, r := range text {
|
for i, r := range text {
|
||||||
drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col)
|
drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col, format)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color) {
|
func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color, format PixelFormat) {
|
||||||
g, ok := fontGlyphs[r]
|
g, ok := fontGlyphs[r]
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rOff, bOff int
|
||||||
|
switch format {
|
||||||
|
case FormatABGR8888, FormatXBGR8888:
|
||||||
|
rOff, bOff = 0, 2
|
||||||
|
default:
|
||||||
|
rOff, bOff = 2, 0
|
||||||
|
}
|
||||||
|
|
||||||
for row := 0; row < fontH; row++ {
|
for row := 0; row < fontH; row++ {
|
||||||
yy := y + row
|
yy := y + row
|
||||||
if yy < 0 || yy >= height {
|
if yy < 0 || yy >= height {
|
||||||
@@ -1123,9 +1153,9 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
data[off+0] = col.B
|
data[off+rOff] = col.R
|
||||||
data[off+1] = col.G
|
data[off+1] = col.G
|
||||||
data[off+2] = col.R
|
data[off+bOff] = col.B
|
||||||
data[off+3] = 255
|
data[off+3] = 255
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
|
||||||
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Compositor int
|
type Compositor int
|
||||||
@@ -44,10 +49,42 @@ func DetectCompositor() Compositor {
|
|||||||
return detectedCompositor
|
return detectedCompositor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if detectDWLProtocol() {
|
||||||
|
detectedCompositor = CompositorDWL
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
|
||||||
detectedCompositor = CompositorUnknown
|
detectedCompositor = CompositorUnknown
|
||||||
return detectedCompositor
|
return detectedCompositor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func detectDWLProtocol() bool {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ctx := display.Context()
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
if e.Interface == dwl_ipc.ZdwlIpcManagerV2InterfaceName {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
func SetCompositorDWL() {
|
func SetCompositorDWL() {
|
||||||
detectedCompositor = CompositorDWL
|
detectedCompositor = CompositorDWL
|
||||||
}
|
}
|
||||||
@@ -57,14 +94,21 @@ type WindowGeometry struct {
|
|||||||
Y int32
|
Y int32
|
||||||
Width int32
|
Width int32
|
||||||
Height int32
|
Height int32
|
||||||
|
Output string
|
||||||
|
Scale float64
|
||||||
|
OutputX int32
|
||||||
|
OutputY int32
|
||||||
|
OutputTransform int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetActiveWindow() (*WindowGeometry, error) {
|
func GetActiveWindow() (*WindowGeometry, error) {
|
||||||
switch DetectCompositor() {
|
switch DetectCompositor() {
|
||||||
case CompositorHyprland:
|
case CompositorHyprland:
|
||||||
return getHyprlandActiveWindow()
|
return getHyprlandActiveWindow()
|
||||||
|
case CompositorDWL:
|
||||||
|
return getDWLActiveWindow()
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("window capture requires Hyprland")
|
return nil, fmt.Errorf("window capture requires Hyprland or DWL")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,8 +264,113 @@ func SetDWLActiveOutput(name string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getDWLFocusedMonitor() string {
|
func getDWLFocusedMonitor() string {
|
||||||
|
if dwlActiveOutput != "" {
|
||||||
return dwlActiveOutput
|
return dwlActiveOutput
|
||||||
}
|
}
|
||||||
|
return queryDWLActiveOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryDWLActiveOutput() string {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
ctx := display.Context()
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var dwlManager *dwl_ipc.ZdwlIpcManagerV2
|
||||||
|
outputs := make(map[uint32]*client.Output)
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
|
||||||
|
mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
||||||
|
dwlManager = mgr
|
||||||
|
}
|
||||||
|
case client.OutputInterfaceName:
|
||||||
|
out := client.NewOutput(ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, version, out); err == nil {
|
||||||
|
outputs[e.Name] = out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if dwlManager == nil || len(outputs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
outputNames := make(map[uint32]string)
|
||||||
|
for name, out := range outputs {
|
||||||
|
n := name
|
||||||
|
out.SetNameHandler(func(e client.OutputNameEvent) {
|
||||||
|
outputNames[n] = e.Name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type outputState struct {
|
||||||
|
name string
|
||||||
|
active bool
|
||||||
|
gotFrame bool
|
||||||
|
}
|
||||||
|
states := make(map[uint32]*outputState)
|
||||||
|
|
||||||
|
for name, out := range outputs {
|
||||||
|
dwlOut, err := dwlManager.GetOutput(out)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
state := &outputState{name: outputNames[name]}
|
||||||
|
states[name] = state
|
||||||
|
|
||||||
|
dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
|
||||||
|
state.active = e.Active != 0
|
||||||
|
})
|
||||||
|
dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
|
||||||
|
state.gotFrame = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
allFramesReceived := func() bool {
|
||||||
|
for _, s := range states {
|
||||||
|
if !s.gotFrame {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for !allFramesReceived() {
|
||||||
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, state := range states {
|
||||||
|
if state.active {
|
||||||
|
return state.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func GetFocusedMonitor() string {
|
func GetFocusedMonitor() string {
|
||||||
switch DetectCompositor() {
|
switch DetectCompositor() {
|
||||||
@@ -236,3 +385,238 @@ func GetFocusedMonitor() string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type outputInfo struct {
|
||||||
|
x, y int32
|
||||||
|
transform int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
ctx := display.Context()
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
|
||||||
|
mgr := wlr_output_management.NewZwlrOutputManagerV1(ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
|
||||||
|
outputManager = mgr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if outputManager == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type headState struct {
|
||||||
|
name string
|
||||||
|
x, y int32
|
||||||
|
transform int32
|
||||||
|
}
|
||||||
|
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
||||||
|
done := false
|
||||||
|
|
||||||
|
outputManager.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
||||||
|
state := &headState{}
|
||||||
|
heads[e.Head] = state
|
||||||
|
e.Head.SetNameHandler(func(ne wlr_output_management.ZwlrOutputHeadV1NameEvent) {
|
||||||
|
state.name = ne.Name
|
||||||
|
})
|
||||||
|
e.Head.SetPositionHandler(func(pe wlr_output_management.ZwlrOutputHeadV1PositionEvent) {
|
||||||
|
state.x = pe.X
|
||||||
|
state.y = pe.Y
|
||||||
|
})
|
||||||
|
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
||||||
|
state.transform = te.Transform
|
||||||
|
})
|
||||||
|
})
|
||||||
|
outputManager.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
|
||||||
|
done = true
|
||||||
|
})
|
||||||
|
|
||||||
|
for !done {
|
||||||
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, state := range heads {
|
||||||
|
if state.name == outputName {
|
||||||
|
return &outputInfo{
|
||||||
|
x: state.x,
|
||||||
|
y: state.y,
|
||||||
|
transform: state.transform,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDWLActiveWindow() (*WindowGeometry, error) {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect: %w", err)
|
||||||
|
}
|
||||||
|
ctx := display.Context()
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dwlManager *dwl_ipc.ZdwlIpcManagerV2
|
||||||
|
outputs := make(map[uint32]*client.Output)
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
|
||||||
|
mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
||||||
|
dwlManager = mgr
|
||||||
|
}
|
||||||
|
case client.OutputInterfaceName:
|
||||||
|
out := client.NewOutput(ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, version, out); err == nil {
|
||||||
|
outputs[e.Name] = out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dwlManager == nil {
|
||||||
|
return nil, fmt.Errorf("dwl_ipc_manager not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(outputs) == 0 {
|
||||||
|
return nil, fmt.Errorf("no outputs found")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputNames := make(map[uint32]string)
|
||||||
|
for name, out := range outputs {
|
||||||
|
n := name
|
||||||
|
out.SetNameHandler(func(e client.OutputNameEvent) {
|
||||||
|
outputNames[n] = e.Name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dwlOutputState struct {
|
||||||
|
output *dwl_ipc.ZdwlIpcOutputV2
|
||||||
|
name string
|
||||||
|
active bool
|
||||||
|
x, y int32
|
||||||
|
w, h int32
|
||||||
|
scalefactor uint32
|
||||||
|
gotFrame bool
|
||||||
|
}
|
||||||
|
|
||||||
|
dwlOutputs := make(map[uint32]*dwlOutputState)
|
||||||
|
for name, out := range outputs {
|
||||||
|
dwlOut, err := dwlManager.GetOutput(out)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
state := &dwlOutputState{output: dwlOut, name: outputNames[name]}
|
||||||
|
dwlOutputs[name] = state
|
||||||
|
|
||||||
|
dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
|
||||||
|
state.active = e.Active != 0
|
||||||
|
})
|
||||||
|
dwlOut.SetXHandler(func(e dwl_ipc.ZdwlIpcOutputV2XEvent) {
|
||||||
|
state.x = e.X
|
||||||
|
})
|
||||||
|
dwlOut.SetYHandler(func(e dwl_ipc.ZdwlIpcOutputV2YEvent) {
|
||||||
|
state.y = e.Y
|
||||||
|
})
|
||||||
|
dwlOut.SetWidthHandler(func(e dwl_ipc.ZdwlIpcOutputV2WidthEvent) {
|
||||||
|
state.w = e.Width
|
||||||
|
})
|
||||||
|
dwlOut.SetHeightHandler(func(e dwl_ipc.ZdwlIpcOutputV2HeightEvent) {
|
||||||
|
state.h = e.Height
|
||||||
|
})
|
||||||
|
dwlOut.SetScalefactorHandler(func(e dwl_ipc.ZdwlIpcOutputV2ScalefactorEvent) {
|
||||||
|
state.scalefactor = e.Scalefactor
|
||||||
|
})
|
||||||
|
dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
|
||||||
|
state.gotFrame = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
allFramesReceived := func() bool {
|
||||||
|
for _, s := range dwlOutputs {
|
||||||
|
if !s.gotFrame {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for !allFramesReceived() {
|
||||||
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
return nil, fmt.Errorf("dispatch: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, state := range dwlOutputs {
|
||||||
|
if !state.active {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if state.w <= 0 || state.h <= 0 {
|
||||||
|
return nil, fmt.Errorf("no active window")
|
||||||
|
}
|
||||||
|
scale := float64(state.scalefactor) / 100.0
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
geom := &WindowGeometry{
|
||||||
|
X: state.x,
|
||||||
|
Y: state.y,
|
||||||
|
Width: state.w,
|
||||||
|
Height: state.h,
|
||||||
|
Output: state.name,
|
||||||
|
Scale: scale,
|
||||||
|
}
|
||||||
|
|
||||||
|
if info, ok := getOutputInfo(state.name); ok {
|
||||||
|
geom.OutputX = info.x
|
||||||
|
geom.OutputY = info.y
|
||||||
|
geom.OutputTransform = info.transform
|
||||||
|
}
|
||||||
|
|
||||||
|
return geom, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no active output found")
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ func BufferToImageWithFormat(buf *ShmBuffer, format uint32) *image.RGBA {
|
|||||||
img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height))
|
img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height))
|
||||||
data := buf.Data()
|
data := buf.Data()
|
||||||
|
|
||||||
swapRB := format == uint32(FormatARGB8888) || format == uint32(FormatXRGB8888) || format == 0
|
var swapRB bool
|
||||||
|
switch format {
|
||||||
|
case uint32(FormatABGR8888), uint32(FormatXBGR8888):
|
||||||
|
swapRB = false
|
||||||
|
default:
|
||||||
|
swapRB = true
|
||||||
|
}
|
||||||
|
|
||||||
for y := 0; y < buf.Height; y++ {
|
for y := 0; y < buf.Height; y++ {
|
||||||
srcOff := y * buf.Stride
|
srcOff := y * buf.Stride
|
||||||
|
|||||||
@@ -380,19 +380,21 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var capturedBuf *ShmBuffer
|
||||||
|
|
||||||
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
||||||
|
if int(e.Stride) < int(e.Width)*4 {
|
||||||
|
log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width)
|
||||||
|
return
|
||||||
|
}
|
||||||
buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
|
buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("create screen buffer failed", "err", err)
|
log.Error("create screen buffer failed", "err", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if withCursor {
|
capturedBuf = buf
|
||||||
pc.screenBuf = buf
|
|
||||||
pc.format = e.Format
|
pc.format = e.Format
|
||||||
} else {
|
|
||||||
pc.screenBufNoCursor = buf
|
|
||||||
}
|
|
||||||
|
|
||||||
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
|
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -421,6 +423,34 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
|
|||||||
|
|
||||||
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
|
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
|
||||||
frame.Destroy()
|
frame.Destroy()
|
||||||
|
|
||||||
|
if capturedBuf == nil {
|
||||||
|
onReady()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if pc.yInverted {
|
||||||
|
capturedBuf.FlipVertical()
|
||||||
|
pc.yInverted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if output.transform != TransformNormal {
|
||||||
|
invTransform := InverseTransform(output.transform)
|
||||||
|
transformed, err := capturedBuf.ApplyTransform(invTransform)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("apply transform failed", "err", err)
|
||||||
|
} else if transformed != capturedBuf {
|
||||||
|
capturedBuf.Close()
|
||||||
|
capturedBuf = transformed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if withCursor {
|
||||||
|
pc.screenBuf = capturedBuf
|
||||||
|
} else {
|
||||||
|
pc.screenBufNoCursor = capturedBuf
|
||||||
|
}
|
||||||
|
|
||||||
onReady()
|
onReady()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -135,16 +135,120 @@ func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
|
|||||||
Height: geom.Height,
|
Height: geom.Height,
|
||||||
}
|
}
|
||||||
|
|
||||||
output := s.findOutputForRegion(region)
|
var output *WaylandOutput
|
||||||
|
if geom.Output != "" {
|
||||||
|
output = s.findOutputByName(geom.Output)
|
||||||
|
}
|
||||||
|
if output == nil {
|
||||||
|
output = s.findOutputForRegion(region)
|
||||||
|
}
|
||||||
if output == nil {
|
if output == nil {
|
||||||
return nil, fmt.Errorf("could not find output for window")
|
return nil, fmt.Errorf("could not find output for window")
|
||||||
}
|
}
|
||||||
|
|
||||||
if DetectCompositor() == CompositorHyprland {
|
switch DetectCompositor() {
|
||||||
|
case CompositorHyprland:
|
||||||
return s.captureAndCrop(output, region)
|
return s.captureAndCrop(output, region)
|
||||||
|
case CompositorDWL:
|
||||||
|
return s.captureDWLWindow(output, region, geom)
|
||||||
|
default:
|
||||||
|
return s.captureRegionOnOutput(output, region)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.captureRegionOnOutput(output, region)
|
func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, geom *WindowGeometry) (*CaptureResult, error) {
|
||||||
|
result, err := s.captureWholeOutput(output)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scale := geom.Scale
|
||||||
|
if scale <= 0 || scale == 1.0 {
|
||||||
|
if output.fractionalScale > 1.0 {
|
||||||
|
scale = output.fractionalScale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
localX := int(float64(region.X-geom.OutputX) * scale)
|
||||||
|
localY := int(float64(region.Y-geom.OutputY) * scale)
|
||||||
|
w := int(float64(region.Width) * scale)
|
||||||
|
h := int(float64(region.Height) * scale)
|
||||||
|
|
||||||
|
if localX < 0 {
|
||||||
|
w += localX
|
||||||
|
localX = 0
|
||||||
|
}
|
||||||
|
if localY < 0 {
|
||||||
|
h += localY
|
||||||
|
localY = 0
|
||||||
|
}
|
||||||
|
if localX+w > result.Buffer.Width {
|
||||||
|
w = result.Buffer.Width - localX
|
||||||
|
}
|
||||||
|
if localY+h > result.Buffer.Height {
|
||||||
|
h = result.Buffer.Height - localY
|
||||||
|
}
|
||||||
|
|
||||||
|
if w <= 0 || h <= 0 {
|
||||||
|
result.Buffer.Close()
|
||||||
|
return nil, fmt.Errorf("window not visible on output")
|
||||||
|
}
|
||||||
|
|
||||||
|
cropped, err := CreateShmBuffer(w, h, w*4)
|
||||||
|
if err != nil {
|
||||||
|
result.Buffer.Close()
|
||||||
|
return nil, fmt.Errorf("create crop buffer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcData := result.Buffer.Data()
|
||||||
|
dstData := cropped.Data()
|
||||||
|
|
||||||
|
for y := 0; y < h; y++ {
|
||||||
|
srcY := localY + y
|
||||||
|
if result.YInverted {
|
||||||
|
srcY = result.Buffer.Height - 1 - (localY + y)
|
||||||
|
}
|
||||||
|
if srcY < 0 || srcY >= result.Buffer.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dstY := y
|
||||||
|
if result.YInverted {
|
||||||
|
dstY = h - 1 - y
|
||||||
|
}
|
||||||
|
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
srcX := localX + x
|
||||||
|
if srcX < 0 || srcX >= result.Buffer.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
si := srcY*result.Buffer.Stride + srcX*4
|
||||||
|
di := dstY*cropped.Stride + x*4
|
||||||
|
|
||||||
|
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dstData[di+0] = srcData[si+0]
|
||||||
|
dstData[di+1] = srcData[si+1]
|
||||||
|
dstData[di+2] = srcData[si+2]
|
||||||
|
dstData[di+3] = srcData[si+3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Buffer.Close()
|
||||||
|
cropped.Format = PixelFormat(result.Format)
|
||||||
|
|
||||||
|
return &CaptureResult{
|
||||||
|
Buffer: cropped,
|
||||||
|
Region: region,
|
||||||
|
YInverted: false,
|
||||||
|
Format: result.Format,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) captureFullScreen() (*CaptureResult, error) {
|
func (s *Screenshoter) captureFullScreen() (*CaptureResult, error) {
|
||||||
@@ -220,13 +324,18 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
|
|
||||||
outX, outY := output.x, output.y
|
outX, outY := output.x, output.y
|
||||||
scale := float64(output.scale)
|
scale := float64(output.scale)
|
||||||
if DetectCompositor() == CompositorHyprland {
|
switch DetectCompositor() {
|
||||||
|
case CompositorHyprland:
|
||||||
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
||||||
outX, outY = hx, hy
|
outX, outY = hx, hy
|
||||||
}
|
}
|
||||||
if s := GetHyprlandMonitorScale(output.name); s > 0 {
|
if s := GetHyprlandMonitorScale(output.name); s > 0 {
|
||||||
scale = s
|
scale = s
|
||||||
}
|
}
|
||||||
|
case CompositorDWL:
|
||||||
|
if info, ok := getOutputInfo(output.name); ok {
|
||||||
|
outX, outY = info.x, info.y
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if scale <= 0 {
|
if scale <= 0 {
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
@@ -354,13 +463,42 @@ func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult
|
|||||||
return nil, fmt.Errorf("capture output: %w", err)
|
return nil, fmt.Errorf("capture output: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.processFrame(frame, Region{
|
result, err := s.processFrame(frame, Region{
|
||||||
X: output.x,
|
X: output.x,
|
||||||
Y: output.y,
|
Y: output.y,
|
||||||
Width: output.width,
|
Width: output.width,
|
||||||
Height: output.height,
|
Height: output.height,
|
||||||
Output: output.name,
|
Output: output.name,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.YInverted {
|
||||||
|
result.Buffer.FlipVertical()
|
||||||
|
result.YInverted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if output.transform == TransformNormal {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
invTransform := InverseTransform(output.transform)
|
||||||
|
transformed, err := result.Buffer.ApplyTransform(invTransform)
|
||||||
|
if err != nil {
|
||||||
|
result.Buffer.Close()
|
||||||
|
return nil, fmt.Errorf("apply transform: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if transformed != result.Buffer {
|
||||||
|
result.Buffer.Close()
|
||||||
|
result.Buffer = transformed
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Region.Width = int32(transformed.Width)
|
||||||
|
result.Region.Height = int32(transformed.Height)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*CaptureResult, error) {
|
func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*CaptureResult, error) {
|
||||||
@@ -441,6 +579,10 @@ func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*Ca
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
|
func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
|
||||||
|
if output.transform != TransformNormal {
|
||||||
|
return s.captureRegionOnTransformedOutput(output, region)
|
||||||
|
}
|
||||||
|
|
||||||
scale := output.fractionalScale
|
scale := output.fractionalScale
|
||||||
if scale <= 0 && DetectCompositor() == CompositorHyprland {
|
if scale <= 0 && DetectCompositor() == CompositorHyprland {
|
||||||
scale = GetHyprlandMonitorScale(output.name)
|
scale = GetHyprlandMonitorScale(output.name)
|
||||||
@@ -457,6 +599,31 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
|
|||||||
w := int32(float64(region.Width) * scale)
|
w := int32(float64(region.Width) * scale)
|
||||||
h := int32(float64(region.Height) * scale)
|
h := int32(float64(region.Height) * scale)
|
||||||
|
|
||||||
|
if DetectCompositor() == CompositorDWL {
|
||||||
|
scaledOutW := int32(float64(output.width) * scale)
|
||||||
|
scaledOutH := int32(float64(output.height) * scale)
|
||||||
|
if localX >= scaledOutW {
|
||||||
|
localX = localX % scaledOutW
|
||||||
|
}
|
||||||
|
if localY >= scaledOutH {
|
||||||
|
localY = localY % scaledOutH
|
||||||
|
}
|
||||||
|
if localX+w > scaledOutW {
|
||||||
|
w = scaledOutW - localX
|
||||||
|
}
|
||||||
|
if localY+h > scaledOutH {
|
||||||
|
h = scaledOutH - localY
|
||||||
|
}
|
||||||
|
if localX < 0 {
|
||||||
|
w += localX
|
||||||
|
localX = 0
|
||||||
|
}
|
||||||
|
if localY < 0 {
|
||||||
|
h += localY
|
||||||
|
localY = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cursor := int32(0)
|
cursor := int32(0)
|
||||||
if s.config.IncludeCursor {
|
if s.config.IncludeCursor {
|
||||||
cursor = 1
|
cursor = 1
|
||||||
@@ -470,6 +637,76 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
|
|||||||
return s.processFrame(frame, region)
|
return s.processFrame(frame, region)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
|
||||||
|
result, err := s.captureWholeOutput(output)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scale := output.fractionalScale
|
||||||
|
if scale <= 0 && DetectCompositor() == CompositorHyprland {
|
||||||
|
scale = GetHyprlandMonitorScale(output.name)
|
||||||
|
}
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = float64(output.scale)
|
||||||
|
}
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
localX := int(float64(region.X-output.x) * scale)
|
||||||
|
localY := int(float64(region.Y-output.y) * scale)
|
||||||
|
w := int(float64(region.Width) * scale)
|
||||||
|
h := int(float64(region.Height) * scale)
|
||||||
|
|
||||||
|
if localX < 0 {
|
||||||
|
w += localX
|
||||||
|
localX = 0
|
||||||
|
}
|
||||||
|
if localY < 0 {
|
||||||
|
h += localY
|
||||||
|
localY = 0
|
||||||
|
}
|
||||||
|
if localX+w > result.Buffer.Width {
|
||||||
|
w = result.Buffer.Width - localX
|
||||||
|
}
|
||||||
|
if localY+h > result.Buffer.Height {
|
||||||
|
h = result.Buffer.Height - localY
|
||||||
|
}
|
||||||
|
|
||||||
|
if w <= 0 || h <= 0 {
|
||||||
|
result.Buffer.Close()
|
||||||
|
return nil, fmt.Errorf("region not visible on output")
|
||||||
|
}
|
||||||
|
|
||||||
|
cropped, err := CreateShmBuffer(w, h, w*4)
|
||||||
|
if err != nil {
|
||||||
|
result.Buffer.Close()
|
||||||
|
return nil, fmt.Errorf("create crop buffer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcData := result.Buffer.Data()
|
||||||
|
dstData := cropped.Data()
|
||||||
|
|
||||||
|
for y := 0; y < h; y++ {
|
||||||
|
srcOff := (localY+y)*result.Buffer.Stride + localX*4
|
||||||
|
dstOff := y * cropped.Stride
|
||||||
|
if srcOff+w*4 <= len(srcData) && dstOff+w*4 <= len(dstData) {
|
||||||
|
copy(dstData[dstOff:dstOff+w*4], srcData[srcOff:srcOff+w*4])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Buffer.Close()
|
||||||
|
cropped.Format = PixelFormat(result.Format)
|
||||||
|
|
||||||
|
return &CaptureResult{
|
||||||
|
Buffer: cropped,
|
||||||
|
Region: region,
|
||||||
|
YInverted: false,
|
||||||
|
Format: result.Format,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) {
|
func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) {
|
||||||
var buf *ShmBuffer
|
var buf *ShmBuffer
|
||||||
var pool *client.ShmPool
|
var pool *client.ShmPool
|
||||||
@@ -480,6 +717,10 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
|
|||||||
failed := false
|
failed := false
|
||||||
|
|
||||||
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
||||||
|
if int(e.Stride) < int(e.Width)*4 {
|
||||||
|
log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width)
|
||||||
|
return
|
||||||
|
}
|
||||||
var err error
|
var err error
|
||||||
buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
|
buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -557,6 +798,17 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) findOutputByName(name string) *WaylandOutput {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
defer s.outputsMu.Unlock()
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
if o.name == name {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) findOutputForRegion(region Region) *WaylandOutput {
|
func (s *Screenshoter) findOutputForRegion(region Region) *WaylandOutput {
|
||||||
s.outputsMu.Lock()
|
s.outputsMu.Lock()
|
||||||
defer s.outputsMu.Unlock()
|
defer s.outputsMu.Unlock()
|
||||||
@@ -766,16 +1018,32 @@ func ListOutputs() ([]Output, error) {
|
|||||||
sc.outputsMu.Lock()
|
sc.outputsMu.Lock()
|
||||||
defer sc.outputsMu.Unlock()
|
defer sc.outputsMu.Unlock()
|
||||||
|
|
||||||
|
compositor := DetectCompositor()
|
||||||
result := make([]Output, 0, len(sc.outputs))
|
result := make([]Output, 0, len(sc.outputs))
|
||||||
for _, o := range sc.outputs {
|
for _, o := range sc.outputs {
|
||||||
result = append(result, Output{
|
out := Output{
|
||||||
Name: o.name,
|
Name: o.name,
|
||||||
X: o.x,
|
X: o.x,
|
||||||
Y: o.y,
|
Y: o.y,
|
||||||
Width: o.width,
|
Width: o.width,
|
||||||
Height: o.height,
|
Height: o.height,
|
||||||
Scale: o.scale,
|
Scale: o.scale,
|
||||||
})
|
FractionalScale: o.fractionalScale,
|
||||||
|
Transform: o.transform,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch compositor {
|
||||||
|
case CompositorHyprland:
|
||||||
|
if hx, hy, hw, hh, ok := GetHyprlandMonitorGeometry(o.name); ok {
|
||||||
|
out.X, out.Y = hx, hy
|
||||||
|
out.Width, out.Height = hw, hh
|
||||||
|
}
|
||||||
|
if s := GetHyprlandMonitorScale(o.name); s > 0 {
|
||||||
|
out.FractionalScale = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, out)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,23 @@ const (
|
|||||||
FormatXBGR8888 = shm.FormatXBGR8888
|
FormatXBGR8888 = shm.FormatXBGR8888
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TransformNormal = shm.TransformNormal
|
||||||
|
Transform90 = shm.Transform90
|
||||||
|
Transform180 = shm.Transform180
|
||||||
|
Transform270 = shm.Transform270
|
||||||
|
TransformFlipped = shm.TransformFlipped
|
||||||
|
TransformFlipped90 = shm.TransformFlipped90
|
||||||
|
TransformFlipped180 = shm.TransformFlipped180
|
||||||
|
TransformFlipped270 = shm.TransformFlipped270
|
||||||
|
)
|
||||||
|
|
||||||
type ShmBuffer = shm.Buffer
|
type ShmBuffer = shm.Buffer
|
||||||
|
|
||||||
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
||||||
return shm.CreateBuffer(width, height, stride)
|
return shm.CreateBuffer(width, height, stride)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InverseTransform(transform int32) int32 {
|
||||||
|
return shm.InverseTransform(transform)
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ type Output struct {
|
|||||||
Width int32
|
Width int32
|
||||||
Height int32
|
Height int32
|
||||||
Scale int32
|
Scale int32
|
||||||
|
FractionalScale float64
|
||||||
|
Transform int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|||||||
@@ -306,6 +306,15 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(ledStates) == 0 {
|
||||||
|
log.Debug("No LED state available (empty map)")
|
||||||
|
|
||||||
|
// This means the device either:
|
||||||
|
// - doesn't support LED reporting at all, or
|
||||||
|
// - the kernel returned an empty state
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
capsLockState := ledStates[ledCapslockKey]
|
capsLockState := ledStates[ledCapslockKey]
|
||||||
m.updateCapsLockStateDirect(capsLockState)
|
m.updateCapsLockStateDirect(capsLockState)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,3 +137,96 @@ func (b *Buffer) Clear() {
|
|||||||
func (b *Buffer) CopyFrom(src *Buffer) {
|
func (b *Buffer) CopyFrom(src *Buffer) {
|
||||||
copy(b.data, src.data)
|
copy(b.data, src.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TransformNormal = 0
|
||||||
|
Transform90 = 1
|
||||||
|
Transform180 = 2
|
||||||
|
Transform270 = 3
|
||||||
|
TransformFlipped = 4
|
||||||
|
TransformFlipped90 = 5
|
||||||
|
TransformFlipped180 = 6
|
||||||
|
TransformFlipped270 = 7
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *Buffer) ApplyTransform(transform int32) (*Buffer, error) {
|
||||||
|
if transform == TransformNormal {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var newW, newH int
|
||||||
|
switch transform {
|
||||||
|
case Transform90, Transform270, TransformFlipped90, TransformFlipped270:
|
||||||
|
newW, newH = b.Height, b.Width
|
||||||
|
default:
|
||||||
|
newW, newH = b.Width, b.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
newBuf, err := CreateBuffer(newW, newH, newW*4)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newBuf.Format = b.Format
|
||||||
|
|
||||||
|
srcData := b.data
|
||||||
|
dstData := newBuf.data
|
||||||
|
|
||||||
|
for sy := 0; sy < b.Height; sy++ {
|
||||||
|
for sx := 0; sx < b.Width; sx++ {
|
||||||
|
var dx, dy int
|
||||||
|
|
||||||
|
switch transform {
|
||||||
|
case Transform90: // 90° CCW
|
||||||
|
dx = sy
|
||||||
|
dy = b.Width - 1 - sx
|
||||||
|
case Transform180:
|
||||||
|
dx = b.Width - 1 - sx
|
||||||
|
dy = b.Height - 1 - sy
|
||||||
|
case Transform270: // 270° CCW = 90° CW
|
||||||
|
dx = b.Height - 1 - sy
|
||||||
|
dy = sx
|
||||||
|
case TransformFlipped:
|
||||||
|
dx = b.Width - 1 - sx
|
||||||
|
dy = sy
|
||||||
|
case TransformFlipped90:
|
||||||
|
dx = sy
|
||||||
|
dy = sx
|
||||||
|
case TransformFlipped180:
|
||||||
|
dx = sx
|
||||||
|
dy = b.Height - 1 - sy
|
||||||
|
case TransformFlipped270:
|
||||||
|
dx = b.Height - 1 - sy
|
||||||
|
dy = b.Width - 1 - sx
|
||||||
|
default:
|
||||||
|
dx, dy = sx, sy
|
||||||
|
}
|
||||||
|
|
||||||
|
si := sy*b.Stride + sx*4
|
||||||
|
di := dy*newBuf.Stride + dx*4
|
||||||
|
|
||||||
|
if si+3 < len(srcData) && di+3 < len(dstData) {
|
||||||
|
dstData[di+0] = srcData[si+0]
|
||||||
|
dstData[di+1] = srcData[si+1]
|
||||||
|
dstData[di+2] = srcData[si+2]
|
||||||
|
dstData[di+3] = srcData[si+3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newBuf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func InverseTransform(transform int32) int32 {
|
||||||
|
switch transform {
|
||||||
|
case Transform90:
|
||||||
|
return Transform270
|
||||||
|
case Transform270:
|
||||||
|
return Transform90
|
||||||
|
case TransformFlipped90:
|
||||||
|
return TransformFlipped270
|
||||||
|
case TransformFlipped270:
|
||||||
|
return TransformFlipped90
|
||||||
|
default:
|
||||||
|
return transform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
installShellFiles
|
installShellFiles
|
||||||
.makeWrapper
|
makeWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ Singleton {
|
|||||||
|
|
||||||
property bool lockScreenShowPowerActions: true
|
property bool lockScreenShowPowerActions: true
|
||||||
property bool enableFprint: false
|
property bool enableFprint: false
|
||||||
property int maxFprintTries: 3
|
property int maxFprintTries: 15
|
||||||
property bool fprintdAvailable: false
|
property bool fprintdAvailable: false
|
||||||
property string lockScreenActiveMonitor: "all"
|
property string lockScreenActiveMonitor: "all"
|
||||||
property string lockScreenInactiveColor: "#000000"
|
property string lockScreenInactiveColor: "#000000"
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ var SPEC = {
|
|||||||
|
|
||||||
lockScreenShowPowerActions: { def: true },
|
lockScreenShowPowerActions: { def: true },
|
||||||
enableFprint: { def: false },
|
enableFprint: { def: false },
|
||||||
maxFprintTries: { def: 3 },
|
maxFprintTries: { def: 15 },
|
||||||
fprintdAvailable: { def: false, persist: false },
|
fprintdAvailable: { def: false, persist: false },
|
||||||
lockScreenActiveMonitor: { def: "all" },
|
lockScreenActiveMonitor: { def: "all" },
|
||||||
lockScreenInactiveColor: { def: "#000000" },
|
lockScreenInactiveColor: { def: "#000000" },
|
||||||
|
|||||||
@@ -108,9 +108,10 @@ Item {
|
|||||||
id: barRepeaterModel
|
id: barRepeaterModel
|
||||||
values: {
|
values: {
|
||||||
const configs = SettingsData.barConfigs;
|
const configs = SettingsData.barConfigs;
|
||||||
return configs
|
return configs.map(c => ({
|
||||||
.map(c => ({ id: c.id, position: c.position }))
|
id: c.id,
|
||||||
.sort((a, b) => {
|
position: c.position
|
||||||
|
})).sort((a, b) => {
|
||||||
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
|
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
|
||||||
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
|
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
|
||||||
return aVertical - bVertical;
|
return aVertical - bVertical;
|
||||||
@@ -142,9 +143,34 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property bool dockEnabled: false
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: dockRecreateDebounce
|
||||||
|
interval: 500
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
root.dockEnabled = false;
|
||||||
|
Qt.callLater(() => {
|
||||||
|
root.dockEnabled = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
dockRecreateDebounce.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SettingsData
|
||||||
|
function onBarConfigsChanged() {
|
||||||
|
dockRecreateDebounce.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: dockLoader
|
id: dockLoader
|
||||||
active: true
|
active: root.dockEnabled
|
||||||
asynchronous: false
|
asynchronous: false
|
||||||
|
|
||||||
property var currentPosition: SettingsData.dockPosition
|
property var currentPosition: SettingsData.dockPosition
|
||||||
@@ -440,62 +466,71 @@ Item {
|
|||||||
title: I18n.tr("Open with...")
|
title: I18n.tr("Open with...")
|
||||||
|
|
||||||
function shellEscape(str) {
|
function shellEscape(str) {
|
||||||
return "'" + str.replace(/'/g, "'\\''") + "'"
|
return "'" + str.replace(/'/g, "'\\''") + "'";
|
||||||
}
|
}
|
||||||
|
|
||||||
onApplicationSelected: (app, filePath) => {
|
onApplicationSelected: (app, filePath) => {
|
||||||
if (!app) return
|
if (!app)
|
||||||
|
return;
|
||||||
|
let cmd = app.exec || "";
|
||||||
|
const escapedPath = shellEscape(filePath);
|
||||||
|
const escapedUri = shellEscape("file://" + filePath);
|
||||||
|
|
||||||
let cmd = app.exec || ""
|
let hasField = false;
|
||||||
const escapedPath = shellEscape(filePath)
|
if (cmd.includes("%f")) {
|
||||||
const escapedUri = shellEscape("file://" + filePath)
|
cmd = cmd.replace("%f", escapedPath);
|
||||||
|
hasField = true;
|
||||||
let hasField = false
|
} else if (cmd.includes("%F")) {
|
||||||
if (cmd.includes("%f")) { cmd = cmd.replace("%f", escapedPath); hasField = true }
|
cmd = cmd.replace("%F", escapedPath);
|
||||||
else if (cmd.includes("%F")) { cmd = cmd.replace("%F", escapedPath); hasField = true }
|
hasField = true;
|
||||||
else if (cmd.includes("%u")) { cmd = cmd.replace("%u", escapedUri); hasField = true }
|
} else if (cmd.includes("%u")) {
|
||||||
else if (cmd.includes("%U")) { cmd = cmd.replace("%U", escapedUri); hasField = true }
|
cmd = cmd.replace("%u", escapedUri);
|
||||||
|
hasField = true;
|
||||||
cmd = cmd.replace(/%[ikc]/g, "")
|
} else if (cmd.includes("%U")) {
|
||||||
|
cmd = cmd.replace("%U", escapedUri);
|
||||||
if (!hasField) {
|
hasField = true;
|
||||||
cmd += " " + escapedPath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("FilePicker: Launching", cmd)
|
cmd = cmd.replace(/%[ikc]/g, "");
|
||||||
|
|
||||||
|
if (!hasField) {
|
||||||
|
cmd += " " + escapedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("FilePicker: Launching", cmd);
|
||||||
|
|
||||||
Quickshell.execDetached({
|
Quickshell.execDetached({
|
||||||
command: ["sh", "-c", cmd]
|
command: ["sh", "-c", cmd]
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: DMSService
|
target: DMSService
|
||||||
function onOpenUrlRequested(url) {
|
function onOpenUrlRequested(url) {
|
||||||
browserPickerModal.url = url
|
browserPickerModal.url = url;
|
||||||
browserPickerModal.open()
|
browserPickerModal.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAppPickerRequested(data) {
|
function onAppPickerRequested(data) {
|
||||||
console.log("DMSShell: App picker requested with data:", JSON.stringify(data))
|
console.log("DMSShell: App picker requested with data:", JSON.stringify(data));
|
||||||
|
|
||||||
if (!data || !data.target) {
|
if (!data || !data.target) {
|
||||||
console.warn("DMSShell: Invalid app picker request data")
|
console.warn("DMSShell: Invalid app picker request data");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
filePickerModal.targetData = data.target
|
filePickerModal.targetData = data.target;
|
||||||
filePickerModal.targetDataLabel = data.requestType || "file"
|
filePickerModal.targetDataLabel = data.requestType || "file";
|
||||||
|
|
||||||
if (data.categories && data.categories.length > 0) {
|
if (data.categories && data.categories.length > 0) {
|
||||||
filePickerModal.categoryFilter = data.categories
|
filePickerModal.categoryFilter = data.categories;
|
||||||
} else {
|
} else {
|
||||||
filePickerModal.categoryFilter = []
|
filePickerModal.categoryFilter = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
filePickerModal.usageHistoryKey = "filePickerUsageHistory"
|
filePickerModal.usageHistoryKey = "filePickerUsageHistory";
|
||||||
filePickerModal.open()
|
filePickerModal.open();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ FocusScope {
|
|||||||
property bool pathInputHasFocus: false
|
property bool pathInputHasFocus: false
|
||||||
property int actualGridColumns: 5
|
property int actualGridColumns: 5
|
||||||
property bool _initialized: false
|
property bool _initialized: false
|
||||||
|
property bool closeOnEscape: true
|
||||||
|
|
||||||
signal fileSelected(string path)
|
signal fileSelected(string path)
|
||||||
signal closeRequested
|
signal closeRequested
|
||||||
@@ -298,7 +299,7 @@ FocusScope {
|
|||||||
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
|
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
|
||||||
|
|
||||||
function handleKey(event) {
|
function handleKey(event) {
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape && root.closeOnEscape) {
|
||||||
closeRequested();
|
closeRequested();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ FloatingWindow {
|
|||||||
minimumSize: Qt.size(500, 400)
|
minimumSize: Qt.size(500, 400)
|
||||||
implicitWidth: 800
|
implicitWidth: 800
|
||||||
implicitHeight: 600
|
implicitHeight: 600
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.surfaceContainer
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
@@ -59,6 +59,7 @@ FloatingWindow {
|
|||||||
id: content
|
id: content
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: true
|
focus: true
|
||||||
|
closeOnEscape: false
|
||||||
|
|
||||||
browserTitle: fileBrowserModal.browserTitle
|
browserTitle: fileBrowserModal.browserTitle
|
||||||
browserIcon: fileBrowserModal.browserIcon
|
browserIcon: fileBrowserModal.browserIcon
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ DankModal {
|
|||||||
|
|
||||||
modalWidth: 500
|
modalWidth: 500
|
||||||
modalHeight: 700
|
modalHeight: 700
|
||||||
|
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
visible: false
|
visible: false
|
||||||
onBackgroundClicked: hide()
|
onBackgroundClicked: hide()
|
||||||
onOpened: () => {
|
onOpened: () => {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ FloatingWindow {
|
|||||||
title: I18n.tr("Authentication")
|
title: I18n.tr("Authentication")
|
||||||
minimumSize: Qt.size(420, calculatedHeight)
|
minimumSize: Qt.size(420, calculatedHeight)
|
||||||
maximumSize: Qt.size(420, calculatedHeight)
|
maximumSize: Qt.size(420, calculatedHeight)
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.surfaceContainer
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ FloatingWindow {
|
|||||||
minimumSize: Qt.size(650, 400)
|
minimumSize: Qt.size(650, 400)
|
||||||
implicitWidth: 900
|
implicitWidth: 900
|
||||||
implicitHeight: 680
|
implicitHeight: 680
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.surfaceContainer
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
@@ -112,12 +112,6 @@ FloatingWindow {
|
|||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
Keys.onPressed: event => {
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
hide();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case Qt.Key_1:
|
case Qt.Key_1:
|
||||||
currentTab = 0;
|
currentTab = 0;
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ FloatingWindow {
|
|||||||
minimumSize: Qt.size(500, 400)
|
minimumSize: Qt.size(500, 400)
|
||||||
implicitWidth: 800
|
implicitWidth: 800
|
||||||
implicitHeight: 940
|
implicitHeight: 940
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.surfaceContainer
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
onIsCompactModeChanged: {
|
onIsCompactModeChanged: {
|
||||||
@@ -139,11 +139,6 @@ FloatingWindow {
|
|||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
Keys.onPressed: event => {
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
hide();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === Qt.Key_Down || (event.key === Qt.Key_Tab && !event.modifiers)) {
|
if (event.key === Qt.Key_Down || (event.key === Qt.Key_Tab && !event.modifiers)) {
|
||||||
sidebar.navigateNext();
|
sidebar.navigateNext();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ FloatingWindow {
|
|||||||
title: isVpnPrompt ? I18n.tr("VPN Password") : I18n.tr("Wi-Fi Password")
|
title: isVpnPrompt ? I18n.tr("VPN Password") : I18n.tr("Wi-Fi Password")
|
||||||
minimumSize: Qt.size(420, calculatedHeight)
|
minimumSize: Qt.size(420, calculatedHeight)
|
||||||
maximumSize: Qt.size(420, calculatedHeight)
|
maximumSize: Qt.size(420, calculatedHeight)
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.surfaceContainer
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ DankPopout {
|
|||||||
|
|
||||||
property alias searchField: searchField
|
property alias searchField: searchField
|
||||||
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
smooth: true
|
smooth: true
|
||||||
|
|||||||
@@ -113,11 +113,7 @@ DankPopout {
|
|||||||
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
|
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
|
||||||
property alias bluetoothCodecSelector: bluetoothCodecSelector
|
property alias bluetoothCodecSelector: bluetoothCodecSelector
|
||||||
|
|
||||||
color: {
|
color: "transparent"
|
||||||
const transparency = Theme.popupTransparency;
|
|
||||||
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1);
|
|
||||||
return Qt.rgba(surface.r, surface.g, surface.b, transparency);
|
|
||||||
}
|
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ Row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DankSlider {
|
DankSlider {
|
||||||
|
id: volumeSlider
|
||||||
|
|
||||||
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -63,7 +65,6 @@ Row {
|
|||||||
enabled: defaultSink !== null
|
enabled: defaultSink !== null
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 100
|
maximum: 100
|
||||||
value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0
|
|
||||||
showValue: true
|
showValue: true
|
||||||
unit: "%"
|
unit: "%"
|
||||||
valueOverride: actualVolumePercent
|
valueOverride: actualVolumePercent
|
||||||
@@ -81,4 +82,11 @@ Row {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
target: volumeSlider
|
||||||
|
property: "value"
|
||||||
|
value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0
|
||||||
|
when: !volumeSlider.isDragging
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Services.UPower
|
import Quickshell.Services.UPower
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -32,7 +29,6 @@ DankPopout {
|
|||||||
if (PowerProfiles.profile !== profile) {
|
if (PowerProfiles.profile !== profile) {
|
||||||
ToastService.showError("Failed to set power profile");
|
ToastService.showError("Failed to set power profile");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
popupWidth: 400
|
popupWidth: 400
|
||||||
@@ -48,7 +44,7 @@ DankPopout {
|
|||||||
id: batteryContent
|
id: batteryContent
|
||||||
|
|
||||||
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
|
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.color: Theme.outlineMedium
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: 0
|
||||||
@@ -59,7 +55,6 @@ DankPopout {
|
|||||||
if (root.shouldBeVisible) {
|
if (root.shouldBeVisible) {
|
||||||
forceActiveFocus();
|
forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Keys.onPressed: function (event) {
|
Keys.onPressed: function (event) {
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
@@ -75,7 +70,6 @@ DankPopout {
|
|||||||
batteryContent.forceActiveFocus();
|
batteryContent.forceActiveFocus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
target: root
|
target: root
|
||||||
@@ -246,7 +240,8 @@ DankPopout {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
if (!BatteryService.batteryAvailable) return "Power profile management available"
|
if (!BatteryService.batteryAvailable)
|
||||||
|
return "Power profile management available";
|
||||||
const time = BatteryService.formatTimeRemaining();
|
const time = BatteryService.formatTimeRemaining();
|
||||||
if (time !== "Unknown") {
|
if (time !== "Unknown") {
|
||||||
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
|
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
|
||||||
@@ -470,14 +465,14 @@ DankPopout {
|
|||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
|
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
|
||||||
return "N/A"
|
return "N/A";
|
||||||
return `${Math.round(modelData.healthPercentage)}%`
|
return `${Math.round(modelData.healthPercentage)}%`;
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: {
|
color: {
|
||||||
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
|
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
|
||||||
return Theme.surfaceText
|
return Theme.surfaceText;
|
||||||
return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText
|
return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText;
|
||||||
}
|
}
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
@@ -526,10 +521,7 @@ DankPopout {
|
|||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: modelData.state === UPowerDeviceState.Charging
|
text: modelData.state === UPowerDeviceState.Charging ? I18n.tr("To Full") : modelData.state === UPowerDeviceState.Discharging ? I18n.tr("Left") : ""
|
||||||
? I18n.tr("To Full")
|
|
||||||
: modelData.state === UPowerDeviceState.Discharging
|
|
||||||
? I18n.tr("Left") : ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceTextMedium
|
color: Theme.surfaceTextMedium
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
@@ -538,17 +530,14 @@ DankPopout {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
const time = modelData.state === UPowerDeviceState.Charging
|
const time = modelData.state === UPowerDeviceState.Charging ? modelData.timeToFull : modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0 ? (3600 * modelData.energy) / BatteryService.changeRate : 0;
|
||||||
? modelData.timeToFull
|
|
||||||
: modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0
|
|
||||||
? (3600 * modelData.energy) / BatteryService.changeRate : 0
|
|
||||||
|
|
||||||
if (!time || time <= 0 || time > 86400)
|
if (!time || time <= 0 || time > 86400)
|
||||||
return "N/A"
|
return "N/A";
|
||||||
|
|
||||||
const hours = Math.floor(time / 3600)
|
const hours = Math.floor(time / 3600);
|
||||||
const minutes = Math.floor((time % 3600) / 60)
|
const minutes = Math.floor((time % 3600) / 60);
|
||||||
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
|
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
@@ -566,8 +555,9 @@ DankPopout {
|
|||||||
DankButtonGroup {
|
DankButtonGroup {
|
||||||
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
|
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
|
||||||
property int currentProfileIndex: {
|
property int currentProfileIndex: {
|
||||||
if (typeof PowerProfiles === "undefined") return 1
|
if (typeof PowerProfiles === "undefined")
|
||||||
return profileModel.findIndex(profile => root.isActiveProfile(profile))
|
return 1;
|
||||||
|
return profileModel.findIndex(profile => root.isActiveProfile(profile));
|
||||||
}
|
}
|
||||||
|
|
||||||
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
|
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
|
||||||
@@ -575,8 +565,9 @@ DankPopout {
|
|||||||
selectionMode: "single"
|
selectionMode: "single"
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
onSelectionChanged: (index, selected) => {
|
onSelectionChanged: (index, selected) => {
|
||||||
if (!selected) return
|
if (!selected)
|
||||||
root.setProfile(profileModel[index])
|
return;
|
||||||
|
root.setProfile(profileModel[index]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,5 +625,4 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -15,31 +11,31 @@ DankPopout {
|
|||||||
property var triggerScreen: null
|
property var triggerScreen: null
|
||||||
|
|
||||||
function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) {
|
function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) {
|
||||||
triggerX = x
|
triggerX = x;
|
||||||
triggerY = y
|
triggerY = y;
|
||||||
triggerWidth = width
|
triggerWidth = width;
|
||||||
triggerSection = section
|
triggerSection = section;
|
||||||
root.screen = screen
|
root.screen = screen;
|
||||||
|
|
||||||
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4)
|
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4);
|
||||||
storedBarSpacing = barSpacing !== undefined ? barSpacing : 4
|
storedBarSpacing = barSpacing !== undefined ? barSpacing : 4;
|
||||||
storedBarConfig = barConfig
|
storedBarConfig = barConfig;
|
||||||
|
|
||||||
const pos = barPosition !== undefined ? barPosition : 0
|
const pos = barPosition !== undefined ? barPosition : 0;
|
||||||
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0
|
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0;
|
||||||
|
|
||||||
setBarContext(pos, bottomGap)
|
setBarContext(pos, bottomGap);
|
||||||
|
|
||||||
updateOutputState()
|
updateOutputState();
|
||||||
}
|
}
|
||||||
|
|
||||||
onScreenChanged: updateOutputState()
|
onScreenChanged: updateOutputState()
|
||||||
|
|
||||||
function updateOutputState() {
|
function updateOutputState() {
|
||||||
if (screen && DwlService.dwlAvailable) {
|
if (screen && DwlService.dwlAvailable) {
|
||||||
outputState = DwlService.getOutputState(screen.name)
|
outputState = DwlService.getOutputState(screen.name);
|
||||||
} else {
|
} else {
|
||||||
outputState = null
|
outputState = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,28 +71,28 @@ DankPopout {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function getLayoutName(symbol) {
|
function getLayoutName(symbol) {
|
||||||
return layoutNames[symbol] || symbol
|
return layoutNames[symbol] || symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLayoutIcon(symbol) {
|
function getLayoutIcon(symbol) {
|
||||||
return layoutIcons[symbol] || "view_quilt"
|
return layoutIcons[symbol] || "view_quilt";
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: DwlService
|
target: DwlService
|
||||||
function onStateChanged() {
|
function onStateChanged() {
|
||||||
updateOutputState()
|
updateOutputState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onShouldBeVisibleChanged: {
|
onShouldBeVisibleChanged: {
|
||||||
if (shouldBeVisible) {
|
if (shouldBeVisible) {
|
||||||
updateOutputState()
|
updateOutputState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
updateOutputState()
|
updateOutputState();
|
||||||
}
|
}
|
||||||
|
|
||||||
popupWidth: 300
|
popupWidth: 300
|
||||||
@@ -111,7 +107,7 @@ DankPopout {
|
|||||||
id: layoutContent
|
id: layoutContent
|
||||||
|
|
||||||
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
|
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.color: Theme.outlineMedium
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: 0
|
||||||
@@ -121,14 +117,14 @@ DankPopout {
|
|||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (root.shouldBeVisible) {
|
if (root.shouldBeVisible) {
|
||||||
forceActiveFocus()
|
forceActiveFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
Keys.onPressed: event => {
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
root.close()
|
root.close();
|
||||||
event.accepted = true
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +133,8 @@ DankPopout {
|
|||||||
function onShouldBeVisibleChanged() {
|
function onShouldBeVisibleChanged() {
|
||||||
if (root.shouldBeVisible) {
|
if (root.shouldBeVisible) {
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
layoutContent.forceActiveFocus()
|
layoutContent.forceActiveFocus();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,7 +208,7 @@ DankPopout {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onPressed: {
|
onPressed: {
|
||||||
root.close()
|
root.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,14 +278,14 @@ DankPopout {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onPressed: {
|
onPressed: {
|
||||||
if (!root.triggerScreen) {
|
if (!root.triggerScreen) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (!DwlService.dwlAvailable) {
|
if (!DwlService.dwlAvailable) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DwlService.setLayout(root.triggerScreen.name, index)
|
DwlService.setLayout(root.triggerScreen.name, index);
|
||||||
root.close()
|
root.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ DankPopout {
|
|||||||
id: content
|
id: content
|
||||||
|
|
||||||
implicitHeight: contentColumn.height + Theme.spacingL * 2
|
implicitHeight: contentColumn.height + Theme.spacingL * 2
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.color: Theme.outlineMedium
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ DankPopout {
|
|||||||
id: mainContainer
|
id: mainContainer
|
||||||
|
|
||||||
implicitHeight: contentColumn.height + Theme.spacingM * 2
|
implicitHeight: contentColumn.height + Theme.spacingM * 2
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
@@ -358,6 +358,8 @@ DankPopout {
|
|||||||
popoutWidth: root.alignedWidth
|
popoutWidth: root.alignedWidth
|
||||||
popoutHeight: root.alignedHeight
|
popoutHeight: root.alignedHeight
|
||||||
contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS
|
contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS
|
||||||
|
section: root.triggerSection
|
||||||
|
barPosition: root.effectiveBarPosition
|
||||||
Component.onCompleted: root.__mediaTabRef = this
|
Component.onCompleted: root.__mediaTabRef = this
|
||||||
onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => {
|
onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => {
|
||||||
root.__showVolumeDropdown(pos, rightEdge, player, players);
|
root.__showVolumeDropdown(pos, rightEdge, player, players);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import QtQuick.Effects
|
|||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -19,6 +18,8 @@ Item {
|
|||||||
property real popoutWidth: 0
|
property real popoutWidth: 0
|
||||||
property real popoutHeight: 0
|
property real popoutHeight: 0
|
||||||
property real contentOffsetY: 0
|
property real contentOffsetY: 0
|
||||||
|
property string section: ""
|
||||||
|
property int barPosition: SettingsData.Position.Top
|
||||||
|
|
||||||
signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
|
signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
|
||||||
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
|
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
|
||||||
@@ -40,7 +41,13 @@ Item {
|
|||||||
id: sharedTooltip
|
id: sharedTooltip
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool isRightEdge: (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Right
|
readonly property bool isRightEdge: {
|
||||||
|
if (barPosition === SettingsData.Position.Right)
|
||||||
|
return true;
|
||||||
|
if (barPosition === SettingsData.Position.Left)
|
||||||
|
return false;
|
||||||
|
return section === "right";
|
||||||
|
}
|
||||||
readonly property bool __isChromeBrowser: {
|
readonly property bool __isChromeBrowser: {
|
||||||
if (!activePlayer?.identity)
|
if (!activePlayer?.identity)
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import QtQuick.Controls
|
|||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Services.Greetd
|
import Quickshell.Services.Greetd
|
||||||
import Quickshell.Services.Pam
|
import Quickshell.Services.Pam
|
||||||
@@ -35,19 +36,40 @@ Item {
|
|||||||
randomFact = Facts.getRandomFact()
|
randomFact = Facts.getRandomFact()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property bool weatherInitialized: false
|
||||||
|
|
||||||
|
function initWeatherService() {
|
||||||
|
if (weatherInitialized)
|
||||||
|
return
|
||||||
|
if (!GreetdSettings.settingsLoaded)
|
||||||
|
return
|
||||||
|
if (!GreetdSettings.weatherEnabled)
|
||||||
|
return
|
||||||
|
|
||||||
|
weatherInitialized = true
|
||||||
|
WeatherService.addRef()
|
||||||
|
WeatherService.forceRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: GreetdSettings
|
||||||
|
function onSettingsLoadedChanged() {
|
||||||
|
if (GreetdSettings.settingsLoaded)
|
||||||
|
initWeatherService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
pickRandomFact()
|
pickRandomFact()
|
||||||
WeatherService.addRef()
|
initWeatherService()
|
||||||
|
|
||||||
if (isPrimaryScreen) {
|
if (isPrimaryScreen) {
|
||||||
sessionListProc.running = true
|
sessionListProc.running = true
|
||||||
applyLastSuccessfulUser()
|
applyLastSuccessfulUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CompositorService.isHyprland) {
|
if (CompositorService.isHyprland)
|
||||||
updateHyprlandLayout()
|
updateHyprlandLayout()
|
||||||
hyprlandLayoutUpdateTimer.start()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyLastSuccessfulUser() {
|
function applyLastSuccessfulUser() {
|
||||||
@@ -61,10 +83,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
Component.onDestruction: {
|
||||||
|
if (weatherInitialized)
|
||||||
WeatherService.removeRef()
|
WeatherService.removeRef()
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
hyprlandLayoutUpdateTimer.stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateHyprlandLayout() {
|
function updateHyprlandLayout() {
|
||||||
@@ -106,14 +126,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Connections {
|
||||||
id: hyprlandLayoutUpdateTimer
|
target: CompositorService.isHyprland ? Hyprland : null
|
||||||
interval: 1000
|
enabled: CompositorService.isHyprland
|
||||||
running: false
|
|
||||||
repeat: true
|
|
||||||
onTriggered: updateHyprlandLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
function onRawEvent(event) {
|
||||||
|
if (event.name === "activelayout")
|
||||||
|
updateHyprlandLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: GreetdMemory
|
target: GreetdMemory
|
||||||
@@ -750,13 +771,13 @@ Item {
|
|||||||
visible: {
|
visible: {
|
||||||
const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) ||
|
const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) ||
|
||||||
(CompositorService.isHyprland && hyprlandLayoutCount > 1)
|
(CompositorService.isHyprland && hyprlandLayoutCount > 1)
|
||||||
return keyboardVisible && WeatherService.weather.available
|
return keyboardVisible && GreetdSettings.weatherEnabled && WeatherService.weather.available
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
spacing: 6
|
spacing: 6
|
||||||
visible: WeatherService.weather.available
|
visible: GreetdSettings.weatherEnabled && WeatherService.weather.available
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
@@ -780,7 +801,7 @@ Item {
|
|||||||
height: 24
|
height: 24
|
||||||
color: Qt.rgba(255, 255, 255, 0.2)
|
color: Qt.rgba(255, 255, 255, 0.2)
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: WeatherService.weather.available && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio) || BatteryService.batteryAvailable)
|
visible: GreetdSettings.weatherEnabled && WeatherService.weather.available && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio) || BatteryService.batteryAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
|||||||
@@ -1110,7 +1110,7 @@ Item {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
name: "vpn_lock"
|
name: "vpn_lock"
|
||||||
size: Theme.iconSize - 2
|
size: Theme.iconSize - 2
|
||||||
color: NetworkService.vpnConnected ? Theme.primary : Qt.rgba(255, 255, 255, 0.5)
|
color: "white"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: NetworkService.vpnAvailable && NetworkService.vpnConnected
|
visible: NetworkService.vpnAvailable && NetworkService.vpnConnected
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ DankPopout {
|
|||||||
return Math.max(300, Math.min(baseHeight, maxHeight));
|
return Math.max(300, Math.min(baseHeight, maxHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ DankPopout {
|
|||||||
id: popoutContainer
|
id: popoutContainer
|
||||||
|
|
||||||
implicitHeight: popoutColumn.implicitHeight + Theme.spacingL * 2
|
implicitHeight: popoutColumn.implicitHeight + Theme.spacingL * 2
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.width: 0
|
border.width: 0
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
@@ -34,14 +33,14 @@ DankPopout {
|
|||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (root.shouldBeVisible) {
|
if (root.shouldBeVisible) {
|
||||||
forceActiveFocus()
|
forceActiveFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
Keys.onPressed: event => {
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
root.close()
|
root.close();
|
||||||
event.accepted = true
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +49,8 @@ DankPopout {
|
|||||||
function onShouldBeVisibleChanged() {
|
function onShouldBeVisibleChanged() {
|
||||||
if (root.shouldBeVisible) {
|
if (root.shouldBeVisible) {
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
popoutContainer.forceActiveFocus()
|
popoutContainer.forceActiveFocus();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,11 +70,11 @@ DankPopout {
|
|||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (item && "closePopout" in item) {
|
if (item && "closePopout" in item) {
|
||||||
item.closePopout = function () {
|
item.closePopout = function () {
|
||||||
root.close()
|
root.close();
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
if (item) {
|
if (item) {
|
||||||
root.contentHeight = Qt.binding(() => item.implicitHeight + Theme.spacingS * 2)
|
root.contentHeight = Qt.binding(() => item.implicitHeight + Theme.spacingS * 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modules.ProcessList
|
import qs.Modules.ProcessList
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -57,7 +51,7 @@ DankPopout {
|
|||||||
id: processListContent
|
id: processListContent
|
||||||
|
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: "transparent"
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
clip: true
|
clip: true
|
||||||
@@ -70,7 +64,7 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
processContextMenu.parent = processListContent;
|
processContextMenu.parent = processListContent;
|
||||||
}
|
}
|
||||||
Keys.onPressed: (event) => {
|
Keys.onPressed: event => {
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
processListPopout.close();
|
processListPopout.close();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
@@ -108,7 +102,6 @@ DankPopout {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: parent.width - Theme.spacingM * 2
|
width: parent.width - Theme.spacingM * 2
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -124,13 +117,8 @@ DankPopout {
|
|||||||
anchors.margins: Theme.spacingS
|
anchors.margins: Theme.spacingS
|
||||||
contextMenu: processContextMenu
|
contextMenu: processContextMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ FloatingWindow {
|
|||||||
minimumSize: Qt.size(450, 400)
|
minimumSize: Qt.size(450, 400)
|
||||||
implicitWidth: 600
|
implicitWidth: 600
|
||||||
implicitHeight: 650
|
implicitHeight: 650
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.surfaceContainer
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
@@ -537,7 +537,7 @@ FloatingWindow {
|
|||||||
title: I18n.tr("Third-Party Plugin Warning")
|
title: I18n.tr("Third-Party Plugin Warning")
|
||||||
implicitWidth: 500
|
implicitWidth: 500
|
||||||
implicitHeight: 350
|
implicitHeight: 350
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.surfaceContainer
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ FloatingWindow {
|
|||||||
minimumSize: Qt.size(400, 350)
|
minimumSize: Qt.size(400, 350)
|
||||||
implicitWidth: 500
|
implicitWidth: 500
|
||||||
implicitHeight: 550
|
implicitHeight: 550
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.surfaceContainer
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ DankPopout {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: updaterPanel
|
id: updaterPanel
|
||||||
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
smooth: true
|
smooth: true
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Modules.Greetd
|
||||||
import "../Common/suncalc.js" as SunCalc
|
import "../Common/suncalc.js" as SunCalc
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
@@ -509,10 +510,15 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateLocation() {
|
function updateLocation() {
|
||||||
if (SettingsData.useAutoLocation) {
|
const useAuto = SessionData.isGreeterMode ? GreetdSettings.useAutoLocation : SettingsData.useAutoLocation;
|
||||||
|
const coords = SessionData.isGreeterMode ? GreetdSettings.weatherCoordinates : SettingsData.weatherCoordinates;
|
||||||
|
const cityName = SessionData.isGreeterMode ? GreetdSettings.weatherLocation : SettingsData.weatherLocation;
|
||||||
|
|
||||||
|
if (useAuto) {
|
||||||
getLocationFromIP();
|
getLocationFromIP();
|
||||||
} else {
|
return;
|
||||||
const coords = SettingsData.weatherCoordinates;
|
}
|
||||||
|
|
||||||
if (coords) {
|
if (coords) {
|
||||||
const parts = coords.split(",");
|
const parts = coords.split(",");
|
||||||
if (parts.length === 2) {
|
if (parts.length === 2) {
|
||||||
@@ -525,12 +531,9 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cityName = SettingsData.weatherLocation;
|
if (cityName)
|
||||||
if (cityName) {
|
|
||||||
getLocationFromCity(cityName);
|
getLocationFromCity(cityName);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLocationFromCoords(lat, lon) {
|
function getLocationFromCoords(lat, lon) {
|
||||||
const url = "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lon + "&format=json&addressdetails=1&accept-language=en";
|
const url = "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lon + "&format=json&addressdetails=1&accept-language=en";
|
||||||
@@ -867,7 +870,7 @@ Singleton {
|
|||||||
Timer {
|
Timer {
|
||||||
id: updateTimer
|
id: updateTimer
|
||||||
interval: nextInterval()
|
interval: nextInterval()
|
||||||
running: root.refCount > 0 && SettingsData.weatherEnabled
|
running: root.refCount > 0 && SettingsData.weatherEnabled && !SessionData.isGreeterMode
|
||||||
repeat: true
|
repeat: true
|
||||||
triggeredOnStart: true
|
triggeredOnStart: true
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
|
|||||||
@@ -46,11 +46,12 @@ Flickable {
|
|||||||
lastWheelTime = currentTime;
|
lastWheelTime = currentTime;
|
||||||
|
|
||||||
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
|
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
|
||||||
const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
|
|
||||||
const deltaY = event.angleDelta.y;
|
const deltaY = event.angleDelta.y;
|
||||||
const isMouseWheel = !hasPixel && hasAngle;
|
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
|
||||||
|
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
|
||||||
|
const isTouchpad = hasPixel;
|
||||||
|
|
||||||
if (isMouseWheel) {
|
if (isTraditionalMouse) {
|
||||||
sessionUsedMouseWheel = true;
|
sessionUsedMouseWheel = true;
|
||||||
momentumTimer.stop();
|
momentumTimer.stop();
|
||||||
flickable.isMomentumActive = false;
|
flickable.isMomentumActive = false;
|
||||||
@@ -58,7 +59,7 @@ Flickable {
|
|||||||
momentum = 0;
|
momentum = 0;
|
||||||
flickable.momentumVelocity = 0;
|
flickable.momentumVelocity = 0;
|
||||||
|
|
||||||
const lines = Math.floor(Math.abs(deltaY) / 120);
|
const lines = Math.round(Math.abs(deltaY) / 120);
|
||||||
const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed;
|
const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed;
|
||||||
let newY = flickable.contentY + scrollAmount;
|
let newY = flickable.contentY + scrollAmount;
|
||||||
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
|
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
|
||||||
@@ -68,17 +69,29 @@ Flickable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
flickable.contentY = newY;
|
flickable.contentY = newY;
|
||||||
} else {
|
} else if (isHighDpiMouse) {
|
||||||
|
sessionUsedMouseWheel = true;
|
||||||
|
momentumTimer.stop();
|
||||||
|
flickable.isMomentumActive = false;
|
||||||
|
velocitySamples = [];
|
||||||
|
momentum = 0;
|
||||||
|
flickable.momentumVelocity = 0;
|
||||||
|
|
||||||
|
let delta = deltaY / 8 * touchpadSpeed;
|
||||||
|
let newY = flickable.contentY - delta;
|
||||||
|
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
|
||||||
|
|
||||||
|
if (flickable.flicking) {
|
||||||
|
flickable.cancelFlick();
|
||||||
|
}
|
||||||
|
|
||||||
|
flickable.contentY = newY;
|
||||||
|
} else if (isTouchpad) {
|
||||||
sessionUsedMouseWheel = false;
|
sessionUsedMouseWheel = false;
|
||||||
momentumTimer.stop();
|
momentumTimer.stop();
|
||||||
flickable.isMomentumActive = false;
|
flickable.isMomentumActive = false;
|
||||||
|
|
||||||
let delta = 0;
|
let delta = event.pixelDelta.y * touchpadSpeed;
|
||||||
if (event.pixelDelta.y !== 0) {
|
|
||||||
delta = event.pixelDelta.y * touchpadSpeed;
|
|
||||||
} else {
|
|
||||||
delta = event.angleDelta.y / 8 * touchpadSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
velocitySamples.push({
|
velocitySamples.push({
|
||||||
"delta": delta,
|
"delta": delta,
|
||||||
@@ -94,7 +107,7 @@ Flickable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
if (timeDelta < 50) {
|
||||||
momentum = momentum * momentumRetention + delta * 0.15;
|
momentum = momentum * momentumRetention + delta * 0.15;
|
||||||
delta += momentum;
|
delta += momentum;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -50,11 +50,12 @@ GridView {
|
|||||||
lastWheelTime = currentTime;
|
lastWheelTime = currentTime;
|
||||||
|
|
||||||
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
|
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
|
||||||
const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
|
|
||||||
const deltaY = event.angleDelta.y;
|
const deltaY = event.angleDelta.y;
|
||||||
const isMouseWheel = !hasPixel && hasAngle;
|
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
|
||||||
|
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
|
||||||
|
const isTouchpad = hasPixel;
|
||||||
|
|
||||||
if (isMouseWheel) {
|
if (isTraditionalMouse) {
|
||||||
sessionUsedMouseWheel = true;
|
sessionUsedMouseWheel = true;
|
||||||
momentumTimer.stop();
|
momentumTimer.stop();
|
||||||
isMomentumActive = false;
|
isMomentumActive = false;
|
||||||
@@ -62,7 +63,7 @@ GridView {
|
|||||||
momentum = 0;
|
momentum = 0;
|
||||||
momentumVelocity = 0;
|
momentumVelocity = 0;
|
||||||
|
|
||||||
const lines = Math.floor(Math.abs(deltaY) / 120);
|
const lines = Math.round(Math.abs(deltaY) / 120);
|
||||||
const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35;
|
const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35;
|
||||||
let newY = contentY + scrollAmount;
|
let newY = contentY + scrollAmount;
|
||||||
newY = Math.max(0, Math.min(contentHeight - height, newY));
|
newY = Math.max(0, Math.min(contentHeight - height, newY));
|
||||||
@@ -72,12 +73,29 @@ GridView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
contentY = newY;
|
contentY = newY;
|
||||||
} else {
|
} else if (isHighDpiMouse) {
|
||||||
|
sessionUsedMouseWheel = true;
|
||||||
|
momentumTimer.stop();
|
||||||
|
isMomentumActive = false;
|
||||||
|
velocitySamples = [];
|
||||||
|
momentum = 0;
|
||||||
|
momentumVelocity = 0;
|
||||||
|
|
||||||
|
let delta = deltaY / 120 * cellHeight * 1.2;
|
||||||
|
let newY = contentY - delta;
|
||||||
|
newY = Math.max(0, Math.min(contentHeight - height, newY));
|
||||||
|
|
||||||
|
if (flicking) {
|
||||||
|
cancelFlick();
|
||||||
|
}
|
||||||
|
|
||||||
|
contentY = newY;
|
||||||
|
} else if (isTouchpad) {
|
||||||
sessionUsedMouseWheel = false;
|
sessionUsedMouseWheel = false;
|
||||||
momentumTimer.stop();
|
momentumTimer.stop();
|
||||||
isMomentumActive = false;
|
isMomentumActive = false;
|
||||||
|
|
||||||
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * touchpadSpeed : event.angleDelta.y / 120 * cellHeight * 1.2;
|
let delta = event.pixelDelta.y * touchpadSpeed;
|
||||||
|
|
||||||
velocitySamples.push({
|
velocitySamples.push({
|
||||||
"delta": delta,
|
"delta": delta,
|
||||||
@@ -93,7 +111,7 @@ GridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
if (timeDelta < 50) {
|
||||||
momentum = momentum * momentumRetention + delta * 0.15;
|
momentum = momentum * momentumRetention + delta * 0.15;
|
||||||
delta += momentum;
|
delta += momentum;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -69,11 +69,12 @@ ListView {
|
|||||||
lastWheelTime = currentTime;
|
lastWheelTime = currentTime;
|
||||||
|
|
||||||
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
|
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
|
||||||
const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
|
|
||||||
const deltaY = event.angleDelta.y;
|
const deltaY = event.angleDelta.y;
|
||||||
const isMouseWheel = !hasPixel && hasAngle;
|
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
|
||||||
|
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
|
||||||
|
const isTouchpad = hasPixel;
|
||||||
|
|
||||||
if (isMouseWheel) {
|
if (isTraditionalMouse) {
|
||||||
sessionUsedMouseWheel = true;
|
sessionUsedMouseWheel = true;
|
||||||
momentumTimer.stop();
|
momentumTimer.stop();
|
||||||
isMomentumActive = false;
|
isMomentumActive = false;
|
||||||
@@ -81,7 +82,7 @@ ListView {
|
|||||||
momentum = 0;
|
momentum = 0;
|
||||||
momentumVelocity = 0;
|
momentumVelocity = 0;
|
||||||
|
|
||||||
const lines = Math.floor(Math.abs(deltaY) / 120);
|
const lines = Math.round(Math.abs(deltaY) / 120);
|
||||||
const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed;
|
const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed;
|
||||||
let newY = listView.contentY + scrollAmount;
|
let newY = listView.contentY + scrollAmount;
|
||||||
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
|
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
|
||||||
@@ -93,17 +94,31 @@ ListView {
|
|||||||
|
|
||||||
listView.contentY = newY;
|
listView.contentY = newY;
|
||||||
savedY = newY;
|
savedY = newY;
|
||||||
} else {
|
} else if (isHighDpiMouse) {
|
||||||
|
sessionUsedMouseWheel = true;
|
||||||
|
momentumTimer.stop();
|
||||||
|
isMomentumActive = false;
|
||||||
|
velocitySamples = [];
|
||||||
|
momentum = 0;
|
||||||
|
momentumVelocity = 0;
|
||||||
|
|
||||||
|
let delta = deltaY / 8 * touchpadSpeed;
|
||||||
|
let newY = listView.contentY - delta;
|
||||||
|
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
|
||||||
|
newY = Math.max(listView.originY, Math.min(maxY, newY));
|
||||||
|
|
||||||
|
if (listView.flicking) {
|
||||||
|
listView.cancelFlick();
|
||||||
|
}
|
||||||
|
|
||||||
|
listView.contentY = newY;
|
||||||
|
savedY = newY;
|
||||||
|
} else if (isTouchpad) {
|
||||||
sessionUsedMouseWheel = false;
|
sessionUsedMouseWheel = false;
|
||||||
momentumTimer.stop();
|
momentumTimer.stop();
|
||||||
isMomentumActive = false;
|
isMomentumActive = false;
|
||||||
|
|
||||||
let delta = 0;
|
let delta = event.pixelDelta.y * touchpadSpeed;
|
||||||
if (event.pixelDelta.y !== 0) {
|
|
||||||
delta = event.pixelDelta.y * touchpadSpeed;
|
|
||||||
} else {
|
|
||||||
delta = event.angleDelta.y / 8 * touchpadSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
velocitySamples.push({
|
velocitySamples.push({
|
||||||
"delta": delta,
|
"delta": delta,
|
||||||
@@ -119,7 +134,7 @@ ListView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
if (timeDelta < 50) {
|
||||||
momentum = momentum * 0.92 + delta * 0.15;
|
momentum = momentum * 0.92 + delta * 0.15;
|
||||||
delta += momentum;
|
delta += momentum;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -396,7 +396,6 @@ Item {
|
|||||||
Item {
|
Item {
|
||||||
id: bgShadowLayer
|
id: bgShadowLayer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: contentWrapper.popupSurfaceAlpha >= 0.95
|
|
||||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||||
layer.smooth: false
|
layer.smooth: false
|
||||||
layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
|
layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
|
||||||
@@ -421,6 +420,7 @@ Item {
|
|||||||
DankRectangle {
|
DankRectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user