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
Binary files /dev/null and b/pub/default.png 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
Binary files /dev/null and b/pub/teatime.png 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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
+
+
+ fluent-grid-dots-symbolic.svg
+ fluent-battery-0-symbolic.svg
+ fluent-battery-1-symbolic.svg
+ fluent-battery-2-symbolic.svg
+ fluent-battery-3-symbolic.svg
+ fluent-battery-4-symbolic.svg
+ fluent-battery-5-symbolic.svg
+ fluent-battery-6-symbolic.svg
+ fluent-battery-7-symbolic.svg
+ fluent-battery-8-symbolic.svg
+ fluent-battery-9-symbolic.svg
+ fluent-battery-10-symbolic.svg
+ fluent-battery-charge-symbolic.svg
+ fluent-plug-connected-symbolic.svg
+ fluent-virtual-network-symbolic.svg
+ fluent-wifi-1-symbolic.svg
+ fluent-wifi-2-symbolic.svg
+ fluent-wifi-3-symbolic.svg
+ fluent-wifi-4-symbolic.svg
+ fluent-wifi-off-symbolic.svg
+ fluent-wifi-warning-symbolic.svg
+ fluent-number-circle-1-symbolic.svg
+ fluent-number-circle-2-symbolic.svg
+ fluent-number-circle-3-symbolic.svg
+ fluent-number-circle-4-symbolic.svg
+ fluent-number-circle-5-symbolic.svg
+ fluent-number-circle-6-symbolic.svg
+ fluent-number-circle-7-symbolic.svg
+ fluent-number-circle-8-symbolic.svg
+ fluent-number-circle-9-symbolic.svg
+ fluent-more-circle-symbolic.svg
+ fluent-checkmark-circle-symbolic.svg
+ fluent-brightness-low-symbolic.svg
+ fluent-brightness-high-symbolic.svg
+ fluent-speaker-0-symbolic.svg
+ fluent-speaker-1-symbolic.svg
+ fluent-speaker-2-symbolic.svg
+ fluent-speaker-mute-symbolic.svg
+ fluent-speaker-off-symbolic.svg
+ fluent-dismiss-symbolic.svg
+ fluent-search-symbolic.svg
+ fluent-bluetooth-symbolic.svg
+ fluent-bluetooth-off-symbolic.svg
+ fluent-bluetooth-connected-symbolic.svg
+ fluent-chevron-right-symbolic.svg
+ fluent-checkmark-symbolic.svg
+ fluent-microphone-symbolic.svg
+
+
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 @@
+
+
+
+ default.css
+ monokai-pro.css
+ teatime.css
+
+
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
+#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
+
+#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
+#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 @@
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
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
+#include
+#include
+
+//
+// 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
+#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
+
+//
+// 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
+#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
+
+//
+// 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
+#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
+#include
+#include
+#include
+
+//
+// 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
+#include
+
+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
+#include
+#include
+#include
+#include
+#include
+
+#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
+#include
+
+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
+
+#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
+
+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
+
+//
+// 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
+#include
+
+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
+#include
+
+//
+// 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
+
+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
+
+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
+
+#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
+
+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
+#include
+#include
+
+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
+#include
+
+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
+#include
+
+#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( ¬ifications ),
+ 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
+#include
+
+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
+#include
+#include
+
+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 "," where 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 "," where 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 ",," where 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 "," where 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 "" where 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:`. 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
+#include
+
+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
+
+//
+// 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
+
+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
+
+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
+
+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
+
+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
+
+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
+
+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
+
+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
+#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
+#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