Skip to content

Commit c05c215

Browse files
author
Evgeni Raikhel
committed
Add MinZ control editor UI: debounced auto-commit, enable wiring, polish
- realsense-viewer panel for RS2_COMPOSITE_OPTION_HKR_MINZ_CONTROL, fixing an embedded-filter-model crash and quieting preset-glob noise surfaced along the way. - composite_control_editor<T>: a reusable debounced-auto-commit editor extracted so any multi-param composite option can get the same touch -> fade in -> reset-on-retouch -> hard-commit-on-lapse behavior by holding one, instead of re-implementing the mechanism per control. Includes a fix for the fade animation freezing partway through, and a focus-loss shortcut that finishes the countdown early once focus genuinely leaves the group instead of making the user wait it out. - Wires the control's `enable` field to the row header's existing toggle (composite-only embedded filters have no scalar RS2_OPTION_EMBEDDED_FILTER_ENABLED to hook into), and forces enable=1 on auto-commit - editing any field in the box implies the control is meant to be active. - Visual polish: tint the row toggle to match the editor's own pending- commit fade, log every real FW GET at DEBUG verbosity, and dim the box while the control is disabled.
1 parent 9783092 commit c05c215

5 files changed

Lines changed: 520 additions & 19 deletions

File tree

common/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ set(COMMON_SRC
7575
"${CMAKE_CURRENT_LIST_DIR}/hdr-model.h"
7676
"${CMAKE_CURRENT_LIST_DIR}/hdr-model.cpp"
7777
"${CMAKE_CURRENT_LIST_DIR}/embedded-filter-model.h"
78+
"${CMAKE_CURRENT_LIST_DIR}/composite-control-editor.h"
7879
"${CMAKE_CURRENT_LIST_DIR}/embedded-filter-model.cpp"
7980
"${CMAKE_CURRENT_LIST_DIR}/textual-icons.h"
8081
)

