ui: sidebar tab selector uses text labels instead of icons
The two drawn tab glyphs were judged not understandable enough; the switcher goes to the wide policy with titles only, and the glyphs, their gresource entries and the icon-naming code are removed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
b6822cd32d
commit
16e6d7fa75
15 files changed
+65
-291
No files matched your search
+18
-108
@@ -2975,64 +2975,6 @@ std::string icon_report(GtkIconTheme* theme, const char* name, const char* resou
|
||||
/// and Reset is inside the cursors pane. Recorded on the first tick -- the widgets exist and the
|
||||
/// window is alive, neither of which is true when the dump runs -- so the dump can print a fact
|
||||
/// instead of asking GTK about a destroyed widget.
|
||||
/// The words the pills do not draw. The pages' `title` is empty on purpose (see the `.ui`), so the
|
||||
/// human name of a tab lives here and is put on the pill — its tooltip and its accessible name — by
|
||||
/// `name_side_tabs()`. That keeps an icon-only pill a *named* control for a screen reader.
|
||||
static const char* side_tab_title(const App& app, const char* name) {
|
||||
if (name != nullptr && std::strcmp(name, "measure") == 0) return "Measure";
|
||||
if (name != nullptr && std::strcmp(name, "signals") == 0) return "Signals";
|
||||
if (name != nullptr) {
|
||||
for (const SourcePanelPage& p : app.source_panels) {
|
||||
if (p.name == name) return p.title.c_str();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/// Names the switcher's pills and takes their empty labels out of the layout: an empty `GtkLabel`
|
||||
/// still asks for its own width, which leaves the icon off-centre in a pill meant to be one glyph.
|
||||
/// Called from the first tick, because the pills are `AdwViewSwitcher`'s own and exist only once the
|
||||
/// widget has been mapped.
|
||||
void name_side_tabs(App& app) {
|
||||
if (app.side_tabs_named || app.side_switcher == nullptr) return;
|
||||
GListModel* pages = app.side_stack != nullptr
|
||||
? G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))
|
||||
: nullptr;
|
||||
// The switcher's children *are* its pills, in page order, and `AdwViewSwitcherButton` is not a
|
||||
// public type (there is no `ADW_IS_VIEW_SWITCHER_BUTTON`), so the two lists are walked together by
|
||||
// index rather than checked by type.
|
||||
GtkWidget* pill = gtk_widget_get_first_child(app.side_switcher);
|
||||
for (guint i = 0; pages != nullptr && pill != nullptr && i < g_list_model_get_n_items(pages);
|
||||
pill = gtk_widget_get_next_sibling(pill), ++i) {
|
||||
AdwViewStackPage* page = ADW_VIEW_STACK_PAGE(g_list_model_get_item(pages, i));
|
||||
const char* title = side_tab_title(app, adw_view_stack_page_get_name(page));
|
||||
g_object_unref(page);
|
||||
if (title[0] == '\0') continue;
|
||||
gtk_widget_set_tooltip_text(pill, title);
|
||||
gtk_accessible_update_property(GTK_ACCESSIBLE(pill), GTK_ACCESSIBLE_PROPERTY_LABEL, title, -1);
|
||||
const std::function<void(GtkWidget*)> hide_labels = [&](GtkWidget* w) {
|
||||
if (GTK_IS_LABEL(w)) gtk_widget_set_visible(w, FALSE);
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
hide_labels(c);
|
||||
}
|
||||
};
|
||||
hide_labels(pill);
|
||||
// The tabs' glyphs are 32x16 documents, and the pill has the width for them: drawn at 32 they are
|
||||
// 1:1, so both motifs sit side by side at the size they were authored at and the 2px bars stay
|
||||
// crisp. A 16px box would scale the pair down to a smear, and a fractional scale would blur it.
|
||||
const std::function<void(GtkWidget*)> size_images = [&](GtkWidget* w) {
|
||||
if (GTK_IS_IMAGE(w)) gtk_image_set_pixel_size(GTK_IMAGE(w), 32);
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
size_images(c);
|
||||
}
|
||||
};
|
||||
size_images(pill);
|
||||
app.side_tabs_named = true;
|
||||
}
|
||||
}
|
||||
|
||||
void note_panel_controls(App& app) {
|
||||
if (!app.dbg_panel_controls.empty()) return;
|
||||
const auto in_bar = [](GtkWidget* w) {
|
||||
@@ -3110,39 +3052,17 @@ void note_panel_controls(App& app) {
|
||||
icon_report(theme, "org.udpscope.app-symbolic",
|
||||
"/org/udpscope/icons/scalable/apps/org.udpscope.app-symbolic.svg");
|
||||
|
||||
// The two sidebar pages' own icons, named and resolved the same way, and what the switcher is
|
||||
// drawing for them. The pills are `AdwViewSwitcher`'s own buttons, built when the stack is mapped,
|
||||
// so this reads on the first tick: `pills` is the guard (0 pills would make a label count of 0
|
||||
// meaningless) and `labels` must be 0, because the switcher's policy is `narrow` -- the pages are
|
||||
// icons, not words, in a 300px column.
|
||||
// Walked through `get_pages()`, not the widget tree: an `AdwViewStackPage` is a plain `GObject`
|
||||
// (only its `child` is a widget), so `gtk_widget_get_first_child()` on the stack yields the pages'
|
||||
// scrolled windows and this line read empty the first time it was written.
|
||||
GListModel* pages = app.side_stack != nullptr
|
||||
? G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))
|
||||
: nullptr;
|
||||
for (guint i = 0; pages != nullptr && i < g_list_model_get_n_items(pages); ++i) {
|
||||
AdwViewStackPage* page = ADW_VIEW_STACK_PAGE(g_list_model_get_item(pages, i));
|
||||
const char* page_name = adw_view_stack_page_get_name(page);
|
||||
const char* icon = adw_view_stack_page_get_icon_name(page);
|
||||
app.dbg_page_icons += std::string(app.dbg_page_icons.empty() ? "" : " ") +
|
||||
(page_name != nullptr ? page_name : "?") + "=" +
|
||||
(icon != nullptr ? icon : "-") +
|
||||
(icon != nullptr && gtk_icon_theme_has_icon(theme, icon) ? "(ok)"
|
||||
: "(MISSING)");
|
||||
g_object_unref(page);
|
||||
}
|
||||
// `mapped`, not `visible`: visibility is per widget, so a label whose *parent* is hidden still
|
||||
// reads visible, and in the narrow policy that is exactly the case. `mapped` requires every
|
||||
// ancestor, which is what "on screen" means -- and it is why the pill count is read the same way:
|
||||
// if nothing is mapped the pill count reads 0 and the label count means nothing, rather than
|
||||
// passing vacuously.
|
||||
// What the switcher is drawing: the sidebar's tabs are words (`wide` policy, a title per page, no
|
||||
// icon). The pills are `AdwViewSwitcher`'s own buttons, built when the stack is mapped, so this
|
||||
// reads on the first tick. `mapped`, not `visible`: visibility is per widget, so a label whose
|
||||
// *parent* is hidden still reads visible; `mapped` requires every ancestor, which is what "on
|
||||
// screen" means -- and it is why the pill count is read the same way: if nothing is mapped the
|
||||
// pill count reads 0 and the label count means nothing, rather than passing vacuously.
|
||||
int pills = 0;
|
||||
int drawn_labels = 0;
|
||||
const std::function<void(GtkWidget*)> count_drawn = [&](GtkWidget* w) {
|
||||
if (GTK_IS_TOGGLE_BUTTON(w) && gtk_widget_get_mapped(w)) ++pills;
|
||||
// A *mapped* label with text: libadwaita's narrow pill keeps an empty label in the layout, so
|
||||
// "mapped" alone would count the empty ones and read 2 whether the pills were icons or words.
|
||||
// A *mapped* label with text: an empty label would be counted by "mapped" alone.
|
||||
if (GTK_IS_LABEL(w) && gtk_widget_get_mapped(w) && gtk_label_get_text(GTK_LABEL(w))[0] != '\0') {
|
||||
++drawn_labels;
|
||||
}
|
||||
@@ -3154,14 +3074,12 @@ void note_panel_controls(App& app) {
|
||||
if (app.side_switcher != nullptr) count_drawn(app.side_switcher);
|
||||
app.dbg_switcher_pills = pills;
|
||||
app.dbg_switcher_labels = drawn_labels;
|
||||
int named_pills = 0;
|
||||
for (GtkWidget* w = app.side_switcher != nullptr ? gtk_widget_get_first_child(app.side_switcher)
|
||||
: nullptr;
|
||||
w != nullptr; w = gtk_widget_get_next_sibling(w)) {
|
||||
const char* tip = gtk_widget_get_tooltip_text(w);
|
||||
if (tip != nullptr && tip[0] != '\0') ++named_pills;
|
||||
}
|
||||
app.dbg_switcher_names = named_pills;
|
||||
// Walked through `get_pages()`, not the widget tree: an `AdwViewStackPage` is a plain `GObject`.
|
||||
app.dbg_switcher_pages =
|
||||
app.side_stack != nullptr
|
||||
? static_cast<int>(g_list_model_get_n_items(
|
||||
G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))))
|
||||
: 0;
|
||||
if (app.side_switcher != nullptr) {
|
||||
g_object_get(G_OBJECT(app.side_switcher), "policy", &app.dbg_switcher_policy, nullptr);
|
||||
}
|
||||
@@ -3456,11 +3374,6 @@ void apply_density(App& app, DisplayMode display, bool live_display) {
|
||||
gboolean tick(gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (app->window == nullptr) return G_SOURCE_REMOVE;
|
||||
// Before the reading, and not after it: the pills are `AdwViewSwitcher`'s own widgets and exist
|
||||
// only once the switcher is mapped, so this is the first frame that can name them -- and their
|
||||
// page titles are deliberately empty, so a frame that read before this ran would read pills with
|
||||
// no text and no name either.
|
||||
name_side_tabs(*app);
|
||||
note_panel_controls(*app);
|
||||
sync_source_panels(*app);
|
||||
// The tour, re-measured every frame: the target's allocation is 0 on the frame it is first shown
|
||||
@@ -5289,8 +5202,8 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
g_signal_connect(app->side_stack, "notify::visible-child-name",
|
||||
G_CALLBACK(on_side_tab_changed), app);
|
||||
}
|
||||
// The source kind's own tabs (a generator, digital I/O). Before the first tick: that is when
|
||||
// `name_side_tabs()` puts the words on the pills, and it must see these pages too.
|
||||
// The source kind's own tabs (a generator, digital I/O), added before the first tick so the
|
||||
// pages-versus-pills reading below counts them too.
|
||||
build_source_panels(*app);
|
||||
|
||||
// The left side is the events pane *and* the plot, in one row: the pane is a column of its own
|
||||
@@ -6324,12 +6237,10 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
app.dbg_panel_switch_trigger, app.dbg_toolbar_switch_trigger,
|
||||
app.dbg_panel_switch_cursors, app.dbg_toolbar_switch_cursors,
|
||||
app.dbg_panel_drove_toolbar);
|
||||
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d names=%d icon=%dx%d "
|
||||
"size=%dx%d\n",
|
||||
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d pages=%d size=%dx%d\n",
|
||||
app.dbg_switcher_policy == 0 ? "narrow" : (app.dbg_switcher_policy == 1 ? "wide" : "?"),
|
||||
app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_names,
|
||||
app.dbg_switcher_icon_w, app.dbg_switcher_icon_h, app.dbg_switcher_w,
|
||||
app.dbg_switcher_h);
|
||||
app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_pages,
|
||||
app.dbg_switcher_w, app.dbg_switcher_h);
|
||||
// The help affordance layer: what the contextual-help toggle left on the surfaces, and where the
|
||||
// guided tour stood when the last capture ran. Both are captured on the tick (`capture_help_state`),
|
||||
// so neither asks a destroyed widget anything.
|
||||
@@ -6463,7 +6374,6 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
// critical (which is how the first version of this line earned four of them).
|
||||
std::printf("[udpscope] panel controls: %s\n", app.dbg_panel_controls.c_str());
|
||||
std::printf("[udpscope] toolbar icons: %s\n", app.dbg_toolbar_icons.c_str());
|
||||
std::printf("[udpscope] page icons: %s\n", app.dbg_page_icons.c_str());
|
||||
std::printf("[udpscope] app icons: %s\n", app.dbg_app_icons.c_str());
|
||||
// The measurements pane, with the numbers the engine computed: a probe adds measurements and
|
||||
// this is where their values are read, so a wrong threshold or a window that moved under a
|
||||
|
||||
Reference in new issue
Block a user