✨ chore: migrate widgets to ags v3 and gtk4
This commit is contained in:
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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>)();
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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, "&")
|
||||||
|
} />
|
||||||
|
|
||||||
|
<Gtk.Label class={"body"} useMarkup={true} xalign={0} wrap={true}
|
||||||
|
wrapMode={Pango.WrapMode.WORD_CHAR} singleLineMode={false}
|
||||||
|
label={notification.body.replace(/[&]/g, "&")} />
|
||||||
|
</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;
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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>;
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user