mirror of
https://github.com/Alexays/Waybar.git
synced 2026-03-31 11:53:47 +00:00
The popup menu was retrieved from GtkBuilder and stored in menu_, but the builder was unref'd immediately after construction. That left the later popup path operating on a builder-owned GtkMenu whose lifetime was no longer guaranteed, which matches the GTK_IS_WIDGET and GTK_IS_MENU assertions from the regression report. Take an owned reference to the built menu and release it in AModule teardown so popup menus stay valid without extending the lifetime of the whole builder. Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
290 lines
9.5 KiB
C++
290 lines
9.5 KiB
C++
#include "AModule.hpp"
|
|
|
|
#include <fmt/format.h>
|
|
#include <spdlog/spdlog.h>
|
|
|
|
#include <util/command.hpp>
|
|
|
|
#include "gdk/gdk.h"
|
|
#include "gdkmm/cursor.h"
|
|
|
|
namespace waybar {
|
|
|
|
AModule::AModule(const Json::Value& config, const std::string& name, const std::string& id,
|
|
bool enable_click, bool enable_scroll)
|
|
: name_(name),
|
|
config_(config),
|
|
isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true},
|
|
isExpand{config_["expand"].isBool() ? config_["expand"].asBool() : false},
|
|
distance_scrolled_y_(0.0),
|
|
distance_scrolled_x_(0.0) {
|
|
// Configure module action Map
|
|
const Json::Value actions{config_["actions"]};
|
|
|
|
for (Json::Value::const_iterator it = actions.begin(); it != actions.end(); ++it) {
|
|
if (it.key().isString() && it->isString())
|
|
if (!eventActionMap_.contains(it.key().asString())) {
|
|
eventActionMap_.insert({it.key().asString(), it->asString()});
|
|
enable_click = true;
|
|
enable_scroll = true;
|
|
} else
|
|
spdlog::warn("Duplicate action is ignored: {0}", it.key().asString());
|
|
else
|
|
spdlog::warn("Wrong actions section configuration. See config by index: {}", it.index());
|
|
}
|
|
|
|
event_box_.signal_enter_notify_event().connect(sigc::mem_fun(*this, &AModule::handleMouseEnter));
|
|
event_box_.signal_leave_notify_event().connect(sigc::mem_fun(*this, &AModule::handleMouseLeave));
|
|
|
|
// configure events' user commands
|
|
// hasUserEvents is true if any element from eventMap_ is satisfying the condition in the lambda
|
|
bool hasUserEvents =
|
|
std::find_if(eventMap_.cbegin(), eventMap_.cend(), [&config](const auto& eventEntry) {
|
|
// True if there is any non-release type event
|
|
return eventEntry.first.second != GdkEventType::GDK_BUTTON_RELEASE &&
|
|
config[eventEntry.second].isString();
|
|
}) != eventMap_.cend();
|
|
|
|
if (enable_click || hasUserEvents) {
|
|
hasUserEvents_ = true;
|
|
event_box_.add_events(Gdk::BUTTON_PRESS_MASK);
|
|
event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &AModule::handleToggle));
|
|
} else {
|
|
hasUserEvents_ = false;
|
|
}
|
|
|
|
bool hasReleaseEvent =
|
|
std::find_if(eventMap_.cbegin(), eventMap_.cend(), [&config](const auto& eventEntry) {
|
|
// True if there is any non-release type event
|
|
return eventEntry.first.second == GdkEventType::GDK_BUTTON_RELEASE &&
|
|
config[eventEntry.second].isString();
|
|
}) != eventMap_.cend();
|
|
if (hasReleaseEvent) {
|
|
event_box_.add_events(Gdk::BUTTON_RELEASE_MASK);
|
|
event_box_.signal_button_release_event().connect(sigc::mem_fun(*this, &AModule::handleRelease));
|
|
}
|
|
if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString() ||
|
|
config_["on-scroll-left"].isString() || config_["on-scroll-right"].isString() ||
|
|
enable_scroll) {
|
|
event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
|
|
event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &AModule::handleScroll));
|
|
}
|
|
|
|
// Respect user configuration of cursor
|
|
if (config_.isMember("cursor")) {
|
|
if (config_["cursor"].isBool() && config_["cursor"].asBool()) {
|
|
setCursor(Gdk::HAND2);
|
|
} else if (config_["cursor"].isInt()) {
|
|
setCursor(Gdk::CursorType(config_["cursor"].asInt()));
|
|
} else {
|
|
spdlog::warn("unknown cursor option configured on module {}", name_);
|
|
}
|
|
}
|
|
}
|
|
|
|
AModule::~AModule() {
|
|
for (const auto& pid : pid_children_) {
|
|
if (pid != -1) {
|
|
killpg(pid, SIGTERM);
|
|
}
|
|
}
|
|
if (menu_ != nullptr) {
|
|
g_object_unref(menu_);
|
|
menu_ = nullptr;
|
|
}
|
|
}
|
|
|
|
auto AModule::update() -> void {
|
|
// Run user-provided update handler if configured
|
|
if (config_["on-update"].isString()) {
|
|
pid_children_.push_back(util::command::forkExec(config_["on-update"].asString()));
|
|
}
|
|
}
|
|
// Get mapping between event name and module action name
|
|
// Then call overridden doAction in order to call appropriate module action
|
|
auto AModule::doAction(const std::string& name) -> void {
|
|
if (!name.empty()) {
|
|
const std::map<std::string, std::string>::const_iterator& recA{eventActionMap_.find(name)};
|
|
// Call overridden action if derived class has implemented it
|
|
if (recA != eventActionMap_.cend() && name != recA->second) this->doAction(recA->second);
|
|
}
|
|
}
|
|
|
|
void AModule::setCursor(Gdk::CursorType const& c) {
|
|
auto gdk_window = event_box_.get_window();
|
|
if (gdk_window) {
|
|
auto cursor = Gdk::Cursor::create(c);
|
|
gdk_window->set_cursor(cursor);
|
|
} else {
|
|
// window may not be accessible yet, in this case,
|
|
// schedule another call for setting the cursor in 1 sec
|
|
Glib::signal_timeout().connect_seconds(
|
|
[this, c]() {
|
|
setCursor(c);
|
|
return false;
|
|
},
|
|
1);
|
|
}
|
|
}
|
|
|
|
bool AModule::handleMouseEnter(GdkEventCrossing* const& e) {
|
|
if (auto* module = event_box_.get_child(); module != nullptr) {
|
|
module->set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
|
|
}
|
|
|
|
// Default behavior indicating event availability
|
|
if (hasUserEvents_ && !config_.isMember("cursor")) {
|
|
setCursor(Gdk::HAND2);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool AModule::handleMouseLeave(GdkEventCrossing* const& e) {
|
|
if (auto* module = event_box_.get_child(); module != nullptr) {
|
|
module->unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
|
|
}
|
|
|
|
// Default behavior indicating event availability
|
|
if (hasUserEvents_ && !config_.isMember("cursor")) {
|
|
setCursor(Gdk::ARROW);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool AModule::handleToggle(GdkEventButton* const& e) { return handleUserEvent(e); }
|
|
|
|
bool AModule::handleRelease(GdkEventButton* const& e) { return handleUserEvent(e); }
|
|
|
|
bool AModule::handleUserEvent(GdkEventButton* const& e) {
|
|
std::string format{};
|
|
const std::map<std::pair<uint, GdkEventType>, std::string>::const_iterator& rec{
|
|
eventMap_.find(std::pair(e->button, e->type))};
|
|
|
|
if (rec != eventMap_.cend()) {
|
|
// First call module actions
|
|
this->AModule::doAction(rec->second);
|
|
|
|
format = rec->second;
|
|
}
|
|
|
|
// Check that a menu has been configured
|
|
if (rec != eventMap_.cend() && config_["menu"].isString()) {
|
|
// Check if the event is the one specified for the "menu" option
|
|
if (rec->second == config_["menu"].asString() && menu_ != nullptr) {
|
|
// Popup the menu
|
|
gtk_widget_show_all(GTK_WIDGET(menu_));
|
|
gtk_menu_popup_at_pointer(GTK_MENU(menu_), reinterpret_cast<GdkEvent*>(e));
|
|
// Manually reset prelight to make sure the module doesn't stay in a hover state
|
|
if (auto* module = event_box_.get_child(); module != nullptr) {
|
|
module->unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
|
|
}
|
|
}
|
|
}
|
|
// Second call user scripts
|
|
if (!format.empty()) {
|
|
if (config_[format].isString())
|
|
format = config_[format].asString();
|
|
else
|
|
format.clear();
|
|
}
|
|
if (!format.empty()) {
|
|
pid_children_.push_back(util::command::forkExec(format));
|
|
}
|
|
dp.emit();
|
|
return true;
|
|
}
|
|
|
|
AModule::SCROLL_DIR AModule::getScrollDir(GdkEventScroll* e) {
|
|
// only affects up/down
|
|
bool reverse = config_["reverse-scrolling"].asBool();
|
|
bool reverse_mouse = config_["reverse-mouse-scrolling"].asBool();
|
|
|
|
// ignore reverse-scrolling if event comes from a mouse wheel
|
|
GdkDevice* device = gdk_event_get_source_device((GdkEvent*)e);
|
|
if (device != nullptr && gdk_device_get_source(device) == GDK_SOURCE_MOUSE) {
|
|
reverse = reverse_mouse;
|
|
}
|
|
|
|
switch (e->direction) {
|
|
case GDK_SCROLL_UP:
|
|
return reverse ? SCROLL_DIR::DOWN : SCROLL_DIR::UP;
|
|
case GDK_SCROLL_DOWN:
|
|
return reverse ? SCROLL_DIR::UP : SCROLL_DIR::DOWN;
|
|
case GDK_SCROLL_LEFT:
|
|
return SCROLL_DIR::LEFT;
|
|
case GDK_SCROLL_RIGHT:
|
|
return SCROLL_DIR::RIGHT;
|
|
case GDK_SCROLL_SMOOTH: {
|
|
SCROLL_DIR dir{SCROLL_DIR::NONE};
|
|
|
|
distance_scrolled_y_ += e->delta_y;
|
|
distance_scrolled_x_ += e->delta_x;
|
|
|
|
gdouble threshold = 0;
|
|
if (config_["smooth-scrolling-threshold"].isNumeric()) {
|
|
threshold = config_["smooth-scrolling-threshold"].asDouble();
|
|
}
|
|
|
|
if (distance_scrolled_y_ < -threshold) {
|
|
dir = reverse ? SCROLL_DIR::DOWN : SCROLL_DIR::UP;
|
|
} else if (distance_scrolled_y_ > threshold) {
|
|
dir = reverse ? SCROLL_DIR::UP : SCROLL_DIR::DOWN;
|
|
} else if (distance_scrolled_x_ > threshold) {
|
|
dir = SCROLL_DIR::RIGHT;
|
|
} else if (distance_scrolled_x_ < -threshold) {
|
|
dir = SCROLL_DIR::LEFT;
|
|
}
|
|
|
|
switch (dir) {
|
|
case SCROLL_DIR::UP:
|
|
case SCROLL_DIR::DOWN:
|
|
distance_scrolled_y_ = 0;
|
|
break;
|
|
case SCROLL_DIR::LEFT:
|
|
case SCROLL_DIR::RIGHT:
|
|
distance_scrolled_x_ = 0;
|
|
break;
|
|
case SCROLL_DIR::NONE:
|
|
break;
|
|
}
|
|
|
|
return dir;
|
|
}
|
|
// Silence -Wreturn-type:
|
|
default:
|
|
return SCROLL_DIR::NONE;
|
|
}
|
|
}
|
|
|
|
bool AModule::handleScroll(GdkEventScroll* e) {
|
|
auto dir = getScrollDir(e);
|
|
std::string eventName{};
|
|
|
|
if (dir == SCROLL_DIR::UP)
|
|
eventName = "on-scroll-up";
|
|
else if (dir == SCROLL_DIR::DOWN)
|
|
eventName = "on-scroll-down";
|
|
else if (dir == SCROLL_DIR::LEFT)
|
|
eventName = "on-scroll-left";
|
|
else if (dir == SCROLL_DIR::RIGHT)
|
|
eventName = "on-scroll-right";
|
|
|
|
// First call module actions
|
|
this->AModule::doAction(eventName);
|
|
// Second call user scripts
|
|
if (config_[eventName].isString())
|
|
pid_children_.push_back(util::command::forkExec(config_[eventName].asString()));
|
|
|
|
dp.emit();
|
|
return true;
|
|
}
|
|
|
|
bool AModule::tooltipEnabled() const { return isTooltip; }
|
|
bool AModule::expandEnabled() const { return isExpand; }
|
|
|
|
AModule::operator Gtk::Widget&() { return event_box_; }
|
|
|
|
} // namespace waybar
|