ui: a slide-over panel over the plot's right edge, with its probe
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
73f74ca495
commit
a40863fa45
7 files changed
+327
-2
No files matched your search
@@ -272,6 +272,7 @@ add_executable(udpscope
|
|||||||
src/ui_math.cpp
|
src/ui_math.cpp
|
||||||
src/ui_layout.cpp
|
src/ui_layout.cpp
|
||||||
src/ui_source.cpp
|
src/ui_source.cpp
|
||||||
|
src/ui_slideover.cpp
|
||||||
src/ui_trigger.cpp
|
src/ui_trigger.cpp
|
||||||
src/ui_signals.cpp
|
src/ui_signals.cpp
|
||||||
src/ui_context_help.cpp
|
src/ui_context_help.cpp
|
||||||
|
|||||||
@@ -418,6 +418,17 @@ precedent, not an exception.
|
|||||||
defect where the accumulation is gated on the visible tab, which would leave a gap in min/max/avg for
|
defect where the accumulation is gated on the visible tab, which would leave a gap in min/max/avg for
|
||||||
exactly as long as the trigger was on screen. It enables *and plots* its own signal, so it needs no other
|
exactly as long as the trigger was on screen. It enables *and plots* its own signal, so it needs no other
|
||||||
probe ahead of it. The reading's own entry is in `probe-readings.md`.
|
probe ahead of it. The reading's own entry is in `probe-readings.md`.
|
||||||
|
- `UDPSCOPE_PROBE_SLIDEOVER_MS=1500` with `UDPSCOPE_EXIT_AFTER_MS=5000` (and `UDPSCOPE_ENABLE_ALL_MS=500`) -- the
|
||||||
|
slide-over panel over the plot's right edge, opened on a throw-away `probe` page, read 0.5 s later. Two
|
||||||
|
`[slideover]` lines. Line 1 must read `panel=360x<overlay height> ... edge_flush=1 full_height=1
|
||||||
|
outside_hits_plot=1 inside_hits_panel=1 ... plot_unchanged=1`: a 360 px opaque panel flush to the overlay's
|
||||||
|
right edge and as tall as it, a pick outside it landing in the plot and one inside it in the panel, and the
|
||||||
|
plot's width unchanged by the panel being open (`plot_w before=` equal to `open=`).
|
||||||
|
`outside_hits_plot=0` means the panel has an allocation wider than 360 or a transparent parent that
|
||||||
|
swallows the plot's pan, drop and hover; `plot_unchanged=0` means the panel resized the plot. Line 2 must
|
||||||
|
read `click_inside_closes=0 still_open=1 click_outside_closes=1 esc_closes=1 closed=1`. For a screenshot
|
||||||
|
add `UDPSCOPE_PROBE_SLIDEOVER_HOLD=1`, which leaves the page open after line 1 (run with `GSK_RENDERER=cairo`
|
||||||
|
and `import -window root`): the panel must be drawn over the traces, opaque.
|
||||||
- `UDPSCOPE_PROBE_SOURCE_KINDS=2000` with `UDPSCOPE_EXIT_AFTER_MS=5000`, run as `--source probe --source-opt rate=500`
|
- `UDPSCOPE_PROBE_SOURCE_KINDS=2000` with `UDPSCOPE_EXIT_AFTER_MS=5000`, run as `--source probe --source-opt rate=500`
|
||||||
-- the source-plugin path end to end. The variable's presence also registers the test-only `probe` kind (a
|
-- the source-plugin path end to end. The variable's presence also registers the test-only `probe` kind (a
|
||||||
synthetic sine with one panel), so it is the only way a run sees that kind; its value is the delay in ms.
|
synthetic sine with one panel), so it is the only way a run sees that kind; its value is the delay in ms.
|
||||||
|
|||||||
+8
-1
@@ -5009,6 +5009,7 @@ void on_activate(GtkApplication* application, gpointer user) {
|
|||||||
GtkWidget* events_divider = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "events_divider"));
|
GtkWidget* events_divider = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "events_divider"));
|
||||||
app->plot_box = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "plot_box"));
|
app->plot_box = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "plot_box"));
|
||||||
app->plot_split = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "plot_split"));
|
app->plot_split = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "plot_split"));
|
||||||
|
app->plot_overlay = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "plot_overlay"));
|
||||||
app->status_badge = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_badge"));
|
app->status_badge = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_badge"));
|
||||||
app->status_label = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_label"));
|
app->status_label = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_label"));
|
||||||
app->status_info_button = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_info"));
|
app->status_info_button = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_info"));
|
||||||
@@ -5023,7 +5024,8 @@ void on_activate(GtkApplication* application, gpointer user) {
|
|||||||
// A missing id is the one mistake neither the build nor `GtkBuilder` sees, and every use below would
|
// A missing id is the one mistake neither the build nor `GtkBuilder` sees, and every use below would
|
||||||
// be a null pointer.
|
// be a null pointer.
|
||||||
if (app->window == nullptr || side_view == nullptr || events_divider == nullptr ||
|
if (app->window == nullptr || side_view == nullptr || events_divider == nullptr ||
|
||||||
app->plot_box == nullptr || app->plot_split == nullptr || app->status_badge == nullptr ||
|
app->plot_box == nullptr || app->plot_overlay == nullptr || app->plot_split == nullptr ||
|
||||||
|
app->status_badge == nullptr ||
|
||||||
app->status_label == nullptr || app->status_info_button == nullptr ||
|
app->status_label == nullptr || app->status_info_button == nullptr ||
|
||||||
app->status_row == nullptr ||
|
app->status_row == nullptr ||
|
||||||
side == nullptr || side_stack == nullptr || side_switcher == nullptr ||
|
side == nullptr || side_stack == nullptr || side_switcher == nullptr ||
|
||||||
@@ -5063,6 +5065,8 @@ void on_activate(GtkApplication* application, gpointer user) {
|
|||||||
app->tour.overlay = overlay;
|
app->tour.overlay = overlay;
|
||||||
build_tour(*app);
|
build_tour(*app);
|
||||||
}
|
}
|
||||||
|
// The slide-over panel: an overlay child of the plot, above the GL area.
|
||||||
|
build_slideover(*app);
|
||||||
|
|
||||||
// The size the window opens at, stated in code as well as in the `.ui`: `default-width` there is the
|
// The size the window opens at, stated in code as well as in the `.ui`: `default-width` there is the
|
||||||
// size it *asks* for, and a window whose content has a larger minimum opens at that minimum instead --
|
// size it *asks* for, and a window whose content has a larger minimum opens at that minimum instead --
|
||||||
@@ -5905,6 +5909,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_SIDE_TAB_MS")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_SIDE_TAB_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), side_tab_probe, &app);
|
g_timeout_add(static_cast<guint>(std::atoi(env)), side_tab_probe, &app);
|
||||||
}
|
}
|
||||||
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_SLIDEOVER_MS")) {
|
||||||
|
g_timeout_add(static_cast<guint>(std::atoi(env)), slideover_probe, &app);
|
||||||
|
}
|
||||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_SEARCH_MS")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_SEARCH_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), search_probe, &app);
|
g_timeout_add(static_cast<guint>(std::atoi(env)), search_probe, &app);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -3,7 +3,8 @@
|
|||||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||||
<!--
|
<!--
|
||||||
The main window's skeleton: the window, the split view that puts the sidebar beside the plot, the
|
The main window's skeleton: the window, the split view that puts the sidebar beside the plot, the
|
||||||
events pane's divider, the plot column with the status line under it, and the sidebar's tab stack.
|
events pane's divider, the plot column (`plot_overlay` > `plot_box`, the slide-over panel being an overlay child added in code)
|
||||||
|
with the status line under it, and the sidebar's tab stack.
|
||||||
|
|
||||||
The sidebar is an `AdwOverlaySplitView`, which is what a utility sidebar is in GNOME (as against
|
The sidebar is an `AdwOverlaySplitView`, which is what a utility sidebar is in GNOME (as against
|
||||||
`AdwNavigationSplitView`, which is for navigation lists). Three things follow from that, and they are
|
`AdwNavigationSplitView`, which is for navigation lists). Three things follow from that, and they are
|
||||||
@@ -123,6 +124,15 @@
|
|||||||
<object class="GtkBox" id="left_column">
|
<object class="GtkBox" id="left_column">
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<property name="hexpand">1</property>
|
<property name="hexpand">1</property>
|
||||||
|
<child>
|
||||||
|
<!-- The plot, and the slide-over panel that opens over its right edge. The panel is
|
||||||
|
one opaque overlay child placed by `halign=end` and a 360 px width, never a
|
||||||
|
full-area transparent child, which would be picked wherever its allocation is
|
||||||
|
and swallow the plot's pan, drop and hover (docs/claude/invariants-plot-and-layout.md).
|
||||||
|
The overlay is the only parent change: `plot_box` keeps its id and its content. -->
|
||||||
|
<object class="GtkOverlay" id="plot_overlay">
|
||||||
|
<property name="hexpand">1</property>
|
||||||
|
<property name="vexpand">1</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox" id="plot_box">
|
<object class="GtkBox" id="plot_box">
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
@@ -138,6 +148,8 @@
|
|||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox" id="status_row">
|
<object class="GtkBox" id="status_row">
|
||||||
<property name="orientation">horizontal</property>
|
<property name="orientation">horizontal</property>
|
||||||
|
|||||||
@@ -117,6 +117,19 @@ struct SourcePanelPage {
|
|||||||
GtkWidget* content = nullptr;
|
GtkWidget* content = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The slide-over panel's widgets and state (src/ui_slideover.cpp). `page` is the open page's id, empty when closed.
|
||||||
|
struct SlideOver {
|
||||||
|
GtkWidget* overlay = nullptr; ///< the GtkOverlay around the plot (main-window.ui `plot_overlay`)
|
||||||
|
GtkWidget* revealer = nullptr; ///< the one overlay child; halign END, valign FILL, 360 px
|
||||||
|
GtkWidget* title = nullptr; ///< GtkLabel in the panel header
|
||||||
|
GtkWidget* stack = nullptr; ///< GtkStack: one scroller per page
|
||||||
|
GtkWidget* source_switch = nullptr; ///< AdwToggleGroup, shown on a source page when there are >= 2
|
||||||
|
std::vector<std::string> source_pages;
|
||||||
|
std::string page;
|
||||||
|
bool syncing = false; ///< true while we write the toggle group ourselves
|
||||||
|
};
|
||||||
|
constexpr int kSlideOverWidth = 360;
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
ScopeModel* model = nullptr;
|
ScopeModel* model = nullptr;
|
||||||
Acquisition* acq = nullptr;
|
Acquisition* acq = nullptr;
|
||||||
@@ -124,6 +137,9 @@ struct App {
|
|||||||
|
|
||||||
GtkWidget* window = nullptr;
|
GtkWidget* window = nullptr;
|
||||||
GtkWidget* plot_box = nullptr;
|
GtkWidget* plot_box = nullptr;
|
||||||
|
/// The `GtkOverlay` around `plot_box` (`main-window.ui`), which the slide-over panel is a child of.
|
||||||
|
GtkWidget* plot_overlay = nullptr;
|
||||||
|
SlideOver slideover;
|
||||||
/// The one GL area, drawing the `RowSet`'s figure: every row is a row of that
|
/// The one GL area, drawing the `RowSet`'s figure: every row is a row of that
|
||||||
/// figure, so there is one widget, one context and one `::render` whatever the row
|
/// figure, so there is one widget, one context and one `::render` whatever the row
|
||||||
/// count is.
|
/// count is.
|
||||||
@@ -1872,6 +1888,19 @@ gboolean stats_probe_report(gpointer user);
|
|||||||
gboolean stats_probe(gpointer user);
|
gboolean stats_probe(gpointer user);
|
||||||
gboolean side_tab_probe_report(gpointer user);
|
gboolean side_tab_probe_report(gpointer user);
|
||||||
gboolean side_tab_probe(gpointer user);
|
gboolean side_tab_probe(gpointer user);
|
||||||
|
/// The slide-over panel (`src/ui_slideover.cpp`): one panel at a time over the plot's right edge, a page
|
||||||
|
/// per setup surface, each page in a scroller of its own.
|
||||||
|
void build_slideover(App& app); // once, after the plot_overlay exists
|
||||||
|
bool slideover_add_page(App& app, const std::string& id, const std::string& title, GtkWidget* content);
|
||||||
|
bool slideover_has_page(const App& app, const std::string& id);
|
||||||
|
bool slideover_open(App& app, const std::string& id); // false when the id is unknown
|
||||||
|
void slideover_close(App& app);
|
||||||
|
bool slideover_toggle(App& app, const std::string& id); // open if closed or on another page, else close
|
||||||
|
const std::string& slideover_page(const App& app); // "" when closed
|
||||||
|
bool slideover_escape(App& app); // closes if open; true when it did
|
||||||
|
/// x, y in the overlay's coordinates; closes unless the point is inside the panel; true when it closed.
|
||||||
|
bool slideover_click_away(App& app, double x, double y);
|
||||||
|
gboolean slideover_probe(gpointer user); // UDPSCOPE_PROBE_SLIDEOVER_MS
|
||||||
gboolean source_kinds_probe_report(gpointer user);
|
gboolean source_kinds_probe_report(gpointer user);
|
||||||
gboolean source_kinds_probe(gpointer user);
|
gboolean source_kinds_probe(gpointer user);
|
||||||
/// MCC E-1608 end to end against `tools/mock_e1608.py`: the stream, the calibration, the panels and
|
/// MCC E-1608 end to end against `tools/mock_e1608.py`: the stream, the calibration, the panels and
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
|
|
||||||
|
// The slide-over panel: one panel at a time over the plot's right edge, a page per setup surface.
|
||||||
|
// See docs/superpowers/specs/2026-11-02-ui-badges-dock-design.md.
|
||||||
|
|
||||||
|
#include "ui_app.hpp"
|
||||||
|
|
||||||
|
#include <adwaita.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace udpscope {
|
||||||
|
namespace ui_impl {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void on_close_clicked(GtkButton*, gpointer user) { slideover_close(*static_cast<App*>(user)); }
|
||||||
|
|
||||||
|
void on_source_switch(GObject* o, GParamSpec*, gpointer user) {
|
||||||
|
App* app = static_cast<App*>(user);
|
||||||
|
if (app->slideover.syncing) return;
|
||||||
|
const char* name = adw_toggle_group_get_active_name(ADW_TOGGLE_GROUP(o));
|
||||||
|
if (name != nullptr && app->slideover.page != name) slideover_open(*app, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Observes every press on the plot overlay, in the capture phase, and never claims the sequence: the
|
||||||
|
/// plot's own pan/zoom/drop gestures see the press exactly as they did before.
|
||||||
|
void on_press_capture(GtkGestureClick*, int, double x, double y, gpointer user) {
|
||||||
|
slideover_click_away(*static_cast<App*>(user), x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_source_page(const std::string& id) { return id.rfind("source-", 0) == 0; }
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void build_slideover(App& app) {
|
||||||
|
SlideOver& s = app.slideover;
|
||||||
|
s.overlay = app.plot_overlay;
|
||||||
|
if (s.overlay == nullptr) return;
|
||||||
|
|
||||||
|
GtkWidget* panel = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
gtk_widget_add_css_class(panel, "udpscope-slideover");
|
||||||
|
gtk_widget_set_size_request(panel, kSlideOverWidth, -1);
|
||||||
|
gtk_widget_set_hexpand(panel, FALSE);
|
||||||
|
|
||||||
|
GtkWidget* head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||||
|
gtk_widget_set_margin_start(head, 12);
|
||||||
|
gtk_widget_set_margin_end(head, 6);
|
||||||
|
gtk_widget_set_margin_top(head, 6);
|
||||||
|
gtk_widget_set_margin_bottom(head, 6);
|
||||||
|
s.title = gtk_label_new("");
|
||||||
|
gtk_widget_add_css_class(s.title, "udpscope-slideover-title");
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(s.title), 0.0f);
|
||||||
|
gtk_widget_set_hexpand(s.title, TRUE);
|
||||||
|
gtk_label_set_ellipsize(GTK_LABEL(s.title), PANGO_ELLIPSIZE_END);
|
||||||
|
GtkWidget* close = gtk_button_new_with_label("Close");
|
||||||
|
gtk_widget_add_css_class(close, "flat");
|
||||||
|
g_signal_connect(close, "clicked", G_CALLBACK(on_close_clicked), &app);
|
||||||
|
gtk_box_append(GTK_BOX(head), s.title);
|
||||||
|
gtk_box_append(GTK_BOX(head), close);
|
||||||
|
gtk_box_append(GTK_BOX(panel), head);
|
||||||
|
|
||||||
|
// A source with several panels (the E-1608: Analog out, Digital I/O) gets a segmented switch under
|
||||||
|
// the header; it is hidden unless the open page is a source page and there are two or more.
|
||||||
|
s.source_switch = adw_toggle_group_new();
|
||||||
|
gtk_widget_set_margin_start(s.source_switch, 12);
|
||||||
|
gtk_widget_set_margin_end(s.source_switch, 12);
|
||||||
|
gtk_widget_set_margin_bottom(s.source_switch, 6);
|
||||||
|
gtk_widget_set_visible(s.source_switch, FALSE);
|
||||||
|
g_signal_connect(s.source_switch, "notify::active-name", G_CALLBACK(on_source_switch), &app);
|
||||||
|
gtk_box_append(GTK_BOX(panel), s.source_switch);
|
||||||
|
gtk_box_append(GTK_BOX(panel), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||||
|
|
||||||
|
s.stack = gtk_stack_new();
|
||||||
|
gtk_widget_set_vexpand(s.stack, TRUE);
|
||||||
|
gtk_box_append(GTK_BOX(panel), s.stack);
|
||||||
|
|
||||||
|
s.revealer = gtk_revealer_new();
|
||||||
|
gtk_revealer_set_transition_type(GTK_REVEALER(s.revealer), GTK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT);
|
||||||
|
gtk_revealer_set_transition_duration(GTK_REVEALER(s.revealer), 120);
|
||||||
|
gtk_revealer_set_child(GTK_REVEALER(s.revealer), panel);
|
||||||
|
gtk_widget_set_halign(s.revealer, GTK_ALIGN_END);
|
||||||
|
gtk_widget_set_valign(s.revealer, GTK_ALIGN_FILL);
|
||||||
|
// Closed, it must not be a pick target for the frames of its slide-out.
|
||||||
|
gtk_widget_set_can_target(s.revealer, FALSE);
|
||||||
|
gtk_overlay_add_overlay(GTK_OVERLAY(s.overlay), s.revealer);
|
||||||
|
gtk_overlay_set_clip_overlay(GTK_OVERLAY(s.overlay), s.revealer, TRUE);
|
||||||
|
|
||||||
|
GtkGesture* click = gtk_gesture_click_new();
|
||||||
|
gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(click), GTK_PHASE_CAPTURE);
|
||||||
|
g_signal_connect(click, "pressed", G_CALLBACK(on_press_capture), &app);
|
||||||
|
gtk_widget_add_controller(s.overlay, GTK_EVENT_CONTROLLER(click));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool slideover_add_page(App& app, const std::string& id, const std::string& title, GtkWidget* content) {
|
||||||
|
SlideOver& s = app.slideover;
|
||||||
|
if (s.stack == nullptr || content == nullptr || id.empty() || slideover_has_page(app, id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// A scroller per page: pages differ in height, and one shared scroller would leave a page scrolled
|
||||||
|
// to somewhere it never asked for. It does not propagate the content's height, so the panel's
|
||||||
|
// height is the plot's and never the page's.
|
||||||
|
GtkWidget* scroller = gtk_scrolled_window_new();
|
||||||
|
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||||
|
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroller), content);
|
||||||
|
gtk_stack_add_titled(GTK_STACK(s.stack), scroller, id.c_str(), title.c_str());
|
||||||
|
if (is_source_page(id)) {
|
||||||
|
s.source_pages.push_back(id);
|
||||||
|
AdwToggle* t = adw_toggle_new();
|
||||||
|
adw_toggle_set_name(t, id.c_str());
|
||||||
|
adw_toggle_set_label(t, title.c_str());
|
||||||
|
adw_toggle_group_add(ADW_TOGGLE_GROUP(s.source_switch), t); // takes the reference
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool slideover_has_page(const App& app, const std::string& id) {
|
||||||
|
return app.slideover.stack != nullptr &&
|
||||||
|
gtk_stack_get_child_by_name(GTK_STACK(app.slideover.stack), id.c_str()) != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool slideover_open(App& app, const std::string& id) {
|
||||||
|
SlideOver& s = app.slideover;
|
||||||
|
if (!slideover_has_page(app, id)) return false;
|
||||||
|
GtkWidget* child = gtk_stack_get_child_by_name(GTK_STACK(s.stack), id.c_str());
|
||||||
|
gtk_stack_set_visible_child(GTK_STACK(s.stack), child);
|
||||||
|
s.page = id;
|
||||||
|
GtkStackPage* sp = gtk_stack_get_page(GTK_STACK(s.stack), child);
|
||||||
|
const char* title = gtk_stack_page_get_title(sp);
|
||||||
|
gtk_label_set_text(GTK_LABEL(s.title), title != nullptr ? title : "");
|
||||||
|
const bool several = is_source_page(id) && s.source_pages.size() >= 2;
|
||||||
|
gtk_widget_set_visible(s.source_switch, several);
|
||||||
|
if (several) {
|
||||||
|
s.syncing = true;
|
||||||
|
adw_toggle_group_set_active_name(ADW_TOGGLE_GROUP(s.source_switch), id.c_str());
|
||||||
|
s.syncing = false;
|
||||||
|
}
|
||||||
|
gtk_widget_set_can_target(s.revealer, TRUE);
|
||||||
|
gtk_revealer_set_reveal_child(GTK_REVEALER(s.revealer), TRUE);
|
||||||
|
// task 2: sync_badge_bar(app);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void slideover_close(App& app) {
|
||||||
|
SlideOver& s = app.slideover;
|
||||||
|
if (s.revealer == nullptr || s.page.empty()) return;
|
||||||
|
s.page.clear();
|
||||||
|
gtk_widget_set_can_target(s.revealer, FALSE);
|
||||||
|
gtk_revealer_set_reveal_child(GTK_REVEALER(s.revealer), FALSE);
|
||||||
|
// task 2: sync_badge_bar(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool slideover_toggle(App& app, const std::string& id) {
|
||||||
|
if (!slideover_has_page(app, id)) return false;
|
||||||
|
if (app.slideover.page == id) {
|
||||||
|
slideover_close(app);
|
||||||
|
} else {
|
||||||
|
slideover_open(app, id);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& slideover_page(const App& app) { return app.slideover.page; }
|
||||||
|
|
||||||
|
bool slideover_escape(App& app) {
|
||||||
|
if (app.slideover.page.empty()) return false;
|
||||||
|
slideover_close(app);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool slideover_click_away(App& app, double x, double y) {
|
||||||
|
SlideOver& s = app.slideover;
|
||||||
|
if (s.page.empty() || s.overlay == nullptr || s.revealer == nullptr) return false;
|
||||||
|
graphene_point_t in = GRAPHENE_POINT_INIT(static_cast<float>(x), static_cast<float>(y));
|
||||||
|
graphene_point_t out;
|
||||||
|
if (gtk_widget_compute_point(s.overlay, s.revealer, &in, &out) &&
|
||||||
|
gtk_widget_contains(s.revealer, out.x, out.y)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
slideover_close(app);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- the probe ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct ProbeState {
|
||||||
|
App* app;
|
||||||
|
int plot_w_before = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool under(GtkWidget* w, GtkWidget* ancestor) {
|
||||||
|
return w != nullptr && ancestor != nullptr && (w == ancestor || gtk_widget_is_ancestor(w, ancestor));
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean slideover_probe_read(gpointer user) {
|
||||||
|
ProbeState* ps = static_cast<ProbeState*>(user);
|
||||||
|
App& a = *ps->app;
|
||||||
|
SlideOver& s = a.slideover;
|
||||||
|
const int ow = gtk_widget_get_width(s.overlay);
|
||||||
|
const int oh = gtk_widget_get_height(s.overlay);
|
||||||
|
graphene_rect_t r;
|
||||||
|
const bool have = gtk_widget_compute_bounds(s.revealer, s.overlay, &r);
|
||||||
|
const int rw = have ? static_cast<int>(r.size.width + 0.5f) : -1;
|
||||||
|
const int rx = have ? static_cast<int>(r.origin.x + 0.5f) : -1;
|
||||||
|
const int rh = have ? static_cast<int>(r.size.height + 0.5f) : -1;
|
||||||
|
// Outside the panel, a quarter in: the plot must be what answers. Inside it, the panel.
|
||||||
|
GtkWidget* outside = gtk_widget_pick(s.overlay, ow / 4.0, oh / 2.0, GTK_PICK_DEFAULT);
|
||||||
|
GtkWidget* inside = gtk_widget_pick(s.overlay, ow - kSlideOverWidth / 2.0, oh / 2.0, GTK_PICK_DEFAULT);
|
||||||
|
const int plot_w_open = gtk_widget_get_width(a.plot_box);
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[slideover] open page=%s panel=%dx%d at x=%d overlay=%dx%d edge_flush=%d full_height=%d "
|
||||||
|
"outside_hits_plot=%d inside_hits_panel=%d plot_w before=%d open=%d plot_unchanged=%d\n",
|
||||||
|
s.page.c_str(), rw, rh, rx, ow, oh, rx + rw == ow ? 1 : 0, rh == oh ? 1 : 0,
|
||||||
|
under(outside, a.plot_box) ? 1 : 0, under(inside, s.revealer) ? 1 : 0, ps->plot_w_before,
|
||||||
|
plot_w_open, ps->plot_w_before == plot_w_open ? 1 : 0);
|
||||||
|
// For a screenshot only: leave the page open instead of exercising click-away and Esc.
|
||||||
|
if (std::getenv("UDPSCOPE_PROBE_SLIDEOVER_HOLD") != nullptr) {
|
||||||
|
delete ps;
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
// Click-away from outside the panel closes; a press inside does not.
|
||||||
|
const bool inside_closed = slideover_click_away(a, ow - kSlideOverWidth / 2.0, oh / 2.0);
|
||||||
|
const bool open_after_inside = !s.page.empty();
|
||||||
|
const bool outside_closed = slideover_click_away(a, ow / 4.0, oh / 2.0);
|
||||||
|
slideover_open(a, "probe");
|
||||||
|
const bool esc_closed = slideover_escape(a);
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[slideover] click_inside_closes=%d still_open=%d click_outside_closes=%d esc_closes=%d "
|
||||||
|
"closed=%d\n",
|
||||||
|
inside_closed ? 1 : 0, open_after_inside ? 1 : 0, outside_closed ? 1 : 0, esc_closed ? 1 : 0,
|
||||||
|
s.page.empty() ? 1 : 0);
|
||||||
|
delete ps;
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
gboolean slideover_probe(gpointer user) {
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
if (a->slideover.stack == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
// A page of the probe's own: the real pages arrive in later tasks, and this one has an entry so the
|
||||||
|
// panel has something that takes focus and keys.
|
||||||
|
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||||
|
gtk_box_append(GTK_BOX(box), gtk_label_new("slide-over probe page"));
|
||||||
|
gtk_box_append(GTK_BOX(box), gtk_entry_new());
|
||||||
|
slideover_add_page(*a, "probe", "Probe", box);
|
||||||
|
ProbeState* ps = new ProbeState{a, gtk_widget_get_width(a->plot_box)};
|
||||||
|
slideover_open(*a, "probe");
|
||||||
|
// The revealer's slide is 120 ms; the reading is taken well after it has settled.
|
||||||
|
g_timeout_add(500, slideover_probe_read, ps);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui_impl
|
||||||
|
} // namespace udpscope
|
||||||
@@ -69,6 +69,11 @@ void install_css() {
|
|||||||
".udpscope-tour-spot { border: 2px solid @accent_bg_color; border-radius: 8px; }\n"
|
".udpscope-tour-spot { border: 2px solid @accent_bg_color; border-radius: 8px; }\n"
|
||||||
".udpscope-tour-card { background-color: @window_bg_color; border: 1px solid @borders;"
|
".udpscope-tour-card { background-color: @window_bg_color; border: 1px solid @borders;"
|
||||||
" border-radius: 12px; padding: 12px; box-shadow: 0 3px 12px rgba(0, 0, 0, 0.45); }\n"
|
" border-radius: 12px; padding: 12px; box-shadow: 0 3px 12px rgba(0, 0, 0, 0.45); }\n"
|
||||||
|
// The slide-over panel over the plot's right edge: opaque (it must hide the plot under it),
|
||||||
|
// with an edge and a shadow so it reads as above the plot rather than beside it.
|
||||||
|
".udpscope-slideover { background-color: @window_bg_color; border-left: 1px solid @borders;"
|
||||||
|
" box-shadow: -3px 0 10px rgba(0, 0, 0, 0.35); }\n"
|
||||||
|
".udpscope-slideover-title { font-weight: bold; }\n"
|
||||||
// The contextual-help "(i)": a *mark*, not a button. Adwaita's menu button is a 34x34 control
|
// The contextual-help "(i)": a *mark*, not a button. Adwaita's menu button is a 34x34 control
|
||||||
// (a 24 px floor plus 5 px of padding) with a 16 px glyph, which beside a heading reads as a
|
// (a 24 px floor plus 5 px of padding) with a 16 px glyph, which beside a heading reads as a
|
||||||
// second toolbar button. Measured on libadwaita 1.10: the outer `menubutton` node *and* its
|
// second toolbar button. Measured on libadwaita 1.10: the outer `menubutton` node *and* its
|
||||||
|
|||||||
Reference in new issue
Block a user