SYSTEM: You are a Python programmer tasked with refactoring code. You are proficient in Python 3.12 and use modern features where appropriate. Your code is clear, easy to read, and free of unnecessary verbosity. You follow PEP 8, 20 and 257 to ensure high quality python code. You remove comments and instead focus on simple and clear on line docstrings. USER: import math
import re
from typing import Any, Union, Callable
from math import ceil

from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLineEdit, QTextEdit, QScrollBar, QPushButton, QLabel, QFrame,
QGridLayout
)
from PySide6.QtGui import QFont
from PySide6.QtCore import Qt, QSize
-- Utility Functions --

def convert_angle(func: Callable[[float], float], x: float, use_degrees: bool) -> float:
"""Convert angle based on degree mode."""
return func(math.radians(x) if use_degrees else x)
-- Conversions Category --

def kilograms_to_pounds(kg: float) -> float:
"""Convert kilograms to pounds."""
return kg / 0.45359237

def pounds_to_kilograms(lb: float) -> float:
"""Convert pounds to kilograms."""
return lb * 0.45359237

def celsius_to_kelvin(c: float) -> float:
"""Convert Celsius to Kelvin."""
return c + 273.15

def kelvin_to_celsius(k: float) -> float:
"""Convert Kelvin to Celsius."""
return k - 273.15

def celsius_to_fahrenheit(c: float) -> float:
"""Convert Celsius to Fahrenheit."""
return c * 9/5 + 32

def fahrenheit_to_celsius(f: float) -> float:
"""Convert Fahrenheit to Celsius."""
return (f - 32) * 5/9
-- Text Category --

def capitalize_sentence(sentence: str) -> str:
"""Capitalize the first letter of each word in the sentence."""
return ' '.join(word.capitalize() for word in sentence.split())

def remove_duplicates(text: str) -> str:
"""Remove duplicate words and empty lines from a string."""
words = ' '.join(line.strip() for line in text.splitlines() if line.strip()).split()
return ' '.join(dict.fromkeys(words))
-- Processing --

def get_safe_globals(use_degrees: bool, variables: dict) -> dict:
"""Return a dictionary of safe globals for expression evaluation, including user-defined variables."""
trig_funcs = {'sin', 'cos', 'tan', 'asin', 'acos', 'atan'}
safe_globals = {
"sqrt": math.sqrt,
"log": math.log10,
"ln": math.log,
"pi": math.pi,
"e": math.e,
"lb": kilograms_to_pounds,
"kg": pounds_to_kilograms,
"C": kelvin_to_celsius,
"K": celsius_to_kelvin,
"F": celsius_to_fahrenheit,
"CF": fahrenheit_to_celsius,
"capitalize": capitalize_sentence,
"duplicates": remove_duplicates,
}
# Add trigonometric functions with angle conversion
for func in trig_funcs:
safe_globals[func] = (lambda f: (lambda x: convert_angle(math.dict[f], x, use_degrees)))(func)
# Include user-defined variables
safe_globals.update(variables)
return safe_globals

def preprocess_text_functions(expression: str) -> str:
"""Preprocess text functions to ensure proper quoting of arguments."""
def add_quotes(match):
func, args = match.groups()
return f'{func}("{args}")' if func in ['capitalize', 'duplicates'] else match.group(0)
# Corrected regex pattern with only two capturing groups
return re.sub(r'\b(capitalize|duplicates)\s*(\s*([^)]+)\s*)', add_quotes, expression)

def validate_inverse_trig(expression: str, safe_globals: dict) -> Union[str, None]:
"""Validate inputs for inverse trigonometric functions."""
for func, arg in re.findall(r"(\b\w+\b)\s*(\s*([^)]+)\s*)", expression):
if func in ["asin", "acos"]:
try:
value = eval(arg, {"builtins": None}, safe_globals)
if abs(value) > 1:
return f"Error: {func}(x) is undefined for |x| > 1"
except Exception as e:
return f"Error: {e}"
return None

def safe_evaluate(expression: str, safe_globals: dict, use_e_notation: bool) -> Union[str, Any]:
"""Safely evaluate the expression and format the result."""
try:
result = eval(expression, {"builtins": None}, safe_globals)
if isinstance(result, float) and abs(result) < 1e-10:
result = 0.0
if isinstance(result, (int, float)):
if use_e_notation:
# Format using scientific notation with up to 2 decimal places
result = f"{result:.2e}"
else:
# Standard formatting with rounding
result = round(result, 2)
return result
except Exception as e:
return f"Error: {e}"

