From 602bf9eb568b663bd93dc8ec879a0e1a085b37ef Mon Sep 17 00:00:00 2001 From: formateli Date: Tue, 21 Apr 2020 12:19:34 -0500 Subject: [PATCH 1/6] OnOffButton widget. --- docs/source/ttkwidgets/ttkwidgets.rst | 1 + .../ttkwidgets/ttkwidgets.OnOffButton.rst | 10 + examples/example_onoffbutton.py | 43 +++ tests/test_onoffbutton.py | 140 ++++++++ ttkwidgets/__init__.py | 1 + ttkwidgets/onoffbutton.py | 319 ++++++++++++++++++ 6 files changed, 514 insertions(+) create mode 100644 docs/source/ttkwidgets/ttkwidgets/ttkwidgets.OnOffButton.rst create mode 100644 examples/example_onoffbutton.py create mode 100644 tests/test_onoffbutton.py create mode 100644 ttkwidgets/onoffbutton.py diff --git a/docs/source/ttkwidgets/ttkwidgets.rst b/docs/source/ttkwidgets/ttkwidgets.rst index eb2f17f4..60908396 100644 --- a/docs/source/ttkwidgets/ttkwidgets.rst +++ b/docs/source/ttkwidgets/ttkwidgets.rst @@ -17,6 +17,7 @@ ttkwidgets DebugWindow ItemsCanvas LinkLabel + OnOffButton ScaleEntry ScrolledListbox Table diff --git a/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.OnOffButton.rst b/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.OnOffButton.rst new file mode 100644 index 00000000..872722bd --- /dev/null +++ b/docs/source/ttkwidgets/ttkwidgets/ttkwidgets.OnOffButton.rst @@ -0,0 +1,10 @@ +OnOffButton +=========== + +.. currentmodule:: ttkwidgets + +.. autoclass:: OnOffButton + :show-inheritance: + :members: + + .. automethod:: __init__ diff --git a/examples/example_onoffbutton.py b/examples/example_onoffbutton.py new file mode 100644 index 00000000..35c2cc06 --- /dev/null +++ b/examples/example_onoffbutton.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Fredy Ramirez +# For license see LICENSE + +from ttkwidgets import OnOffButton +try: + import Tkinter as tk +except ImportError: + import tkinter as tk + + +window = tk.Tk() + + +def grid_button(button, row, column): + window.columnconfigure(column, weight=1) + button.grid(row=row, column=column, padx=10, pady=10) + + +def print_value(button): + print(button.get()) + + +onoff_24_off = OnOffButton(window, command=print_value) +grid_button(onoff_24_off, 0, 0) + +onoff_24_on = OnOffButton(window, command=print_value) +grid_button(onoff_24_on, 0, 1) +onoff_24_off.set(1) + +onoff_24_disabled = OnOffButton(window, command=print_value) +grid_button(onoff_24_disabled, 0, 2) +onoff_24_disabled.config(state='disabled') + +onoff_48_on = OnOffButton(window, size=48, command=print_value) +grid_button(onoff_48_on, 1, 0) + +onoff_100_on = OnOffButton(window, size=100, command=print_value) +grid_button(onoff_100_on, 1, 1) +onoff_100_on.set(1) + +window.mainloop() diff --git a/tests/test_onoffbutton.py b/tests/test_onoffbutton.py new file mode 100644 index 00000000..af9c07f3 --- /dev/null +++ b/tests/test_onoffbutton.py @@ -0,0 +1,140 @@ +# Copyright (c) 2020 Fredy Ramirez +# For license see LICENSE +from ttkwidgets import OnOffButton +from tests import BaseWidgetTest +import tkinter as tk +from tkinter import ttk + + +class TestOnOffButton(BaseWidgetTest): + def test_onoffbutton_init(self): + onoff = OnOffButton(self.window) + onoff.pack() + self.window.update() + + def test_onoffbutton_variable_default(self): + onoff = OnOffButton(self.window) + onoff.pack() + self.window.update() + variable = onoff.cget('variable') + self.assertEqual(isinstance(variable, tk.IntVar), True) + self.assertEqual(variable.get(), 0) + variable.set(1) + self.assertEqual(variable.get(), 1) + + def test_onoffbutton_variable_int(self): + variable = tk.IntVar() + onoff = OnOffButton(self.window, variable=variable) + onoff.pack() + self.window.update() + self.assertEqual(variable.get(), False) + variable.set(1) + self.assertEqual(variable.get(), True) + + def test_onoffbutton_variable_bool(self): + variable = tk.BooleanVar() + onoff = OnOffButton(self.window, variable=variable) + onoff.pack() + self.window.update() + self.assertEqual(variable.get(), 0) + variable.set(1) + self.assertEqual(variable.get(), 1) + + def test_onoffbutton_variable_string(self): + variable = tk.StringVar() + onoff = OnOffButton(self.window, variable=variable) + onoff.pack() + self.window.update() + self.assertEqual(variable.get(), '0') + variable.set(1) + self.assertEqual(variable.get(), '1') + onoff.config(onvalue='ON', offvalue='OFF') + self.assertEqual(variable.get(), 'ON') + onoff.invoke() + self.assertEqual(variable.get(), 'OFF') + + def test_onoffbutton_invoke(self): + onoff = OnOffButton(self.window, command=self._button_changed) + onoff.pack() + self.window.update() + self.assertEqual(onoff.get(), 0) + result = onoff.invoke() + self.assertEqual(onoff, result) + self.assertEqual(onoff.get(), 1) + + def test_onoffbutton_style(self): + style = ttk.Style() + onoff = OnOffButton(self.window) + onoff.pack() + self.window.update() + st = onoff.cget('style') + self.assertEqual(st, 'OnOffButton') + opts = { + 'background': style.lookup('TFrame', 'background'), + 'switchcolor': 'white', + 'oncolor': 'green', + 'offcolor': 'red', + 'disabledcolor': 'gray', + 'switchdisabledcolor': '#d9d9d9', + } + for key, value in opts.items(): + self.assertEqual(style.lookup(st, key), value) + + # Changing 'OnOffButton' style before init + style.configure('OnOffButton', background='red', oncolor='yellow') + onoff2 = OnOffButton(self.window) + onoff2.pack() + self.window.update() + opts = { + 'background': 'red', + 'switchcolor': 'white', + 'oncolor': 'yellow', + 'offcolor': 'red', + 'disabledcolor': 'gray', + 'switchdisabledcolor': '#d9d9d9', + } + for key, value in opts.items(): + self.assertEqual(style.lookup(st, key), value) + + # New custom style passing at init + style.configure('OnOffButtonTest', background='orange', oncolor='brown') + onoff3 = OnOffButton(self.window, style='OnOffButtonTest') + onoff3.pack() + self.window.update() + st = onoff3.cget('style') + self.assertEqual(st, 'OnOffButtonTest') + opts = { + 'background': 'orange', + 'switchcolor': 'white', + 'oncolor': 'brown', + 'offcolor': 'red', + 'disabledcolor': 'gray', + 'switchdisabledcolor': '#d9d9d9', + } + for key, value in opts.items(): + self.assertEqual(style.lookup(st, key), value) + + # Passing style to widget config + style.configure('OnOffButtonTest2', background='purple', oncolor='blue') + onoff4 = OnOffButton(self.window) + onoff4.pack() + self.window.update() + onoff4.config(style='OnOffButtonTest2') + st = onoff4.cget('style') + self.assertEqual(st, 'OnOffButtonTest2') + opts = { + 'background': 'purple', + 'switchcolor': 'white', + 'oncolor': 'blue', + 'offcolor': 'red', + 'disabledcolor': 'gray', + 'switchdisabledcolor': '#d9d9d9', + } + for key, value in opts.items(): + self.assertEqual(style.lookup(st, key), value) + + def test_onoffbutton_kwargs(self): + self.assertRaises(TypeError, lambda: OnOffButton(self.window, size='a')) + + def _button_changed(self, button): + return button diff --git a/ttkwidgets/__init__.py b/ttkwidgets/__init__.py index 9788fb40..05432f36 100644 --- a/ttkwidgets/__init__.py +++ b/ttkwidgets/__init__.py @@ -11,3 +11,4 @@ from ttkwidgets.timeline import TimeLine from ttkwidgets.tickscale import TickScale from ttkwidgets.table import Table +from ttkwidgets.onoffbutton import OnOffButton diff --git a/ttkwidgets/onoffbutton.py b/ttkwidgets/onoffbutton.py new file mode 100644 index 00000000..c6fb94b8 --- /dev/null +++ b/ttkwidgets/onoffbutton.py @@ -0,0 +1,319 @@ +""" +Author: Fredy Ramirez +License: GNU GPLv3 +Source: This repository +""" +try: + import Tkinter as tk + import ttk +except ImportError: + import tkinter as tk + from tkinter import ttk + +# TODO: +# - takefocus (focus traversal) +# - test on linux + +class OnOffButton(ttk.Frame): + """ + A simple On/Off button. + """ + + _style_class = None + _style_opts = None + + def __init__(self, master=None, size=24, command=None, state='!disabled', + onvalue=1, offvalue=0, variable=None, style=None): + """ + Create an OnOffButton. + + :param master: master widget + :type master: widget + :param size: Siwtch button diameter size. + :type size: int + :command: A function to be called whenever the state of this onoffbutton changes. + :type command: callback function. + :param state: Initial state of onoffbutton. Default '!disabled'. + :type master: str + :param onvalue: Value returned by onoffbutton when it is 'on'. Default 1. + :type onvalue: It depends of variable type. + :param offvalue: Value returned by onoffbutton when it is 'off'. Default 0. + :type offvalue: It depends of variable type. + :param variable: A control variable that tracks the current state of the onoffbutton. + : It can be IntVar, BooleanVar or StringVar. Default IntVar. + :type variable: Tk control variable. + :style: The ttk style to be used in rendering onoffbutton. Default 'OnOffButton'. + : Note that onoffbutton is based on canvas drawing, wich is not a ttk widget, + : so some functionalitys does not work. For example, you can not change its style + : dinamicaly, you need to pass the style changes to onoffbutton.config(style=new_style) + : to take efect. + : 'OnOffButton' style defines folowing options: background, switchcolor, oncolor, offcolor, + : disabledcolor and switchdisabledcolor. + :type style: str. + """ + OnOffButton._get_style() + if style is None: + curr_style = OnOffButton._style_class + else: + curr_style = style + OnOffButton._validate_style(curr_style) + # Always pass predefined style for Frame + kwargs_frame = {'style': OnOffButton._style_class + '.TFrame'} + + super(OnOffButton, self).__init__(master, **kwargs_frame) + + self._command = command + self._command_result = None + self._size = self._validate_size(size) + self._variable, self._onvalue, self._offvalue = \ + self._validate_variable(variable, onvalue, offvalue) + self._curr_style_class = curr_style + self._canvas = tk.Canvas(self, + width=self._size * 2, + height=self._size, border=-2, + ) + self._canvas.pack() + self._draw() + self._variable.set(self._offvalue) + + def invoke(self): + ''' + This method toggles the state of the onoffbutton. + If there is a command callback, it calls that callback, and returns whatever value + the callback returned. + ''' + value = \ + self._offvalue if self._variable.get() == self._onvalue else self._onvalue + self._variable.set(value) + #TODO takefocus + return self._command_result + + def get(self): + """ + Get current state value of onoffbutton. + Returns 'onvalue' or 'offvalue'. + """ + return self._variable.get() + + def set(self, value): + """ + Set the state value for onoffbutton. + + :param value: Value to set. It must be 'onvalue' or 'offvalue'. + """ + self._variable.set(value) + + def keys(self): + keys = super(OnOffButton, self).keys() + keys.extend(['state', 'size', + 'variable', 'onvalue', 'offvalue']) + keys.sort() + return keys + + def cget(self, key): + if key == 'state': + return self._canvas.cget('state') + elif key == 'size': + return self._size + elif key == 'variable': + return self._variable + elif key == 'onvalue': + return self._onvalue + elif key == 'offvalue': + return self._offvalue + elif key == 'style': + return self._curr_style_class + else: + super(OnOffButton, self).cget(key) + + def __getitem__(self, key): + return self.cget(key) + + def __setitem__(self, key, value): + return self.configure({key: value}) + + def configure(self, cnf={}, **kw): + kw.update(cnf) + if 'variable' in kw: + # ignore + kw.pop('variable') + if 'state' in kw: + self._canvas.configure(state=kw.pop('state')) + if 'onvalue' in kw: + value = kw.pop('onvalue') + if self._onvalue != value: + change = True if self._variable.get() == self._onvalue else None + self._onvalue = value + if change: + self._variable.set(value) + if 'offvalue' in kw: + value = kw.pop('offvalue') + if self._onvalue != value: + change = True if self._variable.get() == self._offvalue else None + self._offvalue = value + if change: + self._variable.set(value) + if 'style' in kw: + st = kw.pop('style') + OnOffButton._validate_style(st) + self._curr_style_class = st + self._draw() + value = \ + self._offvalue if self._variable.get() == self._offvalue else self._onvalue + self._variable.set(value) + + #super(OnOffButton, self).config(**kw) + + config = configure + + def _draw(self): + style = ttk.Style() + background = style.lookup(OnOffButton._style_class, 'background') + switchcolor = style.lookup(OnOffButton._style_class, 'switchcolor') + oncolor = style.lookup(OnOffButton._style_class, 'oncolor') + offcolor = style.lookup(OnOffButton._style_class, 'offcolor') + disabledcolor = style.lookup( + OnOffButton._style_class, 'disabledcolor') + switchdisabledcolor = style.lookup( + OnOffButton._style_class, 'switchdisabledcolor') + + half = self._size / 2 + space = self._size * 0.15 + + self._canvas.delete('circle') + self._canvas.delete('rectangle_off') + self._canvas.delete('rectangle_on') + self._canvas.tag_unbind('rectangle_on', '') + self._canvas.tag_unbind('rectangle_off', '') + self._canvas.config(bg=background) + + self._create_rectangle('rectangle_off', offcolor, disabledcolor, half) + self._create_rectangle('rectangle_on', oncolor, disabledcolor, half) + + self._create_circle( + half * 3, half, half - space, + tag=('circle', 'right_circle'), fill=switchcolor, + outline=None, width=0, disabledfill=switchdisabledcolor) + self._create_circle( + half, half, half - space, + tag=('circle', 'left_circle'), fill=switchcolor, + outline=None, width=0, disabledfill=switchdisabledcolor) + + self._canvas.tag_bind('rectangle_on', '', self._on_click) + self._canvas.tag_bind('rectangle_off', '', self._on_click) + + def _create_rectangle(self, tag, fill, disabledcolor, half): + self._canvas.create_rectangle(half, 0, half * 3, self._size, + tag=tag, fill=fill, outline=None, width=0, disabledfill='gray') + self._create_circle( + half, half, half - 1, tag=tag, + fill=fill, outline=None, width=0, disabledfill=disabledcolor) + self._create_circle( + half * 3, half, half - 1, tag=tag, + fill=fill, outline=None, width=0, disabledfill=disabledcolor) + + def _create_circle(self, x, y, r, **kwargs): + x0 = x - r + y0 = y - r + x1 = x + r + y1 = y + r + return self._canvas.create_oval(x0, y0, x1, y1, **kwargs) + + def _on_click(self, event=None): + value = \ + self._offvalue if self._variable.get() == self._onvalue else self._onvalue + self._variable.set(value) + + def _variable_write(self, a, b, c): + value = self._variable.get() + if value not in [self._onvalue, self._offvalue]: + raise ValueError("Invalid value '{}'".format(value)) + self._command_result = self._toggle() + + def _toggle(self): + self._canvas.tag_lower('circle') + value = True if self._variable.get() == self._onvalue else None + if value: + self._canvas.tag_raise('rectangle_on') + self._canvas.tag_raise('right_circle') + else: + self._canvas.tag_raise('rectangle_off') + self._canvas.tag_raise('left_circle') + + self._command_result = None + if self._command is not None: + return self._command(self) + + @classmethod + def _validate_style(cls, name): + style = ttk.Style() + add_opts = {} + for opt, value in OnOffButton._style_opts.items(): + if not style.lookup(name, opt): + add_opts[opt] = value + if add_opts: + style.configure(name, **add_opts) + + @classmethod + def _get_style(cls): + if OnOffButton._style_class is not None: + return + + style = ttk.Style() + OnOffButton._style_class = 'OnOffButton' + + # Frame style + style.configure(OnOffButton._style_class + '.TFrame', + background=style.lookup('TFrame', 'background'), padding=0) + + # default options + OnOffButton._style_opts = { + 'background': style.lookup('TFrame', 'background'), + 'switchcolor': 'white', + 'oncolor': 'green', + 'offcolor': 'red', + 'disabledcolor': 'gray', + 'switchdisabledcolor': '#d9d9d9', + } + + onoff_style = style.configure(OnOffButton._style_class) + if onoff_style is None: + style.configure(OnOffButton._style_class, **OnOffButton._style_opts) + else: + cls._validate_style(OnOffButton._style_class) + + def _validate_variable(self, variable, onvalue, offvalue): + if variable is None: + variable = tk.IntVar() + if isinstance(variable, tk.IntVar): + self._validate_type('int', 'onvalue', onvalue) + self._validate_type('int', 'offvalue', offvalue) + if isinstance(variable, tk.BooleanVar): + onvalue = True + offvalue = False + if isinstance(variable, tk.StringVar): + onvalue = str(onvalue) + offvalue = str(offvalue) + if onvalue == offvalue: + raise ValueError('onvalue and offvalue must be diferent.') + variable.trace_variable('w', self._variable_write) + return variable, onvalue, offvalue + + def _validate_size(self, size): + self._validate_type('int', 'size', size) + if size < 12: + size = 12 + if size > 100: + size = 100 + return size + + def _validate_type(self, type_, field, val): + invalid = None + if type_ == 'int': + if not isinstance(val, int): + invalid = True + if type_ == 'bool': + if not isinstance(val, bool): + invalid = True + if invalid: + raise TypeError("'{}' not of '{}' type".format(field, type_)) From 19762f304cc1fa58b5c06c37ae585eb1d4ea4cea Mon Sep 17 00:00:00 2001 From: formateli Date: Mon, 27 Apr 2020 12:27:29 -0500 Subject: [PATCH 2/6] Use current style when draw the widget. --- ttkwidgets/onoffbutton.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/ttkwidgets/onoffbutton.py b/ttkwidgets/onoffbutton.py index c6fb94b8..c6bd10b3 100644 --- a/ttkwidgets/onoffbutton.py +++ b/ttkwidgets/onoffbutton.py @@ -162,20 +162,18 @@ def configure(self, cnf={}, **kw): self._offvalue if self._variable.get() == self._offvalue else self._onvalue self._variable.set(value) - #super(OnOffButton, self).config(**kw) - config = configure def _draw(self): style = ttk.Style() - background = style.lookup(OnOffButton._style_class, 'background') - switchcolor = style.lookup(OnOffButton._style_class, 'switchcolor') - oncolor = style.lookup(OnOffButton._style_class, 'oncolor') - offcolor = style.lookup(OnOffButton._style_class, 'offcolor') + background = style.lookup(self._curr_style_class, 'background') + switchcolor = style.lookup(self._curr_style_class, 'switchcolor') + oncolor = style.lookup(self._curr_style_class, 'oncolor') + offcolor = style.lookup(self._curr_style_class, 'offcolor') disabledcolor = style.lookup( - OnOffButton._style_class, 'disabledcolor') + self._curr_style_class, 'disabledcolor') switchdisabledcolor = style.lookup( - OnOffButton._style_class, 'switchdisabledcolor') + self._curr_style_class, 'switchdisabledcolor') half = self._size / 2 space = self._size * 0.15 From c993e64d03d7e17dae39fa4d35b8783d21130206 Mon Sep 17 00:00:00 2001 From: formateli Date: Mon, 27 Apr 2020 16:06:23 -0500 Subject: [PATCH 3/6] Style improvement. Add bind to event, so widget can be drawn again when style changes (ex: style.theme_use()) --- ttkwidgets/onoffbutton.py | 55 ++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/ttkwidgets/onoffbutton.py b/ttkwidgets/onoffbutton.py index c6bd10b3..85f58cfa 100644 --- a/ttkwidgets/onoffbutton.py +++ b/ttkwidgets/onoffbutton.py @@ -53,12 +53,11 @@ def __init__(self, master=None, size=24, command=None, state='!disabled', """ OnOffButton._get_style() if style is None: - curr_style = OnOffButton._style_class + self._curr_style_class = OnOffButton._style_class else: - curr_style = style - OnOffButton._validate_style(curr_style) + self._curr_style_class = style # Always pass predefined style for Frame - kwargs_frame = {'style': OnOffButton._style_class + '.TFrame'} + kwargs_frame = {'style': OnOffButton._get_frame_style()} super(OnOffButton, self).__init__(master, **kwargs_frame) @@ -67,14 +66,14 @@ def __init__(self, master=None, size=24, command=None, state='!disabled', self._size = self._validate_size(size) self._variable, self._onvalue, self._offvalue = \ self._validate_variable(variable, onvalue, offvalue) - self._curr_style_class = curr_style self._canvas = tk.Canvas(self, width=self._size * 2, height=self._size, border=-2, ) self._canvas.pack() - self._draw() self._variable.set(self._offvalue) + self.bind('', self._configure_event) + self._configure_event() # Force first time draw def invoke(self): ''' @@ -154,16 +153,18 @@ def configure(self, cnf={}, **kw): if change: self._variable.set(value) if 'style' in kw: - st = kw.pop('style') - OnOffButton._validate_style(st) - self._curr_style_class = st - self._draw() - value = \ - self._offvalue if self._variable.get() == self._offvalue else self._onvalue - self._variable.set(value) + self._curr_style_class = kw.pop('style') + self._configure_event() config = configure + def _configure_event(self, event=None): + OnOffButton._validate_style(self._curr_style_class) + self._draw() + value = \ + self._offvalue if self._variable.get() == self._offvalue else self._onvalue + self._variable.set(value) + def _draw(self): style = ttk.Style() background = style.lookup(self._curr_style_class, 'background') @@ -178,9 +179,7 @@ def _draw(self): half = self._size / 2 space = self._size * 0.15 - self._canvas.delete('circle') - self._canvas.delete('rectangle_off') - self._canvas.delete('rectangle_on') + self._canvas.delete('all') self._canvas.tag_unbind('rectangle_on', '') self._canvas.tag_unbind('rectangle_off', '') self._canvas.config(bg=background) @@ -248,10 +247,22 @@ def _validate_style(cls, name): add_opts = {} for opt, value in OnOffButton._style_opts.items(): if not style.lookup(name, opt): - add_opts[opt] = value + if opt == 'background': + add_opts[opt] = style.lookup('TFrame', 'background') + else: + add_opts[opt] = value + if add_opts: style.configure(name, **add_opts) + @classmethod + def _get_frame_style(cls): + # Frame style + style = ttk.Style() + name = OnOffButton._style_class + '.TFrame' + style.configure(name, background=style.lookup('TFrame', 'background'), padding=0) + return name + @classmethod def _get_style(cls): if OnOffButton._style_class is not None: @@ -260,10 +271,6 @@ def _get_style(cls): style = ttk.Style() OnOffButton._style_class = 'OnOffButton' - # Frame style - style.configure(OnOffButton._style_class + '.TFrame', - background=style.lookup('TFrame', 'background'), padding=0) - # default options OnOffButton._style_opts = { 'background': style.lookup('TFrame', 'background'), @@ -274,12 +281,6 @@ def _get_style(cls): 'switchdisabledcolor': '#d9d9d9', } - onoff_style = style.configure(OnOffButton._style_class) - if onoff_style is None: - style.configure(OnOffButton._style_class, **OnOffButton._style_opts) - else: - cls._validate_style(OnOffButton._style_class) - def _validate_variable(self, variable, onvalue, offvalue): if variable is None: variable = tk.IntVar() From e240157a43291f2d8a4f074ef924e92125f1dce8 Mon Sep 17 00:00:00 2001 From: formateli Date: Sat, 9 May 2020 16:01:24 -0500 Subject: [PATCH 4/6] Complete redisign based on ttk.Checkbutton --- examples/example_onoffbutton.py | 32 +-- ttkwidgets/onoffbutton.py | 459 ++++++++++++++------------------ 2 files changed, 217 insertions(+), 274 deletions(-) diff --git a/examples/example_onoffbutton.py b/examples/example_onoffbutton.py index 35c2cc06..03e643e6 100644 --- a/examples/example_onoffbutton.py +++ b/examples/example_onoffbutton.py @@ -8,6 +8,7 @@ import Tkinter as tk except ImportError: import tkinter as tk + from tkinter import ttk window = tk.Tk() @@ -18,26 +19,27 @@ def grid_button(button, row, column): button.grid(row=row, column=column, padx=10, pady=10) -def print_value(button): - print(button.get()) +def print_value(): + print('Button toggled') -onoff_24_off = OnOffButton(window, command=print_value) -grid_button(onoff_24_off, 0, 0) +onoff_off = OnOffButton(window, command=print_value) +grid_button(onoff_off, 0, 0) -onoff_24_on = OnOffButton(window, command=print_value) -grid_button(onoff_24_on, 0, 1) -onoff_24_off.set(1) +onoff_off_dis = OnOffButton(window) +grid_button(onoff_off_dis, 0, 1) +onoff_off_dis['state'] = 'disabled' -onoff_24_disabled = OnOffButton(window, command=print_value) -grid_button(onoff_24_disabled, 0, 2) -onoff_24_disabled.config(state='disabled') +onoff_on = OnOffButton(window, command=print_value, cursor='hand2') +grid_button(onoff_on, 0, 2) +onoff_on.set('on') -onoff_48_on = OnOffButton(window, size=48, command=print_value) -grid_button(onoff_48_on, 1, 0) +onoff_on_dis = OnOffButton(window) +grid_button(onoff_on_dis, 0, 3) +onoff_on_dis.set('on') +onoff_on_dis['state'] = 'disabled' -onoff_100_on = OnOffButton(window, size=100, command=print_value) -grid_button(onoff_100_on, 1, 1) -onoff_100_on.set(1) +chk = ttk.Checkbutton(window) +grid_button(chk, 1, 0) window.mainloop() diff --git a/ttkwidgets/onoffbutton.py b/ttkwidgets/onoffbutton.py index 85f58cfa..1a34e601 100644 --- a/ttkwidgets/onoffbutton.py +++ b/ttkwidgets/onoffbutton.py @@ -1,7 +1,6 @@ """ -Author: Fredy Ramirez +Author: Fredy Ramirez License: GNU GPLv3 -Source: This repository """ try: import Tkinter as tk @@ -10,82 +9,42 @@ import tkinter as tk from tkinter import ttk -# TODO: -# - takefocus (focus traversal) -# - test on linux -class OnOffButton(ttk.Frame): +class OnOffButton(ttk.Checkbutton): """ A simple On/Off button. """ - _style_class = None - _style_opts = None + _images = None - def __init__(self, master=None, size=24, command=None, state='!disabled', - onvalue=1, offvalue=0, variable=None, style=None): + def __init__(self, master=None, **kwargs): """ Create an OnOffButton. - + :param master: master widget :type master: widget - :param size: Siwtch button diameter size. - :type size: int :command: A function to be called whenever the state of this onoffbutton changes. :type command: callback function. - :param state: Initial state of onoffbutton. Default '!disabled'. - :type master: str - :param onvalue: Value returned by onoffbutton when it is 'on'. Default 1. + :param onvalue: Value returned by onoffbutton when it is 'on'. Default 'on'. :type onvalue: It depends of variable type. - :param offvalue: Value returned by onoffbutton when it is 'off'. Default 0. + :param offvalue: Value returned by onoffbutton when it is 'off'. Default 'off'. :type offvalue: It depends of variable type. :param variable: A control variable that tracks the current state of the onoffbutton. - : It can be IntVar, BooleanVar or StringVar. Default IntVar. + : It can be IntVar, BooleanVar or StringVar. Default StringVar. :type variable: Tk control variable. :style: The ttk style to be used in rendering onoffbutton. Default 'OnOffButton'. - : Note that onoffbutton is based on canvas drawing, wich is not a ttk widget, - : so some functionalitys does not work. For example, you can not change its style - : dinamicaly, you need to pass the style changes to onoffbutton.config(style=new_style) - : to take efect. - : 'OnOffButton' style defines folowing options: background, switchcolor, oncolor, offcolor, - : disabledcolor and switchdisabledcolor. :type style: str. """ - OnOffButton._get_style() - if style is None: - self._curr_style_class = OnOffButton._style_class - else: - self._curr_style_class = style - # Always pass predefined style for Frame - kwargs_frame = {'style': OnOffButton._get_frame_style()} - - super(OnOffButton, self).__init__(master, **kwargs_frame) + OnOffButton._setup_style() - self._command = command - self._command_result = None - self._size = self._validate_size(size) - self._variable, self._onvalue, self._offvalue = \ - self._validate_variable(variable, onvalue, offvalue) - self._canvas = tk.Canvas(self, - width=self._size * 2, - height=self._size, border=-2, - ) - self._canvas.pack() - self._variable.set(self._offvalue) - self.bind('', self._configure_event) - self._configure_event() # Force first time draw - - def invoke(self): - ''' - This method toggles the state of the onoffbutton. - If there is a command callback, it calls that callback, and returns whatever value - the callback returned. - ''' - value = \ - self._offvalue if self._variable.get() == self._onvalue else self._onvalue - self._variable.set(value) - #TODO takefocus - return self._command_result + kwargs['variable'], kwargs['onvalue'], kwargs['offvalue'] = \ + self._validate_variable(kwargs.pop('variable', None), + kwargs.pop('onvalue', 'on'), + kwargs.pop('offvalue', 'off')) + kwargs['style'] = kwargs.pop('style', 'OnOffButton') + super(OnOffButton, self).__init__(master, class_='OnOffButton', **kwargs) + self._variable = kwargs['variable'] + self._variable.set(kwargs['offvalue']) def get(self): """ @@ -97,196 +56,17 @@ def get(self): def set(self, value): """ Set the state value for onoffbutton. - + :param value: Value to set. It must be 'onvalue' or 'offvalue'. """ self._variable.set(value) - def keys(self): - keys = super(OnOffButton, self).keys() - keys.extend(['state', 'size', - 'variable', 'onvalue', 'offvalue']) - keys.sort() - return keys - - def cget(self, key): - if key == 'state': - return self._canvas.cget('state') - elif key == 'size': - return self._size - elif key == 'variable': - return self._variable - elif key == 'onvalue': - return self._onvalue - elif key == 'offvalue': - return self._offvalue - elif key == 'style': - return self._curr_style_class - else: - super(OnOffButton, self).cget(key) - - def __getitem__(self, key): - return self.cget(key) - - def __setitem__(self, key, value): - return self.configure({key: value}) - - def configure(self, cnf={}, **kw): - kw.update(cnf) - if 'variable' in kw: - # ignore - kw.pop('variable') - if 'state' in kw: - self._canvas.configure(state=kw.pop('state')) - if 'onvalue' in kw: - value = kw.pop('onvalue') - if self._onvalue != value: - change = True if self._variable.get() == self._onvalue else None - self._onvalue = value - if change: - self._variable.set(value) - if 'offvalue' in kw: - value = kw.pop('offvalue') - if self._onvalue != value: - change = True if self._variable.get() == self._offvalue else None - self._offvalue = value - if change: - self._variable.set(value) - if 'style' in kw: - self._curr_style_class = kw.pop('style') - self._configure_event() - - config = configure - - def _configure_event(self, event=None): - OnOffButton._validate_style(self._curr_style_class) - self._draw() - value = \ - self._offvalue if self._variable.get() == self._offvalue else self._onvalue - self._variable.set(value) - - def _draw(self): - style = ttk.Style() - background = style.lookup(self._curr_style_class, 'background') - switchcolor = style.lookup(self._curr_style_class, 'switchcolor') - oncolor = style.lookup(self._curr_style_class, 'oncolor') - offcolor = style.lookup(self._curr_style_class, 'offcolor') - disabledcolor = style.lookup( - self._curr_style_class, 'disabledcolor') - switchdisabledcolor = style.lookup( - self._curr_style_class, 'switchdisabledcolor') - - half = self._size / 2 - space = self._size * 0.15 - - self._canvas.delete('all') - self._canvas.tag_unbind('rectangle_on', '') - self._canvas.tag_unbind('rectangle_off', '') - self._canvas.config(bg=background) - - self._create_rectangle('rectangle_off', offcolor, disabledcolor, half) - self._create_rectangle('rectangle_on', oncolor, disabledcolor, half) - - self._create_circle( - half * 3, half, half - space, - tag=('circle', 'right_circle'), fill=switchcolor, - outline=None, width=0, disabledfill=switchdisabledcolor) - self._create_circle( - half, half, half - space, - tag=('circle', 'left_circle'), fill=switchcolor, - outline=None, width=0, disabledfill=switchdisabledcolor) - - self._canvas.tag_bind('rectangle_on', '', self._on_click) - self._canvas.tag_bind('rectangle_off', '', self._on_click) - - def _create_rectangle(self, tag, fill, disabledcolor, half): - self._canvas.create_rectangle(half, 0, half * 3, self._size, - tag=tag, fill=fill, outline=None, width=0, disabledfill='gray') - self._create_circle( - half, half, half - 1, tag=tag, - fill=fill, outline=None, width=0, disabledfill=disabledcolor) - self._create_circle( - half * 3, half, half - 1, tag=tag, - fill=fill, outline=None, width=0, disabledfill=disabledcolor) - - def _create_circle(self, x, y, r, **kwargs): - x0 = x - r - y0 = y - r - x1 = x + r - y1 = y + r - return self._canvas.create_oval(x0, y0, x1, y1, **kwargs) - - def _on_click(self, event=None): - value = \ - self._offvalue if self._variable.get() == self._onvalue else self._onvalue - self._variable.set(value) - - def _variable_write(self, a, b, c): - value = self._variable.get() - if value not in [self._onvalue, self._offvalue]: - raise ValueError("Invalid value '{}'".format(value)) - self._command_result = self._toggle() - - def _toggle(self): - self._canvas.tag_lower('circle') - value = True if self._variable.get() == self._onvalue else None - if value: - self._canvas.tag_raise('rectangle_on') - self._canvas.tag_raise('right_circle') - else: - self._canvas.tag_raise('rectangle_off') - self._canvas.tag_raise('left_circle') - - self._command_result = None - if self._command is not None: - return self._command(self) - - @classmethod - def _validate_style(cls, name): - style = ttk.Style() - add_opts = {} - for opt, value in OnOffButton._style_opts.items(): - if not style.lookup(name, opt): - if opt == 'background': - add_opts[opt] = style.lookup('TFrame', 'background') - else: - add_opts[opt] = value - - if add_opts: - style.configure(name, **add_opts) - - @classmethod - def _get_frame_style(cls): - # Frame style - style = ttk.Style() - name = OnOffButton._style_class + '.TFrame' - style.configure(name, background=style.lookup('TFrame', 'background'), padding=0) - return name - - @classmethod - def _get_style(cls): - if OnOffButton._style_class is not None: - return - - style = ttk.Style() - OnOffButton._style_class = 'OnOffButton' - - # default options - OnOffButton._style_opts = { - 'background': style.lookup('TFrame', 'background'), - 'switchcolor': 'white', - 'oncolor': 'green', - 'offcolor': 'red', - 'disabledcolor': 'gray', - 'switchdisabledcolor': '#d9d9d9', - } - def _validate_variable(self, variable, onvalue, offvalue): if variable is None: - variable = tk.IntVar() + variable = tk.StringVar() if isinstance(variable, tk.IntVar): - self._validate_type('int', 'onvalue', onvalue) - self._validate_type('int', 'offvalue', offvalue) + onvalue = self._validate_int(onvalue, 1) + offvalue = self._validate_int(offvalue, 0) if isinstance(variable, tk.BooleanVar): onvalue = True offvalue = False @@ -295,24 +75,185 @@ def _validate_variable(self, variable, onvalue, offvalue): offvalue = str(offvalue) if onvalue == offvalue: raise ValueError('onvalue and offvalue must be diferent.') - variable.trace_variable('w', self._variable_write) return variable, onvalue, offvalue - def _validate_size(self, size): - self._validate_type('int', 'size', size) - if size < 12: - size = 12 - if size > 100: - size = 100 - return size + def _validate_int(self, val, default): + if not isinstance(val, int): + return default + return val + + @classmethod + def _setup_style(cls): + """ + Setups the style for the OnOffButton. + :param bg: + :param disabledbg: + """ + if OnOffButton._images is not None: + return + + style = ttk.Style() + dummy_widget = ttk.Checkbutton() + + OnOffButton._images = ( # Must be on cls to keep reference + tk.PhotoImage("img_switch", data=""" + iVBORw0KGgoAAAANSUhEUgAAADQAAAAYCAMAAACCyC6UAAADAFBMVEUAAAD////Q + 2ePR0eTM1ebO1ubP1eTQ1eXR1ubP1uXQ1+bQ1+bP1ufP1ufQ1+jO1eXO1+bO1efO + 1ebP1ubQ1ubP1ubQ1ubO1ufP1eXP1ubP1ubP1ubQ1+bO1ubP1ubP1ufP1ubP1ubP + 1ubQ1ebP1ufP1ubP1ebP1ubP1ubW3OrP1ubW3OrP1ubP1ubP1ubQ1ubW2+nX3OrP + 1ubP1ubc4u3P1ubb4ezd4u3P1uaKkJ2LkJ6LkZ+Mkp+kqrimq7qmrLqnrbynrr2o + rrypr72psL+qsL2qsL6rssDO1eXP1ubW3Ora4Ozb4ezc4e3s7vXs7/Xz9Pnz9fn3 + +Pv3+fv4+fv7/P38/P38/f79/f7///9aWlpbW1tcXFxdXV1eXl5fX19gYGBhYWFi + YmJjY2NkZGRlZWVmZmZnZ2doaGhpaWlqampra2tsbGxtbW1ubm5vb29wcHBxcXFy + cnJzc3N0dHR1dXV2dnZ3d3d4eHh5eXl6enp7e3t8fHx9fX1+fn5/f3+AgICBgYGC + goKDg4OEhISFhYWGhoaHh4eIiIiJiYmKioqLi4uMjIyNjY2Ojo6Pj4+QkJCRkZGS + kpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqbm5ucnJydnZ2enp6fn5+goKChoaGi + oqKjo6OkpKSlpaWmpqanp6eoqKipqamqqqqrq6usrKytra2urq6vr6+wsLCxsbGy + srKzs7O0tLS1tbW2tra3t7e4uLi5ubm6urq7u7u8vLy9vb2+vr6/v7/AwMDBwcHC + wsLDw8PExMTFxcXGxsbHx8fIyMjJycnKysrLy8vMzMzNzc3Ozs7Pz8/Q0NDR0dHS + 0tLT09PU1NTV1dXW1tbX19fY2NjZ2dna2trb29vc3Nzd3d3e3t7f39/g4ODh4eHi + 4uLj4+Pk5OTl5eXm5ubn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fHy + 8vLz8/P09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz9/f3+/v7////J3IgwAAAA + OXRSTlMAARscHh8wMTJFRkdKS0xOU1Rub3GanJ2foLW2t8LFxtPU1dfb3N3r7O/w + 8PHy9Pf4+Pr7+/z8/P2pOdQBAAABJUlEQVR4nL3U2VbCMBAG4FZAXFBAERAXFMEd + 3JeobBahETC2IKbv/yJODUjb9IzChf/VXPQ7SSeZKMp/J5TY3C9RJKXCRiLkIsH1 + O9bnvMe6mKNX6cDYLJ8Y1jDGK8oOl0YmejuwfjJoo+o+LszchcOAwtc6C9tGzRqW + KwaKaFYFtNi1PMG7QecBpZkXMfnDxwalrQf9u04C2ut7UU9GdaI1SUXUu4CK3Iu4 + jPQaIeVhXQR0KqFPnx9pEVIVu6PHgHLS9kzZNEntmWii3gGUkhrx5tMITddfnsRS + a4AWpJZ3fLbniN1ydet9osPdtg9XCZ9/OM1v12h28gt7Extd88jRX0fjIDIeqEDq + mpmcmwzvwWVyxj27q5k8Pu75zEpw6tdk2nwBcgYKibbxIuoAAAAASUVORK5CYII= + """, master=dummy_widget), + + tk.PhotoImage("img_switch_active", data=""" + iVBORw0KGgoAAAANSUhEUgAAADQAAAAYCAMAAACCyC6UAAADAFBMVEUAAAAA//9V + l+NSkuRVkeZSlN5QleRTkuBSlOBRlOFUleJTk+JTlONSluBRlOFSk+JTk+NSleFR + lOFTk+FRleJTk+FSleJRlOJSlOJRlOJSlOFTleJSlOJRleJSlONSlOFSk+JSlOJR + lOJSlONSlOJSlOJSlOJSlOJSlOJspOZtpOZSlOJRlOJSlOFSlOJSlOJro+ZSlOJS + lOJ/sOuBsep8rel8rulSlOJZmONZmeNloOVmoOVoouZpouZqo+ZrpOZspOZ3q+h7 + rel7rul8rul/sOqAseqBseqLt+yixe+pyvGqyvGuzfK51PO61PO61fS71fS81fTS + 4/fT5PjU5PjU5fjV5fjj7vrk7vrl7/vm7/vr8vzs8/zx9/3z9/3z+P31+f33+v74 + +v78/f////9lZWVmZmZnZ2doaGhpaWlqampra2tsbGxtbW1ubm5vb29wcHBxcXFy + cnJzc3N0dHR1dXV2dnZ3d3d4eHh5eXl6enp7e3t8fHx9fX1+fn5/f3+AgICBgYGC + goKDg4OEhISFhYWGhoaHh4eIiIiJiYmKioqLi4uMjIyNjY2Ojo6Pj4+QkJCRkZGS + kpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqbm5ucnJydnZ2enp6fn5+goKChoaGi + oqKjo6OkpKSlpaWmpqanp6eoqKipqamqqqqrq6usrKytra2urq6vr6+wsLCxsbGy + srKzs7O0tLS1tbW2tra3t7e4uLi5ubm6urq7u7u8vLy9vb2+vr6/v7/AwMDBwcHC + wsLDw8PExMTFxcXGxsbHx8fIyMjJycnKysrLy8vMzMzNzc3Ozs7Pz8/Q0NDR0dHS + 0tLT09PU1NTV1dXW1tbX19fY2NjZ2dna2trb29vc3Nzd3d3e3t7f39/g4ODh4eHi + 4uLj4+Pk5OTl5eXm5ubn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fHy + 8vLz8/P09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz9/f3+/v7///+uaILKAAAA + N3RSTlMAARscHh8wMTJFRkdKS0xOU1Rub3GanJ2foLW2t8LFxtPU1dfb3N3s7e7v + 8PHy9Pf4+vv7/P39KwCFLAAAASxJREFUeJy91GdTwkAQBuBEQCwoFgTEAirYQaxn + 74ggRgQhYAmG7P//DV6EkMvlZsfhg+/X3Wfu5rIbSfrv+ELzqxmCJJOaC/kcxDu7 + hwGSLzfb7cbLQdRjm/F1lFzXoZva5phlgvgx9y3opXU02TFDO/g5jAHQsn7TyHHU + kCo4Uo/LFI0yDSdnLpMHLrlhiqJ2w3Hl44JHrzxSwhStMAY+L3nU5FFjiaK0WTp9 + L4oN0XmkpynaNks3BpSExo2+tyha/q09GiA07uupCYoineITGCJDyjx6nqFopFst + 3IoMeRA+ubwgbO6l4jS1RfPjSn58jM6/WKNlB/80sHcaYw4nrDEPrKHq6s0y1Y2A + vVCeyC7Kcoqq66qyHx5w7u50LImvezI25e37b9JvfgAjU++Y681BdAAAAABJRU5E + rkJggg== + """, master=dummy_widget), + + tk.PhotoImage("img_switch_active_insensitive", data=""" + iVBORw0KGgoAAAANSUhEUgAAADQAAAAYCAMAAACCyC6UAAADAFBMVEUAAAAA//9V + md1Vme5Qj+9LluFOk+tVjuNVl+NRlN1XmuRVluVRleBPkuFTlOFTkONTluNRleFU + kuJSlOJRk+FQlOFSleJRlOJSleNSlOBSlONRlOBTleNSk+FRk+JRleJRlONSleNR + leNSlOFSlORSk+JSleJrpOZtpehRk+JRleJSlOBSlOJRlOFro+dSleNRlOGAsOuB + set9rOl9rulSlOJZl+RZmeRkoOZmoOZoouZqouZrpOZ2q+d8rel8r+mAsemBsemK + tu2ixe+pyvCvzvK60/K61fS81fTR5PjT5PjT5vjV5vjk7/rm7/vr8vvt8vvw+P3y + +P32+v34+v37/f////9XV1dYWFhZWVlaWlpbW1tcXFxdXV1eXl5fX19gYGBhYWFi + YmJjY2NkZGRlZWVmZmZnZ2doaGhpaWlqampra2tsbGxtbW1ubm5vb29wcHBxcXFy + cnJzc3N0dHR1dXV2dnZ3d3d4eHh5eXl6enp7e3t8fHx9fX1+fn5/f3+AgICBgYGC + goKDg4OEhISFhYWGhoaHh4eIiIiJiYmKioqLi4uMjIyNjY2Ojo6Pj4+QkJCRkZGS + kpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqbm5ucnJydnZ2enp6fn5+goKChoaGi + oqKjo6OkpKSlpaWmpqanp6eoqKipqamqqqqrq6usrKytra2urq6vr6+wsLCxsbGy + srKzs7O0tLS1tbW2tra3t7e4uLi5ubm6urq7u7u8vLy9vb2+vr6/v7/AwMDBwcHC + wsLDw8PExMTFxcXGxsbHx8fIyMjJycnKysrLy8vMzMzNzc3Ozs7Pz8/Q0NDR0dHS + 0tLT09PU1NTV1dXW1tbX19fY2NjZ2dna2trb29vc3Nzd3d3e3t7f39/g4ODh4eHi + 4uLj4+Pk5OTl5eXm5ubn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fHy + 8vLz8/P09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz9/f3+/v7///+yPa/OAAAA + V3RSTlMAAQ8PEBEaGxsmJicpKisuLjw9PlVWV1hjZGRrbG10dHV2eHl5goKDg4SE + hYaIiImKioqLi4yMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIzAksdu + AAABH0lEQVR4nL3U21LCQAwG4K2AeABRoIgKKqiogHhcj6hFoHJq1dru+z+KqUBp + t5nMyIX/bfLNdnaTMvbfiWW2D+ucSP1gKxMLkOjmBQV4q2fatvF+lYvMTLJCkuZI + TDI8SUzNGn3MqyW8WDfrY7N0Rp/jM6AacdcoRdLwgQhkVFQArfoa7h5CpiWkaMuA + crOG2/7Hk4x6MtJVQCWfEZ8hZMrI2ANUc0v3Zgc33JaRXQNUdUvPjuiiJoy+TwHt + /9beHIEa5PN2AanjYkc4mEEuIgtoZVJtv2CGa+iVKztos5d+0AwL7uOyOD1Gj19+ + YzUW/z6w16npmCeOSNX0xm9w7K0GYxH1nGSabsAS6pfqQnB30/kyve7l/EZ0/t/J + nPkB7ijb9HXezlAAAAAASUVORK5CYII= + """, master=dummy_widget), + + tk.PhotoImage("img_switch_insensitive", data=""" + iVBORw0KGgoAAAANSUhEUgAAADQAAAAYCAMAAACCyC6UAAADAFBMVEUAAAD////M + 3d3d3e7P3+/S0uHO2OvQ2ePZ2ezQ1+TQ1+vR2OXN0+bN2ubO1efQ1eHN0+PN0+nQ + 2ebN1ebS1ubP1eTQ1eTQ1efQ1ujO1uXR1uXP1ubR2ejN1OXQ1+fO1eXP1eXP1+fP + 1uXP1ufQ1+jP1+bQ1eXO1ubW3OrP1ebR1ubV3OrP1ebP1+bO1uXW2+rY2+rP1ufP + 1uXR1ufc4e3e4e3P1eWKkJ2KkJ6Kkp6Mkp6kqbimq7qorbyor7yor72pr72psb2p + sb+rs7/O1ebQ1ebV3Onb4O3b4u3c4u3t7/by9Pry9vr4+Pv4+vv7+/37/f39/f3/ + //9TU1NUVFRVVVVWVlZXV1dYWFhZWVlaWlpbW1tcXFxdXV1eXl5fX19gYGBhYWFi + YmJjY2NkZGRlZWVmZmZnZ2doaGhpaWlqampra2tsbGxtbW1ubm5vb29wcHBxcXFy + cnJzc3N0dHR1dXV2dnZ3d3d4eHh5eXl6enp7e3t8fHx9fX1+fn5/f3+AgICBgYGC + goKDg4OEhISFhYWGhoaHh4eIiIiJiYmKioqLi4uMjIyNjY2Ojo6Pj4+QkJCRkZGS + kpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqbm5ucnJydnZ2enp6fn5+goKChoaGi + oqKjo6OkpKSlpaWmpqanp6eoqKipqamqqqqrq6usrKytra2urq6vr6+wsLCxsbGy + srKzs7O0tLS1tbW2tra3t7e4uLi5ubm6urq7u7u8vLy9vb2+vr6/v7/AwMDBwcHC + wsLDw8PExMTFxcXGxsbHx8fIyMjJycnKysrLy8vMzMzNzc3Ozs7Pz8/Q0NDR0dHS + 0tLT09PU1NTV1dXW1tbX19fY2NjZ2dna2trb29vc3Nzd3d3e3t7f39/g4ODh4eHi + 4uLj4+Pk5OTl5eXm5ubn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fHy + 8vLz8/P09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz9/f3+/v7////QOFaRAAAA + U3RSTlMAAQ8PEBEaGxsmJicpKSorLi48PT5VVlZXWGNkZGtsbXR0dXZ4eYGCg4SE + hIWGiIiIiYqKioqLjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjDj87KcAAAEd + SURBVHicvdTZVsIwEAbgVkBcUBYREBcEWRVc0biAtNKAhLaYvv/DOCUgtOHMoVz4 + X81Fv5N0komi/Hci6bNKiyJpVU7TEQ8Jnzwzm3ObjTBHH3OhhTm8MZ1ZzAHKagdz + E29PnL9Mhqh6SQqzc79kQOFr3UZdoxZMxxMTRbSgAtofOb7g3aC7gHLMj5j84dsX + pf1XY1pnAF3ZfmTLqEc0nXREXQTU5H7EZWR0CfmY1U1A1xL6WfEjfUI6Yne0Aagk + bc+SjU66n0QT9SWg7FqN0AxDfxdLHQPak1r+vWJ7S3Fbrp6PAx3uhXu4SvQu0DXa + Dn5hnxLzax6rrzsa1dhioELZNrM4txjeg4fMlnd2j/JlfNzL+VR449dk0/wCFTr6 + UPmZVCwAAAAASUVORK5CYII= + """, master=dummy_widget), + ) + + for seq in dummy_widget.bind_class("TCheckbutton"): + dummy_widget.bind_class("OnOffButton", seq, + dummy_widget.bind_class("TCheckbutton", seq), True) + + style.element_create( + 'Switch.switch', 'image', 'img_switch', + ('disabled', 'selected', 'img_switch_active_insensitive'), + ('selected', 'img_switch_active'), + ('disabled', 'img_switch_insensitive'), + width=52, + sticky='w' + ) - def _validate_type(self, type_, field, val): - invalid = None - if type_ == 'int': - if not isinstance(val, int): - invalid = True - if type_ == 'bool': - if not isinstance(val, bool): - invalid = True - if invalid: - raise TypeError("'{}' not of '{}' type".format(field, type_)) + style.layout('OnOffButton', + [ + ( + 'Switch.padding', + { + 'sticky': 'nswe', + 'children': [ + ( + 'Switch.switch', + { + 'side': 'left', 'sticky': '' + } + ), + ( + 'Switch.focus', + { + 'side': 'left', 'sticky': 'w', + 'children': [ + ( + 'Switch.label', + { + 'sticky': 'nswe' + } + ) + ] + } + ) + ] + } + ) + ] + ) From e659816553207531a608315e82c999313b022a56 Mon Sep 17 00:00:00 2001 From: formateli Date: Sat, 9 May 2020 22:46:10 -0500 Subject: [PATCH 5/6] Set Style.configure('OnOffButton') --- ttkwidgets/onoffbutton.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ttkwidgets/onoffbutton.py b/ttkwidgets/onoffbutton.py index 1a34e601..418566bc 100644 --- a/ttkwidgets/onoffbutton.py +++ b/ttkwidgets/onoffbutton.py @@ -257,3 +257,6 @@ def _setup_style(cls): ) ] ) + + style.configure('OnOffButton', + **style.configure('TCheckbutton').copy()) From 21c313adf162bac404a6937937c1cc99f19e9941 Mon Sep 17 00:00:00 2001 From: formateli Date: Sat, 9 May 2020 23:14:55 -0500 Subject: [PATCH 6/6] Update tests --- tests/test_onoffbutton.py | 114 +++++++++++--------------------------- 1 file changed, 33 insertions(+), 81 deletions(-) diff --git a/tests/test_onoffbutton.py b/tests/test_onoffbutton.py index af9c07f3..cc380168 100644 --- a/tests/test_onoffbutton.py +++ b/tests/test_onoffbutton.py @@ -16,125 +16,77 @@ def test_onoffbutton_variable_default(self): onoff = OnOffButton(self.window) onoff.pack() self.window.update() - variable = onoff.cget('variable') - self.assertEqual(isinstance(variable, tk.IntVar), True) - self.assertEqual(variable.get(), 0) - variable.set(1) - self.assertEqual(variable.get(), 1) + variable = onoff._variable + self.assertEqual(isinstance(variable, tk.StringVar), True) + self.assertEqual(variable.get(), 'off') + variable.set('on') + self.assertEqual(variable.get(), 'on') def test_onoffbutton_variable_int(self): variable = tk.IntVar() onoff = OnOffButton(self.window, variable=variable) onoff.pack() self.window.update() - self.assertEqual(variable.get(), False) + self.assertEqual(variable.get(), 0) variable.set(1) - self.assertEqual(variable.get(), True) + self.assertEqual(variable.get(), 1) def test_onoffbutton_variable_bool(self): variable = tk.BooleanVar() onoff = OnOffButton(self.window, variable=variable) onoff.pack() self.window.update() - self.assertEqual(variable.get(), 0) - variable.set(1) - self.assertEqual(variable.get(), 1) + self.assertEqual(variable.get(), False) + variable.set(True) + self.assertEqual(variable.get(), True) def test_onoffbutton_variable_string(self): variable = tk.StringVar() onoff = OnOffButton(self.window, variable=variable) onoff.pack() self.window.update() - self.assertEqual(variable.get(), '0') - variable.set(1) - self.assertEqual(variable.get(), '1') - onoff.config(onvalue='ON', offvalue='OFF') - self.assertEqual(variable.get(), 'ON') - onoff.invoke() - self.assertEqual(variable.get(), 'OFF') + self.assertEqual(variable.get(), 'off') + variable.set('on') + self.assertEqual(variable.get(), 'on') def test_onoffbutton_invoke(self): onoff = OnOffButton(self.window, command=self._button_changed) onoff.pack() self.window.update() - self.assertEqual(onoff.get(), 0) + self.assertEqual(onoff.get(), 'off') result = onoff.invoke() - self.assertEqual(onoff, result) - self.assertEqual(onoff.get(), 1) + self.assertEqual('Invoked', result) + self.assertEqual(onoff.get(), 'on') def test_onoffbutton_style(self): style = ttk.Style() onoff = OnOffButton(self.window) onoff.pack() self.window.update() + self.assertEqual(onoff.cget('class'), 'OnOffButton') st = onoff.cget('style') self.assertEqual(st, 'OnOffButton') - opts = { - 'background': style.lookup('TFrame', 'background'), - 'switchcolor': 'white', - 'oncolor': 'green', - 'offcolor': 'red', - 'disabledcolor': 'gray', - 'switchdisabledcolor': '#d9d9d9', - } - for key, value in opts.items(): - self.assertEqual(style.lookup(st, key), value) - # Changing 'OnOffButton' style before init - style.configure('OnOffButton', background='red', oncolor='yellow') - onoff2 = OnOffButton(self.window) + # New custom style passing at init + style.configure('OnOffButtonTest.OnOffButton', + background='orange', oncolor='brown') + onoff2 = OnOffButton(self.window, + style='OnOffButtonTest.OnOffButton') onoff2.pack() self.window.update() - opts = { - 'background': 'red', - 'switchcolor': 'white', - 'oncolor': 'yellow', - 'offcolor': 'red', - 'disabledcolor': 'gray', - 'switchdisabledcolor': '#d9d9d9', - } - for key, value in opts.items(): - self.assertEqual(style.lookup(st, key), value) + self.assertEqual(onoff2.cget('class'), 'OnOffButton') + st = onoff2.cget('style') + self.assertEqual(st, 'OnOffButtonTest.OnOffButton') - # New custom style passing at init - style.configure('OnOffButtonTest', background='orange', oncolor='brown') - onoff3 = OnOffButton(self.window, style='OnOffButtonTest') + # Passing style to widget config + style.configure('OnOffButtonTest2.OnOffButton', + background='purple', oncolor='blue') + onoff3 = OnOffButton(self.window) onoff3.pack() self.window.update() + onoff3.config(style='OnOffButtonTest2.OnOffButton') st = onoff3.cget('style') - self.assertEqual(st, 'OnOffButtonTest') - opts = { - 'background': 'orange', - 'switchcolor': 'white', - 'oncolor': 'brown', - 'offcolor': 'red', - 'disabledcolor': 'gray', - 'switchdisabledcolor': '#d9d9d9', - } - for key, value in opts.items(): - self.assertEqual(style.lookup(st, key), value) - - # Passing style to widget config - style.configure('OnOffButtonTest2', background='purple', oncolor='blue') - onoff4 = OnOffButton(self.window) - onoff4.pack() - self.window.update() - onoff4.config(style='OnOffButtonTest2') - st = onoff4.cget('style') - self.assertEqual(st, 'OnOffButtonTest2') - opts = { - 'background': 'purple', - 'switchcolor': 'white', - 'oncolor': 'blue', - 'offcolor': 'red', - 'disabledcolor': 'gray', - 'switchdisabledcolor': '#d9d9d9', - } - for key, value in opts.items(): - self.assertEqual(style.lookup(st, key), value) - - def test_onoffbutton_kwargs(self): - self.assertRaises(TypeError, lambda: OnOffButton(self.window, size='a')) + self.assertEqual(st, 'OnOffButtonTest2.OnOffButton') - def _button_changed(self, button): - return button + def _button_changed(self): + return 'Invoked'