From ed1a9a86058f726712355a04ef5dc9ffb981c643 Mon Sep 17 00:00:00 2001 From: Arif Hasanic Date: Mon, 2 Feb 2026 21:46:50 +0100 Subject: [PATCH] close one notification to close all --- .gitea/workflows/ci.yml | 43 ----------- .gitignore | 1 - CMakeLists.txt | 10 ++- include/helpers/socket.hpp | 6 +- include/services/dbus/messages.hpp | 13 +++- include/services/dbus/mpris.hpp | 25 ++++++- .../widgets/controlCenter/mediaControl.hpp | 5 ++ .../widgets/notification/baseNotification.hpp | 2 +- ...otification.hpp => notificationWindow.hpp} | 0 resources/notification.css | 32 +++++++- src/services/dbus/mpris.cpp | 70 +++++++++++++----- src/services/dbus/notification.cpp | 74 ++++++++++++++++--- src/services/hyprland.cpp | 18 +++-- src/services/notificationController.cpp | 10 ++- src/services/tray.cpp | 40 +++++----- src/widgets/controlCenter/mediaControl.cpp | 54 +++++++++++++- src/widgets/notification/baseNotification.cpp | 5 +- ...otification.cpp => notificationWindow.cpp} | 34 ++++++++- .../notification/spotifyNotification.cpp | 4 +- src/widgets/tray.cpp | 44 +++++------ src/widgets/volumeWidget.cpp | 8 +- 21 files changed, 352 insertions(+), 146 deletions(-) delete mode 100644 .gitea/workflows/ci.yml rename include/widgets/notification/{notification.hpp => notificationWindow.hpp} (100%) rename src/widgets/notification/{notification.cpp => notificationWindow.cpp} (56%) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml deleted file mode 100644 index bbcfb01..0000000 --- a/.gitea/workflows/ci.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: ci - -on: - push: - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - container: - image: git.rivercry.com/system/bar:latest - credentials: - username: docker - password: ${{ secrets.DOCKER_PASSWORD }} - steps: - - name: Checkout - run: | - git init - git remote add origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@git.rivercry.com/${{ github.repository }}.git - git fetch --depth 1 origin ${{ github.sha }} - git checkout FETCH_HEAD - - name: Toolchain Info - run: | - echo "CC=$CC" - echo "CXX=$CXX" - command -v gcc || true - command -v g++ || true - command -v gcov || true - gcc --version || true - g++ --version || true - gcov --version || true - - name: Configure - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON -DCMAKE_C_COMPILER=/usr/bin/gcc -DCMAKE_CXX_COMPILER=/usr/bin/g++ - - - name: Build - run: cmake --build build --config Debug -j "$(nproc)" - - - name: Test & Coverage - run: | - cd build - ctest -T Test --output-on-failure - ctest -T Coverage --output-on-failure - gcovr -r .. --xml-pretty -o coverage.xml diff --git a/.gitignore b/.gitignore index 816992a..7c5b040 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,5 @@ test/test_runner cmake-build-debug/**/* .idea - .cache/ Testing/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index e511760..f35e932 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,7 +41,7 @@ target_sources(bar_lib src/widgets/clock.cpp src/widgets/date.cpp src/widgets/notification/baseNotification.cpp - src/widgets/notification/notification.cpp + src/widgets/notification/notificationWindow.cpp src/widgets/notification/spotifyNotification.cpp src/widgets/volumeWidget.cpp src/widgets/webWidget.cpp @@ -69,7 +69,13 @@ add_executable(bar main.cpp) target_link_libraries(bar bar_lib ${GTKMM_LIBRARIES} ${LAYERSHELL_LIBRARIES} ${WEBKIT_LIBRARIES} ${CURL_LIBRARIES} nlohmann_json::nlohmann_json) -# ---- Tests (Catch2) ---- + +# add spdlog +find_package(spdlog REQUIRED) +target_link_libraries(bar_lib PRIVATE spdlog::spdlog) + + +# ---- Testing shit find_package(Catch2 3 REQUIRED) add_executable(bar_tests diff --git a/include/helpers/socket.hpp b/include/helpers/socket.hpp index 0caafb7..0434b51 100644 --- a/include/helpers/socket.hpp +++ b/include/helpers/socket.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include #include #include @@ -25,9 +25,9 @@ class SocketHelper { buffer[bytesRead] = '\0'; data = std::string(buffer); } else if (bytesRead == 0) { - std::cerr << "Socket closed by peer" << std::endl; + spdlog::warn("Socket closed by peer"); } else { - std::cerr << "Error reading from socket" << std::endl; + spdlog::error("Error reading from socket"); } auto delimiterPos = data.find(delimiter); diff --git a/include/services/dbus/messages.hpp b/include/services/dbus/messages.hpp index b4a5e8c..654c80b 100644 --- a/include/services/dbus/messages.hpp +++ b/include/services/dbus/messages.hpp @@ -6,13 +6,14 @@ #include #include #include +#include "gdkmm/pixbuf.h" #include "glibmm/variant.h" struct MprisPlayer2Message { std::string title; - std::string artist; + std::vector artist; std::string artwork_url; int64_t length_ms; @@ -21,6 +22,12 @@ struct MprisPlayer2Message { std::function previous; }; +enum NotificationUrgency { + LOW = 0, + NORMAL = 1, + CRITICAL = 2 +}; + struct NotifyMessage { std::string app_name; uint32_t replaces_id; @@ -28,8 +35,10 @@ struct NotifyMessage { std::string summary; std::string body; std::vector actions; - std::map hints; + NotificationUrgency urgency = NORMAL; int32_t expire_timeout; // Callback to invoke when an action is triggered std::function on_action; + // image data (if any) from dbus + std::optional> imageData; }; \ No newline at end of file diff --git a/include/services/dbus/mpris.hpp b/include/services/dbus/mpris.hpp index 736df94..0fbf2cf 100644 --- a/include/services/dbus/mpris.hpp +++ b/include/services/dbus/mpris.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -7,6 +8,18 @@ class MprisController { public: + struct PlayerState { + std::string title; + std::vector artist; + std::string artwork_url; + int64_t length_ms; + }; + enum class PlaybackStatus { + Playing, + Paused, + Stopped, + }; + static std::shared_ptr getInstance(); void toggle_play(); @@ -15,15 +28,25 @@ class MprisController { void emit_seeked(int64_t position_us); sigc::signal &signal_mpris_updated(); + sigc::signal &signal_playback_status_changed(); + sigc::signal &signal_playback_position_changed(); private: MprisController(); + std::map playbackStatusMap = { + {"Playing", PlaybackStatus::Playing}, + {"Paused", PlaybackStatus::Paused}, + {"Stopped", PlaybackStatus::Stopped}, + }; - bool playerRunning = false; + PlaybackStatus currentPlaybackStatus = PlaybackStatus::Stopped; Glib::RefPtr m_connection; Glib::RefPtr m_proxy; + sigc::signal mprisUpdatedSignal; + sigc::signal playbackStatusChangedSignal; + sigc::signal playbackPositionChangedSignal; void on_bus_connected(const Glib::RefPtr &result); void signalNotification(); diff --git a/include/widgets/controlCenter/mediaControl.hpp b/include/widgets/controlCenter/mediaControl.hpp index 338b46e..e9fd7de 100644 --- a/include/widgets/controlCenter/mediaControl.hpp +++ b/include/widgets/controlCenter/mediaControl.hpp @@ -49,4 +49,9 @@ class MediaControlWidget : public Gtk::Box { Gtk::ScrolledWindow imageWrapper; void onSpotifyMprisUpdated(const MprisPlayer2Message &message); + + void onRunningStateChanged(MprisController::PlaybackStatus status); + void onPlay(); + void onPause(); + void onStop(); }; \ No newline at end of file diff --git a/include/widgets/notification/baseNotification.hpp b/include/widgets/notification/baseNotification.hpp index 24e4606..67884ec 100644 --- a/include/widgets/notification/baseNotification.hpp +++ b/include/widgets/notification/baseNotification.hpp @@ -11,7 +11,7 @@ #include "gtkmm/window.h" -#define DEFAULT_NOTIFICATION_TIMEOUT 4000 +#define DEFAULT_NOTIFICATION_TIMEOUT 7000 class BaseNotification : public Gtk::Window { public: diff --git a/include/widgets/notification/notification.hpp b/include/widgets/notification/notificationWindow.hpp similarity index 100% rename from include/widgets/notification/notification.hpp rename to include/widgets/notification/notificationWindow.hpp diff --git a/resources/notification.css b/resources/notification.css index fb6864f..a634032 100644 --- a/resources/notification.css +++ b/resources/notification.css @@ -5,6 +5,7 @@ --text-font-mono: "Hack Nerd Font Mono", monospace; --color-notification-bg: rgba(30, 30, 30, 0.95); + --color-notification-critical-bg: rgba(50, 20, 20, 0.95); --color-border: rgba(80, 80, 80, 0.8); } @@ -12,11 +13,40 @@ border-radius: 8px; padding: 8px 12px; background: var(--color-notification-bg); - box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2); + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5); border: 1px solid var(--color-border); font-size: 14px; } +.notification-critical { + background: linear-gradient(145deg, #321010, #1e1e1e); + border: 1px solid rgba(255, 75, 75, 0.5); + box-shadow: + 0 4px 30px rgba(0, 0, 0, 0.8), + 0 0 15px rgba(255, 75, 75, 0.2), + inset 0 0 10px rgba(255, 75, 75, 0.1); + + color: #ffffff; + font-weight: 500; +} + +.notification-normal { + background: linear-gradient(90deg, #0c1f3a 0%, #1e1e1e 100%); + border: 1px solid rgba(60, 160, 255, 0.3); + border-left: 3px solid #3c9aff; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), + 0 0 10px rgba(60, 160, 255, 0.1); + color: #ffffff; +} + +.notification-low { + background: linear-gradient(145deg, #2a2a2a, #1e1e1e); + border: 1px solid rgba(255, 255, 255, 0.15); + color: rgba(255, 255, 255, 0.95); + /* white box shadow */ + box-shadow: 0 4px 20px rgba(255, 255, 255, 0.05); + font-size: 13px; +} .notification-button-box { border-top: 1px solid var(--color-border); padding-top: 6px; diff --git a/src/services/dbus/mpris.cpp b/src/services/dbus/mpris.cpp index 2e547ed..9be52a1 100644 --- a/src/services/dbus/mpris.cpp +++ b/src/services/dbus/mpris.cpp @@ -1,6 +1,7 @@ #include "services/dbus/mpris.hpp" -#include + #include +#include #include #include "helpers/string.hpp" @@ -19,14 +20,21 @@ MprisController::MprisController() { sigc::mem_fun(*this, &MprisController::on_bus_connected)); } -sigc::signal & -MprisController::signal_mpris_updated() { +sigc::signal &MprisController::signal_mpris_updated() { return mprisUpdatedSignal; } +sigc::signal &MprisController::signal_playback_status_changed() { + return playbackStatusChangedSignal; +} + +sigc::signal &MprisController::signal_playback_position_changed() { + return playbackPositionChangedSignal; +} + void MprisController::on_bus_connected(const Glib::RefPtr &result) { if (!result) { - std::cerr << "DBus Connection Error: null async result" << std::endl; + spdlog::error("DBus Connection Error: null async result"); return; } try { @@ -42,7 +50,7 @@ void MprisController::on_bus_connected(const Glib::RefPtr &res ); if (m_proxy) { - std::cout << "Connected to: " << player_bus_name << std::endl; + spdlog::info("Connected to: {}", player_bus_name); signalNotification(); @@ -51,7 +59,7 @@ void MprisController::on_bus_connected(const Glib::RefPtr &res } } catch (const Glib::Error &ex) { - std::cerr << "DBus Connection Error: " << ex.what() << std::endl; + spdlog::error("DBus Connection Error: {}", ex.what()); } } @@ -64,12 +72,12 @@ void MprisController::signalNotification() { m_proxy->get_cached_property(metadata_var, "Metadata"); if (!metadata_var) { - std::cout << "No metadata available." << std::endl; + spdlog::info("No metadata available."); return; } if (!metadata_var.is_of_type(Glib::VariantType("a{sv}"))) { - std::cout << "Unexpected metadata type." << std::endl; + spdlog::error("Unexpected metadata type."); return; } @@ -81,7 +89,9 @@ void MprisController::signalNotification() { metadata_map = variant_dict.get(); - std::string title, artist, artwork_url; + std::string title, artwork_url; + std::vector artist; + if (metadata_map.count("xesam:title")) { const auto &title_base = metadata_map["xesam:title"]; if (title_base.is_of_type(Glib::VariantType("s"))) { @@ -96,11 +106,13 @@ void MprisController::signalNotification() { if (artist_var.is_of_type(Glib::VariantType("as"))) { auto artists = Glib::VariantBase::cast_dynamic>>(artist_var).get(); if (!artists.empty()) { - artist = artists[0]; // Take the first artist + for (const auto &a : artists) { + artist.push_back(a); + } } } else if (artist_var.is_of_type(Glib::VariantType("s"))) { auto artist_str = Glib::VariantBase::cast_dynamic>(artist_var); - artist = artist_str.get(); + artist.push_back(artist_str.get()); } } @@ -117,21 +129,21 @@ void MprisController::signalNotification() { const auto &length_base = metadata_map["mpris:length"]; if (length_base.is_of_type(Glib::VariantType("x"))) { auto length_var = Glib::VariantBase::cast_dynamic>(length_base); - length_us = static_cast(length_var.get()); + length_us = static_cast(length_var.get()); } else if (length_base.is_of_type(Glib::VariantType("t"))) { auto length_var = Glib::VariantBase::cast_dynamic>(length_base); - length_us = static_cast(length_var.get()); + length_us = static_cast(length_var.get()); } } MprisPlayer2Message mpris; mpris.title = StringHelper::trimToSize(title, 30); - mpris.artist = StringHelper::trimToSize(artist, 30); + mpris.artist = artist; mpris.artwork_url = artwork_url; mpris.play_pause = [this]() { this->toggle_play(); }; mpris.next = [this]() { this->next_song(); }; mpris.previous = [this]() { this->previous_song(); }; - mpris.length_ms = length_us; + mpris.length_ms = length_us; // Convert microseconds to milliseconds mprisUpdatedSignal.emit(mpris); } @@ -141,6 +153,31 @@ void MprisController::on_properties_changed(const Gio::DBus::Proxy::MapChangedPr if (changed_properties.find("Metadata") != changed_properties.end()) { signalNotification(); } + + if (changed_properties.find("PlaybackStatus") != changed_properties.end()) { + auto status_var = changed_properties.at("PlaybackStatus"); + if (status_var.is_of_type(Glib::VariantType("s"))) { + auto status = Glib::VariantBase::cast_dynamic>(status_var).get(); + spdlog::info("Playback Status changed to: {}", status.raw()); + auto parsedStatusIt = playbackStatusMap.find(static_cast(status)); + + if (parsedStatusIt != playbackStatusMap.end()) { + currentPlaybackStatus = parsedStatusIt->second; + playbackStatusChangedSignal.emit(currentPlaybackStatus); + } else { + spdlog::error("Unknown playback status: {}", status.raw()); + } + } + } + + if (changed_properties.find("Position") != changed_properties.end()) { + auto position_var = changed_properties.at("Position"); + if (position_var.is_of_type(Glib::VariantType("x"))) { + auto position = Glib::VariantBase::cast_dynamic>(position_var).get(); + spdlog::info("Position changed to: {}", position); + playbackPositionChangedSignal.emit(static_cast(position)); + } + } } void MprisController::previous_song() { @@ -149,7 +186,6 @@ void MprisController::previous_song() { } } - void MprisController::toggle_play() { if (m_proxy) { m_proxy->call("PlayPause"); @@ -173,6 +209,6 @@ void MprisController::emit_seeked(int64_t position_us) { m_proxy->call("Seek", params); } catch (const Glib::Error &ex) { - std::cerr << "Error seeking: " << ex.what() << std::endl; + spdlog::error("Error seeking: {}", ex.what()); } } diff --git a/src/services/dbus/notification.cpp b/src/services/dbus/notification.cpp index ee13e31..fb410cf 100644 --- a/src/services/dbus/notification.cpp +++ b/src/services/dbus/notification.cpp @@ -1,14 +1,16 @@ #include "services/dbus/notification.hpp" -#include +#include +#include +#include "helpers/string.hpp" #include "services/notificationController.hpp" #include "glib.h" #include "glibconfig.h" void NotificationService::onBusAcquired(const Glib::RefPtr &connection, const Glib::ustring &name) { - std::cout << "Acquired bus name: " << name << std::endl; + spdlog::info("Acquired bus name: {}", name.raw()); auto introspection_data = Gio::DBus::NodeInfo::create_for_xml(introspection_xml); @@ -74,27 +76,79 @@ void NotificationService::handle_notify(const Glib::VariantContainerBase ¶me std::map hints = Glib::VariantBase::cast_dynamic>>(hints_var).get(); gint32 expire_timeout = Glib::VariantBase::cast_dynamic>(timeout_var).get(); - std::cout << "Notification Received: " << summary << " - " << body << std::endl; + spdlog::info("Notification Received: {} - {}", summary.raw(), body.raw()); NotifyMessage notify; notify.app_name = app_name; notify.replaces_id = replaces_id; notify.app_icon = app_icon; - notify.summary = summary; - notify.body = body; + notify.summary = static_cast(summary); + notify.body = static_cast(body); std::vector actions_converted; actions_converted.reserve(actions.size()); - for (const auto &a : actions) { - actions_converted.emplace_back(static_cast(a)); - } + for (ulong i = 0; i < actions.size(); i += 2) { + auto name = static_cast(actions[i]); + auto label = static_cast(actions[i + 1]); + if (name == "default") { + label = "Open"; + } + + actions_converted.push_back(name); + actions_converted.push_back(label); + } notify.actions = actions_converted; - // notify.hints = hints; + + for (const auto &[key, value] : hints) { + if (key == "urgency") { + if (value.is_of_type(Glib::VariantType("y"))) { + auto urgency = Glib::VariantBase::cast_dynamic>(value).get(); + notify.urgency = static_cast(urgency); + } + } + + if (key == "image-data") { + Glib::VariantBase image_value = value; + if (image_value.is_of_type(Glib::VariantType("v"))) { + try { + image_value = Glib::VariantBase::cast_dynamic>(image_value).get(); + } catch (const std::bad_cast &) { + spdlog::warn("Failed to unwrap image-data variant"); + } + } + + if (image_value.is_of_type(Glib::VariantType("(iiibiiay)"))) { + // This is a raw data image format which describes width, height, rowstride, has alpha, + // bits per sample, channels and image data respectively. + try { + auto image_data_variant = Glib::VariantBase::cast_dynamic< + Glib::Variant>>>(image_value); + auto [width, height, rowstride, has_alpha, bits_per_sample, channels, data] = image_data_variant.get(); + (void)channels; + auto pixbuf = Gdk::Pixbuf::create_from_data( + data.data(), + Gdk::Colorspace::RGB, + has_alpha, + bits_per_sample, + width, + height, + rowstride); + notify.imageData = pixbuf; + } catch (const std::bad_cast &e) { + spdlog::warn("Failed to decode image-data hint: {}", e.what()); + } + } + } + } notify.expire_timeout = expire_timeout; + if (app_name == "Thunderbird") { + notify.expire_timeout = 10000; // 10 seconds for email notifications + } + guint id = notificationIdCounter++; // Set up the callback to emit ActionInvoked on D-Bus Glib::RefPtr dbus_conn = invocation->get_connection(); @@ -108,7 +162,7 @@ void NotificationService::handle_notify(const Glib::VariantContainerBase ¶me Glib::VariantContainerBase::create_tuple({Glib::Variant::create(id), Glib::Variant::create(action_id)})); } catch (const std::exception &e) { - std::cerr << "Failed to emit ActionInvoked: " << e.what() << std::endl; + spdlog::error("Failed to emit ActionInvoked: {}", e.what()); } }; NotificationController::getInstance()->showNotificationOnAllMonitors(notify); diff --git a/src/services/hyprland.cpp b/src/services/hyprland.cpp index de5dd9e..c877099 100644 --- a/src/services/hyprland.cpp +++ b/src/services/hyprland.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -16,6 +15,8 @@ #include "gtkmm/box.h" +#include "spdlog/spdlog.h" + HyprlandService::HyprlandService() { init(); this->bindHyprlandSocket(); @@ -88,7 +89,7 @@ void HyprlandService::bindHyprlandSocket() { socketFd = socket(AF_UNIX, SOCK_STREAM, 0); if (socketFd == -1) { - std::cerr << "[Hyprland] Failed to create socket" << std::endl; + spdlog::error("[Hyprland] Failed to create socket"); return; } @@ -98,7 +99,7 @@ void HyprlandService::bindHyprlandSocket() { std::strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); if (connect(socketFd, reinterpret_cast(&addr), sizeof(addr)) == -1) { - std::cerr << "[Hyprland] Failed to connect to " << socketPath << std::endl; + spdlog::error("[Hyprland] Failed to connect to {}", socketPath); close(socketFd); socketFd = -1; @@ -169,6 +170,7 @@ void HyprlandService::onMonitorRemoved(std::string monitorName) { monitorPtr->monitorWorkspaces.clear(); monitorPtr->bar->close(); + monitorPtr->bar = nullptr; this->monitors.erase(monitorName); } @@ -179,7 +181,7 @@ void HyprlandService::onMoveWindow(std::string windowData) { int newWorkspaceId = std::stoi(parts[1]); if (this->clients.find(addr) == this->clients.end()) { - std::cerr << "[Hyprland] onMoveWindow: Client not found: " << addr << std::endl; + spdlog::warn("[Hyprland] onMoveWindow: Client {} not found", addr); return; } @@ -188,19 +190,19 @@ void HyprlandService::onMoveWindow(std::string windowData) { auto oldWorkspacePtr = workspaces[oldWorkspaceId]; oldWorkspacePtr->state->clients.erase(addr); + bool wasUrgent = oldWorkspacePtr->state->urgentClients.erase(addr) > 0; refreshIndicator(oldWorkspacePtr); clientPtr->workspaceId = newWorkspaceId; auto newWorkspacePtr = workspaces[newWorkspaceId]; newWorkspacePtr->state->clients[addr] = clientPtr; + if (wasUrgent) { + newWorkspacePtr->state->urgentClients.insert(addr); + } refreshIndicator(newWorkspacePtr); } -// void HyprlandService::onMonitorAdded(std::string monitorName) { -// // this->signalMonitorAdded.emit(); -// } - void HyprlandService::onOpenWindow(std::string windowData) { auto parts = StringHelper::split(windowData, ','); std::string addr = "0x" + parts[0]; diff --git a/src/services/notificationController.cpp b/src/services/notificationController.cpp index 5be26e7..c68f031 100644 --- a/src/services/notificationController.cpp +++ b/src/services/notificationController.cpp @@ -3,15 +3,13 @@ #include #include "services/dbus/messages.hpp" -#include "widgets/notification/notification.hpp" +#include "widgets/notification/notificationWindow.hpp" #include "widgets/notification/spotifyNotification.hpp" #include "gdkmm/display.h" #include "glibmm/main.h" -#define DEFAULT_NOTIFICATION_TIMEOUT 4000 - std::shared_ptr NotificationController::instance = nullptr; NotificationController::NotificationController() { @@ -60,7 +58,11 @@ void NotificationController::showNotificationOnAllMonitors(NotifyMessage notify) notification->show(); // -1 means use default timeout, 0 means never expire if (timeout <= 0) { - timeout = DEFAULT_NOTIFICATION_TIMEOUT; // default to 3 seconds + timeout = DEFAULT_NOTIFICATION_TIMEOUT; + } + + if (timeout == 0) { + continue; } Glib::signal_timeout().connect([notification]() { diff --git a/src/services/tray.cpp b/src/services/tray.cpp index 95b8509..9355b4d 100644 --- a/src/services/tray.cpp +++ b/src/services/tray.cpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include #include #include @@ -143,9 +143,9 @@ void on_simple_call_finished(GObject *source, GAsyncResult *res, const bool isUnknownMethod = (error->domain == G_DBUS_ERROR && error->code == G_DBUS_ERROR_UNKNOWN_METHOD); if (!(data && data->ignoreUnknownMethod && isUnknownMethod)) { - std::cerr << "[TrayService] " - << (data ? data->debugLabel : std::string("D-Bus call")) - << " failed: " << error->message << std::endl; + spdlog::error("[TrayService] {} failed: {}", + (data ? data->debugLabel : std::string("D-Bus call")), + error->message); } g_error_free(error); } @@ -288,8 +288,9 @@ void TrayService::start() { nodeInfo = Gio::DBus::NodeInfo::create_for_xml(kWatcherIntrospection); } catch (const Glib::Error &err) { - std::cerr << "[TrayService] Failed to parse introspection data: " - << err.what() << std::endl; + spdlog::error( + "[TrayService] Failed to parse introspection data: {}", + err.what()); return; } } @@ -577,9 +578,9 @@ bool TrayService::activate_menu_item(const std::string &id, int itemId, const guint32 nowMs = static_cast(g_get_real_time() / 1000); const guint32 ts = timestampMs ? timestampMs : nowMs; - std::cerr << "[TrayService] MenuEvent id=" << id << " item=" << itemId - << " x=" << x << " y=" << y << " button=" << button - << " tsMs=" << ts << std::endl; + spdlog::debug( + "[TrayService] MenuEvent id={} item={} x={} y={} button={} tsMs={}", + id, itemId, x, y, button, ts); // dbusmenu Event signature: (i s v u) // Some handlers (e.g., media players) look for both "timestamp" and @@ -637,8 +638,7 @@ void TrayService::on_bus_acquired( auto interface_info = nodeInfo->lookup_interface(kWatcherInterface); if (!interface_info) { - std::cerr << "[TrayService] Missing interface info for watcher." - << std::endl; + spdlog::error("[TrayService] Missing interface info for watcher."); return; } @@ -646,8 +646,9 @@ void TrayService::on_bus_acquired( registrationId = connection->register_object(kWatcherObjectPath, interface_info, vtable); } catch (const Glib::Error &err) { - std::cerr << "[TrayService] Failed to register watcher object: " - << err.what() << std::endl; + spdlog::error( + "[TrayService] Failed to register watcher object: {}", + err.what()); registrationId = 0; return; } @@ -747,8 +748,8 @@ void TrayService::register_item(const Glib::ustring &sender, ParsedService parsed = parse_service_identifier(sender, service); if (parsed.busName.empty() || parsed.objectPath.empty()) { - std::cerr << "[TrayService] Invalid service registration: " << service - << std::endl; + spdlog::warn("[TrayService] Invalid service registration: {}", + service); return; } @@ -845,8 +846,9 @@ void TrayService::on_refresh_finished_static(GObject *source, GAsyncResult *res, g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error); if (!reply) { if (error) { - std::cerr << "[TrayService] Failed to query properties for " - << data->id << ": " << error->message << std::endl; + spdlog::error( + "[TrayService] Failed to query properties for {}: {}", + data->id, error->message); g_error_free(error); } @@ -1240,8 +1242,8 @@ Glib::RefPtr TrayService::parse_icon_pixmap(GVariant *variant) { try { return Gdk::Texture::create_for_pixbuf(pixbuf); } catch (const Glib::Error &err) { - std::cerr << "[TrayService] Failed to create texture: " << err.what() - << std::endl; + spdlog::error("[TrayService] Failed to create texture: {}", + err.what()); return {}; } } diff --git a/src/widgets/controlCenter/mediaControl.cpp b/src/widgets/controlCenter/mediaControl.cpp index 1822c5e..9246549 100644 --- a/src/widgets/controlCenter/mediaControl.cpp +++ b/src/widgets/controlCenter/mediaControl.cpp @@ -1,5 +1,6 @@ #include "widgets/controlCenter/mediaControl.hpp" +#include "helpers/string.hpp" #include "services/textureCache.hpp" MediaControlWidget::MediaControlWidget() @@ -62,7 +63,7 @@ MediaControlWidget::MediaControlWidget() double fraction = this->seekBar.get_value() / 100.0; int64_t new_position_us = static_cast(fraction * static_cast(this->totalLengthUs)); - this->mprisController->emit_seeked(new_position_us); // in ms + this->mprisController->emit_seeked(new_position_us - this->currentPositionUs); // in us this->resetSeekTimer(new_position_us); }); @@ -100,6 +101,16 @@ MediaControlWidget::MediaControlWidget() this->mprisController->signal_mpris_updated().connect( sigc::mem_fun(*this, &MediaControlWidget::onSpotifyMprisUpdated)); + this->mprisController->signal_playback_status_changed().connect( + [this](MprisController::PlaybackStatus status) { + this->onRunningStateChanged(status); + }); + + this->mprisController->signal_playback_position_changed().connect( + [this](int64_t position_us) { + this->setCurrentPosition(position_us); + }); + this->artistLabel.set_text("Artist Name"); this->artistLabel.add_css_class("control-center-spotify-artist-label"); this->titleLabel.set_text("Song Title"); @@ -109,7 +120,11 @@ MediaControlWidget::MediaControlWidget() } void MediaControlWidget::onSpotifyMprisUpdated(const MprisPlayer2Message &message) { - this->artistLabel.set_text(message.artist); + std::string artistText = "Unknown Artist"; + if (!message.artist.empty()) { + artistText = StringHelper::trimToSize(message.artist[0], 30); + } + this->artistLabel.set_text(artistText); this->titleLabel.set_text(message.title); if (auto texture = TextureCacheService::getInstance()->getTexture(message.artwork_url)) { @@ -164,4 +179,39 @@ bool MediaControlWidget::onSeekTick() { } setCurrentPosition(nextPosition); return true; +} + +void MediaControlWidget::onRunningStateChanged(MprisController::PlaybackStatus status) { + switch (status) { + case MprisController::PlaybackStatus::Playing: + this->onPlay(); + break; + case MprisController::PlaybackStatus::Paused: + this->onPause(); + break; + case MprisController::PlaybackStatus::Stopped: + this->onStop(); + break; + } +} + +void MediaControlWidget::onPlay() { + this->playPauseButton.set_label("\u23F8"); // Pause symbol + // strart seek timer if not already running + this->resetSeekTimer(currentPositionUs); +} + +void MediaControlWidget::onPause() { + this->playPauseButton.set_label("\u23EF"); // Play symbol + if (seekTimerConnection.connected()) { + seekTimerConnection.disconnect(); + } +} + +void MediaControlWidget::onStop() { + this->playPauseButton.set_label("\u23EF"); // Play symbol + if (seekTimerConnection.connected()) { + seekTimerConnection.disconnect(); + } + this->setCurrentPosition(0); } \ No newline at end of file diff --git a/src/widgets/notification/baseNotification.cpp b/src/widgets/notification/baseNotification.cpp index 4e5960a..a0db67b 100644 --- a/src/widgets/notification/baseNotification.cpp +++ b/src/widgets/notification/baseNotification.cpp @@ -11,7 +11,7 @@ BaseNotification::BaseNotification(std::shared_ptr monitor) { ensure_notification_css_loaded(); - set_default_size(300, 100); + set_default_size(350, -1); gtk_layer_init_for_window(gobj()); gtk_layer_set_monitor(gobj(), monitor->gobj()); gtk_layer_set_layer(gobj(), GTK_LAYER_SHELL_LAYER_OVERLAY); @@ -20,6 +20,9 @@ BaseNotification::BaseNotification(std::shared_ptr monitor) { gtk_layer_set_margin(gobj(), GTK_LAYER_SHELL_EDGE_TOP, 2); gtk_layer_set_margin(gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, 2); add_css_class("notification-popup"); + + this->set_hexpand(false); + this->set_vexpand(false); } void BaseNotification::ensure_notification_css_loaded() { diff --git a/src/widgets/notification/notification.cpp b/src/widgets/notification/notificationWindow.cpp similarity index 56% rename from src/widgets/notification/notification.cpp rename to src/widgets/notification/notificationWindow.cpp index 953865f..72c3b1b 100644 --- a/src/widgets/notification/notification.cpp +++ b/src/widgets/notification/notificationWindow.cpp @@ -1,25 +1,51 @@ -#include "widgets/notification/notification.hpp" +#include "widgets/notification/notificationWindow.hpp" +#include "helpers/string.hpp" #include "gtkmm/box.h" #include "gtkmm/button.h" +#include "gtkmm/image.h" #include "gtkmm/label.h" NotificationWindow::NotificationWindow(std::shared_ptr monitor, NotifyMessage notify) : BaseNotification(monitor) { set_title(notify.summary); + if (notify.imageData) { + auto img = Gtk::make_managed(*(notify.imageData)); + img->set_pixel_size(64); + img->set_halign(Gtk::Align::CENTER); + img->set_valign(Gtk::Align::CENTER); + img->add_css_class("notification-image"); + set_child(*img); + } + // Main vertical box auto vbox = Gtk::make_managed(Gtk::Orientation::VERTICAL, 8); + switch (notify.urgency) { + case NotificationUrgency::CRITICAL: + add_css_class("notification-critical"); + break; + case NotificationUrgency::NORMAL: + add_css_class("notification-normal"); + break; + case NotificationUrgency::LOW: + add_css_class("notification-low"); + break; + default: + break; + } + // Summary label - auto summary_label = Gtk::make_managed("" + notify.summary + ""); + auto summary_label = Gtk::make_managed("" + StringHelper::trimToSize(notify.summary, 20) + ""); summary_label->set_use_markup(true); summary_label->set_halign(Gtk::Align::START); + summary_label->set_wrap(true); vbox->append(*summary_label); - // Body label - auto body_label = Gtk::make_managed(notify.body); + auto body_label = Gtk::make_managed(StringHelper::trimToSize(notify.body, 100)); body_label->set_use_markup(true); body_label->set_halign(Gtk::Align::START); + body_label->set_wrap(true); vbox->append(*body_label); // If actions exist, add buttons diff --git a/src/widgets/notification/spotifyNotification.cpp b/src/widgets/notification/spotifyNotification.cpp index dc97704..e33afb4 100644 --- a/src/widgets/notification/spotifyNotification.cpp +++ b/src/widgets/notification/spotifyNotification.cpp @@ -1,5 +1,6 @@ #include "widgets/notification/spotifyNotification.hpp" +#include "helpers/string.hpp" #include "services/textureCache.hpp" #include "gtkmm/box.h" @@ -30,7 +31,8 @@ SpotifyNotification::SpotifyNotification(std::shared_ptr monitor, title_label->set_halign(Gtk::Align::CENTER); title_label->set_ellipsize(Pango::EllipsizeMode::END); - auto artistLabel = Gtk::make_managed(mpris.artist); + auto artistLabel = Gtk::make_managed(); + artistLabel->set_text(StringHelper::trimToSize(mpris.artist[0], 30)); artistLabel->set_hexpand(true); artistLabel->set_halign(Gtk::Align::CENTER); diff --git a/src/widgets/tray.cpp b/src/widgets/tray.cpp index e1b0da9..eb45df5 100644 --- a/src/widgets/tray.cpp +++ b/src/widgets/tray.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include "components/base/button.hpp" namespace { @@ -168,11 +168,13 @@ void log_menu_tree(const std::vector &nodes, if (!node.visible) { continue; } - std::cerr << "[TrayIconWidget] menu node id=" << node.id - << " label='" << node.label << "' enabled=" - << (node.enabled ? "1" : "0") << " sep=" - << (node.separator ? "1" : "0") << " depth=" << depth - << std::endl; + spdlog::debug( + "[TrayIconWidget] menu node id={} label='{}' enabled={} sep={} depth={}", + node.id, + node.label, + node.enabled ? 1 : 0, + node.separator ? 1 : 0, + depth); if (!node.children.empty()) { log_menu_tree(node.children, depth + 1); } @@ -299,8 +301,8 @@ void TrayIconWidget::on_primary_released(int /*n_press*/, double x, double y) { } } - std::cerr << "[TrayIconWidget] Activate primary id=" << id << " x=" - << sendX << " y=" << sendY << std::endl; + spdlog::debug("[TrayIconWidget] Activate primary id={} x={} y={}", id, sendX, + sendY); service.activate(id, sendX, sendY); } @@ -317,8 +319,9 @@ void TrayIconWidget::on_middle_released(int /*n_press*/, double x, double y) { } } - std::cerr << "[TrayIconWidget] SecondaryActivate (middle) id=" << id - << " x=" << sendX << " y=" << sendY << std::endl; + spdlog::debug( + "[TrayIconWidget] SecondaryActivate (middle) id={} x={} y={}", id, + sendX, sendY); service.secondaryActivate(id, sendX, sendY); } @@ -329,8 +332,9 @@ void TrayIconWidget::on_secondary_released(int /*n_press*/, double x, // would crash without a mapped surface. GtkWidget *selfWidget = GTK_WIDGET(gobj()); if (!gtk_widget_get_mapped(selfWidget) || !has_popup_surface(selfWidget)) { - std::cerr << "[TrayIconWidget] Secondary fallback ContextMenu (no surface) id=" - << id << std::endl; + spdlog::debug( + "[TrayIconWidget] Secondary fallback ContextMenu (no surface) id={}", + id); service.contextMenu(id, -1, -1); return; } @@ -341,8 +345,7 @@ void TrayIconWidget::on_secondary_released(int /*n_press*/, double x, // Use dbusmenu popover when available and we have a mapped surface; else // fall back to the item's ContextMenu. if (hasRemoteMenu && has_popup_surface(selfWidget)) { - std::cerr << "[TrayIconWidget] Requesting dbusmenu for id=" << id - << std::endl; + spdlog::debug("[TrayIconWidget] Requesting dbusmenu for id={}", id); menuPopupPending = true; if (menuRequestInFlight) { return; @@ -367,12 +370,12 @@ void TrayIconWidget::on_secondary_released(int /*n_press*/, double x, (void)try_get_global_pointer_coords(GTK_WIDGET(gobj()), sendX, sendY); } if (is_wayland_display(GTK_WIDGET(gobj()))) { - std::cerr << "[TrayIconWidget] ContextMenu wayland id=" << id - << " x=-1 y=-1" << std::endl; + spdlog::debug( + "[TrayIconWidget] ContextMenu wayland id={} x=-1 y=-1", id); service.contextMenu(id, -1, -1); } else { - std::cerr << "[TrayIconWidget] ContextMenu id=" << id << " x=" << sendX - << " y=" << sendY << std::endl; + spdlog::debug("[TrayIconWidget] ContextMenu id={} x={} y={}", id, + sendX, sendY); service.contextMenu(id, sendX, sendY); } } @@ -518,9 +521,8 @@ void TrayIconWidget::on_menu_action(const Glib::VariantBase & /*parameter*/, int32_t sendY = -1; (void)try_get_pending_coords(sendX, sendY); - std::cerr << "[TrayIconWidget] Menu action id=" << this->id - << " item=" << itemId << " x=" << sendX << " y=" << sendY - << std::endl; + spdlog::debug("[TrayIconWidget] Menu action id={} item={} x={} y={}", + this->id, itemId, sendX, sendY); const uint32_t nowMs = static_cast(g_get_monotonic_time() / 1000); // Use button 1 for menu activation events; some dbusmenu handlers ignore diff --git a/src/widgets/volumeWidget.cpp b/src/widgets/volumeWidget.cpp index e8c3bf1..bddac16 100644 --- a/src/widgets/volumeWidget.cpp +++ b/src/widgets/volumeWidget.cpp @@ -1,7 +1,7 @@ #include "widgets/volumeWidget.hpp" #include -#include +#include #include #include @@ -26,8 +26,7 @@ VolumeWidget::VolumeWidget() : Gtk::Box(Gtk::Orientation::HORIZONTAL) { (void)CommandHelper::exec( "wpctl set-mute @DEFAULT_SINK@ toggle"); } catch (const std::exception &ex) { - std::cerr << "[VolumeWidget] failed to toggle mute: " << ex.what() - << std::endl; + spdlog::error("[VolumeWidget] failed to toggle mute: {}", ex.what()); } this->update(); }); @@ -77,8 +76,7 @@ void VolumeWidget::update() { } } } catch (const std::exception &ex) { - std::cerr << "[VolumeWidget] failed to read volume: " << ex.what() - << std::endl; + spdlog::error("[VolumeWidget] failed to read volume: {}", ex.what()); label.set_text("N/A"); } }