Skip to content

Commit d8ea369

Browse files
committed
waybar: expose accessible buttons for interactive modules
Expose clickable label modules and tray items as accessible buttons instead of generic containers. This adds the accessibility metadata and action wiring needed for a11y AT-SPI to recognize interactive Waybar elements consistently.
1 parent 4c105f7 commit d8ea369

7 files changed

Lines changed: 130 additions & 21 deletions

File tree

include/ALabel.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <gtkmm/button.h>
34
#include <glibmm/markup.h>
45
#include <gtkmm/label.h>
56
#include <json/json.h>
@@ -20,6 +21,7 @@ class ALabel : public AModule {
2021

2122
protected:
2223
Gtk::Label label_;
24+
Gtk::Button accessible_button_;
2325
std::string format_;
2426
const std::chrono::milliseconds interval_;
2527
bool alt_ = false;

include/AModule.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ class AModule : public IModule {
4242
Gtk::EventBox event_box_;
4343

4444
virtual void setCursor(Gdk::CursorType const& c);
45+
void setupAccessibleButton(Gtk::Button& button);
46+
bool usesAccessibleButton() const;
4547

4648
virtual bool handleToggle(GdkEventButton* const& ev);
4749
virtual bool handleMouseEnter(GdkEventCrossing* const& ev);

include/modules/sni/item.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <dbus-status-notifier-item.h>
44
#include <giomm/dbusproxy.h>
55
#include <glibmm/refptr.h>
6+
#include <gtkmm/button.h>
67
#include <gtkmm/eventbox.h>
78
#include <gtkmm/icontheme.h>
89
#include <gtkmm/image.h>
@@ -39,6 +40,7 @@ class Item : public sigc::trackable {
3940
int icon_size;
4041
int effective_icon_size;
4142
Gtk::Image image;
43+
Gtk::Button button;
4244
Gtk::EventBox event_box;
4345
std::string category;
4446
std::string id;
@@ -89,6 +91,7 @@ class Item : public sigc::trackable {
8991
double getScaledIconSize();
9092
static void onMenuDestroyed(Item* self, GObject* old_menu_pointer);
9193
void makeMenu();
94+
void updateAccessibleName();
9295
bool handleClick(GdkEventButton* const& /*ev*/);
9396
bool handleScroll(GdkEventScroll* const&);
9497
bool handleMouseEnter(GdkEventCrossing* const&);

src/AIconLabel.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ AIconLabel::AIconLabel(const Json::Value& config, const std::string& name, const
5252
box_.add(image_);
5353
}
5454

55-
event_box_.add(box_);
55+
auto& container =
56+
usesAccessibleButton() ? static_cast<Gtk::Bin&>(accessible_button_) : static_cast<Gtk::Bin&>(event_box_);
57+
container.remove();
58+
container.add(box_);
5659
}
5760

5861
auto AIconLabel::update() -> void {

src/ALabel.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st
3434
label_.get_style_context()->add_class(id);
3535
}
3636
label_.get_style_context()->add_class(MODULE_CLASS);
37-
event_box_.add(label_);
3837
if (config_["max-length"].isUInt()) {
3938
label_.set_max_width_chars(config_["max-length"].asInt());
4039
label_.set_ellipsize(Pango::EllipsizeMode::ELLIPSIZE_END);
@@ -135,6 +134,14 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st
135134
label_.set_justify(Gtk::Justification::JUSTIFY_CENTER);
136135
}
137136
}
137+
138+
if (usesAccessibleButton()) {
139+
setupAccessibleButton(accessible_button_);
140+
accessible_button_.add(label_);
141+
event_box_.add(accessible_button_);
142+
} else {
143+
event_box_.add(label_);
144+
}
138145
}
139146

140147
auto ALabel::update() -> void { AModule::update(); }

src/AModule.cpp

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,10 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std::
4545
config[eventEntry.second].isString();
4646
}) != eventMap_.cend();
4747

