pep-mklive/pylibraries/ttkbootstrap/style.py

5180 lines
168 KiB
Python
Raw Normal View History

2024-10-27 15:16:46 -01:00
import json
import re
import colorsys
import tkinter as tk
from tkinter import font
from math import ceil
from tkinter import TclError, ttk
from typing import Any, Callable
from PIL import ImageTk, ImageDraw, Image, ImageFont
from ttkbootstrap.constants import *
from ttkbootstrap.themes.standard import STANDARD_THEMES
from ttkbootstrap.publisher import Publisher, Channel
from ttkbootstrap import utility as util
from ttkbootstrap import colorutils
from PIL import ImageColor
try:
# prevent app from failing if user.py gets corrupted
from ttkbootstrap.themes.user import USER_THEMES
except (ImportError, ModuleNotFoundError):
USER_THEMES = {}
class Colors:
"""A class that defines the color scheme for a theme as well as
provides several static methods for manipulating colors.
A `Colors` object is attached to a `ThemeDefinition` and can also
be accessed through the `Style.colors` property for the
current theme.
Examples:
```python
style = Style()
# dot-notation
style.colors.primary
# get method
style.colors.get('primary')
```
This class is an iterator, so you can iterate over the main
style color labels (primary, secondary, success, info, warning,
danger):
```python
for color_label in style.colors:
color = style.colors.get(color_label)
print(color_label, color)
```
If, for some reason, you need to iterate over all theme color
labels, then you can use the `Colors.label_iter` method. This
will include all theme colors.
```python
for color_label in style.colors.label_iter():
color = Colors.get(color_label)
print(color_label, color)
```
If you want to adjust the hsv values of an existing color by a
specific percentage (delta), you can use the `Colors.update_hsv`
method, which is static. In the example below, the "value delta"
or `vd` is increased by 15%, which will lighten the color:
```python
Colors.update_hsv("#9954bb", vd=0.15)
```
"""
def __init__(
self,
primary,
secondary,
success,
info,
warning,
danger,
light,
dark,
bg,
fg,
selectbg,
selectfg,
border,
inputfg,
inputbg,
active,
):
"""
Parameters:
primary (str):
The primary theme color; used by default for all widgets.
secondary (str):
An accent color; commonly of a `grey` hue.
success (str):
An accent color; commonly of a `green` hue.
info (str):
An accent color; commonly of a `blue` hue.
warning (str):
An accent color; commonly of an `orange` hue.
danger (str):
An accent color; commonly of a `red` hue.
light (str):
An accent color.
dark (str):
An accent color.
bg (str):
Background color.
fg (str):
Default text color.
selectfg (str):
The color of selected text.
selectbg (str):
The background color of selected text.
border (str):
The color used for widget borders.
inputfg (str):
The text color for input widgets.
inputbg (str):
The text background color for input widgets.
active (str):
An accent color.
"""
self.primary = primary
self.secondary = secondary
self.success = success
self.info = info
self.warning = warning
self.danger = danger
self.light = light
self.dark = dark
self.bg = bg
self.fg = fg
self.selectbg = selectbg
self.selectfg = selectfg
self.border = border
self.inputfg = inputfg
self.inputbg = inputbg
self.active = active
@staticmethod
def make_transparent(alpha, foreground, background='#ffffff'):
"""Simulate color transparency.
Parameters:
alpha (float):
The amount of transparency; a number between 0 and 1.
foreground (str):
The foreground color.
background (str):
The background color.
Returns:
str:
A hexadecimal color representing the "transparent"
version of the foreground color against the background
color.
"""
fg = ImageColor.getrgb(foreground)
bg = ImageColor.getrgb(background)
rgb_float = [alpha * c1 + (1 - alpha) * c2 for (c1, c2) in zip(fg, bg)]
rgb_int = [int(x) for x in rgb_float]
return '#{:02x}{:02x}{:02x}'.format(*rgb_int)
@staticmethod
def rgb_to_hsv(r, g, b):
"""Convert an rgb to hsv color value.
Parameters:
r (float):
red
g (float):
green
b (float):
blue
Returns:
Tuple[float, float, float]: The hsv color value.
"""
return colorsys.rgb_to_hsv(r, g, b)
def get_foreground(self, color_label):
"""Return the appropriate foreground color for the specified
color_label.
Parameters:
color_label (str):
A color label corresponding to a class property
"""
if color_label == LIGHT:
return self.dark
elif color_label == DARK:
return self.light
else:
return self.selectfg
def get(self, color_label: str):
"""Lookup a color value from the color name
Parameters:
color_label (str):
A color label corresponding to a class propery
Returns:
str:
A hexadecimal color value.
"""
return self.__dict__.get(color_label)
def set(self, color_label: str, color_value: str):
"""Set a color property value. This does not update any existing
widgets. Can also be used to create on-demand color properties
that can be used in your program after creation.
Parameters:
color_label (str):
The name of the color to be set (key)
color_value (str):
A hexadecimal color value
"""
self.__dict__[color_label] = color_value
def __iter__(self):
return iter(
[
"primary",
"secondary",
"success",
"info",
"warning",
"danger",
"light",
"dark",
]
)
def __repr__(self):
out = tuple(zip(self.__dict__.keys(), self.__dict__.values()))
return str(out)
@staticmethod
def label_iter():
"""Iterate over all color label properties in the Color class
Returns:
iter:
An iterator for color label names
"""
return iter(
[
"primary",
"secondary",
"success",
"info",
"warning",
"danger",
"light",
"dark",
"bg",
"fg",
"selectbg",
"selectfg",
"border",
"inputfg",
"inputbg",
"active",
]
)
@staticmethod
def hex_to_rgb(color: str):
"""Convert hexadecimal color to rgb color value
Parameters:
color (str):
A hexadecimal color value
Returns:
tuple[int, int, int]:
An rgb color value.
"""
r, g, b = colorutils.color_to_rgb(color)
return r/255, g/255, b/255
@staticmethod
def rgb_to_hex(r: int, g: int, b: int):
"""Convert rgb to hexadecimal color value
Parameters:
r (int):
red
g (int):
green
b (int):
blue
Returns:
str:
A hexadecimal color value
"""
r_ = int(r * 255)
g_ = int(g * 255)
b_ = int(b * 255)
return colorutils.color_to_hex((r_, g_, b_))
@staticmethod
def update_hsv(color, hd=0, sd=0, vd=0):
"""Modify the hue, saturation, and/or value of a given hex
color value by specifying the _delta_.
Parameters:
color (str):
A hexadecimal color value to adjust.
hd (float):
% change in hue, _hue delta_.
sd (float):
% change in saturation, _saturation delta_.
vd (float):
% change in value, _value delta_.
Returns:
str:
The resulting hexadecimal color value
"""
r, g, b = Colors.hex_to_rgb(color)
h, s, v = colorsys.rgb_to_hsv(r, g, b)
# hue
if h * (1 + hd) > 1:
h = 1
elif h * (1 + hd) < 0:
h = 0
else:
h *= 1 + hd
# saturation
if s * (1 + sd) > 1:
s = 1
elif s * (1 + sd) < 0:
s = 0
else:
s *= 1 + sd
# value
if v * (1 + vd) > 1:
v = 0.95
elif v * (1 + vd) < 0.05:
v = 0.05
else:
v *= 1 + vd
r, g, b = colorsys.hsv_to_rgb(h, s, v)
return Colors.rgb_to_hex(r, g, b)
class ThemeDefinition:
"""A class to provide defined name, colors, and font settings for a
ttkbootstrap theme."""
def __init__(self, name, colors, themetype=LIGHT):
"""
Parameters:
name (str):
The name of the theme.
colors (Colors):
An object that defines the color scheme for a theme.
themetype (str):
Specifies whether the theme is **light** or **dark**.
"""
self.name = name
self.colors = Colors(**colors)
self.type = themetype
def __repr__(self):
return " ".join(
[
f"name={self.name},",
f"type={self.type},",
f"colors={self.colors}",
]
)
class Style(ttk.Style):
"""A singleton class for creating and managing the application
theme and widget styles.
This class is meant to be a drop-in replacement for `ttk.Style` and
inherits all of it's methods and properties. However, in
ttkbootstrap, this class is implemented as a singleton. Subclassing
is not recommended and may have unintended consequences.
Examples:
```python
# instantiate the style with default theme
style = Style()
# instantiate the style with another theme
style = Style(theme='superhero')
# check all available themes
for theme in style.theme_names():
print(theme)
```
See the [Python documentation](https://docs.python.org/3/library/tkinter.ttk.html#tkinter.ttk.Style)
on this class for more details.
"""
instance = None
def __new__(cls, theme=None):
if Style.instance is None:
return object.__new__(cls)
else:
return Style.instance
def __init__(self, theme=DEFAULT_THEME):
"""
Parameters:
theme (str):
The name of the theme to use when styling the widget.
"""
if Style.instance is not None:
if theme != DEFAULT_THEME:
Style.instance.theme_use(theme)
return
self._theme_objects = {}
self._theme_definitions = {}
self._style_registry = set() # all styles used
self._theme_styles = {} # styles used in theme
self._theme_names = set()
self._load_themes()
super().__init__()
Style.instance = self
self.theme_use(theme)
# apply localization
from ttkbootstrap import localization
localization.initialize_localities()
@property
def colors(self):
"""An object that contains the colors used for the current
theme.
Returns:
Colors:
The colors object for the current theme.
"""
theme = self.theme.name
if theme in list(self._theme_names):
definition = self._theme_definitions.get(theme)
if not definition:
return [] # TODO refactor this
else:
return definition.colors
else:
return [] # TODO refactor this
def configure(self, style, query_opt: Any = None, **kw):
if query_opt:
return super().configure(style, query_opt=query_opt, **kw)
if not self.style_exists_in_theme(style):
ttkstyle = Bootstyle.update_ttk_widget_style(None, style)
else:
ttkstyle = style
if ttkstyle == style:
# configure an existing ttkbootrap theme
return super().configure(style, query_opt=query_opt, **kw)
else:
# subclass a ttkbootstrap theme
result = super().configure(style, query_opt=query_opt, **kw)
self._register_ttkstyle(style)
return result
def theme_names(self):
"""Return a list of all ttkbootstrap themes.
Returns:
List[str, ...]:
A list of theme names.
"""
return list(self._theme_definitions.keys())
def register_theme(self, definition):
"""Register a theme definition for use by the `Style`
object. This makes the definition and name available at
run-time so that the assets and styles can be created when
needed.
Parameters:
definition (ThemeDefinition):
A `ThemeDefinition` object.
"""
theme = definition.name
self._theme_names.add(theme)
self._theme_definitions[theme] = definition
self._theme_styles[theme] = set()
def theme_use(self, themename=None):
"""Changes the theme used in rendering the application widgets.
If themename is None, returns the theme in use, otherwise, set
the current theme to themename, refreshes all widgets and emits
a ``<<ThemeChanged>>`` event.
Only use this method if you are changing the theme *during*
runtime. Otherwise, pass the theme name into the Style
constructor to instantiate the style with a theme.
Parameters:
themename (str):
The name of the theme to apply when creating new widgets
Returns:
Union[str, None]:
The name of the current theme if `themename` is None
otherwise, `None`.
"""
if not themename:
# return current theme
return super().theme_use()
# change to an existing theme
existing_themes = super().theme_names()
if themename in existing_themes:
self.theme = self._theme_definitions.get(themename)
super().theme_use(themename)
self._create_ttk_styles_on_theme_change()
Publisher.publish_message(Channel.STD)
# setup a new theme
elif themename in self._theme_names:
self.theme = self._theme_definitions.get(themename)
self._theme_objects[themename] = StyleBuilderTTK()
self._create_ttk_styles_on_theme_change()
Publisher.publish_message(Channel.STD)
else:
raise TclError(themename, "is not a valid theme.")
def style_exists_in_theme(self, ttkstyle: str):
"""Check if a style exists in the current theme.
Parameters:
ttkstyle (str):
The ttk style to check.
Returns:
bool:
`True` if the style exists, otherwise `False`.
"""
theme_styles = self._theme_styles.get(self.theme.name)
exists_in_theme = ttkstyle in theme_styles
exists_in_registry = ttkstyle in self._style_registry
return exists_in_theme and exists_in_registry
@staticmethod
def get_instance():
"""Returns and instance of the style class"""
return Style.instance
@staticmethod
def _get_builder():
"""Get the object that builds the widget styles for the current
theme.
Returns:
ThemeBuilderTTK:
The theme builder object that builds the ttk styles for
the current theme.
"""
style: Style = Style.get_instance()
theme_name = style.theme.name
return style._theme_objects[theme_name]
@staticmethod
def _get_builder_tk():
"""Get the object that builds the widget styles for the current
theme.
Returns:
ThemeBuilderTK:
The theme builder object that builds the ttk styles for
the current theme.
"""
builder = Style._get_builder()
return builder.builder_tk
def _build_configure(self, style, **kw):
"""Calls configure of superclass; used by style builder classes."""
super().configure(style, **kw)
def _load_themes(self):
"""Load all ttkbootstrap defined themes"""
# create a theme definition object for each theme, this will be
# used to generate the theme in tkinter along with any assets
# at run-time
if USER_THEMES:
STANDARD_THEMES.update(USER_THEMES)
theme_settings = {"themes": STANDARD_THEMES}
for name, definition in theme_settings["themes"].items():
self.register_theme(
ThemeDefinition(
name=name,
themetype=definition["type"],
colors=definition["colors"],
)
)
def _register_ttkstyle(self, ttkstyle):
"""Register that a ttk style name. This ensures that the
builder will not attempt to build a style that has already
been created.
Parameters:
ttkstyle (str):
The name of the ttk style to register.
"""
self._style_registry.add(ttkstyle)
theme = self.theme.name
self._theme_styles[theme].add(ttkstyle)
def _create_ttk_styles_on_theme_change(self):
"""Create existing styles when the theme changes"""
for ttkstyle in self._style_registry:
if not self.style_exists_in_theme(ttkstyle):
color = Bootstyle.ttkstyle_widget_color(ttkstyle)
method_name = Bootstyle.ttkstyle_method_name(string=ttkstyle)
builder: StyleBuilderTTK = self._get_builder()
method: Callable = builder.name_to_method(method_name)
method(builder, color)
def load_user_themes(self, file):
"""Load user themes saved in json format"""
with open(file, encoding='utf-8') as f:
data = json.load(f)
themes = data['themes']
for theme in themes:
for name, definition in theme.items():
self.register_theme(
ThemeDefinition(
name=name,
themetype=definition["type"],
colors=definition["colors"],
)
)
class StyleBuilderTK:
"""A class for styling legacy tkinter widgets (not ttk).
The methods in this classed are used internally to update tk widget
style configurations and are not intended to be called by the end
user.
All legacy tkinter widgets are updated with a callback whenever the
theme is changed. The color configuration of the widget is updated
to match the current theme. Legacy ttk widgets are not the primary
focus of this library, however, an attempt was made to make sure they
did not stick out amongst ttk widgets if used.
Some ttk widgets contain legacy components that must be updated
such as the Combobox popdown, so this ensures they are styled
completely to match the current theme.
"""
def __init__(self):
self.style = Style.get_instance()
self.master = self.style.master
@property
def theme(self) -> ThemeDefinition:
"""A reference to the `ThemeDefinition` object for the current
theme."""
return self.style.theme
@property
def colors(self) -> Colors:
"""A reference to the `Colors` object for the current theme."""
return self.style.colors
@property
def is_light_theme(self) -> bool:
"""Returns `True` if the theme is _light_, otherwise `False`."""
return self.style.theme.type == LIGHT
def update_tk_style(self, widget: tk.Tk):
"""Update the window style.
Parameters:
widget (tkinter.Tk):
The tk object to update.
"""
widget.configure(background=self.colors.bg)
# add default initial font for text widget
widget.option_add('*Text*Font', 'TkDefaultFont')
def update_toplevel_style(self, widget: tk.Toplevel):
"""Update the toplevel style.
Parameters:
widget (tkinter.Toplevel):
The toplevel object to update.
"""
widget.configure(background=self.colors.bg)
def update_canvas_style(self, widget: tk.Canvas):
"""Update the canvas style.
Parameters:
widget (tkinter.Canvas):
The canvas object to update.
"""
# if self.is_light_theme:
# bordercolor = self.colors.border
# else:
# bordercolor = self.colors.selectbg
widget.configure(
background=self.colors.bg,
highlightthickness=0,
# highlightbackground=bordercolor,
)
def update_button_style(self, widget: tk.Button):
"""Update the button style.
Parameters:
widget (tkinter.Button):
The button object to update.
"""
background = self.colors.primary
foreground = self.colors.selectfg
activebackground = Colors.update_hsv(self.colors.primary, vd=-0.1)
widget.configure(
background=background,
foreground=foreground,
relief=tk.FLAT,
borderwidth=0,
activebackground=activebackground,
highlightbackground=self.colors.selectfg,
)
def update_label_style(self, widget: tk.Label):
"""Update the label style.
Parameters:
widget (tkinter.Label):
The label object to update.
"""
widget.configure(foreground=self.colors.fg, background=self.colors.bg)
def update_frame_style(self, widget: tk.Frame):
"""Update the frame style.
Parameters:
widget (tkinter.Frame):
The frame object to update.
"""
widget.configure(background=self.colors.bg)
def update_checkbutton_style(self, widget: tk.Checkbutton):
"""Update the checkbutton style.
Parameters:
widget (tkinter.Checkbutton):
The checkbutton object to update.
"""
widget.configure(
activebackground=self.colors.bg,
activeforeground=self.colors.primary,
background=self.colors.bg,
foreground=self.colors.fg,
selectcolor=self.colors.bg,
)
def update_radiobutton_style(self, widget: tk.Radiobutton):
"""Update the radiobutton style.
Parameters:
widget (tkinter.Radiobutton):
The radiobutton object to update.
"""
widget.configure(
activebackground=self.colors.bg,
activeforeground=self.colors.primary,
background=self.colors.bg,
foreground=self.colors.fg,
selectcolor=self.colors.bg,
)
def update_entry_style(self, widget: tk.Entry):
"""Update the entry style.
Parameters:
widget (tkinter.Entry):
The entry object to update.
"""
if self.is_light_theme:
bordercolor = self.colors.border
else:
bordercolor = self.colors.selectbg
widget.configure(
relief=tk.FLAT,
highlightthickness=1,
foreground=self.colors.inputfg,
highlightbackground=bordercolor,
highlightcolor=self.colors.primary,
background=self.colors.inputbg,
insertbackground=self.colors.inputfg,
insertwidth=1,
)
def update_scale_style(self, widget: tk.Scale):
"""Update the scale style.
Parameters:
widget (tkinter.scale):
The scale object to update.
"""
if self.is_light_theme:
bordercolor = self.colors.border
else:
bordercolor = self.colors.selectbg
activecolor = Colors.update_hsv(self.colors.primary, vd=-0.2)
widget.configure(
background=self.colors.primary,
showvalue=False,
sliderrelief=tk.FLAT,
borderwidth=0,
activebackground=activecolor,
highlightthickness=1,
highlightcolor=bordercolor,
highlightbackground=bordercolor,
troughcolor=self.colors.inputbg,
)
def update_spinbox_style(self, widget: tk.Spinbox):
"""Update the spinbox style.
Parameters:
widget (tkinter.Spinbox):
THe spinbox object to update.
"""
if self.is_light_theme:
bordercolor = self.colors.border
else:
bordercolor = self.colors.selectbg
widget.configure(
relief=tk.FLAT,
highlightthickness=1,
foreground=self.colors.inputfg,
highlightbackground=bordercolor,
highlightcolor=self.colors.primary,
background=self.colors.inputbg,
buttonbackground=self.colors.inputbg,
insertbackground=self.colors.inputfg,
insertwidth=1,
# these options should work, but do not have any affect
buttonuprelief=tk.FLAT,
buttondownrelief=tk.SUNKEN,
)
def update_listbox_style(self, widget: tk.Listbox):
"""Update the listbox style.
Parameters:
widget (tkinter.Listbox):
The listbox object to update.
"""
if self.is_light_theme:
bordercolor = self.colors.border
else:
bordercolor = self.colors.selectbg
widget.configure(
foreground=self.colors.inputfg,
background=self.colors.inputbg,
selectbackground=self.colors.selectbg,
selectforeground=self.colors.selectfg,
highlightcolor=self.colors.primary,
highlightbackground=bordercolor,
highlightthickness=1,
activestyle="none",
relief=tk.FLAT,
)
def update_menubutton_style(self, widget: tk.Menubutton):
"""Update the menubutton style.
Parameters:
widget (tkinter.Menubutton):
The menubutton object to update.
"""
activebackground = Colors.update_hsv(self.colors.primary, vd=-0.2)
widget.configure(
background=self.colors.primary,
foreground=self.colors.selectfg,
activebackground=activebackground,
activeforeground=self.colors.selectfg,
borderwidth=0,
)
def update_menu_style(self, widget: tk.Menu):
"""Update the menu style.
Parameters:
widget (tkinter.Menu):
The menu object to update.
"""
widget.configure(
tearoff=False,
activebackground=self.colors.selectbg,
activeforeground=self.colors.selectfg,
foreground=self.colors.fg,
selectcolor=self.colors.primary,
background=self.colors.bg,
relief=tk.FLAT,
borderwidth=0,
)
def update_labelframe_style(self, widget: tk.LabelFrame):
"""Update the labelframe style.
Parameters:
widget (tkinter.LabelFrame):
The labelframe object to update.
"""
if self.is_light_theme:
bordercolor = self.colors.border
else:
bordercolor = self.colors.selectbg
widget.configure(
highlightcolor=bordercolor,
foreground=self.colors.fg,
borderwidth=1,
highlightthickness=0,
background=self.colors.bg,
)
def update_text_style(self, widget: tk.Text):
"""Update the text style.
Parameters:
widget (tkinter.Text):
The text object to update.
"""
if self.is_light_theme:
bordercolor = self.colors.border
else:
bordercolor = self.colors.selectbg
focuscolor = widget.cget("highlightbackground")
if focuscolor in ["SystemButtonFace", bordercolor]:
focuscolor = bordercolor
widget.configure(
background=self.colors.inputbg,
foreground=self.colors.inputfg,
highlightcolor=focuscolor,
highlightbackground=bordercolor,
insertbackground=self.colors.inputfg,
selectbackground=self.colors.selectbg,
selectforeground=self.colors.selectfg,
insertwidth=1,
highlightthickness=1,
relief=tk.FLAT,
padx=5,
pady=5,
#font="TkDefaultFont",
)
class StyleBuilderTTK:
"""A class containing methods for building new ttk widget styles on
demand.
The methods in this classed are used internally to generate ttk
widget styles on-demand and are not intended to be called by the end
user.
"""
def __init__(self):
self.style: Style = Style.get_instance()
self.theme_images = {}
self.builder_tk = StyleBuilderTK()
self.create_theme()
@staticmethod
def name_to_method(method_name):
"""Get a method by name.
Parameters:
method_name (str):
The name of the style builder method.
Returns:
Callable:
The method that is named by `method_name`
"""
func = getattr(StyleBuilderTTK, method_name)
return func
@property
def colors(self) -> Colors:
"""A reference to the `Colors` object of the current theme."""
return self.style.theme.colors
@property
def theme(self) -> ThemeDefinition:
"""A reference to the `ThemeDefinition` object for the current
theme."""
return self.style.theme
@property
def is_light_theme(self) -> bool:
"""If the current theme is _light_, returns `True`, otherwise
returns `False`."""
return self.style.theme.type == LIGHT
def scale_size(self, size):
"""Scale the size of images and other assets based on the
scaling factor of ttk to ensure that the image matches the
screen resolution.
Parameters:
size (Union[int, List, Tuple]):
A single integer or an iterable of integers
"""
winsys = self.style.master.tk.call("tk", "windowingsystem")
if winsys == "aqua":
BASELINE = 1.000492368291482
else:
BASELINE = 1.33398982438864281
scaling = self.style.master.tk.call("tk", "scaling")
factor = scaling / BASELINE
if isinstance(size, int) or isinstance(size, float):
return ceil(size * factor)
elif isinstance(size, tuple) or isinstance(size, list):
return [ceil(x * factor) for x in size]
def create_theme(self):
"""Create and style a new ttk theme. A wrapper around internal
style methods.
"""
self.style.theme_create(self.theme.name, TTK_CLAM)
ttk.Style.theme_use(self.style, self.theme.name)
self.update_ttk_theme_settings()
def update_ttk_theme_settings(self):
"""This method is called internally every time the theme is
changed to update various components included in the body of
the method."""
self.create_default_style()
def create_default_style(self):
"""Setup the default widget style configuration for the root
ttk style "."; these defaults are applied to any widget that
contains the configuration options updated by this style. This
method should be called *first* before any other style is applied
during theme creation.
"""
self.style._build_configure(
style=".",
background=self.colors.bg,
darkcolor=self.colors.border,
foreground=self.colors.fg,
troughcolor=self.colors.bg,
selectbg=self.colors.selectbg,
selectfg=self.colors.selectfg,
selectforeground=self.colors.selectfg,
selectbackground=self.colors.selectbg,
fieldbg="white",
borderwidth=1,
focuscolor="",
)
# this is general style applied to the tableview
self.create_link_button_style()
self.style.configure("symbol.Link.TButton", font="-size 16")
def create_combobox_style(self, colorname=DEFAULT):
"""Create a style for the ttk.Combobox widget.
Parameters:
colorname (str):
The color label to use as the primary widget color.
"""
STYLE = "TCombobox"
if self.is_light_theme:
disabled_fg = self.colors.border
bordercolor = self.colors.border
readonly = self.colors.light
else:
disabled_fg = self.colors.selectbg
bordercolor = self.colors.selectbg
readonly = bordercolor
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
element = f"{ttkstyle.replace('TC','C')}"
focuscolor = self.colors.primary
else:
ttkstyle = f"{colorname}.{STYLE}"
element = f"{ttkstyle.replace('TC','C')}"
focuscolor = self.colors.get(colorname)
self.style.element_create(f"{element}.downarrow", "from", TTK_DEFAULT)
self.style.element_create(f"{element}.padding", "from", TTK_CLAM)
self.style.element_create(f"{element}.textarea", "from", TTK_CLAM)
if all([colorname, colorname != DEFAULT]):
bordercolor = focuscolor
self.style._build_configure(
ttkstyle,
bordercolor=bordercolor,
darkcolor=self.colors.inputbg,
lightcolor=self.colors.inputbg,
arrowcolor=self.colors.inputfg,
foreground=self.colors.inputfg,
fieldbackground=self.colors.inputbg,
background=self.colors.inputbg,
insertcolor=self.colors.inputfg,
relief=tk.FLAT,
padding=5,
arrowsize=self.scale_size(12),
)
self.style.map(
ttkstyle,
background=[("readonly", readonly)],
fieldbackground=[("readonly", readonly)],
foreground=[("disabled", disabled_fg)],
bordercolor=[
("invalid", self.colors.danger),
("focus !disabled", focuscolor),
("hover !disabled", focuscolor),
],
lightcolor=[
("focus invalid", self.colors.danger),
("focus !disabled", focuscolor),
("pressed !disabled", focuscolor),
("readonly", readonly),
],
darkcolor=[
("focus invalid", self.colors.danger),
("focus !disabled", focuscolor),
("pressed !disabled", focuscolor),
("readonly", readonly),
],
arrowcolor=[
("disabled", disabled_fg),
("pressed !disabled", focuscolor),
("focus !disabled", focuscolor),
("hover !disabled", focuscolor),
],
)
self.style.layout(
ttkstyle,
[
(
"combo.Spinbox.field",
{
"side": tk.TOP,
"sticky": tk.EW,
"children": [
(
"Combobox.downarrow",
{"side": tk.RIGHT, "sticky": tk.NS},
),
(
"Combobox.padding",
{
"expand": "1",
"sticky": tk.NSEW,
"children": [
(
"Combobox.textarea",
{"sticky": tk.NSEW},
)
],
},
),
],
},
)
],
)
self.style._register_ttkstyle(ttkstyle)
def create_separator_style(self, colorname=DEFAULT):
"""Create a style for the ttk.Separator widget.
Parameters:
colorname (str):
The primary widget color.
"""
HSTYLE = "Horizontal.TSeparator"
VSTYLE = "Vertical.TSeparator"
hsize = [40, 1]
vsize = [1, 40]
# style colors
if self.is_light_theme:
default_color = self.colors.border
else:
default_color = self.colors.selectbg
if any([colorname == DEFAULT, colorname == ""]):
background = default_color
h_ttkstyle = HSTYLE
v_ttkstyle = VSTYLE
else:
background = self.colors.get(colorname)
h_ttkstyle = f"{colorname}.{HSTYLE}"
v_ttkstyle = f"{colorname}.{VSTYLE}"
# horizontal separator
h_element = h_ttkstyle.replace(".TS", ".S")
h_img = ImageTk.PhotoImage(Image.new("RGB", hsize, background))
h_name = util.get_image_name(h_img)
self.theme_images[h_name] = h_img
self.style.element_create(f"{h_element}.separator", "image", h_name)
self.style.layout(
h_ttkstyle, [(f"{h_element}.separator", {"sticky": tk.EW})]
)
# vertical separator
v_element = v_ttkstyle.replace(".TS", ".S")
v_img = ImageTk.PhotoImage(Image.new("RGB", vsize, background))
v_name = util.get_image_name(v_img)
self.theme_images[v_name] = v_img
self.style.element_create(f"{v_element}.separator", "image", v_name)
self.style.layout(
v_ttkstyle, [(f"{v_element}.separator", {"sticky": tk.NS})]
)
self.style._register_ttkstyle(h_ttkstyle)
self.style._register_ttkstyle(v_ttkstyle)
def create_striped_progressbar_assets(self, thickness, colorname=DEFAULT):
"""Create the striped progressbar image and return as a
`PhotoImage`
Parameters:
colorname (str):
The color label used to style the widget.
Returns:
Tuple[str]:
A list of photoimage names.
"""
if any([colorname == DEFAULT, colorname == ""]):
barcolor = self.colors.primary
else:
barcolor = self.colors.get(colorname)
# calculate value of the light color
brightness = Colors.rgb_to_hsv(*Colors.hex_to_rgb(barcolor))[2]
if brightness < 0.4:
value_delta = 0.3
elif brightness > 0.8:
value_delta = 0
else:
value_delta = 0.1
barcolor_light = Colors.update_hsv(barcolor, sd=-0.2, vd=value_delta)
# horizontal progressbar
img = Image.new("RGBA", (100, 100), barcolor_light)
draw = ImageDraw.Draw(img)
draw.polygon(
xy=[(0, 0), (48, 0), (100, 52), (100, 100)],
fill=barcolor,
)
draw.polygon(xy=[(0, 52), (48, 100), (0, 100)], fill=barcolor)
_resized = img.resize((thickness, thickness), Image.LANCZOS)
h_img = ImageTk.PhotoImage(_resized)
h_name = h_img._PhotoImage__photo.name
v_img = ImageTk.PhotoImage(_resized.rotate(90))
v_name = v_img._PhotoImage__photo.name
self.theme_images[h_name] = h_img
self.theme_images[v_name] = v_img
return h_name, v_name
def create_striped_progressbar_style(self, colorname=DEFAULT):
"""Create a striped style for the ttk.Progressbar widget.
Parameters:
colorname (str):
The primary widget color label.
"""
HSTYLE = "Striped.Horizontal.TProgressbar"
VSTYLE = "Striped.Vertical.TProgressbar"
thickness = self.scale_size(12)
if any([colorname == DEFAULT, colorname == ""]):
h_ttkstyle = HSTYLE
v_ttkstyle = VSTYLE
else:
h_ttkstyle = f"{colorname}.{HSTYLE}"
v_ttkstyle = f"{colorname}.{VSTYLE}"
if self.is_light_theme:
if colorname == LIGHT:
troughcolor = self.colors.bg
bordercolor = self.colors.light
else:
troughcolor = self.colors.light
bordercolor = troughcolor
else:
troughcolor = Colors.update_hsv(self.colors.selectbg, vd=-0.2)
bordercolor = troughcolor
# ( horizontal, vertical )
images = self.create_striped_progressbar_assets(thickness, colorname)
# horizontal progressbar
h_element = h_ttkstyle.replace(".TP", ".P")
self.style.element_create(
f"{h_element}.pbar",
"image",
images[0],
width=thickness,
sticky=tk.EW,
)
self.style.layout(
h_ttkstyle,
[
(
f"{h_element}.trough",
{
"sticky": tk.NSEW,
"children": [
(
f"{h_element}.pbar",
{"side": tk.LEFT, "sticky": tk.NS},
)
],
},
)
],
)
self.style._build_configure(
h_ttkstyle,
troughcolor=troughcolor,
thickness=thickness,
bordercolor=bordercolor,
borderwidth=1,
)
# vertical progressbar
v_element = v_ttkstyle.replace(".TP", ".P")
self.style.element_create(
f"{v_element}.pbar",
"image",
images[1],
width=thickness,
sticky=tk.NS,
)
self.style.layout(
v_ttkstyle,
[
(
f"{v_element}.trough",
{
"sticky": tk.NSEW,
"children": [
(
f"{v_element}.pbar",
{"side": tk.BOTTOM, "sticky": tk.EW},
)
],
},
)
],
)
self.style._build_configure(
v_ttkstyle,
troughcolor=troughcolor,
bordercolor=bordercolor,
thickness=thickness,
borderwidth=1,
)
self.style._register_ttkstyle(h_ttkstyle)
self.style._register_ttkstyle(v_ttkstyle)
def create_progressbar_style(self, colorname=DEFAULT):
"""Create a solid ttk style for the ttk.Progressbar widget.
Parameters:
colorname (str):
The primary widget color.
"""
H_STYLE = "Horizontal.TProgressbar"
V_STYLE = "Vertical.TProgressbar"
thickness = self.scale_size(10)
if self.is_light_theme:
if colorname == LIGHT:
troughcolor = self.colors.bg
bordercolor = self.colors.light
else:
troughcolor = self.colors.light
bordercolor = troughcolor
else:
troughcolor = Colors.update_hsv(self.colors.selectbg, vd=-0.2)
bordercolor = troughcolor
if any([colorname == DEFAULT, colorname == ""]):
background = self.colors.primary
h_ttkstyle = H_STYLE
v_ttkstyle = V_STYLE
else:
background = self.colors.get(colorname)
h_ttkstyle = f"{colorname}.{H_STYLE}"
v_ttkstyle = f"{colorname}.{V_STYLE}"
self.style._build_configure(
h_ttkstyle,
thickness=thickness,
borderwidth=1,
bordercolor=bordercolor,
lightcolor=self.colors.border,
pbarrelief=tk.FLAT,
troughcolor=troughcolor,
)
existing_elements = self.style.element_names()
self.style._build_configure(
v_ttkstyle,
thickness=thickness,
borderwidth=1,
bordercolor=bordercolor,
lightcolor=self.colors.border,
pbarrelief=tk.FLAT,
troughcolor=troughcolor,
)
existing_elements = self.style.element_names()
# horizontal progressbar
h_element = h_ttkstyle.replace(".TP", ".P")
trough_element = f"{h_element}.trough"
pbar_element = f"{h_element}.pbar"
if trough_element not in existing_elements:
self.style.element_create(trough_element, "from", TTK_CLAM)
self.style.element_create(pbar_element, "from", TTK_DEFAULT)
self.style.layout(
h_ttkstyle,
[
(
trough_element,
{
"sticky": "nswe",
"children": [
(pbar_element, {"side": "left", "sticky": "ns"})
],
},
)
],
)
self.style._build_configure(h_ttkstyle, background=background)
# vertical progressbar
v_element = v_ttkstyle.replace(".TP", ".P")
trough_element = f"{v_element}.trough"
pbar_element = f"{v_element}.pbar"
if trough_element not in existing_elements:
self.style.element_create(trough_element, "from", TTK_CLAM)
self.style.element_create(pbar_element, "from", TTK_DEFAULT)
self.style._build_configure(v_ttkstyle, background=background)
self.style.layout(
v_ttkstyle,
[
(
trough_element,
{
"sticky": "nswe",
"children": [
(pbar_element, {"side": "bottom", "sticky": "we"})
],
},
)
],
)
# register ttkstyles
self.style._register_ttkstyle(h_ttkstyle)
self.style._register_ttkstyle(v_ttkstyle)
def create_scale_assets(self, colorname=DEFAULT, size=14):
"""Create the assets used for the ttk.Scale widget.
The slider handle is automatically adjusted to fit the
screen resolution.
Parameters:
colorname (str):
The color label.
size (int):
The size diameter of the slider circle; default=16.
Returns:
Tuple[str]:
A tuple of PhotoImage names to be used in the image
layout when building the style.
"""
size = self.scale_size(size)
if self.is_light_theme:
disabled_color = self.colors.border
if colorname == LIGHT:
track_color = self.colors.bg
else:
track_color = self.colors.light
else:
disabled_color = self.colors.selectbg
track_color = Colors.update_hsv(self.colors.selectbg, vd=-0.2)
if any([colorname == DEFAULT, colorname == ""]):
normal_color = self.colors.primary
else:
normal_color = self.colors.get(colorname)
pressed_color = Colors.update_hsv(normal_color, vd=-0.1)
hover_color = Colors.update_hsv(normal_color, vd=0.1)
# normal state
_normal = Image.new("RGBA", (100, 100))
draw = ImageDraw.Draw(_normal)
draw.ellipse((0, 0, 95, 95), fill=normal_color)
normal_img = ImageTk.PhotoImage(
_normal.resize((size, size), Image.LANCZOS)
)
normal_name = util.get_image_name(normal_img)
self.theme_images[normal_name] = normal_img
# pressed state
_pressed = Image.new("RGBA", (100, 100))
draw = ImageDraw.Draw(_pressed)
draw.ellipse((0, 0, 95, 95), fill=pressed_color)
pressed_img = ImageTk.PhotoImage(
_pressed.resize((size, size), Image.LANCZOS)
)
pressed_name = util.get_image_name(pressed_img)
self.theme_images[pressed_name] = pressed_img
# hover state
_hover = Image.new("RGBA", (100, 100))
draw = ImageDraw.Draw(_hover)
draw.ellipse((0, 0, 95, 95), fill=hover_color)
hover_img = ImageTk.PhotoImage(
_hover.resize((size, size), Image.LANCZOS)
)
hover_name = util.get_image_name(hover_img)
self.theme_images[hover_name] = hover_img
# disabled state
_disabled = Image.new("RGBA", (100, 100))
draw = ImageDraw.Draw(_disabled)
draw.ellipse((0, 0, 95, 95), fill=disabled_color)
disabled_img = ImageTk.PhotoImage(
_disabled.resize((size, size), Image.LANCZOS)
)
disabled_name = util.get_image_name(disabled_img)
self.theme_images[disabled_name] = disabled_img
# vertical track
h_track_img = ImageTk.PhotoImage(
Image.new("RGB", self.scale_size((40, 5)), track_color)
)
h_track_name = util.get_image_name(h_track_img)
self.theme_images[h_track_name] = h_track_img
# horizontal track
v_track_img = ImageTk.PhotoImage(
Image.new("RGB", self.scale_size((5, 40)), track_color)
)
v_track_name = util.get_image_name(v_track_img)
self.theme_images[v_track_name] = v_track_img
return (
normal_name,
pressed_name,
hover_name,
disabled_name,
h_track_name,
v_track_name,
)
def create_scale_style(self, colorname=DEFAULT):
"""Create a style for the ttk.Scale widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TScale"
if any([colorname == DEFAULT, colorname == ""]):
h_ttkstyle = f"Horizontal.{STYLE}"
v_ttkstyle = f"Vertical.{STYLE}"
else:
h_ttkstyle = f"{colorname}.Horizontal.{STYLE}"
v_ttkstyle = f"{colorname}.Vertical.{STYLE}"
# ( normal, pressed, hover, disabled, htrack, vtrack )
images = self.create_scale_assets(colorname)
# horizontal scale
h_element = h_ttkstyle.replace(".TS", ".S")
self.style.element_create(
f"{h_element}.slider",
"image",
images[0],
("disabled", images[3]),
("pressed", images[1]),
("hover", images[2]),
)
self.style.element_create(f"{h_element}.track", "image", images[4])
self.style.layout(
h_ttkstyle,
[
(
f"{h_element}.focus",
{
"expand": "1",
"sticky": tk.NSEW,
"children": [
(f"{h_element}.track", {"sticky": tk.EW}),
(
f"{h_element}.slider",
{"side": tk.LEFT, "sticky": ""},
),
],
},
)
],
)
# vertical scale
v_element = v_ttkstyle.replace(".TS", ".S")
self.style.element_create(
f"{v_element}.slider",
"image",
images[0],
("disabled", images[3]),
("pressed", images[1]),
("hover", images[2]),
)
self.style.element_create(f"{v_element}.track", "image", images[5])
self.style.layout(
v_ttkstyle,
[
(
f"{v_element}.focus",
{
"expand": "1",
"sticky": tk.NSEW,
"children": [
(f"{v_element}.track", {"sticky": tk.NS}),
(
f"{v_element}.slider",
{"side": tk.TOP, "sticky": ""},
),
],
},
)
],
)
# register ttkstyles
self.style._register_ttkstyle(h_ttkstyle)
self.style._register_ttkstyle(v_ttkstyle)
def create_floodgauge_style(self, colorname=DEFAULT):
"""Create a ttk style for the ttkbootstrap.widgets.Floodgauge
widget. This is a custom widget style that uses components of
the progressbar and label.
Parameters:
colorname (str):
The color label used to style the widget.
"""
HSTYLE = "Horizontal.TFloodgauge"
VSTYLE = "Vertical.TFloodgauge"
FLOOD_FONT = "-size 14"
if any([colorname == DEFAULT, colorname == ""]):
h_ttkstyle = HSTYLE
v_ttkstyle = VSTYLE
background = self.colors.primary
else:
h_ttkstyle = f"{colorname}.{HSTYLE}"
v_ttkstyle = f"{colorname}.{VSTYLE}"
background = self.colors.get(colorname)
if colorname == LIGHT:
foreground = self.colors.fg
troughcolor = self.colors.bg
else:
troughcolor = Colors.update_hsv(background, sd=-0.3, vd=0.8)
foreground = self.colors.selectfg
# horizontal floodgauge
h_element = h_ttkstyle.replace(".TF", ".F")
self.style.element_create(f"{h_element}.trough", "from", TTK_CLAM)
self.style.element_create(f"{h_element}.pbar", "from", TTK_DEFAULT)
self.style.layout(
h_ttkstyle,
[
(
f"{h_element}.trough",
{
"children": [
(f"{h_element}.pbar", {"sticky": tk.NS}),
("Floodgauge.label", {"sticky": ""}),
],
"sticky": tk.NSEW,
},
)
],
)
self.style._build_configure(
h_ttkstyle,
thickness=50,
borderwidth=1,
bordercolor=background,
lightcolor=background,
pbarrelief=tk.FLAT,
troughcolor=troughcolor,
background=background,
foreground=foreground,
justify=tk.CENTER,
anchor=tk.CENTER,
font=FLOOD_FONT,
)
# vertical floodgauge
v_element = v_ttkstyle.replace(".TF", ".F")
self.style.element_create(f"{v_element}.trough", "from", TTK_CLAM)
self.style.element_create(f"{v_element}.pbar", "from", TTK_DEFAULT)
self.style.layout(
v_ttkstyle,
[
(
f"{v_element}.trough",
{
"children": [
(f"{v_element}.pbar", {"sticky": tk.EW}),
("Floodgauge.label", {"sticky": ""}),
],
"sticky": tk.NSEW,
},
)
],
)
self.style._build_configure(
v_ttkstyle,
thickness=50,
borderwidth=1,
bordercolor=background,
lightcolor=background,
pbarrelief=tk.FLAT,
troughcolor=troughcolor,
background=background,
foreground=foreground,
justify=tk.CENTER,
anchor=tk.CENTER,
font=FLOOD_FONT,
)
# register ttkstyles
self.style._register_ttkstyle(h_ttkstyle)
self.style._register_ttkstyle(v_ttkstyle)
def create_arrow_assets(self, arrowcolor, pressed, active):
"""Create arrow assets used for various widget buttons.
!!! note
This method is currently not being utilized.
Parameters:
arrowcolor (str):
The color value to use as the arrow fill color.
pressed (str):
The color value to use when the arrow is pressed.
active (str):
The color value to use when the arrow is active or
hovered.
"""
def draw_arrow(color: str):
img = Image.new("RGBA", (11, 11))
draw = ImageDraw.Draw(img)
size = self.scale_size([11, 11])
draw.line([2, 6, 2, 9], fill=color)
draw.line([3, 5, 3, 8], fill=color)
draw.line([4, 4, 4, 7], fill=color)
draw.line([5, 3, 5, 6], fill=color)
draw.line([6, 4, 6, 7], fill=color)
draw.line([7, 5, 7, 8], fill=color)
draw.line([8, 6, 8, 9], fill=color)
img = img.resize(size, Image.BICUBIC)
up_img = ImageTk.PhotoImage(img)
up_name = util.get_image_name(up_img)
self.theme_images[up_name] = up_img
down_img = ImageTk.PhotoImage(img.rotate(180))
down_name = util.get_image_name(down_img)
self.theme_images[down_name] = down_img
left_img = ImageTk.PhotoImage(img.rotate(90))
left_name = util.get_image_name(left_img)
self.theme_images[left_name] = left_img
right_img = ImageTk.PhotoImage(img.rotate(-90))
right_name = util.get_image_name(right_img)
self.theme_images[right_name] = right_img
return up_name, down_name, left_name, right_name
normal_names = draw_arrow(arrowcolor)
pressed_names = draw_arrow(pressed)
active_names = draw_arrow(active)
return normal_names, pressed_names, active_names
def create_round_scrollbar_assets(self, thumbcolor, pressed, active):
"""Create image assets to be used when building the round
scrollbar style.
Parameters:
thumbcolor (str):
The color value of the thumb in normal state.
pressed (str):
The color value to use when the thumb is pressed.
active (str):
The color value to use when the thumb is active or
hovered.
"""
vsize = self.scale_size([9, 28])
hsize = self.scale_size([28, 9])
def rounded_rect(size, fill):
x = size[0] * 10
y = size[1] * 10
img = Image.new("RGBA", (x, y))
draw = ImageDraw.Draw(img)
radius = min([x, y]) // 2
draw.rounded_rectangle([0, 0, x - 1, y - 1], radius, fill)
image = ImageTk.PhotoImage(img.resize(size, Image.BICUBIC))
name = util.get_image_name(image)
self.theme_images[name] = image
return name
# create images
h_normal_img = rounded_rect(hsize, thumbcolor)
h_pressed_img = rounded_rect(hsize, pressed)
h_active_img = rounded_rect(hsize, active)
v_normal_img = rounded_rect(vsize, thumbcolor)
v_pressed_img = rounded_rect(vsize, pressed)
v_active_img = rounded_rect(vsize, active)
return (
h_normal_img,
h_pressed_img,
h_active_img,
v_normal_img,
v_pressed_img,
v_active_img,
)
def create_round_scrollbar_style(self, colorname=DEFAULT):
"""Create a round style for the ttk.Scrollbar widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TScrollbar"
if any([colorname == DEFAULT, colorname == ""]):
h_ttkstyle = f"Round.Horizontal.{STYLE}"
v_ttkstyle = f"Round.Vertical.{STYLE}"
if self.is_light_theme:
background = self.colors.border
else:
background = self.colors.selectbg
else:
h_ttkstyle = f"{colorname}.Round.Horizontal.{STYLE}"
v_ttkstyle = f"{colorname}.Round.Vertical.{STYLE}"
background = self.colors.get(colorname)
if self.is_light_theme:
if colorname == LIGHT:
troughcolor = self.colors.bg
else:
troughcolor = self.colors.light
else:
troughcolor = Colors.update_hsv(self.colors.selectbg, vd=-0.2)
pressed = Colors.update_hsv(background, vd=-0.05)
active = Colors.update_hsv(background, vd=0.05)
scroll_images = self.create_round_scrollbar_assets(
background, pressed, active
)
# horizontal scrollbar
self.style._build_configure(
h_ttkstyle,
troughcolor=troughcolor,
darkcolor=troughcolor,
bordercolor=troughcolor,
lightcolor=troughcolor,
arrowcolor=background,
arrowsize=self.scale_size(11),
background=troughcolor,
relief=tk.FLAT,
borderwidth=0,
)
self.style.element_create(
f"{h_ttkstyle}.thumb",
"image",
scroll_images[0],
("pressed", scroll_images[1]),
("active", scroll_images[2]),
border=self.scale_size(9),
padding=0,
sticky=tk.EW,
)
self.style.layout(
h_ttkstyle,
[
(
"Horizontal.Scrollbar.trough",
{
"sticky": "we",
"children": [
(
"Horizontal.Scrollbar.leftarrow",
{"side": "left", "sticky": ""},
),
(
"Horizontal.Scrollbar.rightarrow",
{"side": "right", "sticky": ""},
),
(
f"{h_ttkstyle}.thumb",
{"expand": "1", "sticky": "nswe"},
),
],
},
)
],
)
self.style._build_configure(h_ttkstyle, arrowcolor=background)
self.style.map(
h_ttkstyle, arrowcolor=[("pressed", pressed), ("active", active)]
)
# vertical scrollbar
self.style._build_configure(
v_ttkstyle,
troughcolor=troughcolor,
darkcolor=troughcolor,
bordercolor=troughcolor,
lightcolor=troughcolor,
arrowcolor=background,
arrowsize=self.scale_size(11),
background=troughcolor,
relief=tk.FLAT,
)
self.style.element_create(
f"{v_ttkstyle}.thumb",
"image",
scroll_images[3],
("pressed", scroll_images[4]),
("active", scroll_images[5]),
border=self.scale_size(9),
padding=0,
sticky=tk.NS,
)
self.style.layout(
v_ttkstyle,
[
(
"Vertical.Scrollbar.trough",
{
"sticky": "ns",
"children": [
(
"Vertical.Scrollbar.uparrow",
{"side": "top", "sticky": ""},
),
(
"Vertical.Scrollbar.downarrow",
{"side": "bottom", "sticky": ""},
),
(
f"{v_ttkstyle}.thumb",
{"expand": "1", "sticky": "nswe"},
),
],
},
)
],
)
self.style._build_configure(v_ttkstyle, arrowcolor=background)
self.style.map(
v_ttkstyle, arrowcolor=[("pressed", pressed), ("active", active)]
)
# register ttkstyles
self.style._register_ttkstyle(h_ttkstyle)
self.style._register_ttkstyle(v_ttkstyle)
def create_scrollbar_assets(self, thumbcolor, pressed, active):
"""Create the image assets used to build the standard scrollbar
style.
Parameters:
thumbcolor (str):
The primary color value used to color the thumb.
pressed (str):
The color value to use when the thumb is pressed.
active (str):
The color value to use when the thumb is active or
hovered.
"""
vsize = self.scale_size([9, 28])
hsize = self.scale_size([28, 9])
def draw_rect(size, fill):
x = size[0] * 10
y = size[1] * 10
img = Image.new("RGBA", (x, y), fill)
image = ImageTk.PhotoImage(img.resize(size), Image.BICUBIC)
name = util.get_image_name(image)
self.theme_images[name] = image
return name
# create images
h_normal_img = draw_rect(hsize, thumbcolor)
h_pressed_img = draw_rect(hsize, pressed)
h_active_img = draw_rect(hsize, active)
v_normal_img = draw_rect(vsize, thumbcolor)
v_pressed_img = draw_rect(vsize, pressed)
v_active_img = draw_rect(vsize, active)
return (
h_normal_img,
h_pressed_img,
h_active_img,
v_normal_img,
v_pressed_img,
v_active_img,
)
def create_scrollbar_style(self, colorname=DEFAULT):
"""Create a standard style for the ttk.Scrollbar widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TScrollbar"
if any([colorname == DEFAULT, colorname == ""]):
h_ttkstyle = f"Horizontal.{STYLE}"
v_ttkstyle = f"Vertical.{STYLE}"
if self.is_light_theme:
background = self.colors.border
else:
background = self.colors.selectbg
else:
h_ttkstyle = f"{colorname}.Horizontal.{STYLE}"
v_ttkstyle = f"{colorname}.Vertical.{STYLE}"
background = self.colors.get(colorname)
if self.is_light_theme:
if colorname == LIGHT:
troughcolor = self.colors.bg
else:
troughcolor = self.colors.light
else:
troughcolor = Colors.update_hsv(self.colors.selectbg, vd=-0.2)
pressed = Colors.update_hsv(background, vd=-0.05)
active = Colors.update_hsv(background, vd=0.05)
scroll_images = self.create_scrollbar_assets(
background, pressed, active
)
# horizontal scrollbar
self.style._build_configure(
h_ttkstyle,
troughcolor=troughcolor,
darkcolor=troughcolor,
bordercolor=troughcolor,
lightcolor=troughcolor,
arrowcolor=background,
arrowsize=self.scale_size(11),
background=troughcolor,
relief=tk.FLAT,
borderwidth=0,
)
self.style.element_create(
f"{h_ttkstyle}.thumb",
"image",
scroll_images[0],
("pressed", scroll_images[1]),
("active", scroll_images[2]),
border=(3, 0),
sticky=tk.NSEW,
)
self.style.layout(
h_ttkstyle,
[
(
"Horizontal.Scrollbar.trough",
{
"sticky": "we",
"children": [
(
"Horizontal.Scrollbar.leftarrow",
{"side": "left", "sticky": ""},
),
(
"Horizontal.Scrollbar.rightarrow",
{"side": "right", "sticky": ""},
),
(
f"{h_ttkstyle}.thumb",
{"expand": "1", "sticky": "nswe"},
),
],
},
)
],
)
self.style._build_configure(h_ttkstyle, arrowcolor=background)
self.style.map(
h_ttkstyle, arrowcolor=[("pressed", pressed), ("active", active)]
)
# vertical scrollbar
self.style._build_configure(
v_ttkstyle,
troughcolor=troughcolor,
darkcolor=troughcolor,
bordercolor=troughcolor,
lightcolor=troughcolor,
arrowcolor=background,
arrowsize=self.scale_size(11),
background=troughcolor,
relief=tk.FLAT,
borderwidth=0,
)
self.style.element_create(
f"{v_ttkstyle}.thumb",
"image",
scroll_images[3],
("pressed", scroll_images[4]),
("active", scroll_images[5]),
border=(0, 3),
sticky=tk.NSEW,
)
self.style.layout(
v_ttkstyle,
[
(
"Vertical.Scrollbar.trough",
{
"sticky": "ns",
"children": [
(
"Vertical.Scrollbar.uparrow",
{"side": "top", "sticky": ""},
),
(
"Vertical.Scrollbar.downarrow",
{"side": "bottom", "sticky": ""},
),
(
f"{v_ttkstyle}.thumb",
{"expand": "1", "sticky": "nswe"},
),
],
},
)
],
)
self.style._build_configure(v_ttkstyle, arrowcolor=background)
self.style.map(
v_ttkstyle, arrowcolor=[("pressed", pressed), ("active", active)]
)
# register ttkstyles
self.style._register_ttkstyle(h_ttkstyle)
self.style._register_ttkstyle(v_ttkstyle)
def create_spinbox_style(self, colorname=DEFAULT):
"""Create a style for the ttk.Spinbox widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TSpinbox"
if self.is_light_theme:
disabled_fg = self.colors.border
bordercolor = self.colors.border
readonly = self.colors.light
else:
disabled_fg = self.colors.selectbg
bordercolor = self.colors.selectbg
readonly = bordercolor
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
focuscolor = self.colors.primary
else:
ttkstyle = f"{colorname}.{STYLE}"
focuscolor = self.colors.get(colorname)
if all([colorname, colorname != DEFAULT]):
bordercolor = focuscolor
if colorname == "light":
arrowfocus = self.colors.fg
else:
arrowfocus = focuscolor
element = ttkstyle.replace(".TS", ".S")
self.style.element_create(f"{element}.uparrow", "from", TTK_DEFAULT)
self.style.element_create(f"{element}.downarrow", "from", TTK_DEFAULT)
self.style.layout(
ttkstyle,
[
(
f"{element}.field",
{
"side": tk.TOP,
"sticky": tk.EW,
"children": [
(
"null",
{
"side": tk.RIGHT,
"sticky": "",
"children": [
(
f"{element}.uparrow",
{"side": tk.TOP, "sticky": tk.E},
),
(
f"{element}.downarrow",
{
"side": tk.BOTTOM,
"sticky": tk.E,
},
),
],
},
),
(
f"{element}.padding",
{
"sticky": tk.NSEW,
"children": [
(
f"{element}.textarea",
{"sticky": tk.NSEW},
)
],
},
),
],
},
)
],
)
self.style._build_configure(
ttkstyle,
bordercolor=bordercolor,
darkcolor=self.colors.inputbg,
lightcolor=self.colors.inputbg,
fieldbackground=self.colors.inputbg,
foreground=self.colors.inputfg,
borderwidth=0,
background=self.colors.inputbg,
relief=tk.FLAT,
arrowcolor=self.colors.inputfg,
insertcolor=self.colors.inputfg,
arrowsize=self.scale_size(12),
padding=(10, 5),
)
self.style.map(
ttkstyle,
foreground=[("disabled", disabled_fg)],
fieldbackground=[("readonly", readonly)],
background=[("readonly", readonly)],
lightcolor=[
("focus invalid", self.colors.danger),
("focus !disabled", focuscolor),
("readonly", readonly),
],
darkcolor=[
("focus invalid", self.colors.danger),
("focus !disabled", focuscolor),
("readonly", readonly),
],
bordercolor=[
("invalid", self.colors.danger),
("focus !disabled", focuscolor),
("hover !disabled", focuscolor),
],
arrowcolor=[
("disabled !disabled", disabled_fg),
("pressed !disabled", arrowfocus),
("hover !disabled", arrowfocus),
],
)
# register ttkstyles
self.style._register_ttkstyle(ttkstyle)
def create_table_treeview_style(self, colorname=DEFAULT):
"""Create a style for the Tableview widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "Table.Treeview"
f = font.nametofont("TkDefaultFont")
rowheight = f.metrics()["linespace"]
if self.is_light_theme:
disabled_fg = Colors.update_hsv(self.colors.inputbg, vd=-0.2)
bordercolor = self.colors.border
hover = Colors.update_hsv(self.colors.light, vd=-0.1)
else:
disabled_fg = Colors.update_hsv(self.colors.inputbg, vd=-0.3)
bordercolor = self.colors.selectbg
hover = Colors.update_hsv(self.colors.dark, vd=0.1)
if any([colorname == DEFAULT, colorname == ""]):
background = self.colors.inputbg
foreground = self.colors.inputfg
body_style = STYLE
header_style = f"{STYLE}.Heading"
elif colorname == LIGHT and self.is_light_theme:
background = self.colors.get(colorname)
foreground = self.colors.fg
body_style = f"{colorname}.{STYLE}"
header_style = f"{colorname}.{STYLE}.Heading"
hover = Colors.update_hsv(background, vd=-0.1)
else:
background = self.colors.get(colorname)
foreground = self.colors.selectfg
body_style = f"{colorname}.{STYLE}"
header_style = f"{colorname}.{STYLE}.Heading"
hover = Colors.update_hsv(background, vd=0.1)
# treeview header
self.style._build_configure(
header_style,
background=background,
foreground=foreground,
relief=RAISED,
borderwidth=1,
darkcolor=background,
bordercolor=bordercolor,
lightcolor=background,
padding=5,
)
self.style.map(
header_style,
foreground=[("disabled", disabled_fg)],
background=[
("active !disabled", hover),
],
darkcolor=[
("active !disabled", hover),
],
lightcolor=[
("active !disabled", hover),
],
)
self.style._build_configure(
body_style,
background=self.colors.inputbg,
fieldbackground=self.colors.inputbg,
foreground=self.colors.inputfg,
bordercolor=bordercolor,
lightcolor=self.colors.inputbg,
darkcolor=self.colors.inputbg,
borderwidth=2,
padding=0,
rowheight=rowheight,
relief=tk.RAISED,
)
self.style.map(
body_style,
background=[("selected", self.colors.selectbg)],
foreground=[
("disabled", disabled_fg),
("selected", self.colors.selectfg),
],
)
self.style.layout(
body_style,
[
(
"Button.border",
{
"sticky": tk.NSEW,
"border": "1",
"children": [
(
"Treeview.padding",
{
"sticky": tk.NSEW,
"children": [
(
"Treeview.treearea",
{"sticky": tk.NSEW},
)
],
},
)
],
},
)
],
)
# register ttkstyles
self.style._register_ttkstyle(body_style)
def create_treeview_style(self, colorname=DEFAULT):
"""Create a style for the ttk.Treeview widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "Treeview"
f = font.nametofont("TkDefaultFont")
rowheight = f.metrics()["linespace"]
if self.is_light_theme:
disabled_fg = Colors.update_hsv(self.colors.inputbg, vd=-0.2)
bordercolor = self.colors.border
else:
disabled_fg = Colors.update_hsv(self.colors.inputbg, vd=-0.3)
bordercolor = self.colors.selectbg
if any([colorname == DEFAULT, colorname == ""]):
background = self.colors.inputbg
foreground = self.colors.inputfg
body_style = STYLE
header_style = f"{STYLE}.Heading"
focuscolor = self.colors.primary
elif colorname == LIGHT and self.is_light_theme:
background = self.colors.get(colorname)
foreground = self.colors.fg
body_style = f"{colorname}.{STYLE}"
header_style = f"{colorname}.{STYLE}.Heading"
focuscolor = background
bordercolor = focuscolor
else:
background = self.colors.get(colorname)
foreground = self.colors.selectfg
body_style = f"{colorname}.{STYLE}"
header_style = f"{colorname}.{STYLE}.Heading"
focuscolor = background
bordercolor = focuscolor
# treeview header
self.style._build_configure(
header_style,
background=background,
foreground=foreground,
relief=tk.FLAT,
padding=5,
)
self.style.map(
header_style,
foreground=[("disabled", disabled_fg)],
bordercolor=[("focus !disabled", background)],
)
# treeview body
self.style._build_configure(
body_style,
background=self.colors.inputbg,
fieldbackground=self.colors.inputbg,
foreground=self.colors.inputfg,
bordercolor=bordercolor,
lightcolor=self.colors.inputbg,
darkcolor=self.colors.inputbg,
borderwidth=2,
padding=0,
rowheight=rowheight,
relief=tk.RAISED,
)
self.style.map(
body_style,
background=[("selected", self.colors.selectbg)],
foreground=[
("disabled", disabled_fg),
("selected", self.colors.selectfg),
],
bordercolor=[
("disabled", bordercolor),
("focus", focuscolor),
("pressed", focuscolor),
("hover", focuscolor),
],
lightcolor=[("focus", focuscolor)],
darkcolor=[("focus", focuscolor)],
)
self.style.layout(
body_style,
[
(
"Button.border",
{
"sticky": tk.NSEW,
"border": "1",
"children": [
(
"Treeview.padding",
{
"sticky": tk.NSEW,
"children": [
(
"Treeview.treearea",
{"sticky": tk.NSEW},
)
],
},
)
],
},
)
],
)
try:
self.style.element_create("Treeitem.indicator", "from", TTK_ALT)
except:
pass
# register ttkstyles
self.style._register_ttkstyle(body_style)
def create_frame_style(self, colorname=DEFAULT):
"""Create a style for the ttk.Frame widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TFrame"
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
background = self.colors.bg
else:
ttkstyle = f"{colorname}.{STYLE}"
background = self.colors.get(colorname)
self.style._build_configure(ttkstyle, background=background)
# register style
self.style._register_ttkstyle(ttkstyle)
def create_button_style(self, colorname=DEFAULT):
"""Create a solid style for the ttk.Button widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TButton"
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
foreground = self.colors.get_foreground(PRIMARY)
background = self.colors.primary
else:
ttkstyle = f"{colorname}.{STYLE}"
foreground = self.colors.get_foreground(colorname)
background = self.colors.get(colorname)
bordercolor = background
disabled_bg = Colors.make_transparent(0.10, self.colors.fg, self.colors.bg)
disabled_fg = Colors.make_transparent(0.30, self.colors.fg, self.colors.bg)
pressed = Colors.make_transparent(0.80, background, self.colors.bg)
hover = Colors.make_transparent(0.90, background, self.colors.bg)
self.style._build_configure(
ttkstyle,
foreground=foreground,
background=background,
bordercolor=bordercolor,
darkcolor=background,
lightcolor=background,
relief=tk.RAISED,
focusthickness=0,
focuscolor=foreground,
padding=(10, 5),
anchor=tk.CENTER,
)
self.style.map(
ttkstyle,
foreground=[("disabled", disabled_fg)],
background=[
("disabled", disabled_bg),
("pressed !disabled", pressed),
("hover !disabled", hover),
],
bordercolor=[("disabled", disabled_bg)],
darkcolor=[
("disabled", disabled_bg),
("pressed !disabled", pressed),
("hover !disabled", hover),
],
lightcolor=[
("disabled", disabled_bg),
("pressed !disabled", pressed),
("hover !disabled", hover),
],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_outline_button_style(self, colorname=DEFAULT):
"""Create an outline style for the ttk.Button widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "Outline.TButton"
disabled_fg = Colors.make_transparent(0.30, self.colors.fg, self.colors.bg)
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
colorname = PRIMARY
else:
ttkstyle = f"{colorname}.{STYLE}"
foreground = self.colors.get(colorname)
background = self.colors.get_foreground(colorname)
foreground_pressed = background
bordercolor = foreground
pressed = foreground
hover = foreground
self.style._build_configure(
ttkstyle,
foreground=foreground,
background=self.colors.bg,
bordercolor=bordercolor,
darkcolor=self.colors.bg,
lightcolor=self.colors.bg,
relief=tk.RAISED,
focusthickness=0,
focuscolor=foreground,
padding=(10, 5),
anchor=tk.CENTER,
)
self.style.map(
ttkstyle,
foreground=[
("disabled", disabled_fg),
("pressed !disabled", foreground_pressed),
("hover !disabled", foreground_pressed),
],
background=[
("pressed !disabled", pressed),
("hover !disabled", hover),
],
bordercolor=[
("disabled", disabled_fg),
("pressed !disabled", pressed),
("hover !disabled", hover),
],
focuscolor=[
("pressed !disabled", foreground_pressed),
("hover !disabled", foreground_pressed),
],
darkcolor=[
("pressed !disabled", pressed),
("hover !disabled", hover),
],
lightcolor=[
("pressed !disabled", pressed),
("hover !disabled", hover),
],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_link_button_style(self, colorname=DEFAULT):
"""Create a link button style for the ttk.Button widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "Link.TButton"
pressed = self.colors.info
hover = self.colors.info
if any([colorname == DEFAULT, colorname == ""]):
foreground = self.colors.fg
ttkstyle = STYLE
elif colorname == LIGHT:
foreground = self.colors.fg
ttkstyle = f"{colorname}.{STYLE}"
else:
foreground = self.colors.get(colorname)
ttkstyle = f"{colorname}.{STYLE}"
disabled_fg = Colors.make_transparent(0.30, self.colors.fg, self.colors.bg)
self.style._build_configure(
ttkstyle,
foreground=foreground,
background=self.colors.bg,
bordercolor=self.colors.bg,
darkcolor=self.colors.bg,
lightcolor=self.colors.bg,
relief=tk.RAISED,
focusthickness=0,
focuscolor=foreground,
anchor=tk.CENTER,
padding=(10, 5),
)
self.style.map(
ttkstyle,
shiftrelief=[("pressed !disabled", -1)],
foreground=[
("disabled", disabled_fg),
("pressed !disabled", pressed),
("hover !disabled", hover),
],
focuscolor=[
("pressed !disabled", pressed),
("hover !disabled", pressed),
],
background=[
("disabled", self.colors.bg),
("pressed !disabled", self.colors.bg),
("hover !disabled", self.colors.bg),
],
bordercolor=[
("disabled", self.colors.bg),
("pressed !disabled", self.colors.bg),
("hover !disabled", self.colors.bg),
],
darkcolor=[
("disabled", self.colors.bg),
("pressed !disabled", self.colors.bg),
("hover !disabled", self.colors.bg),
],
lightcolor=[
("disabled", self.colors.bg),
("pressed !disabled", self.colors.bg),
("hover !disabled", self.colors.bg),
],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_square_toggle_assets(self, colorname=DEFAULT):
"""Create the image assets used to build a square toggle
style.
Parameters:
colorname (str):
The color label used to style the widget.
Returns:
Tuple[str]:
A tuple of PhotoImage names.
"""
size = self.scale_size([24, 15])
if any([colorname == DEFAULT, colorname == ""]):
colorname = PRIMARY
# set default style color values
prime_color = self.colors.get(colorname)
on_border = prime_color
on_indicator = self.colors.selectfg
on_fill = prime_color
off_fill = self.colors.bg
disabled_fg = Colors.make_transparent(0.3, self.colors.fg, self.colors.bg)
off_border = Colors.make_transparent(0.4, self.colors.fg, self.colors.bg)
off_indicator = Colors.make_transparent(0.4, self.colors.fg, self.colors.bg)
# override defaults for light and dark colors
if colorname == LIGHT:
on_border = self.colors.dark
on_indicator = on_border
elif colorname == DARK:
on_border = self.colors.light
on_indicator = on_border
# toggle off
_off = Image.new("RGBA", (226, 130))
draw = ImageDraw.Draw(_off)
draw.rectangle(
xy=[1, 1, 225, 129], outline=off_border, width=6, fill=off_fill
)
draw.rectangle([18, 18, 110, 110], fill=off_indicator)
off_img = ImageTk.PhotoImage(_off.resize(size, Image.LANCZOS))
off_name = util.get_image_name(off_img)
self.theme_images[off_name] = off_img
# toggle on
toggle_on = Image.new("RGBA", (226, 130))
draw = ImageDraw.Draw(toggle_on)
draw.rectangle(
xy=[1, 1, 225, 129], outline=on_border, width=6, fill=on_fill
)
draw.rectangle([18, 18, 110, 110], fill=on_indicator)
_on = toggle_on.transpose(Image.ROTATE_180)
on_img = ImageTk.PhotoImage(_on.resize(size, Image.LANCZOS))
on_name = util.get_image_name(on_img)
self.theme_images[on_name] = on_img
# toggle disabled
_disabled = Image.new("RGBA", (226, 130))
draw = ImageDraw.Draw(_disabled)
draw.rectangle([1, 1, 225, 129], outline=disabled_fg, width=6)
draw.rectangle([18, 18, 110, 110], fill=disabled_fg)
disabled_img = ImageTk.PhotoImage(
_disabled.resize(size, Image.LANCZOS)
)
disabled_name = util.get_image_name(disabled_img)
self.theme_images[disabled_name] = disabled_img
# toggle on / disabled
toggle_on_disabled = Image.new("RGBA", (226, 130))
draw = ImageDraw.Draw(toggle_on_disabled)
draw.rectangle(
xy=[1, 1, 225, 129], outline=disabled_fg, width=6, fill=off_fill
)
draw.rectangle([18, 18, 110, 110], fill=disabled_fg)
_on_disabled = toggle_on_disabled.transpose(Image.ROTATE_180)
on_dis_img = ImageTk.PhotoImage(_on_disabled.resize(size, Image.LANCZOS))
on_disabled_name = util.get_image_name(on_dis_img)
self.theme_images[on_disabled_name] = on_dis_img
return off_name, on_name, disabled_name, on_disabled_name
def create_toggle_style(self, colorname=DEFAULT):
"""Create a round toggle style for the ttk.Checkbutton widget.
Parameters:
colorname (str):
"""
self.create_round_toggle_style(colorname)
def create_round_toggle_assets(self, colorname=DEFAULT):
"""Create image assets for the round toggle style.
Parameters:
colorname (str):
The color label assigned to the colors property.
Returns:
Tuple[str]:
A tuple of PhotoImage names.
"""
size = self.scale_size([24, 15])
if any([colorname == DEFAULT, colorname == ""]):
colorname = PRIMARY
# set default style color values
prime_color = self.colors.get(colorname)
on_border = prime_color
on_indicator = self.colors.selectfg
on_fill = prime_color
off_fill = self.colors.bg
disabled_fg = Colors.make_transparent(0.3, self.colors.fg, self.colors.bg)
off_border = Colors.make_transparent(0.4, self.colors.fg, self.colors.bg)
off_indicator = Colors.make_transparent(0.4, self.colors.fg, self.colors.bg)
# override defaults for light and dark colors
if colorname == LIGHT:
on_border = self.colors.dark
on_indicator = on_border
elif colorname == DARK:
on_border = self.colors.light
on_indicator = on_border
# toggle off
_off = Image.new("RGBA", (226, 130))
draw = ImageDraw.Draw(_off)
draw.rounded_rectangle(
xy=[1, 1, 225, 129],
radius=(128 / 2),
outline=off_border,
width=6,
fill=off_fill,
)
draw.ellipse([20, 18, 112, 110], fill=off_indicator)
off_img = ImageTk.PhotoImage(_off.resize(size, Image.LANCZOS))
off_name = util.get_image_name(off_img)
self.theme_images[off_name] = off_img
# toggle on
_on = Image.new("RGBA", (226, 130))
draw = ImageDraw.Draw(_on)
draw.rounded_rectangle(
xy=[1, 1, 225, 129],
radius=(128 / 2),
outline=on_border,
width=6,
fill=on_fill,
)
draw.ellipse([20, 18, 112, 110], fill=on_indicator)
_on = _on.transpose(Image.ROTATE_180)
on_img = ImageTk.PhotoImage(_on.resize(size, Image.LANCZOS))
on_name = util.get_image_name(on_img)
self.theme_images[on_name] = on_img
# toggle on / disabled
_on_disabled = Image.new("RGBA", (226, 130))
draw = ImageDraw.Draw(_on_disabled)
draw.rounded_rectangle(
xy=[1, 1, 225, 129],
radius=(128 / 2),
outline=disabled_fg,
width=6,
fill=off_fill,
)
draw.ellipse([20, 18, 112, 110], fill=disabled_fg)
_on_disabled = _on_disabled.transpose(Image.ROTATE_180)
on_dis_img = ImageTk.PhotoImage(_on_disabled.resize(size, Image.LANCZOS))
on_disabled_name = util.get_image_name(on_dis_img)
self.theme_images[on_disabled_name] = on_dis_img
# toggle disabled
_disabled = Image.new("RGBA", (226, 130))
draw = ImageDraw.Draw(_disabled)
draw.rounded_rectangle(
xy=[1, 1, 225, 129], radius=(128 / 2), outline=disabled_fg, width=6
)
draw.ellipse([20, 18, 112, 110], fill=disabled_fg)
disabled_img = ImageTk.PhotoImage(
_disabled.resize(size, Image.LANCZOS)
)
disabled_name = util.get_image_name(disabled_img)
self.theme_images[disabled_name] = disabled_img
return off_name, on_name, disabled_name, on_disabled_name
def create_round_toggle_style(self, colorname=DEFAULT):
"""Create a round toggle style for the ttk.Checkbutton widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "Round.Toggle"
disabled_fg = Colors.make_transparent(0.30, self.colors.fg, self.colors.bg)
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
colorname = PRIMARY
else:
ttkstyle = f"{colorname}.{STYLE}"
# ( off, on, disabled )
images = self.create_round_toggle_assets(colorname)
try:
width = self.scale_size(28)
borderpad = self.scale_size(4)
self.style.element_create(
f"{ttkstyle}.indicator",
"image",
images[1],
("disabled selected", images[3]),
("disabled", images[2]),
("!selected", images[0]),
width=width,
border=borderpad,
sticky=tk.W,
)
except:
"""This method is used as the default Toggle style, so it
is neccessary to catch Tcl Errors when it tries to create
and element that was already created by the Toggle or
Round Toggle style"""
pass
self.style._build_configure(
ttkstyle,
relief=tk.FLAT,
borderwidth=0,
padding=0,
foreground=self.colors.fg,
background=self.colors.bg,
)
self.style.map(
ttkstyle,
foreground=[("disabled", disabled_fg)],
background=[("selected", self.colors.bg)],
)
self.style.layout(
ttkstyle,
[
(
"Toolbutton.border",
{
"sticky": tk.NSEW,
"children": [
(
"Toolbutton.padding",
{
"sticky": tk.NSEW,
"children": [
(
f"{ttkstyle}.indicator",
{"side": tk.LEFT},
),
(
"Toolbutton.label",
{"side": tk.LEFT},
),
],
},
)
],
},
)
],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_square_toggle_style(self, colorname=DEFAULT):
"""Create a square toggle style for the ttk.Checkbutton widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "Square.Toggle"
disabled_fg = Colors.make_transparent(0.30, self.colors.fg, self.colors.bg)
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
else:
ttkstyle = f"{colorname}.{STYLE}"
# ( off, on, disabled )
images = self.create_square_toggle_assets(colorname)
width = self.scale_size(28)
borderpad = self.scale_size(4)
self.style.element_create(
f"{ttkstyle}.indicator",
"image",
images[1],
("disabled selected", images[3]),
("disabled", images[2]),
("!selected", images[0]),
width=width,
border=borderpad,
sticky=tk.W,
)
self.style.layout(
ttkstyle,
[
(
"Toolbutton.border",
{
"sticky": tk.NSEW,
"children": [
(
"Toolbutton.padding",
{
"sticky": tk.NSEW,
"children": [
(
f"{ttkstyle}.indicator",
{"side": tk.LEFT},
),
(
"Toolbutton.label",
{"side": tk.LEFT},
),
],
},
)
],
},
)
],
)
self.style._build_configure(
ttkstyle, relief=tk.FLAT, borderwidth=0, foreground=self.colors.fg
)
self.style.map(
ttkstyle,
foreground=[("disabled", disabled_fg)],
background=[
("selected", self.colors.bg),
("!selected", self.colors.bg),
],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_toolbutton_style(self, colorname=DEFAULT):
"""Create a solid toolbutton style for the ttk.Checkbutton
and ttk.Radiobutton widgets.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "Toolbutton"
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
toggle_on = self.colors.primary
else:
ttkstyle = f"{colorname}.{STYLE}"
toggle_on = self.colors.get(colorname)
foreground = self.colors.get_foreground(colorname)
if self.is_light_theme:
toggle_off = self.colors.border
else:
toggle_off = self.colors.selectbg
disabled_bg = Colors.make_transparent(0.10, self.colors.fg, self.colors.bg)
disabled_fg = Colors.make_transparent(0.30, self.colors.fg, self.colors.bg)
self.style._build_configure(
ttkstyle,
foreground=self.colors.selectfg,
background=toggle_off,
bordercolor=toggle_off,
darkcolor=toggle_off,
lightcolor=toggle_off,
relief=tk.RAISED,
focusthickness=0,
focuscolor="",
padding=(10, 5),
anchor=tk.CENTER,
)
self.style.map(
ttkstyle,
foreground=[
("disabled", disabled_fg),
("hover", foreground),
("selected", foreground),
],
background=[
("disabled", disabled_bg),
("pressed !disabled", toggle_on),
("selected !disabled", toggle_on),
("hover !disabled", toggle_on),
],
bordercolor=[
("disabled", disabled_bg),
("pressed !disabled", toggle_on),
("selected !disabled", toggle_on),
("hover !disabled", toggle_on),
],
darkcolor=[
("disabled", disabled_bg),
("pressed !disabled", toggle_on),
("selected !disabled", toggle_on),
("hover !disabled", toggle_on),
],
lightcolor=[
("disabled", disabled_bg),
("pressed !disabled", toggle_on),
("selected !disabled", toggle_on),
("hover !disabled", toggle_on),
],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_outline_toolbutton_style(self, colorname=DEFAULT):
"""Create an outline toolbutton style for the ttk.Checkbutton
and ttk.Radiobutton widgets.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "Outline.Toolbutton"
disabled_fg = Colors.make_transparent(0.30, self.colors.fg, self.colors.bg)
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
colorname = PRIMARY
else:
ttkstyle = f"{colorname}.{STYLE}"
foreground = self.colors.get(colorname)
background = self.colors.get_foreground(colorname)
foreground_pressed = background
bordercolor = foreground
pressed = foreground
hover = foreground
self.style._build_configure(
ttkstyle,
foreground=foreground,
background=self.colors.bg,
bordercolor=bordercolor,
darkcolor=self.colors.bg,
lightcolor=self.colors.bg,
relief=tk.RAISED,
focusthickness=0,
focuscolor=foreground,
padding=(10, 5),
anchor=tk.CENTER,
arrowcolor=foreground,
arrowpadding=(0, 0, 15, 0),
arrowsize=3,
)
self.style.map(
ttkstyle,
foreground=[
("disabled", disabled_fg),
("pressed !disabled", foreground_pressed),
("selected !disabled", foreground_pressed),
("hover !disabled", foreground_pressed),
],
background=[
("pressed !disabled", pressed),
("selected !disabled", pressed),
("hover !disabled", hover),
],
bordercolor=[
("disabled", disabled_fg),
("pressed !disabled", pressed),
("selected !disabled", pressed),
("hover !disabled", hover),
],
darkcolor=[
("disabled", self.colors.bg),
("pressed !disabled", pressed),
("selected !disabled", pressed),
("hover !disabled", hover),
],
lightcolor=[
("disabled", self.colors.bg),
("pressed !disabled", pressed),
("selected !disabled", pressed),
("hover !disabled", hover),
],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_entry_style(self, colorname=DEFAULT):
"""Create a style for the ttk.Entry widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TEntry"
# general default colors
if self.is_light_theme:
disabled_fg = self.colors.border
bordercolor = self.colors.border
readonly = self.colors.light
else:
disabled_fg = self.colors.selectbg
bordercolor = self.colors.selectbg
readonly = bordercolor
if any([colorname == DEFAULT, not colorname]):
# default style
ttkstyle = STYLE
focuscolor = self.colors.primary
else:
# colored style
ttkstyle = f"{colorname}.{STYLE}"
focuscolor = self.colors.get(colorname)
bordercolor = focuscolor
self.style._build_configure(
ttkstyle,
bordercolor=bordercolor,
darkcolor=self.colors.inputbg,
lightcolor=self.colors.inputbg,
fieldbackground=self.colors.inputbg,
foreground=self.colors.inputfg,
insertcolor=self.colors.inputfg,
padding=5,
)
self.style.map(
ttkstyle,
foreground=[("disabled", disabled_fg)],
fieldbackground=[("readonly", readonly)],
bordercolor=[
("invalid", self.colors.danger),
("focus !disabled", focuscolor),
("hover !disabled", focuscolor),
],
lightcolor=[
("focus invalid", self.colors.danger),
("focus !disabled", focuscolor),
("readonly", readonly),
],
darkcolor=[
("focus invalid", self.colors.danger),
("focus !disabled", focuscolor),
("readonly", readonly),
],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_radiobutton_assets(self, colorname=DEFAULT):
"""Create the image assets used to build the radiobutton style.
Parameters:
colorname (str):
Returns:
Tuple[str]:
A tuple of PhotoImage names
"""
prime_color = self.colors.get(colorname)
on_fill = prime_color
off_fill = self.colors.bg
on_indicator = self.colors.selectfg
size = self.scale_size([14, 14])
off_border = Colors.make_transparent(0.4, self.colors.fg, self.colors.bg)
disabled = Colors.make_transparent(0.3, self.colors.fg, self.colors.bg)
if self.is_light_theme:
if colorname == LIGHT:
on_indicator = self.colors.dark
# radio off
_off = Image.new("RGBA", (134, 134))
draw = ImageDraw.Draw(_off)
draw.ellipse(
xy=[1, 1, 133, 133], outline=off_border, width=6, fill=off_fill
)
off_img = ImageTk.PhotoImage(_off.resize(size, Image.LANCZOS))
off_name = util.get_image_name(off_img)
self.theme_images[off_name] = off_img
# radio on
_on = Image.new("RGBA", (134, 134))
draw = ImageDraw.Draw(_on)
if colorname == LIGHT and self.is_light_theme:
draw.ellipse(xy=[1, 1, 133, 133], outline=off_border, width=6)
else:
draw.ellipse(xy=[1, 1, 133, 133], fill=on_fill)
draw.ellipse([40, 40, 94, 94], fill=on_indicator)
on_img = ImageTk.PhotoImage(_on.resize(size, Image.LANCZOS))
on_name = util.get_image_name(on_img)
self.theme_images[on_name] = on_img
# radio on/disabled
_on_dis = Image.new("RGBA", (134, 134))
draw = ImageDraw.Draw(_on_dis)
if colorname == LIGHT and self.is_light_theme:
draw.ellipse(xy=[1, 1, 133, 133], outline=off_border, width=6)
else:
draw.ellipse(xy=[1, 1, 133, 133], fill=disabled)
draw.ellipse([40, 40, 94, 94], fill=off_fill)
on_dis_img = ImageTk.PhotoImage(_on_dis.resize(size, Image.LANCZOS))
on_disabled_name = util.get_image_name(on_dis_img)
self.theme_images[on_disabled_name] = on_dis_img
# radio disabled
_disabled = Image.new("RGBA", (134, 134))
draw = ImageDraw.Draw(_disabled)
draw.ellipse(
xy=[1, 1, 133, 133], outline=disabled, width=3, fill=off_fill
)
disabled_img = ImageTk.PhotoImage(
_disabled.resize(size, Image.LANCZOS)
)
disabled_name = util.get_image_name(disabled_img)
self.theme_images[disabled_name] = disabled_img
return off_name, on_name, disabled_name, on_disabled_name
def create_radiobutton_style(self, colorname=DEFAULT):
"""Create a style for the ttk.Radiobutton widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TRadiobutton"
disabled_fg = Colors.make_transparent(0.30, self.colors.fg, self.colors.bg)
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
colorname = PRIMARY
else:
ttkstyle = f"{colorname}.{STYLE}"
# ( off, on, disabled )
images = self.create_radiobutton_assets(colorname)
width = self.scale_size(20)
borderpad = self.scale_size(4)
self.style.element_create(
f"{ttkstyle}.indicator",
"image",
images[1],
("disabled selected", images[3]),
("disabled", images[2]),
("!selected", images[0]),
width=width,
border=borderpad,
sticky=tk.W,
)
self.style.map(ttkstyle, foreground=[("disabled", disabled_fg)])
self.style._build_configure(ttkstyle)
self.style.layout(
ttkstyle,
[
(
"Radiobutton.padding",
{
"children": [
(
f"{ttkstyle}.indicator",
{"side": tk.LEFT, "sticky": ""},
),
(
"Radiobutton.focus",
{
"children": [
(
"Radiobutton.label",
{"sticky": tk.NSEW},
)
],
"side": tk.LEFT,
"sticky": "",
},
),
],
"sticky": tk.NSEW,
},
)
],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_date_button_assets(self, foreground):
"""Create the image assets used to build the date button
style. This button style applied to the button in the
ttkbootstrap.widgets.DateEntry.
Parameters:
foreground (str):
The color value used to draw the calendar image.
Returns:
str:
The PhotoImage name.
"""
fill = foreground
image = Image.new("RGBA", (210, 220))
draw = ImageDraw.Draw(image)
draw.rounded_rectangle(
[10, 30, 200, 210], radius=20, outline=fill, width=10
)
calendar_image_coordinates = [
# page spirals
[40, 10, 50, 50],
[100, 10, 110, 50],
[160, 10, 170, 50],
# row 1
[70, 90, 90, 110],
[110, 90, 130, 110],
[150, 90, 170, 110],
# row 2
[30, 130, 50, 150],
[70, 130, 90, 150],
[110, 130, 130, 150],
[150, 130, 170, 150],
# row 3
[30, 170, 50, 190],
[70, 170, 90, 190],
[110, 170, 130, 190],
]
for xy in calendar_image_coordinates:
draw.rectangle(xy=xy, fill=fill)
size = self.scale_size([21, 22])
tk_img = ImageTk.PhotoImage(image.resize(size, Image.LANCZOS))
tk_name = util.get_image_name(tk_img)
self.theme_images[tk_name] = tk_img
return tk_name
def create_date_button_style(self, colorname=DEFAULT):
"""Create a date button style for the ttk.Button widget.
Parameters:
colorname (str):
The color label used to style widget.
"""
STYLE = "Date.TButton"
if self.is_light_theme:
disabled_fg = self.colors.border
else:
disabled_fg = self.colors.selectbg
btn_foreground = Colors.get_foreground(self.colors, colorname)
img_normal = self.create_date_button_assets(btn_foreground)
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
foreground = self.colors.get_foreground(PRIMARY)
background = self.colors.primary
else:
ttkstyle = f"{colorname}.{STYLE}"
foreground = self.colors.get_foreground(colorname)
background = self.colors.get(colorname)
pressed = Colors.update_hsv(background, vd=-0.1)
hover = Colors.update_hsv(background, vd=0.10)
self.style._build_configure(
ttkstyle,
foreground=foreground,
background=background,
bordercolor=background,
darkcolor=background,
lightcolor=background,
relief=tk.RAISED,
focusthickness=0,
focuscolor=foreground,
padding=(2, 2),
anchor=tk.CENTER,
image=img_normal,
)
self.style.map(
ttkstyle,
foreground=[("disabled", disabled_fg)],
background=[
("disabled", disabled_fg),
("pressed !disabled", pressed),
("hover !disabled", hover),
],
bordercolor=[("disabled", disabled_fg)],
darkcolor=[
("disabled", disabled_fg),
("pressed !disabled", pressed),
("hover !disabled", hover),
],
lightcolor=[
("disabled", disabled_fg),
("pressed !disabled", pressed),
("hover !disabled", hover),
],
)
self.style._register_ttkstyle(ttkstyle)
def create_calendar_style(self, colorname=DEFAULT):
"""Create a style for the
ttkbootstrap.dialogs.DatePickerPopup widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TCalendar"
if any([colorname == DEFAULT, colorname == ""]):
prime_color = self.colors.primary
ttkstyle = STYLE
chevron_style = "Chevron.TButton"
else:
prime_color = self.colors.get(colorname)
ttkstyle = f"{colorname}.{STYLE}"
chevron_style = f"Chevron.{colorname}.TButton"
if self.is_light_theme:
disabled_fg = Colors.update_hsv(self.colors.inputbg, vd=-0.2)
pressed = Colors.update_hsv(prime_color, vd=-0.1)
else:
disabled_fg = Colors.update_hsv(self.colors.inputbg, vd=-0.3)
pressed = Colors.update_hsv(prime_color, vd=0.1)
self.style._build_configure(
ttkstyle,
foreground=self.colors.fg,
background=self.colors.bg,
bordercolor=self.colors.bg,
darkcolor=self.colors.bg,
lightcolor=self.colors.bg,
relief=tk.RAISED,
focusthickness=0,
focuscolor="",
borderwidth=1,
padding=(10, 5),
anchor=tk.CENTER,
)
self.style.layout(
ttkstyle,
[
(
"Toolbutton.border",
{
"sticky": tk.NSEW,
"children": [
(
"Toolbutton.padding",
{
"sticky": tk.NSEW,
"children": [
(
"Toolbutton.label",
{"sticky": tk.NSEW},
)
],
},
)
],
},
)
],
)
self.style.map(
ttkstyle,
foreground=[
("disabled", disabled_fg),
("pressed !disabled", self.colors.selectfg),
("selected !disabled", self.colors.selectfg),
("hover !disabled", self.colors.selectfg),
],
background=[
("pressed !disabled", pressed),
("selected !disabled", pressed),
("hover !disabled", pressed),
],
bordercolor=[
("disabled", disabled_fg),
("pressed !disabled", pressed),
("selected !disabled", pressed),
("hover !disabled", pressed),
],
darkcolor=[
("pressed !disabled", pressed),
("selected !disabled", pressed),
("hover !disabled", pressed),
],
lightcolor=[
("pressed !disabled", pressed),
("selected !disabled", pressed),
("hover !disabled", pressed),
],
)
self.style._build_configure(
chevron_style, font="-size 14", focuscolor=""
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
self.style._register_ttkstyle(chevron_style)
def create_metersubtxt_label_style(self, colorname=DEFAULT):
"""Create a subtext label style for the
ttkbootstrap.widgets.Meter widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "Metersubtxt.TLabel"
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
if self.is_light_theme:
foreground = self.colors.secondary
else:
foreground = self.colors.light
else:
ttkstyle = f"{colorname}.{STYLE}"
foreground = self.colors.get(colorname)
background = self.colors.bg
self.style._build_configure(
ttkstyle, foreground=foreground, background=background
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_meter_label_style(self, colorname=DEFAULT):
"""Create a label style for the
ttkbootstrap.widgets.Meter widget. This style also stores some
metadata that is called by the Meter class to lookup relevant
colors for the trough and bar when the new image is drawn.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "Meter.TLabel"
# text color = `foreground`
# trough color = `space`
if self.is_light_theme:
if colorname == LIGHT:
troughcolor = self.colors.bg
else:
troughcolor = self.colors.light
else:
troughcolor = Colors.update_hsv(self.colors.selectbg, vd=-0.2)
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
background = self.colors.bg
textcolor = self.colors.primary
else:
ttkstyle = f"{colorname}.{STYLE}"
textcolor = self.colors.get(colorname)
background = self.colors.bg
self.style._build_configure(
ttkstyle,
foreground=textcolor,
background=background,
space=troughcolor,
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_label_style(self, colorname=DEFAULT):
"""Create a standard style for the ttk.Label widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TLabel"
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
foreground = self.colors.fg
background = self.colors.bg
else:
ttkstyle = f"{colorname}.{STYLE}"
foreground = self.colors.get(colorname)
background = self.colors.bg
# standard label
self.style._build_configure(
ttkstyle, foreground=foreground, background=background
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_inverse_label_style(self, colorname=DEFAULT):
"""Create an inverted style for the ttk.Label.
The foreground and background are inverted versions of that
used in the standard label style.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE_INVERSE = "Inverse.TLabel"
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE_INVERSE
background = self.colors.fg
foreground = self.colors.bg
else:
ttkstyle = f"{colorname}.{STYLE_INVERSE}"
background = self.colors.get(colorname)
foreground = self.colors.get_foreground(colorname)
self.style._build_configure(
ttkstyle, foreground=foreground, background=background
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_labelframe_style(self, colorname=DEFAULT):
"""Create a style for the ttk.Labelframe widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TLabelframe"
background = self.colors.bg
if any([colorname == DEFAULT, colorname == ""]):
foreground = self.colors.fg
ttkstyle = STYLE
if self.is_light_theme:
bordercolor = self.colors.border
else:
bordercolor = self.colors.selectbg
else:
foreground = self.colors.get(colorname)
bordercolor = foreground
ttkstyle = f"{colorname}.{STYLE}"
# create widget style
self.style._build_configure(
f"{ttkstyle}.Label",
foreground=foreground,
background=background,
)
self.style._build_configure(
ttkstyle,
relief=tk.RAISED,
borderwidth=1,
bordercolor=bordercolor,
lightcolor=background,
darkcolor=background,
background=background,
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_checkbutton_style(self, colorname=DEFAULT):
"""Create a standard style for the ttk.Checkbutton widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TCheckbutton"
disabled_fg = Colors.make_transparent(0.3, self.colors.fg, self.colors.bg)
if any([colorname == DEFAULT, colorname == ""]):
colorname = PRIMARY
ttkstyle = STYLE
else:
ttkstyle = f"{colorname}.TCheckbutton"
# ( off, on, disabled )
images = self.create_checkbutton_assets(colorname)
element = ttkstyle.replace(".TC", ".C")
width = self.scale_size(20)
borderpad = self.scale_size(4)
self.style.element_create(
f"{element}.indicator",
"image",
images[1],
("disabled selected", images[4]),
("disabled alternate", images[5]),
("disabled", images[2]),
("alternate", images[3]),
("!selected", images[0]),
width=width,
border=borderpad,
sticky=tk.W,
)
self.style._build_configure(ttkstyle, foreground=self.colors.fg)
self.style.map(ttkstyle, foreground=[("disabled", disabled_fg)])
self.style.layout(
ttkstyle,
[
(
"Checkbutton.padding",
{
"children": [
(
f"{element}.indicator",
{"side": tk.LEFT, "sticky": ""},
),
(
"Checkbutton.focus",
{
"children": [
(
"Checkbutton.label",
{"sticky": tk.NSEW},
)
],
"side": tk.LEFT,
"sticky": "",
},
),
],
"sticky": tk.NSEW,
},
)
],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_checkbutton_assets(self, colorname=DEFAULT):
"""Create the image assets used to build the standard
checkbutton style.
Parameters:
colorname (str):
The color label used to style the widget.
Returns:
Tuple[str]:
A tuple of PhotoImage names.
"""
# set platform specific checkfont
winsys = self.style.tk.call("tk", "windowingsystem")
indicator = ""
if winsys == "win32":
# Windows font
fnt = ImageFont.truetype("seguisym.ttf", 120)
font_offset = -20
# TODO consider using ImageFont.getsize for offsets
elif winsys == "x11":
# Linux fonts
try:
# this should be available on most Linux distros
fnt = ImageFont.truetype("FreeSerif.ttf", 130)
font_offset = 10
except:
try:
# this should be available as a backup on Linux
# distros that don't have the FreeSerif.ttf file
fnt = ImageFont.truetype("DejaVuSans.ttf", 160)
font_offset = -15
except:
# If all else fails, use the default ImageFont
# this won't actually show anything in practice
# because of how I'm scaling the image, but it
# will prevent the program from crashing. I need
# a better solution for a missing font
fnt = ImageFont.load_default()
font_offset = 0
indicator = "x"
else:
# Mac OS font
fnt = ImageFont.truetype("LucidaGrande.ttc", 120)
font_offset = -10
prime_color = self.colors.get(colorname)
on_border = prime_color
on_fill = prime_color
off_fill = self.colors.bg
off_border = self.colors.selectbg
off_border = Colors.make_transparent(0.4, self.colors.fg, self.colors.bg)
disabled_fg = Colors.make_transparent(0.3, self.colors.fg, self.colors.bg)
if colorname == LIGHT:
check_color = self.colors.dark
on_border = check_color
elif colorname == DARK:
check_color = self.colors.light
on_border = check_color
else:
check_color = self.colors.selectfg
size = self.scale_size([14, 14])
# checkbutton off
checkbutton_off = Image.new("RGBA", (134, 134))
draw = ImageDraw.Draw(checkbutton_off)
draw.rounded_rectangle(
[2, 2, 132, 132],
radius=16,
outline=off_border,
width=6,
fill=off_fill,
)
off_img = ImageTk.PhotoImage(
checkbutton_off.resize(size, Image.LANCZOS)
)
off_name = util.get_image_name(off_img)
self.theme_images[off_name] = off_img
# checkbutton on
checkbutton_on = Image.new("RGBA", (134, 134))
draw = ImageDraw.Draw(checkbutton_on)
draw.rounded_rectangle(
[2, 2, 132, 132],
radius=16,
fill=on_fill,
outline=on_border,
width=3,
)
draw.text((20, font_offset), indicator, font=fnt, fill=check_color)
on_img = ImageTk.PhotoImage(checkbutton_on.resize(size, Image.LANCZOS))
on_name = util.get_image_name(on_img)
self.theme_images[on_name] = on_img
# checkbutton on/disabled
checkbutton_on_disabled = Image.new("RGBA", (134, 134))
draw = ImageDraw.Draw(checkbutton_on_disabled)
draw.rounded_rectangle(
[2, 2, 132, 132],
radius=16,
fill=disabled_fg,
outline=disabled_fg,
width=3,
)
draw.text((20, font_offset), indicator, font=fnt, fill=off_fill)
on_dis_img = ImageTk.PhotoImage(checkbutton_on_disabled.resize(size, Image.LANCZOS))
on_dis_name = util.get_image_name(on_dis_img)
self.theme_images[on_dis_name] = on_dis_img
# checkbutton alt
checkbutton_alt = Image.new("RGBA", (134, 134))
draw = ImageDraw.Draw(checkbutton_alt)
draw.rounded_rectangle(
[2, 2, 132, 132],
radius=16,
fill=on_fill,
outline=on_border,
width=3,
)
draw.line([36, 67, 100, 67], fill=check_color, width=12)
alt_img = ImageTk.PhotoImage(
checkbutton_alt.resize(size, Image.LANCZOS)
)
alt_name = util.get_image_name(alt_img)
self.theme_images[alt_name] = alt_img
# checkbutton alt/disabled
checkbutton_alt_disabled = Image.new("RGBA", (134, 134))
draw = ImageDraw.Draw(checkbutton_alt_disabled)
draw.rounded_rectangle(
[2, 2, 132, 132],
radius=16,
fill=disabled_fg,
outline=disabled_fg,
width=3,
)
draw.line([36, 67, 100, 67], fill=off_fill, width=12)
alt_dis_img = ImageTk.PhotoImage(
checkbutton_alt_disabled.resize(size, Image.LANCZOS)
)
alt_dis_name = util.get_image_name(alt_dis_img)
self.theme_images[alt_dis_name] = alt_dis_img
# checkbutton disabled
checkbutton_disabled = Image.new("RGBA", (134, 134))
draw = ImageDraw.Draw(checkbutton_disabled)
draw.rounded_rectangle(
[2, 2, 132, 132], radius=16, outline=disabled_fg, width=3
)
disabled_img = ImageTk.PhotoImage(
checkbutton_disabled.resize(size, Image.LANCZOS)
)
disabled_name = util.get_image_name(disabled_img)
self.theme_images[disabled_name] = disabled_img
return off_name, on_name, disabled_name, alt_name, on_dis_name, alt_dis_name
def create_menubutton_style(self, colorname=DEFAULT):
"""Create a solid style for the ttk.Menubutton widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TMenubutton"
foreground = self.colors.get_foreground(colorname)
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
background = self.colors.primary
else:
ttkstyle = f"{colorname}.{STYLE}"
background = self.colors.get(colorname)
disabled_bg = Colors.make_transparent(0.10, self.colors.fg, self.colors.bg)
disabled_fg = Colors.make_transparent(0.30, self.colors.fg, self.colors.bg)
pressed = Colors.make_transparent(0.80, background, self.colors.bg)
hover = Colors.make_transparent(0.90, background, self.colors.bg)
self.style._build_configure(
ttkstyle,
foreground=foreground,
background=background,
bordercolor=background,
darkcolor=background,
lightcolor=background,
arrowsize=self.scale_size(4),
arrowcolor=foreground,
arrowpadding=(0, 0, 15, 0),
relief=tk.RAISED,
focusthickness=0,
focuscolor=self.colors.selectfg,
padding=(10, 5),
)
self.style.map(
ttkstyle,
arrowcolor=[("disabled", disabled_fg)],
foreground=[("disabled", disabled_fg)],
background=[
("disabled", disabled_bg),
("pressed !disabled", pressed),
("hover !disabled", hover),
],
bordercolor=[
("disabled", disabled_bg),
("pressed !disabled", pressed),
("hover !disabled", hover),
],
darkcolor=[
("disabled", disabled_bg),
("pressed !disabled", pressed),
("hover !disabled", hover),
],
lightcolor=[
("disabled", disabled_bg),
("pressed !disabled", pressed),
("hover !disabled", hover),
],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_outline_menubutton_style(self, colorname=DEFAULT):
"""Create an outline button style for the ttk.Menubutton widget
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "Outline.TMenubutton"
disabled_fg = Colors.make_transparent(0.30, self.colors.fg, self.colors.bg)
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
colorname = PRIMARY
else:
ttkstyle = f"{colorname}.{STYLE}"
foreground = self.colors.get(colorname)
background = self.colors.get_foreground(colorname)
foreground_pressed = background
bordercolor = foreground
pressed = foreground
hover = foreground
self.style._build_configure(
ttkstyle,
foreground=foreground,
background=self.colors.bg,
bordercolor=bordercolor,
darkcolor=self.colors.bg,
lightcolor=self.colors.bg,
relief=tk.RAISED,
focusthickness=0,
focuscolor=foreground,
padding=(10, 5),
arrowcolor=foreground,
arrowpadding=(0, 0, 15, 0),
arrowsize=self.scale_size(4),
)
self.style.map(
ttkstyle,
foreground=[
("disabled", disabled_fg),
("pressed !disabled", foreground_pressed),
("hover !disabled", foreground_pressed),
],
background=[
("pressed !disabled", pressed),
("hover !disabled", hover),
],
bordercolor=[
("disabled", disabled_fg),
("pressed", pressed),
("hover", hover),
],
darkcolor=[
("pressed !disabled", pressed),
("hover !disabled", hover),
],
lightcolor=[
("pressed !disabled", pressed),
("hover !disabled", hover),
],
arrowcolor=[
("disabled", disabled_fg),
("pressed", foreground_pressed),
("hover", foreground_pressed),
],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_notebook_style(self, colorname=DEFAULT):
"""Create a standard style for the ttk.Notebook widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TNotebook"
if self.is_light_theme:
bordercolor = self.colors.border
foreground = self.colors.inputfg
else:
bordercolor = self.colors.selectbg
foreground = self.colors.selectfg
if any([colorname == DEFAULT, colorname == ""]):
background = self.colors.inputbg
selectfg = self.colors.fg
ttkstyle = STYLE
else:
selectfg = self.colors.get_foreground(colorname)
background = self.colors.get(colorname)
ttkstyle = f"{colorname}.{STYLE}"
ttkstyle_tab = f"{ttkstyle}.Tab"
# create widget style
self.style._build_configure(
ttkstyle,
background=self.colors.bg,
bordercolor=bordercolor,
lightcolor=self.colors.bg,
darkcolor=self.colors.bg,
tabmargins=(0, 1, 1, 0),
)
self.style._build_configure(
ttkstyle_tab, focuscolor="", foreground=foreground, padding=(6, 5)
)
self.style.map(
ttkstyle_tab,
background=[
("selected", self.colors.bg),
("!selected", background),
],
lightcolor=[
("selected", self.colors.bg),
("!selected", background),
],
bordercolor=[
("selected", bordercolor),
("!selected", bordercolor),
],
padding=[("selected", (6, 5)), ("!selected", (6, 5))],
foreground=[("selected", foreground), ("!selected", selectfg)],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def create_panedwindow_style(self, colorname=DEFAULT):
"""Create a standard style for the ttk.Panedwindow widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
H_STYLE = "Horizontal.TPanedwindow"
V_STYLE = "Vertical.TPanedwindow"
if self.is_light_theme:
default_color = self.colors.border
else:
default_color = self.colors.selectbg
if any([colorname == DEFAULT, colorname == ""]):
sashcolor = default_color
h_ttkstyle = H_STYLE
v_ttkstyle = V_STYLE
else:
sashcolor = self.colors.get(colorname)
h_ttkstyle = f"{colorname}.{H_STYLE}"
v_ttkstyle = f"{colorname}.{V_STYLE}"
self.style._build_configure(
"Sash", gripcount=0, sashthickness=self.scale_size(2)
)
self.style._build_configure(h_ttkstyle, background=sashcolor)
self.style._build_configure(v_ttkstyle, background=sashcolor)
# register ttkstyle
self.style._register_ttkstyle(h_ttkstyle)
self.style._register_ttkstyle(v_ttkstyle)
def create_sizegrip_assets(self, color):
"""Create image assets used to build the sizegrip style.
Parameters:
color (str):
The color _value_ used to draw the image.
Returns:
str:
The PhotoImage name.
"""
from math import ceil
box = self.scale_size(1)
pad = box * 2
chunk = box + pad # 4
w = chunk * 3 + pad # 14
h = chunk * 3 + pad # 14
size = [w, h]
im = Image.new("RGBA", size)
draw = ImageDraw.Draw(im)
draw.rectangle((chunk * 2 + pad, pad, chunk * 3, chunk), fill=color)
draw.rectangle(
(chunk * 2 + pad, chunk + pad, chunk * 3, chunk * 2), fill=color
)
draw.rectangle(
(chunk * 2 + pad, chunk * 2 + pad, chunk * 3, chunk * 3),
fill=color,
)
draw.rectangle(
(chunk + pad, chunk + pad, chunk * 2, chunk * 2), fill=color
)
draw.rectangle(
(chunk + pad, chunk * 2 + pad, chunk * 2, chunk * 3), fill=color
)
draw.rectangle((pad, chunk * 2 + pad, chunk, chunk * 3), fill=color)
_img = ImageTk.PhotoImage(im)
_name = util.get_image_name(_img)
self.theme_images[_name] = _img
return _name
def create_sizegrip_style(self, colorname=DEFAULT):
"""Create a style for the ttk.Sizegrip widget.
Parameters:
colorname (str):
The color label used to style the widget.
"""
STYLE = "TSizegrip"
if any([colorname == DEFAULT, colorname == ""]):
ttkstyle = STYLE
if self.is_light_theme:
grip_color = self.colors.border
else:
grip_color = self.colors.inputbg
else:
ttkstyle = f"{colorname}.{STYLE}"
grip_color = self.colors.get(colorname)
image = self.create_sizegrip_assets(grip_color)
self.style.element_create(
f"{ttkstyle}.Sizegrip.sizegrip", "image", image
)
self.style.layout(
ttkstyle,
[
(
f"{ttkstyle}.Sizegrip.sizegrip",
{"side": tk.BOTTOM, "sticky": tk.SE},
)
],
)
# register ttkstyle
self.style._register_ttkstyle(ttkstyle)
def update_combobox_popdown_style(self, widget):
"""Update the legacy ttk.Combobox elements. This method is
called every time the theme is changed in order to ensure
that the legacy tkinter components embedded in this ttk widget
are styled appropriate to the current theme.
The ttk.Combobox contains several elements that are not styled
using the ttk theme engine. This includes the **popdownwindow**
and the **scrollbar**. Both of these widgets are configured
manually using calls to tcl/tk.
Parameters:
widget (ttk.Combobox):
The combobox element to be updated.
"""
if self.is_light_theme:
bordercolor = self.colors.border
else:
bordercolor = self.colors.selectbg
tk_settings = []
tk_settings.extend(["-borderwidth", 2])
tk_settings.extend(["-highlightthickness", 1])
tk_settings.extend(["-highlightcolor", bordercolor])
tk_settings.extend(["-background", self.colors.inputbg])
tk_settings.extend(["-foreground", self.colors.inputfg])
tk_settings.extend(["-selectbackground", self.colors.selectbg])
tk_settings.extend(["-selectforeground", self.colors.selectfg])
# set popdown style
popdown = widget.tk.eval(f"ttk::combobox::PopdownWindow {widget}")
widget.tk.call(f"{popdown}.f.l", "configure", *tk_settings)
# set scrollbar style
sb_style = "TCombobox.Vertical.TScrollbar"
widget.tk.call(f"{popdown}.f.sb", "configure", "-style", sb_style)
class Keywords:
# TODO possibly refactor the bootstyle keyword methods into this class?
# Leave for now.
COLORS = [
"primary",
"secondary",
"success",
"info",
"warning",
"danger",
"light",
"dark",
]
ORIENTS = ["horizontal", "vertical"]
TYPES = [
"outline",
"link",
"inverse",
"round",
"square",
"striped",
"focus",
"input",
"date",
"metersubtxt",
"meter",
"table"
]
CLASSES = [
"button",
"progressbar",
"checkbutton",
"combobox",
"entry",
"labelframe",
"label",
"frame",
"floodgauge",
"sizegrip",
"optionmenu",
"menubutton",
"menu",
"notebook",
"panedwindow",
"radiobutton",
"separator",
"scrollbar",
"spinbox",
"scale",
"text",
"toolbutton",
"treeview",
"toggle",
"tk",
"calendar",
"listbox",
"canvas",
"toplevel",
]
COLOR_PATTERN = re.compile("|".join(COLORS))
ORIENT_PATTERN = re.compile("|".join(ORIENTS))
CLASS_PATTERN = re.compile("|".join(CLASSES))
TYPE_PATTERN = re.compile("|".join(TYPES))
class Bootstyle:
@staticmethod
def ttkstyle_widget_class(widget=None, string=""):
"""Find and return the widget class
Parameters:
widget (Widget):
The widget object.
string (str):
A keyword string to parse.
Returns:
str:
A widget class keyword.
"""
# find widget class from string pattern
match = re.search(Keywords.CLASS_PATTERN, string.lower())
if match is not None:
widget_class = match.group(0)
return widget_class
# find widget class from tkinter/tcl method
if widget is None:
return ""
_class = widget.winfo_class()
match = re.search(Keywords.CLASS_PATTERN, _class.lower())
if match is not None:
widget_class = match.group(0)
return widget_class
else:
return ""
@staticmethod
def ttkstyle_widget_type(string):
"""Find and return the widget type.
Parameters:
string (str):
A keyword string to parse.
Returns:
str:
A widget type keyword.
"""
match = re.search(Keywords.TYPE_PATTERN, string.lower())
if match is None:
return ""
else:
widget_type = match.group(0)
return widget_type
@staticmethod
def ttkstyle_widget_orient(widget=None, string="", **kwargs):
"""Find and return widget orient, or default orient for widget if
a widget with orientation.
Parameters:
widget (Widget):
The widget object.
string (str):
A keyword string to parse.
**kwargs:
Optional keyword arguments passed in the widget constructor.
Returns:
str:
A widget orientation keyword.
"""
# string method (priority)
match = re.search(Keywords.ORIENT_PATTERN, string)
widget_orient = ""
if match is not None:
widget_orient = match.group(0)
return widget_orient
# orient from kwargs
if "orient" in kwargs:
_orient = kwargs.pop("orient")
if _orient == "h":
widget_orient = "horizontal"
elif _orient == "v":
widget_orient = "vertical"
else:
widget_orient = _orient
return widget_orient
# orient from settings
if widget is None:
return widget_orient
try:
widget_orient = str(widget.cget("orient"))
except:
pass
return widget_orient
@staticmethod
def ttkstyle_widget_color(string):
"""Find and return widget color
Parameters:
string (str):
A keyword string to parse.
Returns:
str:
A color keyword.
"""
_color = re.search(Keywords.COLOR_PATTERN, string.lower())
if _color is None:
return ""
else:
widget_color = _color.group(0)
return widget_color
@staticmethod
def ttkstyle_name(widget=None, string="", **kwargs):
"""Parse a string to build and return a ttkstyle name.
Parameters:
widget (Widget):
The widget object.
string (str):
A keyword string to parse.
**kwargs:
Other keyword arguments to parse widget orientation.
Returns:
str:
A ttk style name
"""
style_string = "".join(string).lower()
widget_color = Bootstyle.ttkstyle_widget_color(style_string)
widget_type = Bootstyle.ttkstyle_widget_type(style_string)
widget_orient = Bootstyle.ttkstyle_widget_orient(
widget, style_string, **kwargs
)
widget_class = Bootstyle.ttkstyle_widget_class(widget, style_string)
if widget_color:
widget_color = f"{widget_color}."
if widget_type:
widget_type = f"{widget_type.title()}."
if widget_orient:
widget_orient = f"{widget_orient.title()}."
if widget_class.startswith("t"):
widget_class = widget_class.title()
else:
widget_class = f"T{widget_class.title()}"
ttkstyle = f"{widget_color}{widget_type}{widget_orient}{widget_class}"
return ttkstyle
@staticmethod
def ttkstyle_method_name(widget=None, string=""):
"""Parse a string to build and return the name of the
`StyleBuilderTTK` method that creates the ttk style for the
target widget.
Parameters:
widget (Widget):
The widget object to lookup.
string (str):
The keyword string to parse.
Returns:
str:
The name of the update method used to update the widget.
"""
style_string = "".join(string).lower()
widget_type = Bootstyle.ttkstyle_widget_type(style_string)
widget_class = Bootstyle.ttkstyle_widget_class(widget, style_string)
if widget_type:
widget_type = f"_{widget_type}"
if widget_class:
widget_class = f"_{widget_class}"
if not widget_type and not widget_class:
return ""
else:
method_name = f"create{widget_type}{widget_class}_style"
return method_name
@staticmethod
def tkupdate_method_name(widget):
"""Lookup the tkinter style update method from the widget class
Parameters:
widget (Widget):
The widget object to lookup.
Returns:
str:
The name of the method used to update the widget object.
"""
widget_class = Bootstyle.ttkstyle_widget_class(widget)
if widget_class:
widget_class = f"_{widget_class}"
method_name = f"update{widget_class}_style"
return method_name
@staticmethod
def override_ttk_widget_constructor(func):
"""Override widget constructors with bootstyle api options.
Parameters:
func (Callable):
The widget class `__init__` method
"""
def __init__(self, *args, **kwargs):
# capture bootstyle and style arguments
if "bootstyle" in kwargs:
bootstyle = kwargs.pop("bootstyle")
else:
bootstyle = ""
if "style" in kwargs:
style = kwargs.pop("style") or ""
else:
style = ""
# instantiate the widget
func(self, *args, **kwargs)
# must be called AFTER instantiation in order to use winfo_class
# in the `get_ttkstyle_name` method
if style:
if Style.get_instance().style_exists_in_theme(style):
self.configure(style=style)
else:
ttkstyle = Bootstyle.update_ttk_widget_style(
self, style, **kwargs
)
self.configure(style=ttkstyle)
elif bootstyle:
ttkstyle = Bootstyle.update_ttk_widget_style(
self, bootstyle, **kwargs
)
self.configure(style=ttkstyle)
else:
ttkstyle = Bootstyle.update_ttk_widget_style(
self, "default", **kwargs
)
self.configure(style=ttkstyle)
return __init__
@staticmethod
def override_ttk_widget_configure(func):
"""Overrides the configure method on a ttk widget.
Parameters:
func (Callable):
The widget class `configure` method
"""
def configure(self, cnf=None, **kwargs):
# get configuration
if cnf in ("bootstyle", "style"):
return self.cget("style")
if cnf is not None:
return func(self, cnf)
# set configuration
if "bootstyle" in kwargs:
bootstyle = kwargs.pop("bootstyle")
else:
bootstyle = ""
if "style" in kwargs:
style = kwargs.get("style")
ttkstyle = Bootstyle.update_ttk_widget_style(
self, style, **kwargs
)
elif bootstyle:
ttkstyle = Bootstyle.update_ttk_widget_style(
self, bootstyle, **kwargs
)
kwargs.update(style=ttkstyle)
# update widget configuration
func(self, cnf, **kwargs)
return configure
@staticmethod
def update_ttk_widget_style(
widget: ttk.Widget = None, style_string: str = None, **kwargs
):
"""Update the ttk style or create if not existing.
Parameters:
widget (ttk.Widget):
The widget instance being updated.
style_string (str):
The style string to evalulate. May be the `style`, `ttkstyle`
or `bootstyle` argument depending on the context and scenario.
**kwargs:
Other optional keyword arguments.
Returns:
str:
The ttkstyle or empty string if there is none.
"""
style: Style = Style.get_instance() or Style()
# get existing widget style if not provided
if style_string is None:
style_string = widget.cget("style")
# do nothing if the style has not been set
if not style_string:
return ""
if style_string == '.':
return '.'
# build style if not existing (example: theme changed)
ttkstyle = Bootstyle.ttkstyle_name(widget, style_string, **kwargs)
if not style.style_exists_in_theme(ttkstyle):
widget_color = Bootstyle.ttkstyle_widget_color(ttkstyle)
method_name = Bootstyle.ttkstyle_method_name(widget, ttkstyle)
builder: StyleBuilderTTK = style._get_builder()
builder_method = builder.name_to_method(method_name)
builder_method(builder, widget_color)
# subscribe popdown style to theme changes
try:
if widget.winfo_class() == "TCombobox":
builder: StyleBuilderTTK = style._get_builder()
winfo_id = hex(widget.winfo_id())
winfo_pathname = widget.winfo_pathname(winfo_id)
Publisher.subscribe(
name=winfo_pathname,
func=lambda w=widget: builder.update_combobox_popdown_style(
w
),
channel=Channel.STD,
)
builder.update_combobox_popdown_style(widget)
except:
pass
return ttkstyle
@staticmethod
def setup_ttkbootstap_api():
"""Setup ttkbootstrap for use with tkinter and ttk. This method
is called when ttkbootstrap is imported to perform all of the
necessary method overrides that implement the bootstyle api."""
from ttkbootstrap.widgets import TTK_WIDGETS
from ttkbootstrap.widgets import TK_WIDGETS
# TTK WIDGETS
for widget in TTK_WIDGETS:
try:
# override widget constructor
_init = Bootstyle.override_ttk_widget_constructor(
widget.__init__
)
widget.__init__ = _init
# override configure method
_configure = Bootstyle.override_ttk_widget_configure(
widget.configure
)
widget.configure = _configure
widget.config = widget.configure
# override get and set methods
_orig_getitem = widget.__getitem
_orig_setitem = widget.__setitem
def __setitem(self, key, val):
if key in ("bootstyle", "style"):
return _configure(self, **{key: val})
return _orig_setitem(key, val)
def __getitem(self, key):
if key in ("bootstyle", "style"):
return _configure(self, cnf=key)
return _orig_getitem(key)
if (
widget.__name__ != "OptionMenu"
): # this has it's own override
widget.__setitem__ = __setitem
widget.__getitem__ = __getitem
except:
# this may fail in python 3.6 for ttk widgets that do not exist
# in that version.
continue
# TK WIDGETS
for widget in TK_WIDGETS:
# override widget constructor
_init = Bootstyle.override_tk_widget_constructor(widget.__init__)
widget.__init__ = _init
@staticmethod
def update_tk_widget_style(widget):
"""Lookup the widget name and call the appropriate update
method
Parameters:
widget (object):
The tcl/tk name given by `tkinter.Widget.winfo_name()`
"""
try:
style = Style.get_instance()
method_name = Bootstyle.tkupdate_method_name(widget)
builder = style._get_builder_tk()
builder_method = getattr(StyleBuilderTK, method_name)
builder_method(builder, widget)
except:
"""Must pass here to prevent a failure when the user calls
the `Style`method BEFORE an instance of `Tk` is instantiated.
This will defer the update of the `Tk` background until the end
of the `BootStyle` object instantiation (created by the `Style`
method)"""
pass
@staticmethod
def override_tk_widget_constructor(func):
"""Override widget constructors to apply default style for tk
widgets.
Parameters:
func (Callable):
The `__init__` method for this widget.
"""
def __init__wrapper(self, *args, **kwargs):
# check for autostyle flag
if "autostyle" in kwargs:
autostyle = kwargs.pop("autostyle")
else:
autostyle = True
# instantiate the widget
func(self, *args, **kwargs)
if autostyle:
Publisher.subscribe(
name=str(self),
func=lambda w=self: Bootstyle.update_tk_widget_style(w),
channel=Channel.STD,
)
Bootstyle.update_tk_widget_style(self)
return __init__wrapper