diff options
-rw-r--r-- | flake.nix | 1 | ||||
-rw-r--r-- | lang/gjs/src/_app.ts | 122 | ||||
-rw-r--r-- | lang/gjs/src/gobject.ts | 9 | ||||
-rw-r--r-- | lang/gjs/src/gtk3/app.ts | 104 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/app.ts | 8 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/astalify.ts | 2 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/index.ts | 10 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/jsx-runtime.ts | 2 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/widget.ts | 1 | ||||
-rw-r--r-- | lang/gjs/tsconfig.json | 2 | ||||
-rw-r--r-- | lib/astal/gtk4/gir.py | 58 | ||||
-rw-r--r-- | lib/astal/gtk4/meson.build | 18 | ||||
-rw-r--r-- | lib/astal/gtk4/src/application.vala | 241 | ||||
-rw-r--r-- | lib/astal/gtk4/src/config.vala.in | 6 | ||||
-rw-r--r-- | lib/astal/gtk4/src/meson.build | 89 | ||||
-rw-r--r-- | lib/astal/gtk4/src/widget/window.vala | 261 | ||||
-rw-r--r-- | lib/astal/gtk4/version | 1 |
17 files changed, 828 insertions, 107 deletions
@@ -49,6 +49,7 @@ io = mkPkg "astal" ./lib/astal/io []; astal3 = mkPkg "astal" ./lib/astal/gtk3 [self.packages.${system}.io gtk3 gtk-layer-shell]; + astal4 = mkPkg "astal" ./lib/astal/gtk4 [self.packages.${system}.io gtk4 gtk4-layer-shell]; apps = mkPkg "astal-apps" ./lib/apps [json-glib]; auth = mkPkg "astal-auth" ./lib/auth [pam]; battery = mkPkg "astal-battery" ./lib/battery [json-glib]; diff --git a/lang/gjs/src/_app.ts b/lang/gjs/src/_app.ts new file mode 100644 index 0000000..82e8bb5 --- /dev/null +++ b/lang/gjs/src/_app.ts @@ -0,0 +1,122 @@ +import { setConsoleLogDomain } from "console" +import { exit, programArgs } from "system" +import IO from "gi://AstalIO" +import GObject from "gi://GObject" +import Gio from "gi://Gio?version=2.0" +import type Astal3 from "gi://Astal?version=3.0" +import type Astal4 from "gi://Astal?version=4.0" + +type Config = Partial<{ + instanceName: string + css: string + icons: string + gtkTheme: string + iconTheme: string + cursorTheme: string + hold: boolean + requestHandler(request: string, res: (response: any) => void): void + main(...args: string[]): void + client(message: (msg: string) => string, ...args: string[]): void +}> + +interface Astal3JS extends Astal3.Application { + eval(body: string): Promise<any> + requestHandler: Config["requestHandler"] + apply_css(style: string, reset?: boolean): void + quit(code?: number): void + start(config?: Config): void +} + +interface Astal4JS extends Astal4.Application { + eval(body: string): Promise<any> + requestHandler?: Config["requestHandler"] + apply_css(style: string, reset?: boolean): void + quit(code?: number): void + start(config?: Config): void +} + +type App3 = typeof Astal3.Application +type App4 = typeof Astal4.Application + +export function mkApp<App extends App3>(App: App): Astal3JS +export function mkApp<App extends App4>(App: App): Astal4JS + +export function mkApp(App: App3 | App4) { + return new (class AstalJS extends App { + static { GObject.registerClass({ GTypeName: "AstalJS" }, this as any) } + + eval(body: string): Promise<any> { + return new Promise((res, rej) => { + try { + const fn = Function(`return (async function() { + ${body.includes(";") ? body : `return ${body};`} + })`) + fn()().then(res).catch(rej) + } + catch (error) { + rej(error) + } + }) + } + + requestHandler?: Config["requestHandler"] + + vfunc_request(msg: string, conn: Gio.SocketConnection): void { + if (typeof this.requestHandler === "function") { + this.requestHandler(msg, (response) => { + IO.write_sock(conn, String(response), (_, res) => + IO.write_sock_finish(res), + ) + }) + } + else { + super.vfunc_request(msg, conn) + } + } + + apply_css(style: string, reset = false) { + super.apply_css(style, reset) + } + + quit(code?: number): void { + super.quit() + exit(code ?? 0) + } + + start({ requestHandler, css, hold, main, client, icons, ...cfg }: Config = {}) { + const app = this as unknown as InstanceType<App3 | App4> + + client ??= () => { + print(`Astal instance "${app.instanceName}" already running`) + exit(1) + } + + Object.assign(this, cfg) + setConsoleLogDomain(app.instanceName) + + this.requestHandler = requestHandler + app.connect("activate", () => { + main?.(...programArgs) + }) + + try { + app.acquire_socket() + } + catch (error) { + return client(msg => IO.send_message(app.instanceName, msg)!, ...programArgs) + } + + if (css) + this.apply_css(css, false) + + if (icons) + app.add_icons(icons) + + hold ??= true + if (hold) + app.hold() + + app.runAsync([]) + } + }) +} diff --git a/lang/gjs/src/gobject.ts b/lang/gjs/src/gobject.ts index aeb3d9e..72093b0 100644 --- a/lang/gjs/src/gobject.ts +++ b/lang/gjs/src/gobject.ts @@ -40,6 +40,11 @@ type MetaInfo = GObject.MetaInfo<never, Array<{ $gtype: GObject.GType }>, never> export function register(options: MetaInfo = {}) { return function (cls: GObjectConstructor) { + const t = options.Template + if (typeof t === "string" && !t.startsWith("resource://") && !t.startsWith("file://")) { + options.Template = new TextEncoder().encode(t) + } + GObject.registerClass({ Signals: { ...cls[meta]?.Signals }, Properties: { ...cls[meta]?.Properties }, @@ -95,10 +100,10 @@ export function property(declaration: PropertyDeclaration = Object) { } export function signal(...params: Array<{ $gtype: GObject.GType } | typeof Object>): - (target: any, signal: any, desc?: PropertyDescriptor) => void +(target: any, signal: any, desc?: PropertyDescriptor) => void export function signal(declaration?: SignalDeclaration): - (target: any, signal: any, desc?: PropertyDescriptor) => void +(target: any, signal: any, desc?: PropertyDescriptor) => void export function signal( declaration?: SignalDeclaration | { $gtype: GObject.GType } | typeof Object, diff --git a/lang/gjs/src/gtk3/app.ts b/lang/gjs/src/gtk3/app.ts index 1191dc4..ccf714c 100644 --- a/lang/gjs/src/gtk3/app.ts +++ b/lang/gjs/src/gtk3/app.ts @@ -1,105 +1,7 @@ -import IO from "gi://AstalIO" -import GObject from "gi://GObject" -import Astal from "gi://Astal?version=3.0" -import Gio from "gi://Gio?version=2.0" import Gtk from "gi://Gtk?version=3.0" +import Astal from "gi://Astal?version=3.0" +import { mkApp } from "src/_app" Gtk.init(null) -type RequestHandler = { - (request: string, res: (response: any) => void): void -} - -type Config = Partial<{ - icons: string - instanceName: string - gtkTheme: string - iconTheme: string - cursorTheme: string - css: string - requestHandler: RequestHandler - main(...args: string[]): void - client(message: (msg: string) => string, ...args: string[]): void - hold: boolean -}> - -import { setConsoleLogDomain } from "console" -import { exit, programArgs } from "system" - -export default new (class AstalJS extends Astal.Application { - static { GObject.registerClass({ GTypeName: "AstalJS" }, this) } - - eval(body: string): Promise<any> { - return new Promise((res, rej) => { - try { - const fn = Function(`return (async function() { - ${body.includes(";") ? body : `return ${body};`} - })`) - fn()() - .then(res) - .catch(rej) - } - catch (error) { - rej(error) - } - }) - } - - requestHandler?: RequestHandler - - vfunc_request(msg: string, conn: Gio.SocketConnection): void { - if (typeof this.requestHandler === "function") { - this.requestHandler(msg, (response) => { - IO.write_sock(conn, String(response), (_, res) => - IO.write_sock_finish(res), - ) - }) - } - else { - super.vfunc_request(msg, conn) - } - } - - apply_css(style: string, reset = false) { - super.apply_css(style, reset) - } - - quit(code?: number): void { - super.quit() - exit(code ?? 0) - } - - start({ requestHandler, css, hold, main, client, icons, ...cfg }: Config = {}) { - client ??= () => { - print(`Astal instance "${this.instanceName}" already running`) - exit(1) - } - - Object.assign(this, cfg) - setConsoleLogDomain(this.instanceName) - - this.requestHandler = requestHandler - this.connect("activate", () => { - main?.(...programArgs) - }) - - try { - this.acquire_socket() - } - catch (error) { - return client(msg => IO.send_message(this.instanceName, msg)!, ...programArgs) - } - - if (css) - this.apply_css(css, false) - - if (icons) - this.add_icons(icons) - - hold ??= true - if (hold) - this.hold() - - this.runAsync([]) - } -}) +export default mkApp(Astal.Application) diff --git a/lang/gjs/src/gtk4/app.ts b/lang/gjs/src/gtk4/app.ts index d931f73..ad3c1cb 100644 --- a/lang/gjs/src/gtk4/app.ts +++ b/lang/gjs/src/gtk4/app.ts @@ -1 +1,7 @@ -// TODO: gtk4 +import Gtk from "gi://Gtk?version=4.0" +import Astal from "gi://Astal?version=4.0" +import { mkApp } from "src/_app" + +Gtk.init() + +export default mkApp(Astal.Application) diff --git a/lang/gjs/src/gtk4/astalify.ts b/lang/gjs/src/gtk4/astalify.ts index d931f73..6c8ea4d 100644 --- a/lang/gjs/src/gtk4/astalify.ts +++ b/lang/gjs/src/gtk4/astalify.ts @@ -1 +1 @@ -// TODO: gtk4 +// TODO: diff --git a/lang/gjs/src/gtk4/index.ts b/lang/gjs/src/gtk4/index.ts index d931f73..3b1f737 100644 --- a/lang/gjs/src/gtk4/index.ts +++ b/lang/gjs/src/gtk4/index.ts @@ -1 +1,9 @@ -// TODO: gtk4 +import Astal from "gi://Astal?version=4.0" +import Gtk from "gi://Gtk?version=4.0" +import Gdk from "gi://Gdk?version=4.0" +// import astalify, { type ConstructProps } from "./astalify.js" + +export { Astal, Gtk, Gdk } +export { default as App } from "./app.js" +// export { astalify, ConstructProps } +// export * as Widget from "./widget.js" diff --git a/lang/gjs/src/gtk4/jsx-runtime.ts b/lang/gjs/src/gtk4/jsx-runtime.ts index d931f73..6c8ea4d 100644 --- a/lang/gjs/src/gtk4/jsx-runtime.ts +++ b/lang/gjs/src/gtk4/jsx-runtime.ts @@ -1 +1 @@ -// TODO: gtk4 +// TODO: diff --git a/lang/gjs/src/gtk4/widget.ts b/lang/gjs/src/gtk4/widget.ts new file mode 100644 index 0000000..6c8ea4d --- /dev/null +++ b/lang/gjs/src/gtk4/widget.ts @@ -0,0 +1 @@ +// TODO: diff --git a/lang/gjs/tsconfig.json b/lang/gjs/tsconfig.json index 171e75b..3d5ade5 100644 --- a/lang/gjs/tsconfig.json +++ b/lang/gjs/tsconfig.json @@ -11,6 +11,8 @@ "include": [ "@girs", "src/*.ts", + "src/gtk3/*", + "src/gtk4/*", "index.ts", ] } diff --git a/lib/astal/gtk4/gir.py b/lib/astal/gtk4/gir.py new file mode 100644 index 0000000..9ef680f --- /dev/null +++ b/lib/astal/gtk4/gir.py @@ -0,0 +1,58 @@ +""" +Vala's generated gir does not contain comments, +so we use valadoc to generate them. However, they are formatted +for valadoc and not gi-docgen so we need to fix it. +""" + +import xml.etree.ElementTree as ET +import html +import sys +import subprocess + + +def fix_gir(name: str, gir: str, out: str): + namespaces = { + "": "http://www.gtk.org/introspection/core/1.0", + "c": "http://www.gtk.org/introspection/c/1.0", + "glib": "http://www.gtk.org/introspection/glib/1.0", + } + for prefix, uri in namespaces.items(): + ET.register_namespace(prefix, uri) + + tree = ET.parse(gir) + root = tree.getroot() + + for doc in root.findall(".//doc", namespaces): + if doc.text: + doc.text = ( + html.unescape(doc.text).replace("<para>", "").replace("</para>", "") + ) + + if (inc := root.find("c:include", namespaces)) is not None: + inc.set("name", f"{name}.h") + else: + print("no c:include tag found", file=sys.stderr) + exit(1) + + tree.write(out, encoding="utf-8", xml_declaration=True) + + +def valadoc(name: str, gir: str, args: list[str]): + cmd = ["valadoc", "-o", "docs", "--package-name", name, "--gir", gir, *args] + try: + subprocess.run(cmd, check=True, text=True, capture_output=True) + except subprocess.CalledProcessError as e: + print(e.stderr, file=sys.stderr) + exit(1) + + +if __name__ == "__main__": + name = sys.argv[1] + in_out = sys.argv[2].split(":") + args = sys.argv[3:] + + gir = in_out[0] + out = in_out[1] if len(in_out) > 1 else gir + + valadoc(name, gir, args) + fix_gir(name, gir, out) diff --git a/lib/astal/gtk4/meson.build b/lib/astal/gtk4/meson.build new file mode 100644 index 0000000..48d3058 --- /dev/null +++ b/lib/astal/gtk4/meson.build @@ -0,0 +1,18 @@ +project( + 'astal', + 'vala', + 'c', + version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), + meson_version: '>= 0.62.0', + default_options: [ + 'warning_level=2', + 'werror=false', + 'c_std=gnu11', + ], +) + +libdir = get_option('prefix') / get_option('libdir') +pkgdatadir = get_option('prefix') / get_option('datadir') / 'astal' +girpy = files('gir.py') + +subdir('src') diff --git a/lib/astal/gtk4/src/application.vala b/lib/astal/gtk4/src/application.vala new file mode 100644 index 0000000..a6d3688 --- /dev/null +++ b/lib/astal/gtk4/src/application.vala @@ -0,0 +1,241 @@ +[DBus (name="io.Astal.Application")] +public class Astal.Application : Gtk.Application, AstalIO.Application { + private List<Gtk.CssProvider> css_providers = new List<Gtk.CssProvider>(); + private SocketService service; + private DBusConnection conn; + private string _instance_name = "astal"; + private string socket_path { get; private set; } + + /** + * Emitted when a window that has been added using + * [[email protected]_window] changes its visibility . + */ + [DBus (visible=false)] + public signal void window_toggled(Gtk.Window window); + + /** + * Get all monitors from [[email protected]]. + */ + [DBus (visible=false)] + public List<weak Gdk.Monitor> monitors { + owned get { + var mons = Gdk.Display.get_default().get_monitors(); + var list = new List<weak Gdk.Monitor>(); + for (var i = 0; i <= mons.get_n_items(); ++i) { + var mon = (Gdk.Monitor)mons.get_item(i); + if (mon != null) { + list.append(mon); + } + } + return list; + } + } + + /** + * A unique instance name. + * + * This is the identifier used by the AstalIO package and the CLI. + */ + [DBus (visible=false)] + public string instance_name { + owned get { return _instance_name; } + construct set { + _instance_name = value != null ? value : "astal"; + application_id = @"io.Astal.$_instance_name"; + } + } + + /** + * Windows that has been added to this app using [[email protected]_window]. + */ + [DBus (visible=false)] + public List<Gtk.Window> windows { + get { return get_windows(); } + } + + private Gtk.Settings settings { + get { return Gtk.Settings.get_default(); } + } + + private Gdk.Display display { + get { return Gdk.Display.get_default(); } + } + + /** + * Shortcut for [[email protected]:gtk_theme_name] + */ + [DBus (visible=false)] + public string gtk_theme { + owned get { return settings.gtk_theme_name; } + set { settings.gtk_theme_name = value; } + } + + /** + * Shortcut for [[email protected]:gtk_icon_theme_name] + */ + [DBus (visible=false)] + public string icon_theme { + owned get { return settings.gtk_icon_theme_name; } + set { settings.gtk_icon_theme_name = value; } + } + + /** + * Shortcut for [[email protected]:gtk_cursor_theme_name] + */ + [DBus (visible=false)] + public string cursor_theme { + owned get { return settings.gtk_cursor_theme_name; } + set { settings.gtk_cursor_theme_name = value; } + } + + /** + * Remove all [[email protected]] providers. + */ + [DBus (visible=false)] + public void reset_css() { + foreach(var provider in css_providers) { + Gtk.StyleContext.remove_provider_for_display(display, provider); + } + css_providers = new List<Gtk.CssProvider>(); + } + + /** + * Shortcut for [[email protected]_interactive_debugging]. + */ + public void inspector() throws DBusError, IOError { + Gtk.Window.set_interactive_debugging(true); + } + + /** + * Get a window by its [[email protected]:name] that has been added to this app + * using [[email protected]_window]. + */ + [DBus (visible=false)] + public Gtk.Window? get_window(string name) { + foreach(var win in windows) { + if (win.name == name) + return win; + } + + critical("no window with name \"%s\"".printf(name)); + return null; + } + + /** + * Toggle the visibility of a window by its [[email protected]:name] + * that has been added to this app using [[email protected]_window]. + */ + public void toggle_window(string window) throws Error { + var win = get_window(window); + if (win != null) { + win.visible = !win.visible; + } else { + throw new IOError.FAILED("window not found"); + } + } + + /** + * Add a new [[email protected]] provider. + * + * @param style Css string or a path to a css file. + */ + [DBus (visible=false)] + public void apply_css(string style, bool reset = false) { + var provider = new Gtk.CssProvider(); + + if (reset) + reset_css(); + + try { + if (FileUtils.test(style, FileTest.EXISTS)) + provider.load_from_path(style); + else + provider.load_from_string(style); + } catch (Error err) { + critical(err.message); + } + + Gtk.StyleContext.add_provider_for_display( + display, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER); + + css_providers.append(provider); + } + + /** + * Shortcut for [[email protected]_search_path]. + */ + [DBus (visible=false)] + public void add_icons(string? path) { + if (path != null) { + Gtk.IconTheme.get_for_display(display).add_search_path(path); + } + } + + /** + * Handler for an incoming request. + * + * @param msg Body of the message + * @param conn The connection which expects the response. + */ + [DBus (visible=false)] + public virtual void request(string msg, SocketConnection conn) { + AstalIO.write_sock.begin(conn, @"missing response implementation on $application_id"); + } + + /** + * Attempt to acquire the astal socket for this app identified by its [[email protected]:instance_name]. + * If the socket is in use by another app with the same name an [[email protected]_OCCUPIED] is thrown. + */ + [DBus (visible=false)] + public void acquire_socket() throws Error { + string path; + service = AstalIO.acquire_socket(this, out path); + socket_path = path; + + Bus.own_name( + BusType.SESSION, + application_id, + BusNameOwnerFlags.NONE, + (conn) => { + try { + this.conn = conn; + conn.register_object("/io/Astal/Application", this); + } catch (Error err) { + critical(err.message); + } + }, + () => {}, + () => {} + ); + } + + /** + * Quit and stop the socket if it was acquired. + */ + public new void quit() throws DBusError, IOError { + if (service != null) { + service.stop(); + service.close(); + } + + base.quit(); + } + + construct { + window_added.connect((window) => { + ulong id1, id2; + id1 = window.notify["visible"].connect(() => window_toggled(window)); + id2 = window_removed.connect((removed) => { + if (removed == window) { + window.disconnect(id1); + this.disconnect(id2); + } + }); + }); + + shutdown.connect(() => { try { quit(); } catch(Error err) {} }); + Unix.signal_add(1, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); + Unix.signal_add(2, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); + Unix.signal_add(15, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); + } +} diff --git a/lib/astal/gtk4/src/config.vala.in b/lib/astal/gtk4/src/config.vala.in new file mode 100644 index 0000000..88bfe9c --- /dev/null +++ b/lib/astal/gtk4/src/config.vala.in @@ -0,0 +1,6 @@ +namespace Astal { + public const int MAJOR_VERSION = @MAJOR_VERSION@; + public const int MINOR_VERSION = @MINOR_VERSION@; + public const int MICRO_VERSION = @MICRO_VERSION@; + public const string VERSION = "@VERSION@"; +} diff --git a/lib/astal/gtk4/src/meson.build b/lib/astal/gtk4/src/meson.build new file mode 100644 index 0000000..8aac969 --- /dev/null +++ b/lib/astal/gtk4/src/meson.build @@ -0,0 +1,89 @@ +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'Astal-' + api_version + '.gir' +typelib = 'Astal-' + api_version + '.typelib' + +vapi_dir = meson.current_source_dir() / 'vapi' +add_project_arguments(['--vapidir', vapi_dir], language: 'vala') + +config = configure_file( + input: 'config.vala.in', + output: 'config.vala', + configuration: { + 'VERSION': meson.project_version(), + 'MAJOR_VERSION': version_split[0], + 'MINOR_VERSION': version_split[1], + 'MICRO_VERSION': version_split[2], + }, +) + +deps = [ + dependency('astal-io-0.1'), + dependency('glib-2.0'), + dependency('gtk4'), + dependency('gtk4-layer-shell-0'), +] + +sources = [config] + files( + 'widget/window.vala', + 'application.vala', +) + +lib = library( + meson.project_name(), + sources, + dependencies: deps, + vala_args: ['--vapi-comments'], + vala_header: meson.project_name() + '.h', + vala_vapi: meson.project_name() + '-' + api_version + '.vapi', + version: meson.project_version(), + install: true, + install_dir: [true, true, true], +) + +pkgs = [] +foreach dep : deps + pkgs += ['--pkg=' + dep.name()] +endforeach + +gir_tgt = custom_target( + gir, + command: [ + find_program('python3'), + girpy, + meson.project_name(), + gir + ':src/' + gir, + ] + + pkgs + + sources, + input: sources, + depends: lib, + output: gir, + install: true, + install_dir: get_option('datadir') / 'gir-1.0', +) + +custom_target( + typelib, + command: [ + find_program('g-ir-compiler'), + '--output', '@OUTPUT@', + '--shared-library', libdir / '@PLAINNAME@', + meson.current_build_dir() / gir, + ], + input: lib, + output: typelib, + depends: [lib, gir_tgt], + install: true, + install_dir: libdir / 'girepository-1.0', +) + +import('pkgconfig').generate( + lib, + name: meson.project_name(), + filebase: meson.project_name() + '-' + api_version, + version: meson.project_version(), + subdirs: meson.project_name(), + requires: deps, + install_dir: libdir / 'pkgconfig', +) diff --git a/lib/astal/gtk4/src/widget/window.vala b/lib/astal/gtk4/src/widget/window.vala new file mode 100644 index 0000000..7b73b7c --- /dev/null +++ b/lib/astal/gtk4/src/widget/window.vala @@ -0,0 +1,261 @@ +using GtkLayerShell; + +public enum Astal.WindowAnchor { + NONE = 0, + TOP = 1, + RIGHT = 2, + LEFT = 4, + BOTTOM = 8, +} + +public enum Astal.Exclusivity { + NORMAL, + /** + * Request the compositor to allocate space for this window. + */ + EXCLUSIVE, + /** + * Request the compositor to stack layers on top of each other. + */ + IGNORE, +} + +public enum Astal.Layer { + BACKGROUND = 0, // GtkLayerShell.Layer.BACKGROUND + BOTTOM = 1, // GtkLayerShell.Layer.BOTTOM + TOP = 2, // GtkLayerShell.Layer.TOP + OVERLAY = 3, // GtkLayerShell.Layer.OVERLAY +} + +public enum Astal.Keymode { + /** + * Window should not receive keyboard events. + */ + NONE = 0, // GtkLayerShell.KeyboardMode.NONE + /** + * Window should have exclusive focus if it is on the top or overlay layer. + */ + EXCLUSIVE = 1, // GtkLayerShell.KeyboardMode.EXCLUSIVE + /** + * Focus and Unfocues the window as needed. + */ + ON_DEMAND = 2, // GtkLayerShell.KeyboardMode.ON_DEMAND +} + +/** + * Subclass of [[email protected]] which integrates GtkLayerShell as class fields. + */ +public class Astal.Window : Gtk.Window { + private static bool check(string action) { + if (!is_supported()) { + critical(@"can not $action on window: layer shell not supported"); + print("tip: running from an xwayland terminal can cause this, for example VsCode"); + return true; + } + return false; + } + + construct { + if (check("initialize layer shell")) + return; + + // If the window has no size allocatoted when it gets mapped. + // It won't show up later either when it size changes by adding children. + height_request = 1; + width_request = 1; + + init_for_window(this); + } + + /** + * Namespace of this window. This can be used to target the layer in compositor rules. + */ + public string namespace { + get { return get_namespace(this); } + set { set_namespace(this, value); } + } + + /** + * Edges to anchor the window to. + * + * If two perpendicular edges are anchored, the surface will be anchored to that corner. + * If two opposite edges are anchored, the window will be stretched across the screen in that direction. + */ + public int anchor { + set { + if (check("set anchor")) + return; + + set_anchor(this, Edge.TOP, WindowAnchor.TOP in value); + set_anchor(this, Edge.BOTTOM, WindowAnchor.BOTTOM in value); + set_anchor(this, Edge.LEFT, WindowAnchor.LEFT in value); + set_anchor(this, Edge.RIGHT, WindowAnchor.RIGHT in value); + } + get { + var a = WindowAnchor.NONE; + if (get_anchor(this, Edge.TOP)) + a = a | WindowAnchor.TOP; + + if (get_anchor(this, Edge.RIGHT)) + a = a | WindowAnchor.RIGHT; + + if (get_anchor(this, Edge.LEFT)) + a = a | WindowAnchor.LEFT; + + if (get_anchor(this, Edge.BOTTOM)) + a = a | WindowAnchor.BOTTOM; + + return a; + } + } + + /** + * Exclusivity of this window. + */ + public Exclusivity exclusivity { + set { + if (check("set exclusivity")) + return; + + switch (value) { + case Exclusivity.NORMAL: + set_exclusive_zone(this, 0); + break; + case Exclusivity.EXCLUSIVE: + auto_exclusive_zone_enable(this); + break; + case Exclusivity.IGNORE: + set_exclusive_zone(this, -1); + break; + } + } + get { + if (auto_exclusive_zone_is_enabled(this)) + return Exclusivity.EXCLUSIVE; + + if (get_exclusive_zone(this) == -1) + return Exclusivity.IGNORE; + + return Exclusivity.NORMAL; + } + } + + /** + * Which layer to appear this window on. + */ + public Layer layer { + get { return (Layer)get_layer(this); } + set { + if (check("set layer")) + return; + + set_layer(this, (GtkLayerShell.Layer)value); + } + } + + /** + * Keyboard mode of this window. + */ + public Keymode keymode { + get { return (Keymode)get_keyboard_mode(this); } + set { + if (check("set keymode")) + return; + + set_keyboard_mode(this, (GtkLayerShell.KeyboardMode)value); + } + } + + /** + * Which monitor to appear this window on. + */ + public Gdk.Monitor gdkmonitor { + get { return get_monitor(this); } + set { + if (check("set gdkmonitor")) + return; + + set_monitor (this, value); + } + } + + public new int margin_top { + get { return GtkLayerShell.get_margin(this, Edge.TOP); } + set { + if (check("set margin_top")) + return; + + GtkLayerShell.set_margin(this, Edge.TOP, value); + } + } + + public new int margin_bottom { + get { return GtkLayerShell.get_margin(this, Edge.BOTTOM); } + set { + if (check("set margin_bottom")) + return; + + GtkLayerShell.set_margin(this, Edge.BOTTOM, value); + } + } + + public new int margin_left { + get { return GtkLayerShell.get_margin(this, Edge.LEFT); } + set { + if (check("set margin_left")) + return; + + GtkLayerShell.set_margin(this, Edge.LEFT, value); + } + } + + public new int margin_right { + get { return GtkLayerShell.get_margin(this, Edge.RIGHT); } + set { + if (check("set margin_right")) + return; + + GtkLayerShell.set_margin(this, Edge.RIGHT, value); + } + } + + public new int margin { + set { + if (check("set margin")) + return; + + margin_top = value; + margin_right = value; + margin_bottom = value; + margin_left = value; + } + } + + /** + * Which monitor to appear this window on. + * + * CAUTION: the id might not be the same mapped by the compositor. + */ + public int monitor { + set { + if (check("set monitor")) + return; + + if (value < 0) + set_monitor(this, (Gdk.Monitor)null); + + var m = (Gdk.Monitor)Gdk.Display.get_default().get_monitors().get_item(value); + set_monitor(this, m); + } + get { + var m = get_monitor(this); + var mons = Gdk.Display.get_default().get_monitors(); + for (var i = 0; i < mons.get_n_items(); ++i) { + if (m == mons.get_item(i)) + return i; + } + + return -1; + } + } +} diff --git a/lib/astal/gtk4/version b/lib/astal/gtk4/version new file mode 100644 index 0000000..fcdb2e1 --- /dev/null +++ b/lib/astal/gtk4/version @@ -0,0 +1 @@ +4.0.0 |