You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

679 lines
24 KiB

// License: Apache 2.0. See LICENSE file in root directory.
// Copyright(c) 2022 Intel Corporation. All Rights Reserved.
#include "option-model.h"
#include <librealsense2/rs_advanced_mode.hpp>
#include <imgui.h>
#include <imgui_internal.h>
#include "device-model.h"
#include "subdevice-model.h"
namespace rs2
{
option_model create_option_model( option_value const & opt,
const std::string& opt_base_label,
subdevice_model* model,
std::shared_ptr<options> options,
bool* options_invalidated,
std::string& error_message)
{
option_model option = {};
std::string const option_name = options->get_option_name( opt->id );
option.id = rsutils::string::from() << opt_base_label << '/' << option_name;
option.opt = opt->id;
option.endpoint = options;
option.label = rsutils::string::from() << option_name << "##" << option.id;
option.invalidate_flag = options_invalidated;
option.dev = model;
option.value = opt;
option.supported = opt->is_valid; // i.e., supported-and-enabled!
option.range = options->get_option_range( opt->id );
option.read_only = options->is_option_read_only( opt->id );
return option;
}
}
using namespace rs2;
std::string option_model::adjust_description(const std::string& str_in, const std::string& to_be_replaced, const std::string& to_replace)
{
std::string adjusted_string(str_in);
auto pos = adjusted_string.find(to_be_replaced);
adjusted_string.replace(pos, to_be_replaced.size(), to_replace);
return adjusted_string;
}
bool option_model::draw( std::string & error_message,
notifications_model & model,
bool new_line,
bool use_option_name )
{
auto res = false;
if( endpoint->supports( opt ) )
{
// The option's rendering model supports an alternative option title derived from its
// description rather than name. This is applied to the Holes Filling as its display must
// conform with the names used by a 3rd-party tools for consistency.
if( opt == RS2_OPTION_HOLES_FILL )
use_option_name = false;
std::string desc_str( endpoint->get_option_description( opt ) );
// Device D405 is for short range, therefore, its units are in cm - for better UX
bool use_cm_units = false;
std::string device_pid = dev->dev.get_info( RS2_CAMERA_INFO_PRODUCT_ID );
if( device_pid == "0B5B"
&& val_in_range(
opt,
{ RS2_OPTION_MIN_DISTANCE, RS2_OPTION_MAX_DISTANCE, RS2_OPTION_DEPTH_UNITS } ) )
{
use_cm_units = true;
desc_str = adjust_description( desc_str, "meters", "cm" );
}
auto desc = desc_str.c_str();
// remain option to append to the current line
if( ! new_line )
ImGui::SameLine();
if( is_enum() )
{
res = draw_combobox( model, error_message, desc, new_line, use_option_name );
}
else
{
if( is_checkbox() )
{
res = draw_checkbox( model, error_message, desc );
}
else
{
res = draw_slider( model, error_message, desc, use_cm_units );
}
}
if( ! read_only && opt == RS2_OPTION_ENABLE_AUTO_EXPOSURE && dev->auto_exposure_enabled
&& dev->s->is< roi_sensor >() && dev->streaming )
{
ImGui::SameLine( 0, 10 );
std::string button_label = label;
auto index = label.find_last_of( '#' );
if( index != std::string::npos )
{
button_label = label.substr( index + 1 );
}
ImGui::PushStyleColor( ImGuiCol_TextSelectedBg, { 1.f, 1.f, 1.f, 1.f } );
if( ! dev->roi_checked )
{
std::string caption = rsutils::string::from() << "Set ROI##" << button_label;
if( ImGui::Button( caption.c_str(), { 55, 0 } ) )
{
dev->roi_checked = true;
}
}
else
{
std::string caption = rsutils::string::from() << "Cancel##" << button_label;
if( ImGui::Button( caption.c_str(), { 55, 0 } ) )
{
dev->roi_checked = false;
}
}
ImGui::PopStyleColor();
if( ImGui::IsItemHovered() )
ImGui::SetTooltip( "Select custom region of interest for the auto-exposure "
"algorithm\nClick the button, then draw a rect on the frame" );
}
}
return res;
}
void option_model::update_supported( std::string & error_message )
{
try
{
supported = endpoint->supports( opt );
}
catch( const error & e )
{
error_message = error_to_string( e );
}
}
void option_model::update_read_only_status( std::string & error_message )
{
try
{
read_only = endpoint->is_option_read_only( opt );
}
catch( const error & e )
{
error_message = error_to_string( e );
}
}
void option_model::update_all_fields( std::string & error_message, notifications_model & model )
{
try
{
value = endpoint->get_option_value( opt );
supported = value->is_valid;
if( supported )
{
range = endpoint->get_option_range( opt );
read_only = endpoint->is_option_read_only( opt );
}
}
catch( const error & e )
{
if( read_only )
{
model.add_notification( { rsutils::string::from()
<< "Could not refresh read-only option "
<< endpoint->get_option_name( opt ) << ": " << e.what(),
RS2_LOG_SEVERITY_WARN,
RS2_NOTIFICATION_CATEGORY_UNKNOWN_ERROR } );
}
else
error_message = error_to_string( e );
}
}
bool option_model::is_all_integers() const
{
return is_integer( range.min ) && is_integer( range.max ) && is_integer( range.def )
&& is_integer( range.step );
}
bool option_model::is_enum() const
{
// We do not expect enum values to have a step that is smaller than 1,
// and we don't want to compare a floating point value to an integer so 0.9 will do the work.
if( range.step < 0.9f )
return false;
for( auto i = range.min; i <= range.max; i += range.step )
{
if( endpoint->get_option_value_description( opt, i ) == nullptr )
return false;
}
return true;
}
std::vector< const char * > option_model::get_combo_labels( int * p_selected ) const
{
int selected = 0, counter = 0;
std::vector< const char * > labels;
for( auto i = range.min; i <= range.max; i += range.step, counter++ )
{
auto label = endpoint->get_option_value_description( opt, i );
switch( value->type )
{
case RS2_OPTION_TYPE_FLOAT:
if( std::fabs( i - value->as_float ) < 0.001f )
selected = counter;
break;
case RS2_OPTION_TYPE_STRING:
if( 0 == strcmp( label, value->as_string ) )
selected = counter;
break;
}
labels.push_back( label );
}
if( p_selected )
*p_selected = selected;
return labels;
}
bool option_model::draw_combobox( notifications_model & model,
std::string & error_message,
const char * description,
bool new_line,
bool use_option_name )
{
bool item_clicked = false;
std::string txt = rsutils::string::from()
<< ( use_option_name ? endpoint->get_option_name( opt ) : description ) << ":";
float text_length = ImGui::CalcTextSize( txt.c_str() ).x;
float combo_position_x = ImGui::GetCursorPosX() + text_length + 5;
ImGui::Text( "%s", txt.c_str() );
if( ImGui::IsItemHovered() && description )
{
ImGui::SetTooltip( "%s", description );
}
ImGui::SameLine();
if( new_line )
ImGui::SetCursorPosX( combo_position_x );
ImGui::PushItemWidth( new_line ? -1.f : 100.f );
int selected;
std::vector< const char * > labels = get_combo_labels( &selected );
ImGui::PushStyleColor( ImGuiCol_TextSelectedBg, { 1, 1, 1, 1 } );
try
{
if( ImGui::Combo( id.c_str(), &selected, labels.data(), static_cast< int >( labels.size() ) ) )
{
float tmp_value = range.min + range.step * selected;
model.add_log( rsutils::string::from()
<< "Setting " << opt << " to " << tmp_value << " (" << labels[selected] << ")" );
set_option( opt, tmp_value, error_message );
if( invalidate_flag )
*invalidate_flag = true;
item_clicked = true;
}
}
catch( const error & e )
{
error_message = error_to_string( e );
}
ImGui::PopStyleColor();
ImGui::PopItemWidth();
return item_clicked;
}
bool option_model::draw_slider( notifications_model & model,
std::string & error_message,
const char * description,
bool use_cm_units )
{
bool slider_clicked = false;
std::string txt = rsutils::string::from() << endpoint->get_option_name( opt ) << ":";
ImGui::Text( "%s", txt.c_str() );
ImGui::SameLine();
ImGui::SetCursorPosX( read_only ? 268.f : 245.f );
ImGui::PushStyleColor( ImGuiCol_Text, grey );
ImGui::PushStyleColor( ImGuiCol_TextSelectedBg, grey );
ImGui::PushStyleColor( ImGuiCol_ButtonActive, { 1.f, 1.f, 1.f, 0.f } );
ImGui::PushStyleColor( ImGuiCol_ButtonHovered, { 1.f, 1.f, 1.f, 0.f } );
ImGui::PushStyleColor( ImGuiCol_Button, { 1.f, 1.f, 1.f, 0.f } );
ImGui::Button( textual_icons::question_mark, { 20, 20 } );
ImGui::PopStyleColor( 5 );
if( ImGui::IsItemHovered() && description )
{
ImGui::SetTooltip( "%s", description );
}
if( ! read_only )
{
ImGui::SameLine();
ImGui::SetCursorPosX( 268 );
if( ! edit_mode )
{
std::string edit_id = rsutils::string::from() << textual_icons::edit << "##" << id;
ImGui::PushStyleColor( ImGuiCol_Text, light_grey );
ImGui::PushStyleColor( ImGuiCol_TextSelectedBg, light_grey );
ImGui::PushStyleColor( ImGuiCol_ButtonHovered, { 1.f, 1.f, 1.f, 0.f } );
ImGui::PushStyleColor( ImGuiCol_Button, { 1.f, 1.f, 1.f, 0.f } );
if( ImGui::Button( edit_id.c_str(), { 20, 20 } ) )
{
if( is_all_integers() )
edit_value = rsutils::string::from() << (int)value->as_float;
else
edit_value = rsutils::string::from() << value->as_float;
edit_mode = true;
}
if( ImGui::IsItemHovered() )
{
ImGui::SetTooltip( "Enter text-edit mode" );
}
ImGui::PopStyleColor( 4 );
}
else
{
std::string edit_id = rsutils::string::from() << textual_icons::edit << "##" << id;
ImGui::PushStyleColor( ImGuiCol_Text, light_blue );
ImGui::PushStyleColor( ImGuiCol_TextSelectedBg, light_blue );
ImGui::PushStyleColor( ImGuiCol_ButtonHovered, { 1.f, 1.f, 1.f, 0.f } );
ImGui::PushStyleColor( ImGuiCol_Button, { 1.f, 1.f, 1.f, 0.f } );
if( ImGui::Button( edit_id.c_str(), { 20, 20 } ) )
{
edit_mode = false;
}
if( ImGui::IsItemHovered() )
{
ImGui::SetTooltip( "Exit text-edit mode" );
}
ImGui::PopStyleColor( 4 );
}
}
ImGui::PushItemWidth( -1 );
try
{
if( read_only )
{
ImVec2 vec{ 0, 20 };
std::string text = ( value->as_float == (int)value->as_float ) ? std::to_string( (int)value->as_float )
: std::to_string( value->as_float );
if( range.min != range.max )
{
ImGui::ProgressBar( ( value->as_float / ( range.max - range.min ) ), vec, text.c_str() );
}
else // constant value options
{
auto c = ImGui::ColorConvertU32ToFloat4( ImGui::GetColorU32( ImGuiCol_FrameBg ) );
ImGui::PushStyleColor( ImGuiCol_FrameBgActive, c );
ImGui::PushStyleColor( ImGuiCol_FrameBgHovered, c );
float dummy = std::floor( value->as_float );
if( ImGui::DragFloat( id.c_str(), &dummy, 1, 0, 0, text.c_str() ) )
{
// Changing the depth units not on advanced mode is not allowed,
// prompt the user to switch to advanced mode for chaging it.
if( RS2_OPTION_DEPTH_UNITS == opt )
{
auto advanced = dev->dev.as< rs400::advanced_mode >();
if( advanced )
if( ! advanced.is_enabled() )
dev->draw_advanced_mode_prompt = true;
}
}
ImGui::PopStyleColor( 2 );
}
}
else if( edit_mode )
{
std::string buff_str = edit_value;
// lambda function used to convert meters to cm - while the number is a string
auto convert_float_str = []( std::string float_str, float conversion_factor ) {
if( float_str.size() == 0 )
return float_str;
float number_float = std::stof( float_str );
return std::to_string( number_float * conversion_factor );
};
// when cm must be used instead of meters
if( use_cm_units )
buff_str = convert_float_str( buff_str, 100.f );
char buff[TEXT_BUFF_SIZE];
memset( buff, 0, TEXT_BUFF_SIZE );
strcpy( buff, buff_str.c_str() );
if( ImGui::InputText( id.c_str(),
buff,
TEXT_BUFF_SIZE,
ImGuiInputTextFlags_EnterReturnsTrue ) )
{
if( use_cm_units )
{
buff_str = convert_float_str( std::string( buff ), 0.01f );
memset( buff, 0, TEXT_BUFF_SIZE );
strcpy( buff, buff_str.c_str() );
}
float new_value;
if( ! rsutils::string::string_to_value< float >( buff, new_value ) )
{
error_message = "Invalid float input!";
}
else if( new_value < range.min || new_value > range.max )
{
float val = use_cm_units ? new_value * 100.f : new_value;
float min = use_cm_units ? range.min * 100.f : range.min;
float max = use_cm_units ? range.max * 100.f : range.max;
error_message = rsutils::string::from()
<< val << " is out of bounds [" << min << ", " << max << "]";
}
else
{
// run when the value is valid and the enter key is pressed to submit the new value
auto option_was_set = set_option(opt, new_value, error_message);
if (option_was_set)
{
if (invalidate_flag)
*invalidate_flag = true;
model.add_log(rsutils::string::from() << "Setting " << opt << " to " << value->as_float);
}
}
edit_mode = false;
}
else if( use_cm_units )
{
buff_str = convert_float_str( buff_str, 0.01f );
memset( buff, 0, TEXT_BUFF_SIZE );
strcpy( buff, buff_str.c_str() );
}
edit_value = buff;
}
else if( is_all_integers() )
{
// runs when changing a value with slider and not the textbox
auto int_value = static_cast< int >( value->as_float );
if( ImGui::SliderIntWithSteps( id.c_str(),
&int_value,
static_cast< int >( range.min ),
static_cast< int >( range.max ),
static_cast< int >( range.step ) ) )
{
// TODO: Round to step?
slider_clicked = slider_selected( opt,
static_cast< float >( int_value ),
error_message,
model );
}
else
{
slider_clicked = slider_unselected( opt,
static_cast< float >( int_value ),
error_message,
model );
}
}
else
{
float tmp_value = value->as_float;
float temp_value_displayed = tmp_value;
float min_range_displayed = range.min;
float max_range_displayed = range.max;
// computing the number of decimal digits taken from the step options' property
// this will then be used to format the displayed value
auto num_of_decimal_digits = []( float f ) {
float f_0 = std::fabs( f - (int)f );
std::string s = std::to_string( f_0 );
size_t cur_len = s.length();
// removing trailing zeros
while( cur_len > 3 && s[cur_len - 1] == '0' )
cur_len--;
return cur_len - 2;
};
int num_of_decimal_digits_displayed = (int)num_of_decimal_digits( range.step );
// displaying in cm instead of meters for D405
if( use_cm_units )
{
temp_value_displayed *= 100.f;
min_range_displayed *= 100.f;
max_range_displayed *= 100.f;
int updated_num_of_decimal_digits_displayed = num_of_decimal_digits_displayed - 2;
if( updated_num_of_decimal_digits_displayed > 0 )
num_of_decimal_digits_displayed = updated_num_of_decimal_digits_displayed;
}
std::stringstream formatting_ss;
formatting_ss << "%." << num_of_decimal_digits_displayed << "f";
if( ImGui::SliderFloat( id.c_str(),
&temp_value_displayed,
min_range_displayed,
max_range_displayed,
formatting_ss.str().c_str() ) )
{
tmp_value = use_cm_units ? temp_value_displayed / 100.f : temp_value_displayed;
auto loffset = std::abs( fmod( tmp_value, range.step ) );
auto roffset = range.step - loffset;
if( tmp_value >= 0 )
tmp_value = ( loffset < roffset ) ? tmp_value - loffset : tmp_value + roffset;
else
tmp_value = ( loffset < roffset ) ? tmp_value + loffset : tmp_value - roffset;
tmp_value = ( tmp_value < range.min ) ? range.min : tmp_value;
tmp_value = ( tmp_value > range.max ) ? range.max : tmp_value;
slider_clicked = slider_selected( opt, tmp_value, error_message, model );
}
else
{
slider_clicked = slider_unselected( opt, tmp_value, error_message, model );
}
}
}
catch( const error & e )
{
error_message = error_to_string( e );
}
return slider_clicked;
}
bool option_model::is_checkbox() const
{
return range.max == 1.0f && range.min == 0.0f && range.step == 1.0f;
}
bool option_model::draw_checkbox( notifications_model & model,
std::string & error_message,
const char * description )
{
bool checkbox_was_clicked = false;
auto bool_value = value->as_float > 0.0f;
if( ImGui::Checkbox( label.c_str(), &bool_value ) )
{
checkbox_was_clicked = true;
model.add_log( rsutils::string::from() << "Setting " << opt << " to " << ( bool_value ? "1.0" : "0.0" ) << " ("
<< ( bool_value ? "ON" : "OFF" ) << ")" );
set_option( opt, bool_value ? 1.f : 0.f, error_message );
if (invalidate_flag)
*invalidate_flag = true;
}
if( ImGui::IsItemHovered() && description )
{
ImGui::SetTooltip( "%s", description );
}
return checkbox_was_clicked;
}
bool option_model::slider_selected( rs2_option opt,
float value,
std::string & error_message,
notifications_model & model )
{
bool res = false;
auto option_was_set = set_option( opt, value, error_message, std::chrono::milliseconds( 200 ) );
if( option_was_set )
{
have_unset_value = false;
if (invalidate_flag)
*invalidate_flag = true;
model.add_log( rsutils::string::from() << "Setting " << opt << " to " << value );
res = true;
}
else
{
have_unset_value = true;
unset_value = value;
}
return res;
}
bool option_model::slider_unselected( rs2_option opt,
float value,
std::string & error_message,
notifications_model & model )
{
bool res = false;
// Slider unselected, if last value was ignored, set with last value if the value was
// changed.
if( have_unset_value )
{
if( value != unset_value )
{
auto set_ok
= set_option( opt, unset_value, error_message, std::chrono::milliseconds( 100 ) );
if( set_ok )
{
model.add_log( rsutils::string::from() << "Setting " << opt << " to " << unset_value );
if (invalidate_flag)
*invalidate_flag = true;
have_unset_value = false;
res = true;
}
}
else
have_unset_value = false;
}
return res;
}
bool option_model::draw_option(bool update_read_only_options,
bool is_streaming,
std::string& error_message, notifications_model& model)
{
if (update_read_only_options)
{
update_supported(error_message);
if (supported && is_streaming)
{
update_read_only_status(error_message);
if (read_only)
{
update_all_fields(error_message, model);
}
}
}
if (custom_draw_method)
return custom_draw_method(*this, error_message, model);
else
return draw(error_message, model);
}
bool option_model::set_option(rs2_option opt,
float req_value,
std::string& error_message,
std::chrono::steady_clock::duration ignore_period)
{
// Only set the value if `ignore_period` time past since last set_option() call for this option
if (last_set_stopwatch.get_elapsed() < ignore_period)
return false;
try
{
last_set_stopwatch.reset();
endpoint->set_option(opt, req_value);
}
catch (const error& e)
{
error_message = error_to_string(e);
}
// Only update the cached value once set_option is done! That way, if it doesn't change
// anything...
try
{
value = endpoint->get_option_value(opt);
}
catch (...)
{
}
return true;
}