diff --git a/include/factory.hpp b/include/factory.hpp index c698aa32..28273eb0 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -7,6 +7,9 @@ #include "modules/sway/window.hpp" #include "modules/sway/workspaces.hpp" #endif +#ifdef HAVE_WLR +#include "modules/wlr/taskbar.hpp" +#endif #if defined(__linux__) && !defined(NO_FILESYSTEM) #include "modules/battery.hpp" #endif diff --git a/include/modules/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp new file mode 100644 index 00000000..5bcb7ec4 --- /dev/null +++ b/include/modules/wlr/taskbar.hpp @@ -0,0 +1,160 @@ +#pragma once + +#include "AModule.hpp" +#include "bar.hpp" +#include "client.hpp" +#include "util/json.hpp" + +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include "wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + + +namespace waybar::modules::wlr { + +class Taskbar; + +class Task +{ + public: + Task(const waybar::Bar&, const Json::Value&, Taskbar*, + struct zwlr_foreign_toplevel_handle_v1 *, struct wl_seat*); + ~Task(); + + public: + enum State { + MAXIMIZED = (1 << 0), + MINIMIZED = (1 << 1), + ACTIVE = (1 << 2), + FULLSCREEN = (1 << 3), + INVALID = (1 << 4) + }; + + private: + static uint32_t global_id; + + private: + const waybar::Bar &bar_; + const Json::Value &config_; + Taskbar *tbar_; + struct zwlr_foreign_toplevel_handle_v1 *handle_; + struct wl_seat *seat_; + + uint32_t id_; + + Gtk::Button button_; + Gtk::Box content_; + Gtk::Image icon_; + Gtk::Label text_before_; + Gtk::Label text_after_; + bool button_visible_; + + bool with_icon_; + std::string format_before_; + std::string format_after_; + + std::string format_tooltip_; + + std::string title_; + std::string app_id_; + uint32_t state_; + + private: + std::string repr() const; + std::string state_string(bool = false) const; + + public: + /* Getter functions */ + uint32_t id() const { return id_; } + std::string title() const { return title_; } + std::string app_id() const { return app_id_; } + uint32_t state() const { return state_; } + bool maximized() const { return state_ & MAXIMIZED; } + bool minimized() const { return state_ & MINIMIZED; } + bool active() const { return state_ & ACTIVE; } + bool fullscreen() const { return state_ & FULLSCREEN; } + + public: + /* Callbacks for the wlr protocol */ + void handle_title(const char *); + void handle_app_id(const char *); + void handle_output_enter(struct wl_output *); + void handle_output_leave(struct wl_output *); + void handle_state(struct wl_array *); + void handle_done(); + void handle_closed(); + + /* Callbacks for Gtk events */ + bool handle_clicked(GdkEventButton *); + + public: + bool operator==(const Task&) const; + bool operator!=(const Task&) const; + + public: + void update(); + + public: + /* Interaction with the tasks */ + void maximize(bool); + void minimize(bool); + void activate(); + void fullscreen(bool); + void close(); +}; + +using TaskPtr = std::unique_ptr; + + +class Taskbar : public waybar::AModule +{ + public: + Taskbar(const std::string&, const waybar::Bar&, const Json::Value&); + ~Taskbar(); + void update(); + + private: + const waybar::Bar &bar_; + Gtk::Box box_; + std::vector tasks_; + + Glib::RefPtr icon_theme_; + + struct zwlr_foreign_toplevel_manager_v1 *manager_; + struct wl_seat *seat_; + + public: + /* Callbacks for global registration */ + void register_manager(struct wl_registry*, uint32_t name, uint32_t version); + void register_seat(struct wl_registry*, uint32_t name, uint32_t version); + + /* Callbacks for the wlr protocol */ + void handle_toplevel_create(struct zwlr_foreign_toplevel_handle_v1 *); + void handle_finished(); + + public: + void add_button(Gtk::Button &); + void move_button(Gtk::Button &, int); + void remove_button(Gtk::Button &); + void remove_task(uint32_t); + + bool show_output(struct wl_output *) const; + bool all_outputs() const; + + Glib::RefPtr icon_theme() const; +}; + +} /* namespace waybar::modules::wlr */ diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd new file mode 100644 index 00000000..cbce6e77 --- /dev/null +++ b/man/waybar-wlr-taskbar.5.scd @@ -0,0 +1,104 @@ +waybar-wlr-taskbar(5) + +# NAME + +wlroots - Taskbar module + +# DESCRIPTION + +The *taskbar* module displays the currently open applications. This module requires +a compositor that implements the foreign-toplevel-manager interface. + +# CONFIGURATION + +Addressed by *wlr/taskbar* + +*all-outputs*: ++ + typeof: bool ++ + default: false ++ + If set to false applications on the waybar's current output will be shown. Otherwise all applications are shown. + +*format*: ++ + typeof: string ++ + default: {icon} ++ + The format, how information should be displayed. + +*icon-theme*: ++ + typeof: string ++ + The name of the icon-theme that should be used. If omitted, the system default will be used. + +*icon-size*: ++ + typeof: integer ++ + default: 16 ++ + The size of the icon. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + If set to false no tooltip will be shown. + +*tooltip-format*: ++ + typeof: string ++ + default: {title} ++ + The format, how information in the tooltip should be displayed. + +*active-first*: ++ + typeof: bool ++ + default: false ++ + If set to true, always reorder the tasks in the taskbar so that the currently active one is first. Otherwise don't reorder. + +*on-click*: ++ + typeof: string ++ + The action which should be triggered when clicking on the application button with the left mouse button. + +*on-click-middle*: ++ + typeof: string ++ + The action which should be triggered when clicking on the application button with the middle mouse button. + +*on-click-right*: ++ + typeof: string ++ + The action which should be triggered when clicking on the application button with the right mouse button. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +# FORMAT REPLACEMENTS + +*{icon}*: The icon of the application. + +*{title}*: The title of the application. + +*{app_id}*: The app_id (== application name) of the application. + +*{state}*: The state (minimized, maximized, active, fullscreen) of the application. + +*{short_state}*: The state (minimize == m, maximized == M, active == A, fullscreen == F) represented as one character of the application. + +# CLICK ACTIONS + +*activate*: Bring the application into foreground. +*minimize*: Minimize the application. +*maximize*: Maximize the application. +*fullscreen*: Set the application to fullscreen. +*close*: Close the application. + +# EXAMPLES + +``` +"wlr/taskbar": { + "format": "{icon}", + "tooltip-format": "{title}", + "on-click": "activate", + "on-middle-click": "close" +} +``` + +# Style + +- *#taskbar* +- *#taskbar button* +- *#taskbar button.maximized* +- *#taskbar button.minimized* +- *#taskbar button.active* +- *#taskbar button.fullscreen* diff --git a/man/waybar.5.scd b/man/waybar.5.scd index 1e8004f2..9ff18910 100644 --- a/man/waybar.5.scd +++ b/man/waybar.5.scd @@ -196,5 +196,6 @@ Valid options for the "rotate" property are: 0, 90, 180 and 270. - *waybar-sway-mode(5)* - *waybar-sway-window(5)* - *waybar-sway-workspaces(5)* +- *waybar-wlr-taskbar(5)* - *waybar-temperature(5)* - *waybar-tray(5)* diff --git a/meson.build b/meson.build index c16d7854..496fc3a8 100644 --- a/meson.build +++ b/meson.build @@ -165,6 +165,11 @@ if true # find_program('sway', required : false).found() ] endif +if true + add_project_arguments('-DHAVE_WLR', language: 'cpp') + src_files += 'src/modules/wlr/taskbar.cpp' +endif + if libnl.found() and libnlgen.found() add_project_arguments('-DHAVE_LIBNL', language: 'cpp') src_files += 'src/modules/network.cpp' @@ -260,6 +265,7 @@ if scdoc.found() 'waybar-temperature.5.scd', 'waybar-tray.5.scd', 'waybar-states.5.scd', + 'waybar-wlr-taskbar.5.scd', 'waybar-bluetooth.5.scd', ] diff --git a/protocol/meson.build b/protocol/meson.build index 0699a9d9..f4146aeb 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -26,6 +26,7 @@ client_protocols = [ [wl_protocol_dir, 'unstable/xdg-output/xdg-output-unstable-v1.xml'], [wl_protocol_dir, 'unstable/idle-inhibit/idle-inhibit-unstable-v1.xml'], ['wlr-layer-shell-unstable-v1.xml'], + ['wlr-foreign-toplevel-management-unstable-v1.xml'], ] client_protos_src = [] diff --git a/protocol/wlr-foreign-toplevel-management-unstable-v1.xml b/protocol/wlr-foreign-toplevel-management-unstable-v1.xml new file mode 100644 index 00000000..a97738f8 --- /dev/null +++ b/protocol/wlr-foreign-toplevel-management-unstable-v1.xml @@ -0,0 +1,259 @@ + + + + Copyright © 2018 Ilia Bozhinov + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + The purpose of this protocol is to enable the creation of taskbars + and docks by providing them with a list of opened applications and + letting them request certain actions on them, like maximizing, etc. + + After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + toplevel window will be sent via the toplevel event + + + + + This event is emitted whenever a new toplevel window is created. It + is emitted for all toplevels, regardless of the app that has created + them. + + All initial details of the toplevel(title, app_id, states, etc.) will + be sent immediately after this event via the corresponding events in + zwlr_foreign_toplevel_handle_v1. + + + + + + + Indicates the client no longer wishes to receive events for new toplevels. + However the compositor may emit further toplevel_created events, until + the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending events to the + zwlr_foreign_toplevel_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + + A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + window. Each app may have multiple opened toplevels. + + Each toplevel has a list of outputs it is visible on, conveyed to the + client with the output_enter and output_leave events. + + + + + This event is emitted whenever the title of the toplevel changes. + + + + + + + This event is emitted whenever the app-id of the toplevel changes. + + + + + + + This event is emitted whenever the toplevel becomes visible on + the given output. A toplevel may be visible on multiple outputs. + + + + + + + This event is emitted whenever the toplevel stops being visible on + the given output. It is guaranteed that an entered-output event + with the same output has been emitted before this event. + + + + + + + Requests that the toplevel be maximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unmaximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be minimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unminimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Request that this toplevel be activated on the given seat. + There is no guarantee the toplevel will be actually activated. + + + + + + + The different states that a toplevel can have. These have the same meaning + as the states with the same names defined in xdg-toplevel + + + + + + + + + + + This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 + is created and each time the toplevel state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + + This event is sent after all changes in the toplevel state have been + sent. + + This allows changes to the zwlr_foreign_toplevel_handle_v1 properties + to be seen as atomic, even if they happen via multiple events. + + + + + + Send a request to the toplevel to close itself. The compositor would + typically use a shell-specific method to carry out this request, for + example by sending the xdg_toplevel.close event. However, this gives + no guarantees the toplevel will actually be destroyed. If and when + this happens, the zwlr_foreign_toplevel_handle_v1.closed event will + be emitted. + + + + + + The rectangle of the surface specified in this request corresponds to + the place where the app using this protocol represents the given toplevel. + It can be used by the compositor as a hint for some operations, e.g + minimizing. The client is however not required to set this, in which + case the compositor is free to decide some default value. + + If the client specifies more than one rectangle, only the last one is + considered. + + The dimensions are given in surface-local coordinates. + Setting width=height=0 removes the already-set rectangle. + + + + + + + + + + + + + + + + This event means the toplevel has been destroyed. It is guaranteed there + won't be any more events for this zwlr_foreign_toplevel_handle_v1. The + toplevel itself becomes inert so any requests will be ignored except the + destroy request. + + + + + + Destroys the zwlr_foreign_toplevel_handle_v1 object. + + This request should be called either when the client does not want to + use the toplevel anymore or after the closed event to finalize the + destruction of the object. + + + + + + + + Requests that the toplevel be fullscreened on the given output. If the + fullscreen state and/or the outputs the toplevel is visible on actually + change, this will be indicated by the state and output_enter/leave + events. + + The output parameter is only a hint to the compositor. Also, if output + is NULL, the compositor should decide which output the toplevel will be + fullscreened on, if at all. + + + + + + + Requests that the toplevel be unfullscreened. If the fullscreen state + actually changes, this will be indicated by the state event. + + + + diff --git a/src/factory.cpp b/src/factory.cpp index 6005cad5..f09ea31a 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -22,6 +22,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "sway/window") { return new waybar::modules::sway::Window(id, bar_, config_[name]); } +#endif +#ifdef HAVE_WLR + if (ref == "wlr/taskbar") { + return new waybar::modules::wlr::Taskbar(id, bar_, config_[name]); + } #endif if (ref == "idle_inhibitor") { return new waybar::modules::IdleInhibitor(id, bar_, config_[name]); diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp new file mode 100644 index 00000000..845f07c4 --- /dev/null +++ b/src/modules/wlr/taskbar.cpp @@ -0,0 +1,658 @@ +#include "modules/wlr/taskbar.hpp" + +#include "glibmm/refptr.h" +#include "util/format.hpp" + +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include + + +namespace waybar::modules::wlr { + +/* Icon loading functions */ + +/* Method 1 - get the correct icon name from the desktop file */ +static std::string get_from_desktop_app_info(const std::string &app_id) +{ + Glib::RefPtr app_info; + + std::vector prefixes = { + "", + "/usr/share/applications/", + "/usr/share/applications/kde/", + "/usr/share/applications/org.kde.", + "/usr/local/share/applications/", + "/usr/local/share/applications/org.kde.", + }; + + std::string lower_app_id = app_id; + std::transform(std::begin(lower_app_id), std::end(lower_app_id), std::begin(lower_app_id), + [](unsigned char c) { return std::tolower(c); }); + + + std::vector app_id_variations = { + app_id, + lower_app_id + }; + + std::vector suffixes = { + "", + ".desktop" + }; + + for (auto& prefix : prefixes) + for (auto& id : app_id_variations) + for (auto& suffix : suffixes) + if (!app_info) + app_info = Gio::DesktopAppInfo::create_from_filename(prefix + id + suffix); + + if (app_info) + return app_info->get_icon()->to_string(); + + return ""; +} + +/* Method 2 - use the app_id and check whether there is an icon with this name in the icon theme */ +static std::string get_from_icon_theme(Glib::RefPtr icon_theme, + const std::string &app_id) { + + if (icon_theme->lookup_icon(app_id, 24)) + return app_id; + + return ""; +} + +static bool image_load_icon(Gtk::Image& image, Glib::RefPtr icon_theme, + const std::string &app_id_list, int size) +{ + std::string app_id; + std::istringstream stream(app_id_list); + bool found = false; + + + /* Wayfire sends a list of app-id's in space separated format, other compositors + * send a single app-id, but in any case this works fine */ + while (stream >> app_id) + { + std::string icon_name = get_from_desktop_app_info(app_id); + if (icon_name.empty()) + icon_name = get_from_icon_theme(icon_theme, app_id); + + if (icon_name.empty()) + continue; + + auto pixbuf = icon_theme->load_icon(icon_name, size, Gtk::ICON_LOOKUP_FORCE_SIZE); + if (pixbuf) { + image.set(pixbuf); + found = true; + break; + } + } + + return found; +} + +/* Task class implementation */ +uint32_t Task::global_id = 0; + +static void tl_handle_title(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, + const char *title) +{ + return static_cast(data)->handle_title(title); +} + +static void tl_handle_app_id(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, + const char *app_id) +{ + return static_cast(data)->handle_app_id(app_id); +} + +static void tl_handle_output_enter(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, + struct wl_output *output) +{ + return static_cast(data)->handle_output_enter(output); +} + +static void tl_handle_output_leave(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, + struct wl_output *output) +{ + return static_cast(data)->handle_output_leave(output); +} + +static void tl_handle_state(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, + struct wl_array *state) +{ + return static_cast(data)->handle_state(state); +} + +static void tl_handle_done(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle) +{ + return static_cast(data)->handle_done(); +} + +static void tl_handle_closed(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle) +{ + return static_cast(data)->handle_closed(); +} + +static const struct zwlr_foreign_toplevel_handle_v1_listener toplevel_handle_impl = { + .title = tl_handle_title, + .app_id = tl_handle_app_id, + .output_enter = tl_handle_output_enter, + .output_leave = tl_handle_output_leave, + .state = tl_handle_state, + .done = tl_handle_done, + .closed = tl_handle_closed, +}; + +Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, + struct zwlr_foreign_toplevel_handle_v1 *tl_handle, struct wl_seat *seat) : + bar_{bar}, config_{config}, tbar_{tbar}, handle_{tl_handle}, seat_{seat}, + id_{global_id++}, + content_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, + button_visible_{false} +{ + zwlr_foreign_toplevel_handle_v1_add_listener(handle_, &toplevel_handle_impl, this); + + button_.set_relief(Gtk::RELIEF_NONE); + + content_.add(text_before_); + content_.add(icon_); + content_.add(text_after_); + + content_.show(); + button_.add(content_); + + with_icon_ = false; + format_before_.clear(); + format_after_.clear(); + + if (config_["format"].isString()) { + /* The user defined a format string, use it */ + auto format = config_["format"].asString(); + + auto icon_pos = format.find("{icon}"); + if (icon_pos == 0) { + with_icon_ = true; + format_after_ = format.substr(6); + } else if (icon_pos == std::string::npos) { + format_after_ = format; + } else { + with_icon_ = true; + format_before_ = format.substr(0, icon_pos); + format_after_ = format.substr(icon_pos + 6); + } + } else { + /* The default is to only show the icon */ + with_icon_ = true; + } + + /* Strip spaces at the beginning and end of the format strings */ + if (!format_before_.empty() && format_before_.back() == ' ') + format_before_.pop_back(); + if (!format_after_.empty() && format_after_.front() == ' ') + format_after_.erase(std::cbegin(format_after_)); + + format_tooltip_.clear(); + if (!config_["tooltip"].isBool() || config_["tooltip"].asBool()) { + if (config_["tooltip-format"].isString()) + format_tooltip_ = config_["tooltip-format"].asString(); + else + format_tooltip_ = "{title}"; + } + + /* Handle click events if configured */ + if (config_["on-click"].isString() || config_["on-click-middle"].isString() + || config_["on-click-left"].isString()) { + button_.add_events(Gdk::BUTTON_PRESS_MASK); + button_.signal_button_press_event().connect( + sigc::mem_fun(*this, &Task::handle_clicked), false); + } +} + +Task::~Task() +{ + if (handle_) { + zwlr_foreign_toplevel_handle_v1_destroy(handle_); + handle_ = nullptr; + } + if (button_visible_) { + tbar_->remove_button(button_); + button_visible_ = false; + } +} + +std::string Task::repr() const +{ + std::stringstream ss; + ss << "Task (" << id_ << ") " << title_ << " [" << app_id_ << "] <" + << (active() ? "A" : "a") + << (maximized() ? "M" : "m") + << (minimized() ? "I" : "i") + << (fullscreen() ? "F" : "f") + << ">"; + + return ss.str(); +} + +std::string Task::state_string(bool shortened) const +{ + std::stringstream ss; + if (shortened) + ss << (minimized() ? "m" : "") << (maximized() ? "M" : "") + << (active() ? "A" : "") << (fullscreen() ? "F" : ""); + else + ss << (minimized() ? "minimized " : "") << (maximized() ? "maximized " : "") + << (active() ? "active " : "") << (fullscreen() ? "fullscreen " : ""); + + std::string res = ss.str(); + if (shortened || res.empty()) + return res; + else + return res.substr(0, res.size() - 1); +} + +void Task::handle_title(const char *title) +{ + title_ = title; +} + +void Task::handle_app_id(const char *app_id) +{ + app_id_ = app_id; + if (!image_load_icon(icon_, tbar_->icon_theme(), app_id_, + config_["icon-size"].isInt() ? config_["icon-size"].asInt() : 16)) + spdlog::warn("Failed to load icon for {}", app_id); + + if (with_icon_) + icon_.show(); +} + +void Task::handle_output_enter(struct wl_output *output) +{ + spdlog::debug("{} entered output {}", repr(), (void*)output); + + if (!button_visible_ && (tbar_->all_outputs() || tbar_->show_output(output))) { + /* The task entered the output of the current bar make the button visible */ + tbar_->add_button(button_); + button_.show(); + button_visible_ = true; + spdlog::debug("{} now visible on {}", repr(), bar_.output->name); + } +} + +void Task::handle_output_leave(struct wl_output *output) +{ + spdlog::debug("{} left output {}", repr(), (void*)output); + + if (button_visible_ && !tbar_->all_outputs() && tbar_->show_output(output)) { + /* The task left the output of the current bar, make the button invisible */ + tbar_->remove_button(button_); + button_.hide(); + button_visible_ = false; + spdlog::debug("{} now invisible on {}", repr(), bar_.output->name); + } +} + +void Task::handle_state(struct wl_array *state) +{ + state_ = 0; + for (uint32_t* entry = static_cast(state->data); + entry < static_cast(state->data) + state->size; + entry++) { + if (*entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED) + state_ |= MAXIMIZED; + if (*entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED) + state_ |= MINIMIZED; + if (*entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED) + state_ |= ACTIVE; + if (*entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN) + state_ |= FULLSCREEN; + } +} + +void Task::handle_done() +{ + spdlog::debug("{} changed", repr()); + + if (state_ & MAXIMIZED) { + button_.get_style_context()->add_class("maximized"); + } else if (!(state_ & MAXIMIZED)) { + button_.get_style_context()->remove_class("maximized"); + } + + if (state_ & MINIMIZED) { + button_.get_style_context()->add_class("minimized"); + } else if (!(state_ & MINIMIZED)) { + button_.get_style_context()->remove_class("minimized"); + } + + if (state_ & ACTIVE) { + button_.get_style_context()->add_class("active"); + } else if (!(state_ & ACTIVE)) { + button_.get_style_context()->remove_class("active"); + } + + if (state_ & FULLSCREEN) { + button_.get_style_context()->add_class("fullscreen"); + } else if (!(state_ & FULLSCREEN)) { + button_.get_style_context()->remove_class("fullscreen"); + } + + if (config_["active-first"].isBool() && config_["active-first"].asBool() && active()) + tbar_->move_button(button_, 0); + + tbar_->dp.emit(); +} + +void Task::handle_closed() +{ + spdlog::debug("{} closed", repr()); + zwlr_foreign_toplevel_handle_v1_destroy(handle_); + handle_ = nullptr; + if (button_visible_) { + tbar_->remove_button(button_); + button_visible_ = false; + } + tbar_->remove_task(id_); +} + +bool Task::handle_clicked(GdkEventButton *bt) +{ + std::string action; + if (config_["on-click"].isString() && bt->button == 1) + action = config_["on-click"].asString(); + else if (config_["on-click-middle"].isString() && bt->button == 2) + action = config_["on-click-middle"].asString(); + else if (config_["on-click-right"].isString() && bt->button == 3) + action = config_["on-click-right"].asString(); + + if (action.empty()) + return true; + else if (action == "activate") + activate(); + else if (action == "minimize") + minimize(!minimized()); + else if (action == "maximize") + maximize(!maximized()); + else if (action == "fullscreen") + fullscreen(!fullscreen()); + else if (action == "close") + close(); + else + spdlog::warn("Unknown action {}", action); + + return true; +} + +bool Task::operator==(const Task &o) const +{ + return o.id_ == id_; +} + +bool Task::operator!=(const Task &o) const +{ + return o.id_ != id_; +} + +void Task::update() +{ + if (!format_before_.empty()) { + text_before_.set_label( + fmt::format(format_before_, + fmt::arg("title", title_), + fmt::arg("app_id", app_id_), + fmt::arg("state", state_string()), + fmt::arg("short_state", state_string(true)) + ) + ); + text_before_.show(); + } + if (!format_after_.empty()) { + text_after_.set_label( + fmt::format(format_before_, + fmt::arg("title", title_), + fmt::arg("app_id", app_id_), + fmt::arg("state", state_string()), + fmt::arg("short_state", state_string(true)) + ) + ); + text_after_.show(); + } + + if (!format_tooltip_.empty()) { + button_.set_tooltip_markup( + fmt::format(format_tooltip_, + fmt::arg("title", title_), + fmt::arg("app_id", app_id_), + fmt::arg("state", state_string()), + fmt::arg("short_state", state_string(true)) + ) + ); + } +} + +void Task::maximize(bool set) +{ + if (set) + zwlr_foreign_toplevel_handle_v1_set_maximized(handle_); + else + zwlr_foreign_toplevel_handle_v1_unset_maximized(handle_); +} + +void Task::minimize(bool set) +{ + if (set) + zwlr_foreign_toplevel_handle_v1_set_minimized(handle_); + else + zwlr_foreign_toplevel_handle_v1_unset_minimized(handle_); +} + +void Task::activate() +{ + zwlr_foreign_toplevel_handle_v1_activate(handle_, seat_); +} + +void Task::fullscreen(bool set) +{ + if (set) + zwlr_foreign_toplevel_handle_v1_set_fullscreen(handle_, nullptr); + else + zwlr_foreign_toplevel_handle_v1_unset_fullscreen(handle_); +} + +void Task::close() +{ + zwlr_foreign_toplevel_handle_v1_close(handle_); +} + + +/* Taskbar class implementation */ +static void handle_global(void *data, struct wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) +{ + if (std::strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) { + static_cast(data)->register_manager(registry, name, version); + } else if (std::strcmp(interface, wl_seat_interface.name) == 0) { + static_cast(data)->register_seat(registry, name, version); + } +} + +static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) +{ + /* Nothing to do here */ +} + +static const wl_registry_listener registry_listener_impl = { + .global = handle_global, + .global_remove = handle_global_remove +}; + +Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Value &config) + : waybar::AModule(config, "taskbar", id, false, false), + bar_(bar), + box_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, + manager_{nullptr}, seat_{nullptr} +{ + box_.set_name("taskbar"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + event_box_.add(box_); + + struct wl_display *display = Client::inst()->wl_display; + struct wl_registry *registry = wl_display_get_registry(display); + + wl_registry_add_listener(registry, ®istry_listener_impl, this); + wl_display_roundtrip(display); + + if (!manager_) { + spdlog::error("Failed to register as toplevel manager"); + return; + } + if (!seat_) { + spdlog::error("Failed to get wayland seat"); + return; + } + + /* Get the configured icon theme if specified */ + if (config_["icon-theme"].isString()) { + icon_theme_ = Gtk::IconTheme::create(); + icon_theme_->set_custom_theme(config_["icon-theme"].asString()); + spdlog::debug("Use custom icon theme: {}.", config_["icon-theme"].asString()); + } else { + spdlog::debug("Use system default icon theme"); + icon_theme_ = Gtk::IconTheme::get_default(); + } +} + +Taskbar::~Taskbar() +{ + if (manager_) { + zwlr_foreign_toplevel_manager_v1_destroy(manager_); + manager_ = nullptr; + } +} + +void Taskbar::update() +{ + for (auto& t : tasks_) { + t->update(); + } + + AModule::update(); +} + +static void tm_handle_toplevel(void *data, struct zwlr_foreign_toplevel_manager_v1 *manager, + struct zwlr_foreign_toplevel_handle_v1 *tl_handle) +{ + return static_cast(data)->handle_toplevel_create(tl_handle); +} + +static void tm_handle_finished(void *data, struct zwlr_foreign_toplevel_manager_v1 *manager) +{ + return static_cast(data)->handle_finished(); +} + +static const struct zwlr_foreign_toplevel_manager_v1_listener toplevel_manager_impl = { + .toplevel = tm_handle_toplevel, + .finished = tm_handle_finished, +}; + +void Taskbar::register_manager(struct wl_registry *registry, uint32_t name, uint32_t version) +{ + if (manager_) { + spdlog::warn("Register foreign toplevel manager again although already existing!"); + return; + } + if (version != 2) { + spdlog::warn("Using different foreign toplevel manager protocol version: {}", version); + } + + manager_ = static_cast(wl_registry_bind(registry, name, + &zwlr_foreign_toplevel_manager_v1_interface, version)); + + if (manager_) + zwlr_foreign_toplevel_manager_v1_add_listener(manager_, &toplevel_manager_impl, this); + else + spdlog::debug("Failed to register manager"); +} + +void Taskbar::register_seat(struct wl_registry *registry, uint32_t name, uint32_t version) +{ + if (seat_) { + spdlog::warn("Register seat again although already existing!"); + return; + } + + seat_ = static_cast(wl_registry_bind(registry, name, &wl_seat_interface, version)); +} + +void Taskbar::handle_toplevel_create(struct zwlr_foreign_toplevel_handle_v1 *tl_handle) +{ + tasks_.push_back(std::make_unique(bar_, config_, this, tl_handle, seat_)); +} + +void Taskbar::handle_finished() +{ + zwlr_foreign_toplevel_manager_v1_destroy(manager_); + manager_ = nullptr; +} + +void Taskbar::add_button(Gtk::Button &bt) +{ + box_.pack_start(bt, false, false); +} + +void Taskbar::move_button(Gtk::Button &bt, int pos) +{ + box_.reorder_child(bt, pos); +} + +void Taskbar::remove_button(Gtk::Button &bt) +{ + box_.remove(bt); +} + +void Taskbar::remove_task(uint32_t id) +{ + auto it = std::find_if(std::begin(tasks_), std::end(tasks_), + [id](const TaskPtr &p) { return p->id() == id; }); + + if (it == std::end(tasks_)) { + spdlog::warn("Can't find task with id {}", id); + return; + } + + tasks_.erase(it); +} + +bool Taskbar::show_output(struct wl_output *output) const +{ + return output == gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); +} + +bool Taskbar::all_outputs() const +{ + static bool result = config_["all_outputs"].isBool() ? config_["all_outputs"].asBool() : false; + + return result; +} + +Glib::RefPtr Taskbar::icon_theme() const +{ + return icon_theme_; +} + +} /* namespace waybar::modules::wlr */