program_schedule_window
Defines the ProgramScheduleWindow class, a Tkinter Toplevel window. Creates and displays the detailed experimental schedule in a scrollable table format.
This view presents the pre-generated trial sequence, stimuli details, and allows
for updates of trial outcomes (like lick counts and actual TTC
state completion time)
as the experiment progresses. It relies on data provided by external objects
(like models.experiment_process_data
ExperimentProcessData) to populate and update its content.
1""" 2Defines the ProgramScheduleWindow class, a Tkinter Toplevel window. 3Creates and displays the detailed experimental schedule in a scrollable table format. 4 5This view presents the pre-generated trial sequence, stimuli details, and allows 6for updates of trial outcomes (like lick counts and actual `TTC` state completion time) 7as the experiment progresses. It relies on data provided by external objects 8(like `models.experiment_process_data` ExperimentProcessData) to populate and update its content. 9""" 10 11import tkinter as tk 12 13### Type Hinting### 14from models.experiment_process_data import ExperimentProcessData 15### Type Hinting### 16 17from views.gui_common import GUIUtils 18 19 20class ProgramScheduleWindow(tk.Toplevel): 21 """ 22 A Toplevel window that displays the experimental program schedule as a table. 23 24 This window visualizes the `program_schedule_df` DataFrame. 25 Accomplished by creating a scrollable canvas containing a grid 26 of Tkinter Labels representing the schedule data. It updates 27 cell contents (licks, TTC) and highlights the currently active trial row 28 based on calls from the main `app_logic` StateMachine. 29 30 The window is initially hidden and populated only when `show()` is first called. 31 32 Attributes 33 ---------- 34 - **exp_data** (*ExperimentProcessData*): An instance that allows for access to experiment related data and methods. (E.g stimuli names, 35 access trial lick data, etc.) 36 - **canvas_frame** (*tk.Frame*): The main frame holding the canvas and scrollbar. 37 - **canvas** (*tk.Canvas*): The widget providing the scrollable area for the table. 38 - **scrollbar** (*tk.Scrollbar*): The scrollbar linked to the canvas. 39 - **stimuli_frame** (*tk.Frame*): The frame placed inside the canvas, containing the grid of Label widgets that form the table. 40 - **header_labels** (*list[tk.Label] OR None*): List of tk.Label widgets for the table column headers. `None` until populated. 41 - **cell_labels** (*list[list[tk.Label]] OR None*): A 2D list (list of rows, each row is a list of tk.Label widgets) 42 representing the table data cells. `None` until populated. 43 44 Methods 45 ------- 46 - `refresh_end_trial(...)` 47 Updates the display after a trial ends (licks, TTC). 48 - `refresh_start_trial(...)` 49 Updates the display when a new trial starts (row highlighting). 50 - `update_ttc_actual(...)` 51 Specifically updates the 'TTC Actual' cell for a given trial. 52 - `update_licks(...)` 53 Specifically updates the lick count cells ('Port 1 Licks', 'Port 2 Licks') for a given trial. 54 - `update_row_color(...)` 55 Highlights the current trial row in yellow and resets the previous row's color. 56 - `show()` 57 Makes the window visible. Populates the table with data on the first call if not already done. 58 - `populate_stimuli_table()` 59 Creates the header and data cell Label widgets within `stimuli_frame` based on `exp_data.program_schedule_df`. 60 """ 61 62 def __init__(self, exp_process_data: ExperimentProcessData) -> None: 63 super().__init__() 64 self.exp_data = exp_process_data 65 66 self.title("Program Schedule") 67 68 self.grid_rowconfigure(0, weight=1) 69 self.grid_columnconfigure(0, weight=1) 70 71 self.resizable(False, False) 72 73 # bind control + w shortcut to hiding the window 74 self.bind("<Control-w>", lambda event: self.withdraw()) 75 self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw()) 76 77 # Setup the canvas and scrollbar 78 self.canvas_frame = tk.Frame(self) 79 self.canvas_frame.grid(row=0, column=0, sticky="nsew") 80 self.canvas_frame.grid_rowconfigure(0, weight=1) 81 self.canvas_frame.grid_columnconfigure(0, weight=1) 82 83 self.canvas = tk.Canvas(self.canvas_frame) 84 85 self.scrollbar = tk.Scrollbar( 86 self.canvas_frame, orient="vertical", command=self.canvas.yview 87 ) 88 89 self.canvas.configure(yscrollcommand=self.scrollbar.set) 90 self.canvas.grid(row=0, column=0, sticky="nsew") 91 self.scrollbar.grid(row=0, column=1, sticky="ns") 92 93 self.grid_rowconfigure(0, weight=1) 94 self.grid_columnconfigure(0, weight=1) 95 96 self.stimuli_frame = tk.Frame(self.canvas) 97 self.canvas.create_window((0, 0), window=self.stimuli_frame, anchor="nw") 98 99 # initialize stimuli table variables to None, this is used in show() method to 100 # understand if window has been initialized yet 101 self.header_labels: list[tk.Label] | None = None 102 self.cell_labels: list[list[tk.Label]] | None = None 103 104 self.withdraw() 105 106 def refresh_end_trial(self, logical_trial: int) -> None: 107 """ 108 Updates the table display with data from a completed trial. 109 110 Specifically calls methods to update lick counts and the actual 111 Trial Completion Time (TTC) for the given trial index. Forces an 112 update of the display using update_idletasks. 113 114 Parameters 115 ---------- 116 - **logical_trial** (*int*): The zero-based index of the trial that just ended, corresponding to the row in the 117 DataFrame and `cell_labels`. 118 119 Raises 120 ------ 121 - *AttributeError*: If `self.exp_data` or its `program_schedule_df` is not properly initialized. 122 - *IndexError*: If `logical_trial` is out of bounds for `self.cell_labels`. 123 - *KeyError*: If expected columns ('Port 1 Licks', etc.) are missing from the DataFrame. 124 - *tk.TclError*: If there's an issue updating the Label widgets. 125 """ 126 self.update_licks(logical_trial) 127 self.update_ttc_actual(logical_trial) 128 129 self.update_idletasks() 130 131 def refresh_start_trial(self, current_trial: int) -> None: 132 """ 133 Updates the table display to indicate the trial that is starting. 134 135 Specifically highlights the row corresponding to the `current_trial`. 136 Forces an update of the display. 137 138 Parameters 139 ---------- 140 - **current_trial** (*int*): The one-based number of the trial that is starting. 141 142 Raises 143 ------ 144 - *AttributeError*: If `self.cell_labels` is not initialized (i.e., table not populated). 145 - *IndexError*: If `current_trial` (adjusted for zero-based index) is out of bounds. 146 - *tk.TclError*: If there's an issue updating the Label widgets' background color. 147 """ 148 self.update_row_color(current_trial) 149 150 self.update_idletasks() 151 152 def update_ttc_actual(self, logical_trial: int) -> None: 153 """ 154 Updates the 'TTC Actual' cell for the specified trial row. 155 156 Retrieves the value from `self.exp_data.program_schedule_df` and sets 157 the text of the corresponding Label widget in `self.cell_labels`. 158 159 Parameters 160 ---------- 161 - **logical_trial** (*int*): The zero-based index of the trial row to update. 162 163 Raises 164 ------ 165 - *AttributeError*: If `self.exp_data` or `program_schedule_df` is None. 166 - *IndexError*: If `logical_trial` or the column index (9) is out of bounds. 167 - *KeyError*: If the column 'TTC Actual' does not exist in the DataFrame. 168 - *tk.TclError*: If updating the Label text fails. 169 """ 170 df = self.exp_data.program_schedule_df 171 172 if self.cell_labels is None: 173 return 174 175 # ttc actual time taken update 176 self.cell_labels[logical_trial][9].configure( 177 text=df.loc[logical_trial, "TTC Actual"] 178 ) 179 180 def update_licks(self, logical_trial: int) -> None: 181 """ 182 Updates the 'Port 1 Licks' and 'Port 2 Licks' cells for the specified trial row. 183 184 Retrieves values from `self.exp_data.program_schedule_df` and sets 185 the text of the corresponding Label widgets in `self.cell_labels`. 186 187 Parameters 188 ---------- 189 - **logical_trial** (*int*): The zero-based index of the trial row to update. 190 191 Raises 192 ------ 193 - *AttributeError*: If `self.exp_data` or `program_schedule_df` is None. 194 - *IndexError*: If `logical_trial` or column indices (4, 5) are out of bounds. 195 - *KeyError*: If columns 'Port 1 Licks' or 'Port 2 Licks' do not exist. 196 - *tk.TclError*: If updating the Label text fails. 197 """ 198 199 if self.cell_labels is None: 200 return 201 202 df = self.exp_data.program_schedule_df 203 # side 1 licks update 204 self.cell_labels[logical_trial][4].configure( 205 text=df.loc[logical_trial, "Port 1 Licks"] 206 ) 207 # side 2 licks update 208 self.cell_labels[logical_trial][5].configure( 209 text=df.loc[logical_trial, "Port 2 Licks"] 210 ) 211 212 def update_row_color(self, logical_trial: int) -> None: 213 """ 214 Highlights the current trial row and resets the previous trial row's color. 215 216 Sets the background color of all labels in the `current_trial` row (one-based) 217 to yellow. If it's not the first trial, it also resets the background color 218 of the previous trial's row labels to white. 219 220 Parameters 221 ---------- 222 - **logical_trial** (*int*): The zero-based number of the trial to highlight. 223 224 Raises 225 ------ 226 - *AttributeError*: If `self.cell_labels` is None. 227 - *IndexError*: If `current_trial` (adjusted for zero-based index) is out of bounds for `self.cell_labels`. 228 - *tk.TclError*: If configuring label background colors fails. 229 """ 230 # Adjust indices for zero-based indexing 231 if self.cell_labels is None: 232 return 233 234 if logical_trial == 1: 235 for i in range(10): 236 self.cell_labels[logical_trial][i].configure(bg="yellow") 237 else: 238 for i in range(10): 239 self.cell_labels[logical_trial - 1][i].configure(bg="white") 240 for i in range(10): 241 self.cell_labels[logical_trial][i].configure(bg="yellow") 242 243 def show(self) -> None: 244 """ 245 Makes the window visible and populates the schedule table if not already done. 246 247 Checks if the table labels (`header_labels`) have been created. If not, it 248 attempts to populate the table using `populate_stimuli_table()`. Before 249 populating, it checks if the required DataFrame (`program_schedule_df`) exists 250 and displays an error via `GUIUtils.display_error` if it's missing. 251 After successful population (or if already populated), it makes the window 252 visible using `deiconify()`. It also sets the canvas size and scroll region 253 after the first population. 254 255 Raises 256 ------ 257 - *AttributeError*: If `self.exp_data` is None or lacks `program_schedule_df` when checked. 258 - *Exception*: Can propagate exceptions from `populate_stimuli_table()` during the initial population. 259 """ 260 # if this is our first time calling the function, then grab the data and fill the table 261 262 # if we have already initialized we don't need to do anything. updating the window is handled 263 # by the main app when a trial ends 264 if self.header_labels is None: 265 if self.exp_data.program_schedule_df.empty: 266 GUIUtils.display_error( 267 "Schedule Not Generated", 268 "The schedule has not yet been generated, try doing that in the Valve / Stimuli window first and come back!", 269 ) 270 return 271 self.populate_stimuli_table() 272 # Calculate dimensions for canvas and scrollbar 273 canvas_width = self.stimuli_frame.winfo_reqwidth() 274 275 # max height 500px 276 canvas_height = min(500, self.stimuli_frame.winfo_reqheight() + 50) 277 278 # Adjust the canvas size and center the window 279 self.canvas.config(scrollregion=self.canvas.bbox("all")) 280 self.canvas.config(width=canvas_width, height=canvas_height) 281 282 # self.update_row_color(self.controller.data_mgr.current_trial_number) 283 self.deiconify() 284 285 def populate_stimuli_table(self) -> None: 286 """ 287 Creates and .grid()s the Tkinter Label widgets for the schedule table headers and data. 288 289 Reads data from `self.exp_data.program_schedule_df`. Creates a header row 290 and then iterates through the DataFrame rows, creating a Label for each cell. 291 Stores the created labels in `self.header_labels` and `self.cell_labels`. 292 Adds horizontal separators periodically based on experiment settings 293 (Num Stimuli / 2). Assumes `self.stimuli_frame` exists. 294 295 Raises 296 ------ 297 - *AttributeError*: If `self.exp_data` or `program_schedule_df` is None or malformed. 298 - *KeyError*: If `exp_data.exp_var_entries["Num Stimuli"]` is missing or invalid when calculating separator frequency. 299 - *tk.TclError*: If widget creation or grid placement fails. 300 - *TypeError*: If data types in the DataFrame are incompatible with Label text. 301 """ 302 303 df = self.exp_data.program_schedule_df 304 self.header_labels = [] 305 self.cell_labels = [] 306 307 # Create labels for the column headers 308 for j, col in enumerate(df.columns): 309 header_label = tk.Label( 310 self.stimuli_frame, 311 text=col, 312 bg="light blue", 313 fg="black", 314 font=("Helvetica", 10, "bold"), 315 highlightthickness=2, 316 highlightbackground="dark blue", 317 ) 318 header_label.grid(row=0, column=j, sticky="nsew", padx=5, pady=2.5) 319 self.header_labels.append(header_label) 320 321 # Get the total number of columns for spanning the separator 322 total_columns = len(df.columns) 323 current_row = 1 # Start at 1 to account for the header row 324 325 # Create cells for each row of data 326 for i, row in enumerate(df.itertuples(index=False), start=1): 327 row_labels = [] 328 for j, value in enumerate(row): 329 cell_label = tk.Label( 330 self.stimuli_frame, 331 text=value, 332 bg="white", 333 fg="black", 334 font=("Helvetica", 10), 335 highlightthickness=1, 336 highlightbackground="black", 337 ) 338 cell_label.grid( 339 row=current_row, column=j, sticky="nsew", padx=5, pady=2.5 340 ) 341 row_labels.append(cell_label) 342 self.cell_labels.append(row_labels) 343 current_row += 1 # Move to the next row 344 345 # Insert a horizontal line below every two rows 346 if i % (self.exp_data.exp_var_entries["Num Stimuli"] / 2) == 0: 347 separator_frame = tk.Frame(self.stimuli_frame, height=2, bg="black") 348 separator_frame.grid( 349 row=current_row, 350 column=0, 351 columnspan=total_columns, 352 sticky="ew", 353 padx=5, 354 ) 355 self.stimuli_frame.grid_rowconfigure(current_row, minsize=2) 356 current_row += ( 357 1 # Increment the current row to account for the separator 358 ) 359 360 # Make sure the last separator is not placed outside the data rows 361 if len(df) % 2 == 0: 362 self.stimuli_frame.grid_rowconfigure(current_row - 1, minsize=0) 363 self.stimuli_frame.update_idletasks()
21class ProgramScheduleWindow(tk.Toplevel): 22 """ 23 A Toplevel window that displays the experimental program schedule as a table. 24 25 This window visualizes the `program_schedule_df` DataFrame. 26 Accomplished by creating a scrollable canvas containing a grid 27 of Tkinter Labels representing the schedule data. It updates 28 cell contents (licks, TTC) and highlights the currently active trial row 29 based on calls from the main `app_logic` StateMachine. 30 31 The window is initially hidden and populated only when `show()` is first called. 32 33 Attributes 34 ---------- 35 - **exp_data** (*ExperimentProcessData*): An instance that allows for access to experiment related data and methods. (E.g stimuli names, 36 access trial lick data, etc.) 37 - **canvas_frame** (*tk.Frame*): The main frame holding the canvas and scrollbar. 38 - **canvas** (*tk.Canvas*): The widget providing the scrollable area for the table. 39 - **scrollbar** (*tk.Scrollbar*): The scrollbar linked to the canvas. 40 - **stimuli_frame** (*tk.Frame*): The frame placed inside the canvas, containing the grid of Label widgets that form the table. 41 - **header_labels** (*list[tk.Label] OR None*): List of tk.Label widgets for the table column headers. `None` until populated. 42 - **cell_labels** (*list[list[tk.Label]] OR None*): A 2D list (list of rows, each row is a list of tk.Label widgets) 43 representing the table data cells. `None` until populated. 44 45 Methods 46 ------- 47 - `refresh_end_trial(...)` 48 Updates the display after a trial ends (licks, TTC). 49 - `refresh_start_trial(...)` 50 Updates the display when a new trial starts (row highlighting). 51 - `update_ttc_actual(...)` 52 Specifically updates the 'TTC Actual' cell for a given trial. 53 - `update_licks(...)` 54 Specifically updates the lick count cells ('Port 1 Licks', 'Port 2 Licks') for a given trial. 55 - `update_row_color(...)` 56 Highlights the current trial row in yellow and resets the previous row's color. 57 - `show()` 58 Makes the window visible. Populates the table with data on the first call if not already done. 59 - `populate_stimuli_table()` 60 Creates the header and data cell Label widgets within `stimuli_frame` based on `exp_data.program_schedule_df`. 61 """ 62 63 def __init__(self, exp_process_data: ExperimentProcessData) -> None: 64 super().__init__() 65 self.exp_data = exp_process_data 66 67 self.title("Program Schedule") 68 69 self.grid_rowconfigure(0, weight=1) 70 self.grid_columnconfigure(0, weight=1) 71 72 self.resizable(False, False) 73 74 # bind control + w shortcut to hiding the window 75 self.bind("<Control-w>", lambda event: self.withdraw()) 76 self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw()) 77 78 # Setup the canvas and scrollbar 79 self.canvas_frame = tk.Frame(self) 80 self.canvas_frame.grid(row=0, column=0, sticky="nsew") 81 self.canvas_frame.grid_rowconfigure(0, weight=1) 82 self.canvas_frame.grid_columnconfigure(0, weight=1) 83 84 self.canvas = tk.Canvas(self.canvas_frame) 85 86 self.scrollbar = tk.Scrollbar( 87 self.canvas_frame, orient="vertical", command=self.canvas.yview 88 ) 89 90 self.canvas.configure(yscrollcommand=self.scrollbar.set) 91 self.canvas.grid(row=0, column=0, sticky="nsew") 92 self.scrollbar.grid(row=0, column=1, sticky="ns") 93 94 self.grid_rowconfigure(0, weight=1) 95 self.grid_columnconfigure(0, weight=1) 96 97 self.stimuli_frame = tk.Frame(self.canvas) 98 self.canvas.create_window((0, 0), window=self.stimuli_frame, anchor="nw") 99 100 # initialize stimuli table variables to None, this is used in show() method to 101 # understand if window has been initialized yet 102 self.header_labels: list[tk.Label] | None = None 103 self.cell_labels: list[list[tk.Label]] | None = None 104 105 self.withdraw() 106 107 def refresh_end_trial(self, logical_trial: int) -> None: 108 """ 109 Updates the table display with data from a completed trial. 110 111 Specifically calls methods to update lick counts and the actual 112 Trial Completion Time (TTC) for the given trial index. Forces an 113 update of the display using update_idletasks. 114 115 Parameters 116 ---------- 117 - **logical_trial** (*int*): The zero-based index of the trial that just ended, corresponding to the row in the 118 DataFrame and `cell_labels`. 119 120 Raises 121 ------ 122 - *AttributeError*: If `self.exp_data` or its `program_schedule_df` is not properly initialized. 123 - *IndexError*: If `logical_trial` is out of bounds for `self.cell_labels`. 124 - *KeyError*: If expected columns ('Port 1 Licks', etc.) are missing from the DataFrame. 125 - *tk.TclError*: If there's an issue updating the Label widgets. 126 """ 127 self.update_licks(logical_trial) 128 self.update_ttc_actual(logical_trial) 129 130 self.update_idletasks() 131 132 def refresh_start_trial(self, current_trial: int) -> None: 133 """ 134 Updates the table display to indicate the trial that is starting. 135 136 Specifically highlights the row corresponding to the `current_trial`. 137 Forces an update of the display. 138 139 Parameters 140 ---------- 141 - **current_trial** (*int*): The one-based number of the trial that is starting. 142 143 Raises 144 ------ 145 - *AttributeError*: If `self.cell_labels` is not initialized (i.e., table not populated). 146 - *IndexError*: If `current_trial` (adjusted for zero-based index) is out of bounds. 147 - *tk.TclError*: If there's an issue updating the Label widgets' background color. 148 """ 149 self.update_row_color(current_trial) 150 151 self.update_idletasks() 152 153 def update_ttc_actual(self, logical_trial: int) -> None: 154 """ 155 Updates the 'TTC Actual' cell for the specified trial row. 156 157 Retrieves the value from `self.exp_data.program_schedule_df` and sets 158 the text of the corresponding Label widget in `self.cell_labels`. 159 160 Parameters 161 ---------- 162 - **logical_trial** (*int*): The zero-based index of the trial row to update. 163 164 Raises 165 ------ 166 - *AttributeError*: If `self.exp_data` or `program_schedule_df` is None. 167 - *IndexError*: If `logical_trial` or the column index (9) is out of bounds. 168 - *KeyError*: If the column 'TTC Actual' does not exist in the DataFrame. 169 - *tk.TclError*: If updating the Label text fails. 170 """ 171 df = self.exp_data.program_schedule_df 172 173 if self.cell_labels is None: 174 return 175 176 # ttc actual time taken update 177 self.cell_labels[logical_trial][9].configure( 178 text=df.loc[logical_trial, "TTC Actual"] 179 ) 180 181 def update_licks(self, logical_trial: int) -> None: 182 """ 183 Updates the 'Port 1 Licks' and 'Port 2 Licks' cells for the specified trial row. 184 185 Retrieves values from `self.exp_data.program_schedule_df` and sets 186 the text of the corresponding Label widgets in `self.cell_labels`. 187 188 Parameters 189 ---------- 190 - **logical_trial** (*int*): The zero-based index of the trial row to update. 191 192 Raises 193 ------ 194 - *AttributeError*: If `self.exp_data` or `program_schedule_df` is None. 195 - *IndexError*: If `logical_trial` or column indices (4, 5) are out of bounds. 196 - *KeyError*: If columns 'Port 1 Licks' or 'Port 2 Licks' do not exist. 197 - *tk.TclError*: If updating the Label text fails. 198 """ 199 200 if self.cell_labels is None: 201 return 202 203 df = self.exp_data.program_schedule_df 204 # side 1 licks update 205 self.cell_labels[logical_trial][4].configure( 206 text=df.loc[logical_trial, "Port 1 Licks"] 207 ) 208 # side 2 licks update 209 self.cell_labels[logical_trial][5].configure( 210 text=df.loc[logical_trial, "Port 2 Licks"] 211 ) 212 213 def update_row_color(self, logical_trial: int) -> None: 214 """ 215 Highlights the current trial row and resets the previous trial row's color. 216 217 Sets the background color of all labels in the `current_trial` row (one-based) 218 to yellow. If it's not the first trial, it also resets the background color 219 of the previous trial's row labels to white. 220 221 Parameters 222 ---------- 223 - **logical_trial** (*int*): The zero-based number of the trial to highlight. 224 225 Raises 226 ------ 227 - *AttributeError*: If `self.cell_labels` is None. 228 - *IndexError*: If `current_trial` (adjusted for zero-based index) is out of bounds for `self.cell_labels`. 229 - *tk.TclError*: If configuring label background colors fails. 230 """ 231 # Adjust indices for zero-based indexing 232 if self.cell_labels is None: 233 return 234 235 if logical_trial == 1: 236 for i in range(10): 237 self.cell_labels[logical_trial][i].configure(bg="yellow") 238 else: 239 for i in range(10): 240 self.cell_labels[logical_trial - 1][i].configure(bg="white") 241 for i in range(10): 242 self.cell_labels[logical_trial][i].configure(bg="yellow") 243 244 def show(self) -> None: 245 """ 246 Makes the window visible and populates the schedule table if not already done. 247 248 Checks if the table labels (`header_labels`) have been created. If not, it 249 attempts to populate the table using `populate_stimuli_table()`. Before 250 populating, it checks if the required DataFrame (`program_schedule_df`) exists 251 and displays an error via `GUIUtils.display_error` if it's missing. 252 After successful population (or if already populated), it makes the window 253 visible using `deiconify()`. It also sets the canvas size and scroll region 254 after the first population. 255 256 Raises 257 ------ 258 - *AttributeError*: If `self.exp_data` is None or lacks `program_schedule_df` when checked. 259 - *Exception*: Can propagate exceptions from `populate_stimuli_table()` during the initial population. 260 """ 261 # if this is our first time calling the function, then grab the data and fill the table 262 263 # if we have already initialized we don't need to do anything. updating the window is handled 264 # by the main app when a trial ends 265 if self.header_labels is None: 266 if self.exp_data.program_schedule_df.empty: 267 GUIUtils.display_error( 268 "Schedule Not Generated", 269 "The schedule has not yet been generated, try doing that in the Valve / Stimuli window first and come back!", 270 ) 271 return 272 self.populate_stimuli_table() 273 # Calculate dimensions for canvas and scrollbar 274 canvas_width = self.stimuli_frame.winfo_reqwidth() 275 276 # max height 500px 277 canvas_height = min(500, self.stimuli_frame.winfo_reqheight() + 50) 278 279 # Adjust the canvas size and center the window 280 self.canvas.config(scrollregion=self.canvas.bbox("all")) 281 self.canvas.config(width=canvas_width, height=canvas_height) 282 283 # self.update_row_color(self.controller.data_mgr.current_trial_number) 284 self.deiconify() 285 286 def populate_stimuli_table(self) -> None: 287 """ 288 Creates and .grid()s the Tkinter Label widgets for the schedule table headers and data. 289 290 Reads data from `self.exp_data.program_schedule_df`. Creates a header row 291 and then iterates through the DataFrame rows, creating a Label for each cell. 292 Stores the created labels in `self.header_labels` and `self.cell_labels`. 293 Adds horizontal separators periodically based on experiment settings 294 (Num Stimuli / 2). Assumes `self.stimuli_frame` exists. 295 296 Raises 297 ------ 298 - *AttributeError*: If `self.exp_data` or `program_schedule_df` is None or malformed. 299 - *KeyError*: If `exp_data.exp_var_entries["Num Stimuli"]` is missing or invalid when calculating separator frequency. 300 - *tk.TclError*: If widget creation or grid placement fails. 301 - *TypeError*: If data types in the DataFrame are incompatible with Label text. 302 """ 303 304 df = self.exp_data.program_schedule_df 305 self.header_labels = [] 306 self.cell_labels = [] 307 308 # Create labels for the column headers 309 for j, col in enumerate(df.columns): 310 header_label = tk.Label( 311 self.stimuli_frame, 312 text=col, 313 bg="light blue", 314 fg="black", 315 font=("Helvetica", 10, "bold"), 316 highlightthickness=2, 317 highlightbackground="dark blue", 318 ) 319 header_label.grid(row=0, column=j, sticky="nsew", padx=5, pady=2.5) 320 self.header_labels.append(header_label) 321 322 # Get the total number of columns for spanning the separator 323 total_columns = len(df.columns) 324 current_row = 1 # Start at 1 to account for the header row 325 326 # Create cells for each row of data 327 for i, row in enumerate(df.itertuples(index=False), start=1): 328 row_labels = [] 329 for j, value in enumerate(row): 330 cell_label = tk.Label( 331 self.stimuli_frame, 332 text=value, 333 bg="white", 334 fg="black", 335 font=("Helvetica", 10), 336 highlightthickness=1, 337 highlightbackground="black", 338 ) 339 cell_label.grid( 340 row=current_row, column=j, sticky="nsew", padx=5, pady=2.5 341 ) 342 row_labels.append(cell_label) 343 self.cell_labels.append(row_labels) 344 current_row += 1 # Move to the next row 345 346 # Insert a horizontal line below every two rows 347 if i % (self.exp_data.exp_var_entries["Num Stimuli"] / 2) == 0: 348 separator_frame = tk.Frame(self.stimuli_frame, height=2, bg="black") 349 separator_frame.grid( 350 row=current_row, 351 column=0, 352 columnspan=total_columns, 353 sticky="ew", 354 padx=5, 355 ) 356 self.stimuli_frame.grid_rowconfigure(current_row, minsize=2) 357 current_row += ( 358 1 # Increment the current row to account for the separator 359 ) 360 361 # Make sure the last separator is not placed outside the data rows 362 if len(df) % 2 == 0: 363 self.stimuli_frame.grid_rowconfigure(current_row - 1, minsize=0) 364 self.stimuli_frame.update_idletasks()
A Toplevel window that displays the experimental program schedule as a table.
This window visualizes the program_schedule_df
DataFrame.
Accomplished by creating a scrollable canvas containing a grid
of Tkinter Labels representing the schedule data. It updates
cell contents (licks, TTC) and highlights the currently active trial row
based on calls from the main app_logic
StateMachine.
The window is initially hidden and populated only when show()
is first called.
Attributes
- exp_data (ExperimentProcessData): An instance that allows for access to experiment related data and methods. (E.g stimuli names, access trial lick data, etc.)
- canvas_frame (tk.Frame): The main frame holding the canvas and scrollbar.
- canvas (tk.Canvas): The widget providing the scrollable area for the table.
- scrollbar (tk.Scrollbar): The scrollbar linked to the canvas.
- stimuli_frame (tk.Frame): The frame placed inside the canvas, containing the grid of Label widgets that form the table.
- header_labels (list[tk.Label] OR None): List of tk.Label widgets for the table column headers.
None
until populated. - cell_labels (list[list[tk.Label]] OR None): A 2D list (list of rows, each row is a list of tk.Label widgets)
representing the table data cells.
None
until populated.
Methods
refresh_end_trial(...)
Updates the display after a trial ends (licks, TTC).refresh_start_trial(...)
Updates the display when a new trial starts (row highlighting).update_ttc_actual(...)
Specifically updates the 'TTC Actual' cell for a given trial.update_licks(...)
Specifically updates the lick count cells ('Port 1 Licks', 'Port 2 Licks') for a given trial.update_row_color(...)
Highlights the current trial row in yellow and resets the previous row's color.show()
Makes the window visible. Populates the table with data on the first call if not already done.populate_stimuli_table()
Creates the header and data cell Label widgets withinstimuli_frame
based onexp_data.program_schedule_df
.
63 def __init__(self, exp_process_data: ExperimentProcessData) -> None: 64 super().__init__() 65 self.exp_data = exp_process_data 66 67 self.title("Program Schedule") 68 69 self.grid_rowconfigure(0, weight=1) 70 self.grid_columnconfigure(0, weight=1) 71 72 self.resizable(False, False) 73 74 # bind control + w shortcut to hiding the window 75 self.bind("<Control-w>", lambda event: self.withdraw()) 76 self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw()) 77 78 # Setup the canvas and scrollbar 79 self.canvas_frame = tk.Frame(self) 80 self.canvas_frame.grid(row=0, column=0, sticky="nsew") 81 self.canvas_frame.grid_rowconfigure(0, weight=1) 82 self.canvas_frame.grid_columnconfigure(0, weight=1) 83 84 self.canvas = tk.Canvas(self.canvas_frame) 85 86 self.scrollbar = tk.Scrollbar( 87 self.canvas_frame, orient="vertical", command=self.canvas.yview 88 ) 89 90 self.canvas.configure(yscrollcommand=self.scrollbar.set) 91 self.canvas.grid(row=0, column=0, sticky="nsew") 92 self.scrollbar.grid(row=0, column=1, sticky="ns") 93 94 self.grid_rowconfigure(0, weight=1) 95 self.grid_columnconfigure(0, weight=1) 96 97 self.stimuli_frame = tk.Frame(self.canvas) 98 self.canvas.create_window((0, 0), window=self.stimuli_frame, anchor="nw") 99 100 # initialize stimuli table variables to None, this is used in show() method to 101 # understand if window has been initialized yet 102 self.header_labels: list[tk.Label] | None = None 103 self.cell_labels: list[list[tk.Label]] | None = None 104 105 self.withdraw()
Construct a toplevel widget with the parent MASTER.
Valid resource names: background, bd, bg, borderwidth, class, colormap, container, cursor, height, highlightbackground, highlightcolor, highlightthickness, menu, relief, screen, takefocus, use, visual, width.
107 def refresh_end_trial(self, logical_trial: int) -> None: 108 """ 109 Updates the table display with data from a completed trial. 110 111 Specifically calls methods to update lick counts and the actual 112 Trial Completion Time (TTC) for the given trial index. Forces an 113 update of the display using update_idletasks. 114 115 Parameters 116 ---------- 117 - **logical_trial** (*int*): The zero-based index of the trial that just ended, corresponding to the row in the 118 DataFrame and `cell_labels`. 119 120 Raises 121 ------ 122 - *AttributeError*: If `self.exp_data` or its `program_schedule_df` is not properly initialized. 123 - *IndexError*: If `logical_trial` is out of bounds for `self.cell_labels`. 124 - *KeyError*: If expected columns ('Port 1 Licks', etc.) are missing from the DataFrame. 125 - *tk.TclError*: If there's an issue updating the Label widgets. 126 """ 127 self.update_licks(logical_trial) 128 self.update_ttc_actual(logical_trial) 129 130 self.update_idletasks()
Updates the table display with data from a completed trial.
Specifically calls methods to update lick counts and the actual Trial Completion Time (TTC) for the given trial index. Forces an update of the display using update_idletasks.
Parameters
- logical_trial (int): The zero-based index of the trial that just ended, corresponding to the row in the
DataFrame and
cell_labels
.
Raises
- AttributeError: If
self.exp_data
or itsprogram_schedule_df
is not properly initialized. - IndexError: If
logical_trial
is out of bounds forself.cell_labels
. - KeyError: If expected columns ('Port 1 Licks', etc.) are missing from the DataFrame.
- tk.TclError: If there's an issue updating the Label widgets.
132 def refresh_start_trial(self, current_trial: int) -> None: 133 """ 134 Updates the table display to indicate the trial that is starting. 135 136 Specifically highlights the row corresponding to the `current_trial`. 137 Forces an update of the display. 138 139 Parameters 140 ---------- 141 - **current_trial** (*int*): The one-based number of the trial that is starting. 142 143 Raises 144 ------ 145 - *AttributeError*: If `self.cell_labels` is not initialized (i.e., table not populated). 146 - *IndexError*: If `current_trial` (adjusted for zero-based index) is out of bounds. 147 - *tk.TclError*: If there's an issue updating the Label widgets' background color. 148 """ 149 self.update_row_color(current_trial) 150 151 self.update_idletasks()
Updates the table display to indicate the trial that is starting.
Specifically highlights the row corresponding to the current_trial
.
Forces an update of the display.
Parameters
- current_trial (int): The one-based number of the trial that is starting.
Raises
- AttributeError: If
self.cell_labels
is not initialized (i.e., table not populated). - IndexError: If
current_trial
(adjusted for zero-based index) is out of bounds. - tk.TclError: If there's an issue updating the Label widgets' background color.
153 def update_ttc_actual(self, logical_trial: int) -> None: 154 """ 155 Updates the 'TTC Actual' cell for the specified trial row. 156 157 Retrieves the value from `self.exp_data.program_schedule_df` and sets 158 the text of the corresponding Label widget in `self.cell_labels`. 159 160 Parameters 161 ---------- 162 - **logical_trial** (*int*): The zero-based index of the trial row to update. 163 164 Raises 165 ------ 166 - *AttributeError*: If `self.exp_data` or `program_schedule_df` is None. 167 - *IndexError*: If `logical_trial` or the column index (9) is out of bounds. 168 - *KeyError*: If the column 'TTC Actual' does not exist in the DataFrame. 169 - *tk.TclError*: If updating the Label text fails. 170 """ 171 df = self.exp_data.program_schedule_df 172 173 if self.cell_labels is None: 174 return 175 176 # ttc actual time taken update 177 self.cell_labels[logical_trial][9].configure( 178 text=df.loc[logical_trial, "TTC Actual"] 179 )
Updates the 'TTC Actual' cell for the specified trial row.
Retrieves the value from self.exp_data.program_schedule_df
and sets
the text of the corresponding Label widget in self.cell_labels
.
Parameters
- logical_trial (int): The zero-based index of the trial row to update.
Raises
- AttributeError: If
self.exp_data
orprogram_schedule_df
is None. - IndexError: If
logical_trial
or the column index (9) is out of bounds. - KeyError: If the column 'TTC Actual' does not exist in the DataFrame.
- tk.TclError: If updating the Label text fails.
181 def update_licks(self, logical_trial: int) -> None: 182 """ 183 Updates the 'Port 1 Licks' and 'Port 2 Licks' cells for the specified trial row. 184 185 Retrieves values from `self.exp_data.program_schedule_df` and sets 186 the text of the corresponding Label widgets in `self.cell_labels`. 187 188 Parameters 189 ---------- 190 - **logical_trial** (*int*): The zero-based index of the trial row to update. 191 192 Raises 193 ------ 194 - *AttributeError*: If `self.exp_data` or `program_schedule_df` is None. 195 - *IndexError*: If `logical_trial` or column indices (4, 5) are out of bounds. 196 - *KeyError*: If columns 'Port 1 Licks' or 'Port 2 Licks' do not exist. 197 - *tk.TclError*: If updating the Label text fails. 198 """ 199 200 if self.cell_labels is None: 201 return 202 203 df = self.exp_data.program_schedule_df 204 # side 1 licks update 205 self.cell_labels[logical_trial][4].configure( 206 text=df.loc[logical_trial, "Port 1 Licks"] 207 ) 208 # side 2 licks update 209 self.cell_labels[logical_trial][5].configure( 210 text=df.loc[logical_trial, "Port 2 Licks"] 211 )
Updates the 'Port 1 Licks' and 'Port 2 Licks' cells for the specified trial row.
Retrieves values from self.exp_data.program_schedule_df
and sets
the text of the corresponding Label widgets in self.cell_labels
.
Parameters
- logical_trial (int): The zero-based index of the trial row to update.
Raises
- AttributeError: If
self.exp_data
orprogram_schedule_df
is None. - IndexError: If
logical_trial
or column indices (4, 5) are out of bounds. - KeyError: If columns 'Port 1 Licks' or 'Port 2 Licks' do not exist.
- tk.TclError: If updating the Label text fails.
213 def update_row_color(self, logical_trial: int) -> None: 214 """ 215 Highlights the current trial row and resets the previous trial row's color. 216 217 Sets the background color of all labels in the `current_trial` row (one-based) 218 to yellow. If it's not the first trial, it also resets the background color 219 of the previous trial's row labels to white. 220 221 Parameters 222 ---------- 223 - **logical_trial** (*int*): The zero-based number of the trial to highlight. 224 225 Raises 226 ------ 227 - *AttributeError*: If `self.cell_labels` is None. 228 - *IndexError*: If `current_trial` (adjusted for zero-based index) is out of bounds for `self.cell_labels`. 229 - *tk.TclError*: If configuring label background colors fails. 230 """ 231 # Adjust indices for zero-based indexing 232 if self.cell_labels is None: 233 return 234 235 if logical_trial == 1: 236 for i in range(10): 237 self.cell_labels[logical_trial][i].configure(bg="yellow") 238 else: 239 for i in range(10): 240 self.cell_labels[logical_trial - 1][i].configure(bg="white") 241 for i in range(10): 242 self.cell_labels[logical_trial][i].configure(bg="yellow")
Highlights the current trial row and resets the previous trial row's color.
Sets the background color of all labels in the current_trial
row (one-based)
to yellow. If it's not the first trial, it also resets the background color
of the previous trial's row labels to white.
Parameters
- logical_trial (int): The zero-based number of the trial to highlight.
Raises
- AttributeError: If
self.cell_labels
is None. - IndexError: If
current_trial
(adjusted for zero-based index) is out of bounds forself.cell_labels
. - tk.TclError: If configuring label background colors fails.
244 def show(self) -> None: 245 """ 246 Makes the window visible and populates the schedule table if not already done. 247 248 Checks if the table labels (`header_labels`) have been created. If not, it 249 attempts to populate the table using `populate_stimuli_table()`. Before 250 populating, it checks if the required DataFrame (`program_schedule_df`) exists 251 and displays an error via `GUIUtils.display_error` if it's missing. 252 After successful population (or if already populated), it makes the window 253 visible using `deiconify()`. It also sets the canvas size and scroll region 254 after the first population. 255 256 Raises 257 ------ 258 - *AttributeError*: If `self.exp_data` is None or lacks `program_schedule_df` when checked. 259 - *Exception*: Can propagate exceptions from `populate_stimuli_table()` during the initial population. 260 """ 261 # if this is our first time calling the function, then grab the data and fill the table 262 263 # if we have already initialized we don't need to do anything. updating the window is handled 264 # by the main app when a trial ends 265 if self.header_labels is None: 266 if self.exp_data.program_schedule_df.empty: 267 GUIUtils.display_error( 268 "Schedule Not Generated", 269 "The schedule has not yet been generated, try doing that in the Valve / Stimuli window first and come back!", 270 ) 271 return 272 self.populate_stimuli_table() 273 # Calculate dimensions for canvas and scrollbar 274 canvas_width = self.stimuli_frame.winfo_reqwidth() 275 276 # max height 500px 277 canvas_height = min(500, self.stimuli_frame.winfo_reqheight() + 50) 278 279 # Adjust the canvas size and center the window 280 self.canvas.config(scrollregion=self.canvas.bbox("all")) 281 self.canvas.config(width=canvas_width, height=canvas_height) 282 283 # self.update_row_color(self.controller.data_mgr.current_trial_number) 284 self.deiconify()
Makes the window visible and populates the schedule table if not already done.
Checks if the table labels (header_labels
) have been created. If not, it
attempts to populate the table using populate_stimuli_table()
. Before
populating, it checks if the required DataFrame (program_schedule_df
) exists
and displays an error via GUIUtils.display_error
if it's missing.
After successful population (or if already populated), it makes the window
visible using deiconify()
. It also sets the canvas size and scroll region
after the first population.
Raises
- AttributeError: If
self.exp_data
is None or lacksprogram_schedule_df
when checked. - Exception: Can propagate exceptions from
populate_stimuli_table()
during the initial population.
286 def populate_stimuli_table(self) -> None: 287 """ 288 Creates and .grid()s the Tkinter Label widgets for the schedule table headers and data. 289 290 Reads data from `self.exp_data.program_schedule_df`. Creates a header row 291 and then iterates through the DataFrame rows, creating a Label for each cell. 292 Stores the created labels in `self.header_labels` and `self.cell_labels`. 293 Adds horizontal separators periodically based on experiment settings 294 (Num Stimuli / 2). Assumes `self.stimuli_frame` exists. 295 296 Raises 297 ------ 298 - *AttributeError*: If `self.exp_data` or `program_schedule_df` is None or malformed. 299 - *KeyError*: If `exp_data.exp_var_entries["Num Stimuli"]` is missing or invalid when calculating separator frequency. 300 - *tk.TclError*: If widget creation or grid placement fails. 301 - *TypeError*: If data types in the DataFrame are incompatible with Label text. 302 """ 303 304 df = self.exp_data.program_schedule_df 305 self.header_labels = [] 306 self.cell_labels = [] 307 308 # Create labels for the column headers 309 for j, col in enumerate(df.columns): 310 header_label = tk.Label( 311 self.stimuli_frame, 312 text=col, 313 bg="light blue", 314 fg="black", 315 font=("Helvetica", 10, "bold"), 316 highlightthickness=2, 317 highlightbackground="dark blue", 318 ) 319 header_label.grid(row=0, column=j, sticky="nsew", padx=5, pady=2.5) 320 self.header_labels.append(header_label) 321 322 # Get the total number of columns for spanning the separator 323 total_columns = len(df.columns) 324 current_row = 1 # Start at 1 to account for the header row 325 326 # Create cells for each row of data 327 for i, row in enumerate(df.itertuples(index=False), start=1): 328 row_labels = [] 329 for j, value in enumerate(row): 330 cell_label = tk.Label( 331 self.stimuli_frame, 332 text=value, 333 bg="white", 334 fg="black", 335 font=("Helvetica", 10), 336 highlightthickness=1, 337 highlightbackground="black", 338 ) 339 cell_label.grid( 340 row=current_row, column=j, sticky="nsew", padx=5, pady=2.5 341 ) 342 row_labels.append(cell_label) 343 self.cell_labels.append(row_labels) 344 current_row += 1 # Move to the next row 345 346 # Insert a horizontal line below every two rows 347 if i % (self.exp_data.exp_var_entries["Num Stimuli"] / 2) == 0: 348 separator_frame = tk.Frame(self.stimuli_frame, height=2, bg="black") 349 separator_frame.grid( 350 row=current_row, 351 column=0, 352 columnspan=total_columns, 353 sticky="ew", 354 padx=5, 355 ) 356 self.stimuli_frame.grid_rowconfigure(current_row, minsize=2) 357 current_row += ( 358 1 # Increment the current row to account for the separator 359 ) 360 361 # Make sure the last separator is not placed outside the data rows 362 if len(df) % 2 == 0: 363 self.stimuli_frame.grid_rowconfigure(current_row - 1, minsize=0) 364 self.stimuli_frame.update_idletasks()
Creates and .grid()s the Tkinter Label widgets for the schedule table headers and data.
Reads data from self.exp_data.program_schedule_df
. Creates a header row
and then iterates through the DataFrame rows, creating a Label for each cell.
Stores the created labels in self.header_labels
and self.cell_labels
.
Adds horizontal separators periodically based on experiment settings
(Num Stimuli / 2). Assumes self.stimuli_frame
exists.
Raises
- AttributeError: If
self.exp_data
orprogram_schedule_df
is None or malformed. - KeyError: If
exp_data.exp_var_entries["Num Stimuli"]
is missing or invalid when calculating separator frequency. - tk.TclError: If widget creation or grid placement fails.
- TypeError: If data types in the DataFrame are incompatible with Label text.