Make studio python more like studio C++

- C++ Platform UX: Added a "Copy Camera" button to copy XML camera definitions to clipboard.
  - C++ Platform UX: Extracted `SetSpeedIndex` and `GetExpectedLabelWidth` into reusable platform helpers.
  - C++ Platform UX: Fixed combo box widths and removed auto-hide tab bar flag from dockspace.
  - C++ Studio App: Deduplicated label width and speed index logic by adopting platform helpers.
  - C++ Studio App: Updated the toolbar layout to use a 2-column table.
  - Python Bindings: Added pybind definitions for `set_camera_index`, `set_speed_index`, and `camera_to_string`.
  - Python Studio App: Aligned toolbar UI with C++ using a 2-column table and fixed immediate theme switching.
  - Python Studio App: Added a Help menu displaying the MuJoCo version and Stats toggle.
  - Python Events: Added `-`/`=` shortcuts for simulation speed and `Esc`/`[`/`]` shortcuts for camera selection.

PiperOrigin-RevId: 922097319
Change-Id: Iec74afe37bf5c7a1ebf1cab4141e47fff5fe9274
This commit is contained in:
Matija Kecman
2026-05-27 06:42:17 -07:00
committed by Copybara-Service
parent 79aeceeaa3
commit c0b8aa9dec
6 changed files with 59 additions and 54 deletions
+7 -47
View File
@@ -826,14 +826,7 @@ void App::SaveSettings() {
}
void App::SetSpeedIndex(int idx) {
if (idx == tmp_.speed_index || platform::kPercentRealTime.empty()) {
return;
}
tmp_.speed_index =
std::clamp<int>(idx, 0, platform::kPercentRealTime.size() - 1);
float speed = std::stof(platform::kPercentRealTime[tmp_.speed_index]);
step_control_.SetSpeed(speed);
platform::SetSpeedIndex(&step_control_, tmp_.speed_index, idx);
}
void App::MoveCamera(platform::CameraMotion motion, mjtNum reldx,
@@ -1022,7 +1015,7 @@ void App::BuildGui() {
}
void App::ModelOptionsGui() {
const float min_width = GetExpectedLabelWidth();
const float min_width = platform::GetExpectedLabelWidth();
const ImGuiChildFlags child_flags =
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize;
const ImGuiTreeNodeFlags node_flags =
@@ -1064,7 +1057,7 @@ void App::DataInspectorGui() {
return;
}
const float min_width = GetExpectedLabelWidth();
const float min_width = platform::GetExpectedLabelWidth();
const ImGuiChildFlags child_flags =
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize;
const ImGuiTreeNodeFlags node_flags =
@@ -1424,15 +1417,14 @@ void App::ToolBarGui() {
ImVec2(ImGui::GetStyle().ItemSpacing.x * 2.0f,
ImGui::GetStyle().ItemSpacing.y));
const float label_width = GetExpectedLabelWidth();
const float copy_btn_width = ImGui::CalcTextSize(ICON_COPY_CAMERA).x +
ImGui::GetStyle().FramePadding.x * 2;
const float label_width = platform::GetExpectedLabelWidth();
const float copy_btn_width = ImGui::GetFrameHeight();
const float theme_width =
ImGui::CalcTextSize(platform::ICON_FA_CIRCLE_O).x +
ImGui::GetStyle().FramePadding.x * 2;
const float sp = ImGui::GetStyle().ItemSpacing.x;
const float right_width = label_width + sp + label_width + sp +
label_width + sp + copy_btn_width + sp +
const float right_width = copy_btn_width + label_width + sp +
label_width + sp + label_width + sp +
theme_width;
const float separator_width = ImGui::GetFrameHeight() * .6f;
@@ -1467,26 +1459,15 @@ void App::ToolBarGui() {
ImGui::TableNextColumn();
if (ImGui::Button(ICON_COPY_CAMERA, square_size)) {
std::string camera_string = platform::CameraToString(data(), &camera_);
platform::MaybeSaveToClipboard(camera_string);
}
ImGui::SetItemTooltip("%s", "Copy Camera");
ImGui::SameLine(0, 0);
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
platform::CameraSelectionGui(model(), data(), camera_, ui_.camera_idx);
ImGui::SameLine();
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
platform::LabelSelectionGui(&vis_options_);
ImGui::SameLine();
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
platform::FrameSelectionGui(&vis_options_);
ImGui::SameLine();
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
if (platform::ThemeSelectGui(&ui_.theme, square_size)) {
platform::SetupTheme(ui_.theme);
ImGui::GetIO().WantSaveIniSettings = true;
@@ -1836,27 +1817,6 @@ void App::FileDialogGui() {
}
}
float App::GetExpectedLabelWidth() {
// Find the longest label which we'll use to set the minimum toggle button
// width. This isn't perfect because we may have labels that are longer, but
// it's a good enough approximation.
if (tmp_.expected_label_width == 0) {
int longest = 0;
const char* longest_label = "";
for (int i = 0; i < mjNVISFLAG; ++i) {
int length = static_cast<int>(strlen(mjVISSTRING[i][0]));
if (length > longest) {
longest_label = mjVISSTRING[i][0];
longest = length;
}
}
// Pad the width a bit to account for how the labels will be displayed
// (e.g. as button labels or besides checkboxes).
tmp_.expected_label_width = ImGui::CalcTextSize(longest_label).x + 16;
}
return tmp_.expected_label_width;
}
App::UiState::Dict App::UiState::ToDict() const {
return {
{"theme", std::to_string(static_cast<int>(theme))},
-2
View File
@@ -224,8 +224,6 @@ class App {
void SpecExplorerGui();
void SpecEditorGui();
float GetExpectedLabelWidth();
mjSpec* spec() { return model_holder_->spec(); }
mjModel* model() { return model_holder_->model(); }
mjData* data() { return model_holder_->data(); }