1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-31 08:52:49 -05:00

Compare commits

..

11 Commits

Author SHA1 Message Date
bbedward
f236706d6a hyprland: fix workspace overview truncation, update scaling
fixes #871
2025-12-03 12:02:41 -05:00
purian23
b097700591 Add dbus notifications inline to systemd 2025-12-03 11:53:31 -05:00
purian23
50b112c9d6 Revert "Add DMS dbus notification service file"
This reverts commit 33e655becd.
2025-12-03 11:48:56 -05:00
purian23
c2f478b088 Remove notification conflict 2025-12-03 11:16:04 -05:00
bbedward
dccbb137d7 launcher: integrate dsearch into drawer 2025-12-03 10:49:08 -05:00
bbedward
90f9940dbd gamma: fix night mode on startup 2025-12-03 10:37:28 -05:00
bbedward
f3f7cc9077 Revert "modals: single window optimization"
This reverts commit 468e569bc7.
2025-12-03 10:34:40 -05:00
bbedward
c331e2f39e Revert "spotlight: optimize to keep loaded"
This reverts commit 01b28e3ee8.
2025-12-03 10:34:19 -05:00
bbedward
1c7ebc4323 Revert "dankmodal: fix persistent modal handling"
This reverts commit e7cb0d397e.
2025-12-03 10:34:15 -05:00
bbedward
5f5427266f keybinds: always parse binds.kdl, show warning on position-conflicts 2025-12-03 10:32:16 -05:00
purian23
33e655becd Add DMS dbus notification service file 2025-12-03 09:49:34 -05:00
31 changed files with 1248 additions and 878 deletions

View File

@@ -81,7 +81,7 @@ install: build install-bin install-shell install-completions install-systemd ins
@echo "" @echo ""
@echo "Installation complete!" @echo "Installation complete!"
@echo "" @echo ""
@echo "=== The DMS Team! ===" @echo "=== Cheers, the DMS Team! ==="
# Uninstallation targets # Uninstallation targets
uninstall-bin: uninstall-bin:

View File

@@ -5,7 +5,8 @@ After=graphical-session.target
Requisite=graphical-session.target Requisite=graphical-session.target
[Service] [Service]
Type=simple Type=dbus
BusName=org.freedesktop.Notifications
ExecStart=/usr/bin/dms run --session ExecStart=/usr/bin/dms run --session
ExecReload=/usr/bin/pkill -USR1 -x dms ExecReload=/usr/bin/pkill -USR1 -x dms
Restart=always Restart=always

View File

@@ -5,6 +5,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
@@ -53,14 +54,29 @@ func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
n.parsed = true n.parsed = true
categorizedBinds := make(map[string][]keybinds.Keybind) categorizedBinds := make(map[string][]keybinds.Keybind)
n.convertSection(result.Section, "", categorizedBinds) n.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs)
return &keybinds.CheatSheet{ sheet := &keybinds.CheatSheet{
Title: "Niri Keybinds", Title: "Niri Keybinds",
Provider: n.Name(), Provider: n.Name(),
Binds: categorizedBinds, Binds: categorizedBinds,
DMSBindsIncluded: result.DMSBindsIncluded, DMSBindsIncluded: result.DMSBindsIncluded,
}, nil }
if result.DMSStatus != nil {
sheet.DMSStatus = &keybinds.DMSBindsStatus{
Exists: result.DMSStatus.Exists,
Included: result.DMSStatus.Included,
IncludePosition: result.DMSStatus.IncludePosition,
TotalIncludes: result.DMSStatus.TotalIncludes,
BindsAfterDMS: result.DMSStatus.BindsAfterDMS,
Effective: result.DMSStatus.Effective,
OverriddenBy: result.DMSStatus.OverriddenBy,
StatusMessage: result.DMSStatus.StatusMessage,
}
}
return sheet, nil
} }
func (n *NiriProvider) HasDMSBindsIncluded() bool { func (n *NiriProvider) HasDMSBindsIncluded() bool {
@@ -78,7 +94,7 @@ func (n *NiriProvider) HasDMSBindsIncluded() bool {
return n.dmsBindsIncluded return n.dmsBindsIncluded
} }
func (n *NiriProvider) convertSection(section *NiriSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) { func (n *NiriProvider) convertSection(section *NiriSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*NiriKeyBinding) {
currentSubcat := subcategory currentSubcat := subcategory
if section.Name != "" { if section.Name != "" {
currentSubcat = section.Name currentSubcat = section.Name
@@ -86,12 +102,12 @@ func (n *NiriProvider) convertSection(section *NiriSection, subcategory string,
for _, kb := range section.Keybinds { for _, kb := range section.Keybinds {
category := n.categorizeByAction(kb.Action) category := n.categorizeByAction(kb.Action)
bind := n.convertKeybind(&kb, currentSubcat) bind := n.convertKeybind(&kb, currentSubcat, conflicts)
categorizedBinds[category] = append(categorizedBinds[category], bind) categorizedBinds[category] = append(categorizedBinds[category], bind)
} }
for _, child := range section.Children { for _, child := range section.Children {
n.convertSection(&child, currentSubcat, categorizedBinds) n.convertSection(&child, currentSubcat, categorizedBinds, conflicts)
} }
} }
@@ -128,21 +144,35 @@ func (n *NiriProvider) categorizeByAction(action string) string {
} }
} }
func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string) keybinds.Keybind { func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, conflicts map[string]*NiriKeyBinding) keybinds.Keybind {
rawAction := n.formatRawAction(kb.Action, kb.Args) rawAction := n.formatRawAction(kb.Action, kb.Args)
keyStr := n.formatKey(kb)
source := "config" source := "config"
if strings.Contains(kb.Source, "dms/binds.kdl") { if strings.Contains(kb.Source, "dms/binds.kdl") {
source = "dms" source = "dms"
} }
return keybinds.Keybind{ bind := keybinds.Keybind{
Key: n.formatKey(kb), Key: keyStr,
Description: kb.Description, Description: kb.Description,
Action: rawAction, Action: rawAction,
Subcategory: subcategory, Subcategory: subcategory,
Source: source, Source: source,
} }
if source == "dms" && conflicts != nil {
if conflictKb, ok := conflicts[keyStr]; ok {
bind.Conflict = &keybinds.Keybind{
Key: keyStr,
Description: conflictKb.Description,
Action: n.formatRawAction(conflictKb.Action, conflictKb.Args),
Source: "config",
}
}
}
return bind
} }
func (n *NiriProvider) formatRawAction(action string, args []string) string { func (n *NiriProvider) formatRawAction(action string, args []string) string {
@@ -386,6 +416,29 @@ func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error
return os.WriteFile(overridePath, []byte(content), 0644) return os.WriteFile(overridePath, []byte(content), 0644)
} }
func (n *NiriProvider) getBindSortPriority(action string) int {
switch {
case strings.HasPrefix(action, "spawn") && strings.Contains(action, "dms"):
return 0
case strings.Contains(action, "workspace"):
return 1
case strings.Contains(action, "window") || strings.Contains(action, "column") ||
strings.Contains(action, "focus") || strings.Contains(action, "move") ||
strings.Contains(action, "swap") || strings.Contains(action, "resize"):
return 2
case strings.HasPrefix(action, "focus-monitor") || strings.Contains(action, "monitor"):
return 3
case strings.Contains(action, "screenshot"):
return 4
case action == "quit" || action == "power-off-monitors" || strings.Contains(action, "dpms"):
return 5
case strings.HasPrefix(action, "spawn"):
return 6
default:
return 7
}
}
func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string { func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string {
if len(binds) == 0 { if len(binds) == 0 {
return "binds {}\n" return "binds {}\n"
@@ -401,6 +454,18 @@ func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) stri
} }
} }
sort.Slice(regularBinds, func(i, j int) bool {
pi, pj := n.getBindSortPriority(regularBinds[i].Action), n.getBindSortPriority(regularBinds[j].Action)
if pi != pj {
return pi < pj
}
return regularBinds[i].Key < regularBinds[j].Key
})
sort.Slice(recentWindowsBinds, func(i, j int) bool {
return recentWindowsBinds[i].Key < recentWindowsBinds[j].Key
})
var sb strings.Builder var sb strings.Builder
sb.WriteString("binds {\n") sb.WriteString("binds {\n")

View File

