dsh UI improvement

This commit is contained in:
ykcheungab
2026-09-12 13:26:39 +08:00
parent 705eaabcca
commit 803a08fc78
23 changed files with 1861 additions and 167 deletions
@@ -26,6 +26,9 @@ _DEFAULTS: dict[str, Any] = {
"plot_shear_max": 20.0,
"image_view_mode": "arrows_pure",
"arrow_rotation_deg": 90,
"show_fps": False,
"splitter_sizes": [340, 480, 400],
"window_default_size": [1440, 880],
}
@@ -4,8 +4,11 @@
"panel_border": "#2a2d33",
"text_primary": "#e8eaed",
"text_secondary": "#9aa0a6",
"accent": "#536dce",
"accent_hover": "#6b82d9",
"text_muted": "#6f757c",
"accent": "#4a62c0",
"accent_hover": "#5c74d0",
"accent_text": "#ffffff",
"toolbar_bg": "#1a1c20",
"button_bg": "#2a2d33",
"button_hover": "#3a3f47",
"input_bg": "#0f1012",
@@ -15,9 +18,28 @@
"plot_fn": "#8ab4f8",
"plot_fx": "#34a853",
"plot_fy": "#fbbc04",
"plot_bg": "#121316",
"plot_bg": "#15171a",
"plot_grid": "#2a2d33",
"arrow_canvas": "#121316",
"danger": "#ea4335",
"success": "#34a853"
"danger_hover": "#f2554a",
"danger_text": "#ffffff",
"warning": "#fbbc04",
"info": "#8ab4f8",
"success": "#34a853",
"status_idle": "#9aa0a6",
"status_connecting": "#fbbc04",
"status_running": "#34a853",
"status_error": "#ea4335",
"table_header_bg": "#23262b",
"table_row_alt": "#1e2126",
"table_selected": "#33406b",
"scrollbar_bg": "#1a1c20",
"scrollbar_handle": "#3a3f47",
"scrollbar_handle_hover": "#4a5058",
"card_bg": "#1e2126",
"card_border": "#32363d",
"separator": "#2a2d33",
"focus_ring": "#6b82d9",
"hint_text": "#7d838a"
}
@@ -1,23 +1,45 @@
{
"window_bg": "#f5f6f8",
"panel_bg": "#ffffff",
"panel_border": "#d0d4dc",
"text_primary": "#1a1c20",
"text_secondary": "#5f6368",
"accent": "#536dce",
"accent_hover": "#3f56b5",
"button_bg": "#e8eaed",
"button_hover": "#d0d4dc",
"input_bg": "#ffffff",
"input_border": "#c0c4cc",
"menu_bg": "#ffffff",
"menu_hover": "#e8eaed",
"plot_fn": "#1a73e8",
"window_bg": "#ECEFF4",
"panel_bg": "#FFFFFF",
"panel_border": "#D0D5DE",
"text_primary": "#1A1C20",
"text_secondary": "#5F6368",
"text_muted": "#7C838C",
"accent": "#003893",
"accent_hover": "#2A4FA8",
"accent_text": "#ffffff",
"toolbar_bg": "#E8EBF0",
"button_bg": "#F4F6FA",
"button_hover": "#E8ECF5",
"input_bg": "#FFFFFF",
"input_border": "#C5CAD3",
"menu_bg": "#FFFFFF",
"menu_hover": "#EEF1F7",
"plot_fn": "#003893",
"plot_fx": "#188038",
"plot_fy": "#f9ab00",
"plot_bg": "#ffffff",
"plot_grid": "#e0e3e8",
"arrow_canvas": "#eef0f3",
"danger": "#d93025",
"success": "#188038"
"plot_fy": "#E8710A",
"plot_bg": "#FAFBFD",
"plot_grid": "#E3E6EC",
"arrow_canvas": "#EEF1F7",
"danger": "#D93025",
"danger_hover": "#B3261C",
"danger_text": "#ffffff",
"warning": "#F9AB00",
"info": "#1A73E8",
"success": "#1B8A5A",
"status_idle": "#5F6368",
"status_connecting": "#F9AB00",
"status_running": "#1B8A5A",
"status_error": "#D93025",
"table_header_bg": "#F1F3F8",
"table_row_alt": "#F8F9FC",
"table_selected": "#DCE6FA",
"scrollbar_bg": "#EDEFF3",
"scrollbar_handle": "#C3C9D4",
"scrollbar_handle_hover": "#A8B0BE",
"card_bg": "#F7F9FC",
"card_border": "#DDE2EA",
"separator": "#D8DDE6",
"focus_ring": "#2A4FA8",
"hint_text": "#7C838C"
}
+459 -11
View File
@@ -21,8 +21,71 @@ def load_colors(theme: str = "dark") -> dict[str, str]:
return {str(k): str(v) for k, v in data.items()}
def _arrow_icon_cache_dir() -> Path:
import tempfile
path = Path(tempfile.gettempdir()) / "orisys_app_theme_icons"
path.mkdir(parents=True, exist_ok=True)
return path
@lru_cache(maxsize=16)
def _paint_arrow(kind: str, fill: str) -> str:
"""Draw a small chevron PNG on demand; return its path ('' on failure)."""
try:
from PyQt5.QtCore import QPointF, Qt
from PyQt5.QtGui import QColor, QImage, QPainter, QPolygonF
except Exception: # pragma: no cover - headless import failure
return ""
try:
size = 14
safe = fill.strip().lstrip("#").replace("/", "")
path = _arrow_icon_cache_dir() / f"chevron_{kind}_{safe}.png"
if path.is_file():
return str(path)
image = QImage(size, size, QImage.Format_ARGB32)
image.fill(Qt.transparent)
painter = QPainter(image)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setBrush(QColor(fill))
painter.setPen(Qt.NoPen)
half = size / 2.0
top = size * 0.32
bottom = size * 0.68
if kind == "down":
points = [QPointF(size * 0.18, top), QPointF(size * 0.82, top), QPointF(half, bottom)]
else:
points = [
QPointF(size * 0.18, bottom),
QPointF(size * 0.82, bottom),
QPointF(half, top),
]
painter.drawPolygon(QPolygonF(points))
painter.end()
if not image.save(str(path), "PNG"):
return ""
return str(path)
except Exception: # pragma: no cover - never break theming
return ""
def _arrow_image(kind: str, fill: str, size: int = 14) -> str:
path = _paint_arrow(kind, fill)
if not path:
return ""
return 'url("' + path.replace("\\", "/") + '")'
def build_qss(theme: str = "dark") -> str:
c = load_colors(theme)
accent_text = c.get("accent_text", "#FFFFFF")
toolbar_bg = c.get("toolbar_bg", c["panel_bg"])
dim = c.get("text_secondary", "#888888")
muted = c.get("text_muted", "#6f757c")
arrow_down = _arrow_image("down", dim)
arrow_up = _arrow_image("up", dim)
arrow_down_disabled = _arrow_image("down", muted)
arrow_up_disabled = _arrow_image("up", muted)
return f"""
QWidget {{
background-color: {c['window_bg']};
@@ -37,6 +100,11 @@ QMenuBar {{
background-color: {c['menu_bg']};
color: {c['text_primary']};
border-bottom: 1px solid {c['panel_border']};
padding: 2px 4px;
}}
QMenuBar::item {{
padding: 5px 10px;
border-radius: 4px;
}}
QMenuBar::item:selected {{
background-color: {c['menu_hover']};
@@ -45,54 +113,433 @@ QMenu {{
background-color: {c['menu_bg']};
color: {c['text_primary']};
border: 1px solid {c['panel_border']};
border-radius: 8px;
padding: 4px;
}}
QMenu::item {{
padding: 6px 18px;
border-radius: 4px;
}}
QMenu::item:selected {{
background-color: {c['menu_hover']};
}}
QToolBar {{
background-color: {c['panel_bg']};
border-bottom: 1px solid {c['panel_border']};
spacing: 8px;
padding: 4px;
QMenu::separator {{
height: 1px;
background-color: {c['separator']};
margin: 4px 8px;
}}
QMenu::indicator {{
width: 14px;
height: 14px;
margin-left: 6px;
}}
QToolBar {{
background-color: {toolbar_bg};
border-bottom: 1px solid {c['panel_border']};
spacing: 10px;
padding: 8px 10px;
}}
QToolTip {{
background-color: {c['menu_bg']};
color: {c['text_primary']};
border: 1px solid {c['panel_border']};
border-radius: 6px;
padding: 5px 8px;
}}
/* ---------- Buttons ---------- */
QPushButton {{
background-color: {c['button_bg']};
color: {c['text_primary']};
border: 1px solid {c['panel_border']};
border-radius: 4px;
padding: 6px 12px;
border-radius: 8px;
padding: 7px 14px;
min-height: 28px;
}}
QPushButton:hover {{
background-color: {c['button_hover']};
border-color: {c['accent_hover']};
}}
QPushButton:pressed {{
background-color: {c['menu_hover']};
}}
QPushButton:focus {{
border-color: {c['focus_ring']};
}}
QPushButton:disabled {{
color: {c['text_muted']};
background-color: {c['window_bg']};
border: 1px solid {c['separator']};
}}
QPushButton#primaryButton {{
background-color: {c['accent']};
color: {accent_text};
border: 1px solid {c['accent']};
font-weight: 600;
padding: 7px 18px;
}}
QPushButton#primaryButton:hover {{
background-color: {c['accent_hover']};
border-color: {c['accent_hover']};
}}
QPushButton#primaryButton:pressed,
QPushButton#primaryButton:checked {{
background-color: {c['accent_hover']};
border-color: {c['accent']};
}}
QPushButton#primaryButton:disabled {{
background-color: {c['panel_border']};
color: {c['text_muted']};
border-color: {c['panel_border']};
}}
QPushButton#dangerButton {{
background-color: {c['danger']};
color: {c['danger_text']};
border: 1px solid {c['danger']};
font-weight: 600;
padding: 7px 18px;
}}
QPushButton#dangerButton:hover {{
background-color: {c['danger_hover']};
border-color: {c['danger_hover']};
}}
QPushButton#dangerButton:pressed,
QPushButton#dangerButton:checked {{
background-color: {c['danger_hover']};
border-color: {c['danger']};
}}
QPushButton#dangerButton:disabled {{
background-color: {c['panel_border']};
color: {c['text_muted']};
border-color: {c['panel_border']};
}}
QPushButton#ghostButton {{
background-color: transparent;
border: 1px solid {c['panel_border']};
color: {c['text_secondary']};
}}
QComboBox, QLineEdit, QSpinBox {{
QPushButton#ghostButton:hover {{
background-color: {c['button_hover']};
color: {c['text_primary']};
}}
QPushButton[compact="true"] {{
padding: 6px 9px;
font-size: 12px;
}}
QToolButton {{
background-color: transparent;
color: {c['text_secondary']};
border: 1px solid transparent;
border-radius: 6px;
padding: 4px 8px;
}}
QToolButton:hover {{
background-color: {c['button_hover']};
color: {c['text_primary']};
border-color: {c['panel_border']};
}}
/* ---------- Inputs ---------- */
QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox, QAbstractSpinBox {{
background-color: {c['input_bg']};
color: {c['text_primary']};
border: 1px solid {c['input_border']};
border-radius: 8px;
padding: 5px 10px;
min-height: 28px;
selection-background-color: {c['accent']};
selection-color: {accent_text};
}}
QComboBox:hover, QLineEdit:hover, QSpinBox:hover, QDoubleSpinBox:hover {{
border-color: {c['accent_hover']};
}}
QComboBox:focus, QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus {{
border: 1px solid {c['accent']};
}}
QComboBox:disabled, QLineEdit:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled {{
color: {c['text_muted']};
background-color: {c['window_bg']};
border-color: {c['separator']};
}}
QComboBox::drop-down {{
subcontrol-origin: padding;
subcontrol-position: top right;
width: 22px;
border: none;
background: transparent;
}}
QComboBox::down-arrow {{
image: {arrow_down};
width: 12px;
height: 12px;
margin-right: 4px;
}}
QComboBox::down-arrow:disabled {{
image: {arrow_down_disabled};
}}
QComboBox QAbstractItemView {{
background-color: {c['menu_bg']};
color: {c['text_primary']};
border: 1px solid {c['panel_border']};
border-radius: 8px;
padding: 4px;
outline: none;
selection-background-color: {c['menu_hover']};
selection-color: {c['text_primary']};
}}
QAbstractSpinBox::up-button, QAbstractSpinBox::down-button {{
subcontrol-origin: border;
width: 18px;
background: transparent;
border: none;
}}
QAbstractSpinBox::up-button {{
subcontrol-position: top right;
}}
QAbstractSpinBox::down-button {{
subcontrol-position: bottom right;
}}
QAbstractSpinBox::up-arrow {{
image: {arrow_up};
width: 10px;
height: 10px;
}}
QAbstractSpinBox::down-arrow {{
image: {arrow_down};
width: 10px;
height: 10px;
}}
QAbstractSpinBox::up-arrow:disabled {{
image: {arrow_up_disabled};
}}
QAbstractSpinBox::down-arrow:disabled {{
image: {arrow_down_disabled};
}}
/* ---------- Check / radio ---------- */
QCheckBox, QRadioButton {{
background: transparent;
color: {c['text_primary']};
spacing: 7px;
padding: 2px 0;
}}
QCheckBox::indicator, QRadioButton::indicator {{
width: 15px;
height: 15px;
background-color: {c['input_bg']};
border: 1px solid {c['input_border']};
}}
QCheckBox::indicator {{
border-radius: 4px;
padding: 4px 8px;
min-height: 24px;
}}
QRadioButton::indicator {{
border-radius: 8px;
}}
QCheckBox::indicator:hover, QRadioButton::indicator:hover {{
border-color: {c['accent_hover']};
}}
QCheckBox::indicator:checked, QRadioButton::indicator:checked {{
background-color: {c['accent']};
border: 4px solid {c['accent']};
}}
QRadioButton::indicator:checked {{
background-color: {c['input_bg']};
border: 4px solid {c['accent']};
}}
QCheckBox::indicator:disabled, QRadioButton::indicator:disabled {{
background-color: {c['window_bg']};
border-color: {c['separator']};
}}
/* ---------- Progress ---------- */
QProgressBar {{
background-color: {c['input_bg']};
color: {c['text_secondary']};
border: 1px solid {c['input_border']};
border-radius: 7px;
height: 14px;
text-align: center;
font-size: 11px;
}}
QProgressBar::chunk {{
background-color: {c['accent']};
border-radius: 6px;
margin: 1px;
}}
/* ---------- Tables ---------- */
QTableWidget, QTableView {{
background-color: {c['input_bg']};
alternate-background-color: {c['table_row_alt']};
color: {c['text_primary']};
border: 1px solid {c['panel_border']};
border-radius: 8px;
gridline-color: {c['separator']};
outline: none;
selection-background-color: {c['table_selected']};
selection-color: {c['text_primary']};
}}
QTableWidget::item, QTableView::item {{
padding: 4px 6px;
border: none;
}}
QTableWidget::item:selected, QTableView::item:selected {{
background-color: {c['table_selected']};
color: {c['text_primary']};
}}
QHeaderView {{
background-color: {c['table_header_bg']};
border: none;
}}
QHeaderView::section {{
background-color: {c['table_header_bg']};
color: {c['text_secondary']};
border: none;
border-right: 1px solid {c['separator']};
border-bottom: 1px solid {c['panel_border']};
padding: 5px 6px;
font-weight: 600;
font-size: 12px;
}}
QHeaderView::section:last {{
border-right: none;
}}
QTableCornerButton::section {{
background-color: {c['table_header_bg']};
border: none;
}}
/* ---------- Scrollbars ---------- */
QScrollBar:vertical {{
background: {c['scrollbar_bg']};
width: 10px;
margin: 0;
border: none;
}}
QScrollBar:horizontal {{
background: {c['scrollbar_bg']};
height: 10px;
margin: 0;
border: none;
}}
QScrollBar::handle:vertical {{
background: {c['scrollbar_handle']};
min-height: 28px;
border-radius: 5px;
margin: 2px;
}}
QScrollBar::handle:horizontal {{
background: {c['scrollbar_handle']};
min-width: 28px;
border-radius: 5px;
margin: 2px;
}}
QScrollBar::handle:vertical:hover, QScrollBar::handle:horizontal:hover {{
background: {c['scrollbar_handle_hover']};
}}
QScrollBar::add-line, QScrollBar::sub-line {{
width: 0;
height: 0;
border: none;
background: transparent;
}}
QScrollBar::add-page, QScrollBar::sub-page {{
background: transparent;
}}
QScrollArea {{
border: none;
background: transparent;
}}
/* ---------- Labels / cards ---------- */
QLabel {{
background: transparent;
}}
QLabel#panelTitle {{
color: {c['text_secondary']};
font-weight: 600;
font-size: 12px;
letter-spacing: 0.3px;
padding: 1px 0;
}}
QLabel#panelMeta {{
color: {c['text_secondary']};
font-weight: 500;
padding-right: 8px;
}}
QLabel#fieldLabel {{
color: {c['text_secondary']};
font-weight: 500;
}}
QLabel#hintText {{
color: {c['hint_text']};
background: transparent;
}}
QLabel#unitBadge {{
color: {c['text_secondary']};
background-color: {c['card_bg']};
border: 1px solid {c['card_border']};
border-radius: 6px;
padding: 1px 7px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.4px;
}}
QLabel#valueCard {{
background-color: {c['card_bg']};
border: 1px solid {c['card_border']};
border-radius: 8px;
}}
QLabel#statusChip {{
color: {c['status_idle']};
background: transparent;
font-weight: 600;
padding: 0 10px 0 4px;
}}
QLabel#aboutBody {{
color: {c['text_secondary']};
}}
/* ---------- Frames ---------- */
QFrame#contentPanel {{
background-color: {c['panel_bg']};
border: 1px solid {c['panel_border']};
border-radius: 6px;
border-radius: 10px;
}}
QFrame#cardPanel {{
background-color: {c['card_bg']};
border: 1px solid {c['card_border']};
border-radius: 8px;
}}
QFrame#sectionSeparator {{
background-color: {c['separator']};
border: none;
max-width: 1px;
min-width: 1px;
}}
QGroupBox {{
background-color: {c['card_bg']};
border: 1px solid {c['card_border']};
border-radius: 8px;
margin-top: 10px;
padding: 10px 8px 8px 8px;
font-weight: 600;
}}
QGroupBox::title {{
subcontrol-origin: margin;
subcontrol-position: top left;
left: 10px;
padding: 0 4px;
color: {c['text_secondary']};
}}
/* ---------- Status bar ---------- */
QStatusBar {{
background-color: {c['panel_bg']};
color: {c['text_secondary']};
border-top: 1px solid {c['panel_border']};
}}
QStatusBar::item {{
border: none;
}}
QSplitter::handle {{
background-color: {c['window_bg']};
width: 6px;
height: 6px;
}}
QSplitter::handle:hover {{
background-color: {c['accent_hover']};
}}
"""
@@ -103,3 +550,4 @@ def color(theme: str, key: str, default: str = "#888888") -> str:
def clear_theme_cache() -> None:
load_colors.cache_clear()
_paint_arrow.cache_clear()