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
+84
-21
@@ -3,7 +3,9 @@
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "build_info.hpp"
|
||||
#include "layout_store.hpp"
|
||||
@@ -44,27 +46,55 @@ void on_dialog_closed(AdwDialog* dialog, gpointer slot) {
|
||||
*static_cast<AdwDialog**>(slot) = nullptr;
|
||||
}
|
||||
|
||||
/// The launch is asynchronous and can fail -- no help viewer installed, no handler for the
|
||||
/// `help:` scheme, or a document no viewer can find -- and it runs in another process, so
|
||||
/// nothing raises a window of ours. Report the failure here rather than leaving a click that
|
||||
/// silently did nothing; the detail names the identifier a viewer looks for.
|
||||
/// What an asynchronous launch needs to survive the call: the window it is attributed to (held,
|
||||
/// because the call outlives the stack frame that made it) and the caller's reporter, if it asked
|
||||
/// for one. One small struct rather than two `gpointer`s, because a callback that takes both through
|
||||
/// one `user_data` still has to get them there.
|
||||
struct HelpLaunch {
|
||||
GtkWindow* parent = nullptr;
|
||||
std::function<void(bool, const std::string&)> done;
|
||||
};
|
||||
|
||||
/// The launch failed. Report it rather than leaving a click that silently did nothing -- and say
|
||||
/// *which* thing is missing: `gtk_uri_launcher_launch_finish()` reports that the URI could not be
|
||||
/// handed over, which is the handler's absence, while a document that is not installed is a
|
||||
/// different failure the launcher cannot tell it from. `help_installed_path()` settles that from the
|
||||
/// file system, so a missing *document* is not reported as a missing *handler*: with the manual
|
||||
/// installed the message names the viewer, and without it the message names the document, its
|
||||
/// expected path and the install command -- and a viewer as well, because that second possibility
|
||||
/// cannot be ruled out from here.
|
||||
void on_help_launched(GObject* source, GAsyncResult* result, gpointer user) {
|
||||
GtkWindow* parent = user != nullptr ? GTK_WINDOW(user) : nullptr;
|
||||
auto* launch = static_cast<HelpLaunch*>(user);
|
||||
GError* error = nullptr;
|
||||
if (!gtk_uri_launcher_launch_finish(GTK_URI_LAUNCHER(source), result, &error)) {
|
||||
const bool ok = gtk_uri_launcher_launch_finish(GTK_URI_LAUNCHER(source), result, &error);
|
||||
if (!ok) {
|
||||
const std::string installed = help_installed_path();
|
||||
GtkAlertDialog* alert = gtk_alert_dialog_new("Could not open the help manual");
|
||||
const std::string detail =
|
||||
std::string(error->message) +
|
||||
"\n\nThe manual is installed under $XDG_DATA_DIRS/help/C/help:" UDPSCOPE_HELP_NAME
|
||||
"; a desktop help viewer (for example `yelp`) that handles the `help:` URI is needed "
|
||||
"to read it.";
|
||||
std::string detail = error != nullptr ? error->message : std::string("the launch failed");
|
||||
detail += "\n\n";
|
||||
if (!installed.empty()) {
|
||||
detail += "The manual is installed at " + installed +
|
||||
", so what is missing is a viewer for the `help:` URI it is opened with: install "
|
||||
"one (for example `yelp`) or make it the desktop's default for that scheme.";
|
||||
} else {
|
||||
detail += "The manual does not look installed: nothing named \"help:" UDPSCOPE_HELP_NAME
|
||||
"\" was found under $XDG_DATA_HOME/help/<lang>/" UDPSCOPE_HELP_NAME "/ or "
|
||||
"$XDG_DATA_DIRS/help/<lang>/" UDPSCOPE_HELP_NAME "/. Install it with\n\n"
|
||||
" cmake --install build --prefix ~/.local\n\n"
|
||||
"which writes ~/.local/share/help/C/" UDPSCOPE_HELP_NAME "/ -- the directory "
|
||||
"`help:" UDPSCOPE_HELP_NAME "` resolves to. If there is no `help:` viewer either, "
|
||||
"install one as well (for example `yelp`).";
|
||||
}
|
||||
gtk_alert_dialog_set_detail(alert, detail.c_str());
|
||||
gtk_alert_dialog_show(alert, parent);
|
||||
gtk_alert_dialog_show(alert, launch->parent);
|
||||
g_object_unref(alert);
|
||||
g_error_free(error);
|
||||
}
|
||||
// `parent` was referenced for the duration of the async call.
|
||||
if (parent != nullptr) g_object_unref(parent);
|
||||
if (launch->done) {
|
||||
launch->done(ok, ok || error == nullptr ? std::string() : std::string(error->message));
|
||||
}
|
||||
if (launch->parent != nullptr) g_object_unref(launch->parent);
|
||||
delete launch;
|
||||
if (error != nullptr) g_error_free(error);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -108,6 +138,37 @@ const char* about_icon_name() {
|
||||
return adw_about_dialog_get_application_icon(ADW_ABOUT_DIALOG(g_about));
|
||||
}
|
||||
|
||||
std::string help_installed_path() {
|
||||
// The freedesktop Help System Specification's own search order: `$XDG_DATA_HOME` first, then every
|
||||
// `$XDG_DATA_DIRS` entry in order. Yelp resolves `help:<id>` the same way, so this is a reading of
|
||||
// where the document *would* be found rather than a second, private convention.
|
||||
std::vector<std::string> roots;
|
||||
roots.emplace_back(g_get_user_data_dir() != nullptr ? g_get_user_data_dir() : "");
|
||||
const char* const* dirs = g_get_system_data_dirs();
|
||||
for (int i = 0; dirs != nullptr && dirs[i] != nullptr; ++i) roots.emplace_back(dirs[i]);
|
||||
for (const std::string& root : roots) {
|
||||
if (root.empty()) continue;
|
||||
const std::string base = root + "/help";
|
||||
GError* error = nullptr;
|
||||
GDir* dir = g_dir_open(base.c_str(), 0, &error);
|
||||
if (dir == nullptr) {
|
||||
if (error != nullptr) g_error_free(error);
|
||||
continue;
|
||||
}
|
||||
// `<lang>` is a directory of its own (`C`, `en`, `it`, ...), so the document is looked for one
|
||||
// level below the `help` root rather than straight under it.
|
||||
while (const char* lang = g_dir_read_name(dir)) {
|
||||
const std::string candidate = base + "/" + lang + "/" UDPSCOPE_HELP_NAME;
|
||||
if (g_file_test((candidate + "/index.page").c_str(), G_FILE_TEST_IS_REGULAR)) {
|
||||
g_dir_close(dir);
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
g_dir_close(dir);
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
|
||||
HelpReport help_self_check() {
|
||||
HelpReport report;
|
||||
|
||||
@@ -141,14 +202,16 @@ HelpReport help_self_check() {
|
||||
return report;
|
||||
}
|
||||
|
||||
void show_help(GtkWindow* parent) {
|
||||
void show_help(GtkWindow* parent, std::function<void(bool, const std::string&)> done) {
|
||||
// The freedesktop `help:` URI, handed to whatever the desktop registered for it -- Yelp on
|
||||
// GNOME. The launch is asynchronous, so the parent is referenced across it and released in
|
||||
// the callback; `parent` may legitimately be nullptr (the chooser runs before any window
|
||||
// exists).
|
||||
// GNOME. The launch is asynchronous, so both the parent and the reporter are carried across it in
|
||||
// a heap `HelpLaunch` and released in the callback; `parent` may legitimately be nullptr (the
|
||||
// chooser runs before any window exists).
|
||||
auto* launch = new HelpLaunch;
|
||||
launch->parent = parent != nullptr ? GTK_WINDOW(g_object_ref(parent)) : nullptr;
|
||||
launch->done = std::move(done);
|
||||
GtkUriLauncher* launcher = gtk_uri_launcher_new("help:" UDPSCOPE_HELP_NAME);
|
||||
gtk_uri_launcher_launch(launcher, parent, nullptr, on_help_launched,
|
||||
parent != nullptr ? g_object_ref(parent) : nullptr);
|
||||
gtk_uri_launcher_launch(launcher, parent, nullptr, on_help_launched, launch);
|
||||
g_object_unref(launcher);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user