@@ -32,6 +32,16 @@ type NiriParser struct {
bindOrder []string bindOrder []string
currentSource string currentSource string
dmsBindsIncluded bool dmsBindsIncluded bool
dmsBindsExists bool
includeCount int
dmsIncludePos int
bindsBeforeDMS int
bindsAfterDMS int
dmsBindKeys map[string]bool
configBindKeys map[string]bool
dmsProcessed bool
dmsBindMap map[string]*NiriKeyBinding
conflictingConfigs map[string]*NiriKeyBinding
} }
func NewNiriParser(configDir string) *NiriParser { func NewNiriParser(configDir string) *NiriParser {
@@ -41,20 +51,53 @@ func NewNiriParser(configDir string) *NiriParser {
bindMap: make(map[string]*NiriKeyBinding), bindMap: make(map[string]*NiriKeyBinding),
bindOrder: []string{}, bindOrder: []string{},
currentSource: "", currentSource: "",
dmsIncludePos: -1,
dmsBindKeys: make(map[string]bool),
configBindKeys: make(map[string]bool),
dmsBindMap: make(map[string]*NiriKeyBinding),
conflictingConfigs: make(map[string]*NiriKeyBinding),
} }
} }
func (p *NiriParser) Parse() (*NiriSection, error) { func (p *NiriParser) Parse() (*NiriSection, error) {
dmsBindsPath := filepath.Join(p.configDir, "dms", "binds.kdl")
if _, err := os.Stat(dmsBindsPath); err == nil {
p.dmsBindsExists = true
}
configPath := filepath.Join(p.configDir, "config.kdl") configPath := filepath.Join(p.configDir, "config.kdl")
section, err := p.parseFile(configPath, "") section, err := p.parseFile(configPath, "")
if err != nil { if err != nil {
return nil, err return nil, err
} }
if p.dmsBindsExists && !p.dmsProcessed {
p.parseDMSBindsDirectly(dmsBindsPath, section)
}
section.Keybinds = p.finalizeBinds() section.Keybinds = p.finalizeBinds()
return section, nil return section, nil
} }
func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSection) {
data, err := os.ReadFile(dmsBindsPath)
if err != nil {
return
}
doc, err := kdl.Parse(strings.NewReader(string(data)))
if err != nil {
return
}
prevSource := p.currentSource
p.currentSource = dmsBindsPath
baseDir := filepath.Dir(dmsBindsPath)
p.processNodes(doc.Nodes, section, baseDir)
p.currentSource = prevSource
p.dmsProcessed = true
}
func (p *NiriParser) finalizeBinds() []NiriKeyBinding { func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
binds := make([]NiriKeyBinding, 0, len(p.bindOrder)) binds := make([]NiriKeyBinding, 0, len(p.bindOrder))
for _, key := range p.bindOrder { for _, key := range p.bindOrder {
@@ -67,6 +110,20 @@ func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
func (p *NiriParser) addBind(kb *NiriKeyBinding) { func (p *NiriParser) addBind(kb *NiriKeyBinding) {
key := p.formatBindKey(kb) key := p.formatBindKey(kb)
isDMSBind := strings.Contains(kb.Source, "dms/binds.kdl")
if isDMSBind {
p.dmsBindKeys[key] = true
p.dmsBindMap[key] = kb
} else if p.dmsBindKeys[key] {
p.bindsAfterDMS++
p.conflictingConfigs[key] = kb
p.configBindKeys[key] = true
return
} else {
p.configBindKeys[key] = true
}
if _, exists := p.bindMap[key]; !exists { if _, exists := p.bindMap[key]; !exists {
p.bindOrder = append(p.bindOrder, key) p.bindOrder = append(p.bindOrder, key)
} }
@@ -105,9 +162,11 @@ func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, erro
Name: sectionName, Name: sectionName,
} }
prevSource := p.currentSource
p.currentSource = absPath p.currentSource = absPath
baseDir := filepath.Dir(absPath) baseDir := filepath.Dir(absPath)
p.processNodes(doc.Nodes, section, baseDir) p.processNodes(doc.Nodes, section, baseDir)
p.currentSource = prevSource
return section, nil return section, nil
} }
@@ -133,8 +192,13 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba
} }
includePath := strings.Trim(node.Arguments[0].String(), "\"") includePath := strings.Trim(node.Arguments[0].String(), "\"")
if includePath == "dms/binds.kdl" || strings.HasSuffix(includePath, "/dms/binds.kdl") { isDMSInclude := includePath == "dms/binds.kdl" || strings.HasSuffix(includePath, "/dms/binds.kdl")
p.includeCount++
if isDMSInclude {
p.dmsBindsIncluded = true p.dmsBindsIncluded = true
p.dmsIncludePos = p.includeCount
p.bindsBeforeDMS = len(p.bindMap)
} }
fullPath := filepath.Join(baseDir, includePath) fullPath := filepath.Join(baseDir, includePath)
@@ -142,6 +206,10 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba
fullPath = includePath fullPath = includePath
} }
if isDMSInclude {
p.dmsProcessed = true
}
includedSection, err := p.parseFile(fullPath, "") includedSection, err := p.parseFile(fullPath, "")
if err != nil { if err != nil {
return return
@@ -232,6 +300,47 @@ func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
type NiriParseResult struct { type NiriParseResult struct {
Section *NiriSection Section *NiriSection
DMSBindsIncluded bool DMSBindsIncluded bool
DMSStatus *DMSBindsStatusInfo
ConflictingConfigs map[string]*NiriKeyBinding
}
type DMSBindsStatusInfo struct {
Exists bool
Included bool
IncludePosition int
TotalIncludes int
BindsAfterDMS int
Effective bool
OverriddenBy int
StatusMessage string
}
func (p *NiriParser) buildDMSStatus() *DMSBindsStatusInfo {
status := &DMSBindsStatusInfo{
Exists: p.dmsBindsExists,
Included: p.dmsBindsIncluded,
IncludePosition: p.dmsIncludePos,
TotalIncludes: p.includeCount,
BindsAfterDMS: p.bindsAfterDMS,
}
switch {
case !p.dmsBindsExists:
status.Effective = false
status.StatusMessage = "dms/binds.kdl does not exist"
case !p.dmsBindsIncluded:
status.Effective = false
status.StatusMessage = "dms/binds.kdl is not included in config.kdl"
case p.bindsAfterDMS > 0:
status.Effective = true
status.OverriddenBy = p.bindsAfterDMS
status.StatusMessage = "Some DMS binds may be overridden by config binds"
default:
status.Effective = true
status.StatusMessage = "DMS binds are active"
}
return status
} }
func ParseNiriKeys(configDir string) (*NiriParseResult, error) { func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
@@ -243,5 +352,7 @@ func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
return &NiriParseResult{ return &NiriParseResult{
Section: section, Section: section,
DMSBindsIncluded: parser.HasDMSBindsIncluded(), DMSBindsIncluded: parser.HasDMSBindsIncluded(),
DMSStatus: parser.buildDMSStatus(),
ConflictingConfigs: parser.conflictingConfigs,
}, nil }, nil
} }

View File

@@ -6,6 +6,18 @@ type Keybind struct {
Action string `json:"action,omitempty"` Action string `json:"action,omitempty"`
Subcategory string `json:"subcat,omitempty"` Subcategory string `json:"subcat,omitempty"`
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
Conflict *Keybind `json:"conflict,omitempty"`
}
type DMSBindsStatus struct {
Exists bool `json:"exists"`
Included bool `json:"included"`
IncludePosition int `json:"includePosition"`
TotalIncludes int `json:"totalIncludes"`
BindsAfterDMS int `json:"bindsAfterDms"`
Effective bool `json:"effective"`
OverriddenBy int `json:"overriddenBy"`
StatusMessage string `json:"statusMessage"`
} }
type CheatSheet struct { type CheatSheet struct {
@@ -13,6 +25,7 @@ type CheatSheet struct {
Provider string `json:"provider"` Provider string `json:"provider"`
Binds map[string][]Keybind `json:"binds"` Binds map[string][]Keybind `json:"binds"`
DMSBindsIncluded bool `json:"dmsBindsIncluded"` DMSBindsIncluded bool `json:"dmsBindsIncluded"`
DMSStatus *DMSBindsStatus `json:"dmsStatus,omitempty"`
} }
type Provider interface { type Provider interface {

View File

@@ -1,466 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Services
import qs.Widgets
Singleton {
id: root
property var activeModal: null
property bool windowsVisible: false
property var targetScreen: null
property var persistentModal: null
property Item currentDirectContent: null
readonly property bool hasActiveModal: activeModal !== null
readonly property bool hasPersistentModal: persistentModal !== null
readonly property bool isPersistentModalActive: hasActiveModal && activeModal === persistentModal
readonly property bool shouldShowModal: hasActiveModal
readonly property bool shouldKeepWindowsAlive: hasPersistentModal && targetScreen !== null
onPersistentModalChanged: {
if (!persistentModal) {
if (!hasActiveModal)
targetScreen = null;
return;
}
if (!targetScreen)
targetScreen = CompositorService.focusedScreen;
cachedModal = persistentModal;
updateCachedModalProperties(persistentModal);
}
onActiveModalChanged: updateDirectContent()
function updateCachedModalProperties(modal) {
if (!modal)
return;
cachedModalWidth = Theme.px(modal.modalWidth, dpr);
cachedModalHeight = Theme.px(modal.modalHeight, dpr);
cachedModalX = calculateX(modal);
cachedModalY = calculateY(modal);
cachedAnimationDuration = modal.animationDuration ?? Theme.shortDuration;
cachedEnterCurve = modal.animationEnterCurve ?? Theme.expressiveCurves.expressiveFastSpatial;
cachedExitCurve = modal.animationExitCurve ?? Theme.expressiveCurves.expressiveFastSpatial;
cachedScaleCollapsed = modal.animationScaleCollapsed ?? 0.96;
}
function updateDirectContent() {
if (currentDirectContent) {
currentDirectContent.visible = false;
currentDirectContent.parent = null;
currentDirectContent = null;
}
if (!activeModal?.directContent)
return;
currentDirectContent = activeModal.directContent;
currentDirectContent.parent = directContentWrapper;
currentDirectContent.anchors.fill = directContentWrapper;
currentDirectContent.visible = true;
}
function isScreenValid(screen) {
if (!screen)
return false;
for (const s of Quickshell.screens) {
if (s === screen || s.name === screen.name)
return true;
}
return false;
}
function handleScreensChanged() {
if (!targetScreen)
return;
if (isScreenValid(targetScreen))
return;
const newScreen = CompositorService.focusedScreen;
if (hasActiveModal) {
targetScreen = newScreen;
if (cachedModal)
updateCachedModalProperties(cachedModal);
return;
}
if (hasPersistentModal) {
targetScreen = newScreen;
updateCachedModalProperties(persistentModal);
return;
}
targetScreen = null;
}
Connections {
target: Quickshell
function onScreensChanged() {
root.handleScreensChanged();
}
}
readonly property var screen: backgroundWindow.screen
readonly property real dpr: screen ? CompositorService.getScreenScale(screen) : 1
readonly property real shadowBuffer: 5
property bool wantsToHide: false
property real cachedModalWidth: 400
property real cachedModalHeight: 300
property real cachedModalX: 0
property real cachedModalY: 0
property var cachedModal: null
property int cachedAnimationDuration: Theme.shortDuration
property var cachedEnterCurve: Theme.expressiveCurves.expressiveFastSpatial
property var cachedExitCurve: Theme.expressiveCurves.expressiveFastSpatial
property real cachedScaleCollapsed: 0.96
readonly property real modalWidth: cachedModalWidth
readonly property real modalHeight: cachedModalHeight
readonly property real modalX: cachedModalX
readonly property real modalY: cachedModalY
Connections {
target: root.cachedModal
function onModalWidthChanged() {
if (!root.hasActiveModal)
return;
root.cachedModalWidth = Theme.px(root.cachedModal.modalWidth, root.dpr);
root.cachedModalX = root.calculateX(root.cachedModal);
}
function onModalHeightChanged() {
if (!root.hasActiveModal)
return;
root.cachedModalHeight = Theme.px(root.cachedModal.modalHeight, root.dpr);
root.cachedModalY = root.calculateY(root.cachedModal);
}
}
onScreenChanged: {
if (!cachedModal || !screen)
return;
cachedModalWidth = Theme.px(cachedModal.modalWidth, dpr);
cachedModalHeight = Theme.px(cachedModal.modalHeight, dpr);
cachedModalX = calculateX(cachedModal);
cachedModalY = calculateY(cachedModal);
}
function showModal(modal) {
wantsToHide = false;
targetScreen = CompositorService.focusedScreen;
activeModal = modal;
cachedModal = modal;
windowsVisible = true;
updateCachedModalProperties(modal);
if (modal.directContent)
Qt.callLater(focusDirectContent);
}
function focusDirectContent() {
if (!hasActiveModal)
return;
if (!cachedModal?.directContent)
return;
cachedModal.directContent.forceActiveFocus();
}
function hideModal() {
wantsToHide = true;
Qt.callLater(completeHide);
}
function completeHide() {
if (!wantsToHide)
return;
activeModal = null;
wantsToHide = false;
}
function hideModalInstant() {
wantsToHide = false;
const closingModal = activeModal;
activeModal = null;
if (shouldKeepWindowsAlive) {
cachedModal = persistentModal;
updateCachedModalProperties(persistentModal);
} else {
windowsVisible = false;
targetScreen = null;
}
cleanupInputMethod();
if (closingModal && typeof closingModal.onFullyClosed === "function")
closingModal.onFullyClosed();
}
function onCloseAnimationFinished() {
if (hasActiveModal)
return;
if (cachedModal && typeof cachedModal.onFullyClosed === "function")
cachedModal.onFullyClosed();
cleanupInputMethod();
if (shouldKeepWindowsAlive) {
cachedModal = persistentModal;
updateCachedModalProperties(persistentModal);
return;
}
windowsVisible = false;
targetScreen = null;
}
function cleanupInputMethod() {
if (!Qt.inputMethod)
return;
Qt.inputMethod.hide();
Qt.inputMethod.reset();
}
function calculateX(m) {
const screen = backgroundWindow.screen;
if (!screen)
return 0;
const w = Theme.px(m.modalWidth, dpr);
switch (m.positioning) {
case "center":
return Theme.snap((screen.width - w) / 2, dpr);
case "top-right":
return Theme.snap(Math.max(Theme.spacingL, screen.width - w - Theme.spacingL), dpr);
case "custom":
return Theme.snap(m.customPosition.x, dpr);
default:
return 0;
}
}
function calculateY(m) {
const screen = backgroundWindow.screen;
if (!screen)
return 0;
const h = Theme.px(m.modalHeight, dpr);
switch (m.positioning) {
case "center":
return Theme.snap((screen.height - h) / 2, dpr);
case "top-right":
return Theme.snap(Theme.barHeight + Theme.spacingXS, dpr);
case "custom":
return Theme.snap(m.customPosition.y, dpr);
default:
return 0;
}
}
PanelWindow {
id: backgroundWindow
visible: root.windowsVisible || root.shouldKeepWindowsAlive
screen: root.targetScreen
color: "transparent"
WlrLayershell.namespace: "dms:modal:background"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
mask: Region {
item: backgroundMaskRect
intersection: Intersection.Xor
}
Item {
id: backgroundMaskRect
x: root.shouldShowModal ? root.modalX : 0
y: root.shouldShowModal ? root.modalY : 0
width: root.shouldShowModal ? root.modalWidth : (backgroundWindow.screen?.width ?? 1920)
height: root.shouldShowModal ? root.modalHeight : (backgroundWindow.screen?.height ?? 1080)
}
MouseArea {
anchors.fill: parent
enabled: root.windowsVisible
onClicked: mouse => {
if (!root.cachedModal || !root.shouldShowModal)
return;
if (!(root.cachedModal.closeOnBackgroundClick ?? true))
return;
const outside = mouse.x < root.modalX || mouse.x > root.modalX + root.modalWidth || mouse.y < root.modalY || mouse.y > root.modalY + root.modalHeight;
if (!outside)
return;
root.cachedModal.backgroundClicked();
}
}
Rectangle {
anchors.fill: parent
color: "black"
opacity: root.shouldShowModal && SettingsData.modalDarkenBackground ? (root.cachedModal?.backgroundOpacity ?? 0.5) : 0
visible: SettingsData.modalDarkenBackground
Behavior on opacity {
NumberAnimation {
duration: root.cachedAnimationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldShowModal ? root.cachedEnterCurve : root.cachedExitCurve
}
}
}
}
PanelWindow {
id: contentWindow
visible: root.windowsVisible || root.shouldKeepWindowsAlive
screen: root.targetScreen
color: "transparent"
WlrLayershell.namespace: root.cachedModal?.layerNamespace ?? "dms:modal"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (!root.hasActiveModal)
return WlrKeyboardFocus.None;
if (root.cachedModal?.customKeyboardFocus !== null && root.cachedModal?.customKeyboardFocus !== undefined)
return root.cachedModal.customKeyboardFocus;
if (CompositorService.isHyprland)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
anchors {
left: true
top: true
}
WlrLayershell.margins {
left: Math.max(0, Theme.snap(root.modalX - root.shadowBuffer, root.dpr))
top: Math.max(0, Theme.snap(root.modalY - root.shadowBuffer, root.dpr))
}
implicitWidth: root.modalWidth + (root.shadowBuffer * 2)
implicitHeight: root.modalHeight + (root.shadowBuffer * 2)
mask: Region {
item: contentMaskRect
}
Item {
id: contentMaskRect
x: root.shadowBuffer
y: root.shadowBuffer
width: root.shouldShowModal ? root.modalWidth : 0
height: root.shouldShowModal ? root.modalHeight : 0
}
HyprlandFocusGrab {
windows: [contentWindow]
active: CompositorService.isHyprland && root.hasActiveModal && (root.cachedModal?.shouldHaveFocus ?? false)
}
Item {
id: contentContainer
x: root.shadowBuffer
y: root.shadowBuffer
width: root.modalWidth
height: root.modalHeight
readonly property bool hasDirectContent: root.currentDirectContent !== null
opacity: root.shouldShowModal ? 1 : 0
scale: root.shouldShowModal ? 1 : root.cachedScaleCollapsed
Behavior on opacity {
NumberAnimation {
id: opacityAnimation
duration: root.cachedAnimationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldShowModal ? root.cachedEnterCurve : root.cachedExitCurve
onRunningChanged: {
if (running || root.shouldShowModal)
return;
root.onCloseAnimationFinished();
}
}
}
Behavior on scale {
NumberAnimation {
id: scaleAnimation
duration: root.cachedAnimationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldShowModal ? root.cachedEnterCurve : root.cachedExitCurve
}
}
DankRectangle {
anchors.fill: parent
color: root.cachedModal?.backgroundColor ?? Theme.surfaceContainer
borderColor: root.cachedModal?.borderColor ?? Theme.outlineMedium
borderWidth: root.cachedModal?.borderWidth ?? 1
radius: root.cachedModal?.cornerRadius ?? Theme.cornerRadius
z: -1
}
FocusScope {
id: modalFocusScope
anchors.fill: parent
focus: root.hasActiveModal
Keys.onEscapePressed: event => {
if (!root.cachedModal?.closeOnEscapeKey)
return;
root.cachedModal.close();
event.accepted = true;
}
Keys.forwardTo: contentContainer.hasDirectContent ? [directContentWrapper] : (contentLoader.item ? [contentLoader.item] : [])
Item {
id: directContentWrapper
anchors.fill: parent
visible: contentContainer.hasDirectContent
focus: contentContainer.hasDirectContent && root.hasActiveModal
}
Loader {
id: contentLoader
anchors.fill: parent
active: !contentContainer.hasDirectContent && root.windowsVisible
asynchronous: false
sourceComponent: root.cachedModal?.content ?? null
visible: !contentContainer.hasDirectContent
focus: !contentContainer.hasDirectContent && root.hasActiveModal
onLoaded: {
if (!item)
return;
if (root.cachedModal)
root.cachedModal.loadedContent = item;
if (root.hasActiveModal)
item.forceActiveFocus();
}
onActiveChanged: {
if (active || !root.cachedModal)
return;
root.cachedModal.loadedContent = null;
}
}
}
}
}
}

View File

@@ -25,8 +25,6 @@ import qs.Services
Item { Item {
id: root id: root
readonly property bool _forceDisplayService: DisplayService.brightnessAvailable !== undefined
Instantiator { Instantiator {
id: daemonPluginInstantiator id: daemonPluginInstantiator
asynchronous: true asynchronous: true

View File

@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -9,6 +10,11 @@ DankModal {
layerNamespace: "dms:bluetooth-pairing" layerNamespace: "dms:bluetooth-pairing"
HyprlandFocusGrab {
windows: [root.contentWindow]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property string deviceName: "" property string deviceName: ""
property string deviceAddress: "" property string deviceAddress: ""
property string requestType: "" property string requestType: ""

View File

@@ -1,9 +1,10 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
import qs.Modals.Clipboard import qs.Modals.Clipboard
FocusScope { Item {
id: clipboardContent id: clipboardContent
required property var modal required property var modal
@@ -14,7 +15,6 @@ FocusScope {
property alias clipboardListView: clipboardListView property alias clipboardListView: clipboardListView
anchors.fill: parent anchors.fill: parent
focus: true
Column { Column {
anchors.fill: parent anchors.fill: parent
@@ -31,13 +31,14 @@ FocusScope {
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
onClearAllClicked: { onClearAllClicked: {
clearConfirmDialog.show(I18n.tr("Clear All History?"), I18n.tr("This will permanently delete all clipboard history."), function () { clearConfirmDialog.show(I18n.tr("Clear All History?"), I18n.tr("This will permanently delete all clipboard history."), function () {
modal.clearAll(); modal.clearAll()
modal.hide(); modal.hide()
}, function () {}); }, function () {})
} }
onCloseClicked: modal.hide() onCloseClicked: modal.hide()
} }
// Search Field
DankTextField { DankTextField {
id: searchField id: searchField
width: parent.width width: parent.width
@@ -46,24 +47,27 @@ FocusScope {
showClearButton: true showClearButton: true
focus: true focus: true
ignoreTabKeys: true ignoreTabKeys: true
keyForwardTargets: [modal.modalFocusScope]
onTextChanged: { onTextChanged: {
modal.searchText = text; modal.searchText = text
modal.updateFilteredModel(); modal.updateFilteredModel()
} }
Keys.onPressed: event => { Keys.onEscapePressed: function (event) {
if (event.key === Qt.Key_Escape) { modal.hide()
modal.hide(); event.accepted = true
event.accepted = true;
return;
} }
modal.keyboardController?.handleKey(event); Component.onCompleted: {
Qt.callLater(function () {
forceActiveFocus()
})
} }
Component.onCompleted: Qt.callLater(() => forceActiveFocus())
Connections { Connections {
target: modal target: modal
function onOpened() { function onOpened() {
Qt.callLater(() => searchField.forceActiveFocus()); Qt.callLater(function () {
searchField.forceActiveFocus()
})
} }
} }
} }
@@ -93,21 +97,21 @@ FocusScope {
function ensureVisible(index) { function ensureVisible(index) {
if (index < 0 || index >= count) { if (index < 0 || index >= count) {
return; return
} }
const itemHeight = ClipboardConstants.itemHeight + spacing; const itemHeight = ClipboardConstants.itemHeight + spacing
const itemY = index * itemHeight; const itemY = index * itemHeight
const itemBottom = itemY + itemHeight; const itemBottom = itemY + itemHeight
if (itemY < contentY) { if (itemY < contentY) {
contentY = itemY; contentY = itemY
} else if (itemBottom > contentY + height) { } else if (itemBottom > contentY + height) {
contentY = itemBottom - height; contentY = itemBottom - height
} }
} }
onCurrentIndexChanged: { onCurrentIndexChanged: {
if (clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && currentIndex >= 0) { if (clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && currentIndex >= 0) {
ensureVisible(currentIndex); ensureVisible(currentIndex)
} }
} }

View File

@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
@@ -12,6 +13,11 @@ DankModal {
layerNamespace: "dms:clipboard" layerNamespace: "dms:clipboard"
HyprlandFocusGrab {
windows: [clipboardHistoryModal.contentWindow]
active: CompositorService.isHyprland && clipboardHistoryModal.shouldHaveFocus
}
property int totalCount: 0 property int totalCount: 0
property var clipboardEntries: [] property var clipboardEntries: []
property string searchText: "" property string searchText: ""
@@ -142,10 +148,11 @@ DankModal {
borderWidth: 1 borderWidth: 1
enableShadow: true enableShadow: true
onBackgroundClicked: hide() onBackgroundClicked: hide()
modalFocusScope.Keys.onPressed: function (event) {
keyboardController.handleKey(event);
}
content: clipboardContent content: clipboardContent
property alias keyboardController: keyboardController
ClipboardKeyboardController { ClipboardKeyboardController {
id: keyboardController id: keyboardController
modal: clipboardHistoryModal modal: clipboardHistoryModal
@@ -158,11 +165,13 @@ DankModal {
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
clipboardHistoryModal.shouldHaveFocus = false; clipboardHistoryModal.shouldHaveFocus = false;
return; } else if (clipboardHistoryModal.shouldBeVisible) {
}
if (!clipboardHistoryModal.shouldBeVisible)
return;
clipboardHistoryModal.shouldHaveFocus = true; clipboardHistoryModal.shouldHaveFocus = true;
clipboardHistoryModal.modalFocusScope.forceActiveFocus();
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
}
}
} }
} }

View File

@@ -61,21 +61,29 @@ DankModal {
shouldBeVisible: false shouldBeVisible: false
allowStacking: true allowStacking: true
modalWidth: 350 modalWidth: 350
modalHeight: 160 modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 160
enableShadow: true enableShadow: true
shouldHaveFocus: true shouldHaveFocus: true
onBackgroundClicked: { onBackgroundClicked: {
close(); close();
if (onCancel) if (onCancel) {
onCancel(); onCancel();
} }
}
function handleKey(event) { onOpened: {
Qt.callLater(function () {
modalFocusScope.forceActiveFocus();
modalFocusScope.focus = true;
shouldHaveFocus = true;
});
}
modalFocusScope.Keys.onPressed: function (event) {
switch (event.key) { switch (event.key) {
case Qt.Key_Escape: case Qt.Key_Escape:
close(); close();
if (onCancel) if (onCancel) {
onCancel(); onCancel();
}
event.accepted = true; event.accepted = true;
break; break;
case Qt.Key_Left: case Qt.Key_Left:
@@ -91,46 +99,46 @@ DankModal {
event.accepted = true; event.accepted = true;
break; break;
case Qt.Key_N: case Qt.Key_N:
if (!(event.modifiers & Qt.ControlModifier)) if (event.modifiers & Qt.ControlModifier) {
return;
keyboardNavigation = true; keyboardNavigation = true;
selectedButton = (selectedButton + 1) % 2; selectedButton = (selectedButton + 1) % 2;
event.accepted = true; event.accepted = true;
}
break; break;
case Qt.Key_P: case Qt.Key_P:
if (!(event.modifiers & Qt.ControlModifier)) if (event.modifiers & Qt.ControlModifier) {
return;
keyboardNavigation = true; keyboardNavigation = true;
selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2; selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2;
event.accepted = true; event.accepted = true;
}
break; break;
case Qt.Key_J: case Qt.Key_J:
if (!(event.modifiers & Qt.ControlModifier)) if (event.modifiers & Qt.ControlModifier) {
return;
keyboardNavigation = true; keyboardNavigation = true;
selectedButton = 1; selectedButton = 1;
event.accepted = true; event.accepted = true;
}
break; break;
case Qt.Key_K: case Qt.Key_K:
if (!(event.modifiers & Qt.ControlModifier)) if (event.modifiers & Qt.ControlModifier) {
return;
keyboardNavigation = true; keyboardNavigation = true;
selectedButton = 0; selectedButton = 0;
event.accepted = true; event.accepted = true;
}
break; break;
case Qt.Key_H: case Qt.Key_H:
if (!(event.modifiers & Qt.ControlModifier)) if (event.modifiers & Qt.ControlModifier) {
return;
keyboardNavigation = true; keyboardNavigation = true;
selectedButton = 0; selectedButton = 0;
event.accepted = true; event.accepted = true;
}
break; break;
case Qt.Key_L: case Qt.Key_L:
if (!(event.modifiers & Qt.ControlModifier)) if (event.modifiers & Qt.ControlModifier) {
return;
keyboardNavigation = true; keyboardNavigation = true;
selectedButton = 1; selectedButton = 1;
event.accepted = true; event.accepted = true;
}
break; break;
case Qt.Key_Tab: case Qt.Key_Tab:
keyboardNavigation = true; keyboardNavigation = true;
@@ -139,9 +147,9 @@ DankModal {
break; break;
case Qt.Key_Return: case Qt.Key_Return:
case Qt.Key_Enter: case Qt.Key_Enter:
if (selectedButton !== -1) if (selectedButton !== -1) {
selectButton(); selectButton();
else { } else {
selectedButton = 1; selectedButton = 1;
selectButton(); selectButton();
} }
@@ -151,13 +159,10 @@ DankModal {
} }
content: Component { content: Component {
FocusScope { Item {
anchors.fill: parent anchors.fill: parent
focus: true
implicitHeight: mainColumn.implicitHeight implicitHeight: mainColumn.implicitHeight
Keys.onPressed: event => root.handleKey(event)
Column { Column {
id: mainColumn id: mainColumn
anchors.left: parent.left anchors.left: parent.left

View File

@@ -1,19 +1,24 @@
import QtQuick import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
property string layerNamespace: "dms:modal" property string layerNamespace: "dms:modal"
property Component content: null property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader
property Item directContent: null property Item directContent: null
property Item loadedContent: null
readonly property var contentLoader: QtObject {
readonly property var item: root.directContent ?? root.loadedContent
}
property real modalWidth: 400 property real modalWidth: 400
property real modalHeight: 300 property real modalHeight: 300
property var targetScreen: null property var targetScreen
readonly property var effectiveScreen: targetScreen || contentWindow.screen
readonly property real screenWidth: effectiveScreen?.width
readonly property real screenHeight: effectiveScreen?.height
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
property bool showBackground: true property bool showBackground: true
property real backgroundOpacity: 0.5 property real backgroundOpacity: 0.5
property string positioning: "center" property string positioning: "center"
@@ -31,6 +36,7 @@ Item {
property real borderWidth: 1 property real borderWidth: 1
property real cornerRadius: Theme.cornerRadius property real cornerRadius: Theme.cornerRadius
property bool enableShadow: false property bool enableShadow: false
property alias modalFocusScope: focusScope
property bool shouldBeVisible: false property bool shouldBeVisible: false
property bool shouldHaveFocus: shouldBeVisible property bool shouldHaveFocus: shouldBeVisible
property bool allowFocusOverride: false property bool allowFocusOverride: false
@@ -39,41 +45,48 @@ Item {
property bool keepPopoutsOpen: false property bool keepPopoutsOpen: false
property var customKeyboardFocus: null property var customKeyboardFocus: null
property bool useOverlayLayer: false property bool useOverlayLayer: false
readonly property alias contentWindow: contentWindow
readonly property alias backgroundWindow: backgroundWindow
signal opened signal opened
signal dialogClosed signal dialogClosed
signal backgroundClicked signal backgroundClicked
onBackgroundClicked: { property bool animationsEnabled: true
if (closeOnBackgroundClick) readonly property bool useBackgroundWindow: true
close();
}
function open() { function open() {
ModalManager.openModal(root); ModalManager.openModal(root);
closeTimer.stop();
shouldBeVisible = true; shouldBeVisible = true;
shouldHaveFocus = true; contentWindow.visible = false;
DankModalWindow.showModal(root); if (useBackgroundWindow)
opened(); backgroundWindow.visible = true;
} Qt.callLater(() => {
contentWindow.visible = true;
function openCentered() { shouldHaveFocus = false;
positioning = "center"; Qt.callLater(() => {
open(); shouldHaveFocus = Qt.binding(() => shouldBeVisible);
});
});
} }
function close() { function close() {
shouldBeVisible = false; shouldBeVisible = false;
shouldHaveFocus = false; shouldHaveFocus = false;
DankModalWindow.hideModal(); closeTimer.restart();
dialogClosed();
} }
function instantClose() { function instantClose() {
animationsEnabled = false;
shouldBeVisible = false; shouldBeVisible = false;
shouldHaveFocus = false; shouldHaveFocus = false;
DankModalWindow.hideModalInstant(); closeTimer.stop();
contentWindow.visible = false;
if (useBackgroundWindow)
backgroundWindow.visible = false;
dialogClosed(); dialogClosed();
Qt.callLater(() => animationsEnabled = true);
} }
function toggle() { function toggle() {
@@ -83,9 +96,322 @@ Item {
Connections { Connections {
target: ModalManager target: ModalManager
function onCloseAllModalsExcept(excludedModal) { function onCloseAllModalsExcept(excludedModal) {
if (excludedModal === root || allowStacking || !shouldBeVisible) if (excludedModal !== root && !allowStacking && shouldBeVisible) {
return;
close(); close();
} }
} }
}
Timer {
id: closeTimer
interval: animationDuration + 120
onTriggered: {
if (!shouldBeVisible) {
contentWindow.visible = false;
if (useBackgroundWindow)
backgroundWindow.visible = false;
dialogClosed();
}
}
}
readonly property real shadowBuffer: 5
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
readonly property real alignedX: Theme.snap((() => {
switch (positioning) {
case "center":
return (screenWidth - alignedWidth) / 2;
case "top-right":
return Math.max(Theme.spacingL, screenWidth - alignedWidth - Theme.spacingL);
case "custom":
return customPosition.x;
default:
return 0;
}
})(), dpr)
readonly property real alignedY: Theme.snap((() => {
switch (positioning) {
case "center":
return (screenHeight - alignedHeight) / 2;
case "top-right":
return Theme.barHeight + Theme.spacingXS;
case "custom":
return customPosition.y;
default:
return 0;
}
})(), dpr)
PanelWindow {
id: backgroundWindow
visible: false
color: "transparent"
WlrLayershell.namespace: root.layerNamespace + ":background"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
mask: Region {
item: Rectangle {
x: root.alignedX
y: root.alignedY
width: root.shouldBeVisible ? root.alignedWidth : 0
height: root.shouldBeVisible ? root.alignedHeight : 0
}
intersection: Intersection.Xor
}
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: mouse => {
const clickX = mouse.x;
const clickY = mouse.y;
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
if (!outsideContent)
return;
root.backgroundClicked();
}
}
Rectangle {
id: background
anchors.fill: parent
color: "black"
opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.showBackground && SettingsData.modalDarkenBackground
Behavior on opacity {
enabled: root.animationsEnabled
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
}
PanelWindow {
id: contentWindow
visible: false
color: "transparent"
WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: {
if (root.useOverlayLayer)
return WlrLayershell.Overlay;
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.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (customKeyboardFocus !== null)
return customKeyboardFocus;
if (!shouldHaveFocus)
return WlrKeyboardFocus.None;
if (CompositorService.isHyprland)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
anchors {
left: true
top: true
}
WlrLayershell.margins {
left: Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
top: Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
}
implicitWidth: root.alignedWidth + (shadowBuffer * 2)
implicitHeight: root.alignedHeight + (shadowBuffer * 2)
onVisibleChanged: {
if (visible) {
opened();
} else {
if (Qt.inputMethod) {
Qt.inputMethod.hide();
Qt.inputMethod.reset();
}
}
}
Item {
id: modalContainer
x: shadowBuffer
y: shadowBuffer
width: root.alignedWidth
height: root.alignedHeight
readonly property bool slide: root.animationType === "slide"
readonly property real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset
property real animX: 0
property real animY: 0
property real scaleValue: root.animationScaleCollapsed
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
Connections {
target: root
function onShouldBeVisibleChanged() {
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
}
}
Behavior on animX {
enabled: root.animationsEnabled
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Item {
id: contentContainer
anchors.centerIn: parent
width: parent.width
height: parent.height
clip: false
Item {
id: animatedContent
anchors.fill: parent
clip: false
opacity: root.shouldBeVisible ? 1 : 0
scale: modalContainer.scaleValue
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
Behavior on opacity {
enabled: root.animationsEnabled
NumberAnimation {
duration: animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
DankRectangle {
anchors.fill: parent
color: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
radius: root.cornerRadius
}
FocusScope {
anchors.fill: parent
focus: root.shouldBeVisible
clip: false
Item {
id: directContentWrapper
anchors.fill: parent
visible: root.directContent !== null
focus: true
clip: false
Component.onCompleted: {
if (root.directContent) {
root.directContent.parent = directContentWrapper;
root.directContent.anchors.fill = directContentWrapper;
Qt.callLater(() => root.directContent.forceActiveFocus());
}
}
Connections {
target: root
function onDirectContentChanged() {
if (root.directContent) {
root.directContent.parent = directContentWrapper;
root.directContent.anchors.fill = directContentWrapper;
Qt.callLater(() => root.directContent.forceActiveFocus());
}
}
}
}
Loader {
id: contentLoader
anchors.fill: parent
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible)
asynchronous: false
focus: true
clip: false
visible: root.directContent === null
onLoaded: {
if (item) {
Qt.callLater(() => item.forceActiveFocus());
}
}
}
}
}
}
}
FocusScope {
id: focusScope
objectName: "modalFocusScope"
anchors.fill: parent
visible: root.shouldBeVisible || contentWindow.visible
focus: root.shouldBeVisible
Keys.onEscapePressed: event => {
if (root.closeOnEscapeKey && shouldHaveFocus) {
root.close();
event.accepted = true;
}
}
}
}
} }

View File

@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -10,6 +11,11 @@ DankModal {
layerNamespace: "dms:color-picker" layerNamespace: "dms:color-picker"
HyprlandFocusGrab {
windows: [root.contentWindow]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property string pickerTitle: I18n.tr("Choose Color") property string pickerTitle: I18n.tr("Choose Color")
property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary
property var onColorSelectedCallback: null property var onColorSelectedCallback: null

View File

@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -16,6 +17,12 @@ DankModal {
modalWidth: _maxW modalWidth: _maxW
modalHeight: _maxH modalHeight: _maxH
onBackgroundClicked: close() onBackgroundClicked: close()
onOpened: () => Qt.callLater(() => modalFocusScope.forceActiveFocus())
HyprlandFocusGrab {
windows: [root.contentWindow]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
function scrollDown() { function scrollDown() {
if (!root.activeFlickable) if (!root.activeFlickable)
@@ -33,35 +40,25 @@ DankModal {
root.activeFlickable.contentY = newY; root.activeFlickable.contentY = newY;
} }
content: Component { modalFocusScope.Keys.onPressed: event => {
FocusScope { if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
anchors.fill: parent scrollDown();
focus: true event.accepted = true;
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
scrollUp();
event.accepted = true;
} else if (event.key === Qt.Key_Down) {
scrollDown();
event.accepted = true;
} else if (event.key === Qt.Key_Up) {
scrollUp();
event.accepted = true;
}
}
Keys.onPressed: event => { content: Component {
switch (event.key) { Item {
case Qt.Key_J: anchors.fill: parent
if (!(event.modifiers & Qt.ControlModifier))
return;
root.scrollDown();
event.accepted = true;
break;
case Qt.Key_K:
if (!(event.modifiers & Qt.ControlModifier))
return;
root.scrollUp();
event.accepted = true;
break;
case Qt.Key_Down:
root.scrollDown();
event.accepted = true;
break;
case Qt.Key_Up:
root.scrollUp();
event.accepted = true;
break;
}
}
Column { Column {
anchors.fill: parent anchors.fill: parent

View File

@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
@@ -10,6 +11,11 @@ DankModal {
layerNamespace: "dms:notification-center-modal" layerNamespace: "dms:notification-center-modal"
HyprlandFocusGrab {
windows: [notificationModal.contentWindow]
active: CompositorService.isHyprland && notificationModal.shouldHaveFocus
}
property bool notificationModalOpen: false property bool notificationModalOpen: false
property var notificationListRef: null property var notificationListRef: null
@@ -55,6 +61,9 @@ DankModal {
modalHeight: 700 modalHeight: 700
visible: false visible: false
onBackgroundClicked: hide() onBackgroundClicked: hide()
onOpened: () => {
Qt.callLater(() => modalFocusScope.forceActiveFocus());
}
onShouldBeVisibleChanged: shouldBeVisible => { onShouldBeVisibleChanged: shouldBeVisible => {
if (!shouldBeVisible) { if (!shouldBeVisible) {
notificationModalOpen = false; notificationModalOpen = false;
@@ -62,6 +71,7 @@ DankModal {
NotificationService.onOverlayClose(); NotificationService.onOverlayClose();
} }
} }
modalFocusScope.Keys.onPressed: event => modalKeyboardController.handleKey(event)
NotificationKeyboardController { NotificationKeyboardController {
id: modalKeyboardController id: modalKeyboardController
@@ -91,12 +101,10 @@ DankModal {
} }
content: Component { content: Component {
FocusScope { Item {
id: notificationKeyHandler id: notificationKeyHandler
anchors.fill: parent
focus: true
Keys.onPressed: event => modalKeyboardController.handleKey(event) anchors.fill: parent
Column { Column {
anchors.fill: parent anchors.fill: parent

View File

@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -12,6 +13,11 @@ DankModal {
layerNamespace: "dms:power-menu" layerNamespace: "dms:power-menu"
keepPopoutsOpen: true keepPopoutsOpen: true
HyprlandFocusGrab {
windows: [root.contentWindow]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property int selectedIndex: 0 property int selectedIndex: 0
property int selectedRow: 0 property int selectedRow: 0
property int selectedCol: 0 property int selectedCol: 0
@@ -269,11 +275,34 @@ DankModal {
} else { } else {
selectedIndex = defaultIndex; selectedIndex = defaultIndex;
} }
Qt.callLater(() => modalFocusScope.forceActiveFocus());
} }
onDialogClosed: () => { onDialogClosed: () => {
cancelHold(); cancelHold();
} }
Component.onCompleted: updateVisibleActions() Component.onCompleted: updateVisibleActions()
modalFocusScope.Keys.onPressed: event => {
if (event.isAutoRepeat) {
event.accepted = true;
return;
}
if (SettingsData.powerMenuGridLayout) {
handleGridNavigation(event, true);
} else {
handleListNavigation(event, true);
}
}
modalFocusScope.Keys.onReleased: event => {
if (event.isAutoRepeat) {
event.accepted = true;
return;
}
if (SettingsData.powerMenuGridLayout) {
handleGridNavigation(event, false);
} else {
handleListNavigation(event, false);
}
}
function handleListNavigation(event, isPressed) { function handleListNavigation(event, isPressed) {
if (!isPressed) { if (!isPressed) {
@@ -452,33 +481,10 @@ DankModal {
} }
content: Component { content: Component {
FocusScope { Item {
anchors.fill: parent anchors.fill: parent
focus: true
implicitHeight: (SettingsData.powerMenuGridLayout ? buttonGrid.implicitHeight : buttonColumn.implicitHeight) + Theme.spacingL * 2 + (root.needsConfirmation ? hintRow.height + Theme.spacingM : 0) implicitHeight: (SettingsData.powerMenuGridLayout ? buttonGrid.implicitHeight : buttonColumn.implicitHeight) + Theme.spacingL * 2 + (root.needsConfirmation ? hintRow.height + Theme.spacingM : 0)
Keys.onPressed: event => {
if (event.isAutoRepeat) {
event.accepted = true;
return;
}
if (SettingsData.powerMenuGridLayout)
root.handleGridNavigation(event, true);
else
root.handleListNavigation(event, true);
}
Keys.onReleased: event => {
if (event.isAutoRepeat) {
event.accepted = true;
return;
}
if (SettingsData.powerMenuGridLayout)
root.handleGridNavigation(event, false);
else
root.handleListNavigation(event, false);
}
Grid { Grid {
id: buttonGrid id: buttonGrid
visible: SettingsData.powerMenuGridLayout visible: SettingsData.powerMenuGridLayout

View File

@@ -51,21 +51,7 @@ Item {
anchors.fill: parent anchors.fill: parent
focus: true focus: true
clip: false clip: false
onActiveFocusChanged: {
if (!activeFocus)
return;
if (!searchField)
return;
searchField.forceActiveFocus();
}
Keys.onPressed: event => { Keys.onPressed: event => {
const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item;
if (menu?.visible) {
menu.handleKey(event);
return;
}
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
if (parentModal) if (parentModal)
parentModal.hide(); parentModal.hide();
@@ -211,6 +197,7 @@ Item {
parent: spotlightKeyHandler parent: spotlightKeyHandler
appLauncher: spotlightKeyHandler.appLauncher appLauncher: spotlightKeyHandler.appLauncher
parentHandler: spotlightKeyHandler
searchField: spotlightKeyHandler.searchField searchField: spotlightKeyHandler.searchField
visible: false visible: false
z: 1000 z: 1000
@@ -231,6 +218,8 @@ Item {
sourceComponent: Component { sourceComponent: Component {
SpotlightContextMenu { SpotlightContextMenu {
appLauncher: spotlightKeyHandler.appLauncher appLauncher: spotlightKeyHandler.appLauncher
parentHandler: spotlightKeyHandler
parentModal: spotlightKeyHandler.parentModal
} }
} }
} }
@@ -291,12 +280,6 @@ Item {
updateSearchMode(); updateSearchMode();
} }
Keys.onPressed: event => { Keys.onPressed: event => {
const menu = spotlightKeyHandler.usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item;
if (menu?.visible) {
menu.handleKey(event);
return;
}
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
if (parentModal) if (parentModal)
parentModal.hide(); parentModal.hide();
@@ -329,7 +312,7 @@ Item {
Row { Row {
id: viewModeButtons id: viewModeButtons
spacing: Theme.spacingXS spacing: Theme.spacingXS
visible: searchMode === "apps" visible: searchMode === "apps" && appLauncher.model.count > 0
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter

View File

@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modals.Spotlight import qs.Modals.Spotlight
@@ -10,15 +11,17 @@ PanelWindow {
WlrLayershell.namespace: "dms:spotlight-context-menu" WlrLayershell.namespace: "dms:spotlight-context-menu"
WlrLayershell.layer: WlrLayershell.Overlay WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
property var appLauncher: null property var appLauncher: null
property var parentHandler: null
property var parentModal: null
property real menuPositionX: 0 property real menuPositionX: 0
property real menuPositionY: 0 property real menuPositionY: 0
readonly property real shadowBuffer: 5 readonly property real shadowBuffer: 5
screen: DankModalWindow.targetScreen screen: parentModal?.effectiveScreen
function show(x, y, app, fromKeyboard) { function show(x, y, app, fromKeyboard) {
fromKeyboard = fromKeyboard || false; fromKeyboard = fromKeyboard || false;
@@ -27,15 +30,14 @@ PanelWindow {
let screenX = x; let screenX = x;
let screenY = y; let screenY = y;
const modalX = DankModalWindow.modalX; if (parentModal) {
const modalY = DankModalWindow.modalY;
if (fromKeyboard) { if (fromKeyboard) {
screenX = x + modalX; screenX = x + parentModal.alignedX;
screenY = y + modalY; screenY = y + parentModal.alignedY;
} else { } else {
screenX = x + (modalX - shadowBuffer); screenX = x + (parentModal.alignedX - shadowBuffer);
screenY = y + (modalY - shadowBuffer); screenY = y + (parentModal.alignedY - shadowBuffer);
}
} }
menuPositionX = screenX; menuPositionX = screenX;
@@ -44,32 +46,19 @@ PanelWindow {
menuContent.selectedMenuIndex = fromKeyboard ? 0 : -1; menuContent.selectedMenuIndex = fromKeyboard ? 0 : -1;
menuContent.keyboardNavigation = true; menuContent.keyboardNavigation = true;
visible = true; visible = true;
}
function handleKey(event) { if (parentHandler) {
switch (event.key) { parentHandler.enabled = false;
case Qt.Key_Down:
menuContent.selectNext();
event.accepted = true;
break;
case Qt.Key_Up:
menuContent.selectPrevious();
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
menuContent.activateSelected();
event.accepted = true;
break;
case Qt.Key_Escape:
case Qt.Key_Menu:
hide();
event.accepted = true;
break;
} }
Qt.callLater(() => {
menuContent.keyboardHandler.forceActiveFocus();
});
} }
function hide() { function hide() {
if (parentHandler) {
parentHandler.enabled = true;
}
visible = false; visible = false;
} }
@@ -82,6 +71,11 @@ PanelWindow {
bottom: true bottom: true
} }
onVisibleChanged: {
if (!visible && parentHandler) {
parentHandler.enabled = true;
}
}
SpotlightContextMenuContent { SpotlightContextMenuContent {
id: menuContent id: menuContent

View File

@@ -8,6 +8,7 @@ Popup {
id: root id: root
property var appLauncher: null property var appLauncher: null
property var parentHandler: null
property var searchField: null property var searchField: null
function show(x, y, app, fromKeyboard) { function show(x, y, app, fromKeyboard) {
@@ -20,45 +21,38 @@ Popup {
menuContent.selectedMenuIndex = fromKeyboard ? 0 : -1; menuContent.selectedMenuIndex = fromKeyboard ? 0 : -1;
menuContent.keyboardNavigation = true; menuContent.keyboardNavigation = true;
if (parentHandler) {
parentHandler.enabled = false;
}
open(); open();
} }
function handleKey(event) { onOpened: {
switch (event.key) { Qt.callLater(() => {
case Qt.Key_Down: menuContent.keyboardHandler.forceActiveFocus();
menuContent.selectNext(); });
event.accepted = true;
break;
case Qt.Key_Up:
menuContent.selectPrevious();
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
menuContent.activateSelected();
event.accepted = true;
break;
case Qt.Key_Escape:
case Qt.Key_Menu:
hide();
event.accepted = true;
break;
}
} }
function hide() { function hide() {
if (parentHandler) {
parentHandler.enabled = true;
}
close(); close();
} }
width: menuContent.implicitWidth width: menuContent.implicitWidth
height: menuContent.implicitHeight height: menuContent.implicitHeight
padding: 0 padding: 0
closePolicy: Popup.CloseOnPressOutside closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
modal: true modal: true
dim: false dim: false
background: Item {} background: Item {}
onClosed: { onClosed: {
if (parentHandler) {
parentHandler.enabled = true;
}
if (searchField) { if (searchField) {
Qt.callLater(() => { Qt.callLater(() => {
searchField.forceActiveFocus(); searchField.forceActiveFocus();

View File

@@ -1,13 +1,20 @@
import QtQuick import QtQuick
import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services
DankModal { DankModal {
id: spotlightModal id: spotlightModal
layerNamespace: "dms:spotlight" layerNamespace: "dms:spotlight"
HyprlandFocusGrab {
windows: [spotlightModal.contentWindow]
active: CompositorService.isHyprland && spotlightModal.shouldHaveFocus
}
property bool spotlightOpen: false property bool spotlightOpen: false
property alias spotlightContent: spotlightContentInstance property alias spotlightContent: spotlightContentInstance
property bool openedFromOverview: false property bool openedFromOverview: false
@@ -16,18 +23,32 @@ DankModal {
openedFromOverview = false; openedFromOverview = false;
spotlightOpen = true; spotlightOpen = true;
open(); open();
Qt.callLater(() => {
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus();
}
});
} }
function showWithQuery(query) { function showWithQuery(query) {
if (spotlightContent) { if (spotlightContent) {
if (spotlightContent.appLauncher) if (spotlightContent.appLauncher) {
spotlightContent.appLauncher.searchQuery = query; spotlightContent.appLauncher.searchQuery = query;
if (spotlightContent.searchField) }
if (spotlightContent.searchField) {
spotlightContent.searchField.text = query; spotlightContent.searchField.text = query;
} }
}
spotlightOpen = true; spotlightOpen = true;
open(); open();
Qt.callLater(() => {
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus();
}
});
} }
function hide() { function hide() {
@@ -36,25 +57,24 @@ DankModal {
close(); close();
} }
function onFullyClosed() { onDialogClosed: {
resetContent(); if (spotlightContent) {
}
function resetContent() {
if (!spotlightContent)
return;
if (spotlightContent.appLauncher) { if (spotlightContent.appLauncher) {
spotlightContent.appLauncher.searchQuery = ""; spotlightContent.appLauncher.searchQuery = "";
spotlightContent.appLauncher.selectedIndex = 0; spotlightContent.appLauncher.selectedIndex = 0;
spotlightContent.appLauncher.setCategory(I18n.tr("All")); spotlightContent.appLauncher.setCategory(I18n.tr("All"));
} }
if (spotlightContent.fileSearchController) if (spotlightContent.fileSearchController) {
spotlightContent.fileSearchController.reset(); spotlightContent.fileSearchController.reset();
if (spotlightContent.resetScroll) }
if (spotlightContent.resetScroll) {
spotlightContent.resetScroll(); spotlightContent.resetScroll();
if (spotlightContent.searchField) }
if (spotlightContent.searchField) {
spotlightContent.searchField.text = ""; spotlightContent.searchField.text = "";
} }
}
}
function toggle() { function toggle() {
if (spotlightOpen) { if (spotlightOpen) {
@@ -78,11 +98,17 @@ DankModal {
animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
animationExitCurve: Theme.expressiveCurves.emphasized animationExitCurve: Theme.expressiveCurves.emphasized
onVisibleChanged: () => { onVisibleChanged: () => {
if (!visible) if (visible && !spotlightOpen) {
return;
if (!spotlightOpen)
show(); show();
} }
if (visible && spotlightContent) {
Qt.callLater(() => {
if (spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus();
}
});
}
}
onBackgroundClicked: () => { onBackgroundClicked: () => {
return hide(); return hide();
} }
@@ -130,10 +156,6 @@ DankModal {
target: "spotlight" target: "spotlight"
} }
Component.onCompleted: {
DankModalWindow.persistentModal = spotlightModal;
}
SpotlightContent { SpotlightContent {
id: spotlightContentInstance id: spotlightContentInstance

View File

@@ -3,6 +3,7 @@ import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modals.Spotlight
import qs.Modules.AppDrawer import qs.Modules.AppDrawer
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -12,6 +13,27 @@ DankPopout {
layerNamespace: "dms:app-launcher" layerNamespace: "dms:app-launcher"
property string searchMode: "apps"
property alias fileSearch: fileSearchController
function updateSearchMode(text) {
if (text.startsWith("/")) {
if (searchMode === "files") {
fileSearchController.searchQuery = text.substring(1);
return;
}
searchMode = "files";
fileSearchController.searchQuery = text.substring(1);
return;
}
if (searchMode === "apps") {
return;
}
searchMode = "apps";
fileSearchController.reset();
appLauncher.searchQuery = text;
}
function show() { function show() {
open(); open();
} }
@@ -29,9 +51,11 @@ DankPopout {
} }
onOpened: { onOpened: {
searchMode = "apps";
appLauncher.searchQuery = ""; appLauncher.searchQuery = "";
appLauncher.selectedIndex = 0; appLauncher.selectedIndex = 0;
appLauncher.setCategory(I18n.tr("All")); appLauncher.setCategory(I18n.tr("All"));
fileSearchController.reset();
if (contentLoader.item?.searchField) { if (contentLoader.item?.searchField) {
contentLoader.item.searchField.text = ""; contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus(); contentLoader.item.searchField.forceActiveFocus();
@@ -50,6 +74,23 @@ DankPopout {
} }
} }
FileSearchController {
id: fileSearchController
onFileOpened: appDrawerPopout.close()
}
onSearchModeChanged: {
switch (searchMode) {
case "files":
appLauncher.keyboardNavigationActive = false;
break;
case "apps":
fileSearchController.keyboardNavigationActive = false;
break;
}
}
content: Component { content: Component {
Rectangle { Rectangle {
id: launcherPanel id: launcherPanel
@@ -96,17 +137,48 @@ DankPopout {
anchors.fill: parent anchors.fill: parent
focus: true focus: true
function selectNext() {
switch (appDrawerPopout.searchMode) {
case "files":
fileSearchController.selectNext();
return;
default:
appLauncher.selectNext();
}
}
function selectPrevious() {
switch (appDrawerPopout.searchMode) {
case "files":
fileSearchController.selectPrevious();
return;
default:
appLauncher.selectPrevious();
}
}
function activateSelected() {
switch (appDrawerPopout.searchMode) {
case "files":
fileSearchController.openSelected();
return;
default:
appLauncher.launchSelected();
}
}
readonly property var keyMappings: { readonly property var keyMappings: {
const mappings = {}; const mappings = {};
mappings[Qt.Key_Escape] = () => appDrawerPopout.close(); mappings[Qt.Key_Escape] = () => appDrawerPopout.close();
mappings[Qt.Key_Down] = () => appLauncher.selectNext(); mappings[Qt.Key_Down] = () => keyHandler.selectNext();
mappings[Qt.Key_Up] = () => appLauncher.selectPrevious(); mappings[Qt.Key_Up] = () => keyHandler.selectPrevious();
mappings[Qt.Key_Return] = () => appLauncher.launchSelected(); mappings[Qt.Key_Return] = () => keyHandler.activateSelected();
mappings[Qt.Key_Enter] = () => appLauncher.launchSelected(); mappings[Qt.Key_Enter] = () => keyHandler.activateSelected();
mappings[Qt.Key_Tab] = () => appLauncher.viewMode === "grid" ? appLauncher.selectNextInRow() : appLauncher.selectNext(); mappings[Qt.Key_Tab] = () => appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid" ? appLauncher.selectNextInRow() : keyHandler.selectNext();
mappings[Qt.Key_Backtab] = () => appLauncher.viewMode === "grid" ? appLauncher.selectPreviousInRow() : appLauncher.selectPrevious(); mappings[Qt.Key_Backtab] = () => appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid" ? appLauncher.selectPreviousInRow() : keyHandler.selectPrevious();
if (appLauncher.viewMode === "grid") { if (appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid") {
mappings[Qt.Key_Right] = () => appLauncher.selectNextInRow(); mappings[Qt.Key_Right] = () => appLauncher.selectNextInRow();
mappings[Qt.Key_Left] = () => appLauncher.selectPreviousInRow(); mappings[Qt.Key_Left] = () => appLauncher.selectPreviousInRow();
} }
@@ -121,42 +193,34 @@ DankPopout {
return; return;
} }
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) { const hasCtrl = event.modifiers & Qt.ControlModifier;
appLauncher.selectNext(); if (!hasCtrl) {
event.accepted = true;
return; return;
} }
if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) { switch (event.key) {
appLauncher.selectPrevious(); case Qt.Key_N:
case Qt.Key_J:
keyHandler.selectNext();
event.accepted = true; event.accepted = true;
return; return;
} case Qt.Key_P:
case Qt.Key_K:
if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) { keyHandler.selectPrevious();
appLauncher.selectNext();
event.accepted = true; event.accepted = true;
return; return;
} case Qt.Key_L:
if (appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid") {
if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
appLauncher.selectPrevious();
event.accepted = true;
return;
}
if (appLauncher.viewMode === "grid") {
if (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNextInRow(); appLauncher.selectNextInRow();
event.accepted = true; event.accepted = true;
return;
} }
return;
if (event.key === Qt.Key_H && event.modifiers & Qt.ControlModifier) { case Qt.Key_H:
if (appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow(); appLauncher.selectPreviousInRow();
event.accepted = true; event.accepted = true;
return;
} }
return;
} }
} }
@@ -175,7 +239,7 @@ DankPopout {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingS anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Applications") text: appDrawerPopout.searchMode === "files" ? I18n.tr("Files") : I18n.tr("Applications")
font.pixelSize: Theme.fontSizeLarge + 4 font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold font.weight: Font.Bold
color: Theme.surfaceText color: Theme.surfaceText
@@ -185,7 +249,14 @@ DankPopout {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.spacingS anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: appLauncher.model.count + " apps" text: {
switch (appDrawerPopout.searchMode) {
case "files":
return fileSearchController.model.count + " " + I18n.tr("files");
default:
return appLauncher.model.count + " " + I18n.tr("apps");
}
}
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
@@ -201,19 +272,24 @@ DankPopout {
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary focusedBorderColor: Theme.primary
leftIconName: "search" leftIconName: appDrawerPopout.searchMode === "files" ? "folder" : "search"
leftIconSize: Theme.iconSize leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary leftIconFocusedColor: Theme.primary
showClearButton: true showClearButton: true
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
enabled: appDrawerPopout.shouldBeVisible enabled: appDrawerPopout.shouldBeVisible
ignoreLeftRightKeys: appLauncher.viewMode !== "list" ignoreLeftRightKeys: appDrawerPopout.searchMode === "apps" && appLauncher.viewMode !== "list"
ignoreTabKeys: true ignoreTabKeys: true
keyForwardTargets: [keyHandler] keyForwardTargets: [keyHandler]
onTextEdited: { onTextChanged: {
if (appDrawerPopout.searchMode === "apps") {
appLauncher.searchQuery = text; appLauncher.searchQuery = text;
} }
}
onTextEdited: {
appDrawerPopout.updateSearchMode(text);
}
Keys.onPressed: function (event) { Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
appDrawerPopout.close(); appDrawerPopout.close();
@@ -225,6 +301,14 @@ DankPopout {
const hasText = text.length > 0; const hasText = text.length > 0;
if (isEnterKey && hasText) { if (isEnterKey && hasText) {
switch (appDrawerPopout.searchMode) {
case "files":
if (fileSearchController.model.count > 0) {
fileSearchController.openSelected();
}
event.accepted = true;
return;
default:
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) { if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) {
appLauncher.launchSelected(); appLauncher.launchSelected();
} else if (appLauncher.model.count > 0) { } else if (appLauncher.model.count > 0) {
@@ -233,6 +317,7 @@ DankPopout {
event.accepted = true; event.accepted = true;
return; return;
} }
}
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Tab, Qt.Key_Backtab]; const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Tab, Qt.Key_Backtab];
const isNavigationKey = navigationKeys.includes(event.key); const isNavigationKey = navigationKeys.includes(event.key);
@@ -256,7 +341,7 @@ DankPopout {
width: parent.width - Theme.spacingS * 2 width: parent.width - Theme.spacingS * 2
height: 40 height: 40
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
visible: searchField.text.length === 0 visible: searchField.text.length === 0 && appDrawerPopout.searchMode === "apps"
Rectangle { Rectangle {
width: 180 width: 180
@@ -316,7 +401,7 @@ DankPopout {
height: { height: {
let usedHeight = 40 + Theme.spacingS; let usedHeight = 40 + Theme.spacingS;
usedHeight += 52 + Theme.spacingS; usedHeight += 52 + Theme.spacingS;
usedHeight += (searchField.text.length === 0 ? 40 : 0); usedHeight += (searchField.text.length === 0 && appDrawerPopout.searchMode === "apps" ? 40 : 0);
return parent.height - usedHeight; return parent.height - usedHeight;
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -349,7 +434,7 @@ DankPopout {
anchors.fill: parent anchors.fill: parent
anchors.bottomMargin: Theme.spacingS anchors.bottomMargin: Theme.spacingS
visible: appLauncher.viewMode === "list" visible: appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "list"
model: appLauncher.model model: appLauncher.model
currentIndex: appLauncher.selectedIndex currentIndex: appLauncher.selectedIndex
clip: true clip: true
@@ -434,7 +519,7 @@ DankPopout {
anchors.fill: parent anchors.fill: parent
anchors.bottomMargin: Theme.spacingS anchors.bottomMargin: Theme.spacingS
visible: appLauncher.viewMode === "grid" visible: appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid"
model: appLauncher.model model: appLauncher.model
clip: true clip: true
cellWidth: baseCellWidth cellWidth: baseCellWidth
@@ -484,6 +569,12 @@ DankPopout {
onKeyboardNavigationReset: appGrid.keyboardNavigationReset onKeyboardNavigationReset: appGrid.keyboardNavigationReset
} }
} }
FileSearchResults {
anchors.fill: parent
fileSearchController: appDrawerPopout.fileSearch
visible: appDrawerPopout.searchMode === "files"
}
} }
} }
} }

View File

@@ -18,6 +18,9 @@ Item {
property int _lastDataVersion: -1 property int _lastDataVersion: -1
property var _cachedCategories: [] property var _cachedCategories: []
property var _filteredBinds: [] property var _filteredBinds: []
property real _savedScrollY: 0
property bool _preserveScroll: false
property string _editingKey: ""
function _updateFiltered() { function _updateFiltered() {
const allBinds = KeybindsService.getFlatBinds(); const allBinds = KeybindsService.getFlatBinds();
@@ -87,6 +90,9 @@ Item {
function saveNewBind(bindData) { function saveNewBind(bindData) {
KeybindsService.saveBind("", bindData); KeybindsService.saveBind("", bindData);
showingNewBind = false; showingNewBind = false;
selectedCategory = "";
_editingKey = bindData.key;
expandedKey = bindData.action;
} }
function scrollToTop() { function scrollToTop() {
@@ -102,9 +108,24 @@ Item {
Connections { Connections {
target: KeybindsService target: KeybindsService
function onBindsLoaded() { function onBindsLoaded() {
const savedY = keybindsTab._savedScrollY;
const wasPreserving = keybindsTab._preserveScroll;
keybindsTab._lastDataVersion = KeybindsService._dataVersion; keybindsTab._lastDataVersion = KeybindsService._dataVersion;
keybindsTab._updateCategories(); keybindsTab._updateCategories();
keybindsTab._updateFiltered(); keybindsTab._updateFiltered();
keybindsTab._preserveScroll = false;
if (wasPreserving)
Qt.callLater(() => flickable.contentY = savedY);
}
function onBindSaved(key) {
keybindsTab._savedScrollY = flickable.contentY;
keybindsTab._preserveScroll = true;
keybindsTab._editingKey = key;
}
function onBindRemoved(key) {
keybindsTab._savedScrollY = flickable.contentY;
keybindsTab._preserveScroll = true;
keybindsTab._editingKey = "";
} }
} }
@@ -228,14 +249,33 @@ Item {
} }
StyledRect { StyledRect {
id: warningBox
width: Math.min(650, parent.width - Theme.spacingL * 2) width: Math.min(650, parent.width - Theme.spacingL * 2)
height: warningSection.implicitHeight + Theme.spacingL * 2 height: warningSection.implicitHeight + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.error, 0.15)
border.color: Theme.withAlpha(Theme.error, 0.3) readonly property var status: KeybindsService.dmsStatus
readonly property bool showError: !status.included && status.exists
readonly property bool showWarning: status.included && status.overriddenBy > 0
readonly property bool showSetup: !status.exists
color: {
if (showError || showSetup)
return Theme.withAlpha(Theme.error, 0.15);
if (showWarning)
return Theme.withAlpha(Theme.warning ?? Theme.tertiary, 0.15);
return "transparent";
}
border.color: {
if (showError || showSetup)
return Theme.withAlpha(Theme.error, 0.3);
if (showWarning)
return Theme.withAlpha(Theme.warning ?? Theme.tertiary, 0.3);
return "transparent";
}
border.width: 1 border.width: 1
visible: !KeybindsService.dmsBindsIncluded && !KeybindsService.loading visible: (showError || showWarning || showSetup) && !KeybindsService.loading
Column { Column {
id: warningSection id: warningSection
@@ -248,26 +288,44 @@ Item {
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon { DankIcon {
name: "warning" name: warningBox.showWarning ? "info" : "warning"
size: Theme.iconSize size: Theme.iconSize
color: Theme.error color: warningBox.showWarning ? (Theme.warning ?? Theme.tertiary) : Theme.error
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Column { Column {
width: parent.width - Theme.iconSize - 100 - Theme.spacingM * 2 width: parent.width - Theme.iconSize - (fixButton.visible ? fixButton.width + Theme.spacingM : 0) - Theme.spacingM
spacing: Theme.spacingXS spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
StyledText { StyledText {
text: I18n.tr("Binds Include Missing") text: {
if (warningBox.showSetup)
return I18n.tr("First Time Setup");
if (warningBox.showError)
return I18n.tr("Binds Include Missing");
if (warningBox.showWarning)
return I18n.tr("Possible Override Conflicts");
return "";
}
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.error color: warningBox.showWarning ? (Theme.warning ?? Theme.tertiary) : Theme.error
} }
StyledText { StyledText {
text: I18n.tr("dms/binds.kdl is not included in config.kdl. Custom keybinds will not work until this is fixed.") text: {
if (warningBox.showSetup)
return I18n.tr("Click 'Setup' to create dms/binds.kdl and add include to config.kdl.");
if (warningBox.showError)
return I18n.tr("dms/binds.kdl exists but is not included in config.kdl. Custom keybinds will not work until this is fixed.");
if (warningBox.showWarning) {
const count = warningBox.status.overriddenBy;
return I18n.tr("%1 DMS bind(s) may be overridden by config binds that come after the include.").arg(count);
}
return "";
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -280,12 +338,19 @@ Item {
width: fixButtonText.implicitWidth + Theme.spacingL * 2 width: fixButtonText.implicitWidth + Theme.spacingL * 2
height: 36 height: 36
radius: Theme.cornerRadius radius: Theme.cornerRadius
visible: warningBox.showError || warningBox.showSetup
color: KeybindsService.fixing ? Theme.withAlpha(Theme.error, 0.6) : Theme.error color: KeybindsService.fixing ? Theme.withAlpha(Theme.error, 0.6) : Theme.error
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
StyledText { StyledText {
id: fixButtonText id: fixButtonText
text: KeybindsService.fixing ? I18n.tr("Fixing...") : I18n.tr("Fix Now") text: {
if (KeybindsService.fixing)
return I18n.tr("Fixing...");
if (warningBox.showSetup)
return I18n.tr("Setup");
return I18n.tr("Fix Now");
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surface color: Theme.surface
@@ -532,6 +597,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
bindData: modelData bindData: modelData
isExpanded: keybindsTab.expandedKey === modelData.action isExpanded: keybindsTab.expandedKey === modelData.action
restoreKey: isExpanded ? keybindsTab._editingKey : ""
panelWindow: keybindsTab.parentModal panelWindow: keybindsTab.parentModal
onToggleExpand: keybindsTab.toggleExpanded(modelData.action) onToggleExpand: keybindsTab.toggleExpanded(modelData.action)
onSaveBind: (originalKey, newData) => { onSaveBind: (originalKey, newData) => {
@@ -539,6 +605,7 @@ Item {
keybindsTab.expandedKey = modelData.action; keybindsTab.expandedKey = modelData.action;
} }
onRemoveBind: key => KeybindsService.removeBind(key) onRemoveBind: key => KeybindsService.removeBind(key)
onRestoreKeyConsumed: keybindsTab._editingKey = ""
} }
} }
} }

View File

@@ -95,6 +95,10 @@ FloatingWindow {
if (!parentModal) if (!parentModal)
return; return;
parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible); parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible);
Qt.callLater(() => {
if (parentModal.modalFocusScope)
parentModal.modalFocusScope.forceActiveFocus();
});
} }
objectName: "pluginBrowser" objectName: "pluginBrowser"

View File

@@ -82,6 +82,10 @@ FloatingWindow {
if (!parentModal) if (!parentModal)
return; return;
parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible); parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible);
Qt.callLater(() => {
if (parentModal && parentModal.modalFocusScope)
parentModal.modalFocusScope.forceActiveFocus();
});
} }
objectName: "widgetSelectionPopup" objectName: "widgetSelectionPopup"
@@ -108,6 +112,10 @@ FloatingWindow {
if (!parentModal) if (!parentModal)
return; return;
parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible); parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible);
Qt.callLater(() => {
if (parentModal && parentModal.modalFocusScope)
parentModal.modalFocusScope.forceActiveFocus();
});
} }
FocusScope { FocusScope {

View File

@@ -1,12 +1,9 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Services
Scope { Scope {
id: overviewScope id: overviewScope

View File

@@ -2,7 +2,6 @@ import QtQuick
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Services import qs.Services
@@ -13,6 +12,7 @@ Item {
required property var panelWindow required property var panelWindow
required property bool overviewOpen required property bool overviewOpen
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen) readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
readonly property real dpr: CompositorService.getScreenScale(panelWindow.screen)
readonly property int workspacesShown: SettingsData.overviewRows * SettingsData.overviewColumns readonly property int workspacesShown: SettingsData.overviewRows * SettingsData.overviewColumns
readonly property var allWorkspaces: Hyprland.workspaces?.values || [] readonly property var allWorkspaces: Hyprland.workspaces?.values || []
@@ -77,6 +77,9 @@ Item {
readonly property int maxWorkspaceId: displayedWorkspaceIds.length > 0 ? displayedWorkspaceIds[displayedWorkspaceIds.length - 1] : workspacesShown readonly property int maxWorkspaceId: displayedWorkspaceIds.length > 0 ? displayedWorkspaceIds[displayedWorkspaceIds.length - 1] : workspacesShown
readonly property int displayWorkspaceCount: displayedWorkspaceIds.length readonly property int displayWorkspaceCount: displayedWorkspaceIds.length
readonly property int effectiveColumns: SettingsData.overviewColumns
readonly property int effectiveRows: Math.max(SettingsData.overviewRows, Math.ceil(displayWorkspaceCount / effectiveColumns))
function getWorkspaceMonitorName(workspaceId) { function getWorkspaceMonitorName(workspaceId) {
if (!allWorkspaces || !workspaceId) return "" if (!allWorkspaces || !workspaceId) return ""
try { try {
@@ -103,8 +106,10 @@ Item {
property real scale: SettingsData.overviewScale property real scale: SettingsData.overviewScale
property color activeBorderColor: Theme.primary property color activeBorderColor: Theme.primary
property real workspaceImplicitWidth: ((monitor.width / monitor.scale) * root.scale) readonly property real monitorPhysicalWidth: panelWindow.screen ? (panelWindow.screen.width / root.dpr) : (monitor?.width ?? 1920)
property real workspaceImplicitHeight: ((monitor.height / monitor.scale) * root.scale) readonly property real monitorPhysicalHeight: panelWindow.screen ? (panelWindow.screen.height / root.dpr) : (monitor?.height ?? 1080)
property real workspaceImplicitWidth: monitorPhysicalWidth * root.scale
property real workspaceImplicitHeight: monitorPhysicalHeight * root.scale
property int workspaceZ: 0 property int workspaceZ: 0
property int windowZ: 1 property int windowZ: 1
@@ -162,18 +167,18 @@ Item {
spacing: workspaceSpacing spacing: workspaceSpacing
Repeater { Repeater {
model: SettingsData.overviewRows model: root.effectiveRows
delegate: RowLayout { delegate: RowLayout {
id: row id: row
property int rowIndex: index property int rowIndex: index
spacing: workspaceSpacing spacing: workspaceSpacing
Repeater { Repeater {
model: SettingsData.overviewColumns model: root.effectiveColumns
Rectangle { Rectangle {
id: workspace id: workspace
property int colIndex: index property int colIndex: index
property int workspaceIndex: rowIndex * SettingsData.overviewColumns + colIndex property int workspaceIndex: rowIndex * root.effectiveColumns + colIndex
property int workspaceValue: (root.displayedWorkspaceIds && workspaceIndex < root.displayedWorkspaceIds.length) ? root.displayedWorkspaceIds[workspaceIndex] : -1 property int workspaceValue: (root.displayedWorkspaceIds && workspaceIndex < root.displayedWorkspaceIds.length) ? root.displayedWorkspaceIds[workspaceIndex] : -1
property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false
property var workspaceObj: (workspaceExists && Hyprland.workspaces?.values) ? Hyprland.workspaces.values.find(ws => ws?.id === workspaceValue) : null property var workspaceObj: (workspaceExists && Hyprland.workspaces?.values) ? Hyprland.workspaces.values.find(ws => ws?.id === workspaceValue) : null
@@ -292,11 +297,12 @@ Item {
} }
readonly property int workspaceIndex: getWorkspaceIndex() readonly property int workspaceIndex: getWorkspaceIndex()
readonly property int workspaceColIndex: workspaceIndex % SettingsData.overviewColumns readonly property int workspaceColIndex: workspaceIndex % root.effectiveColumns
readonly property int workspaceRowIndex: Math.floor(workspaceIndex / SettingsData.overviewColumns) readonly property int workspaceRowIndex: Math.floor(workspaceIndex / root.effectiveColumns)
toplevel: modelData toplevel: modelData
scale: root.scale scale: root.scale
monitorDpr: root.dpr
availableWorkspaceWidth: root.workspaceImplicitWidth availableWorkspaceWidth: root.workspaceImplicitWidth
availableWorkspaceHeight: root.workspaceImplicitHeight availableWorkspaceHeight: root.workspaceImplicitHeight
widgetMonitorId: root.monitor.id widgetMonitorId: root.monitor.id
@@ -376,7 +382,7 @@ Item {
z: root.monitorLabelZ z: root.monitorLabelZ
Repeater { Repeater {
model: SettingsData.overviewRows model: root.effectiveRows
delegate: Item { delegate: Item {
id: labelRow id: labelRow
property int rowIndex: index property int rowIndex: index
@@ -385,11 +391,11 @@ Item {
height: root.workspaceImplicitHeight height: root.workspaceImplicitHeight
Repeater { Repeater {
model: SettingsData.overviewColumns model: root.effectiveColumns
delegate: Item { delegate: Item {
id: labelItem id: labelItem
property int colIndex: index property int colIndex: index
property int workspaceIndex: labelRow.rowIndex * SettingsData.overviewColumns + colIndex property int workspaceIndex: labelRow.rowIndex * root.effectiveColumns + colIndex
property int workspaceValue: (root.displayedWorkspaceIds && workspaceIndex < root.displayedWorkspaceIds.length) ? root.displayedWorkspaceIds[workspaceIndex] : -1 property int workspaceValue: (root.displayedWorkspaceIds && workspaceIndex < root.displayedWorkspaceIds.length) ? root.displayedWorkspaceIds[workspaceIndex] : -1
property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false
property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : "" property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : ""

View File

@@ -13,19 +13,21 @@ Item {
property var availableWorkspaceWidth property var availableWorkspaceWidth
property var availableWorkspaceHeight property var availableWorkspaceHeight
property bool restrictToWorkspace: true property bool restrictToWorkspace: true
property real monitorDpr: 1
readonly property var windowData: toplevel?.lastIpcObject || null readonly property var windowData: toplevel?.lastIpcObject || null
readonly property var monitorObj: toplevel?.monitor readonly property var monitorObj: toplevel?.monitor
readonly property var monitorData: monitorObj?.lastIpcObject || null readonly property var monitorData: monitorObj?.lastIpcObject || null
readonly property real effectiveScale: root.scale / root.monitorDpr
property real initX: Math.max(((windowData?.at?.[0] ?? 0) - (monitorData?.x ?? 0) - (monitorData?.reserved?.[0] ?? 0)) * root.scale, 0) + xOffset property real initX: Math.max(((windowData?.at?.[0] ?? 0) - (monitorData?.x ?? 0) - (monitorData?.reserved?.[0] ?? 0)) * effectiveScale, 0) + xOffset
property real initY: Math.max(((windowData?.at?.[1] ?? 0) - (monitorData?.y ?? 0) - (monitorData?.reserved?.[1] ?? 0)) * root.scale, 0) + yOffset property real initY: Math.max(((windowData?.at?.[1] ?? 0) - (monitorData?.y ?? 0) - (monitorData?.reserved?.[1] ?? 0)) * effectiveScale, 0) + yOffset
property real xOffset: 0 property real xOffset: 0
property real yOffset: 0 property real yOffset: 0
property int widgetMonitorId: 0 property int widgetMonitorId: 0
property var targetWindowWidth: (windowData?.size?.[0] ?? 100) * scale property var targetWindowWidth: (windowData?.size?.[0] ?? 100) * effectiveScale
property var targetWindowHeight: (windowData?.size?.[1] ?? 100) * scale property var targetWindowHeight: (windowData?.size?.[1] ?? 100) * effectiveScale
property bool hovered: false property bool hovered: false
property bool pressed: false property bool pressed: false
@@ -37,8 +39,8 @@ Item {
x: initX x: initX
y: initY y: initY
width: Math.min((windowData?.size?.[0] ?? 100) * root.scale, availableWorkspaceWidth) width: Math.min((windowData?.size?.[0] ?? 100) * effectiveScale, availableWorkspaceWidth)
height: Math.min((windowData?.size?.[1] ?? 100) * root.scale, availableWorkspaceHeight) height: Math.min((windowData?.size?.[1] ?? 100) * effectiveScale, availableWorkspaceHeight)
opacity: (monitorObj?.id ?? -1) == widgetMonitorId ? 1 : 0.4 opacity: (monitorObj?.id ?? -1) == widgetMonitorId ? 1 : 0.4
Rectangle { Rectangle {

View File

@@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.I3
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
@@ -24,31 +23,6 @@ Singleton {
readonly property string labwcPid: Quickshell.env("LABWC_PID") readonly property string labwcPid: Quickshell.env("LABWC_PID")
property bool useNiriSorting: isNiri && NiriService property bool useNiriSorting: isNiri && NiriService
readonly property string focusedScreenName: {
if (isHyprland && Hyprland.focusedMonitor)
return Hyprland.focusedMonitor.name;
if (isNiri && NiriService.currentOutput)
return NiriService.currentOutput;
if (isDwl && DwlService.activeOutput)
return DwlService.activeOutput;
if (isSway) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
if (focusedWs?.monitor?.name)
return focusedWs.monitor.name;
}
return Quickshell.screens[0]?.name ?? "";
}
readonly property var focusedScreen: {
if (!focusedScreenName)
return Quickshell.screens[0] ?? null;
for (const s of Quickshell.screens) {
if (s.name === focusedScreenName)
return s;
}
return Quickshell.screens[0] ?? null;
}
property var sortedToplevels: [] property var sortedToplevels: []
property bool _sortScheduled: false property bool _sortScheduled: false

View File

@@ -28,6 +28,17 @@ Singleton {
property string lastError: "" property string lastError: ""
property bool dmsBindsIncluded: true property bool dmsBindsIncluded: true
property var dmsStatus: ({
exists: true,
included: true,
includePosition: -1,
totalIncludes: 0,
bindsAfterDms: 0,
effective: true,
overriddenBy: 0,
statusMessage: ""
})
property var _rawData: null property var _rawData: null
property var keybinds: ({}) property var keybinds: ({})
property var _allBinds: ({}) property var _allBinds: ({})
@@ -60,6 +71,14 @@ Singleton {
} }
} }
Connections {
target: NiriService
enabled: CompositorService.isNiri
function onConfigReloaded() {
Qt.callLater(root.loadBinds, false);
}
}
Process { Process {
id: loadProcess id: loadProcess
running: false running: false
@@ -178,7 +197,7 @@ Singleton {
} }
function loadBinds(showLoading) { function loadBinds(showLoading) {
if (loading || !available) if (loadProcess.running || !available)
return; return;
const hasData = Object.keys(_allBinds).length > 0; const hasData = Object.keys(_allBinds).length > 0;
loading = showLoading !== false && !hasData; loading = showLoading !== false && !hasData;
@@ -188,8 +207,22 @@ Singleton {
function _processData() { function _processData() {
keybinds = _rawData || {}; keybinds = _rawData || {};
if (currentProvider === "niri") if (currentProvider === "niri") {
dmsBindsIncluded = _rawData?.dmsBindsIncluded ?? true; dmsBindsIncluded = _rawData?.dmsBindsIncluded ?? true;
const status = _rawData?.dmsStatus;
if (status) {
dmsStatus = {
exists: status.exists ?? true,
included: status.included ?? true,
includePosition: status.includePosition ?? -1,
totalIncludes: status.totalIncludes ?? 0,
bindsAfterDms: status.bindsAfterDms ?? 0,
effective: status.effective ?? true,
overriddenBy: status.overriddenBy ?? 0,
statusMessage: status.statusMessage ?? ""
};
}
}
if (!_rawData?.binds) { if (!_rawData?.binds) {
_allBinds = {}; _allBinds = {};
@@ -239,12 +272,15 @@ Singleton {
actionMap[action].keys.push(keyData); actionMap[action].keys.push(keyData);
if (!actionMap[action].desc && bind.desc) if (!actionMap[action].desc && bind.desc)
actionMap[action].desc = bind.desc; actionMap[action].desc = bind.desc;
if (!actionMap[action].conflict && bind.conflict)
actionMap[action].conflict = bind.conflict;
} else { } else {
const entry = { const entry = {
category: category, category: category,
action: action, action: action,
desc: bind.desc || "", desc: bind.desc || "",
keys: [keyData] keys: [keyData],
conflict: bind.conflict || null
}; };
actionMap[action] = entry; actionMap[action] = entry;
grouped.push(entry); grouped.push(entry);

View File

@@ -39,6 +39,7 @@ Singleton {
property string pendingScreenshotPath: "" property string pendingScreenshotPath: ""
signal windowUrgentChanged signal windowUrgentChanged
signal configReloaded
function setWorkspaces(newMap) { function setWorkspaces(newMap) {
root.workspaces = newMap; root.workspaces = newMap;
@@ -508,17 +509,20 @@ Singleton {
function handleConfigLoaded(data) { function handleConfigLoaded(data) {
if (data.failed) { if (data.failed) {
validateProcess.running = true; validateProcess.running = true;
} else { return;
}
configValidationOutput = ""; configValidationOutput = "";
ToastService.dismissCategory("niri-config"); ToastService.dismissCategory("niri-config");
fetchOutputs(); fetchOutputs();
configReloaded();
if (hasInitialConnection && !suppressConfigToast && !suppressNextConfigToast && !matugenSuppression) { if (hasInitialConnection && !suppressConfigToast && !suppressNextConfigToast && !matugenSuppression) {
ToastService.showInfo("niri: config reloaded", "", "", "niri-config"); ToastService.showInfo("niri: config reloaded", "", "", "niri-config");
} else if (suppressNextConfigToast) { } else if (suppressNextConfigToast) {
suppressNextConfigToast = false; suppressNextConfigToast = false;
suppressResetTimer.stop(); suppressResetTimer.stop();
} }
}
if (!hasInitialConnection) { if (!hasInitialConnection) {
hasInitialConnection = true; hasInitialConnection = true;

View File

@@ -17,6 +17,7 @@ Item {
property var panelWindow: null property var panelWindow: null
property bool recording: false property bool recording: false
property bool isNew: false property bool isNew: false
property string restoreKey: ""
property int editingKeyIndex: -1 property int editingKeyIndex: -1
property string editKey: "" property string editKey: ""
@@ -44,6 +45,8 @@ Item {
} }
return false; return false;
} }
readonly property var configConflict: bindData.conflict || null
readonly property bool hasConfigConflict: configConflict !== null
readonly property string _originalKey: editingKeyIndex >= 0 && editingKeyIndex < keys.length ? keys[editingKeyIndex].key : "" readonly property string _originalKey: editingKeyIndex >= 0 && editingKeyIndex < keys.length ? keys[editingKeyIndex].key : ""
readonly property var _conflicts: editKey ? KeyUtils.getConflictingBinds(editKey, bindData.action, KeybindsService.getFlatBinds()) : [] readonly property var _conflicts: editKey ? KeyUtils.getConflictingBinds(editKey, bindData.action, KeybindsService.getFlatBinds()) : []
readonly property bool hasConflict: _conflicts.length > 0 readonly property bool hasConflict: _conflicts.length > 0
@@ -52,6 +55,7 @@ Item {
signal saveBind(string originalKey, var newData) signal saveBind(string originalKey, var newData)
signal removeBind(string key) signal removeBind(string key)
signal cancelEdit signal cancelEdit
signal restoreKeyConsumed
implicitHeight: contentColumn.implicitHeight implicitHeight: contentColumn.implicitHeight
height: implicitHeight height: implicitHeight
@@ -59,7 +63,36 @@ Item {
Component.onDestruction: _destroyShortcutInhibitor() Component.onDestruction: _destroyShortcutInhibitor()
onIsExpandedChanged: { onIsExpandedChanged: {
if (isExpanded) if (!isExpanded)
return;
if (restoreKey) {
restoreToKey(restoreKey);
restoreKeyConsumed();
} else {
resetEdits();
}
}
onRestoreKeyChanged: {
if (!isExpanded || !restoreKey)
return;
restoreToKey(restoreKey);
restoreKeyConsumed();
}
function restoreToKey(keyToFind) {
for (let i = 0; i < keys.length; i++) {
if (keys[i].key === keyToFind) {
editingKeyIndex = i;
editKey = keyToFind;
editAction = bindData.action || "";
editDesc = bindData.desc || "";
hasChanges = false;
_actionType = Actions.getActionType(editAction);
useCustomCompositor = _actionType === "compositor" && !Actions.isKnownCompositorAction(editAction);
return;
}
}
resetEdits(); resetEdits();
} }
@@ -276,7 +309,21 @@ Item {
text: I18n.tr("Override") text: I18n.tr("Override")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.primary color: Theme.primary
visible: root.hasOverride visible: root.hasOverride && !root.hasConfigConflict
}
DankIcon {
name: "warning"
size: 14
color: Theme.warning ?? Theme.tertiary
visible: root.hasConfigConflict
}
StyledText {
text: I18n.tr("Overridden by config")
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning ?? Theme.tertiary
visible: root.hasConfigConflict
} }
Item { Item {
@@ -334,6 +381,58 @@ Item {
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
spacing: Theme.spacingM spacing: Theme.spacingM
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: conflictColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.warning ?? Theme.tertiary, 0.15)
border.color: Theme.withAlpha(Theme.warning ?? Theme.tertiary, 0.3)
border.width: 1
visible: root.hasConfigConflict
Column {
id: conflictColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
RowLayout {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "warning"
size: 16
color: Theme.warning ?? Theme.tertiary
}
StyledText {
text: I18n.tr("This bind is overridden by config.kdl")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.warning ?? Theme.tertiary
Layout.fillWidth: true
}
}
StyledText {
text: I18n.tr("Config action: %1").arg(root.configConflict?.action ?? "")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
StyledText {
text: I18n.tr("To use this DMS bind, remove or change the keybind in your config.kdl")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
}
}
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Theme.spacingM spacing: Theme.spacingM