import copy import logging import sys import winreg from PyQt6.QtWidgets import ( QMainWindow, QWidget, QTabWidget, QVBoxLayout, QHBoxLayout, QPushButton, QListWidget, QListWidgetItem, QLabel, QLineEdit, QFormLayout, QSpinBox, QCheckBox, QComboBox, QGroupBox, ) from PyQt6.QtCore import Qt logger = logging.getLogger(__name__) VCP_INPUT_OPTIONS = [ (15, "DisplayPort-1 (0x0F)"), (16, "DisplayPort-2 (0x10)"), (17, "HDMI-1 (0x11)"), (18, "HDMI-2 (0x12)"), (1, "VGA-1 (0x01)"), (3, "DVI-1 (0x03)"), ] class SettingsWindow(QMainWindow): def __init__(self, config_manager, switcher, hotkey_manager): super().__init__() self._config_manager = config_manager self._switcher = switcher self._hotkey_manager = hotkey_manager self._working_config = copy.deepcopy(config_manager.get()) self.setWindowTitle("MonitorSwitcher \u2014 Ustawienia") self.setMinimumSize(640, 520) tabs = QTabWidget() tabs.addTab(self._build_profiles_tab(), "Profile") tabs.addTab(self._build_devices_tab(), "Urzadzenia BT") tabs.addTab(self._build_hotkeys_tab(), "Hotkeys") tabs.addTab(self._build_general_tab(), "Ogolne") save_btn = QPushButton("Zapisz i zamknij") save_btn.clicked.connect(self._save_and_close) cancel_btn = QPushButton("Anuluj") cancel_btn.clicked.connect(self.close) btn_row = QHBoxLayout() btn_row.addStretch() btn_row.addWidget(cancel_btn) btn_row.addWidget(save_btn) layout = QVBoxLayout() layout.addWidget(tabs) layout.addLayout(btn_row) central = QWidget() central.setLayout(layout) self.setCentralWidget(central) # ── Profiles tab ────────────────────────────────────────────────────────── def _build_profiles_tab(self) -> QWidget: widget = QWidget() layout = QHBoxLayout(widget) left = QVBoxLayout() self._profile_list = QListWidget() self._profile_list.currentRowChanged.connect(self._on_profile_selected) left.addWidget(QLabel("Profile:")) left.addWidget(self._profile_list) add_btn = QPushButton("+ Dodaj") add_btn.clicked.connect(self._add_profile) del_btn = QPushButton("Usun") del_btn.clicked.connect(self._delete_profile) btn_row = QHBoxLayout() btn_row.addWidget(add_btn) btn_row.addWidget(del_btn) left.addLayout(btn_row) right = QVBoxLayout() self._profile_name_edit = QLineEdit() self._profile_name_edit.setPlaceholderText("Nazwa profilu") self._profile_name_edit.textChanged.connect(self._on_profile_name_changed) right.addWidget(QLabel("Nazwa:")) right.addWidget(self._profile_name_edit) self._monitor_combos: list[QComboBox] = [] monitor_group = QGroupBox("Wejscia monitorow") monitor_layout = QFormLayout() for i in range(2): combo = QComboBox() for vcp, label in VCP_INPUT_OPTIONS: combo.addItem(label, vcp) combo.currentIndexChanged.connect( lambda _, idx=i: self._on_monitor_input_changed(idx) ) self._monitor_combos.append(combo) monitor_layout.addRow(f"Monitor {i}:", combo) monitor_group.setLayout(monitor_layout) right.addWidget(monitor_group) right.addStretch() layout.addLayout(left, 1) layout.addLayout(right, 2) self._refresh_profile_list() return widget def _refresh_profile_list(self) -> None: self._profile_list.clear() for p in self._working_config.get("profiles", []): self._profile_list.addItem(QListWidgetItem(p["name"])) if self._profile_list.count() > 0: self._profile_list.setCurrentRow(0) def _on_profile_selected(self, row: int) -> None: profiles = self._working_config.get("profiles", []) if row < 0 or row >= len(profiles): return profile = profiles[row] self._profile_name_edit.blockSignals(True) self._profile_name_edit.setText(profile["name"]) self._profile_name_edit.blockSignals(False) for combo in self._monitor_combos: combo.blockSignals(True) for inp in profile.get("monitor_inputs", []): idx = inp["monitor_index"] vcp = inp["vcp_value"] if idx < len(self._monitor_combos): data_idx = self._monitor_combos[idx].findData(vcp) self._monitor_combos[idx].setCurrentIndex( data_idx if data_idx >= 0 else 0 ) for combo in self._monitor_combos: combo.blockSignals(False) def _on_profile_name_changed(self, text: str) -> None: row = self._profile_list.currentRow() profiles = self._working_config.get("profiles", []) if 0 <= row < len(profiles): profiles[row]["name"] = text self._profile_list.item(row).setText(text) def _on_monitor_input_changed(self, monitor_idx: int) -> None: row = self._profile_list.currentRow() profiles = self._working_config.get("profiles", []) if row < 0 or row >= len(profiles): return vcp = self._monitor_combos[monitor_idx].currentData() inputs = profiles[row].setdefault("monitor_inputs", []) for inp in inputs: if inp["monitor_index"] == monitor_idx: inp["vcp_value"] = vcp return inputs.append({"monitor_index": monitor_idx, "vcp_value": vcp}) def _add_profile(self) -> None: new_id = f"profile_{len(self._working_config['profiles'])}" new_profile = { "id": new_id, "name": "Nowy profil", "hotkey": "", "monitor_inputs": [ {"monitor_index": 0, "vcp_value": 15}, {"monitor_index": 1, "vcp_value": 15}, ], } self._working_config["profiles"].append(new_profile) self._profile_list.addItem(QListWidgetItem(new_profile["name"])) self._profile_list.setCurrentRow(self._profile_list.count() - 1) def _delete_profile(self) -> None: row = self._profile_list.currentRow() if row < 0: return self._working_config["profiles"].pop(row) self._refresh_profile_list() # ── Devices tab ─────────────────────────────────────────────────────────── def _build_devices_tab(self) -> QWidget: widget = QWidget() outer = QVBoxLayout(widget) outer.addWidget(QLabel("Urzadzenia BT wyzwalajace przelaczeanie profilu:")) self._devices_container = QVBoxLayout() outer.addLayout(self._devices_container) add_btn = QPushButton("+ Dodaj urzadzenie") add_btn.clicked.connect(lambda: self._add_device_row()) outer.addWidget(add_btn) outer.addStretch() for trigger in self._working_config.get("device_triggers", []): self._add_device_row(trigger) return widget def _add_device_row(self, trigger: dict | None = None) -> None: if trigger is None: trigger = { "name": "Nowe urzadzenie", "bt_device_id": "", "on_connect": self._first_profile_id(), "on_disconnect": self._first_profile_id(), "enabled": True, } self._working_config.setdefault("device_triggers", []).append(trigger) group = QGroupBox(trigger.get("name", "")) form = QFormLayout(group) name_edit = QLineEdit(trigger.get("name", "")) bt_id_edit = QLineEdit(trigger.get("bt_device_id", "")) connect_combo = self._profile_combo(trigger.get("on_connect", "")) disconnect_combo = self._profile_combo(trigger.get("on_disconnect", "")) enabled_cb = QCheckBox() enabled_cb.setChecked(trigger.get("enabled", True)) form.addRow("Nazwa:", name_edit) form.addRow("BT Device ID (fragment):", bt_id_edit) form.addRow("Przy podlaczeniu:", connect_combo) form.addRow("Przy odlaczeniu:", disconnect_combo) form.addRow("Aktywne:", enabled_cb) def update_trigger(): trigger["name"] = name_edit.text() trigger["bt_device_id"] = bt_id_edit.text() trigger["on_connect"] = connect_combo.currentData() trigger["on_disconnect"] = disconnect_combo.currentData() trigger["enabled"] = enabled_cb.isChecked() group.setTitle(trigger["name"]) name_edit.textChanged.connect(lambda _: update_trigger()) bt_id_edit.textChanged.connect(lambda _: update_trigger()) connect_combo.currentIndexChanged.connect(lambda _: update_trigger()) disconnect_combo.currentIndexChanged.connect(lambda _: update_trigger()) enabled_cb.stateChanged.connect(lambda _: update_trigger()) self._devices_container.addWidget(group) def _profile_combo(self, selected_id: str) -> QComboBox: combo = QComboBox() for p in self._working_config.get("profiles", []): combo.addItem(p["name"], p["id"]) idx = combo.findData(selected_id) if idx >= 0: combo.setCurrentIndex(idx) return combo def _first_profile_id(self) -> str: profiles = self._working_config.get("profiles", []) return profiles[0]["id"] if profiles else "" # ── Hotkeys tab ─────────────────────────────────────────────────────────── def _build_hotkeys_tab(self) -> QWidget: widget = QWidget() layout = QVBoxLayout(widget) layout.addWidget(QLabel("Przypisz skroty klawiszowe do profili:")) layout.addWidget(QLabel("Format: ctrl+alt+1, win+F1, itp.")) form = QFormLayout() for profile in self._working_config.get("profiles", []): edit = QLineEdit(profile.get("hotkey", "")) edit.setPlaceholderText("np. ctrl+alt+1") pid = profile["id"] edit.textChanged.connect( lambda text, p=pid: self._on_hotkey_changed(p, text) ) form.addRow(f"{profile['name']}:", edit) layout.addLayout(form) layout.addStretch() return widget def _on_hotkey_changed(self, profile_id: str, text: str) -> None: for p in self._working_config.get("profiles", []): if p["id"] == profile_id: p["hotkey"] = text.strip() return # ── General tab ─────────────────────────────────────────────────────────── def _build_general_tab(self) -> QWidget: widget = QWidget() layout = QFormLayout(widget) self._autostart_cb = QCheckBox() self._autostart_cb.setChecked( self._working_config.get("general", {}).get("autostart", True) ) self._minimize_cb = QCheckBox() self._minimize_cb.setChecked( self._working_config.get("general", {}).get("minimize_to_tray", True) ) self._debounce_spin = QSpinBox() self._debounce_spin.setRange(100, 5000) self._debounce_spin.setSuffix(" ms") self._debounce_spin.setValue( self._working_config.get("general", {}).get("debounce_ms", 500) ) layout.addRow("Autostart z Windows:", self._autostart_cb) layout.addRow("Minimalizuj do tray przy zamknieciu:", self._minimize_cb) layout.addRow("Debounce (opoznienie przelaczenia):", self._debounce_spin) return widget # ── Save ────────────────────────────────────────────────────────────────── def _save_and_close(self) -> None: general = self._working_config.setdefault("general", {}) general["autostart"] = self._autostart_cb.isChecked() general["minimize_to_tray"] = self._minimize_cb.isChecked() general["debounce_ms"] = self._debounce_spin.value() self._config_manager.update(self._working_config) self._hotkey_manager.unregister_all() self._hotkey_manager.register_all() _set_autostart(general.get("autostart", False)) self.close() def _set_autostart(enable: bool) -> None: key_path = r"Software\Microsoft\Windows\CurrentVersion\Run" app_name = "MonitorSwitcher" exe_path = sys.executable if not getattr(sys, "frozen", False) else sys.executable try: with winreg.OpenKey( winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_SET_VALUE ) as key: if enable: winreg.SetValueEx(key, app_name, 0, winreg.REG_SZ, f'"{exe_path}"') else: try: winreg.DeleteValue(key, app_name) except FileNotFoundError: pass except Exception as exc: logger.warning("Autostart registry error: %s", exc)