dropdown_menu.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import tkinter
  2. import sys
  3. from typing import Union, Tuple, Callable, List, Optional
  4. from ..theme import ThemeManager
  5. from ..font import CTkFont
  6. from ..appearance_mode import CTkAppearanceModeBaseClass
  7. from ..scaling import CTkScalingBaseClass
  8. class DropdownMenu(tkinter.Menu, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
  9. def __init__(self, *args,
  10. min_character_width: int = 18,
  11. fg_color: Optional[Union[str, Tuple[str, str]]] = None,
  12. hover_color: Optional[Union[str, Tuple[str, str]]] = None,
  13. text_color: Optional[Union[str, Tuple[str, str]]] = None,
  14. font: Optional[Union[tuple, CTkFont]] = None,
  15. command: Union[Callable, None] = None,
  16. values: Optional[List[str]] = None,
  17. **kwargs):
  18. # call init methods of super classes
  19. tkinter.Menu.__init__(self, *args, **kwargs)
  20. CTkAppearanceModeBaseClass.__init__(self)
  21. CTkScalingBaseClass.__init__(self, scaling_type="widget")
  22. self._min_character_width = min_character_width
  23. self._fg_color = ThemeManager.theme["DropdownMenu"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
  24. self._hover_color = ThemeManager.theme["DropdownMenu"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
  25. self._text_color = ThemeManager.theme["DropdownMenu"]["text_color"] if text_color is None else self._check_color_type(text_color)
  26. # font
  27. self._font = CTkFont() if font is None else self._check_font_type(font)
  28. if isinstance(self._font, CTkFont):
  29. self._font.add_size_configure_callback(self._update_font)
  30. self._configure_menu_for_platforms()
  31. self._values = values
  32. self._command = command
  33. self._add_menu_commands()
  34. def destroy(self):
  35. if isinstance(self._font, CTkFont):
  36. self._font.remove_size_configure_callback(self._update_font)
  37. # call destroy methods of super classes
  38. tkinter.Menu.destroy(self)
  39. CTkAppearanceModeBaseClass.destroy(self)
  40. def _update_font(self):
  41. """ pass font to tkinter widgets with applied font scaling """
  42. super().configure(font=self._apply_font_scaling(self._font))
  43. def _configure_menu_for_platforms(self):
  44. """ apply platform specific appearance attributes, configure all colors """
  45. if sys.platform == "darwin":
  46. super().configure(tearoff=False,
  47. font=self._apply_font_scaling(self._font))
  48. elif sys.platform.startswith("win"):
  49. super().configure(tearoff=False,
  50. relief="flat",
  51. activebackground=self._apply_appearance_mode(self._hover_color),
  52. borderwidth=self._apply_widget_scaling(4),
  53. activeborderwidth=self._apply_widget_scaling(4),
  54. bg=self._apply_appearance_mode(self._fg_color),
  55. fg=self._apply_appearance_mode(self._text_color),
  56. activeforeground=self._apply_appearance_mode(self._text_color),
  57. font=self._apply_font_scaling(self._font),
  58. cursor="hand2")
  59. else:
  60. super().configure(tearoff=False,
  61. relief="flat",
  62. activebackground=self._apply_appearance_mode(self._hover_color),
  63. borderwidth=0,
  64. activeborderwidth=0,
  65. bg=self._apply_appearance_mode(self._fg_color),
  66. fg=self._apply_appearance_mode(self._text_color),
  67. activeforeground=self._apply_appearance_mode(self._text_color),
  68. font=self._apply_font_scaling(self._font))
  69. def _add_menu_commands(self):
  70. """ delete existing menu labels and createe new labels with command according to values list """
  71. self.delete(0, "end") # delete all old commands
  72. if sys.platform.startswith("linux"):
  73. for value in self._values:
  74. self.add_command(label=" " + value.ljust(self._min_character_width) + " ",
  75. command=lambda v=value: self._button_callback(v),
  76. compound="left")
  77. else:
  78. for value in self._values:
  79. self.add_command(label=value.ljust(self._min_character_width),
  80. command=lambda v=value: self._button_callback(v),
  81. compound="left")
  82. def _button_callback(self, value):
  83. if self._command is not None:
  84. self._command(value)
  85. def open(self, x: Union[int, float], y: Union[int, float]):
  86. if sys.platform == "darwin":
  87. y += self._apply_widget_scaling(8)
  88. else:
  89. y += self._apply_widget_scaling(3)
  90. if sys.platform == "darwin" or sys.platform.startswith("win"):
  91. self.post(int(x), int(y))
  92. else: # Linux
  93. self.tk_popup(int(x), int(y))
  94. def configure(self, **kwargs):
  95. if "fg_color" in kwargs:
  96. self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
  97. super().configure(bg=self._apply_appearance_mode(self._fg_color))
  98. if "hover_color" in kwargs:
  99. self._hover_color = self._check_color_type(kwargs.pop("hover_color"))
  100. super().configure(activebackground=self._apply_appearance_mode(self._hover_color))
  101. if "text_color" in kwargs:
  102. self._text_color = self._check_color_type(kwargs.pop("text_color"))
  103. super().configure(fg=self._apply_appearance_mode(self._text_color))
  104. if "font" in kwargs:
  105. if isinstance(self._font, CTkFont):
  106. self._font.remove_size_configure_callback(self._update_font)
  107. self._font = self._check_font_type(kwargs.pop("font"))
  108. if isinstance(self._font, CTkFont):
  109. self._font.add_size_configure_callback(self._update_font)
  110. self._update_font()
  111. if "command" in kwargs:
  112. self._command = kwargs.pop("command")
  113. if "values" in kwargs:
  114. self._values = kwargs.pop("values")
  115. self._add_menu_commands()
  116. super().configure(**kwargs)
  117. def cget(self, attribute_name: str) -> any:
  118. if attribute_name == "min_character_width":
  119. return self._min_character_width
  120. elif attribute_name == "fg_color":
  121. return self._fg_color
  122. elif attribute_name == "hover_color":
  123. return self._hover_color
  124. elif attribute_name == "text_color":
  125. return self._text_color
  126. elif attribute_name == "font":
  127. return self._font
  128. elif attribute_name == "command":
  129. return self._command
  130. elif attribute_name == "values":
  131. return self._values
  132. else:
  133. return super().cget(attribute_name)
  134. @staticmethod
  135. def _check_font_type(font: any):
  136. if isinstance(font, CTkFont):
  137. return font
  138. elif type(font) == tuple and len(font) == 1:
  139. sys.stderr.write(f"Warning: font {font} given without size, will be extended with default text size of current theme\n")
  140. return font[0], ThemeManager.theme["text"]["size"]
  141. elif type(font) == tuple and 2 <= len(font) <= 3:
  142. return font
  143. else:
  144. raise ValueError(f"Wrong font type {type(font)} for font '{font}'\n" +
  145. f"For consistency, Customtkinter requires the font argument to be a tuple of len 2 or 3 or an instance of CTkFont.\n" +
  146. f"\nUsage example:\n" +
  147. f"font=customtkinter.CTkFont(family='<name>', size=<size in px>)\n" +
  148. f"font=('<name>', <size in px>)\n")
  149. def _set_scaling(self, new_widget_scaling, new_window_scaling):
  150. super()._set_scaling(new_widget_scaling, new_window_scaling)
  151. self._configure_menu_for_platforms()
  152. def _set_appearance_mode(self, mode_string):
  153. """ colors won't update on appearance mode change when dropdown is open, because it's not necessary """
  154. super()._set_appearance_mode(mode_string)
  155. self._configure_menu_for_platforms()