Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
200 changes: 200 additions & 0 deletions src/rail_network_graph/GUI/counter_panel/input_panel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import logging
from collections.abc import Callable

import customtkinter as ctk
import pandas as pd

from rail_network_graph import config

log = logging.getLogger(__name__)

# Define color constants
ORANGE = "#FFA500"
DARK_BG = "#1a1a1a"
ENTRY_BG = "#2a2a2a"
TEXT_COLOR = "#ffffff"


class InputPanel:
"""
User input panel for selecting origin/destination stations and a ticket discount.

The panel includes:
- two text fields for start/end stations,
- an option menu for discount selection,
- a "Search" button with a user-provided callback.

:param root: Root CTk window or frame to contain the panel.
:param submit_callback: Function to call when the "Search" button is pressed.
"""

def __init__(self, root: ctk.CTk | ctk.CTkFrame, submit_callback: Callable[[], None] | None) -> None:
self.root = root
self.submit_callback = submit_callback

# Stores the currently selected discount percentage (default: 0)
self.selected_discount: int = 0
# Maps discount name (str) to percentage (int)
self.discount_map: dict[str, int] = {}
# List of discount names for the OptionMenu
self.discount_names: list[str] = []

log.info("Initializing InputPanel")
log.debug("InputPanel root=%s, submit_callback=%s", type(root).__name__, bool(submit_callback))

# Load available discounts from file
self._load_discounts()
# Create all GUI elements
self._create_widgets()
# Arrange the created elements
self.layout_widgets()
# Ensure the button command is set
self.set_submit_callback(submit_callback)

def _load_discounts(self) -> None:
"""
Load discounts from file and build ``{discount_name: percent}`` mapping.
"""
log.info("Loading discounts from file: %s", config.DISCOUNTS_PATH)
# Load discounts from a tab-separated file using pandas
discounts_df = pd.read_csv(config.DISCOUNTS_PATH, sep="\t")
# Create a dictionary mapping the 'name' column to the 'percent' column
self.discount_map = dict(zip(discounts_df["name"], discounts_df["percent"], strict=False))
# Extract just the names for the option menu
self.discount_names = list(self.discount_map.keys())
self.selected_discount = 0
log.debug(
"Loaded %d discounts, initial selected_discount=%d",
len(self.discount_map),
self.selected_discount,
)

def _create_widgets(self) -> None:
"""
Create GUI components (without layout/placement).
"""
log.debug("Creating InputPanel widgets")
# Outer frame with orange border effect
self.border_frame = ctk.CTkFrame(master=self.root, fg_color=ORANGE, corner_radius=9)
# Inner frame containing all input widgets
self.input_frame = ctk.CTkFrame(master=self.border_frame, fg_color=DARK_BG, corner_radius=9)

# Entry field for the starting station
self.entry1 = ctk.CTkEntry(
master=self.input_frame,
placeholder_text="Station 1",
fg_color=ENTRY_BG,
text_color=TEXT_COLOR,
placeholder_text_color="#888",
)
# Entry field for the destination station
self.entry2 = ctk.CTkEntry(
master=self.input_frame,
placeholder_text="Station 2",
fg_color=ENTRY_BG,
text_color=TEXT_COLOR,
placeholder_text_color="#888",
)

# Entry field for the desired start time
self.entry_time = ctk.CTkEntry(
master=self.input_frame,
placeholder_text="Start time (HH:MM:SS)",
fg_color=ENTRY_BG,
text_color=TEXT_COLOR,
placeholder_text_color="#888",
)

# Set up the discount selection menu
initial_option = self.discount_names[0] if self.discount_names else "No discount"
self.option_var = ctk.StringVar(value=initial_option)
self.option_menu = ctk.CTkOptionMenu(
master=self.input_frame,
# Provide loaded discount names as options
values=self.discount_names if self.discount_names else ["No discount"],
variable=self.option_var,
# Link the menu selection to the update_discount method
command=self.update_discount,
fg_color=ENTRY_BG,
button_color=ORANGE,
button_hover_color="#cc8400",
dropdown_fg_color=DARK_BG,
dropdown_text_color=TEXT_COLOR,
text_color=TEXT_COLOR,
)

# Create the search/submit button
self.submit_button = ctk.CTkButton(
master=self.input_frame,
text="Search",
fg_color=ORANGE,
hover_color="#cc8400",
text_color="black",
corner_radius=8,
# Command is initially set to the provided callback (or None)
command=self.submit_callback,
)
log.debug("Widgets created; initial option_menu value='%s'", initial_option)

def layout_widgets(self) -> None:
"""
Place components within the window/frame.
"""
log.debug("Laying out InputPanel widgets")
# Place the outer frame (with border) in the top-right corner
self.border_frame.place(relx=1.0, rely=0.0, anchor="ne", x=-10, y=10)
# Pack the inner frame inside the border frame
self.input_frame.pack(padx=2, pady=2)

# Pack input fields with padding
self.entry1.pack(pady=(12, 6), padx=12, fill="x")
self.entry2.pack(pady=6, padx=12, fill="x")
self.entry_time.pack(pady=6, padx=12, fill="x")
self.option_menu.pack(pady=6, padx=12, fill="x")
# Pack the submit button
self.submit_button.pack(pady=(6, 12), padx=12)
log.info("InputPanel layout complete")

def update_discount(self, selected_name: str) -> None:
"""
Update the discount percentage when a new option is selected.

:param selected_name: Selected discount name.
"""
# Retrieve the percentage value from the map, defaulting to 0 if not found
self.selected_discount = int(self.discount_map.get(selected_name, 0))
log.debug(
"Updated discount selection: name='%s', selected_discount=%d",
selected_name,
self.selected_discount,
)

def get_selected_discount(self) -> int:
"""
Return the currently selected discount percentage.

:return: Discount value (e.g., 25).
"""
log.debug("get_selected_discount -> %d", self.selected_discount)
return self.selected_discount

def set_submit_callback(self, callback: Callable[[], None] | None) -> None:
"""
Set the function to be called on "Search" button click.

:param callback: Event handler function.
"""
# Configure the button command, using a no-op lambda if the callback is None
self.submit_button.configure(command=callback or (lambda: None))
log.debug("Submit callback set: has_callback=%s", bool(callback))

def get_start_time_str(self) -> str:
"""
Return the value entered the start-time field.

:return: Time string in ``HH:MM:SS`` format (defaults to ``"05:00:00"`` if empty).
"""
# Get the text, strip whitespace, and default if empty
value = self.entry_time.get().strip() or config.DEFAULT_START_TIME
log.debug("get_start_time_str -> '%s'", value)
return value
Loading