mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-31 15:37:47 -04:00
feat(Hyprland): add native special workspace scratchpad support
Closes #1000
This commit is contained in:
1 parent
f0040ffcba
commit
a3a9fe13eb
16 files changed
+443
-82
No files matched your search
@@ -101,6 +101,11 @@ hl.bind("SUPER + CTRL + I", hl.dsp.window.move({ workspace = "e-1" }))
|
||||
-- === Workspace Management ===
|
||||
hl.bind("CTRL + SHIFT + R", hl.dsp.exec_cmd("dms ipc call workspace-rename open"))
|
||||
|
||||
-- === Scratchpad ===
|
||||
hl.bind("SUPER + S", hl.dsp.workspace.toggle_special())
|
||||
hl.bind("SUPER + SHIFT + S", hl.dsp.window.move({ workspace = "special", follow = false }))
|
||||
hl.bind("SUPER + CTRL + S", hl.dsp.window.move({ workspace = "+0" }))
|
||||
|
||||
-- === Move Workspaces ===
|
||||
hl.bind("SUPER + SHIFT + Page_Down", hl.dsp.window.move({ workspace = "e+1" }))
|
||||
hl.bind("SUPER + SHIFT + Page_Up", hl.dsp.window.move({ workspace = "e-1" }))
|
||||
|
||||
@@ -386,6 +386,7 @@ const HYPRLAND_ACTIONS = {
|
||||
{ id: "movetoworkspacesilent +1", label: "Move to Next (silent)" },
|
||||
{ id: "movetoworkspacesilent -1", label: "Move to Previous (silent)" },
|
||||
{ id: "togglespecialworkspace", label: "Toggle Special Workspace" },
|
||||
{ id: "movetoworkspace +0", label: "Move Out of Special Workspace" },
|
||||
{ id: "focusworkspaceoncurrentmonitor", label: "Focus Workspace on Current Monitor" },
|
||||
{ id: "renameworkspace", label: "Rename Workspace" }
|
||||
],
|
||||
|
||||
@@ -227,8 +227,39 @@ function hyprlandPersistentWorkspaces(raw, records, screenName, followFocus, min
|
||||
return filled.sort(hyprlandOrder);
|
||||
}
|
||||
|
||||
function hyprlandWorkspacesForScreen(raw, screenName, followFocus, occupiedOnly, minCount) {
|
||||
return hyprlandPersistentWorkspaces(raw, hyprlandListedWorkspaces(raw, screenName, followFocus, occupiedOnly), screenName, followFocus, minCount);
|
||||
function hyprlandSpecialDisplayName(name) {
|
||||
return name.startsWith("special:") ? name.slice(8) : name;
|
||||
}
|
||||
|
||||
// Hyprland >= 0.56 reports every special workspace with a null id, so specials only match by name
|
||||
function hyprlandWorkspaceMatches(ws, record) {
|
||||
if (record.special !== true)
|
||||
return ws?.id === record.id;
|
||||
const name = ws?.name ?? "";
|
||||
return hyprlandSpecialName(name) && hyprlandSpecialDisplayName(name) === record.name;
|
||||
}
|
||||
|
||||
// Hyprland overlays a special workspace on the monitor's regular one, so active comes from the tracked overlay, not ws.active
|
||||
function hyprlandSpecialWorkspaces(raw, screenName, followFocus, occupiedOnly) {
|
||||
const perMonitor = !!screenName && !followFocus;
|
||||
const visible = raw.visibleSpecials ?? {};
|
||||
const toplevels = raw.toplevels;
|
||||
const isVisible = normalized => perMonitor ? visible[screenName] === normalized : Object.values(visible).includes(normalized);
|
||||
const specials = raw.workspaces.filter(ws => hyprlandSpecial(ws) && (!perMonitor || ws.monitor?.name === screenName)).map(ws => {
|
||||
const name = ws.name ?? "";
|
||||
return { id: ws.id, idx: null, name: hyprlandSpecialDisplayName(name), output: ws.monitor?.name ?? "", active: isVisible(name === "special" ? "special:special" : name), placeholder: false, urgent: ws.urgent === true, special: true };
|
||||
}).filter(ws => !occupiedOnly || ws.active || toplevels.some(tl => hyprlandWorkspaceMatches(tl.workspace, ws))).sort((a, b) => a.name.localeCompare(b.name));
|
||||
// the default scratchpad is always offered so it can be opened before Hyprland has created it
|
||||
if (!specials.some(ws => ws.name === "special"))
|
||||
specials.push({ id: null, idx: null, name: "special", output: screenName ?? "", active: isVisible("special:special"), placeholder: false, urgent: false, special: true });
|
||||
return specials;
|
||||
}
|
||||
|
||||
function hyprlandWorkspacesForScreen(raw, screenName, followFocus, occupiedOnly, minCount, showSpecial) {
|
||||
const regular = hyprlandPersistentWorkspaces(raw, hyprlandListedWorkspaces(raw, screenName, followFocus, occupiedOnly), screenName, followFocus, minCount);
|
||||
if (!showSpecial)
|
||||
return regular;
|
||||
return regular.concat(hyprlandSpecialWorkspaces(raw, screenName, followFocus, occupiedOnly));
|
||||
}
|
||||
|
||||
function hyprlandScrollWorkspaces(raw, screenName, followFocus) {
|
||||
@@ -246,11 +277,23 @@ function hyprlandScrollCurrentId(raw, screenName) {
|
||||
return raw.monitors.find(m => m.name === screenName)?.activeWorkspace?.id ?? 1;
|
||||
}
|
||||
|
||||
function hyprlandVisibleSpecial(raw, monitorName) {
|
||||
const tracked = raw.visibleSpecials?.[monitorName];
|
||||
if (!tracked)
|
||||
return null;
|
||||
// the overlay event can land before the workspace list refresh, so the tracked name is the OSD's key
|
||||
const ws = raw.workspaces.find(ws => hyprlandSpecial(ws) && (ws.name === "special" ? "special:special" : ws.name) === tracked);
|
||||
return { id: ws?.id || tracked, idx: null, name: hyprlandSpecialDisplayName(tracked), output: monitorName, active: true, placeholder: false, special: true };
|
||||
}
|
||||
|
||||
function hyprlandActiveWorkspace(raw, screenName) {
|
||||
const monitor = raw.monitors.find(m => !screenName || m.name === screenName);
|
||||
const ws = monitor?.activeWorkspace;
|
||||
if (!ws)
|
||||
return null;
|
||||
const special = hyprlandVisibleSpecial(raw, monitor.name ?? screenName);
|
||||
if (special)
|
||||
return special;
|
||||
const name = ws.name ?? "";
|
||||
if (hyprlandSpecialName(name))
|
||||
return null;
|
||||
@@ -265,16 +308,16 @@ function hyprlandActiveWorkspace(raw, screenName) {
|
||||
}
|
||||
|
||||
function hyprlandWindowsOnWorkspace(windows, workspace, toplevels) {
|
||||
const workspaceIds = new Map();
|
||||
const workspaces = new Map();
|
||||
for (const toplevel of toplevels) {
|
||||
if (!workspaceIds.has(toplevel.wayland))
|
||||
workspaceIds.set(toplevel.wayland, toplevel.workspace?.id);
|
||||
if (!workspaces.has(toplevel.wayland))
|
||||
workspaces.set(toplevel.wayland, toplevel.workspace);
|
||||
}
|
||||
return windows.filter(win => win && workspaceIds.get(win) === workspace.id);
|
||||
return windows.filter(win => win && hyprlandWorkspaceMatches(workspaces.get(win), workspace));
|
||||
}
|
||||
|
||||
function hyprlandWorkspaceOccupied(toplevels, workspace) {
|
||||
return toplevels.some(tl => tl.workspace?.id === workspace.id);
|
||||
return toplevels.some(tl => hyprlandWorkspaceMatches(tl.workspace, workspace));
|
||||
}
|
||||
|
||||
function mangoRecord(index, tag, output) {
|
||||
|
||||
@@ -19,6 +19,7 @@ var DEFAULTS = {
|
||||
groupActiveWorkspaceApps: false,
|
||||
workspaceFollowFocus: false,
|
||||
showOccupiedWorkspacesOnly: false,
|
||||
showSpecialWorkspaces: false,
|
||||
reverseScrolling: false,
|
||||
dwlShowAllTags: false,
|
||||
workspaceActiveAppHighlightEnabled: false,
|
||||
|
||||
@@ -1940,6 +1940,39 @@ Item {
|
||||
target: "workspace-rename"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggle(name: string): string {
|
||||
if (!CompositorService.isHyprland)
|
||||
return "SCRATCHPAD_UNSUPPORTED_COMPOSITOR";
|
||||
CompositorService.toggleSpecialWorkspace(name);
|
||||
return "SCRATCHPAD_TOGGLED";
|
||||
}
|
||||
|
||||
function move(name: string): string {
|
||||
if (!CompositorService.isHyprland)
|
||||
return "SCRATCHPAD_UNSUPPORTED_COMPOSITOR";
|
||||
const active = ToplevelManager.activeToplevel;
|
||||
if (!active)
|
||||
return "SCRATCHPAD_NO_FOCUSED_WINDOW";
|
||||
CompositorService.moveWindowToSpecial(active, name || "special");
|
||||
return "SCRATCHPAD_MOVED";
|
||||
}
|
||||
|
||||
function restore(): string {
|
||||
if (!CompositorService.isHyprland)
|
||||
return "SCRATCHPAD_UNSUPPORTED_COMPOSITOR";
|
||||
const active = ToplevelManager.activeToplevel;
|
||||
if (!active)
|
||||
return "SCRATCHPAD_NO_FOCUSED_WINDOW";
|
||||
if (!CompositorService.windowScratchpadName(active))
|
||||
return "SCRATCHPAD_WINDOW_NOT_IN_SCRATCHPAD";
|
||||
CompositorService.moveWindowOutOfSpecial(active);
|
||||
return "SCRATCHPAD_RESTORED";
|
||||
}
|
||||
|
||||
target: "scratchpad"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function getFocusedWindow() {
|
||||
const active = ToplevelManager.activeToplevel;
|
||||
|
||||
@@ -16,6 +16,7 @@ DankPopout {
|
||||
readonly property string windowTitle: currentWindow?.title || ""
|
||||
readonly property string appName: appId ? Paths.getAppName(appId, DesktopEntries.heuristicLookup(Paths.moddedAppId(appId))) : I18n.tr("Unknown")
|
||||
readonly property int pid: processId
|
||||
readonly property string scratchpadName: CompositorService.windowScratchpadName(currentWindow)
|
||||
|
||||
layerNamespace: "dms:focused-window-popout"
|
||||
popupWidth: 340
|
||||
@@ -177,6 +178,79 @@ DankPopout {
|
||||
color: Theme.outlineVariant
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: root.scratchpadName !== ""
|
||||
width: parent.width
|
||||
height: BarMetrics.menuRowHeight
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "outbox"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Move out of scratchpad")
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
cornerRadius: BarMetrics.menuItemRadius
|
||||
onClicked: {
|
||||
CompositorService.moveWindowOutOfSpecial(root.currentWindow);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.scratchpadName !== "" ? [] : CompositorService.specialWorkspaceNames
|
||||
|
||||
Item {
|
||||
required property string modelData
|
||||
|
||||
width: parent.width
|
||||
height: BarMetrics.menuRowHeight
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "inbox"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData === "special" ? I18n.tr("Move to scratchpad") : I18n.tr("Move to scratchpad: %1", "%1 is the named special workspace").arg(modelData)
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
cornerRadius: BarMetrics.menuItemRadius
|
||||
onClicked: {
|
||||
CompositorService.moveWindowToSpecial(root.currentWindow, modelData);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: CompositorService.supportsWindowRules
|
||||
width: parent.width
|
||||
|
||||
@@ -503,6 +503,22 @@ BasePill {
|
||||
targetWindow.minimized = true;
|
||||
}
|
||||
});
|
||||
const scratchpad = CompositorService.windowScratchpadName(currentWindow);
|
||||
if (scratchpad)
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: "outbox",
|
||||
text: I18n.tr("Move out of scratchpad"),
|
||||
action: () => CompositorService.moveWindowOutOfSpecial(windowContextMenu.currentWindow)
|
||||
});
|
||||
for (const name of scratchpad ? [] : CompositorService.specialWorkspaceNames) {
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: "inbox",
|
||||
text: name === "special" ? I18n.tr("Move to scratchpad") : I18n.tr("Move to scratchpad: %1", "%1 is the named special workspace").arg(name),
|
||||
action: () => CompositorService.moveWindowToSpecial(windowContextMenu.currentWindow, name)
|
||||
});
|
||||
}
|
||||
items.push({
|
||||
type: "item",
|
||||
icon: "close",
|
||||
|
||||
@@ -97,7 +97,8 @@ BasePill {
|
||||
const baseList = CompositorService.workspacesForScreen(root.screenName, root.opt("workspaceFollowFocus"), {
|
||||
"occupiedOnly": root.opt("showOccupiedWorkspacesOnly"),
|
||||
"showAllTags": root.opt("dwlShowAllTags"),
|
||||
"minCount": root.opt("showWorkspacePadding") ? root.opt("workspacePaddingCount") : 0
|
||||
"minCount": root.opt("showWorkspacePadding") ? root.opt("workspacePaddingCount") : 0,
|
||||
"showSpecial": root.opt("showSpecialWorkspaces")
|
||||
});
|
||||
if (CompositorService.ephemeralWorkspaces)
|
||||
return hyprlandSlotList(baseList);
|
||||
@@ -178,7 +179,7 @@ BasePill {
|
||||
}
|
||||
|
||||
function hyprlandSlotList(raw) {
|
||||
return raw.map(ws => _hyprSlot(ws.id > 0 ? ws.id : "name:" + (ws.name ?? ""), ws));
|
||||
return raw.map(ws => _hyprSlot(ws.id > 0 ? ws.id : (ws.special ? "special:" : "name:") + (ws.name ?? ""), ws));
|
||||
}
|
||||
|
||||
// Stable placeholder instances so ScriptModel (identity-diffed) reuses padding delegates instead of recreating them on workspace churn
|
||||
@@ -242,7 +243,7 @@ BasePill {
|
||||
if (!data || data.placeholder)
|
||||
return;
|
||||
if (root.useNativeWorkspaces) {
|
||||
CompositorService.switchToWorkspace(data);
|
||||
CompositorService.switchToWorkspace(data, root.effectiveScreenName);
|
||||
return;
|
||||
}
|
||||
if (root.useExtWorkspace && typeof data.activate === "function")
|
||||
@@ -307,7 +308,8 @@ BasePill {
|
||||
}
|
||||
if (!useNativeWorkspaces)
|
||||
return;
|
||||
CompositorService.stepWorkspace(getRealWorkspaces(), root.currentWorkspace, direction);
|
||||
// specials are overlays you toggle, not positions you scroll to
|
||||
CompositorService.stepWorkspace(getRealWorkspaces().map(ws => root.recordOf(ws)).filter(ws => ws.special !== true), root.currentWorkspace, direction);
|
||||
}
|
||||
|
||||
function getWorkspaceIndexFallback(modelData, index) {
|
||||
@@ -563,7 +565,13 @@ BasePill {
|
||||
const name = record?.name;
|
||||
if (!name)
|
||||
return null;
|
||||
return SettingsData.getWorkspaceNameIcon(name);
|
||||
const custom = SettingsData.getWorkspaceNameIcon(name);
|
||||
if (custom || record.special !== true)
|
||||
return custom;
|
||||
return {
|
||||
"type": "icon",
|
||||
"value": "inbox"
|
||||
};
|
||||
}
|
||||
readonly property bool loadedHasIcon: loadedIconData !== null
|
||||
property var loadedIcons: []
|
||||
|
||||
@@ -81,69 +81,22 @@ Column {
|
||||
checked: root.page.value("showOccupiedWorkspacesOnly")
|
||||
onToggled: checked => root.page.set("showOccupiedWorkspacesOnly", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
resetStore: root.page
|
||||
resetKeys: ["showSpecialWorkspaces"]
|
||||
text: I18n.tr("Show scratchpads")
|
||||
description: I18n.tr("Special workspaces appear last; click to show or hide")
|
||||
visible: CompositorService.isHyprland
|
||||
checked: root.page.value("showSpecialWorkspaces")
|
||||
onToggled: checked => root.page.set("showSpecialWorkspaces", checked)
|
||||
}
|
||||
}
|
||||
|
||||
WorkspaceAppearanceCard {
|
||||
store: root.page.store
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: I18n.tr("Icons")
|
||||
settingKey: "workspaceIcons"
|
||||
visible: NiriService.hasNamedWorkspaces()
|
||||
|
||||
Repeater {
|
||||
model: NiriService.getNamedWorkspaces()
|
||||
|
||||
SettingsRow {
|
||||
required property string modelData
|
||||
|
||||
title: modelData
|
||||
|
||||
DankIconPicker {
|
||||
id: iconPicker
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Component.onCompleted: {
|
||||
const iconData = SettingsData.getWorkspaceNameIcon(modelData);
|
||||
if (iconData)
|
||||
setIcon(iconData.value, iconData.type);
|
||||
}
|
||||
|
||||
onIconSelected: (iconName, iconType) => {
|
||||
SettingsData.setWorkspaceNameIcon(modelData, {
|
||||
"type": iconType,
|
||||
"value": iconName
|
||||
});
|
||||
setIcon(iconName, iconType);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onWorkspaceIconsUpdated() {
|
||||
const iconData = SettingsData.getWorkspaceNameIcon(modelData);
|
||||
if (iconData) {
|
||||
iconPicker.setIcon(iconData.value, iconData.type);
|
||||
return;
|
||||
}
|
||||
iconPicker.setIcon("", "icon");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
buttonSize: Theme.iconButtonSize
|
||||
iconName: "close"
|
||||
Accessible.name: I18n.tr("Remove")
|
||||
iconSize: Theme.iconSizeMedium
|
||||
iconColor: Theme.error
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: SettingsData.removeWorkspaceNameIcon(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: I18n.tr("Advanced")
|
||||
settingKey: "workspaceAdvanced"
|
||||
|
||||
@@ -47,7 +47,7 @@ SettingsCard {
|
||||
readonly property var borderColorOptions: colorOptions(["surfaceText", "primary", "primaryContainer", "secondary", "secondaryContainer", "tertiary", "tertiaryContainer", "custom"])
|
||||
|
||||
function isFocusedAppearanceSection(section) {
|
||||
return ["workspaceAppearance", "workspaceColorMode", "workspaceOccupiedColorMode", "workspaceUnfocusedColorMode", "workspaceUrgentColorMode", "workspaceFocusedBorderEnabled", "workspaceFocusedBorderColor", "workspaceFocusedBorderThickness"].includes(section);
|
||||
return ["workspaceAppearance", "workspaceIcons", "workspaceColorMode", "workspaceOccupiedColorMode", "workspaceUnfocusedColorMode", "workspaceUrgentColorMode", "workspaceFocusedBorderEnabled", "workspaceFocusedBorderColor", "workspaceFocusedBorderThickness"].includes(section);
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
@@ -197,4 +197,65 @@ SettingsCard {
|
||||
borderCustomColorKey: "workspaceUnfocusedMonitorBorderCustomColor"
|
||||
borderThicknessKey: "workspaceUnfocusedMonitorBorderThickness"
|
||||
}
|
||||
|
||||
readonly property var namedWorkspaces: NiriService.getNamedWorkspaces().concat(CompositorService.specialWorkspaceNames)
|
||||
|
||||
SettingsRow {
|
||||
visible: root.namedWorkspaces.length > 0
|
||||
settingKey: "workspaceIcons"
|
||||
tags: ["workspace", "icon", "named", "scratchpad", "special"]
|
||||
title: I18n.tr("Icons")
|
||||
subtitle: I18n.tr("Named workspaces and scratchpads")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.namedWorkspaces
|
||||
|
||||
SettingsRow {
|
||||
required property string modelData
|
||||
|
||||
title: modelData
|
||||
|
||||
DankIconPicker {
|
||||
id: iconPicker
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Component.onCompleted: {
|
||||
const iconData = SettingsData.getWorkspaceNameIcon(modelData);
|
||||
if (iconData)
|
||||
setIcon(iconData.value, iconData.type);
|
||||
}
|
||||
|
||||
onIconSelected: (iconName, iconType) => {
|
||||
SettingsData.setWorkspaceNameIcon(modelData, {
|
||||
"type": iconType,
|
||||
"value": iconName
|
||||
});
|
||||
setIcon(iconName, iconType);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onWorkspaceIconsUpdated() {
|
||||
const iconData = SettingsData.getWorkspaceNameIcon(modelData);
|
||||
if (iconData) {
|
||||
iconPicker.setIcon(iconData.value, iconData.type);
|
||||
return;
|
||||
}
|
||||
iconPicker.setIcon("", "icon");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
buttonSize: Theme.iconButtonSize
|
||||
iconName: "close"
|
||||
Accessible.name: I18n.tr("Remove")
|
||||
iconSize: Theme.iconSizeMedium
|
||||
iconColor: Theme.error
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: SettingsData.removeWorkspaceNameIcon(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ Scope {
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [root]
|
||||
windows: overviewLoader.item?.windowMenuWindow ? [root, overviewLoader.item.windowMenuWindow] : [root]
|
||||
active: false
|
||||
property bool hasBeenActivated: false
|
||||
onActiveChanged: {
|
||||
|
||||
@@ -13,6 +13,37 @@ Item {
|
||||
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 alias windowMenuWindow: windowMenu.contextWindow
|
||||
|
||||
function openWindowMenu(item, x, y) {
|
||||
const address = item.windowData?.address;
|
||||
if (!address || CompositorService.specialWorkspaceNames.length === 0)
|
||||
return;
|
||||
windowMenu.targetWindow = address;
|
||||
const pos = item.mapToGlobal(x, y);
|
||||
windowMenu.open(panelWindow.screen, pos.x - (panelWindow.screen?.x || 0), pos.y - (panelWindow.screen?.y || 0), false);
|
||||
}
|
||||
|
||||
// scratchpad windows are not in the grid, so the menu only ever offers a move in
|
||||
DankContextMenu {
|
||||
id: windowMenu
|
||||
|
||||
property string targetWindow: ""
|
||||
|
||||
layerNamespace: "dms:overview-window-context-menu"
|
||||
menuItems: CompositorService.specialWorkspaceNames.map(name => ({
|
||||
type: "item",
|
||||
icon: "inbox",
|
||||
text: name === "special" ? I18n.tr("Move to scratchpad") : I18n.tr("Move to scratchpad: %1", "%1 is the named special workspace").arg(name),
|
||||
action: () => {
|
||||
CompositorService.moveWindowToSpecial(windowMenu.targetWindow, name);
|
||||
Qt.callLater(() => {
|
||||
Hyprland.refreshToplevels();
|
||||
Hyprland.refreshWorkspaces();
|
||||
});
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
readonly property var allWorkspaces: Hyprland.workspaces?.values || []
|
||||
readonly property var allWorkspaceIds: {
|
||||
@@ -456,6 +487,13 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// on top of dragArea so a right press never starts a drag
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: mouse => root.openWindowMenu(window, mouse.x, mouse.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -831,6 +831,73 @@ Singleton {
|
||||
return true;
|
||||
}
|
||||
|
||||
readonly property var specialWorkspaceNames: {
|
||||
if (!isHyprland)
|
||||
return [];
|
||||
const names = ["special"];
|
||||
for (const ws of Hyprland.workspaces?.values || []) {
|
||||
if (!WorkspaceModel.hyprlandSpecial(ws))
|
||||
continue;
|
||||
const name = WorkspaceModel.hyprlandSpecialDisplayName(ws.name ?? "");
|
||||
if (!names.includes(name))
|
||||
names.push(name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function hyprlandAddressFor(window) {
|
||||
return isHyprland ? (_hyprlandToplevelFor(window)?.address ?? "") : "";
|
||||
}
|
||||
|
||||
// IPC callers pass Hyprland's own names, prefixed or not
|
||||
function _scratchpadName(name) {
|
||||
const bare = WorkspaceModel.hyprlandSpecialDisplayName(String(name ?? ""));
|
||||
return !bare || bare === "special" ? "special" : bare;
|
||||
}
|
||||
|
||||
function toggleSpecialWorkspace(name) {
|
||||
if (!isHyprland)
|
||||
return;
|
||||
const bare = _scratchpadName(name);
|
||||
HyprlandService.toggleSpecial(bare === "special" ? "" : bare);
|
||||
}
|
||||
|
||||
function _hyprlandToplevelFor(window) {
|
||||
if (!window)
|
||||
return null;
|
||||
const byAddress = typeof window === "string";
|
||||
for (const t of Hyprland.toplevels?.values || []) {
|
||||
if (byAddress ? t.address === window : t.wayland === window)
|
||||
return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function windowScratchpadName(window) {
|
||||
if (!isHyprland)
|
||||
return "";
|
||||
const t = _hyprlandToplevelFor(window);
|
||||
const ws = String(t?.lastIpcObject?.workspace?.name || t?.workspace?.name || "");
|
||||
return WorkspaceModel.hyprlandSpecialName(ws) ? WorkspaceModel.hyprlandSpecialDisplayName(ws) : "";
|
||||
}
|
||||
|
||||
// "+0" is Hyprland's own way out but resolves against the focused monitor, so prefer the window's monitor when known
|
||||
function moveWindowOutOfSpecial(window) {
|
||||
const t = _hyprlandToplevelFor(window);
|
||||
if (!t?.address)
|
||||
return;
|
||||
const target = t.monitor?.activeWorkspace?.id;
|
||||
HyprlandService.moveToWorkspace(target > 0 ? target : "+0", t.address, true);
|
||||
}
|
||||
|
||||
function moveWindowToSpecial(window, name) {
|
||||
const address = hyprlandAddressFor(window);
|
||||
if (!address)
|
||||
return;
|
||||
const bare = _scratchpadName(name);
|
||||
HyprlandService.moveToWorkspace(bare === "special" ? "special" : "special:" + bare, address, false);
|
||||
}
|
||||
|
||||
function hyprlandVisibleSpecialWorkspaceOnScreen(screenOrName) {
|
||||
const screenName = _screenName(screenOrName);
|
||||
if (!isHyprland || !screenName)
|
||||
@@ -1520,6 +1587,9 @@ Singleton {
|
||||
},
|
||||
get workspaceRules() {
|
||||
return HyprlandService.workspaceRules;
|
||||
},
|
||||
get visibleSpecials() {
|
||||
return root.hyprlandVisibleSpecialWorkspaces;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1581,7 +1651,7 @@ Singleton {
|
||||
case "niri":
|
||||
return WorkspaceModel.niriWorkspacesForScreen(_niriWorkspaceState(), screenName, followFocus, options.occupiedOnly, _workspaceRecords);
|
||||
case "hyprland":
|
||||
return WorkspaceModel.hyprlandWorkspacesForScreen(_hyprlandWorkspaceState(), screenName, followFocus, options.occupiedOnly, options.minCount);
|
||||
return WorkspaceModel.hyprlandWorkspacesForScreen(_hyprlandWorkspaceState(), screenName, followFocus, options.occupiedOnly, options.minCount, options.showSpecial);
|
||||
case "mango":
|
||||
{
|
||||
const name = _followedScreen(screenName, followFocus);
|
||||
@@ -1627,6 +1697,8 @@ Singleton {
|
||||
case "mango":
|
||||
case "aqueous":
|
||||
return record.active === true;
|
||||
case "hyprland":
|
||||
return record.special === true ? record.active === true : record.id === currentKey;
|
||||
default:
|
||||
return _workspaceKey(record) === currentKey;
|
||||
}
|
||||
@@ -1699,6 +1771,8 @@ Singleton {
|
||||
case "scroll":
|
||||
case "miracle":
|
||||
return WorkspaceModel.i3WorkspaceFocused(_i3WorkspaceState(), record);
|
||||
case "hyprland":
|
||||
return isCurrentWorkspace(record, currentKey);
|
||||
default:
|
||||
return record.id === currentKey;
|
||||
}
|
||||
@@ -1725,7 +1799,7 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function switchToWorkspace(record) {
|
||||
function switchToWorkspace(record, screenName) {
|
||||
if (!record || record.placeholder)
|
||||
return;
|
||||
switch (compositor) {
|
||||
@@ -1733,6 +1807,13 @@ Singleton {
|
||||
NiriService.switchToWorkspace(record.id);
|
||||
return;
|
||||
case "hyprland":
|
||||
if (record.special === true) {
|
||||
// togglespecialworkspace acts on the focused monitor, so a pill on another monitor's bar focuses that monitor first
|
||||
if (screenName && screenName !== Hyprland.focusedMonitor?.name)
|
||||
HyprlandService.focusMonitor(screenName);
|
||||
HyprlandService.toggleSpecial(record.name === "special" ? "" : record.name);
|
||||
return;
|
||||
}
|
||||
HyprlandService.focusWorkspace(record.id > 0 ? record.id : "name:" + (record.name ?? ""));
|
||||
return;
|
||||
case "mango":
|
||||
|
||||
@@ -658,6 +658,14 @@ hl.layer_rule({
|
||||
}
|
||||
}
|
||||
|
||||
function focusMonitor(monitor) {
|
||||
if (luaConfigActive) {
|
||||
Hyprland.dispatch(`hl.dsp.focus({ monitor = ${luaString(monitor)} })`);
|
||||
} else {
|
||||
Hyprland.dispatch(`focusmonitor ${monitor}`);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSpecial(specialName) {
|
||||
if (luaConfigActive) {
|
||||
Hyprland.dispatch(`hl.dsp.workspace.toggle_special(${luaString(specialName)})`);
|
||||
|
||||
@@ -28,7 +28,7 @@ function hyprland(scenario) {
|
||||
const workspace = id => workspaces.find(ws => ws.id === id) ?? null;
|
||||
monitors.forEach((m, i) => m.activeWorkspace = workspace(s.monitors[i].activeWorkspaceId));
|
||||
const toplevels = s.toplevels.map(tl => ({ workspace: workspace(tl.workspace), wayland: tl.wayland ? { address: tl.address } : null }));
|
||||
return { workspaces, monitors, focusedWorkspace: workspace(s.focusedWorkspaceId), toplevels, windows: toplevels.filter(tl => tl.wayland).map(tl => tl.wayland) };
|
||||
return { workspaces, monitors, focusedWorkspace: workspace(s.focusedWorkspaceId), toplevels, windows: toplevels.filter(tl => tl.wayland).map(tl => tl.wayland), visibleSpecials: {} };
|
||||
}
|
||||
|
||||
function i3(scenario) {
|
||||
@@ -186,6 +186,45 @@ test("mango padding shows the first tags as real targets and caps at the tag cou
|
||||
assert.deepEqual(pick(model.mangoWorkspacesForScreen(dp1, "DP-1", true, 2), "id"), [0, 1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
test("hyprland special workspaces are opt-in, listed last on their monitor and active while overlaid", () => {
|
||||
const raw = hyprland("twoScreens");
|
||||
assert.deepEqual(pick(model.hyprlandWorkspacesForScreen(raw, "DP-1", false, false, 0, false), "id"), [1, 3]);
|
||||
const shown = model.hyprlandWorkspacesForScreen(raw, "DP-1", false, false, 0, true);
|
||||
assert.deepEqual(pick(shown, "name"), ["1", "3", "scratch", "special"]);
|
||||
assert.deepEqual(plain(shown[2]), { id: -98, idx: null, name: "scratch", output: "DP-1", active: false, placeholder: false, urgent: false, special: true });
|
||||
assert.deepEqual(plain(shown[3]), { id: null, idx: null, name: "special", output: "DP-1", active: false, placeholder: false, urgent: false, special: true });
|
||||
assert.deepEqual(pick(model.hyprlandWorkspacesForScreen(raw, "HDMI-A-1", false, false, 0, true), "name"), ["2", "4", "chat", "special"]);
|
||||
assert.deepEqual(pick(model.hyprlandWorkspacesForScreen(raw, "HDMI-A-1", true, false, 0, true), "name"), ["1", "2", "3", "4", "chat", "scratch", "special"]);
|
||||
assert.deepEqual(pick(model.hyprlandWorkspacesForScreen(raw, "DP-1", false, true, 0, true), "name"), ["1", "3", "special"]);
|
||||
raw.visibleSpecials = { "DP-1": "special:scratch" };
|
||||
assert.deepEqual(pick(model.hyprlandWorkspacesForScreen(raw, "DP-1", false, true, 0, true), "active"), [false, true, true, false]);
|
||||
raw.visibleSpecials = { "HDMI-A-1": "special:special" };
|
||||
assert.deepEqual(pick(model.hyprlandWorkspacesForScreen(raw, "HDMI-A-1", false, false, 0, true), "active"), [true, false, false, true]);
|
||||
assert.equal(model.hyprlandWorkspacesForScreen(raw, "DP-1", true, false, 0, true).at(-1).active, true);
|
||||
assert.deepEqual(plain(model.hyprlandActiveWorkspace(raw, "HDMI-A-1")), { id: "special:special", idx: null, name: "special", output: "HDMI-A-1", active: true, placeholder: false, special: true });
|
||||
assert.deepEqual(pick([model.hyprlandActiveWorkspace(raw, "DP-1")], "id"), [3]);
|
||||
raw.visibleSpecials = { "DP-1": "special:scratch" };
|
||||
assert.deepEqual(plain(model.hyprlandActiveWorkspace(raw, "DP-1")), { id: -98, idx: null, name: "scratch", output: "DP-1", active: true, placeholder: false, special: true });
|
||||
});
|
||||
|
||||
test("hyprland specials without ids match windows by name", () => {
|
||||
const raw = hyprland("twoScreens");
|
||||
const scratch = raw.workspaces.find(ws => ws.name === "special:scratch");
|
||||
scratch.id = 0;
|
||||
raw.workspaces.push({ id: 0, name: "special:special", monitor: raw.monitors[0], lastIpcObject: { monitor: "DP-1" }, active: false, urgent: false });
|
||||
const win = { address: "0xd" };
|
||||
raw.toplevels.push({ workspace: scratch, wayland: win });
|
||||
raw.windows.push(win);
|
||||
const shown = model.hyprlandWorkspacesForScreen(raw, "DP-1", false, true, 0, true);
|
||||
assert.deepEqual(pick(shown, "name"), ["1", "3", "scratch", "special"]);
|
||||
assert.equal(model.hyprlandWorkspaceOccupied(raw.toplevels, shown[2]), true);
|
||||
assert.equal(model.hyprlandWorkspaceOccupied(raw.toplevels, shown[3]), false);
|
||||
assert.deepEqual(pick(model.hyprlandWindowsOnWorkspace(raw.windows, shown[2], raw.toplevels), "address"), ["0xd"]);
|
||||
assert.deepEqual(model.hyprlandWindowsOnWorkspace(raw.windows, shown[3], raw.toplevels), []);
|
||||
raw.visibleSpecials = { "DP-1": "special:scratch" };
|
||||
assert.equal(model.hyprlandActiveWorkspace(raw, "DP-1").id, "special:scratch");
|
||||
});
|
||||
|
||||
test("hyprland scroll targets use ipc monitors, numbered ids and ignore follow focus for current", () => {
|
||||
const raw = hyprland("twoScreens");
|
||||
assert.deepEqual(pick(model.hyprlandScrollWorkspaces(raw, "HDMI-A-1", false), "id"), [2, 4]);
|
||||
|
||||
@@ -8337,14 +8337,6 @@
|
||||
"runtimeType": "barWidgetOption",
|
||||
"optionFile": "SystemTrayOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:WorkspaceSwitcherOptions.qml:8",
|
||||
"label": "Icons",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
"runtimeType": "barWidgetOption",
|
||||
"optionFile": "WorkspaceSwitcherOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:WorkspaceSwitcherOptions.qml:2",
|
||||
"label": "Index numbers",
|
||||
@@ -8625,6 +8617,14 @@
|
||||
"runtimeType": "barWidgetOption",
|
||||
"optionFile": "BatteryOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:WorkspaceSwitcherOptions.qml:8",
|
||||
"label": "Show scratchpads",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
"runtimeType": "barWidgetOption",
|
||||
"optionFile": "WorkspaceSwitcherOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:SystemMonitorOptions.qml:2",
|
||||
"label": "Show swap",
|
||||
|
||||
Reference in new issue
Block a user