From 53929db052021fc2c409a8d951ceb81fab1e4c0d Mon Sep 17 00:00:00 2001 From: retrozinndev Date: Tue, 1 Apr 2025 10:47:40 -0300 Subject: [PATCH] :sparkles: ags: new window management system, adjustments, use adwaita sans --- ags/app.ts | 54 ++++-- ags/runner/Runner.ts | 33 ++-- ags/scripts/arg-handler.ts | 33 ++-- ags/style.scss | 7 +- ags/style/_mixins.scss | 2 +- ags/widget/AskPopup.ts | 15 +- ags/widget/PopupWindow.ts | 19 +- ags/widget/bar/Apps.ts | 2 +- ags/widget/bar/Clock.ts | 7 +- ags/widget/bar/Media.ts | 7 +- ags/widget/bar/Status.ts | 8 +- ags/widget/center-window/BigMedia.ts | 3 +- ags/widget/control-center/NotifHistory.ts | 2 +- ags/widget/control-center/Pages.ts | 39 +++-- ags/widget/control-center/QuickActions.ts | 2 +- ags/widget/control-center/Sliders.ts | 4 +- ags/widget/control-center/Tiles.ts | 10 +- ags/widget/control-center/tiles/Network.ts | 8 +- ags/widget/control-center/tiles/Tile.ts | 6 +- ags/window/AppsWindow.ts | 192 ++++++++++---------- ags/window/Bar.ts | 10 +- ags/window/CenterWindow.ts | 117 ++++++------- ags/window/ControlCenter.ts | 29 ++- ags/window/FloatingNotifications.ts | 25 +-- ags/window/LogoutMenu.ts | 13 +- ags/window/OSD.ts | 8 +- ags/windows.ts | 194 +++++++++++++++++---- 27 files changed, 505 insertions(+), 344 deletions(-) diff --git a/ags/app.ts b/ags/app.ts index 43cd558..8c86412 100644 --- a/ags/app.ts +++ b/ags/app.ts @@ -1,22 +1,25 @@ import { App } from "astal/gtk3" -import { Windows } from "./windows"; import { Wireplumber } from "./scripts/volume"; import { runStyleHandler } from "./scripts/style-handler"; import { handleArguments } from "./scripts/arg-handler"; import { Time, timeout } from "astal/time"; -import { OSD, OSDModes, setOSDMode } from "./window/OSD"; -import { ControlCenter } from "./window/ControlCenter"; +import { OSDModes, setOSDMode } from "./window/OSD"; import { Runner } from "./runner/Runner"; import { PluginApps } from "./runner/plugins/apps"; import { PluginShell } from "./runner/plugins/shell"; import { PluginWebSearch } from "./runner/plugins/websearch"; import { PluginMedia } from "./runner/plugins/media"; +import { Windows } from "./windows"; +import { Notifications } from "./scripts/notifications"; +import AstalNotifd from "gi://AstalNotifd"; +import { GObject } from "astal"; let osdTimer: (Time|undefined); +let connections = new Map | number)>(); const runnerPlugins: Array = [ PluginApps, @@ -27,19 +30,39 @@ const runnerPlugins: Array = [ App.start({ instanceName: "astal", - requestHandler(request: string, response: (result: any) => void) { + async requestHandler(request: string, response: (result: any) => void) { // console.log(`[LOG] Arguments received: ${request}`); - response(handleArguments(request)); + response(await handleArguments(request)); }, main() { console.log(`[LOG] Initialized astal instance as: ${ App.instanceName || "astal" }`); + App.vfunc_quit = () => { + console.log("[LOG] Disconnecting stuff"); + connections.forEach((v, k) => Array.isArray(v) ? + v.map(id => k.disconnect(id)) + : k.disconnect(v)); + }; + console.log(`[LOG] Running Stylesheet handler`); + runStyleHandler(); + //console.log(`[LOG] Starting to monitor scripts to automatically reload instance`); //monitorPaths(); // Only for debugging purposes(testing new widgets and stuff) - Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () => - !Windows.isVisible(ControlCenter) && triggerOSD(OSDModes.SINK)); + connections.set(Wireplumber.getDefault(), [ + Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () => + !Windows.isVisible("osd") && triggerOSD(OSDModes.SINK)) + ]); + + connections.set(Notifications.getDefault(), [ + Notifications.getDefault().connect("notification-added", (_, _notif: AstalNotifd.Notification) => { + Windows.open("floating-notifications"); + }), + Notifications.getDefault().connect("notification-removed", (_: Notifications, _id: number) => { + _.notifications.length === 0 && Windows.close("floating-notifications"); + }) + ]); console.log(`[LOG] Adding runner plugins`); runnerPlugins.map(plugin => Runner.addPlugin(plugin)); @@ -49,17 +72,18 @@ App.start({ function triggerOSD(osdModeParam: OSDModes) { setOSDMode(osdModeParam); - Windows.open(OSD); if(!osdTimer) { + Windows.open("osd"); osdTimer = timeout(3000, () => { - Windows.close(OSD); - osdTimer = undefined; - }); - } else { - osdTimer.cancel(); - osdTimer = timeout(3000, () => { - Windows.close(OSD); + Windows.close("osd"); osdTimer = undefined; }); + return; } + + osdTimer.cancel(); + osdTimer = timeout(3000, () => { + Windows.close("osd"); + osdTimer = undefined; + }); } diff --git a/ags/runner/Runner.ts b/ags/runner/Runner.ts index d7700ae..08453e6 100644 --- a/ags/runner/Runner.ts +++ b/ags/runner/Runner.ts @@ -3,8 +3,9 @@ import { Gdk, Gtk, Widget } from "astal/gtk3"; import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; import { updateApps } from "../scripts/apps"; import { ResultWidget, ResultWidgetProps } from "../widget/runner/ResultWidget"; +import { Windows } from "../windows"; -export let runnerInstance: (Widget.Window|null) = null; +export let runnerInstance: (Gtk.Window|null) = null; let onClickTimeout: (AstalIO.Time|undefined); export function startRunnerDefault() { @@ -12,16 +13,21 @@ export function startRunnerDefault() { entryPlaceHolder: "Start typing..." } as Runner.RunnerProps, () => [ - new ResultWidget({ - icon: "utilities-terminal-symbolic", - title: "Run shell commands", - description: "Start typing with '!' prefix to run shell commands" - } as ResultWidgetProps), new ResultWidget({ icon: "application-x-executable-symbolic", title: "Run your applications", description: "Type the name of the application to search" } as ResultWidgetProps), + new ResultWidget({ + icon: "media-playback-start-symbolic", + title: "Control media", + description: "Use prefix ':' to run" + } as ResultWidgetProps), + new ResultWidget({ + icon: "utilities-terminal-symbolic", + title: "Run shell commands", + description: "Start typing with '!' prefix to run shell commands" + } as ResultWidgetProps), new ResultWidget({ icon: "applications-internet-symbolic", title: "Search the Web", @@ -39,13 +45,11 @@ export namespace Runner { entryPlaceHolder?: string; }; - export function close(gtkWindow?: Widget.Window) { - const window = gtkWindow ? gtkWindow : runnerInstance; - + export function close() { [...plugins.values()].map(plugin => plugin && plugin.onClose && plugin.onClose()); - window?.close(); + runnerInstance?.close(); runnerInstance = null; } @@ -83,7 +87,7 @@ export namespace Runner { return plugins.delete(plugin); } - export function openRunner(props?: RunnerProps, placeholder?: () => Array): (Widget.Window|null) { + export function openRunner(props?: RunnerProps, placeholder?: () => Array): (Gtk.Window|null) { let subs: Array<() => void> = []; const entryText: Variable = new Variable(""); @@ -156,8 +160,9 @@ export namespace Runner { })); if(!runnerInstance) - runnerInstance = PopupWindow({ + runnerInstance = Windows.createWindowForFocusedMonitor((mon: number): (Widget.Window) => PopupWindow({ namespace: "runner", + monitor: mon, widthRequest: props?.width || 750, heightRequest: props?.height || 0, marginTop: 250, @@ -176,8 +181,8 @@ export namespace Runner { }, closeAction: (_) => { - close(_); subs.map(sub => sub()); + close(); }, child: new Widget.Box({ className: "runner main", @@ -195,7 +200,7 @@ export namespace Runner { }) ] } as Widget.BoxProps) - } as PopupWindowProps); + } as PopupWindowProps))(); return runnerInstance; } diff --git a/ags/scripts/arg-handler.ts b/ags/scripts/arg-handler.ts index 767bb23..6a891f7 100644 --- a/ags/scripts/arg-handler.ts +++ b/ags/scripts/arg-handler.ts @@ -1,5 +1,3 @@ -import { Gtk } from "astal/gtk3"; - import { Wireplumber } from "./volume"; import { Windows } from "../windows"; @@ -7,9 +5,10 @@ import { restartInstance } from "./reload-handler"; import { startRunnerDefault } from "../runner/Runner"; import { showWorkspaceNumbers } from "../widget/bar/Workspaces"; import { timeout } from "astal"; +import { App } from "astal/gtk3"; -export function handleArguments(request: string): any { +export async function handleArguments(request: string): Promise { const args: Array = request.split(" "); switch(args[0]) { case "open": @@ -27,6 +26,10 @@ export function handleArguments(request: string): any { case "reload": restartInstance(); return "Restarting instance..." + + case "windows": + return Object.keys(Windows.windows).map(name => + `${name}: ${Windows.isVisible(name) ? "open" : "closed" }`).join('\n'); case "runner": startRunnerDefault(); @@ -39,6 +42,12 @@ export function handleArguments(request: string): any { } return "Showing numbers"; + case "c": + case "code": + const input = request.replace(args[0], "").trimStart(); + console.log(input); + return await (App.eval(input).then((v) => v).catch((r) => r)); + default: return "command not found! try checking help"; } @@ -46,13 +55,13 @@ export function handleArguments(request: string): any { // Didn't want to bloat the switch statement, so I just separated it into functions function handleWindowArgs(args: Array): string { - const specifiedWindow: (Gtk.Window|undefined) = Windows.getWindow(args[1]); - - if(!specifiedWindow) + if(!args[1]) return "Window argument not specified!"; - if(!Windows.getList().has(args[1])) - return `Name "${args[1]}" not found windows map! Make sure to add new Windows on the Map!` + const specifiedWindow: string = args[1]; + + if(!Windows.hasWindow(specifiedWindow)) + return `Name "${specifiedWindow}" not found windows map! Make sure to add new Windows on the Map!` switch(args[0]) { case "open": @@ -137,11 +146,11 @@ function handleVolumeArgs(args: Array) { return ` Control speaker and microphone volumes easily! Options: - sink-set [number]: set sink(speaker) volume with [number], 0 to ${Wireplumber.getDefault().getMaxSinkVolume()}. + sink-set [number]: set sink(speaker) volume with [number], 0 to ${Wireplumber.getDefault().getMaxSinkVolume() || 100}. sink-mute: toggle mute for the sink(speaker) device. sink-increase [number]: increases sink(speaker) volume with [number]. sink-decrease [number]: decreases sink(speaker) volume with [number]. - source-set [number]: set source(microphone) volume with [number], 0 to ${Wireplumber.getDefault().getMaxSourceVolume()}. + source-set [number]: set source(microphone) volume with [number], 0 to ${Wireplumber.getDefault().getMaxSourceVolume() || 100}. source-mute: toggle mute for the source(microphone) device. source-increase [number]: increases source(microphone) volume with [number]. source-decrease [number]: decreases source(microphone) volume with [number] @@ -158,11 +167,13 @@ Options: open [window_name]: sets specified window's visibility to true. close [window_name]: sets specified window's visibility to false. toggle [window_name]: toggles visibility of specified window. + windows: shows available windows to control. reload: creates a new astal instance and removes this one. volume: wireplumber volume controller, see "volume help". runner: open the application runner. + c, code [js]: runs provided js in args and returns result. show-ws-numbers: show or hide workspace numbers in bar. - help, -h, --help: shows this help message. + h, help: shows this help message. 2025 (c) retrozinndev's Hyprland-Dots, licensed under the MIT License. https://github.com/retrozinndev/Hyprland-Dots diff --git a/ags/style.scss b/ags/style.scss index 57271da..ad150b7 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -42,7 +42,12 @@ window.ask-popup { & button { background: colors.$bg-primary; border-radius: 12px; - padding: 6px; + padding: 9px 6px; + + & label { + font-size: 16px; + font-weight: 600; + } margin: { left: 4px; diff --git a/ags/style/_mixins.scss b/ags/style/_mixins.scss index ce784f8..ba540f0 100644 --- a/ags/style/_mixins.scss +++ b/ags/style/_mixins.scss @@ -6,7 +6,7 @@ @mixin reset-props { all: unset; transition: 120ms linear; - font-family: "Cantarell", "Noto Sans", + font-family: "Adwaita Sans", "Cantarell", "Noto Sans", "Noto Sans CJK JP", "Noto Sans CJK KR", "Noto Sans CJK HK", "Noto Sans CJK SC", "Noto Sans CJK TC", sans-serif, diff --git a/ags/widget/AskPopup.ts b/ags/widget/AskPopup.ts index 9283ea4..7cfc48b 100644 --- a/ags/widget/AskPopup.ts +++ b/ags/widget/AskPopup.ts @@ -3,6 +3,7 @@ import { PopupWindow, PopupWindowProps } from "./PopupWindow"; import { Astal, Gtk, Widget } from "astal/gtk3"; import { Separator } from "./Separator"; import { tr } from "../i18n/intl"; +import { Windows } from "../windows"; export type AskPopupProps = { title?: string | Binding; @@ -17,15 +18,16 @@ export type AskPopupProps = { * A Popup Widget that asks yes or no to a certain question. * Runs onAccept() when user accepts or else onDecline() when * user doesn't accept or closes window. + * This window isn't registered in this shell windowing stuff. */ -export function AskPopup(props: AskPopupProps) { +export function AskPopup(props: AskPopupProps): Gtk.Window { const buttons = [ new Widget.Button({ className: "cancel", hexpand: true, label: props.cancelText || tr("ask_popup.options.cancel") || "Cancel", onClick: (_) => { - window.destroy(); + window.close(); props.onCancel && props.onCancel(); } } as Widget.ButtonProps), @@ -34,15 +36,14 @@ export function AskPopup(props: AskPopupProps) { hexpand: true, label: props.acceptText || tr("ask_popup.options.accept") || "Ok", onClick: (_) => { - window.destroy(); + window.close(); props.onAccept && props.onAccept(); } } as Widget.ButtonProps) ]; - const window = PopupWindow({ + const window = Windows.createWindowForFocusedMonitor(PopupWindow({ namespace: "ask-popup", - visible: true, className: "ask-popup", exclusivity: Astal.Exclusivity.IGNORE, widthRequest: 350, @@ -80,5 +81,7 @@ export function AskPopup(props: AskPopupProps) { } as Widget.BoxProps) ] } as Widget.BoxProps) - } as PopupWindowProps); + } as PopupWindowProps))(); + + return window; } diff --git a/ags/widget/PopupWindow.ts b/ags/widget/PopupWindow.ts index 272eeb5..3c6d078 100644 --- a/ags/widget/PopupWindow.ts +++ b/ags/widget/PopupWindow.ts @@ -2,7 +2,7 @@ import { Binding } from "astal"; import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; -const { TOP, BOTTOM, LEFT, RIGHT }: typeof Astal.WindowAnchor = Astal.WindowAnchor; +const { TOP, BOTTOM, LEFT, RIGHT } = Astal.WindowAnchor; export type PopupWindowProps = Pick & { + child?: (Gtk.Widget | Binding); + children?: (Array>); marginTop?: number; marginLeft?: number; marginBottom?: number; marginRight?: number; onKeyPressEvent?: (self: Widget.Window, event: Gdk.Event) => void; - /** Do something else instead of hiding window on close action(clicking outside conent / pressing Escape) + /** Do something else instead of closing window on close action(clicking outside conent/pressing Escape) * Observation: onClose() function will still be ran after close action if defined. */ closeAction?: (self: Widget.Window) => void; onClose?: (self: Widget.Window) => void; }; -export function PopupWindow(props: PopupWindowProps): Widget.Window { +export const PopupWindow = (props: PopupWindowProps): Widget.Window => { if(!props.closeAction) - props.closeAction = (window) => { - window.hide(); - }; + props.closeAction = (window) => window.close(); return new Widget.Window({ namespace: props?.namespace || "popup-window", @@ -47,9 +46,8 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window { keymode: Astal.Keymode.EXCLUSIVE, layer: props?.layer || Astal.Layer.OVERLAY, focusOnMap: true, - visible: props?.visible, - monitor: props?.monitor || 0, setup: props.setup, + monitor: props.monitor || 0, onButtonPressEvent: (_, event: Gdk.Event) => { const [, posX, posY] = event.get_coords(); const childAllocation = _.get_child()!.get_allocation(); @@ -88,7 +86,8 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window { widthRequest: props.widthRequest, heightRequest: props.heightRequest, onButtonPressEvent: () => true, - child: props.child + ...(props.child ? { child: props.child } : {}), + ...(props.children ? { children: props.children } : {}) } as Widget.BoxProps) } as Widget.WindowProps); } diff --git a/ags/widget/bar/Apps.ts b/ags/widget/bar/Apps.ts index 05cf9ba..df77fb4 100644 --- a/ags/widget/bar/Apps.ts +++ b/ags/widget/bar/Apps.ts @@ -4,7 +4,7 @@ import { Windows } from "../../windows"; export function Apps(): Gtk.Widget { return new Widget.EventBox({ - onClickRelease: () => Windows.getWindow("apps-window")?.show(), + onClickRelease: () => Windows.open("apps-window"), className: "apps", child: new Widget.Box({ child: new Widget.Icon({ diff --git a/ags/widget/bar/Clock.ts b/ags/widget/bar/Clock.ts index 5c67964..e9c7585 100644 --- a/ags/widget/bar/Clock.ts +++ b/ags/widget/bar/Clock.ts @@ -2,14 +2,13 @@ import { Gtk, Widget } from "astal/gtk3"; import { getDateTime } from "../../scripts/time"; import { bind, GLib } from "astal"; import { Windows } from "../../windows"; -import { CenterWindow } from "../../window/CenterWindow"; export function Clock(): Gtk.Widget { return new Widget.Box({ - className: bind(CenterWindow, "visible").as((visible: boolean) => - visible ? "clock open" : "clock"), + className: bind(Windows, "openWindows").as((openWins) => + Object.hasOwn(openWins, "center-window") ? "open clock" : "clock"), child: new Widget.Button({ - onClick: () => Windows.toggle(CenterWindow), + onClick: () => Windows.toggle("center-window"), label: getDateTime().as((dateTime: GLib.DateTime) => { return dateTime.format("%A %d, %H:%M") }) diff --git a/ags/widget/bar/Media.ts b/ags/widget/bar/Media.ts index 269d586..5206d7d 100644 --- a/ags/widget/bar/Media.ts +++ b/ags/widget/bar/Media.ts @@ -71,9 +71,8 @@ export function Media(): Gtk.Widget { const mediaWidget = new Widget.EventBox({ className: "media-eventbox", - visible: bind(mpris, "players").as((players: Array) => { - return players[0] && players[0].get_available() || CenterWindow.is_visible(); - }), + visible: bind(mpris, "players").as((players: Array) => + players[0] && players[0].get_available()), onDestroy: (_) => { hoverConnectionId !== undefined && _.disconnect(hoverConnectionId); @@ -81,7 +80,7 @@ export function Media(): Gtk.Widget { hoverLostConnectionId !== undefined && _.disconnect(hoverLostConnectionId); }, - onClick: () => Windows.toggle(CenterWindow), + onClick: () => Windows.toggle("center-window"), child: new Widget.Box({ className: "media", children: [ diff --git a/ags/widget/bar/Status.ts b/ags/widget/bar/Status.ts index 94aa017..8859f21 100644 --- a/ags/widget/bar/Status.ts +++ b/ags/widget/bar/Status.ts @@ -5,16 +5,15 @@ import AstalWp from "gi://AstalWp"; import { bind, Variable } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import { Wireplumber } from "../../scripts/volume"; -import { ControlCenter } from "../../window/ControlCenter"; import { Notifications } from "../../scripts/notifications"; import { Windows } from "../../windows"; export function Status(): Gtk.Widget { return new Widget.EventBox({ - className: bind(ControlCenter, "visible").as((visible: boolean) => - visible ? "status open" : "status"), - onClick: () => Windows.toggle(ControlCenter!), + className: bind(Windows, "openWindows").as((openWins) => + Object.hasOwn(openWins, "control-center") ? "open status" : "status"), + onClick: () => Windows.toggle("control-center"), child: new Widget.Box({ children: [ volumeStatusSlider({ @@ -63,6 +62,7 @@ function volumeStatusSlider(props: { className?: string, endpoint: AstalWp.Endpo revealer.add(new Widget.Slider({ className: "slider", + setup: (slider) => slider.set_value(Math.floor(props.endpoint.get_volume() * 100)), onDragged: (slider) => props.endpoint.set_volume(slider.value / 100), value: bind(props.endpoint, "volume").as((volume) => Math.floor(volume * 100)), diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts index 53d0fea..47f2aad 100644 --- a/ags/widget/center-window/BigMedia.ts +++ b/ags/widget/center-window/BigMedia.ts @@ -2,9 +2,10 @@ import { AstalIO, bind, Binding, execAsync, GLib, timeout } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import AstalMpris from "gi://AstalMpris"; -let dragTimer: (AstalIO.Time|undefined); export function BigMedia(): Gtk.Widget { + let dragTimer: (AstalIO.Time|undefined); + return new Widget.Box({ className: "big-media", orientation: Gtk.Orientation.VERTICAL, diff --git a/ags/widget/control-center/NotifHistory.ts b/ags/widget/control-center/NotifHistory.ts index c5924b9..4ff3952 100644 --- a/ags/widget/control-center/NotifHistory.ts +++ b/ags/widget/control-center/NotifHistory.ts @@ -4,7 +4,7 @@ import { HistoryNotification, Notifications } from "../../scripts/notifications" import { NotificationWidget } from "../Notification"; -export const NotifHistory: Gtk.Widget = new Widget.Box({ +export const NotifHistory = () => new Widget.Box({ orientation: Gtk.Orientation.VERTICAL, className: "history", visible: bind(Notifications.getDefault(), "history").as(history => history.length > 0), diff --git a/ags/widget/control-center/Pages.ts b/ags/widget/control-center/Pages.ts index 4c66b43..34ff5bd 100644 --- a/ags/widget/control-center/Pages.ts +++ b/ags/widget/control-center/Pages.ts @@ -2,20 +2,29 @@ import { timeout, Variable } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import { Page } from "./pages/Page"; -const currentPage = new Variable(undefined); +const currentPage = new Variable(undefined); +let pagesInstance: (Widget.Revealer | undefined); -export const PagesWidget: Widget.Revealer = new Widget.Revealer({ - revealChild: false, - className: "pages", - transitionType: Gtk.RevealerTransitionType.SLIDE_DOWN, - transitionDuration: 360, - child: currentPage((page: (Page|undefined)) => - !page ? new Widget.Box() : page.getPage()) -} as Widget.RevealerProps); +export const PagesWidget = () => { + const revealer = new Widget.Revealer({ + revealChild: false, + className: "pages", + transitionType: Gtk.RevealerTransitionType.SLIDE_DOWN, + transitionDuration: 360, + child: currentPage((page: (Page|undefined)) => + !page ? new Widget.Box() : page.getPage()) + } as Widget.RevealerProps); + + pagesInstance = revealer; + + return revealer; +} export function showPages(page: Page): void { + if(!pagesInstance) return; + currentPage.set(page); - PagesWidget.set_reveal_child(true); + pagesInstance.set_reveal_child(true); page.props.onOpen && page.props.onOpen(); } @@ -24,7 +33,9 @@ export function getPage(): (Page|undefined) { } export function togglePage(page: Page): void { - if(!PagesWidget.revealChild) { + if(!pagesInstance) return; + + if(!pagesInstance.revealChild) { showPages(page); return; } @@ -33,10 +44,12 @@ export function togglePage(page: Page): void { } export function hidePages() { - PagesWidget.set_reveal_child(false); + if(!pagesInstance) return; + + pagesInstance.set_reveal_child(false); if(!currentPage.get()) return; - timeout(500, () => { + timeout(pagesInstance.transitionDuration || 500, () => { if(currentPage.get() && currentPage.get()?.props.onClose) currentPage.get()!.props.onClose!(); diff --git a/ags/widget/control-center/QuickActions.ts b/ags/widget/control-center/QuickActions.ts index 4dd3ac6..92e3ee3 100644 --- a/ags/widget/control-center/QuickActions.ts +++ b/ags/widget/control-center/QuickActions.ts @@ -57,7 +57,7 @@ function LogoutButton(): Widget.Button { } as Widget.ButtonProps); } -export const QuickActions: Widget.Box = new Widget.Box({ +export const QuickActions = () => new Widget.Box({ className: "quickactions", children: [ new Widget.Box({ diff --git a/ags/widget/control-center/Sliders.ts b/ags/widget/control-center/Sliders.ts index 87dda46..276d43e 100644 --- a/ags/widget/control-center/Sliders.ts +++ b/ags/widget/control-center/Sliders.ts @@ -2,7 +2,7 @@ import { bind } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import { Wireplumber } from "../../scripts/volume"; -export const Sliders: Gtk.Widget = new Widget.Box({ +export const Sliders = () => new Widget.Box({ className: "sliders", orientation: Gtk.Orientation.VERTICAL, expand: true, @@ -17,6 +17,7 @@ export const Sliders: Gtk.Widget = new Widget.Box({ new Widget.Slider({ drawValue: false, hexpand: true, + setup: (slider) => slider.set_value(Wireplumber.getDefault().getSinkVolume()), value: bind(Wireplumber.getDefault().getDefaultSink(), "volume").as((volume: number) => Math.floor(volume * 100)), max: Wireplumber.getDefault().getMaxSinkVolume(), @@ -34,6 +35,7 @@ export const Sliders: Gtk.Widget = new Widget.Box({ new Widget.Slider({ drawValue: false, hexpand: true, + setup: (slider) => slider.set_value(Wireplumber.getDefault().getSourceVolume()), value: bind(Wireplumber.getDefault().getDefaultSource(), "volume").as((volume: number) => Math.floor(volume * 100)), max: Wireplumber.getDefault().getMaxSourceVolume(), diff --git a/ags/widget/control-center/Tiles.ts b/ags/widget/control-center/Tiles.ts index f6d23d6..632c2a2 100644 --- a/ags/widget/control-center/Tiles.ts +++ b/ags/widget/control-center/Tiles.ts @@ -3,13 +3,13 @@ import { TileNetwork } from "./tiles/Network"; import { TileBluetooth } from "./tiles/Bluetooth"; import { TileDND } from "./tiles/DoNotDisturb"; -export const tileList: Array = [ +export const tileList: Array<() => Gtk.Widget> = [ TileNetwork, TileBluetooth, TileDND ]; -export function TilesWidget(): Gtk.Widget { +export function Tiles(): Gtk.Widget { const tilesFlowBox: Gtk.FlowBox = new Gtk.FlowBox({ visible: true, orientation: Gtk.Orientation.HORIZONTAL, @@ -21,13 +21,11 @@ export function TilesWidget(): Gtk.Widget { homogeneous: true, } as Gtk.FlowBox.ConstructorProps); - tileList.map((item: Gtk.Widget) => - tilesFlowBox.insert(item, -1)); + tileList.map((item: (() => Gtk.Widget)) => + tilesFlowBox.insert(item(), -1)); return new Widget.Box({ className: "tiles-container", child: tilesFlowBox } as Widget.BoxProps); } - -export const Tiles: Gtk.Widget = TilesWidget(); diff --git a/ags/widget/control-center/tiles/Network.ts b/ags/widget/control-center/tiles/Network.ts index 58221af..2217c01 100644 --- a/ags/widget/control-center/tiles/Network.ts +++ b/ags/widget/control-center/tiles/Network.ts @@ -6,7 +6,7 @@ import { togglePage } from "../Pages"; import { PageNetwork } from "../pages/Network"; import { tr } from "../../../i18n/intl"; -export const TileNetwork = new Widget.Box({ +export const TileNetwork = () => new Widget.Box({ child: Variable.derive([ bind(AstalNetwork.get_default(), "primary"), bind(AstalNetwork.get_default(), "wired"), @@ -36,7 +36,7 @@ export const TileNetwork = new Widget.Box({ icon: "󰤨", iconSize: 16, toggleState: bind(wifi, "enabled") - } as TileProps); + } as TileProps)(); } else if(primary === AstalNetwork.Primary.WIRED) { return Tile({ @@ -69,7 +69,7 @@ export const TileNetwork = new Widget.Box({ internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED ) - } as TileProps); + } as TileProps)(); } return Tile({ @@ -82,6 +82,6 @@ export const TileNetwork = new Widget.Box({ iconSize: 16, toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) => internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED) - } as TileProps); + } as TileProps)(); })() } as Widget.BoxProps); diff --git a/ags/widget/control-center/tiles/Tile.ts b/ags/widget/control-center/tiles/Tile.ts index a146b70..8ad3bae 100644 --- a/ags/widget/control-center/tiles/Tile.ts +++ b/ags/widget/control-center/tiles/Tile.ts @@ -15,7 +15,7 @@ export type TileProps = { onClickMore?: () => void; } -export function Tile(props: TileProps): Widget.EventBox { +export function Tile(props: TileProps): (() => Widget.EventBox) { const toggled = new Variable(props.toggleState instanceof Binding ? (props.toggleState.get() || false) : (props.toggleState || false)); @@ -24,7 +24,7 @@ export function Tile(props: TileProps): Widget.EventBox { if(props?.toggleState instanceof Binding) subscription = props.toggleState.subscribe(val => toggled.set(val || false)); - return new Widget.EventBox({ + return () => new Widget.EventBox({ className: toggled().as((state: boolean) => state ? "tile-eventbox toggled" : "tile-eventbox"), expand: true, @@ -39,7 +39,7 @@ export function Tile(props: TileProps): Widget.EventBox { toggled.set(true); props.onToggledOn && props.onToggledOn(); }, - onDestroy: () => subscription(), + onDestroy: () => subscription?.(), child: new Widget.Box({ className: (props.className instanceof Binding) ? props.className.as((clsName: (string|undefined)) => diff --git a/ags/window/AppsWindow.ts b/ags/window/AppsWindow.ts index 6a5a7cc..423d002 100644 --- a/ags/window/AppsWindow.ts +++ b/ags/window/AppsWindow.ts @@ -4,108 +4,114 @@ import { cleanExec, getAstalApps } from "../scripts/apps"; import AstalApps from "gi://AstalApps"; const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; -const searchString = new Variable(""); -let searchSubscription: () => void; -export const AppsWindow = new Widget.Window({ - namespace: "apps-window", - layer: Astal.Layer.OVERLAY, - exclusivity: Astal.Exclusivity.IGNORE, - anchor: TOP | LEFT | RIGHT | BOTTOM, - visible: false, - keymode: Astal.Keymode.EXCLUSIVE, - onDestroy: () => searchSubscription(), - setup: (window) => { - const flowbox = new Gtk.FlowBox({ - rowSpacing: 6, - homogeneous: true, - columnSpacing: 6, - expand: false, - orientation: Gtk.Orientation.HORIZONTAL, - visible: true - } as Gtk.FlowBox.ConstructorProps); +export const AppsWindow = (mon: number): (Widget.Window) => { + const searchString = new Variable(""); + let searchSubscription: () => void; - const entry = new Widget.Entry({ - className: "entry", - halign: Gtk.Align.CENTER, - primary_icon_name: "system-search", - onChanged: (entry) => { - searchString.set(entry.text); - } - } as Widget.EntryProps); + return new Widget.Window({ + namespace: "apps-window", + layer: Astal.Layer.OVERLAY, + exclusivity: Astal.Exclusivity.IGNORE, + anchor: TOP | LEFT | RIGHT | BOTTOM, + keymode: Astal.Keymode.EXCLUSIVE, + monitor: mon, + onDestroy: () => { + searchString.set(""); + searchSubscription() + }, + setup: (window) => { + const flowbox = new Gtk.FlowBox({ + rowSpacing: 6, + homogeneous: true, + columnSpacing: 6, + expand: false, + orientation: Gtk.Orientation.HORIZONTAL, + visible: true + } as Gtk.FlowBox.ConstructorProps); - searchSubscription = searchString.subscribe((str: string) => { - const results: Array = getAstalApps().fuzzy_query(str); + const entry = new Widget.Entry({ + className: "entry", + halign: Gtk.Align.CENTER, + primary_icon_name: "system-search", + onChanged: (entry) => { + searchString.set(entry.text); + } + } as Widget.EntryProps); - // Destroy is handled by GnomeJS - flowbox.get_children().map(flowboxChild => flowbox.remove(flowboxChild)); + searchSubscription = searchString.subscribe((str: string) => { + const results: Array = getAstalApps().fuzzy_query(str); - results.map(app => { - flowbox.insert(new Widget.Button({ - onClick: (_button, event: Astal.ClickEvent) => { - if(event.button === Astal.MouseButton.PRIMARY) { - searchString.set(""); - entry.text = ""; - window.hide(); - cleanExec(app); + // Destroy is handled by GnomeJS + flowbox.get_children().map(flowboxChild => flowbox.remove(flowboxChild)); - return; - } + results.map(app => { + flowbox.insert(new Widget.Button({ + onClick: (_button, event: Astal.ClickEvent) => { + if(event.button === Astal.MouseButton.PRIMARY) { + searchString.set(""); + entry.text = ""; + window.close(); + cleanExec(app); - // select app launch options TODO - }, - child: new Widget.Box({ - orientation: Gtk.Orientation.VERTICAL, - children: [ - new Widget.Icon({ - className: "icon", - expand: true, - icon: app.get_icon_name() - } as Widget.IconProps), - new Widget.Label({ - className: "name", - truncate: true, - label: app.get_name() - } as Widget.LabelProps) - ] - } as Widget.BoxProps) - } as Widget.ButtonProps), -1); + return; + } - const flowboxchild = flowbox.get_children()[flowbox.get_children().length-1]; - flowboxchild.set_valign(Gtk.Align.START); + // select app launch options TODO + }, + child: new Widget.Box({ + orientation: Gtk.Orientation.VERTICAL, + children: [ + new Widget.Icon({ + className: "icon", + expand: true, + icon: app.get_icon_name() + } as Widget.IconProps), + new Widget.Label({ + className: "name", + truncate: true, + label: app.get_name() + } as Widget.LabelProps) + ] + } as Widget.BoxProps) + } as Widget.ButtonProps), -1); + + const flowboxchild = flowbox.get_children()[flowbox.get_children().length-1]; + flowboxchild.set_valign(Gtk.Align.START); + }); + + const firstChild = flowbox.get_child_at_index(0); + firstChild && flowbox.select_child(firstChild); }); - const firstChild = flowbox.get_child_at_index(0); - firstChild && flowbox.select_child(firstChild); - }); - - window.add(new Widget.EventBox({ - onClick: () => { - searchString.set(""); - entry.text = ""; - window.hide(); - }, - onKeyPressEvent: (_, event: Gdk.Event) => { - if(event.get_keyval()[1] === Gdk.KEY_Escape) { + window.add(new Widget.EventBox({ + onClick: () => { searchString.set(""); entry.text = ""; - window.hide(); - } - }, - child: new Widget.Box({ - className: "apps-window-container", - expand: true, - orientation: Gtk.Orientation.VERTICAL, - children: [ - entry, - new Widget.Scrollable({ - vscroll: Gtk.PolicyType.AUTOMATIC, - hscroll: Gtk.PolicyType.NEVER, - expand: true, - child: flowbox - } as Widget.ScrollableProps) - ] - } as Widget.BoxProps) - } as Widget.EventBoxProps)); - } -} as Widget.WindowProps); + window.close(); + }, + onKeyPressEvent: (_, event: Gdk.Event) => { + if(event.get_keyval()[1] === Gdk.KEY_Escape) { + searchString.set(""); + entry.text = ""; + window.close(); + } + }, + child: new Widget.Box({ + className: "apps-window-container", + expand: true, + orientation: Gtk.Orientation.VERTICAL, + children: [ + entry, + new Widget.Scrollable({ + vscroll: Gtk.PolicyType.AUTOMATIC, + hscroll: Gtk.PolicyType.NEVER, + expand: true, + child: flowbox + } as Widget.ScrollableProps) + ] + } as Widget.BoxProps) + } as Widget.EventBoxProps)); + } + }); +} diff --git a/ags/window/Bar.ts b/ags/window/Bar.ts index 3c74377..dd3a2f1 100644 --- a/ags/window/Bar.ts +++ b/ags/window/Bar.ts @@ -1,22 +1,22 @@ import { Astal, Gtk, Widget } from "astal/gtk3"; -import { Clock } from "../widget/bar/Clock"; import { Tray } from "../widget/bar/Tray"; import { Workspaces } from "../widget/bar/Workspaces"; import { FocusedClient } from "../widget/bar/FocusedClient"; import { Media } from "../widget/bar/Media"; -import { Status } from "../widget/bar/Status"; import { Apps } from "../widget/bar/Apps"; +import { Clock } from "../widget/bar/Clock"; +import { Status } from "../widget/bar/Status"; -export const Bar: Widget.Window = new Widget.Window({ - monitor: 0, +export const Bar = (mon: number) => new Widget.Window({ namespace: "top-bar", anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT, layer: Astal.Layer.TOP, exclusivity: Astal.Exclusivity.EXCLUSIVE, heightRequest: 46, - canFocus: false, + monitor: mon, visible: true, + canFocus: false, child: new Widget.Box({ className: "bar-container", child: new Widget.CenterBox({ diff --git a/ags/window/CenterWindow.ts b/ags/window/CenterWindow.ts index 1b24a57..46d6954 100644 --- a/ags/window/CenterWindow.ts +++ b/ags/window/CenterWindow.ts @@ -1,73 +1,66 @@ import { Gtk, Widget } from "astal/gtk3"; -import { bind, GLib } from "astal"; +import { GLib } from "astal"; import { getDateTime } from "../scripts/time"; -import { BigMedia } from "../widget/center-window/BigMedia"; import { Separator, SeparatorProps } from "../widget/Separator"; import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; +import { BigMedia } from "../widget/center-window/BigMedia"; -const BigMediaWidget = BigMedia(); - -export const CenterWindow: Widget.Window = PopupWindow({ - className: "center-window", +export const CenterWindow = (mon: number) => PopupWindow({ + className: "center-window-container", namespace: "center-window", - monitor: 0, - visible: false, marginTop: 10, valign: Gtk.Align.START, halign: Gtk.Align.CENTER, - child: new Widget.Box({ - className: "center-window-container", - children: [ - new Widget.Box({ - className: "vertical left", - orientation: Gtk.Orientation.VERTICAL, - children: [ - new Widget.Box({ - className: "top", - orientation: Gtk.Orientation.VERTICAL, - valign: Gtk.Align.START, - children: [ - new Widget.Label({ - className: "time", - label: getDateTime().as((dateTime: GLib.DateTime) => - dateTime.format("%H:%M")) - } as Widget.LabelProps), - new Widget.Label({ - className: "date", - label: getDateTime().as((dateTime: GLib.DateTime) => - dateTime.format("%A, %B %d")) - } as Widget.LabelProps) - ] - } as Widget.BoxProps), - new Widget.Box({ - className: "calendar-box", - vexpand: false, - hexpand: true, - valign: Gtk.Align.START, - child: new Gtk.Calendar({ - visible: true, - show_heading: true, - show_day_names: true, - show_week_numbers: false - } as Gtk.Calendar.ConstructorProps) - } as Widget.BoxProps) - ] - } as Widget.BoxProps), - Separator({ - visible: bind(BigMediaWidget, "visible"), - orientation: Gtk.Orientation.HORIZONTAL, - alpha: .5, - cssColor: "gray", - size: 1 - } as SeparatorProps), - new Widget.Box({ - className: "vertical right", - orientation: Gtk.Orientation.VERTICAL, - children: [ - BigMediaWidget - ] - } as Widget.BoxProps) - ] - } as Widget.BoxProps) + monitor: mon, + children: [ + new Widget.Box({ + className: "vertical left", + orientation: Gtk.Orientation.VERTICAL, + children: [ + new Widget.Box({ + className: "top", + orientation: Gtk.Orientation.VERTICAL, + valign: Gtk.Align.START, + children: [ + new Widget.Label({ + className: "time", + label: getDateTime().as((dateTime: GLib.DateTime) => + dateTime.format("%H:%M")) + } as Widget.LabelProps), + new Widget.Label({ + className: "date", + label: getDateTime().as((dateTime: GLib.DateTime) => + dateTime.format("%A, %B %d")) + } as Widget.LabelProps) + ] + } as Widget.BoxProps), + new Widget.Box({ + className: "calendar-box", + vexpand: false, + hexpand: true, + valign: Gtk.Align.START, + child: new Gtk.Calendar({ + visible: true, + showHeading: true, + showDayNames: true, + showWeekNumbers: false + } as Gtk.Calendar.ConstructorProps) + } as Widget.BoxProps) + ] + } as Widget.BoxProps), + Separator({ + orientation: Gtk.Orientation.HORIZONTAL, + alpha: .5, + cssColor: "gray", + size: 1 + } as SeparatorProps), + new Widget.Box({ + className: "vertical right", + orientation: Gtk.Orientation.VERTICAL, + children: [ + BigMedia() + ] + } as Widget.BoxProps) + ] } as PopupWindowProps); diff --git a/ags/window/ControlCenter.ts b/ags/window/ControlCenter.ts index 31f2e28..09cdbff 100644 --- a/ags/window/ControlCenter.ts +++ b/ags/window/ControlCenter.ts @@ -4,11 +4,11 @@ import { Tiles } from "../widget/control-center/Tiles"; import { Sliders } from "../widget/control-center/Sliders"; import { hidePages, PagesWidget } from "../widget/control-center/Pages"; import { NotifHistory } from "../widget/control-center/NotifHistory"; +import { Windows } from "../windows"; -const connections: Array = []; const { TOP, LEFT, BOTTOM, RIGHT } = Astal.WindowAnchor; -export const ControlCenter = new Widget.Window({ +export const ControlCenter = (mon: number) => new Widget.Window({ namespace: "control-center", className: "control-center", anchor: TOP | BOTTOM | LEFT | RIGHT, @@ -16,22 +16,23 @@ export const ControlCenter = new Widget.Window({ keymode: Astal.Keymode.EXCLUSIVE, layer: Astal.Layer.OVERLAY, focusOnMap: true, - visible: false, - monitor: 0, - onDestroy: (_) => connections.map(id => _.disconnect(id)), + monitor: mon, + onDestroy: (_) => { + hidePages(); + }, onButtonPressEvent: (_, event: Gdk.Event) => { const [, posX, posY] = event.get_coords(); const childAllocation = _.get_child()!.get_allocation(); if((posX < childAllocation.x || posX > (childAllocation.x + childAllocation.width)) || (posY < childAllocation.y || posY > (childAllocation.y + childAllocation.height))) { - _.hide(); + Windows.close("control-center"); hidePages(); } }, onKeyPressEvent: (_, event: Gdk.Event) => { if(event.get_keyval()[1] === Gdk.KEY_Escape) { - _.hide(); + Windows.close("control-center"); hidePages(); } }, @@ -54,17 +55,13 @@ export const ControlCenter = new Widget.Window({ widthRequest: 400, vexpand: false, children: [ - QuickActions, - Sliders, - Tiles, - PagesWidget + QuickActions(), + Sliders(), + Tiles(), + PagesWidget() ] } as Widget.BoxProps), - NotifHistory + NotifHistory() ] } as Widget.BoxProps) } as Widget.WindowProps); - -connections.push(ControlCenter.connect("hide", (_) => { - hidePages(); -})); diff --git a/ags/window/FloatingNotifications.ts b/ags/window/FloatingNotifications.ts index 20fac89..937f8e5 100644 --- a/ags/window/FloatingNotifications.ts +++ b/ags/window/FloatingNotifications.ts @@ -1,42 +1,23 @@ import { Astal, Gtk, Widget } from "astal/gtk3"; -import AstalNotifd from "gi://AstalNotifd"; import { bind } from "astal/binding"; import { Notifications } from "../scripts/notifications"; import { NotificationWidget } from "../widget/Notification"; -const connections: Array = []; -export const FloatingNotifications: Widget.Window = new Widget.Window({ +export const FloatingNotifications = (mon: number) => new Widget.Window({ namespace: "floating-notifications", canFocus: false, anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT, - monitor: 0, + monitor: mon, layer: Astal.Layer.OVERLAY, - visible: false, widthRequest: 450, exclusivity: Astal.Exclusivity.NORMAL, - setup: (window) => { - connections.push( - Notifications.getDefault().connect("notification-added", (_, _notif: AstalNotifd.Notification) => { - !window.is_visible() && window.show(); - }), - Notifications.getDefault().connect("notification-removed", (_: Notifications, _id: number) => { - window.is_visible() && _.notifications.length === 0 && window.hide() - window.isFocus = false; - }) - ); - }, - onDestroy: () => connections.map(id => Notifications.getDefault().disconnect(id)), child: new Widget.Box({ className: "floating-notifications-container", orientation: Gtk.Orientation.VERTICAL, homogeneous: false, visible: bind(Notifications.getDefault(), "notifications").as(notifs => notifs.length > 0), children: bind(Notifications.getDefault(), "notifications").as((notifs) => - notifs.map((item) => - NotificationWidget(item, - () => Notifications.getDefault().removeNotification(item)) - ) - ), + notifs.map((item) => NotificationWidget(item, () => Notifications.getDefault().removeNotification(item)))), } as Widget.BoxProps) } as Widget.WindowProps); diff --git a/ags/window/LogoutMenu.ts b/ags/window/LogoutMenu.ts index 8908951..6a6236b 100644 --- a/ags/window/LogoutMenu.ts +++ b/ags/window/LogoutMenu.ts @@ -6,14 +6,13 @@ import { AskPopup } from "../widget/AskPopup"; const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; -export const LogoutMenu: Widget.Window = new Widget.Window({ +export const LogoutMenu = (mon: number) => new Widget.Window({ namespace: "logout-menu", anchor: TOP | LEFT | RIGHT | BOTTOM, layer: Astal.Layer.OVERLAY, exclusivity: Astal.Exclusivity.IGNORE, keymode: Astal.Keymode.EXCLUSIVE, - monitor: 0, - visible: false, + monitor: mon, onKeyPressEvent: (_, event: Gdk.Event) => { event.get_keyval()[1] === Gdk.KEY_Escape && _.hide(); @@ -57,8 +56,6 @@ export const LogoutMenu: Widget.Window = new Widget.Window({ onClick: () => AskPopup({ title: "Power Off", text: "Are you sure you want to power off? Unsaved work will be lost.", - cancelText: "No! Let me go back", - acceptText: "Yes, shutdown", onAccept: () => execAsync("systemctl poweroff") }) } as Widget.ButtonProps), @@ -68,8 +65,6 @@ export const LogoutMenu: Widget.Window = new Widget.Window({ onClick: () => AskPopup({ title: "Reboot", text: "Are you sure you want to Reboot? Unsaved work will be lost.", - cancelText: "No! Let me go back", - acceptText: "Yes, reboot", onAccept: () => execAsync("systemctl reboot") }) } as Widget.ButtonProps), @@ -79,8 +74,6 @@ export const LogoutMenu: Widget.Window = new Widget.Window({ onClick: () => AskPopup({ title: "Suspend", text: "Are you sure you want to Suspend?", - cancelText: "No! Let me go back", - acceptText: "Yes, suspend", onAccept: () => execAsync("systemctl suspend") }) } as Widget.ButtonProps), @@ -90,8 +83,6 @@ export const LogoutMenu: Widget.Window = new Widget.Window({ onClick: () => AskPopup({ title: "Log out", text: "Are you sure you want to log out? Your session will be ended.", - cancelText: "No! Let me go back", - acceptText: "Yes, please log out", onAccept: () => execAsync(`sh -c "loginctl terminate-user ${GLib.getenv("USER") || "$USER"}"`) }) } as Widget.ButtonProps), diff --git a/ags/window/OSD.ts b/ags/window/OSD.ts index 8e16562..9db81a9 100644 --- a/ags/window/OSD.ts +++ b/ags/window/OSD.ts @@ -20,15 +20,15 @@ export function setOSDMode(newMode: OSDModes): void { osdMode.set(newMode); } -export const OSD: Widget.Window = new Widget.Window({ +export const OSD = (mon: number) => new Widget.Window({ namespace: "osd", layer: Astal.Layer.OVERLAY, anchor: Astal.WindowAnchor.BOTTOM, canFocus: false, - margin_bottom: 80, - monitor: 0, - visible: false, + marginBottom: 80, focusOnClick: false, + clickThrough: true, + monitor: mon, child: new Widget.Box({ className: "osd", children: [ diff --git a/ags/windows.ts b/ags/windows.ts index b0eb897..9585290 100644 --- a/ags/windows.ts +++ b/ags/windows.ts @@ -1,58 +1,192 @@ -import { Gtk } from "astal/gtk3"; +import { Widget } from "astal/gtk3"; import { Bar } from "./window/Bar"; import { OSD } from "./window/OSD"; import { ControlCenter } from "./window/ControlCenter"; import { CenterWindow } from "./window/CenterWindow"; -import { FloatingNotifications } from "./window/FloatingNotifications"; -import { GObject, register } from "astal"; import { LogoutMenu } from "./window/LogoutMenu"; +import { FloatingNotifications } from "./window/FloatingNotifications"; import { AppsWindow } from "./window/AppsWindow"; +import AstalHyprland from "gi://AstalHyprland"; +import { GObject } from "astal"; /** * get open windows / interact with windows(e.g.: close, open or toggle) */ -@register({ GTypeName: "Windows" }) -class WindowsClass extends GObject.Object { - private static windowsMap: Map = new Map(); +export const Windows = GObject.registerClass({ + GTypeName: "Windows", + Signals: { + "open": { param_types: [ GObject.TYPE_STRING ] }, + "close": { param_types: [ GObject.TYPE_STRING ] } + }, + Properties: { + "open-windows": GObject.ParamSpec.jsobject( + "open-windows", + "Open Windows", + "A Readonly object that stores open GTKLayerShell Windows", + GObject.ParamFlags.READABLE + ) + } +}, class Windows extends GObject.Object { + #openWindows: Record> = {}; + static #instance: (Windows | null); - static { - this.setWindow("bar", Bar); - this.setWindow("osd", OSD); - this.setWindow("control-center", ControlCenter); - this.setWindow("center-window", CenterWindow); - this.setWindow("logout-menu", LogoutMenu); - this.setWindow("floating-notifications", FloatingNotifications); - this.setWindow("apps-window", AppsWindow); + #windows: Record (Widget.Window | Array))> = { + "bar": this.createWindowForMonitors(Bar), + "osd": this.createWindowForFocusedMonitor(OSD), + "control-center": this.createWindowForFocusedMonitor(ControlCenter), + "center-window": this.createWindowForFocusedMonitor(CenterWindow), + "logout-menu": this.createWindowForFocusedMonitor(LogoutMenu), + "floating-notifications": this.createWindowForFocusedMonitor(FloatingNotifications), + "apps-window": this.createWindowForFocusedMonitor(AppsWindow) + }; + + #windowConnections: Record | Array>)> = {}; + + get windows() { return this.#windows; } + get openWindows(): Record> { return this.#openWindows; }; + + vfunc_dispose() { + for(const name of Object.keys(this.#windowConnections)) { + const window = this.openWindows[name]; + if(!window) continue; + + this.disconnectWindow(name); + } } - public static setWindow(name: string, window: Gtk.Window): void { - WindowsClass.windowsMap.set(name, window); + private disconnectWindow(name: keyof typeof this.windows) { + const window = this.openWindows[name]; + if(!window) { + console.log("couldn't disconnect, window is not open"); + return; + } + + this.#windowConnections[name].map((id: Array | number) => { + if(Array.isArray(window)) { + window.map((win, i) => win.disconnect((id as Array)[i])); + return; + } + + window.disconnect(id as number); + }); + + delete this.#windowConnections[name]; } - public static getWindow(name: string): (Gtk.Window|undefined) { - return WindowsClass.windowsMap.get(name); + private connectWindow(name: keyof typeof this.windows) { + if(Object.hasOwn(this.#windowConnections, name)) return; + if(!this.openWindows?.[name]) { + console.log(`${name} is not open, will not connect`); + return; + } + + if(Array.isArray(this.openWindows[name])) { + this.#windowConnections[name] = this.openWindows[name].map(win => [ + win.connect("map", (window) => { + this.#openWindows[name] = window; + }), + win.connect("destroy", () => { + this.disconnectWindow(name); + delete this.#openWindows[name]; + }) + ]); + + return; + } + + this.#windowConnections[name] = [ + this.openWindows[name].connect("map", (window) => { + this.#openWindows[name] = window; + }), + this.openWindows[name].connect("destroy", () => { + this.disconnectWindow(name); + delete this.#openWindows[name]; + }) + ]; } - public static getList(): Map { - return WindowsClass.windowsMap; + public static getDefault(): Windows { + if(!this.#instance) + this.#instance = new Windows(); + + return this.#instance; } - public static open(window: Gtk.Window): void { - !WindowsClass.isVisible(window) && window.show(); + public createWindowForMonitors(windowFun: (mon: number) => Widget.Window): (() => Array) { + return () => AstalHyprland.get_default().get_monitors().map(mon => + windowFun(mon.id)); } - public static isVisible(window: Gtk.Window): boolean { - return window.get_visible(); + public createWindowForFocusedMonitor(windowFun: (mon: number) => Widget.Window): (() => Widget.Window) { + return () => windowFun(AstalHyprland.get_default().get_monitors().filter(mon => mon.focused)[0].id); } - public static close(window: Gtk.Window): void { - WindowsClass.isVisible(window) && window.hide(); + public addWindow(name: string, window: (() => (Widget.Window | Array))): void { + this.#windows[name] = window; } - public static toggle(window: Gtk.Window): void { - window.is_visible() ? WindowsClass.close(window) : WindowsClass.open(window); + public hasWindow(name: keyof typeof this.windows): boolean { + return Boolean(this.windows?.[name as keyof typeof this.windows]); } -} -export { WindowsClass as Windows }; + public getWindow(name: (keyof typeof this.windows | string)): ((() => (Widget.Window | Array)) | undefined) { + return this.windows?.[name as keyof typeof this.windows]; + } + + public getOpenWindow(name: (keyof typeof this.openWindows)): (Widget.Window | Array | undefined) { + return this.openWindows?.[name as keyof typeof this.openWindows]; + } + + public getWindows(): Array<(() => (Widget.Window | Array))> { + return Object.values(this.windows); + } + + public isVisible(name: keyof typeof this.windows): boolean { + return Object.hasOwn(this.#openWindows, name); + } + + public open(name: keyof typeof this.windows): void { + if(this.isVisible(name)) return; + + let window: (() => (Widget.Window | Array)) = this.getWindow(name)!; + const openWindows: (Array | Widget.Window) = window(); + this.#openWindows[name] = openWindows; + + this.connectWindow(name); + + this.emit("open", name); + this.notify("open-windows"); + + if(Array.isArray(openWindows)) { + openWindows.map(win => win.show()); + return; + } + + openWindows.show(); + } + + public close(name: keyof typeof this.windows): void { + if(!this.isVisible(name)) return; + + this.disconnectWindow(name); + + const windows = this.#openWindows[name]; + delete this.#openWindows[name]; + + if(Array.isArray(windows)) { + windows.map(win => win.close()); + this.emit("close", name); + this.notify("open-windows"); + return; + } + + windows.close(); + this.emit("close", name); + this.notify("open-windows"); + } + + public toggle(name: keyof typeof this.windows): void { + this.isVisible(name) ? this.close(name) : this.open(name); + } +}).getDefault();