help: default the contextual layer off, and carry the help into the chooser
The contextual help was on by default, so a first run met a window with a "?" beside every surface it had not asked to have explained; `contextual_help` now defaults to false and the layer is asked for from the Help menu, from the chooser's new help menu, or with Ctrl+H. The source chooser runs before `App` exists, which makes it the window a user who does not know how to start is looking at, so it now carries the same three help affordances the window's Help view does: Documentation (the manual is another process and needs nothing of the app), Enable contextual help (a row whose check *is* the setting, written to the file the app reads a moment later), and Guided tour. The tour is drawn on the app's own window, so the chooser can only record the wish: it sets `Config::tour_pending` -- the value `main()` owns and hands to both `choose_source()` and `run_ui()` -- and `on_activate()` starts it once the window is up and laid out, through its own callback so the readings can tell a request from the first-run auto-start. The rows are the window's own `menu_button()`/`menu_toggle()`; `install_css()` therefore runs from `choose_source()` (it is idempotent now) so the popover gets the `.udpscope-menu` rules before it is shown. Opening the manual could also fail with a message that blamed the handler whatever was missing. `help_installed_path()` now resolves `help:<id>` the way the specification does and the failure alert says which of the two it is: with the set installed it names the viewer, without it it names the identifier, the expected installed path and the install command. `show_help()` gained an optional reporter, which `UDPSCOPE_PROBE_HELP_MS` uses for a new reading -- `[help] launch attempted= done= ok= error='' installed=` -- since a successful launch leaves no window of ours to observe. Tested: clean --clean-first build, three warnings as documented; test-ui 13/13 and test-help pass; no Gtk-CRITICAL in the help, helpui and chooser runs.
This commit is contained in:
1 parent
e140642ed0
commit
d8a79b96ba
15 files changed
+461
-66
No files matched your search
+132
-6
@@ -5,8 +5,13 @@
|
||||
|
||||
#include "waveform.hpp"
|
||||
|
||||
#include "help_window.hpp"
|
||||
#include "layout_store.hpp"
|
||||
#include "settings.hpp"
|
||||
// The app's shared chrome -- `ui_impl::menu_button()`/`menu_toggle()` and `install_css()` -- so the
|
||||
// chooser's help menu is the *same* menu the window's Help view is, not a second one that merely
|
||||
// looks like it. Heavy header, but a row shape copied here would be a row shape that drifts.
|
||||
#include "ui_app.hpp"
|
||||
#include "ui_prefs.hpp"
|
||||
|
||||
#include <adwaita.h>
|
||||
@@ -127,6 +132,15 @@ struct Chooser {
|
||||
/// The preferences, for the button below. Same reasoning as `form` above: a member of the chooser
|
||||
/// rather than a heap struct, so nothing can outlive the stack frame that owns it.
|
||||
ui_impl::PrefsForm prefs;
|
||||
/// The help menu: the `.ui`'s own `GtkMenuButton` and the three rows built onto it. The rows are
|
||||
/// code for the same reason every other row here is -- each carries a handler, and the toggle
|
||||
/// carries the value the settings file holds -- while the button that opens them is the file's, so
|
||||
/// a `.ui` that lost it is caught by the null check below rather than by an empty popover.
|
||||
GtkWidget* help_button = nullptr;
|
||||
GtkWidget* help_docs = nullptr;
|
||||
GtkWidget* help_tour = nullptr;
|
||||
GtkWidget* help_toggle = nullptr;
|
||||
GtkWidget* help_popover = nullptr;
|
||||
|
||||
};
|
||||
|
||||
@@ -596,14 +610,90 @@ void open_chooser_preferences(Chooser& c) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The chooser's help menu: the same three affordances the window's Help view carries, because a
|
||||
/// user who has not started yet is exactly the one who does not know how to start. The rows are
|
||||
/// `ui_impl::menu_button()`/`menu_toggle()` -- the app's own menu-row shape, so the two menus read
|
||||
/// as one program -- and the popover carries the same `.udpscope-menu` class, which is what trims
|
||||
/// the rows; `install_css()` therefore runs before this, in `choose_source()`.
|
||||
///
|
||||
/// No row shows an accelerator. The window's Help rows show "F1"/"Ctrl+T" because the window handles
|
||||
/// those keys; this dialog binds none of them, and a shortcut printed beside an action that does not
|
||||
/// answer to it is the "documented and does nothing" failure the shortcut tables are checked for.
|
||||
void build_chooser_help(Chooser& c) {
|
||||
c.help_popover = gtk_popover_new();
|
||||
gtk_widget_add_css_class(c.help_popover, "udpscope-menu");
|
||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_set_margin_top(box, 6);
|
||||
gtk_widget_set_margin_bottom(box, 6);
|
||||
gtk_widget_set_margin_start(box, 6);
|
||||
gtk_widget_set_margin_end(box, 6);
|
||||
|
||||
c.help_docs = ui_impl::menu_button("Documentation", nullptr);
|
||||
gtk_widget_set_tooltip_text(c.help_docs,
|
||||
"the udpscope manual, opened in the desktop help viewer");
|
||||
g_signal_connect(c.help_docs, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
Chooser& ch = *static_cast<Chooser*>(user);
|
||||
gtk_popover_popdown(GTK_POPOVER(ch.help_popover));
|
||||
// No window of the app exists yet: the manual is a separate process reached
|
||||
// through the `help:` URI, so this needs no more of the app than the chooser
|
||||
// itself. The parent may be null, and this dialog is the right attribution.
|
||||
show_help(GTK_WINDOW(ch.window));
|
||||
}),
|
||||
&c);
|
||||
gtk_box_append(GTK_BOX(box), c.help_docs);
|
||||
|
||||
c.help_tour = ui_impl::menu_button("Guided tour", nullptr);
|
||||
gtk_widget_set_tooltip_text(c.help_tour,
|
||||
"walk over the plot, the side panel, the trigger, the signals and "
|
||||
"the status line, once the window is up");
|
||||
g_signal_connect(c.help_tour, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
Chooser& ch = *static_cast<Chooser*>(user);
|
||||
gtk_popover_popdown(GTK_POPOVER(ch.help_popover));
|
||||
// The tour lives on the window the app builds, which does not exist yet, so the
|
||||
// request rides on the `Config` this call was handed and the app starts it once
|
||||
// the window is up -- see `Config::tour_pending`.
|
||||
ch.cfg->tour_pending = true;
|
||||
std::fprintf(stderr, "[sources] chooser help: tour requested\n");
|
||||
std::fflush(stderr);
|
||||
}),
|
||||
&c);
|
||||
gtk_box_append(GTK_BOX(box), c.help_tour);
|
||||
|
||||
// The check *is* the setting, exactly as in the window's menu: the row is stated before the handler
|
||||
// is connected, so a row that opens checked does not write the file while the dialog is still being
|
||||
// built. The chooser cannot re-sync anything (there is no "?" layer here), so the handler only has
|
||||
// to leave the setting where the app will read it.
|
||||
c.help_toggle = ui_impl::menu_toggle("Enable contextual help", nullptr,
|
||||
load_settings().contextual_help);
|
||||
gtk_widget_set_tooltip_text(c.help_toggle,
|
||||
"a small \u201c?\u201d beside each surface of the main window, saying "
|
||||
"what that surface is for");
|
||||
g_signal_connect(c.help_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* button, gpointer) {
|
||||
Settings s = load_settings();
|
||||
s.contextual_help = gtk_toggle_button_get_active(button) != FALSE;
|
||||
// Written in full, the way the window's toggle writes it: the keys this row does
|
||||
// not own are carried through from the file rather than dropped.
|
||||
if (!save_settings(s)) {
|
||||
std::fprintf(stderr, "[sources] chooser help: could not write %s\n",
|
||||
settings_path().c_str());
|
||||
std::fflush(stderr);
|
||||
}
|
||||
}),
|
||||
&c);
|
||||
gtk_box_append(GTK_BOX(box), c.help_toggle);
|
||||
|
||||
gtk_popover_set_child(GTK_POPOVER(c.help_popover), box);
|
||||
gtk_menu_button_set_popover(GTK_MENU_BUTTON(c.help_button), c.help_popover);
|
||||
}
|
||||
|
||||
/// `UDPSCOPE_PROBE_PICK_MS`: activate the first row, the way a click does.
|
||||
/// `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.
|
||||
/// switcher wired to the stack, the four buttons the code resolved, and the help menu's three rows.
|
||||
/// `[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;
|
||||
@@ -617,14 +707,21 @@ void report_chooser_ui(const Chooser& c) {
|
||||
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",
|
||||
" visible='%s' switcher_wired=%d buttons=[prefs=%d new_host=%d open=%d cancel=%d] "
|
||||
"help=[button=%d docs=%d tour=%d toggle=%d on=%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);
|
||||
c.waves_open != nullptr ? 1 : 0, c.cancel_button != nullptr ? 1 : 0,
|
||||
c.help_button != nullptr ? 1 : 0, c.help_docs != nullptr ? 1 : 0,
|
||||
c.help_tour != nullptr ? 1 : 0, c.help_toggle != nullptr ? 1 : 0,
|
||||
c.help_toggle != nullptr &&
|
||||
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(c.help_toggle))
|
||||
? 1
|
||||
: 0);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
@@ -638,6 +735,26 @@ gboolean pick_probe(gpointer user) {
|
||||
// What the `.ui` contributed, before anything is driven: the pages, the switcher and the buttons.
|
||||
report_chooser_ui(c);
|
||||
|
||||
// `UDPSCOPE_PROBE_CHOOSER_TOUR=1` and `UDPSCOPE_PROBE_CHOOSER_HELP=1` drive the help menu's two
|
||||
// stateful rows the way a click does, and then fall through to the pick below -- the point of the
|
||||
// tour entry is that it crosses *into the run this chooser starts*, so a probe that stopped here
|
||||
// would assert nothing. The tour is read back out of the `Config` the run will be handed, which is
|
||||
// the carrier; the toggle is read back out of the settings file it writes, since the app is built
|
||||
// from that file and not from the dialog.
|
||||
if (g_getenv("UDPSCOPE_PROBE_CHOOSER_TOUR") != nullptr && c.help_tour != nullptr) {
|
||||
g_signal_emit_by_name(c.help_tour, "clicked");
|
||||
std::fprintf(stderr, "[sources] chooser help: tour_pending=%d\n", c.cfg->tour_pending ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
if (g_getenv("UDPSCOPE_PROBE_CHOOSER_HELP") != nullptr && c.help_toggle != nullptr) {
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(c.help_toggle), TRUE);
|
||||
const Settings back = load_settings();
|
||||
std::fprintf(stderr, "[sources] chooser help: toggle=%d setting=%d file=%s\n",
|
||||
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(c.help_toggle)) ? 1 : 0,
|
||||
back.contextual_help ? 1 : 0, settings_path().c_str());
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
// `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,
|
||||
@@ -906,15 +1023,24 @@ bool choose_source(Config& cfg) {
|
||||
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"));
|
||||
c.help_button = GTK_WIDGET(gtk_builder_get_object(c.ui, "help_menu_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) {
|
||||
c.waves_open == nullptr || c.cancel_button == nullptr || c.help_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));
|
||||
|
||||
// The help menu's rows are the window's own `menu_button()`/`menu_toggle()`, so its popover wants
|
||||
// the same stylesheet the overflow menu is drawn with (`.udpscope-menu`, which trims a row to a
|
||||
// menu row rather than Adwaita's 34 px button). The app installs it in `on_activate()`, which is
|
||||
// *after* this dialog, so it has to be installed here first; `install_css()` is idempotent, so
|
||||
// the later call is the no-op it reads as.
|
||||
ui_impl::install_css();
|
||||
build_chooser_help(c);
|
||||
|
||||
g_signal_connect(c.list, "row-activated", G_CALLBACK(on_row_activated), &c);
|
||||
g_signal_connect(c.wave_list, "row-activated", G_CALLBACK(on_wave_activated), &c);
|
||||
g_signal_connect(c.waves_open, "clicked", G_CALLBACK(on_wave_open_clicked), &c);
|
||||
|
||||
Reference in new issue
Block a user