Skip to content

Commit eb6a459

Browse files
committed
use kevinbotlib theme
1 parent e68f487 commit eb6a459

File tree

6 files changed

+922
-73
lines changed

6 files changed

+922
-73
lines changed

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ classifiers = [
2525
"Programming Language :: Python :: Implementation :: PyPy",
2626
]
2727
dependencies = [
28+
"kevinbotlib==1.0.0a3",
2829
"pyside6~=6.8.2.1",
30+
"qtawesome>=1.3.1",
2931
]
3032

3133
[project.urls]
@@ -68,3 +70,6 @@ exclude_lines = [
6870
"if __name__ == .__main__.:",
6971
"if TYPE_CHECKING:",
7072
]
73+
74+
[tool.ruff.lint]
75+
ignore = ["G004", "TRY400"]

src/kevinbotlib_dashboard/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66

77
from kevinbotlib.logger import Level, Logger, LoggerConfiguration
8+
from kevinbotlib.ui.theme import ThemeStyle, Theme
89
from PySide6.QtCore import QCommandLineOption, QCommandLineParser, QCoreApplication
910
from PySide6.QtWidgets import QApplication
1011

@@ -17,6 +18,8 @@ def run():
1718
app.setApplicationName("KevinbotLib Dashboard")
1819
app.setApplicationVersion(__about__.__version__)
1920

21+
Theme(ThemeStyle.System).apply(app)
22+
2023
parser = QCommandLineParser()
2124
parser.addHelpOption()
2225
parser.addVersionOption()

src/kevinbotlib_dashboard/app.py

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

5-
6-
from PySide6.QtCore import QObject, QPointF, QRect, QRectF, QRegularExpression, QSettings, QSize, Qt, QTimer, Signal, Slot, QModelIndex
5+
from kevinbotlib.comm import KevinbotCommClient
6+
from kevinbotlib.logger import Logger
7+
from kevinbotlib.ui.theme import Theme, ThemeStyle
8+
9+
from PySide6.QtCore import (
10+
QModelIndex,
11+
QObject,
12+
QPointF,
13+
QRect,
14+
QRectF,
15+
QRegularExpression,
16+
QSettings,
17+
QSize,
18+
Qt,
19+
QTimer,
20+
Signal,
21+
Slot,
22+
)
723
from PySide6.QtGui import QAction, QBrush, QCloseEvent, QColor, QPainter, QPen, QRegularExpressionValidator
824
from PySide6.QtWidgets import (
925
QDialog,
@@ -20,15 +36,13 @@
2036
QPushButton,
2137
QSpinBox,
2238
QStyleOptionGraphicsItem,
39+
QTreeView,
2340
QVBoxLayout,
2441
QWidget,
25-
QTreeView,
2642
)
2743

28-
from kevinbotlib.comm import KevinbotCommClient
29-
from kevinbotlib.logger import Logger
30-
3144
from kevinbotlib_dashboard.grid_theme import Themes
45+
from kevinbotlib_dashboard.toast import Notifier, Severity
3246
from kevinbotlib_dashboard.tree import DictTreeModel
3347
from kevinbotlib_dashboard.widgets import Divider
3448

@@ -478,12 +492,20 @@ def __init__(self):
478492
)
479493
self.client.connect()
480494

495+
self.notifier = Notifier(self)
496+
481497
self.menu = self.menuBar()
482498
self.menu.setNativeMenuBar(False)
483499

500+
self.file_menu = self.menu.addMenu("&File")
501+
502+
self.save_action = self.file_menu.addAction("Save Layout", self.save_slot)
503+
self.save_action.setShortcut("Ctrl+S")
504+
484505
self.edit_menu = self.menu.addMenu("&Edit")
485506

486507
self.settings_action = self.edit_menu.addAction("Settings", self.open_settings)
508+
self.settings_action.setShortcut("Ctrl+,")
487509

488510
self.status = self.statusBar()
489511

@@ -534,13 +556,16 @@ def update_latency(self):
534556
self.latency_status.setText(f"Latency: {self.client.websocket.latency:.2f}ms")
535557

536558
@Slot()
537-
def update_tree(self, *args):
559+
def update_tree(self):
538560
# Get the latest data
539561
data_store = self.client.get_keys()
540562
data = {}
541563

