from __future__ import annotations import os from PyQt6.QtWidgets import ( QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QPushButton, QStackedWidget, QSizePolicy, QStatusBar, QApplication ) from PyQt6.QtCore import Qt, QTimer, pyqtSignal from PyQt6.QtGui import QFont, QIcon NAV_ITEMS = [ ("dashboard", "⊞", "Dashboard"), ("chart", "📈", "Chart"), ("watchlist", "★", "Watchlist"), ("portfolio", "💼", "Portfolio"), ("ai", "✦", "AI"), ("news", "📰", "News"), ("screener", "⚙", "Screener"), ("settings", "⚙", "Settings"), ] class SidebarButton(QPushButton): def __init__(self, icon_text: str, tooltip: str, parent=None): super().__init__(icon_text, parent) self.setCheckable(True) self.setFixedSize(44, 44) self.setToolTip(tooltip) self.setFont(QFont("Segoe UI Emoji", 14)) class MainWindow(QMainWindow): def __init__(self, config): super().__init__() self._config = config self._nav_buttons: dict[str, SidebarButton] = {} self._alert_worker = None self._setup_status_bar() self._setup_ui() self._start_alerts() def _setup_ui(self): self.setWindowTitle("StockMind") self.setMinimumSize(1200, 700) self.resize(1440, 860) central = QWidget() self.setCentralWidget(central) layout = QHBoxLayout(central) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # Sidebar sidebar = self._build_sidebar() layout.addWidget(sidebar) # Content stack self._stack = QStackedWidget() layout.addWidget(self._stack) self._build_pages() self._set_page("dashboard") def _build_sidebar(self) -> QWidget: sidebar = QWidget() sidebar.setObjectName("sidebar") sidebar.setFixedWidth(56) sb_layout = QVBoxLayout(sidebar) sb_layout.setContentsMargins(6, 8, 6, 8) sb_layout.setSpacing(4) sb_layout.setAlignment(Qt.AlignmentFlag.AlignTop) logo = QLabel("S") logo.setObjectName("logo_label") logo.setAlignment(Qt.AlignmentFlag.AlignCenter) sb_layout.addWidget(logo) sb_layout.addSpacing(8) icons = { "dashboard": "⊞", "chart": "📈", "watchlist": "★", "portfolio": "💼", "ai": "✦", "news": "📰", "screener": "🔍", } labels = { "dashboard": "Dashboard", "chart": "Chart", "watchlist": "Watchlist", "portfolio": "Portfolio", "ai": "AI", "news": "News", "screener": "Screener", } for page_id, icon in icons.items(): btn = SidebarButton(icon, labels[page_id]) btn.clicked.connect(lambda checked, pid=page_id: self._set_page(pid)) self._nav_buttons[page_id] = btn sb_layout.addWidget(btn) sb_layout.addStretch() # Theme toggle self._theme_btn = SidebarButton("☀", "Toggle Theme") self._theme_btn.setCheckable(False) self._theme_btn.clicked.connect(self._toggle_theme) sb_layout.addWidget(self._theme_btn) # Settings settings_btn = SidebarButton("⚙", "Settings") settings_btn.setCheckable(False) settings_btn.clicked.connect(self._open_settings) sb_layout.addWidget(settings_btn) return sidebar def _build_pages(self): from ui.dashboard_widget import DashboardWidget from ui.chart_widget import ChartWidget from ui.watchlist_widget import WatchlistWidget from ui.portfolio_widget import PortfolioWidget from ui.ai_panel import AIPanelWidget from ui.news_widget import NewsWidget from ui.screener_widget import ScreenerWidget self._dashboard = DashboardWidget() self._chart = ChartWidget() self._watchlist = WatchlistWidget() self._portfolio = PortfolioWidget(self._config) self._ai_panel = AIPanelWidget(self._config) self._news = NewsWidget(self._config) self._screener = ScreenerWidget() pages = [ ("dashboard", self._dashboard), ("chart", self._chart), ("watchlist", self._watchlist), ("portfolio", self._portfolio), ("ai", self._ai_panel), ("news", self._news), ("screener", self._screener), ] self._page_indices: dict[str, int] = {} for page_id, widget in pages: idx = self._stack.addWidget(widget) self._page_indices[page_id] = idx # Wire cross-widget signals self._dashboard.symbol_selected.connect(self._navigate_to_chart) self._watchlist.symbol_selected.connect(self._navigate_to_chart) self._portfolio.symbol_selected.connect(self._navigate_to_chart) self._screener.symbol_selected.connect(self._navigate_to_chart) self._screener.add_to_watchlist.connect(lambda s: self._watchlist.add_symbol_external(s)) self._chart.symbol_changed.connect(self._on_symbol_changed) def _set_page(self, page_id: str): for pid, btn in self._nav_buttons.items(): btn.setChecked(pid == page_id) if page_id in self._page_indices: self._stack.setCurrentIndex(self._page_indices[page_id]) self._status_page_label.setText(page_id.capitalize()) def _navigate_to_chart(self, symbol: str): self._chart.load_symbol(symbol) self._set_page("chart") def _on_symbol_changed(self, symbol: str): self._ai_panel.set_symbol(symbol) self._news.set_symbol(symbol) def _toggle_theme(self): current = self._config.get_setting("theme", "dark") new_theme = "light" if current == "dark" else "dark" self._config.set_setting("theme", new_theme) self._apply_theme(new_theme) self._theme_btn.setText("☀" if new_theme == "dark" else "🌙") def _apply_theme(self, theme: str): base_dir = os.path.dirname(os.path.dirname(__file__)) path = os.path.join(base_dir, "assets", "styles", f"{theme}.qss") if os.path.exists(path): with open(path, "r") as f: QApplication.instance().setStyleSheet(f.read()) def _open_settings(self): from ui.settings_dialog import SettingsDialog dlg = SettingsDialog(self._config, self) dlg.exec() def _start_alerts(self): from core.alerts import AlertWorker self._alert_worker = AlertWorker(interval_seconds=300) self._alert_worker.alert_triggered.connect(self._on_alert_triggered) self._alert_worker.start() def _on_alert_triggered(self, symbol: str, alert_type: str, message: str): self.statusBar().showMessage(f"⚡ Alert: {message}", 10000) def _setup_status_bar(self): sb = self.statusBar() self._status_page_label = QLabel("Dashboard") self._status_page_label.setObjectName("subtitle") sb.addWidget(self._status_page_label) self._market_status_label = QLabel() self._market_status_label.setObjectName("subtitle") sb.addPermanentWidget(self._market_status_label) self._update_market_status() timer = QTimer(self) timer.timeout.connect(self._update_market_status) timer.start(60_000) def _update_market_status(self): from datetime import datetime, timezone import zoneinfo try: et = zoneinfo.ZoneInfo("America/New_York") now_et = datetime.now(et) weekday = now_et.weekday() hour = now_et.hour minute = now_et.minute is_market_hours = (weekday < 5) and (9 * 60 + 30 <= hour * 60 + minute < 16 * 60) status = "🟢 Market Open" if is_market_hours else "🔴 Market Closed" self._market_status_label.setText(status) except Exception: self._market_status_label.setText("") def closeEvent(self, event): if self._alert_worker: self._alert_worker.stop() self._alert_worker.wait(2000) super().closeEvent(event)