host.controller_config.import_dialog
Modal dialog for resolving action name conflicts during import.
Shows each conflicting action side-by-side (existing vs imported) and lets the user choose: keep existing, replace with imported, or skip.
1"""Modal dialog for resolving action name conflicts during import. 2 3Shows each conflicting action side-by-side (existing vs imported) 4and lets the user choose: keep existing, replace with imported, or skip. 5""" 6 7import tkinter as tk 8from tkinter import ttk 9 10import sys 11import os 12sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) 13 14from utils.controller.model import ActionDefinition 15 16 17class ImportConflictDialog(tk.Toplevel): 18 """Dialog for resolving action conflicts during import.""" 19 20 def __init__(self, parent, conflicts: set[str], 21 current: dict[str, ActionDefinition], 22 imported: dict[str, ActionDefinition]): 23 super().__init__(parent) 24 self.transient(parent) 25 self.grab_set() 26 self.title("Resolve Import Conflicts") 27 28 self._result: dict[str, ActionDefinition] | None = None 29 self._conflicts = sorted(conflicts) 30 self._current = current 31 self._imported = imported 32 33 self._build_ui() 34 35 # Center on parent 36 self.update_idletasks() 37 pw, ph = parent.winfo_width(), parent.winfo_height() 38 px, py = parent.winfo_rootx(), parent.winfo_rooty() 39 w, h = self.winfo_width(), self.winfo_height() 40 self.geometry(f"+{px + (pw - w) // 2}+{py + (ph - h) // 2}") 41 42 self.protocol("WM_DELETE_WINDOW", self._on_cancel) 43 44 def _build_ui(self): 45 self.minsize(500, 400) 46 47 main = ttk.Frame(self, padding=10) 48 main.pack(fill=tk.BOTH, expand=True) 49 50 ttk.Label( 51 main, 52 text=f"{len(self._conflicts)} action(s) already exist:", 53 font=("TkDefaultFont", 10, "bold"), 54 ).pack(anchor=tk.W, pady=(0, 5)) 55 56 # Scrollable frame 57 canvas = tk.Canvas(main, highlightthickness=0) 58 scrollbar = ttk.Scrollbar(main, orient=tk.VERTICAL, 59 command=canvas.yview) 60 scroll_frame = ttk.Frame(canvas) 61 62 scroll_frame.bind( 63 "<Configure>", 64 lambda e: canvas.configure(scrollregion=canvas.bbox("all")), 65 ) 66 canvas.create_window((0, 0), window=scroll_frame, anchor=tk.NW) 67 canvas.configure(yscrollcommand=scrollbar.set) 68 69 canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 70 scrollbar.pack(side=tk.RIGHT, fill=tk.Y) 71 72 self._radio_vars: dict[str, tk.StringVar] = {} 73 for qname in self._conflicts: 74 frame = ttk.LabelFrame(scroll_frame, text=qname, padding=5) 75 frame.pack(fill=tk.X, padx=5, pady=3) 76 77 cur = self._current.get(qname) 78 imp = self._imported.get(qname) 79 80 cur_desc = cur.description if cur else "(none)" 81 imp_desc = imp.description if imp else "(none)" 82 ttk.Label(frame, text=f"Existing: {cur_desc}").pack(anchor=tk.W) 83 ttk.Label(frame, text=f"Imported: {imp_desc}").pack(anchor=tk.W) 84 85 var = tk.StringVar(value="keep") 86 self._radio_vars[qname] = var 87 88 radio_frame = ttk.Frame(frame) 89 radio_frame.pack(fill=tk.X, pady=(3, 0)) 90 ttk.Radiobutton(radio_frame, text="Keep Existing", 91 variable=var, value="keep").pack( 92 side=tk.LEFT, padx=5) 93 ttk.Radiobutton(radio_frame, text="Replace", 94 variable=var, value="replace").pack( 95 side=tk.LEFT, padx=5) 96 ttk.Radiobutton(radio_frame, text="Skip (remove both)", 97 variable=var, value="skip").pack( 98 side=tk.LEFT, padx=5) 99 100 # Bulk actions 101 bulk_frame = ttk.Frame(main) 102 bulk_frame.pack(fill=tk.X, pady=(10, 5)) 103 ttk.Button(bulk_frame, text="Keep All Existing", 104 command=lambda: self._set_all("keep")).pack( 105 side=tk.LEFT, padx=3) 106 ttk.Button(bulk_frame, text="Replace All", 107 command=lambda: self._set_all("replace")).pack( 108 side=tk.LEFT, padx=3) 109 110 # OK / Cancel 111 btn_frame = ttk.Frame(main) 112 btn_frame.pack(fill=tk.X, pady=(5, 0)) 113 ttk.Button(btn_frame, text="OK", command=self._on_ok, 114 width=10).pack(side=tk.RIGHT, padx=5) 115 ttk.Button(btn_frame, text="Cancel", command=self._on_cancel, 116 width=10).pack(side=tk.RIGHT) 117 118 def _set_all(self, value: str): 119 for var in self._radio_vars.values(): 120 var.set(value) 121 122 def _on_ok(self): 123 result = {} 124 for qname, var in self._radio_vars.items(): 125 choice = var.get() 126 if choice == "keep": 127 result[qname] = self._current[qname] 128 elif choice == "replace": 129 result[qname] = self._imported[qname] 130 # "skip" -> not included (removed from merged) 131 self._result = result 132 self.destroy() 133 134 def _on_cancel(self): 135 self._result = None 136 self.destroy() 137 138 def get_result(self) -> dict[str, ActionDefinition] | None: 139 """Block until dialog closes. Returns resolved actions or None.""" 140 self.wait_window() 141 return self._result
class
ImportConflictDialog(tkinter.Toplevel):
18class ImportConflictDialog(tk.Toplevel): 19 """Dialog for resolving action conflicts during import.""" 20 21 def __init__(self, parent, conflicts: set[str], 22 current: dict[str, ActionDefinition], 23 imported: dict[str, ActionDefinition]): 24 super().__init__(parent) 25 self.transient(parent) 26 self.grab_set() 27 self.title("Resolve Import Conflicts") 28 29 self._result: dict[str, ActionDefinition] | None = None 30 self._conflicts = sorted(conflicts) 31 self._current = current 32 self._imported = imported 33 34 self._build_ui() 35 36 # Center on parent 37 self.update_idletasks() 38 pw, ph = parent.winfo_width(), parent.winfo_height() 39 px, py = parent.winfo_rootx(), parent.winfo_rooty() 40 w, h = self.winfo_width(), self.winfo_height() 41 self.geometry(f"+{px + (pw - w) // 2}+{py + (ph - h) // 2}") 42 43 self.protocol("WM_DELETE_WINDOW", self._on_cancel) 44 45 def _build_ui(self): 46 self.minsize(500, 400) 47 48 main = ttk.Frame(self, padding=10) 49 main.pack(fill=tk.BOTH, expand=True) 50 51 ttk.Label( 52 main, 53 text=f"{len(self._conflicts)} action(s) already exist:", 54 font=("TkDefaultFont", 10, "bold"), 55 ).pack(anchor=tk.W, pady=(0, 5)) 56 57 # Scrollable frame 58 canvas = tk.Canvas(main, highlightthickness=0) 59 scrollbar = ttk.Scrollbar(main, orient=tk.VERTICAL, 60 command=canvas.yview) 61 scroll_frame = ttk.Frame(canvas) 62 63 scroll_frame.bind( 64 "<Configure>", 65 lambda e: canvas.configure(scrollregion=canvas.bbox("all")), 66 ) 67 canvas.create_window((0, 0), window=scroll_frame, anchor=tk.NW) 68 canvas.configure(yscrollcommand=scrollbar.set) 69 70 canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 71 scrollbar.pack(side=tk.RIGHT, fill=tk.Y) 72 73 self._radio_vars: dict[str, tk.StringVar] = {} 74 for qname in self._conflicts: 75 frame = ttk.LabelFrame(scroll_frame, text=qname, padding=5) 76 frame.pack(fill=tk.X, padx=5, pady=3) 77 78 cur = self._current.get(qname) 79 imp = self._imported.get(qname) 80 81 cur_desc = cur.description if cur else "(none)" 82 imp_desc = imp.description if imp else "(none)" 83 ttk.Label(frame, text=f"Existing: {cur_desc}").pack(anchor=tk.W) 84 ttk.Label(frame, text=f"Imported: {imp_desc}").pack(anchor=tk.W) 85 86 var = tk.StringVar(value="keep") 87 self._radio_vars[qname] = var 88 89 radio_frame = ttk.Frame(frame) 90 radio_frame.pack(fill=tk.X, pady=(3, 0)) 91 ttk.Radiobutton(radio_frame, text="Keep Existing", 92 variable=var, value="keep").pack( 93 side=tk.LEFT, padx=5) 94 ttk.Radiobutton(radio_frame, text="Replace", 95 variable=var, value="replace").pack( 96 side=tk.LEFT, padx=5) 97 ttk.Radiobutton(radio_frame, text="Skip (remove both)", 98 variable=var, value="skip").pack( 99 side=tk.LEFT, padx=5) 100 101 # Bulk actions 102 bulk_frame = ttk.Frame(main) 103 bulk_frame.pack(fill=tk.X, pady=(10, 5)) 104 ttk.Button(bulk_frame, text="Keep All Existing", 105 command=lambda: self._set_all("keep")).pack( 106 side=tk.LEFT, padx=3) 107 ttk.Button(bulk_frame, text="Replace All", 108 command=lambda: self._set_all("replace")).pack( 109 side=tk.LEFT, padx=3) 110 111 # OK / Cancel 112 btn_frame = ttk.Frame(main) 113 btn_frame.pack(fill=tk.X, pady=(5, 0)) 114 ttk.Button(btn_frame, text="OK", command=self._on_ok, 115 width=10).pack(side=tk.RIGHT, padx=5) 116 ttk.Button(btn_frame, text="Cancel", command=self._on_cancel, 117 width=10).pack(side=tk.RIGHT) 118 119 def _set_all(self, value: str): 120 for var in self._radio_vars.values(): 121 var.set(value) 122 123 def _on_ok(self): 124 result = {} 125 for qname, var in self._radio_vars.items(): 126 choice = var.get() 127 if choice == "keep": 128 result[qname] = self._current[qname] 129 elif choice == "replace": 130 result[qname] = self._imported[qname] 131 # "skip" -> not included (removed from merged) 132 self._result = result 133 self.destroy() 134 135 def _on_cancel(self): 136 self._result = None 137 self.destroy() 138 139 def get_result(self) -> dict[str, ActionDefinition] | None: 140 """Block until dialog closes. Returns resolved actions or None.""" 141 self.wait_window() 142 return self._result
Dialog for resolving action conflicts during import.
ImportConflictDialog( parent, conflicts: set[str], current: dict[str, utils.controller.ActionDefinition], imported: dict[str, utils.controller.ActionDefinition])
21 def __init__(self, parent, conflicts: set[str], 22 current: dict[str, ActionDefinition], 23 imported: dict[str, ActionDefinition]): 24 super().__init__(parent) 25 self.transient(parent) 26 self.grab_set() 27 self.title("Resolve Import Conflicts") 28 29 self._result: dict[str, ActionDefinition] | None = None 30 self._conflicts = sorted(conflicts) 31 self._current = current 32 self._imported = imported 33 34 self._build_ui() 35 36 # Center on parent 37 self.update_idletasks() 38 pw, ph = parent.winfo_width(), parent.winfo_height() 39 px, py = parent.winfo_rootx(), parent.winfo_rooty() 40 w, h = self.winfo_width(), self.winfo_height() 41 self.geometry(f"+{px + (pw - w) // 2}+{py + (ph - h) // 2}") 42 43 self.protocol("WM_DELETE_WINDOW", self._on_cancel)
Construct a toplevel widget with the parent MASTER.
Valid option names: background, bd, bg, borderwidth, class, colormap, container, cursor, height, highlightbackground, highlightcolor, highlightthickness, menu, relief, screen, takefocus, use, visual, width.