def evaluate_expression(expression: str, variables: dict, use_degrees: bool = False, use_e_notation: bool = False) -> Union[float, str]:
"""Safely evaluate a mathematical expression considering degree mode and handle variable assignments."""
# Check for variable assignment
assignment_match = re.match(r'^\s*([a-zA-Z_]\w*)\s*=\s*(.+)$', expression)
if assignment_match:
var_name, var_expr = assignment_match.groups()
# Evaluate the expression for the variable
safe_globals = get_safe_globals(use_degrees, variables)
var_value = safe_evaluate(var_expr, safe_globals, use_e_notation)
if isinstance(var_value, (int, float)):
variables[var_name] = var_value
return f"{var_name} = {var_value}"
elif isinstance(var_value, str) and var_value.startswith("Error"):
return var_value
else:
return f"{var_name} = {var_value}"
else:
# Regular expression evaluation
safe_globals = get_safe_globals(use_degrees, variables)
expression = expression.replace("^", "**")
expression = preprocess_text_functions(expression)
error = validate_inverse_trig(expression, safe_globals)
if error:
return error
return safe_evaluate(expression, safe_globals, use_e_notation)
-- GUI --

class CalculatorGUI(QMainWindow):
"""PySide6-based Calculator with Degrees/Radians toggle, Variables, History, and Search."""

def __init__(self):
    super().__init__()
    self.use_degrees = False
    self.use_e_notation = False
    self.variables = {}  # Dictionary to store variables
    self.history_entries = []  # List to store history entries
    self.setup_window()
    self.create_widgets()
    self.layout_widgets()
    self.connect_signals()

def setup_window(self) -> None:
    """Configure the main application window."""
    self.setWindowTitle("Calculator")
    self.setGeometry(100, 100, 1280, 720)

def create_widgets(self) -> None:
    """Initialize all GUI components."""
    font = QFont()
    font.setPointSize(12)
    font.setFamily('Literata')

    # Entry field
    self.entry = QLineEdit()
    self.entry.setFont(font)
    self.entry.setPlaceholderText('Enter expression')
    self.entry.setClearButtonEnabled(True)
    self.entry.returnPressed.connect(self.calculate)  # Ensure this line is present

    # History text area
    self.history_text = QTextEdit()
    self.history_text.setFont(font)
    self.history_text.setReadOnly(True)
    self.history_text.setStyleSheet("")  # Removed padding-right to eliminate vertical bar

    # Scrollbar for history
    self.scrollbar = QScrollBar(Qt.Vertical)
    self.history_text.setVerticalScrollBar(self.scrollbar)

    # Create search button as overlay
    self.search_button = QPushButton("🔍", self.history_text)
    self.search_button.setFixedSize(24, 24)
    self.search_button.setToolTip("Search History")
    self.search_button.setStyleSheet("""
        QPushButton {
            border: none;
            background-color: rgba(255, 255, 255, 200); /* Semi-transparent background */
            font-size: 14px;
            border-radius: 12px;
        }
        QPushButton:hover {
            background-color: rgba(0, 120, 215, 200); /* Blue on hover */
            color: white;
        }
    """)
    self.search_button.clicked.connect(self.toggle_search)

    # Search bar (hidden initially)
    self.search_bar = QLineEdit(self.history_text)
    self.search_bar.setPlaceholderText("Search...")
    self.search_bar.setFixedHeight(24)
    self.search_bar.setVisible(False)
    self.search_bar.setStyleSheet("""
        QLineEdit {
            border: 1px solid gray;
            border-radius: 4px;
            padding: 2px 4px;
            background-color: white;
        }
    """)
    self.search_bar.textChanged.connect(self.filter_history)
    self.search_bar.setClearButtonEnabled(True)  # Enable the clear (delete) button

    # Function buttons
    self.function_buttons = {
        "Functions": ["sqrt", "sin", "cos", "tan", "asin", "acos", "atan", "log", "ln"],
        "Conversions": ["lb", "kg", "K", "C", "F", "CF"],
        "Text": ["capitalize", "duplicates"],
        "Constants": ["pi", "e"],
        "Settings": []
    }
    self.function_frame = QFrame()
    self.create_function_buttons(font)

