mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-21 10:44:01 -04:00
feat(Dank Island): add new island bar instance
This commit is contained in:
+100
@@ -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
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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 || [];
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -45,6 +45,6 @@ Item {
|
||||
anchors.fill: parent
|
||||
radius: root.radius
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
layer.enabled: root.blurActive
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user