import tkinter import copy from typing import Union, Tuple, List, Dict, Callable, Optional 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], None], 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 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