def create_function_buttons(self, font: QFont) -> None:
    """Create buttons for inserting functions into the entry."""
    layout = QGridLayout()

    # Define categories and their column spans
    categories = [("Functions", 3), ("Conversions", 2), ("Text", 2), ("Constants", 2), ("Settings", 1)]
    starting_col = 0

    for category, col_span in categories:
        # Add category label
        label = QLabel(category)
        label.setFont(font)
        layout.addWidget(label, 0, starting_col, 1, col_span, Qt.AlignCenter)

        buttons = self.function_buttons.get(category, [])
        if category == "Functions":
            for i, func in enumerate(buttons):
                button = QPushButton(func)
                button.setFont(font)
                button.clicked.connect(lambda _, f=func: self.insert_function(f, False))
                layout.addWidget(button, (i // 3) + 1, starting_col + (i % 3))

        elif category == "Settings":
            # Arrange Settings buttons vertically within a single column
            self.mode_button = QPushButton("Radians")
            self.mode_button.setFont(font)
            self.mode_button.clicked.connect(self.toggle_mode)
            layout.addWidget(self.mode_button, 1, starting_col, 1, 1)

            self.e_notation_button = QPushButton("E Notation: Off")
            self.e_notation_button.setFont(font)
            self.e_notation_button.clicked.connect(self.toggle_e_notation)
            layout.addWidget(self.e_notation_button, 2, starting_col, 1, 1)

        elif category == "Conversions":
            for idx, func in enumerate(buttons):
                if func == "lb":
                    display_text = "lb(kg)"
                elif func == "kg":
                    display_text = "kg(lb)"
                elif func == "K":
                    display_text = "K(°C)"
                elif func == "C":
                    display_text = "°C(K)"
                elif func == "F":
                    display_text = "F(°C)"
                elif func == "CF":
                    display_text = "°C(F)"
                else:
                    display_text = func
                button = QPushButton(display_text)
                button.setFont(font)
                button.clicked.connect(lambda _, f=func: self.insert_function(f, False))
                row = 1 + (idx // 2)
                col = starting_col + (idx % 2)
                layout.addWidget(button, row, col)

        else:
            num_buttons = len(buttons)
            num_rows = ceil(num_buttons / col_span)

            for idx, func in enumerate(buttons):
                button_text = f"{func}"
                button = QPushButton(button_text)
                button.setFont(font)
                button.clicked.connect(lambda _, f=func, c=(category == "Constants"): self.insert_function(f, c))
                row = 1 + (idx // col_span)
                col = starting_col + (idx % col_span)
                layout.addWidget(button, row, col)

            for extra in range(col_span * num_rows - num_buttons):
                layout.addWidget(QLabel(""), 1 + (extra // col_span), starting_col + (extra % col_span))

        starting_col += col_span

    self.function_frame.setLayout(layout)

def layout_widgets(self) -> None:
    """Arrange widgets within the window."""
    central_widget = QWidget()
    main_layout = QVBoxLayout()

    # Add entry field
    main_layout.addWidget(self.entry)

    # Add history and scrollbar in a horizontal layout
    history_layout = QHBoxLayout()
    history_layout.addWidget(self.history_text)
    history_layout.addWidget(self.scrollbar)
    main_layout.addLayout(history_layout)

    # Add function buttons frame
    main_layout.addWidget(self.function_frame)

    central_widget.setLayout(main_layout)
    self.setCentralWidget(central_widget)

def connect_signals(self) -> None:
    """Connect additional signals."""
    # Connect focusChanged signal to handle automatic hiding of search bar
    QApplication.instance().focusChanged.connect(self.on_focus_changed)

def toggle_mode(self) -> None:
    """Switch between Degrees and Radians modes."""
    self.use_degrees = not self.use_degrees
    self.mode_button.setText("Degrees" if self.use_degrees else "Radians")

def toggle_e_notation(self) -> None:
    """Toggle the E notation setting."""
    self.use_e_notation = not self.use_e_notation
    state = "On" if self.use_e_notation else "Off"
    self.e_notation_button.setText(f"E Notation: {state}")

def insert_function(self, function: str, is_constant: bool = False) -> None:
    """Insert function or constant text into the expression entry."""
    current_text = self.entry.text()
    cursor_position = self.entry.cursorPosition()
    if is_constant:
        new_text = f"{current_text[:cursor_position]}{function}"
        new_cursor_position = cursor_position + len(function)
    else:
        new_text = f"{current_text[:cursor_position]}{function}() {current_text[cursor_position:]}"
        new_cursor_position = cursor_position + len(function) + 2  # Adjusted for parentheses and space
    self.entry.setText(new_text)
    self.entry.setCursorPosition(new_cursor_position)
    self.entry.setFocus()

def calculate(self) -> None:
    """Evaluate the expression and update history."""
    expression = self.entry.text().strip()
    if expression:
        result = evaluate_expression(
            expression, self.variables, use_degrees=self.use_degrees, use_e_notation=self.use_e_notation
        )
        self.display_result(expression, result)
        self.entry.clear()

def display_result(self, expression: str, result: Union[float, str]) -> None:
    """Append the calculation to the history log."""
    mode = "D" if self.use_degrees else "R"
    # Check if the expression is a variable assignment
    if re.match(r'^\s*[a-zA-Z_]\w*\s*=\s*.+$', expression):
        # For variable assignments, display only the expression
        history_entry = f"[{mode}] {result}"
    else:
        # For regular expressions, display expression and result
        history_entry = f"[{mode}] {expression} = {result}"
    self.history_entries.append(history_entry)
    self.refresh_history()
    self.history_text.verticalScrollBar().setValue(
        self.history_text.verticalScrollBar().maximum()
    )

# ------------------- Search Functionality -------------------

def toggle_search(self) -> None:
    """Show or hide the search bar."""
    is_visible = self.search_bar.isVisible()
    self.search_bar.setVisible(not is_visible)
    if not is_visible:
        self.search_bar.setFocus()
        self.search_bar.setText("")  # Clear previous search
    else:
        self.search_bar.clear()
    self.update_search_bar_position()
    self.refresh_history()

def filter_history(self, text: str) -> None:
    """Filter the history based on the search query."""
    self.refresh_history(text)

def refresh_history(self, filter_text: str = "") -> None:
    """Refresh the history text field based on the filter."""
    self.history_text.clear()
    for entry in self.history_entries:
        if filter_text.lower() in entry.lower():
            self.history_text.append(entry)

def resizeEvent(self, event) -> None:
    """Handle window resize events to reposition search button and search bar."""
    super().resizeEvent(event)
    self.update_search_bar_position()

def update_search_bar_position(self) -> None:
    """Update the position of the search button and search bar relative to the history_text."""
    margin_right = 5
    margin_top = 5
    spacing = 5

    history_width = self.history_text.viewport().width()
    history_height = self.history_text.viewport().height()

    # Position the search button at the top-right corner
    self.search_button.move(
        history_width - self.search_button.width() - margin_right,
        margin_top
    )

    if self.search_bar.isVisible():
        # Position the search bar to the left of the search button
        # Ensure it does not go beyond the history_text's viewport
        search_bar_width = int(history_width * 0.25)
        if search_bar_width + self.search_button.width() + spacing > history_width:
            search_bar_width = history_width - self.search_button.width() - spacing - margin_right
        self.search_bar.setFixedWidth(search_bar_width)
        self.search_bar.move(
            self.search_button.x() - self.search_bar.width() - spacing,
            self.search_button.y()
        )

def on_focus_changed(self, old: QWidget, new: QWidget) -> None:
    """
    Handle focus changes to automatically hide the search bar
    if it is empty and loses focus.
    """
    # If the search bar is visible, empty, and not focused, hide it
    if self.search_bar.isVisible() and self.search_bar.text().strip() == "" and new != self.search_bar:
        self.toggle_search()

def showEvent(self, event) -> None:
    """Handle the event when the window is shown to correctly position the search button."""
    super().showEvent(event)
    self.update_search_bar_position()

# --------------------------------------------------------------

def main() -> None:
"""Run the Calculator application."""
app = QApplication([])
calculator = CalculatorGUI()
calculator.show()
app.exec()

if name == "main":
main()