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

Compare commits

...

30 Commits

Author SHA1 Message Date
purian23
6bf1438ef1 fix: dms chroma hang on print 2026-01-21 22:47:53 -05:00
bbedward
b819306ab6 launcher v2: use Top layer by default 2026-01-21 21:59:38 -05:00
bbedward
b140afca8e launcher v2: retire spotlight launcher in favor of dank launcher 2026-01-21 21:34:31 -05:00
bbedward
6735989455 launcher v2: reset visibility on screen change 2026-01-21 19:29:03 -05:00
bbedward
db37ac24c7 launcher v2: support CachingImage in icon renderer 2026-01-21 17:54:36 -05:00
bbedward
0231270f9e launcher v2: use AppIconRenderer from legacy launcha 2026-01-21 17:51:24 -05:00
bbedward
b5194aa9e1 notifications: update dimensions and text expansion logic 2026-01-21 16:51:39 -05:00
bbedward
ea0ffaacb0 launcher v2: fix some plugin icon handling 2026-01-21 16:09:52 -05:00
bbedward
3b1f084a13 notepad: fix unsave changed dialog height 2026-01-21 16:01:59 -05:00
bbedward
39a9e3a89f add dms doctor to issue template 2026-01-21 14:25:41 -05:00
bbedward
7a7af775c2 launcher v2: some optims on meta performance
- limit plugin results to 10
- longer debounce
- search plugins when chars > 1
2026-01-21 14:20:12 -05:00
bbedward
6ac2a305f7 launcher v2: sort order preference for plugin results 2026-01-21 14:08:40 -05:00
bbedward
3507c6cec3 i18n: RTL fixes in about tab and dank bar settings 2026-01-21 11:57:46 -05:00
purian23
3ff00768ac core: dms chroma notepad updates 2026-01-21 11:48:08 -05:00
bbedward
556d253ea8 launcher v2: fix view mode persistence 2026-01-21 11:43:02 -05:00
bbedward
3922070488 launcher v2: meta improvements
- Allow disabling each plugin from "all" mode
- add IPCs for toggling specific modes
- niri: overview respect size & default to apps mode
- fix unicode icon handling
2026-01-21 11:38:48 -05:00
Eggrror404
eebb4827c4 feat(bar): enlarge bar icons if widget background is off (#1425)
* use iconSizeLarge if noBackground is on

* widgets: pass noBackground to barIconSize in param
2026-01-21 10:44:08 -05:00
Kamil Chmielewski
fd2c6a0784 Feat/niri workspace names (#1396)
* dankbar: show niri workspace names

Keep labels aligned with niri indices and live renames.

* dankbar: prefix named workspaces with index

Use workspace index toggle to show index: name labels.

* workspaces: change size conditions for workspace names

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-01-21 10:43:55 -05:00
bbedward
417bf37515 clipboard: fix header GUI and add tooltips 2026-01-21 10:19:52 -05:00
bbedward
132e799265 Revert "settings: fix modal not opening on latest quickshell (#1357)"
This reverts commit bdd01e335d.
2026-01-21 09:19:12 -05:00
dms-ci[bot]
bdc864781b nix: update vendorHash for go.mod changes 2026-01-21 14:18:47 +00:00
purian23
a343bc7562 feat: DMS Core Chroma Syntax Highlighter
- Thanks alecthomas for the project
2026-01-21 09:16:58 -05:00
bbedward
1f2e231386 launcher v2: fix context switch back on empty text field 2026-01-20 21:57:50 -05:00
bbedward
0e7f628c4a launcher v2: improve danksearch context switching behavior 2026-01-20 21:55:05 -05:00
bbedward
553f5257b3 launcher v2: general padding improvements, to more than just launcher v2
but yea
2026-01-20 21:46:02 -05:00
bbedward
80ce6aa19c launcher v2: spacing adjustments 2026-01-20 18:10:55 -05:00
bbedward
2b2977de4a launcher v2: smarter right/left arrow key handler 2026-01-20 18:02:23 -05:00
bbedward
1d5d876e16 launcher: Dank Launcher V2 (beta)
- Aggregate plugins/extensions in new "all" tab
- Quick tab actions
- New tile mode for results
- Plugins can enforce/require view mode, or set preferred default
- Danksearch under "files" category
2026-01-20 17:59:13 -05:00
Body
3c39162016 remove hardcoded width and padding fixing overlap (#1446) 2026-01-20 16:19:59 -05:00
bbedward
d38767fb5a settings: fix power&sleep tab button groups
fixes #1442
2026-01-20 11:41:39 -05:00
98 changed files with 7617 additions and 4905 deletions

View File

@@ -42,12 +42,12 @@ body:
placeholder: e.g., PikaOS, Void Linux, etc.
validations:
required: false
- type: input
id: dms_version
- type: textarea
id: dms_doctor
attributes:
label: dms version
description: Output of dms version command
placeholder: e.g., 1.2.3
label: dms doctor -v
description: Output of `dms doctor -v` command
placeholder: Paste the output of `dms doctor -v` here
validations:
required: true
- type: textarea

View File

@@ -27,12 +27,12 @@ body:
placeholder: Your Linux distribution
validations:
required: false
- type: input
id: dms_version
- type: textarea
id: dms_doctor
attributes:
label: dms version
description: Output of dms version command
placeholder: e.g., 1.2.3
label: dms doctor -v
description: Output of `dms doctor -v` command
placeholder: Paste the output of `dms doctor -v` here
validations:
required: false
- type: textarea

View File

@@ -0,0 +1,300 @@
package main
import (
"bytes"
"fmt"
"io"
"os"
"strings"
"sync"
"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
chromaLineNumbers bool
// Caching layer for performance
lexerCache = make(map[string]chroma.Lexer)
styleCache = make(map[string]*chroma.Style)
formatterCache = make(map[string]*html.Formatter)
cacheMutex sync.RWMutex
maxFileSize = int64(5 * 1024 * 1024) // 5MB default
)
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().BoolVar(&chromaLineNumbers, "line-numbers", false, "Show line numbers in output")
chromaCmd.Flags().BoolVarP(&chromaMarkdown, "markdown", "m", false, "Render markdown with syntax-highlighted code blocks")
chromaCmd.Flags().Int64Var(&maxFileSize, "max-size", 5*1024*1024, "Maximum file size to process without warning (bytes)")
chromaCmd.AddCommand(chromaListLanguagesCmd)
chromaCmd.AddCommand(chromaListStylesCmd)
}
func getCachedLexer(key string, fallbackFunc func() chroma.Lexer) chroma.Lexer {
cacheMutex.RLock()
if lexer, ok := lexerCache[key]; ok {
cacheMutex.RUnlock()
return lexer
}
cacheMutex.RUnlock()
lexer := fallbackFunc()
if lexer != nil {
cacheMutex.Lock()
lexerCache[key] = lexer
cacheMutex.Unlock()
}
return lexer
}
func getCachedStyle(name string) *chroma.Style {
cacheMutex.RLock()
if style, ok := styleCache[name]; ok {
cacheMutex.RUnlock()
return style
}
cacheMutex.RUnlock()
style := styles.Get(name)
if style == nil {
fmt.Fprintf(os.Stderr, "Warning: Style '%s' not found, using fallback\n", name)
style = styles.Fallback
}
cacheMutex.Lock()
styleCache[name] = style
cacheMutex.Unlock()
return style
}
func getCachedFormatter(inline bool, lineNumbers bool) *html.Formatter {
key := fmt.Sprintf("inline=%t,lineNumbers=%t", inline, lineNumbers)
cacheMutex.RLock()
if formatter, ok := formatterCache[key]; ok {
cacheMutex.RUnlock()
return formatter
}
cacheMutex.RUnlock()
var opts []html.Option
if inline {
opts = append(opts, html.WithClasses(false))
} else {
opts = append(opts, html.WithClasses(true))
}
opts = append(opts, html.TabWidth(4))
if lineNumbers {
opts = append(opts, html.WithLineNumbers(true))
opts = append(opts, html.LineNumbersInTable(false))
opts = append(opts, html.WithLinkableLineNumbers(false, ""))
}
formatter := html.New(opts...)
cacheMutex.Lock()
formatterCache[key] = formatter
cacheMutex.Unlock()
return formatter
}
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]
// Check file size before reading
fileInfo, err := os.Stat(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading file info: %v\n", err)
os.Exit(1)
}
if fileInfo.Size() > maxFileSize {
fmt.Fprintf(os.Stderr, "Warning: File size (%d bytes) exceeds recommended limit (%d bytes)\n",
fileInfo.Size(), maxFileSize)
fmt.Fprintf(os.Stderr, "Processing may be slow. Consider using smaller files.\n")
}
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 {
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) != 0 {
_ = cmd.Help()
os.Exit(0)
}
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 = getCachedLexer(chromaLanguage, func() chroma.Lexer {
l := lexers.Get(chromaLanguage)
if l == nil {
fmt.Fprintf(os.Stderr, "Unknown language: %s\n", chromaLanguage)
os.Exit(1)
}
return l
})
} else if filename != "" {
lexer = getCachedLexer("file:"+filename, func() chroma.Lexer {
return lexers.Match(filename)
})
}
// Try content analysis if no lexer found (limit to first 1KB for performance)
if lexer == nil {
analyzeContent := source
if len(source) > 1024 {
analyzeContent = source[:1024]
}
lexer = lexers.Analyse(analyzeContent)
}
// Fallback to plaintext
if lexer == nil {
lexer = lexers.Fallback
}
lexer = chroma.Coalesce(lexer)
// Get cached style
style := getCachedStyle(chromaStyle)
// Get cached formatter
formatter := getCachedFormatter(chromaInline, chromaLineNumbers)
// 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 chromaLineNumbers {
var buf bytes.Buffer
if err := formatter.Format(&buf, style, iterator); err != nil {
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
os.Exit(1)
}
// Add spacing between line numbers
output := buf.String()
output = strings.ReplaceAll(output, "</span><span>", "</span>\u00A0\u00A0<span>")
fmt.Print(output)
} else {
if err := formatter.Format(os.Stdout, style, iterator); err != nil {
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
os.Exit(1)
}
}
}

View File

@@ -515,6 +515,7 @@ func getCommonCommands() []*cobra.Command {
genericNotifyActionCmd,
matugenCmd,
clipboardCmd,
chromaCmd,
doctorCmd,
configCmd,
}

View File

@@ -4,6 +4,7 @@ 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
@@ -15,6 +16,8 @@ require (
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.4.3
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
golang.org/x/image v0.35.0
@@ -28,6 +31,7 @@ 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

View File

@@ -4,6 +4,14 @@ 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=
@@ -24,16 +32,12 @@ 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=
@@ -48,6 +52,10 @@ 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=
@@ -58,14 +66,10 @@ 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=
@@ -78,6 +82,8 @@ 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=
@@ -133,42 +139,35 @@ 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/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
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=
@@ -177,5 +176,6 @@ 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=

View File

@@ -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
}

View File

@@ -78,7 +78,7 @@
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-lXqOJ0yNlOcXuR3vcuVjFI02Hskmavcasb1Ntf3UlPM=";
vendorHash = "sha256-kWHB/FN6Z2Ydh+VvNrDnbg18RuJSDAle4DHDAP4NpNk=";
subPackages = [ "cmd/dms" ];

View File

@@ -79,6 +79,45 @@ Singleton {
saveSettings();
}
property var launcherPluginVisibility: ({})
function getPluginAllowWithoutTrigger(pluginId) {
if (!launcherPluginVisibility[pluginId])
return true;
return launcherPluginVisibility[pluginId].allowWithoutTrigger !== false;
}
function setPluginAllowWithoutTrigger(pluginId, allow) {
const updated = JSON.parse(JSON.stringify(launcherPluginVisibility));
if (!updated[pluginId])
updated[pluginId] = {};
updated[pluginId].allowWithoutTrigger = allow;
launcherPluginVisibility = updated;
saveSettings();
}
property var launcherPluginOrder: []
onLauncherPluginOrderChanged: saveSettings()
function setLauncherPluginOrder(order) {
launcherPluginOrder = order;
}
function getOrderedLauncherPlugins(allPlugins) {
if (!launcherPluginOrder || launcherPluginOrder.length === 0)
return allPlugins;
const orderMap = {};
for (let i = 0; i < launcherPluginOrder.length; i++)
orderMap[launcherPluginOrder[i]] = i;
return allPlugins.slice().sort((a, b) => {
const aOrder = orderMap[a.id] ?? 9999;
const bOrder = orderMap[b.id] ?? 9999;
if (aOrder !== bOrder)
return aOrder - bOrder;
return a.name.localeCompare(b.name);
});
}
property alias dankBarLeftWidgetsModel: leftWidgetsModel
property alias dankBarCenterWidgetsModel: centerWidgetsModel
property alias dankBarRightWidgetsModel: rightWidgetsModel
@@ -236,7 +275,16 @@ Singleton {
property bool sortAppsAlphabetically: false
property int appLauncherGridColumns: 4
property bool spotlightCloseNiriOverview: true
property var spotlightSectionViewModes: ({})
onSpotlightSectionViewModesChanged: saveSettings()
property var appDrawerSectionViewModes: ({})
onAppDrawerSectionViewModesChanged: saveSettings()
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"

View File

@@ -752,9 +752,11 @@ Singleton {
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5;
}
function barIconSize(barThickness, offset) {
function barIconSize(barThickness, offset, noBackground) {
const defaultOffset = offset !== undefined ? offset : -6;
return Math.round((barThickness / 48) * (iconSize + defaultOffset));
const size = (noBackground ?? false) ? iconSizeLarge : iconSize;
return Math.round((barThickness / 48) * (size + defaultOffset));
}
function barTextSize(barThickness, fontScale) {

View File

@@ -134,7 +134,14 @@ var SPEC = {
sortAppsAlphabetically: { def: false },
appLauncherGridColumns: { def: 4 },
spotlightCloseNiriOverview: { def: true },
spotlightSectionViewModes: { def: {} },
appDrawerSectionViewModes: { 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 },
@@ -409,7 +416,9 @@ var SPEC = {
desktopWidgetGroups: { def: [] },
builtInPluginSettings: { def: {} }
builtInPluginSettings: { def: {} },
launcherPluginVisibility: { def: {} },
launcherPluginOrder: { def: [] }
};
function getValidKeys() {

View File

@@ -6,7 +6,7 @@ import qs.Modals.Changelog
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
@@ -473,15 +473,17 @@ Item {
PopoutService.settingsModalLoader = settingsModalLoader;
}
onActiveChanged: {
if (active && item) {
PopoutService.settingsModal = item;
PopoutService._onSettingsModalLoaded();
}
}
SettingsModal {
id: settingsModal
property bool wasShown: false
Component.onCompleted: {
PopoutService.settingsModal = settingsModal;
PopoutService._onSettingsModalLoaded();
}
onVisibleChanged: {
if (visible) {
wasShown = true;
@@ -506,11 +508,22 @@ Item {
}
}
SpotlightModal {
id: spotlightModal
LazyLoader {
id: dankLauncherV2ModalLoader
active: false
Component.onCompleted: {
PopoutService.spotlightModal = spotlightModal;
PopoutService.dankLauncherV2ModalLoader = dankLauncherV2ModalLoader;
}
DankLauncherV2Modal {
id: dankLauncherV2Modal
Component.onCompleted: {
PopoutService.dankLauncherV2Modal = dankLauncherV2Modal;
PopoutService._onDankLauncherV2ModalLoaded();
}
}
}

View File

@@ -1025,6 +1025,94 @@ Item {
target: "clipboard"
}
// ! spotlight and launcher should be synonymous for backwards compat
IpcHandler {
function open(): string {
PopoutService.openDankLauncherV2();
return "LAUNCHER_OPEN_SUCCESS";
}
function close(): string {
PopoutService.closeDankLauncherV2();
return "LAUNCHER_CLOSE_SUCCESS";
}
function toggle(): string {
PopoutService.toggleDankLauncherV2();
return "LAUNCHER_TOGGLE_SUCCESS";
}
function openWith(mode: string): string {
if (!mode)
return "LAUNCHER_OPEN_FAILED: No mode specified";
PopoutService.openDankLauncherV2WithMode(mode);
return `LAUNCHER_OPEN_SUCCESS: ${mode}`;
}
function toggleWith(mode: string): string {
if (!mode)
return "LAUNCHER_TOGGLE_FAILED: No mode specified";
PopoutService.toggleDankLauncherV2WithMode(mode);
return `LAUNCHER_TOGGLE_SUCCESS: ${mode}`;
}
function openQuery(query: string): string {
PopoutService.openDankLauncherV2WithQuery(query);
return "LAUNCHER_OPEN_QUERY_SUCCESS";
}
function toggleQuery(query: string): string {
PopoutService.toggleDankLauncherV2();
return "LAUNCHER_TOGGLE_QUERY_SUCCESS";
}
target: "launcher"
}
// ! spotlight and launcher should be synonymous for backwards compat
IpcHandler {
function open(): string {
PopoutService.openDankLauncherV2();
return "SPOTLIGHT_OPEN_SUCCESS";
}
function close(): string {
PopoutService.closeDankLauncherV2();
return "SPOTLIGHT_CLOSE_SUCCESS";
}
function toggle(): string {
PopoutService.toggleDankLauncherV2();
return "SPOTLIGHT_TOGGLE_SUCCESS";
}
function openWith(mode: string): string {
if (!mode)
return "SPOTLIGHT_OPEN_FAILED: No mode specified";
PopoutService.openDankLauncherV2WithMode(mode);
return `SPOTLIGHT_OPEN_SUCCESS: ${mode}`;
}
function toggleWith(mode: string): string {
if (!mode)
return "SPOTLIGHT_TOGGLE_FAILED: No mode specified";
PopoutService.toggleDankLauncherV2WithMode(mode);
return `SPOTLIGHT_TOGGLE_SUCCESS: ${mode}`;
}
function openQuery(query: string): string {
PopoutService.openDankLauncherV2WithQuery(query);
return "SPOTLIGHT_OPEN_QUERY_SUCCESS";
}
function toggleQuery(query: string): string {
PopoutService.toggleDankLauncherV2();
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS";
}
target: "spotlight"
}
IpcHandler {
function open(): string {
FirstLaunchService.showWelcome();

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -44,26 +44,28 @@ Item {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
iconName: "history"
iconSize: Theme.iconSize - 4
iconColor: header.activeTab === "recents" ? Theme.primary : Theme.surfaceText
onClicked: tabChanged("recents")
}
DankActionButton {
iconName: "push_pin"
iconSize: Theme.iconSize - 4
iconColor: header.activeTab === "saved" ? Theme.primary : Theme.surfaceText
opacity: header.pinnedCount > 0 ? 1 : 0
enabled: header.pinnedCount > 0
visible: header.pinnedCount > 0
tooltipText: I18n.tr("Saved")
onClicked: tabChanged("saved")
}
DankActionButton {
iconName: "history"
iconSize: Theme.iconSize - 4
iconColor: header.activeTab === "recents" ? Theme.primary : Theme.surfaceText
tooltipText: I18n.tr("History")
onClicked: tabChanged("recents")
}
DankActionButton {
iconName: "info"
iconSize: Theme.iconSize - 4
iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
tooltipText: I18n.tr("Keyboard Shortcuts")
onClicked: keyboardHintsToggled()
}
@@ -71,6 +73,7 @@ Item {
iconName: "delete_sweep"
iconSize: Theme.iconSize
iconColor: Theme.surfaceText
tooltipText: I18n.tr("Clear All")
onClicked: clearAllClicked()
}

View File

@@ -82,14 +82,13 @@ DankModal {
filtered = internalEntries;
} else {
const lowerQuery = query.toLowerCase();
filtered = internalEntries.filter(entry =>
entry.preview.toLowerCase().includes(lowerQuery)
);
filtered = internalEntries.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
}
// Sort: pinned first, then by ID descending
filtered.sort((a, b) => {
if (a.pinned !== b.pinned) return b.pinned ? 1 : -1;
if (a.pinned !== b.pinned)
return b.pinned ? 1 : -1;
return b.id - a.id;
});
@@ -193,24 +192,19 @@ DankModal {
}
function deletePinnedEntry(entry) {
clearConfirmDialog.show(
I18n.tr("Delete Saved Item?"),
I18n.tr("This will permanently remove this saved clipboard item. This action cannot be undone."),
function () {
DMSService.sendRequest("clipboard.deleteEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
console.warn("ClipboardHistoryModal: Failed to delete entry:", response.error);
return;
}
internalEntries = internalEntries.filter(e => e.id !== entry.id);
updateFilteredModel();
ToastService.showInfo(I18n.tr("Saved item deleted"));
});
},
function () {}
);
clearConfirmDialog.show(I18n.tr("Delete Saved Item?"), I18n.tr("This will permanently remove this saved clipboard item. This action cannot be undone."), function () {
DMSService.sendRequest("clipboard.deleteEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
console.warn("ClipboardHistoryModal: Failed to delete entry:", response.error);
return;
}
internalEntries = internalEntries.filter(e => e.id !== entry.id);
updateFilteredModel();
ToastService.showInfo(I18n.tr("Saved item deleted"));
});
}, function () {});
}
function pinEntry(entry) {
@@ -226,7 +220,9 @@ DankModal {
return;
}
DMSService.sendRequest("clipboard.pinEntry", { "id": entry.id }, function (response) {
DMSService.sendRequest("clipboard.pinEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
ToastService.showError(I18n.tr("Failed to pin entry"));
return;
@@ -238,7 +234,9 @@ DankModal {
}
function unpinEntry(entry) {
DMSService.sendRequest("clipboard.unpinEntry", { "id": entry.id }, function (response) {
DMSService.sendRequest("clipboard.unpinEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
ToastService.showError(I18n.tr("Failed to unpin entry"));
return;
@@ -250,27 +248,20 @@ DankModal {
function clearAll() {
const hasPinned = pinnedCount > 0;
const message = hasPinned
? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(pinnedCount)
: I18n.tr("This will permanently delete all clipboard history.");
const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
clearConfirmDialog.show(
I18n.tr("Clear History?"),
message,
function () {
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
if (response.error) {
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
return;
}
refreshClipboard();
if (hasPinned) {
ToastService.showInfo(I18n.tr("History cleared. %1 pinned entries kept.").arg(pinnedCount));
}
});
},
function () {}
);
clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
if (response.error) {
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
return;
}
refreshClipboard();
if (hasPinned) {
ToastService.showInfo(I18n.tr("History cleared. %1 pinned entries kept.").arg(pinnedCount));
}
});
}, function () {});
}
function getEntryPreview(entry) {

View 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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,361 @@
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
property bool _windowEnabled: true
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, mode) {
contentVisible = true;
spotlightContent.searchField.forceActiveFocus();
if (spotlightContent.searchField) {
spotlightContent.searchField.text = query;
}
if (spotlightContent.controller) {
var targetMode = mode || "all";
spotlightContent.controller.searchMode = targetMode;
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();
}
function showWithMode(mode) {
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("", mode);
}
function toggleWithMode(mode) {
if (spotlightOpen) {
hide();
} else {
showWithMode(mode);
}
}
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 (Quickshell.screens.length === 0)
return;
const screen = launcherWindow.screen;
const screenName = screen?.name;
let needsReset = !screen || !screenName;
if (!needsReset) {
needsReset = true;
for (let i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name === screenName) {
needsReset = false;
break;
}
}
}
if (!needsReset)
return;
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
if (!newScreen)
return;
root._windowEnabled = false;
launcherWindow.screen = newScreen;
Qt.callLater(() => {
root._windowEnabled = true;
});
}
}
PanelWindow {
id: launcherWindow
visible: root._windowEnabled
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "dms:launcher"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
}
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;
}
}
}
}
}

View File

@@ -0,0 +1,93 @@
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)
readonly property string iconValue: {
if (!item)
return "";
switch (item.iconType) {
case "material":
case "nerd":
return "material:" + (item.icon || "apps");
case "unicode":
return "unicode:" + (item.icon || "");
case "composite":
return item.iconFull || "";
case "image":
default:
return item.icon || "";
}
}
readonly property int computedIconSize: Math.min(48, Math.max(32, width * 0.45))
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
AppIconRenderer {
width: root.computedIconSize
height: root.computedIconSize
anchors.horizontalCenter: parent.horizontalCenter
iconValue: root.iconValue
iconSize: root.computedIconSize
fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
iconColor: root.isSelected ? Theme.primary : Theme.surfaceText
materialIconSizeAdjustment: root.computedIconSize * 0.3
}
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;
}
}
}
}

