From d1b81c4d3e67bd681b8398af9e09b27fbf7e5421 Mon Sep 17 00:00:00 2001 From: Arif Hasanic Date: Mon, 9 Feb 2026 21:01:56 +0100 Subject: [PATCH] fix styling issues --- CMakeLists.txt | 3 +- include/components/types/icon.hpp | 20 + include/connection/dbus/bluetooth.hpp | 20 +- include/connection/dbus/messages.hpp | 4 + include/services/textureCache.hpp | 4 +- .../controlCenter/bluetoothSettings.hpp | 85 +---- .../controlCenter/bluetoothSettingsRow.hpp | 24 ++ resources/bar.css | 78 +++- src/app.cpp | 1 + src/components/button/iconButton.cpp | 2 +- src/components/workspaceIndicator.cpp | 1 - src/connection/dbus/bluetooth.cpp | 349 +++++++++++++++++- src/services/hyprland.cpp | 99 +++-- src/services/textureCache.cpp | 13 + .../controlCenter/bluetoothSettings.cpp | 164 +++++--- .../controlCenter/bluetoothSettingsRow.cpp | 90 +++++ src/widgets/controlCenter/controlCenter.cpp | 6 +- src/widgets/notification/copyNotification.cpp | 1 - .../notification/notificationWindow.cpp | 17 + src/widgets/weather.cpp | 4 - 20 files changed, 778 insertions(+), 207 deletions(-) create mode 100644 include/widgets/controlCenter/bluetoothSettingsRow.hpp create mode 100644 src/widgets/controlCenter/bluetoothSettingsRow.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a773079..2de0215 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,7 +65,8 @@ target_sources(bar_lib src/widgets/tray.cpp src/widgets/controlCenter/bluetoothSettings.cpp src/widgets/controlCenter/settings.cpp - + src/widgets/controlCenter/bluetoothSettingsRow.cpp + src/services/hyprland.cpp src/services/notificationController.cpp src/services/textureCache.cpp diff --git a/include/components/types/icon.hpp b/include/components/types/icon.hpp index ed6cde7..bd3f839 100644 --- a/include/components/types/icon.hpp +++ b/include/components/types/icon.hpp @@ -25,7 +25,17 @@ class Icon { SETTINGS, POWER_SETTINGS_NEW, + BLUETOOTH, + BLUETOOTH_CONNECTED, BLUETOOTH_SEARCHING, + LINK, + LINK_OFF, + + VERIFIED, + VERIFIED_OFF, + + DONE_ALL, + REMOVE_DONE, }; static const std::string toString(Type type) { @@ -53,6 +63,16 @@ class Icon { {SETTINGS, "\ue8b8"}, {POWER_SETTINGS_NEW, "\ue8ac"}, + {BLUETOOTH, "\ue1a7"}, + {BLUETOOTH_CONNECTED, "\ue1a8"}, {BLUETOOTH_SEARCHING, "\ue1aa"}, + {LINK, "\ue157"}, + {LINK_OFF, "\ue16f"}, + + {VERIFIED, "\uef76"}, + {VERIFIED_OFF, "\uf30e"}, + + {DONE_ALL, "\ue877"}, + {REMOVE_DONE, "\ue9d3"}, }; }; \ No newline at end of file diff --git a/include/connection/dbus/bluetooth.hpp b/include/connection/dbus/bluetooth.hpp index 9849c7d..0a74bca 100644 --- a/include/connection/dbus/bluetooth.hpp +++ b/include/connection/dbus/bluetooth.hpp @@ -90,6 +90,17 @@ class BluetoothController : public DbusConnection { const Glib::ustring &signal_name, const Glib::VariantContainerBase ¶meters); + // Agent + guint m_agent_id = 0; + void registerAgent(); + void on_agent_method_call(const Glib::RefPtr &connection, + const Glib::ustring &sender, + const Glib::ustring &object_path, + const Glib::ustring &interface_name, + const Glib::ustring &method_name, + const Glib::VariantContainerBase ¶meters, + const Glib::RefPtr &invocation); + // Adapter void setupAdapter(const std::string &path, const PropertiesMap &properties); void onAdapterPropertiesChanged(const Gio::DBus::Proxy::MapChangedProperties &changed, @@ -105,7 +116,10 @@ class BluetoothController : public DbusConnection { // 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); + const std::string &interface, + const std::string &property, + const Glib::VariantBase &value); + + // HID Authorization handler + void authorizeHIDDevice(const std::string &object_path); }; diff --git a/include/connection/dbus/messages.hpp b/include/connection/dbus/messages.hpp index f820b2c..4c15c33 100644 --- a/include/connection/dbus/messages.hpp +++ b/include/connection/dbus/messages.hpp @@ -44,4 +44,8 @@ struct NotifyMessage { std::shared_ptr actionInvoked; // image data (if any) from dbus std::optional> imageData; + + bool has_input = false; + std::string input_placeholder; + std::function on_input; }; \ No newline at end of file diff --git a/include/services/textureCache.hpp b/include/services/textureCache.hpp index 705ac9f..31d01ba 100644 --- a/include/services/textureCache.hpp +++ b/include/services/textureCache.hpp @@ -12,9 +12,11 @@ class TextureCacheService { Glib::RefPtr getTexture(const std::string &url); void pruneCache(); + void clear(); private: - TextureCacheService() = default; + TextureCacheService(); + ~TextureCacheService(); std::unordered_map> cache; }; diff --git a/include/widgets/controlCenter/bluetoothSettings.hpp b/include/widgets/controlCenter/bluetoothSettings.hpp index 952a71c..4cd5439 100644 --- a/include/widgets/controlCenter/bluetoothSettings.hpp +++ b/include/widgets/controlCenter/bluetoothSettings.hpp @@ -1,88 +1,14 @@ #pragma once +#include + #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" +#include "gtkmm/scrolledwindow.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; -}; +#include "widgets/controlCenter/bluetoothSettingsRow.hpp" class BluetoothSettings : public Gtk::Box { public: @@ -96,8 +22,11 @@ class BluetoothSettings : public Gtk::Box { std::shared_ptr powerButton = std::make_shared(Icon::POWER_SETTINGS_NEW); std::shared_ptr scanButton = std::make_shared(Icon::BLUETOOTH_SEARCHING); + Gtk::Box buttonRow; Gtk::Box connectedDevicesBox; + Gtk::ScrolledWindow connectedDevicesScroll; Gtk::Box availableDevicesBox; + Gtk::ScrolledWindow availableDevicesScroll; bool bluetoothIsPowered = false; bool bluetoothIsScanning = false; diff --git a/include/widgets/controlCenter/bluetoothSettingsRow.hpp b/include/widgets/controlCenter/bluetoothSettingsRow.hpp new file mode 100644 index 0000000..9b18653 --- /dev/null +++ b/include/widgets/controlCenter/bluetoothSettingsRow.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#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); + void updateDevice(const BluetoothDevice &device); + + private: + BluetoothDevice device; + Gtk::Image icon; + Gtk::Label nameLabel; + Gtk::Label addressLabel; + IconButton pairButton; + IconButton connectButton; + IconButton trustButton; +}; diff --git a/resources/bar.css b/resources/bar.css index 642d25e..cacf562 100644 --- a/resources/bar.css +++ b/resources/bar.css @@ -1,3 +1,27 @@ +/* Custom Scrollbar Styling */ +scrollbar, scrollbar * { + min-width: 8px; + background: transparent; +} + +scrollbar slider { + background: rgba(255, 255, 255, 0.25); + border-radius: 6px; + min-width: 8px; +} + +scrollbar slider:hover, scrollbar slider:active { + background: rgba(255, 255, 255, 0.45); +} + +scrollbar trough { + background: rgba(255, 255, 255, 0.08); + border-radius: 6px; +} + +scrollbar button { + background: transparent; +} /** biome-ignore-all lint/correctness/noUnknownTypeSelector: gtk css has more valid identifiers */ * { all: unset; @@ -16,11 +40,9 @@ window { color: #ffffff; font-size: 14px; font-family: var(--text-font); + padding: 3px; } -.bar { - padding: 4px 6px; -} .text-area { font-family: var(--text-font-mono); @@ -34,20 +56,23 @@ window { } .power-button-on { - color: #4caf50; + /* bright green */ + background-color: rgba(0, 255, 0, 0.2); } .power-button-off { - color: #f44336; + /* bright red */ + background-color: rgba(255, 0, 0, 0.2); + color: #ffffff; } .control-center-tab-row { - background-color: rgba(50, 50, 50, 0.8); + background-color: rgba(255, 255, 255, 0.1); border-radius: 4px; - padding: 2px 4px; - margin-bottom: 4px; } + + .active-button { background-color: #ffffff; color: #1e1e1e; @@ -56,20 +81,20 @@ window { } popover { - margin-top: 4px; - font-family: var(--text-font); - border-radius: 4px; + font-family: var(--text-font); + border-radius: 12px; - background: rgba(25, 25, 25, 0.8); + + background: rgba(25, 25, 25, 0.9); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); border: 1px solid rgba(57, 57, 57, 0.71); } .control-center-popover { + margin-top: 6px; background-color: rgba(35, 35, 35, 0.95); padding: 12px; padding-top : 6px; - border-radius: 4px; box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); border: 1px solid rgba(57, 57, 57, 0.8); } @@ -77,7 +102,6 @@ popover { .control-center-player-container { border-radius: 4px; background: rgba(35, 35, 35, 0.95); - margin-bottom: 6px; } @@ -158,9 +182,9 @@ tooltip { .workspace-pill { padding: 2px 5px; - margin-right: 6px; border-radius: 4px; text-shadow: 0 0 2px #646464; + margin: 0 2px; transition: background-color 0.2s, color 0.2s, @@ -249,3 +273,27 @@ tooltip { opacity: 1; } } + +.bluetooth-settings { + font-family: var(--text-font); + font-size: 14px; +} + +.bluetooth-device-address { + font-size: 10px; + color: #cccccc; +} + +.bluetooth-settings-row { + transition: background-color 0.2s; +} + +.active-devices { + background-color: rgba(0, 255, 0, 0.1); + border-radius: 4px; +} + +.available-devices { + background-color: rgba(255, 255, 255, 0.1); + border-radius: 4px; +} \ No newline at end of file diff --git a/src/app.cpp b/src/app.cpp index 8ac60a7..cfac113 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -113,6 +113,7 @@ App::App() : app(Gtk::Application::create("org.example.mybar")) { }); app->signal_shutdown().connect([&]() { + TextureCacheService::getInstance()->clear(); this->trayService->stop(); }); } diff --git a/src/components/button/iconButton.cpp b/src/components/button/iconButton.cpp index 53b7ca5..0a773d9 100644 --- a/src/components/button/iconButton.cpp +++ b/src/components/button/iconButton.cpp @@ -1,8 +1,8 @@ #include "components/button/iconButton.hpp" IconButton::IconButton(Icon::Type icon, std::string fontFamilyCss) : TextButton(Icon::toString(icon)) { - this->get_style_context()->add_class(fontFamilyCss); this->get_style_context()->add_class("icon-button"); + this->get_style_context()->add_class(fontFamilyCss); } void IconButton::setIcon(Icon::Type icon) { diff --git a/src/components/workspaceIndicator.cpp b/src/components/workspaceIndicator.cpp index 4307f55..e3d652a 100644 --- a/src/components/workspaceIndicator.cpp +++ b/src/components/workspaceIndicator.cpp @@ -6,7 +6,6 @@ WorkspaceIndicator::WorkspaceIndicator(int id, std::string label, sigc::slot onClick) : Gtk::Box(Gtk::Orientation::HORIZONTAL) { - overlay = std::make_shared(); auto numLabel = Gtk::make_managed(label); auto pillContainer = Gtk::make_managed(Gtk::Orientation::HORIZONTAL); diff --git a/src/connection/dbus/bluetooth.cpp b/src/connection/dbus/bluetooth.cpp index 9694736..9b68bcd 100644 --- a/src/connection/dbus/bluetooth.cpp +++ b/src/connection/dbus/bluetooth.cpp @@ -1,29 +1,57 @@ #include "connection/dbus/bluetooth.hpp" +#include "services/notificationController.hpp" #include +#include +#include -// --------------------------------------------------------------------------- -// Constructor -// --------------------------------------------------------------------------- +const std::string agent_introspection_xml = R"( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)"; 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; } @@ -46,10 +74,6 @@ std::vector BluetoothController::getPairedDevices() const { return result; } -// --------------------------------------------------------------------------- -// Adapter control -// --------------------------------------------------------------------------- - void BluetoothController::setPowered(bool powered) { spdlog::info("Bluetooth: setting powered to {}", powered); setDbusProperty(m_adapter_path, "org.bluez.Adapter1", "Powered", @@ -70,10 +94,6 @@ void BluetoothController::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); @@ -139,6 +159,8 @@ void BluetoothController::onBusConnected(const Glib::RefPtr &r enumerateObjects(); } + + registerAgent(); } catch (const Glib::Error &ex) { spdlog::error("Bluetooth DBus Connection Error: {}", ex.what()); } @@ -452,3 +474,292 @@ void BluetoothController::setDbusProperty(const std::string &object_path, spdlog::error("Error setting {}.{}: {}", interface, property, ex.what()); } } + +// --------------------------------------------------------------------------- +// Agent +// --------------------------------------------------------------------------- + +void BluetoothController::registerAgent() { + if (!connection) + return; + + try { + auto node_info = Gio::DBus::NodeInfo::create_for_xml(agent_introspection_xml); + auto interface_info = node_info->lookup_interface("org.bluez.Agent1"); + + Gio::DBus::InterfaceVTable vtable( + sigc::mem_fun(*this, &BluetoothController::on_agent_method_call)); + + m_agent_id = connection->register_object("/org/bluez/agent", interface_info, vtable); + + auto agent_manager = Gio::DBus::Proxy::create_sync( + connection, "org.bluez", "/org/bluez", "org.bluez.AgentManager1"); + + if (agent_manager) { + agent_manager->call( + "RegisterAgent", + Glib::VariantContainerBase::create_tuple( + {Glib::Variant::create("/org/bluez/agent"), + Glib::Variant::create("KeyboardDisplay")})); // Using KeyboardDisplay capacity + + agent_manager->call( + "RequestDefaultAgent", + Glib::VariantContainerBase::create_tuple( + {Glib::Variant::create("/org/bluez/agent")})); + + spdlog::info("Bluetooth Agent registered successfully"); + } + } catch (const Glib::Error &ex) { + spdlog::error("Bluetooth: Failed to register agent: {}", ex.what()); + } +} + +void BluetoothController::on_agent_method_call( + const Glib::RefPtr &, + const Glib::ustring &, + const Glib::ustring &, + const Glib::ustring &, + const Glib::ustring &method_name, + const Glib::VariantContainerBase ¶meters, + const Glib::RefPtr &invocation) { + + if (method_name == "RequestConfirmation") { + // Signature: (ou) + std::string device_path = static_cast( + Glib::VariantBase::cast_dynamic>( + parameters.get_child(0)).get()); + + guint32 passkey = Glib::VariantBase::cast_dynamic>( + parameters.get_child(1)).get(); + + std::string device_name = device_path; + if (m_devices.count(device_path)) { + device_name = m_devices[device_path].name; + if (device_name.empty()) + device_name = m_devices[device_path].address; + } + + std::ostringstream ss; + ss << std::setw(6) << std::setfill('0') << passkey; + std::string passkey_str = ss.str(); + + NotifyMessage msg; + msg.summary = "Bluetooth Pairing Request"; + msg.body = "Device '" + device_name + "' wants to pair.\nPasskey: " + passkey_str; + msg.app_name = "Bar"; + msg.actions = {"Confirm", "Cancel"}; + msg.urgency = NotificationUrgency::CRITICAL; + msg.expire_timeout = -1; // No timeout, requires user interaction + + // Capture invocation to respond later + // Note: invocation is a RefPtr, so copying it increases ref count + msg.on_action = [invocation](const std::string &action) { + if (action == "Confirm") { + invocation->return_value(Glib::VariantContainerBase()); + } else { + invocation->return_dbus_error("org.bluez.Error.Rejected", "Rejected by user"); + } + }; + + NotificationController::getInstance()->showNotificationOnAllMonitors(msg); + + } else if (method_name == "RequestAuthorization") { + // Signature: (o) + std::string device_path = static_cast( + Glib::VariantBase::cast_dynamic>( + parameters.get_child(0)).get()); + + std::string device_name = device_path; + if (m_devices.count(device_path)) { + device_name = m_devices[device_path].name; + if (device_name.empty()) + device_name = m_devices[device_path].address; + } + + NotifyMessage msg; + msg.summary = "Bluetooth Authorization Request"; + msg.body = "Device '" + device_name + "' requests authorization to connect."; + msg.app_name = "Bar"; + msg.actions = {"Allow", "Deny"}; + msg.urgency = NotificationUrgency::CRITICAL; + msg.expire_timeout = -1; + + msg.on_action = [invocation](const std::string &action) { + if (action == "Allow") { + invocation->return_value(Glib::VariantContainerBase()); + } else { + invocation->return_dbus_error("org.bluez.Error.Rejected", "Rejected by user"); + } + }; + + NotificationController::getInstance()->showNotificationOnAllMonitors(msg); + + } else if (method_name == "AuthorizeService") { + // Signature: (os) + std::string device_path = static_cast( + Glib::VariantBase::cast_dynamic>( + parameters.get_child(0)).get()); + std::string uuid = static_cast( + Glib::VariantBase::cast_dynamic>( + parameters.get_child(1)).get()); + + std::string device_name = device_path; + if (m_devices.count(device_path)) { + device_name = m_devices[device_path].name; + if (device_name.empty()) + device_name = m_devices[device_path].address; + } + + NotifyMessage msg; + msg.summary = "Bluetooth Service Authorization"; + msg.body = "Device '" + device_name + "' wants to access service: " + uuid; + msg.app_name = "Bar"; + msg.actions = {"Allow", "Deny"}; + msg.urgency = NotificationUrgency::CRITICAL; + msg.expire_timeout = -1; + + msg.on_action = [invocation](const std::string &action) { + if (action == "Allow") { + invocation->return_value(Glib::VariantContainerBase()); + } else { + invocation->return_dbus_error("org.bluez.Error.Rejected", "Rejected by user"); + } + }; + + NotificationController::getInstance()->showNotificationOnAllMonitors(msg); + + } else if (method_name == "DisplayPinCode") { + // Signature: (os) + std::string device_path = static_cast( + Glib::VariantBase::cast_dynamic>( + parameters.get_child(0)).get()); + std::string pincode = static_cast( + Glib::VariantBase::cast_dynamic>( + parameters.get_child(1)).get()); + + std::string device_name = device_path; + if (m_devices.count(device_path)) { + device_name = m_devices[device_path].name; + if (device_name.empty()) + device_name = m_devices[device_path].address; + } + + NotifyMessage msg; + msg.summary = "Bluetooth Pin Code"; + msg.body = "Pin code for device '" + device_name + "': " + pincode; + msg.app_name = "Bar"; + msg.actions = {"Dismiss"}; + msg.urgency = NotificationUrgency::NORMAL; + msg.expire_timeout = 0; + + NotificationController::getInstance()->showNotificationOnAllMonitors(msg); + invocation->return_value(Glib::VariantContainerBase()); + + } else if (method_name == "DisplayPasskey") { + // Signature: (ouq) + std::string device_path = static_cast( + Glib::VariantBase::cast_dynamic>( + parameters.get_child(0)).get()); + guint32 passkey = Glib::VariantBase::cast_dynamic>( + parameters.get_child(1)).get(); + guint16 entered = Glib::VariantBase::cast_dynamic>( + parameters.get_child(2)).get(); + + // Only show on first display (entered == 0) to avoid spam + if (entered == 0) { + std::string device_name = device_path; + if (m_devices.count(device_path)) { + device_name = m_devices[device_path].name; + if (device_name.empty()) + device_name = m_devices[device_path].address; + } + + std::ostringstream ss; + ss << std::setw(6) << std::setfill('0') << passkey; + std::string passkey_str = ss.str(); + + NotifyMessage msg; + msg.summary = "Bluetooth Passkey"; + msg.body = "Type this passkey on '" + device_name + "': " + passkey_str; + msg.app_name = "Bar"; + msg.actions = {"Dismiss"}; + msg.urgency = NotificationUrgency::CRITICAL; + msg.expire_timeout = -1; + + NotificationController::getInstance()->showNotificationOnAllMonitors(msg); + } + invocation->return_value(Glib::VariantContainerBase()); + + } else if (method_name == "RequestPinCode") { + // Signature: (o) -> s + std::string device_path = static_cast( + Glib::VariantBase::cast_dynamic>( + parameters.get_child(0)).get()); + + std::string device_name = device_path; + if (m_devices.count(device_path)) { + device_name = m_devices[device_path].name; + if (device_name.empty()) + device_name = m_devices[device_path].address; + } + + NotifyMessage msg; + msg.summary = "Bluetooth PIN Request"; + msg.body = "Enter PIN Code for '" + device_name + "'"; + msg.app_name = "Bar"; + msg.urgency = NotificationUrgency::CRITICAL; + msg.expire_timeout = -1; + msg.has_input = true; + msg.input_placeholder = "PIN Code"; + + msg.on_input = [invocation](const std::string &input) { + invocation->return_value( + Glib::VariantContainerBase::create_tuple( + {Glib::Variant::create(input)})); + }; + + NotificationController::getInstance()->showNotificationOnAllMonitors(msg); + + } else if (method_name == "RequestPasskey") { + // Signature: (o) -> u + std::string device_path = static_cast( + Glib::VariantBase::cast_dynamic>( + parameters.get_child(0)).get()); + + std::string device_name = device_path; + if (m_devices.count(device_path)) { + device_name = m_devices[device_path].name; + if (device_name.empty()) + device_name = m_devices[device_path].address; + } + + NotifyMessage msg; + msg.summary = "Bluetooth Passkey Request"; + msg.body = "Enter Passkey for '" + device_name + "'"; + msg.app_name = "Bar"; + msg.urgency = NotificationUrgency::CRITICAL; + msg.expire_timeout = -1; + msg.has_input = true; + msg.input_placeholder = "Passkey (0-999999)"; + + msg.on_input = [invocation](const std::string &input) { + try { + uint32_t passkey = std::stoul(input); + invocation->return_value( + Glib::VariantContainerBase::create_tuple( + {Glib::Variant::create(passkey)})); + } catch (...) { + invocation->return_dbus_error("org.bluez.Error.Failed", "Invalid passkey format"); + } + }; + + NotificationController::getInstance()->showNotificationOnAllMonitors(msg); + + } else if (method_name == "Cancel" || method_name == "Release") { + // Just return + invocation->return_value(Glib::VariantContainerBase()); + } else { + // Not implemented + invocation->return_dbus_error("org.bluez.Error.Rejected", "Not implemented"); + } +} diff --git a/src/services/hyprland.cpp b/src/services/hyprland.cpp index 29f98ff..e2706c5 100644 --- a/src/services/hyprland.cpp +++ b/src/services/hyprland.cpp @@ -66,7 +66,11 @@ void HyprlandService::init() { clientPtr->title = client["title"].get(); this->clients[clientPtr->address] = clientPtr; - auto workspacePtr = workspaces[clientPtr->workspaceId]; + auto workspaceIt = workspaces.find(clientPtr->workspaceId); + if (workspaceIt == workspaces.end()) { + continue; + } + auto workspacePtr = workspaceIt->second; workspacePtr->state->clients[clientPtr->address] = clientPtr; if (client.contains("urgent") && client["urgent"].get()) { @@ -77,7 +81,11 @@ void HyprlandService::init() { auto workspaceDataJson = HyprctlHelper::getWorkspaceData(); for (const auto &workspace : workspaceDataJson) { - auto workspacePtr = workspaces[workspace["id"].get()]; + auto workspaceIt = workspaces.find(workspace["id"].get()); + if (workspaceIt == workspaces.end()) { + continue; + } + auto workspacePtr = workspaceIt->second; auto state = workspacePtr->state; state->id = workspace["id"].get(); @@ -135,17 +143,27 @@ void HyprlandService::bindHyprlandSocket() { } void HyprlandService::onWorkspaceChanged(int workspaceId) { - auto newActive = workspaces[workspaceId]; + auto newActiveIt = workspaces.find(workspaceId); + if (newActiveIt == workspaces.end()) { + return; + } + auto newActive = newActiveIt->second; auto state = newActive->state; - auto monitorPtr = monitors[state->monitorName]; + auto monitorIt = monitors.find(state->monitorName); + if (monitorIt == monitors.end()) { + return; + } + auto monitorPtr = monitorIt->second; int oldActiveWorkspaceId = monitorPtr->activeWorkspaceId; - auto oldActive = workspaces[oldActiveWorkspaceId]; - monitorPtr->activeWorkspaceId = workspaceId; refreshIndicator(newActive); - refreshIndicator(oldActive); + + auto oldActiveIt = workspaces.find(oldActiveWorkspaceId); + if (oldActiveIt != workspaces.end()) { + refreshIndicator(oldActiveIt->second); + } } void HyprlandService::onFocusedMonitorChanged(std::string monitorData) { @@ -183,19 +201,24 @@ void HyprlandService::onMoveWindow(std::string windowData) { auto clientPtr = this->clients[addr]; int oldWorkspaceId = clientPtr->workspaceId; - auto oldWorkspacePtr = workspaces[oldWorkspaceId]; - oldWorkspacePtr->state->clients.erase(addr); - bool wasUrgent = oldWorkspacePtr->state->urgentClients.erase(addr) > 0; - refreshIndicator(oldWorkspacePtr); + auto oldWorkspaceIt = workspaces.find(oldWorkspaceId); + bool wasUrgent = false; + if (oldWorkspaceIt != workspaces.end()) { + oldWorkspaceIt->second->state->clients.erase(addr); + wasUrgent = oldWorkspaceIt->second->state->urgentClients.erase(addr) > 0; + refreshIndicator(oldWorkspaceIt->second); + } clientPtr->workspaceId = newWorkspaceId; - auto newWorkspacePtr = workspaces[newWorkspaceId]; - newWorkspacePtr->state->clients[addr] = clientPtr; - if (wasUrgent) { - newWorkspacePtr->state->urgentClients.insert(addr); + auto newWorkspaceIt = workspaces.find(newWorkspaceId); + if (newWorkspaceIt != workspaces.end()) { + newWorkspaceIt->second->state->clients[addr] = clientPtr; + if (wasUrgent) { + newWorkspaceIt->second->state->urgentClients.insert(addr); + } + refreshIndicator(newWorkspaceIt->second); } - refreshIndicator(newWorkspacePtr); } void HyprlandService::onOpenWindow(std::string windowData) { @@ -209,10 +232,11 @@ void HyprlandService::onOpenWindow(std::string windowData) { clientPtr->workspaceId = workspaceId; clientPtr->title = title; this->clients[clientPtr->address] = clientPtr; - auto workspacePtr = workspaces[clientPtr->workspaceId]; - workspacePtr->state->clients[clientPtr->address] = clientPtr; - - refreshIndicator(workspacePtr); + auto workspaceIt = workspaces.find(clientPtr->workspaceId); + if (workspaceIt != workspaces.end()) { + workspaceIt->second->state->clients[clientPtr->address] = clientPtr; + refreshIndicator(workspaceIt->second); + } } void HyprlandService::onCloseWindow(std::string windowData) { @@ -225,12 +249,15 @@ void HyprlandService::onCloseWindow(std::string windowData) { auto clientPtr = this->clients[addr]; int workspaceId = clientPtr->workspaceId; - - auto workspacePtr = workspaces[workspaceId]; - workspacePtr->state->clients.erase(addr); this->clients.erase(addr); - refreshIndicator(workspacePtr); + auto workspaceIt = workspaces.find(workspaceId); + if (workspaceIt == workspaces.end()) { + return; + } + workspaceIt->second->state->clients.erase(addr); + + refreshIndicator(workspaceIt->second); } void HyprlandService::handleSocketMessage(SocketHelper::SocketMessage message) { @@ -322,7 +349,11 @@ void HyprlandService::onMonitorAdded(std::string monitorName) { if (!workspace.contains("monitor") || workspace["monitor"].get() != monitorName) { continue; } - auto workspacePtr = workspaces[workspace["id"].get()]; + auto workspaceIt = workspaces.find(workspace["id"].get()); + if (workspaceIt == workspaces.end()) { + continue; + } + auto workspacePtr = workspaceIt->second; auto state = workspacePtr->state; state->id = workspace["id"].get(); @@ -391,10 +422,13 @@ void HyprlandService::onUrgent(std::string windowAddress) { auto clientPtr = this->clients[addr]; int workspaceId = clientPtr->workspaceId; - auto workspacePtr = workspaces[workspaceId]; - workspacePtr->state->urgentClients.insert(addr); + auto workspaceIt = workspaces.find(workspaceId); + if (workspaceIt == workspaces.end()) { + return; + } + workspaceIt->second->state->urgentClients.insert(addr); - refreshIndicator(workspacePtr); + refreshIndicator(workspaceIt->second); } void HyprlandService::onActiveWindowChanged(std::string windowAddress) { @@ -406,10 +440,13 @@ void HyprlandService::onActiveWindowChanged(std::string windowAddress) { auto clientPtr = this->clients[addr]; int workspaceId = clientPtr->workspaceId; - auto workspacePtr = workspaces[workspaceId]; - workspacePtr->state->urgentClients.erase(addr); + auto workspaceIt = workspaces.find(workspaceId); + if (workspaceIt == workspaces.end()) { + return; + } + workspaceIt->second->state->urgentClients.erase(addr); - refreshIndicator(workspacePtr); + refreshIndicator(workspaceIt->second); } std::shared_ptr HyprlandService::getWorkspaceIndicatorsForMonitor(std::string monitorName) { diff --git a/src/services/textureCache.cpp b/src/services/textureCache.cpp index 1a26595..c626d4f 100644 --- a/src/services/textureCache.cpp +++ b/src/services/textureCache.cpp @@ -132,6 +132,15 @@ TextureCacheService *TextureCacheService::getInstance() { return &instance; } +TextureCacheService::TextureCacheService() { + curl_global_init(CURL_GLOBAL_DEFAULT); +} + +TextureCacheService::~TextureCacheService() { + clear(); + curl_global_cleanup(); +} + Glib::RefPtr TextureCacheService::getTexture(const std::string &url) { if (url.empty()) { return {}; @@ -220,3 +229,7 @@ void TextureCacheService::pruneCache() { } } } + +void TextureCacheService::clear() { + cache.clear(); +} diff --git a/src/widgets/controlCenter/bluetoothSettings.cpp b/src/widgets/controlCenter/bluetoothSettings.cpp index e1b7ab9..c15fb11 100644 --- a/src/widgets/controlCenter/bluetoothSettings.cpp +++ b/src/widgets/controlCenter/bluetoothSettings.cpp @@ -2,6 +2,120 @@ #include +BluetoothSettings::BluetoothSettings() : bluetoothIsPowered(this->bluetoothController->isPowered()), bluetoothIsScanning(this->bluetoothController->isDiscovering()) { + set_orientation(Gtk::Orientation::VERTICAL); + set_spacing(12); + + add_css_class("bluetooth-settings"); + + 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); + }); + + this->buttonRow.append(*powerButton); + + scanButton->add_css_class("power-button"); + setScanning(bluetoothIsScanning); + scanButton->set_tooltip_text(bluetoothIsScanning ? "Stop Scanning" : "Start Scanning"); + scanButton->signal_clicked().connect([this]() { + bluetoothIsScanning = !bluetoothIsScanning; + if (bluetoothIsScanning) { + this->bluetoothController->startDiscovery(); + } else { + this->bluetoothController->stopDiscovery(); + } + }); + this->buttonRow.append(*scanButton); + + this->buttonRow.set_spacing(12); + append(this->buttonRow); + + + connectedDevicesBox.add_css_class("active-devices"); + connectedDevicesBox.set_orientation(Gtk::Orientation::VERTICAL); + connectedDevicesScroll.set_child(connectedDevicesBox); + connectedDevicesScroll.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC); + connectedDevicesScroll.set_propagate_natural_height(true); + connectedDevicesScroll.set_max_content_height(200); + + append(connectedDevicesScroll); + + + availableDevicesBox.add_css_class("available-devices"); + availableDevicesBox.set_orientation(Gtk::Orientation::VERTICAL); + availableDevicesScroll.set_child(availableDevicesBox); + availableDevicesScroll.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC); + availableDevicesScroll.set_propagate_natural_height(true); + availableDevicesScroll.set_max_content_height(200); + append(availableDevicesScroll); + + this->bluetoothController->signalPoweredChanged().connect([this](bool powered) { + bluetoothIsPowered = powered; + setBluetoothPowered(powered); + }); + + this->bluetoothController->signalDiscoveringChanged().connect([this](bool scanning) { + bluetoothIsScanning = scanning; + setScanning(scanning); + }); + + auto devices = this->bluetoothController->getDevices(); + + for (const auto &device : devices) { + auto row = std::make_shared(device); + deviceRows[device.object_path] = row; + if (device.connected) { + connectedDevicesBox.append(*row); + } else { + availableDevicesBox.append(*row); + } + } + + this->bluetoothController->signalDeviceAdded().connect([this](const BluetoothDevice &device) { + auto row = std::make_shared(device); + deviceRows[device.object_path] = row; + if (device.connected) { + connectedDevicesBox.append(*row); + } else { + availableDevicesBox.append(*row); + } + }); + + this->bluetoothController->signalDeviceRemoved().connect([this](const std::string &object_path) { + auto it = deviceRows.find(object_path); + if (it != deviceRows.end()) { + auto parent = it->second->get_parent(); + if (parent) { + dynamic_cast(parent)->remove(*it->second); + } + deviceRows.erase(it); + } + }); + + this->bluetoothController->signalDeviceChanged().connect([this](const BluetoothDevice &device) { + auto it = deviceRows.find(device.object_path); + if (it != deviceRows.end()) { + it->second->updateDevice(device); + + // Move between boxes if connection status changed + auto parent = it->second->get_parent(); + Gtk::Box *targetBox = device.connected ? &connectedDevicesBox : &availableDevicesBox; + if (parent != targetBox) { + if (parent) { + dynamic_cast(parent)->remove(*it->second); + } + targetBox->append(*it->second); + } + } + }); +} + void BluetoothSettings::setBluetoothPowered(bool powered) { this->bluetoothIsPowered = powered; @@ -28,54 +142,4 @@ void BluetoothSettings::setScanning(bool scanning) { 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/bluetoothSettingsRow.cpp b/src/widgets/controlCenter/bluetoothSettingsRow.cpp new file mode 100644 index 0000000..46da09e --- /dev/null +++ b/src/widgets/controlCenter/bluetoothSettingsRow.cpp @@ -0,0 +1,90 @@ +#include "widgets/controlCenter/bluetoothSettingsRow.hpp" + +#include + +#include "components/button/iconButton.hpp" +#include "connection/dbus/bluetooth.hpp" +#include "helpers/string.hpp" +#include "services/notificationController.hpp" + +BluetoothSettingsRow::BluetoothSettingsRow(const BluetoothDevice &device) + : device(device), + pairButton(device.paired ? Icon::BLUETOOTH_CONNECTED : Icon::BLUETOOTH), + connectButton(device.connected ? Icon::LINK : Icon::LINK_OFF), + trustButton(device.trusted ? Icon::DONE_ALL : Icon::REMOVE_DONE) { + set_orientation(Gtk::Orientation::HORIZONTAL); + set_spacing(10); + + add_css_class("bluetooth-settings-row"); + set_hexpand(true); + set_halign(Gtk::Align::FILL); + + if (!device.icon.empty()) { + this->icon.set_from_icon_name(device.icon); + this->icon.set_pixel_size(24); + append(this->icon); + } + + auto deviceInfoBox = Gtk::Box(Gtk::Orientation::VERTICAL); + deviceInfoBox.set_spacing(2); + append(deviceInfoBox); + + std::string shortName = StringHelper::trimToSize(device.name.empty() ? "Unknown Device" : device.name, 14); + + nameLabel.set_text(shortName); + nameLabel.set_halign(Gtk::Align::START); + nameLabel.set_valign(Gtk::Align::CENTER); + deviceInfoBox.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"); + deviceInfoBox.append(addressLabel); + + auto buttonBox = Gtk::Box(Gtk::Orientation::HORIZONTAL); + buttonBox.set_hexpand(true); + buttonBox.set_halign(Gtk::Align::END); + append(buttonBox); + + pairButton.signal_clicked().connect([this]() { + if (this->device.paired) { + BluetoothController::getInstance()->unpairDevice(this->device.object_path); + } else { + BluetoothController::getInstance()->pairDevice(this->device.object_path); + } + }); + buttonBox.append(pairButton); + + connectButton.signal_clicked().connect([this]() { + if (this->device.connected) { + BluetoothController::getInstance()->disconnectDevice(this->device.object_path); + } else { + BluetoothController::getInstance()->connectDevice(this->device.object_path); + } + }); + buttonBox.append(connectButton); + + trustButton.signal_clicked().connect([this]() { + BluetoothController::getInstance()->trustDevice(this->device.object_path, !this->device.trusted); + }); + buttonBox.append(trustButton); +} + +void BluetoothSettingsRow::updateDevice(const BluetoothDevice &device) { + this->device = device; + + if (!device.icon.empty()) { + this->icon.set_from_icon_name(device.icon); + } + + spdlog::info("Updating device {}: paired={}, connected={}, trusted={}", device.name, device.paired, device.connected, device.trusted); + + std::string shortName = StringHelper::trimToSize(device.name.empty() ? "Unknown Device" : device.name, 14); + + nameLabel.set_text(shortName); + addressLabel.set_text(device.address); + pairButton.setIcon(device.paired ? Icon::BLUETOOTH_CONNECTED : Icon::BLUETOOTH); + connectButton.setIcon(device.connected ? Icon::LINK : Icon::LINK_OFF); + trustButton.setIcon(device.trusted ? Icon::DONE_ALL : Icon::REMOVE_DONE); +} diff --git a/src/widgets/controlCenter/controlCenter.cpp b/src/widgets/controlCenter/controlCenter.cpp index bed5684..8f6a095 100644 --- a/src/widgets/controlCenter/controlCenter.cpp +++ b/src/widgets/controlCenter/controlCenter.cpp @@ -21,7 +21,6 @@ ControlCenter::ControlCenter(Icon::Type icon, std::string name) this->tabRow.set_orientation(Gtk::Orientation::HORIZONTAL); this->tabRow.set_spacing(4); - this->tabRow.set_margin_bottom(4); this->tabRow.add_css_class("control-center-tab-row"); this->mediaTabButton = std::make_unique(Icon::PLAY_CIRCLE); @@ -78,12 +77,15 @@ void ControlCenter::setActiveTab(const std::string &tab_name) { this->mediaTabButton->setActive(false); this->infoTabButton->setActive(false); this->timerButton->setActive(false); - + this->settingsTabButton->setActive(false); + if (tab_name == "controls") { this->mediaTabButton->setActive(true); } else if (tab_name == "info") { this->infoTabButton->setActive(true); } else if (tab_name == "timer") { this->timerButton->setActive(true); + } else if (tab_name == "settings") { + this->settingsTabButton->setActive(true); } } diff --git a/src/widgets/notification/copyNotification.cpp b/src/widgets/notification/copyNotification.cpp index dd1f405..36915a8 100644 --- a/src/widgets/notification/copyNotification.cpp +++ b/src/widgets/notification/copyNotification.cpp @@ -79,7 +79,6 @@ void CopyNotification::createImageNotification(NotifyMessage notify) { auto contentBox = Gtk::make_managed(); contentBox->set_orientation(Gtk::Orientation::VERTICAL); - contentBox->set_margin(0); auto imageWidget = Gtk::make_managed(this->copiedImage); contentBox->append(*imageWidget); imageWidget->set_pixel_size(300); diff --git a/src/widgets/notification/notificationWindow.cpp b/src/widgets/notification/notificationWindow.cpp index e518e13..38bcbd1 100644 --- a/src/widgets/notification/notificationWindow.cpp +++ b/src/widgets/notification/notificationWindow.cpp @@ -7,6 +7,7 @@ #include "helpers/string.hpp" #include "gtkmm/box.h" +#include "gtkmm/entry.h" #include "gtkmm/image.h" #include "gtkmm/label.h" @@ -70,6 +71,22 @@ NotificationWindow::NotificationWindow(uint64_t notificationId, std::shared_ptr< body_label->set_wrap(true); vbox->append(*body_label); + if (notify.has_input) { + auto entry = Gtk::make_managed(); + entry->set_placeholder_text(notify.input_placeholder); + entry->set_halign(Gtk::Align::FILL); + entry->set_hexpand(true); + entry->add_css_class("notification-entry"); + vbox->append(*entry); + + entry->signal_activate().connect([this, entry, cb = notify.on_input]() { + if (cb) { + cb(entry->get_text()); + this->getSignalClose().emit(this->getNotificationId()); + } + }); + } + // If actions exist, add buttons if (!notify.actions.empty()) { auto actions_box = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); diff --git a/src/widgets/weather.cpp b/src/widgets/weather.cpp index 7f739b0..b6a13d7 100644 --- a/src/widgets/weather.cpp +++ b/src/widgets/weather.cpp @@ -32,10 +32,6 @@ WeatherWidget::WeatherWidget() : Gtk::Box(Gtk::Orientation::VERTICAL) { this->set_orientation(Gtk::Orientation::VERTICAL); this->set_spacing(6); - this->set_margin_top(4); - this->set_margin_bottom(4); - this->set_margin_start(4); - this->set_margin_end(4); this->titleLabel.set_text("Weather"); this->currentLabel.set_text("Now: --");