Add the spacer frame that already is between the sidebar and the table also after the table. 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()