Open the search bar and the variables bar to the right of the buttons.The buttons should move to the left to make space for the opening bar and move back when the bar closes. The buttons should move individually so if the search bar opens only the search button should move. The bars should not go over the right border of the results field. Python 3.12 code: import math
import re
from typing import Any, Union, Callable

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

def convert_angle(func: Callable[[float], float], x: float, use_degrees: bool) -> float:
"""Apply trig function with optional degree conversion."""
return func(math.radians(x)) if use_degrees else func(x)

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

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

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

def get_safe_globals(use_degrees: bool, variables: dict) -> dict:
"""Provide safe globals for expression evaluation."""
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,
"K": celsius_to_kelvin,
"C": kelvin_to_celsius,
"F": celsius_to_fahrenheit,
"CF": fahrenheit_to_celsius,
"capitalize": capitalize_sentence,
"duplicates": remove_duplicates,
}
for func in trig_funcs:
safe_globals[func] = lambda x, f=func: convert_angle(getattr(math, f), x, use_degrees)
safe_globals.update(variables)
return safe_globals

def preprocess_text_functions(expression: str) -> str:
"""Preprocess text functions with argument quotes."""
def add_quotes(match):
func, args = match.groups()
args = args.strip()
if not (args.startswith('"') and args.endswith('"')) and not (args.startswith("'") and args.endswith("'")):
args = f'"{args}"'
return f'{func}({args})'
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]:
"""Check inverse trig function domain validity."""
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}({arg}) is undefined for |{arg}| > 1"
except Exception as e:
return f"Error: {e}"
return None

def safe_evaluate(expression: str, safe_globals: dict) -> Union[str, Any]:
"""Safely evaluate expression and format result with three decimal places."""
try:
result = eval(expression, {"builtins": None}, safe_globals)
if isinstance(result, (int, float)):
if isinstance(result, float):
abs_result = abs(result)
if (abs_result >= 1e9) or (abs_result != 0 and abs_result < 1e-6):
result = f"{result:.3e}"
else:
result = f"{result:.3f}"
else:
if len(str(abs(result))) > 9:
result = f"{result:.3e}"
else:
result = str(result)
return result
else:
return result
except Exception as e:
return f"Error: {e}"

def evaluate_expression(expression: str, variables: dict, use_degrees: bool = False) -> Union[float, str]:
"""Evaluate math expression considering modes."""
assignment_match = re.match(r'^\s*([a-zA-Z_]\w*)\s*=\s*(.+)$', expression)
expression = preprocess_text_functions(expression)
safe_globals = get_safe_globals(use_degrees, variables)

if assignment_match:
var_name, var_expr = assignment_match.groups()
var_value = safe_evaluate(var_expr, safe_globals)
try:
if isinstance(var_value, str) and not var_value.startswith("Error"):
if '.' in var_value:
var_value_converted = float(var_value)
else:
var_value_converted = int(var_value)
variables[var_name] = var_value_converted
return f"{var_name} = {var_value_converted}"
else:
return var_value
except ValueError:
return f"Error: Cannot assign non-numeric value to variable '{var_name}'"
else:
error = validate_inverse_trig(expression, safe_globals)
if error:
return error
return safe_evaluate(expression, safe_globals)

class CalculatorGUI(QMainWindow):
"""PySide6-based calculator application."""

def init(self):
super().init()
self.use_degrees = False
self.variables = {}
self.history_entries = []
self.text_functions = ['capitalize', 'duplicates']
self.setup_window()
self.create_widgets()
self.layout_widgets()
self.connect_signals()

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

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

self.entry = QLineEdit()
self.entry.setFont(font)
self.entry.setPlaceholderText('Enter expression')
self.entry.setClearButtonEnabled(True)
self.entry.returnPressed.connect(self.calculate)

self.history_text = QTextEdit()
self.history_text.setFont(font)
self.history_text.setReadOnly(True)
self.history_text.setStyleSheet("position: relative;")

self.scrollbar = QScrollBar(Qt.Vertical)
self.history_text.setVerticalScrollBar(self.scrollbar)

# *** Rearranged Buttons: Search, Variables, Delete History ***

# Search Button
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);
        font-size: 14px;
        border-radius: 12px;
    }
    QPushButton:hover {
        background-color: rgba(0, 120, 215, 200);
        color: white;
    }
