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