1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

Compare commits

...

7 Commits

Author SHA1 Message Date
bbedward
ddda87c5a7 less agress dms-open MimeType declarations 2025-12-05 12:36:04 -05:00
bbedward
6b1bbca620 keybinds: fix alt+shift, kdl parsing, allow arguments 2025-12-05 12:31:15 -05:00
bbedward
b5378e5d3c hypr: add exclusive focus override 2025-12-05 10:37:24 -05:00
bbedward
c69a55df29 flickable: update momentum scrolling logic 2025-12-05 10:14:16 -05:00
bbedward
5faa1a993a launcher: reemove background from list and add a bottom fade 2025-12-05 10:04:19 -05:00
bbedward
e56481f6d7 launcher: add 1px gap between grid delegates 2025-12-05 09:33:04 -05:00
bbedward
f9610d457c dankbar: fix border thickness 2025-12-05 09:29:45 -05:00
31 changed files with 1322 additions and 409 deletions

View File

@@ -6,5 +6,5 @@ Exec=dms open %u
Icon=danklogo Icon=danklogo
Terminal=false Terminal=false
NoDisplay=true NoDisplay=true
MimeType=x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/file;text/html; MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;application/xhtml+xml;
Categories=Utility; Categories=Utility;

View File

@@ -333,35 +333,6 @@ func (n *NiriProvider) isRecentWindowsAction(action string) bool {
} }
} }
func (n *NiriProvider) parseSpawnArgs(s string) []string {
var args []string
var current strings.Builder
var inQuote, escaped bool
for _, r := range s {
switch {
case escaped:
current.WriteRune(r)
escaped = false
case r == '\\':
escaped = true
case r == '"':
inQuote = !inQuote
case r == ' ' && !inQuote:
if current.Len() > 0 {
args = append(args, current.String())
current.Reset()
}
default:
current.WriteRune(r)
}
}
if current.Len() > 0 {
args = append(args, current.String())
}
return args
}
func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node { func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node {
node := document.NewNode() node := document.NewNode()
node.SetName(bind.Key) node.SetName(bind.Key)
@@ -392,19 +363,48 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node {
action = strings.TrimSpace(action) action = strings.TrimSpace(action)
node := document.NewNode() node := document.NewNode()
if !strings.HasPrefix(action, "spawn ") { parts := n.parseActionParts(action)
if len(parts) == 0 {
node.SetName(action) node.SetName(action)
return node return node
} }
node.SetName("spawn") node.SetName(parts[0])
args := n.parseSpawnArgs(strings.TrimPrefix(action, "spawn ")) for _, arg := range parts[1:] {
for _, arg := range args {
node.AddArgument(arg, "") node.AddArgument(arg, "")
} }
return node return node
} }
func (n *NiriProvider) parseActionParts(action string) []string {
var parts []string
var current strings.Builder
var inQuote, escaped bool
for _, r := range action {
switch {
case escaped:
current.WriteRune(r)
escaped = false
case r == '\\':
escaped = true
case r == '"':
inQuote = !inQuote
case r == ' ' && !inQuote:
if current.Len() > 0 {
parts = append(parts, current.String())
current.Reset()
}
default:
current.WriteRune(r)
}
}
if current.Len() > 0 {
parts = append(parts, current.String())
}
return parts
}
func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error { func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error {
overridePath := n.GetOverridePath() overridePath := n.GetOverridePath()
content := n.generateBindsContent(binds) content := n.generateBindsContent(binds)
@@ -501,21 +501,46 @@ func (n *NiriProvider) writeBindNode(sb *strings.Builder, bind *overrideBind, in
sb.WriteString(" { ") sb.WriteString(" { ")
if len(node.Children) > 0 { if len(node.Children) > 0 {
child := node.Children[0] child := node.Children[0]
sb.WriteString(child.Name.String()) actionName := child.Name.String()
sb.WriteString(actionName)
forceQuote := actionName == "spawn"
for _, arg := range child.Arguments { for _, arg := range child.Arguments {
sb.WriteString(" ") sb.WriteString(" ")
n.writeQuotedArg(sb, arg.ValueString()) n.writeArg(sb, arg.ValueString(), forceQuote)
} }
} }
sb.WriteString("; }\n") sb.WriteString("; }\n")
} }
func (n *NiriProvider) writeQuotedArg(sb *strings.Builder, val string) { func (n *NiriProvider) writeArg(sb *strings.Builder, val string, forceQuote bool) {
if !forceQuote && n.isNumericArg(val) {
sb.WriteString(val)
return
}
sb.WriteString("\"") sb.WriteString("\"")
sb.WriteString(strings.ReplaceAll(val, "\"", "\\\"")) sb.WriteString(strings.ReplaceAll(val, "\"", "\\\""))
sb.WriteString("\"") sb.WriteString("\"")
} }
func (n *NiriProvider) isNumericArg(val string) bool {
if val == "" {
return false
}
start := 0
if val[0] == '-' || val[0] == '+' {
if len(val) == 1 {
return false
}
start = 1
}
for i := start; i < len(val); i++ {
if val[i] < '0' || val[i] > '9' {
return false
}
}
return true
}
func (n *NiriProvider) validateBindsContent(content string) error { func (n *NiriProvider) validateBindsContent(content string) error {
tmpFile, err := os.CreateTemp("", "dms-binds-*.kdl") tmpFile, err := os.CreateTemp("", "dms-binds-*.kdl")
if err != nil { if err != nil {

View File

@@ -496,3 +496,119 @@ func TestNiriParseMultipleArgs(t *testing.T) {
} }
} }
} }
func TestNiriParseNumericWorkspaceBinds(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
Mod+2 hotkey-overlay-title="Focus Workspace 2" { focus-workspace 2; }
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 4 {
t.Errorf("Expected 4 keybinds, got %d", len(result.Section.Keybinds))
}
for _, kb := range result.Section.Keybinds {
switch kb.Key {
case "1":
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" {
if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "1" {
t.Errorf("Mod+1 action/args mismatch: %+v", kb)
}
if kb.Description != "Focus Workspace 1" {
t.Errorf("Mod+1 description = %q, want 'Focus Workspace 1'", kb.Description)
}
}
case "0":
if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "10" {
t.Errorf("Mod+0 action/args mismatch: %+v", kb)
}
}
}
}
func TestNiriParseQuotedStringArgs(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
Super+Shift+Minus hotkey-overlay-title="Adjust Window Height -10%" { set-window-height "-10%"; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 3 {
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
}
for _, kb := range result.Section.Keybinds {
if kb.Action == "set-column-width" {
if len(kb.Args) != 1 {
t.Errorf("set-column-width should have 1 arg, got %d", len(kb.Args))
continue
}
if kb.Args[0] != "-10%" && kb.Args[0] != "+10%" {
t.Errorf("set-column-width arg = %q, want -10%% or +10%%", kb.Args[0])
}
}
}
}
func TestNiriParseActionWithProperties(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1 focus=false; }
Mod+Shift+2 hotkey-overlay-title="Move to Workspace 2" { move-column-to-workspace 2 focus=false; }
Alt+Tab { next-window scope="output"; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 3 {
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
}
for _, kb := range result.Section.Keybinds {
switch kb.Action {
case "move-column-to-workspace":
if len(kb.Args) != 1 {
t.Errorf("move-column-to-workspace should have 1 arg, got %d", len(kb.Args))
}
case "next-window":
if kb.Key != "Tab" {
t.Errorf("next-window key = %q, want 'Tab'", kb.Key)
}
}
}
}

View File

@@ -397,3 +397,211 @@ recent-windows {
t.Errorf("Expected at least 2 Alt-Tab binds, got %d", len(cheatSheet.Binds["Alt-Tab"])) t.Errorf("Expected at least 2 Alt-Tab binds, got %d", len(cheatSheet.Binds["Alt-Tab"]))
} }
} }
func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
provider := NewNiriProvider("")
tests := []struct {
name string
binds map[string]*overrideBind
expected string
}{
{
name: "workspace with numeric arg",
binds: map[string]*overrideBind{
"Mod+1": {
Key: "Mod+1",
Action: "focus-workspace 1",
Description: "Focus Workspace 1",
},
},
expected: `binds {
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
}
`,
},
{
name: "workspace with large numeric arg",
binds: map[string]*overrideBind{
"Mod+0": {
Key: "Mod+0",
Action: "focus-workspace 10",
Description: "Focus Workspace 10",
},
},
expected: `binds {
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
}
`,
},
{
name: "percentage string arg (should be quoted)",
binds: map[string]*overrideBind{
"Super+Minus": {
Key: "Super+Minus",
Action: `set-column-width "-10%"`,
Description: "Adjust Column Width -10%",
},
},
expected: `binds {
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
}
`,
},
{
name: "positive percentage string arg",
binds: map[string]*overrideBind{
"Super+Equal": {
Key: "Super+Equal",
Action: `set-column-width "+10%"`,
Description: "Adjust Column Width +10%",
},
},
expected: `binds {
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
}
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := provider.generateBindsContent(tt.binds)
if result != tt.expected {
t.Errorf("generateBindsContent() =\n%q\nwant:\n%q", result, tt.expected)
}
})
}
}
func TestNiriGenerateActionWithUnquotedPercentArg(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"Super+Equal": {
Key: "Super+Equal",
Action: "set-window-height +10%",
Description: "Adjust Window Height +10%",
},
}
content := provider.generateBindsContent(binds)
expected := `binds {
Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; }
}
`
if content != expected {
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
}
}
func TestNiriGenerateSpawnWithNumericArgs(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"XF86AudioLowerVolume": {
Key: "XF86AudioLowerVolume",
Action: `spawn "dms" "ipc" "call" "audio" "decrement" "3"`,
Options: map[string]any{"allow-when-locked": true},
},
}
content := provider.generateBindsContent(binds)
expected := `binds {
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
}
`
if content != expected {
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
}
}
func TestNiriGenerateSpawnNumericArgFromCLI(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"XF86AudioLowerVolume": {
Key: "XF86AudioLowerVolume",
Action: "spawn dms ipc call audio decrement 3",
Options: map[string]any{"allow-when-locked": true},
},
}
content := provider.generateBindsContent(binds)
expected := `binds {
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
}
`
if content != expected {
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
}
}
func TestNiriGenerateWorkspaceBindsRoundTrip(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"Mod+1": {
Key: "Mod+1",
Action: "focus-workspace 1",
Description: "Focus Workspace 1",
},
"Mod+2": {
Key: "Mod+2",
Action: "focus-workspace 2",
Description: "Focus Workspace 2",
},
"Mod+Shift+1": {
Key: "Mod+Shift+1",
Action: "move-column-to-workspace 1",
Description: "Move to Workspace 1",
},
"Super+Minus": {
Key: "Super+Minus",
Action: "set-column-width -10%",
Description: "Adjust Column Width -10%",
},
}
content := provider.generateBindsContent(binds)
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write temp file: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("Failed to parse generated content: %v\nContent was:\n%s", err, content)
}
if len(result.Section.Keybinds) != 4 {
t.Errorf("Expected 4 keybinds after round-trip, got %d", len(result.Section.Keybinds))
}
foundFocusWS1 := false
foundMoveWS1 := false
foundSetWidth := false
for _, kb := range result.Section.Keybinds {
switch {
case kb.Action == "focus-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1":
foundFocusWS1 = true
case kb.Action == "move-column-to-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1":
foundMoveWS1 = true
case kb.Action == "set-column-width" && len(kb.Args) > 0 && kb.Args[0] == "-10%":
foundSetWidth = true
}
}
if !foundFocusWS1 {
t.Error("focus-workspace 1 not found after round-trip")
}
if !foundMoveWS1 {
t.Error("move-column-to-workspace 1 not found after round-trip")
}
if !foundSetWidth {
t.Error("set-column-width -10% not found after round-trip")
}
}

View File

@@ -18,6 +18,7 @@ const KEY_MAP = {
96: "grave", 96: "grave",
32: "space", 32: "space",
16777225: "Print", 16777225: "Print",
16777226: "Print",
16777220: "Return", 16777220: "Return",
16777221: "Return", 16777221: "Return",
16777217: "Tab", 16777217: "Tab",
@@ -93,20 +94,20 @@ function xkbKeyFromQtKey(qk) {
function modsFromEvent(mods) { function modsFromEvent(mods) {
var result = []; var result = [];
if (mods & 0x04000000)
result.push("Ctrl");
if (mods & 0x02000000)
result.push("Shift");
var hasAlt = mods & 0x08000000; var hasAlt = mods & 0x08000000;
var hasSuper = mods & 0x10000000; var hasSuper = mods & 0x10000000;
if (hasAlt && hasSuper) { if (hasAlt && hasSuper) {
result.push("Mod"); result.push("Mod");
} else { } else {
if (hasAlt)
result.push("Alt");
if (hasSuper) if (hasSuper)
result.push("Super"); result.push("Super");
if (hasAlt)
result.push("Alt");
} }
if (mods & 0x04000000)
result.push("Ctrl");
if (mods & 0x02000000)
result.push("Shift");
return result; return result;
} }

View File

@@ -34,7 +34,7 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call notepad toggle", label: "Notepad: Toggle" }, { id: "spawn dms ipc call notepad toggle", label: "Notepad: Toggle" },
{ id: "spawn dms ipc call notepad open", label: "Notepad: Open" }, { id: "spawn dms ipc call notepad open", label: "Notepad: Open" },
{ id: "spawn dms ipc call notepad close", label: "Notepad: Close" }, { id: "spawn dms ipc call notepad close", label: "Notepad: Close" },
{ id: "spawn dms ipc call dash toggle", label: "Dashboard: Toggle" }, { id: "spawn dms ipc call dash toggle \"\"", label: "Dashboard: Toggle" },
{ id: "spawn dms ipc call dash open overview", label: "Dashboard: Overview" }, { id: "spawn dms ipc call dash open overview", label: "Dashboard: Overview" },
{ id: "spawn dms ipc call dash open media", label: "Dashboard: Media" }, { id: "spawn dms ipc call dash open media", label: "Dashboard: Media" },
{ id: "spawn dms ipc call dash open weather", label: "Dashboard: Weather" }, { id: "spawn dms ipc call dash open weather", label: "Dashboard: Weather" },
@@ -109,9 +109,15 @@ const COMPOSITOR_ACTIONS = {
{ id: "fullscreen-window", label: "Fullscreen" }, { id: "fullscreen-window", label: "Fullscreen" },
{ id: "maximize-column", label: "Maximize Column" }, { id: "maximize-column", label: "Maximize Column" },
{ id: "center-column", label: "Center Column" }, { id: "center-column", label: "Center Column" },
{ id: "center-visible-columns", label: "Center Visible Columns" },
{ id: "toggle-window-floating", label: "Toggle Floating" }, { id: "toggle-window-floating", label: "Toggle Floating" },
{ id: "switch-focus-between-floating-and-tiling", label: "Switch Floating/Tiling Focus" },
{ id: "switch-preset-column-width", label: "Cycle Column Width" }, { id: "switch-preset-column-width", label: "Cycle Column Width" },
{ id: "switch-preset-window-height", label: "Cycle Window Height" }, { id: "switch-preset-window-height", label: "Cycle Window Height" },
{ id: "set-column-width", label: "Set Column Width" },
{ id: "set-window-height", label: "Set Window Height" },
{ id: "reset-window-height", label: "Reset Window Height" },
{ id: "expand-column-to-available-width", label: "Expand to Available Width" },
{ id: "consume-or-expel-window-left", label: "Consume/Expel Left" }, { id: "consume-or-expel-window-left", label: "Consume/Expel Left" },
{ id: "consume-or-expel-window-right", label: "Consume/Expel Right" }, { id: "consume-or-expel-window-right", label: "Consume/Expel Right" },
{ id: "toggle-column-tabbed-display", label: "Toggle Tabbed" } { id: "toggle-column-tabbed-display", label: "Toggle Tabbed" }
@@ -136,8 +142,10 @@ const COMPOSITOR_ACTIONS = {
{ id: "focus-workspace-down", label: "Focus Workspace Down" }, { id: "focus-workspace-down", label: "Focus Workspace Down" },
{ id: "focus-workspace-up", label: "Focus Workspace Up" }, { id: "focus-workspace-up", label: "Focus Workspace Up" },
{ id: "focus-workspace-previous", label: "Focus Previous Workspace" }, { id: "focus-workspace-previous", label: "Focus Previous Workspace" },
{ id: "focus-workspace", label: "Focus Workspace (by index)" },
{ id: "move-column-to-workspace-down", label: "Move to Workspace Down" }, { id: "move-column-to-workspace-down", label: "Move to Workspace Down" },
{ id: "move-column-to-workspace-up", label: "Move to Workspace Up" }, { id: "move-column-to-workspace-up", label: "Move to Workspace Up" },
{ id: "move-column-to-workspace", label: "Move to Workspace (by index)" },
{ id: "move-workspace-down", label: "Move Workspace Down" }, { id: "move-workspace-down", label: "Move Workspace Down" },
{ id: "move-workspace-up", label: "Move Workspace Up" } { id: "move-workspace-up", label: "Move Workspace Up" }
], ],
@@ -173,6 +181,52 @@ const COMPOSITOR_ACTIONS = {
const CATEGORY_ORDER = ["DMS", "Execute", "Workspace", "Window", "Monitor", "Screenshot", "System", "Overview", "Alt-Tab", "Other"]; const CATEGORY_ORDER = ["DMS", "Execute", "Workspace", "Window", "Monitor", "Screenshot", "System", "Overview", "Alt-Tab", "Other"];
const ACTION_ARGS = {
"set-column-width": {
args: [{ name: "value", type: "text", label: "Width", placeholder: "+10%, -10%, 50%" }]
},
"set-window-height": {
args: [{ name: "value", type: "text", label: "Height", placeholder: "+10%, -10%, 50%" }]
},
"focus-workspace": {
args: [{ name: "index", type: "number", label: "Workspace", placeholder: "1, 2, 3..." }]
},
"move-column-to-workspace": {
args: [
{ name: "index", type: "number", label: "Workspace", placeholder: "1, 2, 3..." },
{ name: "focus", type: "bool", label: "Follow focus", default: false }
]
},
"screenshot": {
args: [{ name: "opts", type: "screenshot", label: "Options" }]
},
"screenshot-screen": {
args: [{ name: "opts", type: "screenshot", label: "Options" }]
},
"screenshot-window": {
args: [{ name: "opts", type: "screenshot", label: "Options" }]
}
};
const DMS_ACTION_ARGS = {
"audio increment": {
base: "spawn dms ipc call audio increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"audio decrement": {
base: "spawn dms ipc call audio decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"brightness increment": {
base: "spawn dms ipc call brightness increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"brightness decrement": {
base: "spawn dms ipc call brightness decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
}
};
function getActionTypes() { function getActionTypes() {
return ACTION_TYPES; return ACTION_TYPES;
} }
@@ -322,3 +376,120 @@ function parseShellCommand(action) {
content = content.slice(1, -1); content = content.slice(1, -1);
return content.replace(/\\"/g, "\""); return content.replace(/\\"/g, "\"");
} }
function getActionArgConfig(action) {
if (!action)
return null;
var baseAction = action.split(" ")[0];
if (ACTION_ARGS[baseAction])
return { type: "compositor", base: baseAction, config: ACTION_ARGS[baseAction] };
for (var key in DMS_ACTION_ARGS) {
if (action.startsWith(DMS_ACTION_ARGS[key].base))
return { type: "dms", base: key, config: DMS_ACTION_ARGS[key] };
}
return null;
}
function parseCompositorActionArgs(action) {
if (!action)
return { base: "", args: {} };
var parts = action.split(" ");
var base = parts[0];
var args = {};
if (!ACTION_ARGS[base])
return { base: action, args: {} };
var argConfig = ACTION_ARGS[base];
var argParts = parts.slice(1);
if (base === "move-column-to-workspace") {
for (var i = 0; i < argParts.length; i++) {
if (argParts[i] === "focus=true" || argParts[i] === "focus=false") {
args.focus = argParts[i] === "focus=true";
} else if (!args.index) {
args.index = argParts[i];
}
}
} else if (base.startsWith("screenshot")) {
args.opts = {};
for (var j = 0; j < argParts.length; j += 2) {
if (j + 1 < argParts.length)
args.opts[argParts[j]] = argParts[j + 1];
}
} else if (argParts.length > 0) {
args.value = argParts.join(" ");
}
return { base: base, args: args };
}
function buildCompositorAction(base, args) {
if (!base)
return "";
var parts = [base];
if (!args || Object.keys(args).length === 0)
return base;
if (base === "move-column-to-workspace") {
if (args.index)
parts.push(args.index);
if (args.focus === true)
parts.push("focus=true");
else if (args.focus === false)
parts.push("focus=false");
} else if (base.startsWith("screenshot") && args.opts) {
for (var key in args.opts) {
if (args.opts[key] !== undefined && args.opts[key] !== "") {
parts.push(key);
parts.push(args.opts[key]);
}
}
} else if (args.value) {
parts.push(args.value);
} else if (args.index) {
parts.push(args.index);
}
return parts.join(" ");
}
function parseDmsActionArgs(action) {
if (!action)
return { base: "", args: {} };
for (var key in DMS_ACTION_ARGS) {
var config = DMS_ACTION_ARGS[key];
if (action.startsWith(config.base)) {
var rest = action.slice(config.base.length).trim();
return { base: key, args: { amount: rest || "" } };
}
}
return { base: action, args: {} };
}
function buildDmsAction(baseKey, args) {
var config = DMS_ACTION_ARGS[baseKey];
if (!config)
return "";
var action = config.base;
if (args && args.amount)
action += " " + args.amount;
return action;
}
function getScreenshotOptions() {
return [
{ id: "write-to-disk", label: "Save to disk", type: "bool" },
{ id: "show-pointer", label: "Show pointer", type: "bool" }
];
}

View File

@@ -12,7 +12,7 @@ DankModal {
HyprlandFocusGrab { HyprlandFocusGrab {
windows: [root.contentWindow] windows: [root.contentWindow]
active: CompositorService.isHyprland && root.shouldHaveFocus active: root.useHyprlandFocusGrab && root.shouldHaveFocus
} }
property string deviceName: "" property string deviceName: ""

View File

@@ -15,7 +15,7 @@ DankModal {
HyprlandFocusGrab { HyprlandFocusGrab {
windows: [clipboardHistoryModal.contentWindow] windows: [clipboardHistoryModal.contentWindow]
active: CompositorService.isHyprland && clipboardHistoryModal.shouldHaveFocus active: clipboardHistoryModal.useHyprlandFocusGrab && clipboardHistoryModal.shouldHaveFocus
} }
property int totalCount: 0 property int totalCount: 0

View File

@@ -47,6 +47,7 @@ Item {
property bool useOverlayLayer: false property bool useOverlayLayer: false
readonly property alias contentWindow: contentWindow readonly property alias contentWindow: contentWindow
readonly property alias backgroundWindow: backgroundWindow readonly property alias backgroundWindow: backgroundWindow
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
signal opened signal opened
signal dialogClosed signal dialogClosed
@@ -262,7 +263,7 @@ Item {
return customKeyboardFocus; return customKeyboardFocus;
if (!shouldHaveFocus) if (!shouldHaveFocus)
return WlrKeyboardFocus.None; return WlrKeyboardFocus.None;
if (CompositorService.isHyprland) if (root.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand; return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive; return WlrKeyboardFocus.Exclusive;
} }

View File

@@ -13,7 +13,7 @@ DankModal {
HyprlandFocusGrab { HyprlandFocusGrab {
windows: [root.contentWindow] windows: [root.contentWindow]
active: CompositorService.isHyprland && root.shouldHaveFocus active: root.useHyprlandFocusGrab && root.shouldHaveFocus
} }
property string pickerTitle: I18n.tr("Choose Color") property string pickerTitle: I18n.tr("Choose Color")

View File

@@ -21,7 +21,7 @@ DankModal {
HyprlandFocusGrab { HyprlandFocusGrab {
windows: [root.contentWindow] windows: [root.contentWindow]
active: CompositorService.isHyprland && root.shouldHaveFocus active: root.useHyprlandFocusGrab && root.shouldHaveFocus
} }
function scrollDown() { function scrollDown() {

View File

@@ -13,7 +13,7 @@ DankModal {
HyprlandFocusGrab { HyprlandFocusGrab {
windows: [notificationModal.contentWindow] windows: [notificationModal.contentWindow]
active: CompositorService.isHyprland && notificationModal.shouldHaveFocus active: notificationModal.useHyprlandFocusGrab && notificationModal.shouldHaveFocus
} }
property bool notificationModalOpen: false property bool notificationModalOpen: false

View File

@@ -15,7 +15,7 @@ DankModal {
HyprlandFocusGrab { HyprlandFocusGrab {
windows: [root.contentWindow] windows: [root.contentWindow]
active: CompositorService.isHyprland && root.shouldHaveFocus active: root.useHyprlandFocusGrab && root.shouldHaveFocus
} }
property int selectedIndex: 0 property int selectedIndex: 0

View File

@@ -10,12 +10,31 @@ Rectangle {
property var fileSearchController: null property var fileSearchController: null
function resetScroll() { function resetScroll() {
filesList.contentY = 0 filesList.contentY = 0;
} }
color: "transparent" color: "transparent"
clip: true clip: true
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 32
z: 100
visible: filesList.contentHeight > filesList.height && (filesList.currentIndex < filesList.count - 1 || filesList.contentY < filesList.contentHeight - filesList.height - 1)
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 1.0
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
}
}
}
DankListView { DankListView {
id: filesList id: filesList
@@ -30,18 +49,22 @@ Rectangle {
function ensureVisible(index) { function ensureVisible(index) {
if (index < 0 || index >= count) if (index < 0 || index >= count)
return return;
const itemY = index * (itemHeight + itemSpacing);
const itemY = index * (itemHeight + itemSpacing) const itemBottom = itemY + itemHeight;
const itemBottom = itemY + itemHeight const fadeHeight = 32;
const isLastItem = index === count - 1;
if (itemY < contentY) if (itemY < contentY)
contentY = itemY contentY = itemY;
else if (itemBottom > contentY + height) else if (itemBottom > contentY + height - (isLastItem ? 0 : fadeHeight))
contentY = itemBottom - height contentY = Math.min(itemBottom - height + (isLastItem ? 0 : fadeHeight), contentHeight - height);
} }
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: 1
model: fileSearchController ? fileSearchController.model : null model: fileSearchController ? fileSearchController.model : null
currentIndex: fileSearchController ? fileSearchController.selectedIndex : -1 currentIndex: fileSearchController ? fileSearchController.selectedIndex : -1
clip: true clip: true
@@ -53,26 +76,26 @@ Rectangle {
onCurrentIndexChanged: { onCurrentIndexChanged: {
if (keyboardNavigationActive) if (keyboardNavigationActive)
ensureVisible(currentIndex) ensureVisible(currentIndex);
} }
onItemClicked: function (index) { onItemClicked: function (index) {
if (fileSearchController) { if (fileSearchController) {
const item = fileSearchController.model.get(index) const item = fileSearchController.model.get(index);
fileSearchController.openFile(item.filePath) fileSearchController.openFile(item.filePath);
} }
} }
onItemRightClicked: function (index) { onItemRightClicked: function (index) {
if (fileSearchController) { if (fileSearchController) {
const item = fileSearchController.model.get(index) const item = fileSearchController.model.get(index);
fileSearchController.openFolder(item.filePath) fileSearchController.openFolder(item.filePath);
} }
} }
onKeyboardNavigationReset: { onKeyboardNavigationReset: {
if (fileSearchController) if (fileSearchController)
fileSearchController.keyboardNavigationActive = false fileSearchController.keyboardNavigationActive = false;
} }
delegate: Rectangle { delegate: Rectangle {
@@ -86,7 +109,7 @@ Rectangle {
width: ListView.view.width width: ListView.view.width
height: filesList.itemHeight height: filesList.itemHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: ListView.isCurrentItem ? Theme.primaryPressed : fileMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: ListView.isCurrentItem ? Theme.widgetBaseHoverColor : fileMouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent
@@ -109,16 +132,16 @@ Rectangle {
id: nerdIcon id: nerdIcon
anchors.centerIn: parent anchors.centerIn: parent
name: { name: {
const lowerName = fileName.toLowerCase() const lowerName = fileName.toLowerCase();
if (lowerName.startsWith("dockerfile")) if (lowerName.startsWith("dockerfile"))
return "docker" return "docker";
if (lowerName.startsWith("makefile")) if (lowerName.startsWith("makefile"))
return "makefile" return "makefile";
if (lowerName.startsWith("license")) if (lowerName.startsWith("license"))
return "license" return "license";
if (lowerName.startsWith("readme")) if (lowerName.startsWith("readme"))
return "readme" return "readme";
return fileExtension.toLowerCase() return fileExtension.toLowerCase();
} }
size: Theme.fontSizeXLarge size: Theme.fontSizeXLarge
color: Theme.surfaceText color: Theme.surfaceText
@@ -196,18 +219,18 @@ Rectangle {
z: 10 z: 10
onEntered: { onEntered: {
if (filesList.hoverUpdatesSelection && !filesList.keyboardNavigationActive) if (filesList.hoverUpdatesSelection && !filesList.keyboardNavigationActive)
filesList.currentIndex = index filesList.currentIndex = index;
} }
onPositionChanged: { onPositionChanged: {
filesList.keyboardNavigationReset() filesList.keyboardNavigationReset();
} }
onClicked: mouse => { onClicked: mouse => {
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
filesList.itemClicked(index) filesList.itemClicked(index);
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
filesList.itemRightClicked(index) filesList.itemRightClicked(index);
} }
} }
} }
} }
} }
@@ -219,21 +242,21 @@ Rectangle {
StyledText { StyledText {
property string displayText: { property string displayText: {
if (!fileSearchController) { if (!fileSearchController) {
return "" return "";
} }
if (!DSearchService.dsearchAvailable) { if (!DSearchService.dsearchAvailable) {
return I18n.tr("DankSearch not available") return I18n.tr("DankSearch not available");
} }
if (fileSearchController.isSearching) { if (fileSearchController.isSearching) {
return I18n.tr("Searching...") return I18n.tr("Searching...");
} }
if (fileSearchController.searchQuery.length === 0) { if (fileSearchController.searchQuery.length === 0) {
return I18n.tr("Enter a search query") return I18n.tr("Enter a search query");
} }
if (!fileSearchController.model || fileSearchController.model.count === 0) { if (!fileSearchController.model || fileSearchController.model.count === 0) {
return I18n.tr("No files found") return I18n.tr("No files found");
} }
return "" return "";
} }
text: displayText text: displayText

View File

@@ -12,7 +12,7 @@ DankModal {
HyprlandFocusGrab { HyprlandFocusGrab {
windows: [spotlightModal.contentWindow] windows: [spotlightModal.contentWindow]
active: CompositorService.isHyprland && spotlightModal.shouldHaveFocus active: spotlightModal.useHyprlandFocusGrab && spotlightModal.shouldHaveFocus
} }
property bool spotlightOpen: false property bool spotlightOpen: false

View File

@@ -51,6 +51,33 @@ Rectangle {
color: "transparent" color: "transparent"
clip: true clip: true
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 32
z: 100
visible: {
if (!appLauncher)
return false;
const view = appLauncher.viewMode === "list" ? resultsList : (gridLoader.item || resultsList);
const isLastItem = appLauncher.viewMode === "list" ? view.currentIndex >= view.count - 1 : (gridLoader.item ? Math.floor(view.currentIndex / view.actualColumns) >= Math.floor((view.count - 1) / view.actualColumns) : false);
const hasOverflow = view.contentHeight > view.height;
const atBottom = view.contentY >= view.contentHeight - view.height - 1;
return hasOverflow && (!isLastItem || !atBottom);
}
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 1.0
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
}
}
}
DankListView { DankListView {
id: resultsList id: resultsList
@@ -70,14 +97,19 @@ Rectangle {
return; return;
const itemY = index * (itemHeight + itemSpacing); const itemY = index * (itemHeight + itemSpacing);
const itemBottom = itemY + itemHeight; const itemBottom = itemY + itemHeight;
const fadeHeight = 32;
const isLastItem = index === count - 1;
if (itemY < contentY) if (itemY < contentY)
contentY = itemY; contentY = itemY;
else if (itemBottom > contentY + height) else if (itemBottom > contentY + height - (isLastItem ? 0 : fadeHeight))
contentY = itemBottom - height; contentY = Math.min(itemBottom - height + (isLastItem ? 0 : fadeHeight), contentHeight - height);
} }
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: 1
visible: appLauncher && appLauncher.viewMode === "list" visible: appLauncher && appLauncher.viewMode === "list"
model: appLauncher ? appLauncher.model : null model: appLauncher ? appLauncher.model : null
currentIndex: appLauncher ? appLauncher.selectedIndex : -1 currentIndex: appLauncher ? appLauncher.selectedIndex : -1
@@ -127,7 +159,10 @@ Rectangle {
property real _lastWidth: 0 property real _lastWidth: 0
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: 1
visible: appLauncher && appLauncher.viewMode === "grid" visible: appLauncher && appLauncher.viewMode === "grid"
active: appLauncher && appLauncher.viewMode === "grid" active: appLauncher && appLauncher.viewMode === "grid"
asynchronous: false asynchronous: false
@@ -177,10 +212,12 @@ Rectangle {
return; return;
const itemY = Math.floor(index / actualColumns) * cellHeight; const itemY = Math.floor(index / actualColumns) * cellHeight;
const itemBottom = itemY + cellHeight; const itemBottom = itemY + cellHeight;
const fadeHeight = 32;
const isLastRow = Math.floor(index / actualColumns) >= Math.floor((count - 1) / actualColumns);
if (itemY < contentY) if (itemY < contentY)
contentY = itemY; contentY = itemY;
else if (itemBottom > contentY + height) else if (itemBottom > contentY + height - (isLastRow ? 0 : fadeHeight))
contentY = itemBottom - height; contentY = Math.min(itemBottom - height + (isLastRow ? 0 : fadeHeight), contentHeight - height);
} }
anchors.fill: parent anchors.fill: parent

View File

@@ -406,6 +406,34 @@ DankPopout {
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: "transparent" color: "transparent"
clip: true
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 32
z: 100
visible: {
if (appDrawerPopout.searchMode !== "apps")
return false;
const view = appLauncher.viewMode === "list" ? appList : appGrid;
const isLastItem = view.currentIndex >= view.count - 1;
const hasOverflow = view.contentHeight > view.height;
const atBottom = view.contentY >= view.contentHeight - view.height - 1;
return hasOverflow && (!isLastItem || !atBottom);
}
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 1.0
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
}
}
}
DankListView { DankListView {
id: appList id: appList
@@ -426,14 +454,16 @@ DankPopout {
return; return;
var itemY = index * (itemHeight + itemSpacing); var itemY = index * (itemHeight + itemSpacing);
var itemBottom = itemY + itemHeight; var itemBottom = itemY + itemHeight;
var fadeHeight = 32;
var isLastItem = index === count - 1;
if (itemY < contentY) if (itemY < contentY)
contentY = itemY; contentY = itemY;
else if (itemBottom > contentY + height) else if (itemBottom > contentY + height - (isLastItem ? 0 : fadeHeight))
contentY = itemBottom - height; contentY = Math.min(itemBottom - height + (isLastItem ? 0 : fadeHeight), contentHeight - height);
} }
anchors.fill: parent anchors.fill: parent
anchors.bottomMargin: Theme.spacingS anchors.bottomMargin: 1
visible: appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "list" visible: appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "list"
model: appLauncher.model model: appLauncher.model
currentIndex: appLauncher.selectedIndex currentIndex: appLauncher.selectedIndex
@@ -511,14 +541,16 @@ DankPopout {
return; return;
var itemY = Math.floor(index / actualColumns) * cellHeight; var itemY = Math.floor(index / actualColumns) * cellHeight;
var itemBottom = itemY + cellHeight; var itemBottom = itemY + cellHeight;
var fadeHeight = 32;
var isLastRow = Math.floor(index / actualColumns) >= Math.floor((count - 1) / actualColumns);
if (itemY < contentY) if (itemY < contentY)
contentY = itemY; contentY = itemY;
else if (itemBottom > contentY + height) else if (itemBottom > contentY + height - (isLastRow ? 0 : fadeHeight))
contentY = itemBottom - height; contentY = Math.min(itemBottom - height + (isLastRow ? 0 : fadeHeight), contentHeight - height);
} }
anchors.fill: parent anchors.fill: parent
anchors.bottomMargin: Theme.spacingS anchors.bottomMargin: 1
visible: appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid" visible: appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid"
model: appLauncher.model model: appLauncher.model
clip: true clip: true

View File

@@ -76,7 +76,7 @@ DankPopout {
return WlrKeyboardFocus.None; return WlrKeyboardFocus.None;
if (anyModalOpen) if (anyModalOpen)
return WlrKeyboardFocus.None; return WlrKeyboardFocus.None;
if (CompositorService.isHyprland) if (CompositorService.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand; return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive; return WlrKeyboardFocus.Exclusive;
} }

View File

@@ -120,7 +120,7 @@ Item {
active: borderFullPathCorrectShape && borderEdgePathCorrectShape active: borderFullPathCorrectShape && borderEdgePathCorrectShape
readonly property real _scale: CompositorService.getScreenScale(barWindow.screen) readonly property real _scale: CompositorService.getScreenScale(barWindow.screen)
readonly property real borderThickness: Theme.px(Math.max(1, barConfig?.borderThickness ?? 1), _scale) readonly property real borderThickness: Math.ceil(Math.max(1, barConfig?.borderThickness ?? 1) * _scale) / _scale
readonly property real inset: borderThickness / 2 readonly property real inset: borderThickness / 2
readonly property string borderColorKey: barConfig?.borderColor || "surfaceText" readonly property string borderColorKey: barConfig?.borderColor || "surfaceText"
readonly property color baseColor: (borderColorKey === "surfaceText") ? Theme.surfaceText : (borderColorKey === "primary") ? Theme.primary : Theme.secondary readonly property color baseColor: (borderColorKey === "surfaceText") ? Theme.surfaceText : (borderColorKey === "primary") ? Theme.primary : Theme.secondary

View File

@@ -427,7 +427,7 @@ Item {
WlrLayershell.keyboardFocus: { WlrLayershell.keyboardFocus: {
if (!root.menuOpen) if (!root.menuOpen)
return WlrKeyboardFocus.None; return WlrKeyboardFocus.None;
if (CompositorService.isHyprland) if (CompositorService.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand; return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive; return WlrKeyboardFocus.Exclusive;
} }
@@ -436,7 +436,7 @@ Item {
HyprlandFocusGrab { HyprlandFocusGrab {
windows: [overflowMenu] windows: [overflowMenu]
active: CompositorService.isHyprland && root.menuOpen active: CompositorService.useHyprlandFocusGrab && root.menuOpen
} }
Connections { Connections {
@@ -915,7 +915,7 @@ Item {
WlrLayershell.keyboardFocus: { WlrLayershell.keyboardFocus: {
if (!menuRoot.showMenu) if (!menuRoot.showMenu)
return WlrKeyboardFocus.None; return WlrKeyboardFocus.None;
if (CompositorService.isHyprland) if (CompositorService.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand; return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive; return WlrKeyboardFocus.Exclusive;
} }
@@ -923,7 +923,7 @@ Item {
HyprlandFocusGrab { HyprlandFocusGrab {
windows: [menuWindow] windows: [menuWindow]
active: CompositorService.isHyprland && menuRoot.showMenu active: CompositorService.useHyprlandFocusGrab && menuRoot.showMenu
} }
anchors { anchors {

View File

@@ -89,12 +89,17 @@ Item {
function saveNewBind(bindData) { function saveNewBind(bindData) {
KeybindsService.saveBind("", bindData); KeybindsService.saveBind("", bindData);
showingNewBind = false;
selectedCategory = "";
_editingKey = bindData.key; _editingKey = bindData.key;
expandedKey = bindData.action; expandedKey = bindData.action;
} }
function _onSaveSuccess() {
if (showingNewBind) {
showingNewBind = false;
selectedCategory = "";
}
}
function scrollToTop() { function scrollToTop() {
flickable.contentY = 0; flickable.contentY = 0;
} }
@@ -121,6 +126,10 @@ Item {
keybindsTab._savedScrollY = flickable.contentY; keybindsTab._savedScrollY = flickable.contentY;
keybindsTab._preserveScroll = true; keybindsTab._preserveScroll = true;
} }
function onBindSaveCompleted(success) {
if (success)
keybindsTab._onSaveSuccess();
}
function onBindRemoved(key) { function onBindRemoved(key) {
keybindsTab._savedScrollY = flickable.contentY; keybindsTab._savedScrollY = flickable.contentY;
keybindsTab._preserveScroll = true; keybindsTab._preserveScroll = true;

View File

@@ -4,6 +4,7 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Services
Scope { Scope {
id: overviewScope id: overviewScope
@@ -32,7 +33,13 @@ Scope {
WlrLayershell.namespace: "dms:workspace-overview" WlrLayershell.namespace: "dms:workspace-overview"
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: overviewScope.overviewOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: {
if (!overviewScope.overviewOpen)
return WlrKeyboardFocus.None;
if (CompositorService.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
anchors { anchors {
top: true top: true
@@ -63,7 +70,8 @@ Scope {
function onOverviewOpenChanged() { function onOverviewOpenChanged() {
if (overviewScope.overviewOpen) { if (overviewScope.overviewOpen) {
grab.hasBeenActivated = false grab.hasBeenActivated = false
delayedGrabTimer.start() if (CompositorService.useHyprlandFocusGrab)
delayedGrabTimer.start()
} else { } else {
delayedGrabTimer.stop() delayedGrabTimer.stop()
grab.active = false grab.active = false
@@ -75,6 +83,8 @@ Scope {
Connections { Connections {
target: root target: root
function onMonitorIsFocusedChanged() { function onMonitorIsFocusedChanged() {
if (!CompositorService.useHyprlandFocusGrab)
return;
if (overviewScope.overviewOpen && root.monitorIsFocused && !grab.active) { if (overviewScope.overviewOpen && root.monitorIsFocused && !grab.active) {
grab.hasBeenActivated = false grab.hasBeenActivated = false
grab.active = true grab.active = true
@@ -89,7 +99,7 @@ Scope {
interval: 150 interval: 150
repeat: false repeat: false
onTriggered: { onTriggered: {
if (overviewScope.overviewOpen && root.monitorIsFocused) { if (CompositorService.useHyprlandFocusGrab && overviewScope.overviewOpen && root.monitorIsFocused) {
grab.active = true grab.active = true
} }
} }

View File

@@ -16,6 +16,7 @@ Singleton {
property bool isSway: false property bool isSway: false
property bool isLabwc: false property bool isLabwc: false
property string compositor: "unknown" property string compositor: "unknown"
readonly property bool useHyprlandFocusGrab: isHyprland && Quickshell.env("DMS_HYPRLAND_EXCLUSIVE_FOCUS") !== "1"
readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET") readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")

View File

@@ -55,6 +55,7 @@ Singleton {
signal bindsLoaded signal bindsLoaded
signal bindSaved(string key) signal bindSaved(string key)
signal bindSaveCompleted(bool success)
signal bindRemoved(string key) signal bindRemoved(string key)
signal dmsBindsFixed signal dmsBindsFixed
@@ -118,12 +119,14 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
root.saving = false; root.saving = false;
if (exitCode === 0) { if (exitCode !== 0) {
root.lastError = "";
root.loadBinds(false);
} else {
console.error("[KeybindsService] Save failed with code:", exitCode); console.error("[KeybindsService] Save failed with code:", exitCode);
root.bindSaveCompleted(false);
return;
} }
root.lastError = "";
root.bindSaveCompleted(true);
root.loadBinds(false);
} }
} }
@@ -141,12 +144,12 @@ Singleton {
} }
onExited: exitCode => { onExited: exitCode => {
if (exitCode === 0) { if (exitCode !== 0) {
root.lastError = "";
root.loadBinds(false);
} else {
console.error("[KeybindsService] Remove failed with code:", exitCode); console.error("[KeybindsService] Remove failed with code:", exitCode);
return;
} }
root.lastError = "";
root.loadBinds(false);
} }
} }
@@ -165,15 +168,15 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
root.fixing = false; root.fixing = false;
if (exitCode === 0) { if (exitCode !== 0) {
root.lastError = "";
root.dmsBindsIncluded = true;
root.dmsBindsFixed();
ToastService.showSuccess(I18n.tr("Binds include added"), I18n.tr("dms/binds.kdl is now included in config.kdl"), "", "keybinds");
Qt.callLater(root.forceReload);
} else {
console.error("[KeybindsService] Fix failed with code:", exitCode); console.error("[KeybindsService] Fix failed with code:", exitCode);
return;
} }
root.lastError = "";
root.dmsBindsIncluded = true;
root.dmsBindsFixed();
ToastService.showSuccess(I18n.tr("Binds include added"), I18n.tr("dms/binds.kdl is now included in config.kdl"), "", "keybinds");
Qt.callLater(root.forceReload);
} }
} }

View File

@@ -30,8 +30,8 @@ Rectangle {
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY) signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
signal keyboardNavigationReset signal keyboardNavigationReset
width: cellWidth width: cellWidth - 1
height: cellHeight height: cellHeight - 1
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: currentIndex === index ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : mouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : "transparent" color: currentIndex === index ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : mouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : "transparent"

View File

@@ -1,6 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
@@ -29,12 +27,12 @@ Rectangle {
signal itemClicked(int index, var modelData) signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY) signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
signal keyboardNavigationReset() signal keyboardNavigationReset
width: listView.width width: listView.width
height: itemHeight height: itemHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: isCurrentItem ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : mouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: isCurrentItem ? Theme.widgetBaseHoverColor : mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent
@@ -97,27 +95,27 @@ Rectangle {
z: 10 z: 10
onEntered: { onEntered: {
if (root.hoverUpdatesSelection && !root.keyboardNavigationActive) if (root.hoverUpdatesSelection && !root.keyboardNavigationActive)
root.listView.currentIndex = root.index root.listView.currentIndex = root.index;
} }
onPositionChanged: { onPositionChanged: {
root.keyboardNavigationReset() root.keyboardNavigationReset();
} }
onClicked: mouse => { onClicked: mouse => {
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
root.itemClicked(root.index, root.model) root.itemClicked(root.index, root.model);
} }
} }
onPressAndHold: mouse => { onPressAndHold: mouse => {
if (!root.isPlugin) { if (!root.isPlugin) {
const globalPos = mapToItem(null, mouse.x, mouse.y) const globalPos = mapToItem(null, mouse.x, mouse.y);
root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y) root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y);
} }
} }
onPressed: mouse => { onPressed: mouse => {
if (mouse.button === Qt.RightButton && !root.isPlugin) { if (mouse.button === Qt.RightButton && !root.isPlugin) {
const globalPos = mapToItem(null, mouse.x, mouse.y) const globalPos = mapToItem(null, mouse.x, mouse.y);
root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y) root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y);
mouse.accepted = true mouse.accepted = true;
} }
} }
} }

View File

@@ -1,6 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import qs.Common
import qs.Widgets import qs.Widgets
Flickable { Flickable {
@@ -24,7 +23,7 @@ Flickable {
WheelHandler { WheelHandler {
id: wheelHandler id: wheelHandler
property real touchpadSpeed: 1.8 property real touchpadSpeed: 2.8
property real momentumRetention: 0.92 property real momentumRetention: 0.92
property real lastWheelTime: 0 property real lastWheelTime: 0
property real momentum: 0 property real momentum: 0
@@ -32,102 +31,104 @@ Flickable {
property bool sessionUsedMouseWheel: false property bool sessionUsedMouseWheel: false
function startMomentum() { function startMomentum() {
flickable.isMomentumActive = true flickable.isMomentumActive = true;
momentumTimer.start() momentumTimer.start();
} }
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => { onWheel: event => {
vbar._scrollBarActive = true vbar._scrollBarActive = true;
vbar.hideTimer.restart() vbar.hideTimer.restart();
const currentTime = Date.now() const currentTime = Date.now();
const timeDelta = currentTime - lastWheelTime const timeDelta = currentTime - lastWheelTime;
lastWheelTime = currentTime lastWheelTime = currentTime;
const deltaY = event.angleDelta.y const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0 const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
const deltaY = event.angleDelta.y;
const isMouseWheel = !hasPixel && hasAngle;
if (isMouseWheel) { if (isMouseWheel) {
sessionUsedMouseWheel = true sessionUsedMouseWheel = true;
momentumTimer.stop() momentumTimer.stop();
flickable.isMomentumActive = false flickable.isMomentumActive = false;
velocitySamples = [] velocitySamples = [];
momentum = 0 momentum = 0;
flickable.momentumVelocity = 0 flickable.momentumVelocity = 0;
const lines = Math.floor(Math.abs(deltaY) / 120) const lines = Math.floor(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed;
let newY = flickable.contentY + scrollAmount let newY = flickable.contentY + scrollAmount;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY)) newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
if (flickable.flicking) { if (flickable.flicking) {
flickable.cancelFlick() flickable.cancelFlick();
} }
flickable.contentY = newY flickable.contentY = newY;
} else { } else {
sessionUsedMouseWheel = false sessionUsedMouseWheel = false;
momentumTimer.stop() momentumTimer.stop();
flickable.isMomentumActive = false flickable.isMomentumActive = false;
let delta = 0 let delta = 0;
if (event.pixelDelta.y !== 0) { if (event.pixelDelta.y !== 0) {
delta = event.pixelDelta.y * touchpadSpeed delta = event.pixelDelta.y * touchpadSpeed;
} else { } else {
delta = event.angleDelta.y / 8 * touchpadSpeed delta = event.angleDelta.y / 8 * touchpadSpeed;
} }
velocitySamples.push({ velocitySamples.push({
"delta": delta, "delta": delta,
"time": currentTime "time": currentTime
}) });
velocitySamples = velocitySamples.filter(s => currentTime - s.time < 100) velocitySamples = velocitySamples.filter(s => currentTime - s.time < 100);
if (velocitySamples.length > 1) { if (velocitySamples.length > 1) {
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0) const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
const timeSpan = currentTime - velocitySamples[0].time const timeSpan = currentTime - velocitySamples[0].time;
if (timeSpan > 0) { if (timeSpan > 0) {
flickable.momentumVelocity = Math.max(-flickable.maxMomentumVelocity, Math.min(flickable.maxMomentumVelocity, totalDelta / timeSpan * 1000)) flickable.momentumVelocity = Math.max(-flickable.maxMomentumVelocity, Math.min(flickable.maxMomentumVelocity, totalDelta / timeSpan * 1000));
} }
} }
if (event.pixelDelta.y !== 0 && timeDelta < 50) { if (event.pixelDelta.y !== 0 && timeDelta < 50) {
momentum = momentum * momentumRetention + delta * 0.15 momentum = momentum * momentumRetention + delta * 0.15;
delta += momentum delta += momentum;
} else { } else {
momentum = 0 momentum = 0;
} }
let newY = flickable.contentY - delta let newY = flickable.contentY - delta;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY)) newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
if (flickable.flicking) { if (flickable.flicking) {
flickable.cancelFlick() flickable.cancelFlick();
} }
flickable.contentY = newY flickable.contentY = newY;
} }
event.accepted = true event.accepted = true;
} }
onActiveChanged: { onActiveChanged: {
if (!active) { if (!active) {
if (!sessionUsedMouseWheel && Math.abs(flickable.momentumVelocity) >= flickable.minMomentumVelocity) { if (!sessionUsedMouseWheel && Math.abs(flickable.momentumVelocity) >= flickable.minMomentumVelocity) {
startMomentum() startMomentum();
} else { } else {
velocitySamples = [] velocitySamples = [];
flickable.momentumVelocity = 0 flickable.momentumVelocity = 0;
} }
} }
} }
} }
onMovementStarted: { onMovementStarted: {
vbar._scrollBarActive = true vbar._scrollBarActive = true;
vbar.hideTimer.stop() vbar.hideTimer.stop();
} }
onMovementEnded: vbar.hideTimer.restart() onMovementEnded: vbar.hideTimer.restart()
@@ -137,24 +138,24 @@ Flickable {
repeat: true repeat: true
onTriggered: { onTriggered: {
const newY = flickable.contentY - flickable.momentumVelocity * 0.016 const newY = flickable.contentY - flickable.momentumVelocity * 0.016;
const maxY = Math.max(0, flickable.contentHeight - flickable.height) const maxY = Math.max(0, flickable.contentHeight - flickable.height);
if (newY < 0 || newY > maxY) { if (newY < 0 || newY > maxY) {
flickable.contentY = newY < 0 ? 0 : maxY flickable.contentY = newY < 0 ? 0 : maxY;
stop() stop();
flickable.isMomentumActive = false flickable.isMomentumActive = false;
flickable.momentumVelocity = 0 flickable.momentumVelocity = 0;
return return;
} }
flickable.contentY = newY flickable.contentY = newY;
flickable.momentumVelocity *= flickable.friction flickable.momentumVelocity *= flickable.friction;
if (Math.abs(flickable.momentumVelocity) < 5) { if (Math.abs(flickable.momentumVelocity) < 5) {
stop() stop();
flickable.isMomentumActive = false flickable.isMomentumActive = false;
flickable.momentumVelocity = 0 flickable.momentumVelocity = 0;
} }
} }
} }

View File

@@ -19,8 +19,8 @@ GridView {
flickableDirection: Flickable.VerticalFlick flickableDirection: Flickable.VerticalFlick
onMovementStarted: { onMovementStarted: {
vbar._scrollBarActive = true vbar._scrollBarActive = true;
vbar.hideTimer.stop() vbar.hideTimer.stop();
} }
onMovementEnded: vbar.hideTimer.restart() onMovementEnded: vbar.hideTimer.restart()
@@ -28,7 +28,7 @@ GridView {
id: wheelHandler id: wheelHandler
property real mouseWheelSpeed: 60 property real mouseWheelSpeed: 60
property real touchpadSpeed: 1.8 property real touchpadSpeed: 2.8
property real momentumRetention: 0.92 property real momentumRetention: 0.92
property real lastWheelTime: 0 property real lastWheelTime: 0
property real momentum: 0 property real momentum: 0
@@ -36,87 +36,89 @@ GridView {
property bool sessionUsedMouseWheel: false property bool sessionUsedMouseWheel: false
function startMomentum() { function startMomentum() {
isMomentumActive = true isMomentumActive = true;
momentumTimer.start() momentumTimer.start();
} }
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => { onWheel: event => {
vbar._scrollBarActive = true vbar._scrollBarActive = true;
vbar.hideTimer.restart() vbar.hideTimer.restart();
const currentTime = Date.now() const currentTime = Date.now();
const timeDelta = currentTime - lastWheelTime const timeDelta = currentTime - lastWheelTime;
lastWheelTime = currentTime lastWheelTime = currentTime;
const deltaY = event.angleDelta.y const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0 const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
const deltaY = event.angleDelta.y;
const isMouseWheel = !hasPixel && hasAngle;
if (isMouseWheel) { if (isMouseWheel) {
sessionUsedMouseWheel = true sessionUsedMouseWheel = true;
momentumTimer.stop() momentumTimer.stop();
isMomentumActive = false isMomentumActive = false;
velocitySamples = [] velocitySamples = [];
momentum = 0 momentum = 0;
momentumVelocity = 0 momentumVelocity = 0;
const lines = Math.floor(Math.abs(deltaY) / 120) const lines = Math.floor(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35 const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35;
let newY = contentY + scrollAmount let newY = contentY + scrollAmount;
newY = Math.max(0, Math.min(contentHeight - height, newY)) newY = Math.max(0, Math.min(contentHeight - height, newY));
if (flicking) { if (flicking) {
cancelFlick() cancelFlick();
} }
contentY = newY contentY = newY;
} else { } else {
sessionUsedMouseWheel = false sessionUsedMouseWheel = false;
momentumTimer.stop() momentumTimer.stop();
isMomentumActive = false isMomentumActive = false;
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * touchpadSpeed : event.angleDelta.y / 120 * cellHeight * 1.2 let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * touchpadSpeed : event.angleDelta.y / 120 * cellHeight * 1.2;
velocitySamples.push({ velocitySamples.push({
"delta": delta, "delta": delta,
"time": currentTime "time": currentTime
}) });
velocitySamples = velocitySamples.filter(s => currentTime - s.time < 100) velocitySamples = velocitySamples.filter(s => currentTime - s.time < 100);
if (velocitySamples.length > 1) { if (velocitySamples.length > 1) {
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0) const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
const timeSpan = currentTime - velocitySamples[0].time const timeSpan = currentTime - velocitySamples[0].time;
if (timeSpan > 0) { if (timeSpan > 0) {
momentumVelocity = Math.max(-maxMomentumVelocity, Math.min(maxMomentumVelocity, totalDelta / timeSpan * 1000)) momentumVelocity = Math.max(-maxMomentumVelocity, Math.min(maxMomentumVelocity, totalDelta / timeSpan * 1000));
} }
} }
if (event.pixelDelta.y !== 0 && timeDelta < 50) { if (event.pixelDelta.y !== 0 && timeDelta < 50) {
momentum = momentum * momentumRetention + delta * 0.15 momentum = momentum * momentumRetention + delta * 0.15;
delta += momentum delta += momentum;
} else { } else {
momentum = 0 momentum = 0;
} }
let newY = contentY - delta let newY = contentY - delta;
newY = Math.max(0, Math.min(contentHeight - height, newY)) newY = Math.max(0, Math.min(contentHeight - height, newY));
if (flicking) { if (flicking) {
cancelFlick() cancelFlick();
} }
contentY = newY contentY = newY;
} }
event.accepted = true event.accepted = true;
} }
onActiveChanged: { onActiveChanged: {
if (!active) { if (!active) {
if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= minMomentumVelocity) { if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= minMomentumVelocity) {
startMomentum() startMomentum();
} else { } else {
velocitySamples = [] velocitySamples = [];
momentumVelocity = 0 momentumVelocity = 0;
} }
} }
} }
@@ -127,24 +129,24 @@ GridView {
interval: 16 interval: 16
repeat: true repeat: true
onTriggered: { onTriggered: {
const newY = contentY - momentumVelocity * 0.016 const newY = contentY - momentumVelocity * 0.016;
const maxY = Math.max(0, contentHeight - height) const maxY = Math.max(0, contentHeight - height);
if (newY < 0 || newY > maxY) { if (newY < 0 || newY > maxY) {
contentY = newY < 0 ? 0 : maxY contentY = newY < 0 ? 0 : maxY;
stop() stop();
isMomentumActive = false isMomentumActive = false;
momentumVelocity = 0 momentumVelocity = 0;
return return;
} }
contentY = newY contentY = newY;
momentumVelocity *= friction momentumVelocity *= friction;
if (Math.abs(momentumVelocity) < 5) { if (Math.abs(momentumVelocity) < 5) {
stop() stop();
isMomentumActive = false isMomentumActive = false;
momentumVelocity = 0 momentumVelocity = 0;
} }
} }
} }

View File

@@ -23,130 +23,132 @@ ListView {
flickableDirection: Flickable.VerticalFlick flickableDirection: Flickable.VerticalFlick
onMovementStarted: { onMovementStarted: {
isUserScrolling = true isUserScrolling = true;
vbar._scrollBarActive = true vbar._scrollBarActive = true;
vbar.hideTimer.stop() vbar.hideTimer.stop();
} }
onMovementEnded: { onMovementEnded: {
isUserScrolling = false isUserScrolling = false;
vbar.hideTimer.restart() vbar.hideTimer.restart();
} }
onContentYChanged: { onContentYChanged: {
if (!justChanged && isUserScrolling) { if (!justChanged && isUserScrolling) {
savedY = contentY savedY = contentY;
} }
justChanged = false justChanged = false;
} }
onModelChanged: { onModelChanged: {
justChanged = true justChanged = true;
contentY = savedY contentY = savedY;
} }
WheelHandler { WheelHandler {
id: wheelHandler id: wheelHandler
property real touchpadSpeed: 1.8 property real touchpadSpeed: 2.8
property real lastWheelTime: 0 property real lastWheelTime: 0
property real momentum: 0 property real momentum: 0
property var velocitySamples: [] property var velocitySamples: []
property bool sessionUsedMouseWheel: false property bool sessionUsedMouseWheel: false
function startMomentum() { function startMomentum() {
isMomentumActive = true isMomentumActive = true;
momentumTimer.start() momentumTimer.start();
} }
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => { onWheel: event => {
isUserScrolling = true isUserScrolling = true;
vbar._scrollBarActive = true vbar._scrollBarActive = true;
vbar.hideTimer.restart() vbar.hideTimer.restart();
const currentTime = Date.now() const currentTime = Date.now();
const timeDelta = currentTime - lastWheelTime const timeDelta = currentTime - lastWheelTime;
lastWheelTime = currentTime lastWheelTime = currentTime;
const deltaY = event.angleDelta.y const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0 const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
const deltaY = event.angleDelta.y;
const isMouseWheel = !hasPixel && hasAngle;
if (isMouseWheel) { if (isMouseWheel) {
sessionUsedMouseWheel = true sessionUsedMouseWheel = true;
momentumTimer.stop() momentumTimer.stop();
isMomentumActive = false isMomentumActive = false;
velocitySamples = [] velocitySamples = [];
momentum = 0 momentum = 0;
momentumVelocity = 0 momentumVelocity = 0;
const lines = Math.floor(Math.abs(deltaY) / 120) const lines = Math.floor(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed;
let newY = listView.contentY + scrollAmount let newY = listView.contentY + scrollAmount;
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY) const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
newY = Math.max(listView.originY, Math.min(maxY, newY)) newY = Math.max(listView.originY, Math.min(maxY, newY));
if (listView.flicking) { if (listView.flicking) {
listView.cancelFlick() listView.cancelFlick();
} }
listView.contentY = newY listView.contentY = newY;
savedY = newY savedY = newY;
} else { } else {
sessionUsedMouseWheel = false sessionUsedMouseWheel = false;
momentumTimer.stop() momentumTimer.stop();
isMomentumActive = false isMomentumActive = false;
let delta = 0 let delta = 0;
if (event.pixelDelta.y !== 0) { if (event.pixelDelta.y !== 0) {
delta = event.pixelDelta.y * touchpadSpeed delta = event.pixelDelta.y * touchpadSpeed;
} else { } else {
delta = event.angleDelta.y / 8 * touchpadSpeed delta = event.angleDelta.y / 8 * touchpadSpeed;
} }
velocitySamples.push({ velocitySamples.push({
"delta": delta, "delta": delta,
"time": currentTime "time": currentTime
}) });
velocitySamples = velocitySamples.filter(s => currentTime - s.time < 100) velocitySamples = velocitySamples.filter(s => currentTime - s.time < 100);
if (velocitySamples.length > 1) { if (velocitySamples.length > 1) {
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0) const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
const timeSpan = currentTime - velocitySamples[0].time const timeSpan = currentTime - velocitySamples[0].time;
if (timeSpan > 0) { if (timeSpan > 0) {
momentumVelocity = Math.max(-maxMomentumVelocity, Math.min(maxMomentumVelocity, totalDelta / timeSpan * 1000)) momentumVelocity = Math.max(-maxMomentumVelocity, Math.min(maxMomentumVelocity, totalDelta / timeSpan * 1000));
} }
} }
if (event.pixelDelta.y !== 0 && timeDelta < 50) { if (event.pixelDelta.y !== 0 && timeDelta < 50) {
momentum = momentum * 0.92 + delta * 0.15 momentum = momentum * 0.92 + delta * 0.15;
delta += momentum delta += momentum;
} else { } else {
momentum = 0 momentum = 0;
} }
let newY = listView.contentY - delta let newY = listView.contentY - delta;
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY) const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
newY = Math.max(listView.originY, Math.min(maxY, newY)) newY = Math.max(listView.originY, Math.min(maxY, newY));
if (listView.flicking) { if (listView.flicking) {
listView.cancelFlick() listView.cancelFlick();
} }
listView.contentY = newY listView.contentY = newY;
savedY = newY savedY = newY;
} }
event.accepted = true event.accepted = true;
} }
onActiveChanged: { onActiveChanged: {
if (!active) { if (!active) {
isUserScrolling = false isUserScrolling = false;
if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= minMomentumVelocity) { if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= minMomentumVelocity) {
startMomentum() startMomentum();
} else { } else {
velocitySamples = [] velocitySamples = [];
momentumVelocity = 0 momentumVelocity = 0;
} }
} }
} }
@@ -158,27 +160,27 @@ ListView {
repeat: true repeat: true
onTriggered: { onTriggered: {
const newY = contentY - momentumVelocity * 0.016 const newY = contentY - momentumVelocity * 0.016;
const maxY = Math.max(0, contentHeight - height + originY) const maxY = Math.max(0, contentHeight - height + originY);
const minY = originY const minY = originY;
if (newY < minY || newY > maxY) { if (newY < minY || newY > maxY) {
contentY = newY < minY ? minY : maxY contentY = newY < minY ? minY : maxY;
savedY = contentY savedY = contentY;
stop() stop();
isMomentumActive = false isMomentumActive = false;
momentumVelocity = 0 momentumVelocity = 0;
return return;
} }
contentY = newY contentY = newY;
savedY = newY savedY = newY;
momentumVelocity *= friction momentumVelocity *= friction;
if (Math.abs(momentumVelocity) < 5) { if (Math.abs(momentumVelocity) < 5) {
stop() stop();
isMomentumActive = false isMomentumActive = false;
momentumVelocity = 0 momentumVelocity = 0;
} }
} }
} }

View File

@@ -286,7 +286,7 @@ Item {
return customKeyboardFocus; return customKeyboardFocus;
if (!shouldBeVisible) if (!shouldBeVisible)
return WlrKeyboardFocus.None; return WlrKeyboardFocus.None;
if (CompositorService.isHyprland) if (CompositorService.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand; return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive; return WlrKeyboardFocus.Exclusive;
} }

View File

@@ -28,6 +28,7 @@ Item {
property bool addingNewKey: false property bool addingNewKey: false
property bool useCustomCompositor: false property bool useCustomCompositor: false
property var _shortcutInhibitor: null property var _shortcutInhibitor: null
property bool _altShiftGhost: false
readonly property bool _shortcutInhibitorAvailable: { readonly property bool _shortcutInhibitorAvailable: {
try { try {
@@ -61,6 +62,11 @@ Item {
Component.onDestruction: _destroyShortcutInhibitor() Component.onDestruction: _destroyShortcutInhibitor()
Component.onCompleted: {
if (isNew && isExpanded)
resetEdits();
}
onIsExpandedChanged: { onIsExpandedChanged: {
if (!isExpanded) if (!isExpanded)
return; return;
@@ -86,7 +92,7 @@ Item {
editDesc = bindData.desc || ""; editDesc = bindData.desc || "";
hasChanges = false; hasChanges = false;
_actionType = Actions.getActionType(editAction); _actionType = Actions.getActionType(editAction);
useCustomCompositor = _actionType === "compositor" && !Actions.isKnownCompositorAction(editAction); useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(editAction);
return; return;
} }
} }
@@ -105,7 +111,7 @@ Item {
editDesc = bindData.desc || ""; editDesc = bindData.desc || "";
hasChanges = false; hasChanges = false;
_actionType = Actions.getActionType(editAction); _actionType = Actions.getActionType(editAction);
useCustomCompositor = _actionType === "compositor" && !Actions.isKnownCompositorAction(editAction); useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(editAction);
} }
function startAddingNewKey() { function startAddingNewKey() {
@@ -590,37 +596,93 @@ Item {
Keys.onPressed: event => { Keys.onPressed: event => {
if (!root.recording) if (!root.recording)
return; return;
if (event.key === Qt.Key_Escape) {
root.stopRecording(); event.accepted = true;
event.accepted = true;
return;
}
switch (event.key) { switch (event.key) {
case Qt.Key_Control: case Qt.Key_Control:
case Qt.Key_Shift: case Qt.Key_Shift:
case Qt.Key_Alt: case Qt.Key_Alt:
case Qt.Key_Meta: case Qt.Key_Meta:
event.accepted = true;
return; return;
} }
const mods = KeyUtils.modsFromEvent(event.modifiers); if (event.key === 0 && (event.modifiers & Qt.AltModifier)) {
const key = KeyUtils.xkbKeyFromQtKey(event.key); root._altShiftGhost = true;
if (key) { return;
root.updateEdit({
key: KeyUtils.formatToken(mods, key)
});
root.stopRecording();
event.accepted = true;
} }
let mods = KeyUtils.modsFromEvent(event.modifiers);
let qtKey = event.key;
if (root._altShiftGhost && (event.modifiers & Qt.AltModifier) && !mods.includes("Shift")) {
mods.push("Shift");
}
root._altShiftGhost = false;
if (qtKey === Qt.Key_Backtab) {
qtKey = Qt.Key_Tab;
if (!mods.includes("Shift"))
mods.push("Shift");
}
const key = KeyUtils.xkbKeyFromQtKey(qtKey);
if (!key) {
console.warn("[KeybindItem] Unknown key:", event.key, "mods:", event.modifiers);
return;
}
root.updateEdit({
key: KeyUtils.formatToken(mods, key)
});
root.stopRecording();
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: !root.recording hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: root.recording ? Qt.CrossCursor : Qt.PointingHandCursor
onClicked: root.startRecording() acceptedButtons: Qt.LeftButton
onClicked: {
if (!root.recording)
root.startRecording();
}
onWheel: wheel => {
if (!root.recording)
return;
wheel.accepted = true;
const mods = [];
if (wheel.modifiers & Qt.ControlModifier)
mods.push("Ctrl");
if (wheel.modifiers & Qt.ShiftModifier)
mods.push("Shift");
if (wheel.modifiers & Qt.AltModifier)
mods.push("Alt");
if (wheel.modifiers & Qt.MetaModifier)
mods.push("Super");
let wheelKey = "";
if (wheel.angleDelta.y > 0)
wheelKey = "WheelScrollUp";
else if (wheel.angleDelta.y < 0)
wheelKey = "WheelScrollDown";
else if (wheel.angleDelta.x > 0)
wheelKey = "WheelScrollRight";
else if (wheel.angleDelta.x < 0)
wheelKey = "WheelScrollLeft";
if (!wheelKey)
return;
root.updateEdit({
key: KeyUtils.formatToken(mods, wheelKey)
});
root.stopRecording();
}
} }
} }
@@ -816,6 +878,69 @@ Item {
} }
} }
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingM
property var dmsArgConfig: {
const action = root.editAction;
if (!action)
return null;
if (action.indexOf("audio increment") !== -1 || action.indexOf("audio decrement") !== -1 || action.indexOf("brightness increment") !== -1 || action.indexOf("brightness decrement") !== -1) {
const parts = action.split(" ");
const lastPart = parts[parts.length - 1];
const hasAmount = /^\d+$/.test(lastPart);
return {
hasAmount: hasAmount,
amount: hasAmount ? lastPart : ""
};
}
return null;
}
visible: root._actionType === "dms" && dmsArgConfig !== null
StyledText {
text: I18n.tr("Amount")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
}
DankTextField {
Layout.preferredWidth: 80
Layout.preferredHeight: 40
placeholderText: "5"
text: parent.dmsArgConfig?.amount || ""
onTextChanged: {
if (!parent.dmsArgConfig)
return;
const action = root.editAction;
const parts = action.split(" ");
const lastPart = parts[parts.length - 1];
const hasOldAmount = /^\d+$/.test(lastPart);
if (hasOldAmount)
parts.pop();
if (text && /^\d+$/.test(text))
parts.push(text);
root.updateEdit({
action: parts.join(" ")
});
}
}
StyledText {
text: "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
Item {
Layout.fillWidth: true
}
}
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Theme.spacingM spacing: Theme.spacingM
@@ -898,6 +1023,154 @@ Item {
} }
} }
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingM
visible: root._actionType === "compositor" && !root.useCustomCompositor && Actions.getActionArgConfig(root.editAction)
property var argConfig: Actions.getActionArgConfig(root.editAction)
property var parsedArgs: Actions.parseCompositorActionArgs(root.editAction)
StyledText {
text: I18n.tr("Options")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingS
DankTextField {
id: argValueField
Layout.fillWidth: true
Layout.preferredHeight: 40
visible: {
const cfg = parent.parent.argConfig;
if (!cfg || !cfg.config || !cfg.config.args)
return false;
const firstArg = cfg.config.args[0];
return firstArg && (firstArg.type === "text" || firstArg.type === "number");
}
placeholderText: {
const cfg = parent.parent.argConfig;
if (!cfg || !cfg.config || !cfg.config.args)
return "";
return cfg.config.args[0]?.placeholder || "";
}
text: parent.parent.parsedArgs?.args?.value || parent.parent.parsedArgs?.args?.index || ""
onTextChanged: {
const cfg = parent.parent.argConfig;
if (!cfg)
return;
const base = parent.parent.parsedArgs?.base || root.editAction.split(" ")[0];
const args = cfg.config.args[0]?.type === "number" ? {
index: text
} : {
value: text
};
root.updateEdit({
action: Actions.buildCompositorAction(base, args)
});
}
}
RowLayout {
visible: {
const cfg = parent.parent.argConfig;
return cfg && cfg.base === "move-column-to-workspace";
}
spacing: Theme.spacingXS
DankToggle {
id: focusToggle
checked: parent.parent.parent.parsedArgs?.args?.focus === true
onCheckedChanged: {
const cfg = parent.parent.parent.argConfig;
if (!cfg)
return;
const parsed = parent.parent.parent.parsedArgs;
const args = {
index: parsed?.args?.index || "",
focus: checked
};
root.updateEdit({
action: Actions.buildCompositorAction("move-column-to-workspace", args)
});
}
}
StyledText {
text: I18n.tr("Follow focus")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
RowLayout {
visible: {
const cfg = parent.parent.argConfig;
return cfg && cfg.base && cfg.base.startsWith("screenshot");
}
spacing: Theme.spacingM
RowLayout {
spacing: Theme.spacingXS
DankToggle {
id: writeToDiskToggle
checked: parent.parent.parent.parent.parsedArgs?.args?.opts?.["write-to-disk"] === "true"
onCheckedChanged: {
const parsed = parent.parent.parent.parent.parsedArgs;
const base = parsed?.base || "screenshot";
const opts = parsed?.args?.opts || {};
opts["write-to-disk"] = checked ? "true" : "";
root.updateEdit({
action: Actions.buildCompositorAction(base, {
opts: opts
})
});
}
}
StyledText {
text: I18n.tr("Save")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
RowLayout {
spacing: Theme.spacingXS
DankToggle {
id: showPointerToggle
checked: parent.parent.parent.parent.parsedArgs?.args?.opts?.["show-pointer"] === "true"
onCheckedChanged: {
const parsed = parent.parent.parent.parent.parsedArgs;
const base = parsed?.base || "screenshot";
const opts = parsed?.args?.opts || {};
opts["show-pointer"] = checked ? "true" : "";
root.updateEdit({
action: Actions.buildCompositorAction(base, {
opts: opts
})
});
}
}
StyledText {
text: I18n.tr("Pointer")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
}
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Theme.spacingM spacing: Theme.spacingM