common/composite-control-editor.h

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// License: Apache 2.0. See LICENSE file in root directory.
2+
// Copyright(c) 2026 RealSense, Inc. All Rights Reserved.
3+
4+
#pragma once
5+
6+
#include <librealsense2/rs.hpp>
7+
#include <imgui.h>
8+
9+
#include <algorithm>
10+
#include <functional>
11+
#include <limits>
12+
#include <memory>
13+
#include <string>
14+
15+
namespace rs2
16+
{
17+
// Shared "under change" color ramp - gold (just touched) fading to the panel's own
18+
// ImGuiCol_FrameBgHovered blue (about to commit) - used by the framed group's own fill in
19+
// end_frame_and_maybe_commit() below, and by anything else (e.g. the row header's enable
20+
// toggle) that wants to visually echo the same pending-auto-commit state. `progress` is
21+
// 0 = just touched/still editing, 1 = about to fire.
22+
inline ImVec4 composite_control_dirty_blend( float progress )
23+
{
24+
ImVec4 start_color( 255.f / 255.f, 210.f / 255.f, 40.f / 255.f, 90.f / 255.f ); // bright gold/yellow
25+
constexpr float initial_brightness = 1.2f; // "under change" color, 20% brighter
26+
start_color.x = std::min( start_color.x * initial_brightness, 1.0f );
27+
start_color.y = std::min( start_color.y * initial_brightness, 1.0f );
28+
start_color.z = std::min( start_color.z * initial_brightness, 1.0f );
29+
const ImVec4 target_blue = ImGui::GetStyle().Colors[ImGuiCol_FrameBgHovered];
30+
return ImVec4(
31+
start_color.x + ( target_blue.x - start_color.x ) * progress,
32+
start_color.y + ( target_blue.y - start_color.y ) * progress,
33+
start_color.z + ( target_blue.z - start_color.z ) * progress,
34+
start_color.w + ( target_blue.w - start_color.w ) * progress );
35+
}
36+
37+
// PROTOTYPE / DEMO: reusable debounced-auto-commit editor for a composite option's local
38+
// struct T. Extracted from the original HKR MinZ Control implementation so any other
39+
// multi-param composite option can get the same behavior - touch -> fade in -> reset on
40+
// retouch -> hard commit on lapse - by holding one of these instead of re-implementing the
41+
// mechanism. See embedded_filter_model::draw_minz_control_editor() for the reference usage.
42+
//
43+
// The whole framed group is treated as ONE editing unit, matching T being one atomic
44+
// multi-field struct sent in a single UVC transaction - there is no per-field Send.
45+
//
46+
// Per-frame usage:
47+
// 1. ensure_initialized(filter, id, error_message) - seeds `value` from a GET, once.
48+
// 2. Draw the control-specific widgets against `value`. For each one:
49+
// - call touch() on every change while the edit is in progress (e.g. every tick of a
50+
// slider drag) - this flags the group dirty and defers any pending commit;
51+
// - call finalize() once that field's edit is done (slider release via
52+
// ImGui::IsItemDeactivatedAfterEdit(), or immediately after a checkbox/radio click,
53+
// which has no drag phase so touch+finalize happen together) - this (re)schedules
54+
// the real auto-commit `commit_delay` seconds out.
55+
// 3. end_frame_and_maybe_commit(filter, id, error_message, frame_min, frame_max,
56+
// any_field_active_this_frame, before_commit) - draws the fading fill + shrinking border
57+
// around the widgets' bounding box (continuously, for the full commit_delay window - no
58+
// early freeze), shows the hover tooltip, and sends `value` to the device in one atomic
59+
// write once the debounce timer has lapsed with no further touch() calls.
60+
// `any_field_active_this_frame` is true if any of THIS control's own widgets is the one
61+
// currently active (mid-drag/mid-click) - pass false once focus has genuinely left the
62+
// group (some OTHER widget elsewhere is now active) to finish the countdown immediately
63+
// instead of making the user wait it out. `before_commit`, if set, is called with `value`
64+
// right before it's actually sent - this control's own code (which knows T's fields,
65+
// unlike this generic class) is the right place to enforce cross-field invariants, e.g.
66+
// "editing any field implies the control should be enabled too".
67+
template< typename T >
68+
class composite_control_editor
69+
{
70+
public:
71+
T value{};
72+
bool initialized = false;
73+
74+
// Seeds `value` from a real GET the first time this is called; a no-op afterward. Returns
75+
// whether `value` is safe to use (false only if the initial GET failed). `on_read`, if
76+
// set, is called with the freshly-read `value` right after this GET succeeds - the
77+
// caller (which knows T's fields, unlike this generic class) is the right place to log
78+
// what actually came back from the device.
79+
bool ensure_initialized( const std::shared_ptr< rs2::embedded_filter > & filter,
80+
rs2_composite_option_id id,
81+
std::string & error_message,
82+
const std::function< void( const T & ) > & on_read = nullptr )
83+
{
84+
if( initialized )
85+
return true;
86+
try
87+
{
88+
value = filter->get_composite_option_as< T >( id );
89+
initialized = true;
90+
if( on_read )
91+
on_read( value );
92+
}
93+
catch( const std::exception & e )
94+
{
95+
error_message = e.what();
96+
}
97+
return initialized;
98+
}
99+
100+
// Call while a field is actively being changed (every tick of a slider drag, or a
101+
// checkbox/radio click). Flags the group dirty and parks the deadline at +infinity so
102+
// nothing commits mid-edit.
103+
void touch()
104+
{
105+
_dirty = true;
106+
_commit_deadline = std::numeric_limits< double >::max();
107+
}
108+
109+
// Call once a field's edit is finalized (slider released, or immediately after a
110+
// checkbox/radio click). Schedules the real auto-commit commit_delay seconds out.
111+
void finalize() { _commit_deadline = ImGui::GetTime() + commit_delay; }
112+
113+
// Side-effect-free readout of the same progress end_frame_and_maybe_commit() animates
114+
// with: false if nothing is pending, true with `progress` in [0,1] (0 = just
115+
// touched/still mid-edit, 1 = about to commit) otherwise. Lets other UI elements for the
116+
// same control (e.g. an enable toggle drawn elsewhere) mirror the pending-commit state
117+
// without duplicating the deadline math.
118+
bool try_get_progress( float & progress ) const
119+
{
120+
if( ! _dirty )
121+
return false;
122+
double remaining = _commit_deadline - ImGui::GetTime();
123+
double frac_remaining = std::min( std::max( remaining / commit_delay, 0.0 ), 1.0 );
124+
progress = static_cast< float >( 1.0 - frac_remaining );
125+
return true;
126+
}
127+
128+
// Draws the dirty-state fill/border for [frame_min, frame_max] - call after drawing this
129+
// control's own fields, once their bounding box is known - and sends `value` to the
130+
// device in one atomic write if the debounce timer has lapsed since the last touch().
131+
void end_frame_and_maybe_commit( const std::shared_ptr< rs2::embedded_filter > & filter,
132+
rs2_composite_option_id id,
133+
std::string & error_message,
134+
const ImVec2 & frame_min,
135+
const ImVec2 & frame_max,
136+
bool any_field_active_this_frame,
137+
const std::function< void( T & ) > & before_commit = nullptr )
138+
{
139+
constexpr float base_border_thickness = 1.0f;
140+
float border_thickness = base_border_thickness;
141+
142+
// While actively being edited (deadline parked at +infinity by touch()) the fill and
143+
// border sit at their starting intensity: 0% faded (bright gold), 400% border width.
144+
// Once a countdown is actually running (finalize() set a real deadline), both fade
145+
// smoothly over the ENTIRE commit_delay window, right up until the instant the commit
146+
// fires below - no early cap/freeze partway through, or the animation visibly stalls
147+
// for the remainder of the wait, which reads as a bug rather than a fade. The fill
148+
// cools from gold toward the border's own blue (ImGuiCol_FrameBgHovered - the same
149+
// color the border is drawn in, so the fill settles into harmony with it rather than
150+
// into the panel background); the border shrinks from 400% down to 250% of normal.
151+
// The commit itself then snaps both back to their plain idle look (no fill, 100%
152+
// border) in one abrupt jump rather than continuing the gentle fade, so the moment
153+
// something was actually sent reads as a distinct, more pronounced change than the
154+
// animation leading up to it.
155+
float progress = 0.0f;
156+
if( try_get_progress( progress ) )
157+
{
158+
ImVec4 blended = composite_control_dirty_blend( progress );
159+
160+
ImGui::GetWindowDrawList()->AddRectFilled( frame_min, frame_max, ImGui::ColorConvertFloat4ToU32( blended ), 3.0f );
161+
border_thickness = base_border_thickness * ( border_start_scale + ( border_end_scale - border_start_scale ) * progress );
162+
}
163+
164+
ImGui::GetWindowDrawList()->AddRect(
165+
frame_min, frame_max,
166+
ImGui::GetColorU32( ImGuiCol_FrameBgHovered ),
167+
3.0f, // rounding
168+
0, // flags
169+
border_thickness );
170+
171+
if( ImGui::IsMouseHoveringRect( frame_min, frame_max ) )
172+
{
173+
try
174+
{
175+
ImGui::SetTooltip( "%s", filter->get_composite_option_description( id ) );
176+
}
177+
catch( const std::exception & )
178+
{
179+
// Best-effort tooltip only - a failure here shouldn't disrupt the editor.
180+
}
181+
}
182+
183+
// Shortcut: focus genuinely left the group - some OTHER widget elsewhere is active
184+
// this frame, not just "nothing is active right now" (which is also true during the
185+
// normal quiet gap between finishing one field and touching the next one in THIS
186+
// group, and must NOT cut the wait short). When that happens, don't make the user
187+
// wait out the rest of the countdown - finish it now, same as if it had lapsed
188+
// naturally.
189+
if( _dirty && ! any_field_active_this_frame && ImGui::IsAnyItemActive() )
190+
_commit_deadline = ImGui::GetTime();
191+
192+
// Fires once the countdown elapses quietly - checked every frame, so any fresh
193+
// touch() (which re-parks the deadline at +infinity) naturally defers this for as
194+
// long as the user keeps adjusting fields.
195+
if( _dirty && ImGui::GetTime() >= _commit_deadline )
196+
{
197+
try
198+
{
199+
if( before_commit )
200+
before_commit( value );
201+
filter->set_composite_option_from( id, value );
202+
}
203+
catch( const std::exception & e )
204+
{
205+
error_message = e.what();
206+
}
207+
_dirty = false;
208+
_commit_deadline = std::numeric_limits< double >::max();
209+
}
210+
}
211+
212+
private:
213+
bool _dirty = false;
214+
double _commit_deadline = std::numeric_limits< double >::max();
215+
216+
static constexpr double commit_delay = 1.7; // seconds of quiet before auto-sending
217+
static constexpr float border_start_scale = 4.0f; // 400% of normal width, right after an edit
218+
static constexpr float border_end_scale = 2.5f; // 250% of normal width, right before commit
219+
};
220+
}

