From c7f499e372edaca11c8f9163db335602698b35bf Mon Sep 17 00:00:00 2001 From: VPavliashvili Date: Sun, 21 Jul 2024 21:34:29 +0400 Subject: [PATCH] image module reworked and added multiple image rendering capability --- include/modules/image.hpp | 71 +++++++++++-- man/waybar-image.5.scd | 157 ++++++++++++++++++++++++++- src/modules/image.cpp | 216 ++++++++++++++++++++++++++++++++++---- 3 files changed, 414 insertions(+), 30 deletions(-) diff --git a/include/modules/image.hpp b/include/modules/image.hpp index 7c0d014f..56a55b63 100644 --- a/include/modules/image.hpp +++ b/include/modules/image.hpp @@ -14,9 +14,67 @@ namespace waybar::modules { +namespace image { + +class IStrategy { + public: + virtual ~IStrategy() = default; + virtual void update() = 0; +}; + +class SingleImageStrategy : public IStrategy { + public: + SingleImageStrategy(const std::string &, const Json::Value &, const std::string &, + Gtk::EventBox &, bool); + ~SingleImageStrategy() override = default; + void update() override; + + private: + void parseOutputRaw(); + + util::command::res output_; + Json::Value config_; + Gtk::Image image_; + std::string path_; + std::string tooltip_; + int size_; + Gtk::Box box_; + bool hasTooltip_; +}; + +class MultipleImageStrategy : public IStrategy { + public: + MultipleImageStrategy(const std::string &, const Json::Value &, const std::string &, + Gtk::EventBox &); + ~MultipleImageStrategy() override = default; + void update() override; + + private: + struct ImageData { + std::string path; + std::string marker; + std::string tooltip; + std::string on_click; + std::shared_ptr img; + std::shared_ptr btn; + }; + + void setImagesData(const Json::Value &); + void setupAndDraw(); + void resetBoxAndMemory(); + void handleClick(const Glib::ustring &data); + + Json::Value config_; + int size_; + Gtk::Box box_; + std::vector images_data_; +}; + +} // namespace image + class Image : public AModule { public: - Image(const std::string&, const Json::Value&); + Image(const std::string &, const Json::Value &); virtual ~Image() = default; auto update() -> void override; void refresh(int /*signal*/) override; @@ -24,16 +82,11 @@ class Image : public AModule { private: void delayWorker(); void handleEvent(); - void parseOutputRaw(); + static std::unique_ptr getStrategy(const std::string &, const Json::Value &, + const std::string &, Gtk::EventBox &, bool); - Gtk::Box box_; - Gtk::Image image_; - std::string path_; - std::string tooltip_; - int size_; int interval_; - util::command::res output_; - + std::unique_ptr strategy_; util::SleeperThread thread_; }; diff --git a/man/waybar-image.5.scd b/man/waybar-image.5.scd index e3a69e38..d549a618 100644 --- a/man/waybar-image.5.scd +++ b/man/waybar-image.5.scd @@ -6,10 +6,19 @@ waybar - image module # DESCRIPTION -The *image* module displays an image from a path. +The *image* module displays container of images(or image) from a provided paths + +# REMARK + +This module has been rewritten to add multiple image rendering functionality. +To avoid users inconvenience of breaking changes, everything related to old(only +one *image*) implementation has been left untouched including configuration part. For this +reason this wiki page is split into two parts # CONFIGURATION +For single *image* + *path*: ++ typeof: string ++ The path to the image. @@ -88,3 +97,149 @@ $path\\n$tooltip - *#image* - *#image.empty* + +# CONFIGURATION + +For multiple *image* + +*size*: ++ + typeof: string ++ + default: 16 ++ + Minumum size of the rendered image in pixels. + +*interval*: ++ + typeof: interger ++ + default: INT_MAX ++ + The interval in seconds to redraw module ++ + Default value is max value of int and changing it is only recommended ++ + if image path or other property is being changed overtime ++ + If no *interval* is provided, the module will only be rendered once. + +*multiple*: ++ + typeof: bool ++ + default: false ++ + this parameter is used to decide if old *image* implementation should be ++ + used(therefore render only one image) or to pick a new implementation to ++ + rended multiple images provided from this config. default value of this ++ + parameter is false. + +*signal*: ++ + typeof: interger ++ + default: 0 ++ + The signal number is used to redraw the module. ++ + This is used if *interval* is not provided and module should change ++ + regularly depended on external events, e.g when clicking on it. ++ + The provided value is valid between 1 and N, where SIGRTMIN+N <= SIGRTMAX. ++ + e.g if *signal* = 7, 34 + 7 = 41, which means waybar process should ++ + receive signal 41 to redraw this module, since SIGRTMIN = 34 (see 'kill -l'). + +*entries*: ++ + typeof: array ++ + default: empty array ++ + Json array of objects consisting of several fields ++ + this is a main part of configuration where per image properties are defined + + *path*: ++ + typeof: string ++ + default: empty string ++ + file path of picture to draw + + *marker*: ++ + typeof: string ++ + default: empty string ++ + user defined per image keyword(marker) to identify them, e.g in styles.css ++ + e.g if you want to draw 3 pictures and one of them dimmed, you can give ++ + *marker*: "dimmed" value and in styles.css write something like this ++ + #image .dimmed { opacity: 0.3 } + + *tooltip*: ++ + typeof: string ++ + default: empty string ++ + description of the image displayed on hover + + *on-click*: ++ + typeof: string ++ + default: empty string ++ + action to perform when clicking on the image. ++ + The action can be any system or custom binary/script ++ + e.g if { "on-click": "notify-send \"hello world\"" } ++ + "hello world" will appear as notification. + +*exec*: ++ + typeof: string ++ + default: empty string ++ + same as *entries* but provided from external script which should dynamically ++ + return json array of objects consisting of same keys as *entries*. ++ + This option is specifically useful in combination with *signal* option without *interval* provided. + +# Examples + +With entries +``` +"image#wentr": { + "entries": [ + { + "path": "/home/user/Pictures/idk1.png", + "marker": "dimmed", + "tooltip": "dimmed image", + "on-click": "notify-send \"hello world\"" + }, + { + "path": "/home/user/Pictures/idk2.png", + "marker": "normal", + "on-click": "myCustomScript.py" + } + ], + "size": 32, + "interval": 1 // will redraw every 5 seconds +} +``` + +With exec +``` +"image#wexec": { + "exec": "imageProvider.py", + "size": 32, + "signal": 7 // e.g 'pkill -n waybar --signal 41' will trigger redraw +} +``` + +where imageProvider.py looks like this +``` +#!/usr/bin/python + +import json + +dir = "/home/user/Pictures/" +out = [ + {"path": dir + "idk1.png", "marker": "clickable", "on-click": "echo 'hello world'"}, + {"path": dir + "idk2.png", "marker": "normal", "tooltip": "sample tooltip"}, + {"path": dir + "idk3.png", "marker": "important"}, +] +res = json.dumps(out) +print(res) +``` + +## Example styles +``` +#image .important { + opacity: 0.9; +} + +#image button.important { + opacity: 0.2 +} + +#image .normal { + opacity: 0.3 +} +``` + +# STYLE + +- *#image* +- *#image*.empty /\*when image could not be rendered\*/ +- *#image*.button /\*when on-click is provided\*/ +- *#image*.*marker* /\*where *marker* is parameter from config\*/ +- *#image*.button.*marker* + diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 8274d323..ff704ab9 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -1,25 +1,13 @@ #include "modules/image.hpp" +#include + waybar::modules::Image::Image(const std::string& id, const Json::Value& config) - : AModule(config, "image", id), box_(Gtk::ORIENTATION_HORIZONTAL, 0) { - box_.pack_start(image_); - box_.set_name("image"); - if (!id.empty()) { - box_.get_style_context()->add_class(id); - } - box_.get_style_context()->add_class(MODULE_CLASS); - event_box_.add(box_); - - dp.emit(); - - size_ = config["size"].asInt(); + : AModule(config, "image", id) { + strategy_ = getStrategy(id, config, MODULE_CLASS, event_box_, tooltipEnabled()); interval_ = config_["interval"].asInt(); - if (size_ == 0) { - size_ = 16; - } - if (interval_ == 0) { interval_ = INT_MAX; } @@ -27,6 +15,20 @@ waybar::modules::Image::Image(const std::string& id, const Json::Value& config) delayWorker(); } +auto waybar::modules::Image::getStrategy( + const std::string& id, const Json::Value& cfg, const std::string& module, Gtk::EventBox& evbox, + bool hasTooltip) -> std::unique_ptr { + std::unique_ptr strat; + if (!cfg["multiple"].empty() && cfg["multiple"].asBool()) { + strat = std::make_unique(id, cfg, module, evbox); + } else { + strat = std::make_unique(id, cfg, module, evbox, + hasTooltip); + } + + return strat; +} + void waybar::modules::Image::delayWorker() { thread_ = [this] { dp.emit(); @@ -42,6 +44,180 @@ void waybar::modules::Image::refresh(int sig) { } auto waybar::modules::Image::update() -> void { + strategy_->update(); + + AModule::update(); +} + +namespace waybar::modules::image { + +MultipleImageStrategy::MultipleImageStrategy(const std::string& id, const Json::Value& config, + const std::string& module, Gtk::EventBox& evbox) + : IStrategy(), box_(Gtk::ORIENTATION_HORIZONTAL, 0) { + config_ = config; + + box_.set_name("image"); + box_.get_style_context()->add_class(id); + box_.get_style_context()->add_class(module); + evbox.add(box_); + + size_ = config["size"].asInt(); + if (size_ == 0) { + size_ = 16; + } +} + +void MultipleImageStrategy::update() { + // spdlog::info("update function run"); + + // clear box_, previous css classes and memory + if (box_.get_children().size() > 0) { + resetBoxAndMemory(); + } + + // set new images from config script + if (!config_["entries"].empty()) { + setImagesData(config_["entries"]); + } else if (!config_["exec"].empty()) { + auto exec = util::command::exec(config_["exec"].asString(), ""); + Json::Value as_json; + Json::Reader reader; + + if (!reader.parse(exec.out, as_json)) { + spdlog::error("invalid json from exec {}", exec.out); + return; + } + + setImagesData(as_json); + } else { + spdlog::error("no image files provded in config"); + return; + } + + setupAndDraw(); +} + +void MultipleImageStrategy::setupAndDraw() { + for (unsigned int i = 0; i < images_data_.size(); i++) { + images_data_[i].img = std::make_shared(); + images_data_[i].btn = std::make_shared(); + + auto img = images_data_[i].img; + auto data = images_data_[i]; + + auto path = data.path; + auto marker = data.marker; + auto tooltip = data.tooltip; + bool has_onclick = !data.on_click.empty(); + + Glib::RefPtr pixbuf; + pixbuf = Gdk::Pixbuf::create_from_file(path, size_, size_); + + if (has_onclick) { + auto btn = images_data_[i].btn; + btn->set_name("button_" + path); + btn->get_style_context()->add_class(marker); + btn->set_tooltip_text(tooltip); + btn->set_image(*img); + box_.pack_start(*btn); + + btn->add_events(Gdk::BUTTON_PRESS_MASK); + btn->signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &MultipleImageStrategy::handleClick), data.on_click)); + + if (pixbuf) { + btn->show_all(); + img->set(pixbuf); + box_.get_style_context()->remove_class("empty"); + } else { + btn->hide(); + img->clear(); + img->hide(); + box_.get_style_context()->add_class("empty"); + } + } else { + img->set_name(path); + img->get_style_context()->add_class(marker); + img->set_tooltip_text(tooltip); + box_.pack_start(*img); + // spdlog::info("added image -> {}:{}", marker, path); + + if (pixbuf) { + img->set(pixbuf); + img->show(); + box_.get_style_context()->remove_class("empty"); + } else { + img->clear(); + img->hide(); + box_.get_style_context()->add_class("empty"); + } + } + } +} + +void MultipleImageStrategy::setImagesData(const Json::Value& entries) { + for (unsigned int i = 0; i < entries.size(); i++) { + auto path = entries[i]["path"]; + auto marker = entries[i]["marker"]; + auto tooltip = entries[i]["tooltip"]; + auto onclick = entries[i]["on-click"]; + + bool has_tooltip_err = !tooltip.empty() && !tooltip.isString(); + bool has_onclick_err = !onclick.empty() && !onclick.isString(); + + if (!path.isString() || !marker.isString() || has_tooltip_err || has_onclick_err || + !Glib::file_test(path.asString(), Glib::FILE_TEST_EXISTS)) { + spdlog::error("invalid input in images config -> {}", entries[i]); + return; + } + ImageData data; + data.path = path.asString(); + data.marker = marker.asString(); + data.tooltip = !tooltip.empty() ? tooltip.asString() : ""; + data.on_click = onclick.asString(); + + images_data_.push_back(data); + } +} + +void MultipleImageStrategy::resetBoxAndMemory() { + auto children = box_.get_children(); + for (auto child : children) { + box_.remove(*child); + // spdlog::info("child removed with name -> {}", std::string(child->get_name())); + } + + images_data_.clear(); +} + +void MultipleImageStrategy::handleClick(const Glib::ustring& data) { + auto msg = std::string(data); + + auto exec = util::command::exec(data, ""); +} + +SingleImageStrategy::SingleImageStrategy(const std::string& id, const Json::Value& config, + const std::string& module, Gtk::EventBox& evbox, + bool tooltipEnabled) + : IStrategy(), box_(Gtk::ORIENTATION_HORIZONTAL, 0) { + config_ = config; + hasTooltip_ = tooltipEnabled; + + box_.pack_start(image_); + box_.set_name("image"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + box_.get_style_context()->add_class(module); + evbox.add(box_); + + size_ = config["size"].asInt(); + if (size_ == 0) { + size_ = 16; + } +} + +void SingleImageStrategy::update() { Glib::RefPtr pixbuf; if (config_["path"].isString()) { path_ = config_["path"].asString(); @@ -57,7 +233,7 @@ auto waybar::modules::Image::update() -> void { pixbuf = {}; if (pixbuf) { - if (tooltipEnabled() && !tooltip_.empty()) { + if (hasTooltip_ && !tooltip_.empty()) { if (box_.get_tooltip_markup() != tooltip_) { box_.set_tooltip_markup(tooltip_); } @@ -70,11 +246,9 @@ auto waybar::modules::Image::update() -> void { image_.hide(); box_.get_style_context()->add_class("empty"); } - - AModule::update(); } -void waybar::modules::Image::parseOutputRaw() { +void SingleImageStrategy::parseOutputRaw() { std::istringstream output(output_.out); std::string line; int i = 0; @@ -89,3 +263,5 @@ void waybar::modules::Image::parseOutputRaw() { i++; } } + +} // namespace waybar::modules::image