Improve monitor reconnect/disconnect logic. (#974)
* WIP: Improve monitor reconnect logic * Organize revealer into a controller and clean up bindings of destroyed bars. * Improve monitor disconnect/reconnect logic. * Add JSDoc
This commit is contained in:
@@ -1,19 +1,24 @@
|
||||
import { GdkMonitorService } from 'src/services/display/monitor';
|
||||
import { JSXElement } from 'src/core/types';
|
||||
import { BarLayout } from './layout/BarLayout';
|
||||
import { getCoreWidgets } from './layout/coreWidgets';
|
||||
import { WidgetRegistry } from './layout/WidgetRegistry';
|
||||
|
||||
const gdkMonitorService = new GdkMonitorService();
|
||||
const widgetRegistry = new WidgetRegistry(getCoreWidgets());
|
||||
|
||||
/**
|
||||
* Factory function to create a Bar for a specific monitor
|
||||
* Creates a bar widget for a specific monitor with proper error handling
|
||||
* to prevent crashes when monitors become invalid.
|
||||
*
|
||||
* @param gdkMonitor - The GDK monitor index where the bar will be displayed
|
||||
* @param hyprlandMonitor - The corresponding Hyprland monitor ID for workspace
|
||||
* filtering and layout assignment
|
||||
* @returns A JSX element representing the bar widget for the specified monitor
|
||||
*/
|
||||
export const Bar = async (monitor: number): Promise<JSX.Element> => {
|
||||
export const Bar = async (gdkMonitor: number, hyprlandMonitor?: number): Promise<JSXElement> => {
|
||||
await widgetRegistry.initialize();
|
||||
|
||||
const hyprlandMonitor = gdkMonitorService.mapGdkToHyprland(monitor);
|
||||
const barLayout = new BarLayout(monitor, hyprlandMonitor, widgetRegistry);
|
||||
const hyprlandId = hyprlandMonitor ?? gdkMonitor;
|
||||
const barLayout = new BarLayout(gdkMonitor, hyprlandId, widgetRegistry);
|
||||
|
||||
return barLayout.render();
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { App, Gtk } from 'astal/gtk3';
|
||||
import { App, Gdk, Gtk } from 'astal/gtk3';
|
||||
import Astal from 'gi://Astal?version=3.0';
|
||||
import { bind, Binding, Variable } from 'astal';
|
||||
import { idleInhibit } from 'src/lib/window/visibility';
|
||||
import { WidgetRegistry } from './WidgetRegistry';
|
||||
import { getLayoutForMonitor, isLayoutEmpty } from '../utils/monitors';
|
||||
import options from 'src/configuration';
|
||||
import { JSXElement } from 'src/core/types';
|
||||
|
||||
/**
|
||||
* Responsible for the bar UI layout and positioning
|
||||
@@ -46,7 +47,27 @@ export class BarLayout {
|
||||
this._initializeReactiveVariables();
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
public render(): JSXElement {
|
||||
const display = Gdk.Display.get_default();
|
||||
if (!display) {
|
||||
console.error('[BarLayout] No display available for bar creation');
|
||||
return null;
|
||||
}
|
||||
|
||||
const monitorCount = display.get_n_monitors();
|
||||
if (this._gdkMonitor < 0 || this._gdkMonitor >= monitorCount) {
|
||||
console.error(
|
||||
`[BarLayout] Invalid monitor index: ${this._gdkMonitor} (total monitors: ${monitorCount})`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const monitor = display.get_monitor(this._gdkMonitor);
|
||||
if (monitor === null) {
|
||||
console.error(`[BarLayout] Monitor at index ${this._gdkMonitor} no longer exists`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<window
|
||||
inhibit={bind(idleInhibit)}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Gdk } from 'astal/gtk3';
|
||||
import { range } from 'src/lib/array/helpers';
|
||||
import { BarLayout, BarLayouts } from 'src/lib/options/types';
|
||||
import { GdkMonitorService } from 'src/services/display/monitor';
|
||||
import { MonitorMapping } from './types';
|
||||
import { JSXElement } from 'src/core/types';
|
||||
|
||||
/**
|
||||
* Returns the bar layout configuration for a specific monitor
|
||||
@@ -43,17 +45,48 @@ export const isLayoutEmpty = (layout: BarLayout): boolean => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates an array of JSX elements for each monitor.
|
||||
* Creates widgets for all available monitors with proper GDK to Hyprland monitor mapping.
|
||||
*
|
||||
* This function creates an array of JSX elements by calling the provided widget function for each monitor.
|
||||
* It uses the number of monitors available in the default Gdk display.
|
||||
*
|
||||
* @param widget A function that takes a monitor index and returns a JSX element.
|
||||
*
|
||||
* @returns An array of JSX elements, one for each monitor.
|
||||
* @param widget - Function that creates a widget for a given monitor index
|
||||
* @returns Array of created widgets for all available monitors
|
||||
*/
|
||||
export async function forMonitors(widget: (monitor: number) => Promise<JSX.Element>): Promise<JSX.Element[]> {
|
||||
const n = Gdk.Display.get_default()?.get_n_monitors() ?? 1;
|
||||
export async function forMonitors(
|
||||
widget: (monitor: number, hyprlandMonitor?: number) => Promise<JSXElement>,
|
||||
): Promise<JSXElement[]> {
|
||||
const display = Gdk.Display.get_default();
|
||||
if (display === null) {
|
||||
console.error('[forMonitors] No display available');
|
||||
return [];
|
||||
}
|
||||
|
||||
return Promise.all(range(n, 0).map(widget));
|
||||
const monitorCount = display.get_n_monitors();
|
||||
const gdkMonitorService = GdkMonitorService.getInstance();
|
||||
const monitorMappings: MonitorMapping[] = [];
|
||||
|
||||
for (let gdkMonitorIndex = 0; gdkMonitorIndex < monitorCount; gdkMonitorIndex++) {
|
||||
const monitor = display.get_monitor(gdkMonitorIndex);
|
||||
if (monitor === null) {
|
||||
console.warn(`[forMonitors] Skipping invalid monitor at index ${gdkMonitorIndex}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const hyprlandId = gdkMonitorService.mapGdkToHyprland(gdkMonitorIndex);
|
||||
|
||||
monitorMappings.push({
|
||||
gdkIndex: gdkMonitorIndex,
|
||||
hyprlandId,
|
||||
});
|
||||
}
|
||||
|
||||
const monitorPromises = monitorMappings.map(async ({ gdkIndex, hyprlandId }) => {
|
||||
try {
|
||||
return await widget(gdkIndex, hyprlandId);
|
||||
} catch (error) {
|
||||
console.error(`[forMonitors] Failed to create widget for monitor ${gdkIndex}:`, error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
const widgets = await Promise.all(monitorPromises);
|
||||
|
||||
return widgets.filter((w): w is JSXElement => w !== null);
|
||||
}
|
||||
|
||||
4
src/components/bar/utils/monitors/types.ts
Normal file
4
src/components/bar/utils/monitors/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface MonitorMapping {
|
||||
gdkIndex: number;
|
||||
hyprlandId: number;
|
||||
}
|
||||
@@ -27,9 +27,13 @@ export const NotificationCard = ({
|
||||
showActions,
|
||||
...props
|
||||
}: NotificationCardProps): JSX.Element => {
|
||||
const actionBox: IActionBox | null = notification.get_actions().length ? (
|
||||
<Actions notification={notification} showActions={showActions} />
|
||||
) : null;
|
||||
let actionBox: ActionBox | null;
|
||||
|
||||
if (notification.get_actions().length) {
|
||||
actionBox = <Actions notification={notification} showActions={showActions} />;
|
||||
} else {
|
||||
actionBox = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<eventbox
|
||||
@@ -63,11 +67,11 @@ interface NotificationCardProps extends Widget.BoxProps {
|
||||
showActions: boolean;
|
||||
}
|
||||
|
||||
interface IActionBox extends Gtk.Widget {
|
||||
interface ActionBox extends Gtk.Widget {
|
||||
revealChild?: boolean;
|
||||
}
|
||||
|
||||
interface NotificationContentProps {
|
||||
actionBox: IActionBox | null;
|
||||
actionBox: ActionBox | null;
|
||||
notification: AstalNotifd.Notification;
|
||||
}
|
||||
|
||||
@@ -23,14 +23,18 @@ export const notifHasImg = (notification: AstalNotifd.Notification): boolean =>
|
||||
};
|
||||
|
||||
/**
|
||||
* Tracks the active monitor and updates the provided variable.
|
||||
* Tracks the currently focused monitor and updates the provided variable with its ID.
|
||||
* Includes null safety to prevent crashes when monitors are disconnected or during DPMS events.
|
||||
*
|
||||
* This function sets up a derived variable that updates the `curMonitor` variable with the ID of the focused monitor.
|
||||
*
|
||||
* @param curMonitor The variable to update with the active monitor ID.
|
||||
* @param curMonitor - Variable that will be updated with the current monitor ID (defaults to 0 if no monitor is focused)
|
||||
*/
|
||||
export const trackActiveMonitor = (curMonitor: Variable<number>): void => {
|
||||
Variable.derive([bind(hyprlandService, 'focusedMonitor')], (monitor) => {
|
||||
if (monitor?.id === undefined) {
|
||||
console.warn('No focused monitor available, defaulting to monitor 0');
|
||||
curMonitor.set(0);
|
||||
return;
|
||||
}
|
||||
curMonitor.set(monitor.id);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { GdkMonitorService } from 'src/services/display/monitor/index.js';
|
||||
import { getPosition } from 'src/lib/window/positioning.js';
|
||||
import { NotificationCard } from './Notification';
|
||||
import { App } from 'astal/gtk3';
|
||||
|
||||
const hyprlandService = AstalHyprland.get_default();
|
||||
const { position, monitor, active_monitor, showActionsOnHover, displayedTotal } = options.notifications;
|
||||
@@ -20,15 +21,13 @@ trackPopupNotifications(popupNotifications);
|
||||
trackAutoTimeout();
|
||||
|
||||
export default (): JSX.Element => {
|
||||
const gdkMonitorMapper = new GdkMonitorService();
|
||||
const gdkMonitorMapper = GdkMonitorService.getInstance();
|
||||
|
||||
const windowLayer = bind(tear).as((tear) => (tear ? Astal.Layer.TOP : Astal.Layer.OVERLAY));
|
||||
const windowAnchor = bind(position).as(getPosition);
|
||||
const windowMonitor = Variable.derive(
|
||||
[bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)],
|
||||
(focusedMonitor, monitor, activeMonitor) => {
|
||||
gdkMonitorMapper.reset();
|
||||
|
||||
if (activeMonitor === true) {
|
||||
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(focusedMonitor.id);
|
||||
return gdkMonitor;
|
||||
@@ -55,6 +54,7 @@ export default (): JSX.Element => {
|
||||
name={'notifications-window'}
|
||||
namespace={'notifications-window'}
|
||||
className={'notifications-window'}
|
||||
application={App}
|
||||
layer={windowLayer}
|
||||
anchor={windowAnchor}
|
||||
exclusivity={Astal.Exclusivity.NORMAL}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const setupOsdBar = (self: LevelBar): void => {
|
||||
self.value = brightnessService.kbd;
|
||||
});
|
||||
|
||||
Variable.derive([bind(audioService.defaultMicrophone, 'volume')], () => {
|
||||
const micVolumeBinding = Variable.derive([bind(audioService.defaultMicrophone, 'volume')], () => {
|
||||
self.toggleClassName('overflow', audioService.defaultMicrophone.volume > 1);
|
||||
self.value =
|
||||
audioService.defaultMicrophone.volume <= 1
|
||||
@@ -37,7 +37,7 @@ export const setupOsdBar = (self: LevelBar): void => {
|
||||
: audioService.defaultMicrophone.volume - 1;
|
||||
});
|
||||
|
||||
Variable.derive([bind(audioService.defaultMicrophone, 'mute')], () => {
|
||||
const micMuteBinding = Variable.derive([bind(audioService.defaultMicrophone, 'mute')], () => {
|
||||
self.toggleClassName(
|
||||
'overflow',
|
||||
audioService.defaultMicrophone.volume > 1 &&
|
||||
@@ -51,7 +51,7 @@ export const setupOsdBar = (self: LevelBar): void => {
|
||||
: audioService.defaultMicrophone.volume - 1;
|
||||
});
|
||||
|
||||
Variable.derive([bind(audioService.defaultSpeaker, 'volume')], () => {
|
||||
const speakerVolumeBinding = Variable.derive([bind(audioService.defaultSpeaker, 'volume')], () => {
|
||||
self.toggleClassName('overflow', audioService.defaultSpeaker.volume > 1);
|
||||
self.value =
|
||||
audioService.defaultSpeaker.volume <= 1
|
||||
@@ -59,7 +59,7 @@ export const setupOsdBar = (self: LevelBar): void => {
|
||||
: audioService.defaultSpeaker.volume - 1;
|
||||
});
|
||||
|
||||
Variable.derive([bind(audioService.defaultSpeaker, 'mute')], () => {
|
||||
const speakerMuteBinding = Variable.derive([bind(audioService.defaultSpeaker, 'mute')], () => {
|
||||
self.toggleClassName(
|
||||
'overflow',
|
||||
audioService.defaultSpeaker.volume > 1 &&
|
||||
@@ -72,4 +72,11 @@ export const setupOsdBar = (self: LevelBar): void => {
|
||||
? audioService.defaultSpeaker.volume
|
||||
: audioService.defaultSpeaker.volume - 1;
|
||||
});
|
||||
|
||||
self.connect('destroy', () => {
|
||||
micVolumeBinding.drop();
|
||||
micMuteBinding.drop();
|
||||
speakerVolumeBinding.drop();
|
||||
speakerMuteBinding.drop();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,79 +1,50 @@
|
||||
import { bind, timeout, Variable } from 'astal';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Widget } from 'astal/gtk3';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||
import options from 'src/configuration';
|
||||
import { GdkMonitorService } from 'src/services/display/monitor';
|
||||
import BrightnessService from 'src/services/system/brightness';
|
||||
import { OsdRevealerController } from './revealer/revealerController';
|
||||
|
||||
const wireplumber = AstalWp.get_default() as AstalWp.Wp;
|
||||
const audioService = wireplumber.audio;
|
||||
const brightnessService = BrightnessService.getInstance();
|
||||
const hyprlandService = AstalHyprland.get_default();
|
||||
|
||||
const { enable, duration, active_monitor, monitor } = options.theme.osd;
|
||||
const { enable, active_monitor, monitor } = options.theme.osd;
|
||||
|
||||
let count = 0;
|
||||
|
||||
/*
|
||||
* So the OSD doesn't show on startup for no reason
|
||||
*/
|
||||
let isStartingUp = true;
|
||||
timeout(3000, () => {
|
||||
isStartingUp = false;
|
||||
});
|
||||
const osdController = OsdRevealerController.getInstance();
|
||||
|
||||
/**
|
||||
* Handles the reveal state of a Widget.Revealer or Widget.Window.
|
||||
* Determines which monitor the OSD should appear on based on user configuration.
|
||||
* Safely handles null monitors and DPMS events to prevent crashes.
|
||||
*
|
||||
* This function delegates the reveal handling to either `handleRevealRevealer` or `handleRevealWindow` based on the type of the widget.
|
||||
*
|
||||
* @param self The Widget.Revealer or Widget.Window instance.
|
||||
* @param property The property to check, either 'revealChild' or 'visible'.
|
||||
*/
|
||||
const handleReveal = (self: Widget.Revealer): void => {
|
||||
if (isStartingUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enable.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.reveal_child = true;
|
||||
|
||||
count++;
|
||||
timeout(duration.get(), () => {
|
||||
count--;
|
||||
|
||||
if (count === 0) {
|
||||
self.reveal_child = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the monitor index for the OSD.
|
||||
*
|
||||
* This function derives the monitor index for the OSD based on the focused monitor, default monitor, and active monitor settings.
|
||||
*
|
||||
* @returns A Variable<number> representing the monitor index for the OSD.
|
||||
* @returns Variable containing the GDK monitor index where OSD should be displayed (defaults to 0 if no valid monitor)
|
||||
*/
|
||||
export const getOsdMonitor = (): Variable<number> => {
|
||||
const gdkMonitorMapper = new GdkMonitorService();
|
||||
const gdkMonitorMapper = GdkMonitorService.getInstance();
|
||||
|
||||
return Variable.derive(
|
||||
[bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)],
|
||||
(currentMonitor, defaultMonitor, followMonitor) => {
|
||||
gdkMonitorMapper.reset();
|
||||
try {
|
||||
if (followMonitor === false) {
|
||||
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(defaultMonitor);
|
||||
return gdkMonitor;
|
||||
}
|
||||
|
||||
if (!currentMonitor || currentMonitor.id === undefined || currentMonitor.id === null) {
|
||||
console.warn('OSD: No focused monitor available, defaulting to monitor 0');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (followMonitor === true) {
|
||||
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(currentMonitor.id);
|
||||
return gdkMonitor;
|
||||
} catch (error) {
|
||||
console.error('OSD: Failed to map monitor, defaulting to 0:', error);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(defaultMonitor);
|
||||
return gdkMonitor;
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -86,29 +57,29 @@ export const getOsdMonitor = (): Variable<number> => {
|
||||
* @param self The Widget.Revealer instance to set up.
|
||||
*/
|
||||
export const revealerSetup = (self: Widget.Revealer): void => {
|
||||
self.hook(enable, () => {
|
||||
handleReveal(self);
|
||||
});
|
||||
osdController.setRevealer(self);
|
||||
|
||||
self.hook(brightnessService, 'notify::screen', () => {
|
||||
handleReveal(self);
|
||||
});
|
||||
const handleReveal = (): void => {
|
||||
osdController.show();
|
||||
};
|
||||
|
||||
self.hook(brightnessService, 'notify::kbd', () => {
|
||||
handleReveal(self);
|
||||
});
|
||||
self.hook(enable, handleReveal);
|
||||
self.hook(brightnessService, 'notify::screen', handleReveal);
|
||||
self.hook(brightnessService, 'notify::kbd', handleReveal);
|
||||
|
||||
Variable.derive(
|
||||
const microphoneBinding = Variable.derive(
|
||||
[bind(audioService.defaultMicrophone, 'volume'), bind(audioService.defaultMicrophone, 'mute')],
|
||||
() => {
|
||||
handleReveal(self);
|
||||
},
|
||||
handleReveal,
|
||||
);
|
||||
|
||||
Variable.derive(
|
||||
const speakerBinding = Variable.derive(
|
||||
[bind(audioService.defaultSpeaker, 'volume'), bind(audioService.defaultSpeaker, 'mute')],
|
||||
() => {
|
||||
handleReveal(self);
|
||||
},
|
||||
handleReveal,
|
||||
);
|
||||
|
||||
self.connect('destroy', () => {
|
||||
microphoneBinding.drop();
|
||||
speakerBinding.drop();
|
||||
osdController.onRevealerDestroy(self);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,11 +7,6 @@ const wireplumber = AstalWp.get_default() as AstalWp.Wp;
|
||||
const audioService = wireplumber.audio;
|
||||
const brightnessService = BrightnessService.getInstance();
|
||||
|
||||
type OSDIcon = {
|
||||
micVariable: Variable<unknown>;
|
||||
speakerVariable: Variable<unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up the OSD icon for a given widget.
|
||||
*
|
||||
@@ -22,7 +17,7 @@ type OSDIcon = {
|
||||
*
|
||||
* @returns An object containing the micVariable and speakerVariable, which are derived variables for microphone and speaker status.
|
||||
*/
|
||||
export const setupOsdIcon = (self: Widget.Label): OSDIcon => {
|
||||
export const setupOsdIcon = (self: Widget.Label): void => {
|
||||
self.hook(brightnessService, 'notify::screen', () => {
|
||||
self.label = '';
|
||||
});
|
||||
@@ -45,8 +40,8 @@ export const setupOsdIcon = (self: Widget.Label): OSDIcon => {
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
micVariable,
|
||||
speakerVariable,
|
||||
};
|
||||
self.connect('destroy', () => {
|
||||
micVariable.drop();
|
||||
speakerVariable.drop();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import options from 'src/configuration';
|
||||
import { bind } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { App, Astal } from 'astal/gtk3';
|
||||
import { getOsdMonitor } from './helpers';
|
||||
import { getPosition } from 'src/lib/window/positioning';
|
||||
import { OsdRevealer } from './revealer';
|
||||
@@ -8,20 +8,26 @@ import { OsdRevealer } from './revealer';
|
||||
const { location } = options.theme.osd;
|
||||
|
||||
export default (): JSX.Element => {
|
||||
const osdMonitorBinding = getOsdMonitor();
|
||||
|
||||
return (
|
||||
<window
|
||||
monitor={getOsdMonitor()()}
|
||||
monitor={osdMonitorBinding()}
|
||||
name={'indicator'}
|
||||
application={App}
|
||||
namespace={'indicator'}
|
||||
className={'indicator'}
|
||||
visible={true}
|
||||
layer={bind(options.tear).as((tear) => (tear ? Astal.Layer.TOP : Astal.Layer.OVERLAY))}
|
||||
anchor={bind(location).as((anchorPoint) => getPosition(anchorPoint))}
|
||||
setup={(self) => {
|
||||
getOsdMonitor().subscribe(() => {
|
||||
osdMonitorBinding().subscribe(() => {
|
||||
self.set_click_through(true);
|
||||
});
|
||||
}}
|
||||
onDestroy={() => {
|
||||
osdMonitorBinding.drop();
|
||||
}}
|
||||
clickThrough
|
||||
>
|
||||
<OsdRevealer />
|
||||
|
||||
@@ -27,12 +27,12 @@ export const setupOsdLabel = (self: Widget.Label): void => {
|
||||
self.label = `${Math.round(brightnessService.kbd * 100)}`;
|
||||
});
|
||||
|
||||
Variable.derive([bind(audioService.defaultMicrophone, 'volume')], () => {
|
||||
const micVolumeBinding = Variable.derive([bind(audioService.defaultMicrophone, 'volume')], () => {
|
||||
self.toggleClassName('overflow', audioService.defaultMicrophone.volume > 1);
|
||||
self.label = `${Math.round(audioService.defaultMicrophone.volume * 100)}`;
|
||||
});
|
||||
|
||||
Variable.derive([bind(audioService.defaultMicrophone, 'mute')], () => {
|
||||
const micMuteBinding = Variable.derive([bind(audioService.defaultMicrophone, 'mute')], () => {
|
||||
self.toggleClassName(
|
||||
'overflow',
|
||||
audioService.defaultMicrophone.volume > 1 &&
|
||||
@@ -45,12 +45,12 @@ export const setupOsdLabel = (self: Widget.Label): void => {
|
||||
self.label = `${inputVolume}`;
|
||||
});
|
||||
|
||||
Variable.derive([bind(audioService.defaultSpeaker, 'volume')], () => {
|
||||
const speakerVolumeBinding = Variable.derive([bind(audioService.defaultSpeaker, 'volume')], () => {
|
||||
self.toggleClassName('overflow', audioService.defaultSpeaker.volume > 1);
|
||||
self.label = `${Math.round(audioService.defaultSpeaker.volume * 100)}`;
|
||||
});
|
||||
|
||||
Variable.derive([bind(audioService.defaultSpeaker, 'mute')], () => {
|
||||
const speakerMuteBinding = Variable.derive([bind(audioService.defaultSpeaker, 'mute')], () => {
|
||||
self.toggleClassName(
|
||||
'overflow',
|
||||
audioService.defaultSpeaker.volume > 1 &&
|
||||
@@ -62,4 +62,11 @@ export const setupOsdLabel = (self: Widget.Label): void => {
|
||||
: Math.round(audioService.defaultSpeaker.volume * 100);
|
||||
self.label = `${speakerVolume}`;
|
||||
});
|
||||
|
||||
self.connect('destroy', () => {
|
||||
micVolumeBinding.drop();
|
||||
micMuteBinding.drop();
|
||||
speakerVolumeBinding.drop();
|
||||
speakerMuteBinding.drop();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -32,7 +32,9 @@ export const OsdRevealer = (): JSX.Element => {
|
||||
<revealer
|
||||
transitionType={Gtk.RevealerTransitionType.CROSSFADE}
|
||||
revealChild={false}
|
||||
setup={revealerSetup}
|
||||
setup={(self) => {
|
||||
revealerSetup(self);
|
||||
}}
|
||||
>
|
||||
<box className={'osd-container'} vertical={osdOrientation}>
|
||||
{bind(orientation).as((currentOrientation) => {
|
||||
|
||||
117
src/components/osd/revealer/revealerController.ts
Normal file
117
src/components/osd/revealer/revealerController.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { timeout } from 'astal';
|
||||
import { Widget } from 'astal/gtk3';
|
||||
import AstalIO from 'gi://AstalIO?version=0.1';
|
||||
import options from 'src/configuration';
|
||||
|
||||
const { enable, duration } = options.theme.osd;
|
||||
|
||||
/**
|
||||
* Manages OSD revealer instances to prevent stale references and ensure proper cleanup
|
||||
*/
|
||||
export class OsdRevealerController {
|
||||
private static _instance: OsdRevealerController;
|
||||
|
||||
private _currentRevealer?: Widget.Revealer;
|
||||
private _autoHideTimeout?: AstalIO.Time;
|
||||
private _startupTimeout?: AstalIO.Time;
|
||||
private _allowReveal = false;
|
||||
|
||||
private constructor() {
|
||||
this._startupTimeout = timeout(3000, () => {
|
||||
this._allowReveal = true;
|
||||
this._startupTimeout = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance of the OSD revealer controller
|
||||
*/
|
||||
public static getInstance(): OsdRevealerController {
|
||||
if (this._instance === undefined) {
|
||||
this._instance = new OsdRevealerController();
|
||||
}
|
||||
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a revealer widget as the active OSD display component
|
||||
* Ensures proper cleanup of previous revealers before setting a new one
|
||||
*
|
||||
* @param revealer - The revealer widget to manage
|
||||
*/
|
||||
public setRevealer(revealer: Widget.Revealer): void {
|
||||
if (this._currentRevealer && this._currentRevealer !== revealer) {
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
this._currentRevealer = revealer;
|
||||
revealer.set_reveal_child(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveals the OSD temporarily and sets up auto-hide behavior
|
||||
* Respects enable state and startup delay before allowing reveal
|
||||
*/
|
||||
public show(): void {
|
||||
const enableRevealer = enable.get();
|
||||
if (!this._allowReveal || this._currentRevealer === undefined || !enableRevealer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentRevealer.set_reveal_child(true);
|
||||
|
||||
if (this._autoHideTimeout !== undefined) {
|
||||
this._autoHideTimeout.cancel();
|
||||
this._autoHideTimeout = undefined;
|
||||
}
|
||||
|
||||
const hideDelay = duration.get();
|
||||
const revealer = this._currentRevealer;
|
||||
|
||||
this._autoHideTimeout = timeout(hideDelay, () => {
|
||||
if (revealer !== undefined) {
|
||||
revealer.set_reveal_child(false);
|
||||
}
|
||||
|
||||
this._autoHideTimeout = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels any active auto-hide timeout to prevent stale callbacks
|
||||
*/
|
||||
private _cleanup(): void {
|
||||
if (this._autoHideTimeout) {
|
||||
this._autoHideTimeout.cancel();
|
||||
this._autoHideTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles cleanup when a revealer widget is destroyed
|
||||
* Ensures the controller doesn't hold references to destroyed widgets
|
||||
*
|
||||
* @param revealer - The revealer being destroyed
|
||||
*/
|
||||
public onRevealerDestroy(revealer: Widget.Revealer): void {
|
||||
if (this._currentRevealer === revealer) {
|
||||
this._cleanup();
|
||||
this._currentRevealer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs complete cleanup of the controller instance
|
||||
* Cancels all active timeouts and clears widget references
|
||||
*/
|
||||
public destroy(): void {
|
||||
this._cleanup();
|
||||
|
||||
if (this._startupTimeout) {
|
||||
this._startupTimeout.cancel();
|
||||
this._startupTimeout = undefined;
|
||||
}
|
||||
this._currentRevealer = undefined;
|
||||
}
|
||||
}
|
||||
3
src/components/osd/types.ts
Normal file
3
src/components/osd/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface RevealerSetupBindings {
|
||||
cleanup: () => void;
|
||||
}
|
||||
Reference in New Issue
Block a user