Files
MonitorSwitcher/ui/settings_window.py

343 lines
13 KiB
Python

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)