542564
# here we process what data can be displayed
543565
for key, value in [(key, self.client.get_raw(key)) for key in data_store]:
566+
if not value:
567+
continue
568+
544569
if "struct" in value and "dashboard" in value["struct"]:
545570
structured = {}
546571
for viewable in value["struct"]["dashboard"]:
@@ -565,13 +590,12 @@ def update_tree(self, *args):
565590
data[key] = structured
566591
else:
567592
self.logger.error(f"Could not display {key}, it dosen't contain a structure")
568-
569593

570594
def to_hierarchical_dict(flat_dict: dict):
571595
"""Convert a flat dictionary into a hierarchical one based on '/'."""
572596
hierarchical_dict = {}
573597
for key, value in flat_dict.items():
574-
parts = key.split('/')
598+
parts = key.split("/")
575599
d = hierarchical_dict
576600
for part in parts[:-1]:
577601
d = d.setdefault(part, {})
@@ -581,21 +605,19 @@ def to_hierarchical_dict(flat_dict: dict):
581605
# Convert flat dictionary to hierarchical
582606

583607
expanded_indexes = []
584-
def store_expansion(parent=QModelIndex()):
608+
609+
def store_expansion(parent):
585610
for row in range(self.model.rowCount(parent)):
586611
index = self.model.index(row, 0, parent)
587612
if self.tree.isExpanded(index):
588-
expanded_indexes.append((
589-
self.get_index_path(index),
590-
True
591-
))
613+
expanded_indexes.append((self.get_index_path(index), True))
592614
store_expansion(index)
593-
store_expansion()
594-
615+
616+
store_expansion(QModelIndex())
617+
595618
# Store selection
596619
selected_paths = self.get_selection_paths()
597620

598-
599621
# Update data...
600622
h = to_hierarchical_dict(data)
601623
self.model.update_data(h)
@@ -606,17 +628,16 @@ def restore_expansion():
606628
index = self.get_index_from_path(path)
607629
if index.isValid() and was_expanded:
608630
self.tree.setExpanded(index, True)
631+
609632
restore_expansion()
610-
633+
611634
# Restore selection
612635
self.restore_selection(selected_paths)
613636

614637
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
638+
return [
639+
self.get_index_path(index) for index in self.tree.selectionModel().selectedIndexes() if index.column() == 0
640+
]
620641

621642
def restore_selection(self, paths):
622643
selection_model = self.tree.selectionModel()
@@ -638,7 +659,7 @@ def get_index_from_path(self, path):
638659
for row in path:
639660
index = self.model.index(row, 0, index)
640661
return index
641-
662+
642663
def on_connect(self):
643664
self.connection_status.setText("Robot Connected")
644665
self.update_tree()
@@ -698,6 +719,7 @@ def closeEvent(self, event: QCloseEvent):
698719

699720
def save_slot(self):
700721
self.settings.setValue("layout", self.controller.get_widgets())
722+
self.notifier.toast("Layout Saved", "Layout saved successfully", severity=Severity.Success)
701723

702724
def open_settings(self):
703725
self.settings_window.show()

