mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-25 05:52:50 -05:00
Compare commits
1 Commits
03cfa55e0b
...
chroma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1ecb5af70 |
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -42,12 +42,12 @@ body:
|
||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: dms_doctor
|
||||
- type: input
|
||||
id: dms_version
|
||||
attributes:
|
||||
label: dms doctor -v
|
||||
description: Output of `dms doctor -v` command
|
||||
placeholder: Paste the output of `dms doctor -v` here
|
||||
label: dms version
|
||||
description: Output of dms version command
|
||||
placeholder: e.g., 1.2.3
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/support_request.yml
vendored
10
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -27,12 +27,12 @@ body:
|
||||
placeholder: Your Linux distribution
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: dms_doctor
|
||||
- type: input
|
||||
id: dms_version
|
||||
attributes:
|
||||
label: dms doctor -v
|
||||
description: Output of `dms doctor -v` command
|
||||
placeholder: Paste the output of `dms doctor -v` here
|
||||
label: dms version
|
||||
description: Output of dms version command
|
||||
placeholder: e.g., 1.2.3
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
||||
@@ -6,8 +6,6 @@ This file is more of a quick reference so I know what to account for before next
|
||||
- dbus API for plugins, KDEConnect
|
||||
- new dank16 algorithm
|
||||
- launcher actions, customize env, args, name, icon
|
||||
- launcher v2 - omega stuff, GIF search, supa powerful
|
||||
- dock on bar
|
||||
|
||||
# 1.2.0
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
@@ -21,18 +20,10 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
chromaLanguage string
|
||||
chromaStyle string
|
||||
chromaInline bool
|
||||
chromaMarkdown bool
|
||||
)
|
||||
|
||||
var chromaCmd = &cobra.Command{
|
||||
@@ -80,83 +71,12 @@ 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
|
||||
@@ -164,20 +84,6 @@ func runChroma(cmd *cobra.Command, args []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)
|
||||
@@ -185,12 +91,6 @@ func runChroma(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
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)
|
||||
@@ -237,27 +137,18 @@ func runChroma(cmd *cobra.Command, args []string) {
|
||||
// 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
|
||||
})
|
||||
lexer = lexers.Get(chromaLanguage)
|
||||
if lexer == nil {
|
||||
fmt.Fprintf(os.Stderr, "Unknown language: %s\n", chromaLanguage)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else if filename != "" {
|
||||
lexer = getCachedLexer("file:"+filename, func() chroma.Lexer {
|
||||
return lexers.Match(filename)
|
||||
})
|
||||
lexer = lexers.Match(filename)
|
||||
}
|
||||
|
||||
// Try content analysis if no lexer found (limit to first 1KB for performance)
|
||||
// Try content analysis if no lexer found
|
||||
if lexer == nil {
|
||||
analyzeContent := source
|
||||
if len(source) > 1024 {
|
||||
analyzeContent = source[:1024]
|
||||
}
|
||||
lexer = lexers.Analyse(analyzeContent)
|
||||
lexer = lexers.Analyse(source)
|
||||
}
|
||||
|
||||
// Fallback to plaintext
|
||||
@@ -267,11 +158,25 @@ func runChroma(cmd *cobra.Command, args []string) {
|
||||
|
||||
lexer = chroma.Coalesce(lexer)
|
||||
|
||||
// Get cached style
|
||||
style := getCachedStyle(chromaStyle)
|
||||
// Get style
|
||||
style := styles.Get(chromaStyle)
|
||||
if style == nil {
|
||||
style = styles.Fallback
|
||||
}
|
||||
|
||||
// Get cached formatter
|
||||
formatter := getCachedFormatter(chromaInline, chromaLineNumbers)
|
||||
// Create HTML formatter
|
||||
var formatter *html.Formatter
|
||||
if chromaInline {
|
||||
formatter = html.New(
|
||||
html.WithClasses(false),
|
||||
html.TabWidth(4),
|
||||
)
|
||||
} else {
|
||||
formatter = html.New(
|
||||
html.WithClasses(true),
|
||||
html.TabWidth(4),
|
||||
)
|
||||
}
|
||||
|
||||
// Tokenize
|
||||
iterator, err := lexer.Tokenise(nil, source)
|
||||
@@ -281,20 +186,8 @@ func runChroma(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
if err := formatter.Format(os.Stdout, style, iterator); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,9 @@ var killCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var ipcCmd = &cobra.Command{
|
||||
Use: "ipc [target] [function] [args...]",
|
||||
Use: "ipc",
|
||||
Short: "Send IPC commands to running DMS shell",
|
||||
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
|
||||
PreRunE: findConfig,
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
_ = findConfig(cmd, args)
|
||||
@@ -76,13 +77,6 @@ var ipcCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||
_ = findConfig(cmd, args)
|
||||
printIPCHelp()
|
||||
})
|
||||
}
|
||||
|
||||
var debugSrvCmd = &cobra.Command{
|
||||
Use: "debug-srv",
|
||||
Short: "Start the debug server",
|
||||
@@ -521,8 +515,8 @@ func getCommonCommands() []*cobra.Command {
|
||||
genericNotifyActionCmd,
|
||||
matugenCmd,
|
||||
clipboardCmd,
|
||||
chromaCmd,
|
||||
doctorCmd,
|
||||
configCmd,
|
||||
chromaCmd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/dms"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -18,9 +20,11 @@ var rootCmd = &cobra.Command{
|
||||
Use: "dms",
|
||||
Short: "dms CLI",
|
||||
Long: "dms is the DankMaterialShell management CLI and backend server.",
|
||||
Run: runInteractiveMode,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Add the -c flag
|
||||
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
||||
}
|
||||
|
||||
@@ -34,7 +38,7 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
||||
if statErr == nil && !info.IsDir() {
|
||||
configPath = customConfigPath
|
||||
log.Debug("Using config from: %s", configPath)
|
||||
return nil
|
||||
return nil // <-- Guard statement
|
||||
}
|
||||
|
||||
if statErr != nil {
|
||||
@@ -72,3 +76,18 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
||||
log.Debug("Using config from: %s", configPath)
|
||||
return nil
|
||||
}
|
||||
func runInteractiveMode(cmd *cobra.Command, args []string) {
|
||||
detector, _ := dms.NewDetector()
|
||||
|
||||
if !detector.IsDMSInstalled() {
|
||||
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")
|
||||
log.Info("Please install DMS using dankinstall before using this management interface.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
model := dms.NewModel(Version)
|
||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||
if _, err := p.Run(); err != nil {
|
||||
log.Fatalf("Error running program: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,8 +618,9 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
||||
|
||||
func runShellIPCCommand(args []string) {
|
||||
if len(args) == 0 {
|
||||
printIPCHelp()
|
||||
return
|
||||
log.Error("IPC command requires arguments")
|
||||
log.Info("Usage: dms ipc <command> [args...]")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if args[0] != "call" {
|
||||
@@ -641,45 +642,3 @@ func runShellIPCCommand(args []string) {
|
||||
log.Fatalf("Error running IPC command: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func printIPCHelp() {
|
||||
fmt.Println("Usage: dms ipc <target> <function> [args...]")
|
||||
fmt.Println()
|
||||
|
||||
cmdArgs := []string{"ipc"}
|
||||
if qsHasAnyDisplay() {
|
||||
cmdArgs = append(cmdArgs, "--any-display")
|
||||
}
|
||||
cmdArgs = append(cmdArgs, "-p", configPath, "show")
|
||||
cmd := exec.Command("qs", cmdArgs...)
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
fmt.Println("Could not retrieve available IPC targets (is DMS running?)")
|
||||
return
|
||||
}
|
||||
|
||||
targets := parseTargetsFromIPCShowOutput(string(output))
|
||||
if len(targets) == 0 {
|
||||
fmt.Println("No IPC targets available")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Targets:")
|
||||
|
||||
targetNames := make([]string, 0, len(targets))
|
||||
for name := range targets {
|
||||
targetNames = append(targetNames, name)
|
||||
}
|
||||
slices.Sort(targetNames)
|
||||
|
||||
for _, targetName := range targetNames {
|
||||
funcs := targets[targetName]
|
||||
funcNames := make([]string, 0, len(funcs))
|
||||
for fn := range funcs {
|
||||
funcNames = append(funcNames, fn)
|
||||
}
|
||||
slices.Sort(funcNames)
|
||||
fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ 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
|
||||
@@ -42,6 +40,8 @@ require (
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/yuin/goldmark v1.7.16 // indirect
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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 and is not empty to preserve user modifications
|
||||
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||
// Skip if file already exists to preserve user modifications
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||
continue
|
||||
}
|
||||
@@ -567,8 +567,7 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
|
||||
|
||||
for _, cfg := range configs {
|
||||
path := filepath.Join(dmsDir, cfg.name)
|
||||
// Skip if file already exists and is not empty to preserve user modifications
|
||||
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -91,9 +91,6 @@ bind = SUPER CTRL, up, movetoworkspace, e-1
|
||||
bind = SUPER CTRL, U, movetoworkspace, e+1
|
||||
bind = SUPER CTRL, I, movetoworkspace, e-1
|
||||
|
||||
# === Workspace Management ===
|
||||
bind = CTRL SHIFT, R, exec, dms ipc call workspace-rename open
|
||||
|
||||
# === Move Workspaces ===
|
||||
bind = SUPER SHIFT, Page_Down, movetoworkspace, e+1
|
||||
bind = SUPER SHIFT, Page_Up, movetoworkspace, e-1
|
||||
|
||||
@@ -133,11 +133,6 @@ binds {
|
||||
Mod+Ctrl+U { move-column-to-workspace-down; }
|
||||
Mod+Ctrl+I { move-column-to-workspace-up; }
|
||||
|
||||
// === Workspace Management ===
|
||||
Ctrl+Shift+R hotkey-overlay-title="Rename Workspace" {
|
||||
spawn "dms" "ipc" "call" "workspace-rename" "open";
|
||||
}
|
||||
|
||||
// === Move Workspaces ===
|
||||
Mod+Shift+Page_Down { move-workspace-down; }
|
||||
Mod+Shift+Page_Up { move-workspace-up; }
|
||||
|
||||
@@ -41,9 +41,6 @@ func init() {
|
||||
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
Register("XeroLinux", "#888fe2", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type ArchDistribution struct {
|
||||
|
||||
450
core/internal/dms/app.go
Normal file
450
core/internal/dms/app.go
Normal file
@@ -0,0 +1,450 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type AppState int
|
||||
|
||||
const (
|
||||
StateMainMenu AppState = iota
|
||||
StateUpdate
|
||||
StateUpdatePassword
|
||||
StateUpdateProgress
|
||||
StateShell
|
||||
StatePluginsMenu
|
||||
StatePluginsBrowse
|
||||
StatePluginDetail
|
||||
StatePluginSearch
|
||||
StatePluginsInstalled
|
||||
StatePluginInstalledDetail
|
||||
StateGreeterMenu
|
||||
StateGreeterCompositorSelect
|
||||
StateGreeterPassword
|
||||
StateGreeterInstalling
|
||||
StateAbout
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
version string
|
||||
detector *Detector
|
||||
dependencies []DependencyInfo
|
||||
state AppState
|
||||
selectedItem int
|
||||
width int
|
||||
height int
|
||||
|
||||
// Menu items
|
||||
menuItems []MenuItem
|
||||
|
||||
updateDeps []DependencyInfo
|
||||
selectedUpdateDep int
|
||||
updateToggles map[string]bool
|
||||
|
||||
updateProgressChan chan updateProgressMsg
|
||||
updateProgress updateProgressMsg
|
||||
updateLogs []string
|
||||
sudoPassword string
|
||||
passwordInput string
|
||||
passwordError string
|
||||
|
||||
// Window manager states
|
||||
hyprlandInstalled bool
|
||||
niriInstalled bool
|
||||
|
||||
selectedGreeterItem int
|
||||
greeterInstallChan chan greeterProgressMsg
|
||||
greeterProgress greeterProgressMsg
|
||||
greeterLogs []string
|
||||
greeterPasswordInput string
|
||||
greeterPasswordError string
|
||||
greeterSudoPassword string
|
||||
greeterCompositors []string
|
||||
greeterSelectedComp int
|
||||
greeterChosenCompositor string
|
||||
|
||||
pluginsMenuItems []MenuItem
|
||||
selectedPluginsMenuItem int
|
||||
pluginsList []pluginInfo
|
||||
filteredPluginsList []pluginInfo
|
||||
selectedPluginIndex int
|
||||
pluginsLoading bool
|
||||
pluginsError string
|
||||
pluginSearchQuery string
|
||||
installedPluginsList []pluginInfo
|
||||
selectedInstalledIndex int
|
||||
installedPluginsLoading bool
|
||||
installedPluginsError string
|
||||
pluginInstallStatus map[string]bool
|
||||
}
|
||||
|
||||
type pluginInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Category string
|
||||
Author string
|
||||
Description string
|
||||
Repo string
|
||||
Path string
|
||||
Capabilities []string
|
||||
Compositors []string
|
||||
Dependencies []string
|
||||
FirstParty bool
|
||||
}
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
Action AppState
|
||||
}
|
||||
|
||||
func NewModel(version string) Model {
|
||||
detector, _ := NewDetector()
|
||||
var dependencies []DependencyInfo
|
||||
var hyprlandInstalled, niriInstalled bool
|
||||
var err error
|
||||
if detector != nil {
|
||||
dependencies = detector.GetInstalledComponents()
|
||||
|
||||
// Use the proper detection method for both window managers
|
||||
hyprlandInstalled, niriInstalled, err = detector.GetWindowManagerStatus()
|
||||
if err != nil {
|
||||
// Fallback to false if detection fails
|
||||
hyprlandInstalled = false
|
||||
niriInstalled = false
|
||||
}
|
||||
}
|
||||
|
||||
updateToggles := make(map[string]bool)
|
||||
for _, dep := range dependencies {
|
||||
if dep.Name == "dms (DankMaterialShell)" && dep.Status == deps.StatusNeedsUpdate {
|
||||
updateToggles[dep.Name] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
m := Model{
|
||||
version: version,
|
||||
detector: detector,
|
||||
dependencies: dependencies,
|
||||
state: StateMainMenu,
|
||||
selectedItem: 0,
|
||||
updateToggles: updateToggles,
|
||||
updateDeps: dependencies,
|
||||
updateProgressChan: make(chan updateProgressMsg, 100),
|
||||
hyprlandInstalled: hyprlandInstalled,
|
||||
niriInstalled: niriInstalled,
|
||||
greeterInstallChan: make(chan greeterProgressMsg, 100),
|
||||
pluginInstallStatus: make(map[string]bool),
|
||||
}
|
||||
|
||||
m.menuItems = m.buildMenuItems()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) buildMenuItems() []MenuItem {
|
||||
items := []MenuItem{
|
||||
{Label: "Update", Action: StateUpdate},
|
||||
}
|
||||
|
||||
// Shell management
|
||||
if m.isShellRunning() {
|
||||
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
|
||||
} else {
|
||||
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
|
||||
}
|
||||
|
||||
// Plugins management
|
||||
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
|
||||
|
||||
// Greeter management
|
||||
items = append(items, MenuItem{Label: "Greeter", Action: StateGreeterMenu})
|
||||
|
||||
items = append(items, MenuItem{Label: "About", Action: StateAbout})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (m *Model) buildPluginsMenuItems() []MenuItem {
|
||||
return []MenuItem{
|
||||
{Label: "Browse Plugins", Action: StatePluginsBrowse},
|
||||
{Label: "View Installed", Action: StatePluginsInstalled},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) isShellRunning() bool {
|
||||
// Check for both -c and -p flag patterns since quickshell can be started either way
|
||||
// -c dms: config name mode
|
||||
// -p <path>/dms: path mode (used when installed via system packages)
|
||||
cmd := exec.Command("pgrep", "-f", "qs.*dms")
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
case shellStartedMsg:
|
||||
m.menuItems = m.buildMenuItems()
|
||||
if m.selectedItem >= len(m.menuItems) {
|
||||
m.selectedItem = len(m.menuItems) - 1
|
||||
}
|
||||
return m, nil
|
||||
case updateProgressMsg:
|
||||
m.updateProgress = msg
|
||||
if msg.logOutput != "" {
|
||||
m.updateLogs = append(m.updateLogs, msg.logOutput)
|
||||
}
|
||||
return m, m.waitForProgress()
|
||||
case updateCompleteMsg:
|
||||
m.updateProgress.complete = true
|
||||
m.updateProgress.err = msg.err
|
||||
m.dependencies = m.detector.GetInstalledComponents()
|
||||
m.updateDeps = m.dependencies
|
||||
m.menuItems = m.buildMenuItems()
|
||||
|
||||
// Restart shell if update was successful and shell is running
|
||||
if msg.err == nil && m.isShellRunning() {
|
||||
restartShell()
|
||||
}
|
||||
return m, nil
|
||||
case greeterProgressMsg:
|
||||
m.greeterProgress = msg
|
||||
if msg.logOutput != "" {
|
||||
m.greeterLogs = append(m.greeterLogs, msg.logOutput)
|
||||
}
|
||||
return m, m.waitForGreeterProgress()
|
||||
case pluginsLoadedMsg:
|
||||
m.pluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.pluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
m.updatePluginInstallStatus()
|
||||
}
|
||||
return m, nil
|
||||
case installedPluginsLoadedMsg:
|
||||
m.installedPluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.installedPluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.selectedInstalledIndex = 0
|
||||
}
|
||||
return m, nil
|
||||
case pluginUninstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
m.state = StatePluginInstalledDetail
|
||||
} else {
|
||||
m.state = StatePluginsInstalled
|
||||
m.installedPluginsLoading = true
|
||||
m.installedPluginsError = ""
|
||||
return m, loadInstalledPlugins
|
||||
}
|
||||
return m, nil
|
||||
case pluginUpdatedMsg:
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.installedPluginsError = ""
|
||||
}
|
||||
return m, nil
|
||||
case pluginInstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginInstallStatus[msg.pluginName] = true
|
||||
m.pluginsError = ""
|
||||
}
|
||||
return m, nil
|
||||
case greeterPasswordValidMsg:
|
||||
if msg.valid {
|
||||
m.greeterSudoPassword = msg.password
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
m.state = StateGreeterInstalling
|
||||
m.greeterProgress = greeterProgressMsg{step: "Starting greeter installation..."}
|
||||
m.greeterLogs = []string{}
|
||||
return m, tea.Batch(m.performGreeterInstall(), m.waitForGreeterProgress())
|
||||
} else {
|
||||
m.greeterPasswordError = "Incorrect password. Please try again."
|
||||
m.greeterPasswordInput = ""
|
||||
}
|
||||
return m, nil
|
||||
case passwordValidMsg:
|
||||
if msg.valid {
|
||||
m.sudoPassword = msg.password
|
||||
m.passwordInput = ""
|
||||
m.passwordError = ""
|
||||
m.state = StateUpdateProgress
|
||||
m.updateProgress = updateProgressMsg{progress: 0.0, step: "Starting update..."}
|
||||
m.updateLogs = []string{}
|
||||
return m, tea.Batch(m.performUpdate(), m.waitForProgress())
|
||||
} else {
|
||||
m.passwordError = "Incorrect password. Please try again."
|
||||
m.passwordInput = ""
|
||||
}
|
||||
return m, nil
|
||||
case tea.KeyMsg:
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.updateMainMenu(msg)
|
||||
case StateUpdate:
|
||||
return m.updateUpdateView(msg)
|
||||
case StateUpdatePassword:
|
||||
return m.updatePasswordView(msg)
|
||||
case StateUpdateProgress:
|
||||
return m.updateProgressView(msg)
|
||||
case StateShell:
|
||||
return m.updateShellView(msg)
|
||||
case StatePluginsMenu:
|
||||
return m.updatePluginsMenu(msg)
|
||||
case StatePluginsBrowse:
|
||||
return m.updatePluginsBrowse(msg)
|
||||
case StatePluginDetail:
|
||||
return m.updatePluginDetail(msg)
|
||||
case StatePluginSearch:
|
||||
return m.updatePluginSearch(msg)
|
||||
case StatePluginsInstalled:
|
||||
return m.updatePluginsInstalled(msg)
|
||||
case StatePluginInstalledDetail:
|
||||
return m.updatePluginInstalledDetail(msg)
|
||||
case StateGreeterMenu:
|
||||
return m.updateGreeterMenu(msg)
|
||||
case StateGreeterCompositorSelect:
|
||||
return m.updateGreeterCompositorSelect(msg)
|
||||
case StateGreeterPassword:
|
||||
return m.updateGreeterPasswordView(msg)
|
||||
case StateGreeterInstalling:
|
||||
return m.updateGreeterInstalling(msg)
|
||||
case StateAbout:
|
||||
return m.updateAboutView(msg)
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type updateProgressMsg struct {
|
||||
progress float64
|
||||
step string
|
||||
complete bool
|
||||
err error
|
||||
logOutput string
|
||||
}
|
||||
|
||||
type updateCompleteMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type passwordValidMsg struct {
|
||||
password string
|
||||
valid bool
|
||||
}
|
||||
|
||||
type greeterProgressMsg struct {
|
||||
step string
|
||||
complete bool
|
||||
err error
|
||||
logOutput string
|
||||
}
|
||||
|
||||
type greeterPasswordValidMsg struct {
|
||||
password string
|
||||
valid bool
|
||||
}
|
||||
|
||||
func (m Model) waitForProgress() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return <-m.updateProgressChan
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) waitForGreeterProgress() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return <-m.greeterInstallChan
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.renderMainMenu()
|
||||
case StateUpdate:
|
||||
return m.renderUpdateView()
|
||||
case StateUpdatePassword:
|
||||
return m.renderPasswordView()
|
||||
case StateUpdateProgress:
|
||||
return m.renderProgressView()
|
||||
case StateShell:
|
||||
return m.renderShellView()
|
||||
case StatePluginsMenu:
|
||||
return m.renderPluginsMenu()
|
||||
case StatePluginsBrowse:
|
||||
return m.renderPluginsBrowse()
|
||||
case StatePluginDetail:
|
||||
return m.renderPluginDetail()
|
||||
case StatePluginSearch:
|
||||
return m.renderPluginSearch()
|
||||
case StatePluginsInstalled:
|
||||
return m.renderPluginsInstalled()
|
||||
case StatePluginInstalledDetail:
|
||||
return m.renderPluginInstalledDetail()
|
||||
case StateGreeterMenu:
|
||||
return m.renderGreeterMenu()
|
||||
case StateGreeterCompositorSelect:
|
||||
return m.renderGreeterCompositorSelect()
|
||||
case StateGreeterPassword:
|
||||
return m.renderGreeterPasswordView()
|
||||
case StateGreeterInstalling:
|
||||
return m.renderGreeterInstalling()
|
||||
case StateAbout:
|
||||
return m.renderAboutView()
|
||||
default:
|
||||
return m.renderMainMenu()
|
||||
}
|
||||
}
|
||||
267
core/internal/dms/app_distro.go
Normal file
267
core/internal/dms/app_distro.go
Normal file
@@ -0,0 +1,267 @@
|
||||
//go:build distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type AppState int
|
||||
|
||||
const (
|
||||
StateMainMenu AppState = iota
|
||||
StateShell
|
||||
StatePluginsMenu
|
||||
StatePluginsBrowse
|
||||
StatePluginDetail
|
||||
StatePluginSearch
|
||||
StatePluginsInstalled
|
||||
StatePluginInstalledDetail
|
||||
StateAbout
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
version string
|
||||
detector *Detector
|
||||
dependencies []DependencyInfo
|
||||
state AppState
|
||||
selectedItem int
|
||||
width int
|
||||
height int
|
||||
|
||||
// Menu items
|
||||
menuItems []MenuItem
|
||||
|
||||
// Window manager states
|
||||
hyprlandInstalled bool
|
||||
niriInstalled bool
|
||||
|
||||
pluginsMenuItems []MenuItem
|
||||
selectedPluginsMenuItem int
|
||||
pluginsList []pluginInfo
|
||||
filteredPluginsList []pluginInfo
|
||||
selectedPluginIndex int
|
||||
pluginsLoading bool
|
||||
pluginsError string
|
||||
pluginSearchQuery string
|
||||
installedPluginsList []pluginInfo
|
||||
selectedInstalledIndex int
|
||||
installedPluginsLoading bool
|
||||
installedPluginsError string
|
||||
pluginInstallStatus map[string]bool
|
||||
}
|
||||
|
||||
type pluginInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Category string
|
||||
Author string
|
||||
Description string
|
||||
Repo string
|
||||
Path string
|
||||
Capabilities []string
|
||||
Compositors []string
|
||||
Dependencies []string
|
||||
FirstParty bool
|
||||
}
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
Action AppState
|
||||
}
|
||||
|
||||
func NewModel(version string) Model {
|
||||
detector, _ := NewDetector()
|
||||
|
||||
var dependencies []DependencyInfo
|
||||
var hyprlandInstalled, niriInstalled bool
|
||||
|
||||
if detector != nil {
|
||||
dependencies = detector.GetInstalledComponents()
|
||||
hyprlandInstalled, niriInstalled, _ = detector.GetWindowManagerStatus()
|
||||
}
|
||||
|
||||
m := Model{
|
||||
version: version,
|
||||
detector: detector,
|
||||
dependencies: dependencies,
|
||||
state: StateMainMenu,
|
||||
selectedItem: 0,
|
||||
hyprlandInstalled: hyprlandInstalled,
|
||||
niriInstalled: niriInstalled,
|
||||
pluginInstallStatus: make(map[string]bool),
|
||||
}
|
||||
|
||||
m.menuItems = m.buildMenuItems()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) buildMenuItems() []MenuItem {
|
||||
items := []MenuItem{}
|
||||
|
||||
// Shell management
|
||||
if m.isShellRunning() {
|
||||
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
|
||||
} else {
|
||||
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
|
||||
}
|
||||
|
||||
// Plugins management
|
||||
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
|
||||
|
||||
items = append(items, MenuItem{Label: "About", Action: StateAbout})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (m *Model) buildPluginsMenuItems() []MenuItem {
|
||||
return []MenuItem{
|
||||
{Label: "Browse Plugins", Action: StatePluginsBrowse},
|
||||
{Label: "View Installed", Action: StatePluginsInstalled},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) isShellRunning() bool {
|
||||
cmd := exec.Command("pgrep", "-f", "qs -c dms")
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
case pluginsLoadedMsg:
|
||||
m.pluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.pluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
m.updatePluginInstallStatus()
|
||||
}
|
||||
return m, nil
|
||||
case installedPluginsLoadedMsg:
|
||||
m.installedPluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.installedPluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.selectedInstalledIndex = 0
|
||||
}
|
||||
return m, nil
|
||||
case pluginUninstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
m.state = StatePluginInstalledDetail
|
||||
} else {
|
||||
m.state = StatePluginsInstalled
|
||||
m.installedPluginsLoading = true
|
||||
m.installedPluginsError = ""
|
||||
return m, loadInstalledPlugins
|
||||
}
|
||||
return m, nil
|
||||
case pluginUpdatedMsg:
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.installedPluginsError = ""
|
||||
}
|
||||
return m, nil
|
||||
case pluginInstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginInstallStatus[msg.pluginName] = true
|
||||
m.pluginsError = ""
|
||||
}
|
||||
return m, nil
|
||||
case tea.KeyMsg:
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.updateMainMenu(msg)
|
||||
case StateShell:
|
||||
return m.updateShellView(msg)
|
||||
case StatePluginsMenu:
|
||||
return m.updatePluginsMenu(msg)
|
||||
case StatePluginsBrowse:
|
||||
return m.updatePluginsBrowse(msg)
|
||||
case StatePluginDetail:
|
||||
return m.updatePluginDetail(msg)
|
||||
case StatePluginSearch:
|
||||
return m.updatePluginSearch(msg)
|
||||
case StatePluginsInstalled:
|
||||
return m.updatePluginsInstalled(msg)
|
||||
case StatePluginInstalledDetail:
|
||||
return m.updatePluginInstalledDetail(msg)
|
||||
case StateAbout:
|
||||
return m.updateAboutView(msg)
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.renderMainMenu()
|
||||
case StateShell:
|
||||
return m.renderShellView()
|
||||
case StatePluginsMenu:
|
||||
return m.renderPluginsMenu()
|
||||
case StatePluginsBrowse:
|
||||
return m.renderPluginsBrowse()
|
||||
case StatePluginDetail:
|
||||
return m.renderPluginDetail()
|
||||
case StatePluginSearch:
|
||||
return m.renderPluginSearch()
|
||||
case StatePluginsInstalled:
|
||||
return m.renderPluginsInstalled()
|
||||
case StatePluginInstalledDetail:
|
||||
return m.renderPluginInstalledDetail()
|
||||
case StateAbout:
|
||||
return m.renderAboutView()
|
||||
default:
|
||||
return m.renderMainMenu()
|
||||
}
|
||||
}
|
||||
143
core/internal/dms/detector.go
Normal file
143
core/internal/dms/detector.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
)
|
||||
|
||||
type Detector struct {
|
||||
homeDir string
|
||||
distribution distros.Distribution
|
||||
}
|
||||
|
||||
func (d *Detector) GetDistribution() distros.Distribution {
|
||||
return d.distribution
|
||||
}
|
||||
|
||||
func NewDetector() (*Detector, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
go func() {
|
||||
for range logChan {
|
||||
}
|
||||
}()
|
||||
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dist, err := distros.NewDistribution(osInfo.Distribution.ID, logChan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Detector{
|
||||
homeDir: homeDir,
|
||||
distribution: dist,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Detector) IsDMSInstalled() bool {
|
||||
_, err := config.LocateDMSConfig()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *Detector) GetDependencyStatus() ([]deps.Dependency, error) {
|
||||
hyprlandDeps, err := d.distribution.DetectDependencies(context.Background(), deps.WindowManagerHyprland)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
niriDeps, err := d.distribution.DetectDependencies(context.Background(), deps.WindowManagerNiri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Combine dependencies and deduplicate
|
||||
depMap := make(map[string]deps.Dependency)
|
||||
|
||||
for _, dep := range hyprlandDeps {
|
||||
depMap[dep.Name] = dep
|
||||
}
|
||||
|
||||
for _, dep := range niriDeps {
|
||||
// If dependency already exists, keep the one that's installed or needs update
|
||||
if existing, exists := depMap[dep.Name]; exists {
|
||||
if dep.Status > existing.Status {
|
||||
depMap[dep.Name] = dep
|
||||
}
|
||||
} else {
|
||||
depMap[dep.Name] = dep
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map back to slice
|
||||
var allDeps []deps.Dependency
|
||||
for _, dep := range depMap {
|
||||
allDeps = append(allDeps, dep)
|
||||
}
|
||||
|
||||
return allDeps, nil
|
||||
}
|
||||
|
||||
func (d *Detector) GetWindowManagerStatus() (bool, bool, error) {
|
||||
// Reuse the existing command detection logic from BaseDistribution
|
||||
// Since all distros embed BaseDistribution, we can access it via interface
|
||||
type CommandChecker interface {
|
||||
CommandExists(string) bool
|
||||
}
|
||||
|
||||
checker, ok := d.distribution.(CommandChecker)
|
||||
if !ok {
|
||||
// Fallback to direct command check if interface not available
|
||||
hyprlandInstalled := d.commandExists("hyprland") || d.commandExists("Hyprland")
|
||||
niriInstalled := d.commandExists("niri")
|
||||
return hyprlandInstalled, niriInstalled, nil
|
||||
}
|
||||
|
||||
hyprlandInstalled := checker.CommandExists("hyprland") || checker.CommandExists("Hyprland")
|
||||
niriInstalled := checker.CommandExists("niri")
|
||||
|
||||
return hyprlandInstalled, niriInstalled, nil
|
||||
}
|
||||
|
||||
func (d *Detector) commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *Detector) GetInstalledComponents() []DependencyInfo {
|
||||
dependencies, err := d.GetDependencyStatus()
|
||||
if err != nil {
|
||||
return []DependencyInfo{}
|
||||
}
|
||||
|
||||
var components []DependencyInfo
|
||||
for _, dep := range dependencies {
|
||||
components = append(components, DependencyInfo{
|
||||
Name: dep.Name,
|
||||
Status: dep.Status,
|
||||
Description: dep.Description,
|
||||
Required: dep.Required,
|
||||
})
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
type DependencyInfo struct {
|
||||
Name string
|
||||
Status deps.DependencyStatus
|
||||
Description string
|
||||
Required bool
|
||||
}
|
||||
54
core/internal/dms/handlers_common.go
Normal file
54
core/internal/dms/handlers_common.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) updateShellView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
default:
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateAboutView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
if msg.String() == "esc" {
|
||||
m.state = StateMainMenu
|
||||
} else {
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func terminateShell() {
|
||||
patterns := []string{"dms run", "qs -c dms"}
|
||||
for _, pattern := range patterns {
|
||||
cmd := exec.Command("pkill", "-f", pattern)
|
||||
cmd.Run()
|
||||
}
|
||||
}
|
||||
|
||||
func startShellDaemon() {
|
||||
cmd := exec.Command("dms", "run", "-d")
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Errorf("Error starting daemon: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func restartShell() {
|
||||
terminateShell()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
startShellDaemon()
|
||||
}
|
||||
392
core/internal/dms/handlers_features.go
Normal file
392
core/internal/dms/handlers_features.go
Normal file
@@ -0,0 +1,392 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) updateUpdateView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
filteredDeps := m.getFilteredDeps()
|
||||
maxIndex := len(filteredDeps) - 1
|
||||
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
case "up", "k":
|
||||
if m.selectedUpdateDep > 0 {
|
||||
m.selectedUpdateDep--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedUpdateDep < maxIndex {
|
||||
m.selectedUpdateDep++
|
||||
}
|
||||
case " ":
|
||||
if dep := m.getDepAtVisualIndex(m.selectedUpdateDep); dep != nil {
|
||||
m.updateToggles[dep.Name] = !m.updateToggles[dep.Name]
|
||||
}
|
||||
case "enter":
|
||||
hasSelected := false
|
||||
for _, toggle := range m.updateToggles {
|
||||
if toggle {
|
||||
hasSelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasSelected {
|
||||
m.state = StateMainMenu
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.state = StateUpdatePassword
|
||||
m.passwordInput = ""
|
||||
m.passwordError = ""
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePasswordView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateUpdate
|
||||
m.passwordInput = ""
|
||||
m.passwordError = ""
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.passwordInput == "" {
|
||||
return m, nil
|
||||
}
|
||||
return m, m.validatePassword(m.passwordInput)
|
||||
case "backspace":
|
||||
if len(m.passwordInput) > 0 {
|
||||
m.passwordInput = m.passwordInput[:len(m.passwordInput)-1]
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 && msg.String()[0] >= 32 && msg.String()[0] <= 126 {
|
||||
m.passwordInput += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateProgressView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
if m.updateProgress.complete {
|
||||
m.state = StateMainMenu
|
||||
m.updateProgress = updateProgressMsg{}
|
||||
m.updateLogs = []string{}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) validatePassword(password string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
fmt.Fprintf(stdin, "%s\n", password)
|
||||
}()
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
outputStr := string(output)
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(outputStr, "Sorry, try again") ||
|
||||
strings.Contains(outputStr, "incorrect password") ||
|
||||
strings.Contains(outputStr, "authentication failure") {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
return passwordValidMsg{password: password, valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) performUpdate() tea.Cmd {
|
||||
var depsToUpdate []deps.Dependency
|
||||
|
||||
for _, depInfo := range m.updateDeps {
|
||||
if m.updateToggles[depInfo.Name] {
|
||||
depsToUpdate = append(depsToUpdate, deps.Dependency{
|
||||
Name: depInfo.Name,
|
||||
Status: depInfo.Status,
|
||||
Description: depInfo.Description,
|
||||
Required: depInfo.Required,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(depsToUpdate) == 0 {
|
||||
return func() tea.Msg {
|
||||
return updateCompleteMsg{err: nil}
|
||||
}
|
||||
}
|
||||
|
||||
wm := deps.WindowManagerHyprland
|
||||
if m.niriInstalled {
|
||||
wm = deps.WindowManagerNiri
|
||||
}
|
||||
|
||||
sudoPassword := m.sudoPassword
|
||||
reinstallFlags := make(map[string]bool)
|
||||
for name, toggled := range m.updateToggles {
|
||||
if toggled {
|
||||
reinstallFlags[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
distribution := m.detector.GetDistribution()
|
||||
progressChan := m.updateProgressChan
|
||||
|
||||
return func() tea.Msg {
|
||||
installerChan := make(chan distros.InstallProgressMsg, 100)
|
||||
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
disabledFlags := make(map[string]bool)
|
||||
err := distribution.InstallPackages(ctx, depsToUpdate, wm, sudoPassword, reinstallFlags, disabledFlags, false, installerChan)
|
||||
close(installerChan)
|
||||
|
||||
if err != nil {
|
||||
progressChan <- updateProgressMsg{complete: true, err: err}
|
||||
} else {
|
||||
progressChan <- updateProgressMsg{complete: true}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for msg := range installerChan {
|
||||
progressChan <- updateProgressMsg{
|
||||
progress: msg.Progress,
|
||||
step: msg.Step,
|
||||
complete: msg.IsComplete,
|
||||
err: msg.Error,
|
||||
logOutput: msg.LogOutput,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
greeterMenuItems := []string{"Install Greeter"}
|
||||
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
case "up", "k":
|
||||
if m.selectedGreeterItem > 0 {
|
||||
m.selectedGreeterItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedGreeterItem < len(greeterMenuItems)-1 {
|
||||
m.selectedGreeterItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedGreeterItem == 0 {
|
||||
compositors := greeter.DetectCompositors()
|
||||
if len(compositors) == 0 {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.greeterCompositors = compositors
|
||||
|
||||
if len(compositors) > 1 {
|
||||
m.state = StateGreeterCompositorSelect
|
||||
m.greeterSelectedComp = 0
|
||||
return m, nil
|
||||
} else {
|
||||
m.greeterChosenCompositor = compositors[0]
|
||||
m.state = StateGreeterPassword
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterCompositorSelect(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateGreeterMenu
|
||||
return m, nil
|
||||
case "up", "k":
|
||||
if m.greeterSelectedComp > 0 {
|
||||
m.greeterSelectedComp--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.greeterSelectedComp < len(m.greeterCompositors)-1 {
|
||||
m.greeterSelectedComp++
|
||||
}
|
||||
case "enter", " ":
|
||||
m.greeterChosenCompositor = m.greeterCompositors[m.greeterSelectedComp]
|
||||
m.state = StateGreeterPassword
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterPasswordView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateGreeterMenu
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.greeterPasswordInput == "" {
|
||||
return m, nil
|
||||
}
|
||||
return m, m.validateGreeterPassword(m.greeterPasswordInput)
|
||||
case "backspace":
|
||||
if len(m.greeterPasswordInput) > 0 {
|
||||
m.greeterPasswordInput = m.greeterPasswordInput[:len(m.greeterPasswordInput)-1]
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 && msg.String()[0] >= 32 && msg.String()[0] <= 126 {
|
||||
m.greeterPasswordInput += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterInstalling(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
if m.greeterProgress.complete {
|
||||
m.state = StateMainMenu
|
||||
m.greeterProgress = greeterProgressMsg{}
|
||||
m.greeterLogs = []string{}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) performGreeterInstall() tea.Cmd {
|
||||
progressChan := m.greeterInstallChan
|
||||
sudoPassword := m.greeterSudoPassword
|
||||
compositor := m.greeterChosenCompositor
|
||||
|
||||
return func() tea.Msg {
|
||||
go func() {
|
||||
logFunc := func(msg string) {
|
||||
progressChan <- greeterProgressMsg{step: msg, logOutput: msg}
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Checking greetd installation..."}
|
||||
if err := performGreeterInstallSteps(progressChan, logFunc, sudoPassword, compositor); err != nil {
|
||||
progressChan <- greeterProgressMsg{step: "Installation failed", complete: true, err: err}
|
||||
return
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Installation complete", complete: true}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) validateGreeterPassword(password string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return greeterPasswordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
fmt.Fprintf(stdin, "%s\n", password)
|
||||
}()
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
outputStr := string(output)
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(outputStr, "Sorry, try again") ||
|
||||
strings.Contains(outputStr, "incorrect password") ||
|
||||
strings.Contains(outputStr, "authentication failure") {
|
||||
return greeterPasswordValidMsg{password: "", valid: false}
|
||||
}
|
||||
return greeterPasswordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
return greeterPasswordValidMsg{password: password, valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
func performGreeterInstallSteps(progressChan chan greeterProgressMsg, logFunc func(string), sudoPassword string, compositor string) error {
|
||||
if err := greeter.EnsureGreetdInstalled(logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Detecting DMS installation..."}
|
||||
dmsPath, err := greeter.DetectDMSPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Found DMS at: %s", dmsPath))
|
||||
|
||||
logFunc(fmt.Sprintf("✓ Selected compositor: %s", compositor))
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Copying greeter files..."}
|
||||
if err := greeter.CopyGreeterFiles(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Configuring greetd..."}
|
||||
if err := greeter.ConfigureGreetd(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Synchronizing DMS configurations..."}
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
61
core/internal/dms/handlers_mainmenu.go
Normal file
61
core/internal/dms/handlers_mainmenu.go
Normal file
@@ -0,0 +1,61 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type shellStartedMsg struct{}
|
||||
|
||||
func (m Model) updateMainMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
return m, tea.Quit
|
||||
case "up", "k":
|
||||
if m.selectedItem > 0 {
|
||||
m.selectedItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedItem < len(m.menuItems)-1 {
|
||||
m.selectedItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedItem < len(m.menuItems) {
|
||||
selectedAction := m.menuItems[m.selectedItem].Action
|
||||
selectedLabel := m.menuItems[m.selectedItem].Label
|
||||
|
||||
switch selectedAction {
|
||||
case StateUpdate:
|
||||
m.state = StateUpdate
|
||||
m.selectedUpdateDep = 0
|
||||
case StateShell:
|
||||
if selectedLabel == "Terminate Shell" {
|
||||
terminateShell()
|
||||
m.menuItems = m.buildMenuItems()
|
||||
if m.selectedItem >= len(m.menuItems) {
|
||||
m.selectedItem = len(m.menuItems) - 1
|
||||
}
|
||||
} else {
|
||||
startShellDaemon()
|
||||
// Wait a moment for the daemon to actually start before checking status
|
||||
return m, tea.Tick(300*time.Millisecond, func(t time.Time) tea.Msg {
|
||||
return shellStartedMsg{}
|
||||
})
|
||||
}
|
||||
case StatePluginsMenu:
|
||||
m.state = StatePluginsMenu
|
||||
m.selectedPluginsMenuItem = 0
|
||||
m.pluginsMenuItems = m.buildPluginsMenuItems()
|
||||
case StateGreeterMenu:
|
||||
m.state = StateGreeterMenu
|
||||
m.selectedGreeterItem = 0
|
||||
case StateAbout:
|
||||
m.state = StateAbout
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
55
core/internal/dms/handlers_mainmenu_distro.go
Normal file
55
core/internal/dms/handlers_mainmenu_distro.go
Normal file
@@ -0,0 +1,55 @@
|
||||
//go:build distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type shellStartedMsg struct{}
|
||||
|
||||
func (m Model) updateMainMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
return m, tea.Quit
|
||||
case "up", "k":
|
||||
if m.selectedItem > 0 {
|
||||
m.selectedItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedItem < len(m.menuItems)-1 {
|
||||
m.selectedItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedItem < len(m.menuItems) {
|
||||
selectedAction := m.menuItems[m.selectedItem].Action
|
||||
selectedLabel := m.menuItems[m.selectedItem].Label
|
||||
|
||||
switch selectedAction {
|
||||
case StateShell:
|
||||
if selectedLabel == "Terminate Shell" {
|
||||
terminateShell()
|
||||
m.menuItems = m.buildMenuItems()
|
||||
if m.selectedItem >= len(m.menuItems) {
|
||||
m.selectedItem = len(m.menuItems) - 1
|
||||
}
|
||||
} else {
|
||||
startShellDaemon()
|
||||
// Wait a moment for the daemon to actually start before checking status
|
||||
return m, tea.Tick(300*time.Millisecond, func(t time.Time) tea.Msg {
|
||||
return shellStartedMsg{}
|
||||
})
|
||||
}
|
||||
case StatePluginsMenu:
|
||||
m.state = StatePluginsMenu
|
||||
m.selectedPluginsMenuItem = 0
|
||||
m.pluginsMenuItems = m.buildPluginsMenuItems()
|
||||
case StateAbout:
|
||||
m.state = StateAbout
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
377
core/internal/dms/plugins_handlers.go
Normal file
377
core/internal/dms/plugins_handlers.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) updatePluginsMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
case "up", "k":
|
||||
if m.selectedPluginsMenuItem > 0 {
|
||||
m.selectedPluginsMenuItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedPluginsMenuItem < len(m.pluginsMenuItems)-1 {
|
||||
m.selectedPluginsMenuItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedPluginsMenuItem < len(m.pluginsMenuItems) {
|
||||
selectedAction := m.pluginsMenuItems[m.selectedPluginsMenuItem].Action
|
||||
switch selectedAction {
|
||||
case StatePluginsBrowse:
|
||||
m.state = StatePluginsBrowse
|
||||
m.pluginsLoading = true
|
||||
m.pluginsError = ""
|
||||
m.pluginsList = nil
|
||||
return m, loadPlugins
|
||||
case StatePluginsInstalled:
|
||||
m.state = StatePluginsInstalled
|
||||
m.installedPluginsLoading = true
|
||||
m.installedPluginsError = ""
|
||||
m.installedPluginsList = nil
|
||||
return m, loadInstalledPlugins
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginsBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsMenu
|
||||
m.pluginSearchQuery = ""
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
case "up", "k":
|
||||
if m.selectedPluginIndex > 0 {
|
||||
m.selectedPluginIndex--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedPluginIndex < len(m.filteredPluginsList)-1 {
|
||||
m.selectedPluginIndex++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedPluginIndex < len(m.filteredPluginsList) {
|
||||
m.state = StatePluginDetail
|
||||
}
|
||||
case "/":
|
||||
m.state = StatePluginSearch
|
||||
m.pluginSearchQuery = ""
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsBrowse
|
||||
case "i":
|
||||
if m.selectedPluginIndex < len(m.filteredPluginsList) {
|
||||
plugin := m.filteredPluginsList[m.selectedPluginIndex]
|
||||
installed := m.pluginInstallStatus[plugin.Name]
|
||||
if !installed {
|
||||
return m, installPlugin(plugin)
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginSearch(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsBrowse
|
||||
m.pluginSearchQuery = ""
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
case "enter":
|
||||
m.state = StatePluginsBrowse
|
||||
m.filterPlugins()
|
||||
case "backspace":
|
||||
if len(m.pluginSearchQuery) > 0 {
|
||||
m.pluginSearchQuery = m.pluginSearchQuery[:len(m.pluginSearchQuery)-1]
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 {
|
||||
m.pluginSearchQuery += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) filterPlugins() {
|
||||
if m.pluginSearchQuery == "" {
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
return
|
||||
}
|
||||
|
||||
rawPlugins := make([]plugins.Plugin, len(m.pluginsList))
|
||||
for i, p := range m.pluginsList {
|
||||
rawPlugins[i] = plugins.Plugin{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
}
|
||||
}
|
||||
|
||||
searchResults := plugins.FuzzySearch(m.pluginSearchQuery, rawPlugins)
|
||||
searchResults = plugins.SortByFirstParty(searchResults)
|
||||
|
||||
filtered := make([]pluginInfo, len(searchResults))
|
||||
for i, p := range searchResults {
|
||||
filtered[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
|
||||
m.filteredPluginsList = filtered
|
||||
m.selectedPluginIndex = 0
|
||||
}
|
||||
|
||||
type pluginsLoadedMsg struct {
|
||||
plugins []plugins.Plugin
|
||||
err error
|
||||
}
|
||||
|
||||
func loadPlugins() tea.Msg {
|
||||
registry, err := plugins.NewRegistry()
|
||||
if err != nil {
|
||||
return pluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
pluginList, err := registry.List()
|
||||
if err != nil {
|
||||
return pluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
return pluginsLoadedMsg{plugins: pluginList}
|
||||
}
|
||||
|
||||
func (m *Model) updatePluginInstallStatus() {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, plugin := range m.pluginsList {
|
||||
p := plugins.Plugin{ID: plugin.ID}
|
||||
installed, err := manager.IsInstalled(p)
|
||||
if err == nil {
|
||||
m.pluginInstallStatus[plugin.Name] = installed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) updatePluginsInstalled(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsMenu
|
||||
case "up", "k":
|
||||
if m.selectedInstalledIndex > 0 {
|
||||
m.selectedInstalledIndex--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList)-1 {
|
||||
m.selectedInstalledIndex++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
||||
m.state = StatePluginInstalledDetail
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginInstalledDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsInstalled
|
||||
case "u":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
||||
return m, uninstallPlugin(plugin)
|
||||
}
|
||||
case "p":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
||||
return m, updatePlugin(plugin)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type installedPluginsLoadedMsg struct {
|
||||
plugins []plugins.Plugin
|
||||
err error
|
||||
}
|
||||
|
||||
type pluginUninstalledMsg struct {
|
||||
pluginName string
|
||||
err error
|
||||
}
|
||||
|
||||
type pluginInstalledMsg struct {
|
||||
pluginName string
|
||||
err error
|
||||
}
|
||||
|
||||
type pluginUpdatedMsg struct {
|
||||
pluginName string
|
||||
err error
|
||||
}
|
||||
|
||||
func loadInstalledPlugins() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
registry, err := plugins.NewRegistry()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
installedNames, err := manager.ListInstalled()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
allPlugins, err := registry.List()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
var installed []plugins.Plugin
|
||||
for _, id := range installedNames {
|
||||
for _, p := range allPlugins {
|
||||
if p.ID == id {
|
||||
installed = append(installed, p)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
installed = plugins.SortByFirstParty(installed)
|
||||
|
||||
return installedPluginsLoadedMsg{plugins: installed}
|
||||
}
|
||||
|
||||
func installPlugin(plugin pluginInfo) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return pluginInstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
p := plugins.Plugin{
|
||||
ID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
Category: plugin.Category,
|
||||
Author: plugin.Author,
|
||||
Description: plugin.Description,
|
||||
Repo: plugin.Repo,
|
||||
Path: plugin.Path,
|
||||
Capabilities: plugin.Capabilities,
|
||||
Compositors: plugin.Compositors,
|
||||
Dependencies: plugin.Dependencies,
|
||||
}
|
||||
|
||||
if err := manager.Install(p); err != nil {
|
||||
return pluginInstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
return pluginInstalledMsg{pluginName: plugin.Name}
|
||||
}
|
||||
}
|
||||
|
||||
func uninstallPlugin(plugin pluginInfo) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return pluginUninstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
p := plugins.Plugin{
|
||||
ID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
Category: plugin.Category,
|
||||
Author: plugin.Author,
|
||||
Description: plugin.Description,
|
||||
Repo: plugin.Repo,
|
||||
Path: plugin.Path,
|
||||
Capabilities: plugin.Capabilities,
|
||||
Compositors: plugin.Compositors,
|
||||
Dependencies: plugin.Dependencies,
|
||||
}
|
||||
|
||||
if err := manager.Uninstall(p); err != nil {
|
||||
return pluginUninstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
return pluginUninstalledMsg{pluginName: plugin.Name}
|
||||
}
|
||||
}
|
||||
|
||||
func updatePlugin(plugin pluginInfo) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
p := plugins.Plugin{
|
||||
ID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
Category: plugin.Category,
|
||||
Author: plugin.Author,
|
||||
Description: plugin.Description,
|
||||
Repo: plugin.Repo,
|
||||
Path: plugin.Path,
|
||||
Capabilities: plugin.Capabilities,
|
||||
Compositors: plugin.Compositors,
|
||||
Dependencies: plugin.Dependencies,
|
||||
}
|
||||
|
||||
if err := manager.Update(p); err != nil {
|
||||
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
return pluginUpdatedMsg{pluginName: plugin.Name}
|
||||
}
|
||||
}
|
||||
367
core/internal/dms/plugins_views.go
Normal file
367
core/internal/dms/plugins_views.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) renderPluginsMenu() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
b.WriteString(titleStyle.Render("Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
for i, item := range m.pluginsMenuItems {
|
||||
if i == m.selectedPluginsMenuItem {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", item.Label)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item.Label)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: Select | Esc: Back | q: Quit"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginsBrowse() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
b.WriteString(titleStyle.Render("Browse Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if m.pluginsLoading {
|
||||
b.WriteString(normalStyle.Render("Fetching plugins from registry..."))
|
||||
} else if m.pluginsError != "" {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.pluginsError)))
|
||||
} else if len(m.filteredPluginsList) == 0 {
|
||||
if m.pluginSearchQuery != "" {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf("No plugins match '%s'", m.pluginSearchQuery)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render("No plugins found in registry."))
|
||||
}
|
||||
} else {
|
||||
installedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
for i, plugin := range m.filteredPluginsList {
|
||||
installed := m.pluginInstallStatus[plugin.Name]
|
||||
installMarker := ""
|
||||
if installed {
|
||||
installMarker = " [Installed]"
|
||||
}
|
||||
|
||||
if i == m.selectedPluginIndex {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", plugin.Name)))
|
||||
if installed {
|
||||
b.WriteString(installedStyle.Render(installMarker))
|
||||
}
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", plugin.Name)))
|
||||
if installed {
|
||||
b.WriteString(installedStyle.Render(installMarker))
|
||||
}
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if m.pluginsLoading || m.pluginsError != "" {
|
||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
||||
} else {
|
||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: View/Install | /: Search | Esc: Back | q: Quit"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginDetail() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
labelStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if m.selectedPluginIndex >= len(m.filteredPluginsList) {
|
||||
return "No plugin selected"
|
||||
}
|
||||
|
||||
plugin := m.filteredPluginsList[m.selectedPluginIndex]
|
||||
|
||||
b.WriteString(titleStyle.Render(plugin.Name))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("ID: "))
|
||||
b.WriteString(normalStyle.Render(plugin.ID))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Category: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Category))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Author: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Author))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Description:"))
|
||||
b.WriteString("\n")
|
||||
wrapped := wrapText(plugin.Description, 60)
|
||||
b.WriteString(normalStyle.Render(wrapped))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Repository: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Repo))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(plugin.Capabilities) > 0 {
|
||||
b.WriteString(labelStyle.Render("Capabilities: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Capabilities, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Compositors) > 0 {
|
||||
b.WriteString(labelStyle.Render("Compositors: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Compositors, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Dependencies) > 0 {
|
||||
b.WriteString(labelStyle.Render("Dependencies: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Dependencies, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
installed := m.pluginInstallStatus[plugin.Name]
|
||||
if installed {
|
||||
b.WriteString(labelStyle.Render("Status: "))
|
||||
installedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
b.WriteString(installedStyle.Render("Installed"))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if installed {
|
||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
||||
} else {
|
||||
b.WriteString(instructionStyle.Render("i: Install | Esc: Back | q: Quit"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginSearch() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(titleStyle.Render("Search Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(normalStyle.Render("Query: "))
|
||||
b.WriteString(titleStyle.Render(m.pluginSearchQuery + "▌"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Enter: Search | Esc: Cancel"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginsInstalled() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
b.WriteString(titleStyle.Render("Installed Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if m.installedPluginsLoading {
|
||||
b.WriteString(normalStyle.Render("Loading installed plugins..."))
|
||||
} else if m.installedPluginsError != "" {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.installedPluginsError)))
|
||||
} else if len(m.installedPluginsList) == 0 {
|
||||
b.WriteString(normalStyle.Render("No plugins installed."))
|
||||
} else {
|
||||
for i, plugin := range m.installedPluginsList {
|
||||
if i == m.selectedInstalledIndex {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", plugin.Name)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", plugin.Name)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if m.installedPluginsLoading || m.installedPluginsError != "" {
|
||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
||||
} else {
|
||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: Details | Esc: Back | q: Quit"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginInstalledDetail() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
labelStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
if m.selectedInstalledIndex >= len(m.installedPluginsList) {
|
||||
return "No plugin selected"
|
||||
}
|
||||
|
||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
||||
|
||||
b.WriteString(titleStyle.Render(plugin.Name))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("ID: "))
|
||||
b.WriteString(normalStyle.Render(plugin.ID))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Category: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Category))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Author: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Author))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Description:"))
|
||||
b.WriteString("\n")
|
||||
wrapped := wrapText(plugin.Description, 60)
|
||||
b.WriteString(normalStyle.Render(wrapped))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Repository: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Repo))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(plugin.Capabilities) > 0 {
|
||||
b.WriteString(labelStyle.Render("Capabilities: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Capabilities, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Compositors) > 0 {
|
||||
b.WriteString(labelStyle.Render("Compositors: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Compositors, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Dependencies) > 0 {
|
||||
b.WriteString(labelStyle.Render("Dependencies: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Dependencies, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if m.installedPluginsError != "" {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.installedPluginsError)))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("u: Uninstall | Esc: Back | q: Quit"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func wrapText(text string, width int) string {
|
||||
words := strings.Fields(text)
|
||||
if len(words) == 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
var lines []string
|
||||
currentLine := words[0]
|
||||
|
||||
for _, word := range words[1:] {
|
||||
if len(currentLine)+1+len(word) <= width {
|
||||
currentLine += " " + word
|
||||
} else {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = word
|
||||
}
|
||||
}
|
||||
lines = append(lines, currentLine)
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
152
core/internal/dms/views_common.go
Normal file
152
core/internal/dms/views_common.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) renderMainMenu() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("dms"))
|
||||
b.WriteString("\n")
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
for i, item := range m.menuItems {
|
||||
if i == m.selectedItem {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", item.Label)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item.Label)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Enter: Select, q/Esc: Exit"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderShellView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Shell"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Opening interactive shell..."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("This will launch a shell with DMS environment loaded."))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Press any key to launch shell, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderAboutView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("About DankMaterialShell"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf("DMS Management Interface %s", m.version)))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(normalStyle.Render("DankMaterialShell is a comprehensive desktop environment"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("built around Quickshell, providing a modern Material Design"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("experience for Wayland compositors."))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(normalStyle.Render("Components:"))
|
||||
b.WriteString("\n")
|
||||
if len(m.dependencies) == 0 {
|
||||
b.WriteString(normalStyle.Render("\n Component detection not supported on this platform."))
|
||||
}
|
||||
for _, dep := range m.dependencies {
|
||||
status := "✗"
|
||||
if dep.Status == 1 {
|
||||
status = "✓"
|
||||
}
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s %s", status, dep.Name)))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Esc: Back to main menu"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderBanner() string {
|
||||
theme := tui.TerminalTheme()
|
||||
|
||||
logo := `
|
||||
██████╗ █████╗ ███╗ ██╗██╗ ██╗
|
||||
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
|
||||
██║ ██║███████║██╔██╗ ██║█████╔╝
|
||||
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
|
||||
██████╔╝██║ ██║██║ ╚████║██║ ██╗
|
||||
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝`
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Primary)).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
return titleStyle.Render(logo)
|
||||
}
|
||||
529
core/internal/dms/views_features.go
Normal file
529
core/internal/dms/views_features.go
Normal file
@@ -0,0 +1,529 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) renderUpdateView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Update Dependencies"))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(m.updateDeps) == 0 {
|
||||
b.WriteString("Loading dependencies...\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
categories := m.categorizeDependencies()
|
||||
currentIndex := 0
|
||||
|
||||
for _, category := range []string{"Shell", "Shared Components", "Hyprland Components", "Niri Components"} {
|
||||
deps, exists := categories[category]
|
||||
if !exists || len(deps) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
categoryStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#7060ac")).
|
||||
Bold(true).
|
||||
MarginTop(1)
|
||||
|
||||
b.WriteString(categoryStyle.Render(category + ":"))
|
||||
b.WriteString("\n")
|
||||
|
||||
for _, dep := range deps {
|
||||
var statusText, icon, reinstallMarker string
|
||||
var style lipgloss.Style
|
||||
|
||||
if m.updateToggles[dep.Name] {
|
||||
reinstallMarker = "🔄 "
|
||||
if dep.Status == 0 {
|
||||
statusText = "Will be installed"
|
||||
} else {
|
||||
statusText = "Will be upgraded"
|
||||
}
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
||||
} else {
|
||||
switch dep.Status {
|
||||
case 1:
|
||||
icon = "✓"
|
||||
statusText = "Installed"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF"))
|
||||
case 0:
|
||||
icon = "○"
|
||||
statusText = "Not installed"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
case 2:
|
||||
icon = "△"
|
||||
statusText = "Needs update"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
||||
case 3:
|
||||
icon = "!"
|
||||
statusText = "Needs reinstall"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
||||
}
|
||||
}
|
||||
|
||||
line := fmt.Sprintf("%s%s%-25s %s", reinstallMarker, icon, dep.Name, statusText)
|
||||
|
||||
if currentIndex == m.selectedUpdateDep {
|
||||
line = "▶ " + line
|
||||
selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#7060ac")).Bold(true)
|
||||
b.WriteString(selectedStyle.Render(line))
|
||||
} else {
|
||||
line = " " + line
|
||||
b.WriteString(style.Render(line))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
currentIndex++
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Space: Toggle, Enter: Update Selected, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPasswordView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Sudo Authentication"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Package installation requires sudo privileges."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("Please enter your password to continue:"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
inputStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
maskedPassword := strings.Repeat("*", len(m.passwordInput))
|
||||
b.WriteString(inputStyle.Render("Password: " + maskedPassword))
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.passwordError != "" {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
b.WriteString(errorStyle.Render("✗ " + m.passwordError))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Enter: Continue, Esc: Back, Ctrl+C: Cancel"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderProgressView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Updating Packages"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if !m.updateProgress.complete {
|
||||
progressStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString(progressStyle.Render(m.updateProgress.step))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
progressBar := fmt.Sprintf("[%s%s] %.0f%%",
|
||||
strings.Repeat("█", int(m.updateProgress.progress*30)),
|
||||
strings.Repeat("░", 30-int(m.updateProgress.progress*30)),
|
||||
m.updateProgress.progress*100)
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Render(progressBar))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(m.updateLogs) > 0 {
|
||||
b.WriteString("\n")
|
||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Live Output:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
maxLines := 8
|
||||
startIdx := 0
|
||||
if len(m.updateLogs) > maxLines {
|
||||
startIdx = len(m.updateLogs) - maxLines
|
||||
}
|
||||
|
||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
for i := startIdx; i < len(m.updateLogs); i++ {
|
||||
if m.updateLogs[i] != "" {
|
||||
b.WriteString(logStyle.Render(" " + m.updateLogs[i]))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.updateProgress.err != nil {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Update failed: %v", m.updateProgress.err)))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(m.updateLogs) > 0 {
|
||||
b.WriteString("\n")
|
||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Error Logs:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
maxLines := 15
|
||||
startIdx := 0
|
||||
if len(m.updateLogs) > maxLines {
|
||||
startIdx = len(m.updateLogs) - maxLines
|
||||
}
|
||||
|
||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
for i := startIdx; i < len(m.updateLogs); i++ {
|
||||
if m.updateLogs[i] != "" {
|
||||
b.WriteString(logStyle.Render(" " + m.updateLogs[i]))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to go back"))
|
||||
} else if m.updateProgress.complete {
|
||||
successStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(successStyle.Render("✓ Update complete!"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to return to main menu"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) getFilteredDeps() []DependencyInfo {
|
||||
categories := m.categorizeDependencies()
|
||||
var filtered []DependencyInfo
|
||||
|
||||
for _, category := range []string{"Shell", "Shared Components", "Hyprland Components", "Niri Components"} {
|
||||
deps, exists := categories[category]
|
||||
if exists {
|
||||
filtered = append(filtered, deps...)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (m Model) getDepAtVisualIndex(index int) *DependencyInfo {
|
||||
filtered := m.getFilteredDeps()
|
||||
if index >= 0 && index < len(filtered) {
|
||||
return &filtered[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterPasswordView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Sudo Authentication"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Greeter installation requires sudo privileges."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("Please enter your password to continue:"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
inputStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
maskedPassword := strings.Repeat("*", len(m.greeterPasswordInput))
|
||||
b.WriteString(inputStyle.Render("Password: " + maskedPassword))
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.greeterPasswordError != "" {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
b.WriteString(errorStyle.Render("✗ " + m.greeterPasswordError))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Enter: Continue, Esc: Back, Ctrl+C: Cancel"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterCompositorSelect() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Select Compositor"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Multiple compositors detected. Choose which one to use for the greeter:"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
for i, comp := range m.greeterCompositors {
|
||||
if i == m.greeterSelectedComp {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", comp)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", comp)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Enter: Select, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterMenu() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Greeter Management"))
|
||||
b.WriteString("\n")
|
||||
|
||||
greeterMenuItems := []string{"Install Greeter"}
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
for i, item := range greeterMenuItems {
|
||||
if i == m.selectedGreeterItem {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", item)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Enter: Select, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterInstalling() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Installing Greeter"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if !m.greeterProgress.complete {
|
||||
progressStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString(progressStyle.Render(m.greeterProgress.step))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(m.greeterLogs) > 0 {
|
||||
b.WriteString("\n")
|
||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Output:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
maxLines := 10
|
||||
startIdx := 0
|
||||
if len(m.greeterLogs) > maxLines {
|
||||
startIdx = len(m.greeterLogs) - maxLines
|
||||
}
|
||||
|
||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
for i := startIdx; i < len(m.greeterLogs); i++ {
|
||||
if m.greeterLogs[i] != "" {
|
||||
b.WriteString(logStyle.Render(" " + m.greeterLogs[i]))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.greeterProgress.err != nil {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Installation failed: %v", m.greeterProgress.err)))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to go back"))
|
||||
} else if m.greeterProgress.complete {
|
||||
successStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(successStyle.Render("✓ Greeter installation complete!"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("To test the greeter, run:"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render(" sudo systemctl start greetd"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(normalStyle.Render("To enable on boot, run:"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render(" sudo systemctl enable --now greetd"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to return to main menu"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) categorizeDependencies() map[string][]DependencyInfo {
|
||||
categories := map[string][]DependencyInfo{
|
||||
"Shell": {},
|
||||
"Shared Components": {},
|
||||
"Hyprland Components": {},
|
||||
"Niri Components": {},
|
||||
}
|
||||
|
||||
excludeList := map[string]bool{
|
||||
"git": true,
|
||||
"polkit-agent": true,
|
||||
"jq": true,
|
||||
"xdg-desktop-portal": true,
|
||||
"xdg-desktop-portal-wlr": true,
|
||||
"xdg-desktop-portal-hyprland": true,
|
||||
"xdg-desktop-portal-gtk": true,
|
||||
}
|
||||
|
||||
for _, dep := range m.updateDeps {
|
||||
if excludeList[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
switch dep.Name {
|
||||
case "dms (DankMaterialShell)", "quickshell":
|
||||
categories["Shell"] = append(categories["Shell"], dep)
|
||||
case "hyprland", "hyprctl":
|
||||
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
||||
case "niri":
|
||||
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
||||
case "kitty", "alacritty", "ghostty":
|
||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
||||
default:
|
||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
||||
}
|
||||
}
|
||||
|
||||
return categories
|
||||
}
|
||||
@@ -502,17 +502,17 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
|
||||
p.dmsProcessed = true
|
||||
}
|
||||
|
||||
expanded, err := utils.ExpandPath(sourcePath)
|
||||
fullPath := sourcePath
|
||||
if !filepath.IsAbs(sourcePath) {
|
||||
fullPath = filepath.Join(baseDir, sourcePath)
|
||||
}
|
||||
|
||||
expanded, err := utils.ExpandPath(fullPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fullPath := expanded
|
||||
if !filepath.IsAbs(expanded) {
|
||||
fullPath = filepath.Join(baseDir, expanded)
|
||||
}
|
||||
|
||||
includedBinds, err := p.parseFileWithSource(fullPath)
|
||||
includedBinds, err := p.parseFileWithSource(expanded)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -521,10 +521,33 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
|
||||
}
|
||||
|
||||
func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding {
|
||||
keybinds, err := p.parseFileWithSource(dmsBindsPath)
|
||||
data, err := os.ReadFile(dmsBindsPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
prevSource := p.currentSource
|
||||
p.currentSource = dmsBindsPath
|
||||
|
||||
var keybinds []MangoWCKeyBinding
|
||||
lines := strings.Split(string(data), "\n")
|
||||
|
||||
for lineNum, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "bind") {
|
||||
continue
|
||||
}
|
||||
|
||||
kb := p.getKeybindAtLineContent(line, lineNum)
|
||||
if kb == nil {
|
||||
continue
|
||||
}
|
||||
kb.Source = dmsBindsPath
|
||||
p.addBind(kb)
|
||||
keybinds = append(keybinds, *kb)
|
||||
}
|
||||
|
||||
p.currentSource = prevSource
|
||||
p.dmsProcessed = true
|
||||
return keybinds
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ var templateRegistry = []TemplateDef{
|
||||
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
|
||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml"},
|
||||
}
|
||||
|
||||
func (c *ColorMode) GTKTheme() string {
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1769018530,
|
||||
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=",
|
||||
"lastModified": 1766651565,
|
||||
"narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1",
|
||||
"rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
kirigami.unwrapped
|
||||
sonnet
|
||||
qtmultimedia
|
||||
qtimageformats
|
||||
];
|
||||
in
|
||||
{
|
||||
@@ -79,7 +78,7 @@
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-kWHB/FN6Z2Ydh+VvNrDnbg18RuJSDAle4DHDAP4NpNk=";
|
||||
vendorHash = "sha256-lXqOJ0yNlOcXuR3vcuVjFI02Hskmavcasb1Ntf3UlPM=";
|
||||
|
||||
subPackages = [ "cmd/dms" ];
|
||||
|
||||
|
||||
53
quickshell/Common/Facts.qml
Normal file
53
quickshell/Common/Facts.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var facts: [
|
||||
"A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.",
|
||||
"A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.",
|
||||
"Right now, 100 trillion solar neutrinos are passing through your body every second.",
|
||||
"The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.",
|
||||
"The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.",
|
||||
"There's a nebula out there that's actually colder than empty space itself.",
|
||||
"We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.",
|
||||
"Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.",
|
||||
"Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.",
|
||||
"Distant galaxies can move away from us faster than light because space itself is stretching.",
|
||||
"The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.",
|
||||
"The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.",
|
||||
"A day on Venus lasts longer than its entire year around the Sun.",
|
||||
"On Mercury, the time between sunrises is 176 Earth days long.",
|
||||
"In about 4.5 billion years, our galaxy will smash into Andromeda.",
|
||||
"Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.",
|
||||
"PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.",
|
||||
"Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.",
|
||||
"Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.",
|
||||
"Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.",
|
||||
"Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.",
|
||||
"Counting to a billion at one number per second would take over 31 years.",
|
||||
"Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.",
|
||||
"Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.",
|
||||
"Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.",
|
||||
"Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.",
|
||||
"Even at light-speed, you'd never catch up to most galaxies—space expands faster.",
|
||||
"Only around 5% of galaxies are ever reachable—even at light-speed.",
|
||||
"If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.",
|
||||
"If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.",
|
||||
"Our oldest radio signals will reach the Milky Way's center in 26,000 years.",
|
||||
"Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.",
|
||||
"The Moon moves 3.8 centimeters farther from Earth every year.",
|
||||
"The universe creates 275 million new stars every single day.",
|
||||
"Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.",
|
||||
"If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.",
|
||||
"The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."
|
||||
]
|
||||
|
||||
function getRandomFact() {
|
||||
return facts[Math.floor(Math.random() * facts.length)]
|
||||
}
|
||||
}
|
||||
@@ -100,8 +100,7 @@ const DMS_ACTIONS = [
|
||||
{ id: "spawn dms ipc call hypr openOverview", label: "Hyprland: Open Overview", compositor: "hyprland" },
|
||||
{ id: "spawn dms ipc call hypr closeOverview", label: "Hyprland: Close Overview", compositor: "hyprland" },
|
||||
{ id: "spawn dms ipc call wallpaper next", label: "Wallpaper: Next" },
|
||||
{ id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" },
|
||||
{ id: "spawn dms ipc call workspace-rename open", label: "Workspace: Rename" }
|
||||
{ id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" }
|
||||
];
|
||||
|
||||
const NIRI_ACTIONS = {
|
||||
|
||||
@@ -83,8 +83,6 @@ Singleton {
|
||||
property string nightModeLocationProvider: ""
|
||||
|
||||
property var pinnedApps: []
|
||||
property var barPinnedApps: []
|
||||
property int dockLauncherPosition: 0
|
||||
property var hiddenTrayIds: []
|
||||
property var recentColors: []
|
||||
property bool showThirdPartyPlugins: false
|
||||
@@ -759,11 +757,6 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setDockLauncherPosition(position) {
|
||||
dockLauncherPosition = position;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function addPinnedApp(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
@@ -785,32 +778,6 @@ Singleton {
|
||||
return appId && pinnedApps.indexOf(appId) !== -1;
|
||||
}
|
||||
|
||||
function setBarPinnedApps(apps) {
|
||||
barPinnedApps = apps;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function addBarPinnedApp(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
var currentPinned = [...barPinnedApps];
|
||||
if (currentPinned.indexOf(appId) === -1) {
|
||||
currentPinned.push(appId);
|
||||
setBarPinnedApps(currentPinned);
|
||||
}
|
||||
}
|
||||
|
||||
function removeBarPinnedApp(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
var currentPinned = barPinnedApps.filter(id => id !== appId);
|
||||
setBarPinnedApps(currentPinned);
|
||||
}
|
||||
|
||||
function isBarPinnedApp(appId) {
|
||||
return appId && barPinnedApps.indexOf(appId) !== -1;
|
||||
}
|
||||
|
||||
function hideTrayId(trayId) {
|
||||
if (!trayId)
|
||||
return;
|
||||
|
||||
@@ -79,45 +79,6 @@ 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
|
||||
@@ -146,9 +107,7 @@ Singleton {
|
||||
|
||||
property bool use24HourClock: true
|
||||
property bool showSeconds: false
|
||||
property bool padHours12Hour: false
|
||||
property bool useFahrenheit: false
|
||||
property string windSpeedUnit: "kmh"
|
||||
property bool nightModeEnabled: false
|
||||
property int animationSpeed: SettingsData.AnimationSpeed.Short
|
||||
property int customAnimationDuration: 500
|
||||
@@ -274,21 +233,10 @@ Singleton {
|
||||
property string spotlightModalViewMode: "list"
|
||||
property string browserPickerViewMode: "grid"
|
||||
property var browserUsageHistory: ({})
|
||||
property string appPickerViewMode: "grid"
|
||||
property var filePickerUsageHistory: ({})
|
||||
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"
|
||||
@@ -416,7 +364,6 @@ Singleton {
|
||||
property bool matugenTemplateDgop: true
|
||||
property bool matugenTemplateKcolorscheme: true
|
||||
property bool matugenTemplateVscode: true
|
||||
property bool matugenTemplateEmacs: true
|
||||
|
||||
property bool showDock: false
|
||||
property bool dockAutoHide: false
|
||||
@@ -434,13 +381,6 @@ Singleton {
|
||||
property real dockBorderOpacity: 1.0
|
||||
property int dockBorderThickness: 1
|
||||
property bool dockIsolateDisplays: false
|
||||
property bool dockLauncherEnabled: false
|
||||
property string dockLauncherLogoMode: "apps"
|
||||
property string dockLauncherLogoCustomPath: ""
|
||||
property string dockLauncherLogoColorOverride: ""
|
||||
property int dockLauncherLogoSizeOffset: 0
|
||||
property real dockLauncherLogoBrightness: 0.5
|
||||
property real dockLauncherLogoContrast: 1
|
||||
|
||||
property bool notificationOverlayEnabled: false
|
||||
property int overviewRows: 2
|
||||
@@ -455,7 +395,6 @@ Singleton {
|
||||
property bool lockScreenShowDate: true
|
||||
property bool lockScreenShowProfileImage: true
|
||||
property bool lockScreenShowPasswordField: true
|
||||
property bool lockScreenShowMediaPlayer: true
|
||||
property bool lockScreenPowerOffMonitorsOnLock: false
|
||||
|
||||
property bool enableFprint: false
|
||||
@@ -1255,11 +1194,11 @@ Singleton {
|
||||
}
|
||||
|
||||
function getEffectiveTimeFormat() {
|
||||
if (use24HourClock)
|
||||
if (use24HourClock) {
|
||||
return showSeconds ? "hh:mm:ss" : "hh:mm";
|
||||
if (padHours12Hour)
|
||||
return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
|
||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
|
||||
} else {
|
||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
|
||||
}
|
||||
}
|
||||
|
||||
function getEffectiveClockDateFormat() {
|
||||
|
||||
@@ -752,11 +752,9 @@ Singleton {
|
||||
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5;
|
||||
}
|
||||
|
||||
function barIconSize(barThickness, offset, noBackground) {
|
||||
function barIconSize(barThickness, offset) {
|
||||
const defaultOffset = offset !== undefined ? offset : -6;
|
||||
const size = (noBackground ?? false) ? iconSizeLarge : iconSize;
|
||||
|
||||
return Math.round((barThickness / 48) * (size + defaultOffset));
|
||||
return Math.round((barThickness / 48) * (iconSize + defaultOffset));
|
||||
}
|
||||
|
||||
function barTextSize(barThickness, fontScale) {
|
||||
@@ -906,7 +904,7 @@ Singleton {
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
const skipTemplates = [];
|
||||
if (!SettingsData.runDmsMatugenTemplates) {
|
||||
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs");
|
||||
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode");
|
||||
} else {
|
||||
if (!SettingsData.matugenTemplateGtk)
|
||||
skipTemplates.push("gtk");
|
||||
@@ -948,8 +946,6 @@ Singleton {
|
||||
skipTemplates.push("kcolorscheme");
|
||||
if (!SettingsData.matugenTemplateVscode)
|
||||
skipTemplates.push("vscode");
|
||||
if (!SettingsData.matugenTemplateEmacs)
|
||||
skipTemplates.push("emacs");
|
||||
}
|
||||
if (skipTemplates.length > 0) {
|
||||
args.push("--skip-templates", skipTemplates.join(","));
|
||||
|
||||
@@ -39,8 +39,6 @@ var SPEC = {
|
||||
weatherCoordinates: { def: "40.7128,-74.0060" },
|
||||
|
||||
pinnedApps: { def: [] },
|
||||
barPinnedApps: { def: [] },
|
||||
dockLauncherPosition: { def: 0 },
|
||||
hiddenTrayIds: { def: [] },
|
||||
recentColors: { def: [] },
|
||||
showThirdPartyPlugins: { def: false },
|
||||
|
||||
@@ -32,9 +32,7 @@ var SPEC = {
|
||||
|
||||
use24HourClock: { def: true },
|
||||
showSeconds: { def: false },
|
||||
padHours12Hour: { def: false },
|
||||
useFahrenheit: { def: false },
|
||||
windSpeedUnit: { def: "kmh" },
|
||||
nightModeEnabled: { def: false },
|
||||
animationSpeed: { def: 1 },
|
||||
customAnimationDuration: { def: 500 },
|
||||
@@ -133,21 +131,10 @@ var SPEC = {
|
||||
|
||||
appLauncherViewMode: { def: "list" },
|
||||
spotlightModalViewMode: { def: "list" },
|
||||
browserPickerViewMode: { def: "grid" },
|
||||
browserUsageHistory: { def: {} },
|
||||
appPickerViewMode: { def: "grid" },
|
||||
filePickerUsageHistory: { def: {} },
|
||||
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 },
|
||||
@@ -242,7 +229,6 @@ var SPEC = {
|
||||
matugenTemplateDgop: { def: true },
|
||||
matugenTemplateKcolorscheme: { def: true },
|
||||
matugenTemplateVscode: { def: true },
|
||||
matugenTemplateEmacs: { def: true },
|
||||
|
||||
showDock: { def: false },
|
||||
dockAutoHide: { def: false },
|
||||
@@ -260,13 +246,6 @@ var SPEC = {
|
||||
dockBorderOpacity: { def: 1.0, coerce: percentToUnit },
|
||||
dockBorderThickness: { def: 1 },
|
||||
dockIsolateDisplays: { def: false },
|
||||
dockLauncherEnabled: { def: false },
|
||||
dockLauncherLogoMode: { def: "apps" },
|
||||
dockLauncherLogoCustomPath: { def: "" },
|
||||
dockLauncherLogoColorOverride: { def: "" },
|
||||
dockLauncherLogoSizeOffset: { def: 0 },
|
||||
dockLauncherLogoBrightness: { def: 0.5, coerce: percentToUnit },
|
||||
dockLauncherLogoContrast: { def: 1, coerce: percentToUnit },
|
||||
|
||||
notificationOverlayEnabled: { def: false },
|
||||
overviewRows: { def: 2, persist: false },
|
||||
@@ -281,7 +260,6 @@ var SPEC = {
|
||||
lockScreenShowDate: { def: true },
|
||||
lockScreenShowProfileImage: { def: true },
|
||||
lockScreenShowPasswordField: { def: true },
|
||||
lockScreenShowMediaPlayer: { def: true },
|
||||
lockScreenPowerOffMonitorsOnLock: { def: false },
|
||||
enableFprint: { def: false },
|
||||
maxFprintTries: { def: 15 },
|
||||
@@ -431,9 +409,7 @@ var SPEC = {
|
||||
|
||||
desktopWidgetGroups: { def: [] },
|
||||
|
||||
builtInPluginSettings: { def: {} },
|
||||
launcherPluginVisibility: { def: {} },
|
||||
launcherPluginOrder: { def: [] }
|
||||
builtInPluginSettings: { def: {} }
|
||||
};
|
||||
|
||||
function getValidKeys() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import qs.Modals.Changelog
|
||||
import qs.Modals.Clipboard
|
||||
import qs.Modals.Greeter
|
||||
import qs.Modals.Settings
|
||||
import qs.Modals.DankLauncherV2
|
||||
import qs.Modals.Spotlight
|
||||
import qs.Modules
|
||||
import qs.Modules.AppDrawer
|
||||
import qs.Modules.DankDash
|
||||
@@ -473,17 +473,15 @@ 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;
|
||||
@@ -508,22 +506,11 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: dankLauncherV2ModalLoader
|
||||
|
||||
active: false
|
||||
SpotlightModal {
|
||||
id: spotlightModal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.dankLauncherV2ModalLoader = dankLauncherV2ModalLoader;
|
||||
}
|
||||
|
||||
DankLauncherV2Modal {
|
||||
id: dankLauncherV2Modal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.dankLauncherV2Modal = dankLauncherV2Modal;
|
||||
PopoutService._onDankLauncherV2ModalLoaded();
|
||||
}
|
||||
PopoutService.spotlightModal = spotlightModal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,11 +537,6 @@ Item {
|
||||
AppPickerModal {
|
||||
id: filePickerModal
|
||||
title: I18n.tr("Open with...")
|
||||
viewMode: SettingsData.appPickerViewMode || "grid"
|
||||
|
||||
onViewModeChanged: {
|
||||
SettingsData.set("appPickerViewMode", viewMode)
|
||||
}
|
||||
|
||||
function shellEscape(str) {
|
||||
return "'" + str.replace(/'/g, "'\\''") + "'";
|
||||
@@ -649,18 +631,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: workspaceRenameModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: PopoutService.workspaceRenameModalLoader = workspaceRenameModalLoader
|
||||
|
||||
WorkspaceRenameModal {
|
||||
id: workspaceRenameModal
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: processListModalLoader
|
||||
|
||||
@@ -786,7 +756,6 @@ Item {
|
||||
hyprKeybindsModalLoader: hyprKeybindsModalLoader
|
||||
dankBarRepeater: dankBarRepeater
|
||||
hyprlandOverviewLoader: hyprlandOverviewLoader
|
||||
workspaceRenameModalLoader: workspaceRenameModalLoader
|
||||
}
|
||||
|
||||
Variants {
|
||||
|
||||
@@ -15,7 +15,6 @@ Item {
|
||||
required property var hyprKeybindsModalLoader
|
||||
required property var dankBarRepeater
|
||||
required property var hyprlandOverviewLoader
|
||||
required property var workspaceRenameModalLoader
|
||||
|
||||
function getFirstBar() {
|
||||
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
|
||||
@@ -1026,167 +1025,6 @@ 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.toggleDankLauncherV2WithQuery(query);
|
||||
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.toggleDankLauncherV2WithQuery(query);
|
||||
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS";
|
||||
}
|
||||
|
||||
target: "spotlight"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function info(message: string): string {
|
||||
if (!message)
|
||||
return "ERROR: No message specified";
|
||||
|
||||
ToastService.showInfo(message);
|
||||
return "TOAST_INFO_SUCCESS";
|
||||
}
|
||||
|
||||
function infoWith(message: string, details: string, command: string, category: string): string {
|
||||
if (!message)
|
||||
return "ERROR: No message specified";
|
||||
|
||||
ToastService.showInfo(message, details, command, category);
|
||||
return "TOAST_INFO_SUCCESS";
|
||||
}
|
||||
|
||||
function warn(message: string): string {
|
||||
if (!message)
|
||||
return "ERROR: No message specified";
|
||||
|
||||
ToastService.showWarning(message);
|
||||
return "TOAST_WARN_SUCCESS";
|
||||
}
|
||||
|
||||
function warnWith(message: string, details: string, command: string, category: string): string {
|
||||
if (!message)
|
||||
return "ERROR: No message specified";
|
||||
|
||||
ToastService.showWarning(message, details, command, category);
|
||||
return "TOAST_WARN_SUCCESS";
|
||||
}
|
||||
|
||||
function error(message: string): string {
|
||||
if (!message)
|
||||
return "ERROR: No message specified";
|
||||
|
||||
ToastService.showError(message);
|
||||
return "TOAST_ERROR_SUCCESS";
|
||||
}
|
||||
|
||||
function errorWith(message: string, details: string, command: string, category: string): string {
|
||||
if (!message)
|
||||
return "ERROR: No message specified";
|
||||
|
||||
ToastService.showError(message, details, command, category);
|
||||
return "TOAST_ERROR_SUCCESS";
|
||||
}
|
||||
|
||||
function hide(): string {
|
||||
ToastService.hideToast();
|
||||
return "TOAST_HIDE_SUCCESS";
|
||||
}
|
||||
|
||||
function dismiss(category: string): string {
|
||||
if (!category)
|
||||
return "ERROR: No category specified";
|
||||
|
||||
ToastService.dismissCategory(category);
|
||||
return "TOAST_DISMISS_SUCCESS";
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
if (!ToastService.toastVisible)
|
||||
return "hidden";
|
||||
|
||||
const levels = ["info", "warn", "error"];
|
||||
return `visible:${levels[ToastService.currentLevel]}:${ToastService.currentMessage}`;
|
||||
}
|
||||
|
||||
target: "toast"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
FirstLaunchService.showWelcome();
|
||||
@@ -1366,40 +1204,4 @@ Item {
|
||||
|
||||
target: "desktopWidget"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
root.workspaceRenameModalLoader.active = true;
|
||||
if (root.workspaceRenameModalLoader.item) {
|
||||
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
|
||||
root.workspaceRenameModalLoader.item.show(ws?.name || "");
|
||||
return "WORKSPACE_RENAME_MODAL_OPENED";
|
||||
}
|
||||
return "WORKSPACE_RENAME_MODAL_NOT_FOUND";
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (root.workspaceRenameModalLoader.item) {
|
||||
root.workspaceRenameModalLoader.item.hide();
|
||||
return "WORKSPACE_RENAME_MODAL_CLOSED";
|
||||
}
|
||||
return "WORKSPACE_RENAME_MODAL_NOT_FOUND";
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
root.workspaceRenameModalLoader.active = true;
|
||||
if (root.workspaceRenameModalLoader.item) {
|
||||
if (root.workspaceRenameModalLoader.item.visible) {
|
||||
root.workspaceRenameModalLoader.item.hide();
|
||||
return "WORKSPACE_RENAME_MODAL_CLOSED";
|
||||
}
|
||||
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
|
||||
root.workspaceRenameModalLoader.item.show(ws?.name || "");
|
||||
return "WORKSPACE_RENAME_MODAL_OPENED";
|
||||
}
|
||||
return "WORKSPACE_RENAME_MODAL_NOT_FOUND";
|
||||
}
|
||||
|
||||
target: "workspace-rename"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@ Singleton {
|
||||
readonly property int viewportBuffer: 100
|
||||
readonly property int extendedBuffer: 200
|
||||
readonly property int keyboardHintsHeight: 80
|
||||
readonly property int headerHeight: 32
|
||||
readonly property int headerHeight: 40
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ Item {
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
focus: false
|
||||
|
||||
ClipboardHeader {
|
||||
@@ -195,7 +195,7 @@ Item {
|
||||
Item {
|
||||
id: keyboardHintsContainer
|
||||
width: parent.width
|
||||
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingM : 0
|
||||
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingL : 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.spacingM
|
||||
anchors.margins: Theme.spacingL
|
||||
visible: modal.showKeyboardHints
|
||||
wtypeAvailable: modal.wtypeAvailable
|
||||
}
|
||||
|
||||
@@ -44,28 +44,26 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
iconName: "push_pin"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: header.activeTab === "saved" ? Theme.primary : Theme.surfaceText
|
||||
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: "push_pin"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: header.activeTab === "saved" ? Theme.primary : Theme.surfaceText
|
||||
opacity: header.pinnedCount > 0 ? 1 : 0
|
||||
enabled: header.pinnedCount > 0
|
||||
onClicked: tabChanged("saved")
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "info"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
|
||||
tooltipText: I18n.tr("Keyboard Shortcuts")
|
||||
onClicked: keyboardHintsToggled()
|
||||
}
|
||||
|
||||
@@ -73,7 +71,6 @@ Item {
|
||||
iconName: "delete_sweep"
|
||||
iconSize: Theme.iconSize
|
||||
iconColor: Theme.surfaceText
|
||||
tooltipText: I18n.tr("Clear All")
|
||||
onClicked: clearAllClicked()
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,16 @@ DankModal {
|
||||
property int activeImageLoads: 0
|
||||
readonly property int maxConcurrentLoads: 3
|
||||
readonly property bool clipboardAvailable: DMSService.isConnected && (DMSService.capabilities.length === 0 || DMSService.capabilities.includes("clipboard"))
|
||||
readonly property bool wtypeAvailable: SessionService.wtypeAvailable
|
||||
property bool wtypeAvailable: false
|
||||
|
||||
Process {
|
||||
id: wtypeCheck
|
||||
command: ["which", "wtype"]
|
||||
running: true
|
||||
onExited: exitCode => {
|
||||
clipboardHistoryModal.wtypeAvailable = (exitCode === 0);
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: wtypeProcess
|
||||
@@ -73,13 +82,14 @@ 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;
|
||||
});
|
||||
|
||||
@@ -183,19 +193,24 @@ 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) {
|
||||
@@ -211,9 +226,7 @@ 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;
|
||||
@@ -225,9 +238,7 @@ 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;
|
||||
@@ -239,20 +250,27 @@ 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) {
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
switch (selectedItem?.type) {
|
||||
case "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
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "plugin_browse":
|
||||
if (selectedItem?.actions) {
|
||||
for (var i = 0; i < selectedItem.actions.length; i++) {
|
||||
result.push(selectedItem.actions[i]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "app":
|
||||
if (selectedItem?.isCore)
|
||||
break;
|
||||
if (selectedItem?.actions) {
|
||||
for (var i = 0; i < selectedItem.actions.length; i++) {
|
||||
result.push(selectedItem.actions[i]);
|
||||
}
|
||||
}
|
||||
if (SessionService.nvidiaCommand) {
|
||||
result.push({
|
||||
name: I18n.tr("Launch on dGPU"),
|
||||
icon: "memory",
|
||||
action: "launch_dgpu"
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
readonly property bool hasActions: {
|
||||
switch (selectedItem?.type) {
|
||||
case "app":
|
||||
return !selectedItem?.isCore;
|
||||
case "plugin":
|
||||
return getPluginContextMenuActions().length > 0;
|
||||
case "plugin_browse":
|
||||
return selectedItem?.actions?.length > 0;
|
||||
default:
|
||||
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
@@ -1,157 +0,0 @@
|
||||
.pragma library
|
||||
|
||||
function getFileIcon(filename) {
|
||||
var ext = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1).toLowerCase() : "";
|
||||
|
||||
switch (ext) {
|
||||
case "pdf":
|
||||
return "picture_as_pdf";
|
||||
case "doc":
|
||||
case "docx":
|
||||
case "odt":
|
||||
return "description";
|
||||
case "xls":
|
||||
case "xlsx":
|
||||
case "ods":
|
||||
return "table_chart";
|
||||
case "ppt":
|
||||
case "pptx":
|
||||
case "odp":
|
||||
return "slideshow";
|
||||
case "txt":
|
||||
case "md":
|
||||
case "rst":
|
||||
return "article";
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "png":
|
||||
case "gif":
|
||||
case "svg":
|
||||
case "webp":
|
||||
return "image";
|
||||
case "mp3":
|
||||
case "wav":
|
||||
case "flac":
|
||||
case "ogg":
|
||||
return "audio_file";
|
||||
case "mp4":
|
||||
case "mkv":
|
||||
case "avi":
|
||||
case "webm":
|
||||
return "video_file";
|
||||
case "zip":
|
||||
case "tar":
|
||||
case "gz":
|
||||
case "7z":
|
||||
case "rar":
|
||||
return "folder_zip";
|
||||
case "js":
|
||||
case "ts":
|
||||
case "py":
|
||||
case "rs":
|
||||
case "go":
|
||||
case "java":
|
||||
case "c":
|
||||
case "cpp":
|
||||
case "h":
|
||||
return "code";
|
||||
case "html":
|
||||
case "css":
|
||||
case "htm":
|
||||
return "web";
|
||||
case "json":
|
||||
case "xml":
|
||||
case "yaml":
|
||||
case "yml":
|
||||
return "data_object";
|
||||
case "sh":
|
||||
case "bash":
|
||||
case "zsh":
|
||||
return "terminal";
|
||||
default:
|
||||
return "insert_drive_file";
|
||||
}
|
||||
}
|
||||
|
||||
function stripIconPrefix(iconName) {
|
||||
if (!iconName)
|
||||
return "extension";
|
||||
if (iconName.startsWith("unicode:"))
|
||||
return iconName.substring(8);
|
||||
if (iconName.startsWith("material:"))
|
||||
return iconName.substring(9);
|
||||
if (iconName.startsWith("image:"))
|
||||
return iconName.substring(6);
|
||||
return iconName;
|
||||
}
|
||||
|
||||
function detectIconType(iconName) {
|
||||
if (!iconName)
|
||||
return "material";
|
||||
if (iconName.startsWith("unicode:"))
|
||||
return "unicode";
|
||||
if (iconName.startsWith("material:"))
|
||||
return "material";
|
||||
if (iconName.startsWith("image:"))
|
||||
return "image";
|
||||
if (iconName.indexOf("/") >= 0 || iconName.indexOf(".") >= 0)
|
||||
return "image";
|
||||
if (/^[a-z]+-[a-z]/.test(iconName.toLowerCase()))
|
||||
return "image";
|
||||
return "material";
|
||||
}
|
||||
|
||||
function evaluateCalculator(query) {
|
||||
if (!query || query.length === 0)
|
||||
return null;
|
||||
|
||||
var mathExpr = query.replace(/[^0-9+\-*/().%\s^]/g, "");
|
||||
if (mathExpr.length < 2)
|
||||
return null;
|
||||
|
||||
var hasMath = /[+\-*/^%]/.test(query) && /\d/.test(query);
|
||||
if (!hasMath)
|
||||
return null;
|
||||
|
||||
try {
|
||||
var sanitized = mathExpr.replace(/\^/g, "**");
|
||||
var result = Function('"use strict"; return (' + sanitized + ')')();
|
||||
|
||||
if (typeof result === "number" && isFinite(result)) {
|
||||
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, "");
|
||||
return {
|
||||
expression: query,
|
||||
result: result,
|
||||
displayResult: displayResult
|
||||
};
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function sortPluginIdsByOrder(pluginIds, order) {
|
||||
if (!order || order.length === 0)
|
||||
return pluginIds;
|
||||
var orderMap = {};
|
||||
for (var i = 0; i < order.length; i++)
|
||||
orderMap[order[i]] = i;
|
||||
return pluginIds.slice().sort(function (a, b) {
|
||||
var aOrder = orderMap[a] !== undefined ? orderMap[a] : 9999;
|
||||
var bOrder = orderMap[b] !== undefined ? orderMap[b] : 9999;
|
||||
return aOrder - bOrder;
|
||||
});
|
||||
}
|
||||
|
||||
function sortPluginsOrdered(plugins, order) {
|
||||
if (!order || order.length === 0)
|
||||
return plugins;
|
||||
var orderMap = {};
|
||||
for (var i = 0; i < order.length; i++)
|
||||
orderMap[order[i]] = i;
|
||||
return plugins.sort(function (a, b) {
|
||||
var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999;
|
||||
var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999;
|
||||
return aOrder - bOrder;
|
||||
});
|
||||
}
|
||||
@@ -1,391 +0,0 @@
|
||||
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: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 500;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 620;
|
||||
}
|
||||
}
|
||||
readonly property int baseHeight: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 480;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 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);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleWithQuery(query) {
|
||||
if (spotlightOpen) {
|
||||
hide();
|
||||
} else {
|
||||
showWithQuery(query);
|
||||
}
|
||||
}
|
||||
|
||||
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:spotlight"
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
.pragma library
|
||||
|
||||
.import "ControllerUtils.js" as Utils
|
||||
|
||||
function transformApp(app, override, defaultActions, primaryActionLabel) {
|
||||
var appId = app.id || app.execString || app.exec || "";
|
||||
|
||||
var actions = [];
|
||||
if (app.actions && app.actions.length > 0) {
|
||||
for (var i = 0; i < app.actions.length; i++) {
|
||||
actions.push({
|
||||
name: app.actions[i].name,
|
||||
icon: "play_arrow",
|
||||
actionData: app.actions[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: appId,
|
||||
type: "app",
|
||||
name: override?.name || app.name || "",
|
||||
subtitle: override?.comment || app.comment || "",
|
||||
icon: override?.icon || app.icon || "application-x-executable",
|
||||
iconType: "image",
|
||||
section: "apps",
|
||||
data: app,
|
||||
keywords: app.keywords || [],
|
||||
actions: actions,
|
||||
primaryAction: {
|
||||
name: primaryActionLabel,
|
||||
icon: "open_in_new",
|
||||
action: "launch"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function transformCoreApp(app, openLabel) {
|
||||
var iconName = "apps";
|
||||
var iconType = "material";
|
||||
|
||||
if (app.icon) {
|
||||
if (app.icon.startsWith("svg+corner:")) {
|
||||
iconType = "composite";
|
||||
} else if (app.icon.startsWith("material:")) {
|
||||
iconName = app.icon.substring(9);
|
||||
} else {
|
||||
iconName = app.icon;
|
||||
iconType = "image";
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: app.builtInPluginId || app.action || "",
|
||||
type: "app",
|
||||
name: app.name || "",
|
||||
subtitle: app.comment || "",
|
||||
icon: iconName,
|
||||
iconType: iconType,
|
||||
iconFull: app.icon,
|
||||
section: "apps",
|
||||
data: app,
|
||||
isCore: true,
|
||||
actions: [],
|
||||
primaryAction: {
|
||||
name: openLabel,
|
||||
icon: "open_in_new",
|
||||
action: "launch"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function transformBuiltInLauncherItem(item, pluginId, openLabel) {
|
||||
var rawIcon = item.icon || "extension";
|
||||
var icon = Utils.stripIconPrefix(rawIcon);
|
||||
var iconType = item.iconType;
|
||||
if (!iconType) {
|
||||
if (rawIcon.startsWith("material:"))
|
||||
iconType = "material";
|
||||
else if (rawIcon.startsWith("unicode:"))
|
||||
iconType = "unicode";
|
||||
else
|
||||
iconType = "image";
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.action || "",
|
||||
type: "plugin",
|
||||
name: item.name || "",
|
||||
subtitle: item.comment || "",
|
||||
icon: icon,
|
||||
iconType: iconType,
|
||||
section: "plugin_" + pluginId,
|
||||
data: item,
|
||||
pluginId: pluginId,
|
||||
isBuiltInLauncher: true,
|
||||
keywords: item.keywords || [],
|
||||
actions: [],
|
||||
primaryAction: {
|
||||
name: openLabel,
|
||||
icon: "open_in_new",
|
||||
action: "execute"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel) {
|
||||
var filename = file.path ? file.path.split("/").pop() : "";
|
||||
var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
|
||||
|
||||
return {
|
||||
id: file.path || "",
|
||||
type: "file",
|
||||
name: filename,
|
||||
subtitle: dirname,
|
||||
icon: Utils.getFileIcon(filename),
|
||||
iconType: "material",
|
||||
section: "files",
|
||||
data: file,
|
||||
actions: [
|
||||
{
|
||||
name: openFolderLabel,
|
||||
icon: "folder_open",
|
||||
action: "open_folder"
|
||||
},
|
||||
{
|
||||
name: copyPathLabel,
|
||||
icon: "content_copy",
|
||||
action: "copy_path"
|
||||
}
|
||||
],
|
||||
primaryAction: {
|
||||
name: openLabel,
|
||||
icon: "open_in_new",
|
||||
action: "open"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function transformPluginItem(item, pluginId, selectLabel) {
|
||||
var rawIcon = item.icon || "extension";
|
||||
var icon = Utils.stripIconPrefix(rawIcon);
|
||||
var iconType = item.iconType;
|
||||
if (!iconType) {
|
||||
if (rawIcon.startsWith("material:"))
|
||||
iconType = "material";
|
||||
else if (rawIcon.startsWith("unicode:"))
|
||||
iconType = "unicode";
|
||||
else
|
||||
iconType = "image";
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id || item.name || "",
|
||||
type: "plugin",
|
||||
name: item.name || "",
|
||||
subtitle: item.comment || item.description || "",
|
||||
icon: icon,
|
||||
iconType: iconType,
|
||||
section: "plugin_" + pluginId,
|
||||
data: item,
|
||||
pluginId: pluginId,
|
||||
keywords: item.keywords || [],
|
||||
actions: item.actions || [],
|
||||
primaryAction: item.primaryAction || {
|
||||
name: selectLabel,
|
||||
icon: "check",
|
||||
action: "execute"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createCalculatorItem(calc, query, copyLabel) {
|
||||
return {
|
||||
id: "calculator_result",
|
||||
type: "calculator",
|
||||
name: calc.displayResult,
|
||||
subtitle: query + " =",
|
||||
icon: "calculate",
|
||||
iconType: "material",
|
||||
section: "calculator",
|
||||
data: {
|
||||
expression: calc.expression,
|
||||
result: calc.result
|
||||
},
|
||||
actions: [],
|
||||
primaryAction: {
|
||||
name: copyLabel,
|
||||
icon: "content_copy",
|
||||
action: "copy"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createPluginBrowseItem(pluginId, plugin, trigger, isBuiltIn, isAllowed, browseLabel, triggerLabel, noTriggerLabel) {
|
||||
var rawIcon = isBuiltIn ? (plugin.cornerIcon || "extension") : (plugin.icon || "extension");
|
||||
return {
|
||||
id: "browse_" + pluginId,
|
||||
type: "plugin_browse",
|
||||
name: plugin.name || pluginId,
|
||||
subtitle: trigger ? triggerLabel.replace("%1", trigger) : noTriggerLabel,
|
||||
icon: isBuiltIn ? rawIcon : Utils.stripIconPrefix(rawIcon),
|
||||
iconType: isBuiltIn ? "material" : Utils.detectIconType(rawIcon),
|
||||
section: "browse_plugins",
|
||||
data: {
|
||||
pluginId: pluginId,
|
||||
plugin: plugin,
|
||||
isBuiltIn: isBuiltIn
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
name: "All",
|
||||
icon: isAllowed ? "visibility" : "visibility_off",
|
||||
action: "toggle_all_visibility"
|
||||
}
|
||||
],
|
||||
primaryAction: {
|
||||
name: browseLabel,
|
||||
icon: "arrow_forward",
|
||||
action: "browse_plugin"
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,813 +0,0 @@
|
||||
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 (!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 (event.modifiers & Qt.ShiftModifier) {
|
||||
controller.pasteSelected();
|
||||
return;
|
||||
}
|
||||
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
|
||||
readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter
|
||||
height: showFooter ? 32 : 0
|
||||
visible: showFooter
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,496 +0,0 @@
|
||||
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")
|
||||
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 bool isCoreApp: item?.type === "app" && item?.isCore
|
||||
readonly property var coreAppData: isCoreApp ? item?.data ?? null : null
|
||||
readonly property var desktopEntry: !isCoreApp ? (item?.data ?? null) : null
|
||||
readonly property string appId: {
|
||||
if (isCoreApp) {
|
||||
return item?.id || coreAppData?.builtInPluginId || "";
|
||||
}
|
||||
return desktopEntry?.id || desktopEntry?.execString || "";
|
||||
}
|
||||
readonly property bool isPinned: appId ? SessionData.isPinnedApp(appId) : false
|
||||
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 (item?.type === "app") {
|
||||
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"
|
||||
});
|
||||
|
||||
if (isRegularApp && SessionService.nvidiaCommand) {
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: "memory",
|
||||
text: I18n.tr("Launch on dGPU"),
|
||||
action: launchWithNvidia
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: "launch",
|
||||
text: I18n.tr("Launch"),
|
||||
action: launchApp
|
||||
});
|
||||
|
||||
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 (isCoreApp) {
|
||||
if (!coreAppData)
|
||||
return;
|
||||
AppSearchService.executeCoreApp(coreAppData);
|
||||
controller?.itemExecuted();
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
.pragma library
|
||||
|
||||
function getFirstItemIndex(flatModel) {
|
||||
for (var i = 0; i < flatModel.length; i++) {
|
||||
if (!flatModel[i].isHeader)
|
||||
return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function findNextNonHeaderIndex(flatModel, startIndex) {
|
||||
for (var i = startIndex; i < flatModel.length; i++) {
|
||||
if (!flatModel[i].isHeader)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function findPrevNonHeaderIndex(flatModel, startIndex) {
|
||||
for (var i = startIndex; i >= 0; i--) {
|
||||
if (!flatModel[i].isHeader)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function getSectionBounds(flatModel, sectionId) {
|
||||
var start = -1, end = -1;
|
||||
for (var i = 0; i < flatModel.length; i++) {
|
||||
if (flatModel[i].isHeader && flatModel[i].section?.id === sectionId) {
|
||||
start = i + 1;
|
||||
} else if (start >= 0 && !flatModel[i].isHeader && flatModel[i].sectionId === sectionId) {
|
||||
end = i;
|
||||
} else if (start >= 0 && end >= 0 && flatModel[i].sectionId !== sectionId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
start: start,
|
||||
end: end,
|
||||
count: end >= start ? end - start + 1 : 0
|
||||
};
|
||||
}
|
||||
|
||||
function getGridColumns(viewMode, gridColumns) {
|
||||
switch (viewMode) {
|
||||
case "tile":
|
||||
return 3;
|
||||
case "grid":
|
||||
return gridColumns;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateNextIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) {
|
||||
if (flatModel.length === 0)
|
||||
return selectedFlatIndex;
|
||||
|
||||
var entry = flatModel[selectedFlatIndex];
|
||||
if (!entry || entry.isHeader) {
|
||||
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
|
||||
return next !== -1 ? next : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId);
|
||||
if (actualViewMode === "list") {
|
||||
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
|
||||
return next !== -1 ? next : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var bounds = getSectionBounds(flatModel, entry.sectionId);
|
||||
var cols = getGridColumns(actualViewMode, gridColumns);
|
||||
var posInSection = selectedFlatIndex - bounds.start;
|
||||
var newPosInSection = posInSection + cols;
|
||||
|
||||
if (newPosInSection < bounds.count) {
|
||||
return bounds.start + newPosInSection;
|
||||
}
|
||||
|
||||
var nextSection = findNextNonHeaderIndex(flatModel, bounds.end + 1);
|
||||
return nextSection !== -1 ? nextSection : selectedFlatIndex;
|
||||
}
|
||||
|
||||
function calculatePrevIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) {
|
||||
if (flatModel.length === 0)
|
||||
return selectedFlatIndex;
|
||||
|
||||
var entry = flatModel[selectedFlatIndex];
|
||||
if (!entry || entry.isHeader) {
|
||||
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
|
||||
return prev !== -1 ? prev : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId);
|
||||
if (actualViewMode === "list") {
|
||||
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
|
||||
return prev !== -1 ? prev : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var bounds = getSectionBounds(flatModel, entry.sectionId);
|
||||
var cols = getGridColumns(actualViewMode, gridColumns);
|
||||
var posInSection = selectedFlatIndex - bounds.start;
|
||||
var newPosInSection = posInSection - cols;
|
||||
|
||||
if (newPosInSection >= 0) {
|
||||
return bounds.start + newPosInSection;
|
||||
}
|
||||
|
||||
var prevItem = findPrevNonHeaderIndex(flatModel, bounds.start - 1);
|
||||
return prevItem !== -1 ? prevItem : selectedFlatIndex;
|
||||
}
|
||||
|
||||
function calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) {
|
||||
if (flatModel.length === 0)
|
||||
return selectedFlatIndex;
|
||||
|
||||
var entry = flatModel[selectedFlatIndex];
|
||||
if (!entry || entry.isHeader) {
|
||||
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
|
||||
return next !== -1 ? next : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var viewMode = getSectionViewModeFn(entry.sectionId);
|
||||
if (viewMode === "list") {
|
||||
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
|
||||
return next !== -1 ? next : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var bounds = getSectionBounds(flatModel, entry.sectionId);
|
||||
var posInSection = selectedFlatIndex - bounds.start;
|
||||
if (posInSection + 1 < bounds.count) {
|
||||
return bounds.start + posInSection + 1;
|
||||
}
|
||||
return selectedFlatIndex;
|
||||
}
|
||||
|
||||
function calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) {
|
||||
if (flatModel.length === 0)
|
||||
return selectedFlatIndex;
|
||||
|
||||
var entry = flatModel[selectedFlatIndex];
|
||||
if (!entry || entry.isHeader) {
|
||||
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
|
||||
return prev !== -1 ? prev : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var viewMode = getSectionViewModeFn(entry.sectionId);
|
||||
if (viewMode === "list") {
|
||||
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
|
||||
return prev !== -1 ? prev : selectedFlatIndex;
|
||||
}
|
||||
|
||||
var bounds = getSectionBounds(flatModel, entry.sectionId);
|
||||
var posInSection = selectedFlatIndex - bounds.start;
|
||||
if (posInSection > 0) {
|
||||
return bounds.start + posInSection - 1;
|
||||
}
|
||||
return selectedFlatIndex;
|
||||
}
|
||||
|
||||
function calculateNextSectionIndex(flatModel, selectedFlatIndex) {
|
||||
var currentSection = null;
|
||||
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
|
||||
currentSection = flatModel[selectedFlatIndex].sectionId;
|
||||
}
|
||||
|
||||
var foundCurrent = false;
|
||||
for (var i = 0; i < flatModel.length; i++) {
|
||||
if (flatModel[i].isHeader) {
|
||||
if (foundCurrent) {
|
||||
for (var j = i + 1; j < flatModel.length; j++) {
|
||||
if (!flatModel[j].isHeader)
|
||||
return j;
|
||||
}
|
||||
}
|
||||
if (flatModel[i].section.id === currentSection) {
|
||||
foundCurrent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedFlatIndex;
|
||||
}
|
||||
|
||||
function calculatePrevSectionIndex(flatModel, selectedFlatIndex) {
|
||||
var currentSection = null;
|
||||
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
|
||||
currentSection = flatModel[selectedFlatIndex].sectionId;
|
||||
}
|
||||
|
||||
var lastSectionStart = -1;
|
||||
var prevSectionStart = -1;
|
||||
|
||||
for (var i = 0; i < flatModel.length; i++) {
|
||||
if (flatModel[i].isHeader) {
|
||||
if (flatModel[i].section.id === currentSection) {
|
||||
break;
|
||||
}
|
||||
prevSectionStart = lastSectionStart;
|
||||
lastSectionStart = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSectionStart >= 0) {
|
||||
for (var j = prevSectionStart + 1; j < flatModel.length; j++) {
|
||||
if (!flatModel[j].isHeader)
|
||||
return j;
|
||||
}
|
||||
}
|
||||
return selectedFlatIndex;
|
||||
}
|
||||
|
||||
function calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems) {
|
||||
if (flatModel.length === 0)
|
||||
return selectedFlatIndex;
|
||||
|
||||
var itemsToSkip = visibleItems || 8;
|
||||
var newIndex = selectedFlatIndex;
|
||||
|
||||
for (var i = 0; i < itemsToSkip; i++) {
|
||||
var next = findNextNonHeaderIndex(flatModel, newIndex + 1);
|
||||
if (next === -1)
|
||||
break;
|
||||
newIndex = next;
|
||||
}
|
||||
|
||||
return newIndex;
|
||||
}
|
||||
|
||||
function calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems) {
|
||||
if (flatModel.length === 0)
|
||||
return selectedFlatIndex;
|
||||
|
||||
var itemsToSkip = visibleItems || 8;
|
||||
var newIndex = selectedFlatIndex;
|
||||
|
||||
for (var i = 0; i < itemsToSkip; i++) {
|
||||
var prev = findPrevNonHeaderIndex(flatModel, newIndex - 1);
|
||||
if (prev === -1)
|
||||
break;
|
||||
newIndex = prev;
|
||||
}
|
||||
|
||||
return newIndex;
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
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 || allModeToggleArea.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
|
||||
|
||||
MouseArea {
|
||||
id: itemArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: root.item?.type === "plugin_browse" ? 40 : 0
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
id: allModeToggle
|
||||
visible: root.item?.type === "plugin_browse"
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: allModeToggleArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
property bool isAllowed: {
|
||||
if (root.item?.type !== "plugin_browse")
|
||||
return false;
|
||||
var pluginId = root.item?.data?.pluginId;
|
||||
if (!pluginId)
|
||||
return false;
|
||||
SettingsData.launcherPluginVisibility;
|
||||
return SettingsData.getPluginAllowWithoutTrigger(pluginId);
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: allModeToggle.isAllowed ? "visibility" : "visibility_off"
|
||||
size: 18
|
||||
color: allModeToggle.isAllowed ? Theme.primary : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: allModeToggleArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
var pluginId = root.item?.data?.pluginId;
|
||||
if (!pluginId)
|
||||
return;
|
||||
SettingsData.setPluginAllowWithoutTrigger(pluginId, !allModeToggle.isAllowed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse"
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,484 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
.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
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
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 toplevelId: item?.data?.toplevelId ?? ""
|
||||
readonly property var waylandToplevel: {
|
||||
if (!toplevelId || !item?.pluginId)
|
||||
return null;
|
||||
const pluginInstance = PluginService.pluginInstances[item.pluginId];
|
||||
if (!pluginInstance?.getToplevelById)
|
||||
return null;
|
||||
return pluginInstance.getToplevelById(toplevelId);
|
||||
}
|
||||
readonly property bool hasScreencopy: waylandToplevel !== null
|
||||
|
||||
readonly property string iconValue: {
|
||||
if (!item)
|
||||
return "";
|
||||
if (hasScreencopy)
|
||||
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
|
||||
|
||||
ScreencopyView {
|
||||
id: screencopyView
|
||||
anchors.fill: parent
|
||||
captureSource: root.waylandToplevel
|
||||
live: root.hasScreencopy
|
||||
visible: root.hasScreencopy
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.isHovered ? Theme.withAlpha(Theme.surfaceVariant, 0.2) : "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
visible: !root.hasScreencopy
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: attributionBadge
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Theme.spacingXS
|
||||
width: root.hasScreencopy ? 28 : 40
|
||||
height: root.hasScreencopy ? 28 : 16
|
||||
radius: root.hasScreencopy ? 14 : 4
|
||||
color: root.hasScreencopy ? Theme.surfaceContainer : "transparent"
|
||||
visible: attributionImage.status === Image.Ready
|
||||
opacity: 0.95
|
||||
|
||||
Image {
|
||||
id: attributionImage
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.hasScreencopy ? 4 : 0
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: root.item?.data?.attribution || ""
|
||||
mipmap: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
var scenePos = mapToItem(null, mouse.x, mouse.y);
|
||||
root.rightClicked(scenePos.x, scenePos.y);
|
||||
return;
|
||||
}
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
onPositionChanged: {
|
||||
if (root.controller)
|
||||
root.controller.keyboardNavigationActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,7 +539,7 @@ Rectangle {
|
||||
|
||||
Item {
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
height: Theme.spacingXS
|
||||
height: Theme.spacingS
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
@@ -717,7 +717,7 @@ Rectangle {
|
||||
|
||||
Item {
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
height: Theme.spacingXS
|
||||
height: Theme.spacingS
|
||||
visible: !root.searchActive
|
||||
}
|
||||
|
||||
|
||||
237
quickshell/Modals/Spotlight/FileSearchController.qml
Normal file
237
quickshell/Modals/Spotlight/FileSearchController.qml
Normal file
@@ -0,0 +1,237 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
155
quickshell/Modals/Spotlight/FileSearchEntry.qml
Normal file
155
quickshell/Modals/Spotlight/FileSearchEntry.qml
Normal file
@@ -0,0 +1,155 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
269
quickshell/Modals/Spotlight/FileSearchResults.qml
Normal file
269
quickshell/Modals/Spotlight/FileSearchResults.qml
Normal file
@@ -0,0 +1,269 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
864
quickshell/Modals/Spotlight/SpotlightContent.qml
Normal file
864
quickshell/Modals/Spotlight/SpotlightContent.qml
Normal file
@@ -0,0 +1,864 @@
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
119
quickshell/Modals/Spotlight/SpotlightContextMenu.qml
Normal file
119
quickshell/Modals/Spotlight/SpotlightContextMenu.qml
Normal file
@@ -0,0 +1,119 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
411
quickshell/Modals/Spotlight/SpotlightContextMenuContent.qml
Normal file
411
quickshell/Modals/Spotlight/SpotlightContextMenuContent.qml
Normal file
@@ -0,0 +1,411 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
quickshell/Modals/Spotlight/SpotlightContextMenuPopup.qml
Normal file
90
quickshell/Modals/Spotlight/SpotlightContextMenuPopup.qml
Normal file
@@ -0,0 +1,90 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
181
quickshell/Modals/Spotlight/SpotlightModal.qml
Normal file
181
quickshell/Modals/Spotlight/SpotlightModal.qml
Normal file
@@ -0,0 +1,181 @@
|
||||
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
|
||||
}
|
||||
267
quickshell/Modals/Spotlight/SpotlightResults.qml
Normal file
267
quickshell/Modals/Spotlight/SpotlightResults.qml
Normal file
@@ -0,0 +1,267 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
FloatingWindow {
|
||||
id: root
|
||||
|
||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||
|
||||
objectName: "workspaceRenameModal"
|
||||
title: I18n.tr("Rename Workspace")
|
||||
minimumSize: Qt.size(400, 160)
|
||||
maximumSize: Qt.size(400, 160)
|
||||
color: Theme.surfaceContainer
|
||||
visible: false
|
||||
|
||||
function show(name) {
|
||||
nameInput.text = name;
|
||||
visible = true;
|
||||
Qt.callLater(() => nameInput.forceActiveFocus());
|
||||
}
|
||||
|
||||
function hide() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
function submitAndClose() {
|
||||
renameWorkspace(nameInput.text);
|
||||
hide();
|
||||
}
|
||||
|
||||
function renameWorkspace(name) {
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.renameWorkspace(name);
|
||||
} else if (CompositorService.isHyprland) {
|
||||
HyprlandService.renameWorkspace(name);
|
||||
} else {
|
||||
console.warn("WorkspaceRenameModal: rename not supported for this compositor");
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
Qt.callLater(() => nameInput.forceActiveFocus());
|
||||
return;
|
||||
}
|
||||
nameInput.text = "";
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: contentFocusScope
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentCol
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingL * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Item {
|
||||
width: contentCol.width
|
||||
height: Math.max(headerText.height, buttonRow.height)
|
||||
|
||||
MouseArea {
|
||||
anchors.left: parent.left
|
||||
anchors.right: buttonRow.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
height: parent.height
|
||||
onPressed: windowControls.tryStartMove()
|
||||
onDoubleClicked: windowControls.tryToggleMaximize()
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: headerText
|
||||
text: I18n.tr("Enter a new name for this workspace")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - buttonRow.width - Theme.spacingM
|
||||
}
|
||||
|
||||
Row {
|
||||
id: buttonRow
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls.supported && windowControls.canMaximize
|
||||
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: windowControls.tryToggleMaximize()
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: inputFieldHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: nameInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: nameInput.activeFocus ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: nameInput.forceActiveFocus()
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: nameInput
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
placeholderText: I18n.tr("Workspace name")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.visible
|
||||
onAccepted: submitAndClose()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
id: cancelText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: hide()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, renameText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: renameArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
id: renameText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Rename")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: renameArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: submitAndClose()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingWindowControls {
|
||||
id: windowControls
|
||||
targetWindow: root
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "workspace-rename"
|
||||
|
||||
function open(): string {
|
||||
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
|
||||
show(ws?.name || "");
|
||||
return "WORKSPACE_RENAME_MODAL_OPENED";
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
hide();
|
||||
return "WORKSPACE_RENAME_MODAL_CLOSED";
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
456
quickshell/Modules/AppDrawer/AppLauncher.qml
Normal file
456
quickshell/Modules/AppDrawer/AppLauncher.qml
Normal file
@@ -0,0 +1,456 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
142
quickshell/Modules/AppDrawer/CategorySelector.qml
Normal file
142
quickshell/Modules/AppDrawer/CategorySelector.qml
Normal file
@@ -0,0 +1,142 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,24 +46,11 @@ Item {
|
||||
|
||||
function getRealWorkspaces() {
|
||||
if (CompositorService.isNiri) {
|
||||
const fallbackWorkspaces = [
|
||||
{
|
||||
"id": 1,
|
||||
"idx": 0,
|
||||
"name": ""
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"idx": 1,
|
||||
"name": ""
|
||||
}
|
||||
];
|
||||
if (!barWindow.screenName || SettingsData.workspaceFollowFocus) {
|
||||
const currentWorkspaces = NiriService.getCurrentOutputWorkspaces();
|
||||
return currentWorkspaces.length > 0 ? currentWorkspaces : fallbackWorkspaces;
|
||||
return NiriService.getCurrentOutputWorkspaceNumbers();
|
||||
}
|
||||
const workspaces = NiriService.allWorkspaces.filter(ws => ws.output === barWindow.screenName);
|
||||
return workspaces.length > 0 ? workspaces : fallbackWorkspaces;
|
||||
const workspaces = NiriService.allWorkspaces.filter(ws => ws.output === barWindow.screenName).map(ws => ws.idx + 1);
|
||||
return workspaces.length > 0 ? workspaces : [1, 2];
|
||||
} else if (CompositorService.isHyprland) {
|
||||
const workspaces = Hyprland.workspaces?.values || [];
|
||||
|
||||
@@ -131,7 +118,7 @@ Item {
|
||||
return NiriService.getCurrentWorkspaceNumber();
|
||||
}
|
||||
const activeWs = NiriService.allWorkspaces.find(ws => ws.output === barWindow.screenName && ws.is_active);
|
||||
return activeWs ? activeWs.idx : 1;
|
||||
return activeWs ? activeWs.idx + 1 : 1;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
const monitors = Hyprland.monitors?.values || [];
|
||||
const currentMonitor = monitors.find(monitor => monitor.name === barWindow.screenName);
|
||||
@@ -164,16 +151,12 @@ Item {
|
||||
|
||||
if (CompositorService.isNiri) {
|
||||
const currentWs = getCurrentWorkspace();
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === currentWs);
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws === 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) {
|
||||
const nextWorkspace = realWorkspaces[nextIndex];
|
||||
if (!nextWorkspace || nextWorkspace.idx === undefined) {
|
||||
return;
|
||||
}
|
||||
NiriService.switchToWorkspace(nextWorkspace.idx);
|
||||
NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1);
|
||||
}
|
||||
} else if (CompositorService.isHyprland) {
|
||||
const currentWs = getCurrentWorkspace();
|
||||
@@ -302,7 +285,6 @@ Item {
|
||||
"workspaceSwitcher": workspaceSwitcherComponent,
|
||||
"focusedWindow": focusedWindowComponent,
|
||||
"runningApps": runningAppsComponent,
|
||||
"appsDock": appsDockComponent,
|
||||
"clock": clockComponent,
|
||||
"music": mediaComponent,
|
||||
"weather": weatherComponent,
|
||||
@@ -344,7 +326,6 @@ Item {
|
||||
"workspaceSwitcherComponent": workspaceSwitcherComponent,
|
||||
"focusedWindowComponent": focusedWindowComponent,
|
||||
"runningAppsComponent": runningAppsComponent,
|
||||
"appsDockComponent": appsDockComponent,
|
||||
"clockComponent": clockComponent,
|
||||
"mediaComponent": mediaComponent,
|
||||
"weatherComponent": weatherComponent,
|
||||
@@ -662,21 +643,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: appsDockComponent
|
||||
|
||||
AppsDock {
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
barSpacing: barConfig?.spacing ?? 4
|
||||
section: topBarContent.getWidgetSection(parent)
|
||||
parentScreen: barWindow.screen
|
||||
topBar: topBarContent
|
||||
barConfig: topBarContent.barConfig
|
||||
isAutoHideBar: topBarContent.barConfig?.autoHide ?? false
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: clockComponent
|
||||
|
||||
|
||||
@@ -242,8 +242,7 @@ Loader {
|
||||
"colorPicker": components.colorPickerComponent,
|
||||
"systemUpdate": components.systemUpdateComponent,
|
||||
"layout": components.layoutComponent,
|
||||
"powerMenuButton": components.powerMenuButtonComponent,
|
||||
"appsDock": components.appsDockComponent
|
||||
"powerMenuButton": components.powerMenuButtonComponent
|
||||
};
|
||||
|
||||
if (componentMap[widgetId]) {
|
||||
|
||||
@@ -1,867 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var widgetData: null
|
||||
property var barConfig: null
|
||||
property bool isVertical: axis?.isVertical ?? false
|
||||
property var axis: null
|
||||
property string section: "left"
|
||||
property var parentScreen
|
||||
property var hoveredItem: null
|
||||
property var topBar: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
property real barSpacing: 4
|
||||
property bool isAutoHideBar: false
|
||||
readonly property real horizontalPadding: (barConfig?.noBackground ?? false) ? 2 : Theme.spacingS
|
||||
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
|
||||
|
||||
property int draggedIndex: -1
|
||||
property int dropTargetIndex: -1
|
||||
property bool suppressShiftAnimation: false
|
||||
property int pinnedAppCount: 0
|
||||
|
||||
readonly property real effectiveBarThickness: {
|
||||
if (barThickness > 0 && barSpacing > 0) {
|
||||
return barThickness + barSpacing;
|
||||
}
|
||||
const innerPadding = barConfig?.innerPadding ?? 4;
|
||||
const spacing = barConfig?.spacing ?? 4;
|
||||
return Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding)) + spacing;
|
||||
}
|
||||
|
||||
readonly property var barBounds: {
|
||||
if (!parentScreen || !barConfig) {
|
||||
return {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"wingSize": 0
|
||||
};
|
||||
}
|
||||
const barPosition = axis.edge === "left" ? 2 : (axis.edge === "right" ? 3 : (axis.edge === "top" ? 0 : 1));
|
||||
return SettingsData.getBarBounds(parentScreen, effectiveBarThickness, barPosition, barConfig);
|
||||
}
|
||||
|
||||
readonly property real barY: barBounds.y
|
||||
|
||||
readonly property real minTooltipY: {
|
||||
if (!parentScreen || !isVertical) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isAutoHideBar) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (parentScreen.y > 0) {
|
||||
return effectiveBarThickness;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Dock Logic Helpers ---
|
||||
function movePinnedApp(fromDockIndex, toDockIndex) {
|
||||
if (fromDockIndex === toDockIndex)
|
||||
return;
|
||||
|
||||
const currentPinned = [...(SessionData.barPinnedApps || [])];
|
||||
if (fromDockIndex < 0 || fromDockIndex >= currentPinned.length || toDockIndex < 0 || toDockIndex >= currentPinned.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const movedApp = currentPinned.splice(fromDockIndex, 1)[0];
|
||||
currentPinned.splice(toDockIndex, 0, movedApp);
|
||||
|
||||
SessionData.setBarPinnedApps(currentPinned);
|
||||
}
|
||||
|
||||
property int _desktopEntriesUpdateTrigger: 0
|
||||
property int _toplevelsUpdateTrigger: 0
|
||||
property int _appIdSubstitutionsTrigger: 0
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onToplevelsChanged() {
|
||||
_toplevelsUpdateTrigger++;
|
||||
updateModel();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
function onApplicationsChanged() {
|
||||
_desktopEntriesUpdateTrigger++;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onAppIdSubstitutionsChanged() {
|
||||
_appIdSubstitutionsTrigger++;
|
||||
updateModel();
|
||||
}
|
||||
function onRunningAppsCurrentWorkspaceChanged() {
|
||||
updateModel();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onBarPinnedAppsChanged() {
|
||||
root.suppressShiftAnimation = true;
|
||||
root.draggedIndex = -1;
|
||||
root.dropTargetIndex = -1;
|
||||
updateModel();
|
||||
Qt.callLater(() => {
|
||||
root.suppressShiftAnimation = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
property var dockItems: []
|
||||
|
||||
function isOnScreen(toplevel, screenName) {
|
||||
if (!toplevel.screens)
|
||||
return false;
|
||||
for (let i = 0; i < toplevel.screens.length; i++) {
|
||||
if (toplevel.screens[i]?.name === screenName)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getCoreAppData(appId) {
|
||||
if (typeof AppSearchService === "undefined")
|
||||
return null;
|
||||
const coreApps = AppSearchService.coreApps || [];
|
||||
for (let i = 0; i < coreApps.length; i++) {
|
||||
if (coreApps[i].builtInPluginId === appId)
|
||||
return coreApps[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCoreAppDataByTitle(windowTitle) {
|
||||
if (typeof AppSearchService === "undefined" || !windowTitle)
|
||||
return null;
|
||||
const coreApps = AppSearchService.coreApps || [];
|
||||
for (let i = 0; i < coreApps.length; i++) {
|
||||
if (coreApps[i].name === windowTitle)
|
||||
return coreApps[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function updateModel() {
|
||||
const items = [];
|
||||
const pinnedApps = [...(SessionData.barPinnedApps || [])];
|
||||
_toplevelsUpdateTrigger;
|
||||
const allToplevels = CompositorService.sortedToplevels;
|
||||
|
||||
let sortedToplevels = allToplevels;
|
||||
if (SettingsData.runningAppsCurrentWorkspace && parentScreen) {
|
||||
sortedToplevels = CompositorService.filterCurrentWorkspace(allToplevels, parentScreen.name) || [];
|
||||
}
|
||||
|
||||
const appGroups = new Map();
|
||||
|
||||
pinnedApps.forEach(rawAppId => {
|
||||
const appId = Paths.moddedAppId(rawAppId);
|
||||
const coreAppData = getCoreAppData(appId);
|
||||
appGroups.set(appId, {
|
||||
appId: appId,
|
||||
isPinned: true,
|
||||
windows: [],
|
||||
isCoreApp: coreAppData !== null,
|
||||
coreAppData: coreAppData
|
||||
});
|
||||
});
|
||||
|
||||
sortedToplevels.forEach((toplevel, index) => {
|
||||
const rawAppId = toplevel.appId || "unknown";
|
||||
let appId = Paths.moddedAppId(rawAppId);
|
||||
|
||||
let coreAppData = null;
|
||||
if (rawAppId === "org.quickshell") {
|
||||
coreAppData = getCoreAppDataByTitle(toplevel.title);
|
||||
if (coreAppData) {
|
||||
appId = coreAppData.builtInPluginId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!appGroups.has(appId)) {
|
||||
appGroups.set(appId, {
|
||||
appId: appId,
|
||||
isPinned: false,
|
||||
windows: [],
|
||||
isCoreApp: coreAppData !== null,
|
||||
coreAppData: coreAppData
|
||||
});
|
||||
}
|
||||
|
||||
appGroups.get(appId).windows.push({
|
||||
toplevel: toplevel,
|
||||
index: index,
|
||||
windowTitle: toplevel.title
|
||||
});
|
||||
});
|
||||
|
||||
const pinnedGroups = [];
|
||||
const unpinnedGroups = [];
|
||||
|
||||
Array.from(appGroups.entries()).forEach(([appId, group]) => {
|
||||
const firstWindow = group.windows.length > 0 ? group.windows[0] : null;
|
||||
|
||||
const item = {
|
||||
uniqueKey: "grouped_" + appId,
|
||||
type: "grouped",
|
||||
appId: appId,
|
||||
toplevel: firstWindow ? firstWindow.toplevel : null,
|
||||
isPinned: group.isPinned,
|
||||
isRunning: group.windows.length > 0,
|
||||
windowCount: group.windows.length,
|
||||
allWindows: group.windows,
|
||||
isCoreApp: group.isCoreApp || false,
|
||||
coreAppData: group.coreAppData || null
|
||||
};
|
||||
|
||||
if (group.isPinned) {
|
||||
pinnedGroups.push(item);
|
||||
} else {
|
||||
unpinnedGroups.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
pinnedGroups.forEach(item => items.push(item));
|
||||
|
||||
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
|
||||
items.push({
|
||||
uniqueKey: "separator_grouped",
|
||||
type: "separator",
|
||||
appId: "__SEPARATOR__",
|
||||
toplevel: null,
|
||||
isPinned: false,
|
||||
isRunning: false
|
||||
});
|
||||
}
|
||||
|
||||
unpinnedGroups.forEach(item => items.push(item));
|
||||
|
||||
root.pinnedAppCount = pinnedGroups.length;
|
||||
dockItems = items;
|
||||
}
|
||||
|
||||
Component.onCompleted: updateModel()
|
||||
|
||||
readonly property int calculatedSize: {
|
||||
const count = dockItems.length;
|
||||
if (count === 0)
|
||||
return 0;
|
||||
|
||||
if (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) {
|
||||
return count * 24 + (count - 1) * Theme.spacingXS + horizontalPadding * 2;
|
||||
} else {
|
||||
return count * (24 + Theme.spacingXS + 120) + (count - 1) * Theme.spacingXS + horizontalPadding * 2;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real realCalculatedSize: {
|
||||
let total = horizontalPadding * 2;
|
||||
const compact = (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode);
|
||||
|
||||
for (let i = 0; i < dockItems.length; i++) {
|
||||
const item = dockItems[i];
|
||||
let itemSize = 0;
|
||||
if (item.type === "separator") {
|
||||
itemSize = 8;
|
||||
} else {
|
||||
itemSize = compact ? 24 : (24 + Theme.spacingXS + 120);
|
||||
}
|
||||
|
||||
total += itemSize;
|
||||
if (i < dockItems.length - 1)
|
||||
total += Theme.spacingXS;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
width: dockItems.length > 0 ? (isVertical ? barThickness : realCalculatedSize) : 0
|
||||
height: dockItems.length > 0 ? (isVertical ? realCalculatedSize : barThickness) : 0
|
||||
visible: dockItems.length > 0
|
||||
|
||||
Item {
|
||||
id: visualBackground
|
||||
width: root.isVertical ? root.widgetThickness : root.realCalculatedSize
|
||||
height: root.isVertical ? root.realCalculatedSize : root.widgetThickness
|
||||
anchors.centerIn: parent
|
||||
clip: false
|
||||
|
||||
Rectangle {
|
||||
id: outline
|
||||
anchors.centerIn: parent
|
||||
width: {
|
||||
const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0;
|
||||
return parent.width + borderWidth * 2;
|
||||
}
|
||||
height: {
|
||||
const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0;
|
||||
return parent.height + borderWidth * 2;
|
||||
}
|
||||
radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius
|
||||
color: "transparent"
|
||||
border.width: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
|
||||
border.color: {
|
||||
if (!(barConfig?.widgetOutlineEnabled ?? false)) {
|
||||
return "transparent";
|
||||
}
|
||||
const colorOption = barConfig?.widgetOutlineColor || "primary";
|
||||
const opacity = barConfig?.widgetOutlineOpacity ?? 1.0;
|
||||
switch (colorOption) {
|
||||
case "surfaceText":
|
||||
return Theme.withAlpha(Theme.surfaceText, opacity);
|
||||
case "secondary":
|
||||
return Theme.withAlpha(Theme.secondary, opacity);
|
||||
case "primary":
|
||||
return Theme.withAlpha(Theme.primary, opacity);
|
||||
default:
|
||||
return Theme.withAlpha(Theme.primary, opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius
|
||||
color: {
|
||||
if (dockItems.length === 0)
|
||||
return "transparent";
|
||||
if ((barConfig?.noBackground ?? false))
|
||||
return "transparent";
|
||||
|
||||
const baseColor = Theme.widgetBaseBackgroundColor;
|
||||
const transparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0;
|
||||
if (Theme.widgetBackgroundHasAlpha) {
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency);
|
||||
}
|
||||
return Theme.withAlpha(baseColor, transparency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: layoutLoader
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: root.isVertical ? columnLayout : rowLayout
|
||||
}
|
||||
|
||||
Component {
|
||||
id: rowLayout
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: ScriptModel {
|
||||
values: root.dockItems
|
||||
objectProp: "uniqueKey"
|
||||
}
|
||||
|
||||
delegate: dockDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: columnLayout
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.dockItems
|
||||
objectProp: "uniqueKey"
|
||||
}
|
||||
delegate: dockDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: tooltipLoader
|
||||
active: false
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: dockDelegate
|
||||
Item {
|
||||
id: delegateItem
|
||||
property bool isSeparator: modelData.type === "separator"
|
||||
|
||||
readonly property real visualSize: isSeparator ? 8 : ((widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? 24 : (24 + Theme.spacingXS + 120))
|
||||
readonly property real visualWidth: root.isVertical ? root.barThickness : visualSize
|
||||
readonly property real visualHeight: root.isVertical ? visualSize : root.barThickness
|
||||
|
||||
width: visualWidth
|
||||
height: visualHeight
|
||||
|
||||
z: (dragHandler.dragging) ? 100 : 0
|
||||
|
||||
// --- Drag and Drop Shift Animation Logic ---
|
||||
property real shiftOffset: {
|
||||
if (root.draggedIndex < 0 || !modelData.isPinned || isSeparator)
|
||||
return 0;
|
||||
if (index === root.draggedIndex)
|
||||
return 0;
|
||||
|
||||
const dragIdx = root.draggedIndex;
|
||||
const dropIdx = root.dropTargetIndex;
|
||||
const myIdx = index;
|
||||
const shiftAmount = visualSize + Theme.spacingXS;
|
||||
|
||||
if (dropIdx < 0)
|
||||
return 0;
|
||||
if (dragIdx < dropIdx && myIdx > dragIdx && myIdx <= dropIdx)
|
||||
return -shiftAmount;
|
||||
if (dragIdx > dropIdx && myIdx >= dropIdx && myIdx < dragIdx)
|
||||
return shiftAmount;
|
||||
return 0;
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
x: root.isVertical ? 0 : delegateItem.shiftOffset
|
||||
y: root.isVertical ? delegateItem.shiftOffset : 0
|
||||
|
||||
Behavior on x {
|
||||
enabled: !root.suppressShiftAnimation
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
enabled: !root.suppressShiftAnimation
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: isSeparator
|
||||
width: root.isVertical ? root.barThickness * 0.6 : 2
|
||||
height: root.isVertical ? 2 : root.barThickness * 0.6
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
radius: 1
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
Item {
|
||||
id: appItem
|
||||
visible: !isSeparator
|
||||
anchors.fill: parent
|
||||
|
||||
property bool isFocused: {
|
||||
if (modelData.type === "grouped") {
|
||||
return modelData.allWindows.some(w => w.toplevel && w.toplevel.activated);
|
||||
}
|
||||
return modelData.toplevel ? modelData.toplevel.activated : false;
|
||||
}
|
||||
|
||||
property var appId: modelData.appId
|
||||
property int windowCount: modelData.windowCount || (modelData.isRunning ? 1 : 0)
|
||||
property string windowTitle: {
|
||||
if (modelData.type === "grouped") {
|
||||
const active = modelData.allWindows.find(w => w.toplevel && w.toplevel.activated);
|
||||
if (active)
|
||||
return active.windowTitle || "(Unnamed)";
|
||||
if (modelData.allWindows.length > 0)
|
||||
return modelData.allWindows[0].windowTitle || "(Unnamed)";
|
||||
return "";
|
||||
}
|
||||
return modelData.toplevel ? (modelData.toplevel.title || "(Unnamed)") : "";
|
||||
}
|
||||
|
||||
property string tooltipText: {
|
||||
root._desktopEntriesUpdateTrigger;
|
||||
const moddedId = Paths.moddedAppId(appId);
|
||||
const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null;
|
||||
const appName = appId ? Paths.getAppName(appId, desktopEntry) : "Unknown";
|
||||
|
||||
if (modelData.type === "grouped" && windowCount > 1) {
|
||||
return appName + " (" + windowCount + " windows)";
|
||||
}
|
||||
return appName + (windowTitle ? " • " + windowTitle : "");
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
x: (dragHandler.dragging && !root.isVertical) ? dragHandler.dragAxisOffset : 0
|
||||
y: (dragHandler.dragging && root.isVertical) ? dragHandler.dragAxisOffset : 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: visualContent
|
||||
width: root.isVertical ? 24 : delegateItem.visualSize
|
||||
height: root.isVertical ? delegateItem.visualSize : 24
|
||||
anchors.centerIn: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (appItem.isFocused) {
|
||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
|
||||
}
|
||||
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent";
|
||||
}
|
||||
|
||||
border.width: dragHandler.dragging ? 2 : 0
|
||||
border.color: Theme.primary
|
||||
opacity: dragHandler.dragging ? 0.8 : 1.0
|
||||
|
||||
AppIconRenderer {
|
||||
id: coreIcon
|
||||
readonly property bool isCompact: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
|
||||
anchors.left: (root.isVertical || isCompact) ? undefined : parent.left
|
||||
anchors.leftMargin: (root.isVertical || isCompact) ? 0 : Theme.spacingXS
|
||||
anchors.top: (root.isVertical && !isCompact) ? parent.top : undefined
|
||||
anchors.topMargin: (root.isVertical && !isCompact) ? Theme.spacingXS : 0
|
||||
anchors.centerIn: (root.isVertical || isCompact) ? parent : undefined
|
||||
|
||||
iconSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
materialIconSizeAdjustment: 0
|
||||
iconValue: {
|
||||
if (!modelData || !modelData.isCoreApp || !modelData.coreAppData)
|
||||
return "";
|
||||
const appId = modelData.coreAppData.id || modelData.coreAppData.builtInPluginId;
|
||||
if ((appId === "dms_settings" || appId === "dms_notepad" || appId === "dms_sysmon") && modelData.coreAppData.cornerIcon) {
|
||||
return "material:" + modelData.coreAppData.cornerIcon;
|
||||
}
|
||||
return modelData.coreAppData.icon || "";
|
||||
}
|
||||
colorOverride: Theme.widgetIconColor
|
||||
fallbackText: "?"
|
||||
visible: iconValue !== ""
|
||||
z: 2
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: iconImg
|
||||
readonly property bool isCompact: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
|
||||
anchors.left: (root.isVertical || isCompact) ? undefined : parent.left
|
||||
anchors.leftMargin: (root.isVertical || isCompact) ? 0 : Theme.spacingXS
|
||||
anchors.top: (root.isVertical && !isCompact) ? parent.top : undefined
|
||||
anchors.topMargin: (root.isVertical && !isCompact) ? Theme.spacingXS : 0
|
||||
anchors.centerIn: (root.isVertical || isCompact) ? parent : undefined
|
||||
|
||||
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
source: {
|
||||
root._desktopEntriesUpdateTrigger;
|
||||
root._appIdSubstitutionsTrigger;
|
||||
if (!appItem.appId)
|
||||
return "";
|
||||
if (modelData.isCoreApp)
|
||||
return ""; // Explicitly skip if core app to avoid flickering or wrong look ups
|
||||
const moddedId = Paths.moddedAppId(appItem.appId);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
||||
return Paths.getAppIcon(appItem.appId, desktopEntry);
|
||||
}
|
||||
smooth: true
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready && !coreIcon.visible
|
||||
layer.enabled: appItem.appId === "org.quickshell"
|
||||
layer.smooth: true
|
||||
layer.mipmap: true
|
||||
layer.effect: MultiEffect {
|
||||
saturation: 0
|
||||
colorization: 1
|
||||
colorizationColor: Theme.primary
|
||||
}
|
||||
z: 2
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
readonly property bool isCompact: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
|
||||
anchors.left: (root.isVertical || isCompact) ? undefined : parent.left
|
||||
anchors.leftMargin: (root.isVertical || isCompact) ? 0 : Theme.spacingXS
|
||||
anchors.top: (root.isVertical && !isCompact) ? parent.top : undefined
|
||||
anchors.topMargin: (root.isVertical && !isCompact) ? Theme.spacingXS : 0
|
||||
anchors.centerIn: (root.isVertical || isCompact) ? parent : undefined
|
||||
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
name: "sports_esports"
|
||||
color: Theme.widgetTextColor
|
||||
visible: !iconImg.visible && !coreIcon.visible && Paths.isSteamApp(appItem.appId)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !iconImg.visible && !coreIcon.visible && !Paths.isSteamApp(appItem.appId)
|
||||
text: {
|
||||
root._desktopEntriesUpdateTrigger;
|
||||
if (!appItem.appId)
|
||||
return "?";
|
||||
const moddedId = Paths.moddedAppId(appItem.appId);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
||||
const appName = Paths.getAppName(appItem.appId, desktopEntry);
|
||||
return appName.charAt(0).toUpperCase();
|
||||
}
|
||||
font.pixelSize: 10
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.rightMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? -2 : 2
|
||||
anchors.bottomMargin: -2
|
||||
width: 14
|
||||
height: 14
|
||||
radius: 7
|
||||
color: Theme.primary
|
||||
visible: modelData.type === "grouped" && appItem.windowCount > 1
|
||||
z: 10
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: appItem.windowCount > 9 ? "9+" : appItem.windowCount
|
||||
font.pixelSize: 9
|
||||
color: Theme.surface
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: !root.isVertical && !(widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
|
||||
anchors.left: iconImg.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: appItem.windowTitle || appItem.appId
|
||||
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
|
||||
color: Theme.widgetTextColor
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: modelData.isRunning
|
||||
width: root.isVertical ? 2 : 20
|
||||
height: root.isVertical ? 20 : 2
|
||||
radius: 1
|
||||
color: appItem.isFocused ? Theme.primary : Theme.surfaceText
|
||||
opacity: appItem.isFocused ? 1 : 0.5
|
||||
|
||||
anchors.bottom: root.isVertical ? undefined : parent.bottom
|
||||
anchors.right: root.isVertical ? parent.right : undefined
|
||||
anchors.horizontalCenter: root.isVertical ? undefined : parent.horizontalCenter
|
||||
anchors.verticalCenter: root.isVertical ? parent.verticalCenter : undefined
|
||||
|
||||
anchors.margins: 0
|
||||
z: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for Drag Logic
|
||||
Item {
|
||||
id: dragHandler
|
||||
anchors.fill: parent
|
||||
property bool dragging: false
|
||||
property point dragStartPos: Qt.point(0, 0)
|
||||
property real dragAxisOffset: 0
|
||||
property bool longPressing: false
|
||||
|
||||
Timer {
|
||||
id: longPressTimer
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (modelData.isPinned) {
|
||||
dragHandler.longPressing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
|
||||
onPressed: mouse => {
|
||||
if (mouse.button === Qt.LeftButton && modelData.isPinned) {
|
||||
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
|
||||
longPressTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: mouse => {
|
||||
longPressTimer.stop();
|
||||
const wasDragging = dragHandler.dragging;
|
||||
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
|
||||
|
||||
if (didReorder) {
|
||||
root.movePinnedApp(root.draggedIndex, root.dropTargetIndex);
|
||||
}
|
||||
|
||||
dragHandler.longPressing = false;
|
||||
dragHandler.dragging = false;
|
||||
dragHandler.dragAxisOffset = 0;
|
||||
root.draggedIndex = -1;
|
||||
root.dropTargetIndex = -1;
|
||||
|
||||
if (wasDragging || mouse.button !== Qt.LeftButton)
|
||||
return;
|
||||
|
||||
if (wasDragging || mouse.button !== Qt.LeftButton)
|
||||
return;
|
||||
|
||||
if (modelData.type === "grouped") {
|
||||
if (modelData.windowCount === 0) {
|
||||
if (modelData.isCoreApp && modelData.coreAppData) {
|
||||
AppSearchService.executeCoreApp(modelData.coreAppData);
|
||||
} else {
|
||||
const moddedId = Paths.moddedAppId(modelData.appId);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
||||
if (desktopEntry)
|
||||
SessionService.launchDesktopEntry(desktopEntry);
|
||||
}
|
||||
} else if (modelData.windowCount === 1) {
|
||||
if (modelData.allWindows[0].toplevel)
|
||||
modelData.allWindows[0].toplevel.activate();
|
||||
} else {
|
||||
let currentIndex = -1;
|
||||
for (var i = 0; i < modelData.allWindows.length; i++) {
|
||||
if (modelData.allWindows[i].toplevel.activated) {
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const nextIndex = (currentIndex + 1) % modelData.allWindows.length;
|
||||
modelData.allWindows[nextIndex].toplevel.activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (dragHandler.longPressing && !dragHandler.dragging) {
|
||||
const distance = Math.sqrt(Math.pow(mouse.x - dragHandler.dragStartPos.x, 2) + Math.pow(mouse.y - dragHandler.dragStartPos.y, 2));
|
||||
if (distance > 5) {
|
||||
dragHandler.dragging = true;
|
||||
root.draggedIndex = index;
|
||||
root.dropTargetIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dragHandler.dragging)
|
||||
return;
|
||||
|
||||
const axisOffset = root.isVertical ? (mouse.y - dragHandler.dragStartPos.y) : (mouse.x - dragHandler.dragStartPos.x);
|
||||
dragHandler.dragAxisOffset = axisOffset;
|
||||
|
||||
const itemSize = (root.isVertical ? delegateItem.height : delegateItem.width) + Theme.spacingXS;
|
||||
const slotOffset = Math.round(axisOffset / itemSize);
|
||||
const newTargetIndex = Math.max(0, Math.min(root.pinnedAppCount - 1, index + slotOffset));
|
||||
|
||||
if (newTargetIndex !== root.dropTargetIndex) {
|
||||
root.dropTargetIndex = newTargetIndex;
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
root.hoveredItem = delegateItem;
|
||||
if (isSeparator)
|
||||
return;
|
||||
|
||||
tooltipLoader.active = true;
|
||||
if (tooltipLoader.item) {
|
||||
if (root.isVertical) {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
|
||||
const screenX = root.parentScreen ? root.parentScreen.x : 0;
|
||||
const screenY = root.parentScreen ? root.parentScreen.y : 0;
|
||||
const relativeY = globalPos.y - screenY;
|
||||
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS);
|
||||
const isLeft = root.axis?.edge === "left";
|
||||
const adjustedY = relativeY + root.minTooltipY;
|
||||
const finalX = screenX + tooltipX;
|
||||
tooltipLoader.item.show(appItem.tooltipText, finalX, adjustedY, root.parentScreen, isLeft, !isLeft);
|
||||
} else {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
|
||||
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
|
||||
const isBottom = root.axis?.edge === "bottom";
|
||||
const tooltipY = isBottom ? (screenHeight - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS - 35) : (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS);
|
||||
tooltipLoader.item.show(appItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (root.hoveredItem === delegateItem) {
|
||||
root.hoveredItem = null;
|
||||
if (tooltipLoader.item)
|
||||
tooltipLoader.item.hide();
|
||||
tooltipLoader.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (tooltipLoader.item) {
|
||||
tooltipLoader.item.hide();
|
||||
}
|
||||
tooltipLoader.active = false;
|
||||
contextMenuLoader.active = true;
|
||||
|
||||
if (contextMenuLoader.item) {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
|
||||
|
||||
const isBarVertical = root.axis?.isVertical ?? false;
|
||||
const barEdge = root.axis?.edge ?? "top";
|
||||
|
||||
let x = globalPos.x;
|
||||
let y = globalPos.y;
|
||||
|
||||
if (barEdge === "bottom") {
|
||||
y = (root.parentScreen ? root.parentScreen.height : Screen.height) - root.effectiveBarThickness;
|
||||
} else if (barEdge === "top") {
|
||||
y = root.effectiveBarThickness;
|
||||
} else if (barEdge === "left") {
|
||||
x = root.effectiveBarThickness;
|
||||
} else if (barEdge === "right") {
|
||||
x = (root.parentScreen ? root.parentScreen.width : Screen.width) - root.effectiveBarThickness;
|
||||
}
|
||||
|
||||
const shouldHidePin = modelData.appId === "org.quickshell";
|
||||
const moddedId = Paths.moddedAppId(modelData.appId);
|
||||
const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null;
|
||||
|
||||
contextMenuLoader.item.showAt(x, y, isBarVertical, barEdge, modelData, shouldHidePin, desktopEntry, root.parentScreen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contextMenuLoader
|
||||
active: false
|
||||
source: "AppsDockContextMenu.qml"
|
||||
}
|
||||
}
|
||||
@@ -1,431 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
WlrLayershell.namespace: "dms:dock-context-menu"
|
||||
|
||||
property var appData: null
|
||||
property var anchorItem: null
|
||||
property int margin: 10
|
||||
property bool hidePin: false
|
||||
property var desktopEntry: null
|
||||
property bool isDmsWindow: appData?.appId === "org.quickshell"
|
||||
|
||||
property bool isVertical: false
|
||||
property string edge: "top"
|
||||
property point anchorPos: Qt.point(0, 0)
|
||||
|
||||
function showAt(x, y, vertical, barEdge, data, hidePinOption, entry, targetScreen) {
|
||||
if (targetScreen) {
|
||||
root.screen = targetScreen;
|
||||
}
|
||||
|
||||
anchorPos = Qt.point(x, y);
|
||||
isVertical = vertical ?? false;
|
||||
edge = barEdge ?? "top";
|
||||
|
||||
appData = data;
|
||||
hidePin = hidePinOption || false;
|
||||
desktopEntry = entry || null;
|
||||
|
||||
visible = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
screen: null
|
||||
visible: false
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: menuContainer
|
||||
|
||||
x: {
|
||||
if (root.isVertical) {
|
||||
if (root.edge === "left") {
|
||||
return Math.min(root.width - width - 10, root.anchorPos.x);
|
||||
} else {
|
||||
return Math.max(10, root.anchorPos.x - width);
|
||||
}
|
||||
} else {
|
||||
const left = 10;
|
||||
const right = root.width - width - 10;
|
||||
const want = root.anchorPos.x - width / 2;
|
||||
return Math.max(left, Math.min(right, want));
|
||||
}
|
||||
}
|
||||
y: {
|
||||
if (root.isVertical) {
|
||||
const top = 10;
|
||||
const bottom = root.height - height - 10;
|
||||
const want = root.anchorPos.y - height / 2;
|
||||
return Math.max(top, Math.min(bottom, want));
|
||||
} else {
|
||||
if (root.edge === "top") {
|
||||
return Math.min(root.height - height - 10, root.anchorPos.y);
|
||||
} else {
|
||||
return Math.max(10, root.anchorPos.y - height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
width: Math.min(400, Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2))
|
||||
height: Math.max(60, 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
|
||||
|
||||
opacity: root.visible ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
// Window list for grouped apps
|
||||
Repeater {
|
||||
model: {
|
||||
if (!root.appData || root.appData.type !== "grouped")
|
||||
return [];
|
||||
|
||||
const toplevels = [];
|
||||
const allToplevels = ToplevelManager.toplevels.values;
|
||||
for (let i = 0; i < allToplevels.length; i++) {
|
||||
const toplevel = allToplevels[i];
|
||||
if (toplevel.appId === root.appData.appId) {
|
||||
toplevels.push(toplevel);
|
||||
}
|
||||
}
|
||||
return toplevels;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: closeButton.left
|
||||
anchors.rightMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: (modelData && modelData.title) ? modelData.title : I18n.tr("(Unnamed)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: closeButton
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: closeMouseArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2) : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "close"
|
||||
size: 12
|
||||
color: closeMouseArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData && modelData.close) {
|
||||
modelData.close();
|
||||
}
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: windowArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 24
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData && modelData.activate) {
|
||||
modelData.activate();
|
||||
}
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: {
|
||||
if (!root.appData)
|
||||
return false;
|
||||
if (root.appData.type !== "grouped")
|
||||
return false;
|
||||
return root.appData.windowCount > 0;
|
||||
}
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.desktopEntry && root.desktopEntry.actions ? root.desktopEntry.actions : []
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: actionArea.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.spacingXS
|
||||
|
||||
Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 16
|
||||
height: 16
|
||||
visible: modelData.icon && modelData.icon !== ""
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actionArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData) {
|
||||
SessionService.launchDesktopAction(root.desktopEntry, modelData);
|
||||
}
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: {
|
||||
if (!root.desktopEntry?.actions || root.desktopEntry.actions.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return !root.hidePin || (!root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand);
|
||||
}
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: !root.hidePin
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.appData && root.appData.isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pinArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!root.appData) {
|
||||
return;
|
||||
}
|
||||
if (root.appData.isPinned) {
|
||||
SessionData.removeBarPinnedApp(root.appData.appId);
|
||||
} else {
|
||||
SessionData.addBarPinnedApp(root.appData.appId);
|
||||
}
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: {
|
||||
const hasNvidia = !root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand;
|
||||
const hasWindow = root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0));
|
||||
const hasPinOption = !root.hidePin;
|
||||
const hasContentAbove = hasPinOption || hasNvidia;
|
||||
return hasContentAbove && hasWindow;
|
||||
}
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: !root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: nvidiaArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("Launch on dGPU")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nvidiaArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.desktopEntry) {
|
||||
SessionService.launchDesktopEntry(root.desktopEntry, true);
|
||||
}
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0))
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: closeArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
if (root.appData && root.appData.type === "grouped") {
|
||||
return I18n.tr("Close All Windows");
|
||||
}
|
||||
return I18n.tr("Close Window");
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.appData?.type === "window") {
|
||||
root.appData?.toplevel?.close();
|
||||
} else if (root.appData?.type === "grouped") {
|
||||
root.appData?.allWindows?.forEach(window => window.toplevel?.close());
|
||||
}
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: BatteryService.getBatteryIcon()
|
||||
size: Theme.barIconSize(battery.barThickness, undefined, battery.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(battery.barThickness)
|
||||
color: {
|
||||
if (!BatteryService.batteryAvailable) {
|
||||
return Theme.widgetIconColor;
|
||||
@@ -78,7 +78,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: BatteryService.getBatteryIcon()
|
||||
size: Theme.barIconSize(battery.barThickness, -4, battery.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(battery.barThickness, -4)
|
||||
color: {
|
||||
if (!BatteryService.batteryAvailable) {
|
||||
return Theme.widgetIconColor;
|
||||
|
||||
@@ -45,7 +45,7 @@ BasePill {
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "shift_lock"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ BasePill {
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "content_paste"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
color: Theme.widgetIconColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ BasePill {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const hours = systemClock?.date?.getHours();
|
||||
if (SettingsData.use24HourClock)
|
||||
return String(hours).padStart(2, '0').charAt(0);
|
||||
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
|
||||
if (SettingsData.padHours12Hour)
|
||||
if (SettingsData.use24HourClock) {
|
||||
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(0);
|
||||
} else {
|
||||
const hours = systemClock?.date?.getHours();
|
||||
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
|
||||
return String(display).padStart(2, '0').charAt(0);
|
||||
return display >= 10 ? String(display).charAt(0) : "";
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
|
||||
color: Theme.widgetTextColor
|
||||
@@ -47,13 +47,13 @@ BasePill {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const hours = systemClock?.date?.getHours();
|
||||
if (SettingsData.use24HourClock)
|
||||
return String(hours).padStart(2, '0').charAt(1);
|
||||
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
|
||||
if (SettingsData.padHours12Hour)
|
||||
if (SettingsData.use24HourClock) {
|
||||
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(1);
|
||||
} else {
|
||||
const hours = systemClock?.date?.getHours();
|
||||
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
|
||||
return String(display).padStart(2, '0').charAt(1);
|
||||
return display >= 10 ? String(display).charAt(1) : String(display);
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
|
||||
color: Theme.widgetTextColor
|
||||
|
||||
@@ -18,7 +18,7 @@ BasePill {
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "palette"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
color: root.isActive ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, root.barConfig?.noBackground)
|
||||
readonly property real vIconSize: Theme.barIconSize(root.barThickness, -4)
|
||||
|
||||
Loader {
|
||||
active: root.showPrinterIcon
|
||||
@@ -459,7 +459,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: "screen_record"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
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, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
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, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
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, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
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, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
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, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
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, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
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, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
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, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
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, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
color: root.isActive ? Theme.primary : Theme.widgetIconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.hasNoVisibleIcons()
|
||||
|
||||
@@ -36,7 +36,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: "memory"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: {
|
||||
if (DgopService.cpuUsage > 80) {
|
||||
return Theme.tempDanger;
|
||||
@@ -74,7 +74,7 @@ BasePill {
|
||||
DankIcon {
|
||||
id: cpuIcon
|
||||
name: "memory"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: {
|
||||
if (DgopService.cpuUsage > 80) {
|
||||
return Theme.tempDanger;
|
||||
|
||||
@@ -36,7 +36,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: "device_thermostat"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: {
|
||||
if (DgopService.cpuTemperature > 85) {
|
||||
return Theme.tempDanger;
|
||||
@@ -74,7 +74,7 @@ BasePill {
|
||||
DankIcon {
|
||||
id: cpuTempIcon
|
||||
name: "device_thermostat"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: {
|
||||
if (DgopService.cpuTemperature > 85) {
|
||||
return Theme.tempDanger;
|
||||
|
||||
@@ -57,7 +57,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: layout.getLayoutIcon(layout.currentLayoutSymbol)
|
||||
size: Theme.barIconSize(layout.barThickness, undefined, layout.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(layout.barThickness)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
@@ -78,7 +78,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: layout.getLayoutIcon(layout.currentLayoutSymbol)
|
||||
size: Theme.barIconSize(layout.barThickness, -4, layout.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(layout.barThickness, -4)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: "storage"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: {
|
||||
if (root.diskUsagePercent > 90) {
|
||||
return Theme.tempDanger;
|
||||
@@ -146,7 +146,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: "storage"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: {
|
||||
if (root.diskUsagePercent > 90) {
|
||||
return Theme.tempDanger;
|
||||
|
||||
@@ -104,7 +104,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: "auto_awesome_mosaic"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
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, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: {
|
||||
if (root.displayTemp > 80) {
|
||||
return Theme.tempDanger;
|
||||
|
||||
@@ -17,7 +17,7 @@ BasePill {
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: "keyboard"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
@@ -21,15 +21,15 @@ BasePill {
|
||||
visible: SettingsData.launcherLogoMode === "apps"
|
||||
anchors.centerIn: parent
|
||||
name: "apps"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
color: Theme.widgetIconColor
|
||||
}
|
||||
|
||||
SystemLogo {
|
||||
visible: SettingsData.launcherLogoMode === "os"
|
||||
anchors.centerIn: parent
|
||||
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
|
||||
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
|
||||
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
|
||||
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
|
||||
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, root.barConfig?.noBackground)
|
||||
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
|
||||
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
|
||||
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
|
||||
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, root.barConfig?.noBackground)
|
||||
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
|
||||
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
|
||||
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
|
||||
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, root.barConfig?.noBackground)
|
||||
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
|
||||
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
|
||||
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
source: SettingsData.launcherLogoCustomPath ? "file://" + SettingsData.launcherLogoCustomPath.replace("file://", "") : ""
|
||||
|
||||
@@ -41,7 +41,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: "network_check"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
@@ -79,7 +79,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: "network_check"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ BasePill {
|
||||
|
||||
anchors.centerIn: parent
|
||||
name: "assignment"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
color: root.isActive ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ BasePill {
|
||||
id: notifIcon
|
||||
anchors.centerIn: parent
|
||||
name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
color: SessionData.doNotDisturb ? Theme.primary : (root.isActive ? Theme.primary : Theme.widgetIconColor)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ BasePill {
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "power_settings_new"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.widgetIconColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: "developer_board"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: {
|
||||
if (DgopService.memoryUsage > 90) {
|
||||
return Theme.tempDanger;
|
||||
@@ -84,7 +84,7 @@ BasePill {
|
||||
DankIcon {
|
||||
id: ramIcon
|
||||
name: "developer_board"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: {
|
||||
if (DgopService.memoryUsage > 90) {
|
||||
return Theme.tempDanger;
|
||||
|
||||
@@ -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, undefined, root.barConfig?.noBackground)) / 2) : Theme.spacingXS
|
||||
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
width: Theme.barIconSize(root.barThickness)
|
||||
height: Theme.barIconSize(root.barThickness)
|
||||
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, undefined, root.barConfig?.noBackground)) / 2) : Theme.spacingXS
|
||||
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
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, undefined, root.barConfig?.noBackground)) / 2) : Theme.spacingXS
|
||||
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
width: Theme.barIconSize(root.barThickness)
|
||||
height: Theme.barIconSize(root.barThickness)
|
||||
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, undefined, root.barConfig?.noBackground)) / 2) : Theme.spacingXS
|
||||
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
name: "sports_esports"
|
||||
color: Theme.widgetTextColor
|
||||
visible: !iconImg.visible && Paths.isSteamApp(appId)
|
||||
|
||||
@@ -198,8 +198,8 @@ Item {
|
||||
IconImage {
|
||||
id: iconImg
|
||||
anchors.centerIn: parent
|
||||
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
width: Theme.barIconSize(root.barThickness)
|
||||
height: Theme.barIconSize(root.barThickness)
|
||||
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, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
|
||||
@@ -331,8 +331,8 @@ Item {
|
||||
IconImage {
|
||||
id: iconImg
|
||||
anchors.centerIn: parent
|
||||
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
width: Theme.barIconSize(root.barThickness)
|
||||
height: Theme.barIconSize(root.barThickness)
|
||||
source: delegateRoot.iconSource
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
@@ -402,7 +402,7 @@ Item {
|
||||
return root.menuOpen ? "chevron_right" : "chevron_left";
|
||||
}
|
||||
}
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
|
||||
@@ -754,8 +754,8 @@ Item {
|
||||
IconImage {
|
||||
id: menuIconImg
|
||||
anchors.centerIn: parent
|
||||
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
|
||||
width: Theme.barIconSize(root.barThickness)
|
||||
height: Theme.barIconSize(root.barThickness)
|
||||
source: parent.iconSource
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
|
||||
@@ -11,14 +11,17 @@ 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 : updaterIcon.implicitWidth
|
||||
implicitHeight: root.widgetThickness
|
||||
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : updaterIcon.implicitWidth
|
||||
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
|
||||
|
||||
DankIcon {
|
||||
id: statusIcon
|
||||
@@ -33,7 +36,7 @@ BasePill {
|
||||
return "system_update_alt";
|
||||
return "check_circle";
|
||||
}
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
color: {
|
||||
if (SystemUpdateService.hasError)
|
||||
return Theme.error;
|
||||
@@ -90,7 +93,7 @@ BasePill {
|
||||
return "system_update_alt";
|
||||
return "check_circle";
|
||||
}
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
color: {
|
||||
if (SystemUpdateService.hasError)
|
||||
return Theme.error;
|
||||
|
||||
@@ -41,7 +41,7 @@ BasePill {
|
||||
id: icon
|
||||
|
||||
name: DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
color: DMSNetworkService.connected ? Theme.primary : Theme.widgetIconColor
|
||||
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -30,7 +30,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
|
||||
size: Theme.barIconSize(root.barThickness, -6, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -6)
|
||||
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, root.barConfig?.noBackground)
|
||||
size: Theme.barIconSize(root.barThickness, -6)
|
||||
color: Theme.widgetIconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
@@ -199,22 +199,15 @@ Item {
|
||||
|
||||
let targetWorkspaceId;
|
||||
if (CompositorService.isNiri) {
|
||||
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 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 (CompositorService.isHyprland) {
|
||||
targetWorkspaceId = ws.id !== undefined ? ws.id : ws;
|
||||
} else if (CompositorService.isDwl) {
|
||||
@@ -307,12 +300,6 @@ Item {
|
||||
"active": false,
|
||||
"hidden": true
|
||||
};
|
||||
} else if (CompositorService.isNiri) {
|
||||
placeholder = {
|
||||
"id": -1,
|
||||
"idx": -1,
|
||||
"name": ""
|
||||
};
|
||||
} else if (CompositorService.isHyprland) {
|
||||
placeholder = {
|
||||
"id": -1,
|
||||
@@ -337,52 +324,28 @@ Item {
|
||||
|
||||
function getNiriWorkspaces() {
|
||||
if (NiriService.allWorkspaces.length === 0) {
|
||||
return [
|
||||
{
|
||||
"id": 1,
|
||||
"idx": 0,
|
||||
"name": ""
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"idx": 1,
|
||||
"name": ""
|
||||
}
|
||||
];
|
||||
return [1, 2];
|
||||
}
|
||||
|
||||
const fallbackWorkspaces = [
|
||||
{
|
||||
"id": 1,
|
||||
"idx": 0,
|
||||
"name": ""
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"idx": 1,
|
||||
"name": ""
|
||||
}
|
||||
];
|
||||
|
||||
let workspaces;
|
||||
if (!root.screenName || SettingsData.workspaceFollowFocus) {
|
||||
const currentWorkspaces = NiriService.getCurrentOutputWorkspaces();
|
||||
workspaces = currentWorkspaces.length > 0 ? currentWorkspaces : fallbackWorkspaces;
|
||||
workspaces = NiriService.getCurrentOutputWorkspaceNumbers();
|
||||
} else {
|
||||
const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName);
|
||||
workspaces = displayWorkspaces.length > 0 ? displayWorkspaces : fallbackWorkspaces;
|
||||
const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName).map(ws => ws.idx + 1);
|
||||
workspaces = displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2];
|
||||
}
|
||||
|
||||
workspaces = workspaces.slice().sort((a, b) => a.idx - b.idx);
|
||||
|
||||
if (!SettingsData.showOccupiedWorkspacesOnly) {
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
return workspaces.filter(ws => {
|
||||
if (ws.is_active)
|
||||
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 true;
|
||||
return NiriService.windows?.some(win => win.workspace_id === ws.id) ?? false;
|
||||
return NiriService.windows?.some(win => win.workspace_id === workspace.id) ?? false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -396,7 +359,7 @@ Item {
|
||||
}
|
||||
|
||||
const activeWs = NiriService.allWorkspaces.find(ws => ws.output === root.screenName && ws.is_active);
|
||||
return activeWs ? activeWs.idx : 1;
|
||||
return activeWs ? activeWs.idx + 1 : 1;
|
||||
}
|
||||
|
||||
function getDwlTags() {
|
||||
@@ -504,22 +467,19 @@ 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, root.barConfig?.noBackground)
|
||||
readonly property real appIconSize: Theme.barIconSize(barThickness, -6)
|
||||
|
||||
function getRealWorkspaces() {
|
||||
return root.workspaceList.filter(ws => {
|
||||
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;
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -546,7 +506,7 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === root.currentWorkspace);
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws === root.currentWorkspace);
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex;
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
|
||||
|
||||
@@ -554,11 +514,7 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextWorkspace = realWorkspaces[nextIndex];
|
||||
if (!nextWorkspace || nextWorkspace.idx === undefined) {
|
||||
return;
|
||||
}
|
||||
NiriService.switchToWorkspace(nextWorkspace.idx);
|
||||
NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1);
|
||||
} else if (CompositorService.isHyprland) {
|
||||
const realWorkspaces = getRealWorkspaces();
|
||||
if (realWorkspaces.length < 2) {
|
||||
@@ -609,26 +565,10 @@ 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) {
|
||||
@@ -642,28 +582,26 @@ Item {
|
||||
if (isPlaceholder)
|
||||
return index + 1;
|
||||
|
||||
let workspaceName = "";
|
||||
if (SettingsData.showWorkspaceName) {
|
||||
workspaceName = modelData?.name ?? "";
|
||||
let workspaceName = modelData?.name;
|
||||
|
||||
if (workspaceName && workspaceName !== "") {
|
||||
if (root.isVertical) {
|
||||
workspaceName = workspaceName.charAt(0);
|
||||
return workspaceName.charAt(0);
|
||||
}
|
||||
} else {
|
||||
workspaceName = "";
|
||||
return workspaceName;
|
||||
}
|
||||
}
|
||||
|
||||
if (workspaceName) {
|
||||
if (SettingsData.showWorkspaceIndex) {
|
||||
const indexLabel = getWorkspaceIndexFallback(modelData, index);
|
||||
return indexLabel ? `${indexLabel}: ${workspaceName}` : workspaceName;
|
||||
}
|
||||
return workspaceName;
|
||||
}
|
||||
|
||||
return getWorkspaceIndexFallback(modelData, index);
|
||||
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;
|
||||
}
|
||||
|
||||
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll
|
||||
@@ -809,8 +747,6 @@ 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)
|
||||
@@ -833,8 +769,6 @@ 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)
|
||||
@@ -866,10 +800,6 @@ 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) {
|
||||
@@ -886,16 +816,8 @@ Item {
|
||||
return 0;
|
||||
}
|
||||
|
||||
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 real visualWidth: baseWidth + iconsExtraWidth
|
||||
readonly property real visualHeight: baseHeight + iconsExtraHeight
|
||||
|
||||
readonly property color unfocusedColor: {
|
||||
switch (SettingsData.workspaceUnfocusedColorMode) {
|
||||
@@ -993,8 +915,8 @@ Item {
|
||||
} else if (CompositorService.isNiri) {
|
||||
if (isRightClick) {
|
||||
NiriService.toggleOverview();
|
||||
} else if (modelData && modelData.idx !== undefined) {
|
||||
NiriService.switchToWorkspace(modelData.idx);
|
||||
} else {
|
||||
NiriService.switchToWorkspace(modelData - 1);
|
||||
}
|
||||
} else if (CompositorService.isHyprland && modelData?.id) {
|
||||
if (isRightClick && root.hyprlandOverviewLoader?.item) {
|
||||
@@ -1036,7 +958,7 @@ Item {
|
||||
if (root.useExtWorkspace) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isNiri) {
|
||||
wsData = modelData || null;
|
||||
wsData = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.effectiveScreenName) || null;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isDwl) {
|
||||
@@ -1062,8 +984,6 @@ 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));
|
||||
}
|
||||
@@ -1176,12 +1096,8 @@ Item {
|
||||
Loader {
|
||||
id: appIconsLoader
|
||||
anchors.fill: parent
|
||||
active: SettingsData.showWorkspaceApps || SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName || loadedHasIcon
|
||||
active: SettingsData.showWorkspaceApps
|
||||
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
|
||||
|
||||
@@ -47,17 +47,10 @@ Rectangle {
|
||||
readonly property var humidity: WeatherService.formatPercent(root.forecastData?.humidity)
|
||||
readonly property string humidityText: humidity ?? "--"
|
||||
|
||||
readonly property var wind: {
|
||||
SettingsData.windSpeedUnit;
|
||||
SettingsData.useFahrenheit;
|
||||
return WeatherService.formatSpeed(root.forecastData?.wind);
|
||||
}
|
||||
readonly property var wind: WeatherService.formatSpeed(root.forecastData?.wind)
|
||||
readonly property string windText: wind ?? "--"
|
||||
|
||||
readonly property var pressure: {
|
||||
SettingsData.useFahrenheit;
|
||||
return WeatherService.formatPressure(root.forecastData?.pressure);
|
||||
}
|
||||
readonly property var pressure: WeatherService.formatPressure(root.forecastData?.pressure)
|
||||
readonly property string pressureText: pressure ?? "--"
|
||||
|
||||
readonly property var precipitation: root.forecastData?.precipitationProbability
|
||||
|
||||
@@ -229,17 +229,10 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
property var feelsLike: SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)
|
||||
text: I18n.tr("Feels Like %1°", "weather feels like temperature").arg(feelsLike)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.withAlpha(Theme.surfaceText, 0.7)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.city || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.withAlpha(Theme.surfaceText, 0.7)
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
@@ -260,7 +253,7 @@ Item {
|
||||
id: sunriseIcon
|
||||
name: "wb_twilight"
|
||||
size: Theme.iconSize - 6
|
||||
color: Theme.withAlpha(Theme.surfaceText, 0.6)
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
@@ -279,7 +272,7 @@ Item {
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.withAlpha(Theme.surfaceText, 0.6)
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.left: sunriseIcon.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user