Skip to content

Commit 8cd703b

Browse files
Merge pull request #108 from drunkenbot-ai/develop
Develop
2 parents a711677 + d07afbe commit 8cd703b

14 files changed

Lines changed: 639 additions & 125 deletions

interface/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
from engine.training_service import run_training_job
9595

9696
from interface.startup_splash import StartupSplash
97+
from interface.theme import apply_theme, load_startup_theme
9798

9899
from engine.license_client import load_stored_license_key
99100
from interface.license_activation_dialog import LicenseActivationDialog, run_license_check_responsively
@@ -269,6 +270,7 @@ def main(app: Optional[QApplication] = None, splash: Optional[StartupSplash] = N
269270
except Exception:
270271
LOGGER.exception("Could not set Windows app user model ID")
271272
app = app or QApplication(sys.argv)
273+
apply_theme(load_startup_theme())
272274
app.setFont(QFont("Arial", 10))
273275
app.setWindowIcon(MainWindow._static_app_icon())
274276
splash = splash or StartupValidationSplash()

interface/charts.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,24 @@
88
from PySide6.QtGui import QCursor
99
from PySide6.QtWidgets import QSizePolicy, QToolTip, QVBoxLayout, QWidget
1010

11+
from interface.theme import DARK_THEME, current_theme
12+
13+
14+
def _apply_plot_theme(plot: pg.PlotWidget, title: str, x_label: str, y_label: str, theme: str) -> None:
15+
"""Apply colors to a pyqtgraph plot, which does not inherit Qt QSS."""
16+
if theme == DARK_THEME:
17+
background, foreground, axis = "#141414", "#eeeeee", "#6a6a6a"
18+
else:
19+
background, foreground, axis = "#ffffff", "#202020", "#8a8a8a"
20+
plot.setBackground(background)
21+
plot.setTitle(title, color=foreground, size="10pt")
22+
plot.setLabel("bottom", x_label, color=foreground)
23+
plot.setLabel("left", y_label, color=foreground)
24+
plot.getAxis("bottom").setPen(pg.mkPen(axis))
25+
plot.getAxis("left").setPen(pg.mkPen(axis))
26+
plot.getAxis("bottom").setTextPen(pg.mkPen(foreground))
27+
plot.getAxis("left").setTextPen(pg.mkPen(foreground))
28+
1129

1230
class DatasetBarChartWidget(QWidget):
1331
"""Compact bar chart for dataset composition and token statistics."""
@@ -33,22 +51,18 @@ def __init__(
3351
self.labels: list[str] = []
3452
self.values: list[float] = []
3553
self.value_suffix = ""
54+
self.title = title
55+
self.y_label = y_label
3656

3757
layout = QVBoxLayout(self)
3858
layout.setContentsMargins(0, 0, 0, 0)
3959
layout.setSpacing(0)
4060
pg.setConfigOptions(antialias=True)
4161
self.plot = pg.PlotWidget()
42-
self.plot.setBackground("#141414")
43-
self.plot.setTitle(title, color="#eeeeee", size="10pt")
44-
self.plot.setLabel("left", y_label, color="#d7d7d7")
62+
self.apply_theme(current_theme())
4563
self.plot.showGrid(x=False, y=True, alpha=0.24)
4664
self.plot.setMenuEnabled(False)
4765
self.plot.setMouseEnabled(x=False, y=False)
48-
self.plot.getAxis("bottom").setPen(pg.mkPen("#6a6a6a"))
49-
self.plot.getAxis("left").setPen(pg.mkPen("#6a6a6a"))
50-
self.plot.getAxis("bottom").setTextPen(pg.mkPen("#cfcfcf"))
51-
self.plot.getAxis("left").setTextPen(pg.mkPen("#cfcfcf"))
5266
self.plot.getPlotItem().setContentsMargins(8, 8, 8, 8)
5367
self.bar_item = pg.BarGraphItem(x=[], height=[], width=0.58, brush=pg.mkBrush("#f5b041"))
5468
self.plot.addItem(self.bar_item)
@@ -58,6 +72,10 @@ def __init__(
5872
layout.addWidget(self.plot)
5973
self.clear()
6074

75+
def apply_theme(self, theme: str) -> None:
76+
"""Refresh this custom plot for the selected application theme."""
77+
_apply_plot_theme(self.plot, self.title, "", self.y_label, theme)
78+
6179
def clear(self) -> None:
6280
"""Clear chart values."""
6381

@@ -163,17 +181,10 @@ def __init__(
163181
layout.setSpacing(0)
164182
pg.setConfigOptions(antialias=True)
165183
self.plot = pg.PlotWidget()
166-
self.plot.setBackground("#141414")
167-
self.plot.setTitle(title, color="#eeeeee", size="11pt")
168-
self.plot.setLabel("bottom", "Optimizer step", color="#d7d7d7")
169-
self.plot.setLabel("left", y_label, color="#d7d7d7")
184+
self.apply_theme(current_theme())
170185
self.plot.showGrid(x=True, y=True, alpha=0.28)
171186
self.plot.setMenuEnabled(False)
172187
self.plot.setMouseEnabled(x=True, y=True)
173-
self.plot.getAxis("bottom").setPen(pg.mkPen("#6a6a6a"))
174-
self.plot.getAxis("left").setPen(pg.mkPen("#6a6a6a"))
175-
self.plot.getAxis("bottom").setTextPen(pg.mkPen("#cfcfcf"))
176-
self.plot.getAxis("left").setTextPen(pg.mkPen("#cfcfcf"))
177188
self.plot.getPlotItem().setContentsMargins(10, 8, 10, 8)
178189
self.legend = self.plot.addLegend(offset=(12, 8), brush=pg.mkBrush(20, 20, 20, 180), pen=pg.mkPen("#444444"))
179190
self.primary_curve = self.plot.plot([], [], pen=pg.mkPen("#f5b041", width=2), name=primary_label)
@@ -188,6 +199,10 @@ def __init__(
188199
layout.addWidget(self.plot)
189200
self._refresh_plot()
190201

202+
def apply_theme(self, theme: str) -> None:
203+
"""Refresh this custom plot for the selected application theme."""
204+
_apply_plot_theme(self.plot, self.title, "Optimizer step", self.y_label, theme)
205+
191206
def clear(self) -> None:
192207
"""Remove all plotted loss values."""
193208

@@ -397,4 +412,3 @@ def _nearest_point(self, x_value: float, y_value: float) -> Optional[tuple[str,
397412
)
398413
distance = ((nearest[1] - x_value) / x_span) ** 2 + ((nearest[2] - y_value) / y_span) ** 2
399414
return nearest if distance < 0.01 else None
400-

interface/core/project_state.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ def _default_project_state(self) -> dict[str, Any]:
2323
return {
2424
"schema": "drunkenbot_ide_project",
2525
"version": 1,
26+
"theme": "dark",
27+
"theme_preference_version": 1,
2628
"project_name": "",
2729
"project_dir": "",
2830
"paths": {
@@ -254,6 +256,8 @@ def _project_state_dict(self, project_name: str, project_dir: Path) -> dict[str,
254256
return {
255257
"schema": "drunkenbot_ide_project",
256258
"version": 1,
259+
"theme": self.theme_name,
260+
"theme_preference_version": 1,
257261
"project_name": project_name,
258262
"project_dir": str(project_dir),
259263
"created_at": created_at,

interface/core/project_state_apply.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# ProjectStateApplyMixin mixin. Shared runtime names are provided by interface.app.
44
from typing import Any, Optional, Union # noqa: F401
55
from interface import app as _app
6+
from interface.theme import project_theme
67

78
globals().update({name: value for name, value in vars(_app).items() if not name.startswith("__")})
89

@@ -15,6 +16,7 @@ def _apply_project_state(self, data: dict[str, Any]) -> None:
1516
data: Project state loaded from JSON.
1617
"""
1718

19+
self.set_theme(project_theme(data), persist=False)
1820
self.search_box.setText(str(data.get("project_name", "")))
1921
paths = data.get("paths", {})
2022
dataset = data.get("dataset", {})

interface/core/window_core.py

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
# WindowCoreMixin mixin. Shared runtime names are provided by interface.app.
44
from typing import Any, Optional, Union # noqa: F401
5-
from interface.widgets.app_shell import build_main_shell
5+
from interface.widgets.app_shell import build_main_shell, update_navigation_icons
6+
from interface.theme import apply_theme, current_theme, normalize_theme
67
from interface import app as _app
78

89
globals().update({name: value for name, value in vars(_app).items() if not name.startswith("__")})
@@ -74,10 +75,12 @@ def __init__(self) -> None:
7475
self.job_manager_timer.setInterval(2500)
7576
self.job_manager_timer.timeout.connect(self.refresh_job_manager_tab)
7677

77-
self._apply_style()
78+
self.theme_name = current_theme()
79+
self._apply_theme()
7880

7981
shell = self._build_shell()
8082
self.setCentralWidget(shell)
83+
update_navigation_icons(self)
8184
self._install_ui_event_logging(shell)
8285
self._install_wheel_guard(shell)
8386
self._refresh_notification_manager()
@@ -152,6 +155,26 @@ def _install_ui_event_logging(self, root: QWidget) -> None:
152155
lambda item=widget: self._log_ui_event("edited", item, item.text())
153156
)
154157

158+
def edit_focused_widget(self, method_name: str) -> None:
159+
"""Run a standard edit operation on the currently focused editor.
160+
161+
Args:
162+
method_name: Qt editor method to invoke, such as ``copy`` or
163+
``selectAll``.
164+
"""
165+
widget = QApplication.focusWidget()
166+
method = getattr(widget, method_name, None) if widget is not None else None
167+
if callable(method):
168+
method()
169+
170+
def show_about_dialog(self) -> None:
171+
"""Display the application identity and version."""
172+
QMessageBox.information(
173+
self,
174+
f"About {APP_NAME}",
175+
f"{APP_NAME} {APP_VERSION}\n\nA desktop environment for building, training, and using LLMs.",
176+
)
177+
155178
def _log_ui_event(self, action: str, widget: QWidget, value: Any) -> None:
156179
"""Log a UI action or parameter value.
157180
@@ -185,11 +208,31 @@ def _widget_log_name(widget: QWidget) -> str:
185208
return widget.objectName()
186209
return widget.__class__.__name__
187210

188-
def _apply_style(self) -> None:
189-
"""Load the application stylesheet from the QSS module file."""
211+
def _apply_theme(self) -> None:
212+
"""Apply the window's selected theme application-wide."""
213+
self.theme_name = apply_theme(self.theme_name)
190214

191-
qss_path = Path(_app.__file__).with_name("styles.qss")
192-
self.setStyleSheet(qss_path.read_text(encoding="utf-8"))
215+
def update_theme_actions(self) -> None:
216+
"""Synchronize the theme menu checks with the active theme."""
217+
if not hasattr(self, "system_theme_action"):
218+
return
219+
self.system_theme_action.setChecked(self.theme_name == "system")
220+
self.dark_theme_action.setChecked(self.theme_name == "dark")
221+
222+
def set_theme(self, theme: object, persist: bool = True) -> None:
223+
"""Select an application theme and persist it for the active project.
224+
225+
Args:
226+
theme: Requested theme identifier.
227+
persist: Whether to save the selection to the active project.
228+
"""
229+
self.theme_name = normalize_theme(theme)
230+
self._apply_theme()
231+
if hasattr(self, "side_rail"):
232+
update_navigation_icons(self)
233+
self.update_theme_actions()
234+
if persist and self.current_project_file is not None:
235+
self.save_project()
193236

194237
def _build_shell(self) -> QWidget:
195238
"""Build the top-level dashboard shell from reusable widgets."""

0 commit comments

Comments
 (0)