host.controller_config.binding_dialog
Modal dialog for assigning/removing actions to a controller input.
Opened when clicking a binding box on the controller canvas. Shows the input name, currently assigned actions, and allows adding/removing actions. Double-click transfers items between lists.
1"""Modal dialog for assigning/removing actions to a controller input. 2 3Opened when clicking a binding box on the controller canvas. 4Shows the input name, currently assigned actions, and allows 5adding/removing actions. Double-click transfers items between lists. 6""" 7 8import tkinter as tk 9from tkinter import ttk 10 11from .layout_coords import XBOX_INPUT_MAP 12 13 14class BindingDialog(tk.Toplevel): 15 """Dialog for editing the action bindings of a single controller input.""" 16 17 def __init__(self, parent, input_name: str, current_actions: list[str], 18 available_actions: list[str], 19 action_descriptions: dict[str, str] | None = None): 20 """ 21 Args: 22 parent: parent window 23 input_name: canonical input name (e.g., "left_stick_x") 24 current_actions: list of currently bound action names 25 available_actions: all action names available to assign 26 action_descriptions: optional mapping of qname -> description 27 """ 28 super().__init__(parent) 29 self.transient(parent) 30 self.grab_set() 31 32 inp = XBOX_INPUT_MAP.get(input_name) 33 display = inp.display_name if inp else input_name 34 self.title(f"Bindings: {display}") 35 36 self._input_name = input_name 37 self._result: list[str] | None = None 38 self._assigned = list(current_actions) 39 self._available = [a for a in available_actions if a not in current_actions] 40 self._descriptions = action_descriptions or {} 41 42 self._build_ui() 43 44 # Center dialog on parent 45 self.update_idletasks() 46 pw = parent.winfo_width() 47 ph = parent.winfo_height() 48 px = parent.winfo_rootx() 49 py = parent.winfo_rooty() 50 w = self.winfo_width() 51 h = self.winfo_height() 52 x = px + (pw - w) // 2 53 y = py + (ph - h) // 2 54 self.geometry(f"+{x}+{y}") 55 56 self.protocol("WM_DELETE_WINDOW", self._on_cancel) 57 self.bind("<Escape>", lambda e: self._on_cancel()) 58 59 def _build_ui(self): 60 self.minsize(400, 350) 61 62 main = ttk.Frame(self, padding=10) 63 main.pack(fill=tk.BOTH, expand=True) 64 65 # --- Description status bar --- 66 self._desc_var = tk.StringVar(value="") 67 self._desc_label = ttk.Label( 68 main, textvariable=self._desc_var, 69 relief=tk.SUNKEN, anchor=tk.W, foreground="grey40") 70 self._desc_label.pack(fill=tk.X, pady=(0, 8)) 71 72 # --- Currently assigned actions --- 73 ttk.Label(main, text="Assigned Actions:", font=("Arial", 9, "bold")).pack(anchor=tk.W) 74 75 assigned_frame = tk.Frame(main) 76 assigned_frame.pack(fill=tk.BOTH, expand=True, pady=(2, 5)) 77 78 self._assigned_listbox = tk.Listbox(assigned_frame, height=5, exportselection=False) 79 self._assigned_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 80 81 assigned_scroll = ttk.Scrollbar(assigned_frame, orient=tk.VERTICAL, 82 command=self._assigned_listbox.yview) 83 assigned_scroll.pack(side=tk.RIGHT, fill=tk.Y) 84 self._assigned_listbox.config(yscrollcommand=assigned_scroll.set) 85 86 for action in self._assigned: 87 self._assigned_listbox.insert(tk.END, action) 88 89 # Double-click to remove, select to show description 90 self._assigned_listbox.bind("<Double-1>", lambda e: self._remove_action()) 91 self._assigned_listbox.bind("<<ListboxSelect>>", 92 lambda e: self._show_description(self._assigned_listbox)) 93 94 # Remove button 95 ttk.Button(main, text="Remove Selected", 96 command=self._remove_action).pack(anchor=tk.W, pady=(0, 10)) 97 98 # --- Available actions to add --- 99 ttk.Label(main, text="Available Actions:", font=("Arial", 9, "bold")).pack(anchor=tk.W) 100 101 avail_frame = tk.Frame(main) 102 avail_frame.pack(fill=tk.BOTH, expand=True, pady=(2, 5)) 103 104 self._avail_listbox = tk.Listbox(avail_frame, height=6, exportselection=False) 105 self._avail_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 106 107 avail_scroll = ttk.Scrollbar(avail_frame, orient=tk.VERTICAL, 108 command=self._avail_listbox.yview) 109 avail_scroll.pack(side=tk.RIGHT, fill=tk.Y) 110 self._avail_listbox.config(yscrollcommand=avail_scroll.set) 111 112 for action in sorted(self._available): 113 self._avail_listbox.insert(tk.END, action) 114 115 # Double-click to add, select to show description 116 self._avail_listbox.bind("<Double-1>", lambda e: self._add_action()) 117 self._avail_listbox.bind("<<ListboxSelect>>", 118 lambda e: self._show_description(self._avail_listbox)) 119 120 # Add button 121 ttk.Button(main, text="Add Selected", 122 command=self._add_action).pack(anchor=tk.W, pady=(0, 10)) 123 124 # --- OK / Cancel --- 125 btn_frame = tk.Frame(main) 126 btn_frame.pack(fill=tk.X) 127 ttk.Button(btn_frame, text="OK", command=self._on_ok, width=10).pack(side=tk.RIGHT, padx=5) 128 ttk.Button(btn_frame, text="Cancel", command=self._on_cancel, width=10).pack(side=tk.RIGHT) 129 130 def _show_description(self, listbox): 131 """Show the description of the selected action.""" 132 sel = listbox.curselection() 133 if not sel: 134 self._desc_var.set("") 135 return 136 action = listbox.get(sel[0]) 137 desc = self._descriptions.get(action, "") 138 self._desc_var.set(desc if desc else "No description") 139 140 def _remove_action(self): 141 """Move selected action from assigned back to available.""" 142 sel = self._assigned_listbox.curselection() 143 if not sel: 144 return 145 action = self._assigned_listbox.get(sel[0]) 146 self._assigned_listbox.delete(sel[0]) 147 self._assigned.remove(action) 148 149 # Add back to available list in sorted position 150 self._available.append(action) 151 self._available.sort() 152 self._avail_listbox.delete(0, tk.END) 153 for a in self._available: 154 self._avail_listbox.insert(tk.END, a) 155 self._desc_var.set("") 156 157 def _add_action(self): 158 """Move selected action from available to assigned.""" 159 sel = self._avail_listbox.curselection() 160 if not sel: 161 return 162 action = self._avail_listbox.get(sel[0]) 163 self._avail_listbox.delete(sel[0]) 164 self._available.remove(action) 165 166 self._assigned.append(action) 167 self._assigned_listbox.insert(tk.END, action) 168 self._desc_var.set("") 169 170 def _on_ok(self): 171 self._result = list(self._assigned) 172 self.destroy() 173 174 def _on_cancel(self): 175 self._result = None 176 self.destroy() 177 178 def get_result(self) -> list[str] | None: 179 """Return the updated action list, or None if canceled.""" 180 self.wait_window() 181 return self._result
class
BindingDialog(tkinter.Toplevel):
15class BindingDialog(tk.Toplevel): 16 """Dialog for editing the action bindings of a single controller input.""" 17 18 def __init__(self, parent, input_name: str, current_actions: list[str], 19 available_actions: list[str], 20 action_descriptions: dict[str, str] | None = None): 21 """ 22 Args: 23 parent: parent window 24 input_name: canonical input name (e.g., "left_stick_x") 25 current_actions: list of currently bound action names 26 available_actions: all action names available to assign 27 action_descriptions: optional mapping of qname -> description 28 """ 29 super().__init__(parent) 30 self.transient(parent) 31 self.grab_set() 32 33 inp = XBOX_INPUT_MAP.get(input_name) 34 display = inp.display_name if inp else input_name 35 self.title(f"Bindings: {display}") 36 37 self._input_name = input_name 38 self._result: list[str] | None = None 39 self._assigned = list(current_actions) 40 self._available = [a for a in available_actions if a not in current_actions] 41 self._descriptions = action_descriptions or {} 42 43 self._build_ui() 44 45 # Center dialog on parent 46 self.update_idletasks() 47 pw = parent.winfo_width() 48 ph = parent.winfo_height() 49 px = parent.winfo_rootx() 50 py = parent.winfo_rooty() 51 w = self.winfo_width() 52 h = self.winfo_height() 53 x = px + (pw - w) // 2 54 y = py + (ph - h) // 2 55 self.geometry(f"+{x}+{y}") 56 57 self.protocol("WM_DELETE_WINDOW", self._on_cancel) 58 self.bind("<Escape>", lambda e: self._on_cancel()) 59 60 def _build_ui(self): 61 self.minsize(400, 350) 62 63 main = ttk.Frame(self, padding=10) 64 main.pack(fill=tk.BOTH, expand=True) 65 66 # --- Description status bar --- 67 self._desc_var = tk.StringVar(value="") 68 self._desc_label = ttk.Label( 69 main, textvariable=self._desc_var, 70 relief=tk.SUNKEN, anchor=tk.W, foreground="grey40") 71 self._desc_label.pack(fill=tk.X, pady=(0, 8)) 72 73 # --- Currently assigned actions --- 74 ttk.Label(main, text="Assigned Actions:", font=("Arial", 9, "bold")).pack(anchor=tk.W) 75 76 assigned_frame = tk.Frame(main) 77 assigned_frame.pack(fill=tk.BOTH, expand=True, pady=(2, 5)) 78 79 self._assigned_listbox = tk.Listbox(assigned_frame, height=5, exportselection=False) 80 self._assigned_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 81 82 assigned_scroll = ttk.Scrollbar(assigned_frame, orient=tk.VERTICAL, 83 command=self._assigned_listbox.yview) 84 assigned_scroll.pack(side=tk.RIGHT, fill=tk.Y) 85 self._assigned_listbox.config(yscrollcommand=assigned_scroll.set) 86 87 for action in self._assigned: 88 self._assigned_listbox.insert(tk.END, action) 89 90 # Double-click to remove, select to show description 91 self._assigned_listbox.bind("<Double-1>", lambda e: self._remove_action()) 92 self._assigned_listbox.bind("<<ListboxSelect>>", 93 lambda e: self._show_description(self._assigned_listbox)) 94 95 # Remove button 96 ttk.Button(main, text="Remove Selected", 97 command=self._remove_action).pack(anchor=tk.W, pady=(0, 10)) 98 99 # --- Available actions to add --- 100 ttk.Label(main, text="Available Actions:", font=("Arial", 9, "bold")).pack(anchor=tk.W) 101 102 avail_frame = tk.Frame(main) 103 avail_frame.pack(fill=tk.BOTH, expand=True, pady=(2, 5)) 104 105 self._avail_listbox = tk.Listbox(avail_frame, height=6, exportselection=False) 106 self._avail_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 107 108 avail_scroll = ttk.Scrollbar(avail_frame, orient=tk.VERTICAL, 109 command=self._avail_listbox.yview) 110 avail_scroll.pack(side=tk.RIGHT, fill=tk.Y) 111 self._avail_listbox.config(yscrollcommand=avail_scroll.set) 112 113 for action in sorted(self._available): 114 self._avail_listbox.insert(tk.END, action) 115 116 # Double-click to add, select to show description 117 self._avail_listbox.bind("<Double-1>", lambda e: self._add_action()) 118 self._avail_listbox.bind("<<ListboxSelect>>", 119 lambda e: self._show_description(self._avail_listbox)) 120 121 # Add button 122 ttk.Button(main, text="Add Selected", 123 command=self._add_action).pack(anchor=tk.W, pady=(0, 10)) 124 125 # --- OK / Cancel --- 126 btn_frame = tk.Frame(main) 127 btn_frame.pack(fill=tk.X) 128 ttk.Button(btn_frame, text="OK", command=self._on_ok, width=10).pack(side=tk.RIGHT, padx=5) 129 ttk.Button(btn_frame, text="Cancel", command=self._on_cancel, width=10).pack(side=tk.RIGHT) 130 131 def _show_description(self, listbox): 132 """Show the description of the selected action.""" 133 sel = listbox.curselection() 134 if not sel: 135 self._desc_var.set("") 136 return 137 action = listbox.get(sel[0]) 138 desc = self._descriptions.get(action, "") 139 self._desc_var.set(desc if desc else "No description") 140 141 def _remove_action(self): 142 """Move selected action from assigned back to available.""" 143 sel = self._assigned_listbox.curselection() 144 if not sel: 145 return 146 action = self._assigned_listbox.get(sel[0]) 147 self._assigned_listbox.delete(sel[0]) 148 self._assigned.remove(action) 149 150 # Add back to available list in sorted position 151 self._available.append(action) 152 self._available.sort() 153 self._avail_listbox.delete(0, tk.END) 154 for a in self._available: 155 self._avail_listbox.insert(tk.END, a) 156 self._desc_var.set("") 157 158 def _add_action(self): 159 """Move selected action from available to assigned.""" 160 sel = self._avail_listbox.curselection() 161 if not sel: 162 return 163 action = self._avail_listbox.get(sel[0]) 164 self._avail_listbox.delete(sel[0]) 165 self._available.remove(action) 166 167 self._assigned.append(action) 168 self._assigned_listbox.insert(tk.END, action) 169 self._desc_var.set("") 170 171 def _on_ok(self): 172 self._result = list(self._assigned) 173 self.destroy() 174 175 def _on_cancel(self): 176 self._result = None 177 self.destroy() 178 179 def get_result(self) -> list[str] | None: 180 """Return the updated action list, or None if canceled.""" 181 self.wait_window() 182 return self._result
Dialog for editing the action bindings of a single controller input.
BindingDialog( parent, input_name: str, current_actions: list[str], available_actions: list[str], action_descriptions: dict[str, str] | None = None)
18 def __init__(self, parent, input_name: str, current_actions: list[str], 19 available_actions: list[str], 20 action_descriptions: dict[str, str] | None = None): 21 """ 22 Args: 23 parent: parent window 24 input_name: canonical input name (e.g., "left_stick_x") 25 current_actions: list of currently bound action names 26 available_actions: all action names available to assign 27 action_descriptions: optional mapping of qname -> description 28 """ 29 super().__init__(parent) 30 self.transient(parent) 31 self.grab_set() 32 33 inp = XBOX_INPUT_MAP.get(input_name) 34 display = inp.display_name if inp else input_name 35 self.title(f"Bindings: {display}") 36 37 self._input_name = input_name 38 self._result: list[str] | None = None 39 self._assigned = list(current_actions) 40 self._available = [a for a in available_actions if a not in current_actions] 41 self._descriptions = action_descriptions or {} 42 43 self._build_ui() 44 45 # Center dialog on parent 46 self.update_idletasks() 47 pw = parent.winfo_width() 48 ph = parent.winfo_height() 49 px = parent.winfo_rootx() 50 py = parent.winfo_rooty() 51 w = self.winfo_width() 52 h = self.winfo_height() 53 x = px + (pw - w) // 2 54 y = py + (ph - h) // 2 55 self.geometry(f"+{x}+{y}") 56 57 self.protocol("WM_DELETE_WINDOW", self._on_cancel) 58 self.bind("<Escape>", lambda e: self._on_cancel())
Args: parent: parent window input_name: canonical input name (e.g., "left_stick_x") current_actions: list of currently bound action names available_actions: all action names available to assign action_descriptions: optional mapping of qname -> description