feat(Dank Island): add new island bar instance

This commit is contained in:
purian23
2026-08-22 22:06:13 -04:00
parent f89b21a0ef
commit 4447d77ba6
101 changed files with 10733 additions and 806 deletions
+100
View File
@@ -517,6 +517,90 @@ dms ipc call bar hide
dms ipc call bar status
```
### Dank Island instances
One bar instance may be designated as Dank Island (Settings → Island → Island instance)
## Target: `island`
Dank Island activity surface. Requires a bar instance designated as the island in Settings (Island → Island instance) and enabled. Commands target the focused screen.
### Activities
- `home` — compact clock (default when the activity argument is unrecognized)
- `media` — now-playing (unavailable when no media session is active)
- `launcher` — native island launcher
- `controlcenter` — Control Center (`control-center` and `cc` are aliases)
- `wallpaper` — wallpaper browser
- `weather` — weather (unavailable when weather is disabled)
- `notificationcenter` — notification center (`notifications`, `notification-center`, and `nc` are aliases)
### Functions
**`open <activity>`**
- Expand the island into the requested activity
- Parameters: `activity` - Activity id (see above)
- Returns: `DANK_ISLAND_OPEN: <activity>\t<screen>`, `DANK_ISLAND_ACTIVITY_UNAVAILABLE: <activity>`, or `DANK_ISLAND_UNAVAILABLE`
**`toggle <activity>`**
- Collapse if the island is expanded; otherwise expand into the requested activity
- Parameters: `activity` - Activity id to open when collapsed
- Returns: `DANK_ISLAND_OPEN: <activity>\t<screen>`, `DANK_ISLAND_CLOSED: <screen>`, `DANK_ISLAND_ACTIVITY_UNAVAILABLE: <activity>`, or `DANK_ISLAND_UNAVAILABLE`
**`show <activity>`**
- Switch the compact island face without expanding
- Parameters: `activity` - Activity id
- Returns: `DANK_ISLAND_SHOW: <activity>\t<screen>`, `DANK_ISLAND_ACTIVITY_UNAVAILABLE: <activity>`, or `DANK_ISLAND_UNAVAILABLE`
**`close`**
- Collapse the island back to its compact face
- Returns: `DANK_ISLAND_CLOSED: <screen>` or `DANK_ISLAND_UNAVAILABLE`
**`cycle`**
- Cycle island activities (`home` → `media` when available → `launcher` when Launcher → Default Opens is Island → `controlcenter` → `notificationcenter`)
- Returns: `DANK_ISLAND_ACTIVITY: <activity>\t<screen>` or `DANK_ISLAND_UNAVAILABLE`
**`status`**
- JSON snapshot of the island on the focused screen
- Returns: Object with `available`, `enabled`, `screen`, `activity`, `expanded`, `mediaAvailable`, `launcherAvailable`, `controlCenterAvailable`, `wallpaperAvailable`, `weatherAvailable`, `notificationCenterAvailable`, `launcherInputFocused`, `launcherResultCount`, and `compactHeight`. When no host exists: `{"available":false,"enabled":true,"launcherAvailable":false}`
**`notifications`**
- Toggle the notification center as an island activity
- Returns: `DANK_ISLAND_OPEN: notificationcenter\t<screen>`, `DANK_ISLAND_CLOSED: <screen>`, or `DANK_ISLAND_UNAVAILABLE`
**`openOn <activity> <screen>` / `toggleOn <activity> <screen>` / `showOn <activity> <screen>` / `closeOn <screen>` / `cycleOn <screen>` / `statusOn <screen>` / `notificationsOn <screen>`**
- Same actions on a specific monitor (e.g. `DP-1`)
- A screen name with no island on it returns `DANK_ISLAND_UNAVAILABLE` rather than falling back to another display
### Shared IPC routing
When Dank Island is the sole top chrome on the focused display, these existing targets open island activities instead of their popouts:
- `control-center` `open` / `toggle` / `hide` / `status`
- `notifications` `open` / `toggle` / `close` (the notification modal falls back when the island does not own the screen)
- `dash` `open` / `toggle` `overview` (or no tab), `media`, `wallpaper`, or `weather`, and `dash close` while those activities are open
- `dankdash wallpaper` (deprecated; same wallpaper path)
`spotlight` and `launcher` open the island launcher when **Launcher → Default Opens** is set to Island, and fall back to Spotlight if the focused screen has no island.
### Examples
```bash
dms ipc call island toggle home
dms ipc call island open home
dms ipc call island open media
dms ipc call island open launcher
dms ipc call island open controlcenter
dms ipc call island open wallpaper
dms ipc call island open weather
dms ipc call island open notifications
dms ipc call island notifications
dms ipc call island show home
dms ipc call island toggleOn media DP-1
dms ipc call island cycle
dms ipc call island close
dms ipc call island status
```
## Target: `systemupdater`
System updater widget control and background update checks.
@@ -610,6 +694,8 @@ Application launcher modal control.
- Parameters: `query` - Search text to pre-fill in the search box (only used when opening)
- Returns: Success confirmation
When **Launcher → Default Opens** is set to Island, `spotlight` and `launcher` open the island launcher on the focused screen (Spotlight is used if that screen has no Dank Island). Direct island control is `island open launcher`.
### Target: `clipboard`
Clipboard history modal control.
@@ -670,6 +756,8 @@ Control Center popout containing network, bluetooth, audio, power, and other qui
- `close` - Hide the control center
- `toggle` - Toggle control center visibility
When Dank Island is enabled and is the sole top chrome on the focused display, these calls open the native Control Center island activity instead of the popout. Use `island` to target a specific screen or activity.
**Examples**
```bash
dms ipc call control-center toggle
@@ -705,6 +793,8 @@ Dashboard popup control with tab selection for overview, media, and weather info
- Parameters: `tab` - Tab to open when showing: "", "overview", "media", or "weather"
- Returns: Success/failure message
On displays where Dank Island is the sole top chrome, `open`/`toggle` with `overview` (or no tab), `media`, `wallpaper`, or `weather` route to the matching island activity. `close` collapses those island activities when they are open.
### Target: `dankdash`
DankDash wallpaper browser control.
@@ -785,6 +875,12 @@ Displays a live overview of all workspaces across all monitors with window previ
# Open application launcher
dms ipc call spotlight toggle
# Toggle Dank Island on the focused screen
dms ipc call island toggle home
dms ipc call island open media
dms ipc call island open controlcenter
dms ipc call island cycle
# Open spotlight with pre-filled search
dms ipc call spotlight openQuery browser
dms ipc call spotlight toggleQuery "!"
@@ -855,6 +951,8 @@ These IPC commands are designed to be used with window manager keybindings.
```kdl
binds {
Mod+Space { spawn "qs" "-c" "dms" "ipc" "call" "spotlight" "toggle"; }
Mod+I { spawn "qs" "-c" "dms" "ipc" "call" "island" "toggle" "home"; }
Mod+Shift+I { spawn "qs" "-c" "dms" "ipc" "call" "island" "open" "controlcenter"; }
Mod+V { spawn "qs" "-c" "dms" "ipc" "call" "clipboard" "toggle"; }
Mod+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "toggle"; }
Mod+Shift+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "expand"; }
@@ -868,6 +966,8 @@ binds {
**Example Hyprland configuration:**
```conf
bind = SUPER, Space, exec, qs -c dms ipc call spotlight toggle
bind = SUPER, I, exec, qs -c dms ipc call island toggle home
bind = SUPER SHIFT, I, exec, qs -c dms ipc call island open controlcenter
bind = SUPER, V, exec, qs -c dms ipc call clipboard toggle
bind = SUPER, P, exec, qs -c dms ipc call notepad toggle
bind = SUPER SHIFT, P, exec, qs -c dms ipc call notepad expand
+9
View File
@@ -11,6 +11,15 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call spotlight toggle", label: "Default Launcher: Toggle" },
{ id: "spawn dms ipc call spotlight open", label: "Default Launcher: Open" },
{ id: "spawn dms ipc call spotlight close", label: "Default Launcher: Close" },
{ id: "spawn dms ipc call island toggle home", label: "DankIsland: Toggle" },
{ id: "spawn dms ipc call island open home", label: "DankIsland: Open Clock" },
{ id: "spawn dms ipc call island open media", label: "DankIsland: Open Media" },
{ id: "spawn dms ipc call island open launcher", label: "DankIsland: Open Launcher" },
{ id: "spawn dms ipc call island open controlcenter", label: "DankIsland: Open Control Center" },
{ id: "spawn dms ipc call island open wallpaper", label: "DankIsland: Open Wallpapers" },
{ id: "spawn dms ipc call island open weather", label: "DankIsland: Open Weather" },
{ id: "spawn dms ipc call island cycle", label: "DankIsland: Cycle Activity" },
{ id: "spawn dms ipc call island close", label: "DankIsland: Close" },
{ id: "spawn dms ipc call defaultApp browser", label: "Default Web Browser: Open" },
{ id: "spawn dms ipc call defaultApp fileManager", label: "Default File Manager: Open" },
{ id: "spawn dms ipc call defaultApp mail", label: "Default Mail: Open" },
+148 -14
View File
@@ -970,6 +970,43 @@ Singleton {
property var notificationRules: []
property bool notificationDndAllowCritical: true
property bool notificationFocusedMonitor: false
// Names the bar config rendered as Dank Island instead of a DankBar. Empty = no island.
property string dankIslandBarId: ""
readonly property var islandBarConfig: dankIslandBarId ? (barConfigs.find(cfg => cfg.id === dankIslandBarId) || null) : null
readonly property bool dankIslandEnabled: !!islandBarConfig && (islandBarConfig.enabled ?? false)
readonly property int dankIslandPosition: islandBarConfig?.position ?? SettingsData.Position.Top
readonly property string dankIslandEdge: dankIslandPosition === SettingsData.Position.Bottom ? "bottom" : "top"
readonly property int dankIslandReservedStripHeight: {
const reserve = Math.max(36, Math.min(128, dankIslandReserveHeight));
const compact = Math.max(36, Math.min(72, dankIslandCompactHeight));
const gap = Math.max(0, Math.min(48, dankIslandOuterGap));
return Math.max(reserve, gap + compact);
}
property bool dankIslandFloating: false
property bool dankIslandUseOverlayLayer: false
property int dankIslandReserveHeight: 40
property int dankIslandCompactHeight: 38
property int dankIslandOuterGap: 4
property int dankIslandHorizontalOffset: 0
property string dankIslandInteractionMode: "hybrid"
property int dankIslandHoverOpenDelay: 220
property int dankIslandHoverCloseDelay: 400
property string dankIslandPalette: "default"
property bool dankIslandHighContrast: false
property bool dankIslandMediaClockVisible: true
property bool dankIslandNotificationExpand: false
property string dankIslandHomeMediaSlot: "left"
property string dankIslandHomeStatusSlot: "right"
property string dankIslandHomeWeatherSlot: "hidden"
property bool dankIslandHomeCompactTight: false
property string dankIslandBatteryStyle: "solid"
property bool dankIslandSatellitesEnabled: true
property string dankIslandSatellitePosition: "edges"
property int dankIslandSatelliteGap: 12
property bool dankIslandReducedMotion: false
property real dankIslandSpringStiffness: 560
property real dankIslandSpringDamping: 37
property real dankIslandSpringMass: 1
property bool osdAlwaysShowValue: false
property int osdPosition: SettingsData.Position.BottomCenter
@@ -1585,7 +1622,13 @@ Singleton {
})
function set(key, value) {
if (key === "dankIslandInteractionMode" && value !== "click" && value !== "hybrid")
value = "hybrid";
Spec.set(root, key, value, saveSettings, _hooks);
if (key === "dankIslandBarId" && value && frameEnabled)
set("frameEnabled", false);
if (key === "frameEnabled" && value && dankIslandBarId)
set("dankIslandBarId", "");
}
function loadSettings() {
@@ -1639,6 +1682,10 @@ Singleton {
Store.parse(root, obj);
// set() enforces this pair, but a hand-edited settings.json bypasses set() entirely.
if (frameEnabled && dankIslandBarId)
dankIslandBarId = "";
if (obj?.directionalAnimationMode === 3 && frameMode !== "connected")
frameMode = "connected";
@@ -2044,8 +2091,14 @@ Singleton {
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
}
function getEffectiveDateFormat(fallback) {
if (clockDateFormat && clockDateFormat.length > 0)
return clockDateFormat;
return fallback || "ddd d";
}
function initializeListModels() {
const defaultBar = barConfigs[0] || getBarConfig("default");
const defaultBar = getPrimaryBarConfig();
if (defaultBar) {
Lists.init(leftWidgetsModel, centerWidgetsModel, rightWidgetsModel, defaultBar.leftWidgets, defaultBar.centerWidgets, defaultBar.rightWidgets);
}
@@ -2059,7 +2112,7 @@ Singleton {
function getPopupTriggerPosition(pos, screen, barThickness, widgetWidth, barSpacing, barPosition, barConfig) {
const relativeX = pos.x;
const relativeY = pos.y;
const defaultBar = barConfigs[0] || getBarConfig("default");
const defaultBar = getPrimaryBarConfig();
const spacing = barSpacing !== undefined ? barSpacing : (defaultBar?.spacing ?? 4);
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
@@ -2119,7 +2172,7 @@ Singleton {
}
const enabledBars = getEnabledBarConfigs();
const defaultBar = barConfigs[0] || getBarConfig("default");
const defaultBar = getPrimaryBarConfig();
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
let topBar = 0;
let bottomBar = 0;
@@ -2162,6 +2215,16 @@ Singleton {
}
}
// The island is not a bar, but it is chrome: popouts still have to clear its strip.
if (!isIslandBarConfig(barConfig)) {
const islandTop = dankIslandEdgeOffset(screen, "top");
const islandBottom = dankIslandEdgeOffset(screen, "bottom");
if (islandTop > 0)
topBar = Math.max(topBar, islandTop);
if (islandBottom > 0)
bottomBar = Math.max(bottomBar, islandBottom);
}
return {
"topBar": topBar,
"bottomBar": bottomBar,
@@ -2181,7 +2244,7 @@ Singleton {
};
}
const defaultBar = barConfigs[0] || getBarConfig("default");
const defaultBar = getPrimaryBarConfig();
const wingRadius = (defaultBar?.gothCornerRadiusOverride ?? false) ? (defaultBar?.gothCornerRadiusValue ?? 12) : Theme.cornerRadius;
const wingSize = (defaultBar?.gothCornersEnabled ?? false) ? Math.max(0, wingRadius) : 0;
const screenWidth = screen.width;
@@ -2306,7 +2369,7 @@ Singleton {
}
function setBarIpcReveal(barId, revealed) {
if (!barId)
if (!barId || barId === dankIslandBarId)
return;
const nextRevealed = !!revealed;
if (!!barIpcRevealStates[barId] === nextRevealed)
@@ -2340,6 +2403,8 @@ Singleton {
const index = configs.findIndex(cfg => cfg.id === barId);
if (index === -1)
return;
if (isIslandBarConfig(configs[index]) && updates.position !== undefined && updates.position !== SettingsData.Position.Top && updates.position !== SettingsData.Position.Bottom)
delete updates.position;
const positionChanged = updates.position !== undefined && configs[index].position !== updates.position;
if (updates.autoHide === false || updates.visible === false)
setBarIpcReveal(barId, false);
@@ -2358,6 +2423,8 @@ Singleton {
return;
const configs = barConfigs.filter(cfg => cfg.id !== barId);
barConfigs = configs;
if (dankIslandBarId === barId)
dankIslandBarId = "";
if (connectedFrameBarStyleBackups?.[barId] !== undefined) {
const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {}));
delete nextBackups[barId];
@@ -2367,8 +2434,22 @@ Singleton {
updateBarConfigs();
}
// Bar-kind instances only. The island reserves its own edge via dankIslandOwnsEdge.
function getEnabledBarConfigs() {
return barConfigs.filter(cfg => cfg.enabled);
return barConfigs.filter(cfg => cfg.enabled && !isIslandBarConfig(cfg));
}
function getBarKindConfigs() {
return barConfigs.filter(cfg => !isIslandBarConfig(cfg));
}
// Style and popup fallbacks never treat the island as "the bar".
function getPrimaryBarConfig() {
const bars = getBarKindConfigs();
if (bars.length > 0)
return bars[0];
const fallback = getBarConfig("default");
return isIslandBarConfig(fallback) ? null : fallback;
}
function _sideToPosition(side) {
@@ -2385,6 +2466,20 @@ Singleton {
return -1;
}
function positionToSide(pos) {
switch (pos) {
case SettingsData.Position.Top:
return "top";
case SettingsData.Position.Bottom:
return "bottom";
case SettingsData.Position.Left:
return "left";
case SettingsData.Position.Right:
return "right";
}
return "";
}
// Check if a bar occupies the specified screen edge
function barOccupiesSide(screen, side) {
if (!screen)
@@ -2507,6 +2602,35 @@ Singleton {
return (bc?.showOnLastDisplay ?? false) && Quickshell.screens.length === 1;
}
function isIslandBarConfig(bc) {
return !!bc && !!dankIslandBarId && bc.id === dankIslandBarId;
}
function getIslandScreens() {
const cfg = islandBarConfig;
if (!cfg || !(cfg.enabled ?? false))
return [];
return Quickshell.screens.filter(screen => barConfigCoversScreen(cfg, screen));
}
function dankIslandCoversScreen(screen) {
const cfg = islandBarConfig;
return !!screen && !!cfg && (cfg.enabled ?? false) && barConfigCoversScreen(cfg, screen);
}
function dankIslandOwnsEdge(screen, edge) {
return dankIslandCoversScreen(screen) && dankIslandEdge === edge;
}
// Painted strip thickness on `edge`, including when floating drops the exclusive zone.
function dankIslandEdgeOffset(screen, edge) {
return dankIslandOwnsEdge(screen, edge) ? dankIslandReservedStripHeight : 0;
}
function dankIslandIsSoleBarForScreen(screen) {
return dankIslandCoversScreen(screen) && getActiveBarEdgesForScreen(screen).length === 0;
}
function getActiveBarEdgesForScreen(screen) {
if (!screen)
return [];
@@ -2517,6 +2641,10 @@ Singleton {
continue;
if (!barConfigCoversScreen(bc, screen))
continue;
if (isIslandBarConfig(bc))
continue;
if (dankIslandOwnsEdge(screen, positionToSide(bc.position ?? SettingsData.Position.Top)))
continue;
switch (bc.position ?? 0) {
case SettingsData.Position.Top:
edges.push("top");
@@ -2545,6 +2673,10 @@ Singleton {
continue;
if (!barConfigCoversScreen(bc, screen))
continue;
if (isIslandBarConfig(bc))
continue;
if (dankIslandOwnsEdge(screen, positionToSide(bc.position ?? SettingsData.Position.Top)))
continue;
switch (bc.position ?? 0) {
case SettingsData.Position.Top:
edges.push("top");
@@ -2727,8 +2859,9 @@ Singleton {
function setShowDock(enabled) {
showDock = enabled;
const defaultBar = barConfigs[0] || getBarConfig("default");
const barPos = defaultBar?.position ?? SettingsData.Position.Top;
const defaultBar = getPrimaryBarConfig();
// -1 matches no Position, so island-only setups have no dock conflict.
const barPos = defaultBar ? (defaultBar.position ?? SettingsData.Position.Top) : -1;
if (enabled && dockPosition === barPos) {
if (barPos === SettingsData.Position.Top) {
setDockPosition(SettingsData.Position.Bottom);
@@ -2752,8 +2885,9 @@ Singleton {
function setDockPosition(position) {
dockPosition = position;
const defaultBar = barConfigs[0] || getBarConfig("default");
const barPos = defaultBar?.position ?? SettingsData.Position.Top;
const defaultBar = getPrimaryBarConfig();
// -1 matches no Position, so island-only setups have no dock conflict.
const barPos = defaultBar ? (defaultBar.position ?? SettingsData.Position.Top) : -1;
if (position === SettingsData.Position.Bottom && barPos === SettingsData.Position.Bottom && showDock) {
setDankBarPosition(SettingsData.Position.Top);
}
@@ -2771,7 +2905,7 @@ Singleton {
}
function setDankBarPosition(position) {
const defaultBar = barConfigs[0] || getBarConfig("default");
const defaultBar = getPrimaryBarConfig();
if (!defaultBar)
return;
if (position === SettingsData.Position.Bottom && dockPosition === SettingsData.Position.Bottom && showDock) {
@@ -2796,7 +2930,7 @@ Singleton {
}
function setDankBarLeftWidgets(order) {
const defaultBar = barConfigs[0] || getBarConfig("default");
const defaultBar = getPrimaryBarConfig();
if (defaultBar) {
updateBarConfig(defaultBar.id, {
"leftWidgets": order
@@ -2806,7 +2940,7 @@ Singleton {
}
function setDankBarCenterWidgets(order) {
const defaultBar = barConfigs[0] || getBarConfig("default");
const defaultBar = getPrimaryBarConfig();
if (defaultBar) {
updateBarConfig(defaultBar.id, {
"centerWidgets": order
@@ -2816,7 +2950,7 @@ Singleton {
}
function setDankBarRightWidgets(order) {
const defaultBar = barConfigs[0] || getBarConfig("default");
const defaultBar = getPrimaryBarConfig();
if (defaultBar) {
updateBarConfig(defaultBar.id, {
"rightWidgets": order
+3 -1
View File
@@ -22,7 +22,7 @@ Singleton {
readonly property real popupDistance: {
if (typeof SettingsData === "undefined")
return 4;
const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
const defaultBar = SettingsData.getPrimaryBarConfig();
if (!defaultBar)
return 4;
const useAuto = defaultBar.popupGapsAuto ?? true;
@@ -390,6 +390,8 @@ Singleton {
property color surfaceContainer: currentThemeData.surfaceContainer
property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh
property color surfaceContainerHighest: currentThemeData.surfaceContainerHighest || surfaceContainerHigh
property color surfaceBright: currentThemeData.surfaceBright || (isLightMode ? surface : surfaceContainerHighest)
property color surfaceDim: currentThemeData.surfaceDim || (isLightMode ? surfaceContainer : background)
property color primaryContainer: currentThemeData.primaryContainer || blend(surfaceContainerHigh, primary, 0.45)
property color secondaryContainer: currentThemeData.secondaryContainer || blend(surfaceContainerHigh, secondary, 0.35)
property color tertiaryContainer: currentThemeData.tertiaryContainer || blend(surfaceContainerHigh, tertiary, 0.35)
@@ -520,6 +520,32 @@ var SPEC = {
notificationRules: { def: [] },
notificationDndAllowCritical: { def: true },
notificationFocusedMonitor: { def: false },
dankIslandBarId: { def: "", onChange: "updateBarConfigs" },
dankIslandFloating: { def: false },
dankIslandUseOverlayLayer: { def: false },
dankIslandReserveHeight: { def: 40 },
dankIslandCompactHeight: { def: 38 },
dankIslandOuterGap: { def: 4 },
dankIslandHorizontalOffset: { def: 0 },
dankIslandInteractionMode: { def: "hybrid" },
dankIslandHoverOpenDelay: { def: 220 },
dankIslandHoverCloseDelay: { def: 400 },
dankIslandPalette: { def: "default" },
dankIslandHighContrast: { def: false },
dankIslandMediaClockVisible: { def: true },
dankIslandNotificationExpand: { def: false },
dankIslandHomeMediaSlot: { def: "left" },
dankIslandHomeStatusSlot: { def: "right" },
dankIslandHomeWeatherSlot: { def: "hidden" },
dankIslandHomeCompactTight: { def: false },
dankIslandBatteryStyle: { def: "solid" },
dankIslandSatellitesEnabled: { def: true },
dankIslandSatellitePosition: { def: "edges" },
dankIslandSatelliteGap: { def: 12 },
dankIslandReducedMotion: { def: false },
dankIslandSpringStiffness: { def: 560 },
dankIslandSpringDamping: { def: 37 },
dankIslandSpringMass: { def: 1 },
osdAlwaysShowValue: { def: false },
osdPosition: { def: 5 },
+32 -3
View File
@@ -13,6 +13,7 @@ import qs.Modals.DankLauncherV2
import qs.Modules
import qs.Modules.AppDrawer
import qs.Modules.DankDash
import qs.Modules.DankIsland
import qs.Modules.ControlCenter
import qs.Modules.Dock
import qs.Modules.Lock
@@ -37,6 +38,18 @@ Item {
property bool osdSurfacesLoaded: false
property int pendingOsdResumeReloads: 0
readonly property var dankIslandScreens: SettingsData.getIslandScreens()
readonly property var notificationPopupScreens: {
const screens = SettingsData.notificationFocusedMonitor ? Quickshell.screens : SettingsData.getFilteredScreens("notifications");
return root.withoutDankIslandScreens(screens);
}
readonly property var legacySystemLevelOsdScreens: root.withoutDankIslandScreens(SettingsData.getFilteredScreens("osd"))
function withoutDankIslandScreens(screens) {
if (!SettingsData.dankIslandEnabled)
return screens;
return screens.filter(screen => root.dankIslandScreens.indexOf(screen) === -1);
}
function recreateOsdSurfaces() {
OSDManager.currentOSDsByScreen = ({});
@@ -386,11 +399,27 @@ Item {
}
Variants {
model: SettingsData.notificationFocusedMonitor ? Quickshell.screens : SettingsData.getFilteredScreens("notifications")
model: root.notificationPopupScreens
delegate: NotificationPopupManager {}
}
Loader {
id: dankIslandLoader
property var modalRef: colorPickerModal
property LazyLoader powerModalLoaderRef: powerMenuModalLoader
active: SettingsData.dankIslandEnabled
asynchronous: false
sourceComponent: DankIsland {
screenModel: root.dankIslandScreens
colorPickerModal: dankIslandLoader.modalRef
powerMenuModalLoader: dankIslandLoader.powerModalLoaderRef
onLockRequested: lock.activate()
}
}
LazyLoader {
id: controlCenterLoader
@@ -1159,7 +1188,7 @@ Item {
sourceComponent: Component {
Item {
Variants {
model: SettingsData.getFilteredScreens("osd")
model: root.legacySystemLevelOsdScreens
delegate: VolumeOSD {}
}
@@ -1183,7 +1212,7 @@ Item {
}
Variants {
model: SettingsData.getFilteredScreens("osd")
model: root.legacySystemLevelOsdScreens
delegate: BrightnessOSD {}
}
+54 -2
View File
@@ -217,6 +217,8 @@ Item {
IpcHandler {
function open(): string {
if (PopoutService.routeControlCenterToIsland(null, "", false))
return "CONTROL_CENTER_OPEN_SUCCESS";
const bar = root.getPreferredBar("controlCenterButtonRef");
if (bar) {
bar.triggerControlCenter();
@@ -226,6 +228,8 @@ Item {
}
function hide(): string {
if (PopoutService.dankIslandRouter?.closeControlCenter?.() === true)
return "CONTROL_CENTER_HIDE_SUCCESS";
if (root.controlCenterLoader.item && root.controlCenterLoader.item.shouldBeVisible) {
root.controlCenterLoader.item.close();
return "CONTROL_CENTER_HIDE_SUCCESS";
@@ -238,6 +242,8 @@ Item {
root.controlCenterLoader.item.close();
return "CONTROL_CENTER_TOGGLE_SUCCESS";
}
if (PopoutService.routeControlCenterToIsland(null, "", true))
return "CONTROL_CENTER_TOGGLE_SUCCESS";
const bar = root.getPreferredBar("controlCenterButtonRef");
if (bar) {
@@ -248,6 +254,8 @@ Item {
}
function status(): string {
if (PopoutService.islandControlCenterOpen)
return "visible";
return (root.controlCenterLoader.item && root.controlCenterLoader.item.shouldBeVisible) ? "visible" : "hidden";
}
@@ -303,11 +311,22 @@ Item {
}
function _openDash(tab, position) {
const tabId = _resolveTabId(tab);
if (!position) {
if (tabId === "overview" && PopoutService.routeOverviewToIsland(null, false))
return true;
if (tabId === "media" && PopoutService.routeMediaToIsland(null, false))
return true;
if (tabId === "wallpaper" && PopoutService.routeWallpaperToIsland(null, false))
return true;
if (tabId === "weather" && PopoutService.routeWeatherToIsland(null, false))
return true;
}
const bar = _dashBar(position);
if (!bar)
return false;
const tabId = _resolveTabId(tab);
const dash = root.dankDashPopoutLoader.item;
if (dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name) {
if (position && bar.positionDash)
@@ -327,10 +346,22 @@ Item {
return true;
}
const tabId = _resolveTabId(tab);
if (!position) {
if (tabId === "overview" && PopoutService.routeOverviewToIsland(null, true))
return true;
if (tabId === "media" && PopoutService.routeMediaToIsland(null, true))
return true;
if (tabId === "wallpaper" && PopoutService.routeWallpaperToIsland(null, true))
return true;
if (tabId === "weather" && PopoutService.routeWeatherToIsland(null, true))
return true;
}
const bar = _dashBar(position);
if (!bar)
return false;
return bar.triggerDashTab(_resolveTabId(tab), position);
return bar.triggerDashTab(tabId, position);
}
function resolveTabIndex(tab: string): int {
@@ -346,6 +377,8 @@ Item {
}
function close(): string {
if (PopoutService.closeIslandHome() || PopoutService.closeIslandMedia() || PopoutService.closeIslandWallpaper() || PopoutService.closeIslandWeather())
return "DASH_CLOSE_SUCCESS";
if (root.dankDashPopoutLoader.item) {
root.dankDashPopoutLoader.item.dashVisible = false;
return "DASH_CLOSE_SUCCESS";
@@ -752,6 +785,8 @@ Item {
// ! TODO - remove for v1.6
IpcHandler {
function wallpaper(): string {
if (PopoutService.routeWallpaperToIsland(null, true))
return "WARN; deprecated, use dms ipc call dash toggle wallpaper instead";
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
if (bar) {
bar.triggerWallpaperBrowser();
@@ -788,6 +823,8 @@ Item {
} = getBarConfig(selector, value);
if (error)
return error;
if (SettingsData.isIslandBarConfig(barConfig))
return "BAR_IS_ISLAND";
SettingsData.updateBarConfig(barConfig.id, {
visible: true
});
@@ -801,6 +838,8 @@ Item {
} = getBarConfig(selector, value);
if (error)
return error;
if (SettingsData.isIslandBarConfig(barConfig))
return "BAR_IS_ISLAND";
SettingsData.updateBarConfig(barConfig.id, {
visible: false
});
@@ -814,6 +853,8 @@ Item {
} = getBarConfig(selector, value);
if (error)
return error;
if (SettingsData.isIslandBarConfig(barConfig))
return "BAR_IS_ISLAND";
SettingsData.updateBarConfig(barConfig.id, {
visible: !barConfig.visible
});
@@ -837,6 +878,8 @@ Item {
} = getBarConfig(selector, value);
if (error)
return error;
if (SettingsData.isIslandBarConfig(barConfig))
return "BAR_IS_ISLAND";
SettingsData.updateBarConfig(barConfig.id, {
autoHide: true
});
@@ -850,6 +893,8 @@ Item {
} = getBarConfig(selector, value);
if (error)
return error;
if (SettingsData.isIslandBarConfig(barConfig))
return "BAR_IS_ISLAND";
SettingsData.updateBarConfig(barConfig.id, {
autoHide: false
});
@@ -863,6 +908,8 @@ Item {
} = getBarConfig(selector, value);
if (error)
return error;
if (SettingsData.isIslandBarConfig(barConfig))
return "BAR_IS_ISLAND";
SettingsData.updateBarConfig(barConfig.id, {
autoHide: !barConfig.autoHide
});
@@ -876,6 +923,8 @@ Item {
} = getBarConfig(selector, value);
if (error)
return error;
if (SettingsData.isIslandBarConfig(barConfig))
return "BAR_IS_ISLAND";
if (!barConfig.autoHide)
return "BAR_AUTO_HIDE_DISABLED";
if (!(barConfig.visible ?? true)) {
@@ -916,6 +965,9 @@ Item {
const posValue = positionMap[position.toLowerCase()];
if (posValue === undefined)
return "BAR_INVALID_POSITION";
// The island silhouette only anchors top or bottom.
if (SettingsData.isIslandBarConfig(barConfig) && posValue !== SettingsData.Position.Top && posValue !== SettingsData.Position.Bottom)
return "BAR_IS_ISLAND";
SettingsData.updateBarConfig(barConfig.id, {
position: posValue
});
@@ -64,7 +64,8 @@ Item {
}
readonly property bool useSpotlightBackend: !FrameTransitionState.effectiveConnectedFrameModeActive && SettingsData.launcherStyle === "spotlight"
readonly property var _desiredBackend: useSpotlightBackend ? spotlightComp : (FrameTransitionState.effectiveConnectedFrameModeActive ? connectedComp : standaloneComp)
readonly property bool useIslandBackend: !FrameTransitionState.effectiveConnectedFrameModeActive && SettingsData.launcherStyle === "island"
readonly property var _desiredBackend: FrameTransitionState.effectiveConnectedFrameModeActive ? connectedComp : useIslandBackend ? islandComp : useSpotlightBackend ? spotlightComp : standaloneComp
property var _resolvedBackend: null
Component.onCompleted: _resolvedBackend = _desiredBackend
@@ -111,6 +112,11 @@ Item {
DankLauncherV2ModalSpotlight {}
}
Component {
id: islandComp
DankLauncherV2ModalIsland {}
}
function _wireBackend(it) {
if (!it)
return;
@@ -0,0 +1,122 @@
import QtQuick
import qs.Services
Item {
id: root
property var modalHandle: root
property bool triggerUsesOverlayLayer: false
property bool usingFallback: false
property bool _islandWasOpen: false
readonly property var log: Log.scoped("DankLauncherV2ModalIsland")
readonly property var router: PopoutService.dankIslandRouter
readonly property bool spotlightOpen: usingFallback ? fallback.spotlightOpen : (router?.launcherOpen ?? false)
readonly property bool isClosing: usingFallback ? fallback.isClosing : false
readonly property bool keyboardActive: usingFallback ? fallback.keyboardActive : spotlightOpen
readonly property bool contentVisible: usingFallback ? fallback.contentVisible : spotlightOpen
readonly property var spotlightContent: usingFallback ? fallback.spotlightContent : null
readonly property bool openedFromOverview: usingFallback ? fallback.openedFromOverview : false
readonly property var effectiveScreen: usingFallback ? fallback.effectiveScreen : null
readonly property real screenWidth: usingFallback ? fallback.screenWidth : 1920
readonly property real screenHeight: usingFallback ? fallback.screenHeight : 1080
readonly property real dpr: usingFallback ? fallback.dpr : 1
readonly property int modalWidth: usingFallback ? fallback.modalWidth : 680
readonly property int modalHeight: usingFallback ? fallback.modalHeight : 560
readonly property real modalX: usingFallback ? fallback.modalX : 0
readonly property real modalY: usingFallback ? fallback.modalY : 0
readonly property bool frameOwnsConnectedChrome: false
readonly property string resolvedConnectedBarSide: ""
readonly property bool launcherArcExtenderActive: false
signal dialogClosed
function _openIsland(query, mode) {
const accepted = router?.openLauncher?.(query || "", mode || "") ?? false;
if (accepted) {
usingFallback = false;
return true;
}
log.warn("No DankIsland is routed to the focused screen; falling back to Spotlight");
usingFallback = true;
return false;
}
function show() {
if (!root._openIsland("", ""))
fallback.show();
}
function showWithQuery(query) {
if (!root._openIsland(query, ""))
fallback.showWithQuery(query);
}
function showWithMode(mode) {
if (!root._openIsland("", mode))
fallback.showWithMode(mode);
}
function hide() {
if (usingFallback) {
fallback.hide();
return;
}
router?.closeLauncher?.();
}
function toggle() {
if (usingFallback && fallback.spotlightOpen) {
fallback.toggle();
return;
}
const accepted = router?.toggleLauncher?.("", "") ?? false;
if (!accepted) {
usingFallback = true;
fallback.toggle();
}
}
function toggleWithQuery(query) {
if (spotlightOpen) {
hide();
return;
}
showWithQuery(query);
}
function toggleWithMode(mode) {
if (spotlightOpen) {
hide();
return;
}
showWithMode(mode);
}
onSpotlightOpenChanged: {
if (usingFallback)
return;
if (spotlightOpen) {
_islandWasOpen = true;
} else if (_islandWasOpen) {
_islandWasOpen = false;
dialogClosed();
}
}
DankLauncherV2ModalSpotlight {
id: fallback
modalHandle: root.modalHandle
triggerUsesOverlayLayer: root.triggerUsesOverlayLayer
}
Connections {
target: fallback
function onDialogClosed() {
root.usingFallback = false;
root.dialogClosed();
}
}
}
@@ -10,16 +10,21 @@ FocusScope {
property var parentModal: null
property alias searchField: searchInput
property alias controller: searchController
property var controllerOverride: null
readonly property var controller: controllerOverride ?? searchController
readonly property alias activeContextMenu: contextMenu
property var transientSurfaceTracker: null
property bool showResultsWithoutQuery: false
property bool suspendSearchUpdates: false
property real maxResultsHeight: 0
readonly property bool _hasQuery: searchInput.text.length > 0
readonly property bool _hasQuery: root.showResultsWithoutQuery || searchInput.text.length > 0
readonly property real _searchBarH: 56
readonly property real _searchAreaH: _searchBarH
readonly property alias searchAreaHeight: root._searchAreaH
readonly property real _statusH: 92
readonly property real _rowH: 64
readonly property real _maxResultsH: Math.min(430, (parentModal?.screenHeight ?? 900) * 0.55)
readonly property real _maxResultsH: root.maxResultsHeight > 0 ? root.maxResultsHeight : Math.min(430, (parentModal?.screenHeight ?? 900) * 0.55)
readonly property var _resultRows: _buildRows()
readonly property real _resultsContentH: _resultRows.length > 0 ? _resultRows.length * _rowH + resultsList.bottomInset : _statusH
readonly property real _resultsH: _hasQuery ? Math.min(_resultsContentH, _maxResultsH) : 0
@@ -58,14 +63,20 @@ FocusScope {
resultsList.resetScroll();
}
function resetSearch() {
root.controller.reset();
if (root.showResultsWithoutQuery)
root.controller.performSearch();
}
function closeTransientUi() {
transientSurfaceTracker?.closeAll?.();
root.enabled = true;
}
function _buildRows() {
const flat = searchController.flatModel || [];
const sections = searchController.sections || [];
const flat = root.controller.flatModel || [];
const sections = root.controller.sections || [];
const rows = [];
const seen = {};
for (let i = 0; i < flat.length; i++) {
@@ -106,7 +117,7 @@ FocusScope {
switch (event.key) {
case Qt.Key_Escape:
if (searchController.clearPluginFilter()) {
if (root.controller.clearPluginFilter()) {
event.accepted = true;
return;
}
@@ -115,12 +126,12 @@ FocusScope {
return;
case Qt.Key_Backspace:
if (searchInput.text.length === 0) {
if (searchController.clearPluginFilter()) {
if (root.controller.clearPluginFilter()) {
event.accepted = true;
return;
}
if (searchController.autoSwitchedToFiles) {
searchController.restorePreviousMode();
if (root.controller.autoSwitchedToFiles) {
root.controller.restorePreviousMode();
event.accepted = true;
return;
}
@@ -128,31 +139,31 @@ FocusScope {
event.accepted = false;
return;
case Qt.Key_Down:
searchController.selectNext();
root.controller.selectNext();
event.accepted = true;
return;
case Qt.Key_Up:
searchController.selectPrevious();
root.controller.selectPrevious();
event.accepted = true;
return;
case Qt.Key_PageDown:
searchController.selectPageDown(7);
root.controller.selectPageDown(7);
event.accepted = true;
return;
case Qt.Key_PageUp:
searchController.selectPageUp(7);
root.controller.selectPageUp(7);
event.accepted = true;
return;
case Qt.Key_J:
if (hasCtrl) {
searchController.selectNext();
root.controller.selectNext();
event.accepted = true;
return;
}
break;
case Qt.Key_K:
if (hasCtrl) {
searchController.selectPrevious();
root.controller.selectPrevious();
event.accepted = true;
return;
}
@@ -168,45 +179,45 @@ FocusScope {
case Qt.Key_Return:
case Qt.Key_Enter:
if (event.modifiers & Qt.ShiftModifier) {
searchController.pasteSelected();
root.controller.pasteSelected();
} else {
searchController.executeSelected();
root.controller.executeSelected();
}
event.accepted = true;
return;
case Qt.Key_Menu:
case Qt.Key_F10:
if (contextMenu.hasContextMenuActions(searchController.selectedItem)) {
if (contextMenu.hasContextMenuActions(root.controller.selectedItem)) {
const scenePos = resultsList.getSelectedItemPosition();
_showContextMenu(searchController.selectedItem, scenePos.x, scenePos.y, true);
_showContextMenu(root.controller.selectedItem, scenePos.x, scenePos.y, true);
event.accepted = true;
return;
}
break;
case Qt.Key_1:
if (hasCtrl || hasAlt) {
searchController.setMode("all");
root.controller.setMode("all");
event.accepted = true;
return;
}
break;
case Qt.Key_2:
if (hasCtrl || hasAlt) {
searchController.setMode("apps");
root.controller.setMode("apps");
event.accepted = true;
return;
}
break;
case Qt.Key_3:
if (hasCtrl || hasAlt) {
searchController.setMode("files");
root.controller.setMode("files");
event.accepted = true;
return;
}
break;
case Qt.Key_4:
if (hasCtrl || hasAlt) {
searchController.setMode("plugins");
root.controller.setMode("plugins");
event.accepted = true;
return;
}
@@ -218,19 +229,15 @@ FocusScope {
Controller {
id: searchController
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
active: !root.controllerOverride && (root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true)
viewModeContext: "spotlight"
forceLinearNavigation: true
onItemExecuted: {
root.parentModal?.hide();
}
}
LauncherContextMenu {
id: contextMenu
parent: root
controller: searchController
controller: root.controller
searchField: searchInput
parentHandler: root
allowEditActions: false
@@ -257,7 +264,13 @@ FocusScope {
}
Connections {
target: searchController
target: root.controller
function onItemExecuted() {
root.parentModal?.hide();
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview)
NiriService.toggleOverview();
}
function onModeChanged(mode, userInitiated) {
if (!userInitiated || !SettingsData.rememberLastMode)
return;
@@ -301,7 +314,7 @@ FocusScope {
DankIcon {
anchors.centerIn: parent
name: searchController.activePluginId ? "extension" : searchController.searchMode === "files" ? "folder" : "search"
name: root.controller.activePluginId ? "extension" : root.controller.searchMode === "files" ? "folder" : "search"
size: 20
color: searchInput.getActiveFocus() ? Theme.primary : Theme.surfaceVariantText
}
@@ -375,7 +388,6 @@ FocusScope {
visible: searchInput.text.length > 0
onClicked: {
searchInput.text = "";
searchController.reset();
root._focusSearch();
}
}
@@ -397,10 +409,12 @@ FocusScope {
keyForwardTargets: [searchKeyHandler]
onTextChanged: {
if (root.suspendSearchUpdates)
return;
if (text.length > 0) {
searchController.setSearchQuery(text);
root.controller.setSearchQuery(text);
} else {
searchController.reset();
root.resetSearch();
}
}
@@ -433,7 +447,7 @@ FocusScope {
SpotlightResultsList {
id: resultsList
anchors.fill: parent
controller: searchController
controller: root.controller
hasQuery: root._hasQuery
rows: root._resultRows
@@ -463,7 +477,7 @@ FocusScope {
]
function _isCategorySelected(cat) {
return searchController.searchMode === cat.mode;
return root.controller.searchMode === cat.mode;
}
function _cycleCategory(reverse) {
@@ -482,9 +496,9 @@ FocusScope {
const cat = _categoryModel[index];
if (!cat)
return;
searchController.setMode(cat.mode, false);
root.controller.setMode(cat.mode, false);
if (root._hasQuery)
searchController.setSearchQuery(searchInput.text);
root.controller.setSearchQuery(searchInput.text);
root._focusSearch();
}
}
+7
View File
@@ -121,17 +121,24 @@ DankModal {
}
IpcHandler {
// DankIsland owns this trigger whenever it is the sole bar for the screen.
function open(): string {
if (PopoutService.routeNotificationCenterToIsland(null, false))
return "NOTIFICATION_ISLAND_OPEN_SUCCESS";
notificationModal.show();
return "NOTIFICATION_MODAL_OPEN_SUCCESS";
}
function close(): string {
if (PopoutService.closeIslandNotificationCenter())
return "NOTIFICATION_ISLAND_CLOSE_SUCCESS";
notificationModal.hide();
return "NOTIFICATION_MODAL_CLOSE_SUCCESS";
}
function toggle(): string {
if (PopoutService.routeNotificationCenterToIsland(null, true))
return "NOTIFICATION_ISLAND_TOGGLE_SUCCESS";
notificationModal.toggle();
return "NOTIFICATION_MODAL_TOGGLE_SUCCESS";
}
+9 -4
View File
@@ -310,8 +310,11 @@ DankModal {
positioning: parentBounds.width > 0 ? "custom" : "center"
customPosition: {
if (parentBounds.width > 0) {
const effectiveBarThickness = Math.max(26 + (SettingsData.barConfigs[0]?.innerPadding ?? 4) * 0.6 + (SettingsData.barConfigs[0]?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (SettingsData.barConfigs[0]?.innerPadding ?? 4)));
const barExclusionZone = effectiveBarThickness + (SettingsData.barConfigs[0]?.spacing ?? 4) + (SettingsData.barConfigs[0]?.bottomGap ?? 0);
const bar = SettingsData.getPrimaryBarConfig();
const barPadding = bar?.innerPadding ?? 4;
const effectiveBarThickness = Math.max(26 + barPadding * 0.6 + barPadding + 4, Theme.barHeight - 4 - (8 - barPadding));
const barExclusionZone = effectiveBarThickness + (bar?.spacing ?? 4) + (bar?.bottomGap ?? 0);
const barPosition = bar?.position ?? SettingsData.Position.Top;
const screenW = parentScreen?.width ?? 1920;
const screenH = parentScreen?.height ?? 1080;
const margin = Theme.spacingL;
@@ -319,8 +322,10 @@ DankModal {
let targetX = parentBounds.x + (parentBounds.width - modalWidth) / 2;
let targetY = parentBounds.y + (parentBounds.height - modalHeight) / 2;
const minY = (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Top ? barExclusionZone + margin : margin;
const maxY = (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Bottom ? screenH - modalHeight - barExclusionZone - margin : screenH - modalHeight - margin;
const topChrome = Math.max(bar && barPosition === SettingsData.Position.Top ? barExclusionZone : 0, SettingsData.dankIslandEdgeOffset(parentScreen, "top"));
const bottomChrome = Math.max(bar && barPosition === SettingsData.Position.Bottom ? barExclusionZone : 0, SettingsData.dankIslandEdgeOffset(parentScreen, "bottom"));
const minY = topChrome + margin;
const maxY = screenH - modalHeight - bottomChrome - margin;
targetY = Math.max(minY, Math.min(maxY, targetY));
@@ -683,6 +683,22 @@ FocusScope {
}
}
Loader {
id: dankIslandLoader
anchors.fill: parent
active: root.currentIndex === 46
visible: active
focus: active
sourceComponent: DankIslandTab {
parentModal: root.parentModal
}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: usersLoader
anchors.fill: parent
@@ -114,7 +114,7 @@ Rectangle {
},
{
"id": "dankbar",
"text": I18n.tr("Dank Bar"),
"text": I18n.tr("Primary Bar"),
"icon": "toolbar",
"children": [
{
@@ -146,6 +146,12 @@ Rectangle {
"text": I18n.tr("Frame"),
"icon": "frame_source",
"tabIndex": 33
},
{
"id": "dank_island",
"text": I18n.tr("Island"),
"icon": "view_in_ar",
"tabIndex": 46
}
]
},
@@ -157,7 +163,7 @@ Rectangle {
"children": [
{
"id": "dank_dash",
"text": I18n.tr("Dank Dash"),
"text": I18n.tr("Dashboard"),
"icon": "space_dashboard",
"tabIndex": 43
},
@@ -10,11 +10,14 @@ Rectangle {
LayoutMirroring.childrenInherit: true
property bool editMode: false
// Hosts that morph in place reuse the header's dead space as their close target.
property bool tapToClose: false
signal powerButtonClicked
signal lockRequested
signal editModeToggled
signal settingsButtonClicked
signal headerTapped
Component.onCompleted: DgopService.addRef("system")
Component.onDestruction: DgopService.removeRef("system")
@@ -25,6 +28,13 @@ Rectangle {
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
MouseArea {
anchors.fill: parent
enabled: root.tapToClose
acceptedButtons: Qt.LeftButton
onClicked: root.headerTapped()
}
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
@@ -0,0 +1,175 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.ControlCenter.Components
import qs.Modules.ControlCenter.Models
import qs.Modules.ControlCenter.Details
import qs.Widgets
// Host-agnostic Control Center body; popout and island both host this.
Rectangle {
id: root
required property var host
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
readonly property real targetImplicitHeight: {
let total = Theme.spacingL + headerPane.implicitHeight + Theme.spacingS + widgetGrid.targetImplicitHeight;
if (editControls.visible)
total += Theme.spacingS + editControls.height;
return total + Theme.spacingL;
}
property alias bluetoothCodecSelector: bluetoothCodecSelector
property alias audioPortSelector: audioPortSelector
implicitHeight: targetImplicitHeight
color: "transparent"
clip: true
WidgetModel {
id: widgetModel
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.6)
radius: parent.radius
visible: root.host.powerMenuOpen
z: 5000
Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.host.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
}
DankFlickable {
id: contentFlickable
anchors.fill: parent
clip: true
contentWidth: width
contentHeight: Math.max(height, mainColumn.implicitHeight + Theme.spacingM)
interactive: contentHeight > height
Column {
id: mainColumn
width: contentFlickable.width - Theme.spacingL * 2
x: Theme.spacingL
y: Theme.spacingL
spacing: Theme.spacingS
HeaderPane {
id: headerPane
width: parent.width
editMode: root.host.editMode
tapToClose: root.host.headerTogglesClose ?? false
onHeaderTapped: root.host.close()
onEditModeToggled: root.host.editMode = !root.host.editMode
onPowerButtonClicked: {
const loader = root.host.powerMenuModalLoader;
if (!loader)
return;
loader.active = true;
if (loader.item) {
const bounds = Qt.rect(root.host.alignedX, root.host.alignedY, root.host.popupWidth, root.host.popupHeight);
loader.item.openFromControlCenter(bounds, root.host.screen);
}
}
onLockRequested: {
root.host.close();
root.host.lockRequested();
}
onSettingsButtonClicked: root.host.close()
}
DragDropGrid {
id: widgetGrid
width: parent.width
editMode: root.host.editMode
maxPopoutHeight: {
const screenHeight = (root.host.triggerScreen?.height ?? 1080);
return screenHeight - 100 - Theme.spacingL - headerPane.implicitHeight - Theme.spacingS;
}
expandedSection: root.host.expandedSection
expandedWidgetIndex: root.host.expandedWidgetIndex
expandedWidgetData: root.host.expandedWidgetData
model: widgetModel
bluetoothCodecSelector: bluetoothCodecSelector
audioPortSelector: audioPortSelector
colorPickerModal: root.host.colorPickerModal
screenName: root.host.triggerScreen?.name || ""
screenModel: root.host.triggerScreen?.model || ""
parentScreen: root.host.triggerScreen
onExpandClicked: (widgetData, globalIndex) => {
root.host.expandedWidgetIndex = globalIndex;
root.host.expandedWidgetData = widgetData;
if (widgetData.id === "diskUsage") {
root.host.toggleSection("diskUsage_" + (widgetData.instanceId || "default"));
} else if (widgetData.id === "brightnessSlider") {
root.host.toggleSection("brightnessSlider_" + (widgetData.instanceId || "default"));
} else {
root.host.toggleSection(widgetData.id);
}
}
onRemoveWidget: index => widgetModel.removeWidget(index)
onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex)
onToggleWidgetSize: index => widgetModel.toggleWidgetSize(index)
onCollapseRequested: root.host.collapseAll()
onConfigRequested: (idx, data, anchor) => widgetConfigOverlay.open(idx, data, anchor)
}
EditControls {
id: editControls
width: parent.width
visible: root.host.editMode
popupScreen: root.host.screen
popoutX: root.host.alignedX
popoutY: root.host.alignedY
popoutWidth: root.host.alignedWidth
popoutHeight: root.host.alignedHeight
availableWidgets: {
if (!root.host.editMode)
return [];
const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id);
const allWidgets = widgetModel.baseWidgetDefinitions.concat(widgetModel.getPluginWidgets());
return allWidgets.filter(w => w.allowMultiple || !existingIds.includes(w.id));
}
onAddWidget: widgetId => widgetModel.addWidget(widgetId)
onResetToDefault: () => widgetModel.resetToDefault()
onClearAll: () => widgetModel.clearAll()
}
}
}
BluetoothCodecSelector {
id: bluetoothCodecSelector
anchors.fill: parent
z: 10000
}
AudioPortSelector {
id: audioPortSelector
anchors.fill: parent
z: 10000
}
WidgetConfigOverlay {
id: widgetConfigOverlay
anchors.fill: parent
}
}
@@ -4,8 +4,6 @@ import qs.Common
import qs.Modules.ControlCenter.Details
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Components
import qs.Modules.ControlCenter.Models
import "./utils/state.js" as StateUtils
DankPopout {
@@ -34,8 +32,9 @@ DankPopout {
const item = contentLoader.item;
if (!item)
return 400;
// The content now reports its own insets, so no popout-side padding.
const naturalHeight = item.targetImplicitHeight !== undefined ? item.targetImplicitHeight : item.implicitHeight;
return Math.max(300, naturalHeight + 20);
return Math.max(300, naturalHeight);
}
function updateTargetPopupHeight() {
@@ -162,160 +161,9 @@ DankPopout {
}
}
WidgetModel {
id: widgetModel
}
content: Component {
Rectangle {
id: controlContent
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
readonly property real targetImplicitHeight: {
let total = headerPane.implicitHeight + Theme.spacingS + widgetGrid.targetImplicitHeight;
if (editControls.visible)
total += Theme.spacingS + editControls.height;
return total + Theme.spacingM;
}
implicitHeight: targetImplicitHeight
property alias bluetoothCodecSelector: bluetoothCodecSelector
property alias audioPortSelector: audioPortSelector
color: "transparent"
clip: true
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.6)
radius: parent.radius
visible: root.powerMenuOpen
z: 5000
Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
}
DankFlickable {
id: contentFlickable
anchors.fill: parent
clip: true
contentWidth: width
contentHeight: Math.max(height, mainColumn.implicitHeight + Theme.spacingM)
interactive: contentHeight > height
Column {
id: mainColumn
width: contentFlickable.width - Theme.spacingL * 2
x: Theme.spacingL
y: Theme.spacingL
spacing: Theme.spacingS
HeaderPane {
id: headerPane
width: parent.width
editMode: root.editMode
onEditModeToggled: root.editMode = !root.editMode
onPowerButtonClicked: {
if (powerMenuModalLoader) {
powerMenuModalLoader.active = true;
if (powerMenuModalLoader.item) {
const bounds = Qt.rect(root.alignedX, root.alignedY, root.popupWidth, root.popupHeight);
powerMenuModalLoader.item.openFromControlCenter(bounds, root.screen);
}
}
}
onLockRequested: {
root.close();
root.lockRequested();
}
onSettingsButtonClicked: {
root.close();
}
}
DragDropGrid {
id: widgetGrid
width: parent.width
editMode: root.editMode
maxPopoutHeight: {
const screenHeight = (root.triggerScreen?.height ?? 1080);
return screenHeight - 100 - Theme.spacingL - headerPane.implicitHeight - Theme.spacingS;
}
expandedSection: root.expandedSection
expandedWidgetIndex: root.expandedWidgetIndex
expandedWidgetData: root.expandedWidgetData
model: widgetModel
bluetoothCodecSelector: bluetoothCodecSelector
audioPortSelector: audioPortSelector
colorPickerModal: root.colorPickerModal
screenName: root.triggerScreen?.name || ""
screenModel: root.triggerScreen?.model || ""
parentScreen: root.triggerScreen
onExpandClicked: (widgetData, globalIndex) => {
root.expandedWidgetIndex = globalIndex;
root.expandedWidgetData = widgetData;
if (widgetData.id === "diskUsage") {
root.toggleSection("diskUsage_" + (widgetData.instanceId || "default"));
} else if (widgetData.id === "brightnessSlider") {
root.toggleSection("brightnessSlider_" + (widgetData.instanceId || "default"));
} else {
root.toggleSection(widgetData.id);
}
}
onRemoveWidget: index => widgetModel.removeWidget(index)
onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex)
onToggleWidgetSize: index => widgetModel.toggleWidgetSize(index)
onCollapseRequested: root.collapseAll()
onConfigRequested: (idx, data, anchor) => widgetConfigOverlay.open(idx, data, anchor)
}
EditControls {
id: editControls
width: parent.width
visible: editMode
popupScreen: root.screen
popoutX: root.alignedX
popoutY: root.alignedY
popoutWidth: root.alignedWidth
popoutHeight: root.alignedHeight
availableWidgets: {
if (!editMode)
return [];
const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id);
const allWidgets = widgetModel.baseWidgetDefinitions.concat(widgetModel.getPluginWidgets());
return allWidgets.filter(w => w.allowMultiple || !existingIds.includes(w.id));
}
onAddWidget: widgetId => widgetModel.addWidget(widgetId)
onResetToDefault: () => widgetModel.resetToDefault()
onClearAll: () => widgetModel.clearAll()
}
}
}
BluetoothCodecSelector {
id: bluetoothCodecSelector
anchors.fill: parent
z: 10000
}
AudioPortSelector {
id: audioPortSelector
anchors.fill: parent
z: 10000
}
WidgetConfigOverlay {
id: widgetConfigOverlay
anchors.fill: parent
}
ControlCenterContent {
host: root
}
}
+8 -1
View File
@@ -165,7 +165,14 @@ Item {
const base = Quickshell.screens.filter(screen => SettingsData.barConfigCoversScreen(root.barConfig, screen));
// Connected frame mode renders the bar inside the frame surface; skip the standalone window there
// unless this bar wants the overlay layer, which the frame surface cannot provide.
return base.filter(screen => !CompositorService.frameHostsBarForConfig(screen, root.barConfig));
return base.filter(screen => {
if (CompositorService.frameHostsBarForConfig(screen, root.barConfig))
return false;
if (SettingsData.isIslandBarConfig(root.barConfig))
return false;
const edge = SettingsData.positionToSide(root.barConfig?.position ?? SettingsData.Position.Top);
return !SettingsData.dankIslandOwnsEdge(screen, edge);
});
}
delegate: DankBarWindow {
@@ -586,6 +586,8 @@ Item {
return SettingsData.barConfigs.some(bc => {
if (!bc.enabled || bc.id === barConfig?.id)
return false;
if (SettingsData.isIslandBarConfig(bc))
return false;
if (bc.autoHide)
return false;
if (!(bc.visible ?? true))
@@ -609,6 +611,8 @@ Item {
const result = SettingsData.barConfigs.some(bc => {
if (!bc.enabled || bc.id === barConfig?.id)
return false;
if (SettingsData.isIslandBarConfig(bc))
return false;
if (bc.autoHide)
return false;
if (!(bc.visible ?? true))
@@ -633,6 +637,8 @@ Item {
const result = SettingsData.barConfigs.some(bc => {
if (!bc.enabled || bc.id === barConfig?.id)
return false;
if (SettingsData.isIslandBarConfig(bc))
return false;
if (bc.autoHide)
return false;
if (!(bc.visible ?? true))
@@ -657,6 +663,8 @@ Item {
const result = SettingsData.barConfigs.some(bc => {
if (!bc.enabled || bc.id === barConfig?.id)
return false;
if (SettingsData.isIslandBarConfig(bc))
return false;
if (bc.autoHide)
return false;
if (!(bc.visible ?? true))
@@ -31,7 +31,7 @@ BasePill {
function updateWidgetPciId(pciId) {
const sections = ["left", "center", "right"];
const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
const defaultBar = SettingsData.getPrimaryBarConfig();
if (!defaultBar)
return;
for (let s = 0; s < sections.length; s++) {
@@ -769,7 +769,7 @@ BasePill {
property string edge: "top"
// New properties for bar context
property int triggerBarPosition: (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top)
property int triggerBarPosition: (SettingsData.getPrimaryBarConfig()?.position ?? SettingsData.Position.Top)
property real triggerBarThickness: 0
property real triggerBarSpacing: 0
property var triggerBarConfig: null
@@ -14,11 +14,30 @@ Item {
property int dropdownType: 0
property var activePlayer: null
property var allPlayers: []
// Chromium keeps dead MPRIS services registered w/empty metadata; avoid listing them
readonly property var selectablePlayers: (allPlayers || []).filter(p => p && !MprisController.isIdle(p))
// Drop idle Chromium MPRIS stubs, and only while the players panel is open.
readonly property var selectablePlayers: root.dropdownType !== 3 ? [] : (allPlayers || []).filter(p => p && !MprisController.isIdle(p))
property point anchorPos: Qt.point(0, 0)
property bool isRightEdge: false
property var targetWindow: null
property rect availableBounds: Qt.rect(0, 0, 0, 0)
function clampY(rawY, panelHeight) {
if (availableBounds.height <= 0)
return rawY;
const margin = Theme.spacingS;
const minY = availableBounds.y + margin;
const maxY = availableBounds.y + availableBounds.height - panelHeight - margin;
return Math.max(minY, Math.min(maxY, rawY));
}
function clampX(rawX, panelWidth) {
if (availableBounds.width <= 0)
return rawX;
const margin = Theme.spacingS;
const minX = availableBounds.x + margin;
const maxX = availableBounds.x + availableBounds.width - panelWidth - margin;
return Math.max(minX, Math.min(maxX, rawX));
}
property bool __isChromeBrowser: {
if (!activePlayer?.identity)
@@ -30,6 +49,8 @@ Item {
property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
property bool volumeAvailable: !!((activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio))
property var availableDevices: {
if (root.dropdownType !== 2)
return [];
const hidden = SessionData.hiddenOutputDeviceNames ?? [];
return Pipewire.nodes.values.filter(node => {
if (!node.audio || !node.isSink || node.isStream)
@@ -113,8 +134,8 @@ Item {
visible: dropdownType === 1 && volumeAvailable
width: 60
height: 180
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
x: clampX(isRightEdge ? anchorPos.x : anchorPos.x - width, width)
y: clampY(anchorPos.y - height / 2, height)
radius: Theme.cornerRadius * 2
color: Theme.floatingSurface
border.color: Theme.outlineStrong
@@ -257,8 +278,8 @@ Item {
visible: dropdownType === 2
width: 280
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
x: clampX(isRightEdge ? anchorPos.x : anchorPos.x - width, width)
y: clampY(anchorPos.y - height / 2, height)
radius: Theme.cornerRadius * 2
color: Theme.floatingSurface
border.color: Theme.outlineStrong
@@ -462,8 +483,8 @@ Item {
visible: dropdownType === 3
width: 240
height: Math.max(180, Math.min(240, (root.selectablePlayers?.length || 0) * 50 + 80))
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
x: clampX(isRightEdge ? anchorPos.x : anchorPos.x - width, width)
y: clampY(anchorPos.y - height / 2, height)
radius: Theme.cornerRadius * 2
color: Theme.floatingSurface
border.color: Theme.outlineStrong
@@ -0,0 +1,568 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Services.Mpris
import Quickshell.Widgets
import qs.Common
import qs.Modules.DankIsland.Activities
import qs.Services
import qs.Widgets
Item {
id: root
required property var player
property alias volumeButton: volumeButton
property alias playerSelectorButton: sourceButton
property alias audioDevicesButton: outputButton
implicitHeight: 352
readonly property var activePlayer: root.player.activePlayer
readonly property color accent: root.player.accent
readonly property color onAccent: root.player.onAccent
readonly property string artUrl: TrackArtService.resolvedArtUrl
readonly property string title: MprisController.stableTitle || I18n.tr("Unknown Track")
readonly property string artist: MprisController.stableArtist || I18n.tr("Unknown Artist")
readonly property string album: MprisController.stableAlbum || ""
Component {
id: artBlobComponent
MediaBlobHalo {
accentColor: root.accent
playing: root.activePlayer?.playbackState === MprisPlaybackState.Playing
}
}
function formatTime(seconds) {
if (!isFinite(seconds) || seconds < 0)
return "0:00";
const minutes = Math.floor(seconds / 60);
const remainder = Math.floor(seconds % 60);
return minutes + ":" + String(remainder).padStart(2, "0");
}
function volumeIcon() {
return root.player.getVolumeIcon();
}
function outputIcon() {
return root.player.getAudioDeviceIcon(AudioService.sink);
}
ClippingRectangle {
anchors {
fill: parent
margins: 1
}
radius: 30
color: Theme.surfaceContainerHigh
antialiasing: true
Image {
id: backdropImage
anchors.fill: parent
source: root.visible ? root.artUrl : ""
asynchronous: true
cache: true
fillMode: Image.PreserveAspectCrop
sourceSize: Qt.size(320, 320)
visible: backdropImage.status === Image.Ready
opacity: 0.62
}
Rectangle {
anchors.fill: parent
visible: backdropImage.status === Image.Ready
gradient: Gradient {
GradientStop {
position: 0
color: Theme.withAlpha(Theme.surfaceContainerHigh, 0.9)
}
GradientStop {
position: 0.4
color: Theme.withAlpha(Theme.surfaceContainerHigh, 0.46)
}
GradientStop {
position: 1
color: Theme.withAlpha(Theme.surfaceContainerHigh, 0.95)
}
}
}
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainerHigh
visible: backdropImage.status !== Image.Ready
}
Rectangle {
anchors.fill: parent
visible: backdropImage.status !== Image.Ready
gradient: Gradient {
GradientStop {
position: 0
color: Theme.withAlpha(root.accent, 0.16)
}
GradientStop {
position: 0.55
color: Theme.withAlpha(root.accent, 0.04)
}
GradientStop {
position: 1
color: "transparent"
}
}
}
DankIcon {
anchors.centerIn: parent
name: "music_note"
size: 120
color: Theme.withAlpha(root.accent, 0.10)
visible: backdropImage.status !== Image.Ready
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
visible: root.player.showNoPlayerNow
z: 2
DankIcon {
name: "music_note"
size: Theme.iconSize * 3
color: Theme.surfaceTextSecondary
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No Active Players")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceTextMedium
anchors.horizontalCenter: parent.horizontalCenter
}
}
Item {
id: cardBody
anchors {
fill: parent
margins: 18
}
visible: !root.player._noneAvailable && !root.player.showNoPlayerNow
Item {
id: headerRow
anchors {
left: parent.left
right: parent.right
top: parent.top
}
height: 132
clip: false
Item {
id: artCluster
anchors {
left: parent.left
top: parent.top
}
width: 120
height: 120
clip: false
z: 1
Loader {
anchors.centerIn: parent
width: 136
height: 136
z: 0
active: root.visible && root.player.live && SettingsData.audioVisualizerEnabled
sourceComponent: artBlobComponent
}
Rectangle {
id: artFrame
anchors.fill: parent
z: 1
radius: 30
color: "transparent"
border.width: 1
border.color: Theme.withAlpha(root.accent, 0.3)
ClippingRectangle {
anchors {
fill: parent
margins: 2
}
radius: 28
color: Theme.primaryContainer
antialiasing: true
DankIcon {
anchors.centerIn: parent
name: "music_note"
size: 46
color: Theme.primary
visible: artwork.status !== Image.Ready
}
Image {
id: artwork
anchors.fill: parent
source: root.visible ? root.artUrl : ""
asynchronous: true
cache: true
fillMode: Image.PreserveAspectCrop
sourceSize: Qt.size(240, 240)
visible: status === Image.Ready
}
}
}
}
Column {
anchors {
left: artCluster.right
leftMargin: Theme.spacingL
right: parent.right
top: parent.top
topMargin: 2
}
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: root.title
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeXLarge
font.weight: Font.Bold
elide: Text.ElideRight
wrapMode: Text.WordWrap
maximumLineCount: 2
lineHeight: 1.08
}
StyledText {
width: parent.width
text: root.artist
color: Theme.surfaceTextMedium
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
width: parent.width
text: root.album
color: Theme.surfaceTextSecondary
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
}
}
}
Item {
id: iconRail
anchors {
right: parent.right
top: headerRow.bottom
topMargin: Theme.spacingL
bottom: parent.bottom
}
width: 44
z: 4
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
TonalIconButton {
id: volumeButton
width: 44
height: 44
radius: 22
iconName: root.volumeIcon()
iconSize: 20
iconColor: root.player.volumeAvailable && root.player.currentVolume > 0 ? root.accent : Theme.surfaceTextMedium
containerColor: Theme.withAlpha(Theme.surfaceText, 0.08)
interactive: root.player.volumeAvailable
onTriggered: root.player.toggleMute()
onScrolled: notches => root.player.adjustVolume(notches * AudioService.wheelVolumeStep)
HoverHandler {
enabled: root.player.menusEnabled && root.player.volumeAvailable
onHoveredChanged: {
if (hovered)
root.player.triggerVolumeDropdown();
else
root.player.dropdownButtonExited();
}
}
}
TonalIconButton {
id: outputButton
width: 44
height: 44
radius: 22
iconName: root.outputIcon()
iconSize: 20
iconColor: Theme.surfaceText
containerColor: Theme.withAlpha(Theme.surfaceText, 0.08)
onTriggered: root.player.cycleNextSink()
onScrolled: notches => AudioService.cycleAudioOutputDirection(notches < 0)
HoverHandler {
enabled: root.player.menusEnabled
onHoveredChanged: {
if (hovered)
root.player.triggerDevicesDropdown();
else
root.player.dropdownButtonExited();
}
}
}
TonalIconButton {
id: sourceButton
width: 44
height: 44
radius: 22
visible: (root.player.allPlayers?.length || 0) > 0
iconName: "assistant_device"
iconSize: 20
iconColor: root.player.playersExpanded ? root.accent : Theme.surfaceText
containerColor: Theme.withAlpha(root.accent, root.player.playersExpanded ? 0.16 : 0.08)
onTriggered: {
if (root.player.playersExpanded) {
root.player.cycleNextPlayer();
return;
}
root.player.triggerPlayersDropdown();
}
HoverHandler {
enabled: root.player.menusEnabled && sourceButton.visible
onHoveredChanged: {
if (hovered)
root.player.triggerPlayersDropdown();
else
root.player.dropdownButtonExited();
}
}
}
}
}
Item {
id: playColumn
anchors {
left: parent.left
right: iconRail.left
rightMargin: Theme.spacingM
top: headerRow.bottom
topMargin: Theme.spacingL
bottom: parent.bottom
}
Item {
id: seekBlock
anchors {
left: parent.left
leftMargin: iconRail.width + Theme.spacingM
right: parent.right
bottom: parent.bottom
bottomMargin: 76
}
height: 40
DankSeekbar {
anchors.top: parent.top
width: parent.width
height: 22
activePlayer: root.activePlayer
stableLength: root.player.stableLength
accentColor: root.accent
accentTrackColor: MediaAccentService.accentTrack
accentSubtleColor: MediaAccentService.accentSubtle
isSeeking: root.player.isSeeking
onIsSeekingChanged: root.player.isSeeking = isSeeking
}
StyledText {
anchors {
left: parent.left
bottom: parent.bottom
}
text: root.formatTime(root.activePlayer?.position || 0)
color: root.accent
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
}
StyledText {
anchors {
right: parent.right
bottom: parent.bottom
}
text: root.player.stableLength > 0 ? root.formatTime(root.player.stableLength) : "--:--"
color: Theme.surfaceTextSecondary
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
}
Row {
anchors {
horizontalCenter: parent.horizontalCenter
horizontalCenterOffset: (iconRail.width + Theme.spacingM) / 2
bottom: parent.bottom
bottomMargin: 2
}
spacing: Theme.spacingS
TonalIconButton {
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 40
radius: 20
visible: !!root.activePlayer?.shuffleSupported
iconName: "shuffle"
iconSize: 18
iconColor: root.activePlayer?.shuffle ? root.accent : Theme.surfaceText
containerColor: Theme.withAlpha(root.accent, root.activePlayer?.shuffle ? 0.16 : 0)
onTriggered: {
if (root.activePlayer?.canControl && root.activePlayer?.shuffleSupported)
root.activePlayer.shuffle = !root.activePlayer.shuffle;
}
}
TonalIconButton {
anchors.verticalCenter: parent.verticalCenter
width: 52
height: 52
radius: 18
iconName: "skip_previous"
iconSize: 26
containerColor: Theme.withAlpha(root.accent, 0.14)
interactive: !!root.activePlayer?.canGoPrevious || !!root.activePlayer?.canSeek
onTriggered: MprisController.previousOrRewind()
}
Rectangle {
id: playHero
anchors.verticalCenter: parent.verticalCenter
width: 68
height: 68
radius: root.activePlayer?.playbackState === MprisPlaybackState.Playing ? 22 : 34
color: root.accent
opacity: root.activePlayer?.canTogglePlaying ? 1 : 0.38
DankIcon {
anchors.centerIn: parent
name: root.activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 34
color: root.onAccent
weight: 500
}
StateLayer {
stateColor: root.onAccent
disabled: !root.activePlayer || !root.activePlayer.canTogglePlaying
onClicked: root.activePlayer.togglePlaying()
}
}
TonalIconButton {
anchors.verticalCenter: parent.verticalCenter
width: 52
height: 52
radius: 18
iconName: "skip_next"
iconSize: 26
containerColor: Theme.withAlpha(root.accent, 0.14)
interactive: !!root.activePlayer?.canGoNext
onTriggered: MprisController.next()
}
TonalIconButton {
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 40
radius: 20
visible: !!root.activePlayer?.loopSupported
iconName: {
if (!root.activePlayer)
return "repeat";
return root.activePlayer.loopState === MprisLoopState.Track ? "repeat_one" : "repeat";
}
iconSize: 18
iconColor: root.activePlayer && root.activePlayer.loopState !== MprisLoopState.None ? root.accent : Theme.surfaceText
containerColor: Theme.withAlpha(root.accent, root.activePlayer && root.activePlayer.loopState !== MprisLoopState.None ? 0.16 : 0)
onTriggered: root.player.cycleLoopState()
}
}
}
}
component TonalIconButton: Rectangle {
id: tonal
property string iconName: ""
property real iconSize: 22
property color iconColor: Theme.surfaceText
property color containerColor: Theme.withAlpha(Theme.surfaceText, 0.08)
property bool interactive: true
signal triggered
signal scrolled(int notches)
color: tonal.containerColor
opacity: tonal.interactive ? 1 : 0.38
DankIcon {
anchors.centerIn: parent
name: tonal.iconName
size: tonal.iconSize
color: tonal.iconColor
}
StateLayer {
stateColor: tonal.iconColor
disabled: !tonal.interactive
enabled: tonal.interactive
onClicked: tonal.triggered()
onWheel: wheelEvent => {
wheelEvent.accepted = true;
tonal.scrolled(wheelEvent.angleDelta.y > 0 ? 1 : -1);
}
}
}
}
+142 -15
View File
@@ -14,7 +14,6 @@ Item {
LayoutMirroring.childrenInherit: true
property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool windowVisible: Window.window?.visible ?? false
readonly property real stableLength: MprisController.activePlayerStableLength
property var allPlayers: MprisController.availablePlayers
property var targetScreen: null
@@ -25,6 +24,10 @@ Item {
property real contentOffsetY: 0
property string section: ""
property int barPosition: SettingsData.Position.Top
property string chrome: "dash"
property bool live: Window.window?.visible ?? false
property bool menusEnabled: true
readonly property bool islandChrome: chrome === "island"
readonly property color accent: MediaAccentService.accent
readonly property color onAccent: MediaAccentService.onAccent
@@ -41,6 +44,7 @@ Item {
property bool volumeExpanded: false
property bool devicesExpanded: false
property bool playersExpanded: false
property real previousVolume: 0.0
function resetDropdownStates() {
volumeExpanded = false;
@@ -80,6 +84,13 @@ Item {
onTriggered: _switchHold = false
}
onMenusEnabledChanged: {
if (!root.menusEnabled) {
resetDropdownStates();
hideDropdowns();
}
}
onActivePlayerChanged: {
if (!activePlayer) {
isSwitching = false;
@@ -109,7 +120,7 @@ Item {
}
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2
implicitHeight: chromeLoader.item ? chromeLoader.item.implicitHeight : 410
Connections {
target: activePlayer
@@ -184,18 +195,109 @@ Item {
}
}
function dropdownAnchor(button) {
if (!button)
return null;
if (root.islandChrome) {
const rightEdge = root.mapToItem(null, root.width, 0).x;
const point = button.mapToItem(null, 0, button.height / 2);
return {
"pos": Qt.point(rightEdge + Theme.spacingS, point.y),
"rightEdge": true
};
}
const buttonsOnRight = !root.isRightEdge;
const btnY = button.mapToItem(root, 0, button.height / 2).y;
return {
"pos": Qt.point(buttonsOnRight ? (root.popoutX + root.popoutWidth) : root.popoutX, root.popoutY + root.contentOffsetY + btnY),
"rightEdge": buttonsOnRight
};
}
function triggerVolumeDropdown() {
if (!volumeAvailable)
if (!root.menusEnabled || !volumeAvailable)
return;
if (volumeExpanded)
return;
const button = chromeLoader.item?.volumeButton;
const anchor = dropdownAnchor(button);
if (!anchor)
return;
hideDropdowns();
volumeExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = volumeButton.y + volumeButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showVolumeDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
showVolumeDropdown(anchor.pos, targetScreen, anchor.rightEdge, activePlayer, allPlayers);
}
function triggerPlayersDropdown() {
if (!root.menusEnabled)
return;
const button = chromeLoader.item?.playerSelectorButton;
const anchor = dropdownAnchor(button);
if (!anchor)
return;
if (playersExpanded)
return;
hideDropdowns();
playersExpanded = true;
showPlayersDropdown(anchor.pos, targetScreen, anchor.rightEdge, activePlayer, allPlayers);
}
function triggerDevicesDropdown() {
if (!root.menusEnabled)
return;
const button = chromeLoader.item?.audioDevicesButton;
const anchor = dropdownAnchor(button);
if (!anchor)
return;
if (devicesExpanded)
return;
hideDropdowns();
devicesExpanded = true;
showAudioDevicesDropdown(anchor.pos, targetScreen, anchor.rightEdge);
}
function cycleNextPlayer() {
const players = (root.allPlayers || []).filter(p => p && !MprisController.isIdle(p));
if (players.length < 2)
return;
let currentIndex = -1;
for (let i = 0; i < players.length; i++) {
if (players[i] === root.activePlayer) {
currentIndex = i;
break;
}
}
MprisController.setActivePlayer(players[(currentIndex + 1) % players.length]);
}
function cycleNextSink() {
const sinks = AudioService.getAvailableSinks();
if (!sinks || sinks.length < 2)
return;
let currentIndex = -1;
for (let i = 0; i < sinks.length; i++) {
if (sinks[i]?.name === AudioService.sink?.name) {
currentIndex = i;
break;
}
}
AudioService.setSink(sinks[(currentIndex + 1) % sinks.length]);
}
function cycleLoopState() {
if (!activePlayer?.canControl || !activePlayer.loopSupported)
return;
switch (activePlayer.loopState) {
case MprisLoopState.None:
activePlayer.loopState = MprisLoopState.Playlist;
break;
case MprisLoopState.Playlist:
activePlayer.loopState = MprisLoopState.Track;
break;
case MprisLoopState.Track:
activePlayer.loopState = MprisLoopState.None;
break;
}
}
function toggleMute() {
@@ -203,14 +305,14 @@ Item {
return;
SessionData.suppressOSDTemporarily();
if (currentVolume > 0) {
volumeButton.previousVolume = currentVolume;
root.previousVolume = currentVolume;
if (usePlayerVolume) {
activePlayer.volume = 0;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = 0;
}
} else {
const restoreVolume = volumeButton.previousVolume > 0 ? volumeButton.previousVolume : 0.5;
const restoreVolume = root.previousVolume > 0 ? root.previousVolume : 0.5;
if (usePlayerVolume) {
activePlayer.volume = restoreVolume;
} else if (AudioService.sink?.audio) {
@@ -284,13 +386,38 @@ Item {
Timer {
interval: 1000
running: root.windowVisible && activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
running: root.live && activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
repeat: true
onTriggered: activePlayer?.positionChanged()
}
Item {
id: bgContainer
Loader {
id: chromeLoader
anchors.fill: parent
sourceComponent: root.islandChrome ? islandChromeComponent : dashChromeComponent
}
Component {
id: islandChromeComponent
MediaPlayerIslandChrome {
player: root
}
}
Component {
id: dashChromeComponent
Item {
anchors.fill: parent
implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2
property alias volumeButton: volumeButton
property alias playerSelectorButton: playerSelectorButton
property alias audioDevicesButton: audioDevicesButton
Item {
id: bgContainer
anchors.fill: parent
// Fall back to the live mpris url so the background is never blank.
@@ -860,8 +987,6 @@ Item {
z: 101
enabled: volumeAvailable
property real previousVolume: 0.0
DankIcon {
anchors.centerIn: parent
name: getVolumeIcon()
@@ -991,5 +1116,7 @@ Item {
dropdownButtonExited();
}
}
}
}
}
}
@@ -7,6 +7,8 @@ import qs.Widgets
Card {
id: root
property bool live: Window.window?.visible ?? false
Column {
anchors.centerIn: parent
spacing: 0
@@ -110,13 +112,16 @@ Card {
SystemClock {
id: systemClock
precision: SettingsData.showSeconds ? SystemClock.Seconds : SystemClock.Minutes
enabled: root.live
precision: root.live && SettingsData.showSeconds ? SystemClock.Seconds : SystemClock.Minutes
}
Connections {
target: SessionService
function onSessionResumed() {
if (!root.live)
return;
systemClock.enabled = false;
systemClock.enabled = true;
}
@@ -11,11 +11,11 @@ Card {
signal clicked
property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool windowVisible: Window.window?.visible ?? false
property bool live: Window.window?.visible ?? false
Timer {
interval: 300
running: root.windowVisible && activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
running: root.live && activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
repeat: true
onTriggered: activePlayer?.positionSupported && activePlayer.positionChanged()
}
@@ -62,7 +62,7 @@ Card {
artUrl: TrackArtService.resolvedArtUrl
accentColor: MediaAccentService.accent
cavaService: CavaService
showAnimation: SettingsData.audioVisualizerEnabled
showAnimation: root.live && SettingsData.audioVisualizerEnabled
albumSize: 76
animationScale: 1.05
}
@@ -6,7 +6,7 @@ import qs.Widgets
Card {
id: root
readonly property bool windowVisible: Window.window?.visible ?? false
property bool live: Window.window?.visible ?? false
property bool dgopRefHeld: false
function syncDgopRef(wanted) {
@@ -20,8 +20,8 @@ Card {
DgopService.removeRef(["cpu", "memory", "system"]);
}
onWindowVisibleChanged: syncDgopRef(windowVisible)
Component.onCompleted: syncDgopRef(windowVisible)
onLiveChanged: syncDgopRef(live)
Component.onCompleted: syncDgopRef(live)
Component.onDestruction: syncDgopRef(false)
Row {
@@ -9,8 +9,23 @@ Card {
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
Component.onCompleted: DgopService.addRef("system")
Component.onDestruction: DgopService.removeRef("system")
property bool live: Window.window?.visible ?? false
property bool dgopRefHeld: false
function syncDgopRef(wanted) {
if (wanted === dgopRefHeld)
return;
dgopRefHeld = wanted;
if (wanted) {
DgopService.addRef("system");
return;
}
DgopService.removeRef("system");
}
onLiveChanged: syncDgopRef(live)
Component.onCompleted: syncDgopRef(live)
Component.onDestruction: syncDgopRef(false)
Row {
anchors.left: parent.left
@@ -11,8 +11,23 @@ Card {
signal clicked
Component.onCompleted: WeatherService.addRef()
Component.onDestruction: WeatherService.removeRef()
property bool live: Window.window?.visible ?? false
property bool weatherRefHeld: false
function syncWeatherRef(wanted) {
if (wanted === weatherRefHeld)
return;
weatherRefHeld = wanted;
if (wanted) {
WeatherService.addRef();
return;
}
WeatherService.removeRef();
}
onLiveChanged: syncWeatherRef(live)
Component.onCompleted: syncWeatherRef(live)
Component.onDestruction: syncWeatherRef(false)
Column {
anchors.centerIn: parent
@@ -11,6 +11,7 @@ Item {
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
implicitHeight: 410
property bool live: Window.window?.visible ?? false
signal switchToWeatherTab
signal switchToMediaTab
@@ -29,6 +30,7 @@ Item {
y: 0
width: parent.width * 0.2 - Theme.spacingM * 2
height: 180
live: root.live
}
// Weather - top middle-left (narrower)
@@ -38,6 +40,7 @@ Item {
width: SettingsData.weatherEnabled ? parent.width * 0.3 : 0
height: 100
visible: SettingsData.weatherEnabled
live: root.live
onClicked: root.switchToWeatherTab()
}
@@ -48,6 +51,7 @@ Item {
y: 0
width: SettingsData.weatherEnabled ? parent.width * 0.5 : parent.width * 0.8
height: 100
live: root.live
}
// SystemMonitor - middle left (narrow and shorter)
@@ -56,6 +60,7 @@ Item {
y: 180 + Theme.spacingM
width: parent.width * 0.2 - Theme.spacingM * 2
height: 220
live: root.live
}
// Calendar - bottom middle; deferred so the grid stays off the emerge frame.
@@ -86,6 +91,7 @@ Item {
y: 100 + Theme.spacingM
width: parent.width * 0.2
height: 300
live: root.live
onClicked: root.switchToMediaTab()
}
+87 -42
View File
@@ -40,6 +40,7 @@ Item {
property bool sortAscending: true
// Forces the page grid to rebuild when the folder model reorders in place.
property int gridRevision: 0
property int pagerCachePages: 1
signal requestTabChange(int newIndex)
@@ -64,15 +65,18 @@ Item {
return Math.max(0, Math.min(itemsPerPage, wallpaperCount - page * itemsPerPage));
}
function matchesSearch(fileName, filePath) {
function searchTerms() {
const query = searchQuery.trim().toLowerCase();
if (!query)
return query ? query.split(/\s+/).filter(t => t.length > 0) : [];
}
function matchesTerms(terms, fileName, filePath) {
if (terms.length === 0)
return true;
const haystack = (fileName + " " + filePath).toLowerCase();
const terms = query.split(/\s+/);
for (let i = 0; i < terms.length; i++) {
if (terms[i] && haystack.indexOf(terms[i]) === -1)
if (haystack.indexOf(terms[i]) === -1)
return false;
}
return true;
@@ -81,27 +85,56 @@ Item {
function rebuildWallpaperList(preferCurrentWallpaper) {
const paths = [];
if (wallpaperFolderModel.status === FolderListModel.Ready) {
for (let i = 0; i < wallpaperFolderModel.count; i++) {
const terms = searchTerms();
const rowCount = wallpaperFolderModel.count;
for (let i = 0; i < rowCount; i++) {
const filePath = cleanFilePath(wallpaperFolderModel.get(i, "filePath"));
const fileName = wallpaperFolderModel.get(i, "fileName") || filePath.substring(filePath.lastIndexOf('/') + 1);
if (filePath && matchesSearch(fileName, filePath))
paths.push(filePath);
if (!filePath)
continue;
if (terms.length > 0) {
const fileName = wallpaperFolderModel.get(i, "fileName") || filePath.substring(filePath.lastIndexOf('/') + 1);
if (!matchesTerms(terms, fileName, filePath))
continue;
}
paths.push(filePath);
}
}
const selectCurrent = preferCurrentWallpaper && visible && active;
if (selectCurrent) {
enableAnimation = false;
const currentWallpaper = getCurrentWallpaper();
let matched = false;
if (currentWallpaper) {
for (let i = 0; i < paths.length; i++) {
if (paths[i] === currentWallpaper) {
currentPage = Math.floor(i / itemsPerPage);
gridIndex = i % itemsPerPage;
matched = true;
break;
}
}
}
if (!matched) {
currentPage = 0;
gridIndex = 0;
}
} else {
const nextTotal = Math.max(1, Math.ceil(paths.length / itemsPerPage));
if (currentPage >= nextTotal)
currentPage = Math.max(0, nextTotal - 1);
const visibleCount = Math.max(0, Math.min(itemsPerPage, paths.length - currentPage * itemsPerPage));
gridIndex = visibleCount > 0 ? Math.min(gridIndex, visibleCount - 1) : 0;
}
filteredWallpaperPaths = paths;
gridRevision++;
if (currentPage >= totalPages)
currentPage = Math.max(0, totalPages - 1);
const visibleCount = pageItemCount(currentPage);
gridIndex = visibleCount > 0 ? Math.min(gridIndex, visibleCount - 1) : 0;
if (preferCurrentWallpaper && visible && active)
setInitialSelection();
else
updateSelectedFileName();
updateSelectedFileName();
if (selectCurrent) {
Qt.callLater(() => {
enableAnimation = true;
});
}
}
function focusSearch() {
@@ -514,27 +547,31 @@ Item {
}
}
FileBrowserSurfaceModal {
id: wallpaperBrowser
Loader {
id: wallpaperBrowserLoader
browserTitle: I18n.tr("Select Wallpaper Directory", "wallpaper directory file browser title")
browserIcon: "folder_open"
browserType: "wallpaper"
showHiddenFiles: false
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
parentPopout: root.parentPopout
active: false
onFileSelected: path => {
const cleanPath = path.replace(/^file:\/\//, '');
setCurrentWallpaper(cleanPath);
sourceComponent: FileBrowserSurfaceModal {
browserTitle: I18n.tr("Select Wallpaper Directory", "wallpaper directory file browser title")
browserIcon: "folder_open"
browserType: "wallpaper"
showHiddenFiles: false
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
parentPopout: root.parentPopout
const dirPath = cleanPath.substring(0, cleanPath.lastIndexOf('/'));
if (dirPath) {
wallpaperDir = dirPath;
CacheData.wallpaperLastPath = dirPath;
CacheData.saveCache();
onFileSelected: path => {
const cleanPath = path.replace(/^file:\/\//, '');
root.setCurrentWallpaper(cleanPath);
const dirPath = cleanPath.substring(0, cleanPath.lastIndexOf('/'));
if (dirPath) {
root.wallpaperDir = dirPath;
CacheData.wallpaperLastPath = dirPath;
CacheData.saveCache();
}
close();
}
close();
}
}
@@ -565,9 +602,14 @@ Item {
keyNavigationEnabled: false
activeFocusOnTab: false
focus: false
cacheBuffer: Math.max(0, height * 2)
reuseItems: true
model: root.totalPages
cacheBuffer: Math.max(0, height * root.pagerCachePages)
reuseItems: false
model: height > 1 ? root.totalPages : 0
onCountChanged: {
if (count > 0 && currentIndex !== root.currentPage)
currentIndex = root.currentPage;
}
onCurrentIndexChanged: {
if (!moving) {
@@ -595,7 +637,7 @@ Item {
property int pageIndex: index
width: pager.width
height: pager.height
height: Math.max(1, pager.height)
cellWidth: width / 4
cellHeight: height / 4
interactive: false
@@ -811,7 +853,7 @@ Item {
DankActionButton {
id: sortButton
anchors.verticalCenter: parent.verticalCenter
iconName: "sort"
iconName: "filter_list"
iconSize: 20
buttonSize: 32
opacity: 0.7
@@ -833,7 +875,10 @@ Item {
opacity: 0.7
tooltipText: I18n.tr("Choose wallpaper folder")
tooltipSide: "top"
onClicked: wallpaperBrowser.open()
onClicked: {
wallpaperBrowserLoader.active = true;
wallpaperBrowserLoader.item.open();
}
}
Item {
+19 -4
View File
@@ -13,12 +13,26 @@ Item {
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
implicitHeight: 410
property bool live: Window.window?.visible ?? false
property bool weatherRefHeld: false
property bool syncing: false
property bool showHourly: false
property bool available: WeatherService.weather.available
Component.onCompleted: WeatherService.addRef()
Component.onDestruction: WeatherService.removeRef()
function syncWeatherRef(wanted) {
if (wanted === weatherRefHeld)
return;
weatherRefHeld = wanted;
if (wanted) {
WeatherService.addRef();
return;
}
WeatherService.removeRef();
}
onLiveChanged: syncWeatherRef(live)
Component.onCompleted: syncWeatherRef(live)
Component.onDestruction: syncWeatherRef(false)
function syncFrom(type) {
if (!dailyLoader.item || !hourlyLoader.item)
@@ -530,6 +544,7 @@ Item {
Loader {
anchors.fill: parent
active: root.live
asynchronous: true
sourceComponent: skyContentComponent
opacity: status === Loader.Ready ? 1 : 0
@@ -871,7 +886,7 @@ Item {
id: dailyLoader
anchors.fill: parent
sourceComponent: dailyComponent
active: root.visible && root.available && width > 0 && height > 0
active: root.live && root.available && width > 0 && height > 0
visible: !root.showHourly
asynchronous: true
opacity: 0
@@ -888,7 +903,7 @@ Item {
id: hourlyLoader
anchors.fill: parent
sourceComponent: hourlyComponent
active: root.visible && root.available && width > 0 && height > 0
active: root.live && root.available && width > 0 && height > 0
visible: root.showHourly
asynchronous: true
opacity: 0
@@ -0,0 +1,139 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool hovered: false
property bool outlined: false
readonly property real thickness: 14
readonly property real bodyWidth: 30
readonly property real level: Math.max(0, Math.min(100, BatteryService.batteryLevel))
readonly property bool charging: BatteryService.batteryAvailable && BatteryService.isCharging
readonly property bool lowState: BatteryService.batteryAvailable && BatteryService.isLowBattery && !BatteryService.isCharging
readonly property color fillColor: root.lowState ? Theme.error : Theme.primary
readonly property color onFillColor: {
const c = root.fillColor;
const lum = 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
return lum > 0.5 ? Qt.rgba(0, 0, 0, 0.9) : Qt.rgba(1, 1, 1, 0.95);
}
readonly property string numberText: Math.round(root.level).toString()
readonly property int glyphSize: Math.round(root.thickness * 0.62)
readonly property int boltSize: Math.round(root.thickness * 0.7)
readonly property real nubBreadth: Math.round(root.thickness * 0.16)
readonly property real nubSpan: Math.round(root.thickness * 0.46)
width: root.bodyWidth + root.nubBreadth
height: root.thickness
component PillGlyphs: Row {
id: glyphs
property color glyphColor
spacing: 2
DankIcon {
anchors.verticalCenter: parent.verticalCenter
visible: root.charging
name: "bolt"
size: root.boltSize
color: glyphs.glyphColor
}
StyledText {
id: numberGlyph
anchors.verticalCenter: parent.verticalCenter
// Digits have no descenders; sink by half the descent to optically center.
anchors.verticalCenterOffset: Math.round(digitMetrics.descent / 2)
text: root.numberText
color: glyphs.glyphColor
font.pixelSize: root.glyphSize
font.weight: Font.Bold
}
FontMetrics {
id: digitMetrics
font: numberGlyph.font
}
}
Rectangle {
id: body
anchors.left: parent.left
width: root.bodyWidth
height: root.thickness
radius: Math.round(Math.min(width, height) * 0.34)
color: root.outlined ? (root.hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.45) : "transparent") : (root.hovered ? Theme.surfaceVariant : Theme.withAlpha(Theme.surfaceVariant, 0.9))
border.width: root.outlined ? 1.2 : 0
border.color: root.fillColor
Rectangle {
id: fill
anchors {
left: parent.left
bottom: parent.bottom
top: parent.top
leftMargin: body.border.width
bottomMargin: body.border.width
topMargin: body.border.width
}
width: Math.round((body.width - body.border.width * 2) * root.level / 100)
radius: Math.max(0, body.radius - body.border.width)
color: root.outlined ? Theme.withAlpha(root.fillColor, 0.32) : root.fillColor
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
PillGlyphs {
anchors.centerIn: parent
glyphColor: Theme.surfaceText
}
Item {
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
}
width: fill.width
clip: true
visible: !root.outlined
Item {
width: body.width
height: body.height
PillGlyphs {
anchors.centerIn: parent
glyphColor: root.onFillColor
}
}
}
}
Rectangle {
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
}
width: root.nubBreadth
height: root.nubSpan
radius: Math.round(root.nubBreadth * 0.35)
color: root.outlined ? root.fillColor : Theme.withAlpha(Theme.surfaceVariant, 0.9)
}
}
@@ -0,0 +1,41 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: root
required property var controller
function pushMeasuredWidth() {
root.controller.setDestinationContentWidth("controlcenter", compactRow.implicitWidth);
}
Component.onCompleted: root.pushMeasuredWidth()
Row {
id: compactRow
anchors.centerIn: parent
spacing: Theme.spacingS
onImplicitWidthChanged: root.pushMeasuredWidth()
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "tune"
size: Theme.iconSizeSmall
color: Theme.primary
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Control Center")
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
}
}
}
@@ -0,0 +1,175 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.ControlCenter
import qs.Services
// Shared Control Center body hosted in the island silhouette.
FocusScope {
id: root
required property var islandController
property var effectiveScreen: null
property var colorPickerModal: null
property var powerMenuModalLoader: null
property real alignedX: 0
property real alignedY: 0
property real alignedWidth: 0
property real alignedHeight: 0
// Report content height once per turn; the controller clamps it before retargeting the spring.
property real bottomInset: Theme.spacingM
property bool _heightReportPending: false
signal lockRequested
clip: true
function resetState() {
hostContract.editMode = false;
hostContract.expandedSection = "";
hostContract.expandedWidgetIndex = -1;
hostContract.expandedWidgetData = null;
}
function beginSession() {
hostContract.editMode = false;
hostContract.expandedSection = root.islandController.controlCenterPendingSection || "";
hostContract.expandedWidgetIndex = -1;
hostContract.expandedWidgetData = null;
root.queueHeightReport();
}
function queueHeightReport() {
if (root._heightReportPending)
return;
root._heightReportPending = true;
Qt.callLater(() => {
root._heightReportPending = false;
root.islandController.setControlCenterContentHeight(content.targetImplicitHeight + Theme.spacingXS + root.bottomInset);
});
}
QtObject {
id: hostContract
property bool editMode: false
property string expandedSection: ""
property int expandedWidgetIndex: -1
property var expandedWidgetData: null
readonly property bool shouldBeVisible: root.islandController.activeActivity === "controlcenter" && root.islandController.expanded
// The island has no outside chrome to click, so the header row closes it.
readonly property bool headerTogglesClose: true
readonly property bool powerMenuOpen: root.powerMenuModalLoader?.item?.shouldBeVisible ?? false
readonly property var screen: root.effectiveScreen
readonly property var triggerScreen: root.effectiveScreen
readonly property var colorPickerModal: root.colorPickerModal
readonly property var powerMenuModalLoader: root.powerMenuModalLoader
readonly property real alignedX: root.alignedX
readonly property real alignedY: root.alignedY
readonly property real alignedWidth: root.alignedWidth
readonly property real alignedHeight: root.alignedHeight
readonly property real popupWidth: root.alignedWidth
readonly property real popupHeight: root.alignedHeight
signal lockRequested
function close() {
root.islandController.requestCollapse();
}
function collapseAll() {
hostContract.expandedSection = "";
hostContract.expandedWidgetIndex = -1;
hostContract.expandedWidgetData = null;
}
function toggleSection(section) {
if (hostContract.expandedSection === section) {
hostContract.expandedSection = "";
hostContract.expandedWidgetIndex = -1;
return;
}
hostContract.expandedSection = section;
}
}
function releaseScanState() {
if (NetworkService.activeService)
NetworkService.activeService.autoRefreshEnabled = false;
if (BluetoothService.adapter && BluetoothService.adapter.discovering)
BluetoothService.adapter.discovering = false;
}
function applyPresentationLifecycle() {
if (hostContract.shouldBeVisible) {
if (NetworkService.activeService)
NetworkService.activeService.autoRefreshEnabled = NetworkService.wifiEnabled;
return;
}
root.releaseScanState();
}
// Hot-unplug skips the visibility path; release scans here too.
Component.onDestruction: {
if (hostContract.shouldBeVisible)
root.releaseScanState();
}
Connections {
target: hostContract
function onLockRequested() {
root.lockRequested();
}
function onShouldBeVisibleChanged() {
Qt.callLater(root.applyPresentationLifecycle);
}
}
Connections {
target: NetworkService
function onCredentialsRequestedChanged() {
if (NetworkService.credentialsRequested && hostContract.shouldBeVisible)
root.islandController.requestCollapse();
}
}
ControlCenterContent {
id: content
anchors {
fill: parent
leftMargin: Theme.spacingXS
rightMargin: Theme.spacingXS
topMargin: Theme.spacingXS
bottomMargin: root.bottomInset
}
host: hostContract
onTargetImplicitHeightChanged: root.queueHeightReport()
}
Connections {
target: root.islandController
function onControlCenterSessionSerialChanged() {
Qt.callLater(root.beginSession);
}
function onExpandedChanged() {
if (!root.islandController.expanded)
root.resetState();
}
function onActiveActivityChanged() {
if (root.islandController.activeActivity !== "controlcenter")
root.resetState();
}
}
Component.onCompleted: root.islandController.markControlCenterVisualsReady()
}
@@ -0,0 +1,289 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
required property var controller
required property var mediaModel
readonly property string timeText: systemClock.date.toLocaleTimeString(I18n.locale(), SettingsData.getEffectiveTimeFormat())
readonly property string dateText: systemClock.date.toLocaleDateString(I18n.locale(), SettingsData.getEffectiveDateFormat("ddd MMM d"))
readonly property bool tight: root.controller.homeCompactTight
readonly property real slotSize: root.tight ? 24 : 32
readonly property real slotSpacing: root.controller.homeSlotSpacing
readonly property bool weatherSlotEnabled: root.controller.homeWeatherSlot !== "hidden"
readonly property var leftActionIds: homeActionsForSide("left")
readonly property var rightActionIds: homeActionsForSide("right")
property bool weatherRefHeld: false
function homeActionsForSide(side) {
const ids = [];
if (root.controller.homeMediaSlot === side)
ids.push("media");
if (root.controller.homeWeatherSlot === side)
ids.push("weather");
if (root.controller.homeStatusSlot === side)
ids.push("status");
return ids;
}
function syncWeatherRef(wanted) {
if (wanted === weatherRefHeld)
return;
weatherRefHeld = wanted;
if (wanted) {
WeatherService.addRef();
return;
}
WeatherService.removeRef();
}
onWeatherSlotEnabledChanged: root.syncWeatherRef(root.weatherSlotEnabled)
Component.onCompleted: {
root.syncWeatherRef(root.weatherSlotEnabled);
root.controller.setHomeContentWidth(compactRow.implicitWidth);
}
Component.onDestruction: root.syncWeatherRef(false)
Connections {
target: compactRow
function onImplicitWidthChanged() {
root.controller.setHomeContentWidth(compactRow.implicitWidth);
}
}
SystemClock {
id: systemClock
precision: SettingsData.showSeconds ? SystemClock.Seconds : SystemClock.Minutes
}
component HomeActionSlot: Item {
id: slot
required property string actionId
required property var controller
required property var mediaModel
property real slotSize: 32
property bool tight: false
readonly property bool isMedia: actionId === "media"
readonly property bool isStatus: actionId === "status"
readonly property bool isWeather: actionId === "weather"
readonly property bool usesBattery: isStatus && BatteryService.batteryAvailable
readonly property string weatherTempText: {
if (!WeatherService.weather.available)
return "--°";
const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp;
return Math.round(Number(temp)) + "°";
}
implicitWidth: {
if (usesBattery)
return statusPill.width + 8;
if (isWeather)
return weatherRow.implicitWidth;
return slot.slotSize;
}
implicitHeight: slot.slotSize
width: implicitWidth
height: implicitHeight
Rectangle {
anchors.fill: parent
radius: height / 2
color: slotArea.containsMouse && !slot.usesBattery ? Theme.surfaceTextHover : "transparent"
}
MediaCava {
anchors.centerIn: parent
width: slot.tight ? 16 : 20
height: slot.tight ? 16 : 20
visible: slot.isMedia && slot.controller.mediaAvailable
mediaModel: slot.mediaModel
}
DankIcon {
anchors.centerIn: parent
visible: slot.isMedia && !slot.controller.mediaAvailable
name: "search"
size: slot.tight ? 16 : Theme.iconSizeSmall
color: Theme.surfaceTextMedium
}
BatteryPill {
id: statusPill
anchors.centerIn: parent
visible: slot.usesBattery
hovered: slotArea.containsMouse
outlined: SettingsData.dankIslandBatteryStyle === "outline"
}
DankIcon {
anchors.centerIn: parent
visible: slot.isStatus && !slot.usesBattery
name: "tune"
size: slot.tight ? 16 : Theme.iconSizeSmall
color: Theme.surfaceText
}
Row {
id: weatherRow
anchors.centerIn: parent
spacing: 2
visible: slot.isWeather
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: slot.tight ? 16 : Theme.iconSizeSmall
color: Theme.primary
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: slot.weatherTempText
color: Theme.surfaceTextSecondary
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
}
IslandSlotHoverArea {
id: slotArea
anchors.fill: parent
controller: slot.controller
onClicked: {
if (slot.isMedia) {
if (slot.controller.mediaAvailable)
slot.controller.requestActivity("media", false, false);
else
slot.controller.requestLauncher("", "", false);
return;
}
if (slot.isWeather) {
slot.controller.requestWeather(false);
return;
}
slot.controller.requestControlCenter("", false);
}
}
}
Row {
id: compactRow
anchors.centerIn: parent
spacing: root.controller.homeClusterGap
Row {
id: leftActions
anchors.verticalCenter: parent.verticalCenter
spacing: root.slotSpacing
visible: root.leftActionIds.length > 0
Repeater {
model: root.leftActionIds
HomeActionSlot {
required property var modelData
actionId: String(modelData)
controller: root.controller
mediaModel: root.mediaModel
slotSize: root.slotSize
tight: root.tight
}
}
}
Row {
id: clockRow
anchors.verticalCenter: parent.verticalCenter
spacing: root.tight ? Theme.spacingXS : Theme.spacingS
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.timeText
color: Theme.surfaceText
font.family: Theme.fontFamily
font.pixelSize: root.tight ? Theme.fontSizeSmall : Theme.fontSizeMedium
font.weight: Font.DemiBold
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: root.tight ? 3 : 4
height: root.tight ? 3 : 4
radius: width / 2
color: Theme.primary
}
Text {
id: dateLabel
anchors.verticalCenter: parent.verticalCenter
text: root.dateText
color: Theme.surfaceTextMedium
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
Item {
anchors.verticalCenter: parent.verticalCenter
visible: root.controller.homeNotificationBadge
width: visible ? 10 : 0
height: 16
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 1
width: 5
height: 5
radius: 2.5
color: Theme.secondary
}
IslandSlotHoverArea {
anchors.fill: parent
controller: root.controller
onClicked: root.controller.requestNotificationCenter(false)
}
}
}
Row {
id: rightActions
anchors.verticalCenter: parent.verticalCenter
spacing: root.slotSpacing
visible: root.rightActionIds.length > 0
Repeater {
model: root.rightActionIds
HomeActionSlot {
required property var modelData
actionId: String(modelData)
controller: root.controller
mediaModel: root.mediaModel
slotSize: root.slotSize
tight: root.tight
}
}
}
}
}
@@ -0,0 +1,73 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.DankDash
import qs.Widgets
FocusScope {
id: root
required property var islandController
property bool contentStaged: false
readonly property real stageWidth: root.islandController.homeExpandedTarget.width - 2
readonly property real stageHeight: root.islandController.homeExpandedTarget.height - 2
readonly property real faceInset: Theme.spacingM
readonly property var overviewTab: tabLoader.item
clip: true
function focusOverview() {
root.forceActiveFocus();
}
Loader {
id: tabLoader
x: root.faceInset
width: root.stageWidth - root.faceInset * 2
height: tabLoader.item?.implicitHeight || 410
y: Math.round((root.stageHeight - height) / 2)
active: root.contentStaged
asynchronous: true
visible: status === Loader.Ready
sourceComponent: overviewTabComponent
}
Component {
id: overviewTabComponent
OverviewTab {
live: root.islandController.expanded && root.islandController.activeActivity === "home"
onCloseDash: root.islandController.requestCollapse()
onNavFocusRequested: root.focusOverview()
onSwitchToMediaTab: root.islandController.requestActivity("media", true, true)
onSwitchToWeatherTab: {
if (SettingsData.weatherEnabled)
root.islandController.requestWeather(false);
}
Component.onCompleted: root.focusOverview()
}
}
DankSpinner {
anchors.centerIn: parent
size: 40
visible: !tabLoader.visible
}
Keys.onPressed: event => {
if (root.overviewTab?.handleKeyEvent(event) === true) {
event.accepted = true;
return;
}
if (event.key === Qt.Key_Escape) {
root.islandController.requestCollapse();
event.accepted = true;
}
}
Component.onCompleted: root.contentStaged = true
}
@@ -0,0 +1,29 @@
pragma ComponentBehavior: Bound
import QtQuick
// Compact-face slot click target; hover here suppresses island hover-expand.
MouseArea {
id: root
required property var controller
property bool hoverCounted: false
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
hoverCounted = true;
controller.slotHoverEntered();
}
onExited: {
if (!hoverCounted)
return;
hoverCounted = false;
controller.slotHoverExited();
}
Component.onDestruction: {
if (hoverCounted)
controller.slotHoverExited();
}
}
@@ -0,0 +1,141 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
required property var controller
readonly property real logoSize: Math.max(12, Theme.iconSizeSmall + SettingsData.launcherLogoSizeOffset)
readonly property string logoMode: SettingsData.launcherLogoMode
readonly property bool compositorAvailable: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc
readonly property bool showCustom: logoMode === "custom" && SettingsData.launcherLogoCustomPath !== ""
readonly property bool showCompositor: logoMode === "compositor" && root.compositorAvailable
readonly property bool showOs: logoMode === "os"
readonly property bool showDank: logoMode === "dank"
readonly property bool showApps: !root.showCustom && !root.showCompositor && !root.showOs && !root.showDank
readonly property color iconColor: Theme.effectiveLogoColor !== "" ? Theme.effectiveLogoColor : Theme.surfaceText
readonly property string compositorSource: {
if (CompositorService.isNiri)
return "file://" + Theme.shellDir + "/assets/niri.svg";
if (CompositorService.isHyprland)
return "file://" + Theme.shellDir + "/assets/hyprland.svg";
if (CompositorService.isMango)
return "file://" + Theme.shellDir + "/assets/mango.png";
if (CompositorService.isSway || CompositorService.isScroll)
return "file://" + Theme.shellDir + "/assets/sway.svg";
if (CompositorService.isMiracle)
return "file://" + Theme.shellDir + "/assets/miraclewm.svg";
if (CompositorService.isLabwc)
return "file://" + Theme.shellDir + "/assets/labwc.png";
return "";
}
function pushMeasuredWidth() {
root.controller.setDestinationContentWidth("launcher", compactRow.implicitWidth);
}
Component.onCompleted: root.pushMeasuredWidth()
Row {
id: compactRow
anchors.centerIn: parent
spacing: Theme.spacingS
onImplicitWidthChanged: root.pushMeasuredWidth()
Item {
width: root.logoSize
height: root.logoSize
anchors.verticalCenter: parent.verticalCenter
DankIcon {
visible: root.showApps
anchors.centerIn: parent
name: "apps"
size: root.logoSize
color: root.iconColor
}
SystemLogo {
visible: root.showOs
anchors.centerIn: parent
width: root.logoSize
height: root.logoSize
colorOverride: root.iconColor
brightnessOverride: SettingsData.launcherLogoBrightness
contrastOverride: SettingsData.launcherLogoContrast
}
IconImage {
visible: root.showDank
anchors.centerIn: parent
width: root.logoSize
height: root.logoSize
smooth: true
mipmap: true
asynchronous: true
source: "file://" + Theme.shellDir + "/assets/danklogo.svg"
layer.enabled: true
layer.smooth: true
layer.mipmap: true
layer.effect: MultiEffect {
saturation: 0
colorization: 1
colorizationColor: root.iconColor
}
}
IconImage {
visible: root.showCompositor
anchors.centerIn: parent
width: root.logoSize
height: root.logoSize
smooth: true
asynchronous: true
source: root.compositorSource
layer.enabled: true
layer.effect: MultiEffect {
saturation: 0
colorization: 1
colorizationColor: root.iconColor
brightness: SettingsData.launcherLogoBrightness
contrast: SettingsData.launcherLogoContrast
}
}
IconImage {
visible: root.showCustom
anchors.centerIn: parent
width: root.logoSize
height: root.logoSize
smooth: true
asynchronous: true
source: SettingsData.launcherLogoCustomPath ? "file://" + SettingsData.launcherLogoCustomPath.replace("file://", "") : ""
layer.enabled: true
layer.effect: MultiEffect {
saturation: 0
colorization: 1
colorizationColor: root.iconColor
brightness: SettingsData.launcherLogoBrightness
contrast: SettingsData.launcherLogoContrast
}
}
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Launcher")
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
}
}
}
@@ -0,0 +1,96 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modals.DankLauncherV2
FocusScope {
id: root
required property var islandController
required property var launcherController
property var transientSurfaceTracker: null
property var effectiveScreen: null
property real screenWidth: effectiveScreen?.width ?? 1920
property real screenHeight: effectiveScreen?.height ?? 1080
property real alignedX: 0
property real alignedY: 0
property real bottomInset: 18
clip: true
function focusSearch() {
launcherContent.searchField.forceActiveFocus();
}
function initializeSession() {
const targetQuery = islandController.launcherPendingQuery || (SettingsData.rememberLastQuery ? (SessionData.launcherLastQuery || "") : "");
const targetMode = islandController.launcherPendingMode || SessionData.getLauncherRestoreMode();
launcherContent.closeTransientUi?.();
launcherController.reset();
launcherController.explicitQuerySession = !!islandController.launcherPendingQuery;
launcherController.searchMode = targetMode;
launcherController.historyIndex = -1;
launcherContent.suspendSearchUpdates = true;
launcherContent.searchField.text = targetQuery;
launcherContent.suspendSearchUpdates = false;
if (targetQuery.length > 0)
launcherController.setSearchQuery(targetQuery);
else
launcherController.performSearch();
launcherContent.resetScroll();
root.focusSearch();
if (islandController.launcherPendingQuery)
launcherContent.searchField.cursorPosition = targetQuery.length;
else
launcherContent.searchField.selectAll();
}
QtObject {
id: hostContract
readonly property bool spotlightOpen: root.islandController.launcherSessionActive
readonly property bool isClosing: false
readonly property bool contentVisible: true
readonly property var effectiveScreen: root.effectiveScreen
readonly property real screenWidth: root.screenWidth
readonly property real screenHeight: root.screenHeight
readonly property real alignedX: root.alignedX
readonly property real alignedY: root.alignedY
function hide() {
root.islandController.requestCollapse();
}
}
SpotlightLauncherContent {
id: launcherContent
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingM
anchors.bottomMargin: root.bottomInset
parentModal: hostContract
controllerOverride: root.launcherController
transientSurfaceTracker: root.transientSurfaceTracker
showResultsWithoutQuery: true
maxResultsHeight: Math.max(120, islandController.launcherExpandedTarget.height - Theme.spacingM - root.bottomInset - launcherContent.searchAreaHeight)
}
onActiveFocusChanged: root.islandController.launcherInputFocused = activeFocus
Connections {
target: root.islandController
function onLauncherSessionSerialChanged() {
Qt.callLater(root.initializeSession);
}
}
Component.onCompleted: root.islandController.markLauncherVisualsReady()
Component.onDestruction: root.islandController.launcherInputFocused = false
}
@@ -0,0 +1,41 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Widgets
import qs.Common
import qs.Widgets
Item {
id: root
required property var mediaModel
property real cornerRadius: Math.min(width, height) * 0.22
readonly property int artPixelSize: Math.max(1, Math.round(Math.max(width, height) * 2))
ClippingRectangle {
anchors.fill: parent
radius: root.cornerRadius
color: Theme.primaryContainer
antialiasing: true
DankIcon {
anchors.centerIn: parent
name: "music_note"
size: Math.min(parent.width, parent.height) * 0.46
color: Theme.primary
visible: artwork.status !== Image.Ready
}
Image {
id: artwork
anchors.fill: parent
source: root.visible && root.width > 0 ? root.mediaModel.artUrl : ""
asynchronous: true
cache: true
fillMode: Image.PreserveAspectCrop
sourceSize: Qt.size(root.artPixelSize, root.artPixelSize)
visible: status === Image.Ready
}
}
}
@@ -0,0 +1,157 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
Item {
id: root
property color accentColor: Theme.primary
property bool playing: false
property real animationScale: 1.0
readonly property bool onScreen: visible && enabled && (Window.window?.visible ?? false)
readonly property bool available: CavaService.cavaAvailable && SettingsData.audioVisualizerEnabled
readonly property bool blobActive: root.onScreen && root.playing && root.available
readonly property real blobBaseRadiusFactor: 0.43
readonly property real blobAmplitudeFactor: 0.115
readonly property real blobOvershoot: 1.15
readonly property real blobEnergySensitivity: 1.15
readonly property real cavaFullScale: 45
readonly property real blobAttack: 0.75
readonly property real blobRelease: 0.2
readonly property real blobBeatBoost: 2.5
readonly property real blobBeatKick: 4
readonly property real blobOnsetThreshold: 1.4
readonly property real blobSpringStiffness: 220
readonly property real blobSpringDamping: 19
readonly property real blobMorphSpeed: 0.05
readonly property real blobMorphBoost: 1.7
readonly property real blobSpinSpeed: 0.03
readonly property var fluxWeights: [1.0, 1.0, 0.6, 0.6, 0.35, 0.35]
property var smoothedBands: [0, 0, 0, 0, 0, 0]
property var slowBands: [0, 0, 0, 0, 0, 0]
property var bandTargets: [0, 0, 0, 0, 0, 0]
property var bandDisplay: [0, 0, 0, 0, 0, 0]
property var prevLevels: [0, 0, 0, 0, 0, 0]
property real fluxAvg: 0.02
property real loudCtx: 0.1
property int beatCooldown: 0
property real energyTarget: 0
property real energyPos: 0
property real energyVel: 0
function updateBands() {
const vals = CavaService.values;
if (!vals || vals.length < 6)
return;
const s = smoothedBands;
const slow = slowBands;
const out = bandTargets;
const prev = prevLevels;
const w = fluxWeights;
let flux = 0;
for (let i = 0; i < 6; i++) {
const level = Math.min(Math.max(vals[i], 0), cavaFullScale) / cavaFullScale;
flux += Math.max(0, level - prev[i]) * w[i];
prev[i] = level;
const alpha = level > s[i] ? blobAttack : blobRelease;
s[i] += alpha * (level - s[i]);
slow[i] += 0.05 * (level - slow[i]);
const punch = Math.max(0, s[i] - slow[i]) * blobBeatBoost;
out[i] = Math.min(1, (0.55 * s[i] + punch) * blobEnergySensitivity);
}
const ratio = flux / Math.max(fluxAvg, 0.004);
fluxAvg += 0.06 * (flux - fluxAvg);
if (beatCooldown > 0) {
beatCooldown--;
} else if (ratio > blobOnsetThreshold && flux > 0.008) {
energyVel += blobBeatKick * Math.min(2.5, ratio - 1);
beatCooldown = 3;
}
const loud = 0.7 * Math.max(prev[0], prev[1]) + 0.3 * Math.max(prev[2], prev[3]);
loudCtx += 0.03 * (loud - loudCtx);
const surge = Math.max(0, loud / Math.max(loudCtx, 0.05) - 1);
energyTarget = Math.min(1, 0.5 * loud + 0.6 * Math.min(1, surge));
}
function stepBlob(dt) {
energyVel += (blobSpringStiffness * (energyTarget - energyPos) - blobSpringDamping * energyVel) * dt;
energyPos = Math.max(0, Math.min(blobOvershoot, energyPos + energyVel * dt));
blobEffect.energy = energyPos;
const d = bandDisplay;
const t = bandTargets;
const f = Math.min(1, dt * 14);
for (let i = 0; i < 6; i++)
d[i] += f * (t[i] - d[i]);
blobEffect.bandsA = Qt.vector4d(d[0], d[1], d[2], d[3]);
blobEffect.bandsB = Qt.vector2d(d[4], d[5]);
const speed = 1 + energyPos * blobMorphBoost;
blobEffect.phase = (blobEffect.phase + dt * blobMorphSpeed * speed) % 1;
blobEffect.spin = (blobEffect.spin + dt * blobSpinSpeed) % 6.28318530718;
}
Loader {
active: root.blobActive
sourceComponent: Component {
Ref {
service: CavaService
}
}
}
Connections {
target: CavaService
enabled: blobEffect.visible && root.available
function onValuesChanged() {
root.updateBands();
}
}
Timer {
running: blobEffect.visible && root.onScreen
interval: 33
repeat: true
onTriggered: root.stepBlob(0.033)
}
ShaderEffect {
id: blobEffect
readonly property real span: Math.min(root.width, root.height)
width: span * (root.blobBaseRadiusFactor + root.blobAmplitudeFactor * root.blobOvershoot) * 2 * root.animationScale + 4
height: width
anchors.centerIn: parent
visible: root.blobActive || activation > 0.004
property real phase: 0
property real spin: 0
property real sizePx: width
property real baseRadiusPx: span * root.blobBaseRadiusFactor * root.animationScale
property real amplitudePx: span * root.blobAmplitudeFactor * root.animationScale
property real activation: root.blobActive ? 1 : 0
property real energy: 0
property vector4d bandsA: Qt.vector4d(0, 0, 0, 0)
property vector2d bandsB: Qt.vector2d(0, 0)
property vector4d fillColor: Qt.vector4d(root.accentColor.r, root.accentColor.g, root.accentColor.b, root.accentColor.a)
Behavior on activation {
NumberAnimation {
duration: 550
easing.type: Easing.InOutQuad
}
}
fragmentShader: Qt.resolvedUrl("../../../Shaders/qsb/blob.frag.qsb")
}
}
@@ -0,0 +1,117 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var mediaModel: null
property bool isPlaying: mediaModel?.playing ?? false
property color barColor: Theme.primary
readonly property bool available: CavaService.cavaAvailable && SettingsData.audioVisualizerEnabled
readonly property bool live: visible && enabled && (Window.window?.visible ?? false) && root.isPlaying && root.available
readonly property real maxBarHeight: Math.max(root.minBarHeight, height - 2)
readonly property real minBarHeight: 3
property vector4d bandsA: Qt.vector4d(0, 0, 0, 0)
property vector2d bandsB: Qt.vector2d(0, 0)
function applyIdleBands() {
bandsA = Qt.vector4d(0.3, 0.55, 0.7, 0.7);
bandsB = Qt.vector2d(0.55, 0.3);
}
function updateBands() {
const values = CavaService.values;
if (!values || values.length < 6)
return;
function normalized(index) {
const value = Math.min(Math.max(values[index], 0), 100) / 100;
return Math.round(Math.sqrt(value + 0.01) * 32) / 32;
}
const nextA = Qt.vector4d(normalized(0), normalized(1), normalized(2), normalized(3));
const nextB = Qt.vector2d(normalized(4), normalized(5));
if (nextA == bandsA && nextB == bandsB)
return;
bandsA = nextA;
bandsB = nextB;
}
onLiveChanged: {
if (live)
updateBands();
else
applyIdleBands();
}
onAvailableChanged: {
if (!available)
return;
if (live)
updateBands();
else
applyIdleBands();
}
Component.onCompleted: {
if (root.live)
updateBands();
else
applyIdleBands();
}
Loader {
active: root.live && root.available
sourceComponent: Component {
Ref {
service: CavaService
}
}
}
Connections {
target: CavaService
enabled: root.live && root.available
function onValuesChanged() {
root.updateBands();
}
}
Loader {
active: root.available && root.live
anchors.fill: parent
sourceComponent: cavaShader
}
Component {
id: cavaShader
ShaderEffect {
property real widthPx: width
property real heightPx: height
property real minH: root.minBarHeight
property real maxH: root.maxBarHeight
property vector4d bandsA: root.bandsA
property vector2d bandsB: root.bandsB
property vector4d fillColor: Qt.vector4d(root.barColor.r, root.barColor.g, root.barColor.b, root.barColor.a)
opacity: root.live ? 1 : 0.45
fragmentShader: Qt.resolvedUrl("../../../Shaders/qsb/viz_bars.frag.qsb")
}
}
DankIcon {
anchors.centerIn: parent
visible: !root.available || !root.live
name: "graphic_eq"
size: Math.min(parent.width, parent.height)
color: root.barColor
}
}
@@ -0,0 +1,241 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Services
Item {
id: root
required property var mediaModel
required property var controller
readonly property bool dense: root.controller.compactDense
readonly property real artworkSize: root.controller.compactIconSize
readonly property real textGutter: Theme.spacingXS
readonly property real cavaWidth: 20
readonly property real minTextWidth: 48
readonly property string timeText: systemClock.date.toLocaleTimeString(I18n.locale(), SettingsData.getEffectiveTimeFormat())
readonly property bool clockVisible: SettingsData.dankIslandMediaClockVisible
readonly property real naturalTextWidth: Math.max(root.minTextWidth, titleMetrics.width, root.dense ? 0 : artistMetrics.width) + root.textGutter * 2
readonly property real measuredWidth: {
let width = Theme.spacingS + root.artworkSize + Theme.spacingXS + root.cavaWidth + Theme.spacingXS + root.naturalTextWidth + Theme.spacingXS;
if (root.clockVisible)
width += clockText.implicitWidth + Theme.spacingXS * 2 + Theme.spacingS;
return width;
}
function pushMeasuredWidth() {
root.controller.setMediaContentWidth(root.measuredWidth);
}
onMeasuredWidthChanged: root.pushMeasuredWidth()
Component.onCompleted: root.pushMeasuredWidth()
TextMetrics {
id: titleMetrics
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
text: root.mediaModel.title
}
TextMetrics {
id: artistMetrics
font.family: Theme.fontFamily
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 2)
text: root.dense ? "" : root.mediaModel.artist
}
SystemClock {
id: systemClock
precision: SettingsData.showSeconds ? SystemClock.Seconds : SystemClock.Minutes
}
MediaArtwork {
id: artwork
anchors {
left: parent.left
leftMargin: Theme.spacingS
verticalCenter: parent.verticalCenter
}
width: root.artworkSize
height: width
mediaModel: root.mediaModel
}
Row {
id: infoRow
anchors {
left: artwork.right
leftMargin: Theme.spacingXS
right: root.clockVisible ? rightActivity.left : parent.right
rightMargin: Theme.spacingXS
verticalCenter: parent.verticalCenter
}
height: root.artworkSize
spacing: Theme.spacingXS
MediaCava {
anchors.verticalCenter: parent.verticalCenter
width: root.cavaWidth
height: parent.height
mediaModel: root.mediaModel
}
Column {
id: textCol
width: parent.width - root.cavaWidth - parent.spacing
anchors.verticalCenter: parent.verticalCenter
spacing: 0
Item {
id: titleClip
width: parent.width - root.textGutter * 2
x: root.textGutter
height: titleText.implicitHeight
clip: true
Text {
id: titleText
readonly property bool onScreen: Window.window?.visible ?? false
readonly property bool needsScrolling: implicitWidth > titleClip.width && SettingsData.scrollTitleEnabled
readonly property bool scrollActive: needsScrolling && titleClip.visible && root.visible && root.enabled && onScreen && root.mediaModel.playing
readonly property real maxScrollOffset: Math.max(0, implicitWidth - titleClip.width + root.textGutter)
property real scrollOffset: 0
property int scrollDirection: 1
property real scrollHoldMs: 1600
function resetScroll() {
scrollOffset = 0;
scrollDirection = 1;
scrollHoldMs = 1600;
}
function stepScroll(deltaMs) {
if (scrollHoldMs > 0) {
scrollHoldMs -= deltaMs;
return;
}
const next = scrollOffset + scrollDirection * deltaMs / 62.5;
if (next >= maxScrollOffset) {
scrollOffset = maxScrollOffset;
scrollDirection = -1;
scrollHoldMs = 1200;
return;
}
if (next <= 0) {
scrollOffset = 0;
scrollDirection = 1;
scrollHoldMs = 1600;
return;
}
scrollOffset = next;
}
onNeedsScrollingChanged: {
if (!needsScrolling)
resetScroll();
}
onTextChanged: resetScroll()
height: implicitHeight
width: needsScrolling ? implicitWidth : titleClip.width
x: needsScrolling ? -scrollOffset : 0
text: root.mediaModel.title
color: Theme.surfaceText
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
wrapMode: Text.NoWrap
elide: needsScrolling ? Text.ElideNone : Text.ElideRight
Timer {
interval: 60
repeat: true
running: titleText.scrollActive
onTriggered: {
if (cavaTickWatch.running)
return;
titleText.stepScroll(60);
}
}
Timer {
id: cavaTickWatch
interval: 150
}
Connections {
target: CavaService
enabled: titleText.scrollActive && SettingsData.audioVisualizerEnabled && CavaService.cavaAvailable
function onValuesChanged() {
cavaTickWatch.restart();
titleText.stepScroll(40);
}
}
}
}
Text {
x: root.textGutter
width: parent.width - root.textGutter * 2
visible: !root.dense
text: root.mediaModel.artist
color: Theme.surfaceTextSecondary
font.family: Theme.fontFamily
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 2)
elide: Text.ElideRight
}
}
}
Item {
id: rightActivity
anchors {
right: parent.right
rightMargin: Theme.spacingS
verticalCenter: parent.verticalCenter
}
width: root.clockVisible ? (clockText.implicitWidth + Theme.spacingXS * 2) : 0
height: root.dense ? root.artworkSize : 36
visible: root.clockVisible
Rectangle {
anchors.centerIn: parent
width: clockText.implicitWidth + Theme.spacingXS * 2
height: root.dense ? Math.max(22, root.artworkSize - Theme.spacingXS) : 32
radius: height / 2
color: clockArea.containsMouse ? Theme.surfaceTextHover : "transparent"
Text {
id: clockText
anchors.centerIn: parent
text: root.timeText
color: Theme.surfaceText
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
}
IslandSlotHoverArea {
id: clockArea
anchors.fill: parent
controller: root.controller
onClicked: root.controller.requestActivity("home", false, false)
}
}
}
}
@@ -0,0 +1,99 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Modules.DankDash
import qs.Widgets
FocusScope {
id: root
required property var islandController
property var effectiveScreen: null
property real alignedX: 0
property real alignedY: 0
property real alignedWidth: 0
property real alignedHeight: 0
property bool contentStaged: false
property bool geometrySettled: false
readonly property bool menusEnabled: islandController.expanded && geometrySettled
readonly property var playerTab: tabLoader.item
signal showVolumeDropdown(point pos)
signal showAudioDevicesDropdown(point pos)
signal showPlayersDropdown(point pos)
signal hideDropdowns
signal dropdownHoverEnded
clip: true
function focusPlayer() {
root.forceActiveFocus();
return true;
}
function resetDropdownStates() {
root.playerTab?.resetDropdownStates();
}
Loader {
id: tabLoader
anchors.fill: parent
active: root.contentStaged
asynchronous: true
visible: status === Loader.Ready
sourceComponent: playerTabComponent
}
Component {
id: playerTabComponent
MediaPlayerTab {
chrome: "island"
live: root.islandController.expanded && root.islandController.activeActivity === "media"
targetScreen: root.effectiveScreen
popoutX: root.alignedX
popoutY: root.alignedY
popoutWidth: root.alignedWidth
popoutHeight: root.alignedHeight
contentOffsetY: 0
section: "right"
menusEnabled: root.menusEnabled
onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => root.showVolumeDropdown(pos)
onShowAudioDevicesDropdown: (pos, screen, rightEdge) => root.showAudioDevicesDropdown(pos)
onShowPlayersDropdown: (pos, screen, rightEdge, player, players) => root.showPlayersDropdown(pos)
onHideDropdowns: root.hideDropdowns()
onDropdownButtonExited: root.dropdownHoverEnded()
Component.onCompleted: root.focusPlayer()
}
}
DankSpinner {
anchors.centerIn: parent
size: 40
visible: !tabLoader.visible
}
Keys.onPressed: event => {
if (root.playerTab?.handleKeyEvent(event) === true) {
event.accepted = true;
return;
}
if (event.key === Qt.Key_Escape) {
root.islandController.requestCollapse();
event.accepted = true;
}
}
Connections {
target: root.islandController
function onMediaDropdownOpenChanged() {
if (!root.islandController.mediaDropdownOpen)
root.resetDropdownStates();
}
}
Component.onCompleted: root.contentStaged = true
}
@@ -0,0 +1,44 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
required property var controller
readonly property int unreadCount: NotificationService.notifications.length
function pushMeasuredWidth() {
root.controller.setDestinationContentWidth("notificationcenter", compactRow.implicitWidth);
}
Component.onCompleted: root.pushMeasuredWidth()
Row {
id: compactRow
anchors.centerIn: parent
spacing: Theme.spacingS
onImplicitWidthChanged: root.pushMeasuredWidth()
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: root.unreadCount > 0 ? "notifications_active" : "notifications"
size: Theme.iconSizeSmall
color: Theme.primary
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: root.unreadCount > 0 ? I18n.tr("Notifications (%1)").arg(root.unreadCount) : I18n.tr("Notifications")
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
}
}
}
@@ -0,0 +1,136 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.Notifications.Center
import qs.Services
// Shared notification center body hosted in the island silhouette.
FocusScope {
id: root
required property var islandController
property var effectiveScreen: null
property var transientSurfaceTracker: null
property real bottomInset: Theme.spacingM
property bool _sessionOpen: false
property bool _heightReportPending: false
clip: true
function focusList() {
root.forceActiveFocus();
keyboardController.rebuildFlatNavigation();
return true;
}
function beginSession() {
if (!hostContract.shouldBeVisible)
return;
root._sessionOpen = true;
// Suppresses transient island cards for as long as the list owns them.
NotificationService.onOverlayOpen();
keyboardController.reset();
keyboardController.listView = content.notificationList;
content.notificationList.keyboardController = keyboardController;
content.notificationHeader.keyboardController = keyboardController;
keyboardController.rebuildFlatNavigation();
root.forceActiveFocus();
root.queueHeightReport();
}
function endSession() {
if (!root._sessionOpen)
return;
root._sessionOpen = false;
keyboardController.keyboardNavigationActive = false;
NotificationService.expandedGroups = {};
NotificationService.expandedMessages = {};
NotificationService.onOverlayClose();
}
function queueHeightReport() {
if (root._heightReportPending)
return;
root._heightReportPending = true;
Qt.callLater(() => {
root._heightReportPending = false;
content.notificationList.syncSessionContentHeight();
root.islandController.setNotificationCenterContentHeight(content.targetImplicitHeight + root.bottomInset);
});
}
function prepareVisuals() {
content.notificationList.syncSessionContentHeight();
root.islandController.setNotificationCenterContentHeight(content.targetImplicitHeight + root.bottomInset);
root.islandController.markNotificationCenterVisualsReady();
}
QtObject {
id: hostContract
readonly property bool shouldBeVisible: root.islandController.activeActivity === "notificationcenter" && root.islandController.expanded
readonly property var screen: root.effectiveScreen
readonly property var transientSurfaceTracker: root.transientSurfaceTracker
readonly property real maxContentHeight: root.islandController.notificationCenterMaxHeight - root.bottomInset
readonly property bool hostOwnsHeight: true
readonly property bool headerTogglesClose: true
readonly property bool animateCardExpansion: false
readonly property bool lightweightNotifications: true
function close() {
root.islandController.requestCollapse();
}
function requestSettings() {
root.islandController.requestCollapse();
PopoutService.openSettingsWithTab("notifications", root, () => root.islandController.requestNotificationCenter(false));
}
}
NotificationKeyboardController {
id: keyboardController
listView: null
isOpen: hostContract.shouldBeVisible
onClose: () => root.islandController.requestCollapse()
}
Keys.onPressed: event => content.handleKey(event)
NotificationCenterContent {
id: content
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
bottomMargin: root.bottomInset
}
width: root.islandController.notificationCenterExpandedTarget.width
host: hostContract
externalKeyboardController: keyboardController
onTargetImplicitHeightChanged: root.queueHeightReport()
}
Connections {
target: root.islandController
function onNotificationCenterSessionSerialChanged() {
Qt.callLater(root.beginSession);
}
function onExpandedChanged() {
if (!root.islandController.expanded)
root.endSession();
}
function onActiveActivityChanged() {
if (root.islandController.activeActivity !== "notificationcenter")
root.endSession();
}
}
Component.onCompleted: Qt.callLater(root.prepareVisuals)
Component.onDestruction: root.endSession()
}
@@ -0,0 +1,69 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: root
required property var notificationModel
property bool dense: false
property real iconSize: 36
Row {
anchors {
fill: parent
leftMargin: Theme.spacingS
rightMargin: Theme.spacingS
}
spacing: Theme.spacingS
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: root.notificationModel.critical ? 3 : 0
height: Math.max(12, parent.height - Theme.spacingS)
radius: 1.5
color: Theme.error
visible: root.notificationModel.critical
}
DankCircularImage {
anchors.verticalCenter: parent.verticalCenter
width: root.iconSize
height: width
imageSource: root.notificationModel.imageSource
fallbackIcon: root.notificationModel.fallbackIcon
fallbackText: root.notificationModel.fallbackText
cacheImages: false
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - root.iconSize - parent.spacing - (root.notificationModel.critical ? 3 + parent.spacing : 0)
spacing: 1
// A dense pill keeps one line, so the app name folds into the summary.
Text {
width: parent.width
visible: !root.dense
text: root.notificationModel.appName + (root.notificationModel.timeText ? " · " + root.notificationModel.timeText : "")
color: Theme.surfaceTextSecondary
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
}
Text {
width: parent.width
text: root.dense && root.notificationModel.appName ? root.notificationModel.appName + " · " + root.notificationModel.summary : root.notificationModel.summary
color: Theme.surfaceText
font.family: Theme.fontFamily
font.pixelSize: root.dense ? Theme.fontSizeSmall : Theme.fontSizeMedium
font.weight: Font.DemiBold
elide: Text.ElideRight
}
}
}
}
@@ -0,0 +1,195 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: root
required property var notificationModel
Item {
anchors {
fill: parent
margins: 22
}
Row {
id: header
anchors {
left: parent.left
right: parent.right
top: parent.top
}
height: 46
spacing: Theme.spacingM
DankCircularImage {
anchors.verticalCenter: parent.verticalCenter
width: 46
height: 46
imageSource: root.notificationModel.imageSource
fallbackIcon: root.notificationModel.fallbackIcon
fallbackText: root.notificationModel.fallbackText
cacheImages: false
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 46 - closeButton.width - parent.spacing * 2
spacing: 2
Text {
width: parent.width
text: root.notificationModel.appName
color: Theme.surfaceText
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
elide: Text.ElideRight
}
Text {
width: parent.width
text: root.notificationModel.timeText
color: Theme.surfaceTextSecondary
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
}
Rectangle {
id: closeButton
anchors.verticalCenter: parent.verticalCenter
width: 38
height: 38
radius: 19
color: closeArea.containsMouse ? Theme.surfaceTextHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "close"
size: 21
color: Theme.surfaceText
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.notificationModel.dismiss()
}
}
}
Column {
anchors {
left: parent.left
right: parent.right
top: header.bottom
topMargin: Theme.spacingM
bottom: actions.top
bottomMargin: Theme.spacingM
}
spacing: Theme.spacingS
Text {
width: parent.width
text: root.notificationModel.summary
color: Theme.surfaceText
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.DemiBold
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
maximumLineCount: 2
elide: Text.ElideRight
}
Text {
width: parent.width
text: root.notificationModel.body
color: Theme.surfaceTextMedium
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
maximumLineCount: 3
elide: Text.ElideRight
visible: text.length > 0
}
}
Row {
id: actions
anchors {
right: parent.right
bottom: parent.bottom
}
height: 34
spacing: Theme.spacingS
Rectangle {
width: dismissLabel.implicitWidth + Theme.spacingL * 2
height: parent.height
radius: height / 2
color: dismissArea.containsMouse ? Theme.surfaceTextHover : "transparent"
border.width: 1
border.color: Theme.withAlpha(Theme.outline, 0.5)
Text {
id: dismissLabel
anchors.centerIn: parent
text: "Dismiss"
color: Theme.surfaceText
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
MouseArea {
id: dismissArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.notificationModel.dismiss()
}
}
Rectangle {
visible: root.notificationModel.hasAction
width: actionLabel.implicitWidth + Theme.spacingL * 2
height: parent.height
radius: height / 2
color: actionArea.containsMouse ? Theme.primaryContainer : Theme.primary
Text {
id: actionLabel
anchors.centerIn: parent
text: root.notificationModel.actionLabel
color: actionArea.containsMouse ? Theme.primary : Theme.onPrimary
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
}
MouseArea {
id: actionArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.notificationModel.activate()
}
}
}
}
}
@@ -0,0 +1,30 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.OSD
Item {
id: root
required property var systemModel
property bool dense: false
property real iconSize: 36
OsdLevelRow {
anchors.fill: parent
iconName: root.systemModel.iconName
iconSize: Math.min(Theme.iconSize, root.iconSize)
iconColor: root.systemModel.volumeActivity ? Theme.surfaceText : Theme.primary
iconInteractive: root.systemModel.volumeActivity
value: Math.round(root.systemModel.value)
minimum: root.systemModel.minimum
maximum: Math.max(root.systemModel.minimum + 1, Math.round(root.systemModel.maximum))
unit: root.systemModel.unit
displayText: root.systemModel.displayValue
sliderEnabled: root.systemModel.available
thumbOutlineColor: Theme.surfaceContainerHigh
onIconClicked: root.systemModel.toggleMute()
onSliderValueChanged: newValue => root.systemModel.setRatio(newValue / Math.max(1, Math.round(root.systemModel.maximum)))
}
}
@@ -0,0 +1,129 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: root
required property var systemModel
Column {
anchors {
fill: parent
margins: 24
}
spacing: Theme.spacingL
Row {
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: 48
height: 48
radius: 18
color: Theme.primaryContainer
DankIcon {
anchors.centerIn: parent
name: root.systemModel.iconName
size: 27
color: Theme.primary
}
MouseArea {
anchors.fill: parent
enabled: root.systemModel.volumeActivity
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.systemModel.toggleMute()
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 48 - valueText.width - parent.spacing * 2
spacing: 2
Text {
text: root.systemModel.title
color: Theme.surfaceText
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.DemiBold
}
Text {
text: root.systemModel.volumeActivity ? "Click the icon to mute" : "Display level"
color: Theme.surfaceTextSecondary
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
Text {
id: valueText
anchors.verticalCenter: parent.verticalCenter
text: root.systemModel.displayValue
color: Theme.primary
font.family: Theme.fontFamily
font.pixelSize: 28
font.weight: Font.DemiBold
}
}
Item {
id: slider
width: parent.width
height: 40
Rectangle {
id: track
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 12
radius: 6
color: Theme.withAlpha(Theme.surfaceText, 0.14)
Rectangle {
width: parent.width * root.systemModel.ratio
height: parent.height
radius: parent.radius
color: Theme.primary
}
Rectangle {
x: Math.max(0, Math.min(parent.width - width, parent.width * root.systemModel.ratio - width / 2))
anchors.verticalCenter: parent.verticalCenter
width: 22
height: 22
radius: 11
color: Theme.primary
border.width: 3
border.color: Theme.surfaceContainerHigh
}
}
MouseArea {
anchors.fill: parent
enabled: root.systemModel.available
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
function updateValue(mouse) {
root.systemModel.setRatio(mouse.x / width);
}
onPressed: mouse => updateValue(mouse)
onPositionChanged: mouse => {
if (pressed)
updateValue(mouse);
}
}
}
}
}
@@ -0,0 +1,41 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: root
required property var controller
function pushMeasuredWidth() {
root.controller.setDestinationContentWidth("wallpaper", compactRow.implicitWidth);
}
Component.onCompleted: root.pushMeasuredWidth()
Row {
id: compactRow
anchors.centerIn: parent
spacing: Theme.spacingS
onImplicitWidthChanged: root.pushMeasuredWidth()
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "wallpaper"
size: Theme.iconSizeSmall
color: Theme.primary
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Wallpaper")
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
}
}
}
@@ -0,0 +1,98 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.DankDash
import qs.Widgets
FocusScope {
id: root
required property var islandController
property var effectiveScreen: null
property bool contentStaged: false
readonly property real stageWidth: root.islandController.wallpaperExpandedTarget.width - 2
readonly property real stageHeight: root.islandController.wallpaperExpandedTarget.height - 2
readonly property var wallpaperTab: tabLoader.item
clip: true
function focusGrid() {
root.forceActiveFocus();
}
function beginSession() {
root.wallpaperTab?.collapseSearch();
root.focusGrid();
}
QtObject {
id: hostContract
property var customKeyboardFocus: null
onCustomKeyboardFocusChanged: root.islandController.keyboardYielded = hostContract.customKeyboardFocus !== null
}
Loader {
id: tabLoader
x: Theme.spacingM
y: Theme.spacingM
width: root.stageWidth - Theme.spacingM * 2
height: root.stageHeight - Theme.spacingM - Theme.spacingS
active: root.contentStaged
asynchronous: true
visible: status === Loader.Ready
sourceComponent: wallpaperTabComponent
}
Component {
id: wallpaperTabComponent
WallpaperTab {
active: true
pagerCachePages: 0
targetScreen: root.effectiveScreen
parentPopout: hostContract
Component.onCompleted: root.focusGrid()
}
}
DankSpinner {
anchors.centerIn: parent
size: 40
visible: !tabLoader.visible
}
Keys.onPressed: event => {
if (root.wallpaperTab?.handleKeyEvent(event) === true) {
event.accepted = true;
return;
}
if (event.key === Qt.Key_Escape) {
root.islandController.requestCollapse();
event.accepted = true;
}
}
Connections {
target: root.islandController
function onWallpaperSessionSerialChanged() {
Qt.callLater(root.beginSession);
}
}
Component.onCompleted: {
root.contentStaged = true;
root.islandController.markWallpaperVisualsReady();
}
Component.onDestruction: {
root.islandController.keyboardYielded = false;
root.islandController.wallpaperVisualsReady = false;
}
}
@@ -0,0 +1,41 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: root
required property var controller
function pushMeasuredWidth() {
root.controller.setDestinationContentWidth("weather", compactRow.implicitWidth);
}
Component.onCompleted: root.pushMeasuredWidth()
Row {
id: compactRow
anchors.centerIn: parent
spacing: Theme.spacingS
onImplicitWidthChanged: root.pushMeasuredWidth()
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "partly_cloudy_day"
size: Theme.iconSizeSmall
color: Theme.primary
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Weather")
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
}
}
}
@@ -0,0 +1,69 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.DankDash
import qs.Widgets
FocusScope {
id: root
required property var islandController
property bool contentStaged: false
readonly property real stageWidth: root.islandController.weatherExpandedTarget.width - 2
readonly property real stageHeight: root.islandController.weatherExpandedTarget.height - 2
readonly property real faceInset: Theme.spacingM
readonly property var weatherTab: tabLoader.item
clip: true
function focusWeather() {
root.forceActiveFocus();
}
Loader {
id: tabLoader
x: root.faceInset
y: root.faceInset
width: root.stageWidth - root.faceInset * 2
height: root.stageHeight - root.faceInset * 2
active: root.contentStaged
asynchronous: true
visible: status === Loader.Ready
sourceComponent: weatherTabComponent
}
Component {
id: weatherTabComponent
WeatherTab {
live: root.islandController.expanded && root.islandController.activeActivity === "weather"
Component.onCompleted: root.focusWeather()
}
}
DankSpinner {
anchors.centerIn: parent
size: 40
visible: !tabLoader.visible
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
root.islandController.requestCollapse();
event.accepted = true;
}
}
Component.onCompleted: {
root.contentStaged = true;
root.islandController.markWeatherVisualsReady();
}
Component.onDestruction: {
root.islandController.weatherVisualsReady = false;
}
}
@@ -0,0 +1,497 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
Item {
id: root
property var screenModel: Quickshell.screens
property var colorPickerModal: null
property var powerMenuModalLoader: null
signal lockRequested
readonly property bool launcherOpen: root.activityOpen("launcher")
readonly property bool controlCenterOpen: root.activityOpen("controlcenter")
readonly property bool wallpaperOpen: root.activityOpen("wallpaper")
readonly property bool weatherOpen: root.activityOpen("weather")
readonly property bool mediaOpen: root.activityOpen("media")
readonly property bool homeOpen: root.activityOpen("home")
readonly property bool notificationCenterOpen: root.activityOpen("notificationcenter")
function activityOpen(activityId) {
const hosts = islandVariants.instances || [];
for (let i = 0; i < hosts.length; i++) {
if (hosts[i]?.islandController?.activeActivity === activityId && hosts[i].islandController.expanded)
return true;
}
return false;
}
function hostWithActivity(activityId) {
const hosts = islandVariants.instances || [];
for (let i = 0; i < hosts.length; i++) {
if (hosts[i]?.islandController?.activeActivity === activityId && hosts[i].islandController.expanded)
return hosts[i];
}
return null;
}
function hasHostForScreen(screen) {
if (!screen)
return (islandVariants.instances || []).length > 0;
const hosts = islandVariants.instances || [];
for (let i = 0; i < hosts.length; i++) {
if (hosts[i]?.screen === screen || hosts[i]?.screen?.name === screen.name)
return true;
}
return false;
}
function hostForScreen(screenName) {
const requestedName = (screenName || "").trim();
const hosts = islandVariants.instances || [];
// An explicitly named screen with no island is an error, not a redirect to another display.
if (requestedName) {
for (let i = 0; i < hosts.length; i++) {
if (hosts[i]?.screen?.name === requestedName)
return hosts[i];
}
return null;
}
const focusedName = CompositorService.getFocusedScreen()?.name ?? "";
if (focusedName) {
for (let i = 0; i < hosts.length; i++) {
if (hosts[i]?.screen?.name === focusedName)
return hosts[i];
}
}
return hosts.length > 0 ? hosts[0] : null;
}
function activityName(activity) {
const requested = (activity || "home").trim().toLowerCase();
if (requested === "media" || requested === "launcher" || requested === "controlcenter" || requested === "wallpaper" || requested === "weather" || requested === "notificationcenter")
return requested;
if (requested === "control-center" || requested === "cc")
return "controlcenter";
if (requested === "notifications" || requested === "notification-center" || requested === "notification" || requested === "nc")
return "notificationcenter";
return "home";
}
function openActivityOn(host, activityId) {
switch (activityId) {
case "launcher":
return host.islandController.requestLauncher("", "", false);
case "controlcenter":
return host.islandController.requestControlCenter("", false);
case "wallpaper":
return host.islandController.requestWallpaper(false);
case "weather":
return host.islandController.requestWeather(false);
case "notificationcenter":
return host.islandController.requestNotificationCenter(false);
}
return host.islandController.requestActivity(activityId, true, true);
}
function focusedHost() {
const focusedName = CompositorService.getFocusedScreen()?.name ?? "";
if (!focusedName)
return null;
const hosts = islandVariants.instances || [];
for (let i = 0; i < hosts.length; i++) {
if (hosts[i]?.screen?.name === focusedName)
return hosts[i];
}
return null;
}
function focusedIslandScreen() {
return root.focusedHost()?.screen ?? null;
}
function activeLauncherHost() {
return root.hostWithActivity("launcher");
}
function openLauncher(query, mode): bool {
const host = root.focusedHost();
return host ? host.islandController.requestLauncher(query || "", mode || "", false) : false;
}
function toggleLauncher(query, mode): bool {
const openHost = root.activeLauncherHost();
if (openHost) {
openHost.islandController.requestCollapse();
return true;
}
const host = root.focusedHost();
return host ? host.islandController.requestLauncher(query || "", mode || "", false) : false;
}
function closeLauncher(): bool {
const host = root.activeLauncherHost();
if (!host)
return false;
host.islandController.requestCollapse();
return true;
}
function controlCenterHostForScreen(screen) {
if (screen) {
const hosts = islandVariants.instances || [];
for (let i = 0; i < hosts.length; i++) {
if (hosts[i]?.screen === screen || hosts[i]?.screen?.name === screen.name)
return hosts[i];
}
}
return root.focusedHost();
}
function openControlCenter(section, screen): bool {
const host = root.controlCenterHostForScreen(screen);
return host ? host.islandController.requestControlCenter(section || "", false) : false;
}
function toggleControlCenter(section, screen): bool {
const openHost = root.hostWithActivity("controlcenter");
if (openHost) {
openHost.islandController.requestCollapse();
return true;
}
return root.openControlCenter(section, screen);
}
function closeControlCenter(): bool {
const host = root.hostWithActivity("controlcenter");
if (!host)
return false;
host.islandController.requestCollapse();
return true;
}
function wallpaperHostForScreen(screen) {
if (screen) {
const hosts = islandVariants.instances || [];
for (let i = 0; i < hosts.length; i++) {
if (hosts[i]?.screen === screen || hosts[i]?.screen?.name === screen.name)
return hosts[i];
}
}
return root.focusedHost();
}
function openWallpaper(screen): bool {
const host = root.wallpaperHostForScreen(screen);
return host ? host.islandController.requestWallpaper(false) : false;
}
function toggleWallpaper(screen): bool {
const openHost = root.hostWithActivity("wallpaper");
if (openHost) {
openHost.islandController.requestCollapse();
return true;
}
return root.openWallpaper(screen);
}
function closeWallpaper(): bool {
const host = root.hostWithActivity("wallpaper");
if (!host)
return false;
host.islandController.requestCollapse();
return true;
}
function weatherHostForScreen(screen) {
if (screen) {
const hosts = islandVariants.instances || [];
for (let i = 0; i < hosts.length; i++) {
if (hosts[i]?.screen === screen || hosts[i]?.screen?.name === screen.name)
return hosts[i];
}
}
return root.focusedHost();
}
function openWeather(screen): bool {
const host = root.weatherHostForScreen(screen);
return host ? host.islandController.requestWeather(false) : false;
}
function toggleWeather(screen): bool {
const openHost = root.hostWithActivity("weather");
if (openHost) {
openHost.islandController.requestCollapse();
return true;
}
return root.openWeather(screen);
}
function closeWeather(): bool {
const host = root.hostWithActivity("weather");
if (!host)
return false;
host.islandController.requestCollapse();
return true;
}
function notificationHostForScreen(screen) {
if (screen) {
const hosts = islandVariants.instances || [];
for (let i = 0; i < hosts.length; i++) {
if (hosts[i]?.screen === screen || hosts[i]?.screen?.name === screen.name)
return hosts[i];
}
}
return root.focusedHost();
}
function openNotifications(screen): bool {
const host = root.notificationHostForScreen(screen);
return host ? host.islandController.requestNotificationCenter(false) : false;
}
function toggleNotifications(screen): bool {
const openHost = root.hostWithActivity("notificationcenter");
if (openHost) {
openHost.islandController.requestCollapse();
return true;
}
return root.openNotifications(screen);
}
function closeNotifications(): bool {
const host = root.hostWithActivity("notificationcenter");
if (!host)
return false;
host.islandController.requestCollapse();
return true;
}
function mediaHostForScreen(screen) {
if (screen) {
const hosts = islandVariants.instances || [];
for (let i = 0; i < hosts.length; i++) {
if (hosts[i]?.screen === screen || hosts[i]?.screen?.name === screen.name)
return hosts[i];
}
}
return root.focusedHost();
}
function openMedia(screen): bool {
const host = root.mediaHostForScreen(screen);
return host ? host.islandController.requestActivity("media", true, true) : false;
}
function toggleMedia(screen): bool {
const openHost = root.hostWithActivity("media");
if (openHost) {
openHost.islandController.requestCollapse();
return true;
}
return root.openMedia(screen);
}
function closeMedia(): bool {
const host = root.hostWithActivity("media");
if (!host)
return false;
host.islandController.requestCollapse();
return true;
}
function homeHostForScreen(screen) {
if (screen) {
const hosts = islandVariants.instances || [];
for (let i = 0; i < hosts.length; i++) {
if (hosts[i]?.screen === screen || hosts[i]?.screen?.name === screen.name)
return hosts[i];
}
}
return root.focusedHost();
}
function openHome(screen): bool {
const host = root.homeHostForScreen(screen);
return host ? host.islandController.requestActivity("home", true, true) : false;
}
function toggleHome(screen): bool {
const openHost = root.hostWithActivity("home");
if (openHost) {
openHost.islandController.requestCollapse();
return true;
}
return root.openHome(screen);
}
function closeHome(): bool {
const host = root.hostWithActivity("home");
if (!host)
return false;
host.islandController.requestCollapse();
return true;
}
Component.onCompleted: PopoutService.dankIslandRouter = root
Component.onDestruction: {
if (PopoutService.dankIslandRouter === root)
PopoutService.dankIslandRouter = null;
}
Variants {
id: islandVariants
model: root.screenModel
delegate: DankIslandHostWindow {
required property var modelData
screen: modelData
colorPickerModal: root.colorPickerModal
powerMenuModalLoader: root.powerMenuModalLoader
onLockRequested: root.lockRequested()
}
}
IpcHandler {
target: "island"
function _runOpen(activity, screen) {
const host = root.hostForScreen(screen);
if (!host)
return "DANK_ISLAND_UNAVAILABLE";
const requested = root.activityName(activity);
const accepted = root.openActivityOn(host, requested);
if (!accepted)
return `DANK_ISLAND_ACTIVITY_UNAVAILABLE: ${requested}`;
return `DANK_ISLAND_OPEN: ${requested}\t${host.screen?.name ?? ""}`;
}
function _runToggle(activity, screen) {
const host = root.hostForScreen(screen);
if (!host)
return "DANK_ISLAND_UNAVAILABLE";
if (host.islandController.expanded && !host.islandController.notificationActive) {
host.islandController.requestCollapse();
return `DANK_ISLAND_CLOSED: ${host.screen?.name ?? ""}`;
}
return _runOpen(activity, screen);
}
function _runShow(activity, screen) {
const host = root.hostForScreen(screen);
if (!host)
return "DANK_ISLAND_UNAVAILABLE";
const requested = root.activityName(activity);
if (!host.islandController.requestActivity(requested, false, false))
return `DANK_ISLAND_ACTIVITY_UNAVAILABLE: ${requested}`;
return `DANK_ISLAND_SHOW: ${requested}\t${host.screen?.name ?? ""}`;
}
function _runClose(screen) {
const host = root.hostForScreen(screen);
if (!host)
return "DANK_ISLAND_UNAVAILABLE";
host.islandController.requestCollapse();
return `DANK_ISLAND_CLOSED: ${host.screen?.name ?? ""}`;
}
function _runCycle(screen) {
const host = root.hostForScreen(screen);
if (!host)
return "DANK_ISLAND_UNAVAILABLE";
host.islandController.cycleActivity(1, host.islandController.expanded);
return `DANK_ISLAND_ACTIVITY: ${host.islandController.activeActivity}\t${host.screen?.name ?? ""}`;
}
function _runStatus(screen) {
const host = root.hostForScreen(screen);
if (!host)
return JSON.stringify({
"available": false,
"enabled": true,
"launcherAvailable": false
});
return JSON.stringify({
"available": true,
"enabled": true,
"screen": host.screen?.name ?? "",
"activity": host.islandController.activeActivity,
"expanded": host.islandController.expanded,
"mediaAvailable": host.islandController.mediaAvailable,
"launcherAvailable": true,
"controlCenterAvailable": true,
"wallpaperAvailable": true,
"weatherAvailable": true,
"notificationCenterAvailable": true,
"launcherInputFocused": host.islandController.launcherInputFocused,
"launcherResultCount": host.launcherResultCount,
"compactHeight": host.islandController.compactHeight
});
}
function open(activity: string): string {
return _runOpen(activity, "");
}
function toggle(activity: string): string {
return _runToggle(activity, "");
}
function show(activity: string): string {
return _runShow(activity, "");
}
function close(): string {
return _runClose("");
}
function cycle(): string {
return _runCycle("");
}
function status(): string {
return _runStatus("");
}
function openOn(activity: string, screen: string): string {
return _runOpen(activity, screen);
}
function toggleOn(activity: string, screen: string): string {
return _runToggle(activity, screen);
}
function showOn(activity: string, screen: string): string {
return _runShow(activity, screen);
}
function closeOn(screen: string): string {
return _runClose(screen);
}
function cycleOn(screen: string): string {
return _runCycle(screen);
}
function statusOn(screen: string): string {
return _runStatus(screen);
}
function notifications(): string {
return _runToggle("notificationcenter", "");
}
function notificationsOn(screen: string): string {
return _runToggle("notificationcenter", screen);
}
}
}
@@ -0,0 +1,519 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Modals.DankLauncherV2 as DankLauncher
import qs.Services
import qs.Widgets
PanelWindow {
id: root
function requestKeyboardFocus() {
if (!controller.keyboardDismissRequested) {
keyboardActivationTimer.stop();
keyboardFocusArmed = false;
keyboardRearmTimer.restart();
return;
}
keyboardRearmTimer.stop();
keyboardFocusArmed = true;
keyboardActivationTimer.restart();
}
function containsGlobalPoint(gx, gy, padding) {
const pad = padding !== undefined ? padding : 16;
const items = [surface.inputMaskItem, surface.mediaDropdownMaskItem, satelliteHost.leftInputItem, satelliteHost.rightInputItem];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (!item || item.width <= 0 || item.height <= 0)
continue;
const topLeft = item.mapToItem(null, 0, 0);
if (!topLeft)
continue;
// mapToItem(null) is window-local; callers pass screen-space cursor coords.
const top = topLeft.y + root.hostOriginY;
if (gx >= topLeft.x - pad && gx < topLeft.x + item.width + pad && gy >= top - pad && gy < top + item.height + pad)
return true;
}
return false;
}
readonly property int reserveHeight: Math.max(36, Math.min(128, SettingsData.dankIslandReserveHeight))
readonly property int compactHeight: Math.max(36, Math.min(72, SettingsData.dankIslandCompactHeight))
readonly property bool floating: SettingsData.dankIslandFloating
readonly property bool usesOverlayLayer: LayerShell.envUsesOverlay("DMS_DANKISLAND_LAYER", SettingsData.dankIslandUseOverlayLayer)
readonly property var islandLayer: LayerShell.fromEnv("DMS_DANKISLAND_LAYER", usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top)
readonly property bool bottomEdge: SettingsData.dankIslandEdge === "bottom"
readonly property int reservedStripHeight: Math.max(reserveHeight, outerGap + compactHeight)
// Input bands hug the anchored edge, which is the far side of the host when anchored bottom.
readonly property int stripY: root.bottomEdge ? root.height - root.reservedStripHeight : 0
// Screen-space Y of this strip's local origin. Zero on top, where the two spaces coincide.
readonly property int hostOriginY: root.bottomEdge ? Math.max(0, (root.screen?.height ?? 0) - root.height) : 0
readonly property int outerGap: Math.max(0, Math.min(48, SettingsData.dankIslandOuterGap))
// The host is larger than the visible island to cover every clamped activity target.
readonly property int maxActivityHeight: Math.max(560, Math.min(680, (screen?.height ?? 1080) - 200))
readonly property int hostHeight: outerGap + maxActivityHeight + 8
readonly property int maxActivityWidth: 736
readonly property real maximumHorizontalOffset: Math.max(0, (width - maxActivityWidth) / 2 - 8)
property var colorPickerModal: null
property var powerMenuModalLoader: null
property bool keyboardFocusArmed: true
signal lockRequested
color: "transparent"
implicitHeight: hostHeight
exclusiveZone: floating ? 0 : reservedStripHeight
readonly property alias islandController: controller
readonly property int launcherResultCount: launcherController.flatModel?.length ?? 0
anchors {
top: !root.bottomEdge
bottom: root.bottomEdge
left: true
right: true
}
WlrLayershell.namespace: "dms:dankisland"
WlrLayershell.layer: islandLayer
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(controller.keyboardDismissRequested && root.keyboardFocusArmed && !controller.keyboardYielded, null)
Timer {
id: keyboardActivationTimer
interval: 60
onTriggered: {
if (controller.keyboardDismissRequested && !surface.requestActivityFocus())
islandFocus.forceActiveFocus(Qt.PopupFocusReason);
}
}
Timer {
id: keyboardRearmTimer
interval: 80
onTriggered: root.keyboardFocusArmed = true
}
DankFocusGrab {
windows: [root]
wanted: KeyboardFocus.wantsGrab(controller.keyboardDismissRequested && !controller.keyboardYielded, null)
}
Component.onCompleted: KeyboardFocus.registerBarWindow(root)
Component.onDestruction: KeyboardFocus.unregisterBarWindow(root)
Connections {
target: controller
function onKeyboardDismissRequestedChanged() {
root.requestKeyboardFocus();
}
}
Connections {
target: PopoutManager
function onPopoutChanged() {
root.popoutRevision++;
}
function onScreenshotActiveChanged() {
if (!PopoutManager.screenshotActive && controller.keyboardDismissRequested)
root.requestKeyboardFocus();
}
}
mask: Region {
Region {
item: controller.inputSuspended ? null : surface.inputMaskItem
}
Region {
item: controller.inputSuspended ? null : satelliteHost.leftInputItem
}
Region {
item: controller.inputSuspended ? null : satelliteHost.rightInputItem
}
Region {
item: controller.inputSuspended ? null : surface.mediaDropdownMaskItem
}
Region {
item: controller.inputSuspended || !satelliteHost.scrollEnabled ? null : scrollStrip
}
Region {
item: controller.inputSuspended || !root.satelliteSurfacesOpen ? null : satelliteDismissStrip
}
}
IslandController {
id: controller
readonly property string interactionOverride: Quickshell.env("DMS_DANKISLAND_INTERACTION") ?? ""
interactionMode: {
const raw = interactionOverride || SettingsData.dankIslandInteractionMode;
return raw === "click" ? "click" : "hybrid";
}
inputSuspended: PopoutManager.screenshotActive
autoDemo: Quickshell.env("DMS_DANKISLAND_DEMO") === "1"
horizontalOffset: Math.max(-root.maximumHorizontalOffset, Math.min(root.maximumHorizontalOffset, SettingsData.dankIslandHorizontalOffset))
outerGap: root.outerGap
compactHeight: root.compactHeight
launcherCycleEnabled: SettingsData.launcherStyle === "island"
controlCenterMaxHeight: root.maxActivityHeight
notificationCenterMaxHeight: root.maxActivityHeight
notificationExpandAllowed: SettingsData.dankIslandNotificationExpand
unreadNotificationCount: NotificationService.notifications.length
hoverOpenDelay: Math.max(0, Math.min(1000, SettingsData.dankIslandHoverOpenDelay))
hoverCloseDelay: Math.max(0, Math.min(1000, SettingsData.dankIslandHoverCloseDelay))
}
DankLauncher.Controller {
id: launcherController
active: controller.launcherSessionActive
viewModeContext: "spotlight"
forceLinearNavigation: true
}
TransientSurfaceTracker {
id: launcherTransientSurfaces
}
TransientSurfaceTracker {
id: notificationTransientSurfaces
}
Connections {
target: controller
function onLauncherSessionActiveChanged() {
if (!controller.launcherSessionActive)
launcherTransientSurfaces.closeAll();
}
}
DmsMediaActivitySource {
id: mediaSource
controller: controller
}
DmsSystemActivitySource {
id: systemSource
controller: controller
}
DmsNotificationActivitySource {
id: notificationSource
controller: controller
targetScreen: root.screen
}
// Empty strip dismisses satellite popouts while something is open; click-through otherwise.
// PopoutManager mutates the map in place, so this tracks popoutChanged instead.
property int popoutRevision: 0
readonly property bool satelliteSurfacesOpen: {
root.popoutRevision;
const screenName = root.screen?.name;
if (!screenName)
return false;
return !!PopoutManager.currentPopoutsByScreen[screenName] || !!ModalManager.currentModalsByScreen[screenName];
}
function dismissSatelliteSurfaces() {
const screenName = root.screen?.name;
if (!screenName)
return;
if (PopoutManager.currentPopoutsByScreen[screenName])
PopoutManager.closeAllPopouts();
if (ModalManager.currentModalsByScreen[screenName])
ModalManager.closeAllModalsExcept(null);
TrayMenuManager.closeAllMenus();
}
MouseArea {
id: satelliteDismissStrip
x: 0
y: root.stripY
z: -2
width: parent.width
height: root.reservedStripHeight
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: false
enabled: root.satelliteSurfacesOpen && !controller.inputSuspended
onClicked: root.dismissSatelliteSurfaces()
}
MouseArea {
id: scrollStrip
x: 0
y: root.stripY
z: -1
width: parent.width
height: root.reservedStripHeight
acceptedButtons: Qt.NoButton
hoverEnabled: false
enabled: satelliteHost.scrollEnabled && !controller.inputSuspended
property real touchpadAccumulatorY: 0
property real touchpadAccumulatorX: 0
property real mouseAccumulatorY: 0
property real mouseAccumulatorX: 0
property bool actionInProgress: false
Timer {
id: cooldownTimer
interval: 100
onTriggered: scrollStrip.actionInProgress = false
}
function handleScrollAction(behavior, direction) {
switch (behavior) {
case "workspace":
satelliteHost.switchWorkspace(direction);
return true;
case "column":
if (!CompositorService.isNiri)
return false;
if (direction > 0)
NiriService.moveColumnRight(root.screen?.name);
else
NiriService.moveColumnLeft(root.screen?.name);
return true;
default:
return false;
}
}
function processWheel(wheel) {
if (!satelliteHost.scrollEnabled || actionInProgress) {
wheel.accepted = false;
return;
}
const deltaY = wheel.angleDelta.y;
const deltaX = wheel.angleDelta.x;
const isTouchpadY = wheel.pixelDelta && wheel.pixelDelta.y !== 0;
const isTouchpadX = wheel.pixelDelta && wheel.pixelDelta.x !== 0;
const xBehavior = satelliteHost.scrollXBehavior || "column";
const yBehavior = satelliteHost.scrollYBehavior || "workspace";
const reverse = SettingsData.reverseScrolling ? -1 : 1;
if (CompositorService.isNiri && xBehavior !== "none" && Math.abs(deltaX) > Math.abs(deltaY)) {
if (isTouchpadX) {
touchpadAccumulatorX += deltaX;
if (Math.abs(touchpadAccumulatorX) >= 500) {
const direction = touchpadAccumulatorX * reverse < 0 ? 1 : -1;
if (handleScrollAction(xBehavior, direction)) {
actionInProgress = true;
cooldownTimer.restart();
}
touchpadAccumulatorX = 0;
}
} else {
mouseAccumulatorX += deltaX;
if (Math.abs(mouseAccumulatorX) >= 120) {
const direction = mouseAccumulatorX * reverse < 0 ? 1 : -1;
if (handleScrollAction(xBehavior, direction)) {
actionInProgress = true;
cooldownTimer.restart();
}
mouseAccumulatorX = 0;
}
}
wheel.accepted = false;
return;
}
if (yBehavior === "none") {
wheel.accepted = false;
return;
}
if (isTouchpadY) {
touchpadAccumulatorY += deltaY;
if (Math.abs(touchpadAccumulatorY) >= 500) {
const direction = touchpadAccumulatorY * reverse < 0 ? 1 : -1;
if (handleScrollAction(yBehavior, direction)) {
actionInProgress = true;
cooldownTimer.restart();
}
touchpadAccumulatorY = 0;
}
} else {
mouseAccumulatorY += deltaY;
if (Math.abs(mouseAccumulatorY) >= 120) {
const direction = mouseAccumulatorY * reverse < 0 ? 1 : -1;
if (handleScrollAction(yBehavior, direction)) {
actionInProgress = true;
cooldownTimer.restart();
}
mouseAccumulatorY = 0;
}
}
wheel.accepted = false;
}
onWheel: wheel => processWheel(wheel)
}
DankIslandSurface {
id: surface
anchors.fill: parent
controller: controller
mediaModel: mediaSource
systemModel: systemSource
notificationModel: notificationSource
launcherController: launcherController
launcherTransientSurfaceTracker: launcherTransientSurfaces
notificationTransientSurfaceTracker: notificationTransientSurfaces
colorPickerModal: root.colorPickerModal
powerMenuModalLoader: root.powerMenuModalLoader
effectiveScreen: root.screen
hostOriginY: root.hostOriginY
reducedMotion: SettingsData.dankIslandReducedMotion || SettingsData.animationSpeed === SettingsData.AnimationSpeed.None
springStiffness: Math.max(100, Math.min(1200, SettingsData.dankIslandSpringStiffness))
springDamping: Math.max(10, Math.min(100, SettingsData.dankIslandSpringDamping))
springMass: Math.max(0.25, Math.min(3, SettingsData.dankIslandSpringMass))
palette: SettingsData.dankIslandPalette
highContrast: SettingsData.dankIslandHighContrast
onLockRequested: root.lockRequested()
onScrollWheel: wheel => scrollStrip.processWheel(wheel)
}
IslandSatelliteHost {
id: satelliteHost
anchors.fill: parent
hostWindow: root
targetScreen: root.screen
controller: controller
islandSurface: surface
outerGap: root.outerGap
}
FocusScope {
id: islandFocus
anchors.fill: parent
Keys.onEscapePressed: event => {
controller.requestCollapse();
event.accepted = true;
}
}
PanelWindow {
id: dismissWindow
screen: root.screen
visible: controller.expanded && !PopoutManager.screenshotActive
color: "transparent"
exclusiveZone: -1
anchors {
top: true
bottom: true
left: true
right: true
}
WlrLayershell.namespace: "dms:dankisland:dismiss"
WlrLayershell.layer: root.islandLayer
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
mask: Region {
item: dismissMask
Region {
item: islandHole
intersection: Intersection.Subtract
}
Region {
item: leftSatelliteHole
intersection: Intersection.Subtract
}
Region {
item: rightSatelliteHole
intersection: Intersection.Subtract
}
Region {
item: mediaDropdownHole
intersection: Intersection.Subtract
}
}
Rectangle {
id: dismissMask
anchors.fill: parent
visible: false
color: "transparent"
}
Item {
id: islandHole
x: surface.inputMaskItem.x
y: surface.inputMaskItem.y + root.hostOriginY
width: surface.inputMaskItem.width
height: surface.inputMaskItem.height
}
Item {
id: leftSatelliteHole
x: satelliteHost.leftInputItem.x
y: satelliteHost.leftInputItem.y + root.hostOriginY
width: satelliteHost.leftInputItem.width
height: satelliteHost.leftInputItem.height
}
Item {
id: rightSatelliteHole
x: satelliteHost.rightInputItem.x
y: satelliteHost.rightInputItem.y + root.hostOriginY
width: satelliteHost.rightInputItem.width
height: satelliteHost.rightInputItem.height
}
Item {
id: mediaDropdownHole
x: surface.mediaDropdownMaskItem ? surface.mediaDropdownMaskItem.x : 0
y: surface.mediaDropdownMaskItem ? surface.mediaDropdownMaskItem.y + root.hostOriginY : 0
width: surface.mediaDropdownMaskItem ? surface.mediaDropdownMaskItem.width : 0
height: surface.mediaDropdownMaskItem ? surface.mediaDropdownMaskItem.height : 0
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: mouse => {
controller.requestCollapse();
mouse.accepted = true;
}
onWheel: wheel => scrollStrip.processWheel(wheel)
}
}
}
@@ -0,0 +1,14 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Variants {
model: Quickshell.screens
delegate: DankIslandLabWindow {
required property var modelData
screen: modelData
}
}
@@ -0,0 +1,37 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
PanelWindow {
id: root
property int reserveHeight: 58
property int hostHeight: 300
color: "transparent"
implicitHeight: hostHeight
exclusiveZone: reserveHeight
anchors {
top: true
left: true
right: true
}
WlrLayershell.namespace: "dms:dankisland-lab"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
mask: Region {
item: lab.inputMaskItem
}
DankIslandMotionLab {
id: lab
anchors.fill: parent
reserveHeight: root.reserveHeight
}
}
@@ -0,0 +1,299 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Services
Item {
id: root
required property int reserveHeight
readonly property alias inputMaskItem: inputEnvelope
readonly property var activeMotion: engineMode === "native" ? nativeMotion : vectorMotion
readonly property list<string> targetStates: ["home", "compact", "expanded", "offset-test"]
readonly property var log: Log.scoped("DankIslandMotionLab")
readonly property bool hudEnabled: Quickshell.env("DMS_DANKISLAND_LAB_HUD") !== "0"
readonly property bool metricsLogEnabled: Quickshell.env("DMS_DANKISLAND_LAB_LOG") === "1"
property string engineMode: Quickshell.env("DMS_DANKISLAND_LAB_ENGINE") === "native" ? "native" : "vector"
property string targetState: "home"
property bool stressRunning: Quickshell.env("DMS_DANKISLAND_LAB_STRESS") === "1"
property real lastFrameMs: 0
property real smoothFrameMs: 0
property real peakFrameMs: 0
property real currentPeakFrameMs: 0
property int metricFrameCount: 0
property string statusText: ""
function targetForState(state) {
switch (state) {
case "compact":
return {
"width": 252,
"height": 48,
"offsetX": 0,
"offsetY": 8,
"topLeftRadius": 24,
"topRightRadius": 24,
"bottomLeftRadius": 24,
"bottomRightRadius": 24
};
case "expanded":
return {
"width": 448,
"height": 176,
"offsetX": 0,
"offsetY": 8,
"topLeftRadius": 26,
"topRightRadius": 26,
"bottomLeftRadius": 32,
"bottomRightRadius": 32
};
case "offset-test":
return {
"width": 520,
"height": 132,
"offsetX": -72,
"offsetY": 14,
"topLeftRadius": 22,
"topRightRadius": 30,
"bottomLeftRadius": 44,
"bottomRightRadius": 18
};
default:
return {
"width": 176,
"height": 42,
"offsetX": 0,
"offsetY": 8,
"topLeftRadius": 21,
"topRightRadius": 21,
"bottomLeftRadius": 21,
"bottomRightRadius": 21
};
}
}
function motionSnapshot(motion) {
return {
"width": motion.currentWidth,
"height": motion.currentHeight,
"offsetX": motion.currentOffsetX,
"offsetY": motion.currentOffsetY,
"topLeftRadius": motion.currentTopLeftRadius,
"topRightRadius": motion.currentTopRightRadius,
"bottomLeftRadius": motion.currentBottomLeftRadius,
"bottomRightRadius": motion.currentBottomRightRadius
};
}
function applyState(state) {
targetState = state;
activeMotion.setTarget(targetForState(state));
}
function cycleState(direction) {
const currentIndex = targetStates.indexOf(targetState);
const nextIndex = (currentIndex + direction + targetStates.length) % targetStates.length;
applyState(targetStates[nextIndex]);
}
function toggleEngine() {
const snapshot = motionSnapshot(activeMotion);
const target = targetForState(targetState);
if (engineMode === "vector") {
nativeMotion.snapTo(snapshot);
engineMode = "native";
nativeMotion.setTarget(target);
} else {
vectorMotion.snapTo(snapshot);
engineMode = "vector";
vectorMotion.setTarget(target);
}
peakFrameMs = 0;
currentPeakFrameMs = 0;
metricFrameCount = 0;
}
function updateStatusText() {
if (!hudEnabled)
return;
const engineLabel = engineMode === "vector" ? "VECTOR" : "NATIVE";
const motionLabel = activeMotion.running ? "MOVING" : "SETTLED";
const stressLabel = stressRunning ? " · STRESS" : "";
statusText = engineLabel + " · " + targetState.toUpperCase() + " · " + motionLabel + stressLabel + "\n" + Math.round(activeMotion.currentWidth) + "×"
+ Math.round(activeMotion.currentHeight) + " · " + smoothFrameMs.toFixed(2) + " ms smooth · " + peakFrameMs.toFixed(2) + " ms peak";
}
Component.onCompleted: {
const home = targetForState("home");
nativeMotion.snapTo(home);
vectorMotion.snapTo(home);
updateStatusText();
}
NativeSpringMotion {
id: nativeMotion
enabled: root.engineMode === "native"
}
VectorSpringMotion {
id: vectorMotion
enabled: root.engineMode === "vector"
reducedMotion: SettingsData.dankIslandReducedMotion || SettingsData.animationSpeed === SettingsData.AnimationSpeed.None
stiffness: Math.max(100, Math.min(1200, SettingsData.dankIslandSpringStiffness))
damping: Math.max(10, Math.min(100, SettingsData.dankIslandSpringDamping))
mass: Math.max(0.25, Math.min(3, SettingsData.dankIslandSpringMass))
}
Timer {
interval: 110
repeat: true
running: root.stressRunning
onTriggered: root.cycleState(1)
}
Timer {
interval: 200
repeat: true
running: root.hudEnabled
triggeredOnStart: true
onTriggered: root.updateStatusText()
}
FrameAnimation {
running: root.activeMotion.running || root.stressRunning
onRunningChanged: {
if (running) {
root.peakFrameMs = 0;
root.currentPeakFrameMs = 0;
root.metricFrameCount = 0;
}
}
onTriggered: {
root.lastFrameMs = frameTime * 1000;
root.smoothFrameMs = smoothFrameTime * 1000;
root.metricFrameCount++;
if (root.metricFrameCount > 8)
root.currentPeakFrameMs = Math.max(root.currentPeakFrameMs, root.lastFrameMs);
}
}
Timer {
interval: 1000
repeat: true
running: root.hudEnabled || root.metricsLogEnabled
onTriggered: {
root.peakFrameMs = root.currentPeakFrameMs;
root.currentPeakFrameMs = 0;
if (root.metricsLogEnabled)
root.log.info("DankIslandLabMetrics engine=" + root.engineMode + " state=" + root.targetState + " moving=" + root.activeMotion.running + " frameMs=" + root.lastFrameMs.toFixed(3) + " smoothMs=" + root.smoothFrameMs.toFixed(3) + " peakMs=" + root.peakFrameMs.toFixed(3));
}
}
readonly property real currentVisualX: (width - activeMotion.currentWidth) / 2 + activeMotion.currentOffsetX
readonly property real currentVisualY: activeMotion.currentOffsetY
readonly property real targetVisualX: (width - activeMotion.targetWidth) / 2 + activeMotion.targetOffsetX
readonly property real targetVisualY: activeMotion.targetOffsetY
Item {
id: inputEnvelope
x: Math.min(root.currentVisualX, root.targetVisualX)
y: Math.min(root.currentVisualY, root.targetVisualY)
width: Math.max(root.currentVisualX + root.activeMotion.currentWidth, root.targetVisualX + root.activeMotion.targetWidth) - x
height: Math.max(root.currentVisualY + root.activeMotion.currentHeight, root.targetVisualY + root.activeMotion.targetHeight) - y
}
Rectangle {
x: 0
y: root.reserveHeight - 1
width: root.width
height: 1
color: Theme.outline
opacity: 0.22
visible: root.hudEnabled
}
Rectangle {
id: island
x: root.currentVisualX
y: root.currentVisualY
width: root.activeMotion.currentWidth
height: root.activeMotion.currentHeight
topLeftRadius: Math.max(0, root.activeMotion.currentTopLeftRadius)
topRightRadius: Math.max(0, root.activeMotion.currentTopRightRadius)
bottomLeftRadius: Math.max(0, root.activeMotion.currentBottomLeftRadius)
bottomRightRadius: Math.max(0, root.activeMotion.currentBottomRightRadius)
color: Theme.surfaceContainerHigh
border.width: 1
border.color: Theme.withAlpha(Theme.outline, 0.28)
Rectangle {
anchors {
left: parent.left
right: parent.right
top: parent.top
}
height: 2
color: root.engineMode === "vector" ? Theme.primary : Theme.tertiary
opacity: 0.85
}
Column {
anchors.centerIn: parent
spacing: 2
visible: root.hudEnabled
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.statusText.split("\n")[0] ?? ""
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.statusText.split("\n")[1] ?? ""
color: Theme.withAlpha(Theme.surfaceText, 0.72)
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 2)
visible: island.width >= 230
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "left: next · right: engine · middle: stress · wheel: retarget"
color: Theme.withAlpha(Theme.surfaceText, 0.62)
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 2)
visible: island.width >= 400 && island.height >= 100
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
root.toggleEngine();
} else if (mouse.button === Qt.MiddleButton) {
root.stressRunning = !root.stressRunning;
} else {
root.cycleState(1);
}
}
onWheel: wheel => {
root.cycleState(wheel.angleDelta.y >= 0 ? 1 : -1);
wheel.accepted = true;
}
}
}
}
@@ -0,0 +1,483 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.DankDash
import qs.Modules.DankIsland.Activities
import qs.Services
Item {
id: root
required property IslandController controller
required property var mediaModel
required property var systemModel
required property var notificationModel
required property var launcherController
property var launcherTransientSurfaceTracker: null
property var notificationTransientSurfaceTracker: null
property var colorPickerModal: null
property var powerMenuModalLoader: null
property var effectiveScreen: null
property bool reducedMotion: false
property real springStiffness: 560
property real springDamping: 37
property real springMass: 1
// Screen-space Y of the host strip's local origin; non-zero only on the bottom edge.
property real hostOriginY: 0
property string palette: "default"
property bool highContrast: false
readonly property color surfaceColor: {
if (root.highContrast)
return Theme.surfaceContainerHighest;
if (root.palette === "bright")
return Theme.surfaceBright;
if (root.palette === "dim")
return Theme.surfaceDim;
return Theme.surfaceContainerHigh;
}
signal lockRequested
signal scrollWheel(var wheel)
readonly property alias inputMaskItem: inputEnvelope
readonly property bool motionRunning: motion.running
readonly property Item mediaDropdownMaskItem: mediaDropdowns.__activePanel
property real fadeCompactHeight: 48
property real fadeExpandedHeight: 352
property rect motionStartBounds: Qt.rect(0, 0, 0, 0)
property int mediaDropdownType: 0
property point mediaDropdownAnchor: Qt.point(0, 0)
readonly property real currentVisualX: (width - motion.currentWidth) / 2 + motion.currentOffsetX
readonly property bool bottomEdge: SettingsData.dankIslandEdge === "bottom"
readonly property real currentVisualY: bottomEdge ? height - motion.currentOffsetY - motion.currentHeight : motion.currentOffsetY
readonly property real currentVisualWidth: motion.currentWidth
readonly property real currentVisualHeight: motion.currentHeight
readonly property real targetVisualX: (width - motion.targetWidth) / 2 + motion.targetOffsetX
readonly property real targetVisualY: bottomEdge ? height - motion.targetOffsetY - motion.targetHeight : motion.targetOffsetY
// Activity hosts hand alignedY to full-screen windows, which need screen space, not surface space.
readonly property real targetScreenY: targetVisualY + root.hostOriginY
readonly property real targetVisualWidth: motion.targetWidth
readonly property real targetVisualHeight: motion.targetHeight
readonly property real morphProgress: {
const span = fadeExpandedHeight - fadeCompactHeight;
if (Math.abs(span) < 1)
return controller.expanded ? 1 : 0;
return Math.max(0, Math.min(1, (motion.currentHeight - fadeCompactHeight) / span));
}
function applyTarget() {
if (controller.expanded)
fadeExpandedHeight = controller.expandedTarget.height;
else
fadeCompactHeight = controller.compactTarget.height;
// A retarget leaves the frozen start stale, so widen it to cover where the island is now.
if (motion.running)
root.unionMotionStartBounds();
motion.setTarget(controller.targetDescriptor);
if (!motion.running)
root.releaseIdleVisuals();
}
function unionMotionStartBounds() {
const b = root.motionStartBounds;
const left = Math.min(b.x, root.currentVisualX);
const top = Math.min(b.y, root.currentVisualY);
const right = Math.max(b.x + b.width, root.currentVisualX + motion.currentWidth);
const bottom = Math.max(b.y + b.height, root.currentVisualY + motion.currentHeight);
root.motionStartBounds = Qt.rect(left, top, right - left, bottom - top);
}
function releaseIdleVisuals() {
root.controller.releaseWallpaperVisuals();
root.controller.releaseWeatherVisuals();
root.controller.releaseNotificationCenterVisuals();
root.controller.releaseLauncherVisuals();
root.controller.releaseControlCenterVisuals();
}
function openMediaDropdown(type, pos) {
stopMediaDropdownCloseTimer();
mediaDropdownAnchor = pos;
mediaDropdownType = type;
controller.mediaDropdownOpen = true;
}
function hideMediaDropdowns() {
stopMediaDropdownCloseTimer();
mediaDropdownType = 0;
controller.mediaDropdownOpen = false;
}
function startMediaDropdownCloseTimer() {
mediaDropdownCloseTimer.restart();
}
function stopMediaDropdownCloseTimer() {
mediaDropdownCloseTimer.stop();
}
function requestActivityFocus() {
return contentHost.requestActivityFocus();
}
Component.onCompleted: {
fadeCompactHeight = controller.compactTarget.height;
fadeExpandedHeight = controller.expandedTarget.height;
motion.snapTo(controller.targetDescriptor);
}
Connections {
target: root.controller
function onTargetDescriptorChanged() {
root.applyTarget();
}
function onExpandedChanged() {
if (!controller.expanded || controller.activeActivity !== "media")
root.hideMediaDropdowns();
}
function onActiveActivityChanged() {
if (controller.activeActivity !== "media")
root.hideMediaDropdowns();
}
}
Connections {
target: motion
function onRunningChanged() {
if (motion.running) {
root.motionStartBounds = Qt.rect(root.currentVisualX, root.currentVisualY, motion.currentWidth, motion.currentHeight);
if (root.controller.activeActivity === "media")
root.hideMediaDropdowns();
return;
}
root.releaseIdleVisuals();
}
}
VectorSpringMotion {
id: motion
reducedMotion: root.reducedMotion
stiffness: root.springStiffness
damping: root.springDamping
mass: root.springMass
}
Timer {
id: mediaDropdownCloseTimer
interval: 400
onTriggered: root.hideMediaDropdowns()
}
// Frozen start/target union so the Wayland mask is not rewritten every spring frame.
Item {
id: inputEnvelope
// Cover underdamped overshoot for the frozen flight, scaled to travel.
readonly property real overshootBudget: {
if (!motion.running)
return 0;
const zeta = motion.damping / (2 * Math.sqrt(Math.max(1, motion.stiffness * motion.mass)));
if (zeta >= 1)
return 0;
const spanX = Math.abs(root.targetVisualX - root.motionStartBounds.x);
const spanY = Math.abs(root.targetVisualY - root.motionStartBounds.y);
const spanW = Math.abs(motion.targetWidth - root.motionStartBounds.width);
const spanH = Math.abs(motion.targetHeight - root.motionStartBounds.height);
const span = Math.max(spanX, spanY, spanW, spanH);
return Math.ceil(span * Math.exp(-Math.PI * zeta / Math.sqrt(1 - zeta * zeta)));
}
x: (motion.running ? Math.min(root.motionStartBounds.x, root.targetVisualX) : root.targetVisualX) - overshootBudget
y: (motion.running ? Math.min(root.motionStartBounds.y, root.targetVisualY) : root.targetVisualY) - overshootBudget
width: (motion.running ? Math.max(root.motionStartBounds.x + root.motionStartBounds.width, root.targetVisualX + motion.targetWidth) : root.targetVisualX + motion.targetWidth) + overshootBudget - x
height: (motion.running ? Math.max(root.motionStartBounds.y + root.motionStartBounds.height, root.targetVisualY + motion.targetHeight) : root.targetVisualY + motion.targetHeight) + overshootBudget - y
}
Rectangle {
id: island
x: root.currentVisualX
y: root.currentVisualY
width: motion.currentWidth
height: motion.currentHeight
topLeftRadius: Math.max(0, motion.currentTopLeftRadius)
topRightRadius: Math.max(0, motion.currentTopRightRadius)
bottomLeftRadius: Math.max(0, motion.currentBottomLeftRadius)
bottomRightRadius: Math.max(0, motion.currentBottomRightRadius)
color: root.surfaceColor
border.width: root.highContrast ? 2 : 0
border.color: root.highContrast ? Theme.outlineStrong : "transparent"
// Full destinations keep blank clicks for their own content instead of collapsing.
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
enabled: !root.controller.expanded || !root.controller.activityOwnsBlankClicks
onClicked: root.controller.requestToggle(true)
onWheel: wheel => {
if (root.controller.expanded) {
wheel.accepted = false;
return;
}
root.scrollWheel(wheel);
wheel.accepted = true;
}
}
IslandContentHost {
id: contentHost
anchors.fill: parent
morphProgress: root.morphProgress
expanded: root.controller.expanded
pointerInside: root.controller.pointerInside
activityId: root.controller.activeActivity
homeCompactComponent: compactHomeComponent
homeExpandedComponent: expandedHomeComponent
mediaCompactComponent: compactMediaComponent
mediaExpandedComponent: expandedMediaComponent
launcherCompactComponent: compactLauncherComponent
launcherExpandedComponent: expandedLauncherComponent
launcherVisualsRequested: root.controller.launcherVisualsRequested
controlCenterCompactComponent: compactControlCenterComponent
controlCenterExpandedComponent: expandedControlCenterComponent
controlCenterVisualsRequested: root.controller.controlCenterVisualsRequested
wallpaperCompactComponent: compactWallpaperComponent
wallpaperExpandedComponent: expandedWallpaperComponent
wallpaperVisualsRequested: root.controller.wallpaperVisualsRequested
weatherCompactComponent: compactWeatherComponent
weatherExpandedComponent: expandedWeatherComponent
weatherVisualsRequested: root.controller.weatherVisualsRequested
systemCompactComponent: compactSystemComponent
systemExpandedComponent: expandedSystemComponent
notificationCompactComponent: compactNotificationComponent
notificationExpandedComponent: expandedNotificationComponent
notificationCenterCompactComponent: compactNotificationCenterComponent
notificationCenterExpandedComponent: expandedNotificationCenterComponent
notificationCenterVisualsRequested: root.controller.notificationCenterVisualsRequested
}
HoverHandler {
onHoveredChanged: {
if (hovered)
root.controller.updatePointerInside(true);
else
root.controller.updatePointerInside(false);
}
}
}
MediaDropdownOverlay {
id: mediaDropdowns
dropdownType: root.mediaDropdownType
anchorPos: root.mediaDropdownAnchor
isRightEdge: true
availableBounds: Qt.rect(0, 0, root.width, root.height)
activePlayer: MprisController.activePlayer
allPlayers: MprisController.availablePlayers
targetWindow: root.Window.window
onCloseRequested: root.hideMediaDropdowns()
onPanelEntered: root.stopMediaDropdownCloseTimer()
onPanelExited: root.startMediaDropdownCloseTimer()
onVolumeChanged: volume => {
const player = MprisController.activePlayer;
const identity = (player?.identity ?? "").toLowerCase();
const chrome = identity.includes("chrome") || identity.includes("chromium");
if (player && player.volumeSupported && !chrome)
player.volume = volume;
else if (AudioService.sink?.audio)
AudioService.sink.audio.volume = volume;
}
onDeviceSelected: device => AudioService.setSink(device)
onPlayerSelected: player => MprisController.setActivePlayer(player)
}
Component {
id: compactHomeComponent
HomeCompact {
controller: root.controller
mediaModel: root.mediaModel
}
}
Component {
id: expandedHomeComponent
HomeExpanded {
islandController: root.controller
}
}
Component {
id: compactMediaComponent
MediaCompact {
mediaModel: root.mediaModel
controller: root.controller
}
}
Component {
id: expandedMediaComponent
MediaExpanded {
islandController: root.controller
geometrySettled: !motion.running
effectiveScreen: root.effectiveScreen
alignedX: root.targetVisualX
// Surface-local on purpose: the media dropdowns it anchors live inside this surface.
alignedY: root.targetVisualY
alignedWidth: root.targetVisualWidth
alignedHeight: root.targetVisualHeight
onShowVolumeDropdown: pos => root.openMediaDropdown(1, pos)
onShowAudioDevicesDropdown: pos => root.openMediaDropdown(2, pos)
onShowPlayersDropdown: pos => root.openMediaDropdown(3, pos)
onHideDropdowns: root.hideMediaDropdowns()
onDropdownHoverEnded: root.startMediaDropdownCloseTimer()
}
}
Component {
id: compactLauncherComponent
LauncherCompact {
controller: root.controller
}
}
Component {
id: expandedLauncherComponent
LauncherExpanded {
islandController: root.controller
launcherController: root.launcherController
transientSurfaceTracker: root.launcherTransientSurfaceTracker
effectiveScreen: root.effectiveScreen
// Settled bounds; animated values rebind the tree every frame.
alignedX: root.targetVisualX
alignedY: root.targetScreenY
}
}
Component {
id: compactControlCenterComponent
ControlCenterCompact {
controller: root.controller
}
}
Component {
id: expandedControlCenterComponent
ControlCenterExpanded {
islandController: root.controller
effectiveScreen: root.effectiveScreen
colorPickerModal: root.colorPickerModal
powerMenuModalLoader: root.powerMenuModalLoader
// Settled bounds; animated values rebind the tree every frame.
alignedX: root.targetVisualX
alignedY: root.targetScreenY
alignedWidth: root.targetVisualWidth
alignedHeight: root.targetVisualHeight
onLockRequested: root.lockRequested()
}
}
Component {
id: compactWallpaperComponent
WallpaperCompact {
controller: root.controller
}
}
Component {
id: expandedWallpaperComponent
WallpaperExpanded {
islandController: root.controller
effectiveScreen: root.effectiveScreen
}
}
Component {
id: compactWeatherComponent
WeatherCompact {
controller: root.controller
}
}
Component {
id: expandedWeatherComponent
WeatherExpanded {
islandController: root.controller
}
}
Component {
id: compactSystemComponent
SystemLevelCompact {
systemModel: root.systemModel
dense: root.controller.compactDense
iconSize: root.controller.compactIconSize
}
}
Component {
id: expandedSystemComponent
SystemLevelExpanded {
systemModel: root.systemModel
}
}
Component {
id: compactNotificationComponent
NotificationCompact {
notificationModel: root.notificationModel
dense: root.controller.compactDense
iconSize: root.controller.compactIconSize
}
}
Component {
id: compactNotificationCenterComponent
NotificationCenterCompact {
controller: root.controller
}
}
Component {
id: expandedNotificationCenterComponent
NotificationCenterExpanded {
islandController: root.controller
effectiveScreen: root.effectiveScreen
transientSurfaceTracker: root.notificationTransientSurfaceTracker
}
}
Component {
id: expandedNotificationComponent
NotificationExpanded {
notificationModel: root.notificationModel
}
}
}
@@ -0,0 +1,115 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.Mpris
import qs.Services
QtObject {
id: root
required property IslandController controller
readonly property bool demoEnabled: Quickshell.env("DMS_DANKISLAND_MEDIA_DEMO") === "1"
readonly property var demoTracks: [
{
"title": "DankIsland Motion Study",
"artist": "DankMaterialShell",
"album": "Physical Springs",
"icon": "applications-multimedia"
},
{
"title": "Event Horizon",
"artist": "Quickshell Ensemble",
"album": "Signal Driven",
"icon": "audio-x-generic"
},
{
"title": "Warm Surface",
"artist": "Scene Graph",
"album": "Sixty Frames",
"icon": "multimedia-player"
}
]
readonly property var demoTrack: demoTracks[demoTrackIndex]
readonly property var player: MprisController.activePlayer
readonly property bool realMediaAvailable: !!player && player.playbackState !== MprisPlaybackState.Stopped
readonly property bool available: demoEnabled || realMediaAvailable
readonly property string title: demoEnabled ? demoTrack.title : (MprisController.stableTitle || player?.trackTitle || "Unknown track")
readonly property string artist: demoEnabled ? demoTrack.artist : (MprisController.stableArtist || player?.trackArtist || player?.identity || "Unknown artist")
readonly property string album: demoEnabled ? demoTrack.album : (MprisController.stableAlbum || player?.trackAlbum || "")
readonly property string artUrl: demoEnabled ? Quickshell.iconPath(demoTrack.icon, true) : TrackArtService.resolvedArtUrl
readonly property bool playing: demoEnabled ? demoPlaying : !!player?.isPlaying
readonly property bool canToggle: demoEnabled || !!player?.canTogglePlaying
readonly property bool canGoPrevious: demoEnabled || !!player?.canGoPrevious
readonly property bool canGoNext: demoEnabled || !!player?.canGoNext
readonly property bool canSeek: demoEnabled || (!!player?.canSeek && length > 0)
readonly property real position: demoEnabled ? demoPosition : Math.max(0, player?.position || 0)
readonly property real length: demoEnabled ? 243 : Math.max(0, MprisController.activePlayerStableLength)
readonly property real progress: length > 0 ? Math.max(0, Math.min(1, position / length)) : 0
property int demoTrackIndex: 0
property bool demoPlaying: true
property real demoPosition: 74
function advanceDemoTrack(delta) {
demoTrackIndex = (demoTrackIndex + delta + demoTracks.length) % demoTracks.length;
demoPosition = 0;
}
function syncAvailability() {
controller.updateMediaAvailability(available);
}
function togglePlaying() {
if (demoEnabled) {
demoPlaying = !demoPlaying;
return;
}
if (player?.canTogglePlaying)
player.togglePlaying();
}
function previous() {
if (demoEnabled) {
advanceDemoTrack(-1);
return;
}
MprisController.previousOrRewind();
}
function next() {
if (demoEnabled) {
advanceDemoTrack(1);
return;
}
MprisController.next();
}
function seekRatio(ratio) {
if (!canSeek || length <= 0)
return;
const nextPosition = Math.max(0.1, Math.min(length - 0.1, length * Math.max(0, Math.min(1, ratio))));
if (demoEnabled)
demoPosition = nextPosition;
else
player.position = nextPosition;
}
Component.onCompleted: syncAvailability()
onAvailableChanged: syncAvailability()
property Timer demoTimer: Timer {
interval: 1000
repeat: true
running: root.demoEnabled && root.demoPlaying
onTriggered: root.demoPosition = (root.demoPosition + 1) % root.length
}
property Timer demoTrackTimer: Timer {
interval: 4200
repeat: true
running: root.demoEnabled
onTriggered: root.advanceDemoTrack(1)
}
}
@@ -0,0 +1,244 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import qs.Common
import qs.Services
QtObject {
id: root
required property IslandController controller
required property var targetScreen
property var currentWrapper: null
property string appName: ""
property string summary: ""
property string body: ""
property string timeText: ""
property string imageSource: ""
property string fallbackIcon: "material:notifications"
property string fallbackText: ""
property string actionLabel: ""
property bool critical: false
property int demoIndex: 0
readonly property bool demoEnabled: Quickshell.env("DMS_DANKISLAND_NOTIFICATION_DEMO") === "1"
readonly property bool hasAction: actionLabel.length > 0
// Keep the wrapper alive while the island is hovered or expanded, matching NotificationPopup.
readonly property bool wrapperTimeoutHeld: controller.notificationActive && controller.timeoutSuspended
function isFocusedScreen() {
if (!SettingsData.notificationFocusedMonitor)
return true;
const focused = CompositorService.getFocusedScreen();
return !!focused && !!root.targetScreen && focused.name === root.targetScreen.name;
}
// Hold display fields through the outgoing crossfade.
function scheduleDisplayFieldClear() {
displayFieldClearTimer.restart();
}
function clearDisplayFields() {
if (currentWrapper)
return;
appName = "";
summary = "";
body = "";
timeText = "";
imageSource = "";
fallbackIcon = "material:notifications";
fallbackText = "";
actionLabel = "";
critical = false;
}
function wrapperTimeout(wrapper) {
const interval = wrapper?.timer?.interval ?? -1;
return interval >= 0 ? interval : 5000;
}
function plainText(value) {
return String(value || "").replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
}
function resolveImage(wrapper) {
const image = wrapper?.cleanImage || "";
if (image)
return image;
const icon = wrapper?.appIcon || "";
if (icon.startsWith("file://") || icon.startsWith("http://") || icon.startsWith("https://") || icon.includes("/"))
return icon;
return "";
}
function resolveFallbackIcon(wrapper) {
const rawImage = wrapper?.image || "";
if (rawImage.startsWith("image://icon/"))
return rawImage.substring(13);
return wrapper?.appIcon || "material:notifications";
}
function applyWrapperTimeoutHold() {
const timer = currentWrapper?.timer;
if (!timer)
return;
if (wrapperTimeoutHeld) {
timer.stop();
return;
}
if (currentWrapper.popup && timer.interval > 0)
timer.restart();
}
function releaseWrapperTimeoutHold() {
const timer = currentWrapper?.timer;
if (!timer || !currentWrapper.popup || timer.interval <= 0)
return;
timer.restart();
}
function adopt(wrapper) {
if (!wrapper || !isFocusedScreen())
return false;
const isCritical = wrapper.urgency === NotificationUrgency.Critical;
if (!controller.canRequestNotification(isCritical))
return false;
releaseWrapperTimeoutHold();
displayFieldClearTimer.stop();
currentWrapper = wrapper;
appName = wrapper.appName || "Notification";
summary = plainText(wrapper.summary) || appName;
body = plainText(wrapper.body);
timeText = wrapper.timeStr || "Now";
imageSource = resolveImage(wrapper);
fallbackIcon = resolveFallbackIcon(wrapper);
fallbackText = appName.charAt(0).toUpperCase();
critical = isCritical;
const actions = wrapper.actions || [];
actionLabel = actions.length > 0 ? (actions[0].text || "Open") : "";
controller.notificationTimeout = wrapperTimeout(wrapper);
const accepted = controller.requestNotification(isCritical);
applyWrapperTimeoutHold();
return accepted;
}
function refreshDeduplicated(wrapper) {
if (!wrapper || wrapper !== currentWrapper)
return;
controller.refreshNotification();
applyWrapperTimeoutHold();
}
onWrapperTimeoutHeldChanged: applyWrapperTimeoutHold()
function syncVisibleNotifications() {
if (demoEnabled)
return;
const visible = NotificationService.visibleNotifications || [];
if (visible.length > 0) {
const latest = visible[visible.length - 1];
if (latest === currentWrapper || adopt(latest))
return;
}
if (currentWrapper && visible.indexOf(currentWrapper) === -1) {
currentWrapper = null;
controller.completeNotification();
scheduleDisplayFieldClear();
}
}
function activate() {
if (demoEnabled) {
controller.completeNotification();
return;
}
const actions = currentWrapper?.actions || [];
if (actions.length > 0)
actions[0].invoke();
dismiss();
}
function dismiss() {
const wrapper = currentWrapper;
currentWrapper = null;
controller.completeNotification();
scheduleDisplayFieldClear();
if (!demoEnabled && wrapper)
NotificationService.dismissNotification(wrapper);
}
function showDemo() {
const variants = [
{
"appName": "Messages",
"summary": "New message from Alex",
"body": "The new activity transition looks great. Are we still on for tomorrow?",
"icon": "material:mark_chat_unread",
"action": "Reply"
},
{
"appName": "Calendar",
"summary": "Design review in 10 minutes",
"body": "DankIsland activity protocol · Studio call",
"icon": "material:event_upcoming",
"action": "Open"
}
];
const demo = variants[demoIndex % variants.length];
demoIndex += 1;
if (!controller.canRequestNotification(false))
return;
releaseWrapperTimeoutHold();
displayFieldClearTimer.stop();
currentWrapper = null;
appName = demo.appName;
summary = demo.summary;
body = demo.body;
timeText = "Now";
imageSource = "";
fallbackIcon = demo.icon;
fallbackText = demo.appName.charAt(0);
actionLabel = demo.action;
critical = false;
controller.notificationTimeout = 5000;
controller.requestNotification(false);
}
property Connections notificationConnections: Connections {
target: NotificationService
function onVisibleNotificationsChanged() {
root.syncVisibleNotifications();
}
function onNotificationDeduplicated(wrapper) {
root.refreshDeduplicated(wrapper);
}
}
property Timer displayFieldClearTimer: Timer {
interval: 400
onTriggered: root.clearDisplayFields()
}
property Timer demoTimer: Timer {
interval: 7000
repeat: true
running: root.demoEnabled
onTriggered: root.showDemo()
}
Component.onCompleted: {
if (demoEnabled)
Qt.callLater(showDemo);
else
syncVisibleNotifications();
}
}
@@ -0,0 +1,145 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Services
QtObject {
id: root
required property IslandController controller
property string kind: "volume"
property real demoRatio: 0.68
readonly property bool demoEnabled: Quickshell.env("DMS_DANKISLAND_SYSTEM_DEMO") === "1"
readonly property bool volumeActivity: kind === "volume"
readonly property bool available: demoEnabled || (volumeActivity ? !!AudioService.sink?.audio : DisplayService.brightnessAvailable)
readonly property bool muted: !demoEnabled && volumeActivity && (AudioService.sink?.audio?.muted ?? false)
readonly property real value: demoEnabled ? demoRatio * 100 : volumeActivity ? Math.min(AudioService.sinkMaxVolume, Math.round((AudioService.sink?.audio?.volume ?? 0) * 100)) : DisplayService.brightnessLevel
readonly property real maximum: {
if (demoEnabled)
return 100;
if (volumeActivity)
return AudioService.sinkMaxVolume;
const deviceInfo = DisplayService.getCurrentDeviceInfo();
if (!deviceInfo)
return 100;
if (SessionData.getBrightnessExponential(deviceInfo.id))
return 100;
return deviceInfo.displayMax || 100;
}
readonly property int minimum: {
if (volumeActivity || demoEnabled)
return 0;
const deviceInfo = DisplayService.getCurrentDeviceInfo();
if (!deviceInfo)
return 1;
if (SessionData.getBrightnessExponential(deviceInfo.id))
return 1;
return (deviceInfo.class === "backlight" || deviceInfo.class === "ddc") ? 1 : 0;
}
readonly property real ratio: maximum > 0 ? Math.max(0, Math.min(1, value / maximum)) : 0
readonly property string title: volumeActivity ? "Volume" : "Brightness"
readonly property string unit: {
if (volumeActivity)
return "%";
const deviceInfo = demoEnabled ? null : DisplayService.getCurrentDeviceInfo();
if (!deviceInfo)
return "%";
if (SessionData.getBrightnessExponential(deviceInfo.id))
return "%";
return deviceInfo.class === "ddc" ? "" : "%";
}
readonly property string displayValue: muted ? I18n.tr("Muted") : Math.round(value) + unit
readonly property string iconName: {
if (!volumeActivity) {
const deviceInfo = demoEnabled ? null : DisplayService.getCurrentDeviceInfo();
if (!demoEnabled && (!available || !deviceInfo))
return "brightness_low";
if (demoEnabled || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") {
if (ratio <= 0.33)
return "brightness_low";
return ratio <= 0.66 ? "brightness_medium" : "brightness_high";
}
if (String(deviceInfo.name ?? "").includes("kbd"))
return "keyboard";
return "lightbulb";
}
if (!available || muted)
return "volume_off";
const volume = demoEnabled ? ratio : (AudioService.sink?.audio?.volume ?? 0);
if (volume === 0)
return "volume_mute";
return volume <= 0.33 ? "volume_down" : "volume_up";
}
function show(activityKind) {
if (controller.requestSystemActivity(activityKind))
kind = activityKind;
}
function setRatio(nextRatio) {
const clampedRatio = Math.max(0, Math.min(1, nextRatio));
if (demoEnabled) {
demoRatio = clampedRatio;
return;
}
SessionData.suppressOSDTemporarily();
if (volumeActivity)
AudioService.setVolume(Math.round(clampedRatio * maximum));
else
DisplayService.setBrightness(Math.round(clampedRatio * maximum), DisplayService.lastIpcDevice, true);
}
function toggleMute() {
if (!volumeActivity || !available)
return;
if (demoEnabled)
return;
SessionData.suppressOSDTemporarily();
AudioService.toggleMute();
}
property Connections audioConnection: Connections {
target: AudioService.sink?.audio ?? null
function onVolumeChanged() {
if (SettingsData.osdVolumeEnabled)
root.show("volume");
}
function onMutedChanged() {
if (SettingsData.osdVolumeEnabled)
root.show("volume");
}
}
property Connections brightnessConnection: Connections {
target: DisplayService
function onBrightnessChanged(showOsd) {
if (showOsd && SettingsData.osdBrightnessEnabled)
root.show("brightness");
}
}
property Timer demoTimer: Timer {
interval: 3200
repeat: true
running: root.demoEnabled
onTriggered: {
const nextKind = root.kind === "volume" ? "brightness" : "volume";
root.demoRatio = nextKind === "volume" ? 0.68 : 0.82;
root.show(nextKind);
}
}
Component.onCompleted: {
if (demoEnabled)
show("volume");
}
}
@@ -0,0 +1,339 @@
pragma ComponentBehavior: Bound
import QtQuick
Item {
id: root
required property real morphProgress
required property bool expanded
required property bool pointerInside
required property string activityId
required property Component homeCompactComponent
required property Component homeExpandedComponent
required property Component mediaCompactComponent
required property Component mediaExpandedComponent
required property Component launcherCompactComponent
required property Component launcherExpandedComponent
required property bool launcherVisualsRequested
required property Component controlCenterCompactComponent
required property Component controlCenterExpandedComponent
required property bool controlCenterVisualsRequested
required property Component wallpaperCompactComponent
required property Component wallpaperExpandedComponent
required property bool wallpaperVisualsRequested
required property Component weatherCompactComponent
required property Component weatherExpandedComponent
required property bool weatherVisualsRequested
required property Component systemCompactComponent
required property Component systemExpandedComponent
required property Component notificationCompactComponent
required property Component notificationExpandedComponent
required property Component notificationCenterCompactComponent
required property Component notificationCenterExpandedComponent
required property bool notificationCenterVisualsRequested
readonly property real compactFade: root.fadeCompact(root.morphProgress)
readonly property real expandedFade: root.fadeExpanded(root.morphProgress)
// Outgoing faces freeze at the morph they were drawn at so they do not flash expanded.
readonly property real outgoingCompactFade: root.fadeCompact(root.outgoingMorph)
readonly property real outgoingExpandedFade: root.fadeExpanded(root.outgoingMorph)
readonly property bool mediaSurfaceActive: root.activityId === "media" || root.outgoingActivity === "media"
property string renderedActivity: "home"
property string outgoingActivity: ""
property real activityFade: 1
property real outgoingMorph: 0
property bool homeExpandedTouched: false
function fadeCompact(morph) {
return 1 - Math.max(0, Math.min(1, morph / 0.34));
}
function fadeExpanded(morph) {
return Math.max(0, Math.min(1, (morph - 0.22) / 0.42));
}
function surfaceActive(activity) {
return root.renderedActivity === activity || root.outgoingActivity === activity;
}
function compactOpacity(activity) {
const incoming = root.renderedActivity === activity ? root.activityFade * root.compactFade : 0;
const outgoing = root.outgoingActivity === activity ? (1 - root.activityFade) * root.outgoingCompactFade : 0;
return Math.max(incoming, outgoing);
}
function expandedOpacity(activity) {
const incoming = root.renderedActivity === activity ? root.activityFade * root.expandedFade : 0;
const outgoing = root.outgoingActivity === activity ? (1 - root.activityFade) * root.outgoingExpandedFade : 0;
return Math.max(incoming, outgoing);
}
function requestActivityFocus() {
if (root.activityId === "launcher" && launcherExpandedLoader.item) {
launcherExpandedLoader.item.focusSearch();
return true;
}
if (root.activityId === "home" && homeExpandedLoader.item) {
homeExpandedLoader.item.focusOverview();
return true;
}
if (root.activityId === "media" && mediaExpandedLoader.item) {
mediaExpandedLoader.item.focusPlayer();
return true;
}
if (root.activityId === "wallpaper" && wallpaperExpandedLoader.item) {
wallpaperExpandedLoader.item.focusGrid();
return true;
}
if (root.activityId === "weather" && weatherExpandedLoader.item) {
weatherExpandedLoader.item.focusWeather();
return true;
}
if (root.activityId === "notificationcenter" && notificationCenterExpandedLoader.item) {
notificationCenterExpandedLoader.item.focusList();
return true;
}
return false;
}
function latchHomeExpanded() {
if (!homeExpandedTouched && expanded && activityId === "home")
homeExpandedTouched = true;
}
// Overview is latched on first pointer intent, not at startup.
onPointerInsideChanged: {
if (root.pointerInside)
root.homeExpandedTouched = true;
}
onExpandedChanged: latchHomeExpanded()
onActivityIdChanged: {
latchHomeExpanded();
if (activityId === renderedActivity)
return;
outgoingMorph = morphProgress;
outgoingActivity = renderedActivity;
renderedActivity = activityId;
activityFade = 0;
activityTransition.restart();
}
SequentialAnimation {
id: activityTransition
PauseAnimation {
duration: 45
}
NumberAnimation {
target: root
property: "activityFade"
to: 1
duration: 180
easing.type: Easing.OutCubic
}
ScriptAction {
script: root.outgoingActivity = ""
}
}
Loader {
anchors.fill: parent
active: true
asynchronous: false
sourceComponent: root.homeCompactComponent
opacity: root.compactOpacity("home")
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
id: homeExpandedLoader
anchors.fill: parent
// Latched: built on first expansion, then kept so later morphs never hitch.
active: root.homeExpandedTouched
asynchronous: true
sourceComponent: root.homeExpandedComponent
opacity: root.expandedOpacity("home")
visible: active || opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
anchors.fill: parent
active: root.mediaSurfaceActive
asynchronous: false
sourceComponent: root.mediaCompactComponent
opacity: root.compactOpacity("media")
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
id: mediaExpandedLoader
anchors.fill: parent
active: root.mediaSurfaceActive && (root.expanded || root.expandedFade > 0)
asynchronous: false
sourceComponent: root.mediaExpandedComponent
opacity: root.expandedOpacity("media")
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
anchors.fill: parent
active: root.surfaceActive("launcher")
asynchronous: false
sourceComponent: root.launcherCompactComponent
opacity: root.compactOpacity("launcher")
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
id: launcherExpandedLoader
anchors.fill: parent
active: root.launcherVisualsRequested
asynchronous: true
sourceComponent: root.launcherExpandedComponent
opacity: root.expandedOpacity("launcher")
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
anchors.fill: parent
active: root.surfaceActive("controlcenter")
asynchronous: false
sourceComponent: root.controlCenterCompactComponent
opacity: root.compactOpacity("controlcenter")
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
id: controlCenterExpandedLoader
anchors.fill: parent
active: root.controlCenterVisualsRequested
asynchronous: false
sourceComponent: root.controlCenterExpandedComponent
opacity: root.expandedOpacity("controlcenter")
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
anchors.fill: parent
active: root.surfaceActive("wallpaper")
asynchronous: false
sourceComponent: root.wallpaperCompactComponent
opacity: root.compactOpacity("wallpaper")
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
id: wallpaperExpandedLoader
anchors.fill: parent
active: root.wallpaperVisualsRequested
asynchronous: true
sourceComponent: root.wallpaperExpandedComponent
opacity: root.expandedOpacity("wallpaper")
visible: active || opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
anchors.fill: parent
active: root.surfaceActive("weather")
asynchronous: false
sourceComponent: root.weatherCompactComponent
opacity: root.compactOpacity("weather")
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
id: weatherExpandedLoader
anchors.fill: parent
active: root.weatherVisualsRequested
asynchronous: true
sourceComponent: root.weatherExpandedComponent
opacity: root.expandedOpacity("weather")
visible: active || opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
anchors.fill: parent
active: root.surfaceActive("notificationcenter")
asynchronous: false
sourceComponent: root.notificationCenterCompactComponent
opacity: root.compactOpacity("notificationcenter")
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
id: notificationCenterExpandedLoader
anchors.fill: parent
active: root.notificationCenterVisualsRequested
asynchronous: true
sourceComponent: root.notificationCenterExpandedComponent
opacity: root.expandedOpacity("notificationcenter")
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
anchors.fill: parent
active: root.surfaceActive("volume") || root.surfaceActive("brightness")
asynchronous: false
sourceComponent: root.systemCompactComponent
opacity: Math.max(root.compactOpacity("volume"), root.compactOpacity("brightness"))
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
anchors.fill: parent
active: (root.surfaceActive("volume") || root.surfaceActive("brightness")) && (root.expanded || root.expandedFade > 0)
asynchronous: false
sourceComponent: root.systemExpandedComponent
opacity: Math.max(root.expandedOpacity("volume"), root.expandedOpacity("brightness"))
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
anchors.fill: parent
active: root.surfaceActive("notification")
asynchronous: false
sourceComponent: root.notificationCompactComponent
opacity: root.compactOpacity("notification")
visible: opacity > 0.001
enabled: opacity >= 0.5
}
Loader {
anchors.fill: parent
active: root.surfaceActive("notification") && (root.expanded || root.expandedFade > 0)
asynchronous: false
sourceComponent: root.notificationExpandedComponent
opacity: root.expandedOpacity("notification")
visible: opacity > 0.001
enabled: opacity >= 0.5
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,215 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Modules.DankBar
import qs.Services
Item {
id: root
required property var hostWindow
required property var targetScreen
required property IslandController controller
required property var islandSurface
property real outerGap: 8
readonly property alias leftInputItem: leftInputEnvelope
readonly property alias rightInputItem: rightInputEnvelope
readonly property var referenceBarConfig: {
SettingsData.barConfigs;
return SettingsData.islandBarConfig ?? ({});
}
readonly property var satelliteConfig: {
const base = SettingsData.effectiveBarConfigForRender(root.referenceBarConfig, false) ?? root.referenceBarConfig ?? ({});
if (!root.hostWindow?.usesOverlayLayer)
return base;
const next = Object.assign({}, base);
next.useOverlayLayer = true;
return next;
}
readonly property bool scrollEnabled: satelliteConfig?.scrollEnabled ?? true
readonly property string scrollXBehavior: satelliteConfig?.scrollXBehavior ?? "column"
readonly property string scrollYBehavior: satelliteConfig?.scrollYBehavior ?? "workspace"
readonly property real screenScale: CompositorService.getScreenScale(root.targetScreen)
readonly property real innerPadding: root.satelliteConfig?.innerPadding ?? 4
readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + root.innerPadding * 0.6), root.screenScale)
readonly property real barThickness: Theme.snap(Math.max(root.widgetThickness + root.innerPadding + 4, Theme.barHeight - 4 - (8 - root.innerPadding)), root.screenScale)
readonly property real satelliteSpacing: root.satelliteConfig?.spacing ?? 4
readonly property bool edgeAligned: SettingsData.dankIslandSatellitePosition === "edges"
readonly property real edgeBaseMargin: Math.max(Theme.spacingXS, root.innerPadding * 0.8)
readonly property real edgeInsetRaw: SettingsData.barInsetPaddingSyncAll ? SettingsData.barInsetPaddingShared : (root.satelliteConfig?.barInsetPadding ?? -1)
readonly property real edgeInset: Theme.snap(Math.max(0, root.edgeInsetRaw < 0 ? root.edgeBaseMargin : root.edgeInsetRaw), root.screenScale)
readonly property bool bottomEdge: SettingsData.dankIslandEdge === "bottom"
readonly property real rowY: root.bottomEdge ? root.height - root.outerGap - root.barThickness : root.outerGap
readonly property bool tracksIsland: !root.edgeAligned && root.visible
readonly property bool motionRunning: root.tracksIsland && (root.islandSurface?.motionRunning ?? false)
readonly property real islandStartX: root.islandSurface?.motionStartBounds.x ?? 0
readonly property real islandStartRight: root.islandStartX + (root.islandSurface?.motionStartBounds.width ?? 0)
readonly property real islandTargetX: root.islandSurface?.targetVisualX ?? 0
readonly property real islandTargetRight: root.islandTargetX + (root.islandSurface?.targetVisualWidth ?? 0)
readonly property real leftEdgeSpread: root.motionRunning ? Math.abs(root.islandStartX - root.islandTargetX) : 0
readonly property real rightEdgeSpread: root.motionRunning ? Math.abs(root.islandStartRight - root.islandTargetRight) : 0
function normalizedWidgets(widgets) {
return (widgets || []).map((widget, index) => {
if (typeof widget === "string") {
return {
"widgetId": widget,
"id": widget + "_" + index,
"enabled": true
};
}
const normalized = Object.assign({}, widget);
normalized.widgetId = widget.widgetId || widget.id;
normalized.id = normalized.widgetId + "_" + index;
normalized.enabled = widget.enabled !== false;
return normalized;
});
}
visible: SettingsData.dankIslandSatellitesEnabled
function switchWorkspace(direction) {
componentProvider.switchWorkspace(direction);
}
AxisContext {
id: satelliteAxis
edge: SettingsData.dankIslandEdge
}
ScriptModel {
id: emptyWidgetsModel
values: []
}
ScriptModel {
id: leftWidgetsModel
values: root.normalizedWidgets(root.referenceBarConfig?.leftWidgets)
}
ScriptModel {
id: rightWidgetsModel
values: root.normalizedWidgets(root.referenceBarConfig?.rightWidgets)
}
QtObject {
id: satelliteBarWindow
property var axis: satelliteAxis
property var screen: root.targetScreen
property string screenName: root.targetScreen?.name ?? ""
property real widgetThickness: root.widgetThickness
property real effectiveBarThickness: root.barThickness
property bool isVertical: false
property bool usesFrameBarChrome: false
property bool hasAdjacentTopBar: false
property bool hasAdjacentBottomBar: false
property bool hasAdjacentLeftBar: false
property bool hasAdjacentRightBar: false
property var hyprlandOverviewLoader: null
property var controlCenterButtonRef: null
property var clockButtonRef: null
property var systemUpdateButtonRef: null
property int notificationCount: 0
signal colorPickerRequested
function registerBlurWidget(item) {
}
function unregisterBlurWidget(item) {
}
}
DankBarContent {
id: componentProvider
width: 0
height: 0
visible: false
barWindow: satelliteBarWindow
rootWindow: root.hostWindow
barConfig: root.satelliteConfig
leftWidgetsModel: emptyWidgetsModel
centerWidgetsModel: emptyWidgetsModel
rightWidgetsModel: emptyWidgetsModel
}
Item {
id: leftInputEnvelope
x: root.motionRunning ? Math.min(root.islandStartX, root.islandTargetX) - SettingsData.dankIslandSatelliteGap - leftInput.width : leftInput.x
y: leftInput.y
width: leftInput.width + root.leftEdgeSpread
height: leftInput.height
}
Item {
id: rightInputEnvelope
x: root.motionRunning ? Math.min(root.islandStartRight, root.islandTargetRight) + SettingsData.dankIslandSatelliteGap : rightInput.x
y: rightInput.y
width: rightInput.width + root.rightEdgeSpread
height: rightInput.height
}
Item {
id: leftInput
x: root.edgeAligned ? root.edgeInset : root.islandSurface.currentVisualX - SettingsData.dankIslandSatelliteGap - width
y: root.rowY
width: root.visible ? leftWidgetSection.implicitWidth : 0
height: root.visible ? root.barThickness : 0
LeftSection {
id: leftWidgetSection
anchors.fill: parent
objectName: "leftSection"
axis: satelliteAxis
widgetsModel: leftWidgetsModel
components: componentProvider.allComponents
noBackground: root.satelliteConfig?.noBackground ?? false
parentScreen: root.targetScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
barSpacing: root.satelliteSpacing
barConfig: root.satelliteConfig
blurBarWindow: satelliteBarWindow
}
}
Item {
id: rightInput
x: root.edgeAligned ? root.width - root.edgeInset - width : root.islandSurface.currentVisualX + root.islandSurface.currentVisualWidth + SettingsData.dankIslandSatelliteGap
y: root.rowY
width: root.visible ? rightWidgetSection.implicitWidth : 0
height: root.visible ? root.barThickness : 0
RightSection {
id: rightWidgetSection
anchors.fill: parent
objectName: "rightSection"
axis: satelliteAxis
widgetsModel: rightWidgetsModel
components: componentProvider.allComponents
noBackground: root.satelliteConfig?.noBackground ?? false
parentScreen: root.targetScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
barSpacing: root.satelliteSpacing
barConfig: root.satelliteConfig
blurBarWindow: satelliteBarWindow
}
}
}
@@ -0,0 +1,176 @@
pragma ComponentBehavior: Bound
import QtQuick
QtObject {
id: root
property bool enabled: true
property bool animationsEnabled: true
property real spring: 5
property real damping: 0.32
property real mass: 1
property real epsilon: 0.04
property real currentWidth: 176
property real currentHeight: 42
property real currentOffsetX: 0
property real currentOffsetY: 8
property real currentTopLeftRadius: 21
property real currentTopRightRadius: 21
property real currentBottomLeftRadius: 21
property real currentBottomRightRadius: 21
property real targetWidth: currentWidth
property real targetHeight: currentHeight
property real targetOffsetX: currentOffsetX
property real targetOffsetY: currentOffsetY
property real targetTopLeftRadius: currentTopLeftRadius
property real targetTopRightRadius: currentTopRightRadius
property real targetBottomLeftRadius: currentBottomLeftRadius
property real targetBottomRightRadius: currentBottomRightRadius
readonly property bool running: widthSpring.running || heightSpring.running || offsetXSpring.running || offsetYSpring.running || topLeftRadiusSpring.running || topRightRadiusSpring.running
|| bottomLeftRadiusSpring.running || bottomRightRadiusSpring.running
function setTarget(target) {
targetWidth = target.width;
targetHeight = target.height;
targetOffsetX = target.offsetX;
targetOffsetY = target.offsetY;
targetTopLeftRadius = target.topLeftRadius;
targetTopRightRadius = target.topRightRadius;
targetBottomLeftRadius = target.bottomLeftRadius;
targetBottomRightRadius = target.bottomRightRadius;
currentWidth = targetWidth;
currentHeight = targetHeight;
currentOffsetX = targetOffsetX;
currentOffsetY = targetOffsetY;
currentTopLeftRadius = targetTopLeftRadius;
currentTopRightRadius = targetTopRightRadius;
currentBottomLeftRadius = targetBottomLeftRadius;
currentBottomRightRadius = targetBottomRightRadius;
}
function snapTo(target) {
animationsEnabled = false;
targetWidth = target.width;
targetHeight = target.height;
targetOffsetX = target.offsetX;
targetOffsetY = target.offsetY;
targetTopLeftRadius = target.topLeftRadius;
targetTopRightRadius = target.topRightRadius;
targetBottomLeftRadius = target.bottomLeftRadius;
targetBottomRightRadius = target.bottomRightRadius;
currentWidth = target.width;
currentHeight = target.height;
currentOffsetX = target.offsetX;
currentOffsetY = target.offsetY;
currentTopLeftRadius = target.topLeftRadius;
currentTopRightRadius = target.topRightRadius;
currentBottomLeftRadius = target.bottomLeftRadius;
currentBottomRightRadius = target.bottomRightRadius;
animationsEnabled = true;
}
Behavior on currentWidth {
enabled: root.enabled && root.animationsEnabled
SpringAnimation {
id: widthSpring
spring: root.spring
damping: root.damping
mass: root.mass
epsilon: root.epsilon
}
}
Behavior on currentHeight {
enabled: root.enabled && root.animationsEnabled
SpringAnimation {
id: heightSpring
spring: root.spring
damping: root.damping
mass: root.mass
epsilon: root.epsilon
}
}
Behavior on currentOffsetX {
enabled: root.enabled && root.animationsEnabled
SpringAnimation {
id: offsetXSpring
spring: root.spring
damping: root.damping
mass: root.mass
epsilon: root.epsilon
}
}
Behavior on currentOffsetY {
enabled: root.enabled && root.animationsEnabled
SpringAnimation {
id: offsetYSpring
spring: root.spring
damping: root.damping
mass: root.mass
epsilon: root.epsilon
}
}
Behavior on currentTopLeftRadius {
enabled: root.enabled && root.animationsEnabled
SpringAnimation {
id: topLeftRadiusSpring
spring: root.spring
damping: root.damping
mass: root.mass
epsilon: root.epsilon
}
}
Behavior on currentTopRightRadius {
enabled: root.enabled && root.animationsEnabled
SpringAnimation {
id: topRightRadiusSpring
spring: root.spring
damping: root.damping
mass: root.mass
epsilon: root.epsilon
}
}
Behavior on currentBottomLeftRadius {
enabled: root.enabled && root.animationsEnabled
SpringAnimation {
id: bottomLeftRadiusSpring
spring: root.spring
damping: root.damping
mass: root.mass
epsilon: root.epsilon
}
}
Behavior on currentBottomRightRadius {
enabled: root.enabled && root.animationsEnabled
SpringAnimation {
id: bottomRightRadiusSpring
spring: root.spring
damping: root.damping
mass: root.mass
epsilon: root.epsilon
}
}
}
@@ -0,0 +1,173 @@
pragma ComponentBehavior: Bound
import QtQuick
QtObject {
id: root
property bool enabled: true
property bool running: false
property bool reducedMotion: false
property real stiffness: 560
property real damping: 37
property real mass: 1
property real positionEpsilon: 0.035
property real velocityEpsilon: 0.035
property real maximumFrameTime: 1 / 30
property real integrationStep: 1 / 240
property real currentWidth: 176
property real currentHeight: 42
property real currentOffsetX: 0
property real currentOffsetY: 8
property real currentTopLeftRadius: 21
property real currentTopRightRadius: 21
property real currentBottomLeftRadius: 21
property real currentBottomRightRadius: 21
property real targetWidth: currentWidth
property real targetHeight: currentHeight
property real targetOffsetX: currentOffsetX
property real targetOffsetY: currentOffsetY
property real targetTopLeftRadius: currentTopLeftRadius
property real targetTopRightRadius: currentTopRightRadius
property real targetBottomLeftRadius: currentBottomLeftRadius
property real targetBottomRightRadius: currentBottomRightRadius
property real velocityWidth: 0
property real velocityHeight: 0
property real velocityOffsetX: 0
property real velocityOffsetY: 0
property real velocityTopLeftRadius: 0
property real velocityTopRightRadius: 0
property real velocityBottomLeftRadius: 0
property real velocityBottomRightRadius: 0
function setTarget(target) {
targetWidth = target.width;
targetHeight = target.height;
targetOffsetX = target.offsetX;
targetOffsetY = target.offsetY;
targetTopLeftRadius = target.topLeftRadius;
targetTopRightRadius = target.topRightRadius;
targetBottomLeftRadius = target.bottomLeftRadius;
targetBottomRightRadius = target.bottomRightRadius;
if (reducedMotion) {
settle();
return;
}
if (!isSettled())
running = true;
}
function snapTo(target) {
targetWidth = target.width;
targetHeight = target.height;
targetOffsetX = target.offsetX;
targetOffsetY = target.offsetY;
targetTopLeftRadius = target.topLeftRadius;
targetTopRightRadius = target.topRightRadius;
targetBottomLeftRadius = target.bottomLeftRadius;
targetBottomRightRadius = target.bottomRightRadius;
currentWidth = target.width;
currentHeight = target.height;
currentOffsetX = target.offsetX;
currentOffsetY = target.offsetY;
currentTopLeftRadius = target.topLeftRadius;
currentTopRightRadius = target.topRightRadius;
currentBottomLeftRadius = target.bottomLeftRadius;
currentBottomRightRadius = target.bottomRightRadius;
velocityWidth = 0;
velocityHeight = 0;
velocityOffsetX = 0;
velocityOffsetY = 0;
velocityTopLeftRadius = 0;
velocityTopRightRadius = 0;
velocityBottomLeftRadius = 0;
velocityBottomRightRadius = 0;
running = false;
}
function isSettled() {
const positionSettled = Math.abs(targetWidth - currentWidth) <= positionEpsilon && Math.abs(targetHeight - currentHeight) <= positionEpsilon
&& Math.abs(targetOffsetX - currentOffsetX) <= positionEpsilon && Math.abs(targetOffsetY - currentOffsetY) <= positionEpsilon
&& Math.abs(targetTopLeftRadius - currentTopLeftRadius) <= positionEpsilon && Math.abs(targetTopRightRadius - currentTopRightRadius) <= positionEpsilon
&& Math.abs(targetBottomLeftRadius - currentBottomLeftRadius) <= positionEpsilon && Math.abs(targetBottomRightRadius - currentBottomRightRadius) <= positionEpsilon;
const velocitySettled = Math.abs(velocityWidth) <= velocityEpsilon && Math.abs(velocityHeight) <= velocityEpsilon && Math.abs(velocityOffsetX) <= velocityEpsilon
&& Math.abs(velocityOffsetY) <= velocityEpsilon && Math.abs(velocityTopLeftRadius) <= velocityEpsilon && Math.abs(velocityTopRightRadius) <= velocityEpsilon
&& Math.abs(velocityBottomLeftRadius) <= velocityEpsilon && Math.abs(velocityBottomRightRadius) <= velocityEpsilon;
return positionSettled && velocitySettled;
}
function settle() {
currentWidth = targetWidth;
currentHeight = targetHeight;
currentOffsetX = targetOffsetX;
currentOffsetY = targetOffsetY;
currentTopLeftRadius = targetTopLeftRadius;
currentTopRightRadius = targetTopRightRadius;
currentBottomLeftRadius = targetBottomLeftRadius;
currentBottomRightRadius = targetBottomRightRadius;
velocityWidth = 0;
velocityHeight = 0;
velocityOffsetX = 0;
velocityOffsetY = 0;
velocityTopLeftRadius = 0;
velocityTopRightRadius = 0;
velocityBottomLeftRadius = 0;
velocityBottomRightRadius = 0;
running = false;
}
function advance(rawFrameTime) {
if (!enabled || !running || reducedMotion)
return;
const frameTime = Math.min(Math.max(rawFrameTime, 0), maximumFrameTime);
if (frameTime <= 0)
return;
const steps = Math.max(1, Math.ceil(frameTime / integrationStep));
const step = frameTime / steps;
const inverseMass = 1 / Math.max(0.001, mass);
for (let i = 0; i < steps; i++) {
velocityWidth += (stiffness * (targetWidth - currentWidth) - damping * velocityWidth) * inverseMass * step;
velocityHeight += (stiffness * (targetHeight - currentHeight) - damping * velocityHeight) * inverseMass * step;
velocityOffsetX += (stiffness * (targetOffsetX - currentOffsetX) - damping * velocityOffsetX) * inverseMass * step;
velocityOffsetY += (stiffness * (targetOffsetY - currentOffsetY) - damping * velocityOffsetY) * inverseMass * step;
velocityTopLeftRadius += (stiffness * (targetTopLeftRadius - currentTopLeftRadius) - damping * velocityTopLeftRadius) * inverseMass * step;
velocityTopRightRadius += (stiffness * (targetTopRightRadius - currentTopRightRadius) - damping * velocityTopRightRadius) * inverseMass * step;
velocityBottomLeftRadius += (stiffness * (targetBottomLeftRadius - currentBottomLeftRadius) - damping * velocityBottomLeftRadius) * inverseMass * step;
velocityBottomRightRadius += (stiffness * (targetBottomRightRadius - currentBottomRightRadius) - damping * velocityBottomRightRadius) * inverseMass * step;
currentWidth += velocityWidth * step;
currentHeight += velocityHeight * step;
currentOffsetX += velocityOffsetX * step;
currentOffsetY += velocityOffsetY * step;
currentTopLeftRadius += velocityTopLeftRadius * step;
currentTopRightRadius += velocityTopRightRadius * step;
currentBottomLeftRadius += velocityBottomLeftRadius * step;
currentBottomRightRadius += velocityBottomRightRadius * step;
}
if (isSettled())
settle();
}
onReducedMotionChanged: {
if (reducedMotion)
settle();
}
property FrameAnimation frameAnimation: FrameAnimation {
running: root.enabled && root.running && !root.reducedMotion
onTriggered: root.advance(frameTime)
}
}
+5 -1
View File
@@ -53,7 +53,7 @@ Item {
}
readonly property real barSpacing: {
const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
const defaultBar = SettingsData.getPrimaryBarConfig();
if (!defaultBar)
return 0;
@@ -84,6 +84,8 @@ Item {
const topBar = SettingsData.barConfigs.find(bc => {
if (!bc.enabled || bc.autoHide || !(bc.visible ?? true))
return false;
if (SettingsData.isIslandBarConfig(bc))
return false;
if (bc.position !== SettingsData.Position.Top && bc.position !== 0)
return false;
const onThisScreen = bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all") || bc.screenPreferences.includes(screenName);
@@ -99,6 +101,8 @@ Item {
const leftBar = SettingsData.barConfigs.find(bc => {
if (!bc.enabled || bc.autoHide || !(bc.visible ?? true))
return false;
if (SettingsData.isIslandBarConfig(bc))
return false;
if (bc.position !== SettingsData.Position.Left && bc.position !== 2)
return false;
const onThisScreen = bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all") || bc.screenPreferences.includes(screenName);
@@ -27,6 +27,10 @@ Item {
continue;
if (!SettingsData.barConfigCoversScreen(bc, host.targetScreen))
continue;
if (SettingsData.isIslandBarConfig(bc))
continue;
if (SettingsData.dankIslandOwnsEdge(host.targetScreen, SettingsData.positionToSide(bc.position ?? SettingsData.Position.Top)))
continue;
let edge = "top";
switch (bc.position ?? 0) {
case SettingsData.Position.Bottom:
@@ -13,25 +13,107 @@ DankListView {
property alias listContentHeight: listView.contentHeight
property real stableContentHeight: 0
property bool cardAnimateExpansion: true
property bool trackStableContentHeight: true
property bool trackSessionContentHeight: false
property bool lightweightCards: false
property bool listInitialized: false
property int swipingCardIndex: -1
property real swipingCardOffset: 0
property bool _stableHeightUpdatePending: false
property bool _sessionHeightUpdatePending: false
property real sessionContentHeight: 0
property var transientSurfaceTracker: null
readonly property real shadowBlurPx: Theme.elevationEnabled ? ((Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined) ? Theme.elevationLevel1.blurPx : 4) : 0
readonly property real shadowHorizontalGutter: Theme.snap(Math.max(Theme.spacingS, Math.min(32, shadowBlurPx * 1.5 + 6)), 1)
readonly property real shadowVerticalGutter: Theme.snap(Math.max(Theme.spacingXS, 6), 1)
readonly property real delegateShadowGutter: Theme.snap(Math.max(Theme.spacingXS, 4), 1)
readonly property real estimatedCollapsedCardHeight: {
const compact = SettingsData.notificationCompactMode;
const padding = compact ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding;
const icon = compact ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal;
const body = Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compact ? 1 : 2);
const actionHeight = compact ? 20 : 24;
const contentSpacing = compact ? Theme.spacingXS : Theme.spacingS;
return padding * 2 + Math.max(icon, body) + actionHeight + contentSpacing + delegateShadowGutter;
}
Component.onCompleted: {
Qt.callLater(() => {
if (listView) {
listView.listInitialized = true;
listView.syncStableContentHeight(false);
if (listView.trackStableContentHeight)
listView.syncStableContentHeight(false);
listView.syncSessionContentHeight();
}
});
}
function estimateContentHeight(groupCount) {
if (groupCount <= 0)
return 0;
return topMargin + bottomMargin + groupCount * estimatedCollapsedCardHeight + Math.max(0, groupCount - 1) * spacing;
}
function estimateExpandedCardHeight(group) {
const compact = SettingsData.notificationCompactMode;
const padding = compact ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding;
const header = compact ? 32 : 40;
const count = Math.min(10, Math.max(1, group?.count || group?.notifications?.length || 1));
const icon = compact ? Theme.notificationExpandedIconSizeCompact : Theme.notificationExpandedIconSizeNormal;
const itemPadding = compact ? Theme.spacingS : Theme.spacingM;
const contentSpacing = compact ? Theme.spacingXS : Theme.spacingS;
const actionHeight = compact ? 20 : 24;
const textBlock = Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * 2;
const row = itemPadding * 2 + Math.max(icon, textBlock) + actionHeight + contentSpacing * 2;
const innerSpacing = compact ? Theme.spacingS : Theme.spacingL;
const headerSpacing = compact ? Theme.spacingXS : Theme.spacingS;
return padding * 2 + header + headerSpacing + count * row + Math.max(0, count - 1) * innerSpacing + delegateShadowGutter;
}
function estimatedHeightForGroup(group) {
if (!group)
return estimatedCollapsedCardHeight;
if (NotificationService.expandedGroups[group.key])
return estimateExpandedCardHeight(group);
return estimatedCollapsedCardHeight;
}
function computeSessionContentHeight() {
const groups = NotificationService.groupedNotifications;
const count = groups ? groups.length : 0;
if (count <= 0)
return 0;
let total = topMargin + bottomMargin + Math.max(0, count - 1) * spacing;
for (let i = 0; i < count; i++) {
const item = itemAtIndex(i);
if (item && item.nonAnimHeight !== undefined && item.nonAnimHeight > 0)
total += item.nonAnimHeight;
else
total += estimatedHeightForGroup(groups[i]);
}
return Math.max(0, total);
}
function syncSessionContentHeight() {
if (!trackSessionContentHeight)
return;
const next = computeSessionContentHeight();
if (Math.abs(next - sessionContentHeight) <= 0.5)
return;
sessionContentHeight = next;
}
function queueSessionContentHeightUpdate() {
if (!trackSessionContentHeight || _sessionHeightUpdatePending)
return;
_sessionHeightUpdatePending = true;
Qt.callLater(() => {
_sessionHeightUpdatePending = false;
syncSessionContentHeight();
});
}
function targetContentHeight() {
if (count <= 0)
return contentHeight;
@@ -47,6 +129,8 @@ DankListView {
}
function syncStableContentHeight(useTarget) {
if (!trackStableContentHeight)
return;
const nextHeight = useTarget ? targetContentHeight() : contentHeight;
if (Math.abs(nextHeight - stableContentHeight) <= 0.5)
return;
@@ -54,7 +138,7 @@ DankListView {
}
function queueStableContentHeightUpdate(useTarget) {
if (_stableHeightUpdatePending)
if (!trackStableContentHeight || _stableHeightUpdatePending)
return;
_stableHeightUpdatePending = true;
Qt.callLater(() => {
@@ -64,11 +148,13 @@ DankListView {
}
onContentHeightChanged: {
if (!isAnimatingExpansion)
if (trackStableContentHeight && !isAnimatingExpansion)
queueStableContentHeightUpdate(false);
}
onIsAnimatingExpansionChanged: {
if (!trackStableContentHeight)
return;
if (isAnimatingExpansion) {
syncStableContentHeight(true);
} else {
@@ -121,10 +207,12 @@ DankListView {
anchors.horizontalCenter: parent.horizontalCenter
}
onCountChanged: listView.queueSessionContentHeightUpdate()
onModelChanged: {
if (!keyboardController || !keyboardController.keyboardNavigationActive) {
listView.queueSessionContentHeightUpdate();
if (!keyboardController || !keyboardController.keyboardNavigationActive)
return;
}
keyboardController.rebuildFlatNavigation();
Qt.callLater(() => {
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) {
@@ -155,7 +243,9 @@ DankListView {
Qt.callLater(() => {
if (delegateRoot) {
delegateRoot.__delegateInitialized = true;
listView.queueStableContentHeightUpdate(listView.isAnimatingExpansion);
if (listView.trackStableContentHeight)
listView.queueStableContentHeightUpdate(listView.isAnimatingExpansion);
listView.queueSessionContentHeightUpdate();
}
});
}
@@ -174,6 +264,7 @@ DankListView {
notificationGroup: modelData
keyboardNavigationActive: listView.keyboardActive
animateExpansion: listView.cardAnimateExpansion && listView.listInitialized
lightweight: listView.lightweightCards
transientSurfaceTracker: listView.transientSurfaceTracker
opacity: {
const swipeAmount = Math.abs(delegateRoot.swipeOffset);
@@ -183,6 +274,8 @@ DankListView {
return Math.max(0, 1 - fadeProgress);
}
onIsAnimatingChanged: {
if (!listView.trackStableContentHeight)
return;
if (isAnimating) {
listView.isAnimatingExpansion = true;
listView.syncStableContentHeight(true);
@@ -204,6 +297,9 @@ DankListView {
}
onTargetHeightChanged: {
listView.queueSessionContentHeightUpdate();
if (!listView.trackStableContentHeight)
return;
if (isAnimating || listView.isAnimatingExpansion)
listView.syncStableContentHeight(true);
else
@@ -289,6 +385,7 @@ DankListView {
target: NotificationService
function onGroupedNotificationsChanged() {
listView.queueSessionContentHeightUpdate();
if (!keyboardController) {
return;
}
@@ -310,12 +407,14 @@ DankListView {
}
function onExpandedGroupsChanged() {
listView.queueSessionContentHeightUpdate();
if (!keyboardController || !keyboardController.keyboardNavigationActive)
return;
expansionEnsureVisibleTimer.restart();
}
function onExpandedMessagesChanged() {
listView.queueSessionContentHeightUpdate();
if (!keyboardController || !keyboardController.keyboardNavigationActive)
return;
expansionEnsureVisibleTimer.restart();
@@ -15,6 +15,7 @@ Rectangle {
property bool userInitiatedExpansion: false
property bool isAnimating: false
property bool animateExpansion: true
property bool lightweight: false
property bool isDescriptionToggleAnimation: false
property bool _retainedExpandedContent: false
property bool _clipAnimatedContent: false
@@ -47,8 +48,8 @@ Rectangle {
height: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
readonly property real targetHeight: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
radius: connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
scale: (cardHoverHandler.hovered ? 1.004 : 1.0) * listLevelAdjacentScaleInfluence
readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
scale: lightweight ? 1 : (cardHoverHandler.hovered ? 1.004 : 1.0) * listLevelAdjacentScaleInfluence
readonly property bool shadowsAllowed: !lightweight && Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
readonly property var shadowElevation: Theme.elevationLevel1
readonly property real baseShadowBlurPx: (shadowElevation && shadowElevation.blurPx !== undefined) ? shadowElevation.blurPx : 4
readonly property real hoverShadowBlurBoost: cardHoverHandler.hovered ? Math.min(2, baseShadowBlurPx * 0.25) : 0
@@ -89,7 +90,7 @@ Rectangle {
}
Behavior on scale {
enabled: listLevelScaleAnimationsEnabled
enabled: !root.lightweight && listLevelScaleAnimationsEnabled
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -97,7 +98,7 @@ Rectangle {
}
Behavior on shadowBlurPx {
enabled: !root.connectedFrameMode
enabled: !root.lightweight && !root.connectedFrameMode
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -105,7 +106,7 @@ Rectangle {
}
Behavior on shadowOffsetXPx {
enabled: !root.connectedFrameMode
enabled: !root.lightweight && !root.connectedFrameMode
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -113,7 +114,7 @@ Rectangle {
}
Behavior on shadowOffsetYPx {
enabled: !root.connectedFrameMode
enabled: !root.lightweight && !root.connectedFrameMode
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -224,6 +225,7 @@ Rectangle {
shadowOffsetY: root.shadowOffsetYPx
shadowColor: root.shadowElevation ? Theme.elevationShadowColor(root.shadowElevation) : Theme.withAlpha(Theme.elevationShadowColor(root.shadowElevation), 0)
shadowEnabled: root.shadowsAllowed && !root.connectedFrameMode
visible: !root.lightweight
}
Rectangle {
@@ -268,7 +270,7 @@ Rectangle {
DankCircularImage {
id: iconContainer
cacheImages: false
cacheImages: root.lightweight
readonly property string rawImage: notificationGroup?.latestNotification?.image || ""
readonly property string iconFromImage: {
if (rawImage.startsWith("image://icon/"))
@@ -611,7 +613,7 @@ Rectangle {
DankCircularImage {
id: messageIcon
cacheImages: false
cacheImages: root.lightweight
readonly property string rawImage: modelData?.image || ""
readonly property string iconFromImage: {
@@ -0,0 +1,156 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
// Host-agnostic notification center body; popout and island both host this.
Item {
id: root
required property var host
property var externalKeyboardController: null
property real cachedHeaderHeight: 32
readonly property alias notificationList: notificationList
readonly property alias notificationHeader: notificationHeader
readonly property int currentTab: notificationHeader.currentTab
readonly property bool hostOwnsHeight: root.host.hostOwnsHeight ?? false
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
readonly property real maxContentHeight: {
const requested = root.host.maxContentHeight ?? 0;
if (requested > 0)
return requested;
return (root.host.screen?.height ?? 1080) * 0.8;
}
readonly property real targetImplicitHeight: {
let baseHeight = Theme.spacingL * 2;
baseHeight += cachedHeaderHeight;
baseHeight += Theme.spacingM * 2;
let listHeight = 200;
if (notificationHeader.currentTab === 0) {
if (NotificationService.groupedNotifications.length === 0) {
listHeight = 200;
} else if (root.hostOwnsHeight) {
listHeight = notificationList.sessionContentHeight > 0 ? notificationList.sessionContentHeight : notificationList.estimateContentHeight(NotificationService.groupedNotifications.length);
} else {
listHeight = root.host.shouldBeVisible ? notificationList.stableContentHeight : notificationList.listContentHeight;
}
} else if (NotificationService.historyList.length > 0) {
listHeight = Math.max(200, NotificationService.historyList.length * 80);
}
if (!root.hostOwnsHeight)
listHeight = Math.min(listHeight, 600);
baseHeight += listHeight;
return Math.max(300, Math.min(baseHeight, maxContentHeight));
}
implicitHeight: root.hostOwnsHeight ? height : targetImplicitHeight
function handleKey(event) {
if (event.key === Qt.Key_Escape) {
root.host.close();
event.accepted = true;
return;
}
if (event.key === Qt.Key_Left) {
if (notificationHeader.currentTab > 0) {
notificationHeader.currentTab = 0;
event.accepted = true;
}
return;
}
if (event.key === Qt.Key_Right) {
if (notificationHeader.currentTab === 0 && SettingsData.notificationHistoryEnabled) {
notificationHeader.currentTab = 1;
event.accepted = true;
}
return;
}
if (notificationHeader.currentTab === 1) {
historyList.handleKey(event);
return;
}
if (root.externalKeyboardController)
root.externalKeyboardController.handleKey(event);
}
FocusScope {
id: contentColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
focus: true
Column {
id: contentColumnInner
anchors.fill: parent
spacing: Theme.spacingM
NotificationHeader {
id: notificationHeader
objectName: "notificationHeader"
transientSurfaceTracker: root.host.transientSurfaceTracker ?? null
tapToClose: root.host.headerTogglesClose ?? false
onHeaderTapped: root.host.close()
onHeightChanged: root.cachedHeaderHeight = height
onSettingsRequested: {
if (typeof root.host.requestSettings === "function")
root.host.requestSettings();
}
}
Item {
visible: notificationHeader.currentTab === 0
width: parent.width
height: parent.height - root.cachedHeaderHeight - contentColumnInner.spacing
KeyboardNavigatedNotificationList {
id: notificationList
objectName: "notificationList"
anchors.fill: parent
anchors.leftMargin: -shadowHorizontalGutter
anchors.rightMargin: -shadowHorizontalGutter
anchors.topMargin: -(shadowVerticalGutter + delegateShadowGutter / 2)
anchors.bottomMargin: -(shadowVerticalGutter + delegateShadowGutter / 2)
cardAnimateExpansion: root.host.animateCardExpansion ?? true
trackStableContentHeight: !root.hostOwnsHeight
trackSessionContentHeight: root.hostOwnsHeight
lightweightCards: root.host.lightweightNotifications ?? false
transientSurfaceTracker: root.host.transientSurfaceTracker ?? null
}
}
HistoryNotificationList {
id: historyList
visible: notificationHeader.currentTab === 1
width: parent.width
height: parent.height - root.cachedHeaderHeight - contentColumnInner.spacing
}
}
}
NotificationKeyboardHints {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
showHints: notificationHeader.currentTab === 0 ? (root.externalKeyboardController?.showKeyboardHints ?? false) : historyList.showKeyboardHints
z: 200
}
}
@@ -181,146 +181,56 @@ DankPopout {
}
content: Component {
Rectangle {
Item {
id: notificationContent
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property alias externalKeyboardController: body.externalKeyboardController
property var externalKeyboardController: null
property real cachedHeaderHeight: 32
implicitHeight: {
let baseHeight = Theme.spacingL * 2;
baseHeight += cachedHeaderHeight;
baseHeight += Theme.spacingM * 2;
const currentListHeight = root.shouldBeVisible ? notificationList.stableContentHeight : notificationList.listContentHeight;
let listHeight = notificationHeader.currentTab === 0 ? currentListHeight : Math.max(200, NotificationService.historyList.length * 80);
if (notificationHeader.currentTab === 0 && NotificationService.groupedNotifications.length === 0) {
listHeight = 200;
}
if (notificationHeader.currentTab === 1 && NotificationService.historyList.length === 0) {
listHeight = 200;
}
const maxContentArea = 600;
baseHeight += Math.min(listHeight, maxContentArea);
const maxHeight = root.screen ? root.screen.height * 0.8 : Screen.height * 0.8;
return Math.max(300, Math.min(baseHeight, maxHeight));
}
color: "transparent"
implicitHeight: body.implicitHeight
focus: true
Component.onCompleted: {
if (root.shouldBeVisible) {
if (root.shouldBeVisible)
forceActiveFocus();
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
notificationHistoryVisible = false;
event.accepted = true;
return;
}
if (event.key === Qt.Key_Left) {
if (notificationHeader.currentTab > 0) {
notificationHeader.currentTab = 0;
event.accepted = true;
}
return;
}
if (event.key === Qt.Key_Right) {
if (notificationHeader.currentTab === 0 && SettingsData.notificationHistoryEnabled) {
notificationHeader.currentTab = 1;
event.accepted = true;
}
return;
}
if (notificationHeader.currentTab === 1) {
historyList.handleKey(event);
return;
}
if (externalKeyboardController) {
externalKeyboardController.handleKey(event);
}
}
Keys.onPressed: event => body.handleKey(event)
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(() => {
notificationContent.forceActiveFocus();
});
} else {
notificationContent.focus = false;
Qt.callLater(() => notificationContent.forceActiveFocus());
return;
}
notificationContent.focus = false;
}
target: root
}
FocusScope {
id: contentColumn
QtObject {
id: popoutHost
readonly property bool shouldBeVisible: root.shouldBeVisible
readonly property var screen: root.screen
readonly property var transientSurfaceTracker: root.transientSurfaceTracker
readonly property real maxContentHeight: (root.screen?.height ?? 1080) * 0.8
function close() {
root.notificationHistoryVisible = false;
}
function requestSettings() {
root.notificationHistoryVisible = false;
PopoutService.openSettingsWithTab("notifications", root, () => root.open());
}
}
NotificationCenterContent {
id: body
anchors.fill: parent
anchors.margins: Theme.spacingL
focus: true
Column {
id: contentColumnInner
anchors.fill: parent
spacing: Theme.spacingM
NotificationHeader {
id: notificationHeader
objectName: "notificationHeader"
transientSurfaceTracker: root.transientSurfaceTracker
onHeightChanged: notificationContent.cachedHeaderHeight = height
onSettingsRequested: {
root.notificationHistoryVisible = false;
PopoutService.openSettingsWithTab("notifications", root, () => root.open());
}
}
Item {
visible: notificationHeader.currentTab === 0
width: parent.width
height: parent.height - notificationContent.cachedHeaderHeight - contentColumnInner.spacing
KeyboardNavigatedNotificationList {
id: notificationList
objectName: "notificationList"
anchors.fill: parent
anchors.leftMargin: -shadowHorizontalGutter
anchors.rightMargin: -shadowHorizontalGutter
anchors.topMargin: -(shadowVerticalGutter + delegateShadowGutter / 2)
anchors.bottomMargin: -(shadowVerticalGutter + delegateShadowGutter / 2)
cardAnimateExpansion: true
transientSurfaceTracker: root.transientSurfaceTracker
}
}
HistoryNotificationList {
id: historyList
visible: notificationHeader.currentTab === 1
width: parent.width
height: parent.height - notificationContent.cachedHeaderHeight - contentColumnInner.spacing
}
}
}
NotificationKeyboardHints {
id: keyboardHints
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
showHints: notificationHeader.currentTab === 0 ? (externalKeyboardController && externalKeyboardController.showKeyboardHints) || false : historyList.showKeyboardHints
z: 200
host: popoutHost
}
}
}
@@ -10,6 +10,9 @@ Item {
property int currentTab: 0
property bool showDndMenu: false
property var transientSurfaceTracker: null
property bool tapToClose: false
signal headerTapped
signal settingsRequested
@@ -54,6 +57,13 @@ Item {
width: parent.width
height: Math.max(titleRow.implicitHeight, actionsRow.implicitHeight)
MouseArea {
anchors.fill: parent
enabled: root.tapToClose
acceptedButtons: Qt.LeftButton
onClicked: root.headerTapped()
}
Row {
id: titleRow
anchors.left: parent.left
+18 -48
View File
@@ -13,7 +13,9 @@ DankOSD {
_displayBrightness = DisplayService.brightnessLevel;
}
osdWidth: useVertical ? (40 + Theme.spacingS * 2) : Math.min(260, screenWidth - Theme.spacingM * 2)
readonly property real osdValueReserve: SettingsData.osdAlwaysShowValue ? 48 : 0
osdWidth: useVertical ? (40 + Theme.spacingS * 2) : Math.min(216 + osdValueReserve, screenWidth - Theme.spacingM * 2)
osdHeight: useVertical ? Math.min(260, screenHeight - Theme.spacingM * 2) : (40 + Theme.spacingS * 2)
autoHideInterval: 3000
enableMouseInteraction: true
@@ -36,47 +38,25 @@ DankOSD {
id: horizontalContent
Item {
property int gap: Theme.spacingS
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
height: 40
anchors.fill: parent
MouseArea {
anchors.fill: parent
onClicked: root.hide()
}
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: {
const deviceInfo = DisplayService.getCurrentDeviceInfo();
if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc")
return "brightness_medium";
if (deviceInfo.name.includes("kbd"))
return "keyboard";
return "lightbulb";
}
size: Theme.iconSize
color: Theme.primary
OsdLevelRow {
anchors.fill: parent
iconName: {
const deviceInfo = DisplayService.getCurrentDeviceInfo();
if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc")
return "brightness_medium";
if (deviceInfo.name.includes("kbd"))
return "keyboard";
return "lightbulb";
}
}
DankSlider {
id: brightnessSlider
width: parent.width - Theme.iconSize - parent.gap * 3
height: 40
x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
iconColor: Theme.primary
value: root._displayBrightness
minimum: {
const deviceInfo = DisplayService.getCurrentDeviceInfo();
if (!deviceInfo)
@@ -93,8 +73,6 @@ DankOSD {
return 100;
return deviceInfo.displayMax || 100;
}
enabled: DisplayService.brightnessAvailable
showValue: true
unit: {
const deviceInfo = DisplayService.getCurrentDeviceInfo();
if (!deviceInfo)
@@ -103,9 +81,8 @@ DankOSD {
return "%";
return deviceInfo.class === "ddc" ? "" : "%";
}
thumbOutlineColor: Theme.surfaceContainer
alwaysShowValue: SettingsData.osdAlwaysShowValue
sliderEnabled: DisplayService.brightnessAvailable
onHoverChanged: hovered => setChildHovered(hovered)
onSliderValueChanged: newValue => {
if (!DisplayService.brightnessAvailable)
return;
@@ -113,13 +90,6 @@ DankOSD {
DisplayService.setBrightness(newValue, DisplayService.lastIpcDevice, true);
resetHideTimer();
}
onContainsMouseChanged: setChildHovered(containsMouse)
Binding on value {
value: root._displayBrightness
when: !brightnessSlider.isDragging
}
}
}
}
@@ -162,7 +132,7 @@ DankOSD {
Item {
id: vertSlider
width: 12
height: parent.height - Theme.iconSize - gap * 3 - 24
height: parent.height - Theme.iconSize - gap * 3 - (SettingsData.osdAlwaysShowValue ? 24 : 0)
anchors.horizontalCenter: parent.horizontalCenter
y: gap * 2 + Theme.iconSize
+14 -59
View File
@@ -30,7 +30,9 @@ DankOSD {
onTriggered: _suppressNewPlayer = false
}
osdWidth: useVertical ? (40 + Theme.spacingS * 2) : Math.min(260, screenWidth - Theme.spacingM * 2)
readonly property real osdValueReserve: SettingsData.osdAlwaysShowValue ? 48 : 0
osdWidth: useVertical ? (40 + Theme.spacingS * 2) : Math.min(216 + osdValueReserve, screenWidth - Theme.spacingM * 2)
osdHeight: useVertical ? Math.min(260, screenHeight - Theme.spacingM * 2) : (40 + Theme.spacingS * 2)
autoHideInterval: 3000
enableMouseInteraction: true
@@ -75,72 +77,25 @@ DankOSD {
id: horizontalContent
Item {
property int gap: Theme.spacingS
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
height: 40
anchors.fill: parent
MouseArea {
anchors.fill: parent
onClicked: root.hide()
}
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: getVolumeIcon(player?.volume ?? 0)
size: Theme.iconSize
color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: muteButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: toggleMute()
onContainsMouseChanged: setChildHovered(containsMouse || volumeSlider.containsMouse)
}
}
DankSlider {
id: volumeSlider
width: parent.width - Theme.iconSize - parent.gap * 3
height: 40
x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
OsdLevelRow {
anchors.fill: parent
iconName: getVolumeIcon(player?.volume ?? 0)
iconInteractive: true
value: root._displayVolume
minimum: 0
maximum: 100
enabled: volumeSupported
showValue: true
unit: "%"
thumbOutlineColor: Theme.surfaceContainer
valueOverride: root._displayVolume
alwaysShowValue: SettingsData.osdAlwaysShowValue
Component.onCompleted: {
root._syncVolume();
value = root._displayVolume;
}
sliderEnabled: volumeSupported
onIconClicked: toggleMute()
onHoverChanged: hovered => setChildHovered(hovered)
onSliderValueChanged: newValue => setVolume(newValue)
onContainsMouseChanged: setChildHovered(containsMouse || muteButton.containsMouse)
Binding on value {
value: root._displayVolume
when: !volumeSlider.pressed
}
Component.onCompleted: root._syncVolume()
}
}
}
@@ -186,7 +141,7 @@ DankOSD {
Item {
id: vertSlider
width: 12
height: parent.height - Theme.iconSize - gap * 3 - 24
height: parent.height - Theme.iconSize - gap * 3 - (SettingsData.osdAlwaysShowValue ? 24 : 0)
anchors.horizontalCenter: parent.horizontalCenter
y: gap * 2 + Theme.iconSize
+16 -59
View File
@@ -15,7 +15,9 @@ DankOSD {
_displayVolume = Math.round(AudioService.source.audio.volume * 100);
}
osdWidth: useVertical ? (40 + Theme.spacingS * 2) : Math.min(260, screenWidth - Theme.spacingM * 2)
readonly property real osdValueReserve: SettingsData.osdAlwaysShowValue ? 48 : 0
osdWidth: useVertical ? (40 + Theme.spacingS * 2) : Math.min(216 + osdValueReserve, screenWidth - Theme.spacingM * 2)
osdHeight: useVertical ? Math.min(260, screenHeight - Theme.spacingM * 2) : (40 + Theme.spacingS * 2)
autoHideInterval: 3000
enableMouseInteraction: true
@@ -59,64 +61,25 @@ DankOSD {
id: horizontalContent
Item {
property int gap: Theme.spacingS
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
height: 40
anchors.fill: parent
MouseArea {
anchors.fill: parent
onClicked: root.hide()
}
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: AudioService.source?.audio?.muted ? "mic_off" : "mic"
size: Theme.iconSize
color: muteButton.containsMouse ? Theme.primary : (AudioService.source?.audio?.muted ? Theme.error : Theme.surfaceText)
}
MouseArea {
id: muteButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMicMute()
onContainsMouseChanged: setChildHovered(containsMouse || volumeSlider.containsMouse)
}
}
DankSlider {
id: volumeSlider
width: parent.width - Theme.iconSize - parent.gap * 3
height: 40
x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
OsdLevelRow {
anchors.fill: parent
iconName: AudioService.source?.audio?.muted ? "mic_off" : "mic"
iconColor: AudioService.source?.audio?.muted ? Theme.error : Theme.surfaceText
iconInteractive: true
value: root._displayVolume
minimum: 0
maximum: 100
enabled: AudioService.source?.audio ?? false
showValue: true
unit: "%"
thumbOutlineColor: Theme.surfaceContainer
valueOverride: root._displayVolume
alwaysShowValue: SettingsData.osdAlwaysShowValue
Component.onCompleted: {
root._syncVolume();
value = root._displayVolume;
}
sliderEnabled: !!AudioService.source?.audio
displayText: AudioService.source?.audio?.muted ? I18n.tr("Muted") : ""
onIconClicked: AudioService.toggleMicMute()
onHoverChanged: hovered => setChildHovered(hovered)
onSliderValueChanged: newValue => {
if (!AudioService.source?.audio)
return;
@@ -124,13 +87,7 @@ DankOSD {
AudioService.source.audio.volume = newValue / 100;
resetHideTimer();
}
onContainsMouseChanged: setChildHovered(containsMouse || muteButton.containsMouse)
Binding on value {
value: root._displayVolume
when: !volumeSlider.pressed
}
Component.onCompleted: root._syncVolume()
}
}
}
@@ -176,7 +133,7 @@ DankOSD {
Item {
id: vertSlider
width: 12
height: parent.height - Theme.iconSize - gap * 3 - 24
height: parent.height - Theme.iconSize - gap * 3 - (SettingsData.osdAlwaysShowValue ? 24 : 0)
anchors.horizontalCenter: parent.horizontalCenter
y: gap * 2 + Theme.iconSize
+137
View File
@@ -0,0 +1,137 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: root
property string iconName: "volume_up"
property color iconColor: Theme.surfaceText
property color iconHoverColor: Theme.primary
property real iconSize: Theme.iconSize
property bool iconInteractive: false
property int value: 0
property int minimum: 0
property int maximum: 100
property string unit: "%"
property string displayText: ""
property string reserveText: ""
property bool sliderEnabled: true
property bool alwaysShowValue: SettingsData.osdAlwaysShowValue
property color thumbOutlineColor: Theme.surfaceContainer
property real horizontalPadding: -1
readonly property bool containsMouse: iconArea.containsMouse || levelSlider.containsMouse
readonly property bool showValueColumn: root.alwaysShowValue
readonly property alias isDragging: levelSlider.isDragging
readonly property real effectiveHorizontalPadding: root.horizontalPadding >= 0 ? root.horizontalPadding : Math.max(Theme.spacingS, Math.round((height - root.iconSize) / 2))
signal sliderValueChanged(int newValue)
signal iconClicked
signal hoverChanged(bool hovered)
onContainsMouseChanged: root.hoverChanged(root.containsMouse)
height: 40
Item {
id: iconSlot
width: root.iconSize
height: root.iconSize
anchors {
left: parent.left
leftMargin: root.effectiveHorizontalPadding
verticalCenter: parent.verticalCenter
}
DankIcon {
anchors.centerIn: parent
name: root.iconName
size: root.iconSize
color: iconArea.containsMouse && root.iconInteractive ? root.iconHoverColor : root.iconColor
}
MouseArea {
id: iconArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: root.iconInteractive ? Qt.LeftButton : Qt.NoButton
cursorShape: root.iconInteractive ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (root.iconInteractive)
root.iconClicked();
}
}
}
DankSlider {
id: levelSlider
anchors {
left: iconSlot.right
leftMargin: Theme.spacingS
right: valueSlot.left
rightMargin: root.showValueColumn ? Theme.spacingS : root.effectiveHorizontalPadding
verticalCenter: parent.verticalCenter
}
height: 40
minimum: root.minimum
maximum: Math.max(root.minimum + 1, root.maximum)
enabled: root.sliderEnabled
showValue: false
unit: root.unit
thumbOutlineColor: root.thumbOutlineColor
valueOverride: root.value
onSliderValueChanged: newValue => root.sliderValueChanged(newValue)
Binding on value {
value: root.value
when: !levelSlider.isDragging
}
}
Item {
id: valueSlot
anchors {
right: parent.right
rightMargin: root.showValueColumn ? root.effectiveHorizontalPadding : 0
top: parent.top
bottom: parent.bottom
}
width: root.showValueColumn ? valueLabel.reservedWidth : 0
visible: root.showValueColumn
NumericText {
id: valueLabel
anchors {
right: parent.right
top: parent.top
bottom: parent.bottom
}
text: root.displayText.length > 0 ? root.displayText : (Math.round(root.value) + root.unit)
reserveText: {
const samples = [root.minimum, root.maximum, root.value];
let widest = Math.round(root.maximum) + root.unit;
for (let i = 0; i < samples.length; i++) {
const candidate = Math.round(samples[i]) + root.unit;
if (candidate.length > widest.length)
widest = candidate;
}
if (root.reserveText.length > widest.length)
return root.reserveText;
return widest;
}
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
}
}
}
+15 -59
View File
@@ -15,7 +15,9 @@ DankOSD {
_displayVolume = Math.min(AudioService.sinkMaxVolume, Math.round(AudioService.sink.audio.volume * 100));
}
osdWidth: useVertical ? (40 + Theme.spacingS * 2) : Math.min(260, screenWidth - Theme.spacingM * 2)
readonly property real osdValueReserve: SettingsData.osdAlwaysShowValue ? 48 : 0
osdWidth: useVertical ? (40 + Theme.spacingS * 2) : Math.min(216 + osdValueReserve, screenWidth - Theme.spacingM * 2)
osdHeight: useVertical ? Math.min(260, screenHeight - Theme.spacingM * 2) : (40 + Theme.spacingS * 2)
autoHideInterval: 3000
enableMouseInteraction: true
@@ -54,64 +56,24 @@ DankOSD {
id: horizontalContent
Item {
property int gap: Theme.spacingS
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
height: 40
anchors.fill: parent
MouseArea {
anchors.fill: parent
onClicked: root.hide()
}
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: AudioService.sink?.audio?.muted ? "volume_off" : "volume_up"
size: Theme.iconSize
color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: muteButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMute()
onContainsMouseChanged: setChildHovered(containsMouse || volumeSlider.containsMouse)
}
}
DankSlider {
id: volumeSlider
width: parent.width - Theme.iconSize - parent.gap * 3
height: 40
x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
OsdLevelRow {
anchors.fill: parent
iconName: AudioService.sink?.audio?.muted ? "volume_off" : "volume_up"
iconInteractive: true
value: root._displayVolume
minimum: 0
maximum: AudioService.sinkMaxVolume
enabled: AudioService.sink?.audio ?? false
showValue: true
unit: "%"
thumbOutlineColor: Theme.surfaceContainer
valueOverride: root._displayVolume
alwaysShowValue: SettingsData.osdAlwaysShowValue
Component.onCompleted: {
root._syncVolume();
value = root._displayVolume;
}
sliderEnabled: !!AudioService.sink?.audio
displayText: AudioService.sink?.audio?.muted ? I18n.tr("Muted") : ""
onIconClicked: AudioService.toggleMute()
onHoverChanged: hovered => setChildHovered(hovered)
onSliderValueChanged: newValue => {
if (!AudioService.sink?.audio)
return;
@@ -119,13 +81,7 @@ DankOSD {
AudioService.sink.audio.volume = newValue / 100;
resetHideTimer();
}
onContainsMouseChanged: setChildHovered(containsMouse || muteButton.containsMouse)
Binding on value {
value: root._displayVolume
when: !volumeSlider.pressed
}
Component.onCompleted: root._syncVolume()
}
}
}
@@ -171,7 +127,7 @@ DankOSD {
Item {
id: vertSlider
width: 12
height: parent.height - Theme.iconSize - gap * 3 - 24
height: parent.height - Theme.iconSize - gap * 3 - (SettingsData.osdAlwaysShowValue ? 24 : 0)
anchors.horizontalCenter: parent.horizontalCenter
y: gap * 2 + Theme.iconSize
@@ -267,42 +267,6 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
}
}
SettingsCard {
width: parent.width
tags: ["compositor", "toast"]
title: I18n.tr("Notifications")
settingKey: "compositorNotifications"
iconName: "notifications"
visible: CompositorService.isNiri || CompositorService.isMango
SettingsToggleRow {
tags: ["compositor", "toast", "reload"]
settingKey: "showConfigReloadToast"
text: I18n.tr("Show \"config reloaded\" Toast")
description: I18n.tr("Show a toast when the compositor config is reloaded")
checked: SessionData.showConfigReloadToast
onToggled: checked => SessionData.showConfigReloadToast = checked
}
}
SettingsCard {
width: parent.width
tags: ["niri", "overview", "window", "focus", "launch", "launcher", "dock", "settings"]
title: I18n.tr("Overview")
settingKey: "niriOverview"
iconName: "overview"
visible: CompositorService.isNiri
SettingsToggleRow {
tags: ["niri", "overview", "window", "focus", "launch", "launcher", "dock", "settings"]
settingKey: "closeNiriOverviewOnWindowFocus"
text: I18n.tr("Close Niri Overview on Window Focus", "Niri setting to leave Niri overview when DMS launches or focuses a window")
description: I18n.tr("Auto-close Niri overview when DMS launches or focuses a window.", "Description of the close Niri overview on window focus setting")
checked: SettingsData.closeNiriOverviewOnWindowFocus
onToggled: checked => SettingsData.set("closeNiriOverviewOnWindowFocus", checked)
}
}
SettingsCard {
width: parent.width
tags: ["niri", "layout", "gaps", "radius", "window", "border"]
@@ -327,7 +291,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
return;
switch (index) {
case 1:
SettingsData.set("niriLayoutGapsOverride", Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)));
SettingsData.set("niriLayoutGapsOverride", Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4)));
return;
case 2:
SettingsData.set("niriLayoutGapsOverride", -2);
@@ -348,7 +312,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
minimum: 0
maximum: 50
unit: "px"
defaultValue: Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4))
defaultValue: Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4))
onSliderValueChanged: newValue => SettingsData.set("niriLayoutGapsOverride", newValue)
}
@@ -432,6 +396,42 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
}
}
SettingsCard {
width: parent.width
tags: ["compositor", "toast"]
title: I18n.tr("Notifications")
settingKey: "compositorNotifications"
iconName: "notifications"
visible: CompositorService.isNiri || CompositorService.isMango
SettingsToggleRow {
tags: ["compositor", "toast", "reload"]
settingKey: "showConfigReloadToast"
text: I18n.tr("Show \"config reloaded\" Toast")
description: I18n.tr("Show a toast when the compositor config is reloaded")
checked: SessionData.showConfigReloadToast
onToggled: checked => SessionData.showConfigReloadToast = checked
}
}
SettingsCard {
width: parent.width
tags: ["niri", "overview", "window", "focus", "launch", "launcher", "dock", "settings"]
title: I18n.tr("Overview")
settingKey: "niriOverview"
iconName: "overview"
visible: CompositorService.isNiri
SettingsToggleRow {
tags: ["niri", "overview", "window", "focus", "launch", "launcher", "dock", "settings"]
settingKey: "closeNiriOverviewOnWindowFocus"
text: I18n.tr("Close Niri Overview on Window Focus", "Niri setting to leave Niri overview when DMS launches or focuses a window")
description: I18n.tr("Auto-close Niri overview when DMS launches or focuses a window.", "Description of the close Niri overview on window focus setting")
checked: SettingsData.closeNiriOverviewOnWindowFocus
onToggled: checked => SettingsData.set("closeNiriOverviewOnWindowFocus", checked)
}
}
SettingsCard {
width: parent.width
tags: ["hyprland", "layout", "gaps", "radius", "window", "border", "rounding"]
@@ -456,7 +456,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
return;
switch (index) {
case 1:
SettingsData.set("hyprlandLayoutGapsOverride", Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)));
SettingsData.set("hyprlandLayoutGapsOverride", Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4)));
return;
case 2:
SettingsData.set("hyprlandLayoutGapsOverride", -2);
@@ -477,7 +477,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
minimum: 0
maximum: 50
unit: "px"
defaultValue: Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4))
defaultValue: Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4))
onSliderValueChanged: newValue => SettingsData.set("hyprlandLayoutGapsOverride", newValue)
}
@@ -607,7 +607,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
return;
switch (index) {
case 1:
SettingsData.set("mangoLayoutGapsOverride", Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)));
SettingsData.set("mangoLayoutGapsOverride", Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4)));
return;
case 2:
SettingsData.set("mangoLayoutGapsOverride", -2);
@@ -628,7 +628,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
minimum: 0
maximum: 50
unit: "px"
defaultValue: Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4))
defaultValue: Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4))
onSliderValueChanged: newValue => SettingsData.set("mangoLayoutGapsOverride", newValue)
}
+150 -57
View File
@@ -52,6 +52,53 @@ Item {
}
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
// The selected instance is itself the island, so bar-surface settings do not apply to it.
readonly property bool selectedBarIsIsland: {
SettingsData.dankIslandBarId;
selectedBarId;
return !!selectedBarId && selectedBarId === SettingsData.dankIslandBarId;
}
readonly property bool islandShadowsSelectedBar: {
SettingsData.dankIslandBarId;
SettingsData.barConfigs;
Quickshell.screens;
selectedBarId;
if (!selectedBarConfig || dankBarTab.selectedBarIsIsland)
return false;
const edge = SettingsData.positionToSide(selectedBarConfig.position ?? SettingsData.Position.Top);
return Quickshell.screens.some(screen => SettingsData.barConfigCoversScreen(selectedBarConfig, screen) && SettingsData.dankIslandOwnsEdge(screen, edge));
}
readonly property bool islandOwnsSelectedBarTop: dankBarTab.selectedBarIsIsland || dankBarTab.islandShadowsSelectedBar
readonly property var positionChoices: {
SettingsData.dankIslandBarId;
SettingsData.barConfigs;
Quickshell.screens;
selectedBarId;
const all = [SettingsData.Position.Top, SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right];
if (dankBarTab.selectedBarIsIsland)
return [SettingsData.Position.Top, SettingsData.Position.Bottom];
if (!selectedBarConfig)
return all;
const current = selectedBarConfig.position ?? SettingsData.Position.Top;
return all.filter(pos => {
if (pos === current)
return true;
const edge = SettingsData.positionToSide(pos);
return !Quickshell.screens.some(screen => SettingsData.barConfigCoversScreen(selectedBarConfig, screen) && SettingsData.dankIslandOwnsEdge(screen, edge));
});
}
function positionLabel(pos) {
switch (pos) {
case SettingsData.Position.Bottom:
return I18n.tr("Bottom", "screen edge position");
case SettingsData.Position.Left:
return I18n.tr("Left", "screen edge position");
case SettingsData.Position.Right:
return I18n.tr("Right", "screen edge position");
}
return I18n.tr("Top", "screen edge position");
}
// Bar Inset Padding: resolve the "auto" sentinel (stored < 0) to each mode's natural inset for the slider display.
readonly property real insetPadAutoUI: SettingsData.connectedFrameModeActive ? SettingsData.frameThickness : (selectedBarIsVertical ? Theme.spacingXS : Math.max(Theme.spacingXS, (selectedBarConfig?.innerPadding ?? 4) * 0.8))
readonly property int insetPadDisplayValue: {
@@ -64,7 +111,7 @@ Item {
interval: 500
repeat: false
onTriggered: {
const verticalBars = SettingsData.barConfigs.filter(cfg => {
const verticalBars = SettingsData.getBarKindConfigs().filter(cfg => {
const pos = cfg.position ?? SettingsData.Position.Top;
return pos === SettingsData.Position.Left || pos === SettingsData.Position.Right;
});
@@ -83,7 +130,7 @@ Item {
}
function _isBarActive(c) {
if (!c.enabled)
if (!c.enabled || SettingsData.isIslandBarConfig(c))
return false;
const prefs = c.screenPreferences || ["all"];
if (prefs.length > 0)
@@ -92,7 +139,7 @@ Item {
}
function notifyHorizontalBarChange() {
const configs = SettingsData.barConfigs;
const configs = SettingsData.getBarKindConfigs();
if (configs.length < 2)
return;
@@ -189,14 +236,14 @@ Item {
function deleteBar(barId) {
if (barId === "default")
return;
if (SettingsData.barConfigs.length <= 1)
if (SettingsData.getBarKindConfigs().length <= 1 && barId !== SettingsData.dankIslandBarId)
return;
SettingsData.deleteBarConfig(barId);
selectedBarId = "default";
}
function toggleBarEnabled(barId) {
if (barId === "default")
if (barId === "default" && barId !== SettingsData.dankIslandBarId)
return;
const config = SettingsData.getBarConfig(barId);
if (!config)
@@ -390,7 +437,7 @@ Item {
SettingsData.barConfigs;
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
const left = cfg?.leftWidgets?.length || 0;
const center = cfg?.centerWidgets?.length || 0;
const center = SettingsData.isIslandBarConfig(cfg) ? 0 : (cfg?.centerWidgets?.length || 0);
const right = cfg?.rightWidgets?.length || 0;
return I18n.tr("%1 widgets").replace("%1", left + center + right);
}
@@ -407,7 +454,7 @@ Item {
visible: {
SettingsData.barConfigs;
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
return !cfg?.enabled && barCard.modelData.id !== "default";
return !cfg?.enabled && (barCard.modelData.id !== "default" || SettingsData.isIslandBarConfig(cfg));
}
}
@@ -419,9 +466,26 @@ Item {
visible: {
SettingsData.barConfigs;
const cfg = SettingsData.getBarConfig(barCard.modelData.id);
return !cfg?.enabled && barCard.modelData.id !== "default";
return !cfg?.enabled && (barCard.modelData.id !== "default" || SettingsData.isIslandBarConfig(cfg));
}
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignLeft
visible: SettingsData.dankIslandBarId === barCard.modelData.id
}
StyledText {
text: I18n.tr("Island")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
horizontalAlignment: Text.AlignLeft
visible: SettingsData.dankIslandBarId === barCard.modelData.id
}
}
}
@@ -433,7 +497,7 @@ Item {
backgroundColor: Theme.withAlpha(Theme.error, 0.15)
iconColor: Theme.error
visible: barCard.modelData.id !== "default"
enabled: SettingsData.barConfigs.length > 1
enabled: SettingsData.getBarKindConfigs().length > 1 || SettingsData.dankIslandBarId === barCard.modelData.id
anchors.verticalCenter: parent.verticalCenter
onClicked: dankBarTab.deleteBar(barCard.modelData.id)
}
@@ -467,11 +531,11 @@ Item {
SettingsCard {
settingKey: "barEnable"
iconName: selectedBarConfig?.enabled ? "visibility" : "visibility_off"
title: I18n.tr("Enable Bar")
visible: !dankBarTab.appearanceOnly && selectedBarId !== "default"
title: dankBarTab.selectedBarIsIsland ? I18n.tr("Enable Island") : I18n.tr("Enable Bar")
visible: !dankBarTab.appearanceOnly && (selectedBarId !== "default" || dankBarTab.selectedBarIsIsland)
SettingsToggleRow {
text: I18n.tr("Toggle visibility of this bar configuration")
text: dankBarTab.selectedBarIsIsland ? I18n.tr("Toggle the Island on this instance's displays") : I18n.tr("Toggle visibility of this bar configuration")
checked: {
selectedBarId;
return selectedBarConfig?.enabled ?? false;
@@ -486,6 +550,20 @@ Item {
settingKey: "barPosition"
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
SettingsControlledByIsland {
visible: dankBarTab.selectedBarIsIsland
parentModal: dankBarTab.parentModal
settingLabel: I18n.tr("Island placement")
reason: I18n.tr("The Island anchors to the top or bottom edge")
}
SettingsControlledByIsland {
visible: dankBarTab.islandShadowsSelectedBar
parentModal: dankBarTab.parentModal
settingLabel: I18n.tr("%1 position").arg(dankBarTab.positionLabel(selectedBarConfig?.position ?? SettingsData.Position.Top))
reason: I18n.tr("The Island holds this edge on a display this bar covers, so the bar stays hidden there")
}
Item {
width: parent.width
height: positionButtonGroup.height
@@ -493,44 +571,17 @@ Item {
DankButtonGroup {
id: positionButtonGroup
anchors.horizontalCenter: parent.horizontalCenter
model: [I18n.tr("Top", "screen edge position"), I18n.tr("Bottom", "screen edge position"), I18n.tr("Left", "screen edge position"), I18n.tr("Right", "screen edge position")]
model: dankBarTab.positionChoices.map(pos => dankBarTab.positionLabel(pos))
currentIndex: {
selectedBarId;
const config = SettingsData.getBarConfig(selectedBarId);
const pos = config?.position ?? 0;
switch (pos) {
case SettingsData.Position.Top:
return 0;
case SettingsData.Position.Bottom:
return 1;
case SettingsData.Position.Left:
return 2;
case SettingsData.Position.Right:
return 3;
default:
return 0;
}
return dankBarTab.positionChoices.indexOf(config?.position ?? SettingsData.Position.Top);
}
onSelectionChanged: (index, selected) => {
if (!selected)
if (!selected || index < 0 || index >= dankBarTab.positionChoices.length)
return;
let newPos = 0;
switch (index) {
case 0:
newPos = SettingsData.Position.Top;
break;
case 1:
newPos = SettingsData.Position.Bottom;
break;
case 2:
newPos = SettingsData.Position.Left;
break;
case 3:
newPos = SettingsData.Position.Right;
break;
}
SettingsData.updateBarConfig(selectedBarId, {
position: newPos
position: dankBarTab.positionChoices[index]
});
notifyHorizontalBarChange();
}
@@ -548,7 +599,7 @@ Item {
StyledText {
width: parent.width
text: I18n.tr("Configure which displays show \"%1\"").replace("%1", selectedBarConfig?.name || "this bar")
text: dankBarTab.selectedBarIsIsland ? I18n.tr("Configure which displays show Dank Island as \"%1\"").replace("%1", selectedBarConfig?.name || "this instance") : I18n.tr("Configure which displays show \"%1\"").replace("%1", selectedBarConfig?.name || "this bar")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
@@ -653,9 +704,17 @@ Item {
expanded: true
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
SettingsControlledByIsland {
visible: dankBarTab.islandOwnsSelectedBarTop
parentModal: dankBarTab.parentModal
settingLabel: I18n.tr("Bar visibility")
reason: I18n.tr("Additional settings are managed by the Island")
}
SettingsToggleRow {
settingKey: "barAutoHide"
tags: ["autohide", "auto-hide", "reveal", "intellihide"]
visible: !dankBarTab.islandOwnsSelectedBarTop
text: I18n.tr("Auto-hide")
description: I18n.tr("Automatically hide the bar when the pointer moves away")
checked: selectedBarConfig?.autoHide ?? false
@@ -670,7 +729,7 @@ Item {
Column {
width: parent.width
spacing: Theme.spacingS
visible: selectedBarConfig?.autoHide ?? false
visible: (selectedBarConfig?.autoHide ?? false) && !dankBarTab.islandOwnsSelectedBarTop
leftPadding: Theme.spacingM
Rectangle {
@@ -740,11 +799,13 @@ Item {
height: 1
color: Theme.outline
opacity: 0.15
visible: !dankBarTab.islandOwnsSelectedBarTop
}
SettingsToggleRow {
settingKey: "barManualVisibility"
tags: ["manual", "show", "hide", "ipc", "toggle"]
visible: !dankBarTab.islandOwnsSelectedBarTop
text: I18n.tr("Manual Show/Hide")
description: I18n.tr("Toggle bar visibility manually via IPC")
checked: selectedBarConfig?.visible ?? true
@@ -761,11 +822,13 @@ Item {
height: 1
color: Theme.outline
opacity: 0.15
visible: !dankBarTab.islandOwnsSelectedBarTop
}
SettingsToggleRow {
settingKey: "barClickThrough"
tags: ["clickthrough", "click", "through", "mouse", "input", "mask", "passthrough"]
visible: !dankBarTab.islandOwnsSelectedBarTop
text: I18n.tr("Click Through")
description: I18n.tr("Mouse clicks pass through the bar to windows behind it")
checked: selectedBarConfig?.clickThrough ?? false
@@ -779,13 +842,13 @@ Item {
height: 1
color: Theme.outline
opacity: 0.15
visible: CompositorService.isNiri
visible: CompositorService.isNiri && !dankBarTab.islandOwnsSelectedBarTop
}
SettingsToggleRow {
settingKey: "barOpenOnOverview"
tags: ["bar", "overview", "niri", "show", "frame"]
visible: CompositorService.isNiri
visible: CompositorService.isNiri && !dankBarTab.islandOwnsSelectedBarTop
text: I18n.tr("Show on Overview")
description: SettingsData.frameEnabled ? I18n.tr("Show during Niri overview") : I18n.tr("Show the bar when niri overview is active")
checked: SettingsData.frameEnabled ? SettingsData.frameShowOnOverview : (selectedBarConfig?.openOnOverview ?? false)
@@ -805,11 +868,13 @@ Item {
height: 1
color: Theme.outline
opacity: 0.15
visible: !dankBarTab.islandOwnsSelectedBarTop
}
SettingsToggleRow {
settingKey: "barUseOverlayLayer"
tags: ["bar", "fullscreen", "overlay", "layer"]
visible: !dankBarTab.islandOwnsSelectedBarTop
text: I18n.tr("Use Overlay Layer", "bar layer toggle: use Wayland overlay layer")
description: I18n.tr("Place the bar on the Wayland overlay layer")
checked: selectedBarConfig?.useOverlayLayer ?? false
@@ -831,7 +896,7 @@ Item {
SettingsSliderRow {
id: barTransparencySlider
visible: !SettingsData.frameEnabled
visible: !SettingsData.frameEnabled && !dankBarTab.islandOwnsSelectedBarTop
text: I18n.tr("Bar Opacity")
value: (selectedBarConfig?.transparency ?? 1.0) * 100
minimum: 0
@@ -880,6 +945,13 @@ Item {
settingLabel: I18n.tr("Bar Opacity")
reason: I18n.tr("Managed by Frame")
}
SettingsControlledByIsland {
visible: !SettingsData.frameEnabled && dankBarTab.islandOwnsSelectedBarTop
parentModal: dankBarTab.parentModal
settingLabel: I18n.tr("Bar Opacity")
reason: I18n.tr("The island draws its own surface")
}
}
SettingsCard {
@@ -898,7 +970,7 @@ Item {
SettingsSliderRow {
id: edgeSpacingSlider
visible: !SettingsData.frameEnabled
visible: !SettingsData.frameEnabled && !dankBarTab.islandOwnsSelectedBarTop
text: I18n.tr("Edge Spacing")
description: I18n.tr("Space between the bar and screen edges")
value: selectedBarConfig?.spacing ?? 4
@@ -923,7 +995,7 @@ Item {
id: exclusiveZoneSlider
settingKey: "barExclusiveZone"
tags: ["exclusive", "zone", "reserved", "offset"]
visible: !SettingsData.frameEnabled
visible: !SettingsData.frameEnabled && !dankBarTab.islandOwnsSelectedBarTop
text: I18n.tr("Exclusive Zone Offset")
description: I18n.tr("Fine-tune the space reserved for the bar from the screen edge")
value: selectedBarConfig?.bottomGap ?? 0
@@ -1034,7 +1106,7 @@ Item {
SettingsSliderRow {
id: barLengthPaddingSlider
settingKey: "barLengthPadding"
visible: !SettingsData.frameEnabled
visible: !SettingsData.frameEnabled && !dankBarTab.islandOwnsSelectedBarTop
text: I18n.tr("Bar Length Padding")
description: I18n.tr("Reduce the visible bar length from both ends")
tags: ["bar", "length", "padding", "size", "shorter"]
@@ -1120,6 +1192,13 @@ Item {
}
}
}
SettingsControlledByIsland {
visible: !SettingsData.frameEnabled && dankBarTab.islandOwnsSelectedBarTop
parentModal: dankBarTab.parentModal
settingLabel: I18n.tr("Bar Spacing")
reason: I18n.tr("Some settings are managed by the island")
}
}
SettingsSliderCard {
@@ -1192,6 +1271,13 @@ Item {
reason: I18n.tr("Managed by Frame")
}
SettingsControlledByIsland {
visible: !SettingsData.frameEnabled && dankBarTab.islandOwnsSelectedBarTop
parentModal: dankBarTab.parentModal
settingLabel: I18n.tr("Bar corners")
reason: I18n.tr("Corner style applies to the bar surface")
}
SettingsToggleRow {
settingKey: "barNoBackground"
tags: ["transparent", "background", "invisible"]
@@ -1249,7 +1335,7 @@ Item {
tags: ["rounded", "attached", "square", "corners", "edge", "screen", "flush"]
text: I18n.tr("Corner Style")
description: I18n.tr("Choose how the bar meets the screen edge")
visible: !SettingsData.frameEnabled
visible: !SettingsData.frameEnabled && !dankBarTab.islandOwnsSelectedBarTop
model: [I18n.tr("Rounded", "bar corner style option"), I18n.tr("Flush", "bar corner style option"), I18n.tr("Square", "bar corner style option")]
currentIndex: {
if (selectedBarConfig?.squareCorners ?? false)
@@ -1273,7 +1359,7 @@ Item {
tags: ["goth", "corners", "concave", "cutout"]
text: I18n.tr("Goth Corners")
description: I18n.tr("Apply inverse concave corner cutouts to the bar")
visible: !SettingsData.frameEnabled
visible: !SettingsData.frameEnabled && !dankBarTab.islandOwnsSelectedBarTop
checked: selectedBarConfig?.gothCornersEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
gothCornersEnabled: checked
@@ -1284,7 +1370,7 @@ Item {
text: I18n.tr("Corner Radius Override")
description: I18n.tr("Use a custom radius for goth corner cutouts")
checked: selectedBarConfig?.gothCornerRadiusOverride ?? false
visible: selectedBarConfig?.gothCornersEnabled ?? false
visible: (selectedBarConfig?.gothCornersEnabled ?? false) && !dankBarTab.islandOwnsSelectedBarTop
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
gothCornerRadiusOverride: checked
})
@@ -1293,7 +1379,7 @@ Item {
Column {
width: parent.width
spacing: Theme.spacingS
visible: (selectedBarConfig?.gothCornersEnabled ?? false) && (selectedBarConfig?.gothCornerRadiusOverride ?? false)
visible: (selectedBarConfig?.gothCornersEnabled ?? false) && (selectedBarConfig?.gothCornerRadiusOverride ?? false) && !dankBarTab.islandOwnsSelectedBarTop
leftPadding: Theme.spacingM
SettingsSliderRow {
@@ -1484,7 +1570,7 @@ Item {
settingKey: "barBorder"
iconName: "border_style"
title: I18n.tr("Border")
visible: dankBarTab.appearanceOnly && selectedBarConfig?.enabled && !dankBarTab.connectedFrameModeActive
visible: dankBarTab.appearanceOnly && selectedBarConfig?.enabled && !dankBarTab.connectedFrameModeActive && !dankBarTab.islandOwnsSelectedBarTop
checked: selectedBarConfig?.borderEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
borderEnabled: checked
@@ -1674,6 +1760,13 @@ Item {
reason: I18n.tr("Managed by Frame in Connected Mode")
}
SettingsControlledByIsland {
visible: dankBarTab.appearanceOnly && !dankBarTab.connectedFrameModeActive && dankBarTab.islandOwnsSelectedBarTop
parentModal: dankBarTab.parentModal
settingLabel: I18n.tr("Bar border and shadow")
reason: I18n.tr("These style the bar surface, which the Island replaces")
}
SettingsCard {
id: shadowCard
tab: "appearance"
@@ -1682,7 +1775,7 @@ Item {
settingKey: "barShadow"
collapsible: true
expanded: false
visible: dankBarTab.appearanceOnly && (selectedBarConfig?.enabled ?? false) && !dankBarTab.connectedFrameModeActive
visible: dankBarTab.appearanceOnly && (selectedBarConfig?.enabled ?? false) && !dankBarTab.connectedFrameModeActive && !dankBarTab.islandOwnsSelectedBarTop
readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0
readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "default") === "custom"
@@ -0,0 +1,486 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.Settings.Widgets
import qs.Services
import qs.Widgets
Item {
id: root
property var parentModal: null
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
readonly property var instanceChoices: {
SettingsData.barConfigs;
SettingsData.dankIslandBarId;
const rows = [({
"id": "",
"name": I18n.tr("Off")
})];
const configs = SettingsData.barConfigs || [];
for (let i = 0; i < configs.length; i++)
rows.push({
"id": configs[i].id,
"name": configs[i].name || I18n.tr("Bar %1").arg(i + 1)
});
return rows;
}
readonly property int instanceIndex: Math.max(0, root.instanceChoices.findIndex(row => row.id === SettingsData.dankIslandBarId))
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
SettingsCard {
width: parent.width
iconName: "view_in_ar"
title: I18n.tr("Dank Island - Beta")
settingKey: "dankIslandBarId"
SettingsButtonGroupRow {
settingKey: "dankIslandBarId"
tags: ["island", "activities", "media", "notifications", "osd", "bar"]
text: I18n.tr("Island Instance")
model: root.instanceChoices.map(row => row.name)
currentIndex: root.instanceIndex
onSelectionChanged: (index, selected) => {
if (selected)
SettingsData.set("dankIslandBarId", root.instanceChoices[index]?.id ?? "");
}
}
}
SettingsCard {
width: parent.width
iconName: "open_with"
title: I18n.tr("Placement")
settingKey: "dankIslandPlacement"
enabled: SettingsData.dankIslandEnabled
SettingsToggleRow {
settingKey: "dankIslandFloating"
tags: ["island", "placement", "float", "overlay", "exclusive", "reserve"]
text: I18n.tr("Float")
description: I18n.tr("Floats above windows and other content")
checked: SettingsData.dankIslandFloating
onToggled: checked => SettingsData.set("dankIslandFloating", checked)
}
SettingsToggleRow {
settingKey: "dankIslandUseOverlayLayer"
tags: ["island", "fullscreen", "overlay", "layer"]
text: I18n.tr("Use Overlay Layer", "island layer toggle: use Wayland overlay layer")
description: I18n.tr("Stage the Wayland overlay layer to remain visible over fullscreen apps")
checked: SettingsData.dankIslandUseOverlayLayer
onToggled: checked => SettingsData.set("dankIslandUseOverlayLayer", checked)
}
SettingsSliderRow {
settingKey: "dankIslandReserveHeight"
tags: ["island", "placement", "reservation", "exclusive", "height"]
text: I18n.tr("Reserved Height")
description: I18n.tr("Space kept clear for compact island and satellites")
unit: "px"
minimum: 36
maximum: 128
step: 1
defaultValue: 40
value: SettingsData.dankIslandReserveHeight
enabled: !SettingsData.dankIslandFloating
onSliderValueChanged: value => SettingsData.set("dankIslandReserveHeight", value)
}
SettingsSliderRow {
settingKey: "dankIslandCompactHeight"
tags: ["island", "placement", "compact", "height", "size", "satellite"]
text: I18n.tr("Compact Island Height")
description: I18n.tr("Resize the resting island to align with nearby widgets")
unit: "px"
minimum: 36
maximum: 72
step: 1
defaultValue: 38
value: SettingsData.dankIslandCompactHeight
onSliderValueChanged: value => SettingsData.set("dankIslandCompactHeight", value)
}
SettingsSliderRow {
settingKey: "dankIslandOuterGap"
tags: ["island", "placement", "gap", "top", "margin"]
text: I18n.tr("Outer Gap")
description: I18n.tr("Distance between display edge and island")
unit: "px"
minimum: 0
maximum: 48
step: 1
defaultValue: 4
value: SettingsData.dankIslandOuterGap
onSliderValueChanged: value => SettingsData.set("dankIslandOuterGap", value)
}
SettingsSliderRow {
settingKey: "dankIslandHorizontalOffset"
tags: ["island", "placement", "horizontal", "offset", "center"]
text: I18n.tr("Horizontal Offset")
description: I18n.tr("Move island and satellites left or right from display center")
unit: "px"
minimum: -600
maximum: 600
step: 1
defaultValue: 0
value: SettingsData.dankIslandHorizontalOffset
onSliderValueChanged: value => SettingsData.set("dankIslandHorizontalOffset", value)
}
}
SettingsCard {
id: homeCompactCard
width: parent.width
iconName: "home"
title: I18n.tr("Home Compact")
settingKey: "dankIslandActivities"
enabled: SettingsData.dankIslandEnabled
readonly property var _homeSlotValues: ["left", "right", "hidden"]
function homeSlotIndex(value, fallback) {
const index = _homeSlotValues.indexOf(value);
if (index >= 0)
return index;
return Math.max(0, _homeSlotValues.indexOf(fallback));
}
StyledText {
width: parent.width
text: I18n.tr("Choose which shortcuts sit left or right of the clock")
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
}
SettingsButtonGroupRow {
settingKey: "dankIslandHomeMediaSlot"
tags: ["island", "home", "compact", "media", "launcher", "search", "cava"]
text: I18n.tr("Media / Launcher")
description: I18n.tr("Search when idle, visualizer when media is playing")
model: [I18n.tr("Left"), I18n.tr("Right"), I18n.tr("Hidden")]
currentIndex: homeCompactCard.homeSlotIndex(SettingsData.dankIslandHomeMediaSlot, "left")
onSelectionChanged: (index, selected) => {
if (selected)
SettingsData.set("dankIslandHomeMediaSlot", homeCompactCard._homeSlotValues[index] ?? "left");
}
}
SettingsButtonGroupRow {
settingKey: "dankIslandHomeStatusSlot"
tags: ["island", "home", "compact", "battery", "control center", "tools"]
text: I18n.tr("Battery / Control Center")
description: BatteryService.batteryAvailable ? I18n.tr("Battery gauge opens Control Center") : I18n.tr("Tools icon opens Control Center")
model: [I18n.tr("Left"), I18n.tr("Right"), I18n.tr("Hidden")]
currentIndex: homeCompactCard.homeSlotIndex(SettingsData.dankIslandHomeStatusSlot, "right")
onSelectionChanged: (index, selected) => {
if (selected)
SettingsData.set("dankIslandHomeStatusSlot", homeCompactCard._homeSlotValues[index] ?? "right");
}
}
SettingsButtonGroupRow {
settingKey: "dankIslandHomeWeatherSlot"
tags: ["island", "home", "compact", "weather", "forecast"]
text: I18n.tr("Weather")
description: SettingsData.weatherEnabled ? I18n.tr("Weather icon and temperature open the weather activity") : I18n.tr("Enable weather in Time & Weather to show this shortcut")
model: [I18n.tr("Left"), I18n.tr("Right"), I18n.tr("Hidden")]
currentIndex: homeCompactCard.homeSlotIndex(SettingsData.dankIslandHomeWeatherSlot, "hidden")
onSelectionChanged: (index, selected) => {
if (selected)
SettingsData.set("dankIslandHomeWeatherSlot", homeCompactCard._homeSlotValues[index] ?? "hidden");
}
}
SettingsToggleRow {
settingKey: "dankIslandHomeCompactTight"
tags: ["island", "home", "compact", "narrow", "width", "height", "clock"]
text: I18n.tr("Compact Clock Pill")
description: I18n.tr("Compact home clock pill in both width and height")
checked: SettingsData.dankIslandHomeCompactTight
onToggled: checked => SettingsData.set("dankIslandHomeCompactTight", checked)
}
Flow {
width: parent.width
spacing: Theme.spacingS
DankButton {
text: I18n.tr("Launcher Settings")
iconName: "grid_view"
onClicked: {
if (!root.parentModal)
return;
SettingsSearchService.navigateToSection("launcherStyle");
root.parentModal.showWithTabName("launcher");
}
}
DankButton {
text: I18n.tr("Weather Settings")
iconName: "cloud"
onClicked: {
if (!root.parentModal)
return;
SettingsSearchService.navigateToSection("weatherEnabled");
root.parentModal.showWithTabName("time_weather");
}
}
}
}
SettingsCard {
width: parent.width
iconName: "palette"
title: I18n.tr("Appearance")
settingKey: "dankIslandAppearance"
enabled: SettingsData.dankIslandEnabled
SettingsButtonGroupRow {
settingKey: "dankIslandPalette"
tags: ["island", "appearance", "palette", "surface", "bright", "dim"]
text: I18n.tr("Palette")
model: [I18n.tr("Default"), I18n.tr("Bright"), I18n.tr("Dim")]
currentIndex: SettingsData.dankIslandPalette === "bright" ? 1 : SettingsData.dankIslandPalette === "dim" ? 2 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("dankIslandPalette", index === 1 ? "bright" : index === 2 ? "dim" : "default");
}
}
SettingsToggleRow {
settingKey: "dankIslandHighContrast"
tags: ["island", "appearance", "contrast", "accessibility", "outline"]
text: I18n.tr("High Contrast")
description: I18n.tr("Draw an outline and use the highest-contrast surface tone")
checked: SettingsData.dankIslandHighContrast
onToggled: checked => SettingsData.set("dankIslandHighContrast", checked)
}
SettingsToggleRow {
settingKey: "dankIslandNotificationExpand"
tags: ["island", "notifications", "expand", "arrival", "size"]
text: I18n.tr("Expand Notifications")
description: I18n.tr("Expand notifications by default instead of click or hover")
checked: SettingsData.dankIslandNotificationExpand
onToggled: checked => SettingsData.set("dankIslandNotificationExpand", checked)
}
SettingsToggleRow {
settingKey: "dankIslandMediaClockVisible"
tags: ["island", "media", "clock", "compact", "time"]
text: I18n.tr("Keep Clock with Media")
description: I18n.tr("Show a clickable clock beside compact media details")
checked: SettingsData.dankIslandMediaClockVisible
onToggled: checked => SettingsData.set("dankIslandMediaClockVisible", checked)
}
SettingsButtonGroupRow {
settingKey: "dankIslandBatteryStyle"
tags: ["island", "battery", "gauge", "solid", "outline", "appearance"]
text: I18n.tr("Battery Gauge")
description: I18n.tr("Solid material type or Outline")
visible: BatteryService.batteryAvailable
model: [I18n.tr("Solid"), I18n.tr("Outline")]
currentIndex: SettingsData.dankIslandBatteryStyle === "outline" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (selected)
SettingsData.set("dankIslandBatteryStyle", index === 1 ? "outline" : "solid");
}
}
}
SettingsCard {
width: parent.width
iconName: "widgets"
title: I18n.tr("Satellite Widgets")
settingKey: "dankIslandSatellites"
collapsible: true
expanded: true
enabled: SettingsData.dankIslandEnabled
SettingsToggleRow {
settingKey: "dankIslandSatellitesEnabled"
tags: ["island", "satellite", "widgets", "left", "right"]
text: I18n.tr("Show Satellite Widgets")
description: I18n.tr("Place independent widgets to left and right of island")
checked: SettingsData.dankIslandSatellitesEnabled
onToggled: checked => SettingsData.set("dankIslandSatellitesEnabled", checked)
}
SettingsButtonGroupRow {
settingKey: "dankIslandSatellitePosition"
tags: ["island", "satellite", "widgets", "position", "edges", "center"]
text: I18n.tr("Placement")
description: I18n.tr("Keep widgets beside the island or align them like a standalone bar")
model: [I18n.tr("Near Island"), I18n.tr("Screen Edges")]
currentIndex: SettingsData.dankIslandSatellitePosition === "edges" ? 1 : 0
enabled: SettingsData.dankIslandSatellitesEnabled
onSelectionChanged: (index, selected) => {
if (selected)
SettingsData.set("dankIslandSatellitePosition", index === 1 ? "edges" : "island");
}
}
SettingsSliderRow {
settingKey: "dankIslandSatelliteGap"
tags: ["island", "satellite", "widgets", "gap", "spacing"]
text: I18n.tr("Island Gap")
unit: "px"
minimum: 4
maximum: 48
step: 1
defaultValue: 12
value: SettingsData.dankIslandSatelliteGap
enabled: SettingsData.dankIslandSatellitesEnabled && SettingsData.dankIslandSatellitePosition !== "edges"
onSliderValueChanged: value => SettingsData.set("dankIslandSatelliteGap", value)
}
StyledText {
width: parent.width
text: I18n.tr("Satellites widget to place left or right of the island")
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
visible: SettingsData.dankIslandSatellitesEnabled
}
}
SettingsCard {
width: parent.width
iconName: "touch_app"
title: I18n.tr("Interaction")
settingKey: "dankIslandInteraction"
enabled: SettingsData.dankIslandEnabled
SettingsButtonGroupRow {
settingKey: "dankIslandInteractionMode"
tags: ["island", "interaction", "click", "hybrid", "expand"]
text: I18n.tr("Expansion Mode")
description: I18n.tr("Click expands only on an intentional press. Hybrid peeks glance faces on hover")
readonly property var _modeValues: ["click", "hybrid"]
model: [I18n.tr("Click"), I18n.tr("Hybrid")]
currentIndex: SettingsData.dankIslandInteractionMode === "click" ? 0 : 1
onSelectionChanged: (index, selected) => {
if (selected)
SettingsData.set("dankIslandInteractionMode", _modeValues[index] ?? "hybrid");
}
}
StyledText {
width: parent.width
text: I18n.tr("Hybrid peeks at the clock, media, system faces, and notifications on hover. Control Center, the launcher, wallpapers, and weather are click actions")
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
visible: SettingsData.dankIslandInteractionMode !== "click"
}
SettingsSliderRow {
settingKey: "dankIslandHoverOpenDelay"
tags: ["island", "interaction", "hover", "open", "delay"]
text: I18n.tr("Hover Open Delay")
unit: "ms"
minimum: 0
maximum: 1000
step: 10
defaultValue: 220
value: SettingsData.dankIslandHoverOpenDelay
enabled: SettingsData.dankIslandInteractionMode !== "click"
onSliderValueChanged: value => SettingsData.set("dankIslandHoverOpenDelay", value)
}
SettingsSliderRow {
settingKey: "dankIslandHoverCloseDelay"
tags: ["island", "interaction", "hover", "close", "delay"]
text: I18n.tr("Hover Close Delay")
unit: "ms"
minimum: 0
maximum: 1000
step: 10
defaultValue: 400
value: SettingsData.dankIslandHoverCloseDelay
enabled: SettingsData.dankIslandInteractionMode !== "click"
onSliderValueChanged: value => SettingsData.set("dankIslandHoverCloseDelay", value)
}
}
SettingsCard {
width: parent.width
iconName: "animation"
title: I18n.tr("Motion & Accessibility")
settingKey: "dankIslandMotion"
collapsible: true
enabled: SettingsData.dankIslandEnabled
SettingsToggleRow {
settingKey: "dankIslandReducedMotion"
text: I18n.tr("Reduce Motion")
description: I18n.tr("Apply island geometry changes immediately without spring overshoot")
checked: SettingsData.dankIslandReducedMotion
onToggled: checked => SettingsData.set("dankIslandReducedMotion", checked)
}
SettingsSliderRow {
settingKey: "dankIslandSpringStiffness"
text: I18n.tr("Spring Stiffness")
description: I18n.tr("Higher values pull island toward its target more strongly")
minimum: 100
maximum: 1200
step: 10
value: Math.round(SettingsData.dankIslandSpringStiffness)
defaultValue: 560
enabled: !SettingsData.dankIslandReducedMotion
onSliderValueChanged: value => SettingsData.set("dankIslandSpringStiffness", value)
}
SettingsSliderRow {
settingKey: "dankIslandSpringDamping"
text: I18n.tr("Spring Damping")
description: I18n.tr("Higher values settle island with less bounce")
minimum: 10
maximum: 100
step: 1
value: Math.round(SettingsData.dankIslandSpringDamping)
defaultValue: 37
enabled: !SettingsData.dankIslandReducedMotion
onSliderValueChanged: value => SettingsData.set("dankIslandSpringDamping", value)
}
SettingsSliderRow {
settingKey: "dankIslandSpringMass"
text: I18n.tr("Spring Mass")
description: I18n.tr("Higher percentages give island more inertia")
minimum: 25
maximum: 300
step: 5
unit: "%"
value: Math.round(SettingsData.dankIslandSpringMass * 100)
defaultValue: 100
enabled: !SettingsData.dankIslandReducedMotion
onSliderValueChanged: value => SettingsData.set("dankIslandSpringMass", value / 100)
}
}
}
}
}
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Modules.Settings.Widgets
import qs.Services
import qs.Widgets
@@ -13,13 +14,16 @@ Item {
function getBarComponentsFromSettings() {
const bars = SettingsData.barConfigs || [];
return bars.map(bar => ({
"id": "bar:" + bar.id,
"name": bar.name || "Bar",
"description": I18n.tr("Individual bar configuration"),
"icon": "toolbar",
"barId": bar.id
}));
return bars.map(bar => {
const isIsland = SettingsData.isIslandBarConfig(bar);
return {
"id": "bar:" + bar.id,
"name": bar.name || "Bar",
"description": isIsland ? I18n.tr("Dank Island instance") : I18n.tr("Individual bar configuration"),
"icon": isIsland ? "view_in_ar" : "toolbar",
"barId": bar.id
};
});
}
property var variantComponents: getVariantComponentsList()
+5 -5
View File
@@ -79,7 +79,7 @@ Item {
StyledText {
width: parent.width
visible: !SettingsData.connectedFrameModeActive
text: SettingsData.launcherStyle === "spotlight" ? I18n.tr("Default launcher shortcuts open the minimal Spotlight Bar. The dedicated Spotlight Bar shortcut below stays independent.") : I18n.tr("Default launcher shortcuts open the full launcher with mode tabs, grid view, and action panel.")
text: SettingsData.launcherStyle === "island" ? I18n.tr("Default launcher shortcuts open a native launcher activity inside Dank Island. Spotlight is used when the focused screen has no DankIsland.") : SettingsData.launcherStyle === "spotlight" ? I18n.tr("Default launcher shortcuts open the minimal Spotlight Bar. The dedicated Spotlight Bar shortcut below stays independent.") : I18n.tr("Default launcher shortcuts open the full launcher with mode tabs, grid view, and action panel.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
@@ -88,14 +88,14 @@ Item {
SettingsButtonGroupRow {
visible: !SettingsData.connectedFrameModeActive
settingKey: "launcherStyleSelector"
tags: ["launcher", "style", "default", "spotlight", "full", "minimal"]
tags: ["launcher", "style", "default", "spotlight", "full", "minimal", "island", "dankisland"]
text: I18n.tr("Default Opens")
model: [I18n.tr("Full"), I18n.tr("Spotlight")]
currentIndex: SettingsData.launcherStyle === "spotlight" ? 1 : 0
model: [I18n.tr("Full"), I18n.tr("Spotlight"), I18n.tr("Island")]
currentIndex: SettingsData.launcherStyle === "island" ? 2 : SettingsData.launcherStyle === "spotlight" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("launcherStyle", index === 1 ? "spotlight" : "full");
SettingsData.set("launcherStyle", index === 2 ? "island" : index === 1 ? "spotlight" : "full");
}
}
@@ -0,0 +1,79 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
StyledRect {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string settingLabel: ""
property string reason: ""
property var parentModal: null
width: parent?.width ?? 0
height: contentRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.primary, 0.08)
border.color: Theme.withAlpha(Theme.primary, 0.18)
border.width: 1
Row {
id: contentRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "view_in_ar"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - Theme.iconSize - openButton.width - Theme.spacingM * 2
spacing: Theme.spacingXXS
StyledText {
text: root.settingLabel
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
}
StyledText {
text: root.reason
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
visible: root.reason !== ""
}
}
DankButton {
id: openButton
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Open Island")
backgroundColor: Theme.primary
textColor: Theme.primaryText
buttonHeight: 32
horizontalPadding: Theme.spacingM
onClicked: {
if (!root.parentModal)
return;
root.parentModal.showWithTabName("dank_island");
}
}
}
}
+16 -14
View File
@@ -24,6 +24,12 @@ Item {
return pos === SettingsData.Position.Left || pos === SettingsData.Position.Right;
}
readonly property bool dankIslandOwnsSelectedBarCenter: {
SettingsData.dankIslandBarId;
selectedBarId;
return !!selectedBarId && selectedBarId === SettingsData.dankIslandBarId;
}
property bool hasMultipleBars: SettingsData.barConfigs.length > 1
property int pluginCatalogRevision: 0
@@ -423,6 +429,8 @@ Item {
}
function setWidgetsForSection(sectionId, widgets) {
if (sectionId === "center" && dankIslandOwnsSelectedBarCenter)
return;
switch (sectionId) {
case "left":
SettingsData.updateBarConfig(selectedBarId, {
@@ -522,16 +530,7 @@ Item {
"id": widget,
"enabled": true
};
var result = {
"id": widget.id,
"enabled": widget.enabled
};
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "clockDateOrder", "focusedWindowSize", "focusedWindowCompactMode", "focusedWindowShowIcon", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "keyboardLayoutNameShowIcon", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showBatteryPercent", "showBatteryPercentOnlyOnBattery", "showBatteryTime", "showBatteryTimeOnlyOnBattery", "batteryPillStyle", "batteryPillPercentSign", "showPrinterIcon", "showScreenSharingIcon", "showIdleInhibitorIcon", "showDoNotDisturbIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion", "trayPopupSingleLine", "trayAutoOverflow", "trayMaxVisibleItems", "hideWhenIdle"];
for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]];
}
return result;
return Object.assign({}, widget);
}
function handleItemEnabledChanged(sectionId, itemId, enabled) {
@@ -574,6 +573,8 @@ Item {
// Move a widget across sections (or within); committed as one atomic bar-config save
function moveWidget(fromSection, toSection, movedId, toIndex) {
if (dankIslandOwnsSelectedBarCenter && (fromSection === "center" || toSection === "center"))
return;
if (fromSection === toSection) {
var arr = getWidgetsForSection(fromSection).slice();
var fi = arr.findIndex(w => (typeof w === "string" ? w : w.id) === movedId);
@@ -598,7 +599,7 @@ Item {
}
function sectionAtY(gy) {
var sections = ["left", "center", "right"];
var sections = dankIslandOwnsSelectedBarCenter ? ["left", "right"] : ["left", "center", "right"];
var nearest = "";
var nearestDist = Infinity;
for (var i = 0; i < sections.length; i++) {
@@ -724,7 +725,7 @@ Item {
}
function moveAcrossSections(sectionId, id, delta) {
var order = ["left", "center", "right"];
var order = dankIslandOwnsSelectedBarCenter ? ["left", "right"] : ["left", "center", "right"];
var si = order.indexOf(sectionId);
var ti = si + delta;
if (si < 0 || ti < 0 || ti >= order.length)
@@ -1419,9 +1420,10 @@ Item {
id: centerSection
anchors.fill: parent
anchors.margins: Theme.spacingL
title: selectedBarIsVertical ? I18n.tr("Middle Section") : I18n.tr("Center Section")
titleIcon: "format_align_center"
title: widgetsTab.dankIslandOwnsSelectedBarCenter ? I18n.tr("Center Section - Reserved by Dank Island") : (selectedBarIsVertical ? I18n.tr("Middle Section") : I18n.tr("Center Section"))
titleIcon: widgetsTab.dankIslandOwnsSelectedBarCenter ? "lock" : "format_align_center"
sectionId: "center"
readOnly: widgetsTab.dankIslandOwnsSelectedBarCenter
allWidgets: widgetsTab.baseWidgetDefinitions
items: widgetsTab.getItemsForSection("center")
onItemEnabledChanged: (sectionId, itemId, enabled) => {
@@ -14,6 +14,10 @@ Column {
property string title: ""
property string titleIcon: "widgets"
property string sectionId: ""
property bool readOnly: false
enabled: !readOnly
opacity: readOnly ? 0.55 : 1.0
DankTooltipV2 {
id: sharedTooltip
+3 -1
View File
@@ -17,7 +17,9 @@ PanelWindow {
readonly property real dpr: modelData ? CompositorService.getScreenScale(modelData) : 1
readonly property real shadowBuffer: 5
readonly property real toastY: Theme.barHeight - 4 + (SettingsData.barConfigs[0]?.spacing ?? 4) + 2
// Toasts always drop from the top edge, so they clear whichever chrome owns it.
readonly property real islandTopOffset: SettingsData.dankIslandEdgeOffset(modelData, "top")
readonly property real toastY: islandTopOffset > 0 ? islandTopOffset + Theme.spacingS : Theme.barHeight - 4 + (SettingsData.getPrimaryBarConfig()?.spacing ?? 4) + 2
Connections {
target: ToastService
+4 -2
View File
@@ -80,7 +80,7 @@ Singleton {
function onBarConfigsChanged() {
if (!CompositorService.isHyprland)
return;
const newGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4));
const newGaps = Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4));
if (newGaps === root._lastGapValue)
return;
root._lastGapValue = newGaps;
@@ -330,7 +330,7 @@ Singleton {
layoutGenerationRunning = true;
const defaultRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4)) : 4;
const defaultBorderSize = 2;
const cornerRadius = (typeof SettingsData !== "undefined" && SettingsData.hyprlandLayoutRadiusOverride >= 0) ? SettingsData.hyprlandLayoutRadiusOverride : defaultRadius;
@@ -344,6 +344,8 @@ Singleton {
// Hyprland `xray = false` is still early-development; unset already samples real content, so only force xray=true
// dms:frame only in separate mode — connected-mode frame blur overlaps windows via popouts/arcs
const xrayNamespaces = ["dms:bar"];
if (typeof SettingsData !== "undefined" && SettingsData.dankIslandEnabled)
xrayNamespaces.push("dms:dankisland");
if (frameEnabled && SettingsData.frameMode !== "connected")
xrayNamespaces.push("dms:frame");
+2 -2
View File
@@ -525,7 +525,7 @@ Singleton {
function onBarConfigsChanged() {
if (!CompositorService.isMango)
return;
const newGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4));
const newGaps = Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4));
if (newGaps === root._lastGapValue)
return;
root._lastGapValue = newGaps;
@@ -631,7 +631,7 @@ Singleton {
return;
const defaultRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4)) : 4;
const defaultBorderSize = 2;
const cornerRadius = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutRadiusOverride >= 0) ? SettingsData.mangoLayoutRadiusOverride : defaultRadius;
+4 -2
View File
@@ -121,7 +121,7 @@ Singleton {
Connections {
target: SettingsData
function onBarConfigsChanged() {
const newGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4));
const newGaps = Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4));
if (newGaps === root._lastGapValue)
return;
root._lastGapValue = newGaps;
@@ -1203,7 +1203,7 @@ Singleton {
log.debug("Generating layout config...");
const defaultRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4)) : 4;
const defaultBorderSize = 2;
const cornerRadius = (typeof SettingsData !== "undefined" && SettingsData.niriLayoutRadiusOverride >= 0) ? SettingsData.niriLayoutRadiusOverride : defaultRadius;
@@ -1214,6 +1214,8 @@ Singleton {
const frameEnabled = typeof SettingsData !== "undefined" && SettingsData.frameEnabled;
// dms:frame only in separate mode — connected-mode frame blur overlaps windows via popouts/arcs
const excludeNamespaces = ["dms:bar"];
if (typeof SettingsData !== "undefined" && SettingsData.dankIslandEnabled)
excludeNamespaces.push("dms:dankisland");
if (frameEnabled && SettingsData.frameMode !== "connected")
excludeNamespaces.push("dms:frame");
+7 -2
View File
@@ -38,6 +38,8 @@ Singleton {
readonly property int notificationDedupBurstMs: 5000
property var _recentDedupKeys: []
signal notificationDeduplicated(var wrapper)
property var _dismissQueue: []
property int _dismissBatchSize: 8
property int _dismissTickMs: 8
@@ -657,8 +659,11 @@ Singleton {
const dedupKey = _notificationDedupKey(notif);
const duplicate = _findActiveDuplicate(notif);
if (duplicate || _hasRecentDuplicate(dedupKey)) {
if (duplicate && duplicate.timer && duplicate.timer.running)
duplicate.timer.restart();
if (duplicate) {
if (duplicate.timer && duplicate.timer.running)
duplicate.timer.restart();
root.notificationDeduplicated(duplicate);
}
try {
notif.dismiss();
} catch (e) {}
+1 -1
View File
@@ -707,7 +707,7 @@ Singleton {
return id !== widgetId;
}
const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
const defaultBar = SettingsData.getPrimaryBarConfig();
if (!defaultBar)
return;
const leftWidgets = defaultBar.leftWidgets || [];
+97
View File
@@ -37,6 +37,7 @@ Singleton {
property var clipboardHistoryModal: null
property var dankLauncherV2Modal: null
property var dankLauncherV2ModalLoader: null
property var dankIslandRouter: null
property var spotlightBarModal: null
property var spotlightBarModalLoader: null
property var powerMenuModal: null
@@ -132,7 +133,93 @@ Singleton {
}
}
function _islandOwnsSharedTrigger(screen) {
const target = screen ?? dankIslandRouter?.focusedIslandScreen?.() ?? null;
if (dankIslandRouter?.hasHostForScreen?.(target) !== true)
return false;
return SettingsData.dankIslandIsSoleBarForScreen(target);
}
readonly property bool islandControlCenterOpen: dankIslandRouter?.controlCenterOpen ?? false
readonly property bool islandMediaOpen: dankIslandRouter?.mediaOpen ?? false
readonly property bool islandWallpaperOpen: dankIslandRouter?.wallpaperOpen ?? false
readonly property bool islandWeatherOpen: dankIslandRouter?.weatherOpen ?? false
readonly property bool islandHomeOpen: dankIslandRouter?.homeOpen ?? false
readonly property bool islandNotificationCenterOpen: dankIslandRouter?.notificationCenterOpen ?? false
// Shared triggers use the island only when it owns that screen; false lets callers keep the popout path.
function routeControlCenterToIsland(screen, section, shouldToggle) {
if (!_islandOwnsSharedTrigger(screen))
return false;
if (shouldToggle === true)
return dankIslandRouter.toggleControlCenter(section || "", screen ?? null);
return dankIslandRouter.openControlCenter(section || "", screen ?? null);
}
// A missing media session is a deliberate fallback signal, not a handled request.
function routeMediaToIsland(screen, shouldToggle) {
if (!_islandOwnsSharedTrigger(screen))
return false;
if (shouldToggle === true)
return dankIslandRouter.toggleMedia(screen ?? null) === true;
return dankIslandRouter.openMedia(screen ?? null) === true;
}
function routeWallpaperToIsland(screen, shouldToggle) {
if (!_islandOwnsSharedTrigger(screen))
return false;
if (shouldToggle === true)
return dankIslandRouter.toggleWallpaper(screen ?? null) === true;
return dankIslandRouter.openWallpaper(screen ?? null) === true;
}
function routeWeatherToIsland(screen, shouldToggle) {
if (!_islandOwnsSharedTrigger(screen))
return false;
if (shouldToggle === true)
return dankIslandRouter.toggleWeather(screen ?? null) === true;
return dankIslandRouter.openWeather(screen ?? null) === true;
}
function routeOverviewToIsland(screen, shouldToggle) {
if (!_islandOwnsSharedTrigger(screen))
return false;
if (shouldToggle === true)
return dankIslandRouter.toggleHome(screen ?? null) === true;
return dankIslandRouter.openHome(screen ?? null) === true;
}
function routeNotificationCenterToIsland(screen, shouldToggle) {
if (!_islandOwnsSharedTrigger(screen))
return false;
if (shouldToggle === true)
return dankIslandRouter.toggleNotifications(screen ?? null) === true;
return dankIslandRouter.openNotifications(screen ?? null) === true;
}
function closeIslandNotificationCenter() {
return dankIslandRouter?.closeNotifications?.() === true;
}
function closeIslandMedia() {
return dankIslandRouter?.closeMedia?.() === true;
}
function closeIslandWallpaper() {
return dankIslandRouter?.closeWallpaper?.() === true;
}
function closeIslandWeather() {
return dankIslandRouter?.closeWeather?.() === true;
}
function closeIslandHome() {
return dankIslandRouter?.closeHome?.() === true;
}
function openControlCenter(x, y, width, section, screen) {
if (_islandOwnsSharedTrigger(screen) && dankIslandRouter.openControlCenter(section || "", screen ?? null))
return;
if (controlCenterPopout) {
setPosition(controlCenterPopout, x, y, width, section, screen);
controlCenterPopout.open();
@@ -140,6 +227,8 @@ Singleton {
}
function closeControlCenter() {
if (dankIslandRouter?.closeControlCenter?.() === true)
return;
controlCenterPopout?.close();
}
@@ -148,6 +237,8 @@ Singleton {
}
function toggleControlCenter(x, y, width, section, screen) {
if (_islandOwnsSharedTrigger(screen) && dankIslandRouter.toggleControlCenter(section || "", screen ?? null))
return;
if (controlCenterPopout) {
setPosition(controlCenterPopout, x, y, width, section, screen);
controlCenterPopout.toggle();
@@ -155,6 +246,8 @@ Singleton {
}
function openNotificationCenter(x, y, width, section, screen) {
if (routeNotificationCenterToIsland(screen, false))
return;
if (notificationCenterPopout) {
setPosition(notificationCenterPopout, x, y, width, section, screen);
notificationCenterPopout.open();
@@ -162,6 +255,8 @@ Singleton {
}
function closeNotificationCenter() {
if (closeIslandNotificationCenter())
return;
notificationCenterPopout?.close();
}
@@ -170,6 +265,8 @@ Singleton {
}
function toggleNotificationCenter(x, y, width, section, screen) {
if (routeNotificationCenterToIsland(screen, true))
return;
if (notificationCenterPopout) {
setPosition(notificationCenterPopout, x, y, width, section, screen);
notificationCenterPopout.toggle();
+2 -2
View File
@@ -23,7 +23,7 @@ Item {
property string _barLayoutStateJson: {
if (!barSurfacesLoaded)
return "[]";
const configs = SettingsData.barConfigs;
const configs = SettingsData.getBarKindConfigs();
const mapped = configs.map(c => ({
id: c.id,
position: c.position,
@@ -117,7 +117,7 @@ Item {
property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader
// Horizontal bars must claim their exclusive zones first, so vertical bars wait for every enabled horizontal bar to load
readonly property int horizontalWanted: SettingsData.barConfigs.filter(c => (c.enabled ?? false) && c.position !== SettingsData.Position.Left && c.position !== SettingsData.Position.Right).length
readonly property int horizontalWanted: SettingsData.getBarKindConfigs().filter(c => (c.enabled ?? false) && c.position !== SettingsData.Position.Left && c.position !== SettingsData.Position.Right).length
property int horizontalReady: 0
function recountHorizontalReady() {
+1 -1
View File
@@ -45,6 +45,6 @@ Item {
anchors.fill: parent
radius: root.radius
visible: false
layer.enabled: true
layer.enabled: root.blurActive
}
}
+6 -1
View File
@@ -130,11 +130,13 @@ PanelWindow {
const configs = SettingsData.barConfigs;
if (!screen || !configs)
return offsets;
const defaultBar = configs[0] || SettingsData.getBarConfig("default");
const defaultBar = SettingsData.getPrimaryBarConfig();
for (var i = 0; i < configs.length; i++) {
const bc = configs[i];
if (!bc || !(bc.enabled ?? true) || !(bc.visible ?? true))
continue;
if (SettingsData.isIslandBarConfig(bc))
continue;
if (!SettingsData.barConfigCoversScreen(bc, screen))
continue;
const innerPadding = bc.innerPadding ?? (defaultBar?.innerPadding ?? 4);
@@ -158,6 +160,9 @@ PanelWindow {
break;
}
}
// Legacy OSDs still instantiate on island screens, so the strip has to be reserved for them.
offsets.top = Math.max(offsets.top, SettingsData.dankIslandEdgeOffset(screen, "top"));
offsets.bottom = Math.max(offsets.bottom, SettingsData.dankIslandEdgeOffset(screen, "bottom"));
return offsets;
}
@@ -86,6 +86,7 @@ CATEGORY_KEYWORDS = {
"Greeter": ["login", "greetd", "display manager"],
"Multiplexers": ["tmux", "zellij", "terminal"],
"Frame": ["window", "border", "decoration"],
"Dank Island": ["island", "activities", "dynamic island"],
"Default Apps": ["browser", "terminal", "handlers", "mime"],
"Users": ["accounts", "user", "profile"],
"Autostart": ["startup", "launch", "boot"],
@@ -96,6 +97,7 @@ TAB_INDEX_MAP = {
"TimeWeatherTab.qml": 1,
"KeybindsTab.qml": 2,
"DankBarTab.qml": 3,
"DankIslandTab.qml": 46,
"WorkspacesTab.qml": 4,
"CompositorLayoutTab.qml": 37,
"WindowRulesTab.qml": 38,
@@ -191,6 +193,7 @@ TAB_CATEGORY_MAP = {
43: "Dank Dash",
44: "System",
45: "System",
46: "Dank Island",
}
SEARCHABLE_COMPONENTS = [

Some files were not shown because too many files have changed in this diff Show More