common/device-model.cpp

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -325,24 +325,35 @@ namespace rs2
325325

326326
auto path = rsutils::os::get_special_folder( rsutils::os::special_folder::user_documents );
327327
path += "librealsense2/presets/";
328-
try
328+
// glob_rec() (third-party/filesystem/glob.h) throws whenever opendir() fails - which is
329+
// the common, expected case here (most machines have never created this folder). Check
330+
// first instead of relying on the exception: avoids a first-chance throw/catch on nearly
331+
// every device refresh, which was showing up as debugger noise unrelated to any real bug.
332+
if( isDir( path, nullptr ) )
329333
{
330-
std::string name = dev.get_info(RS2_CAMERA_INFO_NAME);
331-
std::smatch match;
332-
if( ! std::regex_search( name, match, std::regex( "^RealSense (\\S+)" ) ) )
333-
throw std::runtime_error( "cannot parse device name from '" + name + "'" );
334+
try
335+
{
336+
std::string name = dev.get_info(RS2_CAMERA_INFO_NAME);
337+
std::smatch match;
338+
if( ! std::regex_search( name, match, std::regex( "^RealSense (\\S+)" ) ) )
339+
throw std::runtime_error( "cannot parse device name from '" + name + "'" );
334340

335-
glob(
336-
path,
337-
std::string( match[1] ) + " *.preset",
338-
[&]( std::string const & file ) {
339-
advanced_mode_settings_file_names.insert( path + file );
340-
},
341-
false ); // recursive
341+
glob(
342+
path,
343+
std::string( match[1] ) + " *.preset",
344+
[&]( std::string const & file ) {
345+
advanced_mode_settings_file_names.insert( path + file );
346+
},
347+
false ); // recursive
348+
}
349+
catch( const std::exception & e )
350+
{
351+
LOG_WARNING( "Exception caught trying to detect presets: " << e.what() );
352+
}
342353
}
343-
catch( const std::exception & e )
354+
else
344355
{
345-
LOG_WARNING( "Exception caught trying to detect presets: " << e.what() );
356+
LOG_INFO( "Presets folder not found under " << path << ", skipping detection");
346357
}
347358
}
348359

