experiment_control_window
This module defines the ExperimentCtlWindow class, a Toplevel window in the GUI responsible for allowing the user to assign specific substances (stimuli) to the different valves available on the experimental rig.
It generates labels and input fields for these stimuli based on the number of stimuli defined in the experiment variables and ensures symmetrical assignment between corresponding valves on opposing sides. This ensures that each simulus used is used on both sides. It also includes the button to trigger the generation of the experiment schedule after stimuli assignments are complete.
1""" 2This module defines the ExperimentCtlWindow class, a Toplevel window in the GUI responsible 3for allowing the user to assign specific substances (stimuli) to the different valves available 4on the experimental rig. 5 6It generates labels and input fields for these stimuli based on the number of stimuli 7defined in the experiment variables and ensures symmetrical assignment between corresponding valves 8on opposing sides. This ensures that each simulus used is used on both sides. It also includes the button to trigger the 9generation of the experiment schedule after stimuli assignments are complete. 10""" 11 12import tkinter as tk 13import logging 14from tkinter import ttk 15from typing import Callable, Dict, List 16 17from models.experiment_process_data import ExperimentProcessData 18from models.stimuli_data import StimuliData 19from views.gui_common import GUIUtils 20 21# Get the logger in use to log info here 22logger = logging.getLogger() 23 24 25class ExperimentCtlWindow(tk.Toplevel): 26 """ 27 This class defines a Toplevel window for managing experiment stimuli assignments to specific valves. 28 It allows users to input the substance associated with each valve, ensures symmetry between 29 corresponding valves on Side One and Side Two (e.g., Valve 1 mirrors Valve 5), and provides a button 30 to trigger the generation of the experiment schedule based on these assignments and other experiment variables. 31 32 Attributes 33 ---------- 34 - **exp_process_data** (*ExperimentProcessData*): Reference to the main `models.exp_process_data` model, used to access 35 variables like 'Num Stimuli' and trigger schedule generation. 36 - **stimuli_data** (*StimuliData*): Reference to the model holding stimuli/valve assignment data. Used to 37 get default values and update the model when user input changes. 38 - **trigger** (*Callable[[str], None]*): A callback function from StateMachine class. 39 that is called when the 'Generate Schedule' button is pressed to transition the application state to `GENERATE SCHEDULE` state. 40 - **stimuli_entries** (*Dict[str, tk.StringVar]*): A dictionary mapping valve identifier strings (e.g., "Valve 1 Substance") 41 to `tk.StringVar` objects. These variables are linked to the corresponding Entry widgets. 42 - **stimuli_frame** (*ttk.Frame*): The main container frame within the Toplevel window. 43 - **stimuli_entry_frame** (*tk.Frame*): A frame nested within `stimuli_frame`, specifically holding the 44 labeled entry widgets for valve substance input, organized into two columns. 45 - **ui_components** (*Dict[str, List[tk.Widget]]*): A dictionary storing lists of created UI widgets 46 (frames, labels, entries) within `populate_stimuli_frame`. This allows for potential future access or modification. 47 48 Methods 49 ------- 50 - `show()` 51 Makes the window visible (`deiconify`). It also checks if the 'Num Stimuli' variable in `exp_process_data` has 52 changed since the UI was last built; if so, it re-initializes the content using `init_content()`. 53 - `generate_button_method()` 54 Callback for the 'Generate Schedule' button. It first triggers the schedule generation logic within the 55 `exp_process_data` model. If successful, it hides the current window (`withdraw`) and calls the `trigger` 56 function with the "GENERATE SCHEDULE" state/event. 57 - `init_content()` 58 Builds or rebuilds the main content of the window, including the `stimuli_frame`, populating it with valve 59 entries using `populate_stimuli_frame`, and creating the 'Generate Schedule' button. 60 - `fill_reverse_stimuli(source_var: tk.StringVar, mapped_var: tk.StringVar)` 61 A callback function attached via `trace_add` to the `tk.StringVar`s in `stimuli_entries`. When the text in 62 a valve's entry widget (linked to `source_var`) changes, this function automatically updates the `tk.StringVar` 63 (`mapped_var`) of the corresponding valve on the opposite side (e.g., changing Valve 1 updates Valve 5's variable). 64 - `populate_stimuli_frame()` 65 Areates and arranges the `tk.Label` and `tk.Entry` widgets for each relevant valve within the 66 `stimuli_entry_frame`. It organizes them into "Side One" and "Side Two" columns based on the 'Num Stimuli' 67 variable. It also sets up the `trace_add` callbacks using `fill_reverse_stimuli` to link corresponding valves. 68 Stores created widgets in `self.ui_components`. 69 """ 70 71 def __init__( 72 self, 73 exp_data: ExperimentProcessData, 74 stimuli_data: StimuliData, 75 trigger: Callable[[str], None], 76 ): 77 """ 78 Initialize the ExperimentCtlWindow. Sets up data model references, the trigger callback, 79 window attributes (title, close protocol, bindings), Tkinter variables for stimuli, 80 links variables to the model, initializes UI content, and hides the window. 81 82 Parameters 83 ---------- 84 - **exp_data** (*ExperimentProcessData*): Instance holding experiment variables and methods. 85 - **stimuli_data** (*StimuliData*): Instance holding stimuli assignment data and logic. 86 - **trigger** (*Callable[[str], None]*): Callback function to signal state transitions (e.g., "GENERATE SCHEDULE"). 87 """ 88 super().__init__() 89 self.exp_process_data = exp_data 90 self.stimuli_data = stimuli_data 91 92 # a callback function to immediately show the program window upon genration of schec 93 self.trigger = trigger 94 95 # init window attributes 96 self.title("Stimuli / Valves") 97 self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw()) 98 # lambda requires event here, because it captures the keypress event in case you wanna pass that to the fucntion, 99 # but we don't so we do not use it. still, it is required to capture it 100 self.bind("<Control-w>", lambda event: self.withdraw()) 101 self.resizable(False, False) 102 103 # set grid row and col zero to expand to fill the available space 104 self.grid_rowconfigure(0, weight=1) 105 self.grid_columnconfigure(0, weight=1) 106 107 # tkinter variables for stimuli variables 108 self.stimuli_entries = { 109 "Valve 1 Substance": tk.StringVar(), 110 "Valve 2 Substance": tk.StringVar(), 111 "Valve 3 Substance": tk.StringVar(), 112 "Valve 4 Substance": tk.StringVar(), 113 "Valve 5 Substance": tk.StringVar(), 114 "Valve 6 Substance": tk.StringVar(), 115 "Valve 7 Substance": tk.StringVar(), 116 "Valve 8 Substance": tk.StringVar(), 117 } 118 119 # fill the exp_var entry boxes with their default values as configured in self.exp_data 120 for key in self.stimuli_entries.keys(): 121 self.stimuli_entries[key].set(self.stimuli_data.get_default_value(key)) 122 123 for key, value in self.stimuli_entries.items(): 124 value.trace_add( 125 "write", 126 lambda *args, key=key, value=value: self.stimuli_data.update_model( 127 key, GUIUtils.safe_tkinter_get(value) 128 ), 129 ) 130 131 self.init_content() 132 133 # on init, we just want to create the window so that we don't have to later, but then we want to hide it 134 # until the user chooses to show this window 135 self.withdraw() 136 137 logger.info("Experiment Control Window created, but hidden for now.") 138 139 def show(self): 140 """ 141 Makes the window visible. Checks if the number of stimuli has changed and rebuilds 142 the UI content if necessary before showing the window. 143 """ 144 prev_num_stim = len(self.ui_components["entries"]) 145 curr_num_stimuli = self.exp_process_data.exp_var_entries["Num Stimuli"] 146 if prev_num_stim != curr_num_stimuli: 147 self.init_content() 148 self.deiconify() 149 150 def generate_button_method(self): 151 """ 152 Handles the 'Generate Schedule' button click. Triggers schedule generation 153 in the data model, hides the window, and signals the main application via the trigger callback. 154 """ 155 # generate the program schedule df and fill it with stimuli that will be used 156 if not self.exp_process_data.generate_schedule(): 157 return 158 159 self.withdraw() 160 self.trigger("GENERATE SCHEDULE") 161 162 def init_content(self) -> None: 163 """ 164 Initializes or re-initializes the main UI elements of the window. 165 Creates the main frame, populates it with stimuli/valve entry fields, 166 and adds the 'Generate Schedule' button. 167 """ 168 try: 169 # Create the notebook (method of creating tabs at the bottom of the window), and set items to expand in all directions if window is resized 170 self.stimuli_frame = ttk.Frame(self) 171 self.stimuli_frame.grid(row=1, column=0, sticky="nsew") 172 173 self.populate_stimuli_frame() 174 175 GUIUtils.create_button( 176 self, 177 "Generate Schedule", 178 lambda: self.generate_button_method(), 179 "green", 180 0, 181 0, 182 ) 183 184 logger.info("Filled contents of experiment control window.") 185 except Exception as e: 186 logger.error(f"Error filling experiment control window: {e}") 187 raise 188 189 def fill_reverse_stimuli(self, source_var: tk.StringVar, mapped_var: tk.StringVar): 190 """ 191 Callback triggered when a stimulus entry's StringVar is modified. 192 It reads the new value from the source variable and sets the corresponding 193 (mapped) variable on the opposite side to the same value, ensuring that each stimuli is represented 194 on both sides of the experiment. 195 196 Parameters 197 ---------- 198 - **source_var** (*tk.StringVar*): The variable that was just changed by the user. 199 - **mapped_var** (*tk.StringVar*): The variable for the corresponding valve on the other side. 200 """ 201 try: 202 new_value = source_var.get() 203 # Update the mapped variable with the new value 204 mapped_var.set(new_value) 205 logger.debug(f"Filled reverse stimuli: {new_value}") 206 except Exception as e: 207 logger.error(f"Error filling reverse stimuli: {e}") 208 raise 209 210 def populate_stimuli_frame(self) -> None: 211 """ 212 Creates and places the Label and Entry widgets for valve substance assignments 213 within the `stimuli_entry_frame`. Organizes entries into 'Side One' and 'Side Two'. 214 Sets up trace callbacks to link corresponding valves (1<->5, 2<->6, etc.). 215 """ 216 try: 217 row = 1 218 self.ui_components: Dict[str, List[tk.Widget]] = { 219 "frames": [], 220 "labels": [], 221 "entries": [], 222 } 223 224 self.stimuli_entry_frame = tk.Frame( 225 self.stimuli_frame, highlightthickness=2, highlightbackground="black" 226 ) 227 self.stimuli_entry_frame.grid( 228 row=0, column=0, pady=5, padx=5, sticky="nsew" 229 ) 230 self.stimuli_entry_frame.grid_rowconfigure(0, weight=1) 231 232 side_one_label = tk.Label( 233 self.stimuli_entry_frame, 234 text="Side One", 235 bg="light blue", 236 font=("Helvetica", 24), 237 highlightthickness=2, 238 highlightbackground="dark blue", 239 ) 240 side_one_label.grid(row=0, column=0, pady=5) 241 side_two_label = tk.Label( 242 self.stimuli_entry_frame, 243 pady=0, 244 text="Side Two", 245 bg="light blue", 246 font=("Helvetica", 24), 247 highlightthickness=2, 248 highlightbackground="dark blue", 249 ) 250 side_two_label.grid(row=0, column=1, pady=5) 251 for i in range(self.exp_process_data.exp_var_entries["Num Stimuli"] // 2): 252 column = 0 253 254 frame, label, entry = GUIUtils.create_labeled_entry( 255 self.stimuli_entry_frame, 256 f"Valve {i + 1}", 257 self.stimuli_entries[f"Valve {i + 1} Substance"], 258 row, 259 column, 260 ) 261 262 source_var = self.stimuli_entries[f"Valve {i + 1} Substance"] 263 mapped_var = self.stimuli_entries[f"Valve {i + 5} Substance"] 264 265 self.stimuli_entries[f"Valve {i + 1} Substance"].trace_add( 266 "write", 267 lambda name, 268 index, 269 mode, 270 source_var=source_var, 271 mapped_var=mapped_var: self.fill_reverse_stimuli( 272 source_var, mapped_var 273 ), 274 ) 275 self.ui_components["frames"].append(frame) 276 self.ui_components["labels"].append(label) 277 self.ui_components["entries"].append(entry) 278 279 row += 1 280 281 row = 1 282 for i in range(self.exp_process_data.exp_var_entries["Num Stimuli"] // 2): 283 column = 1 284 frame, label, entry = GUIUtils.create_labeled_entry( 285 self.stimuli_entry_frame, 286 f"Valve {i + 5}", 287 self.stimuli_entries[f"Valve {i + 5} Substance"], 288 row, 289 column, 290 ) 291 292 source_var = self.stimuli_entries[f"Valve {i + 5} Substance"] 293 mapped_var = self.stimuli_entries[f"Valve {i + 1} Substance"] 294 295 self.stimuli_entries[f"Valve {i + 5} Substance"].trace_add( 296 "write", 297 lambda name, 298 index, 299 mode, 300 source_var=source_var, 301 mapped_var=mapped_var: self.fill_reverse_stimuli( 302 source_var, mapped_var 303 ), 304 ) 305 self.ui_components["frames"].append(frame) 306 self.ui_components["labels"].append(label) 307 self.ui_components["entries"].append(entry) 308 309 row += 1 310 311 logger.info("Stimuli frame populated.") 312 except Exception as e: 313 logger.error(f"Error populating stimuli frame: {e}") 314 raise
26class ExperimentCtlWindow(tk.Toplevel): 27 """ 28 This class defines a Toplevel window for managing experiment stimuli assignments to specific valves. 29 It allows users to input the substance associated with each valve, ensures symmetry between 30 corresponding valves on Side One and Side Two (e.g., Valve 1 mirrors Valve 5), and provides a button 31 to trigger the generation of the experiment schedule based on these assignments and other experiment variables. 32 33 Attributes 34 ---------- 35 - **exp_process_data** (*ExperimentProcessData*): Reference to the main `models.exp_process_data` model, used to access 36 variables like 'Num Stimuli' and trigger schedule generation. 37 - **stimuli_data** (*StimuliData*): Reference to the model holding stimuli/valve assignment data. Used to 38 get default values and update the model when user input changes. 39 - **trigger** (*Callable[[str], None]*): A callback function from StateMachine class. 40 that is called when the 'Generate Schedule' button is pressed to transition the application state to `GENERATE SCHEDULE` state. 41 - **stimuli_entries** (*Dict[str, tk.StringVar]*): A dictionary mapping valve identifier strings (e.g., "Valve 1 Substance") 42 to `tk.StringVar` objects. These variables are linked to the corresponding Entry widgets. 43 - **stimuli_frame** (*ttk.Frame*): The main container frame within the Toplevel window. 44 - **stimuli_entry_frame** (*tk.Frame*): A frame nested within `stimuli_frame`, specifically holding the 45 labeled entry widgets for valve substance input, organized into two columns. 46 - **ui_components** (*Dict[str, List[tk.Widget]]*): A dictionary storing lists of created UI widgets 47 (frames, labels, entries) within `populate_stimuli_frame`. This allows for potential future access or modification. 48 49 Methods 50 ------- 51 - `show()` 52 Makes the window visible (`deiconify`). It also checks if the 'Num Stimuli' variable in `exp_process_data` has 53 changed since the UI was last built; if so, it re-initializes the content using `init_content()`. 54 - `generate_button_method()` 55 Callback for the 'Generate Schedule' button. It first triggers the schedule generation logic within the 56 `exp_process_data` model. If successful, it hides the current window (`withdraw`) and calls the `trigger` 57 function with the "GENERATE SCHEDULE" state/event. 58 - `init_content()` 59 Builds or rebuilds the main content of the window, including the `stimuli_frame`, populating it with valve 60 entries using `populate_stimuli_frame`, and creating the 'Generate Schedule' button. 61 - `fill_reverse_stimuli(source_var: tk.StringVar, mapped_var: tk.StringVar)` 62 A callback function attached via `trace_add` to the `tk.StringVar`s in `stimuli_entries`. When the text in 63 a valve's entry widget (linked to `source_var`) changes, this function automatically updates the `tk.StringVar` 64 (`mapped_var`) of the corresponding valve on the opposite side (e.g., changing Valve 1 updates Valve 5's variable). 65 - `populate_stimuli_frame()` 66 Areates and arranges the `tk.Label` and `tk.Entry` widgets for each relevant valve within the 67 `stimuli_entry_frame`. It organizes them into "Side One" and "Side Two" columns based on the 'Num Stimuli' 68 variable. It also sets up the `trace_add` callbacks using `fill_reverse_stimuli` to link corresponding valves. 69 Stores created widgets in `self.ui_components`. 70 """ 71 72 def __init__( 73 self, 74 exp_data: ExperimentProcessData, 75 stimuli_data: StimuliData, 76 trigger: Callable[[str], None], 77 ): 78 """ 79 Initialize the ExperimentCtlWindow. Sets up data model references, the trigger callback, 80 window attributes (title, close protocol, bindings), Tkinter variables for stimuli, 81 links variables to the model, initializes UI content, and hides the window. 82 83 Parameters 84 ---------- 85 - **exp_data** (*ExperimentProcessData*): Instance holding experiment variables and methods. 86 - **stimuli_data** (*StimuliData*): Instance holding stimuli assignment data and logic. 87 - **trigger** (*Callable[[str], None]*): Callback function to signal state transitions (e.g., "GENERATE SCHEDULE"). 88 """ 89 super().__init__() 90 self.exp_process_data = exp_data 91 self.stimuli_data = stimuli_data 92 93 # a callback function to immediately show the program window upon genration of schec 94 self.trigger = trigger 95 96 # init window attributes 97 self.title("Stimuli / Valves") 98 self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw()) 99 # lambda requires event here, because it captures the keypress event in case you wanna pass that to the fucntion, 100 # but we don't so we do not use it. still, it is required to capture it 101 self.bind("<Control-w>", lambda event: self.withdraw()) 102 self.resizable(False, False) 103 104 # set grid row and col zero to expand to fill the available space 105 self.grid_rowconfigure(0, weight=1) 106 self.grid_columnconfigure(0, weight=1) 107 108 # tkinter variables for stimuli variables 109 self.stimuli_entries = { 110 "Valve 1 Substance": tk.StringVar(), 111 "Valve 2 Substance": tk.StringVar(), 112 "Valve 3 Substance": tk.StringVar(), 113 "Valve 4 Substance": tk.StringVar(), 114 "Valve 5 Substance": tk.StringVar(), 115 "Valve 6 Substance": tk.StringVar(), 116 "Valve 7 Substance": tk.StringVar(), 117 "Valve 8 Substance": tk.StringVar(), 118 } 119 120 # fill the exp_var entry boxes with their default values as configured in self.exp_data 121 for key in self.stimuli_entries.keys(): 122 self.stimuli_entries[key].set(self.stimuli_data.get_default_value(key)) 123 124 for key, value in self.stimuli_entries.items(): 125 value.trace_add( 126 "write", 127 lambda *args, key=key, value=value: self.stimuli_data.update_model( 128 key, GUIUtils.safe_tkinter_get(value) 129 ), 130 ) 131 132 self.init_content() 133 134 # on init, we just want to create the window so that we don't have to later, but then we want to hide it 135 # until the user chooses to show this window 136 self.withdraw() 137 138 logger.info("Experiment Control Window created, but hidden for now.") 139 140 def show(self): 141 """ 142 Makes the window visible. Checks if the number of stimuli has changed and rebuilds 143 the UI content if necessary before showing the window. 144 """ 145 prev_num_stim = len(self.ui_components["entries"]) 146 curr_num_stimuli = self.exp_process_data.exp_var_entries["Num Stimuli"] 147 if prev_num_stim != curr_num_stimuli: 148 self.init_content() 149 self.deiconify() 150 151 def generate_button_method(self): 152 """ 153 Handles the 'Generate Schedule' button click. Triggers schedule generation 154 in the data model, hides the window, and signals the main application via the trigger callback. 155 """ 156 # generate the program schedule df and fill it with stimuli that will be used 157 if not self.exp_process_data.generate_schedule(): 158 return 159 160 self.withdraw() 161 self.trigger("GENERATE SCHEDULE") 162 163 def init_content(self) -> None: 164 """ 165 Initializes or re-initializes the main UI elements of the window. 166 Creates the main frame, populates it with stimuli/valve entry fields, 167 and adds the 'Generate Schedule' button. 168 """ 169 try: 170 # Create the notebook (method of creating tabs at the bottom of the window), and set items to expand in all directions if window is resized 171 self.stimuli_frame = ttk.Frame(self) 172 self.stimuli_frame.grid(row=1, column=0, sticky="nsew") 173 174 self.populate_stimuli_frame() 175 176 GUIUtils.create_button( 177 self, 178 "Generate Schedule", 179 lambda: self.generate_button_method(), 180 "green", 181 0, 182 0, 183 ) 184 185 logger.info("Filled contents of experiment control window.") 186 except Exception as e: 187 logger.error(f"Error filling experiment control window: {e}") 188 raise 189 190 def fill_reverse_stimuli(self, source_var: tk.StringVar, mapped_var: tk.StringVar): 191 """ 192 Callback triggered when a stimulus entry's StringVar is modified. 193 It reads the new value from the source variable and sets the corresponding 194 (mapped) variable on the opposite side to the same value, ensuring that each stimuli is represented 195 on both sides of the experiment. 196 197 Parameters 198 ---------- 199 - **source_var** (*tk.StringVar*): The variable that was just changed by the user. 200 - **mapped_var** (*tk.StringVar*): The variable for the corresponding valve on the other side. 201 """ 202 try: 203 new_value = source_var.get() 204 # Update the mapped variable with the new value 205 mapped_var.set(new_value) 206 logger.debug(f"Filled reverse stimuli: {new_value}") 207 except Exception as e: 208 logger.error(f"Error filling reverse stimuli: {e}") 209 raise 210 211 def populate_stimuli_frame(self) -> None: 212 """ 213 Creates and places the Label and Entry widgets for valve substance assignments 214 within the `stimuli_entry_frame`. Organizes entries into 'Side One' and 'Side Two'. 215 Sets up trace callbacks to link corresponding valves (1<->5, 2<->6, etc.). 216 """ 217 try: 218 row = 1 219 self.ui_components: Dict[str, List[tk.Widget]] = { 220 "frames": [], 221 "labels": [], 222 "entries": [], 223 } 224 225 self.stimuli_entry_frame = tk.Frame( 226 self.stimuli_frame, highlightthickness=2, highlightbackground="black" 227 ) 228 self.stimuli_entry_frame.grid( 229 row=0, column=0, pady=5, padx=5, sticky="nsew" 230 ) 231 self.stimuli_entry_frame.grid_rowconfigure(0, weight=1) 232 233 side_one_label = tk.Label( 234 self.stimuli_entry_frame, 235 text="Side One", 236 bg="light blue", 237 font=("Helvetica", 24), 238 highlightthickness=2, 239 highlightbackground="dark blue", 240 ) 241 side_one_label.grid(row=0, column=0, pady=5) 242 side_two_label = tk.Label( 243 self.stimuli_entry_frame, 244 pady=0, 245 text="Side Two", 246 bg="light blue", 247 font=("Helvetica", 24), 248 highlightthickness=2, 249 highlightbackground="dark blue", 250 ) 251 side_two_label.grid(row=0, column=1, pady=5) 252 for i in range(self.exp_process_data.exp_var_entries["Num Stimuli"] // 2): 253 column = 0 254 255 frame, label, entry = GUIUtils.create_labeled_entry( 256 self.stimuli_entry_frame, 257 f"Valve {i + 1}", 258 self.stimuli_entries[f"Valve {i + 1} Substance"], 259 row, 260 column, 261 ) 262 263 source_var = self.stimuli_entries[f"Valve {i + 1} Substance"] 264 mapped_var = self.stimuli_entries[f"Valve {i + 5} Substance"] 265 266 self.stimuli_entries[f"Valve {i + 1} Substance"].trace_add( 267 "write", 268 lambda name, 269 index, 270 mode, 271 source_var=source_var, 272 mapped_var=mapped_var: self.fill_reverse_stimuli( 273 source_var, mapped_var 274 ), 275 ) 276 self.ui_components["frames"].append(frame) 277 self.ui_components["labels"].append(label) 278 self.ui_components["entries"].append(entry) 279 280 row += 1 281 282 row = 1 283 for i in range(self.exp_process_data.exp_var_entries["Num Stimuli"] // 2): 284 column = 1 285 frame, label, entry = GUIUtils.create_labeled_entry( 286 self.stimuli_entry_frame, 287 f"Valve {i + 5}", 288 self.stimuli_entries[f"Valve {i + 5} Substance"], 289 row, 290 column, 291 ) 292 293 source_var = self.stimuli_entries[f"Valve {i + 5} Substance"] 294 mapped_var = self.stimuli_entries[f"Valve {i + 1} Substance"] 295 296 self.stimuli_entries[f"Valve {i + 5} Substance"].trace_add( 297 "write", 298 lambda name, 299 index, 300 mode, 301 source_var=source_var, 302 mapped_var=mapped_var: self.fill_reverse_stimuli( 303 source_var, mapped_var 304 ), 305 ) 306 self.ui_components["frames"].append(frame) 307 self.ui_components["labels"].append(label) 308 self.ui_components["entries"].append(entry) 309 310 row += 1 311 312 logger.info("Stimuli frame populated.") 313 except Exception as e: 314 logger.error(f"Error populating stimuli frame: {e}") 315 raise
This class defines a Toplevel window for managing experiment stimuli assignments to specific valves. It allows users to input the substance associated with each valve, ensures symmetry between corresponding valves on Side One and Side Two (e.g., Valve 1 mirrors Valve 5), and provides a button to trigger the generation of the experiment schedule based on these assignments and other experiment variables.
Attributes
- exp_process_data (ExperimentProcessData): Reference to the main
models.exp_process_data
model, used to access variables like 'Num Stimuli' and trigger schedule generation. - stimuli_data (StimuliData): Reference to the model holding stimuli/valve assignment data. Used to get default values and update the model when user input changes.
- trigger (Callable[[str], None]): A callback function from StateMachine class.
that is called when the 'Generate Schedule' button is pressed to transition the application state to
GENERATE SCHEDULE
state. - stimuli_entries (Dict[str, tk.StringVar]): A dictionary mapping valve identifier strings (e.g., "Valve 1 Substance")
to
tk.StringVar
objects. These variables are linked to the corresponding Entry widgets. - stimuli_frame (ttk.Frame): The main container frame within the Toplevel window.
- stimuli_entry_frame (tk.Frame): A frame nested within
stimuli_frame
, specifically holding the labeled entry widgets for valve substance input, organized into two columns. - ui_components (Dict[str, List[tk.Widget]]): A dictionary storing lists of created UI widgets
(frames, labels, entries) within
populate_stimuli_frame
. This allows for potential future access or modification.
Methods
show()
Makes the window visible (deiconify
). It also checks if the 'Num Stimuli' variable inexp_process_data
has changed since the UI was last built; if so, it re-initializes the content usinginit_content()
.generate_button_method()
Callback for the 'Generate Schedule' button. It first triggers the schedule generation logic within theexp_process_data
model. If successful, it hides the current window (withdraw
) and calls thetrigger
function with the "GENERATE SCHEDULE" state/event.init_content()
Builds or rebuilds the main content of the window, including thestimuli_frame
, populating it with valve entries usingpopulate_stimuli_frame
, and creating the 'Generate Schedule' button.fill_reverse_stimuli(source_var: tk.StringVar, mapped_var: tk.StringVar)
A callback function attached viatrace_add
to thetk.StringVar
s instimuli_entries
. When the text in a valve's entry widget (linked tosource_var
) changes, this function automatically updates thetk.StringVar
(mapped_var
) of the corresponding valve on the opposite side (e.g., changing Valve 1 updates Valve 5's variable).populate_stimuli_frame()
Areates and arranges thetk.Label
andtk.Entry
widgets for each relevant valve within thestimuli_entry_frame
. It organizes them into "Side One" and "Side Two" columns based on the 'Num Stimuli' variable. It also sets up thetrace_add
callbacks usingfill_reverse_stimuli
to link corresponding valves. Stores created widgets inself.ui_components
.
72 def __init__( 73 self, 74 exp_data: ExperimentProcessData, 75 stimuli_data: StimuliData, 76 trigger: Callable[[str], None], 77 ): 78 """ 79 Initialize the ExperimentCtlWindow. Sets up data model references, the trigger callback, 80 window attributes (title, close protocol, bindings), Tkinter variables for stimuli, 81 links variables to the model, initializes UI content, and hides the window. 82 83 Parameters 84 ---------- 85 - **exp_data** (*ExperimentProcessData*): Instance holding experiment variables and methods. 86 - **stimuli_data** (*StimuliData*): Instance holding stimuli assignment data and logic. 87 - **trigger** (*Callable[[str], None]*): Callback function to signal state transitions (e.g., "GENERATE SCHEDULE"). 88 """ 89 super().__init__() 90 self.exp_process_data = exp_data 91 self.stimuli_data = stimuli_data 92 93 # a callback function to immediately show the program window upon genration of schec 94 self.trigger = trigger 95 96 # init window attributes 97 self.title("Stimuli / Valves") 98 self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw()) 99 # lambda requires event here, because it captures the keypress event in case you wanna pass that to the fucntion, 100 # but we don't so we do not use it. still, it is required to capture it 101 self.bind("<Control-w>", lambda event: self.withdraw()) 102 self.resizable(False, False) 103 104 # set grid row and col zero to expand to fill the available space 105 self.grid_rowconfigure(0, weight=1) 106 self.grid_columnconfigure(0, weight=1) 107 108 # tkinter variables for stimuli variables 109 self.stimuli_entries = { 110 "Valve 1 Substance": tk.StringVar(), 111 "Valve 2 Substance": tk.StringVar(), 112 "Valve 3 Substance": tk.StringVar(), 113 "Valve 4 Substance": tk.StringVar(), 114 "Valve 5 Substance": tk.StringVar(), 115 "Valve 6 Substance": tk.StringVar(), 116 "Valve 7 Substance": tk.StringVar(), 117 "Valve 8 Substance": tk.StringVar(), 118 } 119 120 # fill the exp_var entry boxes with their default values as configured in self.exp_data 121 for key in self.stimuli_entries.keys(): 122 self.stimuli_entries[key].set(self.stimuli_data.get_default_value(key)) 123 124 for key, value in self.stimuli_entries.items(): 125 value.trace_add( 126 "write", 127 lambda *args, key=key, value=value: self.stimuli_data.update_model( 128 key, GUIUtils.safe_tkinter_get(value) 129 ), 130 ) 131 132 self.init_content() 133 134 # on init, we just want to create the window so that we don't have to later, but then we want to hide it 135 # until the user chooses to show this window 136 self.withdraw() 137 138 logger.info("Experiment Control Window created, but hidden for now.")
Initialize the ExperimentCtlWindow. Sets up data model references, the trigger callback, window attributes (title, close protocol, bindings), Tkinter variables for stimuli, links variables to the model, initializes UI content, and hides the window.
Parameters
- exp_data (ExperimentProcessData): Instance holding experiment variables and methods.
- stimuli_data (StimuliData): Instance holding stimuli assignment data and logic.
- trigger (Callable[[str], None]): Callback function to signal state transitions (e.g., "GENERATE SCHEDULE").
140 def show(self): 141 """ 142 Makes the window visible. Checks if the number of stimuli has changed and rebuilds 143 the UI content if necessary before showing the window. 144 """ 145 prev_num_stim = len(self.ui_components["entries"]) 146 curr_num_stimuli = self.exp_process_data.exp_var_entries["Num Stimuli"] 147 if prev_num_stim != curr_num_stimuli: 148 self.init_content() 149 self.deiconify()
Makes the window visible. Checks if the number of stimuli has changed and rebuilds the UI content if necessary before showing the window.
163 def init_content(self) -> None: 164 """ 165 Initializes or re-initializes the main UI elements of the window. 166 Creates the main frame, populates it with stimuli/valve entry fields, 167 and adds the 'Generate Schedule' button. 168 """ 169 try: 170 # Create the notebook (method of creating tabs at the bottom of the window), and set items to expand in all directions if window is resized 171 self.stimuli_frame = ttk.Frame(self) 172 self.stimuli_frame.grid(row=1, column=0, sticky="nsew") 173 174 self.populate_stimuli_frame() 175 176 GUIUtils.create_button( 177 self, 178 "Generate Schedule", 179 lambda: self.generate_button_method(), 180 "green", 181 0, 182 0, 183 ) 184 185 logger.info("Filled contents of experiment control window.") 186 except Exception as e: 187 logger.error(f"Error filling experiment control window: {e}") 188 raise
Initializes or re-initializes the main UI elements of the window. Creates the main frame, populates it with stimuli/valve entry fields, and adds the 'Generate Schedule' button.
190 def fill_reverse_stimuli(self, source_var: tk.StringVar, mapped_var: tk.StringVar): 191 """ 192 Callback triggered when a stimulus entry's StringVar is modified. 193 It reads the new value from the source variable and sets the corresponding 194 (mapped) variable on the opposite side to the same value, ensuring that each stimuli is represented 195 on both sides of the experiment. 196 197 Parameters 198 ---------- 199 - **source_var** (*tk.StringVar*): The variable that was just changed by the user. 200 - **mapped_var** (*tk.StringVar*): The variable for the corresponding valve on the other side. 201 """ 202 try: 203 new_value = source_var.get() 204 # Update the mapped variable with the new value 205 mapped_var.set(new_value) 206 logger.debug(f"Filled reverse stimuli: {new_value}") 207 except Exception as e: 208 logger.error(f"Error filling reverse stimuli: {e}") 209 raise
Callback triggered when a stimulus entry's StringVar is modified. It reads the new value from the source variable and sets the corresponding (mapped) variable on the opposite side to the same value, ensuring that each stimuli is represented on both sides of the experiment.
Parameters
- source_var (tk.StringVar): The variable that was just changed by the user.
- mapped_var (tk.StringVar): The variable for the corresponding valve on the other side.
211 def populate_stimuli_frame(self) -> None: 212 """ 213 Creates and places the Label and Entry widgets for valve substance assignments 214 within the `stimuli_entry_frame`. Organizes entries into 'Side One' and 'Side Two'. 215 Sets up trace callbacks to link corresponding valves (1<->5, 2<->6, etc.). 216 """ 217 try: 218 row = 1 219 self.ui_components: Dict[str, List[tk.Widget]] = { 220 "frames": [], 221 "labels": [], 222 "entries": [], 223 } 224 225 self.stimuli_entry_frame = tk.Frame( 226 self.stimuli_frame, highlightthickness=2, highlightbackground="black" 227 ) 228 self.stimuli_entry_frame.grid( 229 row=0, column=0, pady=5, padx=5, sticky="nsew" 230 ) 231 self.stimuli_entry_frame.grid_rowconfigure(0, weight=1) 232 233 side_one_label = tk.Label( 234 self.stimuli_entry_frame, 235 text="Side One", 236 bg="light blue", 237 font=("Helvetica", 24), 238 highlightthickness=2, 239 highlightbackground="dark blue", 240 ) 241 side_one_label.grid(row=0, column=0, pady=5) 242 side_two_label = tk.Label( 243 self.stimuli_entry_frame, 244 pady=0, 245 text="Side Two", 246 bg="light blue", 247 font=("Helvetica", 24), 248 highlightthickness=2, 249 highlightbackground="dark blue", 250 ) 251 side_two_label.grid(row=0, column=1, pady=5) 252 for i in range(self.exp_process_data.exp_var_entries["Num Stimuli"] // 2): 253 column = 0 254 255 frame, label, entry = GUIUtils.create_labeled_entry( 256 self.stimuli_entry_frame, 257 f"Valve {i + 1}", 258 self.stimuli_entries[f"Valve {i + 1} Substance"], 259 row, 260 column, 261 ) 262 263 source_var = self.stimuli_entries[f"Valve {i + 1} Substance"] 264 mapped_var = self.stimuli_entries[f"Valve {i + 5} Substance"] 265 266 self.stimuli_entries[f"Valve {i + 1} Substance"].trace_add( 267 "write", 268 lambda name, 269 index, 270 mode, 271 source_var=source_var, 272 mapped_var=mapped_var: self.fill_reverse_stimuli( 273 source_var, mapped_var 274 ), 275 ) 276 self.ui_components["frames"].append(frame) 277 self.ui_components["labels"].append(label) 278 self.ui_components["entries"].append(entry) 279 280 row += 1 281 282 row = 1 283 for i in range(self.exp_process_data.exp_var_entries["Num Stimuli"] // 2): 284 column = 1 285 frame, label, entry = GUIUtils.create_labeled_entry( 286 self.stimuli_entry_frame, 287 f"Valve {i + 5}", 288 self.stimuli_entries[f"Valve {i + 5} Substance"], 289 row, 290 column, 291 ) 292 293 source_var = self.stimuli_entries[f"Valve {i + 5} Substance"] 294 mapped_var = self.stimuli_entries[f"Valve {i + 1} Substance"] 295 296 self.stimuli_entries[f"Valve {i + 5} Substance"].trace_add( 297 "write", 298 lambda name, 299 index, 300 mode, 301 source_var=source_var, 302 mapped_var=mapped_var: self.fill_reverse_stimuli( 303 source_var, mapped_var 304 ), 305 ) 306 self.ui_components["frames"].append(frame) 307 self.ui_components["labels"].append(label) 308 self.ui_components["entries"].append(entry) 309 310 row += 1 311 312 logger.info("Stimuli frame populated.") 313 except Exception as e: 314 logger.error(f"Error populating stimuli frame: {e}") 315 raise
Creates and places the Label and Entry widgets for valve substance assignments
within the stimuli_entry_frame
. Organizes entries into 'Side One' and 'Side Two'.
Sets up trace callbacks to link corresponding valves (1<->5, 2<->6, etc.).