Files
sedona_editor/connect_dialog.py
T
ArdaandClaude Opus 4.6 b4a504e57d Fix connect dialog: button order and cancel-during-connection cleanup
Swap Connect/Cancel button order so Connect is the default (leftmost).
Guard background thread against cancelled state so a cancelled connect
does not leave the app in live mode. Cancel during connection now
disconnects the session cleanly in a cleanup thread.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-12 09:41:21 +03:00

188 lines
6.7 KiB
Python

"""
Connection dialog for connecting to a live Sedona device.
Modal Toplevel with host, port, username, password fields.
Connect runs in a background thread; progress and errors shown inline.
"""
import configparser
import threading
import tkinter as tk
from tkinter import ttk
import app_state
class ConnectDialog:
"""Modal dialog for entering device connection parameters."""
def __init__(self, parent, on_connect_callback):
self._callback = on_connect_callback
self._thread = None
self._cancelled = False
self._dlg = tk.Toplevel(parent)
self._dlg.title("Connect to Device")
self._dlg.resizable(False, False)
self._dlg.transient(parent)
self._dlg.grab_set()
host, port, user = _load_last_connection()
frame = ttk.Frame(self._dlg, padding=16)
frame.pack(fill=tk.BOTH, expand=True)
row = 0
ttk.Label(frame, text="Host:").grid(row=row, column=0, sticky="e", padx=(0, 8), pady=4)
self._host_var = tk.StringVar(value=host)
self._host_entry = ttk.Entry(frame, textvariable=self._host_var, width=28)
self._host_entry.grid(row=row, column=1, columnspan=2, sticky="ew", pady=4)
row += 1
ttk.Label(frame, text="Port:").grid(row=row, column=0, sticky="e", padx=(0, 8), pady=4)
self._port_var = tk.StringVar(value=port)
ttk.Entry(frame, textvariable=self._port_var, width=8).grid(
row=row, column=1, sticky="w", pady=4)
row += 1
ttk.Label(frame, text="Username:").grid(row=row, column=0, sticky="e", padx=(0, 8), pady=4)
self._user_var = tk.StringVar(value=user)
ttk.Entry(frame, textvariable=self._user_var, width=28).grid(
row=row, column=1, columnspan=2, sticky="ew", pady=4)
row += 1
ttk.Label(frame, text="Password:").grid(row=row, column=0, sticky="e", padx=(0, 8), pady=4)
self._pass_var = tk.StringVar()
ttk.Entry(frame, textvariable=self._pass_var, width=28, show="*").grid(
row=row, column=1, columnspan=2, sticky="ew", pady=4)
row += 1
self._status_label = ttk.Label(frame, text="", foreground="#666666")
self._status_label.grid(row=row, column=0, columnspan=3, sticky="w", pady=(8, 4))
row += 1
btn_frame = ttk.Frame(frame)
btn_frame.grid(row=row, column=0, columnspan=3, sticky="e", pady=(8, 0))
self._btn_cancel = ttk.Button(btn_frame, text="Cancel", command=self._do_cancel)
self._btn_cancel.pack(side=tk.RIGHT)
self._btn_connect = ttk.Button(btn_frame, text="Connect", command=self._do_connect)
self._btn_connect.pack(side=tk.RIGHT, padx=(0, 8))
self._host_entry.focus_set()
self._dlg.bind("<Return>", lambda e: self._do_connect())
self._dlg.bind("<Escape>", lambda e: self._do_cancel())
self._dlg.update_idletasks()
x = parent.winfo_x() + (parent.winfo_width() - self._dlg.winfo_width()) // 2
y = parent.winfo_y() + (parent.winfo_height() - self._dlg.winfo_height()) // 2
self._dlg.geometry(f"+{x}+{y}")
def _do_connect(self):
host = self._host_var.get().strip()
port = self._port_var.get().strip()
user = self._user_var.get().strip()
password = self._pass_var.get()
if not host:
self._status_label.config(text="Host is required", foreground="#c00000")
return
try:
port_int = int(port)
except ValueError:
self._status_label.config(text="Invalid port number", foreground="#c00000")
return
self._btn_connect.config(state="disabled")
self._status_label.config(text="Connecting...", foreground="#666666")
self._cancelled = False
self._thread = threading.Thread(
target=self._connect_worker,
args=(host, port_int, user, password),
daemon=True
)
self._thread.start()
self._poll_connect()
def _connect_worker(self, host, port, user, password):
try:
result = self._callback(host, port, user, password)
if self._cancelled:
return
self._result = ("ok", result, host, port, user)
except Exception as e:
if self._cancelled:
return
self._result = ("error", str(e), host, port, user)
def _poll_connect(self):
if self._cancelled:
return
if self._thread is not None and self._thread.is_alive():
self._dlg.after(100, self._poll_connect)
return
status, *rest = self._result
if status == "ok":
_, host, port, user = rest
_save_last_connection(host, str(port), user)
self._dlg.destroy()
else:
msg = rest[0]
self._status_label.config(text=f"Error: {msg}", foreground="#c00000")
self._btn_connect.config(state="normal")
def _do_cancel(self):
self._cancelled = True
if self._thread is not None and self._thread.is_alive():
# Connection attempt in progress — clean up after it finishes
import app_state as _as
def _cleanup():
self._thread.join(timeout=5)
if _as.device_session is not None and _as.live_mode:
try:
_as.device_session.disconnect()
except Exception:
pass
_as.live_mode = False
_as.device_session = None
_as.connection_status = "offline"
threading.Thread(target=_cleanup, daemon=True).start()
self._dlg.destroy()
def _load_last_connection():
"""Reads last connection params from editor.properties."""
config = configparser.ConfigParser()
host = "192.168.1.100"
port = "1876"
user = "admin"
try:
if app_state.PROPERTIES_FILE:
config.read(app_state.PROPERTIES_FILE)
host = config.get("Device", "host", fallback=host)
port = config.get("Device", "port", fallback=port)
user = config.get("Device", "user", fallback=user)
except Exception:
pass
return host, port, user
def _save_last_connection(host, port, user):
"""Saves connection params to editor.properties."""
config = configparser.ConfigParser()
try:
config.read(app_state.PROPERTIES_FILE)
if "Device" not in config:
config["Device"] = {}
config["Device"]["host"] = host
config["Device"]["port"] = port
config["Device"]["user"] = user
with open(app_state.PROPERTIES_FILE, "w") as f:
config.write(f)
except Exception:
pass