48-
if (enable_click || hasUserEvents) {
49-
hasUserEvents_ = true;
48+
hasUserEvents_ = enable_click || hasUserEvents;
49+
if (hasUserEvents_) {
5050
event_box_.add_events(Gdk::BUTTON_PRESS_MASK);
5151
event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &AModule::handleToggle));
52-
} else {
53-
hasUserEvents_ = false;
5452
}
5553

5654
bool hasReleaseEvent =
@@ -59,13 +57,14 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std::
5957
return eventEntry.first.second == GdkEventType::GDK_BUTTON_RELEASE &&
6058
config[eventEntry.second].isString();
6159
}) != eventMap_.cend();
60+
bool hasScrollEvents = config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString() ||
61+
config_["on-scroll-left"].isString() ||
62+
config_["on-scroll-right"].isString() || enable_scroll;
6263
if (hasReleaseEvent) {
6364
event_box_.add_events(Gdk::BUTTON_RELEASE_MASK);
6465
event_box_.signal_button_release_event().connect(sigc::mem_fun(*this, &AModule::handleRelease));
6566
}
66-
if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString() ||
67-
config_["on-scroll-left"].isString() || config_["on-scroll-right"].isString() ||
68-
enable_scroll) {
67+
if (hasScrollEvents) {
6968
event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
7069
event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &AModule::handleScroll));
7170
}
@@ -127,6 +126,40 @@ void AModule::setCursor(Gdk::CursorType const& c) {
127126
}
128127
}
129128