View File

@@ -0,0 +1,810 @@
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 string viewModeContext: "spotlight"
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
viewModeContext: root.viewModeContext
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()
}
}
}
}
}
}

View 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);
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,142 @@
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)
readonly property string iconValue: {
if (!item)
return "";
switch (item.iconType) {
case "material":
case "nerd":
return "material:" + (item.icon || "apps");
case "unicode":
return "unicode:" + (item.icon || "");
case "composite":
return item.iconFull || "";
case "image":
default:
return item.icon || "";
}
}
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
AppIconRenderer {
width: 36
height: 36
anchors.verticalCenter: parent.verticalCenter
iconValue: root.iconValue
iconSize: 36
fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
materialIconSizeAdjustment: 12
}
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;
}
}
}
}

View 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);
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);
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);
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)
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");
}
}
}
}
}
}

View 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
}

View 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)
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);
}
}
}
}
}
}
}

View 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
}
}

View File

@@ -0,0 +1,136 @@
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 iconValue: {
if (!item)
return "";
var data = item.data;
if (data?.imageUrl)
return "image:" + data.imageUrl;
if (data?.imagePath)
return "image:" + data.imagePath;
if (data?.path && isImageFile(data.path))
return "image:" + data.path;
switch (item.iconType) {
case "material":
case "nerd":
return "material:" + (item.icon || "image");
case "unicode":
return "unicode:" + (item.icon || "");
case "composite":
return item.iconFull || "";
case "image":
default:
return item.icon || "";
}
}
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
AppIconRenderer {
anchors.fill: parent
iconValue: root.iconValue
iconSize: Math.min(parent.width, parent.height)
fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
materialIconSizeAdjustment: iconSize * 0.3
}
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;
}
}
}

View File

@@ -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
}

View File

@@ -1,237 +0,0 @@
import QtQuick
import Quickshell.Io
import qs.Services
Item {
id: controller
property string searchQuery: ""
property alias model: fileModel
property int selectedIndex: 0
property bool keyboardNavigationActive: false
property bool isSearching: false
property int totalResults: 0
property string searchField: "filename"
signal searchCompleted
ListModel {
id: fileModel
}
function performSearch() {
if (!DSearchService.dsearchAvailable) {
model.clear()
totalResults = 0
isSearching = false
return
}
if (searchQuery.length === 0) {
model.clear()
totalResults = 0
isSearching = false
return
}
isSearching = true
const params = {
"limit": 50,
"fuzzy": true,
"sort": "score",
"desc": true
}
if (searchField && searchField !== "all") {
params.field = searchField
}
DSearchService.search(searchQuery, params, response => {
if (response.error) {
model.clear()
totalResults = 0
isSearching = false
return
}
if (response.result) {
updateModel(response.result)
}
isSearching = false
searchCompleted()
})
}
function updateModel(result) {
model.clear()
totalResults = result.total_hits || 0
selectedIndex = 0
keyboardNavigationActive = true
if (!result.hits || result.hits.length === 0) {
selectedIndex = -1
keyboardNavigationActive = false
return
}
for (var i = 0; i < result.hits.length; i++) {
const hit = result.hits[i]
const filePath = hit.id || ""
const fileName = getFileName(filePath)
const fileExt = getFileExtension(fileName)
const fileType = determineFileType(fileName, filePath)
const dirPath = getDirPath(filePath)
model.append({
"filePath": filePath,
"fileName": fileName,
"fileExtension": fileExt,
"fileType": fileType,
"dirPath": dirPath,
"score": hit.score || 0
})
}
}
function getFileName(path) {
const parts = path.split('/')
return parts[parts.length - 1] || path
}
function getFileExtension(fileName) {
const parts = fileName.split('.')
if (parts.length > 1) {
return parts[parts.length - 1].toLowerCase()
}
return ""
}
function getDirPath(path) {
const lastSlash = path.lastIndexOf('/')
if (lastSlash > 0) {
return path.substring(0, lastSlash)
}
return ""
}
function determineFileType(fileName, filePath) {
const ext = getFileExtension(fileName)
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"]
if (imageExts.includes(ext)) {
return "image"
}
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"]
if (videoExts.includes(ext)) {
return "video"
}
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"]
if (audioExts.includes(ext)) {
return "audio"
}
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"]
if (codeExts.includes(ext)) {
return "code"
}
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"]
if (docExts.includes(ext)) {
return "document"
}
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"]
if (archiveExts.includes(ext)) {
return "archive"
}
if (!ext || fileName.indexOf('.') === -1) {
return "binary"
}
return "file"
}
function selectNext() {
if (model.count === 0) {
return
}
keyboardNavigationActive = true
selectedIndex = Math.min(selectedIndex + 1, model.count - 1)
}
function selectPrevious() {
if (model.count === 0) {
return
}
keyboardNavigationActive = true
selectedIndex = Math.max(selectedIndex - 1, 0)
}
signal fileOpened
function openFile(filePath) {
if (!filePath || filePath.length === 0) {
return
}
let url = filePath
if (!url.startsWith("file://")) {
url = "file://" + filePath
}
Qt.openUrlExternally(url)
fileOpened()
}
function openFolder(filePath) {
if (!filePath || filePath.length === 0) {
return
}
const lastSlash = filePath.lastIndexOf('/')
if (lastSlash <= 0) {
return
}
const dirPath = filePath.substring(0, lastSlash)
let url = dirPath
if (!url.startsWith("file://")) {
url = "file://" + dirPath
}
Qt.openUrlExternally(url)
fileOpened()
}
function openSelected() {
if (model.count === 0 || selectedIndex < 0 || selectedIndex >= model.count) {
return
}
const item = model.get(selectedIndex)
if (item && item.filePath) {
openFile(item.filePath)
}
}
function reset() {
searchQuery = ""
model.clear()
selectedIndex = -1
keyboardNavigationActive = false
isSearching = false
totalResults = 0
}
onSearchQueryChanged: {
performSearch()
}
onSearchFieldChanged: {
performSearch()
}
}

View File

@@ -1,155 +0,0 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: entry
required property string filePath
required property string fileName
required property string fileExtension
required property string fileType
required property string dirPath
required property bool isSelected
required property int itemIndex
signal clicked()
readonly property int iconSize: 40
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingL
Item {
width: iconSize
height: iconSize
anchors.verticalCenter: parent.verticalCenter
Image {
id: imagePreview
anchors.fill: parent
source: fileType === "image" ? `file://${filePath}` : ""
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
asynchronous: true
visible: fileType === "image" && status === Image.Ready
sourceSize.width: 128
sourceSize.height: 128
}
MultiEffect {
anchors.fill: parent
source: imagePreview
maskEnabled: true
maskSource: imageMask
visible: fileType === "image" && imagePreview.status === Image.Ready
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: imageMask
width: iconSize
height: iconSize
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: getFileTypeColor()
visible: fileType !== "image" || imagePreview.status !== Image.Ready
StyledText {
anchors.centerIn: parent
text: getFileIconText()
font.pixelSize: fileExtension.length > 0 ? (fileExtension.length > 3 ? Theme.fontSizeSmall - 2 : Theme.fontSizeSmall) : Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Bold
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - iconSize - Theme.spacingL
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: fileName
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideMiddle
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
width: parent.width
text: dirPath
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideMiddle
maximumLineCount: 1
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: entry.clicked()
}
function getFileTypeColor() {
switch (fileType) {
case "code":
return Theme.codeFileColor || Theme.primarySelected
case "document":
return Theme.docFileColor || Theme.secondarySelected
case "video":
return Theme.videoFileColor || Theme.tertiarySelected
case "audio":
return Theme.audioFileColor || Theme.errorSelected
case "archive":
return Theme.archiveFileColor || Theme.warningSelected
case "binary":
return Theme.binaryFileColor || Theme.surfaceDim
default:
return Theme.surfaceLight
}
}
function getFileIconText() {
if (fileType === "binary") {
return "bin"
}
if (fileExtension.length > 0) {
return fileExtension
}
return fileName.charAt(0).toUpperCase()
}
}

View File

@@ -1,269 +0,0 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: resultsContainer
property var fileSearchController: null
function resetScroll() {
filesList.contentY = 0;
}
color: "transparent"
clip: true
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 32
z: 100
visible: filesList.contentHeight > filesList.height && (filesList.currentIndex < filesList.count - 1 || filesList.contentY < filesList.contentHeight - filesList.height - 1)
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 1.0
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
}
}
}
DankListView {
id: filesList
property int itemHeight: 60
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: fileSearchController ? fileSearchController.keyboardNavigationActive : false
signal keyboardNavigationReset
signal itemClicked(int index)
signal itemRightClicked(int index)
function ensureVisible(index) {
if (index < 0 || index >= count)
return;
const itemY = index * (itemHeight + itemSpacing);
const itemBottom = itemY + itemHeight;
const fadeHeight = 32;
const isLastItem = index === count - 1;
if (itemY < contentY)
contentY = itemY;
else if (itemBottom > contentY + height - (isLastItem ? 0 : fadeHeight))
contentY = Math.min(itemBottom - height + (isLastItem ? 0 : fadeHeight), contentHeight - height);
}
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: 1
model: fileSearchController ? fileSearchController.model : null
currentIndex: fileSearchController ? fileSearchController.selectedIndex : -1
clip: true
spacing: itemSpacing
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex);
}
onItemClicked: function (index) {
if (fileSearchController) {
const item = fileSearchController.model.get(index);
fileSearchController.openFile(item.filePath);
}
}
onItemRightClicked: function (index) {
if (fileSearchController) {
const item = fileSearchController.model.get(index);
fileSearchController.openFolder(item.filePath);
}
}
onKeyboardNavigationReset: {
if (fileSearchController)
fileSearchController.keyboardNavigationActive = false;
}
delegate: Rectangle {
required property int index
required property string filePath
required property string fileName
required property string fileExtension
required property string fileType
required property string dirPath
width: ListView.view.width
height: filesList.itemHeight
radius: Theme.cornerRadius
color: ListView.isCurrentItem ? Theme.widgetBaseHoverColor : fileMouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingL
Item {
width: 40
height: 40
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: iconBackground
anchors.fill: parent
radius: width / 2
color: Theme.surfaceLight
visible: fileType !== "image"
DankNFIcon {
id: nerdIcon
anchors.centerIn: parent
name: {
const lowerName = fileName.toLowerCase();
if (lowerName.startsWith("dockerfile"))
return "docker";
if (lowerName.startsWith("makefile"))
return "makefile";
if (lowerName.startsWith("license"))
return "license";
if (lowerName.startsWith("readme"))
return "readme";
return fileExtension.toLowerCase();
}
size: Theme.fontSizeXLarge
color: Theme.surfaceText
}
StyledText {
anchors.centerIn: parent
text: fileExtension ? (fileExtension.length > 4 ? fileExtension.substring(0, 4) : fileExtension) : "?"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Bold
visible: !nerdIcon.visible
}
}
Loader {
anchors.fill: parent
active: fileType === "image"
sourceComponent: Image {
anchors.fill: parent
source: "file://" + filePath
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
maskSource: ShaderEffectSource {
sourceItem: Rectangle {
width: 40
height: 40
radius: 20
}
}
}
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 40 - Theme.spacingL
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: fileName || ""
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideMiddle
maximumLineCount: 1
}
StyledText {
width: parent.width
text: dirPath || ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideMiddle
maximumLineCount: 1
}
}
}
MouseArea {
id: fileMouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
z: 10
onEntered: {
if (filesList.hoverUpdatesSelection && !filesList.keyboardNavigationActive)
filesList.currentIndex = index;
}
onPositionChanged: {
filesList.keyboardNavigationReset();
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
filesList.itemClicked(index);
} else if (mouse.button === Qt.RightButton) {
filesList.itemRightClicked(index);
}
}
}
}
}
Item {
anchors.fill: parent
visible: !fileSearchController || !fileSearchController.model || fileSearchController.model.count === 0
StyledText {
property string displayText: {
if (!fileSearchController) {
return "";
}
if (!DSearchService.dsearchAvailable) {
return I18n.tr("DankSearch not available");
}
if (fileSearchController.isSearching) {
return I18n.tr("Searching...");
}
if (fileSearchController.searchQuery.length === 0) {
return I18n.tr("Enter a search query");
}
if (!fileSearchController.model || fileSearchController.model.count === 0) {
return I18n.tr("No files found");
}
return "";
}
text: displayText
anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: displayText.length > 0
}
}
}

View File