""")
self.search_button.clicked.connect(self.toggle_search)
self.search_button.raise_()

# Variables Button
self.vars_button = QPushButton("🧮", self.history_text)  # Using an abacus emoji as an example
self.vars_button.setFixedSize(24, 24)
self.vars_button.setToolTip("Show Variables")
self.vars_button.setStyleSheet("""
    QPushButton {
        border: none;
        background-color: rgba(255, 255, 255, 200);
        font-size: 14px;
        border-radius: 12px;
    }
    QPushButton:hover {
        background-color: rgba(0, 150, 136, 200);
        color: white;
    }
""")
self.vars_button.clicked.connect(self.toggle_vars_overlay)
self.vars_button.raise_()

# Delete History Button
self.delete_button = QPushButton("🗑️", self.history_text)
self.delete_button.setFixedSize(24, 24)
self.delete_button.setToolTip("Clear History")
self.delete_button.setStyleSheet("""
    QPushButton {
        border: none;
        background-color: rgba(255, 255, 255, 200);
        font-size: 14px;
        border-radius: 12px;
    }
    QPushButton:hover {
        background-color: rgba(255, 0, 0, 200);
        color: white;
    }
""")
self.delete_button.clicked.connect(self.clear_history)
self.delete_button.raise_()

# Variables Overlay
self.vars_overlay = QLineEdit(self.history_text)
self.vars_overlay.setReadOnly(True)
self.vars_overlay.setPlaceholderText("a = 1, b = 85, ...")
self.vars_overlay.setFixedHeight(24)
self.vars_overlay.setStyleSheet("""
    QLineEdit {
        border: 1px solid gray;
        border-radius: 4px;
        padding: 2px 4px;
        background-color: white;
    }
""")
self.vars_overlay.setVisible(False)

# Delete Variables Button within Variables Overlay
self.vars_delete_button = QPushButton("🗑️", self.history_text)
self.vars_delete_button.setFixedSize(24, 24)
self.vars_delete_button.setToolTip("Clear Variables")
self.vars_delete_button.setStyleSheet("""
    QPushButton {
        border: none;
        background-color: rgba(255, 255, 255, 200);
        font-size: 14px;
        border-radius: 12px;
    }
    QPushButton:hover {
        background-color: rgba(255, 0, 0, 200);
        color: white;
    }
""")
self.vars_delete_button.clicked.connect(self.clear_variables)
self.vars_delete_button.setVisible(False)

# Search Bar
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)

# 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 calculator functions."""
layout = QGridLayout()
categories = [("Functions", 3), ("Conversions", 2), ("Text", 2), ("Constants", 2), ("Settings", 1)]
starting_col = 0

