Source code for autojob.coordinator.gui.submission_configuration
"""GUI elements for the submission configuration panel.
Descriptions of exported classes:
- GroupButtonFrame: a :class:`.ttk.Frame` subclass containing a button which
handles the logic of creating submission parameter groups.
- GroupSummary: :class:`tkinter.TopLevel` subclass used to display a summary
for a submission parameter group.
- SelectionPanel: :class:`.ttk.LabelFrame` subclass used as base class for
submission configuration panels (i.e., :class:`StructureSelectionPanel`,
:class:`ValueSelectionPanel`).
- SelectionFrame: :class:`.ttk.Frame` subclass used to display values to be
selected.
- SpecButton: :class:`.ttk.Button` subclass that adds selected values from
:class:`SelectionFrame` to :class:`ViewFrame`.
- ViewFrame: :class:`.ttk.Frame` subclass used to display selected values.
- ButtonFrame: :class:`.ttk.Frame` subclass for adding and removing values
from :class:`ViewFrame`.
- ParameterSelectionCombobox: :class:`.ttk.Combobox` subclass for adding
specifications to submission parameter groups.
- StructureSelectionPanel: :class:`SelectionPanel` subclass for selecting
structures for submission parameter group.
- ValueSelectionPanel: :class:`SelectionPanel` subclass for selecting
parameter values for submission parameter group.
- AddToGroupFrame: :class:`.ttk.Frame` subclass for adding new specifications
to submission parameter groups.
- SubmissionConfigurationTab: :class:`.ttk.Frame` subclass for configuring
submission parameters.
"""
import pathlib
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from typing import Any
from autojob.coordinator import coordinator
from autojob.coordinator import job
from autojob.coordinator import validation
from autojob.coordinator.gui import groups
from autojob.coordinator.gui import widgets
if TYPE_CHECKING:
from autojob.coordinator.gui import gui
[docs]
class GroupButtonFrame(ttk.Frame):
"""A container frame for `ttk.GroupButton` s.
Attributes:
parent: The :class:`~SubmissionConfigurationTab` in which the
`GroupButtonFrame` resides.
create_group_entry: The :class:`.ttk.Entry` widget used to enter the
group name.
create_group_button: The :class:`.ttk.Button` widget used to enter the
group name.
entry_var: The :class:`tkinter.StringVar` storing the group name.
del_group_cb: The :class:`.ttk.Combobox` used to select a group to
delete.
del_group_button: The :class:`.ttk.Button` used to delete a group.
cb_var: The :class:`tkinter.StringVar` referencing the group to delete.
view_group_button: The :class:`.ttk.Button` used to view all
submission groups that have been created.
group_summary: A :class:`~GroupSummary` object used to view the groups
that have been created.
"""
def __init__(self, parent: "SubmissionConfigurationTab") -> None:
"""Initialize a ``GroupButtonFrame``.
Args:
parent: The :class:`~SubmissionConfigurationTab` in which the
``GroupButtonFrame`` resides.
"""
super().__init__(parent)
self.parent: SubmissionConfigurationTab = parent
self._submission_parameter_groups: dict[
str, groups.SubmissionParameterGroup
] = {}
(
self.create_group_entry,
self.create_group_button,
self.entry_var,
) = self.create_create_group()
(
self.del_group_cb,
self.del_group_button,
self.cb_var,
) = self.create_del_group()
(self._placeholder, self.view_group_button) = self.create_view_group()
self.group_summary: tk.Toplevel | None = None
self.organize()
# TODO: confirm type
@property
def submission_parameter_groups(
self,
) -> dict[str, groups.SubmissionParameterGroup]:
"""A map from group names to submission parameter groups."""
return self._submission_parameter_groups.copy()
# TODO: confirm type
@submission_parameter_groups.setter
def submission_parameter_groups(
self, new_groups: dict[str, list[str]]
) -> None:
"""Set the submission parameter groups.
The ``GroupButtonFrame`` is reloaded after setting.
Args:
new_groups: WIP
"""
self._submission_parameter_groups = new_groups
self.parent.load()
[docs]
def create_create_group(
self,
) -> tuple[ttk.Entry, ttk.Button, tk.StringVar]:
"""Create the widgets for creating submission parameter groups.
Returns:
A 3-tuple (``entry``, ``button``, ``var``). ``entry`` is the
:class:`.ttk.Entry` widget in which one specifies the name of a
new submission parameter group. ``button`` is a
:class:`.ttk.Button` used to finalize submission parameter group
creation. ``var`` is the :class:`tkinter.StringVar` associated
with ``entry``.
"""
var: tk.StringVar = tk.StringVar()
entry = ttk.Entry(self, textvariable=var, width=8)
button = ttk.Button(
self, command=self.create_group, text="Create group"
)
return entry, button, var
[docs]
def create_group(self) -> None:
"""Create a submission parameter group.
The text in :meth:`GroupButtonFrame.create_group_entry` is coloured red if a
submission parameter group with the same name already exists.
"""
new_group = self.entry_var.get()
if new_group == "":
return
if new_group not in list(self._submission_parameter_groups):
self._submission_parameter_groups[new_group] = (
groups.SubmissionParameterGroup()
)
colour = ttk.Style().lookup("TEntry", "foreground")
self.parent.load()
else:
colour = "red"
style = ttk.Style()
style.configure("group.TEntry", foreground=colour)
self.create_group_entry.configure(style="group.TEntry")
[docs]
def create_del_group(
self,
) -> tuple[ttk.Combobox, ttk.Button, tk.StringVar]:
"""Creates a group of widgets for deleting submission parameter groups.
Returns:
A 3-tuple (``combo_box``, ``button``, ``var``). ``combo_box`` is a
:class:`.ttk.Combobox` used to select a submission parameter group
to delete. ``button`` is a :class:`.ttk.Button` that when pressed,
deletes a group. ``var`` is a :class:`tkinter.StringVar` containing
the name of the selected group.
"""
var = tk.StringVar()
combo_box = ttk.Combobox(
self,
state="readonly",
textvariable=var,
width=8,
)
button = ttk.Button(self, command=self.del_group, text="Delete group")
return combo_box, button, var
[docs]
def del_group(self) -> None:
"""Delete a submission parameter group."""
to_del = self.cb_var.get()
del self._submission_parameter_groups[to_del]
[docs]
def create_view_group(self) -> tuple[ttk.Frame, ttk.Button]:
"""Create a group of widgets for viewing submission parameter groups.
Returns:
A tuple (``placeholder``, ``button``). ``placeholder`` is an empty
:class:`.ttk.Frame` used for alignment. ``button`` is a
:class:`.ttk.Button` used to view existing submission parameter
groups.
"""
placeholder = ttk.Frame(self)
button = ttk.Button(self, command=self.view_groups, text="View groups")
return placeholder, button
[docs]
def view_groups(self) -> None:
"""View created submission parameter groups in a new window."""
self.group_summary = GroupSummary(self)
[docs]
def load(self) -> None:
"""Load the widgets in ``GroupButtonFrame`` with updated data."""
self.del_group_cb["values"] = validation.alphanum_sort(
self._submission_parameter_groups
)
if self._submission_parameter_groups:
if self.del_group_cb.current() == -1:
self.del_group_cb.current(0)
else:
self.del_group_cb.set("")
if self.group_summary:
self.group_summary.load()
[docs]
def update_groups(
self, group: str, new_group: dict[str, dict[str, list[str]]]
) -> None:
"""Update the selected group with the new spec.
Args:
group: The name of the active submission parameter group.
new_group: A dictionary mapping structures to
mappings from calculation parameter names to values.
"""
self._submission_parameter_groups[group].update(new_group)
[docs]
def organize(self) -> None:
"""Organize ``ttk.Entries`` and ``ttk.Buttons`` in 2x3 grid."""
self.create_group_entry.grid(
column=0, padx=40, row=0, sticky=tk.W + tk.E
)
self.create_group_button.grid(
column=0, padx=40, row=1, sticky=tk.W + tk.E
)
self.del_group_cb.grid(column=1, padx=40, row=0, sticky=tk.W + tk.E)
self.del_group_button.grid(
column=1, padx=40, row=1, sticky=tk.W + tk.E
)
self._placeholder.grid(column=2, padx=40, row=0, sticky=tk.W + tk.E)
self.view_group_button.grid(
column=2, padx=40, row=1, sticky=tk.W + tk.E
)
(cols, _) = self.grid_size()
for i in range(cols):
self.columnconfigure(i, weight=1)
[docs]
class GroupSummary(tk.Toplevel):
"""View a summary of all created submission parameter groups.
Attributes:
controller: The :class:`GroupButtonFrame` responsible for launching
the ``GroupSummary``.
tbf: The :class:`.widgets.TreeviewFrame` containing the summary of all
submission parameter groups.
"""
def __init__(self, controller: GroupButtonFrame) -> None:
"""Initialize a ``GroupSummary``.
Args:
controller: The :class:`GroupButtonFrame` responsible for launching
the `GroupSumma`ry`.
"""
super().__init__(height=300, padx=20, pady=20, width=400)
self.controller: GroupButtonFrame = controller
self.tbf = widgets.TreeviewFrame(self)
self.tbf.treeview.pack(fill=tk.BOTH)
self.populate()
self.title("Parameter Groups")
self.protocol("WM_DELETE_WINDOW", self.on_close)
self.tbf.pack(expand=True, fill=tk.BOTH, side=tk.TOP)
self.lift()
# TODO: refactor
[docs]
def populate(self) -> None:
"""Populate ``GroupSummary.tbf`` with each submission parameter group."""
self.tbf.clear_treeview()
submission_parameter_groups: dict[
str, groups.SubmissionParameterGroup # groups
] = self.controller.submission_parameter_groups
# ? maybe use itertools.groupby?
for group in validation.alphanum_sort(submission_parameter_groups):
group_iid = self.tbf.treeview.insert("", "end", text=group)
structures = list(submission_parameter_groups[group].values)
structures.sort()
for structure in structures:
structure_iid = self.tbf.treeview.insert(
group_iid, "end", text=str(structure)
)
params = submission_parameter_groups[group].values[structure]
for param in params:
param_iid = self.tbf.treeview.insert(
structure_iid, "end", text=str(param)
)
values = [
str(x)
for x in submission_parameter_groups[group].values[
structure
][param]
]
values = validation.alphanum_sort(values)
for value in values:
self.tbf.treeview.insert(param_iid, "end", text=value)
[docs]
def load(self) -> None:
"""Reload the displayed submission parameter groups."""
self.populate()
[docs]
def on_close(self) -> None:
"""Delete ``GroupSummary``."""
self.controller.group_summary = None
self.destroy()
[docs]
class SelectionPanel(ttk.LabelFrame):
"""Select and view items.
Attributes:
parent: The :class:`SubmissionConfigurationTab` to which the
:class:`SelectionPanel` belongs.
selection_frame: A :class:`SelectionFrame` from which users select
items.
view_frame: A :class:`ViewFrame` displaying selected items.
button_frame: A :class:`.ttk.Frame` with :class:`.ttk.Button` s for item removing and
clearing.
spec_button: A :class:`.ttk.Button` controlling spec addition.
"""
def __init__(self, parent: Any, text: str) -> None:
"""Initialize a ``SelectionPanel``.
Args:
parent: The :class:`SubmissionConfigurationTab` to which the
:class:`SelectionPanel` belongs.
text: The text used to indicate what is being selected.
"""
super().__init__(parent, text="Select " + text)
self.parent: SubmissionConfigurationTab = parent
self.selection_frame = SelectionFrame(self, text)
self.view_frame = ViewFrame(self)
self.button_frame = ButtonFrame(self)
self.spec_button = SpecButton(self)
self.organize()
[docs]
def organize(self) -> None:
"""Organize buttons and frames into 1x4 grid."""
self.selection_frame.grid(column=0, row=0, sticky=tk.W + tk.E)
self.spec_button.grid(column=1, row=0, padx=10)
self.view_frame.grid(column=2, row=0, sticky=tk.W + tk.E)
self.button_frame.grid(column=3, row=0, padx=10)
self.columnconfigure(0, weight=1)
self.columnconfigure(2, weight=1)
[docs]
def load(self) -> None:
"""Load the selection and view frames."""
self.selection_frame.load()
self.view_frame.load()
# TODO: Rename to FilterFrame, tbf -> tvf
# ? What does select_from do?
[docs]
class SelectionFrame(ttk.Frame):
"""Create task filters.
Attributes:
parent: The :class:`StructureSelectionPanel` in which structures in
the filter are selected.
select_from: A string indicating from where the values within the frame
are to be populated. If ``"values"``, then the values are obtained
from calculation parameter groups. If ``"structure"``, then the
values are obtained from the structures.
tbf: A :class:`TreeviewFrame` containing the values if
``SelectionFrame.select_from = "values"``, else ``None``.
lbf: A :class:`ListboxFrame` containing the values if
``SelectionFrame.select_from = "structure"``, else ``None``.
"""
def __init__(
self, parent: "StructureSelectionPanel", select_from: str
) -> None:
"""Initialize a ``SelectionFrame``.
Args:
parent: The :class:`StructureSelectionPanel` in which structures in
the filter are selected.
select_from: A string indicating from where the values within the
frame are to be populated. If ``"values"``, then the values are
obtained from calculation parameter groups. If ``"structure"``, then
the values are obtained from the structures.
"""
super().__init__(parent)
self.parent = parent
self.select_from = select_from
# self.tbf = self.lbf = None
if self.select_from == "values":
self.tbf = widgets.TreeviewFrame(self)
self.tbf.treeview.configure(height=7)
else:
self.lbf = widgets.ListboxFrame(self, x_stretch=True)
self.lbf.listbox.configure(height=7)
self.organize()
@property
def items(self) -> list[str] | dict[str, list[str]]:
"""Displayed items (``SelectionFrame.lbf`` or ``SelectionFrame.lbf``).
Raises:
AttributeError: Invalid value of ``SelectionFrame.select_from``.
Returns:
A list of strings representing structure paths
(if ``SelectionFrame.select_from == "structures"``) or a dictionary
mapping parameter names to their values (if
``SelectionFrame.select_from == "values"``)
"""
if self.select_from == "structures":
return self.structures
if self.select_from == "values":
return self.values
msg = f'Invalid value of "select_from" attribute:{self.select_from}'
raise AttributeError(msg)
@property
def structures(self) -> list[str]:
"""Returns the structures for which parameters have been specified.
Returns:
A list of strings representing the structures for which parameters
have been specified.
"""
app: gui.GUI = self.parent.parent.app
structure_groups: dict[str, list[str]] = (
app.coordinator.structure_groups
)
structures: list[str] = []
for group in iter(structure_groups.values()):
for structure in group.structures:
structures.append(structure)
return [str(structure) for structure in dict.fromkeys(structures)]
@property
def values(self) -> dict[str, list[str]]:
"""A map from calculation parameter names to a list of their values."""
values = []
app: gui.GUI = self.parent.parent.app
structures = [
pathlib.Path(x)
for x in self.parent.parent.panels[0].view_frame.lbf.items
]
groups_with_structures = app.coordinator.structure_groups_with(
structures
)
cdr: coordinator.Coordinator = self.parent.parent.app.coordinator
calc_params: list[job.CalculationParameter] = []
for calc_param in cdr.calc_params_for(structures):
param = cdr.calc_param_from(calc_param, groups_with_structures)
if param not in calc_params:
calc_params.append(param)
values = app.coordinator.calc_param_values_for(structures, calc_params)
return values
# TODO: change check to "is None"
[docs]
def load(self) -> None:
"""Reload displayed contents of ``Listbox`` or ``Treeview``."""
if hasattr(self, "lbf"):
self.lbf.clear_listbox()
# Add new items
for item in self.items:
self.lbf.listbox.insert(tk.END, item)
elif hasattr(self, "tbf"):
self.tbf.clear_treeview()
# Add new items
values = self.items
for param in iter(values):
iid = self.tbf.treeview.insert("", "end", text=param)
for value in values[param]:
self.tbf.treeview.insert(iid, "end", text=str(value))
[docs]
def organize(self) -> None:
"""Pack ``ListboxFrame`` or ``TreeviewFrame``."""
# TODO: refer to ttk.TreeView if select_from == 'values'
if hasattr(self, "lbf"):
self.lbf.pack(expand=True, fill=tk.X, side=tk.TOP)
elif hasattr(self, "tbf"):
self.tbf.pack(expand=True, fill=tk.X, side=tk.TOP)
[docs]
class SpecButton(ttk.Button):
"""`ttk.Button` that adds a spec.
Args:
parent: The parent :class:`~SelectionPanel`.
src: The widget containing the source frame. This is either a
:class:`.widgets.TreeviewFrame` (if :attr:`SpecButton.parent` is a
:class:`ValueSelectionPanel`) or a
:class:`.widgets.ListboxFrame` (if :attr:`SpecButton.parent` is a
:class:`StructureSelectionPanel`).
dest: The widget containing the destination frame. This is either a
:class:`.widgets.TreeviewFrame` (if :attr:`SpecButton.parent` is a
:class:`ValueSelectionPanel`) or a
:class:`.widgets.ListboxFrame` (if :attr:`SpecButton.parent` is a
:class:`StructureSelectionPanel`).
"""
def __init__(self, parent: "SelectionPanel") -> None:
"""Initialize ``SpecButton``.
Args:
parent: The parent :class:`~SelectionPanel`.
"""
super().__init__(parent, text="Add spec")
self.parent: SelectionPanel = parent
if isinstance(self.parent, ValueSelectionPanel):
self.src: widgets.TreeviewFrame | widgets.ListboxFrame = (
self.parent.selection_frame.tbf
)
self.dest: widgets.TreeviewFrame | widgets.ListboxFrame = (
self.parent.view_frame.tbf
)
else:
self.src: widgets.TreeviewFrame | widgets.ListboxFrame = (
self.parent.selection_frame.lbf
)
self.dest: widgets.TreeviewFrame | widgets.ListboxFrame = (
self.parent.view_frame.lbf
)
self.configure(command=self.add_spec)
[docs]
def add_spec(self) -> None:
"""Add the current parameter specification to the group."""
if isinstance(self.src, widgets.ListboxFrame):
self.add_from_listbox()
else:
self.add_from_treeview()
self.parent.parent.load()
[docs]
def add_from_listbox(self) -> None:
"""Add spec items from a ``ttk.Listbox``."""
indices_to_add = self.src.listbox.curselection()
if indices_to_add:
copy = self.src.listbox.get(0, self.src.listbox.size() - 1)
items_to_add = [copy[i] for i in indices_to_add]
end = self.dest.listbox.size() - 1
items_in_dest = self.dest.listbox.get(0, end)
for item in items_to_add:
if item not in items_in_dest:
self.dest.listbox.insert(tk.END, item)
[docs]
def add_from_treeview(self) -> None:
"""Add spec items from a ``ttk.Treeview``."""
items_to_add = set(self.src.treeview.selection())
for top_level_item in self.src.treeview.get_children():
if top_level_item in items_to_add:
children = list(self.src.treeview.get_children(top_level_item))
else:
children = list(
items_to_add.intersection(
self.src.treeview.get_children(top_level_item)
)
)
self.add_children(top_level_item, children)
[docs]
def add_children(self, top_level_item: str, children: list[str]) -> None:
"""Adds the contents of children items to a ``ttk.Treeview`` item.
Args:
top_level_item: The iid of the item under which the children will
be added.
children: A list of iids corresponding to items whose values are
to be added under ``top_level_item``.
"""
parent_text = self.src.treeview.item(top_level_item, "text")
values_to_add: list[str] = []
for item in children:
text = self.src.treeview.item(item, "text")
try:
if text not in self.dest.parent.tbf.items[parent_text]:
values_to_add.append(text)
except KeyError:
values_to_add.append(text)
if values_to_add:
values_to_add = validation.alphanum_sort(values_to_add)
iid = self.get_iid_in_dest(parent_text)
for text in values_to_add:
self.dest.treeview.insert(iid, "end", text=text)
[docs]
def get_iid_in_dest(self, parent_text) -> str:
"""Get the iid of an item in the destination ``ttk.Treeview``.
Args:
parent_text: A string representing the text of the item in the
destination.
Returns:
The iid of the item in the destination ``ttk.Treeview``.
"""
for item in self.dest.treeview.get_children():
text = self.dest.treeview.item(item, "text")
if text == parent_text:
return item
return self.dest.treeview.insert("", "end", text=parent_text)
[docs]
class ViewFrame(ttk.Frame):
"""A container ``ttk.Frame`` for viewing the values defining the spec.
Attributes:
parent: The :class:`SelectionPanel` in which the ``ViewFrame`` resides.
tbf: The :class:`.widgets.TreeviewFrame` containing the values
defining the spec. This is ``None`` if `parent` is a
:class:`StructureSelectionPanel`.
lbf: The :class:`.widgets.ListboxFrame` containing the values
defining the spec. This is `None` if ``parent`` is a
:class:`ValueSelectionPanel`.
"""
def __init__(self, parent: Any) -> None:
"""Initialize a ``ViewFrame``.
Args:
parent: The :class:`SelectionPanel` in which the ``ViewFrame`` resides.
"""
super().__init__(parent)
self.parent: SelectionPanel = parent
if isinstance(self.parent, ValueSelectionPanel):
self.tbf = widgets.TreeviewFrame(self)
self.tbf.treeview.configure(height=7)
else:
self.lbf = widgets.ListboxFrame(self, x_stretch=True)
self.lbf.listbox.configure(height=7)
self.organize()
[docs]
def load(self) -> None:
"""Update the ``ViewFrame`` to reflect changes to the allowed values.
This method updates the ``ViewFrame``, removing those items that are
no longer allowed due to updates to the items from which the spec
was created.
"""
if hasattr(self, "tbf"):
old_items: dict[str, dict] = self.tbf.items
self.tbf.clear_treeview()
src: dict[str, dict] = self.parent.selection_frame.tbf.items
for param, values in old_items.items():
if param in src:
iid = self.tbf.treeview.insert("", "end", text=param)
for val in iter(values):
if val in src[param]:
self.tbf.treeview.insert(iid, "end", text=val)
# Remove item related to 'param' if all children related to
# 'param' have been removed by updating
if not self.tbf.treeview.get_children(iid) and values:
self.tbf.treeview.delete(iid)
else:
old_items: list[str] = self.lbf.items
self.lbf.clear_listbox()
for item in old_items:
if item in self.parent.selection_frame.items:
self.lbf.listbox.insert(tk.END, item)
[docs]
def organize(self) -> None:
"""Pack frames."""
if hasattr(self, "tbf"):
self.tbf.pack(expand=True, fill=tk.BOTH, side=tk.TOP)
else:
self.lbf.pack(expand=True, fill=tk.BOTH, side=tk.TOP)
[docs]
class ButtonFrame(ttk.Frame):
"""A ``ttk.Frame`` containing ``ttk.Button`` s for editing specs.
Args:
parent: The :class:`SelectionPanel` containing the ``ButtonFrame``.
listbox: The :class:``tkinter.Listbox`` containing the values for the
spec. This is ``None`` if the parent :class:`SelectionFrame` has a
:class:`.widgets.TreeviewFrame`.
treeview: The :class:`.ttk.Treeview` containing the values for the
spec. This is `No`ne` if the parent :class:`SelectionFrame` has a
:class:`.widgets.ListboxFrame`.
buttons: A list of :class:`.ttk.Button` s. The first element is the "remove"
button. The second element is the "clear" button.
"""
def __init__(self, parent: "SelectionPanel") -> None:
"""Initialize a `ButtonFrame`.
Args:
parent: The :class:`SelectionPanel` containing the ``ButtonFrame``.
"""
super().__init__(parent)
self.parent: SelectionPanel = parent
if hasattr(self.parent.selection_frame, "lbf"):
self.listbox = self.parent.view_frame.lbf.listbox
else:
self.treeview = self.parent.view_frame.tbf.treeview
self.buttons: list[ttk.Button] = self.create_buttons()
[docs]
def create_buttons(self) -> list[ttk.Button]:
"""Create the "remove" and "clear" buttons.
Returns:
A list of :class:`.ttk.Button` s. The first element is the "remove"
button. The second element is the "clear" button.
"""
rm_button = ttk.Button(self, command=self.remove_items, text="remove")
clr_button = ttk.Button(self, command=self.clear_items, text="clear")
rm_button.grid(column=0, row=0)
clr_button.grid(column=0, row=1)
return [rm_button, clr_button]
[docs]
def remove_items(self) -> None:
"""Remove items from the ``Listbox``/``Treeview``."""
if hasattr(self.parent.selection_frame, "lbf"):
indices_to_remove = self.listbox.curselection()
end = self.listbox.size() - 1
items = self.listbox.get(0, end)
items_to_remove = [items[i] for i in indices_to_remove]
self.listbox.delete(0, end)
if items_to_remove:
for item in items:
if item not in items_to_remove:
self.listbox.insert(tk.END, item)
self.parent.parent.load()
else:
to_delete = self.treeview.selection()
for item in to_delete:
self.treeview.delete(item)
[docs]
def clear_items(self) -> None:
"""Remove all items from the ``Listbox``/``Treeview``."""
if hasattr(self.parent.selection_frame, "lbf"):
self.listbox.selection_set(0, self.listbox.size() - 1)
else:
self.treeview.selection_set(self.treeview.get_children())
self.remove_items()
[docs]
class StructureSelectionPanel(SelectionPanel):
"""A :class:`SelectionPanel` for creating specs by selecting structures."""
def __init__(self, parent: "SubmissionConfigurationTab") -> None:
"""Initialize a ``StructureSelectionPanel``.
Args:
parent: The :class:`SubmissionConfigurationTab` to which the
:class:`StructureSelectionPanel` belongs.
"""
super().__init__(parent, text="structures")
[docs]
class ValueSelectionPanel(SelectionPanel):
"""A :class:`SelectionPanel` for creating specs by selecting values."""
def __init__(self, parent) -> None:
"""Initialize a ``ValueSelectionPanel``.
Args:
parent: The :class:`SubmissionConfigurationTab` to which the
:class:`ValueSelectionPanel` belongs.
"""
super().__init__(parent, text="values")
[docs]
class AddToGroupFrame(ttk.Frame):
"""A container ``ttk.Frame`` for adding the current spec to a SubmissionGroup.
Attributes:
parent: The :class:`SubmissionConfigurationTab`.
combo_box: The :class:`.ttk.Combobox` for selecting the SubmissionGroup.
button: The :class:`.ttk.Button` used to add the current spec to the group.
var: A :class:`tkinter.StringVar` referencing the SubmissionGroup to which
the spec will be added.
"""
def __init__(self, parent: Any) -> None:
"""Initialize an ``AddToGroupFrame``.
Args:
parent: The :class:`SubmissionConfigurationTab`.
"""
super().__init__(parent)
self.parent: SubmissionConfigurationTab = parent
(
self.combo_box,
self.button,
self.var,
) = self.create_add_group()
self.organize()
[docs]
def create_add_group(
self,
) -> tuple[ttk.Combobox, ttk.Button, tk.StringVar]:
"""Create the widgets for adding the current spec to a SubmissionGroup.
Returns:
A 3-tuple (``combo_box``, ``button``, ``var``). ``combo_box`` is a
:class:`ttk.Combobox` used to select the SubmissionGroup to which the
spec will be added. ``button`` is the :class:`.ttk.Button` used to
add the current spec to a ``SubmissionGroup``. ``var`` is the
:class:`tkinter.StringVar` referencing the ``SubmissionGroup`` to
which the current spec will be added.
"""
var = tk.StringVar()
combo_box = ttk.Combobox(
self,
state="readonly",
textvariable=var,
width=8,
)
button = ttk.Button(
self,
command=self.add_specs_to_group,
text="Add all specs to group",
)
return combo_box, button, var
[docs]
def load(self) -> None:
"""Update :attr:`AddToGroupFrame.combo_box` values."""
self.combo_box["values"] = validation.alphanum_sort(
self.parent.button_frame.submission_parameter_groups
)
if self.parent.button_frame.submission_parameter_groups:
if self.combo_box.current() == -1:
self.combo_box.current(0)
else:
self.combo_box.set("")
# TODO: Refactor:
# TODO: only add GroupFilter; do not update existing
[docs]
def add_specs_to_group(self) -> None:
"""Add current spec to SubmissionGroup or update existing group.
This method uses the current spec to find all CalculatorParameterGroup
instances that satisfy the spec (structure and values of calculator
parameters) and add them to the SubmissionGroup.
"""
if self.var.get() != "":
group_name = self.var.get()
else:
return
new_p_groups = {}
# Collect all structures included in spec
p_group_structures: list[str] = []
for structure in self.parent.panels[0].view_frame.lbf.items:
p_group_structures.append(pathlib.Path(structure))
p_group_structures.sort()
# Collect all values included in spec
vals_panel: ValueSelectionPanel = self.parent.panels[1]
# Flatten dictionaries (temporary until fixed in Treeview.items)
# TODO: Replace with tbf.extract_values()?
new_specs: dict[
str, dict
] = { # parameter names # keys are parameter values
key: validation.iter_to_native(value)
for key, value in vals_panel.view_frame.tbf.items.items()
}
for structure in p_group_structures:
# Collect all calculation parameters in "new_specs" applicable to
# "structure"
param_names = list(
set(new_specs).intersection(
self.parent.app.coordinator.calc_params_for([structure])
)
)
# Structure groups containing 'structure'
s_groups: list[str] = (
self.parent.app.coordinator.structure_groups_with([structure])
)
param_names = validation.alphanum_sort(param_names)
params: list[job.CalculationParameter] = []
# Convert calculation parameter strings to CalculationParameter
# objects
for param_name in param_names:
params.append(
self.parent.app.coordinator.calc_param_from(
param_name, s_groups
)
)
# All valid parameter values for 'structure'
values: dict[
str, list[str] # parameter names, parameter values
] = self.parent.app.coordinator.calc_param_values_for(
[structure], params
)
param_values: dict[
str, list[str]
] = {} # parameter names, parameter values
# Collect all values of "structure" that are specified in
# "new_specs"
for param_name, param in zip(param_names, params, strict=False):
new_values = set(new_specs[param_name]).intersection(
values[param_name]
)
if len(new_values) == 0:
continue
new_values = [str(x) for x in new_values]
new_values = validation.alphanum_sort(new_values)
new_values = validation.iter_to_native(new_values)
param_values[param] = new_values
# Adding new values to 'p_groups'
if param_values:
new_p_groups[pathlib.Path(structure)] = param_values
self.parent.button_frame.update_groups(group_name, new_p_groups)
[docs]
def organize(self) -> None:
"""Pack ``Combbox`` and ``Button``."""
self.combo_box.pack(fill=tk.X, side=tk.TOP)
self.button.pack(fill=tk.X, side=tk.TOP)
[docs]
class SubmissionConfigurationTab(ttk.Frame):
"""A :class:`.ttk.Frame` for configuring SubmissionGroups.
Attributes:
parent: The :class:`.ttk.Notebook` to which the
``SubmissionConfigurationTab`` belongs.
app: The :class:`.gui.GUI` controlling the GUI.
button_frame: The :class:`GroupButtonFrame` used to create, delete,
and view SubmissionGroups.
panels: A list of :class:`SelectionPanel` s used to create
``GroupFilter`` s.
add_to_group_frame: A :class:`AddToGroupFrame` used to add a
``GroupFilter`` to a SubmissionGroup.
"""
def __init__(self, main_app: "gui.GUI") -> None:
"""Initialize a ``SubmissionConfigurationTab``.
Args:
main_app: The :class:`.gui.GUI` controlling the GUI.
"""
super().__init__(main_app.notebook)
self.parent: ttk.Notebook = main_app.notebook
self.app = main_app
self.button_frame: GroupButtonFrame = GroupButtonFrame(self)
self.panels: list[SelectionPanel] = self.create_panels()
self.add_to_group_frame = AddToGroupFrame(self)
self.organize()
[docs]
def create_panels(self) -> list[SelectionPanel]:
"""Returns a list containing structure and value selection panels."""
panels: list[SelectionPanel] = []
panels.append(StructureSelectionPanel(self))
panels.append(ValueSelectionPanel(self))
return panels
[docs]
def organize(self) -> None:
"""Pack frames."""
self.button_frame.pack(side=tk.TOP)
for panel in self.panels:
panel.pack(fill=tk.X, padx=10, pady=5, side=tk.TOP)
self.add_to_group_frame.pack(side=tk.TOP)
[docs]
def load(self) -> None:
"""Reload frames."""
self.button_frame.load()
for panel in self.panels:
panel.load()
self.add_to_group_frame.load()