@@ -1,864 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modals.Spotlight
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
Item {
id: spotlightKeyHandler
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property alias appLauncher: appLauncher
property alias searchField: searchField
property alias fileSearchController: fileSearchController
property alias resultsView: resultsView
property var parentModal: null
property string searchMode: "apps"
property bool usePopupContextMenu: false
property bool editMode: false
property var editingApp: null
property string editAppId: ""
function resetScroll() {
if (searchMode === "apps") {
resultsView.resetScroll();
} else {
fileSearchResults.resetScroll();
}
}
function updateSearchMode() {
if (searchField.text.startsWith("/")) {
if (searchMode !== "files") {
searchMode = "files";
}
const query = searchField.text.substring(1);
fileSearchController.searchQuery = query;
} else {
if (searchMode !== "apps") {
searchMode = "apps";
fileSearchController.reset();
appLauncher.searchQuery = searchField.text;
}
}
}
function openEditMode(app) {
if (!app)
return;
editingApp = app;
editAppId = app.id || app.execString || app.exec || "";
const 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() {
const 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();
}
onSearchModeChanged: {
if (searchMode === "files") {
appLauncher.keyboardNavigationActive = false;
} else {
fileSearchController.keyboardNavigationActive = false;
}
}
anchors.fill: parent
focus: true
clip: false
Keys.onPressed: event => {
if (editMode) {
if (event.key === Qt.Key_Escape) {
closeEditMode();
event.accepted = true;
}
return;
}
if (event.key === Qt.Key_Escape) {
if (parentModal)
parentModal.hide();
event.accepted = true;
} else if (event.key === Qt.Key_Down) {
if (searchMode === "apps") {
appLauncher.selectNext();
} else {
fileSearchController.selectNext();
}
event.accepted = true;
} else if (event.key === Qt.Key_Up) {
if (searchMode === "apps") {
appLauncher.selectPrevious();
} else {
fileSearchController.selectPrevious();
}
event.accepted = true;
} else if (event.key === Qt.Key_Right && searchMode === "apps" && appLauncher.viewMode === "grid") {
I18n.isRtl ? appLauncher.selectPreviousInRow() : appLauncher.selectNextInRow();
event.accepted = true;
} else if (event.key === Qt.Key_Left && searchMode === "apps" && appLauncher.viewMode === "grid") {
I18n.isRtl ? appLauncher.selectNextInRow() : appLauncher.selectPreviousInRow();
event.accepted = true;
} else if (event.key == Qt.Key_J && event.modifiers & Qt.ControlModifier) {
if (searchMode === "apps") {
appLauncher.selectNext();
} else {
fileSearchController.selectNext();
}
event.accepted = true;
} else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) {
if (searchMode === "apps") {
appLauncher.selectPrevious();
} else {
fileSearchController.selectPrevious();
}
event.accepted = true;
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
I18n.isRtl ? appLauncher.selectPreviousInRow() : appLauncher.selectNextInRow();
event.accepted = true;
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
I18n.isRtl ? appLauncher.selectNextInRow() : appLauncher.selectPreviousInRow();
event.accepted = true;
} else if (event.key === Qt.Key_Tab) {
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow();
} else {
appLauncher.selectNext();
}
} else {
fileSearchController.selectNext();
}
event.accepted = true;
} else if (event.key === Qt.Key_Backtab) {
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow();
} else {
appLauncher.selectPrevious();
}
} else {
fileSearchController.selectPrevious();
}
event.accepted = true;
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow();
} else {
appLauncher.selectNext();
}
} else {
fileSearchController.selectNext();
}
event.accepted = true;
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow();
} else {
appLauncher.selectPrevious();
}
} else {
fileSearchController.selectPrevious();
}
event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (searchMode === "apps") {
appLauncher.launchSelected();
} else if (searchMode === "files") {
fileSearchController.openSelected();
}
event.accepted = true;
} else if (event.key === Qt.Key_Menu || event.key == Qt.Key_F10) {
if (searchMode === "apps" && appLauncher.model.count > 0) {
const selectedApp = appLauncher.model.get(appLauncher.selectedIndex);
const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item;
if (selectedApp && menu && resultsView) {
const itemPos = resultsView.getSelectedItemPosition();
const contentPos = resultsView.mapToItem(spotlightKeyHandler, itemPos.x, itemPos.y);
menu.show(contentPos.x, contentPos.y, selectedApp, true);
}
}
event.accepted = true;
}
}
AppLauncher {
id: appLauncher
viewMode: SettingsData.spotlightModalViewMode
gridColumns: SettingsData.appLauncherGridColumns
onAppLaunched: () => {
if (parentModal)
parentModal.hide();
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
NiriService.toggleOverview();
}
}
onViewModeSelected: mode => {
SettingsData.set("spotlightModalViewMode", mode);
}
}
FileSearchController {
id: fileSearchController
onFileOpened: () => {
if (parentModal)
parentModal.hide();
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
NiriService.toggleOverview();
}
}
}
SpotlightContextMenuPopup {
id: popupContextMenu
parent: spotlightKeyHandler
appLauncher: spotlightKeyHandler.appLauncher
parentHandler: spotlightKeyHandler
searchField: spotlightKeyHandler.searchField
visible: false
z: 1000
}
MouseArea {
anchors.fill: parent
visible: usePopupContextMenu && popupContextMenu.visible
hoverEnabled: true
z: 999
onClicked: popupContextMenu.hide()
}
Loader {
id: layerContextMenuLoader
active: !spotlightKeyHandler.usePopupContextMenu
asynchronous: false
sourceComponent: Component {
SpotlightContextMenu {
appLauncher: spotlightKeyHandler.appLauncher
parentHandler: spotlightKeyHandler
parentModal: spotlightKeyHandler.parentModal
}
}
}
Connections {
target: parentModal
function onSpotlightOpenChanged() {
if (parentModal && !parentModal.spotlightOpen) {
if (layerContextMenuLoader.item)
layerContextMenuLoader.item.hide();
popupContextMenu.hide();
if (editMode)
closeEditMode();
}
}
enabled: parentModal !== null
}
Connections {
target: popupContextMenu
function onEditAppRequested(app) {
spotlightKeyHandler.openEditMode(app);
}
}
Connections {
target: layerContextMenuLoader.item
function onEditAppRequested(app) {
spotlightKeyHandler.openEditMode(app);
}
enabled: layerContextMenuLoader.item !== null
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
clip: false
visible: !editMode
Item {
id: searchRow
width: parent.width - Theme.spacingS * 2
height: 56
anchors.horizontalCenter: parent.horizontalCenter
DankTextField {
id: searchField
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
focusedBorderColor: Theme.primary
leftIconName: searchMode === "files" ? "folder" : "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
enabled: parentModal ? parentModal.spotlightOpen : true
placeholderText: ""
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
ignoreTabKeys: true
keyForwardTargets: [spotlightKeyHandler]
onTextChanged: {
if (searchMode === "apps")
appLauncher.searchQuery = text;
}
onTextEdited: updateSearchMode()
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
if (parentModal)
parentModal.hide();
event.accepted = true;
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
if (searchMode === "apps") {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
appLauncher.launchSelected();
else if (appLauncher.model.count > 0)
appLauncher.launchApp(appLauncher.model.get(0));
} else if (searchMode === "files") {
if (fileSearchController.model.count > 0)
fileSearchController.openSelected();
}
event.accepted = true;
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
event.accepted = false;
}
}
}
Item {
id: buttonsContainer
width: viewModeButtons.visible ? viewModeButtons.width : (fileSearchButtons.visible ? fileSearchButtons.width : 0)
height: 36
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
Row {
id: viewModeButtons
spacing: Theme.spacingXS
visible: searchMode === "apps"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 36
height: 36
radius: Theme.cornerRadius
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "view_list"
size: 18
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: listViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: appLauncher.setViewMode("list")
}
}
Rectangle {
width: 36
height: 36
radius: Theme.cornerRadius
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "grid_view"
size: 18
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: gridViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: appLauncher.setViewMode("grid")
}
}
}
Row {
id: fileSearchButtons
spacing: Theme.spacingXS
visible: searchMode === "files"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: filenameFilterButton
width: 36
height: 36
radius: Theme.cornerRadius
color: fileSearchController.searchField === "filename" ? Theme.primaryHover : filenameFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "title"
size: 18
color: fileSearchController.searchField === "filename" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: filenameFilterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: fileSearchController.searchField = "filename"
onEntered: {
filenameTooltipLoader.active = true;
Qt.callLater(() => {
if (filenameTooltipLoader.item) {
const p = mapToItem(null, width / 2, height + Theme.spacingXS);
filenameTooltipLoader.item.show(I18n.tr("Search filenames"), p.x, p.y, null);
}
});
}
onExited: {
if (filenameTooltipLoader.item)
filenameTooltipLoader.item.hide();
filenameTooltipLoader.active = false;
}
}
}
Rectangle {
id: contentFilterButton
width: 36
height: 36
radius: Theme.cornerRadius
color: fileSearchController.searchField === "body" ? Theme.primaryHover : contentFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "description"
size: 18
color: fileSearchController.searchField === "body" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: contentFilterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: fileSearchController.searchField = "body"
onEntered: {
contentTooltipLoader.active = true;
Qt.callLater(() => {
if (contentTooltipLoader.item) {
const p = mapToItem(null, width / 2, height + Theme.spacingXS);
contentTooltipLoader.item.show(I18n.tr("Search file contents"), p.x, p.y, null);
}
});
}
onExited: {
if (contentTooltipLoader.item)
contentTooltipLoader.item.hide();
contentTooltipLoader.active = false;
}
}
}
}
}
}
Item {
width: parent.width
height: parent.height - y
opacity: parentModal?.isClosing ? 0 : 1
SpotlightResults {
id: resultsView
anchors.fill: parent
appLauncher: spotlightKeyHandler.appLauncher
visible: searchMode === "apps"
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item;
if (menu?.show) {
const isPopup = menu.contentItem !== undefined;
if (isPopup) {
const localPos = popupContextMenu.parent.mapFromItem(null, mouseX, mouseY);
menu.show(localPos.x, localPos.y, modelData, false);
} else {
menu.show(mouseX, mouseY, modelData, false);
}
}
}
}
FileSearchResults {
id: fileSearchResults
anchors.fill: parent
fileSearchController: spotlightKeyHandler.fileSearchController
visible: searchMode === "files"
}
}
}
FocusScope {
id: editView
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: editMode
focus: editMode
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Escape:
closeEditMode();
event.accepted = true;
return;
case Qt.Key_Return:
case Qt.Key_Enter:
if (event.modifiers & Qt.ControlModifier) {
saveAppOverride();
event.accepted = true;
}
return;
case Qt.Key_S:
if (event.modifiers & Qt.ControlModifier) {
saveAppOverride();
event.accepted = true;
}
return;
case Qt.Key_R:
if ((event.modifiers & Qt.ControlModifier) && SessionData.getAppOverride(editAppId) !== null) {
resetAppOverride();
event.accepted = true;
}
return;
}
}
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
height: 44
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
height: 44
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
height: 44
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
height: 44
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
height: 44
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()
}
}
}
}
}
Loader {
id: filenameTooltipLoader
active: false
sourceComponent: DankTooltip {}
}
Loader {
id: contentTooltipLoader
active: false
sourceComponent: DankTooltip {}
}
}

View File

@@ -1,119 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Modals.Spotlight
PanelWindow {
id: root
WlrLayershell.namespace: "dms:spotlight-context-menu"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
property var appLauncher: null
property var parentHandler: null
property var parentModal: null
property real menuPositionX: 0
property real menuPositionY: 0
signal editAppRequested(var app)
readonly property real shadowBuffer: 5
screen: parentModal?.effectiveScreen
function show(x, y, app, fromKeyboard) {
fromKeyboard = fromKeyboard || false;
menuContent.currentApp = app;
let screenX = x;
let screenY = y;
if (parentModal) {
if (fromKeyboard) {
screenX = x + parentModal.alignedX;
screenY = y + parentModal.alignedY;
} else {
screenX = x + (parentModal.alignedX - shadowBuffer);
screenY = y + (parentModal.alignedY - shadowBuffer);
}
}
menuPositionX = screenX;
menuPositionY = screenY;
menuContent.selectedMenuIndex = fromKeyboard ? 0 : -1;
menuContent.keyboardNavigation = true;
visible = true;
if (parentHandler) {
parentHandler.enabled = false;
}
Qt.callLater(() => {
menuContent.keyboardHandler.forceActiveFocus();
});
}
function hide() {
if (parentHandler) {
parentHandler.enabled = true;
}
visible = false;
}
visible: false
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
onVisibleChanged: {
if (!visible && parentHandler) {
parentHandler.enabled = true;
}
}
SpotlightContextMenuContent {
id: menuContent
x: {
const left = 10;
const right = root.width - width - 10;
const want = menuPositionX;
return Math.max(left, Math.min(right, want));
}
y: {
const top = 10;
const bottom = root.height - height - 10;
const want = menuPositionY;
return Math.max(top, Math.min(bottom, want));
}
appLauncher: root.appLauncher
opacity: root.visible ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
onHideRequested: root.hide()
onEditAppRequested: app => root.editAppRequested(app)
}
MouseArea {
anchors.fill: parent
z: -1
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: root.hide()
}
}

View File

@@ -1,411 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var currentApp: null
property var appLauncher: null
property int selectedMenuIndex: 0
property bool keyboardNavigation: false
signal hideRequested
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
readonly property var actualItem: (currentApp && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
function getPluginContextMenuActions() {
if (!currentApp || !currentApp.isPlugin || !actualItem)
return [];
const pluginId = appLauncher.getPluginIdForItem(actualItem);
if (!pluginId) {
console.log("[ContextMenu] No pluginId found for item:", JSON.stringify(actualItem.categories));
return [];
}
const instance = PluginService.pluginInstances[pluginId];
if (!instance) {
console.log("[ContextMenu] No instance for pluginId:", pluginId);
return [];
}
if (typeof instance.getContextMenuActions !== "function") {
console.log("[ContextMenu] Instance has no getContextMenuActions:", pluginId);
return [];
}
const actions = instance.getContextMenuActions(actualItem);
if (!Array.isArray(actions))
return [];
return actions;
}
function executePluginAction(actionData) {
if (!currentApp || !actualItem)
return;
const pluginId = appLauncher.getPluginIdForItem(actualItem);
if (!pluginId)
return;
const instance = PluginService.pluginInstances[pluginId];
if (!instance)
return;
if (typeof actionData === "function") {
actionData();
} else if (typeof instance.executeContextMenuAction === "function") {
instance.executeContextMenuAction(actualItem, actionData);
}
if (appLauncher)
appLauncher.updateFilteredModel();
hideRequested();
}
readonly property bool isRegularApp: desktopEntry && !currentApp?.isPlugin && !currentApp?.isCore && !currentApp?.isAction && !currentApp?.isBuiltInLauncher
signal editAppRequested(var app)
function hideCurrentApp() {
if (!desktopEntry)
return;
const appId = desktopEntry.id || desktopEntry.execString || "";
SessionData.hideApp(appId);
if (appLauncher)
appLauncher.updateFilteredModel();
hideRequested();
}
function editCurrentApp() {
if (!desktopEntry)
return;
editAppRequested(desktopEntry);
hideRequested();
}
readonly property var menuItems: {
const items = [];
if (currentApp && currentApp.isPlugin) {
const pluginActions = getPluginContextMenuActions();
for (let i = 0; i < pluginActions.length; i++) {
const act = pluginActions[i];
items.push({
type: "item",
icon: act.icon || "",
text: act.text || act.name || "",
action: () => executePluginAction(act.action)
});
}
if (items.length === 0) {
items.push({
type: "item",
icon: "content_copy",
text: I18n.tr("Copy"),
action: launchCurrentApp
});
}
return items;
}
const appId = desktopEntry ? (desktopEntry.id || desktopEntry.execString || "") : "";
const isPinned = SessionData.isPinnedApp(appId);
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 (desktopEntry && desktopEntry.actions) {
items.push({
type: "separator"
});
for (let i = 0; i < desktopEntry.actions.length; i++) {
const act = desktopEntry.actions[i];
items.push({
type: "item",
text: act.name || "",
action: () => launchAction(act)
});
}
}
items.push({
type: "separator",
hidden: !desktopEntry || !desktopEntry.actions || desktopEntry.actions.length === 0
});
items.push({
type: "item",
icon: "launch",
text: I18n.tr("Launch"),
action: launchCurrentApp
});
if (SessionService.nvidiaCommand) {
items.push({
type: "separator"
});
items.push({
type: "item",
icon: "memory",
text: I18n.tr("Launch on dGPU"),
action: launchWithNvidia
});
}
return items;
}
readonly property int visibleItemCount: {
let count = 0;
for (let i = 0; i < menuItems.length; i++) {
if (menuItems[i].type === "item" && !menuItems[i].hidden) {
count++;
}
}
return count;
}
function selectNext() {
if (visibleItemCount > 0) {
selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount;
}
}
function selectPrevious() {
if (visibleItemCount > 0) {
selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount;
}
}
function togglePin() {
if (!desktopEntry)
return;
const appId = desktopEntry.id || desktopEntry.execString || "";
if (SessionData.isPinnedApp(appId))
SessionData.removePinnedApp(appId);
else
SessionData.addPinnedApp(appId);
hideRequested();
}
function launchCurrentApp() {
if (currentApp && appLauncher)
appLauncher.launchApp(currentApp);
hideRequested();
}
function launchWithNvidia() {
if (desktopEntry) {
SessionService.launchDesktopEntry(desktopEntry, true);
if (appLauncher && currentApp) {
appLauncher.appLaunched(currentApp);
}
}
hideRequested();
}
function launchAction(action) {
if (desktopEntry) {
SessionService.launchDesktopAction(desktopEntry, action);
if (appLauncher && currentApp) {
appLauncher.appLaunched(currentApp);
}
}
hideRequested();
}
function activateSelected() {
let itemIndex = 0;
for (let i = 0; i < menuItems.length; i++) {
if (menuItems[i].type === "item" && !menuItems[i].hidden) {
if (itemIndex === selectedMenuIndex) {
menuItems[i].action();
return;
}
itemIndex++;
}
}
}
property alias keyboardHandler: keyboardHandler
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
width: implicitWidth
height: implicitHeight
Rectangle {
id: menuContainer
anchors.fill: parent
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
}
Item {
id: keyboardHandler
anchors.fill: parent
focus: keyboardNavigation
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Down:
selectNext();
event.accepted = true;
break;
case Qt.Key_Up:
selectPrevious();
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
activateSelected();
event.accepted = true;
break;
case Qt.Key_Escape:
case Qt.Key_Left:
hideRequested();
event.accepted = true;
break;
}
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Repeater {
model: menuItems
Item {
width: parent.width
height: modelData.type === "separator" ? 5 : 32
visible: !modelData.hidden
property int itemIndex: {
let count = 0;
for (let i = 0; i < index; i++) {
if (menuItems[i].type === "item" && !menuItems[i].hidden) {
count++;
}
}
return count;
}
Rectangle {
visible: 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: modelData.type === "item"
width: parent.width
height: parent.height
radius: Theme.cornerRadius
color: {
if (keyboardNavigation && selectedMenuIndex === itemIndex) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
}
return mouseArea.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: modelData.icon !== undefined && modelData.icon !== ""
name: modelData.icon || ""
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: 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: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
keyboardNavigation = false;
selectedMenuIndex = itemIndex;
}
onClicked: modelData.action()
}
}
}
}
}
}
}
}

