ui: a refused drop says so, and looks like a message rather than the status
The refusal was never silent -- `on_drop_channel()` has always set a notice -- but the
status bar renders `notice + " | " + glance` in one label, so "plot 1 already holds
four signals" arrived looking like part of the routine text a reader skims, which is the
"no clear user feedback" report. The probe's own reading showed it plainly: the label held
`plotting 'sine' on plot 1 | live · 4.44 kS/s · 2 s window`.
Three changes:
- While a notice is fresh the status label carries the stock `.warning` class, so a message
looks like a message and the class goes when the notice does. Every notice benefits, not
just this one.
- The refusal says what to *do*: "plot 1 holds four signals already: drop it on another
plot, or add a row with the layout editor (L)".
- The drop's inline notice now goes through `set_notice()`, the one setter, instead of
writing `app.notice` and its deadline by hand.
[drop-probe] reads the whole path, with the row filled first so the drop under test is the
refusal:
full row refused the drop: 0 (want 0)
refused feedback: notice='plot 1 holds four signals already: ...' warning=1
the status line, composed: warning=1 status='plot 1 holds four signals already: ... | live · 9.97 kS/s · 2 s window'
The label is read in the third line and not the second on purpose: the status line composes
the notice into itself on its own cadence, so a beat after the refusal it may still hold the
previous text -- which the first attempt at this reading mistook for the fix not working.
The row limit is not silent in the same way: the layout editor's `+` is insensitive at
`kMaxRows`, which is a control that says "no" by being unusable rather than a message.
This commit is contained in:
1 parent
d34b97158e
commit
aaf544be1e
5 files changed
+78
-3
No files matched your search
@@ -356,3 +356,12 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
surface closes the window popover. That needs no hit test beyond the button, because a popover *is* its own
|
||||
surface and a click inside it never arrives; the button is left to its own click, whose toggle is what
|
||||
closes it. `[windowpop]` reads both halves, with autohide off so it is testing the rule and not the grab.
|
||||
|
||||
- **A notice shares the status line with the glance, so it has to *look* like a message.** The status bar
|
||||
renders `app.notice + " | " + glance` in one label, which is exactly where a reader skimming the
|
||||
routine text ("live · 9.97 kS/s · 2 s window") will not see it: a refused drop that said "plot 1 holds four
|
||||
signals already" was read as no feedback at all. While a notice is fresh the label carries the stock
|
||||
`.warning` class, so a message looks like a message and the class goes when the notice does. Two corollaries
|
||||
worth knowing: the line composes on its own cadence, so a notice can appear a beat after it was set (do not
|
||||
read the label in the same tick a handler ran), and a refusal should say what to *do* about it -- "drop it on
|
||||
another plot, or add a row with the layout editor (L)" rather than the bare statement of the limit.
|
||||
@@ -1229,3 +1229,17 @@ probe, exactly as the menu probe does it -- a popover dismisses itself when GTK
|
||||
grab goes missing, where the press has to close the bubble anyway. `click_gestures` reads that the controller
|
||||
is *connected* as well as that its handler works; the last press is on the button itself, which must be left
|
||||
to its own toggle (`want 1` rather than `0`).
|
||||
|
||||
## `[drop-probe]` and what a refused drop says
|
||||
|
||||
```
|
||||
[drop-probe] full row refused the drop: 0 (want 0)
|
||||
[drop-probe] refused feedback: notice='plot 1 holds four signals already: drop it on another plot, or add a row with the layout editor (L)' warning=1
|
||||
[drop-probe] the status line, composed: warning=1 status='plot 1 holds four signals already: ... | live · 9.97 kS/s · 2 s window'
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_DROP_MS`. The first line is the refusal itself (a full row takes no more signals); the
|
||||
second is that it *said so* -- the message and the stock `.warning` class the status line carries while a
|
||||
notice is fresh; the third is the composed line the user reads, which is why the label is only checked here
|
||||
and not in the second: the status line folds the notice into itself on its own cadence, so a beat after the
|
||||
refusal it may still hold the previous text.
|
||||
@@ -4009,10 +4009,16 @@ gboolean tick(gpointer user) {
|
||||
}
|
||||
if (glance.size() > app->glance_max_chars) app->glance_max_chars = glance.size();
|
||||
|
||||
// A notice is not the glance. It is prefixed onto the same line, which is exactly where a reader
|
||||
// skimming the routine text will not see it -- "there is no clear feedback" was that, with a refusal
|
||||
// ("plot 2 holds four signals already") looking like part of the status. The stock `.warning` class is
|
||||
// what makes a message look like a message, and it goes when the notice does.
|
||||
if (!app->notice.empty() && g_get_monotonic_time() < app->notice_until_us) {
|
||||
gtk_widget_add_css_class(app->status_label, "warning");
|
||||
const std::string with_notice = app->notice + " | " + glance;
|
||||
gtk_label_set_text(GTK_LABEL(app->status_label), with_notice.c_str());
|
||||
} else {
|
||||
gtk_widget_remove_css_class(app->status_label, "warning");
|
||||
app->notice.clear();
|
||||
gtk_label_set_text(GTK_LABEL(app->status_label), glance.c_str());
|
||||
}
|
||||
|
||||
@@ -3367,6 +3367,49 @@ gboolean drop_probe(gpointer user) {
|
||||
ok ? 1 : 0, a->rows->row(0).name[0].c_str(), a->rows->row(0).name[1].c_str(),
|
||||
a->rows->row(0).name[2].c_str(), a->rows->row(0).name[3].c_str());
|
||||
}
|
||||
// Fill the row first, so that the drop below is the *refusal*: a drop can otherwise land in a slot
|
||||
// whose signal is named but not acquired, which is a success rather than the case under test.
|
||||
{
|
||||
const std::vector<ChannelInfo>& channels = a->model->channels();
|
||||
for (int slot = 0; slot < RowSet::kSlotsPerRow && !channels.empty(); ++slot) {
|
||||
if (a->rows->slot_bound(0, slot)) continue;
|
||||
const ChannelInfo& ch = channels[static_cast<std::size_t>(slot) % channels.size()];
|
||||
a->rows->assign(0, slot, ch.name, ch.name, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// The refusal's *feedback*, a tick later: the status line composes the notice into itself on the
|
||||
// next tick, so this is where both the text and the class that makes it visible can be read.
|
||||
g_timeout_add(500, +[](gpointer u) -> gboolean {
|
||||
App* app = static_cast<App*>(u);
|
||||
// And the drop that the full row has to refuse, fired here so its feedback is what the reading below
|
||||
// sees rather than the last success's.
|
||||
GValue refused_value = G_VALUE_INIT;
|
||||
g_value_init(&refused_value, G_TYPE_STRING);
|
||||
g_value_set_string(&refused_value, "1");
|
||||
const gboolean refused = on_drop_channel(nullptr, &refused_value, 40.0, 20.0, app);
|
||||
g_value_unset(&refused_value);
|
||||
std::fprintf(stderr, "[drop-probe] full row refused the drop: %d (want 0)\n", refused ? 1 : 0);
|
||||
// The message and its class, which are set the moment the drop is refused. The *label* is read in
|
||||
// the later line instead: the status line composes the notice into itself on its own cadence, so
|
||||
// here it may still hold the previous one.
|
||||
std::fprintf(stderr, "[drop-probe] refused feedback: notice='%s' warning=%d\n",
|
||||
app->notice.c_str(),
|
||||
gtk_widget_has_css_class(app->status_label, "warning") ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
// Again a second and a half later: if the label is still the previous notice then the status line
|
||||
// never recomposes it, which would be the whole of the "no clear feedback" report.
|
||||
g_timeout_add(1500, +[](gpointer u) -> gboolean {
|
||||
App* app = static_cast<App*>(u);
|
||||
std::fprintf(stderr,
|
||||
"[drop-probe] the status line, composed: warning=%d status='%s'\n",
|
||||
gtk_widget_has_css_class(app->status_label, "warning") ? 1 : 0,
|
||||
gtk_label_get_text(GTK_LABEL(app->status_label)));
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}, app);
|
||||
return G_SOURCE_REMOVE;
|
||||
}, a);
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
+6
-3
@@ -290,8 +290,7 @@ gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y
|
||||
if (signal >= app->model->signals().size()) return FALSE;
|
||||
|
||||
const auto notice = [&](const std::string& what) {
|
||||
app->notice = what;
|
||||
app->notice_until_us = g_get_monotonic_time() + 4 * G_TIME_SPAN_SECOND;
|
||||
set_notice(*app, what, 4);
|
||||
std::fprintf(stderr, "[udpscope] drop: %s\n", what.c_str());
|
||||
std::fflush(stderr);
|
||||
};
|
||||
@@ -334,7 +333,11 @@ gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y
|
||||
}
|
||||
}
|
||||
if (slot < 0) {
|
||||
notice("plot " + std::to_string(row + 1) + " already holds four signals");
|
||||
// Say what to do about it. A refusal a reader cannot act on is only half an answer: the two ways
|
||||
// out are another plot or another row.
|
||||
notice("plot " + std::to_string(row + 1) +
|
||||
" holds four signals already: drop it on another plot, or add a row with the layout "
|
||||
"editor (L)");
|
||||
return FALSE;
|
||||
}
|
||||
// The slot remembers the *signal* (and element), never the channel index: the
|
||||
|
||||
Reference in new issue
Block a user