diff --git a/CMakeLists.txt b/CMakeLists.txt index e6c9dc8..a773079 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,7 @@ target_sources(bar_lib src/connection/httpConnection.cpp src/connection/dbus/notification.cpp src/connection/dbus/mpris.cpp + src/connection/dbus/bluetooth.cpp src/connection/dbus/tray.cpp src/widgets/clock.cpp @@ -60,15 +61,15 @@ target_sources(bar_lib src/widgets/controlCenter/timer.cpp src/widgets/volumeWidget.cpp src/widgets/weather.cpp - src/widgets/wallpaperWindow.cpp src/widgets/webWidget.cpp src/widgets/tray.cpp + src/widgets/controlCenter/bluetoothSettings.cpp + src/widgets/controlCenter/settings.cpp src/services/hyprland.cpp src/services/notificationController.cpp src/services/textureCache.cpp - src/components/popover.cpp src/components/workspaceIndicator.cpp ) diff --git a/include/app.hpp b/include/app.hpp index a98c564..e51f1a6 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -1,31 +1,36 @@ #pragma once -#include +#include #include "bar/bar.hpp" #include "connection/dbus/notification.hpp" #include "connection/dbus/tray.hpp" #include "services/hyprland.hpp" -#include "widgets/wallpaperWindow.hpp" +#include "services/notificationController.hpp" +#include "gdkmm/monitor.h" #include "glibmm/refptr.h" #include "gtkmm/application.h" -class MprisController; - class App { public: + struct BarMonitorPair { + std::shared_ptr bar; + std::shared_ptr window; + }; + App(); int run(); private: Glib::RefPtr app; - std::vector> bars; - std::vector> wallpaperWindows; - std::shared_ptr notificationService = nullptr; - std::shared_ptr mprisController = nullptr; - HyprlandService *hyprlandService = nullptr; + std::map bars; + + std::shared_ptr notificationService = NotificationService::getInstance(); + std::shared_ptr notificationController = NotificationController::getInstance(); + std::shared_ptr mprisController = MprisController::getInstance(); + std::shared_ptr hyprlandService = HyprlandService::getInstance(); TrayService *trayService = TrayService::getInstance(); void setupServices(); diff --git a/include/components/button/iconButton.hpp b/include/components/button/iconButton.hpp index 1931f64..691ecbd 100644 --- a/include/components/button/iconButton.hpp +++ b/include/components/button/iconButton.hpp @@ -7,6 +7,6 @@ class IconButton : public TextButton { public: - IconButton(Icon::Type icon, std::string fontFamilyCss = "materia-icons"); + IconButton(Icon::Type icon, std::string fontFamilyCss = "material-icons"); void setIcon(Icon::Type icon); }; \ No newline at end of file diff --git a/include/components/types/icon.hpp b/include/components/types/icon.hpp index 7e8e34e..ed6cde7 100644 --- a/include/components/types/icon.hpp +++ b/include/components/types/icon.hpp @@ -22,6 +22,10 @@ class Icon { CONTENT_COPY, TOKEN, + + SETTINGS, + POWER_SETTINGS_NEW, + BLUETOOTH_SEARCHING, }; static const std::string toString(Type type) { @@ -46,5 +50,9 @@ class Icon { {CONTENT_COPY, "\ue14d"}, {TOKEN, "\uea25"}, + + {SETTINGS, "\ue8b8"}, + {POWER_SETTINGS_NEW, "\ue8ac"}, + {BLUETOOTH_SEARCHING, "\ue1aa"}, }; }; \ No newline at end of file diff --git a/include/connection/dbus/bluetooth.hpp b/include/connection/dbus/bluetooth.hpp new file mode 100644 index 0000000..9849c7d --- /dev/null +++ b/include/connection/dbus/bluetooth.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "connection/dbus/dbus.hpp" + +struct BluetoothDevice { + std::string object_path; + std::string address; + std::string name; + std::string icon; + bool paired = false; + bool connected = false; + bool trusted = false; + int16_t rssi = 0; +}; + +class BluetoothController : public DbusConnection { + using PropertiesMap = std::map; + + public: + static std::shared_ptr getInstance() { + if (!instance) { + instance = std::shared_ptr(new BluetoothController()); + } + return instance; + } + + // Adapter control + void setPowered(bool powered); + bool isPowered() const; + + // Discovery + void startDiscovery(); + void stopDiscovery(); + bool isDiscovering() const; + + // Device actions (identified by object_path) + void pairDevice(const std::string &object_path); + void unpairDevice(const std::string &object_path); + void connectDevice(const std::string &object_path); + void disconnectDevice(const std::string &object_path); + void trustDevice(const std::string &object_path, bool trusted); + + // Queries + std::vector getDevices() const; + std::vector getPairedDevices() const; + + // Signals + sigc::signal &signalPoweredChanged(); + sigc::signal &signalDiscoveringChanged(); + sigc::signal &signalDeviceAdded(); + sigc::signal &signalDeviceRemoved(); + sigc::signal &signalDeviceChanged(); + + private: + BluetoothController(); + + inline static std::shared_ptr instance = nullptr; + + bool m_powered = false; + bool m_discovering = false; + + std::string m_adapter_path; + Glib::RefPtr m_adapter_proxy; + Glib::RefPtr m_object_manager_proxy; + + std::map m_devices; + std::map> m_device_proxies; + + sigc::signal m_powered_signal; + sigc::signal m_discovering_signal; + sigc::signal m_device_added_signal; + sigc::signal m_device_removed_signal; + sigc::signal m_device_changed_signal; + + // Bus setup + void onBusConnected(const Glib::RefPtr &result); + void enumerateObjects(); + void parseInterfaces(const std::string &object_path, const Glib::VariantBase &interfaces_var); + + // ObjectManager signals + void onObjectManagerSignal(const Glib::ustring &sender_name, + const Glib::ustring &signal_name, + const Glib::VariantContainerBase ¶meters); + + // Adapter + void setupAdapter(const std::string &path, const PropertiesMap &properties); + void onAdapterPropertiesChanged(const Gio::DBus::Proxy::MapChangedProperties &changed, + const std::vector &invalidated); + + // Devices + void addDevice(const std::string &path, const PropertiesMap &properties); + void removeDevice(const std::string &path); + void onDevicePropertiesChanged(const std::string &object_path, + const Gio::DBus::Proxy::MapChangedProperties &changed, + const std::vector &invalidated); + + // Helpers + static BluetoothDevice parseDeviceProperties(const std::string &path, const PropertiesMap &properties); + void setDbusProperty(const std::string &object_path, + const std::string &interface, + const std::string &property, + const Glib::VariantBase &value); +}; diff --git a/include/connection/dbus/dbus.hpp b/include/connection/dbus/dbus.hpp index c14846a..7aef2da 100644 --- a/include/connection/dbus/dbus.hpp +++ b/include/connection/dbus/dbus.hpp @@ -14,6 +14,10 @@ class DbusConnection { Gio::DBus::Connection::get(Gio::DBus::BusType::SESSION, callback); } + void connect_system_async(const sigc::slot &)> &callback) { + Gio::DBus::Connection::get(Gio::DBus::BusType::SYSTEM, callback); + } + static void ensure_gio_init() { try { Gio::init(); diff --git a/include/connection/dbus/notification.hpp b/include/connection/dbus/notification.hpp index d4a2246..f58b940 100644 --- a/include/connection/dbus/notification.hpp +++ b/include/connection/dbus/notification.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "connection/dbus/dbus.hpp" @@ -43,7 +44,17 @@ const Glib::ustring introspection_xml = R"( class NotificationService : public DbusConnection { public: - NotificationService() : notificationIdCounter(1) { + static std::shared_ptr getInstance() { + if (NotificationService::instance == nullptr) { + NotificationService::instance = std::shared_ptr(new NotificationService()); + } + return NotificationService::instance; + } + + void onBusAcquired(const Glib::RefPtr &connection, const Glib::ustring &name); + + private: + NotificationService() { Gio::DBus::own_name( Gio::DBus::BusType::SESSION, "org.freedesktop.Notifications", @@ -53,10 +64,9 @@ class NotificationService : public DbusConnection { Gio::DBus::BusNameOwnerFlags::REPLACE); } - void onBusAcquired(const Glib::RefPtr &connection, const Glib::ustring &name); + static inline std::shared_ptr instance = nullptr; - private: - guint notificationIdCounter; + guint notificationIdCounter = 0; const Gio::DBus::InterfaceVTable &getMessageInterfaceVTable(); void on_method_call(const Glib::RefPtr &connection, const Glib::ustring &sender, diff --git a/include/services/bluetooth.hpp b/include/services/bluetooth.hpp deleted file mode 100644 index a45bf50..0000000 --- a/include/services/bluetooth.hpp +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "sigc++/signal.h" - -class BluetoothService { - inline static BluetoothService *instance = nullptr; - - public: - sigc::signal powerStateChangedSignal; - sigc::signal isDiscoveringChangedSignal; - - bool getPowerState(); - bool getIsDiscovering(); - - void togglePowerState(); - void toggleIsDiscovering(); - - static BluetoothService *getInstance() { - if (BluetoothService::instance == nullptr) { - - BluetoothService::instance = new BluetoothService(); - } - return BluetoothService::instance; - } - - private: - BluetoothService(); - - GDBusProxy *adapter_proxy = nullptr; - - std::vector getDeviceObjectPaths(); - bool powerState = false; - bool isDiscovering = false; - - void onPropertyChanged(GDBusProxy *proxy, - GVariant *changed_properties, - const gchar *const *invalidated_properties, - gpointer user_data); - - static void onPropertyChangedStatic(GDBusProxy *proxy, - GVariant *changed_properties, - const gchar *const *invalidated_properties, - gpointer user_data); -}; \ No newline at end of file diff --git a/include/services/hyprland.hpp b/include/services/hyprland.hpp index 5242bf1..a1de68a 100644 --- a/include/services/hyprland.hpp +++ b/include/services/hyprland.hpp @@ -17,7 +17,8 @@ #define NUM_WORKSPACES 7 class HyprlandService { - inline static HyprlandService *instance = nullptr; + static inline std::shared_ptr instance; + public: struct Client { @@ -49,9 +50,9 @@ class HyprlandService { std::shared_ptr bar; }; - static HyprlandService *getInstance() { + static std::shared_ptr getInstance() { if (!instance) { - instance = new HyprlandService(); + instance = std::shared_ptr(new HyprlandService()); } return instance; } @@ -60,7 +61,12 @@ class HyprlandService { void addBar(std::shared_ptr bar, std::string monitorName); + sigc::signal &signal_monitor_added() { return m_signal_monitor_added; } + sigc::signal &signal_monitor_removed() { return m_signal_monitor_removed; } + private: + HyprlandService(); + enum SocketEventType { WORKSPACE_CHANGED, @@ -72,6 +78,7 @@ class HyprlandService { FOCUSED_MONITOR, MONITOR_REMOVED, + MONITOR_ADDED, }; std::map socketEventTypeMap = { @@ -83,7 +90,9 @@ class HyprlandService { {"urgent", URGENT}, {"focusedmon", FOCUSED_MONITOR}, {"monitorremoved", MONITOR_REMOVED}, + {"monitoradded", MONITOR_ADDED}, }; + void onWorkspaceChanged(int workspaceId); void onFocusedMonitorChanged(std::string monitorData); void onOpenWindow(std::string windowData); @@ -92,16 +101,15 @@ class HyprlandService { void onUrgent(std::string windowAddress); void onActiveWindowChanged(std::string windowAddress); void onMonitorRemoved(std::string monitorName); - // void onMonitorAdded(std::string monitorName); + void onMonitorAdded(std::string monitorName); - HyprlandService(); std::map> monitors; std::map> workspaces; std::map> clients; /// maybe refactor into reusable class std::string socketBuffer; - int socketFd; + int socketFd = -1; /// void bindHyprlandSocket(); @@ -112,4 +120,7 @@ class HyprlandService { void refreshIndicator(std::shared_ptr workspace); void handleSocketMessage(SocketHelper::SocketMessage message); + + sigc::signal m_signal_monitor_added; + sigc::signal m_signal_monitor_removed; }; diff --git a/include/services/notificationController.hpp b/include/services/notificationController.hpp index 0b8a26f..295f28c 100644 --- a/include/services/notificationController.hpp +++ b/include/services/notificationController.hpp @@ -12,11 +12,11 @@ #include "gdkmm/monitor.h" class NotificationController { - static std::shared_ptr instance; + inline static std::shared_ptr instance = nullptr; public: static std::shared_ptr getInstance() { - if (!NotificationController::instance) { + if (NotificationController::instance == nullptr) { NotificationController::instance = std::shared_ptr(new NotificationController()); } return NotificationController::instance; @@ -26,12 +26,15 @@ class NotificationController { void showNotificationOnAllMonitors(NotifyMessage notify); void showCopyNotification(NotifyMessage notify); + void addMonitor(std::shared_ptr monitor); + void removeMonitor(std::shared_ptr monitor); + private: uint64_t globalNotificationId = 1; std::map>> activeNotifications; std::map hoverCounts; NotificationController(); - std::vector> activeMonitors; + std::map> activeMonitors; void updateHoverState(uint64_t notificationId, bool isHovered); void closeNotification(uint64_t notificationId); }; \ No newline at end of file diff --git a/include/widgets/controlCenter/bluetoothSettings.hpp b/include/widgets/controlCenter/bluetoothSettings.hpp new file mode 100644 index 0000000..8d15341 --- /dev/null +++ b/include/widgets/controlCenter/bluetoothSettings.hpp @@ -0,0 +1,115 @@ +#pragma once + +#include "components/button/iconButton.hpp" +#include "connection/dbus/bluetooth.hpp" + +#include "gtkmm/box.h" +#include "gtkmm/button.h" +#include "gtkmm/image.h" +#include "gtkmm/label.h" + +class BluetoothSettingsRow : public Gtk::Box { + public: + BluetoothSettingsRow(const BluetoothDevice &device) : device(device) { + set_orientation(Gtk::Orientation::HORIZONTAL); + set_spacing(10); + set_margin_bottom(6); + + + if (!device.icon.empty()) { + this->icon.set_from_icon_name(device.icon); + this->icon.set_pixel_size(24); + append(this->icon); + } + + nameLabel.set_text(device.name.empty() ? "Unknown Device" : device.name); + nameLabel.set_halign(Gtk::Align::START); + nameLabel.set_valign(Gtk::Align::CENTER); + append(nameLabel); + + addressLabel.set_text(device.address); + addressLabel.set_halign(Gtk::Align::START); + addressLabel.set_valign(Gtk::Align::CENTER); + addressLabel.add_css_class("bluetooth-device-address"); + append(addressLabel); + + pairButton.set_label(device.paired ? "Unpair" : "Pair"); + pairButton.signal_clicked().connect([device]() { + if (device.paired) { + BluetoothController::getInstance()->unpairDevice(device.object_path); + } else { + BluetoothController::getInstance()->pairDevice(device.object_path); + } + }); + append(pairButton); + + connectButton.set_label(device.connected ? "Disconnect" : "Connect"); + connectButton.signal_clicked().connect([device]() { + if (device.connected) { + BluetoothController::getInstance()->disconnectDevice(device.object_path); + } else { + BluetoothController::getInstance()->connectDevice(device.object_path); + } + }); + append(connectButton); + + trustButton.set_label(device.trusted ? "Distrust" : "Trust"); + trustButton.signal_clicked().connect([device]() { + BluetoothController::getInstance()->trustDevice(device.object_path, !device.trusted); + }); + append(trustButton); + } + + void updateDevice(const BluetoothDevice &device) { + this->device = device; + + if (!device.icon.empty()) { + this->icon.set_from_icon_name(device.icon); + } + + nameLabel.set_text(device.name.empty() ? "Unknown Device" : device.name); + addressLabel.set_text(device.address); + pairButton.set_label(device.paired ? "Unpair" : "Pair"); + connectButton.set_label(device.connected ? "Disconnect" : "Connect"); + trustButton.set_label(device.trusted ? "Distrust" : "Trust"); + } + + private: + BluetoothDevice device; + + Gtk::Image icon; + Gtk::Label nameLabel; + Gtk::Label addressLabel; + Gtk::Button pairButton; + Gtk::Button connectButton; + Gtk::Button trustButton; +}; + +class BluetoothSettings : public Gtk::Box { + public: + BluetoothSettings(); + + private: + std::shared_ptr bluetoothController = BluetoothController::getInstance(); + std::map activeBluetoothDevices; + std::map> deviceRows; + + + std::shared_ptr powerButton = std::make_shared(Icon::POWER_SETTINGS_NEW); + std::shared_ptr scanButton = std::make_shared(Icon::BLUETOOTH_SEARCHING); + + + + Gtk::Box connectedDevicesBox; + Gtk::Box availableDevicesBox; + + bool bluetoothIsPowered = false; + bool bluetoothIsScanning = false; + + void addBluetoothDevice(const BluetoothDevice &device); + void removeBluetoothDevice(const std::string &object_path); + void updateBluetoothDevice(const BluetoothDevice &device); + + void setBluetoothPowered(bool powered); + void setScanning(bool scanning); +}; \ No newline at end of file diff --git a/include/widgets/controlCenter/controlCenter.hpp b/include/widgets/controlCenter/controlCenter.hpp index f463e62..e7412e5 100644 --- a/include/widgets/controlCenter/controlCenter.hpp +++ b/include/widgets/controlCenter/controlCenter.hpp @@ -6,6 +6,7 @@ #include "components/button/tabButton.hpp" #include "components/popover.hpp" #include "widgets/controlCenter/mediaWidget.hpp" +#include "widgets/controlCenter/settings.hpp" #include "widgets/controlCenter/timer.hpp" #include "widgets/weather.hpp" @@ -27,10 +28,12 @@ class ControlCenter : public Popover { std::unique_ptr mediaTabButton; std::unique_ptr infoTabButton; std::unique_ptr timerButton; + std::unique_ptr settingsTabButton; std::unique_ptr weatherWidget; std::unique_ptr mediaControlWidget; std::unique_ptr timerWidget; + std::unique_ptr settingsWidget; void addPlayerWidget(const std::string &bus_name); void removePlayerWidget(const std::string &bus_name); diff --git a/include/widgets/controlCenter/settings.hpp b/include/widgets/controlCenter/settings.hpp new file mode 100644 index 0000000..9bce8c9 --- /dev/null +++ b/include/widgets/controlCenter/settings.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include "connection/dbus/bluetooth.hpp" +#include "widgets/controlCenter/bluetoothSettings.hpp" +#include "gtkmm/box.h" + +class SettingsWidget : public Gtk::Box { + public: + SettingsWidget(); + private: + BluetoothSettings bluetoothSettings; + +}; \ No newline at end of file diff --git a/include/widgets/controlCenter/timer.hpp b/include/widgets/controlCenter/timer.hpp index 7aba82a..99bb5ec 100644 --- a/include/widgets/controlCenter/timer.hpp +++ b/include/widgets/controlCenter/timer.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "components/timer.hpp" #include "services/timerService.hpp" @@ -9,6 +10,7 @@ class TimerWidget : public Gtk::Box { public: TimerWidget(); + ~TimerWidget(); void addTimer(const std::string &duration, uint64_t timerId); void removeTimer(uint64_t timerId); void activateTimer(uint64_t timerId); @@ -19,4 +21,8 @@ class TimerWidget : public Gtk::Box { std::string rawDigits; std::map> activeTimers; + sigc::connection timerSetConnection; + sigc::connection timerCancelledConnection; + sigc::connection timerExpiredConnection; + sigc::connection tickConnection; }; \ No newline at end of file diff --git a/include/widgets/wallpaperWindow.hpp b/include/widgets/wallpaperWindow.hpp deleted file mode 100644 index 14086e6..0000000 --- a/include/widgets/wallpaperWindow.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include -#include -#include - -class WallpaperWindow : public Gtk::Window { - public: - WallpaperWindow(GdkMonitor *monitor, const std::string &imagePath); - - private: - Gtk::Picture picture; - - static std::string expand_user_path(const std::string &path); - static Glib::RefPtr load_texture(const std::string &path); -}; diff --git a/resources/bar.css b/resources/bar.css index 26764c1..642d25e 100644 --- a/resources/bar.css +++ b/resources/bar.css @@ -16,7 +16,10 @@ window { color: #ffffff; font-size: 14px; font-family: var(--text-font); - padding: 2px 6px; +} + +.bar { + padding: 4px 6px; } .text-area { @@ -25,16 +28,17 @@ window { border-radius: 8px; padding: 4px 8px; } + .material-icons { font-family: var(--icon-font-material); } -.icon-button { - font-size: 20px; +.power-button-on { + color: #4caf50; } -.tab-icon { - font-size: 20px; +.power-button-off { + color: #f44336; } .control-center-tab-row { @@ -207,6 +211,17 @@ tooltip { margin-top: 4px; } + + +.icon-button { + font-weight: 700; + font-size: 18px; +} + +.tab-icon { +} + + @keyframes workspace-updown { 0% { transform: translateY(4px); diff --git a/src/app.cpp b/src/app.cpp index 17f3af0..8ac60a7 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1,27 +1,89 @@ #include "app.hpp" +#include #include -#include #include "connection/dbus/mpris.hpp" #include "connection/dbus/notification.hpp" #include "services/notificationController.hpp" #include "services/textureCache.hpp" -#include "widgets/wallpaperWindow.hpp" -App::App() { - this->app = Gtk::Application::create("org.example.mybar"); +App::App() : app(Gtk::Application::create("org.example.mybar")) { this->setupServices(); - this->hyprlandService = HyprlandService::getInstance(); - this->notificationService = std::make_shared(); - this->mprisController = MprisController::getInstance(); - auto notificationController = NotificationController::getInstance(); this->mprisController->signal_mpris_updated().connect( - [notificationController](const MprisPlayer2Message &msg) { - notificationController->showSpotifyNotification(msg); + [this](const MprisPlayer2Message &msg) { + this->notificationController->showSpotifyNotification(msg); }); + this->hyprlandService->signal_monitor_added().connect([this](std::string monitorName) { + spdlog::info("[App] Monitor added: {}", monitorName); + auto tryCreateBar = [this](const std::string &name) -> bool { + if (bars.find(name) != bars.end()) { + return true; + } + + auto display = Gdk::Display::get_default(); + if (!display) { + return false; + } + + auto monitors = display->get_monitors(); + for (guint i = 0; i < monitors->get_n_items(); ++i) { + auto monitor = std::dynamic_pointer_cast( + monitors->get_object(i)); + + if (monitor && monitor->get_connector() == name) { + auto bar = std::make_shared(monitor->gobj()); + + bar->set_application(app); + bar->show(); + + std::string connectorName = monitor->get_connector(); + bar->addLeftWidget(hyprlandService->getWorkspaceIndicatorsForMonitor(connectorName)); + hyprlandService->addBar(bar, connectorName); + + BarMonitorPair barMonitorPair{.bar = bar, .window = monitor}; + bars[connectorName] = barMonitorPair; + if (this->notificationController) { + this->notificationController->addMonitor(monitor); + } + + return true; + } + } + + return false; + }; + + if (tryCreateBar(monitorName)) { + return; + } + + auto attempts = std::make_shared(0); + Glib::signal_timeout().connect( + [monitorName, attempts, tryCreateBar]() -> bool { + if (*attempts >= 10) { + spdlog::warn("[App] Monitor {} not ready after retries", monitorName); + return false; + } + (*attempts)++; + return !tryCreateBar(monitorName); + }, + 200); + }); + + this->hyprlandService->signal_monitor_removed().connect([this](std::string monitorName) { + spdlog::info("[App] Monitor removed: {}", monitorName); + auto bar = bars[monitorName].bar; + + if (bar) { + bar->close(); + } + + bars.erase(monitorName); + }); + app->signal_activate().connect([&]() { auto display = Gdk::Display::get_default(); auto monitors = display->get_monitors(); @@ -40,7 +102,12 @@ App::App() { bar->addLeftWidget(hyprlandService->getWorkspaceIndicatorsForMonitor(monitorName)); hyprlandService->addBar(bar, monitorName); - bars.push_back(bar); + BarMonitorPair barMonitorPair{.bar = bar, .window = monitor}; + + bars[monitorName] = barMonitorPair; + if (this->notificationController) { + this->notificationController->addMonitor(monitor); + } } } }); diff --git a/src/bar/bar.cpp b/src/bar/bar.cpp index eb61508..ec56952 100644 --- a/src/bar/bar.cpp +++ b/src/bar/bar.cpp @@ -6,6 +6,7 @@ #include #include "components/button/textButton.hpp" +#include "connection/dbus/bluetooth.hpp" #include "helpers/system.hpp" #include "widgets/date.hpp" #include "widgets/spacer.hpp" @@ -13,10 +14,12 @@ #include "glibmm/main.h" #include "gtk/gtk.h" -#include "sigc++/functors/mem_fun.h" Bar::Bar(GdkMonitor *monitor) { set_name("bar-window"); + add_css_class("bar"); + + auto bluetooh = BluetoothController::getInstance(); gtk_layer_init_for_window(this->gobj()); @@ -32,6 +35,7 @@ Bar::Bar(GdkMonitor *monitor) { gtk_layer_auto_exclusive_zone_enable(this->gobj()); set_child(main_box); + set_valign(Gtk::Align::CENTER); this->volumeWidget = std::make_shared(); this->trayWidget = std::make_shared(); @@ -43,7 +47,7 @@ Bar::Bar(GdkMonitor *monitor) { date.onUpdate(); Glib::signal_timeout().connect(sigc::mem_fun(clock, &Clock::onUpdate), 1000); -Glib::signal_timeout().connect(sigc::mem_fun(date, &Date::onUpdate), 1000); + Glib::signal_timeout().connect(sigc::mem_fun(date, &Date::onUpdate), 1000); } void Bar::setup_ui() { diff --git a/src/components/button/iconButton.cpp b/src/components/button/iconButton.cpp index 0b7ac5b..53b7ca5 100644 --- a/src/components/button/iconButton.cpp +++ b/src/components/button/iconButton.cpp @@ -2,7 +2,7 @@ IconButton::IconButton(Icon::Type icon, std::string fontFamilyCss) : TextButton(Icon::toString(icon)) { this->get_style_context()->add_class(fontFamilyCss); - this->add_css_class("icon-button"); + this->get_style_context()->add_class("icon-button"); } void IconButton::setIcon(Icon::Type icon) { diff --git a/src/components/popover.cpp b/src/components/popover.cpp index 81a1594..b3b6154 100644 --- a/src/components/popover.cpp +++ b/src/components/popover.cpp @@ -7,14 +7,18 @@ Popover::Popover(Icon::Type icon, std::string name) : IconButton(icon) { set_name(name); - this->add_css_class("material-icons"); - popover = std::make_unique(); popover->set_parent(*this); popover->set_autohide(true); } -Popover::~Popover() = default; +Popover::~Popover() { + if (popover) { + popover->popdown(); + popover->unparent(); + + } +}; void Popover::on_toggle_window() { if (popover->get_visible()) { @@ -22,4 +26,4 @@ void Popover::on_toggle_window() { } else { popover->popup(); } -} +} \ No newline at end of file diff --git a/src/connection/dbus/bluetooth.cpp b/src/connection/dbus/bluetooth.cpp new file mode 100644 index 0000000..221e656 --- /dev/null +++ b/src/connection/dbus/bluetooth.cpp @@ -0,0 +1,448 @@ +#include "connection/dbus/bluetooth.hpp" + +#include + +// --------------------------------------------------------------------------- +// Constructor +// --------------------------------------------------------------------------- + +BluetoothController::BluetoothController() { + connect_system_async(sigc::mem_fun(*this, &BluetoothController::onBusConnected)); +} + +// --------------------------------------------------------------------------- +// Signal accessors +// --------------------------------------------------------------------------- + +sigc::signal &BluetoothController::signalPoweredChanged() { return m_powered_signal; } +sigc::signal &BluetoothController::signalDiscoveringChanged() { return m_discovering_signal; } +sigc::signal &BluetoothController::signalDeviceAdded() { return m_device_added_signal; } +sigc::signal &BluetoothController::signalDeviceRemoved() { return m_device_removed_signal; } +sigc::signal &BluetoothController::signalDeviceChanged() { return m_device_changed_signal; } + +// --------------------------------------------------------------------------- +// Queries +// --------------------------------------------------------------------------- + +bool BluetoothController::isPowered() const { return m_powered; } +bool BluetoothController::isDiscovering() const { return m_discovering; } + +std::vector BluetoothController::getDevices() const { + std::vector result; + result.reserve(m_devices.size()); + for (const auto &[_, device] : m_devices) { + result.push_back(device); + } + return result; +} + +std::vector BluetoothController::getPairedDevices() const { + std::vector result; + for (const auto &[_, device] : m_devices) { + if (device.paired) { + result.push_back(device); + } + } + return result; +} + +// --------------------------------------------------------------------------- +// Adapter control +// --------------------------------------------------------------------------- + +void BluetoothController::setPowered(bool powered) { + spdlog::info("Bluetooth: setting powered to {}", powered); + setDbusProperty(m_adapter_path, "org.bluez.Adapter1", "Powered", + Glib::Variant::create(powered)); +} + +void BluetoothController::startDiscovery() { + spdlog::info("Bluetooth: starting discovery"); + if (m_adapter_proxy) { + m_adapter_proxy->call("StartDiscovery"); + } +} + +void BluetoothController::stopDiscovery() { + spdlog::info("Bluetooth: stopping discovery"); + if (m_adapter_proxy) { + m_adapter_proxy->call("StopDiscovery"); + } +} + +// --------------------------------------------------------------------------- +// Device actions +// --------------------------------------------------------------------------- + +void BluetoothController::pairDevice(const std::string &object_path) { + spdlog::info("Bluetooth: pairing device {}", object_path); + auto it = m_device_proxies.find(object_path); + if (it != m_device_proxies.end() && it->second) { + it->second->call("Pair"); + } +} + +void BluetoothController::unpairDevice(const std::string &object_path) { + spdlog::info("Bluetooth: unpairing device {}", object_path); + if (!m_adapter_proxy) return; + + auto params = Glib::VariantContainerBase::create_tuple( + Glib::Variant::create(object_path)); + m_adapter_proxy->call("RemoveDevice", params); +} + +void BluetoothController::connectDevice(const std::string &object_path) { + spdlog::info("Bluetooth: connecting device {}", object_path); + auto it = m_device_proxies.find(object_path); + if (it != m_device_proxies.end() && it->second) { + it->second->call("Connect"); + } +} + +void BluetoothController::disconnectDevice(const std::string &object_path) { + spdlog::info("Bluetooth: disconnecting device {}", object_path); + auto it = m_device_proxies.find(object_path); + if (it != m_device_proxies.end() && it->second) { + it->second->call("Disconnect"); + } +} + +void BluetoothController::trustDevice(const std::string &object_path, bool trusted) { + spdlog::info("Bluetooth: setting trusted={} for device {}", trusted, object_path); + setDbusProperty(object_path, "org.bluez.Device1", "Trusted", + Glib::Variant::create(trusted)); +} + +// --------------------------------------------------------------------------- +// Bus connection +// --------------------------------------------------------------------------- + +void BluetoothController::onBusConnected(const Glib::RefPtr &result) { + if (!result) { + spdlog::error("Bluetooth: null async result"); + return; + } + + try { + connection = Gio::DBus::Connection::get_finish(result); + + m_object_manager_proxy = Gio::DBus::Proxy::create_sync( + connection, + "org.bluez", + "/", + "org.freedesktop.DBus.ObjectManager"); + + if (m_object_manager_proxy) { + m_object_manager_proxy->signal_signal().connect( + sigc::mem_fun(*this, &BluetoothController::onObjectManagerSignal)); + + enumerateObjects(); + } + } catch (const Glib::Error &ex) { + spdlog::error("Bluetooth DBus Connection Error: {}", ex.what()); + } +} + +// --------------------------------------------------------------------------- +// Enumerate existing BlueZ objects +// --------------------------------------------------------------------------- + +void BluetoothController::enumerateObjects() { + if (!m_object_manager_proxy) return; + + try { + auto result = m_object_manager_proxy->call_sync("GetManagedObjects"); + auto objects = Glib::VariantBase::cast_dynamic( + result.get_child(0)); + + for (gsize i = 0; i < objects.get_n_children(); i++) { + auto entry = Glib::VariantBase::cast_dynamic( + objects.get_child(i)); + + auto path = static_cast( + Glib::VariantBase::cast_dynamic>( + entry.get_child(0)) + .get()); + + parseInterfaces(path, entry.get_child(1)); + } + } catch (const Glib::Error &ex) { + spdlog::error("Bluetooth: Error enumerating objects: {}", ex.what()); + } +} + +// --------------------------------------------------------------------------- +// Parse an interfaces-and-properties dict a{sa{sv}} +// --------------------------------------------------------------------------- + +void BluetoothController::parseInterfaces(const std::string &object_path, + const Glib::VariantBase &interfaces_var) { + auto ifaces = Glib::VariantBase::cast_dynamic(interfaces_var); + + for (gsize i = 0; i < ifaces.get_n_children(); i++) { + auto entry = Glib::VariantBase::cast_dynamic( + ifaces.get_child(i)); + + auto iface_name = static_cast( + Glib::VariantBase::cast_dynamic>( + entry.get_child(0)) + .get()); + + if (iface_name != "org.bluez.Device1" && iface_name != "org.bluez.Adapter1") { + continue; + } + + auto props = Glib::VariantBase::cast_dynamic>( + entry.get_child(1)) + .get(); + + if (iface_name == "org.bluez.Device1") { + addDevice(object_path, props); + } else { + setupAdapter(object_path, props); + } + } +} + +// --------------------------------------------------------------------------- +// ObjectManager signal handler (InterfacesAdded / InterfacesRemoved) +// --------------------------------------------------------------------------- + +void BluetoothController::onObjectManagerSignal( + const Glib::ustring &, + const Glib::ustring &signal_name, + const Glib::VariantContainerBase ¶meters) { + + if (signal_name == "InterfacesAdded") { + // signature: (oa{sa{sv}}) + auto path = static_cast( + Glib::VariantBase::cast_dynamic>( + parameters.get_child(0)) + .get()); + + parseInterfaces(path, parameters.get_child(1)); + + } else if (signal_name == "InterfacesRemoved") { + // signature: (oas) + auto path = static_cast( + Glib::VariantBase::cast_dynamic>( + parameters.get_child(0)) + .get()); + + auto ifaces = Glib::VariantBase::cast_dynamic< + Glib::Variant>>(parameters.get_child(1)).get(); + + for (const auto &iface : ifaces) { + if (iface == "org.bluez.Device1") { + removeDevice(path); + } else if (iface == "org.bluez.Adapter1" && path == m_adapter_path) { + m_adapter_proxy.reset(); + m_adapter_path.clear(); + m_powered = false; + m_discovering = false; + m_powered_signal.emit(false); + m_discovering_signal.emit(false); + spdlog::info("Bluetooth adapter removed"); + } + } + } +} + +// --------------------------------------------------------------------------- +// Adapter +// --------------------------------------------------------------------------- + +void BluetoothController::setupAdapter(const std::string &path, + const PropertiesMap &properties) { + m_adapter_path = path; + spdlog::info("Bluetooth adapter found: {}", path); + + auto it = properties.find("Powered"); + if (it != properties.end() && it->second.is_of_type(Glib::VariantType("b"))) { + m_powered = Glib::VariantBase::cast_dynamic>(it->second).get(); + m_powered_signal.emit(m_powered); + } + + it = properties.find("Discovering"); + if (it != properties.end() && it->second.is_of_type(Glib::VariantType("b"))) { + m_discovering = Glib::VariantBase::cast_dynamic>(it->second).get(); + m_discovering_signal.emit(m_discovering); + } + + try { + m_adapter_proxy = Gio::DBus::Proxy::create_sync( + connection, "org.bluez", path, "org.bluez.Adapter1"); + + if (m_adapter_proxy) { + m_adapter_proxy->signal_properties_changed().connect( + sigc::mem_fun(*this, &BluetoothController::onAdapterPropertiesChanged)); + } + } catch (const Glib::Error &ex) { + spdlog::error("Bluetooth: Error creating adapter proxy: {}", ex.what()); + } +} + +void BluetoothController::onAdapterPropertiesChanged( + const Gio::DBus::Proxy::MapChangedProperties &changed, + const std::vector &) { + + auto it = changed.find("Powered"); + if (it != changed.end() && it->second.is_of_type(Glib::VariantType("b"))) { + m_powered = Glib::VariantBase::cast_dynamic>(it->second).get(); + spdlog::info("Bluetooth powered: {}", m_powered); + m_powered_signal.emit(m_powered); + } + + it = changed.find("Discovering"); + if (it != changed.end() && it->second.is_of_type(Glib::VariantType("b"))) { + m_discovering = Glib::VariantBase::cast_dynamic>(it->second).get(); + spdlog::info("Bluetooth discovering: {}", m_discovering); + m_discovering_signal.emit(m_discovering); + } +} + +// --------------------------------------------------------------------------- +// Device management +// --------------------------------------------------------------------------- + +void BluetoothController::addDevice(const std::string &path, + const PropertiesMap &properties) { + auto device = parseDeviceProperties(path, properties); + m_devices[path] = device; + + spdlog::info("Bluetooth device added: {} ({}) {}", device.name, device.address, device.icon); + + try { + auto proxy = Gio::DBus::Proxy::create_sync( + connection, "org.bluez", path, "org.bluez.Device1"); + + if (proxy) { + proxy->signal_properties_changed().connect( + [this, path](const Gio::DBus::Proxy::MapChangedProperties &changed, + const std::vector &invalidated) { + onDevicePropertiesChanged(path, changed, invalidated); + }); + m_device_proxies[path] = proxy; + } + } catch (const Glib::Error &ex) { + spdlog::error("Bluetooth: Error creating device proxy for {}: {}", path, ex.what()); + } + + m_device_added_signal.emit(device); +} + +void BluetoothController::removeDevice(const std::string &path) { + spdlog::info("Bluetooth device removed: {}", path); + m_devices.erase(path); + m_device_proxies.erase(path); + m_device_removed_signal.emit(path); +} + +void BluetoothController::onDevicePropertiesChanged( + const std::string &object_path, + const Gio::DBus::Proxy::MapChangedProperties &changed, + const std::vector &) { + + auto it = m_devices.find(object_path); + if (it == m_devices.end()) return; + + auto &device = it->second; + + for (const auto &[key, value] : changed) { + if (key == "Name" && value.is_of_type(Glib::VariantType("s"))) { + device.name = static_cast( + Glib::VariantBase::cast_dynamic>(value).get()); + } else if (key == "Alias" && value.is_of_type(Glib::VariantType("s"))) { + // Only use Alias as fallback if Name is still the MAC address + auto alias = static_cast( + Glib::VariantBase::cast_dynamic>(value).get()); + if (device.name == device.address) { + device.name = alias; + } + } else if (key == "Paired" && value.is_of_type(Glib::VariantType("b"))) { + device.paired = Glib::VariantBase::cast_dynamic>(value).get(); + } else if (key == "Connected" && value.is_of_type(Glib::VariantType("b"))) { + device.connected = Glib::VariantBase::cast_dynamic>(value).get(); + } else if (key == "Trusted" && value.is_of_type(Glib::VariantType("b"))) { + device.trusted = Glib::VariantBase::cast_dynamic>(value).get(); + } else if (key == "RSSI" && value.is_of_type(Glib::VariantType("n"))) { + device.rssi = Glib::VariantBase::cast_dynamic>(value).get(); + } else if (key == "Icon" && value.is_of_type(Glib::VariantType("s"))) { + device.icon = static_cast( + Glib::VariantBase::cast_dynamic>(value).get()); + } + } + + m_device_changed_signal.emit(device); +} + +// --------------------------------------------------------------------------- +// Property parsing helper +// --------------------------------------------------------------------------- + +BluetoothDevice BluetoothController::parseDeviceProperties( + const std::string &path, + const PropertiesMap &properties) { + + BluetoothDevice device; + device.object_path = path; + + auto getString = [&](const Glib::ustring &key) -> std::string { + auto it = properties.find(key); + if (it != properties.end() && it->second.is_of_type(Glib::VariantType("s"))) { + return static_cast( + Glib::VariantBase::cast_dynamic>(it->second).get()); + } + return ""; + }; + + auto getBool = [&](const Glib::ustring &key) -> bool { + auto it = properties.find(key); + if (it != properties.end() && it->second.is_of_type(Glib::VariantType("b"))) { + return Glib::VariantBase::cast_dynamic>(it->second).get(); + } + return false; + }; + + device.address = getString("Address"); + device.name = getString("Name"); + if (device.name.empty()) device.name = getString("Alias"); + if (device.name.empty()) device.name = device.address; + device.icon = getString("Icon"); + device.paired = getBool("Paired"); + device.connected = getBool("Connected"); + device.trusted = getBool("Trusted"); + + auto rssi_it = properties.find("RSSI"); + if (rssi_it != properties.end() && rssi_it->second.is_of_type(Glib::VariantType("n"))) { + device.rssi = Glib::VariantBase::cast_dynamic>(rssi_it->second).get(); + } + + return device; +} + +// --------------------------------------------------------------------------- +// D-Bus property setter (calls org.freedesktop.DBus.Properties.Set) +// --------------------------------------------------------------------------- + +void BluetoothController::setDbusProperty(const std::string &object_path, + const std::string &interface, + const std::string &property, + const Glib::VariantBase &value) { + if (!connection || object_path.empty()) return; + + try { + auto props_proxy = Gio::DBus::Proxy::create_sync( + connection, "org.bluez", object_path, "org.freedesktop.DBus.Properties"); + + auto wrapped = Glib::Variant::create(value); + auto params = Glib::VariantContainerBase::create_tuple({ + Glib::Variant::create(interface), + Glib::Variant::create(property), + wrapped}); + + props_proxy->call_sync("Set", params); + } catch (const Glib::Error &ex) { + spdlog::error("Error setting {}.{}: {}", interface, property, ex.what()); + } +} diff --git a/src/connection/dbus/mpris.cpp b/src/connection/dbus/mpris.cpp index 367b003..8cb2d84 100644 --- a/src/connection/dbus/mpris.cpp +++ b/src/connection/dbus/mpris.cpp @@ -10,7 +10,10 @@ #include "giomm/dbusproxy.h" std::shared_ptr MprisController::getInstance() { - static std::shared_ptr instance = std::shared_ptr(new MprisController()); + static std::shared_ptr instance; + if (!instance) { + instance = std::shared_ptr(new MprisController()); + } return instance; } @@ -18,7 +21,7 @@ std::shared_ptr MprisController::createForPlayer(const std::str return std::shared_ptr(new MprisController(bus_name)); } -MprisController::MprisController() { +MprisController::MprisController() : m_player_bus_name() { connect_session_async(sigc::mem_fun(*this, &MprisController::on_bus_connected)); } @@ -63,6 +66,33 @@ void MprisController::on_bus_connected(const Glib::RefPtr &res try { connection = Gio::DBus::Connection::get_finish(result); + // Per-player instances only need to create their own player proxy, + // not monitor all players on the bus. + if (!m_player_bus_name.empty()) { + if (!m_proxy) { + try { + m_proxy = Gio::DBus::Proxy::create_sync( + connection, + m_player_bus_name, + "/org/mpris/MediaPlayer2", + "org.mpris.MediaPlayer2.Player"); + + if (m_proxy) { + m_proxy->signal_properties_changed().connect( + sigc::mem_fun(*this, &MprisController::on_properties_changed)); + signalNotification(); + emit_cached_playback_status(); + emit_cached_position(); + emit_cached_can_seek(); + } + } catch (const Glib::Error &ex) { + spdlog::error("DBus Connection Error for player {}: {}", m_player_bus_name, ex.what()); + } + } + return; + } + + // Singleton instance: monitor all MPRIS players on the bus. if (!m_dbus_proxy) { m_dbus_proxy = Gio::DBus::Proxy::create_sync( connection, diff --git a/src/services/hyprland.cpp b/src/services/hyprland.cpp index 17adbf2..0565cdc 100644 --- a/src/services/hyprland.cpp +++ b/src/services/hyprland.cpp @@ -1,5 +1,6 @@ #include "services/hyprland.hpp" +#include #include #include #include @@ -17,6 +18,9 @@ #include "spdlog/spdlog.h" HyprlandService::HyprlandService() { + if (instance) { + throw std::runtime_error("HyprlandService instance already exists"); + } init(); this->bindHyprlandSocket(); } @@ -84,6 +88,9 @@ void HyprlandService::init() { } void HyprlandService::bindHyprlandSocket() { + if (socketFd != -1) { + return; + } std::string socketPath = HyprSocketHelper::getHyprlandSocketPath(); socketFd = socket(AF_UNIX, SOCK_STREAM, 0); @@ -106,22 +113,25 @@ void HyprlandService::bindHyprlandSocket() { } auto socket_conditions = static_cast(G_IO_IN | G_IO_HUP | G_IO_ERR); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) - GSource *source = g_unix_fd_source_new(socketFd, socket_conditions); + g_unix_fd_add( + socketFd, + socket_conditions, + [](gint fd, GIOCondition condition, gpointer user_data) -> gboolean { + auto *self = static_cast(user_data); + if (condition & (G_IO_HUP | G_IO_ERR)) { + spdlog::error("[Hyprland] Socket read error or connection closed"); + close(fd); + self->socketFd = -1; + return FALSE; + } - auto onSocketEvent = [](gint fd, GIOCondition, gpointer user_data) -> gboolean { - HyprlandService *self = static_cast(user_data); - auto messages = SocketHelper::parseSocketMessage(fd, ">>"); - - for (const auto &message : messages) { - self->handleSocketMessage(message); - } - - return G_SOURCE_CONTINUE; - }; - - g_source_set_callback(source, reinterpret_cast(reinterpret_cast(+onSocketEvent)), this, nullptr); - g_source_attach(source, g_main_context_default()); - g_source_unref(source); + auto messages = SocketHelper::parseSocketMessage(fd, ">>"); + for (const auto &message : messages) { + self->handleSocketMessage(message); + } + return TRUE; + }, + this); } void HyprlandService::onWorkspaceChanged(int workspaceId) { @@ -160,20 +170,6 @@ void HyprlandService::onFocusedMonitorChanged(std::string monitorData) { } } -void HyprlandService::onMonitorRemoved(std::string monitorName) { - auto monitorPtr = this->monitors[monitorName]; - - for (const auto &[wsId, wsPtr] : monitorPtr->monitorWorkspaces) { - this->workspaces.erase(wsId); - } - - monitorPtr->monitorWorkspaces.clear(); - monitorPtr->bar->close(); - monitorPtr->bar = nullptr; - - this->monitors.erase(monitorName); -} - void HyprlandService::onMoveWindow(std::string windowData) { auto parts = StringHelper::split(windowData, ','); std::string addr = "0x" + parts[0]; @@ -270,6 +266,10 @@ void HyprlandService::handleSocketMessage(SocketHelper::SocketMessage message) { this->onActiveWindowChanged(eventData); break; } + case MONITOR_ADDED: { + this->onMonitorAdded(eventData); + break; + } case MONITOR_REMOVED: { this->onMonitorRemoved(eventData); break; @@ -280,6 +280,108 @@ void HyprlandService::handleSocketMessage(SocketHelper::SocketMessage message) { } } } + +void HyprlandService::onMonitorAdded(std::string monitorName) { + spdlog::info("[Hyprland] Monitor added: {}", monitorName); + auto monitorDataJson = HyprctlHelper::getMonitorData(); + auto monitorIt = std::ranges::find_if(monitorDataJson, + [&monitorName](const auto &item) { + return item.contains("name") && item["name"] == monitorName; + }); + + if (monitorIt == monitorDataJson.end()) { + spdlog::warn("[Hyprland] Monitor data not found for {}", monitorName); + return; + } + + auto monitorPtr = std::make_shared(); + monitorPtr->id = (*monitorIt)["id"]; + monitorPtr->name = monitorName; + monitorPtr->activeWorkspaceId = (*monitorIt)["activeWorkspace"]["id"]; + monitorPtr->focused = (*monitorIt)["focused"]; + this->monitors[monitorName] = monitorPtr; + + auto onClick = sigc::mem_fun(*this, &HyprlandService::switchToWorkspace); + for (int i = 1; i <= NUM_WORKSPACES; i++) { + auto state = std::make_shared(); + int workspaceId = i + (NUM_WORKSPACES * monitorPtr->id); + + state->id = workspaceId; + state->monitorName = monitorName; + auto view = std::make_shared(workspaceId, std::to_string(i), onClick); + auto workSpace = std::make_shared(); + workSpace->state = state; + workSpace->view = view; + + monitorPtr->monitorWorkspaces[workspaceId] = workSpace; + this->workspaces[workspaceId] = workSpace; + } + + auto workspaceDataJson = HyprctlHelper::getWorkspaceData(); + for (const auto &workspace : workspaceDataJson) { + if (!workspace.contains("monitor") || workspace["monitor"].get() != monitorName) { + continue; + } + auto workspacePtr = workspaces[workspace["id"].get()]; + auto state = workspacePtr->state; + + state->id = workspace["id"].get(); + state->monitorName = workspace["monitor"].get(); + + refreshIndicator(workspacePtr); + } + + auto clientsDataJson = HyprctlHelper::getClientData(); + for (const auto &client : clientsDataJson) { + auto address = client["address"].get(); + if (this->clients.find(address) != this->clients.end()) { + continue; + } + int workspaceId = client["workspace"]["id"].get(); + auto workspaceIt = workspaces.find(workspaceId); + if (workspaceIt == workspaces.end()) { + continue; + } + if (workspaceIt->second->state->monitorName != monitorName) { + continue; + } + + auto clientPtr = std::make_shared(); + clientPtr->address = address; + clientPtr->workspaceId = workspaceId; + clientPtr->title = client["title"].get(); + this->clients[clientPtr->address] = clientPtr; + + workspaceIt->second->state->clients[clientPtr->address] = clientPtr; + if (client.contains("urgent") && client["urgent"].get()) { + workspaceIt->second->state->urgentClients.insert(clientPtr->address); + } + refreshIndicator(workspaceIt->second); + } + + m_signal_monitor_added.emit(monitorName); +} + +void HyprlandService::onMonitorRemoved(std::string monitorName) { + spdlog::info("[Hyprland] Monitor removed: {}", monitorName); + auto monitorIt = monitors.find(monitorName); + if (monitorIt == monitors.end()) { + return; + } + + auto monitorPtr = monitorIt->second; + for (const auto &[workspaceId, workspacePtr] : monitorPtr->monitorWorkspaces) { + for (const auto &[clientAddr, _] : workspacePtr->state->clients) { + clients.erase(clientAddr); + } + workspaces.erase(workspaceId); + } + + this->monitors.erase(monitorIt); + + m_signal_monitor_removed.emit(monitorName); +} + void HyprlandService::onUrgent(std::string windowAddress) { std::string addr = "0x" + windowAddress; if (this->clients.find(addr) == this->clients.end()) { @@ -312,7 +414,11 @@ void HyprlandService::onActiveWindowChanged(std::string windowAddress) { std::shared_ptr HyprlandService::getWorkspaceIndicatorsForMonitor(std::string monitorName) { auto box = std::make_shared(Gtk::Orientation::HORIZONTAL); - auto monitor = monitors[monitorName]; + auto monitorIt = monitors.find(monitorName); + if (monitorIt == monitors.end()) { + return box; + } + auto monitor = monitorIt->second; for (const auto &[wsId, wsPair] : monitor->monitorWorkspaces) { box->append((Gtk::Box &)*wsPair->view); @@ -329,7 +435,11 @@ void HyprlandService::refreshIndicator(std::shared_ptr workspace) { auto view = workspace->view; auto state = workspace->state; - auto monitorsPtr = monitors[state->monitorName]; + auto monitorIt = monitors.find(state->monitorName); + if (monitorIt == monitors.end()) { + return; + } + auto monitorsPtr = monitorIt->second; bool isUrgent = !state->urgentClients.empty(); bool isEmpty = state->clients.empty(); @@ -350,5 +460,9 @@ void HyprlandService::refreshIndicator(std::shared_ptr workspace) { } void HyprlandService::addBar(std::shared_ptr bar, std::string monitorName) { - this->monitors[monitorName]->bar = bar; + auto monitorIt = monitors.find(monitorName); + if (monitorIt == monitors.end()) { + return; + } + monitorIt->second->bar = bar; } \ No newline at end of file diff --git a/src/services/notificationController.cpp b/src/services/notificationController.cpp index d8aca61..70f592d 100644 --- a/src/services/notificationController.cpp +++ b/src/services/notificationController.cpp @@ -12,8 +12,6 @@ #include "gdkmm/display.h" #include "sigc++/adaptors/bind.h" -std::shared_ptr NotificationController::instance = nullptr; - NotificationController::NotificationController() { if (NotificationController::instance) { throw std::runtime_error("use getInstance()!"); @@ -32,8 +30,8 @@ NotificationController::NotificationController() { for (guint i = 0; i < monitors->get_n_items(); ++i) { auto monitor = std::dynamic_pointer_cast( monitors->get_object(i)); - - this->activeMonitors.push_back(monitor); + auto name = monitor->get_connector(); + this->activeMonitors[name] = monitor; } } @@ -41,7 +39,7 @@ void NotificationController::showNotificationOnAllMonitors(NotifyMessage notify) uint64_t id = this->globalNotificationId++; std::vector> notifications; - for (const auto &monitor : this->activeMonitors) { + for (const auto &[name, monitor] : this->activeMonitors) { auto notification = std::make_shared(id, monitor, notify); notifications.push_back(notification); @@ -74,7 +72,7 @@ void NotificationController::showCopyNotification(NotifyMessage notify) { std::vector> notifications; uint64_t id = this->globalNotificationId++; - for (const auto &monitor : this->activeMonitors) { + for (const auto &[name, monitor] : this->activeMonitors) { auto notification = std::make_shared(id, monitor, notify); notification->show(); @@ -97,7 +95,7 @@ void NotificationController::showSpotifyNotification(MprisPlayer2Message mpris) std::vector> notifications; uint64_t id = this->globalNotificationId++; - for (const auto &monitor : this->activeMonitors) { + for (const auto &[name, monitor] : this->activeMonitors) { auto notification = std::make_shared(id, monitor, mpris); notification->show(); @@ -153,4 +151,14 @@ void NotificationController::closeNotification(uint64_t notificationId) { this->activeNotifications.erase(notificationId); this->hoverCounts.erase(notificationId); +} + +void NotificationController::addMonitor(std::shared_ptr monitor) { + auto name = monitor->get_connector(); + this->activeMonitors[name] = monitor; +} + +void NotificationController::removeMonitor(std::shared_ptr monitor) { + auto name = monitor->get_connector(); + this->activeMonitors.erase(name); } \ No newline at end of file diff --git a/src/widgets/controlCenter/bluetoothSettings.cpp b/src/widgets/controlCenter/bluetoothSettings.cpp new file mode 100644 index 0000000..e1b7ab9 --- /dev/null +++ b/src/widgets/controlCenter/bluetoothSettings.cpp @@ -0,0 +1,81 @@ +#include "widgets/controlCenter/bluetoothSettings.hpp" + +#include + +void BluetoothSettings::setBluetoothPowered(bool powered) { + this->bluetoothIsPowered = powered; + + if (powered) { + powerButton->remove_css_class("power-button-off"); + powerButton->add_css_class("power-button-on"); + powerButton->set_tooltip_text("Turn Bluetooth Off"); + } else { + powerButton->remove_css_class("power-button-on"); + powerButton->add_css_class("power-button-off"); + powerButton->set_tooltip_text("Turn Bluetooth On"); + } +} + +void BluetoothSettings::setScanning(bool scanning) { + this->bluetoothIsScanning = scanning; + + if (scanning) { + scanButton->add_css_class("power-button-on"); + scanButton->remove_css_class("power-button-off"); + scanButton->set_tooltip_text("Stop Scanning"); + } else { + scanButton->remove_css_class("power-button-on"); + scanButton->add_css_class("power-button-off"); + scanButton->set_tooltip_text("Start Scanning"); + } +} + +BluetoothSettings::BluetoothSettings() : bluetoothIsPowered(this->bluetoothController->isPowered()) { + set_orientation(Gtk::Orientation::VERTICAL); + set_spacing(12); + + auto devices = this->bluetoothController->getDevices(); + + powerButton->add_css_class("power-button"); + + setBluetoothPowered(bluetoothIsPowered); + + powerButton->set_tooltip_text(bluetoothIsPowered ? "Turn Bluetooth Off" : "Turn Bluetooth On"); + powerButton->signal_clicked().connect([this]() { + bluetoothIsPowered = !bluetoothIsPowered; + this->bluetoothController->setPowered(bluetoothIsPowered); + }); + + append(*powerButton); + + this->bluetoothController->signalPoweredChanged().connect([this](bool powered) { + bluetoothIsPowered = powered; + setBluetoothPowered(powered); + }); + + for (const auto &device : devices) { + auto row = std::make_shared(device); + deviceRows[device.object_path] = row; + append(*row); + } + bluetoothController->signalDeviceAdded().connect([this](const BluetoothDevice &device) { + auto row = std::make_shared(device); + deviceRows[device.object_path] = row; + append(*row); + }); + + bluetoothController->signalDeviceRemoved().connect([this](const std::string &object_path) { + auto it = deviceRows.find(object_path); + if (it != deviceRows.end()) { + remove(*it->second); + deviceRows.erase(it); + } + }); + + bluetoothController->signalDeviceChanged().connect([this](const BluetoothDevice &device) { + auto it = deviceRows.find(device.object_path); + if (it != deviceRows.end()) { + it->second->updateDevice(device); + } + }); +} \ No newline at end of file diff --git a/src/widgets/controlCenter/controlCenter.cpp b/src/widgets/controlCenter/controlCenter.cpp index 8edc6fa..7c0ff55 100644 --- a/src/widgets/controlCenter/controlCenter.cpp +++ b/src/widgets/controlCenter/controlCenter.cpp @@ -27,10 +27,12 @@ ControlCenter::ControlCenter(Icon::Type icon, std::string name) this->mediaTabButton = std::make_unique(Icon::PLAY_CIRCLE); this->infoTabButton = std::make_unique(Icon::EMPTY_DASHBOARD); this->timerButton = std::make_unique(Icon::TOKEN); + this->settingsTabButton = std::make_unique(Icon::SETTINGS); this->tabRow.append(*this->mediaTabButton); this->tabRow.append(*this->infoTabButton); this->tabRow.append(*this->timerButton); + this->tabRow.append(*this->settingsTabButton); this->container.append(this->tabRow); @@ -41,10 +43,12 @@ ControlCenter::ControlCenter(Icon::Type icon, std::string name) this->mediaControlWidget = std::make_unique(); this->weatherWidget = std::make_unique(); this->timerWidget = std::make_unique(); + this->settingsWidget = std::make_unique(); this->contentStack.add(*this->mediaControlWidget, "controls", "Controls"); this->contentStack.add(*this->weatherWidget, "info", "Info"); this->contentStack.add(*this->timerWidget, "timer", "Timer"); + this->contentStack.add(*this->settingsWidget, "settings", "Settings"); this->contentStack.set_visible_child("controls"); this->setActiveTab("controls"); @@ -63,6 +67,9 @@ ControlCenter::ControlCenter(Icon::Type icon, std::string name) this->setActiveTab("timer"); }); + this->settingsTabButton->signal_clicked().connect([this]() { + this->setActiveTab("settings"); + }); } void ControlCenter::setActiveTab(const std::string &tab_name) { diff --git a/src/widgets/controlCenter/settings.cpp b/src/widgets/controlCenter/settings.cpp new file mode 100644 index 0000000..673cf6e --- /dev/null +++ b/src/widgets/controlCenter/settings.cpp @@ -0,0 +1,8 @@ +#include "widgets/controlCenter/settings.hpp" + +SettingsWidget::SettingsWidget() { + set_orientation(Gtk::Orientation::VERTICAL); + set_spacing(12); + + this->append(this->bluetoothSettings); +} \ No newline at end of file diff --git a/src/widgets/controlCenter/timer.cpp b/src/widgets/controlCenter/timer.cpp index 17a8c3d..2123447 100644 --- a/src/widgets/controlCenter/timer.cpp +++ b/src/widgets/controlCenter/timer.cpp @@ -15,19 +15,19 @@ TimerWidget::TimerWidget() { set_orientation(Gtk::Orientation::VERTICAL); set_spacing(6); - this->timerService->getSignalTimerSet().connect([this](const std::string &duration, uint64_t timerId) { + this->timerSetConnection = this->timerService->getSignalTimerSet().connect([this](const std::string &duration, uint64_t timerId) { this->addTimer(duration, timerId); }); - this->timerService->getSignalTimerCancelled().connect([this](uint64_t timerId) { + this->timerCancelledConnection = this->timerService->getSignalTimerCancelled().connect([this](uint64_t timerId) { this->removeTimer(timerId); }); - this->timerService->getSignalTimerExpired().connect([this](uint64_t timerId) { + this->timerExpiredConnection = this->timerService->getSignalTimerExpired().connect([this](uint64_t timerId) { this->activateTimer(timerId); }); - this->timerService->tickSignal.connect([this]() { + this->tickConnection = this->timerService->tickSignal.connect([this]() { for (auto &[id, timer] : activeTimers) { timer->tickDown(); } @@ -101,6 +101,21 @@ TimerWidget::TimerWidget() { append(*entry); } +TimerWidget::~TimerWidget() { + if (this->timerSetConnection.connected()) { + this->timerSetConnection.disconnect(); + } + if (this->timerCancelledConnection.connected()) { + this->timerCancelledConnection.disconnect(); + } + if (this->timerExpiredConnection.connected()) { + this->timerExpiredConnection.disconnect(); + } + if (this->tickConnection.connected()) { + this->tickConnection.disconnect(); + } +}; + void TimerWidget::addTimer(const std::string &duration, uint64_t timerId) { std::unique_ptr timer = std::make_unique(duration, timerId); diff --git a/src/widgets/tray.cpp b/src/widgets/tray.cpp index 4565d12..103eb3b 100644 --- a/src/widgets/tray.cpp +++ b/src/widgets/tray.cpp @@ -266,6 +266,7 @@ void TrayIconWidget::update(const TrayService::Item &item) { picture.set_visible(true); image.set_visible(false); } else if (!item.iconName.empty()) { + spdlog::info("App {} requesting icon name: {}", id, item.iconName); // Add this image.set_from_icon_name(item.iconName); image.set_pixel_size(20); image.set_visible(true); diff --git a/src/widgets/wallpaperWindow.cpp b/src/widgets/wallpaperWindow.cpp deleted file mode 100644 index f3b0bba..0000000 --- a/src/widgets/wallpaperWindow.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "widgets/wallpaperWindow.hpp" - -#include -#include - -#include -#include - -#include "giomm/file.h" - -WallpaperWindow::WallpaperWindow(GdkMonitor *monitor, const std::string &imagePath) { - set_name("wallpaper-window"); - set_resizable(false); - set_focusable(false); - - gtk_layer_init_for_window(gobj()); - - if (monitor) { - gtk_layer_set_monitor(gobj(), monitor); - } - - gtk_layer_set_layer(gobj(), GTK_LAYER_SHELL_LAYER_BACKGROUND); - gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_TOP, true); - gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, true); - gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_LEFT, true); - gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, true); - gtk_layer_set_exclusive_zone(gobj(), 0); - gtk_layer_set_keyboard_mode(gobj(), GTK_LAYER_SHELL_KEYBOARD_MODE_NONE); - - picture.set_hexpand(true); - picture.set_vexpand(true); - picture.set_halign(Gtk::Align::FILL); - picture.set_valign(Gtk::Align::FILL); - picture.set_can_shrink(true); - picture.set_content_fit(Gtk::ContentFit::COVER); - - if (monitor) { - GdkRectangle geom{}; - gdk_monitor_get_geometry(monitor, &geom); - set_default_size(geom.width, geom.height); - set_size_request(geom.width, geom.height); - picture.set_size_request(geom.width, geom.height); - } - - auto resolved_path = expand_user_path(imagePath); - if (auto texture = load_texture(resolved_path)) { - picture.set_paintable(texture); - } else { - spdlog::warn("Wallpaper image failed to load: {}", resolved_path); - } - - set_child(picture); -} - -std::string WallpaperWindow::expand_user_path(const std::string &path) { - if (path.rfind("~/", 0) != 0) { - return path; - } - - const char *home = std::getenv("HOME"); - if (!home || !*home) { - return path; - } - - return std::filesystem::path(home) / path.substr(2); -} - -Glib::RefPtr WallpaperWindow::load_texture(const std::string &path) { - if (path.empty()) { - spdlog::warn("Wallpaper image path is empty"); - return {}; - } - - if (!std::filesystem::exists(path)) { - spdlog::warn("Wallpaper image not found at path: {}", path); - return {}; - } - - try { - auto file = Gio::File::create_for_path(path); - return Gdk::Texture::create_from_file(file); - } catch (const std::exception &ex) { - spdlog::warn("Failed to load wallpaper image {}: {}", path, ex.what()); - return {}; - } catch (...) { - spdlog::warn("Failed to load wallpaper image {}: unknown error", path); - return {}; - } -}