View File

@@ -1,90 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modals.Spotlight
Popup {
id: root
property var appLauncher: null
property var parentHandler: null
property var searchField: null
signal editAppRequested(var app)
function show(x, y, app, fromKeyboard) {
fromKeyboard = fromKeyboard || false;
menuContent.currentApp = app;
root.x = x + 4;
root.y = y + 4;
menuContent.selectedMenuIndex = fromKeyboard ? 0 : -1;
menuContent.keyboardNavigation = true;
if (parentHandler) {
parentHandler.enabled = false;
}
open();
}
onOpened: {
Qt.callLater(() => {
menuContent.keyboardHandler.forceActiveFocus();
});
}
function hide() {
if (parentHandler) {
parentHandler.enabled = true;
}
close();
}
width: menuContent.implicitWidth
height: menuContent.implicitHeight
padding: 0
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
modal: true
dim: false
background: Item {}
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: SpotlightContextMenuContent {
id: menuContent
appLauncher: root.appLauncher
onHideRequested: root.hide()
onEditAppRequested: app => root.editAppRequested(app)
}
}

View File

@@ -1,181 +0,0 @@
import QtQuick
import Quickshell.Hyprland
import Quickshell.Io
import qs.Common
import qs.Modals.Common
DankModal {
id: spotlightModal
layerNamespace: "dms:spotlight"
HyprlandFocusGrab {
windows: [spotlightModal.contentWindow]
active: spotlightModal.useHyprlandFocusGrab && spotlightModal.shouldHaveFocus
}
property bool spotlightOpen: false
property alias spotlightContent: spotlightContentInstance
property bool openedFromOverview: false
property bool isClosing: false
function resetContent() {
if (!spotlightContent)
return;
if (spotlightContent.appLauncher)
spotlightContent.appLauncher.reset();
if (spotlightContent.fileSearchController)
spotlightContent.fileSearchController.reset();
if (spotlightContent.resetScroll)
spotlightContent.resetScroll();
if (spotlightContent.searchField)
spotlightContent.searchField.text = "";
spotlightContent.searchMode = "apps";
}
function show() {
openedFromOverview = false;
isClosing = false;
resetContent();
spotlightOpen = true;
open();
Qt.callLater(() => {
if (spotlightContent?.appLauncher)
spotlightContent.appLauncher.ensureInitialized();
if (spotlightContent?.searchField)
spotlightContent.searchField.forceActiveFocus();
});
}
function showWithQuery(query) {
openedFromOverview = false;
isClosing = false;
resetContent();
spotlightOpen = true;
if (spotlightContent?.searchField)
spotlightContent.searchField.text = query;
open();
Qt.callLater(() => {
if (spotlightContent?.appLauncher) {
spotlightContent.appLauncher.ensureInitialized();
spotlightContent.appLauncher.searchQuery = query;
}
if (spotlightContent?.searchField)
spotlightContent.searchField.forceActiveFocus();
});
}
function showWithEditApp(app) {
openedFromOverview = false;
isClosing = false;
resetContent();
spotlightOpen = true;
open();
Qt.callLater(() => {
if (spotlightContent?.appLauncher)
spotlightContent.appLauncher.ensureInitialized();
if (spotlightContent?.openEditMode)
spotlightContent.openEditMode(app);
});
}
function hide() {
openedFromOverview = false;
isClosing = true;
spotlightOpen = false;
close();
}
onDialogClosed: {
isClosing = false;
resetContent();
}
function toggle() {
if (spotlightOpen) {
hide();
} else {
show();
}
}
shouldBeVisible: spotlightOpen
modalWidth: 500
modalHeight: 600
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
keepContentLoaded: true
animationScaleCollapsed: 0.96
animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
animationExitCurve: Theme.expressiveCurves.emphasized
onVisibleChanged: () => {
if (visible && !spotlightOpen) {
show();
}
if (visible && spotlightContent) {
Qt.callLater(() => {
if (spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus();
}
});
}
}
onBackgroundClicked: () => {
return hide();
}
Connections {
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) {
spotlightOpen = false;
}
}
target: ModalManager
}
IpcHandler {
function open(): string {
spotlightModal.show();
return "SPOTLIGHT_OPEN_SUCCESS";
}
function close(): string {
spotlightModal.hide();
return "SPOTLIGHT_CLOSE_SUCCESS";
}
function toggle(): string {
spotlightModal.toggle();
return "SPOTLIGHT_TOGGLE_SUCCESS";
}
function openQuery(query: string): string {
spotlightModal.showWithQuery(query);
return "SPOTLIGHT_OPEN_QUERY_SUCCESS";
}
function toggleQuery(query: string): string {
if (spotlightModal.spotlightOpen) {
spotlightModal.hide();
} else {
spotlightModal.showWithQuery(query);
}
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS";
}
target: "spotlight"
}
SpotlightContent {
id: spotlightContentInstance
parentModal: spotlightModal
}
directContent: spotlightContentInstance
}

View File

@@ -1,267 +0,0 @@
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: resultsContainer
property var appLauncher: null
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function resetScroll() {
resultsList.contentY = 0;
if (gridLoader.item) {
gridLoader.item.contentY = 0;
}
}
function getSelectedItemPosition() {
if (!appLauncher)
return {
x: 0,
y: 0
};
const selectedIndex = appLauncher.selectedIndex;
if (appLauncher.viewMode === "list") {
const itemY = selectedIndex * (resultsList.itemHeight + resultsList.itemSpacing) - resultsList.contentY;
return {
x: resultsList.width / 2,
y: itemY + resultsList.itemHeight / 2
};
} else if (gridLoader.item) {
const grid = gridLoader.item;
const row = Math.floor(selectedIndex / grid.actualColumns);
const col = selectedIndex % grid.actualColumns;
const itemX = col * grid.cellWidth + grid.leftMargin + grid.cellWidth / 2;
const itemY = row * grid.cellHeight - grid.contentY + grid.cellHeight / 2;
return {
x: itemX,
y: itemY
};
}
return {
x: 0,
y: 0
};
}
radius: Theme.cornerRadius
color: "transparent"
clip: true
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 32
z: 100
visible: {
if (!appLauncher)
return false;
const view = appLauncher.viewMode === "list" ? resultsList : (gridLoader.item || resultsList);
const isLastItem = appLauncher.viewMode === "list" ? view.currentIndex >= view.count - 1 : (gridLoader.item ? Math.floor(view.currentIndex / view.actualColumns) >= Math.floor((view.count - 1) / view.actualColumns) : false);
const hasOverflow = view.contentHeight > view.height;
const atBottom = view.contentY >= view.contentHeight - view.height - 1;
return hasOverflow && (!isLastItem || !atBottom);
}
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 1.0
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
}
}
}
DankListView {
id: resultsList
property int itemHeight: 60
property int iconSize: 40
property bool showDescription: true
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return;
const itemY = index * (itemHeight + itemSpacing);
const itemBottom = itemY + itemHeight;
const fadeHeight = 32;
const isLastItem = index === count - 1;
if (itemY < contentY)
contentY = itemY;
else if (itemBottom > contentY + height - (isLastItem ? 0 : fadeHeight))
contentY = Math.min(itemBottom - height + (isLastItem ? 0 : fadeHeight), contentHeight - height);
}
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: 1
visible: appLauncher && appLauncher.viewMode === "list"
model: appLauncher ? appLauncher.model : null
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
clip: true
spacing: itemSpacing
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex);
}
onItemClicked: (index, modelData) => {
if (appLauncher)
appLauncher.launchApp(modelData);
}
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
resultsContainer.itemRightClicked(index, modelData, mouseX, mouseY);
}
onKeyboardNavigationReset: () => {
if (appLauncher)
appLauncher.keyboardNavigationActive = false;
}
delegate: AppLauncherListDelegate {
listView: resultsList
itemHeight: resultsList.itemHeight
iconSize: resultsList.iconSize
showDescription: resultsList.showDescription
hoverUpdatesSelection: resultsList.hoverUpdatesSelection
keyboardNavigationActive: resultsList.keyboardNavigationActive
isCurrentItem: ListView.isCurrentItem
iconMaterialSizeAdjustment: 0
iconUnicodeScale: 0.8
onItemClicked: (idx, modelData) => resultsList.itemClicked(idx, modelData)
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
resultsList.itemRightClicked(idx, modelData, mouseX, mouseY);
}
onKeyboardNavigationReset: resultsList.keyboardNavigationReset
}
}
Loader {
id: gridLoader
property real _lastWidth: 0
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: 1
visible: appLauncher && appLauncher.viewMode === "grid"
active: appLauncher && appLauncher.viewMode === "grid"
asynchronous: false
onLoaded: {
if (item) {
item.appLauncher = Qt.binding(() => resultsContainer.appLauncher);
}
}
onWidthChanged: {
if (visible && Math.abs(width - _lastWidth) > 1) {
_lastWidth = width;
active = false;
Qt.callLater(() => {
active = true;
});
}
}
sourceComponent: Component {
DankGridView {
id: resultsGrid
property var appLauncher: null
property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1
property int columns: appLauncher ? appLauncher.gridColumns : 4
property bool adaptiveColumns: false
property int minCellWidth: 120
property int maxCellWidth: 160
property real iconSizeRatio: 0.55
property int maxIconSize: 48
property int minIconSize: 32
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
property real baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : width / columns
property real baseCellHeight: baseCellWidth + 20
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
property int remainingSpace: width - (actualColumns * cellWidth)
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return;
const itemY = Math.floor(index / actualColumns) * cellHeight;
const itemBottom = itemY + cellHeight;
const fadeHeight = 32;
const isLastRow = Math.floor(index / actualColumns) >= Math.floor((count - 1) / actualColumns);
if (itemY < contentY)
contentY = itemY;
else if (itemBottom > contentY + height - (isLastRow ? 0 : fadeHeight))
contentY = Math.min(itemBottom - height + (isLastRow ? 0 : fadeHeight), contentHeight - height);
}
anchors.fill: parent
model: appLauncher ? appLauncher.model : null
clip: true
cellWidth: baseCellWidth
cellHeight: baseCellHeight
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex);
}
onItemClicked: (index, modelData) => {
if (appLauncher)
appLauncher.launchApp(modelData);
}
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
resultsContainer.itemRightClicked(index, modelData, mouseX, mouseY);
}
onKeyboardNavigationReset: () => {
if (appLauncher)
appLauncher.keyboardNavigationActive = false;
}
delegate: AppLauncherGridDelegate {
gridView: resultsGrid
cellWidth: resultsGrid.cellWidth
cellHeight: resultsGrid.cellHeight
minIconSize: resultsGrid.minIconSize
maxIconSize: resultsGrid.maxIconSize
iconSizeRatio: resultsGrid.iconSizeRatio
hoverUpdatesSelection: resultsGrid.hoverUpdatesSelection
keyboardNavigationActive: resultsGrid.keyboardNavigationActive
currentIndex: resultsGrid.currentIndex
onItemClicked: (idx, modelData) => resultsGrid.itemClicked(idx, modelData)
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
resultsGrid.itemRightClicked(idx, modelData, mouseX, mouseY);
}
onKeyboardNavigationReset: resultsGrid.keyboardNavigationReset
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,456 +0,0 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
Item {
id: root
// DEVELOPER NOTE: This component manages the AppDrawer launcher (accessed via DankBar icon).
// Changes to launcher behavior, especially item rendering, filtering, or model structure,
// likely require corresponding updates in Modals/Spotlight/SpotlightResults.qml and vice versa.
property string searchQuery: ""
property string selectedCategory: I18n.tr("All")
property string viewMode: "list" // "list" or "grid"
property int selectedIndex: 0
property int maxResults: 50
property int gridColumns: 4
property bool debounceSearch: true
property int debounceInterval: 50
property bool keyboardNavigationActive: false
property bool suppressUpdatesWhileLaunching: false
property var categories: []
readonly property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category))
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
property alias model: filteredModel
property var _uniqueApps: []
property bool _initialized: false
property bool _isTriggered: false
property string _triggeredCategory: ""
property bool _updatingFromTrigger: false
signal appLaunched(var app)
signal categorySelected(string category)
signal viewModeSelected(string mode)
function ensureInitialized() {
if (_initialized)
return;
_initialized = true;
updateCategories();
updateFilteredModel();
}
function updateCategories() {
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science");
const result = [I18n.tr("All")];
categories = result.concat(allCategories.filter(cat => cat !== I18n.tr("All")));
}
Connections {
target: PluginService
function onPluginLoaded() {
updateCategories();
}
function onPluginUnloaded() {
updateCategories();
}
function onPluginListUpdated() {
updateCategories();
}
function onRequestLauncherUpdate(pluginId) {
// Only update if we are actually looking at this plugin or in All category
updateFilteredModel();
}
}
Connections {
target: SettingsData
function onSortAppsAlphabeticallyChanged() {
updateFilteredModel();
}
}
Connections {
target: SessionData
function onHiddenAppsChanged() {
updateFilteredModel();
}
function onAppOverridesChanged() {
updateFilteredModel();
}
}
function updateFilteredModel() {
if (suppressUpdatesWhileLaunching) {
suppressUpdatesWhileLaunching = false;
return;
}
filteredModel.clear();
selectedIndex = 0;
keyboardNavigationActive = false;
const triggerResult = checkPluginTriggers(searchQuery);
if (triggerResult.triggered) {
console.log("AppLauncher: Plugin trigger detected:", triggerResult.trigger, "for plugin:", triggerResult.pluginId);
}
let apps = [];
const allCategory = I18n.tr("All");
const emptyTriggerPlugins = typeof PluginService !== "undefined" ? PluginService.getPluginsWithEmptyTrigger() : [];
if (triggerResult.triggered) {
_isTriggered = true;
_triggeredCategory = triggerResult.pluginCategory;
_updatingFromTrigger = true;
selectedCategory = triggerResult.pluginCategory;
_updatingFromTrigger = false;
if (triggerResult.isBuiltIn) {
apps = AppSearchService.getBuiltInLauncherItems(triggerResult.pluginId, triggerResult.query);
} else {
apps = AppSearchService.getPluginItems(triggerResult.pluginCategory, triggerResult.query);
}
} else {
if (_isTriggered) {
_updatingFromTrigger = true;
selectedCategory = allCategory;
_updatingFromTrigger = false;
_isTriggered = false;
_triggeredCategory = "";
}
if (searchQuery.length === 0) {
if (selectedCategory === allCategory) {
let emptyTriggerItems = [];
emptyTriggerPlugins.forEach(pluginId => {
const plugin = PluginService.getLauncherPlugin(pluginId);
const pluginCategory = plugin.name || pluginId;
const items = AppSearchService.getPluginItems(pluginCategory, "");
emptyTriggerItems = emptyTriggerItems.concat(items);
});
const builtInEmptyTrigger = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger();
builtInEmptyTrigger.forEach(pluginId => {
const items = AppSearchService.getBuiltInLauncherItems(pluginId, "");
emptyTriggerItems = emptyTriggerItems.concat(items);
});
const coreItems = AppSearchService.getCoreApps("");
apps = AppSearchService.getVisibleApplications().concat(emptyTriggerItems).concat(coreItems);
} else {
apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults);
const coreItems = AppSearchService.getCoreApps("").filter(app => app.categories.includes(selectedCategory));
apps = apps.concat(coreItems);
}
} else {
if (selectedCategory === allCategory) {
apps = AppSearchService.searchApplications(searchQuery);
let emptyTriggerItems = [];
emptyTriggerPlugins.forEach(pluginId => {
const plugin = PluginService.getLauncherPlugin(pluginId);
const pluginCategory = plugin.name || pluginId;
const items = AppSearchService.getPluginItems(pluginCategory, searchQuery);
emptyTriggerItems = emptyTriggerItems.concat(items);
});
const builtInEmptyTrigger = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger();
builtInEmptyTrigger.forEach(pluginId => {
const items = AppSearchService.getBuiltInLauncherItems(pluginId, searchQuery);
emptyTriggerItems = emptyTriggerItems.concat(items);
});
const coreItems = AppSearchService.getCoreApps(searchQuery);
apps = apps.concat(emptyTriggerItems).concat(coreItems);
} else {
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
if (categoryApps.length > 0) {
const allSearchResults = AppSearchService.searchApplications(searchQuery);
const categoryNames = new Set(categoryApps.map(app => app.name));
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults);
} else {
apps = [];
}
const coreItems = AppSearchService.getCoreApps(searchQuery).filter(app => app.categories.includes(selectedCategory));
apps = apps.concat(coreItems);
}
}
}
if (searchQuery.length === 0) {
if (SettingsData.sortAppsAlphabetically) {
apps = apps.sort((a, b) => {
return (a.name || "").localeCompare(b.name || "");
});
} else {
apps = apps.sort((a, b) => {
const aId = a.id || a.execString || a.exec || "";
const bId = b.id || b.execString || b.exec || "";
const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0;
const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0;
if (aUsage !== bUsage) {
return bUsage - aUsage;
}
return (a.name || "").localeCompare(b.name || "");
});
}
}
const seenNames = new Set();
const uniqueApps = [];
apps.forEach(app => {
if (app) {
const itemKey = app.name + "|" + (app.execString || app.exec || app.action || "");
if (seenNames.has(itemKey)) {
return;
}
seenNames.add(itemKey);
uniqueApps.push(app);
const isPluginItem = app.isCore ? false : (app.action !== undefined);
filteredModel.append({
"name": app.name || "",
"exec": app.execString || app.exec || app.action || "",
"icon": app.icon !== undefined ? String(app.icon) : (isPluginItem ? "" : "application-x-executable"),
"comment": app.comment || "",
"categories": app.categories || [],
"isPlugin": isPluginItem,
"isCore": app.isCore === true,
"isBuiltInLauncher": app.isBuiltInLauncher === true,
"isAction": app.isAction === true,
"appIndex": uniqueApps.length - 1,
"pinned": app._pinned === true
});
}
});
root._uniqueApps = uniqueApps;
}
function selectNext() {
if (filteredModel.count === 0) {
return;
}
keyboardNavigationActive = true;
selectedIndex = viewMode === "grid" ? Math.min(selectedIndex + gridColumns, filteredModel.count - 1) : Math.min(selectedIndex + 1, filteredModel.count - 1);
}
function selectPrevious() {
if (filteredModel.count === 0) {
return;
}
keyboardNavigationActive = true;
selectedIndex = viewMode === "grid" ? Math.max(selectedIndex - gridColumns, 0) : Math.max(selectedIndex - 1, 0);
}
function selectNextInRow() {
if (filteredModel.count === 0 || viewMode !== "grid") {
return;
}
keyboardNavigationActive = true;
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1);
}
function selectPreviousInRow() {
if (filteredModel.count === 0 || viewMode !== "grid") {
return;
}
keyboardNavigationActive = true;
selectedIndex = Math.max(selectedIndex - 1, 0);
}
function launchSelected() {
if (filteredModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredModel.count) {
return;
}
const selectedApp = filteredModel.get(selectedIndex);
launchApp(selectedApp);
}
function launchApp(appData) {
if (!appData || typeof appData.appIndex === "undefined" || appData.appIndex < 0 || appData.appIndex >= _uniqueApps.length)
return;
suppressUpdatesWhileLaunching = true;
const actualApp = _uniqueApps[appData.appIndex];
if (appData.isBuiltInLauncher) {
AppSearchService.executeBuiltInLauncherItem(actualApp);
appLaunched(appData);
return;
}
if (appData.isCore) {
AppSearchService.executeCoreApp(actualApp);
appLaunched(appData);
return;
}
if (appData.isPlugin) {
const pluginId = getPluginIdForItem(actualApp);
if (pluginId) {
AppSearchService.executePluginItem(actualApp, pluginId);
appLaunched(appData);
return;
}
return;
}
if (appData.isAction && actualApp.parentApp && actualApp.actionData) {
SessionService.launchDesktopAction(actualApp.parentApp, actualApp.actionData);
appLaunched(appData);
AppUsageHistoryData.addAppUsage(actualApp.parentApp);
return;
}
SessionService.launchDesktopEntry(actualApp);
appLaunched(appData);
AppUsageHistoryData.addAppUsage(actualApp);
}
function reset() {
suppressUpdatesWhileLaunching = false;
searchQuery = "";
selectedIndex = 0;
setCategory(I18n.tr("All"));
updateFilteredModel();
}
function setCategory(category) {
selectedCategory = category;
categorySelected(category);
}
function setViewMode(mode) {
viewMode = mode;
viewModeSelected(mode);
}
onSearchQueryChanged: {
if (!_initialized)
return;
if (debounceSearch) {
searchDebounceTimer.restart();
} else {
updateFilteredModel();
}
}
onSelectedCategoryChanged: {
if (_updatingFromTrigger || !_initialized)
return;
updateFilteredModel();
}
onAppUsageRankingChanged: {
if (_initialized)
updateFilteredModel();
}
Connections {
target: DesktopEntries
function onApplicationsChanged() {
if (!root._initialized)
return;
root.updateCategories();
root.updateFilteredModel();
}
}
ListModel {
id: filteredModel
}
Timer {
id: searchDebounceTimer
interval: root.debounceInterval
repeat: false
onTriggered: updateFilteredModel()
}
function checkPluginTriggers(query) {
if (!query)
return {
triggered: false,
pluginCategory: "",
query: ""
};
const builtInTriggers = AppSearchService.getBuiltInLauncherTriggers();
for (const trigger in builtInTriggers) {
if (!query.startsWith(trigger))
continue;
const pluginId = builtInTriggers[trigger];
const plugin = AppSearchService.builtInPlugins[pluginId];
if (!plugin)
continue;
return {
triggered: true,
pluginId: pluginId,
pluginCategory: plugin.name,
query: query.substring(trigger.length).trim(),
trigger: trigger,
isBuiltIn: true
};
}
if (typeof PluginService === "undefined")
return {
triggered: false,
pluginCategory: "",
query: ""
};
const triggers = PluginService.getAllPluginTriggers();
for (const trigger in triggers) {
if (!query.startsWith(trigger))
continue;
const pluginId = triggers[trigger];
const plugin = PluginService.getLauncherPlugin(pluginId);
if (!plugin)
continue;
return {
triggered: true,
pluginId: pluginId,
pluginCategory: plugin.name || pluginId,
query: query.substring(trigger.length).trim(),
trigger: trigger,
isBuiltIn: false
};
}
return {
triggered: false,
pluginCategory: "",
query: ""
};
}
function getPluginIdForItem(item) {
if (!item || !item.categories || typeof PluginService === "undefined") {
return null;
}
const launchers = PluginService.getLauncherPlugins();
for (const pluginId in launchers) {
const plugin = launchers[pluginId];
const pluginCategory = plugin.name || pluginId;
let hasCategory = false;
if (Array.isArray(item.categories)) {
hasCategory = item.categories.includes(pluginCategory);
} else if (item.categories && typeof item.categories.count !== "undefined") {
for (let i = 0; i < item.categories.count; i++) {
if (item.categories.get(i) === pluginCategory) {
hasCategory = true;
break;
}
}
}
if (hasCategory) {
return pluginId;
}
}
return null;
}
}

