ui: the source chooser's window is a .ui file, and its pages an AdwViewStack
The last hand-built UI outside ui.cpp. The window, its header bar, its two pages and the four buttons that are about the chooser rather than about a source now live in src/ui/source-chooser.ui; the rows stay in code because a source row and a waveform row are data, rebuilt whenever the known hosts or the recent files change. The pages were a GtkNotebook and are an AdwViewStack under an AdwViewSwitcherBar, which is what current libadwaita asks for and the same information without a tab strip. The three actions moved from a button row at the foot into the header bar, the shape source-form.ui already uses -- and deliberately without receives-default, since Enter must keep activating the selected row. Nothing carries a value: the tooltips and labels moved as they were, the handlers are the same functions, and the switch is by page *name* rather than by index. The probe reads the frame back -- `[sources] chooser ui: page='sources' title='Sources' icon=yes page='waveforms' … switcher_wired=1 buttons=[prefs=1 new_host=1 open=1 cancel=1]` -- so a .ui that lost an id is named instead of opening a window with a hole in it, and `check_ui` type-checks the file against the GIR (7 ok, 0 failed). Two things the checks caught while doing it: the new frame dropped the call that fills the waveforms page (the unused-function warning), and `AdwViewStackPage` is a GObject that *wraps* a child rather than being one, so reading the pages needs adw_view_stack_get_page().
This commit is contained in:
1 parent
723f5c3b4c
commit
0a64e9e854
5 files changed
+216
-80
No files matched your search
+91
-77
@@ -69,10 +69,25 @@ struct Chooser {
|
||||
GtkWidget* window = nullptr;
|
||||
GtkWidget* list = nullptr;
|
||||
std::vector<KnownSource> known;
|
||||
/// The Waveforms page: the exports opened before, newest first, and the notebook that holds the
|
||||
/// two pages. A waveform is the *other* answer to "which source", so it gets a page of its own
|
||||
/// rather than a button that hides a file dialog behind one more click.
|
||||
GtkWidget* notebook = nullptr;
|
||||
/// The window's frame is a `.ui` file (`src/ui/source-chooser.ui`): the window, its header bar, the two
|
||||
/// pages and the buttons that are about the chooser rather than about a source. The builder is held
|
||||
/// with the window for the same reason the form's is -- the window belongs to the builder's object list.
|
||||
GtkBuilder* ui = nullptr;
|
||||
/// The Waveforms page: the exports opened before, newest first, and the stack that holds the two pages.
|
||||
/// A waveform is the *other* answer to "which source", so it gets a page of its own rather than a button
|
||||
/// that hides a file dialog behind one more click. A `GtkNotebook` was the first shape of this and a tab
|
||||
/// strip is the wrong one: an `AdwViewStack` under an `AdwViewSwitcherBar` is what current libadwaita
|
||||
/// asks for, and the switcher's stack is wired in code because a property cannot name a widget declared
|
||||
/// elsewhere in the same tree.
|
||||
GtkWidget* page_stack = nullptr;
|
||||
GtkWidget* switcher = nullptr;
|
||||
/// The buttons the header bar holds, kept because two of them are read at runtime: `new_host` takes the
|
||||
/// focus when the list is empty (there is nothing else to do), and all of them are read back by
|
||||
/// `report_chooser_ui()` so a `.ui` that lost one is named rather than silently missing its button.
|
||||
GtkWidget* prefs_button = nullptr;
|
||||
GtkWidget* new_host = nullptr;
|
||||
GtkWidget* waves_open = nullptr;
|
||||
GtkWidget* cancel_button = nullptr;
|
||||
GtkWidget* wave_list = nullptr;
|
||||
std::vector<std::string> waves;
|
||||
/// The connection form, created on demand. Members of the chooser rather than a heap struct:
|
||||
@@ -136,6 +151,9 @@ void chooser_finish(Chooser& c, bool chosen) {
|
||||
c.window = nullptr; // before the destroy: `close-request` must not re-enter this
|
||||
gtk_window_destroy(GTK_WINDOW(w));
|
||||
}
|
||||
// The window belonged to the builder's object list, so it is freed with it -- after the destroy, never
|
||||
// before: a builder freed under a live window takes the window's children with it.
|
||||
g_clear_object(&c.ui);
|
||||
if (c.loop != nullptr) g_main_loop_quit(c.loop);
|
||||
}
|
||||
|
||||
@@ -582,6 +600,34 @@ void open_chooser_preferences(Chooser& c) {
|
||||
/// `UDPSCOPE_PROBE_NEWHOST=host:port` drives the form instead of a row. The crash surface of a
|
||||
/// dialog built, filled and destroyed is as much the point as the values, the same way
|
||||
/// `UDPSCOPE_PROBE_FIELDS_MS` is in `ui.cpp`.
|
||||
/// The chooser's frame, read back from the `.ui` it was built from: the two pages by name and title, the
|
||||
/// switcher wired to the stack, and the four buttons the code resolved. `[help] ui=` does the same against
|
||||
/// its tables -- the frame is what the file holds, so the frame is what is read, while the rows a user sees
|
||||
/// are read by the probes that drive them.
|
||||
void report_chooser_ui(const Chooser& c) {
|
||||
std::fprintf(stderr, "[sources] chooser ui:");
|
||||
for (GtkWidget* w = gtk_widget_get_first_child(c.page_stack); w != nullptr;
|
||||
w = gtk_widget_get_next_sibling(w)) {
|
||||
// A page is not the child: `AdwViewStackPage` is a `GObject` that *wraps* one, so the stack is what
|
||||
// turns a child back into its page.
|
||||
AdwViewStackPage* page = adw_view_stack_get_page(ADW_VIEW_STACK(c.page_stack), w);
|
||||
if (page == nullptr) continue;
|
||||
std::fprintf(stderr, " page='%s' title='%s' icon=%s", adw_view_stack_page_get_name(page),
|
||||
adw_view_stack_page_get_title(page),
|
||||
adw_view_stack_page_get_icon_name(page) != nullptr ? "yes" : "NO");
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
" visible='%s' switcher_wired=%d buttons=[prefs=%d new_host=%d open=%d cancel=%d]\n",
|
||||
adw_view_stack_get_visible_child_name(ADW_VIEW_STACK(c.page_stack)),
|
||||
adw_view_switcher_bar_get_stack(ADW_VIEW_SWITCHER_BAR(c.switcher)) ==
|
||||
ADW_VIEW_STACK(c.page_stack)
|
||||
? 1
|
||||
: 0,
|
||||
c.prefs_button != nullptr ? 1 : 0, c.new_host != nullptr ? 1 : 0,
|
||||
c.waves_open != nullptr ? 1 : 0, c.cancel_button != nullptr ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
gboolean pick_probe(gpointer user) {
|
||||
Chooser& c = *static_cast<Chooser*>(user);
|
||||
// Every path below returns `G_SOURCE_REMOVE`, so the source is gone the moment this returns and
|
||||
@@ -589,6 +635,9 @@ gboolean pick_probe(gpointer user) {
|
||||
// -probe call would otherwise be a GLib critical on every run that let the probe fire.
|
||||
c.pick_source = 0;
|
||||
|
||||
// What the `.ui` contributed, before anything is driven: the pages, the switcher and the buttons.
|
||||
report_chooser_ui(c);
|
||||
|
||||
// `UDPSCOPE_PROBE_CHOOSER_PREFS=1`: the preferences opened from *here* rather than from the app's
|
||||
// menu -- the entry point that exists because this dialog runs before `App`. It is driven through
|
||||
// the same fill-and-apply the app's probe uses, and reports what the run about to start will get,
|
||||
@@ -611,13 +660,13 @@ gboolean pick_probe(gpointer user) {
|
||||
// page selected the way a click on its tab does. `UDPSCOPE_PROBE_WAVE=<path>` is the file dialog's
|
||||
// answer without the dialog -- the same field, set the same way.
|
||||
if (g_getenv("UDPSCOPE_PROBE_WAVEFORMS") != nullptr) {
|
||||
if (c.notebook != nullptr) gtk_notebook_set_current_page(GTK_NOTEBOOK(c.notebook), 1);
|
||||
adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(c.page_stack), "waveforms");
|
||||
std::fprintf(stderr, "[sources] waveforms: %zu recent", c.waves.size());
|
||||
for (const std::string& p : c.waves) {
|
||||
std::fprintf(stderr, " %s(%s)", p.c_str(), ::access(p.c_str(), R_OK) == 0 ? "ok" : "missing");
|
||||
}
|
||||
std::fprintf(stderr, " page=%d\n",
|
||||
static_cast<int>(gtk_notebook_get_current_page(GTK_NOTEBOOK(c.notebook))));
|
||||
std::fprintf(stderr, " page=%s\n",
|
||||
adw_view_stack_get_visible_child_name(ADW_VIEW_STACK(c.page_stack)));
|
||||
std::fflush(stderr);
|
||||
}
|
||||
if (const char* path = g_getenv("UDPSCOPE_PROBE_WAVE")) {
|
||||
@@ -762,88 +811,53 @@ bool choose_source(Config& cfg) {
|
||||
|
||||
Chooser c;
|
||||
c.cfg = &cfg;
|
||||
c.window = gtk_window_new();
|
||||
gtk_window_set_title(GTK_WINDOW(c.window), "udpscope \xe2\x80\x94 choose a source");
|
||||
gtk_window_set_default_size(GTK_WINDOW(c.window), 420, 320);
|
||||
|
||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_window_set_child(GTK_WINDOW(c.window), box);
|
||||
// The frame is a `.ui` file; the rows are not, because a source row and a waveform row are data and are
|
||||
// rebuilt whenever the known hosts or the recent files change. `make test-ui` checks the file's types
|
||||
// against the GIR; this checks that the code and the file still agree on the names, which nothing else
|
||||
// can -- a missing id would otherwise open a window with a hole in it, or crash on the first read.
|
||||
c.ui = gtk_builder_new_from_resource("/org/udpscope/ui/source-chooser.ui");
|
||||
c.window = GTK_WIDGET(gtk_builder_get_object(c.ui, "chooser_window"));
|
||||
c.page_stack = GTK_WIDGET(gtk_builder_get_object(c.ui, "page_stack"));
|
||||
c.switcher = GTK_WIDGET(gtk_builder_get_object(c.ui, "page_switcher"));
|
||||
c.list = GTK_WIDGET(gtk_builder_get_object(c.ui, "sources_list"));
|
||||
c.wave_list = GTK_WIDGET(gtk_builder_get_object(c.ui, "waves_list"));
|
||||
c.prefs_button = GTK_WIDGET(gtk_builder_get_object(c.ui, "prefs_button"));
|
||||
c.new_host = GTK_WIDGET(gtk_builder_get_object(c.ui, "new_host_button"));
|
||||
c.waves_open = GTK_WIDGET(gtk_builder_get_object(c.ui, "waves_open"));
|
||||
c.cancel_button = GTK_WIDGET(gtk_builder_get_object(c.ui, "cancel_button"));
|
||||
if (c.window == nullptr || c.page_stack == nullptr || c.switcher == nullptr || c.list == nullptr ||
|
||||
c.wave_list == nullptr || c.prefs_button == nullptr || c.new_host == nullptr ||
|
||||
c.waves_open == nullptr || c.cancel_button == nullptr) {
|
||||
std::fprintf(stderr, "[sources] chooser ui: source-chooser.ui is missing an id this code reads\n");
|
||||
std::fflush(stderr);
|
||||
return false;
|
||||
}
|
||||
adw_view_switcher_bar_set_stack(ADW_VIEW_SWITCHER_BAR(c.switcher), ADW_VIEW_STACK(c.page_stack));
|
||||
|
||||
// Two pages under one window: the sources it can connect to, and the waveforms it can open. A
|
||||
// waveform is not a source with a file in place of an address -- there is no socket and nothing
|
||||
// arrives -- so it is a *page* rather than a field, which is also where its history belongs.
|
||||
c.notebook = gtk_notebook_new();
|
||||
gtk_widget_set_vexpand(c.notebook, TRUE);
|
||||
gtk_box_append(GTK_BOX(box), c.notebook);
|
||||
|
||||
GtkWidget* scroller = gtk_scrolled_window_new();
|
||||
gtk_widget_set_vexpand(scroller, TRUE);
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), GTK_POLICY_NEVER,
|
||||
GTK_POLICY_AUTOMATIC);
|
||||
c.list = gtk_list_box_new();
|
||||
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(c.list), TRUE);
|
||||
g_signal_connect(c.list, "row-activated", G_CALLBACK(on_row_activated), &c);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroller), c.list);
|
||||
gtk_notebook_append_page(GTK_NOTEBOOK(c.notebook), scroller, gtk_label_new("Sources"));
|
||||
|
||||
GtkWidget* wave_page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
GtkWidget* wave_scroller = gtk_scrolled_window_new();
|
||||
gtk_widget_set_vexpand(wave_scroller, TRUE);
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(wave_scroller), GTK_POLICY_NEVER,
|
||||
GTK_POLICY_AUTOMATIC);
|
||||
c.wave_list = gtk_list_box_new();
|
||||
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(c.wave_list), TRUE);
|
||||
g_signal_connect(c.wave_list, "row-activated", G_CALLBACK(on_wave_activated), &c);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(wave_scroller), c.wave_list);
|
||||
gtk_box_append(GTK_BOX(wave_page), wave_scroller);
|
||||
GtkWidget* wave_actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
gtk_widget_set_halign(wave_actions, GTK_ALIGN_END);
|
||||
gtk_widget_set_margin_top(wave_actions, 6);
|
||||
gtk_widget_set_margin_bottom(wave_actions, 6);
|
||||
gtk_widget_set_margin_end(wave_actions, 12);
|
||||
GtkWidget* wave_open = gtk_button_new_with_label("Open\xe2\x80\xa6");
|
||||
gtk_widget_set_tooltip_text(wave_open, "choose an exported .npy to view and analyse");
|
||||
g_signal_connect(wave_open, "clicked", G_CALLBACK(on_wave_open_clicked), &c);
|
||||
gtk_box_append(GTK_BOX(wave_actions), wave_open);
|
||||
gtk_box_append(GTK_BOX(wave_page), wave_actions);
|
||||
gtk_notebook_append_page(GTK_NOTEBOOK(c.notebook), wave_page, gtk_label_new("Waveforms"));
|
||||
chooser_populate_waves(c);
|
||||
|
||||
GtkWidget* actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
gtk_widget_set_margin_start(actions, 12);
|
||||
gtk_widget_set_margin_end(actions, 12);
|
||||
gtk_widget_set_margin_top(actions, 12);
|
||||
gtk_widget_set_margin_bottom(actions, 12);
|
||||
gtk_widget_set_halign(actions, GTK_ALIGN_END);
|
||||
// The preferences are reachable from here as well as from the main window's menu: this dialog is
|
||||
// the only window that exists before anything is planned, so it is where a user still has the
|
||||
// chance to say how this run should behave.
|
||||
GtkWidget* prefs = gtk_button_new_with_label("Preferences\xe2\x80\xa6");
|
||||
gtk_widget_set_tooltip_text(prefs,
|
||||
"the event cache, relative time and the interface's theme -- the same "
|
||||
"dialog as the main window's menu, and the only place relative time can "
|
||||
"still change this run");
|
||||
g_signal_connect(prefs, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
open_chooser_preferences(*static_cast<Chooser*>(user));
|
||||
}),
|
||||
&c);
|
||||
GtkWidget* new_host = gtk_button_new_with_label("New host\xe2\x80\xa6");
|
||||
g_signal_connect(new_host, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
g_signal_connect(c.waves_open, "clicked", G_CALLBACK(on_wave_open_clicked), &c);
|
||||
g_signal_connect(c.new_host, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
open_form(*static_cast<Chooser*>(user));
|
||||
}),
|
||||
&c);
|
||||
GtkWidget* cancel = gtk_button_new_with_label("Cancel");
|
||||
g_signal_connect(cancel, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
g_signal_connect(c.cancel_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
chooser_finish(*static_cast<Chooser*>(user), false);
|
||||
}),
|
||||
&c);
|
||||
gtk_box_append(GTK_BOX(actions), prefs);
|
||||
gtk_box_append(GTK_BOX(actions), new_host);
|
||||
gtk_box_append(GTK_BOX(actions), cancel);
|
||||
gtk_box_append(GTK_BOX(box), actions);
|
||||
// The preferences are reachable from here as well as from the main window's menu: this dialog is the
|
||||
// only window that exists before anything is planned, so it is where a user still has the chance to say
|
||||
// how this run should behave.
|
||||
g_signal_connect(c.prefs_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
open_chooser_preferences(*static_cast<Chooser*>(user));
|
||||
}),
|
||||
&c);
|
||||
|
||||
chooser_populate(c);
|
||||
if (c.known.empty()) gtk_widget_grab_focus(new_host);
|
||||
// The other page's rows too: fills the sources, and this is the pair of it.
|
||||
chooser_populate_waves(c);
|
||||
if (c.known.empty()) gtk_widget_grab_focus(c.new_host);
|
||||
|
||||
g_signal_connect(c.window, "close-request", G_CALLBACK(on_chooser_close), &c);
|
||||
GtkEventController* keys = gtk_event_controller_key_new();
|
||||
|
||||
Reference in new issue
Block a user