From da9f167747ab3aacbb37d18f7fa8c933d71c9c69 Mon Sep 17 00:00:00 2001 From: Arif Hasanic Date: Mon, 2 Feb 2026 12:23:44 +0100 Subject: [PATCH] add move window to hyprland service --- CMakeLists.txt | 3 +- include/bar/bar.hpp | 2 +- include/services/dbus/messages.hpp | 2 +- include/services/dbus/mpris.hpp | 3 + include/services/hyprland.hpp | 3 + include/widgets/controlCenter.hpp | 46 -------- .../widgets/controlCenter/controlCenter.hpp | 15 +++ .../widgets/controlCenter/mediaControl.hpp | 52 +++++++++ src/services/dbus/mpris.cpp | 56 +++++++-- src/services/hyprland.cpp | 57 +++++++--- src/widgets/controlCenter/controlCenter.cpp | 20 ++++ .../mediaControl.cpp} | 107 +++++++++++++----- 12 files changed, 264 insertions(+), 102 deletions(-) delete mode 100644 include/widgets/controlCenter.hpp create mode 100644 include/widgets/controlCenter/controlCenter.hpp create mode 100644 include/widgets/controlCenter/mediaControl.hpp create mode 100644 src/widgets/controlCenter/controlCenter.cpp rename src/widgets/{controlCenter.cpp => controlCenter/mediaControl.cpp} (57%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c2aab6..e511760 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,7 +54,8 @@ target_sources(bar_lib src/services/dbus/mpris.cpp src/widgets/tray.cpp - src/widgets/controlCenter.cpp + src/widgets/controlCenter/controlCenter.cpp + src/widgets/controlCenter/mediaControl.cpp src/components/popover.cpp src/components/workspaceIndicator.cpp diff --git a/include/bar/bar.hpp b/include/bar/bar.hpp index a739e20..f8ea38f 100644 --- a/include/bar/bar.hpp +++ b/include/bar/bar.hpp @@ -9,7 +9,7 @@ #include "widgets/tray.hpp" #include "widgets/volumeWidget.hpp" #include "widgets/webWidget.hpp" -#include "widgets/controlCenter.hpp" +#include "widgets/controlCenter/controlCenter.hpp" class Bar : public Gtk::Window { public: diff --git a/include/services/dbus/messages.hpp b/include/services/dbus/messages.hpp index 8220ed6..b4a5e8c 100644 --- a/include/services/dbus/messages.hpp +++ b/include/services/dbus/messages.hpp @@ -14,7 +14,7 @@ struct MprisPlayer2Message { std::string title; std::string artist; std::string artwork_url; - uint32_t length_ms; + int64_t length_ms; std::function play_pause; std::function next; diff --git a/include/services/dbus/mpris.hpp b/include/services/dbus/mpris.hpp index 487e211..736df94 100644 --- a/include/services/dbus/mpris.hpp +++ b/include/services/dbus/mpris.hpp @@ -12,12 +12,15 @@ class MprisController { void toggle_play(); void next_song(); void previous_song(); + void emit_seeked(int64_t position_us); sigc::signal &signal_mpris_updated(); private: MprisController(); + bool playerRunning = false; + Glib::RefPtr m_connection; Glib::RefPtr m_proxy; sigc::signal mprisUpdatedSignal; diff --git a/include/services/hyprland.hpp b/include/services/hyprland.hpp index 64072c5..5242bf1 100644 --- a/include/services/hyprland.hpp +++ b/include/services/hyprland.hpp @@ -67,6 +67,7 @@ class HyprlandService { ACTIVE_WINDOW, OPEN_WINDOW, CLOSE_WINDOW, + MOVE_WINDOW, URGENT, FOCUSED_MONITOR, @@ -78,6 +79,7 @@ class HyprlandService { {"activewindowv2", ACTIVE_WINDOW}, {"openwindow", OPEN_WINDOW}, {"closewindow", CLOSE_WINDOW}, + {"movewindowv2", MOVE_WINDOW}, {"urgent", URGENT}, {"focusedmon", FOCUSED_MONITOR}, {"monitorremoved", MONITOR_REMOVED}, @@ -86,6 +88,7 @@ class HyprlandService { void onFocusedMonitorChanged(std::string monitorData); void onOpenWindow(std::string windowData); void onCloseWindow(std::string windowData); + void onMoveWindow(std::string windowData); void onUrgent(std::string windowAddress); void onActiveWindowChanged(std::string windowAddress); void onMonitorRemoved(std::string monitorName); diff --git a/include/widgets/controlCenter.hpp b/include/widgets/controlCenter.hpp deleted file mode 100644 index 4d7a3f0..0000000 --- a/include/widgets/controlCenter.hpp +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include -#include "components/popover.hpp" -#include "gtkmm/box.h" -#include "gtkmm/label.h" -#include "gtkmm/overlay.h" -#include "gtkmm/picture.h" -#include "gtkmm/scale.h" -#include "gtkmm/scrolledwindow.h" -#include "services/dbus/mpris.hpp" - -class ControlCenter : public Popover { - public: - ControlCenter(std::string icon, std::string name); - - private: - std::shared_ptr mprisController = MprisController::getInstance(); - - Gtk::Box container; - Gtk::Box spotifyContainer; - - // image as background, artist, title - Gtk::Overlay topContainer; - Gtk::Picture backgroundImage; - Gtk::Box infoContainer; - Gtk::Label artistLabel; - Gtk::Label titleLabel; - - // - Gtk::Box seekBarContainer; - Gtk::Label currentTimeLabel; - Gtk::Scale seekBar; - Gtk::Label totalTimeLabel; - - // playback controls - Gtk::Box bottomContainer; - Gtk::Button previousButton; - Gtk::Button playPauseButton; - Gtk::Button nextButton; - - Gtk::ScrolledWindow imageWrapper; - - - void onSpotifyMprisUpdated(const MprisPlayer2Message &message); -}; \ No newline at end of file diff --git a/include/widgets/controlCenter/controlCenter.hpp b/include/widgets/controlCenter/controlCenter.hpp new file mode 100644 index 0000000..54261f7 --- /dev/null +++ b/include/widgets/controlCenter/controlCenter.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "components/popover.hpp" +#include "gtkmm/box.h" +#include "widgets/controlCenter/mediaControl.hpp" + +class ControlCenter : public Popover { + public: + ControlCenter(std::string icon, std::string name); + + private: + Gtk::Box container; + MediaControlWidget mediaControlWidget; + +}; \ No newline at end of file diff --git a/include/widgets/controlCenter/mediaControl.hpp b/include/widgets/controlCenter/mediaControl.hpp new file mode 100644 index 0000000..338b46e --- /dev/null +++ b/include/widgets/controlCenter/mediaControl.hpp @@ -0,0 +1,52 @@ +#pragma once +#include "gtkmm/box.h" +#include "gtkmm/button.h" +#include "gtkmm/label.h" +#include "gtkmm/overlay.h" +#include "gtkmm/picture.h" +#include "gtkmm/scale.h" +#include "gtkmm/scrolledwindow.h" + +#include "services/dbus/mpris.hpp" + +class MediaControlWidget : public Gtk::Box { + public: + MediaControlWidget(); + + private: + std::shared_ptr mprisController = MprisController::getInstance(); + + int64_t currentPositionUs = 0; + int64_t totalLengthUs = 0; + sigc::connection seekTimerConnection; + + void setCurrentPosition(int64_t position_us); + void setTotalLength(int64_t length_us); + void resetSeekTimer(int64_t start_position_us); + bool onSeekTick(); + + Gtk::Box spotifyContainer; + + // image as background, artist, title + Gtk::Overlay topContainer; + Gtk::Picture backgroundImage; + Gtk::Box infoContainer; + Gtk::Label artistLabel; + Gtk::Label titleLabel; + + // + Gtk::Box seekBarContainer; + Gtk::Label currentTimeLabel; + Gtk::Scale seekBar; + Gtk::Label totalTimeLabel; + + // playback controls + Gtk::Box bottomContainer; + Gtk::Button previousButton; + Gtk::Button playPauseButton; + Gtk::Button nextButton; + + Gtk::ScrolledWindow imageWrapper; + + void onSpotifyMprisUpdated(const MprisPlayer2Message &message); +}; \ No newline at end of file diff --git a/src/services/dbus/mpris.cpp b/src/services/dbus/mpris.cpp index 2bbfd7d..2e547ed 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 "helpers/string.hpp" @@ -13,7 +14,6 @@ std::shared_ptr MprisController::getInstance() { } MprisController::MprisController() { - // 1. Connect to the Session Bus Gio::DBus::Connection::get( Gio::DBus::BusType::SESSION, sigc::mem_fun(*this, &MprisController::on_bus_connected)); @@ -44,7 +44,6 @@ void MprisController::on_bus_connected(const Glib::RefPtr &res if (m_proxy) { std::cout << "Connected to: " << player_bus_name << std::endl; - // uncomment if launch notification on start signalNotification(); m_proxy->signal_properties_changed().connect( @@ -69,6 +68,11 @@ void MprisController::signalNotification() { return; } + if (!metadata_var.is_of_type(Glib::VariantType("a{sv}"))) { + std::cout << "Unexpected metadata type." << std::endl; + return; + } + using MetadataMap = std::map; MetadataMap metadata_map; @@ -78,26 +82,46 @@ void MprisController::signalNotification() { metadata_map = variant_dict.get(); std::string title, artist, artwork_url; - if (metadata_map.count("xesam:title")) { - auto title_var = Glib::VariantBase::cast_dynamic>(metadata_map["xesam:title"]); - title = title_var.get(); + const auto &title_base = metadata_map["xesam:title"]; + if (title_base.is_of_type(Glib::VariantType("s"))) { + auto title_var = Glib::VariantBase::cast_dynamic>(title_base); + title = title_var.get(); + } } if (metadata_map.count("xesam:artist")) { - auto artist_var = metadata_map["xesam:artist"]; + const auto &artist_var = metadata_map["xesam:artist"]; 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 } + } else if (artist_var.is_of_type(Glib::VariantType("s"))) { + auto artist_str = Glib::VariantBase::cast_dynamic>(artist_var); + artist = artist_str.get(); } } if (metadata_map.count("mpris:artUrl")) { - auto art_var = Glib::VariantBase::cast_dynamic>(metadata_map["mpris:artUrl"]); - artwork_url = art_var.get(); + const auto &art_base = metadata_map["mpris:artUrl"]; + if (art_base.is_of_type(Glib::VariantType("s"))) { + auto art_var = Glib::VariantBase::cast_dynamic>(art_base); + artwork_url = art_var.get(); + } + } + + int64_t length_us = 0; + if (metadata_map.count("mpris:length")) { + 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()); + } 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()); + } } MprisPlayer2Message mpris; @@ -107,6 +131,7 @@ void MprisController::signalNotification() { mpris.play_pause = [this]() { this->toggle_play(); }; mpris.next = [this]() { this->next_song(); }; mpris.previous = [this]() { this->previous_song(); }; + mpris.length_ms = length_us; mprisUpdatedSignal.emit(mpris); } @@ -136,3 +161,18 @@ void MprisController::next_song() { m_proxy->call("Next"); } } + +void MprisController::emit_seeked(int64_t position_us) { + if (!m_proxy) { + return; + } + + try { + Glib::VariantContainerBase params = Glib::VariantContainerBase::create_tuple( + Glib::Variant::create(position_us)); + + m_proxy->call("Seek", params); + } catch (const Glib::Error &ex) { + std::cerr << "Error seeking: " << ex.what() << std::endl; + } +} diff --git a/src/services/hyprland.cpp b/src/services/hyprland.cpp index 09b9ad0..de5dd9e 100644 --- a/src/services/hyprland.cpp +++ b/src/services/hyprland.cpp @@ -29,24 +29,24 @@ void HyprlandService::init() { auto monitorPtr = std::make_shared(); std::string monitorName = item["name"].get(); - int monitorId = item["id"].get(); + int monitorId = item["id"].get(); - monitorPtr->id = monitorId; - monitorPtr->name = monitorName; - monitorPtr->activeWorkspaceId = item["activeWorkspace"]["id"].get(); - monitorPtr->focused = item["focused"].get(); + monitorPtr->id = monitorId; + monitorPtr->name = monitorName; + monitorPtr->activeWorkspaceId = item["activeWorkspace"]["id"].get(); + monitorPtr->focused = item["focused"].get(); this->monitors[monitorPtr->name] = monitorPtr; for (int i = 1; i <= NUM_WORKSPACES; i++) { std::shared_ptr state = std::make_shared(); int workspaceId = i + (NUM_WORKSPACES * monitorId); - state->id = workspaceId; + 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; + 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; @@ -76,7 +76,7 @@ void HyprlandService::init() { auto workspacePtr = workspaces[workspace["id"].get()]; auto state = workspacePtr->state; - state->id = workspace["id"].get(); + state->id = workspace["id"].get(); state->monitorName = workspace["monitor"].get(); refreshIndicator(workspacePtr); @@ -106,9 +106,9 @@ 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); + GSource *source = g_unix_fd_source_new(socketFd, socket_conditions); - auto onSocketEvent = [](gint fd, GIOCondition , gpointer user_data) -> gboolean { + auto onSocketEvent = [](gint fd, GIOCondition, gpointer user_data) -> gboolean { HyprlandService *self = static_cast(user_data); auto messages = SocketHelper::parseSocketMessage(fd, ">>"); @@ -119,7 +119,7 @@ void HyprlandService::bindHyprlandSocket() { return G_SOURCE_CONTINUE; }; - g_source_set_callback(source, reinterpret_cast(reinterpret_cast(+onSocketEvent)), this, nullptr); + g_source_set_callback(source, reinterpret_cast(reinterpret_cast(+onSocketEvent)), this, nullptr); g_source_attach(source, g_main_context_default()); g_source_unref(source); } @@ -173,11 +173,34 @@ void HyprlandService::onMonitorRemoved(std::string monitorName) { this->monitors.erase(monitorName); } +void HyprlandService::onMoveWindow(std::string windowData) { + auto parts = StringHelper::split(windowData, ','); + std::string addr = "0x" + parts[0]; + int newWorkspaceId = std::stoi(parts[1]); + + if (this->clients.find(addr) == this->clients.end()) { + std::cerr << "[Hyprland] onMoveWindow: Client not found: " << addr << std::endl; + return; + } + + auto clientPtr = this->clients[addr]; + int oldWorkspaceId = clientPtr->workspaceId; + + auto oldWorkspacePtr = workspaces[oldWorkspaceId]; + oldWorkspacePtr->state->clients.erase(addr); + refreshIndicator(oldWorkspacePtr); + + clientPtr->workspaceId = newWorkspaceId; + + auto newWorkspacePtr = workspaces[newWorkspaceId]; + newWorkspacePtr->state->clients[addr] = clientPtr; + 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]; @@ -250,6 +273,10 @@ void HyprlandService::handleSocketMessage(SocketHelper::SocketMessage message) { this->onMonitorRemoved(eventData); break; } + case MOVE_WINDOW: { + this->onMoveWindow(eventData); + break; + } } } void HyprlandService::onUrgent(std::string windowAddress) { diff --git a/src/widgets/controlCenter/controlCenter.cpp b/src/widgets/controlCenter/controlCenter.cpp new file mode 100644 index 0000000..a01279a --- /dev/null +++ b/src/widgets/controlCenter/controlCenter.cpp @@ -0,0 +1,20 @@ +#include "widgets/controlCenter/controlCenter.hpp" + + +ControlCenter::ControlCenter(std::string icon, std::string name) + : Popover(icon, name) { + this->popover->add_css_class("control-center-popover"); + this->container.set_orientation(Gtk::Orientation::VERTICAL); + this->container.set_spacing(0); + this->container.set_margin_top(0); + this->container.set_margin_bottom(0); + this->container.set_margin_start(0); + this->container.set_margin_end(0); + + set_popover_child(this->container); + + + this->container.append(this->mediaControlWidget); + +} + diff --git a/src/widgets/controlCenter.cpp b/src/widgets/controlCenter/mediaControl.cpp similarity index 57% rename from src/widgets/controlCenter.cpp rename to src/widgets/controlCenter/mediaControl.cpp index 60789b3..1822c5e 100644 --- a/src/widgets/controlCenter.cpp +++ b/src/widgets/controlCenter/mediaControl.cpp @@ -1,37 +1,28 @@ -#include "widgets/controlCenter.hpp" +#include "widgets/controlCenter/mediaControl.hpp" + #include "services/textureCache.hpp" -ControlCenter::ControlCenter(std::string icon, std::string name) - : Popover(icon, name) { - this->popover->add_css_class("control-center-popover"); - this->container.set_orientation(Gtk::Orientation::VERTICAL); - this->container.set_spacing(0); - this->container.set_margin_top(0); - this->container.set_margin_bottom(0); - this->container.set_margin_start(0); - this->container.set_margin_end(0); - this->container.append(this->spotifyContainer); +MediaControlWidget::MediaControlWidget() + : Gtk::Box(Gtk::Orientation::VERTICAL) { - set_popover_child(this->container); + this->set_orientation(Gtk::Orientation::VERTICAL); + this->set_size_request(200, 240); + this->set_hexpand(false); + this->set_vexpand(false); + this->add_css_class("control-center-spotify-container"); - this->spotifyContainer.set_orientation(Gtk::Orientation::VERTICAL); - this->spotifyContainer.set_size_request(200, 240); - this->spotifyContainer.set_hexpand(false); // Important: Don't let the main box expand freely - this->spotifyContainer.set_vexpand(false); - this->spotifyContainer.add_css_class("control-center-spotify-container"); - - this->spotifyContainer.append(this->topContainer); - this->spotifyContainer.append(this->seekBarContainer); - this->spotifyContainer.append(this->bottomContainer); + this->append(this->topContainer); + this->append(this->seekBarContainer); + this->append(this->bottomContainer); this->backgroundImage.set_content_fit(Gtk::ContentFit::COVER); this->backgroundImage.set_can_shrink(true); this->imageWrapper.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::NEVER); this->imageWrapper.set_child(this->backgroundImage); - - this->topContainer.set_child(this->imageWrapper); - + + this->topContainer.set_child(this->imageWrapper); + this->topContainer.set_size_request(200, 100); this->topContainer.set_vexpand(false); this->topContainer.set_hexpand(true); @@ -44,8 +35,7 @@ ControlCenter::ControlCenter(std::string icon, std::string name) this->topContainer.add_overlay(this->infoContainer); this->artistLabel.set_halign(Gtk::Align::START); - this->titleLabel.set_halign(Gtk::Align::START); - + this->titleLabel.set_halign(Gtk::Align::START); this->seekBarContainer.set_orientation(Gtk::Orientation::HORIZONTAL); this->seekBarContainer.set_vexpand(false); @@ -68,6 +58,13 @@ ControlCenter::ControlCenter(std::string icon, std::string name) this->seekBar.set_halign(Gtk::Align::CENTER); this->seekBar.add_css_class("control-center-seek-bar"); + this->seekBar.signal_value_changed().connect([this]() { + 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->resetSeekTimer(new_position_us); + }); this->bottomContainer.set_orientation(Gtk::Orientation::HORIZONTAL); this->bottomContainer.set_vexpand(false); @@ -101,20 +98,70 @@ ControlCenter::ControlCenter(std::string icon, std::string name) }); this->mprisController->signal_mpris_updated().connect( - sigc::mem_fun(*this, &ControlCenter::onSpotifyMprisUpdated) - ); + sigc::mem_fun(*this, &MediaControlWidget::onSpotifyMprisUpdated)); this->artistLabel.set_text("Artist Name"); this->artistLabel.add_css_class("control-center-spotify-artist-label"); this->titleLabel.set_text("Song Title"); this->titleLabel.add_css_class("control-center-spotify-title-label"); + + this->resetSeekTimer(0); } -void ControlCenter::onSpotifyMprisUpdated(const MprisPlayer2Message &message) { +void MediaControlWidget::onSpotifyMprisUpdated(const MprisPlayer2Message &message) { this->artistLabel.set_text(message.artist); this->titleLabel.set_text(message.title); - + if (auto texture = TextureCacheService::getInstance()->getTexture(message.artwork_url)) { this->backgroundImage.set_paintable(texture); } + + this->setTotalLength(message.length_ms); + this->setCurrentPosition(0); + this->resetSeekTimer(0); +} + +void MediaControlWidget::setCurrentPosition(int64_t position_us) { + this->currentPositionUs = position_us; + int64_t seconds = (position_us / 1000000) % 60; + int64_t minutes = (position_us / (1000000 * 60)) % 60; + this->currentTimeLabel.set_text( + std::to_string(minutes) + ":" + (seconds < 10 ? "0" : "") + std::to_string(seconds)); + if (totalLengthUs > 0) { + double fraction = static_cast(currentPositionUs) / static_cast(totalLengthUs); + this->seekBar.set_value(fraction * 100); + } +} + +void MediaControlWidget::setTotalLength(int64_t length_us) { + this->totalLengthUs = length_us; + int64_t seconds = (length_us / 1000000) % 60; + int64_t minutes = (length_us / (1000000 * 60)) % 60; + this->totalTimeLabel.set_text( + std::to_string(minutes) + ":" + (seconds < 10 ? "0" : "") + std::to_string(seconds)); +} + +void MediaControlWidget::resetSeekTimer(int64_t start_position_us) { + if (seekTimerConnection.connected()) { + seekTimerConnection.disconnect(); + } + + setCurrentPosition(start_position_us); + + seekTimerConnection = Glib::signal_timeout().connect( + sigc::mem_fun(*this, &MediaControlWidget::onSeekTick), + 1000); +} + +bool MediaControlWidget::onSeekTick() { + if (totalLengthUs <= 0) { + return true; + } + + int64_t nextPosition = currentPositionUs + 1000000; + if (nextPosition > totalLengthUs) { + nextPosition = totalLengthUs; + } + setCurrentPosition(nextPosition); + return true; } \ No newline at end of file