mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-22 11:14:01 -04:00
Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84bb4d4753 | ||
|
|
f5acb716b9 | ||
|
|
b138ec1b0f | ||
|
|
5a72cd9616 | ||
|
|
ce1ecfc6bc | ||
|
|
80472c4db8 | ||
|
|
49513047b2 | ||
|
|
045c5c3a89 | ||
|
|
398b360c48 | ||
|
|
a39196e992 | ||
|
|
2ac1fe670c | ||
|
|
f39529d58c | ||
|
|
8874a804b6 | ||
|
|
5baef07048 | ||
|
|
f402731ef2 | ||
|
|
59f6f1fa71 | ||
|
|
c0f775ae49 | ||
|
|
3d6e45f430 | ||
|
|
85bc7604c2 | ||
|
|
3674d0f480 | ||
|
|
5f124de8d4 | ||
|
|
59a03f450d | ||
|
|
3797422832 | ||
|
|
e4793327b8 | ||
|
|
e18931353b | ||
|
|
cf3c76a79f | ||
|
|
7e18a68b82 | ||
|
|
96a21fbe08 | ||
|
|
fbffa410af | ||
|
|
936b42b4e7 | ||
|
|
eb53d8983e | ||
|
|
4e2afb53cc | ||
|
|
a665ba8ca5 | ||
|
|
d57e32c5cb | ||
|
|
07d8e28668 | ||
|
|
ec22e9e594 | ||
|
|
433b13cb0e | ||
|
|
cbe572eea6 | ||
|
|
c1f1da1d26 | ||
|
|
2008c39af3 | ||
|
|
1f22ad4b33 | ||
|
|
8a871aafac |
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
||||
ASSETS_DIR=assets
|
||||
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||
|
||||
.PHONY: all build dev run clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||
.PHONY: all build dev run clean lint-qml install install-bin install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||
|
||||
all: build
|
||||
|
||||
@@ -53,14 +53,6 @@ install-bin:
|
||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||
@echo "Binary installed"
|
||||
|
||||
install-shell:
|
||||
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
|
||||
@test -e $(SHELL_DIR)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; }
|
||||
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||
@cp -rL $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
||||
@echo "Shell files installed"
|
||||
|
||||
install-completions:
|
||||
@echo "Installing shell completions..."
|
||||
@mkdir -p $(DATA_DIR)/bash-completion/completions
|
||||
@@ -98,7 +90,7 @@ install-desktop:
|
||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||
@echo "Desktop entries installed"
|
||||
|
||||
install: install-bin install-shell install-completions install-systemd install-icon install-desktop
|
||||
install: install-bin install-completions install-systemd install-icon install-desktop
|
||||
@echo ""
|
||||
@echo "Installation complete!"
|
||||
@echo ""
|
||||
@@ -111,7 +103,7 @@ uninstall-bin:
|
||||
@echo "Binary removed"
|
||||
|
||||
uninstall-shell:
|
||||
@echo "Removing shell files from $(SHELL_INSTALL_DIR)..."
|
||||
@echo "Removing pre-1.6 shell files from $(SHELL_INSTALL_DIR)..."
|
||||
@rm -rf $(SHELL_INSTALL_DIR)
|
||||
@echo "Shell files removed"
|
||||
|
||||
@@ -159,7 +151,6 @@ help:
|
||||
@echo "Install:"
|
||||
@echo " install - Build and install everything (requires sudo)"
|
||||
@echo " install-bin - Install only the binary"
|
||||
@echo " install-shell - Install only shell files"
|
||||
@echo " install-completions - Install only shell completions"
|
||||
@echo " install-systemd - Install only systemd service"
|
||||
@echo " install-icon - Install only icon"
|
||||
@@ -168,7 +159,7 @@ help:
|
||||
@echo "Uninstall:"
|
||||
@echo " uninstall - Remove everything (requires sudo)"
|
||||
@echo " uninstall-bin - Remove only the binary"
|
||||
@echo " uninstall-shell - Remove only shell files"
|
||||
@echo " uninstall-shell - Remove shell files left by pre-1.6 installs"
|
||||
@echo " uninstall-completions - Remove only shell completions"
|
||||
@echo " uninstall-systemd - Remove only systemd service"
|
||||
@echo " uninstall-icon - Remove only icon"
|
||||
|
||||
@@ -9,7 +9,7 @@ Type=dbus
|
||||
BusName=org.freedesktop.Notifications
|
||||
ExecStart=/usr/bin/dms run --session
|
||||
ExecReload=/bin/kill -USR1 $MAINPID
|
||||
LimitNOFILE=16384
|
||||
LimitNOFILE=16384:infinity
|
||||
Restart=on-failure
|
||||
RestartForceExitStatus=TEMPFAIL
|
||||
SuccessExitStatus=TEMPFAIL
|
||||
|
||||
@@ -11,10 +11,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
colorOutputFmt string
|
||||
colorAutocopy bool
|
||||
colorNotify bool
|
||||
colorLowercase bool
|
||||
colorOutputFmt string
|
||||
colorAutocopy bool
|
||||
colorNotify bool
|
||||
colorLowercase bool
|
||||
colorAllowMulti bool
|
||||
)
|
||||
|
||||
var colorCmd = &cobra.Command{
|
||||
@@ -54,7 +55,8 @@ Examples:
|
||||
dms color pick --rgb # Output as RGB
|
||||
dms color pick --json # Output all formats as JSON
|
||||
dms color pick --hex -l # Output hex in lowercase
|
||||
dms color pick -a # Auto-copy result to clipboard`,
|
||||
dms color pick -a # Auto-copy result to clipboard
|
||||
dms color pick --allow-multiple # Skip the one-picker-at-a-time guard`,
|
||||
Run: runColorPick,
|
||||
}
|
||||
|
||||
@@ -69,6 +71,7 @@ func init() {
|
||||
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
||||
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
||||
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
||||
colorPickCmd.Flags().BoolVar(&colorAllowMulti, "allow-multiple", false, "Open a picker even when another selection overlay is already open")
|
||||
|
||||
colorPickCmd.MarkFlagsMutuallyExclusive("hex", "rgb", "hsl", "hsv", "cmyk", "json")
|
||||
|
||||
@@ -90,11 +93,12 @@ func runColorPick(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
|
||||
config := colorpicker.Config{
|
||||
Format: format,
|
||||
CustomFormat: colorOutputFmt,
|
||||
Lowercase: colorLowercase,
|
||||
Autocopy: colorAutocopy,
|
||||
Notify: colorNotify,
|
||||
Format: format,
|
||||
CustomFormat: colorOutputFmt,
|
||||
Lowercase: colorLowercase,
|
||||
Autocopy: colorAutocopy,
|
||||
Notify: colorNotify,
|
||||
AllowMultiple: colorAllowMulti,
|
||||
}
|
||||
|
||||
// One-shot process holding a few screen-sized buffers: skip GC cycles while it runs.
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -46,7 +47,7 @@ var keybindsSetCmd = &cobra.Command{
|
||||
Short: "Set a keybind override",
|
||||
Long: "Create or update a keybind override for the specified provider",
|
||||
Args: cobra.ExactArgs(3),
|
||||
Run: runKeybindsSet,
|
||||
Run: runKeybindsEdit,
|
||||
}
|
||||
|
||||
var keybindsRemoveCmd = &cobra.Command{
|
||||
@@ -54,7 +55,7 @@ var keybindsRemoveCmd = &cobra.Command{
|
||||
Short: "Remove a keybind",
|
||||
Long: "Remove a keybind. For Hyprland this writes a negative override to dms/binds-user.lua so the key stays unbound across DMS updates. For other providers it deletes the entry from the managed file.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: runKeybindsRemove,
|
||||
Run: runKeybindsEdit,
|
||||
}
|
||||
|
||||
var keybindsResetCmd = &cobra.Command{
|
||||
@@ -62,10 +63,14 @@ var keybindsResetCmd = &cobra.Command{
|
||||
Short: "Reset a keybind override to its DMS default",
|
||||
Long: "Drop the user override for the given key so the DMS default re-applies. For providers without a separate default file (Niri, MangoWC) this is equivalent to remove.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: runKeybindsReset,
|
||||
Run: runKeybindsEdit,
|
||||
}
|
||||
|
||||
func init() {
|
||||
keybindsCmd.PersistentFlags().String("expected-generation", "", "Configuration generation observed when editing Aqueous bindings")
|
||||
for _, command := range []*cobra.Command{keybindsSetCmd, keybindsRemoveCmd, keybindsResetCmd} {
|
||||
command.Flags().Bool("json", false, "Return structured mutation results, including errors")
|
||||
}
|
||||
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
|
||||
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
||||
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
|
||||
@@ -93,6 +98,9 @@ func initializeProviders() {
|
||||
registry := keybinds.GetDefaultRegistry()
|
||||
|
||||
hyprlandProvider := providers.NewHyprlandProvider("")
|
||||
if err := registry.Register(providers.NewAqueousProvider()); err != nil {
|
||||
log.Warnf("Failed to register Aqueous provider: %v", err)
|
||||
}
|
||||
if err := registry.Register(hyprlandProvider); err != nil {
|
||||
log.Warnf("Failed to register Hyprland provider: %v", err)
|
||||
}
|
||||
@@ -206,85 +214,92 @@ func runKeybindsShow(cmd *cobra.Command, args []string) {
|
||||
printCheatSheet(provider)
|
||||
}
|
||||
|
||||
func getWritableProvider(name string) keybinds.WritableProvider {
|
||||
provider, err := keybinds.GetDefaultRegistry().Get(name)
|
||||
func keybindEditFailure(err error) map[string]any {
|
||||
code := "command_failed"
|
||||
var helperError *providers.AqueousError
|
||||
if errors.As(err, &helperError) {
|
||||
code = helperError.Code
|
||||
}
|
||||
return map[string]any{"success": false, "code": code, "message": err.Error()}
|
||||
}
|
||||
|
||||
func runKeybindsEdit(cmd *cobra.Command, args []string) {
|
||||
result, err := editKeybind(cmd, args)
|
||||
if err == nil {
|
||||
_ = json.NewEncoder(cmd.OutOrStdout()).Encode(result)
|
||||
return
|
||||
}
|
||||
structured, _ := cmd.Flags().GetBool("json")
|
||||
if !structured {
|
||||
log.Fatalf("Failed to save keybind: %v", err)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(cmd.OutOrStdout()).Encode(keybindEditFailure(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func editKeybind(cmd *cobra.Command, args []string) (map[string]any, error) {
|
||||
provider, err := keybinds.GetDefaultRegistry().Get(args[0])
|
||||
if err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
key := args[1]
|
||||
if aqueous, ok := provider.(*providers.AqueousProvider); ok {
|
||||
generation, _ := cmd.Flags().GetString("expected-generation")
|
||||
edit := providers.AqueousBindEdit{Generation: generation, Key: key, Remove: cmd.Name() != "set"}
|
||||
if !edit.Remove {
|
||||
for _, flag := range []string{"desc", "allow-when-locked", "cooldown-ms", "no-repeat", "no-inhibiting", "flags"} {
|
||||
if cmd.Flags().Changed(flag) {
|
||||
return nil, fmt.Errorf("aqueous does not support --%s in this provider", flag)
|
||||
}
|
||||
}
|
||||
edit.Action = args[2]
|
||||
edit.OriginalKey, _ = cmd.Flags().GetString("replace-key")
|
||||
}
|
||||
result, err := aqueous.Edit(cmd.Context(), edit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"success": true, "code": "applied", "generation": result.String("generation"), "key": key}, nil
|
||||
}
|
||||
writable, ok := provider.(keybinds.WritableProvider)
|
||||
if !ok {
|
||||
log.Fatalf("Provider %s does not support writing keybinds", name)
|
||||
return nil, fmt.Errorf("provider %s does not support writing keybinds", args[0])
|
||||
}
|
||||
return writable
|
||||
}
|
||||
|
||||
func runKeybindsSet(cmd *cobra.Command, args []string) {
|
||||
providerName, key, action := args[0], args[1], args[2]
|
||||
writable := getWritableProvider(providerName)
|
||||
|
||||
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
|
||||
_ = writable.RemoveBind(replaceKey)
|
||||
}
|
||||
|
||||
options := make(map[string]any)
|
||||
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
|
||||
options["allow-when-locked"] = true
|
||||
}
|
||||
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
|
||||
options["cooldown-ms"] = v
|
||||
}
|
||||
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
||||
options["repeat"] = false
|
||||
}
|
||||
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
|
||||
options["allow-inhibiting"] = false
|
||||
}
|
||||
if v, _ := cmd.Flags().GetString("flags"); v != "" {
|
||||
options["flags"] = v
|
||||
}
|
||||
|
||||
desc, _ := cmd.Flags().GetString("desc")
|
||||
if err := writable.SetBind(key, action, desc, options); err != nil {
|
||||
log.Fatalf("Error setting keybind: %v", err)
|
||||
}
|
||||
|
||||
output, _ := json.MarshalIndent(map[string]any{
|
||||
"success": true,
|
||||
"key": key,
|
||||
"action": action,
|
||||
"path": writable.GetOverridePath(),
|
||||
}, "", " ")
|
||||
fmt.Fprintln(os.Stdout, string(output))
|
||||
}
|
||||
|
||||
func runKeybindsRemove(_ *cobra.Command, args []string) {
|
||||
providerName, key := args[0], args[1]
|
||||
writable := getWritableProvider(providerName)
|
||||
|
||||
if err := writable.RemoveBind(key); err != nil {
|
||||
log.Fatalf("Error removing keybind: %v", err)
|
||||
}
|
||||
|
||||
output, _ := json.MarshalIndent(map[string]any{
|
||||
"success": true,
|
||||
"key": key,
|
||||
"removed": true,
|
||||
}, "", " ")
|
||||
fmt.Fprintln(os.Stdout, string(output))
|
||||
}
|
||||
|
||||
func runKeybindsReset(_ *cobra.Command, args []string) {
|
||||
providerName, key := args[0], args[1]
|
||||
writable := getWritableProvider(providerName)
|
||||
|
||||
if err := writable.ResetBind(key); err != nil {
|
||||
log.Fatalf("Error resetting keybind: %v", err)
|
||||
}
|
||||
|
||||
output, _ := json.MarshalIndent(map[string]any{
|
||||
"success": true,
|
||||
"key": key,
|
||||
"reset": true,
|
||||
}, "", " ")
|
||||
fmt.Fprintln(os.Stdout, string(output))
|
||||
result := map[string]any{"success": true, "key": key}
|
||||
switch cmd.Name() {
|
||||
case "set":
|
||||
options := make(map[string]any)
|
||||
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
|
||||
options["allow-when-locked"] = true
|
||||
}
|
||||
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
|
||||
options["cooldown-ms"] = v
|
||||
}
|
||||
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
||||
options["repeat"] = false
|
||||
}
|
||||
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
|
||||
options["allow-inhibiting"] = false
|
||||
}
|
||||
if v, _ := cmd.Flags().GetString("flags"); v != "" {
|
||||
options["flags"] = v
|
||||
}
|
||||
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
|
||||
_ = writable.RemoveBind(replaceKey)
|
||||
}
|
||||
desc, _ := cmd.Flags().GetString("desc")
|
||||
err = writable.SetBind(key, args[2], desc, options)
|
||||
result["action"] = args[2]
|
||||
result["path"] = writable.GetOverridePath()
|
||||
case "remove":
|
||||
err = writable.RemoveBind(key)
|
||||
result["removed"] = true
|
||||
case "reset":
|
||||
err = writable.ResetBind(key)
|
||||
result["reset"] = true
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported keybind operation: %s", cmd.Name())
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
|
||||
)
|
||||
|
||||
func TestKeybindEditFailure(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
err error
|
||||
code string
|
||||
}{
|
||||
{&providers.AqueousError{Code: "external_change", Message: "changed"}, "external_change"},
|
||||
{fmt.Errorf("helper: %w", &providers.AqueousError{Code: "read_only", Message: "denied"}), "read_only"},
|
||||
{errors.New("transport failed"), "command_failed"},
|
||||
} {
|
||||
result := keybindEditFailure(tc.err)
|
||||
if result["success"] != false || result["code"] != tc.code || result["message"] != tc.err.Error() {
|
||||
t.Fatalf("unexpected failure result: %#v", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
var (
|
||||
ssOutputName string
|
||||
ssSeat string
|
||||
ssCursor string
|
||||
ssFormat string
|
||||
ssQuality int
|
||||
@@ -33,6 +34,7 @@ var (
|
||||
ssStdout bool
|
||||
ssJSON bool
|
||||
ssGeometry bool
|
||||
ssAllowMulti bool
|
||||
)
|
||||
|
||||
type screenshotMetadata struct {
|
||||
@@ -57,7 +59,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/Mango/niri)
|
||||
window - Capture the focused window (Hyprland/Mango/niri/Aqueous)
|
||||
last - Capture the last selected region
|
||||
scroll - Select a region, then scroll to capture a stitched tall image
|
||||
|
||||
@@ -71,7 +73,7 @@ Examples:
|
||||
dms screenshot full # Full screen of focused output
|
||||
dms screenshot all # All screens combined
|
||||
dms screenshot output -o DP-1 # Specific output
|
||||
dms screenshot window # Focused window (Hyprland)
|
||||
dms screenshot window # Focused window
|
||||
dms screenshot last # Last region (pre-selected)
|
||||
dms screenshot --reset # Reset last region pre-selection
|
||||
dms screenshot --no-clipboard # Save file only
|
||||
@@ -80,6 +82,7 @@ Examples:
|
||||
dms screenshot --cursor=on # Include cursor
|
||||
dms screenshot -f jpg -q 85 # JPEG with quality 85
|
||||
dms screenshot --json # Print capture metadata as JSON
|
||||
dms screenshot --allow-multiple # Skip the one-selector-at-a-time guard
|
||||
dms screenshot -g # Print selected region geometry (X,Y WxH) to stdout
|
||||
dms screenshot scroll # Scroll capture, Enter finishes / Esc cancels
|
||||
dms screenshot scroll --interval 250`,
|
||||
@@ -120,7 +123,7 @@ 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. Supported on Hyprland, Mango, and niri.`,
|
||||
Long: `Capture the currently focused window. Supported on Hyprland, Mango, niri, and Aqueous. Aqueous requires a running DMS shell and crops the output including borders and overlapping windows.`,
|
||||
Run: runScreenshotWindow,
|
||||
}
|
||||
|
||||
@@ -163,6 +166,7 @@ var notifyActionCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
|
||||
screenshotCmd.PersistentFlags().StringVar(&ssSeat, "seat", "", "Seat for Aqueous capture (required with multiple seats)")
|
||||
screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)")
|
||||
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
|
||||
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
|
||||
@@ -176,6 +180,7 @@ func init() {
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssJSON, "json", false, "Print capture metadata as JSON")
|
||||
screenshotCmd.PersistentFlags().BoolVarP(&ssGeometry, "geometry", "g", false, "Print selected region geometry (X,Y WxH) to stdout without capturing an image")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssAllowMulti, "allow-multiple", false, "Open a selector even when another one is already open")
|
||||
|
||||
ssScrollCmd.Flags().IntVar(&ssScrollInterval, "interval", 45, "Capture interval in milliseconds (30-1000)")
|
||||
|
||||
@@ -195,6 +200,7 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
||||
config := screenshot.DefaultConfig()
|
||||
config.Mode = mode
|
||||
config.OutputName = ssOutputName
|
||||
config.Seat = ssSeat
|
||||
if strings.EqualFold(ssCursor, "on") {
|
||||
config.Cursor = screenshot.CursorOn
|
||||
}
|
||||
@@ -205,6 +211,7 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
||||
config.Reset = ssReset
|
||||
config.Stdout = ssStdout
|
||||
config.Geometry = ssGeometry
|
||||
config.AllowMultiple = ssAllowMulti
|
||||
|
||||
if ssGeometry {
|
||||
config.Clipboard = false
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
)
|
||||
|
||||
// maxIPCMessageSize allows room for a 50 MB clipboard entry plus JSON/base64
|
||||
@@ -96,10 +97,7 @@ func tryServerRequest(req models.Request) (*models.Response[any], bool) {
|
||||
}
|
||||
|
||||
func getServerSocketPath() string {
|
||||
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
||||
if runtimeDir == "" {
|
||||
runtimeDir = os.TempDir()
|
||||
}
|
||||
runtimeDir := utils.RuntimeDir()
|
||||
|
||||
if sessionSock, ok := shellApp.SessionSocketPath(); ok {
|
||||
return sessionSock
|
||||
|
||||
@@ -2,28 +2,19 @@ package clipboard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap"
|
||||
wlclient "github.com/AvengeMedia/dankgo/wayland/client"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
xkbKeymapFormatV1 = 1
|
||||
|
||||
keyStateReleased = 0
|
||||
keyStatePressed = 1
|
||||
|
||||
// xkb real modifier bit positions are fixed: Shift=0, Lock=1, Control=2
|
||||
shiftModMask = 1 << 0
|
||||
ctrlModMask = 1 << 2
|
||||
|
||||
// evdev fallbacks for a standard pc105 map
|
||||
fallbackCtrlKey = 29 // KEY_LEFTCTRL
|
||||
fallbackShiftKey = 42 // KEY_LEFTSHIFT
|
||||
fallbackVKey = 47 // KEY_V
|
||||
)
|
||||
|
||||
// SendPasteKeystroke emulates a paste shortcut via zwp_virtual_keyboard_v1
|
||||
@@ -50,25 +41,25 @@ func SendPasteKeystroke(withShift bool) (err error) {
|
||||
}
|
||||
defer keyboard.Release()
|
||||
|
||||
var keymap *wlclient.KeyboardKeymapEvent
|
||||
var seatKeymap *wlclient.KeyboardKeymapEvent
|
||||
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
|
||||
if keymap == nil {
|
||||
keymap = &e
|
||||
if seatKeymap == nil {
|
||||
seatKeymap = &e
|
||||
}
|
||||
})
|
||||
|
||||
s.display.Roundtrip()
|
||||
|
||||
if keymap == nil || keymap.Format != xkbKeymapFormatV1 {
|
||||
if seatKeymap == nil || seatKeymap.Format != keymap.FormatXkbV1 {
|
||||
return fmt.Errorf("no xkb keymap from seat")
|
||||
}
|
||||
defer unix.Close(keymap.Fd)
|
||||
defer unix.Close(seatKeymap.Fd)
|
||||
|
||||
keymapText, err := readKeymap(keymap.Fd, keymap.Size)
|
||||
keymapText, err := keymap.Read(seatKeymap.Fd, seatKeymap.Size)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read keymap: %w", err)
|
||||
}
|
||||
keys := resolveKeycodes(keymapText)
|
||||
keys := keymap.Parse(keymapText)
|
||||
|
||||
vk, err := s.virtualKeyboardMgr.CreateVirtualKeyboard(s.seat)
|
||||
if err != nil {
|
||||
@@ -76,15 +67,15 @@ func SendPasteKeystroke(withShift bool) (err error) {
|
||||
}
|
||||
defer vk.Destroy()
|
||||
|
||||
if err := vk.Keymap(xkbKeymapFormatV1, keymap.Fd, keymap.Size); err != nil {
|
||||
if err := vk.Keymap(keymap.FormatXkbV1, seatKeymap.Fd, seatKeymap.Size); err != nil {
|
||||
return fmt.Errorf("set keymap: %w", err)
|
||||
}
|
||||
|
||||
mods := uint32(ctrlModMask)
|
||||
held := []uint32{keys.ctrl}
|
||||
held := []uint32{keys.Keycode("Control_L")}
|
||||
if withShift {
|
||||
mods |= shiftModMask
|
||||
held = append(held, keys.shift)
|
||||
held = append(held, keys.Keycode("Shift_L"))
|
||||
}
|
||||
|
||||
t := uint32(0)
|
||||
@@ -121,93 +112,8 @@ func SendPasteKeystroke(withShift bool) (err error) {
|
||||
if err := vk.Modifiers(mods, 0, 0, 0); err != nil {
|
||||
return fmt.Errorf("set modifiers: %w", err)
|
||||
}
|
||||
if err := press(keys.v); err != nil {
|
||||
if err := press(keys.Keycode("v")); err != nil {
|
||||
return fmt.Errorf("key press: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readKeymap(fd int, size uint32) (string, error) {
|
||||
data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_PRIVATE)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
text := strings.TrimRight(string(data), "\x00")
|
||||
return text, unix.Munmap(data)
|
||||
}
|
||||
|
||||
type pasteKeycodes struct {
|
||||
ctrl uint32
|
||||
shift uint32
|
||||
v uint32
|
||||
}
|
||||
|
||||
var (
|
||||
keycodeDefRe = regexp.MustCompile(`<([A-Za-z0-9+_-]+)>\s*=\s*(\d+)`)
|
||||
keySymbolsRe = regexp.MustCompile(`key\s*<([A-Za-z0-9+_-]+)>\s*\{([^}]*)\}`)
|
||||
groupIndexRe = regexp.MustCompile(`\w+\[\d+\]\s*=`)
|
||||
symbolListRe = regexp.MustCompile(`\[([^\]]*)\]`)
|
||||
)
|
||||
|
||||
// xkbcommon may serialize keysyms as hex escapes instead of names
|
||||
// (e.g. "0x76" for v, "0xffe3" for Control_L).
|
||||
var keysymNames = map[uint32]string{
|
||||
0x76: "v",
|
||||
0xffe3: "Control_L",
|
||||
0xffe1: "Shift_L",
|
||||
}
|
||||
|
||||
func canonicalKeysym(sym string) string {
|
||||
if !strings.HasPrefix(sym, "0x") && !strings.HasPrefix(sym, "0X") {
|
||||
return sym
|
||||
}
|
||||
value, err := strconv.ParseUint(sym[2:], 16, 32)
|
||||
if err != nil {
|
||||
return sym
|
||||
}
|
||||
if name, ok := keysymNames[uint32(value)]; ok {
|
||||
return name
|
||||
}
|
||||
return sym
|
||||
}
|
||||
|
||||
// resolveKeycodes finds the evdev keycodes producing the keysyms we need in
|
||||
// the seat keymap's first group, falling back to pc105 positions.
|
||||
func resolveKeycodes(keymap string) pasteKeycodes {
|
||||
codes := map[string]uint32{}
|
||||
for _, m := range keycodeDefRe.FindAllStringSubmatch(keymap, -1) {
|
||||
if code, err := strconv.Atoi(m[2]); err == nil {
|
||||
codes[m[1]] = uint32(code)
|
||||
}
|
||||
}
|
||||
|
||||
keys := pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}
|
||||
want := map[string]*uint32{
|
||||
"Control_L": &keys.ctrl,
|
||||
"Shift_L": &keys.shift,
|
||||
"v": &keys.v,
|
||||
}
|
||||
|
||||
for _, m := range keySymbolsRe.FindAllStringSubmatch(keymap, -1) {
|
||||
group := symbolListRe.FindStringSubmatch(groupIndexRe.ReplaceAllString(m[2], ""))
|
||||
if group == nil {
|
||||
continue
|
||||
}
|
||||
xkbCode, ok := codes[m[1]]
|
||||
if !ok || xkbCode < 8 {
|
||||
continue
|
||||
}
|
||||
level1 := canonicalKeysym(strings.TrimSpace(strings.Split(group[1], ",")[0]))
|
||||
target, wanted := want[level1]
|
||||
if !wanted {
|
||||
continue
|
||||
}
|
||||
*target = xkbCode - 8
|
||||
delete(want, level1)
|
||||
if len(want) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap"
|
||||
wlclient "github.com/AvengeMedia/dankgo/wayland/client"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
@@ -32,20 +33,20 @@ func TestLiveSeatKeymapResolution(t *testing.T) {
|
||||
}
|
||||
defer keyboard.Release()
|
||||
|
||||
var keymap *wlclient.KeyboardKeymapEvent
|
||||
var seatKeymap *wlclient.KeyboardKeymapEvent
|
||||
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
|
||||
if keymap == nil {
|
||||
keymap = &e
|
||||
if seatKeymap == nil {
|
||||
seatKeymap = &e
|
||||
}
|
||||
})
|
||||
s.display.Roundtrip()
|
||||
|
||||
if keymap == nil {
|
||||
if seatKeymap == nil {
|
||||
t.Fatal("no keymap event")
|
||||
}
|
||||
defer unix.Close(keymap.Fd)
|
||||
defer unix.Close(seatKeymap.Fd)
|
||||
|
||||
text, err := readKeymap(keymap.Fd, keymap.Size)
|
||||
text, err := keymap.Read(seatKeymap.Fd, seatKeymap.Size)
|
||||
if err != nil {
|
||||
t.Fatalf("read keymap: %v", err)
|
||||
}
|
||||
@@ -56,10 +57,7 @@ func TestLiveSeatKeymapResolution(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
keys := resolveKeycodes(text)
|
||||
t.Logf("keymap size=%d resolved ctrl=%d shift=%d v=%d", keymap.Size, keys.ctrl, keys.shift, keys.v)
|
||||
|
||||
if keys.ctrl == fallbackCtrlKey && keys.shift == fallbackShiftKey && keys.v == fallbackVKey {
|
||||
t.Log("all keycodes are fallbacks - parsing may not have matched the live keymap")
|
||||
}
|
||||
keys := keymap.Parse(text)
|
||||
t.Logf("keymap size=%d resolved ctrl=%d shift=%d v=%d", seatKeymap.Size,
|
||||
keys.Keycode("Control_L"), keys.Keycode("Shift_L"), keys.Keycode("v"))
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package clipboard
|
||||
|
||||
import "testing"
|
||||
|
||||
const qwertyKeymap = `xkb_keymap {
|
||||
xkb_keycodes "(unnamed)" {
|
||||
minimum = 8;
|
||||
maximum = 708;
|
||||
<ESC> = 9;
|
||||
<AB04> = 55;
|
||||
<LCTL> = 37;
|
||||
<LFSH> = 50;
|
||||
alias <AL01> = <AC01>;
|
||||
indicator 1 = "Caps Lock";
|
||||
};
|
||||
xkb_types "(unnamed)" {
|
||||
type "ALPHABETIC" {
|
||||
modifiers = Shift+Lock;
|
||||
map[Shift] = Level2;
|
||||
level_name[Level1] = "Base";
|
||||
};
|
||||
};
|
||||
xkb_symbols "(unnamed)" {
|
||||
key <ESC> { [ Escape ] };
|
||||
key <AB04> { type= "ALPHABETIC", [ v, V ] };
|
||||
key <LCTL> { [ Control_L ] };
|
||||
key <LFSH> { [ Shift_L ] };
|
||||
};
|
||||
};`
|
||||
|
||||
const hexKeymap = `xkb_keymap {
|
||||
xkb_keycodes "(unnamed)" {
|
||||
<AB04> = 56;
|
||||
<LCTL> = 38;
|
||||
<LFSH> = 51;
|
||||
};
|
||||
xkb_symbols "(unnamed)" {
|
||||
key <LCTL> { [ 0xffe3 ] };
|
||||
key <LFSH> {
|
||||
type= "PC_ALT_LEVEL2",
|
||||
symbols[1]= [ 0xffe1, 0xfe08 ]
|
||||
};
|
||||
key <AB04> { [ 0x76, 0x56 ] };
|
||||
};
|
||||
};`
|
||||
|
||||
const dvorakKeymap = `xkb_keymap {
|
||||
xkb_keycodes "(unnamed)" {
|
||||
<AB09> = 60;
|
||||
<LCTL> = 37;
|
||||
<LFSH> = 50;
|
||||
};
|
||||
xkb_symbols "(unnamed)" {
|
||||
key <AB09> { [ v, V ] };
|
||||
key <LCTL> { [ Control_L ] };
|
||||
key <LFSH> { [ Shift_L ] };
|
||||
};
|
||||
};`
|
||||
|
||||
func TestResolveKeycodes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keymap string
|
||||
want pasteKeycodes
|
||||
}{
|
||||
{"qwerty", qwertyKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 47}},
|
||||
{"dvorak", dvorakKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 52}},
|
||||
{"hex keysyms", hexKeymap, pasteKeycodes{ctrl: 30, shift: 43, v: 48}},
|
||||
{"empty falls back", "", pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := resolveKeycodes(tt.keymap)
|
||||
if got != tt.want {
|
||||
t.Errorf("resolveKeycodes() = %+v, want %+v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,16 +10,19 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_color_management"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap"
|
||||
"github.com/AvengeMedia/dankgo/wayland/client"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Format OutputFormat
|
||||
CustomFormat string
|
||||
Lowercase bool
|
||||
Autocopy bool
|
||||
Notify bool
|
||||
Format OutputFormat
|
||||
CustomFormat string
|
||||
Lowercase bool
|
||||
Autocopy bool
|
||||
Notify bool
|
||||
AllowMultiple bool
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
@@ -62,6 +65,7 @@ type Picker struct {
|
||||
seat *client.Seat
|
||||
pointer *client.Pointer
|
||||
keyboard *client.Keyboard
|
||||
keymap *keymap.Keymap
|
||||
layerShell *wlr_layer_shell.ZwlrLayerShellV1
|
||||
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
|
||||
viewporter *wp_viewporter.WpViewporter
|
||||
@@ -91,6 +95,14 @@ func New(config Config) *Picker {
|
||||
}
|
||||
|
||||
func (p *Picker) Run() (*Color, error) {
|
||||
if !p.config.AllowMultiple {
|
||||
lock, err := utils.LockSelectionOverlay()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer lock.Release()
|
||||
}
|
||||
|
||||
if err := p.connect(); err != nil {
|
||||
return nil, fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
@@ -728,9 +740,14 @@ func (p *Picker) setupPointerHandlers() {
|
||||
}
|
||||
|
||||
func (p *Picker) setupKeyboardHandlers() {
|
||||
p.keyboard.SetKeymapHandler(func(e client.KeyboardKeymapEvent) {
|
||||
p.keymap = keymap.FromEvent(e)
|
||||
})
|
||||
|
||||
p.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
|
||||
sym := p.keymap.Keysym(e.Key)
|
||||
for _, ls := range p.surfaces {
|
||||
ls.state.OnKey(e.Key, e.State)
|
||||
ls.state.OnKey(sym, e.State)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ func (s *SurfaceState) OnPointerButton(button, state uint32) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SurfaceState) OnKey(key, state uint32) {
|
||||
func (s *SurfaceState) OnKey(sym string, state uint32) {
|
||||
if state != 1 {
|
||||
return
|
||||
}
|
||||
@@ -274,10 +274,10 @@ func (s *SurfaceState) OnKey(key, state uint32) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
switch key {
|
||||
case 1: // KEY_ESC
|
||||
switch sym {
|
||||
case "Escape":
|
||||
s.cancelled = true
|
||||
case 28: // KEY_ENTER
|
||||
case "Return", "KP_Enter":
|
||||
if s.readyForDisplay && s.screenBuf != nil {
|
||||
s.colorPicked = true
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func TestSurfaceState_ConcurrentIsDone(t *testing.T) {
|
||||
for range goroutines / 3 {
|
||||
wg.Go(func() {
|
||||
for range iterations {
|
||||
s.OnKey(1, 1)
|
||||
s.OnKey("Escape", 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||
)
|
||||
|
||||
type AqueousProvider struct{}
|
||||
|
||||
func NewAqueousProvider() *AqueousProvider { return &AqueousProvider{} }
|
||||
func (*AqueousProvider) Name() string { return "aqueous" }
|
||||
|
||||
func aqueousObjects(snapshot aqueousConfig, key string) []aqueousConfig {
|
||||
values, _ := snapshot[key].([]any)
|
||||
result := make([]aqueousConfig, 0, len(values))
|
||||
for _, value := range values {
|
||||
if object, ok := value.(map[string]any); ok {
|
||||
result = append(result, aqueousConfig(object))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func AqueousCheatSheet(snapshot aqueousConfig) (*keybinds.CheatSheet, error) {
|
||||
if err := requireAqueousCapabilities(snapshot, "keybinds"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sheet := &keybinds.CheatSheet{Title: "Aqueous", Provider: "aqueous", Generation: snapshot.String("generation"), ModKey: "Super", DMSBindsIncluded: true, Binds: map[string][]keybinds.Keybind{"Compositor": {}, "Custom": {}}}
|
||||
for _, field := range aqueousObjects(snapshot, "fields") {
|
||||
if field.String("category") != "keybinds" || field.String("type") != "string_list" {
|
||||
continue
|
||||
}
|
||||
values, _ := field["value"].([]any)
|
||||
if len(values) == 0 {
|
||||
values = []any{""}
|
||||
}
|
||||
for _, value := range values {
|
||||
key, ok := value.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid keybinding list")
|
||||
}
|
||||
sheet.Binds["Compositor"] = append(sheet.Binds["Compositor"], keybinds.Keybind{Key: key, Action: field.String("id"), Description: field.String("label"), Source: "dms"})
|
||||
}
|
||||
}
|
||||
for _, binding := range aqueousObjects(snapshot, "custom_keybinds") {
|
||||
sheet.Binds["Custom"] = append(sheet.Binds["Custom"], keybinds.Keybind{Key: binding.String("chord"), Action: "spawn " + binding.String("command"), Description: binding.String("command"), Source: "dms"})
|
||||
}
|
||||
seen := map[string]keybinds.Keybind{}
|
||||
for _, category := range []string{"Compositor", "Custom"} {
|
||||
for i, bind := range sheet.Binds[category] {
|
||||
if bind.Key == "" {
|
||||
continue
|
||||
}
|
||||
if previous, exists := seen[bind.Key]; exists {
|
||||
sheet.Binds[category][i].Conflict = &previous
|
||||
}
|
||||
seen[bind.Key] = bind
|
||||
}
|
||||
}
|
||||
return sheet, nil
|
||||
}
|
||||
|
||||
func (*AqueousProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||
snapshot, err := aqueousHelper(context.Background(), "snapshot", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return AqueousCheatSheet(snapshot)
|
||||
}
|
||||
|
||||
type AqueousBindEdit struct {
|
||||
Generation string
|
||||
OriginalKey string
|
||||
Key string
|
||||
Action string
|
||||
Remove bool
|
||||
}
|
||||
|
||||
func AqueousBindRequest(snapshot aqueousConfig, edit AqueousBindEdit) (aqueousConfig, error) {
|
||||
if edit.Generation == "" || edit.Generation != snapshot.String("generation") {
|
||||
return nil, &AqueousError{Code: "external_change", Message: "reload and reconcile the retained keybind draft"}
|
||||
}
|
||||
if err := requireAqueousCapabilities(snapshot, "keybinds"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if edit.Key == "" {
|
||||
return nil, errors.New("missing key")
|
||||
}
|
||||
original := edit.OriginalKey
|
||||
if original == "" {
|
||||
original = edit.Key
|
||||
}
|
||||
originalCount := 0
|
||||
for _, field := range aqueousObjects(snapshot, "fields") {
|
||||
if field.String("category") != "keybinds" || field.String("type") != "string_list" {
|
||||
continue
|
||||
}
|
||||
values, _ := field["value"].([]any)
|
||||
for _, value := range values {
|
||||
if value == original {
|
||||
originalCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, binding := range aqueousObjects(snapshot, "custom_keybinds") {
|
||||
if binding.String("chord") == original {
|
||||
originalCount++
|
||||
}
|
||||
}
|
||||
if originalCount > 1 {
|
||||
return nil, &AqueousError{Code: "ambiguous_target", Message: "multiple bindings use the original shortcut"}
|
||||
}
|
||||
if originalCount == 0 && (edit.Remove || edit.OriginalKey != "") {
|
||||
return nil, &AqueousError{Code: "target_removed", Message: "the original shortcut no longer exists"}
|
||||
}
|
||||
changes := []any{}
|
||||
customChanges := []any{}
|
||||
foundAction := false
|
||||
for _, field := range aqueousObjects(snapshot, "fields") {
|
||||
if field.String("category") != "keybinds" || field.String("type") != "string_list" {
|
||||
continue
|
||||
}
|
||||
old, _ := field["value"].([]any)
|
||||
values := make([]any, 0, len(old)+1)
|
||||
for _, value := range old {
|
||||
if value == original {
|
||||
continue
|
||||
}
|
||||
if !edit.Remove && value == edit.Key && field.String("id") != edit.Action {
|
||||
return nil, fmt.Errorf("key conflict: %s", edit.Key)
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
if field.String("id") == edit.Action && !edit.Remove {
|
||||
foundAction = true
|
||||
if !slices.Contains(values, any(edit.Key)) {
|
||||
values = append(values, edit.Key)
|
||||
}
|
||||
}
|
||||
if !slices.Equal(old, values) {
|
||||
changes = append(changes, map[string]any{"id": field.String("id"), "value": values})
|
||||
}
|
||||
}
|
||||
for _, binding := range aqueousObjects(snapshot, "custom_keybinds") {
|
||||
if binding.String("chord") == original {
|
||||
customChanges = append(customChanges, map[string]any{"op": "delete", "id": binding.String("id")})
|
||||
continue
|
||||
}
|
||||
if !edit.Remove && binding.String("chord") == edit.Key {
|
||||
return nil, fmt.Errorf("key conflict: %s", edit.Key)
|
||||
}
|
||||
}
|
||||
if !edit.Remove && !foundAction {
|
||||
command, ok := strings.CutPrefix(edit.Action, "spawn ")
|
||||
if !ok || strings.TrimSpace(command) == "" {
|
||||
return nil, errors.New("unknown Aqueous action; custom commands use spawn")
|
||||
}
|
||||
customChanges = append(customChanges, map[string]any{"op": "add", "chord": edit.Key, "command": command})
|
||||
}
|
||||
return aqueousConfig{"expected_generation": edit.Generation, "changes": changes, "custom_keybind_changes": customChanges, "create_user_override": true}, nil
|
||||
}
|
||||
|
||||
func (*AqueousProvider) Edit(ctx context.Context, edit AqueousBindEdit) (aqueousConfig, error) {
|
||||
snapshot, err := aqueousHelper(ctx, "snapshot", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request, err := AqueousBindRequest(snapshot, edit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aqueousHelper(ctx, "apply", request)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
)
|
||||
|
||||
func helperResult(ctx context.Context, verb string, input []byte) (aqueousConfig, error) {
|
||||
var result aqueousConfig
|
||||
args := []string{verb, "--shell", "dms"}
|
||||
if input != nil {
|
||||
args = append(args, "--request", "-")
|
||||
}
|
||||
err := utils.RunJSON(ctx, "aqueous-config", args, input, &result)
|
||||
if err != nil || !result.Bool("ok") {
|
||||
if result.String("code") != "" {
|
||||
return result, &AqueousError{Code: result.String("code"), Message: result.String("message")}
|
||||
}
|
||||
if err != nil {
|
||||
if verb == "apply" {
|
||||
return result, &AqueousError{Code: "uncertain", Message: err.Error()}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
return result, errors.New("aqueous-config rejected the request")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type AqueousError struct {
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *AqueousError) Error() string { return e.Code + ": " + e.Message }
|
||||
|
||||
func requireAqueousCapabilities(result aqueousConfig, required ...string) error {
|
||||
if result.Number("protocol") != 1 {
|
||||
return errors.New("unsupported aqueous-config protocol")
|
||||
}
|
||||
values, ok := result["capabilities"].([]any)
|
||||
if !ok {
|
||||
return errors.New("unsupported aqueous-config: capability discovery required")
|
||||
}
|
||||
for _, capability := range required {
|
||||
if !slices.Contains(values, any(capability)) {
|
||||
return fmt.Errorf("unsupported aqueous-config capability: %s", capability)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func aqueousHelper(ctx context.Context, verb string, request aqueousConfig) (aqueousConfig, error) {
|
||||
if verb == "snapshot" {
|
||||
result, err := helperResult(ctx, verb, nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if err := requireAqueousCapabilities(result, "schema_fields", "shell_dms"); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if result.String("generation") == "" {
|
||||
return result, errors.New("missing configuration generation")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
if verb != "validate" && verb != "apply" {
|
||||
return nil, errors.New("unsupported helper operation")
|
||||
}
|
||||
if request.String("expected_generation") == "" {
|
||||
return nil, errors.New("missing expected_generation; reload configuration")
|
||||
}
|
||||
version, err := helperResult(ctx, "version", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
required := []string{"keybinds", "validate", "stdin_requests", "generation_check", "atomic_file_replace", "shell_dms"}
|
||||
if err := requireAqueousCapabilities(version, required...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request["protocol"] = 1
|
||||
if _, exists := request["backup_dir"]; !exists {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request["backup_dir"] = filepath.Join(configDir, "DankMaterialShell", "aqueous-backups")
|
||||
}
|
||||
input, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
validated, err := helperResult(ctx, "validate", input)
|
||||
if err != nil || verb == "validate" {
|
||||
return validated, err
|
||||
}
|
||||
return helperResult(ctx, "apply", input)
|
||||
}
|
||||
|
||||
type aqueousConfig map[string]any
|
||||
|
||||
func (e aqueousConfig) String(key string) string { value, _ := e[key].(string); return value }
|
||||
func (e aqueousConfig) Bool(key string) bool { value, _ := e[key].(bool); return value }
|
||||
func (e aqueousConfig) Number(key string) float64 {
|
||||
value, _ := e[key].(json.Number)
|
||||
number, _ := value.Float64()
|
||||
return number
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
)
|
||||
|
||||
func aqueousFixture(t *testing.T) aqueousConfig {
|
||||
t.Helper()
|
||||
var snapshot aqueousConfig
|
||||
err := utils.DecodeJSON([]byte(`{"ok":true,"protocol":1,"generation":"abc","capabilities":["keybinds"],"fields":[{"id":"spawn_terminal","category":"keybinds","type":"string_list","label":"Terminal","value":["Super+Return","Super+T"]},{"id":"close","category":"keybinds","type":"string_list","label":"Close","value":[]}],"custom_keybinds":[{"id":"custom:12","chord":"Super+R","command":"printf 'hello; $world'"}]}`), &snapshot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func TestAqueousCheatsheetIncludesUnboundAndCustom(t *testing.T) {
|
||||
sheet, err := AqueousCheatSheet(aqueousFixture(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sheet.Generation != "abc" || len(sheet.Binds["Compositor"]) != 3 {
|
||||
t.Fatal("lost generation or multiple bindings")
|
||||
}
|
||||
if sheet.Binds["Compositor"][2].Key != "" || sheet.Binds["Compositor"][2].Action != "close" {
|
||||
t.Fatal("unbound action lost")
|
||||
}
|
||||
if sheet.Binds["Custom"][0].Action != "spawn printf 'hello; $world'" {
|
||||
t.Fatal("custom command modified")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAqueousBindingReplacementIsOneRequest(t *testing.T) {
|
||||
snapshot := aqueousFixture(t)
|
||||
request, err := AqueousBindRequest(snapshot, AqueousBindEdit{Generation: "abc", OriginalKey: "Super+Return", Key: "Super+Enter", Action: "spawn_terminal"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, _ := json.Marshal(request["changes"])
|
||||
if string(data) != `[{"id":"spawn_terminal","value":["Super+T","Super+Enter"]}]` {
|
||||
t.Fatalf("wrong atomic replacement: %s", data)
|
||||
}
|
||||
if request["expected_generation"] != "abc" {
|
||||
t.Fatal("generation replaced")
|
||||
}
|
||||
if !reflect.DeepEqual(snapshot, aqueousFixture(t)) {
|
||||
t.Fatal("draft mutated snapshot")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAqueousBindingConflictAndRemove(t *testing.T) {
|
||||
for _, edit := range []AqueousBindEdit{
|
||||
{Generation: "stale", Key: "Super+T", Remove: true},
|
||||
{Generation: "abc", OriginalKey: "Super+Return", Key: "Super+R", Action: "close"},
|
||||
{Generation: "abc", Key: "Super+Q", Action: "invented_action"},
|
||||
} {
|
||||
if _, err := AqueousBindRequest(aqueousFixture(t), edit); err == nil {
|
||||
t.Fatal("invalid edit accepted")
|
||||
}
|
||||
}
|
||||
request, err := AqueousBindRequest(aqueousFixture(t), AqueousBindEdit{Generation: "abc", Key: "Super+R", Remove: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, _ := json.Marshal(request["custom_keybind_changes"])
|
||||
if string(data) != `[{"id":"custom:12","op":"delete"}]` {
|
||||
t.Fatalf("wrong removal: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAqueousGenerationConflictCode(t *testing.T) {
|
||||
for _, generation := range []string{"", "stale"} {
|
||||
_, err := AqueousBindRequest(aqueousFixture(t), AqueousBindEdit{Generation: generation, Key: "Super+R", Remove: true})
|
||||
var conflict *AqueousError
|
||||
if !errors.As(err, &conflict) || conflict.Code != "external_change" {
|
||||
t.Fatalf("expected structured generation conflict, got %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAqueousBindingRequiresUniqueOriginal(t *testing.T) {
|
||||
for _, edit := range []AqueousBindEdit{
|
||||
{Generation: "abc", Key: "Super+Missing", Remove: true},
|
||||
{Generation: "abc", OriginalKey: "Super+Missing", Key: "Super+X", Action: "spawn_terminal"},
|
||||
} {
|
||||
_, err := AqueousBindRequest(aqueousFixture(t), edit)
|
||||
var conflict *AqueousError
|
||||
if !errors.As(err, &conflict) || conflict.Code != "target_removed" {
|
||||
t.Fatalf("expected missing original, got %v", err)
|
||||
}
|
||||
}
|
||||
snapshot := aqueousFixture(t)
|
||||
snapshot["custom_keybinds"] = append(snapshot["custom_keybinds"].([]any), map[string]any{"id": "duplicate", "chord": "Super+T", "command": "echo duplicate"})
|
||||
_, err := AqueousBindRequest(snapshot, AqueousBindEdit{Generation: "abc", Key: "Super+T", Remove: true})
|
||||
var conflict *AqueousError
|
||||
if !errors.As(err, &conflict) || conflict.Code != "ambiguous_target" {
|
||||
t.Fatalf("expected ambiguous original, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAqueousHelperFailureCodes(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
script string
|
||||
code string
|
||||
}{
|
||||
{"conflict", `printf '%s' '{"ok":false,"code":"external_change","message":"changed"}'; exit 1`, "external_change"},
|
||||
{"missing_ack", "exit 0", "uncertain"},
|
||||
{"failed_process", "exit 1", "uncertain"},
|
||||
{"malformed_ack", "printf '{'", "uncertain"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "aqueous-config"), []byte("#!/bin/sh\n"+tc.script+"\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
_, err := helperResult(context.Background(), "apply", nil)
|
||||
var failure *AqueousError
|
||||
if !errors.As(err, &failure) || failure.Code != tc.code {
|
||||
t.Fatalf("expected %s, got %v", tc.code, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ type DMSBindsStatus struct {
|
||||
}
|
||||
|
||||
type CheatSheet struct {
|
||||
Generation string `json:"generation,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Provider string `json:"provider"`
|
||||
ModKey string `json:"modKey,omitempty"`
|
||||
|
||||
@@ -500,23 +500,25 @@ func buildImportData(dank16JSON, image string) string {
|
||||
return fmt.Sprintf(`{"dank16": %s, "image": %s}`, dank16JSON, path)
|
||||
}
|
||||
|
||||
func userConfigSection(opts *Options) string {
|
||||
if !opts.RunUserTemplates || opts.ConfigDir == "" {
|
||||
return "[config]\n\n"
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(opts.ConfigDir, "matugen", "config.toml"))
|
||||
if err != nil {
|
||||
return "[config]\n\n"
|
||||
}
|
||||
section := extractTOMLSection(string(data), "[config]", "[templates]")
|
||||
if section == "" {
|
||||
return "[config]\n\n"
|
||||
}
|
||||
return section + "\n"
|
||||
}
|
||||
|
||||
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
|
||||
|
||||
wroteConfig := false
|
||||
if opts.RunUserTemplates {
|
||||
if data, err := os.ReadFile(userConfigPath); err == nil {
|
||||
configSection := extractTOMLSection(string(data), "[config]", "[templates]")
|
||||
if configSection != "" {
|
||||
cfgFile.WriteString(configSection)
|
||||
cfgFile.WriteString("\n")
|
||||
wroteConfig = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !wroteConfig {
|
||||
cfgFile.WriteString("[config]\n\n")
|
||||
}
|
||||
cfgFile.WriteString(userConfigSection(opts))
|
||||
|
||||
baseConfigPath := filepath.Join(opts.ShellDir, "matugen", "configs", "base.toml")
|
||||
if data, err := os.ReadFile(baseConfigPath); err == nil {
|
||||
@@ -983,7 +985,32 @@ func runMatugenDryRun(opts *Options) (string, error) {
|
||||
return execDryRun(opts, newFlags)
|
||||
}
|
||||
|
||||
// matugen aborts on a config file it cannot deserialize, and without -c it
|
||||
// reads the user's own ~/.config/matugen/config.toml, so a dry run gets a
|
||||
// config of its own instead of inheriting whatever is there.
|
||||
func writeDryRunConfig(opts *Options) (string, error) {
|
||||
cfgFile, err := os.CreateTemp("", "matugen-dryrun-*.toml")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create dry-run config: %w", err)
|
||||
}
|
||||
defer cfgFile.Close()
|
||||
|
||||
section := userConfigSection(opts)
|
||||
if idx := strings.Index(section, "\n[templates"); idx != -1 {
|
||||
section = section[:idx+1]
|
||||
}
|
||||
cfgFile.WriteString(section)
|
||||
cfgFile.WriteString("[templates]\n")
|
||||
return cfgFile.Name(), nil
|
||||
}
|
||||
|
||||
func execDryRun(opts *Options, flags matugenFlags) (string, error) {
|
||||
cfgPath, err := writeDryRunConfig(opts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer os.Remove(cfgPath)
|
||||
|
||||
var baseArgs []string
|
||||
switch opts.Kind {
|
||||
case "hex":
|
||||
@@ -991,7 +1018,7 @@ func execDryRun(opts *Options, flags matugenFlags) (string, error) {
|
||||
default:
|
||||
baseArgs = []string{opts.Kind, opts.Value}
|
||||
}
|
||||
baseArgs = append(baseArgs, "-m", string(opts.Mode), "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||
baseArgs = append(baseArgs, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgPath, "--json", "hex", "--dry-run")
|
||||
baseArgs = appendContrastArg(baseArgs, opts.Contrast)
|
||||
if flags.isV4 {
|
||||
baseArgs = append(baseArgs, sourceSelectionArgs(opts.SourceMode, flags.supportsPrefer)...)
|
||||
|
||||
@@ -1327,3 +1327,76 @@ func TestResolveSmartMode(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeUserMatugenConfig(t *testing.T, configDir, content string) {
|
||||
t.Helper()
|
||||
dir := filepath.Join(configDir, "matugen")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create matugen config dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "config.toml"), []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("failed to write user config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteDryRunConfigAlwaysDeclaresConfigAndTemplates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
userConfig string
|
||||
runUserTemplates bool
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "comment only user config",
|
||||
userConfig: "# nothing here\n",
|
||||
runUserTemplates: true,
|
||||
wantNotContains: []string{"# nothing here"},
|
||||
},
|
||||
{
|
||||
name: "user config section is carried over",
|
||||
userConfig: "[config]\nfallback_color = \"#ff0000\"\n\n[templates.mine]\ninput_path = 'in'\noutput_path = 'out'\n",
|
||||
runUserTemplates: true,
|
||||
wantContains: []string{"fallback_color = \"#ff0000\""},
|
||||
wantNotContains: []string{"[templates.mine]"},
|
||||
},
|
||||
{
|
||||
name: "user templates disabled",
|
||||
userConfig: "[config]\nfallback_color = \"#ff0000\"\n",
|
||||
wantNotContains: []string{"fallback_color"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
configDir := filepath.Join(t.TempDir(), "config")
|
||||
writeUserMatugenConfig(t, configDir, tc.userConfig)
|
||||
|
||||
opts := &Options{ConfigDir: configDir, RunUserTemplates: tc.runUserTemplates}
|
||||
cfgPath, err := writeDryRunConfig(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("writeDryRunConfig failed: %v", err)
|
||||
}
|
||||
defer os.Remove(cfgPath)
|
||||
|
||||
data, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read dry-run config: %v", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
assert.Contains(t, content, "[config]")
|
||||
assert.Contains(t, content, "[templates]")
|
||||
for _, want := range tc.wantContains {
|
||||
assert.Contains(t, content, want)
|
||||
}
|
||||
for _, unwanted := range tc.wantNotContains {
|
||||
assert.NotContains(t, content, unwanted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserConfigSectionWithoutConfigDir(t *testing.T) {
|
||||
assert.Equal(t, "[config]\n\n", userConfigSection(&Options{RunUserTemplates: true}))
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -148,9 +149,5 @@ func readQString(r io.Reader) (string, error) {
|
||||
|
||||
// SocketPathForPID returns the instance IPC socket used by qs --pid.
|
||||
func SocketPathForPID(pid int) string {
|
||||
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
||||
if runtimeDir == "" {
|
||||
runtimeDir = fmt.Sprintf("/run/user/%d", os.Getuid())
|
||||
}
|
||||
return filepath.Join(runtimeDir, "quickshell", "by-pid", fmt.Sprint(pid), "ipc.sock")
|
||||
return filepath.Join(utils.RuntimeDir(), "quickshell", "by-pid", fmt.Sprint(pid), "ipc.sock")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"image"
|
||||
"math"
|
||||
)
|
||||
|
||||
func aqueousWindowGeometry(model aqueousSnapshotModel, seatName string) (*WindowGeometry, error) {
|
||||
seat, err := model.Seat(seatName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if seat.FocusKind != "window" {
|
||||
return nil, errors.New("no active window: seat focus is not a window")
|
||||
}
|
||||
window := model.Entities["window:"+seat.Window]
|
||||
if window == nil || !window.Visible || window.Minimized || !window.CanActivate {
|
||||
return nil, errors.New("no eligible active window")
|
||||
}
|
||||
output := model.Entities["output:"+window.Output]
|
||||
if output == nil || !output.Enabled || !output.Powered {
|
||||
return nil, errors.New("active window output is unavailable")
|
||||
}
|
||||
box, bounds := window.OuterGeometry, output.Bounds
|
||||
if box == nil || bounds == nil || !box.valid() || !bounds.valid() || box.Width <= 0 || box.Height <= 0 || output.Scale <= 0 || math.IsInf(output.Scale, 0) || math.IsNaN(output.Scale) {
|
||||
return nil, errors.New("invalid active window geometry")
|
||||
}
|
||||
return &WindowGeometry{X: int32(box.X), Y: int32(box.Y), Width: int32(box.Width), Height: int32(box.Height), Output: output.Name, Scale: output.Scale, OutputX: int32(bounds.X), OutputY: int32(bounds.Y)}, nil
|
||||
}
|
||||
|
||||
func aqueousCropRect(geom *WindowGeometry, pixels image.Rectangle) image.Rectangle {
|
||||
left := math.Floor((float64(geom.X) - float64(geom.OutputX)) * geom.Scale)
|
||||
top := math.Floor((float64(geom.Y) - float64(geom.OutputY)) * geom.Scale)
|
||||
right := math.Ceil((float64(geom.X) + float64(geom.Width) - float64(geom.OutputX)) * geom.Scale)
|
||||
bottom := math.Ceil((float64(geom.Y) + float64(geom.Height) - float64(geom.OutputY)) * geom.Scale)
|
||||
return image.Rect(int(left), int(top), int(right), int(bottom)).Intersect(pixels)
|
||||
}
|
||||
|
||||
func aqueousFocusedOutput(seatName string) (string, error) {
|
||||
model, err := aqueousSnapshot(context.Background())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
seat, err := model.Seat(seatName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output := model.Entities["output:"+seat.Output]
|
||||
if output == nil || !output.Enabled || !output.Powered {
|
||||
return "", errors.New("selected output is unavailable")
|
||||
}
|
||||
return output.Name, nil
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureAqueousWindow() (*CaptureResult, error) {
|
||||
model, err := aqueousSnapshot(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
geom, err := aqueousWindowGeometry(model, s.config.Seat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
region := Region{X: geom.X, Y: geom.Y, Width: geom.Width, Height: geom.Height, Output: geom.Output}
|
||||
if s.config.Geometry {
|
||||
return geometryResult(region), nil
|
||||
}
|
||||
output := s.findOutputByName(geom.Output)
|
||||
if output == nil {
|
||||
return nil, errors.New("active window output disappeared")
|
||||
}
|
||||
result, err := s.captureWholeOutput(output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer result.Buffer.Close()
|
||||
rect := aqueousCropRect(geom, image.Rect(0, 0, result.Buffer.Width, result.Buffer.Height))
|
||||
if rect.Empty() {
|
||||
return nil, errors.New("active window is outside the output")
|
||||
}
|
||||
bpp := PixelFormat(result.Format).BytesPerPixel()
|
||||
cropped, err := CreateShmBuffer(rect.Dx(), rect.Dy(), rect.Dx()*bpp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
src, dst := result.Buffer.Data(), cropped.Data()
|
||||
for y := 0; y < rect.Dy(); y++ {
|
||||
start := (rect.Min.Y+y)*result.Buffer.Stride + rect.Min.X*bpp
|
||||
copy(dst[y*cropped.Stride:y*cropped.Stride+rect.Dx()*bpp], src[start:start+rect.Dx()*bpp])
|
||||
}
|
||||
cropped.Format = PixelFormat(result.Format)
|
||||
return &CaptureResult{Buffer: cropped, Region: region, Format: result.Format, Scale: geom.Scale, CICP: result.CICP}, nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
)
|
||||
|
||||
func aqueousSnapshot(ctx context.Context) (aqueousSnapshotModel, error) {
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return aqueousSnapshotModel{}, err
|
||||
}
|
||||
var data json.RawMessage
|
||||
if err := utils.RunJSON(ctx, executable, []string{"ipc", "call", "aqueous", "snapshot", os.Getenv("AQUEOUS_SOCKET")}, nil, &data); err != nil {
|
||||
return aqueousSnapshotModel{}, fmt.Errorf("get Aqueous state from DMS; a shell must be running in this session: %w", err)
|
||||
}
|
||||
return parseAqueousSnapshot(data)
|
||||
}
|
||||
|
||||
type aqueousBox struct {
|
||||
X, Y, Width, Height int64
|
||||
}
|
||||
|
||||
func (b aqueousBox) valid() bool {
|
||||
for _, value := range []int64{b.X, b.Y, b.Width, b.Height} {
|
||||
if value < math.MinInt32 || value > math.MaxInt32 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type aqueousCaptureEntity struct {
|
||||
Kind string `json:"kind"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Output string `json:"output"`
|
||||
Window string `json:"window"`
|
||||
FocusKind string `json:"focus_kind"`
|
||||
Visible bool `json:"visible"`
|
||||
Minimized bool `json:"minimized"`
|
||||
CanActivate bool `json:"can_activate"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Powered bool `json:"powered"`
|
||||
Scale float64 `json:"scale"`
|
||||
Bounds *aqueousBox `json:"bounds"`
|
||||
OuterGeometry *aqueousBox `json:"outer_geometry"`
|
||||
}
|
||||
|
||||
type aqueousSnapshotModel struct {
|
||||
Entities map[string]*aqueousCaptureEntity
|
||||
}
|
||||
|
||||
func (m aqueousSnapshotModel) Seat(name string) (*aqueousCaptureEntity, error) {
|
||||
if name != "" {
|
||||
if seat := m.Entities["seat:"+name]; seat != nil {
|
||||
return seat, nil
|
||||
}
|
||||
return nil, errors.New("not_found: seat")
|
||||
}
|
||||
var seat *aqueousCaptureEntity
|
||||
for _, entity := range m.Entities {
|
||||
if entity.Kind != "seat" {
|
||||
continue
|
||||
}
|
||||
if seat != nil {
|
||||
return nil, errors.New("ambiguous_seat")
|
||||
}
|
||||
seat = entity
|
||||
}
|
||||
if seat == nil {
|
||||
return nil, errors.New("unavailable: no seat")
|
||||
}
|
||||
return seat, nil
|
||||
}
|
||||
|
||||
func parseAqueousSnapshot(data []byte) (aqueousSnapshotModel, error) {
|
||||
var batch struct {
|
||||
Error string `json:"error"`
|
||||
Schema int `json:"schema"`
|
||||
Session string `json:"session"`
|
||||
Sequence string `json:"sequence"`
|
||||
BaseSequence json.RawMessage `json:"base_sequence"`
|
||||
Type string `json:"type"`
|
||||
Upsert []aqueousCaptureEntity `json:"upsert"`
|
||||
Removed []string `json:"removed"`
|
||||
}
|
||||
var model aqueousSnapshotModel
|
||||
if err := utils.DecodeJSON(data, &batch); err != nil {
|
||||
return model, err
|
||||
}
|
||||
if batch.Error != "" {
|
||||
return model, errors.New("aqueous state is unavailable in the running DMS shell")
|
||||
}
|
||||
if batch.Schema != 1 || batch.Type != "snapshot" || !bytes.Equal(batch.BaseSequence, []byte("null")) || batch.Upsert == nil || batch.Removed == nil || len(batch.Removed) != 0 || !regexp.MustCompile(`^[0-9a-f]{32}$`).MatchString(batch.Session) || !regexp.MustCompile(`^[0-9]+$`).MatchString(batch.Sequence) {
|
||||
return model, errors.New("invalid Aqueous shell snapshot")
|
||||
}
|
||||
model.Entities = make(map[string]*aqueousCaptureEntity, len(batch.Upsert))
|
||||
for i := range batch.Upsert {
|
||||
entity := &batch.Upsert[i]
|
||||
key := entity.Kind + ":" + entity.ID
|
||||
if entity.ID == "" || entity.Kind == "" || model.Entities[key] != nil {
|
||||
return aqueousSnapshotModel{}, errors.New("invalid or duplicate shell entity")
|
||||
}
|
||||
model.Entities[key] = entity
|
||||
}
|
||||
if model.Entities["session:session"] == nil {
|
||||
return aqueousSnapshotModel{}, errors.New("missing session entity")
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAqueousCropRoundsOutwardAndClips(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
geometry WindowGeometry
|
||||
pixels image.Rectangle
|
||||
want image.Rectangle
|
||||
}{
|
||||
{WindowGeometry{X: -1919, Y: 1, Width: 101, Height: 51, OutputX: -1920, Scale: 1.25}, image.Rect(0, 0, 2400, 1350), image.Rect(1, 1, 128, 65)},
|
||||
{WindowGeometry{X: -20, Y: -10, Width: 100, Height: 60, Scale: 1.5}, image.Rect(0, 0, 800, 600), image.Rect(0, 0, 120, 75)},
|
||||
{WindowGeometry{X: 90, Y: 80, Width: 100, Height: 100, Scale: 1}, image.Rect(0, 0, 100, 100), image.Rect(90, 80, 100, 100)},
|
||||
{WindowGeometry{X: 200, Y: 200, Width: 20, Height: 20, Scale: 1}, image.Rect(0, 0, 100, 100), image.Rectangle{}},
|
||||
} {
|
||||
if got := aqueousCropRect(&test.geometry, test.pixels); got != test.want {
|
||||
t.Fatalf("got %v want %v", got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAqueousGeometryUsesSeatAndOuterBounds(t *testing.T) {
|
||||
data, err := os.ReadFile("testdata/aqueous-snapshot.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
model, err := parseAqueousSnapshot(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
geometry, err := aqueousWindowGeometry(model, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seat, _ := model.Seat("")
|
||||
window := model.Entities["window:"+seat.Window]
|
||||
if geometry.Width != int32(window.OuterGeometry.Width) || geometry.X != int32(window.OuterGeometry.X) {
|
||||
t.Fatal("wrong bounds")
|
||||
}
|
||||
seat.FocusKind = "layer_surface"
|
||||
if _, err := aqueousWindowGeometry(model, ""); err == nil {
|
||||
t.Fatal("layer focus captured")
|
||||
}
|
||||
seat.FocusKind = "window"
|
||||
model.Entities["seat:second"] = &aqueousCaptureEntity{Kind: "seat", ID: "second"}
|
||||
if _, err := aqueousWindowGeometry(model, ""); err == nil {
|
||||
t.Fatal("ambiguous seat captured")
|
||||
}
|
||||
if _, err := aqueousWindowGeometry(model, seat.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
delete(model.Entities, "window:"+seat.Window)
|
||||
if _, err := aqueousWindowGeometry(model, seat.ID); err == nil {
|
||||
t.Fatal("removed window captured")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAqueousSnapshotRejectsInvalidInput(t *testing.T) {
|
||||
data, err := os.ReadFile("testdata/aqueous-snapshot.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, input := range [][]byte{
|
||||
[]byte(`{}`),
|
||||
bytes.Replace(data, []byte(`"schema": 1`), []byte(`"schema": 2`), 1),
|
||||
bytes.Replace(data, []byte(`"type": "snapshot"`), []byte(`"type": "delta"`), 1),
|
||||
append(data, []byte(`{}`)...),
|
||||
{0xff},
|
||||
} {
|
||||
if _, err := parseAqueousSnapshot(input); err == nil {
|
||||
t.Fatal("invalid snapshot accepted")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -21,6 +22,7 @@ const (
|
||||
CompositorScroll
|
||||
CompositorMiracle
|
||||
CompositorMango
|
||||
CompositorAqueous
|
||||
)
|
||||
|
||||
var detectedCompositor Compositor = -1
|
||||
@@ -35,6 +37,7 @@ func DetectCompositor() Compositor {
|
||||
needsStat bool
|
||||
compositor Compositor
|
||||
}{
|
||||
{os.Getenv("AQUEOUS_SOCKET"), true, CompositorAqueous},
|
||||
{os.Getenv("MANGO_INSTANCE_SIGNATURE"), true, CompositorMango},
|
||||
{os.Getenv("NIRI_SOCKET"), true, CompositorNiri},
|
||||
{os.Getenv("SCROLLSOCK"), true, CompositorScroll},
|
||||
@@ -78,8 +81,14 @@ func GetActiveWindow() (*WindowGeometry, error) {
|
||||
return getHyprlandActiveWindow()
|
||||
case CompositorMango:
|
||||
return getMangoActiveWindow()
|
||||
case CompositorAqueous:
|
||||
model, err := aqueousSnapshot(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aqueousWindowGeometry(model, "")
|
||||
default:
|
||||
return nil, fmt.Errorf("window capture requires Hyprland, Mango, or niri")
|
||||
return nil, fmt.Errorf("window capture requires Hyprland, Mango, niri, or Aqueous")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +364,9 @@ func getNiriFocusedMonitor() string {
|
||||
|
||||
func GetFocusedMonitor() string {
|
||||
switch DetectCompositor() {
|
||||
case CompositorAqueous:
|
||||
name, _ := aqueousFocusedOutput("")
|
||||
return name
|
||||
case CompositorHyprland:
|
||||
return getHyprlandFocusedMonitor()
|
||||
case CompositorSway:
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
)
|
||||
|
||||
const niriScreenshotTimeout = 5 * time.Second
|
||||
@@ -24,11 +26,7 @@ func CaptureNiriWindowImage(showPointer bool) (image.Image, error) {
|
||||
return nil, fmt.Errorf("NIRI_SOCKET not set")
|
||||
}
|
||||
|
||||
dir := os.Getenv("XDG_RUNTIME_DIR")
|
||||
if dir == "" {
|
||||
dir = os.TempDir()
|
||||
}
|
||||
path := filepath.Join(dir, fmt.Sprintf("dms-window-%d.png", os.Getpid()))
|
||||
path := filepath.Join(utils.RuntimeDir(), fmt.Sprintf("dms-window-%d.png", os.Getpid()))
|
||||
|
||||
events, err := subscribeNiriEvents(socket)
|
||||
if err != nil {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_cursor_shape"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
|
||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap"
|
||||
"github.com/AvengeMedia/dankgo/wayland/client"
|
||||
)
|
||||
|
||||
@@ -108,6 +109,7 @@ type RegionSelector struct {
|
||||
seat *client.Seat
|
||||
pointer *client.Pointer
|
||||
keyboard *client.Keyboard
|
||||
keymap *keymap.Keymap
|
||||
layerShell *wlr_layer_shell.ZwlrLayerShellV1
|
||||
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
|
||||
viewporter *wp_viewporter.WpViewporter
|
||||
@@ -967,7 +969,11 @@ func (r *RegionSelector) renderSurface(os *OutputSurface) {
|
||||
case !slot.cacheValid(srcBuf, r.selection.dragging, r.showCapturedCursor, r.phase, handles, shift):
|
||||
slot.shm.CopyFrom(srcBuf)
|
||||
r.dimBackground(slot.shm)
|
||||
r.drawHUD(slot.shm.Data(), slot.shm.Stride, slot.shm.Width, slot.shm.Height, os.screenFormat)
|
||||
scale := 1
|
||||
if os.output != nil {
|
||||
scale = scaleFactor(os.output.effectiveScale())
|
||||
}
|
||||
r.drawHUD(slot.shm.Data(), slot.shm.Stride, slot.shm.Width, slot.shm.Height, os.screenFormat, scale)
|
||||
slot.backgroundInitialized = true
|
||||
slot.backgroundSource = srcBuf
|
||||
slot.backgroundDragging = r.selection.dragging
|
||||
|
||||
@@ -3,6 +3,7 @@ package screenshot
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap"
|
||||
"github.com/AvengeMedia/dankgo/wayland/client"
|
||||
)
|
||||
|
||||
@@ -432,52 +433,60 @@ func (r *RegionSelector) setupKeyboardHandlers() {
|
||||
}
|
||||
})
|
||||
|
||||
r.keyboard.SetKeymapHandler(func(e client.KeyboardKeymapEvent) {
|
||||
r.keymap = keymap.FromEvent(e)
|
||||
})
|
||||
|
||||
r.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
|
||||
switch e.Key {
|
||||
case 29, 97: // Ctrl left/right
|
||||
ctrl := e.State != 0
|
||||
if ctrl != r.ctrlHeld {
|
||||
r.ctrlHeld = ctrl
|
||||
r.refreshCursor()
|
||||
if r.selection.hasSelection {
|
||||
for _, os := range r.surfaces {
|
||||
r.redrawSurface(os)
|
||||
}
|
||||
r.handleKey(r.keymap.Keysym(e.Key), e.State)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RegionSelector) handleKey(sym string, state uint32) {
|
||||
held := state != 0
|
||||
switch sym {
|
||||
case "Control_L", "Control_R":
|
||||
if held != r.ctrlHeld {
|
||||
r.ctrlHeld = held
|
||||
r.refreshCursor()
|
||||
if r.selection.hasSelection {
|
||||
for _, os := range r.surfaces {
|
||||
r.redrawSurface(os)
|
||||
}
|
||||
}
|
||||
case 56, 100: // Alt left/right
|
||||
r.altHeld = e.State != 0
|
||||
}
|
||||
if e.State != 1 {
|
||||
return
|
||||
}
|
||||
case "Alt_L", "Alt_R":
|
||||
r.altHeld = held
|
||||
}
|
||||
if state != 1 {
|
||||
return
|
||||
}
|
||||
|
||||
if r.phase == phaseScroll {
|
||||
switch e.Key {
|
||||
case 1:
|
||||
r.cancelled = true
|
||||
r.running = false
|
||||
case 28, 96:
|
||||
r.finishScroll()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch e.Key {
|
||||
case 1:
|
||||
if r.phase == phaseScroll {
|
||||
switch sym {
|
||||
case "Escape":
|
||||
r.cancelled = true
|
||||
r.running = false
|
||||
case 25:
|
||||
r.showCapturedCursor = !r.showCapturedCursor
|
||||
for _, os := range r.surfaces {
|
||||
r.redrawSurface(os)
|
||||
}
|
||||
case 28, 57, 96:
|
||||
if r.selection.hasSelection {
|
||||
r.finishSelection()
|
||||
}
|
||||
case "Return", "KP_Enter":
|
||||
r.finishScroll()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
switch sym {
|
||||
case "Escape":
|
||||
r.cancelled = true
|
||||
r.running = false
|
||||
case "p":
|
||||
r.showCapturedCursor = !r.showCapturedCursor
|
||||
for _, os := range r.surfaces {
|
||||
r.redrawSurface(os)
|
||||
}
|
||||
case "Return", "space", "KP_Enter":
|
||||
if r.selection.hasSelection {
|
||||
r.finishSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSelector) selectionDeviceRect() (*OutputSurface, int, int, int, int) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package screenshot
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestHandleKeyEscapeCancels(t *testing.T) {
|
||||
for _, phase := range []selectorPhase{phaseSelect, phaseScroll} {
|
||||
r := &RegionSelector{running: true, phase: phase}
|
||||
r.handleKey("Escape", 1)
|
||||
if !r.cancelled || r.running {
|
||||
t.Errorf("phase %v: cancelled=%v running=%v after Escape", phase, r.cancelled, r.running)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleKeyIgnoresReleaseAndCapsLock(t *testing.T) {
|
||||
r := &RegionSelector{running: true}
|
||||
r.handleKey("Escape", 0)
|
||||
r.handleKey("Caps_Lock", 1)
|
||||
if r.cancelled || !r.running {
|
||||
t.Errorf("cancelled=%v running=%v, want untouched", r.cancelled, r.running)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleKeyTogglesCapturedCursor(t *testing.T) {
|
||||
r := &RegionSelector{running: true, showCapturedCursor: true}
|
||||
r.handleKey("p", 1)
|
||||
if r.showCapturedCursor {
|
||||
t.Error("p did not toggle captured cursor off")
|
||||
}
|
||||
r.handleKey("p", 1)
|
||||
if !r.showCapturedCursor {
|
||||
t.Error("p did not toggle captured cursor back on")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleKeyTracksModifiers(t *testing.T) {
|
||||
r := &RegionSelector{running: true}
|
||||
r.handleKey("Control_R", 1)
|
||||
r.handleKey("Alt_L", 1)
|
||||
if !r.ctrlHeld || !r.altHeld {
|
||||
t.Errorf("ctrl=%v alt=%v after press, want both held", r.ctrlHeld, r.altHeld)
|
||||
}
|
||||
r.handleKey("Control_R", 0)
|
||||
r.handleKey("Alt_L", 0)
|
||||
if r.ctrlHeld || r.altHeld {
|
||||
t.Errorf("ctrl=%v alt=%v after release, want both released", r.ctrlHeld, r.altHeld)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,21 @@ import (
|
||||
"slices"
|
||||
)
|
||||
|
||||
var fontGlyphs = map[rune][12]uint8{
|
||||
const (
|
||||
fontCharW = 8
|
||||
fontCharH = 12
|
||||
fontStep = fontCharW + 1
|
||||
)
|
||||
|
||||
func scaleFactor(scale float64) int {
|
||||
s := int(math.Round(scale))
|
||||
if s < 1 {
|
||||
return 1
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var fontGlyphs = map[rune][fontCharH]uint8{
|
||||
'0': {0x3C, 0x66, 0x66, 0x6E, 0x76, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00},
|
||||
'1': {0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00},
|
||||
'2': {0x3C, 0x66, 0x66, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x66, 0x7E, 0x00, 0x00},
|
||||
@@ -593,12 +607,11 @@ func (r *RegionSelector) drawScrollPreview(data []byte, stride, bufW, bufH int,
|
||||
func (r *RegionSelector) drawScrollBar(data []byte, stride, bufW, bufH int, format uint32) {
|
||||
s := r.scroll
|
||||
style := LoadOverlayStyle()
|
||||
const charH = 12
|
||||
|
||||
r.fillRect(data, stride, bufW, bufH, s.barX, s.barY, s.barW, s.barH,
|
||||
style.BackgroundR, style.BackgroundG, style.BackgroundB, 245, format)
|
||||
|
||||
labelY := s.doneY + (s.btnH-charH)/2
|
||||
labelY := s.doneY + (s.btnH-fontCharH)/2
|
||||
r.fillRect(data, stride, bufW, bufH, s.doneX, s.doneY, s.doneW, s.btnH,
|
||||
style.AccentR, style.AccentG, style.AccentB, 255, format)
|
||||
r.drawText(data, stride, bufW, bufH, s.doneX+12, labelY, "done", 10, 10, 10, format)
|
||||
@@ -617,9 +630,15 @@ func (r *RegionSelector) drawScrollBar(data []byte, stride, bufW, bufH int, form
|
||||
style.TextR, style.TextG, style.TextB, format)
|
||||
}
|
||||
|
||||
func (r *RegionSelector) hudDimensions(bufW, bufH int) (hudX, hudY, hudW, hudH int) {
|
||||
const charW, charH, padding, itemSpacing = 8, 12, 12, 24
|
||||
type hudItem struct {
|
||||
key, desc string
|
||||
}
|
||||
|
||||
func (h hudItem) width(step int) int {
|
||||
return (len(h.key) + 1 + len(h.desc)) * step
|
||||
}
|
||||
|
||||
func (r *RegionSelector) hudItems() []hudItem {
|
||||
cursorLabel := "hide"
|
||||
if !r.showCapturedCursor {
|
||||
cursorLabel = "show"
|
||||
@@ -629,16 +648,25 @@ func (r *RegionSelector) hudDimensions(bufW, bufH int) (hudX, hudY, hudW, hudH i
|
||||
captureKey = "Drag+Release"
|
||||
}
|
||||
|
||||
items := []struct{ key, desc string }{
|
||||
return []hudItem{
|
||||
{captureKey, "capture"},
|
||||
{"Ctrl", "resize/move"},
|
||||
{"P", cursorLabel + " cursor"},
|
||||
{"Esc", "cancel"},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSelector) hudDimensions(bufW, bufH, scale int) (hudX, hudY, hudW, hudH int) {
|
||||
scale = scaleFactor(float64(scale))
|
||||
charH := fontCharH * scale
|
||||
padding, itemSpacing := 12*scale, 24*scale
|
||||
margin := 20 * scale
|
||||
|
||||
items := r.hudItems()
|
||||
totalW := 0
|
||||
step := fontStep * scale
|
||||
for i, item := range items {
|
||||
totalW += len(item.key)*(charW+1) + (1+len(item.desc))*(charW+1)
|
||||
totalW += item.width(step)
|
||||
if i < len(items)-1 {
|
||||
totalW += itemSpacing
|
||||
}
|
||||
@@ -647,46 +675,32 @@ func (r *RegionSelector) hudDimensions(bufW, bufH int) (hudX, hudY, hudW, hudH i
|
||||
hudW = totalW + padding*2
|
||||
hudH = charH + padding*2
|
||||
hudX = (bufW - hudW) / 2
|
||||
hudY = bufH - hudH - 20
|
||||
hudY = bufH - hudH - margin
|
||||
return hudX, hudY, hudW, hudH
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uint32) {
|
||||
func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uint32, scale int) {
|
||||
if r.selection.dragging {
|
||||
return
|
||||
}
|
||||
|
||||
hudX, hudY, hudW, hudH := r.hudDimensions(bufW, bufH)
|
||||
scale = scaleFactor(float64(scale))
|
||||
hudX, hudY, hudW, hudH := r.hudDimensions(bufW, bufH, scale)
|
||||
style := LoadOverlayStyle()
|
||||
r.fillRect(data, stride, bufW, bufH, hudX, hudY, hudW, hudH,
|
||||
style.BackgroundR, style.BackgroundG, style.BackgroundB, style.BackgroundA, format)
|
||||
|
||||
const charW, padding, itemSpacing = 8, 12, 24
|
||||
cursorLabel := "hide"
|
||||
if !r.showCapturedCursor {
|
||||
cursorLabel = "show"
|
||||
}
|
||||
captureKey := "Space/Enter"
|
||||
if r.screenshoter != nil && r.screenshoter.config.NoConfirm {
|
||||
captureKey = "Drag+Release"
|
||||
}
|
||||
|
||||
items := []struct{ key, desc string }{
|
||||
{captureKey, "capture"},
|
||||
{"Ctrl", "resize/move"},
|
||||
{"P", cursorLabel + " cursor"},
|
||||
{"Esc", "cancel"},
|
||||
}
|
||||
padding, itemSpacing := 12*scale, 24*scale
|
||||
step := fontStep * scale
|
||||
items := r.hudItems()
|
||||
|
||||
tx, ty := hudX+padding, hudY+padding
|
||||
for i, item := range items {
|
||||
r.drawText(data, stride, bufW, bufH, tx, ty, item.key,
|
||||
style.AccentR, style.AccentG, style.AccentB, format)
|
||||
tx += len(item.key) * (charW + 1)
|
||||
|
||||
r.drawText(data, stride, bufW, bufH, tx, ty, " "+item.desc,
|
||||
style.TextR, style.TextG, style.TextB, format)
|
||||
tx += (1 + len(item.desc)) * (charW + 1)
|
||||
r.drawTextScaled(data, stride, bufW, bufH, tx, ty, item.key,
|
||||
style.AccentR, style.AccentG, style.AccentB, format, scale)
|
||||
r.drawTextScaled(data, stride, bufW, bufH, tx+len(item.key)*step, ty, " "+item.desc,
|
||||
style.TextR, style.TextG, style.TextB, format, scale)
|
||||
tx += item.width(step)
|
||||
|
||||
if i < len(items)-1 {
|
||||
tx += itemSpacing
|
||||
@@ -741,24 +755,29 @@ func (r *RegionSelector) drawVLine(data []byte, stride, bufW, bufH, x, y, length
|
||||
}
|
||||
|
||||
func labelRect(b selectionRenderBounds, bufW, bufH int) (dirtyRect, string) {
|
||||
scale := scaleFactor(b.scaleX)
|
||||
text := b.labelText
|
||||
|
||||
const charW, charH = 8, 12
|
||||
textW := len(text) * (charW + 1)
|
||||
charH := fontCharH * scale
|
||||
step := fontStep * scale
|
||||
textW := len(text) * step
|
||||
padX := 4 * scale
|
||||
padY := 2 * scale
|
||||
offsetY := 8 * scale
|
||||
|
||||
tx := b.x + (b.w-textW)/2
|
||||
ty := b.y + b.h + 8
|
||||
ty := b.y + b.h + offsetY
|
||||
if ty+charH > bufH {
|
||||
ty = b.y - charH - 8
|
||||
ty = b.y - charH - offsetY
|
||||
}
|
||||
tx = clamp(tx, 0, bufW-textW)
|
||||
return dirtyRect{x1: tx - 4, y1: ty - 2, x2: tx + textW + 4, y2: ty + charH + 2}, text
|
||||
return dirtyRect{x1: tx - padX, y1: ty - padY, x2: tx + textW + padX, y2: ty + charH + padY}, text
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawLabel(data []byte, stride, bufW, bufH int, o *overlay, format uint32) {
|
||||
scale := scaleFactor(o.scaleX)
|
||||
l := o.label
|
||||
r.fillRect(data, stride, bufW, bufH, l.x1, l.y1, l.x2-l.x1, l.y2-l.y1, 0, 0, 0, 200, format)
|
||||
r.drawText(data, stride, bufW, bufH, l.x1+4, l.y1+2, o.text, 255, 255, 255, format)
|
||||
r.drawTextScaled(data, stride, bufW, bufH, l.x1+4*scale, l.y1+2*scale, o.text, 255, 255, 255, format, scale)
|
||||
}
|
||||
|
||||
func formatIsBGR(format uint32) bool {
|
||||
@@ -795,41 +814,55 @@ func (r *RegionSelector) fillRect(data []byte, stride, bufW, bufH, x, y, w, h in
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawText(data []byte, stride, bufW, bufH, x, y int, text string, cr, cg, cb uint8, format uint32) {
|
||||
r.drawTextScaled(data, stride, bufW, bufH, x, y, text, cr, cg, cb, format, 1)
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawTextScaled(data []byte, stride, bufW, bufH, x, y int, text string, cr, cg, cb uint8, format uint32, scale int) {
|
||||
scale = scaleFactor(float64(scale))
|
||||
step := fontStep * scale
|
||||
for i, ch := range text {
|
||||
r.drawChar(data, stride, bufW, bufH, x+i*9, y, ch, cr, cg, cb, format)
|
||||
r.drawChar(data, stride, bufW, bufH, x+i*step, y, ch, cr, cg, cb, format, scale)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawChar(data []byte, stride, bufW, bufH, x, y int, ch rune, cr, cg, cb uint8, format uint32) {
|
||||
func (r *RegionSelector) drawChar(data []byte, stride, bufW, bufH, x, y int, ch rune, cr, cg, cb uint8, format uint32, scale int) {
|
||||
glyph, ok := fontGlyphs[ch]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
scale = scaleFactor(float64(scale))
|
||||
|
||||
c0, c2 := cb, cr
|
||||
if formatIsBGR(format) {
|
||||
c0, c2 = cr, cb
|
||||
}
|
||||
|
||||
for row := range 12 {
|
||||
py := y + row
|
||||
if py < 0 || py >= bufH {
|
||||
continue
|
||||
}
|
||||
for row := range fontCharH {
|
||||
bits := glyph[row]
|
||||
for col := range 8 {
|
||||
if (bits & (1 << (7 - col))) == 0 {
|
||||
for dy := range scale {
|
||||
py := y + row*scale + dy
|
||||
if py < 0 || py >= bufH {
|
||||
continue
|
||||
}
|
||||
px := x + col
|
||||
if px < 0 || px >= bufW {
|
||||
continue
|
||||
rowOff := py * stride
|
||||
for col := range fontCharW {
|
||||
if (bits & (1 << (7 - col))) == 0 {
|
||||
continue
|
||||
}
|
||||
baseX := x + col*scale
|
||||
for dx := range scale {
|
||||
px := baseX + dx
|
||||
if px < 0 || px >= bufW {
|
||||
continue
|
||||
}
|
||||
off := rowOff + px*4
|
||||
if off+3 >= len(data) {
|
||||
continue
|
||||
}
|
||||
data[off], data[off+1], data[off+2], data[off+3] = c0, cg, c2, 255
|
||||
}
|
||||
}
|
||||
off := py*stride + px*4
|
||||
if off+3 >= len(data) {
|
||||
continue
|
||||
}
|
||||
data[off], data[off+1], data[off+2], data[off+3] = c0, cg, c2, 255
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,18 +238,20 @@ func TestHUDDimensionsContainText(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
hudX, hudY, hudW, hudH := r.hudDimensions(bufW, bufH)
|
||||
data := make([]byte, bufH*stride)
|
||||
r.drawHUD(data, stride, bufW, bufH, uint32(FormatARGB8888))
|
||||
for _, scale := range []int{1, 2} {
|
||||
hudX, hudY, hudW, hudH := r.hudDimensions(bufW, bufH, scale)
|
||||
data := make([]byte, bufH*stride)
|
||||
r.drawHUD(data, stride, bufW, bufH, uint32(FormatARGB8888), scale)
|
||||
|
||||
// Assert no lit pixel falls outside the pill boundaries
|
||||
for y := range bufH {
|
||||
for x := range bufW {
|
||||
off := y*stride + x*4
|
||||
if data[off] != 0 || data[off+1] != 0 || data[off+2] != 0 || data[off+3] != 0 {
|
||||
if x < hudX || x >= hudX+hudW || y < hudY || y >= hudY+hudH {
|
||||
t.Fatalf("noConfirm=%v showCursor=%v: pixel (%d, %d) is non-zero outside HUD pill [%d..%d, %d..%d]",
|
||||
noConfirm, showCursor, x, y, hudX, hudX+hudW, hudY, hudY+hudH)
|
||||
// Assert no lit pixel falls outside the pill boundaries
|
||||
for y := range bufH {
|
||||
for x := range bufW {
|
||||
off := y*stride + x*4
|
||||
if data[off] != 0 || data[off+1] != 0 || data[off+2] != 0 || data[off+3] != 0 {
|
||||
if x < hudX || x >= hudX+hudW || y < hudY || y >= hudY+hudH {
|
||||
t.Fatalf("scale=%d noConfirm=%v showCursor=%v: pixel (%d, %d) is non-zero outside HUD pill [%d..%d, %d..%d]",
|
||||
scale, noConfirm, showCursor, x, y, hudX, hudX+hudW, hudY, hudY+hudH)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_color_management"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||
"github.com/AvengeMedia/dankgo/wayland/client"
|
||||
)
|
||||
@@ -177,6 +178,14 @@ func (s *Screenshoter) captureLastRegion() (*CaptureResult, error) {
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
|
||||
if !s.config.AllowMultiple {
|
||||
lock, err := utils.LockSelectionOverlay()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer lock.Release()
|
||||
}
|
||||
|
||||
if s.config.Reset {
|
||||
if err := SaveLastRegion(Region{}); err != nil {
|
||||
log.Debug("failed to reset last region", "err", err)
|
||||
@@ -211,6 +220,9 @@ func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
|
||||
if DetectCompositor() == CompositorAqueous {
|
||||
return s.captureAqueousWindow()
|
||||
}
|
||||
if DetectCompositor() == CompositorNiri {
|
||||
if s.config.Geometry {
|
||||
return nil, fmt.Errorf("window geometry mode is not supported on niri")
|
||||
@@ -353,6 +365,13 @@ func (s *Screenshoter) captureMangoWindow(output *WaylandOutput, region Region,
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureFullScreen() (*CaptureResult, error) {
|
||||
if DetectCompositor() == CompositorAqueous {
|
||||
name, err := aqueousFocusedOutput(s.config.Seat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.captureOutput(name)
|
||||
}
|
||||
output := s.findFocusedOutput()
|
||||
if output == nil {
|
||||
s.outputsMu.Lock()
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
aqueous-snapshot.json contains focused-window state captured from Aqueous 2d8b07fb1ff2354ea0c3acddd5cc84ceb823d6eb in an isolated session on 2026-09-05. The session token is sanitized.
|
||||
@@ -0,0 +1 @@
|
||||
{"schema": 1, "session": "0123456789abcdef0123456789abcdef", "sequence": "5", "base_sequence": null, "type": "snapshot", "upsert": [{"kind": "workspace", "id": "12", "output": "2", "name": "same \"name\"", "number": 3, "active": true, "urgent": false}, {"kind": "workspace", "id": "2", "output": "1", "name": "2", "number": 2, "active": false, "urgent": false}, {"kind": "session", "id": "session", "locked": false, "default_seat": "default", "overview_output": null, "overview_window": null}, {"kind": "workspace", "id": "11", "output": "2", "name": "2", "number": 2, "active": false, "urgent": false}, {"kind": "output", "id": "1", "name": "HEADLESS-2", "enabled": true, "powered": true, "bounds": {"x": 0, "y": 0, "width": 1280, "height": 720}, "usable_bounds": {"x": 0, "y": 0, "width": 1280, "height": 720}, "scale": 1, "transform": "normal", "active_workspace": "1"}, {"kind": "workspace", "id": "18", "output": "2", "name": "9", "number": 9, "active": false, "urgent": false}, {"kind": "workspace", "id": "16", "output": "2", "name": "7", "number": 7, "active": false, "urgent": false}, {"kind": "workspace", "id": "15", "output": "2", "name": "6", "number": 6, "active": false, "urgent": false}, {"kind": "workspace", "id": "10", "output": "2", "name": "1", "number": 1, "active": false, "urgent": false}, {"kind": "workspace", "id": "5", "output": "1", "name": "5", "number": 5, "active": false, "urgent": false}, {"kind": "workspace", "id": "9", "output": "1", "name": "9", "number": 9, "active": false, "urgent": false}, {"kind": "workspace", "id": "4", "output": "1", "name": "4", "number": 4, "active": false, "urgent": false}, {"kind": "seat", "id": "default", "output": "1", "window": "ca89b2eb6850b81d12b92763969d82ca", "focus_kind": "window", "keyboard": "1"}, {"kind": "workspace", "id": "3", "output": "1", "name": "3", "number": 3, "active": false, "urgent": false}, {"kind": "workspace", "id": "14", "output": "2", "name": "5", "number": 5, "active": false, "urgent": false}, {"kind": "workspace", "id": "7", "output": "1", "name": "7", "number": 7, "active": false, "urgent": false}, {"kind": "workspace", "id": "1", "output": "1", "name": "1", "number": 1, "active": true, "urgent": false}, {"kind": "workspace", "id": "6", "output": "1", "name": "6", "number": 6, "active": false, "urgent": false}, {"kind": "workspace", "id": "8", "output": "1", "name": "8", "number": 8, "active": false, "urgent": false}, {"kind": "workspace", "id": "13", "output": "2", "name": "4", "number": 4, "active": false, "urgent": false}, {"kind": "workspace", "id": "17", "output": "2", "name": "8", "number": 8, "active": false, "urgent": false}, {"kind": "output", "id": "2", "name": "HEADLESS-1", "enabled": true, "powered": true, "bounds": {"x": 1280, "y": 0, "width": 1280, "height": 720}, "usable_bounds": {"x": 1280, "y": 0, "width": 1280, "height": 720}, "scale": 1, "transform": "normal", "active_workspace": "12"}, {"kind": "window", "id": "ca89b2eb6850b81d12b92763969d82ca", "backend": "xdg", "app_id": "aq-shell-test", "class": null, "title": "same title", "workspace": "1", "output": "1", "geometry": {"x": 10, "y": 10, "width": 628, "height": 700}, "outer_geometry": {"x": 8, "y": 8, "width": 632, "height": 704}, "focused": true, "visible": true, "floating": false, "minimized": false, "maximized": false, "fullscreen": false, "skip_taskbar": false, "skip_switcher": false, "always_above": false, "always_below": false, "snapped": false, "fixed_position": false, "layout": "tiled", "can_minimize": false, "can_maximize": false, "can_activate": true}, {"kind": "keyboard", "id": "1", "seat": "default", "layouts": ["English (US)", "German"], "index": 0}, {"kind": "keyboard_device", "id": "1", "name": "wlr_virtual_keyboard_v1", "seat": "default", "group": "1", "virtual": true}], "removed": []}
|
||||
@@ -56,21 +56,23 @@ type Output struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Mode Mode
|
||||
OutputName string
|
||||
Cursor CursorMode
|
||||
NoConfirm bool
|
||||
Reset bool
|
||||
Format Format
|
||||
Quality int
|
||||
OutputDir string
|
||||
Filename string
|
||||
Clipboard bool
|
||||
SaveFile bool
|
||||
Notify bool
|
||||
Stdout bool
|
||||
Geometry bool
|
||||
IntervalMs int
|
||||
Seat string
|
||||
Mode Mode
|
||||
OutputName string
|
||||
Cursor CursorMode
|
||||
NoConfirm bool
|
||||
Reset bool
|
||||
Format Format
|
||||
Quality int
|
||||
OutputDir string
|
||||
Filename string
|
||||
Clipboard bool
|
||||
SaveFile bool
|
||||
Notify bool
|
||||
Stdout bool
|
||||
Geometry bool
|
||||
AllowMultiple bool
|
||||
IntervalMs int
|
||||
// SelectorHook runs as the interactive selector starts (true) and ends (false).
|
||||
SelectorHook func(begin bool)
|
||||
}
|
||||
|
||||
@@ -160,15 +160,20 @@ func (b *DDCBackend) getDDCName(bus int) string {
|
||||
}
|
||||
|
||||
func (b *DDCBackend) readInitialBrightness(fd int, dev *ddcDevice) {
|
||||
cap, err := b.getVCPFeature(fd, VCP_BRIGHTNESS)
|
||||
if err != nil {
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
cap, err := b.getVCPFeature(fd, VCP_BRIGHTNESS)
|
||||
if err == nil {
|
||||
dev.max = cap.max
|
||||
dev.lastBrightness = cap.current
|
||||
log.Debugf("initialized %s with brightness %d/%d", dev.name, cap.current, cap.max)
|
||||
return
|
||||
}
|
||||
if attempt < 2 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
log.Debugf("failed to read initial brightness for %s: %v", dev.name, err)
|
||||
return
|
||||
}
|
||||
|
||||
dev.max = cap.max
|
||||
dev.lastBrightness = cap.current
|
||||
log.Debugf("initialized %s with brightness %d/%d", dev.name, cap.current, cap.max)
|
||||
}
|
||||
|
||||
func (b *DDCBackend) GetDevices() ([]Device, error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
@@ -21,15 +22,13 @@ const (
|
||||
|
||||
var _ HotspotBackend = (*NetworkManagerBackend)(nil)
|
||||
|
||||
// ConfigureHotspot validates only DMS-owned constraints: SSID presence, band
|
||||
// names, device capability, and the not-active guard. SSID byte-length and
|
||||
// WPA-PSK password policy are deliberately left to NetworkManager, whose
|
||||
// AddConnection/Update errors are returned to the caller; duplicating its
|
||||
// evolving rules here would only let them drift.
|
||||
func (b *NetworkManagerBackend) ConfigureHotspot(req HotspotRequest) error {
|
||||
if req.SSID == "" {
|
||||
return fmt.Errorf("hotspot SSID cannot be empty")
|
||||
}
|
||||
if err := validateHotspotPassword(req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateHotspotBandName(req.Band); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -495,6 +494,22 @@ func isAPCapableWiFiDevice(devInfo *wifiDeviceInfo) (bool, error) {
|
||||
return caps&nmWiFiDeviceCapAP != 0, nil
|
||||
}
|
||||
|
||||
// Mirrors NetworkManager's nm_utils_wpa_psk_valid: byte length, not rune count.
|
||||
func validateHotspotPassword(password string) error {
|
||||
if password == "" {
|
||||
return nil
|
||||
}
|
||||
if len(password) == 64 {
|
||||
if _, err := hex.DecodeString(password); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if len(password) < 8 || len(password) > 63 {
|
||||
return fmt.Errorf("hotspot password must be 8 to 63 characters, or a 64-digit hex key")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateHotspotBandName(band string) error {
|
||||
if band != "" && band != "bg" && band != "a" {
|
||||
return fmt.Errorf("unsupported hotspot band: %s", band)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2"
|
||||
@@ -916,3 +917,48 @@ func TestGetHotspotSecrets(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateHotspotPassword(t *testing.T) {
|
||||
hex64 := strings.Repeat("0123456789abcdef", 4)
|
||||
cases := []struct {
|
||||
name string
|
||||
password string
|
||||
valid bool
|
||||
}{
|
||||
{"open network", "", true},
|
||||
{"three digits", "123", false},
|
||||
{"seven chars", "testing", false},
|
||||
{"eight chars", "testing1", true},
|
||||
{"sixty three chars", strings.Repeat("a", 63), true},
|
||||
{"sixty four hex", hex64, true},
|
||||
{"sixty four hex uppercase", strings.ToUpper(hex64), true},
|
||||
{"sixty four non-hex", strings.Repeat("g", 64), false},
|
||||
{"sixty five chars", strings.Repeat("a", 65), false},
|
||||
{"multibyte counts bytes", "ääää", true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := validateHotspotPassword(tc.password)
|
||||
if tc.valid {
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "8 to 63 characters")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureHotspotRejectsShortPasswordBeforeTouchingNetworkManager(t *testing.T) {
|
||||
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
|
||||
mockSettings := mock_gonetworkmanager.NewMockSettings(t)
|
||||
|
||||
backend, err := NewNetworkManagerBackend(mockNM)
|
||||
require.NoError(t, err)
|
||||
backend.settings = mockSettings
|
||||
|
||||
err = backend.ConfigureHotspot(HotspotRequest{SSID: "DMS Hotspot", Password: "123", Device: "wlan0"})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "8 to 63 characters")
|
||||
}
|
||||
|
||||
@@ -183,6 +183,12 @@ func (b *NetworkManagerBackend) classifyNMStateReason(reason uint32) string {
|
||||
}
|
||||
}
|
||||
|
||||
// NM raises NO_SECRETS once auth retries run out on a saved profile too, not only
|
||||
// on a dismissed prompt (nm-device-wifi.c handle_8021x_or_psk_auth_fail, #3313).
|
||||
func forgetOnConnectFailure(reasonCode string, preExisting bool) bool {
|
||||
return reasonCode == errdefs.ErrUserCanceled && !preExisting
|
||||
}
|
||||
|
||||
// With several adapters, state must follow the associated device, not whichever
|
||||
// enumerated first (#2460).
|
||||
func (b *NetworkManagerBackend) wifiDeviceForState() (gonetworkmanager.Device, gonetworkmanager.DeviceWireless) {
|
||||
@@ -321,7 +327,7 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
|
||||
b.state.ConnectingSSID = ""
|
||||
b.state.LastError = reasonCode
|
||||
|
||||
if reasonCode == errdefs.ErrUserCanceled {
|
||||
if forgetOnConnectFailure(reasonCode, b.state.ConnectingPreExisting) {
|
||||
forgetSSID = connectingSSID
|
||||
}
|
||||
|
||||
|
||||
@@ -146,3 +146,23 @@ func TestNetworkManagerBackend_UpdateWiFiState_PicksConnectedDevice(t *testing.T
|
||||
assert.True(t, backend.state.WiFiConnected)
|
||||
assert.Equal(t, "wlan1", backend.state.WiFiDevice)
|
||||
}
|
||||
|
||||
func TestForgetOnConnectFailure(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
reasonCode string
|
||||
preExisting bool
|
||||
want bool
|
||||
}{
|
||||
{"cancelled prompt on freshly created profile", errdefs.ErrUserCanceled, false, true},
|
||||
{"no-secrets failure on saved profile", errdefs.ErrUserCanceled, true, false},
|
||||
{"bad credentials on freshly created profile", errdefs.ErrBadCredentials, false, false},
|
||||
{"generic failure on saved profile", errdefs.ErrConnectionFailed, true, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, forgetOnConnectFailure(tt.reasonCode, tt.preExisting))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,15 +28,8 @@ type wpaCtrlConn struct {
|
||||
seq int
|
||||
}
|
||||
|
||||
func wpaCtrlLocalSocketBase() string {
|
||||
if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" {
|
||||
return dir
|
||||
}
|
||||
return os.TempDir()
|
||||
}
|
||||
|
||||
func newWpaCtrlConn(sockPath string) (*wpaCtrlConn, error) {
|
||||
localDir, err := os.MkdirTemp(wpaCtrlLocalSocketBase(), "dms-wpa-")
|
||||
localDir, err := os.MkdirTemp(utils.RuntimeDir(), "dms-wpa-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create wpa_ctrl socket dir: %w", err)
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/AvengeMedia/dankgo/ipc"
|
||||
"github.com/AvengeMedia/dankgo/paths"
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
)
|
||||
|
||||
@@ -86,7 +86,7 @@ var cupsMu sync.Mutex
|
||||
var cupsSubscriberCount int
|
||||
var cupsEverAvailable bool
|
||||
|
||||
var appPaths = paths.New("danklinux")
|
||||
var appPaths = utils.App()
|
||||
|
||||
func GetSocketPath() string {
|
||||
return appPaths.SocketPath()
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type AppChecker interface {
|
||||
@@ -115,3 +123,64 @@ func userBinDirs() []string {
|
||||
dirs = append(dirs, "/usr/local/bin")
|
||||
return dirs
|
||||
}
|
||||
|
||||
const maxJSONProcessBytes = 4 * 1024 * 1024
|
||||
|
||||
type boundedWriter struct {
|
||||
bytes.Buffer
|
||||
limit int
|
||||
}
|
||||
|
||||
func (w *boundedWriter) Write(p []byte) (int, error) {
|
||||
if len(p) > w.limit-w.Len() {
|
||||
return 0, errors.New("process output exceeds size bound")
|
||||
}
|
||||
return w.Buffer.Write(p)
|
||||
}
|
||||
|
||||
func RunJSON(ctx context.Context, executable string, args []string, input []byte, result any) error {
|
||||
if len(input) > maxJSONProcessBytes {
|
||||
return errors.New("request exceeds size bound")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, executable, args...)
|
||||
cmd.WaitDelay = time.Second
|
||||
stdout := &boundedWriter{limit: maxJSONProcessBytes + 1}
|
||||
stderr := &boundedWriter{limit: 16 * 1024}
|
||||
cmd.Stdout, cmd.Stderr = stdout, stderr
|
||||
if input != nil {
|
||||
cmd.Stdin = bytes.NewReader(input)
|
||||
}
|
||||
err := cmd.Run()
|
||||
if ctx.Err() != nil {
|
||||
return fmt.Errorf("unavailable: command completion uncertain: %w", ctx.Err())
|
||||
}
|
||||
if stdout.Len() != 0 {
|
||||
if decodeErr := DecodeJSON(bytes.TrimSuffix(stdout.Bytes(), []byte{'\n'}), result); decodeErr != nil {
|
||||
return errors.Join(err, fmt.Errorf("invalid process JSON: %w", decodeErr))
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", executable, err)
|
||||
}
|
||||
if stdout.Len() == 0 {
|
||||
return errors.New("missing command acknowledgement")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DecodeJSON(data []byte, target any) error {
|
||||
if len(data) > maxJSONProcessBytes || !utf8.Valid(data) {
|
||||
return errors.New("invalid UTF-8 or oversized JSON record")
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.UseNumber()
|
||||
if err := decoder.Decode(target); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := decoder.Decode(new(any)); err != io.EOF {
|
||||
return errors.New("trailing JSON data")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -67,3 +70,58 @@ func TestEnvWithUserBinPathPrependsLocalBin(t *testing.T) {
|
||||
assert.Contains(t, parts, "/usr/local/bin")
|
||||
assert.Contains(t, env, "OTHER=value")
|
||||
}
|
||||
|
||||
func TestJSONProcessFixture(t *testing.T) {
|
||||
mode := os.Getenv("DMS_JSON_PROCESS_FIXTURE")
|
||||
if mode == "" {
|
||||
return
|
||||
}
|
||||
switch mode {
|
||||
case "accepted":
|
||||
fmt.Print(`{"ok":true,"status":"accepted","sequence":"9007199254740993"}`)
|
||||
case "failure":
|
||||
fmt.Print(`{"ok":false,"status":"locked","sequence":"1"}`)
|
||||
os.Exit(1)
|
||||
case "malformed":
|
||||
fmt.Print("invalid JSON")
|
||||
case "timeout":
|
||||
time.Sleep(time.Minute)
|
||||
case "oversize":
|
||||
fmt.Print(strings.Repeat("x", maxJSONProcessBytes+2))
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func TestRunJSONResultsAndTimeout(t *testing.T) {
|
||||
for _, mode := range []string{"accepted", "failure", "malformed", "oversize", "timeout"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
t.Setenv("DMS_JSON_PROCESS_FIXTURE", mode)
|
||||
limit := 4 * time.Second
|
||||
if mode == "timeout" {
|
||||
limit = 100 * time.Millisecond
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), limit)
|
||||
defer cancel()
|
||||
var result jsonProcessResult
|
||||
err := RunJSON(ctx, os.Args[0], []string{"-test.run=^TestJSONProcessFixture$"}, nil, &result)
|
||||
if mode == "accepted" {
|
||||
if err != nil || !result.OK || result.Status != "accepted" || result.Sequence != "9007199254740993" {
|
||||
t.Fatalf("lost acknowledgement: %v %#v", err, result)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("failure accepted")
|
||||
}
|
||||
if mode == "failure" && result.Status != "locked" {
|
||||
t.Fatal("operation failure lost")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type jsonProcessResult struct {
|
||||
OK bool `json:"ok"`
|
||||
Status string `json:"status"`
|
||||
Sequence string `json:"sequence"`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
@@ -10,22 +11,35 @@ func dconfPath(schema, key string) string {
|
||||
return "/" + strings.ReplaceAll(schema, ".", "/") + "/" + key
|
||||
}
|
||||
|
||||
// GsettingsGet reads a gsettings value, falling back to dconf read.
|
||||
// gsettings reaches whichever backend its own GLib links, not necessarily the
|
||||
// dconf store GTK apps read, and it exits 0 either way.
|
||||
func dconfSelected() bool {
|
||||
backend := os.Getenv("GSETTINGS_BACKEND")
|
||||
return backend == "" || backend == "dconf"
|
||||
}
|
||||
|
||||
// GsettingsGet reads a desktop setting from dconf, falling back to gsettings.
|
||||
func GsettingsGet(schema, key string) (string, error) {
|
||||
if out, err := exec.Command("gsettings", "get", schema, key).Output(); err == nil {
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
if dconfSelected() {
|
||||
if out, err := exec.Command("dconf", "read", dconfPath(schema, key)).Output(); err == nil {
|
||||
if value := strings.TrimSpace(string(out)); value != "" {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
out, err := exec.Command("dconf", "read", dconfPath(schema, key)).Output()
|
||||
out, err := exec.Command("gsettings", "get", schema, key).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("gsettings/dconf get failed for %s %s: %w", schema, key, err)
|
||||
return "", fmt.Errorf("dconf/gsettings get failed for %s %s: %w", schema, key, err)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
// GsettingsSet writes a gsettings value, falling back to dconf write.
|
||||
// GsettingsSet writes a desktop setting to dconf, falling back to gsettings.
|
||||
func GsettingsSet(schema, key, value string) error {
|
||||
if err := exec.Command("gsettings", "set", schema, key, value).Run(); err == nil {
|
||||
return nil
|
||||
if dconfSelected() {
|
||||
if err := exec.Command("dconf", "write", dconfPath(schema, key), "'"+value+"'").Run(); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return exec.Command("dconf", "write", dconfPath(schema, key), "'"+value+"'").Run()
|
||||
return exec.Command("gsettings", "set", schema, key, value).Run()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeTool struct {
|
||||
name string
|
||||
stdout string
|
||||
exit int
|
||||
}
|
||||
|
||||
func fakeTools(t *testing.T, tools ...fakeTool) string {
|
||||
t.Helper()
|
||||
binDir := t.TempDir()
|
||||
logPath := filepath.Join(binDir, "calls.log")
|
||||
for _, tool := range tools {
|
||||
script := fmt.Sprintf("#!/bin/sh\necho \"%s $*\" >> %q\nprintf '%%s' %q\nexit %d\n", tool.name, logPath, tool.stdout, tool.exit)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(binDir, tool.name), []byte(script), 0o755))
|
||||
}
|
||||
t.Setenv("PATH", binDir)
|
||||
return logPath
|
||||
}
|
||||
|
||||
func toolCalls(t *testing.T, logPath string) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(logPath)
|
||||
if os.IsNotExist(err) {
|
||||
return ""
|
||||
}
|
||||
require.NoError(t, err)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func TestGsettingsSetWritesDconf(t *testing.T) {
|
||||
t.Setenv("GSETTINGS_BACKEND", "")
|
||||
logPath := fakeTools(t,
|
||||
fakeTool{name: "dconf"},
|
||||
fakeTool{name: "gsettings"},
|
||||
)
|
||||
|
||||
require.NoError(t, GsettingsSet("org.gnome.desktop.interface", "gtk-theme", "adw-gtk3"))
|
||||
|
||||
calls := toolCalls(t, logPath)
|
||||
assert.Contains(t, calls, "dconf write /org/gnome/desktop/interface/gtk-theme 'adw-gtk3'")
|
||||
assert.NotContains(t, calls, "gsettings")
|
||||
}
|
||||
|
||||
func TestGsettingsSetFallsBackWhenDconfFails(t *testing.T) {
|
||||
t.Setenv("GSETTINGS_BACKEND", "")
|
||||
logPath := fakeTools(t,
|
||||
fakeTool{name: "dconf", exit: 1},
|
||||
fakeTool{name: "gsettings"},
|
||||
)
|
||||
|
||||
require.NoError(t, GsettingsSet("org.gnome.desktop.interface", "gtk-theme", "adw-gtk3"))
|
||||
|
||||
assert.Contains(t, toolCalls(t, logPath), "gsettings set org.gnome.desktop.interface gtk-theme adw-gtk3")
|
||||
}
|
||||
|
||||
func TestGsettingsSetHonorsBackendOverride(t *testing.T) {
|
||||
t.Setenv("GSETTINGS_BACKEND", "keyfile")
|
||||
logPath := fakeTools(t,
|
||||
fakeTool{name: "dconf"},
|
||||
fakeTool{name: "gsettings"},
|
||||
)
|
||||
|
||||
require.NoError(t, GsettingsSet("org.gnome.desktop.interface", "gtk-theme", "adw-gtk3"))
|
||||
|
||||
calls := toolCalls(t, logPath)
|
||||
assert.Contains(t, calls, "gsettings set org.gnome.desktop.interface gtk-theme adw-gtk3")
|
||||
assert.NotContains(t, calls, "dconf write")
|
||||
}
|
||||
|
||||
func TestGsettingsGetReadsDconf(t *testing.T) {
|
||||
t.Setenv("GSETTINGS_BACKEND", "")
|
||||
fakeTools(t,
|
||||
fakeTool{name: "dconf", stdout: "'adw-gtk3-dark'"},
|
||||
fakeTool{name: "gsettings", stdout: "'Adwaita'"},
|
||||
)
|
||||
|
||||
value, err := GsettingsGet("org.gnome.desktop.interface", "gtk-theme")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "'adw-gtk3-dark'", value)
|
||||
}
|
||||
|
||||
func TestGsettingsGetFallsBackWhenDconfUnset(t *testing.T) {
|
||||
t.Setenv("GSETTINGS_BACKEND", "")
|
||||
fakeTools(t,
|
||||
fakeTool{name: "dconf"},
|
||||
fakeTool{name: "gsettings", stdout: "'Adwaita'"},
|
||||
)
|
||||
|
||||
value, err := GsettingsGet("org.gnome.desktop.interface", "gtk-theme")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "'Adwaita'", value)
|
||||
}
|
||||
|
||||
func TestGsettingsGetFailsWhenNoToolWorks(t *testing.T) {
|
||||
t.Setenv("GSETTINGS_BACKEND", "")
|
||||
fakeTools(t,
|
||||
fakeTool{name: "dconf", exit: 1},
|
||||
fakeTool{name: "gsettings", exit: 1},
|
||||
)
|
||||
|
||||
_, err := GsettingsGet("org.gnome.desktop.interface", "gtk-theme")
|
||||
|
||||
require.ErrorContains(t, err, "gtk-theme")
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// Shared by the screenshot region selector and the color picker: one grabbing overlay at a time.
|
||||
SelectionOverlayLock = "dms-selection-overlay"
|
||||
// Matches what the wayland client dials when WAYLAND_DISPLAY is unset.
|
||||
defaultWaylandDisplay = "wayland-0"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrLockHeld = errors.New("lock is held by another process")
|
||||
ErrOverlayActive = errors.New("another dms selection overlay is already open")
|
||||
)
|
||||
|
||||
type FileLock struct {
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func displayScope() string {
|
||||
name := filepath.Base(os.Getenv("WAYLAND_DISPLAY"))
|
||||
switch name {
|
||||
case ".", "..", string(filepath.Separator):
|
||||
return defaultWaylandDisplay
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// One user's concurrent sessions share XDG_RUNTIME_DIR, so the lock is per wayland display.
|
||||
func lockPath(name string) string {
|
||||
return filepath.Join(RuntimeDir(), fmt.Sprintf("%s-%d-%s.lock", name, os.Getuid(), displayScope()))
|
||||
}
|
||||
|
||||
func lockHolderPID(name string) int {
|
||||
data, err := os.ReadFile(lockPath(name))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return pid
|
||||
}
|
||||
|
||||
func TryLock(name string) (*FileLock, error) {
|
||||
f, err := os.OpenFile(lockPath(name), os.O_CREATE|os.O_RDWR, 0o600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
||||
f.Close()
|
||||
if errors.Is(err, syscall.EWOULDBLOCK) {
|
||||
return nil, ErrLockHeld
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := f.Truncate(0); err == nil {
|
||||
_, _ = f.WriteString(strconv.Itoa(os.Getpid()))
|
||||
}
|
||||
|
||||
return &FileLock{file: f}, nil
|
||||
}
|
||||
|
||||
// LockSelectionOverlay only refuses on contention: a lock file that cannot be created or
|
||||
// flocked yields a nil lock and no error, so the overlay still runs unguarded.
|
||||
func LockSelectionOverlay() (*FileLock, error) {
|
||||
lock, err := TryLock(SelectionOverlayLock)
|
||||
if err == nil {
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
if !errors.Is(err, ErrLockHeld) {
|
||||
log.Debug("selection overlay lock unavailable", "err", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if pid := lockHolderPID(SelectionOverlayLock); pid > 0 {
|
||||
return nil, fmt.Errorf("%w (pid %d)", ErrOverlayActive, pid)
|
||||
}
|
||||
return nil, ErrOverlayActive
|
||||
}
|
||||
|
||||
func (l *FileLock) Release() {
|
||||
if l == nil || l.file == nil {
|
||||
return
|
||||
}
|
||||
_ = syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN)
|
||||
l.file.Close()
|
||||
l.file = nil
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTryLockRejectsSecondHolder(t *testing.T) {
|
||||
t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
|
||||
|
||||
held, err := TryLock(SelectionOverlayLock)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = TryLock(SelectionOverlayLock)
|
||||
assert.ErrorIs(t, err, ErrLockHeld)
|
||||
|
||||
held.Release()
|
||||
|
||||
reacquired, err := TryLock(SelectionOverlayLock)
|
||||
require.NoError(t, err)
|
||||
reacquired.Release()
|
||||
}
|
||||
|
||||
func TestTryLockSeparatesNames(t *testing.T) {
|
||||
t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
|
||||
|
||||
first, err := TryLock("dms-test-a")
|
||||
require.NoError(t, err)
|
||||
defer first.Release()
|
||||
|
||||
second, err := TryLock("dms-test-b")
|
||||
require.NoError(t, err)
|
||||
second.Release()
|
||||
}
|
||||
|
||||
func TestTryLockSeparatesWaylandDisplays(t *testing.T) {
|
||||
t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
|
||||
|
||||
t.Setenv("WAYLAND_DISPLAY", "wayland-1")
|
||||
first, err := TryLock(SelectionOverlayLock)
|
||||
require.NoError(t, err)
|
||||
defer first.Release()
|
||||
|
||||
t.Setenv("WAYLAND_DISPLAY", "wayland-2")
|
||||
second, err := TryLock(SelectionOverlayLock)
|
||||
require.NoError(t, err)
|
||||
second.Release()
|
||||
}
|
||||
|
||||
func TestTryLockWithoutWaylandDisplay(t *testing.T) {
|
||||
t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
|
||||
t.Setenv("WAYLAND_DISPLAY", "")
|
||||
os.Unsetenv("WAYLAND_DISPLAY")
|
||||
|
||||
unset, err := TryLock(SelectionOverlayLock)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fmt.Sprintf("%s-%d-wayland-0.lock", SelectionOverlayLock, os.Getuid()), filepath.Base(lockPath(SelectionOverlayLock)))
|
||||
|
||||
t.Setenv("WAYLAND_DISPLAY", "wayland-1")
|
||||
other, err := TryLock(SelectionOverlayLock)
|
||||
require.NoError(t, err)
|
||||
other.Release()
|
||||
|
||||
os.Unsetenv("WAYLAND_DISPLAY")
|
||||
_, err = TryLock(SelectionOverlayLock)
|
||||
assert.ErrorIs(t, err, ErrLockHeld)
|
||||
|
||||
unset.Release()
|
||||
}
|
||||
|
||||
func TestDisplayScopeStaysASingleName(t *testing.T) {
|
||||
cases := []struct{ display, want string }{
|
||||
{"", "wayland-0"},
|
||||
{"/", "wayland-0"},
|
||||
{"..", "wayland-0"},
|
||||
{"wayland-1", "wayland-1"},
|
||||
{"/run/user/1000/wayland-2", "wayland-2"},
|
||||
{"/run/user/1000/wayland-3/", "wayland-3"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Setenv("WAYLAND_DISPLAY", tc.display)
|
||||
assert.Equal(t, tc.want, displayScope(), tc.display)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryLockExcludesOtherProcesses(t *testing.T) {
|
||||
if _, err := exec.LookPath("flock"); err != nil {
|
||||
t.Skip("flock not available")
|
||||
}
|
||||
t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
|
||||
|
||||
held, err := TryLock(SelectionOverlayLock)
|
||||
require.NoError(t, err)
|
||||
|
||||
path := lockPath(SelectionOverlayLock)
|
||||
assert.Error(t, exec.Command("flock", "-n", "-x", path, "-c", "true").Run())
|
||||
|
||||
held.Release()
|
||||
assert.NoError(t, exec.Command("flock", "-n", "-x", path, "-c", "true").Run())
|
||||
}
|
||||
|
||||
func TestLockSelectionOverlayReportsHolder(t *testing.T) {
|
||||
t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
|
||||
|
||||
held, err := LockSelectionOverlay()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, held)
|
||||
defer held.Release()
|
||||
|
||||
lock, err := LockSelectionOverlay()
|
||||
assert.Nil(t, lock)
|
||||
assert.ErrorIs(t, err, ErrOverlayActive)
|
||||
assert.Contains(t, err.Error(), fmt.Sprintf("pid %d", os.Getpid()))
|
||||
}
|
||||
|
||||
func TestLockSelectionOverlayFallsBackWhenUnavailable(t *testing.T) {
|
||||
t.Setenv("XDG_RUNTIME_DIR", filepath.Join(t.TempDir(), "missing"))
|
||||
|
||||
lock, err := LockSelectionOverlay()
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, lock)
|
||||
lock.Release()
|
||||
}
|
||||
@@ -8,6 +8,12 @@ import (
|
||||
"github.com/AvengeMedia/dankgo/paths"
|
||||
)
|
||||
|
||||
var app = paths.New("danklinux")
|
||||
|
||||
func App() paths.App { return app }
|
||||
|
||||
func RuntimeDir() string { return app.SocketDir() }
|
||||
|
||||
func XDGStateHome() string { return paths.XDGStateHome() }
|
||||
|
||||
func XDGDataHome() string { return paths.XDGDataHome() }
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package keymap
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/dankgo/wayland/client"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const FormatXkbV1 = 1
|
||||
|
||||
// wl_keyboard.key carries evdev scancodes; xkb keycodes sit 8 above them.
|
||||
const evdevOffset = 8
|
||||
|
||||
var pc105Codes = map[string]uint32{
|
||||
"Escape": 1,
|
||||
"p": 25,
|
||||
"Return": 28,
|
||||
"Control_L": 29,
|
||||
"Shift_L": 42,
|
||||
"v": 47,
|
||||
"Alt_L": 56,
|
||||
"space": 57,
|
||||
"KP_Enter": 96,
|
||||
"Control_R": 97,
|
||||
"Alt_R": 100,
|
||||
}
|
||||
|
||||
var pc105Syms = invert(pc105Codes)
|
||||
|
||||
// xkbcommon may serialize keysyms as hex instead of names.
|
||||
var keysymNames = map[uint32]string{
|
||||
0x20: "space",
|
||||
0x70: "p",
|
||||
0x76: "v",
|
||||
0xff0d: "Return",
|
||||
0xff1b: "Escape",
|
||||
0xff8d: "KP_Enter",
|
||||
0xffe1: "Shift_L",
|
||||
0xffe3: "Control_L",
|
||||
0xffe4: "Control_R",
|
||||
0xffe9: "Alt_L",
|
||||
0xffea: "Alt_R",
|
||||
}
|
||||
|
||||
var (
|
||||
keycodeDefRe = regexp.MustCompile(`<([A-Za-z0-9+_-]+)>\s*=\s*(\d+)`)
|
||||
keySymbolsRe = regexp.MustCompile(`key\s*<([A-Za-z0-9+_-]+)>\s*\{([^}]*)\}`)
|
||||
groupIndexRe = regexp.MustCompile(`\w+\[\d+\]\s*=`)
|
||||
symbolListRe = regexp.MustCompile(`\[([^\]]*)\]`)
|
||||
)
|
||||
|
||||
// Keymap maps evdev scancodes to their first-group, first-level keysym names.
|
||||
// A nil Keymap answers with pc105 positions.
|
||||
type Keymap struct {
|
||||
symByCode map[uint32]string
|
||||
codeBySym map[string]uint32
|
||||
}
|
||||
|
||||
// FromEvent owns and closes the keymap fd; nil means no usable xkb keymap.
|
||||
func FromEvent(e client.KeyboardKeymapEvent) *Keymap {
|
||||
defer unix.Close(e.Fd)
|
||||
if e.Format != FormatXkbV1 {
|
||||
return nil
|
||||
}
|
||||
text, err := Read(e.Fd, e.Size)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return Parse(text)
|
||||
}
|
||||
|
||||
func Read(fd int, size uint32) (string, error) {
|
||||
data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_PRIVATE)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
text := strings.TrimRight(string(data), "\x00")
|
||||
return text, unix.Munmap(data)
|
||||
}
|
||||
|
||||
func Parse(text string) *Keymap {
|
||||
xkbCodes := map[string]uint32{}
|
||||
for _, m := range keycodeDefRe.FindAllStringSubmatch(text, -1) {
|
||||
if code, err := strconv.Atoi(m[2]); err == nil {
|
||||
xkbCodes[m[1]] = uint32(code)
|
||||
}
|
||||
}
|
||||
|
||||
k := &Keymap{symByCode: map[uint32]string{}, codeBySym: map[string]uint32{}}
|
||||
for _, m := range keySymbolsRe.FindAllStringSubmatch(text, -1) {
|
||||
group := symbolListRe.FindStringSubmatch(groupIndexRe.ReplaceAllString(m[2], ""))
|
||||
if group == nil {
|
||||
continue
|
||||
}
|
||||
xkbCode, ok := xkbCodes[m[1]]
|
||||
if !ok || xkbCode < evdevOffset {
|
||||
continue
|
||||
}
|
||||
sym := canonicalKeysym(strings.TrimSpace(strings.Split(group[1], ",")[0]))
|
||||
if sym == "" {
|
||||
continue
|
||||
}
|
||||
code := xkbCode - evdevOffset
|
||||
k.symByCode[code] = sym
|
||||
if _, seen := k.codeBySym[sym]; !seen {
|
||||
k.codeBySym[sym] = code
|
||||
}
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
func (k *Keymap) Keysym(code uint32) string {
|
||||
if k == nil {
|
||||
return pc105Syms[code]
|
||||
}
|
||||
if sym, ok := k.symByCode[code]; ok {
|
||||
return sym
|
||||
}
|
||||
return pc105Syms[code]
|
||||
}
|
||||
|
||||
func (k *Keymap) Keycode(sym string) uint32 {
|
||||
if k == nil {
|
||||
return pc105Codes[sym]
|
||||
}
|
||||
if code, ok := k.codeBySym[sym]; ok {
|
||||
return code
|
||||
}
|
||||
return pc105Codes[sym]
|
||||
}
|
||||
|
||||
func canonicalKeysym(sym string) string {
|
||||
if !strings.HasPrefix(sym, "0x") && !strings.HasPrefix(sym, "0X") {
|
||||
return sym
|
||||
}
|
||||
value, err := strconv.ParseUint(sym[2:], 16, 32)
|
||||
if err != nil {
|
||||
return sym
|
||||
}
|
||||
if name, ok := keysymNames[uint32(value)]; ok {
|
||||
return name
|
||||
}
|
||||
return sym
|
||||
}
|
||||
|
||||
func invert(m map[string]uint32) map[uint32]string {
|
||||
out := make(map[uint32]string, len(m))
|
||||
for sym, code := range m {
|
||||
out[code] = sym
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package keymap
|
||||
|
||||
import "testing"
|
||||
|
||||
const qwertyKeymap = `xkb_keymap {
|
||||
xkb_keycodes "(unnamed)" {
|
||||
minimum = 8;
|
||||
maximum = 708;
|
||||
<ESC> = 9;
|
||||
<AB04> = 55;
|
||||
<LCTL> = 37;
|
||||
<LFSH> = 50;
|
||||
alias <AL01> = <AC01>;
|
||||
indicator 1 = "Caps Lock";
|
||||
};
|
||||
xkb_types "(unnamed)" {
|
||||
type "ALPHABETIC" {
|
||||
modifiers = Shift+Lock;
|
||||
map[Shift] = Level2;
|
||||
level_name[Level1] = "Base";
|
||||
};
|
||||
};
|
||||
xkb_symbols "(unnamed)" {
|
||||
key <ESC> { [ Escape ] };
|
||||
key <AB04> { type= "ALPHABETIC", [ v, V ] };
|
||||
key <LCTL> { [ Control_L ] };
|
||||
key <LFSH> { [ Shift_L ] };
|
||||
};
|
||||
};`
|
||||
|
||||
const hexKeymap = `xkb_keymap {
|
||||
xkb_keycodes "(unnamed)" {
|
||||
<AB04> = 56;
|
||||
<LCTL> = 38;
|
||||
<LFSH> = 51;
|
||||
};
|
||||
xkb_symbols "(unnamed)" {
|
||||
key <LCTL> { [ 0xffe3 ] };
|
||||
key <LFSH> {
|
||||
type= "PC_ALT_LEVEL2",
|
||||
symbols[1]= [ 0xffe1, 0xfe08 ]
|
||||
};
|
||||
key <AB04> { [ 0x76, 0x56 ] };
|
||||
};
|
||||
};`
|
||||
|
||||
const dvorakKeymap = `xkb_keymap {
|
||||
xkb_keycodes "(unnamed)" {
|
||||
<AB09> = 60;
|
||||
<LCTL> = 37;
|
||||
<LFSH> = 50;
|
||||
};
|
||||
xkb_symbols "(unnamed)" {
|
||||
key <AB09> { [ v, V ] };
|
||||
key <LCTL> { [ Control_L ] };
|
||||
key <LFSH> { [ Shift_L ] };
|
||||
};
|
||||
};`
|
||||
|
||||
const swapEscapeKeymap = `xkb_keymap {
|
||||
xkb_keycodes "(unnamed)" {
|
||||
<ESC> = 9;
|
||||
<RTRN> = 36;
|
||||
<CAPS> = 66;
|
||||
<KPEN> = 104;
|
||||
};
|
||||
xkb_symbols "(unnamed)" {
|
||||
key <ESC> { [ Caps_Lock ] };
|
||||
key <RTRN> { [ Return ] };
|
||||
key <CAPS> {
|
||||
type= "ONE_LEVEL",
|
||||
symbols[1]= [ Escape ]
|
||||
};
|
||||
key <KPEN> { [ KP_Enter ] };
|
||||
};
|
||||
};`
|
||||
|
||||
func TestKeycode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keymap string
|
||||
sym string
|
||||
want uint32
|
||||
}{
|
||||
{"qwerty v", qwertyKeymap, "v", 47},
|
||||
{"qwerty ctrl", qwertyKeymap, "Control_L", 29},
|
||||
{"qwerty shift", qwertyKeymap, "Shift_L", 42},
|
||||
{"dvorak v", dvorakKeymap, "v", 52},
|
||||
{"hex v", hexKeymap, "v", 48},
|
||||
{"hex ctrl", hexKeymap, "Control_L", 30},
|
||||
{"hex shift", hexKeymap, "Shift_L", 43},
|
||||
{"swapescape escape", swapEscapeKeymap, "Escape", 58},
|
||||
{"missing falls back to pc105", dvorakKeymap, "Escape", 1},
|
||||
{"empty falls back to pc105", "", "v", 47},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Parse(tt.keymap).Keycode(tt.sym); got != tt.want {
|
||||
t.Errorf("Keycode(%q) = %d, want %d", tt.sym, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeysym(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keymap string
|
||||
code uint32
|
||||
want string
|
||||
}{
|
||||
{"swapescape caps yields Escape", swapEscapeKeymap, 58, "Escape"},
|
||||
{"swapescape esc yields Caps_Lock", swapEscapeKeymap, 1, "Caps_Lock"},
|
||||
{"swapescape return", swapEscapeKeymap, 28, "Return"},
|
||||
{"swapescape keypad enter", swapEscapeKeymap, 96, "KP_Enter"},
|
||||
{"qwerty escape", qwertyKeymap, 1, "Escape"},
|
||||
{"hex ctrl", hexKeymap, 30, "Control_L"},
|
||||
{"unmapped code falls back to pc105", qwertyKeymap, 28, "Return"},
|
||||
{"unknown code is empty", qwertyKeymap, 200, ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Parse(tt.keymap).Keysym(tt.code); got != tt.want {
|
||||
t.Errorf("Keysym(%d) = %q, want %q", tt.code, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilKeymapUsesPc105(t *testing.T) {
|
||||
var k *Keymap
|
||||
if got := k.Keysym(1); got != "Escape" {
|
||||
t.Errorf("Keysym(1) = %q, want Escape", got)
|
||||
}
|
||||
if got := k.Keycode("Control_L"); got != 29 {
|
||||
t.Errorf("Keycode(Control_L) = %d, want 29", got)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
Marble Tabby
|
||||
Heisenberg
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
function socketPath(path, runtime) {
|
||||
if (!runtime.startsWith("/") || runtime.endsWith("/") || !path.startsWith(runtime + "/aqueous/"))
|
||||
return false;
|
||||
if (/[\r\n\u0000]/.test(path) || path.includes("//") || path.split("/").some(part => part === "." || part === ".."))
|
||||
return false;
|
||||
return /^\/aqueous\/[^/]+\/ipc\.sock$/.test(path.slice(runtime.length));
|
||||
}
|
||||
|
||||
function byteLength(text) {
|
||||
return encodeURIComponent(text).replace(/%[0-9A-F]{2}/g, "x").length;
|
||||
}
|
||||
|
||||
function object(value) {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function decimal(value) {
|
||||
return typeof value === "string" && /^[0-9]{1,20}$/.test(value);
|
||||
}
|
||||
|
||||
function nextId(value) {
|
||||
const digits = value.split("");
|
||||
for (let i = digits.length - 1; i >= 0; i--) {
|
||||
if (digits[i] !== "9") {
|
||||
digits[i] = String(Number(digits[i]) + 1);
|
||||
return digits.join("");
|
||||
}
|
||||
digits[i] = "0";
|
||||
}
|
||||
if (digits.length === 20)
|
||||
throw new Error("IPC request IDs exhausted");
|
||||
return "1" + digits.join("");
|
||||
}
|
||||
|
||||
function hello(value) {
|
||||
if (!object(value) || value.schema !== 1 || typeof value.session !== "string" || !/^[0-9a-f]{32}$/.test(value.session) || value.max_pending_requests !== 1)
|
||||
throw new Error("unsupported: Aqueous IPC hello");
|
||||
const limits = {max_request_bytes: 65536, max_frame_bytes: 4259840, max_batch_bytes: 4194304};
|
||||
for (const key of Object.keys(limits)) {
|
||||
if (!Number.isInteger(value[key]) || value[key] <= 0 || value[key] > limits[key])
|
||||
throw new Error("unsupported: IPC " + key);
|
||||
}
|
||||
if (!object(value.capabilities))
|
||||
throw new Error("unsupported: IPC capabilities");
|
||||
for (const name of ["state", "commands", "keyboard", "overview", "shortcut_inhibition"]) {
|
||||
if (typeof value.capabilities[name] !== "boolean")
|
||||
throw new Error("unsupported: missing capability " + name);
|
||||
}
|
||||
if (!value.capabilities.state)
|
||||
throw new Error("unsupported: shell state");
|
||||
return value;
|
||||
}
|
||||
|
||||
function envelope(line, limit) {
|
||||
if (byteLength(line) > limit)
|
||||
throw new Error("IPC frame exceeds size bound");
|
||||
let depth = 0, quoted = false, escaped = false;
|
||||
for (const c of line) {
|
||||
if (quoted) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
switch (c) {
|
||||
case "\\":
|
||||
escaped = true;
|
||||
break;
|
||||
case '"':
|
||||
quoted = false;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
switch (c) {
|
||||
case '"':
|
||||
quoted = true;
|
||||
break;
|
||||
case "{":
|
||||
case "[":
|
||||
depth++;
|
||||
break;
|
||||
case "}":
|
||||
case "]":
|
||||
depth--;
|
||||
break;
|
||||
}
|
||||
if (depth > 32)
|
||||
throw new Error("IPC nesting exceeds limit");
|
||||
}
|
||||
const value = JSON.parse(line);
|
||||
if (!object(value) || value.ipc !== 1)
|
||||
throw new Error("unsupported: IPC envelope");
|
||||
return value;
|
||||
}
|
||||
|
||||
function response(value, id) {
|
||||
if (value.id !== id || !decimal(value.id) || typeof value.ok !== "boolean" || value.event !== undefined)
|
||||
throw new Error("invalid IPC response correlation");
|
||||
if (value.ok) {
|
||||
if (!object(value.result) || value.error !== undefined)
|
||||
throw new Error("invalid IPC result");
|
||||
return "";
|
||||
}
|
||||
if (!object(value.error) || typeof value.error.code !== "string" || !value.error.code || value.error.code.length > 128 || typeof value.error.message !== "string" || byteLength(value.error.message) > 4096 || value.result !== undefined)
|
||||
throw new Error("malformed IPC error");
|
||||
return value.error.code + ": " + value.error.message;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
.pragma library
|
||||
|
||||
// A calendar date with no time and no timezone.
|
||||
//
|
||||
// Arithmetic runs through Date.UTC, which has no DST gaps, so day and month
|
||||
// rollover is exact. Instances are immutable: every operation returns a new one.
|
||||
// month is 0-based, matching Javascripts' Date behavior.
|
||||
|
||||
class DateOnly {
|
||||
constructor(year, month, day) {
|
||||
this.year = year;
|
||||
this.month = month;
|
||||
this.day = day;
|
||||
}
|
||||
|
||||
addDays(days) {
|
||||
return _fromUtc(new Date(Date.UTC(this.year, this.month, this.day + days)));
|
||||
}
|
||||
|
||||
addMonths(months) {
|
||||
const target = new Date(Date.UTC(this.year, this.month + months, 1));
|
||||
const lastDay = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
|
||||
return new DateOnly(target.getUTCFullYear(), target.getUTCMonth(), Math.min(this.day, lastDay));
|
||||
}
|
||||
|
||||
// 0 = Sunday, matching Date.getDay().
|
||||
dayOfWeek() {
|
||||
return new Date(Date.UTC(this.year, this.month, this.day)).getUTCDay();
|
||||
}
|
||||
|
||||
daysUntil(other) {
|
||||
const from = Date.UTC(this.year, this.month, this.day);
|
||||
const to = Date.UTC(other.year, other.month, other.day);
|
||||
return Math.round((to - from) / 86400000);
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
if (!other)
|
||||
return false;
|
||||
return this.year === other.year && this.month === other.month && this.day === other.day;
|
||||
}
|
||||
|
||||
// Local noon, so consumers that read local calendar fields land on this date
|
||||
// even on days where local midnight does not exist.
|
||||
toDate() {
|
||||
return new Date(this.year, this.month, this.day, 12);
|
||||
}
|
||||
}
|
||||
|
||||
function _fromUtc(date) {
|
||||
return new DateOnly(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
||||
}
|
||||
|
||||
function of(year, month, day) {
|
||||
return new DateOnly(year, month, day);
|
||||
}
|
||||
|
||||
function fromDate(date) {
|
||||
return new DateOnly(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
}
|
||||
|
||||
function firstOfMonth(year, month) {
|
||||
return new DateOnly(year, month, 1);
|
||||
}
|
||||
|
||||
function lastOfMonth(year, month) {
|
||||
return _fromUtc(new Date(Date.UTC(year, month + 1, 0)));
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
.pragma library
|
||||
|
||||
function path(schema, key) {
|
||||
return "/" + schema.replace(/\./g, "/") + "/" + key;
|
||||
}
|
||||
|
||||
// gsettings reaches whichever backend its own GLib links, not necessarily the
|
||||
// dconf store GTK apps read, and it exits 0 either way.
|
||||
function backendIsDconf() {
|
||||
return "[ \"${GSETTINGS_BACKEND:-dconf}\" = dconf ]";
|
||||
}
|
||||
|
||||
function getCmd(schema, key) {
|
||||
return "{ { " + backendIsDconf() + " && dconf read " + path(schema, key) + " 2>/dev/null | grep .; } || gsettings get " + schema + " " + key + " 2>/dev/null; } | tr -d \"'\"";
|
||||
}
|
||||
|
||||
function setCmd(schema, key, value) {
|
||||
return "{ " + backendIsDconf() + " && dconf write " + path(schema, key) + " \"'" + value + "'\" 2>/dev/null; } || gsettings set " + schema + " " + key + " '" + value + "'";
|
||||
}
|
||||
+147
-115
@@ -8,6 +8,7 @@ import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Common.settings
|
||||
import qs.Services
|
||||
import "GSettings.js" as GSettings
|
||||
import "settings/SettingsSpec.js" as Spec
|
||||
import "settings/SettingsStore.js" as Store
|
||||
|
||||
@@ -15,7 +16,7 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("SettingsData")
|
||||
|
||||
readonly property int settingsConfigVersion: 17
|
||||
readonly property int settingsConfigVersion: 18
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
@@ -987,37 +988,78 @@ Singleton {
|
||||
property var notificationRules: []
|
||||
property bool notificationDndAllowCritical: true
|
||||
property bool notificationFocusedMonitor: false
|
||||
// Names the bar config rendered as Dank Island instead of a DankBar. Empty = no island.
|
||||
property string dankIslandBarId: ""
|
||||
readonly property var islandBarConfig: dankIslandBarId ? (barConfigs.find(cfg => cfg.id === dankIslandBarId) || null) : null
|
||||
readonly property bool dankIslandEnabled: !!islandBarConfig && (islandBarConfig.enabled ?? false)
|
||||
readonly property int dankIslandPosition: islandBarConfig?.position ?? SettingsData.Position.Top
|
||||
readonly property string dankIslandEdge: dankIslandPosition === SettingsData.Position.Bottom ? "bottom" : "top"
|
||||
readonly property int dankIslandReservedStripHeight: {
|
||||
const reserve = Math.max(24, Math.min(128, dankIslandReserveHeight));
|
||||
const compact = Math.max(24, Math.min(72, dankIslandCompactHeight));
|
||||
const gap = Math.max(0, Math.min(48, dankIslandOuterGap));
|
||||
// Island is a per-instance render mode: any bar config with island:true draws as an island
|
||||
// instead of a DankBar, and carries its own island* look settings.
|
||||
readonly property var islandBarConfigs: {
|
||||
barConfigs;
|
||||
return (barConfigs || []).filter(cfg => cfg && cfg.island === true);
|
||||
}
|
||||
readonly property bool dankIslandEnabled: islandBarConfigs.some(cfg => cfg.enabled ?? false)
|
||||
readonly property var islandDefaults: ({
|
||||
"islandFloating": false,
|
||||
"islandUseOverlayLayer": false,
|
||||
"islandReserveThickness": 40,
|
||||
"islandCompactThickness": 38,
|
||||
"islandOuterGap": 4,
|
||||
"islandAlongOffset": 0,
|
||||
"islandInteractionMode": "hybrid",
|
||||
"islandHoverOpenDelay": 150,
|
||||
"islandHoverCloseDelay": 150,
|
||||
"islandPalette": "default",
|
||||
"islandTransparency": 1,
|
||||
"islandCornerRadius": 34,
|
||||
"islandHighContrast": false,
|
||||
"islandMediaClockVisible": true,
|
||||
"islandNotificationBadgeClearOnOpen": false,
|
||||
"islandNotificationExpand": false,
|
||||
"islandHomeCompactTight": false,
|
||||
"islandHomeClockDisplay": "both",
|
||||
"islandHomeVolumeDisplay": "both",
|
||||
"islandHomeBrightnessDisplay": "both",
|
||||
"islandBatteryStyle": "solid",
|
||||
"islandSatellitesEnabled": true,
|
||||
"islandSatellitePosition": "edges",
|
||||
"islandSatelliteGap": 12,
|
||||
"islandSatelliteBackground": false,
|
||||
"islandSatelliteGothCorners": true,
|
||||
"islandSatelliteTransparency": 1,
|
||||
"islandSatelliteSwoopRadius": 24,
|
||||
"islandReducedMotion": false,
|
||||
"islandSpringStiffness": 560,
|
||||
"islandSpringDamping": 37,
|
||||
"islandSpringMass": 1
|
||||
})
|
||||
|
||||
function islandSetting(bc, key) {
|
||||
const value = bc?.[key];
|
||||
return value === undefined || value === null ? islandDefaults[key] : value;
|
||||
}
|
||||
|
||||
function islandLevelDisplay(bc, key) {
|
||||
const value = islandSetting(bc, key);
|
||||
return value === "icon" || value === "percentage" || value === "both" ? value : "both";
|
||||
}
|
||||
|
||||
function islandClockDisplay(bc) {
|
||||
const value = islandSetting(bc, "islandHomeClockDisplay");
|
||||
return value === "time" || value === "date" || value === "both" ? value : "both";
|
||||
}
|
||||
|
||||
function islandEdge(bc) {
|
||||
return positionToSide(bc?.position ?? SettingsData.Position.Top) || "top";
|
||||
}
|
||||
|
||||
function islandVertical(bc) {
|
||||
const edge = islandEdge(bc);
|
||||
return edge === "left" || edge === "right";
|
||||
}
|
||||
|
||||
function islandStripThickness(bc) {
|
||||
const reserve = Math.max(24, Math.min(128, islandSetting(bc, "islandReserveThickness")));
|
||||
const compact = Math.max(24, Math.min(72, islandSetting(bc, "islandCompactThickness")));
|
||||
const gap = Math.max(0, Math.min(48, islandSetting(bc, "islandOuterGap")));
|
||||
return Math.max(reserve, gap + compact);
|
||||
}
|
||||
property bool dankIslandFloating: false
|
||||
property bool dankIslandUseOverlayLayer: false
|
||||
property int dankIslandReserveHeight: 40
|
||||
property int dankIslandCompactHeight: 38
|
||||
property int dankIslandOuterGap: 4
|
||||
property int dankIslandHorizontalOffset: 0
|
||||
property string dankIslandInteractionMode: "hybrid"
|
||||
property int dankIslandHoverOpenDelay: 150
|
||||
property int dankIslandHoverCloseDelay: 150
|
||||
property string dankIslandPalette: "default"
|
||||
property real dankIslandTransparency: 1
|
||||
property int dankIslandCornerRadius: 34
|
||||
property bool dankIslandHighContrast: false
|
||||
property bool dankIslandMediaClockVisible: true
|
||||
property bool dankIslandNotificationBadgeClearOnOpen: false
|
||||
property bool dankIslandNotificationExpand: false
|
||||
property bool dankIslandHomeCompactTight: false
|
||||
property string dankIslandHomeVolumeDisplay: "both"
|
||||
property string dankIslandHomeBrightnessDisplay: "both"
|
||||
readonly property var _islandHomeGroupIds: ["media", "clock", "weather", "status", "volume", "brightness", "notifications"]
|
||||
readonly property var _islandHomeLayoutDefault: [
|
||||
{
|
||||
@@ -1049,10 +1091,8 @@ Singleton {
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
property var dankIslandHomeLayout: _islandHomeLayoutDefault
|
||||
|
||||
function getIslandHomeLayout() {
|
||||
const stored = Array.isArray(dankIslandHomeLayout) ? dankIslandHomeLayout : [];
|
||||
function getIslandHomeLayout(bc) {
|
||||
const stored = Array.isArray(bc?.islandHomeLayout) ? bc.islandHomeLayout : [];
|
||||
const result = [];
|
||||
const seen = {};
|
||||
for (const entry of stored) {
|
||||
@@ -1075,41 +1115,33 @@ Singleton {
|
||||
return result;
|
||||
}
|
||||
|
||||
function islandHomeGroupEnabled(id) {
|
||||
const entry = getIslandHomeLayout().find(g => g.id === id);
|
||||
function islandHomeGroupEnabled(bc, id) {
|
||||
const entry = getIslandHomeLayout(bc).find(g => g.id === id);
|
||||
return entry ? entry.enabled : false;
|
||||
}
|
||||
|
||||
function setIslandHomeLayoutOrder(ids) {
|
||||
const current = getIslandHomeLayout();
|
||||
function setIslandHomeLayoutOrder(barId, ids) {
|
||||
const current = getIslandHomeLayout(getBarConfig(barId));
|
||||
const ordered = ids.map(id => current.find(g => g.id === id)).filter(g => g);
|
||||
for (const entry of current) {
|
||||
if (ids.indexOf(entry.id) < 0)
|
||||
ordered.push(entry);
|
||||
}
|
||||
set("dankIslandHomeLayout", ordered);
|
||||
updateBarConfig(barId, {
|
||||
islandHomeLayout: ordered
|
||||
});
|
||||
}
|
||||
|
||||
function setIslandHomeGroupEnabled(id, on) {
|
||||
function setIslandHomeGroupEnabled(barId, id, on) {
|
||||
if (id === "clock")
|
||||
return;
|
||||
set("dankIslandHomeLayout", getIslandHomeLayout().map(g => g.id === id ? {
|
||||
"id": g.id,
|
||||
"enabled": on
|
||||
} : g));
|
||||
updateBarConfig(barId, {
|
||||
islandHomeLayout: getIslandHomeLayout(getBarConfig(barId)).map(g => g.id === id ? {
|
||||
"id": g.id,
|
||||
"enabled": on
|
||||
} : g)
|
||||
});
|
||||
}
|
||||
property string dankIslandBatteryStyle: "solid"
|
||||
property bool dankIslandSatellitesEnabled: true
|
||||
property string dankIslandSatellitePosition: "edges"
|
||||
property int dankIslandSatelliteGap: 12
|
||||
property bool dankIslandSatelliteBackground: false
|
||||
property bool dankIslandSatelliteGothCorners: true
|
||||
property real dankIslandSatelliteTransparency: 1
|
||||
property int dankIslandSatelliteSwoopRadius: 24
|
||||
property bool dankIslandReducedMotion: false
|
||||
property real dankIslandSpringStiffness: 560
|
||||
property real dankIslandSpringDamping: 37
|
||||
property real dankIslandSpringMass: 1
|
||||
|
||||
property bool osdAlwaysShowValue: false
|
||||
property int osdPosition: SettingsData.Position.BottomCenter
|
||||
@@ -1523,11 +1555,7 @@ Singleton {
|
||||
return;
|
||||
if (!SessionData.lastAppliedIconTheme)
|
||||
return;
|
||||
const script = `if command -v gsettings >/dev/null 2>&1; then
|
||||
gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed "s/'//g"
|
||||
elif command -v dconf >/dev/null 2>&1; then
|
||||
dconf read /org/gnome/desktop/interface/icon-theme 2>/dev/null | sed "s/'//g"
|
||||
fi`;
|
||||
const script = GSettings.getCmd("org.gnome.desktop.interface", "icon-theme");
|
||||
|
||||
Proc.runCommand("iconThemeDriftCheck", ["sh", "-c", script], (output, exitCode) => {
|
||||
const platform = (output || "").trim();
|
||||
@@ -1565,11 +1593,7 @@ Singleton {
|
||||
const resolved = resolveIconTheme();
|
||||
let cosmicThemeName = (resolved === "System Default") ? systemDefaultIconTheme : resolved;
|
||||
if (!cosmicThemeName || cosmicThemeName === "System Default") {
|
||||
const detectScript = `if command -v gsettings >/dev/null 2>&1; then
|
||||
gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed "s/'//g"
|
||||
elif command -v dconf >/dev/null 2>&1; then
|
||||
dconf read /org/gnome/desktop/interface/icon-theme 2>/dev/null | sed "s/'//g"
|
||||
fi`;
|
||||
const detectScript = GSettings.getCmd("org.gnome.desktop.interface", "icon-theme");
|
||||
|
||||
Proc.runCommand("detectCosmicIconTheme", ["sh", "-c", detectScript], (output, exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
@@ -1631,11 +1655,7 @@ Singleton {
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v gsettings >/dev/null 2>&1; then
|
||||
gsettings set org.gnome.desktop.interface icon-theme '${gtkThemeName}' 2>/dev/null || true
|
||||
elif command -v dconf >/dev/null 2>&1; then
|
||||
dconf write /org/gnome/desktop/interface/icon-theme "'${gtkThemeName}'" 2>/dev/null || true
|
||||
fi
|
||||
${GSettings.setCmd("org.gnome.desktop.interface", "icon-theme", gtkThemeName)} || true
|
||||
|
||||
pkill -HUP -f 'gtk' 2>/dev/null || true`;
|
||||
|
||||
@@ -1738,13 +1758,21 @@ Singleton {
|
||||
})
|
||||
|
||||
function set(key, value) {
|
||||
if (key === "dankIslandInteractionMode" && value !== "click" && value !== "hybrid")
|
||||
value = "hybrid";
|
||||
Spec.set(root, key, value, saveSettings, _hooks);
|
||||
if (key === "dankIslandBarId" && value && frameEnabled)
|
||||
set("frameEnabled", false);
|
||||
if (key === "frameEnabled" && value && dankIslandBarId)
|
||||
set("dankIslandBarId", "");
|
||||
if (key === "frameEnabled" && value)
|
||||
clearIslandBars();
|
||||
}
|
||||
|
||||
// Frame mode hosts bars inside the frame surface, which has nowhere to put an island.
|
||||
function clearIslandBars() {
|
||||
const islands = islandBarConfigs;
|
||||
if (islands.length === 0)
|
||||
return;
|
||||
const configs = JSON.parse(JSON.stringify(barConfigs));
|
||||
for (const cfg of configs)
|
||||
delete cfg.island;
|
||||
barConfigs = configs;
|
||||
updateBarConfigs();
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
@@ -1799,8 +1827,8 @@ Singleton {
|
||||
Store.parse(root, obj);
|
||||
|
||||
// set() enforces this pair, but a hand-edited settings.json bypasses set() entirely.
|
||||
if (frameEnabled && dankIslandBarId)
|
||||
dankIslandBarId = "";
|
||||
if (frameEnabled && islandBarConfigs.length > 0)
|
||||
clearIslandBars();
|
||||
|
||||
if (obj?.directionalAnimationMode === 3 && frameMode !== "connected")
|
||||
frameMode = "connected";
|
||||
@@ -2131,7 +2159,7 @@ Singleton {
|
||||
const pathsArg = iconPaths.join(" ");
|
||||
|
||||
const script = `
|
||||
echo "SYSDEFAULT:$(gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed "s/'//g" || echo '')"
|
||||
echo "SYSDEFAULT:$(${GSettings.getCmd("org.gnome.desktop.interface", "icon-theme")})"
|
||||
for dir in ${pathsArg}; do
|
||||
[ -d "$dir" ] || continue
|
||||
for theme in "$dir"/*/; do
|
||||
@@ -2170,7 +2198,7 @@ Singleton {
|
||||
const pathsArg = cursorPaths.join(" ");
|
||||
|
||||
const script = `
|
||||
echo "SYSDEFAULT:$(gsettings get org.gnome.desktop.interface cursor-theme 2>/dev/null | sed "s/'//g" || echo '')"
|
||||
echo "SYSDEFAULT:$(${GSettings.getCmd("org.gnome.desktop.interface", "cursor-theme")})"
|
||||
for dir in ${pathsArg}; do
|
||||
[ -d "$dir" ] || continue
|
||||
for theme in "$dir"/*/; do
|
||||
@@ -2329,12 +2357,10 @@ Singleton {
|
||||
|
||||
// The island is not a bar, but it is chrome: popouts still have to clear its strip.
|
||||
if (!isIslandBarConfig(barConfig)) {
|
||||
const islandTop = dankIslandEdgeOffset(screen, "top");
|
||||
const islandBottom = dankIslandEdgeOffset(screen, "bottom");
|
||||
if (islandTop > 0)
|
||||
topBar = Math.max(topBar, islandTop);
|
||||
if (islandBottom > 0)
|
||||
bottomBar = Math.max(bottomBar, islandBottom);
|
||||
topBar = Math.max(topBar, dankIslandEdgeOffset(screen, "top"));
|
||||
bottomBar = Math.max(bottomBar, dankIslandEdgeOffset(screen, "bottom"));
|
||||
leftBar = Math.max(leftBar, dankIslandEdgeOffset(screen, "left"));
|
||||
rightBar = Math.max(rightBar, dankIslandEdgeOffset(screen, "right"));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -2470,21 +2496,20 @@ Singleton {
|
||||
return barConfigs.find(cfg => cfg.id === barId) || null;
|
||||
}
|
||||
|
||||
function setIslandBarId(barId) {
|
||||
function setBarIsland(barId, on) {
|
||||
const config = getBarConfig(barId);
|
||||
if (!config)
|
||||
if (!config || (config.island === true) === (on === true))
|
||||
return;
|
||||
set("dankIslandBarId", barId);
|
||||
const updates = {};
|
||||
if (!config.enabled)
|
||||
updates.enabled = true;
|
||||
if ((config.screenPreferences ?? []).length === 0)
|
||||
updates.screenPreferences = ["all"];
|
||||
const position = config.position ?? SettingsData.Position.Top;
|
||||
if (position !== SettingsData.Position.Top && position !== SettingsData.Position.Bottom)
|
||||
updates.position = SettingsData.Position.Top;
|
||||
if (Object.keys(updates).length > 0)
|
||||
updateBarConfig(barId, updates);
|
||||
const updates = {
|
||||
island: on === true
|
||||
};
|
||||
if (on === true) {
|
||||
if (!config.enabled)
|
||||
updates.enabled = true;
|
||||
if ((config.screenPreferences ?? []).length === 0)
|
||||
updates.screenPreferences = ["all"];
|
||||
}
|
||||
updateBarConfig(barId, updates);
|
||||
}
|
||||
|
||||
function isBarIpcRevealed(barId) {
|
||||
@@ -2494,7 +2519,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function setBarIpcReveal(barId, revealed) {
|
||||
if (!barId || barId === dankIslandBarId)
|
||||
if (!barId || isIslandBarConfig(getBarConfig(barId)))
|
||||
return;
|
||||
const nextRevealed = !!revealed;
|
||||
if (!!barIpcRevealStates[barId] === nextRevealed)
|
||||
@@ -2528,8 +2553,6 @@ Singleton {
|
||||
const index = configs.findIndex(cfg => cfg.id === barId);
|
||||
if (index === -1)
|
||||
return;
|
||||
if (isIslandBarConfig(configs[index]) && updates.position !== undefined && updates.position !== SettingsData.Position.Top && updates.position !== SettingsData.Position.Bottom)
|
||||
delete updates.position;
|
||||
const positionChanged = updates.position !== undefined && configs[index].position !== updates.position;
|
||||
if (updates.autoHide === false || updates.visible === false)
|
||||
setBarIpcReveal(barId, false);
|
||||
@@ -2548,8 +2571,6 @@ Singleton {
|
||||
return;
|
||||
const configs = barConfigs.filter(cfg => cfg.id !== barId);
|
||||
barConfigs = configs;
|
||||
if (dankIslandBarId === barId)
|
||||
dankIslandBarId = "";
|
||||
if (connectedFrameBarStyleBackups?.[barId] !== undefined) {
|
||||
const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {}));
|
||||
delete nextBackups[barId];
|
||||
@@ -2559,7 +2580,7 @@ Singleton {
|
||||
updateBarConfigs();
|
||||
}
|
||||
|
||||
// Bar-kind instances only. The island reserves its own edge via dankIslandOwnsEdge.
|
||||
// Bar-kind instances only. Islands reserve their own edges via dankIslandOwnsEdge.
|
||||
function getEnabledBarConfigs() {
|
||||
return barConfigs.filter(cfg => cfg.enabled && !isIslandBarConfig(cfg));
|
||||
}
|
||||
@@ -2728,34 +2749,45 @@ Singleton {
|
||||
}
|
||||
|
||||
function isIslandBarConfig(bc) {
|
||||
return !!bc && !!dankIslandBarId && bc.id === dankIslandBarId;
|
||||
return !!bc && bc.island === true;
|
||||
}
|
||||
|
||||
function getIslandScreens() {
|
||||
const cfg = islandBarConfig;
|
||||
if (!cfg || !(cfg.enabled ?? false))
|
||||
function activeIslandConfigsForScreen(screen) {
|
||||
if (!screen)
|
||||
return [];
|
||||
return Quickshell.screens.filter(screen => barConfigCoversScreen(cfg, screen));
|
||||
return islandBarConfigs.filter(cfg => (cfg.enabled ?? false) && barConfigCoversScreen(cfg, screen));
|
||||
}
|
||||
|
||||
// Only the first island claiming an edge renders there; the rest would stack on top of it.
|
||||
function islandConfigForEdge(screen, edge) {
|
||||
return activeIslandConfigsForScreen(screen).find(cfg => islandEdge(cfg) === edge) ?? null;
|
||||
}
|
||||
|
||||
function dankIslandCoversScreen(screen) {
|
||||
const cfg = islandBarConfig;
|
||||
return !!screen && !!cfg && (cfg.enabled ?? false) && barConfigCoversScreen(cfg, screen);
|
||||
return activeIslandConfigsForScreen(screen).length > 0;
|
||||
}
|
||||
|
||||
function dankIslandOwnsEdge(screen, edge) {
|
||||
return dankIslandCoversScreen(screen) && dankIslandEdge === edge;
|
||||
return islandConfigForEdge(screen, edge) !== null;
|
||||
}
|
||||
|
||||
// Painted strip thickness on `edge`, including when floating drops the exclusive zone.
|
||||
function dankIslandEdgeOffset(screen, edge) {
|
||||
return dankIslandOwnsEdge(screen, edge) ? dankIslandReservedStripHeight : 0;
|
||||
const cfg = islandConfigForEdge(screen, edge);
|
||||
return cfg ? islandStripThickness(cfg) : 0;
|
||||
}
|
||||
|
||||
function dankIslandIsSoleBarForScreen(screen) {
|
||||
return dankIslandCoversScreen(screen) && getActiveBarEdgesForScreen(screen).length === 0;
|
||||
}
|
||||
|
||||
// Edges an island already holds on every screen this config covers, so a second island
|
||||
// instance cannot be dropped on top of it.
|
||||
function islandEdgeTakenFor(bc, screen, edge) {
|
||||
const owner = islandConfigForEdge(screen, edge);
|
||||
return !!owner && owner.id !== bc?.id;
|
||||
}
|
||||
|
||||
function getActiveBarEdgesForScreen(screen) {
|
||||
return ["top", "bottom", "left", "right"].filter(edge => getActiveBarConfigsForEdge(screen, edge).length > 0);
|
||||
}
|
||||
|
||||
+11
-70
@@ -10,6 +10,7 @@ import qs.DankCommon.Common as DankCommon
|
||||
import qs.Services
|
||||
import qs.Modules.Greetd
|
||||
import "StockThemes.js" as StockThemes
|
||||
import "GSettings.js" as GSettings
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -144,10 +145,6 @@ Singleton {
|
||||
|
||||
Component.onCompleted: {
|
||||
Quickshell.execDetached(["mkdir", "-p", stateDir]);
|
||||
// shellDir may be an embedded-UI extraction, which is read-only and
|
||||
// unexecutable (dankgo shellapp/shellfs makeReadOnly chmods 0444)
|
||||
if (typeof SessionData === "undefined" || !SessionData.isGreeterMode)
|
||||
Quickshell.execDetached(["bash", shellDir + "/scripts/gtk.sh", configDir, "assets", "", shellDir]);
|
||||
Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => {
|
||||
matugenAvailable = (code === 0) && !envDisableMatugen;
|
||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
||||
@@ -297,32 +294,7 @@ Singleton {
|
||||
if (currentTheme === "custom") {
|
||||
return customThemeData || StockThemes.getThemeByName("purple", isLightMode);
|
||||
} else if (currentTheme === dynamic) {
|
||||
return {
|
||||
"primary": getMatugenColor("primary", "#42a5f5"),
|
||||
"primaryText": getMatugenColor("on_primary", "#ffffff"),
|
||||
"primaryContainer": getMatugenColor("primary_container", "#1976d2"),
|
||||
"secondary": getMatugenColor("secondary", "#8ab4f8"),
|
||||
"secondaryContainer": getMatugenColor("secondary_container", getMatugenColor("surface_container_high", "#292b2f")),
|
||||
"tertiary": getMatugenColor("tertiary", "#efb8c8"),
|
||||
"tertiaryContainer": getMatugenColor("tertiary_container", getMatugenColor("surface_container_high", "#292b2f")),
|
||||
"surface": getMatugenColor("surface", "#1a1c1e"),
|
||||
"surfaceText": getMatugenColor("on_background", "#e3e8ef"),
|
||||
"surfaceVariant": getMatugenColor("surface_variant", "#44464f"),
|
||||
"surfaceVariantText": getMatugenColor("on_surface_variant", "#c4c7c5"),
|
||||
"surfaceTint": getMatugenColor("surface_tint", "#8ab4f8"),
|
||||
"background": getMatugenColor("background", "#1a1c1e"),
|
||||
"backgroundText": getMatugenColor("on_background", "#e3e8ef"),
|
||||
"outline": getMatugenColor("outline", "#8e918f"),
|
||||
"surfaceContainerLowest": getMatugenColor("surface_container_lowest", "#0e1013"),
|
||||
"surfaceContainerLow": getMatugenColor("surface_container_low", "#181a1d"),
|
||||
"surfaceContainer": getMatugenColor("surface_container", "#1e2023"),
|
||||
"surfaceContainerHigh": getMatugenColor("surface_container_high", "#292b2f"),
|
||||
"surfaceContainerHighest": getMatugenColor("surface_container_highest", "#343740"),
|
||||
"error": "#F2B8B5",
|
||||
"warning": "#FF9800",
|
||||
"info": "#2196F3",
|
||||
"success": "#4CAF50"
|
||||
};
|
||||
return buildExtractedDynamicMode(isLightMode ? "light" : "dark", "Dynamic");
|
||||
} else {
|
||||
return StockThemes.getThemeByName(currentTheme, isLightMode);
|
||||
}
|
||||
@@ -1525,30 +1497,10 @@ Singleton {
|
||||
const dankBarScale = fontScale !== undefined ? fontScale : 1.0;
|
||||
const maxScale = (maximizeText ?? false) ? 1.5 : 1.0;
|
||||
if (scale <= 0.75)
|
||||
return evenLineBoxSize(Math.round(fontSizeSmall * 0.9 * dankBarScale * maxScale));
|
||||
return Math.round(fontSizeSmall * 0.9 * dankBarScale * maxScale);
|
||||
if (scale >= 1.25)
|
||||
return evenLineBoxSize(Math.round(fontSizeMedium * dankBarScale * maxScale));
|
||||
return evenLineBoxSize(Math.round(fontSizeSmall * dankBarScale * maxScale));
|
||||
}
|
||||
|
||||
FontMetrics {
|
||||
id: lineBoxProbe
|
||||
}
|
||||
|
||||
// Text.implicitHeight is ceil(FontMetrics.height); probe.font is written whole and read via boundingRect() so bindings capture no dependency on the probe
|
||||
function lineBoxHeight(pixelSize) {
|
||||
lineBoxProbe.font = Qt.font({
|
||||
family: fontFamily,
|
||||
weight: fontWeight,
|
||||
pixelSize: pixelSize
|
||||
});
|
||||
return Math.ceil(lineBoxProbe.boundingRect("0").height);
|
||||
}
|
||||
|
||||
function evenLineBoxSize(pixelSize) {
|
||||
if (lineBoxHeight(pixelSize) % 2 === 0)
|
||||
return pixelSize;
|
||||
return lineBoxHeight(pixelSize + 1) % 2 === 0 ? pixelSize + 1 : pixelSize;
|
||||
return Math.round(fontSizeMedium * dankBarScale * maxScale);
|
||||
return Math.round(fontSizeSmall * dankBarScale * maxScale);
|
||||
}
|
||||
|
||||
function getBatteryIcon(level, isCharging, batteryAvailable) {
|
||||
@@ -1918,30 +1870,19 @@ Singleton {
|
||||
const theme = isLight ? "adw-gtk3" : "adw-gtk3-dark";
|
||||
const schema = "org.gnome.desktop.interface";
|
||||
const key = "gtk-theme";
|
||||
const reset = GSettings.setCmd(schema, key, "");
|
||||
const apply = GSettings.setCmd(schema, key, theme);
|
||||
|
||||
const makeCmd = (tool, schema, val) => {
|
||||
if (tool === "gsettings") {
|
||||
return `gsettings set ${schema} ${key} '' && gsettings set ${schema} ${key} ${val}`;
|
||||
} else {
|
||||
const dconfPath = `/${schema.replace(/\./g, "/")}`;
|
||||
return `dconf write ${dconfPath}/${key} "''" && dconf write ${dconfPath}/${key} "'${val}'"`;
|
||||
}
|
||||
};
|
||||
|
||||
Proc.runCommand("gtkRefresher", ["sh", "-c", makeCmd("gsettings", schema, theme)], (output, exitCode) => {
|
||||
Proc.runCommand("gtkRefresher", ["sh", "-c", `${reset}; ${apply}`], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
Proc.runCommand("gtkRefreshFallback", ["sh", "-c", makeCmd("dconf", schema, theme)], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
log.warn("Failed to refresh gtk-theme");
|
||||
}
|
||||
});
|
||||
log.warn("Failed to refresh gtk-theme");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function patchGtk3colors() {
|
||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode);
|
||||
Proc.runCommand("gtk3Patcher", ["bash", shellDir + "/scripts/gtk.sh", configDir, "patch", isLight, shellDir], (output, exitCode) => {
|
||||
Proc.runCommand("gtk3Patcher", ["bash", shellDir + "/scripts/gtk.sh", configDir, "patch", isLight], (output, exitCode) => {
|
||||
switch (exitCode) {
|
||||
case 0:
|
||||
refreshGtkTheme();
|
||||
@@ -1963,7 +1904,7 @@ Singleton {
|
||||
}
|
||||
|
||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false";
|
||||
Proc.runCommand("gtkApplier", ["bash", shellDir + "/scripts/gtk.sh", configDir, "apply", isLight, shellDir], (output, exitCode) => {
|
||||
Proc.runCommand("gtkApplier", ["bash", shellDir + "/scripts/gtk.sh", configDir, "apply", isLight], (output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
if (typeof ToastService !== "undefined" && !root.matugenToastSuppressed) {
|
||||
ToastService.showInfo(I18n.tr("GTK colors applied successfully"));
|
||||
|
||||
@@ -534,39 +534,6 @@ var SPEC = {
|
||||
notificationDndAllowCritical: { def: true },
|
||||
notificationFocusedMonitor: { def: false },
|
||||
|
||||
dankIslandBarId: { def: "", onChange: "updateBarConfigs" },
|
||||
dankIslandFloating: { def: false },
|
||||
dankIslandUseOverlayLayer: { def: false },
|
||||
dankIslandReserveHeight: { def: 40 },
|
||||
dankIslandCompactHeight: { def: 38 },
|
||||
dankIslandOuterGap: { def: 4 },
|
||||
dankIslandHorizontalOffset: { def: 0 },
|
||||
dankIslandInteractionMode: { def: "hybrid" },
|
||||
dankIslandHoverOpenDelay: { def: 150 },
|
||||
dankIslandHoverCloseDelay: { def: 150 },
|
||||
dankIslandPalette: { def: "default" },
|
||||
dankIslandTransparency: { def: 1 },
|
||||
dankIslandCornerRadius: { def: 34 },
|
||||
dankIslandHighContrast: { def: false },
|
||||
dankIslandMediaClockVisible: { def: true },
|
||||
dankIslandNotificationBadgeClearOnOpen: { def: false },
|
||||
dankIslandNotificationExpand: { def: false },
|
||||
dankIslandHomeCompactTight: { def: false },
|
||||
dankIslandHomeVolumeDisplay: { def: "both" },
|
||||
dankIslandHomeBrightnessDisplay: { def: "both" },
|
||||
dankIslandHomeLayout: { def: [{ id: "media", enabled: true }, { id: "clock", enabled: true }, { id: "weather", enabled: false }, { id: "status", enabled: false }, { id: "volume", enabled: false }, { id: "brightness", enabled: false }, { id: "notifications", enabled: true }] },
|
||||
dankIslandBatteryStyle: { def: "solid" },
|
||||
dankIslandSatellitesEnabled: { def: true },
|
||||
dankIslandSatellitePosition: { def: "edges" },
|
||||
dankIslandSatelliteGap: { def: 12 },
|
||||
dankIslandSatelliteBackground: { def: false },
|
||||
dankIslandSatelliteGothCorners: { def: true },
|
||||
dankIslandSatelliteTransparency: { def: 1 },
|
||||
dankIslandSatelliteSwoopRadius: { def: 24 },
|
||||
dankIslandReducedMotion: { def: false },
|
||||
dankIslandSpringStiffness: { def: 560 },
|
||||
dankIslandSpringDamping: { def: 37 },
|
||||
dankIslandSpringMass: { def: 1 },
|
||||
|
||||
osdAlwaysShowValue: { def: false },
|
||||
osdPosition: { def: 5 },
|
||||
|
||||
@@ -14,6 +14,43 @@ var STALE_WIDGET_KEYS = ["desktopClockEnabled", "desktopClockStyle", "desktopClo
|
||||
|
||||
var BAR_WIDGET_LIST_KEYS = ["leftWidgets", "centerWidgets", "rightWidgets"];
|
||||
|
||||
// v18: the shell-wide island settings became per-bar-config island* keys
|
||||
var ISLAND_KEY_MOVES = {
|
||||
dankIslandFloating: "islandFloating",
|
||||
dankIslandUseOverlayLayer: "islandUseOverlayLayer",
|
||||
dankIslandReserveHeight: "islandReserveThickness",
|
||||
dankIslandCompactHeight: "islandCompactThickness",
|
||||
dankIslandOuterGap: "islandOuterGap",
|
||||
dankIslandHorizontalOffset: "islandAlongOffset",
|
||||
dankIslandInteractionMode: "islandInteractionMode",
|
||||
dankIslandHoverOpenDelay: "islandHoverOpenDelay",
|
||||
dankIslandHoverCloseDelay: "islandHoverCloseDelay",
|
||||
dankIslandPalette: "islandPalette",
|
||||
dankIslandTransparency: "islandTransparency",
|
||||
dankIslandCornerRadius: "islandCornerRadius",
|
||||
dankIslandHighContrast: "islandHighContrast",
|
||||
dankIslandMediaClockVisible: "islandMediaClockVisible",
|
||||
dankIslandNotificationBadgeClearOnOpen: "islandNotificationBadgeClearOnOpen",
|
||||
dankIslandNotificationExpand: "islandNotificationExpand",
|
||||
dankIslandHomeCompactTight: "islandHomeCompactTight",
|
||||
dankIslandHomeClockDisplay: "islandHomeClockDisplay",
|
||||
dankIslandHomeVolumeDisplay: "islandHomeVolumeDisplay",
|
||||
dankIslandHomeBrightnessDisplay: "islandHomeBrightnessDisplay",
|
||||
dankIslandHomeLayout: "islandHomeLayout",
|
||||
dankIslandBatteryStyle: "islandBatteryStyle",
|
||||
dankIslandSatellitesEnabled: "islandSatellitesEnabled",
|
||||
dankIslandSatellitePosition: "islandSatellitePosition",
|
||||
dankIslandSatelliteGap: "islandSatelliteGap",
|
||||
dankIslandSatelliteBackground: "islandSatelliteBackground",
|
||||
dankIslandSatelliteGothCorners: "islandSatelliteGothCorners",
|
||||
dankIslandSatelliteTransparency: "islandSatelliteTransparency",
|
||||
dankIslandSatelliteSwoopRadius: "islandSatelliteSwoopRadius",
|
||||
dankIslandReducedMotion: "islandReducedMotion",
|
||||
dankIslandSpringStiffness: "islandSpringStiffness",
|
||||
dankIslandSpringDamping: "islandSpringDamping",
|
||||
dankIslandSpringMass: "islandSpringMass"
|
||||
};
|
||||
|
||||
function migrateBatteryPillStyle(target) {
|
||||
if (!target || typeof target !== "object" || target.batteryPillStyle === undefined)
|
||||
return;
|
||||
@@ -433,5 +470,28 @@ function migrateToVersion(obj, targetVersion) {
|
||||
settings.configVersion = 17;
|
||||
}
|
||||
|
||||
if (currentVersion < 18) {
|
||||
console.info("Migrating settings from version", currentVersion, "to version 18");
|
||||
console.info("Moving the shell-wide Dank Island onto its bar config as a per-instance mode");
|
||||
|
||||
var islandId = settings.dankIslandBarId;
|
||||
var islandBars = Array.isArray(settings.barConfigs) ? settings.barConfigs : [];
|
||||
for (var ib = 0; ib < islandBars.length; ib++) {
|
||||
if (!islandBars[ib] || islandBars[ib].id !== islandId)
|
||||
continue;
|
||||
islandBars[ib].island = true;
|
||||
for (var oldKey in ISLAND_KEY_MOVES) {
|
||||
if (!(oldKey in settings))
|
||||
continue;
|
||||
islandBars[ib][ISLAND_KEY_MOVES[oldKey]] = settings[oldKey];
|
||||
}
|
||||
}
|
||||
for (var dropKey in ISLAND_KEY_MOVES)
|
||||
delete settings[dropKey];
|
||||
delete settings.dankIslandBarId;
|
||||
|
||||
settings.configVersion = 18;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ Item {
|
||||
|
||||
property bool osdSurfacesLoaded: false
|
||||
property int pendingOsdResumeReloads: 0
|
||||
readonly property var dankIslandScreens: SettingsData.getIslandScreens()
|
||||
readonly property var dankIslandScreens: Quickshell.screens.filter(screen => SettingsData.dankIslandCoversScreen(screen))
|
||||
readonly property var notificationPopupScreens: {
|
||||
const screens = SettingsData.notificationFocusedMonitor ? Quickshell.screens : SettingsData.getFilteredScreens("notifications");
|
||||
return root.withoutDankIslandScreens(screens);
|
||||
|
||||
@@ -964,9 +964,6 @@ Item {
|
||||
const posValue = positionMap[position.toLowerCase()];
|
||||
if (posValue === undefined)
|
||||
return "BAR_INVALID_POSITION";
|
||||
// The island silhouette only anchors top or bottom.
|
||||
if (SettingsData.isIslandBarConfig(barConfig) && posValue !== SettingsData.Position.Top && posValue !== SettingsData.Position.Bottom)
|
||||
return "BAR_IS_ISLAND";
|
||||
SettingsData.updateBarConfig(barConfig.id, {
|
||||
position: posValue
|
||||
});
|
||||
@@ -1866,6 +1863,8 @@ Item {
|
||||
function open(): string {
|
||||
root.workspaceRenameModalLoader.active = true;
|
||||
if (root.workspaceRenameModalLoader.item) {
|
||||
if (CompositorService.isAqueous)
|
||||
return root.workspaceRenameModalLoader.item.show("") ? "WORKSPACE_RENAME_MODAL_OPENED" : "WORKSPACE_RENAME_UNAVAILABLE";
|
||||
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
|
||||
root.workspaceRenameModalLoader.item.show(ws?.name || "");
|
||||
return "WORKSPACE_RENAME_MODAL_OPENED";
|
||||
@@ -1888,6 +1887,8 @@ Item {
|
||||
root.workspaceRenameModalLoader.item.hide();
|
||||
return "WORKSPACE_RENAME_MODAL_CLOSED";
|
||||
}
|
||||
if (CompositorService.isAqueous)
|
||||
return root.workspaceRenameModalLoader.item.show("") ? "WORKSPACE_RENAME_MODAL_OPENED" : "WORKSPACE_RENAME_UNAVAILABLE";
|
||||
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
|
||||
root.workspaceRenameModalLoader.item.show(ws?.name || "");
|
||||
return "WORKSPACE_RENAME_MODAL_OPENED";
|
||||
|
||||
@@ -1688,18 +1688,60 @@ Item {
|
||||
return result;
|
||||
}
|
||||
|
||||
function _diskCacheSectionDef(sectionId) {
|
||||
for (var i = 0; i < sectionDefinitions.length; i++) {
|
||||
if (sectionDefinitions[i].id === sectionId)
|
||||
return sectionDefinitions[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _diskCacheSection(def, items) {
|
||||
return {
|
||||
id: def.id || "",
|
||||
title: def.title || "",
|
||||
icon: def.icon || "",
|
||||
priority: def.priority || 0,
|
||||
items: items,
|
||||
collapsed: false,
|
||||
flatStartIndex: 0
|
||||
};
|
||||
}
|
||||
|
||||
function _loadDiskCache() {
|
||||
var cached = CacheData.loadLauncherCache();
|
||||
if (!cached || !Array.isArray(cached) || cached.length === 0)
|
||||
return null;
|
||||
|
||||
var browseItems = getPluginBrowseItems();
|
||||
var liveCoreItems = {};
|
||||
var liveCoreApps = AppSearchService.getCoreApps("");
|
||||
for (var c = 0; c < liveCoreApps.length; c++) {
|
||||
var coreItem = transformCoreApp(liveCoreApps[c]);
|
||||
liveCoreItems[coreItem.id] = coreItem;
|
||||
}
|
||||
|
||||
var sectionsData = [];
|
||||
var browseSectionSeen = false;
|
||||
for (var i = 0; i < cached.length; i++) {
|
||||
var s = cached[i];
|
||||
if (s.id === "browse_plugins") {
|
||||
browseSectionSeen = true;
|
||||
if (browseItems.length > 0)
|
||||
sectionsData.push(_diskCacheSection(_diskCacheSectionDef(s.id) || s, browseItems));
|
||||
continue;
|
||||
}
|
||||
var items = [];
|
||||
var srcItems = s.items || [];
|
||||
for (var j = 0; j < srcItems.length; j++) {
|
||||
var it = srcItems[j];
|
||||
if (it.isCore) {
|
||||
if (liveCoreItems[it.id])
|
||||
items.push(liveCoreItems[it.id]);
|
||||
continue;
|
||||
}
|
||||
if (it.type === "plugin_browse" || it.isBuiltInLauncher || it.pluginId)
|
||||
continue;
|
||||
items.push({
|
||||
id: it.id || "",
|
||||
type: it.type || "app",
|
||||
@@ -1709,15 +1751,12 @@ Item {
|
||||
iconType: it.iconType || "image",
|
||||
iconFull: it.iconFull || "",
|
||||
section: it.section || "",
|
||||
isCore: it.isCore || false,
|
||||
isBuiltInLauncher: it.isBuiltInLauncher || false,
|
||||
pluginId: it.pluginId || "",
|
||||
source: it.source || "",
|
||||
data: {
|
||||
id: it.id
|
||||
},
|
||||
actions: _actionsFromDesktopEntry(it.id),
|
||||
primaryAction: it.type === "app" && !it.isCore ? {
|
||||
primaryAction: it.type === "app" ? {
|
||||
name: I18n.tr("Launch"),
|
||||
icon: "open_in_new",
|
||||
action: "launch"
|
||||
@@ -1729,16 +1768,22 @@ Item {
|
||||
_preScored: undefined
|
||||
});
|
||||
}
|
||||
sectionsData.push({
|
||||
id: s.id || "",
|
||||
title: s.title || "",
|
||||
icon: s.icon || "",
|
||||
priority: s.priority || 0,
|
||||
items: items,
|
||||
collapsed: false,
|
||||
flatStartIndex: 0
|
||||
});
|
||||
if (items.length > 0)
|
||||
sectionsData.push(_diskCacheSection(s, items));
|
||||
}
|
||||
|
||||
if (!browseSectionSeen && browseItems.length > 0) {
|
||||
var browseDef = _diskCacheSectionDef("browse_plugins");
|
||||
if (browseDef)
|
||||
sectionsData.push(_diskCacheSection(browseDef, browseItems));
|
||||
}
|
||||
|
||||
if (sectionsData.length === 0)
|
||||
return null;
|
||||
|
||||
sectionsData.sort(function (a, b) {
|
||||
return a.priority - b.priority;
|
||||
});
|
||||
return sectionsData;
|
||||
}
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ Item {
|
||||
if (modelData.repo)
|
||||
Qt.openUrlExternally(modelData.repo);
|
||||
}
|
||||
onInstallRequested: PluginService.installFromRegistry(modelData.name, modelData.type === "desktop")
|
||||
onInstallRequested: PluginService.installFromRegistry(modelData.id, modelData.name, modelData.type === "desktop")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,14 @@ DankModal {
|
||||
|
||||
const topChrome = Math.max(bar && barPosition === SettingsData.Position.Top ? barExclusionZone : 0, SettingsData.dankIslandEdgeOffset(parentScreen, "top"));
|
||||
const bottomChrome = Math.max(bar && barPosition === SettingsData.Position.Bottom ? barExclusionZone : 0, SettingsData.dankIslandEdgeOffset(parentScreen, "bottom"));
|
||||
const leftChrome = Math.max(bar && barPosition === SettingsData.Position.Left ? barExclusionZone : 0, SettingsData.dankIslandEdgeOffset(parentScreen, "left"));
|
||||
const rightChrome = Math.max(bar && barPosition === SettingsData.Position.Right ? barExclusionZone : 0, SettingsData.dankIslandEdgeOffset(parentScreen, "right"));
|
||||
const minY = topChrome + margin;
|
||||
const maxY = screenH - modalHeight - bottomChrome - margin;
|
||||
const minX = leftChrome + margin;
|
||||
const maxX = screenW - modalWidth - rightChrome - margin;
|
||||
|
||||
targetX = Math.max(minX, Math.min(maxX, targetX));
|
||||
targetY = Math.max(minY, Math.min(maxY, targetY));
|
||||
|
||||
return Qt.point(targetX, targetY);
|
||||
|
||||
@@ -92,8 +92,8 @@ Rectangle {
|
||||
root.leaveHiddenTab(33);
|
||||
}
|
||||
|
||||
function onDankIslandBarIdChanged() {
|
||||
if (!SettingsData.dankIslandBarId)
|
||||
function onIslandBarConfigsChanged() {
|
||||
if (SettingsData.islandBarConfigs.length === 0)
|
||||
root.leaveHiddenTab(46);
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ Rectangle {
|
||||
return false;
|
||||
if (item.frameOnly && !SettingsData.frameEnabled)
|
||||
return false;
|
||||
if (item.islandOnly && !SettingsData.dankIslandBarId)
|
||||
if (item.islandOnly && SettingsData.islandBarConfigs.length === 0)
|
||||
return false;
|
||||
if (item.cellularOnly && (NetworkService.cellularDevices?.length ?? 0) === 0)
|
||||
return false;
|
||||
|
||||
@@ -9,6 +9,8 @@ DankFloatingWindow {
|
||||
readonly property var log: Log.scoped("WorkspaceRenameModal")
|
||||
|
||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||
property var aqueousWorkspace: null
|
||||
property bool renaming: false
|
||||
|
||||
objectName: "workspaceRenameModal"
|
||||
title: I18n.tr("Rename Workspace")
|
||||
@@ -19,9 +21,24 @@ DankFloatingWindow {
|
||||
onClosed: hide()
|
||||
|
||||
function show(name) {
|
||||
if (CompositorService.isAqueous) {
|
||||
if (renaming)
|
||||
return false;
|
||||
const workspace = AqueousService.workspaces.find(ws => ws.output === AqueousService.seat?.output && ws.active);
|
||||
if (!AqueousService.available || !AqueousService.seat || !workspace || !AqueousService.capabilities.commands || AqueousService.locked) {
|
||||
ToastService.showError(I18n.tr("Error"), I18n.tr("Unavailable"));
|
||||
return false;
|
||||
}
|
||||
aqueousWorkspace = {
|
||||
id: workspace.id,
|
||||
session: workspace.aqueousSession
|
||||
};
|
||||
name = workspace.name;
|
||||
}
|
||||
nameInput.text = name;
|
||||
visible = true;
|
||||
Qt.callLater(() => nameInput.forceActiveFocus());
|
||||
return true;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
@@ -29,6 +46,24 @@ DankFloatingWindow {
|
||||
}
|
||||
|
||||
function submitAndClose() {
|
||||
if (aqueousWorkspace || CompositorService.isAqueous) {
|
||||
if (!aqueousWorkspace || renaming)
|
||||
return;
|
||||
const target = aqueousWorkspace;
|
||||
renaming = true;
|
||||
AqueousService.command("workspace.rename", {
|
||||
id: target.id,
|
||||
session: target.session,
|
||||
name: nameInput.text
|
||||
}, success => {
|
||||
if (root.aqueousWorkspace !== target)
|
||||
return;
|
||||
root.renaming = false;
|
||||
if (success)
|
||||
root.hide();
|
||||
});
|
||||
return;
|
||||
}
|
||||
renameWorkspace(nameInput.text);
|
||||
hide();
|
||||
}
|
||||
@@ -49,6 +84,8 @@ DankFloatingWindow {
|
||||
return;
|
||||
}
|
||||
nameInput.text = "";
|
||||
aqueousWorkspace = null;
|
||||
renaming = false;
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
@@ -133,7 +170,7 @@ DankFloatingWindow {
|
||||
textColor: Theme.surfaceText
|
||||
placeholderText: I18n.tr("Workspace name")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.visible
|
||||
enabled: root.visible && !root.renaming
|
||||
onAccepted: submitAndClose()
|
||||
}
|
||||
}
|
||||
@@ -190,6 +227,7 @@ DankFloatingWindow {
|
||||
|
||||
MouseArea {
|
||||
id: renameArea
|
||||
enabled: !root.renaming
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
@@ -62,7 +62,7 @@ Item {
|
||||
// M3 elevation shadow — Level 2 baseline (navigation bar), with per-bar override support
|
||||
readonly property bool hasPerBarOverride: (barConfig?.shadowIntensity ?? 0) > 0
|
||||
readonly property var elevLevel: Theme.elevationLevel2
|
||||
readonly property bool shadowEnabled: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || hasPerBarOverride
|
||||
readonly property bool shadowEnabled: Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)
|
||||
readonly property string autoBarShadowDirection: isTop ? "top" : (isBottom ? "bottom" : (isLeft ? "left" : (isRight ? "right" : "top")))
|
||||
readonly property string globalShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
|
||||
readonly property string perBarShadowDirectionMode: barConfig?.shadowDirectionMode ?? "inherit"
|
||||
|
||||
@@ -454,7 +454,7 @@ Item {
|
||||
readonly property real _wingR: Math.max(0, wingtipsRadius)
|
||||
|
||||
// Shadow buffer: extra window space for shadow to render beyond bar bounds
|
||||
readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (renderBarConfig?.shadowIntensity ?? 0) > 0
|
||||
readonly property bool _shadowActive: Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)
|
||||
readonly property real _shadowBuffer: {
|
||||
if (!_shadowActive)
|
||||
return 0;
|
||||
@@ -496,7 +496,7 @@ Item {
|
||||
hasMaximizedToplevel = false;
|
||||
return;
|
||||
}
|
||||
if (!CompositorService.isHyprland && !CompositorService.isNiri) {
|
||||
if (!CompositorService.isHyprland && !CompositorService.isNiri && !CompositorService.isAqueous) {
|
||||
hasMaximizedToplevel = false;
|
||||
return;
|
||||
}
|
||||
@@ -520,7 +520,7 @@ Item {
|
||||
shouldHideForWindows = false;
|
||||
return;
|
||||
}
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango) {
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango && !CompositorService.isAqueous) {
|
||||
shouldHideForWindows = false;
|
||||
return;
|
||||
}
|
||||
@@ -795,7 +795,7 @@ Item {
|
||||
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr)
|
||||
readonly property int lengthPaddingPx: Theme.px(barWindow.effectiveBarLengthPadding, barWindow._dpr)
|
||||
|
||||
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
||||
readonly property bool inOverviewWithShow: CompositorService.overviewActiveOnScreen(barWindow.screenName) && barWindow.effectiveOpenOnOverview
|
||||
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
|
||||
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow)
|
||||
|
||||
@@ -986,7 +986,7 @@ Item {
|
||||
}
|
||||
|
||||
property bool reveal: {
|
||||
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
|
||||
const inOverviewWithShow = CompositorService.overviewActiveOnScreen(barWindow.screenName) && barWindow.effectiveOpenOnOverview;
|
||||
if (inOverviewWithShow)
|
||||
return true;
|
||||
|
||||
@@ -997,7 +997,7 @@ Item {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CompositorService.isNiri && NiriService.inOverview)
|
||||
if (CompositorService.overviewActiveOnScreen(barWindow.screenName))
|
||||
return hoverReveal || popoutPinsReveal || revealSticky || ipcReveal;
|
||||
|
||||
return (barConfig?.visible ?? true) && (!autoHide || hoverReveal || popoutPinsReveal || revealSticky || ipcReveal);
|
||||
@@ -1062,7 +1062,7 @@ Item {
|
||||
top: barWindow.isVertical ? parent.top : undefined
|
||||
bottom: barWindow.isVertical ? parent.bottom : undefined
|
||||
}
|
||||
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
||||
readonly property bool inOverview: CompositorService.overviewActiveOnScreen(barWindow.screenName) && barWindow.effectiveOpenOnOverview
|
||||
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.popoutPinsReveal
|
||||
acceptedButtons: barWindow.clickThroughEnabled ? Qt.NoButton : Qt.RightButton
|
||||
enabled: !inOverview && ((barConfig?.autoHide ?? false) || !barWindow.clickThroughEnabled)
|
||||
|
||||
@@ -156,6 +156,8 @@ Item {
|
||||
|
||||
function getRealWorkspaces() {
|
||||
const screenName = _barScreenName;
|
||||
if (CompositorService.isAqueous && AqueousService.available)
|
||||
return AqueousService.workspacesForOutput(SettingsData.workspaceFollowFocus ? AqueousService.focusedOutput : screenName);
|
||||
if (CompositorService.isNiri) {
|
||||
const fallbackWorkspaces = [
|
||||
{
|
||||
@@ -238,6 +240,8 @@ Item {
|
||||
|
||||
function getCurrentWorkspace() {
|
||||
const screenName = _barScreenName;
|
||||
if (CompositorService.isAqueous && AqueousService.available)
|
||||
return getRealWorkspaces().find(ws => ws.active)?.id || "";
|
||||
if (CompositorService.isNiri) {
|
||||
if (!screenName || SettingsData.workspaceFollowFocus) {
|
||||
return NiriService.getCurrentWorkspaceNumber();
|
||||
@@ -279,7 +283,14 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CompositorService.isNiri) {
|
||||
if (CompositorService.isAqueous && AqueousService.available) {
|
||||
const index = realWorkspaces.findIndex(ws => ws.id === getCurrentWorkspace());
|
||||
if (index < 0)
|
||||
return;
|
||||
const next = Math.max(0, Math.min(realWorkspaces.length - 1, index + (direction > 0 ? 1 : -1)));
|
||||
if (next !== index)
|
||||
AqueousService.activateWorkspace(realWorkspaces[next]);
|
||||
} else if (CompositorService.isNiri) {
|
||||
const currentWs = getCurrentWorkspace();
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === currentWs);
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex;
|
||||
@@ -323,7 +334,7 @@ Item {
|
||||
}
|
||||
|
||||
function switchApp(deltaY) {
|
||||
const windows = sortedToplevels;
|
||||
const windows = sortedToplevels.filter(w => !w.skipSwitcher);
|
||||
if (windows.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ PanelWindow {
|
||||
implicitWidth: body.surfaceImplicitWidth
|
||||
exclusiveZone: body.surfaceExclusiveZone
|
||||
|
||||
BackgroundEffect.blurRegion: body.blurRegion
|
||||
BackgroundEffect.blurRegion: BlurService.enabled ? body.blurRegion : null
|
||||
|
||||
Component.onCompleted: {
|
||||
KeyboardFocus.registerBarWindow(barWindow);
|
||||
|
||||
@@ -160,7 +160,7 @@ PanelWindow {
|
||||
return [];
|
||||
|
||||
const toplevels = [];
|
||||
const allToplevels = ToplevelManager.toplevels.values;
|
||||
const allToplevels = CompositorService.isAqueous && AqueousService.available ? CompositorService.sortedToplevels : ToplevelManager.toplevels.values;
|
||||
for (let i = 0; i < allToplevels.length; i++) {
|
||||
const toplevel = allToplevels[i];
|
||||
if (toplevel.appId === root.appData.appId) {
|
||||
|
||||
@@ -204,7 +204,7 @@ BasePill {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
property real fontSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
property real digitWidth: fontSize * 0.6
|
||||
property real digitWidth: Math.round(fontSize * 0.6)
|
||||
|
||||
property string hoursStr: {
|
||||
const hours = systemClock?.date?.getHours() ?? 0;
|
||||
@@ -234,6 +234,8 @@ BasePill {
|
||||
text: "•"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outlineButton
|
||||
width: Math.ceil(implicitWidth)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !compact
|
||||
}
|
||||
@@ -272,6 +274,8 @@ BasePill {
|
||||
text: ":"
|
||||
font.pixelSize: clockRow.fontSize
|
||||
color: Theme.widgetTextColor
|
||||
width: Math.ceil(implicitWidth)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -295,6 +299,8 @@ BasePill {
|
||||
text: ":"
|
||||
font.pixelSize: clockRow.fontSize
|
||||
color: Theme.widgetTextColor
|
||||
width: Math.ceil(implicitWidth)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -320,6 +326,8 @@ BasePill {
|
||||
text: clockRow.ampmStr
|
||||
font.pixelSize: clockRow.fontSize
|
||||
color: Theme.widgetTextColor
|
||||
width: Math.ceil(implicitWidth)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,6 +345,8 @@ BasePill {
|
||||
}
|
||||
font.pixelSize: clockRow.fontSize
|
||||
color: Theme.widgetTextColor
|
||||
width: Math.ceil(implicitWidth)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !compact
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ BasePill {
|
||||
property int availableWidth: maxWidth
|
||||
readonly property real effectiveHorizontalWidth: Math.max(0, Math.min(maxWidth, availableWidth))
|
||||
readonly property real effectiveHorizontalInnerWidth: Math.max(0, effectiveHorizontalWidth - horizontalPadding * 2)
|
||||
property Toplevel activeWindow: null
|
||||
property var activeWindow: null
|
||||
property var activeDesktopEntry: null
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
property bool isAutoHideBar: false
|
||||
@@ -99,6 +99,11 @@ BasePill {
|
||||
}
|
||||
|
||||
function updateActiveWindow() {
|
||||
if (CompositorService.isAqueous && AqueousService.available) {
|
||||
const focused = AqueousService.focusedWindow;
|
||||
activeWindow = focused && (!parentScreen || focused.screens.some(s => s.name === parentScreen.name)) ? focused : null;
|
||||
return;
|
||||
}
|
||||
let active = ToplevelManager.activeToplevel;
|
||||
|
||||
if (!active && CompositorService.isNiri) {
|
||||
|
||||
@@ -16,6 +16,8 @@ BasePill {
|
||||
property bool showIcon: widgetData?.keyboardLayoutNameShowIcon !== undefined ? widgetData.keyboardLayoutNameShowIcon : SettingsData.keyboardLayoutNameShowIcon
|
||||
readonly property var validVariants: ["US", "UK", "GB", "AZERTY", "QWERTY", "Dvorak", "Colemak", "Mac", "Intl", "International"]
|
||||
property string currentLayout: {
|
||||
if (CompositorService.isAqueous)
|
||||
return AqueousService.keyboardLayout;
|
||||
if (CompositorService.isNiri) {
|
||||
return NiriService.getCurrentKeyboardLayoutName();
|
||||
} else if (CompositorService.isMango) {
|
||||
@@ -25,7 +27,7 @@ BasePill {
|
||||
}
|
||||
property string hyprlandKeyboard: ""
|
||||
property var hyprlandLayoutLabels: []
|
||||
readonly property var _allLayoutLabels: CompositorService.isNiri ? (NiriService.keyboardLayoutNames || []).map(n => displayLabel(n)) : hyprlandLayoutLabels
|
||||
readonly property var _allLayoutLabels: CompositorService.isAqueous ? AqueousService.keyboardLayouts.map(n => displayLabel(n)) : CompositorService.isNiri ? (NiriService.keyboardLayoutNames || []).map(n => displayLabel(n)) : hyprlandLayoutLabels
|
||||
readonly property string reserveLabel: widestLabel(_allLayoutLabels)
|
||||
readonly property string verticalReserveLabel: widestLabel(_allLayoutLabels.map(n => LayoutCodes.layoutCode(n)))
|
||||
|
||||
@@ -131,6 +133,8 @@ BasePill {
|
||||
onClicked: {
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.cycleKeyboardLayout();
|
||||
} else if (CompositorService.isAqueous) {
|
||||
AqueousService.cycleKeyboardLayout();
|
||||
} else if (CompositorService.isHyprland) {
|
||||
Quickshell.execDetached(["hyprctl", "switchxkblayout", root.hyprlandKeyboard, "next"]);
|
||||
} else if (CompositorService.isMango) {
|
||||
|
||||
@@ -163,7 +163,7 @@ BasePill {
|
||||
const deltaY = wheelEvent.angleDelta.y;
|
||||
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
|
||||
|
||||
const windows = root.sortedToplevels;
|
||||
const windows = root.sortedToplevels.filter(w => !w.skipSwitcher);
|
||||
if (windows.length < 2)
|
||||
return;
|
||||
|
||||
@@ -236,7 +236,7 @@ BasePill {
|
||||
id: windowRepeater
|
||||
model: ScriptModel {
|
||||
values: _groupByApp ? groupedWindows : sortedToplevels
|
||||
objectProp: _groupByApp ? "appId" : "address"
|
||||
objectProp: _groupByApp ? "appId" : (CompositorService.isAqueous && AqueousService.available ? "aqueousKey" : "address")
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
@@ -494,7 +494,7 @@ BasePill {
|
||||
id: windowRepeater
|
||||
model: ScriptModel {
|
||||
values: _groupByApp ? groupedWindows : sortedToplevels
|
||||
objectProp: _groupByApp ? "appId" : "address"
|
||||
objectProp: _groupByApp ? "appId" : (CompositorService.isAqueous && AqueousService.available ? "aqueousKey" : "address")
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
|
||||
@@ -24,6 +24,7 @@ Item {
|
||||
property real crossEdgeExtension: 0
|
||||
|
||||
readonly property bool isMango: CompositorService.isMango
|
||||
readonly property bool useAqueous: CompositorService.isAqueous && AqueousService.available && Quickshell.env("DMS_FORCE_EXTWS") !== "1"
|
||||
|
||||
readonly property real _leftMargin: {
|
||||
if (isVertical)
|
||||
@@ -82,8 +83,10 @@ Item {
|
||||
}
|
||||
readonly property bool useUnfocusedAppearance: !isFocusedMonitor && SettingsData.workspaceUnfocusedMonitorSeparateAppearance && BarWidgetService.focusedScreenDetectionSupported
|
||||
|
||||
readonly property var extProjection: (useExtWorkspace && parentScreen) ? WindowManager.screenProjection(parentScreen) : null
|
||||
readonly property var extProjection: (useExtWorkspace && parentScreen) ? WindowManager.screenProjection(CompositorService.isAqueous ? Quickshell.screens.find(s => s.name === effectiveScreenName) || parentScreen : parentScreen) : null
|
||||
readonly property bool useExtWorkspace: {
|
||||
if (useAqueous)
|
||||
return false;
|
||||
if (Quickshell.env("DMS_FORCE_EXTWS") === "1")
|
||||
return (WindowManager.windowsets?.length ?? 0) > 0;
|
||||
if (!CompositorService.compositorDetected)
|
||||
@@ -117,6 +120,8 @@ Item {
|
||||
}
|
||||
|
||||
property var currentWorkspace: {
|
||||
if (useAqueous)
|
||||
return AqueousService.workspacesForOutput(effectiveScreenName).find(w => w.active)?.id || "";
|
||||
if (useExtWorkspace)
|
||||
return getExtWorkspaceActiveWorkspace();
|
||||
|
||||
@@ -143,6 +148,10 @@ Item {
|
||||
return [];
|
||||
}
|
||||
property var workspaceList: {
|
||||
if (useAqueous) {
|
||||
const baseList = AqueousService.workspacesForOutput(effectiveScreenName).filter(ws => !SettingsData.showOccupiedWorkspacesOnly || ws.active || AqueousService.toplevels.some(w => w.aqueousWorkspaceId === ws.id));
|
||||
return SettingsData.showWorkspacePadding && baseList.length ? padWorkspaces(baseList) : baseList;
|
||||
}
|
||||
if (useExtWorkspace) {
|
||||
const baseList = getExtWorkspaceWorkspaces();
|
||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList;
|
||||
@@ -322,12 +331,14 @@ Item {
|
||||
|
||||
function getWorkspaceIcons(ws) {
|
||||
_desktopEntriesUpdateTrigger;
|
||||
if (!SettingsData.showWorkspaceApps || !ws) {
|
||||
if (!SettingsData.showWorkspaceApps || !ws || useExtWorkspace || (useAqueous && ws._placeholder)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let targetWorkspaceId;
|
||||
if (CompositorService.isNiri) {
|
||||
if (useAqueous) {
|
||||
targetWorkspaceId = ws.id;
|
||||
} else if (CompositorService.isNiri) {
|
||||
if (!ws || typeof ws !== "object") {
|
||||
const wsNumber = typeof ws === "number" ? ws : -1;
|
||||
if (wsNumber <= 0) {
|
||||
@@ -387,7 +398,9 @@ Item {
|
||||
return;
|
||||
} else {
|
||||
let winWs = null;
|
||||
if (CompositorService.isNiri) {
|
||||
if (useAqueous) {
|
||||
winWs = w.aqueousWorkspaceId;
|
||||
} else if (CompositorService.isNiri) {
|
||||
winWs = w.workspace_id;
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
winWs = w.workspace?.num;
|
||||
@@ -405,7 +418,7 @@ Item {
|
||||
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown");
|
||||
const moddedId = Paths.moddedAppId(keyBase);
|
||||
const groupThisWs = SettingsData.groupWorkspaceApps && (!isActiveWs || SettingsData.groupActiveWorkspaceApps);
|
||||
const key = groupThisWs ? moddedId : `${moddedId}_${i}`;
|
||||
const key = groupThisWs ? moddedId : (w.aqueousKey || `${moddedId}_${i}`);
|
||||
|
||||
if (!byApp[key]) {
|
||||
const isQuickshell = keyBase === "org.quickshell" || keyBase === "com.danklinux.dms";
|
||||
@@ -421,6 +434,7 @@ Item {
|
||||
"active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)),
|
||||
"count": 1,
|
||||
"windowId": w.address || w.id,
|
||||
"windowSession": useAqueous ? w.aqueousSession : "",
|
||||
"fallbackText": appName || ""
|
||||
};
|
||||
} else {
|
||||
@@ -435,7 +449,7 @@ Item {
|
||||
}
|
||||
|
||||
function _makePlaceholder() {
|
||||
if (useExtWorkspace)
|
||||
if (useAqueous || useExtWorkspace)
|
||||
return {
|
||||
"id": "",
|
||||
"name": "",
|
||||
@@ -648,7 +662,7 @@ Item {
|
||||
if (!extProjection)
|
||||
return "";
|
||||
const activeWs = extProjection.windowsets.find(ws => ws.active);
|
||||
return activeWs ? (activeWs.id || activeWs.name || "") : "";
|
||||
return activeWs || null;
|
||||
}
|
||||
|
||||
readonly property real dpr: parentScreen ? CompositorService.getScreenScale(parentScreen) : 1
|
||||
@@ -659,7 +673,7 @@ Item {
|
||||
|
||||
function getRealWorkspaces() {
|
||||
return root.workspaceList.filter(ws => {
|
||||
if (useExtWorkspace)
|
||||
if (useAqueous || useExtWorkspace)
|
||||
return ws && !ws._placeholder;
|
||||
if (CompositorService.isNiri)
|
||||
return ws && ws.idx !== -1;
|
||||
@@ -674,9 +688,14 @@ Item {
|
||||
}
|
||||
|
||||
function switchToWorkspaceByModelData(data) {
|
||||
if (!data)
|
||||
if (!data || data._placeholder)
|
||||
return;
|
||||
|
||||
if (useAqueous) {
|
||||
AqueousService.activateWorkspace(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.useExtWorkspace) {
|
||||
if (typeof data.activate === "function")
|
||||
data.activate();
|
||||
@@ -727,13 +746,21 @@ Item {
|
||||
}
|
||||
|
||||
function switchWorkspace(direction) {
|
||||
if (useAqueous) {
|
||||
const workspaces = getRealWorkspaces();
|
||||
const index = workspaces.findIndex(w => w.id === currentWorkspace);
|
||||
const next = Math.max(0, Math.min(workspaces.length - 1, index + (direction > 0 ? 1 : -1)));
|
||||
if (next !== index)
|
||||
AqueousService.activateWorkspace(workspaces[next]);
|
||||
return;
|
||||
}
|
||||
if (useExtWorkspace) {
|
||||
const realWorkspaces = getRealWorkspaces();
|
||||
if (realWorkspaces.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = realWorkspaces.findIndex(ws => (ws.id || ws.name) === root.currentWorkspace);
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws === root.currentWorkspace);
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex;
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
|
||||
|
||||
@@ -812,6 +839,8 @@ Item {
|
||||
}
|
||||
|
||||
function getWorkspaceIndexFallback(modelData, index) {
|
||||
if (useAqueous)
|
||||
return modelData?.number ?? "";
|
||||
if (root.useExtWorkspace)
|
||||
return index + 1;
|
||||
if (CompositorService.isNiri)
|
||||
@@ -827,8 +856,8 @@ Item {
|
||||
|
||||
function getWorkspaceIndex(modelData, index) {
|
||||
let isPlaceholder;
|
||||
if (root.useExtWorkspace) {
|
||||
isPlaceholder = modelData?.hidden === true;
|
||||
if (root.useAqueous || root.useExtWorkspace) {
|
||||
isPlaceholder = modelData?._placeholder === true;
|
||||
} else if (CompositorService.isNiri) {
|
||||
isPlaceholder = modelData?.idx === -1;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
@@ -868,7 +897,7 @@ Item {
|
||||
return getWorkspaceIndexFallback(modelData, index);
|
||||
}
|
||||
|
||||
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || root.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
|
||||
readonly property bool hasNativeWorkspaceSupport: useAqueous || CompositorService.isNiri || CompositorService.isHyprland || root.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
|
||||
readonly property bool hasWorkspaces: getRealWorkspaces().length > 0
|
||||
readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
|
||||
|
||||
@@ -962,7 +991,9 @@ Item {
|
||||
const rootPos = edgeMouseArea.mapToItem(root, mouse.x, mouse.y);
|
||||
switch (mouse.button) {
|
||||
case Qt.RightButton:
|
||||
if (CompositorService.isNiri) {
|
||||
if (CompositorService.isAqueous) {
|
||||
AqueousService.toggleOverview(root.effectiveScreenName);
|
||||
} else if (CompositorService.isNiri) {
|
||||
NiriService.toggleOverview();
|
||||
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
|
||||
@@ -1144,8 +1175,10 @@ Item {
|
||||
}
|
||||
|
||||
property bool isActive: {
|
||||
if (root.useAqueous)
|
||||
return modelData?.active === true;
|
||||
if (root.useExtWorkspace)
|
||||
return (modelData?.id || modelData?.name) === root.currentWorkspace;
|
||||
return modelData === root.currentWorkspace;
|
||||
if (CompositorService.isNiri)
|
||||
return !!(modelData && modelData.idx === root.currentWorkspace);
|
||||
if (CompositorService.isHyprland)
|
||||
@@ -1157,6 +1190,8 @@ Item {
|
||||
return modelData === root.currentWorkspace;
|
||||
}
|
||||
property bool isOccupied: {
|
||||
if (root.useAqueous)
|
||||
return AqueousService.toplevels.some(w => w.aqueousWorkspaceId === modelData?.id);
|
||||
if (CompositorService.isHyprland)
|
||||
return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id);
|
||||
if (root.isMango)
|
||||
@@ -1166,7 +1201,7 @@ Item {
|
||||
return false;
|
||||
}
|
||||
property bool isPlaceholder: {
|
||||
if (root.useExtWorkspace)
|
||||
if (root.useAqueous || root.useExtWorkspace)
|
||||
return !!(modelData && modelData._placeholder);
|
||||
if (CompositorService.isNiri)
|
||||
return !!(modelData && modelData.idx === -1);
|
||||
@@ -1183,6 +1218,8 @@ Item {
|
||||
property var loadedWorkspaceData: null
|
||||
property bool loadedIsUrgent: false
|
||||
property bool isUrgent: {
|
||||
if (root.useAqueous)
|
||||
return modelData?.urgent === true;
|
||||
if (root.useExtWorkspace)
|
||||
return modelData?.urgent ?? false;
|
||||
if (CompositorService.isHyprland)
|
||||
@@ -1210,6 +1247,9 @@ Item {
|
||||
if (!SettingsData.showWorkspaceApps || isPlaceholder)
|
||||
return 0;
|
||||
|
||||
if (root.useAqueous)
|
||||
return loadedIcons.length;
|
||||
|
||||
let targetWorkspaceId;
|
||||
if (root.useExtWorkspace) {
|
||||
targetWorkspaceId = modelData?.id || modelData?.name;
|
||||
@@ -1267,7 +1307,7 @@ Item {
|
||||
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) : (isActive ? Math.max(root.widgetHeight * 1.05, root.appIconSize * 1.6) : Math.max(root.widgetHeight * 0.7, root.appIconSize * 1.2))
|
||||
readonly property real baseHeight: root.isVertical ? (isActive ? Math.max(root.widgetHeight * 1.05, root.appIconSize * 1.6) : Math.max(root.widgetHeight * 0.7, root.appIconSize * 1.2)) : (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5)
|
||||
readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== ""
|
||||
readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && (CompositorService.isNiri || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||
readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && (root.useAqueous || CompositorService.isNiri || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||
readonly property real contentImplicitWidth: appIconsLoader.item?.contentWidth ?? 0
|
||||
readonly property real contentImplicitHeight: appIconsLoader.item?.contentHeight ?? 0
|
||||
|
||||
@@ -1479,7 +1519,11 @@ Item {
|
||||
return;
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (root.useExtWorkspace) {
|
||||
if (delegateRoot.focusWindowAt(mouse.x, mouse.y))
|
||||
return;
|
||||
if (root.useAqueous) {
|
||||
root.switchToWorkspaceByModelData(modelData);
|
||||
} else if (root.useExtWorkspace) {
|
||||
if (typeof modelData?.activate === "function")
|
||||
modelData.activate();
|
||||
} else if (CompositorService.isNiri) {
|
||||
@@ -1494,7 +1538,9 @@ Item {
|
||||
CompositorService.dispatchSwayWorkspace(modelData);
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (CompositorService.isNiri) {
|
||||
if (CompositorService.isAqueous) {
|
||||
AqueousService.toggleOverview(root.effectiveScreenName);
|
||||
} else if (CompositorService.isNiri) {
|
||||
NiriService.toggleOverview();
|
||||
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
|
||||
@@ -1517,7 +1563,7 @@ Item {
|
||||
}
|
||||
|
||||
var wsData = null;
|
||||
if (root.useExtWorkspace) {
|
||||
if (root.useAqueous || root.useExtWorkspace) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isNiri) {
|
||||
wsData = modelData || null;
|
||||
@@ -1554,6 +1600,36 @@ Item {
|
||||
dataUpdateTimer.restart();
|
||||
}
|
||||
|
||||
function windowIdAt(x, y) {
|
||||
const layout = appIconsLoader.item?.iconsLayout;
|
||||
if (!layout)
|
||||
return null;
|
||||
const point = layout.mapFromItem(mouseArea, x, y);
|
||||
const icon = layout.childAt(point.x, point.y);
|
||||
if (root.useAqueous)
|
||||
return icon?.windowId ? {id: icon.windowId, session: icon.windowSession} : null;
|
||||
return icon?.windowId ?? null;
|
||||
}
|
||||
|
||||
function focusWindowAt(x, y) {
|
||||
const winId = delegateRoot.windowIdAt(x, y);
|
||||
if (!winId)
|
||||
return false;
|
||||
if (root.useAqueous) {
|
||||
AqueousService.command("window.activate", winId);
|
||||
return true;
|
||||
}
|
||||
if (CompositorService.isHyprland) {
|
||||
HyprlandService.focusWindow(winId);
|
||||
return true;
|
||||
}
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.focusWindow(winId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
width: root.isVertical ? root.widgetHeight : visualWidth
|
||||
height: root.isVertical ? visualHeight : root.widgetHeight
|
||||
|
||||
@@ -1678,6 +1754,7 @@ Item {
|
||||
id: contentRoot
|
||||
readonly property real contentWidth: contentRow.item?.implicitWidth ?? 0
|
||||
readonly property real contentHeight: contentRow.item?.implicitHeight ?? 0
|
||||
property alias iconsLayout: contentRow.item
|
||||
|
||||
Loader {
|
||||
id: contentRow
|
||||
@@ -1743,16 +1820,18 @@ Item {
|
||||
delegate: Item {
|
||||
width: root.appIconSize
|
||||
height: root.appIconSize
|
||||
readonly property var windowId: modelData.windowId
|
||||
readonly property string windowSession: modelData.windowSession || ""
|
||||
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
|
||||
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
|
||||
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
|
||||
readonly property real appOpacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
readonly property real appOpacity: modelData.active ? 1.0 : rowAppHover.hovered ? 0.8 : 0.6
|
||||
|
||||
IconImage {
|
||||
id: rowAppIcon
|
||||
anchors.fill: parent
|
||||
source: modelData.icon || ""
|
||||
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
opacity: modelData.active ? 1.0 : rowAppHover.hovered ? 0.8 : 0.6
|
||||
visible: !modelData.isQuickshell && !modelData.isSteamApp && status === Image.Ready
|
||||
}
|
||||
|
||||
@@ -1794,7 +1873,7 @@ Item {
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon
|
||||
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
opacity: modelData.active ? 1.0 : rowAppHover.hovered ? 0.8 : 0.6
|
||||
visible: modelData.isQuickshell
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
@@ -1808,7 +1887,7 @@ Item {
|
||||
id: rowSteamIcon
|
||||
anchors.fill: parent
|
||||
source: modelData.icon
|
||||
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
opacity: modelData.active ? 1.0 : rowAppHover.hovered ? 0.8 : 0.6
|
||||
visible: modelData.isSteamApp && modelData.icon
|
||||
}
|
||||
|
||||
@@ -1817,7 +1896,7 @@ Item {
|
||||
size: root.appIconSize
|
||||
name: "sports_esports"
|
||||
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
|
||||
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
opacity: modelData.active ? 1.0 : rowAppHover.hovered ? 0.8 : 0.6
|
||||
visible: modelData.isSteamApp && !modelData.icon
|
||||
}
|
||||
|
||||
@@ -1831,21 +1910,8 @@ Item {
|
||||
z: 1
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rowAppMouseArea
|
||||
anchors.fill: parent
|
||||
enabled: isActive
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const winId = modelData.windowId;
|
||||
if (!winId)
|
||||
return;
|
||||
if (CompositorService.isHyprland) {
|
||||
HyprlandService.focusWindow(winId);
|
||||
} else if (CompositorService.isNiri) {
|
||||
NiriService.focusWindow(winId);
|
||||
}
|
||||
}
|
||||
HoverHandler {
|
||||
id: rowAppHover
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -1912,16 +1978,18 @@ Item {
|
||||
delegate: Item {
|
||||
width: root.appIconSize
|
||||
height: root.appIconSize
|
||||
readonly property var windowId: modelData.windowId
|
||||
readonly property string windowSession: modelData.windowSession || ""
|
||||
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
|
||||
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
|
||||
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
|
||||
readonly property real appOpacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
readonly property real appOpacity: modelData.active ? 1.0 : colAppHover.hovered ? 0.8 : 0.6
|
||||
|
||||
IconImage {
|
||||
id: colAppIcon
|
||||
anchors.fill: parent
|
||||
source: modelData.icon || ""
|
||||
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
opacity: modelData.active ? 1.0 : colAppHover.hovered ? 0.8 : 0.6
|
||||
visible: !modelData.isQuickshell && !modelData.isSteamApp && status === Image.Ready
|
||||
}
|
||||
|
||||
@@ -1963,7 +2031,7 @@ Item {
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon
|
||||
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
opacity: modelData.active ? 1.0 : colAppHover.hovered ? 0.8 : 0.6
|
||||
visible: modelData.isQuickshell
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
@@ -1977,7 +2045,7 @@ Item {
|
||||
id: colSteamIcon
|
||||
anchors.fill: parent
|
||||
source: modelData.icon
|
||||
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
opacity: modelData.active ? 1.0 : colAppHover.hovered ? 0.8 : 0.6
|
||||
visible: modelData.isSteamApp && modelData.icon
|
||||
}
|
||||
|
||||
@@ -1986,7 +2054,7 @@ Item {
|
||||
size: root.appIconSize
|
||||
name: "sports_esports"
|
||||
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
|
||||
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
opacity: modelData.active ? 1.0 : colAppHover.hovered ? 0.8 : 0.6
|
||||
visible: modelData.isSteamApp && !modelData.icon
|
||||
}
|
||||
|
||||
@@ -2000,21 +2068,8 @@ Item {
|
||||
z: 1
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: colAppMouseArea
|
||||
anchors.fill: parent
|
||||
enabled: isActive
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const winId = modelData.windowId;
|
||||
if (!winId)
|
||||
return;
|
||||
if (CompositorService.isHyprland) {
|
||||
HyprlandService.focusWindow(winId);
|
||||
} else if (CompositorService.isNiri) {
|
||||
NiriService.focusWindow(winId);
|
||||
}
|
||||
}
|
||||
HoverHandler {
|
||||
id: colAppHover
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -2121,6 +2176,13 @@ Item {
|
||||
delegateRoot.updateAllData();
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: AqueousService
|
||||
function onStateChanged() {
|
||||
if (root.useAqueous)
|
||||
delegateRoot.updateAllData();
|
||||
}
|
||||
}
|
||||
property var _extWindowsetsTrigger: root.useExtWorkspace ? WindowManager.windowsets : null
|
||||
on_ExtWindowsetsTriggerChanged: {
|
||||
if (root.useExtWorkspace)
|
||||
|
||||
@@ -543,6 +543,7 @@ Item {
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: row.selected ? Font.DemiBold : Font.Medium
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -551,6 +552,7 @@ Item {
|
||||
color: Theme.surfaceTextSecondary
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../../../Common/DateOnly.js" as DateOnly
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
@@ -35,53 +36,39 @@ Rectangle {
|
||||
return weekStartQt() % 7;
|
||||
}
|
||||
|
||||
function startOfWeek(dateObj) {
|
||||
const d = new Date(dateObj);
|
||||
const jsDow = d.getDay();
|
||||
const diff = (jsDow - weekStartJs() + 7) % 7;
|
||||
d.setDate(d.getDate() - diff);
|
||||
return d;
|
||||
function startOfWeek(dateOnly) {
|
||||
const diff = (dateOnly.dayOfWeek() - weekStartJs() + 7) % 7;
|
||||
return dateOnly.addDays(-diff);
|
||||
}
|
||||
|
||||
function endOfWeek(dateObj) {
|
||||
const d = new Date(dateObj);
|
||||
const jsDow = d.getDay();
|
||||
const add = (weekStartJs() + 6 - jsDow + 7) % 7;
|
||||
d.setDate(d.getDate() + add);
|
||||
return d;
|
||||
function endOfWeek(dateOnly) {
|
||||
const add = (weekStartJs() + 6 - dateOnly.dayOfWeek() + 7) % 7;
|
||||
return dateOnly.addDays(add);
|
||||
}
|
||||
|
||||
function getWeekNumber(dateObj) {
|
||||
// Set time to noon to avoid potential Daylight Saving Time related bugs
|
||||
const weekStartDay = startOfWeek(dateObj);
|
||||
weekStartDay.setHours(12, 0, 0, 0);
|
||||
function getWeekNumber(dateOnly) {
|
||||
const weekStartDay = startOfWeek(dateOnly);
|
||||
|
||||
let week1Start;
|
||||
|
||||
if (weekStartJs() === 1) {
|
||||
// ISO 8601 Standard, week start on Monday
|
||||
// A week belongs to the year its Thursday falls in
|
||||
// So we have to get the yearTarget from weekStartDay instead of dateObj
|
||||
let yearTarget = weekStartDay;
|
||||
yearTarget.setDate(yearTarget.getDate() + 3); // Monday + 3 = Thursday
|
||||
// So we have to get the yearTarget from weekStartDay instead of dateOnly
|
||||
const yearTarget = weekStartDay.addDays(3); // Monday + 3 = Thursday
|
||||
|
||||
// Week 1 is the week containing Jan 4th
|
||||
const jan4 = new Date(yearTarget.getFullYear(), 0, 4);
|
||||
week1Start = startOfWeek(jan4);
|
||||
week1Start = startOfWeek(DateOnly.of(yearTarget.year, 0, 4));
|
||||
} else {
|
||||
// Traditional / US Standard, week start on Sunday
|
||||
// A week belongs to the year its Sunday falls in
|
||||
let yearTarget = weekStartDay;
|
||||
yearTarget.setDate(yearTarget.getDate() + 6); // Monday + 6 = Sunday
|
||||
const yearTarget = weekStartDay.addDays(6); // Monday + 6 = Sunday
|
||||
|
||||
// Week 1 is the week containing Jan 1st
|
||||
const jan1 = new Date(yearTarget.getFullYear(), 0, 1);
|
||||
week1Start = startOfWeek(jan1);
|
||||
week1Start = startOfWeek(DateOnly.of(yearTarget.year, 0, 1));
|
||||
}
|
||||
|
||||
week1Start.setHours(12, 0, 0, 0);
|
||||
|
||||
const diffDays = Math.round((weekStartDay.getTime() - week1Start.getTime()) / 86400000); // Number of miliseconds in a day
|
||||
const diffDays = week1Start.daysUntil(weekStartDay);
|
||||
return Math.floor(diffDays / 7) + 1;
|
||||
}
|
||||
|
||||
@@ -97,16 +84,16 @@ Rectangle {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstOfMonth = new Date(calendarGrid.displayDate.getFullYear(), calendarGrid.displayDate.getMonth(), 1);
|
||||
const lastOfMonth = new Date(calendarGrid.displayDate.getFullYear(), calendarGrid.displayDate.getMonth() + 1, 0);
|
||||
const year = calendarGrid.displayDate.getFullYear();
|
||||
const month = calendarGrid.displayDate.getMonth();
|
||||
|
||||
const startDate = startOfWeek(firstOfMonth);
|
||||
startDate.setDate(startDate.getDate() - 7);
|
||||
const firstOfMonth = DateOnly.firstOfMonth(year, month);
|
||||
const lastOfMonth = DateOnly.lastOfMonth(year, month);
|
||||
|
||||
const endDate = endOfWeek(lastOfMonth);
|
||||
endDate.setDate(endDate.getDate() + 7);
|
||||
const startDate = startOfWeek(firstOfMonth).addDays(-7);
|
||||
const endDate = endOfWeek(lastOfMonth).addDays(7);
|
||||
|
||||
CalendarService.loadEvents(startDate, endDate);
|
||||
CalendarService.loadEvents(startDate.toDate(), endDate.toDate());
|
||||
}
|
||||
|
||||
function goToToday() {
|
||||
@@ -118,11 +105,11 @@ Rectangle {
|
||||
}
|
||||
|
||||
function moveSelection(days) {
|
||||
let d = new Date(calendarGrid.selectedDate);
|
||||
d.setDate(d.getDate() + days);
|
||||
const moved = DateOnly.fromDate(calendarGrid.selectedDate).addDays(days);
|
||||
const d = moved.toDate();
|
||||
calendarGrid.selectedDate = d;
|
||||
root.selectedDate = d;
|
||||
if (d.getMonth() !== calendarGrid.displayDate.getMonth() || d.getFullYear() !== calendarGrid.displayDate.getFullYear()) {
|
||||
if (moved.month !== calendarGrid.displayDate.getMonth() || moved.year !== calendarGrid.displayDate.getFullYear()) {
|
||||
calendarGrid.displayDate = d;
|
||||
loadEventsForMonth();
|
||||
}
|
||||
@@ -216,10 +203,13 @@ Rectangle {
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
id: cardColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
readonly property real warningInset: dankWarning.visible ? dankWarning.height + spacing : 0
|
||||
|
||||
Rectangle {
|
||||
id: dankWarning
|
||||
width: parent.width
|
||||
@@ -349,10 +339,27 @@ Rectangle {
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
readonly property bool isToday: {
|
||||
const now = systemClock.date;
|
||||
const disp = calendarGrid.displayDate;
|
||||
const sel = calendarGrid.selectedDate;
|
||||
return disp.getFullYear() === now.getFullYear() && disp.getMonth() === now.getMonth() && sel.getFullYear() === now.getFullYear() && sel.getMonth() === now.getMonth() && sel.getDate() === now.getDate();
|
||||
}
|
||||
|
||||
buttonSize: 28
|
||||
iconSize: 14
|
||||
iconName: "today"
|
||||
iconColor: Theme.primary
|
||||
iconColor: enabled ? Theme.primary : Theme.surfaceTextMedium
|
||||
enabled: !isToday
|
||||
opacity: enabled ? 1 : 0.38
|
||||
tooltipText: I18n.tr("Today")
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: root.goToToday()
|
||||
}
|
||||
|
||||
@@ -367,7 +374,7 @@ Rectangle {
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: parent.height - 28 - Theme.spacingS
|
||||
height: parent.height - 28 - Theme.spacingS - cardColumn.warningInset
|
||||
visible: !showEventDetails
|
||||
spacing: SettingsData.showWeekNumber ? Theme.spacingS : 0
|
||||
|
||||
@@ -399,8 +406,7 @@ Rectangle {
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
const rowDate = new Date(calendarGrid.firstDay);
|
||||
rowDate.setDate(rowDate.getDate() + index * 7);
|
||||
const rowDate = calendarGrid.firstDay.addDays(index * 7);
|
||||
return root.getWeekNumber(rowDate);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
@@ -458,8 +464,8 @@ Rectangle {
|
||||
property date displayDate: systemClock.date
|
||||
property date selectedDate: systemClock.date
|
||||
|
||||
readonly property date firstDay: {
|
||||
const firstOfMonth = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
|
||||
readonly property var firstDay: {
|
||||
const firstOfMonth = DateOnly.firstOfMonth(displayDate.getFullYear(), displayDate.getMonth());
|
||||
return startOfWeek(firstOfMonth);
|
||||
}
|
||||
|
||||
@@ -467,14 +473,10 @@ Rectangle {
|
||||
model: 42
|
||||
|
||||
Rectangle {
|
||||
readonly property date dayDate: {
|
||||
const date = new Date(parent.firstDay);
|
||||
date.setDate(date.getDate() + index);
|
||||
return date;
|
||||
}
|
||||
readonly property bool isCurrentMonth: dayDate.getMonth() === calendarGrid.displayDate.getMonth()
|
||||
readonly property bool isToday: dayDate.toDateString() === new Date().toDateString()
|
||||
readonly property bool isSelected: dayDate.toDateString() === calendarGrid.selectedDate.toDateString()
|
||||
readonly property var dayDate: calendarGrid.firstDay.addDays(index)
|
||||
readonly property bool isCurrentMonth: dayDate.month === calendarGrid.displayDate.getMonth()
|
||||
readonly property bool isToday: dayDate.equals(DateOnly.fromDate(systemClock.date))
|
||||
readonly property bool isSelected: dayDate.equals(DateOnly.fromDate(calendarGrid.selectedDate))
|
||||
|
||||
width: parent.width / 7
|
||||
height: parent.height / 6
|
||||
@@ -491,7 +493,7 @@ Rectangle {
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: dayDate.getDate()
|
||||
text: dayDate.day
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Theme.surfaceVariantText
|
||||
font.weight: isToday ? Font.Medium : Font.Normal
|
||||
@@ -502,11 +504,11 @@ Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottomMargin: 3
|
||||
spacing: Theme.spacingXXS
|
||||
visible: CalendarService && CalendarService.calendarAvailable && CalendarService.hasEventsForDate(dayDate)
|
||||
visible: CalendarService && CalendarService.calendarAvailable && CalendarService.hasEventsForDate(dayDate.toDate())
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
const evs = CalendarService.getEventsForDate(dayDate);
|
||||
const evs = CalendarService.getEventsForDate(dayDate.toDate());
|
||||
const seen = [];
|
||||
for (let i = 0; i < evs.length && seen.length < 3; i++) {
|
||||
const c = (evs[i].color && evs[i].color.length) ? evs[i].color : "primary";
|
||||
@@ -533,8 +535,8 @@ Rectangle {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
calendarGrid.selectedDate = dayDate;
|
||||
root.selectedDate = dayDate;
|
||||
calendarGrid.selectedDate = dayDate.toDate();
|
||||
root.selectedDate = dayDate.toDate();
|
||||
root.showEventDetails = true;
|
||||
}
|
||||
}
|
||||
@@ -547,7 +549,7 @@ Rectangle {
|
||||
Flickable {
|
||||
id: flickableArea
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: parent.height - (showEventDetails ? 40 + 42 : 28 + 18) - Theme.spacingS
|
||||
height: parent.height - (showEventDetails ? 40 + 42 : 28 + 18) - Theme.spacingS - cardColumn.warningInset
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: showEventDetails
|
||||
clip: true
|
||||
|
||||
@@ -14,23 +14,27 @@ Item {
|
||||
property color iconColor: Theme.primary
|
||||
property Component leading: null
|
||||
|
||||
function pushMeasuredWidth() {
|
||||
root.controller.setDestinationContentWidth(root.activityId, compactRow.implicitWidth);
|
||||
readonly property bool isVertical: root.controller.isVertical
|
||||
|
||||
function pushMeasuredLength() {
|
||||
root.controller.setDestinationContentLength(root.activityId, root.isVertical ? compactColumn.implicitHeight : compactRow.implicitWidth);
|
||||
}
|
||||
|
||||
Component.onCompleted: root.pushMeasuredWidth()
|
||||
Component.onCompleted: root.pushMeasuredLength()
|
||||
onIsVerticalChanged: root.pushMeasuredLength()
|
||||
|
||||
Row {
|
||||
id: compactRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
visible: !root.isVertical
|
||||
spacing: Theme.spacingS
|
||||
|
||||
onImplicitWidthChanged: root.pushMeasuredWidth()
|
||||
onImplicitWidthChanged: root.pushMeasuredLength()
|
||||
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
active: root.leading !== null
|
||||
active: !root.isVertical && root.leading !== null
|
||||
visible: active
|
||||
sourceComponent: root.leading
|
||||
}
|
||||
@@ -51,4 +55,30 @@ Item {
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
}
|
||||
|
||||
// The pill is only as wide as the strip on a side edge, so the label is dropped.
|
||||
Column {
|
||||
id: compactColumn
|
||||
|
||||
anchors.centerIn: parent
|
||||
visible: root.isVertical
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
onImplicitHeightChanged: root.pushMeasuredLength()
|
||||
|
||||
Loader {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: root.isVertical && root.leading !== null
|
||||
visible: active
|
||||
sourceComponent: root.leading
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.iconName !== ""
|
||||
name: root.iconName
|
||||
size: Theme.iconSizeSmall
|
||||
color: root.iconColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,15 @@ Item {
|
||||
required property var systemModel
|
||||
|
||||
readonly property string timeText: systemClock.date.toLocaleTimeString(I18n.locale(), SettingsData.getEffectiveTimeFormat())
|
||||
readonly property string hourText: {
|
||||
const hours = systemClock.date.getHours();
|
||||
if (SettingsData.use24HourClock)
|
||||
return String(hours).padStart(2, "0");
|
||||
return String(hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)).padStart(2, "0");
|
||||
}
|
||||
readonly property string minuteText: String(systemClock.date.getMinutes()).padStart(2, "0")
|
||||
readonly property string monthText: systemClock.date.toLocaleDateString(I18n.locale(), "MMM")
|
||||
readonly property string dayText: String(systemClock.date.getDate())
|
||||
readonly property string dateText: systemClock.date.toLocaleDateString(I18n.locale(), SettingsData.getEffectiveDateFormat("ddd MMM d"))
|
||||
readonly property bool tight: root.controller.homeCompactTight
|
||||
readonly property real slotSize: root.tight ? Theme.iconSize : Theme.iconSizeLarge
|
||||
@@ -21,7 +30,9 @@ Item {
|
||||
readonly property real iconSize: root.textSize + Theme.spacingXS
|
||||
readonly property real statusIconSize: root.textSize + Theme.spacingXXS
|
||||
readonly property real groupSpacing: root.controller.homeSlotMargin
|
||||
readonly property real edgePad: Math.max(root.groupSpacing, (root.width - compactRow.width) / 2)
|
||||
readonly property bool isVertical: root.controller.isVertical
|
||||
readonly property string clockDisplay: root.controller.homeClockDisplay
|
||||
readonly property real edgePad: Math.max(root.groupSpacing, ((root.isVertical ? root.height - compactColumn.height : root.width - compactRow.width)) / 2)
|
||||
readonly property bool weatherSlotEnabled: root.controller.homeWeatherEnabled
|
||||
readonly property var brightnessDevice: DisplayService.getCurrentDeviceInfo()
|
||||
readonly property real brightnessMaximum: DisplayService.brightnessMaximum(root.brightnessDevice)
|
||||
@@ -63,6 +74,29 @@ Item {
|
||||
return direction;
|
||||
}
|
||||
|
||||
function activateGroup(groupId) {
|
||||
switch (groupId) {
|
||||
case "media":
|
||||
if (root.controller.mediaAvailable) {
|
||||
root.controller.requestActivity("media", false, false);
|
||||
return;
|
||||
}
|
||||
root.controller.requestLauncher("", "", false);
|
||||
return;
|
||||
case "weather":
|
||||
root.controller.requestWeather(false);
|
||||
return;
|
||||
case "notifications":
|
||||
root.controller.requestNotificationCenter(false);
|
||||
return;
|
||||
case "volume":
|
||||
case "brightness":
|
||||
root.systemModel.open(groupId);
|
||||
return;
|
||||
}
|
||||
root.controller.requestControlCenter("", false);
|
||||
}
|
||||
|
||||
function adjustSystemLevel(activityId, delta) {
|
||||
const direction = root.wheelDirection(delta);
|
||||
if (direction === 0)
|
||||
@@ -104,10 +138,15 @@ Item {
|
||||
WeatherService.removeRef();
|
||||
}
|
||||
|
||||
function pushMeasuredLength() {
|
||||
root.controller.setHomeContentLength(root.isVertical ? compactColumn.implicitHeight : compactRow.implicitWidth);
|
||||
}
|
||||
|
||||
onWeatherSlotEnabledChanged: root.syncWeatherRef(root.weatherSlotEnabled)
|
||||
onIsVerticalChanged: root.pushMeasuredLength()
|
||||
Component.onCompleted: {
|
||||
root.syncWeatherRef(root.weatherSlotEnabled);
|
||||
root.controller.setHomeContentWidth(compactRow.implicitWidth);
|
||||
root.pushMeasuredLength();
|
||||
}
|
||||
Component.onDestruction: root.syncWeatherRef(false)
|
||||
|
||||
@@ -115,7 +154,15 @@ Item {
|
||||
target: compactRow
|
||||
|
||||
function onImplicitWidthChanged() {
|
||||
root.controller.setHomeContentWidth(compactRow.implicitWidth);
|
||||
root.pushMeasuredLength();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: compactColumn
|
||||
|
||||
function onImplicitHeightChanged() {
|
||||
root.pushMeasuredLength();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,12 +239,15 @@ Item {
|
||||
Row {
|
||||
id: clockRow
|
||||
|
||||
readonly property string displayMode: root.controller.homeClockDisplay
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: item.isClock
|
||||
spacing: Theme.spacingS
|
||||
|
||||
NumericText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: clockRow.displayMode !== "date"
|
||||
isMonospace: false
|
||||
text: root.timeText
|
||||
reserveText: root.timeText.replace(/\d/g, "0")
|
||||
@@ -208,6 +258,7 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: clockRow.displayMode === "both"
|
||||
width: Theme.spacingXS
|
||||
height: Theme.spacingXS
|
||||
radius: height / 2
|
||||
@@ -216,6 +267,7 @@ Item {
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: clockRow.displayMode !== "time"
|
||||
text: root.dateText
|
||||
color: Theme.surfaceTextMedium
|
||||
font.pixelSize: root.textSize
|
||||
@@ -301,8 +353,8 @@ Item {
|
||||
thickness: root.statusIconSize
|
||||
fontSize: root.textSize
|
||||
hovered: groupArea.containsMouse
|
||||
meterStyle: SettingsData.dankIslandBatteryStyle
|
||||
levelColors: (SettingsData.islandBarConfig?.batteryColorMode ?? "theme") === "level"
|
||||
meterStyle: root.controller.batteryStyle
|
||||
levelColors: (root.controller.barConfig?.batteryColorMode ?? "theme") === "level"
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
@@ -322,29 +374,201 @@ Item {
|
||||
height: root.height
|
||||
enabled: !item.isClock
|
||||
controller: root.controller
|
||||
onClicked: {
|
||||
if (item.isMedia && root.controller.mediaAvailable) {
|
||||
root.controller.requestActivity("media", false, false);
|
||||
onClicked: root.activateGroup(item.groupId)
|
||||
onWheel: wheel => {
|
||||
if (!item.isVolume && !item.isBrightness)
|
||||
return;
|
||||
}
|
||||
if (item.isMedia) {
|
||||
root.controller.requestLauncher("", "", false);
|
||||
return;
|
||||
}
|
||||
if (item.isWeather) {
|
||||
root.controller.requestWeather(false);
|
||||
return;
|
||||
}
|
||||
if (item.isNotifications) {
|
||||
root.controller.requestNotificationCenter(false);
|
||||
return;
|
||||
}
|
||||
if (item.isVolume || item.isBrightness) {
|
||||
root.systemModel.open(item.groupId);
|
||||
return;
|
||||
}
|
||||
root.controller.requestControlCenter("", false);
|
||||
root.adjustSystemLevel(item.groupId, wheel.angleDelta.y || wheel.angleDelta.x);
|
||||
wheel.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Side edges only have the strip's width to work with, so every slot stacks its own content.
|
||||
component VerticalGroupItem: Item {
|
||||
id: item
|
||||
|
||||
required property string groupId
|
||||
required property int slotIndex
|
||||
|
||||
readonly property real leadPad: item.slotIndex === 0 ? root.edgePad : root.groupSpacing / 2
|
||||
readonly property real trailPad: item.slotIndex === root.groupIds.length - 1 ? root.edgePad : root.groupSpacing / 2
|
||||
readonly property bool isClock: item.groupId === "clock"
|
||||
readonly property bool isMedia: item.groupId === "media"
|
||||
readonly property bool isWeather: item.groupId === "weather"
|
||||
readonly property bool isStatus: item.groupId === "status"
|
||||
readonly property bool isNotifications: item.groupId === "notifications"
|
||||
readonly property bool isVolume: item.groupId === "volume"
|
||||
readonly property bool isBrightness: item.groupId === "brightness"
|
||||
readonly property bool usesBattery: item.isStatus && BatteryService.batteryAvailable
|
||||
|
||||
width: root.width
|
||||
height: stack.implicitHeight
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
visible: !item.isClock
|
||||
width: root.slotSize
|
||||
height: parent.height + Theme.spacingXS
|
||||
radius: Theme.cornerRadius
|
||||
color: groupArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
}
|
||||
|
||||
Column {
|
||||
id: stack
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: 0
|
||||
|
||||
NumericText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: item.isClock && root.clockDisplay !== "date"
|
||||
isMonospace: false
|
||||
text: root.hourText
|
||||
reserveText: "00"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: root.textSize
|
||||
}
|
||||
|
||||
NumericText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: item.isClock && root.clockDisplay !== "date"
|
||||
isMonospace: false
|
||||
text: root.minuteText
|
||||
reserveText: "00"
|
||||
color: Theme.surfaceTextMedium
|
||||
font.pixelSize: root.textSize
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: item.isClock && root.clockDisplay === "both"
|
||||
width: Theme.spacingXS
|
||||
height: root.textSize
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: Theme.spacingXS
|
||||
height: Theme.spacingXS
|
||||
radius: height / 2
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: item.isClock && root.clockDisplay !== "time"
|
||||
text: root.monthText
|
||||
color: Theme.surfaceTextMedium
|
||||
font.pixelSize: root.textSize
|
||||
}
|
||||
|
||||
NumericText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: item.isClock && root.clockDisplay !== "time"
|
||||
isMonospace: false
|
||||
text: root.dayText
|
||||
reserveText: "00"
|
||||
color: Theme.surfaceTextMedium
|
||||
font.pixelSize: root.textSize
|
||||
}
|
||||
|
||||
AudioVisualization {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: root.iconSize + Theme.spacingXS
|
||||
height: width
|
||||
maxBarHeight: Math.max(3, height - 2)
|
||||
idleIconName: "graphic_eq"
|
||||
visible: item.isMedia && root.controller.mediaAvailable
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: item.isMedia && !root.controller.mediaAvailable
|
||||
name: "search"
|
||||
size: root.iconSize
|
||||
color: Theme.surfaceTextMedium
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: item.isWeather
|
||||
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
|
||||
size: root.statusIconSize
|
||||
color: Theme.surfaceTextSecondary
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: item.isWeather
|
||||
text: WeatherService.currentTempText()
|
||||
color: Theme.surfaceTextSecondary
|
||||
font.pixelSize: root.textSize
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: item.isNotifications
|
||||
name: "notifications"
|
||||
size: root.statusIconSize
|
||||
color: Theme.secondary
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: item.isNotifications
|
||||
text: root.controller.unreadNotificationCount
|
||||
color: Theme.surfaceTextSecondary
|
||||
font.pixelSize: root.textSize
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: item.isVolume || item.isBrightness
|
||||
name: item.isVolume ? AudioService.sinkVolumeIconName : DisplayService.brightnessIconName(root.brightnessDevice, DisplayService.brightnessLevel)
|
||||
size: root.statusIconSize
|
||||
color: Theme.surfaceTextSecondary
|
||||
}
|
||||
|
||||
NumericText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: (item.isVolume || item.isBrightness) && (item.isVolume ? root.controller.homeVolumeDisplay : root.controller.homeBrightnessDisplay) !== "icon"
|
||||
text: item.isVolume ? root.volumePercent : root.brightnessPercent
|
||||
reserveText: item.isVolume ? String(AudioService.sinkMaxVolume) : "100"
|
||||
color: Theme.surfaceTextSecondary
|
||||
font.pixelSize: root.textSize
|
||||
}
|
||||
|
||||
BatteryMeter {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: item.usesBattery
|
||||
vertical: true
|
||||
thickness: root.statusIconSize
|
||||
fontSize: root.textSize
|
||||
hovered: groupArea.containsMouse
|
||||
meterStyle: root.controller.batteryStyle
|
||||
levelColors: (root.controller.barConfig?.batteryColorMode ?? "theme") === "level"
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: item.isStatus && !item.usesBattery
|
||||
name: "tune"
|
||||
size: root.iconSize
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
IslandSlotHoverArea {
|
||||
id: groupArea
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: -item.leadPad
|
||||
width: root.width
|
||||
height: parent.height + item.leadPad + item.trailPad
|
||||
enabled: !item.isClock
|
||||
controller: root.controller
|
||||
onClicked: root.activateGroup(item.groupId)
|
||||
onWheel: wheel => {
|
||||
if (!item.isVolume && !item.isBrightness)
|
||||
return;
|
||||
@@ -358,11 +582,12 @@ Item {
|
||||
id: compactRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
visible: !root.isVertical
|
||||
height: root.slotSize
|
||||
spacing: root.groupSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.groupIds
|
||||
model: root.isVertical ? [] : root.groupIds
|
||||
|
||||
GroupItem {
|
||||
required property var modelData
|
||||
@@ -372,4 +597,24 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: compactColumn
|
||||
|
||||
anchors.centerIn: parent
|
||||
visible: root.isVertical
|
||||
width: root.width
|
||||
spacing: root.groupSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.isVertical ? root.groupIds : []
|
||||
|
||||
VerticalGroupItem {
|
||||
required property var modelData
|
||||
required property int index
|
||||
groupId: String(modelData)
|
||||
slotIndex: index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ Item {
|
||||
required property var controller
|
||||
|
||||
readonly property bool dense: root.controller.compactDense
|
||||
readonly property bool isVertical: root.controller.isVertical
|
||||
readonly property real artworkSize: root.controller.compactIconSize
|
||||
readonly property real textGutter: Theme.spacingXS
|
||||
readonly property real cavaWidth: 20
|
||||
@@ -21,8 +22,18 @@ Item {
|
||||
readonly property real clockLeadPad: Theme.spacingXS / 2
|
||||
readonly property real clockTrailPad: Theme.spacingS
|
||||
readonly property real clockPillHeight: root.dense ? Math.max(22, root.artworkSize - Theme.spacingXS) : 32
|
||||
// A fully rounded side strip crowds square content, so the vertical face insets on both axes.
|
||||
readonly property real verticalPad: Theme.spacingS
|
||||
readonly property real verticalArtworkSize: Math.max(12, Math.min(root.artworkSize, root.width - root.verticalPad * 2))
|
||||
readonly property string timeText: systemClock.date.toLocaleTimeString(I18n.locale(), SettingsData.getEffectiveTimeFormat())
|
||||
readonly property bool clockVisible: SettingsData.dankIslandMediaClockVisible
|
||||
readonly property string hourText: {
|
||||
const hours = systemClock.date.getHours();
|
||||
if (SettingsData.use24HourClock)
|
||||
return String(hours).padStart(2, "0");
|
||||
return String(hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)).padStart(2, "0");
|
||||
}
|
||||
readonly property string minuteText: String(systemClock.date.getMinutes()).padStart(2, "0")
|
||||
readonly property bool clockVisible: root.controller.mediaClockVisible
|
||||
readonly property real naturalTextWidth: Math.max(root.minTextWidth, titleMetrics.width, root.dense ? 0 : artistMetrics.width) + root.textGutter * 2
|
||||
readonly property real measuredWidth: {
|
||||
let width = Theme.spacingS + root.artworkSize + Theme.spacingXS + root.cavaWidth + Theme.spacingXS + root.naturalTextWidth + Theme.spacingXS;
|
||||
@@ -31,12 +42,13 @@ Item {
|
||||
return width;
|
||||
}
|
||||
|
||||
function pushMeasuredWidth() {
|
||||
root.controller.setMediaContentWidth(root.measuredWidth);
|
||||
function pushMeasuredLength() {
|
||||
root.controller.setMediaContentLength(root.isVertical ? verticalFace.implicitHeight + root.verticalPad * 2 : root.measuredWidth);
|
||||
}
|
||||
|
||||
onMeasuredWidthChanged: root.pushMeasuredWidth()
|
||||
Component.onCompleted: root.pushMeasuredWidth()
|
||||
onMeasuredWidthChanged: root.pushMeasuredLength()
|
||||
onIsVerticalChanged: root.pushMeasuredLength()
|
||||
Component.onCompleted: root.pushMeasuredLength()
|
||||
|
||||
StyledTextMetrics {
|
||||
id: titleMetrics
|
||||
@@ -67,12 +79,14 @@ Item {
|
||||
leftMargin: Theme.spacingS
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
visible: !root.isVertical
|
||||
width: root.artworkSize
|
||||
height: width
|
||||
artUrl: root.mediaModel.artUrl
|
||||
}
|
||||
|
||||
Row {
|
||||
visible: !root.isVertical
|
||||
anchors {
|
||||
left: artwork.right
|
||||
leftMargin: Theme.spacingXS
|
||||
@@ -133,7 +147,7 @@ Item {
|
||||
}
|
||||
width: root.clockVisible ? (clockText.width + Theme.spacingXS * 2) : 0
|
||||
height: root.clockPillHeight
|
||||
visible: root.clockVisible
|
||||
visible: root.clockVisible && !root.isVertical
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
@@ -166,4 +180,74 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A side strip has no room for track text, so the vertical face is artwork over the meter.
|
||||
Column {
|
||||
id: verticalFace
|
||||
|
||||
anchors.centerIn: parent
|
||||
visible: root.isVertical
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
onImplicitHeightChanged: root.pushMeasuredLength()
|
||||
|
||||
MediaArtwork {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: root.verticalArtworkSize
|
||||
height: width
|
||||
artUrl: root.mediaModel.artUrl
|
||||
}
|
||||
|
||||
AudioVisualization {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: root.verticalArtworkSize
|
||||
height: root.cavaWidth
|
||||
maxBarHeight: Math.max(3, height - 2)
|
||||
idleIconName: "graphic_eq"
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.clockVisible
|
||||
width: verticalClock.width
|
||||
height: verticalClock.height
|
||||
|
||||
// A side strip cannot fit "10:42 PM" on one line, so the hour sits over the minute.
|
||||
Column {
|
||||
id: verticalClock
|
||||
|
||||
spacing: 0
|
||||
|
||||
NumericText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
isMonospace: false
|
||||
text: root.hourText
|
||||
reserveText: "00"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
NumericText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
isMonospace: false
|
||||
text: root.minuteText
|
||||
reserveText: "00"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Theme.surfaceTextMedium
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
}
|
||||
|
||||
IslandSlotHoverArea {
|
||||
anchors.centerIn: parent
|
||||
width: root.width
|
||||
height: parent.height + Theme.spacingXS
|
||||
controller: root.controller
|
||||
onClicked: root.controller.requestActivity("home", false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,33 @@ Item {
|
||||
property bool dense: false
|
||||
property real iconSize: 36
|
||||
|
||||
readonly property bool isVertical: root.controller.isVertical
|
||||
readonly property string headerText: root.notificationModel.appName + (root.notificationModel.timeText ? " · " + root.notificationModel.timeText : "")
|
||||
readonly property string summaryText: root.dense && root.notificationModel.appName ? root.notificationModel.appName + " · " + root.notificationModel.summary : root.notificationModel.summary
|
||||
readonly property real measuredWidth: Theme.spacingS * 3 + root.iconSize + Theme.spacingXS + Math.max(root.dense ? 0 : headerLabel.implicitWidth, summaryLabel.implicitWidth)
|
||||
|
||||
function pushMeasuredWidth() {
|
||||
root.controller.setNotificationContentWidth(root.measuredWidth);
|
||||
function pushMeasuredLength() {
|
||||
root.controller.setNotificationContentLength(root.isVertical ? root.iconSize + Theme.spacingS * 2 : root.measuredWidth);
|
||||
}
|
||||
|
||||
onMeasuredWidthChanged: root.pushMeasuredWidth()
|
||||
Component.onCompleted: root.pushMeasuredWidth()
|
||||
onMeasuredWidthChanged: root.pushMeasuredLength()
|
||||
onIsVerticalChanged: root.pushMeasuredLength()
|
||||
Component.onCompleted: root.pushMeasuredLength()
|
||||
|
||||
// A side strip only fits the sender's icon; the text lives in the expanded face.
|
||||
DankCircularImage {
|
||||
anchors.centerIn: parent
|
||||
visible: root.isVertical
|
||||
width: root.iconSize
|
||||
height: width
|
||||
imageSource: root.notificationModel.imageSource
|
||||
fallbackIcon: root.notificationModel.fallbackIcon
|
||||
fallbackText: root.notificationModel.fallbackText
|
||||
cacheImages: false
|
||||
}
|
||||
|
||||
Row {
|
||||
visible: !root.isVertical
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: Theme.spacingS
|
||||
|
||||
@@ -3,27 +3,149 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modules.OSD
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var systemModel
|
||||
property bool isVertical: false
|
||||
property real iconSize: 36
|
||||
|
||||
readonly property int maximum: Math.max(root.systemModel.minimum + 1, Math.round(root.systemModel.maximum))
|
||||
readonly property real fillRatio: Math.max(0, Math.min(1, (root.systemModel.value - root.systemModel.minimum) / Math.max(1, root.maximum - root.systemModel.minimum)))
|
||||
// DankSlider's own metrics, transposed, so the two orientations read as the same control.
|
||||
readonly property real trackThickness: 12
|
||||
readonly property real handleThickness: 4
|
||||
readonly property real handleBreadth: 20
|
||||
readonly property real fillHandleGap: 3
|
||||
|
||||
function trackFraction(localY) {
|
||||
if (track.travel <= 0)
|
||||
return 0;
|
||||
return 1 - (localY - root.handleThickness / 2) / track.travel;
|
||||
}
|
||||
|
||||
function applyFraction(fraction) {
|
||||
const clamped = Math.max(0, Math.min(1, fraction));
|
||||
const level = root.systemModel.minimum + clamped * (root.maximum - root.systemModel.minimum);
|
||||
root.systemModel.setRatio(level / Math.max(1, root.maximum));
|
||||
}
|
||||
|
||||
OsdLevelRow {
|
||||
anchors.fill: parent
|
||||
visible: !root.isVertical
|
||||
iconName: root.systemModel.iconName
|
||||
iconSize: Math.min(Theme.iconSize, root.iconSize)
|
||||
iconColor: root.systemModel.volumeActivity ? Theme.surfaceText : Theme.primary
|
||||
iconInteractive: root.systemModel.volumeActivity
|
||||
value: Math.round(root.systemModel.value)
|
||||
minimum: root.systemModel.minimum
|
||||
maximum: Math.max(root.systemModel.minimum + 1, Math.round(root.systemModel.maximum))
|
||||
maximum: root.maximum
|
||||
unit: root.systemModel.unit
|
||||
displayText: root.systemModel.displayValue
|
||||
sliderEnabled: root.systemModel.available
|
||||
thumbOutlineColor: Theme.surfaceContainerHigh
|
||||
onIconClicked: root.systemModel.toggleMute()
|
||||
onSliderValueChanged: newValue => root.systemModel.setRatio(newValue / Math.max(1, Math.round(root.systemModel.maximum)))
|
||||
onSliderValueChanged: newValue => root.systemModel.setRatio(newValue / Math.max(1, root.maximum))
|
||||
}
|
||||
|
||||
// DankSlider is horizontal only, so a side strip gets its own stacked track.
|
||||
Item {
|
||||
id: verticalFace
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: root.isVertical
|
||||
|
||||
DankIcon {
|
||||
id: levelIcon
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
name: root.systemModel.iconName
|
||||
size: Math.min(Theme.iconSize, root.iconSize)
|
||||
color: root.systemModel.volumeActivity ? Theme.surfaceText : Theme.primary
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.systemModel.volumeActivity
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: root.systemModel.toggleMute()
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: levelLabel
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
text: root.systemModel.displayValue.length > 0 ? root.systemModel.displayValue : (Math.round(root.systemModel.value) + root.systemModel.unit)
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Item {
|
||||
id: track
|
||||
|
||||
readonly property real travel: Math.max(0, height - root.handleThickness)
|
||||
readonly property real handleOffset: track.travel * root.fillRatio
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: levelIcon.bottom
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottom: levelLabel.top
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
width: root.handleBreadth
|
||||
|
||||
Rectangle {
|
||||
id: groove
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.trackThickness
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.outline, Theme.popupTransparency)
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: Math.max(0, Math.min(parent.height, track.handleOffset - root.fillHandleGap))
|
||||
radius: Theme.cornerRadius
|
||||
topLeftRadius: 0
|
||||
topRightRadius: 0
|
||||
color: root.systemModel.available ? Theme.primary : Theme.withAlpha(Theme.surfaceText, 0.12)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: track.travel - track.handleOffset
|
||||
width: root.handleBreadth
|
||||
height: root.handleThickness
|
||||
radius: Theme.cornerRadius
|
||||
color: root.systemModel.available ? Theme.primary : Theme.withAlpha(Theme.surfaceText, 0.12)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: trackArea
|
||||
|
||||
readonly property real grabPad: Theme.spacingS
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: -grabPad
|
||||
enabled: root.systemModel.available
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onPressed: mouse => root.applyFraction(root.trackFraction(mouse.y - grabPad))
|
||||
onPositionChanged: mouse => {
|
||||
if (pressed)
|
||||
root.applyFraction(root.trackFraction(mouse.y - grabPad));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,34 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var screenModel: Quickshell.screens
|
||||
property var hyprlandOverviewLoader: null
|
||||
|
||||
// One host per (island instance, screen), skipping a screen edge another island already holds.
|
||||
readonly property var hostSlots: {
|
||||
SettingsData.barConfigs;
|
||||
Quickshell.screens;
|
||||
const slots = [];
|
||||
for (const screen of Quickshell.screens) {
|
||||
const claimed = {};
|
||||
for (const config of SettingsData.activeIslandConfigsForScreen(screen)) {
|
||||
const edge = SettingsData.islandEdge(config);
|
||||
if (claimed[edge])
|
||||
continue;
|
||||
claimed[edge] = true;
|
||||
slots.push({
|
||||
"barId": config.id,
|
||||
"screen": screen
|
||||
});
|
||||
}
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
readonly property bool launcherOpen: root.activityOpen("launcher")
|
||||
readonly property bool controlCenterOpen: root.activityOpen("controlcenter")
|
||||
@@ -217,7 +239,7 @@ Item {
|
||||
"notificationCenterAvailable": true,
|
||||
"launcherInputFocused": host.islandController.launcherInputFocused,
|
||||
"launcherResultCount": host.launcherResultCount,
|
||||
"compactHeight": host.islandController.compactHeight
|
||||
"compactHeight": host.islandController.compactThickness
|
||||
});
|
||||
}
|
||||
|
||||
@@ -230,12 +252,14 @@ Item {
|
||||
Variants {
|
||||
id: islandVariants
|
||||
|
||||
model: root.screenModel
|
||||
model: root.hostSlots
|
||||
|
||||
delegate: DankIslandHostWindow {
|
||||
required property var modelData
|
||||
|
||||
screen: modelData
|
||||
screen: modelData.screen
|
||||
barId: modelData.barId
|
||||
hyprlandOverviewLoader: root.hyprlandOverviewLoader
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import qs.Widgets
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property var hyprlandOverviewLoader: null
|
||||
|
||||
function requestKeyboardFocus() {
|
||||
if (!controller.keyboardDismissRequested) {
|
||||
keyboardActivationTimer.stop();
|
||||
@@ -26,7 +28,7 @@ PanelWindow {
|
||||
|
||||
function containsGlobalPoint(gx, gy, padding) {
|
||||
const pad = padding !== undefined ? padding : 16;
|
||||
const items = [surface.inputMaskItem, surface.fittsStripItem, satelliteHost.leftInputItem, satelliteHost.rightInputItem];
|
||||
const items = [surface.inputMaskItem, surface.fittsStripItem, satelliteHost.leadingInputItem, satelliteHost.trailingInputItem];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (!item || item.width <= 0 || item.height <= 0)
|
||||
@@ -35,40 +37,55 @@ PanelWindow {
|
||||
if (!topLeft)
|
||||
continue;
|
||||
// mapToItem(null) is window-local; callers pass screen-space cursor coords.
|
||||
const left = topLeft.x + root.hostOriginX;
|
||||
const top = topLeft.y + root.hostOriginY;
|
||||
if (gx >= topLeft.x - pad && gx < topLeft.x + item.width + pad && gy >= top - pad && gy < top + item.height + pad)
|
||||
if (gx >= left - pad && gx < left + item.width + pad && gy >= top - pad && gy < top + item.height + pad)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
readonly property int reserveHeight: Math.max(24, Math.min(128, SettingsData.dankIslandReserveHeight))
|
||||
readonly property int compactHeight: Math.max(24, Math.min(72, SettingsData.dankIslandCompactHeight))
|
||||
readonly property bool floating: SettingsData.dankIslandFloating
|
||||
readonly property bool usesOverlayLayer: LayerShell.envUsesOverlay("DMS_DANKISLAND_LAYER", SettingsData.dankIslandUseOverlayLayer)
|
||||
required property string barId
|
||||
readonly property var barConfig: {
|
||||
SettingsData.barConfigs;
|
||||
return SettingsData.getBarConfig(root.barId);
|
||||
}
|
||||
|
||||
function setting(key) {
|
||||
return SettingsData.islandSetting(root.barConfig, key);
|
||||
}
|
||||
|
||||
readonly property int reserveThickness: Math.max(24, Math.min(128, root.setting("islandReserveThickness")))
|
||||
readonly property int compactThickness: Math.max(24, Math.min(72, root.setting("islandCompactThickness")))
|
||||
readonly property bool floating: root.setting("islandFloating")
|
||||
readonly property bool usesOverlayLayer: LayerShell.envUsesOverlay("DMS_DANKISLAND_LAYER", root.setting("islandUseOverlayLayer"))
|
||||
readonly property var islandLayer: LayerShell.fromEnv("DMS_DANKISLAND_LAYER", usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top)
|
||||
readonly property bool bottomEdge: SettingsData.dankIslandEdge === "bottom"
|
||||
readonly property int reservedStripHeight: Math.max(reserveHeight, outerGap + compactHeight)
|
||||
readonly property int stripY: root.bottomEdge ? root.height - root.reservedStripHeight : 0
|
||||
readonly property int hostOriginY: root.bottomEdge ? Math.max(0, (root.screen?.height ?? 0) - root.height) : 0
|
||||
readonly property int outerGap: Math.max(0, Math.min(48, SettingsData.dankIslandOuterGap))
|
||||
readonly property string edge: SettingsData.islandEdge(root.barConfig)
|
||||
readonly property bool isVertical: SettingsData.islandVertical(root.barConfig)
|
||||
readonly property bool farEdge: root.edge === "bottom" || root.edge === "right"
|
||||
readonly property int reservedStripThickness: Math.max(reserveThickness, outerGap + compactThickness)
|
||||
readonly property int stripPos: root.farEdge ? (root.isVertical ? root.width : root.height) - root.reservedStripThickness : 0
|
||||
readonly property int hostOriginX: root.isVertical && root.farEdge ? Math.max(0, (root.screen?.width ?? 0) - root.width) : 0
|
||||
readonly property int hostOriginY: !root.isVertical && root.farEdge ? Math.max(0, (root.screen?.height ?? 0) - root.height) : 0
|
||||
readonly property int outerGap: Math.max(0, Math.min(48, root.setting("islandOuterGap")))
|
||||
readonly property int maxActivityHeight: Math.max(560, Math.min(680, (screen?.height ?? 1080) - 200))
|
||||
readonly property int hostHeight: outerGap + maxActivityHeight + 8
|
||||
readonly property int maxActivityWidth: 736
|
||||
readonly property real maximumHorizontalOffset: Math.max(0, (width - maxActivityWidth) / 2 - 8)
|
||||
readonly property int maxActivityWidth: Math.min(736, Math.max(320, (screen?.width ?? 1920) - 200))
|
||||
readonly property int hostThickness: outerGap + (root.isVertical ? maxActivityWidth : maxActivityHeight) + 8
|
||||
readonly property real maximumAlongOffset: root.isVertical ? Math.max(0, (height - maxActivityHeight) / 2 - 8) : Math.max(0, (width - maxActivityWidth) / 2 - 8)
|
||||
property bool keyboardFocusArmed: true
|
||||
|
||||
color: "transparent"
|
||||
implicitHeight: hostHeight
|
||||
exclusiveZone: floating ? 0 : reservedStripHeight
|
||||
implicitHeight: hostThickness
|
||||
implicitWidth: hostThickness
|
||||
exclusiveZone: floating ? 0 : reservedStripThickness
|
||||
readonly property alias islandController: controller
|
||||
readonly property int launcherResultCount: launcherController.flatModel?.length ?? 0
|
||||
|
||||
anchors {
|
||||
top: !root.bottomEdge
|
||||
bottom: root.bottomEdge
|
||||
left: true
|
||||
right: true
|
||||
top: root.isVertical || root.edge === "top"
|
||||
bottom: root.isVertical || root.edge === "bottom"
|
||||
left: !root.isVertical || root.edge === "left"
|
||||
right: !root.isVertical || root.edge === "right"
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "dms:dankisland"
|
||||
@@ -131,11 +148,11 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Region {
|
||||
item: controller.inputSuspended ? null : satelliteHost.leftInputItem
|
||||
item: controller.inputSuspended ? null : satelliteHost.leadingInputItem
|
||||
}
|
||||
|
||||
Region {
|
||||
item: controller.inputSuspended ? null : satelliteHost.rightInputItem
|
||||
item: controller.inputSuspended ? null : satelliteHost.trailingInputItem
|
||||
}
|
||||
|
||||
Region {
|
||||
@@ -150,18 +167,27 @@ PanelWindow {
|
||||
IslandController {
|
||||
id: controller
|
||||
|
||||
interactionMode: SettingsData.dankIslandInteractionMode === "click" ? "click" : "hybrid"
|
||||
barConfig: root.barConfig
|
||||
edge: root.edge
|
||||
interactionMode: root.setting("islandInteractionMode") === "click" ? "click" : "hybrid"
|
||||
inputSuspended: PopoutManager.screenshotActive
|
||||
horizontalOffset: Math.max(-root.maximumHorizontalOffset, Math.min(root.maximumHorizontalOffset, SettingsData.dankIslandHorizontalOffset))
|
||||
alongOffset: Math.max(-root.maximumAlongOffset, Math.min(root.maximumAlongOffset, root.setting("islandAlongOffset")))
|
||||
outerGap: root.outerGap
|
||||
compactHeight: root.compactHeight
|
||||
compactThickness: root.compactThickness
|
||||
cornerRadius: Math.max(0, Math.min(64, root.setting("islandCornerRadius")))
|
||||
homeCompactTight: root.setting("islandHomeCompactTight")
|
||||
homeClockDisplay: SettingsData.islandClockDisplay(root.barConfig)
|
||||
homeVolumeDisplay: SettingsData.islandLevelDisplay(root.barConfig, "islandHomeVolumeDisplay")
|
||||
homeBrightnessDisplay: SettingsData.islandLevelDisplay(root.barConfig, "islandHomeBrightnessDisplay")
|
||||
batteryStyle: root.setting("islandBatteryStyle")
|
||||
mediaClockVisible: root.setting("islandMediaClockVisible")
|
||||
launcherCycleEnabled: SettingsData.launcherStyle === "island"
|
||||
controlCenterMaxHeight: root.maxActivityHeight
|
||||
notificationCenterMaxHeight: root.maxActivityHeight
|
||||
notificationExpandAllowed: SettingsData.dankIslandNotificationExpand
|
||||
unreadNotificationCount: SettingsData.dankIslandNotificationBadgeClearOnOpen ? NotificationService.unreadCount : NotificationService.notifications.length
|
||||
hoverOpenDelay: Math.max(0, Math.min(1000, SettingsData.dankIslandHoverOpenDelay))
|
||||
hoverCloseDelay: Math.max(0, Math.min(1000, SettingsData.dankIslandHoverCloseDelay))
|
||||
notificationExpandAllowed: root.setting("islandNotificationExpand")
|
||||
unreadNotificationCount: root.setting("islandNotificationBadgeClearOnOpen") ? NotificationService.unreadCount : NotificationService.notifications.length
|
||||
hoverOpenDelay: Math.max(0, Math.min(1000, root.setting("islandHoverOpenDelay")))
|
||||
hoverCloseDelay: Math.max(0, Math.min(1000, root.setting("islandHoverCloseDelay")))
|
||||
}
|
||||
|
||||
DankLauncher.Controller {
|
||||
@@ -221,11 +247,11 @@ PanelWindow {
|
||||
MouseArea {
|
||||
id: satelliteDismissStrip
|
||||
|
||||
x: 0
|
||||
y: root.stripY
|
||||
x: root.isVertical ? root.stripPos : 0
|
||||
y: root.isVertical ? 0 : root.stripPos
|
||||
z: -2
|
||||
width: parent.width
|
||||
height: root.reservedStripHeight
|
||||
width: root.isVertical ? root.reservedStripThickness : parent.width
|
||||
height: root.isVertical ? parent.height : root.reservedStripThickness
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
hoverEnabled: false
|
||||
enabled: root.satelliteSurfacesOpen && !controller.inputSuspended
|
||||
@@ -235,11 +261,11 @@ PanelWindow {
|
||||
BarScrollArea {
|
||||
id: scrollStrip
|
||||
|
||||
x: 0
|
||||
y: root.stripY
|
||||
x: root.isVertical ? root.stripPos : 0
|
||||
y: root.isVertical ? 0 : root.stripPos
|
||||
z: -1
|
||||
width: parent.width
|
||||
height: root.reservedStripHeight
|
||||
width: root.isVertical ? root.reservedStripThickness : parent.width
|
||||
height: root.isVertical ? parent.height : root.reservedStripThickness
|
||||
hoverEnabled: false
|
||||
enabled: satelliteHost.scrollEnabled && !controller.inputSuspended
|
||||
scrollEnabled: satelliteHost.scrollEnabled
|
||||
@@ -261,13 +287,15 @@ PanelWindow {
|
||||
launcherTransientSurfaceTracker: launcherTransientSurfaces
|
||||
notificationTransientSurfaceTracker: notificationTransientSurfaces
|
||||
effectiveScreen: root.screen
|
||||
hostOriginX: root.hostOriginX
|
||||
hostOriginY: root.hostOriginY
|
||||
reducedMotion: SettingsData.dankIslandReducedMotion || SettingsData.reduceMotion || SettingsData.animationSpeed === SettingsData.AnimationSpeed.None
|
||||
springStiffness: Math.max(100, Math.min(1200, SettingsData.dankIslandSpringStiffness))
|
||||
springDamping: Math.max(10, Math.min(100, SettingsData.dankIslandSpringDamping))
|
||||
springMass: Math.max(0.25, Math.min(3, SettingsData.dankIslandSpringMass))
|
||||
palette: SettingsData.dankIslandPalette
|
||||
highContrast: SettingsData.dankIslandHighContrast
|
||||
reducedMotion: root.setting("islandReducedMotion") || SettingsData.reduceMotion || SettingsData.animationSpeed === SettingsData.AnimationSpeed.None
|
||||
springStiffness: Math.max(100, Math.min(1200, root.setting("islandSpringStiffness")))
|
||||
springDamping: Math.max(10, Math.min(100, root.setting("islandSpringDamping")))
|
||||
springMass: Math.max(0.25, Math.min(3, root.setting("islandSpringMass")))
|
||||
palette: root.setting("islandPalette")
|
||||
highContrast: root.setting("islandHighContrast")
|
||||
transparency: root.setting("islandTransparency")
|
||||
onScrollWheel: wheel => scrollStrip.processWheel(wheel)
|
||||
}
|
||||
|
||||
@@ -277,9 +305,11 @@ PanelWindow {
|
||||
anchors.fill: parent
|
||||
hostWindow: root
|
||||
targetScreen: root.screen
|
||||
barConfig: root.barConfig
|
||||
controller: controller
|
||||
islandSurface: surface
|
||||
outerGap: root.outerGap
|
||||
hyprlandOverviewLoader: root.hyprlandOverviewLoader
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
@@ -325,12 +355,12 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Region {
|
||||
item: leftSatelliteHole
|
||||
item: leadingSatelliteHole
|
||||
intersection: Intersection.Subtract
|
||||
}
|
||||
|
||||
Region {
|
||||
item: rightSatelliteHole
|
||||
item: trailingSatelliteHole
|
||||
intersection: Intersection.Subtract
|
||||
}
|
||||
}
|
||||
@@ -346,7 +376,7 @@ PanelWindow {
|
||||
Item {
|
||||
id: islandHole
|
||||
|
||||
x: surface.inputMaskItem.x
|
||||
x: surface.inputMaskItem.x + root.hostOriginX
|
||||
y: surface.inputMaskItem.y + root.hostOriginY
|
||||
width: surface.inputMaskItem.width
|
||||
height: surface.inputMaskItem.height
|
||||
@@ -355,28 +385,28 @@ PanelWindow {
|
||||
Item {
|
||||
id: fittsStripHole
|
||||
|
||||
x: surface.fittsStripItem.x
|
||||
x: surface.fittsStripItem.x + root.hostOriginX
|
||||
y: surface.fittsStripItem.y + root.hostOriginY
|
||||
width: surface.fittsStripItem.visible ? surface.fittsStripItem.width : 0
|
||||
height: surface.fittsStripItem.visible ? surface.fittsStripItem.height : 0
|
||||
}
|
||||
|
||||
Item {
|
||||
id: leftSatelliteHole
|
||||
id: leadingSatelliteHole
|
||||
|
||||
x: satelliteHost.leftInputItem.x
|
||||
y: satelliteHost.leftInputItem.y + root.hostOriginY
|
||||
width: satelliteHost.leftInputItem.width
|
||||
height: satelliteHost.leftInputItem.height
|
||||
x: satelliteHost.leadingInputItem.x + root.hostOriginX
|
||||
y: satelliteHost.leadingInputItem.y + root.hostOriginY
|
||||
width: satelliteHost.leadingInputItem.width
|
||||
height: satelliteHost.leadingInputItem.height
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rightSatelliteHole
|
||||
id: trailingSatelliteHole
|
||||
|
||||
x: satelliteHost.rightInputItem.x
|
||||
y: satelliteHost.rightInputItem.y + root.hostOriginY
|
||||
width: satelliteHost.rightInputItem.width
|
||||
height: satelliteHost.rightInputItem.height
|
||||
x: satelliteHost.trailingInputItem.x + root.hostOriginX
|
||||
y: satelliteHost.trailingInputItem.y + root.hostOriginY
|
||||
width: satelliteHost.trailingInputItem.width
|
||||
height: satelliteHost.trailingInputItem.height
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
@@ -22,9 +22,11 @@ Item {
|
||||
property real springStiffness: 560
|
||||
property real springDamping: 37
|
||||
property real springMass: 1
|
||||
property real hostOriginX: 0
|
||||
property real hostOriginY: 0
|
||||
property string palette: "default"
|
||||
property bool highContrast: false
|
||||
property real transparency: 1
|
||||
|
||||
readonly property color surfaceColor: {
|
||||
if (root.highContrast)
|
||||
@@ -38,7 +40,7 @@ Item {
|
||||
return Theme.surfaceContainerHigh;
|
||||
}
|
||||
readonly property bool popupStyled: root.controller.expanded
|
||||
readonly property real islandOpacity: Math.max(0, Math.min(1, SettingsData.dankIslandTransparency))
|
||||
readonly property real islandOpacity: Math.max(0, Math.min(1, root.transparency))
|
||||
readonly property color effectiveSurfaceColor: root.highContrast ? Theme.surfaceContainerHighest : Theme.withAlpha(root.surfaceColor, root.islandOpacity)
|
||||
readonly property real surfaceOpacity: root.effectiveSurfaceColor.a
|
||||
readonly property real currentSurfaceRadius: Math.max(0, motion.currentTopLeftRadius, motion.currentBottomLeftRadius)
|
||||
@@ -58,32 +60,51 @@ Item {
|
||||
readonly property alias fittsStripItem: fittsStrip
|
||||
readonly property bool motionRunning: motion.running
|
||||
readonly property real springTimeConstantMs: motion.timeConstantMs
|
||||
property real trackedHeight: 0
|
||||
property real fadeCompactHeight: 48
|
||||
property real fadeExpandedHeight: 352
|
||||
property real trackedCrossExtent: 0
|
||||
property real fadeCompactCross: 48
|
||||
property real fadeExpandedCross: 352
|
||||
property rect motionStartBounds: Qt.rect(0, 0, 0, 0)
|
||||
readonly property bool bottomEdge: SettingsData.dankIslandEdge === "bottom"
|
||||
readonly property bool isVertical: root.controller.isVertical
|
||||
readonly property bool farEdge: root.controller.edge === "bottom" || root.controller.edge === "right"
|
||||
readonly property real alongExtent: root.isVertical ? root.height : root.width
|
||||
readonly property real crossExtent: root.isVertical ? root.width : root.height
|
||||
readonly property real currentVisualWidth: motion.currentWidth
|
||||
readonly property real currentVisualHeight: motion.currentHeight
|
||||
readonly property real currentVisualX: (width - currentVisualWidth) / 2 + motion.currentOffsetX
|
||||
readonly property real currentVisualY: bottomEdge ? height - motion.currentOffsetY - currentVisualHeight : motion.currentOffsetY
|
||||
readonly property real targetVisualX: Math.round((width - motion.targetWidth) / 2 + motion.targetOffsetX)
|
||||
readonly property real targetVisualY: bottomEdge ? height - Math.round(motion.targetOffsetY) - motion.targetHeight : Math.round(motion.targetOffsetY)
|
||||
readonly property real currentVisualCross: isVertical ? motion.currentWidth : motion.currentHeight
|
||||
readonly property real currentAlongPos: (alongExtent - (isVertical ? currentVisualHeight : currentVisualWidth)) / 2 + motion.currentOffsetAlong
|
||||
readonly property real currentCrossPos: farEdge ? crossExtent - motion.currentOffsetCross - currentVisualCross : motion.currentOffsetCross
|
||||
readonly property real currentVisualX: isVertical ? currentCrossPos : currentAlongPos
|
||||
readonly property real currentVisualY: isVertical ? currentAlongPos : currentCrossPos
|
||||
readonly property real targetAlongPos: Math.round((alongExtent - (isVertical ? motion.targetHeight : motion.targetWidth)) / 2 + motion.targetOffsetAlong)
|
||||
readonly property real targetCrossPos: farEdge ? crossExtent - Math.round(motion.targetOffsetCross) - (isVertical ? motion.targetWidth : motion.targetHeight) : Math.round(motion.targetOffsetCross)
|
||||
readonly property real targetVisualX: isVertical ? targetCrossPos : targetAlongPos
|
||||
readonly property real targetVisualY: isVertical ? targetAlongPos : targetCrossPos
|
||||
readonly property real targetScreenX: targetVisualX + root.hostOriginX
|
||||
readonly property real targetScreenY: targetVisualY + root.hostOriginY
|
||||
readonly property real targetVisualWidth: motion.targetWidth
|
||||
readonly property real targetVisualHeight: motion.targetHeight
|
||||
readonly property real currentVisualAlong: isVertical ? motion.currentHeight : motion.currentWidth
|
||||
readonly property real targetVisualAlong: isVertical ? motion.targetHeight : motion.targetWidth
|
||||
readonly property real morphProgress: {
|
||||
const span = fadeExpandedHeight - fadeCompactHeight;
|
||||
const span = fadeExpandedCross - fadeCompactCross;
|
||||
if (Math.abs(span) < 1)
|
||||
return controller.expanded ? 1 : 0;
|
||||
return Math.max(0, Math.min(1, (motion.currentHeight - fadeCompactHeight) / span));
|
||||
return Math.max(0, Math.min(1, (currentVisualCross - fadeCompactCross) / span));
|
||||
}
|
||||
|
||||
function descriptorCross(target) {
|
||||
return root.isVertical ? target.width : target.height;
|
||||
}
|
||||
|
||||
function descriptorAlong(target) {
|
||||
return root.isVertical ? target.height : target.width;
|
||||
}
|
||||
|
||||
function applyTarget() {
|
||||
if (controller.expanded)
|
||||
fadeExpandedHeight = controller.expandedTarget.height;
|
||||
fadeExpandedCross = root.descriptorCross(controller.expandedTarget);
|
||||
else
|
||||
fadeCompactHeight = controller.compactTarget.height;
|
||||
fadeCompactCross = root.descriptorCross(controller.compactTarget);
|
||||
if (motion.running)
|
||||
root.unionMotionStartBounds();
|
||||
motion.setTarget(controller.targetDescriptor);
|
||||
@@ -105,19 +126,21 @@ Item {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
trackedHeight = height;
|
||||
fadeCompactHeight = controller.compactTarget.height;
|
||||
fadeExpandedHeight = controller.expandedTarget.height;
|
||||
trackedCrossExtent = crossExtent;
|
||||
fadeCompactCross = root.descriptorCross(controller.compactTarget);
|
||||
fadeExpandedCross = root.descriptorCross(controller.expandedTarget);
|
||||
motion.snapTo(controller.targetDescriptor);
|
||||
}
|
||||
|
||||
onHeightChanged: {
|
||||
const delta = height - trackedHeight;
|
||||
trackedHeight = height;
|
||||
if (!bottomEdge || !motion.running || delta === 0)
|
||||
// On a far edge the cross coordinate is measured from the far side, so a host resize
|
||||
// shifts everything already in flight by the same delta.
|
||||
onCrossExtentChanged: {
|
||||
const delta = crossExtent - trackedCrossExtent;
|
||||
trackedCrossExtent = crossExtent;
|
||||
if (!farEdge || !motion.running || delta === 0)
|
||||
return;
|
||||
const b = motionStartBounds;
|
||||
motionStartBounds = Qt.rect(b.x, b.y + delta, b.width, b.height);
|
||||
motionStartBounds = isVertical ? Qt.rect(b.x + delta, b.y, b.width, b.height) : Qt.rect(b.x, b.y + delta, b.width, b.height);
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -222,7 +245,9 @@ Item {
|
||||
|
||||
controller: root.controller
|
||||
islandX: root.currentVisualX
|
||||
islandY: root.currentVisualY
|
||||
hostWidth: root.width
|
||||
hostHeight: root.height
|
||||
springTimeConstantMs: root.springTimeConstantMs
|
||||
morphProgress: root.morphProgress
|
||||
expanded: root.controller.expanded
|
||||
@@ -265,14 +290,17 @@ Item {
|
||||
Item {
|
||||
id: fittsStrip
|
||||
|
||||
readonly property real spanWidth: Math.max(root.controller.compactTarget.width, motion.targetWidth)
|
||||
readonly property real edgeGap: root.bottomEdge ? Math.max(0, root.height - (root.targetVisualY + motion.targetHeight)) : Math.max(0, root.targetVisualY)
|
||||
readonly property real targetCross: root.isVertical ? motion.targetWidth : motion.targetHeight
|
||||
readonly property real span: Math.max(root.descriptorAlong(root.controller.compactTarget), root.isVertical ? motion.targetHeight : motion.targetWidth)
|
||||
readonly property real edgeGap: root.farEdge ? Math.max(0, root.crossExtent - (root.targetCrossPos + targetCross)) : Math.max(0, root.targetCrossPos)
|
||||
readonly property real alongPos: Math.round((root.alongExtent - span) / 2 + motion.targetOffsetAlong)
|
||||
readonly property real crossPos: root.farEdge ? root.targetCrossPos + targetCross : 0
|
||||
|
||||
x: Math.round((root.width - spanWidth) / 2 + motion.targetOffsetX)
|
||||
y: root.bottomEdge ? root.targetVisualY + motion.targetHeight : 0
|
||||
width: spanWidth
|
||||
height: edgeGap
|
||||
visible: edgeGap > 0 && spanWidth > 0
|
||||
x: root.isVertical ? crossPos : alongPos
|
||||
y: root.isVertical ? alongPos : crossPos
|
||||
width: root.isVertical ? edgeGap : span
|
||||
height: root.isVertical ? span : edgeGap
|
||||
visible: edgeGap > 0 && span > 0
|
||||
|
||||
HoverHandler {
|
||||
id: stripHover
|
||||
@@ -358,7 +386,7 @@ Item {
|
||||
launcherController: root.launcherController
|
||||
transientSurfaceTracker: root.launcherTransientSurfaceTracker
|
||||
effectiveScreen: root.effectiveScreen
|
||||
alignedX: root.targetVisualX
|
||||
alignedX: root.targetScreenX
|
||||
alignedY: root.targetScreenY
|
||||
}
|
||||
}
|
||||
@@ -380,7 +408,7 @@ Item {
|
||||
ControlCenterExpanded {
|
||||
controller: root.controller
|
||||
effectiveScreen: root.effectiveScreen
|
||||
alignedX: root.targetVisualX
|
||||
alignedX: root.targetScreenX
|
||||
alignedY: root.targetScreenY
|
||||
alignedWidth: root.targetVisualWidth
|
||||
alignedHeight: root.targetVisualHeight
|
||||
@@ -431,6 +459,7 @@ Item {
|
||||
|
||||
SystemLevelCompact {
|
||||
systemModel: root.systemModel
|
||||
isVertical: root.isVertical
|
||||
iconSize: root.controller.compactIconSize
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ Item {
|
||||
|
||||
required property var controller
|
||||
required property real islandX
|
||||
required property real islandY
|
||||
required property real hostWidth
|
||||
required property real hostHeight
|
||||
required property real springTimeConstantMs
|
||||
required property real morphProgress
|
||||
required property bool expanded
|
||||
@@ -137,12 +139,16 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Pinned to the compact target's own screen slot so the face holds still while the island morphs.
|
||||
component CompactFace: Loader {
|
||||
required property string activity
|
||||
readonly property var target: root.controller.compactTargetFor(activity)
|
||||
readonly property bool isVertical: root.controller.isVertical
|
||||
readonly property real alongPos: isVertical ? Math.round((root.hostHeight - target.height) / 2 + target.offsetAlong) - Math.round(root.islandY) : Math.round((root.hostWidth - target.width) / 2 + target.offsetAlong) - Math.round(root.islandX)
|
||||
readonly property real crossPos: isVertical ? Math.round((parent.width - width) / 2) : Math.round((parent.height - height) / 2)
|
||||
|
||||
x: Math.round((root.hostWidth - target.width) / 2 + target.offsetX) - Math.round(root.islandX)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
x: isVertical ? crossPos : alongPos
|
||||
y: isVertical ? alongPos : crossPos
|
||||
width: target.width
|
||||
height: target.height
|
||||
asynchronous: false
|
||||
|
||||
@@ -29,14 +29,16 @@ QtObject {
|
||||
property string controlCenterPendingSection: ""
|
||||
property bool notificationExpandAllowed: false
|
||||
property bool keyboardYielded: false
|
||||
property real horizontalOffset: 0
|
||||
property real alongOffset: 0
|
||||
property real outerGap: 8
|
||||
readonly property bool bottomEdge: SettingsData.dankIslandEdge === "bottom"
|
||||
readonly property real cornerRadius: Math.max(0, Math.min(64, SettingsData.dankIslandCornerRadius))
|
||||
property var barConfig: null
|
||||
property string edge: "top"
|
||||
readonly property bool isVertical: edge === "left" || edge === "right"
|
||||
property real cornerRadius: 34
|
||||
readonly property real edgeCornerRadius: Math.round(cornerRadius * 0.75)
|
||||
readonly property real topCornerRadius: bottomEdge ? cornerRadius : edgeCornerRadius
|
||||
readonly property real bottomCornerRadius: bottomEdge ? edgeCornerRadius : cornerRadius
|
||||
property real compactHeight: 38
|
||||
property real compactThickness: 38
|
||||
property string batteryStyle: "solid"
|
||||
property bool mediaClockVisible: true
|
||||
property int transientTimeout: 2200
|
||||
property int notificationTimeout: 5000
|
||||
property string transientReturnActivity: ""
|
||||
@@ -51,38 +53,38 @@ QtObject {
|
||||
readonly property real controlCenterHeight: Math.max(320, Math.min(controlCenterMaxHeight, destinationContentHeight("controlcenter")))
|
||||
readonly property real notificationCenterHeight: Math.max(320, Math.min(notificationCenterMaxHeight, destinationContentHeight("notificationcenter")))
|
||||
|
||||
readonly property bool compactDense: compactHeight < 40
|
||||
readonly property real compactFaceHeight: compactHeight + (compactDense ? 2 : 4)
|
||||
readonly property real compactIconSize: Math.max(14, Math.min(32, compactHeight - 8))
|
||||
readonly property bool homeCompactTight: SettingsData.dankIslandHomeCompactTight
|
||||
readonly property real homeCompactFaceHeight: homeCompactTight ? Math.max(16, Math.min(32, compactHeight - 8)) : compactFaceHeight
|
||||
readonly property bool compactDense: compactThickness < 40
|
||||
readonly property real compactFaceThickness: compactThickness + (compactDense ? 2 : 4)
|
||||
readonly property real compactIconSize: Math.max(14, Math.min(32, compactThickness - 8))
|
||||
property bool homeCompactTight: false
|
||||
readonly property real homeCompactFaceThickness: homeCompactTight ? Math.max(16, Math.min(32, compactThickness - 8)) : compactFaceThickness
|
||||
|
||||
property int unreadNotificationCount: 0
|
||||
readonly property bool homeNotificationBadge: SettingsData.islandHomeGroupEnabled("notifications") && unreadNotificationCount > 0
|
||||
readonly property bool homeWeatherEnabled: SettingsData.weatherEnabled && SettingsData.islandHomeGroupEnabled("weather")
|
||||
readonly property bool homeNotificationBadge: SettingsData.islandHomeGroupEnabled(root.barConfig, "notifications") && unreadNotificationCount > 0
|
||||
readonly property bool homeWeatherEnabled: SettingsData.weatherEnabled && SettingsData.islandHomeGroupEnabled(root.barConfig, "weather")
|
||||
readonly property real homeSlotMargin: homeCompactTight ? Theme.spacingS : Theme.spacingM
|
||||
property real homeContentWidth: 200
|
||||
property real mediaContentWidth: 360
|
||||
property real homeContentLength: 200
|
||||
property real mediaContentLength: 360
|
||||
property real mediaExpandedHeight: 324
|
||||
readonly property real mediaCompactMaxWidth: 360
|
||||
property real notificationContentWidth: 0
|
||||
readonly property real notificationCompactMinWidth: compactDense ? 200 : 240
|
||||
readonly property real notificationCompactMaxWidth: mediaCompactMaxWidth
|
||||
readonly property real mediaCompactMaxLength: 360
|
||||
property real notificationContentLength: 0
|
||||
readonly property real notificationCompactMinLength: isVertical ? compactFaceThickness : (compactDense ? 200 : 240)
|
||||
readonly property real notificationCompactMaxLength: mediaCompactMaxLength
|
||||
|
||||
signal sessionStarted(string activityId)
|
||||
|
||||
function setHomeContentWidth(width) {
|
||||
const next = Math.ceil(width);
|
||||
if (!isFinite(next) || next <= 0 || Math.abs(next - homeContentWidth) < 2)
|
||||
function setHomeContentLength(length) {
|
||||
const next = Math.ceil(length);
|
||||
if (!isFinite(next) || next <= 0 || Math.abs(next - homeContentLength) < 2)
|
||||
return;
|
||||
homeContentWidth = next;
|
||||
homeContentLength = next;
|
||||
}
|
||||
|
||||
function setNotificationContentWidth(width) {
|
||||
const next = Math.ceil(width);
|
||||
if (!isFinite(next) || next <= 0 || Math.abs(next - notificationContentWidth) < 2)
|
||||
function setNotificationContentLength(length) {
|
||||
const next = Math.ceil(length);
|
||||
if (!isFinite(next) || next <= 0 || Math.abs(next - notificationContentLength) < 2)
|
||||
return;
|
||||
notificationContentWidth = next;
|
||||
notificationContentLength = next;
|
||||
}
|
||||
|
||||
function setMediaExpandedHeight(height) {
|
||||
@@ -92,34 +94,34 @@ QtObject {
|
||||
mediaExpandedHeight = next;
|
||||
}
|
||||
|
||||
function setMediaContentWidth(width) {
|
||||
const next = Math.ceil(Math.min(root.mediaCompactMaxWidth, width));
|
||||
if (!isFinite(next) || next <= 0 || Math.abs(next - mediaContentWidth) < 2)
|
||||
function setMediaContentLength(length) {
|
||||
const next = Math.ceil(Math.min(root.mediaCompactMaxLength, length));
|
||||
if (!isFinite(next) || next <= 0 || Math.abs(next - mediaContentLength) < 2)
|
||||
return;
|
||||
mediaContentWidth = next;
|
||||
mediaContentLength = next;
|
||||
}
|
||||
|
||||
readonly property var destinations: ["launcher", "controlcenter", "wallpaper", "weather", "notificationcenter"]
|
||||
readonly property var blankClickOwners: ["launcher", "controlcenter", "wallpaper", "notificationcenter"]
|
||||
readonly property var destinationDefaults: ({
|
||||
"launcher": {
|
||||
"contentWidth": 160,
|
||||
"contentLength": 160,
|
||||
"contentHeight": 0
|
||||
},
|
||||
"controlcenter": {
|
||||
"contentWidth": 180,
|
||||
"contentLength": 180,
|
||||
"contentHeight": 420
|
||||
},
|
||||
"wallpaper": {
|
||||
"contentWidth": 150,
|
||||
"contentLength": 150,
|
||||
"contentHeight": 0
|
||||
},
|
||||
"weather": {
|
||||
"contentWidth": 130,
|
||||
"contentLength": 130,
|
||||
"contentHeight": 0
|
||||
},
|
||||
"notificationcenter": {
|
||||
"contentWidth": 170,
|
||||
"contentLength": 170,
|
||||
"contentHeight": 320
|
||||
}
|
||||
})
|
||||
@@ -134,7 +136,7 @@ QtObject {
|
||||
"ready": false,
|
||||
"pending": false,
|
||||
"pendingKeyboardFocus": false,
|
||||
"contentWidth": destinationDefaults[id].contentWidth,
|
||||
"contentLength": destinationDefaults[id].contentLength,
|
||||
"contentHeight": destinationDefaults[id].contentHeight
|
||||
};
|
||||
}
|
||||
@@ -158,20 +160,20 @@ QtObject {
|
||||
return destinationEntry(activityId)?.ready ?? false;
|
||||
}
|
||||
|
||||
function destinationContentWidth(activityId) {
|
||||
return destinationEntry(activityId)?.contentWidth ?? 0;
|
||||
function destinationContentLength(activityId) {
|
||||
return destinationEntry(activityId)?.contentLength ?? 0;
|
||||
}
|
||||
|
||||
function destinationContentHeight(activityId) {
|
||||
return destinationEntry(activityId)?.contentHeight ?? 0;
|
||||
}
|
||||
|
||||
function setDestinationContentWidth(activityId, width) {
|
||||
function setDestinationContentLength(activityId, length) {
|
||||
const entry = destinationState[activityId];
|
||||
const next = Math.ceil(width);
|
||||
if (!entry || !isFinite(next) || next <= 0 || Math.abs(next - entry.contentWidth) < 2)
|
||||
const next = Math.ceil(length);
|
||||
if (!entry || !isFinite(next) || next <= 0 || Math.abs(next - entry.contentLength) < 2)
|
||||
return;
|
||||
entry.contentWidth = next;
|
||||
entry.contentLength = next;
|
||||
destinationRevision++;
|
||||
}
|
||||
|
||||
@@ -264,40 +266,50 @@ QtObject {
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real destinationCompactEndPad: Math.max(root.compactFaceHeight / 2, Theme.spacingM) + Theme.spacingS
|
||||
readonly property real destinationCompactEndPad: root.isVertical ? Theme.spacingS : Math.max(root.compactFaceThickness / 2, Theme.spacingM) + Theme.spacingS
|
||||
|
||||
function destinationCompactWidth(activityId) {
|
||||
return Math.ceil(Math.max(root.compactFaceHeight, destinationContentWidth(activityId) + root.destinationCompactEndPad * 2));
|
||||
function destinationCompactLength(activityId) {
|
||||
return Math.ceil(Math.max(root.compactFaceThickness, destinationContentLength(activityId) + root.destinationCompactEndPad * 2));
|
||||
}
|
||||
|
||||
function homeGroups(side) {
|
||||
const layout = SettingsData.getIslandHomeLayout();
|
||||
const layout = SettingsData.getIslandHomeLayout(root.barConfig);
|
||||
const clock = layout.findIndex(g => g.id === "clock");
|
||||
const groups = side === "left" ? layout.slice(0, clock) : layout.slice(clock + 1);
|
||||
return groups.filter(g => g.enabled).map(g => g.id);
|
||||
}
|
||||
|
||||
function resolvedSystemLevelDisplay(value) {
|
||||
if (value === "icon" || value === "percentage" || value === "both")
|
||||
return value;
|
||||
return "both";
|
||||
}
|
||||
|
||||
readonly property var homeLeftGroups: homeGroups("left")
|
||||
readonly property var homeRightGroups: homeGroups("right")
|
||||
readonly property string homeVolumeDisplay: resolvedSystemLevelDisplay(SettingsData.dankIslandHomeVolumeDisplay)
|
||||
readonly property string homeBrightnessDisplay: resolvedSystemLevelDisplay(SettingsData.dankIslandHomeBrightnessDisplay)
|
||||
property string homeClockDisplay: "both"
|
||||
property string homeVolumeDisplay: "both"
|
||||
property string homeBrightnessDisplay: "both"
|
||||
|
||||
readonly property real homeCompactWidth: Math.ceil(homeSlotMargin * 2 + Math.max(homeContentWidth, 1))
|
||||
readonly property real mediaCompactWidth: Math.ceil(Math.max(1, mediaContentWidth))
|
||||
readonly property real homeCompactLength: Math.ceil(homeSlotMargin * 2 + Math.max(homeContentLength, 1))
|
||||
readonly property real mediaCompactLength: Math.ceil(Math.max(1, mediaContentLength))
|
||||
|
||||
function pillTarget(width, height) {
|
||||
const radius = Math.min(height / 2, root.cornerRadius);
|
||||
// Corners touching the attached screen edge stay tighter than the ones facing the desktop.
|
||||
function sheetRadii() {
|
||||
const near = root.edgeCornerRadius;
|
||||
const far = root.cornerRadius;
|
||||
switch (root.edge) {
|
||||
case "bottom":
|
||||
return [far, far, near, near];
|
||||
case "left":
|
||||
return [near, far, near, far];
|
||||
case "right":
|
||||
return [far, near, far, near];
|
||||
}
|
||||
return [near, near, far, far];
|
||||
}
|
||||
|
||||
function pillTarget(alongSize, crossSize) {
|
||||
const radius = Math.min(crossSize / 2, root.cornerRadius);
|
||||
return {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"offsetX": root.horizontalOffset,
|
||||
"offsetY": root.outerGap,
|
||||
"width": root.isVertical ? crossSize : alongSize,
|
||||
"height": root.isVertical ? alongSize : crossSize,
|
||||
"offsetAlong": root.alongOffset,
|
||||
"offsetCross": root.outerGap,
|
||||
"topLeftRadius": radius,
|
||||
"topRightRadius": radius,
|
||||
"bottomLeftRadius": radius,
|
||||
@@ -306,27 +318,28 @@ QtObject {
|
||||
}
|
||||
|
||||
function sheetTarget(width, height) {
|
||||
const radii = root.sheetRadii();
|
||||
return {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"offsetX": root.horizontalOffset,
|
||||
"offsetY": root.outerGap,
|
||||
"topLeftRadius": root.topCornerRadius,
|
||||
"topRightRadius": root.topCornerRadius,
|
||||
"bottomLeftRadius": root.bottomCornerRadius,
|
||||
"bottomRightRadius": root.bottomCornerRadius
|
||||
"offsetAlong": root.alongOffset,
|
||||
"offsetCross": root.outerGap,
|
||||
"topLeftRadius": radii[0],
|
||||
"topRightRadius": radii[1],
|
||||
"bottomLeftRadius": radii[2],
|
||||
"bottomRightRadius": radii[3]
|
||||
};
|
||||
}
|
||||
|
||||
readonly property var homeCompactTarget: pillTarget(homeCompactWidth, homeCompactFaceHeight)
|
||||
readonly property var mediaCompactTarget: pillTarget(mediaCompactWidth, compactFaceHeight)
|
||||
readonly property var homeCompactTarget: pillTarget(homeCompactLength, homeCompactFaceThickness)
|
||||
readonly property var mediaCompactTarget: pillTarget(mediaCompactLength, compactFaceThickness)
|
||||
readonly property var dashSheetTarget: sheetTarget(SettingsData.showWeekNumber ? 736 : 700, 452)
|
||||
readonly property var mediaExpandedTarget: sheetTarget(600, mediaExpandedHeight)
|
||||
readonly property var launcherExpandedTarget: sheetTarget(680, 560)
|
||||
readonly property var controlCenterExpandedTarget: sheetTarget(580, controlCenterHeight)
|
||||
readonly property var systemCompactTarget: pillTarget(SettingsData.osdAlwaysShowValue ? 330 : 282, compactFaceHeight)
|
||||
readonly property var systemCompactTarget: pillTarget(root.isVertical ? 240 : (SettingsData.osdAlwaysShowValue ? 330 : 282), compactFaceThickness)
|
||||
readonly property var systemExpandedTarget: sheetTarget(460, 176)
|
||||
readonly property var notificationCompactTarget: pillTarget(Math.ceil(Math.max(notificationCompactMinWidth, Math.min(notificationCompactMaxWidth, notificationContentWidth))), compactFaceHeight)
|
||||
readonly property var notificationCompactTarget: pillTarget(Math.ceil(Math.max(notificationCompactMinLength, Math.min(notificationCompactMaxLength, notificationContentLength))), compactFaceThickness)
|
||||
readonly property var notificationExpandedTarget: sheetTarget(520, 220)
|
||||
readonly property var notificationCenterExpandedTarget: sheetTarget(480, notificationCenterHeight)
|
||||
|
||||
@@ -349,7 +362,7 @@ QtObject {
|
||||
case "home":
|
||||
return homeCompactTarget;
|
||||
}
|
||||
return isDestination(activityId) ? pillTarget(destinationCompactWidth(activityId), compactFaceHeight) : homeCompactTarget;
|
||||
return isDestination(activityId) ? pillTarget(destinationCompactLength(activityId), compactFaceThickness) : homeCompactTarget;
|
||||
}
|
||||
|
||||
readonly property var compactTarget: compactTargetFor(activeActivity)
|
||||
|
||||
@@ -7,7 +7,8 @@ Item {
|
||||
id: root
|
||||
|
||||
property bool rightSide: false
|
||||
property bool bottomEdge: false
|
||||
property bool crossFar: false
|
||||
property bool isVertical: false
|
||||
property bool floating: false
|
||||
property color fillColor: "transparent"
|
||||
property bool gothEnabled: true
|
||||
@@ -15,30 +16,37 @@ Item {
|
||||
property real bodyRadius: Theme.cornerRadius
|
||||
property var parentScreen: null
|
||||
|
||||
readonly property real cornerR: Math.max(0, Math.min(root.bodyRadius, root.height / 2))
|
||||
readonly property real sweepR: root.gothEnabled ? Math.max(0, Math.min(root.sweep, root.height - root.cornerR, root.width - root.cornerR)) : 0
|
||||
readonly property real alongSize: root.isVertical ? root.height : root.width
|
||||
readonly property real crossSize: root.isVertical ? root.width : root.height
|
||||
readonly property real cornerR: Math.max(0, Math.min(root.bodyRadius, root.crossSize / 2))
|
||||
readonly property real sweepR: root.gothEnabled ? Math.max(0, Math.min(root.sweep, root.crossSize - root.cornerR, root.alongSize - root.cornerR)) : 0
|
||||
|
||||
// Paths are authored once for a top-edge horizontal strip and rotated into place, so
|
||||
// "bottomEdge"/"rightSide" below are canonical, not screen, directions.
|
||||
readonly property bool canonicalBottomEdge: root.isVertical ? false : root.crossFar
|
||||
readonly property bool canonicalRightSide: root.isVertical ? (root.crossFar ? root.rightSide : !root.rightSide) : root.rightSide
|
||||
|
||||
readonly property string fillPath: {
|
||||
root.width;
|
||||
root.height;
|
||||
root.alongSize;
|
||||
root.crossSize;
|
||||
root.cornerR;
|
||||
root.sweepR;
|
||||
root.rightSide;
|
||||
root.bottomEdge;
|
||||
root.canonicalRightSide;
|
||||
root.canonicalBottomEdge;
|
||||
root.floating;
|
||||
return root.buildPath();
|
||||
}
|
||||
|
||||
function buildPath() {
|
||||
const w = root.width;
|
||||
const h = root.height;
|
||||
const w = root.alongSize;
|
||||
const h = root.crossSize;
|
||||
const cr = root.cornerR;
|
||||
const s = root.sweepR;
|
||||
if (root.floating)
|
||||
return root.bottomEdge ? floatBottomPath(w, h, cr, s) : floatTopPath(w, h, cr, s);
|
||||
if (root.bottomEdge)
|
||||
return root.rightSide ? rightBottomPath(w, h, cr, s) : leftBottomPath(w, h, cr, s);
|
||||
return root.rightSide ? rightTopPath(w, h, cr, s) : leftTopPath(w, h, cr, s);
|
||||
return root.canonicalBottomEdge ? floatBottomPath(w, h, cr, s) : floatTopPath(w, h, cr, s);
|
||||
if (root.canonicalBottomEdge)
|
||||
return root.canonicalRightSide ? rightBottomPath(w, h, cr, s) : leftBottomPath(w, h, cr, s);
|
||||
return root.canonicalRightSide ? rightTopPath(w, h, cr, s) : leftTopPath(w, h, cr, s);
|
||||
}
|
||||
|
||||
function floatTopPath(w, h, cr, s) {
|
||||
@@ -77,6 +85,52 @@ Item {
|
||||
return `M ${w} ${h} L 0 ${h} L 0 ${cr} A ${cr} ${cr} 0 0 1 ${cr} 0 L ${w - cr} 0 A ${cr} ${cr} 0 0 1 ${w} ${cr} Z`;
|
||||
}
|
||||
|
||||
// Canonical (along, cross) rect to item-local coordinates, matching the canvas rotation.
|
||||
function localRect(cx, cy, cw, ch) {
|
||||
if (!root.isVertical)
|
||||
return Qt.rect(cx, cy, cw, ch);
|
||||
if (root.crossFar)
|
||||
return Qt.rect(root.crossSize - cy - ch, cx, ch, cw);
|
||||
return Qt.rect(cy, root.alongSize - cx - cw, ch, cw);
|
||||
}
|
||||
|
||||
readonly property real canonicalEdgeCross: root.canonicalBottomEdge ? root.crossSize - root.cornerR : 0
|
||||
readonly property real canonicalSweepCross: root.canonicalBottomEdge ? 0 : root.crossSize - root.cornerR
|
||||
readonly property bool sweepSquared: !root.floating && root.sweepR > 0
|
||||
|
||||
// The chrome paints square corners where it meets the screen edge (and above the sweep in
|
||||
// edge mode); the blur regions have to square the same ones off.
|
||||
readonly property rect leadCornerSquare: root.localRect(0, root.canonicalEdgeCross, root.cornerR, root.cornerR)
|
||||
readonly property rect trailCornerSquare: root.localRect(root.alongSize - root.cornerR, root.canonicalEdgeCross, root.cornerR, root.cornerR)
|
||||
readonly property rect sweepCornerSquare: root.sweepSquared ? root.localRect(root.canonicalRightSide ? root.alongSize - root.cornerR : 0, root.canonicalSweepCross, root.cornerR, root.cornerR) : Qt.rect(0, 0, 0, 0)
|
||||
|
||||
function sweepPieceOrigin(isWing) {
|
||||
const s = root.sweepR;
|
||||
if (root.floating)
|
||||
return [isWing ? -s : root.alongSize, root.canonicalBottomEdge ? root.crossSize - s : 0];
|
||||
if (isWing)
|
||||
return [root.canonicalRightSide ? root.alongSize - s : 0, root.canonicalBottomEdge ? -s : root.crossSize];
|
||||
return [root.canonicalRightSide ? -s : root.alongSize, root.canonicalBottomEdge ? root.crossSize - s : 0];
|
||||
}
|
||||
|
||||
function sweepPieceRect(isWing) {
|
||||
const origin = root.sweepPieceOrigin(isWing);
|
||||
return root.localRect(origin[0], origin[1], root.sweepR, root.sweepR);
|
||||
}
|
||||
|
||||
// The fillet is that square minus a quarter disc; this is the disc's bounding box.
|
||||
function sweepDiscRect(isWing) {
|
||||
const s = root.sweepR;
|
||||
const leadsIn = root.floating ? isWing : root.canonicalRightSide;
|
||||
const origin = root.sweepPieceOrigin(isWing);
|
||||
return root.localRect(origin[0] - (leadsIn ? s : 0), origin[1] - (root.canonicalBottomEdge ? s : 0), s * 2, s * 2);
|
||||
}
|
||||
|
||||
readonly property rect sweepBodyRect: root.sweepR > 0 ? root.sweepPieceRect(false) : Qt.rect(0, 0, 0, 0)
|
||||
readonly property rect sweepBodyDisc: root.sweepR > 0 ? root.sweepDiscRect(false) : Qt.rect(0, 0, 0, 0)
|
||||
readonly property rect sweepWingRect: root.sweepR > 0 ? root.sweepPieceRect(true) : Qt.rect(0, 0, 0, 0)
|
||||
readonly property rect sweepWingDisc: root.sweepR > 0 ? root.sweepDiscRect(true) : Qt.rect(0, 0, 0, 0)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
@@ -95,17 +149,26 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
Item {
|
||||
id: canvas
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.fillColor
|
||||
strokeColor: "transparent"
|
||||
strokeWidth: 0
|
||||
anchors.centerIn: parent
|
||||
width: root.alongSize
|
||||
height: root.crossSize
|
||||
rotation: root.isVertical ? (root.crossFar ? 90 : -90) : 0
|
||||
|
||||
PathSvg {
|
||||
path: root.fillPath
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.fillColor
|
||||
strokeColor: "transparent"
|
||||
strokeWidth: 0
|
||||
|
||||
PathSvg {
|
||||
path: root.fillPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,19 @@ Item {
|
||||
|
||||
required property var hostWindow
|
||||
required property var targetScreen
|
||||
required property var barConfig
|
||||
required property IslandController controller
|
||||
required property var islandSurface
|
||||
property real outerGap: 8
|
||||
property var hyprlandOverviewLoader: null
|
||||
|
||||
readonly property alias leftInputItem: leftInputEnvelope
|
||||
readonly property alias rightInputItem: rightInputEnvelope
|
||||
readonly property var referenceBarConfig: {
|
||||
SettingsData.barConfigs;
|
||||
return SettingsData.islandBarConfig ?? ({});
|
||||
function setting(key) {
|
||||
return SettingsData.islandSetting(root.barConfig, key);
|
||||
}
|
||||
|
||||
readonly property alias leadingInputItem: leadingInputEnvelope
|
||||
readonly property alias trailingInputItem: trailingInputEnvelope
|
||||
readonly property var referenceBarConfig: root.barConfig ?? ({})
|
||||
readonly property var satelliteConfig: {
|
||||
const base = SettingsData.effectiveBarConfigForRender(root.referenceBarConfig, false) ?? root.referenceBarConfig ?? ({});
|
||||
if (!root.hostWindow?.usesOverlayLayer)
|
||||
@@ -38,31 +41,30 @@ Item {
|
||||
readonly property real widgetThickness: Theme.barWidgetThickness(root.innerPadding, root.screenScale)
|
||||
readonly property real barThickness: Theme.barThickness(root.innerPadding, root.screenScale)
|
||||
readonly property real satelliteSpacing: root.satelliteConfig?.spacing ?? 4
|
||||
readonly property bool edgeAligned: SettingsData.dankIslandSatellitePosition === "edges"
|
||||
readonly property bool edgeAligned: root.setting("islandSatellitePosition") === "edges"
|
||||
readonly property real edgeBaseMargin: Math.max(Theme.spacingXS, root.innerPadding * 0.8)
|
||||
readonly property real edgeInsetRaw: SettingsData.barInsetPaddingSyncAll ? SettingsData.barInsetPaddingShared : (root.satelliteConfig?.barInsetPadding ?? -1)
|
||||
readonly property real edgeInset: Theme.snap(Math.max(0, root.edgeInsetRaw < 0 ? root.edgeBaseMargin : root.edgeInsetRaw), root.screenScale)
|
||||
|
||||
readonly property bool bottomEdge: SettingsData.dankIslandEdge === "bottom"
|
||||
readonly property real stripHeight: root.hostWindow?.reservedStripHeight ?? (root.outerGap + root.barThickness)
|
||||
readonly property real rowY: {
|
||||
const centered = Theme.snap((root.stripHeight - root.barThickness) / 2, root.screenScale);
|
||||
return root.bottomEdge ? root.height - root.stripHeight + centered : centered;
|
||||
}
|
||||
readonly property bool backgroundEnabled: SettingsData.dankIslandSatelliteBackground
|
||||
readonly property bool isVertical: root.controller.isVertical
|
||||
readonly property bool crossFar: root.controller.edge === "bottom" || root.controller.edge === "right"
|
||||
readonly property real alongExtent: root.isVertical ? root.height : root.width
|
||||
readonly property real crossExtent: root.isVertical ? root.width : root.height
|
||||
readonly property real stripThickness: root.hostWindow?.reservedStripThickness ?? (root.outerGap + root.barThickness)
|
||||
readonly property real stripStart: root.crossFar ? root.crossExtent - root.stripThickness : 0
|
||||
readonly property real rowCross: root.stripStart + Theme.snap((root.stripThickness - root.barThickness) / 2, root.screenScale)
|
||||
readonly property bool backgroundEnabled: root.setting("islandSatelliteBackground")
|
||||
readonly property bool spanEdges: root.edgeAligned
|
||||
readonly property real chromePad: Theme.snap(root.innerPadding + Theme.spacingXS, root.screenScale)
|
||||
readonly property real chromeInset: root.backgroundEnabled ? root.chromePad : 0
|
||||
readonly property real chromeY: root.bottomEdge ? root.height - root.stripHeight : 0
|
||||
readonly property real chromeHeight: root.stripHeight
|
||||
readonly property real crossEdgeExtension: root.spanEdges ? (root.bottomEdge ? root.height - root.rowY - root.barThickness : root.rowY) : 0
|
||||
readonly property bool gothCorners: SettingsData.dankIslandSatelliteGothCorners
|
||||
readonly property real chromeOpacity: Math.max(0, Math.min(1, SettingsData.dankIslandSatelliteTransparency))
|
||||
readonly property real sweepRadius: Math.max(4, Math.min(64, SettingsData.dankIslandSatelliteSwoopRadius))
|
||||
readonly property real crossEdgeExtension: root.spanEdges ? (root.crossFar ? root.crossExtent - root.rowCross - root.barThickness : root.rowCross) : 0
|
||||
readonly property bool gothCorners: root.setting("islandSatelliteGothCorners")
|
||||
readonly property real chromeOpacity: Math.max(0, Math.min(1, root.setting("islandSatelliteTransparency")))
|
||||
readonly property real sweepRadius: Math.max(4, Math.min(64, root.setting("islandSatelliteSwoopRadius")))
|
||||
readonly property color chromeBase: {
|
||||
if (SettingsData.dankIslandHighContrast)
|
||||
if (root.setting("islandHighContrast"))
|
||||
return Theme.surfaceContainerHighest;
|
||||
switch (SettingsData.dankIslandPalette) {
|
||||
switch (root.setting("islandPalette")) {
|
||||
case "bright":
|
||||
return Theme.surfaceBright;
|
||||
case "dim":
|
||||
@@ -77,13 +79,17 @@ Item {
|
||||
readonly property bool islandTranslucent: root.islandOpacity > 0 && root.islandOpacity < 1
|
||||
readonly property bool tracksIsland: !root.edgeAligned && root.visible
|
||||
readonly property bool motionRunning: root.tracksIsland && (root.islandSurface?.motionRunning ?? false)
|
||||
readonly property real islandStartX: root.islandSurface?.motionStartBounds.x ?? 0
|
||||
readonly property real islandStartRight: root.islandStartX + (root.islandSurface?.motionStartBounds.width ?? 0)
|
||||
readonly property real islandTargetX: root.islandSurface?.targetVisualX ?? 0
|
||||
readonly property real islandTargetRight: root.islandTargetX + (root.islandSurface?.targetVisualWidth ?? 0)
|
||||
readonly property rect islandStartBounds: root.islandSurface?.motionStartBounds ?? Qt.rect(0, 0, 0, 0)
|
||||
readonly property real islandStartAlong: root.isVertical ? root.islandStartBounds.y : root.islandStartBounds.x
|
||||
readonly property real islandStartAlongEnd: root.islandStartAlong + (root.isVertical ? root.islandStartBounds.height : root.islandStartBounds.width)
|
||||
readonly property real islandTargetAlong: root.islandSurface?.targetAlongPos ?? 0
|
||||
readonly property real islandTargetAlongEnd: root.islandTargetAlong + (root.islandSurface?.targetVisualAlong ?? 0)
|
||||
readonly property real islandCurrentAlong: root.islandSurface?.currentAlongPos ?? 0
|
||||
readonly property real islandCurrentAlongEnd: root.islandCurrentAlong + (root.islandSurface?.currentVisualAlong ?? 0)
|
||||
readonly property real satelliteGap: root.setting("islandSatelliteGap")
|
||||
|
||||
readonly property real leftEdgeSpread: root.motionRunning ? Math.abs(root.islandStartX - root.islandTargetX) : 0
|
||||
readonly property real rightEdgeSpread: root.motionRunning ? Math.abs(root.islandStartRight - root.islandTargetRight) : 0
|
||||
readonly property real leadingEdgeSpread: root.motionRunning ? Math.abs(root.islandStartAlong - root.islandTargetAlong) : 0
|
||||
readonly property real trailingEdgeSpread: root.motionRunning ? Math.abs(root.islandStartAlongEnd - root.islandTargetAlongEnd) : 0
|
||||
|
||||
function normalizedWidgets(widgets) {
|
||||
return (widgets || []).map((widget, index) => {
|
||||
@@ -102,7 +108,7 @@ Item {
|
||||
});
|
||||
}
|
||||
|
||||
visible: SettingsData.dankIslandSatellitesEnabled
|
||||
visible: root.setting("islandSatellitesEnabled")
|
||||
|
||||
function switchWorkspace(direction) {
|
||||
componentProvider.switchWorkspace(direction);
|
||||
@@ -111,7 +117,7 @@ Item {
|
||||
AxisContext {
|
||||
id: satelliteAxis
|
||||
|
||||
edge: SettingsData.dankIslandEdge
|
||||
edge: root.controller.edge
|
||||
}
|
||||
|
||||
ScriptModel {
|
||||
@@ -140,13 +146,13 @@ Item {
|
||||
property string screenName: root.targetScreen?.name ?? ""
|
||||
property real widgetThickness: root.widgetThickness
|
||||
property real effectiveBarThickness: root.barThickness
|
||||
property bool isVertical: false
|
||||
property bool isVertical: root.isVertical
|
||||
property bool usesFrameBarChrome: false
|
||||
property bool hasAdjacentTopBar: false
|
||||
property bool hasAdjacentBottomBar: false
|
||||
property bool hasAdjacentLeftBar: false
|
||||
property bool hasAdjacentRightBar: false
|
||||
property var hyprlandOverviewLoader: null
|
||||
property var hyprlandOverviewLoader: root.hyprlandOverviewLoader
|
||||
property var controlCenterButtonRef: null
|
||||
property var clockButtonRef: null
|
||||
property var systemUpdateButtonRef: null
|
||||
@@ -192,7 +198,7 @@ Item {
|
||||
if (!BlurService.enabled)
|
||||
return;
|
||||
const widgets = root.chromeCoversWidgets ? [] : root._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
|
||||
const chromes = [leftChrome, rightChrome].filter(c => root.chromeTranslucent && c.visible);
|
||||
const chromes = [leadingChrome, trailingChrome].filter(c => root.chromeTranslucent && c.visible);
|
||||
if (chromes.length === 0 && widgets.length === 0 && !root.islandTranslucent)
|
||||
return;
|
||||
const region = blurRegionComp.createObject(root.hostWindow);
|
||||
@@ -324,36 +330,31 @@ Item {
|
||||
|
||||
property Item chrome
|
||||
|
||||
readonly property real cr: body.chrome.cornerR
|
||||
readonly property real edgeY: body.chrome.bottomEdge ? body.y + body.height - body.cr : body.y
|
||||
readonly property real sweepY: body.chrome.bottomEdge ? body.y : body.y + body.height - body.cr
|
||||
readonly property bool sweepSquare: !body.chrome.floating && body.chrome.sweepR > 0
|
||||
|
||||
x: body.chrome.x
|
||||
y: body.chrome.y
|
||||
width: body.chrome.width
|
||||
height: body.chrome.height
|
||||
radius: body.cr
|
||||
radius: body.chrome.cornerR
|
||||
|
||||
Region {
|
||||
x: body.x
|
||||
y: body.edgeY
|
||||
width: body.cr
|
||||
height: body.cr
|
||||
x: body.x + body.chrome.leadCornerSquare.x
|
||||
y: body.y + body.chrome.leadCornerSquare.y
|
||||
width: body.chrome.leadCornerSquare.width
|
||||
height: body.chrome.leadCornerSquare.height
|
||||
}
|
||||
|
||||
Region {
|
||||
x: body.x + body.width - body.cr
|
||||
y: body.edgeY
|
||||
width: body.cr
|
||||
height: body.cr
|
||||
x: body.x + body.chrome.trailCornerSquare.x
|
||||
y: body.y + body.chrome.trailCornerSquare.y
|
||||
width: body.chrome.trailCornerSquare.width
|
||||
height: body.chrome.trailCornerSquare.height
|
||||
}
|
||||
|
||||
Region {
|
||||
x: body.chrome.rightSide ? body.x + body.width - body.cr : body.x
|
||||
y: body.sweepY
|
||||
width: body.sweepSquare ? body.cr : 0
|
||||
height: body.sweepSquare ? body.cr : 0
|
||||
x: body.x + body.chrome.sweepCornerSquare.x
|
||||
y: body.y + body.chrome.sweepCornerSquare.y
|
||||
width: body.chrome.sweepCornerSquare.width
|
||||
height: body.chrome.sweepCornerSquare.height
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -380,32 +381,21 @@ Item {
|
||||
property Item chrome
|
||||
property bool isWing
|
||||
|
||||
readonly property real s: piece.chrome.sweepR
|
||||
readonly property rect body: piece.isWing ? piece.chrome.sweepWingRect : piece.chrome.sweepBodyRect
|
||||
readonly property rect disc: piece.isWing ? piece.chrome.sweepWingDisc : piece.chrome.sweepBodyDisc
|
||||
|
||||
readonly property bool leadsIn: piece.chrome.floating ? piece.isWing : piece.chrome.rightSide
|
||||
|
||||
x: {
|
||||
if (piece.chrome.floating)
|
||||
return piece.isWing ? piece.chrome.x - piece.s : piece.chrome.x + piece.chrome.width;
|
||||
if (piece.isWing)
|
||||
return piece.chrome.rightSide ? piece.chrome.x + piece.chrome.width - piece.s : piece.chrome.x;
|
||||
return piece.chrome.rightSide ? piece.chrome.x - piece.s : piece.chrome.x + piece.chrome.width;
|
||||
}
|
||||
y: {
|
||||
if (piece.isWing && !piece.chrome.floating)
|
||||
return piece.chrome.bottomEdge ? piece.chrome.y - piece.s : piece.chrome.y + piece.chrome.height;
|
||||
return piece.chrome.bottomEdge ? piece.chrome.y + piece.chrome.height - piece.s : piece.chrome.y;
|
||||
}
|
||||
width: piece.s
|
||||
height: piece.s
|
||||
x: piece.chrome.x + piece.body.x
|
||||
y: piece.chrome.y + piece.body.y
|
||||
width: piece.body.width
|
||||
height: piece.body.height
|
||||
|
||||
Region {
|
||||
intersection: Intersection.Subtract
|
||||
radius: piece.s
|
||||
width: piece.s * 2
|
||||
height: piece.s * 2
|
||||
x: piece.x - (piece.leadsIn ? piece.s : 0)
|
||||
y: piece.y - (piece.chrome.bottomEdge ? piece.s : 0)
|
||||
radius: piece.chrome.sweepR
|
||||
x: piece.chrome.x + piece.disc.x
|
||||
y: piece.chrome.y + piece.disc.y
|
||||
width: piece.disc.width
|
||||
height: piece.disc.height
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -425,72 +415,93 @@ Item {
|
||||
}
|
||||
|
||||
Item {
|
||||
id: leftInputEnvelope
|
||||
id: leadingInputEnvelope
|
||||
|
||||
x: (root.spanEdges ? 0 : (root.motionRunning ? Math.min(root.islandStartX, root.islandTargetX) - SettingsData.dankIslandSatelliteGap - leftInput.width : leftInput.x)) - (root.spanEdges ? 0 : root.chromeInset)
|
||||
y: root.spanEdges ? root.chromeY : leftInput.y
|
||||
width: root.spanEdges ? (leftInput.width > 0 ? Math.max(0, leftInput.x + leftInput.width + root.chromeInset) : 0) : leftInput.width + root.leftEdgeSpread + root.chromeInset * 2
|
||||
height: root.spanEdges ? (leftInput.width > 0 ? root.chromeHeight : 0) : leftInput.height
|
||||
readonly property real alongPos: (root.spanEdges ? 0 : (root.motionRunning ? Math.min(root.islandStartAlong, root.islandTargetAlong) - root.satelliteGap - leadingInput.alongSize : leadingInput.alongPos)) - (root.spanEdges ? 0 : root.chromeInset)
|
||||
readonly property real alongSize: root.spanEdges ? (leadingInput.alongSize > 0 ? Math.max(0, leadingInput.alongPos + leadingInput.alongSize + root.chromeInset) : 0) : leadingInput.alongSize + root.leadingEdgeSpread + root.chromeInset * 2
|
||||
readonly property real crossPos: root.spanEdges ? root.stripStart : root.rowCross
|
||||
readonly property real crossSize: root.spanEdges ? (leadingInput.alongSize > 0 ? root.stripThickness : 0) : leadingInput.crossSize
|
||||
|
||||
x: root.isVertical ? crossPos : alongPos
|
||||
y: root.isVertical ? alongPos : crossPos
|
||||
width: root.isVertical ? crossSize : alongSize
|
||||
height: root.isVertical ? alongSize : crossSize
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rightInputEnvelope
|
||||
id: trailingInputEnvelope
|
||||
|
||||
readonly property real edgeX: rightInput.width > 0 ? rightInput.x - root.chromeInset : root.width
|
||||
readonly property real edgeAlong: trailingInput.alongSize > 0 ? trailingInput.alongPos - root.chromeInset : root.alongExtent
|
||||
readonly property real alongPos: (root.spanEdges ? edgeAlong : (root.motionRunning ? Math.min(root.islandStartAlongEnd, root.islandTargetAlongEnd) + root.satelliteGap : trailingInput.alongPos)) - (root.spanEdges ? 0 : root.chromeInset)
|
||||
readonly property real alongSize: root.spanEdges ? Math.max(0, root.alongExtent - edgeAlong) : trailingInput.alongSize + root.trailingEdgeSpread + root.chromeInset * 2
|
||||
readonly property real crossPos: root.spanEdges ? root.stripStart : root.rowCross
|
||||
readonly property real crossSize: root.spanEdges ? (trailingInput.alongSize > 0 ? root.stripThickness : 0) : trailingInput.crossSize
|
||||
|
||||
x: (root.spanEdges ? edgeX : (root.motionRunning ? Math.min(root.islandStartRight, root.islandTargetRight) + SettingsData.dankIslandSatelliteGap : rightInput.x)) - (root.spanEdges ? 0 : root.chromeInset)
|
||||
y: root.spanEdges ? root.chromeY : rightInput.y
|
||||
width: root.spanEdges ? Math.max(0, root.width - edgeX) : rightInput.width + root.rightEdgeSpread + root.chromeInset * 2
|
||||
height: root.spanEdges ? (rightInput.width > 0 ? root.chromeHeight : 0) : rightInput.height
|
||||
x: root.isVertical ? crossPos : alongPos
|
||||
y: root.isVertical ? alongPos : crossPos
|
||||
width: root.isVertical ? crossSize : alongSize
|
||||
height: root.isVertical ? alongSize : crossSize
|
||||
}
|
||||
|
||||
IslandSatelliteChrome {
|
||||
id: leftChrome
|
||||
id: leadingChrome
|
||||
|
||||
readonly property real alongPos: root.edgeAligned ? 0 : leadingInput.alongPos - root.chromePad
|
||||
readonly property real alongSize: leadingInput.alongSize > 0 ? (root.edgeAligned ? leadingInput.alongPos + leadingInput.alongSize + root.chromePad : leadingInput.alongSize + root.chromePad * 2) : 0
|
||||
|
||||
floating: !root.edgeAligned
|
||||
x: root.edgeAligned ? 0 : leftInput.x - root.chromePad
|
||||
y: root.chromeY
|
||||
width: leftInput.width > 0 ? (root.edgeAligned ? leftInput.x + leftInput.width + root.chromePad : leftInput.width + root.chromePad * 2) : 0
|
||||
height: root.chromeHeight
|
||||
visible: root.backgroundEnabled && leftInput.width > 0
|
||||
x: root.isVertical ? root.stripStart : alongPos
|
||||
y: root.isVertical ? alongPos : root.stripStart
|
||||
width: root.isVertical ? root.stripThickness : alongSize
|
||||
height: root.isVertical ? alongSize : root.stripThickness
|
||||
visible: root.backgroundEnabled && leadingInput.alongSize > 0
|
||||
fillColor: root.chromeColor
|
||||
gothEnabled: root.gothCorners
|
||||
sweep: root.sweepRadius
|
||||
bottomEdge: root.bottomEdge
|
||||
isVertical: root.isVertical
|
||||
crossFar: root.crossFar
|
||||
parentScreen: root.targetScreen
|
||||
onVisibleChanged: blurRebuildTimer.restart()
|
||||
}
|
||||
|
||||
IslandSatelliteChrome {
|
||||
id: rightChrome
|
||||
id: trailingChrome
|
||||
|
||||
readonly property real alongPos: trailingInput.alongPos - root.chromePad
|
||||
readonly property real alongSize: trailingInput.alongSize > 0 ? (root.edgeAligned ? Math.max(0, root.alongExtent - alongPos) : trailingInput.alongSize + root.chromePad * 2) : 0
|
||||
|
||||
rightSide: true
|
||||
floating: !root.edgeAligned
|
||||
x: rightInput.x - root.chromePad
|
||||
y: root.chromeY
|
||||
width: rightInput.width > 0 ? (root.edgeAligned ? Math.max(0, root.width - x) : rightInput.width + root.chromePad * 2) : 0
|
||||
height: root.chromeHeight
|
||||
visible: root.backgroundEnabled && rightInput.width > 0
|
||||
x: root.isVertical ? root.stripStart : alongPos
|
||||
y: root.isVertical ? alongPos : root.stripStart
|
||||
width: root.isVertical ? root.stripThickness : alongSize
|
||||
height: root.isVertical ? alongSize : root.stripThickness
|
||||
visible: root.backgroundEnabled && trailingInput.alongSize > 0
|
||||
fillColor: root.chromeColor
|
||||
gothEnabled: root.gothCorners
|
||||
sweep: root.sweepRadius
|
||||
bottomEdge: root.bottomEdge
|
||||
isVertical: root.isVertical
|
||||
crossFar: root.crossFar
|
||||
parentScreen: root.targetScreen
|
||||
onVisibleChanged: blurRebuildTimer.restart()
|
||||
}
|
||||
|
||||
Item {
|
||||
id: leftInput
|
||||
id: leadingInput
|
||||
|
||||
x: root.edgeAligned ? root.edgeInset : Math.round(root.islandSurface.currentVisualX - SettingsData.dankIslandSatelliteGap - width)
|
||||
y: root.rowY
|
||||
width: root.visible ? leftWidgetSection.implicitWidth : 0
|
||||
height: root.visible ? root.barThickness : 0
|
||||
onXChanged: root.kickBlur()
|
||||
onWidthChanged: root.kickBlur()
|
||||
readonly property real alongPos: root.edgeAligned ? root.edgeInset : Math.round(root.islandCurrentAlong - root.satelliteGap - alongSize)
|
||||
readonly property real alongSize: root.visible ? (root.isVertical ? leadingWidgetSection.implicitHeight : leadingWidgetSection.implicitWidth) : 0
|
||||
readonly property real crossSize: root.visible ? root.barThickness : 0
|
||||
|
||||
x: root.isVertical ? root.rowCross : alongPos
|
||||
y: root.isVertical ? alongPos : root.rowCross
|
||||
width: root.isVertical ? crossSize : alongSize
|
||||
height: root.isVertical ? alongSize : crossSize
|
||||
onAlongPosChanged: root.kickBlur()
|
||||
onAlongSizeChanged: root.kickBlur()
|
||||
|
||||
LeftSection {
|
||||
id: leftWidgetSection
|
||||
id: leadingWidgetSection
|
||||
|
||||
anchors.fill: parent
|
||||
objectName: "leftSection"
|
||||
@@ -510,17 +521,21 @@ Item {
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rightInput
|
||||
id: trailingInput
|
||||
|
||||
x: root.edgeAligned ? root.width - root.edgeInset - width : Math.round(root.islandSurface.currentVisualX + root.islandSurface.currentVisualWidth + SettingsData.dankIslandSatelliteGap)
|
||||
y: root.rowY
|
||||
width: root.visible ? rightWidgetSection.implicitWidth : 0
|
||||
height: root.visible ? root.barThickness : 0
|
||||
onXChanged: root.kickBlur()
|
||||
onWidthChanged: root.kickBlur()
|
||||
readonly property real alongPos: root.edgeAligned ? root.alongExtent - root.edgeInset - alongSize : Math.round(root.islandCurrentAlongEnd + root.satelliteGap)
|
||||
readonly property real alongSize: root.visible ? (root.isVertical ? trailingWidgetSection.implicitHeight : trailingWidgetSection.implicitWidth) : 0
|
||||
readonly property real crossSize: root.visible ? root.barThickness : 0
|
||||
|
||||
x: root.isVertical ? root.rowCross : alongPos
|
||||
y: root.isVertical ? alongPos : root.rowCross
|
||||
width: root.isVertical ? crossSize : alongSize
|
||||
height: root.isVertical ? alongSize : crossSize
|
||||
onAlongPosChanged: root.kickBlur()
|
||||
onAlongSizeChanged: root.kickBlur()
|
||||
|
||||
RightSection {
|
||||
id: rightWidgetSection
|
||||
id: trailingWidgetSection
|
||||
|
||||
anchors.fill: parent
|
||||
objectName: "rightSection"
|
||||
|
||||
@@ -20,8 +20,8 @@ QtObject {
|
||||
|
||||
property real currentWidth: 176
|
||||
property real currentHeight: 42
|
||||
property real currentOffsetX: 0
|
||||
property real currentOffsetY: 8
|
||||
property real currentOffsetAlong: 0
|
||||
property real currentOffsetCross: 8
|
||||
property real currentTopLeftRadius: 21
|
||||
property real currentTopRightRadius: 21
|
||||
property real currentBottomLeftRadius: 21
|
||||
@@ -29,8 +29,8 @@ QtObject {
|
||||
|
||||
property real targetWidth: currentWidth
|
||||
property real targetHeight: currentHeight
|
||||
property real targetOffsetX: currentOffsetX
|
||||
property real targetOffsetY: currentOffsetY
|
||||
property real targetOffsetAlong: currentOffsetAlong
|
||||
property real targetOffsetCross: currentOffsetCross
|
||||
property real targetTopLeftRadius: currentTopLeftRadius
|
||||
property real targetTopRightRadius: currentTopRightRadius
|
||||
property real targetBottomLeftRadius: currentBottomLeftRadius
|
||||
@@ -38,15 +38,15 @@ QtObject {
|
||||
|
||||
property real velocityWidth: 0
|
||||
property real velocityHeight: 0
|
||||
property real velocityOffsetX: 0
|
||||
property real velocityOffsetY: 0
|
||||
property real velocityOffsetAlong: 0
|
||||
property real velocityOffsetCross: 0
|
||||
property real velocityTopLeftRadius: 0
|
||||
property real velocityTopRightRadius: 0
|
||||
property real velocityBottomLeftRadius: 0
|
||||
property real velocityBottomRightRadius: 0
|
||||
|
||||
function matchesTarget(target) {
|
||||
return targetWidth === target.width && targetHeight === target.height && targetOffsetX === target.offsetX && targetOffsetY === target.offsetY && targetTopLeftRadius === target.topLeftRadius && targetTopRightRadius === target.topRightRadius && targetBottomLeftRadius === target.bottomLeftRadius && targetBottomRightRadius === target.bottomRightRadius;
|
||||
return targetWidth === target.width && targetHeight === target.height && targetOffsetAlong === target.offsetAlong && targetOffsetCross === target.offsetCross && targetTopLeftRadius === target.topLeftRadius && targetTopRightRadius === target.topRightRadius && targetBottomLeftRadius === target.bottomLeftRadius && targetBottomRightRadius === target.bottomRightRadius;
|
||||
}
|
||||
|
||||
function setTarget(target) {
|
||||
@@ -54,8 +54,8 @@ QtObject {
|
||||
return;
|
||||
targetWidth = target.width;
|
||||
targetHeight = target.height;
|
||||
targetOffsetX = target.offsetX;
|
||||
targetOffsetY = target.offsetY;
|
||||
targetOffsetAlong = target.offsetAlong;
|
||||
targetOffsetCross = target.offsetCross;
|
||||
targetTopLeftRadius = target.topLeftRadius;
|
||||
targetTopRightRadius = target.topRightRadius;
|
||||
targetBottomLeftRadius = target.bottomLeftRadius;
|
||||
@@ -73,8 +73,8 @@ QtObject {
|
||||
function snapTo(target) {
|
||||
targetWidth = target.width;
|
||||
targetHeight = target.height;
|
||||
targetOffsetX = target.offsetX;
|
||||
targetOffsetY = target.offsetY;
|
||||
targetOffsetAlong = target.offsetAlong;
|
||||
targetOffsetCross = target.offsetCross;
|
||||
targetTopLeftRadius = target.topLeftRadius;
|
||||
targetTopRightRadius = target.topRightRadius;
|
||||
targetBottomLeftRadius = target.bottomLeftRadius;
|
||||
@@ -82,8 +82,8 @@ QtObject {
|
||||
|
||||
currentWidth = target.width;
|
||||
currentHeight = target.height;
|
||||
currentOffsetX = target.offsetX;
|
||||
currentOffsetY = target.offsetY;
|
||||
currentOffsetAlong = target.offsetAlong;
|
||||
currentOffsetCross = target.offsetCross;
|
||||
currentTopLeftRadius = target.topLeftRadius;
|
||||
currentTopRightRadius = target.topRightRadius;
|
||||
currentBottomLeftRadius = target.bottomLeftRadius;
|
||||
@@ -91,8 +91,8 @@ QtObject {
|
||||
|
||||
velocityWidth = 0;
|
||||
velocityHeight = 0;
|
||||
velocityOffsetX = 0;
|
||||
velocityOffsetY = 0;
|
||||
velocityOffsetAlong = 0;
|
||||
velocityOffsetCross = 0;
|
||||
velocityTopLeftRadius = 0;
|
||||
velocityTopRightRadius = 0;
|
||||
velocityBottomLeftRadius = 0;
|
||||
@@ -101,16 +101,16 @@ QtObject {
|
||||
}
|
||||
|
||||
function isSettled() {
|
||||
const positionSettled = Math.abs(targetWidth - currentWidth) <= positionEpsilon && Math.abs(targetHeight - currentHeight) <= positionEpsilon && Math.abs(targetOffsetX - currentOffsetX) <= positionEpsilon && Math.abs(targetOffsetY - currentOffsetY) <= positionEpsilon && Math.abs(targetTopLeftRadius - currentTopLeftRadius) <= positionEpsilon && Math.abs(targetTopRightRadius - currentTopRightRadius) <= positionEpsilon && Math.abs(targetBottomLeftRadius - currentBottomLeftRadius) <= positionEpsilon && Math.abs(targetBottomRightRadius - currentBottomRightRadius) <= positionEpsilon;
|
||||
const velocitySettled = Math.abs(velocityWidth) <= velocityEpsilon && Math.abs(velocityHeight) <= velocityEpsilon && Math.abs(velocityOffsetX) <= velocityEpsilon && Math.abs(velocityOffsetY) <= velocityEpsilon && Math.abs(velocityTopLeftRadius) <= velocityEpsilon && Math.abs(velocityTopRightRadius) <= velocityEpsilon && Math.abs(velocityBottomLeftRadius) <= velocityEpsilon && Math.abs(velocityBottomRightRadius) <= velocityEpsilon;
|
||||
const positionSettled = Math.abs(targetWidth - currentWidth) <= positionEpsilon && Math.abs(targetHeight - currentHeight) <= positionEpsilon && Math.abs(targetOffsetAlong - currentOffsetAlong) <= positionEpsilon && Math.abs(targetOffsetCross - currentOffsetCross) <= positionEpsilon && Math.abs(targetTopLeftRadius - currentTopLeftRadius) <= positionEpsilon && Math.abs(targetTopRightRadius - currentTopRightRadius) <= positionEpsilon && Math.abs(targetBottomLeftRadius - currentBottomLeftRadius) <= positionEpsilon && Math.abs(targetBottomRightRadius - currentBottomRightRadius) <= positionEpsilon;
|
||||
const velocitySettled = Math.abs(velocityWidth) <= velocityEpsilon && Math.abs(velocityHeight) <= velocityEpsilon && Math.abs(velocityOffsetAlong) <= velocityEpsilon && Math.abs(velocityOffsetCross) <= velocityEpsilon && Math.abs(velocityTopLeftRadius) <= velocityEpsilon && Math.abs(velocityTopRightRadius) <= velocityEpsilon && Math.abs(velocityBottomLeftRadius) <= velocityEpsilon && Math.abs(velocityBottomRightRadius) <= velocityEpsilon;
|
||||
return positionSettled && velocitySettled;
|
||||
}
|
||||
|
||||
function settle() {
|
||||
currentWidth = targetWidth;
|
||||
currentHeight = targetHeight;
|
||||
currentOffsetX = targetOffsetX;
|
||||
currentOffsetY = targetOffsetY;
|
||||
currentOffsetAlong = targetOffsetAlong;
|
||||
currentOffsetCross = targetOffsetCross;
|
||||
currentTopLeftRadius = targetTopLeftRadius;
|
||||
currentTopRightRadius = targetTopRightRadius;
|
||||
currentBottomLeftRadius = targetBottomLeftRadius;
|
||||
@@ -118,8 +118,8 @@ QtObject {
|
||||
|
||||
velocityWidth = 0;
|
||||
velocityHeight = 0;
|
||||
velocityOffsetX = 0;
|
||||
velocityOffsetY = 0;
|
||||
velocityOffsetAlong = 0;
|
||||
velocityOffsetCross = 0;
|
||||
velocityTopLeftRadius = 0;
|
||||
velocityTopRightRadius = 0;
|
||||
velocityBottomLeftRadius = 0;
|
||||
@@ -142,8 +142,8 @@ QtObject {
|
||||
for (let i = 0; i < steps; i++) {
|
||||
velocityWidth += (stiffness * (targetWidth - currentWidth) - damping * velocityWidth) * inverseMass * step;
|
||||
velocityHeight += (stiffness * (targetHeight - currentHeight) - damping * velocityHeight) * inverseMass * step;
|
||||
velocityOffsetX += (stiffness * (targetOffsetX - currentOffsetX) - damping * velocityOffsetX) * inverseMass * step;
|
||||
velocityOffsetY += (stiffness * (targetOffsetY - currentOffsetY) - damping * velocityOffsetY) * inverseMass * step;
|
||||
velocityOffsetAlong += (stiffness * (targetOffsetAlong - currentOffsetAlong) - damping * velocityOffsetAlong) * inverseMass * step;
|
||||
velocityOffsetCross += (stiffness * (targetOffsetCross - currentOffsetCross) - damping * velocityOffsetCross) * inverseMass * step;
|
||||
velocityTopLeftRadius += (stiffness * (targetTopLeftRadius - currentTopLeftRadius) - damping * velocityTopLeftRadius) * inverseMass * step;
|
||||
velocityTopRightRadius += (stiffness * (targetTopRightRadius - currentTopRightRadius) - damping * velocityTopRightRadius) * inverseMass * step;
|
||||
velocityBottomLeftRadius += (stiffness * (targetBottomLeftRadius - currentBottomLeftRadius) - damping * velocityBottomLeftRadius) * inverseMass * step;
|
||||
@@ -151,8 +151,8 @@ QtObject {
|
||||
|
||||
currentWidth += velocityWidth * step;
|
||||
currentHeight += velocityHeight * step;
|
||||
currentOffsetX += velocityOffsetX * step;
|
||||
currentOffsetY += velocityOffsetY * step;
|
||||
currentOffsetAlong += velocityOffsetAlong * step;
|
||||
currentOffsetCross += velocityOffsetCross * step;
|
||||
currentTopLeftRadius += velocityTopLeftRadius * step;
|
||||
currentTopRightRadius += velocityTopRightRadius * step;
|
||||
currentBottomLeftRadius += velocityBottomLeftRadius * step;
|
||||
|
||||
@@ -96,7 +96,7 @@ Item {
|
||||
return toplevel.activated;
|
||||
} else if (appData.type === "grouped") {
|
||||
// For grouped apps, check if any window is focused
|
||||
const allToplevels = ToplevelManager.toplevels.values;
|
||||
const allToplevels = CompositorService.isAqueous && AqueousService.available ? CompositorService.sortedToplevels : ToplevelManager.toplevels.values;
|
||||
for (let i = 0; i < allToplevels.length; i++) {
|
||||
const toplevel = allToplevels[i];
|
||||
if (toplevel.appId === appData.appId && toplevel.activated) {
|
||||
|
||||
@@ -262,7 +262,7 @@ Item {
|
||||
const separatePinnedAndRunning = SettingsData.dockSeparatePinnedAndRunningApps;
|
||||
|
||||
sortedToplevels.forEach((toplevel, index) => {
|
||||
let uniqueKey = "window_" + index;
|
||||
let uniqueKey = toplevel.aqueousKey || "window_" + index;
|
||||
if (CompositorService.isHyprland && Hyprland.toplevels) {
|
||||
const hyprlandToplevels = Array.from(Hyprland.toplevels.values);
|
||||
for (let i = 0; i < hyprlandToplevels.length; i++) {
|
||||
|
||||
@@ -249,7 +249,7 @@ Item {
|
||||
readonly property bool shouldHideForWindows: {
|
||||
if (!SettingsData.dockSmartAutoHide)
|
||||
return false;
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango && !CompositorService.isAqueous)
|
||||
return false;
|
||||
|
||||
const screenName = dock.modelData?.name ?? "";
|
||||
@@ -313,6 +313,9 @@ Item {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CompositorService.isAqueous && AqueousService.available)
|
||||
return AqueousService.overlapsDock(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight);
|
||||
|
||||
if (CompositorService.isMango) {
|
||||
MangoService.windows;
|
||||
MangoService.outputs;
|
||||
@@ -348,7 +351,7 @@ Item {
|
||||
onTriggered: dock.startupRevealDone = true
|
||||
}
|
||||
|
||||
readonly property bool overviewReveal: CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview
|
||||
readonly property bool overviewReveal: CompositorService.overviewActiveOnScreen(screenName) && SettingsData.dockOpenOnOverview
|
||||
readonly property bool hoverOrActive: dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky
|
||||
|
||||
onOverviewRevealChanged: {
|
||||
|
||||
@@ -87,7 +87,7 @@ PanelWindow {
|
||||
|
||||
readonly property bool _connectedActive: CompositorService.usesConnectedFrameChromeForScreen(win.targetScreen)
|
||||
readonly property bool _dockHostedHere: {
|
||||
const dockVisible = SettingsData.showDock || (CompositorService.isNiri && SettingsData.dockOpenOnOverview);
|
||||
const dockVisible = SettingsData.showDock || ((CompositorService.isNiri || CompositorService.isAqueous) && SettingsData.dockOpenOnOverview);
|
||||
if (!win._connectedActive || !dockVisible || SettingsData.dockUseOverlayLayer)
|
||||
return false;
|
||||
const screens = SettingsData.getFilteredScreens("dock");
|
||||
|
||||
@@ -834,6 +834,8 @@ Item {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: passwordBox
|
||||
|
||||
property bool showPassword: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
@@ -1230,61 +1232,100 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: passwordDisplay
|
||||
Item {
|
||||
id: passwordViewport
|
||||
|
||||
property real scrollX: 0
|
||||
property real scrollY: 0
|
||||
|
||||
function followCursor() {
|
||||
const rect = passwordDisplay.cursorRectangle;
|
||||
const contentWidth = passwordDisplay.contentWidth + passwordCursor.width;
|
||||
const contentHeight = passwordDisplay.contentHeight;
|
||||
scrollX = followAxis(scrollX, rect.x, rect.x + passwordCursor.width, contentWidth, width);
|
||||
scrollY = contentHeight < height ? (height - contentHeight) / 2 : followAxis(scrollY, rect.y, rect.y + rect.height, contentHeight, height);
|
||||
}
|
||||
|
||||
function followAxis(offset, start, end, contentSize, viewSize) {
|
||||
if (contentSize <= viewSize)
|
||||
return 0;
|
||||
if (start + offset < 0)
|
||||
return -start;
|
||||
if (end + offset > viewSize)
|
||||
return viewSize - end;
|
||||
return Math.max(viewSize - contentSize, Math.min(0, offset));
|
||||
}
|
||||
|
||||
anchors.left: lockIconContainer.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (securityKeyButton.visible ? securityKeyButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))))
|
||||
anchors.rightMargin: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
if (demoMode) {
|
||||
return "••••••••";
|
||||
}
|
||||
if (parent.showPassword) {
|
||||
return root.passwordBuffer;
|
||||
}
|
||||
return "•".repeat(root.passwordBuffer.length);
|
||||
}
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: parent.showPassword ? Theme.fontSizeMedium : Theme.fontSizeLarge
|
||||
opacity: (demoMode || root.passwordBuffer.length > 0) ? 1 : 0
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
anchors.bottomMargin: Theme.spacingXS
|
||||
clip: true
|
||||
elide: Text.ElideNone
|
||||
horizontalAlignment: implicitWidth > width ? Text.AlignRight : Text.AlignLeft
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
onWidthChanged: followCursor()
|
||||
onHeightChanged: followCursor()
|
||||
|
||||
TextEdit {
|
||||
id: passwordDisplay
|
||||
|
||||
x: passwordViewport.scrollX
|
||||
y: passwordViewport.scrollY
|
||||
width: wrapMode === TextEdit.NoWrap ? implicitWidth : passwordViewport.width
|
||||
readOnly: true
|
||||
activeFocusOnPress: false
|
||||
selectByMouse: false
|
||||
wrapMode: passwordBox.showPassword ? TextEdit.Wrap : TextEdit.NoWrap
|
||||
text: {
|
||||
if (demoMode) {
|
||||
return "••••••••";
|
||||
}
|
||||
if (passwordBox.showPassword) {
|
||||
return root.passwordBuffer;
|
||||
}
|
||||
return "•".repeat(root.passwordBuffer.length);
|
||||
}
|
||||
color: Theme.surfaceText
|
||||
font.family: Theme.fontFamily
|
||||
font.weight: Theme.fontWeight
|
||||
font.pixelSize: passwordBox.showPassword ? Theme.fontSizeMedium : Theme.fontSizeLarge
|
||||
opacity: (demoMode || root.passwordBuffer.length > 0) ? 1 : 0
|
||||
|
||||
onTextChanged: cursorPosition = passwordField.cursorPosition
|
||||
onCursorRectangleChanged: passwordViewport.followCursor()
|
||||
onContentWidthChanged: passwordViewport.followCursor()
|
||||
onContentHeightChanged: passwordViewport.followCursor()
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: passwordCursorMetrics
|
||||
font: passwordDisplay.font
|
||||
text: passwordDisplay.text.slice(0, passwordField.cursorPosition)
|
||||
}
|
||||
DankTextCursor {
|
||||
id: passwordCursor
|
||||
|
||||
DankTextCursor {
|
||||
id: passwordCursor
|
||||
x: passwordDisplay.x + passwordDisplay.cursorRectangle.x
|
||||
y: passwordDisplay.y + passwordDisplay.cursorRectangle.y
|
||||
height: passwordDisplay.cursorRectangle.height
|
||||
shown: !demoMode && passwordField.activeFocus && !pam.passwd.active && !pam.u2fPending && !root.unlocking
|
||||
|
||||
x: passwordDisplay.x + passwordCursorMetrics.advanceWidth + Math.min(0, passwordDisplay.width - passwordDisplay.implicitWidth)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: passwordDisplay.font.pixelSize + 4
|
||||
shown: !demoMode && passwordField.activeFocus && !pam.passwd.active && !pam.u2fPending && !root.unlocking
|
||||
Connections {
|
||||
target: passwordField
|
||||
|
||||
Connections {
|
||||
target: passwordField
|
||||
function onCursorPositionChanged() {
|
||||
passwordDisplay.cursorPosition = passwordField.cursorPosition;
|
||||
passwordCursor.resetBlink();
|
||||
}
|
||||
|
||||
function onCursorPositionChanged() {
|
||||
passwordCursor.resetBlink();
|
||||
}
|
||||
|
||||
function onTextChanged() {
|
||||
passwordCursor.resetBlink();
|
||||
function onTextChanged() {
|
||||
passwordCursor.resetBlink();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1035,7 +1035,7 @@ PanelWindow {
|
||||
iconSize: compactMode ? 14 : 16
|
||||
buttonSize: compactMode ? 20 : 24
|
||||
z: 15
|
||||
visible: (SettingsData.notificationPopupPrivacyMode || win.bodyClickInvokesAction) && win.hasExpandableBody
|
||||
visible: win.hasExpandableBody && (SettingsData.notificationPopupPrivacyMode || win.bodyClickInvokesAction || bodyText.hasMoreText || win.descriptionExpanded)
|
||||
|
||||
onClicked: {
|
||||
if (win.hasExpandableBody)
|
||||
|
||||
@@ -18,6 +18,7 @@ Item {
|
||||
property bool isMiracle: CompositorService.isMiracle
|
||||
property bool isMango: CompositorService.isMango
|
||||
property bool isLabwc: CompositorService.isLabwc
|
||||
property bool isAqueous: CompositorService.isAqueous
|
||||
|
||||
property string compositorName: {
|
||||
if (isHyprland)
|
||||
@@ -32,6 +33,8 @@ Item {
|
||||
return "mangowc";
|
||||
if (isLabwc)
|
||||
return "labwc";
|
||||
if (isAqueous)
|
||||
return "aqueous";
|
||||
return "niri";
|
||||
}
|
||||
|
||||
@@ -48,6 +51,8 @@ Item {
|
||||
return "/assets/mango.png";
|
||||
if (isLabwc)
|
||||
return "/assets/labwc.png";
|
||||
if (isAqueous)
|
||||
return "/assets/aqueous.svg";
|
||||
return "/assets/niri.svg";
|
||||
}
|
||||
|
||||
@@ -64,6 +69,8 @@ Item {
|
||||
return "https://github.com/DreamMaoMao/mangowc";
|
||||
if (isLabwc)
|
||||
return "https://labwc.github.io/";
|
||||
if (isAqueous)
|
||||
return "";
|
||||
return "https://github.com/niri-wm/niri";
|
||||
}
|
||||
|
||||
@@ -80,6 +87,8 @@ Item {
|
||||
return I18n.tr("mangowc GitHub");
|
||||
if (isLabwc)
|
||||
return I18n.tr("LabWC Website");
|
||||
if (isAqueous)
|
||||
return "Aqueous";
|
||||
return I18n.tr("niri GitHub");
|
||||
}
|
||||
|
||||
@@ -376,11 +385,15 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
cursorShape: compositorUrl === "" ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onEntered: parent.hovered = true
|
||||
onExited: parent.hovered = false
|
||||
onClicked: Qt.openUrlExternally(compositorUrl)
|
||||
onClicked: {
|
||||
if (compositorUrl === "")
|
||||
return;
|
||||
Qt.openUrlExternally(compositorUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,12 +61,11 @@ Item {
|
||||
|
||||
// The selected instance is itself the island, so bar-surface settings do not apply to it.
|
||||
readonly property bool selectedBarIsIsland: {
|
||||
SettingsData.dankIslandBarId;
|
||||
SettingsData.barConfigs;
|
||||
selectedBarId;
|
||||
return !!selectedBarId && selectedBarId === SettingsData.dankIslandBarId;
|
||||
return SettingsData.isIslandBarConfig(selectedBarConfig);
|
||||
}
|
||||
readonly property int islandShadowedScreenCount: {
|
||||
SettingsData.dankIslandBarId;
|
||||
SettingsData.barConfigs;
|
||||
if (!selectedBarConfig || dankBarTab.selectedBarIsIsland)
|
||||
return 0;
|
||||
@@ -84,14 +83,12 @@ Item {
|
||||
readonly property bool selectedBarFrameStyled: SettingsData.frameEnabled && dankBarTab.frameStyledScreenCount === dankBarTab.selectedBarScreens.length
|
||||
// Connected mode sanitizes shadow/corner/border fields on every config, so those stay frame-managed shell-wide.
|
||||
readonly property bool selectedBarFrameSanitized: SettingsData.connectedFrameModeActive || dankBarTab.selectedBarFrameStyled
|
||||
// An edge another island already holds on every covered screen would just stack on top of it.
|
||||
readonly property var positionChoices: {
|
||||
SettingsData.dankIslandBarId;
|
||||
SettingsData.barConfigs;
|
||||
Quickshell.screens;
|
||||
selectedBarId;
|
||||
const all = [SettingsData.Position.Top, SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right];
|
||||
if (dankBarTab.selectedBarIsIsland)
|
||||
return [SettingsData.Position.Top, SettingsData.Position.Bottom];
|
||||
if (!selectedBarConfig)
|
||||
return all;
|
||||
const screens = dankBarTab.selectedBarScreens;
|
||||
@@ -102,6 +99,8 @@ Item {
|
||||
if (pos === current)
|
||||
return true;
|
||||
const edge = SettingsData.positionToSide(pos);
|
||||
if (dankBarTab.selectedBarIsIsland)
|
||||
return !screens.some(screen => SettingsData.islandEdgeTakenFor(selectedBarConfig, screen, edge));
|
||||
return !screens.every(screen => SettingsData.dankIslandOwnsEdge(screen, edge));
|
||||
});
|
||||
}
|
||||
@@ -254,14 +253,14 @@ Item {
|
||||
function deleteBar(barId) {
|
||||
if (barId === "default")
|
||||
return;
|
||||
if (SettingsData.getBarKindConfigs().length <= 1 && barId !== SettingsData.dankIslandBarId)
|
||||
if (SettingsData.getBarKindConfigs().length <= 1 && !SettingsData.isIslandBarConfig(SettingsData.getBarConfig(barId)))
|
||||
return;
|
||||
SettingsData.deleteBarConfig(barId);
|
||||
selectedBarId = "default";
|
||||
}
|
||||
|
||||
function toggleBarEnabled(barId) {
|
||||
if (barId === "default" && barId !== SettingsData.dankIslandBarId)
|
||||
if (barId === "default" && !SettingsData.isIslandBarConfig(SettingsData.getBarConfig(barId)))
|
||||
return;
|
||||
const config = SettingsData.getBarConfig(barId);
|
||||
if (!config)
|
||||
@@ -502,7 +501,7 @@ Item {
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
visible: SettingsData.dankIslandBarId === barCard.modelData.id
|
||||
visible: SettingsData.isIslandBarConfig(barCard.modelData)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -511,7 +510,7 @@ Item {
|
||||
font.weight: Font.Medium
|
||||
color: Theme.primary
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
visible: SettingsData.dankIslandBarId === barCard.modelData.id
|
||||
visible: SettingsData.isIslandBarConfig(barCard.modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -524,7 +523,7 @@ Item {
|
||||
backgroundColor: Theme.withAlpha(Theme.error, 0.15)
|
||||
iconColor: Theme.error
|
||||
visible: barCard.modelData.id !== "default"
|
||||
enabled: SettingsData.getBarKindConfigs().length > 1 || SettingsData.dankIslandBarId === barCard.modelData.id
|
||||
enabled: SettingsData.getBarKindConfigs().length > 1 || SettingsData.isIslandBarConfig(barCard.modelData)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: dankBarTab.deleteBar(barCard.modelData.id)
|
||||
}
|
||||
@@ -581,16 +580,16 @@ Item {
|
||||
target: "island"
|
||||
visible: dankBarTab.selectedBarIsIsland
|
||||
parentModal: dankBarTab.parentModal
|
||||
section: "dankIslandPlacement"
|
||||
section: "islandPlacement"
|
||||
settingLabel: I18n.tr("Island placement")
|
||||
reason: I18n.tr("The Island anchors to the top or bottom edge")
|
||||
reason: I18n.tr("The Island anchors to one screen edge")
|
||||
}
|
||||
|
||||
SettingsControlledBy {
|
||||
target: "island"
|
||||
visible: dankBarTab.islandShadowedScreenCount > 0
|
||||
parentModal: dankBarTab.parentModal
|
||||
section: "dankIslandPlacement"
|
||||
section: "islandPlacement"
|
||||
settingLabel: I18n.tr("%1 position").arg(dankBarTab.positionLabel(selectedBarConfig?.position ?? SettingsData.Position.Top))
|
||||
reason: I18n.tr("The Island holds this edge on a display this bar covers, so the bar stays hidden there")
|
||||
}
|
||||
@@ -739,7 +738,7 @@ Item {
|
||||
target: "island"
|
||||
visible: dankBarTab.islandOwnsSelectedBarTop
|
||||
parentModal: dankBarTab.parentModal
|
||||
section: "dankIslandPlacement"
|
||||
section: "islandPlacement"
|
||||
settingLabel: I18n.tr("Bar visibility")
|
||||
reason: I18n.tr("Additional settings are managed by the Island")
|
||||
}
|
||||
@@ -875,15 +874,15 @@ Item {
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.15
|
||||
visible: CompositorService.isNiri && !dankBarTab.islandOwnsSelectedBarTop
|
||||
visible: (CompositorService.isNiri || CompositorService.isAqueous) && !dankBarTab.islandOwnsSelectedBarTop
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barOpenOnOverview"
|
||||
tags: ["bar", "overview", "niri", "show", "frame"]
|
||||
visible: CompositorService.isNiri && !dankBarTab.islandOwnsSelectedBarTop
|
||||
visible: (CompositorService.isNiri || CompositorService.isAqueous) && !dankBarTab.islandOwnsSelectedBarTop
|
||||
text: I18n.tr("Show on Overview")
|
||||
description: dankBarTab.selectedBarFrameStyled ? I18n.tr("Show during Niri overview") : I18n.tr("Show the bar when niri overview is active")
|
||||
description: CompositorService.isAqueous ? I18n.tr("Show on Overview") : dankBarTab.selectedBarFrameStyled ? I18n.tr("Show during Niri overview") : I18n.tr("Show the bar when niri overview is active")
|
||||
checked: dankBarTab.selectedBarFrameStyled ? SettingsData.frameShowOnOverview : (selectedBarConfig?.openOnOverview ?? false)
|
||||
onToggled: toggled => {
|
||||
if (dankBarTab.selectedBarFrameStyled) {
|
||||
@@ -1813,12 +1812,13 @@ Item {
|
||||
visible: dankBarTab.appearanceOnly && (selectedBarConfig?.enabled ?? false) && !dankBarTab.selectedBarFrameSanitized && !dankBarTab.islandOwnsSelectedBarTop
|
||||
|
||||
readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0
|
||||
readonly property bool barShadowsEnabled: (SettingsData.m3ElevationEnabled ?? true) && (SettingsData.barElevationEnabled ?? true)
|
||||
readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "default") === "custom"
|
||||
readonly property string directionSource: selectedBarConfig?.shadowDirectionMode ?? "inherit"
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Enable a custom override below to set per-bar shadow intensity, opacity, and color.")
|
||||
text: shadowCard.barShadowsEnabled ? I18n.tr("Enable a custom override below to set per-bar shadow intensity, opacity, and color.") : I18n.tr("Requires Bar Shadows to be enabled in Theme & Colors.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
@@ -15,9 +15,8 @@ Item {
|
||||
|
||||
readonly property var instanceChoices: {
|
||||
SettingsData.barConfigs;
|
||||
SettingsData.dankIslandBarId;
|
||||
const rows = [];
|
||||
const configs = SettingsData.barConfigs || [];
|
||||
const configs = SettingsData.islandBarConfigs || [];
|
||||
for (let i = 0; i < configs.length; i++)
|
||||
rows.push({
|
||||
"id": configs[i].id,
|
||||
@@ -25,7 +24,36 @@ Item {
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
readonly property int instanceIndex: Math.max(0, root.instanceChoices.findIndex(row => row.id === SettingsData.dankIslandBarId))
|
||||
// Follows the Bars tab selection when that instance is an island, so the two stay in step.
|
||||
readonly property string selectedIslandId: {
|
||||
SettingsUiState.selectedBarId;
|
||||
const choices = root.instanceChoices;
|
||||
if (choices.length === 0)
|
||||
return "";
|
||||
if (choices.some(row => row.id === SettingsUiState.selectedBarId))
|
||||
return SettingsUiState.selectedBarId;
|
||||
return choices[0].id;
|
||||
}
|
||||
readonly property var config: {
|
||||
SettingsData.barConfigs;
|
||||
return SettingsData.getBarConfig(root.selectedIslandId);
|
||||
}
|
||||
readonly property bool islandEnabled: root.config?.enabled ?? false
|
||||
readonly property bool isVertical: SettingsData.islandVertical(root.config)
|
||||
readonly property int instanceIndex: Math.max(0, root.instanceChoices.findIndex(row => row.id === root.selectedIslandId))
|
||||
|
||||
function setting(key) {
|
||||
return SettingsData.islandSetting(root.config, key);
|
||||
}
|
||||
|
||||
function apply(key, value) {
|
||||
if (!root.selectedIslandId)
|
||||
return;
|
||||
const updates = {};
|
||||
updates[key] = value;
|
||||
SettingsData.updateBarConfig(root.selectedIslandId, updates);
|
||||
}
|
||||
readonly property var clockDisplayValues: ["time", "date", "both"]
|
||||
readonly property var systemLevelDisplayValues: ["icon", "percentage", "both"]
|
||||
readonly property var paletteValues: ["default", "bright", "dim"]
|
||||
readonly property var batteryStyleValues: ["solid", "outline", "ring"]
|
||||
@@ -55,32 +83,32 @@ Item {
|
||||
width: parent.width
|
||||
iconName: "view_in_ar"
|
||||
title: I18n.tr("Dank Island", "island settings: page title")
|
||||
settingKey: "dankIslandInstance"
|
||||
settingKey: "islandInstance"
|
||||
tags: ["island", "layout", "standard", "frame", "mode", "bar"]
|
||||
|
||||
SettingsLayoutPicker {}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
settingKey: "dankIslandBarId"
|
||||
settingKey: "islandInstanceSelect"
|
||||
tags: ["island", "activities", "media", "notifications", "osd", "bar"]
|
||||
visible: !!SettingsData.dankIslandBarId && SettingsData.barConfigs.length > 1
|
||||
text: I18n.tr("Island Instance", "island settings: which bar config the island replaces")
|
||||
visible: root.instanceChoices.length > 1
|
||||
text: I18n.tr("Island Instance", "island settings: which island instance these settings edit")
|
||||
model: root.instanceChoices.map(row => row.name)
|
||||
currentIndex: root.instanceIndex
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected)
|
||||
SettingsData.setIslandBarId(root.instanceChoices[index]?.id ?? "");
|
||||
SettingsUiState.selectedBarId = root.instanceChoices[index]?.id ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankIslandEnable"
|
||||
settingKey: "islandEnable"
|
||||
tags: ["island", "enable", "show", "hide"]
|
||||
visible: !!SettingsData.dankIslandBarId
|
||||
visible: !!root.selectedIslandId
|
||||
text: I18n.tr("Enable Island")
|
||||
description: I18n.tr("Toggle the Island on this instance's displays")
|
||||
checked: SettingsData.dankIslandEnabled
|
||||
onToggled: checked => SettingsData.updateBarConfig(SettingsData.dankIslandBarId, {
|
||||
checked: root.islandEnabled
|
||||
onToggled: checked => SettingsData.updateBarConfig(root.selectedIslandId, {
|
||||
enabled: checked
|
||||
})
|
||||
}
|
||||
@@ -90,15 +118,15 @@ Item {
|
||||
width: parent.width
|
||||
iconName: "monitor"
|
||||
title: I18n.tr("Display Assignment")
|
||||
settingKey: "dankIslandDisplays"
|
||||
settingKey: "islandDisplays"
|
||||
collapsible: true
|
||||
expanded: false
|
||||
visible: SettingsData.dankIslandEnabled
|
||||
visible: root.islandEnabled
|
||||
|
||||
SettingsDisplayPicker {
|
||||
emptyMeansAll: false
|
||||
displayPreferences: SettingsData.islandBarConfig?.screenPreferences ?? ["all"]
|
||||
onPreferencesChanged: prefs => SettingsData.updateBarConfig(SettingsData.dankIslandBarId, {
|
||||
displayPreferences: root.config?.screenPreferences ?? ["all"]
|
||||
onPreferencesChanged: prefs => SettingsData.updateBarConfig(root.selectedIslandId, {
|
||||
screenPreferences: prefs
|
||||
})
|
||||
}
|
||||
@@ -108,58 +136,58 @@ Item {
|
||||
width: parent.width
|
||||
iconName: "open_with"
|
||||
title: I18n.tr("Position", "island settings: position card title")
|
||||
settingKey: "dankIslandPlacement"
|
||||
visible: SettingsData.dankIslandEnabled
|
||||
settingKey: "islandPlacement"
|
||||
visible: root.islandEnabled
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankIslandFloating"
|
||||
settingKey: "islandFloating"
|
||||
tags: ["island", "placement", "float", "overlay", "exclusive", "reserve"]
|
||||
text: I18n.tr("Float", "island settings: float toggle")
|
||||
description: I18n.tr("Floats above windows and other content", "island settings: float toggle description")
|
||||
checked: SettingsData.dankIslandFloating
|
||||
onToggled: checked => SettingsData.set("dankIslandFloating", checked)
|
||||
checked: root.setting("islandFloating")
|
||||
onToggled: checked => root.apply("islandFloating", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankIslandUseOverlayLayer"
|
||||
settingKey: "islandUseOverlayLayer"
|
||||
tags: ["island", "fullscreen", "overlay", "layer"]
|
||||
text: I18n.tr("Use Overlay Layer", "island layer toggle: use Wayland overlay layer")
|
||||
description: I18n.tr("Stage the Wayland overlay layer to remain visible over fullscreen apps", "island settings: overlay layer description")
|
||||
checked: SettingsData.dankIslandUseOverlayLayer
|
||||
onToggled: checked => SettingsData.set("dankIslandUseOverlayLayer", checked)
|
||||
checked: root.setting("islandUseOverlayLayer")
|
||||
onToggled: checked => root.apply("islandUseOverlayLayer", checked)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandReserveHeight"
|
||||
tags: ["island", "placement", "reservation", "exclusive", "height"]
|
||||
text: I18n.tr("Reserved Height", "island settings: reserved strip height slider")
|
||||
settingKey: "islandReserveThickness"
|
||||
tags: ["island", "placement", "reservation", "exclusive", "height", "width", "thickness"]
|
||||
text: !root.isVertical ? I18n.tr("Reserved Height", "island settings: reserved strip height slider") : I18n.tr("Reserved Width", "island settings: reserved strip width slider")
|
||||
description: I18n.tr("Space kept clear for compact island and satellites", "island settings: reserved height description")
|
||||
unit: "px"
|
||||
minimum: 24
|
||||
maximum: 128
|
||||
step: 1
|
||||
defaultValue: 40
|
||||
value: SettingsData.dankIslandReserveHeight
|
||||
enabled: !SettingsData.dankIslandFloating
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandReserveHeight", value)
|
||||
value: root.setting("islandReserveThickness")
|
||||
enabled: !root.setting("islandFloating")
|
||||
onSliderValueChanged: value => root.apply("islandReserveThickness", value)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandCompactHeight"
|
||||
tags: ["island", "placement", "compact", "height", "size", "satellite"]
|
||||
text: I18n.tr("Compact Island Height", "island settings: compact pill height slider")
|
||||
settingKey: "islandCompactThickness"
|
||||
tags: ["island", "placement", "compact", "height", "width", "thickness", "size", "satellite"]
|
||||
text: !root.isVertical ? I18n.tr("Compact Island Height", "island settings: compact pill height slider") : I18n.tr("Compact Island Width", "island settings: compact pill width slider")
|
||||
description: I18n.tr("Resize the resting island to align with nearby widgets", "island settings: compact height description")
|
||||
unit: "px"
|
||||
minimum: 24
|
||||
maximum: 72
|
||||
step: 1
|
||||
defaultValue: 38
|
||||
value: SettingsData.dankIslandCompactHeight
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandCompactHeight", value)
|
||||
value: root.setting("islandCompactThickness")
|
||||
onSliderValueChanged: value => root.apply("islandCompactThickness", value)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandOuterGap"
|
||||
settingKey: "islandOuterGap"
|
||||
tags: ["island", "placement", "gap", "top", "margin"]
|
||||
text: I18n.tr("Outer Gaps", "island settings: gap between screen edge and island")
|
||||
description: I18n.tr("Distance between display edge and island", "island settings: outer gap description")
|
||||
@@ -168,22 +196,22 @@ Item {
|
||||
maximum: 48
|
||||
step: 1
|
||||
defaultValue: 4
|
||||
value: SettingsData.dankIslandOuterGap
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandOuterGap", value)
|
||||
value: root.setting("islandOuterGap")
|
||||
onSliderValueChanged: value => root.apply("islandOuterGap", value)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandHorizontalOffset"
|
||||
tags: ["island", "placement", "horizontal", "offset", "center"]
|
||||
text: I18n.tr("Horizontal Offset", "island settings: horizontal offset slider")
|
||||
description: I18n.tr("Move island and satellites left or right from display center", "island settings: horizontal offset description")
|
||||
settingKey: "islandAlongOffset"
|
||||
tags: ["island", "placement", "horizontal", "vertical", "offset", "center"]
|
||||
text: !root.isVertical ? I18n.tr("Horizontal Offset", "island settings: horizontal offset slider") : I18n.tr("Vertical Offset", "island settings: vertical offset slider")
|
||||
description: !root.isVertical ? I18n.tr("Move island and satellites left or right from display center", "island settings: horizontal offset description") : I18n.tr("Move island and satellites up or down from display center", "island settings: vertical offset description")
|
||||
unit: "px"
|
||||
minimum: -600
|
||||
maximum: 600
|
||||
step: 1
|
||||
defaultValue: 0
|
||||
value: SettingsData.dankIslandHorizontalOffset
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandHorizontalOffset", value)
|
||||
value: root.setting("islandAlongOffset")
|
||||
onSliderValueChanged: value => root.apply("islandAlongOffset", value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,8 +219,8 @@ Item {
|
||||
width: parent.width
|
||||
iconName: "home"
|
||||
title: I18n.tr("Home Compact", "island settings: home face card title")
|
||||
settingKey: "dankIslandActivities"
|
||||
visible: SettingsData.dankIslandEnabled
|
||||
settingKey: "islandActivities"
|
||||
visible: root.islandEnabled
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
@@ -204,41 +232,55 @@ Item {
|
||||
|
||||
IslandHomeLayoutEditor {
|
||||
width: parent.width
|
||||
barId: root.selectedIslandId
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
settingKey: "dankIslandHomeVolumeDisplay"
|
||||
tags: ["island", "home", "compact", "volume", "icon", "percentage"]
|
||||
text: I18n.tr("Volume Style", "island settings: volume display mode row")
|
||||
visible: SettingsData.islandHomeGroupEnabled("volume")
|
||||
model: [I18n.tr("Icon", "island settings: level shown as icon only"), I18n.tr("Percentage", "island settings: level shown as percentage only"), I18n.tr("Both", "island settings: level shown as icon and percentage")]
|
||||
currentIndex: root.valueIndex(root.systemLevelDisplayValues, SettingsData.dankIslandHomeVolumeDisplay, "both")
|
||||
settingKey: "islandHomeClockDisplay"
|
||||
tags: ["island", "home", "compact", "clock", "time", "date"]
|
||||
text: I18n.tr("Clock Style", "island settings: clock display mode row")
|
||||
description: I18n.tr("Current time and date pill", "island settings: clock display mode description")
|
||||
model: [I18n.tr("Time", "island settings: clock shows time only"), I18n.tr("Date", "island settings: clock shows date only"), I18n.tr("Both", "island settings: clock shows time and date")]
|
||||
currentIndex: root.valueIndex(root.clockDisplayValues, root.setting("islandHomeClockDisplay"), "both")
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected)
|
||||
SettingsData.set("dankIslandHomeVolumeDisplay", root.systemLevelDisplayValues[index] ?? "both");
|
||||
root.apply("islandHomeClockDisplay", root.clockDisplayValues[index] ?? "both");
|
||||
}
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
settingKey: "dankIslandHomeBrightnessDisplay"
|
||||
tags: ["island", "home", "compact", "brightness", "icon", "percentage"]
|
||||
text: I18n.tr("Brightness Style", "island settings: brightness display mode row")
|
||||
visible: SettingsData.islandHomeGroupEnabled("brightness")
|
||||
settingKey: "islandHomeVolumeDisplay"
|
||||
tags: ["island", "home", "compact", "volume", "icon", "percentage"]
|
||||
text: I18n.tr("Volume Style", "island settings: volume display mode row")
|
||||
visible: SettingsData.islandHomeGroupEnabled(root.config, "volume")
|
||||
model: [I18n.tr("Icon", "island settings: level shown as icon only"), I18n.tr("Percentage", "island settings: level shown as percentage only"), I18n.tr("Both", "island settings: level shown as icon and percentage")]
|
||||
currentIndex: root.valueIndex(root.systemLevelDisplayValues, SettingsData.dankIslandHomeBrightnessDisplay, "both")
|
||||
currentIndex: root.valueIndex(root.systemLevelDisplayValues, root.setting("islandHomeVolumeDisplay"), "both")
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected)
|
||||
SettingsData.set("dankIslandHomeBrightnessDisplay", root.systemLevelDisplayValues[index] ?? "both");
|
||||
root.apply("islandHomeVolumeDisplay", root.systemLevelDisplayValues[index] ?? "both");
|
||||
}
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
settingKey: "islandHomeBrightnessDisplay"
|
||||
tags: ["island", "home", "compact", "brightness", "icon", "percentage"]
|
||||
text: I18n.tr("Brightness Style", "island settings: brightness display mode row")
|
||||
visible: SettingsData.islandHomeGroupEnabled(root.config, "brightness")
|
||||
model: [I18n.tr("Icon", "island settings: level shown as icon only"), I18n.tr("Percentage", "island settings: level shown as percentage only"), I18n.tr("Both", "island settings: level shown as icon and percentage")]
|
||||
currentIndex: root.valueIndex(root.systemLevelDisplayValues, root.setting("islandHomeBrightnessDisplay"), "both")
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected)
|
||||
root.apply("islandHomeBrightnessDisplay", root.systemLevelDisplayValues[index] ?? "both");
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankIslandHomeCompactTight"
|
||||
settingKey: "islandHomeCompactTight"
|
||||
tags: ["island", "home", "compact", "narrow", "width", "height", "clock"]
|
||||
text: I18n.tr("Compact Clock Pill", "island settings: tighter home pill toggle")
|
||||
text: I18n.tr("Compact Pill", "island settings: tighter home pill toggle")
|
||||
description: I18n.tr("Compact home clock pill in both width and height", "island settings: tight pill description")
|
||||
checked: SettingsData.dankIslandHomeCompactTight
|
||||
onToggled: checked => SettingsData.set("dankIslandHomeCompactTight", checked)
|
||||
checked: root.setting("islandHomeCompactTight")
|
||||
onToggled: checked => root.apply("islandHomeCompactTight", checked)
|
||||
}
|
||||
|
||||
Flow {
|
||||
@@ -273,23 +315,23 @@ Item {
|
||||
width: parent.width
|
||||
iconName: "palette"
|
||||
title: I18n.tr("Appearance", "island settings: appearance card title")
|
||||
settingKey: "dankIslandAppearance"
|
||||
visible: SettingsData.dankIslandEnabled
|
||||
settingKey: "islandAppearance"
|
||||
visible: root.islandEnabled
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
settingKey: "dankIslandPalette"
|
||||
settingKey: "islandPalette"
|
||||
tags: ["island", "appearance", "palette", "surface", "bright", "dim"]
|
||||
text: I18n.tr("Palette", "island settings: surface tone choice")
|
||||
model: [I18n.tr("Default", "island settings: default surface tone"), I18n.tr("Bright", "island settings: bright surface tone"), I18n.tr("Dim", "island settings: dim surface tone")]
|
||||
currentIndex: root.valueIndex(root.paletteValues, SettingsData.dankIslandPalette, "default")
|
||||
currentIndex: root.valueIndex(root.paletteValues, root.setting("islandPalette"), "default")
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected)
|
||||
SettingsData.set("dankIslandPalette", root.paletteValues[index] ?? "default");
|
||||
root.apply("islandPalette", root.paletteValues[index] ?? "default");
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandTransparency"
|
||||
settingKey: "islandTransparency"
|
||||
tags: ["island", "appearance", "surface", "opacity", "transparency", "blur"]
|
||||
text: I18n.tr("Opacity", "island settings: island surface opacity slider")
|
||||
unit: "%"
|
||||
@@ -297,12 +339,12 @@ Item {
|
||||
maximum: 100
|
||||
step: 1
|
||||
defaultValue: 100
|
||||
value: Math.round(SettingsData.dankIslandTransparency * 100)
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandTransparency", value / 100)
|
||||
value: Math.round(root.setting("islandTransparency") * 100)
|
||||
onSliderValueChanged: value => root.apply("islandTransparency", value / 100)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandCornerRadius"
|
||||
settingKey: "islandCornerRadius"
|
||||
tags: ["island", "appearance", "corner", "radius", "rounding", "pill", "expanded"]
|
||||
text: I18n.tr("Corner Radius", "island settings: island corner radius slider")
|
||||
unit: "px"
|
||||
@@ -310,39 +352,39 @@ Item {
|
||||
maximum: 64
|
||||
step: 1
|
||||
defaultValue: 34
|
||||
value: SettingsData.dankIslandCornerRadius
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandCornerRadius", value)
|
||||
value: root.setting("islandCornerRadius")
|
||||
onSliderValueChanged: value => root.apply("islandCornerRadius", value)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankIslandHighContrast"
|
||||
settingKey: "islandHighContrast"
|
||||
tags: ["island", "appearance", "contrast", "accessibility", "outline"]
|
||||
text: I18n.tr("High Contrast", "island settings: high contrast toggle")
|
||||
description: I18n.tr("Draw an outline and use the highest-contrast surface tone", "island settings: high contrast description")
|
||||
checked: SettingsData.dankIslandHighContrast
|
||||
onToggled: checked => SettingsData.set("dankIslandHighContrast", checked)
|
||||
checked: root.setting("islandHighContrast")
|
||||
onToggled: checked => root.apply("islandHighContrast", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankIslandMediaClockVisible"
|
||||
settingKey: "islandMediaClockVisible"
|
||||
tags: ["island", "media", "clock", "compact", "time"]
|
||||
text: I18n.tr("Keep Clock with Media", "island settings: clock in media face toggle")
|
||||
description: I18n.tr("Show a clickable clock beside compact media details", "island settings: media clock description")
|
||||
checked: SettingsData.dankIslandMediaClockVisible
|
||||
onToggled: checked => SettingsData.set("dankIslandMediaClockVisible", checked)
|
||||
checked: root.setting("islandMediaClockVisible")
|
||||
onToggled: checked => root.apply("islandMediaClockVisible", checked)
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
settingKey: "dankIslandBatteryStyle"
|
||||
settingKey: "islandBatteryStyle"
|
||||
tags: ["island", "battery", "gauge", "solid", "outline", "ring", "circle", "appearance"]
|
||||
text: I18n.tr("Battery Style", "island settings: battery meter style row")
|
||||
description: I18n.tr("Solid or outlined material meter, or a circular gauge", "island settings: battery style description")
|
||||
visible: BatteryService.batteryAvailable
|
||||
model: [I18n.tr("Solid", "island settings: filled battery meter style"), I18n.tr("Outline", "island settings: outlined battery meter style"), I18n.tr("Circle", "island settings: circular battery meter style")]
|
||||
currentIndex: root.valueIndex(root.batteryStyleValues, SettingsData.dankIslandBatteryStyle, "solid")
|
||||
currentIndex: root.valueIndex(root.batteryStyleValues, root.setting("islandBatteryStyle"), "solid")
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected)
|
||||
SettingsData.set("dankIslandBatteryStyle", root.batteryStyleValues[index] ?? "solid");
|
||||
root.apply("islandBatteryStyle", root.batteryStyleValues[index] ?? "solid");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -351,26 +393,26 @@ Item {
|
||||
width: parent.width
|
||||
iconName: "notifications"
|
||||
title: I18n.tr("Notifications", "island settings: notifications card title")
|
||||
settingKey: "dankIslandNotifications"
|
||||
visible: SettingsData.dankIslandEnabled
|
||||
settingKey: "islandNotifications"
|
||||
visible: root.islandEnabled
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankIslandNotificationExpand"
|
||||
settingKey: "islandNotificationExpand"
|
||||
tags: ["island", "notifications", "expand", "arrival", "size"]
|
||||
text: I18n.tr("Expand Notifications", "island settings: expanded notification toggle")
|
||||
description: I18n.tr("Expand notifications by default instead of click or hover", "island settings: expanded notification description")
|
||||
checked: SettingsData.dankIslandNotificationExpand
|
||||
onToggled: checked => SettingsData.set("dankIslandNotificationExpand", checked)
|
||||
checked: root.setting("islandNotificationExpand")
|
||||
onToggled: checked => root.apply("islandNotificationExpand", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankIslandNotificationBadgeClearOnOpen"
|
||||
settingKey: "islandNotificationBadgeClearOnOpen"
|
||||
tags: ["island", "home", "notifications", "badge", "unread", "clear", "dismiss", "open"]
|
||||
text: I18n.tr("Clear Badge on Open", "island settings: clear the notification badge when the center opens")
|
||||
description: I18n.tr("Clears the badge on open but keeps notifications active", "island settings: clear badge on open description")
|
||||
checked: SettingsData.dankIslandNotificationBadgeClearOnOpen
|
||||
enabled: SettingsData.islandHomeGroupEnabled("notifications")
|
||||
onToggled: checked => SettingsData.set("dankIslandNotificationBadgeClearOnOpen", checked)
|
||||
checked: root.setting("islandNotificationBadgeClearOnOpen")
|
||||
enabled: SettingsData.islandHomeGroupEnabled(root.config, "notifications")
|
||||
onToggled: checked => root.apply("islandNotificationBadgeClearOnOpen", checked)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,42 +420,42 @@ Item {
|
||||
width: parent.width
|
||||
iconName: "widgets"
|
||||
title: I18n.tr("Satellite Widgets", "island settings: satellite widgets card title")
|
||||
settingKey: "dankIslandSatellites"
|
||||
settingKey: "islandSatellites"
|
||||
collapsible: true
|
||||
expanded: true
|
||||
visible: SettingsData.dankIslandEnabled
|
||||
visible: root.islandEnabled
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankIslandSatellitesEnabled"
|
||||
settingKey: "islandSatellitesEnabled"
|
||||
tags: ["island", "satellite", "widgets", "left", "right"]
|
||||
text: I18n.tr("Show Satellite Widgets", "island settings: satellite widgets toggle")
|
||||
description: I18n.tr("Place independent widgets to left and right of island", "island settings: satellite widgets description")
|
||||
checked: SettingsData.dankIslandSatellitesEnabled
|
||||
onToggled: checked => SettingsData.set("dankIslandSatellitesEnabled", checked)
|
||||
checked: root.setting("islandSatellitesEnabled")
|
||||
onToggled: checked => root.apply("islandSatellitesEnabled", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankIslandSatelliteBackground"
|
||||
settingKey: "islandSatelliteBackground"
|
||||
tags: ["island", "satellite", "widgets", "background", "chrome"]
|
||||
text: I18n.tr("Background", "island settings: satellite background toggle")
|
||||
description: I18n.tr("Draw an island-styled background behind satellite widgets", "island settings: satellite background description")
|
||||
checked: SettingsData.dankIslandSatelliteBackground
|
||||
enabled: SettingsData.dankIslandSatellitesEnabled
|
||||
onToggled: checked => SettingsData.set("dankIslandSatelliteBackground", checked)
|
||||
checked: root.setting("islandSatelliteBackground")
|
||||
enabled: root.setting("islandSatellitesEnabled")
|
||||
onToggled: checked => root.apply("islandSatelliteBackground", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankIslandSatelliteGothCorners"
|
||||
settingKey: "islandSatelliteGothCorners"
|
||||
tags: ["island", "satellite", "goth", "corners", "wing", "sweep"]
|
||||
text: I18n.tr("Goth Corners", "island settings: satellite goth corners toggle")
|
||||
description: I18n.tr("Sweep the background into the screen edges", "island settings: satellite goth corners description")
|
||||
checked: SettingsData.dankIslandSatelliteGothCorners
|
||||
enabled: SettingsData.dankIslandSatellitesEnabled && SettingsData.dankIslandSatelliteBackground
|
||||
onToggled: checked => SettingsData.set("dankIslandSatelliteGothCorners", checked)
|
||||
checked: root.setting("islandSatelliteGothCorners")
|
||||
enabled: root.setting("islandSatellitesEnabled") && root.setting("islandSatelliteBackground")
|
||||
onToggled: checked => root.apply("islandSatelliteGothCorners", checked)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandSatelliteSwoopRadius"
|
||||
settingKey: "islandSatelliteSwoopRadius"
|
||||
tags: ["island", "satellite", "goth", "corners", "radius", "sweep", "size"]
|
||||
text: I18n.tr("Goth Corner Radius", "island settings: satellite goth corner radius slider")
|
||||
unit: "px"
|
||||
@@ -421,13 +463,13 @@ Item {
|
||||
maximum: 64
|
||||
step: 1
|
||||
defaultValue: 24
|
||||
value: SettingsData.dankIslandSatelliteSwoopRadius
|
||||
enabled: SettingsData.dankIslandSatellitesEnabled && SettingsData.dankIslandSatelliteBackground && SettingsData.dankIslandSatelliteGothCorners
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandSatelliteSwoopRadius", value)
|
||||
value: root.setting("islandSatelliteSwoopRadius")
|
||||
enabled: root.setting("islandSatellitesEnabled") && root.setting("islandSatelliteBackground") && root.setting("islandSatelliteGothCorners")
|
||||
onSliderValueChanged: value => root.apply("islandSatelliteSwoopRadius", value)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandSatelliteTransparency"
|
||||
settingKey: "islandSatelliteTransparency"
|
||||
tags: ["island", "satellite", "background", "opacity", "transparency", "blur"]
|
||||
text: I18n.tr("Opacity", "island settings: satellite background opacity slider")
|
||||
unit: "%"
|
||||
@@ -435,27 +477,27 @@ Item {
|
||||
maximum: 100
|
||||
step: 1
|
||||
defaultValue: 100
|
||||
value: Math.round(SettingsData.dankIslandSatelliteTransparency * 100)
|
||||
enabled: SettingsData.dankIslandSatellitesEnabled && SettingsData.dankIslandSatelliteBackground
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandSatelliteTransparency", value / 100)
|
||||
value: Math.round(root.setting("islandSatelliteTransparency") * 100)
|
||||
enabled: root.setting("islandSatellitesEnabled") && root.setting("islandSatelliteBackground")
|
||||
onSliderValueChanged: value => root.apply("islandSatelliteTransparency", value / 100)
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
settingKey: "dankIslandSatellitePosition"
|
||||
settingKey: "islandSatellitePosition"
|
||||
tags: ["island", "satellite", "widgets", "position", "edges", "center"]
|
||||
text: I18n.tr("Position", "island settings: position card title")
|
||||
description: I18n.tr("Keep widgets beside the island or align them like a standalone bar", "island settings: satellite position description")
|
||||
model: [I18n.tr("Near Island", "island settings: satellites hug the island"), I18n.tr("Screen Edges", "island settings: satellites sit at screen edges")]
|
||||
currentIndex: root.valueIndex(root.satellitePositionValues, SettingsData.dankIslandSatellitePosition, "island")
|
||||
enabled: SettingsData.dankIslandSatellitesEnabled
|
||||
currentIndex: root.valueIndex(root.satellitePositionValues, root.setting("islandSatellitePosition"), "island")
|
||||
enabled: root.setting("islandSatellitesEnabled")
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected)
|
||||
SettingsData.set("dankIslandSatellitePosition", root.satellitePositionValues[index] ?? "island");
|
||||
root.apply("islandSatellitePosition", root.satellitePositionValues[index] ?? "island");
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandSatelliteGap"
|
||||
settingKey: "islandSatelliteGap"
|
||||
tags: ["island", "satellite", "widgets", "gap", "spacing"]
|
||||
text: I18n.tr("Island Gap", "island settings: satellite to island gap slider")
|
||||
unit: "px"
|
||||
@@ -463,9 +505,9 @@ Item {
|
||||
maximum: 48
|
||||
step: 1
|
||||
defaultValue: 12
|
||||
value: SettingsData.dankIslandSatelliteGap
|
||||
enabled: SettingsData.dankIslandSatellitesEnabled && SettingsData.dankIslandSatellitePosition !== "edges"
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandSatelliteGap", value)
|
||||
value: root.setting("islandSatelliteGap")
|
||||
enabled: root.setting("islandSatellitesEnabled") && root.setting("islandSatellitePosition") !== "edges"
|
||||
onSliderValueChanged: value => root.apply("islandSatelliteGap", value)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -474,7 +516,7 @@ Item {
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
visible: SettingsData.dankIslandSatellitesEnabled
|
||||
visible: root.setting("islandSatellitesEnabled")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,19 +524,19 @@ Item {
|
||||
width: parent.width
|
||||
iconName: "touch_app"
|
||||
title: I18n.tr("Behavior", "island settings: behavior card title")
|
||||
settingKey: "dankIslandInteraction"
|
||||
visible: SettingsData.dankIslandEnabled
|
||||
settingKey: "islandInteraction"
|
||||
visible: root.islandEnabled
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
settingKey: "dankIslandInteractionMode"
|
||||
settingKey: "islandInteractionMode"
|
||||
tags: ["island", "interaction", "click", "hybrid", "expand"]
|
||||
text: I18n.tr("Expansion Mode", "island settings: click or hover expansion row")
|
||||
description: I18n.tr("Click expands only on an intentional press. Hybrid peeks the current compact face on hover", "island settings: expansion mode description")
|
||||
model: [I18n.tr("Click", "island settings: click expansion mode"), I18n.tr("Hybrid", "island settings: hover plus click expansion mode")]
|
||||
currentIndex: root.valueIndex(root.interactionModeValues, SettingsData.dankIslandInteractionMode, "hybrid")
|
||||
currentIndex: root.valueIndex(root.interactionModeValues, root.setting("islandInteractionMode"), "hybrid")
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected)
|
||||
SettingsData.set("dankIslandInteractionMode", root.interactionModeValues[index] ?? "hybrid");
|
||||
root.apply("islandInteractionMode", root.interactionModeValues[index] ?? "hybrid");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,11 +546,11 @@ Item {
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
visible: SettingsData.dankIslandInteractionMode !== "click"
|
||||
visible: root.setting("islandInteractionMode") !== "click"
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandHoverOpenDelay"
|
||||
settingKey: "islandHoverOpenDelay"
|
||||
tags: ["island", "interaction", "hover", "open", "delay"]
|
||||
text: I18n.tr("Open Delay", "island settings: hover open delay slider")
|
||||
unit: "ms"
|
||||
@@ -516,13 +558,13 @@ Item {
|
||||
maximum: 1000
|
||||
step: 10
|
||||
defaultValue: 150
|
||||
value: SettingsData.dankIslandHoverOpenDelay
|
||||
enabled: SettingsData.dankIslandInteractionMode !== "click"
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandHoverOpenDelay", value)
|
||||
value: root.setting("islandHoverOpenDelay")
|
||||
enabled: root.setting("islandInteractionMode") !== "click"
|
||||
onSliderValueChanged: value => root.apply("islandHoverOpenDelay", value)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandHoverCloseDelay"
|
||||
settingKey: "islandHoverCloseDelay"
|
||||
tags: ["island", "interaction", "hover", "close", "delay"]
|
||||
text: I18n.tr("Hide Delay", "island settings: hover hide delay slider")
|
||||
unit: "ms"
|
||||
@@ -530,9 +572,9 @@ Item {
|
||||
maximum: 1000
|
||||
step: 10
|
||||
defaultValue: 150
|
||||
value: SettingsData.dankIslandHoverCloseDelay
|
||||
enabled: SettingsData.dankIslandInteractionMode !== "click"
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandHoverCloseDelay", value)
|
||||
value: root.setting("islandHoverCloseDelay")
|
||||
enabled: root.setting("islandInteractionMode") !== "click"
|
||||
onSliderValueChanged: value => root.apply("islandHoverCloseDelay", value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,49 +582,49 @@ Item {
|
||||
width: parent.width
|
||||
iconName: "animation"
|
||||
title: I18n.tr("Motion & Accessibility", "island settings: motion card title")
|
||||
settingKey: "dankIslandMotion"
|
||||
settingKey: "islandMotion"
|
||||
collapsible: true
|
||||
visible: SettingsData.dankIslandEnabled
|
||||
visible: root.islandEnabled
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankIslandReducedMotion"
|
||||
settingKey: "islandReducedMotion"
|
||||
tags: ["island", "motion", "animation", "reduce", "accessibility", "spring"]
|
||||
text: I18n.tr("Reduce Motion", "island settings: reduce motion toggle")
|
||||
description: I18n.tr("Apply island geometry changes immediately without spring overshoot", "island settings: reduce motion description")
|
||||
checked: SettingsData.dankIslandReducedMotion
|
||||
onToggled: checked => SettingsData.set("dankIslandReducedMotion", checked)
|
||||
checked: root.setting("islandReducedMotion")
|
||||
onToggled: checked => root.apply("islandReducedMotion", checked)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandSpringStiffness"
|
||||
settingKey: "islandSpringStiffness"
|
||||
tags: ["island", "motion", "spring", "stiffness", "animation"]
|
||||
text: I18n.tr("Spring Stiffness", "island settings: spring stiffness slider")
|
||||
description: I18n.tr("Higher values pull island toward its target more strongly", "island settings: stiffness description")
|
||||
minimum: 100
|
||||
maximum: 1200
|
||||
step: 10
|
||||
value: Math.round(SettingsData.dankIslandSpringStiffness)
|
||||
value: Math.round(root.setting("islandSpringStiffness"))
|
||||
defaultValue: 560
|
||||
enabled: !SettingsData.dankIslandReducedMotion
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandSpringStiffness", value)
|
||||
enabled: !root.setting("islandReducedMotion")
|
||||
onSliderValueChanged: value => root.apply("islandSpringStiffness", value)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandSpringDamping"
|
||||
settingKey: "islandSpringDamping"
|
||||
tags: ["island", "motion", "spring", "damping", "bounce", "animation"]
|
||||
text: I18n.tr("Spring Damping", "island settings: spring damping slider")
|
||||
description: I18n.tr("Higher values settle island with less bounce", "island settings: damping description")
|
||||
minimum: 10
|
||||
maximum: 100
|
||||
step: 1
|
||||
value: Math.round(SettingsData.dankIslandSpringDamping)
|
||||
value: Math.round(root.setting("islandSpringDamping"))
|
||||
defaultValue: 37
|
||||
enabled: !SettingsData.dankIslandReducedMotion
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandSpringDamping", value)
|
||||
enabled: !root.setting("islandReducedMotion")
|
||||
onSliderValueChanged: value => root.apply("islandSpringDamping", value)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankIslandSpringMass"
|
||||
settingKey: "islandSpringMass"
|
||||
tags: ["island", "motion", "spring", "mass", "inertia", "animation"]
|
||||
text: I18n.tr("Spring Mass", "island settings: spring mass slider")
|
||||
description: I18n.tr("Higher percentages give island more inertia", "island settings: mass description")
|
||||
@@ -590,10 +632,10 @@ Item {
|
||||
maximum: 300
|
||||
step: 5
|
||||
unit: "%"
|
||||
value: Math.round(SettingsData.dankIslandSpringMass * 100)
|
||||
value: Math.round(root.setting("islandSpringMass") * 100)
|
||||
defaultValue: 100
|
||||
enabled: !SettingsData.dankIslandReducedMotion
|
||||
onSliderValueChanged: value => SettingsData.set("dankIslandSpringMass", value / 100)
|
||||
enabled: !root.setting("islandReducedMotion")
|
||||
onSliderValueChanged: value => root.apply("islandSpringMass", value / 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,197 @@ Singleton {
|
||||
property bool hasPendingChanges: Object.keys(pendingChanges).length > 0 || Object.keys(pendingNiriChanges).length > 0 || Object.keys(pendingHyprlandChanges).length > 0 || formatChanged
|
||||
|
||||
property bool validatingConfig: false
|
||||
property var _cancelOutputWrite: null
|
||||
property var aqueousPreview: null
|
||||
|
||||
function outputFingerprint(outputs) {
|
||||
return JSON.stringify(outputs.map(o => ({
|
||||
name: o.name,
|
||||
id: o.id,
|
||||
enabled: o.enabled,
|
||||
x: o.x,
|
||||
y: o.y,
|
||||
scale: o.scale,
|
||||
transform: o.transform,
|
||||
mode: o.currentMode ? [o.currentMode.width, o.currentMode.height, o.currentMode.refresh] : null,
|
||||
adaptiveSync: o.adaptiveSync
|
||||
})).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
}
|
||||
|
||||
function outputHeads(outputs) {
|
||||
return outputs.map(o => ({
|
||||
name: o.name,
|
||||
enabled: o.enabled,
|
||||
modeId: o.currentMode?.id,
|
||||
position: {
|
||||
x: o.x,
|
||||
y: o.y
|
||||
},
|
||||
scale: o.scale,
|
||||
transform: o.transform,
|
||||
adaptiveSync: o.adaptiveSync
|
||||
}));
|
||||
}
|
||||
|
||||
function outputHeadsMatch(candidate, actual, original) {
|
||||
if (candidate.length !== actual.length || original.length !== actual.length || original.some(o => !actual.some(a => a.id === o.id && a.name === o.name)))
|
||||
return false;
|
||||
return candidate.every(h => {
|
||||
const output = actual.find(o => o.name === h.name);
|
||||
if (!output || output.enabled !== h.enabled)
|
||||
return false;
|
||||
if (!h.enabled)
|
||||
return true;
|
||||
const mode = h.customMode || original.find(o => o.name === h.name)?.modes?.find(m => m.id === h.modeId);
|
||||
return output.x === h.position.x && output.y === h.position.y && Math.abs(output.scale - h.scale) < 0.0001 && output.transform === h.transform && (h.adaptiveSync === undefined || output.adaptiveSync === h.adaptiveSync) && !!mode && output.currentMode?.width === mode.width && output.currentMode?.height === mode.height && output.currentMode?.refresh === mode.refresh;
|
||||
});
|
||||
}
|
||||
|
||||
function freshAqueousOutputs(callback) {
|
||||
DMSService.sendRequest("wlroutput.getState", null, response => {
|
||||
callback(response.result?.outputs || null, response.error || "");
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function aqueousDisplayError(message) {
|
||||
validatingConfig = false;
|
||||
validationError = AqueousService.errorMessage(message);
|
||||
ToastService.showError(I18n.tr("Error"), validationError, message);
|
||||
}
|
||||
|
||||
function discardAqueousPreview() {
|
||||
if (validatingConfig)
|
||||
return;
|
||||
aqueousPreview = null;
|
||||
restoreDisplayNameMode();
|
||||
validationError = "";
|
||||
clearPendingChanges();
|
||||
WlrOutputService.requestState();
|
||||
}
|
||||
|
||||
function previewAqueousOutputs(descriptions) {
|
||||
if (validatingConfig)
|
||||
return;
|
||||
if (!AqueousService.available) {
|
||||
aqueousDisplayError("unavailable: compositor state");
|
||||
return;
|
||||
}
|
||||
if (aqueousPreview) {
|
||||
observeAqueousPreview(aqueousPreview, descriptions, "");
|
||||
return;
|
||||
}
|
||||
const session = AqueousService.session;
|
||||
validatingConfig = true;
|
||||
AqueousConfigService.load((snapshot, error) => {
|
||||
if (!snapshot) {
|
||||
aqueousDisplayError(error);
|
||||
return;
|
||||
}
|
||||
freshAqueousOutputs((original, error) => {
|
||||
if (AqueousService.session !== session) {
|
||||
aqueousDisplayError("conflict: compositor session changed");
|
||||
return;
|
||||
}
|
||||
if (!original) {
|
||||
aqueousDisplayError(error);
|
||||
return;
|
||||
}
|
||||
const candidate = WlrOutputService.outputsConfigHeads(buildOutputsWithPendingChanges(), outputs);
|
||||
WlrOutputService.testConfiguration(candidate, (success, message) => {
|
||||
if (AqueousService.session !== session) {
|
||||
aqueousDisplayError("conflict: compositor session changed");
|
||||
return;
|
||||
}
|
||||
if (!success) {
|
||||
aqueousDisplayError(message);
|
||||
return;
|
||||
}
|
||||
const preview = {
|
||||
snapshot: snapshot,
|
||||
original: original,
|
||||
heads: candidate,
|
||||
fingerprint: null,
|
||||
session: session
|
||||
};
|
||||
aqueousPreview = preview;
|
||||
WlrOutputService.applyConfiguration(candidate, (success, message) => {
|
||||
observeAqueousPreview(preview, descriptions, success ? "" : message);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function observeAqueousPreview(preview, descriptions, applyError) {
|
||||
validatingConfig = true;
|
||||
freshAqueousOutputs((actual, error) => {
|
||||
if (aqueousPreview !== preview)
|
||||
return;
|
||||
if (!actual || AqueousService.session !== preview.session || !outputHeadsMatch(preview.heads, actual, preview.original)) {
|
||||
if (actual && outputFingerprint(actual) === outputFingerprint(preview.original))
|
||||
aqueousPreview = null;
|
||||
aqueousDisplayError(error || applyError || "conflict: display preview changed externally");
|
||||
return;
|
||||
}
|
||||
preview.fingerprint = outputFingerprint(actual);
|
||||
validatingConfig = false;
|
||||
validationError = applyError ? AqueousService.errorMessage(applyError) : "";
|
||||
changesApplied(descriptions);
|
||||
});
|
||||
}
|
||||
|
||||
function finishAqueousPreview(keep) {
|
||||
if (validatingConfig || !aqueousPreview)
|
||||
return;
|
||||
const preview = aqueousPreview;
|
||||
validatingConfig = true;
|
||||
freshAqueousOutputs((actual, error) => {
|
||||
if (!actual || AqueousService.session !== preview.session || outputFingerprint(actual) !== preview.fingerprint) {
|
||||
aqueousDisplayError(error || "conflict: output configuration changed after preview");
|
||||
return;
|
||||
}
|
||||
if (keep) {
|
||||
let draft;
|
||||
try {
|
||||
draft = AqueousConfigService.buildOutputsConfig(preview.snapshot, actual, preview.original);
|
||||
} catch (e) {
|
||||
aqueousDisplayError(String(e));
|
||||
return;
|
||||
}
|
||||
AqueousConfigService.apply(draft, (snapshot, message) => {
|
||||
if (!snapshot) {
|
||||
aqueousDisplayError(message);
|
||||
return;
|
||||
}
|
||||
aqueousPreview = null;
|
||||
if (formatChanged)
|
||||
SettingsData.saveSettings();
|
||||
clearPendingChanges();
|
||||
freshAqueousOutputs((live, error) => {
|
||||
validatingConfig = false;
|
||||
if (!live || AqueousService.session !== preview.session || outputFingerprint(live) !== preview.fingerprint) {
|
||||
aqueousDisplayError("conflict: configuration saved but live display state changed");
|
||||
return;
|
||||
}
|
||||
changesConfirmed();
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
WlrOutputService.applyConfiguration(outputHeads(preview.original), (success, message) => {
|
||||
if (!success) {
|
||||
aqueousDisplayError(message);
|
||||
return;
|
||||
}
|
||||
validatingConfig = false;
|
||||
aqueousPreview = null;
|
||||
restoreDisplayNameMode();
|
||||
clearPendingChanges();
|
||||
WlrOutputService.requestState();
|
||||
changesReverted();
|
||||
});
|
||||
});
|
||||
}
|
||||
property string validationError: ""
|
||||
|
||||
property var currentOutputSet: []
|
||||
@@ -530,20 +721,23 @@ Singleton {
|
||||
const outputKeys = Object.keys(configEntry.outputs || {});
|
||||
if (outputKeys.length === 0)
|
||||
return false;
|
||||
const hasEnabledReal = outputKeys.some(k => {
|
||||
const o = configEntry.outputs[k];
|
||||
return !o.disabled && o.make && o.model;
|
||||
});
|
||||
const hasEnabledReal = outputKeys.some(k => !configEntry.outputs[k].disabled && profileOutputIsReal(k));
|
||||
if (hasEnabledReal)
|
||||
return false;
|
||||
for (const k of outputKeys) {
|
||||
const o = configEntry.outputs[k];
|
||||
if (o.make && o.model) {
|
||||
delete o.disabled;
|
||||
return true;
|
||||
}
|
||||
const firstReal = outputKeys.find(k => profileOutputIsReal(k));
|
||||
if (!firstReal)
|
||||
return false;
|
||||
delete configEntry.outputs[firstReal].disabled;
|
||||
return true;
|
||||
}
|
||||
|
||||
function profileOutputIsReal(outputId) {
|
||||
for (const name in outputs) {
|
||||
if (!profileKeyMatchesOutput(outputId, outputs[name], name))
|
||||
continue;
|
||||
return !!(outputs[name].make && outputs[name].model);
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyConfigEntry(configEntry, configId, profileName, isManual) {
|
||||
@@ -556,26 +750,6 @@ Singleton {
|
||||
showHyprlandReadOnlyWarning();
|
||||
return;
|
||||
}
|
||||
ensureEnabledOutput(configEntry);
|
||||
const outputKeys = Object.keys(configEntry.outputs || {});
|
||||
const hasRealOutput = outputKeys.some(k => {
|
||||
const o = configEntry.outputs[k];
|
||||
return o.make && o.model;
|
||||
});
|
||||
if (!hasRealOutput) {
|
||||
backendWriteOutputsConfig({}, null, success => {
|
||||
if (success) {
|
||||
SettingsData.setActiveDisplayProfile(CompositorService.compositor, configId);
|
||||
WlrOutputService.requestState();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Capture the entry being applied so disabled-output settings fields can read
|
||||
// scale/position/transform back even when wlr reports no logical viewport.
|
||||
root.lastAppliedEntry = JSON.parse(JSON.stringify(configEntry));
|
||||
const outputsData = generateOutputsDataFromConfig(configEntry);
|
||||
|
||||
const onWriteFailed = () => {
|
||||
if (isManual) {
|
||||
profilesLoading = false;
|
||||
@@ -583,6 +757,17 @@ Singleton {
|
||||
profileError(I18n.tr("Failed to apply profile"));
|
||||
}
|
||||
};
|
||||
|
||||
ensureEnabledOutput(configEntry);
|
||||
if (!Object.keys(configEntry.outputs || {}).some(k => profileOutputIsReal(k))) {
|
||||
log.warn("Profile", configId, "has no real outputs, not applying");
|
||||
onWriteFailed();
|
||||
return;
|
||||
}
|
||||
// Capture the entry being applied so disabled-output settings fields can read
|
||||
// scale/position/transform back even when wlr reports no logical viewport.
|
||||
root.lastAppliedEntry = JSON.parse(JSON.stringify(configEntry));
|
||||
const outputsData = generateOutputsDataFromConfig(configEntry);
|
||||
const onWriteSuccess = () => {
|
||||
SessionData.setActiveDisplayProfile(CompositorService.compositor, configId);
|
||||
publishActiveProfileModes();
|
||||
@@ -806,19 +991,17 @@ Singleton {
|
||||
|
||||
const outputConfigs = buildCurrentOutputConfigs();
|
||||
const id = generateAutoProfileId(currentOutputSet);
|
||||
const entry = {
|
||||
"id": id,
|
||||
"name": "",
|
||||
"outputs": outputConfigs
|
||||
};
|
||||
ensureEnabledOutput(entry);
|
||||
const existingIdx = data.configurations.findIndex(c => c.id === id);
|
||||
if (existingIdx >= 0)
|
||||
data.configurations[existingIdx] = {
|
||||
"id": id,
|
||||
"name": "",
|
||||
"outputs": outputConfigs
|
||||
};
|
||||
data.configurations[existingIdx] = entry;
|
||||
else
|
||||
data.configurations.push({
|
||||
"id": id,
|
||||
"name": "",
|
||||
"outputs": outputConfigs
|
||||
});
|
||||
data.configurations.push(entry);
|
||||
writeMonitorsJson(data, success => {
|
||||
if (!success)
|
||||
return;
|
||||
@@ -939,7 +1122,7 @@ Singleton {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
target: SessionData
|
||||
function onActiveDisplayProfileChanged() {
|
||||
root.publishActiveProfileModes();
|
||||
}
|
||||
@@ -1715,9 +1898,21 @@ Singleton {
|
||||
MangoService.generateOutputsConfig(outputsData, finish);
|
||||
break;
|
||||
default:
|
||||
WlrOutputService.applyOutputsConfig(outputsData, outputs);
|
||||
finish(true);
|
||||
break;
|
||||
{
|
||||
if (_cancelOutputWrite)
|
||||
_cancelOutputWrite();
|
||||
let completed = false;
|
||||
const complete = success => {
|
||||
if (completed)
|
||||
return;
|
||||
completed = true;
|
||||
root._cancelOutputWrite = null;
|
||||
finish(success);
|
||||
};
|
||||
_cancelOutputWrite = () => complete(false);
|
||||
WlrOutputService.applyOutputsConfig(outputsData, outputs, complete);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -2153,11 +2348,15 @@ Singleton {
|
||||
originalDisplayNameMode = "";
|
||||
}
|
||||
|
||||
function restoreDisplayNameMode() {
|
||||
if (originalDisplayNameMode === "")
|
||||
return;
|
||||
SettingsData.displayNameMode = originalDisplayNameMode;
|
||||
SettingsData.saveSettings();
|
||||
}
|
||||
|
||||
function discardChanges() {
|
||||
if (originalDisplayNameMode !== "") {
|
||||
SettingsData.displayNameMode = originalDisplayNameMode;
|
||||
SettingsData.saveSettings();
|
||||
}
|
||||
restoreDisplayNameMode();
|
||||
backendFetchOutputs();
|
||||
clearPendingChanges();
|
||||
}
|
||||
@@ -2229,16 +2428,32 @@ Singleton {
|
||||
return;
|
||||
}
|
||||
|
||||
changesApplied(changeDescriptions);
|
||||
|
||||
if (formatChanged)
|
||||
SettingsData.saveSettings();
|
||||
|
||||
if (CompositorService.isHyprland)
|
||||
commitHyprlandSettingsChanges();
|
||||
if (CompositorService.isAqueous) {
|
||||
previewAqueousOutputs(changeDescriptions);
|
||||
return;
|
||||
}
|
||||
|
||||
const mergedOutputs = buildOutputsWithPendingChanges();
|
||||
backendWriteOutputsConfig(mergedOutputs);
|
||||
if (CompositorService.isHyprland || CompositorService.isMango) {
|
||||
changesApplied(changeDescriptions);
|
||||
if (formatChanged)
|
||||
SettingsData.saveSettings();
|
||||
if (CompositorService.isHyprland)
|
||||
commitHyprlandSettingsChanges();
|
||||
backendWriteOutputsConfig(mergedOutputs);
|
||||
return;
|
||||
}
|
||||
validatingConfig = true;
|
||||
backendWriteOutputsConfig(mergedOutputs, success => {
|
||||
validatingConfig = false;
|
||||
if (!success) {
|
||||
ToastService.showError(I18n.tr("Error"), I18n.tr("Failed to apply profile"));
|
||||
return;
|
||||
}
|
||||
if (formatChanged)
|
||||
SettingsData.saveSettings();
|
||||
changesApplied(changeDescriptions);
|
||||
});
|
||||
}
|
||||
|
||||
function validateAndApplyNiriConfig(changeDescriptions) {
|
||||
@@ -2349,6 +2564,10 @@ Singleton {
|
||||
}
|
||||
|
||||
function confirmChanges(profileId) {
|
||||
if (CompositorService.isAqueous) {
|
||||
finishAqueousPreview(true);
|
||||
return;
|
||||
}
|
||||
const outputConfigs = buildCurrentOutputConfigs();
|
||||
lastAppliedEntry = {
|
||||
outputs: outputConfigs
|
||||
@@ -2380,14 +2599,15 @@ Singleton {
|
||||
}
|
||||
|
||||
function revertChanges() {
|
||||
if (CompositorService.isAqueous && aqueousPreview) {
|
||||
finishAqueousPreview(false);
|
||||
return;
|
||||
}
|
||||
const hadFormatChange = originalDisplayNameMode !== "";
|
||||
const hadNiriChanges = originalNiriSettings !== null;
|
||||
const hadHyprlandChanges = originalHyprlandSettings !== null;
|
||||
|
||||
if (hadFormatChange) {
|
||||
SettingsData.displayNameMode = originalDisplayNameMode;
|
||||
SettingsData.saveSettings();
|
||||
}
|
||||
restoreDisplayNameMode();
|
||||
|
||||
if (hadNiriChanges) {
|
||||
SessionData.niriOutputSettings = JSON.parse(JSON.stringify(originalNiriSettings));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user