Source code for ttkbootstrap

"""
    Why does this project exist?
    ============================
    The purpose of this project is create a set of beautifully designed and easy to apply styles for your tkinter
    applications. Ttk can be very time-consuming to style if you are just a casual user. This project takes the pain
    out of getting a modern look and feel so that you can focus on designing your application. This project was created
    to harness the power of ttk's (and thus Python's) existing built-in theme engine to create modern and
    professional-looking user interfaces which are inspired by, and in many cases, whole-sale rip-off's of the themes
    found on Bootswatch_. Even better, you have the abilty to :ref:`create and use your own custom
    themes <tutorial:create a new theme>` using TTK Creator.

    A bootstrap approach to style
    =============================
    Many people are familiar with bootstrap for web developement. It comes pre-packaged with built-in css style classes
    that provide a professional and consistent api for quick development. I took a similar approach with this project
    by pre-defining styles for nearly all ttk widgets. This makes is very easy to apply the theme colors to various
    widgets by using style declarations. If you want a button in the `secondary` theme color, simply apply the
    **secondary.TButton** style to the button. Want a blue outlined button? Apply the **info.Outline.TButton** style to
    the button.

    What about the old tkinter widgets?
    ===================================
    Some of the ttk widgets utilize existing tkinter widgets. For example: there is a tkinter popdown list in the
    ``ttk.Combobox`` and a legacy tkinter widget inside the ``ttk.OptionMenu``. To make sure these widgets didn't stick
    out like a sore thumb, I created a ``StyleTK`` class to apply the same color and style to these legacy widgets.
    While these legacy widgets are not necessarily intended to be used (and will probably not look as nice as the ttk
    versions when they exist), they are available if needed, and shouldn't look completely out-of-place in your
    ttkbootstrap themed application.  :ref:`Check out this example <themes:legacy widget styles>` to see for yourself.

    .. _Bootswatch: https://bootswatch.com/

"""
import colorsys
import json
from pathlib import Path
from ttkbootstrap.themes import STANDARD_THEMES
from ttkbootstrap.themes_custom import USER_THEMES
from tkinter import ttk

from PIL import ImageTk, Image, ImageDraw, ImageFont


