Source code for simba.mixins.pop_up_mixin
__author__ = "Simon Nilsson; sronilsson@gmail.com"
import os
from tkinter import *
from tkinter import ttk
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
try:
from typing import Literal
except:
from typing_extensions import Literal
import PIL.Image
from PIL import ImageTk
from simba.mixins.config_reader import ConfigReader
from simba.ui.tkinter_functions import (DropDownMenu, Entry_Box, FileSelect,
SimbaButton, hxtScrollbar)
from simba.utils.checks import (check_float, check_instance, check_int,
check_str, check_valid_lst)
from simba.utils.enums import Formats, Options
from simba.utils.errors import CountError, NoFilesFoundError
from simba.utils.lookups import (get_color_dict, get_icons_paths,
get_monitor_info, get_named_colors)
from simba.utils.read_write import find_core_cnt
[docs]class PopUpMixin(object):
"""
Methods for pop-up windows in SimBA. E.g., common methods for creating pop-up windows with drop-downs,
checkboxes, entry-boxes, listboxes etc.
:param str title: Pop-up window title
:param Optional[configparser.Configparser] config_path: path to SimBA project_config.ini. If path, the project config is read in. If None, the project config is not read in.
:param tuple size: HxW of the pop-up window. The size of the pop-up window in pixels.
:param bool main_scrollbar: If True, the pop-up window is scrollable.
"""
def __init__(self,
title: str,
config_path: Optional[str] = None,
main_scrollbar: Optional[bool] = True,
size: Tuple[int, int] = (960, 720),
icon: Optional[str] = None):
self.root = Toplevel()
self.root.minsize(size[0], size[1])
self.root.wm_title(title)
self.root.lift()
if main_scrollbar:
self.main_frm = Canvas(hxtScrollbar(self.root))
else:
self.main_frm = Canvas(self.root)
self.main_frm.pack(fill="both", expand=True)
# self.main_frm.bind("<MouseWheel>", lambda event: on_mouse_scroll(event, self.main_frm))
# self.main_frm.bind("<Button-4>", lambda event: on_mouse_scroll(event, self.main_frm))
# self.main_frm.bind("<Button-5>", lambda event: on_mouse_scroll(event, self.main_frm))
self.palette_options = Options.PALETTE_OPTIONS.value
self.resolutions = Options.RESOLUTION_OPTIONS.value
self.shading_options = Options.HEATMAP_SHADING_OPTIONS.value
self.heatmap_bin_size_options = Options.HEATMAP_BIN_SIZE_OPTIONS.value
self.dpi_options = Options.DPI_OPTIONS.value
self.colors = get_named_colors()
self.colors_dict = get_color_dict()
self.cpu_cnt, _ = find_core_cnt()
self.menu_icons = get_icons_paths()
for k in self.menu_icons.keys():
self.menu_icons[k]["img"] = ImageTk.PhotoImage(image=PIL.Image.open(os.path.join(os.path.dirname(__file__), self.menu_icons[k]["icon_path"])))
if config_path:
ConfigReader.__init__(self, config_path=config_path, read_video_info=False)
if icon is not None:
try:
if icon in list(self.menu_icons.keys()):
self.root.iconphoto(False, self.menu_icons[icon]["img"])
except:
pass
[docs] def create_clf_checkboxes(self,
main_frm: Union[Frame, LabelFrame, Toplevel, Canvas],
clfs: List[str],
title: str = "SELECT CLASSIFIER ANNOTATIONS"):
"""
Creates a labelframe with one checkbox per classifier, and inserts the labelframe into the bottom of the pop-up window.
.. note::
Legacy. Use ``create_cb_frame`` instead.
"""
self.choose_clf_frm = LabelFrame(self.main_frm, text=title, font=Formats.FONT_HEADER.value)
self.clf_selections = {}
for clf_cnt, clf in enumerate(clfs):
self.clf_selections[clf] = BooleanVar(value=False)
self.calculate_distance_moved_cb = Checkbutton(self.choose_clf_frm, text=clf, font=Formats.FONT_REGULAR.value, variable=self.clf_selections[clf])
self.calculate_distance_moved_cb.grid(row=clf_cnt, column=0, sticky=NW)
self.choose_clf_frm.grid(row=self.children_cnt_main(), column=0, sticky=NW)
[docs] def create_cb_frame(self,
cb_titles: List[str],
main_frm: Optional[Union[Frame, Canvas, LabelFrame, ttk.Frame]] = None,
frm_title: Optional[str] = '',
idx_row: Optional[int] = -1,
command: Optional[Callable[[str], Any]] = None) -> Dict[str, BooleanVar]:
"""
Creates a labelframe with checkboxes and inserts the labelframe into a window.
.. image:: _static/img/create_cb_frame.png
:alt: Create cb frame
:width: 200
:align: center
.. note::
One checkbox will be created per ``cb_titles``. The checkboxes will be labeled according to the ``cb_titles``.
If checking/un-checking the box should have some effect, pass that function as ``command`` which takes the name of the checked/unchecked box.
:param Optional[Union[Frame, Canvas, LabelFrame, ttk.Frame]] main_frm: The pop-up window to insert the labelframe into.
:param List[str] cb_titles: List of strings representing the names of the checkboxes.
:param Optional[str] frm_title: Title of the frame.
:param Optional[int] idx_row: The location in main_frm to create the LabelFrame. If -1, then at the bottom.
:param Optional[Callable[[str], Any]] command: Optional function callable associated with checking/unchecking the checkboxes.
:return Dict[str, BooleanVar]: Dictionary holding the ``cb_titles`` as keys and the BooleanVar representing if the checkbox is ticked or not.
:example:
>>> PopUpMixin.create_cb_frame(cb_titles=['Attack', 'Sniffing', 'Rearing'], frm_title='My classifiers')
"""
check_valid_lst(data=cb_titles, source=f'{PopUpMixin.create_cb_frame.__name__} cb_titles', valid_dtypes=(str,), min_len=1)
check_int(name=f'{PopUpMixin.create_cb_frame.__name__} idx_row', value=idx_row, min_value=-1)
if main_frm is not None:
check_instance(source=f'{PopUpMixin.create_cb_frame.__name__} parent_frm', accepted_types=(Frame, Canvas, LabelFrame, ttk.Frame), instance=main_frm)
else:
main_frm = Toplevel(); main_frm.minsize(960, 720); main_frm.lift()
if idx_row == -1:
idx_row = int(len(list(main_frm.children.keys())))
cb_frm = LabelFrame(main_frm, text=frm_title, font=Formats.FONT_HEADER.value)
cb_dict = {}
for cnt, title in enumerate(cb_titles):
cb_dict[title] = BooleanVar(value=False)
if command is not None:
cb = Checkbutton(cb_frm, text=title, variable=cb_dict[title], font=Formats.FONT_REGULAR.value, command=lambda k=cb_titles[cnt]: command(k))
else:
cb = Checkbutton(cb_frm, text=title, variable=cb_dict[title], font=Formats.FONT_REGULAR.value)
cb.grid(row=cnt, column=0, sticky=NW)
cb_frm.grid(row=idx_row, column=0, sticky=NW)
# main_frm.mainloop()
return cb_dict
[docs] def place_frm_at_top_right(self, frm: Toplevel):
"""
Place a TopLevel tkinter pop-up at the top right of the monitor. Note: call before putting scrollbars or converting to Canvas.
"""
screen_width, screen_height = frm.winfo_screenwidth(), frm.winfo_screenheight()
window_width, window_height = frm.winfo_width(), frm.winfo_height()
x_position = screen_width - window_width
frm.geometry(f"{window_width}x{window_height}+{x_position}+{0}")
[docs] def create_dropdown_frame(self,
drop_down_titles: List[str],
drop_down_options: List[str],
frm_title: Optional[str] = '',
idx_row: Optional[int] = -1,
main_frm: Optional[Union[Frame, Canvas, LabelFrame, ttk.Frame]] = None) -> Dict[str, DropDownMenu]:
"""
Creates a labelframe with dropdowns.
.. image:: _static/img/create_dropdown_frame.png
:alt: Create dropdown frame
:width: 300
:align: center
:param Optional[Union[Frame, Canvas, LabelFrame, ttk.Frame]] main_frm: The pop-up window to insert the labelframe into. If None, one will be created.
:param List[str] drop_down_titles: The titles of the dropdown menus.
:param List[str] drop_down_options: The options in each dropdown. Note: All dropdowns must have the same options.
:param Optional[str] frm_title: Title of the frame.
:return Dict[str, BooleanVar]: Dictionary holding the ``drop_down_titles`` as keys and the drop-down menus as values.
:example:
>>> PopUpMixin.create_dropdown_frame(drop_down_titles=['Dropdown 1', 'Dropdown 2', 'Dropdown 2'], drop_down_options=['Option 1', 'Option 2'], frm_title='My dropdown frame')
"""
check_valid_lst(data=drop_down_titles, source=f'{PopUpMixin.create_dropdown_frame.__name__} drop_down_titles',
valid_dtypes=(str,), min_len=1)
check_valid_lst(data=drop_down_options, source=f'{PopUpMixin.create_dropdown_frame.__name__} drop_down_options', valid_dtypes=(str,), min_len=2)
check_int(name=f'{PopUpMixin.create_cb_frame.__name__} idx_row', value=idx_row, min_value=-1)
if main_frm is not None:
check_instance(source=f'{PopUpMixin.create_cb_frame.__name__} parent_frm', accepted_types=(Frame, Canvas, LabelFrame, ttk.Frame), instance=main_frm)
else:
main_frm = Toplevel(); main_frm.minsize(960, 720); main_frm.lift()
if idx_row == -1:
idx_row = int(len(list(main_frm.children.keys())))
dropdown_frm = LabelFrame(main_frm, text=frm_title, font=Formats.FONT_HEADER.value)
dropdown_dict = {}
for cnt, title in enumerate(drop_down_titles):
dropdown_dict[title] = DropDownMenu(dropdown_frm, title, drop_down_options, "35")
dropdown_dict[title].setChoices(drop_down_options[0])
dropdown_dict[title].grid(row=cnt, column=0, sticky=NW)
dropdown_frm.grid(row=idx_row, column=0, sticky=NW)
# main_frm.mainloop()
return dropdown_dict
def create_time_bin_entry(self):
if hasattr(self, "time_bin_frm"):
self.time_bin_frm.destroy()
self.time_bin_frm = LabelFrame(self.main_frm, text="TIME BIN", font=Formats.FONT_HEADER.value)
self.time_bin_entrybox = Entry_Box(self.time_bin_frm, "TIME-BIN SIZE (S): ", "15")
self.time_bin_entrybox.grid(row=0, column=0, sticky=NW)
self.time_bin_frm.grid(row=self.children_cnt_main(), column=0, sticky=NW)
[docs] def create_run_frm(self,
run_function: Callable,
title: Optional[str] = "RUN",
btn_txt_clr: Optional[str] = "black",
idx: Optional[int] = None,
btn_icon_name: Optional[str] = 'rocket',
padx: int = 0,
pady: int = 0) -> None:
"""
Create a label frame with a single button with a specified callback.
:param btn_txt_clr: The color of the text on the execute button.
:param idx: If not none, then the index of the main frame where to insert the RUN labelframe.
:param btn_icon_name: Name of the icon to use for the run button. If None, then no icon.
:param object run_function: The function/method callback of the button.
:param str title: The title of the frame.
"""
if hasattr(self, "run_frm"):
self.run_frm.destroy()
self.run_btn.destroy()
self.run_frm = LabelFrame(self.main_frm, text=title, font=Formats.FONT_HEADER.value, fg=btn_txt_clr)
self.run_btn = SimbaButton(parent=self.run_frm, txt=title, txt_clr=btn_txt_clr, font=Formats.FONT_REGULAR.value, img=btn_icon_name, cmd=run_function)
if idx is None:
self.run_frm.grid(row=self.children_cnt_main() + 1, column=0, sticky=NW)
else:
self.run_frm.grid(row=idx, column=0, sticky=NW, padx=padx, pady=pady)
self.run_btn.grid(row=0, column=0, sticky=NW)
[docs] def create_choose_number_of_body_parts_frm(self, project_body_parts: List[str], run_function: object, title: str = "SELECT NUMBER OF BODY-PARTS", dropdown_lbl: str = "# of body-parts"):
"""
Many menus depend on how many animals the user choose to compute metrics for. Thus, we need to populate the menus
dynamically. This function creates a single drop-down menu where the user select the number of animals the
user choose to compute metrics for. It inserts this drop-down iat the bottom of the pop-up window, and ties this dropdown menu
choice to a callback.
:param List[str] project_body_parts: Options of the dropdown menu.
:param object run_function: Function tied to the choice in the dropdown menu.
"""
self.bp_cnt_frm = LabelFrame(self.main_frm, text=title, font=Formats.FONT_HEADER.value,)
self.bp_cnt_dropdown = DropDownMenu( self.bp_cnt_frm, dropdown_lbl, list(range(1, len(project_body_parts) + 1)), "12",)
self.bp_cnt_dropdown.setChoices(1)
self.bp_cnt_confirm_btn = SimbaButton(parent=self.bp_cnt_frm, txt="CONFIRM", txt_clr='black', img='tick', font=Formats.FONT_HEADER.value, cmd=self.create_choose_bp_frm, cmd_kwargs={'bp_list': lambda: project_body_parts, 'run_function': lambda: run_function})
#self.bp_cnt_confirm_btn = Button(self.bp_cnt_frm, text="CONFIRM", font=Formats.FONT_REGULAR.value, command=lambda: self.create_choose_bp_frm(project_body_parts, run_function))
self.bp_cnt_frm.grid(row=0, sticky=NW)
self.bp_cnt_dropdown.grid(row=0, column=0, sticky=NW)
self.bp_cnt_confirm_btn.grid(row=0, column=1, sticky=NW)
[docs] def add_to_listbox_from_entrybox(self, list_box: Listbox, entry_box: Entry_Box):
"""
Add a value that populates a tkinter entry_box to a tkinter listbox.
:param Listbox list_box: The tkinter Listbox to add the value to.
:param Entry_Box entry_box: The tkinter Entry_Box containing the value that should be added to the list_box.
"""
value = entry_box.entry_get
check_float(name="VALUE", value=value)
list_box_content = [float(x) for x in list_box.get(0, END)]
if float(value) not in list_box_content:
list_box.insert(0, value)
[docs] def add_value_to_listbox(self, list_box: Listbox, value: float):
"""
Add a float value to a tkinter listbox.
:param Listbox list_box: The tkinter Listbox to add the value to.
:param float value: Value to add to the listbox.
"""
list_box.insert(0, value)
[docs] def add_values_to_several_listboxes(
self, list_boxes: List[Listbox], values: List[float]
):
"""
Add N values to N listboxes. E.g., values[0] will be added to list_boxes[0].
:param List[Listbox] list_boxes: List of Listboxes that the values should be added to.
:param List[float] values: List of floats that will be added to the list_boxes.
"""
if len(list_boxes) != len(values):
raise CountError(
msg="Value count and list-boxes count are not equal",
source=self.__class__.__name__,
)
for i in range(len(list_boxes)):
list_boxes[i].insert(0, values[i])
[docs] def remove_from_listbox(self, list_box: Listbox):
"""
Remove the current selection in a listbox from a listbox.
:param Listbox list_box: The listbox that the current selection should be removed from.
"""
selection = list_box.curselection()
if selection:
list_box.delete(selection[0])
[docs] def update_file_select_box_from_dropdown(
self, filename: str, fileselectbox: FileSelect
):
"""
Updates the text inside a tkinter FileSelect entrybox with a new string.
"""
fileselectbox.filePath.set(filename)
def check_if_selected_video_path_exist_in_project(
self, video_path: Union[str, os.PathLike]
):
if not os.path.isfile(video_path):
raise NoFilesFoundError(
msg=f"Selected video {os.path.basename(video_path)} is not a video file in the SimBA project video directory."
)
def create_choose_bp_frm(self, bp_list: List[str], run_function: Any):
if hasattr(self, "body_part_frm"):
self.body_part_frm.destroy()
self.body_parts_dropdowns = {}
self.body_part_frm = LabelFrame(self.main_frm, text="CHOOSE ANIMAL BODY-PARTS", font=Formats.FONT_HEADER.value, name="choose animal body-parts",)
self.body_part_frm.grid(row=self.children_cnt_main(), sticky=NW)
for bp_cnt in range(int(self.bp_cnt_dropdown.getChoices())):
self.body_parts_dropdowns[bp_cnt] = DropDownMenu(self.body_part_frm, f"Body-part {str(bp_cnt+1)}:", bp_list, "25",)
self.body_parts_dropdowns[bp_cnt].grid(row=bp_cnt, column=0, sticky=NW)
self.body_parts_dropdowns[bp_cnt].setChoices(bp_list[bp_cnt])
self.create_run_frm(run_function=run_function)
def choose_bp_frm(self, parent: LabelFrame, bp_options: list):
self.body_parts_dropdowns = {}
self.body_part_frm = LabelFrame(parent, text="CHOOSE ANIMAL BODY-PARTS", font=Formats.FONT_HEADER.value, name="choose animal body-parts",)
self.body_part_frm.grid(row=self.frame_children(frame=parent), sticky=NW)
for bp_cnt in range(int(self.animal_cnt_dropdown.getChoices())):
self.body_parts_dropdowns[bp_cnt] = DropDownMenu(
self.body_part_frm, f"Body-part {str(bp_cnt + 1)}:", bp_options, "20"
)
self.body_parts_dropdowns[bp_cnt].grid(row=bp_cnt, column=0, sticky=NW)
self.body_parts_dropdowns[bp_cnt].setChoices(bp_options[bp_cnt])
[docs] def children_cnt_main(self) -> int:
"""
Find the number of children (e.g., labelframes) currently exist within a main pop-up window. Useful for finding the
row at which a new frame within the window should be inserted.
"""
return int(len(list(self.main_frm.children.keys())))
[docs] def frame_children(self, frame: Union[Frame, Toplevel, Canvas, LabelFrame]) -> int:
"""
Find the number of children (e.g., labelframes) currently exist within specified frame.Similar to ``children_cnt_main``,
but accepts a specific frame rather than the main frame beeing hardcoded.
"""
return int(len(list(frame.children.keys())))
[docs] def update_config(self) -> None:
"""Helper to update the SimBA project config file"""
with open(self.config_path, "w") as f:
self.config.write(f)
def show_smoothing_entry_box_from_dropdown(self, choice: str):
if choice == "None":
self.smoothing_time_eb.grid_forget()
if (choice == "Gaussian") or (choice == "Savitzky Golay"):
self.smoothing_time_eb.grid(row=0, column=1, sticky=E)
def choose_bp_threshold_frm(self, parent: LabelFrame):
self.probability_frm = LabelFrame(parent, text="PROBABILITY THRESHOLD", font=Formats.FONT_HEADER.value)
self.probability_frm.grid(row=self.frame_children(frame=parent), column=0, sticky=NW)
self.probability_entry = Entry_Box(self.probability_frm, "Probability threshold: ", labelwidth=20)
self.probability_entry.entry_set("0.00")
self.probability_entry.grid(row=0, column=0, sticky=NW)
[docs] @staticmethod
def place_window_at_corner(window: Toplevel,
corner: Literal["top_left", "top_right", "bottom_left", "bottom_right"],
offset_x: int = 0,
offset_y: int = 0) -> None:
"""
Place the window at a screen corner. Placement is deferred (via after(0, ...)) so the window
is laid out first; call right after building the window.
:param win: A tk.Toplevel or tk.Tk window.
:param corner: One of "top_left", "top_right", "bottom_left", "bottom_right".
:param offset_x: Pixels from the corner horizontally (positive = inward).
:param offset_y: Pixels from the corner vertically (positive = inward).
"""
check_instance(source=f'{PopUpMixin.place_window_at_corner.__name__} window', instance=window, accepted_types=(Toplevel,), raise_error=True)
check_int(name=f'{PopUpMixin.place_window_at_corner.__name__} offset_x', value=offset_x, raise_error=True)
check_int(name=f'{PopUpMixin.place_window_at_corner.__name__} offset_y', value=offset_y, raise_error=True)
check_str(name=f'{PopUpMixin.place_window_at_corner.__name__} corner', value=corner, options=("top_left", "top_right", "bottom_left", "bottom_right"), raise_error=True)
def do_place():
window.update_idletasks()
w = window.winfo_width()
h = window.winfo_height()
if w <= 1 or h <= 1:
w = max(w, window.winfo_reqwidth())
h = max(h, window.winfo_reqheight())
_, (sw, sh) = get_monitor_info()
if corner == "top_left": x, y = offset_x, offset_y
elif corner == "top_right": x, y = sw - w - offset_x, offset_y
elif corner == "bottom_left": x, y = offset_x, sh - h - offset_y
else: x, y = sw - w - offset_x, sh - h - offset_y
window.geometry(f"+{x}+{y}")
window.after(0, do_place)
[docs] def enable_dropdown_from_checkbox(
self, check_box_var: BooleanVar, dropdown_menus: List[DropDownMenu]
):
"""
Given a single checkbox, enable a bunch of dropdowns if the checkbox is ticked, and disable the dropdowns if
the checkbox is un-ticked.
:param BooleanVar check_box_var: The checkbox associated tkinter BooleanVar.
:param List[DropDownMenu] dropdown_menus: List of dropdowns which status is controlled by the ``check_box_var``.
"""
if check_box_var.get():
for menu in dropdown_menus:
menu.enable()
else:
for menu in dropdown_menus:
menu.disable()
def create_entry_boxes_from_entrybox(self, count: int, parent: Frame, current_entries: list):
check_int(name="CLASSIFIER COUNT", value=count, min_value=1)
for entry in current_entries:
entry.destroy()
for clf_cnt in range(int(count)):
entry = Entry_Box(parent, f"Classifier {str(clf_cnt+1)}:", labelwidth=15)
current_entries.append(entry)
entry.grid(row=clf_cnt + 2, column=0, sticky=NW)
def create_animal_names_entry_boxes(self, animal_cnt: str):
check_int(name="NUMBER OF ANIMALS", value=animal_cnt, min_value=0)
if hasattr(self, "animal_names_frm"):
self.animal_names_frm.destroy()
if not hasattr(self, "multi_animal_id_list"):
self.multi_animal_id_list = []
for i in range(int(animal_cnt)):
self.multi_animal_id_list.append(f"Animal {i+1}")
self.animal_names_frm = Frame(self.animal_settings_frm, pady=5, padx=5)
self.animal_name_entry_boxes = {}
for i in range(int(animal_cnt)):
self.animal_name_entry_boxes[i + 1] = Entry_Box(
self.animal_names_frm, f"Animal {str(i+1)} name: ", "25"
)
if i <= len(self.multi_animal_id_list) - 1:
self.animal_name_entry_boxes[i + 1].entry_set(
self.multi_animal_id_list[i]
)
self.animal_name_entry_boxes[i + 1].grid(row=i, column=0, sticky=NW)
self.animal_names_frm.grid(row=1, column=0, sticky=NW)
[docs] def enable_entrybox_from_checkbox(
self,
check_box_var: BooleanVar,
entry_boxes: List[Entry_Box],
reverse: bool = False,
):
"""
Given a single checkbox, enable or disable a bunch of entry-boxes based on the status of the checkbox.
:param BooleanVar check_box_var: The checkbox associated tkinter BooleanVar.
:param List[Entry_Box] entry_boxes: List of entry-boxes which status is controlled by the ``check_box_var``.
:param bool reverse: If False, the entry-boxes are enabled with the checkbox is ticked. Else, the entry-boxes are enabled if checkbox is unticked. Default: False.
"""
if reverse:
if check_box_var.get():
for box in entry_boxes:
box.set_state("disable")
else:
for box in entry_boxes:
box.set_state("normal")
else:
if check_box_var.get():
for box in entry_boxes:
box.set_state("normal")
else:
for box in entry_boxes:
box.set_state("disable")
# def quit(self, e):
# self.main_frm.quit()
# self.main_frm.destroy()
#
# def callback(self, url):
# webbrowser.open_new(url)
#
# def move_app(self, e):
# print(f'+{e.x_root}+{e.y_root}')
# self.main_frm.geometry(f'+{e.x_root}+{e.y_root}')
# #print(f'+{e.x_root}x{e.y_root}')
# #self.main_frm.config(width=e.x_root, height=e.y_root)
# #self.main_frm.update()
# test = PopUpMixin(config_path='/Users/simon/Desktop/envs/troubleshooting/two_animals_16bp_032023/project_folder/project_config.ini',
# title='ss')
# test.create_import_pose_menu(parent_frm=test.main_frm)
# test = PopUpMixin(config_path='/Users/simon/Desktop/envs/troubleshooting/two_animals_16bp_032023/project_folder/project_config.ini',
# title='ss')
# test.create_import_videos_menu(parent_frm=test.main_frm)