src/kevinbotlib_dashboard/toast.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
from dataclasses import dataclass
2+
from enum import Enum
3+
4+
import qtawesome as qta
5+
from PySide6.QtCore import QObject, QTimer, Signal
6+
from PySide6.QtGui import QColor
7+
from PySide6.QtWidgets import QGraphicsOpacityEffect, QHBoxLayout, QLabel, QMainWindow, QVBoxLayout, QWidget
8+
9+
10+
@dataclass
11+
class CustomSeverity:
12+
icon: str
13+
color: QColor
14+
15+
16+
class Severity(Enum):
17+
Success = CustomSeverity("mdi6.check-bold", QColor("#31C376"))
18+
Info = CustomSeverity("mdi6.information-outline", QColor("#005C9F"))
19+
Warning = CustomSeverity("mdi6.alert", QColor("#FF9800"))
20+
Error = CustomSeverity("mdi6.alert-circle", QColor("#F44336"))
21+
Critical = CustomSeverity("mdi6.alert-octagon", QColor("#9C27B0"))
22+
23+
24+
class NotificationWidget(QWidget):
25+
closed = Signal()
26+
27+
def __init__(self, title: str, text: str, severity: CustomSeverity, duration: int, parent=None):
28+
super().__init__(parent)
29+
self.duration = duration
30+
self.setup_ui(title, text, severity)
31+
self.setup_animations()
32+
self.setAutoFillBackground(True)
33+
34+
def setup_ui(self, title: str, text: str, severity: CustomSeverity):
35+
# Main layout
36+
layout = QVBoxLayout(self)
37+
layout.setContentsMargins(0, 0, 0, 0)
38+
39+
# Create content widget with background
40+
self.content = QWidget()
41+
self.content.setObjectName("notification")
42+
content_layout = QHBoxLayout(self.content)
43+
44+
# Icon
45+
icon_label = qta.IconWidget()
46+
icon_label.setIconSize(38)
47+
icon_label.setIcon(qta.icon(severity.icon, color=severity.color.name()))
48+
content_layout.addWidget(icon_label)
49+
50+
# Text content
51+
text_layout = QVBoxLayout()
52+
title_label = QLabel(title)
53+
title_label.setStyleSheet(f"font-weight: bold; color: {severity.color.name()}")
54+
text_layout.addWidget(title_label)
55+
56+
message_label = QLabel(text)
57+
message_label.setWordWrap(True)
58+
text_layout.addWidget(message_label)
59+
60+
content_layout.addLayout(text_layout, 3)
61+
layout.addWidget(self.content)
62+
63+
# Style
64+
self.content.setStyleSheet(f"""
65+
QWidget#notification {{
66+
border: 2px solid {severity.color.name()};
67+
border-radius: 4px;
68+
padding: 8px;
69+
}}
70+
""")
71+
72+
# Set fixed width but dynamic height
73+
self.setFixedWidth(300)
74+
self.adjustSize()
75+
76+
def setup_animations(self):
77+
# Opacity effect for fade animations
78+
self.opacity_effect = QGraphicsOpacityEffect(self)
79+
self.opacity_effect.setOpacity(0)
80+
self.setGraphicsEffect(self.opacity_effect)
81+
82+
# Fade in
83+
self.fade_in_timer = QTimer(self)
84+
self.fade_in_timer.timeout.connect(self._fade_in)
85+
self.fade_in_timer.start(10)
86+
87+
# Display duration
88+
QTimer.singleShot(self.duration, self.start_fade_out)
89+
90+
self.current_opacity = 0
91+
92+
def _fade_in(self):
93+
self.current_opacity += 0.1
94+
if self.current_opacity >= 1:
95+
self.fade_in_timer.stop()
96+
self.current_opacity = 1
97+
self.opacity_effect.setOpacity(self.current_opacity)
98+
99+
def start_fade_out(self):
100+
self.fade_out_timer = QTimer(self)
101+
self.fade_out_timer.timeout.connect(self._fade_out)
102+
self.fade_out_timer.start(10)
103+
104+
def _fade_out(self):
105+
self.current_opacity -= 0.1
106+
if self.current_opacity <= 0:
107+
self.fade_out_timer.stop()
108+
self.close()
109+
self.closed.emit()
110+
self.opacity_effect.setOpacity(self.current_opacity)
111+
112+
113+
class Notifier(QObject):
114+
def __init__(self, parent: QMainWindow):
115+
super().__init__(parent)
116+
self.notifications = []
117+
self.margin = 10
118+
self.parent_window = parent
119+
120+
def toast(self, title: str, text: str, duration: int = 2500, severity: Severity | CustomSeverity = Severity.Info):
121+
if isinstance(severity, Severity):
122+
severity = severity.value
123+
124+
notification = NotificationWidget(title, text, severity, duration, self.parent())
125+
notification.closed.connect(lambda: self._remove_notification(notification))
126+
127+
# Calculate position
128+
self._update_positions(notification)
129+
notification.show()
130+
self.notifications.append(notification)
131+
132+
def _update_positions(self, new_notification=None):
133+
screen_geometry = self.parent_window.geometry()
134+
base_x = screen_geometry.width() - 300 - self.margin # 300 is notification width
135+
current_y = screen_geometry.height() - self.margin - self.parent_window.statusBar().height()
136+
137+
# Position existing notifications
138+
for notification in reversed(self.notifications):
139+
current_y -= notification.height() + self.margin
140+
notification.move(base_x, current_y)
141+
142+
# Position new notification if provided
143+
if new_notification:
144+
current_y -= new_notification.height() + self.margin
145+
new_notification.move(base_x, current_y)
146+
147+
def _remove_notification(self, notification):
148+
if notification in self.notifications:
149+
self.notifications.remove(notification)
150+
self._update_positions() # Reposition remaining notifications

0 commit comments

Comments
 (0)