mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
Compare commits
8 Commits
chroma
...
1f2e231386
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f2e231386 | ||
|
|
0e7f628c4a | ||
|
|
553f5257b3 | ||
|
|
80ce6aa19c | ||
|
|
2b2977de4a | ||
|
|
1d5d876e16 | ||
|
|
3c39162016 | ||
|
|
d38767fb5a |
@@ -1,193 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yuin/goldmark"
|
||||
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
ghtml "github.com/yuin/goldmark/renderer/html"
|
||||
)
|
||||
|
||||
var (
|
||||
chromaLanguage string
|
||||
chromaStyle string
|
||||
chromaInline bool
|
||||
chromaMarkdown bool
|
||||
)
|
||||
|
||||
var chromaCmd = &cobra.Command{
|
||||
Use: "chroma [file]",
|
||||
Short: "Syntax highlight source code",
|
||||
Long: `Generate syntax-highlighted HTML from source code.
|
||||
|
||||
Reads from file or stdin, outputs HTML with syntax highlighting.
|
||||
Language is auto-detected from filename or can be specified with --language.
|
||||
|
||||
Examples:
|
||||
dms chroma main.go
|
||||
dms chroma --language python script.py
|
||||
echo "def foo(): pass" | dms chroma -l python
|
||||
cat code.rs | dms chroma -l rust --style dracula
|
||||
dms chroma --markdown README.md
|
||||
dms chroma --markdown --style github-dark notes.md
|
||||
dms chroma list-languages
|
||||
dms chroma list-styles`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: runChroma,
|
||||
}
|
||||
|
||||
var chromaListLanguagesCmd = &cobra.Command{
|
||||
Use: "list-languages",
|
||||
Short: "List all supported languages",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
for _, name := range lexers.Names(true) {
|
||||
fmt.Println(name)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var chromaListStylesCmd = &cobra.Command{
|
||||
Use: "list-styles",
|
||||
Short: "List all available color styles",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
for _, name := range styles.Names() {
|
||||
fmt.Println(name)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
chromaCmd.Flags().StringVarP(&chromaLanguage, "language", "l", "", "Language for highlighting (auto-detect if not specified)")
|
||||
chromaCmd.Flags().StringVarP(&chromaStyle, "style", "s", "monokai", "Color style (monokai, dracula, github, etc.)")
|
||||
chromaCmd.Flags().BoolVar(&chromaInline, "inline", false, "Output inline styles instead of CSS classes")
|
||||
chromaCmd.Flags().BoolVarP(&chromaMarkdown, "markdown", "m", false, "Render markdown with syntax-highlighted code blocks")
|
||||
|
||||
chromaCmd.AddCommand(chromaListLanguagesCmd)
|
||||
chromaCmd.AddCommand(chromaListStylesCmd)
|
||||
}
|
||||
|
||||
func runChroma(cmd *cobra.Command, args []string) {
|
||||
var source string
|
||||
var filename string
|
||||
|
||||
// Read from file or stdin
|
||||
if len(args) > 0 {
|
||||
filename = args[0]
|
||||
content, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
source = string(content)
|
||||
} else {
|
||||
content, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
source = string(content)
|
||||
}
|
||||
|
||||
// Handle empty input
|
||||
if strings.TrimSpace(source) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle Markdown rendering
|
||||
if chromaMarkdown {
|
||||
md := goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.GFM,
|
||||
highlighting.NewHighlighting(
|
||||
highlighting.WithStyle(chromaStyle),
|
||||
highlighting.WithFormatOptions(
|
||||
html.WithClasses(!chromaInline),
|
||||
),
|
||||
),
|
||||
),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAutoHeadingID(),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
ghtml.WithHardWraps(),
|
||||
ghtml.WithXHTML(),
|
||||
),
|
||||
)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := md.Convert([]byte(source), &buf); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Markdown rendering error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Print(buf.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Detect or use specified lexer
|
||||
var lexer chroma.Lexer
|
||||
if chromaLanguage != "" {
|
||||
lexer = lexers.Get(chromaLanguage)
|
||||
if lexer == nil {
|
||||
fmt.Fprintf(os.Stderr, "Unknown language: %s\n", chromaLanguage)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else if filename != "" {
|
||||
lexer = lexers.Match(filename)
|
||||
}
|
||||
|
||||
// Try content analysis if no lexer found
|
||||
if lexer == nil {
|
||||
lexer = lexers.Analyse(source)
|
||||
}
|
||||
|
||||
// Fallback to plaintext
|
||||
if lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
|
||||
lexer = chroma.Coalesce(lexer)
|
||||
|
||||
// Get style
|
||||
style := styles.Get(chromaStyle)
|
||||
if style == nil {
|
||||
style = styles.Fallback
|
||||
}
|
||||
|
||||
// Create HTML formatter
|
||||
var formatter *html.Formatter
|
||||
if chromaInline {
|
||||
formatter = html.New(
|
||||
html.WithClasses(false),
|
||||
html.TabWidth(4),
|
||||
)
|
||||
} else {
|
||||
formatter = html.New(
|
||||
html.WithClasses(true),
|
||||
html.TabWidth(4),
|
||||
)
|
||||
}
|
||||
|
||||
// Tokenize
|
||||
iterator, err := lexer.Tokenise(nil, source)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Tokenization error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Format and output
|
||||
if err := formatter.Format(os.Stdout, style, iterator); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -517,6 +517,5 @@ func getCommonCommands() []*cobra.Command {
|
||||
clipboardCmd,
|
||||
doctorCmd,
|
||||
configCmd,
|
||||
chromaCmd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ go 1.24.6
|
||||
|
||||
require (
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||
github.com/alecthomas/chroma/v2 v2.17.2
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
@@ -29,7 +28,6 @@ require (
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.2 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc // indirect
|
||||
@@ -40,8 +38,6 @@ require (
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/yuin/goldmark v1.7.16 // indirect
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
)
|
||||
|
||||
44
core/go.sum
44
core/go.sum
@@ -4,14 +4,6 @@ github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBi
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
|
||||
github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
@@ -32,12 +24,16 @@ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoF
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
|
||||
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
|
||||
github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
|
||||
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
|
||||
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/displaywidth v0.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE=
|
||||
github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
@@ -52,10 +48,6 @@ github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
@@ -66,10 +58,14 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd h1:Gd/f9cGi/3h1JOPaa6er+CkKUGyGX2DBJdFbDKVO+R0=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd/go.mod h1:d3XQcsHu1idnquxt48kAv+h+1MUiYKLH/e7LAzjP+pI=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146/go.mod h1:QE/75B8tBSLNGyUUbA9tw3EGHoFtYOtypa2h8YJxsWI=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19 h1:0lz2eJScP8v5YZQsrEw+ggWC5jNySjg4bIZo5BIh6iI=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19/go.mod h1:L+Evfcs7EdTqxwv854354cb6+++7TFL3hJn3Wy4g+3w=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6 h1:Yo1MlE8LpvD0pr7mZ04b6hKZKQcPvLrQFgyY1jNMEyU=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6/go.mod h1:enMzPHv+9hL4B7tH7OJGQKNzCkMzXovUoaiXfsLF7Xs=
|
||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||
@@ -82,8 +78,6 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUv
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
|
||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -139,35 +133,42 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
||||
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -176,6 +177,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -215,8 +215,8 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
||||
|
||||
for _, cfg := range configs {
|
||||
path := filepath.Join(dmsDir, cfg.name)
|
||||
// Skip if file already exists to preserve user modifications
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
// Skip if file already exists and is not empty to preserve user modifications
|
||||
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||
continue
|
||||
}
|
||||
@@ -567,7 +567,8 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
|
||||
|
||||
for _, cfg := range configs {
|
||||
path := filepath.Join(dmsDir, cfg.name)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
// Skip if file already exists and is not empty to preserve user modifications
|
||||
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -236,7 +236,13 @@ Singleton {
|
||||
property bool sortAppsAlphabetically: false
|
||||
property int appLauncherGridColumns: 4
|
||||
property bool spotlightCloseNiriOverview: true
|
||||
property var spotlightSectionViewModes: ({})
|
||||
property bool niriOverviewOverlayEnabled: true
|
||||
property string dankLauncherV2Size: "compact"
|
||||
property bool dankLauncherV2BorderEnabled: false
|
||||
property int dankLauncherV2BorderThickness: 2
|
||||
property string dankLauncherV2BorderColor: "primary"
|
||||
property bool dankLauncherV2ShowFooter: true
|
||||
|
||||
property string _legacyWeatherLocation: "New York, NY"
|
||||
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
||||
|
||||
@@ -134,7 +134,13 @@ var SPEC = {
|
||||
sortAppsAlphabetically: { def: false },
|
||||
appLauncherGridColumns: { def: 4 },
|
||||
spotlightCloseNiriOverview: { def: true },
|
||||
spotlightSectionViewModes: { def: {} },
|
||||
niriOverviewOverlayEnabled: { def: true },
|
||||
dankLauncherV2Size: { def: "compact" },
|
||||
dankLauncherV2BorderEnabled: { def: false },
|
||||
dankLauncherV2BorderThickness: { def: 2 },
|
||||
dankLauncherV2BorderColor: { def: "primary" },
|
||||
dankLauncherV2ShowFooter: { def: true },
|
||||
|
||||
useAutoLocation: { def: false },
|
||||
weatherEnabled: { def: true },
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Modals.Clipboard
|
||||
import qs.Modals.Greeter
|
||||
import qs.Modals.Settings
|
||||
import qs.Modals.Spotlight
|
||||
import qs.Modals.DankLauncherV2
|
||||
import qs.Modules
|
||||
import qs.Modules.AppDrawer
|
||||
import qs.Modules.DankDash
|
||||
@@ -514,6 +515,25 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: spotlightV2ModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.spotlightV2ModalLoader = spotlightV2ModalLoader;
|
||||
}
|
||||
|
||||
DankLauncherV2Modal {
|
||||
id: spotlightV2Modal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.spotlightV2Modal = spotlightV2Modal;
|
||||
PopoutService._onSpotlightV2ModalLoaded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClipboardHistoryModal {
|
||||
id: clipboardHistoryModalPopup
|
||||
|
||||
|
||||
@@ -1025,6 +1025,35 @@ Item {
|
||||
target: "clipboard"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
PopoutService.openSpotlightV2();
|
||||
return "LAUNCHER_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
PopoutService.closeSpotlightV2();
|
||||
return "LAUNCHER_CLOSE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
PopoutService.toggleSpotlightV2();
|
||||
return "LAUNCHER_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
function openQuery(query: string): string {
|
||||
PopoutService.openSpotlightV2WithQuery(query);
|
||||
return "LAUNCHER_OPEN_QUERY_SUCCESS";
|
||||
}
|
||||
|
||||
function toggleQuery(query: string): string {
|
||||
PopoutService.toggleSpotlightV2();
|
||||
return "LAUNCHER_TOGGLE_QUERY_SUCCESS";
|
||||
}
|
||||
|
||||
target: "launcher"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
FirstLaunchService.showWelcome();
|
||||
|
||||
@@ -15,5 +15,5 @@ Singleton {
|
||||
readonly property int viewportBuffer: 100
|
||||
readonly property int extendedBuffer: 200
|
||||
readonly property int keyboardHintsHeight: 80
|
||||
readonly property int headerHeight: 40
|
||||
readonly property int headerHeight: 32
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ Item {
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
focus: false
|
||||
|
||||
ClipboardHeader {
|
||||
@@ -195,7 +195,7 @@ Item {
|
||||
Item {
|
||||
id: keyboardHintsContainer
|
||||
width: parent.width
|
||||
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingL : 0
|
||||
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingM : 0
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
@@ -210,7 +210,7 @@ Item {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
anchors.margins: Theme.spacingM
|
||||
visible: modal.showKeyboardHints
|
||||
wtypeAvailable: modal.wtypeAvailable
|
||||
}
|
||||
|
||||
231
quickshell/Modals/DankLauncherV2/ActionPanel.qml
Normal file
231
quickshell/Modals/DankLauncherV2/ActionPanel.qml
Normal file
@@ -0,0 +1,231 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var selectedItem: null
|
||||
property var controller: null
|
||||
property bool expanded: false
|
||||
property int selectedActionIndex: 0
|
||||
|
||||
function getPluginContextMenuActions() {
|
||||
if (selectedItem?.type !== "plugin" || !selectedItem?.pluginId)
|
||||
return [];
|
||||
var instance = PluginService.pluginInstances[selectedItem.pluginId];
|
||||
if (!instance)
|
||||
return [];
|
||||
if (typeof instance.getContextMenuActions !== "function")
|
||||
return [];
|
||||
var actions = instance.getContextMenuActions(selectedItem.data);
|
||||
if (!Array.isArray(actions))
|
||||
return [];
|
||||
return actions;
|
||||
}
|
||||
|
||||
readonly property var actions: {
|
||||
var result = [];
|
||||
if (selectedItem?.primaryAction) {
|
||||
result.push(selectedItem.primaryAction);
|
||||
}
|
||||
|
||||
if (selectedItem?.type === "plugin") {
|
||||
var pluginActions = getPluginContextMenuActions();
|
||||
for (var i = 0; i < pluginActions.length; i++) {
|
||||
var act = pluginActions[i];
|
||||
result.push({
|
||||
name: act.text || act.name || "",
|
||||
icon: act.icon || "play_arrow",
|
||||
action: "plugin_action",
|
||||
pluginAction: act.action
|
||||
});
|
||||
}
|
||||
} else if (selectedItem?.type === "app" && !selectedItem?.isCore) {
|
||||
if (selectedItem?.actions) {
|
||||
for (var i = 0; i < selectedItem.actions.length; i++) {
|
||||
result.push(selectedItem.actions[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
readonly property bool hasActions: {
|
||||
if (selectedItem?.type === "app" && !selectedItem?.isCore)
|
||||
return true;
|
||||
if (selectedItem?.type === "plugin") {
|
||||
var pluginActions = getPluginContextMenuActions();
|
||||
return pluginActions.length > 0;
|
||||
}
|
||||
return actions.length > 1;
|
||||
}
|
||||
|
||||
width: parent?.width ?? 200
|
||||
height: expanded && hasActions ? 52 : 0
|
||||
color: Theme.surfaceContainerHigh
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
clip: true
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outlineMedium
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
|
||||
Flickable {
|
||||
id: actionsFlickable
|
||||
anchors.left: parent.left
|
||||
anchors.right: tabHint.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
contentWidth: actionsRow.width
|
||||
contentHeight: height
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
flickableDirection: Flickable.HorizontalFlick
|
||||
|
||||
Row {
|
||||
id: actionsRow
|
||||
height: parent.height
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.actions
|
||||
|
||||
Rectangle {
|
||||
id: actionButton
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: actionContent.implicitWidth + Theme.spacingM * 2
|
||||
height: actionsRow.height
|
||||
radius: Theme.cornerRadius
|
||||
color: index === root.selectedActionIndex ? Theme.primaryHover : actionArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
Row {
|
||||
id: actionContent
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: actionButton.modelData?.icon ?? "play_arrow"
|
||||
size: 16
|
||||
color: actionButton.index === root.selectedActionIndex ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: actionButton.modelData?.name ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: actionButton.index === root.selectedActionIndex ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actionArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.controller && root.selectedItem) {
|
||||
root.controller.executeAction(root.selectedItem, actionButton.modelData);
|
||||
}
|
||||
}
|
||||
onEntered: root.selectedActionIndex = actionButton.index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: tabHint
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.hasActions
|
||||
text: "Tab"
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Theme.outlineButton
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
expanded = !expanded;
|
||||
selectedActionIndex = 0;
|
||||
}
|
||||
|
||||
function show() {
|
||||
expanded = true;
|
||||
selectedActionIndex = actions.length > 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
expanded = false;
|
||||
selectedActionIndex = 0;
|
||||
}
|
||||
|
||||
function cycleAction() {
|
||||
if (actions.length > 0) {
|
||||
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
|
||||
ensureSelectedVisible();
|
||||
}
|
||||
}
|
||||
|
||||
function ensureSelectedVisible() {
|
||||
if (selectedActionIndex < 0 || !actionsRow.children || selectedActionIndex >= actionsRow.children.length)
|
||||
return;
|
||||
var buttonX = 0;
|
||||
for (var i = 0; i < selectedActionIndex; i++) {
|
||||
var child = actionsRow.children[i];
|
||||
if (child)
|
||||
buttonX += child.width + actionsRow.spacing;
|
||||
}
|
||||
|
||||
var button = actionsRow.children[selectedActionIndex];
|
||||
if (!button)
|
||||
return;
|
||||
var buttonRight = buttonX + button.width;
|
||||
var viewLeft = actionsFlickable.contentX;
|
||||
var viewRight = viewLeft + actionsFlickable.width;
|
||||
|
||||
if (buttonX < viewLeft) {
|
||||
actionsFlickable.contentX = Math.max(0, buttonX - Theme.spacingS);
|
||||
} else if (buttonRight > viewRight) {
|
||||
actionsFlickable.contentX = Math.min(actionsFlickable.contentWidth - actionsFlickable.width, buttonRight - actionsFlickable.width + Theme.spacingS);
|
||||
}
|
||||
}
|
||||
|
||||
function executeSelectedAction() {
|
||||
if (!controller || !selectedItem || selectedActionIndex >= actions.length)
|
||||
return;
|
||||
var action = actions[selectedActionIndex];
|
||||
if (action.action === "plugin_action" && typeof action.pluginAction === "function") {
|
||||
action.pluginAction();
|
||||
controller.performSearch();
|
||||
controller.itemExecuted();
|
||||
} else {
|
||||
controller.executeAction(selectedItem, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
1598
quickshell/Modals/DankLauncherV2/Controller.qml
Normal file
1598
quickshell/Modals/DankLauncherV2/Controller.qml
Normal file
File diff suppressed because it is too large
Load Diff
306
quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml
Normal file
306
quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml
Normal file
@@ -0,0 +1,306 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
visible: false
|
||||
|
||||
property bool spotlightOpen: false
|
||||
property bool keyboardActive: false
|
||||
property bool contentVisible: false
|
||||
property alias spotlightContent: launcherContent
|
||||
property bool openedFromOverview: false
|
||||
property bool isClosing: false
|
||||
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property var effectiveScreen: launcherWindow.screen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||
|
||||
readonly property int baseWidth: SettingsData.dankLauncherV2Size === "medium" ? 720 : SettingsData.dankLauncherV2Size === "large" ? 860 : 620
|
||||
readonly property int baseHeight: SettingsData.dankLauncherV2Size === "medium" ? 720 : SettingsData.dankLauncherV2Size === "large" ? 860 : 600
|
||||
readonly property int modalWidth: Math.min(baseWidth, screenWidth - 100)
|
||||
readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100)
|
||||
readonly property real modalX: (screenWidth - modalWidth) / 2
|
||||
readonly property real modalY: (screenHeight - modalHeight) / 2
|
||||
|
||||
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
readonly property real cornerRadius: Theme.cornerRadius
|
||||
readonly property color borderColor: {
|
||||
if (!SettingsData.dankLauncherV2BorderEnabled)
|
||||
return Theme.outlineMedium;
|
||||
switch (SettingsData.dankLauncherV2BorderColor) {
|
||||
case "primary":
|
||||
return Theme.primary;
|
||||
case "secondary":
|
||||
return Theme.secondary;
|
||||
case "outline":
|
||||
return Theme.outline;
|
||||
case "surfaceText":
|
||||
return Theme.surfaceText;
|
||||
default:
|
||||
return Theme.primary;
|
||||
}
|
||||
}
|
||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 1
|
||||
|
||||
signal dialogClosed
|
||||
|
||||
function _initializeAndShow(query) {
|
||||
contentVisible = true;
|
||||
spotlightContent.searchField.forceActiveFocus();
|
||||
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = query;
|
||||
}
|
||||
if (spotlightContent.controller) {
|
||||
spotlightContent.controller.searchMode = "all";
|
||||
spotlightContent.controller.activePluginId = "";
|
||||
spotlightContent.controller.activePluginName = "";
|
||||
spotlightContent.controller.pluginFilter = "";
|
||||
spotlightContent.controller.collapsedSections = {};
|
||||
if (query) {
|
||||
spotlightContent.controller.setSearchQuery(query);
|
||||
} else {
|
||||
spotlightContent.controller.searchQuery = "";
|
||||
spotlightContent.controller.performSearch();
|
||||
}
|
||||
}
|
||||
if (spotlightContent.resetScroll) {
|
||||
spotlightContent.resetScroll();
|
||||
}
|
||||
if (spotlightContent.actionPanel) {
|
||||
spotlightContent.actionPanel.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
closeCleanupTimer.stop();
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen)
|
||||
launcherWindow.screen = focusedScreen;
|
||||
|
||||
spotlightOpen = true;
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(root);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_initializeAndShow("");
|
||||
}
|
||||
|
||||
function showWithQuery(query) {
|
||||
closeCleanupTimer.stop();
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen)
|
||||
launcherWindow.screen = focusedScreen;
|
||||
|
||||
spotlightOpen = true;
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(root);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_initializeAndShow(query);
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!spotlightOpen)
|
||||
return;
|
||||
openedFromOverview = false;
|
||||
isClosing = true;
|
||||
contentVisible = false;
|
||||
|
||||
keyboardActive = false;
|
||||
spotlightOpen = false;
|
||||
focusGrab.active = false;
|
||||
ModalManager.closeModal(root);
|
||||
|
||||
closeCleanupTimer.start();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
spotlightOpen ? hide() : show();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeCleanupTimer
|
||||
interval: Theme.expressiveDurations.expressiveFastSpatial + 50
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
isClosing = false;
|
||||
dialogClosed();
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [launcherWindow]
|
||||
active: false
|
||||
|
||||
onCleared: {
|
||||
if (spotlightOpen) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ModalManager
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== root && spotlightOpen) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
if (!launcherWindow.screen)
|
||||
return;
|
||||
const currentScreenName = launcherWindow.screen.name;
|
||||
let screenStillExists = false;
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === currentScreenName) {
|
||||
screenStillExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (screenStillExists)
|
||||
return;
|
||||
const newScreen = CompositorService.getFocusedScreen();
|
||||
if (newScreen)
|
||||
launcherWindow.screen = newScreen;
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: launcherWindow
|
||||
visible: true
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WlrLayershell.namespace: "dms:launcher"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: spotlightOpen ? fullScreenMask : null
|
||||
}
|
||||
|
||||
Item {
|
||||
id: fullScreenMask
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: backgroundDarken
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||
visible: contentVisible || opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveFastSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: spotlightOpen
|
||||
onClicked: mouse => {
|
||||
var contentX = modalContainer.x;
|
||||
var contentY = modalContainer.y;
|
||||
var contentW = modalContainer.width;
|
||||
var contentH = modalContainer.height;
|
||||
|
||||
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.modalX
|
||||
y: root.modalY
|
||||
width: root.modalWidth
|
||||
height: root.modalHeight
|
||||
visible: contentVisible || opacity > 0
|
||||
|
||||
opacity: contentVisible ? 1 : 0
|
||||
scale: contentVisible ? 1 : 0.96
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||
}
|
||||
}
|
||||
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
color: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
radius: root.cornerRadius
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => mouse.accepted = true
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: keyboardActive
|
||||
|
||||
LauncherContent {
|
||||
id: launcherContent
|
||||
anchors.fill: parent
|
||||
parentModal: root
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
139
quickshell/Modals/DankLauncherV2/GridItem.qml
Normal file
139
quickshell/Modals/DankLauncherV2/GridItem.qml
Normal file
@@ -0,0 +1,139 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var item: null
|
||||
property bool isSelected: false
|
||||
property bool isHovered: itemArea.containsMouse
|
||||
property var controller: null
|
||||
property int flatIndex: -1
|
||||
|
||||
signal clicked
|
||||
signal rightClicked(real mouseX, real mouseY)
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
width: parent.width - Theme.spacingM
|
||||
|
||||
Item {
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
property int iconSize: Math.min(48, Math.max(32, root.width * 0.45))
|
||||
|
||||
Image {
|
||||
id: appIcon
|
||||
anchors.fill: parent
|
||||
visible: root.item?.iconType === "image"
|
||||
asynchronous: true
|
||||
source: root.item?.iconType === "image" ? "image://icon/" + (root.item?.icon || "application-x-executable") : ""
|
||||
sourceSize.width: parent.iconSize
|
||||
sourceSize.height: parent.iconSize
|
||||
fillMode: Image.PreserveAspectFit
|
||||
cache: false
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: root.item?.iconType === "material" || root.item?.iconType === "nerd"
|
||||
name: root.item?.icon ?? "apps"
|
||||
size: parent.iconSize * 0.7
|
||||
color: root.isSelected ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: root.item?.iconType === "composite"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
source: {
|
||||
if (!root.item || root.item.iconType !== "composite")
|
||||
return "";
|
||||
var iconFull = root.item.iconFull || "";
|
||||
if (iconFull.startsWith("svg+corner:")) {
|
||||
var parts = iconFull.substring(11).split("|");
|
||||
return parts[0] || "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
sourceSize.width: parent.width
|
||||
sourceSize.height: parent.height
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
width: 16
|
||||
height: 16
|
||||
radius: 8
|
||||
color: Theme.surfaceContainer
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
if (!root.item || root.item.iconType !== "composite")
|
||||
return "";
|
||||
var iconFull = root.item.iconFull || "";
|
||||
if (iconFull.startsWith("svg+corner:")) {
|
||||
var parts = iconFull.substring(11).split("|");
|
||||
return parts[1] || "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
size: 12
|
||||
color: root.isSelected ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.item?.name ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: root.isSelected ? Theme.primary : Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
var scenePos = mapToItem(null, mouse.x, mouse.y);
|
||||
root.rightClicked(scenePos.x, scenePos.y);
|
||||
} else {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: {
|
||||
if (root.controller) {
|
||||
root.controller.keyboardNavigationActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
808
quickshell/Modals/DankLauncherV2/LauncherContent.qml
Normal file
808
quickshell/Modals/DankLauncherV2/LauncherContent.qml
Normal file
@@ -0,0 +1,808 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
property var parentModal: null
|
||||
property alias searchField: searchField
|
||||
property alias controller: controller
|
||||
property alias resultsList: resultsList
|
||||
property alias actionPanel: actionPanel
|
||||
|
||||
property bool editMode: false
|
||||
property var editingApp: null
|
||||
property string editAppId: ""
|
||||
|
||||
function resetScroll() {
|
||||
resultsList.resetScroll();
|
||||
}
|
||||
|
||||
function focusSearchField() {
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
|
||||
function openEditMode(app) {
|
||||
if (!app)
|
||||
return;
|
||||
editingApp = app;
|
||||
editAppId = app.id || app.execString || app.exec || "";
|
||||
var existing = SessionData.getAppOverride(editAppId);
|
||||
editNameField.text = existing?.name || "";
|
||||
editIconField.text = existing?.icon || "";
|
||||
editCommentField.text = existing?.comment || "";
|
||||
editEnvVarsField.text = existing?.envVars || "";
|
||||
editExtraFlagsField.text = existing?.extraFlags || "";
|
||||
editMode = true;
|
||||
Qt.callLater(() => editNameField.forceActiveFocus());
|
||||
}
|
||||
|
||||
function closeEditMode() {
|
||||
editMode = false;
|
||||
editingApp = null;
|
||||
editAppId = "";
|
||||
Qt.callLater(() => searchField.forceActiveFocus());
|
||||
}
|
||||
|
||||
function saveAppOverride() {
|
||||
var override = {};
|
||||
if (editNameField.text.trim())
|
||||
override.name = editNameField.text.trim();
|
||||
if (editIconField.text.trim())
|
||||
override.icon = editIconField.text.trim();
|
||||
if (editCommentField.text.trim())
|
||||
override.comment = editCommentField.text.trim();
|
||||
if (editEnvVarsField.text.trim())
|
||||
override.envVars = editEnvVarsField.text.trim();
|
||||
if (editExtraFlagsField.text.trim())
|
||||
override.extraFlags = editExtraFlagsField.text.trim();
|
||||
SessionData.setAppOverride(editAppId, override);
|
||||
closeEditMode();
|
||||
}
|
||||
|
||||
function resetAppOverride() {
|
||||
SessionData.clearAppOverride(editAppId);
|
||||
closeEditMode();
|
||||
}
|
||||
|
||||
function showContextMenu(item, x, y, fromKeyboard) {
|
||||
if (!item)
|
||||
return;
|
||||
if (item.isCore)
|
||||
return;
|
||||
if (!contextMenu.hasContextMenuActions(item))
|
||||
return;
|
||||
contextMenu.show(x, y, item, fromKeyboard);
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Controller {
|
||||
id: controller
|
||||
|
||||
onItemExecuted: {
|
||||
if (root.parentModal) {
|
||||
root.parentModal.hide();
|
||||
}
|
||||
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
|
||||
NiriService.toggleOverview();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LauncherContextMenu {
|
||||
id: contextMenu
|
||||
parent: root
|
||||
controller: root.controller
|
||||
searchField: root.searchField
|
||||
parentHandler: root
|
||||
|
||||
onEditAppRequested: app => {
|
||||
root.openEditMode(app);
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (editMode) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
closeEditMode();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var hasCtrl = event.modifiers & Qt.ControlModifier;
|
||||
event.accepted = true;
|
||||
|
||||
switch (event.key) {
|
||||
case Qt.Key_Escape:
|
||||
if (actionPanel.expanded) {
|
||||
actionPanel.hide();
|
||||
return;
|
||||
}
|
||||
if (controller.clearPluginFilter())
|
||||
return;
|
||||
if (root.parentModal)
|
||||
root.parentModal.hide();
|
||||
return;
|
||||
case Qt.Key_Backspace:
|
||||
if (searchField.text.length === 0) {
|
||||
if (controller.clearPluginFilter())
|
||||
return;
|
||||
if (controller.autoSwitchedToFiles) {
|
||||
controller.restorePreviousMode();
|
||||
return;
|
||||
}
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Down:
|
||||
controller.selectNext();
|
||||
return;
|
||||
case Qt.Key_Up:
|
||||
controller.selectPrevious();
|
||||
return;
|
||||
case Qt.Key_PageDown:
|
||||
controller.selectPageDown(8);
|
||||
return;
|
||||
case Qt.Key_PageUp:
|
||||
controller.selectPageUp(8);
|
||||
return;
|
||||
case Qt.Key_Right:
|
||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||
controller.selectRight();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Left:
|
||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||
controller.selectLeft();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_J:
|
||||
if (hasCtrl) {
|
||||
controller.selectNext();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_K:
|
||||
if (hasCtrl) {
|
||||
controller.selectPrevious();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_N:
|
||||
if (hasCtrl) {
|
||||
controller.selectNextSection();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_P:
|
||||
if (hasCtrl) {
|
||||
controller.selectPreviousSection();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Tab:
|
||||
if (actionPanel.hasActions) {
|
||||
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
|
||||
}
|
||||
return;
|
||||
case Qt.Key_Backtab:
|
||||
if (actionPanel.expanded)
|
||||
actionPanel.hide();
|
||||
return;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) {
|
||||
actionPanel.executeSelectedAction();
|
||||
} else {
|
||||
controller.executeSelected();
|
||||
}
|
||||
return;
|
||||
case Qt.Key_Menu:
|
||||
case Qt.Key_F10:
|
||||
if (contextMenu.hasContextMenuActions(controller.selectedItem)) {
|
||||
var scenePos = resultsList.getSelectedItemPosition();
|
||||
var localPos = root.mapFromItem(null, scenePos.x, scenePos.y);
|
||||
showContextMenu(controller.selectedItem, localPos.x, localPos.y, true);
|
||||
}
|
||||
return;
|
||||
case Qt.Key_1:
|
||||
if (hasCtrl) {
|
||||
controller.setMode("all");
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_2:
|
||||
if (hasCtrl) {
|
||||
controller.setMode("apps");
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_3:
|
||||
if (hasCtrl) {
|
||||
controller.setMode("files");
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_4:
|
||||
if (hasCtrl) {
|
||||
controller.setMode("plugins");
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Slash:
|
||||
if (event.modifiers === Qt.NoModifier && searchField.text.length === 0) {
|
||||
controller.setMode("files", true);
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
default:
|
||||
event.accepted = false;
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: !editMode
|
||||
|
||||
Rectangle {
|
||||
id: footerBar
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: SettingsData.dankLauncherV2ShowFooter ? 32 : 0
|
||||
visible: SettingsData.dankLauncherV2ShowFooter
|
||||
color: Theme.surfaceContainerHigh
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
Row {
|
||||
id: modeButtonsRow
|
||||
x: I18n.isRtl ? parent.width - width - Theme.spacingS : Theme.spacingS
|
||||
y: (parent.height - height) / 2
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{
|
||||
id: "all",
|
||||
label: I18n.tr("All"),
|
||||
icon: "search"
|
||||
},
|
||||
{
|
||||
id: "apps",
|
||||
label: I18n.tr("Apps"),
|
||||
icon: "apps"
|
||||
},
|
||||
{
|
||||
id: "files",
|
||||
label: I18n.tr("Files"),
|
||||
icon: "folder"
|
||||
},
|
||||
{
|
||||
id: "plugins",
|
||||
label: I18n.tr("Plugins"),
|
||||
icon: "extension"
|
||||
}
|
||||
]
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: modeButtonMetrics.width + 14 + Theme.spacingXS + Theme.spacingM * 2 + Theme.spacingS
|
||||
height: footerBar.height - 4
|
||||
radius: Theme.cornerRadius - 2
|
||||
color: controller.searchMode === modelData.id || modeArea.containsMouse ? Theme.primaryContainer : "transparent"
|
||||
|
||||
TextMetrics {
|
||||
id: modeButtonMetrics
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
text: modelData.label
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon
|
||||
size: 14
|
||||
color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.label
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: modeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: controller.setMode(modelData.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: hintsRow
|
||||
x: I18n.isRtl ? Theme.spacingS : parent.width - width - Theme.spacingS
|
||||
y: (parent.height - height) / 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: "↑↓ " + I18n.tr("nav")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "↵ " + I18n.tr("open")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Tab " + I18n.tr("actions")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
visible: actionPanel.hasActions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: footerBar.top
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingM
|
||||
anchors.bottomMargin: Theme.spacingXS
|
||||
spacing: Theme.spacingXS
|
||||
clip: false
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
id: pluginBadge
|
||||
visible: controller.activePluginName.length > 0
|
||||
width: visible ? pluginBadgeContent.implicitWidth + Theme.spacingM : 0
|
||||
height: searchField.height
|
||||
radius: 16
|
||||
color: Theme.primary
|
||||
|
||||
Row {
|
||||
id: pluginBadgeContent
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: "extension"
|
||||
size: 14
|
||||
color: Theme.primaryText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: controller.activePluginName
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.primaryText
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0)
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
leftIconColor: Theme.surfaceVariantText
|
||||
leftIconFocusedColor: Theme.primary
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
enabled: root.parentModal ? root.parentModal.spotlightOpen : true
|
||||
placeholderText: ""
|
||||
ignoreUpDownKeys: true
|
||||
ignoreTabKeys: true
|
||||
keyForwardTargets: [root]
|
||||
|
||||
onTextChanged: {
|
||||
controller.setSearchQuery(text);
|
||||
if (text.length === 0) {
|
||||
controller.restorePreviousMode();
|
||||
}
|
||||
if (actionPanel.expanded) {
|
||||
actionPanel.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (root.parentModal) {
|
||||
root.parentModal.hide();
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter)) {
|
||||
if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) {
|
||||
actionPanel.executeSelectedAction();
|
||||
} else {
|
||||
controller.executeSelected();
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - searchField.height - actionPanel.height - Theme.spacingXS * 2
|
||||
opacity: root.parentModal?.isClosing ? 0 : 1
|
||||
|
||||
ResultsList {
|
||||
id: resultsList
|
||||
anchors.fill: parent
|
||||
controller: root.controller
|
||||
|
||||
onItemRightClicked: (index, item, sceneX, sceneY) => {
|
||||
if (item && contextMenu.hasContextMenuActions(item)) {
|
||||
var localPos = root.mapFromItem(null, sceneX, sceneY);
|
||||
root.showContextMenu(item, localPos.x, localPos.y, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ActionPanel {
|
||||
id: actionPanel
|
||||
width: parent.width
|
||||
selectedItem: controller.selectedItem
|
||||
controller: controller
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: controller
|
||||
function onSelectedItemChanged() {
|
||||
if (actionPanel.expanded && !actionPanel.hasActions) {
|
||||
actionPanel.hide();
|
||||
}
|
||||
}
|
||||
function onSearchQueryRequested(query) {
|
||||
searchField.text = query;
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: editView
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
visible: editMode
|
||||
focus: editMode
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
closeEditMode();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
saveAppOverride();
|
||||
event.accepted = true;
|
||||
}
|
||||
} else if (event.key === Qt.Key_S && event.modifiers & Qt.ControlModifier) {
|
||||
saveAppOverride();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: backButtonArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "arrow_back"
|
||||
size: 20
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: closeEditMode()
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
width: 40
|
||||
height: 40
|
||||
source: editingApp?.icon ? "image://icon/" + editingApp.icon : "image://icon/application-x-executable"
|
||||
sourceSize.width: 40
|
||||
sourceSize.height: 40
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Edit App")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: editingApp?.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outlineMedium
|
||||
}
|
||||
|
||||
Flickable {
|
||||
width: parent.width
|
||||
height: parent.height - y - buttonsRow.height - Theme.spacingM
|
||||
contentHeight: editFieldsColumn.height
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Column {
|
||||
id: editFieldsColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Name")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editNameField
|
||||
width: parent.width
|
||||
placeholderText: editingApp?.name || ""
|
||||
keyNavigationTab: editIconField
|
||||
keyNavigationBacktab: editExtraFlagsField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Icon")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editIconField
|
||||
width: parent.width
|
||||
placeholderText: editingApp?.icon || ""
|
||||
keyNavigationTab: editCommentField
|
||||
keyNavigationBacktab: editNameField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Description")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editCommentField
|
||||
width: parent.width
|
||||
placeholderText: editingApp?.comment || ""
|
||||
keyNavigationTab: editEnvVarsField
|
||||
keyNavigationBacktab: editIconField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Environment Variables")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "KEY=value KEY2=value2"
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editEnvVarsField
|
||||
width: parent.width
|
||||
placeholderText: "VAR=value"
|
||||
keyNavigationTab: editExtraFlagsField
|
||||
keyNavigationBacktab: editCommentField
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Extra Arguments")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: editExtraFlagsField
|
||||
width: parent.width
|
||||
placeholderText: "--flag --option=value"
|
||||
keyNavigationTab: editNameField
|
||||
keyNavigationBacktab: editEnvVarsField
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: buttonsRow
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
id: resetButton
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: resetButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
|
||||
visible: SessionData.getAppOverride(editAppId) !== null
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Reset")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.error
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: resetButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: resetAppOverride()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: cancelButton
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: closeEditMode()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: saveButton
|
||||
width: 90
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: saveButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.9) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Save")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primaryText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: saveButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: saveAppOverride()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
484
quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml
Normal file
484
quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml
Normal file
@@ -0,0 +1,484 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property var item: null
|
||||
property var controller: null
|
||||
property var searchField: null
|
||||
property var parentHandler: null
|
||||
|
||||
signal hideRequested
|
||||
signal editAppRequested(var app)
|
||||
|
||||
function hasContextMenuActions(spotlightItem) {
|
||||
if (!spotlightItem)
|
||||
return false;
|
||||
if (spotlightItem.type === "app" && !spotlightItem.isCore)
|
||||
return true;
|
||||
if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
|
||||
var instance = PluginService.pluginInstances[spotlightItem.pluginId];
|
||||
if (!instance)
|
||||
return false;
|
||||
if (typeof instance.getContextMenuActions !== "function")
|
||||
return false;
|
||||
var actions = instance.getContextMenuActions(spotlightItem.data);
|
||||
return Array.isArray(actions) && actions.length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
readonly property var desktopEntry: item?.data ?? null
|
||||
readonly property string appId: desktopEntry?.id || desktopEntry?.execString || ""
|
||||
readonly property bool isPinned: SessionData.isPinnedApp(appId)
|
||||
readonly property bool isRegularApp: item?.type === "app" && !item.isCore && desktopEntry
|
||||
readonly property bool isPluginItem: item?.type === "plugin"
|
||||
|
||||
function getPluginContextMenuActions() {
|
||||
if (!isPluginItem || !item?.pluginId)
|
||||
return [];
|
||||
|
||||
var instance = PluginService.pluginInstances[item.pluginId];
|
||||
if (!instance)
|
||||
return [];
|
||||
if (typeof instance.getContextMenuActions !== "function")
|
||||
return [];
|
||||
|
||||
var actions = instance.getContextMenuActions(item.data);
|
||||
if (!Array.isArray(actions))
|
||||
return [];
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
function executePluginAction(actionFunc) {
|
||||
if (typeof actionFunc === "function") {
|
||||
actionFunc();
|
||||
}
|
||||
controller?.performSearch();
|
||||
hide();
|
||||
}
|
||||
|
||||
readonly property var menuItems: {
|
||||
var items = [];
|
||||
|
||||
if (isPluginItem) {
|
||||
var pluginActions = getPluginContextMenuActions();
|
||||
for (var i = 0; i < pluginActions.length; i++) {
|
||||
var act = pluginActions[i];
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: act.icon || "play_arrow",
|
||||
text: act.text || act.name || "",
|
||||
pluginAction: act.action
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
if (!desktopEntry)
|
||||
return items;
|
||||
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: isPinned ? "keep_off" : "push_pin",
|
||||
text: isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock"),
|
||||
action: togglePin
|
||||
});
|
||||
|
||||
if (isRegularApp) {
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: "visibility_off",
|
||||
text: I18n.tr("Hide App"),
|
||||
action: hideCurrentApp
|
||||
});
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: "edit",
|
||||
text: I18n.tr("Edit App"),
|
||||
action: editCurrentApp
|
||||
});
|
||||
}
|
||||
|
||||
if (item?.actions && item.actions.length > 0) {
|
||||
items.push({
|
||||
type: "separator"
|
||||
});
|
||||
for (var i = 0; i < item.actions.length; i++) {
|
||||
var act = item.actions[i];
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: act.icon || "play_arrow",
|
||||
text: act.name || "",
|
||||
actionData: act
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
items.push({
|
||||
type: "separator"
|
||||
});
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: "launch",
|
||||
text: I18n.tr("Launch"),
|
||||
action: launchApp
|
||||
});
|
||||
|
||||
if (SessionService.nvidiaCommand) {
|
||||
items.push({
|
||||
type: "separator"
|
||||
});
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: "memory",
|
||||
text: I18n.tr("Launch on dGPU"),
|
||||
action: launchWithNvidia
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function show(x, y, spotlightItem, fromKeyboard) {
|
||||
if (!spotlightItem?.data)
|
||||
return;
|
||||
item = spotlightItem;
|
||||
selectedMenuIndex = fromKeyboard ? 0 : -1;
|
||||
keyboardNavigation = fromKeyboard;
|
||||
|
||||
if (parentHandler)
|
||||
parentHandler.enabled = false;
|
||||
|
||||
Qt.callLater(() => {
|
||||
var parentW = parent?.width ?? 500;
|
||||
var parentH = parent?.height ?? 600;
|
||||
var menuW = width > 0 ? width : 200;
|
||||
var menuH = height > 0 ? height : 200;
|
||||
var margin = 8;
|
||||
|
||||
var posX = x + 4;
|
||||
var posY = y + 4;
|
||||
|
||||
if (posX + menuW > parentW - margin) {
|
||||
posX = Math.max(margin, parentW - menuW - margin);
|
||||
}
|
||||
if (posY + menuH > parentH - margin) {
|
||||
posY = Math.max(margin, parentH - menuH - margin);
|
||||
}
|
||||
|
||||
root.x = posX;
|
||||
root.y = posY;
|
||||
open();
|
||||
});
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (parentHandler)
|
||||
parentHandler.enabled = true;
|
||||
close();
|
||||
}
|
||||
|
||||
function togglePin() {
|
||||
if (!appId)
|
||||
return;
|
||||
if (isPinned)
|
||||
SessionData.removePinnedApp(appId);
|
||||
else
|
||||
SessionData.addPinnedApp(appId);
|
||||
hide();
|
||||
}
|
||||
|
||||
function hideCurrentApp() {
|
||||
if (!appId)
|
||||
return;
|
||||
SessionData.hideApp(appId);
|
||||
controller?.performSearch();
|
||||
hide();
|
||||
}
|
||||
|
||||
function editCurrentApp() {
|
||||
if (!desktopEntry)
|
||||
return;
|
||||
editAppRequested(desktopEntry);
|
||||
hide();
|
||||
}
|
||||
|
||||
function launchApp() {
|
||||
if (!desktopEntry)
|
||||
return;
|
||||
SessionService.launchDesktopEntry(desktopEntry);
|
||||
AppUsageHistoryData.addAppUsage(desktopEntry);
|
||||
controller?.itemExecuted();
|
||||
hide();
|
||||
}
|
||||
|
||||
function launchWithNvidia() {
|
||||
if (!desktopEntry)
|
||||
return;
|
||||
SessionService.launchDesktopEntry(desktopEntry, true);
|
||||
AppUsageHistoryData.addAppUsage(desktopEntry);
|
||||
controller?.itemExecuted();
|
||||
hide();
|
||||
}
|
||||
|
||||
function executeDesktopAction(actionData) {
|
||||
if (!desktopEntry || !actionData)
|
||||
return;
|
||||
SessionService.launchDesktopAction(desktopEntry, actionData.actionData || actionData);
|
||||
AppUsageHistoryData.addAppUsage(desktopEntry);
|
||||
controller?.itemExecuted();
|
||||
hide();
|
||||
}
|
||||
|
||||
property int selectedMenuIndex: 0
|
||||
property bool keyboardNavigation: false
|
||||
|
||||
readonly property int visibleItemCount: {
|
||||
var count = 0;
|
||||
for (var i = 0; i < menuItems.length; i++) {
|
||||
if (menuItems[i].type === "item")
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (visibleItemCount > 0)
|
||||
selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount;
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (visibleItemCount > 0)
|
||||
selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount;
|
||||
}
|
||||
|
||||
function activateSelected() {
|
||||
var itemIndex = 0;
|
||||
for (var i = 0; i < menuItems.length; i++) {
|
||||
if (menuItems[i].type !== "item")
|
||||
continue;
|
||||
if (itemIndex === selectedMenuIndex) {
|
||||
var menuItem = menuItems[i];
|
||||
if (menuItem.action)
|
||||
menuItem.action();
|
||||
else if (menuItem.pluginAction)
|
||||
executePluginAction(menuItem.pluginAction);
|
||||
else if (menuItem.actionData)
|
||||
executeDesktopAction(menuItem.actionData);
|
||||
return;
|
||||
}
|
||||
itemIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
width: menuContainer.implicitWidth
|
||||
height: menuContainer.implicitHeight
|
||||
padding: 0
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
modal: true
|
||||
dim: false
|
||||
background: Item {}
|
||||
|
||||
onOpened: {
|
||||
Qt.callLater(() => keyboardHandler.forceActiveFocus());
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
if (parentHandler)
|
||||
parentHandler.enabled = true;
|
||||
if (searchField?.visible) {
|
||||
Qt.callLater(() => searchField.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
enter: Transition {
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
id: keyboardHandler
|
||||
focus: true
|
||||
implicitWidth: menuContainer.implicitWidth
|
||||
implicitHeight: menuContainer.implicitHeight
|
||||
|
||||
Keys.onPressed: event => {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Down:
|
||||
root.selectNext();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Up:
|
||||
root.selectPrevious();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
root.activateSelected();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Escape:
|
||||
case Qt.Key_Left:
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: menuContainer
|
||||
anchors.fill: parent
|
||||
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
|
||||
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: -1
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Repeater {
|
||||
model: root.menuItems
|
||||
|
||||
Item {
|
||||
id: menuItemDelegate
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: menuColumn.width
|
||||
height: modelData.type === "separator" ? 5 : 32
|
||||
|
||||
readonly property int itemIndex: {
|
||||
var count = 0;
|
||||
for (var i = 0; i < index; i++) {
|
||||
if (root.menuItems[i].type === "item")
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: menuItemDelegate.modelData.type === "separator"
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: parent.height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: menuItemDelegate.modelData.type === "item"
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
|
||||
}
|
||||
return itemMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent";
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
width: Theme.iconSize - 2
|
||||
height: Theme.iconSize - 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
visible: (menuItemDelegate.modelData?.icon ?? "").length > 0
|
||||
name: menuItemDelegate.modelData?.icon ?? ""
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: menuItemDelegate.modelData.text || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - (Theme.iconSize - 2) - Theme.spacingS
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: {
|
||||
root.keyboardNavigation = false;
|
||||
root.selectedMenuIndex = menuItemDelegate.itemIndex;
|
||||
}
|
||||
onClicked: {
|
||||
var menuItem = menuItemDelegate.modelData;
|
||||
if (menuItem.action)
|
||||
menuItem.action();
|
||||
else if (menuItem.pluginAction)
|
||||
root.executePluginAction(menuItem.pluginAction);
|
||||
else if (menuItem.actionData)
|
||||
root.executeDesktopAction(menuItem.actionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
189
quickshell/Modals/DankLauncherV2/ResultItem.qml
Normal file
189
quickshell/Modals/DankLauncherV2/ResultItem.qml
Normal file
@@ -0,0 +1,189 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var item: null
|
||||
property bool isSelected: false
|
||||
property bool isHovered: itemArea.containsMouse
|
||||
property var controller: null
|
||||
property int flatIndex: -1
|
||||
|
||||
signal clicked
|
||||
signal rightClicked(real mouseX, real mouseY)
|
||||
|
||||
width: parent?.width ?? 200
|
||||
height: 52
|
||||
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Item {
|
||||
width: 36
|
||||
height: 36
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Image {
|
||||
id: appIcon
|
||||
anchors.fill: parent
|
||||
visible: root.item?.iconType === "image"
|
||||
asynchronous: true
|
||||
source: root.item?.iconType === "image" ? "image://icon/" + (root.item?.icon || "application-x-executable") : ""
|
||||
sourceSize.width: 36
|
||||
sourceSize.height: 36
|
||||
fillMode: Image.PreserveAspectFit
|
||||
cache: false
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: root.item?.iconType === "material" || root.item?.iconType === "nerd"
|
||||
name: root.item?.icon ?? "apps"
|
||||
size: 24
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: root.item?.iconType === "composite"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
source: {
|
||||
if (!root.item || root.item.iconType !== "composite")
|
||||
return "";
|
||||
var iconFull = root.item.iconFull || "";
|
||||
if (iconFull.startsWith("svg+corner:")) {
|
||||
var parts = iconFull.substring(11).split("|");
|
||||
return parts[0] || "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
sourceSize.width: 36
|
||||
sourceSize.height: 36
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
width: 16
|
||||
height: 16
|
||||
radius: 8
|
||||
color: Theme.surfaceContainer
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
if (!root.item || root.item.iconType !== "composite")
|
||||
return "";
|
||||
var iconFull = root.item.iconFull || "";
|
||||
if (iconFull.startsWith("svg+corner:")) {
|
||||
var parts = iconFull.substring(11).split("|");
|
||||
return parts[1] || "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
size: 12
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 36 - Theme.spacingM * 3 - rightContent.width
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.item?.name ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.item?.subtitle ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: rightContent
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
visible: root.item?.type && root.item.type !== "app"
|
||||
width: typeBadge.implicitWidth + Theme.spacingS * 2
|
||||
height: 20
|
||||
radius: 10
|
||||
color: Theme.surfaceVariantAlpha
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
id: typeBadge
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
if (!root.item)
|
||||
return "";
|
||||
switch (root.item.type) {
|
||||
case "calculator":
|
||||
return I18n.tr("Calc");
|
||||
case "plugin":
|
||||
return I18n.tr("Plugin");
|
||||
case "file":
|
||||
return I18n.tr("File");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
var scenePos = mapToItem(null, mouse.x, mouse.y);
|
||||
root.rightClicked(scenePos.x, scenePos.y);
|
||||
} else {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: {
|
||||
if (root.controller) {
|
||||
root.controller.keyboardNavigationActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
484
quickshell/Modals/DankLauncherV2/ResultsList.qml
Normal file
484
quickshell/Modals/DankLauncherV2/ResultsList.qml
Normal file
@@ -0,0 +1,484 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var controller: null
|
||||
property int gridColumns: controller?.gridColumns ?? 4
|
||||
|
||||
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
|
||||
|
||||
function resetScroll() {
|
||||
mainFlickable.contentY = 0;
|
||||
}
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || !controller?.flatModel || index >= controller.flatModel.length)
|
||||
return;
|
||||
var entry = controller.flatModel[index];
|
||||
if (!entry || entry.isHeader)
|
||||
return;
|
||||
scrollItemIntoView(index, entry.sectionId);
|
||||
}
|
||||
|
||||
function scrollItemIntoView(flatIndex, sectionId) {
|
||||
var sections = controller?.sections ?? [];
|
||||
var sectionIndex = -1;
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
if (sections[i].id === sectionId) {
|
||||
sectionIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sectionIndex < 0)
|
||||
return;
|
||||
var itemInSection = 0;
|
||||
var foundSection = false;
|
||||
for (var i = 0; i < controller.flatModel.length && i < flatIndex; i++) {
|
||||
var e = controller.flatModel[i];
|
||||
if (e.isHeader && e.section?.id === sectionId)
|
||||
foundSection = true;
|
||||
else if (foundSection && !e.isHeader && e.sectionId === sectionId)
|
||||
itemInSection++;
|
||||
}
|
||||
|
||||
var mode = controller.getSectionViewMode(sectionId);
|
||||
var sectionY = 0;
|
||||
for (var i = 0; i < sectionIndex; i++) {
|
||||
sectionY += getSectionHeight(sections[i]);
|
||||
}
|
||||
|
||||
var itemY, itemHeight;
|
||||
if (mode === "list") {
|
||||
itemY = itemInSection * 52;
|
||||
itemHeight = 52;
|
||||
} else {
|
||||
var cols = controller.getGridColumns(sectionId);
|
||||
var cellWidth = mode === "tile" ? Math.floor(mainFlickable.width / 3) : Math.floor((mainFlickable.width - (root.gridColumns - 1) * 4) / root.gridColumns);
|
||||
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
|
||||
var row = Math.floor(itemInSection / cols);
|
||||
itemY = row * cellHeight;
|
||||
itemHeight = cellHeight;
|
||||
}
|
||||
|
||||
var targetY = sectionY + 32 + itemY;
|
||||
var targetBottom = targetY + itemHeight;
|
||||
var stickyHeight = mainFlickable.contentY > 0 ? 32 : 0;
|
||||
|
||||
var shadowPadding = 24;
|
||||
if (targetY < mainFlickable.contentY + stickyHeight) {
|
||||
mainFlickable.contentY = Math.max(0, targetY - 32);
|
||||
} else if (targetBottom > mainFlickable.contentY + mainFlickable.height - shadowPadding) {
|
||||
mainFlickable.contentY = Math.min(mainFlickable.contentHeight - mainFlickable.height, targetBottom - mainFlickable.height + shadowPadding);
|
||||
}
|
||||
}
|
||||
|
||||
function getSectionHeight(section) {
|
||||
var mode = controller?.getSectionViewMode(section.id) ?? "list";
|
||||
if (section.collapsed)
|
||||
return 32;
|
||||
|
||||
if (mode === "list") {
|
||||
return 32 + (section.items?.length ?? 0) * 52;
|
||||
} else {
|
||||
var cols = controller?.getGridColumns(section.id) ?? root.gridColumns;
|
||||
var rows = Math.ceil((section.items?.length ?? 0) / cols);
|
||||
var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor((root.width - (cols - 1) * 4) / cols);
|
||||
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
|
||||
return 32 + rows * cellHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedItemPosition() {
|
||||
var fallback = mapToItem(null, width / 2, height / 2);
|
||||
if (!controller?.flatModel || controller.selectedFlatIndex < 0)
|
||||
return fallback;
|
||||
|
||||
var entry = controller.flatModel[controller.selectedFlatIndex];
|
||||
if (!entry || entry.isHeader)
|
||||
return fallback;
|
||||
|
||||
var sections = controller.sections;
|
||||
var sectionIndex = -1;
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
if (sections[i].id === entry.sectionId) {
|
||||
sectionIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sectionIndex < 0)
|
||||
return fallback;
|
||||
|
||||
var sectionY = 0;
|
||||
for (var i = 0; i < sectionIndex; i++) {
|
||||
sectionY += getSectionHeight(sections[i]);
|
||||
}
|
||||
|
||||
var mode = controller.getSectionViewMode(entry.sectionId);
|
||||
var itemInSection = entry.indexInSection || 0;
|
||||
|
||||
var itemY, itemX, itemH;
|
||||
if (mode === "list") {
|
||||
itemY = sectionY + 32 + itemInSection * 52;
|
||||
itemX = width / 2;
|
||||
itemH = 52;
|
||||
} else {
|
||||
var cols = controller.getGridColumns(entry.sectionId);
|
||||
var cellWidth = mode === "tile" ? Math.floor(width / 3) : Math.floor((width - (cols - 1) * 4) / cols);
|
||||
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
|
||||
var row = Math.floor(itemInSection / cols);
|
||||
var col = itemInSection % cols;
|
||||
itemY = sectionY + 32 + row * cellHeight;
|
||||
itemX = col * cellWidth + cellWidth / 2;
|
||||
itemH = cellHeight;
|
||||
}
|
||||
|
||||
var visualY = itemY - mainFlickable.contentY + itemH / 2;
|
||||
var clampedY = Math.max(40, Math.min(height - 40, visualY));
|
||||
return mapToItem(null, itemX, clampedY);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.controller
|
||||
function onSelectedFlatIndexChanged() {
|
||||
if (root.controller?.keyboardNavigationActive) {
|
||||
Qt.callLater(() => root.ensureVisible(root.controller.selectedFlatIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
id: mainFlickable
|
||||
anchors.fill: parent
|
||||
contentWidth: width
|
||||
contentHeight: sectionsColumn.height
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: sectionsColumn
|
||||
width: parent.width
|
||||
|
||||
Repeater {
|
||||
model: root.controller?.sections ?? []
|
||||
|
||||
Column {
|
||||
id: sectionDelegate
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
|
||||
readonly property string sectionId: modelData?.id ?? ""
|
||||
readonly property string currentViewMode: {
|
||||
void (versionTrigger);
|
||||
return root.controller?.getSectionViewMode(sectionId) ?? "list";
|
||||
}
|
||||
readonly property bool isGridMode: currentViewMode === "grid" || currentViewMode === "tile"
|
||||
readonly property bool isCollapsed: modelData?.collapsed ?? false
|
||||
|
||||
width: sectionsColumn.width
|
||||
|
||||
SectionHeader {
|
||||
width: parent.width
|
||||
height: 32
|
||||
section: sectionDelegate.modelData
|
||||
controller: root.controller
|
||||
viewMode: sectionDelegate.currentViewMode
|
||||
canChangeViewMode: root.controller?.canChangeSectionViewMode(sectionDelegate.sectionId) ?? false
|
||||
canCollapse: root.controller?.canCollapseSection(sectionDelegate.sectionId) ?? false
|
||||
}
|
||||
|
||||
Column {
|
||||
id: listContent
|
||||
width: parent.width
|
||||
visible: !sectionDelegate.isGridMode && !sectionDelegate.isCollapsed
|
||||
|
||||
Repeater {
|
||||
model: sectionDelegate.isGridMode || sectionDelegate.isCollapsed ? [] : (sectionDelegate.modelData?.items ?? [])
|
||||
|
||||
ResultItem {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: listContent.width
|
||||
height: 52
|
||||
item: modelData
|
||||
isSelected: getFlatIndex() === root.controller?.selectedFlatIndex
|
||||
controller: root.controller
|
||||
flatIndex: getFlatIndex()
|
||||
|
||||
function getFlatIndex() {
|
||||
if (!sectionDelegate?.sectionId)
|
||||
return -1;
|
||||
var flatIdx = 0;
|
||||
var sections = root.controller?.sections ?? [];
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
flatIdx++;
|
||||
if (sections[i].id === sectionDelegate.sectionId)
|
||||
return flatIdx + index;
|
||||
if (!sections[i].collapsed)
|
||||
flatIdx += sections[i].items?.length ?? 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (root.controller) {
|
||||
root.controller.executeItem(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: (mouseX, mouseY) => {
|
||||
root.itemRightClicked(getFlatIndex(), modelData, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Grid {
|
||||
id: gridContent
|
||||
width: parent.width
|
||||
visible: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed
|
||||
columns: sectionDelegate.currentViewMode === "tile" ? 3 : root.gridColumns
|
||||
|
||||
readonly property real cellWidth: sectionDelegate.currentViewMode === "tile" ? Math.floor(width / 3) : Math.floor((width - (root.gridColumns - 1) * 4) / root.gridColumns)
|
||||
readonly property real cellHeight: sectionDelegate.currentViewMode === "tile" ? cellWidth * 0.75 : cellWidth + 24
|
||||
|
||||
Repeater {
|
||||
model: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed ? (sectionDelegate.modelData?.items ?? []) : []
|
||||
|
||||
Item {
|
||||
id: gridDelegateItem
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: gridContent.cellWidth
|
||||
height: gridContent.cellHeight
|
||||
|
||||
function getFlatIndex() {
|
||||
if (!sectionDelegate?.sectionId)
|
||||
return -1;
|
||||
var flatIdx = 0;
|
||||
var sections = root.controller?.sections ?? [];
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
flatIdx++;
|
||||
if (sections[i].id === sectionDelegate.sectionId)
|
||||
return flatIdx + index;
|
||||
if (!sections[i].collapsed)
|
||||
flatIdx += sections[i].items?.length ?? 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
readonly property int cachedFlatIndex: getFlatIndex()
|
||||
|
||||
GridItem {
|
||||
width: parent.width - 4
|
||||
height: parent.height - 4
|
||||
anchors.centerIn: parent
|
||||
visible: sectionDelegate.currentViewMode === "grid"
|
||||
item: gridDelegateItem.modelData
|
||||
isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex
|
||||
controller: root.controller
|
||||
flatIndex: gridDelegateItem.cachedFlatIndex
|
||||
|
||||
onClicked: {
|
||||
if (root.controller) {
|
||||
root.controller.executeItem(gridDelegateItem.modelData);
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: (mouseX, mouseY) => {
|
||||
root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
TileItem {
|
||||
width: parent.width - 4
|
||||
height: parent.height - 4
|
||||
anchors.centerIn: parent
|
||||
visible: sectionDelegate.currentViewMode === "tile"
|
||||
item: gridDelegateItem.modelData
|
||||
isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex
|
||||
controller: root.controller
|
||||
flatIndex: gridDelegateItem.cachedFlatIndex
|
||||
|
||||
onClicked: {
|
||||
if (root.controller) {
|
||||
root.controller.executeItem(gridDelegateItem.modelData);
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: (mouseX, mouseY) => {
|
||||
root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bottomShadow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 24
|
||||
z: 100
|
||||
visible: {
|
||||
if (mainFlickable.contentHeight <= mainFlickable.height)
|
||||
return false;
|
||||
var atBottom = mainFlickable.contentY >= mainFlickable.contentHeight - mainFlickable.height - 5;
|
||||
if (atBottom)
|
||||
return false;
|
||||
|
||||
var flatModel = root.controller?.flatModel;
|
||||
if (!flatModel || flatModel.length === 0)
|
||||
return false;
|
||||
var lastItemIdx = -1;
|
||||
for (var i = flatModel.length - 1; i >= 0; i--) {
|
||||
if (!flatModel[i].isHeader) {
|
||||
lastItemIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastItemIdx >= 0 && root.controller?.selectedFlatIndex === lastItemIdx)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: "transparent"
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: stickyHeader
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
height: 32
|
||||
z: 101
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
visible: stickyHeaderSection !== null
|
||||
|
||||
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
|
||||
|
||||
readonly property var stickyHeaderSection: {
|
||||
if (!root.controller?.sections || root.controller.sections.length === 0)
|
||||
return null;
|
||||
var sections = root.controller.sections;
|
||||
if (sections.length === 0)
|
||||
return null;
|
||||
|
||||
var scrollY = mainFlickable.contentY;
|
||||
if (scrollY <= 0)
|
||||
return null;
|
||||
|
||||
var y = 0;
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
var section = sections[i];
|
||||
var sectionHeight = root.getSectionHeight(section);
|
||||
if (scrollY < y + sectionHeight)
|
||||
return section;
|
||||
y += sectionHeight;
|
||||
}
|
||||
return sections[sections.length - 1];
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
width: parent.width
|
||||
section: stickyHeader.stickyHeaderSection
|
||||
controller: root.controller
|
||||
viewMode: {
|
||||
void (stickyHeader.versionTrigger);
|
||||
return root.controller?.getSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? "list";
|
||||
}
|
||||
canChangeViewMode: {
|
||||
void (stickyHeader.versionTrigger);
|
||||
return root.controller?.canChangeSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? false;
|
||||
}
|
||||
canCollapse: {
|
||||
void (stickyHeader.versionTrigger);
|
||||
return root.controller?.canCollapseSection(stickyHeader.stickyHeaderSection?.id) ?? false;
|
||||
}
|
||||
isSticky: true
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
visible: (!root.controller?.sections || root.controller.sections.length === 0) && !root.controller?.isFileSearching
|
||||
width: emptyColumn.implicitWidth
|
||||
height: emptyColumn.implicitHeight
|
||||
|
||||
Column {
|
||||
id: emptyColumn
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
name: getEmptyIcon()
|
||||
size: 48
|
||||
color: Theme.outlineButton
|
||||
|
||||
function getEmptyIcon() {
|
||||
var mode = root.controller?.searchMode ?? "all";
|
||||
switch (mode) {
|
||||
case "files":
|
||||
return "folder_open";
|
||||
case "plugins":
|
||||
return "extension";
|
||||
case "apps":
|
||||
return "apps";
|
||||
default:
|
||||
return root.controller?.searchQuery?.length > 0 ? "search_off" : "search";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: getEmptyText()
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
function getEmptyText() {
|
||||
var mode = root.controller?.searchMode ?? "all";
|
||||
var hasQuery = root.controller?.searchQuery?.length > 0;
|
||||
|
||||
switch (mode) {
|
||||
case "files":
|
||||
if (!DSearchService.dsearchAvailable)
|
||||
return I18n.tr("File search requires dsearch\nInstall from github.com/morelazers/dsearch");
|
||||
if (!hasQuery)
|
||||
return I18n.tr("Type to search files");
|
||||
if (root.controller.searchQuery.length < 2)
|
||||
return I18n.tr("Type at least 2 characters");
|
||||
return I18n.tr("No files found");
|
||||
case "plugins":
|
||||
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
|
||||
case "apps":
|
||||
return hasQuery ? I18n.tr("No apps found") : I18n.tr("Type to search apps");
|
||||
default:
|
||||
return hasQuery ? I18n.tr("No results found") : I18n.tr("Type to search");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
245
quickshell/Modals/DankLauncherV2/Scorer.js
Normal file
245
quickshell/Modals/DankLauncherV2/Scorer.js
Normal file
@@ -0,0 +1,245 @@
|
||||
.pragma library
|
||||
|
||||
const Weights = {
|
||||
exactMatch: 10000,
|
||||
prefixMatch: 5000,
|
||||
wordBoundary: 1000,
|
||||
substring: 500,
|
||||
fuzzy: 100,
|
||||
frecency: 2000,
|
||||
typeBonus: {
|
||||
app: 1000,
|
||||
plugin: 900,
|
||||
file: 800,
|
||||
action: 600
|
||||
}
|
||||
}
|
||||
|
||||
function tokenize(text) {
|
||||
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function(w) { return w.length > 0 })
|
||||
}
|
||||
|
||||
function hasWordBoundaryMatch(text, query) {
|
||||
var textWords = tokenize(text)
|
||||
var queryWords = tokenize(query)
|
||||
|
||||
if (queryWords.length === 0) return false
|
||||
if (queryWords.length > textWords.length) return false
|
||||
|
||||
for (var i = 0; i <= textWords.length - queryWords.length; i++) {
|
||||
var allMatch = true
|
||||
for (var j = 0; j < queryWords.length; j++) {
|
||||
if (!textWords[i + j].startsWith(queryWords[j])) {
|
||||
allMatch = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (allMatch) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function levenshteinDistance(s1, s2) {
|
||||
var len1 = s1.length
|
||||
var len2 = s2.length
|
||||
var matrix = []
|
||||
|
||||
for (var i = 0; i <= len1; i++) {
|
||||
matrix[i] = [i]
|
||||
}
|
||||
for (var j = 0; j <= len2; j++) {
|
||||
matrix[0][j] = j
|
||||
}
|
||||
|
||||
for (var i = 1; i <= len1; i++) {
|
||||
for (var j = 1; j <= len2; j++) {
|
||||
var cost = s1[i - 1] === s2[j - 1] ? 0 : 1
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i - 1][j] + 1,
|
||||
matrix[i][j - 1] + 1,
|
||||
matrix[i - 1][j - 1] + cost
|
||||
)
|
||||
}
|
||||
}
|
||||
return matrix[len1][len2]
|
||||
}
|
||||
|
||||
function fuzzyScore(text, query) {
|
||||
var maxDistance = query.length === 3 ? 1 : query.length <= 6 ? 2 : 3
|
||||
var bestScore = 0
|
||||
|
||||
if (Math.abs(text.length - query.length) <= maxDistance) {
|
||||
var distance = levenshteinDistance(text, query)
|
||||
if (distance <= maxDistance) {
|
||||
var maxLen = Math.max(text.length, query.length)
|
||||
bestScore = 1 - (distance / maxLen)
|
||||
}
|
||||
}
|
||||
|
||||
var words = tokenize(text)
|
||||
for (var i = 0; i < words.length && bestScore < 0.8; i++) {
|
||||
if (Math.abs(words[i].length - query.length) > maxDistance) continue
|
||||
var wordDistance = levenshteinDistance(words[i], query)
|
||||
if (wordDistance <= maxDistance) {
|
||||
var wordMaxLen = Math.max(words[i].length, query.length)
|
||||
var score = 1 - (wordDistance / wordMaxLen)
|
||||
bestScore = Math.max(bestScore, score)
|
||||
}
|
||||
}
|
||||
|
||||
return bestScore
|
||||
}
|
||||
|
||||
function getTimeBucketWeight(daysSinceUsed) {
|
||||
for (var i = 0; i < TimeBuckets.length; i++) {
|
||||
if (daysSinceUsed <= TimeBuckets[i].maxDays) {
|
||||
return TimeBuckets[i].weight
|
||||
}
|
||||
}
|
||||
return 10
|
||||
}
|
||||
|
||||
function calculateTextScore(name, query) {
|
||||
if (name === query) return Weights.exactMatch
|
||||
if (name.startsWith(query)) return Weights.prefixMatch
|
||||
if (name.includes(query)) return Weights.substring
|
||||
if (hasWordBoundaryMatch(name, query)) return Weights.wordBoundary
|
||||
|
||||
if (query.length >= 3) {
|
||||
var fs = fuzzyScore(name, query)
|
||||
if (fs > 0) return fs * Weights.fuzzy
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function score(item, query, frecencyData) {
|
||||
var typeBonus = Weights.typeBonus[item.type] || 0
|
||||
|
||||
if (!query || query.length === 0) {
|
||||
var usageCount = frecencyData ? frecencyData.usageCount : 0
|
||||
return typeBonus + (usageCount * 100)
|
||||
}
|
||||
|
||||
var name = (item.name || "").toLowerCase()
|
||||
var q = query.toLowerCase()
|
||||
|
||||
var textScore = calculateTextScore(name, q)
|
||||
|
||||
if (textScore === 0 && item.subtitle) {
|
||||
var subtitleScore = calculateTextScore(item.subtitle.toLowerCase(), q)
|
||||
textScore = subtitleScore * 0.5
|
||||
}
|
||||
|
||||
if (textScore === 0 && item.keywords) {
|
||||
for (var i = 0; i < item.keywords.length; i++) {
|
||||
var keywordScore = calculateTextScore(item.keywords[i].toLowerCase(), q)
|
||||
if (keywordScore > 0) {
|
||||
textScore = keywordScore * 0.3
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (textScore === 0) return 0
|
||||
|
||||
var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 10, Weights.frecency) : 0
|
||||
|
||||
return textScore + usageBonus + typeBonus
|
||||
}
|
||||
|
||||
function scoreItems(items, query, getFrecencyFn) {
|
||||
var scored = []
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i]
|
||||
var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null
|
||||
var itemScore = score(item, query, frecencyData)
|
||||
|
||||
if (itemScore > 0 || !query || query.length === 0) {
|
||||
scored.push({
|
||||
item: item,
|
||||
score: itemScore
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
scored.sort(function(a, b) {
|
||||
return b.score - a.score
|
||||
})
|
||||
|
||||
return scored
|
||||
}
|
||||
|
||||
function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSection) {
|
||||
var sections = {}
|
||||
var result = []
|
||||
var limit = maxPerSection || 50
|
||||
|
||||
for (var i = 0; i < sectionOrder.length; i++) {
|
||||
var sectionId = sectionOrder[i].id
|
||||
sections[sectionId] = {
|
||||
id: sectionId,
|
||||
title: sectionOrder[i].title,
|
||||
icon: sectionOrder[i].icon,
|
||||
priority: sectionOrder[i].priority,
|
||||
items: [],
|
||||
collapsed: false
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < scoredItems.length; i++) {
|
||||
var scoredItem = scoredItems[i]
|
||||
var item = scoredItem.item
|
||||
var sectionId = item.section || "apps"
|
||||
|
||||
if (sections[sectionId] && sections[sectionId].items.length < limit) {
|
||||
sections[sectionId].items.push(item)
|
||||
} else if (sections["apps"] && sections["apps"].items.length < limit) {
|
||||
sections["apps"].items.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < sectionOrder.length; i++) {
|
||||
var section = sections[sectionOrder[i].id]
|
||||
if (section && section.items.length > 0) {
|
||||
if (sortAlphabetically && section.id === "apps") {
|
||||
section.items.sort(function(a, b) {
|
||||
return (a.name || "").localeCompare(b.name || "")
|
||||
})
|
||||
}
|
||||
result.push(section)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function flattenSections(sections) {
|
||||
var flat = []
|
||||
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
var section = sections[i]
|
||||
|
||||
flat.push({
|
||||
isHeader: true,
|
||||
section: section,
|
||||
sectionId: section.id,
|
||||
sectionIndex: i
|
||||
})
|
||||
|
||||
if (!section.collapsed) {
|
||||
for (var j = 0; j < section.items.length; j++) {
|
||||
flat.push({
|
||||
isHeader: false,
|
||||
item: section.items[j],
|
||||
sectionId: section.id,
|
||||
sectionIndex: i,
|
||||
indexInSection: j
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return flat
|
||||
}
|
||||
114
quickshell/Modals/DankLauncherV2/Section.qml
Normal file
114
quickshell/Modals/DankLauncherV2/Section.qml
Normal file
@@ -0,0 +1,114 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var section: null
|
||||
property var controller: null
|
||||
property string viewMode: "list"
|
||||
property int gridColumns: 4
|
||||
property int startIndex: 0
|
||||
|
||||
signal itemClicked(int flatIndex)
|
||||
signal itemRightClicked(int flatIndex, var item, real mouseX, real mouseY)
|
||||
|
||||
height: headerItem.height + (section?.collapsed ? 0 : contentLoader.height + Theme.spacingXS)
|
||||
width: parent?.width ?? 200
|
||||
|
||||
SectionHeader {
|
||||
id: headerItem
|
||||
width: parent.width
|
||||
section: root.section
|
||||
controller: root.controller
|
||||
viewMode: root.viewMode
|
||||
canChangeViewMode: root.controller?.canChangeSectionViewMode(root.section?.id) ?? true
|
||||
|
||||
onViewModeToggled: {
|
||||
if (root.controller && root.section) {
|
||||
var newMode = root.viewMode === "list" ? "grid" : "list";
|
||||
root.controller.setSectionViewMode(root.section.id, newMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.top: headerItem.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
active: !root.section?.collapsed
|
||||
visible: active
|
||||
|
||||
sourceComponent: root.viewMode === "grid" ? gridComponent : listComponent
|
||||
|
||||
Component {
|
||||
id: listComponent
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
width: contentLoader.width
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.section?.items ?? []
|
||||
objectProp: "id"
|
||||
}
|
||||
|
||||
ResultItem {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: parent?.width ?? 200
|
||||
item: modelData
|
||||
isSelected: (root.startIndex + index) === root.controller?.selectedFlatIndex
|
||||
controller: root.controller
|
||||
flatIndex: root.startIndex + index
|
||||
|
||||
onClicked: root.itemClicked(root.startIndex + index)
|
||||
onRightClicked: (mouseX, mouseY) => {
|
||||
root.itemRightClicked(root.startIndex + index, modelData, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: gridComponent
|
||||
|
||||
Flow {
|
||||
width: contentLoader.width
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.section?.items ?? []
|
||||
objectProp: "id"
|
||||
}
|
||||
|
||||
GridItem {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: Math.floor((contentLoader.width - (root.gridColumns - 1) * 4) / root.gridColumns)
|
||||
height: width + 24
|
||||
item: modelData
|
||||
isSelected: (root.startIndex + index) === root.controller?.selectedFlatIndex
|
||||
controller: root.controller
|
||||
flatIndex: root.startIndex + index
|
||||
|
||||
onClicked: root.itemClicked(root.startIndex + index)
|
||||
onRightClicked: (mouseX, mouseY) => {
|
||||
root.itemRightClicked(root.startIndex + index, modelData, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
169
quickshell/Modals/DankLauncherV2/SectionHeader.qml
Normal file
169
quickshell/Modals/DankLauncherV2/SectionHeader.qml
Normal file
@@ -0,0 +1,169 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var section: null
|
||||
property var controller: null
|
||||
property string viewMode: "list"
|
||||
property bool canChangeViewMode: true
|
||||
property bool canCollapse: true
|
||||
property bool isSticky: false
|
||||
|
||||
signal viewModeToggled
|
||||
|
||||
width: parent?.width ?? 200
|
||||
height: 32
|
||||
color: isSticky ? "transparent" : (hoverArea.containsMouse ? Theme.surfaceHover : "transparent")
|
||||
radius: Theme.cornerRadius / 2
|
||||
|
||||
MouseArea {
|
||||
id: hoverArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
Row {
|
||||
id: leftContent
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: root.section?.icon ?? "folder"
|
||||
size: 16
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.section?.title ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.section?.items?.length ?? 0
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outlineButton
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: rightContent
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: viewModeRow
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
visible: root.canChangeViewMode && !root.section?.collapsed
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{
|
||||
mode: "list",
|
||||
icon: "view_list"
|
||||
},
|
||||
{
|
||||
mode: "grid",
|
||||
icon: "grid_view"
|
||||
},
|
||||
{
|
||||
mode: "tile",
|
||||
icon: "view_module"
|
||||
}
|
||||
]
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 4
|
||||
color: root.viewMode === modelData.mode ? Theme.primaryHover : modeArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: parent.modelData.icon
|
||||
size: 14
|
||||
color: root.viewMode === parent.modelData.mode ? Theme.primary : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: modeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.viewMode !== parent.modelData.mode && root.controller && root.section) {
|
||||
root.controller.setSectionViewMode(root.section.id, parent.modelData.mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: collapseButton
|
||||
width: root.canCollapse ? 24 : 0
|
||||
height: 24
|
||||
visible: root.canCollapse
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.section?.collapsed ? "expand_more" : "expand_less"
|
||||
size: 16
|
||||
color: collapseArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: collapseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.controller && root.section) {
|
||||
root.controller.toggleSection(root.section.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: rightContent.width + Theme.spacingS
|
||||
cursorShape: root.canCollapse ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: root.canCollapse
|
||||
onClicked: {
|
||||
if (root.canCollapse && root.controller && root.section) {
|
||||
root.controller.toggleSection(root.section.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 1
|
||||
color: Theme.outlineMedium
|
||||
visible: root.isSticky
|
||||
}
|
||||
}
|
||||
147
quickshell/Modals/DankLauncherV2/TileItem.qml
Normal file
147
quickshell/Modals/DankLauncherV2/TileItem.qml
Normal file
@@ -0,0 +1,147 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var item: null
|
||||
property bool isSelected: false
|
||||
property bool isHovered: itemArea.containsMouse
|
||||
property var controller: null
|
||||
property int flatIndex: -1
|
||||
|
||||
signal clicked
|
||||
signal rightClicked(real mouseX, real mouseY)
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
|
||||
border.width: isSelected ? 2 : 0
|
||||
border.color: Theme.primary
|
||||
|
||||
readonly property string imageSource: {
|
||||
if (!item?.data)
|
||||
return "";
|
||||
var data = item.data;
|
||||
if (data.imageUrl)
|
||||
return data.imageUrl;
|
||||
if (data.imagePath)
|
||||
return data.imagePath;
|
||||
if (data.path && isImageFile(data.path))
|
||||
return data.path;
|
||||
return "";
|
||||
}
|
||||
|
||||
readonly property bool useImage: imageSource.length > 0
|
||||
readonly property bool useIconProvider: !useImage && item?.iconType === "image"
|
||||
|
||||
function isImageFile(path) {
|
||||
if (!path)
|
||||
return false;
|
||||
var ext = path.split('.').pop().toLowerCase();
|
||||
return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp"].indexOf(ext) >= 0;
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
|
||||
Rectangle {
|
||||
id: imageContainer
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius - 2
|
||||
color: Theme.surfaceContainerHigh
|
||||
clip: true
|
||||
|
||||
CachingImage {
|
||||
anchors.fill: parent
|
||||
visible: root.useImage
|
||||
imagePath: root.imageSource
|
||||
maxCacheSize: 256
|
||||
}
|
||||
|
||||
Image {
|
||||
id: iconImage
|
||||
anchors.fill: parent
|
||||
visible: root.useIconProvider
|
||||
source: root.useIconProvider ? "image://icon/" + (root.item?.icon || "application-x-executable") : ""
|
||||
sourceSize.width: parent.width * 2
|
||||
sourceSize.height: parent.height * 2
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: false
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: !root.useImage && !root.useIconProvider
|
||||
name: root.item?.icon ?? "image"
|
||||
size: Math.min(parent.width, parent.height) * 0.4
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: labelText.implicitHeight + Theme.spacingS * 2
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, 0.85)
|
||||
visible: root.item?.name?.length > 0
|
||||
|
||||
StyledText {
|
||||
id: labelText
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingXS
|
||||
text: root.item?.name ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingXS
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: Theme.primary
|
||||
visible: root.isSelected
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "check"
|
||||
size: 14
|
||||
color: Theme.primaryText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
var scenePos = mapToItem(null, mouse.x, mouse.y);
|
||||
root.rightClicked(scenePos.x, scenePos.y);
|
||||
return;
|
||||
}
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
onPositionChanged: {
|
||||
if (root.controller)
|
||||
root.controller.keyboardNavigationActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,7 +539,7 @@ Rectangle {
|
||||
|
||||
Item {
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
height: Theme.spacingS
|
||||
height: Theme.spacingXS
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
@@ -717,7 +717,7 @@ Rectangle {
|
||||
|
||||
Item {
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
height: Theme.spacingS
|
||||
height: Theme.spacingXS
|
||||
visible: !root.searchActive
|
||||
}
|
||||
|
||||
|
||||
@@ -324,7 +324,6 @@ Item {
|
||||
anchors.left: parent.left
|
||||
anchors.right: buttonsContainer.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
height: 56
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
@@ -670,7 +669,6 @@ Item {
|
||||
DankTextField {
|
||||
id: editNameField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: editingApp?.name || ""
|
||||
keyNavigationTab: editIconField
|
||||
keyNavigationBacktab: editExtraFlagsField
|
||||
@@ -691,7 +689,6 @@ Item {
|
||||
DankTextField {
|
||||
id: editIconField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: editingApp?.icon || ""
|
||||
keyNavigationTab: editCommentField
|
||||
keyNavigationBacktab: editNameField
|
||||
@@ -712,7 +709,6 @@ Item {
|
||||
DankTextField {
|
||||
id: editCommentField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: editingApp?.comment || ""
|
||||
keyNavigationTab: editEnvVarsField
|
||||
keyNavigationBacktab: editIconField
|
||||
@@ -739,7 +735,6 @@ Item {
|
||||
DankTextField {
|
||||
id: editEnvVarsField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: "VAR=value"
|
||||
keyNavigationTab: editExtraFlagsField
|
||||
keyNavigationBacktab: editCommentField
|
||||
@@ -760,7 +755,6 @@ Item {
|
||||
DankTextField {
|
||||
id: editExtraFlagsField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: "--flag --option=value"
|
||||
keyNavigationTab: editNameField
|
||||
keyNavigationBacktab: editEnvVarsField
|
||||
|
||||
@@ -398,7 +398,6 @@ DankPopout {
|
||||
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: 52
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
@@ -531,7 +530,7 @@ DankPopout {
|
||||
x: searchField.x
|
||||
height: {
|
||||
let usedHeight = 40 + Theme.spacingS;
|
||||
usedHeight += 52 + Theme.spacingS;
|
||||
usedHeight += searchField.height + Theme.spacingS;
|
||||
usedHeight += appDrawerPopout.searchMode === "apps" ? 40 : 0;
|
||||
return parent.height - usedHeight;
|
||||
}
|
||||
@@ -839,7 +838,6 @@ DankPopout {
|
||||
DankTextField {
|
||||
id: editNameField
|
||||
width: parent.width
|
||||
height: 44
|
||||
focus: true
|
||||
placeholderText: appDrawerPopout.editingApp?.name || ""
|
||||
keyNavigationTab: editIconField
|
||||
@@ -861,7 +859,6 @@ DankPopout {
|
||||
DankTextField {
|
||||
id: editIconField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: appDrawerPopout.editingApp?.icon || ""
|
||||
keyNavigationTab: editCommentField
|
||||
keyNavigationBacktab: editNameField
|
||||
@@ -882,7 +879,6 @@ DankPopout {
|
||||
DankTextField {
|
||||
id: editCommentField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: appDrawerPopout.editingApp?.comment || ""
|
||||
keyNavigationTab: editEnvVarsField
|
||||
keyNavigationBacktab: editIconField
|
||||
@@ -909,7 +905,6 @@ DankPopout {
|
||||
DankTextField {
|
||||
id: editEnvVarsField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: "VAR=value"
|
||||
keyNavigationTab: editExtraFlagsField
|
||||
keyNavigationBacktab: editCommentField
|
||||
@@ -930,7 +925,6 @@ DankPopout {
|
||||
DankTextField {
|
||||
id: editExtraFlagsField
|
||||
width: parent.width
|
||||
height: 44
|
||||
placeholderText: "--flag --option=value"
|
||||
keyNavigationTab: editNameField
|
||||
keyNavigationBacktab: editEnvVarsField
|
||||
|
||||
@@ -11,17 +11,14 @@ BasePill {
|
||||
readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
|
||||
readonly property bool isChecking: SystemUpdateService.isChecking
|
||||
|
||||
readonly property real horizontalPadding: (barConfig?.noBackground ?? false) ? 2 : Theme.spacingS
|
||||
width: (SettingsData.updaterHideWidget && !hasUpdates) ? 0 : (18 + horizontalPadding * 2)
|
||||
|
||||
Ref {
|
||||
service: SystemUpdateService
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : updaterIcon.implicitWidth
|
||||
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
|
||||
implicitWidth: root.isVerticalOrientation ? root.widgetThickness : updaterIcon.implicitWidth
|
||||
implicitHeight: root.widgetThickness
|
||||
|
||||
DankIcon {
|
||||
id: statusIcon
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
@@ -7,6 +6,8 @@ import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
@@ -21,181 +22,55 @@ Column {
|
||||
property int currentMatchIndex: -1
|
||||
property int matchCount: 0
|
||||
|
||||
// Plugin-provided markdown/syntax highlighting (via builtInPluginSettings)
|
||||
property bool pluginInstalled: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "enabled", false)
|
||||
property bool pluginMarkdownEnabled: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "markdownPreview", false)
|
||||
property bool pluginSyntaxEnabled: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "syntaxHighlighting", false)
|
||||
property string pluginHighlightedHtml: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "highlightedHtml", "")
|
||||
property string pluginFileExtension: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "currentFileExtension", "")
|
||||
|
||||
// Local toggle for markdown preview (can be toggled from UI)
|
||||
property bool markdownPreviewActive: pluginMarkdownEnabled
|
||||
|
||||
// Toggle markdown preview
|
||||
function toggleMarkdownPreview() {
|
||||
if (!markdownPreviewActive) {
|
||||
// Entering preview mode
|
||||
syncContentToPlugin();
|
||||
markdownPreviewActive = true;
|
||||
} else {
|
||||
// Exiting preview mode
|
||||
markdownPreviewActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Local toggle for syntax highlighting preview (read-only view with colors)
|
||||
property bool syntaxPreviewActive: false
|
||||
|
||||
// Store original text when entering syntax preview mode
|
||||
property string syntaxPreviewOriginalText: ""
|
||||
|
||||
// Function to refresh plugin settings (called from Connections inside TextArea)
|
||||
function refreshPluginSettings() {
|
||||
pluginInstalled = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "enabled", false);
|
||||
pluginMarkdownEnabled = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "markdownPreview", false);
|
||||
pluginSyntaxEnabled = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "syntaxHighlighting", false);
|
||||
pluginHighlightedHtml = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "highlightedHtml", "");
|
||||
pluginFileExtension = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "currentFileExtension", "");
|
||||
|
||||
console.warn("NotepadTextEditor: Plugin settings refreshed. MdEnabled:", pluginMarkdownEnabled, "HtmlLength:", pluginHighlightedHtml.length);
|
||||
}
|
||||
|
||||
// Toggle syntax preview mode
|
||||
function toggleSyntaxPreview() {
|
||||
if (!syntaxPreviewActive) {
|
||||
// Entering preview mode
|
||||
syncContentToPlugin();
|
||||
syntaxPreviewActive = true;
|
||||
} else {
|
||||
// Exiting preview mode
|
||||
syntaxPreviewActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
// File extension detection for current tab
|
||||
readonly property string currentFilePath: currentTab?.filePath || ""
|
||||
readonly property string currentFileExtension: {
|
||||
if (!currentFilePath)
|
||||
return "";
|
||||
var parts = currentFilePath.split('.');
|
||||
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : "";
|
||||
}
|
||||
|
||||
onCurrentTabChanged: handleCurrentTabChanged()
|
||||
|
||||
Component.onCompleted: handleCurrentTabChanged()
|
||||
|
||||
function handleCurrentTabChanged() {
|
||||
if (!currentTab)
|
||||
return;
|
||||
|
||||
// Reset preview state ONLY when tab actually changes
|
||||
markdownPreviewActive = false;
|
||||
syntaxPreviewActive = false;
|
||||
syntaxPreviewOriginalText = "";
|
||||
textArea.readOnly = false;
|
||||
|
||||
syncContentToPlugin();
|
||||
}
|
||||
|
||||
function syncContentToPlugin() {
|
||||
if (!currentTab)
|
||||
return;
|
||||
|
||||
// Notify plugin of content update
|
||||
// console.warn("NotepadTextEditor: Pushing content to plugin. Length:", textArea.text.length, "Path:", currentFilePath);
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadMarkdown", "currentFilePath", currentFilePath);
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadMarkdown", "currentFileExtension", currentFileExtension);
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadMarkdown", "sourceContent", textArea.text);
|
||||
SettingsData.setBuiltInPluginSetting("dankNotepadMarkdown", "currentTabChanged", Date.now());
|
||||
}
|
||||
|
||||
// Debounce content updates to plugin to keep preview ready
|
||||
Timer {
|
||||
id: syncTimer
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: syncContentToPlugin()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: textArea
|
||||
function onTextChanged() {
|
||||
if (!markdownPreviewActive && !syntaxPreviewActive) {
|
||||
syncTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property string fileExtension: {
|
||||
if (!currentFilePath)
|
||||
return "";
|
||||
var parts = currentFilePath.split('.');
|
||||
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : "";
|
||||
}
|
||||
readonly property bool isMarkdownFile: fileExtension === "md" || fileExtension === "markdown" || fileExtension === "mdown"
|
||||
readonly property bool isCodeFile: fileExtension !== "" && fileExtension !== "txt" && !isMarkdownFile
|
||||
|
||||
signal saveRequested
|
||||
signal openRequested
|
||||
signal newRequested
|
||||
signal escapePressed
|
||||
signal contentChanged
|
||||
signal settingsRequested
|
||||
signal saveRequested()
|
||||
signal openRequested()
|
||||
signal newRequested()
|
||||
signal escapePressed()
|
||||
signal contentChanged()
|
||||
signal settingsRequested()
|
||||
|
||||
function hasUnsavedChanges() {
|
||||
if (!currentTab || !contentLoaded) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
if (currentTab.isTemporary) {
|
||||
return textArea.text.length > 0;
|
||||
return textArea.text.length > 0
|
||||
}
|
||||
|
||||
// If in preview mode, compare original text
|
||||
if (markdownPreviewActive || syntaxPreviewActive) {
|
||||
return syntaxPreviewOriginalText !== lastSavedContent;
|
||||
}
|
||||
|
||||
return textArea.text !== lastSavedContent;
|
||||
return textArea.text !== lastSavedContent
|
||||
}
|
||||
|
||||
function loadCurrentTabContent() {
|
||||
if (!currentTab)
|
||||
return;
|
||||
contentLoaded = false;
|
||||
// Reset preview states on load
|
||||
markdownPreviewActive = false;
|
||||
syntaxPreviewActive = false;
|
||||
syntaxPreviewOriginalText = "";
|
||||
if (!currentTab) return
|
||||
|
||||
NotepadStorageService.loadTabContent(NotepadStorageService.currentTabIndex, content => {
|
||||
lastSavedContent = content;
|
||||
textArea.text = content;
|
||||
contentLoaded = true;
|
||||
textArea.readOnly = false;
|
||||
});
|
||||
contentLoaded = false
|
||||
NotepadStorageService.loadTabContent(
|
||||
NotepadStorageService.currentTabIndex,
|
||||
(content) => {
|
||||
lastSavedContent = content
|
||||
textArea.text = content
|
||||
contentLoaded = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function saveCurrentTabContent() {
|
||||
if (!currentTab || !contentLoaded)
|
||||
return;
|
||||
if (!currentTab || !contentLoaded) return
|
||||
|
||||
// If in preview mode, save the original text, NOT the HTML
|
||||
var contentToSave = (markdownPreviewActive || syntaxPreviewActive) ? syntaxPreviewOriginalText : textArea.text;
|
||||
|
||||
NotepadStorageService.saveTabContent(NotepadStorageService.currentTabIndex, contentToSave);
|
||||
lastSavedContent = contentToSave;
|
||||
NotepadStorageService.saveTabContent(
|
||||
NotepadStorageService.currentTabIndex,
|
||||
textArea.text
|
||||
)
|
||||
lastSavedContent = textArea.text
|
||||
}
|
||||
|
||||
function autoSaveToSession() {
|
||||
if (!currentTab || !contentLoaded)
|
||||
return;
|
||||
saveCurrentTabContent();
|
||||
if (!currentTab || !contentLoaded) return
|
||||
saveCurrentTabContent()
|
||||
}
|
||||
|
||||
function setTextDocumentLineHeight() {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
property string lastTextForLineModel: ""
|
||||
@@ -203,102 +78,100 @@ Column {
|
||||
|
||||
function updateLineModel() {
|
||||
if (!SettingsData.notepadShowLineNumbers) {
|
||||
lineModel = [];
|
||||
lastTextForLineModel = "";
|
||||
return;
|
||||
lineModel = []
|
||||
lastTextForLineModel = ""
|
||||
return
|
||||
}
|
||||
|
||||
// In preview mode, line numbers might not match visual lines correctly due to wrapping/HTML
|
||||
// But for now let's use the current text (plain or HTML)
|
||||
if (textArea.text !== lastTextForLineModel || lineModel.length === 0) {
|
||||
lastTextForLineModel = textArea.text;
|
||||
lineModel = textArea.text.split('\n');
|
||||
lastTextForLineModel = textArea.text
|
||||
lineModel = textArea.text.split('\n')
|
||||
}
|
||||
}
|
||||
|
||||
function performSearch() {
|
||||
let matches = [];
|
||||
currentMatchIndex = -1;
|
||||
let matches = []
|
||||
currentMatchIndex = -1
|
||||
|
||||
if (!searchQuery || searchQuery.length === 0) {
|
||||
searchMatches = [];
|
||||
matchCount = 0;
|
||||
textArea.select(0, 0);
|
||||
return;
|
||||
searchMatches = []
|
||||
matchCount = 0
|
||||
textArea.select(0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
const text = textArea.text;
|
||||
const query = searchQuery.toLowerCase();
|
||||
let index = 0;
|
||||
const text = textArea.text
|
||||
const query = searchQuery.toLowerCase()
|
||||
let index = 0
|
||||
|
||||
while (index < text.length) {
|
||||
const foundIndex = text.toLowerCase().indexOf(query, index);
|
||||
if (foundIndex === -1)
|
||||
break;
|
||||
const foundIndex = text.toLowerCase().indexOf(query, index)
|
||||
if (foundIndex === -1) break
|
||||
|
||||
matches.push({
|
||||
start: foundIndex,
|
||||
end: foundIndex + searchQuery.length
|
||||
});
|
||||
index = foundIndex + 1;
|
||||
})
|
||||
index = foundIndex + 1
|
||||
}
|
||||
|
||||
searchMatches = matches;
|
||||
matchCount = matches.length;
|
||||
searchMatches = matches
|
||||
matchCount = matches.length
|
||||
|
||||
if (matchCount > 0) {
|
||||
currentMatchIndex = 0;
|
||||
highlightCurrentMatch();
|
||||
currentMatchIndex = 0
|
||||
highlightCurrentMatch()
|
||||
} else {
|
||||
textArea.select(0, 0);
|
||||
textArea.select(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function highlightCurrentMatch() {
|
||||
if (currentMatchIndex >= 0 && currentMatchIndex < searchMatches.length) {
|
||||
const match = searchMatches[currentMatchIndex];
|
||||
const match = searchMatches[currentMatchIndex]
|
||||
|
||||
textArea.cursorPosition = match.start;
|
||||
textArea.moveCursorSelection(match.end, TextEdit.SelectCharacters);
|
||||
textArea.cursorPosition = match.start
|
||||
textArea.moveCursorSelection(match.end, TextEdit.SelectCharacters)
|
||||
|
||||
const flickable = textArea.parent;
|
||||
const flickable = textArea.parent
|
||||
if (flickable && flickable.contentY !== undefined) {
|
||||
const lineHeight = textArea.font.pixelSize * 1.5;
|
||||
const approxLine = textArea.text.substring(0, match.start).split('\n').length;
|
||||
const targetY = approxLine * lineHeight - flickable.height / 2;
|
||||
flickable.contentY = Math.max(0, Math.min(targetY, flickable.contentHeight - flickable.height));
|
||||
const lineHeight = textArea.font.pixelSize * 1.5
|
||||
const approxLine = textArea.text.substring(0, match.start).split('\n').length
|
||||
const targetY = approxLine * lineHeight - flickable.height / 2
|
||||
flickable.contentY = Math.max(0, Math.min(targetY, flickable.contentHeight - flickable.height))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findNext() {
|
||||
if (matchCount === 0 || searchMatches.length === 0)
|
||||
return;
|
||||
currentMatchIndex = (currentMatchIndex + 1) % matchCount;
|
||||
highlightCurrentMatch();
|
||||
if (matchCount === 0 || searchMatches.length === 0) return
|
||||
|
||||
currentMatchIndex = (currentMatchIndex + 1) % matchCount
|
||||
highlightCurrentMatch()
|
||||
}
|
||||
|
||||
function findPrevious() {
|
||||
if (matchCount === 0 || searchMatches.length === 0)
|
||||
return;
|
||||
currentMatchIndex = currentMatchIndex <= 0 ? matchCount - 1 : currentMatchIndex - 1;
|
||||
highlightCurrentMatch();
|
||||
if (matchCount === 0 || searchMatches.length === 0) return
|
||||
|
||||
currentMatchIndex = currentMatchIndex <= 0 ? matchCount - 1 : currentMatchIndex - 1
|
||||
highlightCurrentMatch()
|
||||
}
|
||||
|
||||
function showSearch() {
|
||||
searchVisible = true;
|
||||
searchVisible = true
|
||||
Qt.callLater(() => {
|
||||
searchField.forceActiveFocus();
|
||||
});
|
||||
searchField.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
|
||||
function hideSearch() {
|
||||
searchVisible = false;
|
||||
searchQuery = "";
|
||||
searchMatches = [];
|
||||
matchCount = 0;
|
||||
currentMatchIndex = -1;
|
||||
textArea.select(0, 0);
|
||||
textArea.forceActiveFocus();
|
||||
searchVisible = false
|
||||
searchQuery = ""
|
||||
searchMatches = []
|
||||
matchCount = 0
|
||||
currentMatchIndex = -1
|
||||
textArea.select(0, 0)
|
||||
textArea.forceActiveFocus()
|
||||
}
|
||||
|
||||
spacing: Theme.spacingM
|
||||
@@ -348,43 +221,43 @@ Column {
|
||||
clip: true
|
||||
|
||||
Component.onCompleted: {
|
||||
text = root.searchQuery;
|
||||
text = root.searchQuery
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onSearchQueryChanged() {
|
||||
if (searchField.text !== root.searchQuery) {
|
||||
searchField.text = root.searchQuery;
|
||||
searchField.text = root.searchQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
if (root.searchQuery !== text) {
|
||||
root.searchQuery = text;
|
||||
root.performSearch();
|
||||
root.searchQuery = text
|
||||
root.performSearch()
|
||||
}
|
||||
}
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hideSearch();
|
||||
event.accepted = true;
|
||||
root.hideSearch()
|
||||
event.accepted = true
|
||||
}
|
||||
Keys.onReturnPressed: event => {
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
root.findPrevious();
|
||||
root.findPrevious()
|
||||
} else {
|
||||
root.findNext();
|
||||
root.findNext()
|
||||
}
|
||||
event.accepted = true;
|
||||
event.accepted = true
|
||||
}
|
||||
Keys.onEnterPressed: event => {
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
root.findPrevious();
|
||||
root.findPrevious()
|
||||
} else {
|
||||
root.findNext();
|
||||
root.findNext()
|
||||
}
|
||||
event.accepted = true;
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +325,6 @@ Column {
|
||||
|
||||
DankFlickable {
|
||||
id: flickable
|
||||
visible: !root.markdownPreviewActive && !root.syntaxPreviewActive
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
clip: true
|
||||
@@ -525,7 +397,6 @@ Column {
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
textFormat: TextEdit.PlainText
|
||||
// readOnly: root.syntaxPreviewActive || root.markdownPreviewActive // Handled by visibility now
|
||||
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
|
||||
persistentSelection: true
|
||||
tabStopDistance: 40
|
||||
@@ -545,45 +416,31 @@ Column {
|
||||
SequentialAnimation on opacity {
|
||||
running: textArea.activeFocus
|
||||
loops: Animation.Infinite
|
||||
PropertyAnimation {
|
||||
from: 1.0
|
||||
to: 0.0
|
||||
duration: 650
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
PropertyAnimation {
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
duration: 650
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
PropertyAnimation { from: 1.0; to: 0.0; duration: 650; easing.type: Easing.InOutQuad }
|
||||
PropertyAnimation { from: 0.0; to: 1.0; duration: 650; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
loadCurrentTabContent();
|
||||
setTextDocumentLineHeight();
|
||||
root.updateLineModel();
|
||||
loadCurrentTabContent()
|
||||
setTextDocumentLineHeight()
|
||||
root.updateLineModel()
|
||||
Qt.callLater(() => {
|
||||
textArea.forceActiveFocus();
|
||||
});
|
||||
textArea.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NotepadStorageService
|
||||
function onCurrentTabIndexChanged() {
|
||||
// Exit syntax preview mode when switching tabs
|
||||
if (root.syntaxPreviewActive) {
|
||||
root.syntaxPreviewActive = false;
|
||||
}
|
||||
loadCurrentTabContent();
|
||||
loadCurrentTabContent()
|
||||
Qt.callLater(() => {
|
||||
textArea.forceActiveFocus();
|
||||
});
|
||||
textArea.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
function onTabsChanged() {
|
||||
if (NotepadStorageService.tabs.length > 0 && !contentLoaded) {
|
||||
loadCurrentTabContent();
|
||||
loadCurrentTabContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -591,49 +448,46 @@ Column {
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onNotepadShowLineNumbersChanged() {
|
||||
root.updateLineModel();
|
||||
}
|
||||
function onBuiltInPluginSettingsChanged() {
|
||||
root.refreshPluginSettings();
|
||||
root.updateLineModel()
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
if (contentLoaded && text !== lastSavedContent) {
|
||||
autoSaveTimer.restart();
|
||||
autoSaveTimer.restart()
|
||||
}
|
||||
root.contentChanged();
|
||||
root.updateLineModel();
|
||||
root.contentChanged()
|
||||
root.updateLineModel()
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.escapePressed();
|
||||
event.accepted = true;
|
||||
Keys.onEscapePressed: (event) => {
|
||||
root.escapePressed()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_S:
|
||||
event.accepted = true;
|
||||
root.saveRequested();
|
||||
break;
|
||||
event.accepted = true
|
||||
root.saveRequested()
|
||||
break
|
||||
case Qt.Key_O:
|
||||
event.accepted = true;
|
||||
root.openRequested();
|
||||
break;
|
||||
event.accepted = true
|
||||
root.openRequested()
|
||||
break
|
||||
case Qt.Key_N:
|
||||
event.accepted = true;
|
||||
root.newRequested();
|
||||
break;
|
||||
event.accepted = true
|
||||
root.newRequested()
|
||||
break
|
||||
case Qt.Key_A:
|
||||
event.accepted = true;
|
||||
selectAll();
|
||||
break;
|
||||
event.accepted = true
|
||||
selectAll()
|
||||
break
|
||||
case Qt.Key_F:
|
||||
event.accepted = true;
|
||||
root.showSearch();
|
||||
break;
|
||||
event.accepted = true
|
||||
root.showSearch()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -641,11 +495,6 @@ Column {
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
// Make links clickable in markdown preview mode
|
||||
onLinkActivated: link => {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -662,47 +511,6 @@ Column {
|
||||
z: textArea.z + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Dedicated Flickable for Preview Mode
|
||||
DankFlickable {
|
||||
id: previewFlickable
|
||||
visible: root.markdownPreviewActive || root.syntaxPreviewActive
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
clip: true
|
||||
contentWidth: width - 11
|
||||
|
||||
TextArea.flickable: TextArea {
|
||||
id: previewAreaReal
|
||||
text: root.pluginHighlightedHtml
|
||||
textFormat: TextEdit.RichText
|
||||
readOnly: true
|
||||
|
||||
// Copy styling from main textArea
|
||||
placeholderText: ""
|
||||
font.family: SettingsData.notepadUseMonospace ? SettingsData.monoFontFamily : (SettingsData.notepadFontFamily || SettingsData.fontFamily)
|
||||
font.pixelSize: SettingsData.notepadFontSize * SettingsData.fontScale
|
||||
font.letterSpacing: 0
|
||||
color: Theme.surfaceText
|
||||
selectedTextColor: Theme.background
|
||||
selectionColor: Theme.primary
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
wrapMode: TextArea.Wrap
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
|
||||
leftPadding: Theme.spacingM
|
||||
topPadding: Theme.spacingM
|
||||
rightPadding: Theme.spacingM
|
||||
bottomPadding: Theme.spacingM
|
||||
|
||||
// Make links clickable
|
||||
onLinkActivated: link => {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -767,44 +575,6 @@ Column {
|
||||
color: Theme.surfaceTextMedium
|
||||
}
|
||||
}
|
||||
|
||||
// Markdown preview toggle (only visible when plugin installed and viewing .md file)
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
visible: root.pluginInstalled && root.isMarkdownFile
|
||||
|
||||
DankActionButton {
|
||||
iconName: root.markdownPreviewActive ? "visibility" : "visibility_off"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: root.markdownPreviewActive ? Theme.primary : Theme.surfaceTextMedium
|
||||
onClicked: root.toggleMarkdownPreview()
|
||||
}
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("Preview")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: root.markdownPreviewActive ? Theme.primary : Theme.surfaceTextMedium
|
||||
}
|
||||
}
|
||||
|
||||
// Syntax highlighting toggle (only visible when plugin installed and viewing code file)
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
visible: root.pluginInstalled && root.pluginSyntaxEnabled && root.isCodeFile
|
||||
|
||||
DankActionButton {
|
||||
iconName: root.syntaxPreviewActive ? "code" : "code_off"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: root.syntaxPreviewActive ? Theme.primary : Theme.surfaceTextMedium
|
||||
onClicked: root.toggleSyntaxPreview()
|
||||
}
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.syntaxPreviewActive ? I18n.tr("Edit") : I18n.tr("Highlight")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: root.syntaxPreviewActive ? Theme.primary : Theme.surfaceTextMedium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
@@ -838,29 +608,29 @@ Column {
|
||||
StyledText {
|
||||
text: {
|
||||
if (autoSaveTimer.running) {
|
||||
return I18n.tr("Auto-saving...");
|
||||
return I18n.tr("Auto-saving...")
|
||||
}
|
||||
|
||||
if (hasUnsavedChanges()) {
|
||||
if (currentTab && currentTab.isTemporary) {
|
||||
return I18n.tr("Unsaved note...");
|
||||
return I18n.tr("Unsaved note...")
|
||||
} else {
|
||||
return I18n.tr("Unsaved changes");
|
||||
return I18n.tr("Unsaved changes")
|
||||
}
|
||||
} else {
|
||||
return I18n.tr("Saved");
|
||||
return I18n.tr("Saved")
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: {
|
||||
if (autoSaveTimer.running) {
|
||||
return Theme.primary;
|
||||
return Theme.primary
|
||||
}
|
||||
|
||||
if (hasUnsavedChanges()) {
|
||||
return Theme.warning;
|
||||
return Theme.warning
|
||||
} else {
|
||||
return Theme.success;
|
||||
return Theme.success
|
||||
}
|
||||
}
|
||||
opacity: textArea.text.length > 0 ? 1.0 : 0.0
|
||||
@@ -873,7 +643,7 @@ Column {
|
||||
interval: 2000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
autoSaveToSession();
|
||||
autoSaveToSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,9 @@ DankPopout {
|
||||
root.close();
|
||||
};
|
||||
}
|
||||
if (item && "parentPopout" in item) {
|
||||
item.parentPopout = root;
|
||||
}
|
||||
if (item) {
|
||||
root.contentHeight = Qt.binding(() => item.implicitHeight + Theme.spacingS * 2);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ Column {
|
||||
property string detailsText: ""
|
||||
property bool showCloseButton: false
|
||||
property var closePopout: null
|
||||
property var parentPopout: null
|
||||
property alias headerActions: headerActionsLoader.sourceComponent
|
||||
|
||||
readonly property int headerHeight: popoutHeader.visible ? popoutHeader.height : 0
|
||||
|
||||
@@ -318,7 +318,7 @@ Popup {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: modelData.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: modelData.enabled
|
||||
enabled: modelData.enabled ?? false
|
||||
onEntered: {
|
||||
keyboardNavigation = false;
|
||||
selectedIndex = index;
|
||||
|
||||
@@ -236,7 +236,6 @@ Item {
|
||||
DankTextField {
|
||||
id: searchField
|
||||
width: parent.width - addButton.width - Theme.spacingM
|
||||
height: Math.round(Theme.fontSizeMedium * 3)
|
||||
placeholderText: I18n.tr("Search keybinds...")
|
||||
leftIconName: "search"
|
||||
onTextChanged: {
|
||||
|
||||
@@ -353,6 +353,116 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "tune"
|
||||
title: I18n.tr("Appearance", "launcher appearance settings")
|
||||
settingKey: "dankLauncherV2Appearance"
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Size", "launcher size option")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: sizeGroup.implicitHeight
|
||||
clip: true
|
||||
|
||||
DankButtonGroup {
|
||||
id: sizeGroup
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
buttonPadding: parent.width < 400 ? Theme.spacingS : Theme.spacingL
|
||||
minButtonWidth: parent.width < 400 ? 60 : 80
|
||||
textSize: parent.width < 400 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||
model: [I18n.tr("Compact", "compact launcher size"), I18n.tr("Medium", "medium launcher size"), I18n.tr("Large", "large launcher size")]
|
||||
currentIndex: SettingsData.dankLauncherV2Size === "compact" ? 0 : SettingsData.dankLauncherV2Size === "large" ? 2 : 1
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
SettingsData.set("dankLauncherV2Size", index === 0 ? "compact" : index === 2 ? "large" : "medium");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankLauncherV2ShowFooter"
|
||||
tags: ["launcher", "footer", "hints", "shortcuts"]
|
||||
text: I18n.tr("Show Footer", "launcher footer visibility")
|
||||
description: I18n.tr("Show mode tabs and keyboard hints at the bottom.", "launcher footer description")
|
||||
checked: SettingsData.dankLauncherV2ShowFooter
|
||||
onToggled: checked => SettingsData.set("dankLauncherV2ShowFooter", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dankLauncherV2BorderEnabled"
|
||||
tags: ["launcher", "border", "outline"]
|
||||
text: I18n.tr("Border", "launcher border option")
|
||||
checked: SettingsData.dankLauncherV2BorderEnabled
|
||||
onToggled: checked => SettingsData.set("dankLauncherV2BorderEnabled", checked)
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: SettingsData.dankLauncherV2BorderEnabled
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dankLauncherV2BorderThickness"
|
||||
tags: ["launcher", "border", "thickness"]
|
||||
text: I18n.tr("Thickness", "border thickness")
|
||||
minimum: 1
|
||||
maximum: 6
|
||||
value: SettingsData.dankLauncherV2BorderThickness
|
||||
defaultValue: 2
|
||||
unit: "px"
|
||||
onSliderValueChanged: newValue => SettingsData.set("dankLauncherV2BorderThickness", newValue)
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Color", "border color")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: borderColorGroup.implicitHeight
|
||||
clip: true
|
||||
|
||||
DankButtonGroup {
|
||||
id: borderColorGroup
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
buttonPadding: parent.width < 400 ? Theme.spacingS : Theme.spacingL
|
||||
minButtonWidth: parent.width < 400 ? 50 : 70
|
||||
textSize: parent.width < 400 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||
model: [I18n.tr("Primary", "primary color"), I18n.tr("Secondary", "secondary color"), I18n.tr("Outline", "outline color"), I18n.tr("Text", "text color")]
|
||||
currentIndex: SettingsData.dankLauncherV2BorderColor === "secondary" ? 1 : SettingsData.dankLauncherV2BorderColor === "outline" ? 2 : SettingsData.dankLauncherV2BorderColor === "surfaceText" ? 3 : 0
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
SettingsData.set("dankLauncherV2BorderColor", index === 1 ? "secondary" : index === 2 ? "outline" : index === 3 ? "surfaceText" : "primary");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "open_in_new"
|
||||
|
||||
@@ -60,6 +60,11 @@ Item {
|
||||
currentIndex: 0
|
||||
selectionMode: "single"
|
||||
checkEnabled: false
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
currentIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +328,7 @@ Item {
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
currentIndex = index;
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acSuspendBehavior", index);
|
||||
} else {
|
||||
@@ -548,7 +554,6 @@ Item {
|
||||
|
||||
DankTextField {
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: modelData.placeholder
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
|
||||
@@ -69,7 +69,6 @@ Item {
|
||||
DankTextField {
|
||||
id: updaterCustomCommand
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "myPkgMngr --sysupdate"
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
@@ -114,7 +113,6 @@ Item {
|
||||
DankTextField {
|
||||
id: updaterTerminalCustomClass
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "-T udpClass"
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
|
||||
@@ -3,7 +3,7 @@ import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Modals.Spotlight
|
||||
import qs.Modals.DankLauncherV2
|
||||
import qs.Services
|
||||
|
||||
Scope {
|
||||
@@ -67,229 +67,215 @@ Scope {
|
||||
hideSpotlight();
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: niriOverlayLoader
|
||||
active: overlayActive || isClosing
|
||||
asynchronous: false
|
||||
Variants {
|
||||
id: overlayVariants
|
||||
model: Quickshell.screens
|
||||
|
||||
sourceComponent: Variants {
|
||||
id: overlayVariants
|
||||
model: Quickshell.screens
|
||||
PanelWindow {
|
||||
id: overlayWindow
|
||||
required property var modelData
|
||||
|
||||
PanelWindow {
|
||||
id: overlayWindow
|
||||
required property var modelData
|
||||
readonly property real dpr: CompositorService.getScreenScale(screen)
|
||||
readonly property bool isActiveScreen: screen.name === NiriService.currentOutput
|
||||
readonly property bool shouldShowSpotlight: niriOverviewScope.searchActive && screen.name === niriOverviewScope.searchActiveScreen && !niriOverviewScope.isClosing
|
||||
readonly property bool isSpotlightScreen: screen.name === niriOverviewScope.searchActiveScreen
|
||||
readonly property bool overlayVisible: NiriService.inOverview || niriOverviewScope.isClosing
|
||||
property bool hasActivePopout: !!PopoutManager.currentPopoutsByScreen[screen.name]
|
||||
property bool hasActiveModal: !!ModalManager.currentModalsByScreen[screen.name]
|
||||
|
||||
readonly property real dpr: CompositorService.getScreenScale(screen)
|
||||
readonly property bool isActiveScreen: screen.name === NiriService.currentOutput
|
||||
readonly property bool shouldShowSpotlight: niriOverviewScope.searchActive && screen.name === niriOverviewScope.searchActiveScreen && !niriOverviewScope.isClosing
|
||||
readonly property bool isSpotlightScreen: screen.name === niriOverviewScope.searchActiveScreen
|
||||
property bool hasActivePopout: !!PopoutManager.currentPopoutsByScreen[screen.name]
|
||||
property bool hasActiveModal: !!ModalManager.currentModalsByScreen[screen.name]
|
||||
|
||||
Connections {
|
||||
target: PopoutManager
|
||||
function onPopoutChanged() {
|
||||
overlayWindow.hasActivePopout = !!PopoutManager.currentPopoutsByScreen[overlayWindow.screen.name];
|
||||
}
|
||||
Connections {
|
||||
target: PopoutManager
|
||||
function onPopoutChanged() {
|
||||
overlayWindow.hasActivePopout = !!PopoutManager.currentPopoutsByScreen[overlayWindow.screen.name];
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ModalManager
|
||||
function onModalChanged() {
|
||||
overlayWindow.hasActiveModal = !!ModalManager.currentModalsByScreen[overlayWindow.screen.name];
|
||||
}
|
||||
Connections {
|
||||
target: ModalManager
|
||||
function onModalChanged() {
|
||||
overlayWindow.hasActiveModal = !!ModalManager.currentModalsByScreen[overlayWindow.screen.name];
|
||||
}
|
||||
}
|
||||
|
||||
screen: modelData
|
||||
visible: NiriService.inOverview || niriOverviewScope.isClosing
|
||||
color: "transparent"
|
||||
screen: modelData
|
||||
visible: true
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.namespace: "dms:niri-overview-spotlight"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: {
|
||||
if (!NiriService.inOverview)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (!isActiveScreen)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (niriOverviewScope.releaseKeyboard)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (hasActivePopout || hasActiveModal)
|
||||
return WlrKeyboardFocus.None;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
WlrLayershell.namespace: "dms:niri-overview-spotlight"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: {
|
||||
if (!NiriService.inOverview)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (!isActiveScreen)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (niriOverviewScope.releaseKeyboard)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (hasActivePopout || hasActiveModal)
|
||||
return WlrKeyboardFocus.None;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: overlayVisible && spotlightContainer.visible ? spotlightContainer : null
|
||||
}
|
||||
|
||||
onShouldShowSpotlightChanged: {
|
||||
if (shouldShowSpotlight) {
|
||||
if (launcherContent?.controller)
|
||||
launcherContent.controller.performSearch();
|
||||
return;
|
||||
}
|
||||
if (!isActiveScreen)
|
||||
return;
|
||||
Qt.callLater(() => keyboardFocusScope.forceActiveFocus());
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: spotlightContainer.visible ? spotlightContainer : null
|
||||
}
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
onShouldShowSpotlightChanged: {
|
||||
if (shouldShowSpotlight) {
|
||||
if (spotlightContent?.appLauncher)
|
||||
spotlightContent.appLauncher.ensureInitialized();
|
||||
FocusScope {
|
||||
id: keyboardFocusScope
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (overlayWindow.shouldShowSpotlight || niriOverviewScope.isClosing)
|
||||
return;
|
||||
}
|
||||
if (!isActiveScreen)
|
||||
return;
|
||||
Qt.callLater(() => keyboardFocusScope.forceActiveFocus());
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: keyboardFocusScope
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (overlayWindow.shouldShowSpotlight || niriOverviewScope.isClosing)
|
||||
return;
|
||||
if ([Qt.Key_Escape, Qt.Key_Return].includes(event.key)) {
|
||||
NiriService.toggleOverview();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Left) {
|
||||
NiriService.moveColumnLeft();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Right) {
|
||||
NiriService.moveColumnRight();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Up) {
|
||||
NiriService.moveWorkspaceUp();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Down) {
|
||||
NiriService.moveWorkspaceDown();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.modifiers & (Qt.ControlModifier | Qt.MetaModifier) || [Qt.Key_Delete, Qt.Key_Backspace].includes(event.key)) {
|
||||
event.accepted = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.isAutoRepeat || !event.text)
|
||||
return;
|
||||
if (!spotlightContent?.searchField)
|
||||
return;
|
||||
const trimmedText = event.text.trim();
|
||||
spotlightContent.searchField.text = trimmedText;
|
||||
if (spotlightContent.appLauncher) {
|
||||
spotlightContent.appLauncher.searchQuery = trimmedText;
|
||||
}
|
||||
niriOverviewScope.showSpotlight(overlayWindow.screen.name);
|
||||
Qt.callLater(() => spotlightContent.searchField.forceActiveFocus());
|
||||
if ([Qt.Key_Escape, Qt.Key_Return].includes(event.key)) {
|
||||
NiriService.toggleOverview();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Left) {
|
||||
NiriService.moveColumnLeft();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Right) {
|
||||
NiriService.moveColumnRight();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Up) {
|
||||
NiriService.moveWorkspaceUp();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Down) {
|
||||
NiriService.moveWorkspaceDown();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.modifiers & (Qt.ControlModifier | Qt.MetaModifier) || [Qt.Key_Delete, Qt.Key_Backspace].includes(event.key)) {
|
||||
event.accepted = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.isAutoRepeat || !event.text)
|
||||
return;
|
||||
if (!launcherContent?.searchField)
|
||||
return;
|
||||
const trimmedText = event.text.trim();
|
||||
launcherContent.searchField.text = trimmedText;
|
||||
launcherContent.controller.setSearchQuery(trimmedText);
|
||||
niriOverviewScope.showSpotlight(overlayWindow.screen.name);
|
||||
Qt.callLater(() => launcherContent.searchField.forceActiveFocus());
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: spotlightContainer
|
||||
x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr)
|
||||
y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr)
|
||||
width: Theme.px(500, overlayWindow.dpr)
|
||||
height: Theme.px(600, overlayWindow.dpr)
|
||||
|
||||
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
|
||||
|
||||
scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96
|
||||
opacity: overlayWindow.shouldShowSpotlight ? 1 : 0
|
||||
visible: overlayWindow.shouldShowSpotlight || animatingOut
|
||||
enabled: overlayWindow.shouldShowSpotlight
|
||||
|
||||
layer.enabled: true
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(Math.round(width * overlayWindow.dpr), Math.round(height * overlayWindow.dpr))
|
||||
|
||||
Behavior on scale {
|
||||
id: scaleAnimation
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||
onRunningChanged: {
|
||||
if (running || !spotlightContainer.animatingOut)
|
||||
return;
|
||||
niriOverviewScope.resetState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: spotlightContainer
|
||||
x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr)
|
||||
y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr)
|
||||
width: Theme.px(500, overlayWindow.dpr)
|
||||
height: Theme.px(600, overlayWindow.dpr)
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96
|
||||
opacity: overlayWindow.shouldShowSpotlight ? 1 : 0
|
||||
visible: overlayWindow.shouldShowSpotlight || animatingOut
|
||||
enabled: overlayWindow.shouldShowSpotlight
|
||||
LauncherContent {
|
||||
id: launcherContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: 0
|
||||
|
||||
layer.enabled: true
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(Math.round(width * overlayWindow.dpr), Math.round(height * overlayWindow.dpr))
|
||||
|
||||
Behavior on scale {
|
||||
id: scaleAnimation
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
onRunningChanged: {
|
||||
if (running || !spotlightContainer.animatingOut)
|
||||
return;
|
||||
niriOverviewScope.resetState();
|
||||
property var fakeParentModal: QtObject {
|
||||
property bool spotlightOpen: spotlightContainer.visible
|
||||
property bool isClosing: niriOverviewScope.isClosing
|
||||
function hide() {
|
||||
if (niriOverviewScope.searchActive) {
|
||||
niriOverviewScope.hideAndReleaseKeyboard();
|
||||
return;
|
||||
}
|
||||
NiriService.toggleOverview();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
Connections {
|
||||
target: launcherContent.searchField
|
||||
function onTextChanged() {
|
||||
if (launcherContent.searchField.text.length > 0 || !niriOverviewScope.searchActive)
|
||||
return;
|
||||
niriOverviewScope.hideSpotlight();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 1
|
||||
Component.onCompleted: {
|
||||
parentModal = fakeParentModal;
|
||||
}
|
||||
|
||||
SpotlightContent {
|
||||
id: spotlightContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: 0
|
||||
usePopupContextMenu: true
|
||||
|
||||
property var fakeParentModal: QtObject {
|
||||
property bool spotlightOpen: spotlightContainer.visible
|
||||
function hide() {
|
||||
if (niriOverviewScope.searchActive) {
|
||||
niriOverviewScope.hideAndReleaseKeyboard();
|
||||
return;
|
||||
}
|
||||
NiriService.toggleOverview();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: spotlightContent.searchField
|
||||
function onTextChanged() {
|
||||
if (spotlightContent.searchField.text.length > 0 || !niriOverviewScope.searchActive)
|
||||
return;
|
||||
niriOverviewScope.hideSpotlight();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
parentModal = fakeParentModal;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: spotlightContent.appLauncher
|
||||
function onAppLaunched() {
|
||||
niriOverviewScope.releaseKeyboard = true;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: spotlightContent.fileSearchController
|
||||
function onFileOpened() {
|
||||
niriOverviewScope.releaseKeyboard = true;
|
||||
}
|
||||
Connections {
|
||||
target: launcherContent.controller
|
||||
function onItemExecuted() {
|
||||
niriOverviewScope.releaseKeyboard = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
113
quickshell/PLUGINS/LauncherImageExample/LauncherImageExample.qml
Normal file
113
quickshell/PLUGINS/LauncherImageExample/LauncherImageExample.qml
Normal file
@@ -0,0 +1,113 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Services
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
property string trigger: "img"
|
||||
|
||||
signal itemsChanged
|
||||
|
||||
readonly property var images: [
|
||||
{
|
||||
name: "DankDash",
|
||||
imageUrl: "https://danklinux.com/img/dankdash.png",
|
||||
comment: "DankMaterialShell Dashboard"
|
||||
},
|
||||
{
|
||||
name: "Control Center",
|
||||
imageUrl: "https://danklinux.com/img/cc.png",
|
||||
comment: "System Control Center"
|
||||
},
|
||||
{
|
||||
name: "Desktop",
|
||||
imageUrl: "https://danklinux.com/img/desktop.png",
|
||||
comment: "Desktop Environment"
|
||||
},
|
||||
{
|
||||
name: "Search",
|
||||
imageUrl: "https://danklinux.com/img/dsearch.png",
|
||||
comment: "Application Search"
|
||||
},
|
||||
{
|
||||
name: "Theme Registry",
|
||||
imageUrl: "https://danklinux.com/img/blog/v1.2/themeregistry.png",
|
||||
comment: "Theme Registry Browser"
|
||||
},
|
||||
{
|
||||
name: "Monitor Settings",
|
||||
imageUrl: "https://danklinux.com/img/blog/v1.2/monitordark.png",
|
||||
comment: "Display Configuration"
|
||||
}
|
||||
]
|
||||
|
||||
function getItems(query) {
|
||||
const lowerQuery = query ? query.toLowerCase().trim() : "";
|
||||
|
||||
if (lowerQuery.length === 0) {
|
||||
return images.map(img => ({
|
||||
name: img.name,
|
||||
icon: "material:image",
|
||||
comment: img.comment,
|
||||
action: "view:" + img.imageUrl,
|
||||
categories: ["Image Gallery"],
|
||||
imageUrl: img.imageUrl
|
||||
}));
|
||||
}
|
||||
|
||||
return images.filter(img => img.name.toLowerCase().includes(lowerQuery) || img.comment.toLowerCase().includes(lowerQuery)).map(img => ({
|
||||
name: img.name,
|
||||
icon: "material:image",
|
||||
comment: img.comment,
|
||||
action: "view:" + img.imageUrl,
|
||||
categories: ["Image Gallery"],
|
||||
imageUrl: img.imageUrl
|
||||
}));
|
||||
}
|
||||
|
||||
function executeItem(item) {
|
||||
if (!item?.action)
|
||||
return;
|
||||
const actionParts = item.action.split(":");
|
||||
const actionType = actionParts[0];
|
||||
const actionData = actionParts.slice(1).join(":");
|
||||
|
||||
if (actionType === "view") {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showInfo("Image Gallery", "Viewing: " + item.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getContextMenuActions(item) {
|
||||
if (!item)
|
||||
return [];
|
||||
return [
|
||||
{
|
||||
icon: "open_in_new",
|
||||
text: "Open in Browser",
|
||||
action: () => {
|
||||
const url = item.imageUrl || "";
|
||||
if (url) {
|
||||
Qt.openUrlExternally(url);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: "content_copy",
|
||||
text: "Copy URL",
|
||||
action: () => {
|
||||
const url = item.imageUrl || "";
|
||||
if (url) {
|
||||
Quickshell.execDetached(["dms", "cl", "copy", url]);
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showInfo("Copied", url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
43
quickshell/PLUGINS/LauncherImageExample/README.md
Normal file
43
quickshell/PLUGINS/LauncherImageExample/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# LauncherImageExample
|
||||
|
||||
Example launcher plugin demonstrating tile mode with URL-based images.
|
||||
|
||||
## Features
|
||||
|
||||
- **Tile Mode**: Uses `viewMode: "tile"` in plugin.json to display results as image tiles
|
||||
- **Enforced View Mode**: Uses `viewModeEnforced: true` to lock the view to tile mode (users cannot change it)
|
||||
- **URL Images**: Demonstrates using `imageUrl` property for remote images
|
||||
|
||||
## Usage
|
||||
|
||||
1. Open the launcher (DankLauncherV2)
|
||||
2. Type `img` to activate the plugin
|
||||
3. Browse DankMaterialShell screenshots in tile view
|
||||
|
||||
## Plugin Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"viewMode": "tile",
|
||||
"viewModeEnforced": true
|
||||
}
|
||||
```
|
||||
|
||||
- `viewMode`: Sets the default view mode ("list", "grid", or "tile")
|
||||
- `viewModeEnforced`: When true, users cannot switch view modes for this plugin
|
||||
|
||||
## Item Data Structure
|
||||
|
||||
To display images in tile mode, set `imageUrl` directly on the item:
|
||||
|
||||
```javascript
|
||||
{
|
||||
name: "Image Title",
|
||||
icon: "material:image",
|
||||
comment: "Image description",
|
||||
categories: ["Category"],
|
||||
imageUrl: "https://example.com/image.png"
|
||||
}
|
||||
```
|
||||
|
||||
The `imageUrl` property supports remote URLs or local files, use `file://` prefix for local files.
|
||||
14
quickshell/PLUGINS/LauncherImageExample/plugin.json
Normal file
14
quickshell/PLUGINS/LauncherImageExample/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "launcherImageExample",
|
||||
"name": "Image Gallery Example",
|
||||
"description": "Example launcher plugin demonstrating tile mode with images",
|
||||
"version": "1.0.0",
|
||||
"author": "DMS Team",
|
||||
"icon": "photo_library",
|
||||
"type": "launcher",
|
||||
"trigger": "img",
|
||||
"viewMode": "tile",
|
||||
"viewModeEnforced": true,
|
||||
"component": "./LauncherImageExample.qml",
|
||||
"permissions": []
|
||||
}
|
||||
@@ -13,6 +13,12 @@ Singleton {
|
||||
property var _cachedVisibleApps: null
|
||||
property var _hiddenAppsSet: new Set()
|
||||
|
||||
property var _transformCache: ({})
|
||||
property var _cachedDefaultSections: []
|
||||
property var _cachedDefaultFlatModel: []
|
||||
property bool _defaultCacheValid: false
|
||||
property int cacheVersion: 0
|
||||
|
||||
readonly property int maxResults: 10
|
||||
readonly property int frecencySampleSize: 10
|
||||
|
||||
@@ -43,6 +49,50 @@ Singleton {
|
||||
applications = DesktopEntries.applications.values;
|
||||
_cachedCategories = null;
|
||||
_cachedVisibleApps = null;
|
||||
invalidateLauncherCache();
|
||||
}
|
||||
|
||||
function invalidateLauncherCache() {
|
||||
_transformCache = {};
|
||||
_defaultCacheValid = false;
|
||||
_cachedDefaultSections = [];
|
||||
_cachedDefaultFlatModel = [];
|
||||
cacheVersion++;
|
||||
}
|
||||
|
||||
function getOrTransformApp(app, transformFn) {
|
||||
const id = app.id || app.execString || app.exec || "";
|
||||
if (!id)
|
||||
return transformFn(app);
|
||||
const cached = _transformCache[id];
|
||||
if (cached) {
|
||||
const currentIcon = app.icon || "";
|
||||
const cachedSourceIcon = cached._sourceIcon || "";
|
||||
if (currentIcon === cachedSourceIcon)
|
||||
return cached;
|
||||
}
|
||||
const transformed = transformFn(app);
|
||||
transformed._sourceIcon = app.icon || "";
|
||||
_transformCache[id] = transformed;
|
||||
return transformed;
|
||||
}
|
||||
|
||||
function getCachedDefaultSections() {
|
||||
if (!_defaultCacheValid)
|
||||
return null;
|
||||
return _cachedDefaultSections;
|
||||
}
|
||||
|
||||
function setCachedDefaultSections(sections, flatModel) {
|
||||
_cachedDefaultSections = sections.map(function (s) {
|
||||
return Object.assign({}, s);
|
||||
});
|
||||
_cachedDefaultFlatModel = flatModel.slice();
|
||||
_defaultCacheValid = true;
|
||||
}
|
||||
|
||||
function isCacheValid() {
|
||||
return _defaultCacheValid;
|
||||
}
|
||||
|
||||
function _rebuildHiddenSet() {
|
||||
@@ -68,9 +118,18 @@ Singleton {
|
||||
target: SessionData
|
||||
function onHiddenAppsChanged() {
|
||||
root._rebuildHiddenSet();
|
||||
root.invalidateLauncherCache();
|
||||
}
|
||||
function onAppOverridesChanged() {
|
||||
root._cachedVisibleApps = null;
|
||||
root.invalidateLauncherCache();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AppUsageHistoryData
|
||||
function onAppUsageRankingChanged() {
|
||||
root.invalidateLauncherCache();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -293,7 +293,6 @@ Singleton {
|
||||
pluginDaemonComponents = newDaemons;
|
||||
} else if (isLauncher) {
|
||||
const instance = comp.createObject(root, {
|
||||
"pluginId": pluginId,
|
||||
"pluginService": root
|
||||
});
|
||||
if (!instance) {
|
||||
@@ -702,6 +701,17 @@ Singleton {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
function getPluginViewPreference(pluginId) {
|
||||
const plugin = availablePlugins[pluginId];
|
||||
if (!plugin)
|
||||
return null;
|
||||
|
||||
return {
|
||||
mode: plugin.viewMode || null,
|
||||
enforced: plugin.viewModeEnforced === true
|
||||
};
|
||||
}
|
||||
|
||||
function getGlobalVar(pluginId, varName, defaultValue) {
|
||||
if (globalVars[pluginId] && varName in globalVars[pluginId]) {
|
||||
return globalVars[pluginId][varName];
|
||||
|
||||
@@ -21,6 +21,8 @@ Singleton {
|
||||
property var settingsModalLoader: null
|
||||
property var clipboardHistoryModal: null
|
||||
property var spotlightModal: null
|
||||
property var spotlightV2Modal: null
|
||||
property var spotlightV2ModalLoader: null
|
||||
property var powerMenuModal: null
|
||||
property var processListModal: null
|
||||
property var processListModalLoader: null
|
||||
@@ -361,6 +363,62 @@ Singleton {
|
||||
spotlightModal?.close();
|
||||
}
|
||||
|
||||
property bool _spotlightV2WantsOpen: false
|
||||
property bool _spotlightV2WantsToggle: false
|
||||
property string _spotlightV2PendingQuery: ""
|
||||
|
||||
function openSpotlightV2() {
|
||||
if (spotlightV2Modal) {
|
||||
spotlightV2Modal.show();
|
||||
} else if (spotlightV2ModalLoader) {
|
||||
_spotlightV2WantsOpen = true;
|
||||
_spotlightV2WantsToggle = false;
|
||||
spotlightV2ModalLoader.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
function openSpotlightV2WithQuery(query: string) {
|
||||
if (spotlightV2Modal) {
|
||||
spotlightV2Modal.showWithQuery(query);
|
||||
} else if (spotlightV2ModalLoader) {
|
||||
_spotlightV2PendingQuery = query;
|
||||
_spotlightV2WantsOpen = true;
|
||||
_spotlightV2WantsToggle = false;
|
||||
spotlightV2ModalLoader.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
function closeSpotlightV2() {
|
||||
spotlightV2Modal?.hide();
|
||||
}
|
||||
|
||||
function toggleSpotlightV2() {
|
||||
if (spotlightV2Modal) {
|
||||
spotlightV2Modal.toggle();
|
||||
} else if (spotlightV2ModalLoader) {
|
||||
_spotlightV2WantsToggle = true;
|
||||
_spotlightV2WantsOpen = false;
|
||||
spotlightV2ModalLoader.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
function _onSpotlightV2ModalLoaded() {
|
||||
if (_spotlightV2WantsOpen) {
|
||||
_spotlightV2WantsOpen = false;
|
||||
if (_spotlightV2PendingQuery) {
|
||||
spotlightV2Modal?.showWithQuery(_spotlightV2PendingQuery);
|
||||
_spotlightV2PendingQuery = "";
|
||||
} else {
|
||||
spotlightV2Modal?.show();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_spotlightV2WantsToggle) {
|
||||
_spotlightV2WantsToggle = false;
|
||||
spotlightV2Modal?.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
function openPowerMenu() {
|
||||
powerMenuModal?.openCentered();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,17 @@ Image {
|
||||
property string imagePath: ""
|
||||
property int maxCacheSize: 512
|
||||
|
||||
readonly property bool isRemoteUrl: imagePath.startsWith("http://") || imagePath.startsWith("https://")
|
||||
readonly property string normalizedPath: {
|
||||
if (!imagePath)
|
||||
return "";
|
||||
if (isRemoteUrl)
|
||||
return imagePath;
|
||||
if (imagePath.startsWith("file://"))
|
||||
return imagePath.substring(7);
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
function djb2Hash(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
@@ -18,9 +29,15 @@ Image {
|
||||
return hash.toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
readonly property string imageHash: imagePath ? djb2Hash(imagePath) : ""
|
||||
readonly property string cachePath: imageHash ? `${Paths.stringify(Paths.imagecache)}/${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : ""
|
||||
readonly property string encodedImagePath: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
readonly property string imageHash: normalizedPath ? djb2Hash(normalizedPath) : ""
|
||||
readonly property string cachePath: imageHash && !isRemoteUrl ? `${Paths.stringify(Paths.imagecache)}/${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : ""
|
||||
readonly property string encodedImagePath: {
|
||||
if (!normalizedPath)
|
||||
return "";
|
||||
if (isRemoteUrl)
|
||||
return normalizedPath;
|
||||
return "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
@@ -33,10 +50,14 @@ Image {
|
||||
source = "";
|
||||
return;
|
||||
}
|
||||
if (isRemoteUrl) {
|
||||
source = imagePath;
|
||||
return;
|
||||
}
|
||||
Paths.mkdir(Paths.imagecache);
|
||||
const hash = djb2Hash(imagePath);
|
||||
const hash = djb2Hash(normalizedPath);
|
||||
const cPath = hash ? `${Paths.stringify(Paths.imagecache)}/${hash}@${maxCacheSize}x${maxCacheSize}.png` : "";
|
||||
const encoded = "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
const encoded = "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
source = cPath || encoded;
|
||||
}
|
||||
|
||||
@@ -45,7 +66,7 @@ Image {
|
||||
source = encodedImagePath;
|
||||
return;
|
||||
}
|
||||
if (source != encodedImagePath || status !== Image.Ready || !cachePath)
|
||||
if (isRemoteUrl || source != encodedImagePath || status !== Image.Ready || !cachePath)
|
||||
return;
|
||||
Paths.mkdir(Paths.imagecache);
|
||||
const grabPath = cachePath;
|
||||
|
||||
@@ -8,17 +8,9 @@ StyledRect {
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
KeyNavigation.tab: keyNavigationTab
|
||||
KeyNavigation.backtab: keyNavigationBacktab
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
textInput.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
property alias text: textInput.text
|
||||
property string placeholderText: ""
|
||||
property alias font: textInput.font
|
||||
@@ -43,15 +35,15 @@ StyledRect {
|
||||
property real cornerRadius: Theme.cornerRadius
|
||||
readonly property real leftPadding: Theme.spacingM + (leftIconName ? leftIconSize + Theme.spacingM : 0)
|
||||
readonly property real rightPadding: {
|
||||
let p = Theme.spacingM;
|
||||
let p = Theme.spacingS;
|
||||
if (showPasswordToggle)
|
||||
p += 24 + Theme.spacingS;
|
||||
p += 20 + Theme.spacingXS;
|
||||
if (showClearButton && text.length > 0)
|
||||
p += 24 + Theme.spacingS;
|
||||
p += 20 + Theme.spacingXS;
|
||||
return p;
|
||||
}
|
||||
property real topPadding: Theme.spacingM
|
||||
property real bottomPadding: Theme.spacingM
|
||||
property real topPadding: Theme.spacingS
|
||||
property real bottomPadding: Theme.spacingS
|
||||
property bool ignoreLeftRightKeys: false
|
||||
property bool ignoreUpDownKeys: false
|
||||
property bool ignoreTabKeys: false
|
||||
@@ -84,7 +76,7 @@ StyledRect {
|
||||
}
|
||||
|
||||
width: 200
|
||||
height: Math.round(Theme.fontSizeMedium * 3.4)
|
||||
height: Math.round(Theme.fontSizeMedium * 3)
|
||||
radius: cornerRadius
|
||||
color: backgroundColor
|
||||
border.color: textInput.activeFocus ? focusedBorderColor : normalBorderColor
|
||||
@@ -172,7 +164,7 @@ StyledRect {
|
||||
id: rightButtonsRow
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
visible: showPasswordToggle || (showClearButton && text.length > 0)
|
||||
@@ -180,16 +172,16 @@ StyledRect {
|
||||
StyledRect {
|
||||
id: passwordToggleButton
|
||||
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: passwordToggleArea.containsMouse ? Theme.outlineStrong : "transparent"
|
||||
visible: showPasswordToggle
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: passwordVisible ? "visibility_off" : "visibility"
|
||||
size: 16
|
||||
size: 14
|
||||
color: passwordToggleArea.containsMouse ? Theme.outline : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
@@ -206,16 +198,16 @@ StyledRect {
|
||||
StyledRect {
|
||||
id: clearButton
|
||||
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: clearArea.containsMouse ? Theme.outlineStrong : "transparent"
|
||||
visible: showClearButton && text.length > 0
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "close"
|
||||
size: 16
|
||||
size: 14
|
||||
color: clearArea.containsMouse ? Theme.outline : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
|
||||
@@ -1109,6 +1109,7 @@
|
||||
"tabIndex": 5,
|
||||
"category": "Dock",
|
||||
"keywords": [
|
||||
"always",
|
||||
"area",
|
||||
"auto",
|
||||
"autohide",
|
||||
@@ -1122,7 +1123,7 @@
|
||||
"reveal",
|
||||
"taskbar"
|
||||
],
|
||||
"description": "Hide the dock when not in use and reveal it when hovering near the dock area"
|
||||
"description": "Always hide the dock and reveal it when hovering near the dock area"
|
||||
},
|
||||
{
|
||||
"section": "dockBehavior",
|
||||
@@ -1276,6 +1277,29 @@
|
||||
"taskbar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "dockSmartAutoHide",
|
||||
"label": "Intelligent Auto-hide",
|
||||
"tabIndex": 5,
|
||||
"category": "Dock",
|
||||
"keywords": [
|
||||
"auto",
|
||||
"autohide",
|
||||
"dock",
|
||||
"floating",
|
||||
"hide",
|
||||
"intelligent",
|
||||
"launcher bar",
|
||||
"overlap",
|
||||
"panel",
|
||||
"show",
|
||||
"smart",
|
||||
"taskbar",
|
||||
"windows"
|
||||
],
|
||||
"description": "Show dock when floating windows don",
|
||||
"conditionKey": "isNiri"
|
||||
},
|
||||
{
|
||||
"section": "dockIsolateDisplays",
|
||||
"label": "Isolate Displays",
|
||||
@@ -1428,6 +1452,58 @@
|
||||
"icon": "computer",
|
||||
"conditionKey": "cupsAvailable"
|
||||
},
|
||||
{
|
||||
"section": "appOverrides",
|
||||
"label": "App Customizations",
|
||||
"tabIndex": 9,
|
||||
"category": "Launcher",
|
||||
"keywords": [
|
||||
"app",
|
||||
"customizations",
|
||||
"drawer",
|
||||
"launcher",
|
||||
"menu",
|
||||
"start"
|
||||
],
|
||||
"icon": "edit"
|
||||
},
|
||||
{
|
||||
"section": "dankLauncherV2Appearance",
|
||||
"label": "Appearance",
|
||||
"tabIndex": 9,
|
||||
"category": "Launcher",
|
||||
"keywords": [
|
||||
"appearance",
|
||||
"bottom",
|
||||
"drawer",
|
||||
"footer",
|
||||
"hints",
|
||||
"keyboard",
|
||||
"launcher",
|
||||
"menu",
|
||||
"mode",
|
||||
"shortcuts",
|
||||
"show",
|
||||
"start",
|
||||
"tabs"
|
||||
],
|
||||
"icon": "tune",
|
||||
"description": "Show mode tabs and keyboard hints at the bottom."
|
||||
},
|
||||
{
|
||||
"section": "dankLauncherV2BorderEnabled",
|
||||
"label": "Border",
|
||||
"tabIndex": 9,
|
||||
"category": "Launcher",
|
||||
"keywords": [
|
||||
"border",
|
||||
"drawer",
|
||||
"launcher",
|
||||
"menu",
|
||||
"outline",
|
||||
"start"
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "launcherLogoBrightness",
|
||||
"label": "Brightness",
|
||||
@@ -1520,6 +1596,21 @@
|
||||
],
|
||||
"description": "Adjust the number of columns in grid view mode."
|
||||
},
|
||||
{
|
||||
"section": "hiddenApps",
|
||||
"label": "Hidden Apps",
|
||||
"tabIndex": 9,
|
||||
"category": "Launcher",
|
||||
"keywords": [
|
||||
"apps",
|
||||
"drawer",
|
||||
"hidden",
|
||||
"launcher",
|
||||
"menu",
|
||||
"start"
|
||||
],
|
||||
"icon": "visibility_off"
|
||||
},
|
||||
{
|
||||
"section": "launcherLogoColorInvertOnMode",
|
||||
"label": "Invert on mode change",
|
||||
@@ -1629,6 +1720,68 @@
|
||||
],
|
||||
"icon": "history"
|
||||
},
|
||||
{
|
||||
"section": "searchAppActions",
|
||||
"label": "Search App Actions",
|
||||
"tabIndex": 9,
|
||||
"category": "Launcher",
|
||||
"keywords": [
|
||||
"actions",
|
||||
"app",
|
||||
"desktop",
|
||||
"drawer",
|
||||
"include",
|
||||
"launcher",
|
||||
"menu",
|
||||
"results",
|
||||
"search",
|
||||
"shortcuts",
|
||||
"start"
|
||||
],
|
||||
"description": "Include desktop actions (shortcuts) in search results."
|
||||
},
|
||||
{
|
||||
"section": "searchOptions",
|
||||
"label": "Search Options",
|
||||
"tabIndex": 9,
|
||||
"category": "Launcher",
|
||||
"keywords": [
|
||||
"actions",
|
||||
"desktop",
|
||||
"drawer",
|
||||
"include",
|
||||
"launcher",
|
||||
"menu",
|
||||
"options",
|
||||
"results",
|
||||
"search",
|
||||
"shortcuts",
|
||||
"start"
|
||||
],
|
||||
"icon": "search",
|
||||
"description": "Include desktop actions (shortcuts) in search results."
|
||||
},
|
||||
{
|
||||
"section": "dankLauncherV2ShowFooter",
|
||||
"label": "Show Footer",
|
||||
"tabIndex": 9,
|
||||
"category": "Launcher",
|
||||
"keywords": [
|
||||
"bottom",
|
||||
"drawer",
|
||||
"footer",
|
||||
"hints",
|
||||
"keyboard",
|
||||
"launcher",
|
||||
"menu",
|
||||
"mode",
|
||||
"shortcuts",
|
||||
"show",
|
||||
"start",
|
||||
"tabs"
|
||||
],
|
||||
"description": "Show mode tabs and keyboard hints at the bottom."
|
||||
},
|
||||
{
|
||||
"section": "launcherLogoSizeOffset",
|
||||
"label": "Size Offset",
|
||||
@@ -1692,6 +1845,20 @@
|
||||
"icon": "sort_by_alpha",
|
||||
"description": "When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency."
|
||||
},
|
||||
{
|
||||
"section": "dankLauncherV2BorderThickness",
|
||||
"label": "Thickness",
|
||||
"tabIndex": 9,
|
||||
"category": "Launcher",
|
||||
"keywords": [
|
||||
"border",
|
||||
"drawer",
|
||||
"launcher",
|
||||
"menu",
|
||||
"start",
|
||||
"thickness"
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "matugenTemplateAlacritty",
|
||||
"label": "Alacritty",
|
||||
@@ -3369,6 +3536,40 @@
|
||||
],
|
||||
"icon": "security"
|
||||
},
|
||||
{
|
||||
"section": "lockScreenPowerOffMonitorsOnLock",
|
||||
"label": "Power off monitors on lock",
|
||||
"tabIndex": 11,
|
||||
"category": "Lock Screen",
|
||||
"keywords": [
|
||||
"activates",
|
||||
"display",
|
||||
"displays",
|
||||
"dpms",
|
||||
"hibernate",
|
||||
"immediately",
|
||||
"lock",
|
||||
"lockscreen",
|
||||
"login",
|
||||
"monitor",
|
||||
"monitors",
|
||||
"off",
|
||||
"output",
|
||||
"outputs",
|
||||
"password",
|
||||
"power",
|
||||
"reboot",
|
||||
"restart",
|
||||
"screen",
|
||||
"screens",
|
||||
"security",
|
||||
"shutdown",
|
||||
"sleep",
|
||||
"suspend",
|
||||
"turn"
|
||||
],
|
||||
"description": "Turn off all displays immediately when the lock screen activates"
|
||||
},
|
||||
{
|
||||
"section": "lockScreenShowPasswordField",
|
||||
"label": "Show Password Field",
|
||||
@@ -5047,6 +5248,26 @@
|
||||
],
|
||||
"description": "Maximum size per clipboard entry"
|
||||
},
|
||||
{
|
||||
"section": "maxPinned",
|
||||
"label": "Maximum Pinned Entries",
|
||||
"tabIndex": 23,
|
||||
"category": "System",
|
||||
"keywords": [
|
||||
"clipboard",
|
||||
"entries",
|
||||
"limit",
|
||||
"linux",
|
||||
"max",
|
||||
"maximum",
|
||||
"number",
|
||||
"os",
|
||||
"pinned",
|
||||
"saved",
|
||||
"system"
|
||||
],
|
||||
"description": "Maximum number of entries that can be saved"
|
||||
},
|
||||
{
|
||||
"section": "_tab_24",
|
||||
"label": "Displays",
|
||||
|
||||
Reference in New Issue
Block a user