Skip to content

Commit e68f487

Browse files
committed
semi-working tree view
1 parent 3ccc114 commit e68f487

File tree

4 files changed

+236
-596
lines changed

4 files changed

+236
-596
lines changed

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ classifiers = [
2626
]
2727
dependencies = [
2828
"pyside6~=6.8.2.1",
29-
"kevinbotlib==1.0.0a2"
3029
]
3130

3231
[project.urls]

src/kevinbotlib_dashboard/app.py

Lines changed: 129 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
from collections.abc import Callable
33
from typing import override
44

5-
from kevinbotlib.comm import KevinbotCommClient
6-
from PySide6.QtCore import QObject, QPointF, QRect, QRectF, QRegularExpression, QSettings, QSize, Qt, QTimer, Signal
5+
6+
from PySide6.QtCore import QObject, QPointF, QRect, QRectF, QRegularExpression, QSettings, QSize, Qt, QTimer, Signal, Slot, QModelIndex
77
from PySide6.QtGui import QAction, QBrush, QCloseEvent, QColor, QPainter, QPen, QRegularExpressionValidator
88
from PySide6.QtWidgets import (
99
QDialog,
@@ -22,9 +22,14 @@
2222
QStyleOptionGraphicsItem,
2323
QVBoxLayout,
2424
QWidget,
25+
QTreeView,
2526
)
2627

28+
from kevinbotlib.comm import KevinbotCommClient
29+
from kevinbotlib.logger import Logger
30+
2731
from kevinbotlib_dashboard.grid_theme import Themes
32+
from kevinbotlib_dashboard.tree import DictTreeModel
2833
from kevinbotlib_dashboard.widgets import Divider
2934

3035

@@ -389,18 +394,16 @@ def __init__(self, graphics_view, parent=None):
389394
super().__init__(parent)
390395
self.graphics_view = graphics_view
391396
self.controller = WidgetGridController(self.graphics_view)
392-
self.setup_ui()
393397

394-
def setup_ui(self):
395398
layout = QVBoxLayout(self)
396399
layout.setSpacing(10)
397400

398-
widgets = ["A", "B", "C", "D", "E"]
399-
for widget_name in widgets:
400-
button = QPushButton(widget_name)
401-
button.clicked.connect(lambda _, name=widget_name: self.add_widget(name))
402-
layout.addWidget(button)
403-
layout.addStretch()
401+
self.tree = QTreeView()
402+
self.tree.setHeaderHidden(True)
403+
layout.addWidget(self.tree)
404+
405+
self.model = DictTreeModel({})
406+
self.tree.setModel(self.model)
404407

405408
def add_widget(self, widget_name):
406409
self.controller.add(WidgetItem(widget_name, self.graphics_view))
@@ -465,6 +468,8 @@ def __init__(self):
465468

466469
self.settings = QSettings("kevinbotlib", "dashboard")
467470

471+
self.logger = Logger()
472+
468473
self.client = KevinbotCommClient(
469474
host=self.settings.value("ip", "10.0.0.2", str), # type: ignore
470475
port=self.settings.value("port", 8765, int), # type: ignore
@@ -502,6 +507,8 @@ def __init__(self):
502507
cols=self.settings.value("cols", 10, int), # type: ignore
503508
)
504509
palette = WidgetPalette(self.graphics_view)
510+
self.model = palette.model
511+
self.tree = palette.tree
505512

506513
layout.addWidget(self.graphics_view)
507514
layout.addWidget(palette)
@@ -511,6 +518,11 @@ def __init__(self):
511518
self.latency_timer.timeout.connect(self.update_latency)
512519
self.latency_timer.start()
513520

521+
self.update_timer = QTimer()
522+
self.update_timer.setInterval(100)
523+
self.update_timer.timeout.connect(self.update_tree)
524+
self.update_timer.start()
525+
514526
self.controller = WidgetGridController(self.graphics_view)
515527
self.controller.load(self.item_loader, self.settings.value("layout", [], type=list)) # type: ignore
516528

@@ -521,8 +533,115 @@ def update_latency(self):
521533
if self.client.websocket:
522534
self.latency_status.setText(f"Latency: {self.client.websocket.latency:.2f}ms")
523535

