diff --git a/.gitignore b/.gitignore index 5575c87..cee7a7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,46 @@ +# IDE配置文件 .vscode/ +.idea/ + +# Python编译文件 __pycache__/ -*.pyc \ No newline at end of file +*.pyc +*.pyo +*.pyd +.Python + +# 虚拟环境 +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# 历史记录文件(可选择是否忽略) +app/settings/history.json + +# 日志文件 +*.log + +# 操作系统生成的文件 +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# 备份文件 +*.bak +*.tmp +*.temp + +# 测试覆盖率报告 +.coverage +htmlcov/ + +# 分发包 +build/ +dist/ +*.egg-info/ \ No newline at end of file diff --git a/README.md b/README.md index eeee17a..00ddfb9 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,40 @@
-

Calculadora Tk

- +

Calculator Tk

+
-## Motivação -O projeto tem por objetivo incentivar iniciantes na programação em python a contribuir com projetos open source que vão além do Terminal, de modo que seja mais visual o desenvolvimento. +## Motivation +The project aims to encourage programming beginners in Python to contribute to open source projects that go beyond the Terminal, making development more visual. -Sendo assim, foi criado a Calculadora Tk com funcionalidades matemáticas básicas e com alguns erros propositais para que as correções e ampliações de novas funcionalidades sejam feitas pelo público alvo (Iniciantes). +Therefore, Calculator Tk was created with basic mathematical functionalities and some intentional errors so that corrections and expansions of new features can be made by the target audience (Beginners). -## Para contribuir -Siga os passos abaixo: +## How to contribute +Follow the steps below: -1. Faça o `Fork` do projeto [Calculadora Tk]() no canto superior direito da tela; -2. Clone o projeto do seu repositório no github (`git clone https://github.com/SEU_USUARIO/calculadora-tk.git`); -3. Crie sua branch para realizar sua modificação (`git checkout -b feature/nome_da_modificação`); -4. Após ter realizado suas modificações, faça um `commit` (`git commit -m "Descrição da modificação"`); -5. Faça o `Push` para seu repositório (`git push origin feature/nome_modificação`); -6. No seu repositório no *Github* crie uma `Pull Request` para que seja avaliada a suas modificações para ser feito o `merge` no projeto principal. +1. Fork the [Calculator Tk]() project in the upper right corner of the screen; +2. Clone the project from your GitHub repository (`git clone https://github.com/YOUR_USERNAME/calculadora-tk.git`); +3. Create your branch to make your modification (`git checkout -b feature/modification_name`); +4. After making your modifications, make a `commit` (`git commit -m "Description of modification"`); +5. Push to your repository (`git push origin feature/modification_name`); +6. In your GitHub repository, create a `Pull Request` so that your modifications can be evaluated for merging into the main project. -## Contribuidores +## Contributors | [
@aguiarcandre](https://github.com/aguiarcandre) | [
@carlos3g](https://github.com/carlos3g) | [
@ericllma](https://github.com/ericllma) | [
@sam-chami](https://github.com/sam-chami) | [
@taisbferreira](https://github.com/taisbferreira) | [
@edilsonmatola](https://github.com/edilsonmatola) | |:-:|:-:|:-:|:-:|:-:|:-:| | [
@maguzzz](https://github.com/maguzzz) | [
@vinayyak](https://github.com/vinayyak) | -## Para ideias/Bugs -Caso encontre algum bug crie uma `issue` descrevendo o Bug encontrado que tem que ser resolvido, informando o passo a passo para replicá-lo. +## For ideas/Bugs +If you find any bug, create an `issue` describing the Bug found that needs to be resolved, providing step-by-step instructions to replicate it. -E caso tenha alguma ideia de nova funcionalidade que possa ser implementada por outros iniciantes, crie uma `issue` descrevendo essa ideia. ;) +And if you have any idea for new functionality that can be implemented by other beginners, create an `issue` describing this idea. ;) ## Start ``` $ python main.py ``` -ou crie seu próprio arquivo com o seguinte script, e depois siga o procedimento acima com o nome correspondente: +or create your own file with the following script, and then follow the procedure above with the corresponding name: ```Python # -*- coding: utf-8 -*- @@ -50,8 +50,8 @@ if __name__ == '__main__': main.start() ``` -## Guias -- Tkinter: [Documentação](https://docs.python.org/3/library/tkinter.html) - *Existe diversos outros guias em mostra logo no ínicio do página* -- Git e Github: [Tutorial no Tableless](https://tableless.com.br/tudo-que-voce-queria-saber-sobre-git-e-github-mas-tinha-vergonha-de-perguntar/) - *Leitura* -- Git e Github: [Tutorial no Youtube](https://www.youtube.com/playlist?list=PLQCmSnNFVYnRdgxOC_ufH58NxlmM6VYd1) - *Vídeo Aula* -- Pull Request no GitHub: [Tutorial DigitalOcean](https://www.digitalocean.com/community/tutorials/como-criar-um-pull-request-no-github-pt) - *Leitura* +## Guides +- Tkinter: [Documentation](https://docs.python.org/3/library/tkinter.html) - *There are several other guides shown right at the beginning of the page* +- Git and Github: [Tutorial on Tableless](https://tableless.com.br/tudo-que-voce-queria-saber-sobre-git-e-github-mas-tinha-vergonha-de-perguntar/) - *Reading* +- Git and Github: [Tutorial on Youtube](https://www.youtube.com/playlist?list=PLQCmSnNFVYnRdgxOC_ufH58NxlmM6VYd1) - *Video Tutorial* +- Pull Request on GitHub: [DigitalOcean Tutorial](https://www.digitalocean.com/community/tutorials/como-criar-um-pull-request-no-github-pt) - *Reading* \ No newline at end of file diff --git a/app/calculador.py b/app/calculador.py index ac24aa2..7848eaf 100644 --- a/app/calculador.py +++ b/app/calculador.py @@ -1,34 +1,34 @@ # -*- coding: utf-8 -*- -# @autor: Matheus Felipe +# @author: Matheus Felipe # @github: github.com/matheusfelipeog class Calculador(object): - """Classe responsável por realizar todos os calculos da calculadora""" + """Class responsible for performing all calculator computations""" def calculation(self, calc): - """Responsável por receber o calculo a ser realizado, retornando - o resultado ou uma mensagem de erro em caso de falha. + """Responsible for receiving the calculation to be performed, returning + the result or an error message in case of failure. """ return self.__calculation_validation(calc=calc) def __calculation_validation(self, calc): - """Responsável por verificar se o calculo informado é possível ser feito""" + """Responsible for verifying if the informed calculation is possible to be done""" try: result = eval(calc) return self.__format_result(result=result) except (NameError, ZeroDivisionError, SyntaxError, ValueError): - return 'Erro' + return 'Error' def __format_result(self, result): - """Formata o resultado em notação cientifica caso seja muito grande - e retorna o valor formatado em tipo string""" + """Formats the result in scientific notation if it's too large + and returns the formatted value as string type""" result = str(result) if len(result) > 15: result = '{:5.5E}'.format(float(result)) - return result + return result \ No newline at end of file diff --git a/app/calculadora.py b/app/calculadora.py index 168cb3f..584f390 100644 --- a/app/calculadora.py +++ b/app/calculadora.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# @autor: Matheus Felipe +# @author: Matheus Felipe (Original) + History Feature Extension # @github: github.com/matheusfelipeog # Builtins @@ -9,7 +9,7 @@ import platform import tkinter as tk -from tkinter import Menu, FALSE +from tkinter import Menu, FALSE, messagebox from functools import partial from json import load as json_load @@ -17,71 +17,102 @@ from copy import deepcopy -# Módulos próprios +# Own modules from .calculador import Calculador +from .history_manager import HistoryManager +from .history_window import HistoryWindow class Calculadora(object): - """Classe para criação do layout da calculadora, distribuição dos botões - e a adição de suas funcionalidades. + """Class for creating the calculator layout, distributing buttons + and adding their functionalities. - Os botões distríbuidos no layout estão conforme o exemplo abaixo: + The buttons distributed in the layout are as shown in the example below: C | ( | ) | < 7 | 8 | 9 | x 4 | 5 | 6 | - 1 | 2 | 3 | + . | 0 | = | / - | | ^ | √ + H | | ^ | √ - OBS: É necessário importar o modulo style contido na pacote view, - e selecionar uma de suas classes de estilo. + NOTE: It's necessary to import the style module contained in the view package, + and select one of its style classes. + + New feature: Calculation history with graphical interface. + Improvements: Visible cursor and Enter key support for calculation. """ def __init__(self, master): self.master = master self.calc = Calculador() + + # Initialize history manager with error handling + try: + self.history_manager = HistoryManager() + except Exception as e: + print(f"History initialization failed: {e}") + self.history_manager = None self.settings = self._load_settings() - # Define estilo padrão para macOS, caso seja o sistema operacional utilizado + # Define default style for macOS, if it's the operating system being used if platform.system() == 'Darwin': self.theme = self._get_theme('Default Theme For MacOS') else: self.theme = self._get_theme(self.settings['current_theme']) - # Edição da Top-Level - self.master.title('Calculadora Tk') + # Top-Level editing + self.master.title('Calculator Tk') self.master.maxsize(width=335, height=415) self.master.minsize(width=335, height=415) self.master.geometry('-150+100') self.master['bg'] = self.theme['master_bg'] - # Área do input + # Input area self._frame_input = tk.Frame(self.master, bg=self.theme['frame_bg'], pady=4) self._frame_input.pack() - # Área dos botões + # Button area self._frame_buttons = tk.Frame(self.master, bg=self.theme['frame_bg'], padx=2) self._frame_buttons.pack() - # Funções de inicialização + # Initialization functions self._create_input(self._frame_input) self._create_buttons(self._frame_buttons) self._create_menu(self.master) + + # Bind keyboard events + self._bind_keyboard_events() + + # Store the last calculation expression for history + self._last_expression = "" @staticmethod def _load_settings(): - """Utilitário para carregar o arquivo de confirgurações da calculadora.""" - with open('./app/settings/settings.json', mode='r', encoding='utf-8') as f: - settings = json_load(f) - - return settings + """Utility to load the calculator configuration file.""" + try: + with open('./app/settings/settings.json', mode='r', encoding='utf-8') as f: + settings = json_load(f) + return settings + except (FileNotFoundError, ValueError) as e: + print(f"Configuration file loading failed: {e}") + # Return default configuration + return { + "current_theme": "Dark", + "global": { + "borderwidth": 0, + "highlightthickness": 0, + "width": 6, + "height": 2, + "font": "Arial 14 bold" + }, + "themes": [] + } def _get_theme(self, name='Dark'): - """Retorna as configurações de estilo para o theme especificado.""" - - list_of_themes = self.settings['themes'] + """Returns the style settings for the specified theme.""" + list_of_themes = self.settings.get('themes', []) found_theme = None for t in list_of_themes: @@ -89,256 +120,660 @@ def _get_theme(self, name='Dark'): found_theme = deepcopy(t) break + # If theme not found, return default theme + if not found_theme: + found_theme = { + "name": "Dark", + "master_bg": "#252729", + "frame_bg": "#252729", + "INPUT": { + "bg": "#252729", + "fg": "#ffffff", + "borderwidth": 0, + "highlightthickness": 0, + "width": 15, + "font": "Arial 28 bold", + "justify": "right" + }, + "BTN_DEFAULT": { + "bg": "#0e0f0f", + "fg": "#f5f6fa", + "activebackground": "#635f5f", + "activeforeground": "#000000" + }, + "BTN_NUMERICO": { + "bg": "#050505", + "fg": "#f5f6fa", + "activebackground": "#635f5f", + "activeforeground": "#000000" + }, + "BTN_OPERADOR": { + "bg": "#0e0f0f", + "fg": "#f5f6fa", + "activebackground": "#0097e6", + "activeforeground": "#000000" + }, + "BTN_CLEAR": { + "bg": "#0e0f0f", + "fg": "#f5f6fa", + "activebackground": "#d63031", + "activeforeground": "#000000" + } + } + return found_theme def _create_input(self, master): - self._entrada = tk.Entry(master, cnf=self.theme['INPUT']) - self._entrada.insert(0,0) + """Create input field and configure cursor visibility""" + # Create input field configuration, ensuring cursor is visible + input_config = deepcopy(self.theme['INPUT']) + + # Set cursor-related configurations + input_config['insertbackground'] = input_config.get('fg', '#ffffff') # Cursor color matches text color + input_config['insertwidth'] = 2 # Cursor width + input_config['insertborderwidth'] = 0 # Cursor border width + input_config['insertontime'] = 600 # Cursor display time (milliseconds) + input_config['insertofftime'] = 300 # Cursor hide time (milliseconds) + + self._entrada = tk.Entry(master, cnf=input_config) + self._entrada.insert(0, 0) self._entrada.pack() + + # Ensure input field gets focus to show cursor + self._entrada.focus_set() + + def _bind_keyboard_events(self): + """Bind keyboard events""" + # Only bind to input field to avoid duplicate triggering + self._entrada.bind('', self._on_key_press) + self._entrada.bind('', self._on_enter_press) + self._entrada.bind('', self._on_enter_press) # Numeric keypad Enter + + # Ensure input field gets focus + self._entrada.focus_set() + + def _on_key_press(self, event): + """Handle keyboard key press events""" + key = event.keysym + char = event.char + + # Handle number keys + if char.isdigit(): + self._set_values_in_input(int(char)) + return 'break' # Prevent default behavior + + # Handle operators + elif char in ['+', '-', '*', '/']: + if char == '*': + self._set_operator_in_input('*') + else: + self._set_operator_in_input(char) + return 'break' + + # Handle decimal point + elif char == '.': + self._set_dot_in_input('.') + return 'break' + + # Handle parentheses + elif char == '(': + self._set_open_parent() + return 'break' + elif char == ')': + self._set_close_parent() + return 'break' + + # Handle special keys + elif key == 'BackSpace': + self._del_last_value_in_input() + return 'break' + elif key in ['Delete', 'c', 'C']: + self._clear_input() + return 'break' + elif key == 'Escape': + self._clear_input() + return 'break' + + # Handle Power operator (^) + elif char == '^': + self._set_operator_in_input('**') + return 'break' + + # Handle equals sign + elif char == '=': + self._get_data_in_input() + return 'break' + + # Handle history shortcut + elif key.lower() == 'h' and event.state & 0x4: # Ctrl+H + if self.history_manager: + self._show_history_window() + return 'break' + + # For other keys, allow default behavior but restrict to only supported characters + elif char and not char.isalnum() and char not in '+-*/().^=': + return 'break' # Block unsupported characters + + # Allow navigation keys (left/right arrows, etc.) + elif key in ['Left', 'Right', 'Home', 'End']: + return # Allow default behavior + + # Block input for other cases + else: + return 'break' + + def _on_enter_press(self, event): + """Handle Enter key press event""" + self._get_data_in_input() + return 'break' # Prevent default behavior def _create_menu(self, master): self.master.option_add('*tearOff', FALSE) calc_menu = Menu(self.master) self.master.config(menu=calc_menu) - #Configuração + # Configuration config = Menu(calc_menu) theme = Menu(config) - #Menu tema + # Theme menu theme_incompatible = ['Default Theme For MacOS'] - for t in self.settings['themes']: - - name = t['name'] - if name in theme_incompatible: # Ignora os temas não compatíveis. + for t in self.settings.get('themes', []): + name = t.get('name', '') + if name in theme_incompatible: # Ignore incompatible themes. continue else: theme.add_command(label=name, command=partial(self._change_theme_to, name)) - #Configuração - calc_menu.add_cascade(label='Configuração', menu=config) - config.add_cascade(label='Tema', menu=theme) + + # Add history menu - only add when history manager is available + if self.history_manager: + history_menu = Menu(calc_menu) + calc_menu.add_cascade(label='History', menu=history_menu) + history_menu.add_command(label='View History (Ctrl+H)', command=self._show_history_window) + history_menu.add_command(label='Clear History', command=self._clear_history_confirm) + + # Add help menu + help_menu = Menu(calc_menu) + calc_menu.add_cascade(label='Help', menu=help_menu) + help_menu.add_command(label='Keyboard Shortcuts', command=self._show_keyboard_shortcuts) + + # Configuration + calc_menu.add_cascade(label='Configuration', menu=config) + config.add_cascade(label='Theme', menu=theme) config.add_separator() - config.add_command(label='Sair', command=self._exit) + config.add_command(label='Exit', command=self._exit) + + def _show_keyboard_shortcuts(self): + """Show keyboard shortcuts help""" + shortcuts_text = """Keyboard Shortcuts: + +Number keys (0-9): Input numbers +Operators (+, -, *, /): Input operators +Decimal point (.): Input decimal point +Parentheses ((, )): Input parentheses +^ : Power operation (**) += or Enter: Calculate result +Backspace: Delete last character +Delete or C: Clear input +Escape: Clear input +Ctrl+H: Open history +H: History button + +Mouse operations: +- Click buttons for input +- Double-click history items to use calculation +- Right-click history for more options""" + + messagebox.showinfo("Keyboard Shortcuts", shortcuts_text) def _change_theme_to(self, name='Dark'): - self.settings['current_theme'] = name - - with open('./app/settings/settings.json', 'w') as outfile: - json_dump(self.settings, outfile, indent=4) - - self._realod_app() + """Change theme settings and restart application""" + try: + self.settings['current_theme'] = name + with open('./app/settings/settings.json', 'w', encoding='utf-8') as outfile: + json_dump(self.settings, outfile, indent=4, ensure_ascii=False) + self._realod_app() + except Exception as e: + messagebox.showerror("Error", f"Theme change failed: {e}") + + def _show_history_window(self): + """Show history window""" + if not self.history_manager: + messagebox.showerror("Error", "History feature is not available") + return + + try: + HistoryWindow( + parent=self.master, + history_manager=self.history_manager, + theme=self.theme, + on_use_calculation=self._use_calculation_from_history + ) + except Exception as e: + print(f"Error opening history window: {e}") + messagebox.showerror("Error", f"Unable to open history window: {e}") + + def _clear_history_confirm(self): + """Confirm clearing history""" + if not self.history_manager: + messagebox.showerror("Error", "History feature is not available") + return + + try: + if messagebox.askyesno("Confirm Clear", "Are you sure you want to clear all history? This action cannot be undone!"): + self.history_manager.clear_history() + messagebox.showinfo("Success", "History has been cleared") + except Exception as e: + messagebox.showerror("Error", f"Failed to clear history: {e}") + + def _use_calculation_from_history(self, expression: str, result: str): + """Use calculation from history""" + try: + # Clear current input + self._entrada.delete(0, tk.END) + # Insert expression + self._entrada.insert(0, expression) + # Ensure cursor is at the end + self._entrada.icursor(tk.END) + # Ensure input field gets focus to show cursor + self._entrada.focus_set() + except Exception as e: + print(f"Failed to use history: {e}") def _create_buttons(self, master): - """"Metódo responsável pela criação de todos os botões da calculadora, - indo desde adição de eventos em cada botão à distribuição no layout grid. + """"Method responsible for creating all calculator buttons, + from adding events to each button to distribution in grid layout. """ - # Seta configurações globais (width, height font etc) no botão especificado. - self.theme['BTN_NUMERICO'].update(self.settings['global']) - - self._BTN_NUM_0 = tk.Button(master, text=0, cnf=self.theme['BTN_NUMERICO']) - self._BTN_NUM_1 = tk.Button(master, text=1, cnf=self.theme['BTN_NUMERICO']) - self._BTN_NUM_2 = tk.Button(master, text=2, cnf=self.theme['BTN_NUMERICO']) - self._BTN_NUM_3 = tk.Button(master, text=3, cnf=self.theme['BTN_NUMERICO']) - self._BTN_NUM_4 = tk.Button(master, text=4, cnf=self.theme['BTN_NUMERICO']) - self._BTN_NUM_5 = tk.Button(master, text=5, cnf=self.theme['BTN_NUMERICO']) - self._BTN_NUM_6 = tk.Button(master, text=6, cnf=self.theme['BTN_NUMERICO']) - self._BTN_NUM_7 = tk.Button(master, text=7, cnf=self.theme['BTN_NUMERICO']) - self._BTN_NUM_8 = tk.Button(master, text=8, cnf=self.theme['BTN_NUMERICO']) - self._BTN_NUM_9 = tk.Button(master, text=9, cnf=self.theme['BTN_NUMERICO']) - - # Seta configurações globais (width, height font etc) no botão especificado. - self.theme['BTN_OPERADOR'].update(self.settings['global']) - - # Instânciação dos botões dos operadores númericos - self._BTN_SOMA = tk.Button(master, text='+', cnf=self.theme['BTN_OPERADOR']) - self._BTN_SUB = tk.Button(master, text='-', cnf=self.theme['BTN_OPERADOR']) - self._BTN_DIV = tk.Button(master, text='/', cnf=self.theme['BTN_OPERADOR']) - self._BTN_MULT = tk.Button(master, text='*', cnf=self.theme['BTN_OPERADOR']) - self._BTN_EXP = tk.Button(master, text='^', cnf=self.theme['BTN_OPERADOR']) - self._BTN_RAIZ = tk.Button(master, text='√', cnf=self.theme['BTN_OPERADOR']) - - # Seta configurações globais (width, height font etc) no botão especificado. - self.theme['BTN_DEFAULT'].update(self.settings['global']) - self.theme['BTN_CLEAR'].update(self.settings['global']) - - # Instânciação dos botões de funcionalidades da calculadora - self._BTN_ABRE_PARENTESE = tk.Button(master, text='(', cnf=self.theme['BTN_DEFAULT']) - self._BTN_FECHA_PARENTESE = tk.Button(master, text=')', cnf=self.theme['BTN_DEFAULT']) - self._BTN_CLEAR = tk.Button(master, text='C', cnf=self.theme['BTN_DEFAULT']) - self._BTN_DEL = tk.Button(master, text='<', cnf=self.theme['BTN_CLEAR']) - self._BTN_RESULT = tk.Button(master, text='=', cnf=self.theme['BTN_OPERADOR']) - self._BTN_DOT = tk.Button(master, text='.', cnf=self.theme['BTN_DEFAULT']) - - # Instânciação dos botões vazios, para futura implementação - self._BTN_VAZIO1 = tk.Button(master, text='', cnf=self.theme['BTN_OPERADOR']) - self._BTN_VAZIO2 = tk.Button(master, text='', cnf=self.theme['BTN_OPERADOR']) - - # Distribuição dos botões em um gerenciador de layout grid - # Linha 0 + # Set global configurations (width, height font etc) on specified button. + btn_numerico_config = deepcopy(self.theme.get('BTN_NUMERICO', {})) + btn_numerico_config.update(self.settings.get('global', {})) + + self._BTN_NUM_0 = tk.Button(master, text=0, cnf=btn_numerico_config) + self._BTN_NUM_1 = tk.Button(master, text=1, cnf=btn_numerico_config) + self._BTN_NUM_2 = tk.Button(master, text=2, cnf=btn_numerico_config) + self._BTN_NUM_3 = tk.Button(master, text=3, cnf=btn_numerico_config) + self._BTN_NUM_4 = tk.Button(master, text=4, cnf=btn_numerico_config) + self._BTN_NUM_5 = tk.Button(master, text=5, cnf=btn_numerico_config) + self._BTN_NUM_6 = tk.Button(master, text=6, cnf=btn_numerico_config) + self._BTN_NUM_7 = tk.Button(master, text=7, cnf=btn_numerico_config) + self._BTN_NUM_8 = tk.Button(master, text=8, cnf=btn_numerico_config) + self._BTN_NUM_9 = tk.Button(master, text=9, cnf=btn_numerico_config) + + # Set global configurations (width, height font etc) on specified button. + btn_operador_config = deepcopy(self.theme.get('BTN_OPERADOR', {})) + btn_operador_config.update(self.settings.get('global', {})) + + # Instantiation of numeric operator buttons + self._BTN_SOMA = tk.Button(master, text='+', cnf=btn_operador_config) + self._BTN_SUB = tk.Button(master, text='-', cnf=btn_operador_config) + self._BTN_DIV = tk.Button(master, text='/', cnf=btn_operador_config) + self._BTN_MULT = tk.Button(master, text='*', cnf=btn_operador_config) + self._BTN_EXP = tk.Button(master, text='^', cnf=btn_operador_config) + self._BTN_RAIZ = tk.Button(master, text='√', cnf=btn_operador_config) + + # Set global configurations (width, height font etc) on specified button. + btn_default_config = deepcopy(self.theme.get('BTN_DEFAULT', {})) + btn_default_config.update(self.settings.get('global', {})) + + btn_clear_config = deepcopy(self.theme.get('BTN_CLEAR', {})) + btn_clear_config.update(self.settings.get('global', {})) + + # Instantiation of calculator functionality buttons + self._BTN_ABRE_PARENTESE = tk.Button(master, text='(', cnf=btn_default_config) + self._BTN_FECHA_PARENTESE = tk.Button(master, text=')', cnf=btn_default_config) + self._BTN_CLEAR = tk.Button(master, text='C', cnf=btn_default_config) + self._BTN_DEL = tk.Button(master, text='<', cnf=btn_clear_config) + self._BTN_RESULT = tk.Button(master, text='=', cnf=btn_operador_config) + self._BTN_DOT = tk.Button(master, text='.', cnf=btn_default_config) + + # Add history button - only enable when history manager is available + if self.history_manager: + self._BTN_HISTORY = tk.Button(master, text='H', cnf=btn_default_config) + else: + # If history feature is not available, create a disabled button + disabled_config = deepcopy(btn_default_config) + disabled_config['state'] = 'disabled' + self._BTN_HISTORY = tk.Button(master, text='H', cnf=disabled_config) + + # Instantiation of empty buttons, reserved for future features + self._BTN_VAZIO2 = tk.Button(master, text='', cnf=btn_operador_config, state='disabled') + + # Distribution of buttons in grid layout manager + # Row 0 self._BTN_CLEAR.grid(row=0, column=0, padx=1, pady=1) self._BTN_ABRE_PARENTESE.grid(row=0, column=1, padx=1, pady=1) self._BTN_FECHA_PARENTESE.grid(row=0, column=2, padx=1, pady=1) self._BTN_DEL.grid(row=0, column=3, padx=1, pady=1) - # Linha 1 + # Row 1 self._BTN_NUM_7.grid(row=1, column=0, padx=1, pady=1) self._BTN_NUM_8.grid(row=1, column=1, padx=1, pady=1) self._BTN_NUM_9.grid(row=1, column=2, padx=1, pady=1) self._BTN_MULT.grid(row=1, column=3, padx=1, pady=1) - # Linha 2 + # Row 2 self._BTN_NUM_4.grid(row=2, column=0, padx=1, pady=1) self._BTN_NUM_5.grid(row=2, column=1, padx=1, pady=1) self._BTN_NUM_6.grid(row=2, column=2, padx=1, pady=1) self._BTN_SUB.grid(row=2, column=3, padx=1, pady=1) - # Linha 3 + # Row 3 self._BTN_NUM_1.grid(row=3, column=0, padx=1, pady=1) self._BTN_NUM_2.grid(row=3, column=1, padx=1, pady=1) self._BTN_NUM_3.grid(row=3, column=2, padx=1, pady=1) self._BTN_SOMA.grid(row=3, column=3, padx=1, pady=1) - # Linha 4 + # Row 4 self._BTN_DOT.grid(row=4, column=0, padx=1, pady=1) self._BTN_NUM_0.grid(row=4, column=1, padx=1, pady=1) self._BTN_RESULT.grid(row=4, column=2, padx=1, pady=1) self._BTN_DIV.grid(row=4, column=3, padx=1, pady=1) - # Linha 5 - self._BTN_VAZIO1.grid(row=5, column=0, padx=1, pady=1) + # Row 5 - History button replaces first empty button + self._BTN_HISTORY.grid(row=5, column=0, padx=1, pady=1) self._BTN_VAZIO2.grid(row=5, column=1, padx=1, pady=1) self._BTN_EXP.grid(row=5, column=2, padx=1, pady=1) self._BTN_RAIZ.grid(row=5, column=3, padx=1, pady=1) - # Eventos dos botões númericos - self._BTN_NUM_0['command'] = partial(self._set_values_in_input, 0) - self._BTN_NUM_1['command'] = partial(self._set_values_in_input, 1) - self._BTN_NUM_2['command'] = partial(self._set_values_in_input, 2) - self._BTN_NUM_3['command'] = partial(self._set_values_in_input, 3) - self._BTN_NUM_4['command'] = partial(self._set_values_in_input, 4) - self._BTN_NUM_5['command'] = partial(self._set_values_in_input, 5) - self._BTN_NUM_6['command'] = partial(self._set_values_in_input, 6) - self._BTN_NUM_7['command'] = partial(self._set_values_in_input, 7) - self._BTN_NUM_8['command'] = partial(self._set_values_in_input, 8) - self._BTN_NUM_9['command'] = partial(self._set_values_in_input, 9) - - # Eventos dos botões de operação matemática - self._BTN_SOMA['command'] = partial(self._set_operator_in_input, '+') - self._BTN_SUB['command'] = partial(self._set_operator_in_input, '-') - self._BTN_MULT['command'] = partial(self._set_operator_in_input, '*') - self._BTN_DIV['command'] = partial(self._set_operator_in_input, '/') - self._BTN_EXP['command'] = partial(self._set_operator_in_input, '**') - self._BTN_RAIZ['command'] = partial(self._set_operator_in_input, '**(1/2)') - - - # Eventos dos botões de funcionalidades da calculadora - self._BTN_DOT['command'] = partial(self._set_dot_in_input, '.') - self._BTN_ABRE_PARENTESE['command'] = self._set_open_parent - self._BTN_FECHA_PARENTESE['command'] = self._set_close_parent - self._BTN_DEL['command'] = self._del_last_value_in_input - self._BTN_CLEAR['command'] = self._clear_input - self._BTN_RESULT['command'] = self._get_data_in_input + # Bind all buttons to refocus input field after click + buttons = [ + self._BTN_NUM_0, self._BTN_NUM_1, self._BTN_NUM_2, self._BTN_NUM_3, self._BTN_NUM_4, + self._BTN_NUM_5, self._BTN_NUM_6, self._BTN_NUM_7, self._BTN_NUM_8, self._BTN_NUM_9, + self._BTN_SOMA, self._BTN_SUB, self._BTN_DIV, self._BTN_MULT, self._BTN_EXP, self._BTN_RAIZ, + self._BTN_ABRE_PARENTESE, self._BTN_FECHA_PARENTESE, self._BTN_CLEAR, self._BTN_DEL, + self._BTN_RESULT, self._BTN_DOT + ] + + if self.history_manager: + buttons.append(self._BTN_HISTORY) + + # Events for numeric buttons + self._BTN_NUM_0['command'] = partial(self._button_click_wrapper, partial(self._set_values_in_input, 0)) + self._BTN_NUM_1['command'] = partial(self._button_click_wrapper, partial(self._set_values_in_input, 1)) + self._BTN_NUM_2['command'] = partial(self._button_click_wrapper, partial(self._set_values_in_input, 2)) + self._BTN_NUM_3['command'] = partial(self._button_click_wrapper, partial(self._set_values_in_input, 3)) + self._BTN_NUM_4['command'] = partial(self._button_click_wrapper, partial(self._set_values_in_input, 4)) + self._BTN_NUM_5['command'] = partial(self._button_click_wrapper, partial(self._set_values_in_input, 5)) + self._BTN_NUM_6['command'] = partial(self._button_click_wrapper, partial(self._set_values_in_input, 6)) + self._BTN_NUM_7['command'] = partial(self._button_click_wrapper, partial(self._set_values_in_input, 7)) + self._BTN_NUM_8['command'] = partial(self._button_click_wrapper, partial(self._set_values_in_input, 8)) + self._BTN_NUM_9['command'] = partial(self._button_click_wrapper, partial(self._set_values_in_input, 9)) + + # Events for mathematical operation buttons + self._BTN_SOMA['command'] = partial(self._button_click_wrapper, partial(self._set_operator_in_input, '+')) + self._BTN_SUB['command'] = partial(self._button_click_wrapper, partial(self._set_operator_in_input, '-')) + self._BTN_MULT['command'] = partial(self._button_click_wrapper, partial(self._set_operator_in_input, '*')) + self._BTN_DIV['command'] = partial(self._button_click_wrapper, partial(self._set_operator_in_input, '/')) + self._BTN_EXP['command'] = partial(self._button_click_wrapper, partial(self._set_operator_in_input, '**')) + self._BTN_RAIZ['command'] = partial(self._button_click_wrapper, partial(self._set_operator_in_input, '**(1/2)')) + + # Events for calculator functionality buttons + self._BTN_DOT['command'] = partial(self._button_click_wrapper, partial(self._set_dot_in_input, '.')) + self._BTN_ABRE_PARENTESE['command'] = partial(self._button_click_wrapper, self._set_open_parent) + self._BTN_FECHA_PARENTESE['command'] = partial(self._button_click_wrapper, self._set_close_parent) + self._BTN_DEL['command'] = partial(self._button_click_wrapper, self._del_last_value_in_input) + self._BTN_CLEAR['command'] = partial(self._button_click_wrapper, self._clear_input) + self._BTN_RESULT['command'] = partial(self._button_click_wrapper, self._get_data_in_input) + + # History button event - only bind when history manager is available + if self.history_manager: + self._BTN_HISTORY['command'] = self._show_history_window # History window doesn't need refocus + + def _button_click_wrapper(self, func): + """Button click wrapper, ensures input field maintains focus after click""" + func() + # Ensure input field maintains focus and cursor is visible + self._entrada.focus_set() + # Move cursor to end + self._entrada.icursor(tk.END) def _set_values_in_input(self, value): - """Metódo responsável por captar o valor númerico clicado e setar no input""" - if self._entrada.get() == 'Erro': - self._entrada.delete(0, len(self._entrada.get())) - - if self._entrada.get() == '0': - self._entrada.delete(0) - self._entrada.insert(0 ,value) - elif self._lenght_max(self._entrada.get()): - self._entrada.insert(len(self._entrada.get()) ,value) + """Method responsible for capturing the clicked numeric value and setting it in the input""" + try: + current_value = self._entrada.get() + cursor_pos = self._entrada.index(tk.INSERT) # Get current cursor position + + if current_value == 'Error': + self._entrada.delete(0, tk.END) + self._entrada.insert(0, value) + self._entrada.icursor(1) # Move cursor after the number + return + + if current_value == '0': + self._entrada.delete(0, tk.END) + self._entrada.insert(0, value) + self._entrada.icursor(1) # Move cursor after the number + elif self._lenght_max(current_value): + # Insert number at cursor position + self._entrada.insert(cursor_pos, value) + self._entrada.icursor(cursor_pos + 1) # Move cursor after inserted number + except Exception as e: + print(f"Error inputting number: {e}") def _set_dot_in_input(self, dot): - """Metódo responsável por setar o ponto de separação decimal no valor""" - if self._entrada.get() == 'Erro': - return - - if self._entrada.get()[-1] not in '.+-/*' and self._lenght_max(self._entrada.get()): - self._entrada.insert(len(self._entrada.get()) ,dot) + """Method responsible for setting the decimal point separator in the value""" + try: + current_value = self._entrada.get() + cursor_pos = self._entrada.index(tk.INSERT) + + if current_value == 'Error': + return + + # Check if current number segment already has a decimal point + if len(current_value) > 0: + # Find operators before and after cursor position + left_part = current_value[:cursor_pos] + right_part = current_value[cursor_pos:] + + # Find the range of current number segment + left_operator_pos = -1 + for i in range(len(left_part) - 1, -1, -1): + if left_part[i] in '+-*/(': + left_operator_pos = i + break + + right_operator_pos = len(current_value) + for i in range(len(right_part)): + if right_part[i] in '+-*/)': + right_operator_pos = cursor_pos + i + break + + # Check if current number segment already has decimal point + current_number = current_value[left_operator_pos + 1:right_operator_pos] + if '.' in current_number: + return # Current number already has decimal point, don't add + + if (len(current_value) > 0 and + cursor_pos > 0 and + current_value[cursor_pos - 1] not in '.+-/*' and + self._lenght_max(current_value)): + self._entrada.insert(cursor_pos, dot) + self._entrada.icursor(cursor_pos + 1) + elif cursor_pos == 0 or current_value[cursor_pos - 1] in '+-*/(': + # Add "0." after operator + self._entrada.insert(cursor_pos, '0.') + self._entrada.icursor(cursor_pos + 2) + except Exception as e: + print(f"Error inputting decimal point: {e}") def _set_open_parent(self): - """Metódo para setar a abertura de parenteses no input""" - if self._entrada.get() == 'Erro': - return - - if self._entrada.get() == '0': - self._entrada.delete(0) - self._entrada.insert(len(self._entrada.get()), '(') - elif self._entrada.get()[-1] in '+-/*' and self._lenght_max(self._entrada.get()): - self._entrada.insert(len(self._entrada.get()), '(') + """Method to set opening parenthesis in input""" + try: + current_value = self._entrada.get() + cursor_pos = self._entrada.index(tk.INSERT) + + if current_value == 'Error': + self._clear_input() + return + + if current_value == '0' and cursor_pos == 1: + self._entrada.delete(0, tk.END) + self._entrada.insert(0, '(') + self._entrada.icursor(1) + elif (cursor_pos == 0 or + current_value[cursor_pos - 1] in '+-/*(' and + self._lenght_max(current_value)): + self._entrada.insert(cursor_pos, '(') + self._entrada.icursor(cursor_pos + 1) + except Exception as e: + print(f"Error inputting left parenthesis: {e}") def _set_close_parent(self): - """Metódo para setar o fechamento de parenteses no input""" - if self._entrada.get() == 'Erro': - return + """Method to set closing parenthesis in input""" + try: + current_value = self._entrada.get() + cursor_pos = self._entrada.index(tk.INSERT) + + if current_value == 'Error': + return - if self._entrada.get().count('(') <= self._entrada.get().count(')'): - return - if self._entrada.get()[-1] not in '+-/*(' and self._lenght_max(self._entrada.get()): - self._entrada.insert(len(self._entrada.get()), ')') + # Check parenthesis balance + open_count = current_value.count('(') + close_count = current_value.count(')') + + if open_count <= close_count: + return # Already balanced or too many closing parentheses + + if (cursor_pos > 0 and + current_value[cursor_pos - 1] not in '+-/*(' and + self._lenght_max(current_value)): + self._entrada.insert(cursor_pos, ')') + self._entrada.icursor(cursor_pos + 1) + except Exception as e: + print(f"Error inputting right parenthesis: {e}") def _clear_input(self): - """Reseta o input da calculadora, limpando-o por completo e inserindo o valor 0""" - self._entrada.delete(0, len(self._entrada.get())) - self._entrada.insert(0,0) + """Reset calculator input, completely clearing it and inserting value 0""" + try: + self._entrada.delete(0, tk.END) + self._entrada.insert(0, 0) + self._entrada.icursor(1) # Move cursor after 0 + except Exception as e: + print(f"Error clearing input: {e}") def _del_last_value_in_input(self): - """Apaga o último digito contido dentro do input""" - if self._entrada.get() == 'Erro': - return + """Delete character before cursor""" + try: + current_value = self._entrada.get() + cursor_pos = self._entrada.index(tk.INSERT) + + if current_value == 'Error': + self._clear_input() + return - if len(self._entrada.get()) == 1: - self._entrada.delete(0) - self._entrada.insert(0,0) - else: - self._entrada.delete(len(self._entrada.get()) - 1) + if cursor_pos > 0: + self._entrada.delete(cursor_pos - 1) + self._entrada.icursor(cursor_pos - 1) + + # If empty after deletion, set to 0 + if not self._entrada.get(): + self._entrada.insert(0, 0) + self._entrada.icursor(1) + except Exception as e: + print(f"Error deleting character: {e}") def _set_operator_in_input(self, operator): - """Metódo responsável por captar o operador matemático clicado e setar no input""" - if self._entrada.get() == 'Erro': - return - - if self._entrada.get() == '': - # print('\33[91mOperação inválida.\33[m') - return - # Evita casos de operadores repetidos sequêncialmente, para evitar erros - if self._entrada.get()[-1] not in '+-*/' and self._lenght_max(self._entrada.get()): - self._entrada.insert(len(self._entrada.get()) ,operator) + """Method responsible for capturing the clicked mathematical operator and setting it in input""" + try: + current_value = self._entrada.get() + cursor_pos = self._entrada.index(tk.INSERT) + + if current_value == 'Error': + return + + if current_value == '' or current_value == '0': + if operator == '-': # Allow minus sign as first character + if current_value == '0': + self._entrada.delete(0, tk.END) + self._entrada.insert(0, '-') + self._entrada.icursor(1) + return + + # Avoid consecutive operators, but allow minus sign + if cursor_pos > 0: + prev_char = current_value[cursor_pos - 1] + if prev_char in '+-*/' and operator != '-': + return + # If last is operator and current is minus, only allow in specific cases + if prev_char in '+-*/' and operator == '-': + if prev_char == '-': # Avoid consecutive minus signs + return + + if self._lenght_max(current_value): + self._entrada.insert(cursor_pos, operator) + self._entrada.icursor(cursor_pos + len(operator)) + except Exception as e: + print(f"Error inputting operator: {e}") def _get_data_in_input(self): - """Pega os dados com todas as operações contidos dentro do input - para realizar o calculo""" - if self._entrada.get() == 'Erro': - return + """Get data with all operations contained within input + to perform calculation and add to history""" + try: + current_value = self._entrada.get() + + if current_value == 'Error' or not current_value: + return - result = self.calc.calculation(self._entrada.get()) - self._set_result_in_input(result=result) + # Save calculation expression + expression = current_value + self._last_expression = expression + + result = self.calc.calculation(expression) + self._set_result_in_input(result=result) + + # Add to history - add null value check + if (self.history_manager and + result != 'Error' and + expression and + expression != '0'): + self.history_manager.add_calculation(expression, result) + except Exception as e: + print(f"Error during calculation: {e}") + self._entrada.delete(0, len(self._entrada.get())) + self._entrada.insert(0, 'Error') def _set_result_in_input(self, result=0): - """Seta o resultado de toda a operação dentro do input""" - if self._entrada.get() == 'Erro': - return - - self._entrada.delete(0, len(self._entrada.get())) - self._entrada.insert(0, result) + """Set the result of entire operation within input""" + try: + self._entrada.delete(0, len(self._entrada.get())) + self._entrada.insert(0, result) + # Ensure cursor is at end of result + self._entrada.icursor(tk.END) + except Exception as e: + print(f"Error setting result: {e}") def _lenght_max(self, data_in_input): - """Para verificar se o input atingiu a quantidade de caracteres máxima""" - if len(str(data_in_input)) >= 15: + """To check if input reached maximum character count""" + try: + return len(str(data_in_input)) < 15 # Changed to less than 15 instead of greater than or equal to 15 + except: return False - return True def start(self): - print('\33[92mCalculadora Tk Iniciada. . .\33[m\n') + """Start calculator application""" + print('\33[92mCalculator Tk Started. . .\33[m\n') + if self.history_manager: + print('\33[94mHistory feature enabled - Press H key or use menu to view history\33[m\n') + else: + print('\33[93mHistory feature not available\33[m\n') + print('\33[96mNew features: Visible cursor + Enter key calculation + Full keyboard support\33[m\n') + print('\33[95mShortcuts: Enter=calculate, Backspace=delete, C/Delete/Esc=clear, Ctrl+H=history\33[m\n') self.master.mainloop() def _realod_app(self): - """Reinicia o aplicativo.""" - python = sys.executable # Recupera o path do executável do python - os.execl(python, python, * sys.argv) + """Restart the application.""" + try: + python = sys.executable # Retrieve python executable path + os.execl(python, python, *sys.argv) + except Exception as e: + print(f"Failed to restart application: {e}") + messagebox.showerror("Error", f"Failed to restart application: {e}") def _exit(self): - exit() + """Safely exit application""" + try: + self.master.quit() + self.master.destroy() + except: + exit() \ No newline at end of file diff --git a/app/history_manager.py b/app/history_manager.py new file mode 100644 index 0000000..16c7266 --- /dev/null +++ b/app/history_manager.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- + +# @author: History Feature Extension +# History record manager + +import json +import os +from datetime import datetime +from typing import List, Dict, Any + +class HistoryManager: + """History manager class, responsible for storing, reading and managing calculation history""" + + def __init__(self, history_file: str = './app/settings/history.json'): + self.history_file = history_file + self.max_history = 100 # Maximum history records + try: + self.history_data = self._load_history() + except Exception as e: + print(f"History initialization failed: {e}") + self.history_data = [] + + def _load_history(self) -> List[Dict[str, Any]]: + """Load history records""" + try: + if os.path.exists(self.history_file): + with open(self.history_file, 'r', encoding='utf-8') as f: + data = json.load(f) + # Validate data format + calculations = data.get('calculations', []) + if isinstance(calculations, list): + # Validate each calculation record format + valid_calculations = [] + for calc in calculations: + if (isinstance(calc, dict) and + 'expression' in calc and + 'result' in calc and + 'timestamp' in calc): + valid_calculations.append(calc) + return valid_calculations + return [] + except (json.JSONDecodeError, FileNotFoundError, KeyError) as e: + print(f"Failed to load history: {e}") + return [] + + def _save_history(self) -> None: + """Save history to file""" + try: + # Ensure directory exists + os.makedirs(os.path.dirname(self.history_file), exist_ok=True) + + history_json = { + 'calculations': self.history_data, + 'last_updated': datetime.now().isoformat(), + 'version': '1.0' # Add version information + } + + with open(self.history_file, 'w', encoding='utf-8') as f: + json.dump(history_json, f, indent=2, ensure_ascii=False) + except Exception as e: + print(f"Failed to save history: {e}") + + def add_calculation(self, expression: str, result: str) -> None: + """Add new calculation record""" + # Validate input parameters + if not expression or not result or result == 'Error': + return + + # Avoid duplicate records of same calculation + if (self.history_data and + len(self.history_data) > 0 and + self.history_data[0].get('expression') == expression and + self.history_data[0].get('result') == result): + return + + try: + # Generate unique ID + new_id = max([calc.get('id', 0) for calc in self.history_data], default=0) + 1 + + calculation = { + 'id': new_id, + 'expression': str(expression).strip(), + 'result': str(result).strip(), + 'timestamp': datetime.now().isoformat(), + 'date_formatted': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + # Add to beginning of history + self.history_data.insert(0, calculation) + + # Limit history count + if len(self.history_data) > self.max_history: + self.history_data = self.history_data[:self.max_history] + + self._save_history() + except Exception as e: + print(f"Failed to add calculation record: {e}") + + def get_history(self) -> List[Dict[str, Any]]: + """Get history list""" + try: + return self.history_data.copy() + except Exception as e: + print(f"Failed to get history: {e}") + return [] + + def get_recent_history(self, count: int = 10) -> List[Dict[str, Any]]: + """Get recent history records""" + try: + count = max(1, min(count, len(self.history_data))) # Ensure count is in valid range + return self.history_data[:count] + except Exception as e: + print(f"Failed to get recent history: {e}") + return [] + + def clear_history(self) -> None: + """Clear all history""" + try: + self.history_data.clear() + self._save_history() + except Exception as e: + print(f"Failed to clear history: {e}") + + def delete_calculation(self, calc_id: int) -> bool: + """Delete specified calculation record""" + try: + original_length = len(self.history_data) + self.history_data = [calc for calc in self.history_data + if calc.get('id') != calc_id] + + if len(self.history_data) < original_length: + self._save_history() + return True + return False + except Exception as e: + print(f"Failed to delete calculation record: {e}") + return False + + def search_history(self, query: str) -> List[Dict[str, Any]]: + """Search history records""" + try: + if not query or not isinstance(query, str): + return self.history_data + + query = query.lower().strip() + if not query: + return self.history_data + + filtered_history = [] + + for calc in self.history_data: + try: + if (query in str(calc.get('expression', '')).lower() or + query in str(calc.get('result', '')).lower() or + query in str(calc.get('date_formatted', '')).lower()): + filtered_history.append(calc) + except (TypeError, AttributeError): + continue # Skip records with format errors + + return filtered_history + except Exception as e: + print(f"Failed to search history: {e}") + return [] + + def get_statistics(self) -> Dict[str, Any]: + """Get history statistics""" + try: + total_calculations = len(self.history_data) + + if total_calculations == 0: + return { + 'total_calculations': 0, + 'most_recent': None, + 'oldest': None + } + + return { + 'total_calculations': total_calculations, + 'most_recent': self.history_data[0].get('date_formatted') if self.history_data else None, + 'oldest': self.history_data[-1].get('date_formatted') if self.history_data else None + } + except Exception as e: + print(f"Failed to get statistics: {e}") + return { + 'total_calculations': 0, + 'most_recent': None, + 'oldest': None + } + + def validate_data_integrity(self) -> bool: + """Validate data integrity""" + try: + if not isinstance(self.history_data, list): + return False + + for calc in self.history_data: + if not isinstance(calc, dict): + return False + if not all(key in calc for key in ['id', 'expression', 'result', 'timestamp']): + return False + + return True + except Exception: + return False + + def repair_data(self) -> bool: + """Repair corrupted data""" + try: + if not self.validate_data_integrity(): + # Try to reload data + self.history_data = self._load_history() + + # If still invalid, create new empty data + if not self.validate_data_integrity(): + self.history_data = [] + self._save_history() + return True + + return True + except Exception as e: + print(f"Failed to repair data: {e}") + self.history_data = [] + return False \ No newline at end of file diff --git a/app/history_window.py b/app/history_window.py new file mode 100644 index 0000000..7718540 --- /dev/null +++ b/app/history_window.py @@ -0,0 +1,345 @@ +# -*- coding: utf-8 -*- + +# @author: History Feature Extension +# History window interface + +import tkinter as tk +from tkinter import ttk, messagebox, StringVar +from functools import partial +from typing import Callable, List, Dict, Any + +class HistoryWindow: + """History window class, provides GUI interface for history records""" + + def __init__(self, parent, history_manager, theme: Dict, on_use_calculation: Callable): + self.parent = parent + self.history_manager = history_manager + self.theme = theme + self.on_use_calculation = on_use_calculation + + # Create window + self.window = tk.Toplevel(parent) + self.setup_window() + + # Search variable + self.search_var = StringVar() + self.search_var.trace('w', self._on_search_change) + + # Currently displayed history + self.current_history = [] + + self._create_interface() + self._load_history() + + def setup_window(self): + """Set window properties""" + self.window.title('Calculation History') + self.window.geometry('600x500') + self.window.minsize(500, 400) + self.window.configure(bg=self.theme.get('master_bg', '#252729')) + + # Set window icon and properties + self.window.transient(self.parent) + self.window.grab_set() # Modal window + + # Center display + self.window.update_idletasks() + x = (self.window.winfo_screenwidth() // 2) - (600 // 2) + y = (self.window.winfo_screenheight() // 2) - (500 // 2) + self.window.geometry(f'600x500+{x}+{y}') + + def _create_interface(self): + """Create interface elements""" + # Main frame + main_frame = tk.Frame(self.window, bg=self.theme.get('frame_bg', '#252729')) + main_frame.pack(fill='both', expand=True, padx=10, pady=10) + + # Top frame - title and search + top_frame = tk.Frame(main_frame, bg=self.theme.get('frame_bg', '#252729')) + top_frame.pack(fill='x', pady=(0, 10)) + + # Title + title_label = tk.Label( + top_frame, + text='Calculation History', + font=('Arial', 16, 'bold'), + bg=self.theme.get('frame_bg', '#252729'), + fg=self.theme.get('INPUT', {}).get('fg', '#ffffff') + ) + title_label.pack(side='left') + + # Search frame + search_frame = tk.Frame(top_frame, bg=self.theme.get('frame_bg', '#252729')) + search_frame.pack(side='right') + + tk.Label( + search_frame, + text='Search:', + bg=self.theme.get('frame_bg', '#252729'), + fg=self.theme.get('INPUT', {}).get('fg', '#ffffff') + ).pack(side='left', padx=(0, 5)) + + self.search_entry = tk.Entry( + search_frame, + textvariable=self.search_var, + width=20, + bg=self.theme.get('INPUT', {}).get('bg', '#252729'), + fg=self.theme.get('INPUT', {}).get('fg', '#ffffff'), + insertbackground=self.theme.get('INPUT', {}).get('fg', '#ffffff') + ) + self.search_entry.pack(side='left') + + # Middle frame - history list + middle_frame = tk.Frame(main_frame, bg=self.theme.get('frame_bg', '#252729')) + middle_frame.pack(fill='both', expand=True, pady=(0, 10)) + + # Create Treeview to display history + style = ttk.Style() + style.theme_use('clam') + + # Configure Treeview style + style.configure( + 'History.Treeview', + background=self.theme.get('BTN_NUMERICO', {}).get('bg', '#050505'), + foreground=self.theme.get('BTN_NUMERICO', {}).get('fg', '#f5f6fa'), + fieldbackground=self.theme.get('BTN_NUMERICO', {}).get('bg', '#050505'), + borderwidth=0 + ) + + style.configure( + 'History.Treeview.Heading', + background=self.theme.get('BTN_OPERADOR', {}).get('bg', '#0e0f0f'), + foreground=self.theme.get('BTN_OPERADOR', {}).get('fg', '#f5f6fa'), + relief='flat' + ) + + # Create Treeview + columns = ('expression', 'result', 'time') + self.tree = ttk.Treeview( + middle_frame, + columns=columns, + show='headings', + style='History.Treeview' + ) + + # Define columns + self.tree.heading('expression', text='Expression') + self.tree.heading('result', text='Result') + self.tree.heading('time', text='Time') + + self.tree.column('expression', width=200, anchor='w') + self.tree.column('result', width=150, anchor='e') + self.tree.column('time', width=180, anchor='center') + + # Add scrollbars + scrollbar_y = ttk.Scrollbar(middle_frame, orient='vertical', command=self.tree.yview) + scrollbar_x = ttk.Scrollbar(middle_frame, orient='horizontal', command=self.tree.xview) + self.tree.configure(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set) + + # Layout + self.tree.grid(row=0, column=0, sticky='nsew') + scrollbar_y.grid(row=0, column=1, sticky='ns') + scrollbar_x.grid(row=1, column=0, sticky='ew') + + middle_frame.grid_rowconfigure(0, weight=1) + middle_frame.grid_columnconfigure(0, weight=1) + + # Bind double-click event + self.tree.bind('', self._on_item_double_click) + self.tree.bind('', self._on_right_click) # Right-click menu + + # Bottom frame - buttons + bottom_frame = tk.Frame(main_frame, bg=self.theme.get('frame_bg', '#252729')) + bottom_frame.pack(fill='x') + + # Statistics information + self.stats_label = tk.Label( + bottom_frame, + text='', + bg=self.theme.get('frame_bg', '#252729'), + fg=self.theme.get('INPUT', {}).get('fg', '#ffffff'), + font=('Arial', 9) + ) + self.stats_label.pack(side='left') + + # Button frame + button_frame = tk.Frame(bottom_frame, bg=self.theme.get('frame_bg', '#252729')) + button_frame.pack(side='right') + + # Button configuration + btn_config = { + 'width': 10, + 'height': 1, + 'font': ('Arial', 10), + 'bg': self.theme.get('BTN_DEFAULT', {}).get('bg', '#0e0f0f'), + 'fg': self.theme.get('BTN_DEFAULT', {}).get('fg', '#f5f6fa'), + 'activebackground': self.theme.get('BTN_DEFAULT', {}).get('activebackground', '#635f5f'), + 'activeforeground': self.theme.get('BTN_DEFAULT', {}).get('activeforeground', '#000000'), + 'border': 0 + } + + # Use button + self.use_btn = tk.Button( + button_frame, + text='Use', + command=self._use_selected, + **btn_config + ) + self.use_btn.pack(side='left', padx=(0, 5)) + + # Delete button + self.delete_btn = tk.Button( + button_frame, + text='Delete', + command=self._delete_selected, + **btn_config + ) + self.delete_btn.pack(side='left', padx=(0, 5)) + + # Clear button + self.clear_btn = tk.Button( + button_frame, + text='Clear All', + command=self._clear_all, + **btn_config + ) + self.clear_btn.pack(side='left', padx=(0, 5)) + + # Close button + self.close_btn = tk.Button( + button_frame, + text='Close', + command=self.window.destroy, + **btn_config + ) + self.close_btn.pack(side='left') + + # Create right-click menu + self._create_context_menu() + + def _create_context_menu(self): + """Create right-click context menu""" + self.context_menu = tk.Menu(self.window, tearoff=0) + self.context_menu.add_command(label="Use this calculation", command=self._use_selected) + self.context_menu.add_command(label="Copy expression", command=self._copy_expression) + self.context_menu.add_command(label="Copy result", command=self._copy_result) + self.context_menu.add_separator() + self.context_menu.add_command(label="Delete", command=self._delete_selected) + + def _load_history(self): + """Load and display history""" + history = self.history_manager.get_history() + self.current_history = history + self._update_tree_view(history) + self._update_statistics() + + def _update_tree_view(self, history_list: List[Dict]): + """Update tree view""" + # Clear existing items + for item in self.tree.get_children(): + self.tree.delete(item) + + # Add history records + for calc in history_list: + self.tree.insert('', 'end', values=( + calc['expression'], + calc['result'], + calc['date_formatted'] + ), tags=(str(calc['id']),)) + + def _update_statistics(self): + """Update statistics information""" + stats = self.history_manager.get_statistics() + if stats['total_calculations'] > 0: + text = f"Total calculations: {stats['total_calculations']}" + if len(self.current_history) != stats['total_calculations']: + text += f" (Showing: {len(self.current_history)})" + else: + text = "No calculation records" + + self.stats_label.config(text=text) + + def _on_search_change(self, *args): + """Callback when search box content changes""" + query = self.search_var.get() + filtered_history = self.history_manager.search_history(query) + self.current_history = filtered_history + self._update_tree_view(filtered_history) + self._update_statistics() + + def _on_item_double_click(self, event): + """Use calculation when item is double-clicked""" + self._use_selected() + + def _on_right_click(self, event): + """Show context menu on right-click""" + item = self.tree.identify_row(event.y) + if item: + self.tree.selection_set(item) + self.context_menu.post(event.x_root, event.y_root) + + def _get_selected_calculation(self) -> Dict: + """Get selected calculation record""" + selection = self.tree.selection() + if not selection: + return None + + item = selection[0] + values = self.tree.item(item)['values'] + tags = self.tree.item(item)['tags'] + + if values and tags: + calc_id = int(tags[0]) + for calc in self.current_history: + if calc['id'] == calc_id: + return calc + return None + + def _use_selected(self): + """Use selected calculation""" + calc = self._get_selected_calculation() + if calc: + self.on_use_calculation(calc['expression'], calc['result']) + self.window.destroy() + else: + messagebox.showwarning("Warning", "Please select a calculation record first") + + def _delete_selected(self): + """Delete selected calculation""" + calc = self._get_selected_calculation() + if calc: + if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete calculation '{calc['expression']} = {calc['result']}'?"): + if self.history_manager.delete_calculation(calc['id']): + self._load_history() + messagebox.showinfo("Success", "Calculation record deleted") + else: + messagebox.showerror("Error", "Delete failed") + else: + messagebox.showwarning("Warning", "Please select a calculation record first") + + def _clear_all(self): + """Clear all history records""" + if messagebox.askyesno("Confirm Clear", "Are you sure you want to clear all history? This action cannot be undone!"): + self.history_manager.clear_history() + self._load_history() + messagebox.showinfo("Success", "All history records have been cleared") + + def _copy_expression(self): + """Copy expression to clipboard""" + calc = self._get_selected_calculation() + if calc: + self.window.clipboard_clear() + self.window.clipboard_append(calc['expression']) + messagebox.showinfo("Success", "Expression copied to clipboard") + else: + messagebox.showwarning("Warning", "Please select a calculation record first") + + def _copy_result(self): + """Copy result to clipboard""" + calc = self._get_selected_calculation() + if calc: + self.window.clipboard_clear() + self.window.clipboard_append(calc['result']) + messagebox.showinfo("Success", "Result copied to clipboard") + else: + messagebox.showwarning("Warning", "Please select a calculation record first") \ No newline at end of file diff --git a/app/settings/settings.json b/app/settings/settings.json index 80f0380..53e83f0 100644 --- a/app/settings/settings.json +++ b/app/settings/settings.json @@ -1,5 +1,5 @@ { - "current_theme": "Dark", + "current_theme": "Purple", "global": { "borderwidth": 0, "highlightthickness": 0, @@ -313,4 +313,4 @@ } } ] -} +} \ No newline at end of file diff --git a/main.py b/main.py index b2086e5..1f9156e 100644 --- a/main.py +++ b/main.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- -# @autor: Matheus Felipe +# @author: Matheus Felipe # @github: github.com/matheusfelipeog # Builtin import tkinter as tk -# Módulo próprio +# Own module from app.calculadora import Calculadora if __name__ == '__main__': master = tk.Tk() main = Calculadora(master) - main.start() + main.start() \ No newline at end of file