for category, col_span in categories:
    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, is_text_function=(f in self.text_functions)))
            layout.addWidget(button, (i // 3) + 1, starting_col + (i % 3))

    elif category == "Settings":
        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)

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

    elif category == "Text":
        for idx, func in enumerate(buttons):
            button = QPushButton(func)
            button.setFont(font)
            button.clicked.connect(lambda _, f=func: self.insert_function(f, is_text_function=True))
            row, col = 1 + idx // col_span, starting_col + idx % col_span
            layout.addWidget(button, row, col)

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

    # Fill empty spaces if needed
    total_rows = math.ceil(len(buttons) / col_span)
    for extra in range(col_span * total_rows - len(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 in the main window."""
central_widget = QWidget()
main_layout = QVBoxLayout()

main_layout.addWidget(self.entry)

history_layout = QHBoxLayout()
history_layout.addWidget(self.history_text)
history_layout.addWidget(self.scrollbar)
main_layout.addLayout(history_layout)

main_layout.addWidget(self.function_frame)

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

def connect_signals(self) -> None:
"""Connect widget signals."""
QApplication.instance().focusChanged.connect(self.on_focus_changed)

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

def insert_function(self, function: str, is_constant: bool = False, is_text_function: bool = False) -> None:
"""Insert function or constant in 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) + 1
elif is_text_function and function in self.text_functions:
new_text = f'{current_text[:cursor_position]}{function}() {current_text[cursor_position:]}'
new_cursor_position = cursor_position + len(function) + 1
else:
new_text = f"{current_text[:cursor_position]}{function}() {current_text[cursor_position:]}"
new_cursor_position = cursor_position + len(function) + 1
self.entry.setText(new_text)
self.entry.setCursorPosition(new_cursor_position)
self.entry.setFocus()

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

def display_result(self, expression: str, result: Union[float, str]) -> None:
"""Show result in history."""
mode = "D" if self.use_degrees else "R"
if re.match(r'^\s*[a-zA-Z_]\w*\s*=\s*.+$', expression):
history_entry = f"[{mode}] {result}"
else:
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()
)

def toggle_search(self) -> None:
"""Toggle search bar visibility."""
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("")
else:
self.search_bar.clear()
self.update_overlay_buttons_position()
self.refresh_history()

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

def refresh_history(self, filter_text: str = "") -> None:
"""Refresh history view with optional filtering."""
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."""
super().resizeEvent(event)
self.update_overlay_buttons_position()

def update_overlay_buttons_position(self) -> None:
"""Reposition search, vars, delete buttons and related overlays over the history_text widget."""
size = self.history_text.viewport().size()

margin_right = 5
margin_top = 5
spacing = 5

# Calculate x position for buttons (right side)
btn_x = size.width() - self.search_button.width() - margin_right

# *** Arrange buttons vertically: Search, Variables, Delete History ***

# Position Search Button at top-right
self.search_button.move(
    btn_x,
    margin_top
)

# Position Variables Button below Search Button
self.vars_button.move(
    btn_x,
    self.search_button.y() + self.search_button.height() + spacing
)

# Position Delete History Button below Variables Button
self.delete_button.move(
    btn_x,
    self.vars_button.y() + self.vars_button.height() + spacing
)

# Variables Overlay Position (to the left of the Variables Button)
if self.vars_overlay.isVisible():
    overlay_width = int(size.width() * 0.3)  # Adjust width as needed
    self.vars_overlay.setFixedWidth(overlay_width)
    self.vars_overlay.move(
        self.vars_button.x() - overlay_width - spacing,
        self.vars_button.y()
    )

    # Position the Delete Variables Button within the Variables Overlay
    self.vars_delete_button.setVisible(True)
    self.vars_delete_button.move(
        self.vars_overlay.x() + self.vars_overlay.width() - self.vars_delete_button.width() - margin_right,
        self.vars_overlay.y()  # Align vertically with vars_overlay
    )
else:
    self.vars_delete_button.setVisible(False)

# Search Bar Position (next to Search Button)
if self.search_bar.isVisible():
    search_bar_width = int(size.width() * 0.25)
    # Ensure the search bar does not overlap with vars_overlay
    if self.vars_overlay.isVisible():
        available_width = self.vars_overlay.x() - spacing - search_bar_width
    else:
        available_width = size.width() - self.search_button.width() - spacing - search_bar_width
    if available_width < 0:
        search_bar_width = self.search_button.x() - spacing
    self.search_bar.setFixedWidth(search_bar_width)
    self.search_bar.move(
        self.search_button.x() - search_bar_width - spacing,
        margin_top
    )

def on_focus_changed(self, old: QWidget, new: QWidget) -> None:
"""Auto-hide search if focus is lost."""
if self.search_bar.isVisible() and not self.search_bar.text().strip() and new != self.search_bar:
self.toggle_search()

def showEvent(self, event) -> None:
"""Position overlay buttons on showing the window."""
super().showEvent(event)
self.update_overlay_buttons_position()

def clear_history(self) -> None:
"""Clear the history entries and update the display."""
self.history_entries.clear()
self.refresh_history()

def toggle_vars_overlay(self) -> None:
"""Toggle the visibility of the variables overlay."""
if self.vars_overlay.isVisible():
self.vars_overlay.setVisible(False)
else:
self.update_vars_overlay()
self.vars_overlay.setVisible(True)
self.update_overlay_buttons_position()

def update_vars_overlay(self) -> None:
"""Update the variables overlay with current variables."""
if not self.variables:
self.vars_overlay.setText("No variables assigned.")
else:
vars_text = ", ".join(f"{var} = {value}" for var, value in self.variables.items())
self.vars_overlay.setText(vars_text)

def clear_variables(self) -> None:
"""Clear all variables and update the variables overlay."""
self.variables.clear()
self.update_vars_overlay()
self.vars_overlay.setVisible(False)
self.vars_delete_button.setVisible(False)

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

if name == "main":
main()