536+
@Slot()
537+
def update_tree(self, *args):
538+
# Get the latest data
539+
data_store = self.client.get_keys()
540+
data = {}
541+
542+
# here we process what data can be displayed
543+
for key, value in [(key, self.client.get_raw(key)) for key in data_store]:
544+
if "struct" in value and "dashboard" in value["struct"]:
545+
structured = {}
546+
for viewable in value["struct"]["dashboard"]:
547+
display = ""
548+
if "element" in viewable:
549+
raw = value[viewable["element"]]
550+
if "format" in viewable:
551+
fmt = viewable["format"]
552+
if fmt == "percent":
553+
display = f"{raw * 100:.2f}%"
554+
elif fmt == "degrees":
555+
display = f"{raw}°"
556+
elif fmt == "radians":
557+
display = f"{raw} rad"
558+
elif fmt.startswith("limit:"):
559+
limit = int(fmt.split(":")[1])
560+
display = raw[:limit]
561+
else:
562+
display = raw
563+
564+
structured[viewable["element"]] = display
565+
data[key] = structured
566+
else:
567+
self.logger.error(f"Could not display {key}, it dosen't contain a structure")
568+
569+
570+
def to_hierarchical_dict(flat_dict: dict):
571+
"""Convert a flat dictionary into a hierarchical one based on '/'."""
572+
hierarchical_dict = {}
573+
for key, value in flat_dict.items():
574+
parts = key.split('/')
575+
d = hierarchical_dict
576+
for part in parts[:-1]:
577+
d = d.setdefault(part, {})
578+
d[parts[-1]] = {"items": value, "key": key}
579+
return hierarchical_dict
580+
581+
# Convert flat dictionary to hierarchical
582+
583+
expanded_indexes = []
584+
def store_expansion(parent=QModelIndex()):
585+
for row in range(self.model.rowCount(parent)):
586+
index = self.model.index(row, 0, parent)
587+
if self.tree.isExpanded(index):
588+
expanded_indexes.append((
589+
self.get_index_path(index),
590+
True
591+
))
592+
store_expansion(index)
593+
store_expansion()
594+
595+
# Store selection
596+
selected_paths = self.get_selection_paths()
597+
598+
599+
# Update data...
600+
h = to_hierarchical_dict(data)
601+
self.model.update_data(h)
602+
603+
# Restore states
604+
def restore_expansion():
605+
for path, was_expanded in expanded_indexes:
606+
index = self.get_index_from_path(path)
607+
if index.isValid() and was_expanded:
608+
self.tree.setExpanded(index, True)
609+
restore_expansion()
610+
611+
# Restore selection
612+
self.restore_selection(selected_paths)
613+
614+
def get_selection_paths(self):
615+
paths = []
616+
for index in self.tree.selectionModel().selectedIndexes():
617+
if index.column() == 0: # Only store for first column
618+
paths.append(self.get_index_path(index))
619+
return paths
620+
621+
def restore_selection(self, paths):
622+
selection_model = self.tree.selectionModel()
623+
selection_model.clear()
624+
for path in paths:
625+
index = self.get_index_from_path(path)
626+
if index.isValid():
627+
selection_model.select(index, selection_model.SelectionFlag.Select | selection_model.SelectionFlag.Rows)
628+
629+
def get_index_path(self, index):
630+
path = []
631+
while index.isValid():
632+
path.append(index.row())
633+
index = self.model.parent(index)
634+
return tuple(reversed(path))
635+
636+
def get_index_from_path(self, path):
637+
index = QModelIndex()
638+
for row in path:
639+
index = self.model.index(row, 0, index)
640+
return index
641+
524642
def on_connect(self):
525643
self.connection_status.setText("Robot Connected")
644+
self.update_tree()
526645

527646
def on_disconnect(self):
528647
self.connection_status.setText("Robot Disconnected")

src/kevinbotlib_dashboard/tree.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from PySide6.QtCore import Qt, QAbstractItemModel, QModelIndex, QPersistentModelIndex
2+
from typing import Any, Dict, List
3+
4+
class TreeItem:
5+
def __init__(self, data: Any, key: str = "", parent: 'TreeItem | None' = None):
6+
self.data = data
7+
self.key = key
8+
self.parent_item = parent
9+
10+
self.child_items: List[TreeItem] = []
11+
if isinstance(data, dict):
12+
for k, v in data.items():
13+
self.child_items.append(TreeItem(v, k, self))
14+
self.userdata = None
15+
16+
if len(self.child_items) > 0:
17+
for child in self.child_items:
18+
if child.key == "key":
19+
self.userdata = child.data
20+
print(self.key, self.userdata)
21+
self.child_items.clear() # This is the sendable, dont show any more data
22+
23+
def child(self, row: int) -> 'TreeItem':
24+
if 0 <= row < len(self.child_items):
25+
return self.child_items[row]
26+
return None
27+
28+
def childCount(self) -> int:
29+
return len(self.child_items)
30+
31+
def row(self) -> int:
32+
if self.parent_item:
33+
return self.parent_item.child_items.index(self)
34+
return 0
35+
36+
def parent(self) -> 'TreeItem':
37+
return self.parent_item
38+
39+
class DictTreeModel(QAbstractItemModel):
40+
def __init__(self, data: Dict):
41+
super().__init__()
42+
self.root_item = TreeItem(data)
43+
44+
def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex:
45+
if not self.hasIndex(row, column, parent):
46+
return QModelIndex()
47+
48+
if not parent.isValid():
49+
parent_item = self.root_item
50+
else:
51+
parent_item = parent.internalPointer()
52+
53+
child_item = parent_item.child(row)
54+
if child_item:
55+
return self.createIndex(row, column, child_item)
56+
return QModelIndex()
57+
58+
def parent(self, index: QModelIndex) -> QModelIndex:
59+
if not index.isValid():
60+
return QModelIndex()
61+
62+
child_item = index.internalPointer()
63+
parent_item = child_item.parent()
64+
65+
if parent_item == self.root_item:
66+
return QModelIndex()
67+
68+
return self.createIndex(parent_item.row(), 0, parent_item)
69+
70+
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
71+
if parent.column() > 0:
72+
return 0
73+
74+
if not parent.isValid():
75+
parent_item = self.root_item
76+
else:
77+
parent_item = parent.internalPointer()
78+
79+
return parent_item.childCount()
80+
81+
def columnCount(self, /, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
82+
return 1
83+
84+
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
85+
if not index.isValid():
86+
return None
87+
88+
item = index.internalPointer()
89+
90+
if role == Qt.ItemDataRole.DisplayRole:
91+
if isinstance(item.data, dict):
92+
# Show userdata alongside the key if it exists
93+
if item.userdata is not None:
94+
return f"{item.key} [{item.userdata}]"
95+
return f"{item.key}"
96+
else:
97+
return f"{item.key}: {item.data}"
98+
elif role == Qt.ItemDataRole.UserRole:
99+
return item.userdata
100+
101+
return None
102+
103+
def update_data(self, new_data: Dict):
104+
self.beginResetModel()
105+
self.root_item = TreeItem(new_data)
106+
self.endResetModel()

0 commit comments

Comments
 (0)