Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge rdbende:numberentry into master to add NumberEntry widget #79

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@ This file contains a list of all the authors of widgets in this repository. Plea
* `AutocompleteEntryListbox`
- [Dogeek](https://github.com/Dogeek)
* `validated_entries` submodule
- [rdbende](https://github.com/rdbende)
* `NumberEntry`
- Multiple authors:
* `ScaleEntry` (RedFantom and Juliette Monsel)
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ A collection of widgets for Tkinter's ttk extensions by various authors.
Copyright (C) Mitja Martini 2008
Copyright (C) Russell Adams 2011
Copyright (C) Juliette Monsel 2017
Copyright (C) rdbende 2021

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
Expand Down
1 change: 1 addition & 0 deletions docs/source/ttkwidgets/ttkwidgets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ttkwidgets
DebugWindow
ItemsCanvas
LinkLabel
NumberEntry
ScaleEntry
ScrolledListbox
Table
Expand Down
14 changes: 14 additions & 0 deletions examples/example_numberentry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-

# Copyright (c) rdbende 2021
# For license see LICENSE

from ttkwidgets import NumberEntry
import tkinter as tk

root = tk.Tk()
root.title('NumberEntry')

NumberEntry(root, expressions=True, roundto=4, allowed_chars={'p': 3.14159, 'x': 5}).pack(pady=30)

root.mainloop()
39 changes: 39 additions & 0 deletions tests/test_numberentry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright (c) rdbende 2021
# For license see LICENSE

from ttkwidgets import NumberEntry
from tests import BaseWidgetTest
import tkinter as tk


class TestNumberEntry(BaseWidgetTest):
def test_numberentry_init(self):
entry = NumberEntry(self.window, roundto=4, allowed_chars={'p': 3.14})
entry.pack()
self.window.update()

def test_numberentry_events(self):
entry = NumberEntry(self.window, roundto=4, allowed_chars={'p': 3.14})
entry.pack()
self.window.update()
entry.insert(0, "1+2-3*4/5**p")
self.window.update()
entry._check()
self.window.update()
entry._eval()
self.window.update()

def test_numberentry_config(self):
entry = NumberEntry(self.window, roundto=4, allowed_chars={'p': 3.14})
entry.pack()
self.window.update()
entry.keys()
self.window.update()
entry.configure(expressions=False, roundto=0)
self.window.update()
entry.cget("expressions")
self.window.update()
value = entry["roundto"]
self.window.update()
entry["allowed_chars"] = {'p': 3.14159}
self.window.update()
1 change: 1 addition & 0 deletions ttkwidgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ttkwidgets.timeline import TimeLine
from ttkwidgets.tickscale import TickScale
from ttkwidgets.table import Table
from ttkwidgets.numberentry import NumberEntry

from ttkwidgets.validated_entries.numbers import (
PercentEntry, IntEntry, FloatEntry,
Expand Down
114 changes: 114 additions & 0 deletions ttkwidgets/numberentry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""
Author: rdbende
License: GNU GPLv3
Copyright (c) 2021 rdbende
"""

import tkinter as tk
from tkinter import ttk


class NumberEntry(ttk.Entry):
"""
An entry that takes only numbers, calculations or variables,
and calculates the result of the calculation
"""
def __init__(self, master=None, allowed_chars={}, **kwargs):
"""
Create a NumberEntry

:param allowed_chars: Set the accepted variables, the name must be one single character
e.g.: allowed_chars={'p': 3.14}
:type allowed_chars: dict
:param expressions: Allow the use of expressions (default is True)
:type expressions: bool
:param roundto: The number of decimals in the result (default is 0)
:type roundto: int
:param variables: Allow the use of the user specified variables
specified in allowed_chars (default is True)
:type variables: bool
"""
self._allowed = allowed_chars
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a serious security threat.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should allow numbers and operator characters only.

self._expr = kwargs.pop("expressions", True)
self._round = kwargs.pop("roundto", 0)
self._vars = kwargs.pop("variables", True)
ttk.Entry.__init__(self, master, **kwargs)
self.bind("<Return>", self._eval)
self.bind("<FocusOut>", self._eval)
self.bind("<KeyRelease>", self._check)

def __getitem__(self, key):
return self.cget(key)

def __setitem__(self, key, value):
self.configure(**{key: value})

def _eval(self, *args):
"""Calculate the result of the entered calculation"""
current = self.get()
for i in current:
if i in self._allowed.keys():
current = current.replace(i, str(self._allowed[i]))
if current:
try:
if int(self._round) == 0:
result = int(round(eval(current), 0))
self.delete(0, tk.END)
self.insert(0, result)
else:
result = round(float(eval(current)), self._round)
self.delete(0, tk.END)
self.insert(0, result)
except SyntaxError:
self.delete(0, tk.END)
self.insert(0, "SyntaxError")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bad idea. Why would the user care? Simply just do nothing with the expression. Or maybe set the invalid ttk state on the entry

self.select_range(0, tk.END)
except ZeroDivisionError:
self.delete(0, tk.END)
self.insert(0, "ZeroDivisionError")
self.select_range(0, tk.END)

def _check(self, *args):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, this looks cursed. And why didn't I use an actual validator function?

typed = self.get()
if not typed == "SyntaxError" and not typed == "ZeroDivisionError":
if self._expr:
allowed = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "-", "*", "/", "%", "."]
else:
allowed = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."]
if self._vars:
allowed.extend(self._allowed.keys())
for current in typed:
if not current in allowed:
typed = typed.replace(current, "")
self.delete(0, tk.END)
self.insert(0, typed)

def configure(self, allowed_chars={}, **kwargs):
"""Configure resources of the widget"""
self._allowed = allowed_chars
self._expr = kwargs.pop("expressions", self._expr)
self._round = kwargs.pop("roundto", self._round)
self._vars = kwargs.pop("variables", self._vars)
ttk.Entry.configure(self, **kwargs)

config = configure

def cget(self, key):
"""Return the resource value for a KEY given as string"""
if key == "allowed_chars":
return self._allowed
elif key == "expressions":
return self._expr
elif key == "roundto":
return self._round
elif key == "variables":
return self._vars
else:
return ttk.Entry.cget(self, key)

def keys(self):
"""Return a list of all resource names of this widget"""
keys = ttk.Entry.keys(self)
keys.extend(["allowed_chars", "expressions", "roundto", "variables"])
keys.sort()
return keys