diff --git a/ags/widget/AskPopup.ts b/ags/widget/AskPopup.ts deleted file mode 100644 index 84c9f21..0000000 --- a/ags/widget/AskPopup.ts +++ /dev/null @@ -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; - text: string | Binding; - 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; -} diff --git a/ags/widget/AskPopup.tsx b/ags/widget/AskPopup.tsx new file mode 100644 index 0000000..bf92b87 --- /dev/null +++ b/ags/widget/AskPopup.tsx @@ -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; + text: string | Accessor; + 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 !accepted && props.onCancel?.()} + options={[ + { text: props.cancelText ?? tr("cancel") }, + { + text: props.acceptText ?? tr("accept"), + onClick: () => { + accepted = true; + props.onAccept?.(); + } + } + ]} /> as Astal.Window; +} diff --git a/ags/widget/BackgroundWindow.ts b/ags/widget/BackgroundWindow.ts deleted file mode 100644 index 6306c75..0000000 --- a/ags/widget/BackgroundWindow.ts +++ /dev/null @@ -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; - /** Monitor number where the window should open */ - monitor: number | Binding; - /** Custom stylesheet used in the window. default: `background: rgba(0, 0, 0, .2)` */ - css?: string | Binding; - /** 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); -} diff --git a/ags/widget/BackgroundWindow.tsx b/ags/widget/BackgroundWindow.tsx new file mode 100644 index 0000000..fb4a138 --- /dev/null +++ b/ags/widget/BackgroundWindow.tsx @@ -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; + /** Monitor number where the window should open */ + monitor: number | Accessor; + /** Custom stylesheet used in the window. default: `background: rgba(0, 0, 0, .2)` */ + css?: string | Accessor; + /* 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 = new Map(); + + return 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; +} diff --git a/ags/widget/CustomDialog.ts b/ags/widget/CustomDialog.ts deleted file mode 100644 index 55523b6..0000000 --- a/ags/widget/CustomDialog.ts +++ /dev/null @@ -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; - className?: string | Binding; - cssBackground?: string; - title?: string | Binding; - text?: string | Binding; - heightRequest?: number | Binding; - widthRequest?: number | Binding; - childOrientation?: Gtk.Orientation | Binding; - children?: Array | Binding>; - child?: Gtk.Widget | Binding; - onFinish?: () => void; - options?: Array; - optionsOrientation?: Gtk.Orientation | Binding; -}; - -export interface CustomDialogOption { - onClick?: () => void; - text: string | Binding; - closeOnClick?: boolean | Binding; -} - -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 = []; - 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; -} diff --git a/ags/widget/CustomDialog.tsx b/ags/widget/CustomDialog.tsx new file mode 100644 index 0000000..6afef16 --- /dev/null +++ b/ags/widget/CustomDialog.tsx @@ -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; + className?: string | Accessor; + cssBackground?: string; + title?: string | Accessor; + text?: string | Accessor; + heightRequest?: number | Accessor; + widthRequest?: number | Accessor; + childOrientation?: Gtk.Orientation | Accessor; + children?: WidgetNodeType; + onFinish?: () => void; + options?: Array | Accessor>; + optionsOrientation?: Gtk.Orientation | Accessor; +}; + +export interface CustomDialogOption { + onClick?: () => void; + text: string | Accessor; + closeOnClick?: boolean | Accessor; +} + +function CustomDialogOption(props: CustomDialogOption & { dialog: Astal.Window }) { + function onClicked() { + props.onClick?.(); + props.closeOnClick && props.dialog.close(); + } + + return +} + +export function CustomDialog({ options = [{ text: tr("accept") }], ...props}: CustomDialogProps) { + let dialog: Astal.Window; + return Windows.getDefault().createWindowForFocusedMonitor((mon: number) => + dialog = self}> + + + + + + + { + (props.children instanceof Accessor) ? + (Array.isArray(props.children) ? + >}> + {(widget) => widget && widget} + + : }> + {(widget) => widget && widget} + ) + : (Array.isArray(props.children) ? + props.children.map(widget => widget && widget).filter(w => w) + : props.children) + } + + 0} + spacing={8} orientation={Gtk.Orientation.VERTICAL} /> + + {( + { + (options instanceof Accessor) ? + + {(option) => } + + : options.map(option => + ) + } + )} + + )(); +} diff --git a/ags/widget/EntryPopup.ts b/ags/widget/EntryPopup.ts deleted file mode 100644 index 266a428..0000000 --- a/ags/widget/EntryPopup.ts +++ /dev/null @@ -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; - text?: string | Binding; - cancelText?: string | Binding; - acceptText?: string | Binding; - closeOnAccept?: boolean; - entryPlaceholder?: string | Binding; - onAccept: (userInput: string) => void; - onCancel?: () => void; - onFinish?: () => void; - isPassword?: boolean | Binding; -}; - -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; -} diff --git a/ags/widget/EntryPopup.tsx b/ags/widget/EntryPopup.tsx new file mode 100644 index 0000000..7941852 --- /dev/null +++ b/ags/widget/EntryPopup.tsx @@ -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; + text?: string | Accessor; + cancelText?: string | Accessor; + acceptText?: string | Accessor; + closeOnAccept?: boolean; + entryPlaceholder?: string | Accessor; + onAccept: (userInput: string) => void; + onCancel?: () => void; + onFinish?: () => void; + isPassword?: boolean | Accessor; +}; + +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 ? + as Gtk.PasswordEntry + : as Gtk.Entry; + + const window = { + entered = true; + props.onAccept(entry.text); + entry.text = ""; + } + } + ]} onFinish={() => { + !entered && props.onCancel?.() + props.onFinish?.(); + }} + /> as Astal.Window; + + return window; +} diff --git a/ags/widget/Notification.ts b/ags/widget/Notification.ts deleted file mode 100644 index e6cc656..0000000 --- a/ags/widget/Notification.ts +++ /dev/null @@ -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, "&") - }), - 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, "&") - } 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); -} diff --git a/ags/widget/Notification.tsx b/ags/widget/Notification.tsx new file mode 100644 index 0000000..bf15442 --- /dev/null +++ b/ags/widget/Notification.tsx @@ -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> = new Map(); + + return { + 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))); + }}> + + + { + 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); + }} /> + + + + + + + + { + const image = getNotificationImage(notification); + + image && + self.prepend(Gtk.Picture.new_for_filename(image)); + }}> + + + + + + + + + + action.label.toLowerCase() !== "view").length > 0) + }> + { + (notification instanceof AstalNotifd.Notification) && + notification.actions.filter(a => a.label.toLowerCase() !== "view").map(action => + { + notification.invoke(action.id); + actionClose?.(notification); + }} + />) + } + + as Gtk.Widget; +} diff --git a/ags/widget/PopupWindow.ts b/ags/widget/PopupWindow.ts deleted file mode 100644 index fe66b05..0000000 --- a/ags/widget/PopupWindow.ts +++ /dev/null @@ -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 & 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); -} diff --git a/ags/widget/PopupWindow.tsx b/ags/widget/PopupWindow.tsx new file mode 100644 index 0000000..4e6939d --- /dev/null +++ b/ags/widget/PopupWindow.tsx @@ -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; + actionClickedOutside?: (self: Astal.Window) => void; + actionKeyPressed?: (self: Astal.Window, keyval: number, keycode: number) => void; +}; + +export type PopupWindowProps = Pick>, + "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 ? ( as Astal.Window) + : undefined; + + const omittedProps = omitObjectKeys(props, [ + "children", + "actionKeyPressed", + "actionClickedOutside", + "cssBackgroundWindow", + "marginTop", + "marginLeft", + "marginRight", + "marginBottom" + ]); + + return + `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 = 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)))); + }}> + + + { + const gestureClick = Gtk.GestureClick.new(); + self.add_controller(gestureClick); + }}> + {props.children} + + + ; +} diff --git a/ags/widget/Separator.ts b/ags/widget/Separator.ts deleted file mode 100644 index 34c50de..0000000 --- a/ags/widget/Separator.ts +++ /dev/null @@ -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; -} - -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); -} diff --git a/ags/widget/Separator.tsx b/ags/widget/Separator.tsx new file mode 100644 index 0000000..aae149c --- /dev/null +++ b/ags/widget/Separator.tsx @@ -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; +} + +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 + + + +} diff --git a/ags/widget/runner/ResultWidget.ts b/ags/widget/runner/ResultWidget.ts deleted file mode 100644 index 04800a6..0000000 --- a/ags/widget/runner/ResultWidget.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Binding, register } from "astal"; -import { Gtk, Widget } from "astal/gtk3"; - -export { ResultWidget, ResultWidgetProps }; - -type ResultWidgetProps = { - icon?: string | Binding | Gtk.Widget | Binding; - title: string | Binding; - description?: string | Binding; - 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 | Gtk.Widget | Binding | 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 - } as Widget.IconProps)); - } else { - this.add(new Widget.Box({ - child: this.icon as Binding - } 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)); - } -} diff --git a/ags/widget/runner/ResultWidget.tsx b/ags/widget/runner/ResultWidget.tsx new file mode 100644 index 0000000..045a9a3 --- /dev/null +++ b/ags/widget/runner/ResultWidget.tsx @@ -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 | JSX.Element | Accessor; + title: string | Accessor; + description?: string | Accessor; + 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 | JSX.Element | Accessor); + 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( + } /> as Gtk.Image); + } else { + this.prepend( + }> + {(widget) => widget} + + as Gtk.Box); + } + } else { + if(typeof this.icon === "string") + this.prepend( as Gtk.Image); + else + this.prepend(this.icon as Gtk.Widget); + } + } + + this.append( + + + + as Gtk.Box); + } +}