diff --git a/ihmc-graphics/src/libgdx/java/us/ihmc/rdx/imgui/ImGuiTools.java b/ihmc-graphics/src/libgdx/java/us/ihmc/rdx/imgui/ImGuiTools.java index 2f23d679dc55..87370b1ca94a 100644 --- a/ihmc-graphics/src/libgdx/java/us/ihmc/rdx/imgui/ImGuiTools.java +++ b/ihmc-graphics/src/libgdx/java/us/ihmc/rdx/imgui/ImGuiTools.java @@ -420,9 +420,9 @@ public static boolean isItemHovered(float itemWidth, float lineHeight) float mousePosXInWidgetFrame = mousePosXInDesktopFrame - ImGui.getWindowPosX() + ImGui.getScrollX(); float mousePosYInWidgetFrame = mousePosYInDesktopFrame - ImGui.getWindowPosY() + ImGui.getScrollY(); - boolean isHovered = mousePosXInWidgetFrame >= ImGui.getCursorPosX(); + boolean isHovered = mousePosXInWidgetFrame > ImGui.getCursorPosX(); isHovered &= mousePosXInWidgetFrame <= ImGui.getCursorPosX() + itemWidth + ImGui.getStyle().getFramePaddingX(); - isHovered &= mousePosYInWidgetFrame >= ImGui.getCursorPosY(); + isHovered &= mousePosYInWidgetFrame > ImGui.getCursorPosY(); isHovered &= mousePosYInWidgetFrame <= ImGui.getCursorPosY() + lineHeight; isHovered &= ImGui.isWindowHovered(); diff --git a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/sequence/RDXActionProgressWidgets.java b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/sequence/RDXActionProgressWidgets.java index 9282f9b27a6f..4a87788de4e3 100644 --- a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/sequence/RDXActionProgressWidgets.java +++ b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/sequence/RDXActionProgressWidgets.java @@ -3,7 +3,7 @@ import imgui.extension.implot.ImPlot; import imgui.extension.implot.flag.ImPlotFlags; import imgui.flag.ImGuiCond; -import imgui.internal.ImGui; +import imgui.ImGui; import us.ihmc.behaviors.sequence.ActionNodeState; import us.ihmc.behaviors.sequence.actions.FootstepPlanActionState; import us.ihmc.commons.thread.ThreadTools; @@ -25,7 +25,6 @@ public class RDXActionProgressWidgets { - public static final float PROGRESS_BAR_HEIGHT = 18.0f; public static final float PLOT_HEIGHT = 40.0f; public static final int MAX_WAYPOINTS = 500; public static final ReferenceFrame worldFrame = ReferenceFrame.getWorldFrame(); @@ -152,7 +151,7 @@ public void renderElapsedTimeBar(float dividedBarWidth) double nominalDuration = action.getState().getNominalExecutionDuration(); double percentComplete = elapsedExecutionTime / nominalDuration; double percentLeft = 1.0 - percentComplete; - ImGui.progressBar((float) percentLeft, dividedBarWidth, PROGRESS_BAR_HEIGHT, "%.2f / %.2f".formatted(elapsedExecutionTime, nominalDuration)); + ImGui.progressBar((float) percentLeft, dividedBarWidth, progressBarHeight(), "%.2f / %.2f".formatted(elapsedExecutionTime, nominalDuration)); } public void renderPositionError(float dividedBarWidth, boolean renderAsPlots) @@ -198,7 +197,7 @@ public void renderPositionError(float dividedBarWidth, boolean renderAsPlots) double barEndValue = Math.max(Math.min(initialToEnd, currentToEnd), 2.0 * tolerance); double toleranceMarkPercent = tolerance / barEndValue; double percentLeft = currentToEnd / barEndValue; - ImGuiTools.markedProgressBar(PROGRESS_BAR_HEIGHT, + ImGuiTools.markedProgressBar(progressBarHeight(), dividedBarWidth, dataColor, percentLeft, @@ -255,7 +254,7 @@ public void renderOrientationError(float dividedBarWidth, boolean renderAsPlots) double barEndValue = Math.max(Math.min(initialToEnd, currentToEnd), 2.0 * tolerance); double toleranceMarkPercent = tolerance / barEndValue; double percentLeft = currentToEnd / barEndValue; - ImGuiTools.markedProgressBar(PROGRESS_BAR_HEIGHT, + ImGuiTools.markedProgressBar(progressBarHeight(), dividedBarWidth, dataColor, percentLeft, @@ -312,7 +311,7 @@ public void renderJointspacePositionError(int jointIndex, float dividedBarWidth, double barEndValue = Math.max(Math.min(initialToEnd, currentToEnd), 2.0 * tolerance); double toleranceMarkPercent = tolerance / barEndValue; double percentLeft = currentToEnd / barEndValue; - ImGuiTools.markedProgressBar(PROGRESS_BAR_HEIGHT, + ImGuiTools.markedProgressBar(progressBarHeight(), dividedBarWidth, dataColor, percentLeft, @@ -351,7 +350,7 @@ else if (action instanceof RDXScrewPrimitiveAction screwPrimitiveAction) } else { - ImGuiTools.markedProgressBar(PROGRESS_BAR_HEIGHT, dividedBarWidth, dataColor, force / limit, 0.5, "%.2f".formatted(force)); + ImGuiTools.markedProgressBar(progressBarHeight(), dividedBarWidth, dataColor, force / limit, 0.5, "%.2f".formatted(force)); } } else @@ -385,7 +384,7 @@ else if (action instanceof RDXScrewPrimitiveAction screwPrimitiveAction) } else { - ImGuiTools.markedProgressBar(PROGRESS_BAR_HEIGHT, dividedBarWidth, dataColor, torque / limit, 0.5, "%.2f".formatted(torque)); + ImGuiTools.markedProgressBar(progressBarHeight(), dividedBarWidth, dataColor, torque / limit, 0.5, "%.2f".formatted(torque)); } } else @@ -412,7 +411,7 @@ public void renderFootstepCompletion(float dividedBarWidth, boolean renderAsPlot } else { - ImGui.progressBar((float) percentLeft, dividedBarWidth, PROGRESS_BAR_HEIGHT, overlay); + ImGui.progressBar((float) percentLeft, dividedBarWidth, progressBarHeight(), overlay); } } else @@ -465,7 +464,7 @@ public void renderFootPositions(float dividedBarWidth, boolean renderAsPlots) double barEndValue = Math.max(Math.min(initialToEnd, currentToEnd), 2.0 * tolerance); double toleranceMarkPercent = tolerance / barEndValue; double percentLeft = currentToEnd / barEndValue; - ImGuiTools.markedProgressBar(PROGRESS_BAR_HEIGHT, + ImGuiTools.markedProgressBar(progressBarHeight(), halfDividedBarWidth, dataColor, percentLeft, @@ -527,7 +526,7 @@ public void renderFootOrientations(float dividedBarWidth, boolean renderAsPlots) double barEndValue = Math.max(Math.min(initialToEnd, currentToEnd), 2.0 * tolerance); double toleranceMarkPercent = tolerance / barEndValue; double percentLeft = currentToEnd / barEndValue; - ImGuiTools.markedProgressBar(PROGRESS_BAR_HEIGHT, + ImGuiTools.markedProgressBar(progressBarHeight(), halfDividedBarWidth, dataColor, percentLeft, @@ -554,11 +553,16 @@ public static void renderBlankProgress(String emptyPlotLabel, float width, boole { if (renderAsPlots && supportsPlots) { - ImPlotTools.renderEmptyPlotArea(emptyPlotLabel, width, RDXActionProgressWidgets.PLOT_HEIGHT); + ImPlotTools.renderEmptyPlotArea(emptyPlotLabel, width, PLOT_HEIGHT); } else { - ImGui.progressBar(Float.NaN, width, RDXActionProgressWidgets.PROGRESS_BAR_HEIGHT, ""); + ImGui.progressBar(Float.NaN, width, progressBarHeight(), ""); } } + + private static float progressBarHeight() + { + return 1.2f * ImGui.getTextLineHeight(); + } } \ No newline at end of file diff --git a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/sequence/RDXActionProgressWidgetsManager.java b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/sequence/RDXActionProgressWidgetsManager.java index df2cda1cc8f6..f4063fca3267 100644 --- a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/sequence/RDXActionProgressWidgetsManager.java +++ b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/sequence/RDXActionProgressWidgetsManager.java @@ -6,6 +6,7 @@ import us.ihmc.rdx.ui.behavior.actions.RDXHandPoseAction; import us.ihmc.rdx.ui.behavior.actions.RDXSakeHandCommandAction; import us.ihmc.rdx.ui.behavior.actions.RDXScrewPrimitiveAction; +import us.ihmc.rdx.ui.behavior.tree.RDXBehaviorTree; import us.ihmc.robotics.EuclidCoreMissingTools; import java.util.ArrayList; @@ -13,13 +14,26 @@ import java.util.SortedSet; import java.util.TreeSet; +/** + * Renders action execution status tracking UI elements. + * + * TODO: Add even more compressed representation. Radial plots. + */ public class RDXActionProgressWidgetsManager { + public enum Type + { + /** Show only the estimated time remaining as a progress bar.. */ + TIME_ONLY, + /** Show progress bars which are more compact than full plots. */ + PROGRESS_BARS, + /** Show full side scrolling plots which show more information about execution but take up more space. */ + SCROLLING_PLOTS + } private final ImGuiUniqueLabelMap labels = new ImGuiUniqueLabelMap(getClass()); private final ImGuiLabelledWidgetAligner widgetAligner = new ImGuiLabelledWidgetAligner(); private final SortedSet> sortedActionNodesToRender = new TreeSet<>(Comparator.comparingInt(node -> node.getState().getLeafIndex())); private final ArrayList> actionNodesToRender = new ArrayList<>(); - private boolean renderAsPlots = true; private int emptyPlotIndex; private int numberOfLines; @@ -52,6 +66,7 @@ public void render() } boolean showPosePlots = containsFootsteps || containsHandMovements; + ImGui.spacing(); widgetAligner.text("Expected time remaining:"); float dividedBarWidth = computeDividedBarWidth(); // Must be computed after above text handleRenderingBlankBar(false); @@ -63,95 +78,100 @@ public void render() ++numberOfLines; ImGui.spacing(); - if (containsFootsteps) - { - widgetAligner.text("Footstep completion:"); - handleRenderingBlankBar(true); - for (int i = 0; i < actionNodesToRender.size(); i++) - { - actionNodesToRender.get(i).getProgressWidgets().renderFootstepCompletion(dividedBarWidth, renderAsPlots); - sameLineExceptLast(i); - } - ++numberOfLines; - ImGui.spacing(); - } - - if (showPosePlots) - { - widgetAligner.text("Position error (m):"); - handleRenderingBlankBar(true); - for (int i = 0; i < actionNodesToRender.size(); i++) - { - if (actionNodesToRender.get(i) instanceof RDXFootstepPlanAction) - actionNodesToRender.get(i).getProgressWidgets().renderFootPositions(dividedBarWidth, renderAsPlots); - else - actionNodesToRender.get(i).getProgressWidgets().renderPositionError(dividedBarWidth, renderAsPlots); - sameLineExceptLast(i); - } - ++numberOfLines; - ImGui.spacing(); - - widgetAligner.text("Orientation error (%s):".formatted(EuclidCoreMissingTools.DEGREE_SYMBOL)); - handleRenderingBlankBar(true); - for (int i = 0; i < actionNodesToRender.size(); i++) - { - if (actionNodesToRender.get(i) instanceof RDXFootstepPlanAction) - actionNodesToRender.get(i).getProgressWidgets().renderFootOrientations(dividedBarWidth, renderAsPlots); - else - actionNodesToRender.get(i).getProgressWidgets().renderOrientationError(dividedBarWidth, renderAsPlots); - sameLineExceptLast(i); - } - ++numberOfLines; - ImGui.spacing(); - } - - if (containsHandMovements) + boolean timeOnly = RDXBehaviorTree.SETTINGS.getProgressWidgetsType() == Type.TIME_ONLY; + boolean renderAsPlots = RDXBehaviorTree.SETTINGS.getProgressWidgetsType() == Type.SCROLLING_PLOTS; + if (!timeOnly) { - widgetAligner.text("Hand force (N):"); - handleRenderingBlankBar(true); - for (int i = 0; i < actionNodesToRender.size(); i++) + if (containsFootsteps) { - actionNodesToRender.get(i).getProgressWidgets().renderHandForce(dividedBarWidth, renderAsPlots); - sameLineExceptLast(i); + widgetAligner.text("Footstep completion:"); + handleRenderingBlankBar(true); + for (int i = 0; i < actionNodesToRender.size(); i++) + { + actionNodesToRender.get(i).getProgressWidgets().renderFootstepCompletion(dividedBarWidth, renderAsPlots); + sameLineExceptLast(i); + } + ++numberOfLines; + ImGui.spacing(); } - ++numberOfLines; - ImGui.spacing(); - widgetAligner.text("Hand torque (Nm):"); - handleRenderingBlankBar(true); - for (int i = 0; i < actionNodesToRender.size(); i++) + if (showPosePlots) { - actionNodesToRender.get(i).getProgressWidgets().renderHandTorque(dividedBarWidth, renderAsPlots); - sameLineExceptLast(i); + widgetAligner.text("Position error (m):"); + handleRenderingBlankBar(true); + for (int i = 0; i < actionNodesToRender.size(); i++) + { + if (actionNodesToRender.get(i) instanceof RDXFootstepPlanAction) + actionNodesToRender.get(i).getProgressWidgets().renderFootPositions(dividedBarWidth, renderAsPlots); + else + actionNodesToRender.get(i).getProgressWidgets().renderPositionError(dividedBarWidth, renderAsPlots); + sameLineExceptLast(i); + } + ++numberOfLines; + ImGui.spacing(); + + widgetAligner.text("Orientation error (%s):".formatted(EuclidCoreMissingTools.DEGREE_SYMBOL)); + handleRenderingBlankBar(true); + for (int i = 0; i < actionNodesToRender.size(); i++) + { + if (actionNodesToRender.get(i) instanceof RDXFootstepPlanAction) + actionNodesToRender.get(i).getProgressWidgets().renderFootOrientations(dividedBarWidth, renderAsPlots); + else + actionNodesToRender.get(i).getProgressWidgets().renderOrientationError(dividedBarWidth, renderAsPlots); + sameLineExceptLast(i); + } + ++numberOfLines; + ImGui.spacing(); } - ++numberOfLines; - ImGui.spacing(); - } - if (containsHandConfiguration) - { - widgetAligner.text("Knuckle X1 (%s):".formatted(EuclidCoreMissingTools.DEGREE_SYMBOL)); - handleRenderingBlankBar(true); - for (int i = 0; i < actionNodesToRender.size(); i++) + if (containsHandMovements) { - actionNodesToRender.get(i).getProgressWidgets().renderJointspacePositionError(0, dividedBarWidth, renderAsPlots); - sameLineExceptLast(i); + widgetAligner.text("Hand force (N):"); + handleRenderingBlankBar(true); + for (int i = 0; i < actionNodesToRender.size(); i++) + { + actionNodesToRender.get(i).getProgressWidgets().renderHandForce(dividedBarWidth, renderAsPlots); + sameLineExceptLast(i); + } + ++numberOfLines; + ImGui.spacing(); + + widgetAligner.text("Hand torque (Nm):"); + handleRenderingBlankBar(true); + for (int i = 0; i < actionNodesToRender.size(); i++) + { + actionNodesToRender.get(i).getProgressWidgets().renderHandTorque(dividedBarWidth, renderAsPlots); + sameLineExceptLast(i); + } + ++numberOfLines; + ImGui.spacing(); } - ++numberOfLines; - ImGui.spacing(); - widgetAligner.text("Knuckle X2 (%s):".formatted(EuclidCoreMissingTools.DEGREE_SYMBOL)); - handleRenderingBlankBar(true); - for (int i = 0; i < actionNodesToRender.size(); i++) + if (containsHandConfiguration) { - actionNodesToRender.get(i).getProgressWidgets().renderJointspacePositionError(1, dividedBarWidth, renderAsPlots); - sameLineExceptLast(i); + widgetAligner.text("Knuckle X1 (%s):".formatted(EuclidCoreMissingTools.DEGREE_SYMBOL)); + handleRenderingBlankBar(true); + for (int i = 0; i < actionNodesToRender.size(); i++) + { + actionNodesToRender.get(i).getProgressWidgets().renderJointspacePositionError(0, dividedBarWidth, renderAsPlots); + sameLineExceptLast(i); + } + ++numberOfLines; + ImGui.spacing(); + + widgetAligner.text("Knuckle X2 (%s):".formatted(EuclidCoreMissingTools.DEGREE_SYMBOL)); + handleRenderingBlankBar(true); + for (int i = 0; i < actionNodesToRender.size(); i++) + { + actionNodesToRender.get(i).getProgressWidgets().renderJointspacePositionError(1, dividedBarWidth, renderAsPlots); + sameLineExceptLast(i); + } + ++numberOfLines; + ImGui.spacing(); } - ++numberOfLines; - ImGui.spacing(); } - while (numberOfLines < 5) + while (numberOfLines < (timeOnly ? 1 : 5)) { widgetAligner.text(""); renderBlankBar(true); @@ -190,19 +210,10 @@ private void handleRenderingBlankBar(boolean supportsPlots) private void renderBlankBar(boolean supportsPlots) { + boolean renderAsPlots = RDXBehaviorTree.SETTINGS.getProgressWidgetsType() == Type.SCROLLING_PLOTS; RDXActionProgressWidgets.renderBlankProgress(labels.get("Empty Plot", emptyPlotIndex++), ImGui.getColumnWidth(), renderAsPlots, supportsPlots); } - public boolean getRenderAsPlots() - { - return renderAsPlots; - } - - public void setRenderAsPlots(boolean renderAsPlots) - { - this.renderAsPlots = renderAsPlots; - } - public SortedSet> getActionNodesToRender() { return sortedActionNodesToRender; diff --git a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTree.java b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTree.java index fb1396aa03c8..1dba78761aee 100644 --- a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTree.java +++ b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTree.java @@ -6,13 +6,15 @@ import gnu.trove.map.TLongObjectMap; import gnu.trove.map.hash.TLongObjectHashMap; import imgui.ImGui; +import imgui.flag.ImGuiMouseButton; +import imgui.flag.ImGuiMouseCursor; import us.ihmc.avatar.drcRobot.DRCRobotModel; import us.ihmc.avatar.drcRobot.ROS2SyncedRobotModel; import us.ihmc.behaviors.behaviorTree.BehaviorTree; import us.ihmc.behaviors.behaviorTree.topology.BehaviorTreeNodeInsertionType; +import us.ihmc.commons.MathTools; import us.ihmc.communication.ros2.ROS2ActorDesignation; import us.ihmc.communication.ros2.sync.ROS2PeerClockOffsetEstimator; -import us.ihmc.rdx.imgui.ImGuiExpandCollapseRenderer; import us.ihmc.rdx.imgui.ImGuiTools; import us.ihmc.rdx.imgui.ImGuiUniqueLabelMap; import us.ihmc.rdx.imgui.RDXPanel; @@ -20,6 +22,7 @@ import us.ihmc.rdx.sceneManager.RDXSceneLevel; import us.ihmc.rdx.ui.RDX3DPanel; import us.ihmc.rdx.ui.RDXBaseUI; +import us.ihmc.rdx.ui.behavior.sequence.RDXActionProgressWidgetsManager.Type; import us.ihmc.rdx.vr.RDXVRContext; import us.ihmc.robotics.physics.RobotCollisionModel; import us.ihmc.robotics.referenceFrames.ReferenceFrameLibrary; @@ -27,6 +30,7 @@ public class RDXBehaviorTree extends BehaviorTree> { + public static final RDXBehaviorTreeSettings SETTINGS = new RDXBehaviorTreeSettings(); private RDXBehaviorTreeRootNode rootNode; /** * Useful for accessing nodes by ID instead of searching. @@ -41,7 +45,7 @@ public class RDXBehaviorTree extends BehaviorTree> private final RDXBehaviorTreeWidgetsVerticalLayout treeWidgetsVerticalLayout; private boolean anyNodeSelected; private RDXBehaviorTreeNode selectedNode; - private boolean enableChildScrollableAreas; + private boolean draggingDivider; public RDXBehaviorTree(WorkspaceResourceDirectory treeFilesDirectory, DRCRobotModel robotModel, @@ -151,6 +155,21 @@ protected void renderImGuiWidgetsPre() { ImGui.beginMenuBar(); fileMenu.renderFileMenu(rootNode, nodeCreationMenu); + if (ImGui.beginMenu(labels.get("View"))) + { + if (rootNode != null) + { + ImGui.text("Progress Widgets:"); + if (ImGui.menuItem(labels.get("Time Only"), null, SETTINGS.getProgressWidgetsType() == Type.TIME_ONLY)) + SETTINGS.setProgressWidgetsType(Type.TIME_ONLY); + if (ImGui.menuItem(labels.get("Progress Bars"), null, SETTINGS.getProgressWidgetsType() == Type.PROGRESS_BARS)) + SETTINGS.setProgressWidgetsType(Type.PROGRESS_BARS); + if (ImGui.menuItem(labels.get("Scrolling Plots"), null, SETTINGS.getProgressWidgetsType() == Type.SCROLLING_PLOTS)) + SETTINGS.setProgressWidgetsType(Type.SCROLLING_PLOTS); + } + + ImGui.endMenu(); + } } protected void renderImGuiWidgetsPost() @@ -159,79 +178,45 @@ protected void renderImGuiWidgetsPost() { rootNode.renderExecutionControlAndProgressWidgets(); - float cursorYAfterControlWidgets = ImGui.getCursorPosY(); - anyNodeSelected = false; - RDXBehaviorTreeTools.runForSubtreeNodes(rootNode, node -> anyNodeSelected |= node.getSelected()); - - float titleHeight = ImGui.getFrameHeightWithSpacing(); - float menuBarHeight = ImGui.getFrameHeightWithSpacing(); - float windowHeight = ImGui.getWindowHeight(); - float availableHeight = windowHeight - titleHeight - menuBarHeight - cursorYAfterControlWidgets; - // There are ~9 rows of stuff in the screw primitive action settings, - // which is the tallest one currently. We could think of ways to improve on this. - float tallestNodeSettings = 9 * ImGui.getFrameHeightWithSpacing(); - - // 60% seems to be the desirable ratio for the visible area - // of the tree view vs the settings area - float treeExplorerPercentage = 0.6f; - float treeExplorerHeight = availableHeight * treeExplorerPercentage; - float nodeSettingsHeight = availableHeight * (1.0f - treeExplorerPercentage); - - float treeContentStartY = ImGui.getCursorPosY(); - - if (enableChildScrollableAreas) - ImGui.beginChild(labels.get("Tree Explorer Scroll Area"), 0.0f, treeExplorerHeight); - - treeWidgetsVerticalLayout.renderImGuiWidgets(rootNode); - - boolean updatedEnableChildScrollableAreas; - float treeContentHeight; - if (enableChildScrollableAreas) - { - float scrollMaxY = ImGui.getScrollMaxY(); - float childWindowHeight = ImGui.getWindowHeight(); - if (scrollMaxY == 0.0) - treeContentHeight = ImGui.getCursorPosY(); - else - treeContentHeight = childWindowHeight + scrollMaxY; - } - else + RDXBehaviorTreeTools.runForSubtreeNodes(rootNode, node -> { - treeContentHeight = ImGui.getCursorPosY() - treeContentStartY; - } - updatedEnableChildScrollableAreas = windowHeight - treeContentStartY - treeContentHeight < tallestNodeSettings; - - if (enableChildScrollableAreas) - ImGui.endChild(); + anyNodeSelected |= node.getSelected(); + if (node.getSelected()) + selectedNode = node; + }); - enableChildScrollableAreas = updatedEnableChildScrollableAreas; + float remainingHeight = ImGui.getContentRegionAvailY(); + float treeExplorerPercentage = SETTINGS.getTreeExplorerHeightPercentage(); + float treeExplorerHeight = anyNodeSelected ? remainingHeight * treeExplorerPercentage : remainingHeight; - ImGui.spacing(); - ImGui.spacing(); + ImGui.beginChild(labels.get("Tree Explorer Scroll Area"), 0.0f, treeExplorerHeight); + treeWidgetsVerticalLayout.renderImGuiWidgets(); + ImGui.endChild(); - if (rootNode != null) // It can become null above + if (rootNode != null && anyNodeSelected) // It can become null above { - anyNodeSelected = false; - RDXBehaviorTreeTools.runForSubtreeNodes(rootNode, node -> + if (ImGuiTools.isItemHovered(ImGui.getColumnWidth(), ImGui.getTextLineHeight()) || draggingDivider) { - anyNodeSelected |= node.getSelected(); - if (node.getSelected()) - selectedNode = node; - }); - - if (anyNodeSelected) - ImGuiTools.separatorText("Node Settings > \"%s\"".formatted(selectedNode.getDefinition().getName())); - else - ImGuiTools.separatorText("Node Settings"); - - if (enableChildScrollableAreas) - ImGui.beginChild(labels.get("Node Settings Scroll Area"), 0.0f, nodeSettingsHeight); + if (!draggingDivider || ImGui.isMouseDown(ImGuiMouseButton.Left)) + { + ImGui.setMouseCursor(ImGuiMouseCursor.ResizeNS); + if (ImGui.isMouseDragging(ImGuiMouseButton.Left, 0.1f)) // default threshold 0.3f is too much + { + treeExplorerPercentage += ImGui.getIO().getMouseDeltaY() / remainingHeight; + treeExplorerPercentage = (float) MathTools.clamp(treeExplorerPercentage, 0.05f, 0.95f); + SETTINGS.setTreeExplorerHeightPercentage(treeExplorerPercentage); + draggingDivider = true; + } + } + else + draggingDivider = false; + } + ImGuiTools.separatorText("Node Settings > \"%s\"".formatted(selectedNode.getDefinition().getName())); + ImGui.beginChild(labels.get("Node Settings Scroll Area"), 0.0f, ImGui.getContentRegionAvailY()); renderSelectedNodeSettingsWidgets(rootNode); - - if (enableChildScrollableAreas) - ImGui.endChild(); + ImGui.endChild(); if (ImGui.isWindowHovered() && ImGui.getIO().getKeyCtrl() && ImGui.isKeyPressed('S')) { diff --git a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeFileMenu.java b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeFileMenu.java index 5da4ef359877..8ca5143a9d39 100644 --- a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeFileMenu.java +++ b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeFileMenu.java @@ -1,7 +1,6 @@ package us.ihmc.rdx.ui.behavior.tree; import imgui.ImGui; -import us.ihmc.commons.thread.Notification; import us.ihmc.rdx.imgui.ImGuiUniqueLabelMap; import us.ihmc.rdx.ui.RDXBaseUI; @@ -10,11 +9,10 @@ public class RDXBehaviorTreeFileMenu { private final ImGuiUniqueLabelMap labels = new ImGuiUniqueLabelMap(getClass()); - private final Notification menuShouldClose = new Notification(); public void renderFileMenu(@Nullable RDXBehaviorTreeNode rootNode, RDXBehaviorTreeNodeCreationMenu nodeCreationMenu) { - if (ImGui.beginMenu(labels.get("File"), !menuShouldClose.poll())) + if (ImGui.beginMenu(labels.get("File"))) { if (rootNode != null) { diff --git a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeNode.java b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeNode.java index 9d972ea1f9d4..3bfd3d92735c 100644 --- a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeNode.java +++ b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeNode.java @@ -57,6 +57,10 @@ public class RDXBehaviorTreeNode, private final String nodePopupID = labels.get("Node popup"); private String modalPopupID = labels.get("Create node"); private final ImGuiScrollableLogArea logArea = new ImGuiScrollableLogArea(); + private RDXBehaviorTreeNode draggedNode = null; + private boolean dragging = false; + private boolean dragReleasedBefore = false; + private boolean dragReleasedAfter = false; /** For extending types. */ public RDXBehaviorTreeNode(S state) @@ -132,10 +136,44 @@ public void renderRowBeginning() lineMin.y = indentMin.y; lineMax.set(indentMin.x + ImGui.getContentRegionAvailX(), lineMin.y + ImGui.getFrameHeight()); - mouseHoveringNodeLine = ImGuiTools.isItemHovered(ImGui.getContentRegionAvailX(), ImGui.getFrameHeight()); + mouseHoveringNodeLine = ImGui.isWindowHovered(); + mouseHoveringNodeLine &= ImGui.getMousePosX() > lineMin.x && ImGui.getMousePosX() <= lineMax.x; + mouseHoveringNodeLine &= ImGui.getMousePosY() > lineMin.y && ImGui.getMousePosY() <= lineMax.y; + dragging = false; + dragReleasedBefore = false; + dragReleasedAfter = false; if (mouseHoveringNodeLine) + { ImGui.getWindowDrawList().addRectFilled(lineMin.x, lineMin.y, lineMax.x, lineMax.y, ImGui.getColorU32(ImGuiCol.MenuBarBg)); + if (!isRootNode()) + { + if (ImGui.isMouseDragging(ImGuiMouseButton.Left)) + { + float dragStartX = ImGui.getMousePosX() - ImGui.getMouseDragDeltaX(); + float dragStartY = ImGui.getMousePosY() - ImGui.getMouseDragDeltaY(); + if (dragStartX > lineMin.x && dragStartX <= lineMax.x && dragStartY > lineMin.y && dragStartY <= lineMax.y) + dragging = true; + } + if (draggedNode != null && draggedNode != this) + { + float height = lineMax.y - lineMin.y; + if (ImGui.getMousePosY() - lineMin.y <= 0.5f * height) + { + ImGui.getWindowDrawList().addRectFilled(indentMin.x, lineMin.y, lineMax.x, lineMin.y + 0.15f * height, ImGui.getColorU32(ImGuiCol.CheckMark)); + if (!ImGui.isMouseDown(ImGuiMouseButton.Left)) + dragReleasedBefore = true; + } + else + { + ImGui.getWindowDrawList().addRectFilled(indentMin.x, lineMin.y + 0.85f * height, lineMax.x, lineMax.y, ImGui.getColorU32(ImGuiCol.CheckMark)); + if (!ImGui.isMouseDown(ImGuiMouseButton.Left)) + dragReleasedAfter = true; + } + } + } + } + float itemWidth = ImGui.getFontSize() * 1.0f; if (!getChildren().isEmpty()) // expand/collapse arrow { @@ -147,19 +185,15 @@ public void renderRowBeginning() { float offsetX = ImGui.getCursorScreenPosX() + ImGui.getFontSize() * 0.2f; float offsetY = ImGui.getCursorScreenPosY() + ImGui.getFrameHeight() * 0.4f; - ImGui.getWindowDrawList().addLine(offsetX, offsetY, - offsetX + halfHeight, offsetY + width, color); - ImGui.getWindowDrawList().addLine(offsetX + halfHeight, offsetY + width, - offsetX + halfHeight * 2.0f, offsetY, color); + ImGui.getWindowDrawList().addLine(offsetX, offsetY, offsetX + halfHeight, offsetY + width, color); + ImGui.getWindowDrawList().addLine(offsetX + halfHeight, offsetY + width, offsetX + halfHeight * 2.0f, offsetY, color); } else { float offsetX = ImGui.getCursorScreenPosX() + width; float offsetY = ImGui.getCursorScreenPosY() + ImGui.getFrameHeight() * 0.2f; - ImGui.getWindowDrawList().addLine(offsetX + width, offsetY + halfHeight, - offsetX, offsetY + halfHeight * 2.0f, color); - ImGui.getWindowDrawList().addLine(offsetX + width, offsetY + halfHeight, - offsetX, offsetY, color); + ImGui.getWindowDrawList().addLine(offsetX + width, offsetY + halfHeight, offsetX, offsetY + halfHeight * 2.0f, color); + ImGui.getWindowDrawList().addLine(offsetX + width, offsetY + halfHeight, offsetX, offsetY, color); } if (isHovered && ImGui.isMouseClicked(ImGuiMouseButton.Left)) { @@ -345,6 +379,26 @@ public String getModalPopupID() return modalPopupID; } + public void setDraggedNode(RDXBehaviorTreeNode draggedNode) + { + this.draggedNode = draggedNode; + } + + public boolean getDragging() + { + return dragging; + } + + public boolean getDragReleasedBefore() + { + return dragReleasedBefore; + } + + public boolean getDragReleasedAfter() + { + return dragReleasedAfter; + } + public List> getChildren() { return children; diff --git a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeRootNode.java b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeRootNode.java index ceb1c150c0e6..8659ae6522c7 100644 --- a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeRootNode.java +++ b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeRootNode.java @@ -102,9 +102,6 @@ public void renderContextMenuItems() if (ImGui.menuItem(labels.get("Print LLM Encoding"))) LogTools.info("LLM Encoding:%n%s".formatted(BehaviorTreeLLMEncoding.encode(state))); - - if (ImGui.menuItem(labels.get("Render Progress Using Plots"), null, progressWidgetsManager.getRenderAsPlots())) - progressWidgetsManager.setRenderAsPlots(!progressWidgetsManager.getRenderAsPlots()); } public void renderExecutionControlAndProgressWidgets() @@ -208,4 +205,9 @@ public void renderNodeSettingsWidgets() { return idToNodeMap; } + + public RDXActionProgressWidgetsManager getProgressWidgetsManager() + { + return progressWidgetsManager; + } } \ No newline at end of file diff --git a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeSettings.java b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeSettings.java new file mode 100644 index 000000000000..7ec82189fa08 --- /dev/null +++ b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeSettings.java @@ -0,0 +1,117 @@ +package us.ihmc.rdx.ui.behavior.tree; + +import us.ihmc.log.LogTools; +import us.ihmc.rdx.ui.behavior.sequence.RDXActionProgressWidgetsManager.Type; +import us.ihmc.tools.IHMCCommonPaths; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; + +public class RDXBehaviorTreeSettings +{ + private static final Path PATH = IHMCCommonPaths.DOT_IHMC_DIRECTORY.resolve("RDXBehaviorTreeSettings.ini"); + private static int deleteRetries = 0; + + /** 60% seems to be the desirable ratio for the visible area of the tree view vs the settings area */ + private float treeExplorerHeightPercentage = 0.6f; + private Type progressWidgetsType = Type.PROGRESS_BARS; + + public float getTreeExplorerHeightPercentage() + { + return treeExplorerHeightPercentage; + } + + public void setTreeExplorerHeightPercentage(float treeExplorerHeightPercentage) + { + this.treeExplorerHeightPercentage = treeExplorerHeightPercentage; + saveAsync(); + } + + public Type getProgressWidgetsType() + { + return progressWidgetsType; + } + + public void setProgressWidgetsType(Type progressWidgetsType) + { + this.progressWidgetsType = progressWidgetsType; + saveAsync(); + } + + public RDXBehaviorTreeSettings() + { + try + { + File file = PATH.toFile(); + + if (!file.exists()) + { + save(); + } + + Properties properties = new Properties(); + + try (FileInputStream input = new FileInputStream(file)) + { + properties.load(input); + } + + try + { + treeExplorerHeightPercentage = Float.parseFloat(properties.getProperty("treeExplorerHeightPercentage")); + progressWidgetsType = Type.valueOf(properties.getProperty("progressWidgetsType")); + } + catch (Exception e) + { + // Delete and re-create the file if there was a parsing error + if (deleteRetries++ > 20) + return; // Stop gap + file.delete(); + save(); + } + } + catch (IOException e) + { + LogTools.error("Unable to load RDXBehaviorTreeSettings.ini", e); + } + } + + public void save() throws IOException + { + File file = PATH.toFile(); + + file.getParentFile().mkdirs(); + + if (!file.exists()) + file.createNewFile(); + + Properties properties = new Properties(); + properties.setProperty("treeExplorerHeightPercentage", String.valueOf(treeExplorerHeightPercentage)); + properties.setProperty("progressWidgetsType", progressWidgetsType.name()); + + try (FileOutputStream output = new FileOutputStream(file)) + { + properties.store(output, null); + } + } + + private void saveAsync() + { + CompletableFuture.runAsync(() -> + { + try + { + save(); + } + catch (IOException e) + { + LogTools.info(e); + } + }); + } +} diff --git a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeWidgetsVerticalLayout.java b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeWidgetsVerticalLayout.java index c0ce077ea8e6..5dc9f65a4c7e 100644 --- a/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeWidgetsVerticalLayout.java +++ b/ihmc-high-level-behaviors/src/libgdx/java/us/ihmc/rdx/ui/behavior/tree/RDXBehaviorTreeWidgetsVerticalLayout.java @@ -2,6 +2,7 @@ import imgui.ImGui; import imgui.flag.ImGuiCol; +import imgui.flag.ImGuiMouseButton; import imgui.flag.ImGuiStyleVar; import imgui.flag.ImGuiWindowFlags; import us.ihmc.behaviors.behaviorTree.topology.BehaviorTreeTopologyOperationQueue; @@ -10,6 +11,7 @@ import us.ihmc.log.LogTools; import us.ihmc.rdx.imgui.ImGuiTools; import us.ihmc.rdx.imgui.ImGuiUniqueLabelMap; +import us.ihmc.rdx.ui.RDXBaseUI; import us.ihmc.rdx.ui.behavior.sequence.RDXActionNode; public class RDXBehaviorTreeWidgetsVerticalLayout @@ -20,6 +22,7 @@ public class RDXBehaviorTreeWidgetsVerticalLayout private BehaviorTreeNodeInsertionType insertionType = null; private RDXBehaviorTreeNode modalPopupNode; private final TypedNotification queuePopupModal = new TypedNotification<>(); + private RDXBehaviorTreeNode draggedNode = null; public RDXBehaviorTreeWidgetsVerticalLayout(RDXBehaviorTree behaviorTree) { @@ -28,12 +31,36 @@ public RDXBehaviorTreeWidgetsVerticalLayout(RDXBehaviorTree behaviorTree) topologyOperationQueue = behaviorTree.getTopologyChangeQueue(); } - public void renderImGuiWidgets(RDXBehaviorTreeNode node) + public void renderImGuiWidgets() + { + renderImGuiWidgets(behaviorTree.getRootNode()); + + if (!ImGui.isMouseDown(ImGuiMouseButton.Left)) + draggedNode = null; + } + + private void renderImGuiWidgets(RDXBehaviorTreeNode node) { ImGui.pushStyleVar(ImGuiStyleVar.ItemSpacing, ImGui.getStyle().getItemSpacingX(), 0.0f); + node.setDraggedNode(draggedNode); node.renderTreeViewRow(); + if (draggedNode == null && node.getDragging()) + draggedNode = node; + if (node.getDragReleasedBefore()) + { + RDXBaseUI.pushNotification("Moved %s to before %s".formatted(draggedNode.getDefinition().getName(), node.getDefinition().getName())); + topologyOperationQueue.queueMoveChildModify(draggedNode.getParent(), node.getParent(), draggedNode, node, BehaviorTreeNodeInsertionType.INSERT_BEFORE); + draggedNode = null; + } + else if (node.getDragReleasedAfter()) + { + RDXBaseUI.pushNotification("Moved %s to after %s".formatted(draggedNode.getDefinition().getName(), node.getDefinition().getName())); + topologyOperationQueue.queueMoveChildModify(draggedNode.getParent(), node.getParent(), draggedNode, node, BehaviorTreeNodeInsertionType.INSERT_AFTER); + draggedNode = null; + } + ImGui.popStyleVar(); if (ImGui.beginPopup(node.getNodePopupID()))