diff --git a/robot_log_visualizer/__init__.py b/robot_log_visualizer/__init__.py
index 88cb257..5567e4a 100644
--- a/robot_log_visualizer/__init__.py
+++ b/robot_log_visualizer/__init__.py
@@ -1,4 +1,3 @@
import os
-# Prefer the PySide6 backend when QtPy resolves the Qt binding.
-os.environ.setdefault("QT_API", "pyside2")
+os.environ.setdefault("QT_API", "pyqt5")
diff --git a/robot_log_visualizer/signal_provider/realtime_signal_provider.py b/robot_log_visualizer/signal_provider/realtime_signal_provider.py
index d41c6c0..a1bdd0c 100644
--- a/robot_log_visualizer/signal_provider/realtime_signal_provider.py
+++ b/robot_log_visualizer/signal_provider/realtime_signal_provider.py
@@ -10,11 +10,10 @@
import numpy as np
from robot_log_visualizer.signal_provider.signal_provider import (
- ProviderType,
- SignalProvider,
-)
+ ProviderType, SignalProvider)
from robot_log_visualizer.utils.utils import PeriodicThreadState
+ROBOT_REALTIME_KEY = "robot_realtime"
def are_deps_installed():
try:
@@ -115,16 +114,16 @@ def __init__(self, period: float, signal_root_name: str):
# Track signals to buffer
self.buffered_signals = set()
# Always include joints_state
- self.buffered_signals.add("robot_realtime::joints_state::positions")
-
- # TODO: implement a logic to remove signals that are not needed anymore
+ self.buffered_signals.add(
+ f"{ROBOT_REALTIME_KEY}::joints_state::positions"
+ ) # TODO: implement a logic to remove signals that are not needed anymore
def add_signals_to_buffer(self, signals: Union[str, Iterable[str]]):
"""Add signals to the buffer set."""
if isinstance(signals, str):
signals = {signals}
self.buffered_signals.update(signals)
# Always include joints_state
- self.buffered_signals.add("robot_realtime::joints_state::positions")
+ self.buffered_signals.add(f"{ROBOT_REALTIME_KEY}::joints_state::positions")
def __len__(self):
"""
@@ -145,6 +144,9 @@ def _update_data_buffer(
Any sample older than the fixed time window is removed.
"""
+ if not keys:
+ return
+
if keys[0] not in raw_data:
raw_data[keys[0]] = DequeToNumpyLeaf()
@@ -171,26 +173,78 @@ def _update_data_buffer(
def _populate_realtime_logger_metadata(self, raw_data: dict, keys: list, value):
"""
Recursively populate metadata into raw_data.
- Here we simply store metadata (e.g. elements names) into a list.
+
+ - Creates only missing nested nodes.
+ - At a leaf: initialize buffers if missing and merge elements_names
+ (do not overwrite existing elements_names).
+ - Returns True if the call created or extended metadata for the given path,
+ False otherwise.
"""
+
+ if not isinstance(keys, list) or not keys:
+ raise ValueError(
+ f"Invalid keys parameter: {keys}. Expected a non-empty list."
+ )
+ if not all(isinstance(k, str) for k in keys):
+ raise ValueError(
+ f"Invalid keys elements: {keys}. All elements must be strings."
+ )
+
+ if not isinstance(raw_data, (dict, DequeToNumpyLeaf)):
+ raise ValueError(
+ f"Invalid raw_data parameter: {raw_data}. Expected a dictionary-like object."
+ )
+
+ if not isinstance(value, (list, tuple, str, int, float)):
+ raise ValueError(
+ f"Invalid value parameter: {value}. Expected a list, tuple, or scalar."
+ )
+
if keys[0] == "timestamps":
- return
+ return False
+
+ # ensure node exists
if keys[0] not in raw_data:
raw_data[keys[0]] = DequeToNumpyLeaf()
+ created = True
+ else:
+ created = False
+
if len(keys) == 1:
+ # leaf
if not value:
- if keys[0] in raw_data:
- del raw_data[keys[0]]
- return
- if "elements_names" not in raw_data[keys[0]]:
- raw_data[keys[0]]["elements_names"] = []
- # Also create empty buffers (which will later be updated in run())
- raw_data[keys[0]]["data"] = deque()
- raw_data[keys[0]]["timestamps"] = deque()
+ # do not delete existing node on empty value; just no-op
+ return created
+
+ node = raw_data[keys[0]]
+
+ # initialize leaf buffers if missing
+ if "elements_names" not in node:
+ node["elements_names"] = (
+ list(value) if isinstance(value, (list, tuple)) else value
+ )
+ node["data"] = deque()
+ node["timestamps"] = deque()
+ return True
- raw_data[keys[0]]["elements_names"] = value
+ # merge element names (append only new entries)
+ if isinstance(node["elements_names"], list) and isinstance(
+ value, (list, tuple)
+ ):
+ added = False
+ for v in value:
+ if v not in node["elements_names"]:
+ node["elements_names"].append(v)
+ added = True
+ return added or created
+
+ # fallback: if elements_names is not a list, don't overwrite
+ return created
else:
- self._populate_realtime_logger_metadata(raw_data[keys[0]], keys[1:], value)
+ # recurse into the subtree
+ return self._populate_realtime_logger_metadata(
+ raw_data[keys[0]], keys[1:], value
+ )
def open(self, source: str) -> bool:
"""
@@ -224,10 +278,12 @@ def open(self, source: str) -> bool:
return False
self.realtime_network_init = True
- self.joints_name = self.rt_metadata_dict["robot_realtime::description_list"]
- self.robot_name = self.rt_metadata_dict["robot_realtime::yarp_robot_name"][
- 0
+ self.joints_name = self.rt_metadata_dict[
+ f"{ROBOT_REALTIME_KEY}::description_list"
]
+ self.robot_name = self.rt_metadata_dict[
+ f"{ROBOT_REALTIME_KEY}::yarp_robot_name"
+ ][0]
# Populate metadata into self.data recursively.
for key_string, value in self.rt_metadata_dict.items():
@@ -254,6 +310,75 @@ def index(self):
finally:
self.index_lock.unlock()
+ def check_for_new_metadata(self) -> bool:
+ """
+ Check if new metadata is available using the client's streaming data flag.
+ This avoids expensive RPC calls.
+
+ Returns:
+ bool: True if new metadata is available, False otherwise.
+ """
+ client = self.vector_collections_client
+ if client is None:
+ return False
+
+ try:
+ return client.is_new_metadata_available()
+ except Exception as exc:
+ print(f"Error checking for new metadata: {exc}")
+ return False
+
+ def update_metadata(self):
+ """
+ Refresh the metadata from the remote realtime logger.
+ New metadata items are added to self.rt_metadata_dict and
+ the corresponding data buffers are created in self.data.
+
+ Returns:
+ dict: New metadata items added, or None if no new items.
+ """
+
+ client = self.vector_collections_client
+ if client is None:
+ print("Refresh metadata: realtime client unavailable.")
+ return
+
+ try:
+ updated_md = client.get_metadata().vectors
+ except Exception as exc:
+ print(f"Error fetching metadata: {exc}")
+ return
+
+ existing_md = self.rt_metadata_dict or {}
+ new_items = {k: v for k, v in updated_md.items() if k not in existing_md}
+
+ if not new_items:
+ return
+
+ existing_md.update(new_items)
+ self.rt_metadata_dict = existing_md
+
+ desc_key = f"{ROBOT_REALTIME_KEY}::description_list"
+ yarp_name_key = f"{ROBOT_REALTIME_KEY}::yarp_robot_name"
+ if desc_key in new_items:
+ self.joints_name = existing_md[desc_key]
+ if yarp_name_key in new_items:
+ names = existing_md.get(yarp_name_key, [])
+ if names:
+ self.robot_name = names[0]
+
+ # Populate metadata into self.data recursively.
+ for key_string, value in self.rt_metadata_dict.items():
+ keys = key_string.split("::")
+ self._populate_realtime_logger_metadata(self.data, keys, value)
+
+ # Remove keys that are not needed for the realtime plotting.
+ if self.root_name in self.data:
+ self.data[self.root_name].pop("description_list", None)
+ self.data[self.root_name].pop("yarp_robot_name", None)
+
+ return new_items
+
def get_item_from_path_at_index(self, path, index, default_path=None, neighbor=0):
"""
Get the latest data item from the given path at the latest index.
@@ -288,49 +413,68 @@ def run(self):
"""
while True:
start = time.time()
+ if self.state == PeriodicThreadState.closed:
+ return
+
if self.state == PeriodicThreadState.running:
- # Read the latest data from the realtime logger.
- vc_input = self.vector_collections_client.read_data(True).vectors
+ new_samples_read = False
+
+ while True:
+ try:
+ packet = self.vector_collections_client.read_data(False)
+ except Exception as exc: # noqa: BLE001 - surface runtime issues
+ print(f"Error reading realtime data: {exc}")
+ break
+
+ if packet is None:
+ break
+
+ vc_input = getattr(packet, "vectors", None)
+ if not vc_input:
+ break
+
+ timestamps_key = f"{ROBOT_REALTIME_KEY}::timestamps"
+ if timestamps_key not in vc_input or not vc_input[timestamps_key]:
+ continue
+
+ recent_timestamp = vc_input[timestamps_key][0]
- if vc_input:
self.index_lock.lock()
- # Retrieve the most recent timestamp from the input.
- recent_timestamp = vc_input["robot_realtime::timestamps"][0]
- self._timestamps.append(recent_timestamp)
- # Keep the global timestamps within the fixed plot window.
- while self._timestamps and (
- recent_timestamp - self._timestamps[0]
- > self.realtime_fixed_plot_window
- ):
- self._timestamps.popleft()
-
- # Update initial and end times.
- if self._timestamps:
- self.initial_time = self._timestamps[0]
- self.end_time = self._timestamps[-1]
-
- # For signal selected from the user that is in the received data (except timestamps),
- # update the appropriate buffer.
- for key_string, value in vc_input.items():
- if key_string == "robot_realtime::timestamps":
- continue
-
- # Check if any selected signal starts with this path
- match = any(
- sel.startswith(key_string) for sel in self.buffered_signals
- )
- if not match:
- continue
-
- keys = key_string.split("::")
- self._update_data_buffer(
- self.data, keys, value, recent_timestamp
- )
-
- self.index_lock.unlock()
-
- # Signal that new data are available.
- self.update_index_signal.emit()
+ try:
+ self._timestamps.append(recent_timestamp)
+
+ while self._timestamps and (
+ recent_timestamp - self._timestamps[0]
+ > self.realtime_fixed_plot_window
+ ):
+ self._timestamps.popleft()
+
+ if self._timestamps:
+ self.initial_time = self._timestamps[0]
+ self.end_time = self._timestamps[-1]
+
+ for key_string, value in vc_input.items():
+ if key_string == timestamps_key:
+ continue
+
+ match = any(
+ sel.startswith(key_string)
+ for sel in self.buffered_signals
+ )
+ if not match:
+ continue
+
+ keys = key_string.split("::")
+ self._update_data_buffer(
+ self.data, keys, value, recent_timestamp
+ )
+ finally:
+ self.index_lock.unlock()
+
+ new_samples_read = True
+
+ if new_samples_read:
+ self.update_index_signal.emit()
# Sleep until the next period.
sleep_time = self.period - (time.time() - start)
diff --git a/robot_log_visualizer/ui/gui.py b/robot_log_visualizer/ui/gui.py
index 02ec82a..46a646f 100644
--- a/robot_log_visualizer/ui/gui.py
+++ b/robot_log_visualizer/ui/gui.py
@@ -2,60 +2,41 @@
# This software may be modified and distributed under the terms of the
# Released under the terms of the BSD 3-Clause License
-# QtPy abstraction
-from qtpy import QtWidgets, QtGui, QtCore
-from qtpy import QtWebEngineWidgets # noqa: F401 # Ensure WebEngine is initialised
-from qtpy.QtCore import QMutex, QMutexLocker, QUrl, Qt, Slot
-from qtpy.QtWidgets import (
- QDialog,
- QDialogButtonBox,
- QFileDialog,
- QLineEdit,
- QToolButton,
- QTreeWidgetItem,
- QVBoxLayout,
-)
-
-pyqtSlot = Slot
-from robot_log_visualizer.robot_visualizer.meshcat_provider import MeshcatProvider
-from robot_log_visualizer.signal_provider.realtime_signal_provider import (
- RealtimeSignalProvider,
- are_deps_installed,
-)
-from robot_log_visualizer.signal_provider.matfile_signal_provider import (
- MatfileSignalProvider,
-)
-
-from robot_log_visualizer.signal_provider.signal_provider import (
- ProviderType,
- SignalProvider,
-)
-from robot_log_visualizer.ui.plot_item import PlotItem
-from robot_log_visualizer.ui.video_item import VideoItem
-from robot_log_visualizer.ui.text_logging import TextLoggingItem
-
-from robot_log_visualizer.utils.utils import (
- PeriodicThreadState,
- RobotStatePath,
- ColorPalette,
-)
-
-import sys
import os
import pathlib
import re
+import sys
+# for logging
+from time import localtime, strftime
import numpy as np
-
+import pyqtconsole.highlighter as hl
+from pyqtconsole.console import PythonConsole
+# QtPy abstraction
+from qtpy import QtWebEngineWidgets # noqa: F401
+from qtpy import QtGui, QtWidgets
+from qtpy.QtCore import QMutex, QMutexLocker, Qt, QTimer, QUrl, Slot
+from qtpy.QtWidgets import (QDialog, QDialogButtonBox, QFileDialog, QLineEdit,
+ QToolButton, QTreeWidgetItem, QVBoxLayout)
+
+from robot_log_visualizer.robot_visualizer.meshcat_provider import \
+ MeshcatProvider
+from robot_log_visualizer.signal_provider.matfile_signal_provider import \
+ MatfileSignalProvider
+from robot_log_visualizer.signal_provider.realtime_signal_provider import (
+ ROBOT_REALTIME_KEY, RealtimeSignalProvider, are_deps_installed)
+from robot_log_visualizer.signal_provider.signal_provider import (
+ ProviderType, SignalProvider)
+from robot_log_visualizer.ui.plot_item import PlotItem
+from robot_log_visualizer.ui.text_logging import TextLoggingItem
# QtDesigner generated classes
from robot_log_visualizer.ui.ui_loader import load_ui
+from robot_log_visualizer.ui.video_item import VideoItem
+from robot_log_visualizer.utils.utils import (ColorPalette,
+ PeriodicThreadState,
+ RobotStatePath)
-# for logging
-from time import localtime, strftime
-
-# Matplotlib class
-from pyqtconsole.console import PythonConsole
-import pyqtconsole.highlighter as hl
+pyqtSlot = Slot
class SetRobotModelDialog(QtWidgets.QDialog):
@@ -258,6 +239,25 @@ def __init__(self, signal_provider_period, meshcat_provider, animation_period):
self.ui.pauseButton.clicked.connect(self.pauseButton_on_click)
self.ui.startButton.clicked.connect(self.startButton_on_click)
+ self.ui.refreshButton.clicked.connect(self.refreshButton_on_click)
+
+ # by default the refresh button is only relevant for realtime connections
+ try:
+ self.ui.refreshButton.setEnabled(False)
+ except Exception:
+ pass
+
+ # Setup for refresh button blinking/color change
+ self.refresh_button_blink_state = False
+ self.refresh_button_timer = QTimer(self)
+ self.refresh_button_timer.timeout.connect(self._toggle_refresh_button_style)
+ self.refresh_button_timer.setInterval(500) # Blink every 500ms
+
+ # Timer to periodically check for new metadata in realtime mode
+ self.metadata_check_timer = QTimer(self)
+ self.metadata_check_timer.timeout.connect(self._check_for_new_metadata)
+ self.metadata_check_timer.setInterval(2000) # Check every 2 seconds
+
self.ui.timeSlider.sliderReleased.connect(self.timeSlider_on_release)
self.ui.timeSlider.sliderPressed.connect(self.timeSlider_on_pressed)
self.ui.timeSlider.sliderMoved.connect(self.timeSlider_on_sliderMoved)
@@ -461,6 +461,7 @@ def variableTreeWidget_on_click(self):
path = []
legend = []
is_leaf = True
+ self.logger.write_to_log(f"Selected index data: {index.data()}")
while index.data() is not None:
legend.append(index.data())
if not is_leaf:
@@ -476,6 +477,10 @@ def variableTreeWidget_on_click(self):
paths.append(path)
legends.append(legend)
+ # Debug logs
+ self.logger.write_to_log(f"Selected paths: {paths}")
+ self.logger.write_to_log(f"Selected legends: {legends}")
+
# if there is no selection we do nothing
if not paths:
return
@@ -617,6 +622,11 @@ def closeEvent(self, event):
if self.signal_provider is not None:
self.signal_provider.state = PeriodicThreadState.closed
self.signal_provider.wait()
+ # hide/disable refresh on close
+ try:
+ self.ui.refreshButton.setEnabled(False)
+ except Exception:
+ pass
self.signal_provider = None
# Stop the meshcat_provider if exists
@@ -643,9 +653,18 @@ def closeEvent(self, event):
if self.realtime_connection_enabled:
self.realtime_connection_enabled = False
+ # Stop timers
+ if hasattr(self, "metadata_check_timer"):
+ self.metadata_check_timer.stop()
+ if hasattr(self, "refresh_button_timer"):
+ self.refresh_button_timer.stop()
+
event.accept()
- def __populate_variable_tree_widget(self, obj, parent) -> QTreeWidgetItem:
+ def __populate_variable_tree_widget(
+ self, obj: dict, parent: QTreeWidgetItem
+ ) -> QTreeWidgetItem:
+
if not isinstance(obj, dict):
return parent
if "data" in obj.keys() and "timestamps" in obj.keys():
@@ -673,6 +692,49 @@ def __populate_variable_tree_widget(self, obj, parent) -> QTreeWidgetItem:
parent.addChild(item)
return parent
+ def _update_variable_tree_widget(
+ self, obj: dict, parent: QTreeWidgetItem
+ ) -> QTreeWidgetItem:
+ """
+ Recursively merge new metadata into the variable tree widget.
+ Only checks for existing items at non-leaf nodes.
+ At leaf nodes, always adds all children.
+ """
+ if not isinstance(obj, dict):
+ return parent
+
+ if "data" in obj.keys() and "timestamps" in obj.keys():
+ temp_array = obj["data"]
+ try:
+ n_cols = temp_array.shape[1]
+ except Exception:
+ n_cols = 1
+
+ # Always add all children at leaf level
+ if "elements_names" in obj.keys():
+ for name in obj["elements_names"]:
+ item = QTreeWidgetItem([name])
+ parent.addChild(item)
+ else:
+ for i in range(n_cols):
+ item = QTreeWidgetItem(["Element " + str(i)])
+ parent.addChild(item)
+ return parent
+
+ for key, value in obj.items():
+ # Only check for existing child at non-leaf nodes
+ child_item = None
+ for i in range(parent.childCount()):
+ if parent.child(i).text(0) == key:
+ child_item = parent.child(i)
+ break
+ if child_item is None:
+ child_item = QTreeWidgetItem([key])
+ child_item.setFlags(child_item.flags() & ~Qt.ItemIsSelectable)
+ parent.addChild(child_item)
+ self._update_variable_tree_widget(value, child_item)
+ return parent
+
def __populate_text_logging_tree_widget(self, obj, parent) -> QTreeWidgetItem:
if not isinstance(obj, dict):
return parent
@@ -742,6 +804,11 @@ def __load_mat_file(self, file_name):
self.ui.timeSlider.setMaximum(self.signal_size)
self.ui.startButton.setEnabled(True)
self.ui.timeSlider.setEnabled(True)
+ # loading a MAT file: refresh is not relevant
+ try:
+ self.ui.refreshButton.setEnabled(False)
+ except Exception:
+ pass
# get all the video associated to the datase
filename_without_path = pathlib.Path(file_name).name
@@ -790,7 +857,7 @@ def open_mat_file(self):
def connect_realtime_logger(self):
self.signal_provider = RealtimeSignalProvider(
- self.signal_provider_period, "robot_realtime"
+ self.signal_provider_period, ROBOT_REALTIME_KEY
)
self.realtime_connection_enabled = True
@@ -799,6 +866,11 @@ def connect_realtime_logger(self):
self.logger.write_to_log("Could not connect to the real-time logger.")
self.realtime_connection_enabled = False
self.signal_provider = None
+ # failed to connect: ensure refresh is disabled
+ try:
+ self.ui.refreshButton.setEnabled(False)
+ except Exception:
+ pass
return
# only display one root in the gui
root = list(self.signal_provider.data.keys())[0]
@@ -834,6 +906,16 @@ def connect_realtime_logger(self):
for plot in self.plot_items:
plot.set_signal_provider(self.signal_provider)
+ # In realtime mode, the refresh button starts disabled until new metadata is available
+ try:
+ self.ui.refreshButton.setEnabled(False)
+ self.ui.refreshButton.setStyleSheet("") # Reset to normal style
+ self.refresh_button_blink_state = False
+ # Start checking for new metadata periodically
+ self.metadata_check_timer.start()
+ except Exception:
+ pass
+
def open_about(self):
self.about.show()
@@ -1081,7 +1163,7 @@ def variableTreeWidget_on_right_click(self, item_position):
and self.signal_provider.provider_type == ProviderType.REALTIME
):
# Convert item_path to signal name string
- signal_name = "robot_realtime::" + "::".join(item_path)
+ signal_name = f"{ROBOT_REALTIME_KEY}::" + "::".join(item_path)
self.signal_provider.add_signals_to_buffer([signal_name])
if use_as_base_orientation_str in action.text():
@@ -1096,7 +1178,7 @@ def variableTreeWidget_on_right_click(self, item_position):
self.signal_provider
and self.signal_provider.provider_type == ProviderType.REALTIME
):
- signal_name = "robot_realtime::" + "::".join(item_path)
+ signal_name = f"{ROBOT_REALTIME_KEY}::" + "::".join(item_path)
self.signal_provider.add_signals_to_buffer([signal_name])
if action.text() == dont_use_as_base_position_str:
@@ -1132,6 +1214,68 @@ def get_item_path(self, item):
path.reverse()
return path
+ def _toggle_refresh_button_style(self):
+ """Toggle the refresh button style to create a blinking effect."""
+ if self.refresh_button_blink_state:
+ # Normal state
+ self.ui.refreshButton.setStyleSheet("")
+ else:
+ # Highlighted state - use a bright color to draw attention
+ self.ui.refreshButton.setStyleSheet(
+ "QPushButton { background-color: #4CAF50; border: 2px solid #45a049; }"
+ )
+ self.refresh_button_blink_state = not self.refresh_button_blink_state
+
+ def _check_for_new_metadata(self):
+ """Periodically check if new metadata is available in realtime mode."""
+ if not isinstance(self.signal_provider, RealtimeSignalProvider):
+ return
+
+ provider = self.signal_provider
+ if provider.check_for_new_metadata():
+ # New metadata is available - enable and start blinking the button
+ self.ui.refreshButton.setEnabled(True)
+ if not self.refresh_button_timer.isActive():
+ self.refresh_button_timer.start()
+ self.logger.write_to_log("New metadata available. Click refresh to update.")
+
+ def refreshButton_on_click(self):
+ """Fetch fresh realtime metadata, add only new keys, and extend the tree."""
+
+ if not isinstance(self.signal_provider, RealtimeSignalProvider):
+ self.logger.write_to_log(
+ "Refresh metadata: realtime provider not connected."
+ )
+ return
+
+ provider = self.signal_provider
+ new_items = provider.update_metadata()
+ if not new_items:
+ self.logger.write_to_log("Refresh metadata: no new metadata keys found.")
+ return
+
+ self.logger.write_to_log(f"New metadata keys found: {list(new_items.keys())}")
+
+ # Treat the top-level item as the robot_realtime node
+ root_item = self.ui.variableTreeWidget.topLevelItem(0)
+ if root_item is None or root_item.text(0) != ROBOT_REALTIME_KEY:
+ self.logger.write_to_log(
+ f"Refresh metadata: '{ROBOT_REALTIME_KEY}' node not found, cannot insert."
+ )
+ return
+
+ # Merge the children of the new subtree into the existing robot_realtime node
+ with QMutexLocker(provider.index_lock):
+ self._update_variable_tree_widget(
+ provider.data[ROBOT_REALTIME_KEY], root_item
+ )
+
+ # After refresh, disable the button and stop blinking until new metadata arrives
+ self.ui.refreshButton.setEnabled(False)
+ self.refresh_button_timer.stop()
+ self.ui.refreshButton.setStyleSheet("") # Reset to normal style
+ self.refresh_button_blink_state = False
+
class Logger:
"""
diff --git a/robot_log_visualizer/ui/misc/visualizer.ui b/robot_log_visualizer/ui/misc/visualizer.ui
index 32e531f..f7bfd04 100644
--- a/robot_log_visualizer/ui/misc/visualizer.ui
+++ b/robot_log_visualizer/ui/misc/visualizer.ui
@@ -53,57 +53,6 @@
9
-
-
-
- false
-
-
-
- 40
- 16777215
-
-
-
-
-
-
-
- ../../../../../.designer/backup../../../../../.designer/backup
-
-
- false
-
-
- false
-
-
- false
-
-
-
- -
-
-
- false
-
-
-
- 0
- 0
-
-
-
- 1
-
-
- true
-
-
- Qt::Horizontal
-
-
-
- -
false
@@ -126,26 +75,7 @@
- -
-
-
-
- 0
- 0
-
-
-
-
- 60
- 0
-
-
-
- 0.0
-
-
-
- -
+
-
Qt::Horizontal
@@ -248,7 +178,7 @@
-
-
+
1
@@ -262,6 +192,101 @@
+ -
+
+
+ false
+
+
+
+ 0
+ 0
+
+
+
+ 1
+
+
+ true
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 60
+ 0
+
+
+
+ 0.0
+
+
+
+ -
+
+
+ false
+
+
+
+ 40
+ 16777215
+
+
+
+
+
+
+
+ ../../../../../.designer/backup../../../../../.designer/backup
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+
+ -
+
+
+ false
+
+
+
+ 40
+ 16777215
+
+
+
+
+
+
+
+
+
+ false
+
+
+ false
+
+
+
@@ -447,7 +472,7 @@
0
0
- 860
+ 858
190
@@ -495,7 +520,7 @@
0
0
929
- 22
+ 24