/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include #include #include #include namespace AtomSampleViewer { ScriptableImGui* ScriptableImGui::s_instance = nullptr; void ScriptableImGui::Create() { AZ_Assert(s_instance == nullptr, "instance already called"); s_instance = aznew ScriptableImGui(); } void ScriptableImGui::Destory() { AZ_Assert(s_instance != nullptr, "instance is null"); delete s_instance; s_instance = nullptr; } void ScriptableImGui::CheckAllActionsConsumed() { AZ_DEBUG_STATIC_MEMEBER(instance, s_instance); AZ_Error("Automation", s_instance->m_scriptedActions.empty(), "Not all scripted ImGui actions were consumed"); for (auto iter : s_instance->m_scriptedActions) { AZ_Error("Automation", false, "Scripted action for '%s' not consumed", iter.first.c_str()); } AZ_Error("Automation", s_instance->m_nameContextStack.empty(), "PushNameContext and PopNameContext calls didn't match"); } void ScriptableImGui::ClearActions() { AZ_DEBUG_STATIC_MEMEBER(instance, s_instance); s_instance->m_scriptedActions.clear(); s_instance->m_nameContextStack.clear(); } void ScriptableImGui::PushNameContext(const AZStd::string& nameContext) { AZ_DEBUG_STATIC_MEMEBER(instance, s_instance); s_instance->m_nameContextStack.push_back(nameContext); } void ScriptableImGui::PopNameContext() { AZ_DEBUG_STATIC_MEMEBER(instance, s_instance); AZ_Assert(!s_instance->m_nameContextStack.empty(), "Called PopNameContext too many times"); s_instance->m_nameContextStack.pop_back(); } ScriptableImGui::ActionItem ScriptableImGui::FindAndRemoveAction(const AZStd::string& pathToImGuiItem) { AZ_DEBUG_STATIC_MEMEBER(instance, s_instance); auto iter = s_instance->m_scriptedActions.find(pathToImGuiItem); if (iter != s_instance->m_scriptedActions.end()) { ScriptableImGui::ActionItem item = AZStd::move(iter->second); s_instance->m_scriptedActions.erase(iter); return AZStd::move(item); } return ScriptableImGui::ActionItem{}; } AZStd::string ScriptableImGui::MakeFullPath(const AZStd::string& forLabel) { static constexpr char Delimiter[] = "/"; AZStd::string fullPath; if (!s_instance->m_nameContextStack.empty()) { AzFramework::StringFunc::Join(fullPath, s_instance->m_nameContextStack.begin(), s_instance->m_nameContextStack.end(), Delimiter); fullPath += Delimiter; } fullPath += forLabel; return fullPath; } void ScriptableImGui::ReportScriptError([[maybe_unused]] const char* message) { AZ_Error("Automation", false, "Script: %s", message); } void ScriptableImGui::SetBool(const AZStd::string& pathToImGuiItem, bool value) { AZ_DEBUG_STATIC_MEMEBER(instance, s_instance); s_instance->m_scriptedActions[pathToImGuiItem] = value; } void ScriptableImGui::SetNumber(const AZStd::string& pathToImGuiItem, float value) { AZ_DEBUG_STATIC_MEMEBER(instance, s_instance); s_instance->m_scriptedActions[pathToImGuiItem] = value; } void ScriptableImGui::SetVector(const AZStd::string& pathToImGuiItem, const AZ::Vector2& value) { AZ_DEBUG_STATIC_MEMEBER(instance, s_instance); s_instance->m_scriptedActions[pathToImGuiItem] = value; } void ScriptableImGui::SetVector(const AZStd::string& pathToImGuiItem, const AZ::Vector3& value) { AZ_DEBUG_STATIC_MEMEBER(instance, s_instance); s_instance->m_scriptedActions[pathToImGuiItem] = value; } void ScriptableImGui::SetString(const AZStd::string& pathToImGuiItem, const AZStd::string& value) { AZ_DEBUG_STATIC_MEMEBER(instance, s_instance); s_instance->m_scriptedActions[pathToImGuiItem] = value; } template bool ScriptableImGui::ActionHelper( const char* label, AZStd::function imguiAction, AZStd::function reportScriptableAction, AZStd::function handleScriptedAction, bool shouldReportScriptableActionAfterAnyChange) { bool imResult = imguiAction(); const AZStd::string pathToImGuiItem = MakeFullPath(label); if (ImGui::IsItemDeactivatedAfterEdit() || (shouldReportScriptableActionAfterAnyChange && imResult)) { reportScriptableAction(pathToImGuiItem); } bool scriptResult = false; ActionItem actionItem = FindAndRemoveAction(pathToImGuiItem); bool foundAction = !AZStd::holds_alternative(actionItem); if (foundAction) { if (AZStd::holds_alternative(actionItem)) { scriptResult = handleScriptedAction(AZStd::get(actionItem)); } else { ReportScriptError(AZStd::string::format("Wrong data type used to set '%s'", pathToImGuiItem.c_str()).c_str()); } } return imResult || scriptResult; } bool ScriptableImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) { PushNameContext(name); return ImGui::Begin(name, p_open, flags); } void ScriptableImGui::End() { ImGui::End(); PopNameContext(); } bool ScriptableImGui::Checkbox(const char* label, bool* v) { auto imguiAction = [&]() { return ImGui::Checkbox(label, v); }; auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem) { Utils::ReportScriptableAction("SetImguiValue('%s', %s)", pathToImGuiItem.c_str(), (*v ? "true" : "false")); }; auto handleScriptedAction = [&](bool scriptArg) { if (*v != scriptArg) { *v = scriptArg; return true; } return false; }; return ActionHelper(label, imguiAction, reportScriptableAction, handleScriptedAction); } bool ScriptableImGui::Button(const char* label, const ImVec2& size_arg) { auto imguiAction = [&]() { return ImGui::Button(label, size_arg); }; auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem) { Utils::ReportScriptableAction("SetImguiValue('%s', true)", pathToImGuiItem.c_str()); }; auto handleScriptedAction = [](bool scriptArg) { return scriptArg; }; return ActionHelper(label, imguiAction, reportScriptableAction, handleScriptedAction); } bool ScriptableImGui::ListBox(const char* label, int* current_item, bool(*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int height_in_items) { auto imguiAction = [&]() { return ImGui::ListBox(label, current_item, items_getter, data, items_count, height_in_items); }; auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem) { const char* itemText = nullptr; if (items_getter(data, *current_item, &itemText) && itemText) { Utils::ReportScriptableAction("SetImguiValue('%s', '%s')", pathToImGuiItem.c_str(), itemText); } }; auto handleScriptedAction = [&](AZStd::string scriptArg) { for (int i = 0; i < items_count; ++i) { const char* itemText = nullptr; if (items_getter(data, i, &itemText) && itemText && scriptArg == itemText) { *current_item = i; return true; } } ReportScriptError(AZStd::string::format("List '%s' does not contain item '%s'", label, scriptArg.c_str()).c_str()); return false; }; return ActionHelper(label, imguiAction, reportScriptableAction, handleScriptedAction); } bool ScriptableImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items) { auto imguiAction = [&]() { return ImGui::Combo(label, current_item, items, items_count, height_in_items); }; auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem) { Utils::ReportScriptableAction("SetImguiValue('%s', '%s')", pathToImGuiItem.c_str(), items[*current_item]); }; auto handleScriptedAction = [&](AZStd::string scriptArg) { for (int i = 0; i < items_count; ++i) { if (items[i] == scriptArg) { *current_item = i; return true; } } ReportScriptError(AZStd::string::format("Combo box '%s' does not contain item '%s'", label, scriptArg.c_str()).c_str()); return false; }; return ActionHelper(label, imguiAction, reportScriptableAction, handleScriptedAction, true /* It seems ImGui::Combo doesn't work with IsItemDeactivatedAfterChange() */); } bool ScriptableImGui::RadioButton(const char* label, int* v, int v_button) { auto imguiAction = [&]() { return ImGui::RadioButton(label, v, v_button); }; auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem) { Utils::ReportScriptableAction("SetImguiValue('%s', true)", pathToImGuiItem.c_str()); }; auto handleScriptedAction = [&](bool scriptArg) { if (scriptArg) { *v = v_button; return true; } return false; }; return ActionHelper(label, imguiAction, reportScriptableAction, handleScriptedAction); } bool ScriptableImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format) { auto imguiAction = [&]() { return ImGui::SliderInt(label, v, v_min, v_max, format); }; auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem) { Utils::ReportScriptableAction("SetImguiValue('%s', %d)", pathToImGuiItem.c_str(), *v); }; auto handleScriptedAction = [&](float scriptArg) { *v = aznumeric_cast(scriptArg); return true; }; return ActionHelper(label, imguiAction, reportScriptableAction, handleScriptedAction); } bool ScriptableImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, float power) { auto imguiAction = [&]() { return ImGui::SliderFloat(label, v, v_min, v_max, format, power); }; auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem) { Utils::ReportScriptableAction("SetImguiValue('%s', %f)", pathToImGuiItem.c_str(), *v); }; auto handleScriptedAction = [&](float scriptArg) { *v = scriptArg; return true; }; return ActionHelper(label, imguiAction, reportScriptableAction, handleScriptedAction); } bool ScriptableImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, float power) { auto imguiAction = [&]() { return ImGui::SliderFloat2(label, v, v_min, v_max, format, power); }; auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem) { Utils::ReportScriptableAction("SetImguiValue('%s', Vector2(%f, %f))", pathToImGuiItem.c_str(), v[0], v[1]); }; auto handleScriptedAction = [&](AZ::Vector2 scriptArg) { v[0] = scriptArg.GetX(); v[1] = scriptArg.GetY(); return true; }; return ActionHelper(label, imguiAction, reportScriptableAction, handleScriptedAction); } template bool ScriptableImGui::ThreeComponentHelper(const char* label, float v[3], ImGuiActionType& imguiAction) { auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem) { Utils::ReportScriptableAction("SetImguiValue('%s', Vector3(%f, %f, %f))", pathToImGuiItem.c_str(), v[0], v[1], v[2]); }; auto handleScriptedAction = [&](AZ::Vector3 scriptArg) { v[0] = scriptArg.GetX(); v[1] = scriptArg.GetY(); v[2] = scriptArg.GetZ(); return true; }; return ActionHelper(label, imguiAction, reportScriptableAction, handleScriptedAction); } bool ScriptableImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, float power) { auto imguiAction = [&]() { return ImGui::SliderFloat3(label, v, v_min, v_max, format, power); }; return ThreeComponentHelper(label, v, imguiAction); } bool ScriptableImGui::ColorEdit3(const char* label, float v[3], ImGuiColorEditFlags flags) { auto imguiAction = [&]() { return ImGui::ColorEdit3(label, v, flags); }; return ThreeComponentHelper(label, v, imguiAction); } bool ScriptableImGui::ColorPicker3(const char* label, float v[3], ImGuiColorEditFlags flags) { auto imguiAction = [&]() { return ImGui::ColorPicker3(label, v, flags); }; return ThreeComponentHelper(label, v, imguiAction); } bool ScriptableImGui::SliderAngle(const char* label, float* v, float v_min, float v_max, const char* format) { auto imguiAction = [&]() { return ImGui::SliderAngle(label, v, v_min, v_max, format); }; auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem) { Utils::ReportScriptableAction("SetImguiValue('%s', %f)", pathToImGuiItem.c_str(), *v); }; auto handleScriptedAction = [&](float scriptArg) { *v = scriptArg; return true; }; return ActionHelper(label, imguiAction, reportScriptableAction, handleScriptedAction); } bool ScriptableImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size) { auto imguiAction = [&]() { return ImGui::Selectable(label, selected, flags, size); }; auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem) { // The "selected" value that's passed determines if the selectable is *currently* selected, and clicking the selectable toggles its state. // So when someone clicks the selectable to change its state, we need to report the opposite of what the original state was. Utils::ReportScriptableAction("SetImguiValue('%s', %s)", pathToImGuiItem.c_str(), (selected ? "false" : "true")); }; auto handleScriptedAction = [&](bool scriptArg) { return scriptArg != selected; }; return ActionHelper(label, imguiAction, reportScriptableAction, handleScriptedAction); } bool ScriptableImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size) { auto imguiAction = [&]() { return ImGui::Selectable(label, p_selected, flags, size); }; auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem) { // The "selected" value that's passed determines if the selectable is *currently* selected, and clicking the selectable toggles its state. // So when someone clicks the selectable to change its state, we need to report the opposite of what the original state was. Utils::ReportScriptableAction("SetImguiValue('%s', %s)", pathToImGuiItem.c_str(), (*p_selected ? "false" : "true")); }; auto handleScriptedAction = [&](bool scriptArg) { if (scriptArg != *p_selected) { *p_selected = scriptArg; return true; } return false; }; return ActionHelper(label, imguiAction, reportScriptableAction, handleScriptedAction); } bool ScriptableImGui::TreeNodeEx(const char* label, ImGuiSelectableFlags flags) { AZ_DEBUG_STATIC_MEMEBER(instance, s_instance); if (ImGui::TreeNodeEx(label, flags)) { PushNameContext(label); return true; } return false; } void ScriptableImGui::TreePop() { ImGui::TreePop(); PopNameContext(); } bool ScriptableImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags) { AZ_DEBUG_STATIC_MEMEBER(instance, s_instance); const AZStd::string pathToImGuiItem = MakeFullPath(label); if (ImGui::BeginCombo(label, preview_value, flags)) { PushNameContext(label); return true; } else if (!s_instance->m_scriptedActions.empty()) { // If a script is running, we return true so that imgui controls inside the combo box are checked. // We also have to set a flag to prevent ScriptableImGui::EndCombo() from calling ImGui::EndCombo() // because that is only allowed when ImGui::BeginCombo() returns true. s_instance->m_isInScriptedComboPopup = true; PushNameContext(label); return true; } return false; } void ScriptableImGui::EndCombo() { if (s_instance->m_isInScriptedComboPopup) { s_instance->m_isInScriptedComboPopup = false; // ScriptableImGui::BeginCombo() returned true even though ImGui::BeginCombo() didn't, so we aren't allowed // to call ImGui::EndCombo() here. } else { ImGui::EndCombo(); } PopNameContext(); } bool ScriptableImGui::BeginMenu(const char* label, bool enabled) { // We don't use ActionHelper here because BeginMenu has to do things a bit different since there is a persistent popup. // It has to run the script code before the ImGui code, and if there is a scripted action then force the menu to open. // Also, we don't include a "scriptResult", just the "imResult", because we need to ensure that ImGui is in the actual // state we are reporting back to the caller. Otherwise the internal state of ImGui could become invalid and crash. const AZStd::string pathToImGuiItem = MakeFullPath(label); ActionItem actionItem = FindAndRemoveAction(pathToImGuiItem); bool foundAction = !AZStd::holds_alternative(actionItem); if (foundAction) { if (AZStd::holds_alternative(actionItem)) { if (AZStd::get(actionItem)) { // Here we force the menu to open before arriving at ImGui::BeginMenu ImGui::OpenPopup(label); } } else { ReportScriptError(AZStd::string::format("Wrong data type used to set '%s'", label).c_str()); } } bool wasPopupOpen = ImGui::IsPopupOpen(label); bool isPopupOpen = ImGui::BeginMenu(label, enabled); if (isPopupOpen) { PushNameContext(label); if (!wasPopupOpen && isPopupOpen) { Utils::ReportScriptableAction("SetImguiValue('%s', true)", pathToImGuiItem.c_str()); } } return isPopupOpen; } void ScriptableImGui::EndMenu() { ImGui::EndMenu(); PopNameContext(); } bool ScriptableImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled) { auto imguiAction = [&]() { return ImGui::MenuItem(label, shortcut, selected, enabled); }; auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem) { Utils::ReportScriptableAction("SetImguiValue('%s', true)", pathToImGuiItem.c_str()); }; auto handleScriptedAction = [](bool scriptArg) { return scriptArg; }; return ActionHelper(label, imguiAction, reportScriptableAction, handleScriptedAction); } } // namespace AtomSampleViewer