Source code for RepeatableRowGroup

import tkinter as tk

from gui.CreateTooltip import CreateTooltip
from gui.ScreenSizeManager import ScreenSizeManager


[docs] class RepeatableRowGroup(object): """ Manages a labeled, repeatable list of rows: an optional header label, a container where rows stack vertically, and three buttons (add / remove last / remove all) that build and destroy rows on demand. This class does not know what a "row" looks like on the inside - it just needs a `row_factory` function that builds one and hands it back. Any row object handed back must provide: - a `.frame` attribute (the widget to display) - a `.destroy()` method (to remove it from the screen) - a `.get_values()` method (used by get_row_values() below) This same class is used twice in UnitCellRegion.py: - Once for the list of unit cell growth rows (with row spacing and scrolling turned on, since that list can grow tall) - Once more, nested inside each growth row, for that row's own list of Miller planes (left at its small default spacing, with scrolling turned off) """ def __init__(self, parent, row_factory, group_label=None, group_tooltip=None, add_button_text="add", remove_last_button_text="Remove Last", remove_all_button_text="Remove All", add_tooltip=None, remove_last_tooltip=None, remove_all_tooltip=None, bg="#e0e0e0", fg="black", start_enabled=True, row_spacing=1, enable_scrolling=False, max_height_fraction=0.2): """ :param parent: The parent widget this group's frame will be placed in. :param row_factory: A function that takes one argument (the parent frame to build the row inside) and returns a new row object. Called once every time "add" is clicked. :param group_label: Optional header text shown above the row list. :param group_tooltip: Optional tooltip text attached to the header label. :param add_button_text: Text for the "add" button. :param remove_last_button_text: Text for the "remove last" button. :param remove_all_button_text: Text for the "remove all" button. :param add_tooltip: Optional tooltip text for the "add" button. :param remove_last_tooltip: Optional tooltip text for the "remove last" button. :param remove_all_tooltip: Optional tooltip text for the "remove all" button. :param bg: Background color used for the frame and labels. :param fg: Text color used for the header label. :param start_enabled: Whether the three buttons start out clickable (True) or greyed out (False). :param row_spacing: Vertical gap, in pixels, placed between rows in the list. :param enable_scrolling: If True, the row list is wrapped in a scrollable area capped at max_height_fraction of the screen's height, with a scrollbar that appears once the rows grow taller than that. Requires ScreenSizeManager.initialize() to already have been called (done once, in TheorChem2Blender.py). :param max_height_fraction: Fraction (0-1) of the screen's height used as the scrollable area's maximum height. Only used when enable_scrolling is True. """ self.row_factory = row_factory self.rows = [] self.row_spacing = row_spacing self.enable_scrolling = enable_scrolling self.frame = tk.Frame(master=parent, bg=bg) current_row = 0 if group_label: self.group_label_widget = tk.Label(master=self.frame, text=group_label, bg=bg, fg=fg) self.group_label_widget.grid(row=current_row, column=0, sticky="w") if group_tooltip: CreateTooltip(self.group_label_widget, group_tooltip) current_row += 1 if self.enable_scrolling: current_row = self._setup_scrollable_rows_container(current_row, bg, max_height_fraction) else: # Rows get added here, one on top of the other, as they're created. self.rows_container = tk.Frame(master=self.frame, bg=bg) self.rows_container.grid(row=current_row, column=0, sticky="w", pady=(2, 2)) current_row += 1 self.buttons_frame = tk.Frame(master=self.frame, bg=bg) self.buttons_frame.grid(row=current_row, column=0, sticky="w") initial_state = tk.NORMAL if start_enabled else tk.DISABLED self.btn_add = tk.Button(master=self.buttons_frame, text=add_button_text, command=self.add_row, state=initial_state) self.btn_add.grid(row=0, column=0, padx=(0, 4)) if add_tooltip: CreateTooltip(self.btn_add, add_tooltip) self.btn_remove_last = tk.Button(master=self.buttons_frame, text=remove_last_button_text, command=self.remove_last, state=initial_state) self.btn_remove_last.grid(row=0, column=1, padx=(0, 4)) if remove_last_tooltip: CreateTooltip(self.btn_remove_last, remove_last_tooltip) self.btn_remove_all = tk.Button(master=self.buttons_frame, text=remove_all_button_text, command=self.remove_all, state=initial_state) self.btn_remove_all.grid(row=0, column=2) if remove_all_tooltip: CreateTooltip(self.btn_remove_all, remove_all_tooltip) def _setup_scrollable_rows_container(self, current_row, bg, max_height_fraction): """ Builds rows_container inside a scrollable canvas instead of placing it directly in self.frame. The canvas grows with its contents up to max_height_fraction of the screen's height, then a scrollbar takes over. Mirrors the canvas+scrollbar pattern already used elsewhere in this app (see InputRegion.py / IonRegion.py). Returns the next free grid row in self.frame, for the buttons below it. """ self.max_scroll_height = int(ScreenSizeManager.get_screen_height() * max_height_fraction) self.canvas = tk.Canvas(master=self.frame, bg=bg, highlightthickness=0) self.scrollbar = tk.Scrollbar(master=self.frame, orient="vertical", command=self.canvas.yview) self.canvas.configure(yscrollcommand=self.scrollbar.set) self.rows_container = tk.Frame(master=self.canvas, bg=bg) self.canvas.create_window((0, 0), window=self.rows_container, anchor="nw") self.rows_container.bind("<Configure>", self._update_scroll_region) self.canvas.grid(row=current_row, column=0, sticky="w", pady=(2, 2)) self.scrollbar.grid(row=current_row, column=1, sticky="ns", pady=(2, 2)) return current_row + 1 def _update_scroll_region(self, event=None): """ Resizes the scrollable canvas to fit its current contents, up to the height cap computed in _setup_scrollable_rows_container(). Runs automatically every time a row is added or removed, since that changes the size of rows_container and triggers its <Configure> event. """ bbox = self.canvas.bbox("all") if bbox is None: return content_width = bbox[2] - bbox[0] content_height = bbox[3] - bbox[1] display_height = min(content_height, self.max_scroll_height) self.canvas.configure(scrollregion=bbox, width=content_width, height=display_height)
[docs] def add_row(self): """Builds one new row via row_factory and adds it to the bottom of the list.""" new_row = self.row_factory(self.rows_container) new_row.frame.grid(row=len(self.rows), column=0, sticky="w", pady=self.row_spacing) self.rows.append(new_row) return new_row
[docs] def remove_last(self): """Removes the most recently added row, if any.""" if not self.rows: return row = self.rows.pop() row.destroy() self.frame.update_idletasks()
[docs] def remove_all(self): """Removes every row, resetting this group back to empty.""" for row in self.rows: row.destroy() self.rows.clear() self.frame.update_idletasks()
[docs] def set_enabled(self, enabled): """ Enables or disables the add/remove buttons. Does not touch existing rows - call remove_all() separately if the rows themselves should also be cleared. """ state = tk.NORMAL if enabled else tk.DISABLED self.btn_add.config(state=state) self.btn_remove_last.config(state=state) self.btn_remove_all.config(state=state)
[docs] def get_row_values(self): """Returns a list built from calling .get_values() on every current row, in order.""" return [row.get_values() for row in self.rows]