diff --git a/AUTHORS.md b/AUTHORS.md index 1b80c6ff..5aa5cfb0 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -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) diff --git a/README.md b/README.md index 0e4047c8..01b0e3b2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/source/ttkwidgets/ttkwidgets.rst b/docs/source/ttkwidgets/ttkwidgets.rst index 21543502..65bef4a9 100644 --- a/docs/source/ttkwidgets/ttkwidgets.rst +++ b/docs/source/ttkwidgets/ttkwidgets.rst @@ -17,6 +17,7 @@ ttkwidgets DebugWindow ItemsCanvas LinkLabel + NumberEntry ScaleEntry ScrolledListbox Table diff --git a/examples/example_numberentry.py b/examples/example_numberentry.py new file mode 100644 index 00000000..5df92370 --- /dev/null +++ b/examples/example_numberentry.py @@ -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() diff --git a/tests/test_numberentry.py b/tests/test_numberentry.py new file mode 100644 index 00000000..07195643 --- /dev/null +++ b/tests/test_numberentry.py @@ -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() diff --git a/ttkwidgets/__init__.py b/ttkwidgets/__init__.py index 2360b89c..f8d9f0f7 100644 --- a/ttkwidgets/__init__.py +++ b/ttkwidgets/__init__.py @@ -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, diff --git a/ttkwidgets/numberentry.py b/ttkwidgets/numberentry.py new file mode 100644 index 00000000..32783e51 --- /dev/null +++ b/ttkwidgets/numberentry.py @@ -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 + 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("", self._eval) + self.bind("", self._eval) + self.bind("", 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") + 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): + 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