summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--flake.lock61
-rw-r--r--flake.nix33
-rw-r--r--license.txt21
-rw-r--r--meson.build26
-rw-r--r--nix/package.nix68
-rw-r--r--pub/default.pngbin0 -> 3147573 bytes
-rw-r--r--pub/foobar.166
-rw-r--r--pub/teatime.conf65
-rw-r--r--pub/teatime.pngbin0 -> 4407671 bytes
-rw-r--r--readme.md79
-rw-r--r--res/icons/README.md27
-rw-r--r--res/icons/fluent-battery-0-symbolic.svg3
-rw-r--r--res/icons/fluent-battery-1-symbolic.svg3
-rw-r--r--res/icons/fluent-battery-10-symbolic.svg3
-rw-r--r--res/icons/fluent-battery-2-symbolic.svg3
-rw-r--r--res/icons/fluent-battery-3-symbolic.svg3
-rw-r--r--res/icons/fluent-battery-4-symbolic.svg3
-rw-r--r--res/icons/fluent-battery-5-symbolic.svg3
-rw-r--r--res/icons/fluent-battery-6-symbolic.svg3
-rw-r--r--res/icons/fluent-battery-7-symbolic.svg3
-rw-r--r--res/icons/fluent-battery-8-symbolic.svg3
-rw-r--r--res/icons/fluent-battery-9-symbolic.svg3
-rw-r--r--res/icons/fluent-battery-charge-symbolic.svg3
-rw-r--r--res/icons/fluent-bluetooth-connected-symbolic.svg3
-rw-r--r--res/icons/fluent-bluetooth-off-symbolic.svg3
-rw-r--r--res/icons/fluent-bluetooth-symbolic.svg3
-rw-r--r--res/icons/fluent-brightness-high-symbolic.svg3
-rw-r--r--res/icons/fluent-brightness-low-symbolic.svg3
-rw-r--r--res/icons/fluent-checkmark-circle-symbolic.svg3
-rw-r--r--res/icons/fluent-checkmark-symbolic.svg3
-rw-r--r--res/icons/fluent-chevron-right-symbolic.svg3
-rw-r--r--res/icons/fluent-dismiss-symbolic.svg3
-rw-r--r--res/icons/fluent-grid-dots-symbolic.svg3
-rw-r--r--res/icons/fluent-microphone-symbolic.svg3
-rw-r--r--res/icons/fluent-more-circle-symbolic.svg3
-rw-r--r--res/icons/fluent-number-circle-1-symbolic.svg3
-rw-r--r--res/icons/fluent-number-circle-2-symbolic.svg3
-rw-r--r--res/icons/fluent-number-circle-3-symbolic.svg3
-rw-r--r--res/icons/fluent-number-circle-4-symbolic.svg3
-rw-r--r--res/icons/fluent-number-circle-5-symbolic.svg3
-rw-r--r--res/icons/fluent-number-circle-6-symbolic.svg3
-rw-r--r--res/icons/fluent-number-circle-7-symbolic.svg3
-rw-r--r--res/icons/fluent-number-circle-8-symbolic.svg3
-rw-r--r--res/icons/fluent-number-circle-9-symbolic.svg3
-rw-r--r--res/icons/fluent-plug-connected-symbolic.svg3
-rw-r--r--res/icons/fluent-search-symbolic.svg3
-rw-r--r--res/icons/fluent-speaker-0-symbolic.svg3
-rw-r--r--res/icons/fluent-speaker-1-symbolic.svg3
-rw-r--r--res/icons/fluent-speaker-2-symbolic.svg3
-rw-r--r--res/icons/fluent-speaker-mute-symbolic.svg3
-rw-r--r--res/icons/fluent-speaker-off-symbolic.svg3
-rw-r--r--res/icons/fluent-virtual-network-symbolic.svg3
-rw-r--r--res/icons/fluent-wifi-1-symbolic.svg3
-rw-r--r--res/icons/fluent-wifi-2-symbolic.svg3
-rw-r--r--res/icons/fluent-wifi-3-symbolic.svg3
-rw-r--r--res/icons/fluent-wifi-4-symbolic.svg3
-rw-r--r--res/icons/fluent-wifi-off-symbolic.svg3
-rw-r--r--res/icons/fluent-wifi-warning-symbolic.svg3
-rw-r--r--res/icons/icons.gresource.xml52
-rw-r--r--res/icons/meson.build9
-rw-r--r--res/meson.build2
-rw-r--r--res/styles/_common.scss5
-rw-r--r--res/styles/default.scss1
-rw-r--r--res/styles/meson.build32
-rw-r--r--res/styles/monokai-pro.scss45
-rw-r--r--res/styles/partials/_base.scss85
-rw-r--r--res/styles/partials/_control-center.scss142
-rw-r--r--res/styles/partials/_launcher.scss67
-rw-r--r--res/styles/partials/_notification.scss67
-rw-r--r--res/styles/partials/_panel.scss129
-rw-r--r--res/styles/styles.gresource.xml8
-rw-r--r--res/styles/teatime.scss46
-rw-r--r--src/application.c686
-rw-r--r--src/application.h19
-rw-r--r--src/control-center.c821
-rw-r--r--src/control-center.h27
-rw-r--r--src/dbus/com.github.hannesschulze.foobar.xml8
-rw-r--r--src/dbus/meson.build37
-rw-r--r--src/dbus/org.bluez.xml66
-rw-r--r--src/dbus/org.freedesktop.Notifications.xml35
-rw-r--r--src/dbus/org.freedesktop.UPower.xml12
-rw-r--r--src/launcher.c619
-rw-r--r--src/launcher.h18
-rw-r--r--src/main.c12
-rw-r--r--src/meson.build41
-rw-r--r--src/notification-area.c196
-rw-r--r--src/notification-area.h18
-rw-r--r--src/panel.c348
-rw-r--r--src/panel.h34
-rw-r--r--src/services/application-service.c733
-rw-r--r--src/services/application-service.h29
-rw-r--r--src/services/audio-service.c930
-rw-r--r--src/services/audio-service.h44
-rw-r--r--src/services/battery-service.c378
-rw-r--r--src/services/battery-service.h36
-rw-r--r--src/services/bluetooth-service.c949
-rw-r--r--src/services/bluetooth-service.h41
-rw-r--r--src/services/brightness-service.c391
-rw-r--r--src/services/brightness-service.h16
-rw-r--r--src/services/clock-service.c141
-rw-r--r--src/services/clock-service.h14
-rw-r--r--src/services/configuration-service.c3248
-rw-r--r--src/services/configuration-service.h305
-rw-r--r--src/services/meson.build12
-rw-r--r--src/services/network-service.c1577
-rw-r--r--src/services/network-service.h62
-rw-r--r--src/services/notification-service.c1833
-rw-r--r--src/services/notification-service.h56
-rw-r--r--src/services/workspace-service.c1355
-rw-r--r--src/services/workspace-service.h42
-rw-r--r--src/utils.c25
-rw-r--r--src/utils.h9
-rw-r--r--src/widgets/control-center/control-button.c496
-rw-r--r--src/widgets/control-center/control-button.h31
-rw-r--r--src/widgets/control-center/control-details-item.c307
-rw-r--r--src/widgets/control-center/control-details-item.h32
-rw-r--r--src/widgets/control-center/control-details.c289
-rw-r--r--src/widgets/control-center/control-details.h22
-rw-r--r--src/widgets/control-center/control-slider.c428
-rw-r--r--src/widgets/control-center/control-slider.h28
-rw-r--r--src/widgets/control-center/meson.build6
-rw-r--r--src/widgets/inset-container.c363
-rw-r--r--src/widgets/inset-container.h22
-rw-r--r--src/widgets/limit-container.c341
-rw-r--r--src/widgets/limit-container.h22
-rw-r--r--src/widgets/meson.build8
-rw-r--r--src/widgets/notification-widget.c821
-rw-r--r--src/widgets/notification-widget.h50
-rw-r--r--src/widgets/panel/meson.build7
-rw-r--r--src/widgets/panel/panel-item-clock.c152
-rw-r--r--src/widgets/panel/panel-item-clock.h15
-rw-r--r--src/widgets/panel/panel-item-icon.c92
-rw-r--r--src/widgets/panel/panel-item-icon.h13
-rw-r--r--src/widgets/panel/panel-item-status.c955
-rw-r--r--src/widgets/panel/panel-item-status.h25
-rw-r--r--src/widgets/panel/panel-item-workspaces.c327
-rw-r--r--src/widgets/panel/panel-item-workspaces.h16
-rw-r--r--src/widgets/panel/panel-item.c195
-rw-r--r--src/widgets/panel/panel-item.h23
-rw-r--r--subprojects/gtk4-layer-shell.wrap4
-rw-r--r--subprojects/gvc.wrap4
142 files changed, 21599 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c36f106
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+buildDir/
+build/
+.idea/
+.vscode/
+subprojects/*/
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..07758f2
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,61 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1710146030,
+ "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1714906307,
+ "narHash": "sha256-UlRZtrCnhPFSJlDQE7M0eyhgvuuHBTe1eJ9N9AQlJQ0=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "25865a40d14b3f9cf19f19b924e2ab4069b09588",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..f344ca7
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,33 @@
+{
+ description = "A simple bar, launcher, control center, and notification daemon.";
+
+ inputs = {
+ nixpkgs = {
+ url = "github:nixos/nixpkgs/nixos-unstable";
+ };
+ flake-utils = {
+ url = "github:numtide/flake-utils";
+ };
+ };
+
+ outputs = { self, nixpkgs, flake-utils }:
+ flake-utils.lib.eachDefaultSystem (system:
+ let
+ pkgs = import nixpkgs { inherit system; };
+ foobar = (pkgs.callPackage ./nix/package.nix {});
+ in
+ {
+ packages = {
+ inherit foobar;
+ default = foobar;
+ };
+
+ devShells = {
+ algoks = pkgs.mkShell {
+ name = "foobar";
+ packages = [ pkgs.libnotify ]; # for testing the notification daemon
+ inputsFrom = foobar;
+ };
+ };
+ });
+}
diff --git a/license.txt b/license.txt
new file mode 100644
index 0000000..8275962
--- /dev/null
+++ b/license.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024, the Foobar developers
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE. \ No newline at end of file
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..8beb181
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,26 @@
+project('foobar', 'c', default_options: ['c_std=gnu11', 'warning_level=1'])
+
+gnome = import('gnome')
+
+glib_dep = dependency('glib-2.0', version: '>=2.78')
+gio_dep = dependency('gio-2.0', version: '>=2.78')
+gio_unix_dep = dependency('gio-unix-2.0', version: '>=2.78')
+gtk_dep = dependency('gtk4', version: '>=4.12')
+json_dep = dependency('json-glib-1.0', version: '>=1.8')
+nm_dep = dependency('libnm', version: '>=1.44')
+m_dep = meson.get_compiler('c').find_library('m', required : false)
+layershell_dep = dependency('gtk4-layer-shell', required: false)
+gvc_dep = dependency('gvc', required: false)
+if not layershell_dep.found()
+ layershell_proj = subproject('gtk4-layer-shell')
+ layershell_dep = layershell_proj.get_variable('gtk_layer_shell')
+endif
+if not gvc_dep.found()
+ gvc_proj = subproject('gvc')
+ gvc_dep = gvc_proj.get_variable('libgvc_dep')
+endif
+
+subdir('res')
+subdir('src')
+
+install_man('pub/foobar.1') \ No newline at end of file
diff --git a/nix/package.nix b/nix/package.nix
new file mode 100644
index 0000000..4c47e83
--- /dev/null
+++ b/nix/package.nix
@@ -0,0 +1,68 @@
+{
+ stdenv,
+ lib,
+ fetchFromGitHub,
+ fetchFromGitLab,
+ makeWrapper,
+ git,
+ meson,
+ ninja,
+ vala,
+ sassc,
+ pkg-config,
+ gobject-introspection,
+ wayland-scanner,
+ glib,
+ gtk4,
+ json-glib,
+ librsvg,
+ networkmanager,
+ wayland,
+ libpulseaudio,
+ alsa-lib,
+ upower,
+ brightnessctl
+}:
+ let
+ dep-gtk4-layer-shell = fetchFromGitHub {
+ owner = "wmww";
+ repo = "gtk4-layer-shell";
+ rev = "v1.0.2";
+ hash = "sha256-decjPkFkYy7kIjyozsB7BEmw33wzq1EQyIBrxO36984=";
+ };
+ dep-gvc = fetchFromGitLab {
+ domain = "gitlab.gnome.org";
+ owner = "GNOME";
+ repo = "libgnome-volume-control";
+ rev = "91f3f41490666a526ed78af744507d7ee1134323";
+ hash = "sha256-lpDWVlRFngMSNfACrfJ5vRTZ2xdlwcrh4/YGcNDogys=";
+ };
+ in stdenv.mkDerivation {
+ pname = "foobar";
+ version = "1.0.0";
+ src = ./..;
+ postUnpack = ''
+ pushd "$sourceRoot"
+ cp -R --no-preserve=mode,ownership ${dep-gtk4-layer-shell} subprojects/gtk4-layer-shell
+ cp -R --no-preserve=mode,ownership ${dep-gvc} subprojects/gvc
+ popd
+ '';
+ # needed to enable support for SVG icons in GTK
+ postInstall = ''
+ wrapProgram "$out/bin/foobar" \
+ --set GDK_PIXBUF_MODULE_FILE ${librsvg.out}/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache
+ '';
+
+ nativeBuildInputs = [ makeWrapper git meson ninja vala sassc pkg-config gobject-introspection wayland-scanner ];
+ buildInputs = [ glib gtk4 json-glib librsvg networkmanager wayland libpulseaudio alsa-lib upower brightnessctl ];
+
+ meta = with lib; {
+ homepage = "https://github.com/hannesschulze/foobar";
+ description = "A simple bar, launcher, control center, and notification daemon.";
+ maintainers = [
+ { name = "Hannes Schulze"; }
+ ];
+ license = licenses.mit;
+ mainProgram = "foobar";
+ };
+ }
diff --git a/pub/default.png b/pub/default.png
new file mode 100644
index 0000000..ce70d22
--- /dev/null
+++ b/pub/default.png
Binary files differ
diff --git a/pub/foobar.1 b/pub/foobar.1
new file mode 100644
index 0000000..117a283
--- /dev/null
+++ b/pub/foobar.1
@@ -0,0 +1,66 @@
+.TH FOOBAR 1 "April 2024" "1.0.0" "User Commands"
+
+.SH NAME
+.PP
+foobar \- a simple bar, launcher, control center, and notification daemon.
+
+.SH SYNPOSIS
+.PP
+.B foobar
+.RI [\| "ACTION" \|]...
+.br
+.B foobar
+.B \-h
+
+.SH DESCRIPTION
+.PP
+.B foobar
+can be used to launch the main
+.I server
+instance of the bar application, or to send commands to an active server process.
+
+If there is no other instance running, the command only launches the bar regardless of the
+.I actions
+passed to it. Otherwise, no new instance is launched and all the specified
+.I actions
+are processed one after another.
+
+.SH OPTIONS
+.TP
+.BR \-q ", " \-\-quit
+Kindly ask the active bar instance to kill itself.
+.TP
+.BR \-l ", " \-\-toggle\-launcher
+Toggle the visibility of the application's
+.I launcher
+window. If not previously shown, it will appear on the currently active monitor.
+.TP
+.BR \-c ", " \-\-toggle\-control\-center
+Toggle the visibility of the application's
+.I control center
+window. If not previously shown, it will appear on the currently active monitor.
+.TP
+.BR \-i ", " \-\-inspector
+Open the GTK inspector for the active bar instance. This can be useful for inspecting the widget hierarchy or quickly testing changes to the application's stylesheet.
+
+.SH FILES
+.TP
+.I $XDG_CONFIG_HOME/foobar.conf
+The main configuration for all components of the application.
+
+This file is created and initialized to the default configuration when
+.I foobar
+is first launched.
+
+.SH BUGS
+.TP
+Submit bug reports and request features online at:
+<\f[I]https://github.com/hannesschulze/foobar/issues\f[R]>
+
+.SH SEE ALSO
+.PP
+Sources at: <\f[I]https://github.com/hannesschulze/foobar\f[R]>
+
+.SH COPYRIGHT
+.PP
+Copyright (c) 2024, the Foobar developers \ No newline at end of file
diff --git a/pub/teatime.conf b/pub/teatime.conf
new file mode 100644
index 0000000..21c02b5
--- /dev/null
+++ b/pub/teatime.conf
@@ -0,0 +1,65 @@
+[general]
+stylesheet = resource:///foobar/styles/teatime.css
+
+[panel]
+position = top
+margin = 16
+padding = 12
+size = 40
+spacing = 12
+multi-monitor = true
+
+[panel.workspaces]
+kind = workspaces
+position = center
+action = none
+button-size = 20
+spacing = 6
+
+[panel.status-left]
+kind = status
+position = start
+action = none
+items = battery;network
+spacing = 6
+show-labels = true
+enable-scrolling = false
+action = none
+
+[panel.status-right]
+kind = status
+position = end
+action = none
+items = brightness;audio;bluetooth;notifications;
+spacing = 6
+show-labels = false
+enable-scrolling = true
+
+[panel.clock]
+kind = clock
+position = end
+action = none
+format = %H:%M
+
+[launcher]
+width = 600
+position = 240
+max-height = 400
+
+[control-center]
+width = 600
+height = 400
+position = top
+offset = 16
+padding = 24
+spacing = 12
+orientation = horizontal
+alignment = center
+rows = connectivity;audio-output;brightness;
+
+[notifications]
+width = 400
+min-height = 48
+spacing = 16
+close-button-inset = -6
+time-format = %H:%M \ No newline at end of file
diff --git a/pub/teatime.png b/pub/teatime.png
new file mode 100644
index 0000000..f4f8f38
--- /dev/null
+++ b/pub/teatime.png
Binary files differ
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..6d346e6
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,79 @@
+# Foobar
+
+[![License](https://img.shields.io/github/license/hannesschulze/foobar)](license.txt)
+[![Release](https://img.shields.io/github/v/release/hannesschulze/foobar?sort=semver)](https://github.com/hannesschulze/foobar/releases)
+
+Foobar is a bar, launcher, control center, and notification daemon I wrote for my personal desktop.
+
+Although it was not designed to be a fully modular widget system like [ags](https://github.com/Aylur/ags) or [eww](https://github.com/elkowar/eww.git), it does offer some customization options.
+
+| Default Configuration | A [Customized](pub/teatime.conf) Configuration |
+|:---:|:---:|
+| ![](pub/default.png) | ![](pub/teatime.png) |
+
+## Features
+
+Current features include:
+- **Panel** with customizable items
+ - **Icon item:** only displays an icon (duh)
+ - **Clock item:** displays the current time
+ - **Workspace item:** displays active workspaces (for hyprland)
+ - **Status item:** displays the current status for things like battery level, brightness level, volume, network connectivity, notifications
+- **Launcher**
+ - Currently only supports launching applications based on `.desktop` files
+- **Control Center** with two sections
+ - **Controls section:** allows managing audio devices, brightness level, etc.
+ - **Notifications section:** shows previous notifications
+- **Notification Area** which displays incoming notifications
+
+## Installation
+
+### Dependencies
+
+These dependencies must be present before building:
+
+- `meson`
+- `ninja`
+- `glib`
+- `gtk4`
+- `json-glib`
+- `alsa-lib`
+- `libpulseaudio`
+- `libnm`
+- `libwayland`
+- `gobject-introspection`
+- [`gtk4-layer-shell`](https://github.com/wmww/gtk4-layer-shell) (otherwise it will be built as a subproject)
+
+In addition, these dependencies should be available at runtime:
+
+- `upower` (for battery state)
+- `brightnessctl` (for adjusting brightness level)
+- `hyprland` (for listing workspaces)
+
+### Building
+
+To manually build foobar, run the following commands:
+
+```sh
+git clone https://github.com/hannesschulze/foobar.git && cd foobar
+meson setup build --prefix=/usr
+ninja -C build
+```
+
+Then, install it using the following command:
+
+```sh
+sudo ninja install -C build
+```
+
+## Usage
+
+Please refer to the man pages that are automatically installed:
+
+```sh
+man foobar
+```
+
+## License
+
+This project is licensed under the MIT License - see the [license.txt](license.txt) file for details. \ No newline at end of file
diff --git a/res/icons/README.md b/res/icons/README.md
new file mode 100644
index 0000000..ba405a2
--- /dev/null
+++ b/res/icons/README.md
@@ -0,0 +1,27 @@
+# Icons
+
+The icons in this directory were imported from the [Fluent System Icons](https://github.com/microsoft/fluentui-system-icons) collection, released under the MIT license:
+
+```
+MIT License
+
+Copyright (c) 2020 Microsoft Corporation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+``` \ No newline at end of file
diff --git a/res/icons/fluent-battery-0-symbolic.svg b/res/icons/fluent-battery-0-symbolic.svg
new file mode 100644
index 0000000..9bd2b77
--- /dev/null
+++ b/res/icons/fluent-battery-0-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5 5C14.8807 5 16 6.11929 16 7.5V8.33333L17.1667 8.3356C17.3184 8.3356 17.4608 8.37619 17.5833 8.4471C17.8324 8.59119 18 8.86052 18 9.16894V10.8356C18 11.144 17.8324 11.4134 17.5833 11.5574C17.4608 11.6284 17.3184 11.6689 17.1667 11.6689L16 11.6667V12.5C16 13.8807 14.8807 15 13.5 15H4.5C3.11929 15 2 13.8807 2 12.5V7.5C2 6.11929 3.11929 5 4.5 5H13.5ZM13.75 5.94604H4.5C3.85025 5.94604 3.09484 6.44178 3.00822 7.07566L3 7.19604V12.6913C3 13.3385 3.49187 13.8709 4.12219 13.9349L4.25 13.9413H13.75C14.3972 13.9413 14.9295 13.4494 14.9935 12.8191L15 12.6913V7.19604C15 6.54883 14.5081 6.01651 13.8778 5.9525L13.75 5.94604Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-battery-1-symbolic.svg b/res/icons/fluent-battery-1-symbolic.svg
new file mode 100644
index 0000000..46974a1
--- /dev/null
+++ b/res/icons/fluent-battery-1-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5 5C14.8807 5 16 6.11929 16 7.5V8.33333L17.1667 8.3356C17.3184 8.3356 17.4608 8.37619 17.5833 8.4471C17.8324 8.59119 18 8.86052 18 9.16894V10.8356C18 11.144 17.8324 11.4134 17.5833 11.5574C17.4608 11.6284 17.3184 11.6689 17.1667 11.6689L16 11.6667V12.5C16 13.8807 14.8807 15 13.5 15H4.5C3.11929 15 2 13.8807 2 12.5V7.5C2 6.11929 3.11929 5 4.5 5H13.5ZM13.75 5.94604H4.5C3.85025 5.94604 3.09484 6.44178 3.00822 7.07566L3 7.19604V12.6913C3 13.3385 3.49187 13.8709 4.12219 13.9349L4.25 13.9413H13.75C14.3972 13.9413 14.9295 13.4494 14.9935 12.8191L15 12.6913V7.19604C15 6.54883 14.5081 6.01651 13.8778 5.9525L13.75 5.94604ZM4.5 6.94604C4.77614 6.94604 5 7.1699 5 7.44604V12.446C5 12.7222 4.77614 12.946 4.5 12.946C4.22386 12.946 4 12.7222 4 12.446V7.44604C4 7.1699 4.22386 6.94604 4.5 6.94604Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-battery-10-symbolic.svg b/res/icons/fluent-battery-10-symbolic.svg
new file mode 100644
index 0000000..23bad06
--- /dev/null
+++ b/res/icons/fluent-battery-10-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5 5C14.8807 5 16 6.11929 16 7.5V8.33333L17.1667 8.3356C17.3184 8.3356 17.4608 8.37619 17.5833 8.4471C17.8324 8.59119 18 8.86052 18 9.16894V10.8356C18 11.144 17.8324 11.4134 17.5833 11.5574C17.4608 11.6284 17.3184 11.6689 17.1667 11.6689L16 11.6667V12.5C16 13.8807 14.8807 15 13.5 15H4.5C3.11929 15 2 13.8807 2 12.5V7.5C2 6.11929 3.11929 5 4.5 5H13.5ZM13.75 5.94604H4.5C3.85025 5.94604 3.09484 6.44178 3.00822 7.07566L3 7.19604V12.6913C3 13.3385 3.49187 13.8709 4.12219 13.9349L4.25 13.9413H13.75C14.3972 13.9413 14.9295 13.4494 14.9935 12.8191L15 12.6913V7.19604C15 6.54883 14.5081 6.01651 13.8778 5.9525L13.75 5.94604ZM4.83436 6.94867H13.1646C13.5905 6.94867 13.9419 7.26736 13.9935 7.67926L14 7.78405V12.1107C14 12.5366 13.6813 12.888 13.2694 12.9395L13.1646 12.946H4.83436C4.40848 12.946 4.05704 12.6274 4.00549 12.2155L3.99898 12.1107V7.78405C3.99898 7.35818 4.31767 7.00673 4.72957 6.95518L4.83436 6.94867H13.1646H4.83436Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-battery-2-symbolic.svg b/res/icons/fluent-battery-2-symbolic.svg
new file mode 100644
index 0000000..194bebb
--- /dev/null
+++ b/res/icons/fluent-battery-2-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5 5C14.8807 5 16 6.11929 16 7.5V8.33333L17.1667 8.3356C17.3184 8.3356 17.4608 8.37619 17.5833 8.4471C17.8324 8.59119 18 8.86052 18 9.16894V10.8356C18 11.144 17.8324 11.4134 17.5833 11.5574C17.4608 11.6284 17.3184 11.6689 17.1667 11.6689L16 11.6667V12.5C16 13.8807 14.8807 15 13.5 15H4.5C3.11929 15 2 13.8807 2 12.5V7.5C2 6.11929 3.11929 5 4.5 5H13.5ZM13.75 5.94604H4.5C3.85025 5.94604 3.09484 6.44178 3.00822 7.07566L3 7.19604V12.6913C3 13.3385 3.49187 13.8709 4.12219 13.9349L4.25 13.9413H13.75C14.3972 13.9413 14.9295 13.4494 14.9935 12.8191L15 12.6913V7.19604C15 6.54883 14.5081 6.01651 13.8778 5.9525L13.75 5.94604ZM4.83436 6.94867H5.16462C5.5905 6.94867 5.94194 7.26736 5.99349 7.67926L6 7.78405V12.1107C6 12.5366 5.68131 12.888 5.26941 12.9395L5.16462 12.946H4.83436C4.40848 12.946 4.05704 12.6274 4.00549 12.2155L3.99898 12.1107V7.78405C3.99898 7.35818 4.31767 7.00673 4.72957 6.95518L4.83436 6.94867H5.16462H4.83436Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-battery-3-symbolic.svg b/res/icons/fluent-battery-3-symbolic.svg
new file mode 100644
index 0000000..e56f649
--- /dev/null
+++ b/res/icons/fluent-battery-3-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5 5C14.8807 5 16 6.11929 16 7.5V8.33333L17.1667 8.3356C17.3184 8.3356 17.4608 8.37619 17.5833 8.4471C17.8324 8.59119 18 8.86052 18 9.16894V10.8356C18 11.144 17.8324 11.4134 17.5833 11.5574C17.4608 11.6284 17.3184 11.6689 17.1667 11.6689L16 11.6667V12.5C16 13.8807 14.8807 15 13.5 15H4.5C3.11929 15 2 13.8807 2 12.5V7.5C2 6.11929 3.11929 5 4.5 5H13.5ZM13.75 5.94604H4.5C3.85025 5.94604 3.09484 6.44178 3.00822 7.07566L3 7.19604V12.6913C3 13.3385 3.49187 13.8709 4.12219 13.9349L4.25 13.9413H13.75C14.3972 13.9413 14.9295 13.4494 14.9935 12.8191L15 12.6913V7.19604C15 6.54883 14.5081 6.01651 13.8778 5.9525L13.75 5.94604ZM4.83436 6.94867H6.16462C6.5905 6.94867 6.94194 7.26736 6.99349 7.67926L7 7.78405V12.1107C7 12.5366 6.68131 12.888 6.26941 12.9395L6.16462 12.946H4.83436C4.40848 12.946 4.05704 12.6274 4.00549 12.2155L3.99898 12.1107V7.78405C3.99898 7.35818 4.31767 7.00673 4.72957 6.95518L4.83436 6.94867H6.16462H4.83436Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-battery-4-symbolic.svg b/res/icons/fluent-battery-4-symbolic.svg
new file mode 100644
index 0000000..5ad3df5
--- /dev/null
+++ b/res/icons/fluent-battery-4-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5 5C14.8807 5 16 6.11929 16 7.5V8.33333L17.1667 8.3356C17.3184 8.3356 17.4608 8.37619 17.5833 8.4471C17.8324 8.59119 18 8.86052 18 9.16894V10.8356C18 11.144 17.8324 11.4134 17.5833 11.5574C17.4608 11.6284 17.3184 11.6689 17.1667 11.6689L16 11.6667V12.5C16 13.8807 14.8807 15 13.5 15H4.5C3.11929 15 2 13.8807 2 12.5V7.5C2 6.11929 3.11929 5 4.5 5H13.5ZM13.75 5.94604H4.5C3.85025 5.94604 3.09484 6.44178 3.00822 7.07566L3 7.19604V12.6913C3 13.3385 3.49187 13.8709 4.12219 13.9349L4.25 13.9413H13.75C14.3972 13.9413 14.9295 13.4494 14.9935 12.8191L15 12.6913V7.19604C15 6.54883 14.5081 6.01651 13.8778 5.9525L13.75 5.94604ZM4.83436 6.94867H7.16462C7.5905 6.94867 7.94194 7.26736 7.99349 7.67926L8 7.78405V12.1107C8 12.5366 7.68131 12.888 7.26941 12.9395L7.16462 12.946H4.83436C4.40848 12.946 4.05704 12.6274 4.00549 12.2155L3.99898 12.1107V7.78405C3.99898 7.35818 4.31767 7.00673 4.72957 6.95518L4.83436 6.94867H7.16462H4.83436Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-battery-5-symbolic.svg b/res/icons/fluent-battery-5-symbolic.svg
new file mode 100644
index 0000000..a0fb733
--- /dev/null
+++ b/res/icons/fluent-battery-5-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5 5C14.8807 5 16 6.11929 16 7.5V8.33333L17.1667 8.3356C17.3184 8.3356 17.4608 8.37619 17.5833 8.4471C17.8324 8.59119 18 8.86052 18 9.16894V10.8356C18 11.144 17.8324 11.4134 17.5833 11.5574C17.4608 11.6284 17.3184 11.6689 17.1667 11.6689L16 11.6667V12.5C16 13.8807 14.8807 15 13.5 15H4.5C3.11929 15 2 13.8807 2 12.5V7.5C2 6.11929 3.11929 5 4.5 5H13.5ZM13.75 5.94604H4.5C3.85025 5.94604 3.09484 6.44178 3.00822 7.07566L3 7.19604V12.6913C3 13.3385 3.49187 13.8709 4.12219 13.9349L4.25 13.9413H13.75C14.3972 13.9413 14.9295 13.4494 14.9935 12.8191L15 12.6913V7.19604C15 6.54883 14.5081 6.01651 13.8778 5.9525L13.75 5.94604ZM4.83436 6.94867H8.16462C8.5905 6.94867 8.94194 7.26736 8.99349 7.67926L9 7.78405V12.1107C9 12.5366 8.68131 12.888 8.26941 12.9395L8.16462 12.946H4.83436C4.40848 12.946 4.05704 12.6274 4.00549 12.2155L3.99898 12.1107V7.78405C3.99898 7.35818 4.31767 7.00673 4.72957 6.95518L4.83436 6.94867H8.16462H4.83436Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-battery-6-symbolic.svg b/res/icons/fluent-battery-6-symbolic.svg
new file mode 100644
index 0000000..bb340c8
--- /dev/null
+++ b/res/icons/fluent-battery-6-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5 5C14.8807 5 16 6.11929 16 7.5V8.33333L17.1667 8.3356C17.3184 8.3356 17.4608 8.37619 17.5833 8.4471C17.8324 8.59119 18 8.86052 18 9.16894V10.8356C18 11.144 17.8324 11.4134 17.5833 11.5574C17.4608 11.6284 17.3184 11.6689 17.1667 11.6689L16 11.6667V12.5C16 13.8807 14.8807 15 13.5 15H4.5C3.11929 15 2 13.8807 2 12.5V7.5C2 6.11929 3.11929 5 4.5 5H13.5ZM13.75 5.94604H4.5C3.85025 5.94604 3.09484 6.44178 3.00822 7.07566L3 7.19604V12.6913C3 13.3385 3.49187 13.8709 4.12219 13.9349L4.25 13.9413H13.75C14.3972 13.9413 14.9295 13.4494 14.9935 12.8191L15 12.6913V7.19604C15 6.54883 14.5081 6.01651 13.8778 5.9525L13.75 5.94604ZM4.83436 6.94867H9.16462C9.5905 6.94867 9.94194 7.26736 9.99349 7.67926L10 7.78405V12.1107C10 12.5366 9.68131 12.888 9.26941 12.9395L9.16462 12.946H4.83436C4.40848 12.946 4.05704 12.6274 4.00549 12.2155L3.99898 12.1107V7.78405C3.99898 7.35818 4.31767 7.00673 4.72957 6.95518L4.83436 6.94867H9.16462H4.83436Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-battery-7-symbolic.svg b/res/icons/fluent-battery-7-symbolic.svg
new file mode 100644
index 0000000..60742ad
--- /dev/null
+++ b/res/icons/fluent-battery-7-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5 5C14.8807 5 16 6.11929 16 7.5V8.33333L17.1667 8.3356C17.3184 8.3356 17.4608 8.37619 17.5833 8.4471C17.8324 8.59119 18 8.86052 18 9.16894V10.8356C18 11.144 17.8324 11.4134 17.5833 11.5574C17.4608 11.6284 17.3184 11.6689 17.1667 11.6689L16 11.6667V12.5C16 13.8807 14.8807 15 13.5 15H4.5C3.11929 15 2 13.8807 2 12.5V7.5C2 6.11929 3.11929 5 4.5 5H13.5ZM13.75 5.94604H4.5C3.85025 5.94604 3.09484 6.44178 3.00822 7.07566L3 7.19604V12.6913C3 13.3385 3.49187 13.8709 4.12219 13.9349L4.25 13.9413H13.75C14.3972 13.9413 14.9295 13.4494 14.9935 12.8191L15 12.6913V7.19604C15 6.54883 14.5081 6.01651 13.8778 5.9525L13.75 5.94604ZM4.83436 6.94867H10.1646C10.5905 6.94867 10.9419 7.26736 10.9935 7.67926L11 7.78405V12.1107C11 12.5366 10.6813 12.888 10.2694 12.9395L10.1646 12.946H4.83436C4.40848 12.946 4.05704 12.6274 4.00549 12.2155L3.99898 12.1107V7.78405C3.99898 7.35818 4.31767 7.00673 4.72957 6.95518L4.83436 6.94867H10.1646H4.83436Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-battery-8-symbolic.svg b/res/icons/fluent-battery-8-symbolic.svg
new file mode 100644
index 0000000..e1c07dc
--- /dev/null
+++ b/res/icons/fluent-battery-8-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5 5C14.8807 5 16 6.11929 16 7.5V8.33333L17.1667 8.3356C17.3184 8.3356 17.4608 8.37619 17.5833 8.4471C17.8324 8.59119 18 8.86052 18 9.16894V10.8356C18 11.144 17.8324 11.4134 17.5833 11.5574C17.4608 11.6284 17.3184 11.6689 17.1667 11.6689L16 11.6667V12.5C16 13.8807 14.8807 15 13.5 15H4.5C3.11929 15 2 13.8807 2 12.5V7.5C2 6.11929 3.11929 5 4.5 5H13.5ZM13.75 5.94604H4.5C3.85025 5.94604 3.09484 6.44178 3.00822 7.07566L3 7.19604V12.6913C3 13.3385 3.49187 13.8709 4.12219 13.9349L4.25 13.9413H13.75C14.3972 13.9413 14.9295 13.4494 14.9935 12.8191L15 12.6913V7.19604C15 6.54883 14.5081 6.01651 13.8778 5.9525L13.75 5.94604ZM4.83436 6.94867H11.1646C11.5905 6.94867 11.9419 7.26736 11.9935 7.67926L12 7.78405V12.1107C12 12.5366 11.6813 12.888 11.2694 12.9395L11.1646 12.946H4.83436C4.40848 12.946 4.05704 12.6274 4.00549 12.2155L3.99898 12.1107V7.78405C3.99898 7.35818 4.31767 7.00673 4.72957 6.95518L4.83436 6.94867H11.1646H4.83436Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-battery-9-symbolic.svg b/res/icons/fluent-battery-9-symbolic.svg
new file mode 100644
index 0000000..a9ac134
--- /dev/null
+++ b/res/icons/fluent-battery-9-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5 5C14.8807 5 16 6.11929 16 7.5V8.33333L17.1667 8.3356C17.3184 8.3356 17.4608 8.37619 17.5833 8.4471C17.8324 8.59119 18 8.86052 18 9.16894V10.8356C18 11.144 17.8324 11.4134 17.5833 11.5574C17.4608 11.6284 17.3184 11.6689 17.1667 11.6689L16 11.6667V12.5C16 13.8807 14.8807 15 13.5 15H4.5C3.11929 15 2 13.8807 2 12.5V7.5C2 6.11929 3.11929 5 4.5 5H13.5ZM13.75 5.94604H4.5C3.85025 5.94604 3.09484 6.44178 3.00822 7.07566L3 7.19604V12.6913C3 13.3385 3.49187 13.8709 4.12219 13.9349L4.25 13.9413H13.75C14.3972 13.9413 14.9295 13.4494 14.9935 12.8191L15 12.6913V7.19604C15 6.54883 14.5081 6.01651 13.8778 5.9525L13.75 5.94604ZM4.83436 6.94867H12.1646C12.5905 6.94867 12.9419 7.26736 12.9935 7.67926L13 7.78405V12.1107C13 12.5366 12.6813 12.888 12.2694 12.9395L12.1646 12.946H4.83436C4.40848 12.946 4.05704 12.6274 4.00549 12.2155L3.99898 12.1107V7.78405C3.99898 7.35818 4.31767 7.00673 4.72957 6.95518L4.83436 6.94867H12.1646H4.83436Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-battery-charge-symbolic.svg b/res/icons/fluent-battery-charge-symbolic.svg
new file mode 100644
index 0000000..ce3b731
--- /dev/null
+++ b/res/icons/fluent-battery-charge-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M15.9958 7.35376L16 7.50065V8.33398L17.1667 8.34885C17.3184 8.34885 17.4608 8.38943 17.5833 8.46035C17.8047 8.58842 17.9618 8.81546 17.9939 9.08094L18 9.18218V10.8488C18 11.1573 17.8324 11.4266 17.5833 11.5707C17.4914 11.6239 17.3883 11.66 17.2786 11.6747L17.1667 11.6822H16V12.5007C16 13.8321 14.9593 14.9204 13.6469 14.9964L13.5 15.0007H7C6.69865 14.9964 6.49794 14.7991 6.49794 14.4962C6.49794 14.1933 6.69865 14.0007 7 14.0007H13.756C14.4057 14.0007 14.9397 13.5049 15.0003 12.871L15.006 12.7507V7.24493C15.006 6.59518 14.5102 6.06122 13.8764 6.00065L13.756 5.99493H9.53536C9.19962 5.99493 9 5.85665 9 5.49882C9 5.14099 9.22276 5.00065 9.53536 5.00065H13.5C14.8314 5.00065 15.9197 6.04142 15.9958 7.35376ZM6.29094 3.33969L6.37575 3.33398C6.69216 3.33398 6.95366 3.56911 6.99505 3.87418L7.00075 3.95898L7 5.00065L7.375 5.00633C7.72018 5.00633 8 5.28615 8 5.63133V7.54305C8 8.34846 7.34708 9.00138 6.54167 9.00138H3.45833C2.65292 9.00138 2 8.34846 2 7.54305V5.63133C2 5.28615 2.27982 5.00633 2.625 5.00633H2.99993L3 3.96467C3 3.64825 3.23513 3.38676 3.54019 3.34537L3.625 3.33967C3.94141 3.33967 4.20291 3.57479 4.24429 3.87986L4.25 3.96467L4.24993 5.00633L5.75 5.00065L5.75075 3.95898C5.75075 3.64257 5.98588 3.38108 6.29094 3.33969ZM5.5 9.00065V14.4999C5.5 14.7761 5.27614 14.9999 5 14.9999C4.72386 14.9999 4.5 14.7761 4.5 14.4999V9.00065H5.5ZM7 6.00065V7.50065C7 7.74611 6.82312 7.95026 6.58988 7.9926L6.5 8.00065H3.5C3.25454 8.00065 3.05039 7.82378 3.00806 7.59053L3 7.50065V6.00065H7Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-bluetooth-connected-symbolic.svg b/res/icons/fluent-bluetooth-connected-symbolic.svg
new file mode 100644
index 0000000..92c7b78
--- /dev/null
+++ b/res/icons/fluent-bluetooth-connected-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.30867 2.03807C9.4955 1.96068 9.71056 2.00346 9.85356 2.14646L13.8536 6.14646C13.9514 6.24431 14.0043 6.37835 13.9997 6.51665C13.9951 6.65496 13.9334 6.78518 13.8293 6.8763L10.2593 10L13.8293 13.1237C13.9334 13.2149 13.9951 13.3451 13.9997 13.4834C14.0043 13.6217 13.9514 13.7557 13.8536 13.8536L9.85356 17.8536C9.71056 17.9966 9.4955 18.0393 9.30867 17.962C9.12183 17.8846 9.00001 17.7022 9.00001 17.5V11.1019L5.82926 13.8763C5.62144 14.0581 5.30556 14.0371 5.12372 13.8293C4.94188 13.6214 4.96294 13.3056 5.17075 13.1237L8.74071 10L5.17075 6.8763C4.96294 6.69446 4.94188 6.37858 5.12372 6.17076C5.30556 5.96294 5.62144 5.94189 5.82926 6.12373L9.00001 8.89813V2.50001C9.00001 2.29778 9.12183 2.11547 9.30867 2.03807ZM10 11.1019V16.2929L12.7685 13.5244L10 11.1019ZM10 8.89813L12.7685 6.47566L10 3.70712V8.89813ZM17 10C17 10.5523 16.5523 11 16 11C15.4477 11 15 10.5523 15 10C15 9.44772 15.4477 9 16 9C16.5523 9 17 9.44772 17 10ZM3 11C3.55228 11 4 10.5523 4 10C4 9.44772 3.55228 9 3 9C2.44772 9 2 9.44772 2 10C2 10.5523 2.44772 11 3 11Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-bluetooth-off-symbolic.svg b/res/icons/fluent-bluetooth-off-symbolic.svg
new file mode 100644
index 0000000..e70f964
--- /dev/null
+++ b/res/icons/fluent-bluetooth-off-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5 14.2071L17.1464 17.8536C17.3417 18.0488 17.6583 18.0488 17.8536 17.8536C18.0488 17.6583 18.0488 17.3417 17.8536 17.1464L2.85355 2.14645C2.65829 1.95118 2.34171 1.95118 2.14645 2.14645C1.95118 2.34171 1.95118 2.65829 2.14645 2.85355L9.00001 9.70711V9.77313L5.17075 13.1237C4.96294 13.3056 4.94188 13.6214 5.12372 13.8293C5.30556 14.0371 5.62144 14.0581 5.82926 13.8763L9.00001 11.1019V17.5C9.00001 17.7022 9.12183 17.8846 9.30867 17.962C9.4955 18.0393 9.71056 17.9966 9.85356 17.8536L13.5 14.2071ZM13.8293 6.8763L11.2524 9.13107L10.5437 8.4224L12.7685 6.47566L10 3.70712V7.8787L9.00001 6.8787V2.50001C9.00001 2.29778 9.12183 2.11547 9.30867 2.03807C9.4955 1.96068 9.71056 2.00346 9.85356 2.14646L13.8536 6.14646C13.9514 6.24431 14.0043 6.37835 13.9997 6.51665C13.9951 6.65496 13.9334 6.78518 13.8293 6.8763ZM10 11.1019L12.7685 13.5244L10 16.2929V11.1019Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-bluetooth-symbolic.svg b/res/icons/fluent-bluetooth-symbolic.svg
new file mode 100644
index 0000000..b802169
--- /dev/null
+++ b/res/icons/fluent-bluetooth-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.30867 2.03806C9.4955 1.96067 9.71056 2.00345 9.85356 2.14645L13.8536 6.14645C13.9514 6.2443 14.0043 6.37833 13.9997 6.51664C13.9951 6.65495 13.9334 6.78516 13.8293 6.87629L10.2593 10L13.8293 13.1237C13.9334 13.2148 13.9951 13.3451 13.9997 13.4834C14.0043 13.6217 13.9514 13.7557 13.8536 13.8536L9.85356 17.8536C9.71056 17.9966 9.4955 18.0393 9.30867 17.9619C9.12183 17.8845 9.00001 17.7022 9.00001 17.5V11.1019L5.82926 13.8763C5.62144 14.0581 5.30556 14.0371 5.12372 13.8293C4.94188 13.6214 4.96294 13.3056 5.17075 13.1237L8.74071 10L5.17075 6.87629C4.96294 6.69445 4.94188 6.37857 5.12372 6.17075C5.30556 5.96293 5.62144 5.94187 5.82926 6.12371L9.00001 8.89812V2.5C9.00001 2.29777 9.12183 2.11545 9.30867 2.03806ZM10 11.1019V16.2929L12.7685 13.5244L10 11.1019ZM10 8.89812L12.7685 6.47564L10 3.70711V8.89812Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-brightness-high-symbolic.svg b/res/icons/fluent-brightness-high-symbolic.svg
new file mode 100644
index 0000000..9f1a403
--- /dev/null
+++ b/res/icons/fluent-brightness-high-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 2C10.2761 2 10.5 2.22386 10.5 2.5V3.5C10.5 3.77614 10.2761 4 10 4C9.72386 4 9.5 3.77614 9.5 3.5V2.5C9.5 2.22386 9.72386 2 10 2ZM10 14C12.2091 14 14 12.2091 14 10C14 7.79086 12.2091 6 10 6C7.79086 6 6 7.79086 6 10C6 12.2091 7.79086 14 10 14ZM10 13C8.34315 13 7 11.6569 7 10C7 8.34315 8.34315 7 10 7C11.6569 7 13 8.34315 13 10C13 11.6569 11.6569 13 10 13ZM17.5 10.5C17.7761 10.5 18 10.2761 18 10C18 9.72386 17.7761 9.5 17.5 9.5H16.5C16.2239 9.5 16 9.72386 16 10C16 10.2761 16.2239 10.5 16.5 10.5H17.5ZM10 16C10.2761 16 10.5 16.2239 10.5 16.5V17.5C10.5 17.7761 10.2761 18 10 18C9.72386 18 9.5 17.7761 9.5 17.5V16.5C9.5 16.2239 9.72386 16 10 16ZM3.5 10.5C3.77614 10.5 4 10.2761 4 10C4 9.72386 3.77614 9.5 3.5 9.5H2.46289C2.18675 9.5 1.96289 9.72386 1.96289 10C1.96289 10.2761 2.18675 10.5 2.46289 10.5H3.5ZM4.14645 4.14645C4.34171 3.95118 4.65829 3.95118 4.85355 4.14645L5.85355 5.14645C6.04882 5.34171 6.04882 5.65829 5.85355 5.85355C5.65829 6.04882 5.34171 6.04882 5.14645 5.85355L4.14645 4.85355C3.95118 4.65829 3.95118 4.34171 4.14645 4.14645ZM4.85355 15.8536C4.65829 16.0488 4.34171 16.0488 4.14645 15.8536C3.95118 15.6583 3.95118 15.3417 4.14645 15.1464L5.14645 14.1464C5.34171 13.9512 5.65829 13.9512 5.85355 14.1464C6.04882 14.3417 6.04882 14.6583 5.85355 14.8536L4.85355 15.8536ZM15.8536 4.14645C15.6583 3.95118 15.3417 3.95118 15.1464 4.14645L14.1464 5.14645C13.9512 5.34171 13.9512 5.65829 14.1464 5.85355C14.3417 6.04882 14.6583 6.04882 14.8536 5.85355L15.8536 4.85355C16.0488 4.65829 16.0488 4.34171 15.8536 4.14645ZM15.1464 15.8536C15.3417 16.0488 15.6583 16.0488 15.8536 15.8536C16.0488 15.6583 16.0488 15.3417 15.8536 15.1464L14.8536 14.1464C14.6583 13.9512 14.3417 13.9512 14.1464 14.1464C13.9512 14.3417 13.9512 14.6583 14.1464 14.8536L15.1464 15.8536Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-brightness-low-symbolic.svg b/res/icons/fluent-brightness-low-symbolic.svg
new file mode 100644
index 0000000..28d5cf9
--- /dev/null
+++ b/res/icons/fluent-brightness-low-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.5 3.5C10.5 3.22386 10.2761 3 10 3C9.72386 3 9.5 3.22386 9.5 3.5V4.5C9.5 4.77614 9.72386 5 10 5C10.2761 5 10.5 4.77614 10.5 4.5V3.5ZM10 6C7.79086 6 6 7.79086 6 10C6 12.2091 7.79086 14 10 14C12.2091 14 14 12.2091 14 10C14 7.79086 12.2091 6 10 6ZM7 10C7 8.34315 8.34315 7 10 7C11.6569 7 13 8.34315 13 10C13 11.6569 11.6569 13 10 13C8.34315 13 7 11.6569 7 10ZM17 10C17 10.2761 16.7761 10.5 16.5 10.5H15.5C15.2239 10.5 15 10.2761 15 10C15 9.72386 15.2239 9.5 15.5 9.5H16.5C16.7761 9.5 17 9.72386 17 10ZM10.5 15.5C10.5 15.2239 10.2761 15 10 15C9.72386 15 9.5 15.2239 9.5 15.5V16.5C9.5 16.7761 9.72386 17 10 17C10.2761 17 10.5 16.7761 10.5 16.5V15.5ZM5 10C5 10.2761 4.77614 10.5 4.5 10.5H3.5C3.22386 10.5 3 10.2761 3 10C3 9.72386 3.22386 9.5 3.5 9.5H4.5C4.77614 9.5 5 9.72386 5 10ZM5.85355 5.14645C5.65829 4.95118 5.34171 4.95118 5.14645 5.14645C4.95118 5.34171 4.95118 5.65829 5.14645 5.85355L5.64645 6.35355C5.84171 6.54882 6.15829 6.54882 6.35355 6.35355C6.54882 6.15829 6.54882 5.84171 6.35355 5.64645L5.85355 5.14645ZM5.14645 14.8536C5.34171 15.0488 5.65829 15.0488 5.85355 14.8536L6.35355 14.3536C6.54882 14.1583 6.54882 13.8417 6.35355 13.6464C6.15829 13.4512 5.84171 13.4512 5.64645 13.6464L5.14645 14.1464C4.95118 14.3417 4.95118 14.6583 5.14645 14.8536ZM14.1464 5.14645C14.3417 4.95118 14.6583 4.95118 14.8536 5.14645C15.0488 5.34171 15.0488 5.65829 14.8536 5.85355L14.3536 6.35355C14.1583 6.54882 13.8417 6.54882 13.6464 6.35355C13.4512 6.15829 13.4512 5.84171 13.6464 5.64645L14.1464 5.14645ZM14.8536 14.8536C14.6583 15.0488 14.3417 15.0488 14.1464 14.8536L13.6464 14.3536C13.4512 14.1583 13.4512 13.8417 13.6464 13.6464C13.8417 13.4512 14.1583 13.4512 14.3536 13.6464L14.8536 14.1464C15.0488 14.3417 15.0488 14.6583 14.8536 14.8536Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-checkmark-circle-symbolic.svg b/res/icons/fluent-checkmark-circle-symbolic.svg
new file mode 100644
index 0000000..37e387a
--- /dev/null
+++ b/res/icons/fluent-checkmark-circle-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 2C14.4183 2 18 5.58172 18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2ZM10 3C6.13401 3 3 6.13401 3 10C3 13.866 6.13401 17 10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3ZM13.3584 7.64645C13.532 7.82001 13.5513 8.08944 13.4163 8.28431L13.3584 8.35355L9.35355 12.3584C9.17999 12.532 8.91056 12.5513 8.71569 12.4163L8.64645 12.3584L6.64645 10.3584C6.45118 10.1632 6.45118 9.84658 6.64645 9.65131C6.82001 9.47775 7.08944 9.45846 7.28431 9.59346L7.35355 9.65131L9 11.298L12.6513 7.64645C12.8466 7.45118 13.1632 7.45118 13.3584 7.64645Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-checkmark-symbolic.svg b/res/icons/fluent-checkmark-symbolic.svg
new file mode 100644
index 0000000..3947755
--- /dev/null
+++ b/res/icons/fluent-checkmark-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.37371 10.1678C3.19025 9.96143 2.87421 9.94284 2.66782 10.1263C2.46143 10.3098 2.44284 10.6258 2.6263 10.8322L6.6263 15.3322C6.81743 15.5472 7.15013 15.557 7.35356 15.3536L17.8536 4.85355C18.0488 4.65829 18.0488 4.34171 17.8536 4.14645C17.6583 3.95118 17.3417 3.95118 17.1465 4.14645L7.02141 14.2715L3.37371 10.1678Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-chevron-right-symbolic.svg b/res/icons/fluent-chevron-right-symbolic.svg
new file mode 100644
index 0000000..2267806
--- /dev/null
+++ b/res/icons/fluent-chevron-right-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.64582 4.14708C7.84073 3.95147 8.15731 3.9509 8.35292 4.14582L13.8374 9.6108C14.0531 9.82574 14.0531 10.1751 13.8374 10.39L8.35292 15.855C8.15731 16.0499 7.84073 16.0493 7.64582 15.8537C7.4509 15.6581 7.45147 15.3415 7.64708 15.1466L12.8117 10.0004L7.64708 4.85418C7.45147 4.65927 7.4509 4.34269 7.64582 4.14708Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-dismiss-symbolic.svg b/res/icons/fluent-dismiss-symbolic.svg
new file mode 100644
index 0000000..ea67891
--- /dev/null
+++ b/res/icons/fluent-dismiss-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.08859 2.21569L2.14645 2.14645C2.32001 1.97288 2.58944 1.9536 2.78431 2.08859L2.85355 2.14645L6 5.293L9.14645 2.14645C9.34171 1.95118 9.65829 1.95118 9.85355 2.14645C10.0488 2.34171 10.0488 2.65829 9.85355 2.85355L6.707 6L9.85355 9.14645C10.0271 9.32001 10.0464 9.58944 9.91141 9.78431L9.85355 9.85355C9.67999 10.0271 9.41056 10.0464 9.21569 9.91141L9.14645 9.85355L6 6.707L2.85355 9.85355C2.65829 10.0488 2.34171 10.0488 2.14645 9.85355C1.95118 9.65829 1.95118 9.34171 2.14645 9.14645L5.293 6L2.14645 2.85355C1.97288 2.67999 1.9536 2.41056 2.08859 2.21569L2.14645 2.14645L2.08859 2.21569Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-grid-dots-symbolic.svg b/res/icons/fluent-grid-dots-symbolic.svg
new file mode 100644
index 0000000..9f49947
--- /dev/null
+++ b/res/icons/fluent-grid-dots-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.25 4C5.25 4.69036 4.69036 5.25 4 5.25C3.30964 5.25 2.75 4.69036 2.75 4C2.75 3.30964 3.30964 2.75 4 2.75C4.69036 2.75 5.25 3.30964 5.25 4ZM17.25 16C17.25 16.6904 16.6904 17.25 16 17.25C15.3096 17.25 14.75 16.6904 14.75 16C14.75 15.3096 15.3096 14.75 16 14.75C16.6904 14.75 17.25 15.3096 17.25 16ZM16 11.25C16.6904 11.25 17.25 10.6904 17.25 10C17.25 9.30964 16.6904 8.75 16 8.75C15.3096 8.75 14.75 9.30964 14.75 10C14.75 10.6904 15.3096 11.25 16 11.25ZM17.25 4C17.25 4.69036 16.6904 5.25 16 5.25C15.3096 5.25 14.75 4.69036 14.75 4C14.75 3.30964 15.3096 2.75 16 2.75C16.6904 2.75 17.25 3.30964 17.25 4ZM10 17.25C10.6904 17.25 11.25 16.6904 11.25 16C11.25 15.3096 10.6904 14.75 10 14.75C9.30964 14.75 8.75 15.3096 8.75 16C8.75 16.6904 9.30964 17.25 10 17.25ZM11.25 10C11.25 10.6904 10.6904 11.25 10 11.25C9.30964 11.25 8.75 10.6904 8.75 10C8.75 9.30964 9.30964 8.75 10 8.75C10.6904 8.75 11.25 9.30964 11.25 10ZM10 5.25C10.6904 5.25 11.25 4.69036 11.25 4C11.25 3.30964 10.6904 2.75 10 2.75C9.30964 2.75 8.75 3.30964 8.75 4C8.75 4.69036 9.30964 5.25 10 5.25ZM5.25 16C5.25 16.6904 4.69036 17.25 4 17.25C3.30964 17.25 2.75 16.6904 2.75 16C2.75 15.3096 3.30964 14.75 4 14.75C4.69036 14.75 5.25 15.3096 5.25 16ZM4 11.25C4.69036 11.25 5.25 10.6904 5.25 10C5.25 9.30964 4.69036 8.75 4 8.75C3.30964 8.75 2.75 9.30964 2.75 10C2.75 10.6904 3.30964 11.25 4 11.25Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-microphone-symbolic.svg b/res/icons/fluent-microphone-symbolic.svg
new file mode 100644
index 0000000..97ea44a
--- /dev/null
+++ b/res/icons/fluent-microphone-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 13C11.6569 13 13 11.6568 13 9.99998V5C13 3.34315 11.6569 2 10 2C8.34315 2 7 3.34315 7 5V9.99998C7 11.6568 8.34315 13 10 13ZM10 12C8.89543 12 8 11.1046 8 9.99998V5C8 3.89543 8.89543 3 10 3C11.1046 3 12 3.89543 12 5V9.99998C12 11.1046 11.1046 12 10 12ZM5 9.49998C5.27614 9.49998 5.5 9.72384 5.5 9.99998C5.5 12.4853 7.51472 14.5 10 14.5C12.4853 14.5 14.5 12.4853 14.5 9.99998C14.5 9.72384 14.7239 9.49998 15 9.49998C15.2761 9.49998 15.5 9.72384 15.5 9.99998C15.5 12.869 13.3033 15.2249 10.5 15.4776V17.5C10.5 17.7761 10.2761 18 10 18C9.72386 18 9.5 17.7761 9.5 17.5V15.4776C6.69675 15.2249 4.5 12.869 4.5 9.99998C4.5 9.72384 4.72386 9.49998 5 9.49998Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-more-circle-symbolic.svg b/res/icons/fluent-more-circle-symbolic.svg
new file mode 100644
index 0000000..682b8a5
--- /dev/null
+++ b/res/icons/fluent-more-circle-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7 10C7 10.5523 6.55228 11 6 11C5.44772 11 5 10.5523 5 10C5 9.44772 5.44772 9 6 9C6.55228 9 7 9.44772 7 10ZM11 10C11 10.5523 10.5523 11 10 11C9.44772 11 9 10.5523 9 10C9 9.44772 9.44772 9 10 9C10.5523 9 11 9.44772 11 10ZM15 10C15 10.5523 14.5523 11 14 11C13.4477 11 13 10.5523 13 10C13 9.44772 13.4477 9 14 9C14.5523 9 15 9.44772 15 10ZM10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2ZM3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-number-circle-1-symbolic.svg b/res/icons/fluent-number-circle-1-symbolic.svg
new file mode 100644
index 0000000..880e506
--- /dev/null
+++ b/res/icons/fluent-number-circle-1-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.0001 6.49997C11.0001 6.24746 10.8118 6.03457 10.5611 6.00372C10.3105 5.97287 10.0762 6.13373 10.015 6.37871C9.91244 6.78889 9.64181 7.29013 9.28755 7.76248C8.93439 8.23336 8.53619 8.62496 8.2227 8.83395C7.99294 8.98713 7.93085 9.29756 8.08403 9.52732C8.2372 9.75709 8.54764 9.81918 8.7774 9.666C9.18228 9.39608 9.62181 8.96065 10.0001 8.47675V13.5C10.0001 13.7761 10.2239 14 10.5001 14C10.7762 14 11.0001 13.7761 11.0001 13.5V6.49997ZM10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2ZM3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-number-circle-2-symbolic.svg b/res/icons/fluent-number-circle-2-symbolic.svg
new file mode 100644
index 0000000..510a060
--- /dev/null
+++ b/res/icons/fluent-number-circle-2-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10ZM10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2ZM8.40025 7.3017C8.23437 7.52097 7.9223 7.56524 7.70192 7.40029C7.48084 7.23482 7.43576 6.92147 7.60123 6.70039L7.60231 6.69895C7.61863 6.67754 7.63556 6.65658 7.65285 6.63595C7.68213 6.60099 7.72357 6.55365 7.77672 6.4981C7.88261 6.38741 8.03738 6.24163 8.23763 6.09581C8.63625 5.80554 9.23694 5.5 9.99959 5.5C11.3804 5.5 12.4994 6.61935 12.4996 7.99992C12.4997 8.78654 12.1743 9.34822 11.7208 9.78284C11.3662 10.1227 10.9117 10.4015 10.499 10.6546C10.4191 10.7036 10.3407 10.7516 10.265 10.799C9.77045 11.1081 9.33701 11.4127 9.02036 11.8217C8.78704 12.1231 8.6059 12.4965 8.53386 13H12C12.2761 13 12.5 13.2239 12.5 13.5C12.5 13.7761 12.2761 14 12 14H8C7.72386 14 7.5 13.7761 7.5 13.5C7.5 12.5184 7.7843 11.7848 8.22963 11.2095C8.66297 10.6498 9.2295 10.2669 9.73494 9.95101C9.82749 9.89316 9.91707 9.838 10.0036 9.78471C10.4156 9.53105 10.7589 9.31961 11.0288 9.06091C11.3253 8.77678 11.4997 8.46346 11.4996 8.00008C11.4994 7.17152 10.8279 6.5 9.99959 6.5C9.51265 6.5 9.11431 6.69446 8.82629 6.90419C8.68323 7.00837 8.57278 7.11259 8.49928 7.1894C8.46274 7.2276 8.43594 7.25838 8.41941 7.27812C8.41116 7.28796 8.40552 7.295 8.40255 7.29875L8.40025 7.3017Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-number-circle-3-symbolic.svg b/res/icons/fluent-number-circle-3-symbolic.svg
new file mode 100644
index 0000000..7b72c32
--- /dev/null
+++ b/res/icons/fluent-number-circle-3-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10ZM10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2ZM7.42748 7.38207C7.63783 7.55964 7.95197 7.5337 8.1304 7.32451L8.13315 7.32139C8.13659 7.31752 8.14307 7.31036 8.15252 7.30035C8.17145 7.28031 8.20209 7.24909 8.24385 7.21036C8.32771 7.13259 8.4544 7.02642 8.61918 6.91987C8.94965 6.70619 9.4186 6.5 9.99892 6.5C11.0233 6.5 11.75 7.22552 11.75 8C11.75 8.77448 11.0233 9.5 9.99892 9.5H9.50001C9.22387 9.5 9.00001 9.72386 9.00001 10C9.00001 10.2761 9.22387 10.5 9.50001 10.5H9.99892C11.0233 10.5 11.75 11.2255 11.75 12C11.75 12.7745 11.0233 13.5 9.99892 13.5C9.4186 13.5 8.94965 13.2938 8.61918 13.0801C8.4544 12.9736 8.32771 12.8674 8.24385 12.7896C8.20209 12.7509 8.17145 12.7197 8.15252 12.6996C8.14307 12.6896 8.13659 12.6825 8.13315 12.6786L8.1304 12.6755C7.95197 12.4663 7.63783 12.4404 7.42748 12.6179C7.21647 12.7961 7.18982 13.1115 7.36794 13.3225L7.36914 13.3239L7.37066 13.3257L7.37466 13.3304L7.38652 13.3438C7.39613 13.3546 7.40915 13.369 7.42553 13.3863C7.45826 13.4209 7.50454 13.4678 7.56387 13.5229C7.68218 13.6326 7.85433 13.7764 8.07619 13.9199C8.51899 14.2062 9.17449 14.5 9.99892 14.5C11.4611 14.5 12.75 13.4347 12.75 12C12.75 11.1603 12.3085 10.4471 11.649 10C12.3085 9.55289 12.75 8.83971 12.75 8C12.75 6.56534 11.4611 5.5 9.99892 5.5C9.17449 5.5 8.51899 5.79381 8.07619 6.08013C7.85433 6.22358 7.68218 6.36741 7.56387 6.47714C7.50454 6.53216 7.45826 6.57906 7.42553 6.61371C7.40915 6.63105 7.39613 6.64537 7.38652 6.65615L7.37466 6.66963L7.37066 6.67428L7.36914 6.67606L7.36821 6.67716C7.19008 6.88817 7.21647 7.20394 7.42748 7.38207Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-number-circle-4-symbolic.svg b/res/icons/fluent-number-circle-4-symbolic.svg
new file mode 100644
index 0000000..6c4e8a1
--- /dev/null
+++ b/res/icons/fluent-number-circle-4-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10ZM10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2ZM11.9949 6.30866C11.9946 5.57105 11.0414 5.27697 10.6255 5.88616L7.09225 11.0617C6.82037 11.46 7.10557 12 7.58779 12H10.9977L10.9984 13.5002C10.9986 13.7764 11.2225 14.0001 11.4987 14C11.7748 13.9999 11.9986 13.7759 11.9984 13.4998L11.9977 12H13C13.2761 12 13.5 11.7761 13.5 11.5C13.5 11.2239 13.2761 11 13 11H11.9972L11.9949 6.30866ZM10.9953 7.11805L10.9972 11H8.34518L10.9953 7.11805Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-number-circle-5-symbolic.svg b/res/icons/fluent-number-circle-5-symbolic.svg
new file mode 100644
index 0000000..e279e23
--- /dev/null
+++ b/res/icons/fluent-number-circle-5-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2ZM3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10ZM8.30295 6.44596C8.33062 6.19231 8.54484 6.00018 8.8 6.00018H12.0004C12.2766 6.00018 12.5004 6.22404 12.5004 6.50018C12.5004 6.77633 12.2766 7.00018 12.0004 7.00018H9.24842L9.05865 8.73976C9.13675 8.73884 9.21942 8.73809 9.3043 8.73764C9.72417 8.73544 10.2536 8.73983 10.525 8.77797C12.0974 8.99895 13.1929 10.4527 12.9719 12.0251C12.7509 13.5975 11.2971 14.693 9.72477 14.472C8.75143 14.3352 7.9611 13.7256 7.5529 12.9102C7.42928 12.6633 7.52925 12.3629 7.77618 12.2393C8.02311 12.1156 8.32349 12.2156 8.4471 12.4625C8.71443 12.9965 9.22987 13.3926 9.86394 13.4817C10.8894 13.6259 11.8375 12.9114 11.9816 11.8859C12.1258 10.8605 11.4113 9.91235 10.3858 9.76824C10.2018 9.74237 9.75354 9.7353 9.30955 9.73763C9.0966 9.73875 8.89732 9.74184 8.75114 9.74466C8.67811 9.74607 8.6185 9.74741 8.57725 9.74839L8.5298 9.74956L8.51368 9.74998C8.3693 9.75393 8.23027 9.69526 8.13237 9.58907C8.03448 9.48287 7.98729 9.33953 8.00295 9.19595L8.30295 6.44596Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-number-circle-6-symbolic.svg b/res/icons/fluent-number-circle-6-symbolic.svg
new file mode 100644
index 0000000..c5ede0d
--- /dev/null
+++ b/res/icons/fluent-number-circle-6-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 3C6.13401 3 3 6.13401 3 10C3 13.866 6.13401 17 10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3ZM2 10C2 5.58172 5.58172 2 10 2C14.4183 2 18 5.58172 18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10ZM10 9.5C8.89543 9.5 8 10.3954 8 11.5C8 12.6046 8.89543 13.5 10 13.5C11.2082 13.5 12 12.6205 12 11.5C12 10.3795 11.2082 9.5 10 9.5ZM10 14.5C9.13755 14.5 8.24115 14.2098 7.56585 13.4595C6.89498 12.7141 6.5 11.5787 6.5 10C6.5 8.82859 6.76373 7.7192 7.33862 6.88705C7.92692 6.03549 8.82705 5.5 10 5.5C11.6228 5.5 12.4169 6.59113 12.6825 7.06307C12.8179 7.30372 12.7326 7.60859 12.492 7.74403C12.2513 7.87948 11.9465 7.79419 11.811 7.55355C11.6213 7.21653 11.0941 6.5 10 6.5C9.17295 6.5 8.57308 6.85951 8.16138 7.45545C7.75851 8.0386 7.52524 8.86896 7.50193 9.8382C8.03966 9.03148 8.95777 8.5 10 8.5C11.7918 8.5 13 9.85907 13 11.5C13 13.1409 11.7918 14.5 10 14.5Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-number-circle-7-symbolic.svg b/res/icons/fluent-number-circle-7-symbolic.svg
new file mode 100644
index 0000000..7ccb41a
--- /dev/null
+++ b/res/icons/fluent-number-circle-7-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10ZM10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2ZM7.5 6C7.22386 6 7 6.22386 7 6.5C7 6.77614 7.22386 7 7.5 7H11.5762C11.3674 7.34491 11.1122 7.79558 10.8412 8.33958C10.1715 9.68405 9.40236 11.6061 9.00716 13.9157C8.96059 14.1879 9.14348 14.4463 9.41567 14.4928C9.68786 14.5394 9.94626 14.3565 9.99284 14.0843C10.3676 11.8939 11.0985 10.066 11.7363 8.78542C12.0549 8.14578 12.3494 7.6446 12.5628 7.30524C12.6695 7.13562 12.7558 7.00659 12.8146 6.92109C12.844 6.87834 12.8665 6.84649 12.8813 6.82589L12.8975 6.8035L12.901 6.79873C13.0135 6.64708 13.0316 6.44419 12.9467 6.27544C12.8618 6.10657 12.689 6 12.5 6H7.5Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-number-circle-8-symbolic.svg b/res/icons/fluent-number-circle-8-symbolic.svg
new file mode 100644
index 0000000..5b4d3f4
--- /dev/null
+++ b/res/icons/fluent-number-circle-8-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 3C6.13401 3 3 6.13401 3 10C3 13.866 6.13401 17 10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3ZM2 10C2 5.58172 5.58172 2 10 2C14.4183 2 18 5.58172 18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10ZM10 6.5C10.8593 6.5 11.5 7.14552 11.5 7.875C11.5 8.60448 10.8593 9.25 10 9.25C9.14069 9.25 8.5 8.60448 8.5 7.875C8.5 7.14552 9.14069 6.5 10 6.5ZM11.572 9.7215C12.1337 9.29103 12.5 8.63209 12.5 7.875C12.5 6.53342 11.3498 5.5 10 5.5C8.65018 5.5 7.5 6.53342 7.5 7.875C7.5 8.63208 7.86628 9.29103 8.42803 9.7215C7.72093 10.1915 7.25 10.9719 7.25 11.875C7.25 13.3513 8.50853 14.5 10 14.5C11.4915 14.5 12.75 13.3513 12.75 11.875C12.75 10.9719 12.2791 10.1915 11.572 9.7215ZM10 10.25C10.9938 10.25 11.75 11.0041 11.75 11.875C11.75 12.7459 10.9938 13.5 10 13.5C9.00619 13.5 8.25 12.7459 8.25 11.875C8.25 11.0041 9.00619 10.25 10 10.25Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-number-circle-9-symbolic.svg b/res/icons/fluent-number-circle-9-symbolic.svg
new file mode 100644
index 0000000..225e7d9
--- /dev/null
+++ b/res/icons/fluent-number-circle-9-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3C6.13401 3 3 6.13401 3 10C3 13.866 6.13401 17 10 17ZM18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2C14.4183 2 18 5.58172 18 10ZM10 10.5C11.1046 10.5 12 9.60457 12 8.5C12 7.39543 11.1046 6.5 10 6.5C8.79181 6.5 8 7.3795 8 8.5C8 9.6205 8.79181 10.5 10 10.5ZM10 5.5C10.8625 5.5 11.7589 5.79019 12.4341 6.54052C13.105 7.28593 13.5 8.42129 13.5 10C13.5 11.1714 13.2363 12.2808 12.6614 13.1129C12.0731 13.9645 11.1729 14.5 10 14.5C8.37722 14.5 7.58314 13.4089 7.31752 12.9369C7.18208 12.6963 7.26736 12.3914 7.50801 12.256C7.74865 12.1205 8.05353 12.2058 8.18897 12.4465C8.37865 12.7835 8.90593 13.5 10 13.5C10.8271 13.5 11.4269 13.1405 11.8386 12.5446C12.2415 11.9614 12.4748 11.131 12.4981 10.1618C11.9603 10.9685 11.0422 11.5 10 11.5C8.20819 11.5 7 10.1409 7 8.5C7 6.85907 8.20819 5.5 10 5.5Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-plug-connected-symbolic.svg b/res/icons/fluent-plug-connected-symbolic.svg
new file mode 100644
index 0000000..cfdc4a0
--- /dev/null
+++ b/res/icons/fluent-plug-connected-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M17.8536 2.85355C18.0488 2.65829 18.0488 2.34171 17.8536 2.14645C17.6583 1.95118 17.3417 1.95118 17.1464 2.14645L14.4781 4.81475C12.8949 3.57957 10.6024 3.69014 9.14607 5.14646L8.84607 5.44646C8.26421 6.02832 8.26421 6.97171 8.84607 7.55357L12.4461 11.1536C13.0279 11.7354 13.9713 11.7354 14.5532 11.1536L14.8532 10.8536C16.3094 9.39737 16.42 7.10518 15.1852 5.52191L17.8536 2.85355ZM13.8461 10.4465C13.6547 10.6378 13.3445 10.6378 13.1532 10.4465L9.55318 6.84646C9.36184 6.65513 9.36184 6.34491 9.55318 6.15357L9.85318 5.85357C11.0386 4.66812 12.9606 4.66812 14.1461 5.85357C15.3315 7.03902 15.3315 8.96101 14.1461 10.1465L13.8461 10.4465ZM7.55393 8.84643C6.97207 8.26457 6.02869 8.26457 5.44682 8.84643L5.14683 9.14643C3.69063 10.6026 3.57995 12.8948 4.8148 14.4781L2.14645 17.1464C1.95118 17.3417 1.95118 17.6583 2.14645 17.8536C2.34171 18.0488 2.65829 18.0488 2.85355 17.8536L5.52186 15.1852C7.10515 16.4204 9.39761 16.3099 10.8539 14.8535L11.1539 14.5535C11.7358 13.9717 11.7358 13.0283 11.1539 12.4464L7.55393 8.84643ZM6.15393 9.55354C6.34527 9.3622 6.65549 9.3622 6.84682 9.55354L10.4468 13.1535C10.6382 13.3449 10.6382 13.6551 10.4468 13.8464L10.1468 14.1464C8.96137 15.3319 7.03938 15.3319 5.85393 14.1464C4.66848 12.961 4.66848 11.039 5.85393 9.85354L6.15393 9.55354Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-search-symbolic.svg b/res/icons/fluent-search-symbolic.svg
new file mode 100644
index 0000000..fee42f8
--- /dev/null
+++ b/res/icons/fluent-search-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.5 10C4.5 6.96243 6.96243 4.5 10 4.5C13.0376 4.5 15.5 6.96243 15.5 10C15.5 13.0376 13.0376 15.5 10 15.5C6.96243 15.5 4.5 13.0376 4.5 10ZM10 3C6.13401 3 3 6.13401 3 10C3 13.866 6.13401 17 10 17C11.6628 17 13.1902 16.4202 14.3911 15.4518L19.7197 20.7803C20.0126 21.0732 20.4874 21.0732 20.7803 20.7803C21.0732 20.4874 21.0732 20.0126 20.7803 19.7197L15.4518 14.3911C16.4202 13.1902 17 11.6628 17 10C17 6.13401 13.866 3 10 3Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-speaker-0-symbolic.svg b/res/icons/fluent-speaker-0-symbolic.svg
new file mode 100644
index 0000000..55a8ae4
--- /dev/null
+++ b/res/icons/fluent-speaker-0-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.9961 3.00588C11.9961 2.13286 10.9558 1.67903 10.3159 2.27288L6.44369 5.86626C6.35122 5.95208 6.22973 5.99976 6.10358 5.99976H3.49609C2.66767 5.99976 1.99609 6.67134 1.99609 7.49976V12.4998C1.99609 13.3282 2.66767 13.9998 3.49609 13.9998H6.10358C6.22973 13.9998 6.35122 14.0474 6.44369 14.1333L10.3159 17.7266C10.9558 18.3205 11.9961 17.8667 11.9961 16.9936V3.00588ZM7.12392 6.59927L10.9961 3.00588V16.9936L7.12392 13.4003C6.84651 13.1428 6.48204 12.9998 6.10358 12.9998H3.49609C3.21995 12.9998 2.99609 12.7759 2.99609 12.4998V7.49976C2.99609 7.22362 3.21995 6.99976 3.49609 6.99976H6.10358C6.48204 6.99976 6.84651 6.8567 7.12392 6.59927Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-speaker-1-symbolic.svg b/res/icons/fluent-speaker-1-symbolic.svg
new file mode 100644
index 0000000..5761170
--- /dev/null
+++ b/res/icons/fluent-speaker-1-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.9961 3.00588C11.9961 2.13286 10.9558 1.67903 10.3159 2.27288L6.44369 5.86626C6.35122 5.95208 6.22973 5.99976 6.10358 5.99976H3.49609C2.66767 5.99976 1.99609 6.67134 1.99609 7.49976V12.4998C1.99609 13.3282 2.66767 13.9998 3.49609 13.9998H6.10358C6.22973 13.9998 6.35122 14.0474 6.44369 14.1333L10.3159 17.7266C10.9558 18.3205 11.9961 17.8667 11.9961 16.9936V3.00588ZM7.12392 6.59927L10.9961 3.00588V16.9936L7.12392 13.4003C6.84651 13.1428 6.48204 12.9998 6.10358 12.9998H3.49609C3.21995 12.9998 2.99609 12.7759 2.99609 12.4998V7.49976C2.99609 7.22362 3.21995 6.99976 3.49609 6.99976H6.10358C6.48204 6.99976 6.84651 6.8567 7.12392 6.59927ZM14.0804 12.933C13.8412 12.7952 13.7589 12.4894 13.8967 12.2502C14.6729 10.9026 14.7284 9.19135 13.8961 7.74967C13.758 7.51052 13.8399 7.20473 14.0791 7.06665C14.3182 6.92858 14.624 7.01052 14.7621 7.24967C15.7801 9.01294 15.7103 11.1052 14.7633 12.7493C14.6254 12.9886 14.3197 13.0708 14.0804 12.933Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-speaker-2-symbolic.svg b/res/icons/fluent-speaker-2-symbolic.svg
new file mode 100644
index 0000000..af1adde
--- /dev/null
+++ b/res/icons/fluent-speaker-2-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.9961 3.00588C11.9961 2.13286 10.9558 1.67903 10.3159 2.27288L6.44369 5.86626C6.35122 5.95208 6.22973 5.99976 6.10358 5.99976H3.49609C2.66767 5.99976 1.99609 6.67134 1.99609 7.49976V12.4998C1.99609 13.3282 2.66767 13.9998 3.49609 13.9998H6.10358C6.22973 13.9998 6.35122 14.0474 6.44369 14.1333L10.3159 17.7266C10.9558 18.3205 11.9961 17.8667 11.9961 16.9936V3.00588ZM7.12392 6.59927L10.9961 3.00588V16.9936L7.12392 13.4003C6.84651 13.1428 6.48204 12.9998 6.10358 12.9998H3.49609C3.21995 12.9998 2.99609 12.7759 2.99609 12.4998V7.49976C2.99609 7.22362 3.21995 6.99976 3.49609 6.99976H6.10358C6.48204 6.99976 6.84651 6.8567 7.12392 6.59927ZM15.2557 4.62735C15.4614 4.44308 15.7775 4.46042 15.9617 4.66609C18.678 7.69768 18.678 12.3018 15.9618 15.3334C15.7775 15.5391 15.4614 15.5564 15.2557 15.3721C15.0501 15.1879 15.0327 14.8718 15.217 14.6661C17.5929 12.0143 17.5929 7.98519 15.217 5.3334C15.0327 5.12773 15.05 4.81162 15.2557 4.62735ZM14.0804 12.933C13.8412 12.7952 13.7589 12.4894 13.8967 12.2502C14.6729 10.9026 14.7284 9.19135 13.8961 7.74967C13.758 7.51052 13.8399 7.20473 14.0791 7.06665C14.3182 6.92858 14.624 7.01052 14.7621 7.24967C15.7801 9.01294 15.7103 11.1052 14.7633 12.7493C14.6254 12.9886 14.3197 13.0708 14.0804 12.933Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-speaker-mute-symbolic.svg b/res/icons/fluent-speaker-mute-symbolic.svg
new file mode 100644
index 0000000..d8ae177
--- /dev/null
+++ b/res/icons/fluent-speaker-mute-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.9961 3.00588C11.9961 2.13286 10.9558 1.67903 10.3159 2.27288L6.44369 5.86626C6.35122 5.95208 6.22973 5.99976 6.10358 5.99976H3.49609C2.66767 5.99976 1.99609 6.67134 1.99609 7.49976V12.4998C1.99609 13.3282 2.66767 13.9998 3.49609 13.9998H6.10358C6.22973 13.9998 6.35122 14.0474 6.44369 14.1333L10.3159 17.7266C10.9558 18.3205 11.9961 17.8667 11.9961 16.9936V3.00588ZM7.12392 6.59927L10.9961 3.00588V16.9936L7.12392 13.4003C6.84651 13.1428 6.48204 12.9998 6.10358 12.9998H3.49609C3.21995 12.9998 2.99609 12.7759 2.99609 12.4998V7.49976C2.99609 7.22362 3.21995 6.99976 3.49609 6.99976H6.10358C6.48204 6.99976 6.84651 6.8567 7.12392 6.59927ZM13.1425 7.64622C13.3378 7.45096 13.6544 7.45096 13.8496 7.64622L15.4961 9.29267L17.1425 7.64622C17.3378 7.45096 17.6544 7.45096 17.8496 7.64622C18.0449 7.84148 18.0449 8.15806 17.8496 8.35333L16.2032 9.99977L17.8496 11.6462C18.0449 11.8415 18.0449 12.1581 17.8496 12.3533C17.6544 12.5486 17.3378 12.5486 17.1425 12.3533L15.4961 10.7069L13.8496 12.3533C13.6544 12.5486 13.3378 12.5486 13.1425 12.3533C12.9473 12.1581 12.9473 11.8415 13.1425 11.6462L14.789 9.99977L13.1425 8.35333C12.9473 8.15806 12.9473 7.84148 13.1425 7.64622Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-speaker-off-symbolic.svg b/res/icons/fluent-speaker-off-symbolic.svg
new file mode 100644
index 0000000..04d0435
--- /dev/null
+++ b/res/icons/fluent-speaker-off-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.85258 2.14645C2.65731 1.95118 2.34073 1.95118 2.14547 2.14645C1.95021 2.34171 1.95021 2.65829 2.14547 2.85355L5.29191 5.99999H3.49609C2.66767 5.99999 1.99609 6.67157 1.99609 7.49999V12.5C1.99609 13.3284 2.66767 14 3.49609 14H6.10358C6.22973 14 6.35122 14.0477 6.44369 14.1335L10.3159 17.7269C10.9558 18.3207 11.9961 17.8669 11.9961 16.9939V12.7042L17.1455 17.8536C17.3407 18.0488 17.6573 18.0488 17.8526 17.8536C18.0478 17.6583 18.0478 17.3417 17.8526 17.1464L15.2899 14.5837C15.2899 14.5837 15.2899 14.5837 15.2899 14.5837L2.85258 2.14645ZM10.9961 11.7042V16.9939L7.12392 13.4005C6.84651 13.1431 6.48204 13 6.10358 13H3.49609C3.21995 13 2.99609 12.7761 2.99609 12.5V7.49999C2.99609 7.22385 3.21995 6.99999 3.49609 6.99999H6.10358C6.16328 6.99999 6.22263 6.99643 6.28134 6.98943L10.9961 11.7042ZM10.9961 3.00611V8.87572L11.9961 9.87572V3.00611C11.9961 2.13309 10.9558 1.67926 10.3159 2.27311L7.2441 5.12372L7.95169 5.83132L10.9961 3.00611ZM14.06 11.9396L14.8018 12.6814C15.712 11.0479 15.7661 8.98893 14.7621 7.2499C14.624 7.01075 14.3182 6.92881 14.0791 7.06688C13.8399 7.20496 13.758 7.51075 13.8961 7.7499C14.6656 9.08286 14.6762 10.6463 14.06 11.9396ZM15.8955 13.7751L16.6172 14.4968C18.6568 11.502 18.4384 7.43048 15.9617 4.66632C15.7775 4.46065 15.4614 4.44331 15.2557 4.62758C15.05 4.81185 15.0327 5.12796 15.217 5.33363C17.3398 7.70288 17.5659 11.1716 15.8955 13.7751Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-virtual-network-symbolic.svg b/res/icons/fluent-virtual-network-symbolic.svg
new file mode 100644
index 0000000..8e29745
--- /dev/null
+++ b/res/icons/fluent-virtual-network-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.8254 6.12038C5.03506 6.30009 5.05934 6.61574 4.87963 6.8254L2.15854 10L4.87963 13.1746C5.05934 13.3843 5.03506 13.6999 4.8254 13.8796C4.61573 14.0593 4.30008 14.0351 4.12037 13.8254L1.12037 10.3254C0.959876 10.1382 0.959876 9.86186 1.12037 9.67461L4.12037 6.17461C4.30008 5.96495 4.61573 5.94067 4.8254 6.12038ZM15.1204 13.1746C14.9407 13.3843 14.965 13.6999 15.1746 13.8796C15.3843 14.0593 15.6999 14.0351 15.8796 13.8254L18.8796 10.3254C19.0401 10.1382 19.0401 9.86186 18.8796 9.67461L15.8796 6.17461C15.6999 5.96495 15.3843 5.94067 15.1746 6.12038C14.965 6.30009 14.9407 6.61574 15.1204 6.8254L17.8415 10L15.1204 13.1746ZM6 11C6.55228 11 7 10.5523 7 10C7 9.44771 6.55228 9 6 9C5.44772 9 5 9.44771 5 10C5 10.5523 5.44772 11 6 11ZM11 10C11 10.5523 10.5523 11 10 11C9.44771 11 9 10.5523 9 10C9 9.44771 9.44771 9 10 9C10.5523 9 11 9.44771 11 10ZM14 11C14.5523 11 15 10.5523 15 10C15 9.44771 14.5523 9 14 9C13.4477 9 13 9.44771 13 10C13 10.5523 13.4477 11 14 11Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-wifi-1-symbolic.svg b/res/icons/fluent-wifi-1-symbolic.svg
new file mode 100644
index 0000000..f5301a1
--- /dev/null
+++ b/res/icons/fluent-wifi-1-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16.832 7.38355C17.2414 7.79294 17.6283 8.26104 17.9651 8.74899C18.1219 8.97626 18.0648 9.28765 17.8376 9.4445C17.6103 9.60136 17.2989 9.54428 17.142 9.31701C16.8389 8.87776 16.4906 8.45628 16.1249 8.09066C12.7619 4.72759 7.30926 4.72759 3.9462 8.09066C3.59796 8.4389 3.25337 8.86025 2.93919 9.31314C2.78178 9.54003 2.47025 9.59636 2.24336 9.43896C2.01647 9.28155 1.96014 8.97002 2.11754 8.74313C2.46401 8.24371 2.84593 7.77671 3.23909 7.38355C6.99268 3.62996 13.0785 3.62996 16.832 7.38355ZM14.5964 9.35699C15.0687 9.82932 15.4782 10.403 15.7902 11.013C15.9159 11.2589 15.8186 11.5601 15.5727 11.6859C15.3269 11.8116 15.0256 11.7143 14.8999 11.4684C14.6341 10.9488 14.2854 10.4603 13.8893 10.0641C11.7609 7.93576 8.31021 7.93576 6.18187 10.0641C5.7685 10.4775 5.43254 10.9425 5.17393 11.4538C5.04929 11.7002 4.74849 11.7989 4.50208 11.6742C4.25567 11.5496 4.15695 11.2488 4.28159 11.0024C4.58783 10.397 4.9862 9.84555 5.47476 9.35699C7.99363 6.83813 12.0775 6.83813 14.5964 9.35699ZM12.8874 11.8541C13.2417 12.2084 13.5336 12.6442 13.7368 13.1079C13.8476 13.3608 13.7324 13.6557 13.4795 13.7665C13.2265 13.8773 12.9317 13.7621 12.8208 13.5092C12.6665 13.157 12.4443 12.8252 12.1803 12.5613C10.9958 11.3768 9.07534 11.3768 7.89084 12.5613C7.62814 12.824 7.41534 13.1432 7.25978 13.4987C7.14909 13.7517 6.85427 13.867 6.60128 13.7563C6.3483 13.6456 6.23295 13.3508 6.34365 13.0978C6.54778 12.6313 6.83011 12.2078 7.18374 11.8541C8.75876 10.2791 11.3124 10.2791 12.8874 11.8541ZM10.9627 13.7868C11.4698 14.2938 11.4698 15.116 10.9627 15.623C10.4557 16.1301 9.63355 16.1301 9.12648 15.623C8.61942 15.116 8.61942 14.2938 9.12648 13.7868C9.63355 13.2797 10.4557 13.2797 10.9627 13.7868Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-wifi-2-symbolic.svg b/res/icons/fluent-wifi-2-symbolic.svg
new file mode 100644
index 0000000..57c5d66
--- /dev/null
+++ b/res/icons/fluent-wifi-2-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.5967 9.35692C15.0691 9.82925 15.4785 10.403 15.7905 11.013C15.9163 11.2588 15.8189 11.5601 15.5731 11.6858C15.3272 11.8116 15.026 11.7142 14.9002 11.4683C14.6345 10.9487 14.2858 10.4602 13.8896 10.064C11.7613 7.93569 8.31058 7.93569 6.18225 10.064C5.76887 10.4774 5.43292 10.9424 5.17431 11.4537C5.04967 11.7001 4.74887 11.7988 4.50246 11.6742C4.25604 11.5495 4.15732 11.2487 4.28196 11.0023C4.5882 10.3969 4.98658 9.84548 5.47514 9.35692C7.994 6.83806 12.0779 6.83806 14.5967 9.35692ZM12.8878 11.8541C13.242 12.2083 13.534 12.6442 13.7371 13.1078C13.848 13.3607 13.7328 13.6556 13.4798 13.7664C13.2269 13.8773 12.932 13.7621 12.8212 13.5091C12.6669 13.1569 12.4446 12.8252 12.1807 12.5612C10.9962 11.3767 9.07572 11.3767 7.89122 12.5612C7.62852 12.8239 7.41571 13.1431 7.26016 13.4986C7.14946 13.7516 6.85464 13.867 6.60166 13.7563C6.34867 13.6456 6.23333 13.3507 6.34402 13.0978C6.54816 12.6312 6.83048 12.2077 7.18411 11.8541C8.75914 10.2791 11.3127 10.2791 12.8878 11.8541ZM10.9631 13.7867C11.4702 14.2938 11.4702 15.1159 10.9631 15.623C10.456 16.13 9.63392 16.13 9.12686 15.623C8.61979 15.1159 8.61979 14.2938 9.12686 13.7867C9.63392 13.2796 10.456 13.2796 10.9631 13.7867Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-wifi-3-symbolic.svg b/res/icons/fluent-wifi-3-symbolic.svg
new file mode 100644
index 0000000..4fa133c
--- /dev/null
+++ b/res/icons/fluent-wifi-3-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.8876 11.8541C13.2418 12.2084 13.5338 12.6442 13.7369 13.1078C13.8478 13.3607 13.7326 13.6556 13.4796 13.7665C13.2267 13.8773 12.9318 13.7621 12.821 13.5092C12.6666 13.1569 12.4444 12.8252 12.1805 12.5612C10.996 11.3767 9.07551 11.3767 7.89101 12.5612C7.62831 12.8239 7.41551 13.1432 7.25995 13.4987C7.14926 13.7516 6.85444 13.867 6.60145 13.7563C6.34847 13.6456 6.23312 13.3508 6.34382 13.0978C6.54795 12.6313 6.83028 12.2077 7.18391 11.8541C8.75893 10.2791 11.3125 10.2791 12.8876 11.8541ZM10.9629 13.7867C11.47 14.2938 11.47 15.1159 10.9629 15.623C10.4558 16.1301 9.63372 16.1301 9.12665 15.623C8.61958 15.1159 8.61958 14.2938 9.12665 13.7867C9.63372 13.2797 10.4558 13.2797 10.9629 13.7867Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-wifi-4-symbolic.svg b/res/icons/fluent-wifi-4-symbolic.svg
new file mode 100644
index 0000000..c87d102
--- /dev/null
+++ b/res/icons/fluent-wifi-4-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.9631 13.7865C11.4702 14.2936 11.4702 15.1157 10.9631 15.6228C10.4561 16.1299 9.63395 16.1299 9.12688 15.6228C8.61982 15.1157 8.61982 14.2936 9.12688 13.7865C9.63395 13.2795 10.4561 13.2795 10.9631 13.7865Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-wifi-off-symbolic.svg b/res/icons/fluent-wifi-off-symbolic.svg
new file mode 100644
index 0000000..ac8fe81
--- /dev/null
+++ b/res/icons/fluent-wifi-off-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.85355 2.14645C2.65829 1.95118 2.34171 1.95118 2.14645 2.14645C1.95118 2.34171 1.95118 2.65829 2.14645 2.85355L5.1773 5.88441C4.48554 6.29026 3.83364 6.78996 3.24007 7.38354C2.84691 7.7767 2.46499 8.2437 2.11852 8.74312C1.96112 8.97001 2.01745 9.28154 2.24434 9.43894C2.47123 9.59634 2.78276 9.54001 2.94016 9.31312C3.25435 8.86023 3.59894 8.43888 3.94718 8.09064C4.54384 7.49398 5.20627 7.00318 5.91113 6.61824L7.34652 8.05363C6.66879 8.36442 6.03385 8.79887 5.47574 9.35698C4.98718 9.84554 4.5888 10.3969 4.28256 11.0024C4.15792 11.2488 4.25664 11.5496 4.50306 11.6742C4.74947 11.7989 5.05027 11.7002 5.17491 11.4537C5.43352 10.9425 5.76947 10.4775 6.18285 10.0641C6.74863 9.49831 7.40785 9.08293 8.11085 8.81795L9.96637 10.6735C8.95773 10.6909 7.95438 11.0845 7.18471 11.8541C6.83108 12.2078 6.54876 12.6313 6.34462 13.0978C6.23393 13.3508 6.34928 13.6456 6.60226 13.7563C6.85524 13.867 7.15006 13.7517 7.26076 13.4987C7.41631 13.1432 7.62912 12.8239 7.89182 12.5612C8.78475 11.6683 10.0959 11.4485 11.1947 11.9019L12.8407 13.5478C12.888 13.6348 12.9582 13.7028 13.0404 13.7475L17.1464 17.8536C17.3417 18.0488 17.6583 18.0488 17.8536 17.8536C18.0488 17.6583 18.0488 17.3417 17.8536 17.1464L2.85355 2.14645ZM9.60341 7.4823L10.6201 8.49897C11.8145 8.62685 12.9747 9.14855 13.8902 10.0641C14.2864 10.4603 14.6351 10.9488 14.9008 11.4684C15.0266 11.7143 15.3278 11.8116 15.5737 11.6859C15.8195 11.5601 15.9169 11.2589 15.7911 11.013C15.4791 10.403 15.0697 9.82931 14.5973 9.35698C13.2277 7.98732 11.3953 7.36243 9.60341 7.4823ZM7.1356 5.01449L7.94577 5.82466C10.7822 5.11775 13.9083 5.87307 16.1259 8.09064C16.4915 8.45627 16.8399 8.87775 17.143 9.317C17.2999 9.54426 17.6113 9.60135 17.8385 9.44449C18.0658 9.28764 18.1229 8.97624 17.966 8.74897C17.6293 8.26103 17.2424 7.79293 16.833 7.38354C14.2091 4.75961 10.4455 3.96992 7.1356 5.01449ZM10.9637 13.7868C11.4708 14.2938 11.4708 15.1159 10.9637 15.623C10.4566 16.1301 9.63452 16.1301 9.12746 15.623C8.62039 15.1159 8.62039 14.2938 9.12746 13.7868C9.63452 13.2797 10.4566 13.2797 10.9637 13.7868Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/fluent-wifi-warning-symbolic.svg b/res/icons/fluent-wifi-warning-symbolic.svg
new file mode 100644
index 0000000..2d5e5cd
--- /dev/null
+++ b/res/icons/fluent-wifi-warning-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16.833 7.38355C17.2424 7.79294 17.6293 8.26104 17.966 8.74899C18.1229 8.97626 18.0658 9.28765 17.8385 9.4445C17.6113 9.60136 17.2999 9.54428 17.143 9.31701C16.8399 8.87776 16.4915 8.45628 16.1259 8.09066C12.7628 4.72759 7.31024 4.72759 3.94718 8.09066C3.59894 8.4389 3.25435 8.86025 2.94016 9.31314C2.78276 9.54003 2.47123 9.59636 2.24434 9.43896C2.01745 9.28155 1.96112 8.97002 2.11852 8.74313C2.46499 8.24371 2.84691 7.77671 3.24007 7.38355C6.99366 3.62996 13.0794 3.62996 16.833 7.38355ZM5.47574 9.35699C7.876 6.95673 11.6974 6.84371 14.2315 9.01795C13.858 9.06771 13.4967 9.22099 13.1984 9.47778C11.0698 7.95803 8.09348 8.15347 6.18285 10.0641C5.76947 10.4775 5.43352 10.9425 5.17491 11.4538C5.05027 11.7002 4.74947 11.7989 4.50306 11.6742C4.25664 11.5496 4.15792 11.2488 4.28256 11.0024C4.5888 10.397 4.98718 9.84555 5.47574 9.35699ZM9.78086 15.9762L10.9029 13.7297C10.3929 13.2805 9.61471 13.2995 9.12746 13.7868C8.62039 14.2938 8.62039 15.116 9.12746 15.623C9.31328 15.8089 9.54143 15.9266 9.78086 15.9762ZM11.6867 12.1603L12.1357 11.2612C10.583 10.3132 8.52797 10.5109 7.18471 11.8541C6.83108 12.2078 6.54876 12.6313 6.34462 13.0978C6.23393 13.3508 6.34928 13.6456 6.60226 13.7563C6.85524 13.867 7.15006 13.7517 7.26076 13.4987C7.41631 13.1432 7.62912 12.824 7.89182 12.5613C8.92275 11.5303 10.5112 11.3967 11.6867 12.1603ZM13.6029 10.5536L10.1068 17.552C9.77438 18.2174 10.2583 19 11.0022 19H17.9978C18.7419 19 19.2258 18.2171 18.893 17.5517L15.3935 10.5532C15.0246 9.81544 13.9716 9.81564 13.6029 10.5536ZM14.9983 12.4951V15.4974C14.9983 15.7738 14.7743 15.9978 14.4979 15.9978C14.2215 15.9978 13.9975 15.7738 13.9975 15.4974V12.4951C13.9975 12.2187 14.2215 11.9947 14.4979 11.9947C14.7743 11.9947 14.9983 12.2187 14.9983 12.4951ZM14.4979 17.9993C14.2215 17.9993 13.9975 17.7753 13.9975 17.499C13.9975 17.2226 14.2215 16.9986 14.4979 16.9986C14.7743 16.9986 14.9983 17.2226 14.9983 17.499C14.9983 17.7753 14.7743 17.9993 14.4979 17.9993Z" fill="#212121"/>
+</svg>
diff --git a/res/icons/icons.gresource.xml b/res/icons/icons.gresource.xml
new file mode 100644
index 0000000..29cab10
--- /dev/null
+++ b/res/icons/icons.gresource.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/foobar/icons/scalable/status">
+ <file compressed="true">fluent-grid-dots-symbolic.svg</file>
+ <file compressed="true">fluent-battery-0-symbolic.svg</file>
+ <file compressed="true">fluent-battery-1-symbolic.svg</file>
+ <file compressed="true">fluent-battery-2-symbolic.svg</file>
+ <file compressed="true">fluent-battery-3-symbolic.svg</file>
+ <file compressed="true">fluent-battery-4-symbolic.svg</file>
+ <file compressed="true">fluent-battery-5-symbolic.svg</file>
+ <file compressed="true">fluent-battery-6-symbolic.svg</file>
+ <file compressed="true">fluent-battery-7-symbolic.svg</file>
+ <file compressed="true">fluent-battery-8-symbolic.svg</file>
+ <file compressed="true">fluent-battery-9-symbolic.svg</file>
+ <file compressed="true">fluent-battery-10-symbolic.svg</file>
+ <file compressed="true">fluent-battery-charge-symbolic.svg</file>
+ <file compressed="true">fluent-plug-connected-symbolic.svg</file>
+ <file compressed="true">fluent-virtual-network-symbolic.svg</file>
+ <file compressed="true">fluent-wifi-1-symbolic.svg</file>
+ <file compressed="true">fluent-wifi-2-symbolic.svg</file>
+ <file compressed="true">fluent-wifi-3-symbolic.svg</file>
+ <file compressed="true">fluent-wifi-4-symbolic.svg</file>
+ <file compressed="true">fluent-wifi-off-symbolic.svg</file>
+ <file compressed="true">fluent-wifi-warning-symbolic.svg</file>
+ <file compressed="true">fluent-number-circle-1-symbolic.svg</file>
+ <file compressed="true">fluent-number-circle-2-symbolic.svg</file>
+ <file compressed="true">fluent-number-circle-3-symbolic.svg</file>
+ <file compressed="true">fluent-number-circle-4-symbolic.svg</file>
+ <file compressed="true">fluent-number-circle-5-symbolic.svg</file>
+ <file compressed="true">fluent-number-circle-6-symbolic.svg</file>
+ <file compressed="true">fluent-number-circle-7-symbolic.svg</file>
+ <file compressed="true">fluent-number-circle-8-symbolic.svg</file>
+ <file compressed="true">fluent-number-circle-9-symbolic.svg</file>
+ <file compressed="true">fluent-more-circle-symbolic.svg</file>
+ <file compressed="true">fluent-checkmark-circle-symbolic.svg</file>
+ <file compressed="true">fluent-brightness-low-symbolic.svg</file>
+ <file compressed="true">fluent-brightness-high-symbolic.svg</file>
+ <file compressed="true">fluent-speaker-0-symbolic.svg</file>
+ <file compressed="true">fluent-speaker-1-symbolic.svg</file>
+ <file compressed="true">fluent-speaker-2-symbolic.svg</file>
+ <file compressed="true">fluent-speaker-mute-symbolic.svg</file>
+ <file compressed="true">fluent-speaker-off-symbolic.svg</file>
+ <file compressed="true">fluent-dismiss-symbolic.svg</file>
+ <file compressed="true">fluent-search-symbolic.svg</file>
+ <file compressed="true">fluent-bluetooth-symbolic.svg</file>
+ <file compressed="true">fluent-bluetooth-off-symbolic.svg</file>
+ <file compressed="true">fluent-bluetooth-connected-symbolic.svg</file>
+ <file compressed="true">fluent-chevron-right-symbolic.svg</file>
+ <file compressed="true">fluent-checkmark-symbolic.svg</file>
+ <file compressed="true">fluent-microphone-symbolic.svg</file>
+ </gresource>
+</gresources>
diff --git a/res/icons/meson.build b/res/icons/meson.build
new file mode 100644
index 0000000..554b9f5
--- /dev/null
+++ b/res/icons/meson.build
@@ -0,0 +1,9 @@
+icons_resource = gnome.compile_resources(
+ 'icons-resource',
+ 'icons.gresource.xml',
+ source_dir: [
+ meson.current_build_dir(),
+ meson.current_source_dir(),
+ ],
+ dependencies: style_targets,
+)
diff --git a/res/meson.build b/res/meson.build
new file mode 100644
index 0000000..28f6137
--- /dev/null
+++ b/res/meson.build
@@ -0,0 +1,2 @@
+subdir('styles')
+subdir('icons') \ No newline at end of file
diff --git a/res/styles/_common.scss b/res/styles/_common.scss
new file mode 100644
index 0000000..4386b30
--- /dev/null
+++ b/res/styles/_common.scss
@@ -0,0 +1,5 @@
+@import './partials/base';
+@import './partials/notification';
+@import './partials/panel';
+@import './partials/launcher';
+@import './partials/control-center'; \ No newline at end of file
diff --git a/res/styles/default.scss b/res/styles/default.scss
new file mode 100644
index 0000000..9f7b9a4
--- /dev/null
+++ b/res/styles/default.scss
@@ -0,0 +1 @@
+@import './monokai-pro'; \ No newline at end of file
diff --git a/res/styles/meson.build b/res/styles/meson.build
new file mode 100644
index 0000000..fbc904c
--- /dev/null
+++ b/res/styles/meson.build
@@ -0,0 +1,32 @@
+sassc = find_program('sassc')
+
+styles = ['default', 'monokai-pro', 'teatime']
+style_targets = []
+
+common_inputs = files(
+ 'partials/_base.scss',
+ 'partials/_notification.scss',
+ 'partials/_panel.scss',
+ 'partials/_launcher.scss',
+ 'partials/_control-center.scss',
+ '_common.scss',
+)
+
+foreach style : styles
+ style_targets += custom_target(
+ style + '.scss',
+ input: [style + '.scss'] + common_inputs,
+ output: style + '.css',
+ command: [sassc, '-a', '-M', '-t', 'compact', '@INPUT0@', '@OUTPUT@'],
+ )
+endforeach
+
+styles_resource = gnome.compile_resources(
+ 'styles-resource',
+ 'styles.gresource.xml',
+ source_dir: [
+ meson.current_build_dir(),
+ meson.current_source_dir(),
+ ],
+ dependencies: style_targets,
+)
diff --git a/res/styles/monokai-pro.scss b/res/styles/monokai-pro.scss
new file mode 100644
index 0000000..13601fe
--- /dev/null
+++ b/res/styles/monokai-pro.scss
@@ -0,0 +1,45 @@
+//
+// Color scheme based on the awesome Monokai Pro (https://monokai.pro/).
+//
+// The accent color is a custom one though ¯\_(ツ)_/¯
+//
+
+$foobar-color-background-primary: #19181A;
+$foobar-color-background-secondary: #2d2a2e;
+$foobar-color-background-tertiary: #3e3b40;
+$foobar-color-background-quaternary: #474449;
+$foobar-color-foreground-primary: #fcfcfa;
+$foobar-color-foreground-secondary: transparentize($foobar-color-foreground-primary, 0.5);
+$foobar-color-foreground-tertiary: transparentize($foobar-color-foreground-primary, 0.8);
+$foobar-color-foreground-quaternary: transparentize($foobar-color-foreground-primary, 0.9);
+$foobar-color-accent-primary: #c3b4b0;
+$foobar-color-accent-secondary: transparentize($foobar-color-accent-primary, 0.5);
+$foobar-color-accent-tertiary: transparentize($foobar-color-accent-primary, 0.8);
+$foobar-color-accent-quaternary: transparentize($foobar-color-accent-primary, 0.9);
+$foobar-color-special-primary: #fc9867;
+$foobar-color-special-secondary: transparentize($foobar-color-special-primary, 0.5);
+$foobar-color-special-tertiary: transparentize($foobar-color-special-primary, 0.8);
+$foobar-color-special-quaternary: transparentize($foobar-color-special-primary, 0.9);
+$foobar-color-urgent-primary: #ff6188;
+$foobar-color-urgent-secondary: transparentize($foobar-color-urgent-primary, 0.5);
+$foobar-color-urgent-tertiary: transparentize($foobar-color-urgent-primary, 0.8);
+$foobar-color-urgent-quaternary: transparentize($foobar-color-urgent-primary, 0.9);
+$foobar-color-border: #101011;
+$foobar-dim-border-strong: 2px;
+$foobar-dim-border-light: 1px;
+$foobar-dim-radius-large: 12px;
+$foobar-dim-radius-small: 6px;
+$foobar-dim-spacing-relaxed: 18px;
+$foobar-dim-spacing-large: 12px;
+$foobar-dim-spacing-medium: 6px;
+$foobar-dim-spacing-small: 3px;
+$foobar-dim-margin-large-horizontal: 24px;
+$foobar-dim-margin-large-vertical: 18px;
+$foobar-dim-margin-medium-horizontal: 18px;
+$foobar-dim-margin-medium-vertical: 12px;
+$foobar-dim-margin-small-horizontal: 6px;
+$foobar-dim-margin-small-vertical: 6px;
+$foobar-dim-font-large: 16px;
+$foobar-dim-font-small: 13px;
+
+@import './common'; \ No newline at end of file
diff --git a/res/styles/partials/_base.scss b/res/styles/partials/_base.scss
new file mode 100644
index 0000000..4e4b88b
--- /dev/null
+++ b/res/styles/partials/_base.scss
@@ -0,0 +1,85 @@
+//
+// Set some more minimal defaults than the ones provided by Adwaita.
+//
+
+label, image {
+ color: $foobar-color-foreground-primary;
+ -gtk-icon-filter: none;
+ font-weight: normal;
+}
+
+listview {
+ background: transparent;
+
+ & row {
+ padding: 0;
+ }
+
+ & row:hover {
+ background: $foobar-color-accent-quaternary;
+ }
+
+ & row:active,
+ & row:active:hover,
+ & row:selected,
+ & row:selected:hover {
+ background: $foobar-color-accent-tertiary;
+ }
+
+ & row:hover,
+ & row:active,
+ & row:focus,
+ & row:selected {
+ outline: none;
+ }
+}
+
+separator {
+ background: $foobar-color-border;
+ min-width: $foobar-dim-border-light;
+ min-width: $foobar-dim-border-light;
+}
+
+button {
+ min-width: 0;
+ min-height: 0;
+ padding: 0;
+ border-radius: $foobar-dim-radius-small;
+ background: transparent;
+ transition: background-color 0.1s;
+
+ &:hover {
+ background: $foobar-color-accent-quaternary;
+ }
+
+ &:active {
+ background: $foobar-color-accent-tertiary;
+ }
+}
+
+scale {
+ &.horizontal {
+ padding: 8px 2px;
+ }
+
+ &.vertical {
+ padding: 2px 8px;
+ }
+
+ & trough {
+ // XXX: Using a transparent color here causes glitches when draggin the slider to the left.
+ background: mix($foobar-color-accent-primary, $foobar-color-background-secondary, 10%);
+ border-color: mix($foobar-color-accent-primary, $foobar-color-background-secondary, 10%);
+ }
+
+ & highlight {
+ background: $foobar-color-accent-primary;
+ border-color: $foobar-color-accent-primary;
+ }
+
+ & slider {
+ background: $foobar-color-accent-primary;
+ border-color: $foobar-color-accent-primary;
+ box-shadow: none;
+ }
+} \ No newline at end of file
diff --git a/res/styles/partials/_control-center.scss b/res/styles/partials/_control-center.scss
new file mode 100644
index 0000000..991f8e1
--- /dev/null
+++ b/res/styles/partials/_control-center.scss
@@ -0,0 +1,142 @@
+//
+// Styles for the control center, including its "controls" and "notifications" sections.
+//
+
+window.control-center {
+ // XXX: To prevent some glitches
+ background: rgba(0, 0, 0, 0.005);
+ border-radius: $foobar-dim-radius-large;
+
+ & > box {
+ background: $foobar-color-background-primary;
+ // XXX: Applying the border directly to the window seems to confuse hyprland, GTK, or something else.
+ // To reproduce: set position to "right", alignment to "start", and foobar-dim-border-light to some large value.
+ border: $foobar-dim-border-light solid $foobar-color-border;
+ border-radius: $foobar-dim-radius-large;
+ }
+
+ & control-button,
+ & control-slider {
+ background: $foobar-color-background-secondary;
+ border: $foobar-dim-border-light solid $foobar-color-border;
+ border-radius: $foobar-dim-radius-large;
+ transition: background-color 0.1s, border 0.1s;
+
+ & label,
+ & image {
+ transition: color 0.1s;
+ }
+
+ & image {
+ -gtk-icon-size: 20px;
+ }
+
+ & button {
+ transition: background-color 0.1s, color 0.1s;
+ border: none;
+ box-shadow: none;
+ border-radius: 0;
+
+ // "Linked" style
+
+ &:last-child {
+ border-top-right-radius: $foobar-dim-radius-large;
+ border-bottom-right-radius: $foobar-dim-radius-large;
+ }
+
+ &:first-child {
+ border-top-left-radius: $foobar-dim-radius-large;
+ border-bottom-left-radius: $foobar-dim-radius-large;
+ }
+ }
+
+ & scale {
+ margin-top: $foobar-dim-spacing-medium;
+ }
+
+ & > .primary {
+ padding: $foobar-dim-margin-medium-vertical $foobar-dim-margin-medium-horizontal;
+
+ & label {
+ font-weight: bold;
+ }
+
+ & image {
+ margin-right: $foobar-dim-spacing-medium;
+ }
+ }
+
+ & > .expand {
+ border-left: $foobar-dim-border-light solid $foobar-color-border;
+ padding: $foobar-dim-margin-small-vertical $foobar-dim-margin-small-horizontal;
+
+ & image {
+ transition: transform 0.1s;
+ transform: rotate(0deg);
+ }
+ }
+
+ &.toggled {
+ background: $foobar-color-accent-primary;
+
+ & label,
+ & image {
+ color: $foobar-color-background-primary;
+ }
+ }
+
+ &.expanded {
+ & > .expand image {
+ transform: rotate(90deg);
+ }
+ }
+ }
+
+ & control-details {
+ & scrolledwindow {
+ background: $foobar-color-background-secondary;
+ border-top: $foobar-dim-border-light solid $foobar-color-border;
+ border-bottom: $foobar-dim-border-light solid $foobar-color-border;
+ }
+
+ & row {
+ $inset: $foobar-dim-spacing-medium;
+
+ padding-top: $foobar-dim-margin-small-vertical;
+ padding-bottom: $foobar-dim-margin-small-vertical;
+ padding-left: $foobar-dim-margin-medium-horizontal - $inset; // Aligned to primary button content
+ padding-right: $foobar-dim-margin-small-horizontal - $inset; // Aligned to expand button content
+ margin-top: $inset;
+ margin-left: $inset;
+ margin-right: $inset;
+ border-radius: $foobar-dim-radius-small;
+
+ &:last-child {
+ margin-bottom: $inset;
+ }
+
+ & .accessory {
+ margin-left: $foobar-dim-spacing-medium;
+ }
+ }
+ }
+
+ & .notifications {
+ & row,
+ & row:hover,
+ & row:active,
+ & row:focus ,
+ & row:selected {
+ background: transparent;
+ }
+
+ & notification .content {
+ background: $foobar-color-background-secondary;
+ }
+ }
+
+ & .placeholder {
+ font-size: $foobar-dim-font-large;
+ color: $foobar-color-foreground-secondary;
+ }
+} \ No newline at end of file
diff --git a/res/styles/partials/_launcher.scss b/res/styles/partials/_launcher.scss
new file mode 100644
index 0000000..baf002e
--- /dev/null
+++ b/res/styles/partials/_launcher.scss
@@ -0,0 +1,67 @@
+//
+// Styles for the launcher.
+//
+// The search icon and label should be aligned to the result item icons (at the center) and labels (at the start).
+//
+
+window.launcher {
+ background: $foobar-color-background-primary;
+ border: $foobar-dim-border-light solid $foobar-color-border;
+ border-radius: $foobar-dim-radius-large;
+
+ $search-icon-size: 24px;
+ $app-icon-size: 32px;
+ // How far the app icon needs to extend into the spacing around it.
+ $icon-overrun: ($app-icon-size - $search-icon-size) / 2;
+
+ & .search {
+ padding: $foobar-dim-margin-large-vertical $foobar-dim-margin-large-horizontal;
+
+ & image {
+ -gtk-icon-size: $search-icon-size;
+ margin-right: $foobar-dim-spacing-relaxed;
+ color: $foobar-color-foreground-secondary;
+ }
+
+ & placeholder {
+ color: $foobar-color-foreground-secondary;
+ }
+
+ & text {
+ font-size: $foobar-dim-font-large;
+ }
+ }
+
+ & row {
+ $inset: $foobar-dim-spacing-medium;
+
+ padding-left: $foobar-dim-margin-large-horizontal - $icon-overrun - $inset;
+ padding-right: $foobar-dim-margin-large-horizontal - $inset;
+ padding-top: $foobar-dim-margin-medium-vertical;
+ padding-bottom: $foobar-dim-margin-medium-vertical;
+ margin-top: $inset;
+ margin-left: $inset;
+ margin-right: $inset;
+ border-radius: $foobar-dim-radius-small;
+
+ &:last-child {
+ margin-bottom: $inset;
+ }
+
+ & .icon {
+ -gtk-icon-size: $app-icon-size;
+ margin-right: $foobar-dim-spacing-relaxed - $icon-overrun;
+ }
+
+ & .name {
+ font-size: $foobar-dim-font-small;
+ font-weight: bold;
+ }
+
+ & .description {
+ font-size: $foobar-dim-font-small;
+ margin-top: $foobar-dim-spacing-small;
+ color: $foobar-color-foreground-secondary;
+ }
+ }
+} \ No newline at end of file
diff --git a/res/styles/partials/_notification.scss b/res/styles/partials/_notification.scss
new file mode 100644
index 0000000..cfcbd40
--- /dev/null
+++ b/res/styles/partials/_notification.scss
@@ -0,0 +1,67 @@
+//
+// General notification style, used both in the control center as well as the notification area
+//
+
+notification {
+ & .content {
+ background: $foobar-color-background-primary;
+ border: $foobar-dim-border-light solid $foobar-color-border;
+ border-radius: $foobar-dim-radius-large;
+ padding: $foobar-dim-margin-medium-vertical $foobar-dim-margin-medium-horizontal;
+ }
+
+ & .icon {
+ -gtk-icon-size: 32px;
+ margin-right: $foobar-dim-spacing-large;
+ }
+
+ & .title {
+ font-weight: bold;
+ }
+
+ & .body {
+ font-size: $foobar-dim-font-small;
+ margin-top: $foobar-dim-spacing-small;
+ }
+
+ & .time {
+ font-size: $foobar-dim-font-small;
+ color: $foobar-color-foreground-secondary;
+ margin-left: $foobar-dim-spacing-medium;
+ }
+
+ & .close-button {
+ min-width: 24px;
+ min-height: 24px;
+ background: $foobar-color-background-secondary;
+ border: $foobar-dim-border-light solid $foobar-color-border;
+ border-radius: 100%;
+
+ &:hover {
+ background: $foobar-color-background-tertiary;
+ }
+
+ &:active {
+ background: $foobar-color-background-quaternary;
+ }
+ }
+}
+
+//
+// Style for the notification area window presenting popup notifications
+//
+
+window.notification-area {
+ &, & listview {
+ // XXX: To prevent some glitches
+ background: rgba(0, 0, 0, 0.005);
+ }
+
+ & row,
+ & row:hover,
+ & row:active,
+ & row:focus ,
+ & row:selected {
+ background: transparent;
+ }
+} \ No newline at end of file
diff --git a/res/styles/partials/_panel.scss b/res/styles/partials/_panel.scss
new file mode 100644
index 0000000..33cb580
--- /dev/null
+++ b/res/styles/partials/_panel.scss
@@ -0,0 +1,129 @@
+//
+// Styles for the panel and its various supported items.
+//
+
+window.panel {
+ background: $foobar-color-background-primary;
+ border: $foobar-dim-border-light solid $foobar-color-border;
+ border-radius: $foobar-dim-radius-large;
+
+ & panel-item.workspaces {
+ & row,
+ & row:hover,
+ & row:active,
+ & row:focus ,
+ & row:selected {
+ background: transparent;
+ }
+
+ & button {
+ border-radius: 100%;
+ outline: $foobar-dim-border-strong solid $foobar-color-accent-primary;
+ outline-offset: 0;
+ border: $foobar-dim-spacing-small solid $foobar-color-background-primary; // Cut-out effect
+ margin: $foobar-dim-border-strong; // To reverse the outline
+ padding: 0;
+ background: transparent;
+ transition: background-color 0.1s;
+
+ &:hover {
+ background: $foobar-color-accent-tertiary;
+ }
+
+ &.visible {
+ background: $foobar-color-accent-secondary;
+ }
+
+ &.active {
+ background: $foobar-color-accent-primary;
+ }
+ }
+
+ @if variable-exists(foobar-color-alternate-primary) {
+ & row:nth-child(even) button:not(.special):not(.urgent) {
+ outline-color: $foobar-color-alternate-primary;
+
+ &:hover {
+ background: $foobar-color-alternate-tertiary;
+ }
+
+ &.visible {
+ background: $foobar-color-alternate-secondary;
+ }
+
+ &.active {
+ background: $foobar-color-alternate-primary;
+ }
+ }
+ }
+
+ & button.special {
+ outline-color: $foobar-color-special-primary;
+
+ &:hover {
+ background: $foobar-color-special-tertiary;
+ }
+
+ &.visible {
+ background: $foobar-color-special-secondary;
+ }
+
+ &.active {
+ background: $foobar-color-special-primary;
+ }
+ }
+
+ & button.urgent {
+ outline-color: $foobar-color-urgent-primary;
+
+ &:hover {
+ background: $foobar-color-urgent-tertiary;
+ }
+
+ &.visible {
+ background: $foobar-color-urgent-secondary;
+ }
+
+ &.active {
+ background: $foobar-color-urgent-primary;
+ }
+ }
+ }
+
+ & panel-item.clock {
+ & button {
+ min-width: 24px;
+ min-height: 24px;
+ }
+
+ & label {
+ font-size: $foobar-dim-font-large;
+ font-weight: bold;
+ }
+ }
+
+ & panel-item.icon {
+ & button {
+ -gtk-icon-size: 20px;
+ min-width: 24px;
+ min-height: 24px;
+ }
+ }
+
+ & panel-item.status {
+ & button {
+ min-width: 24px;
+ min-height: 24px;
+ }
+
+ & image {
+ -gtk-icon-size: 20px;
+ }
+
+ & label {
+ margin-left: $foobar-dim-spacing-small;
+ font-weight: normal;
+ font-size: $foobar-dim-font-small;
+ }
+ }
+} \ No newline at end of file
diff --git a/res/styles/styles.gresource.xml b/res/styles/styles.gresource.xml
new file mode 100644
index 0000000..e708a1d
--- /dev/null
+++ b/res/styles/styles.gresource.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/foobar/styles">
+ <file compressed="true">default.css</file>
+ <file compressed="true">monokai-pro.css</file>
+ <file compressed="true">teatime.css</file>
+ </gresource>
+</gresources>
diff --git a/res/styles/teatime.scss b/res/styles/teatime.scss
new file mode 100644
index 0000000..6889f9b
--- /dev/null
+++ b/res/styles/teatime.scss
@@ -0,0 +1,46 @@
+// Color scheme by the AMAZING Nxdxl
+// ^ some guy who thought i wouldn't review the source code before publishing it under my name
+
+$foobar-color-background-primary: #fff6f6;
+$foobar-color-background-secondary: #fff6f6;
+$foobar-color-background-tertiary: #fff6f6;
+$foobar-color-background-quaternary: #fff6f6;
+$foobar-color-foreground-primary: #1b1b2e;
+$foobar-color-foreground-secondary: transparentize($foobar-color-foreground-primary, 0.5);
+$foobar-color-foreground-tertiary: transparentize($foobar-color-foreground-primary, 0.8);
+$foobar-color-foreground-quaternary: transparentize($foobar-color-foreground-primary, 0.9);
+$foobar-color-accent-primary: #c2649f;
+$foobar-color-accent-secondary: transparentize($foobar-color-accent-primary, 0.5);
+$foobar-color-accent-tertiary: transparentize($foobar-color-accent-primary, 0.8);
+$foobar-color-accent-quaternary: transparentize($foobar-color-accent-primary, 0.9);
+$foobar-color-alternate-primary: #4ba89d;
+$foobar-color-alternate-secondary: transparentize($foobar-color-alternate-primary, 0.5);
+$foobar-color-alternate-tertiary: transparentize($foobar-color-alternate-primary, 0.8);
+$foobar-color-alternate-quaternary: transparentize($foobar-color-alternate-primary, 0.9);
+$foobar-color-special-primary: #4ba89d;
+$foobar-color-special-secondary: transparentize($foobar-color-special-primary, 0.5);
+$foobar-color-special-tertiary: transparentize($foobar-color-special-primary, 0.8);
+$foobar-color-special-quaternary: transparentize($foobar-color-special-primary, 0.9);
+$foobar-color-urgent-primary: #cd6868;
+$foobar-color-urgent-secondary: transparentize($foobar-color-urgent-primary, 0.5);
+$foobar-color-urgent-tertiary: transparentize($foobar-color-urgent-primary, 0.8);
+$foobar-color-urgent-quaternary: transparentize($foobar-color-urgent-primary, 0.9);
+$foobar-color-border: #c2649f;
+$foobar-dim-border-strong: 2px;
+$foobar-dim-border-light: 2px;
+$foobar-dim-radius-large: 12px;
+$foobar-dim-radius-small: 6px;
+$foobar-dim-spacing-relaxed: 18px;
+$foobar-dim-spacing-large: 12px;
+$foobar-dim-spacing-medium: 6px;
+$foobar-dim-spacing-small: 3px;
+$foobar-dim-margin-large-horizontal: 24px;
+$foobar-dim-margin-large-vertical: 18px;
+$foobar-dim-margin-medium-horizontal: 18px;
+$foobar-dim-margin-medium-vertical: 12px;
+$foobar-dim-margin-small-horizontal: 6px;
+$foobar-dim-margin-small-vertical: 6px;
+$foobar-dim-font-large: 13px;
+$foobar-dim-font-small: 13px;
+
+@import './common';
diff --git a/src/application.c b/src/application.c
new file mode 100644
index 0000000..485078a
--- /dev/null
+++ b/src/application.c
@@ -0,0 +1,686 @@
+#include "application.h"
+#include "panel.h"
+#include "launcher.h"
+#include "control-center.h"
+#include "notification-area.h"
+#include "dbus/server.h"
+#include "services/battery-service.h"
+#include "services/clock-service.h"
+#include "services/brightness-service.h"
+#include "services/workspace-service.h"
+#include "services/notification-service.h"
+#include "services/audio-service.h"
+#include "services/network-service.h"
+#include "services/bluetooth-service.h"
+#include "services/application-service.h"
+#include "services/configuration-service.h"
+
+//
+// FoobarApplication:
+//
+// Entry point for the application. This class is responsible for parsing and handling command line arguments, creating
+// services and presenting windows as needed.
+//
+// Only the panel windows are application windows -- when all panels are closed, the application quits. Because of this,
+// the application can't own a reference to the panel windows but needs to store their window IDs. Since all other
+// windows are not attached to the application object, the application can hold a reference on them.
+//
+
+struct _FoobarApplication
+{
+ GtkApplication parent_instance;
+ FoobarBatteryService* battery_service;
+ FoobarClockService* clock_service;
+ FoobarBrightnessService* brightness_service;
+ FoobarWorkspaceService* workspace_service;
+ FoobarNotificationService* notification_service;
+ FoobarAudioService* audio_service;
+ FoobarNetworkService* network_service;
+ FoobarBluetoothService* bluetooth_service;
+ FoobarApplicationService* application_service;
+ FoobarConfigurationService* configuration_service;
+ gulong config_handler_id;
+ FoobarServer* server_skeleton;
+ GtkCssProvider* style_provider;
+ guint bus_owner_id;
+ GArray* panel_window_ids;
+ gboolean panel_is_multi_monitor;
+ GListModel* monitors;
+ gulong monitors_handler_id;
+ FoobarLauncher* launcher;
+ FoobarControlCenter* control_center;
+ FoobarNotificationArea* notification_area;
+ gboolean option_inspector;
+ gboolean option_quit;
+ gboolean option_toggle_launcher;
+ gboolean option_toggle_control_center;
+};
+
+static void foobar_application_class_init ( FoobarApplicationClass* klass );
+static void foobar_application_init ( FoobarApplication* self );
+static void foobar_application_activate ( GApplication* app );
+static int foobar_application_command_line ( GApplication* app,
+ GApplicationCommandLine* cmdline );
+static void foobar_application_finalize ( GObject* object );
+static void foobar_application_handle_bus_acquired ( GDBusConnection* connection,
+ gchar const* name,
+ gpointer userdata );
+static gboolean foobar_application_handle_inspector ( FoobarServer* server,
+ GDBusMethodInvocation* invocation,
+ gpointer userdata );
+static gboolean foobar_application_handle_quit ( FoobarServer* server,
+ GDBusMethodInvocation* invocation,
+ gpointer userdata );
+static gboolean foobar_application_handle_toggle_launcher ( FoobarServer* server,
+ GDBusMethodInvocation* invocation,
+ gpointer userdata );
+static gboolean foobar_application_handle_toggle_control_center( FoobarServer* server,
+ GDBusMethodInvocation* invocation,
+ gpointer userdata );
+static void foobar_application_handle_config_changed ( GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata );
+static void foobar_application_handle_monitors_changed ( GListModel* list,
+ guint position,
+ guint removed,
+ guint added,
+ gpointer userdata );
+static void foobar_application_destroy_panels ( FoobarApplication* self );
+static void foobar_application_create_panels ( FoobarApplication* self );
+static guint foobar_application_create_panel ( FoobarApplication* self,
+ GdkMonitor* monitor );
+
+G_DEFINE_FINAL_TYPE( FoobarApplication, foobar_application, GTK_TYPE_APPLICATION )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Application Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the application.
+//
+void foobar_application_class_init( FoobarApplicationClass* klass )
+{
+ GApplicationClass* app_klass = G_APPLICATION_CLASS( klass );
+ app_klass->activate = foobar_application_activate;
+ app_klass->command_line = foobar_application_command_line;
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->finalize = foobar_application_finalize;
+}
+
+//
+// Instance initialization for the application.
+//
+void foobar_application_init( FoobarApplication* self )
+{
+ g_application_set_default( G_APPLICATION( self ) );
+ g_application_set_option_context_summary( G_APPLICATION( self ), "A bar." );
+
+ GOptionEntry const options[] =
+ {
+ {
+ .long_name = "inspector",
+ .short_name = 'i',
+ .flags = G_OPTION_FLAG_NONE,
+ .arg = G_OPTION_ARG_NONE,
+ .arg_data = &self->option_inspector,
+ .description = "Open the GTK inspector for the active bar instance.",
+ .arg_description = NULL,
+ },
+ {
+ .long_name = "quit",
+ .short_name = 'q',
+ .flags = G_OPTION_FLAG_NONE,
+ .arg = G_OPTION_ARG_NONE,
+ .arg_data = &self->option_quit,
+ .description = "Quit the active bar instance.",
+ .arg_description = NULL,
+ },
+ {
+ .long_name = "toggle-launcher",
+ .short_name = 'l',
+ .flags = G_OPTION_FLAG_NONE,
+ .arg = G_OPTION_ARG_NONE,
+ .arg_data = &self->option_toggle_launcher,
+ .description = "Toggle the launcher visibility for the active bar instance.",
+ .arg_description = NULL,
+ },
+ {
+ .long_name = "toggle-control-center",
+ .short_name = 'c',
+ .flags = G_OPTION_FLAG_NONE,
+ .arg = G_OPTION_ARG_NONE,
+ .arg_data = &self->option_toggle_control_center,
+ .description = "Toggle the control center visibility for the active bar instance.",
+ .arg_description = NULL,
+ },
+ { 0 },
+ };
+ g_application_add_main_option_entries( G_APPLICATION( self ), options );
+}
+
+//
+// Called by GTK when the application is activated (only for a single instance of the application).
+//
+// This is where the services are created and the windows are presented.
+//
+void foobar_application_activate( GApplication* app )
+{
+ FoobarApplication* self = (FoobarApplication*)app;
+
+ // Initialize services.
+
+ self->battery_service = foobar_battery_service_new( );
+ self->clock_service = foobar_clock_service_new( );
+ self->brightness_service = foobar_brightness_service_new( );
+ self->workspace_service = foobar_workspace_service_new( );
+ self->notification_service = foobar_notification_service_new( );
+ self->audio_service = foobar_audio_service_new( );
+ self->network_service = foobar_network_service_new( );
+ self->bluetooth_service = foobar_bluetooth_service_new( );
+ self->application_service = foobar_application_service_new( );
+ self->configuration_service = foobar_configuration_service_new( );
+
+ // Enforce a uniform style by forcing Adwaita and shipping our own icons.
+
+ GtkSettings* settings = gtk_settings_get_default( );
+ g_object_set( settings, "gtk-theme-name", "Adwaita", NULL );
+
+ GtkIconTheme* icon_theme = gtk_icon_theme_get_for_display( gdk_display_get_default( ) );
+ gtk_icon_theme_add_resource_path( icon_theme, "/foobar/icons" );
+
+ // This application instance now becomes the "server" process, listening for commands from clients (which can be
+ // invoked via the command line).
+
+ self->bus_owner_id = g_bus_own_name(
+ G_BUS_TYPE_SESSION,
+ g_application_get_application_id( G_APPLICATION( self ) ),
+ G_BUS_NAME_OWNER_FLAGS_NONE,
+ foobar_application_handle_bus_acquired,
+ NULL,
+ NULL,
+ self,
+ NULL );
+
+ // React to configuration file changes.
+
+ FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service );
+ foobar_application_apply_configuration( self, foobar_configuration_get_general( config ) );
+ self->panel_is_multi_monitor = foobar_panel_configuration_get_multi_monitor( foobar_configuration_get_panel( config ) );
+ self->config_handler_id = g_signal_connect(
+ self->configuration_service,
+ "notify::current",
+ G_CALLBACK( foobar_application_handle_config_changed ),
+ self );
+
+ // React to monitor configuration changes (for multi-monitor support in the panel).
+
+ self->monitors = gdk_display_get_monitors( gdk_display_get_default( ) );
+ g_object_ref( self->monitors );
+
+ self->monitors_handler_id = g_signal_connect(
+ self->monitors,
+ "items-changed",
+ G_CALLBACK( foobar_application_handle_monitors_changed ),
+ self );
+
+ // Create all the windows.
+
+ foobar_application_create_panels( self );
+
+ self->launcher = foobar_launcher_new(
+ self->application_service,
+ self->configuration_service );
+ g_object_ref( self->launcher );
+
+ self->control_center = foobar_control_center_new(
+ self->brightness_service,
+ self->audio_service,
+ self->network_service,
+ self->bluetooth_service,
+ self->notification_service,
+ self->configuration_service );
+ g_object_ref( self->control_center );
+
+ self->notification_area = foobar_notification_area_new(
+ self->notification_service,
+ self->configuration_service );
+ g_object_ref( self->notification_area );
+
+ // Only present the notification area, all other windows start out as hidden windows.
+
+ gtk_window_present( GTK_WINDOW( self->notification_area ) );
+}
+
+//
+// Called before activating the application, when GTK has parsed the command line arguments.
+//
+// This method decides whether the application should be activated (if it is the first instance). Otherwise it will
+// handle command line flags for controlling the application.
+//
+int foobar_application_command_line(
+ GApplication* app,
+ GApplicationCommandLine* cmdline )
+{
+ (void)cmdline;
+ FoobarApplication* self = (FoobarApplication*)app;
+
+ // Try to open a connection to the existing server.
+
+ g_autoptr( GError ) error = NULL;
+ g_autoptr( FoobarServer ) proxy = foobar_server_proxy_new_for_bus_sync(
+ G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ g_application_get_application_id( G_APPLICATION( self ) ),
+ "/com/github/hannesschulze/foobar/server",
+ NULL,
+ &error );
+
+ if ( !proxy )
+ {
+ g_printerr( "Unable to create proxy: %s\n", error->message );
+ return 1;
+ }
+
+ // If there is no existing server application, the "name owner" will not be set.
+
+ g_autofree gchar* owner = g_dbus_proxy_get_name_owner( G_DBUS_PROXY( proxy ) );
+ if ( owner )
+ {
+ // There is an active instance -> this is a client. Handle all command line flags.
+
+ if ( self->option_inspector && !foobar_server_call_inspector_sync( proxy, NULL, &error ) )
+ {
+ g_printerr( "Unable to open inspector: %s\n", error->message );
+ return 2;
+ }
+
+ if ( self->option_toggle_launcher && !foobar_server_call_toggle_launcher_sync( proxy, NULL, &error ) )
+ {
+ g_printerr( "Unable to toggle launcher: %s\n", error->message );
+ return 2;
+ }
+
+ if ( self->option_toggle_control_center && !foobar_server_call_toggle_control_center_sync( proxy, NULL, &error ) )
+ {
+ g_printerr( "Unable to toggle control center: %s\n", error->message );
+ return 2;
+ }
+
+ if ( self->option_quit && !foobar_server_call_quit_sync( proxy, NULL, &error ) )
+ {
+ g_printerr( "Unable to quit server: %s\n", error->message );
+ return 2;
+ }
+ }
+ else if ( !self->option_quit )
+ {
+ // This is the main application -> launch the server.
+
+ g_application_activate( G_APPLICATION( self ) );
+ }
+
+ return 0;
+}
+
+//
+// Instance cleanup for the application.
+//
+void foobar_application_finalize( GObject* object )
+{
+ FoobarApplication* self = (FoobarApplication*)object;
+
+ // Destroy the DBus server.
+
+ if ( self->server_skeleton )
+ {
+ g_dbus_interface_skeleton_unexport( G_DBUS_INTERFACE_SKELETON( self->server_skeleton ) );
+ }
+
+ // Destroy non-application windows.
+
+ if ( self->launcher ) { gtk_window_destroy( GTK_WINDOW( self->launcher ) ); }
+ if ( self->control_center ) { gtk_window_destroy( GTK_WINDOW( self->control_center ) ); }
+ if ( self->notification_area ) { gtk_window_destroy( GTK_WINDOW( self->notification_area ) ); }
+
+ // Destroy all other objects.
+
+ g_clear_handle_id( &self->bus_owner_id, g_bus_unown_name );
+ g_clear_signal_handler( &self->config_handler_id, self->configuration_service );
+ g_clear_signal_handler( &self->monitors_handler_id, self->monitors );
+ g_clear_object( &self->battery_service );
+ g_clear_object( &self->clock_service );
+ g_clear_object( &self->brightness_service );
+ g_clear_object( &self->workspace_service );
+ g_clear_object( &self->notification_service );
+ g_clear_object( &self->audio_service );
+ g_clear_object( &self->network_service );
+ g_clear_object( &self->bluetooth_service );
+ g_clear_object( &self->application_service );
+ g_clear_object( &self->configuration_service );
+ g_clear_object( &self->style_provider );
+ g_clear_object( &self->server_skeleton );
+ g_clear_object( &self->launcher );
+ g_clear_object( &self->control_center );
+ g_clear_object( &self->notification_area );
+ g_clear_object( &self->monitors );
+ g_clear_pointer( &self->panel_window_ids, g_array_unref );
+
+ G_OBJECT_CLASS( foobar_application_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new application instance.
+//
+FoobarApplication* foobar_application_new( void )
+{
+ return g_object_new(
+ FOOBAR_TYPE_APPLICATION,
+ "application-id",
+ "com.github.hannesschulze.foobar",
+ "flags",
+ G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_NON_UNIQUE,
+ NULL );
+}
+
+//
+// Toggle the launcher window's visibility.
+//
+void foobar_application_toggle_launcher( FoobarApplication* self )
+{
+ g_return_if_fail( FOOBAR_IS_APPLICATION( self ) );
+
+ gboolean previously_visible = gtk_widget_is_visible( GTK_WIDGET( self->launcher ) );
+ gtk_widget_set_visible( GTK_WIDGET( self->launcher ), !previously_visible );
+}
+
+//
+// Toggle the control center window's visibility.
+//
+void foobar_application_toggle_control_center( FoobarApplication* self )
+{
+ g_return_if_fail( FOOBAR_IS_APPLICATION( self ) );
+
+ gboolean previously_visible = gtk_widget_is_visible( GTK_WIDGET( self->control_center ) );
+ gtk_widget_set_visible( GTK_WIDGET( self->control_center ), !previously_visible );
+}
+
+//
+// Apply the general application configuration provided by the configuration service.
+//
+void foobar_application_apply_configuration(
+ FoobarApplication* self,
+ FoobarGeneralConfiguration const* config )
+{
+ g_return_if_fail( FOOBAR_IS_APPLICATION( self ) );
+ g_return_if_fail( config != NULL );
+
+ GdkDisplay* display = gdk_display_get_default( );
+
+ // Remove the old stylesheet from the application.
+
+ if ( self->style_provider )
+ {
+ gtk_style_context_remove_provider_for_display( display, GTK_STYLE_PROVIDER( self->style_provider ) );
+ g_clear_object( &self->style_provider );
+ }
+
+ // Add the new stylesheet.
+
+ g_autoptr( GFile ) file = g_file_new_for_uri( foobar_general_configuration_get_stylesheet( config ) );
+ self->style_provider = gtk_css_provider_new( );
+ gtk_css_provider_load_from_file( self->style_provider, file );
+ gtk_style_context_add_provider_for_display(
+ display,
+ GTK_STYLE_PROVIDER( self->style_provider ),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called after the application server process has asynchronously acquired the bus.
+//
+// This is where we can export server functionality using the corresponding DBus skeleton object.
+//
+void foobar_application_handle_bus_acquired(
+ GDBusConnection* connection,
+ gchar const* name,
+ gpointer userdata )
+{
+ (void)name;
+ FoobarApplication* self = (FoobarApplication*)userdata;
+
+ self->server_skeleton = foobar_server_skeleton_new( );
+ g_signal_connect(
+ self->server_skeleton,
+ "handle-inspector",
+ G_CALLBACK( foobar_application_handle_inspector ),
+ self );
+ g_signal_connect(
+ self->server_skeleton,
+ "handle-quit",
+ G_CALLBACK( foobar_application_handle_quit ),
+ self );
+ g_signal_connect(
+ self->server_skeleton,
+ "handle-toggle-launcher",
+ G_CALLBACK( foobar_application_handle_toggle_launcher ),
+ self );
+ g_signal_connect(
+ self->server_skeleton,
+ "handle-toggle-control-center",
+ G_CALLBACK( foobar_application_handle_toggle_control_center ),
+ self );
+
+ g_autoptr( GError ) error = NULL;
+ if ( !g_dbus_interface_skeleton_export(
+ G_DBUS_INTERFACE_SKELETON( self->server_skeleton ),
+ connection,
+ "/com/github/hannesschulze/foobar/server",
+ &error ) )
+ {
+ g_warning( "Unable to export server interface: %s", error->message );
+ }
+}
+
+//
+// DBus skeleton callback for the "Inspector" method.
+//
+gboolean foobar_application_handle_inspector(
+ FoobarServer* server,
+ GDBusMethodInvocation* invocation,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ gtk_window_set_interactive_debugging( TRUE );
+
+ foobar_server_complete_inspector( server, invocation );
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+//
+// DBus skeleton callback for the "Quit" method.
+//
+gboolean foobar_application_handle_quit(
+ FoobarServer* server,
+ GDBusMethodInvocation* invocation,
+ gpointer userdata )
+{
+ FoobarApplication* self = (FoobarApplication*)userdata;
+
+ foobar_application_destroy_panels( self );
+
+ foobar_server_complete_quit( server, invocation );
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+//
+// DBus skeleton callback for the "ToggleLauncher" method.
+//
+gboolean foobar_application_handle_toggle_launcher(
+ FoobarServer* server,
+ GDBusMethodInvocation* invocation,
+ gpointer userdata )
+{
+ FoobarApplication* self = (FoobarApplication*)userdata;
+
+ foobar_application_toggle_launcher( self );
+
+ foobar_server_complete_toggle_launcher( server, invocation );
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+//
+// DBus skeleton callback for the "ToggleControlCenter" method.
+//
+gboolean foobar_application_handle_toggle_control_center(
+ FoobarServer* server,
+ GDBusMethodInvocation* invocation,
+ gpointer userdata )
+{
+ FoobarApplication* self = (FoobarApplication*)userdata;
+
+ foobar_application_toggle_control_center( self );
+
+ foobar_server_complete_toggle_control_center( server, invocation );
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+//
+// Signal handler called when the global configuration file has changed.
+//
+void foobar_application_handle_config_changed(
+ GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)object;
+ (void)pspec;
+ FoobarApplication* self = (FoobarApplication*)userdata;
+
+ // Apply general settings.
+
+ FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service );
+ foobar_application_apply_configuration( self, foobar_configuration_get_general( config ) );
+
+ // Update panel instances depending on whether multi-monitor mode is enabled.
+
+ FoobarPanelConfiguration const* panel_config = foobar_configuration_get_panel( config );
+ gboolean panel_is_multi_monitor = foobar_panel_configuration_get_multi_monitor( panel_config );
+ if ( self->panel_is_multi_monitor != panel_is_multi_monitor )
+ {
+ self->panel_is_multi_monitor = panel_is_multi_monitor;
+
+ // Panel windows keep the application alive, so we need to "hold" it while we re-create the panel windows.
+
+ g_application_hold( G_APPLICATION( self ) );
+ foobar_application_destroy_panels( self );
+ foobar_application_create_panels( self );
+ g_application_release( G_APPLICATION( self ) );
+ }
+}
+
+//
+// Signal handler called when the list of GdkMonitor objects has changed, possibly requiring us to re-create the panel
+// windows.
+//
+void foobar_application_handle_monitors_changed(
+ GListModel* list,
+ guint position,
+ guint removed,
+ guint added,
+ gpointer userdata )
+{
+ (void)list;
+ (void)position;
+ (void)removed;
+ (void)added;
+ FoobarApplication* self = (FoobarApplication*)userdata;
+
+ if ( self->panel_is_multi_monitor )
+ {
+ // Panel windows keep the application alive, so we need to "hold" it while we re-create the panel windows.
+
+ g_application_hold( G_APPLICATION( self ) );
+ foobar_application_destroy_panels( self );
+ foobar_application_create_panels( self );
+ g_application_release( G_APPLICATION( self ) );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Destroy all currently active panel windows.
+//
+void foobar_application_destroy_panels( FoobarApplication* self )
+{
+ for ( guint i = 0; i < self->panel_window_ids->len; ++i )
+ {
+ guint window_id = g_array_index( self->panel_window_ids, guint, i );
+ GtkWindow* window = gtk_application_get_window_by_id( GTK_APPLICATION( self ), window_id );
+ if ( window ) { gtk_window_destroy( window ); }
+ }
+
+ g_clear_pointer( &self->panel_window_ids, g_array_unref );
+}
+
+//
+// Create and present panel windows for
+// - each monitor, if multi-monitor mode is enabled
+// - the current monitor, if multi-monitor mode is disabled.
+//
+void foobar_application_create_panels( FoobarApplication* self )
+{
+ self->panel_window_ids = g_array_new( FALSE, FALSE, sizeof( guint ) );
+
+ if ( self->panel_is_multi_monitor )
+ {
+ for ( guint i = 0; i < g_list_model_get_n_items( self->monitors ); ++i )
+ {
+ GdkMonitor* monitor = g_list_model_get_item( self->monitors, i );
+ guint id = foobar_application_create_panel( self, monitor );
+ g_array_append_val( self->panel_window_ids, id );
+ }
+ }
+ else
+ {
+ guint id = foobar_application_create_panel( self, NULL );
+ g_array_append_val( self->panel_window_ids, id );
+ }
+}
+
+//
+// Create and present a panel for a single monitor (or NULL to use the current monitor).
+//
+guint foobar_application_create_panel(
+ FoobarApplication* self,
+ GdkMonitor* monitor )
+{
+ FoobarPanel* panel = foobar_panel_new(
+ GTK_APPLICATION( self ),
+ monitor,
+ self->battery_service,
+ self->clock_service,
+ self->brightness_service,
+ self->workspace_service,
+ self->audio_service,
+ self->network_service,
+ self->bluetooth_service,
+ self->notification_service,
+ self->configuration_service );
+ gtk_window_present( GTK_WINDOW( panel ) );
+
+ return gtk_application_window_get_id( GTK_APPLICATION_WINDOW( panel ) );
+} \ No newline at end of file
diff --git a/src/application.h b/src/application.h
new file mode 100644
index 0000000..efd517a
--- /dev/null
+++ b/src/application.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <gtk/gtk.h>
+#include "services/configuration-service.h"
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_APPLICATION foobar_application_get_type( )
+#define FOOBAR_APPLICATION_DEFAULT FOOBAR_APPLICATION( g_application_get_default( ) )
+
+G_DECLARE_FINAL_TYPE( FoobarApplication, foobar_application, FOOBAR, APPLICATION, GtkApplication )
+
+FoobarApplication* foobar_application_new ( void );
+void foobar_application_toggle_launcher ( FoobarApplication* self );
+void foobar_application_toggle_control_center( FoobarApplication* self );
+void foobar_application_apply_configuration ( FoobarApplication* self,
+ FoobarGeneralConfiguration const* config );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/control-center.c b/src/control-center.c
new file mode 100644
index 0000000..23c8a1a
--- /dev/null
+++ b/src/control-center.c
@@ -0,0 +1,821 @@
+#include "control-center.h"
+#include "widgets/control-center/control-button.h"
+#include "widgets/control-center/control-slider.h"
+#include "widgets/control-center/control-details.h"
+#include "widgets/control-center/control-details-item.h"
+#include "widgets/inset-container.h"
+#include "widgets/notification-widget.h"
+#include <gtk4-layer-shell.h>
+
+#define STACK_ITEM_LIST "list"
+#define STACK_ITEM_PLACEHOLDER "placeholder"
+
+//
+// FoobarControlCenter:
+//
+// The control center is made up of two sections:
+// - The "controls" section for configuring wi-fi, volume, brightness, etc.
+// - The "notifications" section showing all notifications (including dismissed ones) and allowing the user to close
+// them.
+//
+
+struct _FoobarControlCenter
+{
+ GtkWindow parent_instance;
+ GtkWidget* control_container;
+ GtkWidget* notification_list;
+ GtkWidget* notification_container;
+ GtkWidget* notification_placeholder;
+ GtkWidget* notification_stack;
+ GtkWidget* layout;
+ FoobarBrightnessService* brightness_service;
+ FoobarAudioService* audio_service;
+ FoobarNetworkService* network_service;
+ FoobarBluetoothService* bluetooth_service;
+ FoobarNotificationService* notification_service;
+ FoobarConfigurationService* configuration_service;
+ gchar* notification_time_format;
+ gint notification_min_height;
+ gint notification_close_button_inset;
+ gint padding;
+ gint spacing;
+ gulong config_handler_id;
+};
+
+static void foobar_control_center_class_init ( FoobarControlCenterClass* klass );
+static void foobar_control_center_init ( FoobarControlCenter* self );
+static void foobar_control_center_finalize ( GObject* object );
+static void foobar_control_center_handle_notification_setup ( GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata );
+static void foobar_control_center_handle_network_setup ( GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata );
+static void foobar_control_center_handle_bluetooth_device_setup ( GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata );
+static void foobar_control_center_handle_bluetooth_device_activate ( GtkListView* view,
+ guint position,
+ gpointer userdata );
+static void foobar_control_center_handle_audio_device_setup ( GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata );
+static void foobar_control_center_handle_audio_device_activate ( GtkListView* view,
+ guint position,
+ gpointer userdata );
+static void foobar_control_center_handle_config_change ( GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata );
+static FoobarControlDetailsAccessory foobar_control_center_compute_bluetooth_device_accessory ( GtkExpression* expression,
+ FoobarBluetoothDeviceState state,
+ gpointer userdata );
+static gchar* foobar_control_center_compute_visible_notification_child_name( GtkExpression* expression,
+ guint count,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarControlCenter, foobar_control_center, GTK_TYPE_WINDOW )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Window Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the control center.
+//
+void foobar_control_center_class_init( FoobarControlCenterClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->finalize = foobar_control_center_finalize;
+}
+
+//
+// Instance initialization for the control center.
+//
+void foobar_control_center_init( FoobarControlCenter* self )
+{
+ self->control_container = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_widget_add_css_class( self->control_container, "controls" );
+ gtk_widget_set_hexpand( self->control_container, TRUE );
+
+ GtkListItemFactory* notification_factory = gtk_signal_list_item_factory_new( );
+ g_signal_connect(
+ notification_factory,
+ "setup",
+ G_CALLBACK( foobar_control_center_handle_notification_setup ),
+ self );
+ self->notification_list = gtk_list_view_new( NULL, notification_factory );
+ gtk_widget_add_css_class( self->notification_list, "notifications" );
+
+ // XXX: Get rid of this container to enable virtualization; currently needed because I haven't figured out how to
+ // set padding without using css (setting margin on list view causes glitches when scrolling).
+ self->notification_container = foobar_inset_container_new( );
+ foobar_inset_container_set_child( FOOBAR_INSET_CONTAINER( self->notification_container ), self->notification_list );
+
+ GtkWidget* scrolled_window = gtk_scrolled_window_new( );
+ gtk_scrolled_window_set_child( GTK_SCROLLED_WINDOW( scrolled_window ), self->notification_container );
+ gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scrolled_window ), GTK_POLICY_NEVER, GTK_POLICY_EXTERNAL );
+
+ self->notification_placeholder = gtk_label_new( "No Notifications" );
+ gtk_widget_add_css_class( self->notification_placeholder, "placeholder" );
+ gtk_label_set_ellipsize( GTK_LABEL( self->notification_placeholder ), PANGO_ELLIPSIZE_END );
+ gtk_label_set_wrap( GTK_LABEL( self->notification_placeholder ), FALSE );
+
+ self->notification_stack = gtk_stack_new( );
+ gtk_stack_add_named( GTK_STACK( self->notification_stack ), scrolled_window, STACK_ITEM_LIST );
+ gtk_stack_add_named( GTK_STACK( self->notification_stack ), self->notification_placeholder, STACK_ITEM_PLACEHOLDER );
+ gtk_widget_set_vexpand( self->notification_stack, TRUE );
+ gtk_widget_set_hexpand( self->notification_stack, TRUE );
+
+ self->layout = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_box_append( GTK_BOX( self->layout ), self->control_container );
+ gtk_box_append( GTK_BOX( self->layout ), gtk_separator_new( GTK_ORIENTATION_HORIZONTAL ) );
+ gtk_box_append( GTK_BOX( self->layout ), self->notification_stack );
+
+ g_autoptr( GtkSizeGroup ) size_group = gtk_size_group_new( GTK_SIZE_GROUP_HORIZONTAL );
+ gtk_size_group_add_widget( size_group, self->control_container );
+ gtk_size_group_add_widget( size_group, self->notification_stack );
+
+ gtk_window_set_child( GTK_WINDOW( self ), self->layout );
+ gtk_window_set_title( GTK_WINDOW( self ), "Foobar Control Center" );
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "control-center" );
+ gtk_layer_init_for_window( GTK_WINDOW( self ) );
+ gtk_layer_set_namespace( GTK_WINDOW( self ), "foobar-control-center" );
+}
+
+//
+// Instance cleanup for the control center.
+//
+void foobar_control_center_finalize( GObject* object )
+{
+ FoobarControlCenter* self = (FoobarControlCenter*)object;
+
+ g_clear_signal_handler( &self->config_handler_id, self->configuration_service );
+ g_clear_object( &self->brightness_service );
+ g_clear_object( &self->audio_service );
+ g_clear_object( &self->network_service );
+ g_clear_object( &self->bluetooth_service );
+ g_clear_object( &self->configuration_service );
+ g_clear_object( &self->notification_service );
+ g_clear_pointer( &self->notification_time_format, g_free );
+
+ G_OBJECT_CLASS( foobar_control_center_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new control center instance.
+//
+FoobarControlCenter* foobar_control_center_new(
+ FoobarBrightnessService* brightness_service,
+ FoobarAudioService* audio_service,
+ FoobarNetworkService* network_service,
+ FoobarBluetoothService* bluetooth_service,
+ FoobarNotificationService* notification_service,
+ FoobarConfigurationService* configuration_service )
+{
+ g_return_val_if_fail( FOOBAR_IS_BRIGHTNESS_SERVICE( brightness_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( audio_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_NETWORK_SERVICE( network_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( bluetooth_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( notification_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( configuration_service ), NULL );
+
+ FoobarControlCenter* self = g_object_new( FOOBAR_TYPE_CONTROL_CENTER, NULL );
+ self->brightness_service = g_object_ref( brightness_service );
+ self->audio_service = g_object_ref( audio_service );
+ self->network_service = g_object_ref( network_service );
+ self->bluetooth_service = g_object_ref( bluetooth_service );
+ self->notification_service = g_object_ref( notification_service );
+ self->configuration_service = g_object_ref( configuration_service );
+
+ // Apply the configuration and subscribe to changes.
+
+ FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service );
+ foobar_control_center_apply_configuration(
+ self,
+ foobar_configuration_get_control_center( config ),
+ foobar_configuration_get_notifications( config ) );
+ self->config_handler_id = g_signal_connect(
+ self->configuration_service,
+ "notify::current",
+ G_CALLBACK( foobar_control_center_handle_config_change ),
+ self );
+
+ // Set up the notifications list view.
+
+ GListModel* source_model = foobar_notification_service_get_notifications( self->notification_service );
+ GtkNoSelection* selection_model = gtk_no_selection_new( g_object_ref( source_model ) );
+ gtk_list_view_set_model( GTK_LIST_VIEW( self->notification_list ), GTK_SELECTION_MODEL( selection_model ) );
+
+ // Set up bindings.
+
+ {
+ GtkExpression* count_expr = gtk_property_expression_new( GTK_TYPE_SORT_LIST_MODEL, NULL, "n-items" );
+ GtkExpression* child_params[] = { count_expr };
+ GtkExpression* child_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( child_params ),
+ child_params,
+ G_CALLBACK( foobar_control_center_compute_visible_notification_child_name ),
+ NULL,
+ NULL );
+ gtk_expression_bind( child_expr, self->notification_stack, "visible-child-name", source_model );
+ }
+
+ return self;
+}
+
+//
+// Apply the control center configuration provided by the configuration service, additionally using the provided
+// notification configuration for some settings.
+//
+void foobar_control_center_apply_configuration(
+ FoobarControlCenter* self,
+ FoobarControlCenterConfiguration const* config,
+ FoobarNotificationConfiguration const* notification_config )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_CENTER( self ) );
+ g_return_if_fail( config != NULL );
+ g_return_if_fail( notification_config != NULL );
+
+ // Copy configuration into member variables.
+
+ g_clear_pointer( &self->notification_time_format, g_free );
+ self->notification_time_format = g_strdup( foobar_notification_configuration_get_time_format( notification_config ) );
+ self->notification_min_height = foobar_notification_configuration_get_min_height( notification_config );
+ self->notification_close_button_inset = foobar_notification_configuration_get_close_button_inset( notification_config );
+ self->padding = foobar_control_center_configuration_get_padding( config );
+ self->spacing = foobar_control_center_configuration_get_spacing( config );
+
+ // Configure the window.
+
+ gtk_window_set_default_size(
+ GTK_WINDOW( self ),
+ foobar_control_center_configuration_get_width( config ),
+ foobar_control_center_configuration_get_height( config ) );
+
+ FoobarScreenEdge position = foobar_control_center_configuration_get_position( config );
+ FoobarControlCenterAlignment alignment = foobar_control_center_configuration_get_alignment( config );
+ gboolean attach_left = ( position == FOOBAR_SCREEN_EDGE_LEFT );
+ gboolean attach_right = ( position == FOOBAR_SCREEN_EDGE_RIGHT );
+ gboolean attach_top = ( position == FOOBAR_SCREEN_EDGE_TOP );
+ gboolean attach_bottom = ( position == FOOBAR_SCREEN_EDGE_BOTTOM );
+ switch ( position )
+ {
+ case FOOBAR_SCREEN_EDGE_LEFT:
+ case FOOBAR_SCREEN_EDGE_RIGHT:
+ attach_top =
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_START ) ||
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL );
+ attach_bottom =
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_END ) ||
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL );
+ break;
+ case FOOBAR_SCREEN_EDGE_TOP:
+ case FOOBAR_SCREEN_EDGE_BOTTOM:
+ attach_left =
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_START ) ||
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL );
+ attach_right =
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_END ) ||
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_LEFT, attach_left );
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_RIGHT, attach_right );
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, attach_top );
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_BOTTOM, attach_bottom );
+
+ gint offset = foobar_control_center_configuration_get_offset( config );
+ gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_LEFT, attach_left ? offset : 0 );
+ gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_RIGHT, attach_right ? offset : 0 );
+ gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, attach_top ? offset : 0 );
+ gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_BOTTOM, attach_bottom ? offset : 0 );
+
+ // Configure the orientation.
+
+ FoobarOrientation orientation = foobar_control_center_configuration_get_orientation( config );
+ switch ( orientation )
+ {
+ case FOOBAR_ORIENTATION_HORIZONTAL:
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( self->layout ), GTK_ORIENTATION_HORIZONTAL );
+ break;
+ case FOOBAR_ORIENTATION_VERTICAL:
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( self->layout ), GTK_ORIENTATION_VERTICAL );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+
+ // Adjust spacing for the containers.
+
+ foobar_inset_container_set_inset_vertical( FOOBAR_INSET_CONTAINER( self->notification_container ), -self->spacing / 2 );
+ gtk_widget_set_margin_top( self->notification_container, self->padding );
+ gtk_widget_set_margin_bottom( self->notification_container, self->padding );
+
+ gtk_widget_set_margin_start( self->notification_placeholder, self->padding );
+ gtk_widget_set_margin_end( self->notification_placeholder, self->padding );
+ gtk_widget_set_margin_top( self->notification_placeholder, self->padding );
+ gtk_widget_set_margin_bottom( self->notification_placeholder, self->padding );
+
+ gtk_box_set_spacing( GTK_BOX( self->control_container ), self->spacing );
+ gtk_widget_set_margin_top( self->control_container, self->padding );
+ gtk_widget_set_margin_bottom( self->control_container, self->padding );
+
+ // Re-create list items by resetting the factory.
+
+ GtkListItemFactory* factory = gtk_list_view_get_factory( GTK_LIST_VIEW( self->notification_list ) );
+ g_object_ref( factory );
+ gtk_list_view_set_factory( GTK_LIST_VIEW( self->notification_list ), NULL );
+ gtk_list_view_set_factory( GTK_LIST_VIEW( self->notification_list ), factory );
+ g_object_unref( factory );
+
+ // Re-create control rows.
+
+ GtkWidget* row_widget;
+ while ( ( row_widget = gtk_widget_get_first_child( self->control_container ) ) )
+ {
+ gtk_widget_unparent( row_widget );
+ }
+
+ gsize rows_count;
+ FoobarControlCenterRow const* rows = foobar_control_center_configuration_get_rows( config, &rows_count );
+ for ( gsize i = 0; i < rows_count; ++i )
+ {
+ // Add rows. If there are details for a row, add them into another box that is then added to the
+ // control_container to circumvent the spacing of the box.
+
+ switch ( rows[i] )
+ {
+ case FOOBAR_CONTROL_CENTER_ROW_CONNECTIVITY:
+ {
+ // Create the buttons for network and bluetooth.
+
+ GtkWidget* network_button = foobar_control_button_new( );
+
+ GtkWidget* bluetooth_button = foobar_control_button_new( );
+ foobar_control_button_set_icon_name( FOOBAR_CONTROL_BUTTON( bluetooth_button ), "fluent-bluetooth-symbolic" );
+ foobar_control_button_set_label( FOOBAR_CONTROL_BUTTON( bluetooth_button ), "Bluetooth" );
+
+ GtkWidget* button_container = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, self->spacing );
+ gtk_box_set_homogeneous( GTK_BOX( button_container ), TRUE );
+ gtk_widget_set_margin_start( button_container, self->padding );
+ gtk_widget_set_margin_end( button_container, self->padding );
+ gtk_box_append( GTK_BOX( button_container ), network_button );
+ gtk_box_append( GTK_BOX( button_container ), bluetooth_button );
+
+ GtkWidget* row = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_box_append( GTK_BOX( row ), button_container );
+
+ // Show either a button for managing Wi-Fi or a static element for ethernet.
+
+ FoobarNetworkAdapterWifi* wifi_adapter = foobar_network_service_get_wifi( self->network_service );
+ if ( wifi_adapter )
+ {
+ foobar_control_button_set_icon_name( FOOBAR_CONTROL_BUTTON( network_button ), "fluent-wifi-1-symbolic" );
+ foobar_control_button_set_label( FOOBAR_CONTROL_BUTTON( network_button ), "Wi-Fi" );
+ foobar_control_button_set_can_expand( FOOBAR_CONTROL_BUTTON( network_button ), TRUE );
+ foobar_control_button_set_can_toggle( FOOBAR_CONTROL_BUTTON( network_button ), TRUE );
+ g_object_bind_property(
+ wifi_adapter,
+ "is-enabled",
+ network_button,
+ "is-toggled",
+ G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL );
+ g_object_bind_property(
+ network_button,
+ "is-toggled",
+ network_button,
+ "can-expand",
+ G_BINDING_SYNC_CREATE );
+ g_object_bind_property(
+ network_button,
+ "is-expanded",
+ wifi_adapter,
+ "is-scanning",
+ G_BINDING_SYNC_CREATE );
+
+ GtkListItemFactory* wifi_item_factory = gtk_signal_list_item_factory_new( );
+ g_signal_connect(
+ wifi_item_factory,
+ "setup",
+ G_CALLBACK( foobar_control_center_handle_network_setup ),
+ self );
+
+ GListModel* wifi_source_model = g_object_ref( foobar_network_adapter_wifi_get_networks( wifi_adapter ) );
+ GtkNoSelection* wifi_selection_model = gtk_no_selection_new( wifi_source_model );
+
+ GtkWidget* wifi_list_view = gtk_list_view_new( GTK_SELECTION_MODEL( wifi_selection_model ), wifi_item_factory );
+
+ GtkWidget* wifi_details = foobar_control_details_new( );
+ foobar_control_details_set_inset_top( FOOBAR_CONTROL_DETAILS( wifi_details ), self->spacing );
+ foobar_control_details_set_child( FOOBAR_CONTROL_DETAILS( wifi_details ), wifi_list_view );
+ g_object_bind_property(
+ network_button,
+ "is-expanded",
+ wifi_details,
+ "is-expanded",
+ G_BINDING_SYNC_CREATE );
+
+ gtk_box_append( GTK_BOX( row ), wifi_details );
+ }
+ else
+ {
+ foobar_control_button_set_icon_name( FOOBAR_CONTROL_BUTTON( network_button ), "fluent-virtual-network-symbolic" );
+ foobar_control_button_set_label( FOOBAR_CONTROL_BUTTON( network_button ), "Ethernet" );
+ foobar_control_button_set_can_expand( FOOBAR_CONTROL_BUTTON( network_button ), FALSE );
+ foobar_control_button_set_can_toggle( FOOBAR_CONTROL_BUTTON( network_button ), FALSE );
+ }
+
+ // Enable interactions for the bluetooth button if available.
+
+ g_object_bind_property(
+ self->bluetooth_service,
+ "is-available",
+ bluetooth_button,
+ "can-toggle",
+ G_BINDING_SYNC_CREATE );
+ g_object_bind_property(
+ self->bluetooth_service,
+ "is-enabled",
+ bluetooth_button,
+ "is-toggled",
+ G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL );
+ g_object_bind_property(
+ bluetooth_button,
+ "is-toggled",
+ bluetooth_button,
+ "can-expand",
+ G_BINDING_SYNC_CREATE );
+ g_object_bind_property(
+ bluetooth_button,
+ "is-expanded",
+ self->bluetooth_service,
+ "is-scanning",
+ G_BINDING_SYNC_CREATE );
+
+ GtkListItemFactory* bluetooth_item_factory = gtk_signal_list_item_factory_new( );
+ g_signal_connect(
+ bluetooth_item_factory,
+ "setup",
+ G_CALLBACK( foobar_control_center_handle_bluetooth_device_setup ),
+ self );
+
+ GListModel* bluetooth_source_model = g_object_ref( foobar_bluetooth_service_get_devices( self->bluetooth_service ) );
+ GtkNoSelection* bluetooth_selection_model = gtk_no_selection_new( bluetooth_source_model );
+
+ GtkWidget* bluetooth_list_view = gtk_list_view_new(
+ GTK_SELECTION_MODEL( bluetooth_selection_model ),
+ bluetooth_item_factory );
+ gtk_list_view_set_single_click_activate( GTK_LIST_VIEW( bluetooth_list_view ), TRUE );
+ g_signal_connect(
+ bluetooth_list_view,
+ "activate",
+ G_CALLBACK( foobar_control_center_handle_bluetooth_device_activate ),
+ NULL );
+
+ GtkWidget* bluetooth_details = foobar_control_details_new( );
+ foobar_control_details_set_inset_top( FOOBAR_CONTROL_DETAILS( bluetooth_details ), self->spacing );
+ foobar_control_details_set_child( FOOBAR_CONTROL_DETAILS( bluetooth_details ), bluetooth_list_view );
+ g_object_bind_property(
+ bluetooth_button,
+ "is-expanded",
+ bluetooth_details,
+ "is-expanded",
+ G_BINDING_SYNC_CREATE );
+
+ gtk_box_append( GTK_BOX( row ), bluetooth_details );
+
+ gtk_box_append( GTK_BOX( self->control_container ), row );
+ break;
+ }
+ case FOOBAR_CONTROL_CENTER_ROW_AUDIO_OUTPUT:
+ case FOOBAR_CONTROL_CENTER_ROW_AUDIO_INPUT:
+ {
+ FoobarAudioDevice* default_device;
+ GListModel* device_list;
+ gchar const* icon_name;
+ gchar const* label;
+ if ( rows[i] == FOOBAR_CONTROL_CENTER_ROW_AUDIO_OUTPUT )
+ {
+ default_device = foobar_audio_service_get_default_output( self->audio_service );
+ device_list = foobar_audio_service_get_outputs( self->audio_service );
+ icon_name = "fluent-speaker-2-symbolic";
+ label = "Audio Output";
+ }
+ else
+ {
+ default_device = foobar_audio_service_get_default_input( self->audio_service );
+ device_list = foobar_audio_service_get_inputs( self->audio_service );
+ icon_name = "fluent-microphone-symbolic";
+ label = "Audio Input";
+ }
+
+ // Create the slider for controlling the volume.
+
+ GtkWidget* slider = foobar_control_slider_new( );
+ gtk_widget_set_margin_start( slider, self->padding );
+ gtk_widget_set_margin_end( slider, self->padding );
+ foobar_control_slider_set_icon_name( FOOBAR_CONTROL_SLIDER( slider ), icon_name );
+ foobar_control_slider_set_label( FOOBAR_CONTROL_SLIDER( slider ), label );
+ foobar_control_slider_set_can_expand( FOOBAR_CONTROL_SLIDER( slider ), TRUE );
+ g_object_bind_property(
+ default_device,
+ "volume",
+ slider,
+ "percentage",
+ G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL );
+
+ // Create the details view for selecting the audio device.
+
+ GtkListItemFactory* item_factory = gtk_signal_list_item_factory_new( );
+ g_signal_connect( item_factory, "setup", G_CALLBACK( foobar_control_center_handle_audio_device_setup ), self );
+
+ GListModel* source_model = g_object_ref( device_list );
+ GtkNoSelection* selection_model = gtk_no_selection_new( source_model );
+
+ GtkWidget* list_view = gtk_list_view_new( GTK_SELECTION_MODEL( selection_model ), item_factory );
+ gtk_list_view_set_single_click_activate( GTK_LIST_VIEW( list_view ), TRUE );
+ g_signal_connect(
+ list_view,
+ "activate",
+ G_CALLBACK( foobar_control_center_handle_audio_device_activate ),
+ NULL );
+
+ GtkWidget* details = foobar_control_details_new( );
+ foobar_control_details_set_inset_top( FOOBAR_CONTROL_DETAILS( details ), self->spacing );
+ foobar_control_details_set_child( FOOBAR_CONTROL_DETAILS( details ), list_view );
+ g_object_bind_property(
+ slider,
+ "is-expanded",
+ details,
+ "is-expanded",
+ G_BINDING_SYNC_CREATE );
+
+ GtkWidget* row = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_box_append( GTK_BOX( row ), slider );
+ gtk_box_append( GTK_BOX( row ), details );
+
+ gtk_box_append( GTK_BOX( self->control_container ), row );
+ break;
+ }
+ case FOOBAR_CONTROL_CENTER_ROW_BRIGHTNESS:
+ {
+ // Create the slider for adjusting the brightness.
+
+ GtkWidget* slider = foobar_control_slider_new( );
+ gtk_widget_set_margin_start( slider, self->padding );
+ gtk_widget_set_margin_end( slider, self->padding );
+ foobar_control_slider_set_icon_name( FOOBAR_CONTROL_SLIDER( slider ), "fluent-brightness-high-symbolic" );
+ foobar_control_slider_set_label( FOOBAR_CONTROL_SLIDER( slider ), "Brightness" );
+ g_object_bind_property(
+ self->brightness_service,
+ "percentage",
+ slider,
+ "percentage",
+ G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL );
+
+ gtk_box_append( GTK_BOX( self->control_container ), slider );
+ break;
+ }
+ default:
+ {
+ g_warn_if_reached( );
+ break;
+ }
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called by the notification list view to create a widget for displaying a notification.
+//
+void foobar_control_center_handle_notification_setup(
+ GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata )
+{
+ (void)factory;
+ FoobarControlCenter* self = (FoobarControlCenter*)userdata;
+
+ GtkWidget* widget = foobar_notification_widget_new( );
+ foobar_notification_widget_set_close_action( FOOBAR_NOTIFICATION_WIDGET( widget ), FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE );
+ foobar_notification_widget_set_time_format( FOOBAR_NOTIFICATION_WIDGET( widget ), self->notification_time_format );
+ foobar_notification_widget_set_min_height( FOOBAR_NOTIFICATION_WIDGET( widget ), self->notification_min_height );
+ foobar_notification_widget_set_close_button_inset( FOOBAR_NOTIFICATION_WIDGET( widget ), self->notification_close_button_inset );
+ foobar_notification_widget_set_inset_start( FOOBAR_NOTIFICATION_WIDGET( widget ), self->padding );
+ foobar_notification_widget_set_inset_end( FOOBAR_NOTIFICATION_WIDGET( widget ), self->padding );
+ foobar_notification_widget_set_inset_top( FOOBAR_NOTIFICATION_WIDGET( widget ), self->spacing / 2 );
+ foobar_notification_widget_set_inset_bottom( FOOBAR_NOTIFICATION_WIDGET( widget ), self->spacing / 2 );
+ gtk_list_item_set_child( list_item, widget );
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ gtk_expression_bind( item_expr, widget, "notification", list_item );
+ }
+}
+
+//
+// Called by the wi-fi details list view to create a widget for displaying a network.
+//
+void foobar_control_center_handle_network_setup(
+ GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata )
+{
+ (void)factory;
+ FoobarControlCenter* self = (FoobarControlCenter*)userdata;
+
+ GtkWidget* item = foobar_control_details_item_new( );
+ gtk_widget_set_margin_start( item, self->padding );
+ gtk_widget_set_margin_end( item, self->padding );
+ gtk_list_item_set_child( list_item, item );
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* label_expr = gtk_property_expression_new( FOOBAR_TYPE_NETWORK, item_expr, "name" );
+ gtk_expression_bind( label_expr, item, "label", list_item );
+ }
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* checked_expr = gtk_property_expression_new( FOOBAR_TYPE_NETWORK, item_expr, "is-active" );
+ gtk_expression_bind( checked_expr, item, "is-checked", list_item );
+ }
+}
+
+//
+// Called by the bluetooth details list view to create a widget for displaying a device.
+//
+void foobar_control_center_handle_bluetooth_device_setup(
+ GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata )
+{
+ (void)factory;
+ FoobarControlCenter* self = (FoobarControlCenter*)userdata;
+
+ GtkWidget* item = foobar_control_details_item_new( );
+ gtk_widget_set_margin_start( item, self->padding );
+ gtk_widget_set_margin_end( item, self->padding );
+ gtk_list_item_set_child( list_item, item );
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* label_expr = gtk_property_expression_new( FOOBAR_TYPE_BLUETOOTH_DEVICE, item_expr, "name" );
+ gtk_expression_bind( label_expr, item, "label", list_item );
+ }
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* state_expr = gtk_property_expression_new( FOOBAR_TYPE_BLUETOOTH_DEVICE, item_expr, "state" );
+ GtkExpression* accessory_params[] = { state_expr };
+ GtkExpression* accessory_expr = gtk_cclosure_expression_new(
+ FOOBAR_TYPE_CONTROL_DETAILS_ACCESSORY,
+ NULL,
+ G_N_ELEMENTS( accessory_params ),
+ accessory_params,
+ G_CALLBACK( foobar_control_center_compute_bluetooth_device_accessory ),
+ NULL,
+ NULL );
+ gtk_expression_bind( accessory_expr, item, "accessory", list_item );
+ }
+}
+
+//
+// Called by the bluetooth details list view when a device was selected.
+//
+void foobar_control_center_handle_bluetooth_device_activate(
+ GtkListView* view,
+ guint position,
+ gpointer userdata )
+{
+ (void)view;
+ (void)userdata;
+
+ GListModel* device_list = G_LIST_MODEL( gtk_list_view_get_model( view ) );
+ FoobarBluetoothDevice* device = g_list_model_get_item( device_list, position );
+ foobar_bluetooth_device_toggle_connection( device );
+}
+
+//
+// Called by the audio details list view to create a widget for displaying a device.
+//
+void foobar_control_center_handle_audio_device_setup(
+ GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata )
+{
+ (void)factory;
+ FoobarControlCenter* self = (FoobarControlCenter*)userdata;
+
+ GtkWidget* item = foobar_control_details_item_new( );
+ gtk_widget_set_margin_start( item, self->padding );
+ gtk_widget_set_margin_end( item, self->padding );
+ gtk_list_item_set_child( list_item, item );
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* label_expr = gtk_property_expression_new( FOOBAR_TYPE_AUDIO_DEVICE, item_expr, "description" );
+ gtk_expression_bind( label_expr, item, "label", list_item );
+ }
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* checked_expr = gtk_property_expression_new( FOOBAR_TYPE_AUDIO_DEVICE, item_expr, "is-default" );
+ gtk_expression_bind( checked_expr, item, "is-checked", list_item );
+ }
+}
+
+//
+// Called by the audio details list view when a device was selected.
+//
+void foobar_control_center_handle_audio_device_activate(
+ GtkListView* view,
+ guint position,
+ gpointer userdata )
+{
+ (void)view;
+ (void)userdata;
+
+ GListModel* device_list = G_LIST_MODEL( gtk_list_view_get_model( view ) );
+ FoobarAudioDevice* device = g_list_model_get_item( device_list, position );
+ foobar_audio_device_make_default( device );
+}
+
+//
+// Signal handler called when the global configuration file has changed.
+//
+void foobar_control_center_handle_config_change(
+ GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)object;
+ (void)pspec;
+ FoobarControlCenter* self = (FoobarControlCenter*)userdata;
+
+ FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service );
+ foobar_control_center_apply_configuration(
+ self,
+ foobar_configuration_get_control_center( config ),
+ foobar_configuration_get_notifications( config ) );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Value Converters
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Derive the item accessory from a bluetooth device's connection state.
+//
+FoobarControlDetailsAccessory foobar_control_center_compute_bluetooth_device_accessory(
+ GtkExpression* expression,
+ FoobarBluetoothDeviceState state,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ switch ( state )
+ {
+ case FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED:
+ return FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE;
+ case FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING:
+ return FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS;
+ case FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED:
+ return FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED;
+ default:
+ g_warn_if_reached( );
+ return FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE;
+ }
+}
+
+//
+// Derive the visible child in the "notifications" stack from the number of notifications.
+//
+// If there are no notifications, a placeholder is shown.
+//
+gchar* foobar_control_center_compute_visible_notification_child_name(
+ GtkExpression* expression,
+ guint count,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ return count > 0 ? g_strdup( STACK_ITEM_LIST ) : g_strdup( STACK_ITEM_PLACEHOLDER );
+} \ No newline at end of file
diff --git a/src/control-center.h b/src/control-center.h
new file mode 100644
index 0000000..cbdc0ae
--- /dev/null
+++ b/src/control-center.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <gtk/gtk.h>
+#include "services/brightness-service.h"
+#include "services/audio-service.h"
+#include "services/network-service.h"
+#include "services/bluetooth-service.h"
+#include "services/notification-service.h"
+#include "services/configuration-service.h"
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_CONTROL_CENTER foobar_control_center_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarControlCenter, foobar_control_center, FOOBAR, CONTROL_CENTER, GtkWindow )
+
+FoobarControlCenter* foobar_control_center_new ( FoobarBrightnessService* brightness_service,
+ FoobarAudioService* audio_service,
+ FoobarNetworkService* network_service,
+ FoobarBluetoothService* bluetooth_service,
+ FoobarNotificationService* notification_service,
+ FoobarConfigurationService* configuration_service );
+void foobar_control_center_apply_configuration( FoobarControlCenter* self,
+ FoobarControlCenterConfiguration const* config,
+ FoobarNotificationConfiguration const* notification_config );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/dbus/com.github.hannesschulze.foobar.xml b/src/dbus/com.github.hannesschulze.foobar.xml
new file mode 100644
index 0000000..e8373a6
--- /dev/null
+++ b/src/dbus/com.github.hannesschulze.foobar.xml
@@ -0,0 +1,8 @@
+<node>
+ <interface name="com.github.hannesschulze.foobar.Server">
+ <method name="Inspector"/>
+ <method name="Quit"/>
+ <method name="ToggleLauncher"/>
+ <method name="ToggleControlCenter"/>
+ </interface>
+</node>
diff --git a/src/dbus/meson.build b/src/dbus/meson.build
new file mode 100644
index 0000000..ec22def
--- /dev/null
+++ b/src/dbus/meson.build
@@ -0,0 +1,37 @@
+foobar_sources += gnome.gdbus_codegen(
+ 'server',
+ 'com.github.hannesschulze.foobar.xml',
+ namespace: 'Foobar',
+ annotations: [
+ ['com.github.hannesschulze.foobar.Server', 'org.gtk.GDBus.C.Name', 'Server'],
+ ],
+)
+
+foobar_sources += gnome.gdbus_codegen(
+ 'notifications',
+ 'org.freedesktop.Notifications.xml',
+ namespace: 'Foobar',
+ annotations: [
+ ['org.freedesktop.Notifications', 'org.gtk.GDBus.C.Name', 'Notifications'],
+ ],
+)
+
+foobar_sources += gnome.gdbus_codegen(
+ 'upower',
+ 'org.freedesktop.UPower.xml',
+ namespace: 'Foobar',
+ annotations: [
+ ['org.freedesktop.UPower.Device', 'org.gtk.GDBus.C.Name', 'UPowerDevice'],
+ ],
+)
+
+foobar_sources += gnome.gdbus_codegen(
+ 'bluez',
+ 'org.bluez.xml',
+ namespace: 'Foobar',
+ annotations: [
+ ['org.bluez.Adapter1', 'org.gtk.GDBus.C.Name', 'BluezAdapter'],
+ ['org.bluez.Device1', 'org.gtk.GDBus.C.Name', 'BluezDevice'],
+ ['org.bluez.AgentManager1', 'org.gtk.GDBus.C.Name', 'BluezAgentManager'],
+ ],
+) \ No newline at end of file
diff --git a/src/dbus/org.bluez.xml b/src/dbus/org.bluez.xml
new file mode 100644
index 0000000..669e325
--- /dev/null
+++ b/src/dbus/org.bluez.xml
@@ -0,0 +1,66 @@
+<node>
+ <interface name="org.bluez.Adapter1">
+ <method name="StartDiscovery"/>
+ <method name="StopDiscovery"/>
+ <method name="RemoveDevice">
+ <arg name="device" type="o" direction="in"/>
+ </method>
+ <method name="SetDiscoveryFilter">
+ <arg name="properties" type="a{sv}" direction="in"/>
+ </method>
+ <property name="Address" type="s" access="read"/>
+ <property name="Name" type="s" access="read"/>
+ <property name="Alias" type="s" access="readwrite"/>
+ <property name="Class" type="u" access="read"/>
+ <property name="Powered" type="b" access="readwrite"/>
+ <property name="PowerState" type="s" access="read"/>
+ <property name="Discoverable" type="b" access="readwrite"/>
+ <property name="DiscoverableTimeout" type="u" access="readwrite"/>
+ <property name="Pairable" type="b" access="readwrite"/>
+ <property name="PairableTimeout" type="u" access="readwrite"/>
+ <property name="Discovering" type="b" access="read"/>
+ <property name="UUIDs" type="as" access="read"/>
+ <property name="Modalias" type="s" access="read"/>
+ </interface>
+
+ <interface name="org.bluez.Device1">
+ <method name="Disconnect"/>
+ <method name="Connect"/>
+ <method name="ConnectProfile">
+ <arg name="UUID" type="s" direction="in"/>
+ </method>
+ <method name="DisconnectProfile">
+ <arg name="UUID" type="s" direction="in"/>
+ </method>
+ <method name="Pair"/>
+ <method name="CancelPairing"/>
+ <property name="Address" type="s" access="read"/>
+ <property name="Name" type="s" access="read"/>
+ <property name="Alias" type="s" access="readwrite"/>
+ <property name="Class" type="u" access="read"/>
+ <property name="Appearance" type="q" access="read"/>
+ <property name="Icon" type="s" access="read"/>
+ <property name="Paired" type="b" access="read"/>
+ <property name="Trusted" type="b" access="readwrite"/>
+ <property name="Blocked" type="b" access="readwrite"/>
+ <property name="LegacyPairing" type="b" access="read"/>
+ <property name="RSSI" type="n" access="read"/>
+ <property name="Connected" type="b" access="read"/>
+ <property name="UUIDs" type="as" access="read"/>
+ <property name="Modalias" type="s" access="read"/>
+ <property name="Adapter" type="o" access="read"/>
+ </interface>
+
+ <interface name="org.bluez.AgentManager1">
+ <method name="RegisterAgent">
+ <arg name="agent" type="o" direction="in"/>
+ <arg name="capability" type="s" direction="in"/>
+ </method>
+ <method name="UnregisterAgent">
+ <arg name="agent" type="o" direction="in"/>
+ </method>
+ <method name="RequestDefaultAgent">
+ <arg name="agent" type="o" direction="in"/>
+ </method>
+ </interface>
+</node>
diff --git a/src/dbus/org.freedesktop.Notifications.xml b/src/dbus/org.freedesktop.Notifications.xml
new file mode 100644
index 0000000..6b1d2f2
--- /dev/null
+++ b/src/dbus/org.freedesktop.Notifications.xml
@@ -0,0 +1,35 @@
+<node>
+ <interface name="org.freedesktop.Notifications">
+ <method name="Notify">
+ <arg type="s" direction="in"/>
+ <arg type="u" direction="in"/>
+ <arg type="s" direction="in"/>
+ <arg type="s" direction="in"/>
+ <arg type="s" direction="in"/>
+ <arg type="as" direction="in"/>
+ <arg type="a{sv}" direction="in"/>
+ <arg type="i" direction="in"/>
+ <arg type="u" direction="out"/>
+ </method>
+ <method name="CloseNotification">
+ <arg type="u" direction="in" name="id"/>
+ </method>
+ <method name="GetCapabilities">
+ <arg type="as" direction="out"/>
+ </method>
+ <method name="GetServerInformation">
+ <arg type="s" direction="out"/>
+ <arg type="s" direction="out"/>
+ <arg type="s" direction="out"/>
+ <arg type="s" direction="out"/>
+ </method>
+ <signal name="NotificationClosed">
+ <arg type="u"/>
+ <arg type="u"/>
+ </signal>
+ <signal name="ActionInvoked">
+ <arg type="u"/>
+ <arg type="s"/>
+ </signal>
+ </interface>
+</node>
diff --git a/src/dbus/org.freedesktop.UPower.xml b/src/dbus/org.freedesktop.UPower.xml
new file mode 100644
index 0000000..18fc41d
--- /dev/null
+++ b/src/dbus/org.freedesktop.UPower.xml
@@ -0,0 +1,12 @@
+<node>
+ <interface name="org.freedesktop.UPower.Device">
+ <property name="State" type="u" access="read"/>
+ <property name="Percentage" type="d" access="read"/>
+ <property name="IsPresent" type="b" access="read"/>
+ <property name="TimeToEmpty" type="x" access="read"/>
+ <property name="TimeToFull" type="x" access="read"/>
+ <property name="Energy" type="d" access="read"/>
+ <property name="EnergyFull" type="d" access="read"/>
+ <property name="EnergyRate" type="d" access="read"/>
+ </interface>
+</node>
diff --git a/src/launcher.c b/src/launcher.c
new file mode 100644
index 0000000..115c9b7
--- /dev/null
+++ b/src/launcher.c
@@ -0,0 +1,619 @@
+#include "launcher.h"
+#include "widgets/limit-container.h"
+#include <gtk4-layer-shell.h>
+#include <gdk/gdkkeysyms.h>
+#include <string.h>
+
+//
+// FoobarLauncher:
+//
+// A window allowing the user to quickly search for applications and launch them.
+//
+// Note that it should be possible to continue typing, even when the results list view is focused. Conversely, the arrow
+// keys can be used to select an item, even when the search input is focused.
+//
+
+struct _FoobarLauncher
+{
+ GtkWindow parent_instance;
+ gchar** search_terms;
+ GtkWidget* search_text;
+ GtkWidget* list_view;
+ GtkWidget* limit_container;
+ GtkFilterListModel* filter_model;
+ GtkSingleSelection* selection_model;
+ FoobarApplicationService* application_service;
+ FoobarConfigurationService* configuration_service;
+ gulong config_handler_id;
+};
+
+static void foobar_launcher_class_init ( FoobarLauncherClass* klass );
+static void foobar_launcher_init ( FoobarLauncher* self );
+static void foobar_launcher_finalize ( GObject* object );
+static void foobar_launcher_handle_search_changed ( GtkEditable* editable,
+ gpointer userdata );
+static void foobar_launcher_handle_search_activate ( GtkText* text,
+ gpointer userdata );
+static gboolean foobar_launcher_handle_search_key ( GtkEventControllerKey* controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ gpointer userdata );
+static gboolean foobar_launcher_handle_list_key ( GtkEventControllerKey* controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ gpointer userdata );
+static gboolean foobar_launcher_handle_window_key ( GtkEventControllerKey* controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ gpointer userdata );
+static void foobar_launcher_handle_item_setup ( GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata );
+static void foobar_launcher_handle_item_activate ( GtkListView* view,
+ guint position,
+ gpointer userdata );
+static void foobar_launcher_handle_config_change ( GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata );
+static void foobar_launcher_handle_show ( GtkWidget* widget,
+ gpointer userdata );
+static gboolean foobar_launcher_compute_icon_visible ( GtkExpression* expression,
+ GIcon* icon,
+ gpointer userdata );
+static gboolean foobar_launcher_compute_label_visible ( GtkExpression* expression,
+ gchar const* label,
+ gpointer userdata );
+static gboolean foobar_launcher_compute_separator_visible( GtkExpression* expression,
+ guint item_count,
+ gpointer userdata );
+static gboolean foobar_launcher_filter_func ( gpointer item,
+ gpointer userdata );
+static gboolean foobar_launcher_is_navigation_key ( guint keyval );
+
+G_DEFINE_FINAL_TYPE( FoobarLauncher, foobar_launcher, GTK_TYPE_WINDOW )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Window Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the launcher.
+//
+void foobar_launcher_class_init( FoobarLauncherClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->finalize = foobar_launcher_finalize;
+}
+
+//
+// Instance initialization for the launcher.
+//
+void foobar_launcher_init( FoobarLauncher* self )
+{
+ self->search_terms = g_new0( gchar*, 1 );
+
+ // Set up the search input and an event controller for auto-switching focus to the result list.
+
+ GtkEventController* search_controller = gtk_event_controller_key_new( );
+ g_signal_connect( search_controller, "key-pressed", G_CALLBACK( foobar_launcher_handle_search_key ), self );
+ g_signal_connect( search_controller, "key-released", G_CALLBACK( foobar_launcher_handle_search_key ), self );
+
+ self->search_text = gtk_text_new( );
+ gtk_text_set_placeholder_text( GTK_TEXT( self->search_text ), "Search…" );
+ gtk_widget_add_controller( self->search_text, search_controller );
+ gtk_widget_set_hexpand( self->search_text, TRUE );
+ g_signal_connect( self->search_text, "changed", G_CALLBACK( foobar_launcher_handle_search_changed ), self );
+ g_signal_connect( self->search_text, "activate", G_CALLBACK( foobar_launcher_handle_search_activate ), self );
+
+ GtkWidget* search_icon = gtk_image_new_from_icon_name( "fluent-search-symbolic" );
+
+ GtkWidget* search = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_box_append( GTK_BOX( search ), search_icon );
+ gtk_box_append( GTK_BOX( search ), self->search_text );
+ gtk_widget_add_css_class( search, "search" );
+
+ // Set up the separator (only shown if the results are not empty).
+
+ GtkWidget *separator = gtk_separator_new( GTK_ORIENTATION_HORIZONTAL );
+
+ {
+ GtkExpression* list_expr = gtk_constant_expression_new( GTK_TYPE_FILTER_LIST_MODEL, self->filter_model );
+ GtkExpression* count_expr = gtk_property_expression_new( GTK_TYPE_FILTER_LIST_MODEL, list_expr, "n-items" );
+ GtkExpression* visible_params[] = { count_expr };
+ GtkExpression* visible_expr = gtk_cclosure_expression_new(
+ G_TYPE_BOOLEAN,
+ NULL,
+ G_N_ELEMENTS( visible_params ),
+ visible_params,
+ G_CALLBACK( foobar_launcher_compute_separator_visible ),
+ NULL,
+ NULL );
+ gtk_expression_bind( visible_expr, separator, "visible", NULL );
+ }
+
+ // Set up the results list view and an event controller for auto-switching focus to the input.
+
+ GtkListItemFactory* item_factory = gtk_signal_list_item_factory_new( );
+ g_signal_connect( item_factory, "setup", G_CALLBACK( foobar_launcher_handle_item_setup ), NULL );
+
+ GtkCustomFilter* filter = gtk_custom_filter_new( foobar_launcher_filter_func, self, NULL );
+
+ self->filter_model = gtk_filter_list_model_new( NULL, GTK_FILTER( filter ) );
+
+ self->selection_model = gtk_single_selection_new( G_LIST_MODEL( g_object_ref( self->filter_model ) ) );
+
+ GtkEventController* list_controller = gtk_event_controller_key_new( );
+ g_signal_connect( list_controller, "key-pressed", G_CALLBACK( foobar_launcher_handle_list_key ), self );
+ g_signal_connect( list_controller, "key-released", G_CALLBACK( foobar_launcher_handle_list_key ), self );
+
+ self->list_view = gtk_list_view_new( GTK_SELECTION_MODEL( g_object_ref( self->selection_model ) ), item_factory );
+ gtk_list_view_set_single_click_activate( GTK_LIST_VIEW( self->list_view ), TRUE );
+ gtk_scrollable_set_vscroll_policy( GTK_SCROLLABLE( self->list_view ), GTK_SCROLL_MINIMUM );
+ gtk_widget_add_controller( self->list_view, list_controller );
+ g_signal_connect( self->list_view, "activate", G_CALLBACK( foobar_launcher_handle_item_activate ), self );
+
+ GtkWidget* scrolled_window = gtk_scrolled_window_new( );
+ // XXX: Use GTK_POLICY_AUTOMATIC instead of GTK_POLICY_EXTERNAL once I figured out how to remove the minimum
+ // height in this case. For now, no scrollbar is shown.
+ gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scrolled_window ), GTK_POLICY_NEVER, GTK_POLICY_EXTERNAL );
+ gtk_scrolled_window_set_child( GTK_SCROLLED_WINDOW( scrolled_window ), self->list_view );
+ gtk_scrolled_window_set_propagate_natural_height( GTK_SCROLLED_WINDOW( scrolled_window ), TRUE );
+ gtk_widget_set_vexpand( scrolled_window, TRUE );
+
+ // Set up the layout.
+
+ GtkWidget* layout = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_box_append( GTK_BOX( layout ), search );
+ gtk_box_append( GTK_BOX( layout ), separator );
+ gtk_box_append( GTK_BOX( layout ), scrolled_window );
+
+ // Let the launcher take up space to accommodate the result list's natural height until the configured maximum
+ // height.
+
+ self->limit_container = foobar_limit_container_new( );
+ foobar_limit_container_set_child( FOOBAR_LIMIT_CONTAINER( self->limit_container ), layout );
+
+ // Set up the window, listening for the "Escape" key.
+
+ GtkEventController* window_controller = gtk_event_controller_key_new( );
+ g_signal_connect( window_controller, "key-released", G_CALLBACK( foobar_launcher_handle_window_key ), self );
+
+ gtk_window_set_child( GTK_WINDOW( self ), self->limit_container );
+ gtk_widget_add_controller( GTK_WIDGET( self ), window_controller );
+ gtk_window_set_title( GTK_WINDOW( self ), "Foobar Launcher" );
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "launcher" );
+ gtk_layer_init_for_window( GTK_WINDOW( self ) );
+ gtk_layer_set_keyboard_mode( GTK_WINDOW( self ), GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE );
+ gtk_layer_set_namespace( GTK_WINDOW( self ), "foobar-launcher" );
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, TRUE );
+ g_signal_connect( self, "show", G_CALLBACK( foobar_launcher_handle_show ), self );
+}
+
+//
+// Instance cleanup for the launcher.
+//
+void foobar_launcher_finalize( GObject* object )
+{
+ FoobarLauncher* self = (FoobarLauncher*)object;
+
+ g_clear_signal_handler( &self->config_handler_id, self->configuration_service );
+ g_clear_object( &self->filter_model );
+ g_clear_object( &self->selection_model );
+ g_clear_object( &self->application_service );
+ g_clear_object( &self->configuration_service );
+ g_clear_pointer( &self->search_terms, g_strfreev );
+
+ G_OBJECT_CLASS( foobar_launcher_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new launcher instance.
+//
+FoobarLauncher* foobar_launcher_new(
+ FoobarApplicationService* application_service,
+ FoobarConfigurationService* configuration_service )
+{
+ g_return_val_if_fail( FOOBAR_IS_APPLICATION_SERVICE( application_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( configuration_service ), NULL );
+
+ FoobarLauncher* self = g_object_new( FOOBAR_TYPE_LAUNCHER, NULL );
+ self->application_service = g_object_ref( application_service );
+ self->configuration_service = g_object_ref( configuration_service );
+
+ // Set up the result list view's source model.
+
+ GListModel* source_model = foobar_application_service_get_items( self->application_service );
+ gtk_filter_list_model_set_model( self->filter_model, g_object_ref( source_model ) );
+
+ // Apply the configuration and subscribe to changes.
+
+ FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service );
+ foobar_launcher_apply_configuration( self, foobar_configuration_get_launcher( config ) );
+ self->config_handler_id = g_signal_connect(
+ self->configuration_service,
+ "notify::current",
+ G_CALLBACK( foobar_launcher_handle_config_change ),
+ self );
+
+ return self;
+}
+
+//
+// Apply the launcher configuration provided by the configuration service.
+//
+void foobar_launcher_apply_configuration(
+ FoobarLauncher* self,
+ FoobarLauncherConfiguration const* config )
+{
+ g_return_if_fail( FOOBAR_IS_LAUNCHER( self ) );
+ g_return_if_fail( config != NULL );
+
+ gtk_window_set_default_size( GTK_WINDOW( self ), foobar_launcher_configuration_get_width( config ), 2 );
+ gtk_layer_set_margin(
+ GTK_WINDOW( self ),
+ GTK_LAYER_SHELL_EDGE_TOP,
+ foobar_launcher_configuration_get_position( config ) );
+ foobar_limit_container_set_max_height(
+ FOOBAR_LIMIT_CONTAINER( self->limit_container ),
+ foobar_launcher_configuration_get_max_height( config ) );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called when the user confirms the selection while the search input is focused.
+//
+// We forward the activation to the results list view.
+//
+void foobar_launcher_handle_search_activate(
+ GtkText* text,
+ gpointer userdata )
+{
+ (void)text;
+ FoobarLauncher* self = (FoobarLauncher*)userdata;
+
+ guint selection = gtk_single_selection_get_selected( self->selection_model );
+ if ( selection != GTK_INVALID_LIST_POSITION )
+ {
+ gtk_widget_activate_action( self->list_view, "list.activate-item", "u", selection );
+ }
+}
+
+//
+// Called when the search query has changed.
+//
+// We update the tokenized search terms and then the result filter.
+//
+void foobar_launcher_handle_search_changed(
+ GtkEditable* editable,
+ gpointer userdata )
+{
+ FoobarLauncher* self = (FoobarLauncher*)userdata;
+
+ GStrvBuilder* terms_builder = g_strv_builder_new( );
+ g_autofree gchar* query = g_strdup( gtk_editable_get_text( editable ) );
+ gchar* query_start = query;
+ gchar* save;
+ gchar* token;
+ while ( ( token = strtok_r( query_start, " \t", &save ) ) )
+ {
+ if ( *token ) { g_strv_builder_add( terms_builder, token ); }
+ query_start = NULL;
+ }
+
+ g_clear_pointer( &self->search_terms, g_strfreev );
+ self->search_terms = g_strv_builder_end( terms_builder );
+
+ GtkFilter* filter = gtk_filter_list_model_get_filter( self->filter_model );
+ gtk_filter_changed( filter, GTK_FILTER_CHANGE_DIFFERENT );
+}
+
+//
+// Handle keyboard events while the search input is focused.
+//
+// If this is a navigational key, the event is forwarded to the results list view.
+//
+gboolean foobar_launcher_handle_search_key(
+ GtkEventControllerKey* controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ gpointer userdata )
+{
+ (void)keycode;
+ (void)state;
+ FoobarLauncher* self = (FoobarLauncher*)userdata;
+
+ if ( foobar_launcher_is_navigation_key( keyval ) )
+ {
+ gtk_widget_grab_focus( self->list_view );
+ return gtk_event_controller_key_forward( controller, self->list_view );
+ }
+ else
+ {
+ return FALSE;
+ }
+}
+
+//
+// Handle keyboard events while the list view is focused.
+//
+// If this is not a navigational key, the event is forwarded to the search input, which is also focused.
+//
+gboolean foobar_launcher_handle_list_key(
+ GtkEventControllerKey* controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ gpointer userdata )
+{
+ (void)keycode;
+ (void)state;
+ FoobarLauncher* self = (FoobarLauncher*)userdata;
+
+ if ( foobar_launcher_is_navigation_key( keyval ) )
+ {
+ return FALSE;
+ }
+ else
+ {
+ gtk_text_grab_focus_without_selecting( GTK_TEXT( self->search_text ) );
+ return gtk_event_controller_key_forward( controller, self->search_text );
+ }
+}
+
+//
+// Handle general keyboard events for the window.
+//
+// This is used to allow the user to close the launcher by pressing the "Escape" key.
+//
+gboolean foobar_launcher_handle_window_key(
+ GtkEventControllerKey* controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ gpointer userdata )
+{
+ (void)controller;
+ (void)keycode;
+ (void)state;
+ FoobarLauncher* self = (FoobarLauncher*)userdata;
+
+ if ( keyval == GDK_KEY_Escape )
+ {
+ gtk_widget_set_visible( GTK_WIDGET( self ), FALSE );
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+}
+
+//
+// Called by the result list view to create a widget for displaying an application item.
+//
+void foobar_launcher_handle_item_setup(
+ GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata )
+{
+ (void)factory;
+ (void)userdata;
+
+ // Set up layout.
+
+ GtkWidget* icon = gtk_image_new( );
+ gtk_widget_set_valign( icon, GTK_ALIGN_CENTER );
+ gtk_widget_add_css_class( icon, "icon" );
+
+ GtkWidget* name = gtk_label_new( NULL );
+ gtk_label_set_justify( GTK_LABEL( name ), GTK_JUSTIFY_LEFT );
+ gtk_label_set_xalign( GTK_LABEL( name ), 0 );
+ gtk_label_set_wrap( GTK_LABEL( name ), FALSE );
+ gtk_label_set_ellipsize( GTK_LABEL( name ), PANGO_ELLIPSIZE_END );
+ gtk_widget_add_css_class( name, "name" );
+
+ GtkWidget* description = gtk_label_new( NULL );
+ gtk_label_set_justify( GTK_LABEL( description ), GTK_JUSTIFY_LEFT );
+ gtk_label_set_xalign( GTK_LABEL( description ), 0 );
+ gtk_label_set_wrap( GTK_LABEL( name ), FALSE );
+ gtk_label_set_ellipsize( GTK_LABEL( description ), PANGO_ELLIPSIZE_END );
+ gtk_widget_add_css_class( description, "description" );
+
+ GtkWidget* column = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_widget_set_hexpand( column, TRUE );
+ gtk_widget_set_valign( column, GTK_ALIGN_CENTER );
+ gtk_box_append( GTK_BOX( column ), name );
+ gtk_box_append( GTK_BOX( column ), description );
+
+ GtkWidget* row = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_widget_add_css_class( row, "item" );
+ gtk_box_append( GTK_BOX( row ), icon );
+ gtk_box_append( GTK_BOX( row ), column );
+
+ gtk_list_item_set_child( list_item, row );
+
+ // Set up bindings.
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* icon_expr = gtk_property_expression_new( FOOBAR_TYPE_APPLICATION_ITEM, item_expr, "icon" );
+ gtk_expression_bind( gtk_expression_ref( icon_expr ), icon, "gicon", list_item );
+ GtkExpression* visible_params[] = { icon_expr };
+ GtkExpression* visible_expr = gtk_cclosure_expression_new(
+ G_TYPE_BOOLEAN,
+ NULL,
+ G_N_ELEMENTS( visible_params ),
+ visible_params,
+ G_CALLBACK( foobar_launcher_compute_icon_visible ),
+ NULL,
+ NULL );
+ gtk_expression_bind( visible_expr, icon, "visible", list_item );
+ }
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* name_expr = gtk_property_expression_new( FOOBAR_TYPE_APPLICATION_ITEM, item_expr, "name" );
+ gtk_expression_bind( name_expr, name, "label", list_item );
+ }
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* description_expr = gtk_property_expression_new( FOOBAR_TYPE_APPLICATION_ITEM, item_expr, "description" );
+ gtk_expression_bind( gtk_expression_ref( description_expr ), description, "label", list_item );
+ GtkExpression* visible_params[] = { description_expr };
+ GtkExpression* visible_expr = gtk_cclosure_expression_new(
+ G_TYPE_BOOLEAN,
+ NULL,
+ G_N_ELEMENTS( visible_params ),
+ visible_params,
+ G_CALLBACK( foobar_launcher_compute_label_visible ),
+ NULL,
+ NULL );
+ gtk_expression_bind( visible_expr, description, "visible", list_item );
+ }
+}
+
+//
+// Called by the result list view when an application was selected.
+//
+void foobar_launcher_handle_item_activate(
+ GtkListView* view,
+ guint position,
+ gpointer userdata )
+{
+ (void)view;
+ FoobarLauncher* self = (FoobarLauncher*)userdata;
+
+ GListModel* list = G_LIST_MODEL( gtk_list_view_get_model( view ) );
+ FoobarApplicationItem* item = g_list_model_get_item( list, position );
+ foobar_application_item_launch( item );
+ gtk_widget_set_visible( GTK_WIDGET( self ), FALSE );
+}
+
+//
+// Signal handler called when the global configuration file has changed.
+//
+void foobar_launcher_handle_config_change(
+ GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)object;
+ (void)pspec;
+ FoobarLauncher* self = (FoobarLauncher*)userdata;
+
+ FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service );
+ foobar_launcher_apply_configuration( self, foobar_configuration_get_launcher( config ) );
+}
+
+//
+// Called before the launcher is presented.
+//
+// This is used to reset the UI state.
+//
+void foobar_launcher_handle_show(
+ GtkWidget* widget,
+ gpointer userdata )
+{
+ (void)widget;
+ FoobarLauncher* self = (FoobarLauncher*)userdata;
+
+ gtk_editable_set_text( GTK_EDITABLE( self->search_text ), "" );
+ if ( g_list_model_get_n_items( G_LIST_MODEL( self->selection_model ) ) > 0 )
+ {
+ gtk_list_view_scroll_to(
+ GTK_LIST_VIEW( self->list_view ),
+ 0,
+ GTK_LIST_SCROLL_FOCUS | GTK_LIST_SCROLL_SELECT,
+ NULL );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Value Converters
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Derive the visibility of an application icon from its value.
+//
+gboolean foobar_launcher_compute_icon_visible(
+ GtkExpression* expression,
+ GIcon* icon,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ return icon != NULL;
+}
+
+//
+// Derive the visibility of an application label from its value.
+//
+gboolean foobar_launcher_compute_label_visible(
+ GtkExpression* expression,
+ gchar const* label,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ return label != NULL;
+}
+
+//
+// Derive the visibility of the separator between search input and result list view from the number of results.
+//
+gboolean foobar_launcher_compute_separator_visible(
+ GtkExpression* expression,
+ guint item_count,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ return item_count > 0;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Match a result item against the current search query.
+//
+gboolean foobar_launcher_filter_func( gpointer item, gpointer userdata )
+{
+ FoobarLauncher* self = (FoobarLauncher*)userdata;
+ FoobarApplicationItem* app = (FoobarApplicationItem*)item;
+
+ return foobar_application_item_match( app, (gchar const* const*)self->search_terms );
+}
+
+//
+// Check if a key value is that of a navigation key (i.e., an arrow key).
+//
+gboolean foobar_launcher_is_navigation_key( guint keyval )
+{
+ switch ( keyval )
+ {
+ case GDK_KEY_Up:
+ case GDK_KEY_Down:
+ case GDK_KEY_Left:
+ case GDK_KEY_Right:
+ return TRUE;
+ default:
+ return FALSE;
+ }
+} \ No newline at end of file
diff --git a/src/launcher.h b/src/launcher.h
new file mode 100644
index 0000000..343afd4
--- /dev/null
+++ b/src/launcher.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <gtk/gtk.h>
+#include "services/application-service.h"
+#include "services/configuration-service.h"
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_LAUNCHER foobar_launcher_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarLauncher, foobar_launcher, FOOBAR, LAUNCHER, GtkWindow )
+
+FoobarLauncher* foobar_launcher_new ( FoobarApplicationService* application_service,
+ FoobarConfigurationService* configuration_service );
+void foobar_launcher_apply_configuration( FoobarLauncher* self,
+ FoobarLauncherConfiguration const* config );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..90e629d
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,12 @@
+#include "application.h"
+
+//
+// Entry point of the application, runs the GTK app.
+//
+int main(
+ int argc,
+ char** argv )
+{
+ g_autoptr( FoobarApplication ) app = foobar_application_new( );
+ return g_application_run( G_APPLICATION( app ), argc, argv );
+} \ No newline at end of file
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..b089bce
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,41 @@
+foobar_sources = files(
+ 'main.c',
+ 'application.c',
+ 'utils.c',
+ 'panel.c',
+ 'launcher.c',
+ 'control-center.c',
+ 'notification-area.c',
+)
+
+subdir('dbus')
+subdir('services')
+subdir('widgets')
+
+executable(
+ meson.project_name(),
+ styles_resource,
+ icons_resource,
+ foobar_sources,
+ include_directories: include_directories('.'),
+ dependencies: [
+ glib_dep,
+ gio_dep,
+ gio_unix_dep,
+ gtk_dep,
+ json_dep,
+ nm_dep,
+ m_dep,
+ layershell_dep,
+ gvc_dep,
+ ],
+ install: true,
+ c_args: [
+ '-Wall',
+ '-Werror',
+ '-Wswitch-enum',
+ '-Wswitch-default',
+ '-Wunused',
+ '-Wuninitialized',
+ ],
+)
diff --git a/src/notification-area.c b/src/notification-area.c
new file mode 100644
index 0000000..e8cdba4
--- /dev/null
+++ b/src/notification-area.c
@@ -0,0 +1,196 @@
+#include "notification-area.h"
+#include "widgets/notification-widget.h"
+#include <gtk4-layer-shell.h>
+
+//
+// FoobarNotificationArea:
+//
+// A (usually invisible) window in the corner of the screen, displaying incoming notifications before they are either
+// dismissed or a timeout has passed.
+//
+
+struct _FoobarNotificationArea
+{
+ GtkWindow parent_instance;
+ GtkWidget* notification_list;
+ FoobarNotificationService* notification_service;
+ FoobarConfigurationService* configuration_service;
+ gint min_height;
+ gint spacing;
+ gint close_button_inset;
+ gchar* time_format;
+ gulong config_handler_id;
+};
+
+static void foobar_notification_area_class_init ( FoobarNotificationAreaClass* klass );
+static void foobar_notification_area_init ( FoobarNotificationArea* self );
+static void foobar_notification_area_finalize ( GObject* object );
+static void foobar_notification_area_handle_config_change( GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata );
+static void foobar_notification_area_handle_item_setup ( GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarNotificationArea, foobar_notification_area, GTK_TYPE_WINDOW )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Window Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the notification area.
+//
+void foobar_notification_area_class_init( FoobarNotificationAreaClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->finalize = foobar_notification_area_finalize;
+}
+
+//
+// Instance initialization for the notification area.
+//
+void foobar_notification_area_init( FoobarNotificationArea* self )
+{
+ GtkListItemFactory* notification_factory = gtk_signal_list_item_factory_new( );
+ g_signal_connect( notification_factory, "setup", G_CALLBACK( foobar_notification_area_handle_item_setup ), self );
+ self->notification_list = gtk_list_view_new( NULL, notification_factory );
+
+ gtk_window_set_child( GTK_WINDOW( self ), self->notification_list );
+ gtk_window_set_title( GTK_WINDOW( self ), "Foobar Notification Area" );
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "notification-area" );
+ gtk_layer_init_for_window( GTK_WINDOW( self ) );
+ gtk_layer_set_namespace( GTK_WINDOW( self ), "foobar-notification-area" );
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_RIGHT, TRUE );
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, TRUE );
+}
+
+//
+// Instance cleanup for the notification area.
+//
+void foobar_notification_area_finalize( GObject* object )
+{
+ FoobarNotificationArea* self = (FoobarNotificationArea*)object;
+
+ g_clear_signal_handler( &self->config_handler_id, self->configuration_service );
+ g_clear_object( &self->notification_service );
+ g_clear_object( &self->configuration_service );
+ g_clear_pointer( &self->time_format, g_free );
+
+ G_OBJECT_CLASS( foobar_notification_area_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new notification area instance.
+//
+FoobarNotificationArea* foobar_notification_area_new(
+ FoobarNotificationService* notification_service,
+ FoobarConfigurationService* configuration_service )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( notification_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( configuration_service ), NULL );
+
+ FoobarNotificationArea* self = g_object_new( FOOBAR_TYPE_NOTIFICATION_AREA, NULL );
+ self->notification_service = g_object_ref( notification_service );
+ self->configuration_service = g_object_ref( configuration_service );
+
+ // Apply the configuration and subscribe to changes.
+
+ FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service );
+ foobar_notification_area_apply_configuration( self, foobar_configuration_get_notifications( config ) );
+ self->config_handler_id = g_signal_connect(
+ self->configuration_service,
+ "notify::current",
+ G_CALLBACK( foobar_notification_area_handle_config_change ),
+ self );
+
+ // Set up the notifications list view.
+
+ GListModel* source_model = foobar_notification_service_get_popup_notifications( self->notification_service );
+ GtkNoSelection* selection_model = gtk_no_selection_new( g_object_ref( source_model ) );
+ gtk_list_view_set_model( GTK_LIST_VIEW( self->notification_list ), GTK_SELECTION_MODEL( selection_model ) );
+
+ return self;
+}
+
+//
+// Apply the notification configuration provided by the configuration service.
+//
+void foobar_notification_area_apply_configuration(
+ FoobarNotificationArea* self,
+ FoobarNotificationConfiguration const* config )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_AREA( self ) );
+ g_return_if_fail( config != NULL );
+
+ // Copy configuration into member variables.
+
+ gint width = foobar_notification_configuration_get_width( config );
+ self->min_height = foobar_notification_configuration_get_min_height( config );
+ self->spacing = foobar_notification_configuration_get_spacing( config );
+ self->close_button_inset = foobar_notification_configuration_get_close_button_inset( config );
+ g_clear_pointer( &self->time_format, g_free );
+ self->time_format = g_strdup( foobar_notification_configuration_get_time_format( config ) );
+
+ // Recreate list items by resetting the factory.
+
+ GtkListItemFactory* factory = gtk_list_view_get_factory( GTK_LIST_VIEW( self->notification_list ) );
+ g_object_ref( factory );
+ gtk_list_view_set_factory( GTK_LIST_VIEW( self->notification_list ), NULL );
+ gtk_list_view_set_factory( GTK_LIST_VIEW( self->notification_list ), factory );
+ g_object_unref( factory );
+
+ // We need to use height 2, otherwise it crashes for some reason.
+
+ gtk_window_set_default_size( GTK_WINDOW( self ), width + self->spacing, 2 );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Signal handler called when the global configuration file has changed.
+//
+void foobar_notification_area_handle_config_change(
+ GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)object;
+ (void)pspec;
+ FoobarNotificationArea* self = (FoobarNotificationArea*)userdata;
+
+ FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service );
+ foobar_notification_area_apply_configuration( self, foobar_configuration_get_notifications( config ) );
+}
+
+//
+// Called by the notification list view to create a widget for displaying a notification.
+//
+void foobar_notification_area_handle_item_setup(
+ GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata )
+{
+ (void)factory;
+ FoobarNotificationArea* self = (FoobarNotificationArea*)userdata;
+
+ GtkWidget* widget = foobar_notification_widget_new( );
+ foobar_notification_widget_set_close_action( FOOBAR_NOTIFICATION_WIDGET( widget ), FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS );
+ foobar_notification_widget_set_time_format( FOOBAR_NOTIFICATION_WIDGET( widget ), self->time_format );
+ foobar_notification_widget_set_min_height( FOOBAR_NOTIFICATION_WIDGET( widget ), self->min_height );
+ foobar_notification_widget_set_close_button_inset( FOOBAR_NOTIFICATION_WIDGET( widget ), self->close_button_inset );
+ foobar_notification_widget_set_inset_end( FOOBAR_NOTIFICATION_WIDGET( widget ), self->spacing );
+ foobar_notification_widget_set_inset_top( FOOBAR_NOTIFICATION_WIDGET( widget ), self->spacing );
+ gtk_list_item_set_child( list_item, widget );
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ gtk_expression_bind( item_expr, widget, "notification", list_item );
+ }
+} \ No newline at end of file
diff --git a/src/notification-area.h b/src/notification-area.h
new file mode 100644
index 0000000..aad2f3f
--- /dev/null
+++ b/src/notification-area.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <gtk/gtk.h>
+#include "services/notification-service.h"
+#include "services/configuration-service.h"
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_NOTIFICATION_AREA foobar_notification_area_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarNotificationArea, foobar_notification_area, FOOBAR, NOTIFICATION_AREA, GtkWindow )
+
+FoobarNotificationArea* foobar_notification_area_new ( FoobarNotificationService* notification_service,
+ FoobarConfigurationService* configuration_service );
+void foobar_notification_area_apply_configuration( FoobarNotificationArea* self,
+ FoobarNotificationConfiguration const* config );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/panel.c b/src/panel.c
new file mode 100644
index 0000000..4833b72
--- /dev/null
+++ b/src/panel.c
@@ -0,0 +1,348 @@
+#include "panel.h"
+#include "widgets/panel/panel-item-icon.h"
+#include "widgets/panel/panel-item-clock.h"
+#include "widgets/panel/panel-item-workspaces.h"
+#include "widgets/panel/panel-item-status.h"
+#include <gtk4-layer-shell.h>
+
+//
+// FoobarPanel:
+//
+// A panel that is divided into start, center and end sections. Each section can be configured to present a range of
+// items.
+//
+
+struct _FoobarPanel
+{
+ GtkWindow parent_instance;
+ GtkWidget* main_layout;
+ GtkWidget* start_items;
+ GtkWidget* center_items;
+ GtkWidget* end_items;
+ GPtrArray* item_widgets;
+ GdkMonitor* monitor;
+ FoobarBatteryService* battery_service;
+ FoobarClockService* clock_service;
+ FoobarBrightnessService* brightness_service;
+ FoobarWorkspaceService* workspace_service;
+ FoobarAudioService* audio_service;
+ FoobarNetworkService* network_service;
+ FoobarBluetoothService* bluetooth_service;
+ FoobarNotificationService* notification_service;
+ FoobarConfigurationService* configuration_service;
+ gulong config_handler_id;
+};
+
+static void foobar_panel_class_init ( FoobarPanelClass* klass );
+static void foobar_panel_init ( FoobarPanel* self );
+static void foobar_panel_finalize ( GObject* object );
+static void foobar_panel_handle_config_change( GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarPanel, foobar_panel, GTK_TYPE_APPLICATION_WINDOW )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Window Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the panel.
+//
+void foobar_panel_class_init( FoobarPanelClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->finalize = foobar_panel_finalize;
+}
+
+//
+// Instance initialization for the panel.
+//
+void foobar_panel_init( FoobarPanel* self )
+{
+ self->start_items = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ self->center_items = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ self->end_items = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+
+ self->main_layout = gtk_center_box_new( );
+ gtk_center_box_set_start_widget( GTK_CENTER_BOX( self->main_layout ), self->start_items );
+ gtk_center_box_set_center_widget( GTK_CENTER_BOX( self->main_layout ), self->center_items );
+ gtk_center_box_set_end_widget( GTK_CENTER_BOX( self->main_layout ), self->end_items );
+
+ gtk_window_set_child( GTK_WINDOW( self ), self->main_layout );
+ gtk_window_set_title( GTK_WINDOW( self ), "Foobar Panel" );
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "panel" );
+ gtk_layer_init_for_window( GTK_WINDOW( self ) );
+ gtk_layer_set_namespace( GTK_WINDOW( self ), "foobar-panel" );
+ gtk_layer_auto_exclusive_zone_enable( GTK_WINDOW( self ) );
+}
+
+//
+// Instance cleanup for the panel.
+//
+void foobar_panel_finalize( GObject* object )
+{
+ FoobarPanel* self = (FoobarPanel*)object;
+
+ g_clear_signal_handler( &self->config_handler_id, self->configuration_service );
+ g_clear_object( &self->monitor );
+ g_clear_object( &self->battery_service );
+ g_clear_object( &self->clock_service );
+ g_clear_object( &self->brightness_service );
+ g_clear_object( &self->workspace_service );
+ g_clear_object( &self->audio_service );
+ g_clear_object( &self->network_service );
+ g_clear_object( &self->bluetooth_service );
+ g_clear_object( &self->notification_service );
+ g_clear_object( &self->configuration_service );
+ g_clear_pointer( &self->item_widgets, g_ptr_array_unref );
+
+ G_OBJECT_CLASS( foobar_panel_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new panel instance.
+//
+// An optional GdkMonitor instance can be provided for multi-monitor setups. Otherwise, the default monitor is used.
+//
+FoobarPanel* foobar_panel_new(
+ GtkApplication* app,
+ GdkMonitor* monitor,
+ FoobarBatteryService* battery_service,
+ FoobarClockService* clock_service,
+ FoobarBrightnessService* brightness_service,
+ FoobarWorkspaceService* workspace_service,
+ FoobarAudioService* audio_service,
+ FoobarNetworkService* network_service,
+ FoobarBluetoothService* bluetooth_service,
+ FoobarNotificationService* notification_service,
+ FoobarConfigurationService* configuration_service )
+{
+ g_return_val_if_fail( GTK_IS_APPLICATION( app ), NULL );
+ g_return_val_if_fail( monitor == NULL || GDK_IS_MONITOR( monitor ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_BATTERY_SERVICE( battery_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_CLOCK_SERVICE( clock_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_BRIGHTNESS_SERVICE( brightness_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_WORKSPACE_SERVICE( workspace_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( audio_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_NETWORK_SERVICE( network_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( bluetooth_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( notification_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( configuration_service ), NULL );
+
+ FoobarPanel* self = g_object_new( FOOBAR_TYPE_PANEL, "application", app, NULL );
+ self->battery_service = g_object_ref( battery_service );
+ self->clock_service = g_object_ref( clock_service );
+ self->brightness_service = g_object_ref( brightness_service );
+ self->workspace_service = g_object_ref( workspace_service );
+ self->audio_service = g_object_ref( audio_service );
+ self->network_service = g_object_ref( network_service );
+ self->bluetooth_service = g_object_ref( bluetooth_service );
+ self->notification_service = g_object_ref( notification_service );
+ self->configuration_service = g_object_ref( configuration_service );
+
+ // Set the monitor.
+
+ if ( monitor )
+ {
+ self->monitor = g_object_ref( monitor );
+ gtk_layer_set_monitor( GTK_WINDOW( self ), self->monitor );
+ }
+
+ // Apply the configuration and subscribe to changes.
+
+ FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service );
+ foobar_panel_apply_configuration( self, foobar_configuration_get_panel( config ) );
+ self->config_handler_id = g_signal_connect(
+ self->configuration_service,
+ "notify::current",
+ G_CALLBACK( foobar_panel_handle_config_change ),
+ self );
+
+ return self;
+}
+
+//
+// Apply the panel configuration provided by the configuration service.
+//
+void foobar_panel_apply_configuration(
+ FoobarPanel* self,
+ FoobarPanelConfiguration const* config )
+{
+ g_return_if_fail( FOOBAR_IS_PANEL( self ) );
+ g_return_if_fail( config != NULL );
+
+ // Derive the orientation from the panel's position.
+
+ FoobarScreenEdge position = foobar_panel_configuration_get_position( config );
+ GtkOrientation orientation;
+ gchar const* orientation_css_class;
+ switch ( position )
+ {
+ case FOOBAR_SCREEN_EDGE_LEFT:
+ case FOOBAR_SCREEN_EDGE_RIGHT:
+ orientation = GTK_ORIENTATION_VERTICAL;
+ orientation_css_class = "vertical";
+ break;
+ case FOOBAR_SCREEN_EDGE_TOP:
+ case FOOBAR_SCREEN_EDGE_BOTTOM:
+ orientation = GTK_ORIENTATION_HORIZONTAL;
+ orientation_css_class = "horizontal";
+ break;
+ default:
+ g_warn_if_reached( );
+ return;
+ }
+
+ // Configure the window.
+
+ gtk_widget_remove_css_class( GTK_WIDGET( self ), "horizontal" );
+ gtk_widget_remove_css_class( GTK_WIDGET( self ), "vertical" );
+ gtk_widget_add_css_class( GTK_WIDGET( self ), orientation_css_class );
+
+ gint size = foobar_panel_configuration_get_size( config );
+ gtk_window_set_default_size(
+ GTK_WINDOW( self ),
+ orientation == GTK_ORIENTATION_VERTICAL ? size : -1,
+ orientation == GTK_ORIENTATION_HORIZONTAL ? size : -1 );
+
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_LEFT, position != FOOBAR_SCREEN_EDGE_RIGHT );
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_RIGHT, position != FOOBAR_SCREEN_EDGE_LEFT );
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, position != FOOBAR_SCREEN_EDGE_BOTTOM );
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_BOTTOM, position != FOOBAR_SCREEN_EDGE_TOP );
+
+ // Configure layout spacing.
+
+ gint margin = foobar_panel_configuration_get_margin( config );
+ gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_LEFT, position != FOOBAR_SCREEN_EDGE_RIGHT ? margin : 0 );
+ gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_RIGHT, position != FOOBAR_SCREEN_EDGE_LEFT ? margin : 0 );
+ gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, position != FOOBAR_SCREEN_EDGE_BOTTOM ? margin : 0 );
+ gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_BOTTOM, position != FOOBAR_SCREEN_EDGE_TOP ? margin : 0 );
+
+ gint padding = foobar_panel_configuration_get_padding( config );
+ gtk_widget_set_margin_start( GTK_WIDGET( self->main_layout ), orientation == GTK_ORIENTATION_HORIZONTAL ? padding : 0 );
+ gtk_widget_set_margin_end( GTK_WIDGET( self->main_layout ), orientation == GTK_ORIENTATION_HORIZONTAL ? padding : 0 );
+ gtk_widget_set_margin_top( GTK_WIDGET( self->main_layout ), orientation == GTK_ORIENTATION_VERTICAL ? padding : 0 );
+ gtk_widget_set_margin_bottom( GTK_WIDGET( self->main_layout ), orientation == GTK_ORIENTATION_VERTICAL ? padding : 0 );
+
+ gint spacing = foobar_panel_configuration_get_spacing( config );
+ gtk_box_set_spacing( GTK_BOX(self->start_items), spacing );
+ gtk_box_set_spacing( GTK_BOX(self->center_items), spacing );
+ gtk_box_set_spacing( GTK_BOX(self->end_items), spacing );
+ gtk_widget_set_margin_top( GTK_WIDGET( self->center_items ), orientation == GTK_ORIENTATION_VERTICAL ? spacing : 0 );
+ gtk_widget_set_margin_bottom( GTK_WIDGET( self->center_items ), orientation == GTK_ORIENTATION_VERTICAL ? spacing : 0 );
+ gtk_widget_set_margin_start( GTK_WIDGET( self->center_items ), orientation == GTK_ORIENTATION_HORIZONTAL ? spacing : 0 );
+ gtk_widget_set_margin_end( GTK_WIDGET( self->center_items ), orientation == GTK_ORIENTATION_HORIZONTAL ? spacing : 0 );
+
+ // Configure the container orientations.
+
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( self->main_layout ), orientation );
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( self->start_items ), orientation );
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( self->center_items ), orientation );
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( self->end_items ), orientation );
+
+ // Re-create panel items.
+
+ if ( self->item_widgets )
+ {
+ for ( guint i = 0; i < self->item_widgets->len; ++i )
+ {
+ gtk_widget_unparent( g_ptr_array_index( self->item_widgets, i ) );
+ }
+ g_clear_pointer( &self->item_widgets, g_ptr_array_unref );
+ }
+
+ gsize items_count;
+ FoobarPanelItemConfiguration const* const* items = foobar_panel_configuration_get_items( config, &items_count );
+ self->item_widgets = g_ptr_array_sized_new( items_count );
+ for ( gsize i = 0; i < items_count; ++i )
+ {
+ // Create an item for the specified type and configuration.
+
+ FoobarPanelItemConfiguration const* item = items[i];
+ FoobarPanelItem* item_widget;
+ switch ( foobar_panel_item_configuration_get_kind( item ) )
+ {
+ case FOOBAR_PANEL_ITEM_KIND_ICON:
+ item_widget = foobar_panel_item_icon_new( item );
+ break;
+ case FOOBAR_PANEL_ITEM_KIND_CLOCK:
+ item_widget = foobar_panel_item_clock_new(
+ item,
+ self->clock_service );
+ break;
+ case FOOBAR_PANEL_ITEM_KIND_WORKSPACES:
+ item_widget = foobar_panel_item_workspaces_new(
+ item,
+ self->monitor,
+ self->workspace_service );
+ break;
+ case FOOBAR_PANEL_ITEM_KIND_STATUS:
+ item_widget = foobar_panel_item_status_new(
+ item,
+ self->battery_service,
+ self->brightness_service,
+ self->audio_service,
+ self->network_service,
+ self->bluetooth_service,
+ self->notification_service );
+ break;
+ default:
+ g_warn_if_reached( );
+ continue;
+ }
+
+ // Propagate the panel's orientation if the item supports it.
+
+ if ( GTK_IS_ORIENTABLE( item_widget ) )
+ {
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( item_widget ), orientation );
+ }
+
+ // Add the item to the correct container, depending on its configured position.
+
+ switch ( foobar_panel_item_configuration_get_position( item ) )
+ {
+ case FOOBAR_PANEL_ITEM_POSITION_START:
+ gtk_box_append( GTK_BOX( self->start_items ), GTK_WIDGET( item_widget ) );
+ break;
+ case FOOBAR_PANEL_ITEM_POSITION_CENTER:
+ gtk_box_append( GTK_BOX( self->center_items ), GTK_WIDGET( item_widget ) );
+ break;
+ case FOOBAR_PANEL_ITEM_POSITION_END:
+ gtk_box_append( GTK_BOX( self->end_items ), GTK_WIDGET( item_widget ) );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+
+ // Add the item to the pointer array for easier cleanup.
+
+ g_ptr_array_add( self->item_widgets, GTK_WIDGET( item_widget ) );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Signal handler called when the global configuration file has changed.
+//
+void foobar_panel_handle_config_change(
+ GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)object;
+ (void)pspec;
+ FoobarPanel* self = (FoobarPanel*)userdata;
+
+ FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service );
+ foobar_panel_apply_configuration( self, foobar_configuration_get_panel( config ) );
+} \ No newline at end of file
diff --git a/src/panel.h b/src/panel.h
new file mode 100644
index 0000000..46d7299
--- /dev/null
+++ b/src/panel.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <gtk/gtk.h>
+#include "services/configuration-service.h"
+#include "services/battery-service.h"
+#include "services/clock-service.h"
+#include "services/brightness-service.h"
+#include "services/workspace-service.h"
+#include "services/audio-service.h"
+#include "services/network-service.h"
+#include "services/bluetooth-service.h"
+#include "services/notification-service.h"
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_PANEL foobar_panel_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarPanel, foobar_panel, FOOBAR, PANEL, GtkApplicationWindow )
+
+FoobarPanel* foobar_panel_new ( GtkApplication* app,
+ GdkMonitor* monitor,
+ FoobarBatteryService* battery_service,
+ FoobarClockService* clock_service,
+ FoobarBrightnessService* brightness_service,
+ FoobarWorkspaceService* workspace_service,
+ FoobarAudioService* audio_service,
+ FoobarNetworkService* network_service,
+ FoobarBluetoothService* bluetooth_service,
+ FoobarNotificationService* notification_service,
+ FoobarConfigurationService* configuration_service );
+void foobar_panel_apply_configuration( FoobarPanel* self,
+ FoobarPanelConfiguration const* config );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/services/application-service.c b/src/services/application-service.c
new file mode 100644
index 0000000..4c81a27
--- /dev/null
+++ b/src/services/application-service.c
@@ -0,0 +1,733 @@
+#include "services/application-service.h"
+#include "utils.h"
+#include <gtk/gtk.h>
+#include <gio/gdesktopappinfo.h>
+#include <json-glib/json-glib.h>
+#include <string.h>
+
+//
+// FoobarApplicationItem:
+//
+// Represents a single application/desktop file.
+//
+
+struct _FoobarApplicationItem
+{
+ GObject parent_instance;
+ FoobarApplicationService* service;
+ GAppInfo* info;
+};
+
+enum
+{
+ APP_PROP_ID = 1,
+ APP_PROP_NAME,
+ APP_PROP_DESCRIPTION,
+ APP_PROP_EXECUTABLE,
+ APP_PROP_CATEGORIES,
+ APP_PROP_ICON,
+ APP_PROP_FREQUENCY,
+ N_APP_PROPS,
+};
+
+static GParamSpec* app_props[N_APP_PROPS] = { 0 };
+
+static void foobar_application_item_class_init ( FoobarApplicationItemClass* klass );
+static void foobar_application_item_init ( FoobarApplicationItem* self );
+static void foobar_application_item_get_property( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_application_item_finalize ( GObject* object );
+static FoobarApplicationItem* foobar_application_item_new ( FoobarApplicationService* service );
+static void foobar_application_item_set_info ( FoobarApplicationItem* self,
+ GAppInfo* value );
+
+G_DEFINE_FINAL_TYPE( FoobarApplicationItem, foobar_application_item, G_TYPE_OBJECT )
+
+//
+// FoobarApplicationService:
+//
+// Service providing a list of all installed applications. This implemented using GLib's AppInfo API.
+//
+
+struct _FoobarApplicationService
+{
+ GObject parent_instance;
+ GListStore* items;
+ GtkSortListModel* sorted_items;
+ GAppInfoMonitor* monitor;
+ GHashTable* frequencies; // only modified on the main thread, but possibly read on a background thread
+ GMutex frequencies_mutex;
+ GMutex write_cache_mutex;
+ gchar* cache_path;
+ gulong changed_handler_id;
+};
+
+enum
+{
+ PROP_ITEMS = 1,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_application_service_class_init ( FoobarApplicationServiceClass* klass );
+static void foobar_application_service_init ( FoobarApplicationService* self );
+static void foobar_application_service_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_application_service_finalize ( GObject* object );
+static void foobar_application_service_handle_changed ( GAppInfoMonitor* monitor,
+ gpointer userdata );
+static void foobar_application_service_update ( FoobarApplicationService* self );
+static void foobar_application_service_read_cache ( FoobarApplicationService* self );
+static void foobar_application_service_read_cache_foreach_cb ( JsonObject* object,
+ gchar const* member_name,
+ JsonNode* member_node,
+ gpointer userdata );
+static void foobar_application_service_write_cache ( FoobarApplicationService* self );
+static void foobar_application_service_write_cache_cb ( GObject* object,
+ GAsyncResult* result,
+ gpointer userdata );
+static void foobar_application_service_write_cache_async ( FoobarApplicationService* self,
+ GCancellable* cancellable,
+ GAsyncReadyCallback callback,
+ gpointer userdata );
+static gboolean foobar_application_service_write_cache_finish ( FoobarApplicationService* self,
+ GAsyncResult* result,
+ GError** error );
+static void foobar_application_service_write_cache_thread ( GTask* task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable* cancellable );
+static void foobar_application_service_write_cache_foreach_cb( gpointer key,
+ gpointer value,
+ gpointer userdata );
+static gint foobar_application_service_sort_func ( gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarApplicationService, foobar_application_service, G_TYPE_OBJECT )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Item Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for application items.
+//
+void foobar_application_item_class_init( FoobarApplicationItemClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_application_item_get_property;
+ object_klass->finalize = foobar_application_item_finalize;
+
+ app_props[APP_PROP_ID] = g_param_spec_string(
+ "id",
+ "ID",
+ "Application identifier string.",
+ NULL,
+ G_PARAM_READABLE );
+ app_props[APP_PROP_NAME] = g_param_spec_string(
+ "name",
+ "Name",
+ "Displayed name for the application.",
+ NULL,
+ G_PARAM_READABLE );
+ app_props[APP_PROP_DESCRIPTION] = g_param_spec_string(
+ "description",
+ "Description",
+ "Brief description of the application.",
+ NULL,
+ G_PARAM_READABLE );
+ app_props[APP_PROP_EXECUTABLE] = g_param_spec_string(
+ "executable",
+ "Executable",
+ "The executable name of the application.",
+ NULL,
+ G_PARAM_READABLE );
+ app_props[APP_PROP_CATEGORIES] = g_param_spec_string(
+ "categories",
+ "Categories",
+ "Semicolon-separated list of application categories.",
+ NULL,
+ G_PARAM_READABLE );
+ app_props[APP_PROP_ICON] = g_param_spec_object(
+ "icon",
+ "Icon",
+ "Icon to show for the application.",
+ G_TYPE_ICON,
+ G_PARAM_READABLE );
+ app_props[APP_PROP_FREQUENCY] = g_param_spec_int64(
+ "frequency",
+ "Frequency",
+ "The number of times the application was opened.",
+ 0,
+ INT64_MAX,
+ 0,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_APP_PROPS, app_props );
+}
+
+//
+// Instance initialization for application items.
+//
+void foobar_application_item_init( FoobarApplicationItem* self )
+{
+ (void)self;
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_application_item_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarApplicationItem* self = (FoobarApplicationItem*)object;
+
+ switch ( prop_id )
+ {
+ case APP_PROP_ID:
+ g_value_set_string( value, foobar_application_item_get_id( self ) );
+ break;
+ case APP_PROP_NAME:
+ g_value_set_string( value, foobar_application_item_get_name( self ) );
+ break;
+ case APP_PROP_DESCRIPTION:
+ g_value_set_string( value, foobar_application_item_get_description( self ) );
+ break;
+ case APP_PROP_EXECUTABLE:
+ g_value_set_string( value, foobar_application_item_get_executable( self ) );
+ break;
+ case APP_PROP_CATEGORIES:
+ g_value_set_string( value, foobar_application_item_get_categories( self ) );
+ break;
+ case APP_PROP_ICON:
+ g_value_set_object( value, foobar_application_item_get_icon( self ) );
+ break;
+ case APP_PROP_FREQUENCY:
+ g_value_set_int64( value, foobar_application_item_get_frequency( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for application items.
+//
+void foobar_application_item_finalize( GObject* object )
+{
+ FoobarApplicationItem* self = (FoobarApplicationItem*)object;
+
+ g_clear_object( &self->info );
+
+ G_OBJECT_CLASS( foobar_application_item_parent_class )->finalize( object );
+}
+
+//
+// Create a new application item belonging to the parent service (captured as an unowned reference).
+//
+FoobarApplicationItem* foobar_application_item_new( FoobarApplicationService* service )
+{
+ FoobarApplicationItem* self = g_object_new( FOOBAR_TYPE_APPLICATION_ITEM, NULL );
+ self->service = service;
+ return self;
+}
+
+//
+// Get the application's unique textual identifier.
+//
+gchar const* foobar_application_item_get_id( FoobarApplicationItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL );
+ return self->info ? g_app_info_get_id( self->info ) : NULL;
+}
+
+//
+// Get the application's human-readable name.
+//
+gchar const* foobar_application_item_get_name( FoobarApplicationItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL );
+ return self->info ? g_app_info_get_display_name( self->info ) : NULL;
+}
+
+//
+// Get an optional short description for the application.
+//
+gchar const* foobar_application_item_get_description( FoobarApplicationItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL );
+ return self->info ? g_app_info_get_description( self->info ) : NULL;
+}
+
+//
+// Get the application's executable name.
+//
+gchar const* foobar_application_item_get_executable( FoobarApplicationItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL );
+ return self->info ? g_app_info_get_executable( self->info ) : NULL;
+}
+
+//
+// Get the semicolon-separated list of categories for this application.
+//
+gchar const* foobar_application_item_get_categories( FoobarApplicationItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL );
+ return G_IS_DESKTOP_APP_INFO( self->info )
+ ? g_desktop_app_info_get_categories( G_DESKTOP_APP_INFO( self->info ) )
+ : NULL;
+}
+
+//
+// Get an icon representing the application.
+//
+GIcon* foobar_application_item_get_icon( FoobarApplicationItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL );
+ return self->info ? g_app_info_get_icon( self->info ) : NULL;
+}
+
+//
+// Get the number of times this application was opened using the launcher.
+//
+gint64 foobar_application_item_get_frequency( FoobarApplicationItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), 0 );
+
+ if ( !self->service ) { return 0; }
+ gpointer value = g_hash_table_lookup( self->service->frequencies, foobar_application_item_get_id( self ) );
+ return GPOINTER_TO_SIZE( value );
+}
+
+//
+// Update the backing GAppInfo object for the application.
+//
+void foobar_application_item_set_info(
+ FoobarApplicationItem* self,
+ GAppInfo* value )
+{
+ g_return_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ) );
+
+ if ( self->info != value )
+ {
+ g_clear_object( &self->info );
+
+ if ( value ) { self->info = g_object_ref( value ); }
+
+ g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_ID] );
+ g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_NAME] );
+ g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_DESCRIPTION] );
+ g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_EXECUTABLE] );
+ g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_ICON] );
+ g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_FREQUENCY] );
+ }
+}
+
+//
+// Match the item against the given search terms.
+//
+gboolean foobar_application_item_match(
+ FoobarApplicationItem* self,
+ gchar const* const* terms )
+{
+ g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), FALSE );
+ g_return_val_if_fail( terms != NULL, FALSE );
+
+ for ( gchar const* const* it = terms; *it; ++it )
+ {
+ gchar const* term = *it;
+ gchar const* name = foobar_application_item_get_name( self );
+ gchar const* description = foobar_application_item_get_description( self );
+ gchar const* executable = foobar_application_item_get_executable( self );
+ gchar const* categories = foobar_application_item_get_categories( self );
+ gchar const* id = foobar_application_item_get_id( self );
+ if ( name && strcasestr( name, term ) != NULL ) { continue; }
+ if ( description && strcasestr( description, term ) != NULL ) { continue; }
+ if ( executable && strcasestr( executable, term ) != NULL ) { continue; }
+ if ( categories && strcasestr( categories, term ) != NULL ) { continue; }
+ if ( id && strcasestr( id, term ) != NULL ) { continue; }
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+//
+// Launch the application, increasing its frequency by one.
+//
+void foobar_application_item_launch( FoobarApplicationItem* self )
+{
+ g_return_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ) );
+
+ if ( !self->info ) { return; }
+
+ g_autoptr( GError ) error = NULL;
+ if ( !g_app_info_launch( self->info, NULL, NULL, &error ) )
+ {
+ g_warning( "Unable to launch application: %s", error->message );
+ }
+
+ gchar const* id = foobar_application_item_get_id( self );
+ if ( !self->service || !id ) { return; }
+
+ gpointer value = g_hash_table_lookup( self->service->frequencies, id );
+ value = GSIZE_TO_POINTER( GPOINTER_TO_SIZE( value ) + 1 );
+ g_mutex_lock( &self->service->frequencies_mutex );
+ g_hash_table_insert( self->service->frequencies, g_strdup( id ), value );
+ g_mutex_unlock( &self->service->frequencies_mutex );
+
+ g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_FREQUENCY] );
+ gtk_sorter_changed( gtk_sort_list_model_get_sorter( self->service->sorted_items ), GTK_SORTER_CHANGE_DIFFERENT );
+ foobar_application_service_write_cache( self->service );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Service Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the application service.
+//
+void foobar_application_service_class_init( FoobarApplicationServiceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_application_service_get_property;
+ object_klass->finalize = foobar_application_service_finalize;
+
+ props[PROP_ITEMS] = g_param_spec_object(
+ "items",
+ "Items",
+ "Application items, sorted by their frequency.",
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for the application service.
+//
+void foobar_application_service_init( FoobarApplicationService* self )
+{
+ self->items = g_list_store_new( FOOBAR_TYPE_APPLICATION_ITEM );
+
+ GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_application_service_sort_func, NULL, NULL );
+ self->sorted_items = gtk_sort_list_model_new( G_LIST_MODEL( g_object_ref( self->items ) ), GTK_SORTER( sorter ) );
+
+ self->cache_path = foobar_get_cache_path( "application-frequencies.json" );
+ self->frequencies = g_hash_table_new_full( g_str_hash, g_str_equal, g_free, NULL );
+ g_mutex_init( &self->frequencies_mutex );
+ g_mutex_init( &self->write_cache_mutex );
+
+ foobar_application_service_read_cache( self );
+ foobar_application_service_update( self );
+
+ self->monitor = g_app_info_monitor_get( );
+ self->changed_handler_id = g_signal_connect(
+ self->monitor,
+ "changed",
+ G_CALLBACK( foobar_application_service_handle_changed ),
+ self );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_application_service_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarApplicationService* self = (FoobarApplicationService*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_ITEMS:
+ g_value_set_object( value, foobar_application_service_get_items( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for the application service.
+//
+void foobar_application_service_finalize( GObject* object )
+{
+ FoobarApplicationService* self = (FoobarApplicationService*)object;
+
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->items ) ); ++i )
+ {
+ FoobarApplicationItem* item = g_list_model_get_item( G_LIST_MODEL( self->items ), i );
+ item->service = NULL;
+ g_object_notify_by_pspec( G_OBJECT( item ), app_props[APP_PROP_FREQUENCY] );
+ }
+ g_clear_signal_handler( &self->changed_handler_id, self->monitor );
+ g_clear_object( &self->sorted_items );
+ g_clear_object( &self->items );
+ g_clear_object( &self->monitor );
+ g_clear_pointer( &self->frequencies, g_hash_table_unref );
+ g_clear_pointer( &self->cache_path, g_free );
+
+ g_mutex_clear( &self->frequencies_mutex );
+ g_mutex_clear( &self->write_cache_mutex );
+
+ G_OBJECT_CLASS( foobar_application_service_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new application service instance.
+//
+FoobarApplicationService* foobar_application_service_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_APPLICATION_SERVICE, NULL );
+}
+
+//
+// Get a sorted list of all available applications.
+//
+GListModel* foobar_application_service_get_items( FoobarApplicationService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_APPLICATION_SERVICE( self ), NULL );
+
+ return G_LIST_MODEL( self->sorted_items );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called by GLib when the list of applications has changed.
+//
+void foobar_application_service_handle_changed(
+ GAppInfoMonitor* monitor,
+ gpointer userdata )
+{
+ (void)monitor;
+ FoobarApplicationService* self = (FoobarApplicationService*)userdata;
+
+ foobar_application_service_update( self );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Reload the list of applications.
+//
+void foobar_application_service_update( FoobarApplicationService* self )
+{
+ g_list_store_remove_all( self->items );
+
+ g_autolist( GAppInfo ) info = g_app_info_get_all( );
+ for ( GList* it = info; it; it = it->next )
+ {
+ if ( g_app_info_should_show( it->data ) )
+ {
+ g_autoptr( FoobarApplicationItem ) item = foobar_application_item_new( self );
+ foobar_application_item_set_info( item, it->data );
+ g_list_store_append( self->items, item );
+ }
+ }
+}
+
+//
+// Synchronously read the frequency cache file at self->cache_path, populating self->frequencies.
+//
+void foobar_application_service_read_cache( FoobarApplicationService* self )
+{
+ g_autoptr( GError ) error = NULL;
+
+ if ( self->cache_path && g_file_test( self->cache_path, G_FILE_TEST_EXISTS ) )
+ {
+ g_autoptr( JsonParser ) parser = json_parser_new( );
+ if ( !json_parser_load_from_mapped_file( parser, self->cache_path, &error ) )
+ {
+ g_warning( "Unable to read cached application frequencies: %s", error->message );
+ return;
+ }
+
+ JsonNode* root_node = json_parser_get_root( parser );
+ JsonObject* root_object = json_node_get_object( root_node );
+ json_object_foreach_member( root_object, foobar_application_service_read_cache_foreach_cb, self );
+ }
+}
+
+//
+// Callback used by foobar_application_service_read_cache to read a single entry.
+//
+void foobar_application_service_read_cache_foreach_cb(
+ JsonObject* object,
+ gchar const* member_name,
+ JsonNode* member_node,
+ gpointer userdata )
+{
+ (void)object;
+ FoobarApplicationService* self = (FoobarApplicationService*)userdata;
+
+ gsize frequency = json_node_get_int( member_node );
+ g_hash_table_insert( self->frequencies, g_strdup( member_name ), GSIZE_TO_POINTER( frequency ) );
+}
+
+//
+// Asynchronously start writing the frequency cache file at self->cache_path.
+//
+void foobar_application_service_write_cache( FoobarApplicationService* self )
+{
+ foobar_application_service_write_cache_async( self, NULL, foobar_application_service_write_cache_cb, NULL );
+}
+
+//
+// Callback invoked when the frequency cache was written successfully or failed.
+//
+void foobar_application_service_write_cache_cb(
+ GObject* object,
+ GAsyncResult* result,
+ gpointer userdata )
+{
+ (void)userdata;
+ FoobarApplicationService* self = (FoobarApplicationService*)object;
+
+ g_autoptr( GError ) error = NULL;
+ if ( !foobar_application_service_write_cache_finish( self, result, &error ) )
+ {
+ g_warning( "Unable to write cached application frequencies: %s", error->message );
+ }
+}
+
+//
+// Asynchronously write the frequency cache file at self->cache_path.
+//
+void foobar_application_service_write_cache_async(
+ FoobarApplicationService* self,
+ GCancellable* cancellable,
+ GAsyncReadyCallback callback,
+ gpointer userdata )
+{
+ g_autoptr( GTask ) task = g_task_new( self, cancellable, callback, userdata );
+ g_task_set_name( task, "write-application-cache" );
+ g_task_run_in_thread( task, foobar_application_service_write_cache_thread );
+}
+
+//
+// Get the asynchronous result for writing the frequency cache file, returning TRUE on success, or FALSE on error.
+//
+gboolean foobar_application_service_write_cache_finish(
+ FoobarApplicationService* self,
+ GAsyncResult* result,
+ GError** error )
+{
+ (void)self;
+
+ return g_task_propagate_boolean( G_TASK( result ), error );
+}
+
+//
+// Task implementation for foobar_application_service_write_cache_async, invoked on a background thread.
+//
+void foobar_application_service_write_cache_thread(
+ GTask* task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable* cancellable )
+{
+ (void)cancellable;
+ (void)task_data;
+ FoobarApplicationService* self = (FoobarApplicationService*)source_object;
+
+ if ( !self->cache_path )
+ {
+ g_task_return_boolean( task, TRUE );
+ return;
+ }
+
+ g_autoptr( GError ) error = NULL;
+
+ g_autoptr( JsonBuilder ) builder = json_builder_new( );
+ json_builder_begin_object( builder );
+
+ g_mutex_lock( &self->frequencies_mutex );
+ g_hash_table_foreach( self->frequencies, foobar_application_service_write_cache_foreach_cb, builder );
+ g_mutex_unlock( &self->frequencies_mutex );
+
+ json_builder_end_object( builder );
+
+ g_autoptr( JsonNode ) root_node = json_builder_get_root( builder );
+ g_autoptr( JsonGenerator ) generator = json_generator_new( );
+ json_generator_set_root( generator, root_node );
+
+ g_mutex_lock( &self->write_cache_mutex );
+ gboolean success = json_generator_to_file( generator, self->cache_path, &error );
+ g_mutex_unlock( &self->write_cache_mutex );
+ if ( !success )
+ {
+ g_task_return_error( task, g_steal_pointer( &error ) );
+ return;
+ }
+
+ g_task_return_boolean( task, TRUE );
+}
+
+//
+// Callback used by foobar_application_service_write_cache_thread to build a single entry.
+//
+void foobar_application_service_write_cache_foreach_cb(
+ gpointer key,
+ gpointer value,
+ gpointer userdata )
+{
+ JsonBuilder* builder = (JsonBuilder*)userdata;
+
+ json_builder_set_member_name( builder, key );
+ json_builder_add_int_value( builder, GPOINTER_TO_SIZE( value ) );
+}
+
+//
+// Sorting callback for application items. Items are sorted based on:
+// 1. frequency (descending)
+// 2. name (ascending)
+// 3. id (ascending)
+//
+gint foobar_application_service_sort_func(
+ gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarApplicationItem* app_a = (FoobarApplicationItem*)item_a;
+ FoobarApplicationItem* app_b = (FoobarApplicationItem*)item_b;
+
+ gint64 freq_a = foobar_application_item_get_frequency( app_a );
+ gint64 freq_b = foobar_application_item_get_frequency( app_b );
+ if ( freq_a > freq_b ) { return -1; }
+ if ( freq_a < freq_b ) { return 1; }
+
+ gchar const* name_a = foobar_application_item_get_name( app_a );
+ gchar const* name_b = foobar_application_item_get_name( app_b );
+ gint name_res = g_strcmp0( name_a, name_b );
+ if ( name_res ) { return name_res; }
+
+ gchar const* id_a = foobar_application_item_get_id( app_a );
+ gchar const* id_b = foobar_application_item_get_id( app_b );
+ return g_strcmp0( id_a, id_b );
+} \ No newline at end of file
diff --git a/src/services/application-service.h b/src/services/application-service.h
new file mode 100644
index 0000000..46ebd3c
--- /dev/null
+++ b/src/services/application-service.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_APPLICATION_ITEM foobar_application_item_get_type( )
+#define FOOBAR_TYPE_APPLICATION_SERVICE foobar_application_service_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarApplicationItem, foobar_application_item, FOOBAR, APPLICATION_ITEM, GObject )
+
+gchar const* foobar_application_item_get_id ( FoobarApplicationItem* self );
+gchar const* foobar_application_item_get_name ( FoobarApplicationItem* self );
+gchar const* foobar_application_item_get_description( FoobarApplicationItem* self );
+gchar const* foobar_application_item_get_executable ( FoobarApplicationItem* self );
+gchar const* foobar_application_item_get_categories ( FoobarApplicationItem* self );
+GIcon* foobar_application_item_get_icon ( FoobarApplicationItem* self );
+gint64 foobar_application_item_get_frequency ( FoobarApplicationItem* self );
+gboolean foobar_application_item_match ( FoobarApplicationItem* self,
+ gchar const* const* terms );
+void foobar_application_item_launch ( FoobarApplicationItem* self );
+
+G_DECLARE_FINAL_TYPE( FoobarApplicationService, foobar_application_service, FOOBAR, APPLICATION_SERVICE, GObject )
+
+FoobarApplicationService* foobar_application_service_new ( void );
+GListModel* foobar_application_service_get_items( FoobarApplicationService* self );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/services/audio-service.c b/src/services/audio-service.c
new file mode 100644
index 0000000..25fdad9
--- /dev/null
+++ b/src/services/audio-service.c
@@ -0,0 +1,930 @@
+#include "services/audio-service.h"
+#include <gvc-mixer-stream.h>
+#include <gvc-mixer-control.h>
+#include <gvc-mixer-source.h>
+#include <gvc-mixer-sink.h>
+#include <gtk/gtk.h>
+#include <math.h>
+
+#define DEFAULT_VOLUME 25
+
+//
+// FoobarAudioDeviceKind:
+//
+// The type of an audio device (either speaker/output or microphone/input).
+//
+
+G_DEFINE_ENUM_TYPE(
+ FoobarAudioDeviceKind,
+ foobar_audio_device_kind,
+ G_DEFINE_ENUM_VALUE( FOOBAR_AUDIO_DEVICE_INPUT, "input" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_AUDIO_DEVICE_OUTPUT, "output" ) )
+
+//
+// FoobarAudioDevice:
+//
+// Representation of a real input/output device or an abstract device (like the default device which only acts as a
+// proxy for another device).
+//
+
+struct _FoobarAudioDevice
+{
+ GObject parent_instance;
+ FoobarAudioService* service;
+ FoobarAudioDeviceKind kind;
+ GvcMixerStream* stream;
+ gboolean is_default;
+ gulong notify_handler_id;
+};
+
+enum
+{
+ DEVICE_PROP_KIND = 1,
+ DEVICE_PROP_ID,
+ DEVICE_PROP_NAME,
+ DEVICE_PROP_DESCRIPTION,
+ DEVICE_PROP_VOLUME,
+ DEVICE_PROP_IS_MUTED,
+ DEVICE_PROP_IS_DEFAULT,
+ DEVICE_PROP_IS_AVAILABLE,
+ N_DEVICE_PROPS,
+};
+
+static GParamSpec* device_props[N_DEVICE_PROPS] = { 0 };
+
+static void foobar_audio_device_class_init ( FoobarAudioDeviceClass* klass );
+static void foobar_audio_device_init ( FoobarAudioDevice* self );
+static void foobar_audio_device_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_audio_device_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_audio_device_finalize ( GObject* object );
+static FoobarAudioDevice* foobar_audio_device_new ( FoobarAudioService* service,
+ FoobarAudioDeviceKind kind );
+static void foobar_audio_device_set_stream ( FoobarAudioDevice* self,
+ GvcMixerStream* value );
+static void foobar_audio_device_set_default ( FoobarAudioDevice* self,
+ gboolean value );
+static void foobar_audio_device_handle_notify( GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarAudioDevice, foobar_audio_device, G_TYPE_OBJECT )
+
+//
+// FoobarAudioService:
+//
+// Service managing the available audio input/output devices. This is implemented using the Gnome Volume Control
+// library.
+//
+
+struct _FoobarAudioService
+{
+ GObject parent_instance;
+ GvcMixerControl* control;
+ GListStore* devices;
+ GtkSortListModel* sorted_devices;
+ GtkFilterListModel* inputs;
+ GtkFilterListModel* outputs;
+ FoobarAudioDevice* default_input;
+ FoobarAudioDevice* default_output;
+ gulong default_sink_handler_id;
+ gulong default_source_handler_id;
+ gulong stream_added_handler_id;
+ gulong stream_removed_handler_id;
+};
+
+enum
+{
+ PROP_INPUTS = 1,
+ PROP_OUTPUTS,
+ PROP_DEFAULT_INPUT,
+ PROP_DEFAULT_OUTPUT,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_audio_service_class_init ( FoobarAudioServiceClass* klass );
+static void foobar_audio_service_init ( FoobarAudioService* self );
+static void foobar_audio_service_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_audio_service_finalize ( GObject* object );
+static void foobar_audio_service_handle_default_sink_changed ( GvcMixerControl* control,
+ guint id,
+ gpointer userdata );
+static void foobar_audio_service_handle_default_source_changed( GvcMixerControl* control,
+ guint id,
+ gpointer userdata );
+static void foobar_audio_service_handle_stream_added ( GvcMixerControl* control,
+ guint id,
+ gpointer userdata );
+static void foobar_audio_service_handle_stream_removed ( GvcMixerControl* control,
+ guint id,
+ gpointer userdata );
+static gboolean foobar_audio_service_identify_stream ( GvcMixerStream* stream,
+ FoobarAudioDeviceKind* out_kind );
+static void foobar_audio_service_update_default ( GListModel* list,
+ guint id );
+static gint foobar_audio_service_sort_func ( gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata );
+static gboolean foobar_audio_service_input_filter_func ( gpointer item,
+ gpointer userdata );
+static gboolean foobar_audio_service_output_filter_func ( gpointer item,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarAudioService, foobar_audio_service, G_TYPE_OBJECT )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Device
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for audio devices.
+//
+void foobar_audio_device_class_init( FoobarAudioDeviceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_audio_device_get_property;
+ object_klass->set_property = foobar_audio_device_set_property;
+ object_klass->finalize = foobar_audio_device_finalize;
+
+ device_props[DEVICE_PROP_KIND] = g_param_spec_enum(
+ "kind",
+ "Kind",
+ "The kind of audio device.",
+ FOOBAR_TYPE_AUDIO_DEVICE_KIND,
+ FOOBAR_AUDIO_DEVICE_INPUT,
+ G_PARAM_READABLE );
+ device_props[DEVICE_PROP_ID] = g_param_spec_uint(
+ "id",
+ "ID",
+ "Current numeric ID of the device (may change over time).",
+ 0,
+ UINT_MAX,
+ UINT_MAX,
+ G_PARAM_READABLE );
+ device_props[DEVICE_PROP_NAME] = g_param_spec_string(
+ "name",
+ "Name",
+ "Current name of the device.",
+ NULL,
+ G_PARAM_READABLE );
+ device_props[DEVICE_PROP_DESCRIPTION] = g_param_spec_string(
+ "description",
+ "Description",
+ "Short textual description of the device.",
+ NULL,
+ G_PARAM_READABLE );
+ device_props[DEVICE_PROP_VOLUME] = g_param_spec_int(
+ "volume",
+ "Volume",
+ "Current volume in percent.",
+ 0,
+ 100,
+ 0,
+ G_PARAM_READWRITE );
+ device_props[DEVICE_PROP_IS_MUTED] = g_param_spec_int(
+ "is-muted",
+ "Is Muted",
+ "Indicates whether the device is currently muted (i.e. the volume is 0).",
+ 0,
+ 100,
+ 0,
+ G_PARAM_READWRITE );
+ device_props[DEVICE_PROP_IS_DEFAULT] = g_param_spec_boolean(
+ "is-default",
+ "Is Default",
+ "Indicates whether the device is currently also a default device (either microphone or speaker).",
+ FALSE,
+ G_PARAM_READABLE );
+ device_props[DEVICE_PROP_IS_AVAILABLE] = g_param_spec_boolean(
+ "is-available",
+ "Is Available",
+ "Indicates whether the device is currently available to use/plugged in.",
+ FALSE,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_DEVICE_PROPS, device_props );
+}
+
+//
+// Instance initialization for audio devices.
+//
+void foobar_audio_device_init( FoobarAudioDevice* self )
+{
+ (void)self;
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_audio_device_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarAudioDevice* self = (FoobarAudioDevice*)object;
+
+ switch ( prop_id )
+ {
+ case DEVICE_PROP_KIND:
+ g_value_set_enum( value, foobar_audio_device_get_kind( self ) );
+ break;
+ case DEVICE_PROP_ID:
+ g_value_set_uint( value, foobar_audio_device_get_id( self ) );
+ break;
+ case DEVICE_PROP_NAME:
+ g_value_set_string( value, foobar_audio_device_get_name( self ) );
+ break;
+ case DEVICE_PROP_DESCRIPTION:
+ g_value_set_string( value, foobar_audio_device_get_description( self ) );
+ break;
+ case DEVICE_PROP_VOLUME:
+ g_value_set_int( value, foobar_audio_device_get_volume( self ) );
+ break;
+ case DEVICE_PROP_IS_MUTED:
+ g_value_set_boolean( value, foobar_audio_device_is_muted( self ) );
+ break;
+ case DEVICE_PROP_IS_DEFAULT:
+ g_value_set_boolean( value, foobar_audio_device_is_default( self ) );
+ break;
+ case DEVICE_PROP_IS_AVAILABLE:
+ g_value_set_boolean( value, foobar_audio_device_is_available( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_audio_device_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarAudioDevice* self = (FoobarAudioDevice*)object;
+
+ switch ( prop_id )
+ {
+ case DEVICE_PROP_VOLUME:
+ foobar_audio_device_set_volume( self, g_value_get_int( value ) );
+ break;
+ case DEVICE_PROP_IS_MUTED:
+ foobar_audio_device_set_muted( self, g_value_get_boolean( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for audio devices.
+//
+void foobar_audio_device_finalize( GObject* object )
+{
+ FoobarAudioDevice* self = (FoobarAudioDevice*)object;
+
+ g_clear_signal_handler( &self->notify_handler_id, self->stream );
+ g_clear_object( &self->stream );
+
+ G_OBJECT_CLASS( foobar_audio_device_parent_class )->finalize( object );
+}
+
+//
+// Create a new audio device object with the specified parent service (captured as an unowned reference).
+//
+FoobarAudioDevice* foobar_audio_device_new(
+ FoobarAudioService* service,
+ FoobarAudioDeviceKind kind )
+{
+ FoobarAudioDevice* self = g_object_new( FOOBAR_TYPE_AUDIO_DEVICE, NULL );
+ self->service = service;
+ self->kind = kind;
+ return self;
+}
+
+//
+// Get the type of this device.
+//
+FoobarAudioDeviceKind foobar_audio_device_get_kind( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), 0 );
+ return self->kind;
+}
+
+//
+// Get a numeric identifier for the device provided by the system.
+//
+guint foobar_audio_device_get_id( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), 0 );
+ return self->stream ? gvc_mixer_stream_get_id( self->stream ) : UINT_MAX;
+}
+
+//
+// Get the internal name for the device. This should not be used in the UI.
+//
+gchar const* foobar_audio_device_get_name( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), NULL );
+ return self->stream ? gvc_mixer_stream_get_name( self->stream ) : NULL;
+}
+
+//
+// Get a human-readable description for the device.
+//
+gchar const* foobar_audio_device_get_description( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), NULL );
+ return self->stream ? gvc_mixer_stream_get_description( self->stream ) : NULL;
+}
+
+//
+// Get the current volume as a percentage value. If muted, this is 0.
+//
+gint foobar_audio_device_get_volume( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), 0 );
+
+ if ( !self->service ) { return 0; }
+ if ( self->stream && gvc_mixer_stream_get_is_muted( self->stream ) ) { return 0; }
+ gdouble max_volume = gvc_mixer_control_get_vol_max_norm( self->service->control );
+ gdouble cur_volume = self->stream ? gvc_mixer_stream_get_volume( self->stream ) : 0;
+ gint percentage = (gint)round( 100. * cur_volume / max_volume );
+ return CLAMP( percentage, 0, 100 );
+}
+
+//
+// Check whether the device is currently muted, i.e. its volume is 0.
+//
+gboolean foobar_audio_device_is_muted( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), FALSE );
+ return foobar_audio_device_get_volume( self ) == 0;
+}
+
+//
+// Check whether this device is the system-default for its kind.
+//
+// This will always return TRUE for the static default device instance.
+//
+gboolean foobar_audio_device_is_default( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), FALSE );
+ return self->is_default;
+}
+
+//
+// Check whether the device is currently available.
+//
+gboolean foobar_audio_device_is_available( FoobarAudioDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), FALSE );
+ return self->stream != NULL;
+}
+
+//
+// Update the stream backing this device object.
+//
+void foobar_audio_device_set_stream(
+ FoobarAudioDevice* self,
+ GvcMixerStream* value )
+{
+ g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) );
+
+ if ( self->stream != value )
+ {
+ g_clear_signal_handler( &self->notify_handler_id, self->stream );
+ g_clear_object( &self->stream );
+
+ if ( value )
+ {
+ self->stream = g_object_ref( value );
+ self->notify_handler_id = g_signal_connect(
+ self->stream,
+ "notify",
+ G_CALLBACK( foobar_audio_device_handle_notify ),
+ self );
+ }
+
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_ID] );
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_NAME] );
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_DESCRIPTION] );
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_VOLUME] );
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_MUTED] );
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_AVAILABLE] );
+ }
+}
+
+//
+// Update the device's "default" flag. This does not actually make it the default device.
+//
+void foobar_audio_device_set_default(
+ FoobarAudioDevice* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) );
+
+ value = !!value;
+ if ( self->is_default != value )
+ {
+ self->is_default = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_DEFAULT] );
+ }
+}
+
+//
+// Update the device's volume as a percentage value.
+//
+void foobar_audio_device_set_volume(
+ FoobarAudioDevice* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) );
+
+ if ( !self->service || !self->stream ) { return; }
+
+ value = CLAMP( value, 0, 100 );
+ if ( foobar_audio_device_get_volume( self ) != value )
+ {
+ gdouble max_volume = gvc_mixer_control_get_vol_max_norm( self->service->control );
+ gdouble new_volume = value * max_volume / 100.;
+ gvc_mixer_stream_set_volume( self->stream, (guint32)new_volume );
+ gvc_mixer_stream_push_volume( self->stream );
+ gvc_mixer_stream_set_is_muted( self->stream, value == 0 );
+ gvc_mixer_stream_change_is_muted( self->stream, value == 0 );
+ }
+}
+
+//
+// Update the device's "muted" state.
+//
+// If the device was previously muted, the volume is set to either the previous volume or to a default value.
+//
+void foobar_audio_device_set_muted(
+ FoobarAudioDevice* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) );
+
+ if ( foobar_audio_device_is_muted( self ) != value )
+ {
+ if ( self->stream )
+ {
+ gvc_mixer_stream_set_is_muted( self->stream, value );
+ gvc_mixer_stream_change_is_muted( self->stream, value );
+ }
+
+ if ( !value && foobar_audio_device_get_volume( self ) == 0 )
+ {
+ foobar_audio_device_set_volume( self, DEFAULT_VOLUME );
+ }
+ }
+}
+
+//
+// Let this device become default one for its kind.
+//
+void foobar_audio_device_make_default( FoobarAudioDevice* self )
+{
+ g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) );
+
+ if ( !self->service || !self->stream ) { return; }
+
+ if ( !foobar_audio_device_is_default( self ) )
+ {
+ switch ( self->kind )
+ {
+ case FOOBAR_AUDIO_DEVICE_INPUT:
+ gvc_mixer_control_set_default_source( self->service->control, self->stream );
+ break;
+ case FOOBAR_AUDIO_DEVICE_OUTPUT:
+ gvc_mixer_control_set_default_sink( self->service->control, self->stream );
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+//
+// Called by the underlying stream when one of its properties changes.
+//
+void foobar_audio_device_handle_notify(
+ GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)object;
+ FoobarAudioDevice* self = (FoobarAudioDevice*)userdata;
+
+ gchar const* property = g_param_spec_get_name( pspec );
+ if ( !g_strcmp0( property, "id" ) )
+ {
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_ID] );
+ }
+ else if ( !g_strcmp0( property, "name" ) )
+ {
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_NAME] );
+ }
+ else if ( !g_strcmp0( property, "description" ) )
+ {
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_DESCRIPTION] );
+ }
+ else if ( !g_strcmp0( property, "volume" ) || !g_strcmp0( property, "is-muted" ) )
+ {
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_VOLUME] );
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_MUTED] );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Service Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the audio service.
+//
+void foobar_audio_service_class_init( FoobarAudioServiceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_audio_service_get_property;
+ object_klass->finalize = foobar_audio_service_finalize;
+
+ props[PROP_INPUTS] = g_param_spec_object(
+ "inputs",
+ "Inputs",
+ "Sorted list of input devices.",
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READABLE );
+ props[PROP_OUTPUTS] = g_param_spec_object(
+ "outputs",
+ "Outputs",
+ "Sorted list of output devices.",
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READABLE );
+ props[PROP_DEFAULT_INPUT] = g_param_spec_object(
+ "default-input",
+ "Default Input",
+ "The default input device (reference remains constant).",
+ FOOBAR_TYPE_AUDIO_DEVICE,
+ G_PARAM_READABLE );
+ props[PROP_DEFAULT_OUTPUT] = g_param_spec_object(
+ "default-output",
+ "Default Output",
+ "The default output device (reference remains constant).",
+ FOOBAR_TYPE_AUDIO_DEVICE,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for the audio service.
+//
+void foobar_audio_service_init( FoobarAudioService* self )
+{
+ self->control = gvc_mixer_control_new( "Foobar mixer control" );
+ self->devices = g_list_store_new( FOOBAR_TYPE_AUDIO_DEVICE );
+
+ GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_audio_service_sort_func, NULL, NULL );
+ self->sorted_devices = gtk_sort_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->devices ) ),
+ GTK_SORTER( sorter ) );
+
+ GtkCustomFilter* input_filter = gtk_custom_filter_new( foobar_audio_service_input_filter_func, NULL, NULL );
+ self->inputs = gtk_filter_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->sorted_devices ) ),
+ GTK_FILTER( input_filter ) );
+
+ GtkCustomFilter* output_filter = gtk_custom_filter_new( foobar_audio_service_output_filter_func, NULL, NULL );
+ self->outputs = gtk_filter_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->sorted_devices ) ),
+ GTK_FILTER( output_filter ) );
+
+ self->default_input = foobar_audio_device_new( self, FOOBAR_AUDIO_DEVICE_INPUT );
+ self->default_output = foobar_audio_device_new( self, FOOBAR_AUDIO_DEVICE_OUTPUT );
+ foobar_audio_device_set_default( self->default_input, TRUE );
+ foobar_audio_device_set_default( self->default_output, TRUE );
+
+ self->default_sink_handler_id = g_signal_connect(
+ self->control,
+ "default-sink-changed",
+ G_CALLBACK( foobar_audio_service_handle_default_sink_changed ),
+ self );
+ self->default_source_handler_id = g_signal_connect(
+ self->control,
+ "default-source-changed",
+ G_CALLBACK( foobar_audio_service_handle_default_source_changed ),
+ self );
+ self->stream_added_handler_id = g_signal_connect(
+ self->control,
+ "stream-added",
+ G_CALLBACK( foobar_audio_service_handle_stream_added ),
+ self );
+ self->stream_removed_handler_id = g_signal_connect(
+ self->control,
+ "stream-removed",
+ G_CALLBACK( foobar_audio_service_handle_stream_removed ),
+ self );
+
+ gvc_mixer_control_open( self->control );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_audio_service_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarAudioService* self = (FoobarAudioService*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_INPUTS:
+ g_value_set_object( value, foobar_audio_service_get_inputs( self ) );
+ break;
+ case PROP_OUTPUTS:
+ g_value_set_object( value, foobar_audio_service_get_outputs( self ) );
+ break;
+ case PROP_DEFAULT_INPUT:
+ g_value_set_object( value, foobar_audio_service_get_default_input( self ) );
+ break;
+ case PROP_DEFAULT_OUTPUT:
+ g_value_set_object( value, foobar_audio_service_get_default_output( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for the audio service.
+//
+void foobar_audio_service_finalize( GObject* object )
+{
+ FoobarAudioService* self = (FoobarAudioService*)object;
+
+ g_clear_signal_handler( &self->default_sink_handler_id, self->control );
+ g_clear_signal_handler( &self->default_source_handler_id, self->control );
+ g_clear_signal_handler( &self->stream_added_handler_id, self->control );
+ g_clear_signal_handler( &self->stream_removed_handler_id, self->control );
+
+ gvc_mixer_control_close( self->control );
+
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i )
+ {
+ FoobarAudioDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i );
+ device->service = NULL;
+ }
+
+ self->default_input->service = NULL;
+ self->default_output->service = NULL;
+
+ g_clear_object( &self->control );
+ g_clear_object( &self->inputs );
+ g_clear_object( &self->outputs );
+ g_clear_object( &self->sorted_devices );
+ g_clear_object( &self->devices );
+ g_clear_object( &self->default_input );
+ g_clear_object( &self->default_output );
+
+ G_OBJECT_CLASS( foobar_audio_service_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new audio service instance.
+//
+FoobarAudioService* foobar_audio_service_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_AUDIO_SERVICE, NULL );
+}
+
+//
+// Get the default input device. This reference remains constant.
+//
+FoobarAudioDevice* foobar_audio_service_get_default_input( FoobarAudioService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL );
+ return self->default_input;
+}
+
+//
+// Get the default output device. This reference remains constant.
+//
+FoobarAudioDevice* foobar_audio_service_get_default_output( FoobarAudioService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL );
+ return self->default_output;
+}
+
+//
+// Get a sorted list of input devices.
+//
+GListModel* foobar_audio_service_get_inputs( FoobarAudioService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL );
+ return G_LIST_MODEL( self->inputs );
+}
+
+//
+// Get a sorted list of output devices.
+//
+GListModel* foobar_audio_service_get_outputs( FoobarAudioService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL );
+ return G_LIST_MODEL( self->outputs );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called by the mixer control when the default output device has changed.
+//
+// This will update the "default" state of the device and set the backing stream for the static default device.
+//
+void foobar_audio_service_handle_default_sink_changed(
+ GvcMixerControl* control,
+ guint id,
+ gpointer userdata )
+{
+ (void)control;
+ FoobarAudioService* self = (FoobarAudioService*)userdata;
+
+ foobar_audio_service_update_default( G_LIST_MODEL( self->outputs ), id );
+
+ GvcMixerStream* stream = gvc_mixer_control_lookup_stream_id( self->control, id );
+ foobar_audio_device_set_stream( self->default_output, stream );
+}
+
+//
+// Called by the mixer control when the default input device has changed.
+//
+// This will update the "default" state of the device and set the backing stream for the static default device.
+//
+void foobar_audio_service_handle_default_source_changed(
+ GvcMixerControl* control,
+ guint id,
+ gpointer userdata )
+{
+ (void)control;
+ FoobarAudioService* self = (FoobarAudioService*)userdata;
+
+ foobar_audio_service_update_default( G_LIST_MODEL( self->inputs ), id );
+
+ GvcMixerStream* stream = gvc_mixer_control_lookup_stream_id( self->control, id );
+ foobar_audio_device_set_stream( self->default_input, stream );
+}
+
+//
+// Called when an audio device was added.
+//
+void foobar_audio_service_handle_stream_added(
+ GvcMixerControl* control,
+ guint id,
+ gpointer userdata )
+{
+ (void)control;
+ FoobarAudioService* self = (FoobarAudioService*)userdata;
+
+ GvcMixerStream* stream = gvc_mixer_control_lookup_stream_id( self->control, id );
+ FoobarAudioDeviceKind kind;
+ if ( foobar_audio_service_identify_stream( stream, &kind ) )
+ {
+ g_autoptr( FoobarAudioDevice ) device = foobar_audio_device_new( self, kind );
+ foobar_audio_device_set_stream( device, stream );
+ g_list_store_append( self->devices, device );
+ }
+}
+
+//
+// Called when an audio device was removed.
+//
+void foobar_audio_service_handle_stream_removed(
+ GvcMixerControl* control,
+ guint id,
+ gpointer userdata )
+{
+ (void)control;
+ FoobarAudioService* self = (FoobarAudioService*)userdata;
+
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i )
+ {
+ FoobarAudioDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i );
+ if ( foobar_audio_device_get_id( device ) == id )
+ {
+ g_object_ref( device );
+ device->service = NULL;
+ g_list_store_remove( self->devices, i );
+ foobar_audio_device_set_stream( device, NULL );
+ g_object_unref( device );
+ break;
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Identify whether a stream is an input device or an output device, returning FALSE if unknown.
+//
+gboolean foobar_audio_service_identify_stream(
+ GvcMixerStream* stream,
+ FoobarAudioDeviceKind* out_kind )
+{
+ if ( GVC_IS_MIXER_SINK( stream ) )
+ {
+ *out_kind = FOOBAR_AUDIO_DEVICE_OUTPUT;
+ return TRUE;
+ }
+
+ if ( GVC_IS_MIXER_SOURCE( stream ) )
+ {
+ *out_kind = FOOBAR_AUDIO_DEVICE_INPUT;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+//
+// Set the "default" state for the device with the specified ID to TRUE and all other devices to FALSE.
+//
+void foobar_audio_service_update_default(
+ GListModel* list,
+ guint id )
+{
+ for ( guint i = 0; i < g_list_model_get_n_items( list ); ++i )
+ {
+ FoobarAudioDevice* device = g_list_model_get_item( list, i );
+ gboolean is_default = foobar_audio_device_get_id( device ) == id;
+ foobar_audio_device_set_default( device, is_default );
+ }
+}
+
+//
+// Sorting callback for audio devices.
+//
+gint foobar_audio_service_sort_func(
+ gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarAudioDevice* device_a = (FoobarAudioDevice*)item_a;
+ FoobarAudioDevice* device_b = (FoobarAudioDevice*)item_b;
+ return g_strcmp0(
+ foobar_audio_device_get_description( device_a ),
+ foobar_audio_device_get_description( device_b ) );
+}
+
+//
+// Filtering callback for the list of input devices.
+//
+gboolean foobar_audio_service_input_filter_func(
+ gpointer item,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarAudioDevice* device = item;
+ return foobar_audio_device_get_kind( device ) == FOOBAR_AUDIO_DEVICE_INPUT;
+}
+
+//
+// Filtering callback for the list of output devices.
+//
+gboolean foobar_audio_service_output_filter_func(
+ gpointer item,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarAudioDevice* device = item;
+ return foobar_audio_device_get_kind( device ) == FOOBAR_AUDIO_DEVICE_OUTPUT;
+} \ No newline at end of file
diff --git a/src/services/audio-service.h b/src/services/audio-service.h
new file mode 100644
index 0000000..cd30de1
--- /dev/null
+++ b/src/services/audio-service.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_AUDIO_DEVICE_KIND foobar_audio_device_kind_get_type( )
+#define FOOBAR_TYPE_AUDIO_DEVICE foobar_audio_device_get_type( )
+#define FOOBAR_TYPE_AUDIO_SERVICE foobar_audio_service_get_type( )
+
+typedef enum
+{
+ FOOBAR_AUDIO_DEVICE_INPUT = 0,
+ FOOBAR_AUDIO_DEVICE_OUTPUT,
+} FoobarAudioDeviceKind;
+
+GType foobar_audio_device_kind_get_type( void );
+
+G_DECLARE_FINAL_TYPE( FoobarAudioDevice, foobar_audio_device, FOOBAR, AUDIO_DEVICE, GObject )
+
+FoobarAudioDeviceKind foobar_audio_device_get_kind ( FoobarAudioDevice* self );
+guint foobar_audio_device_get_id ( FoobarAudioDevice* self );
+gchar const* foobar_audio_device_get_name ( FoobarAudioDevice* self );
+gchar const* foobar_audio_device_get_description( FoobarAudioDevice* self );
+gint foobar_audio_device_get_volume ( FoobarAudioDevice* self );
+gboolean foobar_audio_device_is_muted ( FoobarAudioDevice* self );
+gboolean foobar_audio_device_is_default ( FoobarAudioDevice* self );
+gboolean foobar_audio_device_is_available ( FoobarAudioDevice* self );
+void foobar_audio_device_set_volume ( FoobarAudioDevice* self,
+ gint value );
+void foobar_audio_device_set_muted ( FoobarAudioDevice* self,
+ gboolean value );
+void foobar_audio_device_make_default ( FoobarAudioDevice* self );
+
+G_DECLARE_FINAL_TYPE( FoobarAudioService, foobar_audio_service, FOOBAR, AUDIO_SERVICE, GObject )
+
+FoobarAudioService* foobar_audio_service_new ( void );
+FoobarAudioDevice* foobar_audio_service_get_default_input ( FoobarAudioService* self );
+FoobarAudioDevice* foobar_audio_service_get_default_output( FoobarAudioService* self );
+GListModel* foobar_audio_service_get_inputs ( FoobarAudioService* self );
+GListModel* foobar_audio_service_get_outputs ( FoobarAudioService* self );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/services/battery-service.c b/src/services/battery-service.c
new file mode 100644
index 0000000..2f21070
--- /dev/null
+++ b/src/services/battery-service.c
@@ -0,0 +1,378 @@
+#include "services/battery-service.h"
+#include "dbus/upower.h"
+#include <math.h>
+
+#define DEVICE_STATE_CHARGING 1
+#define DEVICE_STATE_FULLY_CHARGED 4
+
+//
+// FoobarBatteryState:
+//
+// Structure storing the current information about the battery.
+//
+
+struct _FoobarBatteryState
+{
+ gboolean is_charging;
+ gboolean is_charged;
+ gint percentage;
+ gint time_remaining;
+};
+
+G_DEFINE_BOXED_TYPE( FoobarBatteryState, foobar_battery_state, foobar_battery_state_copy, foobar_battery_state_free )
+
+//
+// FoobarBatteryService:
+//
+// Service monitoring the state of the battery. This is implemented using the UPower D-Bus API.
+//
+
+struct _FoobarBatteryService
+{
+ GObject parent_instance;
+ FoobarBatteryState* state;
+ FoobarUPowerDevice* proxy;
+ gulong changed_handler_id;
+};
+
+enum
+{
+ PROP_STATE = 1,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_battery_service_class_init ( FoobarBatteryServiceClass* klass );
+static void foobar_battery_service_init ( FoobarBatteryService* self );
+static void foobar_battery_service_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_battery_service_finalize ( GObject* object );
+static void foobar_battery_service_handle_connect ( GObject* object,
+ GAsyncResult* result,
+ gpointer userdata );
+static void foobar_battery_service_handle_properties_changed( GDBusProxy* proxy,
+ GVariant* changed_properties,
+ GStrv invalidated_properties,
+ gpointer userdata );
+static void foobar_battery_service_update ( FoobarBatteryService* self );
+
+G_DEFINE_FINAL_TYPE( FoobarBatteryService, foobar_battery_service, G_TYPE_OBJECT )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// State
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new, default-initialized state structure.
+//
+FoobarBatteryState* foobar_battery_state_new( void )
+{
+ return g_new0( FoobarBatteryState, 1 );
+}
+
+//
+// Create a mutable copy of a state structure.
+//
+FoobarBatteryState* foobar_battery_state_copy( FoobarBatteryState const* self )
+{
+ if ( !self ) { return NULL; }
+
+ FoobarBatteryState* copy = g_new0( FoobarBatteryState, 1 );
+ copy->is_charging = self->is_charging;
+ copy->is_charged = self->is_charged;
+ copy->percentage = self->percentage;
+ copy->time_remaining = self->time_remaining;
+ return copy;
+}
+
+//
+// Release resources associated with a state structure (NULL is allowed).
+//
+void foobar_battery_state_free( FoobarBatteryState* self )
+{
+ g_free( self );
+}
+
+//
+// Compare two battery state structures (NULL is allowed)/
+//
+gboolean foobar_battery_state_equal(
+ FoobarBatteryState const* a,
+ FoobarBatteryState const* b )
+{
+ if ( a == NULL && b == NULL ) { return TRUE; }
+ if ( a == NULL || b == NULL ) { return FALSE; }
+
+ if ( a->is_charging != b->is_charging ) { return FALSE; }
+ if ( a->is_charged != b->is_charged ) { return FALSE; }
+ if ( a->percentage != b->percentage ) { return FALSE; }
+ if ( a->time_remaining != b->time_remaining ) { return FALSE; }
+
+ return TRUE;
+}
+
+//
+// Indicates whether the battery is currently being charged.
+//
+gboolean foobar_battery_state_is_charging( FoobarBatteryState const* self )
+{
+ g_return_val_if_fail( self != NULL, FALSE );
+ return self->is_charging;
+}
+
+//
+// Indicates whether the battery is currently being charged.
+//
+void foobar_battery_state_set_charging(
+ FoobarBatteryState* self,
+ gboolean value )
+{
+ g_return_if_fail( self != NULL );
+ self->is_charging = !!value;
+}
+
+//
+// Indicates whether the battery is fully charged.
+//
+gboolean foobar_battery_state_is_charged( FoobarBatteryState const* self )
+{
+ g_return_val_if_fail( self != NULL, FALSE );
+ return self->is_charged;
+}
+
+//
+// Indicates whether the battery is fully charged.
+//
+void foobar_battery_state_set_charged(
+ FoobarBatteryState* self,
+ gboolean value )
+{
+ g_return_if_fail( self != NULL );
+ self->is_charged = !!value;
+}
+
+//
+// Current battery level as a percentage value.
+//
+gint foobar_battery_state_get_percentage( FoobarBatteryState const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->percentage;
+}
+
+//
+// Current battery level as a percentage value.
+//
+void foobar_battery_state_set_percentage(
+ FoobarBatteryState* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->percentage = CLAMP( value, 0, 100 );
+}
+
+//
+// Estimated remaining time until fully charged or empty in seconds.
+//
+gint foobar_battery_state_get_time_remaining( FoobarBatteryState const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->time_remaining;
+}
+
+//
+// Estimated remaining time until fully charged or empty in seconds.
+//
+void foobar_battery_state_set_time_remaining(
+ FoobarBatteryState* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->time_remaining = MAX( value, 0 );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Service Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the battery service.
+//
+void foobar_battery_service_class_init( FoobarBatteryServiceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_battery_service_get_property;
+ object_klass->finalize = foobar_battery_service_finalize;
+
+ props[PROP_STATE] = g_param_spec_boxed(
+ "state",
+ "State",
+ "Current state of the battery, if available.",
+ FOOBAR_TYPE_BATTERY_STATE,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for the battery service.
+//
+void foobar_battery_service_init( FoobarBatteryService* self )
+{
+ foobar_upower_device_proxy_new_for_bus(
+ G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_NONE,
+ "org.freedesktop.UPower",
+ "/org/freedesktop/UPower/devices/DisplayDevice",
+ NULL,
+ foobar_battery_service_handle_connect,
+ g_object_ref( self ) );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_battery_service_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarBatteryService* self = (FoobarBatteryService*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_STATE:
+ g_value_set_boxed( value, foobar_battery_service_get_state( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for the battery service.
+//
+void foobar_battery_service_finalize( GObject* object )
+{
+ FoobarBatteryService* self = (FoobarBatteryService*)object;
+
+ g_clear_signal_handler( &self->changed_handler_id, self->proxy );
+ g_clear_object( &self->proxy );
+ g_clear_pointer( &self->state, foobar_battery_state_free );
+
+ G_OBJECT_CLASS( foobar_battery_service_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new battery service instance.
+//
+FoobarBatteryService* foobar_battery_service_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_BATTERY_SERVICE, NULL );
+}
+
+//
+// Get the current state of the battery, if available.
+//
+FoobarBatteryState const* foobar_battery_service_get_state( FoobarBatteryService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_BATTERY_SERVICE( self ), NULL );
+
+ return self->state;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called after asynchronous initialization of the D-Bus proxy.
+//
+void foobar_battery_service_handle_connect(
+ GObject* object,
+ GAsyncResult* result,
+ gpointer userdata )
+{
+ (void)object;
+ g_autoptr( FoobarBatteryService ) self = (FoobarBatteryService*)userdata;
+
+ g_autoptr( GError ) error = NULL;
+ self->proxy = foobar_upower_device_proxy_new_for_bus_finish( result, &error );
+ if ( self->proxy )
+ {
+ self->changed_handler_id = g_signal_connect(
+ self->proxy,
+ "g-properties-changed",
+ G_CALLBACK( foobar_battery_service_handle_properties_changed ),
+ self );
+ foobar_battery_service_update( self );
+ }
+ else
+ {
+ g_warning( "Unable to connect to battery status service: %s", error->message );
+ }
+}
+
+//
+// Called by the D-Bus server when the battery state has changed.
+//
+void foobar_battery_service_handle_properties_changed(
+ GDBusProxy* proxy,
+ GVariant* changed_properties,
+ GStrv invalidated_properties,
+ gpointer userdata )
+{
+ (void)proxy;
+ (void)changed_properties;
+ (void)invalidated_properties;
+ FoobarBatteryService* self = (FoobarBatteryService*)userdata;
+
+ foobar_battery_service_update( self );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Reload the battery state.
+//
+void foobar_battery_service_update( FoobarBatteryService* self )
+{
+ g_return_if_fail( FOOBAR_IS_BATTERY_SERVICE( self ) );
+
+ FoobarBatteryState* new_state = NULL;
+ if ( self->proxy && foobar_upower_device_get_is_present( self->proxy ) )
+ {
+ guint state = foobar_upower_device_get_state( self->proxy );
+ gint percent = (gint)round( foobar_upower_device_get_percentage( self->proxy ) );
+ gboolean is_charging = state == DEVICE_STATE_CHARGING;
+ gboolean is_charged =
+ state == DEVICE_STATE_FULLY_CHARGED ||
+ ( state == DEVICE_STATE_CHARGING && percent == 100 );
+ gint time_remaining = is_charging
+ ? (gint)foobar_upower_device_get_time_to_full( self->proxy )
+ : (gint)foobar_upower_device_get_time_to_empty( self->proxy );
+
+ new_state = foobar_battery_state_new( );
+ foobar_battery_state_set_charging( new_state, is_charging );
+ foobar_battery_state_set_charged( new_state, is_charged );
+ foobar_battery_state_set_percentage( new_state, percent );
+ foobar_battery_state_set_time_remaining( new_state, time_remaining );
+ }
+
+ if ( !foobar_battery_state_equal( self->state, new_state ) )
+ {
+ g_clear_pointer( &self->state, foobar_battery_state_free );
+ self->state = new_state;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_STATE] );
+ }
+} \ No newline at end of file
diff --git a/src/services/battery-service.h b/src/services/battery-service.h
new file mode 100644
index 0000000..9813679
--- /dev/null
+++ b/src/services/battery-service.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_BATTERY_STATE foobar_battery_state_get_type( )
+#define FOOBAR_TYPE_BATTERY_SERVICE foobar_battery_service_get_type( )
+
+typedef struct _FoobarBatteryState FoobarBatteryState;
+
+GType foobar_battery_state_get_type ( void );
+FoobarBatteryState* foobar_battery_state_new ( void );
+FoobarBatteryState* foobar_battery_state_copy ( FoobarBatteryState const* self );
+void foobar_battery_state_free ( FoobarBatteryState* self );
+gboolean foobar_battery_state_equal ( FoobarBatteryState const* a,
+ FoobarBatteryState const* b );
+gboolean foobar_battery_state_is_charging ( FoobarBatteryState const* self );
+gboolean foobar_battery_state_is_charged ( FoobarBatteryState const* self );
+gint foobar_battery_state_get_percentage ( FoobarBatteryState const* self );
+gint foobar_battery_state_get_time_remaining( FoobarBatteryState const* self );
+void foobar_battery_state_set_charging ( FoobarBatteryState* self,
+ gboolean value );
+void foobar_battery_state_set_charged ( FoobarBatteryState* self,
+ gboolean value );
+void foobar_battery_state_set_percentage ( FoobarBatteryState* self,
+ gint value );
+void foobar_battery_state_set_time_remaining( FoobarBatteryState* self,
+ gint value );
+
+G_DECLARE_FINAL_TYPE( FoobarBatteryService, foobar_battery_service, FOOBAR, BATTERY_SERVICE, GObject )
+
+FoobarBatteryService* foobar_battery_service_new ( void );
+FoobarBatteryState const* foobar_battery_service_get_state( FoobarBatteryService* self );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/services/bluetooth-service.c b/src/services/bluetooth-service.c
new file mode 100644
index 0000000..992c3b1
--- /dev/null
+++ b/src/services/bluetooth-service.c
@@ -0,0 +1,949 @@
+#include "services/bluetooth-service.h"
+#include "dbus/bluez.h"
+#include <gtk/gtk.h>
+
+//
+// FoobarBluetoothDeviceState:
+//
+// The current connection state of a remote bluetooth device.
+//
+
+G_DEFINE_ENUM_TYPE(
+ FoobarBluetoothDeviceState,
+ foobar_bluetooth_device_state,
+ G_DEFINE_ENUM_VALUE( FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED, "disconnected" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING, "connecting" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED, "connected" ) )
+
+//
+// FoobarBluetoothDevice:
+//
+// Represents an external bluetooth device the client can connect to.
+//
+
+struct _FoobarBluetoothDevice
+{
+ GObject parent_instance;
+ FoobarBluetoothService* service;
+ FoobarBluetoothDeviceState state;
+ FoobarBluezDevice* device;
+ GCancellable* connect_cancellable;
+ gulong notify_handler_id;
+};
+
+enum
+{
+ DEVICE_PROP_NAME = 1,
+ DEVICE_PROP_STATE,
+ N_DEVICE_PROPS,
+};
+
+static GParamSpec* device_props[N_DEVICE_PROPS] = { 0 };
+
+static void foobar_bluetooth_device_class_init ( FoobarBluetoothDeviceClass* klass );
+static void foobar_bluetooth_device_init ( FoobarBluetoothDevice* self );
+static void foobar_bluetooth_device_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_bluetooth_device_finalize ( GObject* object );
+static FoobarBluetoothDevice* foobar_bluetooth_device_new ( FoobarBluetoothService* service,
+ FoobarBluezDevice* device );
+static void foobar_bluetooth_device_handle_notify( GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata );
+static void foobar_bluetooth_device_connect_cb ( GObject* object,
+ GAsyncResult* result,
+ gpointer userdata );
+static void foobar_bluetooth_device_disconnect_cb( GObject* object,
+ GAsyncResult* result,
+ gpointer userdata );
+static void foobar_bluetooth_device_update_state ( FoobarBluetoothDevice* self );
+
+G_DEFINE_FINAL_TYPE( FoobarBluetoothDevice, foobar_bluetooth_device, G_TYPE_OBJECT )
+
+//
+// FoobarBluetoothService:
+//
+// Service managing the state of the bluetooth adapter. This is implemented using the Bluez DBus API.
+//
+
+struct _FoobarBluetoothService
+{
+ GObject parent_instance;
+ GDBusObjectManager* object_manager;
+ GListStore* devices;
+ GtkFilterListModel* filtered_devices;
+ GtkSortListModel* sorted_devices;
+ GtkFilterListModel* connected_devices;
+ GPtrArray* adapters; // First adapter is the default one.
+ FoobarBluezAdapter* default_adapter;
+ gulong interface_added_handler_id;
+ gulong interface_removed_handler_id;
+ gulong object_added_handler_id;
+ gulong object_removed_handler_id;
+ gulong adapter_notify_handler_id;
+};
+
+enum
+{
+ PROP_DEVICES = 1,
+ PROP_CONNECTED_DEVICES,
+ PROP_IS_AVAILABLE,
+ PROP_IS_ENABLED,
+ PROP_IS_SCANNING,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_bluetooth_service_class_init ( FoobarBluetoothServiceClass* klass );
+static void foobar_bluetooth_service_init ( FoobarBluetoothService* self );
+static void foobar_bluetooth_service_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_bluetooth_service_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_bluetooth_service_finalize ( GObject* object );
+static void foobar_bluetooth_service_handle_interface_added ( GDBusObjectManager* manager,
+ GDBusObject* object,
+ GDBusInterface* interface,
+ gpointer userdata );
+static void foobar_bluetooth_service_handle_interface_removed( GDBusObjectManager* manager,
+ GDBusObject* object,
+ GDBusInterface* interface,
+ gpointer userdata );
+static void foobar_bluetooth_service_handle_object_added ( GDBusObjectManager* manager,
+ GDBusObject* object,
+ gpointer userdata );
+static void foobar_bluetooth_service_handle_object_removed ( GDBusObjectManager* manager,
+ GDBusObject* object,
+ gpointer userdata );
+static void foobar_bluetooth_service_handle_adapter_notify ( GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata );
+static void foobar_bluetooth_service_update_default_adapter ( FoobarBluetoothService* self );
+static GType foobar_bluetooth_service_dbus_proxy_type_cb ( GDBusObjectManagerClient* manager,
+ gchar const* object_path,
+ gchar const* interface_name,
+ gpointer userdata );
+static gboolean foobar_bluetooth_service_filter_func ( gpointer item,
+ gpointer userdata );
+static gboolean foobar_bluetooth_service_connected_filter_func ( gpointer item,
+ gpointer userdata );
+static gint foobar_bluetooth_service_sort_func ( gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarBluetoothService, foobar_bluetooth_service, G_TYPE_OBJECT )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Device
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for devices.
+//
+void foobar_bluetooth_device_class_init( FoobarBluetoothDeviceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_bluetooth_device_get_property;
+ object_klass->finalize = foobar_bluetooth_device_finalize;
+
+ device_props[DEVICE_PROP_NAME] = g_param_spec_string(
+ "name",
+ "Name",
+ "Human-readable name for the device.",
+ NULL,
+ G_PARAM_READABLE );
+ device_props[DEVICE_PROP_STATE] = g_param_spec_enum(
+ "state",
+ "State",
+ "Current connection state of the device.",
+ FOOBAR_TYPE_BLUETOOTH_DEVICE_STATE,
+ 0,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_DEVICE_PROPS, device_props );
+}
+
+//
+// Instance initialization for devices.
+//
+void foobar_bluetooth_device_init( FoobarBluetoothDevice* self )
+{
+ (void)self;
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_bluetooth_device_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarBluetoothDevice* self = (FoobarBluetoothDevice*)object;
+
+ switch ( prop_id )
+ {
+ case DEVICE_PROP_NAME:
+ g_value_set_string( value, foobar_bluetooth_device_get_name( self ) );
+ break;
+ case DEVICE_PROP_STATE:
+ g_value_set_enum( value, foobar_bluetooth_device_get_state( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for devices.
+//
+void foobar_bluetooth_device_finalize( GObject* object )
+{
+ FoobarBluetoothDevice* self = (FoobarBluetoothDevice*)object;
+
+ g_clear_signal_handler( &self->notify_handler_id, self->device );
+ g_clear_object( &self->device );
+ g_clear_object( &self->connect_cancellable );
+
+ G_OBJECT_CLASS( foobar_bluetooth_device_parent_class )->finalize( object );
+}
+
+//
+// Create a new device object, wrapping a DBus device instance.
+//
+FoobarBluetoothDevice* foobar_bluetooth_device_new(
+ FoobarBluetoothService* service,
+ FoobarBluezDevice* device )
+{
+ FoobarBluetoothDevice* self = g_object_new( FOOBAR_TYPE_BLUETOOTH_DEVICE, NULL );
+
+ self->service = service;
+ self->device = g_object_ref( device );
+ foobar_bluetooth_device_update_state( self );
+
+ self->notify_handler_id = g_signal_connect(
+ self->device, "notify", G_CALLBACK( foobar_bluetooth_device_handle_notify ), self );
+
+ return self;
+}
+
+//
+// Get the name/alias for the device.
+//
+gchar const* foobar_bluetooth_device_get_name( FoobarBluetoothDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_DEVICE( self ), NULL );
+ return foobar_bluez_device_get_alias( self->device );
+}
+
+//
+// Get the current connection state for the device.
+//
+FoobarBluetoothDeviceState foobar_bluetooth_device_get_state( FoobarBluetoothDevice* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_DEVICE( self ), 0 );
+ return self->state;
+}
+
+//
+// Toggle whether the device is connected:
+//
+// - If the device is currently disconnected, it is connected.
+// - If the device is currently being connected, that process is aborted.
+// - If the device is currently connected, it is disconnected.
+//
+void foobar_bluetooth_device_toggle_connection( FoobarBluetoothDevice* self )
+{
+ g_return_if_fail( FOOBAR_IS_BLUETOOTH_DEVICE( self ) );
+
+ switch ( self->state )
+ {
+ case FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED:
+ self->connect_cancellable = g_cancellable_new( );
+ foobar_bluetooth_device_update_state( self );
+ foobar_bluez_device_call_connect(
+ self->device,
+ self->connect_cancellable,
+ foobar_bluetooth_device_connect_cb,
+ g_object_ref( self ) );
+ break;
+ case FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING:
+ g_cancellable_cancel( self->connect_cancellable );
+ break;
+ case FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED:
+ foobar_bluez_device_call_disconnect(
+ self->device,
+ NULL,
+ foobar_bluetooth_device_disconnect_cb,
+ g_object_ref( self ) );
+ break;
+ default:
+ g_warn_if_reached( );
+ }
+}
+
+//
+// Called by the underlying device when one of its properties changes.
+//
+void foobar_bluetooth_device_handle_notify(
+ GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)object;
+ FoobarBluetoothDevice* self = (FoobarBluetoothDevice*)userdata;
+
+ gchar const* property = g_param_spec_get_name( pspec );
+ if ( !g_strcmp0( property, "alias" ) )
+ {
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_NAME] );
+ }
+ else if ( !g_strcmp0( property, "name" ) || !g_strcmp0( property, "adapter" ) )
+ {
+ if ( self->service )
+ {
+ gtk_filter_changed(
+ gtk_filter_list_model_get_filter( self->service->filtered_devices ),
+ GTK_FILTER_CHANGE_DIFFERENT );
+ }
+ }
+ else if ( !g_strcmp0( property, "connected" ) )
+ {
+ foobar_bluetooth_device_update_state( self );
+ }
+}
+
+//
+// Called when a request to connect the device is completed.
+//
+// This is mainly used to update the device's state.
+//
+void foobar_bluetooth_device_connect_cb(
+ GObject* object,
+ GAsyncResult* result,
+ gpointer userdata )
+{
+ (void)object;
+ g_autoptr( FoobarBluetoothDevice ) self = (FoobarBluetoothDevice*)userdata;
+
+ g_autoptr( GError ) error = NULL;
+ if ( !foobar_bluez_device_call_connect_finish( self->device, result, &error ) )
+ {
+ g_warning( "Unable to connect bluetooth device: %s", error->message );
+ }
+
+ g_clear_object( &self->connect_cancellable );
+ foobar_bluetooth_device_update_state( self );
+}
+
+//
+// Called when a request to disconnect the device is completed.
+//
+void foobar_bluetooth_device_disconnect_cb(
+ GObject* object,
+ GAsyncResult* result,
+ gpointer userdata )
+{
+ (void)object;
+ g_autoptr( FoobarBluetoothDevice ) self = (FoobarBluetoothDevice*)userdata;
+
+ g_autoptr( GError ) error = NULL;
+ if ( !foobar_bluez_device_call_disconnect_finish( self->device, result, &error ) )
+ {
+ g_warning( "Unable to disconnect bluetooth device: %s", error->message );
+ }
+}
+
+//
+// Update the current device state.
+//
+// The connect_cancellable member variable is used to indicate an ongoing connection attempt. Otherwise, the value of
+// is_connected is used.
+//
+void foobar_bluetooth_device_update_state( FoobarBluetoothDevice* self )
+{
+ FoobarBluetoothDeviceState new_state = foobar_bluez_device_get_connected( self->device )
+ ? FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED
+ : FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED;
+ if ( self->connect_cancellable )
+ {
+ new_state = FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING;
+ }
+
+ if ( self->state != new_state )
+ {
+ self->state = new_state;
+ g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_STATE] );
+
+ if ( self->service )
+ {
+ gtk_filter_changed(
+ gtk_filter_list_model_get_filter( self->service->connected_devices ),
+ GTK_FILTER_CHANGE_DIFFERENT );
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Service Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the bluetooth service.
+//
+void foobar_bluetooth_service_class_init( FoobarBluetoothServiceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_bluetooth_service_get_property;
+ object_klass->set_property = foobar_bluetooth_service_set_property;
+ object_klass->finalize = foobar_bluetooth_service_finalize;
+
+ props[PROP_DEVICES] = g_param_spec_object(
+ "devices",
+ "Devices",
+ "Sorted list of available bluetooth devices.",
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READABLE );
+ props[PROP_CONNECTED_DEVICES] = g_param_spec_object(
+ "connected-devices",
+ "Connected Devices",
+ "Sorted list of currently connected bluetooth devices.",
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READABLE );
+ props[PROP_IS_AVAILABLE] = g_param_spec_boolean(
+ "is-available",
+ "Is Available",
+ "Indicates whether the system has a bluetooth adapter.",
+ FALSE,
+ G_PARAM_READABLE );
+ props[PROP_IS_ENABLED] = g_param_spec_boolean(
+ "is-enabled",
+ "Is Enabled",
+ "Indicates whether the bluetooth adapter is currently enabled.",
+ FALSE,
+ G_PARAM_READWRITE );
+ props[PROP_IS_SCANNING] = g_param_spec_boolean(
+ "is-scanning",
+ "Is Scanning",
+ "Indicates whether the bluetooth adapter is currently scanning for devices.",
+ FALSE,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for the bluetooth service.
+//
+void foobar_bluetooth_service_init( FoobarBluetoothService* self )
+{
+ self->devices = g_list_store_new( FOOBAR_TYPE_BLUETOOTH_DEVICE );
+
+ GtkCustomFilter* filter = gtk_custom_filter_new( foobar_bluetooth_service_filter_func, self, NULL );
+ self->filtered_devices = gtk_filter_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->devices ) ),
+ GTK_FILTER( filter ) );
+
+ GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_bluetooth_service_sort_func, NULL, NULL );
+ self->sorted_devices = gtk_sort_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->filtered_devices ) ),
+ GTK_SORTER( sorter ) );
+
+ GtkCustomFilter* connected_filter = gtk_custom_filter_new( foobar_bluetooth_service_connected_filter_func, NULL, NULL );
+ self->connected_devices = gtk_filter_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->sorted_devices ) ),
+ GTK_FILTER( connected_filter ) );
+
+ self->adapters = g_ptr_array_new_with_free_func( g_object_unref );
+
+ g_autoptr( GError ) error = NULL;
+ self->object_manager = g_dbus_object_manager_client_new_for_bus_sync(
+ G_BUS_TYPE_SYSTEM,
+ G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START,
+ "org.bluez",
+ "/",
+ foobar_bluetooth_service_dbus_proxy_type_cb,
+ NULL,
+ NULL,
+ NULL,
+ &error );
+
+ if ( self->object_manager )
+ {
+ self->interface_added_handler_id = g_signal_connect(
+ self->object_manager,
+ "interface-added",
+ G_CALLBACK( foobar_bluetooth_service_handle_interface_added ),
+ self );
+ self->interface_removed_handler_id = g_signal_connect(
+ self->object_manager,
+ "interface-removed",
+ G_CALLBACK( foobar_bluetooth_service_handle_interface_removed ),
+ self );
+ self->object_added_handler_id = g_signal_connect(
+ self->object_manager,
+ "object-added",
+ G_CALLBACK( foobar_bluetooth_service_handle_object_added ),
+ self );
+ self->object_removed_handler_id = g_signal_connect(
+ self->object_manager,
+ "object-removed",
+ G_CALLBACK( foobar_bluetooth_service_handle_object_removed ),
+ self );
+
+ g_autolist( GDBusObject ) objects = g_dbus_object_manager_get_objects( self->object_manager );
+ for ( GList* it = objects; it; it = it->next )
+ {
+ foobar_bluetooth_service_handle_object_added( self->object_manager, it->data, self );
+ }
+ }
+ else
+ {
+ g_warning( "Unable to create a DBus object manager for communication with Bluez: %s", error->message );
+ }
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_bluetooth_service_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarBluetoothService* self = (FoobarBluetoothService*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_DEVICES:
+ g_value_set_object( value, foobar_bluetooth_service_get_devices( self ) );
+ break;
+ case PROP_CONNECTED_DEVICES:
+ g_value_set_object( value, foobar_bluetooth_service_get_connected_devices( self ) );
+ break;
+ case PROP_IS_AVAILABLE:
+ g_value_set_boolean( value, foobar_bluetooth_service_is_available( self ) );
+ break;
+ case PROP_IS_ENABLED:
+ g_value_set_boolean( value, foobar_bluetooth_service_is_enabled( self ) );
+ break;
+ case PROP_IS_SCANNING:
+ g_value_set_boolean( value, foobar_bluetooth_service_is_scanning( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_bluetooth_service_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarBluetoothService* self = (FoobarBluetoothService*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_IS_ENABLED:
+ foobar_bluetooth_service_set_enabled( self, g_value_get_boolean( value ) );
+ break;
+ case PROP_IS_SCANNING:
+ foobar_bluetooth_service_set_scanning( self, g_value_get_boolean( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for the bluetooth service.
+//
+void foobar_bluetooth_service_finalize( GObject* object )
+{
+ FoobarBluetoothService* self = (FoobarBluetoothService*)object;
+
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i )
+ {
+ FoobarBluetoothDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i );
+ device->service = NULL;
+ }
+
+ g_clear_signal_handler( &self->adapter_notify_handler_id, self->default_adapter );
+ g_clear_signal_handler( &self->interface_added_handler_id, self->object_manager );
+ g_clear_signal_handler( &self->interface_removed_handler_id, self->object_manager );
+ g_clear_signal_handler( &self->object_added_handler_id, self->object_manager );
+ g_clear_signal_handler( &self->object_removed_handler_id, self->object_manager );
+ g_clear_object( &self->object_manager );
+ g_clear_object( &self->connected_devices );
+ g_clear_object( &self->sorted_devices );
+ g_clear_object( &self->filtered_devices );
+ g_clear_object( &self->devices );
+ g_clear_object( &self->default_adapter );
+ g_clear_pointer( &self->adapters, g_ptr_array_unref );
+
+ G_OBJECT_CLASS( foobar_bluetooth_service_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new bluetooth service instance.
+//
+FoobarBluetoothService* foobar_bluetooth_service_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_BLUETOOTH_SERVICE, NULL );
+}
+
+//
+// Get the sorted list of available bluetooth devices.
+//
+GListModel* foobar_bluetooth_service_get_devices( FoobarBluetoothService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), NULL );
+ return G_LIST_MODEL( self->sorted_devices );
+}
+
+//
+// Get the sorted list of currently connected bluetooth devices.
+//
+GListModel* foobar_bluetooth_service_get_connected_devices( FoobarBluetoothService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), NULL );
+ return G_LIST_MODEL( self->connected_devices );
+}
+
+//
+// Check whether the system has a bluetooth adapter.
+//
+gboolean foobar_bluetooth_service_is_available( FoobarBluetoothService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), FALSE );
+ return self->default_adapter != NULL;
+}
+
+//
+// Check whether the bluetooth adapter is currently enabled.
+//
+gboolean foobar_bluetooth_service_is_enabled( FoobarBluetoothService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), FALSE );
+ return self->default_adapter ? foobar_bluez_adapter_get_powered( self->default_adapter ) : FALSE;
+}
+
+//
+// Check whether the bluetooth adapter is currently scanning for devices.
+//
+gboolean foobar_bluetooth_service_is_scanning( FoobarBluetoothService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), FALSE );
+ return self->default_adapter ? foobar_bluez_adapter_get_discovering( self->default_adapter ) : FALSE;
+}
+
+//
+// Update whether the bluetooth adapter is currently enabled.
+//
+void foobar_bluetooth_service_set_enabled(
+ FoobarBluetoothService* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ) );
+
+ if (self->default_adapter)
+ {
+ foobar_bluez_adapter_set_powered( self->default_adapter, value );
+ }
+}
+
+//
+// Update whether the bluetooth adapter is currently scanning for devices.
+//
+void foobar_bluetooth_service_set_scanning(
+ FoobarBluetoothService* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ) );
+
+ g_autoptr( GError ) error = NULL;
+
+ if (self->default_adapter && foobar_bluetooth_service_is_scanning( self ) != value)
+ {
+ if (value)
+ {
+ // Make this device discoverable while discovery is active.
+
+ GVariantBuilder filter_builder;
+ g_variant_builder_init( &filter_builder, G_VARIANT_TYPE_VARDICT );
+ g_variant_builder_add( &filter_builder, "{sv}", "Discoverable", g_variant_new_boolean( TRUE ) );
+ GVariant* filter = g_variant_builder_end( &filter_builder );
+ if ( !foobar_bluez_adapter_call_set_discovery_filter_sync( self->default_adapter, filter, NULL, &error ) )
+ {
+ g_warning( "Failed to set discovery filter: %s", error->message );
+ return;
+ }
+ if ( !foobar_bluez_adapter_call_start_discovery_sync( self->default_adapter, NULL, &error ) )
+ {
+ g_warning( "Failed to start discovery: %s", error->message );
+ }
+ }
+ else
+ {
+ if ( !foobar_bluez_adapter_call_stop_discovery_sync( self->default_adapter, NULL, &error ) )
+ {
+ g_warning( "Failed to stop discovery: %s", error->message );
+ }
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called when an interface was added to an existing DBus object (either adapter or device).
+//
+void foobar_bluetooth_service_handle_interface_added(
+ GDBusObjectManager* manager,
+ GDBusObject* object,
+ GDBusInterface* interface,
+ gpointer userdata )
+{
+ (void)manager;
+ (void)object;
+ FoobarBluetoothService* self = (FoobarBluetoothService*)userdata;
+
+ if ( FOOBAR_IS_BLUEZ_DEVICE( interface ) )
+ {
+ g_autoptr( FoobarBluetoothDevice ) device = foobar_bluetooth_device_new(
+ self,
+ FOOBAR_BLUEZ_DEVICE( interface ) );
+ g_list_store_append( self->devices, device );
+ }
+ else if ( FOOBAR_IS_BLUEZ_ADAPTER( interface ) )
+ {
+ g_ptr_array_add( self->adapters, g_object_ref( interface ) );
+ foobar_bluetooth_service_update_default_adapter( self );
+ }
+}
+
+//
+// Called when an interface was removed from an existing DBus object (either adapter or device).
+//
+void foobar_bluetooth_service_handle_interface_removed(
+ GDBusObjectManager* manager,
+ GDBusObject* object,
+ GDBusInterface* interface,
+ gpointer userdata )
+{
+ (void)manager;
+ FoobarBluetoothService* self = (FoobarBluetoothService*)userdata;
+
+ gchar const* removed_path = g_dbus_object_get_object_path( object );
+ if ( FOOBAR_IS_BLUEZ_DEVICE( interface ) )
+ {
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i )
+ {
+ FoobarBluetoothDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i );
+ gchar const* path = g_dbus_proxy_get_object_path( G_DBUS_PROXY( device->device ) );
+ if ( !g_strcmp0( path, removed_path ) )
+ {
+ device->service = NULL;
+ g_list_store_remove( self->devices, i );
+ break;
+ }
+ }
+ }
+ else if ( FOOBAR_IS_BLUEZ_ADAPTER( interface ) )
+ {
+ for ( guint i = 0; i < self->adapters->len; ++i )
+ {
+ FoobarBluezAdapter* adapter = g_ptr_array_index( self->adapters, i );
+ gchar const* path = g_dbus_proxy_get_object_path( G_DBUS_PROXY( adapter ) );
+ if ( !g_strcmp0( path, removed_path ) )
+ {
+ g_ptr_array_remove_index( self->adapters, i );
+ break;
+ }
+ }
+ foobar_bluetooth_service_update_default_adapter( self );
+ }
+}
+
+//
+// Called when a DBus object was added (either adapter or device).
+//
+void foobar_bluetooth_service_handle_object_added(
+ GDBusObjectManager* manager,
+ GDBusObject* object,
+ gpointer userdata )
+{
+ g_autolist( GDBusInterface ) interfaces = g_dbus_object_get_interfaces( object );
+ for ( GList* it = interfaces; it; it = it->next )
+ {
+ foobar_bluetooth_service_handle_interface_added( manager, object, it->data, userdata );
+ }
+}
+
+//
+// Called when a DBus object was removed (either adapter or device).
+//
+void foobar_bluetooth_service_handle_object_removed(
+ GDBusObjectManager* manager,
+ GDBusObject* object,
+ gpointer userdata )
+{
+ g_autolist( GDBusInterface ) interfaces = g_dbus_object_get_interfaces( object );
+ for ( GList* it = interfaces; it; it = it->next )
+ {
+ foobar_bluetooth_service_handle_interface_removed( manager, object, it->data, userdata );
+ }
+}
+
+//
+// Called when a property of the default adapter has changed.
+//
+void foobar_bluetooth_service_handle_adapter_notify(
+ GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)object;
+ FoobarBluetoothService* self = (FoobarBluetoothService*)userdata;
+
+ gchar const* property = g_param_spec_get_name( pspec );
+ if ( !g_strcmp0( property, "powered" ) )
+ {
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_ENABLED] );
+ }
+ else if ( !g_strcmp0( property, "discovering" ) )
+ {
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_SCANNING] );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Update the default_adapter to match the first item in the adapters array.
+//
+void foobar_bluetooth_service_update_default_adapter( FoobarBluetoothService* self )
+{
+ FoobarBluezAdapter* new_adapter = ( self->adapters->len > 0 ) ? g_ptr_array_index( self->adapters, 0 ) : NULL;
+
+ if ( self->default_adapter != new_adapter )
+ {
+ g_clear_signal_handler( &self->adapter_notify_handler_id, self->default_adapter );
+ g_clear_object( &self->default_adapter );
+
+ if ( new_adapter )
+ {
+ self->default_adapter = g_object_ref( new_adapter );
+ self->adapter_notify_handler_id = g_signal_connect(
+ self->default_adapter,
+ "notify",
+ G_CALLBACK( foobar_bluetooth_service_handle_adapter_notify ),
+ self );
+ }
+
+ gtk_filter_changed( gtk_filter_list_model_get_filter( self->filtered_devices ), GTK_FILTER_CHANGE_DIFFERENT );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_AVAILABLE] );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_ENABLED] );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_SCANNING] );
+ }
+}
+
+//
+// Get the type for creating a DBus proxy object.
+//
+// This is used because the DBus object manager should give us instances of the FoobarBluezAdapterProxy and
+// FoobarBluezDeviceProxy types.
+//
+GType foobar_bluetooth_service_dbus_proxy_type_cb(
+ GDBusObjectManagerClient* manager,
+ gchar const* object_path,
+ gchar const* interface_name,
+ gpointer userdata )
+{
+ if ( !interface_name )
+ {
+ return G_TYPE_DBUS_OBJECT_PROXY;
+ }
+
+ if ( !g_strcmp0( interface_name, "org.bluez.Device1" ) )
+ {
+ return FOOBAR_TYPE_BLUEZ_DEVICE_PROXY;
+ }
+
+ if ( !g_strcmp0( interface_name, "org.bluez.Adapter1" ) )
+ {
+ return FOOBAR_TYPE_BLUEZ_ADAPTER_PROXY;
+ }
+
+ return G_TYPE_DBUS_PROXY;
+}
+
+//
+// Filtering callback for the list of devices, only showing devices from the default adapter.
+//
+gboolean foobar_bluetooth_service_filter_func(
+ gpointer item,
+ gpointer userdata )
+{
+ FoobarBluetoothService* self = (FoobarBluetoothService*)userdata;
+ FoobarBluetoothDevice* device = item;
+
+ gchar const* device_adapter = foobar_bluez_device_get_adapter( device->device );
+ gchar const* default_adapter = self->default_adapter
+ ? g_dbus_proxy_get_object_path( G_DBUS_PROXY( self->default_adapter ) )
+ : NULL;
+ gchar const* name = foobar_bluez_device_get_name( device->device );
+ return name != NULL && !g_strcmp0( device_adapter, default_adapter );
+}
+
+//
+// Filtering callback for the list of connected devices.
+//
+gboolean foobar_bluetooth_service_connected_filter_func(
+ gpointer item,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarBluetoothDevice* device = item;
+ return foobar_bluetooth_device_get_state( device ) == FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED;
+}
+
+//
+// Sorting callback for the list of devices.
+//
+// Devices are sorted by their names.
+//
+gint foobar_bluetooth_service_sort_func(
+ gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarBluetoothDevice* device_a = (FoobarBluetoothDevice*)item_a;
+ FoobarBluetoothDevice* device_b = (FoobarBluetoothDevice*)item_b;
+ return g_strcmp0( foobar_bluetooth_device_get_name( device_a ), foobar_bluetooth_device_get_name( device_b ) );
+} \ No newline at end of file
diff --git a/src/services/bluetooth-service.h b/src/services/bluetooth-service.h
new file mode 100644
index 0000000..05083f2
--- /dev/null
+++ b/src/services/bluetooth-service.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_BLUETOOTH_DEVICE_STATE foobar_bluetooth_device_state_get_type( )
+#define FOOBAR_TYPE_BLUETOOTH_DEVICE foobar_bluetooth_device_get_type( )
+#define FOOBAR_TYPE_BLUETOOTH_ADAPTER foobar_bluetooth_adapter_get_type( )
+#define FOOBAR_TYPE_BLUETOOTH_SERVICE foobar_bluetooth_service_get_type( )
+
+typedef enum
+{
+ FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED,
+ FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING,
+ FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED,
+} FoobarBluetoothDeviceState;
+
+GType foobar_bluetooth_device_state_get_type( void );
+
+G_DECLARE_FINAL_TYPE( FoobarBluetoothDevice, foobar_bluetooth_device, FOOBAR, BLUETOOTH_DEVICE, GObject )
+
+gchar const* foobar_bluetooth_device_get_name ( FoobarBluetoothDevice* self );
+FoobarBluetoothDeviceState foobar_bluetooth_device_get_state ( FoobarBluetoothDevice* self );
+void foobar_bluetooth_device_toggle_connection( FoobarBluetoothDevice* self );
+
+G_DECLARE_FINAL_TYPE( FoobarBluetoothService, foobar_bluetooth_service, FOOBAR, BLUETOOTH_SERVICE, GObject )
+
+FoobarBluetoothService* foobar_bluetooth_service_new ( void );
+GListModel* foobar_bluetooth_service_get_devices ( FoobarBluetoothService* self );
+GListModel* foobar_bluetooth_service_get_connected_devices( FoobarBluetoothService* self );
+gboolean foobar_bluetooth_service_is_available ( FoobarBluetoothService* self );
+gboolean foobar_bluetooth_service_is_enabled ( FoobarBluetoothService* self );
+gboolean foobar_bluetooth_service_is_scanning ( FoobarBluetoothService* self );
+void foobar_bluetooth_service_set_enabled ( FoobarBluetoothService* self,
+ gboolean value );
+void foobar_bluetooth_service_set_scanning ( FoobarBluetoothService* self,
+ gboolean value );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/services/brightness-service.c b/src/services/brightness-service.c
new file mode 100644
index 0000000..32f0501
--- /dev/null
+++ b/src/services/brightness-service.c
@@ -0,0 +1,391 @@
+#include "services/brightness-service.h"
+#include <gio/gio.h>
+#include <math.h>
+
+//
+// FoobarBrightnessService:
+//
+// Service managing the brightness level. This is implemented by
+// - for read access: monitoring the "brightness" file in a "/sys/class/backlight" subdirectory,
+// - for write access: invoking the "brightnessctl" command.
+//
+
+struct _FoobarBrightnessService
+{
+ GObject parent_instance;
+ gint percentage;
+ GFileMonitor* file_monitor;
+ gulong file_monitor_handler_id;
+ gchar* device_name;
+ gchar* file_path;
+ gint max_brightness;
+};
+
+enum
+{
+ PROP_PERCENTAGE = 1,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_brightness_service_class_init ( FoobarBrightnessServiceClass* klass );
+static void foobar_brightness_service_init ( FoobarBrightnessService* self );
+static void foobar_brightness_service_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_brightness_service_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_brightness_service_finalize ( GObject* object );
+static void foobar_brightness_service_handle_changed ( GFileMonitor* monitor,
+ GFile* file,
+ GFile* other_file,
+ GFileMonitorEvent event_type,
+ gpointer userdata );
+static gint foobar_brightness_service_load_percentage ( FoobarBrightnessService* self );
+static void foobar_brightness_service_write_percentage ( FoobarBrightnessService* self );
+static void foobar_brightness_service_write_percentage_watch_cb( GPid pid,
+ gint status,
+ gpointer userdata );
+static void foobar_brightness_service_get_info ( gchar** out_device_name,
+ gchar** out_file_path,
+ gint* out_max_brightness );
+static gint foobar_brightness_service_read_value ( gchar const* path );
+static GFileMonitor* foobar_brightness_service_monitor ( gchar const* path );
+
+G_DEFINE_FINAL_TYPE( FoobarBrightnessService, foobar_brightness_service, G_TYPE_OBJECT )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Service Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the brightness service.
+//
+void foobar_brightness_service_class_init( FoobarBrightnessServiceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_brightness_service_get_property;
+ object_klass->set_property = foobar_brightness_service_set_property;
+ object_klass->finalize = foobar_brightness_service_finalize;
+
+ props[PROP_PERCENTAGE] = g_param_spec_int(
+ "percentage",
+ "Percentage",
+ "Current brightness percentage.",
+ 0,
+ 100,
+ 100,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for the brightness service.
+//
+void foobar_brightness_service_init( FoobarBrightnessService* self )
+{
+ foobar_brightness_service_get_info( &self->device_name, &self->file_path, &self->max_brightness );
+ self->percentage = foobar_brightness_service_load_percentage( self );
+ if ( self->file_path )
+ {
+ self->file_monitor = foobar_brightness_service_monitor( self->file_path );
+ }
+ if ( self->file_monitor )
+ {
+ self->file_monitor_handler_id = g_signal_connect(
+ self->file_monitor,
+ "changed",
+ G_CALLBACK( foobar_brightness_service_handle_changed ),
+ self );
+ }
+ else
+ {
+ self->file_monitor_handler_id = 0;
+ }
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_brightness_service_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarBrightnessService* self = (FoobarBrightnessService*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_PERCENTAGE:
+ g_value_set_int( value, foobar_brightness_service_get_percentage( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_brightness_service_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarBrightnessService* self = (FoobarBrightnessService*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_PERCENTAGE:
+ foobar_brightness_service_set_percentage( self, g_value_get_int( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for the brightness service.
+//
+void foobar_brightness_service_finalize( GObject* object )
+{
+ FoobarBrightnessService* self = (FoobarBrightnessService*)object;
+
+ g_clear_signal_handler( &self->file_monitor_handler_id, self->file_monitor );
+ g_clear_object( &self->file_monitor );
+ g_clear_pointer( &self->device_name, g_free );
+ g_clear_pointer( &self->file_path, g_free );
+
+ G_OBJECT_CLASS( foobar_brightness_service_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new brightness service instance.
+//
+FoobarBrightnessService* foobar_brightness_service_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_BRIGHTNESS_SERVICE, NULL );
+}
+
+//
+// Get the current brightness percentage value.
+//
+gint foobar_brightness_service_get_percentage( FoobarBrightnessService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_BRIGHTNESS_SERVICE( self ), 0 );
+
+ return self->percentage;
+}
+
+//
+// Update the current brightness percentage value.
+//
+void foobar_brightness_service_set_percentage(
+ FoobarBrightnessService* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_BRIGHTNESS_SERVICE( self ) );
+
+ value = CLAMP( value, 0, 100 );
+ if ( self->percentage != value )
+ {
+ self->percentage = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_PERCENTAGE] );
+ foobar_brightness_service_write_percentage( self );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called by the file monitor when the contents of the "brightness" file have changed.
+//
+void foobar_brightness_service_handle_changed(
+ GFileMonitor* monitor,
+ GFile* file,
+ GFile* other_file,
+ GFileMonitorEvent event_type,
+ gpointer userdata )
+{
+ (void)monitor;
+ (void)file;
+ (void)other_file;
+ (void)event_type;
+ FoobarBrightnessService* self = (FoobarBrightnessService*)userdata;
+
+ gint new_percentage = foobar_brightness_service_load_percentage( self );
+ if ( self->percentage != new_percentage )
+ {
+ self->percentage = new_percentage;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_PERCENTAGE] );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Load the current percentage value from the file at self->file_path.
+//
+// This requires file_path and max_brightness to be initialized, otherwise the brightness is assumed to be 100%.
+//
+gint foobar_brightness_service_load_percentage( FoobarBrightnessService* self )
+{
+ if ( !self->file_path || self->max_brightness == -1 ) { return 100; }
+
+ gint value = foobar_brightness_service_read_value( self->file_path );
+ if ( value == -1 ) { return 100; }
+
+ gdouble rel = value / (gdouble)self->max_brightness;
+ gint percentage = (gint)round( rel * 100. );
+ return CLAMP( percentage, 0, 100 );
+}
+
+//
+// Asynchronously update the current percentage value by invoking brightnessctl.
+//
+// This requires device_name to be initialized.
+//
+void foobar_brightness_service_write_percentage( FoobarBrightnessService* self )
+{
+ if ( !self->device_name ) { return; }
+
+ g_autofree gchar* percentage_string = g_strdup_printf( "%d%%", self->percentage );
+ gchar* args[] = { "brightnessctl", "-d", self->device_name, "s", percentage_string, NULL };
+
+ g_autoptr( GError ) error = NULL;
+ GPid child_pid;
+ if ( !g_spawn_async(
+ NULL,
+ args,
+ NULL,
+ G_SPAWN_DO_NOT_REAP_CHILD
+ | G_SPAWN_SEARCH_PATH
+ | G_SPAWN_STDIN_FROM_DEV_NULL
+ | G_SPAWN_STDOUT_TO_DEV_NULL
+ | G_SPAWN_STDERR_TO_DEV_NULL,
+ NULL,
+ NULL,
+ &child_pid,
+ &error ) )
+ {
+ g_warning( "Unable to spawn brightnessctl: %s", error->message );
+ return;
+ }
+
+ g_child_watch_add( child_pid, foobar_brightness_service_write_percentage_watch_cb, NULL );
+}
+
+//
+// Called when the brightnessctl child process has finished.
+//
+void foobar_brightness_service_write_percentage_watch_cb(
+ GPid pid,
+ gint status,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ g_autoptr( GError ) error = NULL;
+ if ( !g_spawn_check_wait_status( status, &error ) )
+ {
+ g_warning( "Unable to set brightness with brightnessctl: %s", error->message );
+ }
+
+ g_spawn_close_pid( pid );
+}
+
+//
+// Initialize information for the brightness device.
+//
+// This looks for the first directory in the /sys/class/backlight directories and uses it as the device name. It also
+// populates out_file_path to the path of this directory and out_max_brightness to the contents of the corresponding
+// max_brightness file.
+//
+void foobar_brightness_service_get_info(
+ gchar** out_device_name,
+ gchar** out_file_path,
+ gint* out_max_brightness )
+{
+ *out_device_name = NULL;
+ *out_file_path = NULL;
+ *out_max_brightness = -1;
+
+ g_autoptr( GFile ) dir = g_file_new_for_path( "/sys/class/backlight" );
+ if ( !dir ) { return; }
+
+ g_autoptr( GFileEnumerator ) enumerator = g_file_enumerate_children(
+ dir,
+ "standard::",
+ G_FILE_QUERY_INFO_NONE,
+ NULL,
+ NULL );
+ if ( !enumerator ) { return; }
+
+ g_autoptr( GFileInfo ) info = g_file_enumerator_next_file( enumerator, NULL, NULL );
+ g_file_enumerator_close( enumerator, NULL, NULL );
+ if ( !info ) { return; }
+
+ gchar* device_name = g_strdup( g_file_info_get_name( info ) );
+ gchar* current_file_path = g_strdup_printf( "/sys/class/backlight/%s/brightness", device_name );
+ gchar* max_file_path = g_strdup_printf( "/sys/class/backlight/%s/max_brightness", device_name );
+ *out_device_name = device_name;
+ *out_file_path = current_file_path;
+ *out_max_brightness = foobar_brightness_service_read_value( max_file_path );
+ g_free( max_file_path );
+}
+
+//
+// Read the current brightness integer (not percentage) from the given file path.
+//
+gint foobar_brightness_service_read_value( gchar const* path )
+{
+ g_autofree gchar* contents = NULL;
+ g_autoptr( GError ) error = NULL;
+ if ( !g_file_get_contents( path, &contents, NULL, &error ) )
+ {
+ g_warning( "Unable to read %s: %s", path, error->message );
+ return -1;
+ }
+
+ errno = 0;
+ char* endptr;
+ long result = strtol( contents, &endptr, 10 );
+ if ( errno != 0 || endptr == contents || result > INT_MAX || result < 0 )
+ {
+ g_warning( "Expected integer in %s, but got: %s", path, contents );
+ return -1;
+ }
+
+ return (gint)result;
+}
+
+//
+// Set up a file monitor for the given path.
+//
+GFileMonitor* foobar_brightness_service_monitor( gchar const* path )
+{
+ g_autoptr( GFile ) file = g_file_new_for_path( path );
+ if ( !file ) { return NULL; }
+
+ g_autoptr( GError ) error = NULL;
+ GFileMonitor* monitor = g_file_monitor_file( file, G_FILE_MONITOR_NONE, NULL, &error );
+ if ( !monitor ) { g_warning( "Unable to monitor brightness: %s", error->message ); }
+
+ return monitor;
+}
diff --git a/src/services/brightness-service.h b/src/services/brightness-service.h
new file mode 100644
index 0000000..5fa5538
--- /dev/null
+++ b/src/services/brightness-service.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_BRIGHTNESS_SERVICE foobar_brightness_service_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarBrightnessService, foobar_brightness_service, FOOBAR, BRIGHTNESS_SERVICE, GObject )
+
+FoobarBrightnessService* foobar_brightness_service_new ( void );
+gint foobar_brightness_service_get_percentage( FoobarBrightnessService* self );
+void foobar_brightness_service_set_percentage( FoobarBrightnessService* self,
+ gint percentage );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/services/clock-service.c b/src/services/clock-service.c
new file mode 100644
index 0000000..421ea7d
--- /dev/null
+++ b/src/services/clock-service.c
@@ -0,0 +1,141 @@
+#include "services/clock-service.h"
+
+//
+// FoobarClockService:
+//
+// Service providing an auto-updating timestamp value which can be used to display the current time.
+//
+
+struct _FoobarClockService
+{
+ GObject parent_instance;
+ GDateTime* time;
+ guint source_id;
+};
+
+enum
+{
+ PROP_TIME = 1,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_clock_service_class_init ( FoobarClockServiceClass* klass );
+static void foobar_clock_service_init ( FoobarClockService* self );
+static void foobar_clock_service_get_property( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_clock_service_finalize ( GObject* object );
+static gboolean foobar_clock_service_handle_tick ( gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarClockService, foobar_clock_service, G_TYPE_OBJECT )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Service Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the clock service.
+//
+void foobar_clock_service_class_init( FoobarClockServiceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_clock_service_get_property;
+ object_klass->finalize = foobar_clock_service_finalize;
+
+ props[PROP_TIME] = g_param_spec_boxed(
+ "time",
+ "Time",
+ "The current time, regularly updated.",
+ G_TYPE_DATE_TIME,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for the clock service.
+//
+void foobar_clock_service_init( FoobarClockService* self )
+{
+ self->time = g_date_time_new_now_local( );
+ self->source_id = g_timeout_add_seconds( 1, foobar_clock_service_handle_tick, self );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_clock_service_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarClockService* self = (FoobarClockService*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_TIME:
+ g_value_set_boxed( value, foobar_clock_service_get_time( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for the clock service.
+//
+void foobar_clock_service_finalize( GObject* object )
+{
+ FoobarClockService* self = (FoobarClockService*)object;
+
+ g_clear_pointer( &self->time, g_date_time_unref );
+ g_clear_handle_id( &self->source_id, g_source_remove );
+
+ G_OBJECT_CLASS( foobar_clock_service_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new clock service instance.
+//
+FoobarClockService* foobar_clock_service_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_CLOCK_SERVICE, NULL );
+}
+
+//
+// Get the current snapshot of the timestamp.
+//
+// This may be a bit older than the value returned by g_date_time_new_now_local.
+//
+GDateTime* foobar_clock_service_get_time( FoobarClockService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CLOCK_SERVICE( self ), NULL );
+
+ return self->time;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called every time the update delay has elapsed to update the current timestamp value.
+//
+gboolean foobar_clock_service_handle_tick( gpointer userdata )
+{
+ FoobarClockService* self = (FoobarClockService*)userdata;
+
+ g_clear_pointer( &self->time, g_date_time_unref );
+ self->time = g_date_time_new_now_local( );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_TIME] );
+
+ return G_SOURCE_CONTINUE;
+} \ No newline at end of file
diff --git a/src/services/clock-service.h b/src/services/clock-service.h
new file mode 100644
index 0000000..651c5b5
--- /dev/null
+++ b/src/services/clock-service.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_CLOCK_SERVICE foobar_clock_service_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarClockService, foobar_clock_service, FOOBAR, CLOCK_SERVICE, GObject )
+
+FoobarClockService* foobar_clock_service_new ( void );
+GDateTime* foobar_clock_service_get_time( FoobarClockService* self );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/services/configuration-service.c b/src/services/configuration-service.c
new file mode 100644
index 0000000..b56b281
--- /dev/null
+++ b/src/services/configuration-service.c
@@ -0,0 +1,3248 @@
+#include "services/configuration-service.h"
+#include <gio/gio.h>
+
+#define UPDATE_APPLY_DELAY 250
+#define CONFIGURATION_LOG_DOMAIN "foobar.conf"
+#define CONFIGURATION_WARNING( ... ) g_log( CONFIGURATION_LOG_DOMAIN, G_LOG_LEVEL_WARNING, __VA_ARGS__ )
+
+//
+// FoobarScreenEdge:
+//
+// An edge of the screen.
+//
+
+G_STATIC_ASSERT( sizeof( FoobarScreenEdge ) == sizeof( gint ) );
+G_DEFINE_ENUM_TYPE(
+ FoobarScreenEdge,
+ foobar_screen_edge,
+ G_DEFINE_ENUM_VALUE( FOOBAR_SCREEN_EDGE_LEFT, "left" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_SCREEN_EDGE_RIGHT, "right" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_SCREEN_EDGE_TOP, "top" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_SCREEN_EDGE_BOTTOM, "bottom" ) )
+
+//
+// FoobarOrientation:
+//
+// An orientation value.
+//
+
+G_STATIC_ASSERT( sizeof( FoobarOrientation ) == sizeof( gint ) );
+G_DEFINE_ENUM_TYPE(
+ FoobarOrientation,
+ foobar_orientation,
+ G_DEFINE_ENUM_VALUE( FOOBAR_ORIENTATION_HORIZONTAL, "horizontal" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_ORIENTATION_VERTICAL, "vertical" ) )
+
+//
+// FoobarStatusItem:
+//
+// An item to display in a status panel item.
+//
+
+G_STATIC_ASSERT( sizeof( FoobarStatusItem ) == sizeof( gint ) );
+G_DEFINE_ENUM_TYPE(
+ FoobarStatusItem,
+ foobar_status_item,
+ G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_NETWORK, "network" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_BLUETOOTH, "bluetooth" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_BATTERY, "battery" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_BRIGHTNESS, "brightness" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_AUDIO, "audio" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_NOTIFICATIONS, "notifications" ) )
+
+//
+// FoobarPanelItemKind:
+//
+// Enum representing the various panel item types.
+//
+
+G_STATIC_ASSERT( sizeof( FoobarPanelItemKind ) == sizeof( gint ) );
+G_DEFINE_ENUM_TYPE(
+ FoobarPanelItemKind,
+ foobar_panel_item_kind,
+ G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_KIND_ICON, "icon" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_KIND_CLOCK, "clock" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_KIND_WORKSPACES, "workspaces" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_KIND_STATUS, "status" ) )
+
+//
+// FoobarPanelItemAction:
+//
+// The action to be executed when a panel item is clicked (if supported by the item).
+//
+
+G_STATIC_ASSERT( sizeof( FoobarPanelItemAction ) == sizeof( gint ) );
+G_DEFINE_ENUM_TYPE(
+ FoobarPanelItemAction,
+ foobar_panel_item_action,
+ G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_ACTION_NONE, "none" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_ACTION_LAUNCHER, "launcher" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_ACTION_CONTROL_CENTER, "control-center" ) )
+
+//
+// FoobarPanelItemPosition:
+//
+// Position of a panel item in the panel.
+//
+
+G_STATIC_ASSERT( sizeof( FoobarPanelItemPosition ) == sizeof( gint ) );
+G_DEFINE_ENUM_TYPE(
+ FoobarPanelItemPosition,
+ foobar_panel_item_position,
+ G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_POSITION_START, "start" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_POSITION_CENTER, "center" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_POSITION_END, "end" ) )
+
+//
+// FoobarControlCenterRow:
+//
+// A row in the "controls" section of the control center.
+//
+
+G_STATIC_ASSERT( sizeof( FoobarControlCenterRow ) == sizeof( gint ) );
+G_DEFINE_ENUM_TYPE(
+ FoobarControlCenterRow,
+ foobar_control_center_row,
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ROW_CONNECTIVITY, "connectivity" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ROW_AUDIO_OUTPUT, "audio-output" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ROW_AUDIO_INPUT, "audio-input" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ROW_BRIGHTNESS, "brightness" ) )
+
+//
+// FoobarControlCenterAlignment:
+//
+// Alignment of the control center along its screen edge.
+//
+
+G_STATIC_ASSERT( sizeof( FoobarControlCenterAlignment ) == sizeof( gint ) );
+G_DEFINE_ENUM_TYPE(
+ FoobarControlCenterAlignment,
+ foobar_control_center_alignment,
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ALIGNMENT_START, "start" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ALIGNMENT_CENTER, "center" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ALIGNMENT_END, "end" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL, "fill" ) )
+
+//
+// FoobarGeneralConfiguration:
+//
+// General application settings not specific to a single component.
+//
+
+struct _FoobarGeneralConfiguration
+{
+ gchar* stylesheet;
+};
+
+static void foobar_general_configuration_load ( FoobarGeneralConfiguration* self,
+ GKeyFile* file );
+static void foobar_general_configuration_store( FoobarGeneralConfiguration const* self,
+ GKeyFile* file );
+
+G_DEFINE_BOXED_TYPE(
+ FoobarGeneralConfiguration,
+ foobar_general_configuration,
+ foobar_general_configuration_copy,
+ foobar_general_configuration_free )
+
+//
+// FoobarPanelItemConfiguration:
+//
+// Configuration for an item in the panel. Structures of this type have a specific item kind with more properties.
+//
+
+struct _FoobarPanelItemConfiguration
+{
+ FoobarPanelItemKind kind;
+ gchar* name;
+ FoobarPanelItemPosition position;
+ FoobarPanelItemAction action;
+ union
+ {
+ struct
+ {
+ gchar* icon_name;
+ } icon;
+
+ struct
+ {
+ gchar* format;
+ } clock;
+
+ struct
+ {
+ gint button_size;
+ gint spacing;
+ } workspaces;
+
+ struct
+ {
+ FoobarStatusItem* items;
+ gsize items_count;
+ gint spacing;
+ gboolean show_labels;
+ gboolean enable_scrolling;
+ } status;
+ };
+};
+
+static FoobarPanelItemConfiguration* foobar_panel_item_configuration_load ( GKeyFile* file,
+ gchar const* group );
+static void foobar_panel_item_configuration_store( FoobarPanelItemConfiguration const* self,
+ GKeyFile* file );
+
+G_DEFINE_BOXED_TYPE(
+ FoobarPanelItemConfiguration,
+ foobar_panel_item_configuration,
+ foobar_panel_item_configuration_copy,
+ foobar_panel_item_configuration_free )
+
+//
+// FoobarPanelConfiguration:
+//
+// Configuration for the panel shown at the edge of a screen.
+//
+
+struct _FoobarPanelConfiguration
+{
+ FoobarScreenEdge position;
+ gint margin;
+ gint padding;
+ gint size;
+ gint spacing;
+ gboolean multi_monitor;
+ FoobarPanelItemConfiguration** items;
+ gsize items_count;
+};
+
+static void foobar_panel_configuration_load ( FoobarPanelConfiguration* self,
+ GKeyFile* file );
+static void foobar_panel_configuration_store( FoobarPanelConfiguration const* self,
+ GKeyFile* file );
+
+G_DEFINE_BOXED_TYPE(
+ FoobarPanelConfiguration,
+ foobar_panel_configuration,
+ foobar_panel_configuration_copy,
+ foobar_panel_configuration_free )
+
+//
+// FoobarLauncherConfiguration:
+//
+// Configuration for the application launcher.
+//
+
+struct _FoobarLauncherConfiguration
+{
+ gint width;
+ gint position;
+ gint max_height;
+};
+
+static void foobar_launcher_configuration_load ( FoobarLauncherConfiguration* self,
+ GKeyFile* file );
+static void foobar_launcher_configuration_store( FoobarLauncherConfiguration const* self,
+ GKeyFile* file );
+
+G_DEFINE_BOXED_TYPE(
+ FoobarLauncherConfiguration,
+ foobar_launcher_configuration,
+ foobar_launcher_configuration_copy,
+ foobar_launcher_configuration_free )
+
+//
+// FoobarControlCenterConfiguration:
+//
+// Configuration for the control center.
+//
+
+struct _FoobarControlCenterConfiguration
+{
+ gint width;
+ gint height;
+ FoobarScreenEdge position;
+ gint offset;
+ gint padding;
+ gint spacing;
+ FoobarOrientation orientation;
+ FoobarControlCenterAlignment alignment;
+ FoobarControlCenterRow* rows;
+ gsize rows_count;
+};
+
+static void foobar_control_center_configuration_load ( FoobarControlCenterConfiguration* self,
+ GKeyFile* file );
+static void foobar_control_center_configuration_store( FoobarControlCenterConfiguration const* self,
+ GKeyFile* file );
+
+G_DEFINE_BOXED_TYPE(
+ FoobarControlCenterConfiguration,
+ foobar_control_center_configuration,
+ foobar_control_center_configuration_copy,
+ foobar_control_center_configuration_free )
+
+//
+// FoobarNotificationConfiguration:
+//
+// Configuration for notifications and the notification area shown in the corner of the screen.
+//
+
+struct _FoobarNotificationConfiguration
+{
+ gint width;
+ gint min_height;
+ gint spacing;
+ gint close_button_inset;
+ gchar* time_format;
+};
+
+static void foobar_notification_configuration_load ( FoobarNotificationConfiguration* self,
+ GKeyFile* file );
+static void foobar_notification_configuration_store( FoobarNotificationConfiguration const* self,
+ GKeyFile* file );
+
+G_DEFINE_BOXED_TYPE(
+ FoobarNotificationConfiguration,
+ foobar_notification_configuration,
+ foobar_notification_configuration_copy,
+ foobar_notification_configuration_free )
+
+//
+// FoobarConfiguration:
+//
+// Main configuration structure containing the configuration state for all components of the application.
+//
+
+struct _FoobarConfiguration
+{
+ FoobarGeneralConfiguration* general;
+ FoobarPanelConfiguration* panel;
+ FoobarLauncherConfiguration* launcher;
+ FoobarControlCenterConfiguration* control_center;
+ FoobarNotificationConfiguration* notifications;
+};
+
+static void foobar_configuration_load ( FoobarConfiguration* self,
+ GKeyFile* file );
+static void foobar_configuration_load_from_file( FoobarConfiguration* self,
+ gchar const* path );
+static void foobar_configuration_store ( FoobarConfiguration const* self,
+ GKeyFile* file );
+
+G_DEFINE_BOXED_TYPE( FoobarConfiguration, foobar_configuration, foobar_configuration_copy, foobar_configuration_free )
+
+//
+// FoobarConfigurationService:
+//
+// Service monitoring the configuration state of the application specified in its config file.
+//
+
+struct _FoobarConfigurationService
+{
+ GObject parent_instance;
+ gchar* path;
+ char* actual_path;
+ FoobarConfiguration* current;
+ GFileMonitor* monitor;
+ GFileMonitor* actual_monitor;
+ gulong changed_handler_id;
+ gulong actual_changed_handler_id;
+ guint update_source_id;
+};
+
+enum
+{
+ PROP_CURRENT = 1,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_configuration_service_class_init ( FoobarConfigurationServiceClass* klass );
+static void foobar_configuration_service_init ( FoobarConfigurationService* self );
+static void foobar_configuration_service_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_configuration_service_finalize ( GObject* object );
+static void foobar_configuration_service_handle_changed ( GFileMonitor* monitor,
+ GFile* file,
+ GFile* other_file,
+ GFileMonitorEvent event_type,
+ gpointer userdata );
+static void foobar_configuration_service_handle_actual_changed( GFileMonitor* monitor,
+ GFile* file,
+ GFile* other_file,
+ GFileMonitorEvent event_type,
+ gpointer userdata );
+static void foobar_configuration_service_update ( FoobarConfigurationService* self );
+static gboolean foobar_configuration_service_update_cb ( gpointer userdata );
+static GFileMonitor* foobar_configuration_service_create_monitor ( gchar const* path );
+
+G_DEFINE_FINAL_TYPE( FoobarConfigurationService, foobar_configuration_service, G_TYPE_OBJECT )
+
+//
+// Parsing/Serialization Helpers:
+//
+// These functions make it easier to parse, validate and serialize various data types.
+//
+
+typedef enum
+{
+ VALIDATE_NONE = 0,
+ VALIDATE_NON_NEGATIVE = 1 << 0,
+ VALIDATE_NON_ZERO = 1 << 1,
+ VALIDATE_POSITIVE = VALIDATE_NON_NEGATIVE | VALIDATE_NON_ZERO,
+ VALIDATE_FILE_URL = 1 << 2,
+ VALIDATE_DISTINCT = 1 << 3,
+} ValidationFlags;
+
+static gboolean try_get_string_value ( GKeyFile* file,
+ gchar const* group,
+ gchar const* key,
+ ValidationFlags validation,
+ gchar** out );
+static gboolean try_get_string_list_value( GKeyFile* file,
+ gchar const* group,
+ gchar const* key,
+ ValidationFlags validation,
+ GStrv* out,
+ gsize* out_count );
+static gboolean try_get_int_value ( GKeyFile* file,
+ gchar const* group,
+ gchar const* key,
+ ValidationFlags validation,
+ gint* out );
+static gboolean try_get_enum_value ( GKeyFile* file,
+ gchar const* group,
+ gchar const* key,
+ GType enum_type,
+ ValidationFlags validation,
+ gint* out );
+static gboolean try_get_enum_list_value ( GKeyFile* file,
+ gchar const* group,
+ gchar const* key,
+ GType enum_type,
+ ValidationFlags validation,
+ gpointer* out,
+ gsize* out_count );
+static gboolean try_get_boolean_value ( GKeyFile* file,
+ gchar const* group,
+ gchar const* key,
+ ValidationFlags validation,
+ gboolean* out );
+
+static gchar const* enum_value_to_string ( GType enum_type,
+ gint value );
+static gchar const** enum_list_to_string ( GType enum_type,
+ gconstpointer values,
+ gsize values_count );
+static gchar* enum_format_help_text( GType enum_type );
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Default Configuration
+// ---------------------------------------------------------------------------------------------------------------------
+
+static FoobarGeneralConfiguration default_general_configuration =
+ {
+ .stylesheet = "resource:///foobar/styles/default.css",
+ };
+
+static FoobarPanelItemConfiguration default_panel_item_icon_configuration =
+ {
+ .kind = FOOBAR_PANEL_ITEM_KIND_ICON,
+ .name = "launcher",
+ .position = FOOBAR_PANEL_ITEM_POSITION_START,
+ .action = FOOBAR_PANEL_ITEM_ACTION_LAUNCHER,
+ .icon.icon_name = "fluent-grid-dots-symbolic",
+ };
+
+static FoobarPanelItemConfiguration default_panel_item_clock_configuration =
+ {
+ .kind = FOOBAR_PANEL_ITEM_KIND_CLOCK,
+ .name = "clock",
+ .position = FOOBAR_PANEL_ITEM_POSITION_CENTER,
+ .action = FOOBAR_PANEL_ITEM_ACTION_NONE,
+ .clock.format = "%H\n%M",
+ };
+
+static FoobarPanelItemConfiguration default_panel_item_workspaces_configuration =
+ {
+ .kind = FOOBAR_PANEL_ITEM_KIND_WORKSPACES,
+ .name = "workspaces",
+ .position = FOOBAR_PANEL_ITEM_POSITION_START,
+ .action = FOOBAR_PANEL_ITEM_ACTION_NONE,
+ .workspaces.button_size = 20,
+ .workspaces.spacing = 6,
+ };
+
+static FoobarStatusItem default_panel_item_status_configuration_items[] =
+ {
+ FOOBAR_STATUS_ITEM_BATTERY,
+ FOOBAR_STATUS_ITEM_BRIGHTNESS,
+ FOOBAR_STATUS_ITEM_AUDIO,
+ FOOBAR_STATUS_ITEM_NETWORK,
+ FOOBAR_STATUS_ITEM_BLUETOOTH,
+ FOOBAR_STATUS_ITEM_NOTIFICATIONS
+ };
+
+static FoobarPanelItemConfiguration default_panel_item_status_configuration =
+ {
+ .kind = FOOBAR_PANEL_ITEM_KIND_STATUS,
+ .name = "status",
+ .position = FOOBAR_PANEL_ITEM_POSITION_END,
+ .action = FOOBAR_PANEL_ITEM_ACTION_CONTROL_CENTER,
+ .status.items = default_panel_item_status_configuration_items,
+ .status.items_count = G_N_ELEMENTS( default_panel_item_status_configuration_items ),
+ .status.spacing = 6,
+ .status.show_labels = FALSE,
+ .status.enable_scrolling = FALSE,
+ };
+
+static FoobarPanelItemConfiguration *default_panel_configuration_items[] =
+ {
+ &default_panel_item_icon_configuration,
+ &default_panel_item_workspaces_configuration,
+ &default_panel_item_clock_configuration,
+ &default_panel_item_status_configuration,
+ NULL,
+ };
+
+static FoobarPanelConfiguration default_panel_configuration =
+ {
+ .position = FOOBAR_SCREEN_EDGE_LEFT,
+ .margin = 16,
+ .padding = 12,
+ .size = 48,
+ .spacing = 12,
+ .multi_monitor = TRUE,
+ .items = default_panel_configuration_items,
+ .items_count = G_N_ELEMENTS( default_panel_configuration_items ) - 1,
+ };
+
+static FoobarLauncherConfiguration default_launcher_configuration =
+ {
+ .width = 600,
+ .position = 300,
+ .max_height = 400,
+ };
+
+static FoobarControlCenterRow default_control_center_configuration_rows[] =
+ {
+ FOOBAR_CONTROL_CENTER_ROW_CONNECTIVITY,
+ FOOBAR_CONTROL_CENTER_ROW_AUDIO_OUTPUT,
+ FOOBAR_CONTROL_CENTER_ROW_BRIGHTNESS,
+ };
+
+static FoobarControlCenterConfiguration default_control_center_configuration =
+ {
+ .position = FOOBAR_SCREEN_EDGE_LEFT,
+ .width = 400,
+ .height = 600,
+ .offset = 16,
+ .padding = 24,
+ .spacing = 12,
+ .orientation = FOOBAR_ORIENTATION_VERTICAL,
+ .alignment = FOOBAR_CONTROL_CENTER_ALIGNMENT_CENTER,
+ .rows = default_control_center_configuration_rows,
+ .rows_count = G_N_ELEMENTS( default_control_center_configuration_rows ),
+ };
+
+static FoobarNotificationConfiguration default_notification_configuration =
+ {
+ .width = 400,
+ .min_height = 48,
+ .spacing = 16,
+ .close_button_inset = -6,
+ .time_format = "%H:%M",
+ };
+
+static FoobarConfiguration default_configuration =
+ {
+ .general = &default_general_configuration,
+ .panel = &default_panel_configuration,
+ .launcher = &default_launcher_configuration,
+ .control_center = &default_control_center_configuration,
+ .notifications = &default_notification_configuration,
+ };
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Default Configuration
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a default general configuration structure.
+//
+FoobarGeneralConfiguration* foobar_general_configuration_new( void )
+{
+ return foobar_general_configuration_copy( &default_general_configuration );
+}
+
+//
+// Create a mutable copy of another general configuration structure.
+//
+FoobarGeneralConfiguration* foobar_general_configuration_copy( FoobarGeneralConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+
+ FoobarGeneralConfiguration* copy = g_new0( FoobarGeneralConfiguration, 1 );
+ copy->stylesheet = g_strdup( self->stylesheet );
+ return copy;
+}
+
+//
+// Release resources associated with a general configuration structure.
+//
+void foobar_general_configuration_free( FoobarGeneralConfiguration* self )
+{
+ g_return_if_fail( self != NULL );
+
+ g_free( self->stylesheet );
+ g_free( self );
+}
+
+//
+// Check if two general configuration structures are equal.
+//
+gboolean foobar_general_configuration_equal(
+ FoobarGeneralConfiguration const* a,
+ FoobarGeneralConfiguration const* b )
+{
+ g_return_val_if_fail( a != NULL, FALSE );
+ g_return_val_if_fail( b != NULL, FALSE );
+
+ if ( g_strcmp0( a->stylesheet, b->stylesheet ) ) { return FALSE; }
+
+ return TRUE;
+}
+
+//
+// URI to the CSS stylesheet to use -- this may be a resource that's bundled with Foobar or a "file:" URI.
+//
+gchar const* foobar_general_configuration_get_stylesheet( FoobarGeneralConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ return self->stylesheet;
+}
+
+//
+// URI to the CSS stylesheet to use -- this may be a resource that's bundled with Foobar or a "file:" URI.
+//
+void foobar_general_configuration_set_stylesheet(
+ FoobarGeneralConfiguration* self,
+ gchar const* value )
+{
+ g_return_if_fail( self != NULL );
+ g_return_if_fail( value != NULL );
+
+ g_free( self->stylesheet );
+ self->stylesheet = g_strdup( value );
+}
+
+//
+// Populate a general configuration structure from the "general" section of a keyfile.
+//
+void foobar_general_configuration_load(
+ FoobarGeneralConfiguration* self,
+ GKeyFile* file )
+{
+ g_autofree gchar* stylesheet = NULL;
+ if ( try_get_string_value( file, "general", "stylesheet", VALIDATE_FILE_URL, &stylesheet ) )
+ {
+ foobar_general_configuration_set_stylesheet( self, stylesheet );
+ }
+}
+
+//
+// Store a general configuration structure in the "general" section of a keyfile.
+//
+void foobar_general_configuration_store(
+ FoobarGeneralConfiguration const* self,
+ GKeyFile* file )
+{
+ gchar const* stylesheet = foobar_general_configuration_get_stylesheet( self );
+ g_key_file_set_string( file, "general", "stylesheet", stylesheet );
+ g_key_file_set_comment(
+ file,
+ "general",
+ "stylesheet",
+ " URI to the CSS stylesheet to use -- this may be a resource that's bundled with Foobar or a \"file:\" URI.",
+ NULL );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Panel Item Configuration
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a default icon panel item configuration structure.
+//
+FoobarPanelItemConfiguration* foobar_panel_item_icon_configuration_new( void )
+{
+ return foobar_panel_item_configuration_copy( &default_panel_item_icon_configuration );
+}
+
+//
+// Create a default clock panel item configuration structure.
+//
+FoobarPanelItemConfiguration* foobar_panel_item_clock_configuration_new( void )
+{
+ return foobar_panel_item_configuration_copy( &default_panel_item_clock_configuration );
+}
+
+//
+// Create a default workspaces panel item configuration structure.
+//
+FoobarPanelItemConfiguration* foobar_panel_item_workspaces_configuration_new( void )
+{
+ return foobar_panel_item_configuration_copy( &default_panel_item_workspaces_configuration );
+}
+
+//
+// Create a default status panel item configuration structure.
+//
+FoobarPanelItemConfiguration* foobar_panel_item_status_configuration_new( void )
+{
+ return foobar_panel_item_configuration_copy( &default_panel_item_status_configuration );
+}
+
+//
+// Create a mutable copy of another panel item configuration structure, preserving its item type.
+//
+FoobarPanelItemConfiguration* foobar_panel_item_configuration_copy( FoobarPanelItemConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+
+ FoobarPanelItemConfiguration* copy = g_new0( FoobarPanelItemConfiguration, 1 );
+ copy->kind = self->kind;
+ copy->name = g_strdup( self->name );
+ copy->position = self->position;
+ copy->action = self->action;
+ switch ( copy->kind )
+ {
+ case FOOBAR_PANEL_ITEM_KIND_ICON:
+ copy->icon.icon_name = g_strdup( self->icon.icon_name );
+ break;
+ case FOOBAR_PANEL_ITEM_KIND_CLOCK:
+ copy->clock.format = g_strdup( self->clock.format );
+ break;
+ case FOOBAR_PANEL_ITEM_KIND_WORKSPACES:
+ copy->workspaces.button_size = self->workspaces.button_size;
+ copy->workspaces.spacing = self->workspaces.spacing;
+ break;
+ case FOOBAR_PANEL_ITEM_KIND_STATUS:
+ copy->status.items = g_new( FoobarStatusItem, self->status.items_count );
+ copy->status.items_count = self->status.items_count;
+ memcpy( copy->status.items, self->status.items, sizeof( *copy->status.items ) * copy->status.items_count );
+ copy->status.spacing = self->status.spacing;
+ copy->status.show_labels = self->status.show_labels;
+ copy->status.enable_scrolling = self->status.enable_scrolling;
+ break;
+ default:
+ g_warn_if_reached( );
+ }
+ return copy;
+}
+
+//
+// Release resources associated with a panel item configuration structure.
+//
+void foobar_panel_item_configuration_free( FoobarPanelItemConfiguration* self )
+{
+ g_return_if_fail( self != NULL );
+
+ switch ( self->kind )
+ {
+ case FOOBAR_PANEL_ITEM_KIND_ICON:
+ g_free( self->icon.icon_name );
+ break;
+ case FOOBAR_PANEL_ITEM_KIND_CLOCK:
+ g_free( self->clock.format );
+ break;
+ case FOOBAR_PANEL_ITEM_KIND_WORKSPACES:
+ break;
+ case FOOBAR_PANEL_ITEM_KIND_STATUS:
+ g_free( self->status.items );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+
+ g_free( self->name );
+ g_free( self );
+}
+
+//
+// Check if two panel item configuration structures are equal.
+//
+gboolean foobar_panel_item_configuration_equal(
+ FoobarPanelItemConfiguration const* a,
+ FoobarPanelItemConfiguration const* b )
+{
+ g_return_val_if_fail( a != NULL, FALSE );
+ g_return_val_if_fail( b != NULL, FALSE );
+
+ if ( a->kind != b->kind ) { return FALSE; }
+ if ( a->position != b->position ) { return FALSE; }
+ if ( a->action != b->action ) { return FALSE; }
+ if ( g_strcmp0( a->name, b->name ) ) { return FALSE; }
+
+ switch ( a->kind )
+ {
+ case FOOBAR_PANEL_ITEM_KIND_ICON:
+ if ( g_strcmp0( a->icon.icon_name, b->icon.icon_name ) ) { return FALSE; }
+ break;
+ case FOOBAR_PANEL_ITEM_KIND_CLOCK:
+ if ( g_strcmp0( a->clock.format, b->clock.format ) ) { return FALSE; }
+ break;
+ case FOOBAR_PANEL_ITEM_KIND_WORKSPACES:
+ if ( a->workspaces.button_size != b->workspaces.button_size ) { return FALSE; }
+ if ( a->workspaces.spacing != b->workspaces.spacing ) { return FALSE; }
+ break;
+ case FOOBAR_PANEL_ITEM_KIND_STATUS:
+ if ( a->status.items_count != b->status.items_count ) { return FALSE; }
+ if ( memcmp( a->status.items, b->status.items, sizeof( *a->status.items ) * a->status.items_count ) ) { return FALSE; }
+ if ( a->status.spacing != b->status.spacing ) { return FALSE; }
+ if ( a->status.show_labels != b->status.show_labels ) { return FALSE; }
+ if ( a->status.enable_scrolling != b->status.enable_scrolling ) { return FALSE; }
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+
+ return TRUE;
+}
+
+//
+// The type of panel item to be configured by this structure.
+//
+FoobarPanelItemKind foobar_panel_item_configuration_get_kind( FoobarPanelItemConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->kind;
+}
+
+//
+// Name of the panel item (this is derived from the section name, which has the form "panel.[name]").
+//
+gchar const* foobar_panel_item_configuration_get_name( FoobarPanelItemConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->name;
+}
+
+//
+// Position where the item should be placed item within the panel.
+//
+FoobarPanelItemPosition foobar_panel_item_configuration_get_position( FoobarPanelItemConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->position;
+}
+
+//
+// Action invoked when the user clicks the item (if supported by the item).
+//
+FoobarPanelItemAction foobar_panel_item_configuration_get_action( FoobarPanelItemConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->action;
+}
+
+//
+// The GTK icon to use for the item.
+//
+gchar const* foobar_panel_item_icon_configuration_get_icon_name( FoobarPanelItemConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_ICON, NULL );
+
+ return self->icon.icon_name;
+}
+
+//
+// The time format string as used by g_date_time_format.
+//
+gchar const* foobar_panel_item_clock_configuration_get_format( FoobarPanelItemConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_CLOCK, NULL );
+
+ return self->clock.format;
+}
+
+//
+// Size of each workspace button.
+//
+gint foobar_panel_item_workspaces_configuration_get_button_size( FoobarPanelItemConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_WORKSPACES, 0 );
+
+ return self->workspaces.button_size;
+}
+
+//
+// Inner spacing between the workspace buttons.
+//
+gint foobar_panel_item_workspaces_configuration_get_spacing( FoobarPanelItemConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_WORKSPACES, 0 );
+
+ return self->workspaces.spacing;
+}
+
+//
+// Ordered list of status items to display in the item.
+//
+FoobarStatusItem const* foobar_panel_item_status_configuration_get_items(
+ FoobarPanelItemConfiguration const* self,
+ gsize* out_count )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS, NULL );
+ g_return_val_if_fail( out_count != NULL, NULL );
+
+ *out_count = self->status.items_count;
+ return self->status.items;
+}
+
+//
+// Inner spacing between the status items.
+//
+gint foobar_panel_item_status_configuration_get_spacing( FoobarPanelItemConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS, 0 );
+
+ return self->status.spacing;
+}
+
+//
+// Indicates whether text labels should be shown next to the status icons.
+//
+gboolean foobar_panel_item_status_configuration_get_show_labels( FoobarPanelItemConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, FALSE );
+ g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS, FALSE );
+
+ return self->status.show_labels;
+}
+
+//
+// If set to true, some settings like volume or brightness can be adjusted by scrolling while hovering over the status
+// item.
+//
+gboolean foobar_panel_item_status_configuration_get_enable_scrolling( FoobarPanelItemConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, FALSE );
+ g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS, FALSE );
+
+ return self->status.enable_scrolling;
+}
+
+//
+// Name of the panel item (this is derived from the section name, which has the form "panel.[name]").
+//
+void foobar_panel_item_configuration_set_name(
+ FoobarPanelItemConfiguration* self,
+ gchar const* value )
+{
+ g_return_if_fail( self != NULL );
+ g_return_if_fail( value != NULL );
+
+ g_free( self->name );
+ self->name = g_strdup( value );
+}
+
+//
+// Position where the item should be placed item within the panel.
+//
+void foobar_panel_item_configuration_set_position(
+ FoobarPanelItemConfiguration* self,
+ FoobarPanelItemPosition value )
+{
+ g_return_if_fail( self != NULL );
+ self->position = value;
+}
+
+//
+// Action invoked when the user clicks the item (if supported by the item).
+//
+void foobar_panel_item_configuration_set_action(
+ FoobarPanelItemConfiguration* self,
+ FoobarPanelItemAction value )
+{
+ g_return_if_fail( self != NULL );
+ self->action = value;
+}
+
+//
+// The GTK icon to use for the item.
+//
+void foobar_panel_item_icon_configuration_set_icon_name(
+ FoobarPanelItemConfiguration* self,
+ gchar const* value )
+{
+ g_return_if_fail( self != NULL );
+ g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_ICON );
+ g_return_if_fail( value != NULL );
+
+ g_free( self->icon.icon_name );
+ self->icon.icon_name = g_strdup( value );
+}
+
+//
+// The time format string as used by g_date_time_format.
+//
+void foobar_panel_item_clock_configuration_set_format(
+ FoobarPanelItemConfiguration* self,
+ gchar const* value )
+{
+ g_return_if_fail( self != NULL );
+ g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_CLOCK );
+ g_return_if_fail( value != NULL );
+
+ g_free( self->clock.format );
+ self->clock.format = g_strdup( value );
+}
+
+//
+// Size of each workspace button.
+//
+void foobar_panel_item_workspaces_configuration_set_button_size(
+ FoobarPanelItemConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_WORKSPACES );
+
+ self->workspaces.button_size = value;
+}
+
+//
+// Inner spacing between the workspace buttons.
+//
+void foobar_panel_item_workspaces_configuration_set_spacing(
+ FoobarPanelItemConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_WORKSPACES );
+
+ self->workspaces.spacing = value;
+}
+
+//
+// Ordered list of status items to display in the item.
+//
+void foobar_panel_item_status_configuration_set_items(
+ FoobarPanelItemConfiguration* self,
+ FoobarStatusItem const* value,
+ gsize value_count )
+{
+ g_return_if_fail( self != NULL );
+ g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS );
+ g_return_if_fail( value != NULL || value_count == 0 );
+
+ g_free( self->status.items );
+ self->status.items = g_new( FoobarStatusItem, value_count );
+ self->status.items_count = value_count;
+ memcpy( self->status.items, value, sizeof( *self->status.items ) * self->status.items_count );
+}
+
+//
+// Inner spacing between the status items.
+//
+void foobar_panel_item_status_configuration_set_spacing(
+ FoobarPanelItemConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS );
+
+ self->status.spacing = value;
+}
+
+//
+// Indicates whether text labels should be shown next to the status icons.
+//
+void foobar_panel_item_status_configuration_set_show_labels(
+ FoobarPanelItemConfiguration* self,
+ gboolean value )
+{
+ g_return_if_fail( self != NULL );
+ g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS );
+
+ self->status.show_labels = value;
+}
+
+//
+// If set to true, some settings like volume or brightness can be adjusted by scrolling while hovering over the status
+// item.
+//
+void foobar_panel_item_status_configuration_set_enable_scrolling(
+ FoobarPanelItemConfiguration* self,
+ gboolean value )
+{
+ g_return_if_fail( self != NULL );
+ g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS );
+
+ self->status.enable_scrolling = value;
+}
+
+//
+// Create a panel item configuration structure from the given section of a keyfile, returning NULL if it is not a panel
+// item configuration section.
+//
+FoobarPanelItemConfiguration* foobar_panel_item_configuration_load(
+ GKeyFile* file,
+ gchar const* group )
+{
+ if ( !g_str_has_prefix( group, "panel." ) )
+ {
+ return NULL;
+ }
+
+ gint kind;
+ if ( !try_get_enum_value( file, group, "kind", FOOBAR_TYPE_PANEL_ITEM_KIND, VALIDATE_NONE, &kind ) )
+ {
+ return NULL;
+ }
+
+ FoobarPanelItemConfiguration* result;
+ switch ( (FoobarPanelItemKind)kind )
+ {
+ case FOOBAR_PANEL_ITEM_KIND_ICON:
+ {
+ result = foobar_panel_item_icon_configuration_new( );
+
+ g_autofree gchar* icon_name = NULL;
+ if ( try_get_string_value( file, group, "icon-name", VALIDATE_NONE, &icon_name ) )
+ {
+ foobar_panel_item_icon_configuration_set_icon_name( result, icon_name );
+ }
+
+ break;
+ }
+ case FOOBAR_PANEL_ITEM_KIND_CLOCK:
+ {
+ result = foobar_panel_item_clock_configuration_new( );
+
+ g_autofree gchar* format = NULL;
+ if ( try_get_string_value( file, group, "format", VALIDATE_NONE, &format ) )
+ {
+ foobar_panel_item_clock_configuration_set_format( result, format );
+ }
+
+ break;
+ }
+ case FOOBAR_PANEL_ITEM_KIND_WORKSPACES:
+ {
+ result = foobar_panel_item_workspaces_configuration_new( );
+
+ gint button_size;
+ if ( try_get_int_value( file, group, "button-size", VALIDATE_POSITIVE, &button_size ) )
+ {
+ foobar_panel_item_workspaces_configuration_set_button_size( result, button_size );
+ }
+
+ gint spacing;
+ if ( try_get_int_value( file, group, "spacing", VALIDATE_NON_NEGATIVE, &spacing ) )
+ {
+ foobar_panel_item_workspaces_configuration_set_spacing( result, spacing );
+ }
+
+ break;
+ }
+ case FOOBAR_PANEL_ITEM_KIND_STATUS:
+ {
+ result = foobar_panel_item_status_configuration_new( );
+
+ g_autofree gpointer items = NULL;
+ gsize items_count;
+ if ( try_get_enum_list_value( file, group, "items", FOOBAR_TYPE_STATUS_ITEM, VALIDATE_DISTINCT, &items, &items_count ) )
+ {
+ foobar_panel_item_status_configuration_set_items( result, items, items_count );
+ }
+
+ gint spacing;
+ if ( try_get_int_value( file, group, "spacing", VALIDATE_NON_NEGATIVE, &spacing ) )
+ {
+ foobar_panel_item_status_configuration_set_spacing( result, spacing );
+ }
+
+ gboolean show_labels;
+ if ( try_get_boolean_value( file, group, "show-labels", VALIDATE_NONE, &show_labels ) )
+ {
+ foobar_panel_item_status_configuration_set_show_labels( result, show_labels );
+ }
+
+ gboolean enable_scrolling;
+ if ( try_get_boolean_value( file, group, "enable-scrolling", VALIDATE_NONE, &enable_scrolling ) )
+ {
+ foobar_panel_item_status_configuration_set_enable_scrolling( result, enable_scrolling );
+ }
+
+ break;
+ }
+ default:
+ {
+ g_warn_if_reached( );
+ return NULL;
+ }
+ }
+
+ gchar const* name = group + strlen( "panel." );
+ foobar_panel_item_configuration_set_name( result, name );
+
+ gint position;
+ if ( try_get_enum_value( file, group, "position", FOOBAR_TYPE_PANEL_ITEM_POSITION, VALIDATE_NONE, &position ) )
+ {
+ foobar_panel_item_configuration_set_position( result, position );
+ }
+
+ gint action;
+ if ( try_get_enum_value( file, group, "action", FOOBAR_TYPE_PANEL_ITEM_ACTION, VALIDATE_NONE, &action ) )
+ {
+ foobar_panel_item_configuration_set_action( result, action );
+ }
+
+ return result;
+}
+
+//
+// Store a panel item configuration structure in the "panel.[name]" section of a keyfile.
+//
+void foobar_panel_item_configuration_store(
+ FoobarPanelItemConfiguration const* self,
+ GKeyFile* file )
+{
+ gchar const* name = foobar_panel_item_configuration_get_name( self );
+ g_autofree gchar* group = g_strdup_printf( "panel.%s", name );
+
+ FoobarPanelItemKind kind = foobar_panel_item_configuration_get_kind( self );
+ g_key_file_set_string( file, group, "kind", enum_value_to_string( FOOBAR_TYPE_PANEL_ITEM_KIND, kind ) );
+
+ FoobarPanelItemPosition position = foobar_panel_item_configuration_get_position( self );
+ g_key_file_set_string( file, group, "position", enum_value_to_string( FOOBAR_TYPE_PANEL_ITEM_POSITION, position ) );
+
+ FoobarPanelItemAction action = foobar_panel_item_configuration_get_action( self );
+ g_key_file_set_string( file, group, "action", enum_value_to_string( FOOBAR_TYPE_PANEL_ITEM_ACTION, action ) );
+
+ switch ( kind )
+ {
+ case FOOBAR_PANEL_ITEM_KIND_ICON:
+ {
+ gchar const* icon_name = foobar_panel_item_icon_configuration_get_icon_name( self );
+ g_key_file_set_string( file, group, "icon-name", icon_name );
+ g_key_file_set_comment(
+ file,
+ group,
+ "icon-name",
+ " The GTK icon to use for the item.",
+ NULL );
+
+ break;
+ }
+ case FOOBAR_PANEL_ITEM_KIND_CLOCK:
+ {
+ gchar const* format = foobar_panel_item_clock_configuration_get_format( self );
+ g_key_file_set_string( file, group, "format", format );
+ g_key_file_set_comment(
+ file,
+ group,
+ "format",
+ " The time format string as used by g_date_time_format.",
+ NULL );
+
+ break;
+ }
+ case FOOBAR_PANEL_ITEM_KIND_WORKSPACES:
+ {
+ gint button_size = foobar_panel_item_workspaces_configuration_get_button_size( self );
+ g_key_file_set_integer( file, group, "button-size", button_size );
+ g_key_file_set_comment(
+ file,
+ group,
+ "button-size",
+ " Size of each workspace button.",
+ NULL );
+
+ gint spacing = foobar_panel_item_workspaces_configuration_get_spacing( self );
+ g_key_file_set_integer( file, group, "spacing", spacing );
+ g_key_file_set_comment(
+ file,
+ group,
+ "spacing",
+ " Inner spacing between the workspace buttons.",
+ NULL );
+
+ break;
+ }
+ case FOOBAR_PANEL_ITEM_KIND_STATUS:
+ {
+ gsize items_count;
+ FoobarStatusItem const* items = foobar_panel_item_status_configuration_get_items( self, &items_count );
+ g_autofree gchar const** items_str = enum_list_to_string( FOOBAR_TYPE_STATUS_ITEM, items, items_count );
+ g_key_file_set_string_list( file, group, "items", items_str, items_count );
+ g_key_file_set_comment(
+ file,
+ group,
+ "items",
+ " Ordered list of status items to display in the item.",
+ NULL );
+
+ gint spacing = foobar_panel_item_status_configuration_get_spacing( self );
+ g_key_file_set_integer( file, group, "spacing", spacing );
+ g_key_file_set_comment(
+ file,
+ group,
+ "spacing",
+ " Inner spacing between the status items.",
+ NULL );
+
+ gboolean show_labels = foobar_panel_item_status_configuration_get_show_labels( self );
+ g_key_file_set_boolean( file, group, "show-labels", show_labels );
+ g_key_file_set_comment(
+ file,
+ group,
+ "show-labels",
+ " Indicates whether text labels should be shown next to the status icons.",
+ NULL );
+
+ gboolean enable_scrolling = foobar_panel_item_status_configuration_get_enable_scrolling( self );
+ g_key_file_set_boolean( file, group, "enable-scrolling", enable_scrolling );
+ g_key_file_set_comment(
+ file,
+ group,
+ "enable-scrolling",
+ " If set to true, some settings like volume or brightness can be adjusted by scrolling while hovering"
+ " over the status item.",
+ NULL );
+
+ break;
+ }
+ default:
+ {
+ g_warn_if_reached( );
+ break;
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Panel Configuration
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a default panel configuration structure.
+//
+FoobarPanelConfiguration* foobar_panel_configuration_new( void )
+{
+ return foobar_panel_configuration_copy( &default_panel_configuration );
+}
+
+//
+// Create a mutable copy of another panel configuration structure.
+//
+FoobarPanelConfiguration* foobar_panel_configuration_copy( FoobarPanelConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+
+ FoobarPanelConfiguration* copy = g_new0( FoobarPanelConfiguration, 1 );
+ copy->position = self->position;
+ copy->margin = self->margin;
+ copy->padding = self->padding;
+ copy->size = self->size;
+ copy->spacing = self->spacing;
+ copy->multi_monitor = self->multi_monitor;
+ copy->items = g_new( FoobarPanelItemConfiguration*, self->items_count + 1 );
+ copy->items_count = self->items_count;
+ for ( gsize i = 0; i < copy->items_count; ++i )
+ {
+ copy->items[i] = foobar_panel_item_configuration_copy( self->items[i] );
+ }
+ return copy;
+}
+
+//
+// Release resources associated with a panel configuration structure.
+//
+void foobar_panel_configuration_free( FoobarPanelConfiguration* self )
+{
+ g_return_if_fail( self != NULL );
+
+ for ( gsize i = 0; i < self->items_count; ++i )
+ {
+ foobar_panel_item_configuration_free( self->items[i] );
+ }
+ g_free( self->items );
+ g_free( self );
+}
+
+//
+// Check if two panel configuration structures are equal.
+//
+gboolean foobar_panel_configuration_equal(
+ FoobarPanelConfiguration const* a,
+ FoobarPanelConfiguration const* b )
+{
+ g_return_val_if_fail( a != NULL, FALSE );
+ g_return_val_if_fail( b != NULL, FALSE );
+
+ if ( a->position != b->position ) { return FALSE; }
+ if ( a->margin != b->margin ) { return FALSE; }
+ if ( a->padding != b->padding ) { return FALSE; }
+ if ( a->size != b->size ) { return FALSE; }
+ if ( a->spacing != b->spacing ) { return FALSE; }
+ if ( a->multi_monitor != b->multi_monitor ) { return FALSE; }
+ if ( a->items_count != b->items_count ) { return FALSE; }
+
+ for ( gsize i = 0; i < a->items_count; ++i )
+ {
+ if ( !foobar_panel_item_configuration_equal( a->items[i], b->items[i] ) ) { return FALSE; }
+ }
+
+ return TRUE;
+}
+
+//
+// The screen edge which the panel should appear on.
+//
+FoobarScreenEdge foobar_panel_configuration_get_position( FoobarPanelConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->position;
+}
+
+//
+// Offset of the panel from all screen edges.
+//
+gint foobar_panel_configuration_get_margin( FoobarPanelConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->margin;
+}
+
+//
+// Inset of the panel along its orientation.
+//
+gint foobar_panel_configuration_get_padding( FoobarPanelConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->padding;
+}
+
+//
+// Size of the panel (depending on the orientation, this can be either the width or the height).
+//
+gint foobar_panel_configuration_get_size( FoobarPanelConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->size;
+}
+
+//
+// Spacing between panel items.
+//
+gint foobar_panel_configuration_get_spacing( FoobarPanelConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->spacing;
+}
+
+//
+// Flag to enable the panel on all monitors.
+//
+gboolean foobar_panel_configuration_get_multi_monitor( FoobarPanelConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, FALSE );
+ return self->multi_monitor;
+}
+
+//
+// Items configured to be displayed in the panel.
+//
+FoobarPanelItemConfiguration const* const* foobar_panel_configuration_get_items(
+ FoobarPanelConfiguration const* self,
+ gsize* out_count )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+
+ if ( out_count ) { *out_count = self->items_count; }
+ return (FoobarPanelItemConfiguration const* const*)self->items;
+}
+
+//
+// Mutable items configured to be displayed in the panel.
+//
+FoobarPanelItemConfiguration* const* foobar_panel_configuration_get_items_mut(
+ FoobarPanelConfiguration* self,
+ gsize* out_count )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+
+ if ( out_count ) { *out_count = self->items_count; }
+ return self->items;
+}
+
+//
+// The screen edge which the panel should appear on.
+//
+void foobar_panel_configuration_set_position(
+ FoobarPanelConfiguration* self,
+ FoobarScreenEdge value )
+{
+ g_return_if_fail( self != NULL );
+ self->position = value;
+}
+
+//
+// Offset of the panel from all screen edges.
+//
+void foobar_panel_configuration_set_margin(
+ FoobarPanelConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->margin = value;
+}
+
+//
+// Inset of the panel along its orientation.
+//
+void foobar_panel_configuration_set_padding(
+ FoobarPanelConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->padding = value;
+}
+
+//
+// Size of the panel (depending on the orientation, this can be either the width or the height).
+//
+void foobar_panel_configuration_set_size(
+ FoobarPanelConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->size = value;
+}
+
+//
+// Spacing between panel items.
+//
+void foobar_panel_configuration_set_spacing(
+ FoobarPanelConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->spacing = value;
+}
+
+//
+// Flag to enable the panel on all monitors.
+//
+void foobar_panel_configuration_set_multi_monitor(
+ FoobarPanelConfiguration* self,
+ gboolean value )
+{
+ g_return_if_fail( self != NULL );
+ self->multi_monitor = value;
+}
+
+//
+// Items configured to be displayed in the panel.
+//
+void foobar_panel_configuration_set_items(
+ FoobarPanelConfiguration* self,
+ FoobarPanelItemConfiguration const* const* value,
+ gsize value_count )
+{
+ g_return_if_fail( self != NULL );
+ g_return_if_fail( value != NULL || value_count == 0 );
+
+ for ( gsize i = 0; i < self->items_count; ++i )
+ {
+ foobar_panel_item_configuration_free( self->items[i] );
+ }
+ g_free( self->items );
+ self->items = g_new0( FoobarPanelItemConfiguration*, value_count + 1 );
+ self->items_count = 0;
+ for ( gsize i = 0; i < value_count; ++i )
+ {
+ g_warn_if_fail( value[i] != NULL );
+ if ( value[i] )
+ {
+ self->items[self->items_count++] = foobar_panel_item_configuration_copy( value[i] );
+ }
+ }
+}
+
+//
+// Populate a panel configuration structure from the "panel" section of a keyfile.
+//
+void foobar_panel_configuration_load(
+ FoobarPanelConfiguration* self,
+ GKeyFile* file )
+{
+ gint position;
+ if ( try_get_enum_value( file, "panel", "position", FOOBAR_TYPE_SCREEN_EDGE, VALIDATE_NONE, &position ) )
+ {
+ foobar_panel_configuration_set_position( self, position );
+ }
+
+ gint margin;
+ if ( try_get_int_value( file, "panel", "margin", VALIDATE_NON_NEGATIVE, &margin ) )
+ {
+ foobar_panel_configuration_set_margin( self, margin );
+ }
+
+ gint padding;
+ if ( try_get_int_value( file, "panel", "padding", VALIDATE_NON_NEGATIVE, &padding ) )
+ {
+ foobar_panel_configuration_set_padding( self, padding );
+ }
+
+ gint size;
+ if ( try_get_int_value( file, "panel", "size", VALIDATE_POSITIVE, &size ) )
+ {
+ foobar_panel_configuration_set_size( self, size );
+ }
+
+ gint spacing;
+ if ( try_get_int_value( file, "panel", "spacing", VALIDATE_NON_NEGATIVE, &spacing ) )
+ {
+ foobar_panel_configuration_set_spacing( self, spacing );
+ }
+
+ gboolean multi_monitor;
+ if ( try_get_boolean_value( file, "panel", "multi-monitor", VALIDATE_NONE, &multi_monitor ) )
+ {
+ foobar_panel_configuration_set_multi_monitor( self, multi_monitor );
+ }
+
+ g_autoptr( GPtrArray ) items = g_ptr_array_new_with_free_func( (GDestroyNotify)foobar_panel_item_configuration_free );
+ g_auto( GStrv ) groups = g_key_file_get_groups( file, NULL );
+ for ( gchar** it = groups; *it; ++it )
+ {
+ FoobarPanelItemConfiguration* item = foobar_panel_item_configuration_load( file, *it );
+ if ( item ) { g_ptr_array_add( items, item ); }
+ }
+ foobar_panel_configuration_set_items( self, (FoobarPanelItemConfiguration const* const*)items->pdata, items->len );
+}
+
+//
+// Store a panel configuration structure in the "panel" section of a keyfile.
+//
+void foobar_panel_configuration_store(
+ FoobarPanelConfiguration const* self,
+ GKeyFile* file )
+{
+ FoobarScreenEdge position = foobar_panel_configuration_get_position( self );
+ g_key_file_set_string( file, "panel", "position", enum_value_to_string( FOOBAR_TYPE_SCREEN_EDGE, position ) );
+ g_key_file_set_comment(
+ file,
+ "panel",
+ "position",
+ " The screen edge which the panel should appear on.",
+ NULL );
+
+ gint margin = foobar_panel_configuration_get_margin( self );
+ g_key_file_set_integer( file, "panel", "margin", margin );
+ g_key_file_set_comment(
+ file,
+ "panel",
+ "margin",
+ " Offset of the panel from all screen edges.",
+ NULL );
+
+ gint padding = foobar_panel_configuration_get_padding( self );
+ g_key_file_set_integer( file, "panel", "padding", padding );
+ g_key_file_set_comment(
+ file,
+ "panel",
+ "padding",
+ " Inset of the panel along its orientation.",
+ NULL );
+
+ gint size = foobar_panel_configuration_get_size( self );
+ g_key_file_set_integer( file, "panel", "size", size );
+ g_key_file_set_comment(
+ file,
+ "panel",
+ "size",
+ " Size of the panel (depending on the orientation, this can be either the width or the height).",
+ NULL );
+
+ gint spacing = foobar_panel_configuration_get_spacing( self );
+ g_key_file_set_integer( file, "panel", "spacing", spacing );
+ g_key_file_set_comment(
+ file,
+ "panel",
+ "spacing",
+ " Spacing between panel items.",
+ NULL );
+
+ gboolean multi_monitor = foobar_panel_configuration_get_multi_monitor( self );
+ g_key_file_set_boolean( file, "panel", "multi-monitor", multi_monitor );
+ g_key_file_set_comment(
+ file,
+ "panel",
+ "multi-monitor",
+ " Flag to enable the panel on all monitors.",
+ NULL );
+
+ gsize items_count;
+ FoobarPanelItemConfiguration const* const* items = foobar_panel_configuration_get_items( self, &items_count );
+ for ( gsize i = 0; i < items_count; ++i )
+ {
+ foobar_panel_item_configuration_store( items[i], file );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Launcher Configuration
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a default launcher configuration structure.
+//
+FoobarLauncherConfiguration* foobar_launcher_configuration_new( void )
+{
+ return foobar_launcher_configuration_copy( &default_launcher_configuration );
+}
+
+//
+// Create a mutable copy of another launcher configuration structure.
+//
+FoobarLauncherConfiguration* foobar_launcher_configuration_copy( FoobarLauncherConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+
+ FoobarLauncherConfiguration* copy = g_new0( FoobarLauncherConfiguration, 1 );
+ copy->width = self->width;
+ copy->position = self->position;
+ copy->max_height = self->max_height;
+ return copy;
+}
+
+//
+// Release resources associated with a launcher configuration structure.
+//
+void foobar_launcher_configuration_free( FoobarLauncherConfiguration* self )
+{
+ g_return_if_fail( self != NULL );
+
+ g_free( self );
+}
+
+//
+// Check if two launcher configuration structures are equal.
+//
+gboolean foobar_launcher_configuration_equal(
+ FoobarLauncherConfiguration const* a,
+ FoobarLauncherConfiguration const* b )
+{
+ g_return_val_if_fail( a != NULL, FALSE );
+ g_return_val_if_fail( b != NULL, FALSE );
+
+ if ( a->width != b->width ) { return FALSE; }
+ if ( a->position != b->position ) { return FALSE; }
+ if ( a->max_height != b->max_height ) { return FALSE; }
+
+ return TRUE;
+}
+
+//
+// Horizontal size of the launcher.
+//
+gint foobar_launcher_configuration_get_width( FoobarLauncherConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->width;
+}
+
+//
+// Offset from the top of the screen.
+//
+gint foobar_launcher_configuration_get_position( FoobarLauncherConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->position;
+}
+
+//
+// Maximum allowed height for the launcher before scrolling is enabled.
+//
+gint foobar_launcher_configuration_get_max_height( FoobarLauncherConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->max_height;
+}
+
+//
+// Horizontal size of the launcher.
+//
+void foobar_launcher_configuration_set_width(
+ FoobarLauncherConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->width = value;
+}
+
+//
+// Offset from the top of the screen.
+//
+void foobar_launcher_configuration_set_position(
+ FoobarLauncherConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->position = value;
+}
+
+//
+// Maximum allowed height for the launcher before scrolling is enabled.
+//
+void foobar_launcher_configuration_set_max_height(
+ FoobarLauncherConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->max_height = value;
+}
+
+//
+// Populate a launcher configuration structure from the "launcher" section of a keyfile.
+//
+void foobar_launcher_configuration_load(
+ FoobarLauncherConfiguration* self,
+ GKeyFile* file )
+{
+ gint width;
+ if ( try_get_int_value( file, "launcher", "width", VALIDATE_POSITIVE, &width ) )
+ {
+ foobar_launcher_configuration_set_width( self, width );
+ }
+
+ gint position;
+ if ( try_get_int_value( file, "launcher", "position", VALIDATE_NON_NEGATIVE, &position ) )
+ {
+ foobar_launcher_configuration_set_position( self, position );
+ }
+
+ gint max_height;
+ if ( try_get_int_value( file, "launcher", "max-height", VALIDATE_POSITIVE, &max_height ) )
+ {
+ foobar_launcher_configuration_set_position( self, max_height );
+ }
+}
+
+//
+// Store a launcher configuration structure in the "launcher" section of a keyfile.
+//
+void foobar_launcher_configuration_store(
+ FoobarLauncherConfiguration const* self,
+ GKeyFile* file )
+{
+ gint width = foobar_launcher_configuration_get_width( self );
+ g_key_file_set_integer( file, "launcher", "width", width );
+
+ gint position = foobar_launcher_configuration_get_position( self );
+ g_key_file_set_integer( file, "launcher", "position", position );
+ g_key_file_set_comment(
+ file,
+ "launcher",
+ "position",
+ " Offset from the top of the screen.",
+ NULL );
+
+ gint max_height = foobar_launcher_configuration_get_max_height( self );
+ g_key_file_set_integer( file, "launcher", "max-height", max_height );
+ g_key_file_set_comment(
+ file,
+ "launcher",
+ "max-height",
+ " Maximum allowed height for the launcher before scrolling is enabled.",
+ NULL );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Control Center Configuration
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a default control center configuration structure.
+//
+FoobarControlCenterConfiguration* foobar_control_center_configuration_new( void )
+{
+ return foobar_control_center_configuration_copy( &default_control_center_configuration );
+}
+
+//
+// Create a mutable copy of another control center configuration structure.
+//
+FoobarControlCenterConfiguration* foobar_control_center_configuration_copy( FoobarControlCenterConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+
+ FoobarControlCenterConfiguration* copy = g_new0( FoobarControlCenterConfiguration, 1 );
+ copy->width = self->width;
+ copy->height = self->height;
+ copy->position = self->position;
+ copy->offset = self->offset;
+ copy->padding = self->padding;
+ copy->spacing = self->spacing;
+ copy->orientation = self->orientation;
+ copy->alignment = self->alignment;
+ copy->rows = g_new( FoobarControlCenterRow, self->rows_count );
+ copy->rows_count = self->rows_count;
+ memcpy( copy->rows, self->rows, sizeof( *copy->rows ) * copy->rows_count );
+ return copy;
+}
+
+//
+// Release resources associated with a control center configuration structure.
+//
+void foobar_control_center_configuration_free( FoobarControlCenterConfiguration* self )
+{
+ g_return_if_fail( self != NULL );
+
+ g_free( self->rows );
+ g_free( self );
+}
+
+//
+// Check if two control center configuration structures are equal.
+//
+gboolean foobar_control_center_configuration_equal(
+ FoobarControlCenterConfiguration const* a,
+ FoobarControlCenterConfiguration const* b )
+{
+ g_return_val_if_fail( a != NULL, FALSE );
+ g_return_val_if_fail( b != NULL, FALSE );
+
+ if ( a->width != b->width ) { return FALSE; }
+ if ( a->height != b->height ) { return FALSE; }
+ if ( a->position != b->position ) { return FALSE; }
+ if ( a->offset != b->offset ) { return FALSE; }
+ if ( a->padding != b->padding ) { return FALSE; }
+ if ( a->spacing != b->spacing ) { return FALSE; }
+ if ( a->orientation != b->orientation ) { return FALSE; }
+ if ( a->alignment != b->alignment ) { return FALSE; }
+ if ( a->rows_count != b->rows_count ) { return FALSE; }
+ if ( memcmp( a->rows, b->rows, sizeof( *a->rows ) * a->rows_count ) ) { return FALSE; }
+
+ return TRUE;
+}
+
+//
+// Horizontal size of the control center.
+//
+gint foobar_control_center_configuration_get_width( FoobarControlCenterConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->width;
+}
+
+//
+// Vertical size of the control center.
+//
+gint foobar_control_center_configuration_get_height( FoobarControlCenterConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->height;
+}
+
+//
+// The screen edge which the control center should be attached to.
+//
+FoobarScreenEdge foobar_control_center_configuration_get_position( FoobarControlCenterConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->position;
+}
+
+//
+// Offset from the attached screen edge.
+//
+gint foobar_control_center_configuration_get_offset( FoobarControlCenterConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->offset;
+}
+
+//
+// Spacing between the edges of the control center and its items.
+//
+gint foobar_control_center_configuration_get_padding( FoobarControlCenterConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->padding;
+}
+
+//
+// Spacing between the items.
+//
+gint foobar_control_center_configuration_get_spacing( FoobarControlCenterConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->spacing;
+}
+
+//
+// The orientation used to arrange the controls and notifications sections.
+//
+FoobarOrientation foobar_control_center_configuration_get_orientation( FoobarControlCenterConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->orientation;
+}
+
+//
+// Alignment along the attached screen edge, or "fill".
+//
+FoobarControlCenterAlignment foobar_control_center_configuration_get_alignment( FoobarControlCenterConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->alignment;
+}
+
+//
+// Ordered list of rows to display in the controls section.
+//
+FoobarControlCenterRow const* foobar_control_center_configuration_get_rows(
+ FoobarControlCenterConfiguration const* self,
+ gsize* out_count )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ g_return_val_if_fail( out_count != NULL, NULL );
+
+ *out_count = self->rows_count;
+ return self->rows;
+}
+
+//
+// Horizontal size of the control center.
+//
+void foobar_control_center_configuration_set_width(
+ FoobarControlCenterConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->width = value;
+}
+
+//
+// Vertical size of the control center.
+//
+void foobar_control_center_configuration_set_height(
+ FoobarControlCenterConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->height = value;
+}
+
+//
+// The screen edge which the control center should be attached to.
+//
+void foobar_control_center_configuration_set_position(
+ FoobarControlCenterConfiguration* self,
+ FoobarScreenEdge value )
+{
+ g_return_if_fail( self != NULL );
+ self->position = value;
+}
+
+//
+// Offset from the attached screen edge.
+//
+void foobar_control_center_configuration_set_offset(
+ FoobarControlCenterConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->offset = value;
+}
+
+//
+// Spacing between the edges of the control center and its items.
+//
+void foobar_control_center_configuration_set_padding(
+ FoobarControlCenterConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->padding = value;
+}
+
+//
+// Spacing between the items.
+//
+void foobar_control_center_configuration_set_spacing(
+ FoobarControlCenterConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->spacing = value;
+}
+
+//
+// The orientation used to arrange the controls and notifications sections.
+//
+void foobar_control_center_configuration_set_orientation(
+ FoobarControlCenterConfiguration* self,
+ FoobarOrientation value )
+{
+ g_return_if_fail( self != NULL );
+ self->orientation = value;
+}
+
+//
+// Alignment along the attached screen edge, or "fill".
+//
+void foobar_control_center_configuration_set_alignment(
+ FoobarControlCenterConfiguration* self,
+ FoobarControlCenterAlignment value )
+{
+ g_return_if_fail( self != NULL );
+ self->alignment = value;
+}
+
+//
+// Ordered list of rows to display in the controls section.
+//
+void foobar_control_center_configuration_set_rows(
+ FoobarControlCenterConfiguration* self,
+ FoobarControlCenterRow const* value,
+ gsize value_count )
+{
+ g_return_if_fail( self != NULL );
+ g_return_if_fail( value != NULL || value_count == 0 );
+
+ g_free( self->rows );
+ self->rows = g_new( FoobarControlCenterRow, value_count );
+ self->rows_count = value_count;
+ memcpy( self->rows, value, sizeof( *self->rows ) * self->rows_count );
+}
+
+//
+// Populate a control center configuration structure from the "control-center" section of a keyfile.
+//
+void foobar_control_center_configuration_load(
+ FoobarControlCenterConfiguration* self,
+ GKeyFile* file )
+{
+ gint width;
+ if ( try_get_int_value( file, "control-center", "width", VALIDATE_POSITIVE, &width ) )
+ {
+ foobar_control_center_configuration_set_width( self, width );
+ }
+
+ gint height;
+ if ( try_get_int_value( file, "control-center", "height", VALIDATE_POSITIVE, &height ) )
+ {
+ foobar_control_center_configuration_set_height( self, height );
+ }
+
+ gint position;
+ if ( try_get_enum_value( file, "control-center", "position", FOOBAR_TYPE_SCREEN_EDGE, VALIDATE_NONE, &position ) )
+ {
+ foobar_control_center_configuration_set_position( self, position );
+ }
+
+ gint offset;
+ if ( try_get_int_value( file, "control-center", "offset", VALIDATE_NON_NEGATIVE, &offset ) )
+ {
+ foobar_control_center_configuration_set_offset( self, offset );
+ }
+
+ gint padding;
+ if ( try_get_int_value( file, "control-center", "padding", VALIDATE_NON_NEGATIVE, &padding ) )
+ {
+ foobar_control_center_configuration_set_padding( self, padding );
+ }
+
+ gint spacing;
+ if ( try_get_int_value( file, "control-center", "spacing", VALIDATE_NON_NEGATIVE, &spacing ) )
+ {
+ foobar_control_center_configuration_set_spacing( self, spacing );
+ }
+
+ gint orientation;
+ if ( try_get_enum_value( file, "control-center", "orientation", FOOBAR_TYPE_ORIENTATION, VALIDATE_NONE, &orientation ) )
+ {
+ foobar_control_center_configuration_set_orientation( self, orientation );
+ }
+
+ gint alignment;
+ if ( try_get_enum_value( file, "control-center", "alignment", FOOBAR_TYPE_CONTROL_CENTER_ALIGNMENT, VALIDATE_NONE, &alignment ) )
+ {
+ foobar_control_center_configuration_set_alignment( self, alignment );
+ }
+
+ g_autofree gpointer rows = NULL;
+ gsize rows_count;
+ if ( try_get_enum_list_value( file, "control-center", "rows", FOOBAR_TYPE_CONTROL_CENTER_ROW, VALIDATE_DISTINCT, &rows, &rows_count ) )
+ {
+ foobar_control_center_configuration_set_rows( self, rows, rows_count );
+ }
+}
+
+//
+// Store a control center configuration structure in the "control-center" section of a keyfile.
+//
+void foobar_control_center_configuration_store(
+ FoobarControlCenterConfiguration const* self,
+ GKeyFile* file )
+{
+ gint width = foobar_control_center_configuration_get_width( self );
+ g_key_file_set_integer( file, "control-center", "width", width );
+
+ gint height = foobar_control_center_configuration_get_height( self );
+ g_key_file_set_integer( file, "control-center", "height", height );
+
+ FoobarScreenEdge position = foobar_control_center_configuration_get_position( self );
+ g_key_file_set_string( file, "control-center", "position", enum_value_to_string( FOOBAR_TYPE_SCREEN_EDGE, position ) );
+ g_key_file_set_comment(
+ file,
+ "control-center",
+ "position",
+ " The screen edge which the control center should be attached to.",
+ NULL );
+
+ gint offset = foobar_control_center_configuration_get_offset( self );
+ g_key_file_set_integer( file, "control-center", "offset", offset );
+ g_key_file_set_comment(
+ file,
+ "control-center",
+ "offset",
+ " Offset from the attached screen edge.",
+ NULL );
+
+ gint padding = foobar_control_center_configuration_get_padding( self );
+ g_key_file_set_integer( file, "control-center", "padding", padding );
+ g_key_file_set_comment(
+ file,
+ "control-center",
+ "padding",
+ " Spacing between the edges of the control center and its items.",
+ NULL );
+
+ gint spacing = foobar_control_center_configuration_get_spacing( self );
+ g_key_file_set_integer( file, "control-center", "spacing", spacing );
+ g_key_file_set_comment(
+ file,
+ "control-center",
+ "spacing",
+ " Spacing between the items.",
+ NULL );
+
+ FoobarOrientation orientation = foobar_control_center_configuration_get_orientation( self );
+ g_key_file_set_string( file, "control-center", "orientation", enum_value_to_string( FOOBAR_TYPE_ORIENTATION, orientation ) );
+ g_key_file_set_comment(
+ file,
+ "control-center",
+ "orientation",
+ " The orientation used to arrange the controls and notifications sections.",
+ NULL );
+
+ FoobarControlCenterAlignment alignment = foobar_control_center_configuration_get_alignment( self );
+ g_key_file_set_string( file, "control-center", "alignment", enum_value_to_string( FOOBAR_TYPE_CONTROL_CENTER_ALIGNMENT, alignment ) );
+ g_key_file_set_comment(
+ file,
+ "control-center",
+ "alignment",
+ " Alignment along the attached screen edge, or \"fill\".",
+ NULL );
+
+ gsize rows_count;
+ FoobarControlCenterRow const* rows = foobar_control_center_configuration_get_rows( self, &rows_count );
+ g_autofree gchar const** rows_str = enum_list_to_string( FOOBAR_TYPE_CONTROL_CENTER_ROW, rows, rows_count );
+ g_key_file_set_string_list( file, "control-center", "rows", rows_str, rows_count );
+ g_key_file_set_comment(
+ file,
+ "control-center",
+ "rows",
+ " Ordered list of rows to display in the controls section.",
+ NULL );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Notification Configuration
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a default notification configuration structure.
+//
+FoobarNotificationConfiguration* foobar_notification_configuration_new( void )
+{
+ return foobar_notification_configuration_copy( &default_notification_configuration );
+}
+
+//
+// Create a mutable copy of another notification configuration structure.
+//
+FoobarNotificationConfiguration* foobar_notification_configuration_copy( FoobarNotificationConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+
+ FoobarNotificationConfiguration* copy = g_new0( FoobarNotificationConfiguration, 1 );
+ copy->width = self->width;
+ copy->min_height = self->min_height;
+ copy->spacing = self->spacing;
+ copy->close_button_inset = self->close_button_inset;
+ copy->time_format = g_strdup( self->time_format );
+ return copy;
+}
+
+//
+// Release resources associated with a notification configuration structure.
+//
+void foobar_notification_configuration_free( FoobarNotificationConfiguration* self )
+{
+ g_return_if_fail( self != NULL );
+
+ g_free( self->time_format );
+ g_free( self );
+}
+
+//
+// Check if two notification configuration structures are equal.
+//
+gboolean foobar_notification_configuration_equal(
+ FoobarNotificationConfiguration const* a,
+ FoobarNotificationConfiguration const* b )
+{
+ g_return_val_if_fail( a != NULL, FALSE );
+ g_return_val_if_fail( b != NULL, FALSE );
+
+ if ( a->width != b->width ) { return FALSE; }
+ if ( a->min_height != b->min_height ) { return FALSE; }
+ if ( a->spacing != b->spacing ) { return FALSE; }
+ if ( a->close_button_inset != b->close_button_inset ) { return FALSE; }
+ if ( g_strcmp0( a->time_format, b->time_format ) ) { return FALSE; }
+
+ return TRUE;
+}
+
+//
+// Horizontal size of notifications in the notification area.
+//
+gint foobar_notification_configuration_get_width( FoobarNotificationConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->width;
+}
+
+//
+// Minimum height for each notification.
+//
+gint foobar_notification_configuration_get_min_height( FoobarNotificationConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->min_height;
+}
+
+//
+// Spacing between notifications and from the screen edges.
+//
+gint foobar_notification_configuration_get_spacing( FoobarNotificationConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->spacing;
+}
+
+//
+// Inset of the close button within the notification (may be negative).
+//
+gint foobar_notification_configuration_get_close_button_inset( FoobarNotificationConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, 0 );
+ return self->close_button_inset;
+}
+
+//
+// The time format string as used by g_date_time_format.
+//
+gchar const* foobar_notification_configuration_get_time_format( FoobarNotificationConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ return self->time_format;
+}
+
+//
+// Horizontal size of notifications in the notification area.
+//
+void foobar_notification_configuration_set_width(
+ FoobarNotificationConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->width = value;
+}
+
+//
+// Minimum height for each notification.
+//
+void foobar_notification_configuration_set_min_height(
+ FoobarNotificationConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->min_height = value;
+}
+
+//
+// Spacing between notifications and from the screen edges.
+//
+void foobar_notification_configuration_set_spacing(
+ FoobarNotificationConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->spacing = value;
+}
+
+//
+// Inset of the close button within the notification (may be negative).
+//
+void foobar_notification_configuration_set_close_button_inset(
+ FoobarNotificationConfiguration* self,
+ gint value )
+{
+ g_return_if_fail( self != NULL );
+ self->close_button_inset = value;
+}
+
+//
+// The time format string as used by g_date_time_format.
+//
+void foobar_notification_configuration_set_time_format(
+ FoobarNotificationConfiguration* self,
+ gchar const* value )
+{
+ g_return_if_fail( self != NULL );
+ g_return_if_fail( value != NULL );
+
+ g_free( self->time_format );
+ self->time_format = g_strdup( value );
+}
+
+//
+// Populate a notification configuration structure from the "notifications" section of a keyfile.
+//
+void foobar_notification_configuration_load(
+ FoobarNotificationConfiguration* self,
+ GKeyFile* file )
+{
+ gint width;
+ if ( try_get_int_value( file, "notifications", "width", VALIDATE_POSITIVE, &width ) )
+ {
+ foobar_notification_configuration_set_width( self, width );
+ }
+
+ gint min_height;
+ if ( try_get_int_value( file, "notifications", "min-height", VALIDATE_NON_NEGATIVE, &min_height ) )
+ {
+ foobar_notification_configuration_set_min_height( self, min_height );
+ }
+
+ gint spacing;
+ if ( try_get_int_value( file, "notifications", "spacing", VALIDATE_NON_NEGATIVE, &spacing ) )
+ {
+ foobar_notification_configuration_set_spacing( self, spacing );
+ }
+
+ gint close_button_inset;
+ if ( try_get_int_value( file, "notifications", "close-button-inset", VALIDATE_NONE, &close_button_inset ) )
+ {
+ foobar_notification_configuration_set_close_button_inset( self, close_button_inset );
+ }
+
+ g_autofree gchar* time_format = NULL;
+ if ( try_get_string_value( file, "notifications", "time-format", VALIDATE_NONE, &time_format ) )
+ {
+ foobar_notification_configuration_set_time_format( self, time_format );
+ }
+}
+
+//
+// Store a notification configuration structure in the "notifications" section of a keyfile.
+//
+void foobar_notification_configuration_store(
+ FoobarNotificationConfiguration const* self,
+ GKeyFile* file )
+{
+ gint width = foobar_notification_configuration_get_width( self );
+ g_key_file_set_integer( file, "notifications", "width", width );
+
+ gint min_height = foobar_notification_configuration_get_min_height( self );
+ g_key_file_set_integer( file, "notifications", "min-height", min_height );
+ g_key_file_set_comment(
+ file,
+ "notifications",
+ "min-height",
+ " Minimum height for each notification.",
+ NULL );
+
+ gint spacing = foobar_notification_configuration_get_spacing( self );
+ g_key_file_set_integer( file, "notifications", "spacing", spacing );
+ g_key_file_set_comment(
+ file,
+ "notifications",
+ "spacing",
+ " Spacing between notifications and from the screen edges.",
+ NULL );
+
+ gint close_button_inset = foobar_notification_configuration_get_close_button_inset( self );
+ g_key_file_set_integer( file, "notifications", "close-button-inset", close_button_inset );
+ g_key_file_set_comment(
+ file,
+ "notifications",
+ "close-button-inset",
+ " Inset of the close button within the notification (may be negative).",
+ NULL );
+
+ gchar const* time_format = foobar_notification_configuration_get_time_format( self );
+ g_key_file_set_string( file, "notifications", "time-format", time_format );
+ g_key_file_set_comment(
+ file,
+ "notifications",
+ "time-format",
+ " The time format string as used by g_date_time_format.",
+ NULL );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Configuration
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a default configuration structure.
+//
+FoobarConfiguration* foobar_configuration_new( void )
+{
+ return foobar_configuration_copy( &default_configuration );
+}
+
+//
+// Create a mutable copy of another configuration structure.
+//
+FoobarConfiguration* foobar_configuration_copy( FoobarConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+
+ FoobarConfiguration* copy = g_new0( FoobarConfiguration, 1 );
+ copy->general = foobar_general_configuration_copy( self->general );
+ copy->panel = foobar_panel_configuration_copy( self->panel );
+ copy->launcher = foobar_launcher_configuration_copy( self->launcher );
+ copy->control_center = foobar_control_center_configuration_copy( self->control_center );
+ copy->notifications = foobar_notification_configuration_copy( self->notifications );
+ return copy;
+}
+
+//
+// Release resources associated with a configuration structure.
+//
+void foobar_configuration_free( FoobarConfiguration* self )
+{
+ g_return_if_fail( self != NULL );
+
+ foobar_general_configuration_free( self->general );
+ foobar_panel_configuration_free( self->panel );
+ foobar_launcher_configuration_free( self->launcher );
+ foobar_control_center_configuration_free( self->control_center );
+ foobar_notification_configuration_free( self->notifications );
+ g_free( self );
+}
+
+//
+// Check if two configuration structures are equal.
+//
+gboolean foobar_configuration_equal(
+ FoobarConfiguration const* a,
+ FoobarConfiguration const* b )
+{
+ g_return_val_if_fail( a != NULL, FALSE );
+ g_return_val_if_fail( b != NULL, FALSE );
+
+ if ( !foobar_general_configuration_equal( a->general, b->general ) ) { return FALSE; }
+ if ( !foobar_panel_configuration_equal( a->panel, b->panel ) ) { return FALSE; }
+ if ( !foobar_launcher_configuration_equal( a->launcher, b->launcher ) ) { return FALSE; }
+ if ( !foobar_control_center_configuration_equal( a->control_center, b->control_center ) ) { return FALSE; }
+ if ( !foobar_notification_configuration_equal( a->notifications, b->notifications ) ) { return FALSE; }
+
+ return TRUE;
+}
+
+//
+// Get the general application configuration.
+//
+FoobarGeneralConfiguration const* foobar_configuration_get_general( FoobarConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ return self->general;
+}
+
+//
+// Get a mutable reference to the general application configuration.
+//
+FoobarGeneralConfiguration* foobar_configuration_get_general_mut( FoobarConfiguration* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ return self->general;
+}
+
+//
+// Get the panel configuration.
+//
+FoobarPanelConfiguration const* foobar_configuration_get_panel( FoobarConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ return self->panel;
+}
+
+//
+// Get a mutable reference to the panel configuration.
+//
+FoobarPanelConfiguration* foobar_configuration_get_panel_mut( FoobarConfiguration* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ return self->panel;
+}
+
+//
+// Get the launcher configuration.
+//
+FoobarLauncherConfiguration const* foobar_configuration_get_launcher( FoobarConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ return self->launcher;
+}
+
+//
+// Get a mutable reference to the launcher configuration.
+//
+FoobarLauncherConfiguration* foobar_configuration_get_launcher_mut( FoobarConfiguration* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ return self->launcher;
+}
+
+//
+// Get the control center configuration.
+//
+FoobarControlCenterConfiguration const* foobar_configuration_get_control_center( FoobarConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ return self->control_center;
+}
+
+//
+// Get a mutable reference to the control center configuration.
+//
+FoobarControlCenterConfiguration* foobar_configuration_get_control_center_mut( FoobarConfiguration* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ return self->control_center;
+}
+
+//
+// Get the notification configuration.
+//
+FoobarNotificationConfiguration const* foobar_configuration_get_notifications( FoobarConfiguration const* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ return self->notifications;
+}
+
+//
+// Get a mutable reference to the notification configuration.
+//
+FoobarNotificationConfiguration* foobar_configuration_get_notifications_mut( FoobarConfiguration* self )
+{
+ g_return_val_if_fail( self != NULL, NULL );
+ return self->notifications;
+}
+
+//
+// Populate a configuration structure from a keyfile.
+//
+void foobar_configuration_load(
+ FoobarConfiguration* self,
+ GKeyFile* file )
+{
+ foobar_general_configuration_load( foobar_configuration_get_general_mut( self ), file );
+ foobar_panel_configuration_load( foobar_configuration_get_panel_mut( self ), file );
+ foobar_launcher_configuration_load( foobar_configuration_get_launcher_mut( self ), file );
+ foobar_control_center_configuration_load( foobar_configuration_get_control_center_mut( self ), file );
+ foobar_notification_configuration_load( foobar_configuration_get_notifications_mut( self ), file );
+}
+
+//
+// Store a configuration structure in a keyfile.
+//
+void foobar_configuration_store(
+ FoobarConfiguration const* self,
+ GKeyFile* file )
+{
+ foobar_general_configuration_store( foobar_configuration_get_general( self ), file );
+ foobar_panel_configuration_store( foobar_configuration_get_panel( self ), file );
+ foobar_launcher_configuration_store( foobar_configuration_get_launcher( self ), file );
+ foobar_control_center_configuration_store( foobar_configuration_get_control_center( self ), file );
+ foobar_notification_configuration_store( foobar_configuration_get_notifications( self ), file );
+}
+
+//
+// Populate a configuration structure from a keyfile at the given path.
+//
+void foobar_configuration_load_from_file(
+ FoobarConfiguration* self,
+ gchar const* path )
+{
+ g_autoptr( GKeyFile ) file = g_key_file_new( );
+ g_autoptr( GError ) error = NULL;
+ if ( !g_key_file_load_from_file( file, path, G_KEY_FILE_NONE, &error ) )
+ {
+ CONFIGURATION_WARNING( "%s", error->message );
+ return;
+ }
+ foobar_configuration_load( self, file );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Service Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the configuration service.
+//
+void foobar_configuration_service_class_init( FoobarConfigurationServiceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_configuration_service_get_property;
+ object_klass->finalize = foobar_configuration_service_finalize;
+
+ props[PROP_CURRENT] = g_param_spec_boxed(
+ "current",
+ "Current",
+ "The current configuration state.",
+ FOOBAR_TYPE_CONFIGURATION,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for the configuration service.
+//
+void foobar_configuration_service_init( FoobarConfigurationService* self )
+{
+ self->path = g_strdup_printf( "%s/foobar.conf", g_get_user_config_dir( ) );
+ self->current = foobar_configuration_new( );
+ g_return_if_fail( self->path != NULL );
+
+ // Ensure that the file at path exists.
+
+ if ( g_file_test( self->path, G_FILE_TEST_EXISTS ) )
+ {
+ foobar_configuration_load_from_file( self->current, self->path );
+ }
+ else
+ {
+ g_info( "Config file does not exist -- falling back to default config and creating a copy of it." );
+ g_autoptr( GKeyFile ) file = g_key_file_new( );
+ foobar_configuration_store( self->current, file );
+ g_key_file_save_to_file( file, self->path, NULL );
+ }
+
+ // Monitor the config file.
+
+ self->monitor = foobar_configuration_service_create_monitor( self->path );
+ if ( self->monitor )
+ {
+ self->changed_handler_id = g_signal_connect(
+ self->monitor,
+ "changed",
+ G_CALLBACK( foobar_configuration_service_handle_changed ),
+ self );
+ }
+
+ // In case the config is symlinked, also watch the actual file for content changes.
+
+ self->actual_path = realpath( self->path, NULL );
+ if ( self->actual_path && g_strcmp0( self->actual_path, self->path ) )
+ {
+ self->actual_monitor = foobar_configuration_service_create_monitor( self->actual_path );
+ if ( self->actual_monitor )
+ {
+ self->actual_changed_handler_id = g_signal_connect(
+ self->actual_monitor,
+ "changed",
+ G_CALLBACK( foobar_configuration_service_handle_actual_changed ),
+ self );
+ }
+ }
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_configuration_service_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarConfigurationService* self = (FoobarConfigurationService*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_CURRENT:
+ g_value_set_boxed( value, foobar_configuration_service_get_current( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for the configuration service.
+//
+void foobar_configuration_service_finalize( GObject* object )
+{
+ FoobarConfigurationService* self = (FoobarConfigurationService*)object;
+
+ g_clear_handle_id( &self->update_source_id, g_source_remove );
+ g_clear_signal_handler( &self->changed_handler_id, self->monitor );
+ g_clear_signal_handler( &self->actual_changed_handler_id, self->actual_monitor );
+ g_clear_object( &self->monitor );
+ g_clear_object( &self->actual_monitor );
+ g_clear_pointer( &self->current, foobar_configuration_free );
+ g_clear_pointer( &self->path, g_free );
+ g_clear_pointer( &self->actual_path, free );
+
+ G_OBJECT_CLASS( foobar_configuration_service_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new configuration service instance.
+//
+FoobarConfigurationService* foobar_configuration_service_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_CONFIGURATION_SERVICE, NULL );
+}
+
+//
+// Get the current configuration state.
+//
+FoobarConfiguration const* foobar_configuration_service_get_current( FoobarConfigurationService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( self ), NULL );
+ return self->current;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called when the file at self->path has changed.
+//
+void foobar_configuration_service_handle_changed(
+ GFileMonitor* monitor,
+ GFile* file,
+ GFile* other_file,
+ GFileMonitorEvent event_type,
+ gpointer userdata )
+{
+ (void)monitor;
+ (void)file;
+ (void)other_file;
+ FoobarConfigurationService* self = (FoobarConfigurationService*)userdata;
+
+ // If this file is a symlink, the actual path might have changed.
+
+ char* actual_path = realpath( self->path, NULL );
+ if ( g_strcmp0( self->actual_path, actual_path ) )
+ {
+ free(self->actual_path);
+ self->actual_path = g_steal_pointer( &actual_path );
+
+ // Recreate the file monitor.
+
+ g_clear_signal_handler( &self->actual_changed_handler_id, self->actual_monitor );
+ g_clear_object( &self->actual_monitor );
+
+ if ( self->actual_path && g_strcmp0( self->actual_path, self->path ) )
+ {
+ self->actual_monitor = foobar_configuration_service_create_monitor( self->actual_path );
+ if ( self->actual_monitor )
+ {
+ self->actual_changed_handler_id = g_signal_connect(
+ self->actual_monitor,
+ "changed",
+ G_CALLBACK( foobar_configuration_service_handle_actual_changed ),
+ self );
+ }
+ }
+ }
+ free(actual_path);
+
+ // Handle actual file changes.
+
+ if ( event_type == G_FILE_MONITOR_EVENT_CREATED || event_type == G_FILE_MONITOR_EVENT_CHANGED )
+ {
+ foobar_configuration_service_update( self );
+ }
+}
+
+//
+// Called when the actual file (the one at self->actual_path) behind the symlink has changed.
+//
+void foobar_configuration_service_handle_actual_changed(
+ GFileMonitor* monitor,
+ GFile* file,
+ GFile* other_file,
+ GFileMonitorEvent event_type,
+ gpointer userdata )
+{
+ (void)monitor;
+ (void)file;
+ (void)other_file;
+ FoobarConfigurationService* self = (FoobarConfigurationService*)userdata;
+
+ if ( event_type == G_FILE_MONITOR_EVENT_CREATED || event_type == G_FILE_MONITOR_EVENT_CHANGED )
+ {
+ foobar_configuration_service_update( self );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Schedule a configuration file reload, used to coalesce change events.
+//
+void foobar_configuration_service_update( FoobarConfigurationService* self )
+{
+ if ( !self->update_source_id )
+ {
+ self->update_source_id = g_timeout_add(
+ UPDATE_APPLY_DELAY,
+ foobar_configuration_service_update_cb,
+ self );
+ }
+}
+
+//
+// Reload the configuration file.
+//
+gboolean foobar_configuration_service_update_cb( gpointer userdata )
+{
+ FoobarConfigurationService* self = (FoobarConfigurationService*)userdata;
+
+ FoobarConfiguration* updated = foobar_configuration_new( );
+ foobar_configuration_load_from_file( updated, self->path );
+ if ( !foobar_configuration_equal( self->current, updated ) )
+ {
+ foobar_configuration_free( self->current );
+ self->current = g_steal_pointer( &updated );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CURRENT] );
+ g_info( "Config reloaded." );
+ }
+ g_clear_pointer( &updated, foobar_configuration_free );
+
+ self->update_source_id = 0;
+ return G_SOURCE_REMOVE;
+}
+
+//
+// Create a new file monitor, logging a warning on error.
+//
+GFileMonitor* foobar_configuration_service_create_monitor( gchar const* path )
+{
+ g_autoptr( GError ) error = NULL;
+ g_autoptr( GFile ) file = g_file_new_for_path( path );
+ GFileMonitor* monitor = g_file_monitor( file, G_FILE_MONITOR_NONE, NULL, &error );
+ if ( !monitor )
+ {
+ g_warning( "Unable to monitor config file: %s", error->message );
+ }
+
+ return monitor;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Parsing Helpers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Get and validate a string value from a keyfile. *out should always be freed, even if FALSE is returned.
+//
+gboolean try_get_string_value(
+ GKeyFile* file,
+ gchar const* group,
+ gchar const* key,
+ ValidationFlags validation,
+ gchar** out )
+{
+ if ( !g_key_file_has_key( file, group, key, NULL ) ) { return FALSE; }
+
+ g_autoptr( GError ) error = NULL;
+ *out = g_key_file_get_string( file, group, key, &error );
+ if ( !*out )
+ {
+ CONFIGURATION_WARNING( "%s:%s: %s", group, key, error->message );
+ return FALSE;
+ }
+
+ if ( validation & VALIDATE_FILE_URL )
+ {
+ g_autoptr( GFile ) test_file = g_file_new_for_uri( *out );
+ if ( !g_file_query_exists( test_file, NULL ) )
+ {
+ CONFIGURATION_WARNING( "%s:%s: A file with the URL \"%s\" does not exist.", group, key, *out );
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+//
+// Get and validate a list of string values from a keyfile. *out should always be freed, even if FALSE is returned.
+//
+gboolean try_get_string_list_value(
+ GKeyFile* file,
+ gchar const* group,
+ gchar const* key,
+ ValidationFlags validation,
+ GStrv* out,
+ gsize* out_count )
+{
+ if ( !g_key_file_has_key( file, group, key, NULL ) ) { return FALSE; }
+
+ g_autoptr( GError ) error = NULL;
+ *out = g_key_file_get_string_list( file, group, key, out_count, &error );
+ if ( !*out )
+ {
+ CONFIGURATION_WARNING( "%s:%s: %s", group, key, error->message );
+ return FALSE;
+ }
+
+ if ( validation & VALIDATE_DISTINCT )
+ {
+ g_autoptr( GHashTable ) items = g_hash_table_new( g_str_hash, g_str_equal );
+ for ( gchar** it = *out; *it; ++it )
+ {
+ if ( !g_hash_table_add( items, *it ) )
+ {
+ CONFIGURATION_WARNING( "%s:%s: Duplicate value \"%s\" not allowed.", group, key, *it );
+ return FALSE;
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+//
+// Get and validate an integer value from a keyfile.
+//
+gboolean try_get_int_value(
+ GKeyFile* file,
+ gchar const* group,
+ gchar const* key,
+ ValidationFlags validation,
+ gint* out )
+{
+ if ( !g_key_file_has_key( file, group, key, NULL ) ) { return FALSE; }
+
+ g_autoptr( GError ) error = NULL;
+ *out = g_key_file_get_integer( file, group, key, &error );
+ if ( error )
+ {
+ CONFIGURATION_WARNING( "%s:%s: %s", group, key, error->message );
+ return FALSE;
+ }
+
+ if ( validation & VALIDATE_NON_NEGATIVE )
+ {
+ if ( *out < 0 )
+ {
+ CONFIGURATION_WARNING( "%s:%s: Value is not allowed to be negative.", group, key );
+ return FALSE;
+ }
+ }
+
+ if ( validation & VALIDATE_NON_ZERO )
+ {
+ if ( *out == 0 )
+ {
+ CONFIGURATION_WARNING( "%s:%s: Value is not allowed to be 0.", group, key );
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+//
+// Get and validate an enum value (encoded as a string) from a keyfile.
+//
+gboolean try_get_enum_value(
+ GKeyFile* file,
+ gchar const* group,
+ gchar const* key,
+ GType enum_type,
+ ValidationFlags validation,
+ gint* out )
+{
+ (void)validation;
+
+ g_autofree gchar* str = NULL;
+ if ( !try_get_string_value( file, group, key, VALIDATE_NONE, &str ) ) { return FALSE; }
+
+ g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type );
+ GEnumValue* val = g_enum_get_value_by_nick( enum_klass, str );
+ if ( !val )
+ {
+ g_autofree gchar* help_text = enum_format_help_text( enum_type );
+ CONFIGURATION_WARNING( "%s:%s: Invalid enum value \"%s\" (allowed values: %s).", group, key, str, help_text );
+ return FALSE;
+ }
+
+ *out = val->value;
+ return TRUE;
+}
+
+//
+// Get and validate a list of enum values (encoded as strings) from a keyfile. *out should always be freed, even if
+// FALSE is returned.
+//
+gboolean try_get_enum_list_value(
+ GKeyFile* file,
+ gchar const* group,
+ gchar const* key,
+ GType enum_type,
+ ValidationFlags validation,
+ gpointer* out,
+ gsize* out_count )
+{
+ g_auto( GStrv ) str_values = NULL;
+ if ( !try_get_string_list_value( file, group, key, validation, &str_values, out_count ) ) { return FALSE; }
+
+ gint* untyped_out = g_new0( gint, *out_count );
+ *out = untyped_out;
+
+ g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type );
+ for ( gsize i = 0; i < *out_count; ++i )
+ {
+ GEnumValue* val = g_enum_get_value_by_nick( enum_klass, str_values[i] );
+ if ( !val )
+ {
+ g_autofree gchar* help_text = enum_format_help_text( enum_type );
+ CONFIGURATION_WARNING( "%s:%s: Invalid enum value \"%s\" (allowed values: %s).", group, key, str_values[i], help_text );
+ return FALSE;
+ }
+
+ untyped_out[i] = val->value;
+ }
+
+ return TRUE;
+}
+
+//
+// Get and validate a boolean value from a keyfile.
+//
+gboolean try_get_boolean_value(
+ GKeyFile* file,
+ gchar const* group,
+ gchar const* key,
+ ValidationFlags validation,
+ gboolean* out )
+{
+ (void)validation;
+
+ if ( !g_key_file_has_key( file, group, key, NULL ) ) { return FALSE; }
+
+ g_autoptr( GError ) error = NULL;
+ *out = g_key_file_get_boolean( file, group, key, &error );
+ if ( error )
+ {
+ CONFIGURATION_WARNING( "%s:%s: %s", group, key, error->message );
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Serialization
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Get the string constant for an enum value.
+//
+gchar const* enum_value_to_string(
+ GType enum_type,
+ gint value )
+{
+ g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type );
+ GEnumValue* val = g_enum_get_value( enum_klass, value );
+ return val->value_nick;
+}
+
+//
+// Get a list of string constants for a list of enum values. Only the list itself needs to be freed.
+//
+gchar const** enum_list_to_string(
+ GType enum_type,
+ gconstpointer values,
+ gsize values_count )
+{
+ gint const* untyped_values = values;
+ gchar const** result = g_new0( gchar const*, values_count );
+
+ g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type );
+ for ( gsize i = 0; i < values_count; ++i )
+ {
+ GEnumValue* val = g_enum_get_value( enum_klass, untyped_values[i] );
+ result[i] = val->value_nick;
+ }
+
+ return result;
+}
+
+//
+// Return a comma-separated list of all allowed values for an enum type. The result needs to be freed.
+//
+gchar* enum_format_help_text( GType enum_type )
+{
+ GString* result = g_string_new( NULL );
+ g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type );
+
+ gboolean is_first = TRUE;
+ for ( guint i = 0; i < enum_klass->n_values; ++i )
+ {
+ g_string_append_printf( result, is_first ? "\"%s\"" : ", \"%s\"", enum_klass->values[i].value_nick );
+ is_first = FALSE;
+ }
+
+ return g_string_free_and_steal( result );
+} \ No newline at end of file
diff --git a/src/services/configuration-service.h b/src/services/configuration-service.h
new file mode 100644
index 0000000..c898ae7
--- /dev/null
+++ b/src/services/configuration-service.h
@@ -0,0 +1,305 @@
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_SCREEN_EDGE foobar_screen_edge_get_type( )
+#define FOOBAR_TYPE_ORIENTATION foobar_orientation_get_type( )
+#define FOOBAR_TYPE_STATUS_ITEM foobar_status_item_get_type( )
+#define FOOBAR_TYPE_PANEL_ITEM_KIND foobar_panel_item_kind_get_type( )
+#define FOOBAR_TYPE_PANEL_ITEM_ACTION foobar_panel_item_action_get_type( )
+#define FOOBAR_TYPE_PANEL_ITEM_POSITION foobar_panel_item_position_get_type( )
+#define FOOBAR_TYPE_CONTROL_CENTER_ROW foobar_control_center_row_get_type( )
+#define FOOBAR_TYPE_CONTROL_CENTER_ALIGNMENT foobar_control_center_alignment_get_type( )
+#define FOOBAR_TYPE_GENERAL_CONFIGURATION foobar_general_configuration_get_type( )
+#define FOOBAR_TYPE_PANEL_ITEM_CONFIGURATION foobar_panel_item_configuration_get_type( )
+#define FOOBAR_TYPE_PANEL_CONFIGURATION foobar_panel_configuration_get_type( )
+#define FOOBAR_TYPE_LAUNCHER_CONFIGURATION foobar_launcher_configuration_get_type( )
+#define FOOBAR_TYPE_CONTROL_CENTER_CONFIGURATION foobar_control_center_configuration_get_type( )
+#define FOOBAR_TYPE_NOTIFICATION_CONFIGURATION foobar_notification_configuration_get_type( )
+#define FOOBAR_TYPE_CONFIGURATION foobar_configuration_get_type( )
+#define FOOBAR_TYPE_CONFIGURATION_SERVICE foobar_configuration_service_get_type( )
+
+typedef enum
+{
+ FOOBAR_SCREEN_EDGE_LEFT,
+ FOOBAR_SCREEN_EDGE_RIGHT,
+ FOOBAR_SCREEN_EDGE_TOP,
+ FOOBAR_SCREEN_EDGE_BOTTOM,
+} FoobarScreenEdge;
+
+GType foobar_screen_edge_get_type( void );
+
+typedef enum
+{
+ FOOBAR_ORIENTATION_HORIZONTAL,
+ FOOBAR_ORIENTATION_VERTICAL,
+} FoobarOrientation;
+
+GType foobar_orientation_get_type( void );
+
+typedef enum
+{
+ FOOBAR_STATUS_ITEM_NETWORK,
+ FOOBAR_STATUS_ITEM_BLUETOOTH,
+ FOOBAR_STATUS_ITEM_BATTERY,
+ FOOBAR_STATUS_ITEM_BRIGHTNESS,
+ FOOBAR_STATUS_ITEM_AUDIO,
+ FOOBAR_STATUS_ITEM_NOTIFICATIONS,
+} FoobarStatusItem;
+
+GType foobar_status_item_get_type( void );
+
+typedef enum
+{
+ FOOBAR_PANEL_ITEM_KIND_ICON,
+ FOOBAR_PANEL_ITEM_KIND_CLOCK,
+ FOOBAR_PANEL_ITEM_KIND_WORKSPACES,
+ FOOBAR_PANEL_ITEM_KIND_STATUS,
+} FoobarPanelItemKind;
+
+GType foobar_panel_item_kind_get_type( void );
+
+typedef enum
+{
+ FOOBAR_PANEL_ITEM_ACTION_NONE,
+ FOOBAR_PANEL_ITEM_ACTION_LAUNCHER,
+ FOOBAR_PANEL_ITEM_ACTION_CONTROL_CENTER,
+} FoobarPanelItemAction;
+
+GType foobar_panel_item_action_get_type( void );
+
+typedef enum
+{
+ FOOBAR_PANEL_ITEM_POSITION_START,
+ FOOBAR_PANEL_ITEM_POSITION_CENTER,
+ FOOBAR_PANEL_ITEM_POSITION_END,
+} FoobarPanelItemPosition;
+
+GType foobar_panel_item_position_get_type( void );
+
+typedef enum
+{
+ FOOBAR_CONTROL_CENTER_ROW_CONNECTIVITY,
+ FOOBAR_CONTROL_CENTER_ROW_AUDIO_OUTPUT,
+ FOOBAR_CONTROL_CENTER_ROW_AUDIO_INPUT,
+ FOOBAR_CONTROL_CENTER_ROW_BRIGHTNESS,
+} FoobarControlCenterRow;
+
+GType foobar_control_center_row_get_type( void );
+
+typedef enum
+{
+ FOOBAR_CONTROL_CENTER_ALIGNMENT_START,
+ FOOBAR_CONTROL_CENTER_ALIGNMENT_CENTER,
+ FOOBAR_CONTROL_CENTER_ALIGNMENT_END,
+ FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL,
+} FoobarControlCenterAlignment;
+
+GType foobar_control_center_alignment_get_type( void );
+
+typedef struct _FoobarGeneralConfiguration FoobarGeneralConfiguration;
+
+GType foobar_general_configuration_get_type ( void );
+FoobarGeneralConfiguration* foobar_general_configuration_new ( void );
+FoobarGeneralConfiguration* foobar_general_configuration_copy ( FoobarGeneralConfiguration const* self );
+void foobar_general_configuration_free ( FoobarGeneralConfiguration* self );
+gboolean foobar_general_configuration_equal ( FoobarGeneralConfiguration const* a,
+ FoobarGeneralConfiguration const* b );
+gchar const* foobar_general_configuration_get_stylesheet( FoobarGeneralConfiguration const* self );
+void foobar_general_configuration_set_stylesheet( FoobarGeneralConfiguration* self,
+ gchar const* value );
+
+typedef struct _FoobarPanelItemConfiguration FoobarPanelItemConfiguration;
+
+GType foobar_panel_item_configuration_get_type ( void );
+FoobarPanelItemConfiguration* foobar_panel_item_configuration_copy ( FoobarPanelItemConfiguration const* self );
+void foobar_panel_item_configuration_free ( FoobarPanelItemConfiguration* self );
+gboolean foobar_panel_item_configuration_equal ( FoobarPanelItemConfiguration const* a,
+ FoobarPanelItemConfiguration const* b );
+FoobarPanelItemKind foobar_panel_item_configuration_get_kind ( FoobarPanelItemConfiguration const* self );
+gchar const* foobar_panel_item_configuration_get_name ( FoobarPanelItemConfiguration const* self );
+FoobarPanelItemPosition foobar_panel_item_configuration_get_position( FoobarPanelItemConfiguration const* self );
+FoobarPanelItemAction foobar_panel_item_configuration_get_action ( FoobarPanelItemConfiguration const* self );
+void foobar_panel_item_configuration_set_name ( FoobarPanelItemConfiguration* self,
+ gchar const* value );
+void foobar_panel_item_configuration_set_position( FoobarPanelItemConfiguration* self,
+ FoobarPanelItemPosition value );
+void foobar_panel_item_configuration_set_action ( FoobarPanelItemConfiguration* self,
+ FoobarPanelItemAction value );
+
+FoobarPanelItemConfiguration* foobar_panel_item_icon_configuration_new ( void );
+gchar const* foobar_panel_item_icon_configuration_get_icon_name( FoobarPanelItemConfiguration const* self );
+void foobar_panel_item_icon_configuration_set_icon_name( FoobarPanelItemConfiguration* self,
+ gchar const* value );
+
+FoobarPanelItemConfiguration* foobar_panel_item_clock_configuration_new ( void );
+gchar const* foobar_panel_item_clock_configuration_get_format( FoobarPanelItemConfiguration const* self );
+void foobar_panel_item_clock_configuration_set_format( FoobarPanelItemConfiguration* self,
+ gchar const* value );
+
+FoobarPanelItemConfiguration* foobar_panel_item_workspaces_configuration_new ( void );
+gint foobar_panel_item_workspaces_configuration_get_button_size( FoobarPanelItemConfiguration const* self );
+gint foobar_panel_item_workspaces_configuration_get_spacing ( FoobarPanelItemConfiguration const* self );
+void foobar_panel_item_workspaces_configuration_set_button_size( FoobarPanelItemConfiguration* self,
+ gint value );
+void foobar_panel_item_workspaces_configuration_set_spacing ( FoobarPanelItemConfiguration* self,
+ gint value );
+
+FoobarPanelItemConfiguration* foobar_panel_item_status_configuration_new ( void );
+FoobarStatusItem const* foobar_panel_item_status_configuration_get_items ( FoobarPanelItemConfiguration const* self,
+ gsize* out_count );
+gint foobar_panel_item_status_configuration_get_spacing ( FoobarPanelItemConfiguration const* self );
+gboolean foobar_panel_item_status_configuration_get_show_labels ( FoobarPanelItemConfiguration const* self );
+gboolean foobar_panel_item_status_configuration_get_enable_scrolling( FoobarPanelItemConfiguration const* self );
+void foobar_panel_item_status_configuration_set_items ( FoobarPanelItemConfiguration* self,
+ FoobarStatusItem const* value,
+ gsize value_count );
+void foobar_panel_item_status_configuration_set_spacing ( FoobarPanelItemConfiguration* self,
+ gint value );
+void foobar_panel_item_status_configuration_set_show_labels ( FoobarPanelItemConfiguration* self,
+ gboolean value );
+void foobar_panel_item_status_configuration_set_enable_scrolling( FoobarPanelItemConfiguration* self,
+ gboolean value );
+
+typedef struct _FoobarPanelConfiguration FoobarPanelConfiguration;
+
+GType foobar_panel_configuration_get_type ( void );
+FoobarPanelConfiguration* foobar_panel_configuration_new ( void );
+FoobarPanelConfiguration* foobar_panel_configuration_copy ( FoobarPanelConfiguration const* self );
+void foobar_panel_configuration_free ( FoobarPanelConfiguration* self );
+gboolean foobar_panel_configuration_equal ( FoobarPanelConfiguration const* a,
+ FoobarPanelConfiguration const* b );
+FoobarScreenEdge foobar_panel_configuration_get_position ( FoobarPanelConfiguration const* self );
+gint foobar_panel_configuration_get_margin ( FoobarPanelConfiguration const* self );
+gint foobar_panel_configuration_get_padding ( FoobarPanelConfiguration const* self );
+gint foobar_panel_configuration_get_size ( FoobarPanelConfiguration const* self );
+gint foobar_panel_configuration_get_spacing ( FoobarPanelConfiguration const* self );
+gboolean foobar_panel_configuration_get_multi_monitor( FoobarPanelConfiguration const* self );
+FoobarPanelItemConfiguration const* const* foobar_panel_configuration_get_items ( FoobarPanelConfiguration const* self,
+ gsize* out_count );
+FoobarPanelItemConfiguration* const* foobar_panel_configuration_get_items_mut ( FoobarPanelConfiguration* self,
+ gsize* out_count );
+void foobar_panel_configuration_set_position ( FoobarPanelConfiguration* self,
+ FoobarScreenEdge value );
+void foobar_panel_configuration_set_margin ( FoobarPanelConfiguration* self,
+ gint value );
+void foobar_panel_configuration_set_padding ( FoobarPanelConfiguration* self,
+ gint value );
+void foobar_panel_configuration_set_size ( FoobarPanelConfiguration* self,
+ gint value );
+void foobar_panel_configuration_set_spacing ( FoobarPanelConfiguration* self,
+ gint value );
+void foobar_panel_configuration_set_multi_monitor( FoobarPanelConfiguration* self,
+ gboolean value );
+void foobar_panel_configuration_set_items ( FoobarPanelConfiguration* self,
+ FoobarPanelItemConfiguration const* const* value,
+ gsize value_count );
+
+typedef struct _FoobarLauncherConfiguration FoobarLauncherConfiguration;
+
+GType foobar_launcher_configuration_get_type ( void );
+FoobarLauncherConfiguration* foobar_launcher_configuration_new ( void );
+FoobarLauncherConfiguration* foobar_launcher_configuration_copy ( FoobarLauncherConfiguration const* self );
+void foobar_launcher_configuration_free ( FoobarLauncherConfiguration* self );
+gboolean foobar_launcher_configuration_equal ( FoobarLauncherConfiguration const* a,
+ FoobarLauncherConfiguration const* b );
+gint foobar_launcher_configuration_get_width ( FoobarLauncherConfiguration const* self );
+gint foobar_launcher_configuration_get_position ( FoobarLauncherConfiguration const* self );
+gint foobar_launcher_configuration_get_max_height( FoobarLauncherConfiguration const* self );
+void foobar_launcher_configuration_set_width ( FoobarLauncherConfiguration* self,
+ gint value );
+void foobar_launcher_configuration_set_position ( FoobarLauncherConfiguration* self,
+ gint value );
+void foobar_launcher_configuration_set_max_height( FoobarLauncherConfiguration* self,
+ gint value );
+
+typedef struct _FoobarControlCenterConfiguration FoobarControlCenterConfiguration;
+
+GType foobar_control_center_configuration_get_type ( void );
+FoobarControlCenterConfiguration* foobar_control_center_configuration_new ( void );
+FoobarControlCenterConfiguration* foobar_control_center_configuration_copy ( FoobarControlCenterConfiguration const* self );
+void foobar_control_center_configuration_free ( FoobarControlCenterConfiguration* self );
+gboolean foobar_control_center_configuration_equal ( FoobarControlCenterConfiguration const* a,
+ FoobarControlCenterConfiguration const* b );
+gint foobar_control_center_configuration_get_width ( FoobarControlCenterConfiguration const* self );
+gint foobar_control_center_configuration_get_height ( FoobarControlCenterConfiguration const* self );
+FoobarScreenEdge foobar_control_center_configuration_get_position ( FoobarControlCenterConfiguration const* self );
+gint foobar_control_center_configuration_get_offset ( FoobarControlCenterConfiguration const* self );
+gint foobar_control_center_configuration_get_padding ( FoobarControlCenterConfiguration const* self );
+gint foobar_control_center_configuration_get_spacing ( FoobarControlCenterConfiguration const* self );
+FoobarOrientation foobar_control_center_configuration_get_orientation( FoobarControlCenterConfiguration const* self );
+FoobarControlCenterAlignment foobar_control_center_configuration_get_alignment ( FoobarControlCenterConfiguration const* self );
+FoobarControlCenterRow const* foobar_control_center_configuration_get_rows ( FoobarControlCenterConfiguration const* self,
+ gsize* out_count );
+void foobar_control_center_configuration_set_width ( FoobarControlCenterConfiguration* self,
+ gint value );
+void foobar_control_center_configuration_set_height ( FoobarControlCenterConfiguration* self,
+ gint value );
+void foobar_control_center_configuration_set_position ( FoobarControlCenterConfiguration* self,
+ FoobarScreenEdge value );
+void foobar_control_center_configuration_set_offset ( FoobarControlCenterConfiguration* self,
+ gint value );
+void foobar_control_center_configuration_set_padding ( FoobarControlCenterConfiguration* self,
+ gint value );
+void foobar_control_center_configuration_set_spacing ( FoobarControlCenterConfiguration* self,
+ gint value );
+void foobar_control_center_configuration_set_orientation( FoobarControlCenterConfiguration* self,
+ FoobarOrientation value );
+void foobar_control_center_configuration_set_rows ( FoobarControlCenterConfiguration* self,
+ FoobarControlCenterRow const* value,
+ gsize value_count );
+void foobar_control_center_configuration_set_alignment ( FoobarControlCenterConfiguration* self,
+ FoobarControlCenterAlignment value );
+
+typedef struct _FoobarNotificationConfiguration FoobarNotificationConfiguration;
+
+GType foobar_notification_configuration_get_type ( void );
+FoobarNotificationConfiguration* foobar_notification_configuration_new ( void );
+FoobarNotificationConfiguration* foobar_notification_configuration_copy ( FoobarNotificationConfiguration const* self );
+void foobar_notification_configuration_free ( FoobarNotificationConfiguration* self );
+gboolean foobar_notification_configuration_equal ( FoobarNotificationConfiguration const* a,
+ FoobarNotificationConfiguration const* b );
+gint foobar_notification_configuration_get_width ( FoobarNotificationConfiguration const* self );
+gint foobar_notification_configuration_get_min_height ( FoobarNotificationConfiguration const* self );
+gint foobar_notification_configuration_get_spacing ( FoobarNotificationConfiguration const* self );
+gint foobar_notification_configuration_get_close_button_inset( FoobarNotificationConfiguration const* self );
+gchar const* foobar_notification_configuration_get_time_format ( FoobarNotificationConfiguration const* self );
+void foobar_notification_configuration_set_width ( FoobarNotificationConfiguration* self,
+ gint value );
+void foobar_notification_configuration_set_min_height ( FoobarNotificationConfiguration* self,
+ gint value );
+void foobar_notification_configuration_set_spacing ( FoobarNotificationConfiguration* self,
+ gint value );
+void foobar_notification_configuration_set_close_button_inset( FoobarNotificationConfiguration* self,
+ gint value );
+void foobar_notification_configuration_set_time_format ( FoobarNotificationConfiguration* self,
+ gchar const* value );
+
+typedef struct _FoobarConfiguration FoobarConfiguration;
+
+GType foobar_configuration_get_type ( void );
+FoobarConfiguration* foobar_configuration_new ( void );
+FoobarConfiguration* foobar_configuration_copy ( FoobarConfiguration const* self );
+void foobar_configuration_free ( FoobarConfiguration* self );
+gboolean foobar_configuration_equal ( FoobarConfiguration const* a,
+ FoobarConfiguration const* b );
+FoobarGeneralConfiguration const* foobar_configuration_get_general ( FoobarConfiguration const* self );
+FoobarGeneralConfiguration* foobar_configuration_get_general_mut ( FoobarConfiguration* self );
+FoobarPanelConfiguration const* foobar_configuration_get_panel ( FoobarConfiguration const* self );
+FoobarPanelConfiguration* foobar_configuration_get_panel_mut ( FoobarConfiguration* self );
+FoobarLauncherConfiguration const* foobar_configuration_get_launcher ( FoobarConfiguration const* self );
+FoobarLauncherConfiguration* foobar_configuration_get_launcher_mut ( FoobarConfiguration* self );
+FoobarControlCenterConfiguration const* foobar_configuration_get_control_center ( FoobarConfiguration const* self );
+FoobarControlCenterConfiguration* foobar_configuration_get_control_center_mut( FoobarConfiguration* self );
+FoobarNotificationConfiguration const* foobar_configuration_get_notifications ( FoobarConfiguration const* self );
+FoobarNotificationConfiguration* foobar_configuration_get_notifications_mut ( FoobarConfiguration* self );
+
+
+G_DECLARE_FINAL_TYPE( FoobarConfigurationService, foobar_configuration_service, FOOBAR, CONFIGURATION_SERVICE, GObject )
+
+FoobarConfigurationService* foobar_configuration_service_new ( void );
+FoobarConfiguration const* foobar_configuration_service_get_current( FoobarConfigurationService* self );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/services/meson.build b/src/services/meson.build
new file mode 100644
index 0000000..ff1c693
--- /dev/null
+++ b/src/services/meson.build
@@ -0,0 +1,12 @@
+foobar_sources += files(
+ 'battery-service.c',
+ 'clock-service.c',
+ 'brightness-service.c',
+ 'workspace-service.c',
+ 'notification-service.c',
+ 'audio-service.c',
+ 'network-service.c',
+ 'bluetooth-service.c',
+ 'application-service.c',
+ 'configuration-service.c',
+) \ No newline at end of file
diff --git a/src/services/network-service.c b/src/services/network-service.c
new file mode 100644
index 0000000..bbbe857
--- /dev/null
+++ b/src/services/network-service.c
@@ -0,0 +1,1577 @@
+#include "services/network-service.h"
+#include <NetworkManager.h>
+#include <gtk/gtk.h>
+#include <stdint.h>
+
+typedef struct _ApNetworkMembership ApNetworkMembership;
+
+#define SCAN_REFRESH_INTERVAL 10000
+
+//
+// FoobarNetwork:
+//
+// Represents a Wi-Fi network, identified by its SSID. Instances contain a set of NMAccessPoints and the strength is
+// updated to match that of the best available AP.
+//
+
+struct _FoobarNetwork
+{
+ GObject parent_instance;
+ GBytes* ssid;
+ gchar* name;
+ gint strength;
+ gboolean is_active;
+ GHashTable* aps;
+};
+
+enum
+{
+ NETWORK_PROP_NAME = 1,
+ NETWORK_PROP_STRENGTH,
+ NETWORK_PROP_IS_ACTIVE,
+ N_NETWORK_PROPS,
+};
+
+static GParamSpec* network_props[N_NETWORK_PROPS] = { 0 };
+
+static void foobar_network_class_init ( FoobarNetworkClass* klass );
+static void foobar_network_init ( FoobarNetwork* self );
+static void foobar_network_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_network_finalize ( GObject* object );
+static FoobarNetwork* foobar_network_new ( void );
+static void foobar_network_set_ssid ( FoobarNetwork* self,
+ GBytes* value );
+static void foobar_network_set_active ( FoobarNetwork* self,
+ gboolean value );
+static void foobar_network_add_ap ( FoobarNetwork* self,
+ NMAccessPoint* ap );
+static void foobar_network_remove_ap ( FoobarNetwork* self,
+ NMAccessPoint* ap );
+static void foobar_network_update_strength ( FoobarNetwork* self );
+static void foobar_network_handle_strength_change( GObject* ap,
+ GParamSpec* pspec,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarNetwork, foobar_network, G_TYPE_OBJECT )
+
+//
+// FoobarNetworkAdapterState:
+//
+// The current connection/connectivity state of a network adapter.
+//
+
+G_DEFINE_ENUM_TYPE(
+ FoobarNetworkAdapterState,
+ foobar_network_adapter_state,
+ G_DEFINE_ENUM_VALUE( FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED, "disconnected" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_NETWORK_ADAPTER_STATE_CONNECTING, "connecting" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_NETWORK_ADAPTER_STATE_CONNECTED, "connected" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_NETWORK_ADAPTER_STATE_LIMITED, "limited" ) )
+
+//
+// FoobarNetworkAdapter:
+//
+// Base class for Wi-Fi/ethernet network adapters.
+//
+
+typedef struct _FoobarNetworkAdapterPrivate
+{
+ NMDevice* device;
+ NMActiveConnection* connection;
+ FoobarNetworkAdapterState state;
+ gulong connectivity_handler_id;
+ gulong connection_handler_id;
+ gulong connection_state_handler_id;
+} FoobarNetworkAdapterPrivate;
+
+enum
+{
+ ADAPTER_PROP_STATE = 1,
+ N_ADAPTER_PROPS,
+};
+
+static GParamSpec* adapter_props[N_ADAPTER_PROPS] = { 0 };
+
+static void foobar_network_adapter_class_init ( FoobarNetworkAdapterClass* klass );
+static void foobar_network_adapter_init ( FoobarNetworkAdapter* self );
+static void foobar_network_adapter_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_network_adapter_finalize ( GObject* object );
+static void foobar_network_adapter_set_device ( FoobarNetworkAdapter* self,
+ NMDevice* value );
+static void foobar_network_adapter_update_state ( FoobarNetworkAdapter* self );
+static void foobar_network_adapter_handle_state_change ( GObject* sender,
+ GParamSpec* pspec,
+ gpointer userdata );
+static void foobar_network_adapter_handle_connection_change( GObject* sender,
+ GParamSpec* pspec,
+ gpointer userdata );
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE( FoobarNetworkAdapter, foobar_network_adapter, G_TYPE_OBJECT )
+
+//
+// FoobarNetworkAdapterWired:
+//
+// Represents an ethernet network adapter.
+//
+
+struct _FoobarNetworkAdapterWired
+{
+ FoobarNetworkAdapter parent_instance;
+ NMDeviceEthernet* device;
+ gulong speed_handler_id;
+};
+
+static void foobar_network_adapter_wired_class_init ( FoobarNetworkAdapterWiredClass* klass );
+static void foobar_network_adapter_wired_init ( FoobarNetworkAdapterWired* self );
+static void foobar_network_adapter_wired_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_network_adapter_wired_finalize ( GObject* object );
+static FoobarNetworkAdapterWired* foobar_network_adapter_wired_new ( NMDeviceEthernet* device );
+static void foobar_network_adapter_wired_handle_speed_change( GObject* device,
+ GParamSpec* pspec,
+ gpointer userdata );
+
+enum
+{
+ WIRED_PROP_SPEED = 1,
+ N_WIRED_PROPS,
+};
+
+static GParamSpec* wired_props[N_WIRED_PROPS] = { 0 };
+
+G_DEFINE_FINAL_TYPE( FoobarNetworkAdapterWired, foobar_network_adapter_wired, FOOBAR_TYPE_NETWORK_ADAPTER )
+
+//
+// FoobarNetworkAdapterWifi:
+//
+// Represents a Wi-Fi network adapter.
+//
+
+struct _FoobarNetworkAdapterWifi
+{
+ FoobarNetworkAdapter parent_instance;
+ GHashTable* ap_networks;
+ GHashTable* named_networks;
+ GListStore* networks;
+ GtkFilterListModel* filtered_networks;
+ GtkSortListModel* sorted_networks;
+ FoobarNetwork* active;
+ gboolean is_scanning;
+ NMClient* client;
+ NMDeviceWifi* device;
+ gulong enabled_handler_id;
+ gulong active_handler_id;
+ gulong added_handler_id;
+ gulong removed_handler_id;
+ guint refresh_source_id;
+};
+
+enum
+{
+ WIFI_PROP_NETWORKS = 1,
+ WIFI_PROP_ACTIVE,
+ WIFI_PROP_IS_ENABLED,
+ WIFI_PROP_IS_SCANNING,
+ N_WIFI_PROPS,
+};
+
+static GParamSpec* wifi_props[N_WIFI_PROPS] = { 0 };
+
+static void foobar_network_adapter_wifi_class_init ( FoobarNetworkAdapterWifiClass* klass );
+static void foobar_network_adapter_wifi_init ( FoobarNetworkAdapterWifi* self );
+static void foobar_network_adapter_wifi_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_network_adapter_wifi_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_network_adapter_wifi_finalize ( GObject* object );
+static FoobarNetworkAdapterWifi* foobar_network_adapter_wifi_new ( NMClient* client,
+ NMDeviceWifi* device );
+static void foobar_network_adapter_wifi_add_ap_to_network ( FoobarNetworkAdapterWifi* self,
+ NMAccessPoint* ap );
+static void foobar_network_adapter_wifi_update_active ( FoobarNetworkAdapterWifi* self );
+static void foobar_network_adapter_wifi_handle_ssid_change ( GObject* ap,
+ GParamSpec* pspec,
+ gpointer userdata );
+static void foobar_network_adapter_wifi_handle_enabled_change( GObject* client,
+ GParamSpec* pspec,
+ gpointer userdata );
+static void foobar_network_adapter_wifi_handle_active_change ( GObject* device,
+ GParamSpec* pspec,
+ gpointer userdata );
+static void foobar_network_adapter_wifi_handle_added ( NMDeviceWifi* device,
+ NMAccessPoint* ap,
+ gpointer userdata );
+static void foobar_network_adapter_wifi_handle_removed ( NMDeviceWifi* device,
+ NMAccessPoint* ap,
+ gpointer userdata );
+static gboolean foobar_network_adapter_wifi_refresh_list ( gpointer userdata );
+static gboolean foobar_network_adapter_wifi_filter_func ( gpointer item,
+ gpointer userdata );
+static gint foobar_network_adapter_wifi_sort_func ( gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarNetworkAdapterWifi, foobar_network_adapter_wifi, FOOBAR_TYPE_NETWORK_ADAPTER )
+
+//
+// ApNetworkMembership:
+//
+// Structure representing an AP's membership in a network. The AP is removed when the structure is destroyed.
+//
+
+struct _ApNetworkMembership
+{
+ NMAccessPoint* ap;
+ FoobarNetwork* network;
+};
+
+static ApNetworkMembership* ap_network_membership_new ( NMAccessPoint* ap,
+ FoobarNetwork* network );
+static void ap_network_membership_destroy( ApNetworkMembership* self );
+
+//
+// FoobarNetworkService:
+//
+// Service monitoring the network state of the computer. This is implemented using the NetworkManager client library.
+//
+
+struct _FoobarNetworkService
+{
+ GObject parent_instance;
+ NMClient* client;
+ FoobarNetworkAdapterWired* wired;
+ FoobarNetworkAdapterWifi* wifi;
+ FoobarNetworkAdapter* active;
+ gulong primary_handler_id;
+ gulong activating_handler_id;
+};
+
+enum
+{
+ PROP_WIRED = 1,
+ PROP_WIFI,
+ PROP_ACTIVE,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_network_service_class_init ( FoobarNetworkServiceClass* klass );
+static void foobar_network_service_init ( FoobarNetworkService* self );
+static void foobar_network_service_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_network_service_finalize ( GObject* object );
+static void foobar_network_service_handle_connection_change( GObject* client,
+ GParamSpec* pspec,
+ gpointer userdata );
+static void foobar_network_service_update_active ( FoobarNetworkService* self );
+
+G_DEFINE_FINAL_TYPE( FoobarNetworkService, foobar_network_service, G_TYPE_OBJECT )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Wi-Fi Network
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for networks.
+//
+void foobar_network_class_init( FoobarNetworkClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_network_get_property;
+ object_klass->finalize = foobar_network_finalize;
+
+ network_props[NETWORK_PROP_NAME] = g_param_spec_string(
+ "name",
+ "Name",
+ "The advertised SSID of the network.",
+ NULL,
+ G_PARAM_READABLE );
+ network_props[NETWORK_PROP_STRENGTH] = g_param_spec_int(
+ "strength",
+ "Strength",
+ "The strength of the network in percent.",
+ 0,
+ 100,
+ 0,
+ G_PARAM_READABLE );
+ network_props[NETWORK_PROP_IS_ACTIVE] = g_param_spec_boolean(
+ "is-active",
+ "Is Active",
+ "Indicates whether the network is currently used by the client.",
+ FALSE,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_NETWORK_PROPS, network_props );
+}
+
+//
+// Instance initialization for networks.
+//
+void foobar_network_init( FoobarNetwork* self )
+{
+ self->aps = g_hash_table_new_full( g_direct_hash, g_direct_equal, NULL, NULL );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_network_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarNetwork* self = (FoobarNetwork*)object;
+
+ switch ( prop_id )
+ {
+ case NETWORK_PROP_NAME:
+ g_value_set_string( value, foobar_network_get_name( self ) );
+ break;
+ case NETWORK_PROP_STRENGTH:
+ g_value_set_int( value, foobar_network_get_strength( self ) );
+ break;
+ case NETWORK_PROP_IS_ACTIVE:
+ g_value_set_boolean( value, foobar_network_is_active( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for networks.
+//
+void foobar_network_finalize( GObject* object )
+{
+ FoobarNetwork* self = (FoobarNetwork*)object;
+
+ GHashTableIter iter;
+ gpointer ap;
+ g_hash_table_iter_init( &iter, self->aps );
+ while ( g_hash_table_iter_next( &iter, &ap, NULL ) )
+ {
+ g_signal_handlers_disconnect_by_data( ap, self );
+ }
+
+ g_clear_pointer( &self->ssid, g_bytes_unref );
+ g_clear_pointer( &self->name, g_free );
+ g_clear_pointer( &self->aps, g_hash_table_unref );
+
+ G_OBJECT_CLASS( foobar_network_parent_class )->finalize( object );
+}
+
+//
+// Create a new network instance.
+//
+FoobarNetwork* foobar_network_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_NETWORK, NULL );
+}
+
+//
+// Get the advertized SSID of the network.
+//
+gchar const* foobar_network_get_name( FoobarNetwork* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NETWORK( self ), NULL );
+ return self->name;
+}
+
+//
+// Get the current strength percentage value for the network.
+//
+gint foobar_network_get_strength( FoobarNetwork* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NETWORK( self ), 0 );
+ return self->strength;
+}
+
+//
+// Check whether the network is currently used by the client.
+//
+gboolean foobar_network_is_active( FoobarNetwork* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NETWORK( self ), FALSE );
+ return self->is_active;
+}
+
+//
+// Update the advertized SSID of the network.
+//
+// This also updates the string representation (which may be a lossy conversion).
+//
+void foobar_network_set_ssid(
+ FoobarNetwork* self,
+ GBytes* value )
+{
+ g_return_if_fail( FOOBAR_IS_NETWORK( self ) );
+
+ if ( self->ssid != value )
+ {
+ g_clear_pointer( &self->ssid, g_bytes_unref );
+ g_clear_pointer( &self->name, g_free );
+
+ if ( value )
+ {
+ self->ssid = g_bytes_ref( value );
+ gsize ssid_len;
+ guint8 const* ssid_data = g_bytes_get_data( self->ssid, &ssid_len );
+ self->name = nm_utils_ssid_to_utf8( ssid_data, ssid_len );
+ }
+
+ g_object_notify_by_pspec( G_OBJECT( self ), network_props[NETWORK_PROP_NAME] );
+ }
+}
+
+//
+// Update the "active" flag of the network. This does not actually activate the network.
+//
+void foobar_network_set_active(
+ FoobarNetwork* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_NETWORK( self ) );
+
+ value = !!value;
+ if ( self->is_active != value )
+ {
+ self->is_active = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), network_props[NETWORK_PROP_NAME] );
+ }
+}
+
+//
+// Include the specified AP when computing the network's strength.
+//
+void foobar_network_add_ap(
+ FoobarNetwork* self,
+ NMAccessPoint* ap )
+{
+ if ( g_hash_table_add( self->aps, g_object_ref( ap ) ) )
+ {
+ g_signal_connect( ap, "notify::strength", G_CALLBACK( foobar_network_handle_strength_change ), self );
+ foobar_network_update_strength( self );
+ }
+}
+
+//
+// Stop including the specified AP when computing the network's strength.
+//
+void foobar_network_remove_ap(
+ FoobarNetwork* self,
+ NMAccessPoint* ap )
+{
+ if ( g_hash_table_remove( self->aps, ap ) )
+ {
+ g_signal_handlers_disconnect_by_data( ap, self );
+ foobar_network_update_strength( self );
+ }
+}
+
+//
+// Update the network's "strength" property to match that of the best AP for it.
+//
+void foobar_network_update_strength( FoobarNetwork* self )
+{
+ gint strength = 0;
+ GHashTableIter iter;
+ gpointer ap;
+ g_hash_table_iter_init( &iter, self->aps );
+ while ( g_hash_table_iter_next( &iter, &ap, NULL ) )
+ {
+ gint ap_strength = nm_access_point_get_strength( ap );
+ strength = MAX( strength, ap_strength );
+ }
+
+ strength = CLAMP( strength, 0, 100 );
+ if ( self->strength != strength )
+ {
+ self->strength = strength;
+ g_object_notify_by_pspec( G_OBJECT( self ), network_props[NETWORK_PROP_STRENGTH] );
+ }
+}
+
+//
+// Called when the strength of an access point for this network has changed.
+//
+void foobar_network_handle_strength_change(
+ GObject* ap,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)ap;
+ (void)pspec;
+ FoobarNetwork* self = (FoobarNetwork*)userdata;
+
+ foobar_network_update_strength( self );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Network Adapter
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for network adapters.
+//
+void foobar_network_adapter_class_init( FoobarNetworkAdapterClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_network_adapter_get_property;
+ object_klass->finalize = foobar_network_adapter_finalize;
+
+ adapter_props[ADAPTER_PROP_STATE] = g_param_spec_enum(
+ "state",
+ "State",
+ "The current connection state of the network.",
+ FOOBAR_TYPE_NETWORK_ADAPTER_STATE,
+ FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_ADAPTER_PROPS, adapter_props );
+}
+
+//
+// Instance initialization for network adapters.
+//
+void foobar_network_adapter_init( FoobarNetworkAdapter* self )
+{
+ (void)self;
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_network_adapter_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarNetworkAdapter* self = (FoobarNetworkAdapter*)object;
+
+ switch ( prop_id )
+ {
+ case ADAPTER_PROP_STATE:
+ g_value_set_enum( value, foobar_network_adapter_get_state( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for network adapters.
+//
+void foobar_network_adapter_finalize( GObject* object )
+{
+ FoobarNetworkAdapter* self = (FoobarNetworkAdapter*)object;
+
+ foobar_network_adapter_set_device( self, NULL );
+
+ G_OBJECT_CLASS( foobar_network_adapter_parent_class )->finalize( object );
+}
+
+//
+// Get the current connection/connectivity state of the adapter.
+//
+FoobarNetworkAdapterState foobar_network_adapter_get_state( FoobarNetworkAdapter* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER( self ), FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED );
+
+ FoobarNetworkAdapterPrivate* priv = foobar_network_adapter_get_instance_private( self );
+ return priv->state;
+}
+
+//
+// Update the backing network device which is used to derive the adapter's state.
+//
+void foobar_network_adapter_set_device(
+ FoobarNetworkAdapter* self,
+ NMDevice* value )
+{
+ FoobarNetworkAdapterPrivate* priv = foobar_network_adapter_get_instance_private( self );
+ if ( priv->device != value )
+ {
+ g_clear_signal_handler( &priv->connection_state_handler_id, priv->connection );
+ g_clear_signal_handler( &priv->connectivity_handler_id, priv->device );
+ g_clear_signal_handler( &priv->connection_handler_id, priv->device );
+ g_clear_object( &priv->connection );
+ g_clear_object( &priv->device );
+
+ priv->device = value;
+ if ( priv->device )
+ {
+ g_object_ref( priv->device );
+ priv->connectivity_handler_id = g_signal_connect(
+ priv->device,
+ "notify::ip4-connectivity",
+ G_CALLBACK( foobar_network_adapter_handle_state_change ), self );
+ priv->connection_handler_id = g_signal_connect(
+ priv->device,
+ "notify::active-connection",
+ G_CALLBACK( foobar_network_adapter_handle_connection_change ), self );
+ priv->connection = nm_device_get_active_connection( priv->device );
+ if ( priv->connection )
+ {
+ g_object_ref( priv->connection );
+ priv->connection_state_handler_id = g_signal_connect(
+ priv->connection,
+ "notify::state",
+ G_CALLBACK( foobar_network_adapter_handle_state_change ),
+ self );
+ }
+ }
+
+ foobar_network_adapter_update_state( self );
+ }
+}
+
+//
+// Derive the connection/connectivity state for the network from the backing device.
+//
+void foobar_network_adapter_update_state( FoobarNetworkAdapter* self )
+{
+ FoobarNetworkAdapterPrivate* priv = foobar_network_adapter_get_instance_private( self );
+
+ FoobarNetworkAdapterState state = FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED;
+ if ( priv->device )
+ {
+ NMActiveConnection* connection = nm_device_get_active_connection( priv->device );
+ if ( connection )
+ {
+ switch ( nm_active_connection_get_state( connection ) )
+ {
+ case NM_ACTIVE_CONNECTION_STATE_ACTIVATED:
+ state = ( nm_device_get_connectivity( priv->device, AF_INET ) == NM_CONNECTIVITY_FULL )
+ ? FOOBAR_NETWORK_ADAPTER_STATE_CONNECTED
+ : FOOBAR_NETWORK_ADAPTER_STATE_LIMITED;
+ break;
+ case NM_ACTIVE_CONNECTION_STATE_ACTIVATING:
+ state = FOOBAR_NETWORK_ADAPTER_STATE_CONNECTING;
+ break;
+ case NM_ACTIVE_CONNECTION_STATE_DEACTIVATING:
+ case NM_ACTIVE_CONNECTION_STATE_DEACTIVATED:
+ case NM_ACTIVE_CONNECTION_STATE_UNKNOWN:
+ default:
+ state = FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED;
+ break;
+ }
+ }
+ }
+
+ if ( priv->state != state )
+ {
+ priv->state = state;
+ g_object_notify_by_pspec( G_OBJECT( self ), adapter_props[ADAPTER_PROP_STATE] );
+ }
+}
+
+//
+// Called when the "ip4-connectivity" property of the NetworkManager device or the "state" property of the active
+// connection has changed.
+//
+static void foobar_network_adapter_handle_state_change(
+ GObject* sender,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)sender;
+ (void)pspec;
+ FoobarNetworkAdapter* self = (FoobarNetworkAdapter*)userdata;
+
+ foobar_network_adapter_update_state( self );
+}
+
+//
+// Called when the "active-connection" property of the NetworkManager device has changed.
+//
+// This updates the state of the adapter and sets the connection up for state change listening.
+//
+static void foobar_network_adapter_handle_connection_change(
+ GObject* sender,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)sender;
+ (void)pspec;
+ FoobarNetworkAdapter* self = (FoobarNetworkAdapter*)userdata;
+ FoobarNetworkAdapterPrivate* priv = foobar_network_adapter_get_instance_private( self );
+
+ g_clear_signal_handler( &priv->connection_state_handler_id, priv->connection );
+ g_clear_object( &priv->connection );
+
+ priv->connection = nm_device_get_active_connection( priv->device );
+ if ( priv->connection )
+ {
+ g_object_ref( priv->connection );
+ priv->connection_state_handler_id = g_signal_connect(
+ priv->connection,
+ "notify::state",
+ G_CALLBACK( foobar_network_adapter_handle_state_change ),
+ self );
+ }
+
+ foobar_network_adapter_update_state( self );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Wired Network Adapter
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for wired network adapters.
+//
+void foobar_network_adapter_wired_class_init( FoobarNetworkAdapterWiredClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_network_adapter_wired_get_property;
+ object_klass->finalize = foobar_network_adapter_wired_finalize;
+
+ wired_props[WIRED_PROP_SPEED] = g_param_spec_int64(
+ "speed",
+ "Speed",
+ "The supported speed of the adapter in Mbit/s.",
+ 0,
+ INT64_MAX,
+ 0,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_WIRED_PROPS, wired_props );
+}
+
+//
+// Instance initialization for wired network adapters.
+//
+void foobar_network_adapter_wired_init( FoobarNetworkAdapterWired* self )
+{
+ (void)self;
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_network_adapter_wired_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarNetworkAdapterWired* self = (FoobarNetworkAdapterWired*)object;
+
+ switch ( prop_id )
+ {
+ case WIRED_PROP_SPEED:
+ g_value_set_int64( value, foobar_network_adapter_wired_get_speed( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for wired network adapters.
+//
+void foobar_network_adapter_wired_finalize( GObject* object )
+{
+ FoobarNetworkAdapterWired* self = (FoobarNetworkAdapterWired*)object;
+
+ g_clear_signal_handler( &self->speed_handler_id, self->device );
+ g_clear_object( &self->device );
+
+ G_OBJECT_CLASS( foobar_network_adapter_wired_parent_class )->finalize( object );
+}
+
+//
+// Create a new wired network adapter wrapping the provided NetworkManager device.
+//
+FoobarNetworkAdapterWired* foobar_network_adapter_wired_new( NMDeviceEthernet* device )
+{
+ FoobarNetworkAdapterWired* self = g_object_new( FOOBAR_TYPE_NETWORK_ADAPTER_WIRED, NULL );
+ foobar_network_adapter_set_device( FOOBAR_NETWORK_ADAPTER( self ), NM_DEVICE( device ) );
+
+ self->device = g_object_ref( device );
+ self->speed_handler_id = g_signal_connect(
+ self->device,
+ "notify::speed",
+ G_CALLBACK( foobar_network_adapter_wired_handle_speed_change ),
+ self );
+ return self;
+}
+
+//
+// Get the supported speed of the adapter in Mbit/s.
+//
+gint64 foobar_network_adapter_wired_get_speed( FoobarNetworkAdapterWired* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIRED( self ), 0 );
+ return nm_device_ethernet_get_speed( self->device );
+}
+
+//
+// Called when the "speed" property of the NetworkManager device changes.
+//
+void foobar_network_adapter_wired_handle_speed_change(
+ GObject* device,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)device;
+ (void)pspec;
+ FoobarNetworkAdapterWired* self = (FoobarNetworkAdapterWired*)userdata;
+
+ g_object_notify_by_pspec( G_OBJECT( self ), wired_props[WIRED_PROP_SPEED] );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Wireless Network Adapter
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for wireless network adapters.
+//
+void foobar_network_adapter_wifi_class_init( FoobarNetworkAdapterWifiClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_network_adapter_wifi_get_property;
+ object_klass->set_property = foobar_network_adapter_wifi_set_property;
+ object_klass->finalize = foobar_network_adapter_wifi_finalize;
+
+ wifi_props[WIFI_PROP_NETWORKS] = g_param_spec_object(
+ "networks",
+ "Networks",
+ "Sorted list of available networks.",
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READABLE );
+ wifi_props[WIFI_PROP_ACTIVE] = g_param_spec_object(
+ "active",
+ "Active",
+ "The currently active network (if any).",
+ FOOBAR_TYPE_NETWORK,
+ G_PARAM_READABLE );
+ wifi_props[WIFI_PROP_IS_ENABLED] = g_param_spec_boolean(
+ "is-enabled",
+ "Is Enabled",
+ "Indicates whether the wifi adapter is currently enabled.",
+ FALSE,
+ G_PARAM_READWRITE );
+ wifi_props[WIFI_PROP_IS_SCANNING] = g_param_spec_boolean(
+ "is-scanning",
+ "Is Scanning",
+ "Indicates whether the wifi adapter is currently scanning for available networks.",
+ FALSE,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_WIFI_PROPS, wifi_props );
+}
+
+//
+// Instance initialization for wireless network adapters.
+//
+void foobar_network_adapter_wifi_init( FoobarNetworkAdapterWifi* self )
+{
+ self->ap_networks = g_hash_table_new_full(
+ g_direct_hash,
+ g_direct_equal,
+ g_object_unref,
+ (GDestroyNotify)ap_network_membership_destroy );
+ self->named_networks = g_hash_table_new_full(
+ g_bytes_hash,
+ g_bytes_equal,
+ (GDestroyNotify)g_bytes_unref,
+ g_object_unref );
+ self->networks = g_list_store_new( FOOBAR_TYPE_NETWORK );
+
+ GtkCustomFilter* filter = gtk_custom_filter_new( foobar_network_adapter_wifi_filter_func, NULL, NULL );
+ self->filtered_networks = gtk_filter_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->networks ) ),
+ GTK_FILTER( filter ) );
+
+ GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_network_adapter_wifi_sort_func, NULL, NULL );
+ self->sorted_networks = gtk_sort_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->filtered_networks ) ),
+ GTK_SORTER( sorter ) );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_network_adapter_wifi_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)object;
+
+ switch ( prop_id )
+ {
+ case WIFI_PROP_NETWORKS:
+ g_value_set_object( value, foobar_network_adapter_wifi_get_networks( self ) );
+ break;
+ case WIFI_PROP_ACTIVE:
+ g_value_set_object( value, foobar_network_adapter_wifi_get_active( self ) );
+ break;
+ case WIFI_PROP_IS_ENABLED:
+ g_value_set_boolean( value, foobar_network_adapter_wifi_is_enabled( self ) );
+ break;
+ case WIFI_PROP_IS_SCANNING:
+ g_value_set_boolean( value, foobar_network_adapter_wifi_is_scanning( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_network_adapter_wifi_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)object;
+
+ switch ( prop_id )
+ {
+ case WIFI_PROP_IS_ENABLED:
+ foobar_network_adapter_wifi_set_enabled( self, g_value_get_boolean( value ) );
+ break;
+ case WIFI_PROP_IS_SCANNING:
+ foobar_network_adapter_wifi_set_scanning( self, g_value_get_boolean( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for wireless network adapters.
+//
+void foobar_network_adapter_wifi_finalize( GObject* object )
+{
+ FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)object;
+
+ GPtrArray const* aps = nm_device_wifi_get_access_points( self->device );
+ for ( guint i = 0; i < aps->len; ++i )
+ {
+ NMAccessPoint* ap = g_ptr_array_index( aps, i );
+ g_signal_handlers_disconnect_by_data( ap, self );
+ }
+
+ g_clear_handle_id( &self->refresh_source_id, g_source_remove );
+ g_clear_signal_handler( &self->enabled_handler_id, self->client );
+ g_clear_signal_handler( &self->active_handler_id, self->device );
+ g_clear_signal_handler( &self->added_handler_id, self->device );
+ g_clear_signal_handler( &self->removed_handler_id, self->device );
+ g_clear_object( &self->client );
+ g_clear_object( &self->device );
+ g_clear_object( &self->sorted_networks );
+ g_clear_object( &self->filtered_networks );
+ g_clear_object( &self->networks );
+ g_clear_object( &self->active );
+ g_clear_pointer( &self->named_networks, g_hash_table_unref );
+ g_clear_pointer( &self->ap_networks, g_hash_table_unref );
+
+ G_OBJECT_CLASS( foobar_network_adapter_wifi_parent_class )->finalize( object );
+}
+
+//
+// Create a new wireless network adapter wrapping the provided NetworkManager device.
+//
+FoobarNetworkAdapterWifi* foobar_network_adapter_wifi_new(
+ NMClient* client,
+ NMDeviceWifi* device )
+{
+ FoobarNetworkAdapterWifi* self = g_object_new( FOOBAR_TYPE_NETWORK_ADAPTER_WIFI, NULL );
+ foobar_network_adapter_set_device( FOOBAR_NETWORK_ADAPTER( self ), NM_DEVICE( device ) );
+
+ self->client = g_object_ref( client );
+ self->device = g_object_ref( device );
+ self->enabled_handler_id = g_signal_connect(
+ self->client,
+ "notify::wireless-enabled",
+ G_CALLBACK( foobar_network_adapter_wifi_handle_enabled_change ),
+ self );
+ self->active_handler_id = g_signal_connect(
+ self->device,
+ "notify::active-access-point",
+ G_CALLBACK( foobar_network_adapter_wifi_handle_active_change ),
+ self );
+ self->added_handler_id = g_signal_connect(
+ self->device,
+ "access-point-added",
+ G_CALLBACK( foobar_network_adapter_wifi_handle_added ),
+ self );
+ self->removed_handler_id = g_signal_connect(
+ self->device,
+ "access-point-removed",
+ G_CALLBACK( foobar_network_adapter_wifi_handle_removed ),
+ self );
+
+ GPtrArray const* aps = nm_device_wifi_get_access_points( self->device );
+ for ( guint i = 0; i < aps->len; ++i )
+ {
+ NMAccessPoint* ap = g_ptr_array_index( aps, i );
+ g_signal_connect(
+ ap,
+ "notify::ssid",
+ G_CALLBACK( foobar_network_adapter_wifi_handle_ssid_change ),
+ self );
+ foobar_network_adapter_wifi_add_ap_to_network( self, ap );
+ }
+
+ foobar_network_adapter_wifi_update_active( self );
+ return self;
+}
+
+//
+// Get a sorted list of available networks.
+//
+GListModel* foobar_network_adapter_wifi_get_networks( FoobarNetworkAdapterWifi* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ), NULL );
+ return G_LIST_MODEL( self->sorted_networks );
+}
+
+//
+// Get the currently active network (if any).
+//
+FoobarNetwork* foobar_network_adapter_wifi_get_active( FoobarNetworkAdapterWifi* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ), NULL );
+ return self->active;
+}
+
+//
+// Check whether the wifi adapter is currently enabled.
+//
+gboolean foobar_network_adapter_wifi_is_enabled( FoobarNetworkAdapterWifi* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ), FALSE );
+ return nm_client_wireless_get_enabled( self->client );
+}
+
+//
+// Check whether the wifi adapter is currently scanning for available networks.
+//
+gboolean foobar_network_adapter_wifi_is_scanning( FoobarNetworkAdapterWifi* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ), FALSE );
+ return self->is_scanning;
+}
+
+//
+// Update whether the wifi adapter is currently enabled.
+//
+void foobar_network_adapter_wifi_set_enabled(
+ FoobarNetworkAdapterWifi* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ) );
+
+ if ( !value ) { foobar_network_adapter_wifi_set_scanning( self, FALSE ); }
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+ nm_client_wireless_set_enabled( self->client, value );
+#pragma GCC diagnostic pop
+}
+
+//
+// Update whether the wifi adapter is currently scanning for available networks.
+//
+void foobar_network_adapter_wifi_set_scanning(
+ FoobarNetworkAdapterWifi* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ) );
+
+ value = value && foobar_network_adapter_wifi_is_enabled( self );
+ if ( self->is_scanning != value )
+ {
+ self->is_scanning = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), wifi_props[WIFI_PROP_IS_SCANNING] );
+
+ // Periodically start scans until "is-scanning" is disabled again.
+
+ g_clear_handle_id( &self->refresh_source_id, g_source_remove );
+ if ( self->is_scanning )
+ {
+ foobar_network_adapter_wifi_refresh_list( self );
+ self->refresh_source_id = g_timeout_add(
+ SCAN_REFRESH_INTERVAL,
+ foobar_network_adapter_wifi_refresh_list,
+ self );
+ }
+ }
+}
+
+//
+// Ensure that a network for the specified access point exists.
+//
+// If the AP has an SSID, it is added to the network for this SSID. Otherwise, a new network for this AP is created just
+// for this network.
+//
+void foobar_network_adapter_wifi_add_ap_to_network(
+ FoobarNetworkAdapterWifi* self,
+ NMAccessPoint* ap )
+{
+ // The AP is automatically added/removed from the network when using g_hash_table_replace because of the
+ // ApNetworkMembership destroy function.
+
+ GBytes* ssid = nm_access_point_get_ssid( ap );
+ if ( ssid )
+ {
+ FoobarNetwork* network = g_hash_table_lookup( self->named_networks, ssid );
+ if ( !network )
+ {
+ network = foobar_network_new( );
+ foobar_network_set_ssid( network, ssid );
+ g_list_store_append( self->networks, network );
+ g_hash_table_insert( self->named_networks, g_bytes_ref( ssid ), network );
+ }
+
+ g_hash_table_replace( self->ap_networks, g_object_ref( ap ), ap_network_membership_new( ap, network ) );
+ }
+ else
+ {
+ g_autoptr( FoobarNetwork ) network = foobar_network_new( );
+ g_hash_table_replace( self->ap_networks, g_object_ref( ap ), ap_network_membership_new( ap, network ) );
+ }
+
+ gtk_filter_changed( gtk_filter_list_model_get_filter( self->filtered_networks ), GTK_FILTER_CHANGE_DIFFERENT );
+}
+
+//
+// Update the active network based on the active access point.
+//
+void foobar_network_adapter_wifi_update_active( FoobarNetworkAdapterWifi* self )
+{
+ NMAccessPoint* active_ap = nm_device_wifi_get_active_access_point( self->device );
+ ApNetworkMembership* membership = active_ap ? g_hash_table_lookup( self->ap_networks, active_ap ) : NULL;
+ FoobarNetwork* new_active = membership ? membership->network : NULL;
+
+ if ( self->active != new_active )
+ {
+ if ( self->active )
+ {
+ foobar_network_set_active( self->active, FALSE );
+ }
+ g_clear_object( &self->active );
+
+ self->active = new_active;
+ if ( self->active )
+ {
+ g_object_ref( self->active );
+ foobar_network_set_active( self->active, TRUE );
+ }
+
+ g_object_notify_by_pspec( G_OBJECT( self ), wifi_props[WIFI_PROP_ACTIVE] );
+ }
+}
+
+//
+// Called when the SSID for an access point changes.
+//
+// This will remove the access point from the old network and associate it with its new network.
+//
+void foobar_network_adapter_wifi_handle_ssid_change(
+ GObject* ap,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)pspec;
+ FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata;
+
+ foobar_network_adapter_wifi_add_ap_to_network( self, NM_ACCESS_POINT( ap ) );
+ foobar_network_adapter_wifi_update_active( self );
+}
+
+//
+// Called when the "wireless-enabled" state of the client changes.
+//
+void foobar_network_adapter_wifi_handle_enabled_change(
+ GObject* client,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)client;
+ (void)pspec;
+ FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata;
+
+ g_object_notify_by_pspec( G_OBJECT( self ), wifi_props[WIFI_PROP_IS_ENABLED] );
+}
+
+//
+// Called when the active access point of the backing device changes.
+//
+void foobar_network_adapter_wifi_handle_active_change(
+ GObject* device,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)device;
+ (void)pspec;
+ FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata;
+
+ foobar_network_adapter_wifi_update_active( self );
+}
+
+//
+// Called when a new access point is added.
+//
+// This will associate the access point with its network.
+//
+void foobar_network_adapter_wifi_handle_added(
+ NMDeviceWifi* device,
+ NMAccessPoint* ap,
+ gpointer userdata )
+{
+ (void)device;
+ FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata;
+
+ g_signal_connect(
+ ap,
+ "notify::ssid",
+ G_CALLBACK( foobar_network_adapter_wifi_handle_ssid_change ),
+ self );
+ foobar_network_adapter_wifi_add_ap_to_network( self, ap );
+}
+
+//
+// Called when an access point is removed.
+//
+// This will remove it from its network.
+//
+void foobar_network_adapter_wifi_handle_removed(
+ NMDeviceWifi* device,
+ NMAccessPoint* ap,
+ gpointer userdata )
+{
+ (void)device;
+ FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata;
+
+ g_signal_handlers_disconnect_by_data( ap, self );
+ g_hash_table_remove( self->ap_networks, ap ); // Will remove the AP from the network.
+ gtk_filter_changed( gtk_filter_list_model_get_filter( self->filtered_networks ), GTK_FILTER_CHANGE_MORE_STRICT );
+}
+
+//
+// Periodic callback to start a network scan.
+//
+gboolean foobar_network_adapter_wifi_refresh_list( gpointer userdata )
+{
+ FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata;
+
+ g_autoptr( GError ) error = NULL;
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+ if ( !nm_device_wifi_request_scan( self->device, NULL, &error ) )
+#pragma GCC diagnostic pop
+ {
+ g_warning( "Unable to start Wi-Fi network scan: %s", error->message );
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+//
+// Filtering callback for the list of networks. Only networks with at least one AP are shown.
+//
+gboolean foobar_network_adapter_wifi_filter_func(
+ gpointer item,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarNetwork* network = (FoobarNetwork*)item;
+ return g_hash_table_size( network->aps ) > 0;
+}
+
+//
+// Sorting callback for the list of networks.
+//
+gint foobar_network_adapter_wifi_sort_func(
+ gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarNetwork* network_a = (FoobarNetwork*)item_a;
+ FoobarNetwork* network_b = (FoobarNetwork*)item_b;
+ return g_strcmp0( foobar_network_get_name( network_a ), foobar_network_get_name( network_b ) );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Access Point Network Memberships
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create an association between an access point and the network it belongs to.
+//
+ApNetworkMembership* ap_network_membership_new(
+ NMAccessPoint* ap,
+ FoobarNetwork* network )
+{
+ ApNetworkMembership* self = g_new0( ApNetworkMembership, 1 );
+ self->ap = ap;
+ self->network = g_object_ref( network );
+ foobar_network_add_ap( self->network, self->ap );
+ return self;
+}
+
+//
+// Destroy an association between an access point and its previous network.
+//
+void ap_network_membership_destroy( ApNetworkMembership* self )
+{
+ if ( self )
+ {
+ foobar_network_remove_ap( self->network, self->ap );
+ g_object_unref( self->network );
+ g_free( self );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Service Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the network service.
+//
+void foobar_network_service_class_init( FoobarNetworkServiceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_network_service_get_property;
+ object_klass->finalize = foobar_network_service_finalize;
+
+ props[PROP_WIRED] = g_param_spec_object(
+ "wired",
+ "Wired",
+ "The wired network adapter (if available).",
+ FOOBAR_TYPE_NETWORK_ADAPTER_WIRED,
+ G_PARAM_READABLE );
+ props[PROP_WIFI] = g_param_spec_object(
+ "wifi",
+ "WiFi",
+ "The wireless network adapter (if available).",
+ FOOBAR_TYPE_NETWORK_ADAPTER_WIFI,
+ G_PARAM_READABLE );
+ props[PROP_ACTIVE] = g_param_spec_object(
+ "active",
+ "Active",
+ "The network adapter currently in use.",
+ FOOBAR_TYPE_NETWORK_ADAPTER,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for the network service.
+//
+void foobar_network_service_init( FoobarNetworkService* self )
+{
+ g_autoptr( GError ) error = NULL;
+ self->client = nm_client_new( NULL, &error );
+ if ( !self->client )
+ {
+ g_warning( "Unable to connect to NetworkManager: %s", error->message );
+ return;
+ }
+
+ NMDeviceEthernet* device_wired = NULL;
+ NMDeviceWifi* device_wifi = NULL;
+ GPtrArray const* devices = nm_client_get_devices( self->client );
+ for ( guint i = 0; i < devices->len; ++i )
+ {
+ // XXX: Add support for multiple devices
+ NMDevice* device = g_ptr_array_index( devices, i );
+ if ( !device_wired && NM_IS_DEVICE_ETHERNET( device ) )
+ {
+ device_wired = NM_DEVICE_ETHERNET( device );
+ }
+ if ( !device_wifi && NM_IS_DEVICE_WIFI( device ) )
+ {
+ device_wifi = NM_DEVICE_WIFI( device );
+ }
+ if ( device_wired && device_wifi )
+ {
+ break;
+ }
+ }
+
+ self->wired = device_wired ? foobar_network_adapter_wired_new( device_wired ) : NULL;
+ self->wifi = device_wifi ? foobar_network_adapter_wifi_new( self->client, device_wifi ) : NULL;
+
+ foobar_network_service_update_active( self );
+ self->primary_handler_id = g_signal_connect(
+ self->client,
+ "notify::primary-connection",
+ G_CALLBACK( foobar_network_service_handle_connection_change ),
+ self );
+ self->activating_handler_id = g_signal_connect(
+ self->client,
+ "notify::activating-connection",
+ G_CALLBACK( foobar_network_service_handle_connection_change ),
+ self );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_network_service_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarNetworkService* self = (FoobarNetworkService*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_WIRED:
+ g_value_set_object( value, foobar_network_service_get_wired( self ) );
+ break;
+ case PROP_WIFI:
+ g_value_set_object( value, foobar_network_service_get_wifi( self ) );
+ break;
+ case PROP_ACTIVE:
+ g_value_set_object( value, foobar_network_service_get_active( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for the network service.
+//
+void foobar_network_service_finalize( GObject* object )
+{
+ FoobarNetworkService* self = (FoobarNetworkService*)object;
+
+ g_clear_signal_handler( &self->primary_handler_id, self->client );
+ g_clear_signal_handler( &self->activating_handler_id, self->client );
+ g_clear_object( &self->wifi );
+ g_clear_object( &self->wired );
+ g_clear_object( &self->client );
+
+ G_OBJECT_CLASS( foobar_network_service_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new network service instance.
+//
+FoobarNetworkService* foobar_network_service_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_NETWORK_SERVICE, NULL );
+}
+
+//
+// Get the wired network adapter (if available).
+//
+FoobarNetworkAdapterWired* foobar_network_service_get_wired( FoobarNetworkService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NETWORK_SERVICE( self ), NULL );
+ return self->wired;
+}
+
+//
+// Get the wireless network adapter (if available).
+//
+FoobarNetworkAdapterWifi* foobar_network_service_get_wifi( FoobarNetworkService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NETWORK_SERVICE( self ), NULL );
+ return self->wifi;
+}
+
+//
+// Get the network adapter currently in use.
+//
+FoobarNetworkAdapter* foobar_network_service_get_active( FoobarNetworkService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NETWORK_SERVICE( self ), NULL );
+ return self->active;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called when either the primary connection or the activating connection of the client has changed.
+//
+void foobar_network_service_handle_connection_change(
+ GObject* client,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)client;
+ (void)pspec;
+ FoobarNetworkService* self = (FoobarNetworkService*)userdata;
+
+ foobar_network_service_update_active( self );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Update the active network adapter.
+//
+// This uses the primary connection of the client to determine whether the wireless or wired adapter is used. Otherwise,
+// the activating connection is used instead.
+//
+void foobar_network_service_update_active( FoobarNetworkService* self )
+{
+ NMActiveConnection* connection = nm_client_get_primary_connection( self->client );
+ connection = connection ? connection : nm_client_get_activating_connection( self->client );
+ gchar const* type = connection ? nm_active_connection_get_connection_type( connection ) : NULL;
+
+ FoobarNetworkAdapter* new_active;
+ if ( !g_strcmp0( type, "802-11-wireless" ) )
+ {
+ new_active = FOOBAR_NETWORK_ADAPTER( self->wifi );
+ }
+ else if ( !g_strcmp0( type, "802-3-ethernet" ) )
+ {
+ new_active = FOOBAR_NETWORK_ADAPTER( self->wired );
+ }
+ else
+ {
+ new_active = self->wifi ? FOOBAR_NETWORK_ADAPTER( self->wifi ) : FOOBAR_NETWORK_ADAPTER( self->wired );
+ }
+
+ if ( self->active != new_active )
+ {
+ self->active = new_active;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ACTIVE] );
+ }
+} \ No newline at end of file
diff --git a/src/services/network-service.h b/src/services/network-service.h
new file mode 100644
index 0000000..cab0c48
--- /dev/null
+++ b/src/services/network-service.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_NETWORK foobar_network_get_type( )
+#define FOOBAR_TYPE_NETWORK_ADAPTER_STATE foobar_network_adapter_state_get_type( )
+#define FOOBAR_TYPE_NETWORK_ADAPTER foobar_network_adapter_get_type( )
+#define FOOBAR_TYPE_NETWORK_ADAPTER_WIRED foobar_network_adapter_wired_get_type( )
+#define FOOBAR_TYPE_NETWORK_ADAPTER_WIFI foobar_network_adapter_wifi_get_type( )
+#define FOOBAR_TYPE_NETWORK_SERVICE foobar_network_service_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarNetwork, foobar_network, FOOBAR, NETWORK, GObject )
+
+gchar const* foobar_network_get_name ( FoobarNetwork* self );
+gint foobar_network_get_strength( FoobarNetwork* self );
+gboolean foobar_network_is_active ( FoobarNetwork* self );
+
+typedef enum
+{
+ FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED,
+ FOOBAR_NETWORK_ADAPTER_STATE_CONNECTING,
+ FOOBAR_NETWORK_ADAPTER_STATE_CONNECTED,
+ FOOBAR_NETWORK_ADAPTER_STATE_LIMITED,
+} FoobarNetworkAdapterState;
+
+GType foobar_network_adapter_state_get_type( void );
+
+G_DECLARE_DERIVABLE_TYPE( FoobarNetworkAdapter, foobar_network_adapter, FOOBAR, NETWORK_ADAPTER, GObject )
+
+struct _FoobarNetworkAdapterClass
+{
+ GObjectClass parent_class;
+};
+
+FoobarNetworkAdapterState foobar_network_adapter_get_state( FoobarNetworkAdapter* self );
+
+G_DECLARE_FINAL_TYPE( FoobarNetworkAdapterWired, foobar_network_adapter_wired, FOOBAR, NETWORK_ADAPTER_WIRED, FoobarNetworkAdapter )
+
+gint64 foobar_network_adapter_wired_get_speed( FoobarNetworkAdapterWired* self );
+
+G_DECLARE_FINAL_TYPE( FoobarNetworkAdapterWifi, foobar_network_adapter_wifi, FOOBAR, NETWORK_ADAPTER_WIFI, FoobarNetworkAdapter )
+
+GListModel* foobar_network_adapter_wifi_get_networks( FoobarNetworkAdapterWifi* self );
+FoobarNetwork* foobar_network_adapter_wifi_get_active ( FoobarNetworkAdapterWifi* self );
+gboolean foobar_network_adapter_wifi_is_enabled ( FoobarNetworkAdapterWifi* self );
+gboolean foobar_network_adapter_wifi_is_scanning ( FoobarNetworkAdapterWifi* self );
+void foobar_network_adapter_wifi_set_enabled ( FoobarNetworkAdapterWifi* self,
+ gboolean value );
+void foobar_network_adapter_wifi_set_scanning( FoobarNetworkAdapterWifi* self,
+ gboolean value );
+
+G_DECLARE_FINAL_TYPE( FoobarNetworkService, foobar_network_service, FOOBAR, NETWORK_SERVICE, GObject )
+
+FoobarNetworkService* foobar_network_service_new ( void );
+FoobarNetworkAdapterWired* foobar_network_service_get_wired ( FoobarNetworkService* self );
+FoobarNetworkAdapterWifi* foobar_network_service_get_wifi ( FoobarNetworkService* self );
+FoobarNetworkAdapter* foobar_network_service_get_active( FoobarNetworkService* self );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/services/notification-service.c b/src/services/notification-service.c
new file mode 100644
index 0000000..64f552d
--- /dev/null
+++ b/src/services/notification-service.c
@@ -0,0 +1,1833 @@
+#include "services/notification-service.h"
+#include "dbus/notifications.h"
+#include "utils.h"
+#include <json-glib/json-glib.h>
+#include <gtk/gtk.h>
+
+#define DEFAULT_TIMEOUT 3000
+
+//
+// FoobarNotificationUrgency:
+//
+// The urgency of a notification as provided by the application that sent it.
+//
+
+G_DEFINE_ENUM_TYPE(
+ FoobarNotificationUrgency,
+ foobar_notification_urgency,
+ G_DEFINE_ENUM_VALUE( FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_COMFORT, "loss-of-comfort" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY, "loss-of-money" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_LIFE, "loss-of-life" ) )
+
+//
+// FoobarNotificationAction:
+//
+// A labeled action for a notification that can be invoked by the notification daemon (us).
+//
+
+struct _FoobarNotificationAction
+{
+ GObject parent_instance;
+ FoobarNotification* notification;
+ gchar* id;
+ gchar* label;
+};
+
+enum
+{
+ ACTION_PROP_ID = 1,
+ ACTION_PROP_LABEL,
+ N_ACTION_PROPS,
+};
+
+static GParamSpec* action_props[N_ACTION_PROPS] = { 0 };
+
+static void foobar_notification_action_class_init ( FoobarNotificationActionClass* klass );
+static void foobar_notification_action_init ( FoobarNotificationAction* self );
+static void foobar_notification_action_get_property( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_notification_action_finalize ( GObject* object );
+static FoobarNotificationAction* foobar_notification_action_new ( FoobarNotification* notification );
+static void foobar_notification_action_set_id ( FoobarNotificationAction* self,
+ gchar const* value );
+static void foobar_notification_action_set_label ( FoobarNotificationAction* self,
+ gchar const* value );
+
+G_DEFINE_FINAL_TYPE( FoobarNotificationAction, foobar_notification_action, G_TYPE_OBJECT )
+
+//
+// FoobarNotification:
+//
+// A notification received by the notification daemon (us).
+//
+
+struct _FoobarNotification
+{
+ GObject parent_instance;
+ guint timeout_id;
+ FoobarNotificationService* service;
+ guint id;
+ GPtrArray* actions;
+ gchar* app_entry;
+ gchar* app_name;
+ gchar* body;
+ gchar* summary;
+ GdkPixbuf* image;
+ gchar* image_path;
+ gchar* image_data;
+ gboolean is_dismissed;
+ gboolean is_resident;
+ gboolean is_transient;
+ GDateTime* time;
+ gint64 timeout;
+ FoobarNotificationUrgency urgency;
+};
+
+enum
+{
+ NOTIFICATION_PROP_ID = 1,
+ NOTIFICATION_PROP_APP_ENTRY,
+ NOTIFICATION_PROP_APP_NAME,
+ NOTIFICATION_PROP_BODY,
+ NOTIFICATION_PROP_SUMMARY,
+ NOTIFICATION_PROP_IMAGE,
+ NOTIFICATION_PROP_IS_DISMISSED,
+ NOTIFICATION_PROP_IS_RESIDENT,
+ NOTIFICATION_PROP_IS_TRANSIENT,
+ NOTIFICATION_PROP_TIME,
+ NOTIFICATION_PROP_TIMEOUT,
+ NOTIFICATION_PROP_URGENCY,
+ N_NOTIFICATION_PROPS,
+};
+
+static GParamSpec* notification_props[N_NOTIFICATION_PROPS] = { 0 };
+
+static void foobar_notification_class_init ( FoobarNotificationClass* klass );
+static void foobar_notification_init ( FoobarNotification* self );
+static void foobar_notification_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_notification_finalize ( GObject* object );
+static FoobarNotification* foobar_notification_new ( FoobarNotificationService* service );
+static gchar const* foobar_notification_get_image_path ( FoobarNotification* self );
+static gchar const* foobar_notification_get_image_data ( FoobarNotification* self );
+static void foobar_notification_set_id ( FoobarNotification* self,
+ guint value );
+static void foobar_notification_set_app_entry ( FoobarNotification* self,
+ gchar const* value );
+static void foobar_notification_set_app_name ( FoobarNotification* self,
+ gchar const* value );
+static void foobar_notification_set_body ( FoobarNotification* self,
+ gchar const* value );
+static void foobar_notification_set_summary ( FoobarNotification* self,
+ gchar const* value );
+static void foobar_notification_set_image_from_path( FoobarNotification* self,
+ gchar const* value );
+static void foobar_notification_set_image_from_data( FoobarNotification* self,
+ gchar const* value );
+static void foobar_notification_set_image ( FoobarNotification* self,
+ GdkPixbuf* value );
+static void foobar_notification_set_dismissed ( FoobarNotification* self,
+ gboolean value );
+static void foobar_notification_set_resident ( FoobarNotification* self,
+ gboolean value );
+static void foobar_notification_set_transient ( FoobarNotification* self,
+ gboolean value );
+static void foobar_notification_set_time ( FoobarNotification* self,
+ GDateTime* value );
+static void foobar_notification_set_timeout ( FoobarNotification* self,
+ gint64 value );
+static void foobar_notification_set_urgency ( FoobarNotification* self,
+ FoobarNotificationUrgency value );
+static void foobar_notification_add_action ( FoobarNotification* self,
+ FoobarNotificationAction* action );
+static void foobar_notification_free_action ( gpointer action );
+static gboolean foobar_notification_handle_timeout ( gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarNotification, foobar_notification, G_TYPE_OBJECT )
+
+//
+// FoobarNotificationService:
+//
+// Service acting as the notification daemon, receiving incoming notifications from other applications. The service also
+// persistently saves previously received notifications until they are closed.
+//
+
+struct _FoobarNotificationService
+{
+ GObject parent_instance;
+ GListStore* notifications;
+ GtkSortListModel* sorted_notifications;
+ GtkFilterListModel* popup_notifications;
+ FoobarNotifications* skeleton;
+ guint bus_owner_id;
+ guint next_id;
+ gchar* cache_path;
+ GMutex write_cache_mutex;
+};
+
+enum
+{
+ PROP_NOTIFICATIONS = 1,
+ PROP_POPUP_NOTIFICATIONS,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_notification_service_class_init ( FoobarNotificationServiceClass* klass );
+static void foobar_notification_service_init ( FoobarNotificationService* self );
+static void foobar_notification_service_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_notification_service_finalize ( GObject* object );
+static void foobar_notification_service_read_cache ( FoobarNotificationService* self );
+static void foobar_notification_service_write_cache ( FoobarNotificationService* self );
+static void foobar_notification_service_write_cache_cb ( GObject* object,
+ GAsyncResult* result,
+ gpointer userdata );
+static void foobar_notification_service_write_cache_async ( FoobarNotificationService* self,
+ GPtrArray* notifications,
+ GCancellable* cancellable,
+ GAsyncReadyCallback callback,
+ gpointer userdata );
+static gboolean foobar_notification_service_write_cache_finish ( FoobarNotificationService* self,
+ GAsyncResult* result,
+ GError** error );
+static void foobar_notification_service_write_cache_thread ( GTask* task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable* cancellable );
+static void foobar_notification_service_handle_bus_acquired ( GDBusConnection* connection,
+ gchar const* name,
+ gpointer userdata );
+static void foobar_notification_service_handle_bus_lost ( GDBusConnection* connection,
+ gchar const* name,
+ gpointer userdata );
+static gboolean foobar_notification_service_handle_notify ( FoobarNotifications* iface,
+ GDBusMethodInvocation* invocation,
+ gchar const* app_name,
+ guint replaces_id,
+ gchar const* image,
+ gchar const* summary,
+ gchar const* body,
+ gchar const* const* actions,
+ GVariant* hints,
+ gint expiration,
+ gpointer userdata );
+static gboolean foobar_notification_service_handle_close_notification ( FoobarNotifications* iface,
+ GDBusMethodInvocation* invocation,
+ guint id,
+ gpointer userdata );
+static gboolean foobar_notification_service_handle_get_capabilities ( FoobarNotifications* iface,
+ GDBusMethodInvocation* invocation,
+ gpointer userdata );
+static gboolean foobar_notification_service_handle_get_server_information( FoobarNotifications* iface,
+ GDBusMethodInvocation* invocation,
+ gpointer userdata );
+static gboolean foobar_notification_service_popup_filter_func ( gpointer item,
+ gpointer userdata );
+static gint foobar_notification_service_sort_func ( gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarNotificationService, foobar_notification_service, G_TYPE_OBJECT )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Notification Action
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for notification actions.
+//
+void foobar_notification_action_class_init( FoobarNotificationActionClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_notification_action_get_property;
+ object_klass->finalize = foobar_notification_action_finalize;
+
+ action_props[ACTION_PROP_ID] = g_param_spec_string(
+ "id",
+ "ID",
+ "Textual ID of the action within the notification.",
+ NULL,
+ G_PARAM_READABLE );
+ action_props[ACTION_PROP_LABEL] = g_param_spec_string(
+ "label",
+ "Label",
+ "Human-readable title for the action.",
+ NULL,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_ACTION_PROPS, action_props );
+}
+
+//
+// Instance initialization for notification actions.
+//
+void foobar_notification_action_init( FoobarNotificationAction* self )
+{
+ (void)self;
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_notification_action_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarNotificationAction* self = (FoobarNotificationAction*)object;
+
+ switch ( prop_id )
+ {
+ case ACTION_PROP_ID:
+ g_value_set_string( value, foobar_notification_action_get_id( self ) );
+ break;
+ case ACTION_PROP_LABEL:
+ g_value_set_string( value, foobar_notification_action_get_label( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for notification actions.
+//
+void foobar_notification_action_finalize( GObject* object )
+{
+ FoobarNotificationAction* self = (FoobarNotificationAction*)object;
+
+ g_clear_pointer( &self->id, g_free );
+ g_clear_pointer( &self->label, g_free );
+
+ G_OBJECT_CLASS( foobar_notification_action_parent_class )->finalize( object );
+}
+
+//
+// Create a new notification action that is owned by the given notification (captured as an unowned reference).
+//
+FoobarNotificationAction* foobar_notification_action_new( FoobarNotification* notification )
+{
+ FoobarNotificationAction* self = g_object_new( FOOBAR_TYPE_NOTIFICATION_ACTION, NULL );
+ self->notification = notification;
+ return self;
+}
+
+//
+// Get the textual ID of the action within the notification.
+//
+gchar const* foobar_notification_action_get_id( FoobarNotificationAction* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ), NULL );
+ return self->id;
+}
+
+//
+// Get a human-readable title for the action.
+//
+gchar const* foobar_notification_action_get_label( FoobarNotificationAction* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ), NULL );
+ return self->label;
+}
+
+//
+// Update the textual ID of the action within the notification.
+//
+void foobar_notification_action_set_id(
+ FoobarNotificationAction* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ) );
+
+ if ( g_strcmp0( self->id, value ) )
+ {
+ g_clear_pointer( &self->id, g_free );
+ self->id = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), action_props[ACTION_PROP_ID] );
+ }
+}
+
+//
+// Set a human-readable title for the action.
+//
+void foobar_notification_action_set_label(
+ FoobarNotificationAction* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ) );
+
+ if ( g_strcmp0( self->label, value ) )
+ {
+ g_clear_pointer( &self->label, g_free );
+ self->label = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), action_props[ACTION_PROP_LABEL] );
+ }
+}
+
+//
+// Invoke this action by emitting a signal that can be handled by its sender application.
+//
+void foobar_notification_action_invoke( FoobarNotificationAction* self )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ) );
+
+ if ( self->notification && self->notification->service )
+ {
+ FoobarNotification* notification = self->notification;
+ FoobarNotificationService* service = notification->service;
+
+ foobar_notifications_emit_action_invoked(
+ service->skeleton,
+ foobar_notification_get_id( notification ),
+ foobar_notification_action_get_id( self ) );
+
+ if ( !foobar_notification_is_resident( notification ) )
+ {
+ foobar_notification_close( notification );
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Notification
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for notifications.
+//
+void foobar_notification_class_init( FoobarNotificationClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_notification_get_property;
+ object_klass->finalize = foobar_notification_finalize;
+
+ notification_props[NOTIFICATION_PROP_ID] = g_param_spec_uint(
+ "id",
+ "ID",
+ "Numeric ID of the notification.",
+ 0,
+ UINT_MAX,
+ 0,
+ G_PARAM_READABLE );
+ notification_props[NOTIFICATION_PROP_APP_ENTRY] = g_param_spec_string(
+ "app-entry",
+ "App Entry",
+ "The desktop file of the notification's application.",
+ NULL,
+ G_PARAM_READABLE );
+ notification_props[NOTIFICATION_PROP_APP_NAME] = g_param_spec_string(
+ "app-name",
+ "App Name",
+ "Human-readable name of the application.",
+ NULL,
+ G_PARAM_READABLE );
+ notification_props[NOTIFICATION_PROP_BODY] = g_param_spec_string(
+ "body",
+ "Body",
+ "Main content of the notification.",
+ NULL,
+ G_PARAM_READABLE );
+ notification_props[NOTIFICATION_PROP_SUMMARY] = g_param_spec_string(
+ "summary",
+ "Summary",
+ "Brief summary of the notification's content.",
+ NULL,
+ G_PARAM_READABLE );
+ notification_props[NOTIFICATION_PROP_IMAGE] = g_param_spec_object(
+ "image",
+ "Image",
+ "An image icon for the notification.",
+ GDK_TYPE_PIXBUF,
+ G_PARAM_READABLE );
+ notification_props[NOTIFICATION_PROP_IS_DISMISSED] = g_param_spec_boolean(
+ "is-dismissed",
+ "Is Dismissed",
+ "Indicates that the notification was dismissed or the timeout has passed.",
+ FALSE,
+ G_PARAM_READABLE );
+ notification_props[NOTIFICATION_PROP_IS_RESIDENT] = g_param_spec_boolean(
+ "is-resident",
+ "Is Resident",
+ "Indicates that the notification is not automatically removed once an action is invoked.",
+ FALSE,
+ G_PARAM_READABLE );
+ notification_props[NOTIFICATION_PROP_IS_TRANSIENT] = g_param_spec_boolean(
+ "is-transient",
+ "Is Transient",
+ "Indicates that the persistence capability should be bypassed.",
+ FALSE,
+ G_PARAM_READABLE );
+ notification_props[NOTIFICATION_PROP_TIME] = g_param_spec_boxed(
+ "time",
+ "Time",
+ "The timestamp at which the notification was received.",
+ G_TYPE_DATE_TIME,
+ G_PARAM_READABLE );
+ notification_props[NOTIFICATION_PROP_TIMEOUT] = g_param_spec_int64(
+ "timeout",
+ "Timeout",
+ "Timeout after which the notification auto-closes in milliseconds.",
+ 0,
+ INT64_MAX,
+ 0,
+ G_PARAM_READABLE );
+ notification_props[NOTIFICATION_PROP_URGENCY] = g_param_spec_enum(
+ "urgency",
+ "Urgency",
+ "The urgency state of the notification.",
+ FOOBAR_TYPE_NOTIFICATION_URGENCY,
+ FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_NOTIFICATION_PROPS, notification_props );
+}
+
+//
+// Instance initialization for notifications.
+//
+void foobar_notification_init( FoobarNotification* self )
+{
+ self->actions = g_ptr_array_new_with_free_func( foobar_notification_free_action );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_notification_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarNotification* self = (FoobarNotification*)object;
+
+ switch ( prop_id )
+ {
+ case NOTIFICATION_PROP_ID:
+ g_value_set_int64( value, foobar_notification_get_id( self ) );
+ break;
+ case NOTIFICATION_PROP_APP_ENTRY:
+ g_value_set_string( value, foobar_notification_get_app_entry( self ) );
+ break;
+ case NOTIFICATION_PROP_APP_NAME:
+ g_value_set_string( value, foobar_notification_get_app_name( self ) );
+ break;
+ case NOTIFICATION_PROP_BODY:
+ g_value_set_string( value, foobar_notification_get_body( self ) );
+ break;
+ case NOTIFICATION_PROP_SUMMARY:
+ g_value_set_string( value, foobar_notification_get_summary( self ) );
+ break;
+ case NOTIFICATION_PROP_IMAGE:
+ g_value_set_object( value, foobar_notification_get_image( self ) );
+ break;
+ case NOTIFICATION_PROP_IS_DISMISSED:
+ g_value_set_boolean( value, foobar_notification_is_dismissed( self ) );
+ break;
+ case NOTIFICATION_PROP_IS_RESIDENT:
+ g_value_set_boolean( value, foobar_notification_is_resident( self ) );
+ break;
+ case NOTIFICATION_PROP_IS_TRANSIENT:
+ g_value_set_boolean( value, foobar_notification_is_transient( self ) );
+ break;
+ case NOTIFICATION_PROP_TIME:
+ g_value_set_boxed( value, foobar_notification_get_time( self ) );
+ break;
+ case NOTIFICATION_PROP_TIMEOUT:
+ g_value_set_int64( value, foobar_notification_get_timeout( self ) );
+ break;
+ case NOTIFICATION_PROP_URGENCY:
+ g_value_set_enum( value, foobar_notification_get_urgency( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance initialization for notifications.
+//
+void foobar_notification_finalize( GObject* object )
+{
+ FoobarNotification* self = (FoobarNotification*)object;
+
+ g_clear_object( &self->image );
+ g_clear_pointer( &self->actions, g_ptr_array_unref );
+ g_clear_pointer( &self->app_entry, g_free );
+ g_clear_pointer( &self->app_name, g_free );
+ g_clear_pointer( &self->body, g_free );
+ g_clear_pointer( &self->summary, g_free );
+ g_clear_pointer( &self->image_path, g_free );
+ g_clear_pointer( &self->image_data, g_free );
+ g_clear_pointer( &self->time, g_date_time_unref );
+
+ G_OBJECT_CLASS( foobar_notification_parent_class )->finalize( object );
+}
+
+//
+// Create a new notification that is owned by the given service (captured as an unowned reference).
+//
+FoobarNotification* foobar_notification_new( FoobarNotificationService* service )
+{
+ FoobarNotification* self = g_object_new( FOOBAR_TYPE_NOTIFICATION, NULL );
+ self->service = service;
+ return self;
+}
+
+//
+// Numeric ID of the notification.
+//
+guint foobar_notification_get_id( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), 0 );
+ return self->id;
+}
+
+//
+// A list of actions which can be invoked by notifying the notification's sender.
+//
+FoobarNotificationAction** foobar_notification_get_actions(
+ FoobarNotification* self,
+ guint* out_count )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL );
+ if ( out_count ) { *out_count = self->actions->len; }
+ return (FoobarNotificationAction**)self->actions->pdata;
+}
+
+//
+// The desktop file of the notification's application.
+//
+gchar const* foobar_notification_get_app_entry( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL );
+ return self->app_entry;
+}
+
+//
+// Human-readable name of the application.
+//
+gchar const* foobar_notification_get_app_name( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL );
+ return self->app_name;
+}
+
+//
+// Main content of the notification.
+//
+gchar const* foobar_notification_get_body( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL );
+ return self->body;
+}
+
+//
+// Brief summary of the notification's content.
+//
+gchar const* foobar_notification_get_summary( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL );
+ return self->summary;
+}
+
+//
+// The source path for the notification's image if it was loaded from a file.
+//
+gchar const* foobar_notification_get_image_path( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL );
+ return self->image_path;
+}
+
+//
+// The base64-encoded data for the notification's image if it was loaded from data.
+//
+gchar const* foobar_notification_get_image_data( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL );
+ return self->image_data;
+}
+
+//
+// An image icon for the notification.
+//
+GdkPixbuf* foobar_notification_get_image( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL );
+ return self->image;
+}
+
+//
+// Indicates that the notification was dismissed or the timeout has passed.
+//
+gboolean foobar_notification_is_dismissed( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FALSE );
+ return self->is_dismissed;
+}
+
+//
+// Indicates that the notification is not automatically removed once an action is invoked.
+//
+gboolean foobar_notification_is_resident( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FALSE );
+ return self->is_resident;
+}
+
+//
+// Indicates that the persistence capability should be bypassed.
+//
+gboolean foobar_notification_is_transient( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FALSE );
+ return self->is_transient;
+}
+
+//
+// The timestamp at which the notification was received.
+//
+GDateTime* foobar_notification_get_time( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL );
+ return self->time;
+}
+
+//
+// Timeout after which the notification auto-closes in milliseconds.
+//
+gint64 foobar_notification_get_timeout( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), 0 );
+ return self->timeout;
+}
+
+//
+// The urgency state of the notification.
+//
+FoobarNotificationUrgency foobar_notification_get_urgency( FoobarNotification* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY );
+ return self->urgency;
+}
+
+//
+// Numeric ID of the notification.
+//
+void foobar_notification_set_id(
+ FoobarNotification* self,
+ guint value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ if ( self->id != value )
+ {
+ self->id = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_ID] );
+ }
+}
+
+//
+// The desktop file of the notification's application.
+//
+void foobar_notification_set_app_entry(
+ FoobarNotification* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ if ( g_strcmp0( self->app_entry, value ) )
+ {
+ g_clear_pointer( &self->app_entry, g_free );
+ self->app_entry = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_APP_ENTRY] );
+ }
+}
+
+//
+// Human-readable name of the application.
+//
+void foobar_notification_set_app_name(
+ FoobarNotification* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ if ( g_strcmp0( self->app_name, value ) )
+ {
+ g_clear_pointer( &self->app_name, g_free );
+ self->app_name = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_APP_NAME] );
+ }
+}
+
+//
+// Main content of the notification.
+//
+void foobar_notification_set_body(
+ FoobarNotification* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ if ( g_strcmp0( self->body, value ) )
+ {
+ g_clear_pointer( &self->body, g_free );
+ self->body = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_BODY] );
+ }
+}
+
+//
+// Brief summary of the notification's content.
+//
+void foobar_notification_set_summary(
+ FoobarNotification* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ if ( g_strcmp0( self->summary, value ) )
+ {
+ g_clear_pointer( &self->summary, g_free );
+ self->summary = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_SUMMARY] );
+ }
+}
+
+//
+// Update the notification's image by trying to load a file at the provided path.
+//
+void foobar_notification_set_image_from_path(
+ FoobarNotification* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ if ( g_strcmp0( self->image_path, value ) )
+ {
+ g_clear_pointer( &self->image_path, g_free );
+ g_clear_pointer( &self->image_data, g_free );
+ g_clear_object( &self->image );
+ self->image_path = g_strdup( value );
+
+ g_autoptr( GError ) error = NULL;
+ self->image = gdk_pixbuf_new_from_file( self->image_path, &error );
+ if ( !self->image )
+ {
+ g_warning( "Invalid image path for notification: %s", error->message );
+ }
+
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IMAGE] );
+ }
+}
+
+//
+// Update the notification's image by trying to load base64-encoded data.
+//
+void foobar_notification_set_image_from_data(
+ FoobarNotification* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ if ( g_strcmp0( self->image_data, value ) )
+ {
+ g_clear_pointer( &self->image_path, g_free );
+ g_clear_pointer( &self->image_data, g_free );
+ g_clear_object( &self->image );
+ self->image_data = g_strdup( value );
+
+ gsize data_len;
+ guchar* data = g_base64_decode( self->image_data, &data_len );
+ g_autoptr( GError ) error = NULL;
+ g_autoptr( GInputStream ) input = g_memory_input_stream_new_from_data( data, (gssize)data_len, g_free );
+ self->image = gdk_pixbuf_new_from_stream( input, NULL, &error );
+ if ( !self->image )
+ {
+ g_warning( "Invalid image data for notification: %s", error->message );
+ }
+
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IMAGE] );
+ }
+}
+
+//
+// Update the notification's image to a custom pixbuf.
+//
+static void foobar_notification_set_image(
+ FoobarNotification* self,
+ GdkPixbuf* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ if ( self->image != value )
+ {
+ g_clear_pointer( &self->image_path, g_free );
+ g_clear_pointer( &self->image_data, g_free );
+ g_clear_object( &self->image );
+
+ if ( value )
+ {
+ self->image = g_object_ref( value );
+ g_autoptr( GError ) error = NULL;
+ gchar* data;
+ gsize data_len;
+ if ( gdk_pixbuf_save_to_buffer( self->image, &data, &data_len, "png", &error, NULL ) )
+ {
+ self->image_data = g_base64_encode( (guchar const*)data, data_len );
+ }
+ else
+ {
+ g_warning( "Unable to save image for notification: %s", error->message );
+ g_clear_object( &self->image );
+ }
+ }
+
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IMAGE] );
+ }
+}
+
+//
+// Indicates that the notification was dismissed or the timeout has passed.
+//
+void foobar_notification_set_dismissed(
+ FoobarNotification* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ value = !!value;
+ if ( self->is_dismissed != value )
+ {
+ self->is_dismissed = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IS_DISMISSED] );
+
+ if ( self->service )
+ {
+ gtk_filter_changed(
+ gtk_filter_list_model_get_filter( self->service->popup_notifications ),
+ value ? GTK_FILTER_CHANGE_MORE_STRICT : GTK_FILTER_CHANGE_LESS_STRICT );
+ }
+ }
+}
+
+//
+// Indicates that the notification is not automatically removed once an action is invoked.
+//
+void foobar_notification_set_resident(
+ FoobarNotification* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ value = !!value;
+ if ( self->is_resident != value )
+ {
+ self->is_resident = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IS_RESIDENT] );
+ }
+}
+
+//
+// Indicates that the persistence capability should be bypassed.
+//
+void foobar_notification_set_transient(
+ FoobarNotification* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ value = !!value;
+ if ( self->is_transient != value )
+ {
+ self->is_transient = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IS_TRANSIENT] );
+ }
+}
+
+//
+// The timestamp at which the notification was received.
+//
+void foobar_notification_set_time(
+ FoobarNotification* self,
+ GDateTime* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ if ( self->time != value )
+ {
+ if ( self->time ) { g_date_time_unref( self->time ); }
+ self->time = g_date_time_ref( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_TIME] );
+ }
+}
+
+//
+// Timeout after which the notification auto-closes in milliseconds.
+//
+void foobar_notification_set_timeout(
+ FoobarNotification* self,
+ gint64 value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ value = MAX( value, 0 );
+ if ( self->timeout != value )
+ {
+ self->timeout = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_TIMEOUT] );
+ }
+}
+
+//
+// The urgency state of the notification.
+//
+void foobar_notification_set_urgency(
+ FoobarNotification* self,
+ FoobarNotificationUrgency value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ if ( self->urgency != value )
+ {
+ self->urgency = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_URGENCY] );
+ }
+}
+
+//
+// Register an action for the notification.
+//
+void foobar_notification_add_action(
+ FoobarNotification* self,
+ FoobarNotificationAction* action )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+ g_return_if_fail( action->notification == self );
+
+ g_ptr_array_add( self->actions, g_object_ref( action ) );
+}
+
+//
+// Block the notification from automatically being dismissed.
+//
+void foobar_notification_block_timeout( FoobarNotification* self )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ g_clear_handle_id( &self->timeout_id, g_source_remove );
+}
+
+//
+// If not already started, start the timeout to automatically dismiss the notification after it's timeout.
+//
+void foobar_notification_resume_timeout( FoobarNotification* self )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ if ( !self->is_dismissed && !self->timeout_id )
+ {
+ self->timeout_id = g_timeout_add_full(
+ G_NOTIFICATION_PRIORITY_NORMAL,
+ self->timeout,
+ foobar_notification_handle_timeout,
+ g_object_ref( self ),
+ g_object_unref );
+ }
+}
+
+//
+// Manually dismiss the notification, setting "is-dismissed" to TRUE.
+//
+void foobar_notification_dismiss( FoobarNotification* self )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ foobar_notification_set_dismissed( self, TRUE );
+}
+
+//
+// Close the notification, removing it from the parent service's list.
+//
+void foobar_notification_close( FoobarNotification* self )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) );
+
+ if ( self->service )
+ {
+ FoobarNotificationService* service = self->service;
+ self->service = NULL;
+
+ guint id = foobar_notification_get_id( self );
+
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( service->notifications ) ); ++i )
+ {
+ if ( g_list_model_get_item( G_LIST_MODEL( service->notifications ), i ) == self )
+ {
+ g_list_store_remove( service->notifications, i );
+ foobar_notification_service_write_cache( service );
+ break;
+ }
+ }
+
+ foobar_notifications_emit_notification_closed( service->skeleton, id, 3 );
+ }
+}
+
+//
+// Called when an action is removed from a notification, resetting its weak reference to the notification.
+//
+void foobar_notification_free_action( gpointer action )
+{
+ FoobarNotificationAction* object = (FoobarNotificationAction*)action;
+ object->notification = NULL;
+ g_object_unref( object );
+}
+
+//
+// Called after the notification's timeout has elapsed.
+//
+gboolean foobar_notification_handle_timeout( gpointer userdata )
+{
+ FoobarNotification* notification = (FoobarNotification*)userdata;
+
+ foobar_notification_dismiss( notification );
+ notification->timeout_id = 0;
+
+ return G_SOURCE_REMOVE;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Service Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the notification service.
+//
+void foobar_notification_service_class_init( FoobarNotificationServiceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_notification_service_get_property;
+ object_klass->finalize = foobar_notification_service_finalize;
+
+ props[PROP_NOTIFICATIONS] = g_param_spec_object(
+ "notifications",
+ "Notifications",
+ "Sorted list of all notifications.",
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READABLE );
+ props[PROP_POPUP_NOTIFICATIONS] = g_param_spec_object(
+ "popup-notifications",
+ "Popup Notifications",
+ "Sorted list of all visible notifications (i.e. notifications that are not dismissed).",
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for the notification service.
+//
+void foobar_notification_service_init( FoobarNotificationService* self )
+{
+ self->notifications = g_list_store_new( FOOBAR_TYPE_NOTIFICATION );
+ self->cache_path = foobar_get_cache_path( "notifications.json" );
+ g_mutex_init( &self->write_cache_mutex );
+
+ GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_notification_service_sort_func, NULL, NULL );
+ self->sorted_notifications = gtk_sort_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->notifications ) ),
+ GTK_SORTER( sorter ) );
+
+ GtkCustomFilter* popup_filter = gtk_custom_filter_new( foobar_notification_service_popup_filter_func, NULL, NULL );
+ self->popup_notifications = gtk_filter_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->sorted_notifications ) ),
+ GTK_FILTER( popup_filter ) );
+
+ foobar_notification_service_read_cache( self );
+
+ self->bus_owner_id = g_bus_own_name(
+ G_BUS_TYPE_SESSION,
+ "org.freedesktop.Notifications",
+ G_BUS_NAME_OWNER_FLAGS_NONE,
+ foobar_notification_service_handle_bus_acquired,
+ NULL,
+ foobar_notification_service_handle_bus_lost,
+ self,
+ NULL );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_notification_service_get_property( GObject* object, guint prop_id, GValue* value, GParamSpec* pspec )
+{
+ FoobarNotificationService* self = (FoobarNotificationService*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_NOTIFICATIONS:
+ g_value_set_object( value, foobar_notification_service_get_notifications( self ) );
+ break;
+ case PROP_POPUP_NOTIFICATIONS:
+ g_value_set_object( value, foobar_notification_service_get_popup_notifications( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for the notification service.
+//
+void foobar_notification_service_finalize( GObject* object )
+{
+ FoobarNotificationService* self = (FoobarNotificationService*)object;
+
+ if ( self->skeleton ) { g_dbus_interface_skeleton_unexport( G_DBUS_INTERFACE_SKELETON( self->skeleton ) ); }
+
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->notifications ) ); ++i )
+ {
+ FoobarNotification* notification = g_list_model_get_item( G_LIST_MODEL( self->notifications ), i );
+ notification->service = NULL;
+ }
+
+ g_clear_object( &self->popup_notifications );
+ g_clear_object( &self->sorted_notifications );
+ g_clear_object( &self->notifications );
+ g_clear_object( &self->skeleton );
+ g_clear_handle_id( &self->bus_owner_id, g_bus_unown_name );
+ g_clear_pointer( &self->cache_path, g_free );
+
+ g_mutex_clear( &self->write_cache_mutex );
+
+ G_OBJECT_CLASS( foobar_notification_service_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new notification service instance.
+//
+FoobarNotificationService* foobar_notification_service_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_NOTIFICATION_SERVICE, NULL );
+}
+
+//
+// Get a sorted list of all notifications.
+//
+GListModel* foobar_notification_service_get_notifications( FoobarNotificationService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( self ), NULL );
+ return G_LIST_MODEL( self->sorted_notifications );
+}
+
+//
+// Get a sorted list of all visible notifications (i.e. notifications that are not dismissed).
+//
+GListModel* foobar_notification_service_get_popup_notifications( FoobarNotificationService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( self ), NULL );
+ return G_LIST_MODEL( self->popup_notifications );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called after the process has asynchronously acquired the notification bus.
+//
+// This is where we can export notification daemon functionality using the corresponding DBus skeleton object.
+//
+void foobar_notification_service_handle_bus_acquired(
+ GDBusConnection* connection,
+ gchar const* name,
+ gpointer userdata )
+{
+ (void)name;
+ FoobarNotificationService* self = (FoobarNotificationService*)userdata;
+
+ self->skeleton = foobar_notifications_skeleton_new( );
+ g_signal_connect(
+ self->skeleton,
+ "handle-notify",
+ G_CALLBACK( foobar_notification_service_handle_notify ),
+ self );
+ g_signal_connect(
+ self->skeleton,
+ "handle-close-notification",
+ G_CALLBACK( foobar_notification_service_handle_close_notification ),
+ self );
+ g_signal_connect(
+ self->skeleton,
+ "handle-get-capabilities",
+ G_CALLBACK( foobar_notification_service_handle_get_capabilities ),
+ self );
+ g_signal_connect(
+ self->skeleton,
+ "handle-get-server-information",
+ G_CALLBACK( foobar_notification_service_handle_get_server_information ),
+ self );
+
+ g_autoptr( GError ) error = NULL;
+ if ( !g_dbus_interface_skeleton_export(
+ G_DBUS_INTERFACE_SKELETON( self->skeleton ),
+ connection,
+ "/org/freedesktop/Notifications",
+ &error ) )
+ {
+ g_warning( "Unable to export notifications interface: %s", error->message );
+ }
+}
+
+//
+// Called after the process has failed to acquire the notification bus.
+//
+void foobar_notification_service_handle_bus_lost(
+ GDBusConnection* connection,
+ gchar const* name,
+ gpointer userdata )
+{
+ (void)connection;
+ (void)name;
+ (void)userdata;
+
+ g_autofree gchar* other_name = NULL;
+ g_autoptr( FoobarNotifications ) proxy = foobar_notifications_proxy_new_for_bus_sync(
+ G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ "org.freedesktop.Notifications",
+ "/org/freedesktop/Notifications",
+ NULL,
+ NULL );
+
+ if ( proxy )
+ {
+ foobar_notifications_call_get_server_information_sync(
+ proxy,
+ &other_name,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL );
+ }
+
+ g_warning(
+ "Unable to register notification daemon because %s is already running.",
+ other_name ? other_name : "(unknown)" );
+}
+
+//
+// DBus skeleton callback for the "Notify" method.
+//
+gboolean foobar_notification_service_handle_notify(
+ FoobarNotifications* iface,
+ GDBusMethodInvocation* invocation,
+ gchar const* app_name,
+ guint replaces_id,
+ gchar const* image,
+ gchar const* summary,
+ gchar const* body,
+ gchar const* const* actions,
+ GVariant* hints,
+ gint expiration,
+ gpointer userdata )
+{
+ FoobarNotificationService* self = (FoobarNotificationService*)userdata;
+
+ g_autoptr( GDateTime ) time = g_date_time_new_now_local( );
+ g_autoptr( FoobarNotification ) notification = foobar_notification_new( self );
+ foobar_notification_set_id( notification, replaces_id ? replaces_id : self->next_id++ );
+ foobar_notification_set_app_name( notification, app_name );
+ foobar_notification_set_summary( notification, summary );
+ foobar_notification_set_body( notification, body );
+ foobar_notification_set_time( notification, time );
+ foobar_notification_set_timeout( notification, expiration != -1 ? expiration : DEFAULT_TIMEOUT );
+ if ( image && *image ) { foobar_notification_set_image_from_path( notification, image ); }
+
+ if ( hints )
+ {
+ // Process hints to set some more properties for the notification.
+
+ gsize hint_count = g_variant_n_children( hints );
+ for ( gsize i = 0; i < hint_count; ++i )
+ {
+ g_autoptr( GVariant ) hint = g_variant_get_child_value( hints, i );
+ g_autoptr( GVariant ) key = g_variant_get_child_value( hint, 0 );
+ g_autoptr( GVariant ) value_wrapper = g_variant_get_child_value( hint, 1 );
+ g_autoptr( GVariant ) value = g_variant_get_variant( value_wrapper );
+ gchar const* key_str = g_variant_get_string( key, NULL );
+
+ if ( !g_strcmp0( key_str, "desktop-entry" ) )
+ {
+ foobar_notification_set_app_entry( notification, g_variant_get_string( value, NULL ) );
+ }
+ else if ( !g_strcmp0( key_str, "resident" ) )
+ {
+ foobar_notification_set_resident( notification, g_variant_get_boolean( value ) );
+ }
+ else if ( !g_strcmp0( key_str, "transient" ) )
+ {
+ foobar_notification_set_transient( notification, g_variant_get_boolean( value ) );
+ }
+ else if ( !g_strcmp0( key_str, "urgency" ) )
+ {
+ foobar_notification_set_urgency( notification, g_variant_get_byte( value ) );
+ }
+
+ if ( !foobar_notification_get_image( notification ) )
+ {
+ if ( !g_strcmp0( key_str, "image-path" ) ||
+ !g_strcmp0( key_str, "image_path" ) )
+ {
+ foobar_notification_set_image_from_path( notification, g_variant_get_string( value, NULL ) );
+ }
+ else if ( !g_strcmp0( key_str, "image-data" ) ||
+ !g_strcmp0( key_str, "image_data" ) ||
+ !g_strcmp0( key_str, "icon_data" ) )
+ {
+ gint32 width, height, row_stride, bits_per_sample, channels;
+ gboolean has_alpha;
+ g_variant_get(
+ value,
+ "(iiibiiay)",
+ &width,
+ &height,
+ &row_stride,
+ &has_alpha,
+ &bits_per_sample,
+ &channels,
+ NULL );
+
+ g_autoptr( GVariant ) data_variant = g_variant_get_child_value( value, 6 );
+ gsize data_len;
+ guint8 const* data = g_variant_get_fixed_array( data_variant, &data_len, sizeof( guint8 ) );
+ g_autoptr( GBytes ) bytes = g_bytes_new( data, data_len );
+
+ g_autoptr( GdkPixbuf ) pixbuf = gdk_pixbuf_new_from_bytes(
+ bytes,
+ GDK_COLORSPACE_RGB,
+ has_alpha,
+ bits_per_sample,
+ width,
+ height,
+ row_stride );
+ foobar_notification_set_image( notification, pixbuf );
+ }
+ }
+ }
+ }
+
+ if ( actions )
+ {
+ // Actions are stored in a flat array containing the label followed by the ID of the action.
+
+ while ( *actions )
+ {
+ gchar const* label = *actions++;
+ gchar const* id = *actions++;
+ if ( !id )
+ {
+ g_warning( "Action missing id: %s", label );
+ break;
+ }
+
+ g_autoptr( FoobarNotificationAction ) action = foobar_notification_action_new( notification );
+ foobar_notification_action_set_id( action, id );
+ foobar_notification_action_set_label( action, label );
+ foobar_notification_add_action( notification, action );
+ }
+ }
+
+ // Add the notification to the list and start the timeout.
+
+ g_list_store_append( self->notifications, notification );
+ foobar_notification_resume_timeout( notification );
+ foobar_notification_service_write_cache( self );
+
+ foobar_notifications_complete_notify( iface, invocation, foobar_notification_get_id( notification ) );
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+//
+// DBus skeleton callback for the "CloseNotification" method.
+//
+gboolean foobar_notification_service_handle_close_notification(
+ FoobarNotifications* iface,
+ GDBusMethodInvocation* invocation,
+ guint id,
+ gpointer userdata )
+{
+ FoobarNotificationService* self = (FoobarNotificationService*)userdata;
+
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->notifications ) ); ++i )
+ {
+ FoobarNotification* notification = g_list_model_get_item( G_LIST_MODEL( self->notifications ), i );
+ if ( foobar_notification_get_id( notification ) == id )
+ {
+ foobar_notification_close( notification );
+ break;
+ }
+ }
+
+ foobar_notifications_complete_close_notification( iface, invocation );
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+//
+// DBus skeleton callback for the "GetCapabilities" method.
+//
+gboolean foobar_notification_service_handle_get_capabilities(
+ FoobarNotifications* iface,
+ GDBusMethodInvocation* invocation,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ gchar const* capabilities[] =
+ {
+ "action-icons",
+ "actions",
+ "body",
+ "body-hyperlinks",
+ "body-markup",
+ "icon-static",
+ "persistence",
+ NULL
+ };
+ foobar_notifications_complete_get_capabilities( iface, invocation, capabilities );
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+//
+// DBus skeleton callback for the "GetServerInformation" method.
+//
+gboolean foobar_notification_service_handle_get_server_information(
+ FoobarNotifications* iface,
+ GDBusMethodInvocation* invocation,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ foobar_notifications_complete_get_server_information(
+ iface,
+ invocation,
+ "Foobar",
+ "Hannes Schulze",
+ "1.0.0",
+ "1.2" );
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Synchronously read the cache file at self->cache_path, populating self->notifications and initializing self->next_id.
+//
+void foobar_notification_service_read_cache( FoobarNotificationService* self )
+{
+ g_autoptr( GError ) error = NULL;
+
+ if ( self->cache_path && g_file_test( self->cache_path, G_FILE_TEST_EXISTS ) )
+ {
+ g_autoptr( JsonParser ) parser = json_parser_new( );
+ if ( !json_parser_load_from_mapped_file( parser, self->cache_path, &error ) )
+ {
+ g_warning( "Unable to read cached notifications: %s", error->message );
+ return;
+ }
+
+ JsonNode* root_node = json_parser_get_root( parser );
+ JsonArray* root_array = json_node_get_array( root_node );
+ for ( guint i = 0; i < json_array_get_length( root_array ); ++i )
+ {
+ JsonObject* notification_object = json_array_get_object_element( root_array, i );
+ g_autoptr( FoobarNotification ) notification = foobar_notification_new( self );
+
+ foobar_notification_set_dismissed( notification, TRUE );
+
+ guint id = json_object_get_int_member( notification_object, "id" );
+ foobar_notification_set_id( notification, id );
+
+ gchar const* app_entry = json_object_get_string_member_with_default( notification_object, "app-entry", NULL );
+ foobar_notification_set_app_entry( notification, app_entry );
+
+ gchar const* app_name = json_object_get_string_member_with_default( notification_object, "app-name", NULL );
+ foobar_notification_set_app_name( notification, app_name );
+
+ gchar const* body = json_object_get_string_member_with_default( notification_object, "body", NULL );
+ foobar_notification_set_body( notification, body );
+
+ gchar const* summary = json_object_get_string_member_with_default( notification_object, "summary", NULL );
+ foobar_notification_set_summary( notification, summary );
+
+ gchar const* image_path = json_object_get_string_member_with_default( notification_object, "image-path", NULL );
+ gchar const* image_data = json_object_get_string_member_with_default( notification_object, "image-data", NULL );
+ if ( image_path ) { foobar_notification_set_image_from_path( notification, image_path ); }
+ else if ( image_data ) { foobar_notification_set_image_from_data( notification, image_data ); }
+
+ gboolean is_resident = json_object_get_boolean_member( notification_object, "is-resident" );
+ foobar_notification_set_resident( notification, is_resident );
+
+ gchar const* time_str = json_object_get_string_member_with_default( notification_object, "time", NULL );
+ GTimeZone* tz = g_time_zone_new_local( );
+ GDateTime* time = time_str ? g_date_time_new_from_iso8601( time_str, tz ) : NULL;
+ foobar_notification_set_time( notification, time );
+ g_time_zone_unref( tz );
+ if ( time ) { g_date_time_unref( time ); }
+
+ gint64 timeout = json_object_get_int_member( notification_object, "timeout" );
+ foobar_notification_set_timeout( notification, timeout );
+
+ FoobarNotificationUrgency urgency = json_object_get_int_member( notification_object, "urgency" );
+ foobar_notification_set_urgency( notification, urgency );
+
+ JsonArray* actions_array = json_object_get_array_member( notification_object, "actions" );
+ for ( guint j = 0; j < json_array_get_length( actions_array ); ++j )
+ {
+ JsonObject* action_object = json_array_get_object_element( actions_array, j );
+ g_autoptr( FoobarNotificationAction ) action = foobar_notification_action_new( notification );
+
+ gchar const* action_id = json_object_get_string_member_with_default( action_object, "id", NULL );
+ foobar_notification_action_set_id( action, action_id );
+
+ gchar const* label = json_object_get_string_member_with_default( action_object, "label", NULL );
+ foobar_notification_action_set_label( action, label );
+
+ foobar_notification_add_action( notification, action );
+ }
+
+ g_list_store_append( self->notifications, notification );
+ self->next_id = MAX( self->next_id, id + 1 );
+ }
+ }
+}
+
+//
+// Asynchronously start writing the cache file at self->cache_path.
+//
+void foobar_notification_service_write_cache( FoobarNotificationService* self )
+{
+ guint count = g_list_model_get_n_items( G_LIST_MODEL( self->notifications ) );
+ g_autoptr( GPtrArray ) notifications = g_ptr_array_new_full( count, g_object_unref );
+ for ( guint i = 0; i < count; ++i )
+ {
+ FoobarNotification* notification = g_list_model_get_item( G_LIST_MODEL( self->notifications ), i );
+ if ( !foobar_notification_is_transient( notification ) )
+ {
+ g_ptr_array_add( notifications, g_object_ref( notification ) );
+ }
+ }
+
+ foobar_notification_service_write_cache_async(
+ self,
+ g_steal_pointer( &notifications ),
+ NULL,
+ foobar_notification_service_write_cache_cb,
+ NULL );
+}
+
+//
+// Callback invoked when the cache was written successfully or failed.
+//
+void foobar_notification_service_write_cache_cb(
+ GObject* object,
+ GAsyncResult* result,
+ gpointer userdata )
+{
+ (void)userdata;
+ FoobarNotificationService* self = (FoobarNotificationService*)object;
+
+ g_autoptr( GError ) error = NULL;
+ if ( !foobar_notification_service_write_cache_finish( self, result, &error ) )
+ {
+ g_warning( "Unable to write cached notifications: %s", error->message );
+ }
+}
+
+//
+// Asynchronously write the cache file at self->cache_path, using the provided snapshot of the list.
+//
+void foobar_notification_service_write_cache_async(
+ FoobarNotificationService* self,
+ GPtrArray* notifications,
+ GCancellable* cancellable,
+ GAsyncReadyCallback callback,
+ gpointer userdata )
+{
+ g_autoptr( GTask ) task = g_task_new( self, cancellable, callback, userdata );
+ g_task_set_name( task, "write-notification-cache" );
+ g_task_set_task_data( task, notifications, (GDestroyNotify)g_ptr_array_unref );
+ g_task_run_in_thread( task, foobar_notification_service_write_cache_thread );
+}
+
+//
+// Get the asynchronous result for writing the cache file, returning TRUE on success, or FALSE on error.
+//
+gboolean foobar_notification_service_write_cache_finish(
+ FoobarNotificationService* self,
+ GAsyncResult* result,
+ GError** error )
+{
+ (void)self;
+
+ return g_task_propagate_boolean( G_TASK( result ), error );
+}
+
+//
+// Task implementation for foobar_notification_service_write_cache_async, invoked on a background thread.
+//
+void foobar_notification_service_write_cache_thread(
+ GTask* task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable* cancellable )
+{
+ (void)cancellable;
+ FoobarNotificationService* self = (FoobarNotificationService*)source_object;
+ GPtrArray* notifications = (GPtrArray*)task_data;
+
+ if ( !self->cache_path )
+ {
+ g_task_return_boolean( task, TRUE );
+ return;
+ }
+
+ g_autoptr( GError ) error = NULL;
+
+ g_autoptr( JsonBuilder ) builder = json_builder_new( );
+ json_builder_begin_array( builder );
+ for ( guint i = 0; i < notifications->len; ++i )
+ {
+ // Only init-only properties are read here so no extra synchronization is needed.
+
+ FoobarNotification* notification = g_ptr_array_index( notifications, i );
+
+ json_builder_begin_object( builder );
+
+ json_builder_set_member_name( builder, "id" );
+ json_builder_add_int_value( builder, foobar_notification_get_id( notification ) );
+
+ json_builder_set_member_name( builder, "app-entry" );
+ json_builder_add_string_value( builder, foobar_notification_get_app_entry( notification ) );
+
+ json_builder_set_member_name( builder, "app-name" );
+ json_builder_add_string_value( builder, foobar_notification_get_app_name( notification ) );
+
+ json_builder_set_member_name( builder, "body" );
+ json_builder_add_string_value( builder, foobar_notification_get_body( notification ) );
+
+ json_builder_set_member_name( builder, "summary" );
+ json_builder_add_string_value( builder, foobar_notification_get_summary( notification ) );
+
+ json_builder_set_member_name( builder, "image-path" );
+ json_builder_add_string_value( builder, foobar_notification_get_image_path( notification ) );
+
+ json_builder_set_member_name( builder, "image-data" );
+ json_builder_add_string_value( builder, foobar_notification_get_image_data( notification ) );
+
+ json_builder_set_member_name( builder, "is-resident" );
+ json_builder_add_boolean_value( builder, foobar_notification_is_resident( notification ) );
+
+ GDateTime* time = foobar_notification_get_time( notification );
+ gchar* time_str = time ? g_date_time_format_iso8601( time ) : NULL;
+ json_builder_set_member_name( builder, "time" );
+ json_builder_add_string_value( builder, time_str );
+ g_free( time_str );
+
+ json_builder_set_member_name( builder, "timeout" );
+ json_builder_add_int_value( builder, foobar_notification_get_timeout( notification ) );
+
+ json_builder_set_member_name( builder, "urgency" );
+ json_builder_add_int_value( builder, foobar_notification_get_urgency( notification ) );
+
+ guint action_count;
+ FoobarNotificationAction** actions = foobar_notification_get_actions( notification, &action_count );
+ json_builder_set_member_name( builder, "actions" );
+ json_builder_begin_array( builder );
+ for ( guint j = 0; j < action_count; ++j )
+ {
+ FoobarNotificationAction* action = actions[j];
+
+ json_builder_begin_object( builder );
+
+ json_builder_set_member_name( builder, "id" );
+ json_builder_add_string_value( builder, foobar_notification_action_get_id( action ) );
+
+ json_builder_set_member_name( builder, "label" );
+ json_builder_add_string_value( builder, foobar_notification_action_get_label( action ) );
+
+ json_builder_end_object( builder );
+ }
+ json_builder_end_array( builder );
+
+ json_builder_end_object( builder );
+ }
+ json_builder_end_array( builder );
+
+ g_autoptr( JsonNode ) root_node = json_builder_get_root( builder );
+ g_autoptr( JsonGenerator ) generator = json_generator_new( );
+ json_generator_set_root( generator, root_node );
+
+ g_mutex_lock( &self->write_cache_mutex );
+ gboolean success = json_generator_to_file( generator, self->cache_path, &error );
+ g_mutex_unlock( &self->write_cache_mutex );
+ if ( !success )
+ {
+ g_task_return_error( task, g_steal_pointer( &error ) );
+ return;
+ }
+
+ g_task_return_boolean( task, TRUE );
+}
+
+//
+// Filtering callback for the list of pop-up notifications.
+//
+gboolean foobar_notification_service_popup_filter_func(
+ gpointer item,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarNotification* notification = item;
+ return !foobar_notification_is_dismissed( notification );
+}
+
+//
+// Sorting callback for the list of notifications.
+//
+// Notifications are sorted by timestamp in descending order.
+//
+gint foobar_notification_service_sort_func(
+ gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarNotification* notification_a = (FoobarNotification*)item_a;
+ FoobarNotification* notification_b = (FoobarNotification*)item_b;
+ return -g_date_time_compare(
+ foobar_notification_get_time( notification_a ),
+ foobar_notification_get_time( notification_b ) );
+} \ No newline at end of file
diff --git a/src/services/notification-service.h b/src/services/notification-service.h
new file mode 100644
index 0000000..29b2ca7
--- /dev/null
+++ b/src/services/notification-service.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include <glib-object.h>
+#include <gdk/gdk.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_NOTIFICATION_URGENCY foobar_notification_urgency_get_type( )
+#define FOOBAR_TYPE_NOTIFICATION_ACTION foobar_notification_action_get_type( )
+#define FOOBAR_TYPE_NOTIFICATION foobar_notification_get_type( )
+#define FOOBAR_TYPE_NOTIFICATION_SERVICE foobar_notification_service_get_type( )
+
+typedef enum
+{
+ // reminder to myself that i should have studied this shit instead of writing a fucking bar in c
+ FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_COMFORT = 0,
+ FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY = 1,
+ FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_LIFE = 2,
+} FoobarNotificationUrgency;
+
+GType foobar_notification_urgency_get_type( void );
+
+G_DECLARE_FINAL_TYPE( FoobarNotificationAction, foobar_notification_action, FOOBAR, NOTIFICATION_ACTION, GObject )
+
+gchar const* foobar_notification_action_get_id ( FoobarNotificationAction* self );
+gchar const* foobar_notification_action_get_label( FoobarNotificationAction* self );
+void foobar_notification_action_invoke ( FoobarNotificationAction* self );
+
+G_DECLARE_FINAL_TYPE( FoobarNotification, foobar_notification, FOOBAR, NOTIFICATION, GObject )
+
+guint foobar_notification_get_id ( FoobarNotification* self );
+FoobarNotificationAction** foobar_notification_get_actions ( FoobarNotification* self,
+ guint* out_count );
+gchar const* foobar_notification_get_app_entry ( FoobarNotification* self );
+gchar const* foobar_notification_get_app_name ( FoobarNotification* self );
+gchar const* foobar_notification_get_body ( FoobarNotification* self );
+gchar const* foobar_notification_get_summary ( FoobarNotification* self );
+GdkPixbuf* foobar_notification_get_image ( FoobarNotification* self );
+gboolean foobar_notification_is_dismissed ( FoobarNotification* self );
+gboolean foobar_notification_is_resident ( FoobarNotification* self );
+gboolean foobar_notification_is_transient ( FoobarNotification* self );
+GDateTime* foobar_notification_get_time ( FoobarNotification* self );
+gint64 foobar_notification_get_timeout ( FoobarNotification* self );
+FoobarNotificationUrgency foobar_notification_get_urgency ( FoobarNotification* self );
+void foobar_notification_block_timeout ( FoobarNotification* self );
+void foobar_notification_resume_timeout( FoobarNotification* self );
+void foobar_notification_dismiss ( FoobarNotification* self );
+void foobar_notification_close ( FoobarNotification* self );
+
+G_DECLARE_FINAL_TYPE( FoobarNotificationService, foobar_notification_service, FOOBAR, NOTIFICATION_SERVICE, GObject )
+
+FoobarNotificationService* foobar_notification_service_new ( void );
+GListModel* foobar_notification_service_get_notifications ( FoobarNotificationService* self );
+GListModel* foobar_notification_service_get_popup_notifications( FoobarNotificationService* self );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/services/workspace-service.c b/src/services/workspace-service.c
new file mode 100644
index 0000000..c55896d
--- /dev/null
+++ b/src/services/workspace-service.c
@@ -0,0 +1,1355 @@
+#include "services/workspace-service.h"
+#include <json-glib/json-glib.h>
+#include <gtk/gtk.h>
+#include <string.h>
+
+typedef struct _EventData EventData;
+typedef struct _EventHandler EventHandler;
+
+//
+// FoobarWorkspaceFlags:
+//
+// Some information about the type/state of a workspace.
+//
+
+G_DEFINE_FLAGS_TYPE(
+ FoobarWorkspaceFlags,
+ foobar_workspace_flags,
+ G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_NONE, "none" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_ACTIVE, "active" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_VISIBLE, "visible" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_SPECIAL, "special" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_PERSISTENT, "persistent" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_URGENT, "urgent" ) )
+
+//
+// FoobarWorkspace:
+//
+// Representation of a workspace.
+//
+
+struct _FoobarWorkspace
+{
+ GObject parent_instance;
+ FoobarWorkspaceService* service;
+ gint64 id;
+ gchar* name;
+ gchar* monitor;
+ FoobarWorkspaceFlags flags;
+};
+
+enum
+{
+ WORKSPACE_PROP_ID = 1,
+ WORKSPACE_PROP_NAME,
+ WORKSPACE_PROP_MONITOR,
+ WORKSPACE_PROP_FLAGS,
+ N_WORKSPACE_PROPS,
+};
+
+static GParamSpec* workspace_props[N_WORKSPACE_PROPS] = { 0 };
+
+static void foobar_workspace_class_init ( FoobarWorkspaceClass* klass );
+static void foobar_workspace_init ( FoobarWorkspace* self );
+static void foobar_workspace_get_property( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_workspace_finalize ( GObject* object );
+static FoobarWorkspace* foobar_workspace_new ( FoobarWorkspaceService* service );
+static void foobar_workspace_set_id ( FoobarWorkspace* self,
+ gint64 value );
+static void foobar_workspace_set_name ( FoobarWorkspace* self,
+ gchar const* value );
+static void foobar_workspace_set_monitor ( FoobarWorkspace* self,
+ gchar const* value );
+static void foobar_workspace_set_flags ( FoobarWorkspace* self,
+ FoobarWorkspaceFlags value );
+
+G_DEFINE_FINAL_TYPE( FoobarWorkspace, foobar_workspace, G_TYPE_OBJECT )
+
+//
+// FoobarWorkspaceService:
+//
+// Service monitoring the workspaces provided by the window manager. This is implemented by communicating directly with
+// Hyprland through its IPC socket.
+//
+// Based on the waybar implementation:
+// https://github.com/Alexays/Waybar/blob/master/src/modules/hyprland/workspaces.cpp
+//
+
+struct _FoobarWorkspaceService
+{
+ GObject parent_instance;
+ GListStore* workspaces;
+ GtkSortListModel* sorted_workspaces;
+ GCancellable* event_cancellable;
+ GThread* event_thread;
+ gchar* rx_path;
+ gchar* tx_path;
+};
+
+enum
+{
+ PROP_WORKSPACES = 1,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+enum
+{
+ SIGNAL_MONITOR_CONFIGURATION_CHANGED,
+ N_SIGNALS,
+};
+static unsigned signals[N_SIGNALS] = { 0 };
+
+static void foobar_workspace_service_class_init ( FoobarWorkspaceServiceClass* klass );
+static void foobar_workspace_service_init ( FoobarWorkspaceService* self );
+static void foobar_workspace_service_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_workspace_service_dispose ( GObject* object );
+static void foobar_workspace_service_finalize ( GObject* object );
+static gboolean foobar_workspace_service_handle_workspace_created ( EventData* data );
+static gboolean foobar_workspace_service_handle_workspace_destroyed ( EventData* data );
+static gboolean foobar_workspace_service_handle_workspace_moved ( EventData* data );
+static gboolean foobar_workspace_service_handle_workspace_renamed ( EventData* data );
+static gboolean foobar_workspace_service_handle_workspace_activated ( EventData* data );
+static gboolean foobar_workspace_service_handle_special_workspace_activated( EventData* data );
+static gboolean foobar_workspace_service_handle_monitor_focused ( EventData* data );
+static gboolean foobar_workspace_service_handle_window_urgent ( EventData* data );
+static gboolean foobar_workspace_service_handle_config_reloaded ( EventData* data );
+static gpointer foobar_workspace_service_event_thread_func ( gpointer userdata );
+static void foobar_workspace_service_dispatch_event ( FoobarWorkspaceService* self,
+ gchar* message );
+static gchar* foobar_workspace_service_send_request ( FoobarWorkspaceService* self,
+ gchar const* request,
+ GError** error );
+static JsonNode* foobar_workspace_service_send_json_request ( FoobarWorkspaceService* self,
+ gchar const* request,
+ GError** error );
+static void foobar_workspace_service_initialize ( FoobarWorkspaceService* self );
+static void foobar_workspace_service_update_active ( FoobarWorkspaceService* self,
+ GListStore* workspaces );
+static void foobar_workspace_service_add_workspace ( FoobarWorkspaceService* self,
+ GListStore* workspaces,
+ JsonObject* workspace_object,
+ gboolean is_persistent );
+static FoobarWorkspace* foobar_workspace_service_find_workspace ( GListModel* workspaces,
+ gint64 id,
+ guint* out_index );
+static gboolean foobar_workspace_service_is_invalid_name ( gchar const* name );
+static gint64 foobar_workspace_service_parse_id ( gchar const* str );
+static gint foobar_workspace_service_compare_ids ( gconstpointer a,
+ gconstpointer b );
+static gint foobar_workspace_service_sort_func ( gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarWorkspaceService, foobar_workspace_service, G_TYPE_OBJECT )
+
+//
+// EventData:
+//
+// Custom userdata passed to hyprland event handlers in a single pointer.
+//
+
+struct _EventData
+{
+ FoobarWorkspaceService* service;
+ gchar* name;
+ gchar* payload;
+};
+
+static EventData* event_data_new ( FoobarWorkspaceService* service,
+ gchar const* name,
+ gchar const* payload );
+static void event_data_free( EventData* self );
+
+//
+// EventHandler:
+//
+// Description for the handler of a single hyprland event.
+//
+
+struct _EventHandler
+{
+ gchar const* name;
+ gboolean ( *fn )( EventData* data );
+};
+
+//
+// Static list of registered event handlers.
+//
+static EventHandler const event_handlers[] =
+ {
+ { .name = "createworkspacev2", .fn = foobar_workspace_service_handle_workspace_created },
+ { .name = "destroyworkspacev2", .fn = foobar_workspace_service_handle_workspace_destroyed },
+ { .name = "moveworkspacev2", .fn = foobar_workspace_service_handle_workspace_moved },
+ { .name = "renameworkspace", .fn = foobar_workspace_service_handle_workspace_renamed },
+ { .name = "workspace", .fn = foobar_workspace_service_handle_workspace_activated },
+ { .name = "activespecial", .fn = foobar_workspace_service_handle_special_workspace_activated },
+ { .name = "focusedmon", .fn = foobar_workspace_service_handle_monitor_focused },
+ { .name = "urgent", .fn = foobar_workspace_service_handle_window_urgent },
+ { .name = "configreloaded", .fn = foobar_workspace_service_handle_config_reloaded },
+ };
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Workspace
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for workspaces.
+//
+void foobar_workspace_class_init( FoobarWorkspaceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_workspace_get_property;
+ object_klass->finalize = foobar_workspace_finalize;
+
+ workspace_props[WORKSPACE_PROP_ID] = g_param_spec_int64(
+ "id",
+ "ID",
+ "Numeric ID of the workspace.",
+ INT64_MIN,
+ INT64_MAX,
+ 0,
+ G_PARAM_READABLE );
+ workspace_props[WORKSPACE_PROP_NAME] = g_param_spec_string(
+ "name",
+ "Name",
+ "Optional human-readable workspace name.",
+ NULL,
+ G_PARAM_READABLE );
+ workspace_props[WORKSPACE_PROP_MONITOR] = g_param_spec_string(
+ "monitor",
+ "Monitor",
+ "Connector ID of the workspace's monitor.",
+ NULL,
+ G_PARAM_READABLE );
+ workspace_props[WORKSPACE_PROP_FLAGS] = g_param_spec_flags(
+ "flags",
+ "Flags",
+ "Flags indicating the current state of the workspace.",
+ FOOBAR_TYPE_WORKSPACE_FLAGS,
+ FOOBAR_WORKSPACE_FLAGS_NONE,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_WORKSPACE_PROPS, workspace_props );
+}
+
+//
+// Instance initialization for workspaces.
+//
+void foobar_workspace_init( FoobarWorkspace* self )
+{
+ (void)self;
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_workspace_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarWorkspace* self = (FoobarWorkspace*)object;
+
+ switch ( prop_id )
+ {
+ case WORKSPACE_PROP_ID:
+ g_value_set_int64( value, foobar_workspace_get_id( self ) );
+ break;
+ case WORKSPACE_PROP_NAME:
+ g_value_set_string( value, foobar_workspace_get_name( self ) );
+ break;
+ case WORKSPACE_PROP_MONITOR:
+ g_value_set_string( value, foobar_workspace_get_monitor( self ) );
+ break;
+ case WORKSPACE_PROP_FLAGS:
+ g_value_set_flags( value, foobar_workspace_get_flags( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for workspaces.
+//
+void foobar_workspace_finalize( GObject* object )
+{
+ FoobarWorkspace* self = (FoobarWorkspace*)object;
+
+ g_clear_pointer( &self->name, g_free );
+ g_clear_pointer( &self->monitor, g_free );
+
+ G_OBJECT_CLASS( foobar_workspace_parent_class )->finalize( object );
+}
+
+//
+// Create a new workspace that is owned by the given service (captured as an unowned reference).
+//
+FoobarWorkspace* foobar_workspace_new( FoobarWorkspaceService* service )
+{
+ FoobarWorkspace* self = g_object_new( FOOBAR_TYPE_WORKSPACE, NULL );
+ self->service = service;
+ return self;
+}
+
+//
+// Numeric ID of the workspace.
+//
+gint64 foobar_workspace_get_id( FoobarWorkspace* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), 0 );
+ return self->id;
+}
+
+//
+// Optional human-readable workspace name.
+//
+gchar const* foobar_workspace_get_name( FoobarWorkspace* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), NULL );
+ return self->name;
+}
+
+//
+// Connector ID of the workspace's monitor.
+//
+gchar const* foobar_workspace_get_monitor( FoobarWorkspace* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), NULL );
+ return self->monitor;
+}
+
+//
+// Flags indicating the current state of the workspace.
+//
+FoobarWorkspaceFlags foobar_workspace_get_flags( FoobarWorkspace* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), FOOBAR_WORKSPACE_FLAGS_NONE );
+ return self->flags;
+}
+
+//
+// Numeric ID of the workspace.
+//
+void foobar_workspace_set_id(
+ FoobarWorkspace* self,
+ gint64 value )
+{
+ g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) );
+
+ if ( self->id != value )
+ {
+ self->id = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_ID] );
+ }
+}
+
+//
+// Optional human-readable workspace name.
+//
+void foobar_workspace_set_name(
+ FoobarWorkspace* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) );
+
+ if ( g_strcmp0( self->name, value ) )
+ {
+ g_clear_pointer( &self->name, g_free );
+ self->name = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_NAME] );
+ }
+}
+
+//
+// Connector ID of the workspace's monitor.
+//
+void foobar_workspace_set_monitor(
+ FoobarWorkspace* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) );
+
+ if ( g_strcmp0( self->monitor, value ) )
+ {
+ g_clear_pointer( &self->monitor, g_free );
+ self->monitor = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_MONITOR] );
+ }
+}
+
+//
+// Flags indicating the current state of the workspace.
+//
+void foobar_workspace_set_flags(
+ FoobarWorkspace* self,
+ FoobarWorkspaceFlags value )
+{
+ g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) );
+
+ if ( self->flags != value )
+ {
+ self->flags = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_FLAGS] );
+ }
+}
+
+//
+// Let this workspace become the active one.
+//
+void foobar_workspace_activate( FoobarWorkspace* self )
+{
+ g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) );
+ if ( !self->service ) { return; }
+
+ g_autofree gchar* request = NULL;
+ if ( foobar_workspace_get_id( self ) > 0 )
+ {
+ // Normal
+ request = g_strdup_printf( "dispatch workspace %lld", (long long)foobar_workspace_get_id( self ) );
+ }
+ else if ( !( foobar_workspace_get_flags( self ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) )
+ {
+ // Named (this includes persistent)
+ request = g_strdup_printf( "dispatch workspace name:%s", foobar_workspace_get_name( self ) );
+ }
+ else if ( foobar_workspace_get_id( self ) != -99 )
+ {
+ // Named special
+ request = g_strdup_printf( "dispatch togglespecialworkspace %s", foobar_workspace_get_name( self ) );
+ }
+ else
+ {
+ // Special
+ request = g_strdup( "dispatch togglespecialworkspace" );
+ }
+
+ g_autoptr( GError ) error = NULL;
+ g_free( foobar_workspace_service_send_request( self->service, request, &error ) );
+ if ( error )
+ {
+ g_warning( "Unable to activate workspace: %s", error->message );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Service Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the workspace service.
+//
+void foobar_workspace_service_class_init( FoobarWorkspaceServiceClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_workspace_service_get_property;
+ object_klass->dispose = foobar_workspace_service_dispose;
+ object_klass->finalize = foobar_workspace_service_finalize;
+
+ props[PROP_WORKSPACES] = g_param_spec_object(
+ "workspaces",
+ "Workspaces",
+ "Sorted list of all workspaces.",
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READABLE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+
+ signals[SIGNAL_MONITOR_CONFIGURATION_CHANGED] = g_signal_new(
+ "monitor-configuration-changed",
+ FOOBAR_TYPE_WORKSPACE_SERVICE,
+ G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0 );
+}
+
+//
+// Instance initialization for the workspace service.
+//
+void foobar_workspace_service_init( FoobarWorkspaceService* self )
+{
+ self->workspaces = g_list_store_new( FOOBAR_TYPE_WORKSPACE );
+
+ GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_workspace_service_sort_func, NULL, NULL );
+ self->sorted_workspaces = gtk_sort_list_model_new(
+ G_LIST_MODEL( g_object_ref( self->workspaces ) ),
+ GTK_SORTER( sorter ) );
+
+ gchar const* instance_signature = g_getenv( "HYPRLAND_INSTANCE_SIGNATURE" );
+ if ( !instance_signature )
+ {
+ g_warning( "Unable to connect to hyprland -- is it currently running?" );
+ return;
+ }
+
+ self->tx_path = g_strdup_printf( "/tmp/hypr/%s/.socket.sock", instance_signature );
+ self->rx_path = g_strdup_printf( "/tmp/hypr/%s/.socket2.sock", instance_signature );
+ self->event_cancellable = g_cancellable_new( );
+ self->event_thread = g_thread_new( "workspace-event-listener", foobar_workspace_service_event_thread_func, self );
+
+ foobar_workspace_service_initialize( self );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_workspace_service_get_property( GObject* object, guint prop_id, GValue* value, GParamSpec* pspec )
+{
+ FoobarWorkspaceService* self = (FoobarWorkspaceService*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_WORKSPACES:
+ g_value_set_object( value, foobar_workspace_service_get_workspaces( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for the workspace service.
+//
+// This is where the event thread is joined because it might still create new object references which is a bad thing to
+// do in finalize.
+//
+void foobar_workspace_service_dispose( GObject* object )
+{
+ FoobarWorkspaceService* self = (FoobarWorkspaceService*)object;
+
+ if ( self->event_thread )
+ {
+ g_cancellable_cancel( self->event_cancellable );
+ g_thread_join( self->event_thread );
+ self->event_thread = NULL;
+ }
+
+ G_OBJECT_CLASS( foobar_workspace_service_parent_class )->dispose( object );
+}
+
+//
+// Instance cleanup for the workspace service.
+//
+void foobar_workspace_service_finalize( GObject* object )
+{
+ FoobarWorkspaceService* self = (FoobarWorkspaceService*)object;
+
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->workspaces ) ); ++i )
+ {
+ FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( self->workspaces ), i );
+ workspace->service = NULL;
+ }
+ g_clear_object( &self->event_cancellable );
+ g_clear_object( &self->sorted_workspaces );
+ g_clear_object( &self->workspaces );
+ g_clear_pointer( &self->rx_path, g_free );
+ g_clear_pointer( &self->tx_path, g_free );
+
+ G_OBJECT_CLASS( foobar_workspace_service_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new workspace service instance.
+//
+FoobarWorkspaceService* foobar_workspace_service_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_WORKSPACE_SERVICE, NULL );
+}
+
+//
+// Get a sorted list of all workspaces.
+//
+GListModel* foobar_workspace_service_get_workspaces( FoobarWorkspaceService* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_WORKSPACE_SERVICE( self ), NULL );
+ return G_LIST_MODEL( self->sorted_workspaces );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Event Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Handler for the "createworkspacev2" event.
+//
+// The payload has the format "<id>,<name>" where <id> is the numeric ID.
+//
+gboolean foobar_workspace_service_handle_workspace_created( EventData* data )
+{
+ gchar* save;
+ gchar const* id_str = strtok_r( data->payload, ",", &save );
+ gchar const* name = strtok_r( NULL, ",", &save );
+ g_return_val_if_fail( id_str && name, G_SOURCE_REMOVE );
+
+ if ( foobar_workspace_service_is_invalid_name( name ) ) { return G_SOURCE_REMOVE; }
+ gint64 id = foobar_workspace_service_parse_id( id_str );
+
+ // Get additional information about the workspace.
+
+ g_autoptr( GError ) error = NULL;
+ g_autoptr( JsonNode ) workspaces_node = foobar_workspace_service_send_json_request(
+ data->service,
+ "j/workspaces",
+ &error );
+ if ( !workspaces_node )
+ {
+ g_warning( "Unable to load workspaces: %s", error->message );
+ return G_SOURCE_REMOVE;
+ }
+
+ JsonArray* workspaces_array = json_node_get_array( workspaces_node );
+ JsonObject* workspace_object = NULL;
+ for ( guint i = 0; i < json_array_get_length( workspaces_array ); ++i )
+ {
+ JsonObject* it = json_array_get_object_element( workspaces_array, i );
+ if ( id == json_object_get_int_member( it, "id" ) )
+ {
+ workspace_object = it;
+ break;
+ }
+ }
+
+ if ( !workspace_object )
+ {
+ g_warning( "Unable to find new workspace." );
+ return G_SOURCE_REMOVE;
+ }
+
+ // Check if the workspace is persistent by looking for a rule with its ID.
+
+ gboolean is_persistent = FALSE;
+
+ g_autoptr( JsonNode ) rules_node = foobar_workspace_service_send_json_request(
+ data->service,
+ "j/workspacerules",
+ &error );
+ if ( !rules_node )
+ {
+ g_warning( "Unable to load workspace rules: %s", error->message );
+ return G_SOURCE_REMOVE;
+ }
+
+ JsonArray* rules_array = json_node_get_array( rules_node );
+ for ( guint i = 0; i < json_array_get_length( rules_array ); ++i )
+ {
+ JsonObject* rule_object = json_array_get_object_element( rules_array, i );
+ gchar const* rule_id_str = json_object_get_string_member_with_default( rule_object, "workspaceString", NULL );
+ if ( id == foobar_workspace_service_parse_id( rule_id_str ) )
+ {
+ is_persistent = json_object_get_boolean_member_with_default( rule_object, "persistent", FALSE );
+ break;
+ }
+ }
+
+ foobar_workspace_service_add_workspace( data->service, data->service->workspaces, workspace_object, is_persistent );
+ foobar_workspace_service_update_active( data->service, data->service->workspaces );
+ return G_SOURCE_REMOVE;
+}
+
+//
+// Handler for the "destroyworkspacev2" event.
+//
+// The payload has the format "<id>,<name>" where <id> is the numeric ID.
+//
+gboolean foobar_workspace_service_handle_workspace_destroyed( EventData* data )
+{
+ gchar* save;
+ gchar const* id_str = strtok_r( data->payload, ",", &save );
+ gchar const* name = strtok_r( NULL, ",", &save );
+ g_return_val_if_fail( id_str && name, G_SOURCE_REMOVE );
+
+ if ( foobar_workspace_service_is_invalid_name( name ) ) { return G_SOURCE_REMOVE; }
+ gint64 id = foobar_workspace_service_parse_id( id_str );
+
+ GListStore* workspaces = data->service->workspaces;
+ guint index;
+ FoobarWorkspace* workspace = foobar_workspace_service_find_workspace( G_LIST_MODEL( workspaces ), id, &index );
+ if ( workspace )
+ {
+ workspace->service = NULL;
+ g_list_store_remove( workspaces, index );
+ }
+
+ return G_SOURCE_REMOVE;
+}
+
+//
+// Handler for the "moveworkspacev2" event.
+//
+// The payload has the format "<id>,<name>,<monitor>" where <id> is the numeric ID of the workspace.
+//
+gboolean foobar_workspace_service_handle_workspace_moved( EventData* data )
+{
+ gchar* save;
+ gchar const* workspace_id_str = strtok_r( data->payload, ",", &save );
+ gchar const* workspace_name = strtok_r( NULL, ",", &save );
+ gchar const* monitor_name = strtok_r( NULL, ",", &save );
+ g_return_val_if_fail( workspace_id_str && workspace_name && monitor_name, G_SOURCE_REMOVE );
+
+ gint64 workspace_id = foobar_workspace_service_parse_id( workspace_id_str );
+ FoobarWorkspace* workspace = foobar_workspace_service_find_workspace(
+ G_LIST_MODEL( data->service->workspaces ),
+ workspace_id,
+ NULL );
+ if ( workspace )
+ {
+ foobar_workspace_set_monitor( workspace, monitor_name );
+ g_signal_emit( data->service, signals[SIGNAL_MONITOR_CONFIGURATION_CHANGED], 0 );
+ }
+ foobar_workspace_service_update_active( data->service, data->service->workspaces );
+
+ return G_SOURCE_REMOVE;
+}
+
+//
+// Handler for the "renameworkspace" event.
+//
+// The payload has the format "<id>,<name>" where <id> is the numeric ID of the workspace.
+//
+gboolean foobar_workspace_service_handle_workspace_renamed( EventData* data )
+{
+ gchar* save;
+ gchar const* id_str = strtok_r( data->payload, ",", &save );
+ gchar const* new_name = strtok_r( NULL, ",", &save );
+ g_return_val_if_fail( id_str && new_name, G_SOURCE_REMOVE );
+
+ gint64 id = foobar_workspace_service_parse_id( id_str );
+ FoobarWorkspace* workspace = foobar_workspace_service_find_workspace(
+ G_LIST_MODEL( data->service->workspaces ),
+ id,
+ NULL );
+ if ( workspace )
+ {
+ foobar_workspace_set_name( workspace, new_name );
+ gtk_sorter_changed(
+ gtk_sort_list_model_get_sorter( data->service->sorted_workspaces ),
+ GTK_SORTER_CHANGE_DIFFERENT );
+ }
+
+ return G_SOURCE_REMOVE;
+}
+
+//
+// Handler for the "workspace" event.
+//
+gboolean foobar_workspace_service_handle_workspace_activated( EventData* data )
+{
+ foobar_workspace_service_update_active( data->service, data->service->workspaces );
+
+ return G_SOURCE_REMOVE;
+}
+
+//
+// Handler for the "activespecial" event.
+//
+gboolean foobar_workspace_service_handle_special_workspace_activated( EventData* data )
+{
+ foobar_workspace_service_update_active( data->service, data->service->workspaces );
+
+ return G_SOURCE_REMOVE;
+}
+
+//
+// Handler for the "focusedmon" event.
+//
+gboolean foobar_workspace_service_handle_monitor_focused( EventData* data )
+{
+ foobar_workspace_service_update_active( data->service, data->service->workspaces );
+
+ return G_SOURCE_REMOVE;
+}
+
+//
+// Handler for the "urgent" event.
+//
+// The payload has the format "<address>" where <address> is the sender window's address.
+//
+gboolean foobar_workspace_service_handle_window_urgent( EventData* data )
+{
+ gchar const* address = data->payload;
+
+ // Find the client with the given address.
+
+ g_autoptr( GError ) error = NULL;
+ g_autoptr( JsonNode ) clients_node = foobar_workspace_service_send_json_request(
+ data->service,
+ "j/clients",
+ &error );
+ if ( !clients_node )
+ {
+ g_warning( "Unable to load clients: %s", error->message );
+ return G_SOURCE_REMOVE;
+ }
+
+ JsonArray* clients_array = json_node_get_array( clients_node );
+ JsonObject* client_object = NULL;
+ for ( guint i = 0; i < json_array_get_length( clients_array ); ++i )
+ {
+ JsonObject* it = json_array_get_object_element( clients_array, i );
+ gchar const* it_address = json_object_get_string_member_with_default( it, "address", "" );
+ if ( g_str_has_suffix( it_address, address ) )
+ {
+ client_object = it;
+ break;
+ }
+ }
+
+ if ( !client_object ) { return G_SOURCE_REMOVE; }
+
+ // Find the workspace this window belongs to and mark it as "urgent".
+
+ JsonObject* workspace_object = json_object_get_object_member( client_object, "workspace" );
+ gint64 id = json_object_get_int_member( workspace_object, "id" );
+ FoobarWorkspace* workspace = foobar_workspace_service_find_workspace(
+ G_LIST_MODEL( data->service->workspaces ),
+ id,
+ NULL );
+ if ( workspace )
+ {
+ FoobarWorkspaceFlags flags = foobar_workspace_get_flags( workspace ) | FOOBAR_WORKSPACE_FLAGS_URGENT;
+ foobar_workspace_set_flags( workspace, flags );
+ }
+
+ return G_SOURCE_REMOVE;
+}
+
+//
+// Handler for the "configreloaded" event.
+//
+gboolean foobar_workspace_service_handle_config_reloaded( EventData* data )
+{
+ foobar_workspace_service_initialize( data->service );
+
+ return G_SOURCE_REMOVE;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Implementation of the event thread.
+//
+// This thread listens on hyprland's event socket (socket.2.sock) for incoming event notifications and then submits
+// their registered handlers (if any) to the main loop.
+//
+gpointer foobar_workspace_service_event_thread_func( gpointer userdata )
+{
+ FoobarWorkspaceService* self = (FoobarWorkspaceService*)userdata;
+
+ g_autoptr( GError ) error = NULL;
+ g_autoptr( GSocket ) sock = g_socket_new(
+ G_SOCKET_FAMILY_UNIX,
+ G_SOCKET_TYPE_STREAM,
+ G_SOCKET_PROTOCOL_DEFAULT,
+ &error );
+ if ( !sock )
+ {
+ g_warning( "Unable to create socket for hyprland communication: %s", error->message );
+ return NULL;
+ }
+
+ g_autoptr( GSocketAddress ) addr = g_unix_socket_address_new( self->rx_path );
+ if ( !g_socket_connect( sock, addr, self->event_cancellable, &error ) &&
+ !g_cancellable_is_cancelled( self->event_cancellable ) )
+ {
+ g_warning( "Unable to connect socket to hyprland: %s", error->message );
+ return NULL;
+ }
+
+ // Each event is sent as a complete line, separated by a newline character.
+
+ GString* prev_buf = NULL; // for longer, incomplete messages
+ gchar buf[1024];
+ while ( !g_cancellable_is_cancelled( self->event_cancellable ) )
+ {
+ // Synchronously receive data from the socket, but support cancellation using Gio.
+
+ gssize received = g_socket_receive( sock, buf, sizeof( buf ), self->event_cancellable, &error );
+ if ( error && !g_cancellable_is_cancelled( self->event_cancellable ) )
+ {
+ g_warning( "Unable to receive events from hyprland: %s", error->message );
+ }
+ if ( received <= 0 ) { break; }
+
+ gchar* start = buf;
+ gchar* end = start + received;
+ while ( start < end )
+ {
+ gchar* next_newline = g_strstr_len( start, end - start, "\n" );
+ if ( !next_newline )
+ {
+ // Incomplete message (missing newline) -> store it in the buffer
+
+ prev_buf = ( prev_buf != NULL )
+ ? g_string_append_len( prev_buf, start, end - start )
+ : g_string_new_len( start, end - start );
+ break;
+ }
+
+ if ( prev_buf )
+ {
+ // Prepend buffered data before the actual data.
+
+ prev_buf = g_string_append_len( prev_buf, start, next_newline - start );
+ foobar_workspace_service_dispatch_event( self, prev_buf->str );
+
+ g_string_free( prev_buf, TRUE );
+ prev_buf = NULL;
+ }
+ else
+ {
+ // Skip allocation and use the buffer contents on the stack directly.
+
+ *next_newline = '\0';
+ foobar_workspace_service_dispatch_event( self, start );
+ }
+
+ start = next_newline + 1;
+ }
+ }
+
+ if ( prev_buf ) { g_string_free( prev_buf, TRUE ); }
+
+ return NULL;
+}
+
+//
+// Called when an entire event has been received on the event thread.
+//
+// This will look through the registered handlers and (if found) invoke the event's handler on the main thread.
+//
+void foobar_workspace_service_dispatch_event(
+ FoobarWorkspaceService* self,
+ gchar* message )
+{
+ gchar* delimiter = strstr( message, ">>" );
+ if ( !delimiter )
+ {
+ g_warning( "Received an invalid message from hyprland: %s", message );
+ return;
+ }
+
+ *delimiter = '\0';
+ gchar* name = message;
+ gchar* payload = delimiter + 2;
+ for ( gsize i = 0; i < G_N_ELEMENTS( event_handlers ); ++i )
+ {
+ EventHandler handler = event_handlers[i];
+ if ( !strcmp( name, handler.name ) )
+ {
+ g_idle_add_full(
+ G_PRIORITY_DEFAULT,
+ G_SOURCE_FUNC( handler.fn ),
+ event_data_new( self, name, payload ),
+ (GDestroyNotify)event_data_free );
+ break;
+ }
+ }
+}
+
+//
+// Synchronously send a command to hyprland's control socket (socket.sock), returning the entire response.
+//
+gchar* foobar_workspace_service_send_request(
+ FoobarWorkspaceService* self,
+ gchar const* request,
+ GError** error )
+{
+ g_autoptr( GSocket ) sock = g_socket_new(
+ G_SOCKET_FAMILY_UNIX,
+ G_SOCKET_TYPE_STREAM,
+ G_SOCKET_PROTOCOL_DEFAULT,
+ error );
+ if ( !sock ) { return NULL; }
+
+ g_autoptr( GSocketAddress ) addr = g_unix_socket_address_new( self->tx_path );
+ if ( !g_socket_connect( sock, addr, NULL, error ) ) { return NULL; }
+
+ if ( g_socket_send( sock, request, strlen( request ), NULL, error ) == -1 ) { return NULL; }
+
+ GString* str = g_string_sized_new( 0 );
+ gchar buf[8192];
+ while ( TRUE )
+ {
+ gssize received = g_socket_receive( sock, buf, sizeof( buf ), self->event_cancellable, error );
+ if ( received < 0 ) { return g_string_free( str, TRUE ); }
+
+ if ( received == 0 ) { break; }
+ str = g_string_append_len( str, buf, received );
+ }
+
+ return g_string_free( str, FALSE );
+}
+
+//
+// Synchronously send a command to hyprland's control socket (socket.sock), returning the parsed the JSON response.
+//
+JsonNode* foobar_workspace_service_send_json_request(
+ FoobarWorkspaceService* self,
+ gchar const* request,
+ GError** error )
+{
+ g_autofree gchar* json = foobar_workspace_service_send_request( self, request, error );
+ if ( !json ) { return NULL; }
+
+ g_autoptr( JsonParser ) parser = json_parser_new( );
+ if ( !json_parser_load_from_data( parser, json, (gssize)strlen( json ), error ) ) { return NULL; }
+
+ return json_parser_steal_root( parser );
+}
+
+//
+// Refresh the list of workspaces by actively sending a request to hyprland.
+//
+void foobar_workspace_service_initialize( FoobarWorkspaceService* self )
+{
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->workspaces ) ); ++i )
+ {
+ FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( self->workspaces ), i );
+ workspace->service = NULL;
+ }
+ g_list_store_remove_all( self->workspaces );
+ g_autoptr( GListStore ) workspaces = g_list_store_new( FOOBAR_TYPE_WORKSPACE );
+
+ {
+ g_autoptr( GError ) error = NULL;
+ g_autoptr( JsonNode ) workspaces_node = foobar_workspace_service_send_json_request(
+ self,
+ "j/workspaces",
+ &error );
+ if ( !workspaces_node )
+ {
+ g_warning( "Unable to load workspaces: %s", error->message );
+ return;
+ }
+
+ JsonArray* workspaces_array = json_node_get_array( workspaces_node );
+ for ( guint i = 0; i < json_array_get_length( workspaces_array ); ++i )
+ {
+ JsonObject* workspace_object = json_array_get_object_element( workspaces_array, i );
+ foobar_workspace_service_add_workspace( self, workspaces, workspace_object, FALSE );
+ }
+ }
+
+ {
+ // Load configured persistent workspaces.
+
+ g_autoptr( GError ) error = NULL;
+ g_autoptr( JsonNode ) rules_node = foobar_workspace_service_send_json_request(
+ self,
+ "j/workspacerules",
+ &error );
+ if ( rules_node )
+ {
+ JsonArray* rules_array = json_node_get_array( rules_node );
+ for ( guint i = 0; i < json_array_get_length( rules_array ); ++i )
+ {
+ JsonObject* rule_object = json_array_get_object_element( rules_array, i );
+ gchar const* name = json_object_get_string_member_with_default( rule_object, "workspaceString", NULL );
+ if ( !name )
+ {
+ g_warning( "Found an invalid workspaceString value, skipping." );
+ continue;
+ }
+
+ if ( !json_object_get_boolean_member_with_default( rule_object, "persistent", FALSE ) ) { continue; }
+
+ gint64 id = foobar_workspace_service_parse_id( name );
+ json_object_set_int_member( rule_object, "id", id );
+ json_object_set_string_member( rule_object, "name", name );
+
+ foobar_workspace_service_add_workspace( self, workspaces, rule_object, TRUE );
+ }
+ }
+ else { g_warning( "Unable to load workspace rules: %s", error->message ); }
+ }
+
+ foobar_workspace_service_update_active( self, workspaces );
+
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( workspaces ) ); ++i )
+ {
+ FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( workspaces ), i );
+ g_list_store_append( self->workspaces, workspace );
+ }
+}
+
+//
+// Update the currently active/visible workspaces by actively sending a request to hyprland.
+//
+void foobar_workspace_service_update_active(
+ FoobarWorkspaceService* self,
+ GListStore* workspaces )
+{
+ g_autoptr( GArray ) visible_workspace_ids = g_array_new( FALSE, FALSE, sizeof( gint64 ) );
+ gint64 active_workspace_id = 0;
+ gint64 active_special_workspace_id = 0;
+
+ {
+ // Get all active workspaces
+
+ g_autoptr( GError ) error = NULL;
+ g_autoptr( JsonNode ) monitors_node = foobar_workspace_service_send_json_request(
+ self,
+ "j/monitors",
+ &error );
+ if ( monitors_node )
+ {
+ JsonArray* monitors_array = json_node_get_array( monitors_node );
+ for ( guint i = 0; i < json_array_get_length( monitors_array ); ++i )
+ {
+ JsonObject* monitor_object = json_array_get_object_element( monitors_array, i );
+ gint64 workspace_id = 0;
+ gint64 special_workspace_id = 0;
+
+ if ( json_object_has_member( monitor_object, "activeWorkspace" ) )
+ {
+ JsonObject* workspace_object = json_object_get_object_member( monitor_object, "activeWorkspace" );
+ workspace_id = json_object_get_int_member_with_default( workspace_object, "id", 0 );
+ if ( workspace_id ) { g_array_append_val( visible_workspace_ids, workspace_id ); }
+ }
+
+ if ( json_object_has_member( monitor_object, "specialWorkspace" ) )
+ {
+ JsonObject* workspace_object = json_object_get_object_member( monitor_object, "specialWorkspace" );
+ special_workspace_id = json_object_get_int_member_with_default( workspace_object, "id", 0 );
+ if ( special_workspace_id ) { g_array_append_val( visible_workspace_ids, special_workspace_id ); }
+ }
+
+ // If this is the focused monitor, then the visible workspace is also the active one.
+
+ if ( json_object_get_boolean_member_with_default( monitor_object, "focused", FALSE ) )
+ {
+ active_workspace_id = workspace_id;
+ active_special_workspace_id = special_workspace_id;
+ }
+ }
+ }
+ else { g_warning( "Unable to load monitors: %s", error->message ); }
+ }
+
+ // Sort visible workspaces to allow binary search.
+
+ g_array_sort( visible_workspace_ids, foobar_workspace_service_compare_ids );
+
+ // Go through all workspaces and update their state.
+
+ for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( workspaces ) ); ++i )
+ {
+ FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( workspaces ), i );
+ FoobarWorkspaceFlags flags = foobar_workspace_get_flags( workspace );
+ gint64 id = foobar_workspace_get_id( workspace );
+
+ // Active
+ gboolean is_active = id == active_workspace_id || id == active_special_workspace_id;
+ flags = is_active ? flags | FOOBAR_WORKSPACE_FLAGS_ACTIVE : flags & ~FOOBAR_WORKSPACE_FLAGS_ACTIVE;
+
+ // Disable urgency if workspace is active
+ if ( is_active ) { flags = flags & ~FOOBAR_WORKSPACE_FLAGS_URGENT; }
+
+ // Visible
+ gboolean is_visible = g_array_binary_search(
+ visible_workspace_ids,
+ &id,
+ foobar_workspace_service_compare_ids,
+ NULL );
+ flags = is_visible ? flags | FOOBAR_WORKSPACE_FLAGS_VISIBLE : flags & ~FOOBAR_WORKSPACE_FLAGS_VISIBLE;
+
+ foobar_workspace_set_flags( workspace, flags );
+ }
+}
+
+//
+// Add or update a workspace described by a JSON object.
+//
+void foobar_workspace_service_add_workspace(
+ FoobarWorkspaceService* self,
+ GListStore* workspaces,
+ JsonObject* workspace_object,
+ gboolean is_persistent )
+{
+ gchar const* name = json_object_get_string_member( workspace_object, "name" );
+ gint64 id = json_object_get_int_member( workspace_object, "id" );
+ gboolean is_special = g_str_has_prefix( name, "special" );
+ if ( g_str_has_prefix( name, "name:" ) ) { name += strlen( "name:" ); }
+ else if ( g_str_has_prefix( name, "special:" ) ) { name += strlen( "special:" ); }
+
+ FoobarWorkspace* existing = foobar_workspace_service_find_workspace( G_LIST_MODEL( workspaces ), id, NULL );
+ if ( existing )
+ {
+ // Only update persistence.
+
+ FoobarWorkspaceFlags flags = foobar_workspace_get_flags( existing );
+ flags = is_persistent ? flags | FOOBAR_WORKSPACE_FLAGS_PERSISTENT : flags & ~FOOBAR_WORKSPACE_FLAGS_PERSISTENT;
+ foobar_workspace_set_flags( existing, flags );
+ }
+ else
+ {
+ // Found a new workspace -> create a new object for it.
+
+ FoobarWorkspaceFlags flags = FOOBAR_WORKSPACE_FLAGS_NONE;
+ flags = is_persistent ? flags | FOOBAR_WORKSPACE_FLAGS_PERSISTENT : flags;
+ flags = is_special ? flags | FOOBAR_WORKSPACE_FLAGS_SPECIAL : flags;
+
+ g_autoptr( FoobarWorkspace ) workspace = foobar_workspace_new( self );
+ foobar_workspace_set_id( workspace, id );
+ foobar_workspace_set_name( workspace, name );
+ foobar_workspace_set_monitor( workspace, json_object_get_string_member( workspace_object, "monitor" ) );
+ foobar_workspace_set_flags( workspace, flags );
+ g_list_store_append( workspaces, workspace );
+ }
+}
+
+//
+// Find an existing workspace object by its ID.
+//
+FoobarWorkspace* foobar_workspace_service_find_workspace(
+ GListModel* workspaces,
+ gint64 id,
+ guint* out_index )
+{
+ for ( guint i = 0; i < g_list_model_get_n_items( workspaces ); ++i )
+ {
+ FoobarWorkspace* workspace = g_list_model_get_item( workspaces, i );
+ if ( foobar_workspace_get_id( workspace ) == id )
+ {
+ if ( out_index ) { *out_index = i; }
+ return workspace;
+ }
+ }
+
+ return NULL;
+}
+
+//
+// Hyprland's IPC sometimes reports the creation of workspaces strangely named `special:special:<some_name>`. This
+// function checks for that and is used to avoid creating (and then removing) such workspaces.
+//
+// See hyprwm/Hyprland#3424 for more info.
+//
+gboolean foobar_workspace_service_is_invalid_name( gchar const* name )
+{
+ return g_str_has_prefix( name, "special:special:" );
+}
+
+//
+// Parse an ID string and return its numeric code.
+//
+gint64 foobar_workspace_service_parse_id( gchar const* str )
+{
+ if ( !g_strcmp0( str, "special" ) ) { return -99; }
+
+ return strtoll( str, NULL, 10 );
+}
+
+//
+// Comparison function for two workspace IDs, used for binary search.
+//
+gint foobar_workspace_service_compare_ids(
+ gconstpointer a,
+ gconstpointer b )
+{
+ gint64 const* a_int = a;
+ gint64 const* b_int = b;
+
+ if ( *a_int < *b_int ) { return -1; }
+ if ( *a_int > *b_int ) { return 1; }
+ return 0;
+}
+
+//
+// Sorting callback for workspaces. Items are sorted to be in the following order:
+// 1. normal
+// 2. named persistent
+// 3. named
+// 4. special
+// 5. named special
+//
+gint foobar_workspace_service_sort_func(
+ gconstpointer item_a,
+ gconstpointer item_b,
+ gpointer userdata )
+{
+ (void)userdata;
+
+ FoobarWorkspace* workspace_a = (FoobarWorkspace*)item_a;
+ FoobarWorkspace* workspace_b = (FoobarWorkspace*)item_b;
+ gint64 id_a = foobar_workspace_get_id( workspace_a );
+ gint64 id_b = foobar_workspace_get_id( workspace_b );
+ gboolean special_a = ( foobar_workspace_get_flags( workspace_a ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) != 0;
+ gboolean special_b = ( foobar_workspace_get_flags( workspace_b ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) != 0;
+ gboolean named_a = id_a < 0;
+ gboolean named_b = id_b < 0;
+
+ // both normal (includes numbered persistent) => sort by ID
+ if ( !named_a && !named_b )
+ {
+ if ( id_a < id_b ) { return -1; }
+ if ( id_a > id_b ) { return 1; }
+ return 0;
+ }
+
+ // one normal, one special => normal first
+ if ( special_a && !special_b ) { return 1; }
+ if ( !special_a && special_b ) { return -1; }
+
+ // only one normal, one named
+ if ( named_a && !named_b ) { return -1; }
+ if ( !named_a && named_b ) { return 1; }
+
+ // both special
+ if ( special_a && special_b )
+ {
+ // if one is -99 => put it last
+ if ( id_a == -99 && id_b != -99 ) { return 1; }
+ if ( id_a != -99 && id_b == -99 ) { return -1; }
+ }
+
+ return g_strcmp0( foobar_workspace_get_name( workspace_a ), foobar_workspace_get_name( workspace_b ) );
+}
+
+//
+// Create a new EventData structure.
+//
+static EventData* event_data_new(
+ FoobarWorkspaceService* service,
+ gchar const* name,
+ gchar const* payload )
+{
+ EventData* self = g_new0( EventData, 1 );
+ self->service = g_object_ref( service );
+ self->name = g_strdup( name );
+ self->payload = g_strdup( payload );
+ return self;
+}
+
+//
+// Destroy an EventData structure.
+//
+static void event_data_free( EventData* self )
+{
+ if ( self )
+ {
+ g_object_unref( self->service );
+ g_free( self->name );
+ g_free( self->payload );
+ g_free( self );
+ }
+} \ No newline at end of file
diff --git a/src/services/workspace-service.h b/src/services/workspace-service.h
new file mode 100644
index 0000000..4a4a41a
--- /dev/null
+++ b/src/services/workspace-service.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_WORKSPACE_FLAGS foobar_workspace_flags_get_type( )
+#define FOOBAR_TYPE_WORKSPACE foobar_workspace_get_type( )
+#define FOOBAR_TYPE_WORKSPACE_SERVICE foobar_workspace_service_get_type( )
+
+typedef enum
+{
+ FOOBAR_WORKSPACE_FLAGS_NONE = 0,
+ // Indicates that the workspace is a globally active one (i.e. visible on the focused monitor).
+ FOOBAR_WORKSPACE_FLAGS_ACTIVE = 1 << 0,
+ // Indicates that the workspace is currently visible.
+ FOOBAR_WORKSPACE_FLAGS_VISIBLE = 1 << 1,
+ // Indicates that the workspace is a special workspace.
+ FOOBAR_WORKSPACE_FLAGS_SPECIAL = 1 << 2,
+ // Indicates that the workspace is persistent (i.e. configured in the config file).
+ FOOBAR_WORKSPACE_FLAGS_PERSISTENT = 1 << 3,
+ // Indicates that the workspace requires an urgent action (e.g. a notification).
+ FOOBAR_WORKSPACE_FLAGS_URGENT = 1 << 4,
+} FoobarWorkspaceFlags;
+
+GType foobar_workspace_flags_get_type( void );
+
+G_DECLARE_FINAL_TYPE( FoobarWorkspace, foobar_workspace, FOOBAR, WORKSPACE, GObject )
+
+gint64 foobar_workspace_get_id ( FoobarWorkspace* self );
+gchar const* foobar_workspace_get_name ( FoobarWorkspace* self );
+gchar const* foobar_workspace_get_monitor( FoobarWorkspace* self );
+FoobarWorkspaceFlags foobar_workspace_get_flags ( FoobarWorkspace* self );
+void foobar_workspace_activate ( FoobarWorkspace* self );
+
+G_DECLARE_FINAL_TYPE( FoobarWorkspaceService, foobar_workspace_service, FOOBAR, WORKSPACE_SERVICE, GObject )
+
+FoobarWorkspaceService* foobar_workspace_service_new ( void );
+GListModel* foobar_workspace_service_get_workspaces( FoobarWorkspaceService* self );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/utils.c b/src/utils.c
new file mode 100644
index 0000000..af1a3c7
--- /dev/null
+++ b/src/utils.c
@@ -0,0 +1,25 @@
+#include "utils.h"
+#include <gio/gio.h>
+
+//
+// Return the path for a file in the user's XDG cache directory. The result should be freed using g_free.
+//
+// This will also ensure that the application's cache directory exists.
+//
+gchar* foobar_get_cache_path( gchar const* filename )
+{
+ g_autoptr( GError ) error = NULL;
+ g_autofree gchar* directory_path = g_strdup_printf( "%s/foobar", g_get_user_cache_dir( ) );
+
+ if ( !g_file_test( directory_path, G_FILE_TEST_EXISTS ) )
+ {
+ g_autoptr( GFile ) cache_dir = g_file_new_for_path( directory_path );
+ if ( !g_file_make_directory_with_parents( cache_dir, NULL, &error ) )
+ {
+ g_warning( "Unable to create cache directory: %s", error->message );
+ return NULL;
+ }
+ }
+
+ return g_strdup_printf( "%s/%s", directory_path, filename );
+} \ No newline at end of file
diff --git a/src/utils.h b/src/utils.h
new file mode 100644
index 0000000..0adbc21
--- /dev/null
+++ b/src/utils.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+gchar* foobar_get_cache_path( gchar const* filename );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/control-center/control-button.c b/src/widgets/control-center/control-button.c
new file mode 100644
index 0000000..f535a61
--- /dev/null
+++ b/src/widgets/control-center/control-button.c
@@ -0,0 +1,496 @@
+#include "widgets/control-center/control-button.h"
+
+//
+// FoobarControlButton:
+//
+// A button in the "controls" section of the control center. This can be a regular button or a toggle button with an
+// optional secondary button to toggle a details view.
+//
+
+struct _FoobarControlButton
+{
+ GtkWidget parent_instance;
+ gchar* icon_name;
+ gchar* label;
+ gboolean can_expand;
+ gboolean can_toggle;
+ gboolean is_expanded;
+ gboolean is_toggled;
+};
+
+enum
+{
+ PROP_ICON_NAME = 1,
+ PROP_LABEL,
+ PROP_CAN_EXPAND,
+ PROP_CAN_TOGGLE,
+ PROP_IS_EXPANDED,
+ PROP_IS_TOGGLED,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_control_button_class_init ( FoobarControlButtonClass* klass );
+static void foobar_control_button_init ( FoobarControlButton* self );
+static void foobar_control_button_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_control_button_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_control_button_dispose ( GObject* object );
+static void foobar_control_button_finalize ( GObject* object );
+static void foobar_control_button_handle_primary_clicked( GtkButton* button,
+ gpointer userdata );
+static void foobar_control_button_handle_expand_clicked ( GtkButton* button,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarControlButton, foobar_control_button, GTK_TYPE_WIDGET )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Widget Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for control buttons.
+//
+void foobar_control_button_class_init( FoobarControlButtonClass* klass )
+{
+ GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass );
+ gtk_widget_class_set_css_name( widget_klass, "control-button" );
+ gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT );
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_control_button_get_property;
+ object_klass->set_property = foobar_control_button_set_property;
+ object_klass->dispose = foobar_control_button_dispose;
+ object_klass->finalize = foobar_control_button_finalize;
+
+ props[PROP_ICON_NAME] = g_param_spec_string(
+ "icon-name",
+ "Icon Name",
+ "Name of the icon to be displayed next to the label.",
+ NULL,
+ G_PARAM_READWRITE );
+ props[PROP_LABEL] = g_param_spec_string(
+ "label",
+ "Label",
+ "The button's main label.",
+ NULL,
+ G_PARAM_READWRITE );
+ props[PROP_CAN_EXPAND] = g_param_spec_boolean(
+ "can-expand",
+ "Can Expand",
+ "Indicates whether some details are hidden and can be revealed by a toggle button next to the main action.",
+ FALSE,
+ G_PARAM_READWRITE );
+ props[PROP_CAN_TOGGLE] = g_param_spec_boolean(
+ "can-toggle",
+ "Can Toggle",
+ "Indicates whether the main button can be toggled.",
+ FALSE,
+ G_PARAM_READWRITE );
+ props[PROP_IS_EXPANDED] = g_param_spec_boolean(
+ "is-expanded",
+ "Is Expanded",
+ "Indicates whether additional details should be shown.",
+ FALSE,
+ G_PARAM_READWRITE );
+ props[PROP_IS_TOGGLED] = g_param_spec_boolean(
+ "is-toggled",
+ "Is Toggled",
+ "Indicates whether the main button is currently toggled.",
+ FALSE,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for control buttons.
+//
+void foobar_control_button_init( FoobarControlButton* self )
+{
+ GtkWidget* icon = gtk_image_new( );
+ gtk_widget_set_valign( icon, GTK_ALIGN_CENTER );
+ g_object_bind_property(
+ self,
+ "icon-name",
+ icon,
+ "icon-name",
+ G_BINDING_SYNC_CREATE );
+
+ GtkWidget* label = gtk_label_new( NULL );
+ gtk_widget_set_valign( label, GTK_ALIGN_CENTER );
+ gtk_widget_set_hexpand( label, TRUE );
+ gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT );
+ gtk_label_set_xalign( GTK_LABEL( label ), 0 );
+ g_object_bind_property(
+ self,
+ "label",
+ label,
+ "label",
+ G_BINDING_SYNC_CREATE );
+
+ GtkWidget* content = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_box_append( GTK_BOX( content ), icon );
+ gtk_box_append( GTK_BOX( content ), label );
+
+ GtkWidget* primary_button = gtk_button_new( );
+ gtk_widget_add_css_class( primary_button, "primary" );
+ gtk_widget_set_hexpand( primary_button, TRUE );
+ gtk_button_set_child( GTK_BUTTON( primary_button ), content );
+ g_object_bind_property(
+ self,
+ "can-toggle",
+ primary_button,
+ "sensitive",
+ G_BINDING_SYNC_CREATE );
+ g_signal_connect( primary_button, "clicked", G_CALLBACK( foobar_control_button_handle_primary_clicked ), self );
+
+ GtkWidget* expand_button = gtk_button_new_from_icon_name( "fluent-chevron-right-symbolic" );
+ gtk_widget_add_css_class( expand_button, "expand" );
+ g_object_bind_property(
+ self,
+ "can-expand",
+ expand_button,
+ "visible",
+ G_BINDING_SYNC_CREATE );
+ g_signal_connect( expand_button, "clicked", G_CALLBACK( foobar_control_button_handle_expand_clicked ), self );
+
+ GtkLayoutManager* layout = gtk_widget_get_layout_manager( GTK_WIDGET( self ) );
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( layout ), GTK_ORIENTATION_HORIZONTAL );
+ gtk_widget_insert_before( primary_button, GTK_WIDGET( self ), NULL );
+ gtk_widget_insert_before( expand_button, GTK_WIDGET( self ), NULL );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_control_button_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarControlButton* self = (FoobarControlButton*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_ICON_NAME:
+ g_value_set_string( value, foobar_control_button_get_icon_name( self ) );
+ break;
+ case PROP_LABEL:
+ g_value_set_string( value, foobar_control_button_get_label( self ) );
+ break;
+ case PROP_CAN_EXPAND:
+ g_value_set_boolean( value, foobar_control_button_can_expand( self ) );
+ break;
+ case PROP_CAN_TOGGLE:
+ g_value_set_boolean( value, foobar_control_button_can_toggle( self ) );
+ break;
+ case PROP_IS_EXPANDED:
+ g_value_set_boolean( value, foobar_control_button_is_expanded( self ) );
+ break;
+ case PROP_IS_TOGGLED:
+ g_value_set_boolean( value, foobar_control_button_is_toggled( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_control_button_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarControlButton* self = (FoobarControlButton*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_ICON_NAME:
+ foobar_control_button_set_icon_name( self, g_value_get_string( value ) );
+ break;
+ case PROP_LABEL:
+ foobar_control_button_set_label( self, g_value_get_string( value ) );
+ break;
+ case PROP_CAN_EXPAND:
+ foobar_control_button_set_can_expand( self, g_value_get_boolean( value ) );
+ break;
+ case PROP_CAN_TOGGLE:
+ foobar_control_button_set_can_toggle( self, g_value_get_boolean( value ) );
+ break;
+ case PROP_IS_EXPANDED:
+ foobar_control_button_set_expanded( self, g_value_get_boolean( value ) );
+ break;
+ case PROP_IS_TOGGLED:
+ foobar_control_button_set_toggled( self, g_value_get_boolean( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for control buttons.
+//
+void foobar_control_button_dispose( GObject* object )
+{
+ FoobarControlButton* self = (FoobarControlButton*)object;
+
+ GtkWidget* child;
+ while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) )
+ {
+ gtk_widget_unparent( child );
+ }
+
+ G_OBJECT_CLASS( foobar_control_button_parent_class )->dispose( object );
+}
+
+//
+// Instance cleanup for control buttons.
+//
+void foobar_control_button_finalize( GObject* object )
+{
+ FoobarControlButton* self = (FoobarControlButton*)object;
+
+ g_clear_pointer( &self->icon_name, g_free );
+ g_clear_pointer( &self->label, g_free );
+
+ G_OBJECT_CLASS( foobar_control_button_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new control button instance.
+//
+GtkWidget* foobar_control_button_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_CONTROL_BUTTON, NULL );
+}
+
+//
+// Get the name of the icon to be displayed next to the label.
+//
+gchar const* foobar_control_button_get_icon_name( FoobarControlButton* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), NULL );
+ return self->icon_name;
+}
+
+//
+// Get the button's main label.
+//
+gchar const* foobar_control_button_get_label( FoobarControlButton* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), NULL );
+ return self->label;
+}
+
+//
+// Check whether some details are hidden and can be revealed by a toggle button next to the main action.
+//
+gboolean foobar_control_button_can_expand( FoobarControlButton* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE );
+ return self->can_expand;
+}
+
+//
+// Check whether the main button can be toggled.
+//
+gboolean foobar_control_button_can_toggle( FoobarControlButton* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE );
+ return self->can_toggle;
+}
+
+//
+// Check whether additional details should currently be shown.
+//
+gboolean foobar_control_button_is_expanded( FoobarControlButton* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE );
+ return self->is_expanded;
+}
+
+//
+// Check whether the main button is currently toggled.
+//
+gboolean foobar_control_button_is_toggled( FoobarControlButton* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE );
+ return self->is_toggled;
+}
+
+//
+// Update the name of the icon to be displayed next to the label.
+//
+void foobar_control_button_set_icon_name(
+ FoobarControlButton* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) );
+
+ if ( g_strcmp0( self->icon_name, value ) )
+ {
+ g_clear_pointer( &self->icon_name, g_free );
+ self->icon_name = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ICON_NAME] );
+ }
+}
+
+//
+// Update the button's main label.
+//
+void foobar_control_button_set_label(
+ FoobarControlButton* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) );
+
+ if ( g_strcmp0( self->label, value ) )
+ {
+ g_clear_pointer( &self->label, g_free );
+ self->label = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] );
+ }
+}
+
+//
+// Update whether some details are hidden and can be revealed by a toggle button next to the main action.
+//
+void foobar_control_button_set_can_expand(
+ FoobarControlButton* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) );
+
+ if ( !value ) { foobar_control_button_set_expanded( self, FALSE ); }
+
+ value = !!value;
+ if ( self->can_expand != value )
+ {
+ self->can_expand = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_EXPAND] );
+ }
+}
+
+//
+// Update whether the main button can be toggled.
+//
+void foobar_control_button_set_can_toggle(
+ FoobarControlButton* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) );
+
+ if ( !value ) { foobar_control_button_set_toggled( self, FALSE ); }
+
+ value = !!value;
+ if ( self->can_toggle != value )
+ {
+ self->can_toggle = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_TOGGLE] );
+ }
+}
+
+//
+// Update whether additional details are currently shown.
+//
+void foobar_control_button_set_expanded(
+ FoobarControlButton* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) );
+
+ value = value && self->can_expand;
+ if ( self->is_expanded != value )
+ {
+ self->is_expanded = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] );
+
+ if ( self->is_expanded )
+ {
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" );
+ }
+ else
+ {
+ gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" );
+ }
+ }
+}
+
+//
+// Update whether the main button is currently toggled.
+//
+void foobar_control_button_set_toggled(
+ FoobarControlButton* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) );
+
+ value = value && self->can_toggle;
+ if ( self->is_toggled != value )
+ {
+ self->is_toggled = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_TOGGLED] );
+
+ if ( self->is_toggled )
+ {
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "toggled" );
+ }
+ else
+ {
+ gtk_widget_remove_css_class( GTK_WIDGET( self ), "toggled" );
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called when the primary button was clicked.
+//
+// If allowed, this will toggle the button.
+//
+void foobar_control_button_handle_primary_clicked( GtkButton* button, gpointer userdata )
+{
+ (void)button;
+ FoobarControlButton* self = (FoobarControlButton*)userdata;
+
+ if ( foobar_control_button_can_toggle( self ) )
+ {
+ foobar_control_button_set_toggled( self, !foobar_control_button_is_toggled( self ) );
+ }
+}
+
+//
+// Called when the expand button was clicked.
+//
+// If allowed, this will toggle the "expanded" state of the button.
+//
+void foobar_control_button_handle_expand_clicked( GtkButton* button, gpointer userdata )
+{
+ (void)button;
+ FoobarControlButton* self = (FoobarControlButton*)userdata;
+
+ if ( foobar_control_button_can_expand( self ) )
+ {
+ foobar_control_button_set_expanded( self, !foobar_control_button_is_expanded( self ) );
+ }
+} \ No newline at end of file
diff --git a/src/widgets/control-center/control-button.h b/src/widgets/control-center/control-button.h
new file mode 100644
index 0000000..48dcfb1
--- /dev/null
+++ b/src/widgets/control-center/control-button.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_CONTROL_BUTTON foobar_control_button_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarControlButton, foobar_control_button, FOOBAR, CONTROL_BUTTON, GtkWidget )
+
+GtkWidget* foobar_control_button_new ( void );
+gchar const* foobar_control_button_get_icon_name ( FoobarControlButton* self );
+gchar const* foobar_control_button_get_label ( FoobarControlButton* self );
+gboolean foobar_control_button_can_expand ( FoobarControlButton* self );
+gboolean foobar_control_button_can_toggle ( FoobarControlButton* self );
+gboolean foobar_control_button_is_expanded ( FoobarControlButton* self );
+gboolean foobar_control_button_is_toggled ( FoobarControlButton* self );
+void foobar_control_button_set_icon_name ( FoobarControlButton* self,
+ gchar const* value );
+void foobar_control_button_set_label ( FoobarControlButton* self,
+ gchar const* value );
+void foobar_control_button_set_can_expand( FoobarControlButton* self,
+ gboolean value );
+void foobar_control_button_set_can_toggle( FoobarControlButton* self,
+ gboolean value );
+void foobar_control_button_set_expanded ( FoobarControlButton* self,
+ gboolean value );
+void foobar_control_button_set_toggled ( FoobarControlButton* self,
+ gboolean value );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/control-center/control-details-item.c b/src/widgets/control-center/control-details-item.c
new file mode 100644
index 0000000..f8858d5
--- /dev/null
+++ b/src/widgets/control-center/control-details-item.c
@@ -0,0 +1,307 @@
+#include "widgets/control-center/control-details-item.h"
+
+//
+// FoobarControlDetailsAccessory:
+//
+// The accessory shown next to an item in the control center details list (if any).
+//
+
+G_DEFINE_ENUM_TYPE(
+ FoobarControlDetailsAccessory,
+ foobar_control_details_accessory,
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE, "none" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS, "progress" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED, "checked" ) )
+
+//
+// FoobarControlDetailsItem:
+//
+// Standard row layout for items in a FoobarControlDetails list. The rows are made up of a label and an accessory that
+// is shown or hidden depending on the item's state.
+//
+
+struct _FoobarControlDetailsItem
+{
+ GtkWidget parent_instance;
+ GtkWidget* spinner;
+ GtkWidget* checkmark;
+ gchar* label;
+ FoobarControlDetailsAccessory accessory;
+};
+
+enum
+{
+ PROP_LABEL = 1,
+ PROP_ACCESSORY,
+ PROP_IS_CHECKED,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_control_details_item_class_init ( FoobarControlDetailsItemClass* klass );
+static void foobar_control_details_item_init ( FoobarControlDetailsItem* self );
+static void foobar_control_details_item_get_property( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_control_details_item_set_property( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_control_details_item_dispose ( GObject* object );
+static void foobar_control_details_item_finalize ( GObject* object );
+
+G_DEFINE_FINAL_TYPE( FoobarControlDetailsItem, foobar_control_details_item, GTK_TYPE_WIDGET )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Widget Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for control details items.
+//
+void foobar_control_details_item_class_init( FoobarControlDetailsItemClass* klass )
+{
+ GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass );
+ gtk_widget_class_set_css_name( widget_klass, "control-details-item" );
+ gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT );
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_control_details_item_get_property;
+ object_klass->set_property = foobar_control_details_item_set_property;
+ object_klass->dispose = foobar_control_details_item_dispose;
+ object_klass->finalize = foobar_control_details_item_finalize;
+
+ props[PROP_LABEL] = g_param_spec_string(
+ "label",
+ "Label",
+ "Label shown for the item.",
+ NULL,
+ G_PARAM_READWRITE );
+ props[PROP_ACCESSORY] = g_param_spec_enum(
+ "accessory",
+ "Accessory",
+ "Accessory shown for the item.",
+ FOOBAR_TYPE_CONTROL_DETAILS_ACCESSORY,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_IS_CHECKED] = g_param_spec_boolean(
+ "is-checked",
+ "Is Checked",
+ "Current checked-state for the item.",
+ FALSE,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for control details items.
+//
+void foobar_control_details_item_init( FoobarControlDetailsItem* self )
+{
+ GtkWidget* label = gtk_label_new( NULL );
+ gtk_widget_set_hexpand( label, TRUE );
+ gtk_widget_set_valign( label, GTK_ALIGN_CENTER );
+ gtk_label_set_ellipsize( GTK_LABEL( label ), PANGO_ELLIPSIZE_END );
+ gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT );
+ gtk_label_set_xalign( GTK_LABEL( label ), 0 );
+ gtk_label_set_wrap( GTK_LABEL( label ), FALSE );
+ g_object_bind_property(
+ self,
+ "label",
+ label,
+ "label",
+ G_BINDING_SYNC_CREATE );
+
+ self->spinner = gtk_spinner_new( );
+ gtk_spinner_set_spinning( GTK_SPINNER( self->spinner ), TRUE );
+ gtk_widget_add_css_class( self->spinner, "accessory" );
+ gtk_widget_set_valign( self->spinner, GTK_ALIGN_CENTER );
+ gtk_widget_set_visible( self->spinner, FALSE );
+
+ self->checkmark = gtk_image_new_from_icon_name( "fluent-checkmark-symbolic" );
+ gtk_widget_add_css_class( self->checkmark, "checkmark" );
+ gtk_widget_add_css_class( self->checkmark, "accessory" );
+ gtk_widget_set_valign( self->checkmark, GTK_ALIGN_CENTER );
+ gtk_widget_set_visible( self->checkmark, FALSE );
+
+ gtk_widget_insert_before( label, GTK_WIDGET( self ), NULL );
+ gtk_widget_insert_before( self->spinner, GTK_WIDGET( self ), NULL );
+ gtk_widget_insert_before( self->checkmark, GTK_WIDGET( self ), NULL );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_control_details_item_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_LABEL:
+ g_value_set_string( value, foobar_control_details_item_get_label( self ) );
+ break;
+ case PROP_ACCESSORY:
+ g_value_set_enum( value, foobar_control_details_item_get_accessory( self ) );
+ break;
+ case PROP_IS_CHECKED:
+ g_value_set_boolean( value, foobar_control_details_item_is_checked( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_control_details_item_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_LABEL:
+ foobar_control_details_item_set_label( self, g_value_get_string( value ) );
+ break;
+ case PROP_ACCESSORY:
+ foobar_control_details_item_set_accessory( self, g_value_get_enum( value ) );
+ break;
+ case PROP_IS_CHECKED:
+ foobar_control_details_item_set_checked( self, g_value_get_boolean( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for control details items.
+//
+void foobar_control_details_item_dispose( GObject* object )
+{
+ FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object;
+
+ GtkWidget* child;
+ while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) )
+ {
+ gtk_widget_unparent( child );
+ }
+
+ G_OBJECT_CLASS( foobar_control_details_item_parent_class )->dispose( object );
+}
+
+//
+// Instance cleanup for control details items.
+//
+void foobar_control_details_item_finalize( GObject* object )
+{
+ FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object;
+
+ g_clear_pointer( &self->label, g_free );
+
+ G_OBJECT_CLASS( foobar_control_details_item_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new control details item instance.
+//
+GtkWidget* foobar_control_details_item_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_CONTROL_DETAILS_ITEM, NULL );
+}
+
+//
+// Get the label shown for the item.
+//
+gchar const* foobar_control_details_item_get_label( FoobarControlDetailsItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), NULL );
+ return self->label;
+}
+
+//
+// Get the accessory shown for the item.
+//
+FoobarControlDetailsAccessory foobar_control_details_item_get_accessory( FoobarControlDetailsItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), 0 );
+ return self->accessory;
+}
+
+//
+// Get the current checked-state for the item.
+//
+gboolean foobar_control_details_item_is_checked( FoobarControlDetailsItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), FALSE );
+ return self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED;
+}
+
+//
+// Update the label shown for the item.
+//
+void foobar_control_details_item_set_label(
+ FoobarControlDetailsItem* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) );
+
+ if ( g_strcmp0( self->label, value ) )
+ {
+ g_clear_pointer( &self->label, g_free );
+ self->label = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] );
+ }
+}
+
+//
+// Update the current accessory shown for the item.
+//
+void foobar_control_details_item_set_accessory(
+ FoobarControlDetailsItem* self,
+ FoobarControlDetailsAccessory value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) );
+
+ if ( self->accessory != value )
+ {
+ self->accessory = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ACCESSORY] );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_CHECKED] );
+
+ gtk_widget_set_visible( self->spinner, self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS );
+ gtk_widget_set_visible( self->checkmark, self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED );
+ }
+}
+
+//
+// Update the current checked-state for the item.
+//
+void foobar_control_details_item_set_checked(
+ FoobarControlDetailsItem* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) );
+
+ FoobarControlDetailsAccessory accessory = value
+ ? FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED
+ : FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE;
+ foobar_control_details_item_set_accessory( self, accessory );
+} \ No newline at end of file
diff --git a/src/widgets/control-center/control-details-item.h b/src/widgets/control-center/control-details-item.h
new file mode 100644
index 0000000..b9be4a7
--- /dev/null
+++ b/src/widgets/control-center/control-details-item.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_CONTROL_DETAILS_ACCESSORY foobar_control_details_accessory_get_type( )
+#define FOOBAR_TYPE_CONTROL_DETAILS_ITEM foobar_control_details_item_get_type( )
+
+typedef enum
+{
+ FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE,
+ FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED,
+ FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS,
+} FoobarControlDetailsAccessory;
+
+GType foobar_control_details_accessory_get_type( void );
+
+G_DECLARE_FINAL_TYPE( FoobarControlDetailsItem, foobar_control_details_item, FOOBAR, CONTROL_DETAILS_ITEM, GtkWidget )
+
+GtkWidget* foobar_control_details_item_new ( void );
+gchar const* foobar_control_details_item_get_label ( FoobarControlDetailsItem* self );
+FoobarControlDetailsAccessory foobar_control_details_item_get_accessory( FoobarControlDetailsItem* self );
+gboolean foobar_control_details_item_is_checked ( FoobarControlDetailsItem* self );
+void foobar_control_details_item_set_label ( FoobarControlDetailsItem* self,
+ gchar const* value );
+void foobar_control_details_item_set_accessory( FoobarControlDetailsItem* self,
+ FoobarControlDetailsAccessory value );
+void foobar_control_details_item_set_checked ( FoobarControlDetailsItem* self,
+ gboolean value );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/control-center/control-details.c b/src/widgets/control-center/control-details.c
new file mode 100644
index 0000000..4f0d422
--- /dev/null
+++ b/src/widgets/control-center/control-details.c
@@ -0,0 +1,289 @@
+#include "widgets/control-center/control-details.h"
+
+//
+// FoobarControlDetails:
+//
+// A revealer for details in the "controls" section of the control center. This widget is usually used to embed a list
+// view. It takes care of scrolling by embedding the child in a GtkScrolledWindow.
+//
+
+struct _FoobarControlDetails
+{
+ GtkWidget parent_instance;
+ GtkWidget* child;
+ gboolean is_expanded;
+ gint inset_top;
+};
+
+enum
+{
+ PROP_CHILD = 1,
+ PROP_IS_EXPANDED,
+ PROP_INSET_TOP,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_control_details_class_init ( FoobarControlDetailsClass* klass );
+static void foobar_control_details_init ( FoobarControlDetails* self );
+static void foobar_control_details_get_property( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_control_details_set_property( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_control_details_dispose ( GObject* object );
+
+G_DEFINE_FINAL_TYPE( FoobarControlDetails, foobar_control_details, GTK_TYPE_WIDGET )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Widget Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for control details.
+//
+void foobar_control_details_class_init( FoobarControlDetailsClass* klass )
+{
+ GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass );
+ gtk_widget_class_set_css_name( widget_klass, "control-details" );
+ gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BIN_LAYOUT );
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_control_details_get_property;
+ object_klass->set_property = foobar_control_details_set_property;
+ object_klass->dispose = foobar_control_details_dispose;
+
+ props[PROP_CHILD] = g_param_spec_object(
+ "child",
+ "Child",
+ "Child widget for the details revealer.",
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE );
+ props[PROP_IS_EXPANDED] = g_param_spec_boolean(
+ "is-expanded",
+ "Is Expanded",
+ "Current state of the details revealer.",
+ FALSE,
+ G_PARAM_READWRITE );
+ props[PROP_INSET_TOP] = g_param_spec_int(
+ "inset-top",
+ "Inset Top",
+ "Inset from the top edge (only visible when expanded).",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for control details.
+//
+void foobar_control_details_init( FoobarControlDetails* self )
+{
+ GtkWidget* scrolled_window = gtk_scrolled_window_new( );
+ gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scrolled_window ), GTK_POLICY_NEVER, GTK_POLICY_EXTERNAL );
+ gtk_scrolled_window_set_propagate_natural_height( GTK_SCROLLED_WINDOW( scrolled_window ), TRUE );
+ g_object_bind_property(
+ self,
+ "child",
+ scrolled_window,
+ "child",
+ G_BINDING_SYNC_CREATE );
+ g_object_bind_property(
+ self,
+ "inset-top",
+ scrolled_window,
+ "margin-top",
+ G_BINDING_SYNC_CREATE );
+
+ GtkWidget* revealer = gtk_revealer_new( );
+ gtk_revealer_set_transition_type( GTK_REVEALER( revealer ), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN );
+ gtk_revealer_set_transition_duration( GTK_REVEALER( revealer ), 100 );
+ gtk_revealer_set_child( GTK_REVEALER( revealer ), scrolled_window );
+ g_object_bind_property(
+ self,
+ "is-expanded",
+ revealer,
+ "reveal-child",
+ G_BINDING_SYNC_CREATE );
+
+ gtk_widget_set_parent( revealer, GTK_WIDGET( self ) );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_control_details_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarControlDetails* self = (FoobarControlDetails*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_CHILD:
+ g_value_set_object( value, foobar_control_details_get_child( self ) );
+ break;
+ case PROP_IS_EXPANDED:
+ g_value_set_boolean( value, foobar_control_details_is_expanded( self ) );
+ break;
+ case PROP_INSET_TOP:
+ g_value_set_int( value, foobar_control_details_get_inset_top( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_control_details_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarControlDetails* self = (FoobarControlDetails*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_CHILD:
+ foobar_control_details_set_child( self, g_value_get_object( value ) );
+ break;
+ case PROP_IS_EXPANDED:
+ foobar_control_details_set_expanded( self, g_value_get_boolean( value ) );
+ break;
+ case PROP_INSET_TOP:
+ foobar_control_details_set_inset_top( self, g_value_get_int( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for control details.
+//
+void foobar_control_details_dispose( GObject* object )
+{
+ FoobarControlDetails* self = (FoobarControlDetails*)object;
+
+ GtkWidget* child;
+ while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) )
+ {
+ gtk_widget_unparent( child );
+ }
+
+ g_clear_object( &self->child );
+
+ G_OBJECT_CLASS( foobar_control_details_parent_class )->dispose( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new control details instance.
+//
+GtkWidget* foobar_control_details_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_CONTROL_DETAILS, NULL );
+}
+
+//
+// Get the child widget for the details revealer.
+//
+GtkWidget* foobar_control_details_get_child( FoobarControlDetails* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), NULL );
+ return self->child;
+}
+
+//
+// Get the current state of the details revealer.
+//
+gboolean foobar_control_details_is_expanded( FoobarControlDetails* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), FALSE );
+ return self->is_expanded;
+}
+
+//
+// Get the inset from the top edge (only visible when expanded).
+//
+gint foobar_control_details_get_inset_top( FoobarControlDetails* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), FALSE );
+ return self->inset_top;
+}
+
+//
+// Update the child widget for the details revealer.
+//
+void foobar_control_details_set_child(
+ FoobarControlDetails* self,
+ GtkWidget* value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) );
+
+ if ( self->child != value )
+ {
+ g_clear_object( &self->child );
+ if ( value ) { self->child = g_object_ref( value ); }
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] );
+ }
+}
+
+//
+// Update the current state of the details revealer.
+//
+void foobar_control_details_set_expanded(
+ FoobarControlDetails* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) );
+
+ value = !!value;
+ if ( self->is_expanded != value )
+ {
+ self->is_expanded = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] );
+
+ if ( self->is_expanded )
+ {
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" );
+ }
+ else
+ {
+ gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" );
+ }
+ }
+}
+
+//
+// Update the inset from the top edge (only visible when expanded).
+//
+void foobar_control_details_set_inset_top(
+ FoobarControlDetails* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) );
+
+ value = MAX( value, 0 );
+ if ( self->inset_top != value )
+ {
+ self->inset_top = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_TOP] );
+ }
+} \ No newline at end of file
diff --git a/src/widgets/control-center/control-details.h b/src/widgets/control-center/control-details.h
new file mode 100644
index 0000000..71df683
--- /dev/null
+++ b/src/widgets/control-center/control-details.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_CONTROL_DETAILS foobar_control_details_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarControlDetails, foobar_control_details, FOOBAR, CONTROL_DETAILS, GtkWidget )
+
+GtkWidget* foobar_control_details_new ( void );
+GtkWidget* foobar_control_details_get_child ( FoobarControlDetails* self );
+gboolean foobar_control_details_is_expanded ( FoobarControlDetails* self );
+gint foobar_control_details_get_inset_top( FoobarControlDetails* self );
+void foobar_control_details_set_child ( FoobarControlDetails* self,
+ GtkWidget* value );
+void foobar_control_details_set_expanded ( FoobarControlDetails* self,
+ gboolean value );
+void foobar_control_details_set_inset_top( FoobarControlDetails* self,
+ gint value );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/control-center/control-slider.c b/src/widgets/control-center/control-slider.c
new file mode 100644
index 0000000..d98dc9e
--- /dev/null
+++ b/src/widgets/control-center/control-slider.c
@@ -0,0 +1,428 @@
+#include "widgets/control-center/control-slider.h"
+
+//
+// FoobarControlSlider:
+//
+// A slider in the "controls" section of the control center with a title and icon, as well as an optional secondary
+// button to toggle a details view.
+//
+
+struct _FoobarControlSlider
+{
+ GtkWidget parent_instance;
+ gchar* icon_name;
+ gchar* label;
+ gboolean can_expand;
+ gboolean is_expanded;
+ gint percentage;
+};
+
+enum
+{
+ PROP_ICON_NAME = 1,
+ PROP_LABEL,
+ PROP_CAN_EXPAND,
+ PROP_IS_EXPANDED,
+ PROP_PERCENTAGE,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_control_slider_class_init ( FoobarControlSliderClass* klass );
+static void foobar_control_slider_init ( FoobarControlSlider* self );
+static void foobar_control_slider_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_control_slider_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_control_slider_dispose ( GObject* object );
+static void foobar_control_slider_finalize ( GObject* object );
+static void foobar_control_slider_handle_expand_clicked( GtkButton* button,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarControlSlider, foobar_control_slider, GTK_TYPE_WIDGET )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Widget Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for control sliders.
+//
+void foobar_control_slider_class_init( FoobarControlSliderClass* klass )
+{
+ GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass );
+ gtk_widget_class_set_css_name( widget_klass, "control-slider" );
+ gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT );
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_control_slider_get_property;
+ object_klass->set_property = foobar_control_slider_set_property;
+ object_klass->dispose = foobar_control_slider_dispose;
+ object_klass->finalize = foobar_control_slider_finalize;
+
+ props[PROP_ICON_NAME] = g_param_spec_string(
+ "icon-name",
+ "Icon Name",
+ "Name of the icon to be displayed next to the label.",
+ NULL,
+ G_PARAM_READWRITE );
+ props[PROP_LABEL] = g_param_spec_string(
+ "label",
+ "Label",
+ "The slider's main label.",
+ NULL,
+ G_PARAM_READWRITE );
+ props[PROP_CAN_EXPAND] = g_param_spec_boolean(
+ "can-expand",
+ "Can Expand",
+ "Indicates whether some details are hidden and can be revealed by a toggle button next to the slider.",
+ FALSE,
+ G_PARAM_READWRITE );
+ props[PROP_IS_EXPANDED] = g_param_spec_boolean(
+ "is-expanded",
+ "Is Expanded",
+ "Indicates whether additional details should be shown.",
+ FALSE,
+ G_PARAM_READWRITE );
+ props[PROP_PERCENTAGE] = g_param_spec_int(
+ "percentage",
+ "Percentage",
+ "Value shown by the slider.",
+ 0,
+ 100,
+ 0,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for control sliders.
+//
+void foobar_control_slider_init( FoobarControlSlider* self )
+{
+ GtkWidget* icon = gtk_image_new( );
+ gtk_widget_set_valign( icon, GTK_ALIGN_CENTER );
+ g_object_bind_property(
+ self,
+ "icon-name",
+ icon,
+ "icon-name",
+ G_BINDING_SYNC_CREATE );
+
+ GtkWidget* label = gtk_label_new( NULL );
+ gtk_widget_set_valign( label, GTK_ALIGN_CENTER );
+ gtk_widget_set_hexpand( label, TRUE );
+ gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT );
+ gtk_label_set_xalign( GTK_LABEL( label ), 0 );
+ g_object_bind_property(
+ self,
+ "label",
+ label,
+ "label",
+ G_BINDING_SYNC_CREATE );
+
+ GtkWidget* header = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_box_append( GTK_BOX( header ), icon );
+ gtk_box_append( GTK_BOX( header ), label );
+
+ GtkWidget* scale = gtk_scale_new_with_range( GTK_ORIENTATION_HORIZONTAL, 0, 100, 2 );
+ g_object_bind_property(
+ self,
+ "percentage",
+ gtk_range_get_adjustment( GTK_RANGE( scale ) ),
+ "value",
+ G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL );
+
+ GtkWidget* primary_content = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_widget_add_css_class( primary_content, "primary" );
+ gtk_box_append( GTK_BOX( primary_content ), header );
+ gtk_box_append( GTK_BOX( primary_content ), scale );
+
+ GtkWidget* expand_button = gtk_button_new_from_icon_name( "fluent-chevron-right-symbolic" );
+ gtk_widget_add_css_class( expand_button, "expand" );
+ g_object_bind_property(
+ self,
+ "can-expand",
+ expand_button,
+ "visible",
+ G_BINDING_SYNC_CREATE );
+ g_signal_connect( expand_button, "clicked", G_CALLBACK( foobar_control_slider_handle_expand_clicked ), self );
+
+ GtkLayoutManager* layout = gtk_widget_get_layout_manager( GTK_WIDGET( self ) );
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( layout ), GTK_ORIENTATION_HORIZONTAL );
+ gtk_widget_insert_before( primary_content, GTK_WIDGET( self ), NULL );
+ gtk_widget_insert_before( expand_button, GTK_WIDGET( self ), NULL );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_control_slider_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarControlSlider* self = (FoobarControlSlider*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_ICON_NAME:
+ g_value_set_string( value, foobar_control_slider_get_icon_name( self ) );
+ break;
+ case PROP_LABEL:
+ g_value_set_string( value, foobar_control_slider_get_label( self ) );
+ break;
+ case PROP_CAN_EXPAND:
+ g_value_set_boolean( value, foobar_control_slider_can_expand( self ) );
+ break;
+ case PROP_IS_EXPANDED:
+ g_value_set_boolean( value, foobar_control_slider_is_expanded( self ) );
+ break;
+ case PROP_PERCENTAGE:
+ g_value_set_int( value, foobar_control_slider_get_percentage( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_control_slider_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarControlSlider* self = (FoobarControlSlider*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_ICON_NAME:
+ foobar_control_slider_set_icon_name( self, g_value_get_string( value ) );
+ break;
+ case PROP_LABEL:
+ foobar_control_slider_set_label( self, g_value_get_string( value ) );
+ break;
+ case PROP_CAN_EXPAND:
+ foobar_control_slider_set_can_expand( self, g_value_get_boolean( value ) );
+ break;
+ case PROP_IS_EXPANDED:
+ foobar_control_slider_set_expanded( self, g_value_get_boolean( value ) );
+ break;
+ case PROP_PERCENTAGE:
+ foobar_control_slider_set_percentage( self, g_value_get_int( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for control sliders.
+//
+void foobar_control_slider_dispose( GObject* object )
+{
+ FoobarControlSlider* self = (FoobarControlSlider*)object;
+
+ GtkWidget* child;
+ while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) )
+ {
+ gtk_widget_unparent( child );
+ }
+
+ G_OBJECT_CLASS( foobar_control_slider_parent_class )->dispose( object );
+}
+
+//
+// Instance cleanup for control sliders.
+//
+void foobar_control_slider_finalize( GObject* object )
+{
+ FoobarControlSlider* self = (FoobarControlSlider*)object;
+
+ g_clear_pointer( &self->icon_name, g_free );
+ g_clear_pointer( &self->label, g_free );
+
+ G_OBJECT_CLASS( foobar_control_slider_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new control slider instance.
+//
+GtkWidget* foobar_control_slider_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_CONTROL_SLIDER, NULL );
+}
+
+//
+// Get the name of the icon to be displayed next to the label.
+//
+gchar const* foobar_control_slider_get_icon_name( FoobarControlSlider* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), NULL );
+ return self->icon_name;
+}
+
+//
+// Get the slider's main label.
+//
+gchar const* foobar_control_slider_get_label( FoobarControlSlider* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), NULL );
+ return self->label;
+}
+
+//
+// Check whether some details are hidden and can be revealed by a toggle button next to the slider.
+//
+gboolean foobar_control_slider_can_expand( FoobarControlSlider* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE );
+ return self->can_expand;
+}
+
+//
+// Check whether additional details should currently be shown.
+//
+gboolean foobar_control_slider_is_expanded( FoobarControlSlider* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE );
+ return self->is_expanded;
+}
+
+//
+// Get the value shown by the slider.
+//
+gint foobar_control_slider_get_percentage( FoobarControlSlider* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE );
+ return self->percentage;
+}
+
+//
+// Update the name of the icon to be displayed next to the label.
+//
+void foobar_control_slider_set_icon_name(
+ FoobarControlSlider* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) );
+
+ if ( g_strcmp0( self->icon_name, value ) )
+ {
+ g_clear_pointer( &self->icon_name, g_free );
+ self->icon_name = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ICON_NAME] );
+ }
+}
+
+//
+// Update the slider's main label.
+//
+void foobar_control_slider_set_label(
+ FoobarControlSlider* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) );
+
+ if ( g_strcmp0( self->label, value ) )
+ {
+ g_clear_pointer( &self->label, g_free );
+ self->label = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] );
+ }
+}
+
+//
+// Update whether some details are hidden and can be revealed by a toggle button next to the slider.
+//
+void foobar_control_slider_set_can_expand(
+ FoobarControlSlider* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) );
+
+ if ( !value ) { foobar_control_slider_set_expanded( self, FALSE ); }
+
+ value = !!value;
+ if ( self->can_expand != value )
+ {
+ self->can_expand = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_EXPAND] );
+ }
+}
+
+//
+// Update whether additional details are currently shown.
+//
+void foobar_control_slider_set_expanded(
+ FoobarControlSlider* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) );
+
+ value = value && self->can_expand;
+ if ( self->is_expanded != value )
+ {
+ self->is_expanded = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] );
+
+ if ( self->is_expanded )
+ {
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" );
+ }
+ else
+ {
+ gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" );
+ }
+ }
+}
+
+//
+// Update the value shown by the slider.
+//
+void foobar_control_slider_set_percentage( FoobarControlSlider* self, gint value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) );
+
+ value = CLAMP( value, 0, 100 );
+ if ( self->percentage != value )
+ {
+ self->percentage = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_PERCENTAGE] );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called when the expand button was clicked.
+//
+// If allowed, this will toggle the "expanded" state of the slider.
+//
+void foobar_control_slider_handle_expand_clicked( GtkButton* button, gpointer userdata )
+{
+ (void)button;
+ FoobarControlSlider* self = (FoobarControlSlider*)userdata;
+
+ if ( foobar_control_slider_can_expand( self ) )
+ {
+ foobar_control_slider_set_expanded( self, !foobar_control_slider_is_expanded( self ) );
+ }
+} \ No newline at end of file
diff --git a/src/widgets/control-center/control-slider.h b/src/widgets/control-center/control-slider.h
new file mode 100644
index 0000000..e7dfac0
--- /dev/null
+++ b/src/widgets/control-center/control-slider.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_CONTROL_SLIDER foobar_control_slider_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarControlSlider, foobar_control_slider, FOOBAR, CONTROL_SLIDER, GtkWidget )
+
+GtkWidget* foobar_control_slider_new ( void );
+gchar const* foobar_control_slider_get_icon_name ( FoobarControlSlider* self );
+gchar const* foobar_control_slider_get_label ( FoobarControlSlider* self );
+gboolean foobar_control_slider_can_expand ( FoobarControlSlider* self );
+gboolean foobar_control_slider_is_expanded ( FoobarControlSlider* self );
+gint foobar_control_slider_get_percentage( FoobarControlSlider* self );
+void foobar_control_slider_set_icon_name ( FoobarControlSlider* self,
+ gchar const* value );
+void foobar_control_slider_set_label ( FoobarControlSlider* self,
+ gchar const* value );
+void foobar_control_slider_set_can_expand( FoobarControlSlider* self,
+ gboolean value );
+void foobar_control_slider_set_expanded ( FoobarControlSlider* self,
+ gboolean value );
+void foobar_control_slider_set_percentage( FoobarControlSlider* self,
+ gint value );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/control-center/meson.build b/src/widgets/control-center/meson.build
new file mode 100644
index 0000000..5b1252d
--- /dev/null
+++ b/src/widgets/control-center/meson.build
@@ -0,0 +1,6 @@
+foobar_sources += files(
+ 'control-button.c',
+ 'control-slider.c',
+ 'control-details.c',
+ 'control-details-item.c',
+) \ No newline at end of file
diff --git a/src/widgets/inset-container.c b/src/widgets/inset-container.c
new file mode 100644
index 0000000..9b9b132
--- /dev/null
+++ b/src/widgets/inset-container.c
@@ -0,0 +1,363 @@
+#include "widgets/inset-container.h"
+
+//
+// FoobarInsetContainer:
+//
+// A custom widget which renders a child with an inset/padding.
+//
+// Notably, it also supports negative inset values which is useful in order to provide uniform spacing between widgets.
+//
+
+struct _FoobarInsetContainer
+{
+ GtkWidget parent_instance;
+ GtkWidget* child;
+ gint inset_horizontal;
+ gint inset_vertical;
+};
+
+enum
+{
+ PROP_CHILD = 1,
+ PROP_INSET_HORIZONTAL,
+ PROP_INSET_VERTICAL,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_inset_container_class_init ( FoobarInsetContainerClass* klass );
+static void foobar_inset_container_init ( FoobarInsetContainer* self );
+static void foobar_inset_container_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_inset_container_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_inset_container_dispose ( GObject* object );
+static GtkSizeRequestMode foobar_inset_container_get_request_mode( GtkWidget* widget );
+static void foobar_inset_container_measure ( GtkWidget* widget,
+ GtkOrientation orientation,
+ int for_size,
+ int* minimum,
+ int* natural,
+ int* minimum_baseline,
+ int* natural_baseline );
+static void foobar_inset_container_size_allocate ( GtkWidget* widget,
+ int width,
+ int height,
+ int baseline );
+
+G_DEFINE_FINAL_TYPE( FoobarInsetContainer, foobar_inset_container, GTK_TYPE_WIDGET )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Widget Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for inset containers.
+//
+void foobar_inset_container_class_init( FoobarInsetContainerClass* klass )
+{
+ GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass );
+ widget_klass->get_request_mode = foobar_inset_container_get_request_mode;
+ widget_klass->measure = foobar_inset_container_measure;
+ widget_klass->size_allocate = foobar_inset_container_size_allocate;
+ gtk_widget_class_set_css_name( widget_klass, "inset-container" );
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_inset_container_get_property;
+ object_klass->set_property = foobar_inset_container_set_property;
+ object_klass->dispose = foobar_inset_container_dispose;
+
+ props[PROP_CHILD] = g_param_spec_object(
+ "child",
+ "Child",
+ "Child widget for the panel item.",
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE );
+ props[PROP_INSET_HORIZONTAL] = g_param_spec_int(
+ "inset-horizontal",
+ "Inset Horizontal",
+ "The horizontal offset (may be negative).",
+ INT_MIN,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_INSET_VERTICAL] = g_param_spec_int(
+ "inset-vertical",
+ "Inset Vertical",
+ "The vertical offset (may be negative).",
+ INT_MIN,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for inset containers.
+//
+void foobar_inset_container_init( FoobarInsetContainer* self )
+{
+ (void)self;
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_inset_container_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarInsetContainer* self = (FoobarInsetContainer*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_CHILD:
+ g_value_set_object( value, foobar_inset_container_get_child( self ) );
+ break;
+ case PROP_INSET_HORIZONTAL:
+ g_value_set_int( value, foobar_inset_container_get_inset_horizontal( self ) );
+ break;
+ case PROP_INSET_VERTICAL:
+ g_value_set_int( value, foobar_inset_container_get_inset_vertical( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_inset_container_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarInsetContainer* self = (FoobarInsetContainer*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_CHILD:
+ foobar_inset_container_set_child( self, g_value_get_object( value ) );
+ break;
+ case PROP_INSET_HORIZONTAL:
+ foobar_inset_container_set_inset_horizontal( self, g_value_get_int( value ) );
+ break;
+ case PROP_INSET_VERTICAL:
+ foobar_inset_container_set_inset_vertical( self, g_value_get_int( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for inset containers.
+//
+void foobar_inset_container_dispose( GObject* object )
+{
+ FoobarInsetContainer* self = (FoobarInsetContainer*)object;
+
+ g_clear_pointer( &self->child, gtk_widget_unparent );
+
+ G_OBJECT_CLASS( foobar_inset_container_parent_class )->dispose( object );
+}
+
+//
+// Determine the size request mode used to measure the widget.
+//
+// This implementation just forwards the request mode from the child widget.
+//
+GtkSizeRequestMode foobar_inset_container_get_request_mode( GtkWidget* widget )
+{
+ FoobarInsetContainer* self = (FoobarInsetContainer*)widget;
+ if ( !self->child ) { return GTK_SIZE_REQUEST_CONSTANT_SIZE; }
+
+ return gtk_widget_get_request_mode( self->child );
+}
+
+//
+// Compute the requested size of the widget.
+//
+// This implementation uses the size requested by the child widget and then adds the inset to it.
+//
+void foobar_inset_container_measure(
+ GtkWidget* widget,
+ GtkOrientation orientation,
+ int for_size,
+ int* minimum,
+ int* natural,
+ int* minimum_baseline,
+ int* natural_baseline )
+{
+ FoobarInsetContainer* self = (FoobarInsetContainer*)widget;
+ if ( !self->child )
+ {
+ *minimum = 0;
+ *natural = 0;
+ *minimum_baseline = 0;
+ *natural_baseline = 0;
+ return;
+ }
+
+ // Get the inset for the given orientation and the other one.
+
+ gint inset;
+ gint other_inset;
+ switch ( orientation )
+ {
+ case GTK_ORIENTATION_HORIZONTAL:
+ inset = self->inset_horizontal;
+ other_inset = self->inset_vertical;
+ break;
+ case GTK_ORIENTATION_VERTICAL:
+ inset = self->inset_vertical;
+ other_inset = self->inset_horizontal;
+ break;
+ default:
+ g_warn_if_reached( );
+ return;
+ }
+
+ // Compute the new size, taking into consideration that the inset might have already been added to measure the other
+ // side and needs to be subtracted before passing it to the child widget's measure function.
+
+ for_size = MAX( 0, for_size - ( 2 * other_inset ) );
+ gtk_widget_measure( self->child, orientation, for_size, minimum, natural, minimum_baseline, natural_baseline );
+ *minimum = MAX( 0, *minimum + ( 2 * inset ) );
+ *natural = MAX( 0, *natural + ( 2 * inset ) );
+ if ( orientation == GTK_ORIENTATION_VERTICAL )
+ {
+ *minimum_baseline = MAX( 0, *minimum_baseline + inset );
+ *natural_baseline = MAX( 0, *natural_baseline + inset );
+ }
+}
+
+//
+// Arrange the child widget.
+//
+void foobar_inset_container_size_allocate(
+ GtkWidget* widget,
+ int width,
+ int height,
+ int baseline )
+{
+ FoobarInsetContainer* self = (FoobarInsetContainer*)widget;
+ if ( !self->child ) { return; }
+
+ GtkAllocation allocation =
+ {
+ .width = MAX( 0, width - 2 * self->inset_horizontal ),
+ .height = MAX( 0, height - 2 * self->inset_vertical ),
+ .x = self->inset_horizontal,
+ .y = self->inset_vertical,
+ };
+ gtk_widget_size_allocate( self->child, &allocation, MAX( 0, baseline + self->inset_vertical ) );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new inset container instance.
+//
+GtkWidget* foobar_inset_container_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_INSET_CONTAINER, NULL );
+}
+
+//
+// Get the container's child widget.
+//
+GtkWidget* foobar_inset_container_get_child( FoobarInsetContainer* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_INSET_CONTAINER( self ), NULL );
+ return self->child;
+}
+
+//
+// Get the inset applied to the left and right side of the widget.
+//
+gint foobar_inset_container_get_inset_horizontal( FoobarInsetContainer* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_INSET_CONTAINER( self ), 0 );
+ return self->inset_horizontal;
+}
+
+//
+// Get the inset applied to the top and bottom side of the widget.
+//
+gint foobar_inset_container_get_inset_vertical( FoobarInsetContainer* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_INSET_CONTAINER( self ), 0 );
+ return self->inset_vertical;
+}
+
+//
+// Update the container's child widget.
+//
+void foobar_inset_container_set_child(
+ FoobarInsetContainer* self,
+ GtkWidget* child )
+{
+ g_return_if_fail( FOOBAR_IS_INSET_CONTAINER( self ) );
+ g_return_if_fail( child == NULL || self->child == child || gtk_widget_get_parent( child ) == NULL );
+
+ if ( self->child != child )
+ {
+ g_clear_pointer( &self->child, gtk_widget_unparent );
+
+ if ( child )
+ {
+ self->child = child;
+ gtk_widget_set_parent( child, GTK_WIDGET( self ) );
+ }
+
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] );
+ }
+}
+
+//
+// Update the inset applied to the left and right side of the widget.
+//
+void foobar_inset_container_set_inset_horizontal(
+ FoobarInsetContainer* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_INSET_CONTAINER( self ) );
+
+ if ( self->inset_horizontal != value )
+ {
+ self->inset_horizontal = value;
+ gtk_widget_queue_resize( GTK_WIDGET( self ) );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_HORIZONTAL] );
+ }
+}
+
+//
+// Update the inset applied to the top and bottom side of the widget.
+//
+void foobar_inset_container_set_inset_vertical(
+ FoobarInsetContainer* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_INSET_CONTAINER( self ) );
+
+ if ( self->inset_vertical != value )
+ {
+ self->inset_vertical = value;
+ gtk_widget_queue_resize( GTK_WIDGET( self ) );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_VERTICAL] );
+ }
+} \ No newline at end of file
diff --git a/src/widgets/inset-container.h b/src/widgets/inset-container.h
new file mode 100644
index 0000000..af0e6b1
--- /dev/null
+++ b/src/widgets/inset-container.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_INSET_CONTAINER foobar_inset_container_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarInsetContainer, foobar_inset_container, FOOBAR, INSET_CONTAINER, GtkWidget )
+
+GtkWidget* foobar_inset_container_new ( void );
+GtkWidget* foobar_inset_container_get_child ( FoobarInsetContainer* self );
+gint foobar_inset_container_get_inset_horizontal( FoobarInsetContainer* self );
+gint foobar_inset_container_get_inset_vertical ( FoobarInsetContainer* self );
+void foobar_inset_container_set_child ( FoobarInsetContainer* self,
+ GtkWidget* value );
+void foobar_inset_container_set_inset_horizontal( FoobarInsetContainer* self,
+ gint value );
+void foobar_inset_container_set_inset_vertical ( FoobarInsetContainer* self,
+ gint value );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/limit-container.c b/src/widgets/limit-container.c
new file mode 100644
index 0000000..d0686ef
--- /dev/null
+++ b/src/widgets/limit-container.c
@@ -0,0 +1,341 @@
+#include "widgets/limit-container.h"
+
+//
+// FoobarLimitContainer:
+//
+// A container which uses the natural size requested by the child as its minimum size, but only up to a certain point.
+//
+// This is useful because gtk_scrolled_window_set_max_content_height doesn't seem to be doing what I understand it to
+// do. This way, we can set propagate_natural_height to TRUE and then limit the height through this container.
+//
+
+struct _FoobarLimitContainer
+{
+ GtkWidget parent_instance;
+ GtkWidget* child;
+ gint max_width;
+ gint max_height;
+};
+
+enum
+{
+ PROP_CHILD = 1,
+ PROP_MAX_WIDTH,
+ PROP_MAX_HEIGHT,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_limit_container_class_init ( FoobarLimitContainerClass* klass );
+static void foobar_limit_container_init ( FoobarLimitContainer* self );
+static void foobar_limit_container_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_limit_container_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_limit_container_dispose ( GObject* object );
+static GtkSizeRequestMode foobar_limit_container_get_request_mode( GtkWidget* widget );
+static void foobar_limit_container_measure ( GtkWidget* widget,
+ GtkOrientation orientation,
+ int for_size,
+ int* minimum,
+ int* natural,
+ int* minimum_baseline,
+ int* natural_baseline );
+static void foobar_limit_container_size_allocate ( GtkWidget* widget,
+ int width,
+ int height,
+ int baseline );
+
+G_DEFINE_FINAL_TYPE( FoobarLimitContainer, foobar_limit_container, GTK_TYPE_WIDGET )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Widget Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for limit containers.
+//
+void foobar_limit_container_class_init( FoobarLimitContainerClass* klass )
+{
+ GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass );
+ widget_klass->get_request_mode = foobar_limit_container_get_request_mode;
+ widget_klass->measure = foobar_limit_container_measure;
+ widget_klass->size_allocate = foobar_limit_container_size_allocate;
+ gtk_widget_class_set_css_name( widget_klass, "limit-container" );
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_limit_container_get_property;
+ object_klass->set_property = foobar_limit_container_set_property;
+ object_klass->dispose = foobar_limit_container_dispose;
+
+ props[PROP_CHILD] = g_param_spec_object(
+ "child",
+ "Child",
+ "Child widget for the panel item.",
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE );
+ props[PROP_MAX_WIDTH] = g_param_spec_int(
+ "max-width",
+ "Max Width",
+ "The maximum natural width supported as a minimum size.",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_MAX_HEIGHT] = g_param_spec_int(
+ "max-height",
+ "Max Height",
+ "The maximum natural height supported as a minimum size.",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for limit containers.
+//
+void foobar_limit_container_init( FoobarLimitContainer* self )
+{
+ (void)self;
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_limit_container_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarLimitContainer* self = (FoobarLimitContainer*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_CHILD:
+ g_value_set_object( value, foobar_limit_container_get_child( self ) );
+ break;
+ case PROP_MAX_WIDTH:
+ g_value_set_int( value, foobar_limit_container_get_max_width( self ) );
+ break;
+ case PROP_MAX_HEIGHT:
+ g_value_set_int( value, foobar_limit_container_get_max_height( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_limit_container_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarLimitContainer* self = (FoobarLimitContainer*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_CHILD:
+ foobar_limit_container_set_child( self, g_value_get_object( value ) );
+ break;
+ case PROP_MAX_WIDTH:
+ foobar_limit_container_set_max_width( self, g_value_get_int( value ) );
+ break;
+ case PROP_MAX_HEIGHT:
+ foobar_limit_container_set_max_height( self, g_value_get_int( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for limit containers.
+//
+void foobar_limit_container_dispose( GObject* object )
+{
+ FoobarLimitContainer* self = (FoobarLimitContainer*)object;
+
+ g_clear_pointer( &self->child, gtk_widget_unparent );
+
+ G_OBJECT_CLASS( foobar_limit_container_parent_class )->dispose( object );
+}
+
+//
+// Determine the size request mode used to measure the widget.
+//
+// This implementation just forwards the request mode from the child widget.
+//
+GtkSizeRequestMode foobar_limit_container_get_request_mode( GtkWidget* widget )
+{
+ FoobarLimitContainer* self = (FoobarLimitContainer*)widget;
+ if ( !self->child ) { return GTK_SIZE_REQUEST_CONSTANT_SIZE; }
+
+ return gtk_widget_get_request_mode( self->child );
+}
+
+//
+// Compute the requested size of the widget.
+//
+void foobar_limit_container_measure(
+ GtkWidget* widget,
+ GtkOrientation orientation,
+ int for_size,
+ int* minimum,
+ int* natural,
+ int* minimum_baseline,
+ int* natural_baseline )
+{
+ FoobarLimitContainer* self = (FoobarLimitContainer*)widget;
+ if ( !self->child )
+ {
+ *minimum = 0;
+ *natural = 0;
+ *minimum_baseline = 0;
+ *natural_baseline = 0;
+ return;
+ }
+
+ gtk_widget_measure( self->child, orientation, for_size, minimum, natural, minimum_baseline, natural_baseline );
+ gint natural_minimum = *natural;
+ switch ( orientation )
+ {
+ case GTK_ORIENTATION_HORIZONTAL:
+ natural_minimum = MIN( natural_minimum, self->max_width );
+ break;
+ case GTK_ORIENTATION_VERTICAL:
+ natural_minimum = MIN( natural_minimum, self->max_height );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+ *minimum = MAX( *minimum, natural_minimum );
+}
+
+//
+// Arrange the child widget.
+//
+void foobar_limit_container_size_allocate(
+ GtkWidget* widget,
+ int width,
+ int height,
+ int baseline )
+{
+ FoobarLimitContainer* self = (FoobarLimitContainer*)widget;
+ if ( !self->child ) { return; }
+
+ gtk_widget_allocate( self->child, width, height, baseline, NULL );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new limit container instance.
+//
+GtkWidget* foobar_limit_container_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_LIMIT_CONTAINER, NULL );
+}
+
+//
+// Get the container's child widget.
+//
+GtkWidget* foobar_limit_container_get_child( FoobarLimitContainer* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ), NULL );
+ return self->child;
+}
+
+//
+// Get the maximum natural width of the child to use as the minimum width for this container.
+//
+gint foobar_limit_container_get_max_width( FoobarLimitContainer* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ), 0 );
+ return self->max_width;
+}
+
+//
+// Get the maximum natural height of the child to use as the minimum height for this container.
+//
+gint foobar_limit_container_get_max_height( FoobarLimitContainer* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ), 0 );
+ return self->max_height;
+}
+
+//
+// Update the container's child widget.
+//
+void foobar_limit_container_set_child(
+ FoobarLimitContainer* self,
+ GtkWidget* child )
+{
+ g_return_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ) );
+ g_return_if_fail( child == NULL || self->child == child || gtk_widget_get_parent( child ) == NULL );
+
+ if ( self->child != child )
+ {
+ g_clear_pointer( &self->child, gtk_widget_unparent );
+
+ if ( child )
+ {
+ self->child = child;
+ gtk_widget_set_parent( child, GTK_WIDGET( self ) );
+ }
+
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] );
+ }
+}
+
+//
+// Update the maximum natural width of the child to use as the minimum width for this container.
+//
+void foobar_limit_container_set_max_width(
+ FoobarLimitContainer* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ) );
+ g_return_if_fail( value >= 0 );
+
+ if ( self->max_width != value )
+ {
+ self->max_width = value;
+ gtk_widget_queue_resize( GTK_WIDGET( self ) );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_MAX_WIDTH] );
+ }
+}
+
+//
+// Update the maximum natural height of the child to use as the minimum height for this container.
+//
+void foobar_limit_container_set_max_height(
+ FoobarLimitContainer* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ) );
+ g_return_if_fail( value >= 0 );
+
+ if ( self->max_height != value )
+ {
+ self->max_height = value;
+ gtk_widget_queue_resize( GTK_WIDGET( self ) );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_MAX_HEIGHT] );
+ }
+} \ No newline at end of file
diff --git a/src/widgets/limit-container.h b/src/widgets/limit-container.h
new file mode 100644
index 0000000..c5e7e70
--- /dev/null
+++ b/src/widgets/limit-container.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_LIMIT_CONTAINER foobar_limit_container_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarLimitContainer, foobar_limit_container, FOOBAR, LIMIT_CONTAINER, GtkWidget )
+
+GtkWidget* foobar_limit_container_new ( void );
+GtkWidget* foobar_limit_container_get_child ( FoobarLimitContainer* self );
+gint foobar_limit_container_get_max_width ( FoobarLimitContainer* self );
+gint foobar_limit_container_get_max_height( FoobarLimitContainer* self );
+void foobar_limit_container_set_child ( FoobarLimitContainer* self,
+ GtkWidget* value );
+void foobar_limit_container_set_max_width ( FoobarLimitContainer* self,
+ gint value );
+void foobar_limit_container_set_max_height( FoobarLimitContainer* self,
+ gint value );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/meson.build b/src/widgets/meson.build
new file mode 100644
index 0000000..c6baefc
--- /dev/null
+++ b/src/widgets/meson.build
@@ -0,0 +1,8 @@
+foobar_sources += files(
+ 'inset-container.c',
+ 'limit-container.c',
+ 'notification-widget.c',
+)
+
+subdir('control-center')
+subdir('panel') \ No newline at end of file
diff --git a/src/widgets/notification-widget.c b/src/widgets/notification-widget.c
new file mode 100644
index 0000000..477a57c
--- /dev/null
+++ b/src/widgets/notification-widget.c
@@ -0,0 +1,821 @@
+#include "widgets/notification-widget.h"
+
+//
+// FoobarNotificationCloseAction:
+//
+// The action invoked when the user requests to "close" the notification. The notification is either marked as
+// "dismissed" and kept around or removed from the list.
+//
+
+G_DEFINE_ENUM_TYPE(
+ FoobarNotificationCloseAction,
+ foobar_notification_close_action,
+ G_DEFINE_ENUM_VALUE( FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE, "remove" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS, "dismiss" ) )
+
+//
+// FoobarNotificationWidget:
+//
+// Shared widget for displaying a notification. The widget also manages an inset/margin -- this is done to allow the
+// "close" button to extend into the margin area.
+//
+
+struct _FoobarNotificationWidget
+{
+ GtkWidget parent_instance;
+ GtkWidget* container;
+ GtkWidget* content;
+ GtkWidget* close_button;
+ FoobarNotification* notification;
+ FoobarNotificationCloseAction close_action;
+ gchar* time_format;
+ gint min_height;
+ gint close_button_inset;
+ gint inset_start;
+ gint inset_end;
+ gint inset_top;
+ gint inset_bottom;
+};
+
+enum
+{
+ PROP_NOTIFICATION = 1,
+ PROP_CLOSE_ACTION,
+ PROP_TIME_FORMAT,
+ PROP_MIN_HEIGHT,
+ PROP_CLOSE_BUTTON_INSET,
+ PROP_INSET_START,
+ PROP_INSET_END,
+ PROP_INSET_TOP,
+ PROP_INSET_BOTTOM,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_notification_widget_class_init ( FoobarNotificationWidgetClass* klass );
+static void foobar_notification_widget_init ( FoobarNotificationWidget* self );
+static void foobar_notification_widget_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_notification_widget_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_notification_widget_dispose ( GObject* object );
+static void foobar_notification_widget_finalize ( GObject* object );
+static void foobar_notification_widget_handle_enter ( GtkEventControllerMotion* controller,
+ gdouble x,
+ gdouble y,
+ gpointer userdata );
+static void foobar_notification_widget_handle_leave ( GtkEventControllerMotion* controller,
+ gpointer userdata );
+static void foobar_notification_widget_handle_close_clicked( GtkButton* button,
+ gpointer userdata );
+static gchar* foobar_notification_widget_compute_time_label ( GtkExpression* expression,
+ GDateTime* time,
+ gchar const* format,
+ gpointer userdata );
+static gboolean foobar_notification_widget_compute_icon_visible( GtkExpression* expression,
+ GIcon* icon,
+ gpointer userdata );
+static gboolean foobar_notification_widget_compute_body_visible( GtkExpression* expression,
+ gchar const* body,
+ gpointer userdata );
+static void foobar_notification_widget_update_margins ( FoobarNotificationWidget* self );
+
+G_DEFINE_FINAL_TYPE( FoobarNotificationWidget, foobar_notification_widget, GTK_TYPE_WIDGET )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Widget Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for notifications.
+//
+void foobar_notification_widget_class_init( FoobarNotificationWidgetClass* klass )
+{
+ GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass );
+ gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BIN_LAYOUT );
+ gtk_widget_class_set_css_name( widget_klass, "notification" );
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_notification_widget_get_property;
+ object_klass->set_property = foobar_notification_widget_set_property;
+ object_klass->dispose = foobar_notification_widget_dispose;
+ object_klass->finalize = foobar_notification_widget_finalize;
+
+ props[PROP_NOTIFICATION] = g_param_spec_object(
+ "notification",
+ "Notification",
+ "Content to display.",
+ FOOBAR_TYPE_NOTIFICATION,
+ G_PARAM_READWRITE );
+ props[PROP_CLOSE_ACTION] = g_param_spec_enum(
+ "close-action",
+ "Close Action",
+ "Action to execute when the notification is closed.",
+ FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_TIME_FORMAT] = g_param_spec_string(
+ "time-format",
+ "Time Format",
+ "Time format used to display the notification's timestamp.",
+ NULL,
+ G_PARAM_READWRITE );
+ props[PROP_MIN_HEIGHT] = g_param_spec_int(
+ "min-height",
+ "Minimum Height",
+ "Minimum requested height for the notification itself.",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_CLOSE_BUTTON_INSET] = g_param_spec_int(
+ "close-button-inset",
+ "Close Button Inset",
+ "Offset of the close button within the notification (may be negative, possibly increasing the actual inset).",
+ INT_MIN,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_INSET_START] = g_param_spec_int(
+ "inset-start",
+ "Inset Start",
+ "Inset for the start edge of the widget.",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_INSET_END] = g_param_spec_int(
+ "inset-end",
+ "Inset End",
+ "Inset for the end edge of the widget.",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_INSET_TOP] = g_param_spec_int(
+ "inset-top",
+ "Inset Top",
+ "Inset for the top edge of the widget.",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_INSET_BOTTOM] = g_param_spec_int(
+ "inset-bottom",
+ "Inset Bottom",
+ "Inset for the bottom edge of the widget.",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for notifications.
+//
+void foobar_notification_widget_init( FoobarNotificationWidget* self )
+{
+ // Set up the main notification content.
+
+ GtkWidget* icon = gtk_image_new( );
+ gtk_widget_add_css_class( icon, "icon" );
+ gtk_widget_set_valign( icon, GTK_ALIGN_CENTER );
+
+ GtkWidget* title = gtk_label_new( NULL );
+ gtk_label_set_justify( GTK_LABEL( title ), GTK_JUSTIFY_LEFT );
+ gtk_label_set_ellipsize( GTK_LABEL( title ), PANGO_ELLIPSIZE_END );
+ gtk_label_set_wrap( GTK_LABEL( title ), FALSE );
+ gtk_label_set_xalign( GTK_LABEL( title ), 0 );
+ gtk_widget_add_css_class( title, "title" );
+ gtk_widget_set_valign( title, GTK_ALIGN_BASELINE_CENTER );
+ gtk_widget_set_hexpand( title, TRUE );
+
+ GtkWidget* time = gtk_label_new( NULL );
+ gtk_label_set_wrap( GTK_LABEL( time ), FALSE );
+ gtk_widget_add_css_class( time, "time" );
+ gtk_widget_set_valign( time, GTK_ALIGN_BASELINE_CENTER );
+
+ GtkWidget* body = gtk_label_new( NULL );
+ gtk_label_set_use_markup( GTK_LABEL( body ), TRUE );
+ gtk_label_set_justify( GTK_LABEL( body ), GTK_JUSTIFY_LEFT );
+ gtk_label_set_ellipsize( GTK_LABEL( body ), PANGO_ELLIPSIZE_END );
+ gtk_label_set_wrap( GTK_LABEL( body ), TRUE );
+ gtk_label_set_wrap_mode( GTK_LABEL( body ), PANGO_WRAP_WORD_CHAR );
+ // XXX: Increase once layout is fixed in GTK (https://gitlab.gnome.org/GNOME/gtk/-/issues/5885)
+ gtk_label_set_lines( GTK_LABEL( body ), 1 );
+ gtk_label_set_xalign( GTK_LABEL( body ), 0 );
+ gtk_widget_add_css_class( body, "body" );
+
+ GtkWidget* header = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_box_append( GTK_BOX( header ), title );
+ gtk_box_append( GTK_BOX( header ), time );
+
+ GtkWidget* row = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_box_append( GTK_BOX( row ), header );
+ gtk_box_append( GTK_BOX( row ), body );
+ gtk_widget_set_hexpand( row, TRUE );
+ gtk_widget_set_valign( row, GTK_ALIGN_CENTER );
+
+ self->content = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_box_append( GTK_BOX( self->content ), icon );
+ gtk_box_append( GTK_BOX( self->content ), row );
+ gtk_widget_add_css_class( self->content, "content" );
+
+ // Set up the overlay for the close button.
+
+ GtkWidget* close_icon = gtk_image_new_from_icon_name( "fluent-dismiss-symbolic" );
+ gtk_image_set_pixel_size( GTK_IMAGE( close_icon ), 12 );
+
+ self->close_button = gtk_button_new( );
+ gtk_button_set_child( GTK_BUTTON( self->close_button ), close_icon );
+ gtk_widget_add_css_class( self->close_button, "close-button" );
+ gtk_widget_set_visible( self->close_button, FALSE );
+ gtk_widget_set_halign( self->close_button, GTK_ALIGN_END );
+ gtk_widget_set_valign( self->close_button, GTK_ALIGN_START );
+ g_signal_connect( self->close_button, "clicked", G_CALLBACK( foobar_notification_widget_handle_close_clicked ), self );
+
+ GtkEventController* hover_controller = gtk_event_controller_motion_new( );
+ g_signal_connect( hover_controller, "enter", G_CALLBACK( foobar_notification_widget_handle_enter ), self );
+ g_signal_connect( hover_controller, "leave", G_CALLBACK( foobar_notification_widget_handle_leave ), self );
+
+ self->container = gtk_overlay_new( );
+ gtk_overlay_set_child( GTK_OVERLAY( self->container ), self->content );
+ gtk_overlay_add_overlay( GTK_OVERLAY( self->container ), self->close_button );
+ gtk_widget_add_controller( self->container, hover_controller );
+ gtk_widget_set_parent( self->container, GTK_WIDGET( self ) );
+
+ // Set up bindings.
+
+ {
+ GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" );
+ GtkExpression* image_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "image" );
+ gtk_expression_bind( gtk_expression_ref( image_expr ), icon, "gicon", self );
+ GtkExpression* visible_params[] = { image_expr };
+ GtkExpression* visible_expr = gtk_cclosure_expression_new(
+ G_TYPE_BOOLEAN,
+ NULL,
+ G_N_ELEMENTS( visible_params ),
+ visible_params,
+ G_CALLBACK( foobar_notification_widget_compute_icon_visible ),
+ NULL,
+ NULL );
+ gtk_expression_bind( visible_expr, icon, "visible", self );
+ }
+
+ {
+ GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" );
+ GtkExpression* summary_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "summary" );
+ gtk_expression_bind( summary_expr, title, "label", self );
+ }
+
+ {
+ GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" );
+ GtkExpression* body_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "body" );
+ gtk_expression_bind( gtk_expression_ref( body_expr ), body, "label", self );
+ GtkExpression* visible_params[] = { body_expr };
+ GtkExpression* visible_expr = gtk_cclosure_expression_new(
+ G_TYPE_BOOLEAN,
+ NULL,
+ G_N_ELEMENTS( visible_params ),
+ visible_params,
+ G_CALLBACK( foobar_notification_widget_compute_body_visible ),
+ NULL,
+ NULL );
+ gtk_expression_bind( visible_expr, body, "visible", self );
+ }
+
+ {
+ GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" );
+ GtkExpression* time_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "time" );
+ GtkExpression* format_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "time-format" );
+ GtkExpression* str_params[] = { time_expr, format_expr };
+ GtkExpression* str_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( str_params ),
+ str_params,
+ G_CALLBACK( foobar_notification_widget_compute_time_label ),
+ self,
+ NULL );
+ gtk_expression_bind( str_expr, time, "label", self );
+ }
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_notification_widget_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_NOTIFICATION:
+ g_value_set_object( value, foobar_notification_widget_get_notification( self ) );
+ break;
+ case PROP_CLOSE_ACTION:
+ g_value_set_enum( value, foobar_notification_widget_get_close_action( self ) );
+ break;
+ case PROP_TIME_FORMAT:
+ g_value_set_string( value, foobar_notification_widget_get_time_format( self ) );
+ break;
+ case PROP_MIN_HEIGHT:
+ g_value_set_int( value, foobar_notification_widget_get_min_height( self ) );
+ break;
+ case PROP_CLOSE_BUTTON_INSET:
+ g_value_set_int( value, foobar_notification_widget_get_close_button_inset( self ) );
+ break;
+ case PROP_INSET_START:
+ g_value_set_int( value, foobar_notification_widget_get_inset_start( self ) );
+ break;
+ case PROP_INSET_END:
+ g_value_set_int( value, foobar_notification_widget_get_inset_end( self ) );
+ break;
+ case PROP_INSET_TOP:
+ g_value_set_int( value, foobar_notification_widget_get_inset_top( self ) );
+ break;
+ case PROP_INSET_BOTTOM:
+ g_value_set_int( value, foobar_notification_widget_get_inset_bottom( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_notification_widget_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_NOTIFICATION:
+ foobar_notification_widget_set_notification( self, g_value_get_object( value ) );
+ break;
+ case PROP_CLOSE_ACTION:
+ foobar_notification_widget_set_close_action( self, g_value_get_enum( value ) );
+ break;
+ case PROP_TIME_FORMAT:
+ foobar_notification_widget_set_time_format( self, g_value_get_string( value ) );
+ break;
+ case PROP_MIN_HEIGHT:
+ foobar_notification_widget_set_min_height( self, g_value_get_int( value ) );
+ break;
+ case PROP_CLOSE_BUTTON_INSET:
+ foobar_notification_widget_set_close_button_inset( self, g_value_get_int( value ) );
+ break;
+ case PROP_INSET_START:
+ foobar_notification_widget_set_inset_start( self, g_value_get_int( value ) );
+ break;
+ case PROP_INSET_END:
+ foobar_notification_widget_set_inset_end( self, g_value_get_int( value ) );
+ break;
+ case PROP_INSET_TOP:
+ foobar_notification_widget_set_inset_top( self, g_value_get_int( value ) );
+ break;
+ case PROP_INSET_BOTTOM:
+ foobar_notification_widget_set_inset_bottom( self, g_value_get_int( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for notifications.
+//
+void foobar_notification_widget_dispose( GObject* object )
+{
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)object;
+
+ GtkWidget* child;
+ while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) )
+ {
+ gtk_widget_unparent( child );
+ }
+
+ G_OBJECT_CLASS( foobar_notification_widget_parent_class )->dispose( object );
+}
+
+//
+// Instance cleanup for notifications.
+//
+void foobar_notification_widget_finalize( GObject* object )
+{
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)object;
+
+ g_clear_object( &self->notification );
+ g_clear_pointer( &self->time_format, g_free );
+
+ G_OBJECT_CLASS( foobar_notification_widget_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new notification widget instance.
+//
+GtkWidget* foobar_notification_widget_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL );
+}
+
+//
+// Get the notification model currently displayed.
+//
+FoobarNotification* foobar_notification_widget_get_notification( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), NULL );
+ return self->notification;
+}
+
+//
+// Get the action invoked when the user wants to "close" a notification.
+//
+FoobarNotificationCloseAction foobar_notification_widget_get_close_action( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->close_action;
+}
+
+//
+// Get the time format string used to display the notification's timestamp.
+//
+gchar const* foobar_notification_widget_get_time_format( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), NULL );
+ return self->time_format;
+}
+
+//
+// Get the minimum height of the notification (excluding the inset).
+//
+gint foobar_notification_widget_get_min_height( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->min_height;
+}
+
+//
+// Get the offset of the close button within the notification (may be negative, possibly increasing the actual inset).
+//
+gint foobar_notification_widget_get_close_button_inset( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->close_button_inset;
+}
+
+//
+// Get the margin for the start edge of the notification.
+//
+gint foobar_notification_widget_get_inset_start( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->inset_start;
+}
+
+//
+// Get the margin for the end edge of the notification.
+//
+gint foobar_notification_widget_get_inset_end( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->inset_end;
+}
+
+//
+// Get the margin for the top edge of the notification.
+//
+gint foobar_notification_widget_get_inset_top( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->inset_top;
+}
+
+//
+// Get the margin for the bottom edge of the notification.
+//
+gint foobar_notification_widget_get_inset_bottom( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->inset_bottom;
+}
+
+//
+// Update the notification model currently displayed.
+//
+void foobar_notification_widget_set_notification(
+ FoobarNotificationWidget* self,
+ FoobarNotification* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ if ( self->notification != value )
+ {
+ g_clear_object( &self->notification );
+ if ( value ) { self->notification = g_object_ref( value ); }
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_NOTIFICATION] );
+ }
+}
+
+//
+// Update the action invoked when the user wants to "close" a notification.
+//
+void foobar_notification_widget_set_close_action(
+ FoobarNotificationWidget* self,
+ FoobarNotificationCloseAction value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ if ( self->close_action != value )
+ {
+ self->close_action = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CLOSE_ACTION] );
+ }
+}
+
+//
+// Update the time format string used to display the notification's timestamp.
+//
+void foobar_notification_widget_set_time_format(
+ FoobarNotificationWidget* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ if ( g_strcmp0( self->time_format, value ) )
+ {
+ g_clear_pointer( &self->time_format, g_free );
+ self->time_format = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_TIME_FORMAT] );
+ }
+}
+
+//
+// Update the minimum height of the notification (excluding the inset).
+//
+void foobar_notification_widget_set_min_height(
+ FoobarNotificationWidget* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ value = MAX( value, 0 );
+ if ( self->min_height != value )
+ {
+ self->min_height = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_MIN_HEIGHT] );
+ gtk_widget_set_size_request( self->content, -1, self->min_height );
+ }
+}
+
+//
+// Update the offset of the close button within the notification (may be negative, possibly increasing the actual inset).
+//
+void foobar_notification_widget_set_close_button_inset(
+ FoobarNotificationWidget* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ if ( self->close_button_inset != value )
+ {
+ self->close_button_inset = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CLOSE_BUTTON_INSET] );
+ foobar_notification_widget_update_margins( self );
+ }
+}
+
+//
+// Update the margin for the start edge of the notification.
+//
+void foobar_notification_widget_set_inset_start(
+ FoobarNotificationWidget* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ value = MAX( value, 0 );
+ if ( self->inset_start != value )
+ {
+ self->inset_start = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_START] );
+ foobar_notification_widget_update_margins( self );
+ }
+}
+
+//
+// Update the margin for the end edge of the notification.
+//
+void foobar_notification_widget_set_inset_end(
+ FoobarNotificationWidget* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ value = MAX( value, 0 );
+ if ( self->inset_end != value )
+ {
+ self->inset_end = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_END] );
+ foobar_notification_widget_update_margins( self );
+ }
+}
+
+//
+// Update the margin for the top edge of the notification.
+//
+void foobar_notification_widget_set_inset_top(
+ FoobarNotificationWidget* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ value = MAX( value, 0 );
+ if ( self->inset_top != value )
+ {
+ self->inset_top = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_TOP] );
+ foobar_notification_widget_update_margins( self );
+ }
+}
+
+//
+// Update the margin for the bottom edge of the notification.
+//
+void foobar_notification_widget_set_inset_bottom(
+ FoobarNotificationWidget* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ value = MAX( value, 0 );
+ if ( self->inset_bottom != value )
+ {
+ self->inset_bottom = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_BOTTOM] );
+ foobar_notification_widget_update_margins( self );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called when the mouse cursor enters the notification.
+//
+// We will temporarily block the dismissal timeout for the notification and show the close button.
+//
+void foobar_notification_widget_handle_enter(
+ GtkEventControllerMotion* controller,
+ gdouble x,
+ gdouble y,
+ gpointer userdata )
+{
+ (void)controller;
+ (void)x;
+ (void)y;
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)userdata;
+
+ gtk_widget_set_visible( self->close_button, TRUE );
+ if ( self->notification ) { foobar_notification_block_timeout( self->notification ); }
+}
+
+//
+// Called when the mouse cursor leaves the notification.
+//
+// We will unblock the dismissal timeout for the notification and hide the close button.
+//
+void foobar_notification_widget_handle_leave(
+ GtkEventControllerMotion* controller,
+ gpointer userdata )
+{
+ (void)controller;
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)userdata;
+
+ gtk_widget_set_visible( self->close_button, FALSE );
+ if ( self->notification ) { foobar_notification_resume_timeout( self->notification ); }
+}
+
+//
+// Called when the user has clicked the "close" button.
+//
+// This either marks the notification as dismissed or removes it.
+//
+void foobar_notification_widget_handle_close_clicked(
+ GtkButton* button,
+ gpointer userdata )
+{
+ (void)button;
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)userdata;
+
+ if ( self->notification )
+ {
+ switch ( self->close_action )
+ {
+ case FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE:
+ foobar_notification_close( self->notification );
+ break;
+ case FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS:
+ foobar_notification_dismiss( self->notification );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Value Converters
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Derive the timestamp label value from the notification's timestamp and the selected time format.
+//
+gchar* foobar_notification_widget_compute_time_label(
+ GtkExpression* expression,
+ GDateTime* time,
+ gchar const* format,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ return format != NULL ? g_date_time_format( time, format ) : NULL;
+}
+
+//
+// Derive the visibility of the notification icon from its value.
+//
+gboolean foobar_notification_widget_compute_icon_visible(
+ GtkExpression* expression,
+ GIcon* icon,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ return icon != NULL;
+}
+
+//
+// Derive the visibility of the notification body label from its value.
+//
+gboolean foobar_notification_widget_compute_body_visible(
+ GtkExpression* expression,
+ gchar const* body,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ return body != NULL && g_strcmp0( body, "" );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Update the margins of the content widget and close button to the current inset values.
+//
+void foobar_notification_widget_update_margins( FoobarNotificationWidget* self )
+{
+ gint inset = MAX( self->close_button_inset, 0 );
+ gint overrun = MAX( -self->close_button_inset, 0 );
+ gint actual_inset_end = MAX( self->inset_end, overrun );
+ gint actual_inset_top = MAX( self->inset_top, overrun );
+
+ gtk_widget_set_margin_start( self->container, self->inset_start );
+ gtk_widget_set_margin_end( self->container, actual_inset_end - overrun );
+ gtk_widget_set_margin_top( self->container, actual_inset_top - overrun );
+ gtk_widget_set_margin_bottom( self->container, self->inset_bottom );
+
+ gtk_widget_set_margin_end( self->content, overrun );
+ gtk_widget_set_margin_top( self->content, overrun );
+
+ gtk_widget_set_margin_end( self->close_button, inset );
+ gtk_widget_set_margin_top( self->close_button, inset );
+} \ No newline at end of file
diff --git a/src/widgets/notification-widget.h b/src/widgets/notification-widget.h
new file mode 100644
index 0000000..3c15ee4
--- /dev/null
+++ b/src/widgets/notification-widget.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <gtk/gtk.h>
+#include "services/notification-service.h"
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION foobar_notification_close_action_get_type( )
+#define FOOBAR_TYPE_NOTIFICATION_WIDGET foobar_notification_widget_get_type( )
+
+typedef enum
+{
+ FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE,
+ FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS,
+} FoobarNotificationCloseAction;
+
+GType foobar_notification_close_action_get_type( void );
+
+G_DECLARE_FINAL_TYPE( FoobarNotificationWidget, foobar_notification_widget, FOOBAR, NOTIFICATION_WIDGET, GtkWidget )
+
+GtkWidget* foobar_notification_widget_new ( void );
+FoobarNotification* foobar_notification_widget_get_notification ( FoobarNotificationWidget* self );
+FoobarNotificationCloseAction foobar_notification_widget_get_close_action ( FoobarNotificationWidget* self );
+gchar const* foobar_notification_widget_get_time_format ( FoobarNotificationWidget* self );
+gint foobar_notification_widget_get_min_height ( FoobarNotificationWidget* self );
+gint foobar_notification_widget_get_close_button_inset( FoobarNotificationWidget* self );
+gint foobar_notification_widget_get_inset_start ( FoobarNotificationWidget* self );
+gint foobar_notification_widget_get_inset_end ( FoobarNotificationWidget* self );
+gint foobar_notification_widget_get_inset_top ( FoobarNotificationWidget* self );
+gint foobar_notification_widget_get_inset_bottom ( FoobarNotificationWidget* self );
+void foobar_notification_widget_set_notification ( FoobarNotificationWidget* self,
+ FoobarNotification* value );
+void foobar_notification_widget_set_close_action ( FoobarNotificationWidget* self,
+ FoobarNotificationCloseAction value );
+void foobar_notification_widget_set_time_format ( FoobarNotificationWidget* self,
+ gchar const* value );
+void foobar_notification_widget_set_min_height ( FoobarNotificationWidget* self,
+ gint value );
+void foobar_notification_widget_set_close_button_inset( FoobarNotificationWidget* self,
+ gint value );
+void foobar_notification_widget_set_inset_start ( FoobarNotificationWidget* self,
+ gint value );
+void foobar_notification_widget_set_inset_end ( FoobarNotificationWidget* self,
+ gint value );
+void foobar_notification_widget_set_inset_top ( FoobarNotificationWidget* self,
+ gint value );
+void foobar_notification_widget_set_inset_bottom ( FoobarNotificationWidget* self,
+ gint value );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/panel/meson.build b/src/widgets/panel/meson.build
new file mode 100644
index 0000000..abddd6d
--- /dev/null
+++ b/src/widgets/panel/meson.build
@@ -0,0 +1,7 @@
+foobar_sources += files(
+ 'panel-item.c',
+ 'panel-item-icon.c',
+ 'panel-item-clock.c',
+ 'panel-item-workspaces.c',
+ 'panel-item-status.c',
+) \ No newline at end of file
diff --git a/src/widgets/panel/panel-item-clock.c b/src/widgets/panel/panel-item-clock.c
new file mode 100644
index 0000000..1efd7fa
--- /dev/null
+++ b/src/widgets/panel/panel-item-clock.c
@@ -0,0 +1,152 @@
+#include "widgets/panel/panel-item-clock.h"
+
+//
+// FoobarPanelItemClock:
+//
+// A panel item displaying the current time in the format configured by the user.
+//
+
+struct _FoobarPanelItemClock
+{
+ FoobarPanelItem parent_instance;
+ gchar* format;
+ FoobarPanelItemAction action;
+ GtkWidget* label;
+ GtkWidget* button;
+ FoobarClockService* clock_service;
+};
+
+static void foobar_panel_item_clock_class_init ( FoobarPanelItemClockClass* klass );
+static void foobar_panel_item_clock_init ( FoobarPanelItemClock* self );
+static void foobar_panel_item_clock_finalize ( GObject* object );
+static void foobar_panel_item_clock_handle_clicked( GtkButton* button,
+ gpointer userdata );
+static gchar* foobar_panel_item_clock_compute_label ( GtkExpression* expression,
+ GDateTime* time,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarPanelItemClock, foobar_panel_item_clock, FOOBAR_TYPE_PANEL_ITEM )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Panel Item Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for clock panel items.
+//
+void foobar_panel_item_clock_class_init( FoobarPanelItemClockClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->finalize = foobar_panel_item_clock_finalize;
+}
+
+//
+// Instance initialization for clock panel items.
+//
+void foobar_panel_item_clock_init( FoobarPanelItemClock* self )
+{
+ self->label = gtk_label_new( NULL );
+ gtk_label_set_justify( GTK_LABEL( self->label ), GTK_JUSTIFY_CENTER );
+
+ self->button = gtk_button_new( );
+ gtk_button_set_has_frame( GTK_BUTTON( self->button ), FALSE );
+ gtk_button_set_child( GTK_BUTTON( self->button ), self->label );
+ gtk_widget_set_halign( self->button, GTK_ALIGN_CENTER );
+ gtk_widget_set_valign( self->button, GTK_ALIGN_CENTER );
+ g_signal_connect(
+ self->button,
+ "clicked",
+ G_CALLBACK( foobar_panel_item_clock_handle_clicked ),
+ self );
+
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "clock" );
+ foobar_panel_item_set_child( FOOBAR_PANEL_ITEM( self ), self->button );
+}
+
+//
+// Instance cleanup for clock panel items.
+//
+void foobar_panel_item_clock_finalize( GObject* object )
+{
+ FoobarPanelItemClock* self = (FoobarPanelItemClock*)object;
+
+ g_clear_pointer( &self->format, g_free );
+ g_clear_object( &self->clock_service );
+
+ G_OBJECT_CLASS( foobar_panel_item_clock_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new clock panel item with the given configuration.
+//
+FoobarPanelItem* foobar_panel_item_clock_new(
+ FoobarPanelItemConfiguration const* config,
+ FoobarClockService* clock_service )
+{
+ g_return_val_if_fail( config != NULL, NULL );
+ g_return_val_if_fail( foobar_panel_item_configuration_get_kind( config ) == FOOBAR_PANEL_ITEM_KIND_CLOCK, NULL );
+ g_return_val_if_fail( FOOBAR_IS_CLOCK_SERVICE( clock_service ), NULL );
+
+ FoobarPanelItemClock* self = g_object_new( FOOBAR_TYPE_PANEL_ITEM_CLOCK, NULL );
+ self->format = g_strdup( foobar_panel_item_clock_configuration_get_format( config ) );
+ self->action = foobar_panel_item_configuration_get_action( config );
+ self->clock_service = g_object_ref( clock_service );
+
+ gtk_widget_set_sensitive( self->button, self->action != FOOBAR_PANEL_ITEM_ACTION_NONE );
+
+ {
+ GtkExpression* service_expr = gtk_constant_expression_new( FOOBAR_TYPE_CLOCK_SERVICE, self->clock_service );
+ GtkExpression* time_expr = gtk_property_expression_new( FOOBAR_TYPE_CLOCK_SERVICE, service_expr, "time" );
+ GtkExpression* label_params[] = { time_expr };
+ GtkExpression* label_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( label_params ),
+ label_params,
+ G_CALLBACK( foobar_panel_item_clock_compute_label ),
+ self,
+ NULL );
+ gtk_expression_bind( label_expr, self->label, "label", NULL );
+ }
+
+ return FOOBAR_PANEL_ITEM( self );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called when the button was clicked.
+//
+void foobar_panel_item_clock_handle_clicked(
+ GtkButton* button,
+ gpointer userdata )
+{
+ (void)button;
+ FoobarPanelItemClock* self = (FoobarPanelItemClock*)userdata;
+
+ foobar_panel_item_invoke_action( FOOBAR_PANEL_ITEM( self ), self->action );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Value Converters
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Derive the label from a timestamp, depending on the configured time format.
+//
+gchar* foobar_panel_item_clock_compute_label(
+ GtkExpression* expression,
+ GDateTime* time,
+ gpointer userdata )
+{
+ (void)expression;
+ FoobarPanelItemClock* self = (FoobarPanelItemClock*)userdata;
+
+ return g_date_time_format( time, self->format );
+} \ No newline at end of file
diff --git a/src/widgets/panel/panel-item-clock.h b/src/widgets/panel/panel-item-clock.h
new file mode 100644
index 0000000..244da2f
--- /dev/null
+++ b/src/widgets/panel/panel-item-clock.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "widgets/panel/panel-item.h"
+#include "services/clock-service.h"
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_PANEL_ITEM_CLOCK foobar_panel_item_clock_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarPanelItemClock, foobar_panel_item_clock, FOOBAR, PANEL_ITEM_CLOCK, FoobarPanelItem )
+
+FoobarPanelItem* foobar_panel_item_clock_new( FoobarPanelItemConfiguration const* config,
+ FoobarClockService* clock_service );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/panel/panel-item-icon.c b/src/widgets/panel/panel-item-icon.c
new file mode 100644
index 0000000..327f027
--- /dev/null
+++ b/src/widgets/panel/panel-item-icon.c
@@ -0,0 +1,92 @@
+#include "widgets/panel/panel-item-icon.h"
+
+//
+// FoobarPanelItemIcon:
+//
+// Simple item implementation displaying an icon.
+//
+
+struct _FoobarPanelItemIcon
+{
+ FoobarPanelItem parent_instance;
+ GtkWidget* button;
+ FoobarPanelItemAction action;
+};
+
+static void foobar_panel_item_icon_class_init ( FoobarPanelItemIconClass* klass );
+static void foobar_panel_item_icon_init ( FoobarPanelItemIcon* self );
+static void foobar_panel_item_icon_handle_clicked( GtkButton* button,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarPanelItemIcon, foobar_panel_item_icon, FOOBAR_TYPE_PANEL_ITEM )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Panel Item Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for icon panel items.
+//
+void foobar_panel_item_icon_class_init( FoobarPanelItemIconClass* klass )
+{
+ (void)klass;
+}
+
+//
+// Instance initialization for icon panel items.
+//
+void foobar_panel_item_icon_init( FoobarPanelItemIcon* self )
+{
+ self->button = gtk_button_new( );
+ gtk_button_set_has_frame( GTK_BUTTON( self->button ), FALSE );
+ gtk_widget_set_halign( self->button, GTK_ALIGN_CENTER );
+ gtk_widget_set_valign( self->button, GTK_ALIGN_CENTER );
+ g_signal_connect(
+ self->button,
+ "clicked",
+ G_CALLBACK( foobar_panel_item_icon_handle_clicked ),
+ self );
+
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "icon" );
+ foobar_panel_item_set_child( FOOBAR_PANEL_ITEM( self ), self->button );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new icon panel item with the given configuration.
+//
+FoobarPanelItem* foobar_panel_item_icon_new( FoobarPanelItemConfiguration const* config )
+{
+ g_return_val_if_fail( config != NULL, NULL );
+ g_return_val_if_fail( foobar_panel_item_configuration_get_kind( config ) == FOOBAR_PANEL_ITEM_KIND_ICON, NULL );
+
+ FoobarPanelItemIcon* self = g_object_new( FOOBAR_TYPE_PANEL_ITEM_ICON, NULL );
+ self->action = foobar_panel_item_configuration_get_action( config );
+
+ gtk_widget_set_sensitive( self->button, self->action != FOOBAR_PANEL_ITEM_ACTION_NONE );
+ gtk_button_set_icon_name(
+ GTK_BUTTON( self->button ),
+ foobar_panel_item_icon_configuration_get_icon_name( config ) );
+
+ return FOOBAR_PANEL_ITEM( self );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called when the button was clicked.
+//
+void foobar_panel_item_icon_handle_clicked(
+ GtkButton* button,
+ gpointer userdata )
+{
+ (void)button;
+ FoobarPanelItemIcon* self = (FoobarPanelItemIcon*)userdata;
+
+ foobar_panel_item_invoke_action( FOOBAR_PANEL_ITEM( self ), self->action );
+} \ No newline at end of file
diff --git a/src/widgets/panel/panel-item-icon.h b/src/widgets/panel/panel-item-icon.h
new file mode 100644
index 0000000..77b1144
--- /dev/null
+++ b/src/widgets/panel/panel-item-icon.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "widgets/panel/panel-item.h"
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_PANEL_ITEM_ICON foobar_panel_item_icon_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarPanelItemIcon, foobar_panel_item_icon, FOOBAR, PANEL_ITEM_ICON, FoobarPanelItem )
+
+FoobarPanelItem* foobar_panel_item_icon_new( FoobarPanelItemConfiguration const* config );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/panel/panel-item-status.c b/src/widgets/panel/panel-item-status.c
new file mode 100644
index 0000000..4deacb4
--- /dev/null
+++ b/src/widgets/panel/panel-item-status.c
@@ -0,0 +1,955 @@
+#include "widgets/panel/panel-item-status.h"
+
+//
+// FoobarPanelItemStatus:
+//
+// A panel item showing status indicators for various things such as network state, audio volume, screen brightness,
+// battery level, etc.
+//
+
+struct _FoobarPanelItemStatus
+{
+ FoobarPanelItem parent_instance;
+ FoobarPanelItemAction action;
+ GtkWidget* button;
+ GtkWidget* layout;
+ FoobarBatteryService* battery_service;
+ FoobarBrightnessService* brightness_service;
+ FoobarAudioService* audio_service;
+ FoobarNetworkService* network_service;
+ FoobarBluetoothService* bluetooth_service;
+ FoobarNotificationService* notification_service;
+ GtkWidget* network_icon;
+ GtkWidget* network_label;
+ GtkExpression* network_icon_expr;
+ FoobarNetwork* network;
+ gulong network_handler_id;
+ gulong network_strength_handler_id;
+};
+
+enum
+{
+ PROP_ORIENTATION = 1,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_panel_item_status_class_init ( FoobarPanelItemStatusClass* klass );
+static void foobar_panel_item_status_init ( FoobarPanelItemStatus* self );
+static void foobar_panel_item_status_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_panel_item_status_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_panel_item_status_finalize ( GObject* object );
+static void foobar_panel_item_status_handle_network_change ( FoobarNetworkAdapterWifi* adapter,
+ GParamSpec* pspec,
+ gpointer userdata );
+static void foobar_panel_item_status_handle_network_strength_change( GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata );
+static void foobar_panel_item_status_handle_clicked ( GtkButton* button,
+ gpointer userdata );
+static gboolean foobar_panel_item_status_handle_brightness_scroll ( GtkEventControllerScroll* controller,
+ gdouble dx,
+ gdouble dy,
+ gpointer userdata );
+static gboolean foobar_panel_item_status_handle_audio_scroll ( GtkEventControllerScroll* controller,
+ gdouble dx,
+ gdouble dy,
+ gpointer userdata );
+static gchar* foobar_panel_item_status_compute_network_icon ( GtkExpression* expression,
+ FoobarNetworkAdapter* adapter,
+ FoobarNetworkAdapterState state,
+ gpointer userdata );
+static gchar* foobar_panel_item_status_compute_bluetooth_icon ( GtkExpression* expression,
+ gboolean is_enabled,
+ guint connected_device_count,
+ gpointer userdata );
+static gchar* foobar_panel_item_status_compute_battery_icon ( GtkExpression* expression,
+ FoobarBatteryState const* state,
+ gpointer userdata );
+static gchar* foobar_panel_item_status_compute_battery_label ( GtkExpression* expression,
+ FoobarBatteryState const* state,
+ gpointer userdata );
+static gchar* foobar_panel_item_status_compute_brightness_icon ( GtkExpression* expression,
+ gint percentage,
+ gpointer userdata );
+static gchar* foobar_panel_item_status_compute_brightness_label ( GtkExpression* expression,
+ gint percentage,
+ gpointer userdata );
+static gchar* foobar_panel_item_status_compute_audio_icon ( GtkExpression* expression,
+ gboolean is_available,
+ gint volume,
+ gpointer userdata );
+static gchar* foobar_panel_item_status_compute_audio_label ( GtkExpression* expression,
+ gboolean is_available,
+ gint volume,
+ gpointer userdata );
+static gchar* foobar_panel_item_status_compute_notifications_icon ( GtkExpression* expression,
+ guint count,
+ gpointer userdata );
+static GtkWidget* foobar_panel_item_status_create_item ( FoobarPanelItemStatus* self,
+ FoobarStatusItem item,
+ gboolean show_labels,
+ gboolean enable_scrolling );
+static void foobar_panel_item_status_reevaluate_network ( FoobarPanelItemStatus* self );
+
+G_DEFINE_FINAL_TYPE_WITH_CODE(
+ FoobarPanelItemStatus,
+ foobar_panel_item_status,
+ FOOBAR_TYPE_PANEL_ITEM,
+ G_IMPLEMENT_INTERFACE( GTK_TYPE_ORIENTABLE, NULL ) )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Panel Item Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for status panel items.
+//
+void foobar_panel_item_status_class_init( FoobarPanelItemStatusClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_panel_item_status_get_property;
+ object_klass->set_property = foobar_panel_item_status_set_property;
+ object_klass->finalize = foobar_panel_item_status_finalize;
+
+ gpointer orientable_iface = g_type_default_interface_peek( GTK_TYPE_ORIENTABLE );
+ props[PROP_ORIENTATION] = g_param_spec_override(
+ "orientation",
+ g_object_interface_find_property( orientable_iface, "orientation" ) );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for status panel items.
+//
+void foobar_panel_item_status_init( FoobarPanelItemStatus* self )
+{
+ self->layout = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+
+ self->button = gtk_button_new( );
+ gtk_button_set_has_frame( GTK_BUTTON( self->button ), FALSE );
+ gtk_button_set_child( GTK_BUTTON( self->button ), self->layout );
+ gtk_widget_set_halign( self->button, GTK_ALIGN_CENTER );
+ gtk_widget_set_valign( self->button, GTK_ALIGN_CENTER );
+ g_signal_connect(
+ self->button,
+ "clicked",
+ G_CALLBACK( foobar_panel_item_status_handle_clicked ),
+ self );
+
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "status" );
+ foobar_panel_item_set_child( FOOBAR_PANEL_ITEM( self ), self->button );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_panel_item_status_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_ORIENTATION:
+ g_value_set_enum( value, gtk_orientable_get_orientation( GTK_ORIENTABLE( self->layout ) ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_panel_item_status_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_ORIENTATION:
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( self->layout ), g_value_get_enum( value ) );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ORIENTATION] );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for status panel items.
+//
+void foobar_panel_item_status_finalize( GObject* object )
+{
+ FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)object;
+
+ FoobarNetworkAdapterWifi* adapter = foobar_network_service_get_wifi( self->network_service );
+ g_clear_signal_handler( &self->network_handler_id, adapter );
+ g_clear_signal_handler( &self->network_strength_handler_id, self->network );
+ g_clear_object( &self->network );
+ g_clear_object( &self->battery_service );
+ g_clear_object( &self->brightness_service );
+ g_clear_object( &self->audio_service );
+ g_clear_object( &self->network_service );
+ g_clear_object( &self->bluetooth_service );
+ g_clear_object( &self->notification_service );
+ g_clear_pointer( &self->network_icon_expr, gtk_expression_unref );
+
+ G_OBJECT_CLASS( foobar_panel_item_status_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new status panel item with the given configuration.
+//
+FoobarPanelItem* foobar_panel_item_status_new(
+ FoobarPanelItemConfiguration const* config,
+ FoobarBatteryService* battery_service,
+ FoobarBrightnessService* brightness_service,
+ FoobarAudioService* audio_service,
+ FoobarNetworkService* network_service,
+ FoobarBluetoothService* bluetooth_service,
+ FoobarNotificationService* notification_service )
+{
+ g_return_val_if_fail( config != NULL, NULL );
+ g_return_val_if_fail( foobar_panel_item_configuration_get_kind( config ) == FOOBAR_PANEL_ITEM_KIND_STATUS, NULL );
+ g_return_val_if_fail( FOOBAR_IS_BATTERY_SERVICE( battery_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_BRIGHTNESS_SERVICE( brightness_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( audio_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_NETWORK_SERVICE( network_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( bluetooth_service ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( notification_service ), NULL );
+
+ FoobarPanelItemStatus* self = g_object_new( FOOBAR_TYPE_PANEL_ITEM_STATUS, NULL );
+ self->action = foobar_panel_item_configuration_get_action( config );
+ self->battery_service = g_object_ref( battery_service );
+ self->brightness_service = g_object_ref( brightness_service );
+ self->audio_service = g_object_ref( audio_service );
+ self->network_service = g_object_ref( network_service );
+ self->bluetooth_service = g_object_ref( bluetooth_service );
+ self->notification_service = g_object_ref( notification_service );
+
+ gtk_widget_set_sensitive( self->button, self->action != FOOBAR_PANEL_ITEM_ACTION_NONE );
+
+ gtk_box_set_spacing( GTK_BOX( self->layout ), foobar_panel_item_status_configuration_get_spacing( config ) );
+ gboolean show_labels = foobar_panel_item_status_configuration_get_show_labels( config );
+ gboolean enable_scrolling = foobar_panel_item_status_configuration_get_enable_scrolling( config );
+ gsize items_count;
+ FoobarStatusItem const* items = foobar_panel_item_status_configuration_get_items( config, &items_count );
+ for ( gsize i = 0; i < items_count; ++i )
+ {
+ GtkWidget* widget = foobar_panel_item_status_create_item( self, items[i], show_labels, enable_scrolling );
+ if ( widget )
+ {
+ gtk_widget_set_halign( widget, GTK_ALIGN_CENTER );
+ gtk_box_append( GTK_BOX( self->layout ), widget );
+ }
+ }
+
+ return FOOBAR_PANEL_ITEM( self );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called when the wi-fi adapter's active network changes.
+//
+// This is used to then subscribe to changes to the active network's strength. Unfortunately, we can't just use
+// GtkExpressions here because the active network may be NULL in which case the strength expression would fail. If one
+// expression fails to evaluate, then the closure expression used to derive the network icon/label also does not get
+// updated.
+//
+// In addition, this method also updates the network label to show the active network's name or become invisible.
+//
+void foobar_panel_item_status_handle_network_change(
+ FoobarNetworkAdapterWifi* adapter,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)pspec;
+ FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)userdata;
+
+ FoobarNetwork* network = foobar_network_adapter_wifi_get_active( adapter );
+ if ( self->network != network )
+ {
+ g_clear_signal_handler( &self->network_strength_handler_id, self->network );
+ g_clear_object( &self->network );
+
+ if ( network )
+ {
+ self->network = g_object_ref( network );
+ self->network_strength_handler_id = g_signal_connect(
+ self->network,
+ "notify::strength",
+ G_CALLBACK( foobar_panel_item_status_handle_network_strength_change ),
+ self );
+ }
+
+ if ( self->network_label )
+ {
+ gtk_label_set_label(
+ GTK_LABEL( self->network_label ), self->network ? foobar_network_get_name( self->network ) : NULL );
+ gtk_widget_set_visible( GTK_WIDGET( self->network_label ), self->network != NULL );
+ }
+
+ foobar_panel_item_status_reevaluate_network( self );
+ }
+}
+
+//
+// Called when the wi-fi adapter's active network strength changes.
+//
+// This just forces re-evaluation of the network icon expression for reasons mentioned in
+// foobar_panel_item_status_handle_network_change.
+//
+void foobar_panel_item_status_handle_network_strength_change(
+ GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)object;
+ (void)pspec;
+ FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)userdata;
+ foobar_panel_item_status_reevaluate_network( self );
+}
+
+//
+// Called when the button was clicked.
+//
+void foobar_panel_item_status_handle_clicked(
+ GtkButton* button,
+ gpointer userdata )
+{
+ (void)button;
+ FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)userdata;
+
+ foobar_panel_item_invoke_action( FOOBAR_PANEL_ITEM( self ), self->action );
+}
+
+//
+// Called when the user has scrolled over the brightness item, used to adjust the brightness level.
+//
+gboolean foobar_panel_item_status_handle_brightness_scroll(
+ GtkEventControllerScroll* controller,
+ gdouble dx,
+ gdouble dy,
+ gpointer userdata )
+{
+ (void)controller;
+ (void)dx;
+ FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)userdata;
+
+ gint new_brightness = foobar_brightness_service_get_percentage( self->brightness_service ) + (gint)dy;
+ foobar_brightness_service_set_percentage( self->brightness_service, new_brightness );
+
+ return TRUE;
+}
+
+//
+// Called when the user has scrolled over the audio item, used to adjust the volume.
+//
+gboolean foobar_panel_item_status_handle_audio_scroll(
+ GtkEventControllerScroll* controller,
+ gdouble dx,
+ gdouble dy,
+ gpointer userdata )
+{
+ (void)controller;
+ (void)dx;
+ FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)userdata;
+
+ FoobarAudioDevice* device = foobar_audio_service_get_default_output( self->audio_service );
+ gint new_volume = foobar_audio_device_get_volume( device ) + (gint)dy;
+ foobar_audio_device_set_volume( device, new_volume );
+
+ return TRUE;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Value Converters
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Derive the network icon from the active network adapter and its state.
+//
+// In addition, this also uses the wi-fi adapter's active network strength which is monitored separately (see
+// foobar_panel_item_status_handle_network_change).
+//
+gchar* foobar_panel_item_status_compute_network_icon(
+ GtkExpression* expression,
+ FoobarNetworkAdapter* adapter,
+ FoobarNetworkAdapterState state,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ if ( FOOBAR_IS_NETWORK_ADAPTER_WIFI( adapter ) )
+ {
+ if ( state == FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED ) { return g_strdup( "fluent-wifi-off-symbolic" ); }
+ if ( state != FOOBAR_NETWORK_ADAPTER_STATE_CONNECTED ) { return g_strdup( "fluent-wifi-warning-symbolic" ); }
+
+ FoobarNetwork* network = foobar_network_adapter_wifi_get_active( FOOBAR_NETWORK_ADAPTER_WIFI( adapter ) );
+ gint strength = network ? foobar_network_get_strength( network ) : 0;
+ if ( strength <= 25 ) { return g_strdup( "fluent-wifi-4-symbolic" ); }
+ if ( strength <= 50 ) { return g_strdup( "fluent-wifi-3-symbolic" ); }
+ if ( strength <= 75 ) { return g_strdup( "fluent-wifi-2-symbolic" ); }
+ return g_strdup( "fluent-wifi-1-symbolic" );
+ }
+ else
+ {
+ return g_strdup( "fluent-virtual-network-symbolic" );
+ }
+}
+
+//
+// Derive the bluetooth icon from the bluetooth adapter's state and the number of currently connected devices.
+//
+gchar* foobar_panel_item_status_compute_bluetooth_icon(
+ GtkExpression* expression,
+ gboolean is_enabled,
+ guint connected_device_count,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ if (is_enabled)
+ {
+ return ( connected_device_count > 0 )
+ ? g_strdup( "fluent-bluetooth-connected-symbolic" )
+ : g_strdup( "fluent-bluetooth-symbolic" );
+ }
+ else
+ {
+ return g_strdup( "fluent-bluetooth-off-symbolic" );
+ }
+}
+
+//
+// Derive the battery icon from the current state structure.
+//
+gchar* foobar_panel_item_status_compute_battery_icon(
+ GtkExpression* expression,
+ FoobarBatteryState const* state,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ if ( state )
+ {
+ gint percentage = foobar_battery_state_get_percentage( state );
+ if ( foobar_battery_state_is_charging( state ) ) { return g_strdup( "fluent-battery-charge-symbolic" ); }
+ if ( percentage <= 5 ) { return g_strdup( "fluent-battery-0-symbolic" ); }
+ if ( percentage <= 15 ) { return g_strdup( "fluent-battery-1-symbolic" ); }
+ if ( percentage <= 25 ) { return g_strdup( "fluent-battery-2-symbolic" ); }
+ if ( percentage <= 35 ) { return g_strdup( "fluent-battery-3-symbolic" ); }
+ if ( percentage <= 45 ) { return g_strdup( "fluent-battery-4-symbolic" ); }
+ if ( percentage <= 55 ) { return g_strdup( "fluent-battery-5-symbolic" ); }
+ if ( percentage <= 65 ) { return g_strdup( "fluent-battery-6-symbolic" ); }
+ if ( percentage <= 75 ) { return g_strdup( "fluent-battery-7-symbolic" ); }
+ if ( percentage <= 85 ) { return g_strdup( "fluent-battery-8-symbolic" ); }
+ if ( percentage <= 95 ) { return g_strdup( "fluent-battery-9-symbolic" ); }
+ return g_strdup( "fluent-battery-10-symbolic" );
+ }
+ else
+ {
+ return g_strdup( "fluent-plug-connected-symbolic" );
+ }
+}
+
+//
+// Derive the battery label from the current state structure, showing the percentage if available.
+//
+gchar* foobar_panel_item_status_compute_battery_label(
+ GtkExpression* expression,
+ FoobarBatteryState const* state,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ if ( state )
+ {
+ return g_strdup_printf( "%d%%", foobar_battery_state_get_percentage( state ) );
+ }
+ else
+ {
+ return g_strdup( "n/a" );
+ }
+}
+
+//
+// Derive the brightness icon from the current percentage value.
+//
+gchar* foobar_panel_item_status_compute_brightness_icon(
+ GtkExpression* expression,
+ gint percentage,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ if ( percentage <= 50 )
+ {
+ return g_strdup( "fluent-brightness-low-symbolic" );
+ }
+ else
+ {
+ return g_strdup( "fluent-brightness-high-symbolic" );
+ }
+}
+
+//
+// Derive the brightness label from the current percentage value.
+//
+gchar* foobar_panel_item_status_compute_brightness_label(
+ GtkExpression* expression,
+ gint percentage,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ return g_strdup_printf( "%d%%", percentage );
+}
+
+//
+// Derive the audio icon from the current state and volume of the default output device.
+//
+gchar* foobar_panel_item_status_compute_audio_icon(
+ GtkExpression* expression,
+ gboolean is_available,
+ gint volume,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ if ( is_available )
+ {
+ if ( volume == 0 ) { return g_strdup( "fluent-speaker-mute-symbolic" ); }
+ if ( volume <= 33 ) { return g_strdup( "fluent-speaker-0-symbolic" ); }
+ if ( volume <= 67 ) { return g_strdup( "fluent-speaker-1-symbolic" ); }
+ return g_strdup( "fluent-speaker-2-symbolic" );
+ }
+ else
+ {
+ return g_strdup( "fluent-speaker-off-symbolic" );
+ }
+}
+
+//
+// Derive the audio label from the current state and volume of the default output device.
+//
+gchar* foobar_panel_item_status_compute_audio_label(
+ GtkExpression* expression,
+ gboolean is_available,
+ gint volume,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ if ( is_available )
+ {
+ return g_strdup_printf( "%d%%", volume );
+ }
+ else
+ {
+ return g_strdup( "n/a" );
+ }
+}
+
+//
+// Derive the notification icon from the current number of notifications.
+//
+gchar* foobar_panel_item_status_compute_notifications_icon(
+ GtkExpression* expression,
+ guint count,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ switch ( count )
+ {
+ case 0:
+ return g_strdup( "fluent-checkmark-circle-symbolic" );
+ case 1:
+ return g_strdup( "fluent-number-circle-1-symbolic" );
+ case 2:
+ return g_strdup( "fluent-number-circle-2-symbolic" );
+ case 3:
+ return g_strdup( "fluent-number-circle-3-symbolic" );
+ case 4:
+ return g_strdup( "fluent-number-circle-4-symbolic" );
+ case 5:
+ return g_strdup( "fluent-number-circle-5-symbolic" );
+ case 6:
+ return g_strdup( "fluent-number-circle-6-symbolic" );
+ case 7:
+ return g_strdup( "fluent-number-circle-7-symbolic" );
+ case 8:
+ return g_strdup( "fluent-number-circle-8-symbolic" );
+ case 9:
+ return g_strdup( "fluent-number-circle-9-symbolic" );
+ default:
+ return g_strdup( "fluent-more-circle-symbolic" );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new icon displaying the specified status item, optionally adding a label to it (if supported).
+//
+GtkWidget* foobar_panel_item_status_create_item(
+ FoobarPanelItemStatus* self,
+ FoobarStatusItem item,
+ gboolean show_labels,
+ gboolean enable_scrolling )
+{
+ switch ( item )
+ {
+ case FOOBAR_STATUS_ITEM_NETWORK:
+ {
+ GtkWidget* box = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_widget_set_valign( box, GTK_ALIGN_FILL );
+
+ GtkExpression* service_expr = gtk_constant_expression_new(
+ FOOBAR_TYPE_NETWORK_SERVICE,
+ self->network_service );
+ GtkExpression* active_expr = gtk_property_expression_new(
+ FOOBAR_TYPE_NETWORK_SERVICE,
+ service_expr,
+ "active" );
+ GtkExpression* state_expr = gtk_property_expression_new(
+ FOOBAR_TYPE_NETWORK_ADAPTER,
+ gtk_expression_ref( active_expr ),
+ "state" );
+ GtkExpression* icon_params[] = { active_expr, state_expr };
+ self->network_icon_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( icon_params ),
+ icon_params,
+ G_CALLBACK( foobar_panel_item_status_compute_network_icon ),
+ self,
+ NULL );
+ self->network_icon = gtk_image_new( );
+ gtk_widget_set_valign( self->network_icon, GTK_ALIGN_CENTER );
+ gtk_expression_bind( gtk_expression_ref( self->network_icon_expr ), self->network_icon, "icon-name", NULL );
+ gtk_box_append( GTK_BOX( box ), self->network_icon );
+
+ if ( show_labels )
+ {
+ self->network_label = gtk_label_new( NULL );
+ gtk_widget_set_valign( self->network_label, GTK_ALIGN_CENTER );
+ gtk_label_set_max_width_chars( GTK_LABEL( self->network_label ), 16 );
+ gtk_label_set_ellipsize( GTK_LABEL( self->network_label ), PANGO_ELLIPSIZE_END );
+ gtk_box_append( GTK_BOX( box ), self->network_label );
+ }
+
+ // Manually subscribe to network (/strength) because evaluation might fail.
+
+ FoobarNetworkAdapterWifi* adapter = foobar_network_service_get_wifi( self->network_service );
+ if ( adapter )
+ {
+ self->network_handler_id = g_signal_connect(
+ adapter,
+ "notify::active",
+ G_CALLBACK( foobar_panel_item_status_handle_network_change ),
+ self );
+ if ( foobar_network_adapter_wifi_get_active( adapter ) )
+ {
+ self->network = g_object_ref( foobar_network_adapter_wifi_get_active( adapter ) );
+ self->network_strength_handler_id = g_signal_connect(
+ self->network,
+ "notify::strength",
+ G_CALLBACK( foobar_panel_item_status_handle_network_strength_change ),
+ self );
+ }
+ if ( self->network_label )
+ {
+ gtk_label_set_label(
+ GTK_LABEL( self->network_label ),
+ self->network ? foobar_network_get_name( self->network ) : NULL );
+ gtk_widget_set_visible( GTK_WIDGET( self->network_label ), self->network != NULL );
+ }
+ }
+
+ return box;
+ }
+ case FOOBAR_STATUS_ITEM_BLUETOOTH:
+ {
+ GtkWidget* icon = gtk_image_new( );
+ gtk_widget_set_valign( icon, GTK_ALIGN_CENTER );
+
+ GtkExpression* service_expr = gtk_constant_expression_new(
+ FOOBAR_TYPE_BLUETOOTH_SERVICE,
+ self->bluetooth_service );
+ GtkExpression* enabled_expr = gtk_property_expression_new(
+ FOOBAR_TYPE_BLUETOOTH_SERVICE,
+ gtk_expression_ref( service_expr ),
+ "is-enabled" );
+ GtkExpression* connected_devices_expr = gtk_property_expression_new(
+ FOOBAR_TYPE_BLUETOOTH_SERVICE,
+ service_expr,
+ "connected-devices" );
+ GtkExpression* connected_device_count_expr = gtk_property_expression_new(
+ GTK_TYPE_FILTER_LIST_MODEL,
+ connected_devices_expr,
+ "n-items" );
+ GtkExpression* icon_params[] = { enabled_expr, connected_device_count_expr };
+ GtkExpression* icon_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( icon_params ),
+ icon_params,
+ G_CALLBACK( foobar_panel_item_status_compute_bluetooth_icon ),
+ NULL,
+ NULL );
+ gtk_expression_bind( icon_expr, icon, "icon-name", NULL );
+
+ return icon;
+ }
+ case FOOBAR_STATUS_ITEM_BATTERY:
+ {
+ GtkWidget* box = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_widget_set_valign( box, GTK_ALIGN_FILL );
+
+ GtkExpression* service_expr = gtk_constant_expression_new(
+ FOOBAR_TYPE_BATTERY_SERVICE,
+ self->battery_service );
+ GtkExpression* state_expr = gtk_property_expression_new(
+ FOOBAR_TYPE_BATTERY_SERVICE,
+ service_expr,
+ "state" );
+
+ GtkExpression* icon_params[] = { state_expr };
+ GtkExpression* icon_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( icon_params ),
+ icon_params,
+ G_CALLBACK( foobar_panel_item_status_compute_battery_icon ),
+ NULL,
+ NULL );
+ GtkWidget* icon = gtk_image_new( );
+ gtk_widget_set_valign( icon, GTK_ALIGN_CENTER );
+ gtk_expression_bind( icon_expr, icon, "icon-name", NULL );
+ gtk_box_append( GTK_BOX( box ), icon );
+
+ if ( show_labels )
+ {
+ GtkExpression* label_params[] = { gtk_expression_ref( state_expr ) };
+ GtkExpression* label_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( label_params ),
+ label_params,
+ G_CALLBACK( foobar_panel_item_status_compute_battery_label ),
+ NULL,
+ NULL );
+ GtkWidget* label = gtk_label_new( NULL );
+ gtk_widget_set_valign( label, GTK_ALIGN_CENTER );
+ gtk_expression_bind( label_expr, label, "label", NULL );
+ gtk_box_append( GTK_BOX( box ), label );
+ }
+
+ return box;
+ }
+ case FOOBAR_STATUS_ITEM_BRIGHTNESS:
+ {
+ GtkWidget* box = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_widget_set_valign( box, GTK_ALIGN_FILL );
+
+ if ( enable_scrolling )
+ {
+ GtkEventController* scroll_controller = gtk_event_controller_scroll_new(
+ GTK_EVENT_CONTROLLER_SCROLL_VERTICAL | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE );
+ g_signal_connect(
+ scroll_controller,
+ "scroll",
+ G_CALLBACK( foobar_panel_item_status_handle_brightness_scroll ),
+ self );
+ gtk_widget_add_controller( box, scroll_controller );
+ }
+
+ GtkExpression* service_expr = gtk_constant_expression_new(
+ FOOBAR_TYPE_BRIGHTNESS_SERVICE,
+ self->brightness_service );
+ GtkExpression* percentage_expr = gtk_property_expression_new(
+ FOOBAR_TYPE_BRIGHTNESS_SERVICE,
+ service_expr,
+ "percentage" );
+
+ GtkExpression* icon_params[] = { percentage_expr };
+ GtkExpression* icon_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( icon_params ),
+ icon_params,
+ G_CALLBACK( foobar_panel_item_status_compute_brightness_icon ),
+ NULL,
+ NULL );
+ GtkWidget* icon = gtk_image_new( );
+ gtk_widget_set_valign( icon, GTK_ALIGN_CENTER );
+ gtk_expression_bind( icon_expr, icon, "icon-name", NULL );
+ gtk_box_append( GTK_BOX( box ), icon );
+
+ if ( show_labels )
+ {
+ GtkExpression* label_params[] = { gtk_expression_ref( percentage_expr ) };
+ GtkExpression* label_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( label_params ),
+ label_params,
+ G_CALLBACK( foobar_panel_item_status_compute_brightness_label ),
+ NULL,
+ NULL );
+ GtkWidget* label = gtk_label_new( NULL );
+ gtk_widget_set_valign( label, GTK_ALIGN_CENTER );
+ gtk_expression_bind( label_expr, label, "label", NULL );
+ gtk_box_append( GTK_BOX( box ), label );
+ }
+
+ return box;
+ }
+ case FOOBAR_STATUS_ITEM_AUDIO:
+ {
+ GtkWidget* box = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_widget_set_valign( box, GTK_ALIGN_FILL );
+
+ if ( enable_scrolling )
+ {
+ GtkEventController* scroll_controller = gtk_event_controller_scroll_new(
+ GTK_EVENT_CONTROLLER_SCROLL_VERTICAL | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE );
+ g_signal_connect(
+ scroll_controller,
+ "scroll",
+ G_CALLBACK( foobar_panel_item_status_handle_audio_scroll ),
+ self );
+ gtk_widget_add_controller( box, scroll_controller );
+ }
+
+ GtkExpression* device_expr = gtk_constant_expression_new(
+ FOOBAR_TYPE_AUDIO_DEVICE,
+ foobar_audio_service_get_default_output( self->audio_service ) );
+ GtkExpression* available_expr = gtk_property_expression_new(
+ FOOBAR_TYPE_AUDIO_DEVICE,
+ gtk_expression_ref( device_expr ),
+ "is-available" );
+ GtkExpression* volume_expr = gtk_property_expression_new(
+ FOOBAR_TYPE_AUDIO_DEVICE,
+ gtk_expression_ref( device_expr ),
+ "volume" );
+
+ GtkExpression* icon_params[] = { available_expr, volume_expr };
+ GtkExpression* icon_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( icon_params ),
+ icon_params,
+ G_CALLBACK( foobar_panel_item_status_compute_audio_icon ),
+ NULL,
+ NULL );
+ GtkWidget* icon = gtk_image_new( );
+ gtk_widget_set_valign( icon, GTK_ALIGN_CENTER );
+ gtk_expression_bind( icon_expr, icon, "icon-name", NULL );
+ gtk_box_append( GTK_BOX( box ), icon );
+
+ if ( show_labels )
+ {
+ GtkExpression* label_params[] = { gtk_expression_ref( available_expr ), gtk_expression_ref( volume_expr ) };
+ GtkExpression* label_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( label_params ),
+ label_params,
+ G_CALLBACK( foobar_panel_item_status_compute_audio_label ),
+ NULL,
+ NULL );
+ GtkWidget* label = gtk_label_new( NULL );
+ gtk_widget_set_valign( label, GTK_ALIGN_CENTER );
+ gtk_expression_bind( label_expr, label, "label", NULL );
+ gtk_box_append( GTK_BOX( box ), label );
+ }
+
+ return box;
+ }
+ case FOOBAR_STATUS_ITEM_NOTIFICATIONS:
+ {
+ GtkWidget* icon = gtk_image_new( );
+ gtk_widget_set_valign( icon, GTK_ALIGN_CENTER );
+
+ GtkExpression* list_expr = gtk_constant_expression_new(
+ G_TYPE_LIST_MODEL,
+ foobar_notification_service_get_notifications( self->notification_service ) );
+ GtkExpression* count_expr = gtk_property_expression_new(
+ GTK_TYPE_SORT_LIST_MODEL,
+ list_expr,
+ "n-items" );
+ GtkExpression* icon_params[] = { count_expr };
+ GtkExpression* icon_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( icon_params ),
+ icon_params,
+ G_CALLBACK( foobar_panel_item_status_compute_notifications_icon ),
+ NULL,
+ NULL );
+ gtk_expression_bind( icon_expr, icon, "icon-name", NULL );
+
+ return icon;
+ }
+ default:
+ g_warn_if_reached( );
+ return NULL;
+ }
+}
+
+//
+// Force re-evaluation of the network icon expression.
+//
+// This is used whenever the active network or its strength changes.
+//
+void foobar_panel_item_status_reevaluate_network( FoobarPanelItemStatus* self )
+{
+ if ( self->network_icon && self->network_icon_expr )
+ {
+ GValue value = G_VALUE_INIT;
+ if ( gtk_expression_evaluate( self->network_icon_expr, NULL, &value ) )
+ {
+ gtk_image_set_from_icon_name( GTK_IMAGE( self->network_icon ), g_value_get_string( &value ) );
+ }
+ g_value_unset( &value );
+ }
+} \ No newline at end of file
diff --git a/src/widgets/panel/panel-item-status.h b/src/widgets/panel/panel-item-status.h
new file mode 100644
index 0000000..e18c097
--- /dev/null
+++ b/src/widgets/panel/panel-item-status.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "widgets/panel/panel-item.h"
+#include "services/battery-service.h"
+#include "services/brightness-service.h"
+#include "services/audio-service.h"
+#include "services/network-service.h"
+#include "services/bluetooth-service.h"
+#include "services/notification-service.h"
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_PANEL_ITEM_STATUS foobar_panel_item_status_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarPanelItemStatus, foobar_panel_item_status, FOOBAR, PANEL_ITEM_STATUS, FoobarPanelItem )
+
+FoobarPanelItem* foobar_panel_item_status_new( FoobarPanelItemConfiguration const* config,
+ FoobarBatteryService* battery_service,
+ FoobarBrightnessService* brightness_service,
+ FoobarAudioService* audio_service,
+ FoobarNetworkService* network_service,
+ FoobarBluetoothService* bluetooth_service,
+ FoobarNotificationService* notification_service );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/panel/panel-item-workspaces.c b/src/widgets/panel/panel-item-workspaces.c
new file mode 100644
index 0000000..acdd201
--- /dev/null
+++ b/src/widgets/panel/panel-item-workspaces.c
@@ -0,0 +1,327 @@
+#include "widgets/panel/panel-item-workspaces.h"
+#include "widgets/inset-container.h"
+
+//
+// FoobarPanelItemWorkspaces:
+//
+// A panel item showing all workspaces for the current monitor (if multi-monitor mode is enabled) as buttons, indicating
+// the current state of the workspace (i.e., whether it's active, visible, special, etc.).
+//
+
+struct _FoobarPanelItemWorkspaces
+{
+ FoobarPanelItem parent_instance;
+ GtkWidget* list_view;
+ GtkWidget* inset_container;
+ FoobarWorkspaceService* workspace_service;
+ gint button_size;
+ gint spacing;
+ GtkFilter* monitor_filter;
+ gulong monitor_configuration_handler_id;
+};
+
+enum
+{
+ PROP_ORIENTATION = 1,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_panel_item_workspaces_class_init ( FoobarPanelItemWorkspacesClass* klass );
+static void foobar_panel_item_workspaces_init ( FoobarPanelItemWorkspaces* self );
+static void foobar_panel_item_workspaces_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_panel_item_workspaces_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_panel_item_workspaces_finalize ( GObject* object );
+static void foobar_panel_item_workspaces_handle_item_setup ( GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata );
+static void foobar_panel_item_workspaces_handle_item_clicked ( GtkButton* button,
+ gpointer userdata );
+static void foobar_panel_item_workspaces_handle_monitor_configuration_changed( FoobarWorkspaceService* service,
+ gpointer userdata );
+static gchar** foobar_panel_item_workspaces_compute_css_classes ( GtkExpression* expression,
+ FoobarWorkspaceFlags flags,
+ gpointer userdata );
+static gboolean foobar_panel_item_workspaces_monitor_filter_func ( gpointer item,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE_WITH_CODE(
+ FoobarPanelItemWorkspaces,
+ foobar_panel_item_workspaces,
+ FOOBAR_TYPE_PANEL_ITEM,
+ G_IMPLEMENT_INTERFACE( GTK_TYPE_ORIENTABLE, NULL ) )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Panel Item Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for workspace panel items.
+//
+void foobar_panel_item_workspaces_class_init( FoobarPanelItemWorkspacesClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_panel_item_workspaces_get_property;
+ object_klass->set_property = foobar_panel_item_workspaces_set_property;
+ object_klass->finalize = foobar_panel_item_workspaces_finalize;
+
+ gpointer orientable_iface = g_type_default_interface_peek( GTK_TYPE_ORIENTABLE );
+ props[PROP_ORIENTATION] = g_param_spec_override(
+ "orientation",
+ g_object_interface_find_property( orientable_iface, "orientation" ) );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for workspace panel items.
+//
+void foobar_panel_item_workspaces_init( FoobarPanelItemWorkspaces* self )
+{
+ GtkListItemFactory* item_factory = gtk_signal_list_item_factory_new( );
+ g_signal_connect( item_factory, "setup", G_CALLBACK( foobar_panel_item_workspaces_handle_item_setup ), self );
+
+ self->list_view = gtk_list_view_new( NULL, item_factory );
+ gtk_widget_set_halign( self->list_view, GTK_ALIGN_CENTER );
+ gtk_widget_set_valign( self->list_view, GTK_ALIGN_CENTER );
+ gtk_list_view_set_single_click_activate( GTK_LIST_VIEW( self->list_view ), TRUE );
+
+ self->inset_container = foobar_inset_container_new( );
+ foobar_inset_container_set_child( FOOBAR_INSET_CONTAINER( self->inset_container ), GTK_WIDGET( self->list_view ) );
+
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "workspaces" );
+ foobar_panel_item_set_child( FOOBAR_PANEL_ITEM( self ), GTK_WIDGET( self->inset_container ) );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_panel_item_workspaces_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarPanelItemWorkspaces* self = (FoobarPanelItemWorkspaces*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_ORIENTATION:
+ g_value_set_enum( value, gtk_orientable_get_orientation( GTK_ORIENTABLE( self->list_view ) ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_panel_item_workspaces_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarPanelItemWorkspaces* self = (FoobarPanelItemWorkspaces*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_ORIENTATION:
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( self->list_view ), g_value_get_enum( value ) );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ORIENTATION] );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance cleanup for workspace panel items.
+//
+void foobar_panel_item_workspaces_finalize( GObject* object )
+{
+ FoobarPanelItemWorkspaces* self = (FoobarPanelItemWorkspaces*)object;
+
+ g_clear_signal_handler( &self->monitor_configuration_handler_id, self->workspace_service );
+ g_clear_object( &self->workspace_service );
+ g_clear_object( &self->monitor_filter );
+
+ G_OBJECT_CLASS( foobar_panel_item_workspaces_parent_class )->finalize( object );
+}
+
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new workspaces panel item with the given configuration.
+//
+// If monitor is not NULL, only workspaces on this monitor will be shown.
+//
+FoobarPanelItem* foobar_panel_item_workspaces_new(
+ FoobarPanelItemConfiguration const* config,
+ GdkMonitor* monitor,
+ FoobarWorkspaceService* workspace_service )
+{
+ g_return_val_if_fail( config != NULL, NULL );
+ g_return_val_if_fail( foobar_panel_item_configuration_get_kind( config ) == FOOBAR_PANEL_ITEM_KIND_WORKSPACES, NULL );
+ g_return_val_if_fail( monitor == NULL || GDK_IS_MONITOR( monitor ), NULL );
+ g_return_val_if_fail( FOOBAR_IS_WORKSPACE_SERVICE( workspace_service ), NULL );
+
+ FoobarPanelItemWorkspaces* self = g_object_new( FOOBAR_TYPE_PANEL_ITEM_WORKSPACES, NULL );
+ self->button_size = foobar_panel_item_workspaces_configuration_get_button_size( config );
+ self->spacing = foobar_panel_item_workspaces_configuration_get_spacing( config );
+ self->workspace_service = g_object_ref( workspace_service );
+
+ // Set up the source model, optionally enabling filtering.
+
+ GListModel* source_model = g_object_ref( foobar_workspace_service_get_workspaces( self->workspace_service ) );
+ if ( monitor )
+ {
+ GtkCustomFilter* filter = gtk_custom_filter_new(
+ foobar_panel_item_workspaces_monitor_filter_func,
+ g_object_ref( monitor ),
+ g_object_unref );
+ self->monitor_filter = GTK_FILTER( filter );
+ source_model = G_LIST_MODEL( gtk_filter_list_model_new( source_model, g_object_ref( self->monitor_filter ) ) );
+
+ self->monitor_configuration_handler_id = g_signal_connect(
+ self->workspace_service,
+ "monitor-configuration-changed",
+ G_CALLBACK( foobar_panel_item_workspaces_handle_monitor_configuration_changed ),
+ self );
+ }
+ GtkNoSelection* selection_model = gtk_no_selection_new( source_model );
+ gtk_list_view_set_model( GTK_LIST_VIEW( self->list_view ), GTK_SELECTION_MODEL( selection_model ) );
+
+ // Wrap the list in an inset container to remove the outer margin from the items.
+
+ foobar_inset_container_set_inset_horizontal( FOOBAR_INSET_CONTAINER( self->inset_container ), -self->spacing / 2 );
+ foobar_inset_container_set_inset_vertical( FOOBAR_INSET_CONTAINER( self->inset_container ), -self->spacing / 2 );
+
+ return FOOBAR_PANEL_ITEM( self );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called by the list view to create a widget for displaying a workspace.
+//
+void foobar_panel_item_workspaces_handle_item_setup(
+ GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata )
+{
+ (void)factory;
+ FoobarPanelItemWorkspaces* self = (FoobarPanelItemWorkspaces*)userdata;
+
+ GtkWidget* button = gtk_button_new( );
+ gtk_widget_set_size_request( button, self->button_size, self->button_size );
+ gtk_widget_set_margin_top( button, self->spacing / 2 );
+ gtk_widget_set_margin_bottom( button, self->spacing / 2 );
+ gtk_widget_set_margin_start( button, self->spacing / 2 );
+ gtk_widget_set_margin_end( button, self->spacing / 2 );
+ g_signal_connect(
+ button,
+ "clicked",
+ G_CALLBACK( foobar_panel_item_workspaces_handle_item_clicked ),
+ list_item );
+
+ gtk_list_item_set_child( list_item, button );
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* active_expr = gtk_property_expression_new( FOOBAR_TYPE_WORKSPACE, item_expr, "flags" );
+ GtkExpression* params[] = { active_expr };
+ GtkExpression* css_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRV,
+ NULL,
+ G_N_ELEMENTS( params ),
+ params,
+ G_CALLBACK( foobar_panel_item_workspaces_compute_css_classes ),
+ NULL,
+ NULL );
+ gtk_expression_bind( css_expr, button, "css-classes", list_item );
+ }
+}
+
+//
+// Called by the workspace service when the monitor for any workspace has changed.
+//
+// This is needed because the GtkFilterListModel does not track changes to the item properties and needs to be
+// re-evaluated manually.
+//
+void foobar_panel_item_workspaces_handle_monitor_configuration_changed(
+ FoobarWorkspaceService* service,
+ gpointer userdata )
+{
+ (void)service;
+ FoobarPanelItemWorkspaces* self = (FoobarPanelItemWorkspaces*)userdata;
+
+ gtk_filter_changed( self->monitor_filter, GTK_FILTER_CHANGE_DIFFERENT );
+}
+
+//
+// Called when a workspace button was clicked, activating that workspace.
+//
+void foobar_panel_item_workspaces_handle_item_clicked( GtkButton* button, gpointer userdata )
+{
+ (void)button;
+ GtkListItem* item = (GtkListItem*)userdata;
+
+ FoobarWorkspace* workspace = gtk_list_item_get_item( item );
+ if ( workspace ) { foobar_workspace_activate( workspace ); }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Value Converters
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Derive the list of CSS classes for a workspace button from the workspace's current flags.
+//
+gchar** foobar_panel_item_workspaces_compute_css_classes(
+ GtkExpression* expression,
+ FoobarWorkspaceFlags flags,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ GStrvBuilder* builder = g_strv_builder_new( );
+ if ( flags & FOOBAR_WORKSPACE_FLAGS_ACTIVE ) { g_strv_builder_add( builder, "active" ); }
+ if ( flags & FOOBAR_WORKSPACE_FLAGS_VISIBLE ) { g_strv_builder_add( builder, "visible" ); }
+ if ( flags & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) { g_strv_builder_add( builder, "special" ); }
+ if ( flags & FOOBAR_WORKSPACE_FLAGS_URGENT ) { g_strv_builder_add( builder, "urgent" ); }
+ if ( flags & FOOBAR_WORKSPACE_FLAGS_PERSISTENT ) { g_strv_builder_add( builder, "persistent" ); }
+ return g_strv_builder_end( builder );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Check if a workspace is on the panel's monitor.
+//
+gboolean foobar_panel_item_workspaces_monitor_filter_func( gpointer item, gpointer userdata )
+{
+ FoobarWorkspace* workspace = (FoobarWorkspace*)item;
+ GdkMonitor* monitor = (GdkMonitor*)userdata;
+
+ gchar const* expected = gdk_monitor_get_connector( monitor );
+ gchar const* actual = foobar_workspace_get_monitor( workspace );
+ return !expected || !g_strcmp0( expected, actual );
+} \ No newline at end of file
diff --git a/src/widgets/panel/panel-item-workspaces.h b/src/widgets/panel/panel-item-workspaces.h
new file mode 100644
index 0000000..c9d7ef1
--- /dev/null
+++ b/src/widgets/panel/panel-item-workspaces.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "widgets/panel/panel-item.h"
+#include "services/workspace-service.h"
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_PANEL_ITEM_WORKSPACES foobar_panel_item_workspaces_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarPanelItemWorkspaces, foobar_panel_item_workspaces, FOOBAR, PANEL_ITEM_WORKSPACES, FoobarPanelItem )
+
+FoobarPanelItem* foobar_panel_item_workspaces_new( FoobarPanelItemConfiguration const* config,
+ GdkMonitor* monitor,
+ FoobarWorkspaceService* workspace_service );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/panel/panel-item.c b/src/widgets/panel/panel-item.c
new file mode 100644
index 0000000..f2d4383
--- /dev/null
+++ b/src/widgets/panel/panel-item.c
@@ -0,0 +1,195 @@
+#include "widgets/panel/panel-item.h"
+#include "application.h"
+
+//
+// FoobarPanelItem:
+//
+// Base class for all panel items. This is basically a bin layout where the subclass usually sets the child widget.
+//
+
+typedef struct _FoobarPanelItemPrivate
+{
+ GtkWidget* child;
+} FoobarPanelItemPrivate;
+
+enum
+{
+ PROP_CHILD = 1,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_panel_item_class_init ( FoobarPanelItemClass* klass );
+static void foobar_panel_item_init ( FoobarPanelItem* self );
+static void foobar_panel_item_get_property( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_panel_item_set_property( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_panel_item_dispose ( GObject* object );
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE( FoobarPanelItem, foobar_panel_item, GTK_TYPE_WIDGET )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Widget Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for panel items.
+//
+void foobar_panel_item_class_init( FoobarPanelItemClass* klass )
+{
+ GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass );
+ gtk_widget_class_set_css_name( widget_klass, "panel-item" );
+ gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BIN_LAYOUT );
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_panel_item_get_property;
+ object_klass->set_property = foobar_panel_item_set_property;
+ object_klass->dispose = foobar_panel_item_dispose;
+
+ props[PROP_CHILD] = g_param_spec_object(
+ "child",
+ "Child",
+ "Child widget for the panel item.",
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for panel items.
+//
+void foobar_panel_item_init( FoobarPanelItem* self )
+{
+ (void)self;
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_panel_item_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarPanelItem* self = (FoobarPanelItem*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_CHILD:
+ g_value_set_object( value, foobar_panel_item_get_child( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_panel_item_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarPanelItem* self = (FoobarPanelItem*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_CHILD:
+ foobar_panel_item_set_child( self, g_value_get_object( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for panel items.
+//
+void foobar_panel_item_dispose( GObject* object )
+{
+ FoobarPanelItem* self = (FoobarPanelItem*)object;
+ FoobarPanelItemPrivate* priv = foobar_panel_item_get_instance_private( self );
+
+ g_clear_pointer( &priv->child, gtk_widget_unparent );
+
+ G_OBJECT_CLASS( foobar_panel_item_parent_class )->dispose( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Get the child widget for the panel item.
+//
+GtkWidget* foobar_panel_item_get_child( FoobarPanelItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_PANEL_ITEM( self ), NULL );
+ FoobarPanelItemPrivate* priv = foobar_panel_item_get_instance_private( self );
+
+ return priv->child;
+}
+
+//
+// Update the child widget for the panel item.
+//
+void foobar_panel_item_set_child(
+ FoobarPanelItem* self,
+ GtkWidget* child )
+{
+ g_return_if_fail( FOOBAR_IS_PANEL_ITEM( self ) );
+ FoobarPanelItemPrivate* priv = foobar_panel_item_get_instance_private( self );
+ g_return_if_fail( child == NULL || priv->child == child || gtk_widget_get_parent( child ) == NULL );
+
+ if ( priv->child != child )
+ {
+ g_clear_pointer( &priv->child, gtk_widget_unparent );
+
+ if ( child )
+ {
+ priv->child = child;
+ gtk_widget_set_parent( child, GTK_WIDGET( self ) );
+ }
+
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] );
+ }
+}
+
+//
+// Invoke the action for the panel item.
+//
+// This needs to be triggered by a subclass, for example by adding a button to the item. Support for panel item actions
+// is opt-in and subclasses have to explicitly implement it. This is just a helper for implementing the action.
+//
+void foobar_panel_item_invoke_action(
+ FoobarPanelItem* self,
+ FoobarPanelItemAction action )
+{
+ g_return_if_fail( FOOBAR_IS_PANEL_ITEM( self ) );
+
+ switch ( action )
+ {
+ case FOOBAR_PANEL_ITEM_ACTION_NONE:
+ break;
+ case FOOBAR_PANEL_ITEM_ACTION_LAUNCHER:
+ foobar_application_toggle_launcher( FOOBAR_APPLICATION_DEFAULT );
+ break;
+ case FOOBAR_PANEL_ITEM_ACTION_CONTROL_CENTER:
+ foobar_application_toggle_control_center( FOOBAR_APPLICATION_DEFAULT );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+} \ No newline at end of file
diff --git a/src/widgets/panel/panel-item.h b/src/widgets/panel/panel-item.h
new file mode 100644
index 0000000..318bd85
--- /dev/null
+++ b/src/widgets/panel/panel-item.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <gtk/gtk.h>
+#include "services/configuration-service.h"
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_PANEL_ITEM foobar_panel_item_get_type( )
+
+G_DECLARE_DERIVABLE_TYPE( FoobarPanelItem, foobar_panel_item, FOOBAR, PANEL_ITEM, GtkWidget )
+
+struct _FoobarPanelItemClass
+{
+ GtkWidgetClass parent_class;
+};
+
+GtkWidget* foobar_panel_item_get_child ( FoobarPanelItem* self );
+void foobar_panel_item_set_child ( FoobarPanelItem* self,
+ GtkWidget* child );
+void foobar_panel_item_invoke_action( FoobarPanelItem* self,
+ FoobarPanelItemAction action );
+
+G_END_DECLS \ No newline at end of file
diff --git a/subprojects/gtk4-layer-shell.wrap b/subprojects/gtk4-layer-shell.wrap
new file mode 100644
index 0000000..2cbc691
--- /dev/null
+++ b/subprojects/gtk4-layer-shell.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+url = https://github.com/wmww/gtk4-layer-shell.git
+revision = v1.0.2
+depth = 1 \ No newline at end of file
diff --git a/subprojects/gvc.wrap b/subprojects/gvc.wrap
new file mode 100644
index 0000000..0d8a956
--- /dev/null
+++ b/subprojects/gvc.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+url = https://gitlab.gnome.org/GNOME/libgnome-volume-control.git
+revision = 91f3f41490666a526ed78af744507d7ee1134323
+depth = 1 \ No newline at end of file