scaling_base_class.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. from typing import Union, Tuple
  2. import copy
  3. import re
  4. try:
  5. from typing import Literal
  6. except ImportError:
  7. from typing_extensions import Literal
  8. from .scaling_tracker import ScalingTracker
  9. from ..font import CTkFont
  10. class CTkScalingBaseClass:
  11. """
  12. Super-class that manages the scaling values and callbacks.
  13. Works for widgets and windows, type must be set in init method with
  14. scaling_type attribute. Methods:
  15. - _set_scaling() abstractmethod, gets called when scaling changes, must be overridden
  16. - destroy() must be called when sub-class is destroyed
  17. - _apply_widget_scaling()
  18. - _reverse_widget_scaling()
  19. - _apply_window_scaling()
  20. - _reverse_window_scaling()
  21. - _apply_font_scaling()
  22. - _apply_argument_scaling()
  23. - _apply_geometry_scaling()
  24. - _reverse_geometry_scaling()
  25. - _parse_geometry_string()
  26. """
  27. def __init__(self, scaling_type: Literal["widget", "window"] = "widget"):
  28. self.__scaling_type = scaling_type
  29. if self.__scaling_type == "widget":
  30. ScalingTracker.add_widget(self._set_scaling, self) # add callback for automatic scaling changes
  31. self.__widget_scaling = ScalingTracker.get_widget_scaling(self)
  32. elif self.__scaling_type == "window":
  33. ScalingTracker.activate_high_dpi_awareness() # make process DPI aware
  34. ScalingTracker.add_window(self._set_scaling, self) # add callback for automatic scaling changes
  35. self.__window_scaling = ScalingTracker.get_window_scaling(self)
  36. def destroy(self):
  37. if self.__scaling_type == "widget":
  38. ScalingTracker.remove_widget(self._set_scaling, self)
  39. elif self.__scaling_type == "window":
  40. ScalingTracker.remove_window(self._set_scaling, self)
  41. def _set_scaling(self, new_widget_scaling, new_window_scaling):
  42. """ can be overridden, but super method must be called at the beginning """
  43. self.__widget_scaling = new_widget_scaling
  44. self.__window_scaling = new_window_scaling
  45. def _get_widget_scaling(self) -> float:
  46. return self.__widget_scaling
  47. def _get_window_scaling(self) -> float:
  48. return self.__window_scaling
  49. def _apply_widget_scaling(self, value: Union[int, float]) -> Union[float]:
  50. assert self.__scaling_type == "widget"
  51. return value * self.__widget_scaling
  52. def _reverse_widget_scaling(self, value: Union[int, float]) -> Union[float]:
  53. assert self.__scaling_type == "widget"
  54. return value / self.__widget_scaling
  55. def _apply_window_scaling(self, value: Union[int, float]) -> int:
  56. assert self.__scaling_type == "window"
  57. return int(value * self.__window_scaling)
  58. def _reverse_window_scaling(self, scaled_value: Union[int, float]) -> int:
  59. assert self.__scaling_type == "window"
  60. return int(scaled_value / self.__window_scaling)
  61. def _apply_font_scaling(self, font: Union[Tuple, CTkFont]) -> tuple:
  62. """ Takes CTkFont object and returns tuple font with scaled size, has to be called again for every change of font object """
  63. assert self.__scaling_type == "widget"
  64. if type(font) == tuple:
  65. if len(font) == 1:
  66. return font
  67. elif len(font) == 2:
  68. return font[0], -abs(round(font[1] * self.__widget_scaling))
  69. elif 3 <= len(font) <= 6:
  70. return font[0], -abs(round(font[1] * self.__widget_scaling)), font[2:]
  71. else:
  72. raise ValueError(f"Can not scale font {font}. font needs to be tuple of len 1, 2 or 3")
  73. elif isinstance(font, CTkFont):
  74. return font.create_scaled_tuple(self.__widget_scaling)
  75. else:
  76. raise ValueError(f"Can not scale font '{font}' of type {type(font)}. font needs to be tuple or instance of CTkFont")
  77. def _apply_argument_scaling(self, kwargs: dict) -> dict:
  78. assert self.__scaling_type == "widget"
  79. scaled_kwargs = copy.copy(kwargs)
  80. # scale padding values
  81. if "pady" in scaled_kwargs:
  82. if isinstance(scaled_kwargs["pady"], (int, float)):
  83. scaled_kwargs["pady"] = self._apply_widget_scaling(scaled_kwargs["pady"])
  84. elif isinstance(scaled_kwargs["pady"], tuple):
  85. scaled_kwargs["pady"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["pady"]])
  86. if "padx" in kwargs:
  87. if isinstance(scaled_kwargs["padx"], (int, float)):
  88. scaled_kwargs["padx"] = self._apply_widget_scaling(scaled_kwargs["padx"])
  89. elif isinstance(scaled_kwargs["padx"], tuple):
  90. scaled_kwargs["padx"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["padx"]])
  91. # scaled x, y values for place geometry manager
  92. if "x" in scaled_kwargs:
  93. scaled_kwargs["x"] = self._apply_widget_scaling(scaled_kwargs["x"])
  94. if "y" in scaled_kwargs:
  95. scaled_kwargs["y"] = self._apply_widget_scaling(scaled_kwargs["y"])
  96. return scaled_kwargs
  97. @staticmethod
  98. def _parse_geometry_string(geometry_string: str) -> tuple:
  99. # index: 1 2 3 4 5 6
  100. # regex group structure: ('<width>x<height>', '<width>', '<height>', '+-<x>+-<y>', '-<x>', '-<y>')
  101. result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
  102. width = int(result.group(2)) if result.group(2) is not None else None
  103. height = int(result.group(3)) if result.group(3) is not None else None
  104. x = int(result.group(5)) if result.group(5) is not None else None
  105. y = int(result.group(6)) if result.group(6) is not None else None
  106. return width, height, x, y
  107. def _apply_geometry_scaling(self, geometry_string: str) -> str:
  108. assert self.__scaling_type == "window"
  109. width, height, x, y = self._parse_geometry_string(geometry_string)
  110. if x is None and y is None: # no <x> and <y> in geometry_string
  111. return f"{round(width * self.__window_scaling)}x{round(height * self.__window_scaling)}"
  112. elif width is None and height is None: # no <width> and <height> in geometry_string
  113. return f"+{x}+{y}"
  114. else:
  115. return f"{round(width * self.__window_scaling)}x{round(height * self.__window_scaling)}+{x}+{y}"
  116. def _reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
  117. assert self.__scaling_type == "window"
  118. width, height, x, y = self._parse_geometry_string(scaled_geometry_string)
  119. if x is None and y is None: # no <x> and <y> in geometry_string
  120. return f"{round(width / self.__window_scaling)}x{round(height / self.__window_scaling)}"
  121. elif width is None and height is None: # no <width> and <height> in geometry_string
  122. return f"+{x}+{y}"
  123. else:
  124. return f"{round(width / self.__window_scaling)}x{round(height / self.__window_scaling)}+{x}+{y}"