refactor: modularizar como PocketSync con soporte de perfiles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,220 @@
|
||||
import os
|
||||
import threading
|
||||
import tkinter as tk
|
||||
|
||||
from core.config import ConfigManager, Profile
|
||||
from core.robocopy_engine import RobocopySyncEngine
|
||||
from core.sync_engine import SyncEngine
|
||||
from ui import styles
|
||||
from ui.path_panel import PathPanel
|
||||
from ui.profile_bar import ProfileBar
|
||||
from ui.status_bar import StatusBar
|
||||
from ui.summary_panel import SummaryPanel
|
||||
from ui.system_list import SystemList
|
||||
|
||||
|
||||
class PocketSyncApp:
|
||||
def __init__(self, root: tk.Tk, config_manager: ConfigManager):
|
||||
self.root = root
|
||||
self.cm = config_manager
|
||||
self.engine: SyncEngine = RobocopySyncEngine()
|
||||
|
||||
self._build_ui()
|
||||
self._load_profile(self.cm.active_profile)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Construcción de la UI
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
self.root.title("PocketSync")
|
||||
self.root.geometry(f"{styles.WINDOW_WIDTH}x{styles.WINDOW_HEIGHT}")
|
||||
|
||||
# --- Barra de perfiles ---
|
||||
self.profile_bar = ProfileBar(self.root, on_change=self._on_profile_change)
|
||||
self.profile_bar.pack(fill="x", padx=styles.PAD_X, pady=(8, 2))
|
||||
self.profile_bar.set_callbacks(
|
||||
on_new=self._on_new_profile,
|
||||
on_rename=self._on_rename_profile,
|
||||
on_delete=self._on_delete_profile,
|
||||
)
|
||||
self._refresh_profile_bar()
|
||||
|
||||
# --- Rutas de origen ---
|
||||
self.src_panel = PathPanel(self.root, title="Rutas de origen")
|
||||
self.src_panel.pack(fill="x", padx=styles.PAD_X, pady=styles.PAD_Y)
|
||||
self.src_panel.set_roms_callback(self._on_roms_src_change)
|
||||
|
||||
# --- Lista de sistemas ---
|
||||
self.system_list = SystemList(self.root)
|
||||
self.system_list.pack(fill="both", expand=True, padx=styles.PAD_X)
|
||||
|
||||
# --- Rutas de destino ---
|
||||
self.dst_panel = PathPanel(self.root, title="Rutas de destino")
|
||||
self.dst_panel.pack(fill="x", padx=styles.PAD_X, pady=styles.PAD_Y)
|
||||
|
||||
# --- Botón Sync ---
|
||||
tk.Button(
|
||||
self.root,
|
||||
text="Sync Now",
|
||||
font=styles.FONT_BUTTON,
|
||||
command=self._run_sync,
|
||||
).pack(pady=6)
|
||||
|
||||
# --- Barra de estado ---
|
||||
self.status_bar = StatusBar(self.root)
|
||||
self.status_bar.pack(fill="x", padx=styles.PAD_X, pady=styles.PAD_Y)
|
||||
|
||||
# --- Panel de resumen ---
|
||||
self.summary = SummaryPanel(self.root)
|
||||
self.summary.pack(fill="x", padx=styles.PAD_X, pady=styles.PAD_Y)
|
||||
|
||||
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Gestión de perfiles
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _refresh_profile_bar(self) -> None:
|
||||
self.profile_bar.refresh(self.cm.profile_names(), self.cm.active_profile_name)
|
||||
|
||||
def _collect_ui_into_profile(self) -> None:
|
||||
"""Escribe el estado actual de la UI en el perfil activo."""
|
||||
p = self.cm.active_profile
|
||||
p.esde_src = self.src_panel.get_esde()
|
||||
p.roms_src = self.src_panel.get_roms()
|
||||
p.esde_dst = self.dst_panel.get_esde()
|
||||
p.roms_dst = self.dst_panel.get_roms()
|
||||
p.selected = self.system_list.get_selected()
|
||||
|
||||
def _load_profile(self, profile: Profile) -> None:
|
||||
self.src_panel.set_esde(profile.esde_src)
|
||||
self.src_panel.set_roms(profile.roms_src)
|
||||
self.dst_panel.set_esde(profile.esde_dst)
|
||||
self.dst_panel.set_roms(profile.roms_dst)
|
||||
|
||||
if os.path.isdir(profile.roms_src):
|
||||
self.system_list.populate(profile.roms_src)
|
||||
else:
|
||||
self.system_list.populate("")
|
||||
|
||||
self.system_list.set_selected(profile.selected)
|
||||
|
||||
def _on_profile_change(self, name: str) -> None:
|
||||
self._collect_ui_into_profile()
|
||||
self.cm.active_profile_name = name
|
||||
self._load_profile(self.cm.active_profile)
|
||||
self._refresh_profile_bar()
|
||||
|
||||
def _on_new_profile(self, name: str) -> bool:
|
||||
if self.cm.get_profile(name) is not None:
|
||||
return False
|
||||
self._collect_ui_into_profile()
|
||||
self.cm.add_profile(name)
|
||||
self.cm.active_profile_name = name
|
||||
self._load_profile(self.cm.active_profile)
|
||||
self._refresh_profile_bar()
|
||||
return True
|
||||
|
||||
def _on_rename_profile(self, old: str, new: str) -> bool:
|
||||
ok = self.cm.rename_profile(old, new)
|
||||
if ok:
|
||||
self._refresh_profile_bar()
|
||||
return ok
|
||||
|
||||
def _on_delete_profile(self, name: str) -> bool:
|
||||
ok = self.cm.delete_profile(name)
|
||||
if ok:
|
||||
self._load_profile(self.cm.active_profile)
|
||||
self._refresh_profile_bar()
|
||||
return ok
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Callbacks de widgets
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _on_roms_src_change(self, path: str) -> None:
|
||||
self.system_list.populate(path)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Loop de sincronización
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _run_sync(self) -> None:
|
||||
thread = threading.Thread(target=self._sync_thread)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def _sync_thread(self) -> None:
|
||||
selected = self.system_list.get_selected()
|
||||
|
||||
if not selected:
|
||||
self.summary.append("No hay sistemas seleccionados.")
|
||||
return
|
||||
|
||||
esde_src = self.src_panel.get_esde()
|
||||
roms_src = self.src_panel.get_roms()
|
||||
esde_dst = self.dst_panel.get_esde()
|
||||
roms_dst = self.dst_panel.get_roms()
|
||||
|
||||
if not all([esde_src, roms_src, esde_dst, roms_dst]):
|
||||
self.summary.append("ERROR: Debes configurar todas las rutas antes de continuar.")
|
||||
return
|
||||
|
||||
self.summary.clear()
|
||||
self.summary.append("=" * 60)
|
||||
self.summary.append("INICIANDO PROCESO DE COPIA")
|
||||
self.summary.append(f"Total de sistemas a procesar: {len(selected)}")
|
||||
self.summary.append("=" * 60)
|
||||
|
||||
total = len(selected)
|
||||
|
||||
for idx, system in enumerate(selected, 1):
|
||||
self.status_bar.set_system(f"Sistema: {idx}/{total} - {system.upper()}")
|
||||
self.summary.append(f"\nSISTEMA [{idx}/{total}]: {system.upper()}")
|
||||
|
||||
# Fase 1: ROMs
|
||||
self.status_bar.set_phase("Fase: [1/3] Copiando ROMs...")
|
||||
self.summary.append(" [1/3] Copiando ROMs...")
|
||||
self.engine.sync_folder(
|
||||
os.path.join(roms_src, system),
|
||||
os.path.join(roms_dst, system),
|
||||
on_file=self.status_bar.set_file,
|
||||
on_summary=self.summary.append,
|
||||
)
|
||||
|
||||
# Fase 2: gamelists
|
||||
self.status_bar.set_phase("Fase: [2/3] Copiando gamelists...")
|
||||
self.summary.append(" [2/3] Copiando gamelists...")
|
||||
self.engine.sync_folder(
|
||||
os.path.join(esde_src, "gamelists", system),
|
||||
os.path.join(esde_dst, "gamelists", system),
|
||||
on_file=self.status_bar.set_file,
|
||||
on_summary=self.summary.append,
|
||||
)
|
||||
|
||||
# Fase 3: media
|
||||
self.status_bar.set_phase("Fase: [3/3] Copiando media...")
|
||||
self.summary.append(" [3/3] Copiando media...")
|
||||
self.engine.sync_folder(
|
||||
os.path.join(esde_src, "downloaded_media", system),
|
||||
os.path.join(esde_dst, "downloaded_media", system),
|
||||
on_file=self.status_bar.set_file,
|
||||
on_summary=self.summary.append,
|
||||
)
|
||||
|
||||
self.summary.append(f" Sistema '{system}' completado\n")
|
||||
|
||||
self.status_bar.reset()
|
||||
self.summary.append("=" * 60)
|
||||
self.summary.append("PROCESO COMPLETADO EXITOSAMENTE")
|
||||
self.summary.append("=" * 60)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Cierre
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _on_close(self) -> None:
|
||||
self._collect_ui_into_profile()
|
||||
self.cm.save()
|
||||
self.root.destroy()
|
||||
@@ -0,0 +1,66 @@
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
from typing import Callable, Optional
|
||||
|
||||
from ui import styles
|
||||
|
||||
|
||||
class PathPanel(tk.LabelFrame):
|
||||
"""Panel con dos selectores de ruta (origen o destino)."""
|
||||
|
||||
def __init__(self, parent, title: str, **kwargs):
|
||||
super().__init__(
|
||||
parent,
|
||||
text=title,
|
||||
font=styles.FONT_HEADING,
|
||||
padx=styles.PAD_X,
|
||||
pady=styles.PAD_Y,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.path_esde = tk.StringVar()
|
||||
self.path_roms = tk.StringVar()
|
||||
|
||||
self._add_selector("ES-DE:", self.path_esde)
|
||||
self._add_selector("ROMs:", self.path_roms)
|
||||
|
||||
def _add_selector(self, label: str, var: tk.StringVar, callback: Optional[Callable] = None):
|
||||
frame = tk.Frame(self)
|
||||
frame.pack(fill="x", pady=2)
|
||||
|
||||
tk.Label(frame, text=label, width=10, anchor="w", font=styles.FONT_LABEL).pack(side="left")
|
||||
tk.Entry(frame, textvariable=var, width=55, font=styles.FONT_SMALL).pack(side="left", padx=4)
|
||||
tk.Button(
|
||||
frame,
|
||||
text="Buscar",
|
||||
font=styles.FONT_SMALL,
|
||||
command=lambda: self._choose(var, callback),
|
||||
).pack(side="left")
|
||||
|
||||
def _choose(self, var: tk.StringVar, callback: Optional[Callable]):
|
||||
path = filedialog.askdirectory()
|
||||
if not path:
|
||||
return
|
||||
var.set(path)
|
||||
if callback:
|
||||
callback(path)
|
||||
|
||||
def set_roms_callback(self, callback: Callable[[str], None]) -> None:
|
||||
"""Registra callback que se invoca al cambiar la ruta de ROMs."""
|
||||
# Reemplaza el selector de ROMs con uno que tenga el callback
|
||||
for widget in self.winfo_children():
|
||||
widget.destroy()
|
||||
self._add_selector("ES-DE:", self.path_esde)
|
||||
self._add_selector("ROMs:", self.path_roms, callback=callback)
|
||||
|
||||
def get_esde(self) -> str:
|
||||
return self.path_esde.get()
|
||||
|
||||
def get_roms(self) -> str:
|
||||
return self.path_roms.get()
|
||||
|
||||
def set_esde(self, value: str) -> None:
|
||||
self.path_esde.set(value)
|
||||
|
||||
def set_roms(self, value: str) -> None:
|
||||
self.path_roms.set(value)
|
||||
@@ -0,0 +1,93 @@
|
||||
import tkinter as tk
|
||||
from tkinter import simpledialog, messagebox
|
||||
from typing import Callable, List
|
||||
|
||||
from ui import styles
|
||||
|
||||
|
||||
class ProfileBar(tk.Frame):
|
||||
"""Barra superior con dropdown de perfiles y botones New/Rename/Delete."""
|
||||
|
||||
def __init__(self, parent, on_change: Callable[[str], None], **kwargs):
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
self._on_change = on_change
|
||||
|
||||
tk.Label(self, text="Perfil:", font=styles.FONT_LABEL).pack(side="left", padx=(0, 4))
|
||||
|
||||
self._var = tk.StringVar()
|
||||
self._dropdown = tk.OptionMenu(self, self._var, "")
|
||||
self._dropdown.config(font=styles.FONT_LABEL, width=20)
|
||||
self._dropdown.pack(side="left", padx=4)
|
||||
|
||||
tk.Button(self, text="New", font=styles.FONT_SMALL, command=self._new_profile).pack(side="left", padx=2)
|
||||
tk.Button(self, text="Rename", font=styles.FONT_SMALL, command=self._rename_profile).pack(side="left", padx=2)
|
||||
tk.Button(self, text="Delete", font=styles.FONT_SMALL, command=self._delete_profile).pack(side="left", padx=2)
|
||||
|
||||
# Callbacks inyectados desde app.py
|
||||
self._cb_new: Callable[[str], bool] = lambda name: False
|
||||
self._cb_rename: Callable[[str, str], bool] = lambda old, new: False
|
||||
self._cb_delete: Callable[[str], bool] = lambda name: False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# API pública
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def set_callbacks(
|
||||
self,
|
||||
on_new: Callable[[str], bool],
|
||||
on_rename: Callable[[str, str], bool],
|
||||
on_delete: Callable[[str], bool],
|
||||
) -> None:
|
||||
self._cb_new = on_new
|
||||
self._cb_rename = on_rename
|
||||
self._cb_delete = on_delete
|
||||
|
||||
def refresh(self, names: List[str], active: str) -> None:
|
||||
"""Actualiza el dropdown con la lista de nombres de perfil."""
|
||||
menu = self._dropdown["menu"]
|
||||
menu.delete(0, "end")
|
||||
for name in names:
|
||||
menu.add_command(label=name, command=lambda n=name: self._select(n))
|
||||
self._var.set(active)
|
||||
|
||||
def current(self) -> str:
|
||||
return self._var.get()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Acciones internas
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _select(self, name: str) -> None:
|
||||
self._var.set(name)
|
||||
self._on_change(name)
|
||||
|
||||
def _new_profile(self) -> None:
|
||||
name = simpledialog.askstring("Nuevo perfil", "Nombre del perfil:", parent=self)
|
||||
if not name:
|
||||
return
|
||||
name = name.strip()
|
||||
if not name:
|
||||
return
|
||||
if self._cb_new(name):
|
||||
self._on_change(name)
|
||||
else:
|
||||
messagebox.showerror("Error", f"Ya existe un perfil con el nombre '{name}'.")
|
||||
|
||||
def _rename_profile(self) -> None:
|
||||
old = self._var.get()
|
||||
new = simpledialog.askstring("Renombrar perfil", f"Nuevo nombre para '{old}':", parent=self)
|
||||
if not new:
|
||||
return
|
||||
new = new.strip()
|
||||
if not new:
|
||||
return
|
||||
if not self._cb_rename(old, new):
|
||||
messagebox.showerror("Error", f"No se pudo renombrar el perfil.")
|
||||
|
||||
def _delete_profile(self) -> None:
|
||||
name = self._var.get()
|
||||
if not messagebox.askyesno("Eliminar perfil", f"¿Eliminar el perfil '{name}'?"):
|
||||
return
|
||||
if not self._cb_delete(name):
|
||||
messagebox.showerror("Error", "No se puede eliminar el único perfil existente.")
|
||||
@@ -0,0 +1,61 @@
|
||||
import tkinter as tk
|
||||
|
||||
from ui import styles
|
||||
|
||||
_MAX_FILE_LEN = 80
|
||||
|
||||
|
||||
class StatusBar(tk.Frame):
|
||||
"""Barra de estado con tres labels: sistema, fase y archivo actual."""
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
super().__init__(parent, bg=styles.STATUS_BG, relief="sunken", bd=2, **kwargs)
|
||||
|
||||
self._label_system = tk.Label(
|
||||
self,
|
||||
text="Sistema: -",
|
||||
font=styles.FONT_LABEL + ("bold",) if isinstance(styles.FONT_LABEL, tuple) else styles.FONT_LABEL,
|
||||
bg=styles.STATUS_BG,
|
||||
fg=styles.STATUS_SYSTEM_FG,
|
||||
anchor="w",
|
||||
)
|
||||
self._label_system.pack(fill="x", padx=10, pady=3)
|
||||
|
||||
self._label_phase = tk.Label(
|
||||
self,
|
||||
text="Fase: -",
|
||||
font=styles.FONT_LABEL,
|
||||
bg=styles.STATUS_BG,
|
||||
fg=styles.STATUS_PHASE_FG,
|
||||
anchor="w",
|
||||
)
|
||||
self._label_phase.pack(fill="x", padx=10, pady=3)
|
||||
|
||||
self._label_file = tk.Label(
|
||||
self,
|
||||
text="Archivo: -",
|
||||
font=styles.FONT_SMALL,
|
||||
bg=styles.STATUS_BG,
|
||||
fg=styles.STATUS_FILE_FG,
|
||||
anchor="w",
|
||||
)
|
||||
self._label_file.pack(fill="x", padx=10, pady=3)
|
||||
|
||||
def set_system(self, text: str) -> None:
|
||||
self._label_system.config(text=text)
|
||||
self._label_system.update_idletasks()
|
||||
|
||||
def set_phase(self, text: str) -> None:
|
||||
self._label_phase.config(text=text)
|
||||
self._label_phase.update_idletasks()
|
||||
|
||||
def set_file(self, text: str) -> None:
|
||||
if len(text) > _MAX_FILE_LEN:
|
||||
text = "..." + text[-(_MAX_FILE_LEN - 3):]
|
||||
self._label_file.config(text=f"Archivo: {text}")
|
||||
self._label_file.update_idletasks()
|
||||
|
||||
def reset(self) -> None:
|
||||
self.set_system("Sistema: ✅ COMPLETADO")
|
||||
self.set_phase("Fase: -")
|
||||
self.set_file("-")
|
||||
@@ -0,0 +1,27 @@
|
||||
# Constantes visuales de PocketSync
|
||||
|
||||
FONT_FAMILY = "Segoe UI"
|
||||
|
||||
FONT_HEADING = (FONT_FAMILY, 11, "bold")
|
||||
FONT_LABEL = (FONT_FAMILY, 10)
|
||||
FONT_SMALL = (FONT_FAMILY, 9)
|
||||
FONT_BUTTON = (FONT_FAMILY, 11, "bold")
|
||||
FONT_MONO = ("Consolas", 9)
|
||||
|
||||
# Colores de la barra de estado
|
||||
STATUS_BG = "#2a2a2a"
|
||||
STATUS_SYSTEM_FG = "#00ff00"
|
||||
STATUS_PHASE_FG = "#00aaff"
|
||||
STATUS_FILE_FG = "#ffaa00"
|
||||
|
||||
# Colores del panel de resumen
|
||||
SUMMARY_BG = "#f0f0f0"
|
||||
SUMMARY_FG = "#000000"
|
||||
|
||||
# Dimensiones de ventana
|
||||
WINDOW_WIDTH = 800
|
||||
WINDOW_HEIGHT = 720
|
||||
|
||||
# Padding genérico
|
||||
PAD_X = 10
|
||||
PAD_Y = 5
|
||||
@@ -0,0 +1,33 @@
|
||||
import tkinter as tk
|
||||
|
||||
from ui import styles
|
||||
|
||||
|
||||
class SummaryPanel(tk.Frame):
|
||||
"""Panel de texto deshabilitado para mostrar el resumen de sync."""
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
tk.Label(self, text="Resumen:", font=styles.FONT_LABEL).pack(pady=(6, 2))
|
||||
|
||||
self._text = tk.Text(
|
||||
self,
|
||||
height=6,
|
||||
state="disabled",
|
||||
bg=styles.SUMMARY_BG,
|
||||
fg=styles.SUMMARY_FG,
|
||||
font=styles.FONT_MONO,
|
||||
)
|
||||
self._text.pack(fill="both", expand=False, padx=styles.PAD_X, pady=styles.PAD_Y)
|
||||
|
||||
def append(self, line: str) -> None:
|
||||
self._text.configure(state="normal")
|
||||
self._text.insert(tk.END, line + "\n")
|
||||
self._text.see(tk.END)
|
||||
self._text.configure(state="disabled")
|
||||
|
||||
def clear(self) -> None:
|
||||
self._text.configure(state="normal")
|
||||
self._text.delete(1.0, tk.END)
|
||||
self._text.configure(state="disabled")
|
||||
@@ -0,0 +1,69 @@
|
||||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
from typing import List
|
||||
|
||||
from ui import styles
|
||||
|
||||
|
||||
class SystemList(tk.Frame):
|
||||
"""Listbox de sistemas con botones Select All / Select None."""
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
tk.Label(
|
||||
self,
|
||||
text="Sistemas encontrados en ROMs:",
|
||||
font=styles.FONT_LABEL,
|
||||
).pack(pady=(6, 2))
|
||||
|
||||
self.listbox = tk.Listbox(self, selectmode=tk.MULTIPLE, height=10, font=styles.FONT_SMALL)
|
||||
self.listbox.pack(fill="both", expand=True, padx=styles.PAD_X)
|
||||
|
||||
btn_frame = tk.Frame(self)
|
||||
btn_frame.pack(fill="x", padx=styles.PAD_X, pady=2)
|
||||
|
||||
tk.Button(
|
||||
btn_frame,
|
||||
text="Select All",
|
||||
font=styles.FONT_SMALL,
|
||||
command=self._select_all,
|
||||
).pack(side="left", padx=2)
|
||||
|
||||
tk.Button(
|
||||
btn_frame,
|
||||
text="Select None",
|
||||
font=styles.FONT_SMALL,
|
||||
command=self._select_none,
|
||||
).pack(side="left", padx=2)
|
||||
|
||||
def _select_all(self):
|
||||
self.listbox.selection_set(0, tk.END)
|
||||
|
||||
def _select_none(self):
|
||||
self.listbox.selection_clear(0, tk.END)
|
||||
|
||||
def populate(self, path: str) -> None:
|
||||
"""Rellena el listbox con los subdirectorios de path."""
|
||||
self.listbox.delete(0, tk.END)
|
||||
if not os.path.isdir(path):
|
||||
return
|
||||
try:
|
||||
dirs = sorted(
|
||||
d for d in os.listdir(path) if os.path.isdir(os.path.join(path, d))
|
||||
)
|
||||
for d in dirs:
|
||||
self.listbox.insert(tk.END, d)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"No se pudo leer la ruta:\n{e}")
|
||||
|
||||
def get_selected(self) -> List[str]:
|
||||
return [self.listbox.get(i) for i in self.listbox.curselection()]
|
||||
|
||||
def set_selected(self, names: List[str]) -> None:
|
||||
selected = set(names)
|
||||
self.listbox.selection_clear(0, tk.END)
|
||||
for i in range(self.listbox.size()):
|
||||
if self.listbox.get(i) in selected:
|
||||
self.listbox.selection_set(i)
|
||||
Reference in New Issue
Block a user