2
2
from collections .abc import Callable
3
3
from typing import override
4
4
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
7
7
from PySide6 .QtGui import QAction , QBrush , QCloseEvent , QColor , QPainter , QPen , QRegularExpressionValidator
8
8
from PySide6 .QtWidgets import (
9
9
QDialog ,
22
22
QStyleOptionGraphicsItem ,
23
23
QVBoxLayout ,
24
24
QWidget ,
25
+ QTreeView ,
25
26
)
26
27
28
+ from kevinbotlib .comm import KevinbotCommClient
29
+ from kevinbotlib .logger import Logger
30
+
27
31
from kevinbotlib_dashboard .grid_theme import Themes
32
+ from kevinbotlib_dashboard .tree import DictTreeModel
28
33
from kevinbotlib_dashboard .widgets import Divider
29
34
30
35
@@ -389,18 +394,16 @@ def __init__(self, graphics_view, parent=None):
389
394
super ().__init__ (parent )
390
395
self .graphics_view = graphics_view
391
396
self .controller = WidgetGridController (self .graphics_view )
392
- self .setup_ui ()
393
397
394
- def setup_ui (self ):
395
398
layout = QVBoxLayout (self )
396
399
layout .setSpacing (10 )
397
400
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 )
404
407
405
408
def add_widget (self , widget_name ):
406
409
self .controller .add (WidgetItem (widget_name , self .graphics_view ))
@@ -465,6 +468,8 @@ def __init__(self):
465
468
466
469
self .settings = QSettings ("kevinbotlib" , "dashboard" )
467
470
471
+ self .logger = Logger ()
472
+
468
473
self .client = KevinbotCommClient (
469
474
host = self .settings .value ("ip" , "10.0.0.2" , str ), # type: ignore
470
475
port = self .settings .value ("port" , 8765 , int ), # type: ignore
@@ -502,6 +507,8 @@ def __init__(self):
502
507
cols = self .settings .value ("cols" , 10 , int ), # type: ignore
503
508
)
504
509
palette = WidgetPalette (self .graphics_view )
510
+ self .model = palette .model
511
+ self .tree = palette .tree
505
512
506
513
layout .addWidget (self .graphics_view )
507
514
layout .addWidget (palette )
@@ -511,6 +518,11 @@ def __init__(self):
511
518
self .latency_timer .timeout .connect (self .update_latency )
512
519
self .latency_timer .start ()
513
520
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
+
514
526
self .controller = WidgetGridController (self .graphics_view )
515
527
self .controller .load (self .item_loader , self .settings .value ("layout" , [], type = list )) # type: ignore
516
528
@@ -521,8 +533,115 @@ def update_latency(self):
521
533
if self .client .websocket :
522
534
self .latency_status .setText (f"Latency: { self .client .websocket .latency :.2f} ms" )
523
535
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
+
524
642
def on_connect (self ):
525
643
self .connection_status .setText ("Robot Connected" )
644
+ self .update_tree ()
526
645
527
646
def on_disconnect (self ):
528
647
self .connection_status .setText ("Robot Disconnected" )
0 commit comments