import tkinter as tk
from tkinter import Frame
from GUIBRUSHR.GUI.LAYOUT.ScaleManager import ScaleManager
from GUIBRUSHR.GUI.WIDGET.MyEntry import MyEntry
from GUIBRUSHR.GUI.WIDGET.MyLabel import MyLabel
from GUIBRUSHR.GUI.WIDGET.MyButton import MyButton
def _round_to_nearest(value, step=1.0):
"""
Round value to the nearest multiple of step using half-up rule.
Args:
value: The numeric value to round
step: The step size to round to (default: 1.0)
Returns:
The rounded value as a multiple of step
"""
return round(value / step) * step
[docs]
class StellarDialog(tk.Toplevel):
"""
Custom dialog for downloading stellar spectrum parameters.
This dialog asks whether to download a stellar spectrum and collects
stellar parameters (temperature, log gravity, metallicity) if the user
confirms. The parameters are validated, rounded to appropriate precision,
and returned in a formatted tuple.
Attributes:
stellar_teff: The initial stellar temperature value
result: Tuple containing the validated parameters or (None, None, None)
_entries: Dictionary mapping parameter labels to MyEntry widgets
_message_label: MyLabel widget displaying the main message
_no_button: MyButton widget for "No" action
_ok_button: MyButton widget for "OK" action
"""
# Constants for dialog styling
DIALOG_COLOR = "#FFFFFF"
BUTTON_COLOR = "#E0E0E0"
# Parameter configuration: (label, default_value, rounding_step)
PARAMETER_CONFIG = [
("Stellar Temperature [K]", None, 100), # default set dynamically
("Log Gravity [cm s⁻²]", "4.5", 0.5),
("Metallicity [Fe/H]", "0.0", 0.2),
]
[docs]
def __init__(self, parent, stellar_teff):
"""
Initialize the stellar parameter dialog.
Args:
parent: The parent widget
stellar_teff: Initial stellar temperature value
"""
super().__init__(parent)
self.stellar_teff = stellar_teff
self.result = (None, None, None)
self._entries = {}
self._setup_dialog_properties()
self._create_widgets()
self._setup_bindings()
self._setup_modal_behavior(parent)
def _setup_dialog_properties(self):
"""Configure basic dialog properties."""
self.title("Download stellar spectrum?")
self.resizable(False, False)
self.configure(bg=self.DIALOG_COLOR)
def _create_widgets(self):
"""Create and arrange all dialog widgets."""
self._create_message_label()
self._create_parameter_entries()
self._create_buttons()
def _create_message_label(self):
"""Create the main message label."""
self._message_label = MyLabel(
parent=self,
row=0,
column=0,
color=self.DIALOG_COLOR,
label_text="Stellar spectrum not present.\nDo you want to download it?",
font=ScaleManager.get().font_label_normal if ScaleManager.get() else ("Sans", 10, "normal"),
columnspan=2
)
def _create_parameter_entries(self):
"""Create entry fields for stellar parameters."""
for i, (label, default_value, _) in enumerate(self.PARAMETER_CONFIG, start=1):
# Set default value for temperature dynamically
if "Temperature" in label:
default_value = str(self.stellar_teff)
# Create entry with label
entry_widget = MyEntry(
parent=self,
row=i,
column=0,
text=default_value,
label_text=label,
color=self.DIALOG_COLOR,
columnspan=2,
entry_width=12
)
# Store reference to the entry widget for later access
self._entries[label] = entry_widget
def _create_buttons(self):
"""Create and arrange dialog buttons."""
# Create a frame to hold the buttons
button_frame = Frame(self, bg=self.DIALOG_COLOR)
button_frame.grid(row=4, column=0, columnspan=2, pady=(8, 10))
# Create "No" button
self._no_button = MyButton(
parent=button_frame,
row=0,
column=0,
text="No",
bg=self.BUTTON_COLOR,
command=self._handle_no_action,
color_panel=self.DIALOG_COLOR,
size_text=10
)
# Create "OK" button
self._ok_button = MyButton(
parent=button_frame,
row=0,
column=1,
text="OK",
bg=self.BUTTON_COLOR,
command=self._handle_ok_action,
color_panel=self.DIALOG_COLOR,
size_text=10
)
def _setup_bindings(self):
"""Setup keyboard bindings for the dialog."""
self.bind("<Return>", lambda event: self._handle_ok_action())
self.bind("<Escape>", lambda event: self._handle_no_action())
def _setup_modal_behavior(self, parent):
"""Configure modal dialog behavior."""
self.transient(parent)
self.grab_set()
parent.wait_window(self)
def _handle_no_action(self):
"""
Handle the "No" button click or Escape key press.
Sets result to (None, None, None) and closes the dialog.
"""
self.result = (None, None, None)
self.destroy()
def _handle_ok_action(self):
"""
Handle the "OK" button click or Enter key press.
Validates and processes the entered parameters, then closes the dialog.
If validation fails, behaves like "No" action.
"""
try:
# Process each parameter according to its configuration
processed_params = []
for label, _, rounding_step in self.PARAMETER_CONFIG:
raw_value = self._entries[label].get_value()
processed_value = self._process_parameter(
raw_value, label, rounding_step
)
processed_params.append(processed_value)
self.result = tuple(processed_params)
except ValueError:
# Invalid input → behave like "No" action
self.result = (None, None, None)
finally:
self.destroy()
def _process_parameter(self, raw_value, label, rounding_step):
"""
Process and format a single parameter value.
Args:
raw_value: The raw string value from the entry field
label: The parameter label for identification
rounding_step: The step size for rounding
Returns:
The processed parameter value
Raises:
ValueError: If the input cannot be converted to float
"""
numeric_value = float(raw_value)
rounded_value = _round_to_nearest(numeric_value, rounding_step)
# Special formatting for temperature (5-digit, zero-padded string)
if "Temperature" in label:
return f"{int(rounded_value):05d}"
if "Metallicity" in label:
if rounded_value > 0:
rounded_value = f"+{abs(rounded_value)}"
else:
rounded_value = f"-{abs(rounded_value)}"
# Return as float for other parameters
return rounded_value