From 1241d7c87cda15d5856163f0f00a42de8e966836 Mon Sep 17 00:00:00 2001 From: scorpion-26 <58082714+scorpion-26@users.noreply.github.com> Date: Sat, 18 Mar 2023 00:02:40 +0100 Subject: [PATCH] SNI: Initial proof of concept Implements a rough outline of the SNI (StatusNotifierItem) d-bus protocol for tray icons. Note: This is currently *very* WIP Full implementation will close https://github.com/scorpion-26/gBar/issues/5 --- .gitmodules | 3 + meson.build | 31 ++++ protocols/sni-item.xml | 46 ++++++ protocols/sni-watcher.xml | 23 +++ src/Bar.cpp | 3 + src/Log.h | 1 + src/SNI.cpp | 287 ++++++++++++++++++++++++++++++++++++++ src/SNI.h | 8 ++ src/System.cpp | 5 + src/Widget.cpp | 48 ++++++- src/Widget.h | 19 +++ thirdparty/stb | 1 + 12 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 .gitmodules create mode 100644 protocols/sni-item.xml create mode 100644 protocols/sni-watcher.xml create mode 100644 src/SNI.cpp create mode 100644 src/SNI.h create mode 160000 thirdparty/stb diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7748e45 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "thirdparty/stb"] + path = thirdparty/stb + url = https://github.com/nothings/stb diff --git a/meson.build b/meson.build index c858f21..a443769 100644 --- a/meson.build +++ b/meson.build @@ -23,6 +23,28 @@ ext_workspace_header = custom_target('generate-ext-workspace-header', output: ['ext-workspace-unstable-v1.h'], command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@']) +# DBus protocols +dbus_gen = find_program('gdbus-codegen') +sni_item_src = custom_target('generate-sni-item-src', + input: ['protocols/sni-item.xml'], + output: ['sni-item.c'], + command: [dbus_gen, '--c-namespace', 'sni', '--body', '--output', '@OUTPUT@', '@INPUT@']) + +sni_item_header = custom_target('generate-sni-item-header', + input: ['protocols/sni-item.xml'], + output: ['sni-item.h'], + command: [dbus_gen, '--c-namespace', 'sni', '--header', '--output', '@OUTPUT@', '@INPUT@']) + +sni_watcher_src = custom_target('generate-sni-watcher-src', + input: ['protocols/sni-watcher.xml'], + output: ['sni-watcher.c'], + command: [dbus_gen, '--c-namespace', 'sni', '--body', '--output', '@OUTPUT@', '@INPUT@']) + +sni_watcher_header = custom_target('generate-sni-watcher-header', + input: ['protocols/sni-watcher.xml'], + output: ['sni-watcher.h'], + command: [dbus_gen, '--c-namespace', 'sni', '--header', '--output', '@OUTPUT@', '@INPUT@']) + gtk = dependency('gtk+-3.0') gtk_layer_shell = dependency('gtk-layer-shell-0') @@ -64,9 +86,16 @@ add_global_arguments('-DUSE_LOGFILE', language: 'cpp') pulse = dependency('libpulse') +# stb +stb = include_directories('thirdparty') + libgBar = library('gBar', [ ext_workspace_src, ext_workspace_header, + sni_item_src, + sni_item_header, + sni_watcher_src, + sni_watcher_header, 'src/Window.cpp', 'src/Widget.cpp', 'src/System.cpp', @@ -78,8 +107,10 @@ libgBar = library('gBar', 'src/Config.cpp', 'src/CSS.cpp', 'src/Log.cpp', + 'src/SNI.cpp', ], dependencies: [gtk, gtk_layer_shell, pulse, wayland_client], + include_directories: stb, install: true) pkg = import('pkgconfig') diff --git a/protocols/sni-item.xml b/protocols/sni-item.xml new file mode 100644 index 0000000..74afed2 --- /dev/null +++ b/protocols/sni-item.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/protocols/sni-watcher.xml b/protocols/sni-watcher.xml new file mode 100644 index 0000000..0948962 --- /dev/null +++ b/protocols/sni-watcher.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Bar.cpp b/src/Bar.cpp index 589e8f3..e0f302e 100644 --- a/src/Bar.cpp +++ b/src/Bar.cpp @@ -3,6 +3,7 @@ #include "System.h" #include "Common.h" #include "Config.h" +#include "SNI.h" #include namespace Bar @@ -646,6 +647,8 @@ namespace Bar right->SetSpacing({8, false}); right->SetHorizontalTransform({-1, true, Alignment::Right}); { + SNI::WidgetSNI(*right); + WidgetPackages(*right); WidgetAudio(*right); diff --git a/src/Log.h b/src/Log.h index 79df5a3..33a7ea1 100644 --- a/src/Log.h +++ b/src/Log.h @@ -1,5 +1,6 @@ #pragma once #include +#include #ifdef USE_LOGFILE #define LOG(x) \ diff --git a/src/SNI.cpp b/src/SNI.cpp new file mode 100644 index 0000000..04b8aea --- /dev/null +++ b/src/SNI.cpp @@ -0,0 +1,287 @@ +#include "SNI.h" +#include "Log.h" +#include "Widget.h" + +#include +#include +#include + +#define STB_IMAGE_IMPLEMENTATION +#include + +#include + +namespace SNI +{ + sniWatcher* watcherSkeleton; + guint watcherID; + GDBusConnection* dbusConnection = nullptr; + + guint hostID; + + struct Item + { + std::string name; + std::string object; + size_t w; + size_t h; + uint8_t* iconData = nullptr; + }; + std::vector items; + + // Gtk stuff, TODO: Allow more than one instance + // Simply removing the gtk_drawing_areas doesn't trigger proper redrawing + // HACK: Make an outer permanent and an inner box, which will be deleted and readded + Widget* parentBox; + Widget* iconBox; + + // SNI implements the GTK-Thingies itself internally + static void InvalidateWidget() + { + parentBox->RemoveChild(iconBox); + + auto container = Widget::Create(); + iconBox = container.get(); + for (auto& item : items) + { + if (item.iconData) + { + auto texture = Widget::Create(); + texture->SetHorizontalTransform({32, true, Alignment::Fill}); + texture->SetBuf(item.w, item.h, item.iconData); + iconBox->AddChild(std::move(texture)); + } + } + parentBox->AddChild(std::move(container)); + } + + void WidgetSNI(Widget& parent) + { + // Add parent box + auto box = Widget::Create(); + auto container = Widget::Create(); + iconBox = container.get(); + parentBox = box.get(); + InvalidateWidget(); + box->AddChild(std::move(container)); + parent.AddChild(std::move(box)); + } + + static Item CreateItem(std::string&& name, std::string&& object) + { + Item item{}; + item.name = name; + item.object = object; + auto getProperty = [&](const char* prop) -> GVariant* + { + GError* err = nullptr; + GVariant* params[2]; + params[0] = g_variant_new_string("org.kde.StatusNotifierItem"); + params[1] = g_variant_new_string(prop); + GVariant* param = g_variant_new_tuple(params, 2); + GVariant* res = g_dbus_connection_call_sync(dbusConnection, name.c_str(), object.c_str(), "org.freedesktop.DBus.Properties", "Get", param, + G_VARIANT_TYPE("(v)"), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err); + if (err) + { + g_error_free(err); + return nullptr; + } + // There's probably a better method than to use 3 variants + // g_variant_unref(params[0]); + // g_variant_unref(params[1]); + // g_variant_unref(param); + return res; + }; + GVariant* iconPixmap = getProperty("IconPixmap"); + if (iconPixmap) + { + // Only get first item + GVariant* arr = nullptr; + g_variant_get(iconPixmap, "(v)", &arr); + + GVariantIter* arrIter = nullptr; + g_variant_get(arr, "a(iiay)", &arrIter); + + int width; + int height; + GVariantIter* data = nullptr; + g_variant_iter_next(arrIter, "(iiay)", &width, &height, &data); + + LOG(width); + LOG(height); + item.w = width; + item.h = height; + item.iconData = new uint8_t[width * height * 4]; + + uint8_t px = 0; + int i = 0; + while (g_variant_iter_next(data, "y", &px)) + { + item.iconData[i] = px; + i++; + } + for (int i = 0; i < width * height; i++) + { + struct Px + { + // This should be bgra... + // Since source is ARGB32 in network order(=big-endian) + // and x86 Linux is little-endian, we *should* swap b and r... + uint8_t a, r, g, b; + }; + Px& pixel = ((Px*)item.iconData)[i]; + // Swap to create rgba + pixel = {pixel.r, pixel.g, pixel.b, pixel.a}; + } + + g_variant_iter_free(data); + g_variant_iter_free(arrIter); + g_variant_unref(arr); + g_variant_unref(iconPixmap); + } + else + { + // Get icon theme path + GVariant* themePathVariant = getProperty("IconThemePath"); // Not defined by freedesktop, I think ayatana does this... + GVariant* iconNameVariant = getProperty("IconName"); + + std::string iconPath; + if (themePathVariant && iconNameVariant) + { + // Why GLib? + GVariant* themePathStr = nullptr; + g_variant_get(themePathVariant, "(v)", &themePathStr); + GVariant* iconNameStr = nullptr; + g_variant_get(iconNameVariant, "(v)", &iconNameStr); + + const char* themePath = g_variant_get_string(themePathStr, nullptr); + const char* iconName = g_variant_get_string(iconNameStr, nullptr); + iconPath = std::string(themePath) + "/" + iconName + ".png"; // TODO: Find out if this is always png + + g_variant_unref(themePathVariant); + g_variant_unref(themePathStr); + g_variant_unref(iconNameVariant); + g_variant_unref(iconNameStr); + } + else if (iconNameVariant) + { + GVariant* iconNameStr = nullptr; + g_variant_get(iconNameVariant, "(v)", &iconNameStr); + + const char* iconName = g_variant_get_string(iconNameStr, nullptr); + iconPath = std::string(iconName); + + g_variant_unref(iconNameVariant); + g_variant_unref(iconNameStr); + } + else + { + LOG("SNI: Unknown path!"); + return item; + } + + int width, height, channels; + stbi_uc* pixels = stbi_load(iconPath.c_str(), &width, &height, &channels, STBI_rgb_alpha); + if (!pixels) + { + LOG("SNI: Cannot open " << iconPath); + return item; + } + item.w = width; + item.h = height; + item.iconData = new uint8_t[width * height * 4]; + // Already rgba32 + memcpy(item.iconData, pixels, width * height * 4); + stbi_image_free(pixels); + } + return item; + } + + // Methods + static void RegisterItem(sniWatcher*, GDBusMethodInvocation* invocation, const char* service) + { + std::string name; + std::string object; + if (strncmp(service, "/", 1) == 0) + { + // service is object (used by e.g. ayatana -> steam, discord) + object = service; + name = g_dbus_method_invocation_get_sender(invocation); + } + else + { + // service is bus name (used by e.g. Telegram) + name = service; + object = "/StatusNotifierItem"; + } + auto it = std::find_if(items.begin(), items.end(), [&](const Item& item) + { + return item.name == name && item.object == object; + }); + if (it != items.end()) + { + LOG("Rejecting " << name << " " << object); + return; + } + // TODO: Add mechanism to remove items + LOG("SNI: Registered Item " << name << " " << object); + Item item = CreateItem(std::move(name), std::move(object)); + items.push_back(std::move(item)); + InvalidateWidget(); + } + static void RegisterHost(sniWatcher*, GDBusMethodInvocation*, const char*) + { + LOG("TODO: Implement RegisterHost!"); + } + + // Signals + static void ItemRegistered(sniWatcher*, const char*, void*) + { + // Don't care, since watcher and host will always be from gBar (at least for now) + } + static void ItemUnregistered(sniWatcher*, const char*, void*) + { + // Don't care, since watcher and host will always be from gBar (at least for now) + } + + void Init() + { + auto busAcquired = [](GDBusConnection* connection, const char*, void*) + { + GError* err = nullptr; + g_dbus_interface_skeleton_export((GDBusInterfaceSkeleton*)watcherSkeleton, connection, "/StatusNotifierWatcher", &err); + if (err) + { + LOG("Failed to connect to dbus! Error: " << err->message); + g_error_free(err); + return; + } + dbusConnection = connection; + + // Connect methods and signals + g_signal_connect(watcherSkeleton, "handle-register-status-notifier-item", G_CALLBACK(RegisterItem), nullptr); + g_signal_connect(watcherSkeleton, "handle-register-status-notifier-host", G_CALLBACK(RegisterHost), nullptr); + + g_signal_connect(watcherSkeleton, "status-notifier-item-registered", G_CALLBACK(ItemRegistered), nullptr); + g_signal_connect(watcherSkeleton, "status-notifier-item-unregistered", G_CALLBACK(ItemUnregistered), nullptr); + + // Host is always available + sni_watcher_set_is_status_notifier_host_registered(watcherSkeleton, true); + }; + auto emptyCallback = [](GDBusConnection*, const char*, void*) {}; + auto lostName = [](GDBusConnection*, const char*, void*) + { + LOG("Lost name!"); + }; + auto flags = G_BUS_NAME_OWNER_FLAGS_REPLACE | G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT; + g_bus_own_name(G_BUS_TYPE_SESSION, "org.kde.StatusNotifierWatcher", (GBusNameOwnerFlags)flags, +busAcquired, +emptyCallback, +lostName, + nullptr, nullptr); + watcherSkeleton = sni_watcher_skeleton_new(); + + std::string hostName = "org.kde.StatusNotifierHost-" + std::to_string(getpid()); + g_bus_own_name(G_BUS_TYPE_SESSION, hostName.c_str(), (GBusNameOwnerFlags)flags, +emptyCallback, +emptyCallback, +emptyCallback, nullptr, + nullptr); + } + + void Shutdown() {} +} diff --git a/src/SNI.h b/src/SNI.h new file mode 100644 index 0000000..0a9128e --- /dev/null +++ b/src/SNI.h @@ -0,0 +1,8 @@ +#pragma once +class Widget; +namespace SNI +{ + void Init(); + void WidgetSNI(Widget& parent); + void Shutdown(); +} diff --git a/src/System.cpp b/src/System.cpp index f39b1c3..f5499b0 100644 --- a/src/System.cpp +++ b/src/System.cpp @@ -5,6 +5,7 @@ #include "PulseAudio.h" #include "Workspaces.h" #include "Config.h" +#include "SNI.h" #include #include @@ -646,6 +647,8 @@ namespace System PulseAudio::Init(); + SNI::Init(); + CheckNetwork(); } void FreeResources() @@ -662,6 +665,8 @@ namespace System #ifdef WITH_BLUEZ StopBTScan(); #endif + SNI::Shutdown(); + Logging::Shutdown(); } } diff --git a/src/Widget.cpp b/src/Widget.cpp index 0e62a27..59b36c3 100644 --- a/src/Widget.cpp +++ b/src/Widget.cpp @@ -122,10 +122,32 @@ void Widget::RemoveChild(size_t idx) if (m_Widget) { auto& child = *m_Childs[idx]; - gtk_container_remove((GtkContainer*)child.m_Widget, m_Widget); + gtk_container_remove((GtkContainer*)m_Widget, child.m_Widget); + child.m_Widget = nullptr; } m_Childs.erase(m_Childs.begin() + idx); } +void Widget::RemoveChild(Widget* widget) +{ + auto it = std::find_if(m_Childs.begin(), m_Childs.end(), + [&](std::unique_ptr& other) + { + return other.get() == widget; + }); + if (it != m_Childs.end()) + { + if (m_Widget) + { + gtk_container_remove((GtkContainer*)m_Widget, it->get()->m_Widget); + it->get()->m_Widget = nullptr; + } + m_Childs.erase(it); + } + else + { + LOG("Invalid child!"); + } +} void Widget::SetVisible(bool visible) { @@ -475,6 +497,30 @@ void NetworkSensor::Draw(cairo_t* cr) gdk_rgba_free(colDown); } +Texture::~Texture() +{ + if (m_Pixbuf) + g_free(m_Pixbuf); + if (m_Bytes) + g_free(m_Bytes); +} + +void Texture::SetBuf(size_t width, size_t height, uint8_t* buf) +{ + m_Width = width; + m_Height = height; + m_Bytes = g_bytes_new(buf, m_Width * m_Height * 4); + m_Pixbuf = gdk_pixbuf_new_from_bytes((GBytes*)m_Bytes, GDK_COLORSPACE_RGB, true, 8, m_Width, m_Height, m_Width * 4); +} + +void Texture::Draw(cairo_t* cr) +{ + // TODO: W + H + cairo_rectangle(cr, 0.f, 0.f, 32.f, 32.f); + gdk_cairo_set_source_pixbuf(cr, m_Pixbuf, 0, 0); + cairo_fill(cr); +} + void Revealer::SetTransition(Transition transition) { m_Transition = transition; diff --git a/src/Widget.h b/src/Widget.h index 03b1094..150e71f 100644 --- a/src/Widget.h +++ b/src/Widget.h @@ -116,6 +116,7 @@ public: void AddChild(std::unique_ptr&& widget); void RemoveChild(size_t idx); + void RemoveChild(Widget* widget); std::vector>& GetWidgets() { return m_Childs; } @@ -263,6 +264,24 @@ private: std::unique_ptr contextDown; }; +class Texture : public CairoArea +{ +public: + Texture() = default; + virtual ~Texture(); + + // Non-Owning, ARGB32 + void SetBuf(size_t width, size_t height, uint8_t* buf); + +private: + void Draw(cairo_t* cr) override; + + size_t m_Width; + size_t m_Height; + GBytes* m_Bytes; + GdkPixbuf* m_Pixbuf; +}; + class Revealer : public Widget { public: diff --git a/thirdparty/stb b/thirdparty/stb new file mode 160000 index 0000000..5736b15 --- /dev/null +++ b/thirdparty/stb @@ -0,0 +1 @@ +Subproject commit 5736b15f7ea0ffb08dd38af21067c314d6a3aae9