summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/astal/gtk4/gir.py58
-rw-r--r--lib/astal/gtk4/meson.build18
-rw-r--r--lib/astal/gtk4/src/application.vala241
-rw-r--r--lib/astal/gtk4/src/config.vala.in6
-rw-r--r--lib/astal/gtk4/src/meson.build89
-rw-r--r--lib/astal/gtk4/src/widget/window.vala261
-rw-r--r--lib/astal/gtk4/version1
7 files changed, 674 insertions, 0 deletions
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