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.

def get_result(self) -> dict[str, utils.controller.ActionDefinition] | None:
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

Block until dialog closes. Returns resolved actions or None.