SYSTEM: You are a Python 3.12 programmer, capable of analyzing problems in depth and explaining solutions in a clear and concise manner. Your responses prioritize code efficiency, readability, and simplicity. You leverage the latest Python features. When addressing tasks, you break them down into clear steps and present only essential information, ensuring users receive focused, expert guidance tailored to their needs. You only print the parts of the code you changed in a way that makes them easy to copy paste in the code base. Remember the code uses tksheet 7. Lets think step by step. USER: There should be a 25 px space between the right border of the table and the app border if the sum of the column widths is smaller than the space between the spacer frame and the right app border. Python 3.12 Code: import ctypes import csv import json import re import tkinter as tk from pathlib import Path from tkinter import filedialog, messagebox, simpledialog, ttk from typing import Any, Optional, Union from tksheet import Sheet, int_formatter, float_formatter, bool_formatter, num2alpha def set_dpi_awareness() -> None: """Enable DPI awareness for high DPI settings.""" try: ctypes.windll.shcore.SetProcessDpiAwareness(1) except Exception as e: print(f"Error setting DPI awareness: {e}") def create_frame(parent: tk.Widget, width: Optional[int] = None, bg: str = "SystemButtonFace") -> tk.Frame: """Creates a frame with specified width and background color.""" return tk.Frame(parent, width=width, bg=bg, bd=0, relief="flat") def custom_filter_design( master: tk.BaseWidget, style: ttk.Style, headers: list[str], filter_types: list[str], filter_font: str, filter_combobox_font: str, colors: dict[str, str], ) -> tuple[ttk.Frame, ttk.Combobox, ttk.Combobox, ttk.Entry]: """Creates a custom filter frame with all components.""" style_dict = { "Custom.TFrame": { "background": colors["background"], "relief": "flat", "borderwidth": 0, }, "TCombobox": { "relief": "flat", "borderwidth": 0, "background": colors["background"], "fieldbackground": colors["background"], "foreground": colors["text"], }, "Line.TFrame": {"background": colors["filter_line"]}, "TEntry": { "relief": "flat", "borderwidth": 0, "background": colors["background"], "foreground": colors["text"], }, } for widget_style, settings in style_dict.items(): style.configure(widget_style, **settings) style.map("TCombobox", fieldbackground=[("readonly", colors["background"])], background=[("readonly", colors["background"])]) filter_frame = ttk.Frame(master, style="Custom.TFrame") header_combobox = ttk.Combobox(filter_frame, values=headers, width=15, font=filter_combobox_font, state="readonly") filter_type_combobox = ttk.Combobox(filter_frame, values=filter_types, width=15, font=filter_combobox_font, state="readonly") filter_entry = ttk.Entry(filter_frame, font=filter_font) underline = ttk.Frame(filter_frame, height=1, style="Line.TFrame") header_combobox.grid(row=0, column=0, padx=(0, 30), pady=2, sticky="ew") filter_type_combobox.grid(row=0, column=1, padx=2, pady=2, sticky="ew") filter_entry.grid(row=1, column=0, columnspan=2, padx=2, pady=(2, 0), sticky="ew") underline.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(0, 2)) for combobox in (header_combobox, filter_type_combobox): combobox.bind("<FocusIn>", lambda event: event.widget.selection_clear()) filter_frame.columnconfigure((0, 1), weight=1) underline.pack_propagate(False) return filter_frame, header_combobox, filter_type_combobox, filter_entry def custom_scrollbar_design(style: ttk.Style, colors: dict[str, str]) -> None: """Configure custom vertical scrollbar style.""" STYLE_NAME = "Custom.Vertical.TScrollbar" style.configure( STYLE_NAME, troughcolor=colors["scrollbar_trough"], background=colors["scrollbar_background"], borderwidth=0, arrowcolor=colors["scrollbar_arrow"], ) style.map( STYLE_NAME, background=[("active", colors["scrollbar_active"])], ) style.layout( STYLE_NAME, [("Vertical.Scrollbar.trough", { "children": [("Vertical.Scrollbar.thumb", { "expand": "1", "sticky": "ns", })], "sticky": "ns", })] ) class TableApp: """Main application controller for database table visualization.""" def __init__(self, root: tk.Tk) -> None: """Initialize the TableApp with basic configuration and UI setup.""" self.root = root self.configure_root() self.setup_configurations() self.initialize_core_components() self.setup_ui() self.load_initial_data() def configure_root(self) -> None: """Configure the root window properties.""" self.root.title("Database") self.screen_width = self.root.winfo_screenwidth() self.screen_height = self.root.winfo_screenheight() window_width = int(self.screen_width * 0.6) window_height = int(self.screen_height * 0.6) min_width = int(self.screen_width * 0.4) min_height = int(self.screen_height * 0.4) self.root.geometry(f"{window_width}x{window_height}") self.root.minsize(min_width, min_height) x_position = (self.screen_width - window_width) // 2 y_position = (self.screen_height - window_height) // 2 self.root.geometry(f"+{x_position}+{y_position}") def setup_configurations(self) -> None: """Initialize all configurations including dimensions, fonts, colors, and style.""" self.dimensions = { "sidebar_width": 350, "spacer_frame_width": 25, "header_height": 200, "separator_height": 1 } self.fonts = { "text": ("Literata", 12), "menu": ("Literata", 10), "tksheet": ("Literata", 12, "normal"), "header_info": ("Literata", 10), "filter": ("Literata", 12), "filter_combobox": ("Literata", 8) } self.colors = { "text": "#333333", "background": "#FFFFFF", "sidebar": "#F7F8FC", "button": "#F7F8FC", "button_selected": "#EAEDEF", "separator_sidebar": "#333333", "separator_menu": "#999999", "header_info": "#999999", "scrollbar_trough": "#F7F8FC", "scrollbar_background": "#c0c1c4", "scrollbar_arrow": "#333333", "scrollbar_active": "#878789", "filter_line": "#00bcf0", "checkbox": "#00bcf0", "frame_border": "#999999", } self.style = ttk.Style() self.style.theme_use("default") def initialize_core_components(self) -> None: """Initialize core application components and state variables.""" self.current_table_name: Optional[str] = None self.current_sheet: Optional[tk.Frame] = None self.displayed_row_indices: list[int] = [] self.data_manager = DataManager(self) self.table_operations = TableOperationsManager(self) self.table_display = TableDisplayManager(self) self.table_io = TableIOManager(self) def setup_ui(self) -> None: """Set up all UI components and their layout.""" self.configure_grid() self.create_main_ui() self.create_auxiliary_ui() def configure_grid(self) -> None: """Configure the root window grid layout.""" self.root.grid_rowconfigure(0, weight=0) self.root.grid_rowconfigure(1, weight=1) self.root.grid_columnconfigure(0, weight=0) self.root.grid_columnconfigure(1, weight=0, minsize=self.dimensions["spacer_frame_width"]) self.root.grid_columnconfigure(2, weight=3) def create_main_ui(self) -> None: """Create main UI components.""" self.sidebar = Sidebar(self.root, self) self.header = Header(self.root, self) self.filter_controls = FilterControls(self.header.lower_header_frame, self) def create_auxiliary_ui(self) -> None: """Create auxiliary frames for layout purposes.""" self.create_spacer_frame() self.create_table_frame() def create_spacer_frame(self) -> None: """Create and configure the spacer frame.""" spacer = tk.Frame( master=self.root, width=self.dimensions["spacer_frame_width"], bg=self.colors["background"] ) spacer.grid(row=1, column=1, sticky="nsew") spacer.grid_propagate(False) def create_table_frame(self) -> None: """Create and configure the main table frame.""" self.table_frame = tk.Frame( master=self.root, bg=self.colors["background"] ) self.table_frame.grid(row=1, column=2, sticky="nsew") self.table_frame.grid_rowconfigure(0, weight=1) self.table_frame.grid_columnconfigure(0, weight=1) def load_initial_data(self) -> None: """Load initial application data.""" self.data_manager.load_tables() def load_selected_table(self, table_name: str) -> None: """Load and display the selected table.""" self.table_display.load_selected_table(table_name) self.displayed_row_indices = list(range(len(self.data_manager.tables_data[table_name].get("data", [])))) def refresh_table(self) -> None: """Refresh the currently displayed table.""" if self.current_table_name: self.load_selected_table(self.current_table_name) def apply_filter(self) -> None: """Apply the current filter to the table.""" self.filter_controls.apply_filter() class DataManager: """Manages table data persistence operations.""" def __init__(self, app: 'TableApp') -> None: """Initialize data manager with app reference.""" self.app = app self.save_file_path = Path(__file__).parent / "data.json" self.tables_data: dict[str, dict[str, Any]] = {} self.headers: dict[str, list[str]] = {} def load_tables(self) -> None: """Load and process tables from JSON file.""" if not self._load_json_data(): return for table_name, table_info in self._load_json_data().items(): self._process_table(table_name, table_info) self.app.sidebar.sort_table_labels() def _load_json_data(self) -> Optional[dict]: """Load JSON data from file, handling errors.""" if not self.save_file_path.exists(): return None try: return json.loads(self.save_file_path.read_text(encoding="utf-8")) except Exception as e: messagebox.showerror("Load Error", f"Failed to load tables: {e}") return None def _process_table(self, table_name: str, table_info: dict) -> None: """Process individual table data and update internal storage.""" headers, table_data, data_types = self._parse_table_info(table_info) if not headers or not table_data: return processed_data = self._convert_booleans(table_data, data_types) self.headers[table_name] = headers self.tables_data[table_name] = { "headers": headers, "data": processed_data, "data_types": data_types } self.app.sidebar.create_table_label(table_name) def _parse_table_info(self, table_info: dict) -> tuple[list[str], list[list], list[str]]: """Parse table structure from JSON data.""" if isinstance(table_info, dict): return ( table_info.get("headers", []), table_info.get("data", []), table_info.get("data_types", []) ) if isinstance(table_info, list) and table_info: headers = table_info[0] return headers, table_info[1:], ["string"] * len(headers) return [], [], [] def _convert_booleans(self, table_data: list[list], data_types: list[str]) -> list[list]: """Convert string boolean values to Python booleans.""" BOOLEAN_TRUE_VALUES = {"true", "1", "yes"} if len(data_types) < len(table_data[0]) if table_data else 0: data_types.extend(['string'] * (len(table_data[0]) - len(data_types))) return [ [ str(value).lower() in BOOLEAN_TRUE_VALUES if data_types[col_idx] == "boolean" else value for col_idx, value in enumerate(row) ] for row in table_data ] def save_data(self) -> None: """Save tables data to JSON file.""" all_data = { name: { "headers": info["headers"], "data": info["data"], "data_types": info.get("data_types", ["string"] * len(info["headers"])) } for name, info in self.tables_data.items() } try: self.save_file_path.write_text( json.dumps(all_data, indent=4), encoding="utf-8" ) except Exception as e: print(f"Error saving data: {e}") class TableOperationsManager: """Handles creating, deleting, renaming tables, and changing column data types.""" def __init__(self, app: 'TableApp'): self.app = app def create_new_table(self) -> None: """Create a new table with a unique name.""" existing_numbers = {int(name.split(" ")[-1]) for name in self.app.data_manager.tables_data if name.startswith("Table ") and name.split(" ")[-1].isdigit()} next_number = max(existing_numbers, default=0) + 1 table_name = f"Table {next_number}" self.add_table(table_name) self.app.table_display.load_selected_table(table_name) def add_table( self, table_name: str, data: Optional[list[list[str]]] = None, headers: Optional[list[str]] = None, data_types: Optional[list[str]] = None, ) -> None: """Add a new table with optional headers, data, and data types.""" headers = headers or ["Column 1", "Column 2", "Column 3", "Column 4", "Column 5"] data_types = data_types or ['string'] * len(headers) self.app.data_manager.headers[table_name] = headers self.app.data_manager.tables_data[table_name] = { "headers": headers, "data": data or [[""] * len(headers) for _ in range(10)], "data_types": data_types, } self.app.sidebar.create_table_label(table_name) self.app.data_manager.save_data() def delete_table(self) -> None: """Delete the currently selected table with user confirmation.""" table_name = self.app.current_table_name if not table_name or table_name not in self.app.data_manager.tables_data: messagebox.showwarning("No Table Selected", "Please select a valid table to delete.") return confirmed = messagebox.askyesno("Delete Table", f"Are you sure you want to delete the table '{table_name}'?") if confirmed: self.remove_table(table_name) def remove_table(self, table_name: str) -> None: """Remove the specified table and associated resources.""" if table_name == self.app.current_table_name: self.app.current_sheet.destroy() self.app.current_sheet = None label = self.app.sidebar.table_labels.pop(table_name, None) if label: label.destroy() self.app.data_manager.tables_data.pop(table_name, None) self.app.data_manager.headers.pop(table_name, None) self.app.current_table_name = None self.app.sidebar.selected_table_label = None self.app.data_manager.save_data() self.app.sidebar.sort_table_labels() def begin_rename(self, table_name: str) -> None: """Initiate renaming of the specified table.""" if table_name not in self.app.sidebar.table_labels: messagebox.showerror("Error", f"Table '{table_name}' not found.") return label = self.app.sidebar.table_labels[table_name] self.cancel_rename() rename_entry = tk.Entry( self.app.sidebar.table_labels_frame, bg=label.cget("bg"), fg=self.app.colors["text"], relief="flat", ) rename_entry.insert(0, table_name) rename_entry.place( x=label.winfo_x(), y=label.winfo_y(), width=label.winfo_width(), height=label.winfo_height(), ) rename_entry.focus() rename_entry.select_range(0, tk.END) rename_entry.bind("<Return>", lambda event: self.end_rename(table_name)) rename_entry.bind("<FocusOut>", lambda event: self.cancel_rename()) self.app.sidebar.rename_entry = rename_entry def end_rename(self, old_name: str) -> None: """Finalize renaming of the specified table.""" new_name = self.app.sidebar.rename_entry.get().strip() if not new_name: messagebox.showerror("Invalid Name", "Table name cannot be empty.") self.end_rename(old_name) return if new_name != old_name and new_name in self.app.data_manager.tables_data: messagebox.showerror("Duplicate Name", f"A table named '{new_name}' already exists.") self.end_rename(old_name) return if new_name != old_name: self.update_table_name(old_name, new_name) self.app.table_display.load_selected_table(new_name) self.cancel_rename() def update_table_name(self, old_name: str, new_name: str) -> None: """Update the name of the table and associated UI elements.""" self.app.data_manager.tables_data[new_name] = self.app.data_manager.tables_data.pop(old_name) self.app.data_manager.headers[new_name] = self.app.data_manager.headers.pop(old_name) label = self.app.sidebar.table_labels.pop(old_name) label.config(text=new_name) label.bind("<Button-1>", lambda event: self.app.table_display.load_selected_table(new_name)) label.bind("<Double-1>", lambda event: self.begin_rename(new_name)) self.app.sidebar.table_labels[new_name] = label if self.app.sidebar.selected_table_label == label: label.config(bg=self.app.colors.get("button_selected", "default_color")) self.app.data_manager.save_data() self.app.sidebar.sort_table_labels() def cancel_rename(self) -> None: """Cancel the rename operation.""" if self.app.sidebar.rename_entry: self.app.sidebar.rename_entry.destroy() self.app.sidebar.rename_entry = None def change_column_data_types(self) -> None: """Change the data types of columns in the currently selected table.""" table_name = self.app.current_table_name if not table_name: messagebox.showwarning("No Table Selected", "Please select a table to change datatypes.") return current_headers = self.app.current_sheet.headers() if self.app.current_sheet else [] self.app.data_manager.headers[table_name] = current_headers data_types = self.app.data_manager.tables_data[table_name].get("data_types", []) while len(data_types) < len(current_headers): data_types.append('string') dialog = Header(self.app.root, current_headers, data_types) if dialog.result: new_data_types = dialog.result self.app.data_manager.tables_data[table_name]["data_types"] = new_data_types self.app.data_manager.save_data() self.app.table_display.apply_column_data_types(new_data_types) self.app.table_display.process_and_save_current_sheet_data() self.app.refresh_table() class TableDisplayManager: """Handles loading and displaying tables and related events.""" def __init__(self, app: 'TableApp'): self.app = app def load_selected_table(self, table_name: str) -> None: try: if self.app.current_sheet: self.app.current_sheet.pack_forget() self.app.current_sheet.destroy() self.app.current_sheet = None if self.app.sidebar.selected_table_label: self.app.sidebar.selected_table_label.config(bg=self.app.colors["button"]) self.app.sidebar.selected_table_label = None if table_name not in self.app.data_manager.tables_data: messagebox.showerror("Error", f"Table '{table_name}' does not exist.") self.app.current_table_name = None return table_info = self.app.data_manager.tables_data[table_name] headers = self.auto_rename_empty_headers(table_info["headers"]) table_info["headers"] = headers data = table_info["data"] data_types = table_info.get("data_types", []) if self.app.filter_controls: self.app.filter_controls.filter_header_var.set("All") self.app.filter_controls.filter_option_var.set("contains") self.app.filter_controls.filter_text_var.set("") self.app.filter_controls.update_filter_text_state() self.app.filter_controls.reset_row_indices() sheet = Sheet( self.app.table_frame, data=data, show_table=True, headers=headers, font=self.app.fonts["tksheet"], header_font=self.app.fonts["tksheet"], ) sheet.enable_bindings("edit_header") sheet.set_options( table_fg=self.app.colors["text"], header_fg=self.app.colors["text"] ) sheet.enable_bindings() sheet.extra_bindings( [ ("end_edit_cell", self.on_edit), ("end_delete_columns", self.on_columns_deleted), ("end_insert_column", self.on_columns_inserted), ("cell_select", self.on_cell_select), ("end_edit_header", self.on_edit), ("delete", self.on_edit), ("end_insert_row", self.on_edit), ("end_cut", self.on_edit), ("end_paste", self.on_edit), ("end_delete_rows", self.on_edit), ] ) sheet.bind("<Configure>", lambda e: self.adjust_column_widths()) self.adjust_column_widths() sheet.pack(expand=True, fill="both") self.app.current_sheet = sheet self.app.current_table_name = table_name if data_types: self.apply_column_data_types(data_types) if not self.app.header.table_name_label: self.app.header.table_name_label = tk.Label( self.app.root, text=self.app.current_table_name, bg=self.app.colors["background"], fg=self.app.colors["text"], font=self.app.fonts["header_info"], ) self.app.header.table_name_label.grid( row=0, column=0, padx=(60, 10), pady=(5, 0), sticky="nw" ) else: self.app.header.table_name_label.config(text=self.app.current_table_name) if label := self.app.sidebar.table_labels.get(table_name): self.app.sidebar.selected_table_label = label label.config(bg=self.app.colors["button_selected"]) else: messagebox.showerror("Error", f"Label for '{table_name}' not found.") self.app.sidebar.selected_table_label = None self.app.filter_controls.filter_header_var.set("All") self.app.filter_controls.filter_header_dropdown["values"] = ["All"] + headers self.app.filter_controls.filter_option_var.set("contains") self.app.filter_controls.update_filter_text_state() self.app.apply_filter() except Exception as e: messagebox.showerror("Sheet Error", f"Failed to load table '{table_name}': {e}") self.app.current_table_name = None self.app.sidebar.selected_table_label = None def adjust_column_widths(self) -> None: """Adjusts column widths to ensure headers and content are fully visible. If window is wider than total columns, enlarges columns proportionally.""" if not self.app.current_sheet: return sheet = self.app.current_sheet window_width = sheet.winfo_width() headers = sheet.headers() data = sheet.get_sheet_data() column_widths = [] for col_idx, header in enumerate(headers): header_width = len(str(header)) * 12 + 150 content_width = 0 if data: max_content_width = max( len(str(row[col_idx])) if col_idx < len(row) else 0 for row in data ) * 10 + 0 content_width = max(content_width, max_content_width) column_widths.append(max(header_width, content_width)) total_width = sum(column_widths) window_padding = 95 if window_width > total_width + window_padding and window_width > 0: extra_space = window_width - total_width - window_padding for i in range(len(column_widths)): ratio = column_widths[i] / total_width column_widths[i] += int(extra_space * ratio) for col_idx, width in enumerate(column_widths): sheet.column_width(column=col_idx, width=width) def auto_rename_empty_headers(self, headers: list[str]) -> list[str]: """Automatically rename empty column headers to 'Column X' where X is the next available number.""" max_column_num = 0 for header in headers: if header.startswith("Column "): try: num = int(header.split(" ")[1]) max_column_num = max(max_column_num, num) except (ValueError, IndexError): continue counter = max_column_num + 1 new_headers = [] for header in headers: if not header or header.isspace(): new_headers.append(f"Column {counter}") counter += 1 else: new_headers.append(header) return new_headers def apply_column_data_types(self, data_types: list) -> None: """Apply column data types and format the sheet.""" if not self.app.current_sheet: return sheet = self.app.current_sheet sheet.del_all_formatting() if checkboxes := sheet.get_checkboxes(): for key in list(checkboxes.keys()): sheet.del_checkbox(key) for col_idx, dtype in enumerate(data_types): column_letter = num2alpha(col_idx) match dtype: case 'integer': sheet.format(column_letter, formatter_options=int_formatter(nullable=True, invalid_value='')) case 'float': sheet.format(column_letter, formatter_options=float_formatter(nullable=True, invalid_value='')) case 'boolean': sheet.checkbox(column_letter, checked=None, check_function=self.on_checkbox_click) num_rows = len(sheet.get_sheet_data()) cells_to_highlight = [(row, col_idx) for row in range(num_rows)] sheet.highlight_cells(cells=cells_to_highlight, fg=self.app.colors["checkbox"]) case 'string': continue case _: continue sheet.reapply_formatting() def on_checkbox_click(self, event: dict) -> None: """Handle checkbox click events and update data.""" if not self.app.current_table_name or not self.app.current_sheet: return row, col, new_value = event['row'], event['column'], event['value'] if row < len(self.app.displayed_row_indices): data_row_index = self.app.displayed_row_indices[row] self.app.data_manager.tables_data[self.app.current_table_name]["data"][data_row_index][col] = new_value self.app.data_manager.save_data() else: print(f"Row index {row} out of range for displayed_row_indices") def on_edit(self, event=None): """Handle both cell and header edits and save the updated data.""" self.process_and_save_current_sheet_data() def on_columns_deleted(self, event: dict) -> None: """Handle column deletion and update filter headers.""" if not self.app.current_table_name or not self.app.current_sheet: return new_headers = self.app.current_sheet.headers() self.app.data_manager.headers[self.app.current_table_name] = new_headers deleted_columns = event.get('columns', []) data_types = self.app.data_manager.tables_data[self.app.current_table_name].get("data_types", []) for col_idx in sorted(deleted_columns, reverse=True): if col_idx < len(data_types): del data_types[col_idx] self.app.data_manager.tables_data[self.app.current_table_name]["data_types"] = data_types self.app.data_manager.save_data() self.app.filter_controls.filter_header_dropdown["values"] = ["All"] + new_headers self.app.filter_controls.filter_header_var.set("All") self.app.filter_controls.filter_option_var.set("contains") self.app.filter_controls.filter_text_var.set("") self.app.filter_controls.filter_text_entry.config(state="normal") self.app.apply_filter() def on_columns_inserted(self, event: dict) -> None: """Handle columns inserted and update data types and headers.""" if not self.app.current_table_name or not self.app.current_sheet: return current_headers = list(self.app.current_sheet.headers()) current_headers = self.auto_rename_empty_headers(current_headers) self.app.current_sheet.headers(current_headers) inserted_columns = event.get('columns', []) data_types = self.app.data_manager.tables_data[self.app.current_table_name].get("data_types", []) for col_idx in inserted_columns: data_types.insert(col_idx, 'string') self.app.data_manager.tables_data[self.app.current_table_name]["data_types"] = data_types self.app.data_manager.tables_data[self.app.current_table_name]["headers"] = current_headers self.app.data_manager.save_data() self.app.filter_controls.filter_header_dropdown["values"] = ["All"] + current_headers def on_cell_select(self, event: dict) -> None: """Handle cell selection to give focus to checkboxes.""" if not self.app.current_table_name or not self.app.current_sheet: return row, col = event.row, event.column checkboxes = self.app.current_sheet.get_checkboxes() if (row, col) not in checkboxes: return self.app.current_sheet.focus_set() def process_and_save_current_sheet_data(self) -> None: """Processes sheet data with type conversion and saves to data manager.""" if not (current_table := self.app.current_table_name) or not self.app.current_sheet: return if not (table_info := self.app.data_manager.tables_data.get(current_table)): return sheet_data = self.app.current_sheet.get_sheet_data() data_types = table_info.get("data_types", []) table_data = table_info["data"] table_data.clear() table_data.extend([ [self._convert_cell(cell, data_types[i] if i < len(data_types) else "string") for i, cell in enumerate(row)] for row in sheet_data ]) self.app.data_manager.save_data() def _convert_cell(self, cell: str, dtype: str) -> Union[str, int, float, bool, None]: """Converts cell value to specified data type with error handling.""" if not cell or cell is None: return { 'integer': None, 'float': None, 'boolean': False }.get(dtype, '') type_converters = { 'integer': lambda x: int(x), 'float': lambda x: float(x), 'boolean': lambda x: str(x).lower() in ('true', '1', 'yes'), 'string': lambda x: x } try: return type_converters.get(dtype, type_converters['string'])(cell) except (ValueError, TypeError): return None if dtype in ('integer', 'float') else cell class TableIOManager: """Handles importing and exporting tables.""" def __init__(self, app: "TableApp"): """Initialize the TableIOManager with the provided TableApp instance.""" self.app = app def _parse_header_with_type(self, header: str) -> tuple[str, str]: """Extract column name and data type from header format: 'Name (type)'.""" match = re.match(r'^(.*?)\s*\((string|float|boolean|integer)\)$', header) if match: return match.group(1), match.group(2) return header, 'string' def _format_header_with_type(self, header: str, data_type: str) -> str: """Format header with its data type: 'Name (type)'.""" return f"{header} ({data_type})" def import_file(self): """Import table from file using a file dialog and appropriate readers.""" file_path = filedialog.askopenfilename( title="Import File", filetypes=[ ("All Supported Files", "*.json *.csv"), ("JSON files", "*.json"), ("CSV files", "*.csv"), ], ) if not file_path: return try: headers, table_data, data_types = self._read_table_data(file_path) self.add_table_from_data(file_path, headers, table_data, data_types) except Exception as e: messagebox.showerror("File Import Error", str(e)) def _read_table_data(self, file_path: str) -> tuple[list[str], list[list[Any]], list[str]]: """Read table data and data types from a file based on its extension.""" match Path(file_path).suffix.lower(): case ".csv": with open(file_path, newline="", encoding="utf-8") as csvfile: data = list(csv.reader(csvfile)) raw_headers, table_data = data[0], data[1:] parsed_headers_and_types = [self._parse_header_with_type(h) for h in raw_headers] headers = [h[0] for h in parsed_headers_and_types] data_types = [h[1] for h in parsed_headers_and_types] converted_data = [] for row in table_data: converted_row = [] for value, dtype in zip(row, data_types): try: match dtype: case 'integer': converted_row.append(int(value) if value else '') case 'float': converted_row.append(float(value) if value else '') case 'boolean': converted_row.append(value.lower() == 'true' if value else '') case _: converted_row.append(value) except (ValueError, TypeError): converted_row.append('') converted_data.append(converted_row) return headers, converted_data, data_types case ".json": with open(file_path, "r", encoding="utf-8") as jsonfile: json_data = json.load(jsonfile) headers = json_data.get('headers', []) table_data = json_data.get('data', []) data_types = json_data.get('data_types', ['string'] * len(headers)) return headers, table_data, data_types case _: raise ValueError("Unsupported file format.") def add_table_from_data( self, file_path: str, headers: list[str], table_data: list[list[str]], data_types: list[str] ): """Add a table from file data by creating a unique name and loading it into the application.""" table_name = self.ensure_unique_table_name(Path(file_path).stem) self.app.table_operations.add_table(table_name, data=table_data, headers=headers, data_types=data_types) self.app.table_display.load_selected_table(table_name) self.app.data_manager.save_data() def ensure_unique_table_name(self, table_name: str) -> str: """Ensure table name is unique by appending a counter if necessary.""" original_name = table_name counter = 1 while table_name in self.app.data_manager.tables_data: table_name = f"{original_name} (New {counter})" counter += 1 return table_name def export_table(self): """Export the current table to a file using a file dialog and appropriate writers based on file extension.""" if ( not self.app.current_table_name or self.app.current_table_name not in self.app.data_manager.tables_data ): messagebox.showwarning("No Table Selected", "Please select a table to export.") return file_path = filedialog.asksaveasfilename( title="Export File", defaultextension=".json", filetypes=[("JSON files", "*.json"), ("CSV files", "*.csv")], initialfile=self.app.current_table_name, ) if not file_path: return try: data = self._prepare_table_data_for_export() self._write_table_data(file_path, data) self.app.data_manager.save_data() messagebox.showinfo( "Export Successful", f"Table '{self.app.current_table_name}' has been exported successfully.", ) except Exception as e: messagebox.showerror("Export Failed", f"An error occurred during export: {e}") def _prepare_table_data_for_export(self) -> list[list[Any]]: """Prepare table data for export, including headers and rows.""" return ( [self.app.data_manager.headers[self.app.current_table_name]] + self.app.data_manager.tables_data[self.app.current_table_name]["data"] ) def _write_table_data(self, file_path: str, data: list[list[Any]]): """Write table data to a file in the specified format.""" headers = data[0] table_data = data[1:] data_types = self.app.data_manager.tables_data[self.app.current_table_name].get("data_types", ['string'] * len(headers)) match Path(file_path).suffix.lower(): case ".csv": self.save_table_data(file_path, data, "csv") case ".json": json_data = { 'headers': headers, 'data': table_data, 'data_types': data_types } with open(file_path, "w", encoding="utf-8") as file: json.dump(json_data, file, indent=4) case _: raise ValueError("Unsupported file format.") def save_table_data(self, file_path: str, data: list[list[Any]], file_format: str): """Save table data in a specified format (CSV or JSON).""" if file_format == "csv": headers = data[0] data_rows = data[1:] data_types = self.app.data_manager.tables_data[self.app.current_table_name].get("data_types", ['string'] * len(headers)) headers_with_types = [ self._format_header_with_type(header, dtype) for header, dtype in zip(headers, data_types) ] data_as_strings = [ [self._convert_cell_to_string(cell, data_types[idx]) for idx, cell in enumerate(row)] for row in data_rows ] with open(file_path, "w", newline="", encoding="utf-8") as file: csv.writer(file).writerows([headers_with_types] + data_as_strings) elif file_format == "json": with open(file_path, "w", encoding="utf-8") as file: json.dump(data, file, indent=4) else: raise ValueError("Unsupported file format.") def _convert_cell_to_string(self, cell: Any, data_type: str) -> str: """Convert a cell to a string based on its data type.""" if cell is None: return '' try: match data_type: case "boolean": if isinstance(cell, bool): return str(cell).upper() return str(cell.lower() == 'true').upper() case "float": if isinstance(cell, (int, float)): return str(float(cell)) return str(float(cell.replace(',', ''))) if cell.strip() else '' case "integer": if isinstance(cell, (int, float)): return str(int(cell)) return str(int(float(cell.replace(',', '')))) if cell.strip() else '' case _: return str(cell) except (ValueError, AttributeError, TypeError): return '' class Sidebar: """Handles all sidebar-related UI and interactions.""" def __init__(self, root: tk.Tk, app: 'TableApp'): self.root = root self.app = app self.colors = app.colors self.fonts = app.fonts self.dimensions = app.dimensions self.style = app.style self.table_labels_frame: Optional[tk.Frame] = None self.table_labels: dict[str, tk.Label] = {} self.selected_table_label: Optional[tk.Label] = None self.rename_entry: Optional[tk.Entry] = None self.create_sidebar() def create_sidebar(self) -> None: """Create the sidebar UI components.""" self.sidebar_frame = create_frame( parent=self.root, width=self.dimensions["sidebar_width"], bg=self.colors["sidebar"], ) self.sidebar_frame.grid(row=0, column=0, rowspan=2, sticky="ns") self.sidebar_frame.grid_propagate(False) self.sidebar_frame.columnconfigure(0, weight=1) self.sidebar_frame.rowconfigure(0, weight=1) functional_sidebar = create_frame( parent=self.sidebar_frame, width=self.dimensions["sidebar_width"], bg=self.colors["sidebar"], ) functional_sidebar.grid(row=0, column=0, sticky="nsew") functional_sidebar.columnconfigure(0, weight=1) functional_sidebar.rowconfigure(0, weight=0) functional_sidebar.rowconfigure(1, weight=1) functional_sidebar.rowconfigure(2, weight=0) self._add_normal_buttons(functional_sidebar) self._add_scrollable_canvas(functional_sidebar) self._add_delete_table_section(functional_sidebar) def _add_normal_buttons(self, sidebar: tk.Frame) -> None: """Add buttons to the sidebar.""" normal_buttons_frame = create_frame( parent=sidebar, width=self.dimensions["sidebar_width"], bg=self.colors["sidebar"], ) normal_buttons_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 0)) normal_buttons_frame.columnconfigure(0, weight=1) create_table_label = self._create_clickable_label( parent=normal_buttons_frame, text="Create New Table", command=self.app.table_operations.create_new_table, anchor="center" ) create_table_label.grid(row=0, column=0, pady=10, sticky="ew") self.add_custom_hover(create_table_label) separator = tk.Frame( normal_buttons_frame, height=self.dimensions["separator_height"], bg=self.colors["separator_sidebar"], ) separator.grid(row=1, column=0, sticky="ew", pady=5) def _on_canvas_configure(self, event: tk.Event) -> None: """Update the table_labels_frame width to match the canvas width.""" self.canvas.itemconfig(self.table_labels_window, width=event.width) def _add_scrollable_canvas(self, sidebar: tk.Frame) -> None: """Add scrollable canvas for table labels within a single frame.""" scrollable_container = create_frame( parent=sidebar, width=self.dimensions["sidebar_width"], bg=self.colors["sidebar"], ) scrollable_container.grid(row=1, column=0, sticky="nsew", padx=10, pady=10) scrollable_container.columnconfigure(0, weight=1) scrollable_container.rowconfigure(0, weight=1) self.canvas = tk.Canvas( scrollable_container, bg=self.colors["sidebar"], highlightthickness=0 ) self.canvas.grid(row=0, column=0, sticky="nsew") custom_scrollbar_design(self.style, self.colors) self.scrollbar = ttk.Scrollbar( scrollable_container, orient="vertical", command=self.canvas.yview, style="Custom.Vertical.TScrollbar" ) self.scrollbar.grid(row=0, column=1, sticky="ns") self.canvas.configure(yscrollcommand=self.scrollbar.set) self.table_labels_frame = tk.Frame(self.canvas, bg=self.colors["sidebar"]) self.table_labels_window = self.canvas.create_window( (0, 0), window=self.table_labels_frame, anchor="nw", width=self.dimensions["sidebar_width"] ) self.canvas.bind("<MouseWheel>", self._on_mouse_wheel) self.table_labels_frame.bind("<MouseWheel>", self._on_mouse_wheel) self.table_labels_frame.bind("<Configure>", self._on_table_labels_configure) scrollable_container.bind("<Configure>", self._on_scrollable_container_configure) self.canvas.bind("<Configure>", self._on_canvas_configure) def _on_mouse_wheel(self, event: tk.Event) -> None: """Handle mouse wheel scrolling only if scrollbar is visible.""" if self.scrollbar.winfo_viewable(): self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") def _on_table_labels_configure(self, event: tk.Event) -> None: """Update canvas scroll region and scrollbar visibility.""" self.canvas.configure(scrollregion=self.canvas.bbox("all")) self._update_scrollbar() def _on_scrollable_container_configure(self, event: tk.Event) -> None: """Update scrollbar visibility based on content size.""" self._update_scrollbar() def _update_scrollbar(self) -> None: """Update scrollbar visibility based on content size.""" self.canvas.update_idletasks() canvas_height = self.canvas.winfo_height() content_height = self.table_labels_frame.winfo_reqheight() if content_height > canvas_height: self.scrollbar.grid() self.canvas.bind("<MouseWheel>", self._on_mouse_wheel) else: self.scrollbar.grid_remove() self.canvas.unbind("<MouseWheel>") def _add_delete_table_section(self, sidebar: tk.Frame) -> None: """Add delete table option to the sidebar.""" delete_table_frame = create_frame( parent=sidebar, width=self.dimensions["sidebar_width"], bg=self.colors["sidebar"], ) delete_table_frame.grid(row=2, column=0, sticky="ew", padx=10, pady=(0, 10)) delete_table_frame.columnconfigure(0, weight=1) delete_table_frame.rowconfigure(1, weight=1) separator = tk.Frame( delete_table_frame, height=self.dimensions["separator_height"], bg=self.colors["separator_sidebar"], ) separator.grid(row=0, column=0, sticky="ew", pady=5) delete_table_label = self._create_clickable_label( parent=delete_table_frame, text="🗑 Delete Selected Table", command=self.app.table_operations.delete_table, anchor="center" ) delete_table_label.grid(row=1, column=0, pady=10, sticky="ew") self.add_custom_hover(delete_table_label) def _create_clickable_label( self, parent: tk.Widget, text: str, command, allow_rename: bool = False, anchor: str = "w" ) -> tk.Label: """Create a clickable label with optional rename functionality.""" cursor = "arrow" if allow_rename else "hand2" label = tk.Label( parent, text=text, bg=self.colors["button"], fg=self.colors["text"], cursor=cursor, anchor=anchor, font=self.fonts["text"] ) label.bind("<Button-1>", lambda _: command()) if allow_rename: label.bind("<Double-1>", lambda _: self.app.table_operations.begin_rename(text)) label.bind("<MouseWheel>", self._on_mouse_wheel) return label def add_custom_hover(self, widget: tk.Widget) -> None: """Add custom hover effects to a widget, excluding the selected table label.""" def on_enter(_): if widget != self.selected_table_label: widget.config(bg=self.colors["button_selected"]) def on_leave(_): if widget != self.selected_table_label: widget.config(bg=self.colors["button"]) widget.bind("<Enter>", on_enter) widget.bind("<Leave>", on_leave) def create_table_label(self, table_name: str) -> None: """Create label for table.""" label = self._create_clickable_label( self.table_labels_frame, table_name, lambda: self.app.load_selected_table(table_name), allow_rename=True ) label.pack(fill="x", padx=0, pady=5, expand=True) self.table_labels[table_name] = label self.add_custom_hover(label) self.sort_table_labels() def sort_table_labels(self) -> None: """Sort labels with numeric names first, then alphabetic, then alphanumeric.""" def sort_key(name: str) -> tuple: if name.isdigit(): return (0, int(name)) if (m := re.match(r"^(\d+)\.", name)): return (1, int(m.group(1))) if name.isalpha(): return (2, name.lower()) if (m := re.match(r"^([A-Za-z]+)\s*(\d+)$", name)): return (3, m.group(1).lower(), int(m.group(2))) return (4, name.lower()) sorted_table_names: list[str] = sorted(self.app.data_manager.tables_data.keys(), key=sort_key) for table_name in sorted_table_names: if (label := self.table_labels.get(table_name)) is not None: label.pack_forget() label.pack(fill="x", padx=0, pady=5, expand=True) def cancel_rename(self) -> None: """Cancel rename operation.""" if self.rename_entry: self.rename_entry.destroy() self.rename_entry = None class Header: """Class for handling header UI.""" DATA_TYPE_OPTIONS = ["string", "integer", "float", "boolean"] def __init__(self, root: tk.Tk, app: 'Application') -> None: """Initialize the header component of the table application.""" self.root = root self.app = app self.style = app.style self.colors = app.colors self.fonts = app.fonts self.dimensions = app.dimensions self.table_name_label = None self.lower_header_frame = None self.result = None self._setup_header() self.dropdown = None self.popup_bindings = {} def _setup_header(self) -> None: """Set up the header section and its components.""" header_frame = self._setup_main_frame() self._setup_upper_section(header_frame) self._setup_lower_section(header_frame) def _setup_main_frame(self) -> tk.Frame: """Create and configure the main header frame.""" header_frame = create_frame( parent=self.root, width=None, bg=self.colors["background"] ) header_frame.grid( row=0, column=1, columnspan=2, sticky="ew" ) header_frame.config(height=self.dimensions["header_height"]) header_frame.grid_propagate(False) header_frame.grid_rowconfigure(0, weight=0) header_frame.grid_rowconfigure(1, weight=0) for col_index in range(5): weight = 1 if col_index == 4 else 0 header_frame.grid_columnconfigure(col_index, weight=weight) return header_frame def _setup_upper_section(self, parent: tk.Frame) -> None: """Set up the upper section of the header.""" upper_frame = create_frame(parent=parent, width=None, bg=self.colors["background"]) upper_frame.grid(row=0, column=0, columnspan=6, sticky="ew", pady=(0, 10)) upper_frame.grid_columnconfigure(5, weight=1) self._add_upper_content(upper_frame) def _add_upper_content(self, frame: tk.Frame) -> None: """Add content elements to the upper section.""" tk.Label( frame, text="Data > ", bg=self.colors["background"], fg=self.colors["header_info"], font=self.fonts["header_info"] ).grid(row=0, column=0, padx=22, pady=(22, 0), sticky="nw") self.table_name_label = tk.Label( frame, text=self.app.current_table_name or "No Table Selected", bg=self.colors["background"], fg=self.colors["text"], font=self.fonts["header_info"] ) self.table_name_label.grid(row=0, column=0, padx=(88, 10), pady=(22, 0), sticky="nw") self._setup_settings_section(frame) def _setup_settings_section(self, frame: tk.Frame) -> None: """Set up the settings section with buttons for stats, import, export, and data type dialog.""" settings_container = tk.Frame(frame, bg=self.colors["background"]) settings_container.grid(row=0, column=5, padx=(0, 40), pady=(22, 0), sticky="ne") common_label_options = { 'bg': self.colors["background"], 'fg': self.colors["text"], 'font': self.fonts["text"], 'cursor': "hand2", 'width': 2, 'height': 1 } buttons_frame = tk.Frame(settings_container, bg=self.colors["background"]) buttons_frame.pack(side=tk.LEFT) labels = ['S', 'I', 'E', 'D'] actions = [self.show_stats, self.app.table_io.import_file, self.app.table_io.export_table, self.show_datatype_dialog] for label_text, action in zip(labels, actions): label = tk.Label(buttons_frame, text=label_text, **common_label_options) label.pack(side=tk.LEFT) is_special = label_text in ['S', 'D'] self._bind_label_events(label, action, is_special) def _bind_events(self, widget, on_click, on_enter=None, on_leave=None) -> None: """Bind common events to the given widget.""" if on_click: widget.bind("<Button-1>", lambda e, act=on_click: act(e)) if on_enter: widget.bind("<Enter>", lambda e, w=widget: on_enter(e, w)) if on_leave: widget.bind("<Leave>", lambda e, w=widget: on_leave(e, w)) def _bind_label_events(self, label, action: callable, is_special: bool) -> None: """Bind events to the given label.""" if action == self.app.table_io.import_file or action == self.app.table_io.export_table: on_click = lambda e, act=action: act() else: on_click = (lambda e, btn=label, act=action: self._handle_button_click(e, btn, act)) if is_special else action on_enter = lambda e, lbl=label: lbl.config(bg=self.colors["button_selected"]) on_leave = lambda e, lbl=label: lbl.config(bg=self.colors["background"]) self._bind_events(label, on_click, on_enter, on_leave) def _setup_lower_section(self, parent: tk.Frame) -> None: """Create and configure the lower section of the header.""" self.lower_header_frame = create_frame(parent=parent, width=None, bg=self.colors["background"]) self.lower_header_frame.grid(row=1, column=0, columnspan=5, sticky="ew") def create_styled_popup(self, popup_attr_name: str = 'popup_window', extra_widgets: Optional[list[tk.Widget]] = None) -> tk.Frame: """Create a styled popup window and handle closing events.""" extra_widgets = extra_widgets or [] if hasattr(self, popup_attr_name): getattr(self, popup_attr_name).destroy() popup = tk.Toplevel(self.root) popup.overrideredirect(True) setattr(self, popup_attr_name, popup) inner_frame = self._create_nested_frames(popup) def close_popup(event: Optional[tk.Event] = None) -> None: """Close the popup window and unbind popup events.""" if popup.winfo_exists(): popup.destroy() self._unbind_popup_events(popup_attr_name) def click_outside(event: tk.Event) -> None: """Close the popup if the click is outside the popup and extra widgets.""" if not self._is_descendant(event.widget, [popup] + extra_widgets): close_popup() click_outside_binding = self.root.bind("<Button-1>", click_outside) window_resized_binding = self.root.bind("<Configure>", lambda e: close_popup()) self.popup_bindings[popup_attr_name] = { "click_outside": click_outside_binding, "window_resized": window_resized_binding } popup.bind("<Escape>", close_popup) popup.focus_force() return inner_frame def _create_nested_frames(self, popup: tk.Toplevel) -> tk.Frame: """Create a nested frame structure for popups.""" border_frame = tk.Frame(popup, bg=self.colors["frame_border"]) border_frame.pack(padx=0, pady=0, expand=True, fill=tk.BOTH) content_frame = tk.Frame(border_frame, bg=self.colors["background"]) content_frame.pack(padx=1, pady=1, expand=True, fill=tk.BOTH) inner_frame = tk.Frame(content_frame, bg=self.colors["background"]) inner_frame.pack(padx=10, pady=10, expand=True, fill=tk.BOTH) return inner_frame def _unbind_popup_events(self, popup_attr_name: str) -> None: """Unbind popup events.""" bindings = self.popup_bindings.get(popup_attr_name, {}) for binding_name, binding_id in bindings.items(): self.root.unbind(binding_name, binding_id) self.popup_bindings.pop(popup_attr_name, None) def _is_descendant(self, widget: tk.Widget, ancestors: list[tk.Widget]) -> bool: """Check if a widget is a descendant of any in a list of ancestors.""" while widget: if widget in ancestors: return True widget = widget.master return False def show_stats(self, button: Optional[tk.Widget] = None) -> None: """Display stats about boolean columns in a popup.""" table_name = self.app.current_table_name if not table_name: messagebox.showwarning("No Table Selected", "Please select a table to view stats.") return if not self.app.current_sheet: messagebox.showwarning("No Data", "No data available to show stats.") return data = self.app.current_sheet.get_sheet_data() current_headers = self.app.current_sheet.headers() data_types = self.app.data_manager.tables_data[table_name].get("data_types", []) data_types.extend(['string'] * (len(current_headers) - len(data_types))) total_rows = len(data) boolean_columns = self._get_boolean_columns(data, data_types, current_headers) inner_frame = self.create_styled_popup(popup_attr_name='stats_popup') self._create_stats_content(inner_frame, total_rows, boolean_columns) self._position_popup(self.stats_popup, button) def _get_boolean_columns(self, data, data_types, headers) -> list[tuple[str, int, int]]: """Extract boolean columns and their stats.""" boolean_columns = [] for col_idx, dtype in enumerate(data_types): if dtype == 'boolean': header = headers[col_idx] column_data = [bool(row[col_idx]) if row[col_idx] is not None else False for row in data] true_count = sum(1 for val in column_data if val is True) false_count = sum(1 for val in column_data if val is False) boolean_columns.append((header, true_count, false_count)) return boolean_columns def _create_stats_content(self, inner_frame: tk.Frame, total_rows: int, boolean_columns: list[tuple[str, int, int]]) -> None: """Create content for the stats popup.""" total_label = tk.Label( inner_frame, text=f"Number of Rows: {total_rows}", bg=self.colors["background"], fg=self.colors["text"], font=self.fonts["menu"] ) total_label.pack(pady=(0, 5), anchor='center') separator = tk.Frame(inner_frame, bg=self.colors["separator_menu"], height=1) separator.pack(fill=tk.X, pady=(0, 5)) if boolean_columns: table_frame = tk.Frame(inner_frame, bg=self.colors["background"]) table_frame.pack() headers = ["Column", "True", "False"] for col_num, header_text in enumerate(headers): header_label = tk.Label( table_frame, text=header_text, bg=self.colors["background"], fg=self.colors["text"], font=self.fonts["menu"] ) header_label.grid(row=0, column=col_num, padx=10, pady=2, sticky='w') for row_num, (header, true_count, false_count) in enumerate(boolean_columns, start=1): self._create_stats_row(table_frame, row_num, header, true_count, false_count) for col_num in range(3): table_frame.grid_columnconfigure(col_num, weight=1) else: no_data_label = tk.Label( inner_frame, text="No boolean columns to display.", bg=self.colors["background"], fg=self.colors["text"], font=self.fonts["menu"] ) no_data_label.pack(pady=10) def _create_stats_row(self, table_frame: tk.Frame, row_num: int, header: str, true_count: int, false_count: int) -> None: """Create a row for the stats table.""" header_label = tk.Label( table_frame, text=header, bg=self.colors["background"], fg=self.colors["header_info"], font=self.fonts["menu"] ) header_label.grid(row=row_num, column=0, padx=10, pady=2, sticky='w') true_label = tk.Label( table_frame, text=str(true_count), bg=self.colors["background"], fg=self.colors["header_info"], font=self.fonts["menu"] ) true_label.grid(row=row_num, column=1, padx=10, pady=2, sticky='w') false_label = tk.Label( table_frame, text=str(false_count), bg=self.colors["background"], fg=self.colors["header_info"], font=self.fonts["menu"] ) false_label.grid(row=row_num, column=2, padx=10, pady=2, sticky='w') def show_datatype_dialog(self, button: Optional[tk.Widget] = None) -> None: """Show a popup for changing column data types.""" table_name = self.app.current_table_name if not table_name: messagebox.showwarning("No Table Selected", "Please select a table to change datatypes.") return current_headers = self.app.current_sheet.headers() if self.app.current_sheet else [] self.app.data_manager.headers[table_name] = current_headers self.data_types = self.app.data_manager.tables_data[table_name].get("data_types", []).copy() self.data_types.extend(['string'] * (len(current_headers) - len(self.data_types))) self.datatype_vars = {} inner_frame = self.create_styled_popup(popup_attr_name='datatype_popup') self._create_datatype_content(inner_frame, current_headers) self._position_popup(self.datatype_popup, button) def _create_datatype_content(self, inner_frame: tk.Frame, current_headers: list[str]) -> None: """Create content for the datatype popup.""" for header_index, header in enumerate(current_headers): current_type = self.data_types[header_index] frame = tk.Frame(inner_frame, bg=self.colors["background"]) frame.pack(fill=tk.X, pady=0) header_label = tk.Label( frame, text=f"{header}:", bg=self.colors["background"], fg=self.colors["text"], font=self.fonts["menu"] ) header_label.pack(side=tk.LEFT, padx=(0, 10)) var = tk.StringVar(value=current_type) dropdown_button = tk.Label( frame, textvariable=var, bg=self.colors["background"], fg=self.colors["header_info"], font=self.fonts["menu"], cursor="hand2", width=6, anchor='w' ) dropdown_button.pack(side=tk.RIGHT) self.create_dropdown(dropdown_button, var) self.datatype_vars[header] = var separator = tk.Frame(inner_frame, bg=self.colors["separator_menu"], height=1) separator.pack(fill=tk.X, pady=(5, 5)) self._create_apply_button(inner_frame) def _create_apply_button(self, inner_frame: tk.Frame) -> None: """Create an apply button for the datatype popup.""" apply_container = tk.Frame( inner_frame, bg=self.colors["background"], cursor="hand2" ) apply_container.pack(fill=tk.X, pady=(5, 0)) apply_label = tk.Label( apply_container, text="Apply Changes", bg=self.colors["background"], fg=self.colors["text"], font=self.fonts["menu"], cursor="hand2" ) apply_label.pack(expand=True, fill=tk.BOTH, pady=0) def on_hover_enter(event: tk.Event) -> None: """Change the background color on hover enter.""" apply_container.config(bg=self.colors["button_selected"]) apply_label.config(bg=self.colors["button_selected"]) def on_hover_leave(event: tk.Event) -> None: """Change the background color on hover leave.""" apply_container.config(bg=self.colors["background"]) apply_label.config(bg=self.colors["background"]) apply_container.bind("<Enter>", on_hover_enter) apply_container.bind("<Leave>", on_hover_leave) apply_label.bind("<Enter>", on_hover_enter) apply_label.bind("<Leave>", on_hover_leave) apply_container.bind("<Button-1>", lambda e: self._apply_changes()) apply_label.bind("<Button-1>", lambda e: self._apply_changes()) def create_dropdown(self, button: tk.Label, var: tk.StringVar) -> None: """Create a dropdown menu for the given button and variable.""" on_enter = lambda e, w=button: w.config(bg=self.colors["button_selected"]) on_leave = lambda e, w=button: w.config(bg=self.colors["background"]) self._bind_events(button, None, on_enter, on_leave) def show_dropdown(event: tk.Event) -> str: """Show the dropdown menu for the given button and variable.""" if self.dropdown is not None and self.dropdown.winfo_exists(): self.dropdown.destroy() self.dropdown = None dropdown, inner_frame = self._setup_dropdown_window(button) self.dropdown = dropdown for option in self.DATA_TYPE_OPTIONS: self._create_option_label(inner_frame, option, var, dropdown) dropdown.bind("<Button-1>", lambda e: "break") dropdown.focus_force() self._setup_dropdown_close_handler(dropdown) return "break" button.bind("<Button-1>", show_dropdown) button.bind("<Button-1>", lambda e: "break", add="+") def _create_option_label(self, parent: tk.Frame, option: str, var: tk.StringVar, dropdown: tk.Toplevel) -> tk.Label: """Create and configure a single option label for the dropdown menu.""" option_label = tk.Label( parent, text=option, bg=self.colors["background"], fg=self.colors["header_info"], font=self.fonts["menu"], anchor='w', width=6 ) option_label.pack(fill=tk.X) on_enter = lambda e, w=option_label: w.config(bg=self.colors["button_selected"]) on_leave = lambda e, w=option_label: w.config(bg=self.colors["background"]) on_click = lambda e, opt=option, v=var, d=dropdown: self._on_option_select(e, opt, v, d) self._bind_events(option_label, on_click, on_enter, on_leave) return option_label def _on_option_select(self, event: tk.Event, option: str, var: tk.StringVar, dropdown: tk.Toplevel) -> str: """Handle the selection of an option in the dropdown menu.""" var.set(option) dropdown.destroy() self.dropdown = None return "break" def _setup_dropdown_window(self, button: tk.Label) -> tuple[tk.Toplevel, tk.Frame]: """Set up the dropdown window with proper positioning and styling.""" x = button.winfo_rootx() y = button.winfo_rooty() + button.winfo_height() dropdown = tk.Toplevel(self.datatype_popup) dropdown.overrideredirect(True) dropdown.geometry(f"+{x}+{y}") border_frame = tk.Frame(dropdown, bg=self.colors["frame_border"]) border_frame.pack(padx=0, pady=0, expand=True, fill=tk.BOTH) inner_frame = tk.Frame(border_frame, bg=self.colors["background"]) inner_frame.pack(padx=1, pady=1, expand=True, fill=tk.BOTH) return dropdown, inner_frame def _setup_dropdown_close_handler(self, dropdown: tk.Toplevel) -> None: """Set up a handler to close the dropdown when clicking outside.""" def close_dropdown(event: tk.Event) -> None: if not self._is_descendant(event.widget, [dropdown]): if dropdown.winfo_exists(): dropdown.destroy() self.dropdown = None self.root.unbind("<Button-1>", click_outside_handler) click_outside_handler = self.root.bind("<Button-1>", close_dropdown) return click_outside_handler def _position_popup(self, popup: tk.Toplevel, button: Optional[tk.Widget] = None) -> None: """Position the popup window relative to the button within the main app window bounds.""" POPUP_VERTICAL_OFFSET = 10 WINDOW_MARGIN = 15 self.root.update_idletasks() window_x = self.root.winfo_x() window_y = self.root.winfo_y() window_width = self.root.winfo_width() window_height = self.root.winfo_height() if button: button_x = button.winfo_rootx() - window_x button_y = button.winfo_rooty() - window_y button_width = button.winfo_width() button_height = button.winfo_height() reference_x = button_x + button_width // 2 reference_y = button_y + button_height else: reference_x = self.root.winfo_pointerx() - window_x reference_y = self.root.winfo_pointery() - window_y popup.update_idletasks() popup_width = popup.winfo_width() popup_height = popup.winfo_height() position = { 'x': reference_x - popup_width // 2, 'y': reference_y + POPUP_VERTICAL_OFFSET } if position['x'] < WINDOW_MARGIN: position['x'] = WINDOW_MARGIN elif position['x'] + popup_width > window_width - WINDOW_MARGIN: position['x'] = window_width - popup_width - WINDOW_MARGIN if position['y'] + popup_height > window_height - WINDOW_MARGIN: position['y'] = reference_y - popup_height - POPUP_VERTICAL_OFFSET position['y'] = max(WINDOW_MARGIN, position['y']) screen_x = window_x + position['x'] screen_y = window_y + position['y'] popup.geometry(f"+{screen_x}+{screen_y}") def _handle_button_click(self, event: tk.Event, button: tk.Widget, action: callable) -> None: """Handle button clicks for popup actions.""" if action == self.show_stats: self.show_stats(button) elif action == self.show_datatype_dialog: self.show_datatype_dialog(button) def _apply_changes(self) -> None: """Apply datatype changes and update the table.""" table_name = self.app.current_table_name current_headers = self.app.current_sheet.headers() if self.app.current_sheet else [] self.data_types = [] for header in current_headers: dtype = self.datatype_vars[header].get() self.data_types.append(dtype) self.app.data_manager.tables_data[table_name]["data_types"] = self.data_types self.app.data_manager.save_data() self.app.table_display.apply_column_data_types(self.data_types) self.app.table_display.process_and_save_current_sheet_data() self.app.refresh_table() if hasattr(self, 'datatype_popup') and self.datatype_popup.winfo_exists(): self.datatype_popup.destroy() self._unbind_popup_events('datatype_popup') class FilterControls: """Manages filtering logic and UI components.""" FILTER_OPTIONS: dict[str, callable] = { "is": lambda cell, text: cell == text, "is not": lambda cell, text: cell != text, "contains": lambda cell, text: text in cell, "does not contain": lambda cell, text: text not in cell, "starts with": lambda cell, text: cell.startswith(text), "ends with": lambda cell, text: cell.endswith(text), "is empty": lambda cell, _: cell == "", "is not empty": lambda cell, _: cell != "", } def __init__(self, parent: tk.Frame, app: 'TableApp'): """Initialize the filter controls with the parent frame and application.""" self.app = app self.root = parent self.colors = app.colors self.fonts = app.fonts self.style = app.style self._setup_variables() self.create_filter_controls() def _setup_variables(self) -> None: """Setup tkinter variables used for filtering.""" self.filter_header_var = tk.StringVar(value="All") self.filter_option_var = tk.StringVar(value="contains") self.filter_text_var = tk.StringVar() def create_filter_controls(self) -> None: """Create and configure filter controls.""" header_dropdown, option_dropdown, text_entry = self._create_filter_components() self._configure_filter_components(header_dropdown, option_dropdown, text_entry) self._bind_filter_events() def _create_filter_components(self) -> tuple[tk.Frame, tk.Frame, tk.Frame]: """Create filter UI components.""" headers = self.app.data_manager.headers.get(self.app.current_table_name, ["All"]) filter_frame, header_dropdown, option_dropdown, text_entry = custom_filter_design( self.root, self.style, headers=headers, filter_types=list(self.FILTER_OPTIONS.keys()), filter_font=self.fonts["filter"], filter_combobox_font=self.fonts["filter_combobox"], colors=self.colors, ) filter_frame.grid(row=0, column=1, columnspan=4, padx=25, pady=15, sticky="ew") return header_dropdown, option_dropdown, text_entry def _configure_filter_components(self, header_dropdown: tk.Frame, option_dropdown: tk.Frame, text_entry: tk.Frame) -> None: """Configure filter components with variables and display settings.""" self.filter_header_dropdown = header_dropdown self.filter_option_dropdown = option_dropdown self.filter_text_entry = text_entry self.filter_header_dropdown["textvariable"] = self.filter_header_var self.filter_option_dropdown["textvariable"] = self.filter_option_var self.filter_text_entry["textvariable"] = self.filter_text_var def _bind_filter_events(self) -> None: """Bind events to filter components.""" self.filter_header_dropdown.bind("<<ComboboxSelected>>", self.apply_filter) self.filter_option_dropdown.bind( "<<ComboboxSelected>>", lambda event: [self.update_filter_text_state(event), self.apply_filter()], ) self.filter_text_entry.bind("<KeyRelease>", self.apply_filter) self.filter_text_entry.bind("<Return>", self.apply_filter) def update_filter_text_state(self, event=None) -> None: """Enable or disable the filter text entry based on filter option.""" option = self.filter_option_var.get() is_special_option = option in ["is empty", "is not empty"] self.filter_text_entry.config(state="disabled" if is_special_option else "normal") if is_special_option: self.filter_text_var.set("") self.apply_filter() def apply_filter(self, event=None) -> None: """Apply filter to table data based on selected criteria.""" if not self._is_filtering_possible(): return displayed_indices = self._filter_data() self._update_display(displayed_indices) def _is_filtering_possible(self) -> bool: """Check if filtering is possible with the current application state.""" return bool(self.app.current_table_name and self.app.current_sheet) def _filter_data(self) -> list[int]: """Filter data based on current filter settings.""" header = self.filter_header_var.get() option = self.filter_option_var.get() text = self.filter_text_var.get().lower() original_data = self.app.data_manager.tables_data[self.app.current_table_name].get("data", []) target_indices = self._get_target_indices(header) filter_func = self.FILTER_OPTIONS[option] displayed_indices = [ i for i, row in enumerate(original_data) if any(filter_func(str(row[idx]).lower(), text) for idx in target_indices) ] return displayed_indices def _get_target_indices(self, header: str) -> list[int]: """Return indices to filter on based on the selected header.""" headers = self.app.data_manager.headers[self.app.current_table_name] if header == "All": return list(range(len(headers))) if header in headers: return [headers.index(header)] return [] def _update_display(self, displayed_indices: list[int]) -> None: """Update the display with filtered data and reset row indexing.""" try: total_rows = len(self.app.current_sheet.get_sheet_data()) self.app.current_sheet.hide_rows(set(range(total_rows)), data_indexes=True) self.app.current_sheet.display_rows(displayed_indices, all_displayed=False, data_indexes=True) new_index_values = [] current_number = 1 for i in range(total_rows): if i in displayed_indices: new_index_values.append(str(current_number)) current_number += 1 else: new_index_values.append("") self.app.current_sheet.set_index_data(new_index_values, redraw=True) self.app.displayed_row_indices = displayed_indices except tk.TclError as e: print(f"Error applying filter: {e}") def reset_row_indices(self) -> None: """Restore original row indices when clearing filters.""" if self.app.current_sheet: total_rows = len(self.app.current_sheet.get_sheet_data()) original_indices = [str(i + 1) for i in range(total_rows)] self.app.current_sheet.set_index_data(original_indices, redraw=True) if __name__ == "__main__": set_dpi_awareness() root = tk.Tk() app = TableApp(root) root.mainloop()