From 2a79b2a848802fab1917eb79ad3989791814de68 Mon Sep 17 00:00:00 2001 From: Jahnavidarisetti Date: Fri, 20 Mar 2026 18:54:11 -0500 Subject: [PATCH 1/2] Updated the UI of the New Experiment module --- .../create_experiment/new_experiment_ui.py | 649 ++++++++++++------ 1 file changed, 433 insertions(+), 216 deletions(-) diff --git a/experiment_pages/create_experiment/new_experiment_ui.py b/experiment_pages/create_experiment/new_experiment_ui.py index 3c04b018..d89bbc04 100644 --- a/experiment_pages/create_experiment/new_experiment_ui.py +++ b/experiment_pages/create_experiment/new_experiment_ui.py @@ -1,5 +1,3 @@ -"""New Experiment Module โ€” full functional version with modernized layout.""" - from typing import Optional, List from customtkinter import ( @@ -9,14 +7,15 @@ CTkEntry, CTkRadioButton, CTkButton, + CTkFont, StringVar, BooleanVar, - W, - END, + CTkScrollbar ) -from shared.tk_models import MouserPage, ChangePageButton # pylint: disable=import-error -from shared.scrollable_frame import ScrolledFrame # pylint: disable=import-error +from tkinter import W, END + +from shared.tk_models import MouserPage # pylint: disable=import-error from experiment_pages.experiment.group_config_ui import ( # pylint: disable=import-error GroupConfigUI, ) @@ -31,243 +30,416 @@ class NewExperimentUI( # pylint: disable=too-many-instance-attributes MouserPage ): - """New Experiment user interface (full logic preserved, modern layout).""" + """New Experiment user interface with improved styling.""" def __init__(self, parent: CTk, menu_page: Optional[CTkFrame] = None): """Initialize the New Experiment form layout and bindings.""" super().__init__(parent, "New Experiment", menu_page) + self.configure(fg_color="#eef4ff") + self.canvas.configure(bg="#eef4ff", highlightthickness=0) + self.canvas.configure(yscrollincrement=24) + self._scroll_enabled = False + self.canvas.itemconfig( + self.rectangle, + fill="#0f172a", + outline="#0f172a", + ) + self.canvas.itemconfig( + self.title_label, + text="New Experiment", + fill="#f8fafc", + font=("Segoe UI Semibold", 20), + ) + self.scrollbar = CTkScrollbar( + self, + orientation="vertical", + command=self.canvas.yview + ) + self.canvas.configure(yscrollcommand=self.scrollbar.set) + self.menu_button_window = None + self.next_button_window = None + if hasattr(self, "menu_button") and self.menu_button: - self.menu_button.configure( - corner_radius=12, - height=50, - width=180, - font=("Segoe UI Semibold", 18), - text_color="white", - fg_color="#2563eb", - hover_color="#1e40af", + self.menu_button.destroy() + self.menu_button = CTkButton( + self.canvas, + text="โ†", + corner_radius=18, + height=42, + width=42, + font=("Segoe UI Semibold", 17), + command=lambda: self.menu_page.raise_frame(), + text_color="#f8fafc", + fg_color="#1d4ed8", + hover_color="#1e3a8a", + bg_color="transparent", + border_width=1, + border_color="#93c5fd", + ) + self.menu_button_window = self.canvas.create_window( + 24, + 25, + anchor="w", + window=self.menu_button, ) - self.menu_button.place_configure(relx=0.05, rely=0.13, anchor="w") self.input = Experiment() self.menu_page = menu_page self.next_button = None self.added_invest: List[str] = [] - # ---------------------------- - # Scrollable Main Layout - # ---------------------------- - scroll_canvas = ScrolledFrame(self) - scroll_canvas.place( - relx=0.5, - rely=0.65, - relheight=0.90, - relwidth=0.9, - anchor="center", - ) + self.font_title = CTkFont("Segoe UI Semibold", 26) + self.font_section = CTkFont("Segoe UI Semibold", 18) + self.font_label = CTkFont("Segoe UI Semibold", 15) + self.font_body = CTkFont("Segoe UI", 14) + self.font_small = CTkFont("Segoe UI", 13) + self.field_style = { + "height": 42, + "corner_radius": 14, + "border_width": 1, + "border_color": "#bfdbfe", + "fg_color": "#f8fbff", + "text_color": "#0f172a", + "font": self.font_body, + } self.main_frame = CTkFrame( - scroll_canvas, - fg_color=("white", "#2c2c2c"), - corner_radius=16, - border_width=1, - border_color="#d1d5db", + self.canvas, + fg_color="#eef4ff", + corner_radius=0, + ) + self.main_window = self.canvas.create_window( + 24, + 68, + anchor="nw", + window=self.main_frame, ) - self.main_frame.grid(row=0, column=0, padx=20, pady=20, sticky="nsew") + self.main_frame.grid_columnconfigure(0, weight=1) + self.main_frame.bind("", self._update_scroll_region) + self.canvas.bind("", self._on_canvas_configure) + self.canvas.bind("", self._bind_mousewheel) + self.canvas.bind("", self._unbind_mousewheel) - pad_x, pad_y = 10, 10 + self._build_form_card() + self.bind_all_entries() + self.create_next_button() - # ---------------------------- - # Form Labels - # ---------------------------- - CTkLabel( - self.main_frame, - text="Experiment Name", - ).grid(row=0, column=0, sticky=W, padx=pad_x, pady=(pad_y, 2)) - CTkLabel( - self.main_frame, - text="Password", - ).grid(row=0, column=2, sticky=W, padx=pad_x, pady=(pad_y, 2)) - CTkLabel( - self.main_frame, - text="Investigators", - ).grid(row=1, column=0, sticky=W, padx=pad_x, pady=(pad_y, 2)) - CTkLabel( - self.main_frame, - text="Species", - ).grid(row=3, column=0, sticky=W, padx=pad_x, pady=(pad_y, 2)) - CTkLabel( - self.main_frame, - text="Measurement Items", - ).grid(row=4, column=0, sticky=W, padx=pad_x, pady=(pad_y, 2)) - CTkLabel( - self.main_frame, - text="RFID", - ).grid(row=6, column=0, sticky=W, padx=pad_x, pady=(pad_y, 2)) - CTkLabel( + def _build_form_card(self): + """Create the main form card and all sections.""" + form_card = CTkFrame( self.main_frame, - text="Number of Animals", - ).grid(row=7, column=0, sticky=W, padx=pad_x, pady=(pad_y, 2)) + fg_color="#ffffff", + corner_radius=28, + border_width=1, + border_color="#cbd5e1", + ) + form_card.grid(row=0, column=0, sticky="ew", padx=10, pady=(0, 12)) + form_card.grid_columnconfigure(0, weight=1) + form_card.grid_columnconfigure(1, weight=1) + form_card.grid_rowconfigure(2, weight=1) + form_card.grid_rowconfigure(3, weight=1) + CTkLabel( - self.main_frame, - text="Number of Groups", - ).grid(row=8, column=0, sticky=W, padx=pad_x, pady=(pad_y, 2)) + form_card, + text="Experiment Details", + font=CTkFont("Segoe UI Semibold", 24), + text_color="#0f172a", + anchor="w", + ).grid(row=0, column=0, columnspan=2, sticky="w", padx=24, pady=(24, 4)) CTkLabel( - self.main_frame, - text="Max Animals per Cage", - ).grid(row=9, column=0, sticky=W, padx=pad_x, pady=(pad_y, 2)) - - # ---------------------------- - # Input Fields - # ---------------------------- - self.exper_name = CTkEntry(self.main_frame, width=180) - self.password = CTkEntry(self.main_frame, width=160, show="*") - self.investigators = CTkEntry(self.main_frame, width=180) - self.species = CTkEntry(self.main_frame, width=180) - self.measure_items = CTkEntry( - self.main_frame, - width=180, - textvariable=StringVar(value="Weight"), - ) - self.animal_num = CTkEntry(self.main_frame, width=140) - self.group_num = CTkEntry(self.main_frame, width=140) - self.num_per_cage = CTkEntry(self.main_frame, width=140) - - self.exper_name.grid( - row=0, - column=1, - sticky=W, - padx=pad_x, - pady=(2, pad_y), - ) - self.password.grid( - row=0, - column=3, - sticky=W, - padx=pad_x, - pady=(2, pad_y), + form_card, + text="A brighter layout with clear sections, helper text, and friendlier controls.", + font=self.font_body, + text_color="#475569", + anchor="w", + ).grid(row=1, column=0, columnspan=2, sticky="w", padx=24, pady=(0, 20)) + + basics = self._create_section(form_card, 2, 0, "๐Ÿ“ Basics", "#eff6ff", "#bfdbfe") + team = self._create_section(form_card, 2, 1, "๐Ÿ‘ฉโ€๐Ÿ”ฌ Team", "#f8fafc", "#cbd5e1") + study = self._create_section(form_card, 3, 0, "๐Ÿญ Study Setup", "#f8fffb", "#bbf7d0") + housing = self._create_section(form_card, 3, 1, "๐Ÿ  Housing", "#fff7ed", "#fed7aa") + + basics.grid_columnconfigure(0, weight=1) + basics.grid_columnconfigure(1, weight=1) + + self._add_field_label(basics, 1, 0, "Experiment Name *") + self.exper_name = CTkEntry( + basics, + width=220, + placeholder_text="Enter a study name", + **self.field_style, ) - self.investigators.grid( - row=1, - column=1, - sticky=W, - padx=pad_x, - pady=(2, pad_y), + self.exper_name.grid(row=2, column=0, sticky="ew", padx=(18, 10), pady=(0, 12)) + + self._add_field_label(basics, 1, 1, "Password") + self.password = CTkEntry( + basics, + width=220, + placeholder_text="Optional access password", + show="*", + **self.field_style, ) - self.species.grid( - row=3, - column=1, - sticky=W, - padx=pad_x, - pady=(2, pad_y), + self.password.grid(row=2, column=1, sticky="ew", padx=(10, 18), pady=(0, 12)) + + self._add_field_label(study, 1, 0, "Species *") + self.species = CTkEntry( + study, + width=220, + placeholder_text="Mouse, rat, etc.", + **self.field_style, ) - self.measure_items.grid( - row=4, - column=1, - sticky=W, - padx=pad_x, - pady=(2, pad_y), - ) - self.animal_num.grid( - row=7, - column=1, - sticky=W, - padx=pad_x, - pady=(2, pad_y), - ) - self.group_num.grid( - row=8, - column=1, - sticky=W, - padx=pad_x, - pady=(2, pad_y), - ) - self.num_per_cage.grid( - row=9, - column=1, - sticky=W, - padx=pad_x, - pady=(2, pad_y), + self.species.grid(row=2, column=0, sticky="ew", pady=(0, 12), padx=18) + + self._add_field_label(study, 3, 0, "Measurement Items") + self.measure_items = CTkEntry( + study, + width=220, + textvariable=StringVar(value="Weight"), + **self.field_style, ) + self.measure_items.grid(row=4, column=0, sticky="ew", pady=(0, 12), padx=18) - # ---------------------------- - # Investigator controls - # ---------------------------- - self.invest_frame = CTkFrame(self.main_frame, fg_color="transparent") - self.invest_frame.grid(row=2, column=1, sticky="nw") - self.invest_frame.grid_propagate(False) - self.invest_frame.configure(height=1) + self._add_field_label(study, 5, 0, "RFID") + self.rfid = BooleanVar(value=True) + self.rfid_frame = CTkFrame(study, fg_color="transparent") + self.rfid_frame.grid(row=6, column=0, sticky="w", pady=(0, 4), padx=18) + self._build_rfid_controls() + + team.grid_columnconfigure(0, weight=1) + team.grid_columnconfigure(1, weight=0) + + self._add_field_label(team, 1, 0, "Investigators") + self.investigators = CTkEntry( + team, + placeholder_text="Type a name and press Enter", + **self.field_style, + ) + self.investigators.grid(row=2, column=0, sticky="ew", padx=(18, 10), pady=(0, 12)) add_invest_button = CTkButton( - self.main_frame, - text="+", - width=28, + team, + text="โž• Add", + width=92, + height=42, command=lambda: [ self.add_investigator(), self.investigators.delete(0, END), ], - corner_radius=8, + corner_radius=14, fg_color="#2563eb", - hover_color="#1e40af", - text_color="white", + hover_color="#1d4ed8", + text_color="#eff6ff", + font=self.font_label, ) - add_invest_button.grid(row=1, column=2, padx=pad_x, pady=pad_y) + add_invest_button.grid(row=2, column=1, sticky="e", padx=(0, 18), pady=(0, 12)) - # ---------------------------- - # RFID Options - # ---------------------------- - self.rfid = BooleanVar(value=True) - self.rfid_frame = CTkFrame(self.main_frame, fg_color="transparent") - self.rfid_frame.grid(row=6, column=1, sticky="w") - CTkRadioButton( + self.invest_frame = CTkFrame( + team, + fg_color="#f8fbff", + corner_radius=16, + border_width=1, + border_color="#dbeafe", + ) + self.invest_frame.grid(row=3, column=0, columnspan=2, sticky="ew", padx=18) + self.invest_frame.grid_propagate(False) + self.invest_frame.configure(height=56) + + CTkLabel( + self.invest_frame, + text="No investigators added yet.", + font=self.font_small, + text_color="#64748b", + ).place(relx=0.04, rely=0.5, anchor="w") + + housing.grid_columnconfigure(0, weight=1) + + self._add_field_label(housing, 1, 0, "Number of Animals *") + self.animal_num = CTkEntry( + housing, + width=220, + placeholder_text="Total animals", + **self.field_style, + ) + self.animal_num.grid(row=2, column=0, sticky="ew", padx=18, pady=(0, 12)) + + self._add_field_label(housing, 3, 0, "Number of Groups *") + self.group_num = CTkEntry( + housing, + width=220, + placeholder_text="Group count", + **self.field_style, + ) + self.group_num.grid(row=4, column=0, sticky="ew", padx=18, pady=(0, 12)) + + self._add_field_label(housing, 5, 0, "Max Animals per Cage *") + self.num_per_cage = CTkEntry( + housing, + width=220, + placeholder_text="Capacity per cage", + **self.field_style, + ) + self.num_per_cage.grid(row=6, column=0, sticky="ew", padx=18, pady=(0, 6)) + + def _create_section( + self, + parent, + row, + column, + title, + fg_color, + border_color, + ): + """Create a stylized section card.""" + frame = CTkFrame( + parent, + fg_color=fg_color, + corner_radius=22, + border_width=1, + border_color=border_color, + ) + frame.grid( + row=row, + column=column, + sticky="nsew", + padx=24, + pady=(0, 20), + ) + frame.grid_columnconfigure(0, weight=1) + + CTkLabel( + frame, + text=title, + font=self.font_section, + text_color="#0f172a", + anchor="w", + ).grid(row=0, column=0, sticky="w", padx=18, pady=(18, 14)) + return frame + + def _add_field_label(self, parent, row, column, text): + """Create a consistent field label.""" + CTkLabel( + parent, + text=text, + font=self.font_label, + text_color="#1e293b", + anchor="w", + ).grid(row=row, column=column, sticky=W, padx=18, pady=(0, 6)) + + def _build_rfid_controls(self): + """Create the RFID choice buttons.""" + yes_radio = CTkRadioButton( self.rfid_frame, - text="Yes", + text="๐Ÿ“ก Yes", variable=self.rfid, value=1, - ).grid(row=0, column=0, padx=pad_x, pady=pad_y) - CTkRadioButton( + font=self.font_body, + text_color="#0f172a", + fg_color="#2563eb", + hover_color="#1d4ed8", + border_color="#60a5fa", + ) + yes_radio.grid(row=0, column=0, padx=(0, 18), pady=2) + + no_radio = CTkRadioButton( self.rfid_frame, - text="No", + text="๐Ÿงพ No", variable=self.rfid, value=0, - ).grid(row=0, column=1, padx=pad_x, pady=pad_y) + font=self.font_body, + text_color="#0f172a", + fg_color="#2563eb", + hover_color="#1d4ed8", + border_color="#60a5fa", + ) + no_radio.grid(row=0, column=1, padx=(0, 8), pady=2) - for i in range(0, 10): - self.main_frame.grid_rowconfigure(i, weight=0) - self.main_frame.grid_columnconfigure(i, weight=1) - # Next button and field tracking - self.create_next_button() - self.bind_all_entries() + def _update_scroll_region(self, _event=None): + """Enable page scrolling only when content exceeds the visible area.""" + self.update_idletasks() + + content_height = self.main_frame.winfo_reqheight() + 80 + canvas_height = max(self.canvas.winfo_height(), 1) + self._scroll_enabled = content_height > canvas_height + + if self._scroll_enabled: + self.canvas.configure( + scrollregion=(0, 0, self.canvas.winfo_width(), content_height) + ) + self.scrollbar.place(relx=1.0, rely=0, relheight=1.0, anchor="ne") + else: + self.canvas.configure( + scrollregion=(0, 0, self.canvas.winfo_width(), canvas_height) + ) + self.canvas.yview_moveto(0) + self.scrollbar.place_forget() + + def _on_canvas_configure(self, event): + """Resize the embedded content area with the window width.""" + content_width = max(event.width - 56, 320) + self.canvas.coords(self.main_window, 24, 68) + self.canvas.itemconfigure(self.main_window, width=content_width) + if self.menu_button_window is not None: + self.canvas.coords(self.menu_button_window, 24, 25) + if self.next_button_window is not None: + self.canvas.coords(self.next_button_window, event.width - 24, 25) + self._update_scroll_region() + + def _bind_mousewheel(self, _event=None): + """Enable mousewheel scrolling while the pointer is on this page.""" + self.canvas.bind_all("", self._on_mousewheel) + self.canvas.bind_all("<4>", self._on_mousewheel) + self.canvas.bind_all("<5>", self._on_mousewheel) + + def _unbind_mousewheel(self, _event=None): + """Disable page-level mousewheel binding when leaving the page.""" + self.canvas.unbind_all("") + self.canvas.unbind_all("<4>") + self.canvas.unbind_all("<5>") + + def _on_mousewheel(self, event): + """Scroll the page canvas across supported platforms.""" + if not self._scroll_enabled: + return + if event.num == 4 or event.delta > 0: + self.canvas.yview_scroll(-1, "units") + elif event.num == 5 or event.delta < 0: + self.canvas.yview_scroll(1, "units") - # ------------------------------------------------------------ - # Navigation + Buttons - # ------------------------------------------------------------ def create_next_button(self): """Create the Next button that validates and navigates.""" if self.next_button: self.next_button.destroy() - - self.next_button = ChangePageButton( - self, - next_page=None, - previous=False, - ) - self.next_button.configure( - corner_radius=12, - height=50, - width=180, + if self.next_button_window is not None: + self.canvas.delete(self.next_button_window) + self.next_button_window = None + + self.next_button = CTkButton( + self.canvas, + text="โ†’", + corner_radius=18, + height=42, + width=42, font=("Segoe UI Semibold", 18), - text_color="white", - fg_color="#2563eb", - hover_color="#1e40af", + text_color="#eff6ff", + fg_color="#ea580c", + hover_color="#c2410c", + bg_color="transparent", + border_width=1, + border_color="#fdba74", command=lambda: [ self.check_animals_divisible(), self._go_next(), ], state="disabled", ) - self.next_button.place_configure(relx=0.93, rely=0.13, anchor="e") + self.next_button_window = self.canvas.create_window( + max(self.canvas.winfo_width() - 24, 24), + 25, + anchor="e", + window=self.next_button, + ) def bind_all_entries(self): """Bind entry changes to enable/disable the Next button.""" @@ -277,11 +449,11 @@ def bind_all_entries(self): self.species, self.measure_items, self.animal_num, - self.group_num, self.num_per_cage, ] for entry in fields: entry.bind("", lambda event: self.enable_next_button()) + self.group_num.bind("", lambda event: self.enable_next_button()) self.investigators.bind( "", lambda event: [ @@ -304,9 +476,6 @@ def enable_next_button(self): else: self.next_button.configure(state="disabled") - # ------------------------------------------------------------ - # Investigator Logic - # ------------------------------------------------------------ def update_invest_frame(self): """Refresh the investigator list in the UI.""" for widget in self.invest_frame.winfo_children(): @@ -315,21 +484,47 @@ def update_invest_frame(self): funcs: List = [] buttons: List[CTkButton] = [] - for idx, investigator in enumerate(self.added_invest): + if not self.added_invest: + self.invest_frame.configure(height=56) CTkLabel( self.invest_frame, - text=investigator, - ).grid(row=idx, column=1, sticky=W, padx=10) - rem_button = CTkButton( + text="No investigators added yet.", + font=self.font_small, + text_color="#64748b", + ).place(relx=0.04, rely=0.5, anchor="w") + return + + for idx, investigator in enumerate(self.added_invest): + pill = CTkFrame( self.invest_frame, - text="-", - width=28, - corner_radius=8, + fg_color="#e0ecff", + corner_radius=14, + border_width=1, + border_color="#bfdbfe", + ) + pill.grid(row=idx, column=0, sticky="ew", padx=10, pady=6) + pill.grid_columnconfigure(0, weight=1) + + CTkLabel( + pill, + text=f"๐Ÿ‘ค {investigator}", + font=self.font_body, + text_color="#1e3a8a", + anchor="w", + ).grid(row=0, column=0, sticky=W, padx=(12, 8), pady=8) + + rem_button = CTkButton( + pill, + text="โœ–", + width=34, + height=30, + corner_radius=10, fg_color="#ef4444", hover_color="#b91c1c", - text_color="white", + text_color="#ffffff", + font=self.font_label, ) - rem_button.grid(row=idx, column=2, padx=10) + rem_button.grid(row=0, column=1, padx=(4, 10), pady=8) buttons.append(rem_button) funcs.append(lambda x=investigator: self.remove_investigator(x)) @@ -337,7 +532,7 @@ def update_invest_frame(self): buttons[i].configure(command=func) self.invest_frame.configure( - height=(len(self.added_invest) * 40) or 1, + height=max(len(self.added_invest) * 54, 56), ) def add_investigator(self): @@ -353,9 +548,6 @@ def remove_investigator(self, person: str): self.added_invest.remove(person) self.update_invest_frame() - # ------------------------------------------------------------ - # Validation / Warnings - # ------------------------------------------------------------ def raise_warning(self, option: int): """Display a warning window based on a validation code.""" @@ -363,9 +555,10 @@ def dismiss(event=None): # pylint: disable=unused-argument message.destroy() message = CTk() - message.title("WARNING") - message.geometry("340x180") + message.title("Warning") + message.geometry("390x220") message.resizable(False, False) + message.configure(fg_color="#fff7ed") texts = { 2: ( @@ -375,19 +568,46 @@ def dismiss(event=None): # pylint: disable=unused-argument 3: "Experiment name used. Please use another name.", 4: ( "Unequal Group Size: Please ensure total animals " - "โ‰ค group capacity." + "<= group capacity." ), } - CTkLabel( + + panel = CTkFrame( message, + fg_color="#ffffff", + corner_radius=22, + border_width=1, + border_color="#fed7aa", + ) + panel.pack(fill="both", expand=True, padx=18, pady=18) + + CTkLabel( + panel, + text="โš ๏ธ Please Check Your Entries", + font=CTkFont("Segoe UI Semibold", 20), + text_color="#9a3412", + ).pack(pady=(20, 10), padx=18) + CTkLabel( + panel, text=texts.get(option, "Warning"), - ).grid(row=0, column=0, padx=10, pady=10) + font=self.font_body, + text_color="#7c2d12", + justify="center", + wraplength=300, + ).pack(pady=(0, 18), padx=20) CTkButton( - message, + panel, text="OK", - width=10, + width=110, + height=40, command=dismiss, - ).grid(row=1, column=0, pady=10) + corner_radius=14, + fg_color="#ea580c", + hover_color="#c2410c", + text_color="#fff7ed", + font=self.font_label, + ).pack(pady=(0, 18)) + AudioManager.play(ERROR_SOUND) message.bind("", dismiss) message.bind("