Files
2026-05-26 11:04:27 -04:00

217 lines
9.1 KiB
Python

from __future__ import annotations
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton,
QTabWidget, QWidget, QFormLayout, QGroupBox, QMessageBox, QCheckBox
)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont
class SettingsDialog(QDialog):
def __init__(self, config, parent=None):
super().__init__(parent)
self._config = config
self.setWindowTitle("Settings")
self.setMinimumWidth(480)
self.setMinimumHeight(440)
self._setup_ui()
self._load_values()
def _setup_ui(self):
root = QVBoxLayout(self)
root.setContentsMargins(16, 16, 16, 16)
root.setSpacing(12)
tabs = QTabWidget()
tabs.addTab(self._build_api_tab(), "API Keys")
tabs.addTab(self._build_brokers_tab(), "Brokers")
tabs.addTab(self._build_general_tab(), "General")
root.addWidget(tabs)
btn_row = QHBoxLayout()
btn_row.addStretch()
save_btn = QPushButton("Save")
save_btn.setObjectName("primary_btn")
save_btn.clicked.connect(self._save)
cancel_btn = QPushButton("Cancel")
cancel_btn.clicked.connect(self.reject)
btn_row.addWidget(cancel_btn)
btn_row.addWidget(save_btn)
root.addLayout(btn_row)
def _build_api_tab(self) -> QWidget:
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(12)
# Anthropic
anthropic_box = QGroupBox("Anthropic (Claude AI)")
anthropic_form = QFormLayout(anthropic_box)
self._anthropic_key = QLineEdit()
self._anthropic_key.setEchoMode(QLineEdit.EchoMode.Password)
self._anthropic_key.setPlaceholderText("sk-ant-…")
test_anthropic = QPushButton("Test")
test_anthropic.setMaximumWidth(60)
test_anthropic.clicked.connect(self._test_anthropic)
row = QHBoxLayout()
row.addWidget(self._anthropic_key)
row.addWidget(test_anthropic)
anthropic_form.addRow("API Key:", row)
anthropic_form.addRow("", QLabel("Get key: console.anthropic.com | Pay per token"))
layout.addWidget(anthropic_box)
# NewsAPI
news_box = QGroupBox("NewsAPI")
news_form = QFormLayout(news_box)
self._news_key = QLineEdit()
self._news_key.setEchoMode(QLineEdit.EchoMode.Password)
self._news_key.setPlaceholderText("Your NewsAPI key…")
test_news = QPushButton("Test")
test_news.setMaximumWidth(60)
test_news.clicked.connect(self._test_news)
row2 = QHBoxLayout()
row2.addWidget(self._news_key)
row2.addWidget(test_news)
news_form.addRow("API Key:", row2)
news_form.addRow("", QLabel("Get key: newsapi.org | 100 req/day free"))
layout.addWidget(news_box)
layout.addStretch()
return tab
def _build_brokers_tab(self) -> QWidget:
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(12)
# Schwab
schwab_box = QGroupBox("Charles Schwab")
schwab_form = QFormLayout(schwab_box)
self._schwab_key = QLineEdit()
self._schwab_key.setPlaceholderText("App Key…")
self._schwab_secret = QLineEdit()
self._schwab_secret.setEchoMode(QLineEdit.EchoMode.Password)
self._schwab_secret.setPlaceholderText("App Secret…")
test_schwab = QPushButton("Test Connection")
test_schwab.clicked.connect(self._test_schwab)
schwab_form.addRow("App Key:", self._schwab_key)
schwab_form.addRow("App Secret:", self._schwab_secret)
schwab_form.addRow("", test_schwab)
schwab_form.addRow("", QLabel("Register at: developer.schwab.com\nCallback URL: https://127.0.0.1"))
layout.addWidget(schwab_box)
# Robinhood
rh_box = QGroupBox("Robinhood")
rh_form = QFormLayout(rh_box)
self._rh_username = QLineEdit()
self._rh_username.setPlaceholderText("Email…")
self._rh_password = QLineEdit()
self._rh_password.setEchoMode(QLineEdit.EchoMode.Password)
self._rh_password.setPlaceholderText("Password…")
test_rh = QPushButton("Test Login")
test_rh.clicked.connect(self._test_robinhood)
rh_form.addRow("Username:", self._rh_username)
rh_form.addRow("Password:", self._rh_password)
rh_form.addRow("", test_rh)
layout.addWidget(rh_box)
layout.addStretch()
return tab
def _build_general_tab(self) -> QWidget:
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(12)
general_box = QGroupBox("Appearance & Behavior")
general_form = QFormLayout(general_box)
self._theme_dark = QCheckBox("Dark theme")
self._theme_dark.setChecked(True)
general_form.addRow("Theme:", self._theme_dark)
layout.addWidget(general_box)
layout.addStretch()
return tab
def _load_values(self):
self._anthropic_key.setText(self._config.get_setting("anthropic_api_key") or self._config.get("ANTHROPIC_API_KEY"))
self._news_key.setText(self._config.get_setting("news_api_key") or self._config.get("NEWS_API_KEY"))
self._schwab_key.setText(self._config.get_setting("schwab_app_key") or self._config.get("SCHWAB_APP_KEY"))
self._schwab_secret.setText(self._config.get_setting("schwab_app_secret") or self._config.get("SCHWAB_APP_SECRET"))
self._rh_username.setText(self._config.get_setting("robinhood_username") or self._config.get("ROBINHOOD_USERNAME"))
self._rh_password.setText(self._config.get_setting("robinhood_password") or self._config.get("ROBINHOOD_PASSWORD"))
theme = self._config.get_setting("theme", "dark")
self._theme_dark.setChecked(theme == "dark")
def _save(self):
self._config.set_setting("anthropic_api_key", self._anthropic_key.text().strip())
self._config.set_setting("news_api_key", self._news_key.text().strip())
self._config.set_setting("schwab_app_key", self._schwab_key.text().strip())
self._config.set_setting("schwab_app_secret", self._schwab_secret.text().strip())
self._config.set_setting("robinhood_username", self._rh_username.text().strip())
self._config.set_setting("robinhood_password", self._rh_password.text().strip())
self._config.set_setting("theme", "dark" if self._theme_dark.isChecked() else "light")
QMessageBox.information(self, "Saved", "Settings saved. Restart to apply theme changes.")
self.accept()
def _test_anthropic(self):
key = self._anthropic_key.text().strip()
if not key:
QMessageBox.warning(self, "Test Failed", "Enter an API key first.")
return
try:
import anthropic
client = anthropic.Anthropic(api_key=key)
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=10,
messages=[{"role": "user", "content": "ping"}],
)
QMessageBox.information(self, "Success", "Anthropic API key is valid!")
except Exception as e:
QMessageBox.critical(self, "Test Failed", str(e))
def _test_news(self):
key = self._news_key.text().strip()
if not key:
QMessageBox.warning(self, "Test Failed", "Enter an API key first.")
return
try:
from newsapi import NewsApiClient
client = NewsApiClient(api_key=key)
result = client.get_top_headlines(language="en", page_size=1)
if result.get("status") == "ok":
QMessageBox.information(self, "Success", "NewsAPI key is valid!")
else:
QMessageBox.critical(self, "Test Failed", result.get("message", "Unknown error"))
except Exception as e:
QMessageBox.critical(self, "Test Failed", str(e))
def _test_schwab(self):
key = self._schwab_key.text().strip()
secret = self._schwab_secret.text().strip()
if not key or not secret:
QMessageBox.warning(self, "Test Failed", "Enter both App Key and Secret first.")
return
try:
from brokers.schwab import SchwabBroker
broker = SchwabBroker(key, secret)
broker.connect()
QMessageBox.information(self, "Success", "Schwab connected! Browser may open for OAuth if first time.")
except Exception as e:
QMessageBox.critical(self, "Test Failed", str(e))
def _test_robinhood(self):
user = self._rh_username.text().strip()
pwd = self._rh_password.text().strip()
if not user or not pwd:
QMessageBox.warning(self, "Test Failed", "Enter username and password first.")
return
try:
from brokers.robinhood import RobinhoodBroker
broker = RobinhoodBroker(user, pwd)
broker.connect()
QMessageBox.information(self, "Success", "Robinhood login successful!")
except Exception as e:
QMessageBox.critical(self, "Test Failed", str(e))