chore: migrate widgets to ags v3 and gtk4

This commit is contained in:
retrozinndev
2025-07-06 19:56:44 -03:00
parent 9db1d6fc12
commit b90a799a89
16 changed files with 650 additions and 711 deletions
-46
View File
@@ -1,46 +0,0 @@
import { Binding } from "astal";
import { Widget } from "astal/gtk3";
import { tr } from "../i18n/intl";
import { CustomDialog, CustomDialogProps } from "./CustomDialog";
export type AskPopupProps = {
title?: string | Binding<string | undefined>;
text: string | Binding<string | undefined>;
cancelText?: string;
acceptText?: string;
onAccept?: () => void;
onCancel?: () => void;
};
/**
* A Popup Widget that asks yes or no to a defined promt.
* Runs onAccept() when user accepts, or else onDecline() when
* user doesn't accept / closes window.
* This window isn't usually registered in this shell windowing
* system.
*/
export function AskPopup(props: AskPopupProps): Widget.Window {
let accepted: boolean = false;
const window = CustomDialog({
namespace: "ask-popup",
widthRequest: 400,
heightRequest: 250,
title: props.title ?? tr("ask_popup.title"),
text: props.text,
onFinish: () => !accepted && props.onCancel?.(),
options: [
{ text: props.cancelText ?? tr("cancel") },
{
text: props.acceptText ?? tr("accept"),
onClick: () => {
accepted = true;
props.onAccept?.();
}
}
]
} as CustomDialogProps);
return window;
}
+43
View File
@@ -0,0 +1,43 @@
import { Accessor } from "ags";
import { tr } from "../i18n/intl";
import { CustomDialog } from "./CustomDialog";
import { Astal } from "ags/gtk4";
export type AskPopupProps = {
title?: string | Accessor<string>;
text: string | Accessor<string>;
cancelText?: string;
acceptText?: string;
onAccept?: () => void;
onCancel?: () => void;
};
/**
* A Popup Widget that asks yes or no to a defined promt.
* Runs onAccept() when user accepts, or else onDecline() when
* user doesn't accept / closes window.
* This window isn't usually registered in this shell windowing
* system.
*/
export function AskPopup(props: AskPopupProps): Astal.Window {
let accepted: boolean = false;
return <CustomDialog
namespace={"ask-popup"}
widthRequest={400}
heightRequest={250}
title={props.title ?? tr("ask_popup.title")}
text={props.text}
onFinish={() => !accepted && props.onCancel?.()}
options={[
{ text: props.cancelText ?? tr("cancel") },
{
text: props.acceptText ?? tr("accept"),
onClick: () => {
accepted = true;
props.onAccept?.();
}
}
]} /> as Astal.Window;
}
-60
View File
@@ -1,60 +0,0 @@
import { Binding } from "astal";
import { Astal, Gdk, Widget } from "astal/gtk3";
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
export type BackgroundWindowProps = {
/** GtkWindow Layer */
layer?: Astal.Layer | Binding<Astal.Layer | undefined>;
/** Monitor number where the window should open */
monitor: number | Binding<number | undefined>;
/** Custom stylesheet used in the window. default: `background: rgba(0, 0, 0, .2)` */
css?: string | Binding<string | undefined>;
/** Function that is called when the user triggers a mouse-click or escape action on the window */
onAction?: (window: Widget.Window) => void;
/** Function that is called when the user clicks on the window with primary mouse button */
onClickPrimary?: (window: Widget.Window) => void;
/** Function that is called when the user clicks on the window with secodary mouse button */
onClickSecondary?: (window: Widget.Window) => void;
keymode?: Astal.Keymode;
exclusivity?: Astal.Exclusivity;
};
/** Creates a fullscreen GtkWindow that is used for making
* the user focus on the content after this window(e.g.: AskPopup,
* Authentication Window(futurely) or any PopupWindow)
*
* @param props Properties for background-window
*
* @returns The generated background window
*/
export function BackgroundWindow(props: BackgroundWindowProps) {
return new Widget.Window({
namespace: "background-window",
css: props.css ?? "background: rgba(0, 0, 0, .2);",
monitor: props.monitor,
layer: props.layer ?? Astal.Layer.OVERLAY,
anchor: TOP | LEFT | BOTTOM | RIGHT,
keymode: props.keymode,
exclusivity: props.exclusivity ?? Astal.Exclusivity.IGNORE,
onKeyPressEvent: (self, event: Gdk.Event) => {
event.get_keyval()[1] === Gdk.KEY_Escape &&
props.onAction?.(self);
},
onButtonPressEvent: (self, event: Gdk.Event) => {
if(event.get_button()[1]) {
props.onAction?.(self);
return;
}
if(event.get_button()[1] === Gdk.BUTTON_PRIMARY) {
props.onClickPrimary?.(self);
return;
}
if(event.get_button()[1] === Gdk.BUTTON_SECONDARY)
props.onClickSecondary?.(self);
}
} as Widget.WindowProps);
}
+80
View File
@@ -0,0 +1,80 @@
import { Accessor } from "ags";
import { Astal, Gdk, Gtk } from "ags/gtk4";
import GObject from "gi://GObject?version=2.0";
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
export type BackgroundWindowProps = {
/** GtkWindow Layer */
layer?: Astal.Layer | Accessor<Astal.Layer>;
/** Monitor number where the window should open */
monitor: number | Accessor<number>;
/** Custom stylesheet used in the window. default: `background: rgba(0, 0, 0, .2)` */
css?: string | Accessor<string>;
/* Function that is called when the user releases a key in the keyboard on the window
* The `Escape` key is not passed to this function */
actionKeyPressed?: (window: Astal.Window, keyval: number, keycode: number) => void;
/** Function that is called when the user triggers a mouse-click or escape action on the window */
actionFired?: (window: Astal.Window) => void;
/** Function that is called when the user clicks on the window with primary mouse button */
actionClickPrimary?: (window: Astal.Window) => void;
/** Function that is called when the user clicks on the window with secodary mouse button */
actionClickSecondary?: (window: Astal.Window) => void;
keymode?: Astal.Keymode;
exclusivity?: Astal.Exclusivity;
};
/** Creates a fullscreen GtkWindow that is used for making
* the user focus on the content after this window(e.g.: AskPopup,
* Authentication Window(futurely) or any PopupWindow)
*
* @param props Properties for background-window
*
* @returns The generated background window
*/
export function BackgroundWindow(props: BackgroundWindowProps): Astal.Window {
const conns: Map<GObject.Object, number> = new Map();
return <Astal.Window namespace={"background-window"}
css={props.css ?? "background: rgba(0, 0, 0, .2);"}
monitor={props.monitor}
layer={props.layer ?? Astal.Layer.OVERLAY}
anchor={TOP | LEFT | BOTTOM | RIGHT}
keymode={props.keymode}
exclusivity={props.exclusivity ?? Astal.Exclusivity.IGNORE}
onDestroy={(_) => conns.forEach((id, obj) => obj.disconnect(id))}
$={(self) => {
const gestureClick = Gtk.GestureClick.new(),
eventControllerKey = Gtk.EventControllerKey.new();
self.add_controller(gestureClick);
self.add_controller(eventControllerKey);
conns.set(eventControllerKey, eventControllerKey.connect("key-released",
(_, keyval, keycode) => {
if(keyval === Gdk.KEY_Escape) {
props.actionFired?.(self);
return;
}
props.actionKeyPressed?.(self, keyval, keycode);
}
));
conns.set(gestureClick, gestureClick.connect("released", (gesture) => {
if(gesture.get_current_button() === Gdk.BUTTON_PRIMARY) {
props.actionClickPrimary?.(self);
return;
}
if(gesture.get_current_button() === Gdk.BUTTON_SECONDARY) {
props.actionClickSecondary?.(self);
return;
}
props.actionFired?.(self);
}));
}}
/> as Astal.Window;
}
-109
View File
@@ -1,109 +0,0 @@
import { Binding } from "astal";
import { Astal, Gtk, Widget } from "astal/gtk3";
import { Windows } from "../windows";
import { PopupWindow, PopupWindowProps } from "./PopupWindow";
import { Separator } from "./Separator";
import { tr } from "../i18n/intl";
export type CustomDialogProps = {
namespace?: string | Binding<string>;
className?: string | Binding<string>;
cssBackground?: string;
title?: string | Binding<string>;
text?: string | Binding<string>;
heightRequest?: number | Binding<number>;
widthRequest?: number | Binding<number>;
childOrientation?: Gtk.Orientation | Binding<Gtk.Orientation>;
children?: Array<Gtk.Widget> | Binding<Array<Gtk.Widget>>;
child?: Gtk.Widget | Binding<Gtk.Widget>;
onFinish?: () => void;
options?: Array<CustomDialogOption>;
optionsOrientation?: Gtk.Orientation | Binding<Gtk.Orientation>;
};
export interface CustomDialogOption {
onClick?: () => void;
text: string | Binding<string>;
closeOnClick?: boolean | Binding<boolean>;
}
export function CustomDialog(props: CustomDialogProps = {
options: [{ text: tr("accept") }]
}): Widget.Window {
const window = Windows.createWindowForFocusedMonitor((mon: number) => PopupWindow({
namespace: props.namespace ?? "custom-dialog",
monitor: mon,
cssBackgroundWindow: props.cssBackground ?? "background: rgba(0, 0, 0, .3);",
exclusivity: Astal.Exclusivity.IGNORE,
layer: Astal.Layer.OVERLAY,
halign: Gtk.Align.CENTER,
valign: Gtk.Align.CENTER,
widthRequest: props.widthRequest ?? 400,
heightRequest: props.heightRequest ?? 220,
onDestroy: props.onFinish,
child: new Widget.Box({
className: props.className ?? "custom-dialog-container",
orientation: Gtk.Orientation.VERTICAL,
children: [
new Widget.Label({
className: "title",
visible: props.title,
label: props.title
} as Widget.LabelProps),
new Widget.Label({
className: "text",
visible: props.text,
label: props.text,
yalign: 0,
expand: true
} as Widget.LabelProps),
new Widget.Box({
className: "custom-children custom-child",
visible: props.children || props.child,
orientation: props.childOrientation ?? Gtk.Orientation.VERTICAL,
children: props.children,
child: props.child
} as Widget.BoxProps),
Separator({
alpha: .2,
visible: props.options && props.options.length > 0,
spacing: 8,
orientation: Gtk.Orientation.VERTICAL
}),
new Widget.Box({
className: "options",
orientation: props.optionsOrientation ?? Gtk.Orientation.HORIZONTAL,
hexpand: true,
heightRequest: 38,
homogeneous: true,
children: props.options && props.options.map(option => {
const onClick = () => {
option.onClick?.();
(option.closeOnClick ?? true) &&
window.close();
};
const connections: Array<number> = [];
const btn = new Widget.Button({
className: "option",
label: option.text,
hexpand: true,
setup: (self) => {
connections.push(
self.connect("click-release", (_, event: Astal.ClickEvent) =>
event.button === Astal.MouseButton.PRIMARY &&
onClick()),
self.connect("activate", (_) => onClick())
);
},
onDestroy: (self) => connections.map(id => self.disconnect(id))
} as Widget.ButtonProps);
return btn;
})
} as Widget.BoxProps)
]
} as Widget.BoxProps)
} as PopupWindowProps))();
return window;
}
+88
View File
@@ -0,0 +1,88 @@
import { Astal, Gtk } from "ags/gtk4";
import { Windows } from "../windows";
import { PopupWindow } from "./PopupWindow";
import { Separator } from "./Separator";
import { tr } from "../i18n/intl";
import { Accessor, For, With } from "ags";
import { variableToBoolean, WidgetNodeType } from "../scripts/utils";
export type CustomDialogProps = {
namespace?: string | Accessor<string>;
className?: string | Accessor<string>;
cssBackground?: string;
title?: string | Accessor<string>;
text?: string | Accessor<string>;
heightRequest?: number | Accessor<number>;
widthRequest?: number | Accessor<number>;
childOrientation?: Gtk.Orientation | Accessor<Gtk.Orientation>;
children?: WidgetNodeType;
onFinish?: () => void;
options?: Array<CustomDialogOption> | Accessor<Array<CustomDialogOption>>;
optionsOrientation?: Gtk.Orientation | Accessor<Gtk.Orientation>;
};
export interface CustomDialogOption {
onClick?: () => void;
text: string | Accessor<string>;
closeOnClick?: boolean | Accessor<boolean>;
}
function CustomDialogOption(props: CustomDialogOption & { dialog: Astal.Window }) {
function onClicked() {
props.onClick?.();
props.closeOnClick && props.dialog.close();
}
return <Gtk.Button class="option" hexpand={true} label={props.text}
onClicked={onClicked} onActivate={onClicked}/>
}
export function CustomDialog({ options = [{ text: tr("accept") }], ...props}: CustomDialogProps) {
let dialog: Astal.Window;
return Windows.getDefault().createWindowForFocusedMonitor((mon: number) =>
<PopupWindow namespace={props.namespace ?? "custom-dialog"} monitor={mon}
cssBackgroundWindow={props.cssBackground ?? "background: rgba(0, 0, 0, .3);"}
exclusivity={Astal.Exclusivity.IGNORE} layer={Astal.Layer.OVERLAY}
halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}
widthRequest={props.widthRequest ?? 400} heightRequest={props.heightRequest ?? 220}
onDestroy={props.onFinish} $={(self) => dialog = self}>
<Gtk.Box class={props.className ?? "custom-dialog-container"}
orientation={Gtk.Orientation.VERTICAL}>
<Gtk.Label class={"title"} visible={variableToBoolean(props.title)} label={props.title} />
<Gtk.Label class={"text"} visible={variableToBoolean(props.text)} label={props.text} />
<Gtk.Box class={"custom-children custom-child"} visible={variableToBoolean(props.children)}
orientation={props.childOrientation ?? Gtk.Orientation.VERTICAL}>
{
(props.children instanceof Accessor) ?
(Array.isArray(props.children) ?
<For each={props.children! as Accessor<Array<JSX.Element>>}>
{(widget) => widget && widget}
</For>
: <With value={props.children as Accessor<JSX.Element>}>
{(widget) => widget && widget}
</With>)
: (Array.isArray(props.children) ?
props.children.map(widget => widget && widget).filter(w => w)
: props.children)
}
</Gtk.Box>
<Separator alpha={.2} visible={options && options.length > 0}
spacing={8} orientation={Gtk.Orientation.VERTICAL} />
{(<Gtk.Box class={"options"} orientation={props.optionsOrientation ?? Gtk.Orientation.HORIZONTAL}
hexpand={true} heightRequest={38} homogeneous={true}>
{
(options instanceof Accessor) ?
<For each={options}>
{(option) => <CustomDialogOption {...option} dialog={dialog} />}
</For>
: options.map(option =>
<CustomDialogOption {...option} dialog={dialog} />)
}
</Gtk.Box>)}
</Gtk.Box>
</PopupWindow>)();
}
-69
View File
@@ -1,69 +0,0 @@
import { Binding } from "astal";
import { Widget } from "astal/gtk3";
import { tr } from "../i18n/intl";
import { CustomDialog, CustomDialogProps } from "./CustomDialog";
export type EntryPopupProps = {
title: string | Binding<string>;
text?: string | Binding<string>;
cancelText?: string | Binding<string>;
acceptText?: string | Binding<string>;
closeOnAccept?: boolean;
entryPlaceholder?: string | Binding<string>;
onAccept: (userInput: string) => void;
onCancel?: () => void;
onFinish?: () => void;
isPassword?: boolean | Binding<string>;
};
export function EntryPopup(props: EntryPopupProps): Widget.Window {
props.closeOnAccept = props.closeOnAccept ?? true;
const entry = new Widget.Entry({
className: props.isPassword && "password",
visibility: (props.isPassword instanceof Binding) ?
props.isPassword.as(isPasswd => !isPasswd)
: !props.isPassword,
invisibleChar: 0x00B7, // set '·' as the invisible char
xalign: .5,
placeholderText: props.entryPlaceholder,
onActivate: (self) => {
props.closeOnAccept && window.close();
entered = true;
props.onAccept(self.text);
self.text = "";
},
} as Widget.EntryProps);
let entered: boolean = false;
const window = CustomDialog({
namespace: "entry-popup",
widthRequest: 420,
heightRequest: 220,
title: props.title,
text: props.text,
child: entry,
options: [
{
text: props.cancelText ?? tr("cancel"),
onClick: props.onCancel
},
{
text: props.acceptText ?? tr("accept"),
closeOnClick: props.closeOnAccept,
onClick: () => {
entered = true;
props.onAccept(entry.text);
entry.text = "";
}
}
],
onFinish: () => {
!entered && props.onCancel?.()
props.onFinish?.();
}
} as CustomDialogProps);
return window;
}
+61
View File
@@ -0,0 +1,61 @@
import { Accessor } from "ags";
import { tr } from "../i18n/intl";
import { CustomDialog } from "./CustomDialog";
import { Astal, Gtk } from "ags/gtk4";
export type EntryPopupProps = {
title: string | Accessor<string>;
text?: string | Accessor<string>;
cancelText?: string | Accessor<string>;
acceptText?: string | Accessor<string>;
closeOnAccept?: boolean;
entryPlaceholder?: string | Accessor<string>;
onAccept: (userInput: string) => void;
onCancel?: () => void;
onFinish?: () => void;
isPassword?: boolean | Accessor<string>;
};
export function EntryPopup(props: EntryPopupProps): Astal.Window {
props.closeOnAccept = props.closeOnAccept ?? true;
let entered: boolean = false;
function onActivate(entry: Gtk.Entry|Gtk.PasswordEntry) {
props.closeOnAccept && window.close();
entered = true;
props.onAccept(entry.text);
entry.text = "";
}
const entry = props.isPassword ?
<Gtk.PasswordEntry class={"password"} xalign={.5}
placeholderText={props.entryPlaceholder}
onActivate={onActivate}
/> as Gtk.PasswordEntry
: <Gtk.Entry xalign={.5} placeholderText={props.entryPlaceholder}
onActivate={onActivate} /> as Gtk.Entry;
const window = <CustomDialog namespace={"entry-popup"} widthRequest={420}
heightRequest={220} title={props.title} text={props.text}
options={[
{
text: props.cancelText ?? tr("cancel"),
onClick: props.onCancel
},
{
text: props.acceptText ?? tr("accept"),
closeOnClick: props.closeOnAccept,
onClick: () => {
entered = true;
props.onAccept(entry.text);
entry.text = "";
}
}
]} onFinish={() => {
!entered && props.onCancel?.()
props.onFinish?.();
}}
/> as Astal.Window;
return window;
}
-170
View File
@@ -1,170 +0,0 @@
import { Astal, Gtk, Widget } from "astal/gtk3";
import AstalNotifd from "gi://AstalNotifd";
import { Separator } from "./Separator";
import { HistoryNotification, Notifications } from "../scripts/notifications";
import { GLib } from "astal";
import { getAppIcon } from "../scripts/apps";
import Pango from "gi://Pango";
function getNotificationImage(notif: AstalNotifd.Notification|HistoryNotification): (string|undefined) {
const img = notif.image || notif.appIcon;
if(!img || !img.includes('/'))
return undefined;
switch(true) {
case /^[/]/.test(img):
return `file://${img}`;
case /^[~]/.test(img):
case /^file:\/\/[~]/i.test(img):
return `file://${GLib.get_home_dir()}/${img.replace(/^(file\:\/\/|[~]|file\:\/\[~])/i, "")}`;
}
return img;
}
export function NotificationWidget(notification: AstalNotifd.Notification|number|HistoryNotification,
onClose?: (notif: AstalNotifd.Notification|HistoryNotification) => void,
showTime?: boolean /* It's showTime :speaking_head: :boom: :bangbang: */,
holdOnHover?: boolean): Gtk.Widget {
notification = (typeof notification === "number") ?
AstalNotifd.get_default().get_notification(notification)
: notification;
return new Widget.EventBox({
onClick: () => {
if(notification instanceof AstalNotifd.Notification) {
const viewAction = notification.actions.filter(action =>
action.label.toLowerCase() === "view")?.[0];
viewAction && notification.invoke(viewAction.id);
}
onClose?.(notification);
},
onHover: () => holdOnHover && Notifications.getDefault().holdNotification(notification.id),
onHoverLost: () => holdOnHover && onClose?.(notification),
hexpand: true,
vexpand: false,
child: new Widget.Box({
className: `notification ${ (notification instanceof AstalNotifd.Notification) ?
Notifications.getDefault().getUrgencyString(notification.urgency) : "" }`,
homogeneous: false,
expand: true,
orientation: Gtk.Orientation.VERTICAL,
spacing: 5,
children: [
new Widget.Box({
className: "top",
orientation: Gtk.Orientation.HORIZONTAL,
hexpand: true,
vexpand: false,
children: [
new Widget.Icon({
className: "icon app-icon",
icon: notification.appIcon && Astal.Icon.lookup_icon(notification.appIcon) ?
notification.appIcon
: getAppIcon(notification.appName),
setup: (self) => self.set_visible(Boolean(self.get_icon())),
halign: Gtk.Align.START,
css: "font-size: 16px;"
}),
new Widget.Label({
className: "app-name",
halign: Gtk.Align.START,
hexpand: true,
label: notification.appName || "Unknown Application"
} as Widget.LabelProps),
new Widget.Box({
halign: Gtk.Align.END,
children: [
new Widget.Label({
xalign: 1,
visible: !showTime ? false : true,
className: "time",
label: GLib.DateTime.new_from_unix_local(notification.time).format("%H:%M"),
} as Widget.LabelProps),
new Widget.Button({
className: "close",
onClick: () => onClose && onClose(notification),
image: new Widget.Icon({
className: "close icon",
icon: "window-close-symbolic"
} as Widget.IconProps)
} as Widget.ButtonProps)
]
} as Widget.BoxProps)
]
} as Widget.BoxProps),
Separator({
orientation: Gtk.Orientation.VERTICAL,
alpha: 10
}),
new Widget.Box({
className: "content",
orientation: Gtk.Orientation.HORIZONTAL,
children: [
new Widget.Box({
className: "image",
setup: (box) => {
const img = getNotificationImage(notification);
box.set_visible(Boolean(img));
img && box.set_css(`background-image: image(url("${img}"))`);
}
} as Widget.BoxProps),
new Widget.Box({
className: "text",
orientation: Gtk.Orientation.VERTICAL,
expand: true,
children: [
new Widget.Label({
className: "summary",
useMarkup: true,
xalign: 0,
truncate: true,
label: notification.summary.replace(/\&/g, "&amp;")
}),
new Widget.Label({
className: "body",
useMarkup: true,
halign: Gtk.Align.START,
xalign: 0,
truncate: false,
wrap: true,
singleLineMode: false,
wrapMode: Pango.WrapMode.WORD_CHAR,
label: notification.body.replace(/&/g, "&amp;")
} as Widget.LabelProps)
]
} as Widget.BoxProps)
]
} as Widget.BoxProps),
new Widget.Box({
className: "actions button-row",
hexpand: true,
visible: (notification instanceof AstalNotifd.Notification) ?
(notification.actions.filter(action => action.label.toLowerCase() !== "view").length > 0)
: false,
children: (notification instanceof AstalNotifd.Notification) ?
notification.actions.filter(action => action.label.toLowerCase() !== "view")
.map((action: AstalNotifd.Action) =>
new Widget.Button({
className: "action",
label: action.label,
hexpand: true,
onClicked: () => {
notification.invoke(action.id);
onClose && onClose(notification);
}
} as Widget.ButtonProps)
)
: []
} as Widget.BoxProps)
]
} as Widget.BoxProps),
} as Widget.EventBoxProps);
}
+129
View File
@@ -0,0 +1,129 @@
import { Gdk, Gtk } from "ags/gtk4";
import { Separator } from "./Separator";
import { HistoryNotification, Notifications } from "../scripts/notifications";
import { getAppIcon, getSymbolicIcon } from "../scripts/apps";
import AstalNotifd from "gi://AstalNotifd";
import Pango from "gi://Pango?version=1.0";
import GLib from "gi://GLib?version=2.0";
import GObject from "gi://GObject?version=2.0";
function getNotificationImage(notif: AstalNotifd.Notification|HistoryNotification): (string|undefined) {
const img = notif.image || notif.appIcon;
if(!img || !img.includes('/'))
return undefined;
switch(true) {
case /^[/]/.test(img):
return `file://${img}`;
case /^[~]/.test(img):
case /^file:\/\/[~]/i.test(img):
return `file://${GLib.get_home_dir()}/${img.replace(/^(file\:\/\/|[~]|file\:\/\[~])/i, "")}`;
}
return img;
}
export function NotificationWidget({ notification, actionClicked, holdOnHover, showTime, actionClose }: {
notification: AstalNotifd.Notification|number|HistoryNotification;
actionClicked?: (notif: AstalNotifd.Notification|HistoryNotification) => void;
actionClose?: (notif: AstalNotifd.Notification|HistoryNotification) => void;
holdOnHover?: boolean;
showTime?: boolean; // It's showTime :speaking_head: :boom: :bangbang:
}): Gtk.Widget {
notification = (typeof notification === "number") ?
AstalNotifd.get_default().get_notification(notification)
: notification;
const conns: Map<GObject.Object, Array<number>> = new Map();
return <Gtk.Box hexpand={true} vexpand={true} class={`notification ${
Notifications.getDefault().getUrgencyString(notification.urgency)
}`} orientation={Gtk.Orientation.VERTICAL} spacing={5}
$={(self) => {
const eventControllerMotion = Gtk.EventControllerMotion.new(),
gestureClick = Gtk.GestureClick.new();
self.add_controller(eventControllerMotion);
self.add_controller(gestureClick);
conns.set(eventControllerMotion, [
eventControllerMotion.connect("enter", () =>
holdOnHover && Notifications.getDefault().holdNotification(notification.id)),
eventControllerMotion.connect("leave", () =>
holdOnHover && Notifications.getDefault().removeNotification(notification.id))
]);
conns.set(gestureClick, [
gestureClick.connect("released", (gesture) => {
gesture.get_current_button() === Gdk.BUTTON_PRIMARY &&
actionClicked?.(notification);
})
]);
}} onDestroy={(_) => {
conns.forEach((ids, obj) => ids.forEach(id => obj.disconnect(id)));
}}>
<Gtk.Box class={"top"} hexpand={true}>
<Gtk.Image css={"font-size: 16px;"} $={(self) => {
const icon = getSymbolicIcon(notification.appIcon ?? notification.appName) ??
getSymbolicIcon(notification.appName) ?? getAppIcon(notification.appName);
if(icon) {
self.set_from_icon_name(icon);
return;
}
self.set_visible(false);
}} />
<Gtk.Label class={"app-name"} halign={Gtk.Align.START} hexpand={true}
label={notification.appName || "Application"} />
<Gtk.Label class={"time"} visible={showTime} xalign={1}
label={GLib.DateTime.new_from_unix_local(notification.time).format("%H:%M") ?? ""} />
<Gtk.Button halign={Gtk.Align.END} iconName={"window-close-symbolic"}
class={"close icon"}/>
</Gtk.Box>
<Separator alpha={.1} orientation={Gtk.Orientation.VERTICAL} />
<Gtk.Box class={"content"} $={(self) => {
const image = getNotificationImage(notification);
image &&
self.prepend(Gtk.Picture.new_for_filename(image));
}}>
<Gtk.Box class={"text"} orientation={Gtk.Orientation.VERTICAL}
vexpand={true}>
<Gtk.Label class={"summary"} useMarkup={true} xalign={0}
ellipsize={Pango.EllipsizeMode.END} label={
notification.summary.replace(/[&]/g, "&amp;")
} />
<Gtk.Label class={"body"} useMarkup={true} xalign={0} wrap={true}
wrapMode={Pango.WrapMode.WORD_CHAR} singleLineMode={false}
label={notification.body.replace(/[&]/g, "&amp;")} />
</Gtk.Box>
</Gtk.Box>
<Gtk.Box class={"action button-row"} hexpand={true} visible={
(notification instanceof AstalNotifd.Notification) &&
(notification.actions.filter(action => action.label.toLowerCase() !== "view").length > 0)
}>
{
(notification instanceof AstalNotifd.Notification) &&
notification.actions.filter(a => a.label.toLowerCase() !== "view").map(action =>
<Gtk.Button class={"action"} label={action.label}
hexpand={true} onClicked={(_) => {
notification.invoke(action.id);
actionClose?.(notification);
}}
/>)
}
</Gtk.Box>
</Gtk.Box> as Gtk.Widget;
}
-122
View File
@@ -1,122 +0,0 @@
import { Binding } from "astal";
import { Astal, Gdk, Gtk, Widget } from "astal/gtk3";
import { BackgroundWindow } from "./BackgroundWindow";
type PopupWindowSpecificProps = {
onDestroy?: (self: Widget.Window) => void;
onKeyPressEvent?: (self: Widget.Window, event: Gdk.Event) => void;
onButtonPressEvent?: (self: Gtk.Widget, event: Gdk.Event) => void;
/** Stylesheet for the background of the popup-window */
cssBackgroundWindow?: string;
onClickedOutside?: (self: Widget.Window) => void;
};
export type PopupWindowProps = Pick<Widget.WindowProps,
"child"
| "monitor"
| "css"
| "layer"
| "exclusivity"
| "marginLeft"
| "marginTop"
| "marginRight"
| "marginBottom"
| "expand"
| "cursor"
| "canFocus"
| "hasFocus"
| "tooltipMarkup"
| "namespace"
| "widthRequest"
| "heightRequest"
| "halign"
| "valign"
| "vexpand"
| "hexpand"> & PopupWindowSpecificProps;
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
export function PopupWindow(props: PopupWindowProps): Widget.Window {
props.layer = props.layer ?? Astal.Layer.OVERLAY;
const bgWindow = props.cssBackgroundWindow ? BackgroundWindow({
monitor: props.monitor ?? 0,
layer: props.layer,
css: props.cssBackgroundWindow,
}) : undefined;
const winProps: Widget.WindowProps = {};
for(const key of Object.keys(props).filter(k => k !== "onClickedOutside")) {
// @ts-ignore ignore the `onClickedOutside()` method because astal thinks it's a signal
winProps[key as keyof typeof winProps] = props[key as keyof typeof props];
}
return new Widget.Window({
...winProps,
namespace: props?.namespace ?? "popup-window",
className: `popup-window ${(props.namespace instanceof Binding ?
props.namespace.get() : props.namespace) || ""}`,
keymode: Astal.Keymode.EXCLUSIVE,
anchor: TOP | LEFT | RIGHT | BOTTOM,
exclusivity: props.exclusivity ?? Astal.Exclusivity.NORMAL,
halign: undefined,
valign: undefined,
focusOnMap: true,
widthRequest: undefined,
heightRequest: undefined,
marginTop: undefined,
marginBottom: undefined,
marginLeft: undefined,
marginRight: undefined,
onDestroy: (self) => {
bgWindow?.close();
props.onDestroy?.(self);
},
onButtonPressEvent: (self, event) => {
if((event.get_button()[1] === Gdk.BUTTON_PRIMARY ||
event.get_button()[1] === Gdk.BUTTON_SECONDARY)) {
const [ , x, y ] = event.get_coords();
const allocation = (self.get_child()! as Widget.Box).get_child()!.get_allocation();
if((x < allocation.x || x > (allocation.x + allocation.width)) ||
(y < allocation.y || y > (allocation.y + allocation.height))) {
if(!props.onClickedOutside) {
self.close();
return;
}
props.onClickedOutside?.(self);
}
}
},
onKeyPressEvent: (self, event: Gdk.Event) => {
if(event.get_keyval()[1] === Gdk.KEY_Escape) {
self.close();
return;
}
props.onKeyPressEvent?.(self, event);
},
child: new Widget.Box({
expand: props.expand ?? false,
halign: props.halign,
valign: props.valign,
hexpand: true,
css: `box {
margin-left: ${props.marginLeft ?? 0}px;
margin-right: ${props.marginRight ?? 0}px;
margin-top: ${props.marginTop ?? 0}px;
margin-bottom: ${props.marginBottom ?? 0}px;
}`,
child: new Widget.Box({
onButtonPressEvent: props.onButtonPressEvent ?? (() => true),
widthRequest: props.widthRequest,
heightRequest: props.heightRequest,
child: props.child
} as Widget.BoxProps)
} as Widget.BoxProps)
} as Widget.WindowProps);
}
+134
View File
@@ -0,0 +1,134 @@
import { Astal, Gdk, Gtk } from "ags/gtk4";
import { BackgroundWindow } from "./BackgroundWindow";
import GObject from "gi://GObject?version=2.0";
import { Accessor, CCProps, createComputed } from "ags";
import { omitObjectKeys, WidgetNodeType } from "../scripts/utils";
type PopupWindowSpecificProps = {
children?: WidgetNodeType;
onDestroy?: (self: Astal.Window) => void;
/** Stylesheet for the background of the popup-window */
cssBackgroundWindow?: string;
class?: string | Accessor<string>;
actionClickedOutside?: (self: Astal.Window) => void;
actionKeyPressed?: (self: Astal.Window, keyval: number, keycode: number) => void;
};
export type PopupWindowProps = Pick<Partial<CCProps<Astal.Window, Astal.Window.ConstructorProps>>,
"monitor"
| "layer"
| "exclusivity"
| "marginLeft"
| "marginTop"
| "marginRight"
| "marginBottom"
| "cursor"
| "canFocus"
| "hasFocus"
| "tooltipMarkup"
| "namespace"
| "widthRequest"
| "heightRequest"
| "halign"
| "valign"
| "vexpand"
| "hexpand"> & PopupWindowSpecificProps;
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
export function PopupWindow(props: PopupWindowProps): GObject.Object {
props.layer ??= Astal.Layer.OVERLAY;
const bgWindow = props.cssBackgroundWindow ? (<BackgroundWindow
monitor={props.monitor ?? 0}
layer={props.layer}
css={props.cssBackgroundWindow} /> as Astal.Window)
: undefined;
const omittedProps = omitObjectKeys(props, [
"children",
"actionKeyPressed",
"actionClickedOutside",
"cssBackgroundWindow",
"marginTop",
"marginLeft",
"marginRight",
"marginBottom"
]);
return <Astal.Window {...omittedProps}
namespace={props.namespace ?? "popup-window"} class={
(props.class instanceof Accessor) ?
((props.namespace instanceof Accessor) ?
createComputed([props.class, props.namespace], (clss, namespace) =>
`popup-window ${clss} ${namespace}`)
: props.class.as(clss => `popup-window ${clss} ${props.namespace ?? ""}`))
: `popup-window ${props.class ?? ""} ${props.namespace ?? ""}`
} keymode={Astal.Keymode.EXCLUSIVE} anchor={TOP | LEFT | RIGHT | BOTTOM}
exclusivity={props.exclusivity ?? Astal.Exclusivity.NORMAL}
onDestroy={(self) => {
bgWindow?.close();
props.onDestroy?.(self);
}}
$={(self) => {
props.actionClickedOutside ??= self.close;
const conns: Map<GObject.Object, number> = new Map();
const gestureClick = Gtk.GestureClick.new();
const keyController = Gtk.EventControllerKey.new();
const allocation = (self.get_child()! as Gtk.Box).get_first_child()!.get_allocation();
self.add_controller(gestureClick);
self.add_controller(keyController);
conns.set(gestureClick, gestureClick.connect("released", (_, __, x, y) => {
if((x < allocation.x || x > (allocation.x + allocation.width)) ||
(y < allocation.y || y > (allocation.y + allocation.height))) {
// Disconnect signals because window is being closed
conns.forEach((id, obj) => {
obj.disconnect(id);
});
props.actionClickedOutside!(self);
}
}));
conns.set(keyController, keyController.connect("key-pressed", (_, keyval, keycode) => {
if(keyval === Gdk.KEY_Escape) {
conns.forEach((id, obj) => {
obj.disconnect(id);
});
props.actionClickedOutside!(self);
return;
}
props.actionKeyPressed?.(self, keyval, keycode);
}));
conns.set(self, self.connect("destroy", () => conns.forEach((id, obj) =>
obj.disconnect(id))));
}}>
<Gtk.Box
halign={props.halign}
valign={props.valign}
hexpand vexpand
css={`box {
margin-left: ${props.marginLeft ?? 0}px;
margin-right: ${props.marginRight ?? 0}px;
margin-top: ${props.marginTop ?? 0}px;
margin-bottom: ${props.marginBottom ?? 0}px;
}`}>
<Gtk.Box widthRequest={props.widthRequest} heightRequest={props.heightRequest}
$={(self) => {
const gestureClick = Gtk.GestureClick.new();
self.add_controller(gestureClick);
}}>
{props.children}
</Gtk.Box>
</Gtk.Box>
</Astal.Window>;
}
-57
View File
@@ -1,57 +0,0 @@
import { Binding } from "astal";
import { Gtk, Widget } from "astal/gtk3";
export interface SeparatorProps {
class?: string;
alpha?: number;
cssColor?: string;
orientation?: Gtk.Orientation;
size?: number;
spacing?: number;
margin?: number;
visible?: boolean | Binding<boolean>;
}
export function Separator(props: SeparatorProps = {
orientation: Gtk.Orientation.HORIZONTAL
}) {
props.alpha = props.alpha ?
(props.alpha > 1 ?
props.alpha / 100
: props.alpha)
: 1;
props.orientation = props.orientation ?? Gtk.Orientation.HORIZONTAL;
return new Widget.Box({
name: "separator",
...(props.orientation === Gtk.Orientation.HORIZONTAL ?
{ vexpand: true } : { hexpand: true }),
className: `separator ${ props.orientation === Gtk.Orientation.VERTICAL ?
"vertical" : "horizontal" }`,
visible: props.visible,
css: `.vertical {
padding: ${props.spacing ?? 0}px ${props.margin ?? 7}px;
}
.horizontal {
padding: ${props.margin ?? 4}px ${props.spacing ?? 0}px;
}`,
child: new Widget.Box({
className: `${ props.orientation === Gtk.Orientation.VERTICAL ?
"vertical" : "horizontal" } ${ props.class ? props.class : "" }`,
...(props.orientation === Gtk.Orientation.HORIZONTAL ?
{ vexpand: true } : { hexpand: true }),
css: `* {
background: ${ props.cssColor ?? "lightgray" };
opacity: ${props.alpha};
}
.horizontal {
min-width: ${ props.size ?? 1 }px;
}
.vertical {
min-height: ${ props.size ?? 1 }px;
}`
} as Widget.BoxProps)
} as Widget.BoxProps);
}
+48
View File
@@ -0,0 +1,48 @@
import { Accessor } from "ags";
import { Gtk } from "ags/gtk4";
export interface SeparatorProps {
class?: string;
alpha?: number;
cssColor?: string;
orientation?: Gtk.Orientation;
size?: number;
spacing?: number;
margin?: number;
visible?: boolean | Accessor<boolean>;
}
export function Separator(props: SeparatorProps = {
orientation: Gtk.Orientation.HORIZONTAL
}) {
props.alpha = props.alpha ?
(props.alpha > 1 ?
props.alpha / 100
: props.alpha)
: 1;
props.orientation = props.orientation ?? Gtk.Orientation.HORIZONTAL;
return <Gtk.Box name={"separator"} vexpand={props.orientation === Gtk.Orientation.HORIZONTAL}
hexpand={props.orientation === Gtk.Orientation.VERTICAL}
class={`separator ${ props.orientation === Gtk.Orientation.VERTICAL ?
"vertical" : "horizontal" }`} visible={props.visible}
css={`.vertical { padding: {${props.spacing ?? 0}px ${props.margin ?? 7}px; }
.horizontal { padding: {${props.margin ?? 4}px ${props.spacing ?? 0}px; }`}>
<Gtk.Box class={`${props.orientation === Gtk.Orientation.VERTICAL ?
"vertical"
: "horizontal"} ${props.class ?? ""}`}
vexpand={props.orientation === Gtk.Orientation.HORIZONTAL}
hexpand={props.orientation === Gtk.Orientation.VERTICAL}
css={`* {
background: ${ props.cssColor ?? "lightgray" };
opacity: ${props.alpha};
}
.horizontal { min-width: ${ props.size ?? 1 }px; }
.vertical { min-height: ${ props.size ?? 1 }px; }`}
/>
</Gtk.Box>
}
-78
View File
@@ -1,78 +0,0 @@
import { Binding, register } from "astal";
import { Gtk, Widget } from "astal/gtk3";
export { ResultWidget, ResultWidgetProps };
type ResultWidgetProps = {
icon?: string | Binding<string> | Gtk.Widget | Binding<Gtk.Widget>;
title: string | Binding<string | undefined>;
description?: string | Binding<string | undefined>;
closeOnClick?: boolean;
setup?: () => void;
onClick?: () => void;
};
@register({ GTypeName: "ResultWidget" })
class ResultWidget extends Widget.Box {
public readonly onClick: (() => void);
public readonly setup: ((() => void)|undefined);
public icon: (string | Binding<string> | Gtk.Widget | Binding<Gtk.Widget> | undefined);
public closeOnClick: boolean = true;
constructor(props: ResultWidgetProps) {
super({
className: "result",
hexpand: true
});
this.icon = props.icon;
this.setup = props.setup;
this.closeOnClick = props.closeOnClick ?? true;
this.onClick = () => props.onClick?.();
if(this.icon !== undefined) {
if(this.icon instanceof Binding) {
if(typeof this.icon.get() === "string") {
this.add(new Widget.Icon({
icon: this.icon as Binding<string>
} as Widget.IconProps));
} else {
this.add(new Widget.Box({
child: this.icon as Binding<Gtk.Widget>
} as Widget.BoxProps));
}
} else {
if(typeof this.icon === "string") {
this.add(new Widget.Icon({
icon: this.icon as string
} as Widget.IconProps));
} else
this.add(this.icon as Gtk.Widget);
}
}
this.add(new Widget.Box({
orientation: Gtk.Orientation.VERTICAL,
valign: Gtk.Align.CENTER,
children: [
new Widget.Label({
className: "title",
xalign: 0,
truncate: true,
label: props.title
} as Widget.LabelProps),
new Widget.Label({
className: "description",
visible: (props.description instanceof Binding) ?
props.description.as(Boolean)
: Boolean(props.description),
truncate: true,
xalign: 0,
label: props.description || ""
} as Widget.LabelProps)
]
} as Widget.BoxProps));
}
}
+67
View File
@@ -0,0 +1,67 @@
import { Accessor, With } from "ags";
import { register } from "ags/gobject";
import { Gtk } from "ags/gtk4";
import Pango from "gi://Pango?version=1.0";
import { variableToBoolean } from "../../scripts/utils";
export { ResultWidget, ResultWidgetProps };
type ResultWidgetProps = {
icon?: string | Accessor<string> | JSX.Element | Accessor<JSX.Element>;
title: string | Accessor<string>;
description?: string | Accessor<string>;
closeOnClick?: boolean;
setup?: () => void;
onClick?: () => void;
};
@register({ GTypeName: "ResultWidget" })
class ResultWidget extends Gtk.Box {
public readonly onClick: () => void;
public readonly setup?: () => void;
public icon?: (string | Accessor<string> | JSX.Element | Accessor<JSX.Element>);
public closeOnClick: boolean = true;
constructor(props: ResultWidgetProps) {
super({
cssClasses: ["result"],
hexpand: true
});
this.icon = props.icon;
this.setup = props.setup;
this.closeOnClick = props.closeOnClick ?? true;
this.onClick = () => props.onClick?.();
if(this.icon !== undefined) {
if(this.icon instanceof Accessor) {
if(typeof this.icon.get() === "string") {
this.prepend(<Gtk.Image iconName={
this.icon as Accessor<string>
} /> as Gtk.Image);
} else {
this.prepend(<Gtk.Box>
<With value={this.icon as Accessor<Gtk.Widget>}>
{(widget) => widget}
</With>
</Gtk.Box> as Gtk.Box);
}
} else {
if(typeof this.icon === "string")
this.prepend(<Gtk.Image iconName={this.icon as string} /> as Gtk.Image);
else
this.prepend(this.icon as Gtk.Widget);
}
}
this.append(<Gtk.Box orientation={Gtk.Orientation.VERTICAL} valign={Gtk.Align.CENTER}>
<Gtk.Label class={"title"} xalign={0} ellipsize={Pango.EllipsizeMode.END}
label={props.title} />
<Gtk.Label class={"description"} visible={variableToBoolean(props.description)}
ellipsize={Pango.EllipsizeMode.END} xalign={0} label={props.description ?? ""} />
</Gtk.Box> as Gtk.Box);
}
}