ctk_tabview.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. import tkinter
  2. from typing import Union, Tuple, Dict, List, Callable, Optional, Any
  3. from .theme import ThemeManager
  4. from .ctk_frame import CTkFrame
  5. from .core_rendering import CTkCanvas
  6. from .core_rendering import DrawEngine
  7. from .core_widget_classes import CTkBaseClass
  8. from .ctk_segmented_button import CTkSegmentedButton
  9. class CTkTabview(CTkBaseClass):
  10. """
  11. Tabview...
  12. For detailed information check out the documentation.
  13. """
  14. _outer_spacing: int = 10 # px on top or below the button
  15. _outer_button_overhang: int = 8 # px
  16. _button_height: int = 26
  17. _segmented_button_border_width: int = 3
  18. def __init__(self,
  19. master: Any,
  20. width: int = 300,
  21. height: int = 250,
  22. corner_radius: Optional[int] = None,
  23. border_width: Optional[int] = None,
  24. bg_color: Union[str, Tuple[str, str]] = "transparent",
  25. fg_color: Optional[Union[str, Tuple[str, str]]] = None,
  26. border_color: Optional[Union[str, Tuple[str, str]]] = None,
  27. segmented_button_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
  28. segmented_button_selected_color: Optional[Union[str, Tuple[str, str]]] = None,
  29. segmented_button_selected_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
  30. segmented_button_unselected_color: Optional[Union[str, Tuple[str, str]]] = None,
  31. segmented_button_unselected_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
  32. text_color: Optional[Union[str, Tuple[str, str]]] = None,
  33. text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
  34. command: Union[Callable, Any] = None,
  35. anchor: str = "center",
  36. state: str = "normal",
  37. **kwargs):
  38. # transfer some functionality to CTkFrame
  39. super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
  40. # color
  41. self._border_color = ThemeManager.theme["CTkFrame"]["border_color"] if border_color is None else self._check_color_type(border_color)
  42. # determine fg_color of frame
  43. if fg_color is None:
  44. if isinstance(self.master, (CTkFrame, CTkTabview)):
  45. if self.master.cget("fg_color") == ThemeManager.theme["CTkFrame"]["fg_color"]:
  46. self._fg_color = ThemeManager.theme["CTkFrame"]["top_fg_color"]
  47. else:
  48. self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
  49. else:
  50. self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
  51. else:
  52. self._fg_color = self._check_color_type(fg_color, transparency=True)
  53. # shape
  54. self._corner_radius = ThemeManager.theme["CTkFrame"]["corner_radius"] if corner_radius is None else corner_radius
  55. self._border_width = ThemeManager.theme["CTkFrame"]["border_width"] if border_width is None else border_width
  56. self._anchor = anchor
  57. self._canvas = CTkCanvas(master=self,
  58. bg=self._apply_appearance_mode(self._bg_color),
  59. highlightthickness=0,
  60. width=self._apply_widget_scaling(self._desired_width),
  61. height=self._apply_widget_scaling(self._desired_height - self._outer_spacing - self._outer_button_overhang))
  62. self._draw_engine = DrawEngine(self._canvas)
  63. self._segmented_button = CTkSegmentedButton(self,
  64. values=[],
  65. height=self._button_height,
  66. fg_color=segmented_button_fg_color,
  67. selected_color=segmented_button_selected_color,
  68. selected_hover_color=segmented_button_selected_hover_color,
  69. unselected_color=segmented_button_unselected_color,
  70. unselected_hover_color=segmented_button_unselected_hover_color,
  71. text_color=text_color,
  72. text_color_disabled=text_color_disabled,
  73. corner_radius=corner_radius,
  74. border_width=self._segmented_button_border_width,
  75. command=self._segmented_button_callback,
  76. state=state)
  77. self._configure_segmented_button_background_corners()
  78. self._configure_grid()
  79. self._set_grid_canvas()
  80. self._tab_dict: Dict[str, CTkFrame] = {}
  81. self._name_list: List[str] = [] # list of unique tab names in order of tabs
  82. self._current_name: str = ""
  83. self._command = command
  84. self._draw()
  85. def _segmented_button_callback(self, selected_name):
  86. self._tab_dict[self._current_name].grid_forget()
  87. self._current_name = selected_name
  88. self._set_grid_current_tab()
  89. if self._command is not None:
  90. self._command()
  91. def winfo_children(self) -> List[any]:
  92. """
  93. winfo_children of CTkTabview without canvas and segmented button widgets,
  94. because it's not a child but part of the CTkTabview itself
  95. """
  96. child_widgets = super().winfo_children()
  97. try:
  98. child_widgets.remove(self._canvas)
  99. child_widgets.remove(self._segmented_button)
  100. return child_widgets
  101. except ValueError:
  102. return child_widgets
  103. def _set_scaling(self, *args, **kwargs):
  104. super()._set_scaling(*args, **kwargs)
  105. self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
  106. height=self._apply_widget_scaling(self._desired_height - self._outer_spacing - self._outer_button_overhang))
  107. self._configure_grid()
  108. self._draw(no_color_updates=True)
  109. def _set_dimensions(self, width=None, height=None):
  110. super()._set_dimensions(width, height)
  111. self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
  112. height=self._apply_widget_scaling(self._desired_height - self._outer_spacing - self._outer_button_overhang))
  113. self._draw()
  114. def _configure_segmented_button_background_corners(self):
  115. """ needs to be called for changes in fg_color, bg_color """
  116. if self._fg_color == "transparent":
  117. self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color))
  118. else:
  119. if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"):
  120. self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._fg_color, self._fg_color))
  121. else:
  122. self._segmented_button.configure(background_corner_colors=(self._fg_color, self._fg_color, self._bg_color, self._bg_color))
  123. def _configure_grid(self):
  124. """ create 3 x 4 grid system """
  125. if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"):
  126. self.grid_rowconfigure(0, weight=0, minsize=self._apply_widget_scaling(self._outer_spacing))
  127. self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(self._outer_button_overhang))
  128. self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._button_height - self._outer_button_overhang))
  129. self.grid_rowconfigure(3, weight=1)
  130. else:
  131. self.grid_rowconfigure(0, weight=1)
  132. self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(self._button_height - self._outer_button_overhang))
  133. self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._outer_button_overhang))
  134. self.grid_rowconfigure(3, weight=0, minsize=self._apply_widget_scaling(self._outer_spacing))
  135. self.grid_columnconfigure(0, weight=1)
  136. def _set_grid_canvas(self):
  137. if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"):
  138. self._canvas.grid(row=2, rowspan=2, column=0, columnspan=1, sticky="nsew")
  139. else:
  140. self._canvas.grid(row=0, rowspan=2, column=0, columnspan=1, sticky="nsew")
  141. def _set_grid_segmented_button(self):
  142. """ needs to be called for changes in corner_radius, anchor """
  143. if self._anchor.lower() in ("center", "n", "s"):
  144. self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="ns")
  145. elif self._anchor.lower() in ("nw", "w", "sw"):
  146. self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="nsw")
  147. elif self._anchor.lower() in ("ne", "e", "se"):
  148. self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="nse")
  149. def _set_grid_current_tab(self):
  150. """ needs to be called for changes in corner_radius, border_width """
  151. if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"):
  152. self._tab_dict[self._current_name].grid(row=3, column=0, sticky="nsew",
  153. padx=self._apply_widget_scaling(max(self._corner_radius, self._border_width)),
  154. pady=self._apply_widget_scaling(max(self._corner_radius, self._border_width)))
  155. else:
  156. self._tab_dict[self._current_name].grid(row=0, column=0, sticky="nsew",
  157. padx=self._apply_widget_scaling(max(self._corner_radius, self._border_width)),
  158. pady=self._apply_widget_scaling(max(self._corner_radius, self._border_width)))
  159. def _grid_forget_all_tabs(self, exclude_name=None):
  160. for name, frame in self._tab_dict.items():
  161. if name != exclude_name:
  162. frame.grid_forget()
  163. def _create_tab(self) -> CTkFrame:
  164. new_tab = CTkFrame(self,
  165. height=0,
  166. width=0,
  167. border_width=0,
  168. corner_radius=0)
  169. if self._fg_color == "transparent":
  170. new_tab.configure(fg_color=self._apply_appearance_mode(self._bg_color),
  171. bg_color=self._apply_appearance_mode(self._bg_color))
  172. else:
  173. new_tab.configure(fg_color=self._apply_appearance_mode(self._fg_color),
  174. bg_color=self._apply_appearance_mode(self._fg_color))
  175. return new_tab
  176. def _draw(self, no_color_updates: bool = False):
  177. super()._draw(no_color_updates)
  178. if not self._canvas.winfo_exists():
  179. return
  180. requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
  181. self._apply_widget_scaling(self._current_height - self._outer_spacing - self._outer_button_overhang),
  182. self._apply_widget_scaling(self._corner_radius),
  183. self._apply_widget_scaling(self._border_width))
  184. if no_color_updates is False or requires_recoloring:
  185. if self._fg_color == "transparent":
  186. self._canvas.itemconfig("inner_parts",
  187. fill=self._apply_appearance_mode(self._bg_color),
  188. outline=self._apply_appearance_mode(self._bg_color))
  189. for tab in self._tab_dict.values():
  190. tab.configure(fg_color=self._apply_appearance_mode(self._bg_color),
  191. bg_color=self._apply_appearance_mode(self._bg_color))
  192. else:
  193. self._canvas.itemconfig("inner_parts",
  194. fill=self._apply_appearance_mode(self._fg_color),
  195. outline=self._apply_appearance_mode(self._fg_color))
  196. for tab in self._tab_dict.values():
  197. tab.configure(fg_color=self._apply_appearance_mode(self._fg_color),
  198. bg_color=self._apply_appearance_mode(self._fg_color))
  199. self._canvas.itemconfig("border_parts",
  200. fill=self._apply_appearance_mode(self._border_color),
  201. outline=self._apply_appearance_mode(self._border_color))
  202. self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
  203. tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._bg_color)) # configure bg color of tkinter.Frame, cause canvas does not fill frame
  204. def configure(self, require_redraw=False, **kwargs):
  205. if "corner_radius" in kwargs:
  206. self._corner_radius = kwargs.pop("corner_radius")
  207. self._set_grid_segmented_button()
  208. self._set_grid_current_tab()
  209. self._set_grid_canvas()
  210. self._configure_segmented_button_background_corners()
  211. self._segmented_button.configure(corner_radius=self._corner_radius)
  212. if "border_width" in kwargs:
  213. self._border_width = kwargs.pop("border_width")
  214. require_redraw = True
  215. if "fg_color" in kwargs:
  216. self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
  217. self._configure_segmented_button_background_corners()
  218. require_redraw = True
  219. if "border_color" in kwargs:
  220. self._border_color = self._check_color_type(kwargs.pop("border_color"))
  221. require_redraw = True
  222. if "segmented_button_fg_color" in kwargs:
  223. self._segmented_button.configure(fg_color=kwargs.pop("segmented_button_fg_color"))
  224. if "segmented_button_selected_color" in kwargs:
  225. self._segmented_button.configure(selected_color=kwargs.pop("segmented_button_selected_color"))
  226. if "segmented_button_selected_hover_color" in kwargs:
  227. self._segmented_button.configure(selected_hover_color=kwargs.pop("segmented_button_selected_hover_color"))
  228. if "segmented_button_unselected_color" in kwargs:
  229. self._segmented_button.configure(unselected_color=kwargs.pop("segmented_button_unselected_color"))
  230. if "segmented_button_unselected_hover_color" in kwargs:
  231. self._segmented_button.configure(unselected_hover_color=kwargs.pop("segmented_button_unselected_hover_color"))
  232. if "text_color" in kwargs:
  233. self._segmented_button.configure(text_color=kwargs.pop("text_color"))
  234. if "text_color_disabled" in kwargs:
  235. self._segmented_button.configure(text_color_disabled=kwargs.pop("text_color_disabled"))
  236. if "command" in kwargs:
  237. self._command = kwargs.pop("command")
  238. if "anchor" in kwargs:
  239. self._anchor = kwargs.pop("anchor")
  240. self._configure_grid()
  241. self._set_grid_segmented_button()
  242. if "state" in kwargs:
  243. self._segmented_button.configure(state=kwargs.pop("state"))
  244. super().configure(require_redraw=require_redraw, **kwargs)
  245. def cget(self, attribute_name: str):
  246. if attribute_name == "corner_radius":
  247. return self._corner_radius
  248. elif attribute_name == "border_width":
  249. return self._border_width
  250. elif attribute_name == "fg_color":
  251. return self._fg_color
  252. elif attribute_name == "border_color":
  253. return self._border_color
  254. elif attribute_name == "segmented_button_fg_color":
  255. return self._segmented_button.cget(attribute_name)
  256. elif attribute_name == "segmented_button_selected_color":
  257. return self._segmented_button.cget(attribute_name)
  258. elif attribute_name == "segmented_button_selected_hover_color":
  259. return self._segmented_button.cget(attribute_name)
  260. elif attribute_name == "segmented_button_unselected_color":
  261. return self._segmented_button.cget(attribute_name)
  262. elif attribute_name == "segmented_button_unselected_hover_color":
  263. return self._segmented_button.cget(attribute_name)
  264. elif attribute_name == "text_color":
  265. return self._segmented_button.cget(attribute_name)
  266. elif attribute_name == "text_color_disabled":
  267. return self._segmented_button.cget(attribute_name)
  268. elif attribute_name == "command":
  269. return self._command
  270. elif attribute_name == "anchor":
  271. return self._anchor
  272. elif attribute_name == "state":
  273. return self._segmented_button.cget(attribute_name)
  274. else:
  275. return super().cget(attribute_name)
  276. def tab(self, name: str) -> CTkFrame:
  277. """ returns reference to the tab with given name """
  278. if name in self._tab_dict:
  279. return self._tab_dict[name]
  280. else:
  281. raise ValueError(f"CTkTabview has no tab named '{name}'")
  282. def insert(self, index: int, name: str) -> CTkFrame:
  283. """ creates new tab with given name at position index """
  284. if name not in self._tab_dict:
  285. # if no tab exists, set grid for segmented button
  286. if len(self._tab_dict) == 0:
  287. self._set_grid_segmented_button()
  288. self._name_list.append(name)
  289. self._tab_dict[name] = self._create_tab()
  290. self._segmented_button.insert(index, name)
  291. # if created tab is only tab select this tab
  292. if len(self._tab_dict) == 1:
  293. self._current_name = name
  294. self._segmented_button.set(self._current_name)
  295. self._grid_forget_all_tabs()
  296. self._set_grid_current_tab()
  297. return self._tab_dict[name]
  298. else:
  299. raise ValueError(f"CTkTabview already has tab named '{name}'")
  300. def add(self, name: str) -> CTkFrame:
  301. """ appends new tab with given name """
  302. return self.insert(len(self._tab_dict), name)
  303. def index(self, name) -> int:
  304. """ get index of tab with given name """
  305. return self._segmented_button.index(name)
  306. def move(self, new_index: int, name: str):
  307. if 0 <= new_index < len(self._name_list):
  308. if name in self._tab_dict:
  309. self._segmented_button.move(new_index, name)
  310. else:
  311. raise ValueError(f"CTkTabview has no name '{name}'")
  312. else:
  313. raise ValueError(f"CTkTabview new_index {new_index} not in range of name list with len {len(self._name_list)}")
  314. def rename(self, old_name: str, new_name: str):
  315. if new_name in self._name_list:
  316. raise ValueError(f"new_name '{new_name}' already exists")
  317. # segmented button
  318. old_index = self._segmented_button.index(old_name)
  319. self._segmented_button.delete(old_name)
  320. self._segmented_button.insert(old_index, new_name)
  321. # name list
  322. self._name_list.remove(old_name)
  323. self._name_list.append(new_name)
  324. # tab dictionary
  325. self._tab_dict[new_name] = self._tab_dict.pop(old_name)
  326. def delete(self, name: str):
  327. """ delete tab by name """
  328. if name in self._tab_dict:
  329. self._name_list.remove(name)
  330. self._tab_dict[name].grid_forget()
  331. self._tab_dict.pop(name)
  332. self._segmented_button.delete(name)
  333. # set current_name to '' and remove segmented button if no tab is left
  334. if len(self._name_list) == 0:
  335. self._current_name = ""
  336. self._segmented_button.grid_forget()
  337. # if only one tab left, select this tab
  338. elif len(self._name_list) == 1:
  339. self._current_name = self._name_list[0]
  340. self._segmented_button.set(self._current_name)
  341. self._grid_forget_all_tabs()
  342. self._set_grid_current_tab()
  343. # more tabs are left
  344. else:
  345. # if current_name is deleted tab, select first tab at position 0
  346. if self._current_name == name:
  347. self.set(self._name_list[0])
  348. else:
  349. raise ValueError(f"CTkTabview has no tab named '{name}'")
  350. def set(self, name: str):
  351. """ select tab by name """
  352. if name in self._tab_dict:
  353. self._current_name = name
  354. self._segmented_button.set(name)
  355. self._set_grid_current_tab()
  356. self.after(100, lambda: self._grid_forget_all_tabs(exclude_name=name))
  357. else:
  358. raise ValueError(f"CTkTabview has no tab named '{name}'")
  359. def get(self) -> str:
  360. """ returns name of selected tab, returns empty string if no tab selected """
  361. return self._current_name