Files
StockMind/stockmind/ui/main_window.py
T
2026-05-26 11:04:27 -04:00

237 lines
8.1 KiB
Python

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)