|
|
@@ -42,6 +42,8 @@ except Exception as exc: # pragma: no cover - depends on local Qt install
|
|
|
|
|
|
|
|
|
if QT_IMPORT_ERROR is None:
|
|
|
+ MAX_SUBPLOTS = 4
|
|
|
+ TREE_COLUMN_COUNT = MAX_SUBPLOTS + 1
|
|
|
ITEM_KIND_ROLE = Qt.ItemDataRole.UserRole + 20
|
|
|
ITEM_VALUE_ROLE = Qt.ItemDataRole.UserRole + 21
|
|
|
ITEM_PARENT_ROLE = Qt.ItemDataRole.UserRole + 22
|
|
|
@@ -138,7 +140,7 @@ if QT_IMPORT_ERROR is None:
|
|
|
self.parsed_log: ParsedLog | None = None
|
|
|
self.selected_log: Path | None = None
|
|
|
self.subplot_count = 2
|
|
|
- self.subplot_traces: list[list[PlotTraceSpec]] = [[], []]
|
|
|
+ self.subplot_traces: list[list[PlotTraceSpec]] = self._empty_subplot_traces()
|
|
|
self._updating_tree = False
|
|
|
self._parse_thread: ParseThread | None = None
|
|
|
self.preview_model = DataFrameTableModel()
|
|
|
@@ -172,12 +174,11 @@ if QT_IMPORT_ERROR is None:
|
|
|
toolbar.addSeparator()
|
|
|
|
|
|
layout_menu = QMenu(self)
|
|
|
- one_subplot_action = QAction("1 Subplot", self)
|
|
|
- one_subplot_action.triggered.connect(lambda: self.set_subplot_count(1))
|
|
|
- two_subplot_action = QAction("2 Subplots", self)
|
|
|
- two_subplot_action.triggered.connect(lambda: self.set_subplot_count(2))
|
|
|
- layout_menu.addAction(one_subplot_action)
|
|
|
- layout_menu.addAction(two_subplot_action)
|
|
|
+ for count in range(1, MAX_SUBPLOTS + 1):
|
|
|
+ label = f"{count} Subplot" if count == 1 else f"{count} Subplots"
|
|
|
+ action = QAction(label, self)
|
|
|
+ action.triggered.connect(lambda _checked=False, value=count: self.set_subplot_count(value))
|
|
|
+ layout_menu.addAction(action)
|
|
|
|
|
|
layout_button = QToolButton()
|
|
|
layout_button.setText("Layout")
|
|
|
@@ -188,14 +189,14 @@ if QT_IMPORT_ERROR is None:
|
|
|
toolbar.addSeparator()
|
|
|
|
|
|
clear_menu = QMenu(self)
|
|
|
- clear_s1_action = QAction("Clear S1", self)
|
|
|
- clear_s1_action.triggered.connect(lambda: self.clear_target_subplot(0))
|
|
|
- clear_s2_action = QAction("Clear S2", self)
|
|
|
- clear_s2_action.triggered.connect(lambda: self.clear_target_subplot(1))
|
|
|
+ for subplot_index in range(MAX_SUBPLOTS):
|
|
|
+ action = QAction(f"Clear S{subplot_index + 1}", self)
|
|
|
+ action.triggered.connect(
|
|
|
+ lambda _checked=False, value=subplot_index: self.clear_target_subplot(value)
|
|
|
+ )
|
|
|
+ clear_menu.addAction(action)
|
|
|
clear_all_action = QAction("Clear All", self)
|
|
|
clear_all_action.triggered.connect(self.clear_all_traces)
|
|
|
- clear_menu.addAction(clear_s1_action)
|
|
|
- clear_menu.addAction(clear_s2_action)
|
|
|
clear_menu.addSeparator()
|
|
|
clear_menu.addAction(clear_all_action)
|
|
|
|
|
|
@@ -219,12 +220,12 @@ if QT_IMPORT_ERROR is None:
|
|
|
root_layout.addWidget(splitter, 1)
|
|
|
|
|
|
self.signal_tree = QTreeWidget()
|
|
|
- self.signal_tree.setColumnCount(3)
|
|
|
- self.signal_tree.setHeaderLabels(["Items", "S1", "S2"])
|
|
|
+ self.signal_tree.setColumnCount(TREE_COLUMN_COUNT)
|
|
|
+ self.signal_tree.setHeaderLabels(["Items", "S1", "S2", "S3", "S4"])
|
|
|
self.signal_tree.setAlternatingRowColors(True)
|
|
|
self.signal_tree.setColumnWidth(0, 380)
|
|
|
- self.signal_tree.setColumnWidth(1, 48)
|
|
|
- self.signal_tree.setColumnWidth(2, 48)
|
|
|
+ for column in range(1, TREE_COLUMN_COUNT):
|
|
|
+ self.signal_tree.setColumnWidth(column, 48)
|
|
|
self.signal_tree.itemSelectionChanged.connect(self.on_tree_selection_changed)
|
|
|
self.signal_tree.itemChanged.connect(self.on_tree_item_changed)
|
|
|
splitter.addWidget(self.signal_tree)
|
|
|
@@ -295,7 +296,7 @@ if QT_IMPORT_ERROR is None:
|
|
|
|
|
|
def _on_parse_succeeded(self, parsed_log: ParsedLog) -> None:
|
|
|
self.parsed_log = parsed_log
|
|
|
- self.subplot_traces = [[], []]
|
|
|
+ self.subplot_traces = self._empty_subplot_traces()
|
|
|
self._populate_tree()
|
|
|
self.summary_label.setText(
|
|
|
f"Parsed {len(self.parsed_log.header.buses)} bus definitions, "
|
|
|
@@ -344,24 +345,25 @@ if QT_IMPORT_ERROR is None:
|
|
|
self.plot_widget.set_plot_data(self.parsed_log, self.subplot_traces, self.subplot_count)
|
|
|
|
|
|
def set_subplot_count(self, count: int) -> None:
|
|
|
- self.subplot_count = 1 if count <= 1 else 2
|
|
|
+ self.subplot_count = max(1, min(count, MAX_SUBPLOTS))
|
|
|
+ self._refresh_tree_state()
|
|
|
self.refresh_plot()
|
|
|
|
|
|
def clear_target_subplot(self, subplot_index: int) -> None:
|
|
|
- if subplot_index not in (0, 1):
|
|
|
+ if not 0 <= subplot_index < MAX_SUBPLOTS:
|
|
|
return
|
|
|
self.subplot_traces[subplot_index] = []
|
|
|
self._refresh_tree_state()
|
|
|
self.refresh_plot()
|
|
|
|
|
|
def clear_all_traces(self) -> None:
|
|
|
- self.subplot_traces = [[], []]
|
|
|
+ self.subplot_traces = self._empty_subplot_traces()
|
|
|
self._refresh_tree_state()
|
|
|
self.refresh_plot()
|
|
|
|
|
|
def _clear_loaded_log(self) -> None:
|
|
|
self.parsed_log = None
|
|
|
- self.subplot_traces = [[], []]
|
|
|
+ self.subplot_traces = self._empty_subplot_traces()
|
|
|
self.signal_tree.clear()
|
|
|
self.preview.setPlainText("")
|
|
|
self._clear_preview_table()
|
|
|
@@ -415,7 +417,7 @@ if QT_IMPORT_ERROR is None:
|
|
|
def on_tree_item_changed(self, item: QTreeWidgetItem, column: int) -> None:
|
|
|
if self._updating_tree or self.parsed_log is None:
|
|
|
return
|
|
|
- if column not in (1, 2):
|
|
|
+ if not 1 <= column < TREE_COLUMN_COUNT:
|
|
|
return
|
|
|
if item.childCount() > 0:
|
|
|
return
|
|
|
@@ -429,11 +431,15 @@ if QT_IMPORT_ERROR is None:
|
|
|
return
|
|
|
|
|
|
subplot_index = column - 1
|
|
|
- if subplot_index == 1 and self.subplot_count == 1 and item.checkState(column) == Qt.CheckState.Checked:
|
|
|
+ if subplot_index >= self.subplot_count and item.checkState(column) == Qt.CheckState.Checked:
|
|
|
self._updating_tree = True
|
|
|
item.setCheckState(column, Qt.CheckState.Unchecked)
|
|
|
self._updating_tree = False
|
|
|
- QMessageBox.information(self, "Single subplot mode", "Switch to 2 Subplots before using S2.")
|
|
|
+ QMessageBox.information(
|
|
|
+ self,
|
|
|
+ "Inactive subplot",
|
|
|
+ f"Switch to at least {subplot_index + 1} Subplots before using S{subplot_index + 1}.",
|
|
|
+ )
|
|
|
return
|
|
|
|
|
|
enabled = item.checkState(column) == Qt.CheckState.Checked
|
|
|
@@ -447,31 +453,31 @@ if QT_IMPORT_ERROR is None:
|
|
|
self.signal_tree.clear()
|
|
|
|
|
|
parameter_root = QTreeWidgetItem(
|
|
|
- [f"Parameters ({len(self.parsed_log.header.parameter_groups)} groups)", "", ""]
|
|
|
+ self._tree_row(f"Parameters ({len(self.parsed_log.header.parameter_groups)} groups)")
|
|
|
)
|
|
|
parameter_root.setData(0, ITEM_KIND_ROLE, "param_root")
|
|
|
parameter_root.setData(0, ITEM_VALUE_ROLE, "__parameters__")
|
|
|
self.signal_tree.addTopLevelItem(parameter_root)
|
|
|
|
|
|
for group in self.parsed_log.header.parameter_groups:
|
|
|
- group_item = QTreeWidgetItem([self._parameter_group_display_text(group.name), "", ""])
|
|
|
+ group_item = QTreeWidgetItem(self._tree_row(self._parameter_group_display_text(group.name)))
|
|
|
group_item.setData(0, ITEM_KIND_ROLE, "param_group")
|
|
|
group_item.setData(0, ITEM_VALUE_ROLE, group.name)
|
|
|
parameter_root.addChild(group_item)
|
|
|
|
|
|
- bus_root = QTreeWidgetItem([f"Buses ({len(self.parsed_log.buses)} with data)", "", ""])
|
|
|
+ bus_root = QTreeWidgetItem(self._tree_row(f"Buses ({len(self.parsed_log.buses)} with data)"))
|
|
|
bus_root.setData(0, ITEM_KIND_ROLE, "bus_root")
|
|
|
bus_root.setData(0, ITEM_VALUE_ROLE, "__buses__")
|
|
|
self.signal_tree.addTopLevelItem(bus_root)
|
|
|
|
|
|
for bus_name in self.parsed_log.buses:
|
|
|
- bus_item = QTreeWidgetItem([self._bus_display_text(bus_name), "", ""])
|
|
|
+ bus_item = QTreeWidgetItem(self._tree_row(self._bus_display_text(bus_name)))
|
|
|
bus_item.setData(0, ITEM_KIND_ROLE, "bus")
|
|
|
bus_item.setData(0, ITEM_VALUE_ROLE, bus_name)
|
|
|
bus_root.addChild(bus_item)
|
|
|
|
|
|
for field_name in self._plottable_columns(bus_name):
|
|
|
- field_item = QTreeWidgetItem([field_name, "", ""])
|
|
|
+ field_item = QTreeWidgetItem(self._tree_row(field_name))
|
|
|
field_item.setFlags(
|
|
|
field_item.flags()
|
|
|
| Qt.ItemFlag.ItemIsUserCheckable
|
|
|
@@ -481,8 +487,8 @@ if QT_IMPORT_ERROR is None:
|
|
|
field_item.setData(0, ITEM_KIND_ROLE, "field")
|
|
|
field_item.setData(0, ITEM_VALUE_ROLE, bus_name)
|
|
|
field_item.setData(0, ITEM_PARENT_ROLE, field_name)
|
|
|
- field_item.setCheckState(1, Qt.CheckState.Unchecked)
|
|
|
- field_item.setCheckState(2, Qt.CheckState.Unchecked)
|
|
|
+ for column in range(1, TREE_COLUMN_COUNT):
|
|
|
+ field_item.setCheckState(column, Qt.CheckState.Unchecked)
|
|
|
bus_item.addChild(field_item)
|
|
|
|
|
|
parameter_root.setExpanded(True)
|
|
|
@@ -512,20 +518,24 @@ if QT_IMPORT_ERROR is None:
|
|
|
for child_index in range(item.childCount()):
|
|
|
field_item = item.child(child_index)
|
|
|
field_name = str(field_item.data(0, ITEM_PARENT_ROLE))
|
|
|
- in_s1 = self._trace_exists(bus_name, field_name, 0)
|
|
|
- in_s2 = self._trace_exists(bus_name, field_name, 1)
|
|
|
- field_item.setCheckState(1, Qt.CheckState.Checked if in_s1 else Qt.CheckState.Unchecked)
|
|
|
- field_item.setCheckState(2, Qt.CheckState.Checked if in_s2 else Qt.CheckState.Unchecked)
|
|
|
+ active_subplots: list[int] = []
|
|
|
+ for subplot_index in range(MAX_SUBPLOTS):
|
|
|
+ enabled = self._trace_exists(bus_name, field_name, subplot_index)
|
|
|
+ if enabled:
|
|
|
+ active_subplots.append(subplot_index)
|
|
|
+ field_item.setCheckState(
|
|
|
+ subplot_index + 1,
|
|
|
+ Qt.CheckState.Checked if enabled else Qt.CheckState.Unchecked,
|
|
|
+ )
|
|
|
|
|
|
color = None
|
|
|
- if in_s1 and in_s2:
|
|
|
- color = QColor("#f6ebff")
|
|
|
- elif in_s1:
|
|
|
- color = QColor("#e8f2ff")
|
|
|
- elif in_s2:
|
|
|
- color = QColor("#eef8e8")
|
|
|
-
|
|
|
- for column in range(3):
|
|
|
+ if len(active_subplots) > 1:
|
|
|
+ color = QColor("#f3ecff")
|
|
|
+ elif len(active_subplots) == 1:
|
|
|
+ subplot_colors = ["#e8f2ff", "#eef8e8", "#fff2db", "#fde8ef"]
|
|
|
+ color = QColor(subplot_colors[active_subplots[0]])
|
|
|
+
|
|
|
+ for column in range(TREE_COLUMN_COUNT):
|
|
|
field_item.setBackground(column, QBrush(color) if color else QBrush())
|
|
|
return
|
|
|
|
|
|
@@ -558,12 +568,16 @@ if QT_IMPORT_ERROR is None:
|
|
|
return [column for column in bus.frame.columns if column not in excluded]
|
|
|
|
|
|
def _trace_exists(self, bus_name: str, field_name: str, subplot_index: int) -> bool:
|
|
|
+ if not 0 <= subplot_index < len(self.subplot_traces):
|
|
|
+ return False
|
|
|
return any(
|
|
|
trace.bus_name == bus_name and trace.field_name == field_name
|
|
|
for trace in self.subplot_traces[subplot_index]
|
|
|
)
|
|
|
|
|
|
def _set_trace_enabled(self, bus_name: str, field_name: str, subplot_index: int, enabled: bool) -> None:
|
|
|
+ if not 0 <= subplot_index < len(self.subplot_traces):
|
|
|
+ return
|
|
|
traces = self.subplot_traces[subplot_index]
|
|
|
exists = self._trace_exists(bus_name, field_name, subplot_index)
|
|
|
|
|
|
@@ -588,9 +602,17 @@ if QT_IMPORT_ERROR is None:
|
|
|
|
|
|
def _bus_display_text(self, bus_name: str) -> str:
|
|
|
field_count = len(self._plottable_columns(bus_name)) if self.parsed_log is not None else 0
|
|
|
- s1_count = sum(1 for trace in self.subplot_traces[0] if trace.bus_name == bus_name)
|
|
|
- s2_count = sum(1 for trace in self.subplot_traces[1] if trace.bus_name == bus_name)
|
|
|
- return f"{bus_name} ({field_count} fields | S1:{s1_count} S2:{s2_count})"
|
|
|
+ subplot_counts = " ".join(
|
|
|
+ f"S{subplot_index + 1}:{sum(1 for trace in traces if trace.bus_name == bus_name)}"
|
|
|
+ for subplot_index, traces in enumerate(self.subplot_traces)
|
|
|
+ )
|
|
|
+ return f"{bus_name} ({field_count} fields | {subplot_counts})"
|
|
|
+
|
|
|
+ def _empty_subplot_traces(self) -> list[list[PlotTraceSpec]]:
|
|
|
+ return [[] for _ in range(MAX_SUBPLOTS)]
|
|
|
+
|
|
|
+ def _tree_row(self, first_column: str) -> list[str]:
|
|
|
+ return [first_column, *[""] * MAX_SUBPLOTS]
|
|
|
|
|
|
def _parameter_group_display_text(self, group_name: str) -> str:
|
|
|
assert self.parsed_log is not None
|