diff --git a/habitat-hitl/habitat_hitl/core/client_message_manager.py b/habitat-hitl/habitat_hitl/core/client_message_manager.py index 3daddd3c8c..2a84716587 100644 --- a/habitat-hitl/habitat_hitl/core/client_message_manager.py +++ b/habitat-hitl/habitat_hitl/core/client_message_manager.py @@ -4,6 +4,7 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. +from dataclasses import dataclass from typing import Final, List, Optional, Union import magnum as mn @@ -14,6 +15,19 @@ DEFAULT_NORMAL: Final[List[float]] = [0.0, 1.0, 0.0] +# TODO: Move to another file. +@dataclass +class UIButton: + """ + Networked UI button. Use RemoteClientState.ui_button_pressed() to retrieve state. + """ + + def __init__(self, button_id: str, text: str, enabled: bool): + self.button_id = button_id + self.text = text + self.enabled = enabled + + class ClientMessageManager: r""" Extends gfx-replay keyframes to include server messages to be interpreted by the clients. @@ -144,6 +158,34 @@ def add_text( {"text": text, "position": [pos[0], pos[1]]} ) + def show_modal_dialogue_box( + self, + title: str, + text: str, + buttons: List[UIButton], + destination_mask: Mask = Mask.ALL, + ): + r""" + Show a modal dialog box with buttons. + There can only be one modal dialog box at a time. + """ + for user_index in self._users.indices(destination_mask): + message = self._messages[user_index] + + message["dialog"] = { + "title": title, + "text": text, + "buttons": [], + } + for button in buttons: + message["dialog"]["buttons"].append( + { + "id": button.button_id, + "text": button.text, + "enabled": button.enabled, + } + ) + def change_humanoid_position( self, pos: List[float], destination_mask: Mask = Mask.ALL ) -> None: diff --git a/habitat-hitl/habitat_hitl/core/remote_client_state.py b/habitat-hitl/habitat_hitl/core/remote_client_state.py index d32cdc9ee0..97ace70907 100644 --- a/habitat-hitl/habitat_hitl/core/remote_client_state.py +++ b/habitat-hitl/habitat_hitl/core/remote_client_state.py @@ -5,7 +5,7 @@ # LICENSE file in the root directory of this source tree. import math -from typing import Any, List, Optional, Tuple +from typing import Any, List, Optional, Set, Tuple import magnum as mn @@ -49,6 +49,9 @@ def __init__( self._on_client_connected = Event() self._on_client_disconnected = Event() + # TODO: Handle UI in a different class. + self._pressed_ui_buttons: List[Set[str]] = [] + self._gui_inputs: List[GuiInput] = [] self._client_state_history: List[List[ClientState]] = [] self._receive_rate_trackers: List[AverageRateTracker] = [] @@ -56,6 +59,7 @@ def __init__( self._gui_inputs.append(GuiInput()) self._client_state_history.append([]) self._receive_rate_trackers.append(AverageRateTracker(2.0)) + self._pressed_ui_buttons.append(set()) # temp map VR button to key self._button_map = { @@ -89,6 +93,9 @@ def bind_gui_input(self, gui_input: GuiInput, user_index: int) -> None: assert user_index < len(self._gui_inputs) self._gui_inputs[user_index] = gui_input + def ui_button_pressed(self, user_index: int, button_id: str) -> bool: + return button_id in self._pressed_ui_buttons[user_index] + def get_history_length(self) -> int: """Length of client state history preserved. Anything beyond this horizon is discarded.""" return 4 @@ -211,13 +218,19 @@ def _update_input_state( if len(all_client_states) == 0 or len(self._gui_inputs) == 0: return - # Gather all recent keyDown and keyUp events + # Gather all input events. for user_index in range(len(all_client_states)): client_states = all_client_states[user_index] if len(client_states) == 0: continue gui_input = self._gui_inputs[user_index] for client_state in client_states: + # UI element events. + ui = client_state.get("ui", None) + if ui is not None: + for button in ui.get("buttonsPressed", []): + self._pressed_ui_buttons[user_index].add(button) + input_json = ( client_state["input"] if "input" in client_state else None ) @@ -447,11 +460,13 @@ def get_new_connection_records(self) -> List[ConnectionRecord]: def on_frame_end(self) -> None: for user_index in self._users.indices(Mask.ALL): self._gui_inputs[user_index].on_frame_end() + self._pressed_ui_buttons[user_index].clear() self._new_connection_records = None def clear_history(self, user_mask=Mask.ALL) -> None: for user_index in self._users.indices(user_mask): self._client_state_history[user_index].clear() + self._pressed_ui_buttons[user_index].clear() def kick(self, user_mask: Mask) -> None: """