]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_group.py
Added typing and missing CP7 functions
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _group.py
1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4
5 """
6 `displayio.group`
7 ================================================================================
8
9 displayio for Blinka
10
11 **Software and Dependencies:**
12
13 * Adafruit Blinka:
14   https://github.com/adafruit/Adafruit_Blinka/releases
15
16 * Author(s): Melissa LeBlanc-Williams
17
18 """
19
20 from __future__ import annotations
21 from typing import Union, Callable
22 from recordclass import recordclass
23 from ._tilegrid import TileGrid
24
25 __version__ = "0.0.0-auto.0"
26 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
27
28
29 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
30
31
32 class Group:
33     """
34     Manage a group of sprites and groups and how they are inter-related.
35
36     Create a Group of a given scale. Scale is in one dimension. For example, scale=2
37     leads to a layer's pixel being 2x2 pixels when in the group.
38     """
39
40     def __init__(self, *, scale: int = 1, x: int = 0, y: int = 0):
41         """
42         :param int scale: Scale of layer pixels in one dimension.
43         :param int x: Initial x position within the parent.
44         :param int y: Initial y position within the parent.
45         """
46
47         if not isinstance(scale, int) or scale < 1:
48             raise ValueError("Scale must be >= 1")
49         self._scale = 1  # Use the setter below to actually set the scale
50         self._group_x = x
51         self._group_y = y
52         self._hidden_group = False
53         self._layers = []
54         self._supported_types = (TileGrid, Group)
55         self.in_group = False
56         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
57         self._set_scale(scale)  # Set the scale via the setter
58
59     def _update_transform(self, parent_transform):
60         """Update the parent transform and child transforms"""
61         self.in_group = parent_transform is not None
62         if self.in_group:
63             x = self._group_x
64             y = self._group_y
65             if parent_transform.transpose_xy:
66                 x, y = y, x
67             self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
68             self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
69             self._absolute_transform.dx = parent_transform.dx * self._scale
70             self._absolute_transform.dy = parent_transform.dy * self._scale
71             self._absolute_transform.transpose_xy = parent_transform.transpose_xy
72             self._absolute_transform.mirror_x = parent_transform.mirror_x
73             self._absolute_transform.mirror_y = parent_transform.mirror_y
74             self._absolute_transform.scale = parent_transform.scale * self._scale
75         self._update_child_transforms()
76
77     def _update_child_transforms(self):
78         # pylint: disable=protected-access
79         if self.in_group:
80             for layer in self._layers:
81                 layer._update_transform(self._absolute_transform)
82         # pylint: enable=protected-access
83
84     def _removal_cleanup(self, index):
85         layer = self._layers[index]
86         layer._update_transform(None)  # pylint: disable=protected-access
87
88     def _layer_update(self, index):
89         # pylint: disable=protected-access
90         layer = self._layers[index]
91         layer._update_transform(self._absolute_transform)
92         # pylint: enable=protected-access
93
94     def append(self, layer: Union[Group, TileGrid]) -> None:
95         """Append a layer to the group. It will be drawn
96         above other layers.
97         """
98         self.insert(len(self._layers), layer)
99
100     def insert(self, index: int, layer: Union[Group, TileGrid]) -> None:
101         """Insert a layer into the group."""
102         if not isinstance(layer, self._supported_types):
103             raise ValueError("Invalid Group Member")
104         if layer.in_group:
105             raise ValueError("Layer already in a group.")
106         self._layers.insert(index, layer)
107         self._layer_update(index)
108
109     def index(self, layer: Union[Group, TileGrid]) -> int:
110         """Returns the index of the first copy of layer.
111         Raises ValueError if not found.
112         """
113         return self._layers.index(layer)
114
115     def pop(self, index=-1) -> Union[Group, TileGrid]:
116         """Remove the ith item and return it."""
117         self._removal_cleanup(index)
118         return self._layers.pop(index)
119
120     def remove(self, layer) -> None:
121         """Remove the first copy of layer. Raises ValueError
122         if it is not present."""
123         index = self.index(layer)
124         self._layers.pop(index)
125
126     def __len__(self) -> int:
127         """Returns the number of layers in a Group"""
128         return len(self._layers)
129
130     def __getitem__(self, index) -> Union[Group, TileGrid]:
131         """Returns the value at the given index."""
132         return self._layers[index]
133
134     def __setitem__(self, index, value) -> None:
135         """Sets the value at the given index."""
136         self._removal_cleanup(index)
137         self._layers[index] = value
138         self._layer_update(index)
139
140     def __delitem__(self, index) -> None:
141         """Deletes the value at the given index."""
142         del self._layers[index]
143
144     def _fill_area(self, buffer):
145         if self._hidden_group:
146             return
147
148         for layer in self._layers:
149             if isinstance(layer, (Group, TileGrid)):
150                 layer._fill_area(buffer)  # pylint: disable=protected-access
151
152     def sort(self, key: Callable, reverse: bool) -> None:
153         """Sort the members of the group."""
154         self._layers.sort(key=key, reverse=reverse)
155
156     @property
157     def hidden(self) -> bool:
158         """True when the Group and all of it’s layers are not visible. When False, the
159         Group’s layers are visible if they haven’t been hidden.
160         """
161         return self._hidden_group
162
163     @hidden.setter
164     def hidden(self, value: bool):
165         if not isinstance(value, (bool, int)):
166             raise ValueError("Expecting a boolean or integer value")
167         self._hidden_group = bool(value)
168
169     @property
170     def scale(self) -> int:
171         """Scales each pixel within the Group in both directions. For example, when
172         scale=2 each pixel will be represented by 2x2 pixels.
173         """
174         return self._scale
175
176     @scale.setter
177     def scale(self, value: int):
178         self._set_scale(value)
179
180     def _set_scale(self, value: int):
181         # This is method allows the scale to be set by this class even when
182         # the scale property is over-ridden by a subclass.
183         if not isinstance(value, int) or value < 1:
184             raise ValueError("Scale must be >= 1")
185         if self._scale != value:
186             parent_scale = self._absolute_transform.scale / self._scale
187             self._absolute_transform.dx = (
188                 self._absolute_transform.dx / self._scale * value
189             )
190             self._absolute_transform.dy = (
191                 self._absolute_transform.dy / self._scale * value
192             )
193             self._absolute_transform.scale = parent_scale * value
194
195             self._scale = value
196             self._update_child_transforms()
197
198     @property
199     def x(self) -> int:
200         """X position of the Group in the parent."""
201         return self._group_x
202
203     @x.setter
204     def x(self, value: int):
205         if not isinstance(value, int):
206             raise ValueError("x must be an integer")
207         if self._group_x != value:
208             if self._absolute_transform.transpose_xy:
209                 dy_value = self._absolute_transform.dy / self._scale
210                 self._absolute_transform.y += dy_value * (value - self._group_x)
211             else:
212                 dx_value = self._absolute_transform.dx / self._scale
213                 self._absolute_transform.x += dx_value * (value - self._group_x)
214             self._group_x = value
215             self._update_child_transforms()
216
217     @property
218     def y(self) -> int:
219         """Y position of the Group in the parent."""
220         return self._group_y
221
222     @y.setter
223     def y(self, value: int):
224         if not isinstance(value, int):
225             raise ValueError("y must be an integer")
226         if self._group_y != value:
227             if self._absolute_transform.transpose_xy:
228                 dx_value = self._absolute_transform.dx / self._scale
229                 self._absolute_transform.x += dx_value * (value - self._group_y)
230             else:
231                 dy_value = self._absolute_transform.dy / self._scale
232                 self._absolute_transform.y += dy_value * (value - self._group_y)
233             self._group_y = value
234             self._update_child_transforms()