129+
void AModule::setupAccessibleButton(Gtk::Button& button) {
130+
if (!usesAccessibleButton()) {
131+
return;
132+
}
133+
134+
auto eventMask = event_box_.get_events();
135+
button.set_relief(Gtk::RELIEF_NONE);
136+
button.set_focus_on_click(false);
137+
button.get_style_context()->add_class("flat");
138+
139+
if (hasUserEvents_) {
140+
button.add_events(Gdk::BUTTON_PRESS_MASK);
141+
button.signal_button_press_event().connect(sigc::mem_fun(*this, &AModule::handleToggle), false);
142+
}
143+
144+
if (eventMask & Gdk::BUTTON_RELEASE_MASK) {
145+
button.add_events(Gdk::BUTTON_RELEASE_MASK);
146+
button.signal_button_release_event().connect(sigc::mem_fun(*this, &AModule::handleRelease), false);
147+
}
148+
149+
if (eventMask & (Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK)) {
150+
button.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
151+
button.signal_scroll_event().connect(sigc::mem_fun(*this, &AModule::handleScroll), false);
152+
}
153+
}
154+
155+
bool AModule::usesAccessibleButton() const {
156+
if (hasUserEvents_) {
157+
return true;
158+
}
159+
160+
return (event_box_.get_events() & Gdk::BUTTON_RELEASE_MASK) != 0;
161+
}
162+
130163
bool AModule::handleMouseEnter(GdkEventCrossing* const& e) {
131164
if (auto* module = event_box_.get_child(); module != nullptr) {
132165
module->set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);

src/modules/sni/item.cpp

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#include "modules/sni/item.hpp"
22

3+
#include <atk/atk.h>
34
#include <gdkmm/general.h>
45
#include <glibmm/main.h>
6+
#include <gtkmm/cssprovider.h>
57
#include <gtkmm/tooltip.h>
68
#include <spdlog/spdlog.h>
79

@@ -38,6 +40,33 @@ namespace waybar::modules::SNI {
3840
static const Glib::ustring SNI_INTERFACE_NAME = sn_item_interface_info()->name;
3941
static const unsigned UPDATE_DEBOUNCE_TIME = 10;
4042

43+
namespace {
44+
Glib::RefPtr<Gtk::CssProvider> trayButtonCssProvider() {
45+
static auto provider = [] {
46+
auto css = Gtk::CssProvider::create();
47+
css->load_from_data(R"CSS(
48+
.waybar-accessible-tray-button {
49+
padding: 0;
50+
margin: 0;
51+
min-width: 0;
52+
min-height: 0;
53+
border-width: 0;
54+
border-style: none;
55+
border-radius: 0;
56+
box-shadow: none;
57+
}
58+
59+
.waybar-accessible-tray-button > image {
60+
padding: 0;
61+
margin: 0;
62+
}
63+
)CSS");
64+
return css;
65+
}();
66+
return provider;
67+
}
68+
} // namespace
69+
4170
Item::Item(const std::string& bn, const std::string& op, const Json::Value& config, const Bar& bar,
4271
const std::function<void(Item&)>& on_ready,
4372
const std::function<void(Item&)>& on_invalidate, const std::function<void()>& on_updated)
@@ -62,12 +91,22 @@ Item::Item(const std::string& bn, const std::string& op, const Json::Value& conf
6291

6392
auto& window = const_cast<Bar&>(bar).window;
6493
window.signal_configure_event().connect_notify(sigc::mem_fun(*this, &Item::onConfigure));
65-
event_box.add(image);
66-
event_box.add_events(Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
67-
event_box.signal_button_press_event().connect(sigc::mem_fun(*this, &Item::handleClick));
68-
event_box.signal_scroll_event().connect(sigc::mem_fun(*this, &Item::handleScroll));
69-
event_box.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Item::handleMouseEnter));
70-
event_box.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Item::handleMouseLeave));
94+
button.set_relief(Gtk::RELIEF_NONE);
95+
button.set_border_width(0);
96+
button.set_focus_on_click(false);
97+
button.set_can_focus(false);
98+
button.get_style_context()->add_class("flat");
99+
button.get_style_context()->add_class("waybar-accessible-tray-button");
100+
button.get_style_context()->add_provider(trayButtonCssProvider(),
101+
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
102+
button.add(image);
103+
event_box.add(button);
104+
button.add_events(Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK |
105+
Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
106+
button.signal_button_press_event().connect(sigc::mem_fun(*this, &Item::handleClick));
107+
button.signal_scroll_event().connect(sigc::mem_fun(*this, &Item::handleScroll));
108+
button.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Item::handleMouseEnter));
109+
button.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Item::handleMouseLeave));
71110
// initial visibility
72111
event_box.show_all();
73112
event_box.set_visible(show_passive_);
@@ -182,11 +221,10 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) {
182221
} else {
183222
setCustomIcon(id);
184223
}
224+
updateAccessibleName();
185225
} else if (name == "Title") {
186226
title = get_variant<std::string>(value);
187-
if (tooltip.text.empty()) {
188-
event_box.set_tooltip_markup(title);
189-
}
227+
updateAccessibleName();
190228
} else if (name == "Status") {
191229
setStatus(get_variant<Glib::ustring>(value));
192230
} else if (name == "IconName") {
@@ -205,9 +243,7 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) {
205243
attention_movie_name = get_variant<std::string>(value);
206244
} else if (name == "ToolTip") {
207245
tooltip = get_variant<ToolTip>(value);
208-
if (!tooltip.text.empty()) {
209-
event_box.set_tooltip_markup(tooltip.text);
210-
}
246+
updateAccessibleName();
211247
} else if (name == "IconThemePath") {
212248
icon_theme_path = get_variant<std::string>(value);
213249
if (!icon_theme_path.empty()) {
@@ -260,6 +296,29 @@ void Item::invalidate() {
260296
on_invalidate_(*this);
261297
}
262298

299+
void Item::updateAccessibleName() {
300+
if (!tooltip.text.empty()) {
301+
button.set_tooltip_markup(tooltip.text);
302+
} else if (!title.empty()) {
303+
button.set_tooltip_markup(title);
304+
}
305+
306+
std::string accessible_name;
307+
if (!title.empty()) {
308+
accessible_name = title;
309+
} else if (!id.empty()) {
310+
accessible_name = id;
311+
} else if (!tooltip.text.empty()) {
312+
accessible_name = tooltip.text.raw();
313+
} else {
314+
accessible_name = bus_name;
315+
}
316+
317+
if (auto* accessible = gtk_widget_get_accessible(GTK_WIDGET(button.gobj())); accessible != nullptr) {
318+
atk_object_set_name(accessible, accessible_name.c_str());
319+
}
320+
}
321+
263322
void Item::setCustomIcon(const std::string& id) {
264323
spdlog::debug("SNI tray id: {}", id);
265324

0 commit comments

Comments
 (0)