Source code for autojob.coordinator.gui.structure_selection
"""Select structures for structure groups."""
import tkinter as tk
from tkinter import filedialog
from tkinter import ttk
from typing import TYPE_CHECKING
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 StructureSelectionFrame(ttk.Frame):
"""Select structures to use.
Attributes:
parent: The :class:`StructureSelectionPanel` in which the
`StructureSelectionFrame` resides.
lbf: The :class:`.widgets.ListboxFrame` listing structure
filenames.
button_frame: A :class:`tkinter.ttk.Frame` containing "add", "remove",
and "clear" buttons.
"""
def __init__(self, parent: "StructureSelectionPanel") -> None:
"""Initialize a `StructureSelectionFrame`.
Args:
parent: The :class:`StructureSelectionPanel` in which the
`StructureSelectionFrame` resides.
"""
super().__init__(parent)
self.parent: StructureSelectionPanel = parent
self._structures: list[str] = []
self.lbf: widgets.ListboxFrame = widgets.ListboxFrame(
self, x_stretch=True
)
self.button_frame: ttk.Frame = self.create_button_frame()
self.organize()
@property
def structures(self) -> list[str]:
"""The list of structure filenames."""
return self._structures.copy()
@structures.setter
def structures(self, new_structures: list[str]) -> None:
self._structures = new_structures
self.parent.parent.load()
[docs]
def load(self) -> None:
"""Reload the structures displayed in the Listbox."""
# Clear list
end = self.lbf.listbox.size() - 1
self.lbf.listbox.delete(0, end)
# Add new structures
for structure in self._structures:
self.lbf.listbox.insert(tk.END, structure)
[docs]
def create_button_frame(self) -> ttk.Frame:
"""Create "add", "remove", and "clear" buttons."""
subframe: ttk.Frame = ttk.Frame(self)
# Configure "add" button
add_button = ttk.Button(
subframe, text="add", command=self.add_structures
)
# Configure "remove" button
rm_button = ttk.Button(
subframe, text="remove", command=self.remove_structures
)
# Configure "clear" button
clr_button = ttk.Button(
subframe, text="clear", command=self.clear_structures
)
add_button.grid(column=0, row=0)
rm_button.grid(column=1, row=0)
clr_button.grid(column=2, row=0)
return subframe
[docs]
def add_structures(self) -> None:
"""Add structure filenames from a file dialog."""
filetypes = (
("ase trajectories", "*.traj"),
("CONTCARs", "CONTCAR"),
("POSCARs", "POSCAR"),
("VASP output files", "*.xml"),
("WAVECARs", "WAVECAR"),
("All files", "*.*"),
)
filenames = filedialog.askopenfilenames(
title="Select structures",
# TODO: set to "./" for distribution
initialdir="./tests/test_structure_files",
filetypes=filetypes,
)
# Add new structures and remove duplicates
structures = self.structures
structures.extend(filenames)
structures = list(dict.fromkeys(structures))
self.structures = structures
[docs]
def remove_structures(self) -> None:
"""Remove selected structures from the Listbox."""
indices_to_remove = self.lbf.listbox.curselection()
end = self.lbf.listbox.size() - 1
copy = self.lbf.listbox.get(0, end)
structures_to_remove = [copy[i] for i in indices_to_remove]
new_structures: list[str] = []
if structures_to_remove:
for structure in self._structures:
if structure not in structures_to_remove:
new_structures.append(structure)
# Defer to structures 'property' to handle updating
self.structures = new_structures
[docs]
def clear_structures(self) -> None:
"""Remove all structures from the Listbox."""
self.lbf.listbox.selection_set(0, self.lbf.listbox.size() - 1)
self.remove_structures()
[docs]
def organize(self) -> None:
"""Pack frames."""
self.lbf.pack(expand=True, fill=tk.X, side=tk.TOP)
self.button_frame.pack(fill=tk.X, side=tk.TOP)
[docs]
class GroupButtonFrame(ttk.Frame):
"""Define StructureGroups.
Attributes:
parent: The :class:`StructureSelectionPanel` in which the
`GroupButtonFrame` resides.
create_group_entry: The :class:`tkinter.ttk.Entry` for entering
structure group names.
create_group_button: The :class:`tkinter.ttk.Entry` for creating
structure groups.
entry_var: The :class:`tkinter.StringVar` storing the name of the
structure group to create.
del_group_cb: The :class:`tkinter.Combobox` for selecting which
structure group to delete.
del_group_button: The :class:`tkinter.ttk.Entry` for deleting
structure groups.
cb_var1: The :class:`tkinter.StringVar` storing the name of the
structure group to delete.
groups_cb: The :class:`tkinter.Combobox` for selecting which
structure group structures are to be added to.
add_to_group_button: The :class:`tkinter.ttk.Button` for adding
selected structures to structure groups.
cb_var2: The :class:`tkinter.StringVar` storing the name of the
structure group to which selected structures will be added.
"""
def __init__(self, parent: "StructureSelectionPanel") -> None:
"""Initialize a `StructureSelectionFrame`.
Args:
parent: The :class:`StructureSelectionPanel` in which the
`GroupButtonFrame` resides.
"""
super().__init__(parent)
self.parent: StructureSelectionPanel = parent
self._structure_groups: dict[str, groups.StructureGroup] = {}
(
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_var1,
) = self.create_del_group()
(
self.groups_cb,
self.add_to_group_button,
self.cb_var2,
) = self.create_add_to_group()
self.organize()
@property
def structure_groups(self) -> dict[str, groups.StructureGroup]:
"""A mapping from structure group names to structure filenames."""
return self._structure_groups.copy()
@structure_groups.setter
def structure_groups(
self, new_groups: dict[str, groups.StructureGroup]
) -> None:
self._structure_groups = new_groups
self.parent.parent.load()
[docs]
def create_create_group(
self,
) -> tuple[ttk.Entry, ttk.Button, tk.StringVar]:
"""Create the GUI elements for creating structure groups."""
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 structure group."""
new_group_name = self.entry_var.get()
if new_group_name == "":
return
if new_group_name not in self._structure_groups:
structure_groups = self.structure_groups
structure_groups[new_group_name] = groups.StructureGroup()
self.structure_groups = structure_groups
colour = ttk.Style().lookup("TEntry", "foreground")
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]:
"""Create the GUI elements for deleting structure groups."""
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 structure group."""
del self._structure_groups[self.cb_var1.get()]
self.parent.parent.load()
[docs]
def create_add_to_group(
self,
) -> tuple[ttk.Combobox, ttk.Button, tk.StringVar]:
"""Create the GUI elements for updating structure groups."""
button = ttk.Button(
self, command=self.add_structures_to_group, text="Add to group"
)
var = tk.StringVar()
def reset_listbox(reason: str) -> bool:
if reason != "forced":
self.parent.group_view_frame.load()
return True
cmd = self.register(reset_listbox)
combo_box = ttk.Combobox(
self,
state="readonly",
textvariable=var,
validate="focus",
validatecommand=(cmd, "%V"),
width=8,
)
return combo_box, button, var
[docs]
def add_structures_to_group(self) -> None:
"""Add selected structures to the selected structure group."""
listbox = self.parent.structure_selection_frame.lbf.listbox
indices_to_add = listbox.curselection()
if indices_to_add:
end = listbox.size() - 1
copy = listbox.get(0, end)
structures_to_add = [copy[i] for i in indices_to_add]
try:
active_group = self._structure_groups[self.cb_var2.get()]
active_group.add_structures(structures_to_add)
self.parent.parent.load()
except KeyError:
pass
[docs]
def load(self) -> None:
"""Update the structures in each structure group."""
structures = self.parent.structure_selection_frame.structures
# Update groups if structures have changed
for group in self._structure_groups.values():
indices_to_remove: list[int] = []
for i, structure in enumerate(group.structures):
if str(structure) not in structures:
indices_to_remove.append(i)
group.remove_structures(indices_to_remove)
self.reset_cbs()
[docs]
def reset_cbs(self) -> None:
"""Reset the values displayed by the ``Combobox`` es."""
self.del_group_cb["values"] = validation.alphanum_sort(
list(self._structure_groups)
)
self.groups_cb["values"] = validation.alphanum_sort(
list(self._structure_groups)
)
if self._structure_groups:
if self.del_group_cb.current() == -1:
self.del_group_cb.current(0)
if self.groups_cb.current() == -1:
self.groups_cb.current(0)
else:
self.del_group_cb.set("")
self.groups_cb.set("")
[docs]
def organize(self) -> None:
"""Grid widgets."""
self.create_group_entry.grid(column=0, row=0, sticky=tk.W + tk.E)
self.create_group_button.grid(column=1, row=0, sticky=tk.W + tk.E)
self.del_group_cb.grid(column=0, row=1, sticky=tk.W + tk.E)
self.del_group_button.grid(column=1, row=1, sticky=tk.W + tk.E)
self.groups_cb.grid(column=0, row=2, sticky=tk.W + tk.E)
self.add_to_group_button.grid(column=1, row=2, sticky=tk.W + tk.E)
(_, rows) = self.grid_size()
for i in range(rows):
self.rowconfigure(i, weight=1)
[docs]
class GroupViewFrame(ttk.Frame):
"""View the structures in each structure group.
Attributes:
parent: The :class:`StructureSelectionPanel` in which the
``GroupViewFrame`` resides.
lbf: The :class:`.widgets.ListboxFrame` listing structure
filenames.
button_frame: A :class:`tkinter.ttk.Frame` containing "remove"
and "clear" buttons.
"""
def __init__(self, parent) -> None:
"""Initialize a ``GroupViewFrame``.
Args:
parent: The :class:`StructureSelectionPanel` in which the
``GroupViewFrame`` resides.
"""
super().__init__(parent)
self.parent: StructureSelectionPanel = parent
self.lbf: widgets.ListboxFrame = widgets.ListboxFrame(
self, x_stretch=True
)
self.button_frame: ttk.Frame = self.create_button_frame()
self.organize()
[docs]
def create_button_frame(self) -> ttk.Frame:
"""Create "remove" and "clear" buttons."""
subframe: ttk.Frame = ttk.Frame(self)
cmd1 = self.remove_structures_from_group
rm_button = ttk.Button(subframe, command=cmd1, text="remove")
cmd2 = self.clear_structures_from_group
clr_button = ttk.Button(subframe, command=cmd2, text="clear")
placeholder = ttk.Label(subframe, width=10)
placeholder.grid(column=0, row=0)
rm_button.grid(column=1, row=0)
clr_button.grid(column=2, row=0)
return subframe
[docs]
def remove_structures_from_group(self) -> None:
"""Remove selected structures from the selected structure group."""
indices_to_remove = self.lbf.listbox.curselection()
group = self.parent.group_button_frame.cb_var2.get()
self.parent.group_button_frame.structure_groups[
group
].remove_structures(indices_to_remove)
self.load()
[docs]
def clear_structures_from_group(self) -> None:
"""Remove all structures from the selected structure group."""
self.lbf.listbox.selection_set(0, self.lbf.listbox.size() - 1)
self.remove_structures_from_group()
[docs]
def load(self) -> None:
"""Reload the displayed structures in the selected structure group."""
displayed_group = self.parent.group_button_frame.cb_var2.get()
self.lbf.clear_listbox()
if displayed_group:
structures = self.parent.group_button_frame.structure_groups[
displayed_group
].structures
for structure in structures:
self.lbf.listbox.insert(tk.END, structure)
[docs]
def organize(self) -> None:
"""Pack frames."""
self.lbf.pack(expand=True, fill=tk.X, side=tk.TOP)
self.button_frame.pack(fill=tk.X, side=tk.TOP)
[docs]
class StructureSelectionPanel(ttk.LabelFrame):
"""A ``LabelFrame`` for selecting structures and creating structure groups.
Attributes:
parent: The :class:`StructureSelectionTab` in which the
``StructureSelectionPanel`` resides.
structure_selection_frame: The frame for selecting structures.
group_button_frame: The frame for creating and deleting structure groups.
group_view_frame: The frame for displaying the structures of the
selected group.
"""
def __init__(self, parent: "StructureSelectionTab") -> None:
"""Initialize a ``StructureSelectionPanel``.
Args:
parent: The :class:`StructureSelectionTab` in which the
``StructureSelectionPanel`` resides.
"""
super().__init__(parent, text="Create structure groups")
self.parent: StructureSelectionTab = parent
self.structure_selection_frame = StructureSelectionFrame(self)
self.group_button_frame = GroupButtonFrame(self)
self.group_view_frame = GroupViewFrame(self)
self.organize()
[docs]
def organize(self) -> None:
"""Pack frames."""
self.structure_selection_frame.pack(
expand=True, fill=tk.Y, side=tk.LEFT
)
self.group_button_frame.pack(fill=tk.Y, side=tk.LEFT)
self.group_view_frame.pack(expand=True, fill=tk.Y, side=tk.LEFT)
[docs]
def load(self) -> None:
"""Load frames."""
self.structure_selection_frame.load()
self.group_button_frame.load()
self.group_view_frame.load()
[docs]
class StructureSelectionTab(ttk.Frame):
"""A tab for selecting structures and creating structure groups.
Attributes:
parent: The :class:`tkinter.ttk.Notebook` in which the
``StructureSelectionTab`` resides.
app: The running :class:`.gui.GUI` instance.
panel: The contained :class:`StructureSelectionPanel`.
"""
def __init__(self, main_app: "gui.GUI") -> None:
"""Initialize a ``StructureSelectionTab``.
Args:
main_app: The running :class:`.gui.GUI` instance.
"""
super().__init__(main_app.notebook)
self.parent: ttk.Notebook = main_app.notebook
self.app: gui.GUI = main_app
self.panel = StructureSelectionPanel(self)
self.organize()
[docs]
def organize(self) -> None:
"""Pack panel."""
self.panel.pack(fill=tk.X, ipadx=10, ipady=10, side=tk.TOP)