pep-mklive/pylibraries/ttkbootstrap/widgets.py

1163 lines
40 KiB
Python
Raw Normal View History

2024-10-27 15:16:46 -01:00
import tkinter as tk
from tkinter import ttk
from tkinter import font
from tkinter.ttk import Button, Checkbutton, Combobox
from tkinter.ttk import Entry, Frame, Label
from tkinter.ttk import Labelframe, LabelFrame, Menubutton
from tkinter.ttk import Notebook, OptionMenu, PanedWindow
from tkinter.ttk import Panedwindow, Progressbar, Radiobutton
from tkinter.ttk import Scale, Scrollbar, Separator
from tkinter.ttk import Sizegrip, Spinbox, Treeview
from ttkbootstrap.constants import *
# date entry imports
from ttkbootstrap.dialogs import Querybox
from datetime import datetime
# floodgauge imports
import math
# meter imports
from PIL import Image, ImageTk, ImageDraw
from ttkbootstrap.style import Colors
from ttkbootstrap import utility
from ttkbootstrap.style import Bootstyle
M = 3 # meter image scale, higher number increases resolution
TTK_WIDGETS = (
ttk.Button,
ttk.Checkbutton,
ttk.Combobox,
ttk.Entry,
ttk.Frame,
ttk.Labelframe,
ttk.Label,
ttk.Menubutton,
ttk.Notebook,
ttk.Panedwindow,
ttk.Progressbar,
ttk.Radiobutton,
ttk.Scale,
ttk.Scrollbar,
ttk.Separator,
ttk.Sizegrip,
ttk.Spinbox,
ttk.Treeview,
ttk.OptionMenu,
)
TK_WIDGETS = (
tk.Tk,
tk.Toplevel,
tk.Button,
tk.Label,
tk.Text,
tk.Frame,
tk.Checkbutton,
tk.Radiobutton,
tk.Entry,
tk.Scale,
tk.Listbox,
tk.Menu,
tk.Menubutton,
tk.LabelFrame,
tk.Canvas,
tk.OptionMenu,
tk.Spinbox,
)
class DateEntry(ttk.Frame):
"""A date entry widget combines the `Combobox` and a `Button`
with a callback attached to the `get_date` function.
When pressed, a date chooser popup is displayed. The returned
value is inserted into the combobox.
The date chooser popup will use the date in the combobox as the
date of focus if it is in the format specified by the
`dateformat` parameter. By default, this format is "%Y-%m-%d".
The bootstyle api may be used to change the style of the widget.
The available colors include -> primary, secondary, success,
info, warning, danger, light, dark.
The starting weekday on the date chooser popup can be changed
with the `firstweekday` parameter. By default this value is
`6`, which represents "Sunday".
The `Entry` and `Button` widgets are accessible from the
`DateEntry.Entry` and `DateEntry.Button` properties.
![](../../assets/widgets/date-entry.png)
"""
def __init__(
self,
master=None,
dateformat=r"%x",
firstweekday=6,
startdate=None,
bootstyle="",
**kwargs,
):
"""
Parameters:
master (Widget, optional):
The parent widget.
dateformat (str, optional):
The format string used to render the text in the entry
widget. For more information on acceptable formats, see https://strftime.org/
firstweekday (int, optional):
Specifies the first day of the week. 0=Monday, 1=Tuesday,
etc...
startdate (datetime, optional):
The date that is in focus when the widget is displayed. Default is
current date.
bootstyle (str, optional):
A style keyword used to set the focus color of the entry
and the background color of the date button. Available
options include -> primary, secondary, success, info,
warning, danger, dark, light.
**kwargs (Dict[str, Any], optional):
Other keyword arguments passed to the frame containing the
entry and date button.
"""
self._dateformat = dateformat
self._firstweekday = firstweekday
self._startdate = startdate or datetime.today()
self._bootstyle = bootstyle
super().__init__(master, **kwargs)
# add visual components
entry_kwargs = {"bootstyle": self._bootstyle}
if "width" in kwargs:
entry_kwargs["width"] = kwargs.pop("width")
self.entry = ttk.Entry(self, **entry_kwargs)
self.entry.pack(side=tk.LEFT, fill=tk.X, expand=tk.YES)
self.button = ttk.Button(
master=self,
command=self._on_date_ask,
bootstyle=f"{self._bootstyle}-date",
)
self.button.pack(side=tk.LEFT)
# starting value
self.entry.insert(tk.END, self._startdate.strftime(self._dateformat))
def __getitem__(self, key: str):
return self.configure(cnf=key)
def __setitem__(self, key: str, value):
self.configure(cnf=None, **{key: value})
def _configure_set(self, **kwargs):
"""Override configure method to allow for setting custom
DateEntry parameters"""
if "state" in kwargs:
state = kwargs.pop("state")
if state in ["readonly", "invalid"]:
self.entry.configure(state=state)
elif state in ("disabled", "normal"):
self.entry.configure(state=state)
self.button.configure(state=state)
else:
kwargs[state] = state
if "dateformat" in kwargs:
self._dateformat = kwargs.pop("dateformat")
if "firstweekday" in kwargs:
self._firstweekday = kwargs.pop("firstweekday")
if "startdate" in kwargs:
self._startdate = kwargs.pop("startdate")
if "bootstyle" in kwargs:
self._bootstyle = kwargs.pop("bootstyle")
self.entry.configure(bootstyle=self._bootstyle)
self.button.configure(bootstyle=[self._bootstyle, "date"])
if "width" in kwargs:
width = kwargs.pop("width")
self.entry.configure(width=width)
super(ttk.Frame, self).configure(**kwargs)
def _configure_get(self, cnf):
"""Override the configure get method"""
if cnf == "state":
entrystate = self.entry.cget("state")
buttonstate = self.button.cget("state")
return {"Entry": entrystate, "Button": buttonstate}
if cnf == "dateformat":
return self._dateformat
if cnf == "firstweekday":
return self._firstweekday
if cnf == "startdate":
return self._startdate
if cnf == "bootstyle":
return self._bootstyle
else:
return super(ttk.Frame, self).configure(cnf=cnf)
def configure(self, cnf=None, **kwargs):
"""Configure the options for this widget.
Parameters:
cnf (Dict[str, Any], optional):
A dictionary of configuration options.
**kwargs:
Optional keyword arguments.
"""
if cnf is not None:
return self._configure_get(cnf)
else:
return self._configure_set(**kwargs)
def _on_date_ask(self):
"""Callback for pushing the date button"""
_val = self.entry.get() or datetime.today().strftime(self._dateformat)
try:
self._startdate = datetime.strptime(_val, self._dateformat)
except Exception as e:
print("Date entry text does not match", self._dateformat)
self._startdate = datetime.today()
self.entry.delete(first=0, last=tk.END)
self.entry.insert(
tk.END, self._startdate.strftime(self._dateformat)
)
old_date = datetime.strptime(_val, self._dateformat)
# get the new date and insert into the entry
new_date = Querybox.get_date(
parent=self.entry,
startdate=old_date,
firstweekday=self._firstweekday,
bootstyle=self._bootstyle,
)
self.entry.delete(first=0, last=tk.END)
self.entry.insert(tk.END, new_date.strftime(self._dateformat))
self.entry.focus_force()
class Floodgauge(Progressbar):
"""A widget that shows the status of a long-running operation
with an optional text indicator.
Similar to the `ttk.Progressbar`, this widget can operate in
two modes. *determinate* mode shows the amount completed
relative to the total amount of work to be done, and
*indeterminate* mode provides an animated display to let the
user know that something is happening.
Variable are generated automatically for this widget and can be
linked to other widgets by referencing them via the
`textvariable` and `variable` attributes.
![](../../assets/widgets/floodgauge.gif)
Examples:
```python
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
app = ttk.Window(size=(500, 500))
gauge = ttk.Floodgauge(
bootstyle=INFO,
font=(None, 24, 'bold'),
mask='Memory Used {}%',
)
gauge.pack(fill=BOTH, expand=YES, padx=10, pady=10)
# autoincrement the gauge
gauge.start()
# stop the autoincrement
gauge.stop()
# manually update the gauge value
gauge.configure(value=25)
# increment the value by 10 steps
gauge.step(10)
app.mainloop()
```
"""
def __init__(
self,
master=None,
cursor=None,
font=None,
length=None,
maximum=100,
mode=DETERMINATE,
orient=HORIZONTAL,
bootstyle=PRIMARY,
takefocus=False,
text=None,
value=0,
mask=None,
**kwargs,
):
"""
Parameters:
master (Widget, optional):
Parent widget. Defaults to None.
cursor (str, optional):
The cursor that will appear when the mouse is over the
progress bar. Defaults to None.
font (Union[Font, str], optional):
The font to use for the progress bar label.
length (int, optional):
Specifies the length of the long axis of the progress bar
(width if orient = horizontal, height if if vertical);
maximum (float, optional):
A floating point number specifying the maximum `value`.
Defaults to 100.
mode ('determinate', 'indeterminate'):
Use `indeterminate` if you cannot accurately measure the
relative progress of the underlying process. In this mode,
a rectangle bounces back and forth between the ends of the
widget once you use the `Floodgauge.start()` method.
Otherwise, use `determinate` if the relative progress can be
calculated in advance.
orient ('horizontal', 'vertical'):
Specifies the orientation of the widget.
bootstyle (str, optional):
The style used to render the widget. Options include
primary, secondary, success, info, warning, danger, light,
dark.
takefocus (bool, optional):
This widget is not included in focus traversal by default.
To add the widget to focus traversal, use
`takefocus=True`.
text (str, optional):
A string of text to be displayed in the Floodgauge label.
This is assigned to the attribute `Floodgauge.textvariable`
value (float, optional):
The current value of the progressbar. In `determinate`
mode, this represents the amount of work completed. In
`indeterminate` mode, it is interpreted modulo `maximum`;
that is, the progress bar completes one "cycle" when the
`value` increases by `maximum`.
mask (str, optional):
A string format that can be used to update the Floodgauge
label every time the value is updated. For example, the
string "{}% Storage Used" with a widget value of 45 would
show "45% Storage Used" on the Floodgauge label. If a
mask is set, then the `text` option is ignored.
**kwargs:
Other configuration options from the option database.
"""
# progress bar value variables
if 'variable' in kwargs:
self._variable = kwargs.pop('variable')
else:
self._variable = tk.IntVar(value=value)
if 'textvariable' in kwargs:
self._textvariable = kwargs.pop('textvariable')
else:
self._textvariable = tk.StringVar(value=text)
self._bootstyle = bootstyle
self._font = font or "helvetica 10"
self._mask = mask
self._traceid = None
super().__init__(
master=master,
class_="Floodgauge",
cursor=cursor,
length=length,
maximum=maximum,
mode=mode,
orient=orient,
bootstyle=bootstyle,
takefocus=takefocus,
variable=self._variable,
**kwargs,
)
self._set_widget_text(self._textvariable.get())
self.bind("<<ThemeChanged>>", self._on_theme_change)
self.bind("<<Configure>>", self._on_theme_change)
if self._mask is not None:
self._set_mask()
def _set_widget_text(self, *_):
ttkstyle = self.cget("style")
if self._mask is None:
text = self._textvariable.get()
else:
value = self._variable.get()
text = self._mask.format(value)
self.tk.call("ttk::style", "configure", ttkstyle, "-text", text)
self.tk.call("ttk::style", "configure", ttkstyle, "-font", self._font)
def _set_mask(self):
if self._traceid is None:
self._traceid = self._variable.trace_add(
"write", self._set_widget_text
)
def _unset_mask(self):
if self._traceid is not None:
self._variable.trace_remove("write", self._traceid)
self._traceid = None
def _on_theme_change(self, *_):
text = self._textvariable.get()
self._set_widget_text(text)
def _configure_get(self, cnf):
if cnf == "value":
return self._variable.get()
if cnf == "text":
return self._textvariable.get()
if cnf == "bootstyle":
return self._bootstyle
if cnf == "mask":
return self._mask
if cnf == "font":
return self._font
else:
return super(Progressbar, self).configure(cnf=cnf)
def _configure_set(self, **kwargs):
if "value" in kwargs:
self._variable.set(kwargs.pop("value"))
if "text" in kwargs:
self._textvariable.set(kwargs.pop("text"))
if "bootstyle" in kwargs:
self._bootstyle = kwargs.get("bootstyle")
if "mask" in kwargs:
self._mask = kwargs.pop("mask")
if "font" in kwargs:
self._font = kwargs.pop("font")
if "variable" in kwargs:
self._variable = kwargs.get("variable")
Progressbar.configure(self, cnf=None, **kwargs)
if "textvariable" in kwargs:
self.textvariable = kwargs.pop("textvariable")
else:
Progressbar.configure(self, cnf=None, **kwargs)
def __getitem__(self, key: str):
return self._configure_get(cnf=key)
def __setitem__(self, key: str, value):
self._configure_set(**{key: value})
def configure(self, cnf=None, **kwargs):
"""Configure the options for this widget.
Parameters:
cnf (Dict[str, Any], optional):
A dictionary of configuration options.
**kwargs:
Optional keyword arguments.
"""
if cnf is not None:
return self._configure_get(cnf)
else:
self._configure_set(**kwargs)
@property
def textvariable(self):
"""Returns the textvariable object"""
return self._textvariable
@textvariable.setter
def textvariable(self, value):
"""Set the new textvariable property"""
self._textvariable = value
self._set_widget_text(self._textvariable.get())
@property
def variable(self):
"""Returns the variable object"""
return self._variable
@variable.setter
def variable(self, value):
"""Set the new variable object"""
self._variable = value
if self.cget('variable') != value:
self.configure(variable=self._variable)
class Meter(ttk.Frame):
"""A radial meter that can be used to show progress of long
running operations or the amount of work completed; can also be
used as a dial when set to `interactive=True`.
This widget is very flexible. There are two primary meter types
which can be set with the `metertype` parameter: 'full' and
'semi', which shows the arc of the meter in a full or
semi-circle. You can also customize the arc of the circle with
the `arcrange` and `arcoffset` parameters.
The meter indicator can be displayed as a solid color or with
stripes using the `stripethickness` parameter. By default, the
`stripethickness` is 0, which results in a solid meter
indicator. A higher `stripethickness` results in larger wedges
around the arc of the meter.
Various text and label options exist. The center text and
meter indicator is formatted with the `meterstyle` parameter.
You can set text on the left and right of this center label
using the `textleft` and `textright` parameters. This is most
commonly used for '$', '%', or other such symbols.
If you need access to the variables that update the meter, you
you can access these via the `amountusedvar`, `amounttotalvar`,
and the `labelvar`. The value of these properties can also be
retrieved via the `configure` method.
![](../../assets/widgets/meter.gif)
Examples:
```python
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
app = ttk.Window()
meter = ttk.Meter(
metersize=180,
padding=5,
amountused=25,
metertype="semi",
subtext="miles per hour",
interactive=True,
)
meter.pack()
# update the amount used directly
meter.configure(amountused = 50)
# update the amount used with another widget
entry = ttk.Entry(textvariable=meter.amountusedvar)
entry.pack(fill=X)
# increment the amount by 10 steps
meter.step(10)
# decrement the amount by 15 steps
meter.step(-15)
# update the subtext
meter.configure(subtext="loading...")
app.mainloop()
```
"""
def __init__(
self,
master=None,
bootstyle=DEFAULT,
arcrange=None,
arcoffset=None,
amounttotal=100,
amountused=0,
wedgesize=0,
metersize=200,
metertype=FULL,
meterthickness=10,
showtext=True,
interactive=False,
stripethickness=0,
textleft=None,
textright=None,
textfont="-size 20 -weight bold",
subtext=None,
subtextstyle=DEFAULT,
subtextfont="-size 10",
stepsize=1,
**kwargs,
):
"""
Parameters:
master (Widget):
The parent widget.
arcrange (int):
The range of the arc if degrees from start to end.
arcoffset (int):
The amount to offset the arc's starting position in degrees.
0 is at 3 o'clock.
amounttotal (int):
The maximum value of the meter.
amountused (int):
The current value of the meter; displayed in a center label
if the `showtext` property is set to True.
wedgesize (int):
Sets the length of the indicator wedge around the arc. If
greater than 0, this wedge is set as an indicator centered
on the current meter value.
metersize (int):
The meter is square. This represents the size of one side
if the square as measured in screen units.
bootstyle (str):
Sets the indicator and center text color. One of primary,
secondary, success, info, warning, danger, light, dark.
metertype ('full', 'semi'):
Displays the meter as a full circle or semi-circle.
meterthickness (int):
The thickness of the indicator.
showtext (bool):
Indicates whether to show the left, center, and right text
labels on the meter.
interactive (bool):
Indicates that the user may adjust the meter value with
mouse interaction.
stripethickness (int):
The indicator can be displayed as a solid band or as
striped wedges around the arc. If the value is greater than
0, the indicator changes from a solid to striped, where the
value is the thickness of the stripes (or wedges).
textleft (str):
A short string inserted to the left of the center text.
textright (str):
A short string inserted to the right of the center text.
textfont (Union[str, Font]):
The font used to render the center text.
subtext (str):
Supplemental text that appears below the center text.
subtextstyle (str):
The bootstyle color of the subtext. One of primary,
secondary, success, info, warning, danger, light, dark.
The default color is Theme specific and is a lighter
shade based on whether it is a 'light' or 'dark' theme.
subtextfont (Union[str, Font]):
The font used to render the subtext.
stepsize (int):
Sets the amount by which to change the meter indicator
when incremented by mouse interaction.
**kwargs:
Other keyword arguments that are passed directly to the
`Frame` widget that contains the meter components.
"""
super().__init__(master=master, **kwargs)
# widget variables
self.amountusedvar = tk.IntVar(value=amountused)
self.amountusedvar.trace_add("write", self._draw_meter)
self.amounttotalvar = tk.IntVar(value=amounttotal)
self.labelvar = tk.StringVar(value=subtext)
# misc settings
self._set_arc_offset_range(metertype, arcoffset, arcrange)
self._towardsmaximum = True
self._metersize = utility.scale_size(self, metersize)
self._meterthickness = utility.scale_size(self, meterthickness)
self._stripethickness = stripethickness
self._showtext = showtext
self._wedgesize = wedgesize
self._stepsize = stepsize
self._textleft = textleft
self._textright = textright
self._textfont = textfont
self._subtext = subtext
self._subtextfont = subtextfont
self._subtextstyle = subtextstyle
self._bootstyle = bootstyle
self._interactive = interactive
self._bindids = {}
self._setup_widget()
def _setup_widget(self):
self.meterframe = ttk.Frame(
master=self, width=self._metersize, height=self._metersize
)
self.indicator = ttk.Label(self.meterframe)
self.textframe = ttk.Frame(self.meterframe)
self.textleft = ttk.Label(
master=self.textframe,
text=self._textleft,
font=self._subtextfont,
bootstyle=(self._subtextstyle, "metersubtxt"),
anchor=tk.S,
padding=(0, 5),
)
self.textcenter = ttk.Label(
master=self.textframe,
textvariable=self.amountusedvar,
bootstyle=(self._bootstyle, "meter"),
font=self._textfont,
)
self.textright = ttk.Label(
master=self.textframe,
text=self._textright,
font=self._subtextfont,
bootstyle=(self._subtextstyle, "metersubtxt"),
anchor=tk.S,
padding=(0, 5),
)
self.subtext = ttk.Label(
master=self.meterframe,
text=self._subtext,
bootstyle=(self._subtextstyle, "metersubtxt"),
font=self._subtextfont,
)
self.bind("<<ThemeChanged>>", self._on_theme_change)
self.bind("<<Configure>>", self._on_theme_change)
self._set_interactive_bind()
self._draw_base_image()
self._draw_meter()
# set widget geometery
self.indicator.place(x=0, y=0)
self.meterframe.pack()
self._set_show_text()
def _set_widget_colors(self):
bootstyle = (self._bootstyle, "meter", "label")
ttkstyle = Bootstyle.ttkstyle_name(string="-".join(bootstyle))
textcolor = self._lookup_style_option(ttkstyle, "foreground")
background = self._lookup_style_option(ttkstyle, "background")
troughcolor = self._lookup_style_option(ttkstyle, "space")
self._meterforeground = textcolor
self._meterbackground = Colors.update_hsv(background, vd=-0.1)
self._metertrough = troughcolor
def _set_meter_text(self):
"""Setup and pack the widget labels in the appropriate order"""
self._set_show_text()
self._set_subtext()
def _set_subtext(self):
if self._subtextfont:
if self._showtext:
self.subtext.place(relx=0.5, rely=0.6, anchor=tk.CENTER)
else:
self.subtext.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
def _set_show_text(self):
self.textframe.pack_forget()
self.textcenter.pack_forget()
self.textleft.pack_forget()
self.textright.pack_forget()
self.subtext.pack_forget()
if self._showtext:
if self._subtext:
self.textframe.place(relx=0.5, rely=0.45, anchor=tk.CENTER)
else:
self.textframe.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
self._set_text_left()
self._set_text_center()
self._set_text_right()
self._set_subtext()
def _set_text_left(self):
if self._showtext and self._textleft:
self.textleft.pack(side=tk.LEFT, fill=tk.Y)
def _set_text_center(self):
if self._showtext:
self.textcenter.pack(side=tk.LEFT, fill=tk.Y)
def _set_text_right(self):
self.textright.configure(text=self._textright)
if self._showtext and self._textright:
self.textright.pack(side=tk.RIGHT, fill=tk.Y)
def _set_interactive_bind(self):
seq1 = "<B1-Motion>"
seq2 = "<Button-1>"
if self._interactive:
self._bindids[seq1] = self.indicator.bind(
seq1, self._on_dial_interact
)
self._bindids[seq2] = self.indicator.bind(
seq2, self._on_dial_interact
)
return
if seq1 in self._bindids:
self.indicator.unbind(seq1, self._bindids.get(seq1))
self.indicator.unbind(seq2, self._bindids.get(seq2))
self._bindids.clear()
def _set_arc_offset_range(self, metertype, arcoffset, arcrange):
if metertype == SEMI:
self._arcoffset = 135 if arcoffset is None else arcoffset
self._arcrange = 270 if arcrange is None else arcrange
else:
self._arcoffset = -90 if arcoffset is None else arcoffset
self._arcrange = 360 if arcrange is None else arcrange
self._metertype = metertype
def _draw_meter(self, *_):
"""Draw a meter"""
img = self._base_image.copy()
draw = ImageDraw.Draw(img)
if self._stripethickness > 0:
self._draw_striped_meter(draw)
else:
self._draw_solid_meter(draw)
self._meterimage = ImageTk.PhotoImage(
img.resize((self._metersize, self._metersize), Image.CUBIC)
)
self.indicator.configure(image=self._meterimage)
def _draw_base_image(self):
"""Draw base image to be used for subsequent updates"""
self._set_widget_colors()
self._base_image = Image.new(
mode="RGBA", size=(self._metersize * M, self._metersize * M)
)
draw = ImageDraw.Draw(self._base_image)
x1 = y1 = self._metersize * M - 20
width = self._meterthickness * M
# striped meter
if self._stripethickness > 0:
_from = self._arcoffset
_to = self._arcrange + self._arcoffset
_step = 2 if self._stripethickness == 1 else self._stripethickness
for x in range(_from, _to, _step):
draw.arc(
xy=(0, 0, x1, y1),
start=x,
end=x + self._stripethickness - 1,
fill=self._metertrough,
width=width,
)
# solid meter
else:
draw.arc(
xy=(0, 0, x1, y1),
start=self._arcoffset,
end=self._arcrange + self._arcoffset,
fill=self._metertrough,
width=width,
)
def _draw_solid_meter(self, draw: ImageDraw.Draw):
"""Draw a solid meter"""
x1 = y1 = self._metersize * M - 20
width = self._meterthickness * M
if self._wedgesize > 0:
meter_value = self._meter_value()
draw.arc(
xy=(0, 0, x1, y1),
start=meter_value - self._wedgesize,
end=meter_value + self._wedgesize,
fill=self._meterforeground,
width=width,
)
else:
draw.arc(
xy=(0, 0, x1, y1),
start=self._arcoffset,
end=self._meter_value(),
fill=self._meterforeground,
width=width,
)
def _draw_striped_meter(self, draw: ImageDraw.Draw):
"""Draw a striped meter"""
meter_value = self._meter_value()
x1 = y1 = self._metersize * M - 20
width = self._meterthickness * M
if self._wedgesize > 0:
draw.arc(
xy=(0, 0, x1, y1),
start=meter_value - self._wedgesize,
end=meter_value + self._wedgesize,
fill=self._meterforeground,
width=width,
)
else:
_from = self._arcoffset
_to = meter_value - 1
_step = self._stripethickness
for x in range(_from, _to, _step):
draw.arc(
xy=(0, 0, x1, y1),
start=x,
end=x + self._stripethickness - 1,
fill=self._meterforeground,
width=width,
)
def _meter_value(self) -> int:
"""Calculate the value to be used to draw the arc length of the
progress meter."""
value = int(
(self["amountused"] / self["amounttotal"]) * self._arcrange
+ self._arcoffset
)
return value
def _on_theme_change(self, *_):
self._draw_base_image()
self._draw_meter()
def _on_dial_interact(self, e: tk.Event):
"""Callback for mouse drag motion on meter indicator"""
dx = e.x - self._metersize // 2
dy = e.y - self._metersize // 2
rads = math.atan2(dy, dx)
degs = math.degrees(rads)
if degs > self._arcoffset:
factor = degs - self._arcoffset
else:
factor = 360 + degs - self._arcoffset
# clamp the value between 0 and `amounttotal`
amounttotal = self.amounttotalvar.get()
lastused = self.amountusedvar.get()
amountused = (amounttotal / self._arcrange * factor)
# calculate amount used given stepsize
if amountused > self._stepsize//2:
amountused = amountused // self._stepsize * self._stepsize + self._stepsize
else:
amountused = 0
# if the number is the name, then do not redraw
if lastused == amountused:
return
# set the amount used variable
if amountused < 0:
self.amountusedvar.set(0)
elif amountused > amounttotal:
self.amountusedvar.set(amounttotal)
else:
self.amountusedvar.set(amountused)
def _lookup_style_option(self, style: str, option: str):
"""Wrapper around the tcl style lookup command"""
value = self.tk.call(
"ttk::style", "lookup", style, "-%s" % option, None, None
)
return value
def _configure_get(self, cnf):
"""Override the configuration get method"""
if cnf == "arcrange":
return self._arcrange
elif cnf == "arcoffset":
return self._arcoffset
elif cnf == "amounttotal":
return self.amounttotalvar.get()
elif cnf == "amountused":
return self.amountusedvar.get()
elif cnf == "interactive":
return self._interactive
elif cnf == "subtextfont":
return self._subtextfont
elif cnf == "subtextstyle":
return self._subtextstyle
elif cnf == "subtext":
return self._subtext
elif cnf == "metersize":
return self._metersize
elif cnf == "bootstyle":
return self._bootstyle
elif cnf == "metertype":
return self._metertype
elif cnf == "meterthickness":
return self._meterthickness
elif cnf == "showtext":
return self._showtext
elif cnf == "stripethickness":
return self._stripethickness
elif cnf == "textleft":
return self._textleft
elif cnf == "textright":
return self._textright
elif cnf == "textfont":
return self._textfont
elif cnf == "wedgesize":
return self._wedgesize
elif cnf == "stepsize":
return self._stepsize
else:
return super(ttk.Frame, self).configure(cnf)
def _configure_set(self, **kwargs):
"""Override the configuration set method"""
meter_text_changed = False
if "arcrange" in kwargs:
self._arcrange = kwargs.pop("arcrange")
if "arcoffset" in kwargs:
self._arcoffset = kwargs.pop("arcoffset")
if "amounttotal" in kwargs:
amounttotal = kwargs.pop("amounttotal")
self.amounttotalvar.set(amounttotal)
if "amountused" in kwargs:
amountused = kwargs.pop("amountused")
self.amountusedvar.set(amountused)
if "interactive" in kwargs:
self._interactive = kwargs.pop("interactive")
self._set_interactive_bind()
if "subtextfont" in kwargs:
self._subtextfont = kwargs.pop("subtextfont")
self.subtext.configure(font=self._subtextfont)
self.textleft.configure(font=self._subtextfont)
self.textright.configure(font=self._subtextfont)
if "subtextstyle" in kwargs:
self._subtextstyle = kwargs.pop("subtextstyle")
self.subtext.configure(bootstyle=[self._subtextstyle, "meter"])
if "metersize" in kwargs:
self._metersize = utility.scale_size(kwargs.pop("metersize"))
self.meterframe.configure(
height=self._metersize, width=self._metersize
)
if "bootstyle" in kwargs:
self._bootstyle = kwargs.pop("bootstyle")
self.textcenter.configure(bootstyle=[self._bootstyle, "meter"])
if "metertype" in kwargs:
self._metertype = kwargs.pop("metertype")
if "meterthickness" in kwargs:
self._meterthickness = self.scale_size(
kwargs.pop("meterthickness")
)
if "stripethickness" in kwargs:
self._stripethickness = kwargs.pop("stripethickness")
if "subtext" in kwargs:
self._subtext = kwargs.pop("subtext")
self.subtext.configure(text=self._subtext)
meter_text_changed = True
if "textleft" in kwargs:
self._textleft = kwargs.pop("textleft")
self.textleft.configure(text=self._textleft)
meter_text_changed = True
if "textright" in kwargs:
self._textright = kwargs.pop("textright")
meter_text_changed = True
if "showtext" in kwargs:
self._showtext = kwargs.pop("showtext")
meter_text_changed = True
if "textfont" in kwargs:
self._textfont = kwargs.pop("textfont")
self.textcenter.configure(font=self._textfont)
if "wedgesize" in kwargs:
self._wedgesize = kwargs.pop("wedgesize")
if "stepsize" in kwargs:
self._stepsize = kwargs.pop("stepsize")
if meter_text_changed:
self._set_meter_text()
try:
if self._metertype:
self._set_arc_offset_range(
metertype=self._metertype,
arcoffset=self._arcoffset,
arcrange=self._arcrange,
)
except AttributeError:
return
self._draw_base_image()
self._draw_meter()
# pass remaining configurations to `ttk.Frame.configure`
super(ttk.Frame, self).configure(**kwargs)
def __getitem__(self, key: str):
return self._configure_get(key)
def __setitem__(self, key: str, value) -> None:
self._configure_set(**{key: value})
def configure(self, cnf=None, **kwargs):
"""Configure the options for this widget.
Parameters:
cnf (Dict[str, Any], optional):
A dictionary of configuration options.
**kwargs: Optional keyword arguments.
"""
if cnf is not None:
return self._configure_get(cnf)
else:
self._configure_set(**kwargs)
def step(self, delta=1):
"""Increase the indicator value by `delta`
The indicator will reverse direction and count down once it
reaches the maximum value.
Parameters:
delta (int):
The amount to change the indicator.
"""
amountused = self.amountusedvar.get()
amounttotal = self.amounttotalvar.get()
if amountused >= amounttotal:
self._towardsmaximum = True
self.amountusedvar.set(amountused - delta)
elif amountused <= 0:
self._towardsmaximum = False
self.amountusedvar.set(amountused + delta)
elif self._towardsmaximum:
self.amountusedvar.set(amountused - delta)
else:
self.amountusedvar.set(amountused + delta)