1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-31 00:42:50 -05:00

Compare commits

...

16 Commits

Author SHA1 Message Date
bbedward
deaac3fdf0 list: approve mouse detection 2025-12-08 14:11:44 -05:00
bbedward
b7062fe40c windows: dont close on esc
fixes #911
2025-12-08 14:02:58 -05:00
bbedward
64d5e99b9d dock: ensure creation after bars
fixes #919
2025-12-08 13:54:44 -05:00
bbedward
f9d8a7d22b greeter: fix weather setting
fixes #921
2025-12-08 13:45:26 -05:00
bbedward
52fcd3ad98 lock: make VPN icon white to be consistent with others
fixes #926
2025-12-08 13:24:53 -05:00
bbedward
9d1e0ee29b fix color picker color space 2025-12-08 12:59:24 -05:00
bbedward
de62f48f50 screenshot: handle transformed displays 2025-12-08 12:45:05 -05:00
bbedward
f47b19274c media: fix position/bar awareness
- shift media control column so it doesnt go off screen
fixes #942
2025-12-08 11:51:40 -05:00
bbedward
bb7f7083b9 meta: transparency fixes
- fixes #949 - transparency not working > 95%
- fixes #947 - dont apply opacity to windows, defer to window-rules
2025-12-08 11:43:29 -05:00
Yuxiang Qiu
cd580090dc evdev: improve capslock detection for no led device (#923)
* evdev: improve capslock detection for no led device

* style: fmt
2025-12-08 11:16:43 -05:00
Marcus Ramberg
ddb74b598d ci: add flake check (#951) 2025-12-08 11:15:35 -05:00
bbedward
29571fc3aa screenshot: use wlr-output-management on DWL for x/y offsets 2025-12-08 10:53:08 -05:00
bbedward
57ee0fb2bd bump: failed fprint tries 2025-12-08 10:02:53 -05:00
osscar
3ef10e73a5 nix: remove leading dot in nativeBuildInputs (#948)
Co-authored-by: osscar <osscar.unheard025@passmail.net>
2025-12-08 15:52:32 +01:00
bbedward
dc40492fc7 cc: fix audio slider binding 2025-12-08 09:45:25 -05:00
bbedward
e606a76a86 screenshot: add screenshot-window support for DWL/MangoWC 2025-12-08 09:39:42 -05:00
43 changed files with 1314 additions and 327 deletions

30
.github/workflows/nix-pr-check.yml vendored Normal file
View 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

View File

@@ -35,7 +35,7 @@ Modes:
full - Capture the focused output
all - Capture all outputs combined
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
Output format (--format):
@@ -91,9 +91,8 @@ If no previous region exists, falls back to interactive selection.`,
var ssWindowCmd = &cobra.Command{
Use: "window",
Short: "Capture the focused window",
Long: `Capture the currently focused window.
Currently only supported on Hyprland.`,
Run: runScreenshotWindow,
Long: `Capture the currently focused window. Supported on Hyprland and DWL.`,
Run: runScreenshotWindow,
}
var ssListCmd = &cobra.Command{
@@ -296,7 +295,14 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
data := buf.Data()
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++ {
srcY := int(float64(y) / scale)
@@ -310,16 +316,17 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
}
si := srcY*buf.Stride + srcX*4
di := (y*dstW + x) * 3
if si+2 < len(data) {
if swapRB {
rgb[di+0] = data[si+2]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+0]
} else {
rgb[di+0] = data[si+0]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+2]
}
if si+3 >= len(data) {
continue
}
if swapRB {
rgb[di+0] = data[si+2]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+0]
} else {
rgb[di+0] = data[si+0]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+2]
}
}
}
@@ -371,7 +378,37 @@ func runScreenshotList(cmd *cobra.Command, args []string) {
}
for _, o := range outputs {
fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n",
o.Name, o.Width, o.Height, o.X, o.Y, o.Scale)
scaleStr := fmt.Sprintf("%.2f", o.FractionalScale)
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)
}
}

View File

@@ -1,6 +1,7 @@
package colorpicker
import (
"fmt"
"math"
"strings"
"sync"
@@ -79,6 +80,10 @@ func (s *SurfaceState) OnScreencopyBuffer(format PixelFormat, width, height, str
s.mu.Lock()
defer s.mu.Unlock()
if stride < width*4 {
return fmt.Errorf("invalid stride %d for width %d", stride, width)
}
if s.screenBuf != nil {
s.screenBuf.Close()
s.screenBuf = nil
@@ -279,10 +284,10 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
drawMagnifierWithInversion(
dst.Data(), dst.Stride, dst.Width, dst.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
}
@@ -390,6 +395,7 @@ func drawMagnifierWithInversion(
cx, cy int,
borderColor Color,
yInverted bool,
format PixelFormat,
) {
if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 {
return
@@ -407,6 +413,14 @@ func drawMagnifierWithInversion(
innerRadius := float64(outerRadius - borderThickness)
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++ {
y := cy + dy
if y < 0 || y >= dstH {
@@ -431,9 +445,9 @@ func drawMagnifierWithInversion(
}
bgColor := Color{
B: dst[dstOff+0],
R: dst[dstOff+rOff],
G: dst[dstOff+1],
R: dst[dstOff+2],
B: dst[dstOff+bOff],
A: dst[dstOff+3],
}
@@ -462,7 +476,7 @@ func drawMagnifierWithInversion(
}
srcOff := sy*srcStride + sx*4
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)
} else {
finalColor = borderColor
@@ -483,24 +497,25 @@ func drawMagnifierWithInversion(
}
srcOff := sy*srcStride + sx*4
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 {
continue
}
}
dst[dstOff+0] = finalColor.B
dst[dstOff+rOff] = finalColor.R
dst[dstOff+1] = finalColor.G
dst[dstOff+2] = finalColor.R
dst[dstOff+bOff] = finalColor.B
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(
data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int,
format PixelFormat,
) {
if width <= 0 || height <= 0 {
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)
if len(text) == 0 {
return
@@ -1033,9 +1048,8 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
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)
var fg Color
if lum > 128 {
@@ -1043,7 +1057,7 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
} else {
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 {
@@ -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 {
return
}
@@ -1073,6 +1087,14 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
x = clamp(x, 0, width)
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++ {
rowOff := yy * stride
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) {
continue
}
data[off+0] = col.B
data[off+rOff] = col.R
data[off+1] = col.G
data[off+2] = col.R
data[off+bOff] = col.B
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 {
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]
if !ok {
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++ {
yy := y + row
if yy < 0 || yy >= height {
@@ -1123,9 +1153,9 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color)
continue
}
data[off+0] = col.B
data[off+rOff] = col.R
data[off+1] = col.G
data[off+2] = col.R
data[off+bOff] = col.B
data[off+3] = 255
}
}

View File

@@ -5,6 +5,11 @@ import (
"fmt"
"os"
"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
@@ -44,27 +49,66 @@ func DetectCompositor() Compositor {
return detectedCompositor
}
if detectDWLProtocol() {
detectedCompositor = CompositorDWL
return detectedCompositor
}
detectedCompositor = CompositorUnknown
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() {
detectedCompositor = CompositorDWL
}
type WindowGeometry struct {
X int32
Y int32
Width int32
Height int32
X int32
Y int32
Width int32
Height int32
Output string
Scale float64
OutputX int32
OutputY int32
OutputTransform int32
}
func GetActiveWindow() (*WindowGeometry, error) {
switch DetectCompositor() {
case CompositorHyprland:
return getHyprlandActiveWindow()
case CompositorDWL:
return getDWLActiveWindow()
default:
return nil, fmt.Errorf("window capture requires Hyprland")
return nil, fmt.Errorf("window capture requires Hyprland or DWL")
}
}
@@ -220,7 +264,112 @@ func SetDWLActiveOutput(name string) {
}
func getDWLFocusedMonitor() string {
return dwlActiveOutput
if 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 {
@@ -236,3 +385,238 @@ func GetFocusedMonitor() string {
}
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")
}

View File

@@ -20,7 +20,13 @@ func BufferToImageWithFormat(buf *ShmBuffer, format uint32) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height))
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++ {
srcOff := y * buf.Stride

View File

@@ -380,19 +380,21 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
return
}
var capturedBuf *ShmBuffer
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))
if err != nil {
log.Error("create screen buffer failed", "err", err)
return
}
if withCursor {
pc.screenBuf = buf
pc.format = e.Format
} else {
pc.screenBufNoCursor = buf
}
capturedBuf = buf
pc.format = e.Format
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
if err != nil {
@@ -421,6 +423,34 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
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()
})

View File

@@ -135,16 +135,120 @@ func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
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 {
return nil, fmt.Errorf("could not find output for window")
}
if DetectCompositor() == CompositorHyprland {
switch DetectCompositor() {
case CompositorHyprland:
return s.captureAndCrop(output, region)
case CompositorDWL:
return s.captureDWLWindow(output, region, geom)
default:
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
}
return s.captureRegionOnOutput(output, region)
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) {
@@ -220,13 +324,18 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
outX, outY := output.x, output.y
scale := float64(output.scale)
if DetectCompositor() == CompositorHyprland {
switch DetectCompositor() {
case CompositorHyprland:
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
outX, outY = hx, hy
}
if s := GetHyprlandMonitorScale(output.name); s > 0 {
scale = s
}
case CompositorDWL:
if info, ok := getOutputInfo(output.name); ok {
outX, outY = info.x, info.y
}
}
if scale <= 0 {
scale = 1.0
@@ -354,13 +463,42 @@ func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult
return nil, fmt.Errorf("capture output: %w", err)
}
return s.processFrame(frame, Region{
result, err := s.processFrame(frame, Region{
X: output.x,
Y: output.y,
Width: output.width,
Height: output.height,
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) {
@@ -441,6 +579,10 @@ func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*Ca
}
func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
if output.transform != TransformNormal {
return s.captureRegionOnTransformedOutput(output, region)
}
scale := output.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name)
@@ -457,6 +599,31 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
w := int32(float64(region.Width) * 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)
if s.config.IncludeCursor {
cursor = 1
@@ -470,6 +637,76 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
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) {
var buf *ShmBuffer
var pool *client.ShmPool
@@ -480,6 +717,10 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
failed := false
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
buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
if err != nil {
@@ -557,6 +798,17 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
}, 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 {
s.outputsMu.Lock()
defer s.outputsMu.Unlock()
@@ -766,16 +1018,32 @@ func ListOutputs() ([]Output, error) {
sc.outputsMu.Lock()
defer sc.outputsMu.Unlock()
compositor := DetectCompositor()
result := make([]Output, 0, len(sc.outputs))
for _, o := range sc.outputs {
result = append(result, Output{
Name: o.name,
X: o.x,
Y: o.y,
Width: o.width,
Height: o.height,
Scale: o.scale,
})
out := Output{
Name: o.name,
X: o.x,
Y: o.y,
Width: o.width,
Height: o.height,
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
}

View File

@@ -11,8 +11,23 @@ const (
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
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
return shm.CreateBuffer(width, height, stride)
}
func InverseTransform(transform int32) int32 {
return shm.InverseTransform(transform)
}

View File

@@ -32,11 +32,13 @@ func (r Region) IsEmpty() bool {
}
type Output struct {
Name string
X, Y int32
Width int32
Height int32
Scale int32
Name string
X, Y int32
Width int32
Height int32
Scale int32
FractionalScale float64
Transform int32
}
type Config struct {

View File

@@ -306,6 +306,15 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
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]
m.updateCapsLockStateDirect(capsLockState)
}

View File

@@ -137,3 +137,96 @@ func (b *Buffer) Clear() {
func (b *Buffer) CopyFrom(src *Buffer) {
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
}
}

View File

@@ -71,7 +71,7 @@
nativeBuildInputs = with pkgs; [
installShellFiles
.makeWrapper
makeWrapper
];
postInstall = ''

View File

@@ -295,7 +295,7 @@ Singleton {
property bool lockScreenShowPowerActions: true
property bool enableFprint: false
property int maxFprintTries: 3
property int maxFprintTries: 15
property bool fprintdAvailable: false
property string lockScreenActiveMonitor: "all"
property string lockScreenInactiveColor: "#000000"

View File

@@ -194,7 +194,7 @@ var SPEC = {
lockScreenShowPowerActions: { def: true },
enableFprint: { def: false },
maxFprintTries: { def: 3 },
maxFprintTries: { def: 15 },
fprintdAvailable: { def: false, persist: false },
lockScreenActiveMonitor: { def: "all" },
lockScreenInactiveColor: { def: "#000000" },

View File

@@ -108,13 +108,14 @@ Item {
id: barRepeaterModel
values: {
const configs = SettingsData.barConfigs;
return configs
.map(c => ({ id: c.id, position: c.position }))
.sort((a, b) => {
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
return aVertical - bVertical;
});
return configs.map(c => ({
id: c.id,
position: c.position
})).sort((a, b) => {
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
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 {
id: dockLoader
active: true
active: root.dockEnabled
asynchronous: false
property var currentPosition: SettingsData.dockPosition
@@ -440,62 +466,71 @@ Item {
title: I18n.tr("Open with...")
function shellEscape(str) {
return "'" + str.replace(/'/g, "'\\''") + "'"
return "'" + str.replace(/'/g, "'\\''") + "'";
}
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 || ""
const escapedPath = shellEscape(filePath)
const escapedUri = shellEscape("file://" + filePath)
let hasField = false
if (cmd.includes("%f")) { cmd = cmd.replace("%f", escapedPath); hasField = true }
else if (cmd.includes("%F")) { cmd = cmd.replace("%F", escapedPath); hasField = true }
else if (cmd.includes("%u")) { cmd = cmd.replace("%u", escapedUri); hasField = true }
else if (cmd.includes("%U")) { cmd = cmd.replace("%U", escapedUri); hasField = true }
cmd = cmd.replace(/%[ikc]/g, "")
if (!hasField) {
cmd += " " + escapedPath
let hasField = false;
if (cmd.includes("%f")) {
cmd = cmd.replace("%f", escapedPath);
hasField = true;
} else if (cmd.includes("%F")) {
cmd = cmd.replace("%F", escapedPath);
hasField = true;
} else if (cmd.includes("%u")) {
cmd = cmd.replace("%u", escapedUri);
hasField = true;
} else if (cmd.includes("%U")) {
cmd = cmd.replace("%U", escapedUri);
hasField = true;
}
console.log("FilePicker: Launching", cmd)
cmd = cmd.replace(/%[ikc]/g, "");
if (!hasField) {
cmd += " " + escapedPath;
}
console.log("FilePicker: Launching", cmd);
Quickshell.execDetached({
command: ["sh", "-c", cmd]
})
});
}
}
Connections {
target: DMSService
function onOpenUrlRequested(url) {
browserPickerModal.url = url
browserPickerModal.open()
browserPickerModal.url = url;
browserPickerModal.open();
}
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) {
console.warn("DMSShell: Invalid app picker request data")
return
console.warn("DMSShell: Invalid app picker request data");
return;
}
filePickerModal.targetData = data.target
filePickerModal.targetDataLabel = data.requestType || "file"
filePickerModal.targetData = data.target;
filePickerModal.targetDataLabel = data.requestType || "file";
if (data.categories && data.categories.length > 0) {
filePickerModal.categoryFilter = data.categories
filePickerModal.categoryFilter = data.categories;
} else {
filePickerModal.categoryFilter = []
filePickerModal.categoryFilter = [];
}
filePickerModal.usageHistoryKey = "filePickerUsageHistory"
filePickerModal.open()
filePickerModal.usageHistoryKey = "filePickerUsageHistory";
filePickerModal.open();
}
}

View File

@@ -46,6 +46,7 @@ FocusScope {
property bool pathInputHasFocus: false
property int actualGridColumns: 5
property bool _initialized: false
property bool closeOnEscape: true
signal fileSelected(string path)
signal closeRequested
@@ -298,7 +299,7 @@ FocusScope {
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
function handleKey(event) {
if (event.key === Qt.Key_Escape) {
if (event.key === Qt.Key_Escape && root.closeOnEscape) {
closeRequested();
event.accepted = true;
return;

View File

@@ -35,7 +35,7 @@ FloatingWindow {
minimumSize: Qt.size(500, 400)
implicitWidth: 800
implicitHeight: 600
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: Theme.surfaceContainer
visible: false
onVisibleChanged: {
@@ -59,6 +59,7 @@ FloatingWindow {
id: content
anchors.fill: parent
focus: true
closeOnEscape: false
browserTitle: fileBrowserModal.browserTitle
browserIcon: fileBrowserModal.browserIcon

View File

@@ -59,6 +59,7 @@ DankModal {
modalWidth: 500
modalHeight: 700
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false
onBackgroundClicked: hide()
onOpened: () => {

View File

@@ -45,7 +45,7 @@ FloatingWindow {
title: I18n.tr("Authentication")
minimumSize: Qt.size(420, calculatedHeight)
maximumSize: Qt.size(420, calculatedHeight)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: Theme.surfaceContainer
visible: false
onVisibleChanged: {

View File

@@ -66,7 +66,7 @@ FloatingWindow {
minimumSize: Qt.size(650, 400)
implicitWidth: 900
implicitHeight: 680
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: Theme.surfaceContainer
visible: false
onVisibleChanged: {
@@ -112,12 +112,6 @@ FloatingWindow {
focus: true
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
return;
}
switch (event.key) {
case Qt.Key_1:
currentTab = 0;

View File

@@ -60,7 +60,7 @@ FloatingWindow {
minimumSize: Qt.size(500, 400)
implicitWidth: 800
implicitHeight: 940
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: Theme.surfaceContainer
visible: false
onIsCompactModeChanged: {
@@ -139,11 +139,6 @@ FloatingWindow {
focus: true
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)) {
sidebar.navigateNext();
event.accepted = true;

View File

@@ -191,7 +191,7 @@ FloatingWindow {
title: isVpnPrompt ? I18n.tr("VPN Password") : I18n.tr("Wi-Fi Password")
minimumSize: Qt.size(420, calculatedHeight)
maximumSize: Qt.size(420, calculatedHeight)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: Theme.surfaceContainer
visible: false
onVisibleChanged: {

View File

@@ -97,7 +97,7 @@ DankPopout {
property alias searchField: searchField
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: "transparent"
radius: Theme.cornerRadius
antialiasing: true
smooth: true

View File

@@ -113,11 +113,7 @@ DankPopout {
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
property alias bluetoothCodecSelector: bluetoothCodecSelector
color: {
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);
}
color: "transparent"
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0

View File

@@ -56,6 +56,8 @@ Row {
}
DankSlider {
id: volumeSlider
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
anchors.verticalCenter: parent.verticalCenter
@@ -63,7 +65,6 @@ Row {
enabled: defaultSink !== null
minimum: 0
maximum: 100
value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0
showValue: true
unit: "%"
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
}
}

View File

@@ -1,9 +1,6 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.UPower
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
@@ -26,13 +23,12 @@ DankPopout {
function setProfile(profile) {
if (typeof PowerProfiles === "undefined") {
ToastService.showError("power-profiles-daemon not available");
return ;
return;
}
PowerProfiles.profile = profile;
if (PowerProfiles.profile !== profile) {
ToastService.showError("Failed to set power profile");
}
}
popupWidth: 400
@@ -48,7 +44,7 @@ DankPopout {
id: batteryContent
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: "transparent"
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0
@@ -59,9 +55,8 @@ DankPopout {
if (root.shouldBeVisible) {
forceActiveFocus();
}
}
Keys.onPressed: function(event) {
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
root.close();
event.accepted = true;
@@ -71,11 +66,10 @@ DankPopout {
Connections {
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(function() {
Qt.callLater(function () {
batteryContent.forceActiveFocus();
});
}
}
target: root
@@ -246,7 +240,8 @@ DankPopout {
StyledText {
text: {
if (!BatteryService.batteryAvailable) return "Power profile management available"
if (!BatteryService.batteryAvailable)
return "Power profile management available";
const time = BatteryService.formatTimeRemaining();
if (time !== "Unknown") {
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
@@ -470,14 +465,14 @@ DankPopout {
StyledText {
text: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return "N/A"
return `${Math.round(modelData.healthPercentage)}%`
return "N/A";
return `${Math.round(modelData.healthPercentage)}%`;
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return Theme.surfaceText
return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText
return Theme.surfaceText;
return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText;
}
font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
@@ -526,10 +521,7 @@ DankPopout {
spacing: 2
StyledText {
text: modelData.state === UPowerDeviceState.Charging
? I18n.tr("To Full")
: modelData.state === UPowerDeviceState.Discharging
? I18n.tr("Left") : ""
text: modelData.state === UPowerDeviceState.Charging ? I18n.tr("To Full") : modelData.state === UPowerDeviceState.Discharging ? I18n.tr("Left") : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
@@ -538,17 +530,14 @@ DankPopout {
StyledText {
text: {
const time = modelData.state === UPowerDeviceState.Charging
? modelData.timeToFull
: modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0
? (3600 * modelData.energy) / BatteryService.changeRate : 0
const time = modelData.state === UPowerDeviceState.Charging ? modelData.timeToFull : modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0 ? (3600 * modelData.energy) / BatteryService.changeRate : 0;
if (!time || time <= 0 || time > 86400)
return "N/A"
return "N/A";
const hours = Math.floor(time / 3600)
const minutes = Math.floor((time % 3600) / 60)
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60);
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
@@ -566,8 +555,9 @@ DankPopout {
DankButtonGroup {
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
property int currentProfileIndex: {
if (typeof PowerProfiles === "undefined") return 1
return profileModel.findIndex(profile => root.isActiveProfile(profile))
if (typeof PowerProfiles === "undefined")
return 1;
return profileModel.findIndex(profile => root.isActiveProfile(profile));
}
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
@@ -575,8 +565,9 @@ DankPopout {
selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => {
if (!selected) return
root.setProfile(profileModel[index])
if (!selected)
return;
root.setProfile(profileModel[index]);
}
}
@@ -634,5 +625,4 @@ DankPopout {
}
}
}
}

View File

@@ -1,8 +1,4 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
@@ -15,31 +11,31 @@ DankPopout {
property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) {
triggerX = x
triggerY = y
triggerWidth = width
triggerSection = section
root.screen = screen
triggerX = x;
triggerY = y;
triggerWidth = width;
triggerSection = section;
root.screen = screen;
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4)
storedBarSpacing = barSpacing !== undefined ? barSpacing : 4
storedBarConfig = barConfig
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4);
storedBarSpacing = barSpacing !== undefined ? barSpacing : 4;
storedBarConfig = barConfig;
const pos = barPosition !== undefined ? barPosition : 0
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0
const pos = barPosition !== undefined ? barPosition : 0;
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0;
setBarContext(pos, bottomGap)
setBarContext(pos, bottomGap);
updateOutputState()
updateOutputState();
}
onScreenChanged: updateOutputState()
function updateOutputState() {
if (screen && DwlService.dwlAvailable) {
outputState = DwlService.getOutputState(screen.name)
outputState = DwlService.getOutputState(screen.name);
} else {
outputState = null
outputState = null;
}
}
@@ -47,56 +43,56 @@ DankPopout {
property string currentLayoutSymbol: outputState?.layoutSymbol || ""
readonly property var layoutNames: ({
"CT": I18n.tr("Center Tiling"),
"G": I18n.tr("Grid"),
"K": I18n.tr("Deck"),
"M": I18n.tr("Monocle"),
"RT": I18n.tr("Right Tiling"),
"S": I18n.tr("Scrolling"),
"T": I18n.tr("Tiling"),
"VG": I18n.tr("Vertical Grid"),
"VK": I18n.tr("Vertical Deck"),
"VS": I18n.tr("Vertical Scrolling"),
"VT": I18n.tr("Vertical Tiling")
})
"CT": I18n.tr("Center Tiling"),
"G": I18n.tr("Grid"),
"K": I18n.tr("Deck"),
"M": I18n.tr("Monocle"),
"RT": I18n.tr("Right Tiling"),
"S": I18n.tr("Scrolling"),
"T": I18n.tr("Tiling"),
"VG": I18n.tr("Vertical Grid"),
"VK": I18n.tr("Vertical Deck"),
"VS": I18n.tr("Vertical Scrolling"),
"VT": I18n.tr("Vertical Tiling")
})
readonly property var layoutIcons: ({
"CT": "view_compact",
"G": "grid_view",
"K": "layers",
"M": "fullscreen",
"RT": "view_sidebar",
"S": "view_carousel",
"T": "view_quilt",
"VG": "grid_on",
"VK": "view_day",
"VS": "scrollable_header",
"VT": "clarify"
})
"CT": "view_compact",
"G": "grid_view",
"K": "layers",
"M": "fullscreen",
"RT": "view_sidebar",
"S": "view_carousel",
"T": "view_quilt",
"VG": "grid_on",
"VK": "view_day",
"VS": "scrollable_header",
"VT": "clarify"
})
function getLayoutName(symbol) {
return layoutNames[symbol] || symbol
return layoutNames[symbol] || symbol;
}
function getLayoutIcon(symbol) {
return layoutIcons[symbol] || "view_quilt"
return layoutIcons[symbol] || "view_quilt";
}
Connections {
target: DwlService
function onStateChanged() {
updateOutputState()
updateOutputState();
}
}
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
updateOutputState()
updateOutputState();
}
}
Component.onCompleted: {
updateOutputState()
updateOutputState();
}
popupWidth: 300
@@ -111,7 +107,7 @@ DankPopout {
id: layoutContent
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: "transparent"
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0
@@ -121,14 +117,14 @@ DankPopout {
Component.onCompleted: {
if (root.shouldBeVisible) {
forceActiveFocus()
forceActiveFocus();
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
root.close()
event.accepted = true
root.close();
event.accepted = true;
}
}
@@ -137,8 +133,8 @@ DankPopout {
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(() => {
layoutContent.forceActiveFocus()
})
layoutContent.forceActiveFocus();
});
}
}
}
@@ -212,7 +208,7 @@ DankPopout {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
root.close()
root.close();
}
}
}
@@ -282,14 +278,14 @@ DankPopout {
cursorShape: Qt.PointingHandCursor
onPressed: {
if (!root.triggerScreen) {
return
return;
}
if (!DwlService.dwlAvailable) {
return
return;
}
DwlService.setLayout(root.triggerScreen.name, index)
root.close()
DwlService.setLayout(root.triggerScreen.name, index);
root.close();
}
}

View File

@@ -36,7 +36,7 @@ DankPopout {
id: content
implicitHeight: contentColumn.height + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: "transparent"
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0

View File

@@ -167,7 +167,7 @@ DankPopout {
id: mainContainer
implicitHeight: contentColumn.height + Theme.spacingM * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: "transparent"
radius: Theme.cornerRadius
focus: true
@@ -358,6 +358,8 @@ DankPopout {
popoutWidth: root.alignedWidth
popoutHeight: root.alignedHeight
contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS
section: root.triggerSection
barPosition: root.effectiveBarPosition
Component.onCompleted: root.__mediaTabRef = this
onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => {
root.__showVolumeDropdown(pos, rightEdge, player, players);

View File

@@ -3,7 +3,6 @@ import QtQuick.Effects
import QtQuick.Layouts
import Quickshell.Services.Mpris
import Quickshell.Io
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
@@ -19,6 +18,8 @@ Item {
property real popoutWidth: 0
property real popoutHeight: 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 showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
@@ -40,7 +41,13 @@ Item {
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: {
if (!activePlayer?.identity)
return false;

View File

@@ -4,6 +4,7 @@ import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Services.Greetd
import Quickshell.Services.Pam
@@ -35,19 +36,40 @@ Item {
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: {
pickRandomFact()
WeatherService.addRef()
initWeatherService()
if (isPrimaryScreen) {
sessionListProc.running = true
applyLastSuccessfulUser()
}
if (CompositorService.isHyprland) {
if (CompositorService.isHyprland)
updateHyprlandLayout()
hyprlandLayoutUpdateTimer.start()
}
}
function applyLastSuccessfulUser() {
@@ -61,10 +83,8 @@ Item {
}
Component.onDestruction: {
WeatherService.removeRef()
if (CompositorService.isHyprland) {
hyprlandLayoutUpdateTimer.stop()
}
if (weatherInitialized)
WeatherService.removeRef()
}
function updateHyprlandLayout() {
@@ -106,14 +126,15 @@ Item {
}
}
Timer {
id: hyprlandLayoutUpdateTimer
interval: 1000
running: false
repeat: true
onTriggered: updateHyprlandLayout()
}
Connections {
target: CompositorService.isHyprland ? Hyprland : null
enabled: CompositorService.isHyprland
function onRawEvent(event) {
if (event.name === "activelayout")
updateHyprlandLayout()
}
}
Connections {
target: GreetdMemory
@@ -750,13 +771,13 @@ Item {
visible: {
const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) ||
(CompositorService.isHyprland && hyprlandLayoutCount > 1)
return keyboardVisible && WeatherService.weather.available
return keyboardVisible && GreetdSettings.weatherEnabled && WeatherService.weather.available
}
}
Row {
spacing: 6
visible: WeatherService.weather.available
visible: GreetdSettings.weatherEnabled && WeatherService.weather.available
anchors.verticalCenter: parent.verticalCenter
DankIcon {
@@ -780,7 +801,7 @@ Item {
height: 24
color: Qt.rgba(255, 255, 255, 0.2)
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 {

View File

@@ -1110,7 +1110,7 @@ Item {
DankIcon {
name: "vpn_lock"
size: Theme.iconSize - 2
color: NetworkService.vpnConnected ? Theme.primary : Qt.rgba(255, 255, 255, 0.5)
color: "white"
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.vpnAvailable && NetworkService.vpnConnected
}

View File

@@ -124,7 +124,7 @@ DankPopout {
return Math.max(300, Math.min(baseHeight, maxHeight));
}
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: "transparent"
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0

View File

@@ -1,5 +1,4 @@
import QtQuick
import Quickshell.Wayland
import qs.Common
import qs.Widgets
@@ -25,7 +24,7 @@ DankPopout {
id: popoutContainer
implicitHeight: popoutColumn.implicitHeight + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: "transparent"
radius: Theme.cornerRadius
border.width: 0
antialiasing: true
@@ -34,14 +33,14 @@ DankPopout {
Component.onCompleted: {
if (root.shouldBeVisible) {
forceActiveFocus()
forceActiveFocus();
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
root.close()
event.accepted = true
root.close();
event.accepted = true;
}
}
@@ -50,8 +49,8 @@ DankPopout {
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(() => {
popoutContainer.forceActiveFocus()
})
popoutContainer.forceActiveFocus();
});
}
}
}
@@ -70,12 +69,12 @@ DankPopout {
onLoaded: {
if (item && "closePopout" in item) {
item.closePopout = function() {
root.close()
}
item.closePopout = function () {
root.close();
};
}
if (item) {
root.contentHeight = Qt.binding(() => item.implicitHeight + Theme.spacingS * 2)
root.contentHeight = Qt.binding(() => item.implicitHeight + Theme.spacingS * 2);
}
}
}

View File

@@ -1,11 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.ProcessList
import qs.Services
@@ -57,7 +51,7 @@ DankPopout {
id: processListContent
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.width: 0
clip: true
@@ -70,7 +64,7 @@ DankPopout {
}
processContextMenu.parent = processListContent;
}
Keys.onPressed: (event) => {
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
processListPopout.close();
event.accepted = true;
@@ -108,7 +102,6 @@ DankPopout {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
}
}
Rectangle {
@@ -124,13 +117,8 @@ DankPopout {
anchors.margins: Theme.spacingS
contextMenu: processContextMenu
}
}
}
}
}
}

View File

@@ -106,7 +106,7 @@ FloatingWindow {
minimumSize: Qt.size(450, 400)
implicitWidth: 600
implicitHeight: 650
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: Theme.surfaceContainer
visible: false
onVisibleChanged: {
@@ -537,7 +537,7 @@ FloatingWindow {
title: I18n.tr("Third-Party Plugin Warning")
implicitWidth: 500
implicitHeight: 350
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: Theme.surfaceContainer
visible: false
FocusScope {

View File

@@ -93,7 +93,7 @@ FloatingWindow {
minimumSize: Qt.size(400, 350)
implicitWidth: 500
implicitHeight: 550
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: Theme.surfaceContainer
visible: false
onVisibleChanged: {

View File

@@ -36,7 +36,7 @@ DankPopout {
Rectangle {
id: updaterPanel
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: "transparent"
radius: Theme.cornerRadius
antialiasing: true
smooth: true

View File

@@ -5,6 +5,7 @@ import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modules.Greetd
import "../Common/suncalc.js" as SunCalc
Singleton {
@@ -509,27 +510,29 @@ Singleton {
}
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();
} else {
const coords = SettingsData.weatherCoordinates;
if (coords) {
const parts = coords.split(",");
if (parts.length === 2) {
const lat = parseFloat(parts[0]);
const lon = parseFloat(parts[1]);
if (!isNaN(lat) && !isNaN(lon)) {
getLocationFromCoords(lat, lon);
return;
}
return;
}
if (coords) {
const parts = coords.split(",");
if (parts.length === 2) {
const lat = parseFloat(parts[0]);
const lon = parseFloat(parts[1]);
if (!isNaN(lat) && !isNaN(lon)) {
getLocationFromCoords(lat, lon);
return;
}
}
const cityName = SettingsData.weatherLocation;
if (cityName) {
getLocationFromCity(cityName);
}
}
if (cityName)
getLocationFromCity(cityName);
}
function getLocationFromCoords(lat, lon) {
@@ -867,7 +870,7 @@ Singleton {
Timer {
id: updateTimer
interval: nextInterval()
running: root.refCount > 0 && SettingsData.weatherEnabled
running: root.refCount > 0 && SettingsData.weatherEnabled && !SessionData.isGreeterMode
repeat: true
triggeredOnStart: true
onTriggered: {

View File

@@ -46,11 +46,12 @@ Flickable {
lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
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;
momentumTimer.stop();
flickable.isMomentumActive = false;
@@ -58,7 +59,7 @@ Flickable {
momentum = 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;
let newY = flickable.contentY + scrollAmount;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
@@ -68,17 +69,29 @@ Flickable {
}
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;
momentumTimer.stop();
flickable.isMomentumActive = false;
let delta = 0;
if (event.pixelDelta.y !== 0) {
delta = event.pixelDelta.y * touchpadSpeed;
} else {
delta = event.angleDelta.y / 8 * touchpadSpeed;
}
let delta = event.pixelDelta.y * touchpadSpeed;
velocitySamples.push({
"delta": delta,
@@ -94,7 +107,7 @@ Flickable {
}
}
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
if (timeDelta < 50) {
momentum = momentum * momentumRetention + delta * 0.15;
delta += momentum;
} else {

View File

@@ -50,11 +50,12 @@ GridView {
lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
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;
momentumTimer.stop();
isMomentumActive = false;
@@ -62,7 +63,7 @@ GridView {
momentum = 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;
let newY = contentY + scrollAmount;
newY = Math.max(0, Math.min(contentHeight - height, newY));
@@ -72,12 +73,29 @@ GridView {
}
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;
momentumTimer.stop();
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({
"delta": delta,
@@ -93,7 +111,7 @@ GridView {
}
}
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
if (timeDelta < 50) {
momentum = momentum * momentumRetention + delta * 0.15;
delta += momentum;
} else {

View File

@@ -69,11 +69,12 @@ ListView {
lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
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;
momentumTimer.stop();
isMomentumActive = false;
@@ -81,7 +82,7 @@ ListView {
momentum = 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;
let newY = listView.contentY + scrollAmount;
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
@@ -93,17 +94,31 @@ ListView {
listView.contentY = 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;
momentumTimer.stop();
isMomentumActive = false;
let delta = 0;
if (event.pixelDelta.y !== 0) {
delta = event.pixelDelta.y * touchpadSpeed;
} else {
delta = event.angleDelta.y / 8 * touchpadSpeed;
}
let delta = event.pixelDelta.y * touchpadSpeed;
velocitySamples.push({
"delta": delta,
@@ -119,7 +134,7 @@ ListView {
}
}
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
if (timeDelta < 50) {
momentum = momentum * 0.92 + delta * 0.15;
delta += momentum;
} else {

View File

@@ -396,7 +396,6 @@ Item {
Item {
id: bgShadowLayer
anchors.fill: parent
visible: contentWrapper.popupSurfaceAlpha >= 0.95
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
layer.smooth: false
layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
@@ -421,6 +420,7 @@ Item {
DankRectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
}
}