Files
Waybar/src/AModule.cpp
Austin Horstman f48fce57dc fix(menu): keep popup menus alive after builder teardown
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>
2026-03-06 18:39:16 -06:00

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