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
|
### 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
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
|
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()
|
||||||
|
|||||||
@ -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
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)
|
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."""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user