diff --git a/CMakeLists.txt b/CMakeLists.txt index b4963ae..e6c9dc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,8 +57,10 @@ target_sources(bar_lib src/widgets/notification/spotifyNotification.cpp src/widgets/controlCenter/controlCenter.cpp src/widgets/controlCenter/mediaWidget.cpp + src/widgets/controlCenter/timer.cpp src/widgets/volumeWidget.cpp src/widgets/weather.cpp + src/widgets/wallpaperWindow.cpp src/widgets/webWidget.cpp src/widgets/tray.cpp diff --git a/include/app.hpp b/include/app.hpp index d97883b..a98c564 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -6,6 +6,7 @@ #include "connection/dbus/notification.hpp" #include "connection/dbus/tray.hpp" #include "services/hyprland.hpp" +#include "widgets/wallpaperWindow.hpp" #include "glibmm/refptr.h" #include "gtkmm/application.h" @@ -21,6 +22,7 @@ class App { private: Glib::RefPtr app; std::vector> bars; + std::vector> wallpaperWindows; std::shared_ptr notificationService = nullptr; std::shared_ptr mprisController = nullptr; HyprlandService *hyprlandService = nullptr; diff --git a/include/components/timer.hpp b/include/components/timer.hpp new file mode 100644 index 0000000..7aa4d81 --- /dev/null +++ b/include/components/timer.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include +#include + +#include "components/button/iconButton.hpp" +#include "services/timerService.hpp" + +#include "gtkmm/box.h" +#include "gtkmm/label.h" + +namespace { +std::string format_duration(const std::string &digits) { + std::string d = digits; + if (d.size() > 6) { + d = d.substr(d.size() - 6); + } + if (d.empty()) { + return ""; + } + + std::string padded = std::string(6 - d.size(), '0') + d; + + int h = std::stoi(padded.substr(0, 2)); + int m = std::stoi(padded.substr(2, 2)); + int s = std::stoi(padded.substr(4, 2)); + + std::ostringstream out; + if (h > 0) { + out << h << "h " << std::setw(2) << std::setfill('0') << m << "m " + << std::setw(2) << std::setfill('0') << s << "s"; + } else if (m > 0) { + out << m << "m " << std::setw(2) << std::setfill('0') << s << "s"; + } else { + out << s << "s"; + } + + return out.str(); +} +} // namespace +class Timer : public Gtk::Box { + public: + Timer(const std::string &duration, uint64_t timerId) { + uint32_t totalTime = std::stoul(duration); + u_int64_t seconds = totalTime % 100; + u_int64_t minutes = (totalTime / 100) % 100; + u_int64_t hours = totalTime / 10000; + + u_int64_t totalSeconds = hours * 3600 + minutes * 60 + seconds + 1; // +1 to account for the first tick happening after 1 second + + this->timeLeft = totalSeconds; + this->timerLabel = std::make_shared(format_duration(duration)); + timerLabel->add_css_class("control-center-timer-label"); + + auto timerBox = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 5); + timerBox->append(*timerLabel); + + auto cancelButton = Gtk::make_managed(Icon::Type::TOKEN); + cancelButton->set_tooltip_text("Cancel Timer"); + cancelButton->signal_clicked().connect([this, timerId]() { + this->timerService->removeTimer(timerId); + }); + timerBox->append(*cancelButton); + this->append(*timerBox); + } + + void activateTimer() { + std::cout << "Timer activated" << std::endl; + } + + void updateTimeLeft(uint64_t timeValue) { + uint64_t s = timeValue % 100; + uint64_t m = (timeValue / 100) % 100; + uint64_t h = timeValue / 10000; + uint64_t totalSeconds = h * 3600 + m * 60 + s; + this->timeLeft = totalSeconds; + + std::ostringstream out; + if (h > 0) { + out << h << "h " << std::setw(2) << std::setfill('0') << m << "m " + << std::setw(2) << std::setfill('0') << s << "s"; + } else if (m > 0) { + out << m << "m " << std::setw(2) << std::setfill('0') << s << "s"; + } else { + out << s << "s"; + } + + this->timerLabel->set_text(out.str()); + } + + void tickDown() { + if (timeLeft > 0) { + this->updateTimeLeft(this->timeLeft - 1); + } else { + timeLeft = 0; + } + } + + private: + std::shared_ptr timerService = TimerService::getInstance(); + uint64_t timeLeft; + std::shared_ptr timerLabel; +}; \ No newline at end of file diff --git a/include/services/timerService.hpp b/include/services/timerService.hpp new file mode 100644 index 0000000..abdbf1a --- /dev/null +++ b/include/services/timerService.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include + +#include "glibmm/main.h" +#include "sigc++/signal.h" +class TimerService { + struct TimerData { + uint64_t duration; + uint64_t timerId; + }; + + public: + static std::shared_ptr getInstance() { + if (!instance) { + instance = std::make_shared(); + + // add a timer to tick every second + Glib::signal_timeout().connect([]() { + instance->tick(); + return true; // continue calling every second + }, + 1000); + } + return instance; + } + + void addTimer(uint64_t duration) { + this->timerAddedSignal.emit(std::to_string(duration), timerIdCounter); + + this->activeTimers[timerIdCounter] = TimerData{.duration = duration, .timerId = timerIdCounter}; + + timerIdCounter++; + } + + void expireTimer(uint64_t timerId) { + this->timerExpiredSignal.emit(timerId); + } + + void removeTimer(uint64_t timerId) { + this->activeTimers.erase(timerId); + this->timerCancelledSignal.emit(timerId); + } + + sigc::signal &getSignalTimerSet() { + return this->timerAddedSignal; + } + + sigc::signal &getSignalTimerExpired() { + return this->timerExpiredSignal; + } + + sigc::signal &getSignalTimerCancelled() { + return this->timerCancelledSignal; + } + + sigc::signal tickSignal; + + private: + static inline uint64_t timerIdCounter = 0; + sigc::signal timerAddedSignal; + sigc::signal timerCancelledSignal; + sigc::signal timerExpiredSignal; + inline static std::shared_ptr instance = nullptr; + + std::map activeTimers; + + void tick() { + for (auto &[id, timer] : activeTimers) { + + if (timer.duration > 0) { + timer.duration--; + } else if (timer.duration == 0) { + expireTimer(timer.timerId); + timer.duration = -1; // Mark as expired to prevent multiple expirations + } + } + + tickSignal.emit(); + } +}; \ No newline at end of file diff --git a/include/widgets/controlCenter/controlCenter.hpp b/include/widgets/controlCenter/controlCenter.hpp index e9c7435..f463e62 100644 --- a/include/widgets/controlCenter/controlCenter.hpp +++ b/include/widgets/controlCenter/controlCenter.hpp @@ -6,6 +6,7 @@ #include "components/button/tabButton.hpp" #include "components/popover.hpp" #include "widgets/controlCenter/mediaWidget.hpp" +#include "widgets/controlCenter/timer.hpp" #include "widgets/weather.hpp" #include "gtkmm/box.h" @@ -29,6 +30,7 @@ class ControlCenter : public Popover { std::unique_ptr weatherWidget; std::unique_ptr mediaControlWidget; + std::unique_ptr timerWidget; void addPlayerWidget(const std::string &bus_name); void removePlayerWidget(const std::string &bus_name); diff --git a/include/widgets/controlCenter/timer.hpp b/include/widgets/controlCenter/timer.hpp new file mode 100644 index 0000000..7aba82a --- /dev/null +++ b/include/widgets/controlCenter/timer.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include "components/timer.hpp" +#include "services/timerService.hpp" +#include "gtkmm/box.h" + +class TimerWidget : public Gtk::Box { + public: + TimerWidget(); + void addTimer(const std::string &duration, uint64_t timerId); + void removeTimer(uint64_t timerId); + void activateTimer(uint64_t timerId); + + private: + std::shared_ptr timerService = TimerService::getInstance(); + bool updatingText = false; + std::string rawDigits; + + std::map> activeTimers; +}; \ No newline at end of file diff --git a/include/widgets/wallpaperWindow.hpp b/include/widgets/wallpaperWindow.hpp new file mode 100644 index 0000000..14086e6 --- /dev/null +++ b/include/widgets/wallpaperWindow.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +class WallpaperWindow : public Gtk::Window { + public: + WallpaperWindow(GdkMonitor *monitor, const std::string &imagePath); + + private: + Gtk::Picture picture; + + static std::string expand_user_path(const std::string &path); + static Glib::RefPtr load_texture(const std::string &path); +}; diff --git a/resources/bar.css b/resources/bar.css index 3378a1f..26764c1 100644 --- a/resources/bar.css +++ b/resources/bar.css @@ -19,6 +19,12 @@ window { padding: 2px 6px; } +.text-area { + font-family: var(--text-font-mono); + background-color: rgba(25, 25, 25, 0.8); + border-radius: 8px; + padding: 4px 8px; +} .material-icons { font-family: var(--icon-font-material); } diff --git a/src/app.cpp b/src/app.cpp index c1ca47c..17f3af0 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -7,6 +7,7 @@ #include "connection/dbus/notification.hpp" #include "services/notificationController.hpp" #include "services/textureCache.hpp" +#include "widgets/wallpaperWindow.hpp" App::App() { this->app = Gtk::Application::create("org.example.mybar"); diff --git a/src/services/timeSignal.cpp b/src/services/timeSignal.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/include/services/timeSignal.hpp b/src/services/timerService.cpp similarity index 100% rename from include/services/timeSignal.hpp rename to src/services/timerService.cpp diff --git a/src/widgets/controlCenter/controlCenter.cpp b/src/widgets/controlCenter/controlCenter.cpp index b22dcfc..8edc6fa 100644 --- a/src/widgets/controlCenter/controlCenter.cpp +++ b/src/widgets/controlCenter/controlCenter.cpp @@ -39,15 +39,15 @@ ControlCenter::ControlCenter(Icon::Type icon, std::string name) this->contentStack.set_transition_type(Gtk::StackTransitionType::CROSSFADE); this->mediaControlWidget = std::make_unique(); - this->weatherWidget = std::make_unique(); + this->timerWidget = std::make_unique(); this->contentStack.add(*this->mediaControlWidget, "controls", "Controls"); this->contentStack.add(*this->weatherWidget, "info", "Info"); - this->contentStack.add(*Gtk::make_managed("Timer"), "timer", "Timer"); + this->contentStack.add(*this->timerWidget, "timer", "Timer"); this->contentStack.set_visible_child("controls"); - this->setActiveTab("info"); + this->setActiveTab("controls"); this->container.append(this->contentStack); diff --git a/src/widgets/controlCenter/timer.cpp b/src/widgets/controlCenter/timer.cpp new file mode 100644 index 0000000..17a8c3d --- /dev/null +++ b/src/widgets/controlCenter/timer.cpp @@ -0,0 +1,125 @@ +#include "widgets/controlCenter/timer.hpp" + +#include +#include +#include +#include +#include + + +#include "gtkmm/entry.h" +#include "gtkmm/eventcontrollerkey.h" +#include "gtkmm/label.h" + +TimerWidget::TimerWidget() { + set_orientation(Gtk::Orientation::VERTICAL); + set_spacing(6); + + this->timerService->getSignalTimerSet().connect([this](const std::string &duration, uint64_t timerId) { + this->addTimer(duration, timerId); + }); + + this->timerService->getSignalTimerCancelled().connect([this](uint64_t timerId) { + this->removeTimer(timerId); + }); + + this->timerService->getSignalTimerExpired().connect([this](uint64_t timerId) { + this->activateTimer(timerId); + }); + + this->timerService->tickSignal.connect([this]() { + for (auto &[id, timer] : activeTimers) { + timer->tickDown(); + } + }); + + auto label = Gtk::make_managed("Set Timer"); + label->add_css_class("control-center-timer-label"); + + auto entry = Gtk::make_managed(); + entry->set_placeholder_text("0s"); + entry->set_valign(Gtk::Align::CENTER); + entry->set_alignment(0.5); + entry->add_css_class("text-area"); + entry->set_editable(false); + entry->set_focusable(true); + entry->set_position(-1); + + set_focusable(false); + + auto keyController = Gtk::EventControllerKey::create(); + keyController->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); + keyController->signal_key_pressed().connect([this, entry](guint keyval, guint, Gdk::ModifierType) -> bool { + if (updatingText) { + return true; + } + + if (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9) { + rawDigits.push_back(static_cast('0' + (keyval - GDK_KEY_0))); + } else if (keyval >= GDK_KEY_KP_0 && keyval <= GDK_KEY_KP_9) { + rawDigits.push_back(static_cast('0' + (keyval - GDK_KEY_KP_0))); + } else if (keyval == GDK_KEY_BackSpace || keyval == GDK_KEY_Delete || + keyval == GDK_KEY_KP_Delete) { + if (!rawDigits.empty()) { + rawDigits.pop_back(); + } + } else if (keyval == GDK_KEY_Return || keyval == GDK_KEY_KP_Enter || + keyval == GDK_KEY_Tab || keyval == GDK_KEY_ISO_Left_Tab) { + return false; + } else { + return true; + } + + if (rawDigits.size() > 6) { + rawDigits.erase(0, rawDigits.size() - 6); + } + + updatingText = true; + entry->set_text(format_duration(rawDigits)); + entry->set_position(-1); + updatingText = false; + return true; + }, + false); + entry->add_controller(keyController); + + entry->signal_activate().connect([this, entry]() { + if (rawDigits.empty()) { + return; + } + spdlog::info("Timer set for {} seconds", rawDigits); + this->timerService->addTimer(std::stoul(rawDigits)); + rawDigits.clear(); + this->updatingText = true; + entry->set_text(""); + entry->set_position(-1); + this->updatingText = false; + }, + false); + + append(*label); + append(*entry); +} + +void TimerWidget::addTimer(const std::string &duration, uint64_t timerId) { + std::unique_ptr timer = std::make_unique(duration, timerId); + + append(*timer); + + this->activeTimers[timerId] = std::move(timer); +} + +void TimerWidget::removeTimer(uint64_t timerId) { + auto it = this->activeTimers.find(timerId); + if (it != this->activeTimers.end()) { + this->remove(*it->second); + this->activeTimers.erase(it); + } +} + +void TimerWidget::activateTimer(uint64_t timerId) { + auto it = this->activeTimers.find(timerId); + if (it != this->activeTimers.end()) { + it->second->activateTimer(); + } +} \ No newline at end of file diff --git a/src/widgets/wallpaperWindow.cpp b/src/widgets/wallpaperWindow.cpp new file mode 100644 index 0000000..f3b0bba --- /dev/null +++ b/src/widgets/wallpaperWindow.cpp @@ -0,0 +1,89 @@ +#include "widgets/wallpaperWindow.hpp" + +#include +#include + +#include +#include + +#include "giomm/file.h" + +WallpaperWindow::WallpaperWindow(GdkMonitor *monitor, const std::string &imagePath) { + set_name("wallpaper-window"); + set_resizable(false); + set_focusable(false); + + gtk_layer_init_for_window(gobj()); + + if (monitor) { + gtk_layer_set_monitor(gobj(), monitor); + } + + gtk_layer_set_layer(gobj(), GTK_LAYER_SHELL_LAYER_BACKGROUND); + gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_TOP, true); + gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, true); + gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_LEFT, true); + gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, true); + gtk_layer_set_exclusive_zone(gobj(), 0); + gtk_layer_set_keyboard_mode(gobj(), GTK_LAYER_SHELL_KEYBOARD_MODE_NONE); + + picture.set_hexpand(true); + picture.set_vexpand(true); + picture.set_halign(Gtk::Align::FILL); + picture.set_valign(Gtk::Align::FILL); + picture.set_can_shrink(true); + picture.set_content_fit(Gtk::ContentFit::COVER); + + if (monitor) { + GdkRectangle geom{}; + gdk_monitor_get_geometry(monitor, &geom); + set_default_size(geom.width, geom.height); + set_size_request(geom.width, geom.height); + picture.set_size_request(geom.width, geom.height); + } + + auto resolved_path = expand_user_path(imagePath); + if (auto texture = load_texture(resolved_path)) { + picture.set_paintable(texture); + } else { + spdlog::warn("Wallpaper image failed to load: {}", resolved_path); + } + + set_child(picture); +} + +std::string WallpaperWindow::expand_user_path(const std::string &path) { + if (path.rfind("~/", 0) != 0) { + return path; + } + + const char *home = std::getenv("HOME"); + if (!home || !*home) { + return path; + } + + return std::filesystem::path(home) / path.substr(2); +} + +Glib::RefPtr WallpaperWindow::load_texture(const std::string &path) { + if (path.empty()) { + spdlog::warn("Wallpaper image path is empty"); + return {}; + } + + if (!std::filesystem::exists(path)) { + spdlog::warn("Wallpaper image not found at path: {}", path); + return {}; + } + + try { + auto file = Gio::File::create_for_path(path); + return Gdk::Texture::create_from_file(file); + } catch (const std::exception &ex) { + spdlog::warn("Failed to load wallpaper image {}: {}", path, ex.what()); + return {}; + } catch (...) { + spdlog::warn("Failed to load wallpaper image {}: unknown error", path); + return {}; + } +}