ctk_entry.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import tkinter
  2. from typing import Union, Tuple, Optional, Any
  3. from .core_rendering import CTkCanvas
  4. from .theme import ThemeManager
  5. from .core_rendering import DrawEngine
  6. from .core_widget_classes import CTkBaseClass
  7. from .font import CTkFont
  8. from .utility import pop_from_dict_by_set, check_kwargs_empty
  9. class CTkEntry(CTkBaseClass):
  10. """
  11. Entry with rounded corners, border, textvariable support, focus and placeholder.
  12. For detailed information check out the documentation.
  13. """
  14. _minimum_x_padding = 6 # minimum padding between tkinter entry and frame border
  15. # attributes that are passed to and managed by the tkinter entry only:
  16. _valid_tk_entry_attributes = {"exportselection", "insertborderwidth", "insertofftime",
  17. "insertontime", "insertwidth", "justify", "selectborderwidth",
  18. "show", "takefocus", "validate", "validatecommand", "xscrollcommand"}
  19. def __init__(self,
  20. master: Any,
  21. width: int = 140,
  22. height: int = 28,
  23. corner_radius: Optional[int] = None,
  24. border_width: Optional[int] = None,
  25. bg_color: Union[str, Tuple[str, str]] = "transparent",
  26. fg_color: Optional[Union[str, Tuple[str, str]]] = None,
  27. border_color: Optional[Union[str, Tuple[str, str]]] = None,
  28. text_color: Optional[Union[str, Tuple[str, str]]] = None,
  29. placeholder_text_color: Optional[Union[str, Tuple[str, str]]] = None,
  30. textvariable: Union[tkinter.Variable, None] = None,
  31. placeholder_text: Union[str, None] = None,
  32. font: Optional[Union[tuple, CTkFont]] = None,
  33. state: str = tkinter.NORMAL,
  34. **kwargs):
  35. # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
  36. super().__init__(master=master, bg_color=bg_color, width=width, height=height)
  37. # configure grid system (1x1)
  38. self.grid_rowconfigure(0, weight=1)
  39. self.grid_columnconfigure(0, weight=1)
  40. # color
  41. self._fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
  42. self._text_color = ThemeManager.theme["CTkEntry"]["text_color"] if text_color is None else self._check_color_type(text_color)
  43. self._placeholder_text_color = ThemeManager.theme["CTkEntry"]["placeholder_text_color"] if placeholder_text_color is None else self._check_color_type(placeholder_text_color)
  44. self._border_color = ThemeManager.theme["CTkEntry"]["border_color"] if border_color is None else self._check_color_type(border_color)
  45. # shape
  46. self._corner_radius = ThemeManager.theme["CTkEntry"]["corner_radius"] if corner_radius is None else corner_radius
  47. self._border_width = ThemeManager.theme["CTkEntry"]["border_width"] if border_width is None else border_width
  48. # text and state
  49. self._is_focused: bool = True
  50. self._placeholder_text = placeholder_text
  51. self._placeholder_text_active = False
  52. self._pre_placeholder_arguments = {} # some set arguments of the entry will be changed for placeholder and then set back
  53. self._textvariable = textvariable
  54. self._state = state
  55. self._textvariable_callback_name: str = ""
  56. # font
  57. self._font = CTkFont() if font is None else self._check_font_type(font)
  58. if isinstance(self._font, CTkFont):
  59. self._font.add_size_configure_callback(self._update_font)
  60. if not (self._textvariable is None or self._textvariable == ""):
  61. self._textvariable_callback_name = self._textvariable.trace_add("write", self._textvariable_callback)
  62. self._canvas = CTkCanvas(master=self,
  63. highlightthickness=0,
  64. width=self._apply_widget_scaling(self._current_width),
  65. height=self._apply_widget_scaling(self._current_height))
  66. self._draw_engine = DrawEngine(self._canvas)
  67. self._entry = tkinter.Entry(master=self,
  68. bd=0,
  69. width=1,
  70. highlightthickness=0,
  71. font=self._apply_font_scaling(self._font),
  72. state=self._state,
  73. textvariable=self._textvariable,
  74. **pop_from_dict_by_set(kwargs, self._valid_tk_entry_attributes))
  75. check_kwargs_empty(kwargs, raise_error=True)
  76. self._create_grid()
  77. self._activate_placeholder()
  78. self._create_bindings()
  79. self._draw()
  80. def _create_bindings(self, sequence: Optional[str] = None):
  81. """ set necessary bindings for functionality of widget, will overwrite other bindings """
  82. if sequence is None or sequence == "<FocusIn>":
  83. self._entry.bind("<FocusIn>", self._entry_focus_in)
  84. if sequence is None or sequence == "<FocusOut>":
  85. self._entry.bind("<FocusOut>", self._entry_focus_out)
  86. def _create_grid(self):
  87. self._canvas.grid(column=0, row=0, sticky="nswe")
  88. if self._corner_radius >= self._minimum_x_padding:
  89. self._entry.grid(column=0, row=0, sticky="nswe",
  90. padx=min(self._apply_widget_scaling(self._corner_radius), round(self._apply_widget_scaling(self._current_height/2))),
  91. pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width + 1)))
  92. else:
  93. self._entry.grid(column=0, row=0, sticky="nswe",
  94. padx=self._apply_widget_scaling(self._minimum_x_padding),
  95. pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width + 1)))
  96. def _textvariable_callback(self, var_name, index, mode):
  97. if self._textvariable.get() == "":
  98. self._activate_placeholder()
  99. def _set_scaling(self, *args, **kwargs):
  100. super()._set_scaling(*args, **kwargs)
  101. self._entry.configure(font=self._apply_font_scaling(self._font))
  102. self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height))
  103. self._create_grid()
  104. self._draw(no_color_updates=True)
  105. def _set_dimensions(self, width=None, height=None):
  106. super()._set_dimensions(width, height)
  107. self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
  108. height=self._apply_widget_scaling(self._desired_height))
  109. self._draw(no_color_updates=True)
  110. def _update_font(self):
  111. """ pass font to tkinter widgets with applied font scaling and update grid with workaround """
  112. self._entry.configure(font=self._apply_font_scaling(self._font))
  113. # Workaround to force grid to be resized when text changes size.
  114. # Otherwise grid will lag and only resizes if other mouse action occurs.
  115. self._canvas.grid_forget()
  116. self._canvas.grid(column=0, row=0, sticky="nswe")
  117. def destroy(self):
  118. if isinstance(self._font, CTkFont):
  119. self._font.remove_size_configure_callback(self._update_font)
  120. super().destroy()
  121. def _draw(self, no_color_updates=False):
  122. super()._draw(no_color_updates)
  123. requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
  124. self._apply_widget_scaling(self._current_height),
  125. self._apply_widget_scaling(self._corner_radius),
  126. self._apply_widget_scaling(self._border_width))
  127. if requires_recoloring or no_color_updates is False:
  128. self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
  129. if self._apply_appearance_mode(self._fg_color) == "transparent":
  130. self._canvas.itemconfig("inner_parts",
  131. fill=self._apply_appearance_mode(self._bg_color),
  132. outline=self._apply_appearance_mode(self._bg_color))
  133. self._entry.configure(bg=self._apply_appearance_mode(self._bg_color),
  134. disabledbackground=self._apply_appearance_mode(self._bg_color),
  135. readonlybackground=self._apply_appearance_mode(self._bg_color),
  136. highlightcolor=self._apply_appearance_mode(self._bg_color))
  137. else:
  138. self._canvas.itemconfig("inner_parts",
  139. fill=self._apply_appearance_mode(self._fg_color),
  140. outline=self._apply_appearance_mode(self._fg_color))
  141. self._entry.configure(bg=self._apply_appearance_mode(self._fg_color),
  142. disabledbackground=self._apply_appearance_mode(self._fg_color),
  143. readonlybackground=self._apply_appearance_mode(self._fg_color),
  144. highlightcolor=self._apply_appearance_mode(self._fg_color))
  145. self._canvas.itemconfig("border_parts",
  146. fill=self._apply_appearance_mode(self._border_color),
  147. outline=self._apply_appearance_mode(self._border_color))
  148. if self._placeholder_text_active:
  149. self._entry.config(fg=self._apply_appearance_mode(self._placeholder_text_color),
  150. disabledforeground=self._apply_appearance_mode(self._placeholder_text_color),
  151. insertbackground=self._apply_appearance_mode(self._placeholder_text_color))
  152. else:
  153. self._entry.config(fg=self._apply_appearance_mode(self._text_color),
  154. disabledforeground=self._apply_appearance_mode(self._text_color),
  155. insertbackground=self._apply_appearance_mode(self._text_color))
  156. def configure(self, require_redraw=False, **kwargs):
  157. if "state" in kwargs:
  158. self._state = kwargs.pop("state")
  159. self._entry.configure(state=self._state)
  160. if "fg_color" in kwargs:
  161. self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
  162. require_redraw = True
  163. if "text_color" in kwargs:
  164. self._text_color = self._check_color_type(kwargs.pop("text_color"))
  165. require_redraw = True
  166. if "placeholder_text_color" in kwargs:
  167. self._placeholder_text_color = self._check_color_type(kwargs.pop("placeholder_text_color"))
  168. require_redraw = True
  169. if "border_color" in kwargs:
  170. self._border_color = self._check_color_type(kwargs.pop("border_color"))
  171. require_redraw = True
  172. if "border_width" in kwargs:
  173. self._border_width = kwargs.pop("border_width")
  174. self._create_grid()
  175. require_redraw = True
  176. if "corner_radius" in kwargs:
  177. self._corner_radius = kwargs.pop("corner_radius")
  178. self._create_grid()
  179. require_redraw = True
  180. if "placeholder_text" in kwargs:
  181. self._placeholder_text = kwargs.pop("placeholder_text")
  182. if self._placeholder_text_active:
  183. self._entry.delete(0, tkinter.END)
  184. self._entry.insert(0, self._placeholder_text)
  185. else:
  186. self._activate_placeholder()
  187. if "textvariable" in kwargs:
  188. self._textvariable = kwargs.pop("textvariable")
  189. self._entry.configure(textvariable=self._textvariable)
  190. if "font" in kwargs:
  191. if isinstance(self._font, CTkFont):
  192. self._font.remove_size_configure_callback(self._update_font)
  193. self._font = self._check_font_type(kwargs.pop("font"))
  194. if isinstance(self._font, CTkFont):
  195. self._font.add_size_configure_callback(self._update_font)
  196. self._update_font()
  197. if "show" in kwargs:
  198. if self._placeholder_text_active:
  199. self._pre_placeholder_arguments["show"] = kwargs.pop("show") # remember show argument for when placeholder gets deactivated
  200. else:
  201. self._entry.configure(show=kwargs.pop("show"))
  202. self._entry.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_entry_attributes)) # configure Tkinter.Entry
  203. super().configure(require_redraw=require_redraw, **kwargs) # configure CTkBaseClass
  204. def cget(self, attribute_name: str) -> any:
  205. if attribute_name == "corner_radius":
  206. return self._corner_radius
  207. elif attribute_name == "border_width":
  208. return self._border_width
  209. elif attribute_name == "fg_color":
  210. return self._fg_color
  211. elif attribute_name == "border_color":
  212. return self._border_color
  213. elif attribute_name == "text_color":
  214. return self._text_color
  215. elif attribute_name == "placeholder_text_color":
  216. return self._placeholder_text_color
  217. elif attribute_name == "textvariable":
  218. return self._textvariable
  219. elif attribute_name == "placeholder_text":
  220. return self._placeholder_text
  221. elif attribute_name == "font":
  222. return self._font
  223. elif attribute_name == "state":
  224. return self._state
  225. elif attribute_name in self._valid_tk_entry_attributes:
  226. return self._entry.cget(attribute_name) # cget of tkinter.Entry
  227. else:
  228. return super().cget(attribute_name) # cget of CTkBaseClass
  229. def bind(self, sequence=None, command=None, add=True):
  230. """ called on the tkinter.Entry """
  231. if not (add == "+" or add is True):
  232. raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
  233. self._entry.bind(sequence, command, add=True)
  234. def unbind(self, sequence=None, funcid=None):
  235. """ called on the tkinter.Entry """
  236. if funcid is not None:
  237. raise ValueError("'funcid' argument can only be None, because there is a bug in" +
  238. " tkinter and its not clear whether the internal callbacks will be unbinded or not")
  239. self._entry.unbind(sequence, None) # unbind all callbacks for sequence
  240. self._create_bindings(sequence=sequence) # restore internal callbacks for sequence
  241. def _activate_placeholder(self):
  242. if self._entry.get() == "" and self._placeholder_text is not None and (self._textvariable is None or self._textvariable == ""):
  243. self._placeholder_text_active = True
  244. self._pre_placeholder_arguments = {"show": self._entry.cget("show")}
  245. self._entry.config(fg=self._apply_appearance_mode(self._placeholder_text_color),
  246. disabledforeground=self._apply_appearance_mode(self._placeholder_text_color),
  247. show="")
  248. self._entry.delete(0, tkinter.END)
  249. self._entry.insert(0, self._placeholder_text)
  250. def _deactivate_placeholder(self):
  251. if self._placeholder_text_active and self._entry.cget("state") != "readonly":
  252. self._placeholder_text_active = False
  253. self._entry.config(fg=self._apply_appearance_mode(self._text_color),
  254. disabledforeground=self._apply_appearance_mode(self._text_color),)
  255. self._entry.delete(0, tkinter.END)
  256. for argument, value in self._pre_placeholder_arguments.items():
  257. self._entry[argument] = value
  258. def _entry_focus_out(self, event=None):
  259. self._activate_placeholder()
  260. self._is_focused = False
  261. def _entry_focus_in(self, event=None):
  262. self._deactivate_placeholder()
  263. self._is_focused = True
  264. def delete(self, first_index, last_index=None):
  265. self._entry.delete(first_index, last_index)
  266. if not self._is_focused and self._entry.get() == "":
  267. self._activate_placeholder()
  268. def insert(self, index, string):
  269. self._deactivate_placeholder()
  270. return self._entry.insert(index, string)
  271. def get(self):
  272. if self._placeholder_text_active:
  273. return ""
  274. else:
  275. return self._entry.get()
  276. def focus(self):
  277. self._entry.focus()
  278. def focus_set(self):
  279. self._entry.focus_set()
  280. def focus_force(self):
  281. self._entry.focus_force()
  282. def index(self, index):
  283. return self._entry.index(index)
  284. def icursor(self, index):
  285. return self._entry.icursor(index)
  286. def select_adjust(self, index):
  287. return self._entry.select_adjust(index)
  288. def select_from(self, index):
  289. return self._entry.icursor(index)
  290. def select_clear(self):
  291. return self._entry.select_clear()
  292. def select_present(self):
  293. return self._entry.select_present()
  294. def select_range(self, start_index, end_index):
  295. return self._entry.select_range(start_index, end_index)
  296. def select_to(self, index):
  297. return self._entry.select_to(index)
  298. def xview(self, index):
  299. return self._entry.xview(index)
  300. def xview_moveto(self, f):
  301. return self._entry.xview_moveto(f)
  302. def xview_scroll(self, number, what):
  303. return self._entry.xview_scroll(number, what)