View File

@@ -1,142 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var categories: []
property string selectedCategory: I18n.tr("All")
property bool compact: false
signal categorySelected(string category)
readonly property int maxCompactItems: 8
readonly property int itemHeight: 36
readonly property color selectedBorderColor: "transparent"
readonly property color unselectedBorderColor: "transparent"
function handleCategoryClick(category) {
categorySelected(category)
}
function getButtonWidth(itemCount, containerWidth) {
return itemCount > 0 ? (containerWidth - (itemCount - 1) * Theme.spacingS) / itemCount : 0
}
height: compact ? itemHeight : (itemHeight * 2 + Theme.spacingS)
Row {
visible: compact
width: parent.width
spacing: Theme.spacingS
Repeater {
model: categories ? categories.slice(0, Math.min(categories.length || 0, maxCompactItems)) : []
Rectangle {
property int itemCount: Math.min(categories ? categories.length || 0 : 0, maxCompactItems)
height: root.itemHeight
width: root.getButtonWidth(itemCount, parent.width)
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.handleCategoryClick(modelData)
}
}
}
}
Column {
visible: !compact
width: parent.width
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
Repeater {
model: categories ? categories.slice(0, Math.min(4, categories.length || 0)) : []
Rectangle {
property int itemCount: Math.min(4, categories ? categories.length || 0 : 0)
height: root.itemHeight
width: root.getButtonWidth(itemCount, parent.width)
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.handleCategoryClick(modelData)
}
}
}
}
Row {
width: parent.width
spacing: Theme.spacingS
visible: categories && categories.length > 4
Repeater {
model: categories && categories.length > 4 ? categories.slice(4) : []
Rectangle {
property int itemCount: categories && categories.length > 4 ? categories.length - 4 : 0
height: root.itemHeight
width: root.getButtonWidth(itemCount, parent.width)
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.handleCategoryClick(modelData)
}
}
}
}
}
}

View File

