Add README, automotive dark theme, and polished GUI

- README.md with full project documentation, architecture, setup guide
- Automotive/industrial dark theme (theme.py) inspired by Vector CANoe
- Dark blue-black background with amber/orange accents
- Styled buttons (green Start, red Stop), orange hover effects
- Custom scrollbars, tooltips, group box titles, tree widget headers
- Updated highlight colors to match theme palette

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mohamed Salem 2026-04-08 03:28:02 +02:00
parent 251f5d327e
commit c6f0d2fdde
6 changed files with 686 additions and 19 deletions

View File

@ -190,7 +190,8 @@ LIN_Control_Tool/
--- ---
### Step 8 — Integration & End-to-End ### Step 8 — Integration & End-to-End
- **Status:** In progress - **Status:** DONE — Python (182 tests) | C++ (124 tests)
- **Features:** BabyLinBackend wired to GUI, global rate live update, full workflow tests, edge case handling
- **Goal:** Wire all components together, full workflow testing. - **Goal:** Wire all components together, full workflow testing.
**Features:** **Features:**

186
README.md Normal file
View File

@ -0,0 +1,186 @@
# LIN Simulator
A cross-platform GUI tool for simulating LIN master nodes using **BabyLIN-RC-II** devices by Lipowsky. Built in two parallel implementations — **Python (PyQt6)** and **C++ (Qt6)** — maintained in feature parity.
**Owner:** TeqanyLogix LTD
**Developer:** Mohamed Salem
---
## Features
- **LDF Loading** — Parse LIN Description Files, auto-reload on file change
- **Tx Panel** — Expandable tree view of master frames with editable signal values
- **Rx Panel** — Real-time slave frame display with timestamps and change highlighting
- **Bit Packing** — Signal value edits automatically update frame bytes (and vice versa)
- **Hex/Dec Toggle** — Switch all values between hexadecimal and decimal display
- **Schedule Tables** — Select and run LDF schedule tables with start/stop/pause
- **Mock Rx Simulation** — Simulated slave responses for testing without hardware
- **Connection Panel** — Serial port discovery, connect/disconnect with status indicator
- **BabyLIN Backend** — Wraps Lipowsky's BabyLIN DLL for hardware communication
- **Baud Rate Detection** — Automatically extracted from LDF's LIN_speed field
- **FreeFormat Entries** — Support for raw diagnostic/configuration schedule entries
- **Automotive Dark Theme** — Industrial UI inspired by Vector CANoe and ETAS INCA
## Screenshots
*(Launch the app and load an LDF to see the automotive dark theme in action)*
## Project Structure
```
LIN_Control_Tool/
├── python/ # Python implementation
│ ├── src/
│ │ ├── main.py # Entry point
│ │ ├── main_window.py # GUI layout and logic
│ │ ├── ldf_handler.py # LDF parsing adapter (wraps ldfparser)
│ │ ├── connection_manager.py # Serial port state machine
│ │ ├── babylin_backend.py # BabyLIN DLL wrapper
│ │ ├── scheduler.py # QTimer-based schedule execution
│ │ └── theme.py # Automotive dark theme stylesheet
│ ├── tests/ # pytest test suite (182 tests)
│ └── requirements.txt
├── cpp/ # C++ implementation
│ ├── src/
│ │ ├── main.cpp # Entry point
│ │ ├── main_window.h/.cpp # GUI layout and logic
│ │ ├── ldf_parser.h/.cpp # Custom LDF parser (regex-based)
│ │ ├── connection_manager.h/.cpp # QSerialPort state machine
│ │ └── scheduler.h/.cpp # QTimer-based schedule execution
│ ├── tests/ # QTest test suite (124 tests)
│ └── CMakeLists.txt
├── resources/
│ ├── sample.ldf # Sample LIN 2.1 LDF for testing
│ ├── logo.svg # Application logo (SVG source)
│ └── logo.png # Application logo (512x512 PNG)
└── docs/ # Step-by-step documentation
```
## Quick Start
### Python
```bash
cd python
pip install -r requirements.txt
cd src && python main.py
```
### C++
```bash
cd cpp
mkdir build && cd build
cmake .. -DCMAKE_PREFIX_PATH=$(brew --prefix qt@6) # macOS
# cmake .. # Linux/Windows
cmake --build .
./lin_simulator
```
## Running Tests
### Python (pytest)
```bash
cd python
python -m pytest tests/ -v # all tests
python -m pytest tests/test_ldf_handler.py -v # single file
```
### C++ (QTest / CTest)
```bash
cd cpp/build
ctest --output-on-failure -V # all tests
./test_main_window # single test executable
```
## Dependencies
### Python
| Package | Version | Purpose |
|---------|---------|---------|
| PyQt6 | >= 6.5.0 | GUI framework |
| ldfparser | >= 0.25.0 | LDF file parsing |
| pyserial | >= 3.5 | Serial port communication |
| pytest | >= 7.0.0 | Test framework |
### C++
| Library | Purpose |
|---------|---------|
| Qt6 Widgets | GUI framework |
| Qt6 SerialPort | Serial port communication |
| Qt6 Test | Test framework |
| CMake >= 3.16 | Build system |
## BabyLIN Hardware Support
The tool communicates with **BabyLIN-RC-II** devices using Lipowsky's official BabyLIN DLL.
| Platform | Status |
|----------|--------|
| **Linux** | Full hardware support via `libBabyLIN.so` |
| **Windows** | Full hardware support via `BabyLIN.dll` |
| **macOS** | Mock mode (development/testing only — no native DLL) |
### Hardware Setup
1. Install LinWorks from Lipowsky
2. Compile your LDF into an SDF file using LinWorks
3. Connect the BabyLIN-RC-II via USB
4. Launch the LIN Simulator → Refresh → Connect → Load SDF → Start
### Mock Mode
When the BabyLIN DLL is not available, the tool operates in **mock mode**:
- All GUI features work normally
- The scheduler generates simulated Rx data
- Useful for GUI development and testing without hardware
## Architecture
```
┌─────────────────────────────────────────────────────────────────────┐
│ MainWindow (GUI) │
│ ┌──────────┐ ┌──────────────────────────┐ ┌──────────────────┐ │
│ │Connection│ │ Tx Tree │ Rx Tree │ │ Control Bar │ │
│ │ Panel │ │ (editable) │ (live) │ │ Start/Stop/Pause│ │
│ └────┬─────┘ └──────┬─────┴──────┬──────┘ └────────┬─────────┘ │
│ │ │ │ │ │
├───────┴───────────────┴────────────┴───────────────────┴────────────┤
│ Application Logic │
│ ┌────────────────┐ ┌─────────────┐ ┌────────────────────────┐ │
│ │ LDF Handler │ │ Scheduler │ │ Connection Manager │ │
│ │ (parse LDF) │ │ (QTimer) │ │ (serial port state) │ │
│ └────────────────┘ └──────┬──────┘ └───────────┬────────────┘ │
│ │ │ │
├─────────────────────────────┴─────────────────────┴─────────────────┤
│ Hardware Layer │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ BabyLIN Backend (wraps Lipowsky DLL / mock mode) │ │
│ │ → libBabyLIN.so (Linux) / BabyLIN.dll (Windows) │ │
│ └──────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
## LDF File Support
The tool parses standard LIN 2.0/2.1 LDF files and extracts:
- Protocol/language version and baud rate
- Master and slave node definitions
- Frame definitions with signal mappings (bit offset, width, initial value)
- Schedule tables (including FreeFormat diagnostic entries)
- Node attributes
### Sample LDF
A test LDF (`resources/sample.ldf`) is included with:
- 1 master node (ECU_Master), 2 slave nodes
- 4 frames (2 Tx, 2 Rx), 9 signals
- 2 schedule tables (NormalSchedule, FastSchedule)
- 19200 baud
## License
Copyright 2026 TeqanyLogix LTD. All rights reserved.

View File

@ -39,8 +39,9 @@ Qt's exit code (0 = normal, non-zero = error).
import sys import sys
from pathlib import Path from pathlib import Path
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
from PyQt6.QtGui import QIcon from PyQt6.QtGui import QIcon, QFont
from main_window import MainWindow from main_window import MainWindow
from theme import STYLESHEET
def main(): def main():
@ -53,12 +54,14 @@ def main():
app.setApplicationName("LIN Simulator") app.setApplicationName("LIN Simulator")
app.setOrganizationName("TeqanyLogix LTD") app.setOrganizationName("TeqanyLogix LTD")
# Set application icon — used in the taskbar, window title bar, and dock. # Set application icon
# Path resolves relative to this script's location: src/ → ../../resources/
icon_path = Path(__file__).parent.parent.parent / "resources" / "logo.png" icon_path = Path(__file__).parent.parent.parent / "resources" / "logo.png"
if icon_path.exists(): if icon_path.exists():
app.setWindowIcon(QIcon(str(icon_path))) app.setWindowIcon(QIcon(str(icon_path)))
# Apply automotive/industrial dark theme
app.setStyleSheet(STYLESHEET)
# Step 2: Create and show the main window. # Step 2: Create and show the main window.
# show() makes it visible — without this call, the window exists but is hidden. # show() makes it visible — without this call, the window exists but is hidden.
window = MainWindow() window = MainWindow()

View File

@ -127,7 +127,8 @@ Our layout strategy:
The Connection Panel is a QDockWidget meaning the user can: The Connection Panel is a QDockWidget meaning the user can:
- Drag it to any edge of the window - Drag it to any edge of the window
- Undock it to a floating window - Undock it
window
- Close/reopen it from the View menu - Close/reopen it from the View menu
This is useful because during normal operation you might want more This is useful because during normal operation you might want more
space for Tx/Rx tables and hide the connection panel. space for Tx/Rx tables and hide the connection panel.
@ -593,7 +594,7 @@ class MainWindow(QMainWindow):
# ── Status display ── # ── Status display ──
self.lbl_conn_status = QLabel("Status: Disconnected") self.lbl_conn_status = QLabel("Status: Disconnected")
self.lbl_conn_status.setStyleSheet("color: red; font-weight: bold;") self.lbl_conn_status.setStyleSheet("color: #e74c3c; font-weight: bold;")
layout.addWidget(self.lbl_conn_status) layout.addWidget(self.lbl_conn_status)
# ── Device info ── # ── Device info ──
@ -714,7 +715,7 @@ class MainWindow(QMainWindow):
# Permanent connection status on the right side # Permanent connection status on the right side
self.lbl_status_connection = QLabel("● Disconnected") self.lbl_status_connection = QLabel("● Disconnected")
self.lbl_status_connection.setStyleSheet("color: red;") self.lbl_status_connection.setStyleSheet("color: #e74c3c;")
status_bar.addPermanentWidget(self.lbl_status_connection) status_bar.addPermanentWidget(self.lbl_status_connection)
# Show initial message # Show initial message
@ -1428,7 +1429,7 @@ class MainWindow(QMainWindow):
old_val = prev_values.get(sig_info.name) old_val = prev_values.get(sig_info.name)
if old_val is not None and old_val != value: if old_val is not None and old_val != value:
# Yellow background for changed signals # Yellow background for changed signals
sig_item.setBackground(4, QBrush(QColor(255, 255, 100))) sig_item.setBackground(4, QBrush(QColor(241, 196, 15, 100)))
else: else:
# Reset background # Reset background
sig_item.setBackground(4, QBrush()) sig_item.setBackground(4, QBrush())
@ -1593,7 +1594,7 @@ class MainWindow(QMainWindow):
if item.text(0) == frame_name: if item.text(0) == frame_name:
# Light blue background for currently transmitting frame # Light blue background for currently transmitting frame
for col in range(tree.columnCount()): for col in range(tree.columnCount()):
item.setBackground(col, QBrush(QColor(173, 216, 230))) item.setBackground(col, QBrush(QColor(230, 126, 34, 60)))
break break
def _clear_frame_highlight(self): def _clear_frame_highlight(self):
@ -1707,17 +1708,17 @@ class MainWindow(QMainWindow):
if state == ConnectionState.DISCONNECTED: if state == ConnectionState.DISCONNECTED:
self.lbl_conn_status.setText("Status: Disconnected") self.lbl_conn_status.setText("Status: Disconnected")
self.lbl_conn_status.setStyleSheet("color: red; font-weight: bold;") self.lbl_conn_status.setStyleSheet("color: #e74c3c; font-weight: bold;")
self.btn_connect.setEnabled(True) self.btn_connect.setEnabled(True)
self.btn_disconnect.setEnabled(False) self.btn_disconnect.setEnabled(False)
self.lbl_device_info.setText("Device Info: —") self.lbl_device_info.setText("Device Info: —")
self.lbl_status_connection.setText("● Disconnected") self.lbl_status_connection.setText("● Disconnected")
self.lbl_status_connection.setStyleSheet("color: red;") self.lbl_status_connection.setStyleSheet("color: #e74c3c;")
elif state == ConnectionState.CONNECTING: elif state == ConnectionState.CONNECTING:
self.lbl_conn_status.setText("Status: Connecting...") self.lbl_conn_status.setText("Status: Connecting...")
self.lbl_conn_status.setStyleSheet( self.lbl_conn_status.setStyleSheet(
"color: orange; font-weight: bold;" "color: #e67e22; font-weight: bold;"
) )
self.btn_connect.setEnabled(False) self.btn_connect.setEnabled(False)
self.btn_disconnect.setEnabled(False) self.btn_disconnect.setEnabled(False)
@ -1726,7 +1727,7 @@ class MainWindow(QMainWindow):
port_info = self._conn_mgr.connected_port port_info = self._conn_mgr.connected_port
self.lbl_conn_status.setText("Status: Connected") self.lbl_conn_status.setText("Status: Connected")
self.lbl_conn_status.setStyleSheet( self.lbl_conn_status.setStyleSheet(
"color: green; font-weight: bold;" "color: #27ae60; font-weight: bold;"
) )
self.btn_connect.setEnabled(False) self.btn_connect.setEnabled(False)
self.btn_disconnect.setEnabled(True) self.btn_disconnect.setEnabled(True)
@ -1736,17 +1737,17 @@ class MainWindow(QMainWindow):
f"{port_info.description}" f"{port_info.description}"
) )
self.lbl_status_connection.setText("● Connected") self.lbl_status_connection.setText("● Connected")
self.lbl_status_connection.setStyleSheet("color: green;") self.lbl_status_connection.setStyleSheet("color: #27ae60;")
elif state == ConnectionState.ERROR: elif state == ConnectionState.ERROR:
self.lbl_conn_status.setText( self.lbl_conn_status.setText(
f"Status: Error\n{self._conn_mgr.error_message}" f"Status: Error\n{self._conn_mgr.error_message}"
) )
self.lbl_conn_status.setStyleSheet("color: red; font-weight: bold;") self.lbl_conn_status.setStyleSheet("color: #e74c3c; font-weight: bold;")
self.btn_connect.setEnabled(True) self.btn_connect.setEnabled(True)
self.btn_disconnect.setEnabled(False) self.btn_disconnect.setEnabled(False)
self.lbl_status_connection.setText("● Error") self.lbl_status_connection.setText("● Error")
self.lbl_status_connection.setStyleSheet("color: red;") self.lbl_status_connection.setStyleSheet("color: #e74c3c;")
# ─── Slot: About ────────────────────────────────────────────────── # ─── Slot: About ──────────────────────────────────────────────────

476
python/src/theme.py Normal file
View File

@ -0,0 +1,476 @@
"""
theme.py Automotive/Industrial dark theme for the LIN Simulator.
Inspired by Vector CANoe, ETAS INCA, and automotive diagnostic tools.
Dark background with amber/orange accents for an industrial look.
QT STYLESHEETS:
===============
Qt supports CSS-like stylesheets for customizing widget appearance.
The syntax is similar to web CSS but with Qt-specific selectors:
QPushButton { applies to ALL QPushButtons
background-color: #333;
color: white;
border-radius: 4px;
}
QPushButton:hover { when mouse hovers over button
background-color: #555;
}
QPushButton#myButton { ← specific widget with objectName "myButton"
background-color: red;
}
QTreeWidget::item { items inside the tree widget
padding: 4px;
}
This is how professional Qt applications get their polished look
without creating custom paint code for every widget.
"""
# ── Color Palette ─────────────────────────────────────────────────────
# Automotive/industrial color scheme
COLORS = {
# Base colors
"bg_dark": "#1a1a2e", # Main background (very dark blue-black)
"bg_medium": "#16213e", # Panel backgrounds
"bg_light": "#0f3460", # Lighter panels, selected items
"bg_input": "#1c2541", # Input field backgrounds
# Accent colors (amber/orange — automotive instrument cluster feel)
"accent": "#e67e22", # Primary accent (amber/orange)
"accent_hover": "#f39c12", # Hover state (brighter amber)
"accent_dim": "#d35400", # Pressed/active state
# Status colors
"green": "#27ae60", # Connected, success
"red": "#e74c3c", # Disconnected, error
"yellow": "#f1c40f", # Warning, change highlight
"blue": "#3498db", # Info, frame highlight
# Text
"text": "#ecf0f1", # Primary text (light gray)
"text_dim": "#95a5a6", # Secondary text (dimmer)
"text_dark": "#2c3e50", # Text on light backgrounds
# Borders
"border": "#2c3e50", # Subtle borders
"border_light": "#34495e", # Lighter borders
}
# ── Main Stylesheet ───────────────────────────────────────────────────
STYLESHEET = f"""
/* Global */
QMainWindow {{
background-color: {COLORS['bg_dark']};
}}
QWidget {{
background-color: {COLORS['bg_dark']};
color: {COLORS['text']};
font-family: "Segoe UI", "SF Pro Display", "Helvetica Neue", Arial, sans-serif;
font-size: 13px;
}}
/* Menu Bar */
QMenuBar {{
background-color: {COLORS['bg_medium']};
color: {COLORS['text']};
border-bottom: 1px solid {COLORS['border']};
padding: 2px;
}}
QMenuBar::item {{
padding: 6px 12px;
border-radius: 4px;
}}
QMenuBar::item:selected {{
background-color: {COLORS['accent']};
color: white;
}}
QMenu {{
background-color: {COLORS['bg_medium']};
color: {COLORS['text']};
border: 1px solid {COLORS['border']};
padding: 4px;
}}
QMenu::item {{
padding: 6px 24px;
border-radius: 3px;
}}
QMenu::item:selected {{
background-color: {COLORS['accent']};
color: white;
}}
QMenu::separator {{
height: 1px;
background-color: {COLORS['border']};
margin: 4px 8px;
}}
/* Toolbars */
QToolBar {{
background-color: {COLORS['bg_medium']};
border: 1px solid {COLORS['border']};
padding: 4px;
spacing: 6px;
}}
QToolBar QLabel {{
color: {COLORS['text_dim']};
font-weight: bold;
font-size: 12px;
}}
QToolBar::separator {{
width: 1px;
background-color: {COLORS['border_light']};
margin: 4px 6px;
}}
/* Buttons */
QPushButton {{
background-color: {COLORS['bg_light']};
color: {COLORS['text']};
border: 1px solid {COLORS['border_light']};
border-radius: 5px;
padding: 6px 16px;
font-weight: bold;
min-height: 24px;
}}
QPushButton:hover {{
background-color: {COLORS['accent']};
border-color: {COLORS['accent']};
color: white;
}}
QPushButton:pressed {{
background-color: {COLORS['accent_dim']};
}}
QPushButton:disabled {{
background-color: {COLORS['bg_dark']};
color: {COLORS['text_dim']};
border-color: {COLORS['border']};
}}
/* Start button green accent */
QPushButton[text*="Start"] {{
background-color: {COLORS['green']};
color: white;
border-color: {COLORS['green']};
}}
QPushButton[text*="Start"]:hover {{
background-color: #2ecc71;
}}
QPushButton[text*="Start"]:disabled {{
background-color: {COLORS['bg_dark']};
color: {COLORS['text_dim']};
border-color: {COLORS['border']};
}}
/* Stop button red accent */
QPushButton[text*="Stop"] {{
background-color: {COLORS['red']};
color: white;
border-color: {COLORS['red']};
}}
QPushButton[text*="Stop"]:hover {{
background-color: #c0392b;
}}
QPushButton[text*="Stop"]:disabled {{
background-color: {COLORS['bg_dark']};
color: {COLORS['text_dim']};
border-color: {COLORS['border']};
}}
/* Input Fields */
QLineEdit {{
background-color: {COLORS['bg_input']};
color: {COLORS['text']};
border: 1px solid {COLORS['border_light']};
border-radius: 4px;
padding: 5px 8px;
selection-background-color: {COLORS['accent']};
}}
QLineEdit:read-only {{
background-color: {COLORS['bg_dark']};
color: {COLORS['text_dim']};
}}
QSpinBox {{
background-color: {COLORS['bg_input']};
color: {COLORS['accent']};
border: 1px solid {COLORS['border_light']};
border-radius: 4px;
padding: 4px 8px;
font-weight: bold;
font-size: 14px;
}}
QSpinBox::up-button, QSpinBox::down-button {{
background-color: {COLORS['bg_light']};
border: none;
width: 20px;
}}
QSpinBox::up-button:hover, QSpinBox::down-button:hover {{
background-color: {COLORS['accent']};
}}
/* Combo Boxes */
QComboBox {{
background-color: {COLORS['bg_input']};
color: {COLORS['text']};
border: 1px solid {COLORS['border_light']};
border-radius: 4px;
padding: 5px 8px;
min-height: 24px;
}}
QComboBox:hover {{
border-color: {COLORS['accent']};
}}
QComboBox::drop-down {{
background-color: {COLORS['bg_light']};
border: none;
width: 24px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}}
QComboBox QAbstractItemView {{
background-color: {COLORS['bg_medium']};
color: {COLORS['text']};
border: 1px solid {COLORS['border']};
selection-background-color: {COLORS['accent']};
selection-color: white;
}}
/* Check Boxes */
QCheckBox {{
color: {COLORS['text']};
spacing: 6px;
}}
QCheckBox::indicator {{
width: 18px;
height: 18px;
border: 2px solid {COLORS['border_light']};
border-radius: 3px;
background-color: {COLORS['bg_input']};
}}
QCheckBox::indicator:checked {{
background-color: {COLORS['accent']};
border-color: {COLORS['accent']};
}}
QCheckBox::indicator:hover {{
border-color: {COLORS['accent']};
}}
/* Group Boxes */
QGroupBox {{
background-color: {COLORS['bg_medium']};
border: 1px solid {COLORS['border']};
border-radius: 6px;
margin-top: 12px;
padding-top: 16px;
font-weight: bold;
font-size: 13px;
}}
QGroupBox::title {{
subcontrol-origin: margin;
subcontrol-position: top left;
padding: 4px 12px;
background-color: {COLORS['accent']};
color: white;
border-radius: 4px;
margin-left: 8px;
}}
/* Tree Widget (Tx/Rx Tables) */
QTreeWidget {{
background-color: {COLORS['bg_dark']};
alternate-background-color: {COLORS['bg_medium']};
color: {COLORS['text']};
border: 1px solid {COLORS['border']};
border-radius: 4px;
gridline-color: {COLORS['border']};
outline: none;
}}
QTreeWidget::item {{
padding: 4px 2px;
border-bottom: 1px solid {COLORS['border']};
min-height: 24px;
}}
QTreeWidget::item:selected {{
background-color: {COLORS['bg_light']};
color: {COLORS['accent']};
}}
QTreeWidget::item:hover {{
background-color: rgba(230, 126, 34, 0.15);
}}
QTreeWidget::branch {{
background-color: transparent;
}}
/* Header for tree/table */
QHeaderView::section {{
background-color: {COLORS['bg_medium']};
color: {COLORS['accent']};
border: 1px solid {COLORS['border']};
padding: 6px 8px;
font-weight: bold;
font-size: 12px;
text-transform: uppercase;
}}
/* Dock Widget */
QDockWidget {{
color: {COLORS['text']};
font-weight: bold;
titlebar-close-icon: none;
}}
QDockWidget::title {{
background-color: {COLORS['accent']};
color: white;
padding: 8px;
border-radius: 0px;
font-size: 13px;
}}
QDockWidget > QWidget {{
background-color: {COLORS['bg_medium']};
border: 1px solid {COLORS['border']};
}}
/* Status Bar */
QStatusBar {{
background-color: {COLORS['bg_medium']};
color: {COLORS['text_dim']};
border-top: 1px solid {COLORS['border']};
font-size: 12px;
padding: 2px;
}}
QStatusBar QLabel {{
font-weight: bold;
padding: 2px 8px;
}}
/* Scroll Bars */
QScrollBar:vertical {{
background-color: {COLORS['bg_dark']};
width: 10px;
border: none;
}}
QScrollBar::handle:vertical {{
background-color: {COLORS['border_light']};
border-radius: 5px;
min-height: 30px;
}}
QScrollBar::handle:vertical:hover {{
background-color: {COLORS['accent']};
}}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
height: 0px;
}}
QScrollBar:horizontal {{
background-color: {COLORS['bg_dark']};
height: 10px;
border: none;
}}
QScrollBar::handle:horizontal {{
background-color: {COLORS['border_light']};
border-radius: 5px;
min-width: 30px;
}}
QScrollBar::handle:horizontal:hover {{
background-color: {COLORS['accent']};
}}
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{
width: 0px;
}}
/* Tooltips */
QToolTip {{
background-color: {COLORS['bg_medium']};
color: {COLORS['text']};
border: 1px solid {COLORS['accent']};
border-radius: 4px;
padding: 6px;
font-size: 12px;
}}
/* Message Boxes */
QMessageBox {{
background-color: {COLORS['bg_medium']};
}}
QMessageBox QLabel {{
color: {COLORS['text']};
}}
QMessageBox QPushButton {{
min-width: 80px;
}}
/* Splitter */
QSplitter::handle {{
background-color: {COLORS['border']};
height: 3px;
}}
QSplitter::handle:hover {{
background-color: {COLORS['accent']};
}}
"""

View File

@ -101,9 +101,9 @@ class TestChangeHighlighting:
frame_item = window.rx_table.topLevelItem(0) frame_item = window.rx_table.topLevelItem(0)
temp_sig = frame_item.child(1) # MotorTemp changed: 0x10 → 0x20 temp_sig = frame_item.child(1) # MotorTemp changed: 0x10 → 0x20
bg = temp_sig.background(4) bg = temp_sig.background(4)
# Should be yellow (255, 255, 100) # Should be highlighted (amber/yellow with alpha)
assert bg.color().red() == 255 assert bg.color().red() > 200
assert bg.color().green() == 255 assert bg.color().green() > 150
def test_unchanged_signal_not_highlighted(self, window): def test_unchanged_signal_not_highlighted(self, window):
"""Signal that didn't change should not be highlighted.""" """Signal that didn't change should not be highlighted."""