diff options
Diffstat (limited to 'examples/gtk4/simple-bar')
39 files changed, 1680 insertions, 0 deletions
diff --git a/examples/gtk4/simple-bar/js/.gitignore b/examples/gtk4/simple-bar/js/.gitignore new file mode 100644 index 0000000..2b7806f --- /dev/null +++ b/examples/gtk4/simple-bar/js/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +build/ +result +result/ +@types/ diff --git a/examples/gtk4/simple-bar/js/README.md b/examples/gtk4/simple-bar/js/README.md new file mode 100644 index 0000000..6719fdb --- /dev/null +++ b/examples/gtk4/simple-bar/js/README.md @@ -0,0 +1,53 @@ +# Simple Astal Bar example in TypeScript + +This example shows you how to get a TypeScript+Blueprint+Sass project going. + +## Dependencies + +- gjs +- meson +- esbuild +- blueprint-compiler +- sass +- astal4 +- astal-battery +- astal-wireplumber +- astak-network +- astal-mpris +- astak-power-profiles +- astal-tray +- astal-bluetooth + +## How to use + +> [!NOTE] +> If you are on Nix, there is an example flake included +> otherwise feel free to `rm flake.nix` + +- generate types with `ts-for-gir` + + ```sh + # might take a while + # also, don't worry about warning and error logs + npx @ts-for-gir/cli generate --ignoreVersionConflicts + ``` + +- developing + + ```sh + meson setup build --wipe --prefix "$pwd/result" + meson install -C build + ./result/bin/simple-bar + ``` + +- installing + + ```sh + meson setup build --wipe --prefix /usr + meson install -C build + simple-bar + ``` + +- adding new typescript files requires no additional steps +- adding new scss files requires no additional steps as long as they are imported from `main.scss` +- adding new ui (blueprint) files will also have to be listed in `meson.build` and in `gresource.xml` diff --git a/examples/gtk4/simple-bar/js/flake.nix b/examples/gtk4/simple-bar/js/flake.nix new file mode 100644 index 0000000..b626880 --- /dev/null +++ b/examples/gtk4/simple-bar/js/flake.nix @@ -0,0 +1,52 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + astal = { + url = "github:aylur/astal"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + self, + nixpkgs, + astal, + }: let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + + nativeBuildInputs = with pkgs; [ + meson + ninja + pkg-config + gobject-introspection + wrapGAppsHook4 + blueprint-compiler + dart-sass + esbuild + ]; + + astalPackages = with astal.packages.${system}; [ + io + astal4 + battery + wireplumber + network + mpris + powerprofiles + tray + bluetooth + ]; + in { + packages.${system}.default = pkgs.stdenv.mkDerivation { + name = "simple-bar"; + src = ./.; + inherit nativeBuildInputs; + buildInputs = astalPackages ++ [pkgs.gjs]; + }; + + devShells.${system}.default = pkgs.mkShell { + packages = nativeBuildInputs ++ astalPackages ++ [pkgs.gjs]; + }; + }; +} diff --git a/examples/gtk4/simple-bar/js/meson.build b/examples/gtk4/simple-bar/js/meson.build new file mode 100644 index 0000000..f49af2e --- /dev/null +++ b/examples/gtk4/simple-bar/js/meson.build @@ -0,0 +1,12 @@ +project('simple-bar') + +dependency('astal-4-4.0') +dependency('astal-battery-0.1') +dependency('astal-wireplumber-0.1') +dependency('astal-network-0.1') +dependency('astal-mpris-0.1') +dependency('astal-power-profiles-0.1') +dependency('astal-tray-0.1') +dependency('astal-bluetooth-0.1') + +subdir('src') diff --git a/examples/gtk4/simple-bar/js/src/gresource.xml b/examples/gtk4/simple-bar/js/src/gresource.xml new file mode 100644 index 0000000..0960d6a --- /dev/null +++ b/examples/gtk4/simple-bar/js/src/gresource.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/"> + <file>index.js</file> + <file>main.css</file> + <file>ui/Bar.ui</file> + </gresource> +</gresources> diff --git a/examples/gtk4/simple-bar/js/src/main.in.js b/examples/gtk4/simple-bar/js/src/main.in.js new file mode 100755 index 0000000..90e8f99 --- /dev/null +++ b/examples/gtk4/simple-bar/js/src/main.in.js @@ -0,0 +1,12 @@ +#!@GJS@ -m + +import { exit, programArgs } from "system" +import Gio from "gi://Gio" +import GLib from "gi://GLib" + +// makes sure `LD_PRELOAD` does not leak into subprocesses +GLib.setenv("LD_PRELOAD", "", true) +Gio.Resource.load("@PKGDATADIR@/data.gresource")._register() + +const module = await import("resource:///index.js") +exit(await module.default.main(programArgs)) diff --git a/examples/gtk4/simple-bar/js/src/main.in.sh b/examples/gtk4/simple-bar/js/src/main.in.sh new file mode 100755 index 0000000..32a0326 --- /dev/null +++ b/examples/gtk4/simple-bar/js/src/main.in.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +LD_PRELOAD="@LAYER_SHELL_PREFIX@/lib/libgtk4-layer-shell.so" "@INDEX@" diff --git a/examples/gtk4/simple-bar/js/src/main.scss b/examples/gtk4/simple-bar/js/src/main.scss new file mode 100644 index 0000000..c37695a --- /dev/null +++ b/examples/gtk4/simple-bar/js/src/main.scss @@ -0,0 +1 @@ +@use "./scss/Bar.scss"; diff --git a/examples/gtk4/simple-bar/js/src/meson.build b/examples/gtk4/simple-bar/js/src/meson.build new file mode 100644 index 0000000..8d03182 --- /dev/null +++ b/examples/gtk4/simple-bar/js/src/meson.build @@ -0,0 +1,84 @@ +pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name() +bindir = get_option('prefix') / get_option('bindir') +blp = find_program('blueprint-compiler', required: true) +sass = find_program('sass', required: true) +esbuild = find_program('esbuild', required: true) +gjs = find_program('gjs', required: true) +layer_shell = dependency('gtk4-layer-shell-0') + +blueprint_sources = files( + 'ui/Bar.blp', +) + +# transplie blueprints +ui = custom_target( + 'blueprint', + input: blueprint_sources, + output: '.', + command: [ + blp, + 'batch-compile', + '@OUTPUT@', + '@CURRENT_SOURCE_DIR@', + '@INPUT@', + ], +) + +# bundle ts files +js = custom_target( + 'typescript', + input: files('ts/App.ts'), + command: [ + esbuild, + '--bundle', '@INPUT@', + '--format=esm', + '--outfile=@OUTPUT@', + '--sourcemap=inline', + '--external:gi://*', + '--external:gettext', + '--external:system', + ], + output: 'index.js', +) + +# bundle scss files +css = custom_target( + 'scss', + input: files('main.scss'), + command: [sass, '@INPUT@', '@OUTPUT@'], + output: 'main.css', +) + +# compiling source files into a binary +import('gnome').compile_resources( + 'data', + files('gresource.xml'), + dependencies: [ui, css, js], + gresource_bundle: true, + install: true, + install_dir: pkgdatadir, +) + +# gresource can't be run with gjs, we still need an entry script +configure_file( + input: files('main.in.js'), + output: 'main.js', + configuration: { + 'GJS': gjs.full_path(), + 'PKGDATADIR': pkgdatadir, + }, + install: true, + install_dir: pkgdatadir, +) + +# and we need to wrap the entry script to preload gtk4-layer-shell +configure_file( + input: 'main.in.sh', + output: meson.project_name(), + configuration: { + 'LAYER_SHELL_PREFIX': layer_shell.get_variable('prefix'), + 'INDEX': pkgdatadir / 'main.js', + }, + install: true, + install_dir: bindir, +) diff --git a/examples/gtk4/simple-bar/js/src/scss/Bar.scss b/examples/gtk4/simple-bar/js/src/scss/Bar.scss new file mode 100644 index 0000000..86ea856 --- /dev/null +++ b/examples/gtk4/simple-bar/js/src/scss/Bar.scss @@ -0,0 +1,19 @@ +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-4-16/gtk/theme/Default/_colors-public.scss +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; + +window.Bar { + > box { + background: $bg-color; + color: $fg-color; + font-weight: bold; + } + + button { + min-height: 0; + min-width: 0; + border-radius: 8px; + margin: 4px; + padding: 4px 8px; + } +} diff --git a/examples/gtk4/simple-bar/js/src/ts/App.ts b/examples/gtk4/simple-bar/js/src/ts/App.ts new file mode 100644 index 0000000..012fafa --- /dev/null +++ b/examples/gtk4/simple-bar/js/src/ts/App.ts @@ -0,0 +1,52 @@ +import GObject from "gi://GObject" +import Astal from "gi://Astal?version=4.0" +import Gio from "gi://Gio" +import GLib from "gi://GLib" +import AstalIO from "gi://AstalIO" +import Bar from "./Bar" + +export default class App extends Astal.Application { + static { + GObject.registerClass(this) + } + + static instance: App + static instanceName = "simple-bar" + + // this is where request handlers can be implemented + // that will be used to handle `astal` cli invocations + vfunc_request(request: string, conn: Gio.SocketConnection): void { + print("incoming request", request) + AstalIO.write_sock(conn, "response", null) + } + + // this is the method that will be invoked on `app.runAsync()` + // this is where everything should be initialized and instantiated + vfunc_activate(): void { + this.apply_css("resource:///main.css", false) + this.add_window(new Bar()) + } + + // entry point of our app + static async main(argv: string[]): Promise<number> { + GLib.set_prgname(App.instanceName) + App.instance = new App({ instanceName: App.instanceName }) + + try { + // `app.acquire_socket()` needed for the request API to work + App.instance.acquire_socket() + + // if it succeeds we can run the app + return await App.instance.runAsync([]) + } catch (error) { + // if it throws an error it means there is already an instance + // with `instanceName` running, so we just send a request instead + const response = AstalIO.send_request( + App.instanceName, + argv.join(" "), + ) + print(response) + return 0 + } + } +} diff --git a/examples/gtk4/simple-bar/js/src/ts/Bar.ts b/examples/gtk4/simple-bar/js/src/ts/Bar.ts new file mode 100644 index 0000000..a81623c --- /dev/null +++ b/examples/gtk4/simple-bar/js/src/ts/Bar.ts @@ -0,0 +1,189 @@ +import Astal from "gi://Astal?version=4.0" +import AstalIO from "gi://AstalIO" +import GLib from "gi://GLib" +import Gtk from "gi://Gtk?version=4.0" +import GObject from "gi://GObject?version=2.0" +import AstalBattery from "gi://AstalBattery" +import AstalWp from "gi://AstalWp" +import AstalNetwork from "gi://AstalNetwork" +import AstalMpris from "gi://AstalMpris" +import AstalPowerProfiles from "gi://AstalPowerProfiles" +import AstalTray from "gi://AstalTray" +import AstalBluetooth from "gi://AstalBluetooth" +import { string, number, boolean } from "./props" + +const { TOP, LEFT, RIGHT } = Astal.WindowAnchor +const SYNC = GObject.BindingFlags.SYNC_CREATE + +export default class Bar extends Astal.Window { + static { + GObject.registerClass( + { + GTypeName: "Bar", + Template: "resource:///ui/Bar.ui", + InternalChildren: ["popover", "calendar", "traybox"], + Properties: { + ...string("clock"), + ...string("volume-icon"), + ...boolean("battery-visible"), + ...string("battery-label"), + ...string("battery-icon"), + ...number("volume", 0, 1), + ...string("network-icon"), + ...boolean("mpris-visible"), + ...string("mpris-label"), + ...string("mpris-art"), + ...string("power-profile-icon"), + ...boolean("bluetooth-visible"), + }, + }, + this, + ) + } + + declare clock: string + declare battery_label: string + declare mpris_label: string + declare bluetooth_visible: string + + declare _popover: Gtk.Popover + declare _calendar: Gtk.Calendar + declare _traybox: Gtk.Box + + constructor() { + super({ + visible: true, + exclusivity: Astal.Exclusivity.EXCLUSIVE, + anchor: TOP | LEFT | RIGHT, + cssClasses: ["Bar"], + }) + + // clock + const timer = AstalIO.Time.interval(1000, () => { + this.clock = GLib.DateTime.new_now_local().format("%H:%M:%S")! + }) + this.connect("destroy", () => timer.cancel()) + + // everytime popover is opened, select current day + this._popover.connect("notify::visible", ({ visible }) => { + if (visible) { + this._calendar.select_day(GLib.DateTime.new_now_local()) + } + }) + + // network + const nw = AstalNetwork.get_default() + let networkBinding: GObject.Binding + + // @ts-expect-error mistyped + nw.bind_property_full( + "primary", + this, + "network-icon", + SYNC, + (_, primary: AstalNetwork.Primary) => { + networkBinding?.unbind() + + switch (primary) { + case AstalNetwork.Primary.WIRED: + networkBinding = nw.wired.bind_property( + "icon-name", + this, + "network-icon", + SYNC, + ) + return [false, ""] + case AstalNetwork.Primary.WIFI: + networkBinding = nw.wifi.bind_property( + "icon-name", + this, + "network-icon", + SYNC, + ) + return [false, ""] + default: + return [true, "network-idle-symbolic"] + } + }, + null, + ) + + // battery + const bat = AstalBattery.get_default() + + bat.bind_property("is-present", this, "battery-visible", SYNC) + bat.bind_property("icon-name", this, "battery-icon", SYNC) + + this.battery_label = `${Math.floor(bat.percentage * 100)}%` + const batteryId = bat.connect("notify::percentage", () => { + this.battery_label = `${Math.floor(bat.percentage * 100)}%` + }) + this.connect("destroy", () => bat.disconnect(batteryId)) + + // volume + const speaker = AstalWp.get_default()!.defaultSpeaker + speaker.bind_property("volume-icon", this, "volume-icon", SYNC) + speaker.bind_property("volume", this, "volume", SYNC) + + // mpris + const player = AstalMpris.Player.new("spotify") + player.bind_property("available", this, "mpris-visible", SYNC) + player.bind_property("cover-art", this, "mpris-art", SYNC) + + this.mpris_label = `${player.artist} - ${player.title}` + const playerId = player.connect("notify::metadata", () => { + this.mpris_label = `${player.artist} - ${player.title}` + }) + this.connect("destroy", () => player.disconnect(playerId)) + + // powerprofiles + const powerprofile = AstalPowerProfiles.get_default() + powerprofile.bind_property( + "icon-name", + this, + "power-profile-icon", + SYNC, + ) + + // tray + const tray = AstalTray.get_default() + const trayItems = new Map<string, Gtk.MenuButton>() + const trayId1 = tray.connect("item-added", (_, id) => { + const item = tray.get_item(id) + const popover = Gtk.PopoverMenu.new_from_model(item.menu_model) + const icon = new Gtk.Image() + const button = new Gtk.MenuButton({ popover, child: icon }) + + item.bind_property("gicon", icon, "gicon", SYNC) + popover.insert_action_group("dbusmenu", item.action_group) + item.connect("notify::action-group", () => { + popover.insert_action_group("dbusmenu", item.action_group) + }) + + trayItems.set(id, button) + this._traybox.append(button) + }) + + const trayId2 = tray.connect("item-removed", (_, id) => { + const button = trayItems.get(id) + if (button) { + this._traybox.remove(button) + button.run_dispose() + trayItems.delete(id) + } + }) + + this.connect("destroy", () => { + tray.disconnect(trayId1) + tray.disconnect(trayId2) + }) + + // bluetooth + const bt = AstalBluetooth.get_default() + bt.bind_property("is-connected", this, "bluetooth-visible", SYNC) + } + + change_volume(_scale: Gtk.Scale, _type: Gtk.ScrollType, value: number) { + AstalWp.get_default()?.defaultSpeaker.set_volume(value) + } +} diff --git a/examples/gtk4/simple-bar/js/src/ts/props.ts b/examples/gtk4/simple-bar/js/src/ts/props.ts new file mode 100644 index 0000000..dfa79ee --- /dev/null +++ b/examples/gtk4/simple-bar/js/src/ts/props.ts @@ -0,0 +1,45 @@ +import GObject from "gi://GObject?version=2.0" + +export function string(name: string, defaultValue = "") { + return { + [name]: GObject.ParamSpec.string( + name, + null, + null, + GObject.ParamFlags.READWRITE, + defaultValue, + ), + } +} + +export function number( + name: string, + min = -Number.MIN_SAFE_INTEGER, + max = Number.MAX_SAFE_INTEGER, + defaultValue = 0, +) { + return { + [name]: GObject.ParamSpec.double( + name, + null, + null, + GObject.ParamFlags.READWRITE, + min, + max, + defaultValue, + ), + } +} + +export function boolean(name: string, defaultValue = false) { + return { + [name]: GObject.ParamSpec.boolean( + name, + null, + null, + GObject.ParamFlags.READWRITE, + defaultValue, + ), + } +} + diff --git a/examples/gtk4/simple-bar/js/src/ui/Bar.blp b/examples/gtk4/simple-bar/js/src/ui/Bar.blp new file mode 100644 index 0000000..6e401e7 --- /dev/null +++ b/examples/gtk4/simple-bar/js/src/ui/Bar.blp @@ -0,0 +1,84 @@ +using Gtk 4.0; +using Astal 4.0; + +template $Bar: Astal.Window { + CenterBox centerbox { + start-widget: Box { + MenuButton { + Label { + label: bind template.clock; + } + + popover: Popover popover { + Calendar calendar { + show-day-names: true; + show-heading: true; + show-week-numbers: true; + } + }; + } + }; + + center-widget: Box { + Box { + visible: bind template.mpris-visible; + + Image { + file: bind template.mpris-art; + } + + Label { + label: bind template.mpris-label; + } + } + }; + + end-widget: Box { + spacing: 4; + + Image { + visible: bind template.bluetooth-visible; + icon-name: "bluetooth-symbolic"; + } + + Image { + icon-name: bind template.power-profile-icon; + } + + Image { + icon-name: bind template.network-icon; + } + + Box { + Image { + icon-name: bind template.volume-icon; + } + + Scale { + width-request: 100; + change-value => $change_volume(); + + adjustment: Adjustment { + value: bind template.volume; + lower: 0; + upper: 1; + }; + } + } + + Box { + Image { + icon-name: bind template.battery-icon; + } + + Label { + label: bind template.battery-label; + } + } + + Box traybox { + spacing: 4; + } + }; + } +} diff --git a/examples/gtk4/simple-bar/js/tsconfig.json b/examples/gtk4/simple-bar/js/tsconfig.json new file mode 100644 index 0000000..65b3aba --- /dev/null +++ b/examples/gtk4/simple-bar/js/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler" + } +} diff --git a/examples/gtk4/simple-bar/py/.gitignore b/examples/gtk4/simple-bar/py/.gitignore new file mode 100644 index 0000000..bb252c6 --- /dev/null +++ b/examples/gtk4/simple-bar/py/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +build/ +result diff --git a/examples/gtk4/simple-bar/py/README.md b/examples/gtk4/simple-bar/py/README.md new file mode 100644 index 0000000..25f296b --- /dev/null +++ b/examples/gtk4/simple-bar/py/README.md @@ -0,0 +1,45 @@ +# Simple Astal Bar example in Python + +This example shows you how to get a Python+Blueprint+Sass project going. + +## Dependencies + +- python3 +- pygobject +- meson +- blueprint-compiler +- sass +- astal4 +- astal-battery +- astal-wireplumber +- astak-network +- astal-mpris +- astak-powerprofiles +- astal-tray +- astal-bluetooth + +## How to use + +> [!NOTE] +> If you are on Nix, there is an example flake included +> otherwise feel free to `rm flake.nix` + +- developing + + ```sh + meson setup build --wipe --prefix "$pwd/result" + meson install -C build + ./result/bin/simple-bar + ``` + +- installing + + ```sh + meson setup build --wipe --prefix /usr + meson install -C build + simple-bar + ``` + +- adding new python files requires no additional steps +- adding new scss files requires no additional steps as long as they are imported from `main.scss` +- adding new ui (blueprint) files will also have to be listed in `meson.build` and in `gresource.xml` diff --git a/examples/gtk4/simple-bar/py/flake.nix b/examples/gtk4/simple-bar/py/flake.nix new file mode 100644 index 0000000..4002826 --- /dev/null +++ b/examples/gtk4/simple-bar/py/flake.nix @@ -0,0 +1,55 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + astal = { + url = "github:aylur/astal"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + self, + nixpkgs, + astal, + }: let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + + python = pkgs.python3.withPackages (ps: [ + ps.pygobject3 + ]); + + nativeBuildInputs = with pkgs; [ + meson + ninja + pkg-config + gobject-introspection + wrapGAppsHook4 + blueprint-compiler + dart-sass + ]; + + astalPackages = with astal.packages.${system}; [ + io + astal4 + battery + wireplumber + network + mpris + powerprofiles + tray + bluetooth + ]; + in { + packages.${system}.default = pkgs.stdenv.mkDerivation { + name = "simple-bar"; + src = ./.; + inherit nativeBuildInputs; + buildInputs = astalPackages ++ [python]; + }; + + devShells.${system}.default = pkgs.mkShell { + packages = nativeBuildInputs ++ astalPackages ++ [python]; + }; + }; +} diff --git a/examples/gtk4/simple-bar/py/meson.build b/examples/gtk4/simple-bar/py/meson.build new file mode 100644 index 0000000..f49af2e --- /dev/null +++ b/examples/gtk4/simple-bar/py/meson.build @@ -0,0 +1,12 @@ +project('simple-bar') + +dependency('astal-4-4.0') +dependency('astal-battery-0.1') +dependency('astal-wireplumber-0.1') +dependency('astal-network-0.1') +dependency('astal-mpris-0.1') +dependency('astal-power-profiles-0.1') +dependency('astal-tray-0.1') +dependency('astal-bluetooth-0.1') + +subdir('src') diff --git a/examples/gtk4/simple-bar/py/src/gresource.xml b/examples/gtk4/simple-bar/py/src/gresource.xml new file mode 100644 index 0000000..36dad8c --- /dev/null +++ b/examples/gtk4/simple-bar/py/src/gresource.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/"> + <file>ui/Bar.ui</file> + <file>main.css</file> + </gresource> +</gresources> diff --git a/examples/gtk4/simple-bar/py/src/main.in.py b/examples/gtk4/simple-bar/py/src/main.in.py new file mode 100755 index 0000000..58042f4 --- /dev/null +++ b/examples/gtk4/simple-bar/py/src/main.in.py @@ -0,0 +1,19 @@ +#!@PYTHON@ + +import gi + +gi.require_version("Gio", "2.0") + +from gi.repository import Gio +from sys import argv, path +from ctypes import CDLL + +CDLL("@LAYER_SHELL_PREFIX@/lib/libgtk4-layer-shell.so") +path.insert(1, "@PKGDATADIR@") +Gio.Resource.load("@PKGDATADIR@/data.gresource")._register() + + +if __name__ == "__main__": + from py.App import App + + App.main(argv[2:]) diff --git a/examples/gtk4/simple-bar/py/src/main.scss b/examples/gtk4/simple-bar/py/src/main.scss new file mode 100644 index 0000000..c37695a --- /dev/null +++ b/examples/gtk4/simple-bar/py/src/main.scss @@ -0,0 +1 @@ +@use "./scss/Bar.scss"; diff --git a/examples/gtk4/simple-bar/py/src/meson.build b/examples/gtk4/simple-bar/py/src/meson.build new file mode 100644 index 0000000..54ecd17 --- /dev/null +++ b/examples/gtk4/simple-bar/py/src/meson.build @@ -0,0 +1,59 @@ +pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name() +bindir = get_option('prefix') / get_option('bindir') + +blp = find_program('blueprint-compiler', required: true) +sass = find_program('sass', required: true) +python = find_program('python3', required: true) +layer_shell = dependency('gtk4-layer-shell-0') + +blueprint_sources = files( + 'ui/Bar.blp', +) + +# transplie blueprints +ui = custom_target( + 'blueprint', + input: blueprint_sources, + output: '.', + command: [ + blp, + 'batch-compile', + '@OUTPUT@', + '@CURRENT_SOURCE_DIR@', + '@INPUT@', + ], +) + +# bundle styles +css = custom_target( + 'scss', + input: files('main.scss'), + command: [sass, '@INPUT@', '@OUTPUT@'], + output: 'main.css', +) + +# compiling ui and css into a binary +import('gnome').compile_resources( + 'data', + files('gresource.xml'), + dependencies: [ui, css], + gresource_bundle: true, + install: true, + install_dir: pkgdatadir, +) + +# install python sources +install_subdir('py', install_dir: pkgdatadir) + +# configure the main python entry file +configure_file( + input: 'main.in.py', + output: meson.project_name(), + configuration: { + 'PYTHON': python.full_path(), + 'LAYER_SHELL_PREFIX': layer_shell.get_variable('prefix'), + 'PKGDATADIR': pkgdatadir, + }, + install: true, + install_dir: bindir, +) diff --git a/examples/gtk4/simple-bar/py/src/py/App.py b/examples/gtk4/simple-bar/py/src/py/App.py new file mode 100644 index 0000000..678fb5f --- /dev/null +++ b/examples/gtk4/simple-bar/py/src/py/App.py @@ -0,0 +1,41 @@ +from pathlib import Path +from gi.repository import Gio, GLib, Astal, GLib, AstalIO +from py.Bar import Bar + + +class App(Astal.Application): + __gtype_name__ = "App" + instance_name = "simple-bar" + + # this is where request handlers can be implemented + # that will be used to handle `astal` cli invocations + def do_astal_application_request(self, request, conn): + print("incoming request", request) + AstalIO.write_sock(conn, "response", None) + + # this is the method that will be invoked on `app.run()` + # this is where everything should be initialized and instantiated + def do_activate(self): + self.apply_css("resource:///main.css", False) + self.add_window(Bar()) + + @staticmethod + def main(argv): + GLib.set_prgname(App.instance_name) + App.instance = App(instance_name=App.instance_name) + + try: + # `app.acquire_socket()` needed for the request API to work + App.instance.acquire_socket() + + # if it succeeds we can run the app + return App.instance.run([]) + except Exception: + # if it throws an error it means there is already an instance + # with `instance_name` running, so we just send a request instead + response = AstalIO.send_request( + App.instance_name, + argv.join(" "), + ) + print(response) + return 0 diff --git a/examples/gtk4/simple-bar/py/src/py/Bar.py b/examples/gtk4/simple-bar/py/src/py/Bar.py new file mode 100644 index 0000000..cf521a9 --- /dev/null +++ b/examples/gtk4/simple-bar/py/src/py/Bar.py @@ -0,0 +1,184 @@ +import math +from gi.repository import ( + Astal, + AstalIO, + GObject, + GLib, + Gtk, + GObject, + AstalBattery, + AstalWp, + AstalNetwork, + AstalMpris, + AstalPowerProfiles, + AstalTray, + AstalBluetooth, +) + +SYNC = GObject.BindingFlags.SYNC_CREATE + + [email protected](resource_path="/ui/Bar.ui") +class Bar(Astal.Window): + __gtype_name__ = "Bar" + + clock = GObject.Property(type=str) + volume_icon = GObject.Property(type=str) + battery_visible = GObject.Property(type=bool, default=False) + battery_label = GObject.Property(type=str) + battery_icon = GObject.Property(type=str) + volume = GObject.Property(type=float) + network_icon = GObject.Property(type=str) + mpris_visible = GObject.Property(type=bool, default=False) + mpris_label = GObject.Property(type=str) + mpris_art = GObject.Property(type=str) + power_profile_icon = GObject.Property(type=str) + bluetooth_visible = GObject.Property(type=bool, default=False) + + popover = Gtk.Template.Child() + calendar = Gtk.Template.Child() + traybox = Gtk.Template.Child() + + def __init__(self): + super().__init__( + anchor=Astal.WindowAnchor.TOP + | Astal.WindowAnchor.LEFT + | Astal.WindowAnchor.RIGHT, + exclusivity=Astal.Exclusivity.EXCLUSIVE, + css_classes=["Bar"], + visible=True, + ) + + # clock + timer = AstalIO.Time.interval(1000, self.set_clock) + self.connect("destroy", lambda _: timer.cancel()) + + # everytime popover is opened, select current day + self.popover.connect("notify::visible", self.on_popover_visible) + + # network + nw = AstalNetwork.get_default() + self._network_binding = None + + nw.bind_property( + "primary", + self, + "network-icon", + SYNC, + self.on_nm_primary, + None, + ) + + # battery + bat = AstalBattery.get_default() + bat.bind_property("is-present", self, "battery-visible", SYNC) + bat.bind_property("icon-name", self, "battery-icon", SYNC) + bat.bind_property( + "percentage", + self, + "battery-label", + SYNC, + lambda _, percentage: f"{math.floor(percentage * 100)}%", + None, + ) + + # volume + speaker = AstalWp.get_default().get_default_speaker() + speaker.bind_property("volume-icon", self, "volume-icon", SYNC) + speaker.bind_property("volume", self, "volume", SYNC) + + # mpris + player = AstalMpris.Player.new("spotify") + player.bind_property("available", self, "mpris-visible", SYNC) + player.bind_property("cover-art", self, "mpris-art", SYNC) + player.bind_property( + "metadata", + self, + "mpris-label", + SYNC, + lambda *_: f"{player.get_artist()} - {player.get_title()}", + None, + ) + + # powerprofiles + powerprofile = AstalPowerProfiles.get_default() + powerprofile.bind_property("icon-name", self, "power-profile-icon", SYNC) + + # bluetooth + bt = AstalBluetooth.get_default() + bt.bind_property("is-connected", self, "bluetooth-visible", SYNC) + + # tray + tray = AstalTray.get_default() + self._tray_items = {} + + def on_tray_item_added(tray, id): + item = tray.get_item(id) + popover = Gtk.PopoverMenu.new_from_model(item.get_menu_model()) + icon = Gtk.Image() + button = Gtk.MenuButton(popover=popover, child=icon) + + item.bind_property("gicon", icon, "gicon", SYNC) + popover.insert_action_group("dbusmenu", item.get_action_group()) + item.connect( + "notify::action-group", + lambda *_: popover.insert_action_group( + "dbusmenu", item.get_action_group() + ), + ) + + self._tray_items[id] = button + self.traybox.append(button) + + def on_tray_item_removed(_tray, id): + button = self._tray_items.get(id) + if button: + self.traybox.remove(button) + button.run_dispose() + del self._tray_items[id] + + tray.connect("item_added", on_tray_item_added) + tray.connect("item_removed", on_tray_item_removed) + self.connect( + "destroy", + lambda *_: ( + tray.disconnect(on_tray_item_added), + tray.disconnect(on_tray_item_removed), + ), + ) + + def on_popover_visible(self, popover, _pspec): + if popover.get_visible(): + self.calendar.select_day(GLib.DateTime.new_now_local()) + + def on_nm_primary(self, _binding, primary): + nw = AstalNetwork.get_default() + if self._network_binding is not None: + self._network_binding.unbind() + + match primary: + case AstalNetwork.Primary.WIRED: + self._network_binding = nw.get_wired().bind_property( + "icon-name", + self, + "network-icon", + SYNC, + ) + return nw.get_wired().get_icon_name() + case AstalNetwork.Primary.WIFI: + self._network_binding = nw.get_wifi().bind_property( + "icon-name", + self, + "network-icon", + SYNC, + ) + return nw.get_wifi().get_icon_name() + case _: + return "network-idle-symbolic" + + def set_clock(self): + self.clock = GLib.DateTime.new_now_local().format("%H:%M:%S") + + @Gtk.Template.Callback() + def change_volume(self, _scale, _type, value): + AstalWp.get_default().get_default_speaker().set_volume(value) diff --git a/examples/gtk4/simple-bar/py/src/py/__init__.py b/examples/gtk4/simple-bar/py/src/py/__init__.py new file mode 100644 index 0000000..57f4b1d --- /dev/null +++ b/examples/gtk4/simple-bar/py/src/py/__init__.py @@ -0,0 +1,15 @@ +import gi + +gi.require_version("GObject", "2.0") +gi.require_version("GLib", "2.0") +gi.require_version("Gtk", "4.0") +gi.require_version("AstalIO", "0.1") +gi.require_version("Astal", "4.0") + +gi.require_version("AstalBattery", "0.1") +gi.require_version("AstalWp", "0.1") +gi.require_version("AstalNetwork", "0.1") +gi.require_version("AstalMpris", "0.1") +gi.require_version("AstalPowerProfiles", "0.1") +gi.require_version("AstalTray", "0.1") +gi.require_version("AstalBluetooth", "0.1") diff --git a/examples/gtk4/simple-bar/py/src/scss/Bar.scss b/examples/gtk4/simple-bar/py/src/scss/Bar.scss new file mode 100644 index 0000000..86ea856 --- /dev/null +++ b/examples/gtk4/simple-bar/py/src/scss/Bar.scss @@ -0,0 +1,19 @@ +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-4-16/gtk/theme/Default/_colors-public.scss +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; + +window.Bar { + > box { + background: $bg-color; + color: $fg-color; + font-weight: bold; + } + + button { + min-height: 0; + min-width: 0; + border-radius: 8px; + margin: 4px; + padding: 4px 8px; + } +} diff --git a/examples/gtk4/simple-bar/py/src/ui/Bar.blp b/examples/gtk4/simple-bar/py/src/ui/Bar.blp new file mode 100644 index 0000000..6e401e7 --- /dev/null +++ b/examples/gtk4/simple-bar/py/src/ui/Bar.blp @@ -0,0 +1,84 @@ +using Gtk 4.0; +using Astal 4.0; + +template $Bar: Astal.Window { + CenterBox centerbox { + start-widget: Box { + MenuButton { + Label { + label: bind template.clock; + } + + popover: Popover popover { + Calendar calendar { + show-day-names: true; + show-heading: true; + show-week-numbers: true; + } + }; + } + }; + + center-widget: Box { + Box { + visible: bind template.mpris-visible; + + Image { + file: bind template.mpris-art; + } + + Label { + label: bind template.mpris-label; + } + } + }; + + end-widget: Box { + spacing: 4; + + Image { + visible: bind template.bluetooth-visible; + icon-name: "bluetooth-symbolic"; + } + + Image { + icon-name: bind template.power-profile-icon; + } + + Image { + icon-name: bind template.network-icon; + } + + Box { + Image { + icon-name: bind template.volume-icon; + } + + Scale { + width-request: 100; + change-value => $change_volume(); + + adjustment: Adjustment { + value: bind template.volume; + lower: 0; + upper: 1; + }; + } + } + + Box { + Image { + icon-name: bind template.battery-icon; + } + + Label { + label: bind template.battery-label; + } + } + + Box traybox { + spacing: 4; + } + }; + } +} diff --git a/examples/gtk4/simple-bar/vala/.gitignore b/examples/gtk4/simple-bar/vala/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/examples/gtk4/simple-bar/vala/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/examples/gtk4/simple-bar/vala/README.md b/examples/gtk4/simple-bar/vala/README.md new file mode 100644 index 0000000..a1a3eae --- /dev/null +++ b/examples/gtk4/simple-bar/vala/README.md @@ -0,0 +1,44 @@ +# Simple Astal Bar example in Vala + +This example shows you how to get a Vala+Blueprint+Sass project going. + +## Dependencies + +- vala +- meson +- blueprint-compiler +- sass +- astal4 +- astal-battery +- astal-wireplumber +- astak-network +- astal-mpris +- astak-power-profiles +- astal-tray +- astal-bluetooth + +## How to use + +> [!NOTE] +> If you are on Nix, there is an example flake included +> otherwise feel free to `rm flake.nix` + +- developing + + ```sh + meson setup build --wipe --prefix "$pwd/result" + meson install -C build + ./result/bin/simple-bar + ``` + +- installing + + ```sh + meson setup build --wipe --prefix /usr + meson install -C build + simple-bar + ``` + +- adding new vala files will also have to be listed in `meson.build` +- adding new scss files requires no additional steps as long as they are imported from `main.scss` +- adding new ui (blueprint) files will also have to be listed in `meson.build` and in `gresource.xml` diff --git a/examples/gtk4/simple-bar/vala/flake.nix b/examples/gtk4/simple-bar/vala/flake.nix new file mode 100644 index 0000000..52db45b --- /dev/null +++ b/examples/gtk4/simple-bar/vala/flake.nix @@ -0,0 +1,52 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + astal = { + url = "github:aylur/astal"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + self, + nixpkgs, + astal, + }: let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + + nativeBuildInputs = with pkgs; [ + meson + ninja + pkg-config + gobject-introspection + wrapGAppsHook4 + blueprint-compiler + dart-sass + vala + ]; + + astalPackages = with astal.packages.${system}; [ + io + astal4 + battery + wireplumber + network + mpris + powerprofiles + tray + bluetooth + ]; + in { + packages.${system}.default = pkgs.stdenv.mkDerivation { + name = "simple-bar"; + src = ./.; + inherit nativeBuildInputs; + buildInputs = astalPackages; + }; + + devShells.${system}.default = pkgs.mkShell { + packages = nativeBuildInputs ++ astalPackages; + }; + }; +} diff --git a/examples/gtk4/simple-bar/vala/meson.build b/examples/gtk4/simple-bar/vala/meson.build new file mode 100644 index 0000000..6858ee0 --- /dev/null +++ b/examples/gtk4/simple-bar/vala/meson.build @@ -0,0 +1,3 @@ +project('simple-bar', 'vala') + +subdir('src') diff --git a/examples/gtk4/simple-bar/vala/src/gresource.xml b/examples/gtk4/simple-bar/vala/src/gresource.xml new file mode 100644 index 0000000..abc50e5 --- /dev/null +++ b/examples/gtk4/simple-bar/vala/src/gresource.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/"> + <file>main.css</file> + <file>ui/Bar.ui</file> + </gresource> +</gresources> diff --git a/examples/gtk4/simple-bar/vala/src/main.scss b/examples/gtk4/simple-bar/vala/src/main.scss new file mode 100644 index 0000000..c37695a --- /dev/null +++ b/examples/gtk4/simple-bar/vala/src/main.scss @@ -0,0 +1 @@ +@use "./scss/Bar.scss"; diff --git a/examples/gtk4/simple-bar/vala/src/meson.build b/examples/gtk4/simple-bar/vala/src/meson.build new file mode 100644 index 0000000..a11da46 --- /dev/null +++ b/examples/gtk4/simple-bar/vala/src/meson.build @@ -0,0 +1,67 @@ +pkgdatadir = get_option('prefix') / get_option('datadir') +bindir = get_option('prefix') / get_option('bindir') +blp = find_program('blueprint-compiler', required: true) +sass = find_program('sass', required: true) + +dependencies = [ + dependency('gtk4-layer-shell-0'), + dependency('astal-io-0.1'), + dependency('glib-2.0'), + dependency('astal-4-4.0'), + dependency('astal-battery-0.1'), + dependency('astal-wireplumber-0.1'), + dependency('astal-network-0.1'), + dependency('libnm'), + dependency('astal-mpris-0.1'), + dependency('astal-power-profiles-0.1'), + dependency('astal-tray-0.1'), + dependency('astal-bluetooth-0.1'), +] + +blueprint_sources = files( + 'ui/Bar.blp', +) + +vala_sources = files( + 'vala/Bar.vala', + 'vala/App.vala', +) + +# transplie blueprints +ui = custom_target( + 'blueprint', + input: blueprint_sources, + output: '.', + command: [ + blp, + 'batch-compile', + '@OUTPUT@', + '@CURRENT_SOURCE_DIR@', + '@INPUT@', + ], +) + +# bundle scss files +css = custom_target( + 'scss', + input: files('main.scss'), + command: [sass, '@INPUT@', '@OUTPUT@'], + output: ['main.css'], +) + +# compiling data files into a binary +resource = import('gnome').compile_resources( + 'data', + files('gresource.xml'), + dependencies: [ui, css], + source_dir: meson.current_build_dir(), +) + +executable( + meson.project_name(), + dependencies: dependencies, + sources: [vala_sources, resource], + link_args: ['-lm'], # Link math library + install: true, + install_dir: bindir, +) diff --git a/examples/gtk4/simple-bar/vala/src/scss/Bar.scss b/examples/gtk4/simple-bar/vala/src/scss/Bar.scss new file mode 100644 index 0000000..86ea856 --- /dev/null +++ b/examples/gtk4/simple-bar/vala/src/scss/Bar.scss @@ -0,0 +1,19 @@ +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-4-16/gtk/theme/Default/_colors-public.scss +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; + +window.Bar { + > box { + background: $bg-color; + color: $fg-color; + font-weight: bold; + } + + button { + min-height: 0; + min-width: 0; + border-radius: 8px; + margin: 4px; + padding: 4px 8px; + } +} diff --git a/examples/gtk4/simple-bar/vala/src/ui/Bar.blp b/examples/gtk4/simple-bar/vala/src/ui/Bar.blp new file mode 100644 index 0000000..6e401e7 --- /dev/null +++ b/examples/gtk4/simple-bar/vala/src/ui/Bar.blp @@ -0,0 +1,84 @@ +using Gtk 4.0; +using Astal 4.0; + +template $Bar: Astal.Window { + CenterBox centerbox { + start-widget: Box { + MenuButton { + Label { + label: bind template.clock; + } + + popover: Popover popover { + Calendar calendar { + show-day-names: true; + show-heading: true; + show-week-numbers: true; + } + }; + } + }; + + center-widget: Box { + Box { + visible: bind template.mpris-visible; + + Image { + file: bind template.mpris-art; + } + + Label { + label: bind template.mpris-label; + } + } + }; + + end-widget: Box { + spacing: 4; + + Image { + visible: bind template.bluetooth-visible; + icon-name: "bluetooth-symbolic"; + } + + Image { + icon-name: bind template.power-profile-icon; + } + + Image { + icon-name: bind template.network-icon; + } + + Box { + Image { + icon-name: bind template.volume-icon; + } + + Scale { + width-request: 100; + change-value => $change_volume(); + + adjustment: Adjustment { + value: bind template.volume; + lower: 0; + upper: 1; + }; + } + } + + Box { + Image { + icon-name: bind template.battery-icon; + } + + Label { + label: bind template.battery-label; + } + } + + Box traybox { + spacing: 4; + } + }; + } +} diff --git a/examples/gtk4/simple-bar/vala/src/vala/App.vala b/examples/gtk4/simple-bar/vala/src/vala/App.vala new file mode 100644 index 0000000..20be55d --- /dev/null +++ b/examples/gtk4/simple-bar/vala/src/vala/App.vala @@ -0,0 +1,45 @@ +class App : Astal.Application { + static App instance; + + // this is where request handlers can be implemented + // that will be used to handle `astal` cli invocations + public override void request(string request, GLib.SocketConnection conn) { + print(@"incoming request: $request\n"); + + AstalIO.write_sock.begin(conn, "response", null); + } + + // this is the method that will be invoked on `app.run()` + // this is where everything should be initialized and instantiated + public override void activate() { + this.apply_css("resource:///main.css", false); + this.add_window(new Bar()); + } + + // entry point of our app + static int main(string[] argv) { + App.instance = new App() { instance_name = "simple-bar" }; + + try { + // `app.acquire_socket()` needed for the request API to work + App.instance.acquire_socket(); + + // if it succeeds we can run the app + return App.instance.run(null); + } catch (Error _) { + // if it throws an error it means there is already an instance + // with `instance_name` running, so we just send a request instead + try { + var response = AstalIO.send_request( + "simple-bar", + string.joinv(" ", argv[1:]) + ); + print(@"$response\n"); + return 0; + } catch (Error err) { + printerr(err.message); + return 1; + } + } + } +} diff --git a/examples/gtk4/simple-bar/vala/src/vala/Bar.vala b/examples/gtk4/simple-bar/vala/src/vala/Bar.vala new file mode 100644 index 0000000..010b97e --- /dev/null +++ b/examples/gtk4/simple-bar/vala/src/vala/Bar.vala @@ -0,0 +1,186 @@ +[GtkTemplate(ui="/ui/Bar.ui")] +class Bar : Astal.Window { + public string clock { get; set; } + public string volume_icon { get; set; } + public string battery_visible { get; set; } + public string battery_label { get; set; } + public string battery_icon { get; set; } + public double volume { get; set; } + public string network_icon { get; set; } + public bool mpris_visible { get; set; } + public string mpris_label { get; set; } + public string mpris_art { get; set; } + public string power_profile_icon { get; set; } + public bool bluetooth_visible { get; set; } + + AstalIO.Time timer; + AstalMpris.Player player; + HashTable<string, TrayButton> tray_items; + + [GtkChild] unowned Gtk.Popover popover; + [GtkChild] unowned Gtk.Calendar calendar; + [GtkChild] unowned Gtk.Box traybox; + + public Bar() { + anchor = TOP | LEFT | RIGHT; + exclusivity = EXCLUSIVE; + add_css_class("Bar"); + present(); + + // clock + timer = AstalIO.Time.interval(1000, null); + timer.now.connect(() => { + clock = new DateTime.now_local().format("%H:%M:%S"); + }); + + // everytime popover is opened, select current day + popover.notify["visible"].connect(() => { + if (popover.visible) { + calendar.select_day(new DateTime.now_local()); + } + }); + + // network + var nw = AstalNetwork.get_default(); + Binding networkBinding = null; + + nw.bind_property( + "primary", + this, + "network-icon", + BindingFlags.SYNC_CREATE, + (_, primary) => { + if (networkBinding != null) networkBinding.unbind(); + + switch (primary.get_enum()) { + case AstalNetwork.Primary.WIRED: + networkBinding = nw.wired.bind_property( + "icon-name", + this, + "network-icon", + BindingFlags.SYNC_CREATE + ); + return false; + + case AstalNetwork.Primary.WIFI: + networkBinding = nw.wifi.bind_property( + "icon-name", + this, + "network-icon", + BindingFlags.SYNC_CREATE + ); + return false; + + default: + network_icon = "network-idle-symbolic"; + return false; + } + }, + null + ); + + // battery + var bat = AstalBattery.get_default(); + bat.bind_property("is-present", this, "battery-visible", BindingFlags.SYNC_CREATE); + bat.bind_property("icon-name", this, "battery-icon", BindingFlags.SYNC_CREATE); + bat.bind_property("percentage", this, "battery-label", BindingFlags.SYNC_CREATE, (_, src, ref target) => { + target.set_string(@"$(Math.floor(bat.percentage * 100))%"); + return true; + }, null); + + // volume + var speaker = AstalWp.get_default().get_default_speaker(); + speaker.bind_property("volume-icon", this, "volume-icon", BindingFlags.SYNC_CREATE); + speaker.bind_property("volume", this, "volume", BindingFlags.SYNC_CREATE); + + // mpris + player = new AstalMpris.Player("spotify"); + player.bind_property("available", this, "mpris-visible", BindingFlags.SYNC_CREATE); + player.bind_property("cover-art", this, "mpris-art", BindingFlags.SYNC_CREATE); + player.bind_property("metadata", this, "mpris-label", BindingFlags.SYNC_CREATE, (_, src, ref target) => { + if (player.title == null || player.artist == null) { + return false; + } + target.set_string(@"$(player.artist) - $(player.title)"); + return true; + }, null); + + // powerprofiles + var powerprofile = AstalPowerProfiles.get_default(); + powerprofile.bind_property("icon-name", this, "power-profile-icon", BindingFlags.SYNC_CREATE); + + // tray + var tray = AstalTray.get_default(); + tray_items = new HashTable<string, TrayButton>(str_hash, str_equal); + tray.item_added.connect(on_tray_item_added); + tray.item_removed.connect(on_tray_item_removed); + + // bluetooth + var bt = AstalBluetooth.get_default(); + bt.bind_property("is-connected", this, "bluetooth-visible", BindingFlags.SYNC_CREATE); + } + + void on_tray_item_added(AstalTray.Tray tray, string id) { + var button = new TrayButton(id); + tray_items.set(id, button); + traybox.append(button); + } + + void on_tray_item_removed(string id) { + var button = tray_items.get(id); + traybox.remove(button); + tray_items.remove(id); + } + + [GtkCallback] + bool change_volume(Gtk.Range scale, Gtk.ScrollType type, double value) { + AstalWp.get_default().get_default_speaker().set_volume(value); + return true; + } + + public override void dispose() { + var tray = AstalTray.get_default(); + tray.item_added.disconnect(on_tray_item_added); + tray.item_removed.disconnect(on_tray_item_removed); + + foreach (var button in tray_items.get_values()) { + button.dispose(); + } + + timer.cancel(); + timer.dispose(); + player.dispose(); + base.dispose(); + } + + class TrayButton : Astal.Bin { + AstalTray.TrayItem item; + Gtk.Popover popover; + Gtk.Image image; + + public TrayButton(string id) { + var tray = AstalTray.get_default(); + item = tray.get_item(id); + + image = new Gtk.Image(); + popover = new Gtk.PopoverMenu.from_model(item.menu_model); + + child = new Gtk.MenuButton() { + child = image, + popover = popover, + }; + + item.bind_property("gicon", image, "gicon", BindingFlags.SYNC_CREATE); + popover.insert_action_group("dbusmenu", item.action_group); + item.notify["action-group"].connect(on_action_group); + } + + void on_action_group() { + popover.insert_action_group("dbusmenu", item.action_group); + } + + public override void dispose() { + item.notify.disconnect(on_action_group); + } + } +} |