@@ -3252,15 +3263,26 @@ namespace rs2
32523263
int font_size = window.get_font_size();
32533264
const ImVec2 button_size = { font_size * 2.f, font_size * 1.5f };
32543265

3266+
// While this filter's composite editor (e.g. MinZ) has a debounced
3267+
// commit pending, tint the toggle with the exact same gold->blue ramp
3268+
// the editor's own framed box fades through (before it snaps to idle
3269+
// on commit), so the row header echoes "about to send" instead of
3270+
// just sitting in its last on/off color.
3271+
float dirty_progress = 0.0f;
3272+
const bool composite_dirty = pb->has_pending_composite_commit(dirty_progress);
3273+
ImVec4 dirty_tint = composite_control_dirty_blend(dirty_progress);
3274+
dirty_tint.w = 1.0f; // full opacity for text - the fill's own alpha ramp doesn't apply here
3275+
32553276
if (!pb->is_enabled())
32563277
{
32573278
std::string label = rsutils::string::from()
32583279
<< " " << textual_icons::toggle_off << "##" << id << ","
32593280
<< sub->s->get_info(RS2_CAMERA_INFO_NAME) << ","
32603281
<< pb->get_name();
32613282

3262-
ImGui::PushStyleColor(ImGuiCol_Text, redish);
3263-
ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, redish + 0.1f);
3283+
const ImVec4 text_color = composite_dirty ? dirty_tint : redish;
3284+
ImGui::PushStyleColor(ImGuiCol_Text, text_color);
3285+
ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, text_color + 0.1f);
32643286

32653287
if (ImGui::Button(label.c_str(), button_size))
32663288
{
@@ -3279,8 +3301,9 @@ namespace rs2
32793301
<< " " << textual_icons::toggle_on << "##" << id << ","
32803302
<< sub->s->get_info(RS2_CAMERA_INFO_NAME) << ","
32813303
<< pb->get_name();
3282-
ImGui::PushStyleColor(ImGuiCol_Text, light_blue);
3283-
ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, light_blue + 0.1f);
3304+
const ImVec4 text_color = composite_dirty ? dirty_tint : light_blue;
3305+
ImGui::PushStyleColor(ImGuiCol_Text, text_color);
3306+
ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, text_color + 0.1f);
32843307

32853308
if (ImGui::Button(label.c_str(), button_size))
32863309
{

0 commit comments

Comments
 (0)