Source code for ccu.fancyplots._gui.mechanism

"""GUI elements for defining reaction mechanisms.

Specifically, this module defines the :class:`StepPanel`, :class:`PathPanel`,
and :class:`MechanismSection` classes.
"""

import logging
import tkinter as tk
from tkinter import messagebox
from tkinter import ttk
from typing import TYPE_CHECKING
from typing import Literal

from ccu.fancyplots._gui.energy import EnergyWindow
from ccu.fancyplots._gui.frames import UpdatableFrame
from ccu.fancyplots.data import FEDData

if TYPE_CHECKING:
    from ccu.fancyplots._gui.root import FancyPlotsGUI

logger = logging.getLogger(__name__)


[docs] class StepPanel(ttk.LabelFrame): """Define the name and number of mechanism steps. Note that step names are stripped of all surrounding whitespace. Attributes: entry: The :class:`ttk.Entry` used to define mechanism steps. parent: The parent :class:`ccu.fancyplots._gui.mechanism.MechanismSection`. var: The :class:`tkinter.StringVar` for the :class:`ttk.Entry`. """ def __init__(self, parent: "MechanismSection", *args, **kwargs) -> None: """Create a subpanel for defining the mechanism steps. Args: parent: The containing :class:`.mechanism.MechanismSection`. *args: Positional arguments for :class:`.ttk.LabelFrame`. **kwargs: Keyword arguments for :class:`.ttk.LabelFrame`. """ text = "Mechanism Steps:" super().__init__(parent, *args, text=text, **kwargs) self.var = tk.StringVar(self) self.parent = parent cmd = self.register(self.update_data) self.entry = ttk.Entry( self, width=30, justify=tk.CENTER, textvariable=self.var, validate="focusout", validatecommand=cmd, ) self.entry.grid(row=1, column=1) @property def mechanism(self) -> list[str]: """The names used to define mechanism steps.""" return [x.strip() for x in self.entry.get().split(",") if x.strip()] @mechanism.setter def mechanism(self, new_value: list[str]) -> None: self.var.set(",".join(x.strip() for x in new_value))
[docs] def update_data(self) -> Literal[True]: """Update the mechanism data. Returns True so as to ensure that tkinter accepts the new input. Note: No validation is performed on the mechanism steps. """ return self.parent.update_data()
[docs] class PathPanel(ttk.LabelFrame): """Define the number of pathways. Attributes: parent: The parent :class:`ccu.fancyplots._gui.mechanism.MechanismSection`. spinbox: The :class:`tkinter.Spinbox` used to define the number of pathways. var: The :class:`tkinter.IntVar` for the :class:`tkinter.Spinbox`. """ def __init__(self, parent: "MechanismSection", *args, **kwargs) -> None: """Create a subpanel for defining the number of mechanism pathways. Args: parent: The containing :class:`.mechanism.MechanismSection`. *args: Positional arguments for :class:`.ttk.LabelFrame`. **kwargs: Keyword arguments for :class:`.ttk.LabelFrame`. """ text = "Number of Paths:" super().__init__(parent, *args, text=text, **kwargs) self.parent = parent self.var = tk.IntVar(self, 1) cmd = self.register(self.update_data) self.spinbox = tk.Spinbox( self, from_=1, to=30, textvariable=self.var, width=3, command=cmd, state="readonly", ) self.spinbox.pack(side="top", fill="both") @property def npaths(self) -> int: """The number of pathways.""" return self.var.get() @npaths.setter def npaths(self, new_value: int) -> None: self.var.set(new_value)
[docs] def update_data(self) -> None: """Update the mechanism data. Returns True so as to ensure that tkinter accepts the new input. Note: No validation is performed on the mechanism steps. """ _ = self.parent.update_data()
[docs] class MechanismSection(ttk.LabelFrame, UpdatableFrame): """GUI element for specifying mechanism free energies. Attributes: parent: The running :class:`.root.FancyPlotsGUI` instance. step_panel: A :class:`ttk.LabelFrame` for defining the names of mechanism steps. path_panel: A :class:`ttk.LabelFrame` for defining the number of pathways. gibbs_button: A :class:`ttk.Button` for launching the :class:`~ccu.fancyplots._gui.energy.EnergyWindow`. """ def __init__(self, parent: "FancyPlotsGUI", *args, **kwargs) -> None: """Create a subpanel for defining a reaction mechanism. Args: parent: The containing :class:`.mechanism.MechanismSection`. *args: Positional arguments for :class:`.ttk.LabelFrame`. **kwargs: Keyword arguments for :class:`.ttk.LabelFrame`. """ super().__init__( parent._frame, *args, text="Mechanism Design", **kwargs ) self.parent = parent self.step_panel = StepPanel(self) self.path_panel = PathPanel(self) self.gibbs_button = ttk.Button( self, text="Define Gibbs Free Energies", command=self.launch_energy_window, ) self._organize() self.diagram_data = FEDData( pathways=[], step_names=[], legend_entries=[] )
[docs] def launch_energy_window(self) -> None: """Launch a window for defining mechanism free energies.""" logger.debug("Launching energy window") energy_window = self.parent.windows["energy_window"] if energy_window: self.parent.windows["energy_window"] = energy_window.destroy() if not self.mechanism: messagebox.showerror( "Divisions not found!", "Please define the mechanism's divisions under 'Mechanism Steps'.", ) elif not self.npaths: messagebox.showerror( "The number of pathways is zero!", "Please define the mechanism's divisions in 'Full Mechanism " "Divisions' entry box.", ) else: self.parent.windows["energy_window"] = EnergyWindow(self)
@property def npaths(self) -> int: """The number of pathways.""" return self.path_panel.npaths @npaths.setter def npaths(self, new_value: int) -> None: self.path_panel.npaths = new_value @property def mechanism(self) -> list[str]: """The names used to define mechanism steps.""" return self.step_panel.mechanism @mechanism.setter def mechanism(self, new_value: list[str]) -> None: self.step_panel.mechanism = new_value
[docs] def update_data(self) -> Literal[True]: """Update the free energy diagram (meta)data. Note that this method returns True in order for validation via tkinter to permit the value within :attr:`.StepPanel.entry` to change. """ logger.debug("Saving mechanism data") new_pathways = [] new_legend_labels = [] nsteps = len(self.mechanism) # Update mechanism self.diagram_data["mechanism"] = self.mechanism # Update free energy data and legend entries for i in range(self.npaths): if i < len(self.diagram_data["energy_data"]): existing_data = self.diagram_data["energy_data"][i] new_data = [] for j in range(nsteps): if j < len(existing_data): new_data.append(existing_data[j]) else: new_data.append(None) new_pathways.append(new_data) else: new_pathways.append([None] * nsteps) if i < len(self.diagram_data["legend_labels"]): new_legend_labels.append(self.diagram_data["legend_labels"][i]) else: new_legend_labels.append(None) self.diagram_data["energy_data"] = new_pathways self.diagram_data["legend_labels"] = new_legend_labels return True
[docs] def update_frames(self) -> None: """Update the path and step panels.""" logger.debug( "Updating frames in %s.%s", __package__, __class__.__name__ ) mechanism = ",".join(step for step in self.diagram_data["mechanism"]) self.step_panel.var.set(mechanism) self.path_panel.var.set(len(self.diagram_data["energy_data"]))
[docs] def _organize(self) -> None: """Update the values in the step and path panels.""" self.step_panel.grid(row=1, column=1, columnspan=3, padx=5) self.path_panel.grid(row=1, column=4, padx=5) self.gibbs_button.grid( row=1, column=5, columnspan=2, padx=10, sticky=tk.NSEW )