1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -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 "Installation complete!"
@echo ""
@echo "=== The DMS Team! ==="
@echo "=== Cheers, the DMS Team! ==="
# Uninstallation targets
uninstall-bin:

View File

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

View File

@@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
@@ -53,14 +54,29 @@ func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
n.parsed = true
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",
Provider: n.Name(),
Binds: categorizedBinds,
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 {
@@ -78,7 +94,7 @@ func (n *NiriProvider) HasDMSBindsIncluded() bool {
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
if section.Name != "" {
currentSubcat = section.Name
@@ -86,12 +102,12 @@ func (n *NiriProvider) convertSection(section *NiriSection, subcategory string,
for _, kb := range section.Keybinds {
category := n.categorizeByAction(kb.Action)
bind := n.convertKeybind(&kb, currentSubcat)
bind := n.convertKeybind(&kb, currentSubcat, conflicts)
categorizedBinds[category] = append(categorizedBinds[category], bind)
}
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)
keyStr := n.formatKey(kb)
source := "config"
if strings.Contains(kb.Source, "dms/binds.kdl") {
source = "dms"
}
return keybinds.Keybind{
Key: n.formatKey(kb),
bind := keybinds.Keybind{
Key: keyStr,
Description: kb.Description,
Action: rawAction,
Subcategory: subcategory,
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 {
@@ -386,6 +416,29 @@ func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error
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 {
if len(binds) == 0 {
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
sb.WriteString("binds {\n")

View File

@@ -26,35 +26,78 @@ type NiriSection struct {
}
type NiriParser struct {
configDir string
processedFiles map[string]bool
bindMap map[string]*NiriKeyBinding
bindOrder []string
currentSource string
dmsBindsIncluded bool
configDir string
processedFiles map[string]bool
bindMap map[string]*NiriKeyBinding
bindOrder []string
currentSource string
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 {
return &NiriParser{
configDir: configDir,
processedFiles: make(map[string]bool),
bindMap: make(map[string]*NiriKeyBinding),
bindOrder: []string{},
currentSource: "",
configDir: configDir,
processedFiles: make(map[string]bool),
bindMap: make(map[string]*NiriKeyBinding),
bindOrder: []string{},
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) {
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")
section, err := p.parseFile(configPath, "")
if err != nil {
return nil, err
}
if p.dmsBindsExists && !p.dmsProcessed {
p.parseDMSBindsDirectly(dmsBindsPath, section)
}
section.Keybinds = p.finalizeBinds()
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 {
binds := make([]NiriKeyBinding, 0, len(p.bindOrder))
for _, key := range p.bindOrder {
@@ -67,6 +110,20 @@ func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
func (p *NiriParser) addBind(kb *NiriKeyBinding) {
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 {
p.bindOrder = append(p.bindOrder, key)
}
@@ -105,9 +162,11 @@ func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, erro
Name: sectionName,
}
prevSource := p.currentSource
p.currentSource = absPath
baseDir := filepath.Dir(absPath)
p.processNodes(doc.Nodes, section, baseDir)
p.currentSource = prevSource
return section, nil
}
@@ -133,8 +192,13 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba
}
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.dmsIncludePos = p.includeCount
p.bindsBeforeDMS = len(p.bindMap)
}
fullPath := filepath.Join(baseDir, includePath)
@@ -142,6 +206,10 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba
fullPath = includePath
}
if isDMSInclude {
p.dmsProcessed = true
}
includedSection, err := p.parseFile(fullPath, "")
if err != nil {
return
@@ -230,8 +298,49 @@ func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
}
type NiriParseResult struct {
Section *NiriSection
DMSBindsIncluded bool
Section *NiriSection
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) {
@@ -241,7 +350,9 @@ func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
return nil, err
}
return &NiriParseResult{
Section: section,
DMSBindsIncluded: parser.HasDMSBindsIncluded(),
Section: section,
DMSBindsIncluded: parser.HasDMSBindsIncluded(),
DMSStatus: parser.buildDMSStatus(),
ConflictingConfigs: parser.conflictingConfigs,
}, nil
}

View File

@@ -1,11 +1,23 @@
package keybinds
type Keybind struct {
Key string `json:"key"`
Description string `json:"desc"`
Action string `json:"action,omitempty"`
Subcategory string `json:"subcat,omitempty"`
Source string `json:"source,omitempty"`
Key string `json:"key"`
Description string `json:"desc"`
Action string `json:"action,omitempty"`
Subcategory string `json:"subcat,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 {
@@ -13,6 +25,7 @@ type CheatSheet struct {
Provider string `json:"provider"`
Binds map[string][]Keybind `json:"binds"`
DMSBindsIncluded bool `json:"dmsBindsIncluded"`
DMSStatus *DMSBindsStatus `json:"dmsStatus,omitempty"`
}
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 {
id: root
readonly property bool _forceDisplayService: DisplayService.brightnessAvailable !== undefined
Instantiator {
id: daemonPluginInstantiator
asynchronous: true

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,24 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property string layerNamespace: "dms:modal"
property Component content: null
property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader
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 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 real backgroundOpacity: 0.5
property string positioning: "center"
@@ -31,6 +36,7 @@ Item {
property real borderWidth: 1
property real cornerRadius: Theme.cornerRadius
property bool enableShadow: false
property alias modalFocusScope: focusScope
property bool shouldBeVisible: false
property bool shouldHaveFocus: shouldBeVisible
property bool allowFocusOverride: false
@@ -39,41 +45,48 @@ Item {
property bool keepPopoutsOpen: false
property var customKeyboardFocus: null
property bool useOverlayLayer: false
readonly property alias contentWindow: contentWindow
readonly property alias backgroundWindow: backgroundWindow
signal opened
signal dialogClosed
signal backgroundClicked
onBackgroundClicked: {
if (closeOnBackgroundClick)
close();
}
property bool animationsEnabled: true
readonly property bool useBackgroundWindow: true
function open() {
ModalManager.openModal(root);
closeTimer.stop();
shouldBeVisible = true;
shouldHaveFocus = true;
DankModalWindow.showModal(root);
opened();
}
function openCentered() {
positioning = "center";
open();
contentWindow.visible = false;
if (useBackgroundWindow)
backgroundWindow.visible = true;
Qt.callLater(() => {
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => {
shouldHaveFocus = Qt.binding(() => shouldBeVisible);
});
});
}
function close() {
shouldBeVisible = false;
shouldHaveFocus = false;
DankModalWindow.hideModal();
dialogClosed();
closeTimer.restart();
}
function instantClose() {
animationsEnabled = false;
shouldBeVisible = false;
shouldHaveFocus = false;
DankModalWindow.hideModalInstant();
closeTimer.stop();
contentWindow.visible = false;
if (useBackgroundWindow)
backgroundWindow.visible = false;
dialogClosed();
Qt.callLater(() => animationsEnabled = true);
}
function toggle() {
@@ -83,9 +96,322 @@ Item {
Connections {
target: ModalManager
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal === root || allowStacking || !shouldBeVisible)
return;
close();
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
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 Quickshell
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -10,6 +11,11 @@ DankModal {
layerNamespace: "dms:color-picker"
HyprlandFocusGrab {
windows: [root.contentWindow]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property string pickerTitle: I18n.tr("Choose Color")
property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary
property var onColorSelectedCallback: null

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -12,6 +13,11 @@ DankModal {
layerNamespace: "dms:power-menu"
keepPopoutsOpen: true
HyprlandFocusGrab {
windows: [root.contentWindow]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property int selectedIndex: 0
property int selectedRow: 0
property int selectedCol: 0
@@ -269,11 +275,34 @@ DankModal {
} else {
selectedIndex = defaultIndex;
}
Qt.callLater(() => modalFocusScope.forceActiveFocus());
}
onDialogClosed: () => {
cancelHold();
}
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) {
if (!isPressed) {
@@ -452,33 +481,10 @@ DankModal {
}
content: Component {
FocusScope {
Item {
anchors.fill: parent
focus: true
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 {
id: buttonGrid
visible: SettingsData.powerMenuGridLayout

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Modals.Spotlight
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
@@ -12,6 +13,27 @@ DankPopout {
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() {
open();
}
@@ -29,9 +51,11 @@ DankPopout {
}
onOpened: {
searchMode = "apps";
appLauncher.searchQuery = "";
appLauncher.selectedIndex = 0;
appLauncher.setCategory(I18n.tr("All"));
fileSearchController.reset();
if (contentLoader.item?.searchField) {
contentLoader.item.searchField.text = "";
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 {
Rectangle {
id: launcherPanel
@@ -96,17 +137,48 @@ DankPopout {
anchors.fill: parent
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: {
const mappings = {};
mappings[Qt.Key_Escape] = () => appDrawerPopout.close();
mappings[Qt.Key_Down] = () => appLauncher.selectNext();
mappings[Qt.Key_Up] = () => appLauncher.selectPrevious();
mappings[Qt.Key_Return] = () => appLauncher.launchSelected();
mappings[Qt.Key_Enter] = () => appLauncher.launchSelected();
mappings[Qt.Key_Tab] = () => appLauncher.viewMode === "grid" ? appLauncher.selectNextInRow() : appLauncher.selectNext();
mappings[Qt.Key_Backtab] = () => appLauncher.viewMode === "grid" ? appLauncher.selectPreviousInRow() : appLauncher.selectPrevious();
mappings[Qt.Key_Down] = () => keyHandler.selectNext();
mappings[Qt.Key_Up] = () => keyHandler.selectPrevious();
mappings[Qt.Key_Return] = () => keyHandler.activateSelected();
mappings[Qt.Key_Enter] = () => keyHandler.activateSelected();
mappings[Qt.Key_Tab] = () => appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid" ? appLauncher.selectNextInRow() : keyHandler.selectNext();
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_Left] = () => appLauncher.selectPreviousInRow();
}
@@ -121,42 +193,34 @@ DankPopout {
return;
}
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNext();
event.accepted = true;
const hasCtrl = event.modifiers & Qt.ControlModifier;
if (!hasCtrl) {
return;
}
if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
appLauncher.selectPrevious();
switch (event.key) {
case Qt.Key_N:
case Qt.Key_J:
keyHandler.selectNext();
event.accepted = true;
return;
}
if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNext();
case Qt.Key_P:
case Qt.Key_K:
keyHandler.selectPrevious();
event.accepted = true;
return;
}
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) {
case Qt.Key_L:
if (appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow();
event.accepted = true;
return;
}
if (event.key === Qt.Key_H && event.modifiers & Qt.ControlModifier) {
return;
case Qt.Key_H:
if (appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow();
event.accepted = true;
return;
}
return;
}
}
@@ -175,7 +239,7 @@ DankPopout {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Applications")
text: appDrawerPopout.searchMode === "files" ? I18n.tr("Files") : I18n.tr("Applications")
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
@@ -185,7 +249,14 @@ DankPopout {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
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
color: Theme.surfaceVariantText
}
@@ -201,18 +272,23 @@ DankPopout {
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconName: appDrawerPopout.searchMode === "files" ? "folder" : "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
font.pixelSize: Theme.fontSizeLarge
enabled: appDrawerPopout.shouldBeVisible
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
ignoreLeftRightKeys: appDrawerPopout.searchMode === "apps" && appLauncher.viewMode !== "list"
ignoreTabKeys: true
keyForwardTargets: [keyHandler]
onTextChanged: {
if (appDrawerPopout.searchMode === "apps") {
appLauncher.searchQuery = text;
}
}
onTextEdited: {
appLauncher.searchQuery = text;
appDrawerPopout.updateSearchMode(text);
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
@@ -225,13 +301,22 @@ DankPopout {
const hasText = text.length > 0;
if (isEnterKey && hasText) {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) {
appLauncher.launchSelected();
} else if (appLauncher.model.count > 0) {
appLauncher.launchApp(appLauncher.model.get(0));
switch (appDrawerPopout.searchMode) {
case "files":
if (fileSearchController.model.count > 0) {
fileSearchController.openSelected();
}
event.accepted = true;
return;
default:
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) {
appLauncher.launchSelected();
} else if (appLauncher.model.count > 0) {
appLauncher.launchApp(appLauncher.model.get(0));
}
event.accepted = true;
return;
}
event.accepted = true;
return;
}
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Tab, Qt.Key_Backtab];
@@ -256,7 +341,7 @@ DankPopout {
width: parent.width - Theme.spacingS * 2
height: 40
anchors.horizontalCenter: parent.horizontalCenter
visible: searchField.text.length === 0
visible: searchField.text.length === 0 && appDrawerPopout.searchMode === "apps"
Rectangle {
width: 180
@@ -316,7 +401,7 @@ DankPopout {
height: {
let usedHeight = 40 + 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;
}
radius: Theme.cornerRadius
@@ -349,7 +434,7 @@ DankPopout {
anchors.fill: parent
anchors.bottomMargin: Theme.spacingS
visible: appLauncher.viewMode === "list"
visible: appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
clip: true
@@ -434,7 +519,7 @@ DankPopout {
anchors.fill: parent
anchors.bottomMargin: Theme.spacingS
visible: appLauncher.viewMode === "grid"
visible: appDrawerPopout.searchMode === "apps" && appLauncher.viewMode === "grid"
model: appLauncher.model
clip: true
cellWidth: baseCellWidth
@@ -484,6 +569,12 @@ DankPopout {
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 var _cachedCategories: []
property var _filteredBinds: []
property real _savedScrollY: 0
property bool _preserveScroll: false
property string _editingKey: ""
function _updateFiltered() {
const allBinds = KeybindsService.getFlatBinds();
@@ -87,6 +90,9 @@ Item {
function saveNewBind(bindData) {
KeybindsService.saveBind("", bindData);
showingNewBind = false;
selectedCategory = "";
_editingKey = bindData.key;
expandedKey = bindData.action;
}
function scrollToTop() {
@@ -102,9 +108,24 @@ Item {
Connections {
target: KeybindsService
function onBindsLoaded() {
const savedY = keybindsTab._savedScrollY;
const wasPreserving = keybindsTab._preserveScroll;
keybindsTab._lastDataVersion = KeybindsService._dataVersion;
keybindsTab._updateCategories();
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 {
id: warningBox
width: Math.min(650, parent.width - Theme.spacingL * 2)
height: warningSection.implicitHeight + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
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
visible: !KeybindsService.dmsBindsIncluded && !KeybindsService.loading
visible: (showError || showWarning || showSetup) && !KeybindsService.loading
Column {
id: warningSection
@@ -248,26 +288,44 @@ Item {
spacing: Theme.spacingM
DankIcon {
name: "warning"
name: warningBox.showWarning ? "info" : "warning"
size: Theme.iconSize
color: Theme.error
color: warningBox.showWarning ? (Theme.warning ?? Theme.tertiary) : Theme.error
anchors.verticalCenter: parent.verticalCenter
}
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
anchors.verticalCenter: parent.verticalCenter
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.weight: Font.Medium
color: Theme.error
color: warningBox.showWarning ? (Theme.warning ?? Theme.tertiary) : Theme.error
}
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
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
@@ -280,12 +338,19 @@ Item {
width: fixButtonText.implicitWidth + Theme.spacingL * 2
height: 36
radius: Theme.cornerRadius
visible: warningBox.showError || warningBox.showSetup
color: KeybindsService.fixing ? Theme.withAlpha(Theme.error, 0.6) : Theme.error
anchors.verticalCenter: parent.verticalCenter
StyledText {
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.weight: Font.Medium
color: Theme.surface
@@ -532,6 +597,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
bindData: modelData
isExpanded: keybindsTab.expandedKey === modelData.action
restoreKey: isExpanded ? keybindsTab._editingKey : ""
panelWindow: keybindsTab.parentModal
onToggleExpand: keybindsTab.toggleExpanded(modelData.action)
onSaveBind: (originalKey, newData) => {
@@ -539,6 +605,7 @@ Item {
keybindsTab.expandedKey = modelData.action;
}
onRemoveBind: key => KeybindsService.removeBind(key)
onRestoreKeyConsumed: keybindsTab._editingKey = ""
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.I3
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
@@ -24,31 +23,6 @@ Singleton {
readonly property string labwcPid: Quickshell.env("LABWC_PID")
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 bool _sortScheduled: false

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ Item {
property var panelWindow: null
property bool recording: false
property bool isNew: false
property string restoreKey: ""
property int editingKeyIndex: -1
property string editKey: ""
@@ -44,6 +45,8 @@ Item {
}
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 var _conflicts: editKey ? KeyUtils.getConflictingBinds(editKey, bindData.action, KeybindsService.getFlatBinds()) : []
readonly property bool hasConflict: _conflicts.length > 0
@@ -52,6 +55,7 @@ Item {
signal saveBind(string originalKey, var newData)
signal removeBind(string key)
signal cancelEdit
signal restoreKeyConsumed
implicitHeight: contentColumn.implicitHeight
height: implicitHeight
@@ -59,8 +63,37 @@ Item {
Component.onDestruction: _destroyShortcutInhibitor()
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();
}
onEditActionChanged: {
@@ -276,7 +309,21 @@ Item {
text: I18n.tr("Override")
font.pixelSize: Theme.fontSizeSmall
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 {
@@ -334,6 +381,58 @@ Item {
anchors.margins: Theme.spacingL
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 {
Layout.fillWidth: true
spacing: Theme.spacingM