mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-15 15:45:20 -04:00
feat(window-rules): view & convert external rules to DMS
- Read and convert external compositor rules into editable DMS rules - Preserve niri multi-match rules and add match editor - niri background-effect (blur/xray/noise/saturation) support
This commit is contained in:
@@ -14,6 +14,18 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type NiriMatch struct {
|
||||||
|
AppID string
|
||||||
|
Title string
|
||||||
|
IsFloating *bool
|
||||||
|
IsActive *bool
|
||||||
|
IsFocused *bool
|
||||||
|
IsActiveInColumn *bool
|
||||||
|
IsWindowCastTarget *bool
|
||||||
|
IsUrgent *bool
|
||||||
|
AtStartup *bool
|
||||||
|
}
|
||||||
|
|
||||||
type NiriWindowRule struct {
|
type NiriWindowRule struct {
|
||||||
MatchAppID string
|
MatchAppID string
|
||||||
MatchTitle string
|
MatchTitle string
|
||||||
@@ -24,6 +36,7 @@ type NiriWindowRule struct {
|
|||||||
MatchIsWindowCastTarget *bool
|
MatchIsWindowCastTarget *bool
|
||||||
MatchIsUrgent *bool
|
MatchIsUrgent *bool
|
||||||
MatchAtStartup *bool
|
MatchAtStartup *bool
|
||||||
|
Matches []NiriMatch
|
||||||
Opacity *float64
|
Opacity *float64
|
||||||
OpenFloating *bool
|
OpenFloating *bool
|
||||||
OpenMaximized *bool
|
OpenMaximized *bool
|
||||||
@@ -50,6 +63,10 @@ type NiriWindowRule struct {
|
|||||||
FocusRingOff *bool
|
FocusRingOff *bool
|
||||||
BorderOff *bool
|
BorderOff *bool
|
||||||
DrawBorderWithBg *bool
|
DrawBorderWithBg *bool
|
||||||
|
BgBlur *bool
|
||||||
|
BgXray *bool
|
||||||
|
BgNoise *float64
|
||||||
|
BgSaturation *float64
|
||||||
Source string
|
Source string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +208,7 @@ func (p *NiriRulesParser) parseWindowRuleNode(node *document.Node) {
|
|||||||
|
|
||||||
switch childName {
|
switch childName {
|
||||||
case "match":
|
case "match":
|
||||||
p.parseMatchNode(child, &rule)
|
rule.Matches = append(rule.Matches, p.parseMatchNode(child))
|
||||||
case "opacity":
|
case "opacity":
|
||||||
if len(child.Arguments) > 0 {
|
if len(child.Arguments) > 0 {
|
||||||
val := child.Arguments[0].ResolvedValue()
|
val := child.Arguments[0].ResolvedValue()
|
||||||
@@ -297,9 +314,24 @@ func (p *NiriRulesParser) parseWindowRuleNode(node *document.Node) {
|
|||||||
case "draw-border-with-background":
|
case "draw-border-with-background":
|
||||||
b := p.parseBoolArg(child)
|
b := p.parseBoolArg(child)
|
||||||
rule.DrawBorderWithBg = &b
|
rule.DrawBorderWithBg = &b
|
||||||
|
case "background-effect":
|
||||||
|
p.parseBackgroundEffectNode(child, &rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(rule.Matches) > 0 {
|
||||||
|
first := rule.Matches[0]
|
||||||
|
rule.MatchAppID = first.AppID
|
||||||
|
rule.MatchTitle = first.Title
|
||||||
|
rule.MatchIsFloating = first.IsFloating
|
||||||
|
rule.MatchIsActive = first.IsActive
|
||||||
|
rule.MatchIsFocused = first.IsFocused
|
||||||
|
rule.MatchIsActiveInColumn = first.IsActiveInColumn
|
||||||
|
rule.MatchIsWindowCastTarget = first.IsWindowCastTarget
|
||||||
|
rule.MatchIsUrgent = first.IsUrgent
|
||||||
|
rule.MatchAtStartup = first.AtStartup
|
||||||
|
}
|
||||||
|
|
||||||
p.rules = append(p.rules, rule)
|
p.rules = append(p.rules, rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,45 +358,47 @@ func (p *NiriRulesParser) parseSizeNode(node *document.Node) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NiriRulesParser) parseMatchNode(node *document.Node, rule *NiriWindowRule) {
|
func (p *NiriRulesParser) parseMatchNode(node *document.Node) NiriMatch {
|
||||||
|
m := NiriMatch{}
|
||||||
if node.Properties == nil {
|
if node.Properties == nil {
|
||||||
return
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
if val, ok := node.Properties.Get("app-id"); ok {
|
if val, ok := node.Properties.Get("app-id"); ok {
|
||||||
rule.MatchAppID = val.ValueString()
|
m.AppID = val.ValueString()
|
||||||
}
|
}
|
||||||
if val, ok := node.Properties.Get("title"); ok {
|
if val, ok := node.Properties.Get("title"); ok {
|
||||||
rule.MatchTitle = val.ValueString()
|
m.Title = val.ValueString()
|
||||||
}
|
}
|
||||||
if val, ok := node.Properties.Get("is-floating"); ok {
|
if val, ok := node.Properties.Get("is-floating"); ok {
|
||||||
b := val.ValueString() == "true"
|
b := val.ValueString() == "true"
|
||||||
rule.MatchIsFloating = &b
|
m.IsFloating = &b
|
||||||
}
|
}
|
||||||
if val, ok := node.Properties.Get("is-active"); ok {
|
if val, ok := node.Properties.Get("is-active"); ok {
|
||||||
b := val.ValueString() == "true"
|
b := val.ValueString() == "true"
|
||||||
rule.MatchIsActive = &b
|
m.IsActive = &b
|
||||||
}
|
}
|
||||||
if val, ok := node.Properties.Get("is-focused"); ok {
|
if val, ok := node.Properties.Get("is-focused"); ok {
|
||||||
b := val.ValueString() == "true"
|
b := val.ValueString() == "true"
|
||||||
rule.MatchIsFocused = &b
|
m.IsFocused = &b
|
||||||
}
|
}
|
||||||
if val, ok := node.Properties.Get("is-active-in-column"); ok {
|
if val, ok := node.Properties.Get("is-active-in-column"); ok {
|
||||||
b := val.ValueString() == "true"
|
b := val.ValueString() == "true"
|
||||||
rule.MatchIsActiveInColumn = &b
|
m.IsActiveInColumn = &b
|
||||||
}
|
}
|
||||||
if val, ok := node.Properties.Get("is-window-cast-target"); ok {
|
if val, ok := node.Properties.Get("is-window-cast-target"); ok {
|
||||||
b := val.ValueString() == "true"
|
b := val.ValueString() == "true"
|
||||||
rule.MatchIsWindowCastTarget = &b
|
m.IsWindowCastTarget = &b
|
||||||
}
|
}
|
||||||
if val, ok := node.Properties.Get("is-urgent"); ok {
|
if val, ok := node.Properties.Get("is-urgent"); ok {
|
||||||
b := val.ValueString() == "true"
|
b := val.ValueString() == "true"
|
||||||
rule.MatchIsUrgent = &b
|
m.IsUrgent = &b
|
||||||
}
|
}
|
||||||
if val, ok := node.Properties.Get("at-startup"); ok {
|
if val, ok := node.Properties.Get("at-startup"); ok {
|
||||||
b := val.ValueString() == "true"
|
b := val.ValueString() == "true"
|
||||||
rule.MatchAtStartup = &b
|
m.AtStartup = &b
|
||||||
}
|
}
|
||||||
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NiriRulesParser) parseBorderNode(node *document.Node, rule *NiriWindowRule) {
|
func (p *NiriRulesParser) parseBorderNode(node *document.Node, rule *NiriWindowRule) {
|
||||||
@@ -385,6 +419,45 @@ func (p *NiriRulesParser) parseBorderNode(node *document.Node, rule *NiriWindowR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) parseBackgroundEffectNode(node *document.Node, rule *NiriWindowRule) {
|
||||||
|
if node.Children == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range node.Children {
|
||||||
|
switch child.Name.String() {
|
||||||
|
case "blur":
|
||||||
|
b := p.parseBoolArg(child)
|
||||||
|
rule.BgBlur = &b
|
||||||
|
case "xray":
|
||||||
|
b := p.parseBoolArg(child)
|
||||||
|
rule.BgXray = &b
|
||||||
|
case "noise":
|
||||||
|
if f, ok := p.parseFloatArg(child); ok {
|
||||||
|
rule.BgNoise = &f
|
||||||
|
}
|
||||||
|
case "saturation":
|
||||||
|
if f, ok := p.parseFloatArg(child); ok {
|
||||||
|
rule.BgSaturation = &f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) parseFloatArg(node *document.Node) (float64, bool) {
|
||||||
|
if len(node.Arguments) == 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
val := node.Arguments[0].ResolvedValue()
|
||||||
|
switch v := val.(type) {
|
||||||
|
case float64:
|
||||||
|
return v, true
|
||||||
|
case int64:
|
||||||
|
return float64(v), true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
func (p *NiriRulesParser) parseFocusRingNode(node *document.Node, rule *NiriWindowRule) {
|
func (p *NiriRulesParser) parseFocusRingNode(node *document.Node, rule *NiriWindowRule) {
|
||||||
if node.Children == nil {
|
if node.Children == nil {
|
||||||
return
|
return
|
||||||
@@ -461,6 +534,27 @@ func ParseNiriWindowRules(configDir string) (*NiriRulesParseResult, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertNiriMatches(matches []NiriMatch) []windowrules.MatchCriteria {
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]windowrules.MatchCriteria, 0, len(matches))
|
||||||
|
for _, m := range matches {
|
||||||
|
result = append(result, windowrules.MatchCriteria{
|
||||||
|
AppID: m.AppID,
|
||||||
|
Title: m.Title,
|
||||||
|
IsFloating: m.IsFloating,
|
||||||
|
IsActive: m.IsActive,
|
||||||
|
IsFocused: m.IsFocused,
|
||||||
|
IsActiveInColumn: m.IsActiveInColumn,
|
||||||
|
IsWindowCastTarget: m.IsWindowCastTarget,
|
||||||
|
IsUrgent: m.IsUrgent,
|
||||||
|
AtStartup: m.AtStartup,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func ConvertNiriRulesToWindowRules(niriRules []NiriWindowRule) []windowrules.WindowRule {
|
func ConvertNiriRulesToWindowRules(niriRules []NiriWindowRule) []windowrules.WindowRule {
|
||||||
result := make([]windowrules.WindowRule, 0, len(niriRules))
|
result := make([]windowrules.WindowRule, 0, len(niriRules))
|
||||||
for i, nr := range niriRules {
|
for i, nr := range niriRules {
|
||||||
@@ -479,6 +573,7 @@ func ConvertNiriRulesToWindowRules(niriRules []NiriWindowRule) []windowrules.Win
|
|||||||
IsUrgent: nr.MatchIsUrgent,
|
IsUrgent: nr.MatchIsUrgent,
|
||||||
AtStartup: nr.MatchAtStartup,
|
AtStartup: nr.MatchAtStartup,
|
||||||
},
|
},
|
||||||
|
Matches: convertNiriMatches(nr.Matches),
|
||||||
Actions: windowrules.Actions{
|
Actions: windowrules.Actions{
|
||||||
Opacity: nr.Opacity,
|
Opacity: nr.Opacity,
|
||||||
OpenFloating: nr.OpenFloating,
|
OpenFloating: nr.OpenFloating,
|
||||||
@@ -506,6 +601,10 @@ func ConvertNiriRulesToWindowRules(niriRules []NiriWindowRule) []windowrules.Win
|
|||||||
FocusRingOff: nr.FocusRingOff,
|
FocusRingOff: nr.FocusRingOff,
|
||||||
BorderOff: nr.BorderOff,
|
BorderOff: nr.BorderOff,
|
||||||
DrawBorderWithBg: nr.DrawBorderWithBg,
|
DrawBorderWithBg: nr.DrawBorderWithBg,
|
||||||
|
BackgroundBlur: nr.BgBlur,
|
||||||
|
BackgroundXray: nr.BgXray,
|
||||||
|
BackgroundNoise: nr.BgNoise,
|
||||||
|
BackgroundSaturation: nr.BgSaturation,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
result = append(result, wr)
|
result = append(result, wr)
|
||||||
@@ -684,6 +783,7 @@ func (p *NiriWritableProvider) LoadDMSRules() ([]windowrules.WindowRule, error)
|
|||||||
IsUrgent: nr.MatchIsUrgent,
|
IsUrgent: nr.MatchIsUrgent,
|
||||||
AtStartup: nr.MatchAtStartup,
|
AtStartup: nr.MatchAtStartup,
|
||||||
},
|
},
|
||||||
|
Matches: convertNiriMatches(nr.Matches),
|
||||||
Actions: windowrules.Actions{
|
Actions: windowrules.Actions{
|
||||||
Opacity: nr.Opacity,
|
Opacity: nr.Opacity,
|
||||||
OpenFloating: nr.OpenFloating,
|
OpenFloating: nr.OpenFloating,
|
||||||
@@ -711,6 +811,10 @@ func (p *NiriWritableProvider) LoadDMSRules() ([]windowrules.WindowRule, error)
|
|||||||
FocusRingOff: nr.FocusRingOff,
|
FocusRingOff: nr.FocusRingOff,
|
||||||
BorderOff: nr.BorderOff,
|
BorderOff: nr.BorderOff,
|
||||||
DrawBorderWithBg: nr.DrawBorderWithBg,
|
DrawBorderWithBg: nr.DrawBorderWithBg,
|
||||||
|
BackgroundBlur: nr.BgBlur,
|
||||||
|
BackgroundXray: nr.BgXray,
|
||||||
|
BackgroundNoise: nr.BgNoise,
|
||||||
|
BackgroundSaturation: nr.BgSaturation,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,44 +844,54 @@ func (p *NiriWritableProvider) writeDMSRules(rules []windowrules.WindowRule) err
|
|||||||
return os.WriteFile(rulesPath, []byte(strings.Join(lines, "\n")), 0644)
|
return os.WriteFile(rulesPath, []byte(strings.Join(lines, "\n")), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatNiriMatchLine(m windowrules.MatchCriteria) (string, bool) {
|
||||||
|
var matchProps []string
|
||||||
|
if m.AppID != "" {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("app-id=%q", m.AppID))
|
||||||
|
}
|
||||||
|
if m.Title != "" {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("title=%q", m.Title))
|
||||||
|
}
|
||||||
|
if m.IsFloating != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("is-floating=%t", *m.IsFloating))
|
||||||
|
}
|
||||||
|
if m.IsActive != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("is-active=%t", *m.IsActive))
|
||||||
|
}
|
||||||
|
if m.IsFocused != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("is-focused=%t", *m.IsFocused))
|
||||||
|
}
|
||||||
|
if m.IsActiveInColumn != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("is-active-in-column=%t", *m.IsActiveInColumn))
|
||||||
|
}
|
||||||
|
if m.IsWindowCastTarget != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("is-window-cast-target=%t", *m.IsWindowCastTarget))
|
||||||
|
}
|
||||||
|
if m.IsUrgent != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("is-urgent=%t", *m.IsUrgent))
|
||||||
|
}
|
||||||
|
if m.AtStartup != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("at-startup=%t", *m.AtStartup))
|
||||||
|
}
|
||||||
|
if len(matchProps) == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return " match " + strings.Join(matchProps, " "), true
|
||||||
|
}
|
||||||
|
|
||||||
func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
|
func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
|
||||||
var lines []string
|
var lines []string
|
||||||
lines = append(lines, fmt.Sprintf("// @id=%s @name=%s", rule.ID, rule.Name))
|
lines = append(lines, fmt.Sprintf("// @id=%s @name=%s", rule.ID, rule.Name))
|
||||||
lines = append(lines, "window-rule {")
|
lines = append(lines, "window-rule {")
|
||||||
|
|
||||||
m := rule.MatchCriteria
|
matches := rule.Matches
|
||||||
if m.AppID != "" || m.Title != "" || m.IsFloating != nil || m.IsActive != nil ||
|
if len(matches) == 0 {
|
||||||
m.IsFocused != nil || m.IsActiveInColumn != nil || m.IsWindowCastTarget != nil ||
|
matches = []windowrules.MatchCriteria{rule.MatchCriteria}
|
||||||
m.IsUrgent != nil || m.AtStartup != nil {
|
}
|
||||||
var matchProps []string
|
for _, m := range matches {
|
||||||
if m.AppID != "" {
|
if line, ok := formatNiriMatchLine(m); ok {
|
||||||
matchProps = append(matchProps, fmt.Sprintf("app-id=%q", m.AppID))
|
lines = append(lines, line)
|
||||||
}
|
}
|
||||||
if m.Title != "" {
|
|
||||||
matchProps = append(matchProps, fmt.Sprintf("title=%q", m.Title))
|
|
||||||
}
|
|
||||||
if m.IsFloating != nil {
|
|
||||||
matchProps = append(matchProps, fmt.Sprintf("is-floating=%t", *m.IsFloating))
|
|
||||||
}
|
|
||||||
if m.IsActive != nil {
|
|
||||||
matchProps = append(matchProps, fmt.Sprintf("is-active=%t", *m.IsActive))
|
|
||||||
}
|
|
||||||
if m.IsFocused != nil {
|
|
||||||
matchProps = append(matchProps, fmt.Sprintf("is-focused=%t", *m.IsFocused))
|
|
||||||
}
|
|
||||||
if m.IsActiveInColumn != nil {
|
|
||||||
matchProps = append(matchProps, fmt.Sprintf("is-active-in-column=%t", *m.IsActiveInColumn))
|
|
||||||
}
|
|
||||||
if m.IsWindowCastTarget != nil {
|
|
||||||
matchProps = append(matchProps, fmt.Sprintf("is-window-cast-target=%t", *m.IsWindowCastTarget))
|
|
||||||
}
|
|
||||||
if m.IsUrgent != nil {
|
|
||||||
matchProps = append(matchProps, fmt.Sprintf("is-urgent=%t", *m.IsUrgent))
|
|
||||||
}
|
|
||||||
if m.AtStartup != nil {
|
|
||||||
matchProps = append(matchProps, fmt.Sprintf("at-startup=%t", *m.AtStartup))
|
|
||||||
}
|
|
||||||
lines = append(lines, " match "+strings.Join(matchProps, " "))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a := rule.Actions
|
a := rule.Actions
|
||||||
@@ -858,10 +972,31 @@ func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
|
|||||||
lines = append(lines, fmt.Sprintf(" draw-border-with-background %t", *a.DrawBorderWithBg))
|
lines = append(lines, fmt.Sprintf(" draw-border-with-background %t", *a.DrawBorderWithBg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.BackgroundBlur != nil || a.BackgroundXray != nil || a.BackgroundNoise != nil || a.BackgroundSaturation != nil {
|
||||||
|
lines = append(lines, " background-effect {")
|
||||||
|
if a.BackgroundBlur != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf(" blur %t", *a.BackgroundBlur))
|
||||||
|
}
|
||||||
|
if a.BackgroundXray != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf(" xray %t", *a.BackgroundXray))
|
||||||
|
}
|
||||||
|
if a.BackgroundNoise != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf(" noise %s", formatFloat(*a.BackgroundNoise)))
|
||||||
|
}
|
||||||
|
if a.BackgroundSaturation != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf(" saturation %s", formatFloat(*a.BackgroundSaturation)))
|
||||||
|
}
|
||||||
|
lines = append(lines, " }")
|
||||||
|
}
|
||||||
|
|
||||||
lines = append(lines, "}")
|
lines = append(lines, "}")
|
||||||
return strings.Join(lines, "\n")
|
return strings.Join(lines, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatFloat(f float64) string {
|
||||||
|
return strconv.FormatFloat(f, 'f', -1, 64)
|
||||||
|
}
|
||||||
|
|
||||||
func formatSizeProperty(name, value string) string {
|
func formatSizeProperty(name, value string) string {
|
||||||
parts := strings.SplitN(value, " ", 2)
|
parts := strings.SplitN(value, " ", 2)
|
||||||
if len(parts) == 2 {
|
if len(parts) == 2 {
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ type Actions struct {
|
|||||||
FocusRingOff *bool `json:"focusRingOff,omitempty"`
|
FocusRingOff *bool `json:"focusRingOff,omitempty"`
|
||||||
BorderOff *bool `json:"borderOff,omitempty"`
|
BorderOff *bool `json:"borderOff,omitempty"`
|
||||||
DrawBorderWithBg *bool `json:"drawBorderWithBackground,omitempty"`
|
DrawBorderWithBg *bool `json:"drawBorderWithBackground,omitempty"`
|
||||||
|
BackgroundBlur *bool `json:"backgroundBlur,omitempty"`
|
||||||
|
BackgroundXray *bool `json:"backgroundXray,omitempty"`
|
||||||
|
BackgroundNoise *float64 `json:"backgroundNoise,omitempty"`
|
||||||
|
BackgroundSaturation *float64 `json:"backgroundSaturation,omitempty"`
|
||||||
Size string `json:"size,omitempty"`
|
Size string `json:"size,omitempty"`
|
||||||
Move string `json:"move,omitempty"`
|
Move string `json:"move,omitempty"`
|
||||||
Monitor string `json:"monitor,omitempty"`
|
Monitor string `json:"monitor,omitempty"`
|
||||||
@@ -62,12 +66,13 @@ type Actions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WindowRule struct {
|
type WindowRule struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
MatchCriteria MatchCriteria `json:"matchCriteria"`
|
MatchCriteria MatchCriteria `json:"matchCriteria"`
|
||||||
Actions Actions `json:"actions"`
|
Matches []MatchCriteria `json:"matches,omitempty"`
|
||||||
Source string `json:"source,omitempty"`
|
Actions Actions `json:"actions"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DMSRulesStatus struct {
|
type DMSRulesStatus struct {
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ FloatingWindow {
|
|||||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||||
readonly property int sectionSpacing: Theme.spacingL
|
readonly property int sectionSpacing: Theme.spacingL
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: extraMatchModel
|
||||||
|
}
|
||||||
|
|
||||||
objectName: "windowRuleModal"
|
objectName: "windowRuleModal"
|
||||||
title: isEditMode ? I18n.tr("Edit Window Rule") : I18n.tr("Create Window Rule")
|
title: isEditMode ? I18n.tr("Edit Window Rule") : I18n.tr("Create Window Rule")
|
||||||
minimumSize: Qt.size(500, 600)
|
minimumSize: Qt.size(500, 600)
|
||||||
@@ -31,6 +35,18 @@ FloatingWindow {
|
|||||||
nameInput.text = "";
|
nameInput.text = "";
|
||||||
appIdInput.text = "";
|
appIdInput.text = "";
|
||||||
titleInput.text = "";
|
titleInput.text = "";
|
||||||
|
extraMatchModel.clear();
|
||||||
|
condFloating.triState = 0;
|
||||||
|
condActive.triState = 0;
|
||||||
|
condFocused.triState = 0;
|
||||||
|
condActiveInColumn.triState = 0;
|
||||||
|
condCastTarget.triState = 0;
|
||||||
|
condUrgent.triState = 0;
|
||||||
|
condAtStartup.triState = 0;
|
||||||
|
condXwayland.triState = 0;
|
||||||
|
condFullscreen.triState = 0;
|
||||||
|
condPinned.triState = 0;
|
||||||
|
condInitialised.triState = 0;
|
||||||
opacityEnabled.checked = false;
|
opacityEnabled.checked = false;
|
||||||
opacitySlider.value = 100;
|
opacitySlider.value = 100;
|
||||||
floatingToggle.checked = false;
|
floatingToggle.checked = false;
|
||||||
@@ -52,6 +68,12 @@ FloatingWindow {
|
|||||||
clipToGeometryToggle.checked = false;
|
clipToGeometryToggle.checked = false;
|
||||||
tiledStateToggle.checked = false;
|
tiledStateToggle.checked = false;
|
||||||
drawBorderBgToggle.checked = false;
|
drawBorderBgToggle.checked = false;
|
||||||
|
blurCond.triState = 0;
|
||||||
|
xrayCond.triState = 0;
|
||||||
|
noiseEnabled.checked = false;
|
||||||
|
noiseSlider.value = 5;
|
||||||
|
saturationEnabled.checked = false;
|
||||||
|
saturationSlider.value = 100;
|
||||||
minWidthInput.text = "";
|
minWidthInput.text = "";
|
||||||
maxWidthInput.text = "";
|
maxWidthInput.text = "";
|
||||||
minHeightInput.text = "";
|
minHeightInput.text = "";
|
||||||
@@ -84,18 +106,39 @@ FloatingWindow {
|
|||||||
Qt.callLater(() => nameInput.forceActiveFocus());
|
Qt.callLater(() => nameInput.forceActiveFocus());
|
||||||
}
|
}
|
||||||
|
|
||||||
function showEdit(rule) {
|
function triFromBool(v) {
|
||||||
if (!rule) {
|
if (v === true)
|
||||||
show();
|
return 1;
|
||||||
return;
|
if (v === false)
|
||||||
}
|
return 2;
|
||||||
editingRule = rule;
|
return 0;
|
||||||
resetForm();
|
}
|
||||||
|
|
||||||
|
function populateForm(rule) {
|
||||||
nameInput.text = rule.name || "";
|
nameInput.text = rule.name || "";
|
||||||
const match = rule.matchCriteria || {};
|
const matchList = (rule.matches && rule.matches.length > 0) ? rule.matches : [rule.matchCriteria || {}];
|
||||||
|
const match = matchList[0] || {};
|
||||||
appIdInput.text = match.appId || "";
|
appIdInput.text = match.appId || "";
|
||||||
titleInput.text = match.title || "";
|
titleInput.text = match.title || "";
|
||||||
|
extraMatchModel.clear();
|
||||||
|
for (let i = 1; i < matchList.length; i++) {
|
||||||
|
extraMatchModel.append({
|
||||||
|
"rowAppId": matchList[i].appId || "",
|
||||||
|
"rowTitle": matchList[i].title || ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
condFloating.triState = triFromBool(match.isFloating);
|
||||||
|
condActive.triState = triFromBool(match.isActive);
|
||||||
|
condFocused.triState = triFromBool(match.isFocused);
|
||||||
|
condActiveInColumn.triState = triFromBool(match.isActiveInColumn);
|
||||||
|
condCastTarget.triState = triFromBool(match.isWindowCastTarget);
|
||||||
|
condUrgent.triState = triFromBool(match.isUrgent);
|
||||||
|
condAtStartup.triState = triFromBool(match.atStartup);
|
||||||
|
condXwayland.triState = triFromBool(match.xwayland);
|
||||||
|
condFullscreen.triState = triFromBool(match.fullscreen);
|
||||||
|
condPinned.triState = triFromBool(match.pinned);
|
||||||
|
condInitialised.triState = triFromBool(match.initialised);
|
||||||
|
|
||||||
const actions = rule.actions || {};
|
const actions = rule.actions || {};
|
||||||
const hasOpacity = actions.opacity !== undefined && actions.opacity !== null;
|
const hasOpacity = actions.opacity !== undefined && actions.opacity !== null;
|
||||||
@@ -131,6 +174,15 @@ FloatingWindow {
|
|||||||
|
|
||||||
drawBorderBgToggle.checked = actions.drawBorderWithBackground || false;
|
drawBorderBgToggle.checked = actions.drawBorderWithBackground || false;
|
||||||
|
|
||||||
|
xrayCond.triState = triFromBool(actions.backgroundXray);
|
||||||
|
blurCond.triState = triFromBool(actions.backgroundBlur);
|
||||||
|
const hasNoise = actions.backgroundNoise !== undefined && actions.backgroundNoise !== null;
|
||||||
|
noiseEnabled.checked = hasNoise;
|
||||||
|
noiseSlider.value = hasNoise ? Math.round(actions.backgroundNoise * 100) : 5;
|
||||||
|
const hasSaturation = actions.backgroundSaturation !== undefined && actions.backgroundSaturation !== null;
|
||||||
|
saturationEnabled.checked = hasSaturation;
|
||||||
|
saturationSlider.value = hasSaturation ? Math.round(actions.backgroundSaturation * 100) : 100;
|
||||||
|
|
||||||
minWidthInput.text = actions.minWidth !== undefined ? String(actions.minWidth) : "";
|
minWidthInput.text = actions.minWidth !== undefined ? String(actions.minWidth) : "";
|
||||||
maxWidthInput.text = actions.maxWidth !== undefined ? String(actions.maxWidth) : "";
|
maxWidthInput.text = actions.maxWidth !== undefined ? String(actions.maxWidth) : "";
|
||||||
minHeightInput.text = actions.minHeight !== undefined ? String(actions.minHeight) : "";
|
minHeightInput.text = actions.minHeight !== undefined ? String(actions.minHeight) : "";
|
||||||
@@ -150,7 +202,28 @@ FloatingWindow {
|
|||||||
moveInput.text = actions.move || "";
|
moveInput.text = actions.move || "";
|
||||||
monitorInput.text = actions.monitor || "";
|
monitorInput.text = actions.monitor || "";
|
||||||
hyprWorkspaceInput.text = actions.workspace || "";
|
hyprWorkspaceInput.text = actions.workspace || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEdit(rule) {
|
||||||
|
if (!rule) {
|
||||||
|
show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editingRule = rule;
|
||||||
|
resetForm();
|
||||||
|
populateForm(rule);
|
||||||
|
visible = true;
|
||||||
|
Qt.callLater(() => nameInput.forceActiveFocus());
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCopy(rule) {
|
||||||
|
if (!rule) {
|
||||||
|
show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editingRule = null;
|
||||||
|
resetForm();
|
||||||
|
populateForm(rule);
|
||||||
visible = true;
|
visible = true;
|
||||||
Qt.callLater(() => nameInput.forceActiveFocus());
|
Qt.callLater(() => nameInput.forceActiveFocus());
|
||||||
}
|
}
|
||||||
@@ -161,6 +234,13 @@ FloatingWindow {
|
|||||||
targetWindow = null;
|
targetWindow = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyCond(obj, key, triState) {
|
||||||
|
if (triState === 1)
|
||||||
|
obj[key] = true;
|
||||||
|
else if (triState === 2)
|
||||||
|
obj[key] = false;
|
||||||
|
}
|
||||||
|
|
||||||
function submitAndClose() {
|
function submitAndClose() {
|
||||||
const matchCriteria = {};
|
const matchCriteria = {};
|
||||||
if (appIdInput.text.trim())
|
if (appIdInput.text.trim())
|
||||||
@@ -168,6 +248,38 @@ FloatingWindow {
|
|||||||
if (titleInput.text.trim())
|
if (titleInput.text.trim())
|
||||||
matchCriteria.title = titleInput.text.trim();
|
matchCriteria.title = titleInput.text.trim();
|
||||||
|
|
||||||
|
applyCond(matchCriteria, "isFloating", condFloating.triState);
|
||||||
|
if (isNiri) {
|
||||||
|
applyCond(matchCriteria, "isActive", condActive.triState);
|
||||||
|
applyCond(matchCriteria, "isFocused", condFocused.triState);
|
||||||
|
applyCond(matchCriteria, "isActiveInColumn", condActiveInColumn.triState);
|
||||||
|
applyCond(matchCriteria, "isWindowCastTarget", condCastTarget.triState);
|
||||||
|
applyCond(matchCriteria, "isUrgent", condUrgent.triState);
|
||||||
|
applyCond(matchCriteria, "atStartup", condAtStartup.triState);
|
||||||
|
}
|
||||||
|
if (isHyprland) {
|
||||||
|
applyCond(matchCriteria, "xwayland", condXwayland.triState);
|
||||||
|
applyCond(matchCriteria, "fullscreen", condFullscreen.triState);
|
||||||
|
applyCond(matchCriteria, "pinned", condPinned.triState);
|
||||||
|
applyCond(matchCriteria, "initialised", condInitialised.triState);
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = [];
|
||||||
|
if (Object.keys(matchCriteria).length > 0)
|
||||||
|
matches.push(matchCriteria);
|
||||||
|
if (isNiri) {
|
||||||
|
for (let i = 0; i < extraMatchModel.count; i++) {
|
||||||
|
const row = extraMatchModel.get(i);
|
||||||
|
const m = {};
|
||||||
|
if ((row.rowAppId || "").trim())
|
||||||
|
m.appId = row.rowAppId.trim();
|
||||||
|
if ((row.rowTitle || "").trim())
|
||||||
|
m.title = row.rowTitle.trim();
|
||||||
|
if (Object.keys(m).length > 0)
|
||||||
|
matches.push(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const actions = {};
|
const actions = {};
|
||||||
|
|
||||||
if (opacityEnabled.checked)
|
if (opacityEnabled.checked)
|
||||||
@@ -206,6 +318,14 @@ FloatingWindow {
|
|||||||
actions.tiledState = true;
|
actions.tiledState = true;
|
||||||
if (drawBorderBgToggle.checked && isNiri)
|
if (drawBorderBgToggle.checked && isNiri)
|
||||||
actions.drawBorderWithBackground = true;
|
actions.drawBorderWithBackground = true;
|
||||||
|
if (isNiri) {
|
||||||
|
applyCond(actions, "backgroundBlur", blurCond.triState);
|
||||||
|
applyCond(actions, "backgroundXray", xrayCond.triState);
|
||||||
|
}
|
||||||
|
if (noiseEnabled.checked && isNiri)
|
||||||
|
actions.backgroundNoise = noiseSlider.value / 100;
|
||||||
|
if (saturationEnabled.checked && isNiri)
|
||||||
|
actions.backgroundSaturation = saturationSlider.value / 100;
|
||||||
|
|
||||||
const minW = parseInt(minWidthInput.text);
|
const minW = parseInt(minWidthInput.text);
|
||||||
const maxW = parseInt(maxWidthInput.text);
|
const maxW = parseInt(maxWidthInput.text);
|
||||||
@@ -260,6 +380,8 @@ FloatingWindow {
|
|||||||
actions: actions,
|
actions: actions,
|
||||||
enabled: true
|
enabled: true
|
||||||
};
|
};
|
||||||
|
if (isNiri && extraMatchModel.count > 0)
|
||||||
|
ruleData.matches = matches;
|
||||||
|
|
||||||
submitting = true;
|
submitting = true;
|
||||||
|
|
||||||
@@ -369,6 +491,61 @@ FloatingWindow {
|
|||||||
border.width: hasFocus ? 2 : 1
|
border.width: hasFocus ? 2 : 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tri-state toggle: 0 = unset (Inherit/Any), 1 = true, 2 = false
|
||||||
|
component MatchCond: Rectangle {
|
||||||
|
id: mc
|
||||||
|
property string label: ""
|
||||||
|
property int triState: 0
|
||||||
|
property string unsetLabel: I18n.tr("Any")
|
||||||
|
property bool readOnly: false
|
||||||
|
readonly property var stateText: [mc.unsetLabel, "true", "false"]
|
||||||
|
readonly property var stateColor: [Theme.surfaceVariantText, Theme.primary, Theme.error]
|
||||||
|
|
||||||
|
width: condRow.implicitWidth + Theme.spacingM * 2
|
||||||
|
height: root.inputFieldHeight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceHover
|
||||||
|
border.width: 1
|
||||||
|
border.color: mc.triState === 0 ? Theme.outlineStrong : mc.stateColor[mc.triState]
|
||||||
|
opacity: mc.readOnly ? 0.4 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: condRow
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: mc.label
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: stateBadge.implicitWidth + Theme.spacingS * 2
|
||||||
|
height: 18
|
||||||
|
radius: 9
|
||||||
|
color: Theme.withAlpha(mc.stateColor[mc.triState], 0.15)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: stateBadge
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: mc.stateText[mc.triState]
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 2
|
||||||
|
color: mc.stateColor[mc.triState]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: root.visible && !mc.readOnly
|
||||||
|
onClicked: mc.triState = (mc.triState + 1) % 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: true
|
focus: true
|
||||||
@@ -514,6 +691,176 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.isNiri
|
||||||
|
text: I18n.tr("The rule applies to any window matching one of these.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: extraMatchModel
|
||||||
|
|
||||||
|
delegate: Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
InputField {
|
||||||
|
width: (parent.width - removeMatchBtn.width - Theme.spacingS * 2) / 2
|
||||||
|
hasFocus: extraAppId.activeFocus
|
||||||
|
DankTextField {
|
||||||
|
id: extraAppId
|
||||||
|
anchors.fill: parent
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
placeholderText: root.isNiri ? I18n.tr("App ID regex") : I18n.tr("Class regex")
|
||||||
|
backgroundColor: "transparent"
|
||||||
|
enabled: root.visible
|
||||||
|
text: rowAppId
|
||||||
|
onTextEdited: extraMatchModel.setProperty(index, "rowAppId", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InputField {
|
||||||
|
width: (parent.width - removeMatchBtn.width - Theme.spacingS * 2) / 2
|
||||||
|
hasFocus: extraTitle.activeFocus
|
||||||
|
DankTextField {
|
||||||
|
id: extraTitle
|
||||||
|
anchors.fill: parent
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
placeholderText: I18n.tr("Title regex (optional)")
|
||||||
|
backgroundColor: "transparent"
|
||||||
|
enabled: root.visible
|
||||||
|
text: rowTitle
|
||||||
|
onTextEdited: extraMatchModel.setProperty(index, "rowTitle", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: removeMatchBtn
|
||||||
|
width: root.inputFieldHeight
|
||||||
|
height: root.inputFieldHeight
|
||||||
|
circular: false
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: 16
|
||||||
|
iconColor: Theme.surfaceVariantText
|
||||||
|
tooltipText: I18n.tr("Remove match")
|
||||||
|
tooltipSide: "left"
|
||||||
|
onClicked: extraMatchModel.remove(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: root.inputFieldHeight
|
||||||
|
visible: root.isNiri
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "add"
|
||||||
|
size: 18
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Add match")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: extraMatchModel.append({
|
||||||
|
"rowAppId": "",
|
||||||
|
"rowTitle": ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
title: I18n.tr("Match Conditions")
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("Optional state-based conditions applied to the first match.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
MatchCond {
|
||||||
|
id: condFloating
|
||||||
|
label: I18n.tr("Floating")
|
||||||
|
}
|
||||||
|
MatchCond {
|
||||||
|
id: condActive
|
||||||
|
label: I18n.tr("Active")
|
||||||
|
visible: isNiri
|
||||||
|
}
|
||||||
|
MatchCond {
|
||||||
|
id: condFocused
|
||||||
|
label: I18n.tr("Focused")
|
||||||
|
visible: isNiri
|
||||||
|
}
|
||||||
|
MatchCond {
|
||||||
|
id: condActiveInColumn
|
||||||
|
label: I18n.tr("Active in Column")
|
||||||
|
visible: isNiri
|
||||||
|
}
|
||||||
|
MatchCond {
|
||||||
|
id: condCastTarget
|
||||||
|
label: I18n.tr("Cast Target")
|
||||||
|
visible: isNiri
|
||||||
|
}
|
||||||
|
MatchCond {
|
||||||
|
id: condUrgent
|
||||||
|
label: I18n.tr("Urgent")
|
||||||
|
visible: isNiri
|
||||||
|
}
|
||||||
|
MatchCond {
|
||||||
|
id: condAtStartup
|
||||||
|
label: I18n.tr("At Startup")
|
||||||
|
visible: isNiri
|
||||||
|
}
|
||||||
|
MatchCond {
|
||||||
|
id: condXwayland
|
||||||
|
label: I18n.tr("XWayland")
|
||||||
|
visible: isHyprland
|
||||||
|
}
|
||||||
|
MatchCond {
|
||||||
|
id: condFullscreen
|
||||||
|
label: I18n.tr("Fullscreen")
|
||||||
|
visible: isHyprland
|
||||||
|
}
|
||||||
|
MatchCond {
|
||||||
|
id: condPinned
|
||||||
|
label: I18n.tr("Pinned")
|
||||||
|
visible: isHyprland
|
||||||
|
}
|
||||||
|
MatchCond {
|
||||||
|
id: condInitialised
|
||||||
|
label: I18n.tr("Initialised")
|
||||||
|
visible: isHyprland
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SectionHeader {
|
SectionHeader {
|
||||||
title: I18n.tr("Window Opening")
|
title: I18n.tr("Window Opening")
|
||||||
}
|
}
|
||||||
@@ -682,6 +1029,7 @@ FloatingWindow {
|
|||||||
|
|
||||||
DankSlider {
|
DankSlider {
|
||||||
id: opacitySlider
|
id: opacitySlider
|
||||||
|
wheelEnabled: false
|
||||||
width: parent.width - 100
|
width: parent.width - 100
|
||||||
minimum: 10
|
minimum: 10
|
||||||
maximum: 100
|
maximum: 100
|
||||||
@@ -710,7 +1058,7 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
CheckboxRow {
|
CheckboxRow {
|
||||||
id: drawBorderBgToggle
|
id: drawBorderBgToggle
|
||||||
label: I18n.tr("Border with BG")
|
label: I18n.tr("Border with Background")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -777,6 +1125,7 @@ FloatingWindow {
|
|||||||
|
|
||||||
DankSlider {
|
DankSlider {
|
||||||
id: scrollFactorSlider
|
id: scrollFactorSlider
|
||||||
|
wheelEnabled: false
|
||||||
width: parent.width - 120
|
width: parent.width - 120
|
||||||
minimum: 10
|
minimum: 10
|
||||||
maximum: 200
|
maximum: 200
|
||||||
@@ -798,6 +1147,7 @@ FloatingWindow {
|
|||||||
|
|
||||||
DankSlider {
|
DankSlider {
|
||||||
id: cornerRadiusSlider
|
id: cornerRadiusSlider
|
||||||
|
wheelEnabled: false
|
||||||
width: parent.width - 130
|
width: parent.width - 130
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 24
|
maximum: 24
|
||||||
@@ -807,6 +1157,88 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
title: I18n.tr("Background Effect")
|
||||||
|
visible: isNiri
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
visible: isNiri
|
||||||
|
text: I18n.tr("Xray blurs only the wallpaper (efficient) and is the default when Blur is on. Set Xray to Off for regular full blur of everything beneath the window (more expensive).")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: isNiri
|
||||||
|
|
||||||
|
MatchCond {
|
||||||
|
id: blurCond
|
||||||
|
label: I18n.tr("Blur")
|
||||||
|
unsetLabel: I18n.tr("Inherit")
|
||||||
|
onTriStateChanged: {
|
||||||
|
if (triState === 2)
|
||||||
|
xrayCond.triState = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MatchCond {
|
||||||
|
id: xrayCond
|
||||||
|
label: I18n.tr("X-Ray")
|
||||||
|
unsetLabel: I18n.tr("Inherit")
|
||||||
|
readOnly: blurCond.triState === 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
visible: isNiri
|
||||||
|
|
||||||
|
CheckboxRow {
|
||||||
|
id: noiseEnabled
|
||||||
|
label: I18n.tr("Noise")
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
id: noiseSlider
|
||||||
|
wheelEnabled: false
|
||||||
|
width: parent.width - 130
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
value: 5
|
||||||
|
enabled: noiseEnabled.checked
|
||||||
|
opacity: enabled ? 1 : 0.4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
visible: isNiri
|
||||||
|
|
||||||
|
CheckboxRow {
|
||||||
|
id: saturationEnabled
|
||||||
|
label: I18n.tr("Saturation")
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
id: saturationSlider
|
||||||
|
wheelEnabled: false
|
||||||
|
width: parent.width - 130
|
||||||
|
minimum: 0
|
||||||
|
maximum: 200
|
||||||
|
value: 100
|
||||||
|
enabled: saturationEnabled.checked
|
||||||
|
opacity: enabled ? 1 : 0.4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SectionHeader {
|
SectionHeader {
|
||||||
title: I18n.tr("Size Constraints")
|
title: I18n.tr("Size Constraints")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,107 @@ Item {
|
|||||||
property bool checkingInclude: false
|
property bool checkingInclude: false
|
||||||
property bool fixingInclude: false
|
property bool fixingInclude: false
|
||||||
property var windowRules: []
|
property var windowRules: []
|
||||||
|
property var externalRules: []
|
||||||
property var activeWindows: getActiveWindows()
|
property var activeWindows: getActiveWindows()
|
||||||
|
property string expandedExternalId: ""
|
||||||
|
|
||||||
|
readonly property var matchLabels: ({
|
||||||
|
"appId": I18n.tr("App ID"),
|
||||||
|
"title": I18n.tr("Title"),
|
||||||
|
"isFloating": I18n.tr("Is Floating"),
|
||||||
|
"isActive": I18n.tr("Is Active"),
|
||||||
|
"isFocused": I18n.tr("Is Focused"),
|
||||||
|
"isActiveInColumn": I18n.tr("Active In Column"),
|
||||||
|
"isWindowCastTarget": I18n.tr("Cast Target"),
|
||||||
|
"isUrgent": I18n.tr("Is Urgent"),
|
||||||
|
"atStartup": I18n.tr("At Startup"),
|
||||||
|
"xwayland": I18n.tr("XWayland"),
|
||||||
|
"fullscreen": I18n.tr("Fullscreen"),
|
||||||
|
"pinned": I18n.tr("Pinned"),
|
||||||
|
"initialised": I18n.tr("Initialised")
|
||||||
|
})
|
||||||
|
|
||||||
|
function matchesOf(rule) {
|
||||||
|
const m = rule.matches;
|
||||||
|
if (m && m.length > 0)
|
||||||
|
return m;
|
||||||
|
return [rule.matchCriteria || {}];
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCriteria(obj, labels) {
|
||||||
|
let out = [];
|
||||||
|
const keys = Object.keys(obj || {});
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const k = keys[i];
|
||||||
|
const v = obj[k];
|
||||||
|
if (v === undefined || v === null || v === "")
|
||||||
|
continue;
|
||||||
|
const label = labels[k] || k;
|
||||||
|
if (typeof v === "boolean")
|
||||||
|
out.push(label + ": " + (v ? I18n.tr("yes") : I18n.tr("no")));
|
||||||
|
else
|
||||||
|
out.push(label + ": " + v);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchSummary(rule) {
|
||||||
|
const matches = matchesOf(rule);
|
||||||
|
const first = matches[0] || {};
|
||||||
|
const label = first.appId || first.title || I18n.tr("Any window");
|
||||||
|
if (matches.length > 1)
|
||||||
|
return I18n.tr("%1 (+%2 more)").arg(label).arg(matches.length - 1);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var actionLabels: ({
|
||||||
|
"opacity": I18n.tr("Opacity"),
|
||||||
|
"openFloating": I18n.tr("Float"),
|
||||||
|
"openMaximized": I18n.tr("Maximize"),
|
||||||
|
"openMaximizedToEdges": I18n.tr("Max Edges"),
|
||||||
|
"openFullscreen": I18n.tr("Fullscreen"),
|
||||||
|
"openFocused": I18n.tr("Focus"),
|
||||||
|
"openOnOutput": I18n.tr("Output"),
|
||||||
|
"openOnWorkspace": I18n.tr("Workspace"),
|
||||||
|
"defaultColumnWidth": I18n.tr("Width"),
|
||||||
|
"defaultWindowHeight": I18n.tr("Height"),
|
||||||
|
"variableRefreshRate": I18n.tr("VRR"),
|
||||||
|
"blockOutFrom": I18n.tr("Block Out"),
|
||||||
|
"defaultColumnDisplay": I18n.tr("Display"),
|
||||||
|
"scrollFactor": I18n.tr("Scroll"),
|
||||||
|
"cornerRadius": I18n.tr("Radius"),
|
||||||
|
"clipToGeometry": I18n.tr("Clip"),
|
||||||
|
"tiledState": I18n.tr("Tiled"),
|
||||||
|
"minWidth": I18n.tr("Min W"),
|
||||||
|
"maxWidth": I18n.tr("Max W"),
|
||||||
|
"minHeight": I18n.tr("Min H"),
|
||||||
|
"maxHeight": I18n.tr("Max H"),
|
||||||
|
"tile": I18n.tr("Tile"),
|
||||||
|
"nofocus": I18n.tr("No Focus"),
|
||||||
|
"noborder": I18n.tr("No Border"),
|
||||||
|
"noshadow": I18n.tr("No Shadow"),
|
||||||
|
"nodim": I18n.tr("No Dim"),
|
||||||
|
"noblur": I18n.tr("No Blur"),
|
||||||
|
"noanim": I18n.tr("No Anim"),
|
||||||
|
"norounding": I18n.tr("No Round"),
|
||||||
|
"pin": I18n.tr("Pin"),
|
||||||
|
"opaque": I18n.tr("Opaque"),
|
||||||
|
"size": I18n.tr("Size"),
|
||||||
|
"move": I18n.tr("Move"),
|
||||||
|
"monitor": I18n.tr("Monitor"),
|
||||||
|
"workspace": I18n.tr("Workspace"),
|
||||||
|
"drawBorderWithBackground": I18n.tr("Border w/ Bg"),
|
||||||
|
"backgroundBlur": I18n.tr("Blur"),
|
||||||
|
"backgroundXray": I18n.tr("X-Ray"),
|
||||||
|
"backgroundNoise": I18n.tr("Noise"),
|
||||||
|
"backgroundSaturation": I18n.tr("Saturation"),
|
||||||
|
"borderColor": I18n.tr("Border Color"),
|
||||||
|
"focusRingColor": I18n.tr("Focus Ring Color"),
|
||||||
|
"focusRingOff": I18n.tr("Focus Ring Off"),
|
||||||
|
"borderOff": I18n.tr("Border Off"),
|
||||||
|
"forcergbx": I18n.tr("Force RGBX"),
|
||||||
|
"idleinhibit": I18n.tr("Idle Inhibit")
|
||||||
|
})
|
||||||
|
|
||||||
signal rulesChanged
|
signal rulesChanged
|
||||||
|
|
||||||
@@ -72,18 +172,21 @@ Item {
|
|||||||
const compositor = CompositorService.compositor;
|
const compositor = CompositorService.compositor;
|
||||||
if (compositor !== "niri" && compositor !== "hyprland") {
|
if (compositor !== "niri" && compositor !== "hyprland") {
|
||||||
windowRules = [];
|
windowRules = [];
|
||||||
|
externalRules = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Proc.runCommand("load-windowrules", ["dms", "config", "windowrules", "list", compositor], (output, exitCode) => {
|
Proc.runCommand("load-windowrules", ["dms", "config", "windowrules", "list", compositor], (output, exitCode) => {
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
windowRules = [];
|
windowRules = [];
|
||||||
|
externalRules = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = JSON.parse(output.trim());
|
const result = JSON.parse(output.trim());
|
||||||
const allRules = result.rules || [];
|
const allRules = result.rules || [];
|
||||||
windowRules = allRules.filter(r => (r.source || "").includes("dms/windowrules"));
|
windowRules = allRules.filter(r => (r.source || "").includes("dms/windowrules"));
|
||||||
|
externalRules = allRules.filter(r => !(r.source || "").includes("dms/windowrules"));
|
||||||
if (result.dmsStatus) {
|
if (result.dmsStatus) {
|
||||||
windowRulesIncludeStatus = {
|
windowRulesIncludeStatus = {
|
||||||
"exists": result.dmsStatus.exists,
|
"exists": result.dmsStatus.exists,
|
||||||
@@ -94,6 +197,7 @@ Item {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
windowRules = [];
|
windowRules = [];
|
||||||
|
externalRules = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -232,6 +336,20 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyRuleToDms(rule) {
|
||||||
|
if (readOnly) {
|
||||||
|
showHyprlandReadOnlyWarning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!PopoutService.windowRuleModalLoader)
|
||||||
|
return;
|
||||||
|
PopoutService.windowRuleModalLoader.active = true;
|
||||||
|
if (PopoutService.windowRuleModalLoader.item) {
|
||||||
|
PopoutService.windowRuleModalLoader.item.onRuleSubmitted.connect(loadWindowRules);
|
||||||
|
PopoutService.windowRuleModalLoader.item.showCopy(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showHyprlandReadOnlyWarning() {
|
function showHyprlandReadOnlyWarning() {
|
||||||
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing window rules in Settings."), "dms setup", "hyprland-migration");
|
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing window rules in Settings."), "dms setup", "hyprland-migration");
|
||||||
}
|
}
|
||||||
@@ -311,6 +429,8 @@ Item {
|
|||||||
iconColor: Theme.primary
|
iconColor: Theme.primary
|
||||||
enabled: !root.readOnly
|
enabled: !root.readOnly
|
||||||
opacity: enabled ? 1 : 0.5
|
opacity: enabled ? 1 : 0.5
|
||||||
|
tooltipText: I18n.tr("Add Window Rule")
|
||||||
|
tooltipSide: "left"
|
||||||
onClicked: root.openRuleModal()
|
onClicked: root.openRuleModal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -575,47 +695,11 @@ Item {
|
|||||||
Repeater {
|
Repeater {
|
||||||
model: {
|
model: {
|
||||||
const a = ruleDelegateItem.liveRuleData.actions || {};
|
const a = ruleDelegateItem.liveRuleData.actions || {};
|
||||||
const labels = {
|
const labels = root.actionLabels;
|
||||||
"opacity": I18n.tr("Opacity"),
|
|
||||||
"openFloating": I18n.tr("Float"),
|
|
||||||
"openMaximized": I18n.tr("Maximize"),
|
|
||||||
"openMaximizedToEdges": I18n.tr("Max Edges"),
|
|
||||||
"openFullscreen": I18n.tr("Fullscreen"),
|
|
||||||
"openFocused": I18n.tr("Focus"),
|
|
||||||
"openOnOutput": I18n.tr("Output"),
|
|
||||||
"openOnWorkspace": I18n.tr("Workspace"),
|
|
||||||
"defaultColumnWidth": I18n.tr("Width"),
|
|
||||||
"defaultWindowHeight": I18n.tr("Height"),
|
|
||||||
"variableRefreshRate": I18n.tr("VRR"),
|
|
||||||
"blockOutFrom": I18n.tr("Block Out"),
|
|
||||||
"defaultColumnDisplay": I18n.tr("Display"),
|
|
||||||
"scrollFactor": I18n.tr("Scroll"),
|
|
||||||
"cornerRadius": I18n.tr("Radius"),
|
|
||||||
"clipToGeometry": I18n.tr("Clip"),
|
|
||||||
"tiledState": I18n.tr("Tiled"),
|
|
||||||
"minWidth": I18n.tr("Min W"),
|
|
||||||
"maxWidth": I18n.tr("Max W"),
|
|
||||||
"minHeight": I18n.tr("Min H"),
|
|
||||||
"maxHeight": I18n.tr("Max H"),
|
|
||||||
"tile": I18n.tr("Tile"),
|
|
||||||
"nofocus": I18n.tr("No Focus"),
|
|
||||||
"noborder": I18n.tr("No Border"),
|
|
||||||
"noshadow": I18n.tr("No Shadow"),
|
|
||||||
"nodim": I18n.tr("No Dim"),
|
|
||||||
"noblur": I18n.tr("No Blur"),
|
|
||||||
"noanim": I18n.tr("No Anim"),
|
|
||||||
"norounding": I18n.tr("No Round"),
|
|
||||||
"pin": I18n.tr("Pin"),
|
|
||||||
"opaque": I18n.tr("Opaque"),
|
|
||||||
"size": I18n.tr("Size"),
|
|
||||||
"move": I18n.tr("Move"),
|
|
||||||
"monitor": I18n.tr("Monitor"),
|
|
||||||
"workspace": I18n.tr("Workspace")
|
|
||||||
};
|
|
||||||
return Object.keys(a).filter(k => a[k] !== undefined && a[k] !== null && a[k] !== "").map(k => {
|
return Object.keys(a).filter(k => a[k] !== undefined && a[k] !== null && a[k] !== "").map(k => {
|
||||||
const val = a[k];
|
const val = a[k];
|
||||||
if (typeof val === "boolean")
|
if (typeof val === "boolean")
|
||||||
return labels[k] || k;
|
return val ? (labels[k] || k) : (labels[k] || k) + ": " + I18n.tr("off");
|
||||||
return (labels[k] || k) + ": " + val;
|
return (labels[k] || k) + ": " + val;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -651,26 +735,26 @@ Item {
|
|||||||
iconColor: Theme.surfaceVariantText
|
iconColor: Theme.surfaceVariantText
|
||||||
enabled: !root.readOnly
|
enabled: !root.readOnly
|
||||||
opacity: enabled ? 1 : 0.5
|
opacity: enabled ? 1 : 0.5
|
||||||
|
tooltipText: I18n.tr("Edit Rule")
|
||||||
|
tooltipSide: "top"
|
||||||
onClicked: root.editRule(ruleDelegateItem.liveRuleData)
|
onClicked: root.editRule(ruleDelegateItem.liveRuleData)
|
||||||
}
|
}
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
id: deleteBtn
|
id: deleteBtn
|
||||||
|
property bool hovered: false
|
||||||
buttonSize: 28
|
buttonSize: 28
|
||||||
iconName: "delete"
|
iconName: "delete"
|
||||||
iconSize: 16
|
iconSize: 16
|
||||||
backgroundColor: "transparent"
|
backgroundColor: "transparent"
|
||||||
iconColor: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText
|
iconColor: hovered ? Theme.error : Theme.surfaceVariantText
|
||||||
enabled: !root.readOnly
|
enabled: !root.readOnly
|
||||||
opacity: enabled ? 1 : 0.5
|
opacity: enabled ? 1 : 0.5
|
||||||
|
tooltipText: I18n.tr("Delete Rule")
|
||||||
MouseArea {
|
tooltipSide: "top"
|
||||||
id: deleteArea
|
onEntered: hovered = true
|
||||||
anchors.fill: parent
|
onExited: hovered = false
|
||||||
hoverEnabled: !root.readOnly
|
onClicked: root.removeRule(ruleDelegateItem.ruleIdRef)
|
||||||
cursorShape: root.readOnly ? Qt.ArrowCursor : Qt.PointingHandCursor
|
|
||||||
onClicked: root.removeRule(ruleDelegateItem.ruleIdRef)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -729,6 +813,283 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: Math.min(650, parent.width - Theme.spacingL * 2)
|
||||||
|
height: externalSection.implicitHeight + Theme.spacingL * 2
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
visible: root.externalRules && root.externalRules.length > 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: externalSection
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "description"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("User Window Rules (%1)").arg(root.externalRules?.length ?? 0)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Rules found in your compositor config. These are read-only here, use Convert to DMS to make an editable copy.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ScriptModel {
|
||||||
|
objectProp: "id"
|
||||||
|
values: root.externalRules || []
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: externalCard
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
readonly property string displayName: {
|
||||||
|
const name = externalCard.modelData.name || "";
|
||||||
|
if (name)
|
||||||
|
return name;
|
||||||
|
return root.matchSummary(externalCard.modelData);
|
||||||
|
}
|
||||||
|
readonly property string sourceFile: (externalCard.modelData.source || "").split("/").pop()
|
||||||
|
readonly property bool expanded: root.expandedExternalId === externalCard.modelData.id
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: externalContent.implicitHeight + Theme.spacingM * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainer, 0.4)
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.expandedExternalId = externalCard.expanded ? "" : externalCard.modelData.id
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: externalContent
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: externalCard.displayName
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: externalCard.sourceFile.length > 0
|
||||||
|
width: sourceText.implicitWidth + Theme.spacingS * 2
|
||||||
|
height: 20
|
||||||
|
radius: 10
|
||||||
|
color: Theme.withAlpha(Theme.surfaceVariantText, 0.15)
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: sourceText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: externalCard.sourceFile
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 2
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
const m = externalCard.modelData.matchCriteria || {};
|
||||||
|
let parts = [];
|
||||||
|
if (m.appId)
|
||||||
|
parts.push(m.appId);
|
||||||
|
if (m.title)
|
||||||
|
parts.push("title: " + m.title);
|
||||||
|
const base = parts.length > 0 ? parts.join(" · ") : I18n.tr("No match criteria");
|
||||||
|
const count = root.matchesOf(externalCard.modelData).length;
|
||||||
|
return count > 1 ? I18n.tr("%1 (+%2 more)").arg(base).arg(count - 1) : base;
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 4
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: {
|
||||||
|
const a = externalCard.modelData.actions || {};
|
||||||
|
return Object.keys(a).some(k => a[k] !== undefined && a[k] !== null && a[k] !== "");
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: {
|
||||||
|
const a = externalCard.modelData.actions || {};
|
||||||
|
const labels = root.actionLabels;
|
||||||
|
return Object.keys(a).filter(k => a[k] !== undefined && a[k] !== null && a[k] !== "").map(k => {
|
||||||
|
const val = a[k];
|
||||||
|
if (typeof val === "boolean")
|
||||||
|
return val ? (labels[k] || k) : (labels[k] || k) + ": " + I18n.tr("off");
|
||||||
|
return (labels[k] || k) + ": " + val;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property string modelData
|
||||||
|
width: extChipText.implicitWidth + Theme.spacingS * 2
|
||||||
|
height: 20
|
||||||
|
radius: 10
|
||||||
|
color: Theme.withAlpha(Theme.primary, 0.15)
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: extChipText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 2
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: externalCard.expanded ? "expand_less" : "expand_more"
|
||||||
|
size: 20
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 28
|
||||||
|
iconName: "content_copy"
|
||||||
|
iconSize: 16
|
||||||
|
backgroundColor: "transparent"
|
||||||
|
iconColor: Theme.surfaceVariantText
|
||||||
|
enabled: !root.readOnly
|
||||||
|
opacity: enabled ? 1 : 0.5
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
tooltipText: I18n.tr("Convert to DMS")
|
||||||
|
tooltipSide: "left"
|
||||||
|
onClicked: root.copyRuleToDms(externalCard.modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: externalCard.expanded
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Theme.withAlpha(Theme.outline, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Match (%1)").arg(root.matchesOf(externalCard.modelData).length)
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.matchesOf(externalCard.modelData)
|
||||||
|
|
||||||
|
delegate: StyledText {
|
||||||
|
required property var modelData
|
||||||
|
width: parent.width
|
||||||
|
text: {
|
||||||
|
const c = root.formatCriteria(modelData, root.matchLabels);
|
||||||
|
return "• " + (c.length > 0 ? c.join(" · ") : I18n.tr("Any window"));
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Actions")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
topPadding: Theme.spacingXS
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: {
|
||||||
|
const a = root.formatCriteria(externalCard.modelData.actions, root.actionLabels);
|
||||||
|
return a.length > 0 ? a.join(" · ") : I18n.tr("None");
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("Source: %1").arg(externalCard.modelData.source || "")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
topPadding: Theme.spacingXS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user