@@ -645,16 +645,15 @@ void open_scope_window(App& app, int figure_row, bool detached) {
if ( detached ) title + = " (detached) " ;
gtk_window_set_title ( scope - > window , title . c_str ( ) ) ;
gtk_window_set_default_size ( scope - > window , 900 , 500 ) ;
if ( detached ) {
// The attach control, and it is the main window's own reveal mechanism in miniature: an
// overlay wrapping the GL area (so a revealed button sits *under* the pointer and the motion
// event still travels the ancestor -- a lea ve on the GL widget would fire the moment the
// button appeared and hide it again), and one button in the top-right corner, hidden until
// the pointer nears it. Only a detached window carries one: a copy or an empty window has
// nothing to put back.
scope - > attach_overlay = gtk_overlay_new ( ) ;
gtk_overlay_set_child ( GTK_OVERLAY ( scope - > attach_overlay ) , scope - > widget ) ;
gtk_window_set_child ( scope - > window , scope - > attach_overlay ) ;
// The overlay holds every corner control this window has: the attach button (detached windows
// only), the layout toggle, and the layout editor's own buttons. It is **always** created, not
// only for detached windows, because a scope is a view with its own arrangement and every one of
// them can be rearranged. The motion controller li ves on the overlay, so a revealed button sits
// *under* the pointer and the event still travels the ancestor -- a leave on the GL widget would
// fire the moment the button appeared and hide it again.
scope - > overlay = gtk_overlay_new ( ) ;
gtk_overlay_set_child ( GTK_OVERLAY ( scope - > overlay ) , scope - > widget ) ;
gtk_window_set_child ( scope - > window , scope - > overlay ) ;
GtkEventController * motion = gtk_event_controller_motion_new ( ) ;
g_signal_connect ( motion , " motion " , G_CALLBACK ( + [ ] ( GtkEventControllerMotion * , double x ,
double y , gpointer user ) {
@@ -668,7 +667,9 @@ void open_scope_window(App& app, int figure_row, bool detached) {
static_cast < App : : ScopeWindow * > ( user ) - > pointer_valid = false ;
} ) ,
scope . get ( ) ) ;
gtk_widget_add_controller ( scope - > attach_overlay , GTK_EVENT_CONTROLLER ( motion ) ) ;
gtk_widget_add_controller ( scope - > overlay , GTK_EVENT_CONTROLLER ( motion ) ) ;
if ( detached ) {
// Only a detached window has something to put back.
GtkWidget * attach = gtk_button_new_from_icon_name ( " view-restore-symbolic " ) ;
gtk_widget_set_tooltip_text ( attach , " attach this figure back to the main window " ) ;
gtk_widget_add_css_class ( attach , " circular " ) ;
@@ -686,11 +687,34 @@ void open_scope_window(App& app, int figure_row, bool detached) {
attach_scope_window ( a , * scope ) ;
} ) ,
& app ) ;
gtk_overlay_add_overlay ( GTK_OVERLAY ( scope - > attach_overlay ) , attach ) ;
gtk_overlay_add_overlay ( GTK_OVERLAY ( scope - > overlay ) , attach ) ;
scope - > attach_buttons . push_back ( attach ) ;
} else {
gtk_window_set_child ( scope - > window , scope - > widget ) ;
}
// The layout toggle: the editor's door, the same one the main window's overflow menu carries,
// revealed by the same proximity rule.
scope - > layout_toggle = gtk_button_new_from_icon_name ( " view-grid-symbolic " ) ;
gtk_widget_set_tooltip_text ( scope - > layout_toggle ,
" layout editor: add and remove figure rows in this window " ) ;
gtk_widget_add_css_class ( scope - > layout_toggle , " circular " ) ;
gtk_widget_add_css_class ( scope - > layout_toggle , " osd " ) ;
gtk_widget_set_size_request ( scope - > layout_toggle , kLayoutButtonPx , kLayoutButtonPx ) ;
gtk_widget_set_visible ( scope - > layout_toggle , FALSE ) ;
g_object_set_data ( G_OBJECT ( scope - > layout_toggle ) , " udpscope-scope " , scope . get ( ) ) ;
g_signal_connect ( scope - > layout_toggle , " clicked " , G_CALLBACK ( + [ ] ( GtkButton * b , gpointer user ) {
App & a = * static_cast < App * > ( user ) ;
auto * scope =
static_cast < App : : ScopeWindow * > ( g_object_get_data ( G_OBJECT ( b ) ,
" udpscope-scope " ) ) ;
if ( scope = = nullptr ) return ;
set_scope_layout_editing ( a , * scope , ! scope - > layout_editing ) ;
} ) ,
& app ) ;
gtk_overlay_add_overlay ( GTK_OVERLAY ( scope - > overlay ) , scope - > layout_toggle ) ;
// The window's keyboard: the same accelerators the main window's keys carry. `L` acts on *this*
// window's layout; every other action reaches the application (see `on_scope_key()`).
GtkEventController * keys = gtk_event_controller_key_new ( ) ;
g_signal_connect ( keys , " key-pressed " , G_CALLBACK ( on_scope_key ) , & app ) ;
gtk_widget_add_controller ( GTK_WIDGET ( scope - > window ) , GTK_EVENT_CONTROLLER ( keys ) ) ;
if ( GtkApplication * application = gtk_window_get_application ( GTK_WINDOW ( app . window ) ) ) {
gtk_window_set_application ( scope - > window , application ) ;
}
@@ -1255,9 +1279,7 @@ void on_detach_clicked(GtkButton* b, gpointer user) {
/// numbers so they cannot disagree.
void sync_scope_attach ( App & app , App : : ScopeWindow & scope ) {
( void ) app ;
if ( scope . attach_overlay = = nullptr | | scope . attach_buttons . empty ( ) | | scope . widget = = nullptr ) {
return ;
}
if ( scope . overlay = = nullptr | | scope . widget = = nullptr ) return ;
const double scale = scope . area ! = nullptr & & scope . area - > device_scale ( ) > 0.0f
? static_cast < double > ( scope . area - > device_scale ( ) )
: 1.0 ;
@@ -1280,6 +1302,227 @@ void sync_scope_attach(App& app, App::ScopeWindow& scope) {
scope . pointer_y < = by + kLayoutButtonPx + kMaximizeRevealPx ;
gtk_widget_set_visible ( b , revealed ) ;
}
if ( scope . layout_toggle ! = nullptr ) {
// The layout toggle sits at the corner, to the left of the attach button when there is one.
const double bx = std : : clamp ( static_cast < double > ( width - kLayoutButtonPx - kLayoutButtonPad -
( scope . attach_buttons . empty ( )
? 0
: kLayoutButtonPx + kLayoutButtonPad ) ) ,
0.0 , std : : max ( 0.0 , width - kLayoutButtonPx ) ) ;
const double by = std : : clamp ( static_cast < double > ( kLayoutButtonPad ) , 0.0 ,
std : : max ( 0.0 , height - kLayoutButtonPx ) ) ;
gtk_widget_set_halign ( scope . layout_toggle , GTK_ALIGN_START ) ;
gtk_widget_set_valign ( scope . layout_toggle , GTK_ALIGN_START ) ;
gtk_widget_set_margin_start ( scope . layout_toggle , static_cast < int > ( bx ) ) ;
gtk_widget_set_margin_top ( scope . layout_toggle , static_cast < int > ( by ) ) ;
const bool revealed =
! scope . layout_editing & & scope . pointer_valid & &
scope . pointer_x > = bx - kMaximizeRevealPx & &
scope . pointer_x < = bx + kLayoutButtonPx + kMaximizeRevealPx & &
scope . pointer_y > = by - kMaximizeRevealPx & &
scope . pointer_y < = by + kLayoutButtonPx + kMaximizeRevealPx ;
gtk_widget_set_visible ( scope . layout_toggle , revealed ) ;
}
// The attach button steps aside while the editor is up: Done is the way out of that mode.
for ( GtkWidget * b : scope . attach_buttons ) {
if ( scope . layout_editing ) gtk_widget_set_visible ( b , FALSE ) ;
}
}
// --- the scope windows' own layout editor -----------------------------------------------------
//
// A scope window is a view with its own arrangement, and its arrangement is the user's: it holds
// as many figure rows as it is given and edits them with the same controls the main window's
// editor carries (`+` on a boundary, `× ` on a row, Done to leave). The buttons are the window's
// own -- the main editor's target is the main window -- so the two editors cannot move each
// other's rows.
namespace {
/// Data keys of a scope editor's buttons: the figure row a `× ` removes, the boundary a `+` splits.
constexpr const char * kScopeLayoutRowKey = " udpscope-scope-layout-row " ;
constexpr const char * kScopeLayoutBoundaryKey = " udpscope-scope-layout-boundary " ;
App : : ScopeWindow * scope_layout_scope ( GtkWidget * w ) {
return static_cast < App : : ScopeWindow * > ( g_object_get_data ( G_OBJECT ( w ) , " udpscope-scope " ) ) ;
}
} // namespace
void rebuild_scope_layout_editor ( App & app , App : : ScopeWindow & scope ) ;
void set_scope_layout_editing ( App & app , App : : ScopeWindow & scope , bool on ) ;
void clear_scope_layout_editor ( App : : ScopeWindow & scope ) {
if ( scope . overlay ! = nullptr ) {
for ( GtkWidget * b : scope . layout_buttons ) {
gtk_overlay_remove_overlay ( GTK_OVERLAY ( scope . overlay ) , b ) ;
}
if ( scope . layout_done ! = nullptr ) {
gtk_overlay_remove_overlay ( GTK_OVERLAY ( scope . overlay ) , scope . layout_done ) ;
}
}
scope . layout_buttons . clear ( ) ;
scope . layout_done = nullptr ;
scope . layout_buttons_rows = - 1 ;
}
/// One of the scope editor's buttons: the main editor's look (`circular`, `osd`, the button size)
/// on this window's overlay.
GtkWidget * scope_layout_button ( App : : ScopeWindow & scope , const char * icon , const char * tip ) {
GtkWidget * b = gtk_button_new_from_icon_name ( icon ) ;
gtk_widget_set_tooltip_text ( b , tip ) ;
gtk_widget_add_css_class ( b , " circular " ) ;
gtk_widget_add_css_class ( b , " osd " ) ;
gtk_widget_set_size_request ( b , kLayoutButtonPx , kLayoutButtonPx ) ;
g_object_set_data ( G_OBJECT ( b ) , " udpscope-scope " , & scope ) ;
gtk_overlay_add_overlay ( GTK_OVERLAY ( scope . overlay ) , b ) ;
scope . layout_buttons . push_back ( b ) ;
return b ;
}
void on_scope_layout_row_remove ( GtkButton * b , gpointer user ) {
App & app = * static_cast < App * > ( user ) ;
App : : ScopeWindow * scope = scope_layout_scope ( GTK_WIDGET ( b ) ) ;
if ( scope = = nullptr | | scope - > rows = = nullptr ) return ;
const int fr = GPOINTER_TO_INT ( g_object_get_data ( G_OBJECT ( b ) , kScopeLayoutRowKey ) ) - 1 ;
outlive_the_rebuild ( GTK_WIDGET ( b ) ) ;
if ( ! scope - > rows - > remove_row ( fr ) ) {
set_notice ( app , " a window keeps at least one figure " , 4 ) ;
return ;
}
if ( scope - > area ! = nullptr ) scope - > area - > set_figure ( & scope - > rows - > figure ( ) ) ;
rebuild_scope_layout_editor ( app , * scope ) ;
if ( scope - > widget ! = nullptr ) gtk_gl_area_queue_render ( GTK_GL_AREA ( scope - > widget ) ) ;
}
void on_scope_layout_row_insert ( GtkButton * b , gpointer user ) {
App & app = * static_cast < App * > ( user ) ;
App : : ScopeWindow * scope = scope_layout_scope ( GTK_WIDGET ( b ) ) ;
if ( scope = = nullptr | | scope - > rows = = nullptr ) return ;
const int at = GPOINTER_TO_INT ( g_object_get_data ( G_OBJECT ( b ) , kScopeLayoutBoundaryKey ) ) ;
outlive_the_rebuild ( GTK_WIDGET ( b ) ) ;
if ( ! scope - > rows - > insert_row ( at ) ) {
set_notice ( app , " this window holds " + std : : to_string ( RowSet : : kMaxRows ) + " figures already " , 4 ) ;
return ;
}
if ( scope - > area ! = nullptr ) scope - > area - > set_figure ( & scope - > rows - > figure ( ) ) ;
rebuild_scope_layout_editor ( app , * scope ) ;
if ( scope - > widget ! = nullptr ) gtk_gl_area_queue_render ( GTK_GL_AREA ( scope - > widget ) ) ;
}
void on_scope_layout_done ( GtkButton * b , gpointer user ) {
App & app = * static_cast < App * > ( user ) ;
App : : ScopeWindow * scope = scope_layout_scope ( GTK_WIDGET ( b ) ) ;
if ( scope = = nullptr ) return ;
set_scope_layout_editing ( app , * scope , false ) ;
}
void rebuild_scope_layout_editor ( App & app , App : : ScopeWindow & scope ) {
clear_scope_layout_editor ( scope ) ;
if ( ! scope . layout_editing | | scope . rows = = nullptr | | scope . overlay = = nullptr ) return ;
const int frows = scope . rows - > figure_row_count ( ) ;
scope . layout_buttons_rows = frows ;
for ( int fr = 0 ; fr < frows ; + + fr ) {
GtkWidget * rm = scope_layout_button ( scope , " window-close-symbolic " , " remove this figure " ) ;
g_object_set_data ( G_OBJECT ( rm ) , kScopeLayoutRowKey , GINT_TO_POINTER ( fr + 1 ) ) ;
g_signal_connect ( rm , " clicked " , G_CALLBACK ( on_scope_layout_row_remove ) , & app ) ;
}
for ( int at = 0 ; at < = frows ; + + at ) {
GtkWidget * add = scope_layout_button ( scope , " list-add-symbolic " , " add a figure here " ) ;
g_object_set_data ( G_OBJECT ( add ) , kScopeLayoutBoundaryKey , GINT_TO_POINTER ( at ) ) ;
g_signal_connect ( add , " clicked " , G_CALLBACK ( on_scope_layout_row_insert ) , & app ) ;
}
scope . layout_done = gtk_button_new_with_label ( " Done " ) ;
gtk_widget_set_tooltip_text ( scope . layout_done , " leave the layout editor " ) ;
gtk_widget_add_css_class ( scope . layout_done , " osd " ) ;
gtk_widget_set_size_request ( scope . layout_done , - 1 , kLayoutButtonPx ) ;
g_object_set_data ( G_OBJECT ( scope . layout_done ) , " udpscope-scope " , & scope ) ;
g_signal_connect ( scope . layout_done , " clicked " , G_CALLBACK ( on_scope_layout_done ) , & app ) ;
gtk_overlay_add_overlay ( GTK_OVERLAY ( scope . overlay ) , scope . layout_done ) ;
}
/// Restates where the editor's buttons sit, from the rects the last frame was laid out with: the
/// `× ` inside its row's left edge, the `+` on its boundary, Done in the corner.
void sync_scope_layout_editor ( App & app , App : : ScopeWindow & scope ) {
if ( ! scope . layout_editing | | scope . rows = = nullptr | | scope . area = = nullptr | |
scope . widget = = nullptr ) {
return ;
}
const int frows = scope . rows - > figure_row_count ( ) ;
if ( frows ! = scope . layout_buttons_rows ) rebuild_scope_layout_editor ( app , scope ) ;
const plot : : FigureLayout & fl = scope . rows - > figure ( ) . layout ( ) ;
if ( static_cast < int > ( fl . rows . size ( ) ) ! = frows ) return ;
const double scale =
scope . area - > device_scale ( ) > 0.0f ? static_cast < double > ( scope . area - > device_scale ( ) ) : 1.0 ;
const double height = static_cast < double > ( gtk_widget_get_height ( scope . widget ) ) ;
const double width = static_cast < double > ( gtk_widget_get_width ( scope . widget ) ) ;
if ( ! ( height > 0.0 ) | | ! ( width > 0.0 ) ) return ;
const double half = kLayoutButtonPx * 0.5 ;
for ( GtkWidget * b : scope . layout_buttons ) {
const int fr = GPOINTER_TO_INT ( g_object_get_data ( G_OBJECT ( b ) , kScopeLayoutRowKey ) ) - 1 ;
const int at = GPOINTER_TO_INT ( g_object_get_data ( G_OBJECT ( b ) , kScopeLayoutBoundaryKey ) ) ;
gtk_widget_set_halign ( b , GTK_ALIGN_START ) ;
gtk_widget_set_valign ( b , GTK_ALIGN_START ) ;
if ( fr > = 0 ) {
if ( fr > = frows ) continue ;
const plot : : Rect & row = fl . rows [ static_cast < std : : size_t > ( fr ) ] ;
gtk_widget_set_margin_top (
b , static_cast < int > ( std : : clamp ( static_cast < double > ( row . y ) / scale + kLayoutButtonPad , 0.0 ,
height - kLayoutButtonPx ) + 0.5 ) ) ;
// Inside the axes, not over the y labels: the row's first cell's plot knows where they end.
const plot : : Plot * p = scope . rows - > row ( scope . rows - > first_plot_of_row ( fr ) ) . plot ;
const double left =
( p ! = nullptr ? static_cast < double > ( p - > layout ( ) . axes . x ) / scale
: static_cast < double > ( row . x ) / scale ) + kLayoutButtonPad ;
gtk_widget_set_margin_start ( b , static_cast < int > ( std : : clamp ( left , 0.0 , width - kLayoutButtonPx ) + 0.5 ) ) ;
} else if ( at > = 0 ) {
const double edge = at < frows
? static_cast < double > ( fl . rows [ static_cast < std : : size_t > ( at ) ] . y ) / scale
: ( static_cast < double > ( fl . rows . back ( ) . y ) +
static_cast < double > ( fl . rows . back ( ) . h ) ) / scale ;
gtk_widget_set_margin_top (
b , static_cast < int > ( std : : clamp ( edge - half , 0.0 , height - kLayoutButtonPx ) + 0.5 ) ) ;
gtk_widget_set_margin_start ( b , static_cast < int > ( kLayoutButtonPad ) ) ;
}
}
if ( scope . layout_done ! = nullptr ) {
gtk_widget_set_halign ( scope . layout_done , GTK_ALIGN_END ) ;
gtk_widget_set_valign ( scope . layout_done , GTK_ALIGN_START ) ;
gtk_widget_set_margin_end ( scope . layout_done , kLayoutButtonPad ) ;
gtk_widget_set_margin_top ( scope . layout_done , kLayoutButtonPad ) ;
}
}
void set_scope_layout_editing ( App & app , App : : ScopeWindow & scope , bool on ) {
if ( scope . layout_editing = = on ) return ;
scope . layout_editing = on ;
if ( on ) {
rebuild_scope_layout_editor ( app , scope ) ;
} else {
clear_scope_layout_editor ( scope ) ;
}
sync_scope_attach ( app , scope ) ;
if ( scope . widget ! = nullptr ) gtk_gl_area_queue_render ( GTK_GL_AREA ( scope . widget ) ) ;
}
gboolean on_scope_key ( GtkEventControllerKey * controller , guint keyval , guint , GdkModifierType state ,
gpointer user ) {
App & app = * static_cast < App * > ( user ) ;
const Accel * accel = find_accel ( keyval , static_cast < unsigned > ( state ) ) ;
if ( accel = = nullptr ) return FALSE ;
if ( accel - > action = = AppAction : : LayoutEditor ) {
// The window the key arrived in, not the main one: `L` is the layout editor of the view you
// are looking at.
GtkWidget * widget = gtk_event_controller_get_widget ( GTK_EVENT_CONTROLLER ( controller ) ) ;
GtkWidget * window = widget ! = nullptr ? gtk_widget_get_ancestor ( widget , GTK_TYPE_WINDOW ) : nullptr ;
for ( const std : : unique_ptr < App : : ScopeWindow > & scope : app . scopes ) {
if ( GTK_WIDGET ( scope - > window ) ! = window ) continue ;
set_scope_layout_editing ( app , * scope , ! scope - > layout_editing ) ;
return TRUE ;
}
return FALSE ;
}
// Everything else is the application's, exactly as the main window's keys take it.
run_app_action ( app , accel - > action ) ;
return TRUE ;
}
/// **Detach**: the figure row moves to a window of its own. Copied first (the new window opens
@@ -4650,7 +4893,10 @@ gboolean tick(gpointer user) {
if ( scope - > rows - > rebuild_if_dirty ( ) & & scope - > area ! = nullptr ) {
scope - > area - > set_figure ( & scope - > rows - > figure ( ) ) ;
}
if ( scope - > detached ) sync_scope_attach ( * app , * scope ) ;
if ( scope - > detached | | scope - > layout_toggle ! = nullptr ) sync_scope_attach ( * app , * scope ) ;
// The window's own editor: buttons restated from the rects this frame was laid out with.
// Its own arrangement is its own -- nothing here saves the main window's session.
if ( scope - > layout_editing ) sync_scope_layout_editor ( * app , * scope ) ;
// **A window that cannot paint says so.** A blank plot is indistinguishable from a
// context that failed or an area that never got a frame, and the report ("nothing is
// drawn, no axes") is exactly what this turns into a sentence: the window's own title