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:
parent
251f5d327e
commit
c6f0d2fdde
3
PLAN.md
3
PLAN.md
@ -190,7 +190,8 @@ LIN_Control_Tool/
|
||||
---
|
||||
|
||||
### 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.
|
||||
|
||||
**Features:**
|
||||
|
||||
186
README.md
Normal file
186
README.md
Normal 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.
|
||||
@ -39,8 +39,9 @@ Qt's exit code (0 = normal, non-zero = error).
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from PyQt6.QtGui import QIcon
|
||||
from PyQt6.QtGui import QIcon, QFont
|
||||
from main_window import MainWindow
|
||||
from theme import STYLESHEET
|
||||
|
||||
|
||||
def main():
|
||||
@ -53,12 +54,14 @@ def main():
|
||||
app.setApplicationName("LIN Simulator")
|
||||
app.setOrganizationName("TeqanyLogix LTD")
|
||||
|
||||
# Set application icon — used in the taskbar, window title bar, and dock.
|
||||
# Path resolves relative to this script's location: src/ → ../../resources/
|
||||
# Set application icon
|
||||
icon_path = Path(__file__).parent.parent.parent / "resources" / "logo.png"
|
||||
if icon_path.exists():
|
||||
app.setWindowIcon(QIcon(str(icon_path)))
|
||||
|
||||
# Apply automotive/industrial dark theme
|
||||
app.setStyleSheet(STYLESHEET)
|
||||
|
||||
# Step 2: Create and show the main window.
|
||||
# show() makes it visible — without this call, the window exists but is hidden.
|
||||
window = MainWindow()
|
||||
|
||||
@ -127,7 +127,8 @@ Our layout strategy:
|
||||
|
||||
The Connection Panel is a QDockWidget — meaning the user can:
|
||||
- Drag it to any edge of the window
|
||||
- Undock it to a floating window
|
||||
- Undock it
|
||||
window
|
||||
- Close/reopen it from the View menu
|
||||
This is useful because during normal operation you might want more
|
||||
space for Tx/Rx tables and hide the connection panel.
|
||||
@ -593,7 +594,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# ── Status display ──
|
||||
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)
|
||||
|
||||
# ── Device info ──
|
||||
@ -714,7 +715,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# Permanent connection status on the right side
|
||||
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)
|
||||
|
||||
# Show initial message
|
||||
@ -1428,7 +1429,7 @@ class MainWindow(QMainWindow):
|
||||
old_val = prev_values.get(sig_info.name)
|
||||
if old_val is not None and old_val != value:
|
||||
# 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:
|
||||
# Reset background
|
||||
sig_item.setBackground(4, QBrush())
|
||||
@ -1593,7 +1594,7 @@ class MainWindow(QMainWindow):
|
||||
if item.text(0) == frame_name:
|
||||
# Light blue background for currently transmitting frame
|
||||
for col in range(tree.columnCount()):
|
||||
item.setBackground(col, QBrush(QColor(173, 216, 230)))
|
||||
item.setBackground(col, QBrush(QColor(230, 126, 34, 60)))
|
||||
break
|
||||
|
||||
def _clear_frame_highlight(self):
|
||||
@ -1707,17 +1708,17 @@ class MainWindow(QMainWindow):
|
||||
|
||||
if state == ConnectionState.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_disconnect.setEnabled(False)
|
||||
self.lbl_device_info.setText("Device Info: —")
|
||||
self.lbl_status_connection.setText("● Disconnected")
|
||||
self.lbl_status_connection.setStyleSheet("color: red;")
|
||||
self.lbl_status_connection.setStyleSheet("color: #e74c3c;")
|
||||
|
||||
elif state == ConnectionState.CONNECTING:
|
||||
self.lbl_conn_status.setText("Status: Connecting...")
|
||||
self.lbl_conn_status.setStyleSheet(
|
||||
"color: orange; font-weight: bold;"
|
||||
"color: #e67e22; font-weight: bold;"
|
||||
)
|
||||
self.btn_connect.setEnabled(False)
|
||||
self.btn_disconnect.setEnabled(False)
|
||||
@ -1726,7 +1727,7 @@ class MainWindow(QMainWindow):
|
||||
port_info = self._conn_mgr.connected_port
|
||||
self.lbl_conn_status.setText("Status: Connected")
|
||||
self.lbl_conn_status.setStyleSheet(
|
||||
"color: green; font-weight: bold;"
|
||||
"color: #27ae60; font-weight: bold;"
|
||||
)
|
||||
self.btn_connect.setEnabled(False)
|
||||
self.btn_disconnect.setEnabled(True)
|
||||
@ -1736,17 +1737,17 @@ class MainWindow(QMainWindow):
|
||||
f"{port_info.description}"
|
||||
)
|
||||
self.lbl_status_connection.setText("● Connected")
|
||||
self.lbl_status_connection.setStyleSheet("color: green;")
|
||||
self.lbl_status_connection.setStyleSheet("color: #27ae60;")
|
||||
|
||||
elif state == ConnectionState.ERROR:
|
||||
self.lbl_conn_status.setText(
|
||||
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_disconnect.setEnabled(False)
|
||||
self.lbl_status_connection.setText("● Error")
|
||||
self.lbl_status_connection.setStyleSheet("color: red;")
|
||||
self.lbl_status_connection.setStyleSheet("color: #e74c3c;")
|
||||
|
||||
# ─── Slot: About ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
476
python/src/theme.py
Normal file
476
python/src/theme.py
Normal 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']};
|
||||
}}
|
||||
|
||||
"""
|
||||
@ -101,9 +101,9 @@ class TestChangeHighlighting:
|
||||
frame_item = window.rx_table.topLevelItem(0)
|
||||
temp_sig = frame_item.child(1) # MotorTemp changed: 0x10 → 0x20
|
||||
bg = temp_sig.background(4)
|
||||
# Should be yellow (255, 255, 100)
|
||||
assert bg.color().red() == 255
|
||||
assert bg.color().green() == 255
|
||||
# Should be highlighted (amber/yellow with alpha)
|
||||
assert bg.color().red() > 200
|
||||
assert bg.color().green() > 150
|
||||
|
||||
def test_unchanged_signal_not_highlighted(self, window):
|
||||
"""Signal that didn't change should not be highlighted."""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user