[docs]class Style(ttk.Style): """A class for setting the application style. Sets the theme of the ``tkinter.Tk`` instance and supports all ttkbootstrap and ttk themes provided. This class is meant to be a drop-in replacement for ``ttk.Style`` and inherits all of it's methods and properties. Creating a ``Style`` object will instantiate the ``tkinter.Tk`` instance in the ``Style.master`` property, and so it is not necessary to explicitly create an instance of ``tkinter.Tk``. For more details on the ``ttk.Style`` class, see the python documentation_. .. code-block:: python # instantiate the style with default theme *flatly* style = Style() # instantiate the style with another theme style = Style(theme='superhero') # instantiate the style with a theme from a specific themes file style = Style(theme='custom_name', themes_file='C:/example/my_themes.json') # available themes for theme in style.theme_names(): print(theme) .. _documentation: https://docs.python.org/3.9/library/tkinter.ttk.html#tkinter.ttk.Style """ def __init__(self, theme='flatly', themes_file=None, *args, **kwargs): """ Args: theme (str): the name of the theme to use at runtime; *flatly* by default. themes_file (str): Path to a user-defined themes file. Defaults to the themes file set in ttkcreator. """ super().__init__(*args, **kwargs) self._styler = None self._theme_names = set(self.theme_names()) self._theme_objects = {} # used to prevent garbage collection of theme assets when changing themes at runtime self._theme_definitions = {} self._load_themes(themes_file) # load selected or default theme self.theme_use(themename=theme) @property def colors(self): theme = self.theme_use() if theme in list(self._theme_names): return self._theme_definitions.get(theme).colors else: return Colors() def _load_themes(self, themes_file=None): """Load all ttkbootstrap defined themes Args: themes_file (str): the path of the `themes.json` file. """ # application-defined or user-defined themes if themes_file: user_path = Path(themes_file) if user_path.exists(): with user_path.open(encoding='utf-8') as f: user_themes = json.load(f) else: user_themes = {} else: user_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 theme_settings = {**STANDARD_THEMES, **user_themes, **USER_THEMES} for name, settings in theme_settings.items(): self.register_theme( ThemeDefinition( name=name, themetype=settings['type'], colors=Colors(**settings['colors'])))
[docs] def register_theme(self, definition): """Registers a theme definition for use by the ``Style`` class. This makes the definition and name available at run-time so that the assets and styles can be created. Args: definition (ThemeDefinition): an instance of the ``ThemeDefinition`` class """ self._theme_names.add(definition.name) self._theme_definitions[definition.name] = definition
[docs] 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. Keyword Args: themename (str): the theme to apply when creating new widgets """ if not themename: return super().theme_use() self.theme = self._theme_definitions.get(themename) if all([themename, themename not in self._theme_names]): print(f"{themename} is not a valid theme name. Please try one of the following:") print(list(self._theme_names)) return if themename in self.theme_names(): # the theme has already been created in tkinter super().theme_use(themename) if not self.theme: # this is not a bootstrap theme # self._theme_definitions[themename] = ThemeDefinition() return return # theme has not yet been created self._theme_objects[themename] = StylerTTK(self, self.theme) self._theme_objects[themename].styler_tk.style_tkinter_widgets() super().theme_use(themename) return
[docs]class ThemeDefinition: """A class to provide defined name, colors, and font settings for a ttkbootstrap theme.""" def __init__(self, name='default', themetype='light', font='helvetica', colors=None): """ Args: name (str): the name of the theme; default is 'default'. themetype (str): the type of theme: *light* or *dark*; default is 'light'. font (str): the default font to use for the application; default is 'helvetica'. colors (Colors): an instance of the `Colors` class. One is provided by default. """ self.name = name self.type = themetype # self.font = font self.colors = colors if colors else Colors() def __repr__(self): return f'name={self.name}, type={self.type}, colors={self.colors}'
[docs]class Colors: """A class that contains the theme colors as well as several helper methods for manipulating colors. This class is attached to the ``Style`` object at run-time for the selected theme, and so is available to use with ``Style.colors``. The colors can be accessed via dot notation or get method: .. code-block:: python # dot-notation Colors.primary # get method Colors.get('primary') This class is an iterator, so you can iterate over the main style color labels (primary, secondary, success, info, warning, danger): .. code-block:: python for color_label in Colors: color = 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, including border, fg, bg, etc... .. code-block:: python for color_label in Colors.label_iter(): color = Colors.get(color_label) print(color_label, color) """ def __init__(self, primary, secondary, success, info, warning, danger, bg, fg, selectbg, selectfg, border, inputfg, inputbg, light='#ddd', dark='#333'): """ Args: 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. 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: ie. ``Entry``, ``Combobox``, etc... inputbg (str): the text background color for input widgets. light (str): a light color dark (str): a dark color """ self.primary = primary self.secondary = secondary self.success = success self.info = info self.warning = warning self.danger = danger self.bg = bg self.fg = fg self.selectbg = selectbg self.selectfg = selectfg self.border = border self.inputfg = inputfg self.inputbg = inputbg self.light = light self.dark = dark
[docs] def get(self, color_label): """Lookup a color property Args: color_label (str): a color label corresponding to a class propery (primary, secondary, success, etc...) Returns: str: a hexadecimal color value. """ return self.__dict__.get(color_label)
[docs] def set(self, color_label, color_value): """Set a color property Args: color_label (str): the name of the color to be set (key) color_value (str): a hexadecimal color value Example: .. code-block: set('primary', '#fafafa') """ self.__dict__[color_label] = color_value
def __iter__(self): return iter(['primary', 'secondary', 'success', 'info', 'warning', 'danger']) def __repr__(self): return str((tuple(zip(self.__dict__.keys(), self.__dict__.values()))))
[docs] @staticmethod def label_iter(): """Iterate over all color label properties in the Color class Returns: iter: an iterator representing the name of the color properties """ return iter(['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'bg', 'fg', 'selectbg', 'selectfg', 'border', 'inputfg', 'inputbg', 'light', 'dark'])
[docs] @staticmethod def hex_to_rgb(color): """Convert hexadecimal color to rgb color value Args: color (str): param str color: hexadecimal color value Returns: tuple[int, int, int]: rgb color value. """ if len(color) == 4: # 3 digit hexadecimal colors r = int(color[1], 16) / 15 g = int(color[2], 16) / 15 b = int(color[3], 16) / 15 else: # 6 digit hexadecimal colors r = int(color[1:3], 16) / 255 g = int(color[3:5], 16) / 255 b = int(color[5:], 16) / 255 return r, g, b
[docs] @staticmethod def rgb_to_hex(r, g, b): """Convert rgb to hexadecimal color value Args: r (int): red g (int): green b (int): blue Returns: str: a hexadecimal colorl value """ r_ = int(r * 255) g_ = int(g * 255) b_ = int(b * 255) return '#{:02x}{:02x}{:02x}'.format(r_, g_, b_)
[docs] @staticmethod def update_hsv(color, hd=0, sd=0, vd=0): """Modify the hue, saturation, and/or value of a given hex color value. Args: color (str): the hexadecimal color value that is the target of hsv changes. hd (float): % change in hue sd (float): % change in saturation vd (float): % change in value Returns: str: a new hexadecimal color value that results from the hsv arguments passed into the function """ 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)
[docs]class StylerTK: """A class for styling tkinter widgets (not ttk). Several ttk widgets utilize tkinter widgets in some capacity, such as the `popdownlist` on the ``ttk.Combobox``. To create a consistent user experience, standard tkinter widgets are themed as much as possible with the look and feel of the **ttkbootstrap** theme applied. Tkinter widgets are not the primary target of this project; however, they can be used without looking entirely out-of-place in most cases. Attributes: master (Tk): the root window. theme (ThemeDefinition): the color settings defined in the `themes.json` file. """ def __init__(self, styler_ttk): """ Args: styler_ttk (StylerTTK): an instance of the ``StylerTTK`` class. """ self.master = styler_ttk.style.master self.theme = styler_ttk.theme
[docs] def style_tkinter_widgets(self): """A wrapper on all widget style methods. Applies current theme to all standard tkinter widgets""" self._style_spinbox() self._style_textwidget() self._style_button() self._style_label() self._style_checkbutton() self._style_radiobutton() self._style_entry() self._style_scale() self._style_listbox() self._style_menu() self._style_menubutton() self._style_labelframe() self._style_canvas() self._style_window()
def _set_option(self, *args): """A convenience wrapper method to shorten the call to ``option_add``. *Laziness is next to godliness*. Args: *args (Tuple[str]): (pattern, value, priority=80) """ self.master.option_add(*args) def _style_window(self): """Apply global options to all matching ``tkinter`` widgets""" self.master.configure(background=self.theme.colors.bg) self._set_option('*background', self.theme.colors.bg, 60) # self._set_option('*font', self.theme.font, 60) self._set_option('*activeBackground', self.theme.colors.selectbg, 60) self._set_option('*activeForeground', self.theme.colors.selectfg, 60) self._set_option('*selectBackground', self.theme.colors.selectbg, 60) self._set_option('*selectForeground', self.theme.colors.selectfg, 60) def _style_canvas(self): """Apply style to ``tkinter.Canvas``""" self._set_option('*Canvas.highlightThickness', 1) self._set_option('*Canvas.highlightBackground', self.theme.colors.border) self._set_option('*Canvas.background', self.theme.colors.bg) def _style_button(self): """Apply style to ``tkinter.Button``""" active_bg = Colors.update_hsv(self.theme.colors.primary, vd=-0.2) self._set_option('*Button.relief', 'flat') self._set_option('*Button.borderWidth', 0) self._set_option('*Button.activeBackground', active_bg) self._set_option('*Button.foreground', self.theme.colors.selectfg) self._set_option('*Button.background', self.theme.colors.primary) def _style_label(self): """Apply style to ``tkinter.Label``""" self._set_option('*Label.foreground', self.theme.colors.fg) self._set_option('*Label.background', self.theme.colors.bg) def _style_checkbutton(self): """Apply style to ``tkinter.Checkbutton``""" self._set_option('*Checkbutton.activeBackground', self.theme.colors.bg) self._set_option('*Checkbutton.activeForeground', self.theme.colors.primary) self._set_option('*Checkbutton.background', self.theme.colors.bg) self._set_option('*Checkbutton.foreground', self.theme.colors.fg) self._set_option('*Checkbutton.selectColor', self.theme.colors.primary if self.theme.type == 'dark' else 'white') def _style_radiobutton(self): """Apply style to ``tkinter.Radiobutton``""" self._set_option('*Radiobutton.activeBackground', self.theme.colors.bg) self._set_option('*Radiobutton.activeForeground', self.theme.colors.primary) self._set_option('*Radiobutton.background', self.theme.colors.bg) self._set_option('*Radiobutton.foreground', self.theme.colors.fg) self._set_option('*Radiobutton.selectColor', self.theme.colors.primary if self.theme.type == 'dark' else 'white') def _style_entry(self): """Apply style to ``tkinter.Entry``""" self._set_option('*Entry.relief', 'flat') self._set_option('*Entry.background', (self.theme.colors.inputbg if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.inputbg, vd=-0.1))) self._set_option('*Entry.foreground', self.theme.colors.inputfg) self._set_option('*Entry.highlightThickness', 1) self._set_option('*Entry.highlightBackground', self.theme.colors.border) self._set_option('*Entry.highlightColor', self.theme.colors.primary) def _style_scale(self): """Apply style to ``tkinter.Scale``""" active_color = Colors.update_hsv(self.theme.colors.primary, vd=-0.2) self._set_option('*Scale.background', self.theme.colors.primary) self._set_option('*Scale.showValue', False) self._set_option('*Scale.sliderRelief', 'flat') self._set_option('*Scale.borderWidth', 0) self._set_option('*Scale.activeBackground', active_color) self._set_option('*Scale.highlightThickness', 1) self._set_option('*Scale.highlightColor', self.theme.colors.border) self._set_option('*Scale.highlightBackground', self.theme.colors.border) self._set_option('*Scale.troughColor', self.theme.colors.inputbg) def _style_spinbox(self): """Apply style to `tkinter.Spinbox``""" self._set_option('*Spinbox.foreground', self.theme.colors.inputfg) self._set_option('*Spinbox.relief', 'flat') self._set_option('*Spinbox.background', (self.theme.colors.inputbg if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.inputbg, vd=-0.1))) self._set_option('*Spinbox.highlightThickness', 1) self._set_option('*Spinbox.highlightColor', self.theme.colors.primary) self._set_option('*Spinbox.highlightBackground', self.theme.colors.border) def _style_listbox(self): """Apply style to ``tkinter.Listbox``""" self._set_option('*Listbox.foreground', self.theme.colors.inputfg) self._set_option('*Listbox.background', self.theme.colors.inputbg) self._set_option('*Listbox.selectBackground', self.theme.colors.selectbg) self._set_option('*Listbox.selectForeground', self.theme.colors.selectfg) self._set_option('*Listbox.relief', 'flat') self._set_option('*Listbox.activeStyle', 'none') self._set_option('*Listbox.highlightThickness', 1) self._set_option('*Listbox.highlightColor', self.theme.colors.primary) self._set_option('*Listbox.highlightBackground', self.theme.colors.border) def _style_menubutton(self): """Apply style to ``tkinter.Menubutton``""" hover_color = Colors.update_hsv(self.theme.colors.primary, vd=-0.2) self._set_option('*Menubutton.activeBackground', hover_color) self._set_option('*Menubutton.background', self.theme.colors.primary) self._set_option('*Menubutton.foreground', self.theme.colors.selectfg) self._set_option('*Menubutton.borderWidth', 0) def _style_menu(self): """Apply style to ``tkinter.Menu``""" self._set_option('*Menu.tearOff', 0) self._set_option('*Menu.foreground', self.theme.colors.fg) self._set_option('*Menu.selectColor', self.theme.colors.primary) # self._set_option('*Menu.font', self.theme.font) self._set_option('*Menu.background', ( self.theme.colors.inputbg if self.theme.type == 'light' else self.theme.colors.bg)) self._set_option('*Menu.activeBackground', self.theme.colors.selectbg) self._set_option('*Menu.activeForeground', self.theme.colors.selectfg) def _style_labelframe(self): """Apply style to ``tkinter.Labelframe``""" # self._set_option('*Labelframe.font', self.theme.font) self._set_option('*Labelframe.foreground', self.theme.colors.fg) self._set_option('*Labelframe.highlightColor', self.theme.colors.border) self._set_option('*Labelframe.borderWidth', 1) self._set_option('*Labelframe.highlightThickness', 0) def _style_textwidget(self): """Apply style to ``tkinter.Text``""" if self.theme.type == 'light': bordercolor = self.theme.colors.border else: bordercolor = self.theme.colors.selectbg self._set_option('*Text.background', self.theme.colors.inputbg) self._set_option('*Text.foreground', self.theme.colors.inputfg) self._set_option('*Text.highlightColor', self.theme.colors.primary) self._set_option('*Text.highlightBackground', bordercolor) self._set_option('*Text.borderColor', bordercolor) self._set_option('*Text.highlightThickness', 1) self._set_option('*Text.relief', 'flat') self._set_option('*Text.font', 'TkDefaultFont') self._set_option('*Text.padX', 5) self._set_option('*Text.padY', 5)
[docs]class StylerTTK: """A class to create a new ttk theme. Create a new ttk theme by using a combination of built-in themes and some image-based elements using ``pillow``. A theme is generated at runtime and is available to use with the ``Style`` class methods. The base theme of all **ttkbootstrap** themes is *clam*. In many cases, widget layouts are re-created using an assortment of elements from various styles such as *clam*, *alt*, *default*, etc... Attributes: theme_images (dict): theme assets used for various widgets. settings (dict): settings used to build the actual theme using the ``theme_create`` method. styler_tk (StylerTk): an object used to style tkinter widgets (not ttk). theme (ThemeDefinition): the theme settings defined in the `themes.json` file. """ def __init__(self, style, definition): """ Args: style (Style): an instance of ``ttk.Style``. definition (ThemeDefinition): an instance of ``ThemeDefinition``; used to create the theme settings. """ self.style = style self.theme = definition self.theme_images = {} self.settings = {} self.styler_tk = StylerTK(self) self.create_theme()
[docs] def create_theme(self): """Create and style a new ttk theme. A wrapper around internal style methods.""" self.update_ttk_theme_settings() self.style.theme_create(self.theme.name, 'clam', self.settings)
[docs] def update_ttk_theme_settings(self): """Update the settings dictionary that is used to create a theme. This is a wrapper on all the `_style_widget` methods which define the layout, configuration, and styling mapping for each ttk widget. """ self._style_labelframe() self._style_spinbox() self._style_scale() self._style_scrollbar() self._style_combobox() self._style_exit_button() self._style_frame() self._style_calendar() self._style_checkbutton() self._style_entry() self._style_label() self._style_meter() self._style_notebook() self._style_flat_notebook() self._style_interactive_notebook() self._style_flat_interactive_notebook() self._style_outline_buttons() self._style_outline_menubutton() self._style_outline_toolbutton() self._style_progressbar() self._style_striped_progressbar() self._style_floodgauge() self._style_radiobutton() self._style_solid_buttons() self._style_link_buttons() self._style_solid_menubutton() self._style_solid_toolbutton() self._style_treeview() self._style_separator() self._style_panedwindow() self._style_roundtoggle_toolbutton() self._style_squaretoggle_toolbutton() self._style_sizegrip() self._style_defaults()
def _style_defaults(self): """Setup the default ``ttk.Style`` configuration. These defaults are applied to any widget that contains these element options. This method should be called *first* before any other style is applied during theme creation. """ self.settings.update({ '.': { 'configure': { 'background': self.theme.colors.bg, 'darkcolor': self.theme.colors.border, 'foreground': self.theme.colors.fg, 'troughcolor': self.theme.colors.bg, 'selectbg': self.theme.colors.selectbg, 'selectfg': self.theme.colors.selectfg, 'selectforeground': self.theme.colors.selectfg, 'selectbackground': self.theme.colors.selectbg, 'fieldbg': 'white', # 'font': self.theme.font, 'borderwidth': 1, 'focuscolor': ''}}}) def _style_combobox(self): """Create style configuration for ``ttk.Combobox``. This element style is created with a layout that combines *clam* and *default* theme elements. The options available in this widget based on this layout include: - Combobox.downarrow: arrowsize, background, bordercolor, relief, arrowcolor - Combobox.field: bordercolor, lightcolor, darkcolor, fieldbackground - Combobox.padding: padding, relief, shiftrelief - Combobox.textarea: font, width .. info:: When the dark theme is used, I used the *spinbox.field* from the *default* theme because the background shines through the corners using the `clam` theme. This is an unfortuate hack to make it look ok. Hopefully there will be a more permanent/better solution in the future. """ disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) # if self.theme.type == 'dark': # self.settings.update({ # 'combo.Spinbox.field': {'element create': ('from', 'default')}}) if self.theme.type == 'light': bordercolor = self.theme.colors.border else: bordercolor = self.theme.colors.selectbg self.settings.update({ 'Combobox.downarrow': {'element create': ('from', 'default')}, 'Combobox.padding': {'element create': ('from', 'clam')}, 'Combobox.textarea': {'element create': ('from', 'clam')}, 'TCombobox': { 'layout': [('combo.Spinbox.field', {'side': 'top', 'sticky': 'we', 'children': [ ('Combobox.downarrow', {'side': 'right', 'sticky': 'ns'}), ('Combobox.padding', {'expand': '1', 'sticky': 'nswe', 'children': [ ('Combobox.textarea', {'sticky': 'nswe'})]})]})], 'configure': { 'bordercolor': bordercolor, 'darkcolor': self.theme.colors.inputbg, 'lightcolor': self.theme.colors.inputbg, 'arrowcolor': self.theme.colors.inputfg, 'foreground': self.theme.colors.inputfg, 'fieldbackground ': self.theme.colors.inputbg, 'background ': self.theme.colors.inputbg, 'relief': 'flat', # 'borderwidth ': 0, # only applies to dark theme border 'padding': 5, 'arrowsize ': 14}, 'map': { 'foreground': [ ('disabled', disabled_fg)], 'bordercolor': [ ('focus !disabled', self.theme.colors.primary), ('hover !disabled', self.theme.colors.bg)], 'lightcolor': [ ('focus !disabled', self.theme.colors.primary), ('hover !disabled', self.theme.colors.primary)], 'darkcolor': [ ('focus !disabled', self.theme.colors.primary), ('hover !disabled', self.theme.colors.primary)], 'arrowcolor': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.inputbg), ('focus !disabled', self.theme.colors.inputfg), ('hover !disabled', self.theme.colors.primary)]}}}) for color in self.theme.colors: self.settings.update({ f'{color}.TCombobox': { 'map': { 'foreground': [ ('disabled', disabled_fg)], 'bordercolor': [ ('focus !disabled', self.theme.colors.get(color)), ('hover !disabled', self.theme.colors.get(color))], 'lightcolor': [ ('focus !disabled', self.theme.colors.get(color)), ('pressed !disabled', self.theme.colors.get(color))], 'darkcolor': [ ('focus !disabled', self.theme.colors.get(color)), ('pressed !disabled', self.theme.colors.get(color))], 'arrowcolor': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.inputbg), ('focus !disabled', self.theme.colors.inputfg), ('hover !disabled', self.theme.colors.primary)]}}}) def _style_separator(self): """Create style configuration for ttk separator: *ttk.Separator*. The default style for light will be border, but dark will be primary, as this makes the most sense for general use. However, all other colors will be available as well through styling. The options available in this widget include: - Separator.separator: orient, background """ # horizontal separator default default_color = self.theme.colors.border if self.theme.type == 'light' else self.theme.colors.selectbg h_im = Image.new('RGB', (40, 1)) draw = ImageDraw.Draw(h_im) draw.rectangle([0, 0, 40, 1], fill=default_color) self.theme_images['hseparator'] = ImageTk.PhotoImage(h_im) self.settings.update({ 'Horizontal.Separator.separator': { 'element create': ('image', self.theme_images['hseparator'])}, 'Horizontal.TSeparator': { 'layout': [ ('Horizontal.Separator.separator', {'sticky': 'ew'})]}}) # horizontal separator variations for color in self.theme.colors: h_im = Image.new('RGB', (40, 1)) draw = ImageDraw.Draw(h_im) draw.rectangle([0, 0, 40, 1], fill=self.theme.colors.get(color)) self.theme_images[f'{color}_hseparator'] = ImageTk.PhotoImage(h_im) self.settings.update({ f'{color}.Horizontal.Separator.separator': { 'element create': ('image', self.theme_images[f'{color}_hseparator'])}, f'{color}.Horizontal.TSeparator': { 'layout': [ (f'{color}.Horizontal.Separator.separator', {'sticky': 'ew'})]}}) # vertical separator default default_color = self.theme.colors.border if self.theme.type == 'light' else self.theme.colors.selectbg v_im = Image.new('RGB', (1, 40)) draw = ImageDraw.Draw(v_im) draw.rectangle([0, 0, 1, 40], fill=default_color) self.theme_images['vseparator'] = ImageTk.PhotoImage(v_im) self.settings.update({ 'Vertical.Separator.separator': { 'element create': ('image', self.theme_images['vseparator'])}, 'Vertical.TSeparator': { 'layout': [ ('Vertical.Separator.separator', {'sticky': 'ns'})]}}) # vertical separator variations for color in self.theme.colors: v_im = Image.new('RGB', (1, 40)) draw = ImageDraw.Draw(v_im) draw.rectangle([0, 0, 1, 40], fill=self.theme.colors.get(color)) self.theme_images[f'{color}_vseparator'] = ImageTk.PhotoImage(v_im) self.settings.update({ f'{color}.Vertical.Separator.separator': { 'element create': ('image', self.theme_images[f'{color}_vseparator'])}, f'{color}.Vertical.TSeparator': { 'layout': [ (f'{color}.Vertical.Separator.separator', {'sticky': 'ns'})]}}) def _style_striped_progressbar(self): """Apply a striped theme to the progressbar""" self.theme_images.update(self._create_striped_progressbar_image('primary')) self.settings.update({ 'Striped.Horizontal.Progressbar.pbar': { 'element create': ('image', self.theme_images['primary_striped_hpbar'], {'width': 20, 'sticky': 'ew'})}, 'Striped.Horizontal.TProgressbar': { 'layout': [ ('Horizontal.Progressbar.trough', {'sticky': 'nswe', 'children': [ ('Striped.Horizontal.Progressbar.pbar', {'side': 'left', 'sticky': 'ns'})]})], 'configure': { 'troughcolor': self.theme.colors.inputbg, 'thickness': 20, 'borderwidth': 1, 'lightcolor': self.theme.colors.border if self.theme.type == 'light' else self.theme.colors.inputbg}}}) for color in self.theme.colors: self.theme_images.update(self._create_striped_progressbar_image(color)) self.settings.update({ f'{color}.Striped.Horizontal.Progressbar.pbar': { 'element create': ( 'image', self.theme_images[f'{color}_striped_hpbar'], {'width': 20, 'sticky': 'ew'})}, f'{color}.Striped.Horizontal.TProgressbar': { 'layout': [ ('Horizontal.Progressbar.trough', {'sticky': 'nswe', 'children': [ (f'{color}.Striped.Horizontal.Progressbar.pbar', {'side': 'left', 'sticky': 'ns'})]})], 'configure': { 'troughcolor': self.theme.colors.inputbg, 'thickness': 20, 'borderwidth': 1, 'lightcolor': self.theme.colors.border if self.theme.type == 'light' else self.theme.colors.inputbg}}}) def _create_striped_progressbar_image(self, colorname): """Create the striped progressbar image and return as a ``PhotoImage`` Args: colorname (str): the color label assigned to the colors property; eg. `primary`, `secondary`, `success`. Returns: dict: a dictionary containing the widget images. """ bar_primary = self.theme.colors.get(colorname) # calculate value of light color brightness = colorsys.rgb_to_hsv(*Colors.hex_to_rgb(bar_primary))[2] if brightness < 0.4: value_delta = 0.3 elif brightness > 0.8: value_delta = 0 else: value_delta = 0.1 bar_secondary = Colors.update_hsv(bar_primary, sd=-0.2, vd=value_delta) # need to check the darkness of the color before setting the secondary # horizontal progressbar h_im = Image.new('RGBA', (100, 100), bar_secondary) draw = ImageDraw.Draw(h_im) draw.polygon([(0, 0), (48, 0), (100, 52), (100, 100), (100, 100)], fill=bar_primary) draw.polygon([(0, 52), (48, 100), (0, 100)], fill=bar_primary) horizontal_img = ImageTk.PhotoImage(h_im.resize((22, 22), Image.LANCZOS)) # TODO vertical progressbar return {f'{colorname}_striped_hpbar': horizontal_img} def _style_progressbar(self): """Create style configuration for ttk progressbar: *ttk.Progressbar* The options available in this widget include: - Progressbar.trough: borderwidth, troughcolor, troughrelief - Progressbar.pbar: orient, thickness, barsize, pbarrelief, borderwidth, background """ self.settings.update({ 'Progressbar.trough': {'element create': ('from', 'clam')}, 'Progressbar.pbar': {'element create': ('from', 'default')}, 'TProgressbar': {'configure': { 'thickness': 20, 'borderwidth': 1, 'bordercolor': self.theme.colors.border if self.theme.type == 'light' else self.theme.colors.inputbg, 'lightcolor': self.theme.colors.border, 'pbarrelief': 'flat', 'troughcolor': self.theme.colors.inputbg, 'background': self.theme.colors.primary}}}) for color in self.theme.colors: self.settings.update({ f'{color}.Horizontal.TProgressbar': { 'configure': { 'background': self.theme.colors.get(color)}}, f'{color}.Vertical.TProgressbar': { 'configure': { 'background': self.theme.colors.get(color)}}}) @staticmethod def _create_slider_image(color, size=16): """Create a circle slider image based on given size and color; used in the slider widget. Args: color (str): a hexadecimal color value. size (int): the size diameter of the slider circle; default=16. Returns: ImageTk.PhotoImage: an image drawn in the shape of the circle of the theme color specified. """ im = Image.new('RGBA', (100, 100)) draw = ImageDraw.Draw(im) draw.ellipse((0, 0, 95, 95), fill=color) return ImageTk.PhotoImage(im.resize((size, size), Image.LANCZOS)) def _style_scale(self): """Create style configuration for ttk scale: *ttk.Scale* The options available in this widget include: - Scale.trough: borderwidth, troughcolor, troughrelief - Scale.slider: sliderlength, sliderthickness, sliderrelief, borderwidth, background, bordercolor, orient """ disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) if self.theme.type == 'light': trough_color = self.theme.colors.light else: trough_color = Colors.update_hsv(self.theme.colors.selectbg, vd=-0.2) pressed_vd = -0.2 hover_vd = -0.1 # create widget images self.theme_images.update({ 'primary_disabled': self._create_slider_image(disabled_fg), 'primary_regular': self._create_slider_image(self.theme.colors.primary), 'primary_pressed': self._create_slider_image( Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), 'primary_hover': self._create_slider_image( Colors.update_hsv(self.theme.colors.primary, vd=hover_vd)), 'htrough': ImageTk.PhotoImage(Image.new('RGB', (40, 8), trough_color)), 'vtrough': ImageTk.PhotoImage(Image.new('RGB', (8, 40), trough_color))}) # The layout is derived from the 'xpnative' theme self.settings.update({ 'Horizontal.TScale': { 'layout': [ ('Scale.focus', {'expand': '1', 'sticky': 'nswe', 'children': [ ('Horizontal.Scale.track', {'sticky': 'we'}), ('Horizontal.Scale.slider', {'side': 'left', 'sticky': ''})]})]}, 'Vertical.TScale': { 'layout': [ ('Scale.focus', {'expand': '1', 'sticky': 'nswe', 'children': [ ('Vertical.Scale.track', {'sticky': 'ns'}), ('Vertical.Scale.slider', {'side': 'top', 'sticky': ''})]})]}, 'Horizontal.Scale.track': {'element create': ('image', self.theme_images['htrough'])}, 'Vertical.Scale.track': {'element create': ('image', self.theme_images['vtrough'])}, 'Scale.slider': { 'element create': ('image', self.theme_images['primary_regular'], ('disabled', self.theme_images['primary_disabled']), ('pressed !disabled', self.theme_images['primary_pressed']), ('hover !disabled', self.theme_images['primary_hover']))}}) for color in self.theme.colors: self.theme_images.update({ f'{color}_regular': self._create_slider_image(self.theme.colors.get(color)), f'{color}_pressed': self._create_slider_image( Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), f'{color}_hover': self._create_slider_image( Colors.update_hsv(self.theme.colors.get(color), vd=hover_vd))}) # The layout is derived from the 'xpnative' theme self.settings.update({ f'{color}.Horizontal.TScale': { 'layout': [ ('Scale.focus', { 'expand': '1', 'sticky': 'nswe', 'children': [ ('Horizontal.Scale.track', {'sticky': 'we'}), (f'{color}.Scale.slider', {'side': 'left', 'sticky': ''})]})]}, f'{color}.Vertical.TScale': { 'layout': [ (f'{color}.Scale.focus', { 'expand': '1', 'sticky': 'nswe', 'children': [ ('Vertical.Scale.track', {'sticky': 'ns'}), (f'{color}.Scale.slider', {'side': 'top', 'sticky': ''})]})]}, f'{color}.Scale.slider': { 'element create': ('image', self.theme_images[f'{color}_regular'], ('disabled', self.theme_images['primary_disabled']), ('pressed', self.theme_images[f'{color}_pressed']), ('hover', self.theme_images[f'{color}_hover']))}}) def _style_scrollbar(self): """Create style configuration for ttk scrollbar: *ttk.Scrollbar*. This theme uses elements from the *alt* theme tobuild the widget layout. The options available in this widget include: - Scrollbar.trough: orient, troughborderwidth, troughcolor, troughrelief, groovewidth - Scrollbar.uparrow: arrowsize, background, bordercolor, relief, arrowcolor - Scrollbar.downarrow: arrowsize, background, bordercolor, relief, arrowcolor - Scrollbar.thumb: width, background, bordercolor, relief, orient """ self._create_scrollbar_images() if self.theme.type == 'light': trough_color = self.theme.colors.light else: trough_color = Colors.update_hsv(self.theme.colors.selectbg, vd=-0.2) self.settings.update({ 'Vertical.Scrollbar.trough': { 'element create': ('from', 'alt')}, 'Vertical.Scrollbar.thumb': { 'element create': ('from', 'alt')}, 'Vertical.Scrollbar.uparrow': { 'element create': ('from', 'default')}, 'Vertical.Scrollbar.downarrow': { 'element create': ('from', 'default')}, 'Horizontal.Scrollbar.trough': { 'element create': ('from', 'alt')}, 'Horizontal.Scrollbar.thumb': { 'element create': ('from', 'alt')}, 'Horizontal.Scrollbar.leftarrow': { 'element create': ('from', 'default')}, 'Horizontal.Scrollbar.rightarrow': { 'element create': ('from', 'default')}, 'TScrollbar': { 'configure': { 'troughrelief': 'flat', 'relief': 'flat', 'troughborderwidth': 1, 'troughcolor': trough_color, 'arrowcolor': self.theme.colors.fg, 'background': Colors.update_hsv(self.theme.colors.bg, vd=-0.15) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.selectbg, vd=0.25, sd=-0.1), 'width': 16}, 'map': { 'background': [ ('pressed', Colors.update_hsv(self.theme.colors.bg, vd=-0.35) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.selectbg, vd=0.05)), ('active', Colors.update_hsv(self.theme.colors.bg, vd=-0.25) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.selectbg, vd=0.15))]}}}) def _create_scrollbar_images(self): """Create assets needed for scrollbar arrows. The assets are saved to the ``theme_images`` property.""" winsys = self.style.tk.call("tk", "windowingsystem") if winsys == "win32": fnt = ImageFont.truetype("seguisym.ttf", 12) elif winsys == "x11": fnt = ImageFont.truetype("FreeSerif.ttf", 13) else: fnt = ImageFont.truetype("Apple Symbols.ttf", 13) # up arrow vs_upim = Image.new('RGBA', (13, 13)) up_draw = ImageDraw.Draw(vs_upim) up_draw.text((1, 1), "▲", font=fnt, fill=self.theme.colors.inputfg if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.selectbg, vd=0.35, sd=-0.1)) self.theme_images['vsup'] = ImageTk.PhotoImage(vs_upim) # down arrow vsdown_im = Image.new('RGBA', (13, 13)) down_draw = ImageDraw.Draw(vsdown_im) down_draw.text((1, 1), "▼", font=fnt, fill=self.theme.colors.inputfg if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.selectbg, vd=0.35, sd=-0.1)) self.theme_images['vsdown'] = ImageTk.PhotoImage(vsdown_im) # left arrow hs_lfim = Image.new('RGBA', (13, 13)) up_draw = ImageDraw.Draw(hs_lfim) up_draw.text((0, -1), "◀", font=fnt, fill=self.theme.colors.inputfg if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.selectbg, vd=0.35, sd=-0.1)) self.theme_images['hsleft'] = ImageTk.PhotoImage(hs_lfim) # right arrow hs_rtim = Image.new('RGBA', (13, 13)) up_draw = ImageDraw.Draw(hs_rtim) up_draw.text((4, -1), "▶", font=fnt, fill=self.theme.colors.inputfg if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.selectbg, vd=0.35, sd=-0.1)) self.theme_images['hsright'] = ImageTk.PhotoImage(hs_rtim) def _style_floodgauge(self): """Create a style configuration for the *ttk.Progressbar* that makes it into a floodgauge. Which is essentially a very large progress bar with text in the middle. The options available in this widget include: - Floodgauge.trough: borderwidth, troughcolor, troughrelief - Floodgauge.pbar: orient, thickness, barsize, pbarrelief, borderwidth, background - Floodgauge.text: 'text', 'font', 'foreground', 'underline', 'width', 'anchor', 'justify', 'wraplength', 'embossed' """ self.settings.update({ 'Floodgauge.trough': {'element create': ('from', 'clam')}, 'Floodgauge.pbar': {'element create': ('from', 'default')}, 'Horizontal.TFloodgauge': { 'layout': [('Floodgauge.trough', {'children': [ ('Floodgauge.pbar', {'sticky': 'ns'}), ("Floodgauge.label", {"sticky": ""})], 'sticky': 'nswe'})], 'configure': { 'thickness': 50, 'borderwidth': 1, 'bordercolor': self.theme.colors.primary, 'lightcolor': self.theme.colors.primary, 'pbarrelief': 'flat', 'troughcolor': Colors.update_hsv(self.theme.colors.primary, sd=-0.3, vd=0.8), 'background': self.theme.colors.primary, 'foreground': self.theme.colors.selectfg, 'justify': 'center', 'anchor': 'center', 'font': '-size 14'}}, 'Vertical.TFloodgauge': { 'layout': [('Floodgauge.trough', {'children': [ ('Floodgauge.pbar', {'sticky': 'we'}), ("Floodgauge.label", {"sticky": ""})], 'sticky': 'nswe'})], 'configure': { 'thickness': 50, 'borderwidth': 1, 'bordercolor': self.theme.colors.primary, 'lightcolor': self.theme.colors.primary, 'pbarrelief': 'flat', 'troughcolor': Colors.update_hsv(self.theme.colors.primary, sd=-0.3, vd=0.8), 'background': self.theme.colors.primary, 'foreground': self.theme.colors.selectfg, 'justify': 'center', 'anchor': 'center', 'font': '-size 14'} }}) for color in self.theme.colors: self.settings.update({ f'{color}.Horizontal.TFloodgauge': { 'configure': { 'thickness': 50, 'borderwidth': 1, 'bordercolor': self.theme.colors.get(color), 'lightcolor': self.theme.colors.get(color), 'pbarrelief': 'flat', 'troughcolor': Colors.update_hsv(self.theme.colors.get(color), sd=-0.3, vd=0.8), 'background': self.theme.colors.get(color), 'foreground': self.theme.colors.selectfg, 'justify': 'center', 'anchor': 'center', 'font': '-size 14'}}, f'{color}.Vertical.TFloodgauge': { 'configure': { 'thickness': 50, 'borderwidth': 1, 'bordercolor': self.theme.colors.get(color), 'lightcolor': self.theme.colors.get(color), 'pbarrelief': 'flat', 'troughcolor': Colors.update_hsv(self.theme.colors.get(color), sd=-0.3, vd=0.8), 'background': self.theme.colors.get(color), 'foreground': self.theme.colors.selectfg, 'justify': 'center', 'anchor': 'center', 'font': '-size 14'} }}) def _style_spinbox(self): """Create style configuration for ttk spinbox: *ttk.Spinbox* This widget uses elements from the *default* and *clam* theme to create the widget layout. For dark themes,the spinbox.field is created from the *default* theme element because the background color shines through the corners of the widget when the primary theme background color is dark. The options available in this widget include: - Spinbox.field: bordercolor, lightcolor, darkcolor, fieldbackground - spinbox.uparrow: background, relief, borderwidth, arrowcolor, arrowsize - spinbox.downarrow: background, relief, borderwidth, arrowcolor, arrowsize - spinbox.padding: padding, relief, shiftrelief - spinbox.textarea: font, width """ disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) # if self.theme.type == 'dark': # self.settings.update({'custom.Spinbox.field': {'element create': ('from', 'default')}}) if self.theme.type == 'light': bordercolor = self.theme.colors.border else: bordercolor = self.theme.colors.selectbg self.settings.update({ 'Spinbox.uparrow': {'element create': ('from', 'default')}, 'Spinbox.downarrow': {'element create': ('from', 'default')}, 'TSpinbox': { 'layout': [ ('custom.Spinbox.field', {'side': 'top', 'sticky': 'we', 'children': [ ('null', {'side': 'right', 'sticky': '', 'children': [ ('Spinbox.uparrow', {'side': 'top', 'sticky': 'e'}), ('Spinbox.downarrow', {'side': 'bottom', 'sticky': 'e'})]}), ('Spinbox.padding', {'sticky': 'nswe', 'children': [ ('Spinbox.textarea', {'sticky': 'nswe'})]})]})], 'configure': { 'bordercolor': bordercolor, 'darkcolor': self.theme.colors.inputbg, 'lightcolor': self.theme.colors.inputbg, 'fieldbackground': self.theme.colors.inputbg, 'foreground': self.theme.colors.inputfg, # 'borderwidth': 0, 'background': self.theme.colors.inputbg, 'relief': 'flat', 'arrowcolor': self.theme.colors.inputfg, 'arrowsize': 14, 'padding': (10, 5) }, 'map': { 'foreground': [ ('disabled', disabled_fg)], 'bordercolor': [ ('focus !disabled', self.theme.colors.primary), ('hover !disabled', self.theme.colors.bg)], 'lightcolor': [ ('focus !disabled', self.theme.colors.primary), ('hover !disabled', self.theme.colors.primary)], 'darkcolor': [ ('focus !disabled', self.theme.colors.primary), ('hover !disabled', self.theme.colors.primary)], 'arrowcolor': [ ('disabled !disabled', disabled_fg), ('pressed !disabled', self.theme.colors.primary), ('focus !disabled', self.theme.colors.inputfg), ('hover !disabled', self.theme.colors.inputfg)]}}}) for color in self.theme.colors: self.settings.update({ f'{color}.TSpinbox': { 'map': { 'foreground': [ ('disabled', disabled_fg)], 'bordercolor': [ ('focus !disabled', self.theme.colors.get(color)), ('hover !disabled', self.theme.colors.get(color))], 'arrowcolor': [ ('disabled !disabled', disabled_fg), ('pressed !disabled', self.theme.colors.get(color)), ('hover !disabled', self.theme.colors.inputfg)], 'lightcolor': [ ('focus !disabled', self.theme.colors.get(color))], 'darkcolor': [ ('focus !disabled', self.theme.colors.get(color))]}}}) def _style_treeview(self): """Create style configuration for ttk treeview: *ttk.Treeview*. This widget uses elements from the *alt* and *clam* theme to create the widget layout. The options available in this widget include: Treeview: - Treeview.field: bordercolor, lightcolor, darkcolor, fieldbackground - Treeview.padding: padding, relief, shiftrelief - Treeview.treearea Item: - Treeitem.padding: padding, relief, shiftrelief - Treeitem.indicator: foreground, diameter, indicatormargins - Treeitem.image: image, stipple, background - Treeitem.focus: focuscolor, focusthickness - Treeitem.text: text, font, foreground, underline, width, anchor, justify, wraplength, embossed Heading: - Treeheading.cell: background, rownumber - Treeheading.border: bordercolor, lightcolor, darkcolor, relief, borderwidth - Treeheading.padding: padding, relief, shiftrelief - Treeheading.image: image, stipple, background - Treeheading.text: text, font, foreground, underline, width, anchor, justify, wraplength, embossed Cell: - Treedata.padding: padding, relief, shiftrelief - Treeitem.text: text, font, foreground, underline, width, anchor, justify, wraplength, embossed """ disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) self.settings.update({ 'Treeview': { 'layout': [ ('Button.border', {'sticky': 'nswe', 'border': '1', 'children': [ ('Treeview.padding', {'sticky': 'nswe', 'children': [ ('Treeview.treearea', {'sticky': 'nswe'})]})]})], 'configure': { 'background': self.theme.colors.inputbg, 'foreground': self.theme.colors.inputfg, 'bordercolor': self.theme.colors.bg, 'lightcolor': self.theme.colors.border, 'darkcolor': self.theme.colors.border, 'relief': 'raised' if self.theme.type == 'light' else 'flat', 'padding': 0 if self.theme.type == 'light' else -2 }, 'map': { 'background': [ ('selected', self.theme.colors.selectbg)], 'foreground': [ ('disabled', disabled_fg), ('selected', self.theme.colors.selectfg)]}}, 'Treeview.Heading': { 'configure': { 'background': self.theme.colors.primary, 'foreground': self.theme.colors.selectfg, 'relief': 'flat', 'padding': 5}}, 'Treeitem.indicator': {'element create': ('from', 'alt')}}) for color in self.theme.colors: self.settings.update({ f'{color}.Treeview.Heading': { 'configure': { 'background': self.theme.colors.get(color)}, 'map': { 'foreground': [ ('disabled', disabled_fg)], 'bordercolor': [ ('focus !disabled', self.theme.colors.get(color))]}}}) def _style_frame(self): """Create style configuration for ttk frame: *ttk.Frame* The options available in this widget include: - Frame.border: bordercolor, lightcolor, darkcolor, relief, borderwidth """ self.settings.update({ 'TFrame': {'configure': {'background': self.theme.colors.bg}}}) for color in self.theme.colors: self.settings.update({ f'{color}.TFrame': {'configure': {'background': self.theme.colors.get(color)}}}) def _style_solid_buttons(self): """Apply a solid color style to ttk button: *ttk.Button* The options available in this widget include: - Button.border: bordercolor, lightcolor, darkcolor, relief, borderwidth - Button.focus: focuscolor, focusthickness - Button.padding: padding, relief, shiftrelief - Button.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ # disabled settings disabled_fg = (self.theme.colors.inputfg if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.inputfg, vd=-0.4)) disabled_bg = (Colors.update_hsv(self.theme.colors.inputbg, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.inputbg, vd=+0.3)) # pressed and hover settings pressed_vd = -0.2 hover_vd = -0.1 self.settings.update({ 'TButton': { 'configure': { 'foreground': self.theme.colors.selectfg, 'background': self.theme.colors.primary, 'bordercolor': self.theme.colors.primary, 'darkcolor': self.theme.colors.primary, 'lightcolor': self.theme.colors.primary, # 'font': self.theme.font, 'anchor': 'center', 'relief': 'raised', 'focusthickness': 0, 'focuscolor': '', 'padding': (10, 5)}, # TODO should I remove default padding? I can also use: width: -12 to set a minimum width 'map': { 'foreground': [ ('disabled', disabled_fg)], 'background': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.primary, vd=hover_vd))], 'bordercolor': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.primary, vd=hover_vd))], 'darkcolor': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.primary, vd=hover_vd))], 'lightcolor': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.primary, vd=hover_vd))]}}}) for color in self.theme.colors: self.settings.update({ f'{color}.TButton': { 'configure': { 'foreground': self.theme.colors.selectfg, 'background': self.theme.colors.get(color), 'bordercolor': self.theme.colors.get(color), 'darkcolor': self.theme.colors.get(color), 'lightcolor': self.theme.colors.get(color), 'relief': 'raised', 'focusthickness': 0, 'focuscolor': '', 'padding': (10, 5)}, 'map': { 'foreground': [ ('disabled', disabled_fg)], 'background': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=hover_vd))], 'bordercolor': [ ('disabled', disabled_bg), ('hover !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=hover_vd))], 'darkcolor': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=hover_vd))], 'lightcolor': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=hover_vd))]}}}) def _style_outline_buttons(self): """Apply an outline style to ttk button: *ttk.Button*. This button has a solid button look on focus and hover. The options available in this widget include: - Button.border: bordercolor, lightcolor, darkcolor, relief, borderwidth - Button.focus: focuscolor, focusthickness - Button.padding: padding, relief, shiftrelief - Button.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ # disabled settings disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) # pressed and hover settings pressed_vd = -0.10 self.settings.update({ 'Outline.TButton': { 'configure': { 'foreground': self.theme.colors.primary, 'background': self.theme.colors.bg, 'bordercolor': self.theme.colors.primary, 'darkcolor': self.theme.colors.bg, 'lightcolor': self.theme.colors.bg, 'relief': 'raised', # 'font': self.theme.font, 'focusthickness': 0, 'focuscolor': '', 'padding': (10, 5)}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.selectfg), ('hover !disabled', self.theme.colors.selectfg)], 'background': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', self.theme.colors.primary)], 'bordercolor': [ ('disabled', disabled_fg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', self.theme.colors.primary)], 'darkcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', self.theme.colors.primary)], 'lightcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', self.theme.colors.primary)]}}}) for color in self.theme.colors: self.settings.update({ f'{color}.Outline.TButton': { 'configure': { 'foreground': self.theme.colors.get(color), 'background': self.theme.colors.bg, 'bordercolor': self.theme.colors.get(color), 'darkcolor': self.theme.colors.bg, 'lightcolor': self.theme.colors.bg, 'relief': 'raised', 'focusthickness': 0, 'focuscolor': '', 'padding': (10, 5)}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.selectfg), ('hover !disabled', self.theme.colors.selectfg)], 'background': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', self.theme.colors.get(color))], 'bordercolor': [ ('disabled', disabled_fg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', self.theme.colors.get(color))], 'darkcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', self.theme.colors.get(color))], 'lightcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', self.theme.colors.get(color))]}}}) def _style_link_buttons(self): """Apply a solid color style to ttk button: *ttk.Button* The options available in this widget include: - Button.border: bordercolor, lightcolor, darkcolor, relief, borderwidth - Button.focus: focuscolor, focusthickness - Button.padding: padding, relief, shiftrelief - Button.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ # disabled settings disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) # pressed and hover settings pressed_vd = 0 hover_vd = 0 self.settings.update({ 'Link.TButton': { 'configure': { 'foreground': self.theme.colors.fg, 'background': self.theme.colors.bg, 'bordercolor': self.theme.colors.bg, 'darkcolor': self.theme.colors.bg, 'lightcolor': self.theme.colors.bg, 'relief': 'raised', # 'font': self.theme.font, 'focusthickness': 0, 'focuscolor': '', 'padding': (10, 5)}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.info, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.info, vd=hover_vd))], 'shiftrelief': [ ('pressed !disabled', -1)], 'background': [ ('pressed !disabled', self.theme.colors.bg), ('hover !disabled', self.theme.colors.bg)], 'bordercolor': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.bg), ('hover !disabled', self.theme.colors.bg)], 'darkcolor': [ ('pressed !disabled', self.theme.colors.bg), ('hover !disabled', self.theme.colors.bg)], 'lightcolor': [ ('pressed !disabled', self.theme.colors.bg), ('hover !disabled', self.theme.colors.bg)]}}}) for color in self.theme.colors: self.settings.update({ f'{color}.Link.TButton': { 'configure': { 'foreground': self.theme.colors.get(color), 'background': self.theme.colors.bg, 'bordercolor': self.theme.colors.bg, 'darkcolor': self.theme.colors.bg, 'lightcolor': self.theme.colors.bg, 'relief': 'raised', # 'font': self.theme.font, 'focusthickness': 0, 'focuscolor': '', 'padding': (10, 5)}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.info, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.info, vd=hover_vd))], 'shiftrelief': [ ('pressed !disabled', -1)], 'background': [ ('pressed !disabled', self.theme.colors.bg), ('hover !disabled', self.theme.colors.bg)], 'bordercolor': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.bg), ('hover !disabled', self.theme.colors.bg)], 'darkcolor': [ ('pressed !disabled', self.theme.colors.bg), ('hover !disabled', self.theme.colors.bg)], 'lightcolor': [ ('pressed !disabled', self.theme.colors.bg), ('hover !disabled', self.theme.colors.bg)]}}}) def _create_squaretoggle_image(self, colorname): """Create a set of images for the square toggle button and return as ``PhotoImage`` Args: colorname (str): the color label assigned to the colors property Returns: Tuple[PhotoImage]: a tuple of images (toggle_on, toggle_off, toggle_disabled) """ prime_color = self.theme.colors.get(colorname) on_border = prime_color on_indicator = prime_color on_fill = self.theme.colors.bg off_border = self.theme.colors.selectbg if self.theme.type == 'light' else self.theme.colors.inputbg off_indicator = self.theme.colors.selectbg if self.theme.type == 'light' else self.theme.colors.inputbg off_fill = self.theme.colors.bg disabled_fill = self.theme.colors.bg disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) toggle_off = Image.new('RGBA', (226, 130)) draw = ImageDraw.Draw(toggle_off) draw.rectangle([1, 1, 225, 129], outline=off_border, width=6, fill=off_fill) draw.rectangle([18, 18, 110, 110], fill=off_indicator) toggle_on = Image.new('RGBA', (226, 130)) draw = ImageDraw.Draw(toggle_on) draw.rectangle([1, 1, 225, 129], outline=on_border, width=6, fill=on_fill) draw.rectangle([18, 18, 110, 110], fill=on_indicator) toggle_on = toggle_on.transpose(Image.ROTATE_180) toggle_disabled = Image.new('RGBA', (226, 130)) draw = ImageDraw.Draw(toggle_disabled) draw.rectangle([1, 1, 225, 129], outline=disabled_fg, width=6) draw.rectangle([18, 18, 110, 110], fill=disabled_fg) images = {} images[f'{colorname}_squaretoggle_on'] = ImageTk.PhotoImage(toggle_on.resize((24, 15), Image.LANCZOS)) images[f'{colorname}_squaretoggle_off'] = ImageTk.PhotoImage(toggle_off.resize((24, 15), Image.LANCZOS)) images[f'{colorname}_squaretoggle_disabled'] = ImageTk.PhotoImage( toggle_disabled.resize((24, 15), Image.LANCZOS)) return images def _create_roundtoggle_image(self, colorname): """Create a set of images for the rounded toggle button and return as ``PhotoImage`` Args: colorname (str): the color label assigned to the colors property Returns: Tuple[PhotoImage] """ prime_color = self.theme.colors.get(colorname) on_border = prime_color on_indicator = self.theme.colors.selectfg on_fill = prime_color off_border = self.theme.colors.selectbg if self.theme.type == 'light' else self.theme.colors.inputbg off_indicator = self.theme.colors.selectbg if self.theme.type == 'light' else self.theme.colors.inputbg off_fill = self.theme.colors.bg disabled_fill = self.theme.colors.bg disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) toggle_off = Image.new('RGBA', (226, 130)) draw = ImageDraw.Draw(toggle_off) draw.rounded_rectangle([1, 1, 225, 129], radius=(128 / 2), outline=off_border, width=6, fill=off_fill) draw.ellipse([20, 18, 112, 110], fill=off_indicator) toggle_on = Image.new('RGBA', (226, 130)) draw = ImageDraw.Draw(toggle_on) draw.rounded_rectangle([1, 1, 225, 129], radius=(128 / 2), outline=on_border, width=6, fill=on_fill) draw.ellipse([20, 18, 112, 110], fill=on_indicator) toggle_on = toggle_on.transpose(Image.ROTATE_180) toggle_disabled = Image.new('RGBA', (226, 130)) draw = ImageDraw.Draw(toggle_disabled) draw.rounded_rectangle([1, 1, 225, 129], radius=(128 / 2), outline=disabled_fg, width=6) draw.ellipse([20, 18, 112, 110], fill=disabled_fg) images = {} images[f'{colorname}_roundtoggle_on'] = ImageTk.PhotoImage(toggle_on.resize((24, 15), Image.LANCZOS)) images[f'{colorname}_roundtoggle_off'] = ImageTk.PhotoImage(toggle_off.resize((24, 15), Image.LANCZOS)) images[f'{colorname}_roundtoggle_disabled'] = ImageTk.PhotoImage( toggle_disabled.resize((24, 15), Image.LANCZOS)) return images def _style_roundtoggle_toolbutton(self): """Apply a rounded toggle switch style to ttk widgets that accept the toolbutton style (for example, a checkbutton: *ttk.Checkbutton*) """ self.theme_images.update(self._create_roundtoggle_image('primary')) disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) # create indicator element self.settings.update({ 'Roundtoggle.Toolbutton.indicator': { 'element create': ('image', self.theme_images['primary_roundtoggle_on'], ('disabled', self.theme_images['primary_roundtoggle_disabled']), ('!selected', self.theme_images['primary_roundtoggle_off']), {'width': 28, 'border': 4, 'sticky': 'w'})}, 'Roundtoggle.Toolbutton': { 'layout': [('Toolbutton.border', {'sticky': 'nswe', 'children': [ ('Toolbutton.padding', {'sticky': 'nswe', 'children': [ ('Roundtoggle.Toolbutton.indicator', {'side': 'left'}), ('Toolbutton.label', {'side': 'left'})]})]})], 'configure': { 'relief': 'flat', 'borderwidth': 0, 'foreground': self.theme.colors.fg}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('hover', self.theme.colors.primary)], 'background': [ ('selected', self.theme.colors.bg), ('!selected', self.theme.colors.bg)]}}}) # color variations for color in self.theme.colors: self.theme_images.update(self._create_roundtoggle_image(color)) # create indicator element self.settings.update({ f'{color}.Roundtoggle.Toolbutton.indicator': { 'element create': ('image', self.theme_images[f'{color}_roundtoggle_on'], ('disabled', self.theme_images[f'{color}_roundtoggle_disabled']), ('!selected', self.theme_images[f'{color}_roundtoggle_off']), {'width': 28, 'border': 4, 'sticky': 'w'})}, f'{color}.Roundtoggle.Toolbutton': { 'layout': [('Toolbutton.border', {'sticky': 'nswe', 'children': [ ('Toolbutton.padding', {'sticky': 'nswe', 'children': [ (f'{color}.Roundtoggle.Toolbutton.indicator', {'side': 'left'}), ('Toolbutton.label', {'side': 'left'})]})]})], 'configure': { 'relief': 'flat', 'borderwidth': 0, 'foreground': self.theme.colors.fg}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('hover', self.theme.colors.get(color))], 'background': [ ('selected', self.theme.colors.bg), ('!selected', self.theme.colors.bg)]}}}) def _style_squaretoggle_toolbutton(self): """Apply a square toggle switch style to ttk widgets that accept the toolbutton style (for example, a checkbutton: *ttk.Checkbutton*) """ self.theme_images.update(self._create_squaretoggle_image('primary')) disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) # create indicator element self.settings.update({ 'Squaretoggle.Toolbutton.indicator': { 'element create': ('image', self.theme_images['primary_squaretoggle_on'], ('disabled', self.theme_images['primary_squaretoggle_disabled']), ('!selected', self.theme_images['primary_squaretoggle_off']), {'width': 28, 'border': 4, 'sticky': 'w'})}, 'Squaretoggle.Toolbutton': { 'layout': [('Toolbutton.border', {'sticky': 'nswe', 'children': [ ('Toolbutton.padding', {'sticky': 'nswe', 'children': [ ('Squaretoggle.Toolbutton.indicator', {'side': 'left'}), ('Toolbutton.label', {'side': 'left'})]})]})], 'configure': { 'relief': 'flat', 'borderwidth': 0, 'foreground': self.theme.colors.fg}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('hover', self.theme.colors.primary)], 'background': [ ('selected', self.theme.colors.bg), ('!selected', self.theme.colors.bg)]}}}) # color variations for color in self.theme.colors: self.theme_images.update(self._create_squaretoggle_image(color)) # create indicator element self.settings.update({ f'{color}.Squaretoggle.Toolbutton.indicator': { 'element create': ('image', self.theme_images[f'{color}_squaretoggle_on'], ('disabled', self.theme_images[f'{color}_squaretoggle_disabled']), ('!selected', self.theme_images[f'{color}_squaretoggle_off']), {'width': 28, 'border': 4, 'sticky': 'w'})}, f'{color}.Squaretoggle.Toolbutton': { 'layout': [('Toolbutton.border', {'sticky': 'nswe', 'children': [ ('Toolbutton.padding', {'sticky': 'nswe', 'children': [ (f'{color}.Squaretoggle.Toolbutton.indicator', {'side': 'left'}), ('Toolbutton.label', {'side': 'left'})]})]})], 'configure': { 'relief': 'flat', 'borderwidth': 0, 'foreground': self.theme.colors.fg}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('hover', self.theme.colors.get(color))], 'background': [ ('selected', self.theme.colors.bg), ('!selected', self.theme.colors.bg)]}}}) def _style_solid_toolbutton(self): """Apply a solid color style to ttk widgets that use the Toolbutton style (for example, a checkbutton: *ttk.Checkbutton*) The options available in this widget include: - Button.border: bordercolor, lightcolor, darkcolor, relief, borderwidth - Button.focus: focuscolor, focusthickness - Button.padding: padding, relief, shiftrelief - Button.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ # disabled settings disabled_fg = (self.theme.colors.inputfg if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.inputfg, vd=-0.4)) disabled_bg = (Colors.update_hsv(self.theme.colors.inputbg, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.inputbg, vd=+0.3)) # pressed and hover settings pressed_vd = -0.2 hover_vd = -0.1 normal_sd = -0.5 normal_vd = 0.1 self.settings.update({ 'Toolbutton': { 'configure': { 'foreground': self.theme.colors.selectfg, 'background': Colors.update_hsv(self.theme.colors.primary, sd=normal_sd, vd=normal_vd), 'bordercolor': Colors.update_hsv(self.theme.colors.primary, sd=normal_sd, vd=normal_vd), 'darkcolor': Colors.update_hsv(self.theme.colors.primary, sd=normal_sd, vd=normal_vd), 'lightcolor': Colors.update_hsv(self.theme.colors.primary, sd=normal_sd, vd=normal_vd), # 'font': self.theme.font, 'anchor': 'center', 'relief': 'raised', 'focusthickness': 0, 'focuscolor': '', 'padding': (10, 5)}, 'map': { 'foreground': [ ('disabled', disabled_fg)], 'background': [ ('disabled', disabled_bg), ('pressed !disabled', self.theme.colors.primary), ('selected !disabled', self.theme.colors.primary), ('hover !disabled', self.theme.colors.primary)], 'bordercolor': [ ('disabled', disabled_bg), ('selected !disabled', self.theme.colors.primary), ('pressed !disabled', self.theme.colors.primary), ('hover !disabled', self.theme.colors.primary)], 'darkcolor': [ ('disabled', disabled_bg), ('pressed !disabled', self.theme.colors.primary), ('selected !disabled', self.theme.colors.primary), ('hover !disabled', self.theme.colors.primary)], 'lightcolor': [ ('disabled', disabled_bg), ('pressed !disabled', self.theme.colors.primary), ('selected !disabled', self.theme.colors.primary), ('hover !disabled', self.theme.colors.primary)]}}}) for color in self.theme.colors: self.settings.update({ f'{color}.Toolbutton': { 'configure': { 'foreground': self.theme.colors.selectfg, 'background': Colors.update_hsv(self.theme.colors.get(color), sd=normal_sd, vd=normal_vd), 'bordercolor': Colors.update_hsv(self.theme.colors.get(color), sd=normal_sd, vd=normal_vd), 'darkcolor': Colors.update_hsv(self.theme.colors.get(color), sd=normal_sd, vd=normal_vd), 'lightcolor': Colors.update_hsv(self.theme.colors.get(color), sd=normal_sd, vd=normal_vd), 'relief': 'raised', 'focusthickness': 0, 'focuscolor': '', 'padding': (10, 5)}, 'map': { 'foreground': [ ('disabled', disabled_fg)], 'background': [ ('disabled', disabled_bg), ('pressed !disabled', self.theme.colors.get(color)), ('selected !disabled', self.theme.colors.get(color)), ('hover !disabled', self.theme.colors.get(color))], 'bordercolor': [ ('disabled', disabled_bg), ('pressed !disabled', self.theme.colors.get(color)), ('selected !disabled', self.theme.colors.get(color)), ('hover !disabled', self.theme.colors.get(color))], 'darkcolor': [ ('disabled', disabled_bg), ('pressed !disabled', self.theme.colors.get(color)), ('selected !disabled', self.theme.colors.get(color)), ('hover !disabled', self.theme.colors.get(color))], 'lightcolor': [ ('disabled', disabled_bg), ('pressed !disabled', self.theme.colors.get(color)), ('selected !disabled', self.theme.colors.get(color)), ('hover !disabled', self.theme.colors.get(color))]}}}) def _style_outline_toolbutton(self): """Apply an outline style to ttk widgets that use the Toolbutton style (for example, a checkbutton: *ttk.Checkbutton*). This button has a solid button look on focus and hover. The options available in this widget include: - Button.border: bordercolor, lightcolor, darkcolor, relief, borderwidth - Button.focus: focuscolor, focusthickness - Button.padding: padding, relief, shiftrelief - Button.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ # disabled settings disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) # pressed and hover settings pressed_vd = -0.10 self.settings.update({ 'Outline.Toolbutton': { 'configure': { 'foreground': self.theme.colors.primary, 'background': self.theme.colors.bg, 'bordercolor': self.theme.colors.border, 'darkcolor': self.theme.colors.bg, 'lightcolor': self.theme.colors.bg, 'relief': 'raised', # 'font': self.theme.font, 'focusthickness': 0, 'focuscolor': '', 'borderwidth': 1, 'padding': (10, 5)}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.selectfg), ('selected !disabled', self.theme.colors.selectfg), ('hover !disabled', self.theme.colors.selectfg)], 'background': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', self.theme.colors.primary)], 'bordercolor': [ ('disabled', disabled_fg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', self.theme.colors.primary)], 'darkcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', self.theme.colors.primary)], 'lightcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', self.theme.colors.primary)]}}}) for color in self.theme.colors: self.settings.update({ f'{color}.Outline.Toolbutton': { 'configure': { 'foreground': self.theme.colors.get(color), 'background': self.theme.colors.bg, 'bordercolor': self.theme.colors.border, 'darkcolor': self.theme.colors.bg, 'lightcolor': self.theme.colors.bg, 'relief': 'raised', 'focusthickness': 0, 'focuscolor': '', 'borderwidth': 1, 'padding': (10, 5)}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.selectfg), ('selected !disabled', self.theme.colors.selectfg), ('hover !disabled', self.theme.colors.selectfg)], 'background': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', self.theme.colors.get(color))], 'bordercolor': [ ('disabled', disabled_fg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', self.theme.colors.get(color))], 'darkcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', self.theme.colors.get(color))], 'lightcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', self.theme.colors.get(color))]}}}) def _style_entry(self): """Create style configuration for ttk entry: *ttk.Entry* The options available in this widget include: - Entry.field: bordercolor, lightcolor, darkcolor, fieldbackground - Entry.padding: padding, relief, shiftrelief - Entry.textarea: font, width """ disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) # if self.theme.type == 'dark': # self.settings.update({'Entry.field': {'element create': ('from', 'default')}}) if self.theme.type == 'light': bordercolor = self.theme.colors.border else: bordercolor = self.theme.colors.selectbg self.settings.update({ 'TEntry': { 'configure': { 'bordercolor': bordercolor, 'darkcolor': self.theme.colors.inputbg, 'lightcolor': self.theme.colors.inputbg, 'fieldbackground': self.theme.colors.inputbg, 'foreground': self.theme.colors.inputfg, # 'borderwidth': 0, # only applies to border on darktheme 'padding': 5}, 'map': { 'foreground': [('disabled', disabled_fg)], 'bordercolor': [ ('focus !disabled', self.theme.colors.primary), ('hover !disabled', self.theme.colors.bg)], 'lightcolor': [ ('focus !disabled', self.theme.colors.primary), ('hover !disabled', self.theme.colors.primary)], 'darkcolor': [ ('focus !disabled', self.theme.colors.primary), ('hover !disabled', self.theme.colors.primary)]}}}) for color in self.theme.colors: self.settings.update({ f'{color}.TEntry': { 'map': { 'foreground': [ ('disabled', disabled_fg)], 'bordercolor': [ ('focus !disabled', self.theme.colors.get(color)), ('hover !disabled', self.theme.colors.bg)], 'lightcolor': [ ('focus !disabled', self.theme.colors.get(color)), ('hover !disabled', self.theme.colors.get(color))], 'darkcolor': [ ('focus !disabled', self.theme.colors.get(color)), ('hover !disabled', self.theme.colors.get(color))]}}}) def _style_radiobutton(self): """Create style configuration for ttk radiobutton: *ttk.Radiobutton* The options available in this widget include: - Radiobutton.padding: padding, relief, shiftrelief - Radiobutton.indicator: indicatorsize, indicatormargin, indicatorbackground, indicatorforeground, upperbordercolor, lowerbordercolor - Radiobutton.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) disabled_bg = self.theme.colors.inputbg if self.theme.type == 'light' else disabled_fg self.theme_images.update(self._create_radiobutton_images('primary')) self.settings.update({ 'Radiobutton.indicator': { 'element create': ('image', self.theme_images['primary_radio_on'], ('disabled', self.theme_images['primary_radio_disabled']), ('!selected', self.theme_images['primary_radio_off']), {'width': 20, 'border': 4, 'sticky': 'w'})}, 'TRadiobutton': { 'layout': [ ('Radiobutton.padding', {'children': [ ('Radiobutton.indicator', {'side': 'left', 'sticky': ''}), ('Radiobutton.focus', {'children': [ ('Radiobutton.label', {'sticky': 'nswe'})], 'side': 'left', 'sticky': ''})], 'sticky': 'nswe'})], # 'configure': { # 'font': self.theme.font}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('active', self.theme.colors.primary)], 'indicatorforeground': [ ('disabled', disabled_fg), ('active selected !disabled', self.theme.colors.primary)]}}}) # variations change the indicator color for color in self.theme.colors: self.theme_images.update(self._create_radiobutton_images(color)) self.settings.update({ f'{color}.Radiobutton.indicator': { 'element create': ('image', self.theme_images[f'{color}_radio_on'], ('disabled', self.theme_images[f'{color}_radio_disabled']), ('!selected', self.theme_images[f'{color}_radio_off']), {'width': 20, 'border': 4, 'sticky': 'w'})}, f'{color}.TRadiobutton': { 'layout': [ ('Radiobutton.padding', {'children': [ (f'{color}.Radiobutton.indicator', {'side': 'left', 'sticky': ''}), ('Radiobutton.focus', {'children': [ ('Radiobutton.label', {'sticky': 'nswe'})], 'side': 'left', 'sticky': ''})], 'sticky': 'nswe'})], # 'configure': { # 'font': self.theme.font}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('active', Colors.update_hsv(self.theme.colors.get(color), vd=-0.2))], 'indicatorforeground': [ ('disabled', disabled_fg), ('active selected !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=-0.2))]}}}) def _create_radiobutton_images(self, colorname): """Create radiobutton assets Args: colorname (str): the name of the color to use for the button on state Returns: Tuple[PhotoImage]: a tuple of widget images. """ prime_color = self.theme.colors.get(colorname) on_fill = prime_color off_fill = self.theme.colors.bg on_indicator = self.theme.colors.selectfg if self.theme.type == 'light': off_border = self.theme.colors.border disabled = Colors.update_hsv(self.theme.colors.light, vd=-0.2) else: off_border = self.theme.colors.selectbg disabled = Colors.update_hsv(self.theme.colors.light, vd=-0.3) # radio off radio_off = Image.new("RGBA", (134, 134)) draw = ImageDraw.Draw(radio_off) draw.ellipse( xy=[1, 1, 133, 133], outline=off_border, width=6, fill=off_fill ) # radio on radio_on = Image.new("RGBA", (134, 134)) draw = ImageDraw.Draw(radio_on) draw.ellipse(xy=[1, 1, 133, 133], fill=on_fill) draw.ellipse([40, 40, 94, 94], fill=on_indicator) # radio disabled radio_disabled = Image.new("RGBA", (134, 134)) draw = ImageDraw.Draw(radio_disabled) draw.ellipse( xy=[1, 1, 133, 133], outline=disabled, width=3, fill=off_fill ) return { f'{colorname}_radio_off': ImageTk.PhotoImage(radio_off.resize((14, 14), Image.LANCZOS)), f'{colorname}_radio_on': ImageTk.PhotoImage(radio_on.resize((14, 14), Image.LANCZOS)), f'{colorname}_radio_disabled': ImageTk.PhotoImage(radio_disabled.resize((14, 14), Image.LANCZOS))} def _style_calendar(self): """Create style configuration for the ttkbootstrap.widgets.datechooser The options available in this widget include: - Label.border: bordercolor, lightcolor, darkcolor, relief, borderwidth - Label.padding: padding, relief, shiftrelief - Label.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ # disabled settings disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) # pressed and hover settings pressed_vd = -0.10 self.settings.update({ 'TCalendar': { 'layout': [ ('Toolbutton.border', {'sticky': 'nswe', 'children': [ ('Toolbutton.padding', {'sticky': 'nswe', 'children': [ ('Toolbutton.label', {'sticky': 'nswe'})]})]})], 'configure': { 'foreground': self.theme.colors.fg, 'background': self.theme.colors.bg, 'bordercolor': self.theme.colors.bg, 'darkcolor': self.theme.colors.bg, 'lightcolor': self.theme.colors.bg, 'relief': 'raised', # 'font': self.theme.font, 'focusthickness': 0, 'focuscolor': '', 'borderwidth': 1, 'anchor': 'center', 'padding': (10, 5)}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.selectfg), ('selected !disabled', self.theme.colors.selectfg), ('hover !disabled', self.theme.colors.selectfg)], 'background': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', self.theme.colors.primary)], 'bordercolor': [ ('disabled', disabled_fg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', self.theme.colors.primary)], 'darkcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', self.theme.colors.primary)], 'lightcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', self.theme.colors.primary)]}}, 'chevron.TButton': { 'configure': {'font': '-size 14'}}}) for color in self.theme.colors: self.settings.update({ f'{color}.TCalendar': { 'configure': { 'foreground': self.theme.colors.fg, 'background': self.theme.colors.bg, 'bordercolor': self.theme.colors.bg, 'darkcolor': self.theme.colors.bg, 'lightcolor': self.theme.colors.bg, 'relief': 'raised', 'focusthickness': 0, 'focuscolor': '', 'borderwidth': 1, 'padding': (10, 5)}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.selectfg), ('selected !disabled', self.theme.colors.selectfg), ('hover !disabled', self.theme.colors.selectfg)], 'background': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', self.theme.colors.get(color))], 'bordercolor': [ ('disabled', disabled_fg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', self.theme.colors.get(color))], 'darkcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', self.theme.colors.get(color))], 'lightcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('selected !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', self.theme.colors.get(color))]}}, f'chevron.{color}.TButton': { 'configure': {'font': '-size 14'}}}) def _style_exit_button(self): """Create style configuration for the toolbutton exit button""" disabled_bg = (Colors.update_hsv(self.theme.colors.inputbg, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.inputbg, vd=+0.3)) pressed_vd = -0.2 self.settings.update({ 'exit.TButton': { 'configure': { 'relief': 'flat', 'font': '-size 12'}, 'map': { 'background': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', self.theme.colors.danger)]}}}) for color in self.theme.colors: self.settings.update({ f'exit.{color}.TButton': { 'configure': { 'relief': 'flat', 'font': '-size 12'}, 'map': { 'background': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', self.theme.colors.danger)]}}}) def _style_meter(self): """Create style configuration for the ttkbootstrap.widgets.meter The options available in this widget include: - Label.border: bordercolor, lightcolor, darkcolor, relief, borderwidth - Label.padding: padding, relief, shiftrelief - Label.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ if self.theme.type == 'light': troughcolor = self.theme.colors.light else: troughcolor = Colors.update_hsv(self.theme.colors.selectbg, vd=-0.2) self.settings.update({ 'TMeter': { 'layout': [ ('Label.border', {'sticky': 'nswe', 'border': '1', 'children': [ ('Label.padding', {'sticky': 'nswe', 'border': '1', 'children': [ ('Label.label', {'sticky': 'nswe'})]})]})], 'configure': { 'foreground': self.theme.colors.fg, 'background': self.theme.colors.bg, 'space': troughcolor}}}) for color in self.theme.colors: self.settings.update({ f'{color}.TMeter': { 'configure': { 'foreground': self.theme.colors.get(color)}}}) def _style_label(self): """Create style configuration for ttk label: *ttk.Label* The options available in this widget include: - Label.border: bordercolor, lightcolor, darkcolor, relief, borderwidth - Label.padding: padding, relief, shiftrelief - Label.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ self.settings.update({ 'TLabel': { 'configure': { 'foreground': self.theme.colors.fg, 'background': self.theme.colors.bg}}, 'Inverse.TLabel': { 'configure': { 'foreground': self.theme.colors.bg, 'background': self.theme.colors.fg}}}) for color in self.theme.colors: self.settings.update({ f'{color}.TLabel': { 'configure': { 'foreground': self.theme.colors.get(color)}}, f'{color}.Inverse.TLabel': { 'configure': { 'foreground': self.theme.colors.selectfg, 'background': self.theme.colors.get(color)}}, # TODO deprecate this version down the road f'{color}.Invert.TLabel': { 'configure': { 'foreground': self.theme.colors.selectfg, 'background': self.theme.colors.get(color)}}}) def _style_labelframe(self): """Create style configuration for ttk labelframe: *ttk.LabelFrame* The options available in this widget include: - Labelframe.border: bordercolor, lightcolor, darkcolor, relief, borderwidth - Label.fill: background - Label.text: text, font, foreground, underline, width, anchor, justify, wraplength, embossed """ self.settings.update({ 'Labelframe.Label': {'element create': ('from', 'clam')}, 'Label.fill': {'element create': ('from', 'clam')}, 'Label.text': {'element create': ('from', 'clam')}, 'TLabelframe.Label': { 'layout': [('Label.fill', {'sticky': 'nswe', 'children': [('Label.text', {'sticky': 'nswe'})]})], 'configure': { 'foreground': self.theme.colors.fg }}, 'TLabelframe': { 'layout': [('Labelframe.border', {'sticky': 'nswe'})], 'configure': { 'relief': 'raised', 'borderwidth': '1', 'bordercolor': (self.theme.colors.border if self.theme.type == 'light' else self.theme.colors.selectbg), 'lightcolor': self.theme.colors.bg, 'darkcolor': self.theme.colors.bg}}}) for color in self.theme.colors: self.settings.update({ f'{color}.TLabelframe': { 'configure': { 'background': self.theme.colors.get(color), 'lightcolor': self.theme.colors.get(color), 'darkcolor': self.theme.colors.get(color)}}, f'{color}.TLabelframe.Label': { 'configure': { 'foreground': self.theme.colors.selectfg, 'background': self.theme.colors.get(color), 'lightcolor': self.theme.colors.get(color), 'darkcolor': self.theme.colors.get(color)}}}) def _style_checkbutton(self): """Create style configuration for ttk checkbutton: *ttk.Checkbutton* The options available in this widget include: - Checkbutton.padding: padding, relief, shiftrelief - Checkbutton.indicator: indicatorsize, indicatormargin, indicatorbackground, indicatorforeground, upperbordercolor, lowerbordercolor - Checkbutton.focus: focuscolor, focusthickness - Checkbutton.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) self.theme_images.update(self._create_checkbutton_images('primary')) self.settings.update({ 'Checkbutton.indicator': { 'element create': ('image', self.theme_images['primary_checkbutton_on'], ('disabled', self.theme_images['primary_checkbutton_disabled']), ('!selected', self.theme_images['primary_checkbutton_off']), {'width': 20, 'border': 4, 'sticky': 'w'})}, 'TCheckbutton': { 'layout': [ ('Checkbutton.padding', {'children': [ ('primary.Checkbutton.indicator', {'side': 'left', 'sticky': ''}), ('Checkbutton.focus', {'children': [ ('Checkbutton.label', {'sticky': 'nswe'})], 'side': 'left', 'sticky': ''})], 'sticky': 'nswe'})], 'configure': { 'foreground': self.theme.colors.fg, 'background': self.theme.colors.bg, 'focuscolor': ''}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('active !disabled', self.theme.colors.primary)]}}}) # variations change indicator color for color in self.theme.colors: self.theme_images.update(self._create_checkbutton_images(color)) self.settings.update({ f'{color}.Checkbutton.indicator': { 'element create': ('image', self.theme_images[f'{color}_checkbutton_on'], ('disabled', self.theme_images[f'{color}_checkbutton_disabled']), ('!selected', self.theme_images[f'{color}_checkbutton_off']), {'width': 20, 'border': 4, 'sticky': 'w'})}, f'{color}.TCheckbutton': { 'layout': [ ('Checkbutton.padding', {'children': [ (f'{color}.Checkbutton.indicator', {'side': 'left', 'sticky': ''}), ('Checkbutton.focus', {'children': [ ('Checkbutton.label', {'sticky': 'nswe'})], 'side': 'left', 'sticky': ''})], 'sticky': 'nswe'})], 'map': { 'foreground': [ ('disabled', disabled_fg), ('active !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=-0.2))]}}}) def _create_checkbutton_images(self, colorname): """Create radiobutton assets Args: colorname (str): the name of the color to use for the button on state Returns: Tuple[PhotoImage]: a tuple of widget images. """ winsys = self.style.tk.call("tk", "windowingsystem") if winsys == "win32": fnt = ImageFont.truetype("seguisym.ttf", 120) font_offset = -20 elif winsys == "x11": fnt = ImageFont.truetype("FreeSerif.ttf", 130) font_offset = 10 else: fnt = ImageFont.truetype("LucidaGrande.ttc", 120) font_offset = -10 prime_color = self.theme.colors.get(colorname) on_border = prime_color on_fill = prime_color off_border = self.theme.colors.selectbg off_fill = self.theme.colors.bg if self.theme.type == 'light': disabled_bg = Colors.update_hsv(self.theme.colors.light, vd=-0.2) else: disabled_bg = Colors.update_hsv(self.theme.colors.light, vd=-0.3) check_color = self.theme.colors.selectfg # 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, ) # 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), "✓", font=fnt, fill=check_color) # 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_bg, width=3 ) return { f'{colorname}_checkbutton_off': ImageTk.PhotoImage(checkbutton_off.resize((14, 14), Image.LANCZOS)), f'{colorname}_checkbutton_on': ImageTk.PhotoImage(checkbutton_on.resize((14, 14), Image.LANCZOS)), f'{colorname}_checkbutton_disabled': ImageTk.PhotoImage(checkbutton_disabled.resize((14, 14), Image.LANCZOS))} def _style_solid_menubutton(self): """Apply a solid color style to ttk menubutton: *ttk.Menubutton* The options available in this widget include: - Menubutton.border: bordercolor, lightcolor, darkcolor, relief, borderwidth - Menubutton.focus: focuscolor, focusthickness - Menubutton.indicator: arrowsize, arrowcolor, arrowpadding - Menubutton.padding: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background - Menubutton.label: """ # disabled settings disabled_fg = (self.theme.colors.inputfg if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.inputfg, vd=-0.4)) disabled_bg = (Colors.update_hsv(self.theme.colors.inputbg, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.inputbg, vd=+0.3)) # pressed and hover settings pressed_vd = -0.2 hover_vd = -0.1 self.settings.update({ 'TMenubutton': { 'configure': { 'foreground': self.theme.colors.selectfg, 'background': self.theme.colors.primary, 'bordercolor': self.theme.colors.primary, 'darkcolor': self.theme.colors.primary, 'lightcolor': self.theme.colors.primary, 'arrowsize': 4, 'arrowcolor': self.theme.colors.bg if self.theme.type == 'light' else 'white', 'arrowpadding': (0, 0, 15, 0), 'relief': 'raised', 'focusthickness': 0, 'focuscolor': '', 'padding': (10, 5)}, 'map': { 'arrowcolor': [ ('disabled', disabled_fg)], 'foreground': [ ('disabled', disabled_fg)], 'background': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.primary, vd=hover_vd))], 'bordercolor': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.primary, vd=hover_vd))], 'darkcolor': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.primary, vd=hover_vd))], 'lightcolor': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.primary, vd=hover_vd))]}}}) for color in self.theme.colors: self.settings.update({ f'{color}.TMenubutton': { 'configure': { 'foreground': self.theme.colors.selectfg, 'background': self.theme.colors.get(color), 'bordercolor': self.theme.colors.get(color), 'darkcolor': self.theme.colors.get(color), 'lightcolor': self.theme.colors.get(color), 'arrowsize': 4, 'arrowcolor': self.theme.colors.bg if self.theme.type == 'light' else 'white', 'arrowpadding': (0, 0, 15, 0), 'relief': 'raised', 'focusthickness': 0, 'focuscolor': '', 'padding': (10, 5)}, 'map': { 'arrowcolor': [ ('disabled', disabled_fg)], 'foreground': [ ('disabled', disabled_fg)], 'background': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=hover_vd))], 'bordercolor': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=hover_vd))], 'darkcolor': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=hover_vd))], 'lightcolor': [ ('disabled', disabled_bg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=hover_vd))]}}}) def _style_outline_menubutton(self): """Apply and outline style to ttk menubutton: *ttk.Menubutton* The options available in this widget include: - Menubutton.border: bordercolor, lightcolor, darkcolor, relief, borderwidth - Menubutton.focus: focuscolor, focusthickness - Menubutton.indicator: arrowsize, arrowcolor, arrowpadding - Menubutton.padding: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background - Menubutton.label: """ # disabled settings disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) # pressed and hover settings pressed_vd = -0.2 hover_vd = -0.1 self.settings.update({ 'Outline.TMenubutton': { 'configure': { # 'font': self.theme.font, 'foreground': self.theme.colors.primary, 'background': self.theme.colors.bg, 'bordercolor': self.theme.colors.primary, 'darkcolor': self.theme.colors.bg, 'lightcolor': self.theme.colors.bg, 'arrowcolor': self.theme.colors.primary, 'arrowpadding': (0, 0, 15, 0), 'relief': 'raised', 'focusthickness': 0, 'focuscolor': '', 'padding': (10, 5)}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.selectfg), ('hover !disabled', self.theme.colors.selectfg)], 'background': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.primary, vd=hover_vd))], 'bordercolor': [ ('disabled', disabled_fg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.primary, vd=hover_vd))], 'darkcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.primary, vd=hover_vd))], 'lightcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.primary, vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.primary, vd=hover_vd))], 'arrowcolor': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.selectfg), ('hover !disabled', self.theme.colors.selectfg)]}}}) for color in self.theme.colors: self.settings.update({ f'{color}.Outline.TMenubutton': { 'configure': { 'foreground': self.theme.colors.get(color), 'background': self.theme.colors.bg, 'bordercolor': self.theme.colors.get(color), 'darkcolor': self.theme.colors.bg, 'lightcolor': self.theme.colors.bg, 'arrowcolor': self.theme.colors.get(color), 'arrowpadding': (0, 0, 15, 0), 'relief': 'raised', 'focusthickness': 0, 'focuscolor': '', 'padding': (10, 5)}, 'map': { 'foreground': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.selectfg), ('hover !disabled', self.theme.colors.selectfg)], 'background': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=hover_vd))], 'bordercolor': [ ('disabled', disabled_fg), ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=hover_vd))], 'darkcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=hover_vd))], 'lightcolor': [ ('pressed !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=pressed_vd)), ('hover !disabled', Colors.update_hsv(self.theme.colors.get(color), vd=hover_vd))], 'arrowcolor': [ ('disabled', disabled_fg), ('pressed !disabled', self.theme.colors.selectfg), ('hover !disabled', self.theme.colors.selectfg)]}}}) def _style_notebook(self): """Create style configuration for ttk notebook: *ttk.Notebook* The options available in this widget include: - Notebook.client: background, bordercolor, lightcolor, darkcolor - Notebook.tab: background, bordercolor, lightcolor, darkcolor - Notebook.padding: padding, relief, shiftrelief - Notebook.focus: focuscolor, focusthickness - Notebook.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ border_color = self.theme.colors.border if self.theme.type == 'light' else self.theme.colors.selectbg selectfg = self.theme.colors.fg if self.theme.type == 'light' else self.theme.colors.selectfg bg_color = self.theme.colors.inputbg if self.theme.type == 'light' else border_color disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) self.theme_images.update({ 'blank_image': self._create_blank_image() }) self.settings.update({ # To separate the tabs from each other, we set the expand option to # -1. However, that cuts off one pixel from the content of the tab. # To overcome this, we add a blank 0x0 image element to every tab # with 1 px padding on the right, which will be cut off by the # expand option. 'TNotebook.paddingpixel': { 'element create': ('image', self.theme_images['blank_image'], {'padding': '0 0 1 0', 'sticky': ''})}, 'TNotebook': { 'configure': { 'bordercolor': border_color, 'lightcolor': self.theme.colors.bg, 'darkcolor': self.theme.colors.bg}}, 'TNotebook.Tab': { 'layout': [ ('TNotebook.tab', {'sticky': 'nswe', 'children': [ ('TNotebook.padding', {'side': 'top', 'sticky': 'nswe', 'children': [ ('TNotebook.focus', {'side': 'top', 'sticky': 'nswe', 'children': [ ('TNotebook.label', {'side': 'left', 'sticky': ''}), ('TNotebook.paddingpixel', {'side': 'right', 'sticky': ''}), ]}) ]}) ]}) ], 'configure': { 'bordercolor': border_color, 'lightcolor': self.theme.colors.bg, 'foreground': self.theme.colors.fg, # Expand -1 on the right to separate the tabs, so that each # tab has a fully visible border that can be highlighted # when mouse is over it. 'expand': (0, 0, -1, 0), 'padding': (10, 5)}, 'map': { 'background': [ ('!selected', bg_color)], 'lightcolor': [ ('!selected', bg_color)], 'darkcolor': [ ('!selected', bg_color)], 'bordercolor': [ ('active', '!selected', '!disabled', self.theme.colors.primary), ('!selected', border_color)], 'foreground': [ ('disabled', disabled_fg), ('!selected', selectfg)]}}}) for color in self.theme.colors: self.settings.update({ f'{color}.TNotebook.Tab': { 'map': { 'bordercolor': [ ('active', '!selected', '!disabled', self.theme.colors.get(color))]}}}) def _create_blank_image(self): """Create a blank image to separate elements Returns: ImageTk.PhotoImage: a blank 0x0 image """ im = Image.new('RGBA', (0, 0)) return ImageTk.PhotoImage(im) def _style_flat_notebook(self): """Create style configuration with a flat look for ttk notebook: *ttk.Notebook* The options available in this widget include: - Notebook.client: background, bordercolor, lightcolor, darkcolor - Notebook.tab: background, bordercolor, lightcolor, darkcolor - Notebook.padding: padding, relief, shiftrelief - Notebook.focus: focuscolor, focusthickness - Notebook.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ # based on the Bootstrap style: https://getbootstrap.com/docs/5.0/components/navs-tabs/#tabs border_color = self.theme.colors.border if self.theme.type == 'light' else self.theme.colors.selectbg disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) self.settings.update({ 'Flat.TNotebook.Tab': { 'configure': { 'bordercolor': border_color, 'lightcolor': self.theme.colors.bg, # Need to set bottom expand to -1 to prevent the border # from overlapping the horizontal line below the tabs. # Interestingly, as long as the -1 on the right is only # cutting into the paddingpixel, the -1 on the bottom # does not cut into the label, only into the border, so no # extra paddingpixel on the bottom is needed. 'expand': (0, 0, -1, -1), 'foreground': self.theme.colors.fg}, 'map': { # remove the 1 px gap between the leftmost tab border and # the frame, as the frame has a rounded corner 'expand': [ ('selected', (0, 0, -1, 0))], # overwrite the default TNotebook style to make all tabs # have the same background color 'background': [], 'lightcolor': [ ('!selected', self.theme.colors.bg)], 'darkcolor': [ ('!selected', self.theme.colors.bg)], 'bordercolor': [ ('active', '!selected', '!disabled', disabled_fg), ('!selected', self.theme.colors.bg)], 'foreground': [ ('disabled', disabled_fg), ('!selected', self.theme.colors.primary)]}}}) for color in self.theme.colors: self.settings.update({ f'{color}.Flat.TNotebook.Tab': { 'map': { 'foreground': [ ('disabled', disabled_fg), ('!selected', self.theme.colors.get(color))]}}}) def _style_interactive_notebook(self): """Create style configuration for interactive notebook: *ttkbootstrap.widgets.InteractiveNotebook* The options available in this widget include: - Notebook.client: background, bordercolor, lightcolor, darkcolor - Notebook.tab: background, bordercolor, lightcolor, darkcolor - Notebook.padding: padding, relief, shiftrelief - Notebook.focus: focuscolor, focusthickness - Notebook.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ border_color = self.theme.colors.border if self.theme.type == 'light' else self.theme.colors.selectbg self.theme_images.update({ 'close_button': self._create_closebutton_images(self.theme.colors.fg), 'close_button_pressed': self._create_closebutton_images(self.theme.colors.primary), 'newtab_button': self._create_plusbutton_images(self.theme.colors.fg), 'newtab_button_pressed': self._create_plusbutton_images(self.theme.colors.primary) }) self.settings.update({ 'Interactive.TNotebook.closebutton': { 'element create': ('image', self.theme_images['close_button'], ('active', 'pressed', '!disabled', self.theme_images['close_button_pressed']), ('active', 'pressed', 'disabled', self.theme_images['newtab_button_pressed']), ('disabled', self.theme_images['newtab_button']), {'padding': '8 7 8 7', 'sticky': ''})}, 'Interactive.TNotebook.Tab': { 'layout': [ ('Interactive.TNotebook.tab', {'sticky': 'nswe', 'children': [ ('Interactive.TNotebook.padding', {'side': 'top', 'sticky': 'nswe', 'children': [ ('Interactive.TNotebook.focus', {'side': 'top', 'sticky': 'nswe', 'children': [ ('Interactive.TNotebook.closebutton', {'side': 'right', 'sticky': ''}), ('Interactive.TNotebook.label', {'side': 'left', 'sticky': ''}), ('Interactive.TNotebook.paddingpixel', {'side': 'right', 'sticky': ''}) ]}) ]}) ]}) ], 'map': { 'bordercolor': [ ('active', '!selected', self.theme.colors.primary), ('!selected', border_color)]}}}) for color in self.theme.colors: self.theme_images.update({ f'{color}_close_button_pressed': self._create_closebutton_images(self.theme.colors.get(color)), f'{color}_newtab_button_pressed': self._create_plusbutton_images(self.theme.colors.get(color)) }) self.settings.update({ f'{color}.Interactive.TNotebook.closebutton': { 'element create': ('image', self.theme_images['close_button'], ('active', 'pressed', '!disabled', self.theme_images[f'{color}_close_button_pressed']), ('active', 'pressed', 'disabled', self.theme_images[f'{color}_newtab_button_pressed']), ('disabled', self.theme_images['newtab_button']), {'padding': '8 8 8 8', 'sticky': ''})}, f'{color}.Interactive.TNotebook.Tab': { 'layout': [ (f'{color}.Interactive.TNotebook.tab', {'sticky': 'nswe', 'children': [ (f'{color}.Interactive.TNotebook.padding', {'side': 'top', 'sticky': 'nswe', 'children': [ (f'{color}.Interactive.TNotebook.focus', {'side': 'top', 'sticky': 'nswe', 'children': [ (f'{color}.Interactive.TNotebook.closebutton', {'side': 'right', 'sticky': ''}), (f'{color}.Interactive.TNotebook.label', {'side': 'left', 'sticky': ''}), (f'{color}.Interactive.TNotebook.paddingpixel', {'side': 'right', 'sticky': ''}) ]}) ]}) ]}) ], 'map': { 'bordercolor': [ ('active', '!selected', self.theme.colors.get(color))]}}}) def _style_flat_interactive_notebook(self): """Create style configuration with a flat look for interactive notebook: *ttkbootstrap.widgets.InteractiveNotebook* The options available in this widget include: - Notebook.client: background, bordercolor, lightcolor, darkcolor - Notebook.tab: background, bordercolor, lightcolor, darkcolor - Notebook.padding: padding, relief, shiftrelief - Notebook.focus: focuscolor, focusthickness - Notebook.label: compound, space, text, font, foreground, underline, width, anchor, justify, wraplength, embossed, image, stipple, background """ border_color = self.theme.colors.border if self.theme.type == 'light' else self.theme.colors.selectbg disabled_fg = (Colors.update_hsv(self.theme.colors.light, vd=-0.2) if self.theme.type == 'light' else Colors.update_hsv(self.theme.colors.light, vd=-0.3)) self.settings.update({ 'Flat.Interactive.TNotebook.closebutton': { 'element create': ('image', self.theme_images['close_button'], ('selected', 'active', 'pressed', '!disabled', self.theme_images['close_button_pressed']), ('!selected', 'active', 'pressed', '!disabled', self.theme_images['close_button']), ('!selected', '!disabled', self.theme_images['close_button_pressed']), ('active', 'pressed', 'disabled', self.theme_images['newtab_button']), ('disabled', self.theme_images['newtab_button_pressed']), # need 1 extra pixel of padding at the bottom to keep positioning the same as Interactive.TNotebook {'padding': '8 7 8 8', 'sticky': ''})}, 'Flat.Interactive.TNotebook.Tab': { 'layout': [ ('Flat.Interactive.TNotebook.tab', {'sticky': 'nswe', 'children': [ ('Flat.Interactive.TNotebook.padding', {'side': 'top', 'sticky': 'nswe', 'children': [ ('Flat.Interactive.TNotebook.focus', {'side': 'top', 'sticky': 'nswe', 'children': [ ('Flat.Interactive.TNotebook.closebutton', {'side': 'right', 'sticky': ''}), ('Flat.Interactive.TNotebook.label', {'side': 'left', 'sticky': ''}), ('Flat.Interactive.TNotebook.paddingpixel', {'side': 'right', 'sticky': ''}) ]}) ]}) ]}) ], 'configure': { 'bordercolor': border_color, 'lightcolor': self.theme.colors.bg, # need to set bottom expand to -1 to prevent the border # from overlapping the horizontal line below the tabs 'expand': (0, 0, -1, -1), # reduce the bottom padding by 1 to make up for the # increase in the closebutton padding, to keep the style # identical to the other notebook layouts 'padding': (10, 5, 10, 4), 'foreground': self.theme.colors.fg}, 'map': { # remove the 1 px gap between the leftmost tab border and # the frame, as the frame has a rounded corner 'expand': [ ('selected', (0, 0, -1, 0))], # overwrite the default TNotebook style to make all tabs # have the same background color 'background': [], 'lightcolor': [ ('!selected', self.theme.colors.bg)], 'darkcolor': [ ('!selected', self.theme.colors.bg)], 'bordercolor': [ ('active', '!selected', disabled_fg), ('!selected', self.theme.colors.bg)], 'foreground': [ ('!selected', self.theme.colors.primary)]}}}) for color in self.theme.colors: self.settings.update({ f'{color}.Flat.Interactive.TNotebook.closebutton': { 'element create': ('image', self.theme_images['close_button'], ('selected', 'active', 'pressed', '!disabled', self.theme_images[f'{color}_close_button_pressed']), ('!selected', 'active', 'pressed', '!disabled', self.theme_images['close_button']), ('!selected', '!disabled', self.theme_images[f'{color}_close_button_pressed']), ('active', 'pressed', 'disabled', self.theme_images['newtab_button']), ('disabled', self.theme_images[f'{color}_newtab_button_pressed']), {'padding': '8 8 8 9', 'sticky': ''})}, f'{color}.Flat.Interactive.TNotebook.Tab': { 'layout': [ (f'{color}.Flat.Interactive.TNotebook.tab', {'sticky': 'nswe', 'children': [ (f'{color}.Flat.Interactive.TNotebook.padding', {'side': 'top', 'sticky': 'nswe', 'children': [ (f'{color}.Flat.Interactive.TNotebook.focus', {'side': 'top', 'sticky': 'nswe', 'children': [ (f'{color}.Flat.Interactive.TNotebook.closebutton', {'side': 'right', 'sticky': ''}), (f'{color}.Flat.Interactive.TNotebook.label', {'side': 'left', 'sticky': ''}), (f'{color}.Flat.Interactive.TNotebook.paddingpixel', {'side': 'right', 'sticky': ''}) ]}) ]}) ]}) ], 'map': { 'foreground': [ ('!selected', self.theme.colors.get(color))]}}}) def _create_closebutton_images(self, color): """Create assets for the close tab button Args: color (str): a hexadecimal color value. Returns: ImageTk.PhotoImage: an image drawn in the theme color specified. """ size = 8 factor = 4 fullsize = size * factor im = Image.new('RGBA', (fullsize, fullsize)) draw = ImageDraw.Draw(im) imin = 0 imax = fullsize - 1 draw.line(((imin, imin), (imax, imax)), fill=color, width=factor) draw.line(((imin, imax), (imax, imin)), fill=color, width=factor) return ImageTk.PhotoImage(im.resize((size, size), Image.LANCZOS)) def _create_plusbutton_images(self, color): """Create assets for the new tab button Args: color (str): a hexadecimal color value. Returns: ImageTk.PhotoImage: an image drawn in the theme color specified. """ size = 9 factor = 5 fullsize = size * factor im = Image.new('RGBA', (fullsize, fullsize)) draw = ImageDraw.Draw(im) imin = 0 imax = fullsize - 1 center = imax / 2 draw.line(((center, imin), (center, imax)), fill=color, width=factor) draw.line(((imin, center), (imax, center)), fill=color, width=factor) return ImageTk.PhotoImage(im.resize((size, size), Image.LANCZOS)) def _style_panedwindow(self): """Create style configuration for ttk paned window: *ttk.PanedWindow* The options available in this widget include: Paned Window: - Panedwindow.background: background Sash: - Sash.hsash: sashthickness - Sash.hgrip: lightcolor, bordercolor, gripcount - Sash.vsash: sashthickness - Sash.vgrip: lightcolor, bordercolor, gripcount """ self.settings.update({ 'TPanedwindow': { 'configure': { 'background': self.theme.colors.bg}}, 'Sash': { 'configure': { 'bordercolor': self.theme.colors.bg, 'lightcolor': self.theme.colors.bg, 'sashthickness': 8, 'sashpad': 0, 'gripcount': 0}}}) def _style_sizegrip(self): """Create style configuration for ttk sizegrip: *ttk.Sizegrip* The options available in this widget include: - Sizegrip.sizegrip: background """ default_color = 'border' if self.theme.type == 'light' else 'inputbg' self._create_sizegrip_images(default_color) self.settings.update({ 'Sizegrip.sizegrip': { 'element create': ('image', self.theme_images[f'{default_color}_sizegrip'])}, 'TSizegrip': { 'layout': [('Sizegrip.sizegrip', {'side': 'bottom', 'sticky': 'se'})]}}) for color in self.theme.colors: self._create_sizegrip_images(color) self.settings.update({ f'{color}.Sizegrip.sizegrip': { 'element create': ('image', self.theme_images[f'{color}_sizegrip'])}, f'{color}.TSizegrip': { 'layout': [(f'{color}.Sizegrip.sizegrip', {'side': 'bottom', 'sticky': 'se'})]}}) def _create_sizegrip_images(self, colorname): """Create assets for size grip Args: colorname (str): the name of the color to use for the sizegrip images """ im = Image.new('RGBA', (14, 14)) draw = ImageDraw.Draw(im) color = self.theme.colors.get(colorname) draw.rectangle((9, 3, 10, 4), fill=color) # top draw.rectangle((6, 6, 7, 7), fill=color) # middle draw.rectangle((9, 6, 10, 7), fill=color) draw.rectangle((3, 9, 4, 10), fill=color) # bottom draw.rectangle((6, 9, 7, 10), fill=color) draw.rectangle((9, 9, 10, 10), fill=color) self.theme_images[f'{colorname}_sizegrip'] = ImageTk.PhotoImage(im)