@@ -46,11 +46,24 @@ Item {
function getRealWorkspaces() {
if (CompositorService.isNiri) {
const fallbackWorkspaces = [
{
"id": 1,
"idx": 0,
"name": ""
},
{
"id": 2,
"idx": 1,
"name": ""
}
];
if (!barWindow.screenName || SettingsData.workspaceFollowFocus) {
return NiriService.getCurrentOutputWorkspaceNumbers();
const currentWorkspaces = NiriService.getCurrentOutputWorkspaces();
return currentWorkspaces.length > 0 ? currentWorkspaces : fallbackWorkspaces;
}
const workspaces = NiriService.allWorkspaces.filter(ws => ws.output === barWindow.screenName).map(ws => ws.idx + 1);
return workspaces.length > 0 ? workspaces : [1, 2];
const workspaces = NiriService.allWorkspaces.filter(ws => ws.output === barWindow.screenName);
return workspaces.length > 0 ? workspaces : fallbackWorkspaces;
} else if (CompositorService.isHyprland) {
const workspaces = Hyprland.workspaces?.values || [];
@@ -118,7 +131,7 @@ Item {
return NiriService.getCurrentWorkspaceNumber();
}
const activeWs = NiriService.allWorkspaces.find(ws => ws.output === barWindow.screenName && ws.is_active);
return activeWs ? activeWs.idx + 1 : 1;
return activeWs ? activeWs.idx : 1;
} else if (CompositorService.isHyprland) {
const monitors = Hyprland.monitors?.values || [];
const currentMonitor = monitors.find(monitor => monitor.name === barWindow.screenName);
@@ -151,12 +164,16 @@ Item {
if (CompositorService.isNiri) {
const currentWs = getCurrentWorkspace();
const currentIndex = realWorkspaces.findIndex(ws => ws === currentWs);
const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === currentWs);
const validIndex = currentIndex === -1 ? 0 : currentIndex;
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
if (nextIndex !== validIndex) {
NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1);
const nextWorkspace = realWorkspaces[nextIndex];
if (!nextWorkspace || nextWorkspace.idx === undefined) {
return;
}
NiriService.switchToWorkspace(nextWorkspace.idx);
}
} else if (CompositorService.isHyprland) {
const currentWs = getCurrentWorkspace();

View File

@@ -42,7 +42,7 @@ BasePill {
DankIcon {
name: BatteryService.getBatteryIcon()
size: Theme.barIconSize(battery.barThickness)
size: Theme.barIconSize(battery.barThickness, undefined, battery.barConfig?.noBackground)
color: {
if (!BatteryService.batteryAvailable) {
return Theme.widgetIconColor;
@@ -78,7 +78,7 @@ BasePill {
DankIcon {
name: BatteryService.getBatteryIcon()
size: Theme.barIconSize(battery.barThickness, -4)
size: Theme.barIconSize(battery.barThickness, -4, battery.barConfig?.noBackground)
color: {
if (!BatteryService.batteryAvailable) {
return Theme.widgetIconColor;

View File

@@ -45,7 +45,7 @@ BasePill {
DankIcon {
anchors.centerIn: parent
name: "shift_lock"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: Theme.primary
}
}

View File

@@ -17,7 +17,7 @@ BasePill {
DankIcon {
anchors.centerIn: parent
name: "content_paste"
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: Theme.widgetIconColor
}
}

View File

@@ -18,7 +18,7 @@ BasePill {
DankIcon {
anchors.centerIn: parent
name: "palette"
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: root.isActive ? Theme.primary : Theme.surfaceText
}
}

View File

@@ -29,7 +29,7 @@ BasePill {
property real micAccumulator: 0
property real volumeAccumulator: 0
property real brightnessAccumulator: 0
readonly property real vIconSize: Theme.barIconSize(root.barThickness, -4)
readonly property real vIconSize: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
Loader {
active: root.showPrinterIcon
@@ -459,7 +459,7 @@ BasePill {
DankIcon {
name: "screen_record"
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: root.showScreenSharingIcon && NiriService.hasCasts
@@ -468,7 +468,7 @@ BasePill {
DankIcon {
id: networkIcon
name: root.getNetworkIconName()
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: root.getNetworkIconColor()
anchors.verticalCenter: parent.verticalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable
@@ -477,7 +477,7 @@ BasePill {
DankIcon {
id: vpnIcon
name: "vpn_lock"
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
@@ -486,7 +486,7 @@ BasePill {
DankIcon {
id: bluetoothIcon
name: "bluetooth"
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
@@ -502,7 +502,7 @@ BasePill {
DankIcon {
id: audioIcon
name: root.getVolumeIconName()
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
@@ -544,7 +544,7 @@ BasePill {
DankIcon {
id: micIcon
name: root.getMicIconName()
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: root.getMicIconColor()
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
@@ -586,7 +586,7 @@ BasePill {
DankIcon {
id: brightnessIcon
name: root.getBrightnessIconName()
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
@@ -618,7 +618,7 @@ BasePill {
DankIcon {
id: batteryIcon
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: root.getBatteryIconColor()
anchors.verticalCenter: parent.verticalCenter
visible: root.showBatteryIcon && BatteryService.batteryAvailable
@@ -627,7 +627,7 @@ BasePill {
DankIcon {
id: printerIcon
name: "print"
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
@@ -635,7 +635,7 @@ BasePill {
DankIcon {
name: "settings"
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: root.isActive ? Theme.primary : Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
visible: root.hasNoVisibleIcons()

View File

@@ -36,7 +36,7 @@ BasePill {
DankIcon {
name: "memory"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: {
if (DgopService.cpuUsage > 80) {
return Theme.tempDanger;
@@ -74,7 +74,7 @@ BasePill {
DankIcon {
id: cpuIcon
name: "memory"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: {
if (DgopService.cpuUsage > 80) {
return Theme.tempDanger;

View File

@@ -36,7 +36,7 @@ BasePill {
DankIcon {
name: "device_thermostat"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: {
if (DgopService.cpuTemperature > 85) {
return Theme.tempDanger;
@@ -74,7 +74,7 @@ BasePill {
DankIcon {
id: cpuTempIcon
name: "device_thermostat"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: {
if (DgopService.cpuTemperature > 85) {
return Theme.tempDanger;

View File

@@ -57,7 +57,7 @@ BasePill {
DankIcon {
name: layout.getLayoutIcon(layout.currentLayoutSymbol)
size: Theme.barIconSize(layout.barThickness)
size: Theme.barIconSize(layout.barThickness, undefined, layout.barConfig?.noBackground)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
}
@@ -78,7 +78,7 @@ BasePill {
DankIcon {
name: layout.getLayoutIcon(layout.currentLayoutSymbol)
size: Theme.barIconSize(layout.barThickness, -4)
size: Theme.barIconSize(layout.barThickness, -4, layout.barConfig?.noBackground)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
}

View File

@@ -112,7 +112,7 @@ BasePill {
DankIcon {
name: "storage"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: {
if (root.diskUsagePercent > 90) {
return Theme.tempDanger;
@@ -146,7 +146,7 @@ BasePill {
DankIcon {
name: "storage"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: {
if (root.diskUsagePercent > 90) {
return Theme.tempDanger;

View File

@@ -104,7 +104,7 @@ BasePill {
DankIcon {
name: "auto_awesome_mosaic"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: {
if (root.displayTemp > 80) {
return Theme.tempDanger;
@@ -142,7 +142,7 @@ BasePill {
DankIcon {
id: gpuTempIcon
name: "auto_awesome_mosaic"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: {
if (root.displayTemp > 80) {
return Theme.tempDanger;

View File

@@ -17,7 +17,7 @@ BasePill {
DankIcon {
anchors.centerIn: parent
name: SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: Theme.widgetTextColor
}
}

View File

@@ -61,7 +61,7 @@ BasePill {
DankIcon {
name: "keyboard"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
}

View File

@@ -21,15 +21,15 @@ BasePill {
visible: SettingsData.launcherLogoMode === "apps"
anchors.centerIn: parent
name: "apps"
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: Theme.widgetIconColor
}
SystemLogo {
visible: SettingsData.launcherLogoMode === "os"
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
colorOverride: Theme.effectiveLogoColor
brightnessOverride: SettingsData.launcherLogoBrightness
contrastOverride: SettingsData.launcherLogoContrast
@@ -38,8 +38,8 @@ BasePill {
IconImage {
visible: SettingsData.launcherLogoMode === "dank"
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
smooth: true
mipmap: true
asynchronous: true
@@ -57,8 +57,8 @@ BasePill {
IconImage {
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isLabwc)
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
smooth: true
asynchronous: true
source: {
@@ -94,8 +94,8 @@ BasePill {
IconImage {
visible: SettingsData.launcherLogoMode === "custom" && SettingsData.launcherLogoCustomPath !== ""
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
smooth: true
asynchronous: true
source: SettingsData.launcherLogoCustomPath ? "file://" + SettingsData.launcherLogoCustomPath.replace("file://", "") : ""

View File

@@ -41,7 +41,7 @@ BasePill {
DankIcon {
name: "network_check"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
}
@@ -79,7 +79,7 @@ BasePill {
DankIcon {
name: "network_check"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
}

View File

@@ -44,7 +44,7 @@ BasePill {
anchors.centerIn: parent
name: "assignment"
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: root.isActive ? Theme.primary : Theme.surfaceText
}
}

View File

@@ -18,7 +18,7 @@ BasePill {
id: notifIcon
anchors.centerIn: parent
name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: SessionData.doNotDisturb ? Theme.primary : (root.isActive ? Theme.primary : Theme.widgetIconColor)
}

View File

@@ -16,7 +16,7 @@ BasePill {
DankIcon {
anchors.centerIn: parent
name: "power_settings_new"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: Theme.widgetIconColor
}
}

View File

@@ -38,7 +38,7 @@ BasePill {
DankIcon {
name: "developer_board"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: {
if (DgopService.memoryUsage > 90) {
return Theme.tempDanger;
@@ -84,7 +84,7 @@ BasePill {
DankIcon {
id: ramIcon
name: "developer_board"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: {
if (DgopService.memoryUsage > 90) {
return Theme.tempDanger;

View File

@@ -366,10 +366,10 @@ Item {
IconImage {
id: iconImg
anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
source: {
root._desktopEntriesUpdateTrigger;
root._appIdSubstitutionsTrigger;
@@ -395,9 +395,9 @@ Item {
DankIcon {
anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
name: "sports_esports"
color: Theme.widgetTextColor
visible: !iconImg.visible && Paths.isSteamApp(appId)
@@ -611,10 +611,10 @@ Item {
IconImage {
id: iconImg
anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
source: {
root._desktopEntriesUpdateTrigger;
root._appIdSubstitutionsTrigger;
@@ -640,9 +640,9 @@ Item {
DankIcon {
anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
name: "sports_esports"
color: Theme.widgetTextColor
visible: !iconImg.visible && Paths.isSteamApp(appId)

View File

@@ -198,8 +198,8 @@ Item {
IconImage {
id: iconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
source: delegateRoot.iconSource
asynchronous: true
smooth: true
@@ -262,7 +262,7 @@ Item {
DankIcon {
anchors.centerIn: parent
name: root.menuOpen ? "expand_less" : "expand_more"
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: Theme.widgetTextColor
}
@@ -331,8 +331,8 @@ Item {
IconImage {
id: iconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
source: delegateRoot.iconSource
asynchronous: true
smooth: true
@@ -402,7 +402,7 @@ Item {
return root.menuOpen ? "chevron_right" : "chevron_left";
}
}
size: Theme.barIconSize(root.barThickness)
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
color: Theme.widgetTextColor
}
@@ -754,8 +754,8 @@ Item {
IconImage {
id: menuIconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
source: parent.iconSource
asynchronous: true
smooth: true

View File

@@ -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
@@ -36,7 +33,7 @@ BasePill {
return "system_update_alt";
return "check_circle";
}
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: {
if (SystemUpdateService.hasError)
return Theme.error;
@@ -93,7 +90,7 @@ BasePill {
return "system_update_alt";
return "check_circle";
}
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: {
if (SystemUpdateService.hasError)
return Theme.error;

View File

@@ -41,7 +41,7 @@ BasePill {
id: icon
name: DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off"
size: Theme.barIconSize(root.barThickness, -4)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
color: DMSNetworkService.connected ? Theme.primary : Theme.widgetIconColor
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
anchors.centerIn: parent

View File

@@ -30,7 +30,7 @@ BasePill {
DankIcon {
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.barIconSize(root.barThickness, -6)
size: Theme.barIconSize(root.barThickness, -6, root.barConfig?.noBackground)
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
}
@@ -57,7 +57,7 @@ BasePill {
DankIcon {
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.barIconSize(root.barThickness, -6)
size: Theme.barIconSize(root.barThickness, -6, root.barConfig?.noBackground)
color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
}

View File

@@ -199,15 +199,22 @@ Item {
let targetWorkspaceId;
if (CompositorService.isNiri) {
const wsNumber = typeof ws === "number" ? ws : -1;
if (wsNumber <= 0) {
return [];
if (!ws || typeof ws !== "object") {
const wsNumber = typeof ws === "number" ? ws : -1;
if (wsNumber <= 0) {
return [];
}
const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNumber && w.output === root.effectiveScreenName);
if (!workspace) {
return [];
}
targetWorkspaceId = workspace.id;
} else {
if (ws.id === undefined || ws.id === -1 || ws.idx === -1) {
return [];
}
targetWorkspaceId = ws.id;
}
const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNumber && w.output === root.effectiveScreenName);
if (!workspace) {
return [];
}
targetWorkspaceId = workspace.id;
} else if (CompositorService.isHyprland) {
targetWorkspaceId = ws.id !== undefined ? ws.id : ws;
} else if (CompositorService.isDwl) {
@@ -300,6 +307,12 @@ Item {
"active": false,
"hidden": true
};
} else if (CompositorService.isNiri) {
placeholder = {
"id": -1,
"idx": -1,
"name": ""
};
} else if (CompositorService.isHyprland) {
placeholder = {
"id": -1,
@@ -324,28 +337,52 @@ Item {
function getNiriWorkspaces() {
if (NiriService.allWorkspaces.length === 0) {
return [1, 2];
return [
{
"id": 1,
"idx": 0,
"name": ""
},
{
"id": 2,
"idx": 1,
"name": ""
}
];
}
const fallbackWorkspaces = [
{
"id": 1,
"idx": 0,
"name": ""
},
{
"id": 2,
"idx": 1,
"name": ""
}
];
let workspaces;
if (!root.screenName || SettingsData.workspaceFollowFocus) {
workspaces = NiriService.getCurrentOutputWorkspaceNumbers();
const currentWorkspaces = NiriService.getCurrentOutputWorkspaces();
workspaces = currentWorkspaces.length > 0 ? currentWorkspaces : fallbackWorkspaces;
} else {
const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName).map(ws => ws.idx + 1);
workspaces = displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2];
const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName);
workspaces = displayWorkspaces.length > 0 ? displayWorkspaces : fallbackWorkspaces;
}
workspaces = workspaces.slice().sort((a, b) => a.idx - b.idx);
if (!SettingsData.showOccupiedWorkspacesOnly) {
return workspaces;
}
return workspaces.filter(wsNum => {
const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNum && w.output === root.effectiveScreenName);
if (!workspace)
return false;
if (workspace.is_active)
return workspaces.filter(ws => {
if (ws.is_active)
return true;
return NiriService.windows?.some(win => win.workspace_id === workspace.id) ?? false;
return NiriService.windows?.some(win => win.workspace_id === ws.id) ?? false;
});
}
@@ -359,7 +396,7 @@ Item {
}
const activeWs = NiriService.allWorkspaces.find(ws => ws.output === root.screenName && ws.is_active);
return activeWs ? activeWs.idx + 1 : 1;
return activeWs ? activeWs.idx : 1;
}
function getDwlTags() {
@@ -467,19 +504,22 @@ Item {
readonly property real padding: Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
readonly property real visualWidth: isVertical ? widgetHeight : (workspaceRow.implicitWidth + padding * 2)
readonly property real visualHeight: isVertical ? (workspaceRow.implicitHeight + padding * 2) : widgetHeight
readonly property real appIconSize: Theme.barIconSize(barThickness, -6)
readonly property real appIconSize: Theme.barIconSize(barThickness, -6, root.barConfig?.noBackground)
function getRealWorkspaces() {
return root.workspaceList.filter(ws => {
if (useExtWorkspace)
return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden;
if (CompositorService.isHyprland)
return ws && ws.id !== -1;
if (CompositorService.isDwl)
return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll)
return ws && ws.num !== -1;
return ws !== -1;
if (useExtWorkspace)
return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden;
if (CompositorService.isNiri)
return ws && ws.idx !== -1;
if (CompositorService.isHyprland)
return ws && ws.id !== -1;
if (CompositorService.isDwl)
return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll)
return ws && ws.num !== -1;
return ws !== -1;
});
}
@@ -506,7 +546,7 @@ Item {
return;
}
const currentIndex = realWorkspaces.findIndex(ws => ws === root.currentWorkspace);
const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === root.currentWorkspace);
const validIndex = currentIndex === -1 ? 0 : currentIndex;
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
@@ -514,7 +554,11 @@ Item {
return;
}
NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1);
const nextWorkspace = realWorkspaces[nextIndex];
if (!nextWorkspace || nextWorkspace.idx === undefined) {
return;
}
NiriService.switchToWorkspace(nextWorkspace.idx);
} else if (CompositorService.isHyprland) {
const realWorkspaces = getRealWorkspaces();
if (realWorkspaces.length < 2) {
@@ -565,10 +609,26 @@ Item {
}
}
function getWorkspaceIndexFallback(modelData, index) {
if (root.useExtWorkspace)
return index + 1;
if (CompositorService.isNiri)
return (modelData?.idx !== undefined && modelData?.idx !== -1) ? modelData.idx : "";
if (CompositorService.isHyprland)
return modelData?.id || "";
if (CompositorService.isDwl)
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
if (CompositorService.isSway || CompositorService.isScroll)
return modelData?.num || "";
return modelData - 1;
}
function getWorkspaceIndex(modelData, index) {
let isPlaceholder;
if (root.useExtWorkspace) {
isPlaceholder = modelData?.hidden === true;
} else if (CompositorService.isNiri) {
isPlaceholder = modelData?.idx === -1;
} else if (CompositorService.isHyprland) {
isPlaceholder = modelData?.id === -1;
} else if (CompositorService.isDwl) {
@@ -582,26 +642,28 @@ Item {
if (isPlaceholder)
return index + 1;
let workspaceName = "";
if (SettingsData.showWorkspaceName) {
let workspaceName = modelData?.name;
workspaceName = modelData?.name ?? "";
if (workspaceName && workspaceName !== "") {
if (root.isVertical) {
return workspaceName.charAt(0);
workspaceName = workspaceName.charAt(0);
}
return workspaceName;
} else {
workspaceName = "";
}
}
if (root.useExtWorkspace)
return index + 1;
if (CompositorService.isHyprland)
return modelData?.id || "";
if (CompositorService.isDwl)
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
if (CompositorService.isSway || CompositorService.isScroll)
return modelData?.num || "";
return modelData - 1;
if (workspaceName) {
if (SettingsData.showWorkspaceIndex) {
const indexLabel = getWorkspaceIndexFallback(modelData, index);
return indexLabel ? `${indexLabel}: ${workspaceName}` : workspaceName;
}
return workspaceName;
}
return getWorkspaceIndexFallback(modelData, index);
}
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll
@@ -747,6 +809,8 @@ Item {
property bool isActive: {
if (root.useExtWorkspace)
return (modelData?.id || modelData?.name) === root.currentWorkspace;
if (CompositorService.isNiri)
return !!(modelData && modelData.idx === root.currentWorkspace);
if (CompositorService.isHyprland)
return !!(modelData && modelData.id === root.currentWorkspace);
if (CompositorService.isDwl)
@@ -769,6 +833,8 @@ Item {
property bool isPlaceholder: {
if (root.useExtWorkspace)
return !!(modelData && modelData.hidden);
if (CompositorService.isNiri)
return !!(modelData && modelData.idx === -1);
if (CompositorService.isHyprland)
return !!(modelData && modelData.id === -1);
if (CompositorService.isDwl)
@@ -800,6 +866,10 @@ Item {
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7)
readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5)
readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== ""
readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && CompositorService.isNiri
readonly property real contentImplicitWidth: (hasWorkspaceName || loadedHasIcon) ? (appIconsLoader.item?.contentWidth ?? 0) : 0
readonly property real contentImplicitHeight: (workspaceNamesEnabled || loadedHasIcon) ? (appIconsLoader.item?.contentHeight ?? 0) : 0
readonly property real iconsExtraWidth: {
if (!root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
@@ -816,8 +886,16 @@ Item {
return 0;
}
readonly property real visualWidth: baseWidth + iconsExtraWidth
readonly property real visualHeight: baseHeight + iconsExtraHeight
readonly property real visualWidth: {
if (contentImplicitWidth <= 0) return baseWidth + iconsExtraWidth;
const padding = root.isVertical ? Theme.spacingXS : Theme.spacingS;
return Math.max(baseWidth + iconsExtraWidth, contentImplicitWidth + padding);
}
readonly property real visualHeight: {
if (contentImplicitHeight <= 0) return baseHeight + iconsExtraHeight;
const padding = root.isVertical ? Theme.spacingS : Theme.spacingXS;
return Math.max(baseHeight + iconsExtraHeight, contentImplicitHeight + padding);
}
readonly property color unfocusedColor: {
switch (SettingsData.workspaceUnfocusedColorMode) {
@@ -915,8 +993,8 @@ Item {
} else if (CompositorService.isNiri) {
if (isRightClick) {
NiriService.toggleOverview();
} else {
NiriService.switchToWorkspace(modelData - 1);
} else if (modelData && modelData.idx !== undefined) {
NiriService.switchToWorkspace(modelData.idx);
}
} else if (CompositorService.isHyprland && modelData?.id) {
if (isRightClick && root.hyprlandOverviewLoader?.item) {
@@ -958,7 +1036,7 @@ Item {
if (root.useExtWorkspace) {
wsData = modelData;
} else if (CompositorService.isNiri) {
wsData = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.effectiveScreenName) || null;
wsData = modelData || null;
} else if (CompositorService.isHyprland) {
wsData = modelData;
} else if (CompositorService.isDwl) {
@@ -984,6 +1062,8 @@ Item {
if (SettingsData.showWorkspaceApps) {
if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
} else if (CompositorService.isNiri) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData);
} else {
delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData));
}
@@ -1096,8 +1176,12 @@ Item {
Loader {
id: appIconsLoader
anchors.fill: parent
active: SettingsData.showWorkspaceApps
active: SettingsData.showWorkspaceApps || SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName || loadedHasIcon
sourceComponent: Item {
id: contentRoot
readonly property real contentWidth: contentRow.item?.implicitWidth ?? 0
readonly property real contentHeight: contentRow.item?.implicitHeight ?? 0
Loader {
id: contentRow
anchors.centerIn: parent

View File

@@ -23,6 +23,7 @@ Item {
property string pendingSaveContent: ""
signal hideRequested
signal previewRequested(string content)
Ref {
service: NotepadStorageService
@@ -198,6 +199,10 @@ Item {
createNewTab();
}
onPreviewRequested: {
textEditor.togglePreview();
}
onEscapePressed: {
root.hideRequested();
}
@@ -357,8 +362,8 @@ Item {
DankModal {
id: confirmationDialog
width: 400
height: 180
modalWidth: 400
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 180
shouldBeVisible: false
allowStacking: true
@@ -371,6 +376,7 @@ Item {
FocusScope {
anchors.fill: parent
focus: true
implicitHeight: contentColumn.implicitHeight
Keys.onEscapePressed: event => {
confirmationDialog.close();
@@ -379,47 +385,31 @@ Item {
}
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Row {
StyledText {
text: I18n.tr("Unsaved Changes")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: root.pendingAction === "new" ? I18n.tr("You have unsaved changes. Save before creating a new file?") : root.pendingAction.startsWith("close_tab_") ? I18n.tr("You have unsaved changes. Save before closing this tab?") : root.pendingAction === "load_file" || root.pendingAction === "open" ? I18n.tr("You have unsaved changes. Save before opening a file?") : I18n.tr("You have unsaved changes. Save before continuing?")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Unsaved Changes")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: root.pendingAction === "new" ? I18n.tr("You have unsaved changes. Save before creating a new file?") : root.pendingAction.startsWith("close_tab_") ? I18n.tr("You have unsaved changes. Save before closing this tab?") : root.pendingAction === "load_file" || root.pendingAction === "open" ? I18n.tr("You have unsaved changes. Save before opening a file?") : I18n.tr("You have unsaved changes. Save before continuing?")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
wrapMode: Text.Wrap
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: {
confirmationDialog.close();
root.confirmationDialogOpen = false;
}
}
wrapMode: Text.Wrap
}
Item {
width: parent.width
height: 40
height: 36
Row {
anchors.right: parent.right
@@ -510,6 +500,20 @@ Item {
}
}
}
DankActionButton {
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: {
confirmationDialog.close();
root.confirmationDialogOpen = false;
}
}
}
}
}

View File

@@ -11,6 +11,12 @@ pragma ComponentBehavior: Bound
Column {
id: root
Component.onCompleted: {
if (PluginService.isPluginLoaded("dankNotepadModule")) {
pluginHighlightedHtml = SettingsData.getBuiltInPluginSetting("dankNotepadModule", "highlightedHtml", "")
}
}
property alias text: textArea.text
property alias textArea: textArea
property bool contentLoaded: false
@@ -21,10 +27,15 @@ Column {
property var searchMatches: []
property int currentMatchIndex: -1
property int matchCount: 0
property bool inlinePreviewVisible: false
property string previewMode: "split" // split | full
property string pluginHighlightedHtml: ""
property string lastPluginContent: ""
signal saveRequested()
signal openRequested()
signal newRequested()
signal previewRequested()
signal escapePressed()
signal contentChanged()
signal settingsRequested()
@@ -50,6 +61,7 @@ Column {
lastSavedContent = content
textArea.text = content
contentLoaded = true
syncContentToPlugin()
}
)
}
@@ -164,6 +176,47 @@ Column {
})
}
function togglePreview() {
if (!inlinePreviewVisible) {
inlinePreviewVisible = true
previewMode = "split"
} else if (previewMode === "split") {
previewMode = "full"
} else {
inlinePreviewVisible = false
previewMode = "split"
}
syncContentToPlugin()
}
function renderPreviewHtml() {
if (!inlinePreviewVisible) return ""
return pluginHighlightedHtml.length > 0 ? pluginHighlightedHtml : "<p><i>Rendering preview…</i></p>"
}
function syncContentToPlugin() {
if (!PluginService.isPluginLoaded("dankNotepadModule"))
return
if (!currentTab)
return
const filePath = currentTab?.filePath || ""
const ext = filePath.split('.').pop().toLowerCase()
const content = textArea.text
if (content === lastPluginContent && SettingsData.getBuiltInPluginSetting("dankNotepadModule", "previewActive", false) === inlinePreviewVisible) {
return
}
lastPluginContent = content
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "previewActive", inlinePreviewVisible)
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "currentFilePath", filePath)
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "currentFileExtension", ext)
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "sourceContent", content)
SettingsData.setBuiltInPluginSetting("dankNotepadModule", "updatedAt", Date.now())
}
function hideSearch() {
searchVisible = false
searchQuery = ""
@@ -174,6 +227,57 @@ Column {
textArea.forceActiveFocus()
}
function copyPlainTextToClipboard() {
if (!inlinePreviewVisible || !textArea.text) return
const content = textArea.text
if (content.length > 0) {
const proc = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
Process {
property string content: ""
command: ["sh", "-c", "printf '%s' \\"$CONTENT\\" | dms clipboard copy"]
environment: { "CONTENT": content }
running: false
}`,
root,
"copyProc"
)
proc.content = content
proc.running = true
proc.exited.connect(() => {
ToastService.showInfo(I18n.tr("Copied to clipboard"))
proc.destroy()
})
}
}
function copyHtmlToClipboard() {
if (!inlinePreviewVisible || !pluginHighlightedHtml) return
if (pluginHighlightedHtml.length > 0) {
const proc = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
Process {
property string content: ""
command: ["sh", "-c", "printf '%s' \\"$CONTENT\\" | dms clipboard copy"]
environment: { "CONTENT": content }
running: false
}`,
root,
"copyProcHtml"
)
proc.content = pluginHighlightedHtml
proc.running = true
proc.exited.connect(() => {
ToastService.showInfo(I18n.tr("HTML copied to clipboard"))
proc.destroy()
})
}
}
spacing: Theme.spacingM
StyledRect {
@@ -303,7 +407,6 @@ Column {
onClicked: root.findNext()
}
// Close button
DankActionButton {
id: closeSearchButton
Layout.alignment: Qt.AlignVCenter
@@ -323,192 +426,311 @@ Column {
border.width: 1
radius: Theme.cornerRadius
DankFlickable {
id: flickable
RowLayout {
id: editorPreviewRow
anchors.fill: parent
anchors.margins: 1
clip: true
contentWidth: width - 11
spacing: Theme.spacingM
Item {
id: editorPane
visible: !inlinePreviewVisible || previewMode === "split"
Layout.fillHeight: true
Layout.fillWidth: !inlinePreviewVisible || previewMode === "split"
Layout.preferredWidth: inlinePreviewVisible ? parent.width * 0.55 : parent.width
clip: true
DankFlickable {
id: flickable
anchors.fill: parent
clip: true
contentWidth: width - 11
Rectangle {
id: lineNumberArea
anchors.left: parent.left
anchors.top: parent.top
width: SettingsData.notepadShowLineNumbers ? Math.max(30, 32 + Theme.spacingXS) : 0
height: textArea.contentHeight + textArea.topPadding + textArea.bottomPadding
color: "transparent"
visible: SettingsData.notepadShowLineNumbers
ListView {
id: lineNumberList
anchors.top: parent.top
anchors.topMargin: textArea.topPadding
anchors.right: parent.right
anchors.rightMargin: 2
width: 32
height: textArea.contentHeight
model: SettingsData.notepadShowLineNumbers ? root.lineModel : []
interactive: false
spacing: 0
delegate: Item {
id: lineDelegate
required property int index
required property string modelData
width: 32
height: measuringText.contentHeight
Text {
id: measuringText
width: textArea.width - textArea.leftPadding - textArea.rightPadding
text: modelData || " "
font: textArea.font
wrapMode: Text.Wrap
visible: false
}
StyledText {
anchors.right: parent.right
anchors.rightMargin: 4
anchors.top: parent.top
text: index + 1
font.family: textArea.font.family
font.pixelSize: textArea.font.pixelSize
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
horizontalAlignment: Text.AlignRight
}
}
}
}
TextArea.flickable: TextArea {
id: textArea
placeholderText: ""
placeholderTextColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
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
textFormat: TextEdit.PlainText
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
persistentSelection: true
tabStopDistance: 40
leftPadding: (SettingsData.notepadShowLineNumbers ? lineNumberArea.width + Theme.spacingXS : Theme.spacingM)
topPadding: Theme.spacingM
rightPadding: Theme.spacingM
bottomPadding: Theme.spacingM
cursorDelegate: Rectangle {
width: 1.5
radius: 1
color: Theme.surfaceText
x: textArea.cursorRectangle.x
y: textArea.cursorRectangle.y
height: textArea.cursorRectangle.height
opacity: 1.0
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 }
}
}
Component.onCompleted: {
loadCurrentTabContent()
setTextDocumentLineHeight()
root.updateLineModel()
Qt.callLater(() => {
textArea.forceActiveFocus()
})
}
Connections {
target: NotepadStorageService
function onCurrentTabIndexChanged() {
loadCurrentTabContent()
Qt.callLater(() => {
textArea.forceActiveFocus()
})
}
function onTabsChanged() {
if (NotepadStorageService.tabs.length > 0 && !contentLoaded) {/* Lines 444-445 omitted */}
}
}
Connections {
target: SettingsData
function onNotepadShowLineNumbersChanged() {
root.updateLineModel()
}
}
onTextChanged: {
if (contentLoaded && text !== lastSavedContent) {
autoSaveTimer.restart()
}
root.contentChanged()
root.updateLineModel()
pluginSyncTimer.restart()
}
Keys.onEscapePressed: (event) => {
root.escapePressed()
event.accepted = true
}
Keys.onPressed: (event) => {
if (event.modifiers & Qt.ControlModifier) {
switch (event.key) {
case Qt.Key_S:
event.accepted = true
root.saveRequested()
break
case Qt.Key_O:
event.accepted = true
root.openRequested()
break
case Qt.Key_N:
event.accepted = true
root.newRequested()
break
case Qt.Key_A:
event.accepted = true
textArea.selectAll()
break
case Qt.Key_F:
event.accepted = true
root.showSearch()
break
case Qt.Key_P:
if (PluginService.isPluginLoaded("dankNotepadModule")) {
event.accepted = true
root.previewRequested()
}
break
}
}
}
background: Rectangle {
color: "transparent"
}
}
StyledText {
id: placeholderOverlay
text: I18n.tr("Start typing your notes here...")
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.family: textArea.font.family
font.pixelSize: textArea.font.pixelSize
visible: textArea.text.length === 0
anchors.left: textArea.left
anchors.top: textArea.top
anchors.leftMargin: textArea.leftPadding
anchors.topMargin: textArea.topPadding
z: textArea.z + 1
}
}
}
Rectangle {
id: lineNumberArea
anchors.left: parent.left
anchors.top: parent.top
width: SettingsData.notepadShowLineNumbers ? Math.max(30, 32 + Theme.spacingXS) : 0
height: textArea.contentHeight + textArea.topPadding + textArea.bottomPadding
color: "transparent"
visible: SettingsData.notepadShowLineNumbers
id: previewDivider
visible: inlinePreviewVisible && previewMode === "split"
Layout.fillHeight: true
Layout.preferredWidth: 1
color: Theme.outlineMedium
}
ListView {
id: lineNumberList
Item {
id: previewPane
visible: inlinePreviewVisible
Layout.fillHeight: true
Layout.fillWidth: previewMode === "full"
Layout.preferredWidth: previewMode === "full" ? parent.width : parent.width * 0.45
clip: true
// Preview header with copy buttons
Rectangle {
id: previewHeader
anchors.top: parent.top
anchors.topMargin: textArea.topPadding
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: 2
width: 32
height: textArea.contentHeight
model: SettingsData.notepadShowLineNumbers ? root.lineModel : []
interactive: false
spacing: 0
height: 36
color: Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, Theme.notepadTransparency)
z: 2
delegate: Item {
id: lineDelegate
required property int index
required property string modelData
width: 32
height: measuringText.contentHeight
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
Text {
id: measuringText
width: textArea.width - textArea.leftPadding - textArea.rightPadding
text: modelData || " "
font: textArea.font
wrapMode: Text.Wrap
visible: false
// Copy plain text button
DankActionButton {
iconName: "content_copy"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceTextMedium
onClicked: copyPlainTextToClipboard()
}
StyledText {
anchors.right: parent.right
anchors.rightMargin: 4
anchors.top: parent.top
text: index + 1
font.family: textArea.font.family
font.pixelSize: textArea.font.pixelSize
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Copy Text")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
}
}
TextArea.flickable: TextArea {
id: textArea
placeholderText: ""
placeholderTextColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
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
textFormat: TextEdit.PlainText
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
persistentSelection: true
tabStopDistance: 40
leftPadding: (SettingsData.notepadShowLineNumbers ? lineNumberArea.width + Theme.spacingXS : Theme.spacingM)
topPadding: Theme.spacingM
rightPadding: Theme.spacingM
bottomPadding: Theme.spacingM
cursorDelegate: Rectangle {
width: 1.5
radius: 1
color: Theme.surfaceText
x: textArea.cursorRectangle.x
y: textArea.cursorRectangle.y
height: textArea.cursorRectangle.height
opacity: 1.0
Rectangle {
width: 1
height: 20
color: Theme.outlineVariant
anchors.verticalCenter: parent.verticalCenter
}
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 }
}
}
// Copy HTML button
DankActionButton {
iconName: "code"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceTextMedium
onClicked: copyHtmlToClipboard()
}
Component.onCompleted: {
loadCurrentTabContent()
setTextDocumentLineHeight()
root.updateLineModel()
Qt.callLater(() => {
textArea.forceActiveFocus()
})
}
Connections {
target: NotepadStorageService
function onCurrentTabIndexChanged() {
loadCurrentTabContent()
Qt.callLater(() => {
textArea.forceActiveFocus()
})
}
function onTabsChanged() {
if (NotepadStorageService.tabs.length > 0 && !contentLoaded) {
loadCurrentTabContent()
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Copy HTML")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
}
Connections {
target: SettingsData
function onNotepadShowLineNumbersChanged() {
root.updateLineModel()
DankFlickable {
id: previewFlickable
anchors.top: previewHeader.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: Theme.spacingS
clip: true
contentWidth: width - 11
contentHeight: previewText.paintedHeight + Theme.spacingM * 2
Text {
id: previewText
width: parent.width - Theme.spacingM
padding: Theme.spacingM
wrapMode: Text.WordWrap
textFormat: Text.RichText
text: inlinePreviewVisible ? renderPreviewHtml() : ""
color: Theme.surfaceText
font.family: SettingsData.notepadFontFamily || SettingsData.fontFamily
font.pixelSize: Theme.fontSizeMedium
linkColor: Theme.primary
onLinkActivated: url => Qt.openUrlExternally(url)
}
}
onTextChanged: {
if (contentLoaded && text !== lastSavedContent) {
autoSaveTimer.restart()
}
root.contentChanged()
root.updateLineModel()
}
Keys.onEscapePressed: (event) => {
root.escapePressed()
event.accepted = true
}
Keys.onPressed: (event) => {
if (event.modifiers & Qt.ControlModifier) {
switch (event.key) {
case Qt.Key_S:
event.accepted = true
root.saveRequested()
break
case Qt.Key_O:
event.accepted = true
root.openRequested()
break
case Qt.Key_N:
event.accepted = true
root.newRequested()
break
case Qt.Key_A:
event.accepted = true
selectAll()
break
case Qt.Key_F:
event.accepted = true
root.showSearch()
break
}
}
}
background: Rectangle {
color: "transparent"
}
}
StyledText {
id: placeholderOverlay
text: I18n.tr("Start typing your notes here...")
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.family: textArea.font.family
font.pixelSize: textArea.font.pixelSize
visible: textArea.text.length === 0
anchors.left: textArea.left
anchors.top: textArea.top
anchors.leftMargin: textArea.leftPadding
anchors.topMargin: textArea.topPadding
z: textArea.z + 1
}
}
}
@@ -575,6 +797,24 @@ Column {
color: Theme.surfaceTextMedium
}
}
Row {
spacing: Theme.spacingS
visible: PluginService.isPluginLoaded("dankNotepadModule")
DankActionButton {
iconName: inlinePreviewVisible ? "visibility" : "visibility_off"
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText
enabled: textArea.text.length > 0
onClicked: root.previewRequested()
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Preview")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
}
DankActionButton {
@@ -646,4 +886,20 @@ Column {
autoSaveToSession()
}
}
Timer {
id: pluginSyncTimer
interval: 350
repeat: false
onTriggered: syncContentToPlugin()
}
Connections {
target: SettingsData
function onBuiltInPluginSettingsChanged() {
if (PluginService.isPluginLoaded("dankNotepadModule")) {
pluginHighlightedHtml = SettingsData.getBuiltInPluginSetting("dankNotepadModule", "highlightedHtml", "")
}
}
}
}

View File

@@ -558,8 +558,8 @@ Rectangle {
visible: !expanded
anchors.right: clearButton.visible ? clearButton.left : parent.right
anchors.rightMargin: clearButton.visible ? contentSpacing : Theme.spacingL
anchors.bottom: parent.bottom
anchors.bottomMargin: contentSpacing
anchors.top: collapsedContent.bottom
anchors.topMargin: contentSpacing
spacing: contentSpacing
Repeater {
@@ -614,8 +614,8 @@ Rectangle {
visible: !expanded && actionCount < 3
anchors.right: parent.right
anchors.rightMargin: Theme.spacingL
anchors.bottom: parent.bottom
anchors.bottomMargin: contentSpacing
anchors.top: collapsedContent.bottom
anchors.topMargin: contentSpacing
width: Math.max(clearText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50)
height: actionButtonHeight
radius: Theme.spacingXS

View File

@@ -531,8 +531,8 @@ PanelWindow {
Row {
anchors.right: clearButton.visible ? clearButton.left : parent.right
anchors.rightMargin: clearButton.visible ? contentSpacing : Theme.spacingL
anchors.bottom: parent.bottom
anchors.bottomMargin: contentSpacing
anchors.top: notificationContent.bottom
anchors.topMargin: contentSpacing
spacing: contentSpacing
z: 20
@@ -585,8 +585,8 @@ PanelWindow {
visible: actionCount < 3
anchors.right: parent.right
anchors.rightMargin: Theme.spacingL
anchors.bottom: parent.bottom
anchors.bottomMargin: contentSpacing
anchors.top: notificationContent.bottom
anchors.topMargin: contentSpacing
width: Math.max(clearTextLabel.implicitWidth + Theme.spacingM, compactMode ? 40 : 50)
height: actionButtonHeight
radius: Theme.spacingXS

View File

@@ -59,8 +59,8 @@ Item {
readonly property bool hasVerticalPill: verticalBarPill !== null
readonly property bool hasPopout: popoutContent !== null
readonly property int iconSize: Theme.barIconSize(barThickness, -4)
readonly property int iconSizeLarge: Theme.barIconSize(barThickness)
readonly property int iconSize: Theme.barIconSize(barThickness, -4, root.barConfig?.noBackground)
readonly property int iconSizeLarge: Theme.barIconSize(barThickness, undefined, root.barConfig?.noBackground)
Component.onCompleted: {
loadPluginData();

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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;

View File

@@ -7,6 +7,9 @@ import qs.Widgets
Item {
id: aboutTab
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property bool isHyprland: CompositorService.isHyprland
property bool isNiri: CompositorService.isNiri
property bool isSway: CompositorService.isSway
@@ -255,7 +258,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingS
property bool compactMode: parent.width < 400
property bool compactMode: parent.width < 450
DankButton {
id: docsButton
@@ -628,6 +631,7 @@ Item {
}
Row {
anchors.left: parent.left
spacing: Theme.spacingL
Column {
@@ -637,6 +641,7 @@ Item {
text: I18n.tr("Version")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignLeft
}
StyledText {
@@ -644,6 +649,7 @@ Item {
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
}
}
@@ -660,6 +666,7 @@ Item {
text: I18n.tr("API")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignLeft
}
StyledText {
@@ -667,6 +674,7 @@ Item {
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
}
}
@@ -683,6 +691,7 @@ Item {
text: I18n.tr("Status")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignLeft
}
Row {
@@ -701,6 +710,7 @@ Item {
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
}
}
}
@@ -715,6 +725,8 @@ Item {
text: I18n.tr("Capabilities")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
Flow {
@@ -780,6 +792,7 @@ Item {
}
Row {
anchors.left: parent.left
spacing: Theme.spacingS
DankButton {

View File

@@ -9,6 +9,9 @@ import qs.Modules.Settings.Widgets
Item {
id: dankBarTab
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var parentModal: null
property string selectedBarId: "default"
@@ -366,9 +369,12 @@ Item {
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
@@ -390,12 +396,14 @@ Item {
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignLeft
}
StyledText {
@@ -409,12 +417,14 @@ Item {
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignLeft
}
StyledText {
@@ -428,12 +438,14 @@ Item {
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignLeft
visible: {
SettingsData.barConfigs;
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
@@ -445,6 +457,7 @@ Item {
text: I18n.tr("Disabled")
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
horizontalAlignment: Text.AlignLeft
visible: {
SettingsData.barConfigs;
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
@@ -520,6 +533,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignLeft
}
Column {
@@ -1126,7 +1140,9 @@ Item {
text: I18n.tr("Color")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
x: Theme.spacingM
horizontalAlignment: Text.AlignLeft
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
}
Item {

View File

@@ -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: {

View File

@@ -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"
@@ -462,6 +572,227 @@ Item {
}
}
SettingsCard {
id: pluginVisibilityCard
width: parent.width
iconName: "filter_list"
title: I18n.tr("Plugin Visibility")
settingKey: "pluginVisibility"
property var allLauncherPlugins: {
SettingsData.launcherPluginVisibility;
SettingsData.launcherPluginOrder;
var plugins = [];
var builtIn = AppSearchService.getBuiltInLauncherPlugins() || {};
for (var pluginId in builtIn) {
var plugin = builtIn[pluginId];
plugins.push({
id: pluginId,
name: plugin.name || pluginId,
icon: plugin.cornerIcon || "extension",
iconType: "material",
isBuiltIn: true,
trigger: AppSearchService.getBuiltInPluginTrigger(pluginId) || ""
});
}
var thirdParty = PluginService.getLauncherPlugins() || {};
for (var pluginId in thirdParty) {
var plugin = thirdParty[pluginId];
var rawIcon = plugin.icon || "extension";
plugins.push({
id: pluginId,
name: plugin.name || pluginId,
icon: rawIcon.startsWith("material:") ? rawIcon.substring(9) : rawIcon.startsWith("unicode:") ? rawIcon.substring(8) : rawIcon,
iconType: rawIcon.startsWith("unicode:") ? "unicode" : "material",
isBuiltIn: false,
trigger: PluginService.getPluginTrigger(pluginId) || ""
});
}
return SettingsData.getOrderedLauncherPlugins(plugins);
}
function reorderPlugin(fromIndex, toIndex) {
if (fromIndex === toIndex)
return;
var currentOrder = allLauncherPlugins.map(p => p.id);
var item = currentOrder.splice(fromIndex, 1)[0];
currentOrder.splice(toIndex, 0, item);
SettingsData.setLauncherPluginOrder(currentOrder);
}
StyledText {
width: parent.width
text: I18n.tr("Control which plugins appear in 'All' mode without requiring a trigger prefix. Drag to reorder.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Column {
id: pluginVisibilityColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: pluginVisibilityCard.allLauncherPlugins
delegate: Item {
id: visibilityDelegateItem
required property var modelData
required property int index
property bool held: pluginDragArea.pressed
property real originalY: y
width: pluginVisibilityColumn.width
height: 52
z: held ? 2 : 1
Rectangle {
id: visibilityDelegate
width: parent.width
height: 52
radius: Theme.cornerRadius
color: visibilityDelegateItem.held ? Theme.surfaceHover : Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3)
Row {
anchors.left: parent.left
anchors.leftMargin: 28
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Item {
width: Theme.iconSize
height: Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
visible: visibilityDelegateItem.modelData.iconType !== "unicode"
name: visibilityDelegateItem.modelData.icon
size: Theme.iconSize
color: Theme.primary
}
StyledText {
anchors.centerIn: parent
visible: visibilityDelegateItem.modelData.iconType === "unicode"
text: visibilityDelegateItem.modelData.icon
font.pixelSize: Theme.iconSize
color: Theme.primary
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Row {
spacing: Theme.spacingS
StyledText {
text: visibilityDelegateItem.modelData.name
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
Rectangle {
visible: visibilityDelegateItem.modelData.isBuiltIn
width: dmsBadgeLabel.implicitWidth + Theme.spacingS
height: 16
radius: 8
color: Theme.primaryContainer
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: dmsBadgeLabel
anchors.centerIn: parent
text: "DMS"
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.primaryText
}
}
}
StyledText {
text: visibilityDelegateItem.modelData.trigger ? I18n.tr("Trigger: %1").arg(visibilityDelegateItem.modelData.trigger) : I18n.tr("No trigger")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
DankToggle {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.getPluginAllowWithoutTrigger(visibilityDelegateItem.modelData.id)
onToggled: function (isChecked) {
SettingsData.setPluginAllowWithoutTrigger(visibilityDelegateItem.modelData.id, isChecked);
}
}
}
MouseArea {
id: pluginDragArea
anchors.left: parent.left
anchors.top: parent.top
width: 28
height: parent.height
hoverEnabled: true
cursorShape: Qt.SizeVerCursor
drag.target: visibilityDelegateItem.held ? visibilityDelegateItem : undefined
drag.axis: Drag.YAxis
preventStealing: true
onPressed: {
visibilityDelegateItem.originalY = visibilityDelegateItem.y;
}
onReleased: {
if (!drag.active) {
visibilityDelegateItem.y = visibilityDelegateItem.originalY;
return;
}
const spacing = Theme.spacingS;
const itemH = visibilityDelegateItem.height + spacing;
var newIndex = Math.round(visibilityDelegateItem.y / itemH);
newIndex = Math.max(0, Math.min(newIndex, pluginVisibilityCard.allLauncherPlugins.length - 1));
pluginVisibilityCard.reorderPlugin(visibilityDelegateItem.index, newIndex);
visibilityDelegateItem.y = visibilityDelegateItem.originalY;
}
}
DankIcon {
x: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
name: "drag_indicator"
size: 18
color: Theme.outline
opacity: pluginDragArea.containsMouse || pluginDragArea.pressed ? 1 : 0.5
}
Behavior on y {
enabled: !pluginDragArea.pressed && !pluginDragArea.drag.active
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
StyledText {
width: parent.width
text: I18n.tr("No launcher plugins installed.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: pluginVisibilityCard.allLauncherPlugins.length === 0
}
}
}
SettingsCard {
width: parent.width
iconName: "search"

View File

@@ -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

View File

@@ -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

View File

@@ -61,6 +61,8 @@ StyledRect {
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignLeft
visible: root.title !== ""
}

View File

@@ -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 {
@@ -13,7 +13,7 @@ Scope {
property string searchActiveScreen: ""
property bool isClosing: false
property bool releaseKeyboard: false
readonly property bool spotlightModalOpen: PopoutService.spotlightModal?.spotlightOpen ?? false
readonly property bool spotlightModalOpen: PopoutService.dankLauncherV2Modal?.spotlightOpen ?? false
property bool overlayActive: NiriService.inOverview || searchActive
function showSpotlight(screenName) {
@@ -67,229 +67,220 @@ 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];
}
}
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;
}
mask: Region {
item: overlayVisible && spotlightContainer.visible ? spotlightContainer : null
}
onShouldShowSpotlightChanged: {
if (shouldShowSpotlight) {
if (launcherContent?.controller) {
launcherContent.controller.searchMode = "apps";
launcherContent.controller.performSearch();
}
return;
}
if (!isActiveScreen)
return;
Qt.callLater(() => keyboardFocusScope.forceActiveFocus());
}
Connections {
target: ModalManager
function onModalChanged() {
overlayWindow.hasActiveModal = !!ModalManager.currentModalsByScreen[overlayWindow.screen.name];
}
}
anchors {
top: true
left: true
right: true
bottom: true
}
screen: modelData
visible: NiriService.inOverview || niriOverviewScope.isClosing
color: "transparent"
FocusScope {
id: keyboardFocusScope
anchors.fill: parent
focus: true
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: spotlightContainer.visible ? spotlightContainer : null
}
onShouldShowSpotlightChanged: {
if (shouldShowSpotlight) {
if (spotlightContent?.appLauncher)
spotlightContent.appLauncher.ensureInitialized();
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)
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
width: Math.min(baseWidth, overlayWindow.screen.width - 100)
height: Math.min(baseHeight, overlayWindow.screen.height - 100)
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)
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.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
onRunningChanged: {
if (running || !spotlightContainer.animatingOut)
return;
niriOverviewScope.resetState();
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.fast
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Rectangle {
anchors.fill: parent
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 1
}
Rectangle {
anchors.fill: parent
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 1
}
LauncherContent {
id: launcherContent
anchors.fill: parent
anchors.margins: 0
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;
property var fakeParentModal: QtObject {
property bool spotlightOpen: spotlightContainer.visible
property bool isClosing: niriOverviewScope.isClosing
function hide() {
if (niriOverviewScope.searchActive) {
niriOverviewScope.hideSpotlight();
return;
}
NiriService.toggleOverview();
}
}
Component.onCompleted: {
parentModal = fakeParentModal;
Connections {
target: launcherContent.searchField
function onTextChanged() {
if (launcherContent.searchField.text.length > 0 || !niriOverviewScope.searchActive)
return;
niriOverviewScope.hideSpotlight();
}
}
Connections {
target: spotlightContent.appLauncher
function onAppLaunched() {
niriOverviewScope.releaseKeyboard = true;
}
}
Component.onCompleted: {
parentModal = fakeParentModal;
}
Connections {
target: spotlightContent.fileSearchController
function onFileOpened() {
niriOverviewScope.releaseKeyboard = true;
}
Connections {
target: launcherContent.controller
function onItemExecuted() {
niriOverviewScope.releaseKeyboard = true;
}
}
}

View 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);
}
}
}
}
];
}
}

View 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.

View 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": []
}

View File

@@ -41,7 +41,7 @@ property var popoutService: null
| ------------------ | ------------------------- | ------------------------- | -------------------------------------------------- |
| Settings | `openSettings()` | `closeSettings()` | Full settings interface |
| Clipboard History | `openClipboardHistory()` | `closeClipboardHistory()` | Clipboard integration |
| Spotlight | `openSpotlight()` | `closeSpotlight()` | Command launcher |
| Launcher | `openDankLauncherV2()` | `closeDankLauncherV2()` | Command launcher, also has `toggleDankLauncherV2()` |
| Power Menu | `openPowerMenu()` | `closePowerMenu()` | Also has `togglePowerMenu()` |
| Process List Modal | `showProcessListModal()` | `hideProcessListModal()` | Fullscreen version, has `toggleProcessListModal()` |
| Color Picker | `showColorPicker()` | `hideColorPicker()` | Theme color selection |

View File

@@ -28,20 +28,62 @@ PluginSettings {
label: "Popout to Open"
description: "Select which popout or modal opens when you click the widget"
options: [
{label: "Control Center", value: "controlCenter"},
{label: "Notification Center", value: "notificationCenter"},
{label: "App Drawer", value: "appDrawer"},
{label: "Process List", value: "processList"},
{label: "DankDash", value: "dankDash"},
{label: "Battery Info", value: "battery"},
{label: "VPN", value: "vpn"},
{label: "System Update", value: "systemUpdate"},
{label: "Settings", value: "settings"},
{label: "Clipboard History", value: "clipboardHistory"},
{label: "Spotlight", value: "spotlight"},
{label: "Power Menu", value: "powerMenu"},
{label: "Color Picker", value: "colorPicker"},
{label: "Notepad", value: "notepad"}
{
label: "Control Center",
value: "controlCenter"
},
{
label: "Notification Center",
value: "notificationCenter"
},
{
label: "App Drawer",
value: "appDrawer"
},
{
label: "Process List",
value: "processList"
},
{
label: "DankDash",
value: "dankDash"
},
{
label: "Battery Info",
value: "battery"
},
{
label: "VPN",
value: "vpn"
},
{
label: "System Update",
value: "systemUpdate"
},
{
label: "Settings",
value: "settings"
},
{
label: "Clipboard History",
value: "clipboardHistory"
},
{
label: "Spotlight",
value: "spotlight"
},
{
label: "Power Menu",
value: "powerMenu"
},
{
label: "Color Picker",
value: "colorPicker"
},
{
label: "Notepad",
value: "notepad"
}
]
defaultValue: "controlCenter"
}

View File

@@ -1,6 +1,4 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
@@ -13,42 +11,42 @@ PluginComponent {
property string selectedPopout: pluginData.selectedPopout || "controlCenter"
property var popoutActions: ({
"controlCenter": (x, y, w, s, scr) => popoutService?.toggleControlCenter(x, y, w, s, scr),
"notificationCenter": (x, y, w, s, scr) => popoutService?.toggleNotificationCenter(x, y, w, s, scr),
"appDrawer": (x, y, w, s, scr) => popoutService?.toggleAppDrawer(x, y, w, s, scr),
"processList": (x, y, w, s, scr) => popoutService?.toggleProcessList(x, y, w, s, scr),
"dankDash": (x, y, w, s, scr) => popoutService?.toggleDankDash(0, x, y, w, s, scr),
"battery": (x, y, w, s, scr) => popoutService?.toggleBattery(x, y, w, s, scr),
"vpn": (x, y, w, s, scr) => popoutService?.toggleVpn(x, y, w, s, scr),
"systemUpdate": (x, y, w, s, scr) => popoutService?.toggleSystemUpdate(x, y, w, s, scr),
"settings": () => popoutService?.openSettings(),
"clipboardHistory": () => popoutService?.openClipboardHistory(),
"spotlight": () => popoutService?.openSpotlight(),
"powerMenu": () => popoutService?.togglePowerMenu(),
"colorPicker": () => popoutService?.showColorPicker(),
"notepad": () => popoutService?.toggleNotepad()
})
"controlCenter": (x, y, w, s, scr) => popoutService?.toggleControlCenter(x, y, w, s, scr),
"notificationCenter": (x, y, w, s, scr) => popoutService?.toggleNotificationCenter(x, y, w, s, scr),
"appDrawer": (x, y, w, s, scr) => popoutService?.toggleAppDrawer(x, y, w, s, scr),
"processList": (x, y, w, s, scr) => popoutService?.toggleProcessList(x, y, w, s, scr),
"dankDash": (x, y, w, s, scr) => popoutService?.toggleDankDash(0, x, y, w, s, scr),
"battery": (x, y, w, s, scr) => popoutService?.toggleBattery(x, y, w, s, scr),
"vpn": (x, y, w, s, scr) => popoutService?.toggleVpn(x, y, w, s, scr),
"systemUpdate": (x, y, w, s, scr) => popoutService?.toggleSystemUpdate(x, y, w, s, scr),
"settings": () => popoutService?.openSettings(),
"clipboardHistory": () => popoutService?.openClipboardHistory(),
"spotlight": () => popoutService?.toggleDankLauncherV2(),
"powerMenu": () => popoutService?.togglePowerMenu(),
"colorPicker": () => popoutService?.showColorPicker(),
"notepad": () => popoutService?.toggleNotepad()
})
property var popoutNames: ({
"controlCenter": "Control Center",
"notificationCenter": "Notification Center",
"appDrawer": "App Drawer",
"processList": "Process List",
"dankDash": "DankDash",
"battery": "Battery Info",
"vpn": "VPN",
"systemUpdate": "System Update",
"settings": "Settings",
"clipboardHistory": "Clipboard",
"spotlight": "Spotlight",
"powerMenu": "Power Menu",
"colorPicker": "Color Picker",
"notepad": "Notepad"
})
"controlCenter": "Control Center",
"notificationCenter": "Notification Center",
"appDrawer": "App Drawer",
"processList": "Process List",
"dankDash": "DankDash",
"battery": "Battery Info",
"vpn": "VPN",
"systemUpdate": "System Update",
"settings": "Settings",
"clipboardHistory": "Clipboard",
"spotlight": "Spotlight",
"powerMenu": "Power Menu",
"colorPicker": "Color Picker",
"notepad": "Notepad"
})
pillClickAction: (x, y, width, section, screen) => {
if (popoutActions[selectedPopout]) {
popoutActions[selectedPopout](x, y, width, section, screen)
popoutActions[selectedPopout](x, y, width, section, screen);
}
}

View File

@@ -84,10 +84,11 @@ popoutService.openClipboardHistory()
popoutService.closeClipboardHistory()
```
#### Spotlight Modal
#### Launcher Modal
```qml
popoutService.openSpotlight()
popoutService.closeSpotlight()
popoutService.openDankLauncherV2()
popoutService.closeDankLauncherV2()
popoutService.toggleDankLauncherV2()
```
#### Power Menu Modal

View File

@@ -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,52 @@ 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, {
items: s.items ? s.items.slice() : []
});
});
_cachedDefaultFlatModel = flatModel.slice();
_defaultCacheValid = true;
}
function isCacheValid() {
return _defaultCacheValid;
}
function _rebuildHiddenSet() {
@@ -68,9 +120,18 @@ Singleton {
target: SessionData
function onHiddenAppsChanged() {
root._rebuildHiddenSet();
root.invalidateLauncherCache();
}
function onAppOverridesChanged() {
root._cachedVisibleApps = null;
root.invalidateLauncherCache();
}
}
Connections {
target: AppUsageHistoryData
function onAppUsageRankingChanged() {
root.invalidateLauncherCache();
}
}

View File

@@ -49,7 +49,7 @@ Singleton {
function setWorkspaces(newMap) {
root.workspaces = newMap;
allWorkspaces = Object.values(newMap).sort((a, b) => a.idx - b.idx);
root.allWorkspaces = Object.values(newMap).sort((a, b) => a.idx - b.idx);
}
Component.onCompleted: fetchOutputs()
@@ -863,9 +863,13 @@ Singleton {
return currentOutputWorkspaces.map(w => w.idx + 1);
}
function getCurrentOutputWorkspaces() {
return currentOutputWorkspaces.slice();
}
function getCurrentWorkspaceNumber() {
if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) {
return allWorkspaces[focusedWorkspaceIndex].idx + 1;
return allWorkspaces[focusedWorkspaceIndex].idx;
}
return 1;
}

View File

@@ -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];

View File

@@ -20,7 +20,8 @@ Singleton {
property var settingsModal: null
property var settingsModalLoader: null
property var clipboardHistoryModal: null
property var spotlightModal: null
property var dankLauncherV2Modal: null
property var dankLauncherV2ModalLoader: null
property var powerMenuModal: null
property var processListModal: null
property var processListModalLoader: null
@@ -353,12 +354,91 @@ Singleton {
clipboardHistoryModal?.close();
}
function openSpotlight() {
spotlightModal?.show();
property bool _dankLauncherV2WantsOpen: false
property bool _dankLauncherV2WantsToggle: false
property string _dankLauncherV2PendingQuery: ""
property string _dankLauncherV2PendingMode: ""
function openDankLauncherV2() {
if (dankLauncherV2Modal) {
dankLauncherV2Modal.show();
} else if (dankLauncherV2ModalLoader) {
_dankLauncherV2WantsOpen = true;
_dankLauncherV2WantsToggle = false;
dankLauncherV2ModalLoader.active = true;
}
}
function closeSpotlight() {
spotlightModal?.close();
function openDankLauncherV2WithQuery(query: string) {
if (dankLauncherV2Modal) {
dankLauncherV2Modal.showWithQuery(query);
} else if (dankLauncherV2ModalLoader) {
_dankLauncherV2PendingQuery = query;
_dankLauncherV2WantsOpen = true;
_dankLauncherV2WantsToggle = false;
dankLauncherV2ModalLoader.active = true;
}
}
function openDankLauncherV2WithMode(mode: string) {
if (dankLauncherV2Modal) {
dankLauncherV2Modal.showWithMode(mode);
} else if (dankLauncherV2ModalLoader) {
_dankLauncherV2PendingMode = mode;
_dankLauncherV2WantsOpen = true;
_dankLauncherV2WantsToggle = false;
dankLauncherV2ModalLoader.active = true;
}
}
function closeDankLauncherV2() {
dankLauncherV2Modal?.hide();
}
function toggleDankLauncherV2() {
if (dankLauncherV2Modal) {
dankLauncherV2Modal.toggle();
} else if (dankLauncherV2ModalLoader) {
_dankLauncherV2WantsToggle = true;
_dankLauncherV2WantsOpen = false;
dankLauncherV2ModalLoader.active = true;
}
}
function toggleDankLauncherV2WithMode(mode: string) {
if (dankLauncherV2Modal) {
dankLauncherV2Modal.toggleWithMode(mode);
} else if (dankLauncherV2ModalLoader) {
_dankLauncherV2PendingMode = mode;
_dankLauncherV2WantsToggle = true;
_dankLauncherV2WantsOpen = false;
dankLauncherV2ModalLoader.active = true;
}
}
function _onDankLauncherV2ModalLoaded() {
if (_dankLauncherV2WantsOpen) {
_dankLauncherV2WantsOpen = false;
if (_dankLauncherV2PendingQuery) {
dankLauncherV2Modal?.showWithQuery(_dankLauncherV2PendingQuery);
_dankLauncherV2PendingQuery = "";
} else if (_dankLauncherV2PendingMode) {
dankLauncherV2Modal?.showWithMode(_dankLauncherV2PendingMode);
_dankLauncherV2PendingMode = "";
} else {
dankLauncherV2Modal?.show();
}
return;
}
if (_dankLauncherV2WantsToggle) {
_dankLauncherV2WantsToggle = false;
if (_dankLauncherV2PendingMode) {
dankLauncherV2Modal?.toggleWithMode(_dankLauncherV2PendingMode);
_dankLauncherV2PendingMode = "";
} else {
dankLauncherV2Modal?.toggle();
}
}
}
function openPowerMenu() {

View File

@@ -26,8 +26,10 @@ Item {
readonly property bool isUnicode: iconValue.startsWith("unicode:")
readonly property bool isSvgCorner: iconValue.startsWith("svg+corner:")
readonly property bool isSvg: !isSvgCorner && iconValue.startsWith("svg:")
readonly property bool isImage: iconValue.startsWith("image:")
readonly property string materialName: isMaterial ? iconValue.substring(9) : ""
readonly property string unicodeChar: isUnicode ? iconValue.substring(8) : ""
readonly property string imagePath: isImage ? iconValue.substring(6) : ""
readonly property string svgSource: {
if (isSvgCorner) {
const parts = iconValue.substring(11).split("|");
@@ -38,7 +40,7 @@ Item {
return "";
}
readonly property string svgCornerIcon: isSvgCorner ? (iconValue.substring(11).split("|")[1] || "") : ""
readonly property string iconPath: isMaterial || isUnicode || isSvg || isSvgCorner ? "" : Quickshell.iconPath(iconValue, true) || DesktopService.resolveIconPath(iconValue)
readonly property string iconPath: isMaterial || isUnicode || isSvg || isSvgCorner || isImage ? "" : Quickshell.iconPath(iconValue, true) || DesktopService.resolveIconPath(iconValue)
visible: iconValue !== undefined && iconValue !== ""
@@ -66,14 +68,23 @@ Item {
visible: root.isSvg || root.isSvgCorner
}
CachingImage {
id: cachingImg
anchors.fill: parent
imagePath: root.imagePath
maxCacheSize: root.iconSize * 2
visible: root.isImage && status === Image.Ready
}
IconImage {
id: iconImg
anchors.fill: parent
source: root.iconPath
smooth: true
backer.sourceSize: Qt.size(root.iconSize, root.iconSize)
mipmap: true
asynchronous: true
visible: !root.isMaterial && !root.isUnicode && !root.isSvg && !root.isSvgCorner && root.iconPath !== "" && status === Image.Ready
visible: !root.isMaterial && !root.isUnicode && !root.isSvg && !root.isSvgCorner && !root.isImage && root.iconPath !== "" && status === Image.Ready
}
Rectangle {
@@ -84,7 +95,7 @@ Item {
anchors.rightMargin: root.fallbackRightMargin
anchors.topMargin: root.fallbackTopMargin
anchors.bottomMargin: root.fallbackBottomMargin
visible: !root.isMaterial && !root.isUnicode && !root.isSvg && !root.isSvgCorner && (root.iconPath === "" || iconImg.status !== Image.Ready)
visible: !root.isMaterial && !root.isUnicode && !root.isSvg && !root.isSvgCorner && !root.isImage && (root.iconPath === "" || iconImg.status !== Image.Ready)
color: root.fallbackBackgroundColor
radius: Theme.cornerRadius
border.width: 0

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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",