[AGS] GTK4 Migration: Partially complete

This commit is contained in:
2025-04-21 21:48:29 +02:00
parent 7380c75818
commit 8b70f80e60
8 changed files with 321 additions and 267 deletions

View File

@@ -7,209 +7,267 @@
*
*/
import { Astal, Gtk, Gdk } from "astal/gtk3"
import { Astal, Gdk } from "astal/gtk4"
import Notifd from "gi://AstalNotifd";
import Notification from "./notifications";
import { type Subscribable } from "astal/binding";
import { Variable, bind, timeout } from "astal"
import { timeout, Variable } from "astal"
// Config
// ───────────────────────────────────────────────────────────────────
// Config
// ───────────────────────────────────────────────────────────────────
const TIMEOUT_DELAY = 5000;
let isRunning = false;
let notificationMenuOpen = false;
class Notifier implements Subscribable {
private display: Map<number, Gtk.Widget> = new Map();
private notifications: Map<number, Notifd.Notification> = new Map();
interface NotificationDetails {
notification: Notifd.Notification;
backendID: number;
notifdID: number;
}
private notifd: Notifd.Notifd;
private subscriberData: Variable<Gtk.Widget[]> = Variable( [] );
private instanceID: number;
private menuOpen: boolean;
/**
* Sets up the notifier
*/
constructor( id: number ) {
this.instanceID = id;
this.menuOpen = false;
this.notifd = Notifd.get_default();
this.notifd.ignoreTimeout = true;
// ───────────────────────────────────────────────────────────────────
// ╭───────────────────────────────────────────────╮
// │ Handler
// ╰───────────────────────────────────────────────╯
// ───────────────────────────────────────────────────────────────────
let ShownNotifications: number[] = [];
const ShownNotificationsCount: Variable<number> = Variable( 0 );
let Notifications: NotificationDetails[] = [];
this.notifd.connect( 'notified', ( _, id ) => {
this.add( id );
} );
const notifd = Notifd.get_default();
notifd.ignoreTimeout = true;
this.notifd.connect( 'resolved', ( _, id ) => {
this.delete( id );
} );
/**
* Delete a notification by its internal ID
* @param index The notifd ID of the notification
*/
const deleteNotification = ( index: number ): void => {
hideNotification( index );
Notifications.splice( index, 1 );
}
// ───────────────────────────────────────────────────────────────────
/**
* Delete a notification by notifd id
* @param id The notifd ID of the notification
*/
const deleteNotificationByNotificationID = ( id: number ): void => {
const index = findNotificationByNotificationID( id );
if ( index > -1 ) {
deleteNotification( index );
}
}
private notify () {
this.subscriberData.set( [ ...this.display.values() ].reverse() );
}
// ───────────────────────────────────────────────────────────────────
private add ( id: number ) {
this.notifications.set( id, this.notifd.get_notification( id )! );
this.show( id );
}
/**
* Show an element on screen
* @param id - The id of the element to be shown
*/
private show ( id: number ) {
this.set( id, Notification( {
notification: this.notifications.get( id )!,
onHoverLost: () => {
if ( !this.menuOpen ) {
this.hide( id );
}
},
setup: () => timeout( TIMEOUT_DELAY, () => {
if ( !this.menuOpen ) {
this.hide( id );
}
} ),
id: id,
delete: deleteHelper,
instanceID: this.instanceID
} ) )
}
/**
* Set a selected widget to be shown
* @param id - The id of the element to be referenced for later
* @param widget - A GTK widget instance
*/
private set ( id: number, widget: Gtk.Widget ) {
this.display.get( id )?.destroy();
this.display.set( id, widget );
this.notify();
}
/**
* Hide, not delete notification (= send to notification centre)
* @param id - The id of the notification to hide
*/
private hide ( id: number ) {
this.display.get( id )?.destroy();
this.display.delete( id );
this.notify();
}
/**
* Delete a notification (from notification centre too)
* @param id - The id of the notification to hide
*/
delete ( id: number ) {
this.hide( id );
this.notifications.get( id )?.dismiss();
this.notifications.delete( id );
if ( this.notifications.size == 0 ) {
this.menuOpen = false;
/**
* Find the internal ID from the notifd id for a notification (helper function)
* @param id The notifd ID of the notification
* @returns The internal ID or -1 if not found
*/
const findNotificationByNotificationID = ( id: number ): number => {
// Find index in Notifications array
for (let index = 0; index < Notifications.length; index++) {
if ( Notifications[ index ].notifdID === id ) {
return index;
}
}
openNotificationMenu () {
// Show all notifications that have not been cleared
if ( this.notifications.size > 0 ) {
this.menuOpen = true;
this.notifications.forEach( ( _, id ) => {
this.show( id );
} )
}
}
return -1;
}
hideNotifications () {
this.menuOpen = false;
this.notifications.forEach( ( _, id ) => {
this.hide( id );
// ───────────────────────────────────────────────────────────────────
/**
* Add a notification to the notification handler
* @param id The notifd ID of the notification
*/
const addNotification = ( id: number ): void => {
print( '[ Notifications ] Notification with id ' + id + ' added.' );
const currIndex = Notifications.length;
Notifications.push( {
notifdID: id,
backendID: currIndex,
notification: notifd.get_notification( id )
} );
showNotification( currIndex );
}
// ───────────────────────────────────────────────────────────────────
/**
* Start the notifd runner and handle notifications.
*/
const hookToNotificationDaemon = (): void => {
if ( isRunning ) {
printerr( '[ Notifications ] Error: Already running' );
return;
}
isRunning = true;
notifd.connect( 'notified', ( _, id ) => {
addNotification( id );
} );
notifd.connect( 'resolved', ( _, id ) => {
deleteNotificationByNotificationID( id );
} );
}
// ───────────────────────────────────────────────────────────────────
/**
* Show a notification. It will stay on screen (regardless of removeAgain passed in), if
* critical urgency
* @param id The internal id (index in Notifications array)
* @param removeAgain = true If to remove the notification from the screen again automatically
*/
const showNotification = ( id: number, removeAgain: boolean = true ) => {
// Add notification to UI for display
ShownNotifications.reverse().push( id );
ShownNotifications.reverse();
ShownNotificationsCount.set( ShownNotifications.length );
// Set delay to remove the notification again
if ( removeAgain && Notifications[ id ].notification.get_urgency() !== Notifd.Urgency.CRITICAL ) {
timeout( TIMEOUT_DELAY, () => {
hideNotification( id );
} );
}
}
toggleNotificationMenu () {
if ( this.menuOpen ) {
this.hideNotifications();
} else {
this.openNotificationMenu();
}
}
// ───────────────────────────────────────────────────────────────────
clearAllNotifications () {
this.menuOpen = false;
this.notifications.forEach( ( _, id ) => {
this.delete( id );
} )
}
clearNewestNotification () {
this.delete( [ ...this.notifications.keys() ][0] );
}
subscribe(callback: (value: unknown) => void): () => void {
return this.subscriberData.subscribe( callback );
}
get() {
return this.subscriberData.get();
/**
* Stop displaying notification
* @param id The internal id (index in the Notifications array)
*/
const hideNotification = ( id: number ) => {
if ( !notificationMenuOpen ) {
ShownNotifications.splice( ShownNotifications.indexOf( id ), 1 );
}
}
const notifiers: Map<number, Notifier> = new Map();
const deleteHelper = ( id: number, instanceID: number ) => {
notifiers.get( instanceID )?.delete( id );
// ───────────────────────────────────────────────────────────────────
/**
* Open the notification menu. Called by toggleNotificationMenu too
*/
const openNotificationMenu = () => {
// Simply show all notifications
notificationMenuOpen = true;
const ShownNotifications = [];
for (let index = 0; index < Notifications.length; index++) {
ShownNotifications.push( index );
}
}
const openNotificationMenu = ( id: number ) => {
notifiers.get( id )?.openNotificationMenu();
// ───────────────────────────────────────────────────────────────────
/**
* Close the notification menu. Called by toggleNotificationMenu too
*/
const closeNotificationMenu = () => {
// Hide all notifications
notificationMenuOpen = true;
ShownNotifications = [];
ShownNotificationsCount.set( 0 );
}
const closeNotificationMenu = ( id: number ) => {
notifiers.get( id )?.hideNotifications();
// ───────────────────────────────────────────────────────────────────
/**
* Toggle the notification menu (i.e. show all notifications)
*/
const toggleNotificationMenu = () => {
if ( notificationMenuOpen ) {
closeNotificationMenu();
} else {
openNotificationMenu();
}
}
const toggleNotificationMenu = ( id: number ) => {
notifiers.get( id )?.toggleNotificationMenu();
// ───────────────────────────────────────────────────────────────────
/**
* Delete all notifications
*/
const clearAllNotifications = () => {
Notifications = [];
ShownNotifications = [];
ShownNotificationsCount.set( 0 );
}
const clearAllNotifications = ( id: number ) => {
notifiers.get( id )?.clearAllNotifications();
// ───────────────────────────────────────────────────────────────────
/**
* Delete the newest notifications
*/
const clearNewestNotifications = () => {
ShownNotifications.splice( 0, 1 );
Notifications.splice( Notifications.length - 1, 1 );
}
const clearNewestNotifications = ( id: number ) => {
notifiers.get( id )?.clearNewestNotification();
}
const startNotificationHandler = (id: number, gdkmonitor: Gdk.Monitor) => {
// ───────────────────────────────────────────────────────────────────
// ╭───────────────────────────────────────────────╮
// │ User Interface │
// ╰───────────────────────────────────────────────╯
// ───────────────────────────────────────────────────────────────────
const startNotificationHandler = (gdkmonitor: Gdk.Monitor) => {
const { TOP, RIGHT } = Astal.WindowAnchor
const notifier: Notifier = new Notifier( id );
notifiers.set( id, notifier );
hookToNotificationDaemon();
const range = (n: number) => [...Array(n).keys()];
return <window
className="NotificationHandler"
cssClasses={["NotificationHandler"]}
gdkmonitor={gdkmonitor}
exclusivity={Astal.Exclusivity.EXCLUSIVE}
anchor={TOP | RIGHT}>
<box vertical noImplicitDestroy>
{bind(notifier)}
{ShownNotificationsCount( n => range( n ).map( i => {
print( 'Rendering' );
// i is index in ShownNotifications array
return <Notification id={i} delete={deleteNotification} notification={Notifications[ i ].notification}></Notification>
} ) ) }
</box>
</window>
}
const cliHandler = ( args: string[] ): string => {
if ( args[ 1 ] == 'show' ) {
openNotificationMenu( 0 );
openNotificationMenu();
return 'Showing all open notifications';
} else if ( args[ 1 ] == 'hide' ) {
closeNotificationMenu( 0 );
closeNotificationMenu();
return 'Hid all notifications';
} else if ( args[ 1 ] == 'clear' ) {
clearAllNotifications( 0 );
clearAllNotifications();
return 'Cleared all notifications';
} else if ( args[ 1 ] == 'clear-newest' ) {
clearNewestNotifications( 0 );
clearNewestNotifications();
return 'Cleared newest notification';
} else if ( args[ 1 ] == 'toggle' ) {
toggleNotificationMenu( 0 );
toggleNotificationMenu();
return 'Toggled notifications';
} else {
return 'Unknown command!';