123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447 |
- import tkinter
- import copy
- from typing import Union, Tuple, List, Dict, Callable, Optional, Any
- try:
- from typing import Literal
- except ImportError:
- from typing_extensions import Literal
- from .theme import ThemeManager
- from .font import CTkFont
- from .ctk_button import CTkButton
- from .ctk_frame import CTkFrame
- from .utility import check_kwargs_empty
- class CTkSegmentedButton(CTkFrame):
- """
- Segmented button with corner radius, border width, variable support.
- For detailed information check out the documentation.
- """
- def __init__(self,
- master: Any,
- width: int = 140,
- height: int = 28,
- corner_radius: Optional[int] = None,
- border_width: int = 3,
- bg_color: Union[str, Tuple[str, str]] = "transparent",
- fg_color: Optional[Union[str, Tuple[str, str]]] = None,
- selected_color: Optional[Union[str, Tuple[str, str]]] = None,
- selected_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
- unselected_color: Optional[Union[str, Tuple[str, str]]] = None,
- unselected_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
- text_color: Optional[Union[str, Tuple[str, str]]] = None,
- text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
- background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None,
- font: Optional[Union[tuple, CTkFont]] = None,
- values: Optional[list] = None,
- variable: Union[tkinter.Variable, None] = None,
- dynamic_resizing: bool = True,
- command: Union[Callable[[str], Any], None] = None,
- state: str = "normal"):
- super().__init__(master=master, bg_color=bg_color, width=width, height=height)
- self._sb_fg_color = ThemeManager.theme["CTkSegmentedButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
- self._sb_selected_color = ThemeManager.theme["CTkSegmentedButton"]["selected_color"] if selected_color is None else self._check_color_type(selected_color)
- self._sb_selected_hover_color = ThemeManager.theme["CTkSegmentedButton"]["selected_hover_color"] if selected_hover_color is None else self._check_color_type(selected_hover_color)
- self._sb_unselected_color = ThemeManager.theme["CTkSegmentedButton"]["unselected_color"] if unselected_color is None else self._check_color_type(unselected_color)
- self._sb_unselected_hover_color = ThemeManager.theme["CTkSegmentedButton"]["unselected_hover_color"] if unselected_hover_color is None else self._check_color_type(unselected_hover_color)
- self._sb_text_color = ThemeManager.theme["CTkSegmentedButton"]["text_color"] if text_color is None else self._check_color_type(text_color)
- self._sb_text_color_disabled = ThemeManager.theme["CTkSegmentedButton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
- self._sb_corner_radius = ThemeManager.theme["CTkSegmentedButton"]["corner_radius"] if corner_radius is None else corner_radius
- self._sb_border_width = ThemeManager.theme["CTkSegmentedButton"]["border_width"] if border_width is None else border_width
- self._background_corner_colors = background_corner_colors # rendering options for DrawEngine
- self._command: Callable[[str], None] = command
- self._font = CTkFont() if font is None else font
- self._state = state
- self._buttons_dict: Dict[str, CTkButton] = {} # mapped from value to button object
- if values is None:
- self._value_list: List[str] = ["CTkSegmentedButton"]
- else:
- self._value_list: List[str] = values # Values ordered like buttons rendered on widget
- self._dynamic_resizing = dynamic_resizing
- if not self._dynamic_resizing:
- self.grid_propagate(False)
- self._check_unique_values(self._value_list)
- self._current_value: str = ""
- if len(self._value_list) > 0:
- self._create_buttons_from_values()
- self._create_button_grid()
- self._variable = variable
- self._variable_callback_blocked: bool = False
- self._variable_callback_name: Union[str, None] = None
- if self._variable is not None:
- self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
- self.set(self._variable.get(), from_variable_callback=True)
- super().configure(corner_radius=self._sb_corner_radius, fg_color="transparent")
- def destroy(self):
- if self._variable is not None: # remove old callback
- self._variable.trace_remove("write", self._variable_callback_name)
- super().destroy()
- def _set_dimensions(self, width: int = None, height: int = None):
- super()._set_dimensions(width, height)
- for button in self._buttons_dict.values():
- button.configure(height=height)
- def _variable_callback(self, var_name, index, mode):
- if not self._variable_callback_blocked:
- self.set(self._variable.get(), from_variable_callback=True)
- def _get_index_by_value(self, value: str):
- for index, value_from_list in enumerate(self._value_list):
- if value_from_list == value:
- return index
- raise ValueError(f"CTkSegmentedButton does not contain value '{value}'")
- def _configure_button_corners_for_index(self, index: int):
- if index == 0 and len(self._value_list) == 1:
- if self._background_corner_colors is None:
- self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color))
- else:
- self._buttons_dict[self._value_list[index]].configure(background_corner_colors=self._background_corner_colors)
- elif index == 0:
- if self._background_corner_colors is None:
- self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._sb_fg_color, self._sb_fg_color, self._bg_color))
- else:
- self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._background_corner_colors[0], self._sb_fg_color, self._sb_fg_color, self._background_corner_colors[3]))
- elif index == len(self._value_list) - 1:
- if self._background_corner_colors is None:
- self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._bg_color, self._bg_color, self._sb_fg_color))
- else:
- self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._background_corner_colors[1], self._background_corner_colors[2], self._sb_fg_color))
- else:
- self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._sb_fg_color, self._sb_fg_color, self._sb_fg_color))
- def _unselect_button_by_value(self, value: str):
- if value in self._buttons_dict:
- self._buttons_dict[value].configure(fg_color=self._sb_unselected_color,
- hover_color=self._sb_unselected_hover_color)
- def _select_button_by_value(self, value: str):
- if self._current_value is not None and self._current_value != "":
- self._unselect_button_by_value(self._current_value)
- self._current_value = value
- self._buttons_dict[value].configure(fg_color=self._sb_selected_color,
- hover_color=self._sb_selected_hover_color)
- def _create_button(self, index: int, value: str) -> CTkButton:
- new_button = CTkButton(self,
- width=0,
- height=self._current_height,
- corner_radius=self._sb_corner_radius,
- border_width=self._sb_border_width,
- fg_color=self._sb_unselected_color,
- border_color=self._sb_fg_color,
- hover_color=self._sb_unselected_hover_color,
- text_color=self._sb_text_color,
- text_color_disabled=self._sb_text_color_disabled,
- text=value,
- font=self._font,
- state=self._state,
- command=lambda v=value: self.set(v, from_button_callback=True),
- background_corner_colors=None,
- round_width_to_even_numbers=False,
- round_height_to_even_numbers=False) # DrawEngine rendering option (so that theres no gap between buttons)
- return new_button
- @staticmethod
- def _check_unique_values(values: List[str]):
- """ raises exception if values are not unique """
- if len(values) != len(set(values)):
- raise ValueError("CTkSegmentedButton values are not unique")
- def _create_button_grid(self):
- # remove minsize from every grid cell in the first row
- number_of_columns, _ = self.grid_size()
- for n in range(number_of_columns):
- self.grid_columnconfigure(n, weight=1, minsize=0)
- self.grid_rowconfigure(0, weight=1)
- for index, value in enumerate(self._value_list):
- self.grid_columnconfigure(index, weight=1, minsize=self._current_height)
- self._buttons_dict[value].grid(row=0, column=index, sticky="nsew")
- def _create_buttons_from_values(self):
- assert len(self._buttons_dict) == 0
- assert len(self._value_list) > 0
- for index, value in enumerate(self._value_list):
- self._buttons_dict[value] = self._create_button(index, value)
- self._configure_button_corners_for_index(index)
- def configure(self, **kwargs):
- if "width" in kwargs:
- super().configure(width=kwargs.pop("width"))
- if "height" in kwargs:
- super().configure(height=kwargs.pop("height"))
- if "corner_radius" in kwargs:
- self._sb_corner_radius = kwargs.pop("corner_radius")
- super().configure(corner_radius=self._sb_corner_radius)
- for button in self._buttons_dict.values():
- button.configure(corner_radius=self._sb_corner_radius)
- if "border_width" in kwargs:
- self._sb_border_width = kwargs.pop("border_width")
- for button in self._buttons_dict.values():
- button.configure(border_width=self._sb_border_width)
- if "bg_color" in kwargs:
- super().configure(bg_color=kwargs.pop("bg_color"))
- if len(self._buttons_dict) > 0:
- self._configure_button_corners_for_index(0)
- if len(self._buttons_dict) > 1:
- max_index = len(self._buttons_dict) - 1
- self._configure_button_corners_for_index(max_index)
- if "fg_color" in kwargs:
- self._sb_fg_color = self._check_color_type(kwargs.pop("fg_color"))
- for index, button in enumerate(self._buttons_dict.values()):
- button.configure(border_color=self._sb_fg_color)
- self._configure_button_corners_for_index(index)
- if "selected_color" in kwargs:
- self._sb_selected_color = self._check_color_type(kwargs.pop("selected_color"))
- if self._current_value in self._buttons_dict:
- self._buttons_dict[self._current_value].configure(fg_color=self._sb_selected_color)
- if "selected_hover_color" in kwargs:
- self._sb_selected_hover_color = self._check_color_type(kwargs.pop("selected_hover_color"))
- if self._current_value in self._buttons_dict:
- self._buttons_dict[self._current_value].configure(hover_color=self._sb_selected_hover_color)
- if "unselected_color" in kwargs:
- self._sb_unselected_color = self._check_color_type(kwargs.pop("unselected_color"))
- for value, button in self._buttons_dict.items():
- if value != self._current_value:
- button.configure(fg_color=self._sb_unselected_color)
- if "unselected_hover_color" in kwargs:
- self._sb_unselected_hover_color = self._check_color_type(kwargs.pop("unselected_hover_color"))
- for value, button in self._buttons_dict.items():
- if value != self._current_value:
- button.configure(hover_color=self._sb_unselected_hover_color)
- if "text_color" in kwargs:
- self._sb_text_color = self._check_color_type(kwargs.pop("text_color"))
- for button in self._buttons_dict.values():
- button.configure(text_color=self._sb_text_color)
- if "text_color_disabled" in kwargs:
- self._sb_text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
- for button in self._buttons_dict.values():
- button.configure(text_color_disabled=self._sb_text_color_disabled)
- if "background_corner_colors" in kwargs:
- self._background_corner_colors = kwargs.pop("background_corner_colors")
- for i in range(len(self._buttons_dict)):
- self._configure_button_corners_for_index(i)
- if "font" in kwargs:
- self._font = kwargs.pop("font")
- for button in self._buttons_dict.values():
- button.configure(font=self._font)
- if "values" in kwargs:
- for button in self._buttons_dict.values():
- button.destroy()
- self._buttons_dict.clear()
- self._value_list = kwargs.pop("values")
- self._check_unique_values(self._value_list)
- if len(self._value_list) > 0:
- self._create_buttons_from_values()
- self._create_button_grid()
- if self._current_value in self._value_list:
- self._select_button_by_value(self._current_value)
- if "variable" in kwargs:
- if self._variable is not None: # remove old callback
- self._variable.trace_remove("write", self._variable_callback_name)
- self._variable = kwargs.pop("variable")
- if self._variable is not None and self._variable != "":
- self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
- self.set(self._variable.get(), from_variable_callback=True)
- else:
- self._variable = None
- if "dynamic_resizing" in kwargs:
- self._dynamic_resizing = kwargs.pop("dynamic_resizing")
- if not self._dynamic_resizing:
- self.grid_propagate(False)
- else:
- self.grid_propagate(True)
- if "command" in kwargs:
- self._command = kwargs.pop("command")
- if "state" in kwargs:
- self._state = kwargs.pop("state")
- for button in self._buttons_dict.values():
- button.configure(state=self._state)
- check_kwargs_empty(kwargs, raise_error=True)
- def cget(self, attribute_name: str) -> any:
- if attribute_name == "width":
- return super().cget(attribute_name)
- elif attribute_name == "height":
- return super().cget(attribute_name)
- elif attribute_name == "corner_radius":
- return self._sb_corner_radius
- elif attribute_name == "border_width":
- return self._sb_border_width
- elif attribute_name == "bg_color":
- return super().cget(attribute_name)
- elif attribute_name == "fg_color":
- return self._sb_fg_color
- elif attribute_name == "selected_color":
- return self._sb_selected_color
- elif attribute_name == "selected_hover_color":
- return self._sb_selected_hover_color
- elif attribute_name == "unselected_color":
- return self._sb_unselected_color
- elif attribute_name == "unselected_hover_color":
- return self._sb_unselected_hover_color
- elif attribute_name == "text_color":
- return self._sb_text_color
- elif attribute_name == "text_color_disabled":
- return self._sb_text_color_disabled
- elif attribute_name == "font":
- return self._font
- elif attribute_name == "values":
- return copy.copy(self._value_list)
- elif attribute_name == "variable":
- return self._variable
- elif attribute_name == "dynamic_resizing":
- return self._dynamic_resizing
- elif attribute_name == "command":
- return self._command
- else:
- raise ValueError(f"'{attribute_name}' is not a supported argument. Look at the documentation for supported arguments.")
- def set(self, value: str, from_variable_callback: bool = False, from_button_callback: bool = False):
- if value == self._current_value:
- return
- elif value in self._buttons_dict:
- self._select_button_by_value(value)
- if self._variable is not None and not from_variable_callback:
- self._variable_callback_blocked = True
- self._variable.set(value)
- self._variable_callback_blocked = False
- else:
- if self._current_value in self._buttons_dict:
- self._unselect_button_by_value(self._current_value)
- self._current_value = value
- if self._variable is not None and not from_variable_callback:
- self._variable_callback_blocked = True
- self._variable.set(value)
- self._variable_callback_blocked = False
- if from_button_callback:
- if self._command is not None:
- self._command(self._current_value)
- def get(self) -> str:
- return self._current_value
- def index(self, value: str) -> int:
- return self._value_list.index(value)
- def insert(self, index: int, value: str):
- if value not in self._buttons_dict:
- if value != "":
- self._value_list.insert(index, value)
- self._buttons_dict[value] = self._create_button(index, value)
- self._configure_button_corners_for_index(index)
- if index > 0:
- self._configure_button_corners_for_index(index - 1)
- if index < len(self._buttons_dict) - 1:
- self._configure_button_corners_for_index(index + 1)
- self._create_button_grid()
- if value == self._current_value:
- self._select_button_by_value(self._current_value)
- else:
- raise ValueError(f"CTkSegmentedButton can not insert value ''")
- else:
- raise ValueError(f"CTkSegmentedButton can not insert value '{value}', already part of the values")
- def move(self, new_index: int, value: str):
- if 0 <= new_index < len(self._value_list):
- if value in self._buttons_dict:
- self.delete(value)
- self.insert(new_index, value)
- else:
- raise ValueError(f"CTkSegmentedButton has no value named '{value}'")
- else:
- raise ValueError(f"CTkSegmentedButton new_index {new_index} not in range of value list with len {len(self._value_list)}")
- def delete(self, value: str):
- if value in self._buttons_dict:
- self._buttons_dict[value].destroy()
- self._buttons_dict.pop(value)
- index_to_remove = self._get_index_by_value(value)
- self._value_list.pop(index_to_remove)
- # removed index was outer right element
- if index_to_remove == len(self._buttons_dict) and len(self._buttons_dict) > 0:
- self._configure_button_corners_for_index(index_to_remove - 1)
- # removed index was outer left element
- if index_to_remove == 0 and len(self._buttons_dict) > 0:
- self._configure_button_corners_for_index(0)
- #if index_to_remove <= len(self._buttons_dict) - 1:
- # self._configure_button_corners_for_index(index_to_remove)
- self._create_button_grid()
- else:
- raise ValueError(f"CTkSegmentedButton does not contain value '{value}'")
- def bind(self, sequence=None, command=None, add=None):
- raise NotImplementedError
- def unbind(self, sequence=None, funcid=None):
- raise NotImplementedError
|